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