diff --git a/acdc-editor-wasm/CHANGELOG.md b/acdc-editor-wasm/CHANGELOG.md index a2078fd..4b4376d 100644 --- a/acdc-editor-wasm/CHANGELOG.md +++ b/acdc-editor-wasm/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `aside.admonition-block`, `figure.example-block`, `aside.sidebar`, `section.quote-block`, `.listing-block`, `figure.image-block`, `ol.toc-list`, `section.footnotes`, `ol.callout-list`, semantic list wrappers, etc.). +- **Highlighter simplified to use AST locations** — replaced ~315 lines of raw text + scanning with direct use of parser location data. Inline delimiter highlighting, + block metadata spans, delimited block delimiters, macro spans, and description list + markers now use precise AST locations instead of heuristic text scanning. ### Fixed diff --git a/acdc-editor-wasm/src/ast_highlight.rs b/acdc-editor-wasm/src/ast_highlight.rs index aa22655..08d0b22 100644 --- a/acdc-editor-wasm/src/ast_highlight.rs +++ b/acdc-editor-wasm/src/ast_highlight.rs @@ -5,7 +5,7 @@ //! `` for CSS-based highlighting. use acdc_parser::{ - AdmonitionVariant, Block, DelimitedBlockType, Document, InlineMacro, InlineNode, + AdmonitionVariant, Block, DelimitedBlockType, Document, Form, InlineMacro, InlineNode, }; /// A span of source text that should receive a CSS class. @@ -140,7 +140,7 @@ fn collect_block_spans(input: &str, block: &Block, spans: &mut Vec) { collect_inline_spans(input, &pb.title, spans); } Block::Admonition(adm) => { - collect_block_metadata_spans(input, &adm.metadata, adm.location.absolute_start, spans); + collect_block_metadata_spans(&adm.metadata, spans); let label_len = admonition_label_len(&adm.variant); let start = adm.location.absolute_start; // Only highlight the label if the text actually starts with it (handles NOTE: vs [NOTE]) @@ -201,12 +201,7 @@ fn collect_block_spans(input: &str, block: &Block, spans: &mut Vec) { collect_inline_spans(input, &db.title, spans); } Block::Paragraph(para) => { - collect_block_metadata_spans( - input, - ¶.metadata, - para.location.absolute_start, - spans, - ); + collect_block_metadata_spans(¶.metadata, spans); // Highlight title if present collect_inline_spans(input, ¶.title, spans); collect_inline_spans(input, ¶.content, spans); @@ -270,81 +265,31 @@ fn push_inline_span(spans: &mut Vec, location: &acdc_parser::Location, cla /// Highlight opening and closing delimiters for an inline node. /// -/// Returns the effective end position of the delimited region. This may -/// extend past `location.absolute_end` when the parser's location is off -/// by one for unconstrained markers (e.g. `t**he**`). +/// Returns the exclusive end position of the delimited region. +/// `absolute_end` is inclusive, so we convert with `+1`. fn highlight_delimited_inline( - input: &str, location: &acdc_parser::Location, - marker_char: char, + delimiter_len: usize, spans: &mut Vec, ) -> usize { let start = location.absolute_start; - let end = location.absolute_end; - let Some(text) = input.get(start..end) else { - return end; - }; - - // Count consecutive markers at start (e.g. "**" or "*") - let len = text.chars().take_while(|&c| c == marker_char).count(); - if len == 0 { - return end; - } + let end = location.absolute_end + 1; // inclusive → exclusive - // Opening marker + // Opening delimiter spans.push(Span { start, - end: start + len, + end: start + delimiter_len, class: "adoc-delimiter", priority: 3, }); - // Closing marker — try several positions to handle parser off-by-one - let is_marker = |s: usize, e: usize| -> bool { - s >= start + len - && e <= input.len() - && input - .get(s..e) - .is_some_and(|t| t.len() == len && t.chars().all(|c| c == marker_char)) - }; - - // 1. Standard: closing delimiter at end of node location - let close_start = end.saturating_sub(len); - if is_marker(close_start, end) { - spans.push(Span { - start: close_start, - end, - class: "adoc-delimiter", - priority: 3, - }); - return end; - } - - // 2. Immediately after the node (parser excluded closing delim entirely) - if is_marker(end, end + len) { - spans.push(Span { - start: end, - end: end + len, - class: "adoc-delimiter", - priority: 3, - }); - return end + len; - } - - // 3. Straddling the node boundary (off by one for unconstrained markers) - if len > 1 { - let alt_start = end - len + 1; - let alt_end = alt_start + len; - if is_marker(alt_start, alt_end) { - spans.push(Span { - start: alt_start, - end: alt_end, - class: "adoc-delimiter", - priority: 3, - }); - return alt_end; - } - } + // Closing delimiter + spans.push(Span { + start: end - delimiter_len, + end, + class: "adoc-delimiter", + priority: 3, + }); end } @@ -355,16 +300,10 @@ fn collect_description_list_spans( spans: &mut Vec, ) { for item in &list.items { - let item_start = item.location.absolute_start; - let item_source = input - .get(item_start..item.location.absolute_end) - .unwrap_or(""); - if let Some(delim_pos) = item_source.find(&item.delimiter) { - let abs_delim_start = item_start + delim_pos; - let abs_delim_end = abs_delim_start + item.delimiter.len(); + if let Some(loc) = &item.delimiter_location { spans.push(Span { - start: abs_delim_start, - end: abs_delim_end, + start: loc.absolute_start, + end: loc.absolute_end + 1, // inclusive → exclusive class: "adoc-description-marker", priority: 1, }); @@ -425,7 +364,6 @@ fn collect_list_item_spans(input: &str, item: &acdc_parser::ListItem, spans: &mu // Delimited blocks // --------------------------------------------------------------------------- -#[allow(clippy::too_many_lines)] fn collect_delimited_block_spans( input: &str, db: &acdc_parser::DelimitedBlock, @@ -434,70 +372,30 @@ fn collect_delimited_block_spans( let block_start = db.location.absolute_start; let block_end = db.location.absolute_end; - collect_block_metadata_spans(input, &db.metadata, block_start, spans); - collect_delimiter_lines(input, db, block_start, block_end, spans); + collect_block_metadata_spans(&db.metadata, spans); + collect_delimiter_lines(db, spans); collect_delimited_content(input, &db.inner, block_start, block_end, spans); } -/// Find the opening and closing delimiter lines within a delimited block. -/// -/// The block location may include preceding metadata lines (e.g. `[source,rust]`), -/// so we scan forward through lines to find one matching the delimiter string. -fn collect_delimiter_lines( - input: &str, - db: &acdc_parser::DelimitedBlock, - block_start: usize, - block_end: usize, - spans: &mut Vec, -) { - let delim = &db.delimiter; - if delim.is_empty() { +/// Highlight opening and closing delimiter lines of a delimited block. +fn collect_delimiter_lines(db: &acdc_parser::DelimitedBlock, spans: &mut Vec) { + if db.delimiter.is_empty() { return; } let cls = delimiter_class(&db.inner); - // Scan forward from block_start to find the opening delimiter line - let mut pos = block_start; - while pos < block_end { - let line_end = find_line_end(input, pos); - let line = input.get(pos..line_end).unwrap_or(""); - if line.trim() == delim { - spans.push(Span { - start: pos, - end: line_end, - class: cls, - priority: 1, - }); - break; - } - // Skip past the newline - pos = if line_end < input.len() { - line_end + 1 - } else { - break; - }; - } - - // Closing delimiter: check the last line of the block. - // Try both block_end and block_end+1 to handle parser off-by-one. - if block_end > 0 { - for effective_end in [block_end, block_end + 1] { - if effective_end > input.len() { - continue; - } - let close_start = find_line_start(input, effective_end.saturating_sub(1)); - let close_line = input.get(close_start..effective_end).unwrap_or(""); - if close_start > block_start && close_line.trim() == delim { - spans.push(Span { - start: close_start, - end: effective_end, - class: cls, - priority: 1, - }); - break; - } - } + for loc in db + .open_delimiter_location + .iter() + .chain(db.close_delimiter_location.iter()) + { + spans.push(Span { + start: loc.absolute_start, + end: loc.absolute_end + 1, // inclusive → exclusive + class: cls, + priority: 1, + }); } } @@ -603,27 +501,9 @@ fn collect_table_spans( block_end: usize, spans: &mut Vec, ) { - let open_end = find_line_end(input, block_start); - spans.push(Span { - start: block_start, - end: open_end, - class: "adoc-table-delimiter", - priority: 1, - }); - - if block_end > 0 { - let close_start = find_line_start(input, block_end.saturating_sub(1)); - if close_start > block_start { - spans.push(Span { - start: close_start, - end: block_end, - class: "adoc-table-delimiter", - priority: 1, - }); - } - } - - let content_start = open_end; + // Delimiters are already highlighted by collect_delimiter_lines. + // Compute content range between delimiter lines for cell highlighting. + let content_start = find_line_end(input, block_start); let content_end = if block_end > 0 { find_line_start(input, block_end.saturating_sub(1)) } else { @@ -666,15 +546,7 @@ fn collect_table_row_block_spans( } /// Add spans for block metadata (attributes like `[source,rust]`, block titles). -/// -/// Metadata lines may either precede the block (scanned backwards) or be -/// included in the block's own location range (scanned forwards). -fn collect_block_metadata_spans( - input: &str, - metadata: &acdc_parser::BlockMetadata, - block_start: usize, - spans: &mut Vec, -) { +fn collect_block_metadata_spans(metadata: &acdc_parser::BlockMetadata, spans: &mut Vec) { if let Some(anchor) = &metadata.id { push_block_span(spans, &anchor.location, "adoc-anchor"); } @@ -683,86 +555,8 @@ fn collect_block_metadata_spans( push_block_span(spans, &anchor.location, "adoc-anchor"); } - let has_attrs = !metadata.attributes.is_empty() - || !metadata.positional_attributes.is_empty() - || !metadata.roles.is_empty() - || !metadata.options.is_empty() - || metadata.style.is_some(); - - // Even if metadata is empty (e.g. [NOTE] consumed into AdmonitionVariant), - // we should check for attribute lines in the source. - let looks_like_meta = input.get(block_start..).is_some_and(|s| { - let trimmed = s.trim_start(); - trimmed.starts_with('[') || (trimmed.starts_with('.') && !trimmed.starts_with("..")) - }); - - if has_attrs || looks_like_meta { - // Try scanning backwards first (metadata before block location) - scan_preceding_attributes(input, block_start, spans); - // Also scan forward from block_start for metadata lines included in - // the block's location (e.g. when `[source,rust]` is at byte 0) - scan_leading_attributes(input, block_start, spans); - } -} - -fn scan_preceding_attributes(input: &str, block_start: usize, spans: &mut Vec) { - let mut pos = block_start; - while pos > 0 { - let line_start = find_line_start(input, pos.saturating_sub(1)); - let line = input.get(line_start..pos).unwrap_or(""); - let trimmed = line.trim(); - if trimmed.starts_with('[') && trimmed.ends_with(']') { - spans.push(Span { - start: line_start, - end: pos, - class: "adoc-attribute", - priority: 1, - }); - pos = line_start; - } else if trimmed.starts_with('.') && !trimmed.starts_with("..") { - spans.push(Span { - start: line_start, - end: pos, - class: "adoc-block-title", - priority: 1, - }); - pos = line_start; - } else { - break; - } - } -} - -/// Scan forward from `block_start` for attribute/title lines that are part of -/// the block's location (e.g. `[source,rust]` at byte 0 before `----`). -fn scan_leading_attributes(input: &str, block_start: usize, spans: &mut Vec) { - let mut pos = block_start; - loop { - let line_end = find_line_end(input, pos); - let line = input.get(pos..line_end).unwrap_or(""); - let trimmed = line.trim(); - if trimmed.starts_with('[') && trimmed.ends_with(']') { - spans.push(Span { - start: pos, - end: line_end, - class: "adoc-attribute", - priority: 1, - }); - } else if trimmed.starts_with('.') && !trimmed.starts_with("..") { - spans.push(Span { - start: pos, - end: line_end, - class: "adoc-block-title", - priority: 1, - }); - } else { - break; - } - pos = if line_end < input.len() { - line_end + 1 - } else { - break; - }; + if let Some(loc) = &metadata.location { + push_block_span(spans, loc, "adoc-attribute"); } } @@ -865,10 +659,15 @@ fn collect_inline_spans(input: &str, nodes: &[InlineNode], spans: &mut Vec } } +#[allow(clippy::too_many_lines)] fn collect_single_inline_span(input: &str, node: &InlineNode, spans: &mut Vec) { match node { InlineNode::BoldText(b) => { - let effective_end = highlight_delimited_inline(input, &b.location, '*', spans); + let dlen = match b.form { + Form::Constrained => 1, + Form::Unconstrained => 2, + }; + let effective_end = highlight_delimited_inline(&b.location, dlen, spans); spans.push(Span { start: b.location.absolute_start, end: effective_end, @@ -878,7 +677,11 @@ fn collect_single_inline_span(input: &str, node: &InlineNode, spans: &mut Vec { - let effective_end = highlight_delimited_inline(input, &i.location, '_', spans); + let dlen = match i.form { + Form::Constrained => 1, + Form::Unconstrained => 2, + }; + let effective_end = highlight_delimited_inline(&i.location, dlen, spans); spans.push(Span { start: i.location.absolute_start, end: effective_end, @@ -888,7 +691,11 @@ fn collect_single_inline_span(input: &str, node: &InlineNode, spans: &mut Vec { - let effective_end = highlight_delimited_inline(input, &m.location, '`', spans); + let dlen = match m.form { + Form::Constrained => 1, + Form::Unconstrained => 2, + }; + let effective_end = highlight_delimited_inline(&m.location, dlen, spans); spans.push(Span { start: m.location.absolute_start, end: effective_end, @@ -898,7 +705,11 @@ fn collect_single_inline_span(input: &str, node: &InlineNode, spans: &mut Vec { - let effective_end = highlight_delimited_inline(input, &h.location, '#', spans); + let dlen = match h.form { + Form::Constrained => 1, + Form::Unconstrained => 2, + }; + let effective_end = highlight_delimited_inline(&h.location, dlen, spans); spans.push(Span { start: h.location.absolute_start, end: effective_end, @@ -908,7 +719,7 @@ fn collect_single_inline_span(input: &str, node: &InlineNode, spans: &mut Vec { - let effective_end = highlight_delimited_inline(input, &s.location, '^', spans); + let effective_end = highlight_delimited_inline(&s.location, 1, spans); spans.push(Span { start: s.location.absolute_start, end: effective_end, @@ -918,7 +729,7 @@ fn collect_single_inline_span(input: &str, node: &InlineNode, spans: &mut Vec { - let effective_end = highlight_delimited_inline(input, &s.location, '~', spans); + let effective_end = highlight_delimited_inline(&s.location, 1, spans); spans.push(Span { start: s.location.absolute_start, end: effective_end, @@ -928,7 +739,7 @@ fn collect_single_inline_span(input: &str, node: &InlineNode, spans: &mut Vec { - let effective_end = highlight_delimited_inline(input, &q.location, '"', spans); + let effective_end = highlight_delimited_inline(&q.location, 2, spans); spans.push(Span { start: q.location.absolute_start, end: effective_end, @@ -977,14 +788,9 @@ fn collect_macro_span(input: &str, mac: &InlineMacro, spans: &mut Vec) { _ => return, }; - // Parser locations use inclusive end (absolute_end points to last byte). - // Convert to exclusive end for the span system. + // Convert inclusive end to exclusive for the span system. let start = location.absolute_start; - let mut end = location.absolute_end + 1; - // Parser may also exclude trailing `]` — include it if present - if input.as_bytes().get(end) == Some(&b']') { - end += 1; - } + let end = location.absolute_end + 1; // Base span for the whole macro spans.push(Span { diff --git a/acdc-parser/CHANGELOG.md b/acdc-parser/CHANGELOG.md index f967951..51f97f8 100644 --- a/acdc-parser/CHANGELOG.md +++ b/acdc-parser/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`latexmath:[]` and `asciimath:[]` inline macros** — explicit notation overrides that set the stem notation directly instead of resolving from the `:stem:` document attribute. +- **`BlockMetadata.location`** — metadata blocks now carry an `Option` tracking + their source position (attribute lines, anchors, titles). +- **`DelimitedBlock.open_delimiter_location` / `close_delimiter_location`** — delimited + blocks now carry precise locations for both the opening and closing delimiter lines. +- **`DescriptionListItem.delimiter_location`** — description list items now carry the + source location of their delimiter (`::`, `:::`, etc.). ## [0.6.0] - 2026-02-23 diff --git a/acdc-parser/src/grammar/document.rs b/acdc-parser/src/grammar/document.rs index adf97d0..4fac74d 100644 --- a/acdc-parser/src/grammar/document.rs +++ b/acdc-parser/src/grammar/document.rs @@ -292,6 +292,7 @@ struct TableParseParams<'a> { close_delim: &'a str, content: &'a str, default_separator: &'a str, + close_start: usize, } /// Parse a table block from pre-extracted positions and content. @@ -309,12 +310,13 @@ fn parse_table_block_impl( offset, table_start, content_start, - content_end, + content_end: _content_end, end, open_delim, close_delim, content, default_separator, + close_start, } = params; check_delimiters( @@ -328,7 +330,11 @@ fn parse_table_block_impl( metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); let table_location = state.create_block_location(table_start, end, offset); - let _content_location = state.create_block_location(content_start, content_end, offset); + let open_delimiter_location = state.create_location( + table_start + offset, + table_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); let separator = if let Some(AttributeValue::String(sep)) = block_metadata.metadata.attributes.get("separator") @@ -648,6 +654,8 @@ fn parse_table_block_impl( inner: DelimitedBlockType::DelimitedTable(table), title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } @@ -1536,12 +1544,12 @@ peg::parser! { } rule block_metadata(offset: usize, parent_section_level: Option) -> Result - = lines:( + = meta_start:position!() lines:( anchor:anchor() { Ok::, Error>(BlockMetadataLine::Anchor(anchor)) } / attr:attributes_line() { Ok::, Error>(BlockMetadataLine::Attributes((attr.0, Box::new(attr.1)))) } / doc_attr:document_attribute_line() { Ok::, Error>(BlockMetadataLine::DocumentAttribute(doc_attr.key, doc_attr.value)) } / title:title_line(offset) { title.map(BlockMetadataLine::Title) } - )* + )* meta_end:position!() { let mut metadata = BlockMetadata::default(); let mut discrete = false; @@ -1586,6 +1594,9 @@ peg::parser! { } } } + if meta_start != meta_end { + metadata.location = Some(state.create_block_location(meta_start, meta_end, offset)); + } Ok(BlockParsingMetadata { metadata, title, @@ -1837,9 +1848,9 @@ peg::parser! { = lang:$((['a'..='z'] / ['A'..='Z'] / ['0'..='9'] / "_" / "+" / "-")+) { lang } rule example_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result - = open_delim:example_delimiter() eol() + = open_start:position!() open_delim:example_delimiter() eol() content_start:position!() content:until_example_delimiter(open_delim) content_end:position!() - eol() close_delim:example_delimiter() end:position!() + eol() close_start:position!() close_delim:example_delimiter() end:position!() { tracing::info!(?start, ?offset, ?content_start, ?block_metadata, ?content, "Parsing example block"); @@ -1847,6 +1858,11 @@ peg::parser! { let mut metadata = block_metadata.metadata.clone(); metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); let blocks = if content.trim().is_empty() { Vec::new() @@ -1872,13 +1888,15 @@ peg::parser! { inner: DelimitedBlockType::DelimitedExample(blocks), title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } rule comment_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result - = open_delim:comment_delimiter() eol() + = open_start:position!() open_delim:comment_delimiter() eol() content_start:position!() content:until_comment_delimiter(open_delim) content_end:position!() - eol() close_delim:comment_delimiter() end:position!() + eol() close_start:position!() close_delim:comment_delimiter() end:position!() { check_delimiters(open_delim, close_delim, "comment", state.create_error_source_location(state.create_block_location(start, end, offset)))?; let mut metadata = block_metadata.metadata.clone(); @@ -1886,6 +1904,11 @@ peg::parser! { let location = state.create_block_location(start, end, offset); let content_location = state.create_block_location(content_start, content_end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); Ok(Block::DelimitedBlock(DelimitedBlock { metadata, @@ -1897,6 +1920,8 @@ peg::parser! { })]), title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } @@ -1905,15 +1930,20 @@ peg::parser! { / markdown_listing_block(start, offset, block_metadata) rule traditional_listing_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result - = open_delim:listing_delimiter() eol() + = open_start:position!() open_delim:listing_delimiter() eol() content_start:position!() content:until_listing_delimiter(open_delim) content_end:position!() - eol() close_delim:listing_delimiter() end:position!() + eol() close_start:position!() close_delim:listing_delimiter() end:position!() { check_delimiters(open_delim, close_delim, "listing", state.create_error_source_location(state.create_block_location(start, end, offset)))?; let mut metadata = block_metadata.metadata.clone(); metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); let content_location = state.create_block_location(content_start, content_end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); let (inlines, callouts) = resolve_verbatim_callouts(content, content_location); state.last_block_was_verbatim = true; @@ -1925,13 +1955,15 @@ peg::parser! { inner: DelimitedBlockType::DelimitedListing(inlines), title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } rule markdown_listing_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result - = open_delim:markdown_code_delimiter() lang:markdown_language()? eol() + = open_start:position!() open_delim:markdown_code_delimiter() lang:markdown_language()? eol() content_start:position!() content:until_markdown_code_delimiter(open_delim) content_end:position!() - eol() close_delim:markdown_code_delimiter() end:position!() + eol() close_start:position!() close_delim:markdown_code_delimiter() end:position!() { check_delimiters(open_delim, close_delim, "listing", state.create_error_source_location(state.create_block_location(start, end, offset)))?; let mut metadata = block_metadata.metadata.clone(); @@ -1947,6 +1979,11 @@ peg::parser! { metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); let content_location = state.create_block_location(content_start, content_end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); let (inlines, callouts) = resolve_verbatim_callouts(content, content_location); state.last_block_was_verbatim = true; @@ -1958,15 +1995,19 @@ peg::parser! { inner: DelimitedBlockType::DelimitedListing(inlines), title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } pub(crate) rule literal_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result = + open_start:position!() open_delim:literal_delimiter() eol() content_start:position!() content:until_literal_delimiter(open_delim) content_end:position!() eol() + close_start:position!() close_delim:literal_delimiter() end:position!() { @@ -1975,6 +2016,11 @@ peg::parser! { metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); let content_location = state.create_block_location(content_start, content_end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); let (inlines, callouts) = resolve_verbatim_callouts(content, content_location); state.last_block_was_verbatim = true; @@ -1986,18 +2032,25 @@ peg::parser! { inner: DelimitedBlockType::DelimitedLiteral(inlines), title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } rule open_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result - = open_delim:open_delimiter() eol() + = open_start:position!() open_delim:open_delimiter() eol() content_start:position!() content:until_open_delimiter(open_delim) content_end:position!() - eol() close_delim:open_delimiter() end:position!() + eol() close_start:position!() close_delim:open_delimiter() end:position!() { check_delimiters(open_delim, close_delim, "open", state.create_error_source_location(state.create_block_location(start, end, offset)))?; let mut metadata = block_metadata.metadata.clone(); metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); let blocks = if content.trim().is_empty() { Vec::new() @@ -2014,13 +2067,15 @@ peg::parser! { inner: DelimitedBlockType::DelimitedOpen(blocks), title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } rule sidebar_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result - = open_delim:sidebar_delimiter() eol() + = open_start:position!() open_delim:sidebar_delimiter() eol() content_start:position!() content:until_sidebar_delimiter(open_delim) content_end:position!() - eol() close_delim:sidebar_delimiter() end:position!() + eol() close_start:position!() close_delim:sidebar_delimiter() end:position!() { tracing::info!(?start, ?offset, ?content_start, ?block_metadata, ?content, "Parsing sidebar block"); @@ -2028,6 +2083,11 @@ peg::parser! { let mut metadata = block_metadata.metadata.clone(); metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); let blocks = if content.trim().is_empty() { Vec::new() @@ -2044,6 +2104,8 @@ peg::parser! { inner: DelimitedBlockType::DelimitedSidebar(blocks), title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } @@ -2059,12 +2121,13 @@ peg::parser! { rule pipe_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result = table_start:position!() open_delim:pipe_table_delimiter() eol() content_start:position!() content:until_pipe_table_delimiter() content_end:position!() - eol() close_delim:pipe_table_delimiter() end:position!() + eol() close_start:position!() close_delim:pipe_table_delimiter() end:position!() { parse_table_block_impl( &TableParseParams { start, offset, table_start, content_start, content_end, end, open_delim, close_delim, content, default_separator: "|", + close_start, }, state, block_metadata, @@ -2074,12 +2137,13 @@ peg::parser! { rule excl_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result = table_start:position!() open_delim:excl_table_delimiter() eol() content_start:position!() content:until_excl_table_delimiter() content_end:position!() - eol() close_delim:excl_table_delimiter() end:position!() + eol() close_start:position!() close_delim:excl_table_delimiter() end:position!() { parse_table_block_impl( &TableParseParams { start, offset, table_start, content_start, content_end, end, open_delim, close_delim, content, default_separator: "!", + close_start, }, state, block_metadata, @@ -2089,12 +2153,13 @@ peg::parser! { rule comma_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result = table_start:position!() open_delim:comma_table_delimiter() eol() content_start:position!() content:until_comma_table_delimiter() content_end:position!() - eol() close_delim:comma_table_delimiter() end:position!() + eol() close_start:position!() close_delim:comma_table_delimiter() end:position!() { parse_table_block_impl( &TableParseParams { start, offset, table_start, content_start, content_end, end, open_delim, close_delim, content, default_separator: ",", + close_start, }, state, block_metadata, @@ -2104,12 +2169,13 @@ peg::parser! { rule colon_table_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result = table_start:position!() open_delim:colon_table_delimiter() eol() content_start:position!() content:until_colon_table_delimiter() content_end:position!() - eol() close_delim:colon_table_delimiter() end:position!() + eol() close_start:position!() close_delim:colon_table_delimiter() end:position!() { parse_table_block_impl( &TableParseParams { start, offset, table_start, content_start, content_end, end, open_delim, close_delim, content, default_separator: ":", + close_start, }, state, block_metadata, @@ -2117,15 +2183,20 @@ peg::parser! { } rule pass_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result - = open_delim:pass_delimiter() eol() + = open_start:position!() open_delim:pass_delimiter() eol() content_start:position!() content:until_pass_delimiter(open_delim) content_end:position!() - eol() close_delim:pass_delimiter() end:position!() + eol() close_start:position!() close_delim:pass_delimiter() end:position!() { check_delimiters(open_delim, close_delim, "pass", state.create_error_source_location(state.create_block_location(start, end, offset)))?; let mut metadata = block_metadata.metadata.clone(); metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); let content_location = state.create_block_location(content_start, content_end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); // Check if this is a stem block let inner = if let Some(ref style) = metadata.style { @@ -2166,19 +2237,26 @@ peg::parser! { inner, title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } rule quote_block(start: usize, offset: usize, block_metadata: &BlockParsingMetadata) -> Result - = open_delim:quote_delimiter() eol() + = open_start:position!() open_delim:quote_delimiter() eol() content_start:position!() content:until_quote_delimiter(open_delim) content_end:position!() - eol() close_delim:quote_delimiter() end:position!() + eol() close_start:position!() close_delim:quote_delimiter() end:position!() { check_delimiters(open_delim, close_delim, "quote", state.create_error_source_location(state.create_block_location(start, end, offset)))?; let mut metadata = block_metadata.metadata.clone(); metadata.move_positional_attributes_to_attributes(); let location = state.create_block_location(start, end, offset); let content_location = state.create_block_location(content_start, content_end, offset); + let open_delimiter_location = state.create_location( + open_start + offset, + open_start + offset + open_delim.len().saturating_sub(1), + ); + let close_delimiter_location = state.create_block_location(close_start, end, offset); let inner = if let Some(ref style) = metadata.style { if style == "verse" { @@ -2212,6 +2290,8 @@ peg::parser! { inner, title: block_metadata.title.clone(), location, + open_delimiter_location: Some(open_delimiter_location), + close_delimiter_location: Some(close_delimiter_location), })) } @@ -3406,7 +3486,7 @@ peg::parser! { rule description_list_item(offset: usize, block_metadata: &BlockParsingMetadata) -> Result = start:position!() term:$((!(description_list_marker() (eol() / " ") / eol()*<2,2>) [_])+) - delimiter:description_list_marker() + delim_start:position!() delimiter:description_list_marker() delim_end:position!() whitespace()? principal_start:position() principal_content:$( @@ -3485,10 +3565,12 @@ peg::parser! { }, ); + let delimiter_location = state.create_block_location(delim_start, delim_end, offset); Ok(DescriptionListItem { anchors: vec![], term, delimiter: delimiter.to_string(), + delimiter_location: Some(delimiter_location), principal_text, description, location: state.create_location(start+offset, actual_end+offset), @@ -5036,6 +5118,8 @@ peg::parser! { inner: DelimitedBlockType::DelimitedQuote(blocks), title: block_metadata.title.clone(), location: state.create_block_location(start, end, offset), + open_delimiter_location: None, + close_delimiter_location: None, })) } @@ -5116,6 +5200,8 @@ peg::parser! { inner: DelimitedBlockType::DelimitedQuote(blocks), title: block_metadata.title.clone(), location, + open_delimiter_location: None, + close_delimiter_location: None, })) } diff --git a/acdc-parser/src/grammar/inline_preprocessor.rs b/acdc-parser/src/grammar/inline_preprocessor.rs index 94a2bab..c587432 100644 --- a/acdc-parser/src/grammar/inline_preprocessor.rs +++ b/acdc-parser/src/grammar/inline_preprocessor.rs @@ -287,11 +287,6 @@ parser!( "Counters ({{{counter_type}:{name}}}) are not supported and will be removed from output" )); - // Calculate total length for position tracking - // We capture the full match including any optional initial value - let total_len = counter_type.len() + 1 + name.len() + 2; // "{" + counter_type + ":" + name + "}" - let _location = state.calculate_location(start, "", total_len); - // Return empty string - counter is removed from output String::new() } diff --git a/acdc-parser/src/model/lists.rs b/acdc-parser/src/model/lists.rs index e7ee341..b1f6a96 100644 --- a/acdc-parser/src/model/lists.rs +++ b/acdc-parser/src/model/lists.rs @@ -83,6 +83,9 @@ pub struct DescriptionListItem { pub term: Vec, /// The delimiter used (`::`, `:::`, `::::`, or `;;`). pub delimiter: String, + /// Location of the delimiter in the source. + #[serde(skip)] + pub delimiter_location: Option, /// Inline content immediately after the delimiter on the same line. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub principal_text: Vec, diff --git a/acdc-parser/src/model/metadata.rs b/acdc-parser/src/model/metadata.rs index 48adce4..9ba7688 100644 --- a/acdc-parser/src/model/metadata.rs +++ b/acdc-parser/src/model/metadata.rs @@ -5,6 +5,7 @@ use serde::Serialize; use super::anchor::Anchor; use super::attributes::{AttributeValue, ElementAttributes}; use super::attribution::{Attribution, CiteTitle}; +use super::location::Location; use super::substitution::SubstitutionSpec; pub type Role = String; @@ -39,6 +40,8 @@ pub struct BlockMetadata { pub attribution: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub citetitle: Option, + #[serde(skip)] + pub location: Option, } impl BlockMetadata { diff --git a/acdc-parser/src/model/mod.rs b/acdc-parser/src/model/mod.rs index 0a45443..9aba51b 100644 --- a/acdc-parser/src/model/mod.rs +++ b/acdc-parser/src/model/mod.rs @@ -409,6 +409,8 @@ pub struct DelimitedBlock { pub delimiter: String, pub title: Title, pub location: Location, + pub open_delimiter_location: Option, + pub close_delimiter_location: Option, } impl DelimitedBlock { @@ -421,6 +423,8 @@ impl DelimitedBlock { delimiter, title: Title::default(), location, + open_delimiter_location: None, + close_delimiter_location: None, } }