Docs: rewrite README.md for users, add MANUAL.md, sync from readme.html on every release
People landing on the repo page were having to install RemSound just to read what it does and how to use it. Two doc changes fix that: 1) README.md rewritten as a plain-English landing page. Drops the developer-focused highlights / build-from-source / project-layout sections in favour of what RemSound is, who it's for, how to install it, and a prominent link to the manual. No jargon, no command lines, no NuGet / SDK / ASIO-protocol talk. The dev-side information that used to live here (build commands, source layout, relay setup) is still discoverable for anyone who wants it — the source itself is on the same page, and the relay docs are under server/README.md. 2) MANUAL.md added at the repo root as the GitHub-rendered version of the F1 help. Markdown derived directly from readme.html via sync-manual.py (new), so visitors can read the manual inline on the repo page with no download. readme.html stays exactly where it was (bundled inside RemSound, opened by F1) — it remains the canonical source of the manual content; MANUAL.md is auto-generated from it. The sync-manual.py script is invoked automatically from build-release.ps1 as step 0, before any other release work. It regenerates MANUAL.md from readme.html and then checks `git diff` on MANUAL.md — if the file changed, the release is paused with a message asking the user to commit the updated MANUAL.md alongside the release commit. That makes it structurally impossible to ship a release with a stale GitHub-facing manual: forgetting to commit MANUAL.md after editing the bundled help triggers a deliberate release-time stop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
8aa8d0c3bd
commit
0189b12668
+145
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sync MANUAL.md from readme.html.
|
||||
|
||||
readme.html is the canonical user manual — bundled inside RemSound and opened by F1.
|
||||
MANUAL.md is the GitHub-facing Markdown rendition shown on the repository page.
|
||||
|
||||
This script reads readme.html, converts it to Markdown via html2text, and post-processes
|
||||
the output to fix two things html2text doesn't do well on this document:
|
||||
|
||||
* Table of contents links. The HTML uses custom anchors like `#what-it-does` which
|
||||
html2text strips. We rebuild the TOC after conversion using GitHub's auto-generated
|
||||
heading anchors (lowercased, punctuation stripped, spaces → hyphens).
|
||||
* Cosmetic backslash escapes before periods after numbers in headings (`## 1\. ...`
|
||||
instead of `## 1. ...`). GitHub renders both the same but the un-escaped version
|
||||
looks cleaner in raw source.
|
||||
|
||||
The script is invoked automatically by build-release.ps1 as the first step before
|
||||
packaging a release, so MANUAL.md can never get out of sync with the bundled help.
|
||||
It can also be run by hand from the repo root: `python sync-manual.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html2text
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).parent
|
||||
README_HTML = REPO_ROOT / "readme.html"
|
||||
MANUAL_MD = REPO_ROOT / "MANUAL.md"
|
||||
|
||||
|
||||
def github_anchor(heading_text: str) -> str:
|
||||
"""Replicate GitHub's auto-anchor rule for a Markdown heading.
|
||||
|
||||
Rules (per https://gist.github.com/asabaylus/3071099):
|
||||
* Lowercase everything.
|
||||
* Replace spaces with hyphens.
|
||||
* Strip all punctuation EXCEPT hyphens and underscores.
|
||||
* Collapse runs of hyphens (though GitHub does NOT — it keeps them).
|
||||
"""
|
||||
s = heading_text.strip().lower()
|
||||
# Replace whitespace with hyphens
|
||||
s = re.sub(r"\s+", "-", s)
|
||||
# Strip punctuation except hyphens and underscores
|
||||
s = re.sub(r"[^\w\-]", "", s)
|
||||
return s
|
||||
|
||||
|
||||
def convert_html_to_markdown(html: str) -> str:
|
||||
# Strip <head> entirely — none of its content belongs in the Markdown
|
||||
html = re.sub(r"<head>.*?</head>", "", html, flags=re.DOTALL)
|
||||
# Render every <div class="..."> callout (note / warn / etc.) as a blockquote —
|
||||
# Markdown's native callout. The wildcard catches all classes in one rule so a new
|
||||
# callout class added later doesn't silently break the structure (which it did for
|
||||
# the original `<div class="note">`-only rule when `class="warn"` was introduced).
|
||||
html = re.sub(r'<div\s+class="[^"]*">', "<blockquote>", html)
|
||||
html = re.sub(r"</div>", "</blockquote>", html)
|
||||
|
||||
h = html2text.HTML2Text()
|
||||
h.body_width = 0 # never hard-wrap; let the rendering layer reflow
|
||||
h.ignore_links = False
|
||||
h.unicode_snob = True
|
||||
h.use_automatic_links = True
|
||||
h.bypass_tables = False
|
||||
h.escape_snob = False
|
||||
md = h.handle(html)
|
||||
|
||||
# Replace any leftover U+FFFD with the em-dash that almost always belongs there
|
||||
md = md.replace("�", "—")
|
||||
# Strip cosmetic backslash escapes before periods after numbers in headings
|
||||
md = re.sub(
|
||||
r"^(##+ \d+)\\\.",
|
||||
lambda m: m.group(1) + ".",
|
||||
md,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
# Trim trailing whitespace on every line
|
||||
md = "\n".join(line.rstrip() for line in md.splitlines()) + "\n"
|
||||
return md
|
||||
|
||||
|
||||
def rebuild_toc(md: str) -> str:
|
||||
"""Rebuild the Table of contents section using GitHub-style anchor links.
|
||||
|
||||
The HTML version of readme.html uses a hand-written `<ol>` with `<a href="#id">` items
|
||||
where `id` matches the `id=""` attribute on later `<h2>` elements. html2text drops those
|
||||
links entirely on conversion. Here we regenerate the list from the actual `## ` headings
|
||||
in the post-conversion Markdown, so the TOC always reflects the real document.
|
||||
"""
|
||||
# Collect all top-level (##) headings in document order, skipping the TOC heading itself
|
||||
headings: list[str] = []
|
||||
for line in md.splitlines():
|
||||
m = re.match(r"^##\s+(.+?)\s*$", line)
|
||||
if not m:
|
||||
continue
|
||||
text = m.group(1).strip()
|
||||
if text.lower() == "table of contents":
|
||||
continue
|
||||
headings.append(text)
|
||||
|
||||
if not headings:
|
||||
return md
|
||||
|
||||
# Build the new TOC: numbered list with anchor links to each heading
|
||||
toc_lines = []
|
||||
for h_text in headings:
|
||||
# Strip the leading "N. " from headings like "1. What RemSound does" so the displayed
|
||||
# link text in the TOC reads naturally. The anchor still references the FULL heading.
|
||||
display = re.sub(r"^\d+\.\s*", "", h_text)
|
||||
anchor = github_anchor(h_text)
|
||||
toc_lines.append(f" 1. [{display}](#{anchor})")
|
||||
new_toc_block = "\n".join(toc_lines) + "\n"
|
||||
|
||||
# Replace the existing TOC block (everything between "## Table of contents" and the next "## ")
|
||||
# with the regenerated one. Using a callback to keep the surrounding markers intact.
|
||||
def replace_toc(match: re.Match[str]) -> str:
|
||||
return match.group(1) + "\n\n" + new_toc_block + "\n"
|
||||
|
||||
pattern = re.compile(
|
||||
r"(##\s+Table of contents\s*\n)(?:.*?)(?=\n##\s)",
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
if not pattern.search(md):
|
||||
# Defensive: TOC section wasn't found in the expected shape — leave the doc alone.
|
||||
return md
|
||||
return pattern.sub(replace_toc, md, count=1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not README_HTML.exists():
|
||||
print(f"ERROR: {README_HTML} not found", file=sys.stderr)
|
||||
return 1
|
||||
html = README_HTML.read_text(encoding="utf-8")
|
||||
md = convert_html_to_markdown(html)
|
||||
md = rebuild_toc(md)
|
||||
MANUAL_MD.write_text(md, encoding="utf-8", newline="\n")
|
||||
print(f"OK - MANUAL.md regenerated from readme.html ({len(md):,} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user