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}