Skip to main content

aozora_syntax/
node_kind.rs

1//! Cross-cutting "kind" tag for AST nodes.
2//!
3//! [`NodeKind`] enumerates every wire-distinct tag the borrowed-AST
4//! surfaces produce. It is used both for **internal** projection
5//! ([`AozoraNode::kind`](crate::borrowed::AozoraNode::kind),
6//! [`NodeRef::kind`](crate::borrowed::NodeRef::kind)) and for the
7//! **driver wire format** ([`crate`]'s host crate `aozora` projects
8//! the tag to a stable camelCase string via [`NodeKind::as_camel_case`]).
9//!
10//! The typed enum (rather than a `&'static str` constant) lets every
11//! consumer pattern-match the tag exhaustively — the compiler points
12//! out a new variant landing without a wire mapping — and concentrates
13//! the camelCase string in a single authority.
14
15/// Cross-cutting tag for an AST node or `NodeRef` projection.
16///
17/// The first 17 variants ([`Self::Ruby`] through [`Self::Container`])
18/// project from [`crate::borrowed::AozoraNode`]'s discriminant. The
19/// final two ([`Self::ContainerOpen`] / [`Self::ContainerClose`])
20/// only arise from [`crate::borrowed::NodeRef`]'s container open /
21/// close variants — the inline `Container` payload uses
22/// [`Self::Container`].
23///
24/// `#[non_exhaustive]` so adding a new `AozoraNode` variant only needs
25/// to land here and on the per-call `match` sites; existing wire
26/// consumers see the new variant as an unrecognised tag and gracefully
27/// degrade (the camelCase mapping is exhaustive within this crate;
28/// downstream `match` over `NodeKind` is required to handle a `_` arm
29/// for forward-compat).
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31#[non_exhaustive]
32pub enum NodeKind {
33    /// Ruby annotation (`|base《reading》`).
34    Ruby,
35    /// Bouten (傍点) — emphasis dots over a span.
36    Bouten,
37    /// 縦中横 (tate-chu-yoko) — horizontal text inside vertical run.
38    TateChuYoko,
39    /// 外字 (gaiji) — non-Unicode character reference.
40    Gaiji,
41    /// Inline indent (字下げ) marker.
42    Indent,
43    /// Right-edge alignment (字上げ) marker.
44    AlignEnd,
45    /// 割注 (warichu) — split-line annotation.
46    Warichu,
47    /// 罫囲み (keigakomi) — ruled box.
48    Keigakomi,
49    /// 改ページ (page break).
50    PageBreak,
51    /// Section break (大見出し系統合).
52    SectionBreak,
53    /// Aozora heading (見出し).
54    AozoraHeading,
55    /// Heading hint that informs downstream rendering decisions.
56    HeadingHint,
57    /// 挿絵 (sashie) — illustration reference.
58    Sashie,
59    /// 返り点 (kaeriten) — kanbun reading marker.
60    Kaeriten,
61    /// Generic annotation that no specific recogniser claimed.
62    Annotation,
63    /// Double ruby (《《…》》).
64    DoubleRuby,
65    /// Inline-attached container (字下げ系の `AozoraNode` 包み込み).
66    Container,
67    /// `NodeRef::BlockOpen` projection — paired-container open
68    /// sentinel position.
69    ContainerOpen,
70    /// `NodeRef::BlockClose` projection — paired-container close
71    /// sentinel position.
72    ContainerClose,
73}
74
75impl NodeKind {
76    /// Every variant in declaration order.
77    ///
78    /// Used by `aozora kinds` (CLI introspection) and the
79    /// TypeScript / JSON-Schema codegen so the artefact list
80    /// tracks the enum without a hand-maintained parallel.
81    pub const ALL: [Self; 19] = [
82        Self::Ruby,
83        Self::Bouten,
84        Self::TateChuYoko,
85        Self::Gaiji,
86        Self::Indent,
87        Self::AlignEnd,
88        Self::Warichu,
89        Self::Keigakomi,
90        Self::PageBreak,
91        Self::SectionBreak,
92        Self::AozoraHeading,
93        Self::HeadingHint,
94        Self::Sashie,
95        Self::Kaeriten,
96        Self::Annotation,
97        Self::DoubleRuby,
98        Self::Container,
99        Self::ContainerOpen,
100        Self::ContainerClose,
101    ];
102
103    /// Stable camelCase string identifier for this kind.
104    ///
105    /// Driver crates (`aozora-ffi` / `aozora-wasm` / `aozora-py`) all
106    /// emit JSON whose `kind` field equals this string verbatim, so
107    /// downstream TypeScript / Python / C consumers can switch on the
108    /// tag without consulting an out-of-band table.
109    #[must_use]
110    pub const fn as_camel_case(self) -> &'static str {
111        match self {
112            Self::Ruby => "ruby",
113            Self::Bouten => "bouten",
114            Self::TateChuYoko => "tateChuYoko",
115            Self::Gaiji => "gaiji",
116            Self::Indent => "indent",
117            Self::AlignEnd => "alignEnd",
118            Self::Warichu => "warichu",
119            Self::Keigakomi => "keigakomi",
120            Self::PageBreak => "pageBreak",
121            Self::SectionBreak => "sectionBreak",
122            Self::AozoraHeading => "heading",
123            Self::HeadingHint => "headingHint",
124            Self::Sashie => "sashie",
125            Self::Kaeriten => "kaeriten",
126            Self::Annotation => "annotation",
127            Self::DoubleRuby => "doubleRuby",
128            Self::Container => "container",
129            Self::ContainerOpen => "containerOpen",
130            Self::ContainerClose => "containerClose",
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    /// camelCase strings are pinned — accidental rename of one breaks
140    /// this test instead of silently breaking downstream tooling that
141    /// switches on the tag.
142    #[test]
143    fn camel_case_strings_are_stable() {
144        assert_eq!(NodeKind::Ruby.as_camel_case(), "ruby");
145        assert_eq!(NodeKind::Bouten.as_camel_case(), "bouten");
146        assert_eq!(NodeKind::TateChuYoko.as_camel_case(), "tateChuYoko");
147        assert_eq!(NodeKind::Gaiji.as_camel_case(), "gaiji");
148        assert_eq!(NodeKind::Indent.as_camel_case(), "indent");
149        assert_eq!(NodeKind::AlignEnd.as_camel_case(), "alignEnd");
150        assert_eq!(NodeKind::Warichu.as_camel_case(), "warichu");
151        assert_eq!(NodeKind::Keigakomi.as_camel_case(), "keigakomi");
152        assert_eq!(NodeKind::PageBreak.as_camel_case(), "pageBreak");
153        assert_eq!(NodeKind::SectionBreak.as_camel_case(), "sectionBreak");
154        assert_eq!(NodeKind::AozoraHeading.as_camel_case(), "heading");
155        assert_eq!(NodeKind::HeadingHint.as_camel_case(), "headingHint");
156        assert_eq!(NodeKind::Sashie.as_camel_case(), "sashie");
157        assert_eq!(NodeKind::Kaeriten.as_camel_case(), "kaeriten");
158        assert_eq!(NodeKind::Annotation.as_camel_case(), "annotation");
159        assert_eq!(NodeKind::DoubleRuby.as_camel_case(), "doubleRuby");
160        assert_eq!(NodeKind::Container.as_camel_case(), "container");
161        assert_eq!(NodeKind::ContainerOpen.as_camel_case(), "containerOpen");
162        assert_eq!(NodeKind::ContainerClose.as_camel_case(), "containerClose");
163    }
164}