1use std::{
4 error::Error,
5 ffi::OsStr,
6 fmt, fs,
7 io::Read,
8 path::{Path, PathBuf},
9};
10
11use mant_ast::{MantDocument, QueryBundle, QueryInput, QueryRequest, QuerySchema, TldrDocument};
12
13use crate::{ManualRequest, find_registered_document, parse_markdown, read_cached_tldr_page};
14
15#[cfg(unix)]
16use crate::{locate_manual_source, parse_manual_source};
17
18pub const MAX_MARKDOWN_BYTES: u64 = 16 * 1024 * 1024;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum QueryError {
29 EmptyName,
30 InvalidSection,
31 EmptyMarkdownPath,
32 Markdown { path: String, detail: String },
33 EmptyMarkdown { label: String },
34 Manual { name: String, detail: String },
35 NoReadableContent { name: String },
36}
37
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
40pub struct QueryPolicy {
41 pub manual_only: bool,
43}
44
45impl fmt::Display for QueryError {
46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 Self::EmptyName => formatter.write_str("name must not be empty"),
49 Self::InvalidSection => formatter.write_str("manual section must not be empty"),
50 Self::EmptyMarkdownPath => formatter.write_str("Markdown path must not be empty"),
51 Self::Markdown { path, detail } => {
52 write!(
53 formatter,
54 "could not load Markdown document '{path}': {detail}"
55 )
56 }
57 Self::EmptyMarkdown { label } => {
58 write!(
59 formatter,
60 "Markdown document '{label}' has no readable content"
61 )
62 }
63 Self::Manual { detail, .. } => formatter.write_str(detail),
64 Self::NoReadableContent { name } => {
65 write!(
66 formatter,
67 "no readable document content was found for '{name}'"
68 )
69 }
70 }
71 }
72}
73
74impl Error for QueryError {}
75
76pub fn query(request: &QueryRequest) -> Result<QueryBundle, QueryError> {
83 query_with(request, QueryPolicy::default(), &SystemQueryHost)
84}
85
86pub fn query_with_policy(
92 request: &QueryRequest,
93 policy: QueryPolicy,
94) -> Result<QueryBundle, QueryError> {
95 query_with(request, policy, &SystemQueryHost)
96}
97
98trait QueryHost {
99 fn locate_registered_document(&self, name: &str) -> Option<PathBuf>;
100 fn locate_manual(&self, request: &ManualRequest) -> Result<PathBuf, String>;
101 fn parse_manual(&self, path: &Path) -> Result<MantDocument, String>;
102 fn read_tldr(&self, name: &str) -> Result<Option<TldrDocument>, String>;
103 fn read_markdown(&self, path: &Path) -> Result<String, String>;
104}
105
106struct SystemQueryHost;
107
108impl QueryHost for SystemQueryHost {
109 fn locate_registered_document(&self, name: &str) -> Option<PathBuf> {
110 find_registered_document(name).map(|registered| registered.path)
111 }
112
113 fn locate_manual(&self, request: &ManualRequest) -> Result<PathBuf, String> {
114 #[cfg(unix)]
115 {
116 locate_manual_source(request).map_err(|error| error.to_string())
117 }
118 #[cfg(not(unix))]
119 {
120 Err(format!(
121 "native manual pages are unavailable on this platform; register a Markdown document named '{}'",
122 request.name
123 ))
124 }
125 }
126
127 fn parse_manual(&self, path: &Path) -> Result<MantDocument, String> {
128 #[cfg(unix)]
129 {
130 parse_manual_source(path).map_err(|error| error.to_string())
131 }
132 #[cfg(not(unix))]
133 {
134 Err(format!(
135 "native manual parsing is unavailable on this platform: {}",
136 path.display()
137 ))
138 }
139 }
140
141 fn read_tldr(&self, name: &str) -> Result<Option<TldrDocument>, String> {
142 read_cached_tldr_page(name).map_err(|error| error.to_string())
143 }
144
145 fn read_markdown(&self, path: &Path) -> Result<String, String> {
146 let file = fs::File::open(path).map_err(|error| error.to_string())?;
147 read_capped_utf8(file, MAX_MARKDOWN_BYTES)
148 }
149}
150
151fn read_capped_utf8(reader: impl Read, limit: u64) -> Result<String, String> {
157 let mut bytes = Vec::new();
158 reader
159 .take(limit + 1)
160 .read_to_end(&mut bytes)
161 .map_err(|error| error.to_string())?;
162 if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > limit {
163 return Err(format!("Markdown document exceeds the {limit}-byte limit"));
164 }
165 String::from_utf8(bytes).map_err(|_| "Markdown document must be UTF-8".to_owned())
166}
167
168fn query_with(
169 request: &QueryRequest,
170 policy: QueryPolicy,
171 host: &dyn QueryHost,
172) -> Result<QueryBundle, QueryError> {
173 match &request.input {
174 QueryInput::Document { name, section } => {
175 query_manual(name, section.as_deref(), policy, host)
176 }
177 QueryInput::MarkdownFile { path } => query_markdown_file(path, policy, host),
178 }
179}
180
181fn query_markdown_file(
182 requested_path: &str,
183 policy: QueryPolicy,
184 host: &dyn QueryHost,
185) -> Result<QueryBundle, QueryError> {
186 let path = requested_path.trim();
187 if path.is_empty() {
188 return Err(QueryError::EmptyMarkdownPath);
189 }
190 if policy.manual_only {
191 return Err(QueryError::Markdown {
192 path: path.to_owned(),
193 detail: "the manual-only policy does not apply to Markdown input".to_owned(),
194 });
195 }
196 let source = host
197 .read_markdown(Path::new(path))
198 .map_err(|detail| QueryError::Markdown {
199 path: path.to_owned(),
200 detail,
201 })?;
202 query_markdown_text(&source, Some(path.to_owned()))
203}
204
205pub fn query_markdown_text(
215 source: &str,
216 source_path: Option<String>,
217) -> Result<QueryBundle, QueryError> {
218 let label = source_path.as_deref().map_or_else(
219 || "stdin".to_owned(),
220 |path| {
221 Path::new(path)
222 .file_name()
223 .and_then(OsStr::to_str)
224 .unwrap_or(path)
225 .to_owned()
226 },
227 );
228 let error_path = source_path.clone().unwrap_or_else(|| "stdin".to_owned());
229 let parsed = parse_markdown(source, source_path).map_err(|error| QueryError::Markdown {
230 path: error_path,
231 detail: error.to_string(),
232 })?;
233 let document_is_empty =
234 parsed.document.blocks.is_empty() && parsed.document.sections.is_empty();
235 if document_is_empty && parsed.tldr.is_none() {
236 return Err(QueryError::EmptyMarkdown {
237 label: label.clone(),
238 });
239 }
240 Ok(QueryBundle {
241 schema: QuerySchema::V4,
242 label,
243 document: (!document_is_empty).then_some(parsed.document),
244 tldr: parsed.tldr,
245 })
246}
247
248fn query_manual(
249 name: &str,
250 requested_section: Option<&str>,
251 policy: QueryPolicy,
252 host: &dyn QueryHost,
253) -> Result<QueryBundle, QueryError> {
254 let name = name.trim();
255 if name.is_empty() {
256 return Err(QueryError::EmptyName);
257 }
258 let section = requested_section.map(str::trim);
259 if section.is_some_and(str::is_empty) {
260 return Err(QueryError::InvalidSection);
261 }
262 let section = section.map(ToOwned::to_owned);
263 let require_manual = policy.manual_only || section.is_some();
264
265 if section.is_none()
269 && !policy.manual_only
270 && let Some(path) = host.locate_registered_document(name)
271 {
272 return query_registered_document(name, &path, host);
273 }
274
275 let manual_request = ManualRequest::new(name, section.clone());
276
277 let tldr = host.read_tldr(name).ok().flatten();
280 let mut manual = load_manual(&manual_request, host);
281
282 if let (Ok(Some(document)), Some(section)) = (&mut manual, section.as_deref())
285 && document.meta.section.is_none()
286 {
287 document.meta.section = Some(section.to_owned());
288 }
289
290 if require_manual {
293 return match manual {
294 Ok(Some(manual)) => Ok(QueryBundle {
295 schema: QuerySchema::V4,
296 label: name.to_owned(),
297 document: Some(manual),
298 tldr,
299 }),
300 Ok(None) => Err(QueryError::NoReadableContent {
301 name: name.to_owned(),
302 }),
303 Err(detail) => Err(QueryError::Manual {
304 name: name.to_owned(),
305 detail,
306 }),
307 };
308 }
309
310 match manual {
311 Ok(Some(manual)) => Ok(QueryBundle {
312 schema: QuerySchema::V4,
313 label: name.to_owned(),
314 document: Some(manual),
315 tldr,
316 }),
317 Ok(None) | Err(_) if tldr.is_some() => Ok(QueryBundle {
318 schema: QuerySchema::V4,
319 label: name.to_owned(),
320 document: None,
321 tldr,
322 }),
323 Ok(None) => Err(QueryError::NoReadableContent {
324 name: name.to_owned(),
325 }),
326 Err(detail) => Err(QueryError::Manual {
327 name: name.to_owned(),
328 detail,
329 }),
330 }
331}
332
333fn query_registered_document(
334 name: &str,
335 path: &Path,
336 host: &dyn QueryHost,
337) -> Result<QueryBundle, QueryError> {
338 let source_path = path.to_string_lossy().into_owned();
339 let source = host
340 .read_markdown(path)
341 .map_err(|detail| QueryError::Markdown {
342 path: source_path.clone(),
343 detail,
344 })?;
345 let mut query = query_markdown_text(&source, Some(source_path))?;
346 name.clone_into(&mut query.label);
347 Ok(query)
348}
349
350fn load_manual(
351 request: &ManualRequest,
352 host: &dyn QueryHost,
353) -> Result<Option<MantDocument>, String> {
354 let located = host.locate_manual(request);
355 let (source_path, direct) = match located {
356 Ok(path) => {
357 let direct = host.parse_manual(&path);
358 (Some(path), direct)
359 }
360 Err(error) => (None, Err(error)),
361 };
362
363 let document = direct.map_err(|error| {
364 format!(
365 "could not load manual '{}': source/libmandoc: {error}",
366 request.name
367 )
368 })?;
369 if document.sections.is_empty() {
370 let path = source_path.as_deref().map_or_else(
371 || "<unknown source>".to_owned(),
372 |path| path.display().to_string(),
373 );
374 let diagnostics = document
375 .diagnostics
376 .iter()
377 .map(|diagnostic| {
378 let location = diagnostic.source.map_or_else(String::new, |source| {
379 format!(" at {}:{}", source.line, source.column)
380 });
381 format!("{:?}{location}: {}", diagnostic.level, diagnostic.message)
382 })
383 .collect::<Vec<_>>()
384 .join("; ");
385 let detail = if diagnostics.is_empty() {
386 String::new()
387 } else {
388 format!("; diagnostics: {diagnostics}")
389 };
390 return Err(format!(
391 "could not load manual '{}': libmandoc parsed {path} but produced no readable sections{detail}",
392 request.name,
393 ));
394 }
395 Ok(Some(document))
396}
397
398#[cfg(test)]
399mod tests {
400 use std::{
401 io,
402 path::{Path, PathBuf},
403 sync::Mutex,
404 };
405
406 use mant_ast::{
407 Diagnostic, DiagnosticLevel, DocumentMeta, DocumentSchema, DocumentSource, MantDocument,
408 Producer, QueryInput, QueryRequest, QueryView, RequestSchema, Section, SourceFormat,
409 TldrDocument, TldrOrigin,
410 };
411
412 use crate::ManualRequest;
413
414 use super::{
415 MAX_MARKDOWN_BYTES, QueryError, QueryHost, QueryPolicy, query_markdown_text, query_with,
416 read_capped_utf8,
417 };
418
419 #[derive(Clone)]
420 struct StubHost {
421 registered_document: Option<PathBuf>,
422 locate: Result<PathBuf, String>,
423 direct: Result<MantDocument, String>,
424 tldr: Result<Option<TldrDocument>, String>,
425 markdown: Result<String, String>,
426 calls: std::sync::Arc<Mutex<Vec<&'static str>>>,
427 }
428
429 impl QueryHost for StubHost {
430 fn locate_registered_document(&self, _name: &str) -> Option<PathBuf> {
431 self.calls.lock().expect("calls lock").push("name");
432 self.registered_document.clone()
433 }
434
435 fn locate_manual(&self, _request: &ManualRequest) -> Result<PathBuf, String> {
436 self.calls.lock().expect("calls lock").push("locate");
437 self.locate.clone()
438 }
439
440 fn parse_manual(&self, _path: &Path) -> Result<MantDocument, String> {
441 self.calls.lock().expect("calls lock").push("parse");
442 self.direct.clone()
443 }
444
445 fn read_tldr(&self, _name: &str) -> Result<Option<TldrDocument>, String> {
446 self.calls.lock().expect("calls lock").push("tldr");
447 self.tldr.clone()
448 }
449
450 fn read_markdown(&self, _path: &Path) -> Result<String, String> {
451 self.calls.lock().expect("calls lock").push("markdown");
452 self.markdown.clone()
453 }
454 }
455
456 fn document(format: SourceFormat, unsupported: bool, readable: bool) -> MantDocument {
457 MantDocument {
458 schema: DocumentSchema::V4,
459 producer: Producer {
460 name: "test".to_owned(),
461 version: "1".to_owned(),
462 engine: None,
463 },
464 source: DocumentSource { format, path: None },
465 meta: DocumentMeta::default(),
466 diagnostics: unsupported
467 .then_some(Diagnostic {
468 level: DiagnosticLevel::Unsupported,
469 code: None,
470 message: "unsupported request".to_owned(),
471 source: None,
472 })
473 .into_iter()
474 .collect(),
475 blocks: Vec::new(),
476 sections: readable
477 .then_some(Section {
478 id: "name-1".to_owned(),
479 title: "NAME".to_owned(),
480 spacing_before_lines: 0,
481 blocks: Vec::new(),
482 children: Vec::new(),
483 source: None,
484 })
485 .into_iter()
486 .collect(),
487 }
488 }
489
490 fn tldr() -> TldrDocument {
491 TldrDocument {
492 title: "tool".to_owned(),
493 description: vec!["quick reference".to_owned()],
494 more_information: None,
495 examples: Vec::new(),
496 platform: "common".to_owned(),
497 language: "en".to_owned(),
498 source_path: "/cache/pages/common/tool.md".to_owned(),
499 origin: TldrOrigin::TldrPages,
500 }
501 }
502
503 fn host(direct: Result<MantDocument, String>) -> StubHost {
504 StubHost {
505 registered_document: None,
506 locate: Ok(PathBuf::from("/man/tool.1")),
507 direct,
508 tldr: Ok(None),
509 markdown: Err("Markdown unavailable".to_owned()),
510 calls: std::sync::Arc::default(),
511 }
512 }
513
514 fn request() -> QueryRequest {
515 QueryRequest {
516 schema: RequestSchema::V4,
517 input: QueryInput::Document {
518 name: " tool ".to_owned(),
519 section: None,
520 },
521 view: QueryView::Full {},
522 }
523 }
524
525 #[test]
526 fn ordinary_manual_uses_the_native_parser() {
527 let host = host(Ok(document(SourceFormat::Man, false, true)));
528 let result = query_with(&request(), QueryPolicy::default(), &host).expect("query");
529
530 assert_eq!(result.label, "tool");
531 assert_eq!(
532 result.document.expect("manual").source.format,
533 SourceFormat::Man
534 );
535 assert_eq!(
536 *host.calls.lock().expect("calls lock"),
537 ["name", "tldr", "locate", "parse"]
538 );
539 }
540
541 #[test]
542 fn requested_section_backfills_metadata_the_parser_left_empty() {
543 let host = host(Ok(document(SourceFormat::Man, false, true)));
544 let request = QueryRequest {
545 schema: RequestSchema::V4,
546 input: QueryInput::Document {
547 name: "tool".to_owned(),
548 section: Some("3".to_owned()),
549 },
550 view: QueryView::Full {},
551 };
552
553 let result = query_with(&request, QueryPolicy::default(), &host).expect("query");
554 assert_eq!(
555 result.document.expect("manual").meta.section.as_deref(),
556 Some("3"),
557 "requested section must label output when the parser omits it"
558 );
559 assert_eq!(
560 *host.calls.lock().expect("calls lock"),
561 ["tldr", "locate", "parse"],
562 "an explicit manual section bypasses registered Markdown"
563 );
564 }
565
566 #[test]
567 fn complete_direct_document_survives_an_unsupported_finding() {
568 let host = host(Ok(document(SourceFormat::Man, true, true)));
569 let result = query_with(&request(), QueryPolicy::default(), &host).expect("query");
570
571 assert_eq!(
572 result.document.expect("manual").source.format,
573 SourceFormat::Man
574 );
575 assert_eq!(
576 *host.calls.lock().expect("calls lock"),
577 ["name", "tldr", "locate", "parse"]
578 );
579 }
580
581 #[test]
582 fn manual_only_bypasses_registered_markdown() {
583 let mut host = host(Ok(document(SourceFormat::Man, true, true)));
584 host.registered_document = Some(PathBuf::from("/data/mant/tool.md"));
585 host.markdown = Ok("# Registered".to_owned());
586 let result = query_with(&request(), QueryPolicy { manual_only: true }, &host)
587 .expect("manual-only query");
588
589 assert_eq!(
590 result.document.expect("manual").source.format,
591 SourceFormat::Man
592 );
593 assert_eq!(
594 *host.calls.lock().expect("calls lock"),
595 ["tldr", "locate", "parse"],
596 "manual-only lookup must not inspect the registered-document namespace"
597 );
598 }
599
600 #[test]
601 fn manual_only_failure_is_not_hidden_by_tldr() {
602 let mut host = host(Ok(document(SourceFormat::Man, true, false)));
603 host.tldr = Ok(Some(tldr()));
604
605 let error = query_with(&request(), QueryPolicy { manual_only: true }, &host)
606 .expect_err("an optional tldr page must not hide native parser failure");
607
608 let QueryError::Manual { detail, .. } = error else {
609 panic!("expected the native parser diagnostic");
610 };
611 assert!(detail.contains("/man/tool.1"));
612 assert!(detail.contains("Unsupported: unsupported request"));
613 assert_eq!(
614 *host.calls.lock().expect("calls lock"),
615 ["tldr", "locate", "parse"]
616 );
617 }
618
619 #[test]
620 fn requested_section_failure_is_not_hidden_by_tldr() {
621 let mut host = host(Err("libmandoc failed".to_owned()));
622 host.locate = Err("section not found".to_owned());
623 host.tldr = Ok(Some(tldr()));
624 let request = QueryRequest {
625 schema: RequestSchema::V4,
626 input: QueryInput::Document {
627 name: "tool".to_owned(),
628 section: Some("7".to_owned()),
629 },
630 view: QueryView::Full {},
631 };
632
633 let error = query_with(&request, QueryPolicy::default(), &host)
634 .expect_err("an explicit section must require a native manual");
635
636 assert!(matches!(&error, QueryError::Manual { .. }));
637 assert!(error.to_string().contains("section not found"));
638 assert_eq!(*host.calls.lock().expect("calls lock"), ["tldr", "locate"]);
639 }
640
641 #[test]
642 fn truncated_unsupported_document_is_an_error_by_default() {
643 let host = host(Ok(document(SourceFormat::Man, true, false)));
644
645 let QueryError::Manual { detail, .. } =
646 query_with(&request(), QueryPolicy::default(), &host)
647 .expect_err("empty-section document must error by default")
648 else {
649 panic!("expected Manual error");
650 };
651 assert!(detail.contains("produced no readable sections"));
652 }
653
654 #[test]
655 fn readable_best_effort_document_survives_parser_findings() {
656 let host = host(Ok(document(SourceFormat::Mdoc, true, true)));
657 let result = query_with(&request(), QueryPolicy::default(), &host).expect("query");
658 assert_eq!(
659 result.document.expect("manual").source.format,
660 SourceFormat::Mdoc
661 );
662 }
663
664 #[test]
665 fn cached_tldr_survives_total_manual_failure() {
666 let mut host = host(Err("libmandoc failed".to_owned()));
667 host.locate = Err("source not found".to_owned());
668 host.tldr = Ok(Some(tldr()));
669 let result =
670 query_with(&request(), QueryPolicy::default(), &host).expect("tldr-only query");
671
672 assert!(result.document.is_none());
673 assert_eq!(result.tldr.expect("tldr").title, "tool");
674 }
675
676 #[test]
677 fn reports_both_manual_paths_when_no_content_exists() {
678 let mut host = host(Err("libmandoc failed".to_owned()));
679 host.locate = Err("source not found".to_owned());
680 let error = query_with(&request(), QueryPolicy::default(), &host)
681 .expect_err("empty query must fail");
682 assert_eq!(
683 error.to_string(),
684 "could not load manual 'tool': source/libmandoc: source not found"
685 );
686 }
687
688 #[test]
689 fn validates_before_touching_host_state() {
690 let host = host(Ok(document(SourceFormat::Man, false, true)));
691 assert_eq!(
692 query_with(
693 &QueryRequest {
694 schema: RequestSchema::V4,
695 input: QueryInput::Document {
696 name: " ".to_owned(),
697 section: None,
698 },
699 view: QueryView::Full {},
700 },
701 QueryPolicy::default(),
702 &host
703 ),
704 Err(QueryError::EmptyName)
705 );
706 assert!(host.calls.lock().expect("calls lock").is_empty());
707 }
708
709 #[test]
710 fn registered_markdown_shadows_an_unqualified_manual_name() {
711 let mut host = host(Err("manual parser must not run".to_owned()));
712 host.registered_document = Some(PathBuf::from("/data/mant/tool.md"));
713 host.markdown = Ok("# Tool\n\n## Options\n\n- `--help`: Show help.\n".to_owned());
714
715 let result = query_with(&request(), QueryPolicy::default(), &host)
716 .expect("registered Markdown name");
717
718 assert_eq!(result.label, "tool");
719 assert!(result.tldr.is_none());
720 let document = result.document.expect("registered document");
721 assert_eq!(document.source.format, SourceFormat::Markdown);
722 assert_eq!(document.source.path.as_deref(), Some("/data/mant/tool.md"));
723 assert_eq!(
724 *host.calls.lock().expect("calls lock"),
725 ["name", "markdown"],
726 "a registered name must not consult man or external tldr caches"
727 );
728 }
729
730 #[test]
731 fn markdown_files_bypass_manual_and_tldr_sources() {
732 let mut host = host(Err("manual parser must not run".to_owned()));
733 host.markdown = Ok("# Tool\n\n## Options\n\n- `--help`: Show help.\n".to_owned());
734 let result = query_with(
735 &QueryRequest {
736 schema: RequestSchema::V4,
737 input: QueryInput::MarkdownFile {
738 path: "docs/tool.md".to_owned(),
739 },
740 view: QueryView::Full {},
741 },
742 QueryPolicy::default(),
743 &host,
744 )
745 .expect("Markdown query");
746
747 assert_eq!(result.label, "tool.md");
748 assert!(result.tldr.is_none());
749 let document = result.document.expect("document");
750 assert_eq!(document.source.format, SourceFormat::Markdown);
751 assert_eq!(document.source.path.as_deref(), Some("docs/tool.md"));
752 assert_eq!(
753 *host.calls.lock().expect("calls lock"),
754 ["markdown"],
755 "Markdown must not consult man or tldr"
756 );
757 }
758
759 #[test]
760 fn in_memory_markdown_is_available_without_a_protocol_content_field() {
761 let result = query_markdown_text("# Piped\n\nBody.\n", None).expect("stdin Markdown query");
762
763 assert_eq!(result.label, "stdin");
764 assert!(result.tldr.is_none());
765 let document = result.document.expect("document");
766 assert_eq!(document.meta.title.as_deref(), Some("Piped"));
767 assert_eq!(document.source.path, None);
768 }
769
770 #[test]
771 fn leading_tldr_directives_are_independent_from_the_markdown_document() {
772 let source = "\
773<!-- mant:tldr:start -->
774# demo
775
776> Concise embedded help.
777
778- Run the demo:
779
780`demo {{path}}`
781<!-- mant:tldr:end -->
782
783# Demo
784
785Document overview.
786
787## Options
788
789- `--help`: Show help.
790";
791 let result =
792 query_markdown_text(source, Some("docs/demo.md".to_owned())).expect("Markdown query");
793
794 let tldr = result.tldr.expect("embedded tldr");
795 assert_eq!(tldr.title, "demo");
796 assert_eq!(tldr.origin, TldrOrigin::Embedded);
797 assert_eq!(tldr.source_path, "docs/demo.md");
798 assert_eq!(tldr.examples[0].command, "demo {{path}}");
799
800 let document = result.document.expect("document body");
801 assert_eq!(document.meta.title.as_deref(), Some("Demo"));
802 assert_eq!(document.sections[0].title, "Options");
803 assert!(
804 document
805 .blocks
806 .iter()
807 .any(|block| matches!(block, mant_ast::Block::Paragraph { .. }))
808 );
809 assert!(
810 document
811 .diagnostics
812 .iter()
813 .all(|diagnostic| !diagnostic.message.contains("mant:tldr"))
814 );
815 }
816
817 #[test]
818 fn malformed_leading_tldr_directives_report_the_source_path() {
819 let error = query_markdown_text(
820 "<!-- mant:tldr:start -->\n# demo\n\n- Run:\n\n`demo`\n",
821 Some("docs/broken.md".to_owned()),
822 )
823 .expect_err("unterminated directive");
824
825 assert_eq!(
826 error.to_string(),
827 "could not load Markdown document 'docs/broken.md': top-level <!-- mant:tldr:start --> marker is missing its <!-- mant:tldr:end --> marker"
828 );
829 }
830
831 #[test]
832 fn capped_read_accepts_input_up_to_the_limit() {
833 let source = "abcd";
834 assert_eq!(
835 read_capped_utf8(source.as_bytes(), source.len() as u64).expect("within limit"),
836 source
837 );
838 }
839
840 #[test]
841 fn capped_read_rejects_input_past_the_limit_without_buffering_it_whole() {
842 let error = read_capped_utf8(io::repeat(b'a'), 8).expect_err("over limit");
845 assert!(error.contains("exceeds the 8-byte limit"), "{error}");
846 }
847
848 #[test]
849 fn capped_read_rejects_non_utf8_input() {
850 let error =
851 read_capped_utf8(&[0xff, 0xfe][..], MAX_MARKDOWN_BYTES).expect_err("invalid UTF-8");
852 assert!(error.contains("must be UTF-8"), "{error}");
853 }
854}