Skip to main content

onetaskgraph_core/template/
mod.rs

1// llmlint: ignore-file[code_lands_in_the_domain_that_owns_it] This module is where the task that introduced templates fixes their library API: at `onetaskgraph-core`'s crate root, beside the engine every surface shares, so a Rust consumer renders without the binary. A crate of its own would be a new published sibling — a change to the release surface `release-targets.toml` freezes — which is not this change's to make.
2//! Task templates: a minijinja document with a declared set of variables, rendered from
3//! answers.
4//!
5//! # The format
6//!
7//! A template file is UTF-8 minijinja source, optionally opened by YAML front matter — a
8//! first line reading `---`, closed by the next line reading `---`:
9//!
10//! ```yaml
11//! ---
12//! onetaskgraph_template: 1          # required when front matter is present
13//! description: <string>             # optional
14//! variables:                        # optional; name -> declaration
15//!   <name>:                         # ^[a-z][a-z0-9_]*$
16//!     description: <string>         # required, non-empty: what a prompt shows
17//!     type: string                  # string | text | integer | boolean | list | object
18//!     items: string                 # list only: string | object; default string
19//!     required: true                # default: true without `default`, false with one
20//!     default: <value of `type`>    # optional; `required: true` beside one is refused
21//! ---
22//! ```
23//!
24//! Nothing here restricts how the body uses a variable: it is minijinja, whole.
25//!
26//! # The chain
27//!
28//! `extends`, `include` and `import` resolve names over a [`TemplateLoader`]'s search path —
29//! the directories it was given, in order, then the `(name, source)` pairs registered on it —
30//! and never the working directory. The **declared set** is the union of every chain file's
31//! front matter. When two files declare one variable, the file nearer the rendered one (fewer
32//! references away from it, then loaded first) gives its `description`, `default` and
33//! `required`; its `type` and `items` must be the other's, or the chain is refused naming both
34//! files.
35//!
36//! # Rendering
37//!
38//! Strict: a name that is neither a declared variable nor set by the template fails the
39//! render, naming the name and the file. No auto-escaping, trailing newlines kept,
40//! `trim_blocks` and `lstrip_blocks` on. An optional variable given no answer and no default
41//! renders as `none`.
42//!
43//! # The digest
44//!
45//! `sha256:` and the lowercase hex SHA-256 over, for each chain file in first-load order, its
46//! resolved name, a NUL, its full bytes — front matter included — and a NUL. First-load
47//! order is the order rendering reads the files — for the chain's own digest, rendering with
48//! every variable at its default — the rendered file first, and a file named by an expression
49//! where the render first reads it like any other; the files no render reads, in a branch it
50//! did not take, follow in the order the chain names them.
51//!
52//! No prompting happens here. A caller that prompts asks [`Template::unanswered`] what is
53//! left, puts the answers it gathers over the ones it had with [`Answers::overlay`], and
54//! renders; the command line is the one caller that does.
55
56mod answers;
57mod front_matter;
58mod input;
59mod provenance;
60mod scan;
61
62use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
63use std::fmt;
64use std::path::{Component, Path, PathBuf};
65use std::sync::{Arc, Mutex, PoisonError};
66
67use schemars::JsonSchema;
68use serde::Serialize;
69use serde_json::Value;
70use sha2::{Digest as _, Sha256};
71
72pub use answers::Answers;
73use answers::Given;
74pub use front_matter::{DECLARATION_KEYS, FRONT_MATTER_KEYS, is_variable_name};
75pub use input::{LoaderDocument, TemplateInput};
76pub use provenance::{Sha256Digest, TemplateProvenance, answers_digest, body_digest};
77
78/// What one variable holds.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
80#[serde(rename_all = "kebab-case")]
81pub enum VariableType {
82    /// One line of text.
83    String,
84    /// Text of any number of lines.
85    Text,
86    /// A whole number.
87    Integer,
88    /// `true` or `false`.
89    Boolean,
90    /// A list, of the variable's `items`.
91    List,
92    /// A mapping from string keys to values.
93    Object,
94}
95
96impl VariableType {
97    /// Every type, in the order front matter documents them.
98    pub const ALL: [Self; 6] = [
99        Self::String,
100        Self::Text,
101        Self::Integer,
102        Self::Boolean,
103        Self::List,
104        Self::Object,
105    ];
106
107    /// The type a front matter `type:` spells, if it spells one.
108    fn parse(text: &str) -> Option<Self> {
109        Self::ALL.into_iter().find(|kind| kind.as_str() == text)
110    }
111
112    /// How front matter spells it.
113    #[must_use]
114    pub fn as_str(self) -> &'static str {
115        match self {
116            Self::String => "string",
117            Self::Text => "text",
118            Self::Integer => "integer",
119            Self::Boolean => "boolean",
120            Self::List => "list",
121            Self::Object => "object",
122        }
123    }
124}
125
126impl fmt::Display for VariableType {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        formatter.write_str(self.as_str())
129    }
130}
131
132/// What each entry of a `list` variable holds.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
134#[serde(rename_all = "kebab-case")]
135pub enum ItemType {
136    /// Strings.
137    String,
138    /// Mappings from string keys to values.
139    Object,
140}
141
142impl ItemType {
143    /// Every item type, in the order front matter documents them.
144    pub const ALL: [Self; 2] = [Self::String, Self::Object];
145
146    /// The item type a front matter `items:` spells, if it spells one.
147    fn parse(text: &str) -> Option<Self> {
148        Self::ALL.into_iter().find(|items| items.as_str() == text)
149    }
150
151    /// How front matter spells it.
152    #[must_use]
153    pub fn as_str(self) -> &'static str {
154        match self {
155            Self::String => "string",
156            Self::Object => "object",
157        }
158    }
159}
160
161/// One variable of a template's declared set, merged down its chain.
162///
163/// Built only by loading a template, so `items` is present exactly when `type` is `list`,
164/// a `default` is a value of the variable's type, and `required` is never `true` beside one.
165// llmlint: ignore-block[invalid_states_unrepresentable] Every field is private and the one constructor is `front_matter::declaration_of`, which refuses a declaration unless those three rules hold; the chain merge only clones what it built. The flat shape is the wire contract C1 fixes and `TemplateVariables` emits (`type`, `items`, `required`, `default` side by side), which a nested enum would change for both SDKs.
166#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
167pub struct TemplateVariable {
168    /// The variable's name, as the template body uses it.
169    name: String,
170    /// What the variable is for; what a prompt shows.
171    description: String,
172    /// What the variable holds.
173    #[serde(rename = "type")]
174    kind: VariableType,
175    /// What each entry holds, for a `list` variable and no other.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    items: Option<ItemType>,
178    /// Whether rendering without an answer is refused. An optional variable with no answer
179    /// and no default renders as `none`.
180    required: bool,
181    /// The value used when no answer is given.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    default: Option<Value>,
184    /// The chain file whose declaration this is: the one nearest the rendered template.
185    declared_in: String,
186}
187// llmlint: ignore-end[invalid_states_unrepresentable]
188
189impl TemplateVariable {
190    /// The variable's name.
191    #[must_use]
192    pub fn name(&self) -> &str {
193        &self.name
194    }
195
196    /// What the variable is for.
197    #[must_use]
198    pub fn description(&self) -> &str {
199        &self.description
200    }
201
202    /// What the variable holds.
203    #[must_use]
204    pub fn kind(&self) -> VariableType {
205        self.kind
206    }
207
208    /// What each entry holds, for a `list` variable.
209    #[must_use]
210    pub fn items(&self) -> Option<ItemType> {
211        self.items
212    }
213
214    /// Whether rendering without an answer is refused.
215    #[must_use]
216    pub fn required(&self) -> bool {
217        self.required
218    }
219
220    /// The value used when no answer is given.
221    #[must_use]
222    pub fn default(&self) -> Option<&Value> {
223        self.default.as_ref()
224    }
225
226    /// The chain file whose declaration this is.
227    #[must_use]
228    pub fn declared_in(&self) -> &str {
229        &self.declared_in
230    }
231
232    /// Whether a value of this variable is written over several lines: `text`, `list` and
233    /// `object`.
234    #[must_use]
235    pub fn multi_line(&self) -> bool {
236        matches!(
237            self.kind,
238            VariableType::Text | VariableType::List | VariableType::Object
239        )
240    }
241
242    /// Read text as this variable's value: literally for `string` and `text`, as YAML for
243    /// every other type — the rule `--var` and a prompt both follow.
244    ///
245    /// # Errors
246    ///
247    /// Why the text is not a value of this variable's type.
248    pub fn parse(&self, text: &str) -> Result<Value, String> {
249        let value = match self.kind {
250            VariableType::String | VariableType::Text => Value::String(text.to_owned()),
251            _ => {
252                serde_norway::from_str(text).map_err(|error| format!("it is not YAML: {error}"))?
253            }
254        };
255        self.check(&value)?;
256        Ok(value)
257    }
258
259    /// Whether `value` is a value of this variable's type.
260    ///
261    /// # Errors
262    ///
263    /// Why it is not.
264    pub fn check(&self, value: &Value) -> Result<(), String> {
265        check_value(self.kind, self.items, value)
266    }
267}
268
269/// Whether `value` is a value of `kind` (with `items`, for a list).
270fn check_value(kind: VariableType, items: Option<ItemType>, value: &Value) -> Result<(), String> {
271    let fits = match kind {
272        VariableType::String => {
273            if let Value::String(text) = value
274                && text.contains(['\n', '\r'])
275            {
276                return Err("a `string` is one line; declare the variable `text` for more".into());
277            }
278            value.is_string()
279        }
280        VariableType::Text => value.is_string(),
281        VariableType::Integer => value.is_i64() || value.is_u64(),
282        VariableType::Boolean => value.is_boolean(),
283        VariableType::Object => value.is_object(),
284        VariableType::List => {
285            let Value::Array(entries) = value else {
286                return Err(format!("expected a list, got {}", describe(value)));
287            };
288            let items = items.unwrap_or(ItemType::String);
289            if let Some((index, entry)) =
290                entries.iter().enumerate().find(|(_, entry)| match items {
291                    ItemType::String => !entry.is_string(),
292                    ItemType::Object => !entry.is_object(),
293                })
294            {
295                return Err(format!(
296                    "entry {index} of the list is {}, and its items are {}s",
297                    describe(entry),
298                    items.as_str()
299                ));
300            }
301            true
302        }
303    };
304    if fits {
305        Ok(())
306    } else {
307        Err(format!(
308            "expected {}, got {}",
309            article(kind),
310            describe(value)
311        ))
312    }
313}
314
315/// A type with its article, for a message.
316fn article(kind: VariableType) -> String {
317    match kind {
318        VariableType::Integer | VariableType::Object => format!("an {kind}"),
319        _ => format!("a {kind}"),
320    }
321}
322
323/// What kind of JSON value `value` is, for a message.
324fn describe(value: &Value) -> String {
325    match value {
326        Value::Null => "null".to_owned(),
327        Value::Bool(value) => format!("the boolean {value}"),
328        Value::Number(number) => format!("the number {number}"),
329        Value::String(text) => format!("the string {text:?}"),
330        Value::Array(_) => "a list".to_owned(),
331        Value::Object(_) => "a mapping".to_owned(),
332    }
333}
334
335/// Which field of a declaration two chain files disagree about.
336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337pub enum ChainField {
338    /// The variable's `type`.
339    Type,
340    /// A `list` variable's `items`.
341    Items,
342}
343
344impl fmt::Display for ChainField {
345    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
346        formatter.write_str(match self {
347            Self::Type => "type",
348            Self::Items => "items",
349        })
350    }
351}
352
353/// Why a template could not be loaded, answered or rendered.
354#[derive(Debug, Clone, PartialEq, thiserror::Error)]
355#[non_exhaustive]
356pub enum TemplateError {
357    /// A name or path the chain needs resolved to nothing.
358    #[error(
359        "template {name:?} was not found{}\nnext: {}",
360        referenced_from.as_ref().map(|from| format!(" (named by {from})")).unwrap_or_default(),
361        if searched.is_empty() {
362            "give the directory holding it as a search path, or register it by name.".to_owned()
363        } else {
364            format!("it was looked for in {}; add the directory holding it as a search path.", searched.join(", "))
365        }
366    )]
367    NotFound {
368        /// The name or path.
369        name: String,
370        /// The chain file that named it, when one did.
371        referenced_from: Option<String>,
372        /// The search path directories it was looked for in.
373        searched: Vec<String>,
374    },
375    /// A template that is there and could not be read — a permission, or a directory where a
376    /// file was named.
377    #[error(
378        "template {name} could not be read: {message}\n\
379         next: name a readable file, or give it the permissions this process reads with."
380    )]
381    Unreadable {
382        /// The path it was read at.
383        name: String,
384        /// Why the read failed.
385        message: String,
386    },
387    /// A search path directory that is missing or is not a directory, refused before any
388    /// template is looked for, so a mistyped one is not silently searched as empty.
389    #[error(
390        "search path {directory} cannot be searched: {message}\n\
391         next: give a directory that exists, or leave it out of the search path."
392    )]
393    SearchPath {
394        /// The directory as it was given.
395        directory: String,
396        /// Why it cannot be searched.
397        message: String,
398    },
399    /// A chain file that is not a template this format reads.
400    #[error(
401        "template {file}: {}{message}\nnext: {}",
402        key.as_ref().map(|key| format!("front matter key `{key}` ")).unwrap_or_default(),
403        "correct that file; a template's front matter is described in the README under \"Task templates\"."
404    )]
405    Malformed {
406        /// The chain file.
407        file: String,
408        /// The front matter key the refusal is about, when it is about one.
409        key: Option<String>,
410        /// What is wrong.
411        message: String,
412    },
413    /// Two chain files declaring one variable with different `type`s or `items`.
414    #[error(
415        "template variable {variable:?} is declared with {field} {nearer_value} in {nearer} and \
416         with {field} {farther_value} in {farther}; a redeclaration may change `description`, \
417         `default` and `required`, never `type` or `items`\n\
418         next: declare it with the same {field} in both files, or rename one of them."
419    )]
420    ChainConflict {
421        /// The variable.
422        variable: String,
423        /// Which field differs.
424        field: ChainField,
425        /// The file nearer the rendered template.
426        nearer: String,
427        /// What that file declares.
428        nearer_value: &'static str,
429        /// The file farther from it.
430        farther: String,
431        /// What that file declares.
432        farther_value: &'static str,
433    },
434    /// An answers document that is not a mapping of answers.
435    #[error(
436        "the answers document is refused: {message}\n\
437         next: write the answers as a YAML mapping from variable name to value."
438    )]
439    MalformedAnswers {
440        /// What is wrong with it.
441        message: String,
442    },
443    /// Answers naming no declared variable.
444    #[error(
445        "{} no variable the template declares: {}\n\
446         next: remove {}, or declare {} in the template's front matter — `onetaskgraph template \
447         variables` lists what it declares.",
448        if names.len() == 1 { "an answer names" } else { "answers name" },
449        names.join(", "),
450        if names.len() == 1 { "that answer" } else { "those answers" },
451        if names.len() == 1 { "it" } else { "them" }
452    )]
453    UnknownAnswer {
454        /// Every undeclared name answered, in name order.
455        names: Vec<String>,
456    },
457    /// An answer that is not a value of its variable's type.
458    #[error(
459        "the answer to {name:?} is not {expected}: {problem}\n\
460         next: give {name} a value of type {kind}."
461    )]
462    MistypedAnswer {
463        /// The variable.
464        name: String,
465        /// Its type.
466        kind: VariableType,
467        /// Its type with an article, for the message.
468        expected: String,
469        /// Why the answer is not one.
470        problem: String,
471    },
472    /// Required variables no answer and no default covers.
473    #[error(
474        "{} unanswered: {}\n\
475         next: answer {} with --var NAME=VALUE or in an answers file (--answers FILE), or run \
476         interactively to be asked.",
477        if names.len() == 1 { "a required variable is" } else { "required variables are" },
478        names.join(", "),
479        if names.len() == 1 { "it" } else { "each" }
480    )]
481    MissingRequired {
482        /// Every one, in declaration order.
483        names: Vec<String>,
484    },
485    /// The render itself failed: an undefined name, or an error a filter or tag raised.
486    #[error(
487        "template {}{}: {message}\n\
488         next: {}",
489        file.as_deref().unwrap_or("?"),
490        line.map(|line| format!(" line {line}")).unwrap_or_default(),
491        match name {
492            Some(name) => format!(
493                "declare {name} in the front matter of a file in the chain, or set it in the \
494                 template before it is used."
495            ),
496            None => "correct the template at that line.".to_owned(),
497        }
498    )]
499    Render {
500        /// The chain file the render failed in.
501        file: Option<String>,
502        /// The file's own line, front matter counted.
503        line: Option<usize>,
504        /// The undefined name, when that is what failed.
505        name: Option<String>,
506        /// What failed.
507        message: String,
508    },
509    /// A template loader document that is not one this product reads.
510    #[error(
511        "the template loader document is refused: {message}\n\
512         next: supply a JSON object with a non-empty string `reference`, a non-empty string \
513         `entry`, an optional `search_path` of absolute directories, optional `templates` of \
514         {{\"name\", \"source\"}} string pairs and an optional string `digest`."
515    )]
516    MalformedLoader {
517        /// What is wrong with it.
518        message: String,
519    },
520    /// A loader document stating a digest its chain does not compute.
521    #[error(
522        "the template loader document states the digest {stated}, and the chain it names \
523         computes {computed}\n\
524         next: state the digest of the chain the document names — `onetaskgraph template \
525         variables --template-loader` reports it — or leave `digest` out."
526    )]
527    LoaderDigest {
528        /// The digest the document states.
529        stated: String,
530        /// The digest its chain computes.
531        computed: String,
532    },
533}
534
535impl TemplateError {
536    /// This product's kebab-case name for the failure.
537    #[must_use]
538    pub fn kind(&self) -> &'static str {
539        match self {
540            Self::NotFound { .. } => "template-not-found",
541            Self::Unreadable { .. } => "template-unreadable",
542            Self::SearchPath { .. } => "template-search-path",
543            Self::Malformed { .. } => "template-malformed",
544            Self::ChainConflict { .. } => "template-chain-conflict",
545            Self::MalformedAnswers { .. } => "template-answers-malformed",
546            Self::UnknownAnswer { .. } => "template-unknown-answer",
547            Self::MistypedAnswer { .. } => "template-mistyped-answer",
548            Self::MissingRequired { .. } => "template-missing-required",
549            Self::Render { .. } => "template-render",
550            Self::MalformedLoader { .. } => "template-loader-malformed",
551            Self::LoaderDigest { .. } => "template-loader-digest",
552        }
553    }
554
555    /// Whether this refuses the answers rather than the template: an answers document that
556    /// is not one, an answer to no declared variable or of the wrong type, and required
557    /// variables left unanswered. The command line exits `2` for these.
558    #[must_use]
559    pub fn refuses_answers(&self) -> bool {
560        matches!(
561            self,
562            Self::MalformedAnswers { .. }
563                | Self::UnknownAnswer { .. }
564                | Self::MistypedAnswer { .. }
565                | Self::MissingRequired { .. }
566        )
567    }
568
569    fn malformed(file: &str, key: Option<&str>, message: impl Into<String>) -> Self {
570        Self::Malformed {
571            file: file.to_owned(),
572            key: key.map(str::to_owned),
573            message: message.into(),
574        }
575    }
576}
577
578/// Where a template and the templates it names are found.
579///
580/// Directories are searched first, in the order given, then the registered pairs — which
581/// is how a Rust consumer supplies templates embedded in its own binary. The working
582/// directory is never searched unless it is given.
583#[derive(Debug, Clone, Default)]
584pub struct TemplateLoader {
585    directories: Vec<PathBuf>,
586    registered: Vec<(String, String)>,
587}
588
589impl TemplateLoader {
590    /// A loader with an empty search path.
591    #[must_use]
592    pub fn new() -> Self {
593        Self::default()
594    }
595
596    /// Add `directory` to the end of the search path's directories.
597    #[must_use]
598    pub fn with_directory(mut self, directory: impl Into<PathBuf>) -> Self {
599        self.directories.push(directory.into());
600        self
601    }
602
603    /// Register a template's source under `name`, searched after every directory.
604    #[must_use]
605    pub fn with_template(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
606        self.registered.push((name.into(), source.into()));
607        self
608    }
609
610    /// Load the template at `path` and every template its chain names.
611    ///
612    /// The file is known to its chain — and in its digest — by its file name.
613    ///
614    /// # Errors
615    ///
616    /// [`TemplateError::NotFound`] for a file that cannot be read or a chain name nothing
617    /// resolves; [`TemplateError::Malformed`] for a chain file that is not UTF-8, whose front
618    /// matter is refused, or whose body does not parse; [`TemplateError::ChainConflict`] for
619    /// a variable two chain files type differently; [`TemplateError::SearchPath`] for a
620    /// search path directory that is missing or is not a directory.
621    pub fn load_path(&self, path: &Path) -> Result<Template, TemplateError> {
622        self.searchable()?;
623        let shown = path.display().to_string();
624        let bytes = std::fs::read(path).map_err(|error| {
625            if error.kind() == std::io::ErrorKind::NotFound {
626                TemplateError::NotFound {
627                    name: shown.clone(),
628                    referenced_from: None,
629                    searched: Vec::new(),
630                }
631            } else {
632                TemplateError::Unreadable {
633                    name: shown.clone(),
634                    message: error.to_string(),
635                }
636            }
637        })?;
638        let source = String::from_utf8(bytes)
639            .map_err(|_| TemplateError::malformed(&shown, None, "it is not UTF-8 text"))?;
640        let name = path
641            .file_name()
642            .map_or_else(|| shown.clone(), |name| name.to_string_lossy().into_owned());
643        self.load_chain(name, source)
644    }
645
646    /// Load the template the search path resolves `name` to, and every template its chain
647    /// names.
648    ///
649    /// # Errors
650    ///
651    /// As [`TemplateLoader::load_path`], and [`TemplateError::NotFound`] when the search
652    /// path resolves `name` to nothing.
653    pub fn load_name(&self, name: &str) -> Result<Template, TemplateError> {
654        self.searchable()?;
655        let source = self.find(name)?.ok_or_else(|| self.not_found(name, None))?;
656        self.load_chain(name.to_owned(), source)
657    }
658
659    /// The source the search path resolves `name` to, or `None`.
660    ///
661    /// A name spelled to climb out of the search path (`..`), or an absolute one, is looked
662    /// for among the registered pairs alone, so a template cannot name its way to a file
663    /// outside the directories it was given. A symbolic link inside one of those directories
664    /// is followed like any other file there: the check is on how the name is spelled, and
665    /// whoever put the link in a search directory chose what it reaches.
666    fn find(&self, name: &str) -> Result<Option<String>, TemplateError> {
667        let path = Path::new(name);
668        let spelled_within = !name.is_empty()
669            && path
670                .components()
671                .all(|component| matches!(component, Component::Normal(_)));
672        if spelled_within {
673            for directory in &self.directories {
674                let candidate = directory.join(path);
675                if candidate.is_file() {
676                    let bytes =
677                        std::fs::read(&candidate).map_err(|error| TemplateError::Unreadable {
678                            name: candidate.display().to_string(),
679                            message: error.to_string(),
680                        })?;
681                    return String::from_utf8(bytes)
682                        .map(Some)
683                        .map_err(|_| TemplateError::malformed(name, None, "it is not UTF-8 text"));
684                }
685            }
686        }
687        Ok(self
688            .registered
689            .iter()
690            .find(|(registered, _)| registered == name)
691            .map(|(_, source)| source.clone()))
692    }
693
694    fn not_found(&self, name: &str, referenced_from: Option<&str>) -> TemplateError {
695        TemplateError::NotFound {
696            name: name.to_owned(),
697            referenced_from: referenced_from.map(str::to_owned),
698            searched: self
699                .directories
700                .iter()
701                .map(|directory| directory.display().to_string())
702                .collect(),
703        }
704    }
705
706    /// Read the whole chain from its root, depth first, put it in first-load order and merge
707    /// its declarations.
708    ///
709    /// First-load order is the order a render with every variable at its default reads the
710    /// files. A file that render cannot read, found by an expression, is not this chain's
711    /// yet: the answers that name it reach it through [`Template::expand`], which refuses it.
712    fn load_chain(&self, root: String, source: String) -> Result<Template, TemplateError> {
713        let resolutions = Resolutions::new();
714        let files = self.build(root.clone(), source, &resolutions)?;
715        let variables = merge(&files)?;
716        let Discovered { read, .. } = discover(&root, &files, &variables, self, &Answers::new());
717        let files = in_read_order(files, &read);
718        let variables = merge(&files)?;
719        Ok(Template::assemble(
720            root,
721            files,
722            variables,
723            resolutions,
724            self.clone(),
725        ))
726    }
727
728    /// Refuse a search path directory that is missing or is not a directory.
729    fn searchable(&self) -> Result<(), TemplateError> {
730        for directory in &self.directories {
731            let refused = |message: String| TemplateError::SearchPath {
732                directory: directory.display().to_string(),
733                message,
734            };
735            match std::fs::metadata(directory) {
736                Ok(metadata) if metadata.is_dir() => {}
737                Ok(_) => return Err(refused("it is not a directory".to_owned())),
738                Err(error) => return Err(refused(error.to_string())),
739            }
740        }
741        Ok(())
742    }
743
744    /// Read the chain from `name`, whose source is `source`: that file, then every file it
745    /// names, depth first in the order its tags name them, skipping any already read. A tag
746    /// naming its template by a literal names what it spells; one naming it by an expression
747    /// names what `resolutions` records it evaluating to, in the order it was reached. That
748    /// is only the order the chain is found in: its callers put it in first-load order before
749    /// a digest or a nearness tie is taken over it.
750    fn build(
751        &self,
752        name: String,
753        source: String,
754        resolutions: &Resolutions,
755    ) -> Result<Vec<ChainFile>, TemplateError> {
756        let mut files = Vec::new();
757        let mut seen: HashSet<String> = HashSet::new();
758        let mut pending = vec![(name, source)];
759        // A stack, pushed in reverse, so the first name a file spells is the next one read.
760        while let Some((name, source)) = pending.pop() {
761            if !seen.insert(name.clone()) {
762                continue;
763            }
764            let split = front_matter::split(&name, &source)?;
765            compile_check(&name, &split.body, split.offset_lines)?;
766            let mut names = Vec::new();
767            let mut children = Vec::new();
768            for named in scan::named(&split.body) {
769                let references = match named {
770                    scan::Named::Literal(reference) => vec![reference],
771                    // What the render named there resolves or fails in the render itself.
772                    scan::Named::Expression { ordinal, .. } => resolutions
773                        .get(&NamingTag {
774                            file: name.clone(),
775                            ordinal,
776                        })
777                        .into_iter()
778                        .flatten()
779                        .map(|candidates| scan::Reference {
780                            candidates: candidates.names().to_vec(),
781                            optional: true,
782                        })
783                        .collect(),
784                };
785                for reference in references {
786                    // The first candidate that resolves is the one the render loads, so it is
787                    // the one that belongs to the chain; the rest are not read.
788                    let mut resolved = false;
789                    for candidate in &reference.candidates {
790                        if seen.contains(candidate) || *candidate == name {
791                            names.push(candidate.clone());
792                            resolved = true;
793                            break;
794                        }
795                        if let Some(source) = self.find(candidate)? {
796                            names.push(candidate.clone());
797                            children.push((candidate.clone(), source));
798                            resolved = true;
799                            break;
800                        }
801                    }
802                    if !resolved && !reference.optional {
803                        return Err(self.not_found(&reference.candidates.join(" or "), Some(&name)));
804                    }
805                }
806            }
807            pending.extend(children.into_iter().rev());
808            files.push(ChainFile {
809                name,
810                source,
811                body: split.body,
812                offset_lines: split.offset_lines,
813                declarations: split.declarations,
814                names,
815            });
816        }
817        Ok(files)
818    }
819}
820
821/// A tag naming a template by an expression: the chain file it is written in, and which of
822/// that file's such tags it is, counting from zero.
823#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
824struct NamingTag {
825    file: String,
826    ordinal: usize,
827}
828
829/// What one evaluation of a naming expression gave: a name, or a list of them of which the
830/// first that resolves is loaded. Never empty.
831#[derive(Debug, Clone, PartialEq, Eq)]
832struct Candidates(Vec<String>);
833
834impl Candidates {
835    /// The names `value` gives, or `None` when it gives none — it is neither a string nor a
836    /// sequence holding one.
837    fn of(value: &minijinja::Value) -> Option<Self> {
838        let names: Vec<String> = match value.as_str() {
839            Some(name) => vec![name.to_owned()],
840            None if value.kind() == minijinja::value::ValueKind::Seq => value
841                .try_iter()
842                .into_iter()
843                .flatten()
844                .filter_map(|item| item.as_str().map(str::to_owned))
845                .collect(),
846            None => Vec::new(),
847        };
848        (!names.is_empty()).then_some(Self(names))
849    }
850
851    fn names(&self) -> &[String] {
852        &self.0
853    }
854}
855
856/// What each naming tag was found to name in one render: every distinct value it gave, in the
857/// order the render first reached it.
858type Resolutions = BTreeMap<NamingTag, Vec<Candidates>>;
859
860/// The body of each of `files` as [`discover`] renders it: with every expression that names a
861/// template wrapped in [`scan::HOOK`], so the render reports what it named.
862fn hooked_bodies(files: &[ChainFile]) -> HashMap<String, String> {
863    files
864        .iter()
865        .enumerate()
866        .map(|(index, file)| (file.name.clone(), scan::hooked(&file.body, index)))
867        .collect()
868}
869
870/// Give `environment` the function [`hooked_bodies`] calls, and answer where what it
871/// records collects.
872fn record_names(
873    environment: &mut minijinja::Environment<'static>,
874    files: &[ChainFile],
875) -> Arc<Mutex<Resolutions>> {
876    let names: Vec<String> = files.iter().map(|file| file.name.clone()).collect();
877    let recorded: Arc<Mutex<Resolutions>> = Arc::default();
878    let sink = Arc::clone(&recorded);
879    environment.add_function(
880        scan::HOOK,
881        move |file: usize, ordinal: usize, value: minijinja::Value| {
882            if let Some(name) = names.get(file)
883                && let Some(candidates) = Candidates::of(&value)
884            {
885                let mut recorded = sink.lock().unwrap_or_else(PoisonError::into_inner);
886                let found = recorded
887                    .entry(NamingTag {
888                        file: name.clone(),
889                        ordinal,
890                    })
891                    .or_default();
892                if !found.contains(&candidates) {
893                    found.push(candidates);
894                }
895            }
896            value
897        },
898    );
899    recorded
900}
901
902/// Refuse a body that does not parse, before anything renders it.
903fn compile_check(name: &str, body: &str, offset_lines: usize) -> Result<(), TemplateError> {
904    let environment = environment();
905    environment
906        .template_from_named_str(name, body)
907        .map(|_| ())
908        .map_err(|error| TemplateError::Malformed {
909            file: name.to_owned(),
910            key: None,
911            message: format!(
912                "its body does not parse{}: {}",
913                error
914                    .line()
915                    .map(|line| format!(" at line {}", line + offset_lines))
916                    .unwrap_or_default(),
917                error.detail().unwrap_or("a syntax error")
918            ),
919        })
920}
921
922/// One file of a loaded chain.
923#[derive(Debug, Clone)]
924struct ChainFile {
925    name: String,
926    source: String,
927    body: String,
928    offset_lines: usize,
929    declarations: Vec<TemplateVariable>,
930    /// The chain files its tags name, in the order they name them: what nearness is measured
931    /// over.
932    names: Vec<String>,
933}
934
935/// Merge every chain file's declarations into the declared set.
936///
937/// Nearness is the fewest references from the rendered file, ties going to the file earlier
938/// in `files`; the order is theirs, each variable where it was first declared.
939fn merge(files: &[ChainFile]) -> Result<Vec<TemplateVariable>, TemplateError> {
940    let index_of: HashMap<&str, usize> = files
941        .iter()
942        .enumerate()
943        .map(|(index, file)| (file.name.as_str(), index))
944        .collect();
945    let mut depth = vec![usize::MAX; files.len()];
946    if !files.is_empty() {
947        depth[0] = 0;
948    }
949    let mut queue = VecDeque::from([0usize]);
950    while let Some(at) = queue.pop_front() {
951        for child in &files[at].names {
952            if let Some(&child) = index_of.get(child.as_str())
953                && depth[child] == usize::MAX
954            {
955                depth[child] = depth[at] + 1;
956                queue.push_back(child);
957            }
958        }
959    }
960    let mut nearness: Vec<usize> = (0..files.len()).collect();
961    nearness.sort_by_key(|&index| (depth[index], index));
962
963    let mut merged: BTreeMap<&str, TemplateVariable> = BTreeMap::new();
964    for &index in &nearness {
965        let file = &files[index];
966        for declaration in &file.declarations {
967            match merged.get(declaration.name.as_str()) {
968                None => {
969                    merged.insert(&declaration.name, declaration.clone());
970                }
971                Some(nearer) => {
972                    if nearer.kind != declaration.kind {
973                        return Err(conflict(
974                            nearer,
975                            ChainField::Type,
976                            nearer.kind.as_str(),
977                            &file.name,
978                            declaration.kind.as_str(),
979                            &declaration.name,
980                        ));
981                    }
982                    if nearer.items != declaration.items {
983                        let spell =
984                            |items: Option<ItemType>| items.map_or("none", ItemType::as_str);
985                        return Err(conflict(
986                            nearer,
987                            ChainField::Items,
988                            spell(nearer.items),
989                            &file.name,
990                            spell(declaration.items),
991                            &declaration.name,
992                        ));
993                    }
994                }
995            }
996        }
997    }
998
999    let mut ordered = Vec::with_capacity(merged.len());
1000    for file in files {
1001        for declaration in &file.declarations {
1002            if let Some(variable) = merged.remove(declaration.name.as_str()) {
1003                ordered.push(variable);
1004            }
1005        }
1006    }
1007    Ok(ordered)
1008}
1009
1010fn conflict(
1011    nearer: &TemplateVariable,
1012    field: ChainField,
1013    nearer_value: &'static str,
1014    farther: &str,
1015    farther_value: &'static str,
1016    variable: &str,
1017) -> TemplateError {
1018    TemplateError::ChainConflict {
1019        variable: variable.to_owned(),
1020        field,
1021        nearer: nearer.declared_in.clone(),
1022        nearer_value,
1023        farther: farther.to_owned(),
1024        farther_value,
1025    }
1026}
1027
1028/// The digest over `files`, each a resolved name and its full source, in first-load order.
1029fn digest<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> String {
1030    let mut hasher = Sha256::new();
1031    for (name, source) in files {
1032        hasher.update(name.as_bytes());
1033        hasher.update([0]);
1034        hasher.update(source.as_bytes());
1035        hasher.update([0]);
1036    }
1037    let hash = hasher.finalize();
1038    let mut rendered = String::with_capacity(7 + hash.len() * 2);
1039    rendered.push_str("sha256:");
1040    for byte in hash {
1041        rendered.push_str(&format!("{byte:02x}"));
1042    }
1043    rendered
1044}
1045
1046/// The renderer every template is compiled and rendered in.
1047fn environment() -> minijinja::Environment<'static> {
1048    let mut environment = minijinja::Environment::new();
1049    environment.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
1050    environment.set_auto_escape_callback(|_| minijinja::AutoEscape::None);
1051    environment.set_keep_trailing_newline(true);
1052    environment.set_trim_blocks(true);
1053    environment.set_lstrip_blocks(true);
1054    environment
1055}
1056
1057/// A loaded template: its chain, its declared set and its digest.
1058#[derive(Debug, Clone)]
1059pub struct Template {
1060    name: String,
1061    files: Vec<ChainFile>,
1062    variables: Vec<TemplateVariable>,
1063    /// What the tags naming a template by an expression were found to name.
1064    resolutions: Resolutions,
1065    digest: String,
1066    loader: TemplateLoader,
1067}
1068
1069/// What `onetaskgraph template variables` answers with: a template's declared set.
1070#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
1071pub struct TemplateVariables {
1072    /// The rendered template's resolved name.
1073    pub template: String,
1074    /// The chain's digest: `sha256:` and 64 lowercase hex digits, over every file it reads
1075    /// in first-load order — each template an expression names counted as it names one when
1076    /// every variable takes its default.
1077    pub digest: String,
1078    /// Every declared variable, in declaration order along the chain.
1079    pub variables: Vec<TemplateVariable>,
1080}
1081
1082/// A rendered template: what `onetaskgraph template render` answers with.
1083#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
1084pub struct RenderedTemplate {
1085    /// The rendered text.
1086    pub body: String,
1087    /// The digest of every file of the chain these answers render, in first-load order:
1088    /// `sha256:` and 64 lowercase hex digits. That is the order the render first read each
1089    /// file, a template an expression named included, then any file of the chain it did not
1090    /// read.
1091    pub digest: String,
1092    /// Every declared variable and the value it rendered with — an answer, a default, or
1093    /// `null` for an optional variable given neither.
1094    pub answers: BTreeMap<String, Value>,
1095}
1096
1097impl Template {
1098    fn assemble(
1099        name: String,
1100        files: Vec<ChainFile>,
1101        variables: Vec<TemplateVariable>,
1102        resolutions: Resolutions,
1103        loader: TemplateLoader,
1104    ) -> Self {
1105        let digest = digest(
1106            files
1107                .iter()
1108                .map(|file| (file.name.as_str(), file.source.as_str())),
1109        );
1110        Self {
1111            name,
1112            files,
1113            variables,
1114            resolutions,
1115            digest,
1116            loader,
1117        }
1118    }
1119
1120    /// This template with every template an expression names for `answers` added to its
1121    /// chain — each with the files its own literals name — and their front matter merged into
1122    /// the declared set under the same rules as the rest of the chain.
1123    ///
1124    /// Which file an expression names can depend on the answers, so it is found by rendering:
1125    /// leniently, from the answers that fit their declarations and the defaults of the rest,
1126    /// every tag naming a template by an expression reporting what it named. A file found that
1127    /// way may declare a variable, or a nearer default, that decides what an expression
1128    /// names, so it repeats until a render names what the one before it did; a file only an
1129    /// earlier render named is not part of the chain. Each file so found joins the chain at
1130    /// the tag that names it, exactly as a file a literal names would: its nearness is its own
1131    /// distance from the rendered template. The expanded chain is in first-load order — the
1132    /// order that last render first read each file, then any it did not read. A template
1133    /// named by no expression expands to its own files in that order.
1134    ///
1135    /// [`Template::unanswered`], [`Template::resolve`] and [`Template::render`] each expand
1136    /// first, so a caller need not.
1137    ///
1138    /// # Errors
1139    ///
1140    /// [`TemplateError::Malformed`], [`TemplateError::Unreadable`] and
1141    /// [`TemplateError::NotFound`] for a file found this way, or one its literals name, as for
1142    /// any chain file; [`TemplateError::ChainConflict`] for a declaration of it whose `type`
1143    /// or `items` another chain file's contradicts; and [`TemplateError::Malformed`] for the
1144    /// rendered template when the files its expressions name give defaults that name one
1145    /// another in turn, so that what they name never settles.
1146    pub fn expand(&self, answers: &Answers) -> Result<Self, TemplateError> {
1147        let mut expanded = self.clone();
1148        let mut named = vec![expanded.resolutions.clone()];
1149        loop {
1150            let Discovered {
1151                resolutions,
1152                read,
1153                failure,
1154            } = discover(
1155                &expanded.name,
1156                &expanded.files,
1157                &expanded.variables,
1158                &expanded.loader,
1159                answers,
1160            );
1161            if let Some(error) = failure {
1162                return Err(error);
1163            }
1164            if resolutions == expanded.resolutions {
1165                let files = in_read_order(expanded.files, &read);
1166                let variables = merge(&files)?;
1167                return Ok(Self::assemble(
1168                    self.name.clone(),
1169                    files,
1170                    variables,
1171                    resolutions,
1172                    self.loader.clone(),
1173                ));
1174            }
1175            if named.contains(&resolutions) {
1176                return Err(TemplateError::malformed(
1177                    &self.name,
1178                    None,
1179                    "what its expressions name never settles: each template they name gives a \
1180                     default naming another, round a cycle; answer the variable that decides it",
1181                ));
1182            }
1183            named.push(resolutions.clone());
1184            let files = expanded.rebuild(&resolutions)?;
1185            let variables = merge(&files)?;
1186            expanded = Self::assemble(
1187                self.name.clone(),
1188                files,
1189                variables,
1190                resolutions,
1191                self.loader.clone(),
1192            );
1193        }
1194    }
1195
1196    fn rebuild(&self, resolutions: &Resolutions) -> Result<Vec<ChainFile>, TemplateError> {
1197        let root = self
1198            .files
1199            .first()
1200            .map_or_else(String::new, |file| file.source.clone());
1201        self.loader.build(self.name.clone(), root, resolutions)
1202    }
1203
1204    /// The rendered template's resolved name.
1205    #[must_use]
1206    pub fn name(&self) -> &str {
1207        &self.name
1208    }
1209
1210    /// The chain's digest.
1211    #[must_use]
1212    pub fn digest(&self) -> &str {
1213        &self.digest
1214    }
1215
1216    /// The declared set, in declaration order along the chain.
1217    #[must_use]
1218    pub fn variables(&self) -> &[TemplateVariable] {
1219        &self.variables
1220    }
1221
1222    /// The resolved name of every file of the chain, in first-load order: the files
1223    /// [`Template::digest`] is taken over, in its order. For a loaded template these are the
1224    /// rendered one and each `extends`, `include` and `import` its literals reach; what
1225    /// [`Template::expand`] answers adds each template an expression names.
1226    pub fn chain(&self) -> impl Iterator<Item = &str> {
1227        self.files.iter().map(|file| file.name.as_str())
1228    }
1229
1230    /// The declared set as `template variables` reports it.
1231    #[must_use]
1232    pub fn describe(&self) -> TemplateVariables {
1233        TemplateVariables {
1234            template: self.name.clone(),
1235            digest: self.digest.clone(),
1236            variables: self.variables.clone(),
1237        }
1238    }
1239
1240    /// The declared variables `answers` leaves unanswered, in declaration order — whether or
1241    /// not a default covers them — over the chain [`Template::expand`] reaches for `answers`.
1242    /// What a prompting caller asks for; answering one can name a further template, so a
1243    /// caller asks again until nothing new is left.
1244    ///
1245    /// # Errors
1246    ///
1247    /// [`TemplateError::UnknownAnswer`] and [`TemplateError::MistypedAnswer`], as
1248    /// [`Template::render`] refuses them.
1249    pub fn unanswered(&self, answers: &Answers) -> Result<Vec<TemplateVariable>, TemplateError> {
1250        let expanded = self.expand(answers)?;
1251        let typed = expanded.typed(answers)?;
1252        Ok(expanded
1253            .variables
1254            .into_iter()
1255            .filter(|variable| !typed.contains_key(&variable.name))
1256            .collect())
1257    }
1258
1259    /// Every declared variable's value — over the chain [`Template::expand`] reaches for
1260    /// `answers` — its answer, else its default, else `null` for an optional one.
1261    ///
1262    /// # Errors
1263    ///
1264    /// [`TemplateError::UnknownAnswer`] naming every answer to no declared variable,
1265    /// [`TemplateError::MistypedAnswer`] for an answer not of its variable's type, and
1266    /// [`TemplateError::MissingRequired`] naming every required variable left unanswered.
1267    pub fn resolve(&self, answers: &Answers) -> Result<BTreeMap<String, Value>, TemplateError> {
1268        self.expand(answers)?.resolve_here(answers)
1269    }
1270
1271    /// [`Template::resolve`] over this chain as it stands.
1272    fn resolve_here(&self, answers: &Answers) -> Result<BTreeMap<String, Value>, TemplateError> {
1273        let mut typed = self.typed(answers)?;
1274        let missing: Vec<String> = self
1275            .variables
1276            .iter()
1277            .filter(|variable| {
1278                variable.required
1279                    && variable.default.is_none()
1280                    && !typed.contains_key(&variable.name)
1281            })
1282            .map(|variable| variable.name.clone())
1283            .collect();
1284        if !missing.is_empty() {
1285            return Err(TemplateError::MissingRequired { names: missing });
1286        }
1287        for variable in &self.variables {
1288            typed
1289                .entry(variable.name.clone())
1290                .or_insert_with(|| variable.default.clone().unwrap_or(Value::Null));
1291        }
1292        Ok(typed)
1293    }
1294
1295    /// Render the template from `answers`, over the chain [`Template::expand`] reaches for
1296    /// them: a template an expression names is part of the chain, its declarations part of
1297    /// the declared set the answers are held to, and its bytes part of the digest.
1298    ///
1299    /// # Errors
1300    ///
1301    /// Every refusal of [`Template::expand`] and [`Template::resolve`], and
1302    /// [`TemplateError::Render`] for a render that fails — an undefined name among them.
1303    pub fn render(&self, answers: &Answers) -> Result<RenderedTemplate, TemplateError> {
1304        self.expand(answers)?.render_here(answers)
1305    }
1306
1307    /// [`Template::render`] over this chain as it stands.
1308    ///
1309    /// The render reads only the files of this chain. [`Template::expand`] found them by
1310    /// rendering from exactly the values this render is given, so a file it names that the
1311    /// chain does not hold is one that was not there to find, and is not found here either.
1312    fn render_here(&self, answers: &Answers) -> Result<RenderedTemplate, TemplateError> {
1313        let resolved = self.resolve_here(answers)?;
1314
1315        let offsets: HashMap<String, usize> = self
1316            .files
1317            .iter()
1318            .map(|file| (file.name.clone(), file.offset_lines))
1319            .collect();
1320        let bodies: HashMap<String, String> = self
1321            .files
1322            .iter()
1323            .map(|file| (file.name.clone(), file.body.clone()))
1324            .collect();
1325        let mut environment = environment();
1326        environment.set_loader(move |name| Ok(bodies.get(name).cloned()));
1327        let rendered = environment
1328            .get_template(&self.name)
1329            .and_then(|template| template.render(&resolved))
1330            .map_err(|error| render_error(&error, &offsets))?;
1331        Ok(RenderedTemplate {
1332            body: rendered,
1333            digest: self.digest.clone(),
1334            answers: resolved,
1335        })
1336    }
1337
1338    /// Check `answers` against the declared set and type each one.
1339    fn typed(&self, answers: &Answers) -> Result<BTreeMap<String, Value>, TemplateError> {
1340        let declared: HashMap<&str, &TemplateVariable> = self
1341            .variables
1342            .iter()
1343            .map(|variable| (variable.name.as_str(), variable))
1344            .collect();
1345        let unknown: Vec<String> = answers
1346            .names()
1347            .filter(|name| !declared.contains_key(name))
1348            .map(str::to_owned)
1349            .collect();
1350        if !unknown.is_empty() {
1351            return Err(TemplateError::UnknownAnswer { names: unknown });
1352        }
1353
1354        let mut typed = BTreeMap::new();
1355        for variable in &self.variables {
1356            let value = match answers.given(&variable.name) {
1357                None => continue,
1358                Some(Given::Value(value)) => variable.check(value).map(|()| value.clone()),
1359                Some(Given::Text(text)) => variable.parse(text),
1360            };
1361            let value = value.map_err(|problem| TemplateError::MistypedAnswer {
1362                name: variable.name.clone(),
1363                kind: variable.kind,
1364                expected: article(variable.kind),
1365                problem,
1366            })?;
1367            typed.insert(variable.name.clone(), value);
1368        }
1369        Ok(typed)
1370    }
1371}
1372
1373/// What a lenient render of `files` reads: what every tag naming a template by an
1374/// expression named, in the order the render reached them, and the order it first read each
1375/// chain file.
1376///
1377/// The render is only a way to learn what an expression names: its output is dropped, and
1378/// so is any failure of the render itself — a strict render reports those. A file it asks
1379/// for that is there and cannot be read is that file's failure, and is answered as one for
1380/// the caller to raise.
1381/// Each variable takes the answer given for it when that is one of its type, else its
1382/// default, else `none` for an optional one — as the strict render has it; a required one
1383/// with neither is left undefined, which a lenient render reads as empty.
1384fn discover(
1385    root: &str,
1386    files: &[ChainFile],
1387    variables: &[TemplateVariable],
1388    loader: &TemplateLoader,
1389    answers: &Answers,
1390) -> Discovered {
1391    let mut context = BTreeMap::new();
1392    for variable in variables {
1393        let answered = match answers.given(&variable.name) {
1394            Some(Given::Value(value)) => variable.check(value).ok().map(|()| value.clone()),
1395            Some(Given::Text(text)) => variable.parse(text).ok(),
1396            None => None,
1397        };
1398        if let Some(value) = answered.or_else(|| variable.default.clone()) {
1399            context.insert(variable.name.clone(), value);
1400        } else if !variable.required {
1401            context.insert(variable.name.clone(), Value::Null);
1402        }
1403    }
1404
1405    let bodies = hooked_bodies(files);
1406    let loader = loader.clone();
1407    let failed: Arc<Mutex<Option<TemplateError>>> = Arc::default();
1408    let failure = Arc::clone(&failed);
1409    let read: Arc<Mutex<Vec<String>>> = Arc::default();
1410    let reading = Arc::clone(&read);
1411    let mut environment = environment();
1412    environment.set_undefined_behavior(minijinja::UndefinedBehavior::Lenient);
1413    let recorded = record_names(&mut environment, files);
1414    environment.set_loader(move |name| {
1415        let mut reading = reading.lock().unwrap_or_else(PoisonError::into_inner);
1416        if let Some(body) = bodies.get(name) {
1417            if !reading.iter().any(|read| read == name) {
1418                reading.push(name.to_owned());
1419            }
1420            return Ok(Some(body.clone()));
1421        }
1422        let source = match loader.find(name) {
1423            Ok(Some(source)) => source,
1424            Ok(None) => return Ok(None),
1425            Err(error) => {
1426                failure
1427                    .lock()
1428                    .unwrap_or_else(PoisonError::into_inner)
1429                    .get_or_insert(error);
1430                return Ok(None);
1431            }
1432        };
1433        Ok(Some(
1434            front_matter::split(name, &source)
1435                .map(|split| split.body)
1436                .unwrap_or_default(),
1437        ))
1438    });
1439    let _ = environment
1440        .get_template(root)
1441        .and_then(|template| template.render(&context));
1442    let failure = failed.lock().unwrap_or_else(PoisonError::into_inner).take();
1443    let resolutions = recorded
1444        .lock()
1445        .unwrap_or_else(PoisonError::into_inner)
1446        .clone();
1447    let read = read.lock().unwrap_or_else(PoisonError::into_inner).clone();
1448    Discovered {
1449        resolutions,
1450        read,
1451        failure,
1452    }
1453}
1454
1455/// What a lenient render of a chain found: what its naming expressions named, which of the
1456/// chain's files it read, in the order it first read each, and the first file it asked for
1457/// that is there and could not be read.
1458struct Discovered {
1459    resolutions: Resolutions,
1460    read: Vec<String>,
1461    failure: Option<TemplateError>,
1462}
1463
1464/// `files` in first-load order: the root, then every file a render of them read in the order
1465/// it first read it, then the files no render reaches — an untaken branch's — in the order
1466/// the chain names them.
1467fn in_read_order(files: Vec<ChainFile>, read: &[String]) -> Vec<ChainFile> {
1468    let mut files: Vec<(usize, ChainFile)> = files
1469        .into_iter()
1470        .enumerate()
1471        .map(|(index, file)| {
1472            let at = if index == 0 {
1473                0
1474            } else {
1475                read.iter()
1476                    .position(|name| *name == file.name)
1477                    .map_or(read.len() + index, |at| at + 1)
1478            };
1479            (at, file)
1480        })
1481        .collect();
1482    files.sort_by_key(|(at, _)| *at);
1483    files.into_iter().map(|(_, file)| file).collect()
1484}
1485
1486/// A minijinja failure as the template failure it is, located in the file's own lines.
1487///
1488/// A failure inside an included or imported file reaches here wrapped in one per template
1489/// it passed through; the innermost is where it happened.
1490fn render_error(error: &minijinja::Error, offsets: &HashMap<String, usize>) -> TemplateError {
1491    let mut error = error;
1492    while let Some(inner) = std::error::Error::source(error)
1493        .and_then(|source| source.downcast_ref::<minijinja::Error>())
1494    {
1495        error = inner;
1496    }
1497    let file = error.name().map(str::to_owned);
1498    let offset = file
1499        .as_deref()
1500        .and_then(|file| offsets.get(file))
1501        .copied()
1502        .unwrap_or(0);
1503    let line = error.line().map(|line| line + offset);
1504    if error.kind() == minijinja::ErrorKind::UndefinedError {
1505        let name = error
1506            .template_source()
1507            .zip(error.range())
1508            .and_then(|(source, range)| source.get(range))
1509            .map(str::trim)
1510            .filter(|name| !name.is_empty())
1511            .map(str::to_owned);
1512        return TemplateError::Render {
1513            message: match &name {
1514                Some(name) => format!(
1515                    "{name} is undefined: it is neither a declared variable nor set by the template"
1516                ),
1517                None => "a value there is undefined".to_owned(),
1518            },
1519            file,
1520            line,
1521            name,
1522        };
1523    }
1524    TemplateError::Render {
1525        file,
1526        line,
1527        name: None,
1528        message: error
1529            .detail()
1530            .map_or_else(|| error.kind().to_string(), str::to_owned),
1531    }
1532}