1use super::matching::{aligned_score, unique_max, UniqueMatch};
3use super::SourceDocument;
4use crate::prelude::{String, ToString, Vec};
5use crate::util::{frontmatter_and_body, MimeType};
6use core::fmt;
7use core::ops::{Add, Range};
8use jsonc_parser::ast::Value as JsonValue;
9use jsonc_parser::{parse_to_ast, CollectOptions, ParseOptions};
10use serde_json::Value;
11
12pub(crate) trait DocumentParser {
13 fn try_entries(&self, document: &SourceDocument) -> Option<Vec<DocumentEntry>>;
14 fn entries(&self, document: &SourceDocument) -> Vec<DocumentEntry>;
15 fn line(path: &str, start: usize, line: &str, prefix: &str) -> Option<DocumentEntry>
16 where
17 Self: Sized;
18 fn lists(path: &str, lines: &[(usize, &str)]) -> Vec<DocumentEntry>
19 where
20 Self: Sized;
21 fn scalar(content: &str, path: &str, lines: &[(usize, &str)]) -> Option<DocumentEntry>
22 where
23 Self: Sized;
24 fn sections(content: &str, name: &str, lines: &[(usize, &str)]) -> Vec<DocumentEntry>
25 where
26 Self: Sized;
27}
28#[derive(Clone, Debug, Eq, PartialEq)]
30pub enum DocumentMatch {
31 Unique(DocumentSpan),
33 Missing,
35 Ambiguous,
37}
38#[derive(Clone, Debug, Eq, Hash, PartialEq)]
40pub enum DocumentPathSegment {
41 Key(String),
43 Index(usize),
45}
46#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
48pub struct DocumentPath(Vec<DocumentPathSegment>);
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct DocumentPosition {
52 pub byte: usize,
54 pub line: usize,
56 pub column: usize,
58}
59#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct DocumentSpan(pub Range<usize>);
62#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct DocumentExcerpt {
65 pub content: String,
67 pub span: DocumentSpan,
69}
70#[derive(Clone, Debug, Default, Eq, PartialEq)]
72pub struct DocumentQuery {
73 paths: Vec<DocumentPath>,
74 value: Option<String>,
75 needle: Option<String>,
76}
77#[derive(Clone, Debug)]
78pub(crate) struct DocumentEntry {
79 path: DocumentPath,
80 value: Option<String>,
81 span: DocumentSpan,
82}
83#[derive(Clone, Debug)]
84struct SemanticEntry {
85 path: DocumentPath,
86 value: Option<String>,
87}
88#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
90struct Confidence {
91 exact_indices: usize,
92 exact_keys: usize,
93 normalized_keys: usize,
94 matching_ancestry: usize,
95}
96#[derive(Clone, Debug)]
98pub struct DocumentIndex {
99 document: SourceDocument,
100 entries: Vec<DocumentEntry>,
101 line_starts: Vec<usize>,
102 physical: bool,
103 semantic: Vec<SemanticEntry>,
104}
105impl DocumentPath {
106 pub fn parse(value: &str) -> Self {
108 let (mut segments, key) = value
109 .replace("r#", "")
110 .chars()
111 .fold((Vec::new(), String::new()), |(mut segments, mut key), character| {
112 match character {
113 | '.' => {
114 if !key.is_empty() {
115 segments.push(DocumentPathSegment::Key(core::mem::take(&mut key)));
116 }
117 }
118 | '[' => {
119 if !key.is_empty() {
120 segments.push(DocumentPathSegment::Key(core::mem::take(&mut key)));
121 }
122 key.push(character);
123 }
124 | ']' if key.starts_with('[') => {
125 let index = key.trim_start_matches('[').parse::<usize>().ok();
126 if let Some(index) = index {
127 segments.push(DocumentPathSegment::Index(index));
128 }
129 key.clear();
130 }
131 | _ => key.push(character),
132 }
133 (segments, key)
134 });
135 if !key.is_empty() {
136 segments.push(DocumentPathSegment::Key(key));
137 }
138 Self(segments)
139 }
140 fn with(&self, segment: DocumentPathSegment) -> Self {
141 Self(self.0.iter().cloned().chain(core::iter::once(segment)).collect())
142 }
143 fn confidence(&self, actual: &Self) -> Option<(Confidence, bool, bool)> {
144 let depth = self.0.len();
145 let confidence = aligned_score(&self.0, &actual.0, |index, expected, actual| match (expected, actual) {
146 | (DocumentPathSegment::Index(expected), DocumentPathSegment::Index(actual)) if expected == actual => {
147 let confidence = Confidence {
148 exact_indices: 1,
149 ..Confidence::default()
150 };
151 Some(confidence)
152 }
153 | (DocumentPathSegment::Key(expected), DocumentPathSegment::Key(actual)) if expected == actual => {
154 let confidence = Confidence {
155 exact_keys: 1,
156 matching_ancestry: usize::from(index.saturating_add(1) < depth),
157 ..Confidence::default()
158 };
159 Some(confidence)
160 }
161 | (DocumentPathSegment::Key(expected), DocumentPathSegment::Key(actual)) if field_names_match(expected, actual) => {
162 let confidence = Confidence {
163 normalized_keys: 1,
164 matching_ancestry: usize::from(index.saturating_add(1) < depth),
165 ..Confidence::default()
166 };
167 Some(confidence)
168 }
169 | (DocumentPathSegment::Key(_), DocumentPathSegment::Key(_)) => Some(Confidence::default()),
170 | _ => None,
171 })?;
172 let mechanical = self.0.iter().zip(&actual.0).all(|(expected, actual)| match (expected, actual) {
173 | (DocumentPathSegment::Key(expected), DocumentPathSegment::Key(actual)) => field_names_match(expected, actual),
174 | (DocumentPathSegment::Index(expected), DocumentPathSegment::Index(actual)) => expected == actual,
175 | _ => false,
176 });
177 let anchored = confidence.exact_keys.saturating_add(confidence.normalized_keys) > 0;
178 Some((confidence, mechanical, anchored))
179 }
180 fn semantic_entries(&self, value: &Value) -> Vec<SemanticEntry> {
181 match value {
182 | Value::Object(object) => object
183 .iter()
184 .flat_map(|(key, value)| self.with(DocumentPathSegment::Key(key.clone())).semantic_entries(value))
185 .collect(),
186 | Value::Array(array) => array
187 .iter()
188 .enumerate()
189 .flat_map(|(index, value)| self.with(DocumentPathSegment::Index(index)).semantic_entries(value))
190 .collect(),
191 | value => vec![SemanticEntry {
192 path: self.clone(),
193 value: Some(scalar(value)),
194 }],
195 }
196 }
197 fn collect_entries(&self, value: &JsonValue<'_>) -> Vec<DocumentEntry> {
198 match value {
199 | JsonValue::Object(object) => object
200 .properties
201 .iter()
202 .flat_map(|property| {
203 self.with(DocumentPathSegment::Key(property.name.as_str().to_string()))
204 .collect_entries(&property.value)
205 })
206 .collect(),
207 | JsonValue::Array(array) => array
208 .elements
209 .iter()
210 .enumerate()
211 .flat_map(|(index, value)| self.with(DocumentPathSegment::Index(index)).collect_entries(value))
212 .collect(),
213 | JsonValue::StringLit(value) => vec![DocumentEntry {
214 path: self.clone(),
215 value: Some(value.value.to_string()),
216 span: DocumentSpan(value.range.start..value.range.end),
217 }],
218 | JsonValue::NumberLit(value) => vec![DocumentEntry {
219 path: self.clone(),
220 value: Some(value.value.to_string()),
221 span: DocumentSpan(value.range.start..value.range.end),
222 }],
223 | JsonValue::BooleanLit(value) => vec![DocumentEntry {
224 path: self.clone(),
225 value: Some(value.value.to_string()),
226 span: DocumentSpan(value.range.start..value.range.end),
227 }],
228 | JsonValue::NullKeyword(value) => vec![DocumentEntry {
229 path: self.clone(),
230 value: None,
231 span: DocumentSpan(value.range.start..value.range.end),
232 }],
233 }
234 }
235}
236impl DocumentQuery {
237 pub fn new() -> Self {
239 Self::default()
240 }
241 pub fn with_path(mut self, path: DocumentPath) -> Self {
243 self.paths.push(path);
244 self
245 }
246 pub fn with_value(mut self, value: impl Into<String>) -> Self {
248 self.value = Some(value.into());
249 self
250 }
251 pub fn with_needle(mut self, needle: impl Into<String>) -> Self {
253 self.needle = Some(needle.into());
254 self
255 }
256 fn matches(&self, value: Option<&str>) -> bool {
257 let value_matches = self.value.as_deref().is_none_or(|expected| value == Some(expected));
258 let needle_matches = self
259 .needle
260 .as_deref()
261 .is_none_or(|needle| value.is_some_and(|value| value.contains(needle)));
262 value_matches && needle_matches
263 }
264}
265impl DocumentIndex {
266 pub fn new(document: SourceDocument) -> Self {
268 Self::with_parsers(document, &[])
269 }
270 pub(crate) fn with_parsers(document: SourceDocument, parsers: &[&dyn DocumentParser]) -> Self {
271 let markdown_entries = parsers.iter().find_map(|parser| parser.try_entries(&document)).unwrap_or_default();
272 let entries = document.json_entries().into_iter().chain(markdown_entries).collect();
273 let line_starts = core::iter::once(0)
274 .chain(document.content.match_indices('\n').map(|(index, _)| index.saturating_add(1)))
275 .collect();
276 let semantic = document.semantic_entries();
277 Self {
278 physical: document.is_physical_text(),
279 document,
280 entries,
281 line_starts,
282 semantic,
283 }
284 }
285 pub fn document(&self) -> &SourceDocument {
287 &self.document
288 }
289 pub fn position(&self, byte: usize) -> Option<DocumentPosition> {
291 self.document.content.is_char_boundary(byte).then(|| {
292 let line_index = self.line_starts.partition_point(|start| *start <= byte).saturating_sub(1);
293 let line_start = self.line_starts.get(line_index).copied().unwrap_or_default();
294 let column = self.document.content[line_start..byte].chars().count().saturating_add(1);
295 DocumentPosition {
296 byte,
297 line: line_index.saturating_add(1),
298 column,
299 }
300 })
301 }
302 pub fn locate(&self, query: &DocumentQuery) -> Option<DocumentPosition> {
304 match self.resolve(query) {
305 | DocumentMatch::Unique(span) => self.position(span.0.start),
306 | DocumentMatch::Missing | DocumentMatch::Ambiguous => None,
307 }
308 }
309 pub fn excerpt(&self, span: &DocumentSpan, max_prefix: usize) -> Option<DocumentExcerpt> {
311 let DocumentSpan(range) = span;
312 let content = &self.document.content;
313 let ordered = range.start <= range.end;
314 let in_bounds = range.end <= content.len();
315 let boundaries = content.is_char_boundary(range.start) && content.is_char_boundary(range.end);
316 let valid = ordered && in_bounds && boundaries;
317 valid.then(|| self.position(range.start)).flatten().and_then(|position| {
318 let line_offset = position.line.saturating_sub(1);
319 self.line_starts.get(line_offset).copied().and_then(|line_start| {
320 let should_truncate = range.start.saturating_sub(line_start) > max_prefix;
321 let (excerpt_start, ellipsis) = if should_truncate {
322 (
323 prefix_boundary(&self.document.content, range.start.saturating_sub(max_prefix), range.start),
324 "...",
325 )
326 } else {
327 (line_start, "")
328 };
329 self.document.content.get(excerpt_start..).map(|suffix| {
330 let content = format!("{}{ellipsis}{suffix}", "\n".repeat(line_offset));
331 let adjustment = line_offset.saturating_add(ellipsis.len());
332 let adjusted_start = range.start.saturating_sub(excerpt_start).saturating_add(adjustment);
333 let adjusted_end = range.end.saturating_sub(excerpt_start).saturating_add(adjustment);
334 let span = DocumentSpan(adjusted_start..adjusted_end);
335 DocumentExcerpt { content, span }
336 })
337 })
338 })
339 }
340 pub fn resolve(&self, query: &DocumentQuery) -> DocumentMatch {
342 match (self.physical, query.paths.is_empty()) {
343 | (false, _) => DocumentMatch::Missing,
344 | (true, true) => self.resolve_text(query),
345 | (true, false) => match resolve_candidate(
346 &self.entries,
347 query,
348 |entry| &entry.path,
349 |entry| entry.value.as_deref(),
350 |entry| {
351 query.needle.as_deref().is_none_or(|needle| {
352 self.document
353 .content
354 .get(entry.span.0.clone())
355 .is_some_and(|value| value.contains(needle))
356 })
357 },
358 ) {
359 | UniqueMatch::Unique(entry) => DocumentMatch::Unique(narrow_span(&self.document.content, &entry.span, query.needle.as_deref())),
360 | UniqueMatch::Ambiguous => DocumentMatch::Ambiguous,
361 | UniqueMatch::Missing => {
362 match resolve_candidate(&self.semantic, query, |entry| &entry.path, |entry| entry.value.as_deref(), |_| true) {
363 | UniqueMatch::Unique(_) => self.resolve_text(query),
364 | UniqueMatch::Ambiguous => DocumentMatch::Ambiguous,
365 | UniqueMatch::Missing => DocumentMatch::Missing,
366 }
367 }
368 },
369 }
370 }
371 fn resolve_text(&self, query: &DocumentQuery) -> DocumentMatch {
372 let sought = query.needle.as_ref().or(query.value.as_ref());
373 sought.map_or(DocumentMatch::Missing, |sought| {
374 unique_span(
375 self.document
376 .content
377 .match_indices(sought)
378 .map(|(start, value)| DocumentSpan(start..start.saturating_add(value.len())))
379 .collect(),
380 )
381 })
382 }
383}
384impl Add for Confidence {
385 type Output = Self;
386 fn add(self, other: Self) -> Self {
387 Self {
388 exact_indices: self.exact_indices.saturating_add(other.exact_indices),
389 exact_keys: self.exact_keys.saturating_add(other.exact_keys),
390 normalized_keys: self.normalized_keys.saturating_add(other.normalized_keys),
391 matching_ancestry: self.matching_ancestry.saturating_add(other.matching_ancestry),
392 }
393 }
394}
395impl fmt::Display for DocumentPosition {
396 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
397 write!(formatter, "{}:{}", self.line, self.column)
398 }
399}
400impl DocumentEntry {
401 pub(crate) fn markdown(path: &str, value: String, span: Range<usize>) -> Self {
402 DocumentEntry {
403 path: DocumentPath::parse(path),
404 value: Some(value),
405 span: DocumentSpan(span),
406 }
407 }
408}
409impl SourceDocument {
410 fn frontmatter(&self) -> Option<String> {
411 self.is_markdown().then(|| frontmatter_and_body(&self.content).0).flatten()
412 }
413 fn semantic_entries(&self) -> Vec<SemanticEntry> {
414 let yaml = self.is_yaml().then(|| self.content.clone()).or_else(|| self.frontmatter());
415 yaml.and_then(|content| serde_norway::from_str::<Value>(&content).ok())
416 .map(|value| DocumentPath::default().semantic_entries(&value))
417 .unwrap_or_default()
418 }
419 fn json_entries(&self) -> Vec<DocumentEntry> {
420 let mime = MimeType::from(self.source.as_str());
421 if mime.is_json() || mime.is_jsonc() {
422 {
423 parse_to_ast(&self.content, &CollectOptions::default(), &ParseOptions::default())
424 .ok()
425 .and_then(|result| result.value)
426 .map(|value| DocumentPath::default().collect_entries(&value))
427 .unwrap_or_default()
428 }
429 } else {
430 Default::default()
431 }
432 }
433 fn is_physical_text(&self) -> bool {
434 let mime = MimeType::from(self.format.as_str());
435 !(mime.is_doc()
436 || mime.is_docx()
437 || mime.is_epub()
438 || mime.is_odp()
439 || mime.is_ods()
440 || mime.is_odt()
441 || mime.is_pdf()
442 || mime.is_ppt()
443 || mime.is_powerpoint()
444 || mime.is_rtf())
445 }
446}
447fn field_names_match(left: &str, right: &str) -> bool {
448 normalized_bytes(left).eq(normalized_bytes(right))
449}
450fn normalized_bytes(value: &str) -> impl Iterator<Item = u8> + '_ {
451 value
452 .bytes()
453 .filter(|byte| !matches!(byte, b'_' | b'-'))
454 .map(|byte| byte.to_ascii_lowercase())
455}
456fn prefix_boundary(content: &str, candidate: usize, span_start: usize) -> usize {
457 let candidate = (0..=candidate.min(content.len()))
458 .rev()
459 .find(|index| content.is_char_boundary(*index))
460 .unwrap_or_default();
461 content
462 .get(candidate..span_start)
463 .and_then(|prefix| {
464 prefix
465 .char_indices()
466 .find(|(_, character)| character.is_whitespace())
467 .map(|(index, character)| candidate.saturating_add(index).saturating_add(character.len_utf8()))
468 })
469 .unwrap_or(candidate)
470}
471fn resolve_candidate<'a, T>(
472 candidates: &'a [T],
473 query: &DocumentQuery,
474 path: impl Copy + Fn(&T) -> &DocumentPath,
475 value: impl Copy + Fn(&T) -> Option<&str>,
476 additional_constraint: impl Copy + Fn(&T) -> bool,
477) -> UniqueMatch<&'a T> {
478 let matches = |candidate: &T| query.matches(value(candidate)) && additional_constraint(candidate);
479 let exact = unique_candidate(query.paths.iter().flat_map(|expected| {
480 candidates
481 .iter()
482 .filter(move |candidate| path(candidate) == expected && matches(candidate))
483 }));
484 match exact {
485 | UniqueMatch::Missing => {
486 let matching_values = candidates.iter().filter(|candidate| matches(candidate)).count();
487 let scored = |mechanical_only: bool| {
488 query.paths.iter().flat_map(move |expected| {
489 candidates.iter().filter_map(move |candidate| {
490 matches(candidate)
491 .then(|| expected.confidence(path(candidate)))
492 .flatten()
493 .filter(|(_, mechanical, anchored)| {
494 let convention_matches = !mechanical_only || *mechanical;
495 let anchor_matches = *anchored || matching_values == 1;
496 convention_matches && anchor_matches
497 })
498 .map(|(confidence, _, _)| (confidence, candidate))
499 })
500 })
501 };
502 match unique_max(scored(true)) {
503 | UniqueMatch::Missing => unique_max(scored(false)),
504 | result => result,
505 }
506 }
507 | result => result,
508 }
509}
510fn unique_candidate<T>(mut candidates: impl Iterator<Item = T>) -> UniqueMatch<T> {
511 match (candidates.next(), candidates.next()) {
512 | (None, _) => UniqueMatch::Missing,
513 | (Some(candidate), None) => UniqueMatch::Unique(candidate),
514 | (Some(_), Some(_)) => UniqueMatch::Ambiguous,
515 }
516}
517fn scalar(value: &Value) -> String {
518 match value {
519 | Value::String(value) => value.clone(),
520 | Value::Null => "null".to_string(),
521 | _ => value.to_string(),
522 }
523}
524fn narrow_span(content: &str, span: &DocumentSpan, needle: Option<&str>) -> DocumentSpan {
525 needle
526 .and_then(|needle| {
527 content
528 .get(span.0.clone())
529 .and_then(|value| value.find(needle))
530 .map(|offset| (offset, needle.len()))
531 })
532 .map_or_else(
533 || span.clone(),
534 |(offset, length)| DocumentSpan(span.0.start.saturating_add(offset)..span.0.start.saturating_add(offset).saturating_add(length)),
535 )
536}
537fn unique_span(spans: Vec<DocumentSpan>) -> DocumentMatch {
538 match spans.as_slice() {
539 | [] => DocumentMatch::Missing,
540 | [span] => DocumentMatch::Unique(span.clone()),
541 | _ => DocumentMatch::Ambiguous,
542 }
543}