Skip to main content

rl_lang/docs/
mod.rs

1//! In-process documentation system for the `rl docs` CLI command.
2//!
3//! Stores all language reference material as static data compiled into the binary -
4//! no external files needed at runtime. Organized into three categories:
5//!
6//! - **stdlib** - [`StdEntry`] / [`FnEntry`] for every stdlib module and function
7//! - **concepts** - [`ConceptEntry`] for language features (types, loops, imports, etc.)
8//! - **tutorials** - [`ConceptEntry`] steps for the beginner and advanced tutorials
9//!
10//! The [`std_to_markdown`], [`concept_to_markdown`], and [`tutorial_to_markdown`]
11//! functions render these entries into Markdown for display in the terminal.
12
13pub mod entries;
14pub mod entry;
15use serde::Serialize;
16use serde_json::to_string_pretty;
17
18use crate::docs::entry::{
19    ConceptCategory, ConceptEntry, DescriptionEntry, DescriptionKind, FnEntry, StdEntry,
20};
21
22/// Searches all stdlib entries for a function whose signature starts with `fn_name(`.
23///
24/// `module` narrows the search to a specific stdlib module (e.g. `"io"`, `"math"`).
25/// Accepts both bare names (`print`) and qualified names (`std::io::print`).
26///
27/// Returns `None` if no match is found.
28pub fn find_fn_doc(
29    module: Option<&str>,
30    fn_name: &str,
31) -> Option<(&'static StdEntry, &'static FnEntry)> {
32    for std_entry in entries::stdlib_entries() {
33        if let Some(m) = module
34            && std_entry.name != m
35        {
36            continue;
37        }
38        for func in std_entry.functions {
39            let bare = func.signature.split('(').next().unwrap_or(func.signature);
40            if bare == fn_name {
41                return Some((std_entry, func));
42            }
43        }
44    }
45    None
46}
47
48impl ConceptCategory {
49    /// A short human-readable label used as a section tag in Markdown output.
50    fn label(&self) -> &'static str {
51        match self {
52            ConceptCategory::Syntax => "syntax",
53            ConceptCategory::Types => "types",
54            ConceptCategory::ControlFlow => "control flow",
55            ConceptCategory::Functions => "functions",
56            ConceptCategory::Modules => "modules",
57            ConceptCategory::Tooling => "tooling",
58            ConceptCategory::ErrorHandling => "error handling",
59        }
60    }
61}
62
63impl DescriptionKind {
64    /// The Markdown admonition label used to prefix a description block.
65    /// `None` for plain explanations, which render with no prefix.
66    fn label(&self) -> Option<&'static str> {
67        match self {
68            DescriptionKind::Explanation => None,
69            DescriptionKind::Syntax => Some("Syntax"),
70            DescriptionKind::Pitfall => Some("⚠ Pitfall"),
71            DescriptionKind::Note => Some("Note"),
72        }
73    }
74}
75
76/// Renders a `see_also`/`related`/`related_stdlib` list as a Markdown line,
77/// or an empty string if the list is empty.
78fn render_related(label: &str, names: &[&str], prefix: &str) -> String {
79    if names.is_empty() {
80        return String::new();
81    }
82    let links = names
83        .iter()
84        .map(|n| format!("`{}{}`", prefix, n))
85        .collect::<Vec<_>>()
86        .join(", ");
87    format!("**{}:** {}\n\n", label, links)
88}
89
90/// Renders one [`DescriptionEntry`] (title, kind label, prose, examples,
91/// and expected output) into the given output buffer.
92fn render_description(output: &mut String, description: &DescriptionEntry) {
93    if let Some(title) = description.title {
94        output.push_str(&format!("#### {}\n\n", title));
95    }
96    if let Some(kind_label) = description.kind.label() {
97        output.push_str(&format!("**{}:** ", kind_label));
98    }
99    output.push_str(&format!("{}\n\n", description.description));
100
101    for (i, example) in description.examples.iter().enumerate() {
102        output.push_str(&format!("```rl\n{}\n```\n\n", example));
103        if let Some(expected) = description.expected_output.get(i) {
104            output.push_str(&format!("output:\n```text\n{}\n```\n\n", expected));
105        }
106    }
107}
108
109/// Renders all stdlib entries into a Markdown reference document.
110///
111/// Output is structured as:
112/// ```text
113/// # rl stdlib reference
114/// ## std::<module> (since v0.1.0) [unstable]
115/// ### `<signature>`
116/// <description>
117/// **Returns:** <returns>
118/// **Errors:** <errors>
119/// ```rl
120/// <example>
121/// ```
122/// output:
123/// ```text
124/// <expected_output>
125/// ```
126/// **See also:** `<see_also>`
127/// ```
128pub fn std_to_markdown(entries: &[&StdEntry]) -> String {
129    let mut output = String::from("# rl stdlib reference\n\n");
130    for entry in entries {
131        output.push_str(&format!("## std::{}", entry.name));
132        if let Some(since) = entry.since {
133            output.push_str(&format!(" (since {})", since));
134        }
135        if entry.unstable {
136            output.push_str(" `unstable`");
137        }
138        output.push_str("\n\n");
139        output.push_str(&format!("{}\n\n", entry.description));
140
141        for func in entry.functions {
142            output.push_str(&format!("### `{}`", func.signature));
143            if let Some(since) = func.since {
144                output.push_str(&format!(" (since {})", since));
145            }
146            output.push_str("\n\n");
147            output.push_str(&format!("{}\n\n", func.description));
148            output.push_str(&format!("**Returns:** {}\n\n", func.returns));
149            if let Some(errors) = func.errors {
150                output.push_str(&format!("**Errors:** {}\n\n", errors));
151            }
152            output.push_str(&format!("```rl\n{}\n```\n\n", func.example));
153            if let Some(expected) = func.expected_output {
154                output.push_str(&format!("output:\n```text\n{}\n```\n\n", expected));
155            }
156            output.push_str(&render_related("See also", func.see_also, ""));
157        }
158    }
159
160    output
161}
162
163/// shared renderer for any list of ConceptEntry under a custom header
164fn entries_to_markdown(header: &str, entries: &[&ConceptEntry]) -> String {
165    let mut output = format!("# {}\n\n", header);
166    for entry in entries {
167        output.push_str(&format!("## {} ({})", entry.name, entry.category.label()));
168        if let Some(since) = entry.since {
169            output.push_str(&format!(" (since {})", since));
170        }
171        output.push_str("\n\n");
172        output.push_str(&format!("{}\n\n", entry.summary));
173        output.push_str(&render_related("Prerequisites", entry.prerequisites, ""));
174
175        for description in entry.descriptions {
176            render_description(&mut output, description);
177        }
178
179        if !entry.pitfalls.is_empty() {
180            output.push_str("**Pitfalls:**\n\n");
181            for pitfall in entry.pitfalls {
182                output.push_str(&format!("- {}\n", pitfall));
183            }
184            output.push('\n');
185        }
186
187        output.push_str(&render_related("Related concepts", entry.related, ""));
188        output.push_str(&render_related(
189            "Related stdlib",
190            entry.related_stdlib,
191            "std::",
192        ));
193    }
194    output
195}
196
197/// Renders concept entries into a Markdown language reference document.
198pub fn concept_to_markdown(entries: &[&ConceptEntry]) -> String {
199    entries_to_markdown("rl concepts reference", entries)
200}
201
202/// Renders tutorial entries into a Markdown step-by-step tutorial document.
203pub fn tutorial_to_markdown(entries: &[&ConceptEntry]) -> String {
204    entries_to_markdown("rl tutorial", entries)
205}
206
207#[derive(Serialize)]
208pub struct DocsJson<'a> {
209    stdlib: &'a [&'static StdEntry],
210    concepts: &'a [&'static ConceptEntry],
211    tutorial: &'a [&'static ConceptEntry],
212}
213
214pub fn docs_to_json(
215    stdlib: &[&'static StdEntry],
216    concepts: &[&'static ConceptEntry],
217    tutorial: &[&'static ConceptEntry],
218) -> Result<String, String> {
219    let doc = DocsJson {
220        stdlib,
221        concepts,
222        tutorial,
223    };
224
225    to_string_pretty(&doc).map_err(|e| e.to_string())
226}