1#![doc = include_str!("../README.md")]
2#![feature(downcast_unchecked)]
3#![warn(
4 clippy::pedantic,
5 clippy::allow_attributes_without_reason,
6 missing_docs
7)]
8#![allow(
9 clippy::missing_panics_doc,
10 reason = "lot of unwraps that shouldnt really be hit"
11)]
12#![allow(clippy::missing_errors_doc, reason = "capitalization :(")]
13#![allow(
14 clippy::match_wildcard_for_single_variants,
15 reason = "future may add more tags"
16)]
17#![allow(
18 clippy::match_same_arms,
19 reason = "more confusing to merge in many cases"
20)]
21#![allow(clippy::wildcard_imports, reason = "used in parsing modules")]
22#![allow(
23 clippy::module_inception,
24 reason = "tag names may share their module name, but it doesn't make sense to merge them"
25)]
26
27use {
28 crate::{
29 diagnostics::{DiagnosticKind, TagDiagnosticTranslation},
30 emit::BuiltInEmitters,
31 ext::{Emitters, TagDefinition},
32 tree::{parser, AST},
33 },
34 ::core::fmt::Debug,
35 ::hashbrown::HashMap,
36 ::miette::{Diagnostic, LabeledSpan, Severity, SourceSpan},
37 ::spanner::{BufferSource, Span, Spanned, Spanner},
38 ::std::sync::Arc,
39 ::tracing::{instrument, Level},
40};
41pub use {::miette, ::spanner, ::thiserror};
42
43pub mod diagnostics;
44pub mod emit;
45pub mod ext;
46pub mod tree;
47
48#[derive(Debug)]
50pub enum SourceMetadata {
51 File {
53 filename: String,
55 referenced_from: Option<Span>,
57 },
58 LineTag {
60 from: Span,
62 verbatim: bool,
64 },
65 BlockTag {
67 translation: TagDiagnosticTranslation,
69 },
70 TagArgument {
72 from: Span,
74 verbatim: bool,
76 },
77}
78
79#[derive(Debug)]
81pub struct MarkDollSrc {
82 pub metadata: SourceMetadata,
84 pub source: String,
86}
87
88impl BufferSource for MarkDollSrc {
89 fn source(&self) -> &str {
90 &self.source
91 }
92
93 fn name(&self) -> Option<&str> {
94 Some(match &self.metadata {
95 SourceMetadata::File { filename, .. } => filename,
96 SourceMetadata::LineTag {
97 verbatim: false, ..
98 } => "<transformed line tag>",
99 SourceMetadata::LineTag { verbatim: true, .. } => "<verbatim line tag>",
100 SourceMetadata::BlockTag { .. } => "<block tag>",
101 SourceMetadata::TagArgument {
102 verbatim: false, ..
103 } => "<transformed tag argument>",
104 SourceMetadata::TagArgument { verbatim: true, .. } => "<verbatim tag argument>",
105 })
106 }
107}
108
109impl Default for MarkDollSrc {
110 fn default() -> Self {
111 Self {
112 metadata: SourceMetadata::File {
113 filename: "empty".to_string(),
114 referenced_from: None,
115 },
116 source: String::new(),
117 }
118 }
119}
120
121#[derive(Debug)]
123pub struct MarkDoll<Ctx = ()> {
124 pub tags: HashMap<&'static str, TagDefinition<Ctx>>,
126
127 pub builtin_emitters: Emitters<BuiltInEmitters<Ctx, ()>>,
129
130 pub ok: bool,
134 pub diagnostics: Vec<DiagnosticKind>,
136 pub spanner: Spanner<MarkDollSrc>,
138}
139
140impl<Ctx> MarkDoll<Ctx> {
141 #[must_use]
143 pub fn new() -> Self {
144 Self {
145 tags: HashMap::new(),
146
147 builtin_emitters: Emitters::new(),
148
149 ok: true,
150 diagnostics: Vec::new(),
151 spanner: Spanner::new(),
152 }
153 }
154
155 pub fn add_tag(&mut self, tag: TagDefinition<Ctx>) {
157 self.tags.insert(tag.key, tag);
158 }
159
160 pub fn add_tags(&mut self, tags: impl IntoIterator<Item = TagDefinition<Ctx>>) {
162 for tag in tags {
163 self.add_tag(tag);
164 }
165 }
166
167 #[instrument(skip(self), level = Level::INFO)]
175 pub fn parse_embedded(&mut self, src: Span) -> AST {
176 let mut ctx = parser::ParseCtx::new(self, src);
177 let (ok, ast) = parser::parse(&mut ctx);
178 self.ok &= ok;
179 ast
180 }
181
182 #[instrument(skip(self), level = Level::INFO, ret)]
190 pub fn parse_document(
191 &mut self,
192 filename: String,
193 source: String,
194 referenced_from: Option<Span>,
195 ) -> (bool, Vec<DiagnosticKind>, Option<String>, AST) {
196 let old_ok = ::core::mem::replace(&mut self.ok, true);
198 let old_diagnostics = ::core::mem::take(&mut self.diagnostics);
199
200 let buf = self.spanner.add(|_| MarkDollSrc {
202 metadata: SourceMetadata::File {
203 filename,
204 referenced_from,
205 },
206 source,
207 });
208 let mut ctx = parser::ParseCtx::new(self, buf.span());
209 let frontmatter = parser::frontmatter(&mut ctx);
210 let (ok, ast) = parser::parse(&mut ctx);
211
212 let _ = ::core::mem::replace(&mut self.ok, old_ok);
214 let diagnostics = ::core::mem::replace(&mut self.diagnostics, old_diagnostics);
215
216 (ok, diagnostics, frontmatter, ast)
217 }
218
219 #[instrument(skip(self, ctx), level = Level::INFO)]
225 pub fn emit<To: Debug + 'static>(
226 &mut self,
227 ast: &mut AST,
228 to: &mut To,
229 ctx: &mut Ctx,
230 ) -> (bool, Vec<DiagnosticKind>) {
231 let old_ok = ::core::mem::replace(&mut self.ok, true);
233 let old_diagnostics = ::core::mem::take(&mut self.diagnostics);
234
235 for Spanned(_, node) in ast {
237 node.emit(self, to, ctx, true);
238 }
239
240 let _ = ::core::mem::replace(&mut self.ok, old_ok);
242 let diagnostics = ::core::mem::replace(&mut self.diagnostics, old_diagnostics);
243
244 (self.ok, diagnostics)
245 }
246
247 pub fn finish(&mut self) -> Arc<Spanner<MarkDollSrc>> {
251 Arc::new(::core::mem::take(&mut self.spanner))
252 }
253
254 #[track_caller]
256 #[instrument(skip(self), level = Level::ERROR)]
257 pub fn diag(&mut self, diagnostic: DiagnosticKind) {
258 if let None | Some(Severity::Error) = diagnostic.severity() {
259 self.ok = false;
260 }
261
262 ::tracing::info!(origin = %::core::panic::Location::caller(), "rust origin");
263
264 self.diagnostics.push(diagnostic);
265 }
266
267 #[instrument(skip(self), ret)]
269 pub fn resolve_span(&self, mut span: Span) -> (SourceSpan, Vec<LabeledSpan>) {
270 let mut init = span;
271 let mut labels = Vec::new();
272
273 loop {
274 let file = &self.spanner.lookup_buf(span.start());
275 span = match &file.src.metadata {
276 SourceMetadata::File {
277 referenced_from: None,
278 ..
279 } => break,
280 SourceMetadata::File {
281 referenced_from: Some(ref_from),
282 ..
283 } => {
284 labels.push(LabeledSpan::new_with_span(
285 Some("referenced by".to_string()),
286 self.spanner.lookup_linear_index(ref_from.start())
287 ..self.spanner.lookup_linear_index(ref_from.end()),
288 ));
289 *ref_from
290 }
291 SourceMetadata::TagArgument {
292 from: new,
293 verbatim: true,
294 }
295 | SourceMetadata::LineTag {
296 from: new,
297 verbatim: true,
298 } => {
299 let final_span = (new.start() + span.start().pos).with_len(span.len());
300 if let Some(label) = labels.pop() {
301 labels.push(LabeledSpan::new_with_span(
302 label.label().map(ToString::to_string),
303 self.spanner.lookup_linear_index(final_span.start())
304 ..self.spanner.lookup_linear_index(final_span.end()),
305 ));
306 } else {
307 init = final_span;
308 }
309 final_span
310 }
311 SourceMetadata::TagArgument {
312 from: new,
313 verbatim: false,
314 }
315 | SourceMetadata::LineTag {
316 from: new,
317 verbatim: false,
318 } => {
319 labels.push(LabeledSpan::new_with_span(
320 Some("from here".to_string()),
321 self.spanner.lookup_linear_index(new.start())
322 ..self.spanner.lookup_linear_index(new.end()),
323 ));
324 *new
325 }
326 SourceMetadata::BlockTag {
327 translation: trans, ..
328 } => {
329 let parent = trans.to_parent(&self.spanner, span);
330 if let Some(label) = labels.pop() {
331 labels.push(LabeledSpan::new_with_span(
332 label.label().map(ToString::to_string),
333 self.spanner.lookup_linear_index(parent.start())
334 ..self.spanner.lookup_linear_index(parent.end()),
335 ));
336 } else {
337 init = parent;
338 }
339 parent
340 }
341 }
342 }
343
344 (
345 (self.spanner.lookup_linear_index(init.start())
346 ..self.spanner.lookup_linear_index(init.end()))
347 .into(),
348 labels,
349 )
350 }
351}
352
353impl<Ctx> Default for MarkDoll<Ctx> {
354 fn default() -> Self {
355 Self::new()
356 }
357}