pharos_runtime_docs/docs.rs
1use std::fs;
2use std::path::{Path, PathBuf};
3
4use saphyr::LoadableYamlNode;
5use serde_json::Value;
6
7/// Performs dbuf put tag open.
8///
9/// # Parameters
10///
11/// - `tag_name`: Input consumed by [`crate::docs::dbuf_put_tag_open`].
12///
13/// # Returns
14///
15/// Returns [`Some`] when [`crate::docs::dbuf_put_tag_open`] can produce a value and [`None`] otherwise.
16///
17/// # Examples
18///
19/// ```rust,ignore
20/// let _result = pharos_runtime_docs::docs::dbuf_put_tag_open(todo!());
21/// ```
22///
23/// # Related
24///
25/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
26pub fn dbuf_put_tag_open(tag_name: &str) -> Option<String> {
27 (!tag_name.is_empty()).then(|| format!("<{tag_name}>"))
28}
29
30/// Performs dbuf put anchor open.
31///
32/// # Parameters
33///
34/// - `href`: Input consumed by [`crate::docs::dbuf_put_anchor_open`].
35///
36/// # Returns
37///
38/// Returns [`Some`] when [`crate::docs::dbuf_put_anchor_open`] can produce a value and [`None`] otherwise.
39///
40/// # Examples
41///
42/// ```rust,ignore
43/// let _result = pharos_runtime_docs::docs::dbuf_put_anchor_open(todo!());
44/// ```
45///
46/// # Related
47///
48/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
49pub fn dbuf_put_anchor_open(href: &str) -> Option<String> {
50 Some(format!("<a href=\"{}\">", html_escape(href)))
51}
52
53/// Performs line len.
54///
55/// # Parameters
56///
57/// - `line`: Input consumed by [`crate::docs::line_len`].
58///
59/// # Returns
60///
61/// Returns the computed value produced by [`crate::docs::line_len`].
62///
63/// # Examples
64///
65/// ```rust,ignore
66/// let _result = pharos_runtime_docs::docs::line_len(todo!());
67/// ```
68///
69/// # Related
70///
71/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
72pub fn line_len(line: &str) -> usize {
73 line.len()
74}
75
76/// Performs line starts with.
77///
78/// # Parameters
79///
80/// - `line`: Input consumed by [`crate::docs::line_starts_with`].
81/// - `prefix`: Input consumed by [`crate::docs::line_starts_with`].
82///
83/// # Returns
84///
85/// Returns `true` when the condition checked by [`crate::docs::line_starts_with`] holds and `false` otherwise.
86///
87/// # Examples
88///
89/// ```rust,ignore
90/// let _result = pharos_runtime_docs::docs::line_starts_with(todo!(), todo!());
91/// ```
92///
93/// # Related
94///
95/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
96pub fn line_starts_with(line: &str, prefix: &str) -> bool {
97 line.starts_with(prefix)
98}
99
100/// Performs skip spaces.
101///
102/// # Parameters
103///
104/// - `s`: Input consumed by [`crate::docs::skip_spaces`].
105///
106/// # Returns
107///
108/// Returns the computed value produced by [`crate::docs::skip_spaces`].
109///
110/// # Examples
111///
112/// ```rust,ignore
113/// let _result = pharos_runtime_docs::docs::skip_spaces(todo!());
114/// ```
115///
116/// # Related
117///
118/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
119pub fn skip_spaces(s: &str) -> &str {
120 s.trim_start_matches([' ', '\t'])
121}
122
123/// Performs HTML escape.
124fn html_escape(input: &str) -> String {
125 input
126 .replace('&', "&")
127 .replace('<', "<")
128 .replace('>', ">")
129 .replace('"', """)
130}
131
132/// Resets a documentation buffer and releases its previous allocation.
133///
134/// # Parameters
135///
136/// - `output`: Input consumed by [`crate::docs::dbuf_init`].
137///
138/// # Returns
139///
140/// Returns after [`crate::docs::dbuf_init`] applies its in-place effect.
141///
142/// # Examples
143///
144/// ```rust,ignore
145/// let _result = pharos_runtime_docs::docs::dbuf_init(todo!());
146/// ```
147///
148/// # Related
149///
150/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
151pub fn dbuf_init(output: &mut String) {
152 *output = String::new();
153}
154
155/// Ensures bounded capacity for one raw buffer append.
156///
157/// # Parameters
158///
159/// - `output`: Input consumed by [`crate::docs::dbuf_ensure`].
160/// - `additional`: Input consumed by [`crate::docs::dbuf_ensure`].
161///
162/// # Returns
163///
164/// Returns `true` when the condition checked by [`crate::docs::dbuf_ensure`] holds and `false` otherwise.
165///
166/// # Examples
167///
168/// ```rust,ignore
169/// let _result = pharos_runtime_docs::docs::dbuf_ensure(todo!(), todo!());
170/// ```
171///
172/// # Related
173///
174/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
175pub fn dbuf_ensure(output: &mut String, additional: usize) -> bool {
176 let Some(required_len) = output.len().checked_add(additional) else {
177 return false;
178 };
179 if required_len > MAX_DOC_TEXT_BYTES {
180 return false;
181 }
182 output.try_reserve(additional.saturating_add(1)).is_ok()
183}
184
185/// Appends one raw buffer fragment within the documentation size limit.
186///
187/// # Parameters
188///
189/// - `output`: Input consumed by [`crate::docs::dbuf_append`].
190/// - `value`: Input consumed by [`crate::docs::dbuf_append`].
191///
192/// # Returns
193///
194/// Returns `true` when the condition checked by [`crate::docs::dbuf_append`] holds and `false` otherwise.
195///
196/// # Examples
197///
198/// ```rust,ignore
199/// let _result = pharos_runtime_docs::docs::dbuf_append(todo!(), "value");
200/// ```
201///
202/// # Related
203///
204/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
205pub fn dbuf_append(output: &mut String, value: &str) -> bool {
206 if !dbuf_ensure(output, value.len()) {
207 return false;
208 }
209 output.push_str(value);
210 true
211}
212
213/// Appends one NUL-terminated C-string equivalent to the buffer.
214///
215/// # Parameters
216///
217/// - `output`: Input consumed by [`crate::docs::dbuf_puts`].
218/// - `value`: Input consumed by [`crate::docs::dbuf_puts`].
219///
220/// # Returns
221///
222/// Returns `true` when the condition checked by [`crate::docs::dbuf_puts`] holds and `false` otherwise.
223///
224/// # Examples
225///
226/// ```rust,ignore
227/// let _result = pharos_runtime_docs::docs::dbuf_puts(todo!(), "value");
228/// ```
229///
230/// # Related
231///
232/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
233pub fn dbuf_puts(output: &mut String, value: &str) -> bool {
234 dbuf_append(output, value)
235}
236
237/// Transfers the buffer contents, leaving an unallocated empty buffer.
238///
239/// # Parameters
240///
241/// - `output`: Input consumed by [`crate::docs::dbuf_detach`].
242///
243/// # Returns
244///
245/// Returns [`Some`] when [`crate::docs::dbuf_detach`] can produce a value and [`None`] otherwise.
246///
247/// # Examples
248///
249/// ```rust,ignore
250/// let _result = pharos_runtime_docs::docs::dbuf_detach(todo!());
251/// ```
252///
253/// # Related
254///
255/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
256pub fn dbuf_detach(output: &mut String) -> Option<String> {
257 (output.capacity() > 0).then(|| std::mem::take(output))
258}
259
260/// Performs replace placeholder.
261///
262/// # Parameters
263///
264/// - `out`: Input consumed by [`crate::docs::replace_placeholder`].
265/// - `template_text`: Input consumed by [`crate::docs::replace_placeholder`].
266/// - `placeholder`: Input consumed by [`crate::docs::replace_placeholder`].
267/// - `value`: Input consumed by [`crate::docs::replace_placeholder`].
268/// - `is_html`: Input consumed by [`crate::docs::replace_placeholder`].
269///
270/// # Returns
271///
272/// Returns [`Some`] when [`crate::docs::replace_placeholder`] can produce a value and [`None`] otherwise.
273///
274/// # Performance
275///
276/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::replace_placeholder`].
277///
278/// # Examples
279///
280/// ```rust,ignore
281/// let _result = pharos_runtime_docs::docs::replace_placeholder(todo!(), todo!(), todo!(), "value", todo!());
282/// ```
283///
284/// # Related
285///
286/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
287pub fn replace_placeholder<'a>(
288 out: &mut String,
289 template_text: &'a str,
290 placeholder: &str,
291 value: &str,
292 is_html: bool,
293) -> Option<&'a str> {
294 let mut cursor = 0usize;
295 while let Some(relative_open) = template_text[cursor..].find('{') {
296 let open = cursor + relative_open;
297 let key_start = open + 1;
298 let relative_close = template_text[key_start..].find('}')?;
299 let close = key_start + relative_close;
300 let mut effective_key = &template_text[key_start..close];
301 let mut this_is_html = is_html;
302 if let Some(stripped) = effective_key.strip_prefix("html:") {
303 effective_key = stripped;
304 this_is_html = true;
305 }
306 if effective_key == placeholder {
307 out.push_str(&template_text[..open]);
308 if this_is_html {
309 out.push_str(value);
310 } else {
311 out.push_str(&html_escape(value));
312 }
313 return Some(&template_text[close + 1..]);
314 }
315 cursor = close + 1;
316 }
317 None
318}
319
320/// Converts a bounded YAML scalar value into its canonical text representation.
321///
322/// # Parameters
323///
324/// - `value`: Input consumed by [`crate::docs::docs_yaml_value`].
325///
326/// # Returns
327///
328/// Returns [`Some`] when [`crate::docs::docs_yaml_value`] can produce a value and [`None`] otherwise.
329///
330/// # Examples
331///
332/// ```rust,ignore
333/// let _result = pharos_runtime_docs::docs::docs_yaml_value("value");
334/// ```
335///
336/// # Related
337///
338/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
339pub fn docs_yaml_value(value: &str) -> Option<String> {
340 let trimmed = value.trim();
341 if trimmed.is_empty() || trimmed.len() > 64 * 1024 {
342 return None;
343 }
344 Some(trimmed.trim_matches(['\"', '\'']).to_owned())
345}
346
347/// Completes one output remap item only when both sides are present.
348///
349/// # Parameters
350///
351/// - `source`: Input consumed by [`crate::docs::docs_output_remap_finish_item`].
352/// - `output`: Input consumed by [`crate::docs::docs_output_remap_finish_item`].
353/// - `) -> Option<(String`: Input consumed by [`crate::docs::docs_output_remap_finish_item`].
354/// - `String`: Input consumed by [`crate::docs::docs_output_remap_finish_item`].
355///
356/// # Returns
357///
358/// Returns [`Some`] when [`crate::docs::docs_output_remap_finish_item`] can produce a value and [`None`] otherwise.
359///
360/// # Examples
361///
362/// ```rust,ignore
363/// let _result = pharos_runtime_docs::docs::docs_output_remap_finish_item(todo!(), todo!(), todo!(), todo!());
364/// ```
365///
366/// # Related
367///
368/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
369pub fn docs_output_remap_finish_item(
370 source: Option<&str>,
371 output: Option<&str>,
372) -> Option<(String, String)> {
373 let source = source?.trim();
374 let output = output?;
375 (!source.is_empty()).then(|| (source.to_owned(), output.to_owned()))
376}
377
378/// Appends a bounded inline-markup fragment with safe link and code handling.
379///
380/// # Parameters
381///
382/// - `output`: Input consumed by [`crate::docs::append_inline_markup`].
383/// - `text`: Input consumed by [`crate::docs::append_inline_markup`].
384///
385/// # Returns
386///
387/// Returns `true` when the condition checked by [`crate::docs::append_inline_markup`] holds and `false` otherwise.
388///
389/// # Performance
390///
391/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::append_inline_markup`].
392///
393/// # Examples
394///
395/// ```rust,ignore
396/// let _result = pharos_runtime_docs::docs::append_inline_markup(todo!(), "{}");
397/// ```
398///
399/// # Related
400///
401/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
402pub fn append_inline_markup(output: &mut String, text: &str) -> bool {
403 if text.len() > 256 * 1024 || output.len().saturating_add(text.len()) > 2 * 1024 * 1024 {
404 return false;
405 }
406 let mut cursor = 0usize;
407 while cursor < text.len() {
408 let remainder = &text[cursor..];
409 if let Some(end) = remainder.find('`') {
410 output.push_str(&html_escape(&remainder[..end]));
411 let after_open = &remainder[end + 1..];
412 let Some(close) = after_open.find('`') else {
413 output.push('`');
414 output.push_str(&html_escape(after_open));
415 return true;
416 };
417 output.push_str("<code>");
418 output.push_str(&html_escape(&after_open[..close]));
419 output.push_str("</code>");
420 cursor += end + close + 2;
421 } else {
422 output.push_str(&html_escape(remainder));
423 break;
424 }
425 }
426 true
427}
428
429#[allow(non_snake_case)]
430/// Flushes one markdown paragraph into the output buffer.
431///
432/// # Parameters
433///
434/// - `output`: Input consumed by [`crate::docs::FLUSH_PARA`].
435/// - `paragraph`: Input consumed by [`crate::docs::FLUSH_PARA`].
436///
437/// # Returns
438///
439/// Returns `true` when the condition checked by [`crate::docs::FLUSH_PARA`] holds and `false` otherwise.
440///
441/// # Examples
442///
443/// ```rust,ignore
444/// let _result = pharos_runtime_docs::docs::FLUSH_PARA(todo!(), todo!());
445/// ```
446///
447/// # Related
448///
449/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
450pub fn FLUSH_PARA(output: &mut String, paragraph: &str) -> bool {
451 if paragraph.trim().is_empty() {
452 return true;
453 }
454 if !append_inline_markup(output, paragraph.trim()) {
455 return false;
456 }
457 output.push_str("\n\n");
458 true
459}
460
461/// Closes a validated markup tag.
462///
463/// # Parameters
464///
465/// - `tag_name`: Input consumed by [`crate::docs::dbuf_put_tag_close`].
466///
467/// # Returns
468///
469/// Returns [`Some`] when [`crate::docs::dbuf_put_tag_close`] can produce a value and [`None`] otherwise.
470///
471/// # Performance
472///
473/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::dbuf_put_tag_close`].
474///
475/// # Examples
476///
477/// ```rust,ignore
478/// let _result = pharos_runtime_docs::docs::dbuf_put_tag_close(todo!());
479/// ```
480///
481/// # Related
482///
483/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
484pub fn dbuf_put_tag_close(tag_name: &str) -> Option<String> {
485 (!tag_name.is_empty() && tag_name.bytes().all(|byte| byte.is_ascii_alphanumeric()))
486 .then(|| format!("</{tag_name}>"))
487}
488
489/// Rewrites one documentation link target while rejecting unsafe schemes.
490///
491/// # Parameters
492///
493/// - `target`: Input consumed by [`crate::docs::rewrite_link_target`].
494///
495/// # Returns
496///
497/// Returns [`Some`] when [`crate::docs::rewrite_link_target`] can produce a value and [`None`] otherwise.
498///
499/// # Performance
500///
501/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::rewrite_link_target`].
502///
503/// # Examples
504///
505/// ```rust,ignore
506/// let _result = pharos_runtime_docs::docs::rewrite_link_target("/");
507/// ```
508///
509/// # Related
510///
511/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
512pub fn rewrite_link_target(target: &str) -> Option<String> {
513 if target.is_empty() || target.chars().any(char::is_control) {
514 return None;
515 }
516 if target.starts_with("javascript:") || target.starts_with("data:") {
517 return None;
518 }
519 Some(target.to_owned())
520}
521
522/// Appends one escaped character to a documentation buffer.
523///
524/// # Parameters
525///
526/// - `output`: Input consumed by [`crate::docs::dbuf_putc`].
527/// - `character`: Input consumed by [`crate::docs::dbuf_putc`].
528///
529/// # Returns
530///
531/// Returns `true` when the condition checked by [`crate::docs::dbuf_putc`] holds and `false` otherwise.
532///
533/// # Examples
534///
535/// ```rust,ignore
536/// let _result = pharos_runtime_docs::docs::dbuf_putc(todo!(), todo!());
537/// ```
538///
539/// # Related
540///
541/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
542pub fn dbuf_putc(output: &mut String, character: char) -> bool {
543 if character.is_control() && character != '\n' {
544 return false;
545 }
546 let mut encoded = [0u8; 4];
547 dbuf_append(output, character.encode_utf8(&mut encoded))
548}
549
550/// Appends escaped HTML text to a documentation buffer.
551///
552/// # Parameters
553///
554/// - `output`: Input consumed by [`crate::docs::dbuf_append_html_escaped`].
555/// - `value`: Input consumed by [`crate::docs::dbuf_append_html_escaped`].
556///
557/// # Returns
558///
559/// Returns `true` when the condition checked by [`crate::docs::dbuf_append_html_escaped`] holds and `false` otherwise.
560///
561/// # Examples
562///
563/// ```rust,ignore
564/// let _result = pharos_runtime_docs::docs::dbuf_append_html_escaped(todo!(), "value");
565/// ```
566///
567/// # Related
568///
569/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
570pub fn dbuf_append_html_escaped(output: &mut String, value: &str) -> bool {
571 let escaped = html_escape(value);
572 dbuf_append(output, &escaped)
573}
574
575const MAX_DOC_FILES: usize = 4096;
576const MAX_DOC_TEXT_BYTES: usize = 2 * 1024 * 1024;
577
578#[derive(Debug, Clone, Default, Eq, PartialEq)]
579/// Public struct `DocsOutputRemap` used by this module.
580///
581/// # Fields
582///
583/// - `entries`: Stored field on `DocsOutputRemap`.
584///
585/// # Examples
586///
587/// ```rust,ignore
588/// let _ = std::mem::size_of::<pharos_runtime_docs::docs::DocsOutputRemap>();
589/// ```
590///
591/// # Related
592///
593/// - [`crate::docs`] for constructors, helpers, and neighboring runtime types.
594pub struct DocsOutputRemap {
595 pub entries: Vec<(String, String)>,
596}
597
598#[derive(Debug, Clone, Eq, PartialEq)]
599/// Public struct `DocsNavEntry` used by this module.
600///
601/// # Fields
602///
603/// - `path`: Stored field on `DocsNavEntry`.
604/// - `label`: Stored field on `DocsNavEntry`.
605/// - `depth`: Stored field on `DocsNavEntry`.
606///
607/// # Examples
608///
609/// ```rust,ignore
610/// let _ = std::mem::size_of::<pharos_runtime_docs::docs::DocsNavEntry>();
611/// ```
612///
613/// # Related
614///
615/// - [`crate::docs`] for constructors, helpers, and neighboring runtime types.
616pub struct DocsNavEntry {
617 pub path: String,
618 pub label: String,
619 pub depth: usize,
620}
621
622struct DocsNavNode {
623 path: String,
624 label: String,
625 depth: usize,
626 children: Vec<DocsNavNode>,
627}
628
629/// Performs dbuf free.
630///
631/// # Parameters
632///
633/// - `output`: Input consumed by [`crate::docs::dbuf_free`].
634///
635/// # Returns
636///
637/// Returns after [`crate::docs::dbuf_free`] applies its in-place effect.
638///
639/// # Examples
640///
641/// ```rust,ignore
642/// let _result = pharos_runtime_docs::docs::dbuf_free(todo!());
643/// ```
644///
645/// # Related
646///
647/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
648pub fn dbuf_free(output: &mut String) {
649 dbuf_init(output);
650}
651
652/// Performs docs output remap clear.
653///
654/// # Parameters
655///
656/// - `remap`: Input consumed by [`crate::docs::docs_output_remap_clear`].
657///
658/// # Returns
659///
660/// Returns after [`crate::docs::docs_output_remap_clear`] applies its in-place effect.
661///
662/// # Examples
663///
664/// ```rust,ignore
665/// let _result = pharos_runtime_docs::docs::docs_output_remap_clear(todo!());
666/// ```
667///
668/// # Related
669///
670/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
671pub fn docs_output_remap_clear(remap: &mut DocsOutputRemap) {
672 remap.entries.clear();
673}
674
675/// Performs docs output remap append.
676///
677/// # Parameters
678///
679/// - `remap`: Input consumed by [`crate::docs::docs_output_remap_append`].
680/// - `source`: Input consumed by [`crate::docs::docs_output_remap_append`].
681/// - `output`: Input consumed by [`crate::docs::docs_output_remap_append`].
682///
683/// # Returns
684///
685/// Returns `true` when the condition checked by [`crate::docs::docs_output_remap_append`] holds and `false` otherwise.
686///
687/// # Examples
688///
689/// ```rust,ignore
690/// let _result = pharos_runtime_docs::docs::docs_output_remap_append(todo!(), todo!(), todo!());
691/// ```
692///
693/// # Related
694///
695/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
696pub fn docs_output_remap_append(remap: &mut DocsOutputRemap, source: &str, output: &str) -> bool {
697 if source.trim().is_empty() || output.trim().is_empty() || remap.entries.len() >= MAX_DOC_FILES
698 {
699 return false;
700 }
701 remap.entries.push((source.to_owned(), output.to_owned()));
702 true
703}
704
705/// Performs docs output remap lookup source.
706///
707/// # Parameters
708///
709/// - `remap`: Input consumed by [`crate::docs::docs_output_remap_lookup_source`].
710/// - `source`: Input consumed by [`crate::docs::docs_output_remap_lookup_source`].
711///
712/// # Returns
713///
714/// Returns [`Some`] when [`crate::docs::docs_output_remap_lookup_source`] can produce a value and [`None`] otherwise.
715///
716/// # Performance
717///
718/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_output_remap_lookup_source`].
719///
720/// # Examples
721///
722/// ```rust,ignore
723/// let _result = pharos_runtime_docs::docs::docs_output_remap_lookup_source(todo!(), todo!());
724/// ```
725///
726/// # Related
727///
728/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
729pub fn docs_output_remap_lookup_source<'a>(
730 remap: &'a DocsOutputRemap,
731 source: &str,
732) -> Option<&'a str> {
733 remap
734 .entries
735 .iter()
736 .find_map(|(key, value)| (key == source).then_some(value.as_str()))
737}
738
739/// Performs docs output remap load JSON array.
740///
741/// # Parameters
742///
743/// - `entries`: Input consumed by [`crate::docs::docs_output_remap_load_json_array`].
744/// - `remap`: Input consumed by [`crate::docs::docs_output_remap_load_json_array`].
745///
746/// # Returns
747///
748/// Returns the computed value produced by [`crate::docs::docs_output_remap_load_json_array`].
749///
750/// # Performance
751///
752/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_output_remap_load_json_array`].
753///
754/// # Examples
755///
756/// ```rust,ignore
757/// let _result = pharos_runtime_docs::docs::docs_output_remap_load_json_array(todo!(), todo!());
758/// ```
759///
760/// # Related
761///
762/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
763pub fn docs_output_remap_load_json_array(entries: &Value, remap: &mut DocsOutputRemap) -> i32 {
764 let Some(entries) = entries.as_array() else {
765 return 66;
766 };
767 docs_output_remap_clear(remap);
768 for entry in entries {
769 let Some(source) = entry.get("source").and_then(Value::as_str) else {
770 docs_output_remap_clear(remap);
771 return 66;
772 };
773 let Some(output) = entry.get("output").and_then(Value::as_str) else {
774 docs_output_remap_clear(remap);
775 return 66;
776 };
777 if !docs_output_remap_append(remap, source, output) {
778 docs_output_remap_clear(remap);
779 return 69;
780 }
781 }
782 0
783}
784
785/// Performs docs output remap load YAML.
786///
787/// # Parameters
788///
789/// - `yaml_text`: Input consumed by [`crate::docs::docs_output_remap_load_yaml`].
790/// - `remap`: Input consumed by [`crate::docs::docs_output_remap_load_yaml`].
791///
792/// # Returns
793///
794/// Returns the computed value produced by [`crate::docs::docs_output_remap_load_yaml`].
795///
796/// # Performance
797///
798/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_output_remap_load_yaml`].
799///
800/// # Examples
801///
802/// ```rust,ignore
803/// let _result = pharos_runtime_docs::docs::docs_output_remap_load_yaml(todo!(), todo!());
804/// ```
805///
806/// # Related
807///
808/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
809pub fn docs_output_remap_load_yaml(yaml_text: &str, remap: &mut DocsOutputRemap) -> i32 {
810 let Ok(documents) = saphyr::Yaml::load_from_str(yaml_text) else {
811 return 66;
812 };
813 let Some(root) = documents.first() else {
814 docs_output_remap_clear(remap);
815 return 0;
816 };
817 let Some(items) = root.as_vec() else {
818 return 66;
819 };
820 let values: Vec<Value> = items
821 .iter()
822 .filter_map(|item| {
823 let saphyr::Yaml::Mapping(mapping) = item else {
824 return None;
825 };
826 let source = mapping.iter().find_map(|(key, value)| {
827 (key.as_str() == Some("source"))
828 .then(|| value.as_str().map(ToOwned::to_owned))
829 .flatten()
830 })?;
831 let output = mapping.iter().find_map(|(key, value)| {
832 (key.as_str() == Some("output"))
833 .then(|| value.as_str().map(ToOwned::to_owned))
834 .flatten()
835 })?;
836 Some(serde_json::json!({"source": source, "output": output}))
837 })
838 .collect();
839 docs_output_remap_load_json_array(&Value::Array(values), remap)
840}
841
842/// Performs docs default HTML output rel.
843fn docs_default_html_output_rel(rel_path: &str) -> Option<String> {
844 if rel_path.is_empty() {
845 return None;
846 }
847 let path = Path::new(rel_path);
848 let name = path.file_name()?.to_str()?;
849 let parent = path.parent().filter(|value| !value.as_os_str().is_empty());
850 let output_name = if matches!(name, "README.md" | "index.md") {
851 "index.html".to_owned()
852 } else if let Some(stem) = name.strip_suffix(".md") {
853 format!("{stem}.html")
854 } else {
855 name.to_owned()
856 };
857 Some(
858 parent
859 .map(|value| value.join(&output_name).to_string_lossy().into_owned())
860 .unwrap_or(output_name),
861 )
862}
863
864/// Performs docs output remap lookup default HTML.
865///
866/// # Parameters
867///
868/// - `remap`: Input consumed by [`crate::docs::docs_output_remap_lookup_default_html`].
869/// - `html_target`: Input consumed by [`crate::docs::docs_output_remap_lookup_default_html`].
870///
871/// # Returns
872///
873/// Returns [`Some`] when [`crate::docs::docs_output_remap_lookup_default_html`] can produce a value and [`None`] otherwise.
874///
875/// # Performance
876///
877/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_output_remap_lookup_default_html`].
878///
879/// # Examples
880///
881/// ```rust,ignore
882/// let _result = pharos_runtime_docs::docs::docs_output_remap_lookup_default_html(todo!(), todo!());
883/// ```
884///
885/// # Related
886///
887/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
888pub fn docs_output_remap_lookup_default_html<'a>(
889 remap: &'a DocsOutputRemap,
890 html_target: &str,
891) -> Option<&'a str> {
892 remap.entries.iter().find_map(|(source, output)| {
893 (docs_default_html_output_rel(source).as_deref() == Some(html_target))
894 .then_some(output.as_str())
895 })
896}
897
898/// Performs docs HTML output rel.
899///
900/// # Parameters
901///
902/// - `rel_path`: Input consumed by [`crate::docs::docs_html_output_rel`].
903///
904/// # Returns
905///
906/// Returns [`Some`] when [`crate::docs::docs_html_output_rel`] can produce a value and [`None`] otherwise.
907///
908/// # Examples
909///
910/// ```rust,ignore
911/// let _result = pharos_runtime_docs::docs::docs_html_output_rel(todo!());
912/// ```
913///
914/// # Related
915///
916/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
917pub fn docs_html_output_rel(rel_path: &str) -> Option<String> {
918 docs_default_html_output_rel(rel_path)
919}
920
921/// Performs docs HTML output rel with remap.
922///
923/// # Parameters
924///
925/// - `rel_path`: Input consumed by [`crate::docs::docs_html_output_rel_with_remap`].
926/// - `remap`: Input consumed by [`crate::docs::docs_html_output_rel_with_remap`].
927///
928/// # Returns
929///
930/// Returns [`Some`] when [`crate::docs::docs_html_output_rel_with_remap`] can produce a value and [`None`] otherwise.
931///
932/// # Examples
933///
934/// ```rust,ignore
935/// let _result = pharos_runtime_docs::docs::docs_html_output_rel_with_remap(todo!(), todo!());
936/// ```
937///
938/// # Related
939///
940/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
941pub fn docs_html_output_rel_with_remap(
942 rel_path: &str,
943 remap: Option<&DocsOutputRemap>,
944) -> Option<String> {
945 if rel_path.is_empty() {
946 return None;
947 }
948 if rel_path.ends_with(".html") {
949 return Some(rel_path.to_owned());
950 }
951 remap
952 .and_then(|value| docs_output_remap_lookup_source(value, rel_path))
953 .map(ToOwned::to_owned)
954 .or_else(|| docs_default_html_output_rel(rel_path))
955}
956
957/// Performs docs page depth.
958///
959/// # Parameters
960///
961/// - `page_rel`: Input consumed by [`crate::docs::docs_page_depth`].
962///
963/// # Returns
964///
965/// Returns the computed value produced by [`crate::docs::docs_page_depth`].
966///
967/// # Performance
968///
969/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_page_depth`].
970///
971/// # Examples
972///
973/// ```rust,ignore
974/// let _result = pharos_runtime_docs::docs::docs_page_depth(todo!());
975/// ```
976///
977/// # Related
978///
979/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
980pub fn docs_page_depth(page_rel: &str) -> usize {
981 page_rel.bytes().filter(|byte| *byte == b'/').count()
982}
983
984/// Performs docs relative prefix.
985///
986/// # Parameters
987///
988/// - `depth`: Input consumed by [`crate::docs::docs_relative_prefix`].
989///
990/// # Returns
991///
992/// Returns [`Some`] when [`crate::docs::docs_relative_prefix`] can produce a value and [`None`] otherwise.
993///
994/// # Examples
995///
996/// ```rust,ignore
997/// let _result = pharos_runtime_docs::docs::docs_relative_prefix(todo!());
998/// ```
999///
1000/// # Related
1001///
1002/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1003pub fn docs_relative_prefix(depth: usize) -> Option<String> {
1004 (depth <= MAX_DOC_FILES).then(|| "../".repeat(depth))
1005}
1006
1007/// Performs docs root href.
1008///
1009/// # Parameters
1010///
1011/// - `page_rel`: Input consumed by [`crate::docs::docs_root_href`].
1012///
1013/// # Returns
1014///
1015/// Returns [`Some`] when [`crate::docs::docs_root_href`] can produce a value and [`None`] otherwise.
1016///
1017/// # Examples
1018///
1019/// ```rust,ignore
1020/// let _result = pharos_runtime_docs::docs::docs_root_href(todo!());
1021/// ```
1022///
1023/// # Related
1024///
1025/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1026pub fn docs_root_href(page_rel: &str) -> Option<String> {
1027 if page_rel.is_empty() {
1028 return None;
1029 }
1030 let depth = docs_page_depth(page_rel);
1031 if depth == 0 {
1032 Some("index.html".to_owned())
1033 } else {
1034 Some(format!("{}index.html", docs_relative_prefix(depth)?))
1035 }
1036}
1037
1038/// Builds the relative href from output.
1039fn relative_href_from_output(from_page: &str, target: &str) -> Option<String> {
1040 let from_dir = from_page
1041 .rfind('/')
1042 .map(|index| &from_page[..=index])
1043 .unwrap_or("");
1044 let mut common = 0usize;
1045 let from_bytes = from_dir.as_bytes();
1046 let target_bytes = target.as_bytes();
1047 while common < from_bytes.len()
1048 && common < target_bytes.len()
1049 && from_bytes[common] == target_bytes[common]
1050 {
1051 common += 1;
1052 }
1053 while common > 0 && from_dir.as_bytes().get(common - 1) != Some(&b'/') {
1054 common -= 1;
1055 }
1056 let up = from_dir[common..]
1057 .bytes()
1058 .filter(|byte| *byte == b'/')
1059 .count();
1060 Some(format!(
1061 "{}{}",
1062 "../".repeat(up),
1063 &target[common.min(target.len())..]
1064 ))
1065}
1066
1067/// Resolves docs relative target.
1068fn resolve_docs_relative_target(page_rel: &str, target: &str) -> Option<String> {
1069 if page_rel.is_empty() || target.is_empty() {
1070 return None;
1071 }
1072 let page_dir = Path::new(page_rel)
1073 .parent()
1074 .unwrap_or_else(|| Path::new(""));
1075 let candidate = page_dir.join(target);
1076 let mut segments: Vec<String> = Vec::new();
1077 for component in candidate.components() {
1078 match component {
1079 std::path::Component::CurDir => {}
1080 std::path::Component::Normal(segment) => {
1081 segments.push(segment.to_str()?.to_owned());
1082 }
1083 std::path::Component::ParentDir => {
1084 segments.pop()?;
1085 }
1086 std::path::Component::RootDir | std::path::Component::Prefix(_) => return None,
1087 }
1088 }
1089 (!segments.is_empty()).then(|| segments.join("/"))
1090}
1091
1092#[allow(non_snake_case)]
1093/// Performs CLOSE LIST.
1094///
1095/// # Parameters
1096///
1097/// - `output`: Input consumed by [`crate::docs::CLOSE_LIST`].
1098/// - `ordered`: Input consumed by [`crate::docs::CLOSE_LIST`].
1099///
1100/// # Returns
1101///
1102/// Returns `true` when the condition checked by [`crate::docs::CLOSE_LIST`] holds and `false` otherwise.
1103///
1104/// # Examples
1105///
1106/// ```rust,ignore
1107/// let _result = pharos_runtime_docs::docs::CLOSE_LIST(todo!(), todo!());
1108/// ```
1109///
1110/// # Related
1111///
1112/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1113pub fn CLOSE_LIST(output: &mut String, ordered: bool) -> bool {
1114 if ordered {
1115 dbuf_append(output, "</ol>\n")
1116 } else {
1117 dbuf_append(output, "</ul>\n")
1118 }
1119}
1120
1121/// Performs docs markdown to HTML.
1122///
1123/// # Parameters
1124///
1125/// - `markdown_text`: Input consumed by [`crate::docs::docs_markdown_to_html`].
1126///
1127/// # Returns
1128///
1129/// Returns [`Some`] when [`crate::docs::docs_markdown_to_html`] can produce a value and [`None`] otherwise.
1130///
1131/// # Examples
1132///
1133/// ```rust,ignore
1134/// let _result = pharos_runtime_docs::docs::docs_markdown_to_html(todo!());
1135/// ```
1136///
1137/// # Related
1138///
1139/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1140pub fn docs_markdown_to_html(markdown_text: &str) -> Option<String> {
1141 docs_markdown_to_html_with_remap_for_page(markdown_text, None, "index.html")
1142}
1143
1144/// Performs docs markdown to HTML with remap.
1145///
1146/// # Parameters
1147///
1148/// - `markdown_text`: Input consumed by [`crate::docs::docs_markdown_to_html_with_remap`].
1149/// - `remap`: Input consumed by [`crate::docs::docs_markdown_to_html_with_remap`].
1150///
1151/// # Returns
1152///
1153/// Returns [`Some`] when [`crate::docs::docs_markdown_to_html_with_remap`] can produce a value and [`None`] otherwise.
1154///
1155/// # Examples
1156///
1157/// ```rust,ignore
1158/// let _result = pharos_runtime_docs::docs::docs_markdown_to_html_with_remap(todo!(), todo!());
1159/// ```
1160///
1161/// # Related
1162///
1163/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1164pub fn docs_markdown_to_html_with_remap(
1165 markdown_text: &str,
1166 remap: Option<&DocsOutputRemap>,
1167) -> Option<String> {
1168 docs_markdown_to_html_with_remap_for_page(markdown_text, remap, "index.html")
1169}
1170
1171/// Appends one required docs fragment and converts failure into a render failure.
1172fn append_docs_text(output: &mut String, value: &str) -> Option<()> {
1173 dbuf_append(output, value).then_some(())
1174}
1175
1176/// Converts the supported Markdown and authored HTML mixture used by Pharos docs.
1177///
1178/// # Parameters
1179///
1180/// - `markdown_text`: Input consumed by [`crate::docs::docs_markdown_to_html_with_remap_for_page`].
1181/// - `remap`: Input consumed by [`crate::docs::docs_markdown_to_html_with_remap_for_page`].
1182/// - `page_rel`: Input consumed by [`crate::docs::docs_markdown_to_html_with_remap_for_page`].
1183///
1184/// # Returns
1185///
1186/// Returns [`Some`] when [`crate::docs::docs_markdown_to_html_with_remap_for_page`] can produce a value and [`None`] otherwise.
1187///
1188/// # Performance
1189///
1190/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_markdown_to_html_with_remap_for_page`].
1191///
1192/// # Examples
1193///
1194/// ```rust,ignore
1195/// let _result = pharos_runtime_docs::docs::docs_markdown_to_html_with_remap_for_page(todo!(), todo!(), todo!());
1196/// ```
1197///
1198/// # Related
1199///
1200/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1201pub fn docs_markdown_to_html_with_remap_for_page(
1202 markdown_text: &str,
1203 remap: Option<&DocsOutputRemap>,
1204 page_rel: &str,
1205) -> Option<String> {
1206 if markdown_text.len() > MAX_DOC_TEXT_BYTES || page_rel.is_empty() {
1207 return None;
1208 }
1209 let mut output = String::new();
1210 let mut paragraph = String::new();
1211 let mut list_kind = None;
1212 let mut code_fence = None;
1213 for line in markdown_text.lines() {
1214 let trimmed = line.trim_end();
1215 if let Some(language) = trimmed.strip_prefix("```") {
1216 if code_fence.is_some() {
1217 append_docs_text(&mut output, "</code></pre>\n")?;
1218 code_fence = None;
1219 } else {
1220 flush_docs_paragraph(&mut output, &mut paragraph, page_rel, remap)?;
1221 close_docs_list(&mut output, &mut list_kind);
1222 let language = language.trim();
1223 if language.len() > 32
1224 || !language
1225 .bytes()
1226 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
1227 {
1228 return None;
1229 }
1230 if language.is_empty() {
1231 append_docs_text(&mut output, "<pre><code>")?;
1232 } else {
1233 append_docs_text(
1234 &mut output,
1235 &format!("<pre><code class=\"language-{language}\">"),
1236 )?;
1237 }
1238 code_fence = Some(language.to_owned());
1239 }
1240 continue;
1241 }
1242 if code_fence.is_some() {
1243 append_docs_text(&mut output, &html_escape(line))?;
1244 append_docs_text(&mut output, "\n")?;
1245 continue;
1246 }
1247 if let Some((level, title)) = markdown_heading(trimmed) {
1248 flush_docs_paragraph(&mut output, &mut paragraph, page_rel, remap)?;
1249 close_docs_list(&mut output, &mut list_kind);
1250 append_docs_text(&mut output, &format!("<h{level}"))?;
1251 append_docs_text(&mut output, ">")?;
1252 append_inline_markup_for_page(&mut output, title, page_rel, remap)?;
1253 append_docs_text(&mut output, &format!("</h{level}>\n"))?;
1254 continue;
1255 }
1256 if let Some((ordered, item)) = markdown_list_item(trimmed) {
1257 flush_docs_paragraph(&mut output, &mut paragraph, page_rel, remap)?;
1258 if list_kind.is_some_and(|current| current != ordered) {
1259 close_docs_list(&mut output, &mut list_kind);
1260 }
1261 if list_kind.is_none() {
1262 append_docs_text(&mut output, if ordered { "<ol>\n" } else { "<ul>\n" })?;
1263 list_kind = Some(ordered);
1264 }
1265 append_docs_text(&mut output, "<li>")?;
1266 append_inline_markup_for_page(&mut output, item, page_rel, remap)?;
1267 append_docs_text(&mut output, "</li>\n")?;
1268 continue;
1269 }
1270 if trimmed.is_empty() {
1271 flush_docs_paragraph(&mut output, &mut paragraph, page_rel, remap)?;
1272 close_docs_list(&mut output, &mut list_kind);
1273 continue;
1274 }
1275 if trimmed.starts_with('<') {
1276 flush_docs_paragraph(&mut output, &mut paragraph, page_rel, remap)?;
1277 close_docs_list(&mut output, &mut list_kind);
1278 let rewritten = rewrite_docs_html_targets(trimmed, page_rel, remap)?;
1279 append_docs_text(&mut output, &rewritten)?;
1280 append_docs_text(&mut output, "\n")?;
1281 continue;
1282 }
1283 if !paragraph.is_empty() {
1284 paragraph.push(' ');
1285 }
1286 paragraph.push_str(trimmed);
1287 }
1288 if code_fence.is_some() {
1289 return None;
1290 }
1291 flush_docs_paragraph(&mut output, &mut paragraph, page_rel, remap)?;
1292 close_docs_list(&mut output, &mut list_kind);
1293 Some(output)
1294}
1295
1296/// Extracts one supported unordered or ordered Markdown list item.
1297fn markdown_list_item(line: &str) -> Option<(bool, &str)> {
1298 if let Some(item) = line.strip_prefix("- ") {
1299 return Some((false, item));
1300 }
1301 let (number, item) = line.split_once(". ")?;
1302 (!number.is_empty() && number.bytes().all(|byte| byte.is_ascii_digit())).then_some((true, item))
1303}
1304
1305/// Extracts a Markdown heading level and its title from one source line.
1306fn markdown_heading(line: &str) -> Option<(usize, &str)> {
1307 let level = line.bytes().take_while(|byte| *byte == b'#').count();
1308 (1..=6)
1309 .contains(&level)
1310 .then(|| {
1311 line.get(level..)?
1312 .strip_prefix(' ')
1313 .map(|title| (level, title.trim()))
1314 })
1315 .flatten()
1316}
1317
1318/// Flushes one paragraph through the page-aware inline renderer.
1319fn flush_docs_paragraph(
1320 output: &mut String,
1321 paragraph: &mut String,
1322 page_rel: &str,
1323 remap: Option<&DocsOutputRemap>,
1324) -> Option<()> {
1325 if paragraph.trim().is_empty() {
1326 paragraph.clear();
1327 return Some(());
1328 }
1329 append_docs_text(output, "<p>")?;
1330 append_inline_markup_for_page(output, paragraph.trim(), page_rel, remap)?;
1331 append_docs_text(output, "</p>\n")?;
1332 paragraph.clear();
1333 Some(())
1334}
1335
1336/// Closes an open Markdown list.
1337fn close_docs_list(output: &mut String, list_kind: &mut Option<bool>) {
1338 if let Some(ordered) = list_kind.take() {
1339 let _ = dbuf_append(output, if ordered { "</ol>\n" } else { "</ul>\n" });
1340 }
1341}
1342
1343/// Renders inline Markdown while rewriting local links for one output page.
1344fn append_inline_markup_for_page(
1345 output: &mut String,
1346 text: &str,
1347 page_rel: &str,
1348 remap: Option<&DocsOutputRemap>,
1349) -> Option<()> {
1350 if text.len() > 256 * 1024 {
1351 return None;
1352 }
1353 let mut cursor = 0usize;
1354 while cursor < text.len() {
1355 let remainder = &text[cursor..];
1356 if let Some(close) = remainder
1357 .strip_prefix('`')
1358 .and_then(|value| value.find('`'))
1359 {
1360 append_docs_text(output, "<code>")?;
1361 append_docs_text(output, &html_escape(&remainder[1..close + 1]))?;
1362 append_docs_text(output, "</code>")?;
1363 cursor += close + 2;
1364 continue;
1365 }
1366 if let Some(close) = remainder
1367 .strip_prefix("**")
1368 .and_then(|value| value.find("**"))
1369 {
1370 append_docs_text(output, "<strong>")?;
1371 append_inline_markup_for_page(output, &remainder[2..close + 2], page_rel, remap)?;
1372 append_docs_text(output, "</strong>")?;
1373 cursor += close + 4;
1374 continue;
1375 }
1376 if remainder.starts_with('[')
1377 && let Some(label_end) = remainder.find("](")
1378 && let Some(target_end) = remainder[label_end + 2..].find(')')
1379 {
1380 let target_end = label_end + 2 + target_end;
1381 let target = &remainder[label_end + 2..target_end];
1382 let href = rewrite_docs_target(target, page_rel, remap)?;
1383 append_docs_text(output, "<a href=\"")?;
1384 append_docs_text(output, &html_escape(&href))?;
1385 append_docs_text(output, "\">")?;
1386 append_inline_markup_for_page(output, &remainder[1..label_end], page_rel, remap)?;
1387 append_docs_text(output, "</a>")?;
1388 cursor += target_end + 1;
1389 continue;
1390 }
1391 let next = [
1392 remainder.find('`'),
1393 remainder.find("**"),
1394 remainder.find('['),
1395 ]
1396 .into_iter()
1397 .flatten()
1398 .min()
1399 .unwrap_or(remainder.len());
1400 append_docs_text(output, &html_escape(&remainder[..next]))?;
1401 cursor += next.max(1);
1402 }
1403 Some(())
1404}
1405
1406/// Rewrites local HTML attribute targets relative to a generated docs page.
1407fn rewrite_docs_html_targets(
1408 line: &str,
1409 page_rel: &str,
1410 remap: Option<&DocsOutputRemap>,
1411) -> Option<String> {
1412 let mut output = String::with_capacity(line.len() + 32);
1413 let mut cursor = 0usize;
1414 while cursor < line.len() {
1415 let remainder = &line[cursor..];
1416 let candidates = [remainder.find("src=\""), remainder.find("href=\"")];
1417 let Some(offset) = candidates.into_iter().flatten().min() else {
1418 output.push_str(remainder);
1419 break;
1420 };
1421 let attribute_end = offset + 5;
1422 output.push_str(&remainder[..attribute_end]);
1423 let value_start = attribute_end;
1424 let value = &remainder[value_start..];
1425 let close = value.find('\"')?;
1426 let target = &value[..close];
1427 output.push_str(&html_escape(&rewrite_docs_target(target, page_rel, remap)?));
1428 output.push('\"');
1429 cursor += value_start + close + 1;
1430 }
1431 Some(output)
1432}
1433
1434/// Rewrites one safe local docs link or asset target.
1435fn rewrite_docs_target(
1436 target: &str,
1437 page_rel: &str,
1438 remap: Option<&DocsOutputRemap>,
1439) -> Option<String> {
1440 let (base, suffix) = target.split_once('#').map_or((target, ""), |(base, hash)| {
1441 (base, &target[target.len() - hash.len() - 1..])
1442 });
1443 if base.is_empty()
1444 || base.starts_with('/')
1445 || base.starts_with('#')
1446 || base.contains("://")
1447 || base.starts_with("mailto:")
1448 || base.starts_with("javascript:")
1449 || base.starts_with("data:")
1450 {
1451 return Some(target.to_owned());
1452 }
1453 let resolved_base =
1454 resolve_docs_relative_target(page_rel, base).unwrap_or_else(|| base.to_owned());
1455 let output_target = if resolved_base.ends_with(".md") {
1456 docs_html_output_rel_with_remap(&resolved_base, remap)?
1457 } else {
1458 resolved_base
1459 };
1460 Some(format!(
1461 "{}{}",
1462 relative_href_from_output(page_rel, &output_target)?,
1463 suffix
1464 ))
1465}
1466
1467/// Renders template value.
1468fn render_template_value(template: &str, values: &[(&str, &str, bool)]) -> Option<String> {
1469 let mut output = String::with_capacity(template.len() + 128);
1470 let mut cursor = 0usize;
1471 while cursor < template.len() {
1472 let Some(relative_open) = template[cursor..].find('{') else {
1473 output.push_str(&template[cursor..]);
1474 break;
1475 };
1476 let open = cursor + relative_open;
1477 output.push_str(&template[cursor..open]);
1478 let key_start = open + 1;
1479 let close = template[key_start..].find('}')? + key_start;
1480 let raw_key = &template[key_start..close];
1481 let (key, explicit_html) = raw_key
1482 .strip_prefix("html:")
1483 .map_or((raw_key, false), |value| (value, true));
1484 if let Some((_, value, is_html)) = values.iter().find(|(name, _, _)| *name == key) {
1485 if explicit_html || *is_html {
1486 output.push_str(value);
1487 } else {
1488 output.push_str(&html_escape(value));
1489 }
1490 } else {
1491 output.push_str(&template[open..=close]);
1492 }
1493 if output.len() > MAX_DOC_TEXT_BYTES {
1494 return None;
1495 }
1496 cursor = close + 1;
1497 }
1498 Some(output)
1499}
1500
1501/// Performs docs render sidebar.
1502///
1503/// # Parameters
1504///
1505/// - `sidebar_template_html`: Input consumed by [`crate::docs::docs_render_sidebar`].
1506/// - `sidebar_item_template_html`: Input consumed by [`crate::docs::docs_render_sidebar`].
1507/// - `sidebar_title`: Input consumed by [`crate::docs::docs_render_sidebar`].
1508/// - `nav_entries`: Input consumed by [`crate::docs::docs_render_sidebar`].
1509/// - `current_page_html_rel`: Input consumed by [`crate::docs::docs_render_sidebar`].
1510///
1511/// # Returns
1512///
1513/// Returns [`Some`] when [`crate::docs::docs_render_sidebar`] can produce a value and [`None`] otherwise.
1514///
1515/// # Performance
1516///
1517/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_render_sidebar`].
1518///
1519/// # Examples
1520///
1521/// ```rust,ignore
1522/// let _result = pharos_runtime_docs::docs::docs_render_sidebar(todo!(), todo!(), todo!(), todo!(), todo!());
1523/// ```
1524///
1525/// # Related
1526///
1527/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1528pub fn docs_render_sidebar(
1529 sidebar_template_html: &str,
1530 sidebar_item_template_html: &str,
1531 sidebar_group_template_html: &str,
1532 sidebar_title: &str,
1533 nav_entries: &[DocsNavEntry],
1534 current_page_html_rel: &str,
1535) -> Option<String> {
1536 if sidebar_template_html.is_empty()
1537 || sidebar_item_template_html.is_empty()
1538 || current_page_html_rel.is_empty()
1539 {
1540 return None;
1541 }
1542 let nav_nodes = docs_nav_tree(nav_entries)?;
1543 let mut items = String::new();
1544 let mut group_id = 0usize;
1545 for node in nav_nodes.iter().take(MAX_DOC_FILES) {
1546 let item = docs_render_sidebar_node(
1547 sidebar_item_template_html,
1548 sidebar_group_template_html,
1549 node,
1550 current_page_html_rel,
1551 &mut group_id,
1552 )?;
1553 items.push_str(&item);
1554 if items.len() > MAX_DOC_TEXT_BYTES {
1555 return None;
1556 }
1557 }
1558 let title_block = if sidebar_title.is_empty() {
1559 String::new()
1560 } else {
1561 format!(
1562 "<div class=\"docs-sidebar-title\">{}</div>\n",
1563 html_escape(sidebar_title)
1564 )
1565 };
1566 render_template_value(
1567 sidebar_template_html,
1568 &[
1569 ("title", sidebar_title, false),
1570 ("title_block", &title_block, true),
1571 ("items", &items, true),
1572 ],
1573 )
1574}
1575
1576fn docs_nav_tree(nav_entries: &[DocsNavEntry]) -> Option<Vec<DocsNavNode>> {
1577 let mut index = 0usize;
1578 docs_nav_parse_nodes(nav_entries, &mut index, 0)
1579}
1580
1581fn docs_nav_parse_nodes(
1582 nav_entries: &[DocsNavEntry],
1583 index: &mut usize,
1584 depth: usize,
1585) -> Option<Vec<DocsNavNode>> {
1586 let mut nodes = Vec::new();
1587 while *index < nav_entries.len() {
1588 let entry = nav_entries.get(*index)?;
1589 if entry.depth < depth {
1590 break;
1591 }
1592 if entry.depth > depth {
1593 return None;
1594 }
1595 let mut node = DocsNavNode {
1596 path: entry.path.clone(),
1597 label: entry.label.clone(),
1598 depth: entry.depth,
1599 children: Vec::new(),
1600 };
1601 *index += 1;
1602 if *index < nav_entries.len() && nav_entries.get(*index)?.depth > depth {
1603 node.children = docs_nav_parse_nodes(nav_entries, index, depth + 1)?;
1604 }
1605 nodes.push(node);
1606 }
1607 Some(nodes)
1608}
1609
1610fn docs_sidebar_href(current_page_html_rel: &str, path: &str) -> Option<String> {
1611 if path == "index.html" {
1612 let depth = docs_page_depth(current_page_html_rel);
1613 if depth == 0 {
1614 Some("index.html".to_owned())
1615 } else {
1616 Some(format!("{}index.html", "../".repeat(depth)))
1617 }
1618 } else {
1619 relative_href_from_output(current_page_html_rel, path)
1620 }
1621}
1622
1623fn docs_nav_node_contains_current(node: &DocsNavNode, current_page_html_rel: &str) -> bool {
1624 node.path == current_page_html_rel
1625 || node
1626 .children
1627 .iter()
1628 .any(|child| docs_nav_node_contains_current(child, current_page_html_rel))
1629}
1630
1631fn docs_nav_default_open(node: &DocsNavNode) -> bool {
1632 node.depth == 0 && node.label == "Start Here"
1633}
1634
1635fn docs_render_sidebar_node(
1636 sidebar_item_template_html: &str,
1637 sidebar_group_template_html: &str,
1638 node: &DocsNavNode,
1639 current_page_html_rel: &str,
1640 group_id: &mut usize,
1641) -> Option<String> {
1642 let href = docs_sidebar_href(current_page_html_rel, &node.path)?;
1643 let current = node.path == current_page_html_rel;
1644 let contains_current = docs_nav_node_contains_current(node, current_page_html_rel);
1645 let depth_class = format!("docs-depth-{}", node.depth);
1646 let active_suffix = if current { " is-active" } else { "" };
1647 let active_attr = if current {
1648 " aria-current=\"page\""
1649 } else {
1650 ""
1651 };
1652 let item_link = render_template_value(
1653 sidebar_item_template_html,
1654 &[
1655 ("href", &href, false),
1656 ("label", &node.label, false),
1657 ("depth_class", &depth_class, false),
1658 ("active_suffix", active_suffix, false),
1659 ("active_attr", active_attr, true),
1660 ],
1661 )?;
1662 if node.children.is_empty() {
1663 return Some(item_link);
1664 }
1665 *group_id = group_id.saturating_add(1);
1666 let current_group_id = format!("docs-nav-group-{}", group_id);
1667 let is_open = docs_nav_default_open(node) || contains_current;
1668 let expanded_attr = if is_open { "true" } else { "false" };
1669 let open_class = if is_open { " is-open" } else { "" };
1670 let mut children_html = String::new();
1671 for child in node.children.iter().take(MAX_DOC_FILES) {
1672 let child_html = docs_render_sidebar_node(
1673 sidebar_item_template_html,
1674 sidebar_group_template_html,
1675 child,
1676 current_page_html_rel,
1677 group_id,
1678 )?;
1679 children_html.push_str(&child_html);
1680 if children_html.len() > MAX_DOC_TEXT_BYTES {
1681 return None;
1682 }
1683 }
1684 render_template_value(
1685 sidebar_group_template_html,
1686 &[
1687 ("depth", &node.depth.to_string(), false),
1688 ("open_class", open_class, false),
1689 ("expanded", expanded_attr, false),
1690 ("group_id", ¤t_group_id, false),
1691 ("label", &node.label, false),
1692 ("item_link", &item_link, true),
1693 ("children", &children_html, true),
1694 ],
1695 )
1696}
1697
1698#[allow(clippy::too_many_arguments)]
1699/// Performs docs render page.
1700///
1701/// # Parameters
1702///
1703/// - `template_html`: Input consumed by [`crate::docs::docs_render_page`].
1704/// - `title`: Input consumed by [`crate::docs::docs_render_page`].
1705/// - `sidebar_html`: Input consumed by [`crate::docs::docs_render_page`].
1706/// - `body_html`: Input consumed by [`crate::docs::docs_render_page`].
1707/// - `back_href`: Input consumed by [`crate::docs::docs_render_page`].
1708/// - `wordmark_src`: Input consumed by [`crate::docs::docs_render_page`].
1709/// - `favicon_src`: Input consumed by [`crate::docs::docs_render_page`].
1710/// - `stylesheet_src`: Input consumed by [`crate::docs::docs_render_page`].
1711///
1712/// # Returns
1713///
1714/// Returns [`Some`] when [`crate::docs::docs_render_page`] can produce a value and [`None`] otherwise.
1715///
1716/// # Examples
1717///
1718/// ```rust,ignore
1719/// let _result = pharos_runtime_docs::docs::docs_render_page(todo!(), todo!(), todo!(), todo!(), todo!(), todo!(), todo!(), todo!());
1720/// ```
1721///
1722/// # Related
1723///
1724/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1725pub fn docs_render_page(
1726 template_html: &str,
1727 title: &str,
1728 sidebar_html: &str,
1729 body_html: &str,
1730 back_href: &str,
1731 wordmark_src: &str,
1732 favicon_src: &str,
1733 stylesheet_src: &str,
1734) -> Option<String> {
1735 if template_html.is_empty() {
1736 return None;
1737 }
1738 render_template_value(
1739 template_html,
1740 &[
1741 ("title", title, false),
1742 ("sidebar", sidebar_html, true),
1743 ("body", body_html, true),
1744 ("back_href", back_href, false),
1745 ("wordmark_src", wordmark_src, false),
1746 ("favicon_src", favicon_src, false),
1747 ("stylesheet_src", stylesheet_src, false),
1748 ],
1749 )
1750}
1751
1752/// Collects md files.
1753///
1754/// # Parameters
1755///
1756/// - `dir_path`: Input consumed by [`crate::docs::collect_md_files`].
1757/// - `base_prefix`: Input consumed by [`crate::docs::collect_md_files`].
1758/// - `depth`: Input consumed by [`crate::docs::collect_md_files`].
1759/// - `max_depth`: Input consumed by [`crate::docs::collect_md_files`].
1760/// - `files`: Input consumed by [`crate::docs::collect_md_files`].
1761///
1762/// # Returns
1763///
1764/// Returns `true` when the condition checked by [`crate::docs::collect_md_files`] holds and `false` otherwise.
1765///
1766/// # Performance
1767///
1768/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::collect_md_files`].
1769///
1770/// # Examples
1771///
1772/// ```rust,ignore
1773/// let _result = pharos_runtime_docs::docs::collect_md_files(todo!(), todo!(), todo!(), todo!(), todo!());
1774/// ```
1775///
1776/// # Related
1777///
1778/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1779pub fn collect_md_files(
1780 dir_path: &Path,
1781 base_prefix: &str,
1782 depth: usize,
1783 max_depth: usize,
1784 files: &mut Vec<(String, PathBuf)>,
1785) -> bool {
1786 if depth > max_depth || files.len() > MAX_DOC_FILES {
1787 return false;
1788 }
1789 let Ok(entries) = fs::read_dir(dir_path) else {
1790 return true;
1791 };
1792 for entry in entries {
1793 let Ok(entry) = entry else {
1794 return false;
1795 };
1796 let path = entry.path();
1797 let Some(name) = entry.file_name().to_str().map(ToOwned::to_owned) else {
1798 continue;
1799 };
1800 if name == "." || name == ".." {
1801 continue;
1802 }
1803 let relative = if base_prefix.is_empty() {
1804 name.clone()
1805 } else {
1806 format!("{base_prefix}/{name}")
1807 };
1808 if path.is_dir() {
1809 if !collect_md_files(&path, &relative, depth + 1, max_depth, files) {
1810 return false;
1811 }
1812 } else if name.ends_with(".md") {
1813 if files.len() >= MAX_DOC_FILES {
1814 return false;
1815 }
1816 files.push((relative, path));
1817 }
1818 }
1819 true
1820}
1821
1822/// Performs docs page title.
1823///
1824/// # Parameters
1825///
1826/// - `markdown_text`: Input consumed by [`crate::docs::docs_page_title`].
1827/// - `rel_path`: Input consumed by [`crate::docs::docs_page_title`].
1828///
1829/// # Returns
1830///
1831/// Returns [`Some`] when [`crate::docs::docs_page_title`] can produce a value and [`None`] otherwise.
1832///
1833/// # Performance
1834///
1835/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_page_title`].
1836///
1837/// # Examples
1838///
1839/// ```rust,ignore
1840/// let _result = pharos_runtime_docs::docs::docs_page_title(todo!(), todo!());
1841/// ```
1842///
1843/// # Related
1844///
1845/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1846pub fn docs_page_title(markdown_text: Option<&str>, rel_path: Option<&str>) -> Option<String> {
1847 if let Some(text) = markdown_text {
1848 for line in text.lines() {
1849 if let Some(title) = line
1850 .strip_prefix("# ")
1851 .map(str::trim)
1852 .filter(|value| !value.is_empty())
1853 {
1854 return Some(title.to_owned());
1855 }
1856 }
1857 }
1858 let path = rel_path.unwrap_or_default();
1859 let name = Path::new(path)
1860 .file_name()
1861 .and_then(|value| value.to_str())
1862 .unwrap_or("");
1863 if name.is_empty() {
1864 return Some("Pharos Documentation".to_owned());
1865 }
1866 if matches!(name, "README.md" | "index.md") {
1867 return if let Some(parent) = Path::new(path)
1868 .parent()
1869 .filter(|value| !value.as_os_str().is_empty())
1870 {
1871 Some(
1872 parent
1873 .file_name()?
1874 .to_string_lossy()
1875 .replace(['-', '_'], " "),
1876 )
1877 } else {
1878 Some("Documentation".to_owned())
1879 };
1880 }
1881 let stem = name.strip_suffix(".md").unwrap_or(name);
1882 let mut chars = stem.chars();
1883 let mut title = chars
1884 .next()
1885 .map(|value| value.to_ascii_uppercase())
1886 .into_iter()
1887 .chain(chars)
1888 .collect::<String>();
1889 title = title.replace(['-', '_'], " ");
1890 Some(title)
1891}
1892
1893/// Performs docs nav collect entries local.
1894///
1895/// # Parameters
1896///
1897/// - `items`: Input consumed by [`crate::docs::docs_nav_collect_entries_local`].
1898/// - `depth`: Input consumed by [`crate::docs::docs_nav_collect_entries_local`].
1899/// - `entries`: Input consumed by [`crate::docs::docs_nav_collect_entries_local`].
1900///
1901/// # Returns
1902///
1903/// Returns `true` when the condition checked by [`crate::docs::docs_nav_collect_entries_local`] holds and `false` otherwise.
1904///
1905/// # Performance
1906///
1907/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::docs::docs_nav_collect_entries_local`].
1908///
1909/// # Examples
1910///
1911/// ```rust,ignore
1912/// let _result = pharos_runtime_docs::docs::docs_nav_collect_entries_local(todo!(), todo!(), todo!());
1913/// ```
1914///
1915/// # Related
1916///
1917/// - [`crate::docs`] for adjacent helpers that share the same runtime surface.
1918pub fn docs_nav_collect_entries_local(
1919 items: &Value,
1920 depth: usize,
1921 entries: &mut Vec<DocsNavEntry>,
1922) -> bool {
1923 let Some(items) = items.as_array() else {
1924 return true;
1925 };
1926 for item in items {
1927 if let (Some(path), Some(label)) = (
1928 item.get("path")
1929 .and_then(Value::as_str)
1930 .filter(|value| !value.is_empty()),
1931 item.get("label")
1932 .and_then(Value::as_str)
1933 .filter(|value| !value.is_empty()),
1934 ) {
1935 if entries.len() >= MAX_DOC_FILES {
1936 return false;
1937 }
1938 let Some(path) = docs_html_output_rel_with_remap(path, None) else {
1939 return false;
1940 };
1941 entries.push(DocsNavEntry {
1942 path,
1943 label: label.to_owned(),
1944 depth,
1945 });
1946 }
1947 if let Some(children) = item.get("children")
1948 && !docs_nav_collect_entries_local(children, depth + 1, entries)
1949 {
1950 return false;
1951 }
1952 }
1953 true
1954}
1955
1956#[cfg(test)]
1957mod tests {
1958 use super::{
1959 docs_markdown_to_html, docs_markdown_to_html_with_remap_for_page, docs_root_href,
1960 docs_sidebar_href,
1961 };
1962
1963 /// Keeps authored HTML inert and readable inside fenced examples.
1964 #[test]
1965 fn markdown_renders_fenced_code_as_escaped_preformatted_text() {
1966 let html = docs_markdown_to_html("```html\n<form action=\"/cart\">\n```\n")
1967 .expect("rendered markdown");
1968 assert_eq!(
1969 html,
1970 "<pre><code class=\"language-html\"><form action="/cart">\n</code></pre>\n"
1971 );
1972 }
1973
1974 /// Preserves the semantic distinction between numbered steps and bullets.
1975 #[test]
1976 fn markdown_renders_ordered_and_unordered_lists_separately() {
1977 let html = docs_markdown_to_html("1. Run demo\n2. Add pack\n\n- Page\n- Form\n")
1978 .expect("rendered markdown");
1979 assert!(html.contains("<ol>\n<li>Run demo</li>\n<li>Add pack</li>\n</ol>"));
1980 assert!(html.contains("<ul>\n<li>Page</li>\n<li>Form</li>\n</ul>"));
1981 }
1982
1983 #[test]
1984 /// Performs markdown rewrites nested relative links against the current page.
1985 fn markdown_rewrites_nested_relative_links_against_the_current_page() {
1986 let html = docs_markdown_to_html_with_remap_for_page(
1987 "Continue into [Why Pharos changes the app boundary](../why-pharos.md).\n",
1988 None,
1989 "tutorials/ucal.html",
1990 )
1991 .expect("rendered markdown");
1992 assert!(
1993 html.contains("<a href=\"../why-pharos.html\">Why Pharos changes the app boundary</a>")
1994 );
1995 assert!(!html.contains("../../why-pharos.html"));
1996 }
1997
1998 #[test]
1999 /// Performs docs root href matches mounted docs home route shape.
2000 fn docs_root_href_matches_mounted_docs_home_route_shape() {
2001 assert_eq!(docs_root_href("index.html").as_deref(), Some("index.html"));
2002 assert_eq!(
2003 docs_root_href("why-pharos.html").as_deref(),
2004 Some("index.html")
2005 );
2006 assert_eq!(
2007 docs_root_href("tutorials/ucal.html").as_deref(),
2008 Some("../index.html")
2009 );
2010 assert_eq!(
2011 docs_root_href("reference/library-reference.html").as_deref(),
2012 Some("../index.html")
2013 );
2014 }
2015
2016 #[test]
2017 /// Keeps generated sidebar home links explicit instead of rendering dot-path hrefs.
2018 fn docs_sidebar_href_uses_explicit_home_file() {
2019 assert_eq!(
2020 docs_sidebar_href("index.html", "index.html").as_deref(),
2021 Some("index.html")
2022 );
2023 assert_eq!(
2024 docs_sidebar_href("reference/index.html", "index.html").as_deref(),
2025 Some("../index.html")
2026 );
2027 }
2028}