Skip to content

Commit e40e83e

Browse files
authored
add support for front matter
Add front matter parsing and selection. The syntax is: ```text +++variant contents ``` ## Examples ```md --- title: my title --- ``` The following will match: ``` +++ ``` ``` +++ my title ``` ``` +++yaml ``` ``` +++yaml my title ``` ... but this won't: ``` +++toml ```` ## Breaking changes to API: - rm `CodeVariant::{ Toml, Yaml }` (these really should have been front matter variants, not general code variants) - add `MdElem::FrontMatter`, with supporting types for the contents and variants - `ParseOptions::default()` now enables front matter - add a new selector (this isn't actually a breaking change, since the selectors enum is non-exhaustive; but it's worth mentioning) - `BlockHtml` no longer implements `Display`; it was the only one of its siblings that did, so this makes it more consistent with the others ## Issues Resolves #214
1 parent c22c3d1 commit e40e83e

19 files changed

+512
-96
lines changed

README.md

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,22 @@ cat example.md | mdq '# usage | -'
106106

107107
The filter syntax is designed to mirror Markdown syntax. You can select...
108108

109-
| Element | Syntax |
110-
|------------------|----------------------------------|
111-
| Sections | `# title text` |
112-
| Lists | `- unordered list item text` |
113-
| " | `1. ordered list item text` |
114-
| " | `- [ ] uncompleted task` |
115-
| " | `- [x] completed task` |
116-
| " | `- [?] any task` |
117-
| Links | `[display text](url)` |
118-
| Images | `![alt text](url)` |
119-
| Block quotes | `> block quote text` |
120-
| Code blocks | ` ```language <code block text>` |
121-
| Raw HTML | `</> html_tag` |
122-
| Plain paragraphs | `P: paragraph text ` |
123-
| Tables | `:-: header text :-: row text` |
109+
| Element | Syntax |
110+
|------------------|-------------------------------------|
111+
| Sections | `# title text` |
112+
| Lists | `- unordered list item text` |
113+
| " | `1. ordered list item text` |
114+
| " | `- [ ] uncompleted task` |
115+
| " | `- [x] completed task` |
116+
| " | `- [?] any task` |
117+
| Links | `[display text](url)` |
118+
| Images | `![alt text](url)` |
119+
| Block quotes | `> block quote text` |
120+
| Code blocks | ` ```language <code block text>` |
121+
| Raw HTML | `</> html_tag` |
122+
| Plain paragraphs | `P: paragraph text ` |
123+
| Tables | `:-: header text :-: row text` |
124+
| Front matter | `+++[toml\|yaml] front matter text` |
124125

125126
(Tables selection differs from other selections in that you can actually select only certain headers and rows, such that
126127
the resulting element is of a different shape than the original. See the example below, or the wiki for more detail.)

examples/hello.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
title: My example
3+
---
4+
15
# Greetings
26

37
![welcome image](https://example.com/welcome.png)

src/md_elem/tree.rs

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::md_elem::concatenate::Concatenate;
2+
use crate::util::str_utils::TrimmedEmptyLines;
23
use std::backtrace::Backtrace;
34
use std::collections::hash_map::Entry;
45
use std::collections::HashMap;
@@ -69,7 +70,14 @@ impl MdDoc {
6970
///
7071
/// See the various examples in [`elem`] for examples of this parsing in action.
7172
pub fn parse(text: &str, options: &ParseOptions) -> Result<Self, InvalidMd> {
72-
parse0(text, options)
73+
// mdast requires front matter to be literally the first char. We'll be more forgiving.
74+
let trimmed = TrimmedEmptyLines::from(text);
75+
let mut result = parse0(trimmed.remaining, options);
76+
if result.is_err() && !trimmed.trimmed.is_empty() {
77+
// re-parse on the original text, so that we get the correct offsets
78+
result = parse0(text, options);
79+
}
80+
result
7381
}
7482
}
7583

@@ -128,6 +136,7 @@ pub enum MdElem {
128136

129137
// Leaf blocks
130138
CodeBlock(CodeBlock),
139+
FrontMatter(FrontMatter),
131140
Paragraph(Paragraph),
132141
Table(Table),
133142
/// A thematic break:
@@ -146,7 +155,7 @@ pub enum MdElem {
146155
/// Options for parsing Markdown.
147156
///
148157
/// See: [`MdDoc::parse`].
149-
#[derive(Default, Debug)]
158+
#[derive(Debug)]
150159
pub struct ParseOptions {
151160
pub(crate) mdast_options: markdown::ParseOptions,
152161
/// Usually only required for debugging. Defaults to `false`.
@@ -157,6 +166,14 @@ pub struct ParseOptions {
157166
pub allow_unknown_markdown: bool,
158167
}
159168

169+
impl Default for ParseOptions {
170+
fn default() -> Self {
171+
let mut me = Self::gfm();
172+
me.mdast_options.constructs.frontmatter = true;
173+
me
174+
}
175+
}
176+
160177
impl ParseOptions {
161178
pub fn gfm() -> Self {
162179
Self {
@@ -364,8 +381,6 @@ pub mod elem {
364381
Math {
365382
metadata: Option<String>,
366383
},
367-
Toml,
368-
Yaml,
369384
}
370385

371386
/// Some inline text.
@@ -469,9 +484,47 @@ pub mod elem {
469484
pub value: String,
470485
}
471486

472-
impl Display for BlockHtml {
473-
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
474-
write!(f, "{}", self.value)
487+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
488+
pub struct FrontMatter {
489+
pub variant: FrontMatterVariant,
490+
pub body: String,
491+
}
492+
493+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
494+
pub enum FrontMatterVariant {
495+
Toml,
496+
Yaml,
497+
}
498+
499+
impl FrontMatterVariant {
500+
/// Gets the written-out name of this variant.
501+
///
502+
/// ```
503+
/// use mdq::md_elem::elem::FrontMatterVariant;
504+
///
505+
/// assert_eq!(FrontMatterVariant::Toml.name(), "toml");
506+
/// assert_eq!(FrontMatterVariant::Yaml.name(), "yaml");
507+
/// ```
508+
pub fn name(self) -> &'static str {
509+
match self {
510+
FrontMatterVariant::Toml => "toml",
511+
FrontMatterVariant::Yaml => "yaml",
512+
}
513+
}
514+
515+
/// Gets the separator that's used in front matter syntax to specify this variant.
516+
///
517+
/// ```
518+
/// use mdq::md_elem::elem::FrontMatterVariant;
519+
///
520+
/// assert_eq!(FrontMatterVariant::Toml.separator(), "+++");
521+
/// assert_eq!(FrontMatterVariant::Yaml.separator(), "---");
522+
/// ```
523+
pub fn separator(self) -> &'static str {
524+
match self {
525+
FrontMatterVariant::Toml => "+++",
526+
FrontMatterVariant::Yaml => "---",
527+
}
475528
}
476529
}
477530

@@ -1195,6 +1248,7 @@ pub mod elem {
11951248
from_for_md_elem! { List }
11961249
from_for_md_elem! { Section }
11971250
from_for_md_elem! { CodeBlock }
1251+
from_for_md_elem! { FrontMatter }
11981252
from_for_md_elem! { Paragraph }
11991253
from_for_md_elem! { Table }
12001254
from_for_md_elem! { Inline }
@@ -1438,13 +1492,13 @@ impl MdElem {
14381492
mdast::Node::Paragraph(node) => m_node!(MdElem::Paragraph {
14391493
body: Self::inlines(node.children, lookups, ctx)?,
14401494
}),
1441-
mdast::Node::Toml(node) => m_node!(MdElem::CodeBlock {
1442-
variant: CodeVariant::Toml,
1443-
value: node.value,
1495+
mdast::Node::Toml(node) => m_node!(MdElem::FrontMatter {
1496+
variant: FrontMatterVariant::Toml,
1497+
body: node.value,
14441498
}),
1445-
mdast::Node::Yaml(node) => m_node!(MdElem::CodeBlock {
1446-
variant: CodeVariant::Yaml,
1447-
value: node.value,
1499+
mdast::Node::Yaml(node) => m_node!(MdElem::FrontMatter {
1500+
variant: FrontMatterVariant::Yaml,
1501+
body: node.value,
14481502
}),
14491503
mdast::Node::Html(node) => m_node!(MdElem::BlockHtml { value: node.value }),
14501504
mdx_nodes! {} => {
@@ -2724,9 +2778,9 @@ mod tests {
27242778
my: toml
27252779
+++"#},
27262780
);
2727-
check!(&root.children[0], Node::Toml(_), lookups => m_node!(MdElem::CodeBlock{variant, value}) = {
2728-
assert_eq!(variant, CodeVariant::Toml);
2729-
assert_eq!(value, r#"my: toml"#);
2781+
check!(&root.children[0], Node::Toml(_), lookups => m_node!(MdElem::FrontMatter{variant, body}) = {
2782+
assert_eq!(variant, FrontMatterVariant::Toml);
2783+
assert_eq!(body, r#"my: toml"#);
27302784
})
27312785
}
27322786

@@ -2738,12 +2792,12 @@ mod tests {
27382792
&opts,
27392793
indoc! {r#"
27402794
---
2741-
my: toml
2795+
my: yaml
27422796
---"#},
27432797
);
2744-
check!(&root.children[0], Node::Yaml(_), lookups => m_node!(MdElem::CodeBlock{variant, value}) = {
2745-
assert_eq!(variant, CodeVariant::Yaml);
2746-
assert_eq!(value, r#"my: toml"#);
2798+
check!(&root.children[0], Node::Yaml(_), lookups => m_node!(MdElem::FrontMatter{variant, body}) = {
2799+
assert_eq!(variant, FrontMatterVariant::Yaml);
2800+
assert_eq!(body, r#"my: yaml"#);
27472801
})
27482802
}
27492803

src/output/fmt_md.rs

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,16 @@ impl<'md> MdWriterState<'_, 'md> {
188188
}
189189
self.prev_was_thematic_break = true;
190190
}
191-
MdElem::CodeBlock(block) => {
192-
self.write_code_block(out, block);
193-
}
191+
MdElem::CodeBlock(block) => self.write_code_block(out, block),
194192
MdElem::Paragraph(para) => self.write_paragraph(out, para),
195193
MdElem::BlockQuote(block) => self.write_block_quote(out, block),
196194
MdElem::List(list) => self.write_list(out, list),
197195
MdElem::Table(table) => self.write_table(out, table),
198-
MdElem::Inline(inline) => {
199-
self.inlines_writer.write_inline_element(out, inline);
200-
}
196+
MdElem::Inline(inline) => self.inlines_writer.write_inline_element(out, inline),
201197
MdElem::BlockHtml(html) => out.with_block(Block::Plain, |out| {
202198
out.write_str(&html.value);
203199
}),
200+
MdElem::FrontMatter(front_matter) => self.write_front_matter(out, front_matter),
204201
}
205202
}
206203

@@ -389,8 +386,6 @@ impl<'md> MdWriterState<'_, 'md> {
389386
};
390387
(Cow::Borrowed("$$"), meta)
391388
}
392-
CodeVariant::Toml => (Cow::Borrowed("+++"), None),
393-
CodeVariant::Yaml => (Cow::Borrowed("---"), None),
394389
};
395390

396391
out.with_pre_block(|out| {
@@ -405,6 +400,16 @@ impl<'md> MdWriterState<'_, 'md> {
405400
});
406401
}
407402

403+
fn write_front_matter<W: SimpleWrite>(&mut self, out: &mut Output<W>, front_matter: &'md FrontMatter) {
404+
out.with_pre_block(|out| {
405+
out.write_str(front_matter.variant.separator());
406+
out.write_char('\n');
407+
out.write_str(&front_matter.body);
408+
out.write_char('\n');
409+
out.write_str(front_matter.variant.separator());
410+
})
411+
}
412+
408413
fn count_longest_opening_backticks(contents: &str) -> usize {
409414
let mut max_len = 0;
410415
for line in contents.split('\n') {
@@ -574,8 +579,8 @@ pub mod tests {
574579
CodeBlock(CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: Some(_), ..})), ..}),
575580
CodeBlock(CodeBlock{variant: CodeVariant::Math{metadata: None}, ..}),
576581
CodeBlock(CodeBlock{variant: CodeVariant::Math{metadata: Some(_)}, ..}),
577-
CodeBlock(CodeBlock{variant: CodeVariant::Toml, ..}),
578-
CodeBlock(CodeBlock{variant: CodeVariant::Yaml, ..}),
582+
FrontMatter(FrontMatter{variant: FrontMatterVariant::Toml, ..}),
583+
FrontMatter(FrontMatter{variant: FrontMatterVariant::Yaml, ..}),
579584
Paragraph(_),
580585
BlockQuote(_),
581586
List(_),
@@ -1280,36 +1285,6 @@ pub mod tests {
12801285
);
12811286
}
12821287

1283-
#[test]
1284-
fn toml() {
1285-
check_render(
1286-
md_elems![CodeBlock {
1287-
variant: CodeVariant::Toml,
1288-
value: "one\ntwo".to_string(),
1289-
}],
1290-
indoc! {r#"
1291-
+++
1292-
one
1293-
two
1294-
+++"#},
1295-
);
1296-
}
1297-
1298-
#[test]
1299-
fn yaml() {
1300-
check_render(
1301-
md_elems![CodeBlock {
1302-
variant: CodeVariant::Yaml,
1303-
value: "one\ntwo".to_string(),
1304-
}],
1305-
indoc! {r#"
1306-
---
1307-
one
1308-
two
1309-
---"#},
1310-
);
1311-
}
1312-
13131288
#[test]
13141289
fn nested_block() {
13151290
check_render(
@@ -1386,6 +1361,40 @@ pub mod tests {
13861361
}
13871362
}
13881363

1364+
mod front_matter {
1365+
use super::*;
1366+
1367+
#[test]
1368+
fn yaml() {
1369+
check_render(
1370+
md_elems![FrontMatter {
1371+
variant: FrontMatterVariant::Yaml,
1372+
body: "one\ntwo".to_string(),
1373+
}],
1374+
indoc! {r#"
1375+
---
1376+
one
1377+
two
1378+
---"#},
1379+
);
1380+
}
1381+
1382+
#[test]
1383+
fn toml() {
1384+
check_render(
1385+
md_elems![FrontMatter {
1386+
variant: FrontMatterVariant::Toml,
1387+
body: "one\ntwo".to_string(),
1388+
}],
1389+
indoc! {r#"
1390+
+++
1391+
one
1392+
two
1393+
+++"#},
1394+
);
1395+
}
1396+
}
1397+
13891398
mod inline {
13901399
use super::*;
13911400

src/output/fmt_md_inlines.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ impl<'md> MdInlinesWriter<'md> {
222222
MdElem::Inline(inline) => {
223223
self.find_references_in_footnote_inlines([inline]);
224224
}
225-
MdElem::CodeBlock(_) | MdElem::BlockHtml(_) | MdElem::ThematicBreak => {
225+
MdElem::CodeBlock(_) | MdElem::FrontMatter(_) | MdElem::BlockHtml(_) | MdElem::ThematicBreak => {
226226
// nothing
227227
}
228228
}

0 commit comments

Comments
 (0)