Add MIT license and remove the one-off content migration scripts
This commit is contained in:
@@ -1,284 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
One-shot converter: old docudjeex (Docus v3 / @nuxt-themes/docus MDC) markdown
|
|
||||||
-> new docudjeex-v2-docusv4 (Docus v4 / Nuxt UI MDC) markdown.
|
|
||||||
|
|
||||||
Docus v4 kept the same MDC block-component *syntax* (::name{props} ... ::)
|
|
||||||
but swapped the component *vocabulary* to Nuxt UI components:
|
|
||||||
::alert{type=info} -> ::note
|
|
||||||
::alert{type=success} ("Tip:") -> ::tip
|
|
||||||
::alert{type=warning} -> ::warning
|
|
||||||
::alert{type=danger} -> ::caution
|
|
||||||
:::list{type=} (nested in alert) -> unwrapped, passthrough
|
|
||||||
::card{icon=} #title #description -> ::card{icon="i-x" title="T"} description ::
|
|
||||||
::card-grid ... #title #default -> "### Title" + ::card-group ... ::
|
|
||||||
::terminal (yaml content: list) -> ```console fenced block
|
|
||||||
::code-group -> unchanged (same syntax in both versions)
|
|
||||||
:ellipsis{...} -> dropped (Docus v3-only decoration)
|
|
||||||
[text]{style="..."} -> unchanged (generic MDC inline syntax)
|
|
||||||
icon `prefix:name` -> `i-prefix-name` (Nuxt Icon: full Iconify catalog)
|
|
||||||
|
|
||||||
Usage: convert-content.py <src.md> <dest.md>
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
FENCE_OPEN = re.compile(r'^(\s*)(:{2,})([\w-]+)(\{(.*)\})?\s*$')
|
|
||||||
FENCE_CLOSE = re.compile(r'^\s*(:{2,})\s*$')
|
|
||||||
|
|
||||||
|
|
||||||
def convert_icon(raw):
|
|
||||||
raw = raw.strip()
|
|
||||||
if ':' not in raw:
|
|
||||||
return raw
|
|
||||||
prefix, name = raw.split(':', 1)
|
|
||||||
return f"i-{prefix}-{name}"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_props(props_str):
|
|
||||||
props = {}
|
|
||||||
if not props_str:
|
|
||||||
return props
|
|
||||||
for m in re.finditer(r'([\w-]+)=("([^"]*)"|(\S+))', props_str):
|
|
||||||
key = m.group(1)
|
|
||||||
val = m.group(3) if m.group(3) is not None else m.group(4)
|
|
||||||
props[key] = val
|
|
||||||
return props
|
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
|
||||||
def __init__(self, kind, name=None, props=None):
|
|
||||||
self.kind = kind
|
|
||||||
self.name = name
|
|
||||||
self.props = props or {}
|
|
||||||
self.children = []
|
|
||||||
|
|
||||||
|
|
||||||
def parse_blocks(lines):
|
|
||||||
root = Node('component', name='__root__')
|
|
||||||
stack = [root]
|
|
||||||
text_buf = []
|
|
||||||
|
|
||||||
def flush_text():
|
|
||||||
if text_buf:
|
|
||||||
n = Node('text')
|
|
||||||
n.children = text_buf[:]
|
|
||||||
stack[-1].children.append(n)
|
|
||||||
text_buf.clear()
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
close_m = FENCE_CLOSE.match(line)
|
|
||||||
open_m = FENCE_OPEN.match(line)
|
|
||||||
if close_m and len(stack) > 1:
|
|
||||||
flush_text()
|
|
||||||
stack.pop()
|
|
||||||
continue
|
|
||||||
if open_m:
|
|
||||||
flush_text()
|
|
||||||
name = open_m.group(3)
|
|
||||||
props = parse_props(open_m.group(5))
|
|
||||||
node = Node('component', name=name, props=props)
|
|
||||||
stack[-1].children.append(node)
|
|
||||||
stack.append(node)
|
|
||||||
continue
|
|
||||||
text_buf.append(line)
|
|
||||||
flush_text()
|
|
||||||
return root
|
|
||||||
|
|
||||||
|
|
||||||
def indent(text, n=2):
|
|
||||||
pad = ' ' * n
|
|
||||||
return '\n'.join(pad + line if line.strip() else line for line in text.split('\n'))
|
|
||||||
|
|
||||||
|
|
||||||
def render_children(node):
|
|
||||||
return '\n'.join(render_node(c) for c in node.children)
|
|
||||||
|
|
||||||
|
|
||||||
def split_slots(node):
|
|
||||||
slots = {'__default__': []}
|
|
||||||
current = '__default__'
|
|
||||||
for c in node.children:
|
|
||||||
if c.kind == 'text':
|
|
||||||
for line in c.children:
|
|
||||||
m = re.match(r'^\s*#(\w+)\s*$', line)
|
|
||||||
if m:
|
|
||||||
current = m.group(1)
|
|
||||||
slots.setdefault(current, [])
|
|
||||||
else:
|
|
||||||
slots[current].append(line)
|
|
||||||
else:
|
|
||||||
slots.setdefault(current, [])
|
|
||||||
slots[current].append(c)
|
|
||||||
return slots
|
|
||||||
|
|
||||||
|
|
||||||
def render_slot(slot_items):
|
|
||||||
parts, buf = [], []
|
|
||||||
for item in slot_items:
|
|
||||||
if isinstance(item, str):
|
|
||||||
buf.append(item)
|
|
||||||
else:
|
|
||||||
if buf:
|
|
||||||
parts.append('\n'.join(buf))
|
|
||||||
buf = []
|
|
||||||
parts.append(render_node(item))
|
|
||||||
if buf:
|
|
||||||
parts.append('\n'.join(buf))
|
|
||||||
return '\n'.join(parts).strip('\n')
|
|
||||||
|
|
||||||
|
|
||||||
ALERT_TYPE_MAP = {
|
|
||||||
'info': 'note',
|
|
||||||
'success': 'tip', # Docus's "success" was always used for "✨ Tip:" callouts
|
|
||||||
'warning': 'warning',
|
|
||||||
'danger': 'caution',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def render_alert(node):
|
|
||||||
atype = node.props.get('type', 'info')
|
|
||||||
new_name = ALERT_TYPE_MAP.get(atype, 'note')
|
|
||||||
body = render_children(node).strip('\n')
|
|
||||||
return f"::{new_name}\n{body}\n::"
|
|
||||||
|
|
||||||
|
|
||||||
def render_list(node):
|
|
||||||
return render_children(node).strip('\n')
|
|
||||||
|
|
||||||
|
|
||||||
def render_card(node):
|
|
||||||
slots = split_slots(node)
|
|
||||||
icon = convert_icon(node.props.get('icon', ''))
|
|
||||||
title = render_slot(slots.get('title', [])).strip()
|
|
||||||
title = re.sub(r'^__(.*)__$', r'\1', title)
|
|
||||||
desc = render_slot(slots.get('description', [])).strip()
|
|
||||||
props = []
|
|
||||||
if icon:
|
|
||||||
props.append(f'icon="{icon}"')
|
|
||||||
if title:
|
|
||||||
title_escaped = title.replace('"', '\\"')
|
|
||||||
props.append(f'title="{title_escaped}"')
|
|
||||||
props_str = '{' + ' '.join(props) + '}' if props else ''
|
|
||||||
return f"::card{props_str}\n{desc}\n::"
|
|
||||||
|
|
||||||
|
|
||||||
def render_card_grid(node):
|
|
||||||
slots = split_slots(node)
|
|
||||||
title = render_slot(slots.get('title', [])).strip()
|
|
||||||
default_items = slots.get('default', [])
|
|
||||||
cards = [render_node(c) for c in default_items if not isinstance(c, str)]
|
|
||||||
out = []
|
|
||||||
if title:
|
|
||||||
out.append(f"### {title}")
|
|
||||||
out.append("")
|
|
||||||
out.append("::card-group")
|
|
||||||
out.append('\n\n'.join(cards))
|
|
||||||
out.append("::")
|
|
||||||
return '\n'.join(out)
|
|
||||||
|
|
||||||
|
|
||||||
def render_terminal(node):
|
|
||||||
text = render_children(node)
|
|
||||||
m = re.search(r'content:\s*\n((?:\s*-.*\n?)+)', text)
|
|
||||||
lines_out = []
|
|
||||||
if m:
|
|
||||||
for item in re.finditer(r'^\s*-\s*(.*)$', m.group(1), re.M):
|
|
||||||
lines_out.append(item.group(1))
|
|
||||||
return "```console\n" + '\n'.join(lines_out) + "\n```"
|
|
||||||
|
|
||||||
|
|
||||||
def render_code_group(node):
|
|
||||||
# Same syntax in both versions: reconstruct verbatim.
|
|
||||||
body = render_children(node).strip('\n')
|
|
||||||
return f"::code-group\n{body}\n::"
|
|
||||||
|
|
||||||
|
|
||||||
RENDERERS = {
|
|
||||||
'alert': render_alert,
|
|
||||||
'list': render_list,
|
|
||||||
'card': render_card,
|
|
||||||
'card-grid': render_card_grid,
|
|
||||||
'terminal': render_terminal,
|
|
||||||
'code-group': render_code_group,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def render_node(node):
|
|
||||||
if node.kind == 'text':
|
|
||||||
return '\n'.join(node.children)
|
|
||||||
if node.name == '__root__':
|
|
||||||
return render_children(node)
|
|
||||||
fn = RENDERERS.get(node.name)
|
|
||||||
if fn is None:
|
|
||||||
inner = render_children(node)
|
|
||||||
return f"<!-- UNCONVERTED ::{node.name} -->\n{inner}\n<!-- /UNCONVERTED -->"
|
|
||||||
return fn(node)
|
|
||||||
|
|
||||||
|
|
||||||
def strip_ellipsis(text):
|
|
||||||
return re.sub(r'^\s*:ellipsis\{[^}]*\}\s*\n?', '', text, flags=re.M)
|
|
||||||
|
|
||||||
|
|
||||||
LIST_START_RE = re.compile(r'^\s*([-*]\s|\d+\.\s)')
|
|
||||||
|
|
||||||
|
|
||||||
def fix_list_starts(text):
|
|
||||||
"""Nuxt Content's remark-based parser is generally lenient, but keep this
|
|
||||||
guard (proved necessary for the Zensical/Python-Markdown port) in case a
|
|
||||||
list-directly-after-text pattern needs it here too."""
|
|
||||||
lines = text.split('\n')
|
|
||||||
out = []
|
|
||||||
for line in lines:
|
|
||||||
if LIST_START_RE.match(line) and out and out[-1].strip() != '' and not LIST_START_RE.match(out[-1]):
|
|
||||||
out.append('')
|
|
||||||
out.append(line)
|
|
||||||
return '\n'.join(out)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_frontmatter(fm_text):
|
|
||||||
keep = {}
|
|
||||||
for m in re.finditer(r'^(title|description):\s*(.*)$', fm_text, re.M):
|
|
||||||
keep[m.group(1)] = m.group(2)
|
|
||||||
icon_m = re.search(r'^icon:\s*(\S+)', fm_text, re.M)
|
|
||||||
icon_line = f"navigation:\n icon: {convert_icon(icon_m.group(1))}\n" if icon_m else ""
|
|
||||||
lines = ['---']
|
|
||||||
if 'title' in keep:
|
|
||||||
lines.append(f"title: {keep['title']}")
|
|
||||||
if 'description' in keep:
|
|
||||||
lines.append(f"description: {keep['description']}")
|
|
||||||
if icon_line:
|
|
||||||
lines.append(icon_line.rstrip('\n'))
|
|
||||||
lines.append('---')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_file(src_path: Path, dest_path: Path):
|
|
||||||
raw = src_path.read_text(encoding='utf-8')
|
|
||||||
fm_match = re.match(r'^---\s*\n(.*?)\n---\s*\n(.*)$', raw, re.S)
|
|
||||||
if fm_match:
|
|
||||||
fm_text, body = fm_match.group(1), fm_match.group(2)
|
|
||||||
new_fm = convert_frontmatter(fm_text)
|
|
||||||
else:
|
|
||||||
new_fm, body = '', raw
|
|
||||||
|
|
||||||
body = strip_ellipsis(body)
|
|
||||||
# Inline icon props like {icon=logos:docker-icon} outside of the ::card
|
|
||||||
# renderer (none expected after conversion, but harmless if present).
|
|
||||||
root = parse_blocks(body.split('\n'))
|
|
||||||
converted = render_node(root)
|
|
||||||
converted = fix_list_starts(converted)
|
|
||||||
converted = re.sub(r'\n{3,}', '\n\n', converted)
|
|
||||||
|
|
||||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
out = (new_fm + '\n\n' + converted.strip() + '\n') if new_fm else (converted.strip() + '\n')
|
|
||||||
dest_path.write_text(out, encoding='utf-8')
|
|
||||||
|
|
||||||
return 'UNCONVERTED' in converted
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
src, dest = Path(sys.argv[1]), Path(sys.argv[2])
|
|
||||||
if convert_file(src, dest):
|
|
||||||
print(f"WARNING: unconverted constructs remain in {dest}")
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
"""Old content/ path -> new content/en/ path mapping.
|
|
||||||
|
|
||||||
Unlike the Zensical port, Docus v4 keeps the same numeric-prefix
|
|
||||||
auto-navigation convention as the old site, so paths are reused almost
|
|
||||||
verbatim (just rooted under content/en/, and index.md must sit exactly at
|
|
||||||
the locale root per Docus v4's content.config.ts landing-page convention).
|
|
||||||
"""
|
|
||||||
|
|
||||||
MAPPING = [
|
|
||||||
("0.index.md", "index.md"),
|
|
||||||
("1.about/1.welcome.md", "1.about/1.welcome.md"),
|
|
||||||
("2.general/1.networking/1.nat.md", "2.general/1.networking/1.nat.md"),
|
|
||||||
("2.general/1.networking/2.dns.md", "2.general/1.networking/2.dns.md"),
|
|
||||||
("2.general/1.networking/3.samba.md", "2.general/1.networking/3.samba.md"),
|
|
||||||
("2.general/2.storage/1.raid.md", "2.general/2.storage/1.raid.md"),
|
|
||||||
("2.general/2.storage/2.zfs.md", "2.general/2.storage/2.zfs.md"),
|
|
||||||
("2.general/3.hardware/1.basics.md", "2.general/3.hardware/1.basics.md"),
|
|
||||||
("2.general/3.hardware/2.network.md", "2.general/3.hardware/2.network.md"),
|
|
||||||
("2.general/3.hardware/3.prolonas.md", "2.general/3.hardware/3.prolonas.md"),
|
|
||||||
("3.serveex/1.introduction.md", "3.serveex/1.introduction.md"),
|
|
||||||
("3.serveex/2.core/1.installation.md", "3.serveex/2.core/1.installation.md"),
|
|
||||||
("3.serveex/2.core/2.docker.md", "3.serveex/2.core/2.docker.md"),
|
|
||||||
("3.serveex/2.core/3.swag.md", "3.serveex/2.core/3.swag.md"),
|
|
||||||
("3.serveex/3.security/1.wireguard.md", "3.serveex/3.security/1.wireguard.md"),
|
|
||||||
("3.serveex/3.security/2.authentik.md", "3.serveex/3.security/2.authentik.md"),
|
|
||||||
("3.serveex/3.security/3.cloudflare.md", "3.serveex/3.security/3.cloudflare.md"),
|
|
||||||
("3.serveex/4.monitoring/1.uptime-kuma.md", "3.serveex/4.monitoring/1.uptime-kuma.md"),
|
|
||||||
("3.serveex/4.monitoring/2.dozzle.md", "3.serveex/4.monitoring/2.dozzle.md"),
|
|
||||||
("3.serveex/4.monitoring/3.speedtest-tracker.md", "3.serveex/4.monitoring/3.speedtest-tracker.md"),
|
|
||||||
("3.serveex/4.monitoring/4.beszel.md", "3.serveex/4.monitoring/4.beszel.md"),
|
|
||||||
("3.serveex/4.monitoring/5.upsnap.md", "3.serveex/4.monitoring/5.upsnap.md"),
|
|
||||||
("3.serveex/5.media/1.plex.md", "3.serveex/5.media/1.plex.md"),
|
|
||||||
("3.serveex/5.media/2.qbittorrent.md", "3.serveex/5.media/2.qbittorrent.md"),
|
|
||||||
("3.serveex/5.media/3.servarr.md", "3.serveex/5.media/3.servarr.md"),
|
|
||||||
("3.serveex/6.cloud/1.immich.md", "3.serveex/6.cloud/1.immich.md"),
|
|
||||||
("3.serveex/6.cloud/2.nextcloud.md", "3.serveex/6.cloud/2.nextcloud.md"),
|
|
||||||
("3.serveex/7.files/1.file-browser.md", "3.serveex/7.files/1.file-browser.md"),
|
|
||||||
("3.serveex/7.files/2.pingvin.md", "3.serveex/7.files/2.pingvin.md"),
|
|
||||||
("3.serveex/8.development/1.code-server.md", "3.serveex/8.development/1.code-server.md"),
|
|
||||||
("3.serveex/8.development/2.gitea.md", "3.serveex/8.development/2.gitea.md"),
|
|
||||||
("3.serveex/8.development/3.it-tools.md", "3.serveex/8.development/3.it-tools.md"),
|
|
||||||
("3.serveex/9.apps/1.adguard.md", "3.serveex/9.apps/1.adguard.md"),
|
|
||||||
("3.serveex/9.apps/2.vaultwarden.md", "3.serveex/9.apps/2.vaultwarden.md"),
|
|
||||||
("4.stockeex/1.introduction.md", "4.stockeex/1.introduction.md"),
|
|
||||||
("5.nonsense/1.python/1.nvidia-stock-bot.md", "5.nonsense/1.python/1.nvidia-stock-bot.md"),
|
|
||||||
("5.nonsense/1.python/2. adguard-cidre.md", "5.nonsense/1.python/2.adguard-cidre.md"),
|
|
||||||
("5.nonsense/1.python/3.lumeex.md", "5.nonsense/1.python/3.lumeex.md"),
|
|
||||||
("5.nonsense/1.python/4.instameex.md", "5.nonsense/1.python/4.instameex.md"),
|
|
||||||
("5.nonsense/2.bash/1.servarr-duplicates.md", "5.nonsense/2.bash/1.servarr-duplicates.md"),
|
|
||||||
("5.nonsense/2.bash/2.luks- backup.md", "5.nonsense/2.bash/2.luks-backup.md"),
|
|
||||||
("5.nonsense/2.bash/3.socat-proxy.md", "5.nonsense/2.bash/3.socat-proxy.md"),
|
|
||||||
("5.nonsense/2.bash/4.hotdisk.md", "5.nonsense/2.bash/4.hotdisk.md"),
|
|
||||||
("5.nonsense/2.bash/5.backrest-docker-stop.md", "5.nonsense/2.bash/5.backrest-docker-stop.md"),
|
|
||||||
("Recycled/1.deprecated/1.wireguard-14.md", "6.recycled/1.deprecated/1.wireguard-14.md"),
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user