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}