Migrate docudjeex to Docus v4 with EN/FR content

This commit is contained in:
Djeex
2026-08-30 16:41:37 +02:00
commit c51fcd5df6
241 changed files with 41143 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
#!/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}")
+55
View File
@@ -0,0 +1,55 @@
"""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"),
]