guides moved in his own folder
This commit is contained in:
parent
7459b1e70b
commit
32b62d10a2
|
|
@ -0,0 +1,228 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Render the user guide to a print-ready HTML page.
|
||||
|
||||
From web/, to regenerate docs/user-guide.pdf after editing docs/user-guide.md:
|
||||
|
||||
python3 docs/render-user-guide.py docs/user-guide.md /tmp/ug.html \
|
||||
"HumAIn Flow" "User Guide" "Version of 16 September 2026"
|
||||
npx playwright pdf file:///tmp/ug.html docs/user-guide.pdf
|
||||
|
||||
Deliberately not a general Markdown implementation: it handles the constructs the
|
||||
guide actually uses, so the output is predictable and the print styling can be
|
||||
tuned for it (page breaks per chapter, a real table of contents, no orphan
|
||||
headings). Pandoc is not installed here and pulling one in for one document costs
|
||||
more than it saves.
|
||||
"""
|
||||
import base64
|
||||
import html
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
INLINE = [
|
||||
(re.compile(r"`([^`]+)`"), r"<code>\1</code>"),
|
||||
(re.compile(r"\*\*([^*]+)\*\*"), r"<strong>\1</strong>"),
|
||||
(re.compile(r"(?<![\w*])\*([^*\n]+)\*(?![\w*])"), r"<em>\1</em>"),
|
||||
(re.compile(r"\[([^\]]+)\]\(([^)]+)\)"), r'<a href="\2">\1</a>'),
|
||||
]
|
||||
|
||||
|
||||
def inline(text):
|
||||
out = html.escape(text)
|
||||
for pattern, replacement in INLINE:
|
||||
out = pattern.sub(replacement, out)
|
||||
return out
|
||||
|
||||
|
||||
def convert(markdown, figures=None):
|
||||
lines = markdown.split("\n")
|
||||
out, toc = [], []
|
||||
i = 0
|
||||
list_stack = []
|
||||
|
||||
def close_lists(to_depth=0):
|
||||
while len(list_stack) > to_depth:
|
||||
out.append(f"</{list_stack.pop()}>")
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
|
||||
if not stripped:
|
||||
close_lists()
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if stripped.startswith("```"):
|
||||
close_lists()
|
||||
i += 1
|
||||
block = []
|
||||
while i < len(lines) and not lines[i].strip().startswith("```"):
|
||||
block.append(html.escape(lines[i]))
|
||||
i += 1
|
||||
i += 1
|
||||
out.append("<pre><code>" + "\n".join(block) + "</code></pre>")
|
||||
continue
|
||||
|
||||
heading = re.match(r"^(#{1,4})\s+(.*)$", stripped)
|
||||
if heading:
|
||||
close_lists()
|
||||
level = len(heading.group(1))
|
||||
text = heading.group(2)
|
||||
anchor = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
||||
# Chapters are h2 and sections h3, so the contents list is those two levels.
|
||||
if 2 <= level <= 3:
|
||||
toc.append((level - 1, text, anchor))
|
||||
out.append(f'<h{level} id="{anchor}">{inline(text)}</h{level}>')
|
||||
if level == 2 and figures and text in figures:
|
||||
image_data, caption = figures[text]
|
||||
out.append(
|
||||
'<figure><img src="data:image/png;base64,' + image_data
|
||||
+ '" alt="' + html.escape(caption) + '">'
|
||||
+ '<figcaption>' + html.escape(caption) + '</figcaption></figure>'
|
||||
)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if stripped.startswith("|") and i + 1 < len(lines) and re.match(r"^\|[\s:|-]+\|$", lines[i + 1].strip()):
|
||||
close_lists()
|
||||
header = [c.strip() for c in stripped.strip("|").split("|")]
|
||||
i += 2
|
||||
rows = []
|
||||
while i < len(lines) and lines[i].strip().startswith("|"):
|
||||
rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")])
|
||||
i += 1
|
||||
head = "".join(f"<th>{inline(c)}</th>" for c in header)
|
||||
body = "".join("<tr>" + "".join(f"<td>{inline(c)}</td>" for c in r) + "</tr>" for r in rows)
|
||||
out.append(f"<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>")
|
||||
continue
|
||||
|
||||
if stripped in ("---", "***"):
|
||||
close_lists()
|
||||
out.append('<hr class="page-break">')
|
||||
i += 1
|
||||
continue
|
||||
|
||||
bullet = re.match(r"^(\s*)[-*]\s+(.*)$", line)
|
||||
number = re.match(r"^(\s*)\d+\.\s+(.*)$", line)
|
||||
if bullet or number:
|
||||
match = bullet or number
|
||||
tag = "ul" if bullet else "ol"
|
||||
depth = len(match.group(1)) // 2 + 1
|
||||
while len(list_stack) > depth:
|
||||
out.append(f"</{list_stack.pop()}>")
|
||||
while len(list_stack) < depth:
|
||||
out.append(f"<{tag}>")
|
||||
list_stack.append(tag)
|
||||
out.append(f"<li>{inline(match.group(2))}</li>")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if stripped.startswith("> "):
|
||||
close_lists()
|
||||
out.append(f'<p class="note">{inline(stripped[2:])}</p>')
|
||||
i += 1
|
||||
continue
|
||||
|
||||
close_lists()
|
||||
paragraph = [stripped]
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].strip() and not re.match(r"^\s*([-*]|\d+\.|#|\||>|```)", lines[i]):
|
||||
paragraph.append(lines[i].strip())
|
||||
i += 1
|
||||
out.append(f"<p>{inline(' '.join(paragraph))}</p>")
|
||||
|
||||
close_lists()
|
||||
return "\n".join(out), toc
|
||||
|
||||
|
||||
CSS = """
|
||||
@page { size: A4; margin: 18mm 16mm 20mm; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { background: #ffffff; }
|
||||
body { font: 10.5pt/1.55 -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
color: #1e293b; margin: 0; }
|
||||
h1 { font-size: 21pt; color: #0f172a; margin: 0 0 6pt; line-height: 1.25; }
|
||||
h2 { font-size: 19pt; color: #0f172a; page-break-before: always; page-break-after: avoid;
|
||||
border-top: 2px solid #2563eb; padding-top: 6pt; margin: 0 0 10pt; line-height: 1.25; }
|
||||
h3 { font-size: 12.5pt; color: #1d4ed8; margin: 16pt 0 5pt; page-break-after: avoid; }
|
||||
h4 { font-size: 10.8pt; color: #0f172a; margin: 12pt 0 3pt; page-break-after: avoid; }
|
||||
p { margin: 0 0 7pt; orphans: 2; widows: 2; }
|
||||
ul, ol { margin: 0 0 8pt; padding-left: 18pt; }
|
||||
li { margin-bottom: 2.5pt; }
|
||||
code { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 9pt;
|
||||
background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 3px; padding: 0 3px; }
|
||||
pre { background: #f8fafc; border: 1px solid #e2e8f0; border-left: 3px solid #94a3b8;
|
||||
border-radius: 4px; padding: 8pt 10pt; overflow-x: auto; page-break-inside: avoid; }
|
||||
pre code { background: none; border: none; padding: 0; font-size: 8.5pt; line-height: 1.45; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 0 0 10pt; font-size: 9.5pt;
|
||||
page-break-inside: avoid; }
|
||||
th { background: #eff6ff; color: #1e3a8a; text-align: left; font-weight: 600; }
|
||||
th, td { border: 1px solid #cbd5e1; padding: 4pt 6pt; vertical-align: top; }
|
||||
.note { border-left: 3px solid #f59e0b; background: #fffbeb; padding: 6pt 9pt;
|
||||
margin: 0 0 8pt; page-break-inside: avoid; }
|
||||
hr.page-break { border: 0; page-break-after: always; }
|
||||
.cover { height: 247mm; display: flex; flex-direction: column; justify-content: center; }
|
||||
.cover-logo { width: 66pt; height: 66pt; object-fit: contain; margin-bottom: 14pt; }
|
||||
.cover-rule { width: 46pt; height: 4pt; border-radius: 2pt; background: #14b8a6; margin: 0 0 16pt; }
|
||||
.cover-title { font-size: 34pt; border: 0; padding: 0; }
|
||||
.cover-sub { font-size: 13pt; color: #475569; margin-top: 2pt; }
|
||||
.cover-meta { margin-top: 26pt; font-size: 10pt; color: #64748b; }
|
||||
.toc { page-break-after: always; }
|
||||
.toc-title { font-size: 19pt; color: #0f172a; border-top: 2px solid #2563eb;
|
||||
padding-top: 6pt; margin: 0 0 10pt; }
|
||||
.toc ol { list-style: none; padding-left: 0; counter-reset: chapter; }
|
||||
.toc li.l1 { counter-increment: chapter; font-weight: 600; margin-top: 6pt; }
|
||||
.toc li.l1::before { content: counter(chapter) ". "; color: #2563eb; }
|
||||
.toc li.l2 { padding-left: 16pt; font-weight: 400; font-size: 9.5pt; color: #475569; }
|
||||
.toc a { color: inherit; text-decoration: none; }
|
||||
a { color: #1d4ed8; }
|
||||
figure { margin: 7pt 0 14pt; page-break-inside: avoid; }
|
||||
figure img { display: block; width: 100%; border: 1px solid #cbd5e1; border-radius: 6px; }
|
||||
figcaption { margin-top: 4pt; color: #64748b; text-align: center; font-size: 8.5pt; line-height: 1.35; }
|
||||
"""
|
||||
|
||||
|
||||
def main(md_path, html_path, title, subtitle, meta):
|
||||
# The cover carries the document title, so the file's own leading H1 would only repeat it.
|
||||
source = re.sub(r"\A\s*#\s+.*\n", "", open(md_path).read())
|
||||
docs_dir = os.path.dirname(os.path.abspath(md_path))
|
||||
figure_specs = {
|
||||
"Signing in and finding your way around": (
|
||||
"images/login.png",
|
||||
"Figure 1 — Sign-in page. Use your assigned username and password to access the workspace.",
|
||||
),
|
||||
"Building a flow by hand": (
|
||||
"images/flow-editor.png",
|
||||
"Figure 2 — Editor workspace. The left rail manages flows; the central area is the canvas; the right rail offers navigation and validation tools.",
|
||||
),
|
||||
"Working in the Tasks tab": (
|
||||
"images/tasks.png",
|
||||
"Figure 3 — Tasks workspace with execution history, read-only graph and the input/output panel.",
|
||||
),
|
||||
}
|
||||
figures = {
|
||||
label: (base64.b64encode(open(os.path.join(docs_dir, filename), "rb").read()).decode("ascii"), caption)
|
||||
for label, (filename, caption) in figure_specs.items()
|
||||
}
|
||||
body, toc = convert(source, figures)
|
||||
logo_path = os.path.join(docs_dir, "..", "public", "logoNoName.png")
|
||||
logo = base64.b64encode(open(logo_path, "rb").read()).decode("ascii")
|
||||
items = "".join(
|
||||
f'<li class="l{level}"><a href="#{anchor}">{html.escape(text)}</a></li>'
|
||||
for level, text, anchor in toc
|
||||
)
|
||||
page = f"""<!doctype html><html><head><meta charset="utf-8"><title>{html.escape(title)}</title>
|
||||
<style>{CSS}</style></head><body>
|
||||
<div class="cover"><img class="cover-logo" src="data:image/png;base64,{logo}" alt="HumAIn Flow logo">
|
||||
<div class="cover-rule"></div><h1 class="cover-title">{html.escape(title)}</h1>
|
||||
<div class="cover-sub">{html.escape(subtitle)}</div>
|
||||
<div class="cover-meta">{html.escape(meta)}</div></div>
|
||||
<div class="toc"><div class="toc-title">Contents</div><ol>{items}</ol></div>
|
||||
{body}</body></html>"""
|
||||
open(html_path, "w").write(page)
|
||||
print(f"html written: {html_path} ({len(page)} bytes, {len(toc)} toc entries)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(*sys.argv[1:])
|
||||
Loading…
Reference in New Issue