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}