feat: a composite figure is an editable figure in the editor - #17
Merged
Conversation
markup-carve/carve-grammars#225 gave the Tiptap layer a `carveFigureGroup` node and moved its engine pin onto a carve-js build that parses a bare `::: figure` into a `figure_group`. Both halves were the blocker; this bumps the pin onto them and wires the editor up. A group now loads as one editable node: its direct `carveFigure` and table children are the panels in source order, the `^ ` line below the closing fence is the group's own caption, and everything else in the body stays where it was written. Before this, the whole document became one opaque `carveUnsupported` atom whose source lived in the document envelope - lossless to load and save, and gone on the first edit anywhere. `tests/composite-figure.test.ts` said in as many words that it would go red the day carve-grammars gave the group a schema entry, and that this was the signal to model it here. It went red, and it now asserts the rich shape, the round trip through the app's own import and serialize path, and an EDIT - the case the opaque atom could never survive. The titled and labelled openers, which are a different production, keep their old reading; that pair used to prove nothing because the engine could not tell them apart, and the test now asserts the two READINGS DIFFER so it fails rather than agrees with itself if the pin ever moves back. THE BUMP ALSO SURFACED A SILENT REGRESSION, and it is not about figures. `pruneDefaults` in src/editor.ts removed attributes the editor materialized from schema defaults, so an unedited document could be recognized and its source envelope honored - but it only removed NULL ones. A default does not have to be null: carve-grammars#221 gave `carveCaption` a `short` attribute defaulting to `false`, so every document holding a caption came back carrying `{"short":false}`, never compared equal, lost its envelope, and was written back from the lossy rich model. The visible loss was `{#fig-x}` above a titled `::: figure` disappearing on save. Nothing failed - the round trip just became lossy, which is the failure the envelope exists to prevent. The fix compares the mounted document with the one that was loaded, both reduced to what the author wrote, and hands the serializer the ORIGINAL document when they match. Reducing BOTH sides is the other half: the bridge does set some attributes to a value that is also the schema default (`carveComment` writes `block: false` for a `%%` line), so pruning only the mounted side would break exactly the documents pruning exists to keep. Handing over the original rather than a reconstruction of it is what makes the envelope usable at all - its fingerprint was taken over that exact JSON. Also here: - Preview and editor styles for the group. The preview pane holds the engine's HTML (`figure.carve-figure-group` around `div.carve-figure-panels`) and the editor holds CarveKit's node, which has no panels row, so both shapes are styled to read as one figure. - `tests/blockquote-attribution.test.ts` is `blockquote-caption.test.ts`. Its assertions were already shape-independent and passed unchanged; its docblock claimed the engine carries a quote's caption as an `attribution` field, which PART 9 §4a said and §4b withdrew (markup-carve/carve#1213). A captioned quote is a `figure` whose target is the quote, and the loader builds the `carveFigure`/`carveCaption` pair for it. - Two README limitations retired. The composite-figure one is this change. The `{:TAG}`-in-the-source-pane one was already false before it - carve-wysiwyg#14 moved the pin onto a build that parses it, and `tests/language-attribute.test.ts` has asserted it since - so the README was contradicting a passing test.
…ounted one
Reducing only the mounted document was a mutation the suite did not
notice. It is a real failure: the bridge sets some attributes to a value
that is also the schema default - `carveComment` declares `block` with a
default of `false` and writes `block: false` for every `%%` line - so a
one-sided reduction makes those documents stop matching their own loaded
form and discards the envelope they depend on.
The pair is what pins it. A `{#fig-x}` above a `::: note` passes either
way; the same document with a `%%` line inside loses the attribute line
under a one-sided reduction. A third case edits the document and asserts
the output CHANGED, so a serializer that simply returned the loaded
source could not pass the first two.
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.
Closes #15.
markup-carve/carve-grammars#225 landed both halves this was waiting on: an
engine pin that parses a bare
::: figureinto afigure_group, and acarveFigureGroupentry in the CarveKit schema. This bumps the pin onto it andwires the editor up.
loads as one editable
carveFigureGroup: its directcarveFigureand tablechildren are the panels in source order, the
^line below the closing fenceis the group's own caption, and anything else in the body stays where it was
written. Before this the whole document became one opaque
carveUnsupportedatom - lossless to load and save, and gone on the first edit anywhere.
tests/composite-figure.test.tssaid, in as many words, that it would go redthe day carve-grammars gave the group a schema entry and that this was the
signal to model it here. It went red. It now asserts the rich shape, the round
trip through the app's own import and serialize path, and an edit, which is
the case the opaque atom could never survive.
The titled and labelled openers are a different production and keep their old
reading. That control used to prove nothing - the engine could not tell the two
spellings apart, so both parsed to the same generic container and the pair
agreed with itself. The test now asserts the two READINGS DIFFER, so it fails
rather than passes if the pin ever moves back.
The bump surfaced a silent regression, and it is not about figures
pruneDefaultsinsrc/editor.tsstripped attributes the editor materializedfrom schema defaults, so an unedited document could be recognized and its source
envelope honored. It stripped only the null ones.
A default does not have to be null. markup-carve/carve-grammars#221 gave
carveCaptionashortattribute defaulting tofalse, so from that pinonward every document holding a caption came back carrying
{"short":false},never compared equal to the document it was loaded from, lost its envelope, and
was written back from the lossy rich model. The visible loss was
{#fig-x}above a titled
::: figuredisappearing on save. Nothing failed - the roundtrip simply became lossy, which is the failure the envelope exists to prevent.
The check now compares the mounted document with the one that was loaded, both
reduced to what the author wrote, and hands the serializer the original
document when they match. Two things about that:
also the schema default -
carveCommentwritesblock: falsefor every%%line - so reducing only the mounted side breaks exactly the documents the
reduction exists to keep.
envelope's fingerprint was taken over that exact JSON, and the envelope is
only honored while the fingerprint matches, so anything less is a guess at
what the fingerprint will accept.
Also here
HTML (
figure.carve-figure-groupwrappingdiv.carve-figure-panels) and theeditor holds CarveKit's node, which has no panels row; both are styled so the
group reads as one figure on either side of the split.
tests/blockquote-attribution.test.tsis nowblockquote-caption.test.ts.Its assertions were deliberately shape-independent and passed unchanged, but
its docblock claimed the engine carries a quote's caption as an
attributionfield on
block_quote. PART 9 §4a said that; §4b withdrew it(spec: withdraw the quote attribution, a captioned quote is a figure again carve#1213), and a captioned quote is a
figurewhose target isthe quote again - the loader builds the
carveFigure/carveCaptionpair.Verified against the spec's current
main, not from the ticket text.change. The
{:TAG}-typed-in-the-source-pane one was already false before it:fix: bump carve-grammars onto the build that parses {:TAG} and keeps a quote's attribution #14 moved the pin onto a build that parses it and
tests/language-attribute.test.tshas asserted it ever since, so the READMEwas contradicting a passing test.
Proofs
Every claim above was checked in both directions - mutate, watch it go red,
restore from HEAD, watch it go green:
convertFigureGroupreduced to the generic container (an engine without §4c)which is why a test was added
red
The one-sided reduction is worth naming.
{#fig-x}above a::: noteround-trips either way; the same document with a
%%line inside it does not,because that is where an attribute set to its own default appears. The pair in
tests/roundtrip.test.tsis what pins the symmetry, and a third case edits thedocument and asserts the output changed, so a serializer that just returned the
loaded source could not pass the first two.
The spec submodule pin
npm run check:pinswarns that carve-grammars' build was written against a spec53 commits behind the engine's. That gap is inside carve-grammars and no pin
here can move it; bumping its
spec/submodule is 58 commits and 316 corpusfiles, which is its own change. The warning is the watchdog naming the
repository that can close it, which is what it is for.