)>, W: StrWrite, E: EmbedCon
match tag {
Tag::HtmlBlock => Ok(()),
Tag::Paragraph => {
+ // Record paragraph start for boundary tracking
+ self.current_paragraph_start = Some((self.last_byte_offset, self.last_char_offset));
+
let node_id = self.gen_node_id();
if self.end_newline {
write!(&mut self.writer, "", node_id)?;
@@ -791,6 +857,10 @@ impl<'a, I: Iterator- , Range)>, W: StrWrite, E: EmbedCon
classes,
attrs,
} => {
+ // Record paragraph start for boundary tracking
+ // Treat headings as paragraph-level blocks
+ self.current_paragraph_start = Some((self.last_byte_offset, self.last_char_offset));
+
if !self.end_newline {
self.write("\n")?;
}
@@ -835,11 +905,24 @@ impl<'a, I: Iterator
- , Range)>, W: StrWrite, E: EmbedCon
self.write(">")?;
// Begin node tracking for offset mapping
- self.begin_node(node_id);
+ self.begin_node(node_id.clone());
+
+ // Map the start position of the heading (before any content)
+ // This allows cursor to be placed at the very beginning
+ let heading_start_char = self.last_char_offset;
+ let mapping = OffsetMapping {
+ byte_range: range.start..range.start,
+ char_range: heading_start_char..heading_start_char,
+ node_id: node_id.clone(),
+ char_offset_in_node: 0,
+ child_index: Some(0), // position before first child
+ utf16_len: 0,
+ };
+ self.offset_maps.push(mapping);
// Emit # syntax inside the heading tag
if range.start < range.end {
- let raw_text = &self.source[range];
+ let raw_text = &self.source[range.clone()];
let count = level as usize;
let pattern = "#".repeat(count);
@@ -849,10 +932,24 @@ impl<'a, I: Iterator
- , Range)>, W: StrWrite, E: EmbedCon
let syntax_start = hash_pos;
let syntax_end = (hash_pos + count + 1).min(raw_text.len());
let syntax = &raw_text[syntax_start..syntax_end];
+ let syntax_char_len = syntax.chars().count();
+
+ // Calculate byte range for this syntax in the source
+ let syntax_byte_start = range.start + syntax_start;
+ let syntax_byte_end = range.start + syntax_end;
+ let char_start = self.last_char_offset;
self.write("")?;
escape_html(&mut self.writer, syntax)?;
self.write("")?;
+
+ // Record offset mapping and update char tracking
+ // Note: last_byte_offset is managed by the main event loop
+ self.record_mapping(
+ syntax_byte_start..syntax_byte_end,
+ char_start..char_start + syntax_char_len
+ );
+ self.last_char_offset = char_start + syntax_char_len;
}
}
Ok(())
@@ -1193,10 +1290,24 @@ impl<'a, I: Iterator
- , Range)>, W: StrWrite, E: EmbedCon
let result = match tag {
TagEnd::HtmlBlock => Ok(()),
TagEnd::Paragraph => {
+ // Record paragraph end for boundary tracking
+ if let Some((byte_start, char_start)) = self.current_paragraph_start.take() {
+ let byte_range = byte_start..self.last_byte_offset;
+ let char_range = char_start..self.last_char_offset;
+ self.paragraph_ranges.push((byte_range, char_range));
+ }
+
self.end_node();
self.write("
\n")
}
TagEnd::Heading(level) => {
+ // Record paragraph end for boundary tracking
+ if let Some((byte_start, char_start)) = self.current_paragraph_start.take() {
+ let byte_range = byte_start..self.last_byte_offset;
+ let char_range = char_start..self.last_char_offset;
+ self.paragraph_ranges.push((byte_range, char_range));
+ }
+
self.end_node();
self.write("")?;
write!(&mut self.writer, "{}", level)?;
@@ -1255,16 +1366,12 @@ impl<'a, I: Iterator- , Range)>, W: StrWrite, E: EmbedCon
LazyLock::new(|| SyntaxSet::load_defaults_newlines());
if let Some((lang, buffer)) = self.code_buffer.take() {
- // Create offset mapping for code block content if we tracked a range
- if let Some(code_byte_range) = self.code_buffer_byte_range.take() {
- // Calculate char range from the tracked byte range
- let char_start = byte_to_char(self.source, code_byte_range.start);
- let char_end = byte_to_char(self.source, code_byte_range.end);
- let char_range = char_start..char_end;
-
+ // Create offset mapping for code block content if we tracked ranges
+ if let (Some(code_byte_range), Some(code_char_range)) =
+ (self.code_buffer_byte_range.take(), self.code_buffer_char_range.take()) {
// Record mapping before writing HTML
// (current_node_id should be set by start_tag for CodeBlock)
- self.record_mapping(code_byte_range, char_range);
+ self.record_mapping(code_byte_range, code_char_range);
}
if let Some(ref lang_str) = lang {
@@ -1348,65 +1455,9 @@ impl<'a, I: Iterator
- , Range)>, W: StrWrite, E: EmbedCon
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("")?;
- }
- }
+ // Note: Closing syntax for inline tags (Strong, Emphasis, etc.) is now handled
+ // by emit_gap_before(range.end) which is called before end_tag() in the main loop.
+ // No need for manual emission here anymore.
Ok(())
}