1use crate::{
2 Diagnostic, DiagnosticSeverity, Document, MDI_IR_VERSION, MDI_SPEC_VERSION, ParserCapabilities,
3 SourceSpan, diagnostics, parse_document,
4};
5use serde::Serialize;
6use unicode_segmentation::UnicodeSegmentation;
7
8pub(crate) enum PlainInline<'a> {
9 Value(&'a str),
10 Break,
11 Skip,
12 Children,
13}
14
15pub(crate) fn plain_inline(node: &serde_json::Value) -> PlainInline<'_> {
18 match node_type(node) {
19 "text" | "inlineCode" | "code" | "html" | "tcy" => PlainInline::Value(
20 node.get("value")
21 .and_then(serde_json::Value::as_str)
22 .unwrap_or_default(),
23 ),
24 "ruby" => PlainInline::Value(
25 node.get("base")
26 .and_then(serde_json::Value::as_str)
27 .unwrap_or_default(),
28 ),
29 "image" => PlainInline::Value(
30 node.get("alt")
31 .and_then(serde_json::Value::as_str)
32 .unwrap_or_default(),
33 ),
34 "break" => PlainInline::Break,
35 "footnoteReference" => PlainInline::Skip,
36 _ if node.get("children").is_some() => PlainInline::Children,
37 _ => node
38 .get("value")
39 .and_then(serde_json::Value::as_str)
40 .map_or(PlainInline::Skip, PlainInline::Value),
41 }
42}
43
44pub const MDI_TEXT_PROJECTION_VERSION: &str = "1.0";
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct MdiTextBlocksResult {
49 pub projection_version: &'static str,
50 pub position_encoding: &'static str,
51 pub ir_version: &'static str,
52 pub syntax_version: &'static str,
53 pub capabilities: ParserCapabilities,
54 pub blocks: Vec<MdiTextBlock>,
55 pub document: Document,
56 pub diagnostics: Vec<Diagnostic>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct MdiTextBlock {
62 pub index: u32,
63 pub kind: MdiTextBlockKind,
64 pub text: String,
65 pub range: MdiTextRange,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub span: Option<SourceSpan>,
68 pub source_map: MdiTextSourceMap,
69 pub annotations: Vec<MdiTextAnnotation>,
70 pub node: serde_json::Value,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
74#[serde(rename_all = "camelCase")]
75pub enum MdiTextBlockKind {
76 Heading,
77 Paragraph,
78 ListItem,
79 Blockquote,
80 Code,
81 Table,
82 Footnote,
83 Html,
84 Other,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct MdiTextPosition {
90 pub block: u32,
91 pub character: u32,
92}
93
94impl Serialize for MdiTextPosition {
95 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96 where
97 S: serde::Serializer,
98 {
99 serializer.serialize_str(&format!("{}:{}", self.block, self.character))
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
104pub struct MdiTextRange {
105 pub start: MdiTextPosition,
106 pub end: MdiTextPosition,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
110pub struct MdiTextSourceMap {
111 pub runs: Vec<MdiTextSourceRun>,
112 pub synthetic: Vec<MdiTextRange>,
113 pub unmapped: Vec<MdiTextRange>,
114}
115
116pub type MdiAnnotationSourceMap = MdiTextSourceMap;
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct MdiTextSourceRun {
123 pub range: MdiTextRange,
124 pub source_boundaries: Vec<u32>,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
128#[serde(rename_all = "camelCase")]
129pub struct MdiTextAnnotation {
130 pub kind: &'static str,
131 pub text: String,
132 pub anchor: MdiTextRange,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub span: Option<SourceSpan>,
135 pub source_map: MdiAnnotationSourceMap,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139enum UnitMap {
140 Mapped(SourceSpan),
141 Synthetic,
142 Unmapped,
143}
144
145struct AnnotationDraft {
146 text: String,
147 anchor_start: usize,
148 anchor_end: usize,
149 span: Option<SourceSpan>,
150 units: Vec<UnitMap>,
151}
152
153struct BlockDraft {
154 kind: MdiTextBlockKind,
155 text: String,
156 units: Vec<UnitMap>,
157 unit_texts: Vec<String>,
158 annotations: Vec<AnnotationDraft>,
159 span: Option<SourceSpan>,
160 node: serde_json::Value,
161 mapping_warning: bool,
162 source_cursor: u32,
163 source_end: u32,
164}
165
166impl BlockDraft {
167 fn new(kind: MdiTextBlockKind, node: &serde_json::Value) -> Self {
168 let span = node_span(node);
169 Self {
170 kind,
171 text: String::new(),
172 units: Vec::new(),
173 unit_texts: Vec::new(),
174 annotations: Vec::new(),
175 span,
176 node: node.clone(),
177 mapping_warning: false,
178 source_cursor: span.map_or(0, |span| span.start_byte),
179 source_end: span.map_or(0, |span| span.end_byte),
180 }
181 }
182
183 fn grapheme_len(&self) -> usize {
184 self.text.graphemes(true).count()
185 }
186
187 fn append_synthetic(&mut self, value: &str) {
188 self.text.push_str(value);
189 for grapheme in value.graphemes(true) {
190 self.unit_texts.push(grapheme.to_owned());
191 self.units.push(UnitMap::Synthetic);
192 }
193 }
194
195 fn append_unmapped(&mut self, value: &str) {
196 if value.is_empty() {
197 return;
198 }
199 self.text.push_str(value);
200 for grapheme in value.graphemes(true) {
201 self.unit_texts.push(grapheme.to_owned());
202 self.units.push(UnitMap::Unmapped);
203 }
204 self.mapping_warning = true;
205 }
206
207 fn append_mapped(&mut self, value: &str, spans: Option<Vec<SourceSpan>>) {
208 if value.is_empty() {
209 return;
210 }
211 let count = value.graphemes(true).count();
212 match spans {
213 Some(spans) if spans.len() == count => {
214 self.text.push_str(value);
215 for (grapheme, span) in value.graphemes(true).zip(spans) {
216 self.unit_texts.push(grapheme.to_owned());
217 self.units.push(UnitMap::Mapped(span));
218 }
219 }
220 _ => self.append_unmapped(value),
221 }
222 }
223}
224
225struct Collector<'a> {
226 source: &'a str,
227 blocks: Vec<MdiTextBlock>,
228 diagnostics: Vec<Diagnostic>,
229}
230
231pub fn get_mdi_text_blocks(source: &str) -> MdiTextBlocksResult {
234 let document = parse_document(source);
235 let mut collector = Collector {
236 source,
237 blocks: Vec::new(),
238 diagnostics: diagnostics(&document),
239 };
240 for node in &document.children {
241 collector.collect(node, false);
242 }
243 MdiTextBlocksResult {
244 projection_version: MDI_TEXT_PROJECTION_VERSION,
245 position_encoding: "unicode-grapheme-cluster-1-based",
246 ir_version: MDI_IR_VERSION,
247 syntax_version: MDI_SPEC_VERSION,
248 capabilities: ParserCapabilities {
249 mdi: true,
250 common_mark: true,
251 gfm: true,
252 front_matter: true,
253 source_spans: true,
254 },
255 blocks: collector.blocks,
256 document,
257 diagnostics: collector.diagnostics,
258 }
259}
260
261pub fn get_mdi_text_blocks_json(source: &str) -> String {
262 serde_json::to_string(&get_mdi_text_blocks(source))
263 .expect("serializing the MDI text projection cannot fail")
264}
265
266impl Collector<'_> {
267 fn collect(&mut self, node: &serde_json::Value, quoted: bool) {
268 let kind = node_type(node);
269 match kind {
270 "heading" => self.inline_block(MdiTextBlockKind::Heading, node),
271 "paragraph" => self.inline_block(
272 if quoted {
273 MdiTextBlockKind::Blockquote
274 } else {
275 MdiTextBlockKind::Paragraph
276 },
277 node,
278 ),
279 "blockquote" => {
280 for child in children(node) {
281 self.collect(child, true);
282 }
283 }
284 "list" => {
285 for child in children(node) {
286 self.collect(child, quoted);
287 }
288 }
289 "listItem" => self.list_item(node, quoted),
290 "code" => self.scalar_block(MdiTextBlockKind::Code, node, "value"),
291 "html" => self.scalar_block(MdiTextBlockKind::Html, node, "value"),
292 "table" => self.table(node),
293 "footnoteDefinition" => self.footnote(node),
294 "yaml" | "definition" | "blank" | "pagebreak" | "thematicBreak" => {}
295 _ => {
296 if node_span(node).is_some() {
297 let mut draft = BlockDraft::new(MdiTextBlockKind::Other, node);
298 self.project_inline(node, &mut draft);
299 self.finish(draft);
300 }
301 }
302 }
303 }
304
305 fn inline_block(&mut self, kind: MdiTextBlockKind, node: &serde_json::Value) {
306 let mut draft = BlockDraft::new(kind, node);
307 self.project_children(node, &mut draft);
308 self.finish(draft);
309 }
310
311 fn scalar_block(&mut self, kind: MdiTextBlockKind, node: &serde_json::Value, field: &str) {
312 let mut draft = BlockDraft::new(kind, node);
313 if let Some(value) = node.get(field).and_then(serde_json::Value::as_str) {
314 let normalized;
318 let value = if kind == MdiTextBlockKind::Code && value.contains('\r') {
319 normalized = value.replace("\r\n", "\n").replace('\r', "\n");
320 normalized.as_str()
321 } else {
322 value
323 };
324 let spans = if kind == MdiTextBlockKind::Code {
325 self.map_code_block(value, &mut draft)
326 } else {
327 self.map_value_in_block(value, &mut draft)
328 };
329 draft.append_mapped(value, spans);
330 }
331 self.finish(draft);
332 }
333
334 fn list_item(&mut self, node: &serde_json::Value, quoted: bool) {
335 let paragraphs: Vec<_> = children(node)
336 .filter(|child| node_type(child) == "paragraph")
337 .collect();
338 if !paragraphs.is_empty() {
339 let mut draft = BlockDraft::new(MdiTextBlockKind::ListItem, node);
340 for (index, paragraph) in paragraphs.into_iter().enumerate() {
341 if index > 0 {
342 draft.append_synthetic("\n\n");
343 }
344 self.project_children(paragraph, &mut draft);
345 }
346 self.finish(draft);
347 }
348 for child in children(node) {
349 if node_type(child) != "paragraph" {
350 self.collect(child, quoted);
351 }
352 }
353 }
354
355 fn footnote(&mut self, node: &serde_json::Value) {
356 let mut draft = BlockDraft::new(MdiTextBlockKind::Footnote, node);
357 for (index, child) in children(node).enumerate() {
358 if index > 0 {
359 draft.append_synthetic("\n\n");
360 }
361 if node_type(child) == "paragraph" {
362 self.project_children(child, &mut draft);
363 } else {
364 self.project_inline(child, &mut draft);
365 }
366 }
367 self.finish(draft);
368 }
369
370 fn table(&mut self, node: &serde_json::Value) {
371 let mut draft = BlockDraft::new(MdiTextBlockKind::Table, node);
372 for (row_index, row) in children(node).enumerate() {
373 if row_index > 0 {
374 draft.append_synthetic("\n");
375 }
376 for (cell_index, cell) in children(row).enumerate() {
377 if cell_index > 0 {
378 draft.append_synthetic("\t");
379 }
380 self.project_children(cell, &mut draft);
381 }
382 }
383 self.finish(draft);
384 }
385
386 fn project_children(&mut self, node: &serde_json::Value, draft: &mut BlockDraft) {
387 for child in children(node) {
388 self.project_inline(child, draft);
389 }
390 }
391
392 fn project_inline(&mut self, node: &serde_json::Value, draft: &mut BlockDraft) {
393 match plain_inline(node) {
394 PlainInline::Value(value) => {
395 if node_type(node) == "ruby" {
396 self.project_ruby(node, draft);
397 return;
398 }
399 let spans = match node_type(node) {
400 "tcy" => self.map_delimited_in_block(value, &mut *draft, '^', '^'),
401 "image" => self.map_image_in_block(value, draft),
402 "inlineCode" => self.map_inline_code_in_block(value, draft),
403 _ => self.map_value_from_node(value, node, draft),
404 };
405 draft.append_mapped(value, spans);
406 }
407 PlainInline::Break => {
408 let spans = self.map_break_in_block(draft);
409 draft.append_mapped("\n", spans);
410 }
411 PlainInline::Skip => {}
412 PlainInline::Children => {
413 self.project_children(node, draft);
414 self.advance_after_container(node, draft);
415 }
416 }
417 }
418
419 fn project_ruby(&mut self, node: &serde_json::Value, draft: &mut BlockDraft) {
420 let base = node
421 .get("base")
422 .and_then(serde_json::Value::as_str)
423 .unwrap_or_default();
424 let base_start = draft.grapheme_len();
425 let reading_value = node
426 .pointer("/ruby/value")
427 .map(|value| match value {
428 serde_json::Value::String(value) => value.clone(),
429 serde_json::Value::Array(values) => values
430 .iter()
431 .filter_map(serde_json::Value::as_str)
432 .collect::<String>(),
433 _ => String::new(),
434 })
435 .unwrap_or_default();
436 let parts = find_ruby_parts(
437 base,
438 &reading_value,
439 self.source,
440 draft.source_cursor,
441 draft.source_end,
442 );
443 if let Some(parts) = &parts {
444 draft.source_cursor = parts.token_end;
445 }
446 let base_spans = parts
447 .as_ref()
448 .and_then(|parts| map_decoded(base, parts.base, parts.base_start));
449 draft.append_mapped(base, base_spans);
450 let base_end = draft.grapheme_len();
451
452 let ruby = node.get("ruby");
453 let ruby_type = ruby
454 .and_then(|value| value.get("type"))
455 .and_then(serde_json::Value::as_str)
456 .unwrap_or("group");
457 if ruby_type == "split" {
458 let readings = ruby
459 .and_then(|value| value.get("value"))
460 .and_then(serde_json::Value::as_array);
461 if let Some(readings) = readings {
462 let raw_parts = parts.as_ref().map(|value| split_raw_reading(value));
463 for (index, reading) in readings
464 .iter()
465 .filter_map(serde_json::Value::as_str)
466 .enumerate()
467 {
468 let raw = raw_parts.as_ref().and_then(|parts| parts.get(index));
469 let units = annotation_units(reading, raw.copied());
470 draft.annotations.push(AnnotationDraft {
471 text: reading.to_owned(),
472 anchor_start: base_start + index,
473 anchor_end: base_start + index + 1,
474 span: raw.map(|part| SourceSpan {
475 start_byte: part.1,
476 end_byte: part.1 + part.0.len() as u32,
477 }),
478 units,
479 });
480 }
481 return;
482 }
483 }
484
485 let reading = ruby
486 .and_then(|value| value.get("value"))
487 .and_then(serde_json::Value::as_str)
488 .unwrap_or_default();
489 if reading.is_empty() {
490 return;
491 }
492 let raw = parts
493 .as_ref()
494 .map(|parts| (parts.reading, parts.reading_start));
495 let had_split_syntax = parts
496 .as_ref()
497 .is_some_and(|parts| split_unescaped_offsets(parts.reading, '.').len() > 1);
498 if had_split_syntax {
499 self.diagnostics.push(Diagnostic {
500 severity: DiagnosticSeverity::Warning,
501 code: "mdi.textProjection.rubySplitMismatch".to_owned(),
502 message: "split ruby component count does not match the base grapheme count; the reading was anchored to the complete base".to_owned(),
503 span: parts.as_ref().map(|parts| SourceSpan {
504 start_byte: parts.token_start,
505 end_byte: parts.token_end,
506 }),
507 });
508 }
509 draft.annotations.push(AnnotationDraft {
510 text: reading.to_owned(),
511 anchor_start: base_start,
512 anchor_end: base_end,
513 span: raw.map(|(raw, start)| SourceSpan {
514 start_byte: start,
515 end_byte: start + raw.len() as u32,
516 }),
517 units: annotation_ruby_units(reading, raw),
518 });
519 }
520
521 fn finish(&mut self, draft: BlockDraft) {
522 if draft.text.is_empty() {
523 return;
524 }
525 let index = self.blocks.len() as u32 + 1;
526 let grapheme_count = draft.text.graphemes(true).count();
527 let units = normalize_units(&draft.text, &draft.unit_texts, &draft.units);
528 let mapping_warning = draft.mapping_warning
529 || units.contains(&UnitMap::Unmapped)
530 || draft
531 .annotations
532 .iter()
533 .any(|annotation| annotation.units.contains(&UnitMap::Unmapped));
534 if mapping_warning {
535 self.diagnostics.push(Diagnostic {
536 severity: DiagnosticSeverity::Warning,
537 code: "mdi.textProjection.unmapped".to_owned(),
538 message: format!(
539 "text block {index} contains text that could not be mapped precisely"
540 ),
541 span: draft.span,
542 });
543 }
544 let annotations = draft
545 .annotations
546 .into_iter()
547 .map(|annotation| MdiTextAnnotation {
548 kind: "rubyReading",
549 text: annotation.text,
550 anchor: text_range(index, annotation.anchor_start, annotation.anchor_end),
551 span: annotation.span,
552 source_map: source_map(index, &annotation.units),
553 })
554 .collect();
555 self.blocks.push(MdiTextBlock {
556 index,
557 kind: draft.kind,
558 text: draft.text,
559 range: text_range(index, 0, grapheme_count),
560 span: draft.span,
561 source_map: source_map(index, &units),
562 annotations,
563 node: draft.node,
564 });
565 }
566
567 fn map_value_in_block(&self, value: &str, draft: &mut BlockDraft) -> Option<Vec<SourceSpan>> {
568 let mapped = find_mapped_value(value, self.source, draft.source_cursor, draft.source_end)?;
569 draft.source_cursor = mapped.consumed_end;
570 Some(mapped.spans)
571 }
572
573 fn map_value_from_node(
574 &self,
575 value: &str,
576 node: &serde_json::Value,
577 draft: &mut BlockDraft,
578 ) -> Option<Vec<SourceSpan>> {
579 if let Some(span) = node_span(node) {
580 let mut suggested = span.start_byte;
581 if suggested > draft.source_cursor
582 && self.source.as_bytes().get(suggested as usize - 1) == Some(&b'\\')
583 {
584 suggested -= 1;
585 }
586 if suggested >= draft.source_cursor && suggested <= draft.source_end {
587 draft.source_cursor = suggested;
588 }
589 }
590 self.map_value_in_block(value, draft)
591 }
592
593 fn map_delimited_in_block(
594 &self,
595 value: &str,
596 draft: &mut BlockDraft,
597 open: char,
598 close: char,
599 ) -> Option<Vec<SourceSpan>> {
600 let needle = format!("{open}{value}{close}");
601 let raw = self
602 .source
603 .get(draft.source_cursor as usize..draft.source_end as usize)?;
604 let offset = raw.find(&needle)?;
605 let token_start = draft.source_cursor + offset as u32;
606 draft.source_cursor = token_start + needle.len() as u32;
607 map_direct(value, token_start + open.len_utf8() as u32)
608 }
609
610 fn map_image_in_block(&self, value: &str, draft: &mut BlockDraft) -> Option<Vec<SourceSpan>> {
611 let raw = self
612 .source
613 .get(draft.source_cursor as usize..draft.source_end as usize)?;
614 let image_start = raw.find("![")?;
615 let alt_start = image_start + 2;
616 let alt_end = first_unescaped(&raw[alt_start..], ']')? + alt_start;
617 let mapped = map_decoded(
618 value,
619 &raw[alt_start..alt_end],
620 draft.source_cursor + alt_start as u32,
621 )?;
622 let consumed = raw[alt_end..]
623 .find(')')
624 .map_or(alt_end + 1, |end| alt_end + end + 1);
625 draft.source_cursor += consumed as u32;
626 Some(mapped)
627 }
628
629 fn map_inline_code_in_block(
630 &self,
631 value: &str,
632 draft: &mut BlockDraft,
633 ) -> Option<Vec<SourceSpan>> {
634 let raw = self
635 .source
636 .get(draft.source_cursor as usize..draft.source_end as usize)?;
637 for (offset, _) in raw.match_indices('`') {
638 let opening = raw[offset..]
639 .chars()
640 .take_while(|character| *character == '`')
641 .count();
642 let delimiter = "`".repeat(opening);
643 let inner_start = offset + opening;
644 let Some(close_offset) = raw[inner_start..].find(&delimiter) else {
645 continue;
646 };
647 let inner_end = inner_start + close_offset;
648 let inner = &raw[inner_start..inner_end];
649 let mut normalized = String::new();
650 let mut spans = Vec::new();
651 for (grapheme_offset, grapheme) in inner.grapheme_indices(true) {
652 normalized.push_str(if grapheme == "\n" || grapheme == "\r\n" {
653 " "
654 } else {
655 grapheme
656 });
657 spans.push(SourceSpan {
658 start_byte: draft.source_cursor + inner_start as u32 + grapheme_offset as u32,
659 end_byte: draft.source_cursor
660 + inner_start as u32
661 + grapheme_offset as u32
662 + grapheme.len() as u32,
663 });
664 }
665 if normalized.starts_with(' ')
666 && normalized.ends_with(' ')
667 && normalized.chars().any(|character| character != ' ')
668 {
669 normalized.remove(0);
670 normalized.pop();
671 spans.remove(0);
672 spans.pop();
673 }
674 if normalized == value {
675 draft.source_cursor += (inner_end + opening) as u32;
676 return Some(spans);
677 }
678 }
679 None
680 }
681
682 fn map_code_block(&self, value: &str, draft: &mut BlockDraft) -> Option<Vec<SourceSpan>> {
683 let raw = self
684 .source
685 .get(draft.source_cursor as usize..draft.source_end as usize)?;
686 let trimmed = raw.trim_start_matches([' ', '\t']);
687 let fenced = trimmed.starts_with("```") || trimmed.starts_with("~~~");
688 if fenced {
689 let opening_end = raw.find('\n')? + 1;
690 draft.source_cursor += opening_end as u32;
691 }
692 self.map_value_in_block(value, draft)
693 }
694
695 fn advance_after_container(&self, node: &serde_json::Value, draft: &mut BlockDraft) {
696 let Some(raw) = self
697 .source
698 .get(draft.source_cursor as usize..draft.source_end as usize)
699 else {
700 return;
701 };
702 let consumed = match node_type(node) {
703 "link" => raw.find(']').map(|label_end| {
704 let after_label = label_end + 1;
705 if raw[after_label..].starts_with('(') {
706 raw[after_label + 1..]
707 .find(')')
708 .map_or(after_label, |end| after_label + end + 2)
709 } else if raw[after_label..].starts_with('[') {
710 raw[after_label + 1..]
711 .find(']')
712 .map_or(after_label, |end| after_label + end + 2)
713 } else {
714 after_label
715 }
716 }),
717 "noBreak" | "warichu" | "kern" => raw.find("\x5d\x5d").map(|offset| offset + 2),
718 "em" => raw
719 .find("\x5d\x5d")
720 .map(|offset| offset + 2)
721 .or_else(|| raw.find("》》").map(|offset| offset + "》》".len())),
722 "emphasis" | "strong" | "delete" => {
723 let width = if node_type(node) == "emphasis" { 1 } else { 2 };
724 raw.char_indices()
725 .find(|(_, character)| matches!(character, '*' | '_' | '~'))
726 .map(|(offset, character)| offset + character.len_utf8() * width)
727 }
728 _ => None,
729 };
730 if let Some(consumed) = consumed {
731 draft.source_cursor += consumed as u32;
732 }
733 }
734
735 fn map_break_in_block(&self, draft: &mut BlockDraft) -> Option<Vec<SourceSpan>> {
736 let raw = self
737 .source
738 .get(draft.source_cursor as usize..draft.source_end as usize)?;
739 if let Some(offset) = raw.find("[[br]]") {
740 let span = SourceSpan {
741 start_byte: draft.source_cursor + offset as u32,
742 end_byte: draft.source_cursor + offset as u32 + "[[br]]".len() as u32,
743 };
744 draft.source_cursor = span.end_byte;
745 return Some(vec![span]);
746 }
747 let newline = raw.find('\n')?;
748 let prefix = &raw[..newline];
749 let marker_prefix = prefix.strip_suffix('\r').unwrap_or(prefix);
750 let marker_start = marker_prefix
751 .rfind('\\')
752 .unwrap_or_else(|| marker_prefix.trim_end_matches(' ').len());
753 let span = SourceSpan {
754 start_byte: draft.source_cursor + marker_start as u32,
755 end_byte: draft.source_cursor + newline as u32 + 1,
756 };
757 draft.source_cursor = span.end_byte;
758 Some(vec![span])
759 }
760}
761
762fn annotation_units(value: &str, raw: Option<(&str, u32)>) -> Vec<UnitMap> {
763 raw.and_then(|(raw, start)| map_decoded(value, raw, start))
764 .map(|spans| spans.into_iter().map(UnitMap::Mapped).collect())
765 .unwrap_or_else(|| vec![UnitMap::Unmapped; value.graphemes(true).count()])
766}
767
768fn annotation_ruby_units(value: &str, raw: Option<(&str, u32)>) -> Vec<UnitMap> {
769 let Some((raw, start)) = raw else {
770 return vec![UnitMap::Unmapped; value.graphemes(true).count()];
771 };
772 let mut decoded = String::new();
773 let mut spans = Vec::new();
774 for (part_start, part_end) in split_unescaped_offsets(raw, '.') {
775 let part = &raw[part_start..part_end];
776 let part_decoded: String = decoded_atoms(part, start + part_start as u32)
777 .iter()
778 .map(|atom| atom.text.as_str())
779 .collect();
780 let Some(mut part_spans) = map_decoded(&part_decoded, part, start + part_start as u32)
781 else {
782 return vec![UnitMap::Unmapped; value.graphemes(true).count()];
783 };
784 decoded.push_str(&part_decoded);
785 spans.append(&mut part_spans);
786 }
787 if decoded == value && spans.len() == value.graphemes(true).count() {
788 spans.into_iter().map(UnitMap::Mapped).collect()
789 } else {
790 vec![UnitMap::Unmapped; value.graphemes(true).count()]
791 }
792}
793
794fn normalize_units(text: &str, unit_texts: &[String], units: &[UnitMap]) -> Vec<UnitMap> {
795 if unit_texts.len() == units.len()
796 && unit_texts
797 .iter()
798 .map(String::as_str)
799 .eq(text.graphemes(true))
800 {
801 return units.to_vec();
802 }
803 let mut pieces = Vec::with_capacity(unit_texts.len());
804 let mut offset = 0;
805 for (unit_text, unit) in unit_texts.iter().zip(units) {
806 let end = offset + unit_text.len();
807 pieces.push((offset, end, *unit));
808 offset = end;
809 }
810 if offset != text.len() {
811 return vec![UnitMap::Unmapped; text.graphemes(true).count()];
812 }
813 text.grapheme_indices(true)
814 .map(|(start, grapheme)| {
815 let end = start + grapheme.len();
816 let overlapping: Vec<_> = pieces
817 .iter()
818 .filter(|(piece_start, piece_end, _)| *piece_start < end && *piece_end > start)
819 .map(|(_, _, unit)| *unit)
820 .collect();
821 if overlapping
822 .iter()
823 .all(|unit| matches!(unit, UnitMap::Mapped(_)))
824 {
825 let first = match overlapping.first() {
826 Some(UnitMap::Mapped(span)) => *span,
827 _ => return UnitMap::Unmapped,
828 };
829 let last = match overlapping.last() {
830 Some(UnitMap::Mapped(span)) => *span,
831 _ => return UnitMap::Unmapped,
832 };
833 UnitMap::Mapped(SourceSpan {
834 start_byte: first.start_byte,
835 end_byte: last.end_byte,
836 })
837 } else if overlapping
838 .iter()
839 .all(|unit| matches!(unit, UnitMap::Synthetic))
840 {
841 UnitMap::Synthetic
842 } else {
843 UnitMap::Unmapped
844 }
845 })
846 .collect()
847}
848
849fn source_map(block: u32, units: &[UnitMap]) -> MdiTextSourceMap {
850 let mut map = MdiTextSourceMap::default();
851 let mut index = 0;
852 while index < units.len() {
853 match units[index] {
854 UnitMap::Mapped(first) => {
855 let start = index;
856 let mut boundaries = vec![first.start_byte, first.end_byte];
857 index += 1;
858 while let Some(UnitMap::Mapped(next)) = units.get(index).copied() {
859 if boundaries.last().copied() != Some(next.start_byte) {
860 break;
861 }
862 boundaries.push(next.end_byte);
863 index += 1;
864 }
865 map.runs.push(MdiTextSourceRun {
866 range: text_range(block, start, index),
867 source_boundaries: boundaries,
868 });
869 }
870 UnitMap::Synthetic => {
871 let start = index;
872 while matches!(units.get(index), Some(UnitMap::Synthetic)) {
873 index += 1;
874 }
875 map.synthetic.push(text_range(block, start, index));
876 }
877 UnitMap::Unmapped => {
878 let start = index;
879 while matches!(units.get(index), Some(UnitMap::Unmapped)) {
880 index += 1;
881 }
882 map.unmapped.push(text_range(block, start, index));
883 }
884 }
885 }
886 map
887}
888
889fn text_range(block: u32, start: usize, end: usize) -> MdiTextRange {
890 MdiTextRange {
891 start: MdiTextPosition {
892 block,
893 character: start as u32 + 1,
894 },
895 end: MdiTextPosition {
896 block,
897 character: end as u32 + 1,
898 },
899 }
900}
901
902fn node_type(node: &serde_json::Value) -> &str {
903 node.get("type")
904 .and_then(serde_json::Value::as_str)
905 .unwrap_or_default()
906}
907
908fn children(node: &serde_json::Value) -> impl Iterator<Item = &serde_json::Value> {
909 node.get("children")
910 .and_then(serde_json::Value::as_array)
911 .into_iter()
912 .flatten()
913}
914
915fn node_span(node: &serde_json::Value) -> Option<SourceSpan> {
916 Some(SourceSpan {
917 start_byte: node.pointer("/span/startByte")?.as_u64()? as u32,
918 end_byte: node.pointer("/span/endByte")?.as_u64()? as u32,
919 })
920}
921
922struct MappedValue {
923 spans: Vec<SourceSpan>,
924 consumed_end: u32,
925}
926
927fn find_mapped_value(value: &str, source: &str, start: u32, end: u32) -> Option<MappedValue> {
928 if value.contains('\n') {
929 let mut spans = Vec::new();
930 let mut cursor = start;
931 let lines: Vec<_> = value.split('\n').collect();
932 for (index, line) in lines.iter().enumerate() {
933 if !line.is_empty() {
934 let mapped = find_mapped_value(line, source, cursor, end)?;
935 cursor = mapped.consumed_end;
936 spans.extend(mapped.spans);
937 }
938 if index + 1 < lines.len() {
939 let remaining = source.get(cursor as usize..end as usize)?;
940 let newline = remaining.find('\n')?;
941 let newline_end = cursor + newline as u32 + 1;
942 let newline_start = if newline > 0 && remaining.as_bytes()[newline - 1] == b'\r' {
943 newline_end - 2
944 } else {
945 newline_end - 1
946 };
947 spans.push(SourceSpan {
948 start_byte: newline_start,
949 end_byte: newline_end,
950 });
951 cursor = newline_end;
952 }
953 }
954 return Some(MappedValue {
955 spans,
956 consumed_end: cursor,
957 });
958 }
959 let raw = source.get(start as usize..end as usize)?;
960 let direct_offset = raw
961 .find(value)
962 .filter(|offset| direct_match_is_source_literal(raw, *offset, value));
963 if direct_offset == Some(0) {
964 return Some(MappedValue {
965 spans: map_direct(value, start)?,
966 consumed_end: start + value.len() as u32,
967 });
968 }
969 for (candidate, _) in raw.char_indices() {
970 if direct_offset.is_some_and(|offset| candidate > offset) {
971 break;
972 }
973 if let Some((spans, consumed)) =
974 map_decoded_prefix(value, &raw[candidate..], start + candidate as u32)
975 {
976 return Some(MappedValue {
977 spans,
978 consumed_end: start + candidate as u32 + consumed as u32,
979 });
980 }
981 }
982 None
983}
984
985fn map_decoded_prefix(value: &str, raw: &str, start: u32) -> Option<(Vec<SourceSpan>, usize)> {
986 let atoms = decoded_atoms(raw, start);
987 let mut decoded = String::new();
988 for atom in atoms {
989 decoded.push_str(&atom.text);
990 if decoded == value {
991 let consumed = atom.span.end_byte.checked_sub(start)? as usize;
992 return map_decoded(value, &raw[..consumed], start).map(|spans| (spans, consumed));
993 }
994 if !value.starts_with(&decoded) {
995 return None;
996 }
997 }
998 None
999}
1000
1001fn direct_match_is_source_literal(raw: &str, offset: usize, value: &str) -> bool {
1002 if offset > 0 && raw.as_bytes()[offset - 1] == b'\\' {
1003 return false;
1004 }
1005 let candidate = &raw[offset..];
1006 if candidate.starts_with('&')
1007 && let Some(end) = candidate.find(';')
1008 && decode_reference(&candidate[1..end]).as_deref() == Some(value)
1009 && end + 1 != value.len()
1010 {
1011 return false;
1012 }
1013 true
1014}
1015
1016fn map_direct(value: &str, start: u32) -> Option<Vec<SourceSpan>> {
1017 Some(
1018 value
1019 .grapheme_indices(true)
1020 .map(|(offset, grapheme)| SourceSpan {
1021 start_byte: start + offset as u32,
1022 end_byte: start + offset as u32 + grapheme.len() as u32,
1023 })
1024 .collect(),
1025 )
1026}
1027
1028struct Atom {
1029 text: String,
1030 span: SourceSpan,
1031}
1032
1033fn map_decoded(value: &str, raw: &str, start: u32) -> Option<Vec<SourceSpan>> {
1034 if value == raw {
1035 return map_direct(value, start);
1036 }
1037 let atoms = decoded_atoms(raw, start);
1038 let decoded: String = atoms.iter().map(|atom| atom.text.as_str()).collect();
1039 if decoded != value {
1040 return None;
1041 }
1042 let mut atom_ranges = Vec::with_capacity(atoms.len());
1043 let mut decoded_offset = 0;
1044 for atom in &atoms {
1045 let end = decoded_offset + atom.text.len();
1046 atom_ranges.push((decoded_offset, end, atom.span));
1047 decoded_offset = end;
1048 }
1049 let mut result = Vec::new();
1050 for (offset, grapheme) in value.grapheme_indices(true) {
1051 let end = offset + grapheme.len();
1052 let overlapping: Vec<_> = atom_ranges
1053 .iter()
1054 .filter(|(atom_start, atom_end, _)| *atom_start < end && *atom_end > offset)
1055 .collect();
1056 let first = overlapping.first()?.2;
1057 let last = overlapping.last()?.2;
1058 result.push(SourceSpan {
1059 start_byte: first.start_byte,
1060 end_byte: last.end_byte,
1061 });
1062 }
1063 Some(result)
1064}
1065
1066fn decoded_atoms(raw: &str, start: u32) -> Vec<Atom> {
1067 let mut atoms = Vec::new();
1068 let mut index = 0;
1069 while index < raw.len() {
1070 let rest = &raw[index..];
1071 if rest.starts_with('\\')
1072 && let Some(next) = rest.chars().nth(1)
1073 && (next.is_ascii_punctuation() || "{}|^[]:《》\\.".contains(next))
1074 {
1075 let len = 1 + next.len_utf8();
1076 atoms.push(Atom {
1077 text: next.to_string(),
1078 span: SourceSpan {
1079 start_byte: start + index as u32,
1080 end_byte: start + (index + len) as u32,
1081 },
1082 });
1083 index += len;
1084 continue;
1085 }
1086 if rest.starts_with('&')
1087 && let Some(end) = rest.find(';')
1088 && let Some(decoded) = decode_reference(&rest[1..end])
1089 {
1090 atoms.push(Atom {
1091 text: decoded,
1092 span: SourceSpan {
1093 start_byte: start + index as u32,
1094 end_byte: start + (index + end + 1) as u32,
1095 },
1096 });
1097 index += end + 1;
1098 continue;
1099 }
1100 let character = rest.chars().next().expect("non-empty remainder");
1101 let len = character.len_utf8();
1102 atoms.push(Atom {
1103 text: character.to_string(),
1104 span: SourceSpan {
1105 start_byte: start + index as u32,
1106 end_byte: start + (index + len) as u32,
1107 },
1108 });
1109 index += len;
1110 }
1111 atoms
1112}
1113
1114fn decode_reference(body: &str) -> Option<String> {
1115 if let Some(hex) = body.strip_prefix("#x").or_else(|| body.strip_prefix("#X")) {
1116 return (!hex.is_empty() && hex.chars().all(|character| character.is_ascii_hexdigit()))
1117 .then(|| markdown::decode_numeric(hex, 16));
1118 }
1119 if let Some(decimal) = body.strip_prefix('#') {
1120 return (!decimal.is_empty()
1121 && decimal.chars().all(|character| character.is_ascii_digit()))
1122 .then(|| markdown::decode_numeric(decimal, 10));
1123 }
1124 markdown::decode_named(body, true)
1125}
1126
1127struct RubyParts<'a> {
1128 token_start: u32,
1129 base: &'a str,
1130 base_start: u32,
1131 reading: &'a str,
1132 reading_start: u32,
1133 token_end: u32,
1134}
1135
1136fn find_ruby_parts<'a>(
1137 base: &str,
1138 reading: &str,
1139 source: &'a str,
1140 start: u32,
1141 end: u32,
1142) -> Option<RubyParts<'a>> {
1143 let raw = source.get(start as usize..end as usize)?;
1144 for (offset, _) in raw.match_indices('{') {
1145 let candidate = &raw[offset..];
1146 let Some(close) = first_unescaped(&candidate[1..], '}').map(|close| close + 1) else {
1147 continue;
1148 };
1149 let body = &candidate[1..close];
1150 let Some(separator) = first_unescaped(body, '|') else {
1151 continue;
1152 };
1153 let raw_base = &body[..separator];
1154 let raw_reading = &body[separator + 1..];
1155 let decoded_base: String = decoded_atoms(raw_base, 0)
1156 .into_iter()
1157 .map(|atom| atom.text)
1158 .collect();
1159 let decoded_reading: String = split_unescaped_offsets(raw_reading, '.')
1160 .into_iter()
1161 .flat_map(|(part_start, part_end)| {
1162 decoded_atoms(&raw_reading[part_start..part_end], 0)
1163 .into_iter()
1164 .map(|atom| atom.text)
1165 })
1166 .collect();
1167 if decoded_base == base && decoded_reading == reading {
1168 let token_start = start + offset as u32;
1169 return Some(RubyParts {
1170 token_start,
1171 base: raw_base,
1172 base_start: token_start + 1,
1173 reading: raw_reading,
1174 reading_start: token_start + 1 + separator as u32 + 1,
1175 token_end: token_start + close as u32 + 1,
1176 });
1177 }
1178 }
1179 None
1180}
1181
1182fn split_raw_reading<'a>(parts: &'a RubyParts<'a>) -> Vec<(&'a str, u32)> {
1183 split_unescaped_offsets(parts.reading, '.')
1184 .into_iter()
1185 .map(|(start, end)| {
1186 (
1187 &parts.reading[start..end],
1188 parts.reading_start + start as u32,
1189 )
1190 })
1191 .collect()
1192}
1193
1194fn first_unescaped(value: &str, needle: char) -> Option<usize> {
1195 let mut escaped = false;
1196 for (index, character) in value.char_indices() {
1197 if escaped {
1198 escaped = false;
1199 } else if character == '\\' {
1200 escaped = true;
1201 } else if character == needle {
1202 return Some(index);
1203 }
1204 }
1205 None
1206}
1207
1208fn split_unescaped_offsets(value: &str, separator: char) -> Vec<(usize, usize)> {
1209 let mut result = Vec::new();
1210 let mut start = 0;
1211 let mut escaped = false;
1212 for (index, character) in value.char_indices() {
1213 if escaped {
1214 escaped = false;
1215 } else if character == '\\' {
1216 escaped = true;
1217 } else if character == separator {
1218 result.push((start, index));
1219 start = index + character.len_utf8();
1220 }
1221 }
1222 result.push((start, value.len()));
1223 result
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228 use super::*;
1229
1230 fn position(range: &MdiTextRange) -> (u32, u32, u32, u32) {
1231 (
1232 range.start.block,
1233 range.start.character,
1234 range.end.block,
1235 range.end.character,
1236 )
1237 }
1238
1239 fn assert_complete_mapping(block: &MdiTextBlock, source: &str) {
1240 let count = block.text.graphemes(true).count();
1241 let mut coverage = vec![0_u8; count];
1242 for run in &block.source_map.runs {
1243 let start = run.range.start.character as usize - 1;
1244 let end = run.range.end.character as usize - 1;
1245 assert_eq!(run.source_boundaries.len(), end - start + 1);
1246 for boundary in &run.source_boundaries {
1247 assert!((*boundary as usize) <= source.len());
1248 assert!(source.is_char_boundary(*boundary as usize));
1249 }
1250 for covered in &mut coverage[start..end] {
1251 *covered += 1;
1252 }
1253 }
1254 for range in &block.source_map.synthetic {
1255 let start = range.start.character as usize - 1;
1256 let end = range.end.character as usize - 1;
1257 for covered in &mut coverage[start..end] {
1258 *covered += 1;
1259 }
1260 }
1261 assert!(block.source_map.unmapped.is_empty(), "{block:#?}");
1262 assert!(coverage.iter().all(|covered| *covered == 1), "{block:#?}");
1263 }
1264
1265 #[test]
1266 fn projects_grapheme_positions_and_ruby_channels() {
1267 let result = get_mdi_text_blocks("# 序章\n\n我喜歡{東京|とうきょう}。\n\ne\u{301} 👩🏽💻");
1268 assert_eq!(result.blocks.len(), 3);
1269 assert_eq!(result.blocks[0].text, "序章");
1270 assert_eq!(position(&result.blocks[0].range), (1, 1, 1, 3));
1271 assert_eq!(result.blocks[1].text, "我喜歡東京。");
1272 assert_eq!(position(&result.blocks[1].range), (2, 1, 2, 7));
1273 let annotation = &result.blocks[1].annotations[0];
1274 assert_eq!(annotation.text, "とうきょう");
1275 assert_eq!(position(&annotation.anchor), (2, 4, 2, 6));
1276 assert_eq!(result.blocks[2].text.graphemes(true).count(), 3);
1277 assert_eq!(position(&result.blocks[2].range), (3, 1, 3, 4));
1278 assert!(result.diagnostics.is_empty());
1279
1280 let across_wrapper = get_mdi_text_blocks("e*\u{301}*");
1281 assert_eq!(across_wrapper.blocks[0].text, "e\u{301}");
1282 assert_eq!(position(&across_wrapper.blocks[0].range), (1, 1, 1, 2));
1283 assert!(across_wrapper.blocks[0].source_map.unmapped.is_empty());
1284
1285 let marker_text = get_mdi_text_blocks("# \\#\n\n- \\-\n\n> \\>");
1286 assert_eq!(
1287 marker_text
1288 .blocks
1289 .iter()
1290 .map(|block| block.source_map.runs[0].source_boundaries[0])
1291 .collect::<Vec<_>>(),
1292 vec![2, 8, 14]
1293 );
1294 }
1295
1296 #[test]
1297 fn maps_entities_escapes_and_mdi_delimiters_to_complete_source_tokens() {
1298 let source = r"& \* {東京|とうきょう} ^12^ 前[[br]]次";
1299 let result = get_mdi_text_blocks(source);
1300 let block = &result.blocks[0];
1301 assert_eq!(block.text, "& * 東京 12 前\n次");
1302 assert!(block.source_map.unmapped.is_empty(), "{block:#?}");
1303 assert!(block.source_map.synthetic.is_empty());
1304
1305 let spans: Vec<_> = block
1306 .source_map
1307 .runs
1308 .iter()
1309 .flat_map(|run| run.source_boundaries.windows(2))
1310 .map(|pair| &source[pair[0] as usize..pair[1] as usize])
1311 .collect();
1312 assert!(spans.contains(&"&"));
1313 assert!(spans.contains(&r"\*"));
1314 assert!(spans.contains(&"[[br]]"));
1315 assert!(!spans.contains(&"とうきょう"));
1316 assert!(result.diagnostics.is_empty());
1317 }
1318
1319 #[test]
1320 fn gives_each_split_ruby_reading_its_base_grapheme_anchor() {
1321 let result = get_mdi_text_blocks("{東京|とう.きょう}");
1322 let annotations = &result.blocks[0].annotations;
1323 assert_eq!(annotations.len(), 2);
1324 assert_eq!(annotations[0].text, "とう");
1325 assert_eq!(position(&annotations[0].anchor), (1, 1, 1, 2));
1326 assert_eq!(annotations[1].text, "きょう");
1327 assert_eq!(position(&annotations[1].anchor), (1, 2, 1, 3));
1328 assert!(
1329 annotations
1330 .iter()
1331 .all(|annotation| annotation.source_map.unmapped.is_empty())
1332 );
1333 }
1334
1335 #[test]
1336 fn mismatched_split_ruby_degrades_to_a_mapped_group_warning() {
1337 let result = get_mdi_text_blocks("{東京|とう.きょ.う}");
1338 let annotation = &result.blocks[0].annotations[0];
1339 assert_eq!(annotation.text, "とうきょう");
1340 assert_eq!(position(&annotation.anchor), (1, 1, 1, 3));
1341 assert!(annotation.source_map.unmapped.is_empty());
1342 assert!(
1343 result
1344 .diagnostics
1345 .iter()
1346 .any(|diagnostic| diagnostic.code == "mdi.textProjection.rubySplitMismatch")
1347 );
1348 }
1349
1350 #[test]
1351 fn collects_leaf_blocks_without_parent_text_duplication() {
1352 let source = "- first\n\n second\n - nested\n\n> quote one\n>\n> quote two\n\n| a | b |\n| - | - |\n| c | d |\n\n```mdi\ncode\nline\n```\n\nbody[^n]\n\n[^n]: note\n\n---";
1353 let result = get_mdi_text_blocks(source);
1354 let summaries: Vec<_> = result
1355 .blocks
1356 .iter()
1357 .map(|block| (block.kind, block.text.as_str()))
1358 .collect();
1359 assert_eq!(
1360 summaries,
1361 vec![
1362 (MdiTextBlockKind::ListItem, "first\n\nsecond"),
1363 (MdiTextBlockKind::ListItem, "nested"),
1364 (MdiTextBlockKind::Blockquote, "quote one"),
1365 (MdiTextBlockKind::Blockquote, "quote two"),
1366 (MdiTextBlockKind::Table, "a\tb\nc\td"),
1367 (MdiTextBlockKind::Code, "code\nline"),
1368 (MdiTextBlockKind::Paragraph, "body"),
1369 (MdiTextBlockKind::Footnote, "note"),
1370 ]
1371 );
1372 assert_eq!(result.blocks[0].source_map.synthetic.len(), 1);
1373 assert_eq!(result.blocks[4].source_map.synthetic.len(), 3);
1374 assert!(
1375 result
1376 .blocks
1377 .iter()
1378 .all(|block| block.source_map.unmapped.is_empty())
1379 );
1380
1381 let fenced = get_mdi_text_blocks("```rust\nrust\n```");
1382 assert_eq!(fenced.blocks[0].text, "rust");
1383 assert_eq!(fenced.blocks[0].source_map.runs[0].source_boundaries[0], 8);
1384 }
1385
1386 #[test]
1387 fn projection_json_is_deterministic_and_keeps_the_parse_envelope() {
1388 let source = "---\nmdi: '2.0'\ntitle: x\n---\n\n# heading\n\ntext";
1389 let first = get_mdi_text_blocks_json(source);
1390 assert_eq!(first, get_mdi_text_blocks_json(source));
1391 let value: serde_json::Value = serde_json::from_str(&first).unwrap();
1392 assert_eq!(value["projectionVersion"], "1.0");
1393 assert_eq!(
1394 value["positionEncoding"],
1395 "unicode-grapheme-cluster-1-based"
1396 );
1397 assert_eq!(value["irVersion"], MDI_IR_VERSION);
1398 assert_eq!(value["document"]["frontmatter"]["entries"][0]["key"], "mdi");
1399 }
1400
1401 #[test]
1402 fn supported_inline_and_wrapped_markdown_is_fully_mapped() {
1403 let source = "**強調** [label](https://example.test)  `code` \\* & [[no-break:禁則]][[warichu:割注]][[kern:-0.1em:詰め]][[em:傍点]]\n\n> first\n> continued\n\n- item\n continued";
1404 let result = get_mdi_text_blocks(source);
1405 assert_eq!(
1406 result
1407 .blocks
1408 .iter()
1409 .map(|block| block.text.as_str())
1410 .collect::<Vec<_>>(),
1411 vec![
1412 "強調 label 代替 code * & 禁則割注詰め傍点",
1413 "first\ncontinued",
1414 "item\ncontinued",
1415 ]
1416 );
1417 for block in &result.blocks {
1418 assert_complete_mapping(block, source);
1419 }
1420 }
1421
1422 #[test]
1423 fn malformed_literals_remain_searchable_and_precisely_mapped() {
1424 for source in [
1425 "{東京|とうきょう",
1426 "[[em:未閉",
1427 "《《未閉",
1428 "^1234567^ ^12^",
1429 "<custom>literal</custom>",
1430 ] {
1431 let result = get_mdi_text_blocks(source);
1432 assert!(!result.blocks.is_empty(), "{source:?}");
1433 for block in &result.blocks {
1434 assert_complete_mapping(block, source);
1435 }
1436 }
1437 let frontmatter = get_mdi_text_blocks("---\ntitle: hidden\n---\n\nvisible");
1438 assert_eq!(frontmatter.blocks.len(), 1);
1439 assert_eq!(frontmatter.blocks[0].text, "visible");
1440 }
1441}