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,
10 document_attribute::{InputKind, initialize_configuration, initialize_intrinsics},
11 model::RawAttributes,
12};
13
14#[derive(Debug, Clone, Default)]
15#[non_exhaustive]
16pub struct Options<'a> {
17 pub safe_mode: SafeMode,
18 pub timings: bool,
19 pub(crate) document_attributes: DocumentAttributes<'a>,
20 /// Directory used to resolve relative includes from the entry input.
21 ///
22 /// String and reader input default to the current working directory. File
23 /// input normally uses the entry file's parent, unless this value overrides
24 /// it. In Safe and Server modes this directory is also the local-include
25 /// boundary.
26 pub base_dir: Option<PathBuf>,
27 /// Reject a manpage title that does not use the `name(volume)` form.
28 ///
29 /// This does not promote all warnings to errors. Use
30 /// [`crate::ParseResult::source_recovery`] to detect recovered content.
31 pub strict: bool,
32 /// Enable Setext-style (underlined) header parsing.
33 ///
34 /// When enabled, headers can use the legacy two-line syntax:
35 /// ```text
36 /// Document Title
37 /// ==============
38 /// ```
39 #[cfg(feature = "setext")]
40 pub setext: bool,
41}
42
43impl<'a> Options<'a> {
44 /// Create a new `OptionsBuilder` for fluent configuration.
45 ///
46 /// # Example
47 ///
48 /// ```
49 /// use acdc_parser::{Options, SafeMode};
50 ///
51 /// let options = Options::builder()
52 /// .with_safe_mode(SafeMode::Safe)
53 /// .with_timings()
54 /// .with_attribute("toc", "left")
55 /// .build()?;
56 /// # Ok::<(), acdc_parser::Error>(())
57 /// ```
58 #[must_use]
59 pub fn builder() -> OptionsBuilder<'a> {
60 OptionsBuilder::default()
61 }
62
63 /// Create a new `Options` with default settings.
64 ///
65 /// Equivalent to `Options::default()`.
66 #[must_use]
67 pub fn new() -> Self {
68 Self::default()
69 }
70
71 /// Create options from application-supplied attributes.
72 ///
73 /// # Errors
74 ///
75 /// Returns a structured error for an invalid attribute value.
76 pub fn with_attributes<N, V>(
77 attributes: impl IntoIterator<Item = (N, V)>,
78 ) -> Result<Self, Error>
79 where
80 N: Into<Cow<'a, str>>,
81 V: Into<AttributeValue<'a>>,
82 {
83 Self::builder().with_attributes(attributes).build()
84 }
85
86 /// Return the validated attributes configured for parsing.
87 #[must_use]
88 pub const fn document_attributes(&self) -> &DocumentAttributes<'a> {
89 &self.document_attributes
90 }
91
92 /// Consume the options and return their validated attribute collection.
93 #[must_use]
94 pub fn into_document_attributes(self) -> DocumentAttributes<'a> {
95 self.document_attributes
96 }
97
98 /// Replace the attribute snapshot with one from an already parsed document.
99 ///
100 /// This does not reclassify or revalidate assignments. Use the builder when
101 /// supplied values should become new application overrides.
102 #[must_use]
103 pub fn with_document_attributes(mut self, attributes: DocumentAttributes<'a>) -> Self {
104 self.document_attributes = attributes;
105 self
106 }
107
108 /// Reopen configuration while preserving application overrides and defaults.
109 #[must_use]
110 pub fn into_builder(self) -> OptionsBuilder<'a> {
111 let (attributes, defaults) = self.document_attributes.into_configuration();
112 OptionsBuilder {
113 attributes,
114 defaults,
115 safe_mode: self.safe_mode,
116 timings: self.timings,
117 base_dir: self.base_dir,
118 strict: self.strict,
119 #[cfg(feature = "setext")]
120 setext: self.setext,
121 }
122 }
123
124 pub(crate) fn prepare_for_parse(mut self, input_kind: InputKind<'_>) -> Self {
125 initialize_intrinsics(&mut self.document_attributes, self.safe_mode, input_kind);
126 self
127 }
128
129 /// Consume the options, producing an independent `'static` copy.
130 #[must_use]
131 pub fn into_static(self) -> Options<'static> {
132 Options {
133 safe_mode: self.safe_mode,
134 timings: self.timings,
135 document_attributes: self.document_attributes.into_static(),
136 base_dir: self.base_dir,
137 strict: self.strict,
138 #[cfg(feature = "setext")]
139 setext: self.setext,
140 }
141 }
142}
143
144/// Builder for `Options` that provides an API for configuration.
145///
146/// Create an `OptionsBuilder` using `Options::builder()`.
147///
148/// # Example
149///
150/// ```
151/// use acdc_parser::{Options, SafeMode};
152///
153/// let options = Options::builder()
154/// .with_safe_mode(SafeMode::Safe)
155/// .with_timings()
156/// .with_attribute("toc", "left")
157/// .with_attribute("sectnums", true)
158/// .build()?;
159/// # Ok::<(), acdc_parser::Error>(())
160/// ```
161#[derive(Debug, Clone, Default)]
162#[non_exhaustive]
163pub struct OptionsBuilder<'a> {
164 safe_mode: SafeMode,
165 timings: bool,
166 attributes: RawAttributes<'a>,
167 defaults: RawAttributes<'a>,
168 base_dir: Option<PathBuf>,
169 strict: bool,
170 #[cfg(feature = "setext")]
171 setext: bool,
172}
173
174impl<'a> OptionsBuilder<'a> {
175 /// Set the safe mode for parsing.
176 ///
177 /// # Example
178 ///
179 /// ```
180 /// use acdc_parser::{Options, SafeMode};
181 ///
182 /// let options = Options::builder()
183 /// .with_safe_mode(SafeMode::Safe)
184 /// .build()?;
185 /// # Ok::<(), acdc_parser::Error>(())
186 /// ```
187 #[must_use]
188 pub fn with_safe_mode(mut self, safe_mode: SafeMode) -> Self {
189 self.safe_mode = safe_mode;
190 self
191 }
192
193 /// Enable timing information during parsing.
194 ///
195 /// # Example
196 ///
197 /// ```
198 /// use acdc_parser::Options;
199 ///
200 /// let options = Options::builder()
201 /// .with_timings()
202 /// .build()?;
203 /// # Ok::<(), acdc_parser::Error>(())
204 /// ```
205 #[must_use]
206 pub fn with_timings(mut self) -> Self {
207 self.timings = true;
208 self
209 }
210
211 /// Set the directory used to resolve relative includes from the entry input.
212 ///
213 /// For file input this overrides the entry file's parent directory. Nested
214 /// includes remain relative to the file that contains them.
215 #[must_use]
216 pub fn with_base_dir(mut self, base_dir: impl AsRef<Path>) -> Self {
217 self.base_dir = Some(base_dir.as_ref().to_path_buf());
218 self
219 }
220
221 /// Reject manpage titles that do not use the `name(volume)` form.
222 ///
223 /// Other recoverable problems still produce warnings.
224 ///
225 /// # Example
226 ///
227 /// ```
228 /// use acdc_parser::Options;
229 ///
230 /// let options = Options::builder()
231 /// .with_strict()
232 /// .build()?;
233 /// # Ok::<(), acdc_parser::Error>(())
234 /// ```
235 #[must_use]
236 pub fn with_strict(mut self) -> Self {
237 self.strict = true;
238 self
239 }
240
241 /// Supply an application attribute.
242 ///
243 /// Application values take precedence over document entries, subject to the
244 /// attribute's standard assignment rules.
245 ///
246 /// This is a convenience method that accepts various types for the value:
247 /// - `&str` becomes `AttributeValue::String`
248 /// - `bool` becomes `AttributeValue::Bool`
249 /// - `()` becomes `AttributeValue::None`
250 ///
251 /// # Example
252 ///
253 /// ```
254 /// use acdc_parser::Options;
255 ///
256 /// let options = Options::builder()
257 /// .with_attribute("toc", "left")
258 /// .with_attribute("sectnums", true)
259 /// .build()?;
260 /// # Ok::<(), acdc_parser::Error>(())
261 /// ```
262 #[must_use]
263 pub fn with_attribute(
264 mut self,
265 name: impl Into<Cow<'a, str>>,
266 value: impl Into<AttributeValue<'a>>,
267 ) -> Self {
268 self.attributes.insert(name.into(), value.into());
269 self
270 }
271
272 /// Replace the application-supplied attributes.
273 #[must_use]
274 pub fn with_attributes<N, V>(mut self, attributes: impl IntoIterator<Item = (N, V)>) -> Self
275 where
276 N: Into<Cow<'a, str>>,
277 V: Into<AttributeValue<'a>>,
278 {
279 self.attributes = attributes
280 .into_iter()
281 .map(|(name, value)| (name.into(), value.into()))
282 .collect();
283 self
284 }
285
286 /// Replace defaults for attributes not supplied by the application.
287 ///
288 /// Document entries can replace these defaults when the attribute permits it.
289 /// Intrinsic backend values retain their normal precedence.
290 /// Backend, base backend, file type, and document type defaults also determine
291 /// their convenience attributes when the options are built.
292 #[must_use]
293 pub fn with_defaults<N, V>(mut self, defaults: impl IntoIterator<Item = (N, V)>) -> Self
294 where
295 N: Into<Cow<'a, str>>,
296 V: Into<AttributeValue<'a>>,
297 {
298 self.defaults = defaults
299 .into_iter()
300 .map(|(name, value)| (name.into(), value.into()))
301 .collect();
302 self
303 }
304
305 /// Supply a default without changing an application override.
306 #[must_use]
307 pub fn with_default_attribute(
308 mut self,
309 name: impl Into<Cow<'a, str>>,
310 value: impl Into<AttributeValue<'a>>,
311 ) -> Self {
312 self.defaults.insert(name.into(), value.into());
313 self
314 }
315
316 /// Inspect a supplied override or default before validation.
317 #[must_use]
318 pub fn attribute(&self, name: &str) -> Option<&AttributeValue<'a>> {
319 self.attributes
320 .get(name)
321 .or_else(|| self.defaults.get(name))
322 }
323
324 /// Enable Setext-style (underlined) header parsing.
325 ///
326 /// When enabled, headers can use the legacy two-line syntax where
327 /// the title is underlined with `=`, `-`, `~`, `^`, or `+` characters.
328 ///
329 /// # Example
330 ///
331 /// ```
332 /// use acdc_parser::Options;
333 ///
334 /// let options = Options::builder()
335 /// .with_setext()
336 /// .build()?;
337 /// # Ok::<(), acdc_parser::Error>(())
338 /// ```
339 #[cfg(feature = "setext")]
340 #[must_use]
341 pub fn with_setext(mut self) -> Self {
342 self.setext = true;
343 self
344 }
345
346 /// Build the `Options` from this builder.
347 ///
348 /// # Example
349 ///
350 /// ```
351 /// use acdc_parser::{Options, SafeMode};
352 ///
353 /// let options = Options::builder()
354 /// .with_safe_mode(SafeMode::Safe)
355 /// .build()?;
356 /// # Ok::<(), acdc_parser::Error>(())
357 /// ```
358 ///
359 /// # Errors
360 ///
361 /// Returns a structured error for an invalid attribute value.
362 pub fn build(self) -> Result<Options<'a>, Error> {
363 let document_attributes = initialize_configuration(self.attributes, self.defaults)?;
364 Ok(Options {
365 safe_mode: self.safe_mode,
366 timings: self.timings,
367 document_attributes,
368 base_dir: self.base_dir,
369 strict: self.strict,
370 #[cfg(feature = "setext")]
371 setext: self.setext,
372 })
373 }
374}