| foo | bar |
")?;
+
+ // Track offset mapping for code content
+ let char_start = self.last_char_offset;
+ let text_char_len = escape_html_body_text_with_char_count(&mut self.writer, &text)?;
+ let char_end = char_start + text_char_len;
+
+ // Record offset mapping (code content is visible)
+ self.record_mapping(range.clone(), char_start..char_end);
+ self.last_char_offset = char_end;
+
+ self.write("")?;
+
+ // Emit closing backtick
+ if range.start < range.end {
+ let raw_text = &self.source[range];
+ if raw_text.ends_with('`') {
+ self.write("`")?;
+ }
+ }
+ }
+ InlineMath(text) => {
+ // Emit opening $
+ if range.start < range.end {
+ let raw_text = &self.source[range.clone()];
+ if raw_text.starts_with('$') {
+ self.write("$")?;
+ }
+ }
+
+ self.write(r#""#)?;
+ escape_html(&mut self.writer, &text)?;
+ self.write("")?;
+
+ // Emit closing $
+ if range.start < range.end {
+ let raw_text = &self.source[range];
+ if raw_text.ends_with('$') {
+ self.write("$")?;
+ }
+ }
+ }
+ DisplayMath(text) => {
+ // Emit opening $$
+ if range.start < range.end {
+ let raw_text = &self.source[range.clone()];
+ if raw_text.starts_with("$$") {
+ self.write("$$")?;
+ }
+ }
+
+ self.write(r#""#)?;
+ escape_html(&mut self.writer, &text)?;
+ self.write("")?;
+
+ // Emit closing $$
+ if range.start < range.end {
+ let raw_text = &self.source[range];
+ if raw_text.ends_with("$$") {
+ self.write("$$")?;
+ }
+ }
+ }
+ Html(html) | InlineHtml(html) => {
+ // Track offset mapping for raw HTML
+ let char_start = self.last_char_offset;
+ let html_char_len = html.chars().count();
+ let char_end = char_start + html_char_len;
+
+ self.write(&html)?;
+
+ // Record mapping for inline HTML
+ self.record_mapping(range.clone(), char_start..char_end);
+ self.last_char_offset = char_end;
+ }
+ SoftBreak => self.write_newline()?,
+ HardBreak => {
+ // Emit the two spaces as visible (dimmed) text, then ", node_id)?; + } else { + write!(&mut self.writer, "\n
", node_id)?; + } + self.begin_node(node_id.clone()); + + // Map the start position of the paragraph (before any content) + // This allows cursor to be placed at the very beginning + let para_start_char = self.last_char_offset; + let mapping = OffsetMapping { + byte_range: range.start..range.start, + char_range: para_start_char..para_start_char, + node_id, + char_offset_in_node: 0, + child_index: Some(0), // position before first child + utf16_len: 0, + }; + self.offset_maps.push(mapping); + + // Emit > syntax if we're inside a blockquote + if let Some(bq_range) = self.pending_blockquote_range.take() { + if bq_range.start < bq_range.end { + let raw_text = &self.source[bq_range]; + if let Some(gt_pos) = raw_text.find('>') { + // Extract > [!NOTE] or just > + let after_gt = &raw_text[gt_pos + 1..]; + let syntax_end = if after_gt.trim_start().starts_with("[!") { + // Find the closing ] + if let Some(close_bracket) = after_gt.find(']') { + gt_pos + 1 + close_bracket + 1 + } else { + gt_pos + 1 + } + } else { + // Just > and maybe a space + (gt_pos + 2).min(raw_text.len()) + }; + + let syntax = &raw_text[gt_pos..syntax_end]; + self.write("")?; + escape_html(&mut self.writer, syntax)?; + self.write(" ")?; // Add space after + } + } + } + Ok(()) + } + Tag::Heading { + level, + id, + classes, + attrs, + } => { + if !self.end_newline { + self.write("\n")?; + } + + // Generate node ID for offset tracking + let node_id = self.gen_node_id(); + + self.write("<")?; + write!(&mut self.writer, "{}", level)?; + + // Add our tracking ID as data attribute (preserve user's id if present) + self.write(" data-node-id=\"")?; + self.write(&node_id)?; + self.write("\"")?; + + if let Some(id) = id { + self.write(" id=\"")?; + escape_html(&mut self.writer, &id)?; + self.write("\"")?; + } + if !classes.is_empty() { + self.write(" class=\"")?; + for (i, class) in classes.iter().enumerate() { + if i > 0 { + self.write(" ")?; + } + escape_html(&mut self.writer, class)?; + } + self.write("\"")?; + } + for (attr, value) in attrs { + self.write(" ")?; + escape_html(&mut self.writer, &attr)?; + if let Some(val) = value { + self.write("=\"")?; + escape_html(&mut self.writer, &val)?; + self.write("\"")?; + } else { + self.write("=\"\"")?; + } + } + self.write(">")?; + + // Begin node tracking for offset mapping + self.begin_node(node_id); + + // Emit # syntax inside the heading tag + if range.start < range.end { + let raw_text = &self.source[range]; + let count = level as usize; + let pattern = "#".repeat(count); + + // Find where the # actually starts (might have leading whitespace) + if let Some(hash_pos) = raw_text.find(&pattern) { + // Extract "# " or "## " etc + let syntax_start = hash_pos; + let syntax_end = (hash_pos + count + 1).min(raw_text.len()); + let syntax = &raw_text[syntax_start..syntax_end]; + + self.write("")?; + escape_html(&mut self.writer, syntax)?; + self.write("")?; + } + } + Ok(()) + } + Tag::Table(alignments) => { + if self.render_tables_as_markdown { + // Store start offset and skip HTML rendering + self.table_start_offset = Some(range.start); + self.in_non_writing_block = true; // Suppress content output + Ok(()) + } else { + self.table_alignments = alignments; + self.write("
| self.write(" | self.write(" style=\"text-align: left\">"),
+ Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"),
+ Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"),
+ _ => self.write(">"),
+ }
+ }
+ }
+ Tag::BlockQuote(kind) => {
+ let class_str = match kind {
+ None => "",
+ Some(BlockQuoteKind::Note) => " class=\"markdown-alert-note\"",
+ Some(BlockQuoteKind::Tip) => " class=\"markdown-alert-tip\"",
+ Some(BlockQuoteKind::Important) => " class=\"markdown-alert-important\"",
+ Some(BlockQuoteKind::Warning) => " class=\"markdown-alert-warning\"",
+ Some(BlockQuoteKind::Caution) => " class=\"markdown-alert-caution\"",
+ };
+ if self.end_newline {
+ write!(&mut self.writer, "\n", class_str)?; + } else { + write!(&mut self.writer, "\n\n", class_str)?; + } + + // Store range for emitting > inside the next paragraph + self.pending_blockquote_range = Some(range); + Ok(()) + } + Tag::CodeBlock(info) => { + if !self.end_newline { + self.write_newline()?; + } + + // Generate node ID for code block + let node_id = self.gen_node_id(); + + match info { + CodeBlockKind::Fenced(info) => { + // Emit opening ```language + if range.start < range.end { + let raw_text = &self.source[range]; + if let Some(fence_pos) = raw_text.find("```") { + let fence_end = (fence_pos + 3 + info.len()).min(raw_text.len()); + let syntax = &raw_text[fence_pos..fence_end]; + self.write("")?; + escape_html(&mut self.writer, syntax)?; + self.write("\n")?; + } + } + + let lang = info.split(' ').next().unwrap(); + let lang_opt = if lang.is_empty() { + None + } else { + Some(lang.to_string()) + }; + // Start buffering + self.code_buffer = Some((lang_opt, String::new())); + + // Begin node tracking for offset mapping + self.begin_node(node_id); + Ok(()) + } + CodeBlockKind::Indented => { + // Ignore indented code blocks (as per executive decision) + self.code_buffer = Some((None, String::new())); + + // Begin node tracking for offset mapping + self.begin_node(node_id); + Ok(()) + } + } + } + Tag::List(Some(1)) => { + if self.end_newline { + self.write(" |
|---|
")?;
+ escape_html_body_text(&mut self.writer, &buffer)?;
+ self.write("\n")?;
+ }
+ }
+ } else {
+ self.write("")?;
+ escape_html_body_text(&mut self.writer, &buffer)?;
+ self.write("\n")?;
+ }
+
+ // End node tracking
+ self.end_node();
+ } else {
+ self.write("\n")?;
+ }
+
+ // Emit closing ```
+ if range.start < range.end {
+ let raw_text = &self.source[range.clone()];
+ if let Some(fence_line) = raw_text.lines().last() {
+ if fence_line.trim() == "```" {
+ self.write("```")?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+ TagEnd::List(true) => self.write("\n"),
+ TagEnd::List(false) => self.write("\n"),
+ TagEnd::Item => {
+ self.end_node();
+ self.write("\n")
+ }
+ TagEnd::DefinitionList => self.write("\n"),
+ TagEnd::DefinitionListTitle => {
+ self.end_node();
+ self.write("\n")
+ }
+ TagEnd::DefinitionListDefinition => {
+ self.end_node();
+ self.write("\n")
+ }
+ TagEnd::Emphasis => self.write(""),
+ TagEnd::Superscript => self.write(""),
+ TagEnd::Subscript => self.write(""),
+ TagEnd::Strong => self.write(""),
+ TagEnd::Strikethrough => self.write(""),
+ TagEnd::Link => self.write(""),
+ TagEnd::Image => Ok(()), // No-op: raw_text() already consumed the End(Image) event
+ TagEnd::Embed => Ok(()),
+ TagEnd::WeaverBlock(_) => {
+ self.in_non_writing_block = false;
+ Ok(())
+ }
+ TagEnd::FootnoteDefinition => self.write("\n"),
+ TagEnd::MetadataBlock(_) => {
+ self.in_non_writing_block = false;
+ Ok(())
+ }
+ };
+
+ result?;
+
+ // Extract and emit closing syntax based on tag type
+ if range.start < range.end {
+ let raw_text = &self.source[range];
+ let closing_syntax = match &tag {
+ TagEnd::Strong => {
+ if raw_text.ends_with("**") {
+ Some("**")
+ } else if raw_text.ends_with("__") {
+ Some("__")
+ } else {
+ None
+ }
+ }
+ TagEnd::Emphasis => {
+ if raw_text.ends_with("*") {
+ Some("*")
+ } else if raw_text.ends_with("_") {
+ Some("_")
+ } else {
+ None
+ }
+ }
+ TagEnd::Strikethrough => {
+ if raw_text.ends_with("~~") {
+ Some("~~")
+ } else {
+ None
+ }
+ }
+ TagEnd::Link => {
+ // Extract ](url) part
+ if let Some(idx) = raw_text.rfind("](") {
+ Some(&raw_text[idx..])
+ } else {
+ None
+ }
+ }
+ TagEnd::CodeBlock => {
+ if raw_text.ends_with("```") {
+ raw_text.lines().last()
+ } else {
+ None
+ }
+ }
+ _ => None,
+ };
+
+ if let Some(syntax) = closing_syntax {
+ let class = match classify_syntax(syntax) {
+ SyntaxClass::Inline => "md-syntax-inline",
+ SyntaxClass::Block => "md-syntax-block",
+ };
+ self.write("")?;
+ escape_html(&mut self.writer, syntax)?;
+ self.write("")?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl<'a, I: Iterator