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 content of the include file (wrapped in [`IncludeContent`])
26    /// if found. If no file is found, return `None` and an appropriate warning
27    /// message will be generated.
28    ///
29    /// # Options
30    /// With the exception of `encoding` (see below), the implementation should
31    /// not attempt to interpret any of the built-in attributes (i.e.
32    /// `leveloffset`, `lines`, `tags`, or `indent`). Correct handling of these
33    /// attributes will be provided by the parser itself.
34    ///
35    /// # Encoding
36    /// The content returned in [`IncludeContent`] is a typical Rust [`String`]
37    /// and therefore must be encoded as UTF-8.
38    ///
39    /// If the implementation is capable of transcoding from other formats, it
40    /// may use the `encoding` attribute as a hint of the source format. When it
41    /// transcodes the content to UTF-8, it should return the result via
42    /// [`IncludeContent::transcoded`] so that the parser knows the requested
43    /// encoding was honored and suppresses the non-UTF-8 include-encoding
44    /// warning.
45    ///
46    /// An implementation that only deals in UTF-8 should return its content via
47    /// [`IncludeContent::new`] (or the [`From`] conversions). If the directive
48    /// requested a non-UTF-8 `encoding`, the parser will emit a non-UTF-8
49    /// include-encoding warning in that case.
50    ///
51    /// If the implementation finds a file that is not encoded in UTF-8 and is
52    /// incapable of transcoding it, it should return `None`.
53    fn resolve_target<'src>(
54        &self,
55        source: Option<&str>,
56        target: &str,
57        attrlist: &Attrlist<'src>,
58        parser: &Parser,
59    ) -> Option<IncludeContent>;
60}
61
62/// The content returned by an [`IncludeFileHandler`] for an `include::`
63/// directive, together with the metadata the parser needs to finish processing
64/// the include.
65///
66/// Construct one via [`IncludeContent::new`] for UTF-8 content that was read
67/// without interpreting the `encoding` attribute, or via
68/// [`IncludeContent::transcoded`] for content the handler transcoded to UTF-8
69/// while honoring a requested `encoding`. See the `# Encoding` section of
70/// [`IncludeFileHandler::resolve_target`] for details.
71#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct IncludeContent {
73    content: String,
74    encoding_handled: bool,
75}
76
77impl IncludeContent {
78    /// UTF-8 content that was read **without** interpreting the `encoding`
79    /// attribute.
80    ///
81    /// If the `include::` directive requested a non-UTF-8 `encoding`, the
82    /// parser will emit a non-UTF-8 include-encoding warning. This is the
83    /// appropriate constructor for a handler that only deals in UTF-8.
84    pub fn new(content: impl Into<String>) -> Self {
85        Self {
86            content: content.into(),
87            encoding_handled: false,
88        }
89    }
90
91    /// Content that the handler transcoded to UTF-8 while honoring the
92    /// requested `encoding` attribute.
93    ///
94    /// The parser will **not** emit a non-UTF-8 include-encoding warning for
95    /// content returned this way, since the handler has already reencoded it.
96    pub fn transcoded(content: impl Into<String>) -> Self {
97        Self {
98            content: content.into(),
99            encoding_handled: true,
100        }
101    }
102
103    /// Returns the UTF-8 content of the include.
104    pub fn content(&self) -> &str {
105        &self.content
106    }
107
108    /// Returns `true` if the handler honored the requested `encoding` attribute
109    /// (i.e. the content was created via [`IncludeContent::transcoded`]).
110    pub fn encoding_handled(&self) -> bool {
111        self.encoding_handled
112    }
113
114    /// Consumes the `IncludeContent`, returning the owned UTF-8 content.
115    pub fn into_content(self) -> String {
116        self.content
117    }
118}
119
120impl From<String> for IncludeContent {
121    fn from(content: String) -> Self {
122        Self::new(content)
123    }
124}
125
126impl From<&str> for IncludeContent {
127    fn from(content: &str) -> Self {
128        Self::new(content)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::IncludeContent;
135
136    #[test]
137    fn new_does_not_mark_encoding_handled() {
138        let content = IncludeContent::new("Content.");
139        assert_eq!(content.content(), "Content.");
140        assert!(!content.encoding_handled());
141        assert_eq!(content.into_content(), "Content.".to_owned());
142    }
143
144    #[test]
145    fn transcoded_marks_encoding_handled() {
146        let content = IncludeContent::transcoded("Résumé.");
147        assert_eq!(content.content(), "Résumé.");
148        assert!(content.encoding_handled());
149        assert_eq!(content.into_content(), "Résumé.".to_owned());
150    }
151
152    #[test]
153    fn from_string_and_str_do_not_mark_encoding_handled() {
154        let from_string = IncludeContent::from("Content.".to_owned());
155        assert_eq!(from_string, IncludeContent::new("Content."));
156        assert!(!from_string.encoding_handled());
157
158        let from_str: IncludeContent = "Content.".into();
159        assert_eq!(from_str, IncludeContent::new("Content."));
160        assert!(!from_str.encoding_handled());
161    }
162}