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