Skip to main content

asciidoc_parser/parser/
include_file_handler.rs

1use std::fmt::Debug;
2
3use crate::{Parser, attributes::Attrlist};
4
5/// An `IncludeFileHandler` is responsible for providing the text content for an
6/// `include::` directive when encountered.
7///
8/// A client of [`Parser`] may provide an `IncludeFileHandler` to customize how
9/// include file resolution is handled.
10///
11/// [`Parser`]: crate::Parser
12pub trait IncludeFileHandler: Debug {
13    /// Provide the file content for an `include::` directive, if available.
14    ///
15    /// # Parameters
16    /// - `source`: The path to the document that is including the file. A root
17    ///   document may be signaled via `None` depending on how the parser was
18    ///   invoked. This path should be considered when resolving relative paths.
19    /// - `target`: The path to the document that was provided in the
20    ///   `include::` directive.
21    /// - `attrlist`: Any attributes specified on the include directive.
22    /// - `parser`: An implementation may read document attribute values from
23    ///   the [`Parser`] state.
24    ///
25    /// Return the outcome as an [`IncludeResolution`]:
26    ///
27    /// - [`IncludeResolution::Found`] carries the content of the include file
28    ///   (wrapped in [`IncludeContent`]).
29    /// - [`IncludeResolution::NotFound`] signals that no such file exists; the
30    ///   parser records a [`WarningType::IncludeFileNotFound`] warning.
31    /// - [`IncludeResolution::NotReadable`] signals that the file exists but
32    ///   could not be read (for example a permission or other IO error); the
33    ///   parser records a [`WarningType::IncludeFileNotReadable`] warning.
34    ///
35    /// The rendered replacement (`Unresolved directive …`) is identical for the
36    /// two failure reasons; only the warning differs, mirroring Asciidoctor's
37    /// separate `include file not found` and `include file not readable`
38    /// messages.
39    ///
40    /// [`WarningType::IncludeFileNotFound`]: crate::warnings::WarningType::IncludeFileNotFound
41    /// [`WarningType::IncludeFileNotReadable`]: crate::warnings::WarningType::IncludeFileNotReadable
42    ///
43    /// # Options
44    /// With the exception of `encoding` (see below), the implementation should
45    /// not attempt to interpret any of the built-in attributes (i.e.
46    /// `leveloffset`, `lines`, `tags`, or `indent`). Correct handling of these
47    /// attributes will be provided by the parser itself.
48    ///
49    /// # Encoding
50    /// The content returned in [`IncludeContent`] is a typical Rust [`String`]
51    /// and therefore must be encoded as UTF-8.
52    ///
53    /// If the implementation is capable of transcoding from other formats, it
54    /// may use the `encoding` attribute as a hint of the source format. When it
55    /// transcodes the content to UTF-8, it should return the result via
56    /// [`IncludeContent::transcoded`] so that the parser knows the requested
57    /// encoding was honored and suppresses the non-UTF-8 include-encoding
58    /// warning.
59    ///
60    /// An implementation that only deals in UTF-8 should return its content via
61    /// [`IncludeContent::new`] (or the [`From`] conversions). If the directive
62    /// requested a non-UTF-8 `encoding`, the parser will emit a non-UTF-8
63    /// include-encoding warning in that case.
64    ///
65    /// If the implementation finds a file that is not encoded in UTF-8 and is
66    /// incapable of transcoding it, it should return
67    /// [`IncludeResolution::NotFound`].
68    fn resolve_target<'src>(
69        &self,
70        source: Option<&str>,
71        target: &str,
72        attrlist: &Attrlist<'src>,
73        parser: &Parser,
74    ) -> IncludeResolution;
75}
76
77/// The outcome of an [`IncludeFileHandler::resolve_target`] call: either the
78/// resolved content, or the reason resolution failed.
79///
80/// The two failure reasons map to distinct warnings – mirroring Asciidoctor,
81/// which distinguishes a missing include file (`include file not found`) from
82/// one that is present but unreadable (`include file not readable`).
83///
84/// This enum is `non_exhaustive`: future resolution reasons may be recognized
85/// as the parser grows, so a host matching on it needs a catch-all arm. New
86/// variants can still be returned by an [`IncludeFileHandler`] implementation.
87#[derive(Clone, Debug, Eq, PartialEq)]
88#[non_exhaustive]
89pub enum IncludeResolution {
90    /// The include file was resolved; its content is carried here.
91    Found(IncludeContent),
92
93    /// No file could be found for the directive's target. The parser records a
94    /// [`WarningType::IncludeFileNotFound`] warning.
95    ///
96    /// [`WarningType::IncludeFileNotFound`]: crate::warnings::WarningType::IncludeFileNotFound
97    NotFound,
98
99    /// A file was found for the directive's target but could not be read (for
100    /// example a permission or other IO error). The parser records a
101    /// [`WarningType::IncludeFileNotReadable`] warning.
102    ///
103    /// [`WarningType::IncludeFileNotReadable`]: crate::warnings::WarningType::IncludeFileNotReadable
104    NotReadable,
105}
106
107impl From<IncludeContent> for IncludeResolution {
108    fn from(content: IncludeContent) -> Self {
109        IncludeResolution::Found(content)
110    }
111}
112
113/// The content returned by an [`IncludeFileHandler`] for an `include::`
114/// directive, together with the metadata the parser needs to finish processing
115/// the include.
116///
117/// Construct one via [`IncludeContent::new`] for UTF-8 content that was read
118/// without interpreting the `encoding` attribute, or via
119/// [`IncludeContent::transcoded`] for content the handler transcoded to UTF-8
120/// while honoring a requested `encoding`. See the `# Encoding` section of
121/// [`IncludeFileHandler::resolve_target`] for details.
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct IncludeContent {
124    content: String,
125    encoding_handled: bool,
126}
127
128impl IncludeContent {
129    /// UTF-8 content that was read **without** interpreting the `encoding`
130    /// attribute.
131    ///
132    /// If the `include::` directive requested a non-UTF-8 `encoding`, the
133    /// parser will emit a non-UTF-8 include-encoding warning. This is the
134    /// appropriate constructor for a handler that only deals in UTF-8.
135    pub fn new(content: impl Into<String>) -> Self {
136        Self {
137            content: content.into(),
138            encoding_handled: false,
139        }
140    }
141
142    /// Content that the handler transcoded to UTF-8 while honoring the
143    /// requested `encoding` attribute.
144    ///
145    /// The parser will **not** emit a non-UTF-8 include-encoding warning for
146    /// content returned this way, since the handler has already reencoded it.
147    pub fn transcoded(content: impl Into<String>) -> Self {
148        Self {
149            content: content.into(),
150            encoding_handled: true,
151        }
152    }
153
154    /// Returns the UTF-8 content of the include.
155    pub fn content(&self) -> &str {
156        &self.content
157    }
158
159    /// Returns `true` if the handler honored the requested `encoding` attribute
160    /// (i.e. the content was created via [`IncludeContent::transcoded`]).
161    pub fn encoding_handled(&self) -> bool {
162        self.encoding_handled
163    }
164
165    /// Consumes the `IncludeContent`, returning the owned UTF-8 content.
166    pub fn into_content(self) -> String {
167        self.content
168    }
169}
170
171impl From<String> for IncludeContent {
172    fn from(content: String) -> Self {
173        Self::new(content)
174    }
175}
176
177impl From<&str> for IncludeContent {
178    fn from(content: &str) -> Self {
179        Self::new(content)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::{IncludeContent, IncludeResolution};
186
187    #[test]
188    fn resolution_from_include_content_is_found() {
189        let content = IncludeContent::new("Content.");
190        let resolution: IncludeResolution = content.clone().into();
191        assert_eq!(resolution, IncludeResolution::Found(content));
192    }
193
194    #[test]
195    fn new_does_not_mark_encoding_handled() {
196        let content = IncludeContent::new("Content.");
197        assert_eq!(content.content(), "Content.");
198        assert!(!content.encoding_handled());
199        assert_eq!(content.into_content(), "Content.".to_owned());
200    }
201
202    #[test]
203    fn transcoded_marks_encoding_handled() {
204        let content = IncludeContent::transcoded("Résumé.");
205        assert_eq!(content.content(), "Résumé.");
206        assert!(content.encoding_handled());
207        assert_eq!(content.into_content(), "Résumé.".to_owned());
208    }
209
210    #[test]
211    fn from_string_and_str_do_not_mark_encoding_handled() {
212        let from_string = IncludeContent::from("Content.".to_owned());
213        assert_eq!(from_string, IncludeContent::new("Content."));
214        assert!(!from_string.encoding_handled());
215
216        let from_str: IncludeContent = "Content.".into();
217        assert_eq!(from_str, IncludeContent::new("Content."));
218        assert!(!from_str.encoding_handled());
219    }
220}