Repository navigation
CND format 0.5.0: rich text and block containers - #13
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Move the forward edge classes and RawSource into cnd.core.text, add Edge, Mark, RichText and Numbering, and regenerate the schema for the reworded NodeRef description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…onition, layout (proposal 0013) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
… the schema Migrate the six top-level fixtures to 0.5.0 (heading refs into text, grid table to layout, nested list item to nested list node), add fixtures/elements.cnd, regenerate schema/cnd.schema.json, and port test_cnd.py and test_schema.py out of collect_ignore. Two tests added: elements fixture coverage and incoming() reading edges from texts. No tests deleted; no assertion needed loosening. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…h in the context Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…t (proposal 0013) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…ths in the document hash Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…ts twin fixture Field(default_factory=list|dict) left no default in the generated schema, so the section 10 rule could not be followed from the schema alone. Use literal defaults, regenerate the schema and vectors, add elements_explicit.cnd (every default written out) which must hash like elements.cnd, list the defaults in spec section 6, walk iter_all() in Cnd.paginated, clarify iter_nodes path docs, rewrap the NodeRef docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…pty marks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…ries; test paginated on footnote-only CNDs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…core CND 0.5.0 core: rich text, block containers, traversal, validation, hashing
…plain text (proposal 0013) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…ription and table kind Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…HtmlNodeRenderer ported to 0.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…, block cells and layouts; text_only option Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
The list escape-callback test is deleted: render_list_markdown is gone and list escaping stays pinned by the escape-flag test. The tight-list assertion is deleted: proposal 0013 removes the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
… markers from every text, pools hold blocks, status/version/extra, text_only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…r line Under a list, table or quote the marker line read as a lazy continuation or an extra table row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…ences, status/version/extra meta Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…fixture; unpark test_converters Every golden diff is a 0.5 decision: cnd_version, layout flattening and CSS grid, nested lists keeping their own kind, RichText.lang, table.kind, footnotes holding blocks, and the new stylesheet rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…e the fence Strip exactly one trailing newline before the closing fence; regenerate the two Markdown goldens whose code blocks gained a blank line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…section A renderer emits only a heading's line and the body walk visits the section under it, so a heading held by a quote, a list item, an admonition, a table or layout cell, a figure or a footnote lost its whole section, while its markers still printed. expand_headings lists a heading followed by its section, recursively in pre-order; both renderers render container children through it, and both converters render footnote blocks through it. Looseness and tightness are counted on the expanded list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…n its own line A definition's ": ", a task item's checkbox and a linearized row's "Label: " were glued to the first block. Before a fence that turned the rest of the document into code; before a quote or an ordered list the block became paragraph text. The prefix now shares the line only with a paragraph or an image; before any other block it ends its line and the block starts on the next one, at the content indent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A delimiter's inner neighbours were read from the plain value, but when a nested mark opens or closes at the edge the real neighbour is that mark's delimiter or tag. "xab" with emph and strike over "ab" rendered x*~~ab~~*, which a parser reads with literal asterisks. The content is now rendered first and its first and last characters decide; a mark that cannot be delimited still falls back to its tag, or to plain text under text_only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…kers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
With flanking judged on the rendered edge, a strong under text_only can fall back to plain text that ends in a nested emph's "*". The guard against a "*" opener right after a "*" read the syntax chosen, not what was written, so the next emph opened there and the two runs merged. It now reads the rendered text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…y a blank line A tight terms list or linearized row puts the next term or cell on the next line. After a quote or a list that line is a lazy continuation, so the next term or cell was read as part of the block. Anything other than one paragraph or one image now makes the terms list loose, and the row separate its cells with blank lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A links edge with a text_span now makes the text it spans an <a class="cnd-link">, through the inline layer's edge_wrap, which the HTML renderer enables for the links family only; refs, cites and footnotes keep their marker line. A links edge without a span goes to the marker line as a link to its href. Every href goes through one check, cnd.core.urls.is_safe_url: http, https, mailto or no scheme at all, read as a browser reads it (leading controls stripped, every tab and newline removed). An unsafe href is never a link: the spanned text stays plain and a span-less href is written as text. The Markdown link marker uses the same check, escapes its text, and escapes its destination so that it parses back to the href that was checked (no entity or backslash turns into a scheme). A Markdown HTML table renders its cells with links=False: the converter already appends every link to the marker line. Part of CND-36 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
CommonMark and GFM compare link and footnote labels after Unicode case folding, so two CND labels that differ only by case (Fn and fn, ß and SS) could resolve to one definition. markdown_label now keeps a letter, digit or safe punctuation only when case folding leaves it unchanged and percent-encodes everything else, so the mapping stays injective under folding. Goldens are unchanged. Closes CND-38 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…s comparison markdown-it compares labels with lower() then upper(), not Unicode case folding, so dotless i and i still met after the previous commit. A label character is now kept only when case folding, lowering, and an upper-then-lower round trip all leave it unchanged. Tests sweep the whole Basic Multilingual Plane under both comparisons. Part of CND-38 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Keep Markdown labels apart under case-insensitive matching
Proposal 0013 lets a CND omit every field that holds its default. The writer keeps id, required fields and free-form objects; the round trip and the content hash are pinned over the whole fixture corpus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…eclaration Texts take the CND's RichText or a plain string; the link families live in texts, so hyperlinks are declarable; numbering keeps only the authored name; list items, term items, cells and footnotes hold declared nodes and are strict. Version 0.2.0: a 0.1.0 declaration is refused rather than misread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…ilder Every nested node is minted an id; heading_path is threaded through every container; the counter engine numbers headings, figures and equations in full-walk order and keeps only authored words. Authored numbering with the engine off is a build error (numbering-without-engine), never a silent drop. Goldens are written through Cnd.to_json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…est_reconcile Matching v1 keeps its passes and keys; the node set is the full walk and the structural path is the spec section 8 path, with footnote content keyed by the note's label so a pool reorder moves nothing. The remap and its guards reach every slot and the footnotes. New vector cases: an edited list item, two cells as two slots, footnote content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…s id MATCHER_VERSION is now v2: the same 0.5 inputs pair differently from the previous matcher (spec section 8 path, full walk, footnote content keyed by its note's label). A node hash excludes nested nodes, so two containers of one shape hash alike; in the two hash passes a node with child slots is keyed on its node hash plus a digest of its descendants' hashes. The spec node hash is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A heading whose section changed has not itself changed (spec section 10), so a moved heading must stay recognisable. The nested-content digest applies only to list, terms, table, layout, quote and admonition, whose own hash carries little content; headings and figures already hash their text or caption. Restores the original moved-into-a-section expectation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
A container whose nested content was edited and which also shifted position missed every earlier pass and lost its id. After path+type, the still-unmatched list, terms, table, layout, quote and admonition pair on their own node hash without the digest; twins have already paired through the digest. Also fixes a stale "v1" in a test docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
matching.json is the cross-implementation oracle, so every pass needs a vector. Case 11: a table with one cell edited and a paragraph inserted above it pairs on its node hash alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…test_visitor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…itor Admonition and layout panels, rich-text details (marks, every link family read from the node's texts, hyperlinks by href), numbering as number and counter, task-list counts, code titles, figure descriptions, document status, version and extra, and footnote blocks in the pool panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
The document panel and the pool panels passed CND values (title, status, version, extra, footnote and bibliography text) to Rich as plain strings, which Rich parses as markup: a stray closing tag crashed the render. They are wrapped in Text now. The document grid also gets a column gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
hash --nodes lists the full walk, as fixtures/hashes.json does; build writes through Cnd.to_json; inspect displays every 0.5 node type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…013) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…hyperlinks; empty parking list Every test module runs again. Spec section 12 states the shorthand, the authored numbering name and the build error without the engine, the moved strictness boundary, and that hyperlinks are declarable through rich text. The CND-26 wording items are fixed (paragraph example id, "rendered text"). Proposal 0013 notes that the authoring hint rename supersedes the content_kind part of ADR 0023. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Every read_text and write_text of CND or declaration content in the CLI and the fixture scripts names its encoding, so a non-UTF-8 locale can neither misread a file nor write one another implementation cannot read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
… shape A 0.1 declaration failed on "Extra inputs" errors that never named its version, and `cnd validate` printed ok for a CND stamped 0.4.0 or 9.9.9. - The CLI loaders read cnd_version and declaration_version from the raw data first and refuse another version by name, with the supported one. - Declaration refuses another declaration_version in a before-validator, so a caller of model_validate gets the same single error. - validate() gains cnd-version-unsupported for a CND built in code with another version; spec section 9 lists it. Tests use main's own minimal.cnd (stamped 0.4.0, a shape 0.4 parsed unchanged) and article.decl.yaml (0.1.0) as real old documents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
build() handed source, doc, raw and nested state_metadata values to the CND as the declaration's own objects. Mutating the declaration after a build silently edited the earlier CND, and diff(c1, c2) then reported nothing changed. build() now works on a deep copy of the declaration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
After `cnd.metadata.update(it => it + (kind: "report", revision: "1"))` and one `cnd.table(..., kind: "data")`, the state was `(revision: "1")`: the hint's cleanup removed the author's document-level kind, and an inner hint removed an outer one. The hint now saves the previous value in a context before its update and restores it after the table, or removes the key only when it was absent. The body stays outside the context, at the call site. The new tests compile a document with `typst query` and skip when the typst binary is missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…aths
The BaseVisitor docstring told users to walk cnd.footnotes[i].children,
which yields ("nodes", 0) paths that name a body node. visit() gains
full=True, which runs Cnd.iter_all(): the body, then each footnote's
content, with spec section 8 paths and slot "footnotes". The default
stays the body walk. iter_all() now takes max_depth and stop_predicate,
so should_stop_descent prunes inside footnotes too.
Spec section 8 no longer claims `cnd inspect` shows the order a CND
walks in: it shows the body walk only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…declaration 0.2.0 only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
ADR 0029 extends ADR 0018 and records what matching v2 computes. It covers the full walk, spec section 8 paths, footnote content keyed by its note's label, and the container key: node hash plus a descendants digest, with headings and figures left out. It also covers the last pass, empty containers and the known limits. The docs index marks 0018 "extended by 0029". The last pass, which pairs leftover containers on their node hash alone, now reports matched_by "container-hash" (PASS_CONTAINER_HASH) instead of "hash", so a caller can tell it from an unchanged move. In matching.json, the one changed hunk is case 11's table. The match.py docstring's numbered passes now match its prose. The README, the CLI and the diff docstring point at ADR 0029. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
This reverts commit 6e08449.
The author still writes `cnd.table(..., kind: "data")`. The state now carries the hint under the dedicated key `table-kind`, which an emitter reads and the CND writes as `table.kind`. A document-level `kind` the author sets therefore never collides with the hint. The hint keeps the plain set-then-unset pattern: nothing is read back, so no context update has to settle. Compile tests run with the typst binary, and skip without it: - five sequential hinted tables, failing on any convergence warning; - a document-level kind that survives two hinted tables, while each table sees its own table-kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
…state key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS
CND 0.5.0 production: declaration 0.2.0, builder, matching v2, CLI, visitors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings CND format 0.5.0 to
main: the proposal 0013 implementation, landed on the integration branch through five reviewed pull requests (#8, #9, #10, #11, #12).What 0.5.0 changes
RichTextwith stand-off marks (strong, emph, code, strike, sup, sub, underline, highlight, math, or an open kind), references, citations, footnotes and links.children. Newadmonitionandlayoutnodes.Numbering{value, name},table.kind(data/content/ null),checked,figure.description,code.title,doc.status/doc.version/doc.extra;tight,content_kind,table.rawandcounter_labelare gone.Cnd.to_jsonomits defaults, declaration 0.2.0, builder, matching v2 (ADR 0029), CLI, visitors,cnd.typkind:hint under thetable-kindstate key.0.5.0 is a breaking release (ADR 0027): 0.4 documents are refused with a message naming their version.
Before tagging
The package version is not bumped here; the release (version bump and tag) follows the merge.
Closes CND-15
🤖 Generated with Claude Code
https://claude.ai/code/session_01XxXUiDn2QyzNTYnZpeVFVS