mod blocks;
pub(crate) mod definitions;
mod edit;
pub(crate) mod frontmatter;
mod inlines;
pub(crate) mod line;
pub(crate) mod params;
mod source;
pub(crate) mod yaml;
pub use edit::TextEdit;
pub(crate) use edit::{Change, Spliced, reparse};
use crate::diagnostic::{Code, Diagnostic, Location};
use crate::settings::Settings;
use crate::tree::{Point, Root};
use blocks::Marks;
use edit::Unit;
use markdown::mdast;
use serde_json::Value;
use source::{START, Source};
use std::rc::Rc;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ParseOptions {
pub metadata: Option<Value>,
}
#[derive(Clone, Debug)]
pub(crate) struct Parsed {
pub(crate) root: Rc<Root>,
pub(crate) settings: Settings,
pub(crate) diagnostics: Vec<Diagnostic>,
units: Vec<Unit>,
syntax: Syntax,
}
impl Parsed {
pub(crate) fn for_resolving(&self) -> Self {
Self {
root: Rc::clone(&self.root),
settings: self.settings.clone(),
diagnostics: Vec::new(),
units: Vec::new(),
syntax: self.syntax,
}
}
}
pub(crate) fn parse(text: &str, options: &ParseOptions) -> Parsed {
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let mut diagnostics = Vec::new();
let mut metadata = options.metadata.clone();
let mut parsed = None;
let mut settings = Settings::default();
let mut compmd = true;
if let Some(yaml) = frontmatter::find(text) {
if options.metadata.is_some() {
diagnostics.push(Diagnostic::new(
Code::W115,
Location::new(1, 1),
"the document has its own frontmatter, so the host's metadata is ignored",
));
}
let document = frontmatter::read(yaml, &mut diagnostics);
compmd = document.is_some();
parsed = document.as_ref().map(frontmatter::tree_value);
metadata.clone_from(&parsed);
if let Some(document) = document {
settings = Settings::read(document, yaml);
}
}
let syntax = Syntax {
compmd,
wiki_links: metadata.as_ref().is_some_and(frontmatter::wiki_links),
};
let (root, units) = Converter {
source: Source::new(text, START),
syntax,
labels: &Labels::NONE,
frontmatter: parsed,
diagnostics: &mut diagnostics,
}
.units(true);
Parsed {
root: Rc::new(root),
settings,
diagnostics,
units,
syntax,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Syntax {
compmd: bool,
wiki_links: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Labels {
pub(crate) links: Vec<String>,
pub(crate) footnotes: Vec<String>,
}
impl Labels {
const NONE: Self = Self {
links: Vec::new(),
footnotes: Vec::new(),
};
fn with(&self, root: &mdast::Root) -> Self {
let mut labels = self.clone();
labels.collect(&root.children);
labels
}
fn collect(&mut self, nodes: &[mdast::Node]) {
for node in nodes {
match node {
mdast::Node::Definition(definition) => {
self.links.push(definition.identifier.clone());
}
mdast::Node::FootnoteDefinition(definition) => {
self.footnotes.push(definition.identifier.clone());
}
_ => {}
}
if let Some(children) = node.children() {
self.collect(children);
}
}
}
}
impl Syntax {
fn mdast(self, text: &str, frontmatter: bool, labels: &Labels) -> mdast::Root {
let options = markdown::ParseOptions {
definitions: labels.links.clone(),
gfm_footnote_definitions: labels.footnotes.clone(),
..self.fork_options(frontmatter)
};
match markdown::to_mdast(text, &options) {
Ok(mdast::Node::Root(root)) => root,
_ => mdast::Root {
children: Vec::new(),
position: None,
},
}
}
fn fork_options(self, frontmatter: bool) -> markdown::ParseOptions {
let constructs = if self.compmd {
markdown::Constructs {
compmd_wiki_link: self.wiki_links,
frontmatter,
..markdown::Constructs::compmd()
}
} else {
markdown::Constructs {
frontmatter,
frontmatter_toml: false,
..markdown::Constructs::gfm()
}
};
markdown::ParseOptions {
constructs,
..markdown::ParseOptions::gfm()
}
}
}
struct Converter<'s, 'd> {
source: Source<'s>,
syntax: Syntax,
labels: &'d Labels,
frontmatter: Option<Value>,
diagnostics: &'d mut Vec<Diagnostic>,
}
impl Converter<'_, '_> {
fn document(self, frontmatter: bool) -> Root {
self.convert(frontmatter, None).0
}
fn units(self, frontmatter: bool) -> (Root, Vec<Unit>) {
let mut marks = Marks::default();
let (root, diagnostics) = self.convert(frontmatter, Some(&mut marks));
let units = Unit::from_marks(&marks, &root.children, diagnostics);
(root, units)
}
fn convert(self, frontmatter: bool, marks: Option<&mut Marks>) -> (Root, usize) {
let root = self
.syntax
.mdast(self.source.text(), frontmatter, self.labels);
let labels = self.labels.with(&root);
let mut converter = Converter {
labels: &labels,
..self
};
let position = converter.source.position(root.position.as_ref());
let end = position.map_or(START, |position| position.end);
let children = converter.blocks(root.children, end, &[], marks).0;
(Root { children, position }, converter.diagnostics.len())
}
fn placed<'t, 'c>(
&'c mut self,
text: &'t str,
origin: Point,
labels: &'c Labels,
) -> Converter<'t, 'c> {
Converter {
source: Source::new(text, origin),
syntax: self.syntax,
labels,
frontmatter: None,
diagnostics: self.diagnostics,
}
}
fn diagnose(&mut self, code: Code, at: impl Into<Location>, message: impl Into<String>) {
self.diagnostics
.push(Diagnostic::new(code, at.into(), message));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tree::Node;
#[test]
fn a_byte_order_mark_is_ignored() {
let parsed = parse("\u{feff}---\nmd: {}\n---\n", &ParseOptions::default());
assert!(matches!(parsed.root.children[..], [Node::Yaml(_)]));
}
#[test]
fn host_metadata_opts_in_to_syntax_when_there_is_no_frontmatter() {
let options = ParseOptions {
metadata: Some(serde_json::json!({ "md": { "syntax": ["wiki-links"] } })),
};
let tree = serde_json::to_value(&*parse("[[Home]]", &options).root).unwrap();
assert_eq!(tree["children"][0]["children"][0]["type"], "wikiLink");
}
}