Skip to main content

acdc_parser/
options.rs

1use std::{
2    borrow::Cow,
3    path::{Path, PathBuf},
4};
5
6pub use crate::safe_mode::SafeMode;
7
8use crate::{
9    AttributeValue, DocumentAttributes, Error, IncludeLoader,
10    document_attribute::{InputKind, initialize_configuration, initialize_intrinsics},
11    model::RawAttributes,
12};
13
14const DEFAULT_MAX_TOTAL_INCLUDE_BYTES: usize = 64 * 1024 * 1024;
15
16#[derive(Debug, Clone, Default)]
17#[non_exhaustive]
18pub struct Options<'a> {
19    /// Defaults to [`SafeMode::Secure`], which prevents include reads.
20    pub safe_mode: SafeMode,
21    pub timings: bool,
22    pub(crate) document_attributes: DocumentAttributes<'a>,
23    /// Selects how the parser loads include sources.
24    ///
25    /// Defaults to [`IncludeLoader::System`], subject to `safe_mode`. Set
26    /// [`IncludeLoader::Disabled`] to preserve directives in modes below Secure.
27    pub include_loader: IncludeLoader,
28    pub(crate) max_total_include_bytes: Option<usize>,
29    /// Directory used to resolve relative includes from the entry input.
30    ///
31    /// Only consulted when include loading is enabled. String and reader input
32    /// default to the current working directory. File input normally uses the
33    /// entry file's parent, unless this value overrides it. In Safe and Server
34    /// modes this directory is also the local-include boundary.
35    pub base_dir: Option<PathBuf>,
36    /// Reject a manpage title that does not use the `name(volume)` form.
37    ///
38    /// This does not promote all warnings to errors. Use
39    /// [`crate::ParseResult::source_recovery`] to detect recovered content.
40    pub strict: bool,
41    /// Enable Setext-style (underlined) header parsing.
42    ///
43    /// When enabled, headers can use the legacy two-line syntax:
44    /// ```text
45    /// Document Title
46    /// ==============
47    /// ```
48    #[cfg(feature = "setext")]
49    pub setext: bool,
50}
51
52impl<'a> Options<'a> {
53    /// Create a new `OptionsBuilder` for fluent configuration.
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// use acdc_parser::{Options, SafeMode};
59    ///
60    /// let options = Options::builder()
61    ///     .with_safe_mode(SafeMode::Safe)
62    ///     .with_timings()
63    ///     .with_attribute("toc", "left")
64    ///     .build()?;
65    /// # Ok::<(), acdc_parser::Error>(())
66    /// ```
67    #[must_use]
68    pub fn builder() -> OptionsBuilder<'a> {
69        OptionsBuilder::default()
70    }
71
72    /// Create a new `Options` with default settings.
73    ///
74    /// Equivalent to `Options::default()`.
75    #[must_use]
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Create options from application-supplied attributes.
81    ///
82    /// # Errors
83    ///
84    /// Returns a structured error for an invalid attribute value.
85    pub fn with_attributes<N, V>(
86        attributes: impl IntoIterator<Item = (N, V)>,
87    ) -> Result<Self, Error>
88    where
89        N: Into<Cow<'a, str>>,
90        V: Into<AttributeValue<'a>>,
91    {
92        Self::builder().with_attributes(attributes).build()
93    }
94
95    /// Return the validated attributes configured for parsing.
96    #[must_use]
97    pub const fn document_attributes(&self) -> &DocumentAttributes<'a> {
98        &self.document_attributes
99    }
100
101    /// Maximum selected include text per parse, in bytes. Defaults to 64 MiB.
102    ///
103    /// See [`OptionsBuilder::with_max_total_include_bytes`] for what counts.
104    #[must_use]
105    pub fn max_total_include_bytes(&self) -> usize {
106        self.max_total_include_bytes
107            .unwrap_or(DEFAULT_MAX_TOTAL_INCLUDE_BYTES)
108    }
109
110    /// Consume the options and return their validated attribute collection.
111    #[must_use]
112    pub fn into_document_attributes(self) -> DocumentAttributes<'a> {
113        self.document_attributes
114    }
115
116    /// Replace the attribute snapshot with one from an already parsed document.
117    ///
118    /// This does not reclassify or revalidate assignments. Use the builder when
119    /// supplied values should become new application overrides.
120    #[must_use]
121    pub fn with_document_attributes(mut self, attributes: DocumentAttributes<'a>) -> Self {
122        self.document_attributes = attributes;
123        self
124    }
125
126    /// Reopen configuration while preserving application overrides and defaults.
127    #[must_use]
128    pub fn into_builder(self) -> OptionsBuilder<'a> {
129        let (attributes, defaults) = self.document_attributes.into_configuration();
130        OptionsBuilder {
131            attributes,
132            defaults,
133            safe_mode: self.safe_mode,
134            timings: self.timings,
135            include_loader: self.include_loader,
136            max_total_include_bytes: self.max_total_include_bytes,
137            base_dir: self.base_dir,
138            strict: self.strict,
139            #[cfg(feature = "setext")]
140            setext: self.setext,
141        }
142    }
143
144    pub(crate) fn prepare_for_parse(mut self, input_kind: InputKind<'_>) -> Self {
145        initialize_intrinsics(&mut self.document_attributes, self.safe_mode, input_kind);
146        self
147    }
148
149    /// Consume the options, producing an independent `'static` copy.
150    #[must_use]
151    pub fn into_static(self) -> Options<'static> {
152        Options {
153            safe_mode: self.safe_mode,
154            timings: self.timings,
155            document_attributes: self.document_attributes.into_static(),
156            include_loader: self.include_loader,
157            max_total_include_bytes: self.max_total_include_bytes,
158            base_dir: self.base_dir,
159            strict: self.strict,
160            #[cfg(feature = "setext")]
161            setext: self.setext,
162        }
163    }
164}
165
166/// Builder for `Options` that provides an API for configuration.
167///
168/// Create an `OptionsBuilder` using `Options::builder()`.
169///
170/// # Example
171///
172/// ```
173/// use acdc_parser::{Options, SafeMode};
174///
175/// let options = Options::builder()
176///     .with_safe_mode(SafeMode::Safe)
177///     .with_timings()
178///     .with_attribute("toc", "left")
179///     .with_attribute("sectnums", true)
180///     .build()?;
181/// # Ok::<(), acdc_parser::Error>(())
182/// ```
183#[derive(Debug, Clone, Default)]
184#[non_exhaustive]
185pub struct OptionsBuilder<'a> {
186    safe_mode: SafeMode,
187    timings: bool,
188    attributes: RawAttributes<'a>,
189    defaults: RawAttributes<'a>,
190    include_loader: IncludeLoader,
191    max_total_include_bytes: Option<usize>,
192    base_dir: Option<PathBuf>,
193    strict: bool,
194    #[cfg(feature = "setext")]
195    setext: bool,
196}
197
198impl<'a> OptionsBuilder<'a> {
199    /// Set the safe mode for parsing.
200    ///
201    /// # Example
202    ///
203    /// ```
204    /// use acdc_parser::{Options, SafeMode};
205    ///
206    /// let options = Options::builder()
207    ///     .with_safe_mode(SafeMode::Safe)
208    ///     .build()?;
209    /// # Ok::<(), acdc_parser::Error>(())
210    /// ```
211    #[must_use]
212    pub fn with_safe_mode(mut self, safe_mode: SafeMode) -> Self {
213        self.safe_mode = safe_mode;
214        self
215    }
216
217    /// Enable timing information during parsing.
218    ///
219    /// # Example
220    ///
221    /// ```
222    /// use acdc_parser::Options;
223    ///
224    /// let options = Options::builder()
225    ///     .with_timings()
226    ///     .build()?;
227    /// # Ok::<(), acdc_parser::Error>(())
228    /// ```
229    #[must_use]
230    pub fn with_timings(mut self) -> Self {
231        self.timings = true;
232        self
233    }
234
235    /// Select how the parser loads include sources.
236    ///
237    /// Defaults to [`IncludeLoader::System`]. The default Secure mode prevents
238    /// include reads even when a provider is supplied. Use [`IncludeLoader::Disabled`]
239    /// to disable loading in lower modes, or [`IncludeLoader::custom`] to supply
240    /// sources from an editor overlay, virtual filesystem, or custom transport.
241    ///
242    /// # Example
243    ///
244    /// ```
245    /// use acdc_parser::{IncludeLoader, Options, SafeMode};
246    ///
247    /// let options = Options::builder()
248    ///     .with_safe_mode(SafeMode::Safe)
249    ///     .with_include_loader(IncludeLoader::System)
250    ///     .build()?;
251    /// # Ok::<(), acdc_parser::Error>(())
252    /// ```
253    #[must_use]
254    pub fn with_include_loader(mut self, include_loader: IncludeLoader) -> Self {
255        self.include_loader = include_loader;
256        self
257    }
258
259    /// Set the total selected include text allowed per parse, in bytes.
260    ///
261    /// Defaults to 64 MiB. Each selection counts after `indent=`, plus one byte
262    /// for a separating newline if the text is not empty. This count precedes
263    /// nested processing and removal of comments and inactive conditionals.
264    /// Nested and repeated includes share the limit. The entry text does not count.
265    ///
266    /// Zero permits only empty selections. Document attributes cannot change this
267    /// limit. The 10 MiB per-include limit and safe-mode permissions still apply.
268    ///
269    /// # Example
270    ///
271    /// ```
272    /// use acdc_parser::{Options, SafeMode};
273    ///
274    /// let options = Options::builder()
275    ///     .with_safe_mode(SafeMode::Safe)
276    ///     .with_max_total_include_bytes(256 * 1024 * 1024)
277    ///     .build()?;
278    /// # Ok::<(), acdc_parser::Error>(())
279    /// ```
280    #[must_use]
281    pub fn with_max_total_include_bytes(mut self, bytes: usize) -> Self {
282        self.max_total_include_bytes = Some(bytes);
283        self
284    }
285
286    /// Set the directory used to resolve relative includes from the entry input.
287    ///
288    /// Only takes effect when include loading is enabled.
289    /// For file input this overrides the entry file's parent directory.
290    /// For string or reader input it overrides the current working directory.
291    /// Nested includes remain relative to the file that contains them.
292    #[must_use]
293    pub fn with_base_dir(mut self, base_dir: impl AsRef<Path>) -> Self {
294        self.base_dir = Some(base_dir.as_ref().to_path_buf());
295        self
296    }
297
298    /// Reject manpage titles that do not use the `name(volume)` form.
299    ///
300    /// Other recoverable problems still produce warnings.
301    ///
302    /// # Example
303    ///
304    /// ```
305    /// use acdc_parser::Options;
306    ///
307    /// let options = Options::builder()
308    ///     .with_strict()
309    ///     .build()?;
310    /// # Ok::<(), acdc_parser::Error>(())
311    /// ```
312    #[must_use]
313    pub fn with_strict(mut self) -> Self {
314        self.strict = true;
315        self
316    }
317
318    /// Supply an application attribute.
319    ///
320    /// Application values take precedence over document entries, subject to the
321    /// attribute's standard assignment rules.
322    ///
323    /// This is a convenience method that accepts various types for the value:
324    /// - `&str` becomes `AttributeValue::String`
325    /// - `bool` becomes `AttributeValue::Bool`
326    /// - `()` becomes `AttributeValue::None`
327    ///
328    /// # Example
329    ///
330    /// ```
331    /// use acdc_parser::Options;
332    ///
333    /// let options = Options::builder()
334    ///     .with_attribute("toc", "left")
335    ///     .with_attribute("sectnums", true)
336    ///     .build()?;
337    /// # Ok::<(), acdc_parser::Error>(())
338    /// ```
339    #[must_use]
340    pub fn with_attribute(
341        mut self,
342        name: impl Into<Cow<'a, str>>,
343        value: impl Into<AttributeValue<'a>>,
344    ) -> Self {
345        self.attributes.insert(name.into(), value.into());
346        self
347    }
348
349    /// Replace the application-supplied attributes.
350    #[must_use]
351    pub fn with_attributes<N, V>(mut self, attributes: impl IntoIterator<Item = (N, V)>) -> Self
352    where
353        N: Into<Cow<'a, str>>,
354        V: Into<AttributeValue<'a>>,
355    {
356        self.attributes = attributes
357            .into_iter()
358            .map(|(name, value)| (name.into(), value.into()))
359            .collect();
360        self
361    }
362
363    /// Replace defaults for attributes not supplied by the application.
364    ///
365    /// Document entries can replace these defaults when the attribute permits it.
366    /// Intrinsic backend values retain their normal precedence.
367    /// Backend, base backend, file type, and document type defaults also determine
368    /// their convenience attributes when the options are built.
369    #[must_use]
370    pub fn with_defaults<N, V>(mut self, defaults: impl IntoIterator<Item = (N, V)>) -> Self
371    where
372        N: Into<Cow<'a, str>>,
373        V: Into<AttributeValue<'a>>,
374    {
375        self.defaults = defaults
376            .into_iter()
377            .map(|(name, value)| (name.into(), value.into()))
378            .collect();
379        self
380    }
381
382    /// Supply a default without changing an application override.
383    #[must_use]
384    pub fn with_default_attribute(
385        mut self,
386        name: impl Into<Cow<'a, str>>,
387        value: impl Into<AttributeValue<'a>>,
388    ) -> Self {
389        self.defaults.insert(name.into(), value.into());
390        self
391    }
392
393    /// Inspect a supplied override or default before validation.
394    #[must_use]
395    pub fn attribute(&self, name: &str) -> Option<&AttributeValue<'a>> {
396        self.attributes
397            .get(name)
398            .or_else(|| self.defaults.get(name))
399    }
400
401    /// Enable Setext-style (underlined) header parsing.
402    ///
403    /// When enabled, headers can use the legacy two-line syntax where
404    /// the title is underlined with `=`, `-`, `~`, `^`, or `+` characters.
405    ///
406    /// # Example
407    ///
408    /// ```
409    /// use acdc_parser::Options;
410    ///
411    /// let options = Options::builder()
412    ///     .with_setext()
413    ///     .build()?;
414    /// # Ok::<(), acdc_parser::Error>(())
415    /// ```
416    #[cfg(feature = "setext")]
417    #[must_use]
418    pub fn with_setext(mut self) -> Self {
419        self.setext = true;
420        self
421    }
422
423    /// Build the `Options` from this builder.
424    ///
425    /// # Example
426    ///
427    /// ```
428    /// use acdc_parser::{Options, SafeMode};
429    ///
430    /// let options = Options::builder()
431    ///     .with_safe_mode(SafeMode::Safe)
432    ///     .build()?;
433    /// # Ok::<(), acdc_parser::Error>(())
434    /// ```
435    ///
436    /// # Errors
437    ///
438    /// Returns a structured error for an invalid attribute value.
439    pub fn build(self) -> Result<Options<'a>, Error> {
440        let document_attributes = initialize_configuration(self.attributes, self.defaults)?;
441        Ok(Options {
442            safe_mode: self.safe_mode,
443            timings: self.timings,
444            document_attributes,
445            include_loader: self.include_loader,
446            max_total_include_bytes: self.max_total_include_bytes,
447            base_dir: self.base_dir,
448            strict: self.strict,
449            #[cfg(feature = "setext")]
450            setext: self.setext,
451        })
452    }
453}