asciidoc-parser 0.22.1

Parser for AsciiDoc format
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
use std::{
    collections::HashMap,
    sync::{Arc, LazyLock},
};

use crate::{
    document::InterpretedValue,
    parser::{AllowableValue, AttributeValue, ModificationContext},
};

/// The built-in default value of the `iconsdir` attribute, used when neither
/// `iconsdir` nor `imagesdir` has been configured. When `imagesdir` is set to a
/// non-empty value and `iconsdir` is left at this default, the icons directory
/// is instead derived as `{imagesdir}/icons` (see
/// [`Document::parse`](crate::document::Document)).
pub(crate) const DEFAULT_ICONSDIR: &str = "./images/icons";

/// The built-in attribute table is identical for every parser, so build it
/// once and keep it in a shared `static`. A [`Parser`] does **not** copy these
/// defaults into its own [`attribute_values`] map; instead it falls back to
/// this table on a lookup miss (see [`Parser::attribute_value`]), so creating
/// or cloning a parser allocates nothing per built-in attribute. This matters
/// because [`Parser::default`] (and parser cloning, e.g. for nested AsciiDoc
/// table cells) happens frequently. A parser only materializes an entry in its
/// own map when it *overrides* or *unsets* a built-in (the per-parser entry
/// then shadows this default).
///
/// [`Parser`]: crate::Parser
/// [`Parser::default`]: crate::Parser::default
/// [`Parser::attribute_value`]: crate::Parser::attribute_value
/// [`attribute_values`]: crate::Parser
static BUILT_IN_ATTRS: LazyLock<HashMap<String, AttributeValue>> =
    LazyLock::new(build_built_in_attrs);

/// The synthesized `backend-html5-doctype-{doctype}` attribute, which is
/// defined (with an empty value) only for the document's active doctype. It is
/// resolved on the fly from the current `doctype` rather than materialized, so
/// it lives here as a single shared value that lookups can hand out by
/// reference. See [`synthesized_attr`].
///
/// It is a read-only intrinsic: it tracks `doctype` automatically, so a
/// document header or body assignment to it (e.g.
/// `:backend-html5-doctype-article: x`) is silently ignored ([`ApiOnly`] +
/// [`silent_when_locked`]) rather than being allowed to shadow the intrinsic
/// empty value. This self-protection replaces the previous scheme
/// (materialize-and-lock inside an AsciiDoc table cell), which could not follow
/// the cell's dynamically-changing doctype.
///
/// [`ApiOnly`]: ModificationContext::ApiOnly
/// [`silent_when_locked`]: AttributeValue::silent_when_locked
static DERIVED_DOCTYPE_ATTR: LazyLock<AttributeValue> = LazyLock::new(|| AttributeValue {
    allowable_value: AllowableValue::Any,
    modification_context: ModificationContext::ApiOnly,
    silent_when_locked: true,
    value: InterpretedValue::Value(String::new()),
});

/// The synthesized `safe-mode-{name}` flag, which is defined (with an empty
/// value) only for the active safe mode. Like the derived doctype attribute it
/// is resolved on the fly (from `safe-mode-name`) rather than materialized, so
/// the flags of the inactive modes stay genuinely absent. See
/// [`synthesized_attr`].
///
/// It is likewise a read-only intrinsic (`ApiOnly` + `silent_when_locked`): a
/// document assignment to it is silently ignored rather than shadowing the
/// intrinsic value.
static SAFE_MODE_ACTIVE_FLAG: LazyLock<AttributeValue> = LazyLock::new(|| AttributeValue {
    allowable_value: AllowableValue::Any,
    modification_context: ModificationContext::ApiOnly,
    silent_when_locked: true,
    value: InterpretedValue::Set,
});

static BUILT_IN_DEFAULT_VALUES: LazyLock<Arc<HashMap<String, String>>> =
    LazyLock::new(|| Arc::new(build_built_in_default_values()));

/// Returns the shared built-in default for `name`, if one is defined.
pub(crate) fn built_in_attr(name: &str) -> Option<&'static AttributeValue> {
    BUILT_IN_ATTRS.get(name)
}

/// Iterates the shared built-in attribute defaults.
pub(crate) fn built_in_attrs_iter()
-> impl Iterator<Item = (&'static String, &'static AttributeValue)> {
    BUILT_IN_ATTRS.iter()
}

/// Resolves a *synthesized* attribute — one computed from other state rather
/// than stored in either attribute table — for the active document state:
///
/// * `backend-html5-doctype-{doctype}` is defined (empty) only for the active
///   `doctype`.
/// * `safe-mode-{name}` is defined (empty) only for the active safe mode (as
///   reported by `safe-mode-name`).
///
/// `overrides` is the caller's per-parser attribute map; its entries (layered
/// over the shared built-in defaults) determine the active doctype / safe mode.
/// Returns `None` for any name that is not a currently-active synthesized
/// attribute (so the inactive doctype/safe-mode flags stay absent, matching
/// Asciidoctor).
pub(crate) fn synthesized_attr(
    name: &str,
    overrides: &HashMap<String, AttributeValue>,
) -> Option<&'static AttributeValue> {
    // Reports whether the *stored* attribute `key` (never itself a synthesized
    // one) currently resolves to the plain value `expected`, letting a per-parser
    // entry shadow the built-in default.
    let has_value = |key: &str, expected: &str| -> bool {
        overrides
            .get(key)
            .or_else(|| BUILT_IN_ATTRS.get(key))
            .is_some_and(|av| matches!(&av.value, InterpretedValue::Value(v) if v == expected))
    };

    if let Some(suffix) = name.strip_prefix("backend-html5-doctype-") {
        return has_value("doctype", suffix).then(|| &*DERIVED_DOCTYPE_ATTR);
    }

    if let Some(suffix) = name.strip_prefix("safe-mode-")
        && matches!(suffix, "unsafe" | "safe" | "server" | "secure")
    {
        return has_value("safe-mode-name", suffix).then(|| &*SAFE_MODE_ACTIVE_FLAG);
    }

    None
}

pub(super) fn built_in_default_values() -> Arc<HashMap<String, String>> {
    BUILT_IN_DEFAULT_VALUES.clone()
}

fn build_built_in_attrs() -> HashMap<String, AttributeValue> {
    let mut attrs: HashMap<String, AttributeValue> = HashMap::new();

    // ## Character replacement attributes
    //
    // These provide portable replacements for common typographical marks,
    // non-visible characters, escapes for characters with special meaning in
    // AsciiDoc, and passthroughs for characters that get encoded by default.
    // See the reference page:
    // `ref/asciidoc-lang/docs/modules/attributes/pages/character-replacement-ref.
    // adoc`.
    //
    // The entries below are listed in the same order they appear on that
    // reference page. The replacement values match Ruby Asciidoctor's
    // `INTRINSIC_ATTRIBUTES` table (e.g. `cpp` resolves to `C&#43;&#43;`, not a
    // literal `C++`).
    let char_replacement = |value: &str| AttributeValue {
        allowable_value: AllowableValue::Any,
        modification_context: ModificationContext::ApiOnly,
        silent_when_locked: false,
        value: InterpretedValue::Value(value.into()),
    };

    // `blank` is an alias for `empty` for those who find this terminology
    // clearer.
    attrs.insert("blank".to_owned(), char_replacement(""));
    attrs.insert("empty".to_owned(), char_replacement(""));
    attrs.insert("sp".to_owned(), char_replacement(" "));
    attrs.insert("nbsp".to_owned(), char_replacement("&#160;"));
    attrs.insert("zwsp".to_owned(), char_replacement("&#8203;"));
    attrs.insert("wj".to_owned(), char_replacement("&#8288;"));
    attrs.insert("apos".to_owned(), char_replacement("&#39;"));
    attrs.insert("quot".to_owned(), char_replacement("&#34;"));
    attrs.insert("lsquo".to_owned(), char_replacement("&#8216;"));
    attrs.insert("rsquo".to_owned(), char_replacement("&#8217;"));
    attrs.insert("ldquo".to_owned(), char_replacement("&#8220;"));
    attrs.insert("rdquo".to_owned(), char_replacement("&#8221;"));
    attrs.insert("deg".to_owned(), char_replacement("&#176;"));
    attrs.insert("plus".to_owned(), char_replacement("&#43;"));
    attrs.insert("brvbar".to_owned(), char_replacement("&#166;"));
    attrs.insert("vbar".to_owned(), char_replacement("|"));
    attrs.insert("amp".to_owned(), char_replacement("&"));
    attrs.insert("lt".to_owned(), char_replacement("<"));
    attrs.insert("gt".to_owned(), char_replacement(">"));
    attrs.insert("startsb".to_owned(), char_replacement("["));
    attrs.insert("endsb".to_owned(), char_replacement("]"));
    attrs.insert("caret".to_owned(), char_replacement("^"));
    attrs.insert("asterisk".to_owned(), char_replacement("*"));
    attrs.insert("tilde".to_owned(), char_replacement("~"));
    attrs.insert("backslash".to_owned(), char_replacement("\\"));
    attrs.insert("backtick".to_owned(), char_replacement("`"));
    attrs.insert("two-colons".to_owned(), char_replacement("::"));
    attrs.insert("two-semicolons".to_owned(), char_replacement(";;"));

    // `cpp` is deprecated in favor of `cxx`; both resolve to the same value.
    attrs.insert("cpp".to_owned(), char_replacement("C&#43;&#43;"));
    attrs.insert("cxx".to_owned(), char_replacement("C&#43;&#43;"));
    attrs.insert("pp".to_owned(), char_replacement("&#43;&#43;"));

    // ## Other predefined document attributes
    //
    // The groups below mirror the catalog in
    // `ref/asciidoc-lang/docs/modules/attributes/pages/document-attributes-ref.
    // adoc`. Order is not significant. Default values match Ruby Asciidoctor.
    //
    // Only attributes that are *set by default* are registered here, so they
    // resolve on a pristine parser. Attributes that are not set by default but
    // have a default value (the reference page's implied `(x)` and effective
    // `_empty_[=x]` values, e.g. `lang`, `toclevels`, `icons`) are recorded in
    // [`build_built_in_default_values`] instead: they stay absent (so an
    // attribute reference such as `{lang}` is treated as missing), but the
    // default value is applied when the attribute is later set with an empty
    // value.
    use InterpretedValue::{Set, Unset, Value};
    use ModificationContext::{Anywhere, ApiOnly, ApiOrHeader};

    // Holds a fixed value (`allowable_value` is `Any`).
    let any = |ctx, value| AttributeValue {
        allowable_value: AllowableValue::Any,
        modification_context: ctx,
        silent_when_locked: false,
        value,
    };

    // Set by default to a concrete `default` value. The value is stored directly
    // (not via [`build_built_in_default_values`]) so that setting the attribute
    // with an empty value overrides it with an empty value, rather than
    // re-applying the default.
    let set = |ctx, default: &str| AttributeValue {
        allowable_value: AllowableValue::Any,
        modification_context: ctx,
        silent_when_locked: false,
        value: Value(default.to_owned()),
    };

    // Set by default to an empty value (a boolean-style switch).
    let empty = |ctx, value| AttributeValue {
        allowable_value: AllowableValue::Empty,
        modification_context: ctx,
        silent_when_locked: false,
        value,
    };

    // ### Compliance attributes
    attrs.insert("attribute-missing".to_owned(), set(Anywhere, "skip"));
    attrs.insert("attribute-undefined".to_owned(), set(Anywhere, "drop-line"));

    // ### Localization and numbering attributes
    attrs.insert("appendix-caption".to_owned(), set(Anywhere, "Appendix"));
    attrs.insert("appendix-refsig".to_owned(), set(Anywhere, "Appendix"));
    attrs.insert("caution-caption".to_owned(), set(Anywhere, "Caution"));
    attrs.insert("chapter-refsig".to_owned(), set(Anywhere, "Chapter"));
    attrs.insert("example-caption".to_owned(), set(Anywhere, "Example"));
    attrs.insert("figure-caption".to_owned(), set(Anywhere, "Figure"));
    attrs.insert("important-caption".to_owned(), set(Anywhere, "Important"));
    attrs.insert(
        "last-update-label".to_owned(),
        set(ApiOrHeader, "Last updated"),
    );
    attrs.insert("note-caption".to_owned(), set(Anywhere, "Note"));
    attrs.insert("part-refsig".to_owned(), set(Anywhere, "Part"));
    attrs.insert("section-refsig".to_owned(), set(Anywhere, "Section"));
    attrs.insert("table-caption".to_owned(), set(Anywhere, "Table"));
    attrs.insert("tip-caption".to_owned(), set(Anywhere, "Tip"));
    attrs.insert(
        "toc-title".to_owned(),
        set(ApiOrHeader, "Table of Contents"),
    );
    attrs.insert("untitled-label".to_owned(), set(ApiOrHeader, "Untitled"));
    attrs.insert("version-label".to_owned(), set(ApiOrHeader, "Version"));
    attrs.insert("warning-caption".to_owned(), set(Anywhere, "Warning"));

    // ### Section title and table of contents attributes
    attrs.insert("idprefix".to_owned(), any(Anywhere, Value("_".into())));
    attrs.insert("idseparator".to_owned(), any(Anywhere, Value("_".into())));
    attrs.insert("sectids".to_owned(), empty(Anywhere, Set));
    attrs.insert("sectnums".to_owned(), empty(Anywhere, Unset));
    attrs.insert(
        "sectnumlevels".to_owned(),
        any(ApiOrHeader, Value("3".into())),
    );
    attrs.insert("toc".to_owned(), any(ApiOrHeader, Unset));

    // ### General content and formatting attributes
    //
    // The document type defaults to `article` and may be set in the header or
    // via the API. The derived `backend-html5-doctype-{doctype}` attribute
    // (defined below) is kept in sync by `Parser::refresh_doctype_derived_attr`
    // whenever `doctype` changes.
    attrs.insert(
        "doctype".to_owned(),
        any(ApiOrHeader, Value("article".into())),
    );

    // The file extension of the output file (always begins with a period),
    // defaulting to `.html`. Docinfo file names are built from this suffix.
    attrs.insert(
        "outfilesuffix".to_owned(),
        any(ApiOrHeader, Value(".html".into())),
    );

    // TO DO (https://github.com/asciidoc-rs/asciidoc-parser/issues/657): `relfilesuffix` should default to the current value of
    // `outfilesuffix` rather than a hardcoded `.html`; they diverge for
    // non-HTML backends (e.g. `.xml` for DocBook).
    attrs.insert("relfilesuffix".to_owned(), set(Anywhere, ".html"));
    attrs.insert("webfonts".to_owned(), empty(ApiOrHeader, Set));

    // ### Image and icon attributes
    attrs.insert("iconfont-remote".to_owned(), empty(ApiOrHeader, Set));

    // The default is `{imagesdir}/icons`; when `imagesdir` is left empty this
    // resolves to `./images/icons`. The `imagesdir`-relative derivation for a
    // non-empty `imagesdir` is applied after the header is parsed (see
    // `Document::parse`).
    attrs.insert("iconsdir".to_owned(), set(Anywhere, DEFAULT_ICONSDIR));
    attrs.insert("imagesdir".to_owned(), any(Anywhere, Set));

    // ### Source highlighting and formatting attributes
    attrs.insert("prewrap".to_owned(), empty(Anywhere, Set));

    // ### HTML styling attributes
    attrs.insert("copycss".to_owned(), any(ApiOrHeader, Set));
    attrs.insert("stylesdir".to_owned(), set(ApiOrHeader, "."));
    attrs.insert("stylesheet".to_owned(), any(ApiOrHeader, Set));

    // ### Security attributes
    attrs.insert("max-include-depth".to_owned(), set(ApiOnly, "64"));

    // ### Safe-mode intrinsic attributes
    //
    // These describe the default safe mode (`SafeMode::Secure`).
    // `Parser::with_safe_mode` overrides `safe-mode-level` and `safe-mode-name`
    // (via `apply_safe_mode_attributes`) when the caller chooses a different
    // mode. The active `safe-mode-<name>` flag is *not* stored here: it is
    // synthesized on the fly from `safe-mode-name` (see [`synthesized_attr`]), so
    // exactly one flag is ever defined and the inactive flags stay absent.
    attrs.insert(
        "safe-mode-level".to_owned(),
        any(ApiOnly, Value("20".into())),
    );
    attrs.insert(
        "safe-mode-name".to_owned(),
        any(ApiOnly, Value("secure".into())),
    );

    // NOTE: The derived `backend-html5-doctype-{doctype}` attribute (see
    // `doctype` above) is *not* registered here. It is synthesized on the fly
    // for the active doctype by `Parser::attribute_value` (via
    // [`derived_doctype_attr`]), so it never needs to be materialized or kept in
    // sync when `doctype` changes.

    attrs
}

fn build_built_in_default_values() -> HashMap<String, String> {
    // The value assigned to a built-in attribute when it is set (or turned on)
    // with an empty value (e.g. a bare `:toc:` resolves to `auto`, and a bare
    // `:lang:` resolves to `en`).
    //
    // This map holds the defaults for attributes that are *not* set by default:
    // the reference page's "turn-on" attributes (`toc`, `sectnums`) and its
    // implied `(x)` / effective `_empty_[=x]` values. Because these attributes
    // are absent from [`build_built_in_attrs`], they are treated as missing when
    // referenced while unset; their default is applied only once they are
    // explicitly set. Attributes that *are* set by default store their value
    // directly in [`build_built_in_attrs`] and do not appear here, so setting
    // one with an empty value overrides it with an empty value.
    //
    // `docinfosubs` is intentionally omitted: its implied default of
    // `attributes` is handled where docinfo substitution is resolved (an unset
    // value means "apply attribute substitution"), so a bare `:docinfosubs:`
    // must remain empty rather than resolving to `attributes`.
    let mut defaults: HashMap<String, String> = HashMap::new();

    // Turn-on attributes (reference page section: section title / TOC).
    defaults.insert("sectnums".to_owned(), "all".to_owned());
    defaults.insert("toc".to_owned(), "auto".to_owned());
    defaults.insert("toclevels".to_owned(), "2".to_owned());

    // Numbering seeds (localization and numbering attributes).
    defaults.insert("appendix-number".to_owned(), "@".to_owned());
    defaults.insert("chapter-number".to_owned(), "0".to_owned());
    defaults.insert("example-number".to_owned(), "0".to_owned());
    defaults.insert("figure-number".to_owned(), "0".to_owned());
    defaults.insert("footnote-number".to_owned(), "0".to_owned());
    defaults.insert("listing-number".to_owned(), "0".to_owned());
    defaults.insert("table-number".to_owned(), "0".to_owned());
    defaults.insert("lang".to_owned(), "en".to_owned());
    defaults.insert("manname-title".to_owned(), "Name".to_owned());

    // General content and formatting attributes.
    defaults.insert("asset-uri-scheme".to_owned(), "https".to_owned());
    defaults.insert("docinfo".to_owned(), "private".to_owned());
    defaults.insert("eqnums".to_owned(), "AMS".to_owned());
    defaults.insert("media".to_owned(), "screen".to_owned());
    defaults.insert("pagewidth".to_owned(), "425".to_owned());
    defaults.insert("stem".to_owned(), "asciimath".to_owned());
    defaults.insert("table-frame".to_owned(), "all".to_owned());
    defaults.insert("table-grid".to_owned(), "all".to_owned());
    defaults.insert("table-stripes".to_owned(), "none".to_owned());

    // Image and icon attributes.
    defaults.insert("iconfont-name".to_owned(), "font-awesome".to_owned());
    defaults.insert("icons".to_owned(), "image".to_owned());
    defaults.insert("icontype".to_owned(), "png".to_owned());

    // Source highlighting and formatting attributes.
    defaults.insert("coderay-css".to_owned(), "class".to_owned());
    defaults.insert("coderay-linenums-mode".to_owned(), "table".to_owned());
    defaults.insert("highlightjs-theme".to_owned(), "github".to_owned());
    defaults.insert("prettify-theme".to_owned(), "prettify".to_owned());
    defaults.insert("pygments-css".to_owned(), "class".to_owned());
    defaults.insert("pygments-linenums-mode".to_owned(), "table".to_owned());
    defaults.insert("pygments-style".to_owned(), "default".to_owned());
    defaults.insert("rouge-css".to_owned(), "class".to_owned());
    defaults.insert("rouge-linenums-mode".to_owned(), "table".to_owned());
    defaults.insert("rouge-style".to_owned(), "github".to_owned());

    // HTML styling attributes.
    defaults.insert("toc-class".to_owned(), "toc".to_owned());

    // Manpage attributes.
    defaults.insert("man-linkstyle".to_owned(), "blue R <>".to_owned());

    defaults
}