1use crate::{
6 stream::{EventKind, Tag, Text},
7 CodeFence, ReferenceKind,
8};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum NodeSyntax<'a> {
16 Reference {
18 kind: ReferenceKind,
19 label: Text<'a>,
20 },
21 Footnote { label: Text<'a> },
23 Code { info: Text<'a> },
25 Frontmatter {
27 info: Text<'a>,
28 fence: CodeFence,
29 closing_fence_length: usize,
30 },
31 ListItem { spread: bool },
33}
34
35impl NodeSyntax<'_> {
36 pub(crate) fn borrowed(&self) -> NodeSyntax<'_> {
37 match self {
38 Self::Reference { kind, label } => NodeSyntax::Reference {
39 kind: *kind,
40 label: label.borrowed(),
41 },
42 Self::Footnote { label } => NodeSyntax::Footnote {
43 label: label.borrowed(),
44 },
45 Self::Code { info } => NodeSyntax::Code {
46 info: info.borrowed(),
47 },
48 Self::Frontmatter {
49 info,
50 fence,
51 closing_fence_length,
52 } => NodeSyntax::Frontmatter {
53 info: info.borrowed(),
54 fence: *fence,
55 closing_fence_length: *closing_fence_length,
56 },
57 Self::ListItem { spread } => NodeSyntax::ListItem { spread: *spread },
58 }
59 }
60
61 pub(crate) fn copied(self, source: &str) -> Option<NodeSyntax<'static>> {
63 let own = |text: Text<'_>| text.materialized(source);
64 Some(match self {
65 Self::Reference { .. } => return None,
66 Self::Footnote { label } => NodeSyntax::Footnote { label: own(label) },
67 Self::Code { info } => NodeSyntax::Code { info: own(info) },
68 Self::Frontmatter {
69 info,
70 fence,
71 closing_fence_length,
72 } => NodeSyntax::Frontmatter {
73 info: own(info),
74 fence,
75 closing_fence_length,
76 },
77 Self::ListItem { spread } => NodeSyntax::ListItem { spread },
78 })
79 }
80
81 pub(crate) fn valid_for(&self, kind: &EventKind<'_>, source: &str) -> bool {
82 match (self, kind) {
83 (
84 Self::Reference { label, .. },
85 EventKind::Start(Tag::Link { .. } | Tag::Image { .. }),
86 )
87 | (
88 Self::Footnote { label },
89 EventKind::Start(Tag::FootnoteDefinition(_)) | EventKind::FootnoteReference(_),
90 ) => label.resolve(source).is_ok(),
91 (Self::Code { info }, EventKind::CodeBlock { .. }) => info.resolve(source).is_ok(),
92 (
93 Self::Frontmatter {
94 info,
95 fence,
96 closing_fence_length,
97 },
98 EventKind::Opaque { block: true, .. },
99 ) => {
100 info.resolve(source).is_ok()
101 && matches!(fence.character, '`' | '~' | '-' | '+')
102 && fence.length >= 3
103 && *closing_fence_length >= fence.length
104 }
105 (Self::ListItem { .. }, EventKind::Start(Tag::Item { .. })) => true,
106 _ => false,
107 }
108 }
109}
110
111pub(crate) fn infer(
112 source: &str,
113 span: core::ops::Range<usize>,
114 kind: &EventKind<'_>,
115) -> Option<NodeSyntax<'static>> {
116 let raw = source
117 .get(span.clone())
118 .expect("document source extents are validated before syntax inference");
119 match kind {
120 EventKind::Start(Tag::Link { .. } | Tag::Image { .. }) => {
121 let prefix = if raw.starts_with("![") {
122 2
123 } else if raw.starts_with('[') {
124 1
125 } else {
126 return None;
127 };
128 if !raw.ends_with(']') {
129 return None;
130 }
131 let bytes = raw.as_bytes();
133 let mut depth = 0;
134 let mut start = None;
135 for index in memchr::memrchr2_iter(b'[', b']', bytes) {
137 let mut back = index;
138 while back > 0 && bytes[back - 1] == b'\\' {
139 back -= 1;
140 }
141 if (index - back) % 2 == 1 {
142 continue;
143 }
144 if bytes[index] == b']' {
145 depth += 1;
146 } else {
147 depth -= 1;
148 if depth == 0 {
149 start = Some(index);
150 break;
151 }
152 }
153 }
154 let start = start?;
155 let (kind, label) = if start > prefix && bytes[start - 1] == b']' {
156 if start + 1 == bytes.len() - 1 {
157 (
158 ReferenceKind::Collapsed,
159 span.start + prefix..span.start + start - 1,
160 )
161 } else {
162 (ReferenceKind::Full, span.start + start + 1..span.end - 1)
163 }
164 } else {
165 (ReferenceKind::Shortcut, span.start + prefix..span.end - 1)
166 };
167 Some(NodeSyntax::Reference {
168 kind,
169 label: Text::Source(label),
170 })
171 }
172 EventKind::Start(Tag::FootnoteDefinition(_)) | EventKind::FootnoteReference(_) => {
173 let start = raw.find("[^")? + 2;
174 let mut end = start;
175 while end < raw.len() {
176 match raw.as_bytes()[end] {
177 b'\\' => end += 2,
178 b']' => break,
179 _ => end += 1,
180 }
181 }
182 if end >= raw.len() {
183 return None;
184 }
185 Some(NodeSyntax::Footnote {
186 label: Text::Source(span.start + start..span.start + end),
187 })
188 }
189 EventKind::CodeBlock {
190 fence: Some(fence), ..
191 } => {
192 let first = &raw[..raw.find(['\r', '\n']).unwrap_or(raw.len())];
193 let sequence = &first[first.find(fence.character)?..];
194 let info = sequence.get(fence.length..)?;
195 if !sequence[..fence.length]
196 .chars()
197 .all(|character| character == fence.character)
198 {
199 return None;
200 }
201 let info = info.trim_matches([' ', '\t']);
202 let start = span.start + (info.as_ptr() as usize - first.as_ptr() as usize);
203 let text = crate::value_builder::decode_string(source, start..start + info.len());
204 Some(NodeSyntax::Code { info: text })
205 }
206 EventKind::Opaque { block: true, .. } => frontmatter(raw).map(
207 |(info, fence, closing_fence_length, _)| NodeSyntax::Frontmatter {
208 info: Text::Source(span.start + info.start..span.start + info.end),
209 fence,
210 closing_fence_length,
211 },
212 ),
213 _ => None,
214 }
215}
216
217pub(crate) fn frontmatter(
219 raw: &str,
220) -> Option<(
221 core::ops::Range<usize>,
222 CodeFence,
223 usize,
224 core::ops::Range<usize>,
225)> {
226 let bytes = raw.as_bytes();
227 let fence = CodeFence::frontmatter_open(bytes)?;
228 let first_end = bytes.iter().position(|b| matches!(b, b'\r' | b'\n'))?;
229 let mut body_start = first_end + 1;
230 if bytes[first_end] == b'\r' && bytes.get(body_start) == Some(&b'\n') {
231 body_start += 1;
232 }
233 let raw_end = raw.trim_end_matches(['\r', '\n']).len();
234 let closing_start = raw[..raw_end].rfind(['\r', '\n']).map_or(0, |i| i + 1);
235 if closing_start < body_start {
236 return None;
237 }
238 let close = raw[closing_start..raw_end].trim_matches([' ', '\t']);
239 let closing_fence_length = fence.frontmatter_close(close.as_bytes())?;
240 let info = raw[fence.length..first_end].trim_matches([' ', '\t']);
241 if !fence.valid_frontmatter_info(info.as_bytes()) {
242 return None;
243 }
244 let start = info.as_ptr() as usize - raw.as_ptr() as usize;
245 Some((
246 start..start + info.len(),
247 fence,
248 closing_fence_length,
249 body_start..closing_start,
250 ))
251}