2026-05-06 10:23:00 +02:00
|
|
|
#!/usr/bin/env bash
|
2026-08-22 15:57:58 +02:00
|
|
|
# Build every Marp deck in the repository into static HTML.
|
|
|
|
|
#
|
|
|
|
|
# A deck is any *.md carrying `marp: true` in its front matter. Two source
|
|
|
|
|
# roots, two output conventions:
|
|
|
|
|
#
|
|
|
|
|
# docs/presentations/<name>.md -> <name>.html (flat, historical)
|
|
|
|
|
# slides/<path>/<name>.md -> <path>/<name>.html (path mirrors URL)
|
|
|
|
|
#
|
|
|
|
|
# So slides/tappaas/how-to/new-module/index.md is served at
|
|
|
|
|
# https://slides.makerfloss.eu/tappaas/how-to/new-module/ .
|
2026-05-06 10:23:00 +02:00
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
2026-05-17 17:24:34 +02:00
|
|
|
OUTPUT_DIR="${OUTPUT_DIR:-$REPO_ROOT/slides}"
|
|
|
|
|
SLIDES_TITLE="${SLIDES_TITLE:-MakerFLOSS Slides}"
|
|
|
|
|
REPO_WEB_URL="${REPO_WEB_URL:-https://forgejo.makerfloss.eu/sjat/MakerFLOSS}"
|
|
|
|
|
|
|
|
|
|
mkdir -p "$OUTPUT_DIR"
|
|
|
|
|
|
2026-08-22 15:57:58 +02:00
|
|
|
# Collect decks as "<repo-relative source>|<output-relative target>" pairs.
|
|
|
|
|
DECKS=()
|
2026-05-06 10:23:00 +02:00
|
|
|
while IFS= read -r f; do
|
2026-08-22 15:57:58 +02:00
|
|
|
rel="${f#"$REPO_ROOT"/}"
|
|
|
|
|
DECKS+=("$rel|$(basename "${rel%.md}").html")
|
|
|
|
|
done < <(grep -rl "^marp: true" "$REPO_ROOT/docs" --include="*.md" 2>/dev/null | sort || true)
|
|
|
|
|
|
|
|
|
|
if [ -d "$REPO_ROOT/slides" ]; then
|
|
|
|
|
while IFS= read -r f; do
|
|
|
|
|
rel="${f#"$REPO_ROOT"/}"
|
|
|
|
|
out="${rel#slides/}"
|
|
|
|
|
DECKS+=("$rel|${out%.md}.html")
|
|
|
|
|
done < <(grep -rl "^marp: true" "$REPO_ROOT/slides" --include="*.md" 2>/dev/null | sort || true)
|
|
|
|
|
fi
|
2026-05-06 10:23:00 +02:00
|
|
|
|
2026-08-22 15:57:58 +02:00
|
|
|
if [ ${#DECKS[@]} -eq 0 ]; then
|
|
|
|
|
echo "No marp presentations found."
|
2026-05-06 10:23:00 +02:00
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
2026-08-22 15:57:58 +02:00
|
|
|
echo "Found ${#DECKS[@]} presentation(s):"
|
|
|
|
|
printf ' %s\n' "${DECKS[@]%%|*}"
|
2026-05-06 10:23:00 +02:00
|
|
|
|
2026-08-22 15:57:58 +02:00
|
|
|
# Wipe previously built HTML so deleted/de-tagged decks disappear. Only *.html
|
|
|
|
|
# is removed, so deck sources living inside slides/ are never touched.
|
|
|
|
|
find "$OUTPUT_DIR" -name "*.html" -type f -delete
|
2026-05-10 17:49:02 +02:00
|
|
|
|
2026-08-22 15:57:58 +02:00
|
|
|
render() { # render <source-rel> <target-rel>
|
|
|
|
|
local src="$1" dst="$2"
|
|
|
|
|
mkdir -p "$OUTPUT_DIR/$(dirname "$dst")"
|
|
|
|
|
if command -v marp &>/dev/null; then
|
|
|
|
|
marp --html --output "$OUTPUT_DIR/$dst" "$REPO_ROOT/$src"
|
|
|
|
|
elif command -v npx &>/dev/null; then
|
|
|
|
|
npx --yes @marp-team/marp-cli --html --output "$OUTPUT_DIR/$dst" "$REPO_ROOT/$src"
|
|
|
|
|
else
|
|
|
|
|
# marpteam/marp-cli drops privileges to its own "marp" user, so the mounted
|
|
|
|
|
# output tree has to be writable by anyone — and --user must NOT be passed.
|
|
|
|
|
chmod -R a+rwX "$OUTPUT_DIR"
|
2026-05-10 17:49:02 +02:00
|
|
|
docker run --rm \
|
|
|
|
|
-v "$REPO_ROOT":/home/marp/app:ro \
|
2026-08-22 15:57:58 +02:00
|
|
|
-v "$OUTPUT_DIR":/home/marp/out \
|
|
|
|
|
marpteam/marp-cli --html --output "/home/marp/out/$dst" "$src"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ! command -v marp &>/dev/null && ! command -v npx &>/dev/null; then
|
|
|
|
|
if command -v docker &>/dev/null; then
|
|
|
|
|
echo "marp not found locally — using Docker (marpteam/marp-cli)..."
|
|
|
|
|
else
|
|
|
|
|
echo "error: need one of marp, npx or docker on PATH" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-05-06 10:23:00 +02:00
|
|
|
fi
|
|
|
|
|
|
2026-08-22 15:57:58 +02:00
|
|
|
for deck in "${DECKS[@]}"; do
|
|
|
|
|
render "${deck%%|*}" "${deck#*|}"
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# Post-process each built page: highlight unfinished `TODO ...` markers, and
|
|
|
|
|
# turn mermaid code blocks into live diagrams.
|
|
|
|
|
#
|
2026-05-10 17:42:35 +02:00
|
|
|
# Marp emits fenced mermaid blocks as <pre><code class="language-mermaid">.
|
2026-08-22 15:57:58 +02:00
|
|
|
# The script replaces those elements with <div class="mermaid">, then loads and
|
|
|
|
|
# runs mermaid.js from CDN.
|
|
|
|
|
postprocess() {
|
2026-05-10 17:42:35 +02:00
|
|
|
local html_file="$1"
|
|
|
|
|
python3 - "$html_file" << 'PYEOF'
|
2026-08-22 15:57:58 +02:00
|
|
|
import re
|
2026-05-10 17:42:35 +02:00
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
path = sys.argv[1]
|
|
|
|
|
snippet = """\
|
|
|
|
|
<script type="module">
|
|
|
|
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
|
|
|
document.querySelectorAll('code.language-mermaid').forEach(el => {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'mermaid';
|
|
|
|
|
div.textContent = el.textContent;
|
2026-05-10 18:14:02 +02:00
|
|
|
el.parentElement.replaceWith(div);
|
2026-05-10 17:42:35 +02:00
|
|
|
});
|
2026-05-10 18:14:02 +02:00
|
|
|
mermaid.initialize({ startOnLoad: false, theme: 'dark' });
|
|
|
|
|
await mermaid.run();
|
2026-05-10 17:42:35 +02:00
|
|
|
</script>"""
|
|
|
|
|
|
2026-05-10 17:43:59 +02:00
|
|
|
with open(path, encoding='utf-8') as f:
|
|
|
|
|
content = f.read()
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
# Inline <code>TODO…</code> -> <code class="todo">…</code>; decks style that class.
|
|
|
|
|
content, todos = re.subn(r'<code>(TODO\b[^<]*)</code>', r'<code class="todo">\1</code>', content)
|
|
|
|
|
|
|
|
|
|
if 'class="language-mermaid"' in content:
|
|
|
|
|
if '</body>' not in content:
|
|
|
|
|
print(f"Warning: </body> not found in {path}", file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
content = content.replace('</body>', snippet + '\n</body>', 1)
|
|
|
|
|
print(f" Injected mermaid.js into {path}")
|
|
|
|
|
|
2026-05-10 17:43:59 +02:00
|
|
|
with open(path, 'w', encoding='utf-8') as f:
|
2026-08-22 15:57:58 +02:00
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
|
|
if todos:
|
|
|
|
|
print(f" {todos} unfinished TODO marker(s) in {path}")
|
2026-05-10 17:42:35 +02:00
|
|
|
PYEOF
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if command -v python3 &>/dev/null; then
|
2026-08-22 15:57:58 +02:00
|
|
|
# The landing index.html is generated further down, so every *.html present
|
|
|
|
|
# right now is a built deck.
|
|
|
|
|
while IFS= read -r html_file; do
|
|
|
|
|
postprocess "$html_file"
|
|
|
|
|
done < <(find "$OUTPUT_DIR" -name "*.html" -type f)
|
2026-05-10 17:42:35 +02:00
|
|
|
else
|
2026-08-22 15:57:58 +02:00
|
|
|
echo "Warning: python3 not found — skipping post-processing"
|
2026-05-10 17:42:35 +02:00
|
|
|
fi
|
|
|
|
|
|
2026-08-25 18:42:11 +02:00
|
|
|
# Copy deck assets (images) that live next to a source under slides/. Marp keeps
|
|
|
|
|
# relative image URLs in the HTML, so the files must exist at the same relative
|
|
|
|
|
# path in the output tree. Only slides/ can carry these: docs/presentations/
|
|
|
|
|
# decks are flattened to <name>.html, so a relative path would not survive.
|
|
|
|
|
# When OUTPUT_DIR is the in-repo slides/ dir, source and target are the same
|
|
|
|
|
# file and there is nothing to copy.
|
|
|
|
|
if [ -d "$REPO_ROOT/slides" ]; then
|
|
|
|
|
while IFS= read -r -d '' asset; do
|
|
|
|
|
rel="${asset#"$REPO_ROOT"/slides/}"
|
|
|
|
|
dest="$OUTPUT_DIR/$rel"
|
|
|
|
|
[ "$asset" -ef "$dest" ] && continue
|
|
|
|
|
mkdir -p "$(dirname "$dest")"
|
|
|
|
|
cp "$asset" "$dest"
|
|
|
|
|
echo " Copied asset $rel"
|
|
|
|
|
done < <(find "$REPO_ROOT/slides" -type f \
|
|
|
|
|
\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \
|
|
|
|
|
-o -iname "*.gif" -o -iname "*.svg" -o -iname "*.webp" \) -print0)
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-17 17:24:34 +02:00
|
|
|
# Regenerate index.html listing every built deck.
|
|
|
|
|
INDEX="$OUTPUT_DIR/index.html"
|
|
|
|
|
{
|
|
|
|
|
printf '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
|
|
|
|
|
printf '<meta name="viewport" content="width=device-width,initial-scale=1">'
|
|
|
|
|
printf '<title>%s</title>' "$SLIDES_TITLE"
|
|
|
|
|
printf '<style>'
|
|
|
|
|
printf 'body{font-family:sans-serif;max-width:700px;margin:3rem auto;padding:0 1.5rem;background:#fafafa;}'
|
|
|
|
|
printf 'h1{font-size:1.6rem;margin-bottom:.25rem;}p.sub{color:#666;margin-top:0;}'
|
|
|
|
|
printf 'ul{list-style:none;padding:0;margin-top:2rem;}'
|
|
|
|
|
printf 'li{border:1px solid #e0e0e0;border-radius:6px;margin:.75rem 0;background:#fff;}'
|
|
|
|
|
printf 'li a{display:block;padding:1rem 1.25rem;text-decoration:none;color:#1a1a1a;font-size:1.05rem;}'
|
|
|
|
|
printf 'li a:hover{background:#f0f4ff;border-radius:6px;}'
|
|
|
|
|
printf 'footer{margin-top:3rem;color:#aaa;font-size:.8rem;}'
|
|
|
|
|
printf '</style></head><body>'
|
|
|
|
|
printf '<h1>%s</h1>' "$SLIDES_TITLE"
|
|
|
|
|
printf '<p class="sub">Built from <a href="%s">%s</a></p>' \
|
|
|
|
|
"$REPO_WEB_URL" "${REPO_WEB_URL#https://}"
|
|
|
|
|
printf '<ul>'
|
2026-08-22 15:57:58 +02:00
|
|
|
for deck in "${DECKS[@]}"; do
|
|
|
|
|
href="${deck#*|}"
|
|
|
|
|
label="${href%/index.html}"; label="${label%.html}"
|
|
|
|
|
printf '<li><a href="%s">%s</a></li>' "$href" "$label"
|
2026-05-17 17:24:34 +02:00
|
|
|
done
|
|
|
|
|
printf '</ul>'
|
|
|
|
|
printf '<footer>Last built: %s · <a href="https://marp.app">Marp</a></footer>' "$(date -Iseconds)"
|
|
|
|
|
printf '</body></html>'
|
|
|
|
|
} > "$INDEX"
|
|
|
|
|
|
2026-05-06 10:23:00 +02:00
|
|
|
echo "Done — slides in $OUTPUT_DIR/"
|