markdoll/
lib.rs

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/// the metadata of this [`MarkDollSrc`], describing where it came from
49#[derive(Debug)]
50pub enum SourceMetadata {
51	/// this source originates from a file
52	File {
53		/// filename of the file
54		filename: String,
55		/// if applicable, the span that referenced this
56		referenced_from: Option<Span>,
57	},
58	/// the content of a line-tag
59	LineTag {
60		/// what this content is derived from
61		from: Span,
62		/// whether this content is "verbatim" (exactly matches what's in its containing source)
63		verbatim: bool,
64	},
65	/// the content of a block-tag
66	BlockTag {
67		/// translation
68		translation: TagDiagnosticTranslation,
69	},
70	/// argument of a tag
71	TagArgument {
72		/// what this argument is derived from
73		from: Span,
74		/// whether this argument is "verbatim" (exactly matches what's in its containing source)
75		verbatim: bool,
76	},
77}
78
79/// markdoll source
80#[derive(Debug)]
81pub struct MarkDollSrc {
82	/// metadata containing information about the source's origin
83	pub metadata: SourceMetadata,
84	/// contents of this source
85	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/// markdoll's main context
122#[derive(Debug)]
123pub struct MarkDoll<Ctx = ()> {
124	/// the tags registered
125	pub tags: HashMap<&'static str, TagDefinition<Ctx>>,
126
127	/// emitters for built-in items
128	pub builtin_emitters: Emitters<BuiltInEmitters<Ctx, ()>>,
129
130	/// whether the current operation is "ok"
131	///
132	/// this shouldn't really be set to `true` by anything except the language
133	pub ok: bool,
134	/// diagnostics from the current document
135	pub diagnostics: Vec<DiagnosticKind>,
136	/// source-mapping
137	pub spanner: Spanner<MarkDollSrc>,
138}
139
140impl<Ctx> MarkDoll<Ctx> {
141	/// construct an empty instance with no tags and the default [`BuiltInEmitters`]
142	#[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	/// add a tag
156	pub fn add_tag(&mut self, tag: TagDefinition<Ctx>) {
157		self.tags.insert(tag.key, tag);
158	}
159
160	/// add multiple tags
161	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	/// parse the input into an AST, used to parse the content of tags in an existing parse operation
168	///
169	/// returns the produced [`AST`]
170	///
171	/// # errors
172	///
173	/// if the operation does not succeed, the [`AST`] may be in an incomplete/incorrect state
174	#[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	/// parse a complete document into an AST, including frontmatter
183	///
184	/// returns
185	/// - whether the operation was successful
186	/// - the diagnostics produced during the operation (may not be ampty on success)
187	/// - the frontmatter
188	/// - the [`AST`]
189	#[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		// stash state
197		let old_ok = ::core::mem::replace(&mut self.ok, true);
198		let old_diagnostics = ::core::mem::take(&mut self.diagnostics);
199
200		// parse
201		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		// restore stash
213		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	/// emit the given [`AST`] to an output
220	///
221	/// returns
222	/// - whether the operation was successful
223	/// - the diagnostics produced during the operation (may not be ampty on success)
224	#[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		// stash state
232		let old_ok = ::core::mem::replace(&mut self.ok, true);
233		let old_diagnostics = ::core::mem::take(&mut self.diagnostics);
234
235		// emit
236		for Spanned(_, node) in ast {
237			node.emit(self, to, ctx, true);
238		}
239
240		// restore stash
241		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	/// finish a set of files and prepare to render diagnostics
248	///
249	/// returns the shared spanner to use with [`Report::with_source_code`](::miette::Report::with_source_code)
250	pub fn finish(&mut self) -> Arc<Spanner<MarkDollSrc>> {
251		Arc::new(::core::mem::take(&mut self.spanner))
252	}
253
254	/// emit a diagnostic, mapping the position accordingly
255	#[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	/// returns (outer, inner) span
268	#[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}