1use super::LoadError;
3use crate::{ManualPage, ManualRequest};
4use mant_ir::{Document, DocumentAddress, TldrDocument};
5use mant_protocol::{
6 InputFormat, MAX_DOCUMENT_SELECTOR_CHARS, MAX_SOURCE_SELECTOR_CHARS, ScopeTextError,
7 validate_scope_text,
8};
9use std::{
10 io::{self, Read},
11 path::{Path, PathBuf},
12};
13
14#[derive(Debug, Clone, Copy)]
16pub enum LoadSpec<'a> {
17 Document {
19 selector: &'a str,
21 source: Option<&'a str>,
23 manual_section: Option<&'a str>,
25 },
26 File {
28 path: &'a str,
30 format: InputFormat,
32 },
33}
34
35pub const MAX_MARKDOWN_BYTES: u64 = 16 * 1024 * 1024;
42
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
45pub enum LoadPolicy {
46 #[default]
48 Combined,
49 ManualOnly,
51 TldrOnly,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub(super) enum FullDocumentMode {
57 Priority,
58 NativeManual,
59 None,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub(super) enum QuickReferenceMode {
64 AttachToCommandManual,
65 Exclude,
66 Only,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub(super) struct NamedResolutionPlan {
71 pub(super) document: FullDocumentMode,
72 pub(super) quick_reference: QuickReferenceMode,
73}
74
75impl LoadPolicy {
76 pub(super) fn named_resolution_plan(self, has_manual_section: bool) -> NamedResolutionPlan {
77 match self {
78 Self::Combined => NamedResolutionPlan {
79 document: if has_manual_section {
80 FullDocumentMode::NativeManual
81 } else {
82 FullDocumentMode::Priority
83 },
84 quick_reference: QuickReferenceMode::AttachToCommandManual,
85 },
86 Self::ManualOnly => NamedResolutionPlan {
87 document: FullDocumentMode::NativeManual,
88 quick_reference: QuickReferenceMode::Exclude,
89 },
90 Self::TldrOnly => NamedResolutionPlan {
91 document: FullDocumentMode::None,
92 quick_reference: QuickReferenceMode::Only,
93 },
94 }
95 }
96}
97
98pub(super) trait LoadHost {
99 fn native_available(&self) -> bool {
100 true
101 }
102 fn name_candidates(&self, name: &str) -> Vec<String>;
103 fn locate_registered_document(
104 &self,
105 candidates: &[String],
106 source: Option<&str>,
107 phase: RegisteredLookupPhase,
108 ) -> Result<Option<RegisteredSelection>, String>;
109 fn locate_registered_document_groups(
110 &self,
111 candidates: &[String],
112 source: Option<&str>,
113 phase: RegisteredLookupPhase,
114 ) -> Result<Vec<RegisteredSelectionGroup>, String>;
115 fn locate_registered_address(
116 &self,
117 address: &DocumentAddress,
118 ) -> Result<Option<RegisteredSelection>, String>;
119 fn locate_manual(&self, request: &ManualRequest) -> Result<ManualPage, String>;
120 fn parse_manual(&self, page: &ManualPage) -> Result<Document, String>;
121 fn parse_manual_input(&self, path: &Path) -> Result<Document, String>;
122 fn read_tldr(&self, name: &str) -> Result<Option<TldrDocument>, String>;
123 fn read_markdown(&self, path: &Path) -> Result<String, String>;
124}
125
126#[derive(Clone, Copy)]
127pub(super) enum RegisteredLookupPhase {
128 BeforeBuiltin,
129 AfterBuiltin,
130}
131
132#[derive(Clone)]
133pub(super) struct RegisteredSelection {
134 pub(super) path: PathBuf,
135 pub(super) address: DocumentAddress,
136}
137
138pub(super) struct RegisteredSelectionGroup {
139 pub(super) documents: Vec<RegisteredSelection>,
140}
141
142pub(super) struct LoadedManual {
143 pub(super) document: Document,
144 pub(super) address: DocumentAddress,
145}
146
147pub(super) fn read_capped_utf8(reader: impl Read, limit: u64) -> Result<String, String> {
153 read_capped_utf8_io(reader, limit).map_err(|error| error.to_string())
154}
155
156pub(crate) fn read_capped_utf8_io(reader: impl Read, limit: u64) -> io::Result<String> {
158 crate::bounded::read_utf8(reader, limit, "Markdown document")
159}
160
161pub fn validate_load_spec(spec: LoadSpec<'_>, policy: LoadPolicy) -> Result<(), LoadError> {
166 match spec {
167 LoadSpec::Document {
168 selector,
169 source,
170 manual_section,
171 } => {
172 validate_scope_text(selector, MAX_DOCUMENT_SELECTOR_CHARS).map_err(|error| {
173 if error == ScopeTextError::Empty {
174 LoadError::EmptyName
175 } else {
176 LoadError::InvalidSelector {
177 field: "document selector",
178 error,
179 }
180 }
181 })?;
182 if let Some(source) = source {
183 validate_scope_text(source, MAX_SOURCE_SELECTOR_CHARS).map_err(|error| {
184 if error == ScopeTextError::Empty {
185 LoadError::InvalidSource
186 } else {
187 LoadError::InvalidSelector {
188 field: "document source",
189 error,
190 }
191 }
192 })?;
193 }
194 if manual_section.is_some_and(|value| !crate::is_manual_section(value.trim())) {
195 return Err(LoadError::InvalidManualSection);
196 }
197 if policy == LoadPolicy::TldrOnly
198 && let Some(section) = manual_section
199 && !crate::is_command_manual_section(section.trim())
200 {
201 return Err(LoadError::TldrManualSection {
202 section: section.trim().to_owned(),
203 });
204 }
205 if source.is_some() && (manual_section.is_some() || policy == LoadPolicy::ManualOnly) {
206 return Err(LoadError::ConflictingSourceSelectors);
207 }
208 }
209 LoadSpec::File { path, .. } => {
210 if path.trim().is_empty() {
211 return Err(LoadError::EmptyMarkdownPath);
212 }
213 if policy != LoadPolicy::Combined {
214 return Err(LoadError::Markdown {
215 path: path.trim().to_owned(),
216 detail: "content-only policies do not apply to direct input".to_owned(),
217 });
218 }
219 }
220 }
221 Ok(())
222}