1use std::{
4 error::Error,
5 ffi::OsStr,
6 fmt, fs,
7 io::{self, Read},
8 path::{Path, PathBuf},
9 sync::OnceLock,
10};
11
12use mant_ir::{Document, DocumentAddress, MarkdownOrigin, ResolvedContent, TldrDocument};
13use mant_protocol::{
14 CatalogQuery, DocumentCatalog, EntryProjection, InputFormat, MAX_DOCUMENT_SELECTOR_CHARS,
15 MAX_NODE_SELECTORS, MAX_SEMANTIC_ENTRY_CHARS, MAX_SOURCE_SELECTOR_CHARS, QueryExcerpt,
16 QueryInput, QueryOutline, QueryRequest, QuerySearch, QueryView, ScopeTextError, SearchCase,
17 SearchQuery, SearchScope, SearchSyntax, validate_scope_text,
18};
19use mant_sources::{RegisteredDocumentIndex, RegisteredDocumentOrigin, SourceConfigError};
20
21use crate::{
22 ManualIndex, ManualPage, ManualRequest, ProjectionError, SearchError, discover_manual_roots,
23 executable::query_name_candidates, locate_manual_source_in, parse_manual_bytes,
24 parse_manual_page, parse_manual_source, parse_markdown, read_cached_tldr_page, search_query,
25 select_excerpt, select_explanation, validate_search_query,
26};
27
28mod input;
29mod named;
30
31use input::query_with;
32pub use input::{query_markdown_text, query_roff_bytes};
33use named::query_named_document;
34
35pub const MAX_MARKDOWN_BYTES: u64 = 16 * 1024 * 1024;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum QueryError {
46 EmptyName,
48 InvalidManualSection,
50 TldrManualSection {
52 section: String,
54 },
55 InvalidSource,
57 ConflictingSourceSelectors,
59 EmptyMarkdownPath,
61 UnsupportedInputFormat {
63 path: String,
65 },
66 EmptySelection,
68 TooManySelections {
70 maximum: usize,
72 },
73 EmptySelector,
75 InvalidEntryKinds,
77 EmptyEntry,
79 InvalidViewSelector {
81 field: &'static str,
83 error: ScopeTextError,
85 },
86 InvalidSearch(SearchError),
88 Markdown {
90 path: String,
92 detail: String,
94 },
95 EmptyMarkdown {
97 label: String,
99 },
100 Registry {
102 detail: String,
104 },
105 Manual(ManualLoadError),
107 ManualWithTldr {
109 error: ManualLoadError,
111 topic: String,
113 },
114 TldrNotFound {
116 topic: String,
118 },
119 Tldr {
121 topic: String,
123 detail: String,
125 },
126 NoReadableContent {
128 name: String,
130 },
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum ManualLoadError {
136 NotFound {
138 name: String,
140 detail: String,
142 },
143 Parse {
145 name: String,
147 detail: String,
149 },
150 Empty {
152 name: String,
154 path: PathBuf,
156 diagnostics: Vec<String>,
158 },
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum QueryViewResult {
164 Full(Box<ResolvedContent>),
166 Outline(QueryOutline),
168 Excerpt(QueryExcerpt),
170 Search(QuerySearch),
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum QueryExecutionError {
177 Query(QueryError),
179 Projection(ProjectionError),
181 Search(SearchError),
183}
184
185#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
187pub enum QueryPolicy {
188 #[default]
190 Combined,
191 ManualOnly,
193 TldrOnly,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198enum FullDocumentMode {
199 Priority,
200 NativeManual,
201 None,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205enum QuickReferenceMode {
206 AttachToCommandManual,
207 Exclude,
208 Only,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212struct NamedResolutionPlan {
213 document: FullDocumentMode,
214 quick_reference: QuickReferenceMode,
215}
216
217impl QueryPolicy {
218 fn named_resolution_plan(self, has_manual_section: bool) -> NamedResolutionPlan {
219 match self {
220 Self::Combined => NamedResolutionPlan {
221 document: if has_manual_section {
222 FullDocumentMode::NativeManual
223 } else {
224 FullDocumentMode::Priority
225 },
226 quick_reference: QuickReferenceMode::AttachToCommandManual,
227 },
228 Self::ManualOnly => NamedResolutionPlan {
229 document: FullDocumentMode::NativeManual,
230 quick_reference: QuickReferenceMode::Exclude,
231 },
232 Self::TldrOnly => NamedResolutionPlan {
233 document: FullDocumentMode::None,
234 quick_reference: QuickReferenceMode::Only,
235 },
236 }
237 }
238}
239
240impl fmt::Display for QueryError {
241 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 Self::EmptyName => formatter.write_str("name must not be empty"),
244 Self::InvalidManualSection => formatter.write_str(
245 "manual section must be a conventional number or the single letter 'l' or 'n'",
246 ),
247 Self::TldrManualSection { section } => write!(
248 formatter,
249 "manual section '{section}' does not identify a command quick reference; tldr supports section families 1 and 8"
250 ),
251 Self::InvalidSource => formatter.write_str("document source must not be empty"),
252 Self::ConflictingSourceSelectors => formatter.write_str(
253 "document source cannot be combined with a manual section or manual-only policy",
254 ),
255 Self::EmptyMarkdownPath => formatter.write_str("Markdown path must not be empty"),
256 Self::UnsupportedInputFormat { path } => write!(
257 formatter,
258 "could not infer the input format for '{path}'; use --input-format markdown or roff"
259 ),
260 Self::EmptySelection => formatter.write_str("at least one outline node is required"),
261 Self::TooManySelections { maximum } => {
262 write!(
263 formatter,
264 "outline nodes must not contain more than {maximum} values"
265 )
266 }
267 Self::EmptySelector => formatter.write_str("outline node must not be empty"),
268 Self::InvalidEntryKinds => {
269 formatter.write_str("outline entry kinds must contain between 1 and 9 values")
270 }
271 Self::EmptyEntry => formatter.write_str("semantic entry must not be empty"),
272 Self::InvalidViewSelector { field, error } => {
273 write!(formatter, "{field} {}", view_selector_error_message(*error))
274 }
275 Self::InvalidSearch(error) => error.fmt(formatter),
276 Self::Markdown { path, detail } => {
277 write!(
278 formatter,
279 "could not load Markdown document '{path}': {detail}"
280 )
281 }
282 Self::EmptyMarkdown { label } => {
283 write!(
284 formatter,
285 "Markdown document '{label}' has no readable content"
286 )
287 }
288 Self::Registry { detail } => formatter.write_str(detail),
289 Self::Manual(error) => error.fmt(formatter),
290 Self::ManualWithTldr { error, topic } => {
291 error.fmt(formatter)?;
292 write!(
293 formatter,
294 "\nhint: a tldr entry is available; run `mant {topic} --tldr`"
295 )
296 }
297 Self::TldrNotFound { topic } => {
298 write!(formatter, "no tldr quick reference was found for '{topic}'")
299 }
300 Self::Tldr { topic, detail } => {
301 write!(formatter, "could not load tldr entry '{topic}': {detail}")
302 }
303 Self::NoReadableContent { name } => {
304 write!(
305 formatter,
306 "no readable document content was found for '{name}'"
307 )
308 }
309 }
310 }
311}
312
313impl Error for QueryError {
314 fn source(&self) -> Option<&(dyn Error + 'static)> {
315 match self {
316 Self::InvalidSearch(error) => Some(error),
317 Self::Manual(error) | Self::ManualWithTldr { error, .. } => Some(error),
318 Self::EmptyName
319 | Self::InvalidManualSection
320 | Self::TldrManualSection { .. }
321 | Self::InvalidSource
322 | Self::ConflictingSourceSelectors
323 | Self::EmptyMarkdownPath
324 | Self::UnsupportedInputFormat { .. }
325 | Self::EmptySelection
326 | Self::TooManySelections { .. }
327 | Self::EmptySelector
328 | Self::InvalidEntryKinds
329 | Self::EmptyEntry
330 | Self::InvalidViewSelector { .. }
331 | Self::Markdown { .. }
332 | Self::EmptyMarkdown { .. }
333 | Self::Registry { .. }
334 | Self::TldrNotFound { .. }
335 | Self::Tldr { .. }
336 | Self::NoReadableContent { .. } => None,
337 }
338 }
339}
340
341fn view_selector_error_message(error: ScopeTextError) -> String {
342 match error {
343 ScopeTextError::Empty => "must not be empty".to_owned(),
344 ScopeTextError::ControlCharacter => "must not contain control characters".to_owned(),
345 ScopeTextError::TooLong { maximum } => {
346 format!("must not exceed {maximum} Unicode scalar values")
347 }
348 }
349}
350
351impl fmt::Display for ManualLoadError {
352 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
353 match self {
354 Self::NotFound { name, detail } => {
355 write!(formatter, "could not load manual '{name}': {detail}")
356 }
357 Self::Parse { name, detail } => write!(
358 formatter,
359 "could not load manual '{name}': manual source: {detail}"
360 ),
361 Self::Empty {
362 name,
363 path,
364 diagnostics,
365 } => {
366 write!(
367 formatter,
368 "could not load manual '{name}': libmandoc parsed {} but produced no readable sections",
369 path.display()
370 )?;
371 if !diagnostics.is_empty() {
372 write!(formatter, "; diagnostics: {}", diagnostics.join("; "))?;
373 }
374 Ok(())
375 }
376 }
377 }
378}
379
380impl Error for ManualLoadError {}
381
382impl fmt::Display for QueryExecutionError {
383 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
384 match self {
385 Self::Query(error) => error.fmt(formatter),
386 Self::Projection(error) => error.fmt(formatter),
387 Self::Search(error) => error.fmt(formatter),
388 }
389 }
390}
391
392impl Error for QueryExecutionError {
393 fn source(&self) -> Option<&(dyn Error + 'static)> {
394 match self {
395 Self::Query(error) => Some(error),
396 Self::Projection(error) => Some(error),
397 Self::Search(error) => Some(error),
398 }
399 }
400}
401
402pub fn resolve_query(request: &QueryRequest) -> Result<ResolvedContent, QueryError> {
409 resolve_query_with_policy(request, QueryPolicy::default())
410}
411
412pub fn resolve_query_with_policy(
418 request: &QueryRequest,
419 policy: QueryPolicy,
420) -> Result<ResolvedContent, QueryError> {
421 let resolver = DocumentResolver::from_system();
422 resolver.resolve(request, policy)
423}
424
425pub fn execute_query(
431 request: &QueryRequest,
432 policy: QueryPolicy,
433) -> Result<QueryViewResult, QueryExecutionError> {
434 let resolver = DocumentResolver::from_system();
435 resolver.execute(request, policy)
436}
437
438pub fn project_query_view(
444 query: ResolvedContent,
445 view: &QueryView,
446) -> Result<QueryViewResult, QueryExecutionError> {
447 validate_query_view(view).map_err(QueryExecutionError::Query)?;
448 match view {
449 QueryView::Full {} => Ok(QueryViewResult::Full(Box::new(query))),
450 QueryView::Outline { entries, root } => {
451 { crate::projection::build_outline_projection(&query, entries.clone(), root.clone()) }
452 .map(QueryViewResult::Outline)
453 .map_err(QueryExecutionError::Projection)
454 }
455 QueryView::Excerpt { selectors } => select_excerpt(&query, selectors)
456 .map(QueryViewResult::Excerpt)
457 .map_err(QueryExecutionError::Projection),
458 QueryView::Explain { entry } => select_explanation_with_text_hint(&query, entry)
459 .map(QueryViewResult::Excerpt)
460 .map_err(QueryExecutionError::Projection),
461 QueryView::Search {
462 pattern,
463 syntax,
464 case,
465 scope,
466 word,
467 context_lines,
468 limit,
469 offset,
470 } => search_query(
471 &query,
472 &SearchQuery {
473 pattern: pattern.clone(),
474 syntax: *syntax,
475 case: *case,
476 scope: *scope,
477 word: *word,
478 context_lines: *context_lines,
479 limit: *limit,
480 offset: *offset,
481 },
482 )
483 .map(QueryViewResult::Search)
484 .map_err(QueryExecutionError::Search),
485 }
486}
487
488pub(crate) fn select_explanation_with_text_hint(
489 query: &ResolvedContent,
490 entry: &str,
491) -> Result<QueryExcerpt, ProjectionError> {
492 match select_explanation(query, entry) {
493 Err(ProjectionError::UnknownSelector { document, selector }) => {
494 let probe = SearchQuery {
495 pattern: selector.clone(),
496 syntax: SearchSyntax::Literal,
497 case: SearchCase::Insensitive,
498 scope: SearchScope::Visible,
499 word: false,
500 context_lines: 0,
501 limit: 1,
502 offset: 0,
503 };
504 if let Some(found) = search_query(query, &probe)
505 .ok()
506 .and_then(|result| result.matches.into_iter().next())
507 {
508 let line = found
509 .occurrences
510 .first()
511 .map_or(1, |occurrence| occurrence.markdown.start_line);
512 return Err(ProjectionError::SelectorFoundOnlyInText {
513 document,
514 selector,
515 path: found.outline.path().to_owned(),
516 title: found.outline.title().to_owned(),
517 line,
518 });
519 }
520 Err(ProjectionError::UnknownSelector { document, selector })
521 }
522 result => result,
523 }
524}
525
526pub fn validate_query_request(
532 request: &QueryRequest,
533 policy: QueryPolicy,
534) -> Result<(), QueryError> {
535 match &request.input {
536 QueryInput::Document {
537 selector,
538 source,
539 manual_section,
540 } => {
541 validate_scope_text(selector, MAX_DOCUMENT_SELECTOR_CHARS).map_err(|error| {
542 if error == ScopeTextError::Empty {
543 QueryError::EmptyName
544 } else {
545 QueryError::InvalidViewSelector {
546 field: "document selector",
547 error,
548 }
549 }
550 })?;
551 if let Some(source) = source {
552 validate_scope_text(source, MAX_SOURCE_SELECTOR_CHARS).map_err(|error| {
553 if error == ScopeTextError::Empty {
554 QueryError::InvalidSource
555 } else {
556 QueryError::InvalidViewSelector {
557 field: "document source",
558 error,
559 }
560 }
561 })?;
562 }
563 if manual_section
564 .as_deref()
565 .is_some_and(|value| !crate::is_manual_section(value.trim()))
566 {
567 return Err(QueryError::InvalidManualSection);
568 }
569 if policy == QueryPolicy::TldrOnly
570 && let Some(section) = manual_section.as_deref()
571 && !crate::is_command_manual_section(section.trim())
572 {
573 return Err(QueryError::TldrManualSection {
574 section: section.trim().to_owned(),
575 });
576 }
577 if source.is_some() && (manual_section.is_some() || policy == QueryPolicy::ManualOnly) {
578 return Err(QueryError::ConflictingSourceSelectors);
579 }
580 }
581 QueryInput::File { path, .. } => {
582 if path.trim().is_empty() {
583 return Err(QueryError::EmptyMarkdownPath);
584 }
585 if policy != QueryPolicy::Combined {
586 return Err(QueryError::Markdown {
587 path: path.trim().to_owned(),
588 detail: "content-only policies do not apply to direct input".to_owned(),
589 });
590 }
591 }
592 }
593 validate_query_view(&request.view)
594}
595
596fn validate_query_view(view: &QueryView) -> Result<(), QueryError> {
597 match view {
598 QueryView::Excerpt { selectors } => {
599 if selectors.is_empty() {
600 return Err(QueryError::EmptySelection);
601 }
602 if selectors.len() > MAX_NODE_SELECTORS {
603 return Err(QueryError::TooManySelections {
604 maximum: MAX_NODE_SELECTORS,
605 });
606 }
607 for selector in selectors {
608 validate_scope_text(selector, MAX_SEMANTIC_ENTRY_CHARS).map_err(|error| {
609 if error == ScopeTextError::Empty {
610 QueryError::EmptySelector
611 } else {
612 QueryError::InvalidViewSelector {
613 field: "outline node",
614 error,
615 }
616 }
617 })?;
618 }
619 }
620 QueryView::Explain { entry } => {
621 validate_scope_text(entry, MAX_SEMANTIC_ENTRY_CHARS).map_err(|error| {
622 if error == ScopeTextError::Empty {
623 QueryError::EmptyEntry
624 } else {
625 QueryError::InvalidViewSelector {
626 field: "semantic entry",
627 error,
628 }
629 }
630 })?;
631 }
632 QueryView::Search {
633 pattern,
634 syntax,
635 case,
636 scope,
637 word,
638 context_lines,
639 limit,
640 offset,
641 } => validate_search_query(&SearchQuery {
642 pattern: pattern.clone(),
643 syntax: *syntax,
644 case: *case,
645 scope: *scope,
646 word: *word,
647 context_lines: *context_lines,
648 limit: *limit,
649 offset: *offset,
650 })
651 .map_err(QueryError::InvalidSearch)?,
652 QueryView::Outline { entries, root } => {
653 if let Some(selector) = root {
654 validate_scope_text(selector, MAX_SEMANTIC_ENTRY_CHARS).map_err(|error| {
655 if error == ScopeTextError::Empty {
656 QueryError::EmptySelector
657 } else {
658 QueryError::InvalidViewSelector {
659 field: "outline root",
660 error,
661 }
662 }
663 })?;
664 }
665 if let EntryProjection::Kinds { kinds } = entries
666 && (kinds.is_empty() || kinds.len() > 9)
667 {
668 return Err(QueryError::InvalidEntryKinds);
669 }
670 }
671 QueryView::Full {} => {}
672 }
673 Ok(())
674}
675
676trait QueryHost {
677 fn name_candidates(&self, name: &str) -> Vec<String>;
678 fn locate_registered_document(
679 &self,
680 candidates: &[String],
681 source: Option<&str>,
682 phase: RegisteredLookupPhase,
683 ) -> Result<Option<RegisteredSelection>, String>;
684 fn locate_registered_document_groups(
685 &self,
686 candidates: &[String],
687 source: Option<&str>,
688 phase: RegisteredLookupPhase,
689 ) -> Result<Vec<RegisteredSelectionGroup>, String>;
690 fn locate_registered_address(
691 &self,
692 address: &DocumentAddress,
693 ) -> Result<Option<RegisteredSelection>, String>;
694 fn locate_manual(&self, request: &ManualRequest) -> Result<ManualPage, String>;
695 fn parse_manual(&self, page: &ManualPage) -> Result<Document, String>;
696 fn parse_manual_input(&self, path: &Path) -> Result<Document, String>;
697 fn read_tldr(&self, name: &str) -> Result<Option<TldrDocument>, String>;
698 fn read_markdown(&self, path: &Path) -> Result<String, String>;
699}
700
701#[derive(Clone, Copy)]
702enum RegisteredLookupPhase {
703 BeforeBuiltin,
704 AfterBuiltin,
705}
706
707#[derive(Clone)]
708struct RegisteredSelection {
709 path: PathBuf,
710 address: DocumentAddress,
711}
712
713struct RegisteredSelectionGroup {
714 documents: Vec<RegisteredSelection>,
715}
716
717fn registered_selection(document: &mant_sources::RegisteredDocument) -> RegisteredSelection {
718 RegisteredSelection {
719 path: document.path.clone(),
720 address: DocumentAddress::Markdown {
721 path: document.logical_path.clone(),
722 origin: match &document.origin {
723 RegisteredDocumentOrigin::Documents => MarkdownOrigin::Documents,
724 RegisteredDocumentOrigin::Source(name) => {
725 MarkdownOrigin::Source { name: name.clone() }
726 }
727 },
728 },
729 }
730}
731
732struct LoadedManual {
733 document: Document,
734 address: DocumentAddress,
735}
736
737pub struct DocumentResolver {
739 registered: OnceLock<Result<RegisteredDocumentIndex, SourceConfigError>>,
740 manual_roots: Vec<PathBuf>,
741 manuals: OnceLock<ManualIndex>,
742 available: OnceLock<Vec<crate::catalog::AvailableDocument>>,
743}
744
745impl DocumentResolver {
746 #[must_use]
749 pub fn from_system() -> Self {
750 Self {
751 registered: OnceLock::new(),
752 manual_roots: discover_manual_roots(),
753 manuals: OnceLock::new(),
754 available: OnceLock::new(),
755 }
756 }
757
758 pub fn resolve(
768 &self,
769 request: &QueryRequest,
770 policy: QueryPolicy,
771 ) -> Result<ResolvedContent, QueryError> {
772 validate_query_request(request, policy)?;
773 query_with(request, policy, self)
774 }
775
776 pub fn execute(
782 &self,
783 request: &QueryRequest,
784 policy: QueryPolicy,
785 ) -> Result<QueryViewResult, QueryExecutionError> {
786 let query = self
787 .resolve(request, policy)
788 .map_err(QueryExecutionError::Query)?;
789 project_query_view(query, &request.view)
790 }
791
792 pub fn discover(&self, query: &CatalogQuery) -> Result<DocumentCatalog, String> {
800 let registered = self
801 .registered
802 .get_or_init(RegisteredDocumentIndex::load)
803 .as_ref()
804 .map_err(ToString::to_string)?;
805 let manuals = self
806 .manuals
807 .get_or_init(|| ManualIndex::from_roots(self.manual_roots.clone()));
808 let documents = self.available.get_or_init(|| {
809 crate::catalog::list_available_documents_from(
810 registered.documents().to_vec(),
811 manuals.pages(),
812 )
813 });
814 crate::catalog::query_available_documents(documents, query)
815 .map_err(|error| error.to_string())
816 }
817}
818
819impl QueryHost for DocumentResolver {
820 fn name_candidates(&self, name: &str) -> Vec<String> {
821 query_name_candidates(name)
822 }
823
824 fn locate_registered_document(
825 &self,
826 candidates: &[String],
827 source: Option<&str>,
828 phase: RegisteredLookupPhase,
829 ) -> Result<Option<RegisteredSelection>, String> {
830 let index = self
831 .registered
832 .get_or_init(RegisteredDocumentIndex::load)
833 .as_ref()
834 .map_err(ToString::to_string)?;
835 let selected = if source.is_some() {
836 index.find(candidates, source)
837 } else {
838 match phase {
839 RegisteredLookupPhase::BeforeBuiltin => index.find_before_builtin(candidates),
840 RegisteredLookupPhase::AfterBuiltin => index.find_after_builtin(candidates),
841 }
842 };
843 selected
844 .map(|registered| registered.map(registered_selection))
845 .map_err(|error| error.to_string())
846 }
847
848 fn locate_registered_document_groups(
849 &self,
850 candidates: &[String],
851 source: Option<&str>,
852 phase: RegisteredLookupPhase,
853 ) -> Result<Vec<RegisteredSelectionGroup>, String> {
854 let index = self
855 .registered
856 .get_or_init(RegisteredDocumentIndex::load)
857 .as_ref()
858 .map_err(ToString::to_string)?;
859 let groups = if let Some(source) = source {
860 index.matches_in_source(candidates, source)
861 } else {
862 Ok(match phase {
863 RegisteredLookupPhase::BeforeBuiltin => index.matches_before_builtin(candidates),
864 RegisteredLookupPhase::AfterBuiltin => index.matches_after_builtin(candidates),
865 })
866 }
867 .map_err(|error| error.to_string())?;
868 Ok(groups
869 .into_iter()
870 .map(|group| RegisteredSelectionGroup {
871 documents: group.documents.iter().map(registered_selection).collect(),
872 })
873 .collect())
874 }
875
876 fn locate_registered_address(
877 &self,
878 address: &DocumentAddress,
879 ) -> Result<Option<RegisteredSelection>, String> {
880 let DocumentAddress::Markdown { path, origin } = address else {
881 return Ok(None);
882 };
883 let origin = match origin {
884 MarkdownOrigin::Documents => RegisteredDocumentOrigin::Documents,
885 MarkdownOrigin::Source { name } => RegisteredDocumentOrigin::Source(name.clone()),
886 };
887 let index = self
888 .registered
889 .get_or_init(RegisteredDocumentIndex::load)
890 .as_ref()
891 .map_err(ToString::to_string)?;
892 index
893 .find_address(path, &origin)
894 .map(|document| {
895 document.map(|document| RegisteredSelection {
896 path: document.path.clone(),
897 address: address.clone(),
898 })
899 })
900 .map_err(|error| error.to_string())
901 }
902
903 fn locate_manual(&self, request: &ManualRequest) -> Result<ManualPage, String> {
904 let manuals = self
905 .manuals
906 .get_or_init(|| ManualIndex::from_roots(self.manual_roots.clone()));
907 locate_manual_source_in(request, manuals).map_err(|error| error.load_detail())
908 }
909
910 fn parse_manual(&self, page: &ManualPage) -> Result<Document, String> {
911 parse_manual_page(page).map_err(|error| error.to_string())
912 }
913
914 fn parse_manual_input(&self, path: &Path) -> Result<Document, String> {
915 parse_manual_source(path).map_err(|error| error.to_string())
916 }
917
918 fn read_tldr(&self, name: &str) -> Result<Option<TldrDocument>, String> {
919 read_cached_tldr_page(name).map_err(|error| error.to_string())
920 }
921
922 fn read_markdown(&self, path: &Path) -> Result<String, String> {
923 let file = fs::File::open(path).map_err(|error| error.to_string())?;
924 read_capped_utf8(file, MAX_MARKDOWN_BYTES)
925 }
926}
927
928fn read_capped_utf8(reader: impl Read, limit: u64) -> Result<String, String> {
934 read_capped_utf8_io(reader, limit).map_err(|error| error.to_string())
935}
936
937pub(crate) fn read_capped_utf8_io(reader: impl Read, limit: u64) -> io::Result<String> {
939 crate::bounded::read_utf8(reader, limit, "Markdown document")
940}
941
942#[cfg(test)]
943mod tests;