diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef5e6ce..c3262f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,9 +45,8 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). whose value attribute is absent gives the bare boolean, leftover `id`, `class` and `data-*` ride the same span, and the import report records no loss for any of the seven, in all three modes. `` keeps its `=m=` spelling, inline - `` keeps its code span, `` inside `
` keeps going to a code
-  block, and a `` read as a block quote's attribution stays an
-  attribution. `abbr`, `time` and `kbd` render back as the original element;
+  `` keeps its code span, and `` inside `
` keeps going to a
+  code block. `abbr`, `time` and `kbd` render back as the original element;
   `samp`, `var`, `cite` and `dfn` need `SemanticSpanExtension` registered to do
   so, and render as `` without it.
 
@@ -107,28 +106,6 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
   (§10a), one an authored `abbr` outranks, and one a later definition of the
   same term shadowed.
 
-- **A caption on a block quote is now that quote's attribution** (PART 9 §4a,
-  markup-carve/carve#1159). `> To be` followed by `^ Hamlet` no longer parses as
-  a `figure` wrapping a `block_quote`; it is a `block_quote` carrying an
-  `attribution`, and HTML renders `
Hamlet
` inside the - `
` rather than a `
` / `
` pair. A quote is not - a figure, takes no number, and no longer turns up in a walk for figures. The - Markdown, plain-text, ANSI and Carve writers all carry the attribution, the - AST codec encodes and decodes it, and the ProseMirror bridge carries it in - both directions - without that last one, four corpus documents did not survive - the round trip through the editor model. - -- **The HTML importer reads a quote's `
` back as its attribution.** This - renderer emits the attribution that way, so importing it as a second quoted - paragraph meant the engine's own HTML did not round-trip. The LAST `footer` or - `cite` child wins when a quote carries several, and the earlier one stays - ordinary quoted content rather than being dropped - which is what taking the - first and skipping every other used to do. An element holding BLOCK content is - not an attribution: the slot takes inline content, and flattening blocks into - it would run their paragraphs together with no separator, so it stays quoted - content and every word survives. All three engines agree byte for byte on both - rules. - - **Leftover attributes ride the outermost semantic element.** `[Ctrl+C]{kbd .shortcut #copy}` is `Ctrl+C` where it was a `` @@ -239,27 +216,6 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). as a single byte: one tab under a two-column item is that item's content, two are code. Document-level indented code is unchanged. -- **A block quote's source span covers the attribution it owns** (#1249, - PART 12 §4). The span stopped at the end of the last quoted line, so the - attribution's inline nodes sat outside their own parent in the serialized - AST - a tree contradicting itself, which no rendered output could show. - A container "ends after its last placed child", and the attribution is one, - so the span now reaches the end of the `^` line. Four corpus documents - change; a quote with no attribution is unaffected. Consumers indexing a - quote by `pos` see the wider extent. - -- **A quote attribution stays attached to its quote on every target** - (markup-carve/carve#1179, PART 11 §10c). It used to follow the quote as a - sibling separated by a blank line, which kept the words but not what they - mean - read back the attribution was attached to nothing, and a round trip - produced a blockquote with no attribution at all. Markdown now emits a - `
` element inside the quote (that target already writes ``, - `` and `` where Markdown has no spelling, and through a CommonMark - reader `
` opens an HTML block rather than being wrapped in a - paragraph, so the rendered HTML matches the HTML target's); the terminal - carries its quote bar onto the attribution line; plain text attaches by - adjacency, dropping the blank line. A quote with no attribution is unchanged. - - **Presentation targets no longer discard authored text** (PART 11 §10e, markup-carve/carve#1179). `docs/graceful-degradation.md` states the floor as a MUST - "losing the click is fine; losing the words is not" - and three kinds diff --git a/resources/ast-schema.json b/resources/ast-schema.json index 210435f1..a7bbc5ab 100644 --- a/resources/ast-schema.json +++ b/resources/ast-schema.json @@ -611,13 +611,6 @@ "$ref": "#/$defs/blockNode" } }, - "attribution": { - "description": "The source of the quotation (PART 9 \u00a74a). Present when a `^` caption attaches to the quote; it is not a figure caption and takes no number.", - "type": "array", - "items": { - "$ref": "#/$defs/inlineNode" - } - }, "attrs": { "$ref": "#/$defs/attrs" }, @@ -1084,11 +1077,14 @@ "const": "figure" }, "target": { - "description": "The captioned block: an image, table, code block or paragraph. A captioned block quote is NOT a figure - it is a quote carrying an `attribution` (PART 9 \u00a74a, carve#1159).", + "description": "The captioned block: an image, block quote, table, code block or paragraph. What a captioned host is called and counted as comes from the caption's label, never from the host (PART 9 \u00a74b); for a quote the HTML Standard also requires the attribution outside the `blockquote`.", "oneOf": [ { "$ref": "#/$defs/image" }, + { + "$ref": "#/$defs/block_quote" + }, { "$ref": "#/$defs/table" }, diff --git a/src/Ast/AstCodec.php b/src/Ast/AstCodec.php index c3842d02..8f028a7b 100644 --- a/src/Ast/AstCodec.php +++ b/src/Ast/AstCodec.php @@ -1861,15 +1861,6 @@ private static function spanShape(array $encoded): array */ private static function captionShape(array $encoded): array { - // PART 9 §4a: a quote's `attribution` is inline content on the wire for - // the same reason a caption is - the reference has no `caption` node - // type, and this engine stores one internally. Flattened here so both - // fields answer to one rule rather than two (carve#1159). - $attribution = $encoded['attribution'] ?? null; - if (is_array($attribution) && ($attribution['type'] ?? null) === 'caption') { - $encoded['attribution'] = $attribution['children'] ?? []; - } - $caption = $encoded['caption'] ?? null; if (!is_array($caption) || ($caption['type'] ?? null) !== 'caption') { return $encoded; @@ -2352,18 +2343,6 @@ private static function captionFromWire(array $data): array $data['caption'] = ['type' => 'caption', 'children' => $caption]; } - // PART 9 §4a: the mirror of the flattening in `captionShape`. On the - // wire an attribution is inline content; this engine models it with the - // same `caption` block it uses for a figure's caption (carve#1159). - $attribution = $data['attribution'] ?? null; - if ( - ($data['type'] ?? null) === 'block_quote' - && is_array($attribution) - && !isset($attribution['type']) - ) { - $data['attribution'] = ['type' => 'caption', 'children' => $attribution]; - } - return $data; } diff --git a/src/Converter/HtmlToCarve.php b/src/Converter/HtmlToCarve.php index a8ce56c4..bd96977f 100644 --- a/src/Converter/HtmlToCarve.php +++ b/src/Converter/HtmlToCarve.php @@ -1598,25 +1598,13 @@ protected function processHr(DOMNode $node): string protected function processBlockquote(DOMElement $node): string { - // Check for attribution (footer or cite element) - $attributionNode = $this->findBlockquoteAttribution($node); - $attribution = $attributionNode !== null - ? trim($this->processChildren($attributionNode)) - : null; - - // Process content excluding attribution elements, preserving paragraph breaks + // Process content, preserving paragraph breaks. $parts = []; foreach ($node->childNodes as $child) { if ($child instanceof DOMText && trim($child->textContent) === '') { continue; } - // Skip the ONE element read as the attribution. Any other footer - // or cite is ordinary quoted content and stays in the body. - if ($child === $attributionNode) { - continue; - } - $part = rtrim($this->processNode($child), "\n"); if ($part !== '') { $parts[] = $part; @@ -1631,64 +1619,11 @@ protected function processBlockquote(DOMElement $node): string $quoted[] = $line === '' ? '>' : '> ' . rtrim($line); } - // A `footer`/`cite` is the quote's ATTRIBUTION, and Carve spells that as - // a caption line BELOW the quote (PART 9 §4a, carve#1159) - not as a - // second quoted paragraph, which is ordinary quoted content and comes - // back as such. Without this the renderer's own `
` - // did not survive a round trip through this importer. - // - // A caption folds its continuation lines like a paragraph, so a - // multi-line attribution needs the marker on its first line only. - if ($attribution !== null) { - $first = true; - foreach (explode("\n", $attribution) as $line) { - $quoted[] = ($first ? '^ ' : '') . rtrim($line); - $first = false; - } - } - $attrs = $this->formatBlockAttributes($node); return $attrs . "\n" . implode("\n", $quoted) . "\n\n"; } - /** - * The `footer`/`cite` child that carries the quote's attribution, if any. - * - * The LAST one, because that is the element this renderer emits and the one - * an author puts after the quoted text. A quote has ONE attribution and the - * slot holds inline content, so an earlier footer cannot join it - it stays - * ordinary quoted content rather than being dropped, which is what taking - * the first one and skipping every other did. - */ - protected function findBlockquoteAttribution(DOMElement $node): ?DOMElement - { - $found = null; - foreach ($node->childNodes as $child) { - if (!$child instanceof DOMElement) { - continue; - } - - $tag = strtolower($child->tagName); - if (($tag !== 'footer' && $tag !== 'cite') || trim($this->processChildren($child)) === '') { - continue; - } - // The slot holds INLINE content, so an element carrying blocks does - // not fit it. Flattening one would run its paragraphs together with - // no separator; leaving it an ordinary block inside the quote keeps - // every word, which is the better answer when the shape cannot be - // represented. - foreach ($child->childNodes as $inner) { - if ($inner instanceof DOMElement && in_array(strtolower($inner->tagName), $this->blockElements, true)) { - continue 2; - } - } - $found = $child; - } - - return $found; - } - /** * Process MathML element to Djot math syntax * diff --git a/src/Node/Block/BlockQuote.php b/src/Node/Block/BlockQuote.php index a5940702..fc323d7a 100644 --- a/src/Node/Block/BlockQuote.php +++ b/src/Node/Block/BlockQuote.php @@ -9,29 +9,8 @@ */ class BlockQuote extends BlockNode { - /** - * The source of the quotation (PART 9 SS4a). - * - * A `^` caption on a quote is its ATTRIBUTION, not a figure caption: the - * quote is not a figure, takes no number, and nothing walking the tree for - * figures finds it (carve#1159). - * - * @var \MarkupCarve\Carve\Node\Block\Caption|null - */ - protected ?Caption $attribution = null; - public function getType(): string { return 'block_quote'; } - - public function getAttribution(): ?Caption - { - return $this->attribution; - } - - public function setAttribution(?Caption $attribution): void - { - $this->attribution = $attribution; - } } diff --git a/src/Node/Block/Figure.php b/src/Node/Block/Figure.php index 94b034b0..9bfbb34a 100644 --- a/src/Node/Block/Figure.php +++ b/src/Node/Block/Figure.php @@ -9,7 +9,7 @@ * * Used to wrap: * - Images with captions →
...
...
- * - Blockquotes with attribution →
...
...
+ * - Block quotes with captions →
...
...
* * Tables with captions use the element inside the table instead. */ diff --git a/src/Parser/BlockParser.php b/src/Parser/BlockParser.php index ab112d1c..b621171b 100644 --- a/src/Parser/BlockParser.php +++ b/src/Parser/BlockParser.php @@ -9209,35 +9209,29 @@ protected function tryParseCaption(Node $parent, array $lines, int $start): ?int return $linesConsumed; } - // Handle BlockQuote - the caption is its ATTRIBUTION (PART 9 SS4a) + // Handle BlockQuote - wrap in figure if ($lastChild instanceof BlockQuote) { - // NO FIGURE WRAPPER. A quote carrying a source is not a figure: it - // takes no number, and nothing walking the tree for figures finds - // it (carve#1159). The quote keeps its own attributes for the same - // reason - there is no outer node to move them to. - $attribution = new Caption(); - $attribution->setPos($this->wholeLineSpan($start)); - // PARSED WITHOUT CAPTION CONTEXT: a caption's bare `#` is the - // number placeholder, and an attribution has no number to place, so - // SS4a keeps it literal. + $figure = new Figure(); + + foreach ($lastChild->getAttributes() as $key => $value) { + $figure->setAttribute($key, $value); + $lastChild->removeAttribute($key); + } + + $caption = new Caption(); + $caption->setPos($this->wholeLineSpan($start)); $this->inlineParser->parse( - $attribution, + $caption, $captionText, $start, - false, + true, $this->contiguousMapFor($start, $lines[$start], $captionText), ); - $lastChild->setAttribution($attribution); - // The attribution is written AFTER the quote, and it is the quote's - // own child, so the quote's span has to reach the end of that line. - // PART 12 §4: a span contains its children's spans, and "a container - // ends after its explicit closer when it has one, otherwise after - // its last placed child". Without this the attribution's inlines sat - // outside their own parent - a tree contradicting itself, which no - // renderer could show and which the containment sweep could not see - // either, because the attribution is not in `getChildren()` - // (carve-php#1249). The table arm above widens for the same reason. - $this->widenSpanTo($lastChild, $attribution->getPos()); + + $figure->appendChild($lastChild); + $figure->appendChild($caption); + + $parent->replaceChild(count($children) - 1, $figure); return $linesConsumed; } diff --git a/src/ProseMirror/ProseMirrorRenderer.php b/src/ProseMirror/ProseMirrorRenderer.php index 554dbae5..406e1462 100644 --- a/src/ProseMirror/ProseMirrorRenderer.php +++ b/src/ProseMirror/ProseMirrorRenderer.php @@ -4,7 +4,6 @@ namespace MarkupCarve\Carve\ProseMirror; -use MarkupCarve\Carve\Node\Block\BlockQuote; use MarkupCarve\Carve\Node\Block\CodeBlock; use MarkupCarve\Carve\Node\Block\Div; use MarkupCarve\Carve\Node\Block\Footnote; @@ -204,20 +203,6 @@ protected function renderBlock(Node $node): ?array : $this->renderBlocks($node->getChildren()); } - if ($node instanceof BlockQuote) { - // Same asymmetry as a table caption, at the other end: a quote's - // attribution is state rather than a child (PART 9 §4a), so walking - // children alone loses the source of the quotation and nothing - // reports it. It goes LAST, where the author wrote it. - $attribution = $node->getAttribution(); - if ($attribution !== null) { - $content[] = [ - 'type' => 'carveCaption', - 'content' => $this->renderInlines($attribution->getChildren(), []), - ]; - } - } - if ($content !== []) { $out['content'] = $content; } diff --git a/src/ProseMirror/ProseMirrorToCarve.php b/src/ProseMirror/ProseMirrorToCarve.php index b0bffc06..888bf184 100644 --- a/src/ProseMirror/ProseMirrorToCarve.php +++ b/src/ProseMirror/ProseMirrorToCarve.php @@ -355,15 +355,6 @@ protected function buildBlock(array $data): ?Node continue; } - // The trailing `carveCaption` a quote carries is its ATTRIBUTION - // (PART 9 §4a), which is state on the quote rather than a child - - // appending it would publish a `caption` block inside the quote, - // which no schema names. - if ($built instanceof Caption && $node instanceof BlockQuote) { - $node->setAttribution($built); - - continue; - } $node->appendChild($built); } diff --git a/src/Renderer/AnsiRenderer.php b/src/Renderer/AnsiRenderer.php index e598a9d9..72b5f554 100644 --- a/src/Renderer/AnsiRenderer.php +++ b/src/Renderer/AnsiRenderer.php @@ -880,30 +880,6 @@ protected function renderBlockQuote(BlockQuote $node): string $content = $this->renderChildren($node); $this->blockQuoteDepth--; - // Keeps the styling the caption had while a quote was a figure, so a - // terminal reader sees the same thing in a different place. - // - // PART 11 section 10c T2: it also carries the QUOTE BAR. The bar is - // already this target's marker for "inside the quote", and the - // attribution was the one line in the quote that did not get it - so the - // source read as a separate block that merely happened to follow. - // Nothing new is invented; the prefix the body lines already use is - // applied one line further. The caption's own trailing separator is - // trimmed BEFORE prefixing, or the bar would be drawn on blank lines and - // the quote would appear to continue past its end. - $attribution = $node->getAttribution(); - if ($attribution !== null) { - $this->blockQuoteDepth++; - $bar = $this->getBlockQuotePrefix(); - $this->blockQuoteDepth--; - $rendered = rtrim($this->renderCaption($attribution), "\n"); - $prefixed = implode("\n", array_map( - fn (string $line): string => $bar . $line, - explode("\n", $rendered), - )); - $content = rtrim($content, "\n") . "\n" . rtrim($bar) . "\n" . $prefixed . "\n\n"; - } - return $content; } diff --git a/src/Renderer/CarveRenderer.php b/src/Renderer/CarveRenderer.php index a392d211..a43637ff 100644 --- a/src/Renderer/CarveRenderer.php +++ b/src/Renderer/CarveRenderer.php @@ -776,13 +776,7 @@ protected function renderBlockQuote(BlockQuote $node): string $quoted = implode("\n", array_map(static fn (string $line): string => $line === '' ? '>' : '> ' . $line, $lines)); - // PART 9 SS4a: an attribution is written back as the `^` line it was - // read from. Dropping it would lose content, which PART 11 SS1 forbids. - $attribution = $node->getAttribution(); - - return $attribution === null - ? $quoted - : $quoted . "\n^ " . $this->renderInlines($attribution->getChildren()); + return $quoted; } protected function renderList(ListBlock $node): string diff --git a/src/Renderer/HtmlRenderer.php b/src/Renderer/HtmlRenderer.php index 10e2f8f8..843d1453 100644 --- a/src/Renderer/HtmlRenderer.php +++ b/src/Renderer/HtmlRenderer.php @@ -1250,29 +1250,16 @@ protected function renderBlockQuote(BlockQuote $node): string } } - // PART 9 SS4a: the attribution renders INSIDE the quote, where a - // quotation's source belongs, rather than as a figcaption on a figure - // wrapping it (carve#1159). Its presence also forces the expanded form, - // because the compact one has nowhere to put a second element. - $attribution = ''; - $attributionNode = $node->getAttribution(); - if ($attributionNode !== null) { - $attribution = '
' . $this->renderChildren($attributionNode) . "
\n"; - } - if ( - $attribution === '' - && count($visible) === 1 + count($visible) === 1 && $visible[0] instanceof Paragraph && !$this->isBlockImageParagraph($visible[0]) ) { return '' . $inner . "
\n"; } - $body = $attribution === '' ? $inner : $inner . "\n" . rtrim($attribution, "\n"); - return '\n" - . $this->indentBlock($body, 2) . "\n
\n"; + . $this->indentBlock($inner, 2) . "\n\n"; } /** diff --git a/src/Renderer/MarkdownRenderer.php b/src/Renderer/MarkdownRenderer.php index 064acc55..2619ab89 100644 --- a/src/Renderer/MarkdownRenderer.php +++ b/src/Renderer/MarkdownRenderer.php @@ -904,23 +904,6 @@ protected function renderBlockQuote(BlockQuote $node): string $body = trim($content, StringUtil::TRIMMABLE_WHITESPACE); - // PART 11 section 10c T1. The attribution is the quotation's SOURCE, so - // it stays INSIDE the quote. It used to follow as a sibling paragraph, - // which kept the words but not what they mean - read back it was - // attached to nothing, and a round trip produced a blockquote with no - // attribution. - // - // Markdown has no attribution syntax but does admit HTML, and this - // target already writes , , , and for - // constructs with no Markdown spelling. Through a CommonMark reader - //