1#[cfg(any(test, feature = "wasm"))]
2use crate::MDI_MDAST_PROVENANCE_VERSION;
3use crate::{
4 Diagnostic, DiagnosticSeverity, Document, MDI_IR_VERSION, MDI_SPEC_VERSION, ParserCapabilities,
5 SourceSpan, diagnostics, parse_document_without_provenance,
6};
7use serde::Serialize;
8#[cfg(test)]
9use std::cell::Cell;
10use std::fmt;
11use unicode_segmentation::UnicodeSegmentation;
12
13#[cfg(test)]
14thread_local! {
15 static PROVENANCE_QUERY_VISITS: Cell<usize> = const { Cell::new(0) };
16}
17
18pub(crate) enum PlainInline<'a> {
19 Value(&'a str),
20 Break,
21 Skip,
22 Children,
23}
24
25pub(crate) fn plain_inline(node: &serde_json::Value) -> PlainInline<'_> {
28 match node_type(node) {
29 "text" | "inlineCode" | "code" | "html" | "tcy" => PlainInline::Value(
30 node.get("value")
31 .and_then(serde_json::Value::as_str)
32 .unwrap_or_default(),
33 ),
34 "ruby" => PlainInline::Value(
35 node.get("base")
36 .and_then(serde_json::Value::as_str)
37 .unwrap_or_default(),
38 ),
39 "image" => PlainInline::Value(
40 node.get("alt")
41 .and_then(serde_json::Value::as_str)
42 .unwrap_or_default(),
43 ),
44 "break" => PlainInline::Break,
45 "footnoteReference" => PlainInline::Skip,
46 _ if node.get("children").is_some() => PlainInline::Children,
47 _ => node
48 .get("value")
49 .and_then(serde_json::Value::as_str)
50 .map_or(PlainInline::Skip, PlainInline::Value),
51 }
52}
53
54pub const MDI_TEXT_PROJECTION_VERSION: &str = "1.0";
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "camelCase")]
58pub struct MdiTextBlocksResult {
59 pub projection_version: &'static str,
60 pub position_encoding: &'static str,
61 pub ir_version: &'static str,
62 pub syntax_version: &'static str,
63 pub capabilities: ParserCapabilities,
64 pub blocks: Vec<MdiTextBlock>,
65 pub document: Document,
66 pub diagnostics: Vec<Diagnostic>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct MdiTextBlock {
72 pub index: u32,
73 pub kind: MdiTextBlockKind,
74 pub text: String,
75 pub range: MdiTextRange,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub span: Option<SourceSpan>,
78 pub source_map: MdiTextSourceMap,
79 pub annotations: Vec<MdiTextAnnotation>,
80 pub node: serde_json::Value,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
84#[serde(rename_all = "camelCase")]
85pub enum MdiTextBlockKind {
86 Heading,
87 Paragraph,
88 ListItem,
89 Blockquote,
90 Code,
91 Table,
92 Footnote,
93 Html,
94 Other,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct MdiTextPosition {
100 pub block: u32,
101 pub character: u32,
102}
103
104impl Serialize for MdiTextPosition {
105 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
106 where
107 S: serde::Serializer,
108 {
109 serializer.serialize_str(&format!("{}:{}", self.block, self.character))
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
114pub struct MdiTextRange {
115 pub start: MdiTextPosition,
116 pub end: MdiTextPosition,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
120pub struct MdiTextSourceMap {
121 pub runs: Vec<MdiTextSourceRun>,
122 pub synthetic: Vec<MdiTextRange>,
123 pub unmapped: Vec<MdiTextRange>,
124}
125
126pub type MdiAnnotationSourceMap = MdiTextSourceMap;
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
131#[serde(rename_all = "camelCase")]
132pub struct MdiTextSourceRun {
133 pub range: MdiTextRange,
134 pub source_boundaries: Vec<u32>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct MdiTextAnnotation {
140 pub kind: &'static str,
141 pub text: String,
142 pub anchor: MdiTextRange,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub span: Option<SourceSpan>,
145 pub source_map: MdiAnnotationSourceMap,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct MdiSourceSpanTextResolution {
152 pub projection_version: &'static str,
153 pub source_span: SourceSpan,
154 pub coverage: MdiSourceSpanCoverage,
155 pub matches: Vec<MdiSourceSpanTextMatch>,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
160#[serde(rename_all = "camelCase")]
161pub enum MdiSourceSpanCoverage {
162 Complete,
163 Partial,
164 None,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
170#[serde(rename_all = "camelCase")]
171pub enum MdiSourceSpanRelation {
172 Exact,
173 Overlap,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
178#[serde(
179 tag = "kind",
180 rename_all = "camelCase",
181 rename_all_fields = "camelCase"
182)]
183pub enum MdiSourceSpanTextMatch {
184 BlockText {
185 block_index: u32,
186 range: MdiTextRange,
187 relation: MdiSourceSpanRelation,
188 },
189 Annotation {
190 block_index: u32,
191 annotation_index: u32,
192 range: MdiTextRange,
193 relation: MdiSourceSpanRelation,
194 },
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum MdiSourceSpanResolutionError {
200 Reversed,
201 OutOfBounds,
202 NotUtf8Boundary,
203}
204
205impl fmt::Display for MdiSourceSpanResolutionError {
206 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207 formatter.write_str(match self {
208 Self::Reversed => "source span startByte must not exceed endByte",
209 Self::OutOfBounds => "source span falls outside the UTF-8 source length",
210 Self::NotUtf8Boundary => "source span endpoints must be UTF-8 code-point boundaries",
211 })
212 }
213}
214
215impl std::error::Error for MdiSourceSpanResolutionError {}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218enum UnitMap {
219 Mapped(SourceSpan),
220 Synthetic,
221 Unmapped,
222}
223
224struct AnnotationDraft {
225 text: String,
226 anchor_start: usize,
227 anchor_end: usize,
228 span: Option<SourceSpan>,
229 units: Vec<UnitMap>,
230}
231
232struct BlockDraft {
233 kind: MdiTextBlockKind,
234 text: String,
235 units: Vec<UnitMap>,
236 unit_texts: Vec<String>,
237 annotations: Vec<AnnotationDraft>,
238 span: Option<SourceSpan>,
239 node: serde_json::Value,
240 mapping_warning: bool,
241 source_cursor: u32,
242 source_end: u32,
243}
244
245impl BlockDraft {
246 fn new(kind: MdiTextBlockKind, node: &serde_json::Value) -> Self {
247 let span = node_span(node);
248 Self {
249 kind,
250 text: String::new(),
251 units: Vec::new(),
252 unit_texts: Vec::new(),
253 annotations: Vec::new(),
254 span,
255 node: node.clone(),
256 mapping_warning: false,
257 source_cursor: span.map_or(0, |span| span.start_byte),
258 source_end: span.map_or(0, |span| span.end_byte),
259 }
260 }
261
262 fn grapheme_len(&self) -> usize {
263 self.text.graphemes(true).count()
264 }
265
266 fn append_synthetic(&mut self, value: &str) {
267 self.text.push_str(value);
268 for grapheme in value.graphemes(true) {
269 self.unit_texts.push(grapheme.to_owned());
270 self.units.push(UnitMap::Synthetic);
271 }
272 }
273
274 fn append_unmapped(&mut self, value: &str) {
275 if value.is_empty() {
276 return;
277 }
278 self.text.push_str(value);
279 for grapheme in value.graphemes(true) {
280 self.unit_texts.push(grapheme.to_owned());
281 self.units.push(UnitMap::Unmapped);
282 }
283 self.mapping_warning = true;
284 }
285
286 fn append_mapped(&mut self, value: &str, spans: Option<Vec<SourceSpan>>) {
287 if value.is_empty() {
288 return;
289 }
290 let count = value.graphemes(true).count();
291 match spans {
292 Some(spans) if spans.len() == count => {
293 self.text.push_str(value);
294 for (grapheme, span) in value.graphemes(true).zip(spans) {
295 self.unit_texts.push(grapheme.to_owned());
296 self.units.push(UnitMap::Mapped(span));
297 }
298 }
299 _ => self.append_unmapped(value),
300 }
301 }
302}
303
304struct Collector<'a> {
305 source: &'a str,
306 blocks: Vec<MdiTextBlock>,
307 diagnostics: Vec<Diagnostic>,
308}
309
310pub fn get_mdi_text_blocks(source: &str) -> MdiTextBlocksResult {
313 let document = parse_document_without_provenance(source);
314 let mut collector = Collector {
315 source,
316 blocks: Vec::new(),
317 diagnostics: diagnostics(&document),
318 };
319 for node in &document.children {
320 collector.collect(node, false);
321 }
322 MdiTextBlocksResult {
323 projection_version: MDI_TEXT_PROJECTION_VERSION,
324 position_encoding: "unicode-grapheme-cluster-1-based",
325 ir_version: MDI_IR_VERSION,
326 syntax_version: MDI_SPEC_VERSION,
327 capabilities: ParserCapabilities {
328 mdi: true,
329 common_mark: true,
330 gfm: true,
331 front_matter: true,
332 source_spans: true,
333 },
334 blocks: collector.blocks,
335 document,
336 diagnostics: collector.diagnostics,
337 }
338}
339
340#[cfg(any(test, feature = "wasm"))]
344pub(crate) fn attach_mdast_provenance(document: &mut Document, source: &str) {
345 let mut collector = Collector {
346 source,
347 blocks: Vec::new(),
348 diagnostics: Vec::new(),
349 };
350 for node in &document.children {
351 collector.collect(node, false);
352 }
353 let provenance_index = ProvenanceIndex::new(&collector.blocks);
354 for (node_index, node) in document.children.iter_mut().enumerate() {
355 attach_node_provenance(node, &provenance_index, &node_index.to_string());
356 }
357}
358
359#[cfg(any(test, feature = "wasm"))]
360fn attach_node_provenance(node: &mut serde_json::Value, index: &ProvenanceIndex, path: &str) {
361 let Some(object) = node.as_object_mut() else {
362 return;
363 };
364 let node_type = object
365 .get("type")
366 .and_then(serde_json::Value::as_str)
367 .unwrap_or("");
368 let span = object
369 .get("span")
370 .and_then(|value| serde_json::from_value::<SourceSpan>(value.clone()).ok());
371 let role = if is_text_bearing(node_type) {
372 "textBearing"
373 } else {
374 "container"
375 };
376 let targets = if role == "textBearing" {
377 span.map(|span| index.targets(span)).unwrap_or_default()
378 } else {
379 Vec::new()
380 };
381 let status = if span.is_none() {
382 "synthetic"
383 } else if role == "textBearing" && targets.is_empty() {
384 "unmapped"
385 } else {
386 "sourceBacked"
387 };
388 object.insert(
389 "mdiProvenance".to_owned(),
390 serde_json::json!({
391 "version": MDI_MDAST_PROVENANCE_VERSION,
392 "construct": { "path": path, "type": node_type },
393 "span": span,
394 "role": role,
395 "status": status,
396 "targets": targets,
397 }),
398 );
399 if let Some(children) = object
400 .get_mut("children")
401 .and_then(serde_json::Value::as_array_mut)
402 {
403 for (child_index, child) in children.iter_mut().enumerate() {
404 attach_node_provenance(child, index, &format!("{path}.{child_index}"));
405 }
406 }
407}
408
409#[cfg(any(test, feature = "wasm"))]
410fn is_text_bearing(node_type: &str) -> bool {
411 matches!(
412 node_type,
413 "text" | "inlineCode" | "code" | "html" | "ruby" | "tcy" | "image" | "break"
414 )
415}
416
417#[cfg(any(test, feature = "wasm"))]
418#[derive(Clone, Copy, PartialEq, Eq)]
419enum ProvenanceChannel {
420 BlockText,
421 Annotation(u32),
422}
423
424#[cfg(any(test, feature = "wasm"))]
425struct ProvenanceUnit {
426 span: SourceSpan,
427 block_index: u32,
428 channel: ProvenanceChannel,
429 character: u32,
430 ordinal: usize,
431}
432
433#[cfg(any(test, feature = "wasm"))]
437struct ProvenanceIndex {
438 units: Vec<ProvenanceUnit>,
439 prefix_max_end: Vec<u32>,
440}
441
442#[cfg(any(test, feature = "wasm"))]
443impl ProvenanceIndex {
444 fn new(blocks: &[MdiTextBlock]) -> Self {
445 let mut units = Vec::new();
446 for block in blocks {
447 Self::push_channel(
448 &mut units,
449 block.index,
450 ProvenanceChannel::BlockText,
451 &block.source_map,
452 );
453 for (annotation_index, annotation) in block.annotations.iter().enumerate() {
454 Self::push_channel(
455 &mut units,
456 block.index,
457 ProvenanceChannel::Annotation(annotation_index as u32),
458 &annotation.source_map,
459 );
460 }
461 }
462 units.sort_by_key(|unit| (unit.span.start_byte, unit.span.end_byte, unit.ordinal));
463 let mut maximum = 0;
464 let prefix_max_end = units
465 .iter()
466 .map(|unit| {
467 maximum = maximum.max(unit.span.end_byte);
468 maximum
469 })
470 .collect();
471 Self {
472 units,
473 prefix_max_end,
474 }
475 }
476
477 fn push_channel(
478 units: &mut Vec<ProvenanceUnit>,
479 block_index: u32,
480 channel: ProvenanceChannel,
481 map: &MdiTextSourceMap,
482 ) {
483 for run in &map.runs {
484 for (offset, boundaries) in run.source_boundaries.windows(2).enumerate() {
485 units.push(ProvenanceUnit {
486 span: SourceSpan {
487 start_byte: boundaries[0],
488 end_byte: boundaries[1],
489 },
490 block_index,
491 channel,
492 character: run.range.start.character + offset as u32,
493 ordinal: units.len(),
494 });
495 }
496 }
497 }
498
499 fn targets(&self, span: SourceSpan) -> Vec<serde_json::Value> {
500 if span.start_byte == span.end_byte || self.units.is_empty() {
501 return Vec::new();
502 }
503 let first = self
504 .prefix_max_end
505 .partition_point(|&end| end <= span.start_byte);
506 let after_last = self
507 .units
508 .partition_point(|unit| unit.span.start_byte < span.end_byte);
509 let mut targets = Vec::new();
510 let mut pending: Option<(u32, ProvenanceChannel, u32, u32)> = None;
511 for unit in self.units.get(first..after_last).unwrap_or_default() {
512 #[cfg(test)]
513 PROVENANCE_QUERY_VISITS.with(|visits| visits.set(visits.get() + 1));
514 if unit.span.end_byte <= span.start_byte {
515 continue;
516 }
517 match pending.as_mut() {
518 Some((block, channel, _, end))
519 if *block == unit.block_index
520 && *channel == unit.channel
521 && *end == unit.character =>
522 {
523 *end = unit.character + 1;
524 }
525 _ => {
526 if let Some(target) = pending.take() {
527 targets.push(provenance_target(target));
528 }
529 pending = Some((
530 unit.block_index,
531 unit.channel,
532 unit.character,
533 unit.character + 1,
534 ));
535 }
536 }
537 }
538 if let Some(target) = pending {
539 targets.push(provenance_target(target));
540 }
541 targets
542 }
543}
544
545#[cfg(any(test, feature = "wasm"))]
546fn provenance_target(
547 (block_index, channel, start, end): (u32, ProvenanceChannel, u32, u32),
548) -> serde_json::Value {
549 let range = MdiTextRange {
550 start: MdiTextPosition {
551 block: block_index,
552 character: start,
553 },
554 end: MdiTextPosition {
555 block: block_index,
556 character: end,
557 },
558 };
559 match channel {
560 ProvenanceChannel::BlockText => serde_json::json!({
561 "blockIndex": block_index, "channel": "blockText", "range": range,
562 }),
563 ProvenanceChannel::Annotation(annotation_index) => serde_json::json!({
564 "blockIndex": block_index, "channel": "annotation", "annotationIndex": annotation_index, "range": range,
565 }),
566 }
567}
568
569#[cfg(test)]
570pub(crate) fn reset_provenance_query_visits() {
571 PROVENANCE_QUERY_VISITS.with(|visits| visits.set(0));
572}
573
574#[cfg(test)]
575pub(crate) fn provenance_query_visits() -> usize {
576 PROVENANCE_QUERY_VISITS.with(Cell::get)
577}
578
579pub fn get_mdi_text_blocks_json(source: &str) -> String {
580 serde_json::to_string(&get_mdi_text_blocks(source))
581 .expect("serializing the MDI text projection cannot fail")
582}
583
584pub fn resolve_mdi_source_span(
587 source: &str,
588 span: SourceSpan,
589) -> Result<MdiSourceSpanTextResolution, MdiSourceSpanResolutionError> {
590 validate_source_span(source, span)?;
591 let projection = get_mdi_text_blocks(source);
592 Ok(resolve_mdi_source_span_in_projection(&projection, span))
593}
594
595pub fn resolve_mdi_source_spans(
598 source: &str,
599 spans: &[SourceSpan],
600) -> Result<Vec<MdiSourceSpanTextResolution>, MdiSourceSpanResolutionError> {
601 for &span in spans {
602 validate_source_span(source, span)?;
603 }
604 if spans.is_empty() {
605 return Ok(Vec::new());
606 }
607 let projection = get_mdi_text_blocks(source);
608 Ok(spans
609 .iter()
610 .map(|&span| resolve_mdi_source_span_in_projection(&projection, span))
611 .collect())
612}
613
614fn resolve_mdi_source_span_in_projection(
615 projection: &MdiTextBlocksResult,
616 span: SourceSpan,
617) -> MdiSourceSpanTextResolution {
618 if span.start_byte == span.end_byte {
619 return MdiSourceSpanTextResolution {
620 projection_version: MDI_TEXT_PROJECTION_VERSION,
621 source_span: span,
622 coverage: MdiSourceSpanCoverage::None,
623 matches: Vec::new(),
624 };
625 }
626
627 let mut matches = Vec::new();
628 let mut covered = Vec::new();
629 for block in &projection.blocks {
630 resolve_source_map_channel(
631 block.index,
632 None,
633 &block.source_map,
634 span,
635 &mut matches,
636 &mut covered,
637 );
638 for (annotation_index, annotation) in block.annotations.iter().enumerate() {
639 resolve_source_map_channel(
640 block.index,
641 Some(annotation_index as u32),
642 &annotation.source_map,
643 span,
644 &mut matches,
645 &mut covered,
646 );
647 }
648 }
649
650 let covered = merged_intervals(covered);
651 let coverage = if covered.is_empty() {
652 MdiSourceSpanCoverage::None
653 } else if covered.len() == 1
654 && covered[0].start_byte == span.start_byte
655 && covered[0].end_byte == span.end_byte
656 {
657 MdiSourceSpanCoverage::Complete
658 } else {
659 MdiSourceSpanCoverage::Partial
660 };
661 MdiSourceSpanTextResolution {
662 projection_version: MDI_TEXT_PROJECTION_VERSION,
663 source_span: span,
664 coverage,
665 matches,
666 }
667}
668
669pub fn resolve_mdi_source_span_json(
671 source: &str,
672 span: SourceSpan,
673) -> Result<String, MdiSourceSpanResolutionError> {
674 let resolution = resolve_mdi_source_span(source, span)?;
675 Ok(serde_json::to_string(&resolution)
676 .expect("serializing an MDI source-span resolution cannot fail"))
677}
678
679pub fn resolve_mdi_source_spans_json(
682 source: &str,
683 spans: &[SourceSpan],
684) -> Result<String, MdiSourceSpanResolutionError> {
685 let resolutions = resolve_mdi_source_spans(source, spans)?;
686 Ok(serde_json::to_string(&resolutions)
687 .expect("serializing MDI source-span resolutions cannot fail"))
688}
689
690fn validate_source_span(
691 source: &str,
692 span: SourceSpan,
693) -> Result<(), MdiSourceSpanResolutionError> {
694 if span.start_byte > span.end_byte {
695 return Err(MdiSourceSpanResolutionError::Reversed);
696 }
697 let start = span.start_byte as usize;
698 let end = span.end_byte as usize;
699 if end > source.len() {
700 return Err(MdiSourceSpanResolutionError::OutOfBounds);
701 }
702 if !source.is_char_boundary(start) || !source.is_char_boundary(end) {
703 return Err(MdiSourceSpanResolutionError::NotUtf8Boundary);
704 }
705 Ok(())
706}
707
708fn resolve_source_map_channel(
709 block_index: u32,
710 annotation_index: Option<u32>,
711 map: &MdiTextSourceMap,
712 requested: SourceSpan,
713 matches: &mut Vec<MdiSourceSpanTextMatch>,
714 covered: &mut Vec<SourceSpan>,
715) {
716 let mut current_start = None;
717 let mut current_end = 0;
718 let mut current_spans = Vec::new();
719
720 let flush = |start: &mut Option<u32>,
721 end: &mut u32,
722 spans: &mut Vec<SourceSpan>,
723 matches: &mut Vec<MdiSourceSpanTextMatch>| {
724 let Some(start_character) = start.take() else {
725 return;
726 };
727 let relation = if intervals_equal_span(spans, requested) {
728 MdiSourceSpanRelation::Exact
729 } else {
730 MdiSourceSpanRelation::Overlap
731 };
732 let range = MdiTextRange {
733 start: MdiTextPosition {
734 block: block_index,
735 character: start_character,
736 },
737 end: MdiTextPosition {
738 block: block_index,
739 character: *end,
740 },
741 };
742 matches.push(match annotation_index {
743 Some(annotation_index) => MdiSourceSpanTextMatch::Annotation {
744 block_index,
745 annotation_index,
746 range,
747 relation,
748 },
749 None => MdiSourceSpanTextMatch::BlockText {
750 block_index,
751 range,
752 relation,
753 },
754 });
755 spans.clear();
756 };
757
758 for run in &map.runs {
759 let run_start = run.range.start.character;
760 for (offset, boundaries) in run.source_boundaries.windows(2).enumerate() {
761 let character = run_start + offset as u32;
762 let unit_span = SourceSpan {
763 start_byte: boundaries[0],
764 end_byte: boundaries[1],
765 };
766 if unit_span.start_byte < requested.end_byte
767 && requested.start_byte < unit_span.end_byte
768 {
769 if current_start.is_some() && character != current_end {
770 flush(
771 &mut current_start,
772 &mut current_end,
773 &mut current_spans,
774 matches,
775 );
776 }
777 current_start.get_or_insert(character);
778 current_end = character + 1;
779 current_spans.push(unit_span);
780 covered.push(SourceSpan {
781 start_byte: unit_span.start_byte.max(requested.start_byte),
782 end_byte: unit_span.end_byte.min(requested.end_byte),
783 });
784 } else if current_start.is_some() && character == current_end {
785 flush(
786 &mut current_start,
787 &mut current_end,
788 &mut current_spans,
789 matches,
790 );
791 }
792 }
793 }
794 flush(
795 &mut current_start,
796 &mut current_end,
797 &mut current_spans,
798 matches,
799 );
800}
801
802fn intervals_equal_span(intervals: &[SourceSpan], span: SourceSpan) -> bool {
803 let merged = merged_intervals(intervals.to_vec());
804 merged.len() == 1 && merged[0] == span
805}
806
807fn merged_intervals(mut intervals: Vec<SourceSpan>) -> Vec<SourceSpan> {
808 intervals.sort_unstable_by_key(|span| (span.start_byte, span.end_byte));
809 let mut merged: Vec<SourceSpan> = Vec::new();
810 for interval in intervals {
811 if let Some(previous) = merged.last_mut()
812 && interval.start_byte <= previous.end_byte
813 {
814 previous.end_byte = previous.end_byte.max(interval.end_byte);
815 } else {
816 merged.push(interval);
817 }
818 }
819 merged
820}
821
822impl Collector<'_> {
823 fn collect(&mut self, node: &serde_json::Value, quoted: bool) {
824 let kind = node_type(node);
825 match kind {
826 "heading" => self.inline_block(MdiTextBlockKind::Heading, node),
827 "paragraph" => self.inline_block(
828 if quoted {
829 MdiTextBlockKind::Blockquote
830 } else {
831 MdiTextBlockKind::Paragraph
832 },
833 node,
834 ),
835 "blockquote" => {
836 for child in children(node) {
837 self.collect(child, true);
838 }
839 }
840 "list" => {
841 for child in children(node) {
842 self.collect(child, quoted);
843 }
844 }
845 "listItem" => self.list_item(node, quoted),
846 "code" => self.scalar_block(MdiTextBlockKind::Code, node, "value"),
847 "html" => self.scalar_block(MdiTextBlockKind::Html, node, "value"),
848 "table" => self.table(node),
849 "footnoteDefinition" => self.footnote(node),
850 "yaml" | "definition" | "blank" | "pagebreak" | "thematicBreak" => {}
851 _ => {
852 if node_span(node).is_some() {
853 let mut draft = BlockDraft::new(MdiTextBlockKind::Other, node);
854 self.project_inline(node, &mut draft);
855 self.finish(draft);
856 }
857 }
858 }
859 }
860
861 fn inline_block(&mut self, kind: MdiTextBlockKind, node: &serde_json::Value) {
862 let mut draft = BlockDraft::new(kind, node);
863 self.project_children(node, &mut draft);
864 self.finish(draft);
865 }
866
867 fn scalar_block(&mut self, kind: MdiTextBlockKind, node: &serde_json::Value, field: &str) {
868 let mut draft = BlockDraft::new(kind, node);
869 if let Some(value) = node.get(field).and_then(serde_json::Value::as_str) {
870 let normalized;
874 let value = if kind == MdiTextBlockKind::Code && value.contains('\r') {
875 normalized = value.replace("\r\n", "\n").replace('\r', "\n");
876 normalized.as_str()
877 } else {
878 value
879 };
880 let spans = if kind == MdiTextBlockKind::Code {
881 self.map_code_block(value, &mut draft)
882 } else {
883 self.map_value_in_block(value, &mut draft)
884 };
885 draft.append_mapped(value, spans);
886 }
887 self.finish(draft);
888 }
889
890 fn list_item(&mut self, node: &serde_json::Value, quoted: bool) {
891 let paragraphs: Vec<_> = children(node)
892 .filter(|child| node_type(child) == "paragraph")
893 .collect();
894 if !paragraphs.is_empty() {
895 let mut draft = BlockDraft::new(MdiTextBlockKind::ListItem, node);
896 for (index, paragraph) in paragraphs.into_iter().enumerate() {
897 if index > 0 {
898 draft.append_synthetic("\n\n");
899 }
900 self.project_children(paragraph, &mut draft);
901 }
902 self.finish(draft);
903 }
904 for child in children(node) {
905 if node_type(child) != "paragraph" {
906 self.collect(child, quoted);
907 }
908 }
909 }
910
911 fn footnote(&mut self, node: &serde_json::Value) {
912 let mut draft = BlockDraft::new(MdiTextBlockKind::Footnote, node);
913 for (index, child) in children(node).enumerate() {
914 if index > 0 {
915 draft.append_synthetic("\n\n");
916 }
917 if node_type(child) == "paragraph" {
918 self.project_children(child, &mut draft);
919 } else {
920 self.project_inline(child, &mut draft);
921 }
922 }
923 self.finish(draft);
924 }
925
926 fn table(&mut self, node: &serde_json::Value) {
927 let mut draft = BlockDraft::new(MdiTextBlockKind::Table, node);
928 for (row_index, row) in children(node).enumerate() {
929 if row_index > 0 {
930 draft.append_synthetic("\n");
931 }
932 for (cell_index, cell) in children(row).enumerate() {
933 if cell_index > 0 {
934 draft.append_synthetic("\t");
935 }
936 self.project_children(cell, &mut draft);
937 }
938 }
939 self.finish(draft);
940 }
941
942 fn project_children(&mut self, node: &serde_json::Value, draft: &mut BlockDraft) {
943 for child in children(node) {
944 self.project_inline(child, draft);
945 }
946 }
947
948 fn project_inline(&mut self, node: &serde_json::Value, draft: &mut BlockDraft) {
949 match plain_inline(node) {
950 PlainInline::Value(value) => {
951 if node_type(node) == "ruby" {
952 self.project_ruby(node, draft);
953 return;
954 }
955 let spans = match node_type(node) {
956 "tcy" => self.map_delimited_in_block(value, &mut *draft, '^', '^'),
957 "image" => self.map_image_in_block(value, draft),
958 "inlineCode" => self.map_inline_code_in_block(value, draft),
959 _ => self.map_value_from_node(value, node, draft),
960 };
961 draft.append_mapped(value, spans);
962 }
963 PlainInline::Break => {
964 let spans = self.map_break_in_block(draft);
965 draft.append_mapped("\n", spans);
966 }
967 PlainInline::Skip => {}
968 PlainInline::Children => {
969 self.project_children(node, draft);
970 self.advance_after_container(node, draft);
971 }
972 }
973 }
974
975 fn project_ruby(&mut self, node: &serde_json::Value, draft: &mut BlockDraft) {
976 let base = node
977 .get("base")
978 .and_then(serde_json::Value::as_str)
979 .unwrap_or_default();
980 let base_start = draft.grapheme_len();
981 let reading_value = node
982 .pointer("/ruby/value")
983 .map(|value| match value {
984 serde_json::Value::String(value) => value.clone(),
985 serde_json::Value::Array(values) => values
986 .iter()
987 .filter_map(serde_json::Value::as_str)
988 .collect::<String>(),
989 _ => String::new(),
990 })
991 .unwrap_or_default();
992 let parts = find_ruby_parts(
993 base,
994 &reading_value,
995 self.source,
996 draft.source_cursor,
997 draft.source_end,
998 );
999 if let Some(parts) = &parts {
1000 draft.source_cursor = parts.token_end;
1001 }
1002 let base_spans = parts
1003 .as_ref()
1004 .and_then(|parts| map_decoded(base, parts.base, parts.base_start));
1005 draft.append_mapped(base, base_spans);
1006 let base_end = draft.grapheme_len();
1007
1008 let ruby = node.get("ruby");
1009 let ruby_type = ruby
1010 .and_then(|value| value.get("type"))
1011 .and_then(serde_json::Value::as_str)
1012 .unwrap_or("group");
1013 if ruby_type == "split" {
1014 let readings = ruby
1015 .and_then(|value| value.get("value"))
1016 .and_then(serde_json::Value::as_array);
1017 if let Some(readings) = readings {
1018 let raw_parts = parts.as_ref().map(|value| split_raw_reading(value));
1019 for (index, reading) in readings
1020 .iter()
1021 .filter_map(serde_json::Value::as_str)
1022 .enumerate()
1023 {
1024 let raw = raw_parts.as_ref().and_then(|parts| parts.get(index));
1025 let units = annotation_units(reading, raw.copied());
1026 draft.annotations.push(AnnotationDraft {
1027 text: reading.to_owned(),
1028 anchor_start: base_start + index,
1029 anchor_end: base_start + index + 1,
1030 span: raw.map(|part| SourceSpan {
1031 start_byte: part.1,
1032 end_byte: part.1 + part.0.len() as u32,
1033 }),
1034 units,
1035 });
1036 }
1037 return;
1038 }
1039 }
1040
1041 let reading = ruby
1042 .and_then(|value| value.get("value"))
1043 .and_then(serde_json::Value::as_str)
1044 .unwrap_or_default();
1045 if reading.is_empty() {
1046 return;
1047 }
1048 let raw = parts
1049 .as_ref()
1050 .map(|parts| (parts.reading, parts.reading_start));
1051 let had_split_syntax = parts
1052 .as_ref()
1053 .is_some_and(|parts| split_unescaped_offsets(parts.reading, '.').len() > 1);
1054 if had_split_syntax {
1055 self.diagnostics.push(Diagnostic {
1056 severity: DiagnosticSeverity::Warning,
1057 code: "mdi.textProjection.rubySplitMismatch".to_owned(),
1058 message: "split ruby component count does not match the base grapheme count; the reading was anchored to the complete base".to_owned(),
1059 span: parts.as_ref().map(|parts| SourceSpan {
1060 start_byte: parts.token_start,
1061 end_byte: parts.token_end,
1062 }),
1063 });
1064 }
1065 draft.annotations.push(AnnotationDraft {
1066 text: reading.to_owned(),
1067 anchor_start: base_start,
1068 anchor_end: base_end,
1069 span: raw.map(|(raw, start)| SourceSpan {
1070 start_byte: start,
1071 end_byte: start + raw.len() as u32,
1072 }),
1073 units: annotation_ruby_units(reading, raw),
1074 });
1075 }
1076
1077 fn finish(&mut self, draft: BlockDraft) {
1078 if draft.text.is_empty() {
1079 return;
1080 }
1081 let index = self.blocks.len() as u32 + 1;
1082 let grapheme_count = draft.text.graphemes(true).count();
1083 let units = normalize_units(&draft.text, &draft.unit_texts, &draft.units);
1084 let mapping_warning = draft.mapping_warning
1085 || units.contains(&UnitMap::Unmapped)
1086 || draft
1087 .annotations
1088 .iter()
1089 .any(|annotation| annotation.units.contains(&UnitMap::Unmapped));
1090 if mapping_warning {
1091 self.diagnostics.push(Diagnostic {
1092 severity: DiagnosticSeverity::Warning,
1093 code: "mdi.textProjection.unmapped".to_owned(),
1094 message: format!(
1095 "text block {index} contains text that could not be mapped precisely"
1096 ),
1097 span: draft.span,
1098 });
1099 }
1100 let annotations = draft
1101 .annotations
1102 .into_iter()
1103 .map(|annotation| MdiTextAnnotation {
1104 kind: "rubyReading",
1105 text: annotation.text,
1106 anchor: text_range(index, annotation.anchor_start, annotation.anchor_end),
1107 span: annotation.span,
1108 source_map: source_map(index, &annotation.units),
1109 })
1110 .collect();
1111 self.blocks.push(MdiTextBlock {
1112 index,
1113 kind: draft.kind,
1114 text: draft.text,
1115 range: text_range(index, 0, grapheme_count),
1116 span: draft.span,
1117 source_map: source_map(index, &units),
1118 annotations,
1119 node: draft.node,
1120 });
1121 }
1122
1123 fn map_value_in_block(&self, value: &str, draft: &mut BlockDraft) -> Option<Vec<SourceSpan>> {
1124 let mapped = find_mapped_value(value, self.source, draft.source_cursor, draft.source_end)?;
1125 draft.source_cursor = mapped.consumed_end;
1126 Some(mapped.spans)
1127 }
1128
1129 fn map_value_from_node(
1130 &self,
1131 value: &str,
1132 node: &serde_json::Value,
1133 draft: &mut BlockDraft,
1134 ) -> Option<Vec<SourceSpan>> {
1135 if let Some(span) = node_span(node) {
1136 let mut suggested = span.start_byte;
1137 if suggested > draft.source_cursor
1138 && self.source.as_bytes().get(suggested as usize - 1) == Some(&b'\\')
1139 {
1140 suggested -= 1;
1141 }
1142 if suggested >= draft.source_cursor && suggested <= draft.source_end {
1143 draft.source_cursor = suggested;
1144 }
1145 }
1146 self.map_value_in_block(value, draft)
1147 }
1148
1149 fn map_delimited_in_block(
1150 &self,
1151 value: &str,
1152 draft: &mut BlockDraft,
1153 open: char,
1154 close: char,
1155 ) -> Option<Vec<SourceSpan>> {
1156 let needle = format!("{open}{value}{close}");
1157 let raw = self
1158 .source
1159 .get(draft.source_cursor as usize..draft.source_end as usize)?;
1160 let offset = raw.find(&needle)?;
1161 let token_start = draft.source_cursor + offset as u32;
1162 draft.source_cursor = token_start + needle.len() as u32;
1163 map_direct(value, token_start + open.len_utf8() as u32)
1164 }
1165
1166 fn map_image_in_block(&self, value: &str, draft: &mut BlockDraft) -> Option<Vec<SourceSpan>> {
1167 let raw = self
1168 .source
1169 .get(draft.source_cursor as usize..draft.source_end as usize)?;
1170 let image_start = raw.find("![")?;
1171 let alt_start = image_start + 2;
1172 let alt_end = first_unescaped(&raw[alt_start..], ']')? + alt_start;
1173 let mapped = map_decoded(
1174 value,
1175 &raw[alt_start..alt_end],
1176 draft.source_cursor + alt_start as u32,
1177 )?;
1178 let consumed = raw[alt_end..]
1179 .find(')')
1180 .map_or(alt_end + 1, |end| alt_end + end + 1);
1181 draft.source_cursor += consumed as u32;
1182 Some(mapped)
1183 }
1184
1185 fn map_inline_code_in_block(
1186 &self,
1187 value: &str,
1188 draft: &mut BlockDraft,
1189 ) -> Option<Vec<SourceSpan>> {
1190 let raw = self
1191 .source
1192 .get(draft.source_cursor as usize..draft.source_end as usize)?;
1193 for (offset, _) in raw.match_indices('`') {
1194 let opening = raw[offset..]
1195 .chars()
1196 .take_while(|character| *character == '`')
1197 .count();
1198 let delimiter = "`".repeat(opening);
1199 let inner_start = offset + opening;
1200 let Some(close_offset) = raw[inner_start..].find(&delimiter) else {
1201 continue;
1202 };
1203 let inner_end = inner_start + close_offset;
1204 let inner = &raw[inner_start..inner_end];
1205 let mut normalized = String::new();
1206 let mut spans = Vec::new();
1207 for (grapheme_offset, grapheme) in inner.grapheme_indices(true) {
1208 normalized.push_str(if grapheme == "\n" || grapheme == "\r\n" {
1209 " "
1210 } else {
1211 grapheme
1212 });
1213 spans.push(SourceSpan {
1214 start_byte: draft.source_cursor + inner_start as u32 + grapheme_offset as u32,
1215 end_byte: draft.source_cursor
1216 + inner_start as u32
1217 + grapheme_offset as u32
1218 + grapheme.len() as u32,
1219 });
1220 }
1221 if normalized.starts_with(' ')
1222 && normalized.ends_with(' ')
1223 && normalized.chars().any(|character| character != ' ')
1224 {
1225 normalized.remove(0);
1226 normalized.pop();
1227 spans.remove(0);
1228 spans.pop();
1229 }
1230 if normalized == value {
1231 draft.source_cursor += (inner_end + opening) as u32;
1232 return Some(spans);
1233 }
1234 }
1235 None
1236 }
1237
1238 fn map_code_block(&self, value: &str, draft: &mut BlockDraft) -> Option<Vec<SourceSpan>> {
1239 let raw = self
1240 .source
1241 .get(draft.source_cursor as usize..draft.source_end as usize)?;
1242 let trimmed = raw.trim_start_matches([' ', '\t']);
1243 let fenced = trimmed.starts_with("```") || trimmed.starts_with("~~~");
1244 if fenced {
1245 let opening_end = raw.find('\n')? + 1;
1246 draft.source_cursor += opening_end as u32;
1247 }
1248 self.map_value_in_block(value, draft)
1249 }
1250
1251 fn advance_after_container(&self, node: &serde_json::Value, draft: &mut BlockDraft) {
1252 let Some(raw) = self
1253 .source
1254 .get(draft.source_cursor as usize..draft.source_end as usize)
1255 else {
1256 return;
1257 };
1258 let consumed = match node_type(node) {
1259 "link" => raw.find(']').map(|label_end| {
1260 let after_label = label_end + 1;
1261 if raw[after_label..].starts_with('(') {
1262 raw[after_label + 1..]
1263 .find(')')
1264 .map_or(after_label, |end| after_label + end + 2)
1265 } else if raw[after_label..].starts_with('[') {
1266 raw[after_label + 1..]
1267 .find(']')
1268 .map_or(after_label, |end| after_label + end + 2)
1269 } else {
1270 after_label
1271 }
1272 }),
1273 "noBreak" | "warichu" | "kern" => raw.find("\x5d\x5d").map(|offset| offset + 2),
1274 "em" => raw
1275 .find("\x5d\x5d")
1276 .map(|offset| offset + 2)
1277 .or_else(|| raw.find("》》").map(|offset| offset + "》》".len())),
1278 "emphasis" | "strong" | "delete" => {
1279 let width = if node_type(node) == "emphasis" { 1 } else { 2 };
1280 raw.char_indices()
1281 .find(|(_, character)| matches!(character, '*' | '_' | '~'))
1282 .map(|(offset, character)| offset + character.len_utf8() * width)
1283 }
1284 _ => None,
1285 };
1286 if let Some(consumed) = consumed {
1287 draft.source_cursor += consumed as u32;
1288 }
1289 }
1290
1291 fn map_break_in_block(&self, draft: &mut BlockDraft) -> Option<Vec<SourceSpan>> {
1292 let raw = self
1293 .source
1294 .get(draft.source_cursor as usize..draft.source_end as usize)?;
1295 if let Some(offset) = raw.find("[[br]]") {
1296 let span = SourceSpan {
1297 start_byte: draft.source_cursor + offset as u32,
1298 end_byte: draft.source_cursor + offset as u32 + "[[br]]".len() as u32,
1299 };
1300 draft.source_cursor = span.end_byte;
1301 return Some(vec![span]);
1302 }
1303 let newline = raw.find('\n')?;
1304 let prefix = &raw[..newline];
1305 let marker_prefix = prefix.strip_suffix('\r').unwrap_or(prefix);
1306 let marker_start = marker_prefix
1307 .rfind('\\')
1308 .unwrap_or_else(|| marker_prefix.trim_end_matches(' ').len());
1309 let span = SourceSpan {
1310 start_byte: draft.source_cursor + marker_start as u32,
1311 end_byte: draft.source_cursor + newline as u32 + 1,
1312 };
1313 draft.source_cursor = span.end_byte;
1314 Some(vec![span])
1315 }
1316}
1317
1318fn annotation_units(value: &str, raw: Option<(&str, u32)>) -> Vec<UnitMap> {
1319 raw.and_then(|(raw, start)| map_decoded(value, raw, start))
1320 .map(|spans| spans.into_iter().map(UnitMap::Mapped).collect())
1321 .unwrap_or_else(|| vec![UnitMap::Unmapped; value.graphemes(true).count()])
1322}
1323
1324fn annotation_ruby_units(value: &str, raw: Option<(&str, u32)>) -> Vec<UnitMap> {
1325 let Some((raw, start)) = raw else {
1326 return vec![UnitMap::Unmapped; value.graphemes(true).count()];
1327 };
1328 let mut decoded = String::new();
1329 let mut spans = Vec::new();
1330 for (part_start, part_end) in split_unescaped_offsets(raw, '.') {
1331 let part = &raw[part_start..part_end];
1332 let part_decoded: String = decoded_atoms(part, start + part_start as u32)
1333 .iter()
1334 .map(|atom| atom.text.as_str())
1335 .collect();
1336 let Some(mut part_spans) = map_decoded(&part_decoded, part, start + part_start as u32)
1337 else {
1338 return vec![UnitMap::Unmapped; value.graphemes(true).count()];
1339 };
1340 decoded.push_str(&part_decoded);
1341 spans.append(&mut part_spans);
1342 }
1343 if decoded == value && spans.len() == value.graphemes(true).count() {
1344 spans.into_iter().map(UnitMap::Mapped).collect()
1345 } else {
1346 vec![UnitMap::Unmapped; value.graphemes(true).count()]
1347 }
1348}
1349
1350fn normalize_units(text: &str, unit_texts: &[String], units: &[UnitMap]) -> Vec<UnitMap> {
1351 if unit_texts.len() == units.len()
1352 && unit_texts
1353 .iter()
1354 .map(String::as_str)
1355 .eq(text.graphemes(true))
1356 {
1357 return units.to_vec();
1358 }
1359 let mut pieces = Vec::with_capacity(unit_texts.len());
1360 let mut offset = 0;
1361 for (unit_text, unit) in unit_texts.iter().zip(units) {
1362 let end = offset + unit_text.len();
1363 pieces.push((offset, end, *unit));
1364 offset = end;
1365 }
1366 if offset != text.len() {
1367 return vec![UnitMap::Unmapped; text.graphemes(true).count()];
1368 }
1369 text.grapheme_indices(true)
1370 .map(|(start, grapheme)| {
1371 let end = start + grapheme.len();
1372 let overlapping: Vec<_> = pieces
1373 .iter()
1374 .filter(|(piece_start, piece_end, _)| *piece_start < end && *piece_end > start)
1375 .map(|(_, _, unit)| *unit)
1376 .collect();
1377 if overlapping
1378 .iter()
1379 .all(|unit| matches!(unit, UnitMap::Mapped(_)))
1380 {
1381 let first = match overlapping.first() {
1382 Some(UnitMap::Mapped(span)) => *span,
1383 _ => return UnitMap::Unmapped,
1384 };
1385 let last = match overlapping.last() {
1386 Some(UnitMap::Mapped(span)) => *span,
1387 _ => return UnitMap::Unmapped,
1388 };
1389 UnitMap::Mapped(SourceSpan {
1390 start_byte: first.start_byte,
1391 end_byte: last.end_byte,
1392 })
1393 } else if overlapping
1394 .iter()
1395 .all(|unit| matches!(unit, UnitMap::Synthetic))
1396 {
1397 UnitMap::Synthetic
1398 } else {
1399 UnitMap::Unmapped
1400 }
1401 })
1402 .collect()
1403}
1404
1405fn source_map(block: u32, units: &[UnitMap]) -> MdiTextSourceMap {
1406 let mut map = MdiTextSourceMap::default();
1407 let mut index = 0;
1408 while index < units.len() {
1409 match units[index] {
1410 UnitMap::Mapped(first) => {
1411 let start = index;
1412 let mut boundaries = vec![first.start_byte, first.end_byte];
1413 index += 1;
1414 while let Some(UnitMap::Mapped(next)) = units.get(index).copied() {
1415 if boundaries.last().copied() != Some(next.start_byte) {
1416 break;
1417 }
1418 boundaries.push(next.end_byte);
1419 index += 1;
1420 }
1421 map.runs.push(MdiTextSourceRun {
1422 range: text_range(block, start, index),
1423 source_boundaries: boundaries,
1424 });
1425 }
1426 UnitMap::Synthetic => {
1427 let start = index;
1428 while matches!(units.get(index), Some(UnitMap::Synthetic)) {
1429 index += 1;
1430 }
1431 map.synthetic.push(text_range(block, start, index));
1432 }
1433 UnitMap::Unmapped => {
1434 let start = index;
1435 while matches!(units.get(index), Some(UnitMap::Unmapped)) {
1436 index += 1;
1437 }
1438 map.unmapped.push(text_range(block, start, index));
1439 }
1440 }
1441 }
1442 map
1443}
1444
1445fn text_range(block: u32, start: usize, end: usize) -> MdiTextRange {
1446 MdiTextRange {
1447 start: MdiTextPosition {
1448 block,
1449 character: start as u32 + 1,
1450 },
1451 end: MdiTextPosition {
1452 block,
1453 character: end as u32 + 1,
1454 },
1455 }
1456}
1457
1458fn node_type(node: &serde_json::Value) -> &str {
1459 node.get("type")
1460 .and_then(serde_json::Value::as_str)
1461 .unwrap_or_default()
1462}
1463
1464fn children(node: &serde_json::Value) -> impl Iterator<Item = &serde_json::Value> {
1465 node.get("children")
1466 .and_then(serde_json::Value::as_array)
1467 .into_iter()
1468 .flatten()
1469}
1470
1471fn node_span(node: &serde_json::Value) -> Option<SourceSpan> {
1472 Some(SourceSpan {
1473 start_byte: node.pointer("/span/startByte")?.as_u64()? as u32,
1474 end_byte: node.pointer("/span/endByte")?.as_u64()? as u32,
1475 })
1476}
1477
1478struct MappedValue {
1479 spans: Vec<SourceSpan>,
1480 consumed_end: u32,
1481}
1482
1483fn find_mapped_value(value: &str, source: &str, start: u32, end: u32) -> Option<MappedValue> {
1484 if value.contains('\n') {
1485 let mut spans = Vec::new();
1486 let mut cursor = start;
1487 let lines: Vec<_> = value.split('\n').collect();
1488 for (index, line) in lines.iter().enumerate() {
1489 if !line.is_empty() {
1490 let mapped = find_mapped_value(line, source, cursor, end)?;
1491 cursor = mapped.consumed_end;
1492 spans.extend(mapped.spans);
1493 }
1494 if index + 1 < lines.len() {
1495 let remaining = source.get(cursor as usize..end as usize)?;
1496 let newline = remaining.find('\n')?;
1497 let newline_end = cursor + newline as u32 + 1;
1498 let newline_start = if newline > 0 && remaining.as_bytes()[newline - 1] == b'\r' {
1499 newline_end - 2
1500 } else {
1501 newline_end - 1
1502 };
1503 spans.push(SourceSpan {
1504 start_byte: newline_start,
1505 end_byte: newline_end,
1506 });
1507 cursor = newline_end;
1508 }
1509 }
1510 return Some(MappedValue {
1511 spans,
1512 consumed_end: cursor,
1513 });
1514 }
1515 let raw = source.get(start as usize..end as usize)?;
1516 let direct_offset = raw
1517 .find(value)
1518 .filter(|offset| direct_match_is_source_literal(raw, *offset, value));
1519 if direct_offset == Some(0) {
1520 return Some(MappedValue {
1521 spans: map_direct(value, start)?,
1522 consumed_end: start + value.len() as u32,
1523 });
1524 }
1525 for (candidate, _) in raw.char_indices() {
1526 if direct_offset.is_some_and(|offset| candidate > offset) {
1527 break;
1528 }
1529 if let Some((spans, consumed)) =
1530 map_decoded_prefix(value, &raw[candidate..], start + candidate as u32)
1531 {
1532 return Some(MappedValue {
1533 spans,
1534 consumed_end: start + candidate as u32 + consumed as u32,
1535 });
1536 }
1537 }
1538 None
1539}
1540
1541fn map_decoded_prefix(value: &str, raw: &str, start: u32) -> Option<(Vec<SourceSpan>, usize)> {
1542 let atoms = decoded_atoms(raw, start);
1543 let mut decoded = String::new();
1544 for atom in atoms {
1545 decoded.push_str(&atom.text);
1546 if decoded == value {
1547 let consumed = atom.span.end_byte.checked_sub(start)? as usize;
1548 return map_decoded(value, &raw[..consumed], start).map(|spans| (spans, consumed));
1549 }
1550 if !value.starts_with(&decoded) {
1551 return None;
1552 }
1553 }
1554 None
1555}
1556
1557fn direct_match_is_source_literal(raw: &str, offset: usize, value: &str) -> bool {
1558 if offset > 0 && raw.as_bytes()[offset - 1] == b'\\' {
1559 return false;
1560 }
1561 let candidate = &raw[offset..];
1562 if candidate.starts_with('&')
1563 && let Some(end) = candidate.find(';')
1564 && decode_reference(&candidate[1..end]).as_deref() == Some(value)
1565 && end + 1 != value.len()
1566 {
1567 return false;
1568 }
1569 true
1570}
1571
1572fn map_direct(value: &str, start: u32) -> Option<Vec<SourceSpan>> {
1573 Some(
1574 value
1575 .grapheme_indices(true)
1576 .map(|(offset, grapheme)| SourceSpan {
1577 start_byte: start + offset as u32,
1578 end_byte: start + offset as u32 + grapheme.len() as u32,
1579 })
1580 .collect(),
1581 )
1582}
1583
1584struct Atom {
1585 text: String,
1586 span: SourceSpan,
1587}
1588
1589fn map_decoded(value: &str, raw: &str, start: u32) -> Option<Vec<SourceSpan>> {
1590 if value == raw {
1591 return map_direct(value, start);
1592 }
1593 let atoms = decoded_atoms(raw, start);
1594 let decoded: String = atoms.iter().map(|atom| atom.text.as_str()).collect();
1595 if decoded != value {
1596 return None;
1597 }
1598 let mut atom_ranges = Vec::with_capacity(atoms.len());
1599 let mut decoded_offset = 0;
1600 for atom in &atoms {
1601 let end = decoded_offset + atom.text.len();
1602 atom_ranges.push((decoded_offset, end, atom.span));
1603 decoded_offset = end;
1604 }
1605 let mut result = Vec::new();
1606 for (offset, grapheme) in value.grapheme_indices(true) {
1607 let end = offset + grapheme.len();
1608 let overlapping: Vec<_> = atom_ranges
1609 .iter()
1610 .filter(|(atom_start, atom_end, _)| *atom_start < end && *atom_end > offset)
1611 .collect();
1612 let first = overlapping.first()?.2;
1613 let last = overlapping.last()?.2;
1614 result.push(SourceSpan {
1615 start_byte: first.start_byte,
1616 end_byte: last.end_byte,
1617 });
1618 }
1619 Some(result)
1620}
1621
1622fn decoded_atoms(raw: &str, start: u32) -> Vec<Atom> {
1623 let mut atoms = Vec::new();
1624 let mut index = 0;
1625 while index < raw.len() {
1626 let rest = &raw[index..];
1627 if rest.starts_with('\\')
1628 && let Some(next) = rest.chars().nth(1)
1629 && (next.is_ascii_punctuation() || "{}|^[]:《》\\.".contains(next))
1630 {
1631 let len = 1 + next.len_utf8();
1632 atoms.push(Atom {
1633 text: next.to_string(),
1634 span: SourceSpan {
1635 start_byte: start + index as u32,
1636 end_byte: start + (index + len) as u32,
1637 },
1638 });
1639 index += len;
1640 continue;
1641 }
1642 if rest.starts_with('&')
1643 && let Some(end) = rest.find(';')
1644 && let Some(decoded) = decode_reference(&rest[1..end])
1645 {
1646 atoms.push(Atom {
1647 text: decoded,
1648 span: SourceSpan {
1649 start_byte: start + index as u32,
1650 end_byte: start + (index + end + 1) as u32,
1651 },
1652 });
1653 index += end + 1;
1654 continue;
1655 }
1656 let character = rest.chars().next().expect("non-empty remainder");
1657 let len = character.len_utf8();
1658 atoms.push(Atom {
1659 text: character.to_string(),
1660 span: SourceSpan {
1661 start_byte: start + index as u32,
1662 end_byte: start + (index + len) as u32,
1663 },
1664 });
1665 index += len;
1666 }
1667 atoms
1668}
1669
1670fn decode_reference(body: &str) -> Option<String> {
1671 if let Some(hex) = body.strip_prefix("#x").or_else(|| body.strip_prefix("#X")) {
1672 return (!hex.is_empty() && hex.chars().all(|character| character.is_ascii_hexdigit()))
1673 .then(|| markdown::decode_numeric(hex, 16));
1674 }
1675 if let Some(decimal) = body.strip_prefix('#') {
1676 return (!decimal.is_empty()
1677 && decimal.chars().all(|character| character.is_ascii_digit()))
1678 .then(|| markdown::decode_numeric(decimal, 10));
1679 }
1680 markdown::decode_named(body, true)
1681}
1682
1683struct RubyParts<'a> {
1684 token_start: u32,
1685 base: &'a str,
1686 base_start: u32,
1687 reading: &'a str,
1688 reading_start: u32,
1689 token_end: u32,
1690}
1691
1692fn find_ruby_parts<'a>(
1693 base: &str,
1694 reading: &str,
1695 source: &'a str,
1696 start: u32,
1697 end: u32,
1698) -> Option<RubyParts<'a>> {
1699 let raw = source.get(start as usize..end as usize)?;
1700 for (offset, _) in raw.match_indices('{') {
1701 let candidate = &raw[offset..];
1702 let Some(close) = first_unescaped(&candidate[1..], '}').map(|close| close + 1) else {
1703 continue;
1704 };
1705 let body = &candidate[1..close];
1706 let Some(separator) = first_unescaped(body, '|') else {
1707 continue;
1708 };
1709 let raw_base = &body[..separator];
1710 let raw_reading = &body[separator + 1..];
1711 let decoded_base: String = decoded_atoms(raw_base, 0)
1712 .into_iter()
1713 .map(|atom| atom.text)
1714 .collect();
1715 let decoded_reading: String = split_unescaped_offsets(raw_reading, '.')
1716 .into_iter()
1717 .flat_map(|(part_start, part_end)| {
1718 decoded_atoms(&raw_reading[part_start..part_end], 0)
1719 .into_iter()
1720 .map(|atom| atom.text)
1721 })
1722 .collect();
1723 if decoded_base == base && decoded_reading == reading {
1724 let token_start = start + offset as u32;
1725 return Some(RubyParts {
1726 token_start,
1727 base: raw_base,
1728 base_start: token_start + 1,
1729 reading: raw_reading,
1730 reading_start: token_start + 1 + separator as u32 + 1,
1731 token_end: token_start + close as u32 + 1,
1732 });
1733 }
1734 }
1735 None
1736}
1737
1738fn split_raw_reading<'a>(parts: &'a RubyParts<'a>) -> Vec<(&'a str, u32)> {
1739 split_unescaped_offsets(parts.reading, '.')
1740 .into_iter()
1741 .map(|(start, end)| {
1742 (
1743 &parts.reading[start..end],
1744 parts.reading_start + start as u32,
1745 )
1746 })
1747 .collect()
1748}
1749
1750fn first_unescaped(value: &str, needle: char) -> Option<usize> {
1751 let mut escaped = false;
1752 for (index, character) in value.char_indices() {
1753 if escaped {
1754 escaped = false;
1755 } else if character == '\\' {
1756 escaped = true;
1757 } else if character == needle {
1758 return Some(index);
1759 }
1760 }
1761 None
1762}
1763
1764fn split_unescaped_offsets(value: &str, separator: char) -> Vec<(usize, usize)> {
1765 let mut result = Vec::new();
1766 let mut start = 0;
1767 let mut escaped = false;
1768 for (index, character) in value.char_indices() {
1769 if escaped {
1770 escaped = false;
1771 } else if character == '\\' {
1772 escaped = true;
1773 } else if character == separator {
1774 result.push((start, index));
1775 start = index + character.len_utf8();
1776 }
1777 }
1778 result.push((start, value.len()));
1779 result
1780}
1781
1782#[cfg(test)]
1783mod tests {
1784 use super::*;
1785
1786 fn position(range: &MdiTextRange) -> (u32, u32, u32, u32) {
1787 (
1788 range.start.block,
1789 range.start.character,
1790 range.end.block,
1791 range.end.character,
1792 )
1793 }
1794
1795 fn assert_complete_mapping(block: &MdiTextBlock, source: &str) {
1796 let count = block.text.graphemes(true).count();
1797 let mut coverage = vec![0_u8; count];
1798 for run in &block.source_map.runs {
1799 let start = run.range.start.character as usize - 1;
1800 let end = run.range.end.character as usize - 1;
1801 assert_eq!(run.source_boundaries.len(), end - start + 1);
1802 for boundary in &run.source_boundaries {
1803 assert!((*boundary as usize) <= source.len());
1804 assert!(source.is_char_boundary(*boundary as usize));
1805 }
1806 for covered in &mut coverage[start..end] {
1807 *covered += 1;
1808 }
1809 }
1810 for range in &block.source_map.synthetic {
1811 let start = range.start.character as usize - 1;
1812 let end = range.end.character as usize - 1;
1813 for covered in &mut coverage[start..end] {
1814 *covered += 1;
1815 }
1816 }
1817 assert!(block.source_map.unmapped.is_empty(), "{block:#?}");
1818 assert!(coverage.iter().all(|covered| *covered == 1), "{block:#?}");
1819 }
1820
1821 #[test]
1822 fn projects_grapheme_positions_and_ruby_channels() {
1823 let result = get_mdi_text_blocks("# 序章\n\n我喜歡{東京|とうきょう}。\n\ne\u{301} 👩🏽💻");
1824 assert_eq!(result.blocks.len(), 3);
1825 assert_eq!(result.blocks[0].text, "序章");
1826 assert_eq!(position(&result.blocks[0].range), (1, 1, 1, 3));
1827 assert_eq!(result.blocks[1].text, "我喜歡東京。");
1828 assert_eq!(position(&result.blocks[1].range), (2, 1, 2, 7));
1829 let annotation = &result.blocks[1].annotations[0];
1830 assert_eq!(annotation.text, "とうきょう");
1831 assert_eq!(position(&annotation.anchor), (2, 4, 2, 6));
1832 assert_eq!(result.blocks[2].text.graphemes(true).count(), 3);
1833 assert_eq!(position(&result.blocks[2].range), (3, 1, 3, 4));
1834 assert!(result.diagnostics.is_empty());
1835
1836 let across_wrapper = get_mdi_text_blocks("e*\u{301}*");
1837 assert_eq!(across_wrapper.blocks[0].text, "e\u{301}");
1838 assert_eq!(position(&across_wrapper.blocks[0].range), (1, 1, 1, 2));
1839 assert!(across_wrapper.blocks[0].source_map.unmapped.is_empty());
1840
1841 let marker_text = get_mdi_text_blocks("# \\#\n\n- \\-\n\n> \\>");
1842 assert_eq!(
1843 marker_text
1844 .blocks
1845 .iter()
1846 .map(|block| block.source_map.runs[0].source_boundaries[0])
1847 .collect::<Vec<_>>(),
1848 vec![2, 8, 14]
1849 );
1850 }
1851
1852 #[test]
1853 fn maps_entities_escapes_and_mdi_delimiters_to_complete_source_tokens() {
1854 let source = r"& \* {東京|とうきょう} ^12^ 前[[br]]次";
1855 let result = get_mdi_text_blocks(source);
1856 let block = &result.blocks[0];
1857 assert_eq!(block.text, "& * 東京 12 前\n次");
1858 assert!(block.source_map.unmapped.is_empty(), "{block:#?}");
1859 assert!(block.source_map.synthetic.is_empty());
1860
1861 let spans: Vec<_> = block
1862 .source_map
1863 .runs
1864 .iter()
1865 .flat_map(|run| run.source_boundaries.windows(2))
1866 .map(|pair| &source[pair[0] as usize..pair[1] as usize])
1867 .collect();
1868 assert!(spans.contains(&"&"));
1869 assert!(spans.contains(&r"\*"));
1870 assert!(spans.contains(&"[[br]]"));
1871 assert!(!spans.contains(&"とうきょう"));
1872 assert!(result.diagnostics.is_empty());
1873 }
1874
1875 #[test]
1876 fn gives_each_split_ruby_reading_its_base_grapheme_anchor() {
1877 let result = get_mdi_text_blocks("{東京|とう.きょう}");
1878 let annotations = &result.blocks[0].annotations;
1879 assert_eq!(annotations.len(), 2);
1880 assert_eq!(annotations[0].text, "とう");
1881 assert_eq!(position(&annotations[0].anchor), (1, 1, 1, 2));
1882 assert_eq!(annotations[1].text, "きょう");
1883 assert_eq!(position(&annotations[1].anchor), (1, 2, 1, 3));
1884 assert!(
1885 annotations
1886 .iter()
1887 .all(|annotation| annotation.source_map.unmapped.is_empty())
1888 );
1889 }
1890
1891 #[test]
1892 fn mismatched_split_ruby_degrades_to_a_mapped_group_warning() {
1893 let result = get_mdi_text_blocks("{東京|とう.きょ.う}");
1894 let annotation = &result.blocks[0].annotations[0];
1895 assert_eq!(annotation.text, "とうきょう");
1896 assert_eq!(position(&annotation.anchor), (1, 1, 1, 3));
1897 assert!(annotation.source_map.unmapped.is_empty());
1898 assert!(
1899 result
1900 .diagnostics
1901 .iter()
1902 .any(|diagnostic| diagnostic.code == "mdi.textProjection.rubySplitMismatch")
1903 );
1904 }
1905
1906 #[test]
1907 fn collects_leaf_blocks_without_parent_text_duplication() {
1908 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---";
1909 let result = get_mdi_text_blocks(source);
1910 let summaries: Vec<_> = result
1911 .blocks
1912 .iter()
1913 .map(|block| (block.kind, block.text.as_str()))
1914 .collect();
1915 assert_eq!(
1916 summaries,
1917 vec![
1918 (MdiTextBlockKind::ListItem, "first\n\nsecond"),
1919 (MdiTextBlockKind::ListItem, "nested"),
1920 (MdiTextBlockKind::Blockquote, "quote one"),
1921 (MdiTextBlockKind::Blockquote, "quote two"),
1922 (MdiTextBlockKind::Table, "a\tb\nc\td"),
1923 (MdiTextBlockKind::Code, "code\nline"),
1924 (MdiTextBlockKind::Paragraph, "body"),
1925 (MdiTextBlockKind::Footnote, "note"),
1926 ]
1927 );
1928 assert_eq!(result.blocks[0].source_map.synthetic.len(), 1);
1929 assert_eq!(result.blocks[4].source_map.synthetic.len(), 3);
1930 assert!(
1931 result
1932 .blocks
1933 .iter()
1934 .all(|block| block.source_map.unmapped.is_empty())
1935 );
1936
1937 let fenced = get_mdi_text_blocks("```rust\nrust\n```");
1938 assert_eq!(fenced.blocks[0].text, "rust");
1939 assert_eq!(fenced.blocks[0].source_map.runs[0].source_boundaries[0], 8);
1940 }
1941
1942 #[test]
1943 fn projection_json_is_deterministic_and_keeps_the_parse_envelope() {
1944 let source = "---\nmdi: '2.0'\ntitle: x\n---\n\n# heading\n\ntext";
1945 let first = get_mdi_text_blocks_json(source);
1946 assert_eq!(first, get_mdi_text_blocks_json(source));
1947 let value: serde_json::Value = serde_json::from_str(&first).unwrap();
1948 assert_eq!(value["projectionVersion"], "1.0");
1949 assert_eq!(
1950 value["positionEncoding"],
1951 "unicode-grapheme-cluster-1-based"
1952 );
1953 assert_eq!(value["irVersion"], MDI_IR_VERSION);
1954 assert_eq!(value["document"]["frontmatter"]["entries"][0]["key"], "mdi");
1955 }
1956
1957 #[test]
1958 fn supported_inline_and_wrapped_markdown_is_fully_mapped() {
1959 let source = "**強調** [label](https://example.test)  `code` \\* & [[no-break:禁則]][[warichu:割注]][[kern:-0.1em:詰め]][[em:傍点]]\n\n> first\n> continued\n\n- item\n continued";
1960 let result = get_mdi_text_blocks(source);
1961 assert_eq!(
1962 result
1963 .blocks
1964 .iter()
1965 .map(|block| block.text.as_str())
1966 .collect::<Vec<_>>(),
1967 vec![
1968 "強調 label 代替 code * & 禁則割注詰め傍点",
1969 "first\ncontinued",
1970 "item\ncontinued",
1971 ]
1972 );
1973 for block in &result.blocks {
1974 assert_complete_mapping(block, source);
1975 }
1976 }
1977
1978 #[test]
1979 fn malformed_literals_remain_searchable_and_precisely_mapped() {
1980 for source in [
1981 "{東京|とうきょう",
1982 "[[em:未閉",
1983 "《《未閉",
1984 "^1234567^ ^12^",
1985 "<custom>literal</custom>",
1986 ] {
1987 let result = get_mdi_text_blocks(source);
1988 assert!(!result.blocks.is_empty(), "{source:?}");
1989 for block in &result.blocks {
1990 assert_complete_mapping(block, source);
1991 }
1992 }
1993 let frontmatter = get_mdi_text_blocks("---\ntitle: hidden\n---\n\nvisible");
1994 assert_eq!(frontmatter.blocks.len(), 1);
1995 assert_eq!(frontmatter.blocks[0].text, "visible");
1996 }
1997}