Uh oh!
There was an error while loading. Please reload this page.
ADFA-5039: Convert kotlin-web-site docs to JSON - #23
Conversation
Converts kotlin-web-site/docs (Writerside-flavored Markdown) into the JSON block schema this project's templating engine renders - one JSON file per topic, plus theme.json and a copy of images/. Split out of the larger Kotlin-docs pipeline PR so this ticket's scope (producing the JSON) can be reviewed independently of the database-insertion side (ADFA-4739). Includes review_build_json.sh, a throwaway helper that clones kotlin-web-site and runs the converter against it, for reviewers to see real output without any other setup - not part of the actual pipeline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
luisguzman-adfa
left a comment
There was a problem hiding this comment.
PR #23 — review comments
Ran md_to_json.py against a small fixture with intentionally broken references (and cross-checked with review_build_json.sh): it converts as expected and the output matches the documented block schema:
title + %var% substitution, cross-page link resolution (b.md#first-heading → /b.html#first-heading), external/broken-link coloring, image rewrite with {width=...} folding, and %var% inside code all come through, with the correct "unknown topic" warning for the deliberately-broken link.
Docstring and README are accurate and the "known limitations" are well-scoped.
Test block (Ubuntu)
# 1) get the repo on the PR branch
git clone https://github.com/appdevforall/OfflineDocumentationTools.git
cd OfflineDocumentationTools
git switch fix/ADFA-5039
cd ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON
# 2) run md_to_json.py against a tiny fixture with intentionally broken refs
python3 -m venv /tmp/mdvenv && source /tmp/mdvenv/bin/activate
pip install markdown-it-py --quiet
D=$(mktemp -d); mkdir -p "$D/docs/topics" "$D/docs/images"
printf '<vars><var name="v" value="2.0.0"/></vars>' > "$D/docs/v.list"
printf 'PNG' > "$D/docs/images/m.png"
cat > "$D/docs/topics/a.md" <<'MD'
[//]: # (title: Sample %v%)
## First heading
Para with [x-link](b.md#first-heading), [external](https://x.com), [broken](missing.md).
{width="200"}
```kotlin
fun main() { println("%v%") }
```
MD
printf '[//]: # (title: B)\n## First heading\nhi\n' > "$D/docs/topics/b.md"
python3 md_to_json.py "$D/docs" "$D/out" config.json && python3 -m json.tool "$D/out/topics/a.json"
Decision points
Heading
ids vs. the anchors links point at.slugify()derives ids from heading text whileresolve_hrefkeeps the source#anchorverbatim. Simple headings line up (First heading→first-headingmatched its link), but if Writerside's anchor algorithm diverges fromslugifyon headings with inline code/punctuation, or on duplicate headings, a link resolves to the page but lands on no anchor, silently. Worth spot-checking those cases against a real cross-reference.Tests. No automated test for
md_to_json.py(onlyreview_build_json.sh). The block schema is the contract the templating step depends on, so a small fixture + a check on the emitted blocks would guard it, or, if that's landing with ADFA-4739, a one-line note saying so.
Suggestion
parse_attrsblanks a bare attribute value when the tag also has a quoted one:parse_attrs('group-key=gradle title="Gradle"')→{'group-key': '', 'title': 'Gradle'}. The'="' in attr_strcheck is whole-string, so any quoted attribute empties every bare one. Only fires for a block-level<tab>/<tabs>with a bare attr (Writerside usually quotes everything), so low-trigger, but real, deciding per-pair (use the bare group when the quoted one didn't match) fixes it.
Minor suggestions
slugifydoesn't de-dup ids — two headings with the same text share an id, so an anchor to the second lands on the first. The usual-2/-3suffixing handles it.Broken/external coloring appends
style="color: …"; on an<a>that already has astylethat's a duplicate attribute (first wins in HTML5), so those links won't be colored — and the color is injected unescaped, so a hex check inload_configwould fail a typo loudly.page_idisn't posix-normalized (str(rel.with_suffix(""))), whilebuild_topic_indexnormalizes backslashes, so the id (and the resolved/….htmlpath) come out with backslashes on Windows..as_posix()keeps them consistent; no-op on Linux/CI.README usage shows
--topics-subdirbut not--images-subdir(both exist).
hal-eisen-adfa
left a comment
There was a problem hiding this comment.
Deep review of md_to_json.py and friends. 15 findings, left inline.
These aren't read-the-code guesses — I sparse-cloned JetBrains/kotlin-web-site (304 topics + v.list) and ran this converter over the whole corpus, so most findings below come with real file:line citations and occurrence counts from actual Kotlin docs.
Three separate content-loss bugs, all confirmed against the live corpus:
TAG_REeats<table>(line 84) —<table>appears 38 times in the source and 0 times in the generated JSON. 76 table lines across 18 files are silently consumed.merge_attr_linesdeletes any{...}paragraph (line 427) even when it recovers zero attributes — the content is dropped and nothing is gained.<tabs>discards all non-tab children (line 469) — intro and trailing prose inside a tabs block vanish with no warning.
Findings 2 and 5 compound badly: { style = "note" } (spaces around =) parses to no attributes and gets swallowed, so tour/kotlin-tour-welcome.md — the first page of the Kotlin tour — renders its note as a plain blockquote. 7 files hit this.
Also flagging line 278 as a markup-injection hole: broken-ext-link-color is interpolated into an HTML attribute unvalidated. Config is repo-controlled so it isn't remotely exploitable, but it's a one-line fix.
Please note this file is byte-identical to the md_to_json.py in #21 — git diff between the two branches is empty for it. Findings on lines 84, 142 and 278 duplicate comments I left there. Fixing once fixes both, but whichever PR merges second will carry them if only one gets patched. Worth deciding how #21 and #23 relate before either lands.
In fairness, a number of things I went looking for turned out to be fine, and I want to be explicit so this doesn't read as a wall of doom: no _raw leakage into the output JSON (0 corpus-wide), no tag_marker leakage (0), no empty-string heading ids (0), no IndexError on empty table cells, no blank-line stripping in raw <pre> blocks, and the unpinned markdown-it-py is genuinely safe (1.1.0 already exposes Token.attrs as a dict). The overall shape of the converter is sound — the damage is concentrated in the Writerside tag/attribute parsing.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Fixes 12 correctness bugs found by review (verified against a live
kotlin-web-site corpus, not just read-the-code):
- TAG_RE matched "tab" as a prefix of "table", silently eating every raw
HTML table in the corpus (38 occurrences / 18 files).
- merge_attr_lines deleted any {...}-shaped paragraph even when it parsed
to zero attrs, dropping real content.
- <tabs> blocks discarded any non-<tab> sibling (intro/trailing prose).
- A mismatched closing tag_marker was silently ignored, leaving the wrong
frame open and relocating later content into it.
- ATTR_PAIR_RE didn't allow whitespace around "=", and parse_attrs' whole-
string quoted-vs-bare check blanked bare values whenever any sibling in
the same group was quoted.
- fold_image_attrs only handled exactly one trailing {...} group and
required the whole text token to be nothing else, so a second attribute
group or trailing prose leaked as literal visible text.
- broken-ext-link-color was interpolated into a style="..." attribute
unvalidated (markup-injection hole) and unconditionally appended even
when the <a> already had a style=, producing a silently-ignored
duplicate attribute.
- main() swallowed per-file conversion failures and still exited 0.
- The image-src regex rewrote src= on any element, not just <img>,
producing false "image not found" warnings for <script>/<iframe>.
- page_id/sourceFile used str(Path(...)) instead of .as_posix(), which
would disagree with build_topic_index's forward-slashed ids on Windows.
- build_topic_index resolved duplicate topic stems first-wins with no
warning, unlike the equivalent image-filename collision handling.
- Heading lines with a trailing Writerside attribute suffix (most commonly
{id="..."}, also seen as {completion-point=...}) rendered that suffix as
literal visible garbage text, with no attribute handling and no
anchor-override support - found via corpus verification, not flagged by
either reviewer, but the same bug family and comparably common (~39
affected pages).
Also: de-dupes heading ids that collide on identical text (anchor to the
second heading no longer lands on the first); skips re-copying unchanged
images and prunes stale topic JSON left over from a previous run; switches
review_build_json.sh from raw pip/python3 to `uv run` (PEP 668
externally-managed-environment installs were crashing it outright on
Homebrew/Debian/Ubuntu Python).
Adds tests/test_md_to_json.py (30 cases, one per fix above, several reusing
reviewers' own repro snippets) and tests/conftest.py so pytest can find the
module under test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>alexmmiller
commented
Aug 7, 2026
Pushed 6b25e26 addressing both reviews. Replied inline to each of the 15 line comments individually; summary + the two review-level threads here. Fixed (all 12 correctness findings)Every "blocking" item from @hal-eisen-adfa's review is fixed, plus @luisguzman-adfa's suggestion (#3, same bug as the line-142 finding) and minor points (#4 slugify de-dup, #5 duplicate style attr, #6 path separator, #7 README One more bug found during verification (not flagged by either review)While re-running the corpus to confirm the Decision point #1 (heading id vs. anchor mismatch)Didn't fix - it's not a demonstrated bug, no concrete repro was given, and reproducing Writerside's own anchor-slugging algorithm to compare against is a bigger investigation than this PR's scope. Documented as a known limitation in both the module docstring and README instead, so it's tracked rather than silently dropped. Decision point #2 (tests)Added - On #21/#23/#24@hal-eisen-adfa flagged that Worth flagging for @hal-eisen-adfa and @luisguzman-adfa specifically: |
hal-eisen-adfa
left a comment
There was a problem hiding this comment.
Round 2 — all 15 previous findings verified fixed; 5 new ones introduced by the fixes
I re-ran the converter over a fresh 304-topic kotlin-web-site corpus and ran your test suite (30/30 pass, 304/304 converted, exit 0). Every one of the 15 findings from my last pass is genuinely fixed — I verified against real output, not just the diff:
| Finding | Evidence |
|---|---|
TAG_RE ate <table> | 30 in source → 30 in output (was 0); orphan "type":"tab" blocks: 0 |
merge_attr_lines deleted {...} paragraphs | guarded on non-empty parse_attrs |
<tabs> dropped non-tab children | whatsnew24.md intro prose survives |
{ style = "note" } | kotlin-tour-welcome.json now emits attrs: {"style": "note"} |
| bare attrs blanked | 411 populated width="N", 0 blanked; group-key populates |
| image attr braces leaked | 15 → 0 genuine |
color injection / duplicate style= | validated at load_config, merged not appended |
main() exited 0 on failure | exit 1 confirmed, --allow-failures works |
src= on every element | the 8 remaining warnings are all genuine missing .png — no false positives |
| Windows separators | as_posix() throughout |
| image re-copy / stale JSON | ctime unchanged on 2nd run; stale JSON pruned |
Two things I want to be explicit about, because you pushed back and you were right:
- I checked your "the 2 residual
{width=hits are inside<!--comments" claim. It holds — both are in commented-out markdown. - I checked
converter.warnings/build_image_index's tuple against #24.find_missing_assets.pydoes read both. That's not dead code and I was wrong to call it that — keeping the signatures stable was the correct call.
Also: finding the heading {id=...} suffix bug yourself, during verification, on a corpus path neither reviewer's checks exercised, is exactly the right instinct. ~39 pages is not a small catch.
The 5 new findings
All five are introduced by this round's fixes — none existed in 7f67b2fb. Two are serious. The pattern worth noting: three of the five are cases where the fix is correct in exactly the case the new test exercises and wrong just outside it.
group_containersnow crashes on a top-level unmatched closer (line 588) — droppedlen(stack) > 1guard,KeyError.rmtreecan delete the source docs (line 812) — nooutput_dir != docs_rootguard. Reproduced.- The
style=merge emits invalid CSS (line 352) — no;separator; both rules get dropped. fold_image_attrseats the separating space (line 382) — minor.COLOR_RE.matchraisesTypeErroron a non-string (line 741) — minor.
Details inline. 1 and 2 are each a couple of lines; I'd want both before this lands, especially since #24 wires this into CI. Everything else here is in good shape — this was a thorough pass and the corpus numbers back it up.
Resolving all 15 previous threads.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
group_containers crashed (KeyError) on a top-level unmatched closing tag since the len(stack) > 1 guard was dropped during the round-1 fix. The topics-dir pruning rmtree could delete the source docs when output_dir resolves to the same tree as docs_root. The duplicate-style merge could produce invalid (dropped) CSS when the existing style had no trailing ";". fold_image_attrs re-stripped the remainder after an image, eating the leading space before trailing prose. load_config raised a bare TypeError instead of its own clean error on a non-string color value. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Blocking: CLAUDE.md's templateId "out of scope" claim was falsified by this same PR (populate_db.py/insert_optimized_media.py both read/write it) - narrowed the claim to the names that are actually absent. The nav-hidden class nav.peb emits was inert (no consuming CSS rule), rendering Writerside-hidden nav entries (e.g. individual tour steps) visible - added the missing docs.css rule. Non-blocking: rewrote the Dokka-plugin-kdoc2json bullet (and decisions log) to reflect that fix/ADFA-4514 is merged, rather than describing it as an outstanding rebase. find_missing_assets.py swallowed per-file scan failures and always exited 0, so a totally broken corpus still looked clean - added a failure counter, --allow-failures flag, and a report line, mirroring md_to_json.py's pattern from #23. Converted README.md, optimize_media.py, and run_e2e_pipeline_test.sh from bare pip/python3 to uv run --with-requirements, and added scour/cairosvg to requirements.txt, matching the repo's established uv convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
alexmmiller
commented
Aug 10, 2026
Pushed 5ec1f60 addressing round 2. Replied inline to each of the 5 new line comments individually; summary here. Fixed (all 5 round-2 findings)
TestsOne regression test per fix, plus tightened |
hal-eisen-adfa
commented
Aug 11, 2026
Code reviewFound 3 issues:
Frequency note, so these can be triaged rather than taken at face value: against a Also confirmed: all round-1 and round-2 review findings are genuinely fixed at 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Self-closing container tags (<tab .../>) had their trailing "/" eaten by TAG_RE's greedy attrs group, so they were recorded as openers that never close - silently nesting the rest of the page inside them. TAG_RE now captures the self-close marker in its own group, and the html_block dispatch emits an immediate open/close pair for one. Indented (4-space) code blocks produced markdown-it's "code_block" token, which convert_node had no case for - they fell through to the generic fallback and rendered as unescaped raw HTML instead of a typed code block. Added a code_block case alongside the existing fence one. The documented "image" block type was never actually emitted - images are inline-only in markdown-it, always folded into their containing block's own html. Fixed the docstring/README to match reality instead of documenting a block shape that can't occur. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
alexmmiller
commented
Aug 12, 2026
Pushed 5336ee5 addressing all 3.
38/38 tests pass (34 previous + 4 new). |
hal-eisen-adfa
left a comment
There was a problem hiding this comment.
Automated review at high effort, run against the live corpus rather than the diff alone: I sparse-cloned JetBrains/kotlin-web-site (303 topics, 7,863 links), ran the converter over it, and audited the output. Every finding below is reproduced.
Baseline health is good — 303/303 topics convert with only upstream-broken-link warnings, no schema leaks (_raw, tag_marker, and tabs-without-tabs are all zero), and the 38-test suite passes.
1 HIGH, 3 MEDIUM, 7 LOW inline. The HIGH is the one that matters: slugify() strips punctuation instead of hyphenating it, which breaks 209 real cross-references. The rest are mostly latent or documentation-contract issues.
Things I specifically tried to break and couldn't: all four bug classes fixed across the earlier review rounds hold up on the live corpus — TAG_RE's (?![\w-]) correctly rejects <table>, self-closing <tab/> yields a balanced marker pair, merge_attr_lines preserves { it.length }-style paragraphs (1 real occurrence, in functions.md), and the style-attribute merge produces valid CSS. A 32-vs-31 heading discrepancy I chased in compatibility-guide-17.md turned out to be a heading inside an HTML comment — correct behavior, not a loss.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
HIGH: slugify() deleted punctuation instead of hyphenating it, diverging
from Writerside's own anchor algorithm on any heading with "." "/" ":"
etc. - 209 dead #anchor links measured on the live kotlin-web-site
corpus, down to 15 with this fix.
MEDIUM: fold_image_attrs set folded_any on a regex match rather than a
successful parse, so a non-attribute "{...}" group (a CSS class
shorthand, or prose) was swallowed instead of left as visible text -
same guard merge_attr_lines/extract_trailing_attrs already apply.
Also extended fold_image_attrs to handle a trailing "{...}" group
after a *link*, not just an image/heading (real hit in
js/js-ir-compiler.md). group_containers now warns when a container is
still open at EOF, symmetric with the existing unmatched-closer warning.
LOW: MD_LINK_RE and resolve_image_src now resolve a ".md"/image
reference that carries a path prefix by bare filename (Writerside's own
convention) instead of silently failing to match/resolve with no
warning; also drops resolve_image_src's dead startswith("/") check.
Blank lines inside a raw HTML run (e.g. <pre>, or between <table> rows)
are preserved instead of dropped. main()'s output-aliases-source guard
now runs before theme.json/images are written, not just before the
topics rmtree. images/ now prunes stale files the same way topics/
JSON already did, via a new _prune_stale_files. Documented that "html"
blocks are passthrough fragments that may be structurally unbalanced
across block boundaries. markdown-it-py pinned with a >=2.0 floor
(Token.attrs was a list before that version).
13 new regression tests; 55/55 pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>alexmmiller
commented
Aug 24, 2026
Pushed 4fddb34 addressing all 11 findings from the latest automated review. Replied inline to each thread individually; summary here. HIGH (1)
MEDIUM (3)
LOW (7)
Tests13 new regression tests, several reusing your exact repro snippets/corpus examples. 55/55 pass (42 previous + 13 new). |
All six are latent: the live kotlin-web-site corpus triggers none of them, and
converting all 303 pages before and after produces byte-identical JSON with an
identical warning set. Each is a case the corpus happens not to contain today.
- An explicit {id="..."} only beat a *later* auto-slug. In the other order
(`## Custom anchor` then `## Something {id="custom-anchor"}`) the auto-slug
claimed the id first and both headings shipped id="custom-anchor", so any
link to the explicit one landed on the auto one. reserve_explicit_heading_ids
now registers every explicit id up front, before any heading converts, so
explicit wins in both directions. Scoped to inlines directly inside a
heading - a paragraph merely ending in "{id=...}" is not an anchor. Two
genuinely identical explicit ids are a source bug this can't fix, so both
are kept as authored and it warns.
- A heading whose text is all punctuation ("## ...") slugified to "", i.e.
id="" - invalid HTML and unlinkable. Falls back to "section", numbered by
the existing de-dup loop.
- extract_title matched a `[//]: # (title: ...)` comment inside a fenced code
block, which both set a bogus title and deleted that line out of the code
sample being displayed. It now skips matches inside ``` / ~~~ fences,
handling tilde fences, longer fences nesting shorter ones, and an
unterminated fence running to EOF.
- A raw html_block run of nothing but blank lines emitted
{"type": "html", "html": ""} - a truthy list of empty strings. Blank lines
inside a run that has content are still preserved, which is what keeps
<pre>/<script> bodies intact.
- COLOR_RE's flat {3,8} accepted #12345 and #1234567, which no browser does.
Narrowed to the lengths CSS defines. This value is interpolated into a
style="" attribute, so the validator should mean what it says.
- build_tree popped an already-empty stack on a stray closing token, turning
a malformed stream into "IndexError: pop from empty list" with nothing
naming the cause. Defensive - markdown-it always balances - but it now
ignores the stray closer and says so.
Docstring: the "Block shapes" list omitted note/tip/warning, which
group_containers genuinely emits (README already documented them). Also records
that a <tab> written outside any <tabs> comes through as a bare "tab" block a
template won't render - verified, and it does not warn.
Tests: 55 -> 91, coverage 66% -> 81% (main() is subprocess-tested, so
under-reported either way). Beyond one regression test per fix, this closes the
coverage gaps the review named: %variables% substitution and load_variables had
no test at all; block-level <note> was covered by nothing, since the corpus only
uses the single-line form; and both <tabs> fallback paths - synthesizing tabs
from adjacent code fences, and dropping a wrapper with nothing tab-like - were
untested despite each having fixed a content-dropping bug. Adds direct tests for
the blockquote/list/table/hr converters and a guard that "_raw" never reaches
the JSON.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>…line) Brings in the ADFA-5153/ADFA-5171 work merged to fix/ADFA-4737 via PRs #26 and #27, which this branch forked from #21 too early to receive. Without it the pipeline cannot run against the current production database at all: ~/documentation.db is schema 2.0.0, every "brotli" Content row is compressed against the shared 256 KiB raw LZ77 dictionary in CompressionDictionary, and plain Brotli cannot decode any of it (measured: 0 of 24 sampled rows). Conflict resolution - all twelve were add/add, so each was decided per file rather than 3-way merged: Took theirs (the dictionary lineage is strictly ahead on these three), then re-applied this branch's review fixes on top: - populate_db.py: DictionaryCompressor, train/load_or_create_dictionary, fragment_chain, page_size pinning. Re-applied the conversion-failure abort, the same-stem dedupe, and the basename-keyed image index. - insert_optimized_media.py: dictionary-aware reads/writes. Re-applied the delete_unreferenced_media floor check, the delete-before-insert ordering, and --dry-run. - sync_kdoc_json_to_db.py: DictionaryBrotli, load_compression_dictionary, MAX_DELETE_FRACTION. Re-applied CHUNK_SIZE fragmentation, the fatal unknown-contentTypeID, and the VACUUM INTO backup. Took ours (PR #23/#24 refined these after the split): md_to_json.py, find_missing_assets.py, optimize_media.py, assets/docs.css, README.md, run_e2e_pipeline_test.sh, .gitignore. Hand-merged: build-kotlin-docs.yaml (our corrected requirements/webp comments plus their brotli-CLI rationale); CLAUDE.md (ours, with the 2.0.0 blocker note rewritten as a description of how the three writers now handle the dictionary, since the merge resolves it). Two of this branch's own fixes were dropped as superseded: - like_escape/ESCAPE '\' is replaced by fragment_chain, which does the over-matching LIKE once and re-checks each candidate's digit suffix. That also handles ADFA-5171 chains numbered from -2, which escaping does not. sync_kdoc_json_to_db.fragment_paths was rewritten to match rather than probing "-1" and stopping at the first gap. - The hand-rolled DictionaryCompressor added to the sync script last commit is replaced by theirs. Tests updated for the merged APIs (collect_referenced_media and delete_unreferenced_media now take a compressor; DictionaryBrotli is compress-only, so its tests decode through the brotli CLI). 105 pass: 78 in ProcessKotlinWebsiteJSON, 27 in scripts/sync_kotlin_stdlib_docs. Verified against a copy of ~/documentation.db: 3,238 stdlib rows rewritten, 12/12 sampled decode against the dictionary, untouched trees unaffected, row count unchanged at 30,649. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings this branch's copy of md_to_json.py up to PR #23's tip. It had been stale since the 1cf41d2 merge: 4fddb34 ("Fix 10 issues from Hal's automated corpus review") and 1d7f6a7 ("Fix 6 latent defects from the md_to_json.py code review") both landed on fix/ADFA-5039 afterwards, and populate_db.py, build_nav.py and find_missing_assets.py all import this module directly. md_to_json.py and tests/test_md_to_json.py are now identical on both branches. One conflict, in requirements.txt, resolved as the union: this branch added scour/cairosvg for optimize_media.py, #23 pinned markdown-it-py>=2.0. 158 tests pass (131 in ProcessKotlinWebsiteJSON, 27 in sync_kotlin_stdlib_docs). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alexmmiller
commented
Aug 25, 2026
Full code review of |
| # | Defect | Fix |
|---|---|---|
| 1 | An explicit {id="..."} only beat a later auto-slug. In the other order the auto-slug claimed the id first and both headings shipped the same id, so any link to the explicit one landed on the auto one. | New reserve_explicit_heading_ids pass registers every explicit id before any heading converts, so explicit wins in both directions. Scoped to inlines directly inside a heading — a paragraph merely ending in {id=...} is not an anchor. Two genuinely identical explicit ids are a source bug this can't fix, so both are kept as authored and it warns. |
| 2 | A punctuation-only heading (## ...) slugified to "" — id="" is invalid HTML and unlinkable. | Falls back to section, numbered by the existing de-dup loop. |
| 3 | extract_title matched a [//]: # (title: ...) comment inside a fenced code block, both setting a bogus title and deleting that line out of the code sample being displayed. | New fenced_spans skips matches inside ``` / ~~~ fences — handles tilde fences, longer fences nesting shorter ones, and an unterminated fence running to EOF. |
| 4 | A raw html_block run of nothing but blank lines emitted {"type": "html", "html": ""} (a truthy list of empty strings). | flush_raw requires actual content. Blank lines within a run that has content are still preserved, which is what keeps <pre>/<script> bodies intact. |
| 5 | COLOR_RE's flat {3,8} accepted #12345 and #1234567, which no browser does. | Narrowed to the lengths CSS defines. This value is interpolated into a style="" attribute, so the validator should mean what it says. |
| 6 | build_tree popped an already-empty stack on a stray closing token — IndexError: pop from empty list with nothing naming the cause. | Ignores the stray closer and warns. Defensive; markdown-it always balances. |
How I verified these are safe
Since every finding is latent, the meaningful check wasn't the unit tests — it was a differential corpus run. I converted all 303 pages with the converter at 4fddb348 and again with the fixed one, and diffed:
- 0 differing output files
- identical 57-warning set
So these change nothing about today's output; they only close cases the corpus doesn't currently contain.
On those 57 link warnings, for the record: they're 27 distinct references, and I checked every one against the checkout. All are genuinely absent from topics/ — cross-repo links into kotlinx.coroutines and dokka. No false positives from the resolver.
Docstring
The "Block shapes" list omitted note/tip/warning, which group_containers genuinely emits (the README already documented them, and points readers at the module docstring "for full shapes"). Now added.
Also recorded, after testing it: a <tab> written outside any <tabs> comes through as a bare {"type": "tab"} block that a template written against the documented shapes won't render — and it does not warn. I documented the actual behaviour rather than changing it, since that's beyond the scope of this review. Worth filing separately if you want it to warn.
Tests: 55 → 91, coverage 66% → 81%
Beyond one regression test per fix, this closes the coverage gaps the review turned up — features that had no test at all:
%variables%substitution (substitute_vars/load_variables) — a documented feature that reaches every rendered string.- Block-level
<note>— covered by nothing. Note the corpus can't cover it either: all 5<note>uses inkotlin-web-siteare the single-line form insidereleases.md's hand-written HTML tables, which is the documented passthrough case and behaves correctly. - Both
<tabs>fallback paths — synthesising tabs from adjacent code fences, and dropping a wrapper with nothing tab-like inside. Each had previously fixed a content-dropping bug, and neither was tested. - Direct tests for the
blockquote/list/table/hrconverters, and a guard that the internal_rawkey never reaches the JSON.
(Coverage is under-reported either way — main() is exercised by 10 subprocess tests that coverage can't see.)
Note for #24
fix/ADFA-4739 had a stale copy of md_to_json.py — it merged this branch before 4fddb348 landed. I've synced it (80e234a9); md_to_json.py and its tests are now identical on both branches.
Summary
Adds
md_to_json.py, which converts akotlin-web-site/docscheckout(JetBrains Writerside-flavored Markdown) into the JSON block schema this
project's templating engine renders.
Scope
This is split out of the larger end-to-end Kotlin-docs pipeline in
#21,
which is being separated into two ticket-scoped PRs:
build_nav.py,find_missing_assets.py,populate_db.py, media insertion, and the GitHub Action that loadseverything into
documentation.db. That PR depends on this one mergingfirst, since
populate_db.py/build_nav.pyimportmd_to_json.py.Changes
ProcessDocs/ProcessKotlinDocs/ProcessKotlinWebsiteJSON/md_to_json.py- the converter.config.json- theming config it takes as input.markdown-it-pyadded to rootrequirements.txt.README.mdscoped to this script's usage and output schema.review_build_json.sh- a throwaway reviewer helper (installsrequirements, clones
kotlin-web-site, runs the converter) so you can seereal JSON output with no other setup. Not part of the actual pipeline.
Test plan
review_build_json.shlocally end-to-end against a realkotlin-web-siteclone - converted 304/304 files, output matches thedocumented schema (
topics/**/*.json,theme.json,images/).md_to_json.py/config.jsonmatch their originalsin PR ADFA-4739: Pipeline for producing template-based Kotlin documentation #21 (no edits made during the split).