diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dce2962..075474c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -198,15 +198,6 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
around it, so `[x]{#k .key kbd}` is `x `, and
where nothing is left over there is no wrapper at all. A derived `title` or
`datetime` yields to an authored one of the same name.
-- **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 `` 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, and
- the HTML importer reads a trailing `` in a `` back as the
- attribution so the renderer's own output round-trips.
- **Every `` carries a `scope`** (PART 10 §T9, markup-carve/carve#1159).
`col` for a header cell in the head-row run, `row` for one below it, on pipe
tables and list tables alike. An authored `scope` replaces the emitted one
@@ -518,18 +509,6 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
welding the words onto the last data cell (`| a |Fruit prices` on Markdown,
`aFruit prices` on plain text).
-- **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.
-
- **An authored `abbr` wins on the Markdown, ANSI and plain targets**
(markup-carve/carve#1176). markup-carve/carve#1127 ruled that an explicit
`abbr` outranks automatic expansion; the HTML renderer honoured it while
diff --git a/src/ast.rs b/src/ast.rs
index c3b03422..f0e7478a 100644
--- a/src/ast.rs
+++ b/src/ast.rs
@@ -284,12 +284,6 @@ pub struct ListItem {
pub struct BlockQuote {
pub attrs: Option,
pub children: Vec,
- /// The source of the quotation (PART 9 §4a).
- ///
- /// 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).
- pub attribution: Option>,
/// Span in the original source, when the parser could determine it.
pub pos: Option,
}
diff --git a/src/ast_json.rs b/src/ast_json.rs
index 48a1fd41..a70eac04 100644
--- a/src/ast_json.rs
+++ b/src/ast_json.rs
@@ -839,9 +839,6 @@ fn write_block_quote(out: &mut String, n: &BlockQuote) {
let mut w = typed(out, "block_quote");
w.field("children", |out| write_blocks(out, &n.children));
// PART 9 §4a: the source of the quotation, absent when there is none.
- if let Some(attribution) = &n.attribution {
- w.field("attribution", |out| write_inlines(out, attribution));
- }
write_attrs_field(&mut w, &n.attrs);
write_pos_field(&mut w, &n.pos);
w.finish();
@@ -1513,7 +1510,6 @@ fn decode_block(value: &Json) -> Result {
"block_quote" => Ok(BlockNode::BlockQuote(BlockQuote {
attrs: optional_attrs(obj)?,
children: decode_blocks(required_array(obj, "block_quote", "children")?)?,
- attribution: optional_inlines(obj, "attribution")?,
pos: optional_pos(obj, "block_quote")?,
})),
"list" => Ok(BlockNode::List(List {
diff --git a/src/citations.rs b/src/citations.rs
index e5c0aeb7..e752870f 100644
--- a/src/citations.rs
+++ b/src/citations.rs
@@ -764,9 +764,6 @@ fn annotate_citations_block(
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- annotate_citations_inline(attribution, defs, mode, has_bib, seen, order, uses);
- }
for child in &mut b.children {
annotate_citations_block(child, defs, mode, has_bib, seen, order, uses);
}
diff --git a/src/document_ids.rs b/src/document_ids.rs
index c863871b..e36740ed 100644
--- a/src/document_ids.rs
+++ b/src/document_ids.rs
@@ -382,9 +382,6 @@ impl Seeder {
fn walk_blockquote(&mut self, b: &BlockQuote) {
self.reserve_attrs(&b.attrs);
- if let Some(attribution) = &b.attribution {
- self.walk_inlines(attribution);
- }
self.walk_blocks(&b.children);
}
diff --git a/src/extensions/external_links.rs b/src/extensions/external_links.rs
index b9242b86..432863af 100644
--- a/src/extensions/external_links.rs
+++ b/src/extensions/external_links.rs
@@ -107,9 +107,6 @@ impl ExternalLinks {
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- self.visit_inlines(attribution);
- }
for child in &mut b.children {
self.visit_block(child);
}
diff --git a/src/extensions/heading_numbers.rs b/src/extensions/heading_numbers.rs
index fcf36844..b34292e0 100644
--- a/src/extensions/heading_numbers.rs
+++ b/src/extensions/heading_numbers.rs
@@ -294,9 +294,6 @@ fn rewrite_links_blocks(
BlockNode::Heading(h) => rewrite_links_inlines(&mut h.children, by_id, opts),
BlockNode::Paragraph(p) => rewrite_links_inlines(&mut p.children, by_id, opts),
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- rewrite_links_inlines(attribution, by_id, opts);
- }
rewrite_links_blocks(&mut b.children, by_id, opts);
}
BlockNode::Div(d) => rewrite_links_blocks(&mut d.children, by_id, opts),
diff --git a/src/extensions/index_terms.rs b/src/extensions/index_terms.rs
index 9f63b8d1..62b2902b 100644
--- a/src/extensions/index_terms.rs
+++ b/src/extensions/index_terms.rs
@@ -174,9 +174,6 @@ fn rewrite_markers_block(
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- rewrite_markers_inline(attribution, counts, display);
- }
for child in &mut b.children {
rewrite_markers_block(child, counts, display);
}
diff --git a/src/extensions/smart_quotes.rs b/src/extensions/smart_quotes.rs
index af0c3082..9ef99a75 100644
--- a/src/extensions/smart_quotes.rs
+++ b/src/extensions/smart_quotes.rs
@@ -93,9 +93,6 @@ impl SmartQuotes {
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- self.visit_inlines(attribution);
- }
self.visit_blocks(&mut b.children);
}
BlockNode::Table(t) => {
diff --git a/src/extensions/tab_normalize.rs b/src/extensions/tab_normalize.rs
index ed81050f..f067d77f 100644
--- a/src/extensions/tab_normalize.rs
+++ b/src/extensions/tab_normalize.rs
@@ -87,9 +87,6 @@ impl TabNormalize {
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- self.visit_inlines(attribution);
- }
for child in &mut b.children {
self.visit_block(child);
}
diff --git a/src/html_import.rs b/src/html_import.rs
index e7312847..ade78c2d 100644
--- a/src/html_import.rs
+++ b/src/html_import.rs
@@ -375,46 +375,9 @@ impl<'a> Importer<'a> {
})]);
}
if tag == "blockquote" {
- // A trailing `` is the quote's ATTRIBUTION (PART 9 §4a).
- // This renderer emits the source of a quotation that way, so
- // reading it back as an ordinary second paragraph meant the
- // engine's own HTML did not survive a round trip - the `^ ` line
- // was gone from the Carve it wrote (carve#1159).
- //
- // The LAST footer, because that is the one the renderer emits and
- // the one an author puts after the quoted text; an earlier one
- // stays an ordinary block.
- // The slot holds INLINE content, so a footer 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.
- let footer = children.iter().rposition(|n| {
- Self::tag(n).as_deref() == Some("footer")
- && !n.children.borrow().iter().any(|c| {
- Self::tag(c)
- .as_deref()
- .map(Self::is_block_tag)
- .unwrap_or(false)
- })
- });
- let attribution = match footer {
- Some(index) => {
- let inner = children[index].children.borrow().clone();
- Some(self.inlines(&inner, path, depth + 1)?)
- }
- None => None,
- };
- let body: Vec<_> = children
- .iter()
- .enumerate()
- .filter(|(index, _)| Some(*index) != footer)
- .map(|(_, node)| node.clone())
- .collect();
return Ok(vec![BlockNode::BlockQuote(BlockQuote {
attrs,
- children: self.blocks(&body, path, depth + 1)?,
- attribution,
+ children: self.blocks(&children, path, depth + 1)?,
pos: None,
})]);
}
diff --git a/src/lint.rs b/src/lint.rs
index df574a06..ad847544 100644
--- a/src/lint.rs
+++ b/src/lint.rs
@@ -391,9 +391,6 @@ fn walk_block(node: &BlockNode, visit: &mut Visit<'_>) {
fn walk_block_quote(n: &BlockQuote, visit: &mut Visit<'_>) {
report("block_quote", &n.attrs, n.pos, visit);
walk_blocks(&n.children, visit);
- if let Some(attribution) = &n.attribution {
- walk_inlines(attribution, visit);
- }
}
fn walk_table(n: &Table, visit: &mut Visit<'_>) {
diff --git a/src/markdown_import.rs b/src/markdown_import.rs
index f6a8b205..0197f260 100644
--- a/src/markdown_import.rs
+++ b/src/markdown_import.rs
@@ -324,7 +324,6 @@ impl Builder {
Frame::BlockQuote(children) => self.block(BlockNode::BlockQuote(BlockQuote {
attrs: None,
children,
- attribution: None,
pos: None,
})),
Frame::List {
diff --git a/src/parse.rs b/src/parse.rs
index 46138bd2..37961d0a 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -416,9 +416,6 @@ fn fill_crossref_hrefs(doc: &mut Document, lowercase_ids: bool) {
BlockNode::Paragraph(p) => inlines(&mut p.children, index),
BlockNode::Heading(h) => inlines(&mut h.children, index),
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- inlines(attribution, index);
- }
blocks(&mut b.children, index);
}
BlockNode::Div(d) => blocks(&mut d.children, index),
@@ -2810,9 +2807,6 @@ fn fill_offsets(blocks: &mut [BlockNode], line_starts: &[usize]) {
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- apply_inline_offsets(attribution, line_starts);
- }
fill_offsets(&mut b.children, line_starts);
}
BlockNode::Div(d) => fill_offsets(&mut d.children, line_starts),
@@ -5077,21 +5071,22 @@ fn parse_blockquote(cur: &mut LineCursor, options: &Options<'_>) -> BlockNode {
}
let inner = inner.into_source();
let children = parse_mapped_source(&inner, options);
- let mut quote = BlockQuote {
+ let quote = BlockQuote {
pos: span_of(cur, span_start, cur.pos, options),
attrs: None,
- attribution: None,
children,
};
- // PART 9 §4a: the caption on a quote is its ATTRIBUTION, so no figure wraps
- // it - it takes no number, and nothing walking the tree for figures finds
- // it (carve#1159). The span still runs from the quote's first line through
- // the caption the cursor just consumed.
- if let Some(attribution) = consume_attribution(cur, options) {
- quote.attribution = Some(attribution);
- quote.pos = span_of(cur, span_start, cur.pos, options);
+ if let Some(caption) = consume_caption(cur, options) {
+ BlockNode::Figure(Figure {
+ attrs: None,
+ target: FigureTarget::BlockQuote(quote),
+ caption,
+ short_caption: None,
+ pos: span_of(cur, span_start, cur.pos, options),
+ })
+ } else {
+ BlockNode::BlockQuote(quote)
}
- BlockNode::BlockQuote(quote)
}
fn is_list_marker(line: &str) -> bool {
@@ -8678,13 +8673,6 @@ fn image_is_block(cur: &mut LineCursor) -> bool {
interrupts
}
-/// A quote's attribution (PART 9 §4a): the same slot as a caption, parsed
-/// without caption context so a bare `#` stays literal - an attribution has no
-/// number to place, and §4a says the placeholder does not resolve there.
-fn consume_attribution(cur: &mut LineCursor, options: &Options<'_>) -> Option> {
- consume_caption_slot(cur, options, false)
-}
-
fn consume_caption(cur: &mut LineCursor, options: &Options<'_>) -> Option> {
consume_caption_slot(cur, options, true)
}
@@ -13744,9 +13732,6 @@ fn apply_abbreviations_block(block: &mut BlockNode, index: &AbbreviationIndex<'_
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- apply_abbreviations_inline(attribution, index);
- }
for child in &mut b.children {
apply_abbreviations_block(child, index);
}
@@ -14432,9 +14417,6 @@ fn resolve_reference_links_block(
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- resolve_reference_links_inline(attribution, defs, heading_index);
- }
for child in &mut b.children {
resolve_reference_links_block(child, defs, heading_index);
}
@@ -15340,9 +15322,6 @@ fn coalesce_block(block: &mut BlockNode) {
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- coalesce_inlines(attribution);
- }
for child in &mut b.children {
coalesce_block(child);
}
diff --git a/src/profile_filter.rs b/src/profile_filter.rs
index d2e7d187..4c71fa3e 100644
--- a/src/profile_filter.rs
+++ b/src/profile_filter.rs
@@ -305,9 +305,6 @@ impl ProfileFilter<'_> {
}
}
BlockNode::BlockQuote(bq) => {
- if let Some(attribution) = &mut bq.attribution {
- self.filter_inlines(attribution, depth + 1)?;
- }
self.filter_blocks(&mut bq.children, depth)?;
}
BlockNode::Table(table) => {
@@ -860,15 +857,6 @@ fn extract_block_text(node: &BlockNode, smart: SmartTypographyMode) -> String {
paras.push(format!("> {text}"));
}
}
- if let Some(attribution) = &bq.attribution {
- let text: String = attribution
- .iter()
- .map(|n| extract_inline_text(n, smart))
- .collect();
- if !text.is_empty() {
- paras.push(format!("^ {text}"));
- }
- }
paras.join("\n")
}
BlockNode::DefinitionList(dl) => {
@@ -1090,9 +1078,6 @@ fn cleanup_block_children(block: &mut BlockNode) {
}
}
BlockNode::BlockQuote(bq) => {
- if let Some(attribution) = &mut bq.attribution {
- cleanup_inlines(attribution);
- }
cleanup_blocks(&mut bq.children);
}
BlockNode::Table(t) => {
diff --git a/src/render.rs b/src/render.rs
index 59055132..58ede28d 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -370,16 +370,6 @@ fn collect_footnotes_block(
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- collect_footnotes_inline(
- assign_ref_ids,
- attribution,
- def_labels,
- label_indices,
- seen,
- order,
- );
- }
for child in &mut b.children {
collect_footnotes_block(
assign_ref_ids,
@@ -1490,10 +1480,7 @@ fn render_blockquote(
state: &mut RenderState,
) {
indent(out, level);
- // PART 9 §4a: an attribution renders INSIDE the quote, where a quotation's
- // source belongs, and forces the expanded form - the compact one has
- // nowhere to put a second element (carve#1159).
- if b.attribution.is_none() && b.children.len() == 1 {
+ if b.children.len() == 1 {
if let BlockNode::Paragraph(p) = &b.children[0] {
out.push_str("");
- render_inlines(out, attribution, options, state);
- out.push_str(" ");
- }
out.push('\n');
indent(out, level);
out.push_str("");
diff --git a/src/render_ansi.rs b/src/render_ansi.rs
index f1c1ea61..d784b114 100644
--- a/src/render_ansi.rs
+++ b/src/render_ansi.rs
@@ -191,33 +191,7 @@ fn render_block(node: &BlockNode, ctx: &mut AnsiContext, depth: usize) -> String
ctx.block_quote_depth += 1;
let out = render_blocks("e.children, ctx, depth + 1);
ctx.block_quote_depth -= 1;
- // 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 §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.
- match "e.attribution {
- Some(attribution) => {
- ctx.block_quote_depth += 1;
- let bar = block_quote_prefix(ctx);
- ctx.block_quote_depth -= 1;
- let rendered = render_caption(attribution, ctx);
- let prefixed = prefix_lines(rendered.trim_end_matches('\n'), &bar);
- format!(
- "{}\n{}\n{}\n\n",
- out.trim_end_matches('\n'),
- bar.trim_end(),
- prefixed
- )
- }
- None => out,
- }
+ out
}
BlockNode::List(list) => render_list(list, ctx, depth + 1),
BlockNode::ThematicBreak(_) => format!("{}\n\n", style(&"─".repeat(40), DIM)),
diff --git a/src/render_carve.rs b/src/render_carve.rs
index 357b4ee3..0b71c0e4 100644
--- a/src/render_carve.rs
+++ b/src/render_carve.rs
@@ -655,9 +655,6 @@ fn normalize_escapes_block(block: &mut BlockNode) {
}
}
BlockNode::BlockQuote(b) => {
- if let Some(attribution) = &mut b.attribution {
- normalize_escapes_inlines(attribution);
- }
for child in &mut b.children {
normalize_escapes_block(child);
}
@@ -1153,14 +1150,6 @@ fn render_block(node: &BlockNode, ctx: &mut CarveContext) -> String {
})
.collect::>()
.join("\n");
- // PART 9 §4a: an attribution is written back as the `^` line it was
- // read from. Dropping it would lose content, which PART 11 §1 forbids.
- let body = match "e.attribution {
- Some(attribution) => {
- format!("{body}\n^ {}", render_inlines(attribution, ctx))
- }
- None => body,
- };
with_block_attrs("e.attrs, &body)
}
BlockNode::List(list) => with_block_attrs(
diff --git a/src/render_markdown.rs b/src/render_markdown.rs
index 9c636942..ccbd6d71 100644
--- a/src/render_markdown.rs
+++ b/src/render_markdown.rs
@@ -276,22 +276,7 @@ fn render_block(node: &BlockNode, ctx: &mut MarkdownContext, depth: usize) -> St
}
BlockNode::BlockQuote(quote) => {
let lines = render_blocks("e.children, ctx, depth + 1);
- let mut body = trim_block_output(&lines).to_string();
- // PART 11 §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
- // opens an HTML BLOCK inside the quote (it is not wrapped
- // in a paragraph), so the rendered HTML matches the HTML target's.
- if let Some(attribution) = "e.attribution {
- let text = render_inlines(attribution, ctx, depth + 1);
- body.push_str(&format!("\n\n", text.trim()));
- }
+ let body = trim_block_output(&lines).to_string();
let quoted = body
.split('\n')
.map(|line| {
diff --git a/src/render_plain.rs b/src/render_plain.rs
index ed50ebac..44b76638 100644
--- a/src/render_plain.rs
+++ b/src/render_plain.rs
@@ -193,18 +193,7 @@ fn render_block(node: &BlockNode, depth: usize) -> String {
"\"{}\"",
trim_block_output(&render_blocks("e.children, depth + 1))
);
- // PART 11 §10c T3. ADJACENCY, not a blank line. A blank line is what
- // separates blocks on this target, so putting one here said the
- // attribution was a block of its own rather than the quotation's
- // source - the words survived, the attachment did not. No
- // punctuation is invented: a dash prefix would put a character in
- // the output the author never wrote.
- match "e.attribution {
- Some(attribution) => {
- format!("{quoted}\n{}\n\n", render_inlines(attribution).trim())
- }
- None => format!("{quoted}\n\n"),
- }
+ format!("{quoted}\n\n")
}
BlockNode::List(list) => render_list(list, depth + 1),
BlockNode::ThematicBreak(_) => "---\n\n".to_string(),
diff --git a/src/render_text.rs b/src/render_text.rs
index 1c605c2f..de1d7f87 100644
--- a/src/render_text.rs
+++ b/src/render_text.rs
@@ -115,9 +115,6 @@ fn collect_block_quote(
suppressed: bool,
out: &mut ConsumedAbbreviations,
) {
- if let Some(attribution) = "e.attribution {
- collect_inlines(attribution, suppressed, out);
- }
collect_blocks("e.children, suppressed, out);
}
diff --git a/src/wire_fields.rs b/src/wire_fields.rs
index 01864977..63d4d1af 100644
--- a/src/wire_fields.rs
+++ b/src/wire_fields.rs
@@ -12,7 +12,7 @@ pub(crate) const WIRE_FIELDS: &[(&str, &[&str])] = &[
("abbreviation_def", &["abbr", "attrs", "expansion", "pos", "type"]),
("admonition", &["attrs", "children", "kind", "label", "pos", "title", "type"]),
("autolink", &["attrs", "href", "pos", "text", "type"]),
- ("block_quote", &["attribution", "attrs", "children", "pos", "type"]),
+ ("block_quote", &["attrs", "children", "pos", "type"]),
("caption_number", &["attrs", "n", "pos", "type"]),
("citation_group", &["attrs", "items", "mode", "pos", "raw", "type"]),
("code", &["attrs", "pos", "type", "value"]),
diff --git a/tests/a_block_quote_carries_its_attribution.rs b/tests/a_block_quote_carries_its_attribution.rs
deleted file mode 100644
index 999ec309..00000000
--- a/tests/a_block_quote_carries_its_attribution.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-//! `BlockQuote::attribution` is back, and this time it is load-bearing
-//! (PART 9 §4a, markup-carve/carve#1159). These rows pin that.
-//!
-//! History matters here, because the field name has meant two opposite things.
-//! carve-rs#832 REMOVED an `attribution` field that no code path could ever
-//! populate: the one parse-side assignment was `attribution: None`, and the
-//! decoder line that could have set it sat BENEATH the unknown-field check, so
-//! ingest refused `attribution` exactly as it refused a name nobody had ever
-//! used. Its writer, its walks and its escape pass were all unreachable, and a
-//! reader of `ast_json.rs` reasonably concluded attribution was part of the
-//! wire format when it was not.
-//!
-//! §4a makes it real: `^ Attr` under a quote is that quote's attribution, not a
-//! caption on a figure wrapping it. So every assertion the removal left behind
-//! is inverted below, and the file keeps its job - if the field ever goes back
-//! to being written-but-never-produced, these rows fail.
-
-use carve::{from_json, to_json, Options};
-
-fn published(source: &str) -> String {
- to_json(&carve::parse_with_options(
- source,
- &Options::default().with_positions(true),
- ))
-}
-
-const QUOTE_WITH_ATTRIBUTION: &str = r#"{"type":"document","children":[{"type":"block_quote","attribution":[{"type":"text","value":"A"}],"children":[{"type":"paragraph","children":[{"type":"text","value":"q"}]}]}],"srcByteLength":4}"#;
-
-const QUOTE_WITH_A_NAME_NOBODY_USES: &str = r#"{"type":"document","children":[{"type":"block_quote","zzzbogus":[{"type":"text","value":"A"}],"children":[{"type":"paragraph","children":[{"type":"text","value":"q"}]}]}],"srcByteLength":4}"#;
-
-const QUOTE_ALONE: &str = r#"{"type":"document","children":[{"type":"block_quote","children":[{"type":"paragraph","children":[{"type":"text","value":"q"}]}]}],"srcByteLength":4}"#;
-
-#[test]
-fn ingest_accepts_attribution_and_still_refuses_a_bogus_name() {
- let doc = from_json(QUOTE_WITH_ATTRIBUTION)
- .expect("a block_quote carrying `attribution` was refused");
- assert_eq!(
- carve::render_html(&doc).expect("the quote exceeded the render ceiling"),
- "\n q
\n \n "
- );
-
- let bogus = from_json(QUOTE_WITH_A_NAME_NOBODY_USES)
- .expect_err("a block_quote carrying `zzzbogus` was accepted");
- assert!(
- bogus.to_string().contains("zzzbogus"),
- "wrong message: {bogus}"
- );
-}
-
-#[test]
-fn the_same_payload_without_the_property_decodes() {
- let doc = from_json(QUOTE_ALONE).expect("a plain block_quote was refused");
- assert_eq!(
- carve::render_html(&doc).expect("the quote exceeded the render ceiling"),
- "q
"
- );
-}
-
-#[test]
-fn only_the_caption_spelling_publishes_an_attribution_property() {
- // A caption line under a quote IS its attribution. A nested quote takes one
- // the same way, and the marker sits one level OUT - `> ^ Attr` - because
- // the caption line has to follow a CLOSED block, and the inner quote is
- // what closes when the `> >` prefix stops.
- for source in [
- "> q\n^ Attr\n",
- // §4's attachment allowance: adjacent, or across exactly one blank line.
- "> q\n\n^ Attr\n",
- "> outer\n> > inner\n> ^ Attr\n",
- ] {
- let json = published(source);
- assert!(
- json.contains("\"attribution\""),
- "{source:?} published no attribution property: {json}"
- );
- }
-
- // The two near-misses. A second paragraph inside the quote is ordinary
- // content, and `> > ^ Attr` is a LAZY CONTINUATION of the inner paragraph,
- // so the caret never reaches the caption slot and lands in the text
- // verbatim.
- for source in ["> q\n>\n> Attr\n", "> outer\n> > inner\n> > ^ Attr\n"] {
- let json = published(source);
- assert!(
- !json.contains("\"attribution\""),
- "{source:?} published an attribution property: {json}"
- );
- }
-}
-
-#[test]
-fn a_quote_with_an_attribution_line_renders_as_an_attribution() {
- // `> q` / `^ Attr` is a quote carrying an attribution (PART 9 §4a), not a
- // figure wrapping a quote, on every target.
- assert_eq!(
- carve::to_html("> q\n^ Attr"),
- "\n q
\n \n "
- );
- // PART 11 §10c: the attribution stays ATTACHED on every target. Markdown
- // keeps it inside the quote as a ; plain text attaches by adjacency.
- assert_eq!(
- carve::to_markdown("> q\n^ Attr"),
- "> q\n>\n> \n"
- );
- assert_eq!(carve::to_plain_text("> q\n^ Attr"), "\"q\"\nAttr\n");
- assert_eq!(carve::to_carve("> q\n^ Attr"), "> q\n^ Attr\n");
-}
-
-#[test]
-fn the_attribution_survives_a_round_trip_through_json() {
- let doc = carve::parse("> q\n^ Attr");
- let back = from_json(&to_json(&doc)).expect("the published quote was refused on ingest");
- assert_eq!(
- carve::render_html(&back).expect("the quote exceeded the render ceiling"),
- carve::render_html(&doc).expect("the quote exceeded the render ceiling")
- );
-}
diff --git a/tests/caption_inline_positions.rs b/tests/caption_inline_positions.rs
index 421b5cf9..7314dd6b 100644
--- a/tests/caption_inline_positions.rs
+++ b/tests/caption_inline_positions.rs
@@ -10,13 +10,7 @@
use carve::ast::{BlockNode, InlineNode};
-/// The inline content a `^ ` line produced, whichever slot it landed in.
-///
-/// A quote's `^ ` line is its ATTRIBUTION now, not a figure caption (PART 9
-/// §4a, carve#1159) - a DIFFERENT field, reached by a different arm of every
-/// walker, and it failed the offsets exactly the way a figure caption did
-/// before carve-rs#333. So both slots are driven through the same rows here
-/// rather than the quote fixtures being retired to an image host.
+/// The inline content produced by a `^ ` caption line.
fn caption_of(source: &str) -> Vec {
let options = carve::Options {
positions: true,
@@ -25,11 +19,7 @@ fn caption_of(source: &str) -> Vec {
let doc = carve::parse_with_options(source, &options);
match &doc.children[0] {
BlockNode::Figure(figure) => figure.caption.clone(),
- BlockNode::BlockQuote(quote) => quote
- .attribution
- .clone()
- .expect("the fixture's caption line did not attach to the quote"),
- _ => panic!("the fixture parsed as neither a figure nor a quote"),
+ _ => panic!("the fixture did not parse as a figure"),
}
}
@@ -56,8 +46,7 @@ fn a_caption_slices_back_to_its_source() {
assert_eq!(slice, value);
}
-/// The same row on the slot carve-rs#333 was filed about, so retargeting the
-/// fixtures above at the attribution cannot quietly drop the figure's caption.
+/// The same row on the image host carve-rs#333 was filed about.
#[test]
fn a_figure_caption_slices_back_to_its_source_too() {
let source = "\n^ Steve Jobs\n";
diff --git a/tests/caption_inside_a_list_item.rs b/tests/caption_inside_a_list_item.rs
index 8197e7cf..2ee1250f 100644
--- a/tests/caption_inside_a_list_item.rs
+++ b/tests/caption_inside_a_list_item.rs
@@ -53,11 +53,11 @@ fn and_inside_a_nested_item() {
fn and_for_the_other_captionable_targets_in_an_item() {
// A caption is not image-only. These passed before the fix as well, and are
// pinned here so a narrowed change cannot trade one target for another.
- // The quote's caption is its ATTRIBUTION (PART 9 §4a, carve#1159), so it
- // is a `` rather than a ``. What the slot ACCEPTS in a
- // list item is the question here, and that is unchanged.
let quote = to_html("- > quoted\n ^ quote cap\n");
- assert!(quote.contains(""), "{quote}");
+ assert!(
+ quote.contains("quote cap "),
+ "{quote}"
+ );
let code = to_html("- ```\n code\n ```\n ^ code cap\n");
assert!(code.contains("code cap "), "{code}");
diff --git a/tests/html_import.rs b/tests/html_import.rs
index a8787441..dd5a399b 100644
--- a/tests/html_import.rs
+++ b/tests/html_import.rs
@@ -104,30 +104,26 @@ fn shared_contract_fixtures_match() {
}
}
-/// PART 9 §4a, carve#1159. The renderer emits a quote's attribution as a
-/// `` inside the ``, so an importer that read it as an
-/// ordinary second paragraph made the engine's own HTML un-round-trippable.
+/// A footer inside a quote is ordinary quoted block content.
#[test]
-fn a_trailing_footer_in_a_quote_is_its_attribution() {
+fn a_trailing_footer_in_a_quote_stays_quoted_content() {
let result = html_to_carve(
"To be
",
&HtmlImportOptions::default(),
)
.unwrap();
- assert_eq!(result.value, "> To be\n^ Hamlet\n");
+ assert_eq!(result.value, "> To be\n>\n> Hamlet\n");
}
-/// A quote has ONE attribution, so a second footer cannot join it. The LAST is
-/// the one this renderer emits and the one an author puts after the quoted
-/// text; the earlier footer stays an ordinary block rather than being dropped.
+/// Multiple footers are ordinary blocks and none are dropped.
#[test]
-fn the_last_footer_is_the_attribution_and_the_others_stay() {
+fn every_footer_in_a_quote_stays_quoted_content() {
let result = html_to_carve(
"To be
",
&HtmlImportOptions::default(),
)
.unwrap();
- assert_eq!(result.value, "> First\n>\n> To be\n^ Hamlet\n");
+ assert_eq!(result.value, "> First\n>\n> To be\n>\n> Hamlet\n");
}
/// The slot holds INLINE content, so a footer carrying blocks does not fit it.
diff --git a/tests/list_item_span_contains_children.rs b/tests/list_item_span_contains_children.rs
index 217eae64..59f51a49 100644
--- a/tests/list_item_span_contains_children.rs
+++ b/tests/list_item_span_contains_children.rs
@@ -96,36 +96,26 @@ fn an_item_with_only_its_marker_line_is_unchanged() {
assert_eq!(children.len(), 1);
}
-/// A quote carrying an attribution kept line and column and offsets of 0..0:
-/// the walk filled its children and its attribution and skipped the quote
-/// itself, so the quote reported a span that selects nothing and every block
-/// inside it fell outside its own parent (carve#565).
-///
-/// The fixture used to reach the quote through a figure wrapping it. §4a
-/// removed that wrapper - the caption is the quote's attribution now
-/// (carve#1159) - so the quote is a direct child, and the span it has to get
-/// right is its own.
+/// A captioned quote and its target both carry real, nested offsets.
#[test]
-fn an_attributed_quote_carries_real_offsets() {
+fn a_captioned_quote_carries_real_offsets() {
let source = "Intro\n\n> Stay hungry\n^ Steve Jobs\n";
let doc = document(source);
- let BlockNode::BlockQuote(quote) = &doc.children[1] else {
- panic!("expected a quote second, got {:?}", doc.children[1]);
+ let BlockNode::Figure(figure) = &doc.children[1] else {
+ panic!("expected a figure second, got {:?}", doc.children[1]);
};
- assert!(
- quote.attribution.is_some(),
- "the caption line did not attach as an attribution"
- );
- let pos = quote.pos.expect("the quote carries no position");
+ let carve::ast::FigureTarget::BlockQuote(quote) = &figure.target else {
+ panic!("expected a block quote target");
+ };
+ let pos = figure.pos.expect("the figure carries no position");
assert!(
pos.end_offset > pos.start_offset,
- "quote span is empty: [{}, {}]",
+ "figure span is empty: [{}, {}]",
pos.start_offset,
pos.end_offset,
);
- // The span runs from the quote's first line THROUGH the attribution, so it
- // contains every child and the attribution alike.
+ // The figure span runs from the quote through its caption.
for child in "e.children {
let child_pos = child_pos(child).expect("a child of the quote carries no position");
assert!(
diff --git a/tests/non_html_parity.rs b/tests/non_html_parity.rs
index 4d0c137d..5c5708b9 100644
--- a/tests/non_html_parity.rs
+++ b/tests/non_html_parity.rs
@@ -83,25 +83,6 @@ fn ansi_parity() {
check("ansi", carve::to_ansi);
}
-#[test]
-fn blockquote_attribution_stays_attached_in_non_html_renderers() {
- // This pinned the OPPOSITE - the attribution as a sibling separated by a
- // blank line - and that spacing was the defect (PART 11 §10c). The words
- // survived, the attachment did not: re-read, the attribution was a paragraph
- // that merely followed a quotation rather than its source.
- let input = "> q\n^ Attr";
-
- assert_eq!(
- carve::to_markdown(input),
- "> q\n>\n> \n"
- );
- assert_eq!(carve::to_plain_text(input), "\"q\"\nAttr\n");
- assert_eq!(
- carve::to_ansi(input),
- "\x1b[36m\x1b[2m│\x1b[0m q\n\x1b[36m\x1b[2m│\x1b[0m\n\x1b[36m\x1b[2m│\x1b[0m \x1b[3m\x1b[2mAttr\x1b[0m\n"
- );
-}
-
#[test]
fn markdown_code_fence_keeps_quoted_header() {
assert_eq!(
diff --git a/tests/presentation_targets_keep_authored_text.rs b/tests/presentation_targets_keep_authored_text.rs
index 6eee5d09..3b4f958a 100644
--- a/tests/presentation_targets_keep_authored_text.rs
+++ b/tests/presentation_targets_keep_authored_text.rs
@@ -175,47 +175,13 @@ fn a_figure_over_a_table_separates_its_caption_too() {
);
}
-/// PART 11 §10c. The attribution is the quotation's SOURCE, so every target
-/// keeps it ATTACHED. It used to follow as a sibling separated by a blank line:
-/// the words survived, the relationship did not, and a round trip produced a
-/// blockquote with no attribution at all.
-#[test]
-fn the_attribution_stays_attached_to_its_quote() {
- let src = "> q\n^ Attr\n";
-
- // Markdown: a element inside the quote. Through a CommonMark reader
- // that opens an HTML block rather than being wrapped in a paragraph, so the
- // rendered HTML matches the HTML target's.
- assert_eq!(carve::to_markdown(src), "> q\n>\n> \n");
-
- // Plain text: adjacency. No blank line, and no invented punctuation.
- assert_eq!(carve::to_plain_text(src), "\"q\"\nAttr\n");
-
- // Terminal: the quote bar carried onto the attribution line, which keeps its
- // italic-dim caption styling.
- assert_eq!(
- strip_ansi(&carve::to_ansi(src)),
- "\u{2502} q\n\u{2502}\n\u{2502} Attr\n"
- );
-}
-
-/// A quote with no attribution is untouched: the change adds a line only where
-/// the author wrote one.
+/// An uncaptioned quote is unchanged.
#[test]
fn a_quote_without_an_attribution_is_unchanged() {
assert_eq!(carve::to_markdown("> q\n"), "> q\n");
assert_eq!(carve::to_plain_text("> q\n"), "\"q\"\n");
}
-/// A block after an attributed quote keeps its separation.
-#[test]
-fn a_block_after_an_attributed_quote_stays_separate() {
- assert_eq!(
- carve::to_markdown("> q\n^ A\n\nafter\n"),
- "> q\n>\n> \n\nafter\n"
- );
-}
-
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
diff --git a/tests/recursion_and_panics.rs b/tests/recursion_and_panics.rs
index 4df9f981..f5fd9717 100644
--- a/tests/recursion_and_panics.rs
+++ b/tests/recursion_and_panics.rs
@@ -114,7 +114,6 @@ fn non_html_renderers_bound_programmatic_block_depth() {
block = carve::BlockNode::BlockQuote(carve::BlockQuote {
attrs: None,
children: vec![block],
- attribution: None,
pos: None,
});
}
diff --git a/tests/render_ceiling_refuses.rs b/tests/render_ceiling_refuses.rs
index 9b85f887..0d1de8ac 100644
--- a/tests/render_ceiling_refuses.rs
+++ b/tests/render_ceiling_refuses.rs
@@ -46,7 +46,6 @@ fn nested(depth: usize) -> Document {
block = BlockNode::BlockQuote(BlockQuote {
attrs: None,
children: vec![block],
- attribution: None,
pos: None,
});
}
diff --git a/tests/source_positions.rs b/tests/source_positions.rs
index b3639dc2..007db9af 100644
--- a/tests/source_positions.rs
+++ b/tests/source_positions.rs
@@ -404,19 +404,15 @@ fn a_resolved_reference_link_is_unaffected() {
}
#[test]
-fn an_attributed_quote_spans_the_quote_and_its_attribution() {
- // The image path placed its figure; a quote with a caption line did not.
- // §4a made that caption the quote's ATTRIBUTION rather than a figure
- // wrapping it (carve#1159), so the span to get right is the quote's own -
- // and it still has to reach through the `^ ` line the cursor consumed.
+fn a_captioned_quote_figure_spans_the_quote_and_its_caption() {
let source = "> Stay hungry\n^ Steve Jobs\n";
let doc = parse_with_positions(source);
- let BlockNode::BlockQuote(quote) = &doc.children[0] else {
- panic!("expected a quote, got {:?}", doc.children[0]);
+ let BlockNode::Figure(figure) = &doc.children[0] else {
+ panic!("expected a figure, got {:?}", doc.children[0]);
};
assert_eq!(
- slice(source, quote.pos.expect("the quote carries a position")),
+ slice(source, figure.pos.expect("the figure carries a position")),
"> Stay hungry\n^ Steve Jobs"
);
}
diff --git a/tests/spec b/tests/spec
index 372c5cbd..0c9e8e8c 160000
--- a/tests/spec
+++ b/tests/spec
@@ -1 +1 @@
-Subproject commit 372c5cbddddd74311c832c5566857fe72d51d242
+Subproject commit 0c9e8e8c34453b2d3cc050904f01875368183708
diff --git a/tests/two_blank_lines_detach_a_caption.rs b/tests/two_blank_lines_detach_a_caption.rs
index d56ccc14..4c96700a 100644
--- a/tests/two_blank_lines_detach_a_caption.rs
+++ b/tests/two_blank_lines_detach_a_caption.rs
@@ -65,12 +65,10 @@ fn two_blank_lines_detach_a_listing_caption() {
#[test]
fn one_blank_line_attaches_a_blockquote_caption() {
// CONTROL. This is the one shape the corpus pins, via
- // `55-blockquote-caption-after-a-blank-line.crv`. A quote's caption is its
- // ATTRIBUTION (PART 9 §4a, carve#1159) - a different node shape, the same
- // slot and the same blank-line allowance, which is what this file is about.
+ // `55-blockquote-caption-after-a-blank-line.crv`.
assert_eq!(
html("> q\n\n^ cap\n"),
- "\n q
\n \n "
+ "\n q
\n cap \n "
);
}
@@ -138,6 +136,6 @@ fn an_adjacent_caption_still_attaches() {
// documents that carry a caption) and is untouched by the slot's width.
assert_eq!(
html("> q\n^ cap\n"),
- "\n q
\n \n "
+ "\n q
\n cap \n "
);
}