aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Titus Wormer <tituswormer@gmail.com>2022-08-16 16:49:29 +0200
committerLibravatar Titus Wormer <tituswormer@gmail.com>2022-08-16 16:49:53 +0200
commit6ee90b34c87354baf8e03d5469a92cf5dd17a82b (patch)
treecfa64be772be6464e6f790dabccf8a77e7afe60e
parent93d0b7c6465f4ffe220b3ddada729746b11eb6ce (diff)
downloadmarkdown-rs-6ee90b34c87354baf8e03d5469a92cf5dd17a82b.tar.gz
markdown-rs-6ee90b34c87354baf8e03d5469a92cf5dd17a82b.tar.bz2
markdown-rs-6ee90b34c87354baf8e03d5469a92cf5dd17a82b.zip
Add support for frontmatter
-rw-r--r--examples/lib.rs17
-rw-r--r--readme.md7
-rw-r--r--src/compiler.rs13
-rw-r--r--src/constant.rs7
-rw-r--r--src/construct/code_indented.rs2
-rw-r--r--src/construct/document.rs20
-rw-r--r--src/construct/flow.rs24
-rw-r--r--src/construct/frontmatter.rs293
-rw-r--r--src/construct/mod.rs2
-rw-r--r--src/construct/thematic_break.rs2
-rw-r--r--src/event.rs92
-rw-r--r--src/lib.rs12
-rw-r--r--src/state.rs24
-rw-r--r--src/tokenizer.rs5
-rw-r--r--tests/frontmatter.rs67
15 files changed, 563 insertions, 24 deletions
diff --git a/examples/lib.rs b/examples/lib.rs
index b1869bb..94c2c58 100644
--- a/examples/lib.rs
+++ b/examples/lib.rs
@@ -1,5 +1,5 @@
extern crate micromark;
-use micromark::{micromark, micromark_with_options, Options};
+use micromark::{micromark, micromark_with_options, Constructs, Options};
fn main() {
// Turn on debugging.
@@ -21,4 +21,19 @@ fn main() {
}
)
);
+
+ // Support extensions that are not in CommonMark.
+ println!(
+ "{:?}",
+ micromark_with_options(
+ "---\ntitle: Neptune\n---\nSome stuff on the moons of Neptune.",
+ &Options {
+ constructs: Constructs {
+ frontmatter: true,
+ ..Constructs::default()
+ },
+ ..Options::default()
+ }
+ )
+ );
}
diff --git a/readme.md b/readme.md
index bad92da..24e6a72 100644
--- a/readme.md
+++ b/readme.md
@@ -8,7 +8,6 @@ Crate docs are currently at
### Docs
- [ ] (1) Add overview docs on how everything works
-- [ ] (1) Add more examples
### Refactor
@@ -41,14 +40,10 @@ Crate docs are currently at
### Extensions
-The main thing here is is to figure out if folks could extend from the outside
-with their own code, or if we need to maintain it all here.
-Regardless, it is essential for the launch of `micromark-rs` that extensions
-are theoretically or practically possible.
The extensions below are listed from top to bottom from more important to less
important.
-- [ ] (1) frontmatter (yaml, toml) (flow)
+- [x] (1) frontmatter (yaml, toml) (flow)
— [`micromark-extension-frontmatter`](https://github.com/micromark/micromark-extension-frontmatter)
- [ ] (3) autolink literal (GFM) (text)
— [`micromark-extension-gfm-autolink-literal`](https://github.com/micromark/micromark-extension-gfm-autolink-literal)
diff --git a/src/compiler.rs b/src/compiler.rs
index db0df9b..a5b2bf5 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -319,6 +319,7 @@ fn enter(context: &mut CompileContext) {
Name::Definition => on_enter_definition(context),
Name::DefinitionDestinationString => on_enter_definition_destination_string(context),
Name::Emphasis => on_enter_emphasis(context),
+ Name::Frontmatter => on_enter_frontmatter(context),
Name::HtmlFlow => on_enter_html_flow(context),
Name::HtmlText => on_enter_html_text(context),
Name::Image => on_enter_image(context),
@@ -361,6 +362,7 @@ fn exit(context: &mut CompileContext) {
Name::DefinitionLabelString => on_exit_definition_label_string(context),
Name::DefinitionTitleString => on_exit_definition_title_string(context),
Name::Emphasis => on_exit_emphasis(context),
+ Name::Frontmatter => on_exit_frontmatter(context),
Name::HardBreakEscape | Name::HardBreakTrailing => on_exit_break(context),
Name::HeadingAtx => on_exit_heading_atx(context),
Name::HeadingAtxSequence => on_exit_heading_atx_sequence(context),
@@ -451,6 +453,11 @@ fn on_enter_emphasis(context: &mut CompileContext) {
}
}
+/// Handle [`Enter`][Kind::Enter]:[`Frontmatter`][Name::Frontmatter].
+fn on_enter_frontmatter(context: &mut CompileContext) {
+ context.buffer();
+}
+
/// Handle [`Enter`][Kind::Enter]:[`HtmlFlow`][Name::HtmlFlow].
fn on_enter_html_flow(context: &mut CompileContext) {
context.line_ending_if_needed();
@@ -908,6 +915,12 @@ fn on_exit_emphasis(context: &mut CompileContext) {
}
}
+/// Handle [`Exit`][Kind::Exit]:[`Frontmatter`][Name::Frontmatter].
+fn on_exit_frontmatter(context: &mut CompileContext) {
+ context.resume();
+ context.slurp_one_line_ending = true;
+}
+
/// Handle [`Exit`][Kind::Exit]:[`HeadingAtx`][Name::HeadingAtx].
fn on_exit_heading_atx(context: &mut CompileContext) {
let rank = context
diff --git a/src/constant.rs b/src/constant.rs
index b856fd0..45853a7 100644
--- a/src/constant.rs
+++ b/src/constant.rs
@@ -67,6 +67,13 @@ pub const CHARACTER_REFERENCE_NAMED_SIZE_MAX: usize = 31;
/// [code_fenced]: crate::construct::code_fenced
pub const CODE_FENCED_SEQUENCE_SIZE_MIN: usize = 3;
+/// The number of markers needed for [frontmatter][] to form.
+///
+/// Like many things in markdown, the number is `3`.
+///
+/// [frontmatter]: crate::construct::frontmatter
+pub const FRONTMATTER_SEQUENCE_SIZE: usize = 3;
+
/// The number of preceding spaces needed for a [hard break
/// (trailing)][whitespace] to form.
///
diff --git a/src/construct/code_indented.rs b/src/construct/code_indented.rs
index 89c5652..c5439f1 100644
--- a/src/construct/code_indented.rs
+++ b/src/construct/code_indented.rs
@@ -53,8 +53,8 @@
//! [html_code]: https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-code-element
//! [html_pre]: https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
-use super::partial_space_or_tab::{space_or_tab, space_or_tab_min_max};
use crate::constant::TAB_SIZE;
+use crate::construct::partial_space_or_tab::{space_or_tab, space_or_tab_min_max};
use crate::event::Name;
use crate::state::{Name as StateName, State};
use crate::tokenizer::Tokenizer;
diff --git a/src/construct/document.rs b/src/construct/document.rs
index 0cda368..2cc170d 100644
--- a/src/construct/document.rs
+++ b/src/construct/document.rs
@@ -58,13 +58,29 @@ pub fn start(tokenizer: &mut Tokenizer) -> State {
)));
tokenizer.attempt(
- State::Next(StateName::DocumentContainerExistingBefore),
- State::Next(StateName::DocumentContainerExistingBefore),
+ State::Next(StateName::DocumentBeforeFrontmatter),
+ State::Next(StateName::DocumentBeforeFrontmatter),
);
State::Retry(StateName::BomStart)
}
+/// At optional frontmatter.
+///
+/// ```markdown
+/// > | ---
+/// ^
+/// | title: Venus
+/// | ---
+/// ```
+pub fn before_frontmatter(tokenizer: &mut Tokenizer) -> State {
+ tokenizer.attempt(
+ State::Next(StateName::DocumentContainerNewBefore),
+ State::Next(StateName::DocumentContainerNewBefore),
+ );
+ State::Retry(StateName::FrontmatterStart)
+}
+
/// At optional existing containers.
//
/// ```markdown
diff --git a/src/construct/flow.rs b/src/construct/flow.rs
index 08c7891..f3c7685 100644
--- a/src/construct/flow.rs
+++ b/src/construct/flow.rs
@@ -35,28 +35,28 @@ use crate::tokenizer::Tokenizer;
/// ```
pub fn start(tokenizer: &mut Tokenizer) -> State {
match tokenizer.current {
- Some(b'`' | b'~') => {
+ Some(b'#') => {
tokenizer.attempt(
State::Next(StateName::FlowAfter),
State::Next(StateName::FlowBeforeParagraph),
);
- State::Retry(StateName::CodeFencedStart)
+ State::Retry(StateName::HeadingAtxStart)
}
- Some(b'<') => {
+ Some(b'*' | b'_') => {
tokenizer.attempt(
State::Next(StateName::FlowAfter),
State::Next(StateName::FlowBeforeParagraph),
);
- State::Retry(StateName::HtmlFlowStart)
+ State::Retry(StateName::ThematicBreakStart)
}
- Some(b'#') => {
+ Some(b'<') => {
tokenizer.attempt(
State::Next(StateName::FlowAfter),
State::Next(StateName::FlowBeforeParagraph),
);
- State::Retry(StateName::HeadingAtxStart)
+ State::Retry(StateName::HtmlFlowStart)
}
- // Note: `-` is also used in thematic breaks, so it’s not included here.
+ // Note: `-` is also used in thematic breaks so it’s not included here.
Some(b'=') => {
tokenizer.attempt(
State::Next(StateName::FlowAfter),
@@ -64,22 +64,22 @@ pub fn start(tokenizer: &mut Tokenizer) -> State {
);
State::Retry(StateName::HeadingSetextStart)
}
- Some(b'*' | b'_') => {
+ Some(b'[') => {
tokenizer.attempt(
State::Next(StateName::FlowAfter),
State::Next(StateName::FlowBeforeParagraph),
);
- State::Retry(StateName::ThematicBreakStart)
+ State::Retry(StateName::DefinitionStart)
}
- Some(b'[') => {
+ Some(b'`' | b'~') => {
tokenizer.attempt(
State::Next(StateName::FlowAfter),
State::Next(StateName::FlowBeforeParagraph),
);
- State::Retry(StateName::DefinitionStart)
+ State::Retry(StateName::CodeFencedStart)
}
// Actual parsing: blank line? Indented code? Indented anything?
- // Also includes `-` which can be a setext heading underline or a thematic break.
+ // Also includes `-` which can be a setext heading underline or thematic break.
None | Some(b'\t' | b'\n' | b' ' | b'-') => State::Retry(StateName::FlowBlankLineBefore),
// Must be a paragraph.
Some(_) => {
diff --git a/src/construct/frontmatter.rs b/src/construct/frontmatter.rs
new file mode 100644
index 0000000..dc47bee
--- /dev/null
+++ b/src/construct/frontmatter.rs
@@ -0,0 +1,293 @@
+//! Frontmatter occurs at the start of the document.
+//!
+//! ## Grammar
+//!
+//! Frontmatter forms with the following BNF
+//! (<small>see [construct][crate::construct] for character groups</small>):
+//!
+//! ```bnf
+//! frontmatter ::= fence_open *( eol *byte ) eol fence_close
+//! fence_open ::= sequence *space_or_tab
+//! ; Restriction: markers in `sequence` must match markers in opening sequence.
+//! fence_close ::= sequence *space_or_tab
+//! sequence ::= 3'+' | 3'-'
+//! ```
+//!
+//! Frontmatter can only occur once.
+//! It cannot occur in a container.
+//! It must have a closing fence.
+//! Like flow constructs, it must be followed by an eol (line ending) or
+//! eof (end of file).
+//!
+//! ## Extension
+//!
+//! > 👉 **Note**: frontmatter is not part of `CommonMark`, so frontmatter is
+//! > not enabled by default.
+//! > You need to enable it manually.
+//! > See [`Constructs`][constructs] for more info.
+//!
+//! As there is no spec for frontmatter in markdown, this extension follows how
+//! YAML frontmatter works on `github.com`.
+//! It also parses TOML frontmatter, just like YAML except that it uses a `+`.
+//!
+//! ## Recommendation
+//!
+//! When authoring markdown with frontmatter, it’s recommended to use YAML
+//! frontmatter if possible.
+//! While YAML has some warts, it works in the most places, so using it
+//! guarantees the highest chance of portability.
+//!
+//! In certain ecosystems, other flavors are widely used.
+//! For example, in the Rust ecosystem, TOML is often used.
+//! In such cases, using TOML is an okay choice.
+//!
+//! ## Tokens
+//!
+//! * [`Frontmatter`][Name::Frontmatter]
+//! * [`FrontmatterFence`][Name::FrontmatterFence]
+//! * [`FrontmatterSequence`][Name::FrontmatterSequence]
+//! * [`FrontmatterChunk`][Name::FrontmatterChunk]
+//! * [`LineEnding`][Name::LineEnding]
+//! * [`SpaceOrTab`][Name::SpaceOrTab]
+//!
+//! ## References
+//!
+//! * [`micromark-extension-frontmatter`](https://github.com/micromark/micromark-extension-frontmatter)
+//!
+//! [constructs]: crate::Constructs
+
+use crate::constant::FRONTMATTER_SEQUENCE_SIZE;
+use crate::construct::partial_space_or_tab::space_or_tab;
+use crate::event::Name;
+use crate::state::{Name as StateName, State};
+use crate::tokenizer::Tokenizer;
+
+/// Start of frontmatter.
+///
+/// ```markdown
+/// > | ---
+/// ^
+/// | title: "Venus"
+/// | ---
+/// ```
+pub fn start(tokenizer: &mut Tokenizer) -> State {
+ // Indent not allowed.
+ if tokenizer.parse_state.constructs.frontmatter
+ && matches!(tokenizer.current, Some(b'+' | b'-'))
+ {
+ tokenizer.tokenize_state.marker = tokenizer.current.unwrap();
+ tokenizer.enter(Name::Frontmatter);
+ tokenizer.enter(Name::FrontmatterFence);
+ tokenizer.enter(Name::FrontmatterSequence);
+ State::Retry(StateName::FrontmatterOpenSequence)
+ } else {
+ State::Nok
+ }
+}
+
+/// In open sequence.
+///
+/// ```markdown
+/// > | ---
+/// ^
+/// | title: "Venus"
+/// | ---
+/// ```
+pub fn open_sequence(tokenizer: &mut Tokenizer) -> State {
+ if tokenizer.current == Some(tokenizer.tokenize_state.marker) {
+ tokenizer.tokenize_state.size += 1;
+ tokenizer.consume();
+ State::Next(StateName::FrontmatterOpenSequence)
+ } else if tokenizer.tokenize_state.size == FRONTMATTER_SEQUENCE_SIZE {
+ tokenizer.tokenize_state.size = 0;
+ tokenizer.exit(Name::FrontmatterSequence);
+
+ if matches!(tokenizer.current, Some(b'\t' | b' ')) {
+ tokenizer.attempt(State::Next(StateName::FrontmatterOpenAfter), State::Nok);
+ State::Retry(space_or_tab(tokenizer))
+ } else {
+ State::Retry(StateName::FrontmatterOpenAfter)
+ }
+ } else {
+ tokenizer.tokenize_state.marker = 0;
+ tokenizer.tokenize_state.size = 0;
+ State::Nok
+ }
+}
+
+/// After open sequence.
+///
+/// ```markdown
+/// > | ---
+/// ^
+/// | title: "Venus"
+/// | ---
+/// ```
+pub fn open_after(tokenizer: &mut Tokenizer) -> State {
+ if let Some(b'\n') = tokenizer.current {
+ tokenizer.exit(Name::FrontmatterFence);
+ tokenizer.enter(Name::LineEnding);
+ tokenizer.consume();
+ tokenizer.exit(Name::LineEnding);
+ tokenizer.attempt(
+ State::Next(StateName::FrontmatterAfter),
+ State::Next(StateName::FrontmatterContentStart),
+ );
+ State::Next(StateName::FrontmatterCloseStart)
+ } else {
+ tokenizer.tokenize_state.marker = 0;
+ State::Nok
+ }
+}
+
+/// Start of close sequence.
+///
+/// ```markdown
+/// | ---
+/// | title: "Venus"
+/// > | ---
+/// ^
+/// ```
+pub fn close_start(tokenizer: &mut Tokenizer) -> State {
+ if tokenizer.current == Some(tokenizer.tokenize_state.marker) {
+ tokenizer.enter(Name::FrontmatterFence);
+ tokenizer.enter(Name::FrontmatterSequence);
+ State::Retry(StateName::FrontmatterCloseSequence)
+ } else {
+ State::Nok
+ }
+}
+
+/// In close sequence.
+///
+/// ```markdown
+/// | ---
+/// | title: "Venus"
+/// > | ---
+/// ^
+/// ```
+pub fn close_sequence(tokenizer: &mut Tokenizer) -> State {
+ if tokenizer.current == Some(tokenizer.tokenize_state.marker) {
+ tokenizer.tokenize_state.size += 1;
+ tokenizer.consume();
+ State::Next(StateName::FrontmatterCloseSequence)
+ } else if tokenizer.tokenize_state.size == FRONTMATTER_SEQUENCE_SIZE {
+ tokenizer.tokenize_state.size = 0;
+ tokenizer.exit(Name::FrontmatterSequence);
+
+ if matches!(tokenizer.current, Some(b'\t' | b' ')) {
+ tokenizer.attempt(State::Next(StateName::FrontmatterCloseAfter), State::Nok);
+ State::Retry(space_or_tab(tokenizer))
+ } else {
+ State::Retry(StateName::FrontmatterCloseAfter)
+ }
+ } else {
+ tokenizer.tokenize_state.size = 0;
+ State::Nok
+ }
+}
+
+/// After close sequence.
+///
+/// ```markdown
+/// | ---
+/// | title: "Venus"
+/// > | ---
+/// ^
+/// ```
+pub fn close_after(tokenizer: &mut Tokenizer) -> State {
+ match tokenizer.current {
+ None | Some(b'\n') => {
+ tokenizer.exit(Name::FrontmatterFence);
+ State::Ok
+ }
+ _ => State::Nok,
+ }
+}
+
+/// Start of content chunk.
+///
+/// ```markdown
+/// | ---
+/// > | title: "Venus"
+/// ^
+/// | ---
+/// ```
+pub fn content_start(tokenizer: &mut Tokenizer) -> State {
+ match tokenizer.current {
+ None | Some(b'\n') => State::Retry(StateName::FrontmatterContentEnd),
+ Some(_) => {
+ tokenizer.enter(Name::FrontmatterChunk);
+ State::Retry(StateName::FrontmatterContentInside)
+ }
+ }
+}
+
+/// In content chunk.
+///
+/// ```markdown
+/// | ---
+/// > | title: "Venus"
+/// ^
+/// | ---
+/// ```
+pub fn content_inside(tokenizer: &mut Tokenizer) -> State {
+ match tokenizer.current {
+ None | Some(b'\n') => {
+ tokenizer.exit(Name::FrontmatterChunk);
+ State::Retry(StateName::FrontmatterContentEnd)
+ }
+ Some(_) => {
+ tokenizer.consume();
+ State::Next(StateName::FrontmatterContentInside)
+ }
+ }
+}
+
+/// End of content chunk.
+///
+/// ```markdown
+/// | ---
+/// > | title: "Venus"
+/// ^
+/// | ---
+/// ```
+pub fn content_end(tokenizer: &mut Tokenizer) -> State {
+ match tokenizer.current {
+ None => {
+ tokenizer.tokenize_state.marker = 0;
+ State::Nok
+ }
+ Some(b'\n') => {
+ tokenizer.enter(Name::LineEnding);
+ tokenizer.consume();
+ tokenizer.exit(Name::LineEnding);
+ tokenizer.attempt(
+ State::Next(StateName::FrontmatterAfter),
+ State::Next(StateName::FrontmatterContentStart),
+ );
+ State::Next(StateName::FrontmatterCloseStart)
+ }
+ Some(_) => unreachable!("expected eof/eol"),
+ }
+}
+
+/// After frontmatter.
+///
+/// ```markdown
+/// | ---
+/// | title: "Venus"
+/// > | ---
+/// ^
+/// ```
+pub fn after(tokenizer: &mut Tokenizer) -> State {
+ tokenizer.tokenize_state.marker = 0;
+
+ match tokenizer.current {
+ None | Some(b'\n') => {
+ tokenizer.exit(Name::Frontmatter);
+ State::Ok
+ }
+ _ => State::Nok,
+ }
+}
diff --git a/src/construct/mod.rs b/src/construct/mod.rs
index 5630143..1c1c6f7 100644
--- a/src/construct/mod.rs
+++ b/src/construct/mod.rs
@@ -40,6 +40,7 @@
//! * [code (indented)][code_indented]
//! * [code (text)][code_text]
//! * [definition][]
+//! * [frontmatter][]
//! * [hard break (escape)][hard_break_escape]
//! * [heading (atx)][heading_atx]
//! * [heading (setext)][heading_setext]
@@ -139,6 +140,7 @@ pub mod code_text;
pub mod definition;
pub mod document;
pub mod flow;
+pub mod frontmatter;
pub mod hard_break_escape;
pub mod heading_atx;
pub mod heading_setext;
diff --git a/src/construct/thematic_break.rs b/src/construct/thematic_break.rs
index 0a8ebe9..74fd961 100644
--- a/src/construct/thematic_break.rs
+++ b/src/construct/thematic_break.rs
@@ -56,8 +56,8 @@
//! [list-item]: crate::construct::list_item
//! [html]: https://html.spec.whatwg.org/multipage/grouping-content.html#the-hr-element
-use super::partial_space_or_tab::{space_or_tab, space_or_tab_min_max};
use crate::constant::{TAB_SIZE, THEMATIC_BREAK_MARKER_COUNT_MIN};
+use crate::construct::partial_space_or_tab::{space_or_tab, space_or_tab_min_max};
use crate::event::Name;
use crate::state::{Name as StateName, State};
use crate::tokenizer::Tokenizer;
diff --git a/src/event.rs b/src/event.rs
index 8058d64..f2f8ae1 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -1832,10 +1832,98 @@ pub enum Name {
/// ^ ^ ^
/// ```
ThematicBreakSequence,
+
+ /// Whole frontmatter.
+ ///
+ /// ## Info
+ ///
+ /// * **Context**:
+ /// [document content][crate::construct::document]
+ /// * **Content model**:
+ /// [`FrontmatterFence`][Name::FrontmatterFence],
+ /// [`FrontmatterChunk`][Name::FrontmatterChunk],
+ /// [`LineEnding`][Name::LineEnding]
+ /// * **Construct**:
+ /// [`frontmatter`][crate::construct::frontmatter]
+ ///
+ /// ## Example
+ ///
+ /// ````markdown
+ /// > | ---
+ /// ^^^
+ /// > | title: Neptune
+ /// ^^^^^^^^^^^^^^
+ /// > | ---
+ /// ^^^
+ /// ````
+ Frontmatter,
+ /// Frontmatter chunk.
+ ///
+ /// ## Info
+ ///
+ /// * **Context**:
+ /// [`Frontmatter`][Name::Frontmatter]
+ /// * **Content model**:
+ /// void
+ /// * **Construct**:
+ /// [`frontmatter`][crate::construct::frontmatter]
+ ///
+ /// ## Example
+ ///
+ /// ````markdown
+ /// | ---
+ /// > | title: Neptune
+ /// ^^^^^^^^^^^^^^
+ /// | ---
+ /// ````
+ FrontmatterChunk,
+ /// Frontmatter fence.
+ ///
+ /// ## Info
+ ///
+ /// * **Context**:
+ /// [`Frontmatter`][Name::Frontmatter]
+ /// * **Content model**:
+ /// [`FrontmatterSequence`][Name::FrontmatterSequence],
+ /// [`SpaceOrTab`][Name::SpaceOrTab]
+ /// * **Construct**:
+ /// [`frontmatter`][crate::construct::frontmatter]
+ ///
+ /// ## Example
+ ///
+ /// ````markdown
+ /// > | ---
+ /// ^^^
+ /// | title: Neptune
+ /// > | ---
+ /// ^^^
+ /// ````
+ FrontmatterFence,
+ /// Frontmatter sequence.
+ ///
+ /// ## Info
+ ///
+ /// * **Context**:
+ /// [`FrontmatterFence`][Name::FrontmatterFence]
+ /// * **Content model**:
+ /// void
+ /// * **Construct**:
+ /// [`frontmatter`][crate::construct::frontmatter]
+ ///
+ /// ## Example
+ ///
+ /// ````markdown
+ /// > | ---
+ /// ^^^
+ /// | title: Neptune
+ /// > | ---
+ /// ^^^
+ /// ````
+ FrontmatterSequence,
}
/// List of void events, used to make sure everything is working well.
-pub const VOID_EVENTS: [Name; 41] = [
+pub const VOID_EVENTS: [Name; 43] = [
Name::AttentionSequence,
Name::AutolinkEmail,
Name::AutolinkMarker,
@@ -1860,6 +1948,8 @@ pub const VOID_EVENTS: [Name; 41] = [
Name::DefinitionMarker,
Name::DefinitionTitleMarker,
Name::EmphasisSequence,
+ Name::FrontmatterChunk,
+ Name::FrontmatterSequence,
Name::HardBreakEscape,
Name::HardBreakTrailing,
Name::HeadingAtxSequence,
diff --git a/src/lib.rs b/src/lib.rs
index 4de633c..428838a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -150,6 +150,17 @@ pub struct Constructs {
/// ^^^^^^^^^^
/// ```
pub definition: bool,
+ /// Frontmatter.
+ ///
+ /// ````markdown
+ /// > | ---
+ /// ^^^
+ /// > | title: Neptune
+ /// ^^^^^^^^^^^^^^
+ /// > | ---
+ /// ^^^
+ /// ````
+ pub frontmatter: bool,
/// Hard break (escape).
///
/// ```markdown
@@ -246,6 +257,7 @@ impl Default for Constructs {
code_fenced: true,
code_text: true,
definition: true,
+ frontmatter: false,
hard_break_escape: true,
hard_break_trailing: true,
heading_atx: true,
diff --git a/src/state.rs b/src/state.rs
index f9cc39a..da935d1 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -110,6 +110,7 @@ pub enum Name {
DestinationRawEscape,
DocumentStart,
+ DocumentBeforeFrontmatter,
DocumentContainerExistingBefore,
DocumentContainerExistingAfter,
DocumentContainerNewBefore,
@@ -133,6 +134,17 @@ pub enum Name {
FlowBlankLineAfter,
FlowBeforeParagraph,
+ FrontmatterStart,
+ FrontmatterOpenSequence,
+ FrontmatterOpenAfter,
+ FrontmatterAfter,
+ FrontmatterContentStart,
+ FrontmatterContentInside,
+ FrontmatterContentEnd,
+ FrontmatterCloseStart,
+ FrontmatterCloseSequence,
+ FrontmatterCloseAfter,
+
HardBreakEscapeStart,
HardBreakEscapeAfter,
@@ -393,6 +405,7 @@ pub fn call(tokenizer: &mut Tokenizer, name: Name) -> State {
Name::DestinationRawEscape => construct::partial_destination::raw_escape,
Name::DocumentStart => construct::document::start,
+ Name::DocumentBeforeFrontmatter => construct::document::before_frontmatter,
Name::DocumentContainerExistingBefore => construct::document::container_existing_before,
Name::DocumentContainerExistingAfter => construct::document::container_existing_after,
Name::DocumentContainerNewBefore => construct::document::container_new_before,
@@ -420,6 +433,17 @@ pub fn call(tokenizer: &mut Tokenizer, name: Name) -> State {
Name::FlowBlankLineAfter => construct::flow::blank_line_after,
Name::FlowBeforeParagraph => construct::flow::before_paragraph,
+ Name::FrontmatterStart => construct::frontmatter::start,
+ Name::FrontmatterOpenSequence => construct::frontmatter::open_sequence,
+ Name::FrontmatterOpenAfter => construct::frontmatter::open_after,
+ Name::FrontmatterAfter => construct::frontmatter::after,
+ Name::FrontmatterContentStart => construct::frontmatter::content_start,
+ Name::FrontmatterContentInside => construct::frontmatter::content_inside,
+ Name::FrontmatterContentEnd => construct::frontmatter::content_end,
+ Name::FrontmatterCloseStart => construct::frontmatter::close_start,
+ Name::FrontmatterCloseSequence => construct::frontmatter::close_sequence,
+ Name::FrontmatterCloseAfter => construct::frontmatter::close_after,
+
Name::HardBreakEscapeStart => construct::hard_break_escape::start,
Name::HardBreakEscapeAfter => construct::hard_break_escape::after,
diff --git a/src/tokenizer.rs b/src/tokenizer.rs
index 7eba194..2edab03 100644
--- a/src/tokenizer.rs
+++ b/src/tokenizer.rs
@@ -509,6 +509,11 @@ impl<'a> Tokenizer<'a> {
/// Stack an attempt, moving to `ok` on [`State::Ok`][] and `nok` on
/// [`State::Nok`][], reverting in both cases.
pub fn check(&mut self, ok: State, nok: State) {
+ debug_assert_ne!(
+ nok,
+ State::Nok,
+ "checking w/ `State::Nok` should likely be an attempt"
+ );
// Always capture (and restore) when checking.
// No need to capture (and restore) when `nok` is `State::Nok`, because the
// parent attempt will do it.
diff --git a/tests/frontmatter.rs b/tests/frontmatter.rs
new file mode 100644
index 0000000..4882bf2
--- /dev/null
+++ b/tests/frontmatter.rs
@@ -0,0 +1,67 @@
+extern crate micromark;
+use micromark::{micromark, micromark_with_options, Constructs, Options};
+
+#[test]
+fn frontmatter() {
+ let frontmatter = Options {
+ constructs: Constructs {
+ frontmatter: true,
+ ..Constructs::default()
+ },
+ ..Options::default()
+ };
+
+ assert_eq!(
+ micromark("---\ntitle: Jupyter\n---"),
+ "<hr />\n<h2>title: Jupyter</h2>",
+ "should not support frontmatter by default"
+ );
+
+ assert_eq!(
+ micromark_with_options("---\ntitle: Jupyter\n---", &frontmatter),
+ "",
+ "should support frontmatter (yaml)"
+ );
+
+ assert_eq!(
+ micromark_with_options("+++\ntitle = \"Jupyter\"\n+++", &frontmatter),
+ "",
+ "should support frontmatter (toml)"
+ );
+
+ assert_eq!(
+ micromark_with_options("---\n---", &frontmatter),
+ "",
+ "should support empty frontmatter"
+ );
+
+ assert_eq!(
+ micromark_with_options("---\n---\n## Neptune", &frontmatter),
+ "<h2>Neptune</h2>",
+ "should support content after frontmatter"
+ );
+
+ assert_eq!(
+ micromark_with_options("## Neptune\n---\n---", &frontmatter),
+ "<h2>Neptune</h2>\n<hr />\n<hr />",
+ "should not support frontmatter after content"
+ );
+
+ assert_eq!(
+ micromark_with_options("> ---\n> ---\n> ## Neptune", &frontmatter),
+ "<blockquote>\n<hr />\n<hr />\n<h2>Neptune</h2>\n</blockquote>",
+ "should not support frontmatter in a container"
+ );
+
+ assert_eq!(
+ micromark_with_options("---", &frontmatter),
+ "<hr />",
+ "should not support just an opening fence"
+ );
+
+ assert_eq!(
+ micromark_with_options("---\ntitle: Neptune", &frontmatter),
+ "<hr />\n<p>title: Neptune</p>",
+ "should not support a missing closing fence"
+ );
+}