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}