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    /// - [`IncludeResolution::NotDecodable`] signals that the file exists and
35    ///   was read but is not valid UTF-8 and the handler could not transcode it
36    ///   (see the `# Encoding` section); the parser records a
37    ///   [`WarningType::IncludeFileNotDecodable`] warning.
38    ///
39    /// The rendered replacement (`Unresolved directive …`) is identical for the
40    /// three failure reasons; only the warning differs, mirroring Asciidoctor's
41    /// separate `include file not found`, `include file not readable`, and
42    /// `invalid byte sequence in UTF-8` messages.
43    ///
44    /// [`WarningType::IncludeFileNotFound`]: crate::warnings::WarningType::IncludeFileNotFound
45    /// [`WarningType::IncludeFileNotReadable`]: crate::warnings::WarningType::IncludeFileNotReadable
46    /// [`WarningType::IncludeFileNotDecodable`]: crate::warnings::WarningType::IncludeFileNotDecodable
47    ///
48    /// # Options
49    /// With the exception of `encoding` (see below), the implementation should
50    /// not attempt to interpret any of the built-in attributes (i.e.
51    /// `leveloffset`, `lines`, `tags`, or `indent`). Correct handling of these
52    /// attributes will be provided by the parser itself.
53    ///
54    /// # Encoding
55    /// The content returned in [`IncludeContent`] is a typical Rust [`String`]
56    /// and therefore must be encoded as UTF-8.
57    ///
58    /// If the implementation is capable of transcoding from other formats, it
59    /// may use the `encoding` attribute as a hint of the source format. When it
60    /// transcodes the content to UTF-8, it should return the result via
61    /// [`IncludeContent::transcoded`] so that the parser knows the requested
62    /// encoding was honored and suppresses the non-UTF-8 include-encoding
63    /// warning.
64    ///
65    /// An implementation that only deals in UTF-8 should return its content via
66    /// [`IncludeContent::new`] (or the [`From`] conversions). If the directive
67    /// requested a non-UTF-8 `encoding`, the parser will emit a non-UTF-8
68    /// include-encoding warning in that case.
69    ///
70    /// If the implementation finds a file that is not encoded in UTF-8 and is
71    /// incapable of transcoding it (no `encoding` attribute, or one it does not
72    /// support), it should return [`IncludeResolution::NotDecodable`] so the
73    /// parser can name the real cause. Asciidoctor treats this condition as
74    /// fatal (`invalid byte sequence in UTF-8`); this crate favors recoverable
75    /// warnings, so it instead drops the include and records a
76    /// [`WarningType::IncludeFileNotDecodable`] warning.
77    ///
78    /// [`WarningType::IncludeFileNotDecodable`]: crate::warnings::WarningType::IncludeFileNotDecodable
79    fn resolve_target<'src>(
80        &self,
81        source: Option<&str>,
82        target: &str,
83        attrlist: &Attrlist<'src>,
84        parser: &Parser,
85    ) -> IncludeResolution;
86}
87
88/// The outcome of an [`IncludeFileHandler::resolve_target`] call: either the
89/// resolved content, or the reason resolution failed.
90///
91/// The failure reasons map to distinct warnings – mirroring Asciidoctor, which
92/// distinguishes a missing include file (`include file not found`) from one
93/// that is present but unreadable (`include file not readable`) or present but
94/// not valid UTF-8 (`invalid byte sequence in UTF-8`).
95///
96/// This enum is `non_exhaustive`: future resolution reasons may be recognized
97/// as the parser grows, so a host matching on it needs a catch-all arm. New
98/// variants can still be returned by an [`IncludeFileHandler`] implementation.
99#[derive(Clone, Debug, Eq, PartialEq)]
100#[non_exhaustive]
101pub enum IncludeResolution {
102    /// The include file was resolved; its content is carried here.
103    Found(IncludeContent),
104
105    /// No file could be found for the directive's target. The parser records a
106    /// [`WarningType::IncludeFileNotFound`] warning.
107    ///
108    /// [`WarningType::IncludeFileNotFound`]: crate::warnings::WarningType::IncludeFileNotFound
109    NotFound,
110
111    /// A file was found for the directive's target but could not be read (for
112    /// example a permission or other IO error). The parser records a
113    /// [`WarningType::IncludeFileNotReadable`] warning.
114    ///
115    /// [`WarningType::IncludeFileNotReadable`]: crate::warnings::WarningType::IncludeFileNotReadable
116    NotReadable,
117
118    /// A file was found and read for the directive's target but is not valid
119    /// UTF-8, and the handler was unable to transcode it (no `encoding`
120    /// attribute, or one it does not support). The parser records a
121    /// [`WarningType::IncludeFileNotDecodable`] warning.
122    ///
123    /// See the `# Encoding` section of [`IncludeFileHandler::resolve_target`]
124    /// for how a handler that *can* transcode should instead return
125    /// [`IncludeContent::transcoded`].
126    ///
127    /// [`WarningType::IncludeFileNotDecodable`]: crate::warnings::WarningType::IncludeFileNotDecodable
128    NotDecodable,
129}
130
131impl From<IncludeContent> for IncludeResolution {
132    fn from(content: IncludeContent) -> Self {
133        IncludeResolution::Found(content)
134    }
135}
136
137/// The content returned by an [`IncludeFileHandler`] for an `include::`
138/// directive, together with the metadata the parser needs to finish processing
139/// the include.
140///
141/// Construct one via [`IncludeContent::new`] for UTF-8 content that was read
142/// without interpreting the `encoding` attribute, or via
143/// [`IncludeContent::transcoded`] for content the handler transcoded to UTF-8
144/// while honoring a requested `encoding`. See the `# Encoding` section of
145/// [`IncludeFileHandler::resolve_target`] for details.
146#[derive(Clone, Debug, Eq, PartialEq)]
147pub struct IncludeContent {
148    content: String,
149    encoding_handled: bool,
150}
151
152impl IncludeContent {
153    /// UTF-8 content that was read **without** interpreting the `encoding`
154    /// attribute.
155    ///
156    /// If the `include::` directive requested a non-UTF-8 `encoding`, the
157    /// parser will emit a non-UTF-8 include-encoding warning. This is the
158    /// appropriate constructor for a handler that only deals in UTF-8.
159    pub fn new(content: impl Into<String>) -> Self {
160        Self {
161            content: content.into(),
162            encoding_handled: false,
163        }
164    }
165
166    /// Content that the handler transcoded to UTF-8 while honoring the
167    /// requested `encoding` attribute.
168    ///
169    /// The parser will **not** emit a non-UTF-8 include-encoding warning for
170    /// content returned this way, since the handler has already reencoded it.
171    pub fn transcoded(content: impl Into<String>) -> Self {
172        Self {
173            content: content.into(),
174            encoding_handled: true,
175        }
176    }
177
178    /// Returns the UTF-8 content of the include.
179    pub fn content(&self) -> &str {
180        &self.content
181    }
182
183    /// Returns `true` if the handler honored the requested `encoding` attribute
184    /// (i.e. the content was created via [`IncludeContent::transcoded`]).
185    pub fn encoding_handled(&self) -> bool {
186        self.encoding_handled
187    }
188
189    /// Consumes the `IncludeContent`, returning the owned UTF-8 content.
190    pub fn into_content(self) -> String {
191        self.content
192    }
193}
194
195impl From<String> for IncludeContent {
196    fn from(content: String) -> Self {
197        Self::new(content)
198    }
199}
200
201impl From<&str> for IncludeContent {
202    fn from(content: &str) -> Self {
203        Self::new(content)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::{IncludeContent, IncludeResolution};
210
211    #[test]
212    fn resolution_from_include_content_is_found() {
213        let content = IncludeContent::new("Content.");
214        let resolution: IncludeResolution = content.clone().into();
215        assert_eq!(resolution, IncludeResolution::Found(content));
216    }
217
218    #[test]
219    fn new_does_not_mark_encoding_handled() {
220        let content = IncludeContent::new("Content.");
221        assert_eq!(content.content(), "Content.");
222        assert!(!content.encoding_handled());
223        assert_eq!(content.into_content(), "Content.".to_owned());
224    }
225
226    #[test]
227    fn transcoded_marks_encoding_handled() {
228        let content = IncludeContent::transcoded("Résumé.");
229        assert_eq!(content.content(), "Résumé.");
230        assert!(content.encoding_handled());
231        assert_eq!(content.into_content(), "Résumé.".to_owned());
232    }
233
234    #[test]
235    fn from_string_and_str_do_not_mark_encoding_handled() {
236        let from_string = IncludeContent::from("Content.".to_owned());
237        assert_eq!(from_string, IncludeContent::new("Content."));
238        assert!(!from_string.encoding_handled());
239
240        let from_str: IncludeContent = "Content.".into();
241        assert_eq!(from_str, IncludeContent::new("Content."));
242        assert!(!from_str.encoding_handled());
243    }
244}