1use std::{fmt, num::NonZeroUsize};
2
3use serde::Serialize;
4
5use crate::{ElementAttributes, InlineNode, Location, Source, StemNotation, Substitution};
6
7pub const ICON_SIZES: &[&str] = &["1x", "2x", "3x", "4x", "5x", "lg", "fw"];
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
11#[non_exhaustive]
12pub struct Pass<'a> {
13 #[serde(skip)]
14 pub(crate) attribute_fragments: Box<[crate::model::AttributeInlineFragment]>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub text: Option<&'a str>,
17 #[serde(default, skip_serializing_if = "Vec::is_empty")]
18 pub substitutions: Vec<Substitution>,
19 pub location: Location,
20 #[serde(skip)]
21 pub kind: PassthroughKind,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq, Serialize, Default)]
25pub enum PassthroughKind {
26 #[default]
27 Single,
28 Double,
29 Triple,
30 Macro,
31 AttributeRef,
34}
35
36#[derive(Clone, Debug, PartialEq, Serialize)]
38#[non_exhaustive]
39pub struct Footnote<'a> {
40 #[serde(skip)]
42 pub(crate) definition_source: Option<&'a str>,
43 #[serde(skip)]
45 pub(crate) registration_substitutions: Option<crate::model::substitution::SubstitutionPlan>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub id: Option<&'a str>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub content: Vec<InlineNode<'a>>,
50 #[serde(skip)]
51 pub number: u32,
52 pub location: Location,
53}
54
55#[derive(Clone, Debug, PartialEq, Serialize)]
57#[non_exhaustive]
58pub struct Icon<'a> {
59 pub target: Source<'a>,
60 pub attributes: ElementAttributes<'a>,
61 pub location: Location,
62}
63
64#[derive(Clone, Debug, PartialEq, Serialize)]
66#[non_exhaustive]
67pub struct Link<'a> {
68 #[serde(skip_serializing)]
69 pub text: Vec<InlineNode<'a>>,
70 pub target: Source<'a>,
71 pub attributes: ElementAttributes<'a>,
72 pub location: Location,
73 #[serde(skip)]
74 pub(crate) hide_uri_scheme: bool,
75}
76
77impl<'a> Link<'a> {
78 #[must_use]
80 pub fn new(target: Source<'a>, location: Location) -> Self {
81 Self {
82 text: Vec::new(),
83 target,
84 attributes: ElementAttributes::default(),
85 location,
86 hide_uri_scheme: false,
87 }
88 }
89
90 #[must_use]
92 pub fn with_text(mut self, text: Vec<InlineNode<'a>>) -> Self {
93 self.text = text;
94 self
95 }
96
97 #[must_use]
99 pub fn with_attributes(mut self, attributes: ElementAttributes<'a>) -> Self {
100 self.attributes = attributes;
101 self
102 }
103
104 #[must_use]
106 pub fn hides_uri_scheme(&self) -> bool {
107 self.hide_uri_scheme
108 }
109}
110
111#[derive(Clone, Debug, PartialEq, Serialize)]
113#[non_exhaustive]
114pub struct Url<'a> {
115 #[serde(skip_serializing)]
116 pub text: Vec<InlineNode<'a>>,
117 pub target: Source<'a>,
118 pub attributes: ElementAttributes<'a>,
119 pub location: Location,
120 #[serde(skip)]
121 pub(crate) hide_uri_scheme: bool,
122}
123
124impl Url<'_> {
125 #[must_use]
127 pub fn hides_uri_scheme(&self) -> bool {
128 self.hide_uri_scheme
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, Serialize)]
134#[non_exhaustive]
135pub struct Mailto<'a> {
136 #[serde(skip_serializing)]
137 pub text: Vec<InlineNode<'a>>,
138 pub target: Source<'a>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub subject: Option<&'a str>,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub body: Option<&'a str>,
145 pub attributes: ElementAttributes<'a>,
146 pub location: Location,
147}
148
149#[derive(Clone, Debug, PartialEq, Serialize)]
151#[non_exhaustive]
152pub struct Button<'a> {
153 pub label: &'a str,
154 pub location: Location,
155}
156
157#[derive(Clone, Debug, PartialEq, Serialize)]
159#[non_exhaustive]
160pub struct Menu<'a> {
161 pub target: &'a str,
162 #[serde(default, skip_serializing_if = "Vec::is_empty")]
163 pub items: Vec<&'a str>,
164 pub location: Location,
165}
166
167#[derive(Clone, Debug, PartialEq, Serialize)]
169#[non_exhaustive]
170pub struct Keyboard<'a> {
171 pub keys: Vec<Key<'a>>,
172 pub location: Location,
173}
174
175impl<'a> Keyboard<'a> {
176 #[must_use]
178 pub fn new(keys: Vec<Key<'a>>, location: Location) -> Self {
179 Self { keys, location }
180 }
181}
182
183pub type Key<'a> = &'a str;
185
186#[derive(Clone, Serialize)]
191#[non_exhaustive]
192pub struct CrossReference<'a> {
193 pub target: &'a str,
198 #[serde(skip)]
201 pub target_is_local: bool,
202 #[serde(skip_serializing)]
203 pub text: Vec<InlineNode<'a>>,
204 pub location: Location,
205 #[serde(skip)]
206 pub xrefstyle: XrefStyle,
207 #[serde(skip)]
208 pub caption_label: XrefCaptionLabel<'a>,
209 #[serde(skip)]
210 signifier: XrefSignifier<'a>,
211 #[serde(skip)]
212 pub(crate) section_signifiers: Option<&'a [XrefSignifier<'a>; REFSIG_ATTRIBUTES.len()]>,
213 #[serde(skip)]
216 pub role: Option<&'a str>,
217 #[serde(skip)]
218 pub(crate) caption_label_snapshot_id: Option<NonZeroUsize>,
219 #[serde(skip)]
220 pub(crate) resolve_natural_target: bool,
221 #[serde(skip)]
222 pub(crate) source_syntax: XrefSourceSyntax,
223}
224
225impl<'a> CrossReference<'a> {
226 #[must_use]
230 pub fn new(target: &'a str, location: Location) -> Self {
231 Self {
232 target,
233 target_is_local: Self::is_local_target(target),
234 text: Vec::new(),
235 location,
236 xrefstyle: XrefStyle::Default,
237 caption_label: XrefCaptionLabel::AtTarget,
238 signifier: XrefSignifier::Standard,
239 section_signifiers: None,
240 role: None,
241 caption_label_snapshot_id: None,
242 resolve_natural_target: false,
243 source_syntax: XrefSourceSyntax::Literal,
244 }
245 }
246
247 pub(crate) fn is_local_target(target: &str) -> bool {
248 match target.split_once('#') {
249 Some((path, _)) => path.is_empty(),
250 None => !target.contains(['.', ':']),
251 }
252 }
253
254 #[must_use]
256 pub fn with_text(mut self, text: Vec<InlineNode<'a>>) -> Self {
257 self.text = text;
258 self
259 }
260
261 #[must_use]
264 pub const fn signifier(&self) -> XrefSignifier<'a> {
265 self.signifier
266 }
267
268 pub fn set_signifier(&mut self, signifier: XrefSignifier<'a>) {
271 self.signifier = signifier;
272 self.section_signifiers = None;
273 }
274
275 pub(crate) fn refresh_signifier(&mut self, name: Option<&str>) {
276 let Some(signifiers) = self.section_signifiers else {
277 return;
278 };
279 self.signifier = name
280 .and_then(|name| {
281 REFSIG_ATTRIBUTES
282 .iter()
283 .position(|(candidate, _)| *candidate == name)
284 })
285 .and_then(|index| signifiers.get(index))
286 .copied()
287 .unwrap_or_default();
288 }
289}
290
291impl fmt::Debug for CrossReference<'_> {
292 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
293 formatter
294 .debug_struct("CrossReference")
295 .field("target", &self.target)
296 .field("target_is_local", &self.target_is_local)
297 .field("text", &self.text)
298 .field("location", &self.location)
299 .field("xrefstyle", &self.xrefstyle)
300 .field("caption_label", &self.caption_label)
301 .field("signifier", &self.signifier)
302 .field("role", &self.role)
303 .finish()
304 }
305}
306
307impl PartialEq for CrossReference<'_> {
308 fn eq(&self, other: &Self) -> bool {
309 self.target == other.target
310 && self.target_is_local == other.target_is_local
311 && self.text == other.text
312 && self.location == other.location
313 && self.xrefstyle == other.xrefstyle
314 && self.caption_label == other.caption_label
315 && self.signifier == other.signifier
316 && self.role == other.role
317 }
318}
319
320#[derive(Clone, Copy, Debug)]
321pub(crate) enum XrefSourceSyntax {
322 Literal,
323 Shorthand,
324 Macro,
325}
326
327impl XrefSourceSyntax {
328 pub(crate) fn target_is_local(self, target: &str) -> bool {
329 if matches!(self, Self::Literal) {
333 return true;
334 }
335 match target.split_once('#') {
336 Some((path, _)) => path.is_empty(),
337 None => {
338 matches!(self, Self::Shorthand)
339 || !target.rsplit('/').next().unwrap_or(target).contains('.')
340 }
341 }
342 }
343}
344
345#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
350#[non_exhaustive]
351pub enum XrefSignifier<'a> {
352 #[default]
355 Standard,
356 AtReference(&'a str),
360 Omitted,
363}
364
365pub(crate) const REFSIG_ATTRIBUTES: [(&str, &str); 11] = [
368 ("part", "part-refsig"),
369 ("chapter", "chapter-refsig"),
370 ("section", "section-refsig"),
371 ("appendix", "appendix-refsig"),
372 ("preface", "preface-refsig"),
373 ("abstract", "abstract-refsig"),
374 ("dedication", "dedication-refsig"),
375 ("colophon", "colophon-refsig"),
376 ("glossary", "glossary-refsig"),
377 ("bibliography", "bibliography-refsig"),
378 ("index", "index-refsig"),
379];
380
381#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
383#[non_exhaustive]
384pub enum XrefCaptionLabel<'a> {
385 #[default]
387 AtTarget,
388 AtReference(&'a str),
390 NumberOnly,
392}
393
394#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
399#[non_exhaustive]
400pub enum XrefStyle {
401 #[default]
403 Default,
404 Basic,
406 Short,
408 Full,
410}
411
412impl XrefStyle {
413 pub(crate) fn from_attribute(value: Option<&str>) -> Self {
414 match value {
415 None => Self::Default,
416 Some("short") => Self::Short,
417 Some("full") => Self::Full,
418 _ => Self::Basic,
419 }
420 }
421}
422
423#[derive(Clone, Debug, PartialEq, Serialize)]
425#[non_exhaustive]
426pub struct Autolink<'a> {
427 pub url: Source<'a>,
428 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
431 pub bracketed: bool,
432 pub location: Location,
433 #[serde(skip)]
434 pub(crate) hide_uri_scheme: bool,
435}
436
437impl Autolink<'_> {
438 #[must_use]
440 pub fn hides_uri_scheme(&self) -> bool {
441 self.hide_uri_scheme
442 }
443}
444
445#[derive(Clone, Debug, PartialEq, Serialize)]
447#[non_exhaustive]
448pub struct Stem<'a> {
449 pub content: &'a str,
450 pub notation: StemNotation,
451 pub location: Location,
452}
453
454#[derive(Clone, Debug, PartialEq, Serialize)]
456#[non_exhaustive]
457pub enum IndexTermKind<'a> {
458 Flow(Vec<InlineNode<'a>>),
460 Concealed {
462 term: Vec<InlineNode<'a>>,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
466 secondary: Option<Vec<InlineNode<'a>>>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
469 tertiary: Option<Vec<InlineNode<'a>>>,
470 },
471}
472
473#[derive(Clone, Debug, PartialEq, Serialize)]
475#[serde(tag = "kind", rename_all = "kebab-case")]
476#[non_exhaustive]
477pub enum IndexTermRelationship<'a> {
478 See {
480 target: Vec<InlineNode<'a>>,
482 },
483 SeeAlso {
485 targets: Vec<Vec<InlineNode<'a>>>,
487 },
488}
489
490#[derive(Clone, Debug, PartialEq, Serialize)]
492#[non_exhaustive]
493pub struct IndexTerm<'a> {
494 pub kind: IndexTermKind<'a>,
496 #[serde(skip_serializing_if = "Option::is_none")]
498 pub relationship: Option<IndexTermRelationship<'a>>,
499 #[serde(skip_serializing_if = "Option::is_none")]
501 pub(crate) catalog: Option<Box<IndexTerm<'a>>>,
502 #[serde(skip)]
504 pub(crate) catalog_substitutions: Option<crate::model::substitution::SubstitutionPlan>,
505 pub location: Location,
506}
507
508impl<'a> IndexTerm<'a> {
509 pub(crate) fn for_each_label_mut(&mut self, mut visit: impl FnMut(&mut Vec<InlineNode<'a>>)) {
510 match &mut self.kind {
511 IndexTermKind::Flow(term) => visit(term),
512 IndexTermKind::Concealed {
513 term,
514 secondary,
515 tertiary,
516 } => {
517 visit(term);
518 if let Some(secondary) = secondary {
519 visit(secondary);
520 }
521 if let Some(tertiary) = tertiary {
522 visit(tertiary);
523 }
524 }
525 }
526 match &mut self.relationship {
527 Some(IndexTermRelationship::See { target }) => visit(target),
528 Some(IndexTermRelationship::SeeAlso { targets }) => {
529 for target in targets {
530 visit(target);
531 }
532 }
533 None => {}
534 }
535 }
536
537 #[must_use]
542 pub fn catalog_entry(&self) -> &Self {
543 self.catalog.as_deref().unwrap_or(self)
544 }
545
546 #[must_use]
548 pub fn term(&self) -> &[InlineNode<'a>] {
549 match &self.kind {
550 IndexTermKind::Flow(term) | IndexTermKind::Concealed { term, .. } => term,
551 }
552 }
553
554 #[must_use]
556 pub fn secondary(&self) -> Option<&[InlineNode<'a>]> {
557 match &self.kind {
558 IndexTermKind::Flow(_) => None,
559 IndexTermKind::Concealed { secondary, .. } => secondary.as_deref(),
560 }
561 }
562
563 #[must_use]
565 pub fn tertiary(&self) -> Option<&[InlineNode<'a>]> {
566 match &self.kind {
567 IndexTermKind::Flow(_) => None,
568 IndexTermKind::Concealed { tertiary, .. } => tertiary.as_deref(),
569 }
570 }
571
572 #[must_use]
574 pub fn is_visible(&self) -> bool {
575 matches!(self.kind, IndexTermKind::Flow(_))
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use crate::{Document, InlineMacro, Options, grammar::walk_document_inline_nodes_mut, parse};
582
583 use super::*;
584
585 #[test]
586 fn signifier_snapshots_are_not_part_of_the_value_contract()
587 -> Result<(), Box<dyn std::error::Error>> {
588 let parsed = parse(
589 include_str!("../../../fixtures/tests/xref_signifier_renumber.adoc"),
590 &Options::default(),
591 )?;
592 let mut document = Document {
593 blocks: parsed.document().blocks.clone(),
594 ..Document::default()
595 };
596 let mut xrefs = Vec::new();
597 walk_document_inline_nodes_mut(&mut document, &mut |inline| {
598 if let InlineNode::Macro(InlineMacro::CrossReference(xref)) = inline {
599 xrefs.push(xref.clone());
600 }
601 });
602 assert_ne!(xrefs, []);
603 for original in xrefs {
604 assert!(original.section_signifiers.is_some());
605 let mut explicit = original.clone();
606 explicit.set_signifier(original.signifier());
607 assert!(explicit.section_signifiers.is_none());
608 assert_eq!(original, explicit);
609 assert_eq!(format!("{original:?}"), format!("{explicit:?}"));
610 assert_eq!(
611 serde_json::to_value(&original)?,
612 serde_json::to_value(&explicit)?
613 );
614 }
615 Ok(())
616 }
617}