From 1e4c95079cb97b2b02440b21945c6d12741a7d19 Mon Sep 17 00:00:00 2001
From: Titus Wormer
");
}
+/// Generate a footnote section.
+fn generate_footnote_section(context: &mut CompileContext) {
+ context.line_ending_if_needed();
+ context.push("");
+
+ let mut index = 0;
+ while index < context.gfm_footnote_definition_calls.len() {
+ generate_footnote_item(context, index);
+ index += 1;
+ }
+
+ context.line_ending();
+ context.push("
");
+ context.line_ending();
+ context.push("
!^a
+///!^a
+/// ``` +pub fn after(tokenizer: &mut Tokenizer) -> State { + if tokenizer + .parse_state + .options + .constructs + .gfm_label_start_footnote + && tokenizer.current == Some(b'^') + { + State::Nok + } else { + tokenizer.exit(Name::LabelImage); + tokenizer.tokenize_state.label_starts.push(LabelStart { + kind: LabelKind::Image, + start: (tokenizer.events.len() - 6, tokenizer.events.len() - 1), + inactive: false, + }); + tokenizer.register_resolver_before(ResolveName::Label); + State::Ok + } +} diff --git a/src/construct/label_start_link.rs b/src/construct/label_start_link.rs index 3aeb68b..3454724 100644 --- a/src/construct/label_start_link.rs +++ b/src/construct/label_start_link.rs @@ -34,7 +34,7 @@ use crate::event::Name; use crate::resolve::Name as ResolveName; use crate::state::State; -use crate::tokenizer::{LabelStart, Tokenizer}; +use crate::tokenizer::{LabelKind, LabelStart, Tokenizer}; /// Start of label (link) start. /// @@ -52,6 +52,7 @@ pub fn start(tokenizer: &mut Tokenizer) -> State { tokenizer.exit(Name::LabelMarker); tokenizer.exit(Name::LabelLink); tokenizer.tokenize_state.label_starts.push(LabelStart { + kind: LabelKind::Link, start: (start, tokenizer.events.len() - 1), inactive: false, }); diff --git a/src/construct/list_item.rs b/src/construct/list_item.rs index 39b5d13..658c2c7 100644 --- a/src/construct/list_item.rs +++ b/src/construct/list_item.rs @@ -17,7 +17,7 @@ //! ``` //! //! Further lines that are not prefixed with `list_item_cont` cause the list -//! item to be exited, except when those lines are lazy continuation. +//! item to be exited, except when those lines are lazy continuation or blank. //! Like so many things in markdown, list items too, are complex. //! See [*§ Phase 1: block structure* in `CommonMark`][commonmark_block] for //! more on parsing details. diff --git a/src/construct/mod.rs b/src/construct/mod.rs index 7ac3899..c5002bb 100644 --- a/src/construct/mod.rs +++ b/src/construct/mod.rs @@ -59,6 +59,9 @@ //! //! * [frontmatter][] //! * [gfm autolink literal][gfm_autolink_literal] +//! * [gfm footnote definition][gfm_footnote_definition] +//! * [gfm task list item check][gfm_task_list_item_check] +//! * [gfm label start footnote][gfm_label_start_footnote] //! //! There are also several small subroutines typically used in different places: //! @@ -146,6 +149,8 @@ pub mod document; pub mod flow; pub mod frontmatter; pub mod gfm_autolink_literal; +pub mod gfm_footnote_definition; +pub mod gfm_label_start_footnote; pub mod gfm_task_list_item_check; pub mod hard_break_escape; pub mod heading_atx; diff --git a/src/construct/partial_label.rs b/src/construct/partial_label.rs index 47ffd90..ab436b2 100644 --- a/src/construct/partial_label.rs +++ b/src/construct/partial_label.rs @@ -81,13 +81,37 @@ pub fn start(tokenizer: &mut Tokenizer) -> State { tokenizer.enter(tokenizer.tokenize_state.token_2.clone()); tokenizer.consume(); tokenizer.exit(tokenizer.tokenize_state.token_2.clone()); - tokenizer.enter(tokenizer.tokenize_state.token_3.clone()); - State::Next(StateName::LabelAtBreak) + State::Next(StateName::LabelAtMarker) } _ => State::Nok, } } +/// At an optional extra marker. +/// +/// Used for footnotes. +/// +/// ```markdown +/// > | [^a] +/// ^ +/// ``` +pub fn at_marker(tokenizer: &mut Tokenizer) -> State { + // For footnotes (and potentially other custom things in the future), + // We need to make sure there is a certain marker after `[`. + if tokenizer.tokenize_state.marker == 0 { + tokenizer.enter(tokenizer.tokenize_state.token_3.clone()); + State::Retry(StateName::LabelAtBreak) + } else if tokenizer.current == Some(tokenizer.tokenize_state.marker) { + tokenizer.enter(tokenizer.tokenize_state.token_4.clone()); + tokenizer.consume(); + tokenizer.exit(tokenizer.tokenize_state.token_4.clone()); + tokenizer.enter(tokenizer.tokenize_state.token_3.clone()); + State::Next(StateName::LabelAtBreak) + } else { + State::Nok + } +} + /// In label, at something, before something else. /// /// ```markdown diff --git a/src/construct/text.rs b/src/construct/text.rs index 65f55d4..5535e3f 100644 --- a/src/construct/text.rs +++ b/src/construct/text.rs @@ -11,6 +11,8 @@ //! * [Character escape][crate::construct::character_escape] //! * [Character reference][crate::construct::character_reference] //! * [Code (text)][crate::construct::code_text] +//! * [GFM: Label start (footnote)][crate::construct::gfm_label_start_footnote] +//! * [GFM: Task list item check][crate::construct::gfm_task_list_item_check] //! * [Hard break (escape)][crate::construct::hard_break_escape] //! * [HTML (text)][crate::construct::html_text] //! * [Label start (image)][crate::construct::label_start_image] @@ -34,7 +36,7 @@ const MARKERS: [u8; 10] = [ b'<', // `autolink`, `html_text` b'[', // `label_start_link` b'\\', // `character_escape`, `hard_break_escape` - b']', // `label_end` + b']', // `label_end`, `gfm_label_start_footnote` b'_', // `attention` b'`', // `code_text` b'~', // `attention` (w/ `gfm_strikethrough`) @@ -104,9 +106,9 @@ pub fn before(tokenizer: &mut Tokenizer) -> State { Some(b'[') => { tokenizer.attempt( State::Next(StateName::TextBefore), - State::Next(StateName::TextBeforeData), + State::Next(StateName::TextBeforeLabelStartLink), ); - State::Retry(StateName::LabelStartLinkStart) + State::Retry(StateName::GfmLabelStartFootnoteStart) } Some(b'\\') => { tokenizer.attempt( @@ -165,6 +167,22 @@ pub fn before_hard_break_escape(tokenizer: &mut Tokenizer) -> State { State::Retry(StateName::HardBreakEscapeStart) } +/// Before label start (link). +/// +/// At `[`, which wasn’t a GFM label start (footnote). +/// +/// ```markdown +/// > | [a](b) +/// ^ +/// ``` +pub fn before_label_start_link(tokenizer: &mut Tokenizer) -> State { + tokenizer.attempt( + State::Next(StateName::TextBefore), + State::Next(StateName::TextBeforeData), + ); + State::Retry(StateName::LabelStartLinkStart) +} + /// Before data. /// /// ```markdown diff --git a/src/event.rs b/src/event.rs index f20c599..3b805e5 100644 --- a/src/event.rs +++ b/src/event.rs @@ -753,7 +753,8 @@ pub enum Name { /// ## Info /// /// * **Context**: - /// [`Definition`][Name::Definition] + /// [`Definition`][Name::Definition], + /// [`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition] /// * **Content model**: /// void /// * **Construct**: @@ -1019,7 +1020,172 @@ pub enum Name { /// ^^^^^^^^^^^^^^^ /// ``` GfmAutolinkLiteralWww, - /// GFM: Strikethrough. + /// GFM extension: whole footnote call. + /// + /// ## Info + /// + /// * **Context**: + /// [text content][crate::construct::text] + /// * **Content model**: + /// [`Label`][Name::Label] + /// * **Construct**: + /// [`label_end`][crate::construct::label_end] + /// + /// ## Example + /// + /// ```markdown + /// > | a [^b] c + /// ^^^^ + /// ``` + GfmFootnoteCall, + /// GFM extension: label start (footnote). + /// + /// ## Info + /// + /// * **Context**: + /// [`Label`][Name::Label] + /// * **Content model**: + /// [`GfmFootnoteCallMarker`][Name::GfmFootnoteCallMarker], + /// [`LabelMarker`][Name::LabelMarker] + /// * **Construct**: + /// [`gfm_label_start_footnote`][crate::construct::gfm_label_start_footnote] + /// + /// ## Example + /// + /// ```markdown + /// > | a [^b] c + /// ^^ + /// ``` + GfmFootnoteCallLabel, + /// GFM extension: label start (footnote) marker. + /// + /// ## Info + /// + /// * **Context**: + /// [`GfmFootnoteCallLabel`][Name::GfmFootnoteCallLabel] + /// * **Content model**: + /// void + /// * **Construct**: + /// [`gfm_label_start_footnote`][crate::construct::gfm_label_start_footnote] + /// + /// ## Example + /// + /// ```markdown + /// > | a [^b] c + /// ^ + /// ``` + GfmFootnoteCallMarker, + /// GFM extension: whole footnote definition. + /// + /// ## Info + /// + /// * **Context**: + /// [document content][crate::construct::document] + /// * **Content model**: + /// [`GfmFootnoteDefinitionPrefix`][Name::GfmFootnoteDefinitionPrefix], + /// [document content][crate::construct::flow] + /// * **Construct**: + /// [`gfm_footnote_definition`][crate::construct::gfm_footnote_definition] + /// + /// ## Example + /// + /// ```markdown + /// > | [^a]: b + /// ^^^^^^^ + /// ``` + GfmFootnoteDefinition, + /// GFM extension: footnote definition prefix. + /// + /// ## Info + /// + /// * **Context**: + /// [`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition] + /// * **Content model**: + /// [`DefinitionMarker`][Name::DefinitionMarker], + /// [`GfmFootnoteDefinitionLabel`][Name::GfmFootnoteDefinitionLabel], + /// [`SpaceOrTab`][Name::SpaceOrTab] + /// * **Construct**: + /// [`gfm_footnote_definition`][crate::construct::gfm_footnote_definition] + /// + /// ## Example + /// + /// ```markdown + /// > | [^a]: b + /// ^^^^^^ + /// ``` + GfmFootnoteDefinitionPrefix, + /// GFM extension: footnote definition label. + /// + /// ## Info + /// + /// * **Context**: + /// [`GfmFootnoteDefinitionPrefix`][Name::GfmFootnoteDefinitionPrefix] + /// * **Content model**: + /// [`GfmFootnoteDefinitionLabelMarker`][Name::GfmFootnoteDefinitionLabelMarker], + /// [`GfmFootnoteDefinitionLabelString`][Name::GfmFootnoteDefinitionLabelString], + /// [`GfmFootnoteDefinitionMarker`][Name::GfmFootnoteDefinitionMarker] + /// * **Construct**: + /// [`gfm_footnote_definition`][crate::construct::gfm_footnote_definition] + /// + /// ## Example + /// + /// ```markdown + /// > | [^a]: b + /// ^^^^ + /// ``` + GfmFootnoteDefinitionLabel, + /// GFM extension: footnote definition label marker. + /// + /// ## Info + /// + /// * **Context**: + /// [`GfmFootnoteDefinitionLabel`][Name::GfmFootnoteDefinitionLabel] + /// * **Content model**: + /// void + /// * **Construct**: + /// [`gfm_footnote_definition`][crate::construct::gfm_footnote_definition] + /// + /// ## Example + /// + /// ```markdown + /// > | [^a]: b + /// ^ ^ + GfmFootnoteDefinitionLabelMarker, + /// GFM extension: footnote definition label string. + /// + /// ## Info + /// + /// * **Context**: + /// [`GfmFootnoteDefinitionLabel`][Name::GfmFootnoteDefinitionLabel] + /// * **Content model**: + /// [string content][crate::construct::string] + /// * **Construct**: + /// [`gfm_footnote_definition`][crate::construct::gfm_footnote_definition] + /// + /// ## Example + /// + /// ```markdown + /// > | [^a]: b + /// ^ + GfmFootnoteDefinitionLabelString, + /// GFM extension: footnote definition marker. + /// + /// ## Info + /// + /// * **Context**: + /// [`GfmFootnoteDefinitionLabel`][Name::GfmFootnoteDefinitionLabel] + /// * **Content model**: + /// void + /// * **Construct**: + /// [`gfm_footnote_definition`][crate::construct::gfm_footnote_definition] + /// + /// ## Example + /// + /// ```markdown + /// > | [^a]: b + /// ^ + GfmFootnoteDefinitionMarker, + /// GFM extension: Strikethrough. /// /// ## Info /// @@ -1038,7 +1204,7 @@ pub enum Name { /// ^^^ /// ``` GfmStrikethrough, - /// Gfm: Strikethrough sequence. + /// GFM extension: Strikethrough sequence. /// /// ## Info /// @@ -1056,7 +1222,7 @@ pub enum Name { /// ^ ^ /// ``` GfmStrikethroughSequence, - /// Gfm: Strikethrough text. + /// GFM extension: Strikethrough text. /// /// ## Info /// @@ -1074,7 +1240,7 @@ pub enum Name { /// ^ /// ``` GfmStrikethroughText, - /// GFM: Task list item check. + /// GFM extension: task list item check. /// /// ## Info /// @@ -1094,7 +1260,7 @@ pub enum Name { /// ^^^ /// ``` GfmTaskListItemCheck, - /// GFM: Task list item check marker. + /// GFM extension: task list item check marker. /// /// ## Info /// @@ -1112,7 +1278,7 @@ pub enum Name { /// ^ ^ /// ``` GfmTaskListItemMarker, - /// GFM: Task list item value: checked. + /// GFM extension: task list item value: checked. /// /// ## Info /// @@ -1130,7 +1296,7 @@ pub enum Name { /// ^ /// ``` GfmTaskListItemValueChecked, - /// GFM: Task list item value: unchecked. + /// GFM extension: task list item value: unchecked. /// /// ## Info /// @@ -2105,7 +2271,7 @@ pub enum Name { } /// List of void events, used to make sure everything is working well. -pub const VOID_EVENTS: [Name; 50] = [ +pub const VOID_EVENTS: [Name; 53] = [ Name::AttentionSequence, Name::AutolinkEmail, Name::AutolinkMarker, @@ -2134,6 +2300,9 @@ pub const VOID_EVENTS: [Name; 50] = [ Name::GfmAutolinkLiteralEmail, Name::GfmAutolinkLiteralProtocol, Name::GfmAutolinkLiteralWww, + Name::GfmFootnoteCallMarker, + Name::GfmFootnoteDefinitionLabelMarker, + Name::GfmFootnoteDefinitionMarker, Name::GfmStrikethroughSequence, Name::GfmTaskListItemMarker, Name::GfmTaskListItemValueChecked, diff --git a/src/lib.rs b/src/lib.rs index 5b7836c..fd5e500 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,7 +171,20 @@ pub struct Constructs { /// ^^^^^^^^^^^^^^^^^^^ /// ``` pub gfm_autolink_literal: bool, - /// GFM: strikethrough. + /// GFM: footnote definition. + /// + /// ```markdown + /// > | [^a]: b + /// ^^^^^^^ + /// ``` + pub gfm_footnote_definition: bool, + /// GFM: footnote label start. + /// + /// ```markdown + /// > | a[^b] + /// ^^ + /// ``` + pub gfm_label_start_footnote: bool, /// /// ```markdown /// > | a ~b~ c. @@ -283,6 +296,8 @@ impl Default for Constructs { definition: true, frontmatter: false, gfm_autolink_literal: false, + gfm_label_start_footnote: false, + gfm_footnote_definition: false, gfm_strikethrough: false, gfm_task_list_item: false, hard_break_escape: true, @@ -308,6 +323,8 @@ impl Constructs { pub fn gfm() -> Self { Self { gfm_autolink_literal: true, + gfm_footnote_definition: true, + gfm_label_start_footnote: true, gfm_strikethrough: true, gfm_task_list_item: true, ..Self::default() @@ -376,6 +393,206 @@ pub struct Options { /// ``` pub allow_dangerous_protocol: bool, + /// Label to use for the footnotes section. + /// + /// Change it when the markdown is not in English. + /// Typically affects screen readers (change `gfm_footnote_label_attributes` + /// to make it visible). + /// + /// ## Examples + /// + /// ``` + /// use micromark::{micromark, micromark_with_options, Options, Constructs}; + /// + /// // `"Footnotes"` is used by default: + /// assert_eq!( + /// micromark_with_options( + /// "[^a]\n\n[^a]: b", + /// &Options { + /// constructs: Constructs::gfm(), + /// ..Options::default() + /// } + /// ), + /// "\nb ↩
\nb ↩
\nb ↩
\nb ↩
\nb ↩
\nb ↩
\nb ↩
\nb ↩
\nb ↩
\nb ↩
\na
a
~a~
" + /// "~a~", + /// &Options { + /// constructs: Constructs::gfm(), + /// gfm_strikethrough_single_tilde: false, + /// ..Options::default() + /// } + /// ), + /// "~a~
" /// ); /// ``` pub gfm_strikethrough_single_tilde: bool, @@ -488,6 +705,11 @@ impl Default for Options { Self { allow_dangerous_html: false, allow_dangerous_protocol: false, + gfm_footnote_label: None, + gfm_footnote_label_tag_name: None, + gfm_footnote_label_attributes: None, + gfm_footnote_back_label: None, + gfm_footnote_clobber_prefix: None, gfm_strikethrough_single_tilde: true, default_line_ending: LineEnding::default(), constructs: Constructs::default(), diff --git a/src/parser.rs b/src/parser.rs index afa08ac..62b3e03 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -17,8 +17,10 @@ pub struct ParseState<'a> { pub options: &'a Options, /// List of chars. pub bytes: &'a [u8], - /// Set of defined identifiers. + /// Set of defined definition identifiers. pub definitions: Vec