use std::{fmt, num::NonZeroUsize};
use serde::Serialize;
use crate::{ElementAttributes, InlineNode, Location, Source, StemNotation, Substitution};
pub const ICON_SIZES: &[&str] = &["1x", "2x", "3x", "4x", "5x", "lg", "fw"];
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Pass<'a> {
#[serde(skip)]
pub(crate) attribute_fragments: Box<[crate::model::AttributeInlineFragment]>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<&'a str>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub substitutions: Vec<Substitution>,
pub location: Location,
#[serde(skip)]
pub kind: PassthroughKind,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Default)]
pub enum PassthroughKind {
#[default]
Single,
Double,
Triple,
Macro,
AttributeRef,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Footnote<'a> {
#[serde(skip)]
pub(crate) definition_source: Option<&'a str>,
#[serde(skip)]
pub(crate) registration_substitutions: Option<crate::model::substitution::SubstitutionPlan>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<&'a str>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content: Vec<InlineNode<'a>>,
#[serde(skip)]
pub number: u32,
pub location: Location,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Icon<'a> {
pub target: Source<'a>,
pub attributes: ElementAttributes<'a>,
pub location: Location,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Link<'a> {
#[serde(skip_serializing)]
pub text: Vec<InlineNode<'a>>,
pub target: Source<'a>,
pub attributes: ElementAttributes<'a>,
pub location: Location,
#[serde(skip)]
pub(crate) hide_uri_scheme: bool,
}
impl<'a> Link<'a> {
#[must_use]
pub fn new(target: Source<'a>, location: Location) -> Self {
Self {
text: Vec::new(),
target,
attributes: ElementAttributes::default(),
location,
hide_uri_scheme: false,
}
}
#[must_use]
pub fn with_text(mut self, text: Vec<InlineNode<'a>>) -> Self {
self.text = text;
self
}
#[must_use]
pub fn with_attributes(mut self, attributes: ElementAttributes<'a>) -> Self {
self.attributes = attributes;
self
}
#[must_use]
pub fn hides_uri_scheme(&self) -> bool {
self.hide_uri_scheme
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Url<'a> {
#[serde(skip_serializing)]
pub text: Vec<InlineNode<'a>>,
pub target: Source<'a>,
pub attributes: ElementAttributes<'a>,
pub location: Location,
#[serde(skip)]
pub(crate) hide_uri_scheme: bool,
}
impl Url<'_> {
#[must_use]
pub fn hides_uri_scheme(&self) -> bool {
self.hide_uri_scheme
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Mailto<'a> {
#[serde(skip_serializing)]
pub text: Vec<InlineNode<'a>>,
pub target: Source<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<&'a str>,
pub attributes: ElementAttributes<'a>,
pub location: Location,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Button<'a> {
pub label: &'a str,
pub location: Location,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Menu<'a> {
pub target: &'a str,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub items: Vec<&'a str>,
pub location: Location,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Keyboard<'a> {
pub keys: Vec<Key<'a>>,
pub location: Location,
}
impl<'a> Keyboard<'a> {
#[must_use]
pub fn new(keys: Vec<Key<'a>>, location: Location) -> Self {
Self { keys, location }
}
}
pub type Key<'a> = &'a str;
#[derive(Clone, Serialize)]
#[non_exhaustive]
pub struct CrossReference<'a> {
pub target: &'a str,
#[serde(skip)]
pub target_is_local: bool,
#[serde(skip_serializing)]
pub text: Vec<InlineNode<'a>>,
pub location: Location,
#[serde(skip)]
pub xrefstyle: XrefStyle,
#[serde(skip)]
pub caption_label: XrefCaptionLabel<'a>,
#[serde(skip)]
signifier: XrefSignifier<'a>,
#[serde(skip)]
pub(crate) section_signifiers: Option<&'a [XrefSignifier<'a>; REFSIG_ATTRIBUTES.len()]>,
#[serde(skip)]
pub role: Option<&'a str>,
#[serde(skip)]
pub(crate) caption_label_snapshot_id: Option<NonZeroUsize>,
#[serde(skip)]
pub(crate) resolve_natural_target: bool,
#[serde(skip)]
pub(crate) source_syntax: XrefSourceSyntax,
}
impl<'a> CrossReference<'a> {
#[must_use]
pub fn new(target: &'a str, location: Location) -> Self {
Self {
target,
target_is_local: Self::is_local_target(target),
text: Vec::new(),
location,
xrefstyle: XrefStyle::Default,
caption_label: XrefCaptionLabel::AtTarget,
signifier: XrefSignifier::Standard,
section_signifiers: None,
role: None,
caption_label_snapshot_id: None,
resolve_natural_target: false,
source_syntax: XrefSourceSyntax::Literal,
}
}
pub(crate) fn is_local_target(target: &str) -> bool {
match target.split_once('#') {
Some((path, _)) => path.is_empty(),
None => !target.contains(['.', ':']),
}
}
#[must_use]
pub fn with_text(mut self, text: Vec<InlineNode<'a>>) -> Self {
self.text = text;
self
}
#[must_use]
pub const fn signifier(&self) -> XrefSignifier<'a> {
self.signifier
}
pub fn set_signifier(&mut self, signifier: XrefSignifier<'a>) {
self.signifier = signifier;
self.section_signifiers = None;
}
pub(crate) fn refresh_signifier(&mut self, name: Option<&str>) {
let Some(signifiers) = self.section_signifiers else {
return;
};
self.signifier = name
.and_then(|name| {
REFSIG_ATTRIBUTES
.iter()
.position(|(candidate, _)| *candidate == name)
})
.and_then(|index| signifiers.get(index))
.copied()
.unwrap_or_default();
}
}
impl fmt::Debug for CrossReference<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CrossReference")
.field("target", &self.target)
.field("target_is_local", &self.target_is_local)
.field("text", &self.text)
.field("location", &self.location)
.field("xrefstyle", &self.xrefstyle)
.field("caption_label", &self.caption_label)
.field("signifier", &self.signifier)
.field("role", &self.role)
.finish()
}
}
impl PartialEq for CrossReference<'_> {
fn eq(&self, other: &Self) -> bool {
self.target == other.target
&& self.target_is_local == other.target_is_local
&& self.text == other.text
&& self.location == other.location
&& self.xrefstyle == other.xrefstyle
&& self.caption_label == other.caption_label
&& self.signifier == other.signifier
&& self.role == other.role
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum XrefSourceSyntax {
Literal,
Shorthand,
Macro,
}
impl XrefSourceSyntax {
pub(crate) fn target_is_local(self, target: &str) -> bool {
if matches!(self, Self::Literal) {
return true;
}
match target.split_once('#') {
Some((path, _)) => path.is_empty(),
None => {
matches!(self, Self::Shorthand)
|| !target.rsplit('/').next().unwrap_or(target).contains('.')
}
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum XrefSignifier<'a> {
#[default]
Standard,
AtReference(&'a str),
Omitted,
}
pub(crate) const REFSIG_ATTRIBUTES: [(&str, &str); 11] = [
("part", "part-refsig"),
("chapter", "chapter-refsig"),
("section", "section-refsig"),
("appendix", "appendix-refsig"),
("preface", "preface-refsig"),
("abstract", "abstract-refsig"),
("dedication", "dedication-refsig"),
("colophon", "colophon-refsig"),
("glossary", "glossary-refsig"),
("bibliography", "bibliography-refsig"),
("index", "index-refsig"),
];
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum XrefCaptionLabel<'a> {
#[default]
AtTarget,
AtReference(&'a str),
NumberOnly,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum XrefStyle {
#[default]
Default,
Basic,
Short,
Full,
}
impl XrefStyle {
pub(crate) fn from_attribute(value: Option<&str>) -> Self {
match value {
None => Self::Default,
Some("short") => Self::Short,
Some("full") => Self::Full,
_ => Self::Basic,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Autolink<'a> {
pub url: Source<'a>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub bracketed: bool,
pub location: Location,
#[serde(skip)]
pub(crate) hide_uri_scheme: bool,
}
impl Autolink<'_> {
#[must_use]
pub fn hides_uri_scheme(&self) -> bool {
self.hide_uri_scheme
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Stem<'a> {
pub content: &'a str,
pub notation: StemNotation,
pub location: Location,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub enum IndexTermKind<'a> {
Flow(Vec<InlineNode<'a>>),
Concealed {
term: Vec<InlineNode<'a>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
secondary: Option<Vec<InlineNode<'a>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tertiary: Option<Vec<InlineNode<'a>>>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum IndexTermRelationship<'a> {
See {
target: Vec<InlineNode<'a>>,
},
SeeAlso {
targets: Vec<Vec<InlineNode<'a>>>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[non_exhaustive]
pub struct IndexTerm<'a> {
pub kind: IndexTermKind<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relationship: Option<IndexTermRelationship<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) catalog: Option<Box<IndexTerm<'a>>>,
#[serde(skip)]
pub(crate) catalog_substitutions: Option<crate::model::substitution::SubstitutionPlan>,
pub location: Location,
}
impl<'a> IndexTerm<'a> {
pub(crate) fn for_each_label_mut(&mut self, mut visit: impl FnMut(&mut Vec<InlineNode<'a>>)) {
match &mut self.kind {
IndexTermKind::Flow(term) => visit(term),
IndexTermKind::Concealed {
term,
secondary,
tertiary,
} => {
visit(term);
if let Some(secondary) = secondary {
visit(secondary);
}
if let Some(tertiary) = tertiary {
visit(tertiary);
}
}
}
match &mut self.relationship {
Some(IndexTermRelationship::See { target }) => visit(target),
Some(IndexTermRelationship::SeeAlso { targets }) => {
for target in targets {
visit(target);
}
}
None => {}
}
}
#[must_use]
pub fn catalog_entry(&self) -> &Self {
self.catalog.as_deref().unwrap_or(self)
}
#[must_use]
pub fn term(&self) -> &[InlineNode<'a>] {
match &self.kind {
IndexTermKind::Flow(term) | IndexTermKind::Concealed { term, .. } => term,
}
}
#[must_use]
pub fn secondary(&self) -> Option<&[InlineNode<'a>]> {
match &self.kind {
IndexTermKind::Flow(_) => None,
IndexTermKind::Concealed { secondary, .. } => secondary.as_deref(),
}
}
#[must_use]
pub fn tertiary(&self) -> Option<&[InlineNode<'a>]> {
match &self.kind {
IndexTermKind::Flow(_) => None,
IndexTermKind::Concealed { tertiary, .. } => tertiary.as_deref(),
}
}
#[must_use]
pub fn is_visible(&self) -> bool {
matches!(self.kind, IndexTermKind::Flow(_))
}
}
#[cfg(test)]
mod tests {
use crate::{Document, InlineMacro, Options, grammar::walk_document_inline_nodes_mut, parse};
use super::*;
#[test]
fn signifier_snapshots_are_not_part_of_the_value_contract()
-> Result<(), Box<dyn std::error::Error>> {
let parsed = parse(
include_str!("../../../fixtures/tests/xref_signifier_renumber.adoc"),
&Options::default(),
)?;
let mut document = Document {
blocks: parsed.document().blocks.clone(),
..Document::default()
};
let mut xrefs = Vec::new();
walk_document_inline_nodes_mut(&mut document, &mut |inline| {
if let InlineNode::Macro(InlineMacro::CrossReference(xref)) = inline {
xrefs.push(xref.clone());
}
});
assert_ne!(xrefs, []);
for original in xrefs {
assert!(original.section_signifiers.is_some());
let mut explicit = original.clone();
explicit.set_signifier(original.signifier());
assert!(explicit.section_signifiers.is_none());
assert_eq!(original, explicit);
assert_eq!(format!("{original:?}"), format!("{explicit:?}"));
assert_eq!(
serde_json::to_value(&original)?,
serde_json::to_value(&explicit)?
);
}
Ok(())
}
}