From 9ed4f225a73a2a8a7b7f81753e11bbb43c205f62 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Thu, 15 Aug 2024 23:08:08 +0200 Subject: [PATCH 01/25] lib: check offsets in test_parse --- src/lib.rs | 1429 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 839 insertions(+), 590 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0abbb24..28b40aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1205,7 +1205,10 @@ mod test { macro_rules! test_parse { ($src:expr $(,$($token:expr),* $(,)?)?) => { #[allow(unused)] - let actual = super::Parser::new($src).collect::>(); + let actual = super::Parser::new($src) + .into_offset_iter() + .map(|(e, r)| (e, &$src[r])) + .collect::>(); let expected = &[$($($token),*,)?]; assert_eq!( actual, @@ -1255,49 +1258,67 @@ mod test { fn heading() { test_parse!( "#\n", - Start(Section { id: "s-1".into() }, Attributes::new()), - Start( - Heading { + (Start(Section { id: "s-1".into() }, Attributes::new()), ""), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "s-1".into(), + }, + Attributes::new(), + ), + "#", + ), + ( + End(Heading { level: 1, has_section: true, - id: "s-1".into() - }, - Attributes::new() - ), - End(Heading { - level: 1, - has_section: true, - id: "s-1".into() - }), - End(Section { id: "s-1".into() }), + id: "s-1".into(), + }), + "", + ), + (End(Section { id: "s-1".into() }), ""), ); test_parse!( "# abc\ndef\n", - Start( - Section { - id: "abc-def".into() - }, - Attributes::new() + ( + Start( + Section { + id: "abc-def".into(), + }, + Attributes::new(), + ), + "", + ), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "abc-def".into(), + }, + Attributes::new(), + ), + "#", ), - Start( - Heading { + (Str("abc".into()), "abc"), + (Softbreak, "\n"), + (Str("def".into()), "def"), + ( + End(Heading { level: 1, has_section: true, - id: "abc-def".into() - }, - Attributes::new() - ), - Str("abc".into()), - Softbreak, - Str("def".into()), - End(Heading { - level: 1, - has_section: true, - id: "abc-def".into(), - }), - End(Section { - id: "abc-def".into() - }), + id: "abc-def".into(), + }), + "", + ), + ( + End(Section { + id: "abc-def".into(), + }), + "", + ), ); } @@ -1309,41 +1330,56 @@ mod test { "{a=b}\n", "# def\n", // ), - Start(Section { id: "abc".into() }, Attributes::new()), - Start( - Heading { + (Start(Section { id: "abc".into() }, Attributes::new()), ""), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "abc".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("abc".into()), "abc"), + ( + End(Heading { level: 1, has_section: true, - id: "abc".into() - }, - Attributes::new() - ), - Str("abc".into()), - End(Heading { - level: 1, - has_section: true, - id: "abc".into(), - }), - End(Section { id: "abc".into() }), - Start( - Section { id: "def".into() }, - [("a", "b")].into_iter().collect(), - ), - Start( - Heading { + id: "abc".into(), + }), + "", + ), + (End(Section { id: "abc".into() }), ""), + ( + Start( + Section { id: "def".into() }, + [("a", "b")].into_iter().collect(), + ), + "{a=b}\n", + ), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "def".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("def".into()), "def"), + ( + End(Heading { level: 1, has_section: true, - id: "def".into() - }, - Attributes::new(), - ), - Str("def".into()), - End(Heading { - level: 1, - has_section: true, - id: "def".into(), - }), - End(Section { id: "def".into() }), + id: "def".into(), + }), + "", + ), + (End(Section { id: "def".into() }), ""), ); } @@ -1355,46 +1391,64 @@ mod test { "\n", // "# Some Section", // ), - Start(Paragraph, Attributes::new()), - Str("A ".into()), - Start( - Link( + (Start(Paragraph, Attributes::new()), ""), + (Str("A ".into()), "A "), + ( + Start( + Link( + "#Some-Section".into(), + LinkType::Span(SpanLinkType::Reference), + ), + Attributes::new(), + ), + "[", + ), + (Str("link".into()), "link"), + ( + End(Link( "#Some-Section".into(), - LinkType::Span(SpanLinkType::Reference) + LinkType::Span(SpanLinkType::Reference), + )), + "][Some Section]", + ), + (Str(" to another section.".into()), " to another section."), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start( + Section { + id: "Some-Section".into(), + }, + Attributes::new(), ), - Attributes::new() - ), - Str("link".into()), - End(Link( - "#Some-Section".into(), - LinkType::Span(SpanLinkType::Reference) - )), - Str(" to another section.".into()), - End(Paragraph), - Blankline, - Start( - Section { - id: "Some-Section".into() - }, - Attributes::new() + "", + ), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "Some-Section".into(), + }, + Attributes::new(), + ), + "#", ), - Start( - Heading { + (Str("Some Section".into()), "Some Section"), + ( + End(Heading { level: 1, has_section: true, id: "Some-Section".into(), - }, - Attributes::new(), - ), - Str("Some Section".into()), - End(Heading { - level: 1, - has_section: true, - id: "Some-Section".into(), - }), - End(Section { - id: "Some-Section".into() - }), + }), + "", + ), + ( + End(Section { + id: "Some-Section".into(), + }), + "", + ), ); test_parse!( concat!( @@ -1405,55 +1459,79 @@ mod test { "\n", // "# a\n", // ), - Start(Paragraph, Attributes::new()), - Start( - Link("#a".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("a".into()), - End(Link("#a".into(), LinkType::Span(SpanLinkType::Reference))), - Softbreak, - Start( - Link("#b".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("b".into()), - End(Link("#b".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start(Section { id: "b".into() }, Attributes::new()), - Start( - Heading { + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("#a".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("a".into()), "a"), + ( + End(Link("#a".into(), LinkType::Span(SpanLinkType::Reference))), + "][]", + ), + (Softbreak, "\n"), + ( + Start( + Link("#b".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("b".into()), "b"), + ( + End(Link("#b".into(), LinkType::Span(SpanLinkType::Reference))), + "][]", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Section { id: "b".into() }, Attributes::new()), ""), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "b".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("b".into()), "b"), + ( + End(Heading { level: 1, has_section: true, id: "b".into(), - }, - Attributes::new(), - ), - Str("b".into()), - End(Heading { - level: 1, - has_section: true, - id: "b".into(), - }), - Blankline, - End(Section { id: "b".into() }), - Start(Section { id: "a".into() }, Attributes::new()), - Start( - Heading { + }), + "", + ), + (Blankline, "\n"), + (End(Section { id: "b".into() }), ""), + (Start(Section { id: "a".into() }, Attributes::new()), ""), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "a".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("a".into()), "a"), + ( + End(Heading { level: 1, has_section: true, id: "a".into(), - }, - Attributes::new(), - ), - Str("a".into()), - End(Heading { - level: 1, - has_section: true, - id: "a".into(), - }), - End(Section { id: "a".into() }), + }), + "", + ), + (End(Section { id: "a".into() }), ""), ); } @@ -1461,9 +1539,9 @@ mod test { fn blockquote() { test_parse!( ">\n", - Start(Blockquote, Attributes::new()), - Blankline, - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Blankline, "\n"), + (End(Blockquote), ""), ); } @@ -1471,25 +1549,25 @@ mod test { fn para() { test_parse!( "para", - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); test_parse!( "pa ra", - Start(Paragraph, Attributes::new()), - Str("pa ra".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("pa ra".into()), "pa ra"), + (End(Paragraph), ""), ); test_parse!( "para0\n\npara1", - Start(Paragraph, Attributes::new()), - Str("para0".into()), - End(Paragraph), - Blankline, - Start(Paragraph, Attributes::new()), - Str("para1".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("para0".into()), "para0"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Paragraph, Attributes::new()), ""), + (Str("para1".into()), "para1"), + (End(Paragraph), ""), ); } @@ -1497,25 +1575,25 @@ mod test { fn verbatim() { test_parse!( "`abc\ndef", - Start(Paragraph, Attributes::new()), - Start(Verbatim, Attributes::new()), - Str("abc\ndef".into()), - End(Verbatim), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Start(Verbatim, Attributes::new()), "`"), + (Str("abc\ndef".into()), "abc\ndef"), + (End(Verbatim), ""), + (End(Paragraph), ""), ); test_parse!( concat!( "> `abc\n", "> def\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Verbatim, Attributes::new()), - Str("abc\n".into()), - Str("def".into()), - End(Verbatim), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + (Start(Verbatim, Attributes::new()), "`"), + (Str("abc\n".into()), "abc\n"), + (Str("def".into()), "def"), + (End(Verbatim), ""), + (End(Paragraph), ""), + (End(Blockquote), ""), ); } @@ -1523,11 +1601,14 @@ mod test { fn raw_inline() { test_parse!( "``raw\nraw``{=format}", - Start(Paragraph, Attributes::new()), - Start(RawInline { format: "format" }, Attributes::new()), - Str("raw\nraw".into()), - End(RawInline { format: "format" }), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start(RawInline { format: "format" }, Attributes::new()), + "``", + ), + (Str("raw\nraw".into()), "raw\nraw"), + (End(RawInline { format: "format" }), "``{=format}"), + (End(Paragraph), ""), ); } @@ -1535,9 +1616,12 @@ mod test { fn raw_block() { test_parse!( "``` =html\n\n```", - Start(RawBlock { format: "html" }, Attributes::new()), - Str("
".into()), - End(RawBlock { format: "html" }), + ( + Start(RawBlock { format: "html" }, Attributes::new()), + "``` =html\n", + ), + (Str("
".into()), "
"), + (End(RawBlock { format: "html" }), "```"), ); } @@ -1557,19 +1641,25 @@ mod test { "\n", // "```\n", // ), - Start(RawBlock { format: "html" }, Attributes::new()), - Str("\n".into()), - Str("".into()), - End(RawBlock { format: "html" }), - Blankline, - Start(Paragraph, Attributes::new()), - Str("paragraph".into()), - End(Paragraph), - Blankline, - Start(RawBlock { format: "html" }, Attributes::new()), - Str("\n".into()), - Str("".into()), - End(RawBlock { format: "html" }), + ( + Start(RawBlock { format: "html" }, Attributes::new()), + "```=html\n", + ), + (Str("\n".into()), "\n"), + (Str("".into()), ""), + (End(RawBlock { format: "html" }), "```\n"), + (Blankline, "\n"), + (Start(Paragraph, Attributes::new()), ""), + (Str("paragraph".into()), "paragraph"), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(RawBlock { format: "html" }, Attributes::new()), + "```=html\n", + ), + (Str("\n".into()), "\n"), + (Str("".into()), ""), + (End(RawBlock { format: "html" }), "```\n"), ); } @@ -1577,11 +1667,11 @@ mod test { fn symbol() { test_parse!( "abc :+1: def", - Start(Paragraph, Attributes::new()), - Str("abc ".into()), - Symbol("+1".into()), - Str(" def".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc ".into()), "abc "), + (Symbol("+1".into()), ":+1:"), + (Str(" def".into()), " def"), + (End(Paragraph), ""), ); } @@ -1589,14 +1679,20 @@ mod test { fn link_inline() { test_parse!( "[text](url)", - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Inline)), - Attributes::new() - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Inline))), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Inline)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Inline))), + "](url)", + ), + (End(Paragraph), ""), ); } @@ -1607,16 +1703,22 @@ mod test { "> [text](url\n", "> url)\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start( - Link("urlurl".into(), LinkType::Span(SpanLinkType::Inline)), - Attributes::new() + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("urlurl".into(), LinkType::Span(SpanLinkType::Inline)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("urlurl".into(), LinkType::Span(SpanLinkType::Inline))), + "](url\n> url)", ), - Str("text".into()), - End(Link("urlurl".into(), LinkType::Span(SpanLinkType::Inline))), - End(Paragraph), - End(Blockquote), + (End(Paragraph), ""), + (End(Blockquote), ""), ); test_parse!( concat!( @@ -1624,16 +1726,22 @@ mod test { "> bc\n", // "> def)\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start( - Link("abcdef".into(), LinkType::Span(SpanLinkType::Inline)), - Attributes::new() + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("abcdef".into(), LinkType::Span(SpanLinkType::Inline)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("abcdef".into(), LinkType::Span(SpanLinkType::Inline))), + "](a\n> bc\n> def)", ), - Str("text".into()), - End(Link("abcdef".into(), LinkType::Span(SpanLinkType::Inline))), - End(Paragraph), - End(Blockquote), + (End(Paragraph), ""), + (End(Blockquote), ""), ); } @@ -1645,18 +1753,27 @@ mod test { "\n", "[tag]: url\n" // ), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start(LinkDefinition { label: "tag" }, Attributes::new()), - Str("url".into()), - End(LinkDefinition { label: "tag" }), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][tag]", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "tag" }, Attributes::new()), + "[tag]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "tag" }), ""), ); test_parse!( concat!( @@ -1664,18 +1781,24 @@ mod test { "\n", "[tag]: url\n" // ), - Start(Paragraph, Attributes::new()), - Start( - Image("url".into(), SpanLinkType::Reference), - Attributes::new() - ), - Str("text".into()), - End(Image("url".into(), SpanLinkType::Reference)), - End(Paragraph), - Blankline, - Start(LinkDefinition { label: "tag" }, Attributes::new()), - Str("url".into()), - End(LinkDefinition { label: "tag" }), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Image("url".into(), SpanLinkType::Reference), + Attributes::new(), + ), + "![", + ), + (Str("text".into()), "text"), + (End(Image("url".into(), SpanLinkType::Reference)), "][tag]"), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "tag" }, Attributes::new()), + "[tag]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "tag" }), ""), ); } @@ -1683,25 +1806,34 @@ mod test { fn link_reference_unresolved() { test_parse!( "[text][tag]", - Start(Paragraph, Attributes::new()), - Start( - Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved)), - Attributes::new() - ), - Str("text".into()), - End(Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved))), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved))), + "][tag]", + ), + (End(Paragraph), ""), ); test_parse!( "![text][tag]", - Start(Paragraph, Attributes::new()), - Start( - Image("tag".into(), SpanLinkType::Unresolved), - Attributes::new() - ), - Str("text".into()), - End(Image("tag".into(), SpanLinkType::Unresolved)), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Image("tag".into(), SpanLinkType::Unresolved), + Attributes::new(), + ), + "![", + ), + (Str("text".into()), "text"), + (End(Image("tag".into(), SpanLinkType::Unresolved)), "][tag]"), + (End(Paragraph), ""), ); } @@ -1714,20 +1846,29 @@ mod test { "\n", // "[a b]: url\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - End(Blockquote), - Blankline, - Start(LinkDefinition { label: "a b" }, Attributes::new()), - Str("url".into()), - End(LinkDefinition { label: "a b" }), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][a\n> b]", + ), + (End(Paragraph), ""), + (End(Blockquote), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "a b" }, Attributes::new()), + "[a b]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "a b" }), ""), ); } @@ -1742,32 +1883,47 @@ mod test { "\n", // "[a b]: url\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("a".into()), - Softbreak, - Str("b".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - Softbreak, - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("a".into()), - Escape, - Hardbreak, - Str("b".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - End(Blockquote), - Blankline, - Start(LinkDefinition { label: "a b" }, Attributes::new()), - Str("url".into()), - End(LinkDefinition { label: "a b" }), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("a".into()), "a"), + (Softbreak, "\n"), + (Str("b".into()), "b"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][]", + ), + (Softbreak, "\n"), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("a".into()), "a"), + (Escape, "\\"), + (Hardbreak, "\n"), + (Str("b".into()), "b"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][]", + ), + (End(Paragraph), ""), + (End(Blockquote), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "a b" }, Attributes::new()), + "[a b]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "a b" }), ""), ); } @@ -1780,19 +1936,28 @@ mod test { "[tag]: u\n", " rl\n", // ), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start(LinkDefinition { label: "tag" }, Attributes::new()), - Str("u".into()), - Str("rl".into()), - End(LinkDefinition { label: "tag" }), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][tag]", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "tag" }, Attributes::new()), + "[tag]:", + ), + (Str("u".into()), "u"), + (Str("rl".into()), "rl"), + (End(LinkDefinition { label: "tag" }), ""), ); test_parse!( concat!( @@ -1802,22 +1967,31 @@ mod test { " url\n", // " cont\n", // ), - Start(Paragraph, Attributes::new()), - Start( - Link("urlcont".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("text".into()), - End(Link( - "urlcont".into(), - LinkType::Span(SpanLinkType::Reference) - )), - End(Paragraph), - Blankline, - Start(LinkDefinition { label: "tag" }, Attributes::new()), - Str("url".into()), - Str("cont".into()), - End(LinkDefinition { label: "tag" }), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("urlcont".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link( + "urlcont".into(), + LinkType::Span(SpanLinkType::Reference), + )), + "][tag]", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "tag" }, Attributes::new()), + "[tag]:", + ), + (Str("url".into()), "url"), + (Str("cont".into()), "cont"), + (End(LinkDefinition { label: "tag" }), ""), ); } @@ -1831,24 +2005,33 @@ mod test { "[tag]: url\n", "para\n", ), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - [("b", "c"), ("a", "b")].into_iter().collect(), - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start( - LinkDefinition { label: "tag" }, - [("a", "b")].into_iter().collect() - ), - Str("url".into()), - End(LinkDefinition { label: "tag" }), - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + [("b", "c"), ("a", "b")].into_iter().collect(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][tag]{b=c}", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start( + LinkDefinition { label: "tag" }, + [("a", "b")].into_iter().collect(), + ), + "{a=b}\n[tag]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "tag" }), ""), + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); } @@ -1862,24 +2045,33 @@ mod test { "[tag]: url\n", "para\n", ), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - [("class", "link"), ("class", "def")].into_iter().collect(), - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start( - LinkDefinition { label: "tag" }, - [("class", "def")].into_iter().collect() - ), - Str("url".into()), - End(LinkDefinition { label: "tag" }), - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + [("class", "link"), ("class", "def")].into_iter().collect(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][tag]{.link}", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start( + LinkDefinition { label: "tag" }, + [("class", "def")].into_iter().collect(), + ), + "{.def}\n[tag]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "tag" }), ""), + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); } @@ -1887,14 +2079,17 @@ mod test { fn autolink() { test_parse!( "\n", - Start(Paragraph, Attributes::new()), - Start( - Link("proto:url".into(), LinkType::AutoLink), - Attributes::new() - ), - Str("proto:url".into()), - End(Link("proto:url".into(), LinkType::AutoLink)), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("proto:url".into(), LinkType::AutoLink), + Attributes::new(), + ), + "<", + ), + (Str("proto:url".into()), "proto:url"), + (End(Link("proto:url".into(), LinkType::AutoLink)), ">"), + (End(Paragraph), ""), ); } @@ -1902,14 +2097,17 @@ mod test { fn email() { test_parse!( "\n", - Start(Paragraph, Attributes::new()), - Start( - Link("name@domain".into(), LinkType::Email), - Attributes::new() - ), - Str("name@domain".into()), - End(Link("name@domain".into(), LinkType::Email)), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("name@domain".into(), LinkType::Email), + Attributes::new(), + ), + "<", + ), + (Str("name@domain".into()), "name@domain"), + (End(Link("name@domain".into(), LinkType::Email)), ">"), + (End(Paragraph), ""), ); } @@ -1917,11 +2115,11 @@ mod test { fn footnote_references() { test_parse!( "[^a][^b][^c]", - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - FootnoteReference("b"), - FootnoteReference("c"), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (FootnoteReference("b"), "[^b]"), + (FootnoteReference("c"), "[^c]"), + (End(Paragraph), ""), ); } @@ -1929,15 +2127,15 @@ mod test { fn footnote() { test_parse!( "[^a]\n\n[^a]: a\n", - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - End(Paragraph), - Blankline, - Start(Footnote { label: "a" }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("a".into()), - End(Paragraph), - End(Footnote { label: "a" }), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Footnote { label: "a" }, Attributes::new()), "[^a]:"), + (Start(Paragraph, Attributes::new()), ""), + (Str("a".into()), "a"), + (End(Paragraph), ""), + (End(Footnote { label: "a" }), ""), ); } @@ -1951,19 +2149,19 @@ mod test { "\n", " def", // ), - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - End(Paragraph), - Blankline, - Start(Footnote { label: "a" }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("abc".into()), - End(Paragraph), - Blankline, - Start(Paragraph, Attributes::new()), - Str("def".into()), - End(Paragraph), - End(Footnote { label: "a" }), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Footnote { label: "a" }, Attributes::new()), "[^a]:"), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc".into()), "abc"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Paragraph, Attributes::new()), ""), + (Str("def".into()), "def"), + (End(Paragraph), ""), + (End(Footnote { label: "a" }), ""), ); } @@ -1978,21 +2176,21 @@ mod test { "\n", "para\n", // ), - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - End(Paragraph), - Blankline, - Start(Footnote { label: "a" }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("note".into()), - Softbreak, - Str("cont".into()), - End(Paragraph), - Blankline, - End(Footnote { label: "a" }), - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Footnote { label: "a" }, Attributes::new()), "[^a]:"), + (Start(Paragraph, Attributes::new()), ""), + (Str("note".into()), "note"), + (Softbreak, "\n"), + (Str("cont".into()), "cont"), + (End(Paragraph), ""), + (Blankline, "\n"), + (End(Footnote { label: "a" }), ""), + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); test_parse!( concat!( @@ -2001,17 +2199,17 @@ mod test { "[^a]: note\n", // ":::\n", // ), - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - End(Paragraph), - Blankline, - Start(Footnote { label: "a" }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("note".into()), - End(Paragraph), - End(Footnote { label: "a" }), - Start(Div { class: "" }, Attributes::new()), - End(Div { class: "" }), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Footnote { label: "a" }, Attributes::new()), "[^a]:"), + (Start(Paragraph, Attributes::new()), ""), + (Str("note".into()), "note"), + (End(Paragraph), ""), + (End(Footnote { label: "a" }), ""), + (Start(Div { class: "" }, Attributes::new()), ":::\n"), + (End(Div { class: "" }), ""), ); } @@ -2019,9 +2217,12 @@ mod test { fn attr_block() { test_parse!( "{.some_class}\npara\n", - Start(Paragraph, [("class", "some_class")].into_iter().collect()), - Str("para".into()), - End(Paragraph), + ( + Start(Paragraph, [("class", "some_class")].into_iter().collect()), + "{.some_class}\n", + ), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); } @@ -2029,12 +2230,15 @@ mod test { fn attr_inline() { test_parse!( "abc _def_{.ghi}", - Start(Paragraph, Attributes::new()), - Str("abc ".into()), - Start(Emphasis, [("class", "ghi")].into_iter().collect()), - Str("def".into()), - End(Emphasis), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc ".into()), "abc "), + ( + Start(Emphasis, [("class", "ghi")].into_iter().collect()), + "_", + ), + (Str("def".into()), "def"), + (End(Emphasis), "_{.ghi}"), + (End(Paragraph), ""), ); } @@ -2042,25 +2246,31 @@ mod test { fn attr_inline_consecutive() { test_parse!( "_abc def_{.a}{.b #i}", - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [("class", "a b"), ("id", "i")].into_iter().collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{.b #i}"), + (End(Paragraph), ""), ); test_parse!( "_abc def_{.a}{%%}{.b #i}", - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [("class", "a b"), ("id", "i")].into_iter().collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{%%}{.b #i}"), + (End(Paragraph), ""), ); } @@ -2068,41 +2278,50 @@ mod test { fn attr_inline_consecutive_invalid() { test_parse!( "_abc def_{.a}{.b #i}{.c invalid}", - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - Str("{.c invalid}".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [("class", "a b"), ("id", "i")].into_iter().collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{.b #i}"), + (Str("{.c invalid}".into()), "{.c invalid}"), + (End(Paragraph), ""), ); test_parse!( "_abc def_{.a}{.b #i}{%%}{.c invalid}", - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - Str("{.c invalid}".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [("class", "a b"), ("id", "i")].into_iter().collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{.b #i}{%%}"), + (Str("{.c invalid}".into()), "{.c invalid}"), + (End(Paragraph), ""), ); test_parse!( concat!("_abc def_{.a}{.b #i}{%%}{.c\n", "invalid}\n"), - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - Str("{.c".into()), - Softbreak, - Str("invalid}".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [("class", "a b"), ("id", "i")].into_iter().collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{.b #i}{%%}"), + (Str("{.c".into()), "{.c"), + (Softbreak, "\n"), + (Str("invalid}".into()), "invalid}"), + (End(Paragraph), ""), ); } @@ -2113,13 +2332,16 @@ mod test { "> _abc_{a=b\n", // "> c=d}\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Emphasis, [("a", "b"), ("c", "d")].into_iter().collect()), - Str("abc".into()), - End(Emphasis), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start(Emphasis, [("a", "b"), ("c", "d")].into_iter().collect()), + "_", + ), + (Str("abc".into()), "abc"), + (End(Emphasis), "_{a=b\n> c=d}"), + (End(Paragraph), ""), + (End(Blockquote), ""), ); test_parse!( concat!( @@ -2127,13 +2349,13 @@ mod test { "> %%\n", // "> a=a}\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Span, [("a", "a")].into_iter().collect()), - Str("a".into()), - End(Span), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + (Start(Span, [("a", "a")].into_iter().collect()), ""), + (Str("a".into()), "a"), + (End(Span), "{\n> %%\n> a=a}"), + (End(Paragraph), ""), + (End(Blockquote), ""), ); test_parse!( concat!( @@ -2141,26 +2363,26 @@ mod test { "> b\n", // "> c\"}\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Span, [("a", "a b c")].into_iter().collect()), - Str("a".into()), - End(Span), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + (Start(Span, [("a", "a b c")].into_iter().collect()), ""), + (Str("a".into()), "a"), + (End(Span), "{a=\"a\n> b\n> c\"}"), + (End(Paragraph), ""), + (End(Blockquote), ""), ); test_parse!( concat!( "> a{a=\"\n", // "> b\"}\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Span, [("a", "b")].into_iter().collect()), - Str("a".into()), - End(Span), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + (Start(Span, [("a", "b")].into_iter().collect()), ""), + (Str("a".into()), "a"), + (End(Span), "{a=\"\n> b\"}"), + (End(Paragraph), ""), + (End(Blockquote), ""), ); } @@ -2171,11 +2393,11 @@ mod test { "a{\n", // " b\n", // ), - Start(Paragraph, Attributes::new()), - Str("a{".into()), - Softbreak, - Str("b".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("a{".into()), "a{"), + (Softbreak, "\n"), + (Str("b".into()), "b"), + (End(Paragraph), ""), ); } @@ -2187,13 +2409,13 @@ mod test { " b\n", // "}", // ), - Start(Paragraph, Attributes::new()), - Str("a{a=b".into()), - Softbreak, - Str("b".into()), - Softbreak, - Str("}".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("a{a=b".into()), "a{a=b"), + (Softbreak, "\n"), + (Str("b".into()), "b"), + (Softbreak, "\n"), + (Str("}".into()), "}"), + (End(Paragraph), ""), ); } @@ -2201,22 +2423,28 @@ mod test { fn list_item_unordered() { test_parse!( "- abc", - Start( - List { + ( + Start( + List { + kind: ListKind::Unordered, + tight: true, + }, + Attributes::new(), + ), + "", + ), + (Start(ListItem, Attributes::new()), "-"), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc".into()), "abc"), + (End(Paragraph), ""), + (End(ListItem), ""), + ( + End(List { kind: ListKind::Unordered, tight: true, - }, - Attributes::new(), - ), - Start(ListItem, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("abc".into()), - End(Paragraph), - End(ListItem), - End(List { - kind: ListKind::Unordered, - tight: true, - }), + }), + "", + ), ); } @@ -2224,30 +2452,36 @@ mod test { fn list_item_ordered_decimal() { test_parse!( "123. abc", - Start( - List { + ( + Start( + List { + kind: ListKind::Ordered { + numbering: Decimal, + style: Period, + start: 123, + }, + tight: true, + }, + Attributes::new(), + ), + "", + ), + (Start(ListItem, Attributes::new()), "123."), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc".into()), "abc"), + (End(Paragraph), ""), + (End(ListItem), ""), + ( + End(List { kind: ListKind::Ordered { numbering: Decimal, style: Period, - start: 123 + start: 123, }, tight: true, - }, - Attributes::new(), - ), - Start(ListItem, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("abc".into()), - End(Paragraph), - End(ListItem), - End(List { - kind: ListKind::Ordered { - numbering: Decimal, - style: Period, - start: 123 - }, - tight: true, - }), + }), + "", + ), ); } @@ -2259,32 +2493,47 @@ mod test { "- [x] b\n", // "- [X] c\n", // ), - Start( - List { + ( + Start( + List { + kind: ListKind::Task, + tight: true, + }, + Attributes::new(), + ), + "", + ), + ( + Start(TaskListItem { checked: false }, Attributes::new()), + "- [ ]", + ), + (Start(Paragraph, Attributes::new()), ""), + (Str("a".into()), "a"), + (End(Paragraph), ""), + (End(TaskListItem { checked: false }), ""), + ( + Start(TaskListItem { checked: true }, Attributes::new()), + "- [x]", + ), + (Start(Paragraph, Attributes::new()), ""), + (Str("b".into()), "b"), + (End(Paragraph), ""), + (End(TaskListItem { checked: true }), ""), + ( + Start(TaskListItem { checked: true }, Attributes::new()), + "- [X]", + ), + (Start(Paragraph, Attributes::new()), ""), + (Str("c".into()), "c"), + (End(Paragraph), ""), + (End(TaskListItem { checked: true }), ""), + ( + End(List { kind: ListKind::Task, tight: true, - }, - Attributes::new(), - ), - Start(TaskListItem { checked: false }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("a".into()), - End(Paragraph), - End(TaskListItem { checked: false }), - Start(TaskListItem { checked: true }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("b".into()), - End(Paragraph), - End(TaskListItem { checked: true }), - Start(TaskListItem { checked: true }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("c".into()), - End(Paragraph), - End(TaskListItem { checked: true }), - End(List { - kind: ListKind::Task, - tight: true, - }), + }), + "", + ), ); } From 505fe1a9dbd5d68b51df0caabfa0788ce187ab2a Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Tue, 20 Aug 2024 23:05:40 +0200 Subject: [PATCH 02/25] lib: add test for multiple block attrs in order to verify offsets --- src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 28b40aa..53f6477 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2224,6 +2224,22 @@ mod test { (Str("para".into()), "para"), (End(Paragraph), ""), ); + test_parse!( + concat!( + "{.a}\n", + "{#b}\n", + "para\n", // + ), + ( + Start( + Paragraph, + [("class", "a"), ("id", "b")].into_iter().collect(), + ), + "{.a}\n{#b}\n", + ), + (Str("para".into()), "para"), + (End(Paragraph), ""), + ); } #[test] From 671ac4cd2380098747e0e8591f754ea43e6b36ef Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 17 Aug 2024 15:12:50 +0200 Subject: [PATCH 03/25] attr: impl TryFrom<&'s str> for Attributes allow parsing to be used outside crate, and for doc examples --- src/attr.rs | 80 ++++++++++++++++++++++++++++++++++++++++----------- src/inline.rs | 4 ++- src/lib.rs | 16 +++++++---- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/attr.rs b/src/attr.rs index c826ea6..5813569 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -1,13 +1,6 @@ use crate::CowStr; use std::fmt; -/// Parse attributes, assumed to be valid. -pub(crate) fn parse(src: &str) -> Attributes { - let mut a = Attributes::new(); - a.parse(src); - a -} - pub fn valid(src: &str) -> usize { use State::*; @@ -133,11 +126,12 @@ impl<'s> Attributes<'s> { Self(self.0.take()) } - /// Parse and append attributes, assumed to be valid. - pub(crate) fn parse(&mut self, input: &'s str) { + /// Parse and append attributes. + pub(crate) fn parse(&mut self, input: &'s str) -> Result<(), usize> { let mut parser = Parser::new(self.take()); - parser.parse(input); + parser.parse(input)?; *self = parser.finish(); + Ok(()) } /// Combine all attributes from both objects, prioritizing self on conflicts. @@ -222,6 +216,55 @@ impl<'s> Attributes<'s> { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ParseAttributesError { + /// Location in input string where attributes became invalid. + pub pos: usize, +} + +impl<'s> TryFrom<&'s str> for Attributes<'s> { + type Error = ParseAttributesError; + + /// Parse attributes represented in the djot syntax. + /// + /// Note: The [`Attributes`] borrows from the provided [`&str`], it is therefore not compatible + /// with the existing [`std::str::FromStr`] trait. + /// + /// # Examples + /// + /// A single set of attributes can be parsed: + /// + /// ``` + /// # use jotdown::Attributes; + /// let mut a = Attributes::try_from("{.a}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some(("class", "a".into()))); + /// assert_eq!(a.next(), None); + /// ``` + /// + /// Multiple sets can be parsed if they immediately follow the each other: + /// + /// ``` + /// # use jotdown::Attributes; + /// let mut a = Attributes::try_from("{.a}{.b}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some(("class", "a b".into()))); + /// assert_eq!(a.next(), None); + /// ``` + /// + /// When the attributes are invalid, the position where the parsing failed is returned: + /// + /// ``` + /// # use jotdown::*; + /// assert_eq!(Attributes::try_from("{.a $}"), Err(ParseAttributesError { pos: 4 })); + /// ``` + fn try_from(s: &'s str) -> Result { + let mut a = Attributes::new(); + match a.parse(s) { + Ok(()) => Ok(a), + Err(pos) => Err(ParseAttributesError { pos }), + } + } +} + #[cfg(test)] impl<'s> FromIterator<(&'s str, &'s str)> for Attributes<'s> { fn from_iter>(iter: I) -> Self { @@ -364,9 +407,6 @@ impl Validator { /// Attributes parser, take input of one or more consecutive attributes and create an `Attributes` /// object. -/// -/// Input is assumed to contain a valid series of attribute sets, the attributes are added as they -/// are encountered. pub struct Parser<'s> { attrs: Attributes<'s>, i_prev: usize, @@ -384,12 +424,17 @@ impl<'s> Parser<'s> { /// Return value indicates the number of bytes parsed if finished. If None, more input is /// required to finish the attributes. - pub fn parse(&mut self, input: &'s str) { + pub fn parse(&mut self, input: &'s str) -> Result<(), usize> { use State::*; let mut pos_prev = 0; for (pos, c) in input.bytes().enumerate() { let state_next = self.state.step(c); + + if matches!(state_next, Invalid) { + return Err(pos); + } + let st = std::mem::replace(&mut self.state, state_next); if st != self.state && !matches!((st, self.state), (ValueEscape, _) | (_, ValueEscape)) @@ -415,10 +460,12 @@ impl<'s> Parser<'s> { if input[pos + 1..].starts_with('{') { self.state = Start; } else { - return; + return Ok(()); } } } + + Ok(()) } pub fn finish(self) -> Attributes<'s> { @@ -500,8 +547,7 @@ mod test { macro_rules! test_attr { ($src:expr $(,$($av:expr),* $(,)?)?) => { #[allow(unused)] - let mut attr = Attributes::new(); - attr.parse($src); + let mut attr = Attributes::try_from($src).unwrap(); let actual = attr.iter().collect::>(); let expected = &[$($($av),*,)?]; for i in 0..actual.len() { diff --git a/src/inline.rs b/src/inline.rs index 0327070..a41a9f9 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -516,7 +516,9 @@ impl<'s> Parser<'s> { .chain(self.input.ahead.iter().take(state.valid_lines).cloned()) { let line = line.start..usize::min(state.end_attr, line.end); - parser.parse(&self.input.src[line]); + parser + .parse(&self.input.src[line]) + .expect("should be valid"); } parser.finish() }; diff --git a/src/lib.rs b/src/lib.rs index 53f6477..720fc87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ mod lex; pub use attr::{ AttributeValue, AttributeValueParts, Attributes, AttributesIntoIter, AttributesIter, - AttributesIterMut, + AttributesIterMut, ParseAttributesError, }; type CowStr<'s> = std::borrow::Cow<'s, str>; @@ -619,9 +619,9 @@ impl<'s> PrePass<'s> { })) => { // All link definition tags have to be obtained initially, as references can // appear before the definition. - let attrs = attr_prev - .as_ref() - .map_or_else(Attributes::new, |sp| attr::parse(&src[sp.clone()])); + let attrs = attr_prev.as_ref().map_or_else(Attributes::new, |sp| { + src[sp.clone()].try_into().expect("should be valid") + }); let url = if let Some(block::Event { kind: block::EventKind::Inline, span, @@ -663,7 +663,9 @@ impl<'s> PrePass<'s> { // as formatting must be removed. // // We choose to parse all headers twice instead of caching them. - let attrs = attr_prev.as_ref().map(|sp| attr::parse(&src[sp.clone()])); + let attrs = attr_prev + .as_ref() + .map(|sp| Attributes::try_from(&src[sp.clone()]).expect("should be valid")); let id_override = attrs .as_ref() .and_then(|attrs| attrs.get("id")) @@ -1037,7 +1039,9 @@ impl<'s> Parser<'s> { if self.block_attributes_pos.is_none() { self.block_attributes_pos = Some(ev.span.start); } - self.block_attributes.parse(&self.src[ev.span.clone()]); + self.block_attributes + .parse(&self.src[ev.span.clone()]) + .expect("should be valid"); continue; } }, From c6e03647cda31410857909156d3bbdbb24953db0 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 18 Aug 2024 11:25:49 +0200 Subject: [PATCH 04/25] attr: explicitly make valid pub(crate) --- src/attr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr.rs b/src/attr.rs index 5813569..bdd54ee 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -1,7 +1,7 @@ use crate::CowStr; use std::fmt; -pub fn valid(src: &str) -> usize { +pub(crate) fn valid(src: &str) -> usize { use State::*; let mut n = 0; From 0f5f7684065fe44be4c8d86c2e62c1f98648c93e Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 12 Aug 2024 21:20:10 +0200 Subject: [PATCH 05/25] test: add attr test --- tests/html-ut/ut/attributes.test | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/html-ut/ut/attributes.test diff --git a/tests/html-ut/ut/attributes.test b/tests/html-ut/ut/attributes.test new file mode 100644 index 0000000..168bfb4 --- /dev/null +++ b/tests/html-ut/ut/attributes.test @@ -0,0 +1,7 @@ +Classes should be concatenated + +``` +word{.a #a class=b #b .c} +. +

word

+``` From c6f7c436da1313a54080fab0616c78d18d7f532e Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 11 Aug 2024 11:41:01 +0200 Subject: [PATCH 06/25] attr: add tests for Attributes::get --- src/attr.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/attr.rs b/src/attr.rs index bdd54ee..7b751db 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -694,6 +694,59 @@ mod test { assert_eq!(super::valid("{.abc.}"), 0); } + #[test] + fn get_value_named() { + assert_eq!( + Attributes::try_from("{x=a}").unwrap().get("x"), + Some(&"a".into()), + ); + assert_eq!( + Attributes::try_from("{x=a x=b}").unwrap().get("x"), + Some(&"b".into()), + ); + } + + #[test] + fn get_value_id() { + assert_eq!( + Attributes::try_from("{#a}").unwrap().get("id"), + Some(&"a".into()), + ); + assert_eq!( + Attributes::try_from("{#a #b}").unwrap().get("id"), + Some(&"b".into()), + ); + assert_eq!( + Attributes::try_from("{#a id=b}").unwrap().get("id"), + Some(&"b".into()), + ); + assert_eq!( + Attributes::try_from("{id=a #b}").unwrap().get("id"), + Some(&"b".into()), + ); + } + + #[test] + fn get_value_class() { + assert_eq!( + Attributes::try_from("{.a #a .b #b .c}") + .unwrap() + .get("class"), + Some(&"a b c".into()), + ); + assert_eq!(Attributes::try_from("{#a}").unwrap().get("class"), None,); + assert_eq!( + Attributes::try_from("{.a}").unwrap().get("class"), + Some(&"a".into()), + ); + assert_eq!( + Attributes::try_from("{.a #a class=b #b .c}") + .unwrap() + .get("class"), + Some(&"a b c".into()), // bug: extra space? + ); + } + fn make_attrs<'a>(v: Vec<(&'a str, &'a str)>) -> Attributes<'a> { v.into_iter().collect() } From e7156ae62afdfed5f95f5164d3684196eb3a6fad Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 12 Aug 2024 21:31:15 +0200 Subject: [PATCH 07/25] attr: fix extra spaces in concatenated classes --- src/attr.rs | 13 ++++++++++--- tests/html-ut/ut/attributes.test | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/attr.rs b/src/attr.rs index 7b751db..f03b12c 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -38,6 +38,9 @@ impl<'s> AttributeValue<'s> { // lifetime is 's to avoid allocation if empty value is concatenated with single value fn extend(&mut self, s: &'s str) { + if s.is_empty() { + return; + } match &mut self.raw { CowStr::Borrowed(prev) => { if prev.is_empty() { @@ -47,8 +50,12 @@ impl<'s> AttributeValue<'s> { } } CowStr::Owned(ref mut prev) => { - prev.push(' '); - prev.push_str(s); + if prev.is_empty() { + self.raw = s.into(); + } else { + prev.push(' '); + prev.push_str(s); + } } } } @@ -743,7 +750,7 @@ mod test { Attributes::try_from("{.a #a class=b #b .c}") .unwrap() .get("class"), - Some(&"a b c".into()), // bug: extra space? + Some(&"a b c".into()), ); } diff --git a/tests/html-ut/ut/attributes.test b/tests/html-ut/ut/attributes.test index 168bfb4..8dc1033 100644 --- a/tests/html-ut/ut/attributes.test +++ b/tests/html-ut/ut/attributes.test @@ -3,5 +3,5 @@ Classes should be concatenated ``` word{.a #a class=b #b .c} . -

word

+

word

``` From 124f677902be9c7cf31d088edfd2d5a041af72dc Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 20:43:29 +0200 Subject: [PATCH 08/25] test: add newline after input if none --- src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 720fc87..4ae4665 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1220,12 +1220,17 @@ mod test { concat!( "\n", "\x1b[0;1m====================== INPUT =========================\x1b[0m\n", - "\x1b[2m{}", + "\x1b[2m{}{}", "\x1b[0;1m================ ACTUAL vs EXPECTED ==================\x1b[0m\n", "{}", "\x1b[0;1m======================================================\x1b[0m\n", ), $src, + if $src.ends_with('\n') { + "" + } else { + "\n" + }, { let a = actual.iter().map(|n| format!("{:?}", n)).collect::>(); let b = expected.iter().map(|n| format!("{:?}", n)).collect::>(); From ff8273e3a9876d1f3b8998b424d7ad049927a605 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 12 Aug 2024 21:57:04 +0200 Subject: [PATCH 09/25] attr: add Debug fmt example --- src/attr.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/attr.rs b/src/attr.rs index f03b12c..83291b5 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -288,6 +288,16 @@ impl<'s> FromIterator<(&'s str, &'s str)> for Attributes<'s> { } impl<'s> std::fmt::Debug for Attributes<'s> { + /// Formats the attributes using the given formatter. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let a = r#"{#a .b id=c class=d key="val" %comment%}"#; + /// let b = r#"{id="c", class="b d", key="val"}"#; + /// assert_eq!(format!("{:?}", Attributes::try_from(a).unwrap()), b); + /// ``` fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{{")?; let mut first = true; From ed4ee255cdcf062bdadc77effcfb6a896979d2dd Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 23:29:55 +0200 Subject: [PATCH 10/25] inline: do not emit attrs on quotes --- src/inline.rs | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/inline.rs b/src/inline.rs index a41a9f9..c406681 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -819,13 +819,16 @@ impl<'s> Parser<'s> { if self.input.peek().map_or(false, |t| { matches!(t.kind, lex::Kind::Open(Delimiter::Brace)) }) { - self.ahead_attributes( + let elem_ty = if matches!(opener, Opener::DoubleQuoted | Opener::SingleQuoted) { + // quote delimiters will turn into atoms instead of containers, so cannot + // place attributes on the container start + AttributesElementType::Word + } else { AttributesElementType::Container { e_placeholder: e_attr, - }, - false, - ) - .or(Some(Continue)) + } + }; + self.ahead_attributes(elem_ty, false).or(Some(Continue)) } else { closed } @@ -1862,4 +1865,26 @@ mod test { (Str, " "), ); } + + #[test] + fn quote_attr() { + test_parse!( + "'a'{.b}", + ( + Atom(Quote { + ty: QuoteType::Single, + left: true + }), + "'" + ), + (Str, "a"), + ( + Atom(Quote { + ty: QuoteType::Single, + left: false + }), + "'" + ) + ); + } } From 1daa5309860e3d932317c458331e442a52eb31b1 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 20:33:43 +0200 Subject: [PATCH 11/25] lib: assert attrs handled --- src/lib.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4ae4665..32bf715 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -933,7 +933,7 @@ impl<'s> Parser<'s> { inline => (Some(inline), Attributes::new()), }; - inline.map(|inline| { + let event = inline.map(|inline| { let enter = matches!(inline.kind, inline::EventKind::Enter(_)); let event = match inline.kind { inline::EventKind::Enter(c) | inline::EventKind::Exit(c) => { @@ -968,7 +968,9 @@ impl<'s> Parser<'s> { .cloned(); let (url_or_tag, ty) = if let Some((url, attrs_def)) = link_def { - attributes.union(attrs_def); + if enter { + attributes.union(attrs_def); + } (url, SpanLinkType::Reference) } else { self.pre_pass.heading_id_by_tag(tag.as_ref()).map_or_else( @@ -993,7 +995,7 @@ impl<'s> Parser<'s> { } }; if enter { - Event::Start(t, attributes) + Event::Start(t, attributes.take()) } else { Event::End(t) } @@ -1021,7 +1023,15 @@ impl<'s> Parser<'s> { } }; (event, inline.span) - }) + }); + + debug_assert!( + attributes.is_empty(), + "unhandled attributes: {:?}", + attributes + ); + + event } fn block(&mut self) -> Option<(Event<'s>, Range)> { From 218caf4cacdae198cc977c6f296088a732938e5b Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 16:21:52 +0200 Subject: [PATCH 12/25] attr: add AttributeValue::{new,default} --- src/attr.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/attr.rs b/src/attr.rs index 83291b5..ac3467b 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -24,12 +24,18 @@ pub(crate) fn valid(src: &str) -> usize { /// Stores an attribute value that supports backslash escapes of ASCII punctuation upon displaying, /// without allocating. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct AttributeValue<'s> { raw: CowStr<'s>, } impl<'s> AttributeValue<'s> { + /// Create an empty attribute value. + #[must_use] + pub fn new() -> Self { + Self::default() + } + /// Processes the attribute value escapes and returns an iterator of the parts of the value /// that should be displayed. pub fn parts(&'s self) -> AttributeValueParts<'s> { From 011635eb8f56faac5dff87bfcf9c81d5e7aa7ada Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Wed, 7 Aug 2024 19:01:38 +0200 Subject: [PATCH 13/25] attr: simplify Option -> Vec Option saves memory but does not seem to have any noticable performance benefit. Always having a vec will allow us to impl Deref Event size went from 40 to 56 bytes --- src/attr.rs | 52 ++++++++++++++++------------------------------------ 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/src/attr.rs b/src/attr.rs index ac3467b..d72037c 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -121,11 +121,8 @@ impl<'s> Iterator for AttributeValueParts<'s> { } /// A collection of attributes, i.e. a key-value map. -// Attributes are relatively rare, we choose to pay 8 bytes always and sometimes an extra -// indirection instead of always 24 bytes. -#[allow(clippy::box_collection)] #[derive(Clone, PartialEq, Eq, Default)] -pub struct Attributes<'s>(Option)>>>); +pub struct Attributes<'s>(Vec<(&'s str, AttributeValue<'s>)>); impl<'s> Attributes<'s> { /// Create an empty collection. @@ -136,7 +133,7 @@ impl<'s> Attributes<'s> { #[must_use] pub(crate) fn take(&mut self) -> Self { - Self(self.0.take()) + std::mem::take(self) } /// Parse and append attributes. @@ -149,16 +146,10 @@ impl<'s> Attributes<'s> { /// Combine all attributes from both objects, prioritizing self on conflicts. pub(crate) fn union(&mut self, other: Self) { - if let Some(attrs0) = &mut self.0 { - if let Some(mut attrs1) = other.0 { - for (key, val) in attrs1.drain(..) { - if key == "class" || !attrs0.iter().any(|(k, _)| *k == key) { - attrs0.push((key, val)); - } - } + for (key, val) in other.0 { + if key == "class" || !self.0.iter().any(|(k, _)| *k == key) { + self.0.push((key, val)); } - } else { - self.0 = other.0; } } @@ -171,13 +162,8 @@ impl<'s> Attributes<'s> { // duplicate of insert but returns position of inserted value fn insert_pos(&mut self, key: &'s str, val: AttributeValue<'s>) -> usize { - if self.0.is_none() { - self.0 = Some(Vec::new().into()); - }; - - let attrs = self.0.as_mut().unwrap(); - if let Some(i) = attrs.iter().position(|(k, _)| *k == key) { - let prev = &mut attrs[i].1; + if let Some(i) = self.0.iter().position(|(k, _)| *k == key) { + let prev = &mut self.0[i].1; if key == "class" { match val.raw { CowStr::Borrowed(s) => prev.extend(s), @@ -190,8 +176,8 @@ impl<'s> Attributes<'s> { } i } else { - let i = attrs.len(); - attrs.push((key, val)); + let i = self.0.len(); + self.0.push((key, val)); i } } @@ -199,12 +185,12 @@ impl<'s> Attributes<'s> { /// Returns true if the collection contains no attributes. #[must_use] pub fn is_empty(&self) -> bool { - self.0.as_ref().map_or(true, |v| v.is_empty()) + self.0.is_empty() } #[must_use] pub fn len(&self) -> usize { - self.0.as_ref().map_or(0, |v| v.len()) + self.0.len() } /// Returns a reference to the value corresponding to the attribute key. @@ -285,11 +271,7 @@ impl<'s> FromIterator<(&'s str, &'s str)> for Attributes<'s> { .into_iter() .map(|(a, v)| (a, v.into())) .collect::>(); - if attrs.is_empty() { - Attributes::new() - } else { - Attributes(Some(attrs.into())) - } + Attributes(attrs) } } @@ -339,7 +321,7 @@ impl<'s> IntoIterator for Attributes<'s> { type IntoIter = AttributesIntoIter<'s>; fn into_iter(self) -> Self::IntoIter { - AttributesIntoIter(self.0.map_or(vec![].into_iter(), |b| (*b).into_iter())) + AttributesIntoIter(self.0.into_iter()) } } @@ -364,8 +346,7 @@ impl<'i, 's> IntoIterator for &'i Attributes<'s> { type IntoIter = AttributesIter<'i, 's>; fn into_iter(self) -> Self::IntoIter { - let sl = self.0.as_ref().map_or(&[][..], |a| a.as_slice()); - AttributesIter(sl.iter()) + AttributesIter(self.0.iter()) } } @@ -391,8 +372,7 @@ impl<'i, 's> IntoIterator for &'i mut Attributes<'s> { type IntoIter = AttributesIterMut<'i, 's>; fn into_iter(self) -> Self::IntoIter { - let sl = self.0.as_mut().map_or(&mut [][..], |a| a.as_mut()); - AttributesIterMut(sl.iter_mut()) + AttributesIterMut(self.0.iter_mut()) } } @@ -469,7 +449,7 @@ impl<'s> Parser<'s> { Identifier => self.attrs.insert("id", content.into()), Key => self.i_prev = self.attrs.insert_pos(content, "".into()), Value | ValueQuoted | ValueContinued => { - self.attrs.0.as_mut().unwrap()[self.i_prev] + self.attrs.0[self.i_prev] .1 .extend(&content[usize::from(matches!(st, ValueQuoted))..]); } From fb0a565fa12af57ae4b2c536066476929c8d69f4 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 15:36:58 +0200 Subject: [PATCH 14/25] attr: store attrs in order and with duplicates now conceptually a list rather than a map this makes the attributes object simpler and more flexible. there are no real restrictions on how to modify or build new attribute sets, e.g. keys can now be modified as there is no uniqueness requirement. provide helper functions for when duplicates not desired, e.g. for html rendering. --- src/attr.rs | 481 +++++++++++++++++++++++++++++++++++++--------------- src/html.rs | 10 +- src/lib.rs | 129 +++++++++++--- 3 files changed, 458 insertions(+), 162 deletions(-) diff --git a/src/attr.rs b/src/attr.rs index d72037c..62b681e 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -24,6 +24,8 @@ pub(crate) fn valid(src: &str) -> usize { /// Stores an attribute value that supports backslash escapes of ASCII punctuation upon displaying, /// without allocating. +/// +/// Each value is paired together with an [`AttributeKind`] in order to form an element. #[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct AttributeValue<'s> { raw: CowStr<'s>, @@ -120,9 +122,72 @@ impl<'s> Iterator for AttributeValueParts<'s> { } } -/// A collection of attributes, i.e. a key-value map. +/// The kind of an element within an attribute set. +/// +/// Each kind is paired together with an [`AttributeValue`] to form an element. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AttributeKind<'s> { + /// A class element, e.g. `.a`. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{.a}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some((AttributeKind::Class, "a".into()))); + /// assert_eq!(a.next(), None); + /// ``` + Class, + /// An id element, e.g. `#a`. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{#a}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some((AttributeKind::Id, "a".into()))); + /// assert_eq!(a.next(), None); + /// ``` + Id, + /// A key-value pair element, e.g. `key=value`. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from(r#"{key=value id="a"}"#) + /// .unwrap() + /// .into_iter(); + /// assert_eq!( + /// a.next(), + /// Some((AttributeKind::Pair { key: "key" }, "value".into())), + /// ); + /// assert_eq!( + /// a.next(), + /// Some((AttributeKind::Pair { key: "id" }, "a".into())), + /// ); + /// assert_eq!(a.next(), None); + /// ``` + Pair { key: &'s str }, +} + +impl<'s> AttributeKind<'s> { + /// Returns the element's key. + #[must_use] + pub fn key(&self) -> &'s str { + match self { + AttributeKind::Class => "class", + AttributeKind::Id => "id", + AttributeKind::Pair { key: k } => k, + } + } +} + +/// A set of attributes, with order, duplicates and comments preserved. #[derive(Clone, PartialEq, Eq, Default)] -pub struct Attributes<'s>(Vec<(&'s str, AttributeValue<'s>)>); +pub struct Attributes<'s>(Vec>); + +type AttributeElem<'s> = (AttributeKind<'s>, AttributeValue<'s>); impl<'s> Attributes<'s> { /// Create an empty collection. @@ -145,41 +210,13 @@ impl<'s> Attributes<'s> { } /// Combine all attributes from both objects, prioritizing self on conflicts. - pub(crate) fn union(&mut self, other: Self) { - for (key, val) in other.0 { - if key == "class" || !self.0.iter().any(|(k, _)| *k == key) { - self.0.push((key, val)); - } - } + pub(crate) fn concat(&mut self, mut other: Self) { + other.0.append(&mut self.0); + self.0 = other.0; } - /// Insert an attribute. If the attribute already exists, the previous value will be - /// overwritten, unless it is a "class" attribute. In that case the provided value will be - /// appended to the existing value. - pub fn insert(&mut self, key: &'s str, val: AttributeValue<'s>) { - self.insert_pos(key, val); - } - - // duplicate of insert but returns position of inserted value - fn insert_pos(&mut self, key: &'s str, val: AttributeValue<'s>) -> usize { - if let Some(i) = self.0.iter().position(|(k, _)| *k == key) { - let prev = &mut self.0[i].1; - if key == "class" { - match val.raw { - CowStr::Borrowed(s) => prev.extend(s), - CowStr::Owned(s) => { - *prev = format!("{} {}", prev, s).into(); - } - } - } else { - *prev = val; - } - i - } else { - let i = self.0.len(); - self.0.push((key, val)); - i - } + pub fn push(&mut self, key: AttributeKind<'s>, val: AttributeValue<'s>) { + self.0.push((key, val)); } /// Returns true if the collection contains no attributes. @@ -193,15 +230,65 @@ impl<'s> Attributes<'s> { self.0.len() } - /// Returns a reference to the value corresponding to the attribute key. + /// Returns whether the specified key exists in the set. + /// + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let a = Attributes::try_from("{x=y .a}").unwrap(); + /// assert!(a.contains_key("x")); + /// assert!(!a.contains_key("y")); + /// assert!(a.contains_key("class")); + /// ``` #[must_use] - pub fn get(&self, key: &str) -> Option<&AttributeValue> { - self.iter().find(|(k, _)| *k == key).map(|(_, v)| v) + pub fn contains_key(&self, key: &str) -> bool { + self.0.iter().any(|(k, _)| k.key() == key) } - /// Returns a mutable reference to the value corresponding to the attribute key. - pub fn get_mut(&'s mut self, key: &str) -> Option<&mut AttributeValue> { - self.iter_mut().find(|(k, _)| *k == key).map(|(_, v)| v) + /// Returns the value corresponding to the provided attribute key. + /// + /// Note: A copy of the value is returned rather than a reference, due to class values + /// differing from its internal representation. + /// + /// # Examples + /// + /// For the "class" key, concatenate all class values: + /// + /// ``` + /// # use jotdown::*; + /// assert_eq!( + /// Attributes::try_from("{.a class=b}").unwrap().get_value("class"), + /// Some("a b".into()), + /// ); + /// ``` + /// + /// For other keys, return the last set value: + /// + /// ``` + /// # use jotdown::*; + /// assert_eq!( + /// Attributes::try_from("{x=a x=b}").unwrap().get_value("x"), + /// Some("b".into()), + /// ); + /// ``` + #[must_use] + pub fn get_value(&self, key: &str) -> Option { + if key == "class" && self.0.iter().filter(|(k, _)| k.key() == "class").count() > 1 { + let mut value = AttributeValue::new(); + for (k, v) in &self.0 { + if k.key() == "class" { + value.extend(&v.raw); + } + } + Some(value) + } else { + self.0 + .iter() + .rfind(|(k, _)| k.key() == key) + .map(|(_, v)| v.clone()) + } } /// Returns an iterator over references to the attribute keys and values in undefined order. @@ -213,6 +300,48 @@ impl<'s> Attributes<'s> { pub fn iter_mut<'i>(&'i mut self) -> AttributesIterMut<'i, 's> { self.into_iter() } + + /// Returns an iterator that only emits a single key-value pair per unique key, i.e. like they + /// appear in the rendered output. + /// + /// # Examples + /// + /// For "class" elements, values are concatenated: + /// + /// ``` + /// # use jotdown::*; + /// let a: Attributes = "{class=a .b}".try_into().unwrap(); + /// let mut pairs = a.unique_pairs(); + /// assert_eq!(pairs.next(), Some(("class", "a b".into()))); + /// assert_eq!(pairs.next(), None); + /// ``` + /// + /// For other keys, the last set value is used: + /// + /// ``` + /// # use jotdown::*; + /// let a: Attributes = "{id=a key=b #c key=d}".try_into().unwrap(); + /// let mut pairs = a.unique_pairs(); + /// assert_eq!(pairs.next(), Some(("id", "c".into()))); + /// assert_eq!(pairs.next(), Some(("key", "d".into()))); + /// assert_eq!(pairs.next(), None); + /// ``` + /// + /// Comments are ignored: + /// + /// ``` + /// # use jotdown::*; + /// let a: Attributes = "{%cmt% #a}".try_into().unwrap(); + /// let mut pairs = a.unique_pairs(); + /// assert_eq!(pairs.next(), Some(("id", "a".into()))); + /// ``` + #[must_use] + pub fn unique_pairs<'a>(&'a self) -> AttributePairsIter<'a, 's> { + AttributePairsIter { + attrs: &self.0, + pos: 0, + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -234,18 +363,19 @@ impl<'s> TryFrom<&'s str> for Attributes<'s> { /// A single set of attributes can be parsed: /// /// ``` - /// # use jotdown::Attributes; + /// # use jotdown::*; /// let mut a = Attributes::try_from("{.a}").unwrap().into_iter(); - /// assert_eq!(a.next(), Some(("class", "a".into()))); + /// assert_eq!(a.next(), Some((AttributeKind::Class, "a".into()))); /// assert_eq!(a.next(), None); /// ``` /// /// Multiple sets can be parsed if they immediately follow the each other: /// /// ``` - /// # use jotdown::Attributes; + /// # use jotdown::*; /// let mut a = Attributes::try_from("{.a}{.b}").unwrap().into_iter(); - /// assert_eq!(a.next(), Some(("class", "a b".into()))); + /// assert_eq!(a.next(), Some((AttributeKind::Class, "a".into()))); + /// assert_eq!(a.next(), Some((AttributeKind::Class, "b".into()))); /// assert_eq!(a.next(), None); /// ``` /// @@ -265,8 +395,8 @@ impl<'s> TryFrom<&'s str> for Attributes<'s> { } #[cfg(test)] -impl<'s> FromIterator<(&'s str, &'s str)> for Attributes<'s> { - fn from_iter>(iter: I) -> Self { +impl<'s> FromIterator<(AttributeKind<'s>, &'s str)> for Attributes<'s> { + fn from_iter, &'s str)>>(iter: I) -> Self { let attrs = iter .into_iter() .map(|(a, v)| (a, v.into())) @@ -283,7 +413,7 @@ impl<'s> std::fmt::Debug for Attributes<'s> { /// ``` /// # use jotdown::*; /// let a = r#"{#a .b id=c class=d key="val" %comment%}"#; - /// let b = r#"{id="c", class="b d", key="val"}"#; + /// let b = r#"{#a .b id="c" class="d" key="val"}"#; /// assert_eq!(format!("{:?}", Attributes::try_from(a).unwrap()), b); /// ``` fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -291,20 +421,24 @@ impl<'s> std::fmt::Debug for Attributes<'s> { let mut first = true; for (k, v) in self { if !first { - write!(f, ", ")?; + write!(f, " ")?; } first = false; - write!(f, "{}=\"{}\"", k, v.raw)?; + match k { + AttributeKind::Class => write!(f, ".{}", v.raw)?, + AttributeKind::Id => write!(f, "#{}", v.raw)?, + AttributeKind::Pair { key } => write!(f, "{}=\"{}\"", key, v.raw)?, + } } write!(f, "}}") } } /// Iterator over [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIntoIter<'s>(std::vec::IntoIter<(&'s str, AttributeValue<'s>)>); +pub struct AttributesIntoIter<'s>(std::vec::IntoIter>); impl<'s> Iterator for AttributesIntoIter<'s> { - type Item = (&'s str, AttributeValue<'s>); + type Item = AttributeElem<'s>; fn next(&mut self) -> Option { self.0.next() @@ -316,7 +450,7 @@ impl<'s> Iterator for AttributesIntoIter<'s> { } impl<'s> IntoIterator for Attributes<'s> { - type Item = (&'s str, AttributeValue<'s>); + type Item = AttributeElem<'s>; type IntoIter = AttributesIntoIter<'s>; @@ -326,10 +460,10 @@ impl<'s> IntoIterator for Attributes<'s> { } /// Iterator over references to [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIter<'i, 's>(std::slice::Iter<'i, (&'s str, AttributeValue<'s>)>); +pub struct AttributesIter<'i, 's>(std::slice::Iter<'i, AttributeElem<'s>>); impl<'i, 's> Iterator for AttributesIter<'i, 's> { - type Item = (&'s str, &'i AttributeValue<'s>); + type Item = (AttributeKind<'s>, &'i AttributeValue<'s>); fn next(&mut self) -> Option { self.0.next().map(move |(k, v)| (*k, v)) @@ -341,7 +475,7 @@ impl<'i, 's> Iterator for AttributesIter<'i, 's> { } impl<'i, 's> IntoIterator for &'i Attributes<'s> { - type Item = (&'s str, &'i AttributeValue<'s>); + type Item = (AttributeKind<'s>, &'i AttributeValue<'s>); type IntoIter = AttributesIter<'i, 's>; @@ -351,10 +485,10 @@ impl<'i, 's> IntoIterator for &'i Attributes<'s> { } /// Iterator over mutable references to [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIterMut<'i, 's>(std::slice::IterMut<'i, (&'s str, AttributeValue<'s>)>); +pub struct AttributesIterMut<'i, 's>(std::slice::IterMut<'i, AttributeElem<'s>>); impl<'i, 's> Iterator for AttributesIterMut<'i, 's> { - type Item = (&'s str, &'i mut AttributeValue<'s>); + type Item = (AttributeKind<'s>, &'i mut AttributeValue<'s>); fn next(&mut self) -> Option { // this map splits &(&k, v) into (&&k, &v) @@ -367,7 +501,7 @@ impl<'i, 's> Iterator for AttributesIterMut<'i, 's> { } impl<'i, 's> IntoIterator for &'i mut Attributes<'s> { - type Item = (&'s str, &'i mut AttributeValue<'s>); + type Item = (AttributeKind<'s>, &'i mut AttributeValue<'s>); type IntoIter = AttributesIterMut<'i, 's>; @@ -376,6 +510,48 @@ impl<'i, 's> IntoIterator for &'i mut Attributes<'s> { } } +/// Iterator of unique attribute pairs. +/// +/// See [`Attributes::unique_pairs`] for more information. +pub struct AttributePairsIter<'a, 's> { + attrs: &'a [AttributeElem<'s>], + pos: usize, +} + +impl<'a: 's, 's> Iterator for AttributePairsIter<'a, 's> { + type Item = (&'s str, AttributeValue<'s>); + fn next(&mut self) -> Option { + while let Some((key, value)) = self.attrs[self.pos..].first() { + self.pos += 1; + let key = key.key(); + + if self.attrs[..self.pos - 1] + .iter() + .any(|(k, _)| k.key() == key) + { + continue; // already emitted when this key first encountered + } + + if key == "class" { + let mut value = value.clone(); + for (k, v) in &self.attrs[self.pos..] { + if k.key() == "class" { + value.extend(&v.raw); + } + } + return Some((key, value)); + } + + if let Some((_, v)) = self.attrs[self.pos..].iter().rfind(|(k, _)| k.key() == key) { + return Some((key, v.clone())); // emit last value when key first encountered + } + + return Some((key, value.clone())); + } + None + } +} + #[derive(Clone)] pub struct Validator { state: State, @@ -412,7 +588,6 @@ impl Validator { /// object. pub struct Parser<'s> { attrs: Attributes<'s>, - i_prev: usize, state: State, } @@ -420,7 +595,6 @@ impl<'s> Parser<'s> { pub fn new(attrs: Attributes<'s>) -> Self { Self { attrs, - i_prev: usize::MAX, state: State::Start, } } @@ -445,11 +619,14 @@ impl<'s> Parser<'s> { let content = &input[pos_prev..pos]; pos_prev = pos; match st { - Class => self.attrs.insert("class", content.into()), - Identifier => self.attrs.insert("id", content.into()), - Key => self.i_prev = self.attrs.insert_pos(content, "".into()), + Class => self.attrs.push(AttributeKind::Class, content.into()), + Identifier => self.attrs.push(AttributeKind::Id, content.into()), + Key => self + .attrs + .push(AttributeKind::Pair { key: content }, "".into()), Value | ValueQuoted | ValueContinued => { - self.attrs.0[self.i_prev] + let last = self.attrs.len() - 1; + self.attrs.0[last] .1 .extend(&content[usize::from(matches!(st, ValueQuoted))..]); } @@ -545,43 +722,49 @@ pub fn is_name(c: u8) -> bool { #[cfg(test)] mod test { + use super::AttributeKind::*; use super::*; macro_rules! test_attr { - ($src:expr $(,$($av:expr),* $(,)?)?) => { + ($src:expr, [$($exp:expr),* $(,)?], [$($exp_uniq:expr),* $(,)?] $(,)?) => { #[allow(unused)] let mut attr = Attributes::try_from($src).unwrap(); - let actual = attr.iter().collect::>(); - let expected = &[$($($av),*,)?]; - for i in 0..actual.len() { - let actual_val = format!("{}", actual[i].1); - assert_eq!((actual[i].0, actual_val.as_str()), expected[i], "\n\n{}\n\n", $src); - } + + let actual = attr.iter().map(|(k, v)| (k, v.to_string())).collect::>(); + let expected = &[$($exp),*].map(|(k, v): (_, &str)| (k, v.to_string())); + assert_eq!(actual, expected, "\n\n{}\n\n", $src); + + let actual = attr.unique_pairs().map(|(k, v)| (k, v.to_string())).collect::>(); + let expected = &[$($exp_uniq),*].map(|(k, v): (_, &str)| (k, v.to_string())); + assert_eq!(actual, expected, "\n\n{}\n\n", $src); }; } #[test] fn empty() { - test_attr!("{}"); + test_attr!("{}", [], []); } #[test] fn class_id() { test_attr!( "{.some_class #some_id}", - ("class", "some_class"), - ("id", "some_id"), + [(Class, "some_class"), (Id, "some_id")], + [("class", "some_class"), ("id", "some_id")], ); - test_attr!("{.a .b}", ("class", "a b")); - test_attr!("{#a #b}", ("id", "b")); + test_attr!("{.a .b}", [(Class, "a"), (Class, "b")], [("class", "a b")]); + test_attr!("{#a #b}", [(Id, "a"), (Id, "b")], [("id", "b")]); } #[test] fn value_unquoted() { test_attr!( "{attr0=val0 attr1=val1}", - ("attr0", "val0"), - ("attr1", "val1"), + [ + (Pair { key: "attr0" }, "val0"), + (Pair { key: "attr1" }, "val1"), + ], + [("attr0", "val0"), ("attr1", "val1")], ); } @@ -589,32 +772,46 @@ mod test { fn value_quoted() { test_attr!( r#"{attr0="val0" attr1="val1"}"#, - ("attr0", "val0"), - ("attr1", "val1"), + [ + (Pair { key: "attr0" }, "val0"), + (Pair { key: "attr1" }, "val1"), + ], + [("attr0", "val0"), ("attr1", "val1")], ); test_attr!( r#"{#id .class style="color:red"}"#, - ("id", "id"), - ("class", "class"), - ("style", "color:red") + [ + (Id, "id"), + (Class, "class"), + (Pair { key: "style" }, "color:red"), + ], + [("id", "id"), ("class", "class"), ("style", "color:red")] ); } #[test] fn value_newline() { - test_attr!("{attr0=\"abc\ndef\"}", ("attr0", "abc def")); + test_attr!( + "{attr0=\"abc\ndef\"}", + [(Pair { key: "attr0" }, "abc def")], + [("attr0", "abc def")] + ); } #[test] fn comment() { - test_attr!("{%}"); - test_attr!("{%%}"); - test_attr!("{ % abc % }"); - test_attr!("{ .some_class % #some_id }", ("class", "some_class")); + test_attr!("{%}", [], []); + test_attr!("{%%}", [], []); + test_attr!("{ % abc % }", [], []); + test_attr!( + "{ .some_class % #some_id }", + [(Class, "some_class")], + [("class", "some_class")] + ); test_attr!( "{ .some_class % abc % #some_id}", - ("class", "some_class"), - ("id", "some_id"), + [(Class, "some_class"), (Id, "some_id")], + [("class", "some_class"), ("id", "some_id")], ); } @@ -622,33 +819,46 @@ mod test { fn escape() { test_attr!( r#"{attr="with escaped \~ char"}"#, - ("attr", "with escaped ~ char") + [(Pair { key: "attr" }, "with escaped ~ char")], + [("attr", "with escaped ~ char")] ); test_attr!( r#"{key="quotes \" should be escaped"}"#, - ("key", r#"quotes " should be escaped"#) + [(Pair { key: "key" }, r#"quotes " should be escaped"#)], + [("key", r#"quotes " should be escaped"#)] ); } #[test] fn escape_backslash() { - test_attr!(r#"{attr="with\\backslash"}"#, ("attr", r"with\backslash")); + test_attr!( + r#"{attr="with\\backslash"}"#, + [(Pair { key: "attr" }, r"with\backslash")], + [("attr", r"with\backslash")] + ); test_attr!( r#"{attr="with many backslashes\\\\"}"#, - ("attr", r"with many backslashes\\") + [(Pair { key: "attr" }, r"with many backslashes\\")], + [("attr", r"with many backslashes\\")] ); test_attr!( r#"{attr="\\escaped backslash at start"}"#, - ("attr", r"\escaped backslash at start") + [(Pair { key: "attr" }, r"\escaped backslash at start")], + [("attr", r"\escaped backslash at start")] ); } #[test] fn only_escape_punctuation() { - test_attr!(r#"{attr="do not \escape"}"#, ("attr", r"do not \escape")); + test_attr!( + r#"{attr="do not \escape"}"#, + [(Pair { key: "attr" }, r"do not \escape")], + [("attr", r"do not \escape")] + ); test_attr!( r#"{attr="\backslash at the beginning"}"#, - ("attr", r"\backslash at the beginning") + [(Pair { key: "attr" }, r"\backslash at the beginning")], + [("attr", r"\backslash at the beginning")] ); } @@ -700,32 +910,32 @@ mod test { #[test] fn get_value_named() { assert_eq!( - Attributes::try_from("{x=a}").unwrap().get("x"), - Some(&"a".into()), + Attributes::try_from("{x=a}").unwrap().get_value("x"), + Some("a".into()), ); assert_eq!( - Attributes::try_from("{x=a x=b}").unwrap().get("x"), - Some(&"b".into()), + Attributes::try_from("{x=a x=b}").unwrap().get_value("x"), + Some("b".into()), ); } #[test] fn get_value_id() { assert_eq!( - Attributes::try_from("{#a}").unwrap().get("id"), - Some(&"a".into()), + Attributes::try_from("{#a}").unwrap().get_value("id"), + Some("a".into()), ); assert_eq!( - Attributes::try_from("{#a #b}").unwrap().get("id"), - Some(&"b".into()), + Attributes::try_from("{#a #b}").unwrap().get_value("id"), + Some("b".into()), ); assert_eq!( - Attributes::try_from("{#a id=b}").unwrap().get("id"), - Some(&"b".into()), + Attributes::try_from("{#a id=b}").unwrap().get_value("id"), + Some("b".into()), ); assert_eq!( - Attributes::try_from("{id=a #b}").unwrap().get("id"), - Some(&"b".into()), + Attributes::try_from("{id=a #b}").unwrap().get_value("id"), + Some("b".into()), ); } @@ -734,58 +944,59 @@ mod test { assert_eq!( Attributes::try_from("{.a #a .b #b .c}") .unwrap() - .get("class"), - Some(&"a b c".into()), + .get_value("class"), + Some("a b c".into()), + ); + assert_eq!( + Attributes::try_from("{#a}").unwrap().get_value("class"), + None, ); - assert_eq!(Attributes::try_from("{#a}").unwrap().get("class"), None,); assert_eq!( - Attributes::try_from("{.a}").unwrap().get("class"), - Some(&"a".into()), + Attributes::try_from("{.a}").unwrap().get_value("class"), + Some("a".into()), ); assert_eq!( Attributes::try_from("{.a #a class=b #b .c}") .unwrap() - .get("class"), - Some(&"a b c".into()), + .get_value("class"), + Some("a b c".into()), ); } - fn make_attrs<'a>(v: Vec<(&'a str, &'a str)>) -> Attributes<'a> { - v.into_iter().collect() - } - #[test] fn can_iter() { - let attrs = make_attrs(vec![("key1", "val1"), ("key2", "val2")]); - let as_vec = attrs.iter().collect::>(); assert_eq!( - as_vec, + Attributes::try_from("{key1=val1 key2=val2}") + .unwrap() + .iter() + .collect::>(), vec![ - ("key1", &AttributeValue::from("val1")), - ("key2", &AttributeValue::from("val2")), + (Pair { key: "key1" }, &AttributeValue::from("val1")), + (Pair { key: "key2" }, &AttributeValue::from("val2")), ] ); } #[test] fn can_iter_mut() { - let mut attrs = make_attrs(vec![("key1", "val1"), ("key2", "val2")]); - let as_vec = attrs.iter_mut().collect::>(); assert_eq!( - as_vec, + Attributes::try_from("{key1=val1 key2=val2}") + .unwrap() + .iter_mut() + .collect::>(), vec![ - ("key1", &mut AttributeValue::from("val1")), - ("key2", &mut AttributeValue::from("val2")), + (Pair { key: "key1" }, &mut AttributeValue::from("val1")), + (Pair { key: "key2" }, &mut AttributeValue::from("val2")), ] ); } #[test] fn iter_after_iter_mut() { - let mut attrs: Attributes = make_attrs(vec![("key1", "val1"), ("key2", "val2")]); + let mut attrs = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); for (attr, value) in &mut attrs { - if attr == "key2" { + if attr.key() == "key2" { *value = "new_val".into(); } } @@ -793,8 +1004,8 @@ mod test { assert_eq!( attrs.iter().collect::>(), vec![ - ("key1", &AttributeValue::from("val1")), - ("key2", &AttributeValue::from("new_val")), + (Pair { key: "key1" }, &AttributeValue::from("val1")), + (Pair { key: "key2" }, &AttributeValue::from("new_val")), ] ); } diff --git a/src/html.rs b/src/html.rs index 83b96e4..073aa97 100644 --- a/src/html.rs +++ b/src/html.rs @@ -208,7 +208,7 @@ impl<'s> Writer<'s> { Container::LinkDefinition { .. } => return Ok(()), } - for (a, v) in attrs.into_iter().filter(|(a, _)| *a != "class") { + for (a, v) in attrs.unique_pairs().filter(|(a, _)| *a != "class") { write!(out, r#" {}=""#, a)?; v.parts().try_for_each(|part| write_attr(part, &mut out))?; out.write_char('"')?; @@ -221,14 +221,14 @@ impl<'s> Writer<'s> { } | Container::Section { id } = &c { - if !attrs.into_iter().any(|(a, _)| a == "id") { + if !attrs.unique_pairs().any(|(a, _)| a == "id") { out.write_str(r#" id=""#)?; write_attr(id, &mut out)?; out.write_char('"')?; } } - if attrs.into_iter().any(|(a, _)| a == "class") + if attrs.unique_pairs().any(|(a, _)| a == "class") || matches!( c, Container::Div { class } if !class.is_empty()) @@ -254,7 +254,7 @@ impl<'s> Writer<'s> { out.write_str(cls)?; } for cls in attrs - .into_iter() + .unique_pairs() .filter(|(a, _)| a == &"class") .map(|(_, cls)| cls) { @@ -415,7 +415,7 @@ impl<'s> Writer<'s> { out.write_char('\n')?; } out.write_str(" = std::borrow::Cow<'s, str>; @@ -668,8 +668,8 @@ impl<'s> PrePass<'s> { .map(|sp| Attributes::try_from(&src[sp.clone()]).expect("should be valid")); let id_override = attrs .as_ref() - .and_then(|attrs| attrs.get("id")) - .map(ToString::to_string); + .and_then(|attrs| attrs.get_value("id")) + .map(|s| s.to_string()); let mut id_auto = String::new(); let mut text = String::new(); @@ -969,7 +969,7 @@ impl<'s> Parser<'s> { let (url_or_tag, ty) = if let Some((url, attrs_def)) = link_def { if enter { - attributes.union(attrs_def); + attributes.concat(attrs_def); } (url, SpanLinkType::Reference) } else { @@ -1207,6 +1207,7 @@ impl<'s> Iterator for OffsetIter<'s> { #[cfg(test)] mod test { + use super::AttributeKind; use super::Attributes; use super::Container::*; use super::Event::*; @@ -1374,7 +1375,9 @@ mod test { ( Start( Section { id: "def".into() }, - [("a", "b")].into_iter().collect(), + [(AttributeKind::Pair { key: "a" }, "b")] + .into_iter() + .collect(), ), "{a=b}\n", ), @@ -2028,7 +2031,12 @@ mod test { ( Start( Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - [("b", "c"), ("a", "b")].into_iter().collect(), + [ + (AttributeKind::Pair { key: "a" }, "b"), + (AttributeKind::Pair { key: "b" }, "c"), + ] + .into_iter() + .collect(), ), "[", ), @@ -2042,7 +2050,9 @@ mod test { ( Start( LinkDefinition { label: "tag" }, - [("a", "b")].into_iter().collect(), + [(AttributeKind::Pair { key: "a" }, "b")] + .into_iter() + .collect(), ), "{a=b}\n[tag]:", ), @@ -2068,7 +2078,12 @@ mod test { ( Start( Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - [("class", "link"), ("class", "def")].into_iter().collect(), + [ + (AttributeKind::Class, "def"), + (AttributeKind::Class, "link"), + ] + .into_iter() + .collect(), ), "[", ), @@ -2082,7 +2097,7 @@ mod test { ( Start( LinkDefinition { label: "tag" }, - [("class", "def")].into_iter().collect(), + [(AttributeKind::Class, "def")].into_iter().collect(), ), "{.def}\n[tag]:", ), @@ -2237,7 +2252,10 @@ mod test { test_parse!( "{.some_class}\npara\n", ( - Start(Paragraph, [("class", "some_class")].into_iter().collect()), + Start( + Paragraph, + [(AttributeKind::Class, "some_class")].into_iter().collect(), + ), "{.some_class}\n", ), (Str("para".into()), "para"), @@ -2252,7 +2270,9 @@ mod test { ( Start( Paragraph, - [("class", "a"), ("id", "b")].into_iter().collect(), + [(AttributeKind::Class, "a"), (AttributeKind::Id, "b")] + .into_iter() + .collect(), ), "{.a}\n{#b}\n", ), @@ -2268,7 +2288,10 @@ mod test { (Start(Paragraph, Attributes::new()), ""), (Str("abc ".into()), "abc "), ( - Start(Emphasis, [("class", "ghi")].into_iter().collect()), + Start( + Emphasis, + [(AttributeKind::Class, "ghi")].into_iter().collect(), + ), "_", ), (Str("def".into()), "def"), @@ -2285,7 +2308,13 @@ mod test { ( Start( Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + ] + .into_iter() + .collect(), ), "_", ), @@ -2299,7 +2328,13 @@ mod test { ( Start( Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + ] + .into_iter() + .collect(), ), "_", ), @@ -2317,7 +2352,13 @@ mod test { ( Start( Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + ] + .into_iter() + .collect(), ), "_", ), @@ -2332,7 +2373,13 @@ mod test { ( Start( Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + ] + .into_iter() + .collect(), ), "_", ), @@ -2347,7 +2394,13 @@ mod test { ( Start( Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + ] + .into_iter() + .collect(), ), "_", ), @@ -2370,7 +2423,15 @@ mod test { (Start(Blockquote, Attributes::new()), ">"), (Start(Paragraph, Attributes::new()), ""), ( - Start(Emphasis, [("a", "b"), ("c", "d")].into_iter().collect()), + Start( + Emphasis, + [ + (AttributeKind::Pair { key: "a" }, "b"), + (AttributeKind::Pair { key: "c" }, "d"), + ] + .into_iter() + .collect(), + ), "_", ), (Str("abc".into()), "abc"), @@ -2386,7 +2447,15 @@ mod test { ), (Start(Blockquote, Attributes::new()), ">"), (Start(Paragraph, Attributes::new()), ""), - (Start(Span, [("a", "a")].into_iter().collect()), ""), + ( + Start( + Span, + [(AttributeKind::Pair { key: "a" }, "a")] + .into_iter() + .collect(), + ), + "", + ), (Str("a".into()), "a"), (End(Span), "{\n> %%\n> a=a}"), (End(Paragraph), ""), @@ -2400,7 +2469,15 @@ mod test { ), (Start(Blockquote, Attributes::new()), ">"), (Start(Paragraph, Attributes::new()), ""), - (Start(Span, [("a", "a b c")].into_iter().collect()), ""), + ( + Start( + Span, + [(AttributeKind::Pair { key: "a" }, "a b c")] + .into_iter() + .collect(), + ), + "", + ), (Str("a".into()), "a"), (End(Span), "{a=\"a\n> b\n> c\"}"), (End(Paragraph), ""), @@ -2413,7 +2490,15 @@ mod test { ), (Start(Blockquote, Attributes::new()), ">"), (Start(Paragraph, Attributes::new()), ""), - (Start(Span, [("a", "b")].into_iter().collect()), ""), + ( + Start( + Span, + [(AttributeKind::Pair { key: "a" }, "b")] + .into_iter() + .collect(), + ), + "", + ), (Str("a".into()), "a"), (End(Span), "{a=\"\n> b\"}"), (End(Paragraph), ""), From e6bd586af6a1b1ef5a238c66551d6687160cbc77 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 16:33:42 +0200 Subject: [PATCH 15/25] attr: dereference Attributes into Vec in order to inherit full functionality of vec, avoid re-implementing only a subset and provide a lot more flexibility --- src/attr.rs | 219 ++++++++++++++++++++++++---------------------------- src/lib.rs | 8 +- 2 files changed, 104 insertions(+), 123 deletions(-) diff --git a/src/attr.rs b/src/attr.rs index 62b681e..bda874e 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -209,27 +209,6 @@ impl<'s> Attributes<'s> { Ok(()) } - /// Combine all attributes from both objects, prioritizing self on conflicts. - pub(crate) fn concat(&mut self, mut other: Self) { - other.0.append(&mut self.0); - self.0 = other.0; - } - - pub fn push(&mut self, key: AttributeKind<'s>, val: AttributeValue<'s>) { - self.0.push((key, val)); - } - - /// Returns true if the collection contains no attributes. - #[must_use] - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - #[must_use] - pub fn len(&self) -> usize { - self.0.len() - } - /// Returns whether the specified key exists in the set. /// /// @@ -291,16 +270,6 @@ impl<'s> Attributes<'s> { } } - /// Returns an iterator over references to the attribute keys and values in undefined order. - pub fn iter(&self) -> AttributesIter { - self.into_iter() - } - - /// Returns an iterator over mutable references to the attribute keys and values in undefined order. - pub fn iter_mut<'i>(&'i mut self) -> AttributesIterMut<'i, 's> { - self.into_iter() - } - /// Returns an iterator that only emits a single key-value pair per unique key, i.e. like they /// appear in the rendered output. /// @@ -394,6 +363,20 @@ impl<'s> TryFrom<&'s str> for Attributes<'s> { } } +impl<'s> std::ops::Deref for Attributes<'s> { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'s> std::ops::DerefMut for Attributes<'s> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + #[cfg(test)] impl<'s> FromIterator<(AttributeKind<'s>, &'s str)> for Attributes<'s> { fn from_iter, &'s str)>>(iter: I) -> Self { @@ -434,79 +417,105 @@ impl<'s> std::fmt::Debug for Attributes<'s> { } } -/// Iterator over [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIntoIter<'s>(std::vec::IntoIter>); - -impl<'s> Iterator for AttributesIntoIter<'s> { - type Item = AttributeElem<'s>; - - fn next(&mut self) -> Option { - self.0.next() - } - - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - impl<'s> IntoIterator for Attributes<'s> { type Item = AttributeElem<'s>; - type IntoIter = AttributesIntoIter<'s>; + type IntoIter = std::vec::IntoIter>; + /// Turn into an iterator of attribute elements. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let a = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); + /// let mut elems = a.into_iter(); + /// assert_eq!( + /// elems.next(), + /// Some(( + /// AttributeKind::Pair { key: "key1" }, + /// AttributeValue::from("val1"), + /// )), + /// ); + /// assert_eq!( + /// elems.next(), + /// Some(( + /// AttributeKind::Pair { key: "key2" }, + /// AttributeValue::from("val2"), + /// )), + /// ); + /// assert_eq!(elems.next(), None); + /// ``` fn into_iter(self) -> Self::IntoIter { - AttributesIntoIter(self.0.into_iter()) - } -} - -/// Iterator over references to [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIter<'i, 's>(std::slice::Iter<'i, AttributeElem<'s>>); - -impl<'i, 's> Iterator for AttributesIter<'i, 's> { - type Item = (AttributeKind<'s>, &'i AttributeValue<'s>); - - fn next(&mut self) -> Option { - self.0.next().map(move |(k, v)| (*k, v)) - } - - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() + self.0.into_iter() } } impl<'i, 's> IntoIterator for &'i Attributes<'s> { - type Item = (AttributeKind<'s>, &'i AttributeValue<'s>); + type Item = &'i AttributeElem<'s>; - type IntoIter = AttributesIter<'i, 's>; + type IntoIter = std::slice::Iter<'i, AttributeElem<'s>>; + /// Create an iterator of borrowed attribute elements. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let a = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); + /// let mut elems = a.iter(); + /// assert_eq!( + /// elems.next(), + /// Some(&( + /// AttributeKind::Pair { key: "key1" }, + /// AttributeValue::from("val1"), + /// )), + /// ); + /// assert_eq!( + /// elems.next(), + /// Some(&( + /// AttributeKind::Pair { key: "key2" }, + /// AttributeValue::from("val2"), + /// )), + /// ); + /// assert_eq!(elems.next(), None); + /// ``` fn into_iter(self) -> Self::IntoIter { - AttributesIter(self.0.iter()) - } -} - -/// Iterator over mutable references to [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIterMut<'i, 's>(std::slice::IterMut<'i, AttributeElem<'s>>); - -impl<'i, 's> Iterator for AttributesIterMut<'i, 's> { - type Item = (AttributeKind<'s>, &'i mut AttributeValue<'s>); - - fn next(&mut self) -> Option { - // this map splits &(&k, v) into (&&k, &v) - self.0.next().map(|(k, v)| (*k, v)) - } - - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() + self.0.iter() } } impl<'i, 's> IntoIterator for &'i mut Attributes<'s> { - type Item = (AttributeKind<'s>, &'i mut AttributeValue<'s>); + type Item = &'i mut AttributeElem<'s>; - type IntoIter = AttributesIterMut<'i, 's>; + type IntoIter = std::slice::IterMut<'i, AttributeElem<'s>>; + /// Create an iterator of mutably borrowed attribute elements. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); + /// let mut elems = a.iter_mut(); + /// assert_eq!( + /// elems.next(), + /// Some(&mut ( + /// AttributeKind::Pair { key: "key1" }, + /// AttributeValue::from("val1"), + /// )), + /// ); + /// assert_eq!( + /// elems.next(), + /// Some(&mut ( + /// AttributeKind::Pair { key: "key2" }, + /// AttributeValue::from("val2"), + /// )), + /// ); + /// assert_eq!(elems.next(), None); + /// ``` fn into_iter(self) -> Self::IntoIter { - AttributesIterMut(self.0.iter_mut()) + self.0.iter_mut() } } @@ -619,11 +628,11 @@ impl<'s> Parser<'s> { let content = &input[pos_prev..pos]; pos_prev = pos; match st { - Class => self.attrs.push(AttributeKind::Class, content.into()), - Identifier => self.attrs.push(AttributeKind::Id, content.into()), + Class => self.attrs.push((AttributeKind::Class, content.into())), + Identifier => self.attrs.push((AttributeKind::Id, content.into())), Key => self .attrs - .push(AttributeKind::Pair { key: content }, "".into()), + .push((AttributeKind::Pair { key: content }, "".into())), Value | ValueQuoted | ValueContinued => { let last = self.attrs.len() - 1; self.attrs.0[last] @@ -730,7 +739,7 @@ mod test { #[allow(unused)] let mut attr = Attributes::try_from($src).unwrap(); - let actual = attr.iter().map(|(k, v)| (k, v.to_string())).collect::>(); + let actual = attr.iter().map(|(k, v)| (k.clone(), v.to_string())).collect::>(); let expected = &[$($exp),*].map(|(k, v): (_, &str)| (k, v.to_string())); assert_eq!(actual, expected, "\n\n{}\n\n", $src); @@ -963,34 +972,6 @@ mod test { ); } - #[test] - fn can_iter() { - assert_eq!( - Attributes::try_from("{key1=val1 key2=val2}") - .unwrap() - .iter() - .collect::>(), - vec![ - (Pair { key: "key1" }, &AttributeValue::from("val1")), - (Pair { key: "key2" }, &AttributeValue::from("val2")), - ] - ); - } - - #[test] - fn can_iter_mut() { - assert_eq!( - Attributes::try_from("{key1=val1 key2=val2}") - .unwrap() - .iter_mut() - .collect::>(), - vec![ - (Pair { key: "key1" }, &mut AttributeValue::from("val1")), - (Pair { key: "key2" }, &mut AttributeValue::from("val2")), - ] - ); - } - #[test] fn iter_after_iter_mut() { let mut attrs = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); @@ -1004,8 +985,8 @@ mod test { assert_eq!( attrs.iter().collect::>(), vec![ - (Pair { key: "key1" }, &AttributeValue::from("val1")), - (Pair { key: "key2" }, &AttributeValue::from("new_val")), + &(Pair { key: "key1" }, AttributeValue::from("val1")), + &(Pair { key: "key2" }, AttributeValue::from("new_val")), ] ); } diff --git a/src/lib.rs b/src/lib.rs index 32993a6..03d6186 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,8 +57,7 @@ mod inline; mod lex; pub use attr::{ - AttributeKind, AttributeValue, AttributeValueParts, Attributes, AttributesIntoIter, - AttributesIter, AttributesIterMut, ParseAttributesError, + AttributeKind, AttributeValue, AttributeValueParts, Attributes, ParseAttributesError, }; type CowStr<'s> = std::borrow::Cow<'s, str>; @@ -967,9 +966,10 @@ impl<'s> Parser<'s> { .get::(tag.as_ref()) .cloned(); - let (url_or_tag, ty) = if let Some((url, attrs_def)) = link_def { + let (url_or_tag, ty) = if let Some((url, mut attrs_def)) = link_def { if enter { - attributes.concat(attrs_def); + attrs_def.append(&mut attributes); + attributes = attrs_def; } (url, SpanLinkType::Reference) } else { From 9bb5d744de685fdecd70d1bf6797daeed06b4366 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 12 Aug 2024 21:50:39 +0200 Subject: [PATCH 16/25] attr: impl From/Into Vec for Attributes --- src/attr.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/attr.rs b/src/attr.rs index bda874e..ab182b6 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -363,6 +363,18 @@ impl<'s> TryFrom<&'s str> for Attributes<'s> { } } +impl<'s> From>> for Attributes<'s> { + fn from(v: Vec>) -> Self { + Self(v) + } +} + +impl<'s> From> for Vec> { + fn from(a: Attributes<'s>) -> Self { + a.0 + } +} + impl<'s> std::ops::Deref for Attributes<'s> { type Target = Vec>; @@ -972,6 +984,15 @@ mod test { ); } + #[test] + fn from_to_vec() { + let v0: Vec<(AttributeKind, AttributeValue)> = vec![(Class, "a".into()), (Id, "b".into())]; + let a: Attributes = v0.clone().into(); + assert_eq!(format!("{:?}", a), "{.a #b}"); + let v1: Vec<(AttributeKind, AttributeValue)> = a.into(); + assert_eq!(v0, v1); + } + #[test] fn iter_after_iter_mut() { let mut attrs = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); From 3b79fcbaa2ffc93d4c10f9dcdd4528bc46054262 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 17:05:34 +0200 Subject: [PATCH 17/25] attr: store comment attributes --- src/attr.rs | 82 +++++++++++++++++++++++++++++++++++---------------- src/inline.rs | 22 +++++++++++++- src/lib.rs | 12 ++++++-- 3 files changed, 87 insertions(+), 29 deletions(-) diff --git a/src/attr.rs b/src/attr.rs index ab182b6..a7b7a4a 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -169,16 +169,29 @@ pub enum AttributeKind<'s> { /// assert_eq!(a.next(), None); /// ``` Pair { key: &'s str }, + /// A comment element, e.g. `%cmt%`. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{%cmt0% %cmt1}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some((AttributeKind::Comment, "cmt0".into()))); + /// assert_eq!(a.next(), Some((AttributeKind::Comment, "cmt1".into()))); + /// assert_eq!(a.next(), None); + /// ``` + Comment, } impl<'s> AttributeKind<'s> { - /// Returns the element's key. + /// Returns the element's key, if applicable. #[must_use] - pub fn key(&self) -> &'s str { + pub fn key(&self) -> Option<&'s str> { match self { - AttributeKind::Class => "class", - AttributeKind::Id => "id", - AttributeKind::Pair { key: k } => k, + AttributeKind::Class => Some("class"), + AttributeKind::Id => Some("id"), + AttributeKind::Pair { key } => Some(key), + AttributeKind::Comment => None, } } } @@ -223,7 +236,9 @@ impl<'s> Attributes<'s> { /// ``` #[must_use] pub fn contains_key(&self, key: &str) -> bool { - self.0.iter().any(|(k, _)| k.key() == key) + self.0 + .iter() + .any(|(k, _)| matches!(k.key(), Some(k) if k == key)) } /// Returns the value corresponding to the provided attribute key. @@ -254,10 +269,17 @@ impl<'s> Attributes<'s> { /// ``` #[must_use] pub fn get_value(&self, key: &str) -> Option { - if key == "class" && self.0.iter().filter(|(k, _)| k.key() == "class").count() > 1 { + if key == "class" + && self + .0 + .iter() + .filter(|(k, _)| k.key() == Some("class")) + .count() + > 1 + { let mut value = AttributeValue::new(); for (k, v) in &self.0 { - if k.key() == "class" { + if k.key() == Some("class") { value.extend(&v.raw); } } @@ -265,7 +287,7 @@ impl<'s> Attributes<'s> { } else { self.0 .iter() - .rfind(|(k, _)| k.key() == key) + .rfind(|(k, _)| k.key() == Some(key)) .map(|(_, v)| v.clone()) } } @@ -408,7 +430,7 @@ impl<'s> std::fmt::Debug for Attributes<'s> { /// ``` /// # use jotdown::*; /// let a = r#"{#a .b id=c class=d key="val" %comment%}"#; - /// let b = r#"{#a .b id="c" class="d" key="val"}"#; + /// let b = r#"{#a .b id="c" class="d" key="val" %comment%}"#; /// assert_eq!(format!("{:?}", Attributes::try_from(a).unwrap()), b); /// ``` fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -423,6 +445,7 @@ impl<'s> std::fmt::Debug for Attributes<'s> { AttributeKind::Class => write!(f, ".{}", v.raw)?, AttributeKind::Id => write!(f, "#{}", v.raw)?, AttributeKind::Pair { key } => write!(f, "{}=\"{}\"", key, v.raw)?, + AttributeKind::Comment => write!(f, "%{}%", v.raw)?, } } write!(f, "}}") @@ -544,11 +567,15 @@ impl<'a: 's, 's> Iterator for AttributePairsIter<'a, 's> { fn next(&mut self) -> Option { while let Some((key, value)) = self.attrs[self.pos..].first() { self.pos += 1; - let key = key.key(); + let key = if let Some(k) = key.key() { + k + } else { + continue; // ignore comments + }; if self.attrs[..self.pos - 1] .iter() - .any(|(k, _)| k.key() == key) + .any(|(k, _)| k.key() == Some(key)) { continue; // already emitted when this key first encountered } @@ -556,14 +583,17 @@ impl<'a: 's, 's> Iterator for AttributePairsIter<'a, 's> { if key == "class" { let mut value = value.clone(); for (k, v) in &self.attrs[self.pos..] { - if k.key() == "class" { + if k.key() == Some("class") { value.extend(&v.raw); } } return Some((key, value)); } - if let Some((_, v)) = self.attrs[self.pos..].iter().rfind(|(k, _)| k.key() == key) { + if let Some((_, v)) = self.attrs[self.pos..] + .iter() + .rfind(|(k, _)| k.key() == Some(key)) + { return Some((key, v.clone())); // emit last value when key first encountered } @@ -645,12 +675,13 @@ impl<'s> Parser<'s> { Key => self .attrs .push((AttributeKind::Pair { key: content }, "".into())), - Value | ValueQuoted | ValueContinued => { + Value | ValueQuoted | ValueContinued | Comment => { let last = self.attrs.len() - 1; self.attrs.0[last] .1 .extend(&content[usize::from(matches!(st, ValueQuoted))..]); } + CommentFirst => self.attrs.push((AttributeKind::Comment, "".into())), _ => {} } }; @@ -678,6 +709,7 @@ impl<'s> Parser<'s> { enum State { Start, Whitespace, + CommentFirst, Comment, ClassFirst, Class, @@ -705,14 +737,14 @@ impl State { b'}' => Done, b'.' => ClassFirst, b'#' => IdentifierFirst, - b'%' => Comment, + b'%' => CommentFirst, c if is_name(c) => Key, c if c.is_ascii_whitespace() => Whitespace, _ => Invalid, }, - Comment if c == b'%' => Whitespace, - Comment if c == b'}' => Done, - Comment => Comment, + CommentFirst | Comment if c == b'%' => Whitespace, + CommentFirst | Comment if c == b'}' => Done, + CommentFirst | Comment => Comment, ClassFirst if is_name(c) => Class, ClassFirst => Invalid, IdentifierFirst if is_name(c) => Identifier, @@ -821,17 +853,17 @@ mod test { #[test] fn comment() { - test_attr!("{%}", [], []); - test_attr!("{%%}", [], []); - test_attr!("{ % abc % }", [], []); + test_attr!("{%}", [(Comment, "")], []); + test_attr!("{%%}", [(Comment, "")], []); + test_attr!("{ % abc % }", [(Comment, " abc ")], []); test_attr!( "{ .some_class % #some_id }", - [(Class, "some_class")], + [(Class, "some_class"), (Comment, " #some_id ")], [("class", "some_class")] ); test_attr!( "{ .some_class % abc % #some_id}", - [(Class, "some_class"), (Id, "some_id")], + [(Class, "some_class"), (Comment, " abc "), (Id, "some_id")], [("class", "some_class"), ("id", "some_id")], ); } @@ -998,7 +1030,7 @@ mod test { let mut attrs = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); for (attr, value) in &mut attrs { - if attr.key() == "key2" { + if attr.key() == Some("key2") { *value = "new_val".into(); } } diff --git a/src/inline.rs b/src/inline.rs index c406681..9dacf24 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -1736,6 +1736,13 @@ mod test { ); test_parse!( "_abc def_{ % comment % } ghi", + ( + Attributes { + container: true, + attrs: 0 + }, + "{ % comment % }" + ), (Enter(Emphasis), "_"), (Str, "abc def"), (Exit(Emphasis), "_{ % comment % }"), @@ -1821,7 +1828,20 @@ mod test { #[test] fn attr_empty() { test_parse!("word{}", (Str, "word")); - test_parse!("word{ % comment % } trail", (Str, "word"), (Str, " trail")); + test_parse!( + "word{ % comment % } trail", + ( + Attributes { + container: false, + attrs: 0 + }, + "{ % comment % }" + ), + (Enter(Span), ""), + (Str, "word"), + (Exit(Span), "{ % comment % }"), + (Str, " trail") + ); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 03d6186..c579a03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2330,6 +2330,7 @@ mod test { Emphasis, [ (AttributeKind::Class, "a"), + (AttributeKind::Comment, ""), (AttributeKind::Class, "b"), (AttributeKind::Id, "i"), ] @@ -2377,6 +2378,7 @@ mod test { (AttributeKind::Class, "a"), (AttributeKind::Class, "b"), (AttributeKind::Id, "i"), + (AttributeKind::Comment, ""), ] .into_iter() .collect(), @@ -2398,6 +2400,7 @@ mod test { (AttributeKind::Class, "a"), (AttributeKind::Class, "b"), (AttributeKind::Id, "i"), + (AttributeKind::Comment, ""), ] .into_iter() .collect(), @@ -2450,9 +2453,12 @@ mod test { ( Start( Span, - [(AttributeKind::Pair { key: "a" }, "a")] - .into_iter() - .collect(), + [ + (AttributeKind::Comment, ""), + (AttributeKind::Pair { key: "a" }, "a"), + ] + .into_iter() + .collect(), ), "", ), From 5fbd422f3376c4c37f88137100a3fc2aa4557ae1 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Mon, 12 Aug 2024 22:23:07 +0200 Subject: [PATCH 18/25] attr: impl FromIterator for Attributes --- src/attr.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/attr.rs b/src/attr.rs index a7b7a4a..412ef50 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -422,6 +422,26 @@ impl<'s> FromIterator<(AttributeKind<'s>, &'s str)> for Attributes<'s> { } } +impl<'s> FromIterator> for Attributes<'s> { + /// Create `Attributes` from an iterator of elements. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let e0 = (AttributeKind::Class, AttributeValue::from("a")); + /// let e1 = (AttributeKind::Id, AttributeValue::from("b")); + /// let a: Attributes = [e0.clone(), e1.clone()].into_iter().collect(); + /// assert_eq!(format!("{:?}", a), "{.a #b}"); + /// let mut elems = a.into_iter(); + /// assert_eq!(elems.next(), Some(e0)); + /// assert_eq!(elems.next(), Some(e1)); + /// ``` + fn from_iter>>(iter: I) -> Self { + Attributes(iter.into_iter().collect()) + } +} + impl<'s> std::fmt::Debug for Attributes<'s> { /// Formats the attributes using the given formatter. /// From 32a0af0f2a8c419b1ab49d5539b2b69e5e9e57e5 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 17 Aug 2024 20:02:41 +0200 Subject: [PATCH 19/25] attr: add description/examples for Attributes --- src/attr.rs | 89 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/src/attr.rs b/src/attr.rs index 412ef50..92f1bd9 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -197,6 +197,76 @@ impl<'s> AttributeKind<'s> { } /// A set of attributes, with order, duplicates and comments preserved. +/// +/// `Attributes` is a wrapper object around a [`Vec`] containing the elements of the set, each a +/// pair of an [`AttributeKind`] and an [`AttributeValue`]. It implements [`std::ops::Deref`] and +/// [`std::ops::DerefMut`] so methods of the inner [`Vec`] and [`slice`] can be used directly on +/// the `Attributes` to access or modify the elements. The wrapper also implements [`From`] and +/// [`Into`] for [`Vec`] so one can easily add or remove the wrapper. +/// +/// `Attributes` are typically created by a [`crate::Parser`] and placed in the [`crate::Event`]s +/// that it emits. `Attributes` can also be created from a djot string representation, see +/// [`Attributes::try_from`]. +/// +/// The attribute elements can be accessed using e.g. [`slice::iter`] or [`slice::iter_mut`], but +/// if e.g. duplicate keys or comments are not desired, refer to [`Attributes::get_value`] and +/// [`Attributes::unique_pairs`]. +/// +/// # Examples +/// +/// Access the inner [`Vec`]: +/// +/// ``` +/// # use jotdown::*; +/// let a: Attributes = r#"{#a .b id=c class=d key="val" %comment%}"# +/// .try_into() +/// .unwrap(); +/// assert_eq!( +/// Vec::from(a), +/// vec![ +/// (AttributeKind::Id, "a".into()), +/// (AttributeKind::Class, "b".into()), +/// (AttributeKind::Pair { key: "id" }, "c".into()), +/// (AttributeKind::Pair { key: "class" }, "d".into()), +/// (AttributeKind::Pair { key: "key" }, "val".into()), +/// (AttributeKind::Comment, "comment".into()), +/// ], +/// ); +/// ``` +/// +/// Replace a value: +/// +/// ``` +/// # use jotdown::*; +/// let mut attrs = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); +/// +/// for (attr, value) in &mut attrs { +/// if attr.key() == Some("key2") { +/// *value = "new_val".into(); +/// } +/// } +/// +/// assert_eq!( +/// attrs.as_slice(), +/// &[ +/// (AttributeKind::Pair { key: "key1" }, "val1".into()), +/// (AttributeKind::Pair { key: "key2" }, "new_val".into()), +/// ] +/// ); +/// ``` +/// +/// Filter out keys with a specific prefix: +/// +/// ``` +/// # use jotdown::*; +/// let a: Attributes = Attributes::try_from("{ign:x=a ign:y=b z=c}") +/// .unwrap() +/// .into_iter() +/// .filter(|(k, _)| !matches!(k.key(), Some(key) if key.starts_with("ign:"))) +/// .collect(); +/// let b = Attributes::try_from("{z=c}").unwrap(); +/// assert_eq!(a, b); +/// ``` #[derive(Clone, PartialEq, Eq, Default)] pub struct Attributes<'s>(Vec>); @@ -1044,23 +1114,4 @@ mod test { let v1: Vec<(AttributeKind, AttributeValue)> = a.into(); assert_eq!(v0, v1); } - - #[test] - fn iter_after_iter_mut() { - let mut attrs = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); - - for (attr, value) in &mut attrs { - if attr.key() == Some("key2") { - *value = "new_val".into(); - } - } - - assert_eq!( - attrs.iter().collect::>(), - vec![ - &(Pair { key: "key1" }, AttributeValue::from("val1")), - &(Pair { key: "key2" }, AttributeValue::from("new_val")), - ] - ); - } } From 9c5cfa05036f0c69a5fc72eca109f8415f4925b6 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 18 Aug 2024 15:06:44 +0200 Subject: [PATCH 20/25] lib: add examples to event variants --- src/lib.rs | 1411 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1407 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c579a03..51a82ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,40 +205,429 @@ impl<'s> AsRef> for &Event<'s> { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Event<'s> { /// Start of a container. + /// + /// Always paired with a matching [`Event::End`]. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "{#a}\n", + /// "[word]{#b}\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::Paragraph, + /// [(AttributeKind::Id, "a".into())].into_iter().collect(), + /// ), + /// Event::Start( + /// Container::Span, + /// [(AttributeKind::Id, "b".into())].into_iter().collect(), + /// ), + /// Event::Str("word".into()), + /// Event::End(Container::Span), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

word

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Start(Container<'s>, Attributes<'s>), /// End of a container. + /// + /// Always paired with a matching [`Event::Start`]. End(Container<'s>), /// A string object, text only. + /// + /// The strings from the parser will always be borrowed, but users may replace them with owned + /// variants before rendering. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "str"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("str".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

str

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Str(CowStr<'s>), /// A footnote reference. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "txt[^nb]."; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("txt".into()), + /// Event::FootnoteReference("nb"), + /// Event::Str(".".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

txt1.

\n", + /// "
\n", + /// "
\n", + /// "
    \n", + /// "
  1. \n", + /// "

    ↩\u{fe0e}

    \n", + /// "
  2. \n", + /// "
\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` FootnoteReference(&'s str), /// A symbol, by default rendered literally but may be treated specially. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "a :sym:"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a ".into()), + /// Event::Symbol("sym".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

a :sym:

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Symbol(CowStr<'s>), /// Left single quotation mark. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = r#"'quote'"#; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::LeftSingleQuote, + /// Event::Str("quote".into()), + /// Event::RightSingleQuote, + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

‘quote’

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` LeftSingleQuote, - /// Right double quotation mark. + /// Right single quotation mark. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = r#"'}Tis Socrates'"#; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::RightSingleQuote, + /// Event::Str("Tis Socrates".into()), + /// Event::RightSingleQuote, + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

’Tis Socrates’

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` RightSingleQuote, /// Left single quotation mark. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = r#""Hello," he said"#; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::LeftDoubleQuote, + /// Event::Str("Hello,".into()), + /// Event::RightDoubleQuote, + /// Event::Str(" he said".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

“Hello,” he said

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` LeftDoubleQuote, /// Right double quotation mark. RightDoubleQuote, /// A horizontal ellipsis, i.e. a set of three periods. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "yes..."; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("yes".into()), + /// Event::Ellipsis, + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

yes…

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Ellipsis, /// An en dash. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "57--33"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("57".into()), + /// Event::EnDash, + /// Event::Str("33".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

57–33

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` EnDash, /// An em dash. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "oxen---and"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("oxen".into()), + /// Event::EmDash, + /// Event::Str("and".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

oxen—and

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` EmDash, /// A space that must not break a line. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "no\\ break"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("no".into()), + /// Event::Escape, + /// Event::NonBreakingSpace, + /// Event::Str("break".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

no break

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` NonBreakingSpace, /// A newline that may or may not break a line in the output. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "soft\n", + /// "break\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("soft".into()), + /// Event::Softbreak, + /// Event::Str("break".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

soft\n", + /// "break

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Softbreak, /// A newline that must break a line in the output. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "hard\\\n", + /// "break\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("hard".into()), + /// Event::Escape, + /// Event::Hardbreak, + /// Event::Str("break".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

hard
\n", + /// "break

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Hardbreak, /// An escape character, not visible in output. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "\\*a\\*"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Escape, + /// Event::Str("*a".into()), + /// Event::Escape, + /// Event::Str("*".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

*a*

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Escape, /// A blank line, not visible in output. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "para0\n", + /// "\n", + /// "para1\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("para0".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("para1".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

para0

\n", + /// "

para1

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Blankline, /// A thematic break, typically a horizontal rule. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "para0\n", + /// "\n", + /// " * * * *\n", + /// "para1\n", + /// "\n", + /// "{.c}\n", + /// "----\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("para0".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::ThematicBreak(Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("para1".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::ThematicBreak( + /// [(AttributeKind::Class, "c".into())] + /// .into_iter() + /// .collect(), + /// ), + /// ], + /// ); + /// let html = concat!( + /// "

para0

\n", + /// "
\n", + /// "

para1

\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` ThematicBreak(Attributes<'s>), } @@ -252,30 +641,500 @@ pub enum Event<'s> { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Container<'s> { /// A blockquote element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "> a\n", + /// "> b\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Blockquote, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a".into()), + /// Event::Softbreak, + /// Event::Str("b".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::Blockquote), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "

a\n", + /// "b

\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Blockquote, /// A list. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "- a\n", + /// "\n", + /// "- b\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::List { + /// kind: ListKind::Unordered, + /// tight: false, + /// }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::ListItem, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::End(Container::ListItem), + /// Event::Start(Container::ListItem, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("b".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::ListItem), + /// Event::End(Container::List { + /// kind: ListKind::Unordered, + /// tight: false + /// }), + /// ], + /// ); + /// let html = concat!( + /// "
    \n", + /// "
  • \n", + /// "

    a

    \n", + /// "
  • \n", + /// "
  • \n", + /// "

    b

    \n", + /// "
  • \n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` List { kind: ListKind, tight: bool }, /// An item of a list + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "- a"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::List { kind: ListKind::Unordered, tight: true }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::ListItem, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::ListItem), + /// Event::End(Container::List { + /// kind: ListKind::Unordered, + /// tight: true, + /// }), + /// ], + /// ); + /// let html = concat!( + /// "
    \n", + /// "
  • \n", + /// "a\n", + /// "
  • \n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` ListItem, /// An item of a task list, either checked or unchecked. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "- [x] a"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::List { kind: ListKind::Task, tight: true }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::TaskListItem { checked: true }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::TaskListItem { checked: true }), + /// Event::End(Container::List { + /// kind: ListKind::Task, + /// tight: true, + /// }), + /// ], + /// ); + /// let html = concat!( + /// "
    \n", + /// "
  • \n", + /// "a\n", + /// "
  • \n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` TaskListItem { checked: bool }, - /// A description list element. + /// A description list. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// ": orange\n", + /// "\n", + /// " citrus fruit\n", + /// ": apple\n", + /// "\n", + /// " malus fruit\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::DescriptionList, Attributes::new()), + /// Event::Start(Container::DescriptionTerm, Attributes::new()), + /// Event::Str("orange".into()), + /// Event::End(Container::DescriptionTerm), + /// Event::Blankline, + /// Event::Start(Container::DescriptionDetails, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("citrus fruit".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::DescriptionDetails), + /// Event::Start(Container::DescriptionTerm, Attributes::new()), + /// Event::Str("apple".into()), + /// Event::End(Container::DescriptionTerm), + /// Event::Blankline, + /// Event::Start(Container::DescriptionDetails, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("malus fruit".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::DescriptionDetails), + /// Event::End(Container::DescriptionList), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "
orange
\n", + /// "
\n", + /// "

citrus fruit

\n", + /// "
\n", + /// "
apple
\n", + /// "
\n", + /// "

malus fruit

\n", + /// "
\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` DescriptionList, /// Details describing a term within a description list. DescriptionDetails, /// A footnote definition. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "txt[^nb]\n", + /// "\n", + /// "[^nb]: actually..\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("txt".into()), + /// Event::FootnoteReference("nb".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::Start( + /// Container::Footnote { label: "nb" }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("actually..".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::Footnote { label: "nb" }), + /// ], + /// ); + /// let html = concat!( + /// "

txt1

\n", + /// "
\n", + /// "
\n", + /// "
    \n", + /// "
  1. \n", + /// "

    actually..↩\u{fe0e}

    \n", + /// "
  2. \n", + /// "
\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Footnote { label: &'s str }, /// A table element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "| a | b |\n", + /// "|---|--:|\n", + /// "| 1 | 2 |\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Table, Attributes::new()), + /// Event::Start( + /// Container::TableRow { head: true }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: true + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("a".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: true, + /// }), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Right, + /// head: true, + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("b".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Right, + /// head: true, + /// }), + /// Event::End(Container::TableRow { head: true } ), + /// Event::Start( + /// Container::TableRow { head: false }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: false + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("1".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: false, + /// }), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Right, + /// head: false, + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("2".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Right, + /// head: false, + /// }), + /// Event::End(Container::TableRow { head: false } ), + /// Event::End(Container::Table), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "
ab
12
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Table, /// A row element of a table. TableRow { head: bool }, /// A section belonging to a top level heading. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "# outer\n", + /// "\n", + /// "## inner\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::Section { id: "outer".into() }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::Heading { + /// level: 1, + /// has_section: true, + /// id: "outer".into(), + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("outer".into()), + /// Event::End(Container::Heading { + /// level: 1, + /// has_section: true, + /// id: "outer".into(), + /// }), + /// Event::Blankline, + /// Event::Start( + /// Container::Section { id: "inner".into() }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::Heading { + /// level: 2, + /// has_section: true, + /// id: "inner".into(), + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("inner".into()), + /// Event::End(Container::Heading { + /// level: 2, + /// has_section: true, + /// id: "inner".into(), + /// }), + /// Event::End(Container::Section { id: "inner".into() }), + /// Event::End(Container::Section { id: "outer".into() }), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "

outer

\n", + /// "
\n", + /// "

inner

\n", + /// "
\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Section { id: CowStr<'s> }, /// A block-level divider element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "::: note\n", + /// "this is a note\n", + /// ":::\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::Div { class: "note" }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("this is a note".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::Div { class: "note" }), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "

this is a note

\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Div { class: &'s str }, /// A paragraph. Paragraph, /// A heading. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "# heading"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::Section { id: "heading".into() }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::Heading { + /// level: 1, + /// has_section: true, + /// id: "heading".into(), + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("heading".into()), + /// Event::End(Container::Heading { + /// level: 1, + /// has_section: true, + /// id: "heading".into(), + /// }), + /// Event::End(Container::Section { id: "heading".into() }), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "

heading

\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Heading { level: u16, has_section: bool, @@ -284,41 +1143,585 @@ pub enum Container<'s> { /// A cell element of row within a table. TableCell { alignment: Alignment, head: bool }, /// A caption within a table. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "|a|\n", + /// "^ caption\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Table, Attributes::new()), + /// Event::Start(Container::Caption, Attributes::new()), + /// Event::Str("caption".into()), + /// Event::End(Container::Caption), + /// Event::Start( + /// Container::TableRow { head: false }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: false + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("a".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: false, + /// }), + /// Event::End(Container::TableRow { head: false } ), + /// Event::End(Container::Table), + /// ], + /// ); + /// let html = concat!( + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "
caption
a
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Caption, /// A term within a description list. DescriptionTerm, /// A link definition. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "[label]: url"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::LinkDefinition { label: "label" }, + /// Attributes::new(), + /// ), + /// Event::Str("url".into()), + /// Event::End(Container::LinkDefinition { label: "label" }), + /// ], + /// ); + /// let html = "\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` LinkDefinition { label: &'s str }, /// A block with raw markup for a specific output format. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "```=html\n", + /// "x\n", + /// "```\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::RawBlock { format: "html" }, + /// Attributes::new(), + /// ), + /// Event::Str("x".into()), + /// Event::End(Container::RawBlock { format: "html" }), + /// ], + /// ); + /// let html = "x\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` RawBlock { format: &'s str }, /// A block with code in a specific language. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "```html\n", + /// "x\n", + /// "```\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::CodeBlock { language: "html" }, + /// Attributes::new(), + /// ), + /// Event::Str("x\n".into()), + /// Event::End(Container::CodeBlock { language: "html" }), + /// ], + /// ); + /// let html = concat!( + /// "
<tag>x</tag>\n",
+    ///     "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` CodeBlock { language: &'s str }, /// An inline divider element. + /// + /// # Examples + /// + /// Can be used to add attributes: + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "word{#a}\n", + /// "[two words]{#b}\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Span, + /// [(AttributeKind::Id, "a".into())].into_iter().collect(), + /// ), + /// Event::Str("word".into()), + /// Event::End(Container::Span), + /// Event::Softbreak, + /// Event::Start( + /// Container::Span, + /// [(AttributeKind::Id, "b".into())].into_iter().collect(), + /// ), + /// Event::Str("two words".into()), + /// Event::End(Container::Span), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

word\n", + /// "two words

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Span, /// An inline link, the first field is either a destination URL or an unresolved tag. + /// + /// # Examples + /// + /// URLs or email addresses can be enclosed with angled brackets to create a hyperlink: + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "\n", + /// "\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Link( + /// "https://example.com".into(), + /// LinkType::AutoLink, + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("https://example.com".into()), + /// Event::End(Container::Link( + /// "https://example.com".into(), + /// LinkType::AutoLink, + /// )), + /// Event::Softbreak, + /// Event::Start( + /// Container::Link( + /// "me@example.com".into(), + /// LinkType::Email, + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("me@example.com".into()), + /// Event::End(Container::Link( + /// "me@example.com".into(), + /// LinkType::Email, + /// )), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

https://example.com\n", + /// "me@example.com

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` + /// + /// Anchor text and the URL can be specified inline: + /// + /// ``` + /// # use jotdown::*; + /// let src = "[anchor](url)\n"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Link( + /// "url".into(), + /// LinkType::Span(SpanLinkType::Inline), + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("anchor".into()), + /// Event::End( + /// Container::Link("url".into(), + /// LinkType::Span(SpanLinkType::Inline)), + /// ), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

anchor

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` + /// + /// Alternatively, the URL can be retrieved from a link definition using hard brackets, if it + /// exists: + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "[a][label]\n", + /// "[b][non-existent]\n", + /// "\n", + /// "[label]: url\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Link( + /// "url".into(), + /// LinkType::Span(SpanLinkType::Reference), + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("a".into()), + /// Event::End( + /// Container::Link("url".into(), + /// LinkType::Span(SpanLinkType::Reference)), + /// ), + /// Event::Softbreak, + /// Event::Start( + /// Container::Link( + /// "non-existent".into(), + /// LinkType::Span(SpanLinkType::Unresolved), + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("b".into()), + /// Event::End( + /// Container::Link("non-existent".into(), + /// LinkType::Span(SpanLinkType::Unresolved)), + /// ), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::Start( + /// Container::LinkDefinition { label: "label" }, + /// Attributes::new(), + /// ), + /// Event::Str("url".into()), + /// Event::End(Container::LinkDefinition { label: "label" }), + /// ], + /// ); + /// let html = concat!( + /// "

a\n", + /// "b

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Link(CowStr<'s>, LinkType), - /// An inline image, the first field is either a destination URL or an unresolved tag. Inner - /// Str objects compose the alternative text. + /// An inline image, the first field is either a destination URL or an unresolved tag. + /// + /// # Examples + /// + /// Inner Str objects compose the alternative text: + /// + /// ``` + /// # use jotdown::*; + /// let src = "![alt text](img.png)"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Image("img.png".into(), SpanLinkType::Inline), + /// Attributes::new(), + /// ), + /// Event::Str("alt text".into()), + /// Event::End( + /// Container::Image("img.png".into(), SpanLinkType::Inline), + /// ), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

\"alt

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Image(CowStr<'s>, SpanLinkType), /// An inline verbatim string. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "inline `verbatim`"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("inline ".into()), + /// Event::Start(Container::Verbatim, Attributes::new()), + /// Event::Str("verbatim".into()), + /// Event::End(Container::Verbatim), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

inline verbatim

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Verbatim, /// An inline or display math element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "inline $`a\\cdot{}b` or\n", + /// "display $$`\\frac{a}{b}`\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("inline ".into()), + /// Event::Start( + /// Container::Math { display: false }, + /// Attributes::new(), + /// ), + /// Event::Str(r"a\cdot{}b".into()), + /// Event::End(Container::Math { display: false }), + /// Event::Str(" or".into()), + /// Event::Softbreak, + /// Event::Str("display ".into()), + /// Event::Start( + /// Container::Math { display: true }, + /// Attributes::new(), + /// ), + /// Event::Str(r"\frac{a}{b}".into()), + /// Event::End(Container::Math { display: true }), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

inline \\(a\\cdot{}b\\) or\n", + /// "display \\[\\frac{a}{b}\\]

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Math { display: bool }, /// Inline raw markup for a specific output format. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "`a`{=html}"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::RawInline { format: "html" }, Attributes::new(), + /// ), + /// Event::Str("a".into()), + /// Event::End(Container::RawInline { format: "html" }), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

a

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` RawInline { format: &'s str }, /// A subscripted element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "~SUB~"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Subscript, Attributes::new()), + /// Event::Str("SUB".into()), + /// Event::End(Container::Subscript), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

SUB

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Subscript, /// A superscripted element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "^SUP^"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Superscript, Attributes::new()), + /// Event::Str("SUP".into()), + /// Event::End(Container::Superscript), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

SUP

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Superscript, /// An inserted inline element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "{+INS+}"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Insert, Attributes::new()), + /// Event::Str("INS".into()), + /// Event::End(Container::Insert), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

INS

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Insert, /// A deleted inline element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "{-DEL-}"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Delete, Attributes::new()), + /// Event::Str("DEL".into()), + /// Event::End(Container::Delete), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

DEL

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Delete, /// An inline element emphasized with a bold typeface. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "*STRONG*"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Strong, Attributes::new()), + /// Event::Str("STRONG".into()), + /// Event::End(Container::Strong), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

STRONG

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Strong, /// An emphasized inline element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "_EM_"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Emphasis, Attributes::new()), + /// Event::Str("EM".into()), + /// Event::End(Container::Emphasis), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

EM

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Emphasis, /// A highlighted inline element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "{=MARK=}"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Mark, Attributes::new()), + /// Event::Str("MARK".into()), + /// Event::End(Container::Mark), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

MARK

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Mark, } From b17e86912e0d290f1f7dd3dcd60ea050507fc60d Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 22:49:06 +0200 Subject: [PATCH 21/25] lib: block attrs cannot be separated with blank line previously e.g. {.a} {.b} para would result in

para

and now

para

For later commit when the "Attributes" event is added: This allows us to emit Attributes events when we encounter a blank line instead of buffering all Attributes/blank lines until we encounter an actual block or end of file and only then choosing whether to attach them to a block or create standalone Attributes events. And if we dont want to emit out of order events we would also need to buffer the blank lines.. This also prevents standalone comments from being attached to the next block (if separated by blank line). --- src/lib.rs | 28 ++++++++++++++++++++++++---- tests/afl/src/lib.rs | 11 ----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 51a82ca..2ea2d2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2217,10 +2217,8 @@ impl<'s> Parser<'s> { /// Generally, the range of each event does not overlap with any other event and the ranges are /// in same order as the events are emitted, i.e. the start offset of an event must be greater /// or equal to the (exclusive) end offset of all events that were emitted before that event. - /// However, there are some exceptions to this rule: + /// However, there is an exception to this rule: /// - /// - Blank lines inbetween block attributes and the block causes the blankline events to - /// overlap with the block start event. /// - Caption events are emitted before the table rows while the input for the caption content /// is located after the table rows, causing the ranges to be out of order. /// @@ -2441,7 +2439,11 @@ impl<'s> Parser<'s> { while let Some(mut ev) = self.blocks.next() { let event = match ev.kind { block::EventKind::Atom(a) => match a { - block::Atom::Blankline => Event::Blankline, + block::Atom::Blankline => { + self.block_attributes_pos = None; + self.block_attributes.clear(); + Event::Blankline + } block::Atom::ThematicBreak => { if let Some(pos) = self.block_attributes_pos.take() { ev.span.start = pos; @@ -3684,6 +3686,24 @@ mod test { ); } + #[test] + fn attr_block_blankline() { + test_parse!( + "{.a}\n\n{.b}\n\n{.c}\npara", + (Blankline, "\n"), + (Blankline, "\n"), + ( + Start( + Paragraph, + [(AttributeKind::Class, "c")].into_iter().collect(), + ), + "{.c}\n", + ), + (Str("para".into()), "para"), + (End(Paragraph), ""), + ); + } + #[test] fn attr_inline() { test_parse!( diff --git a/tests/afl/src/lib.rs b/tests/afl/src/lib.rs index 697ba74..591f46c 100644 --- a/tests/afl/src/lib.rs +++ b/tests/afl/src/lib.rs @@ -15,17 +15,6 @@ pub fn parse(data: &[u8]) { // no overlap, out of order assert!( last.1.end <= range.start - // block attributes may overlap with start event - || ( - matches!(last.0, jotdown::Event::Blankline) - && ( - matches!( - event, - jotdown::Event::Start(ref cont, ..) if cont.is_block() - ) - || matches!(event, jotdown::Event::ThematicBreak(..)) - ) - ) // caption event is before table rows but src is after || ( matches!( From 7c10e5a39a71aa267bf65f3829f27b569cbcf7fb Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Tue, 20 Aug 2024 23:15:23 +0200 Subject: [PATCH 22/25] lib: use single option for block attrs+span avoid duplicate check --- src/lib.rs | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2ea2d2e..38097dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1966,8 +1966,7 @@ pub struct Parser<'s> { pre_pass: PrePass<'s>, /// Last parsed block attributes, and its starting offset. - block_attributes: Attributes<'s>, - block_attributes_pos: Option, + block_attributes: Option<(Attributes<'s>, usize)>, /// Current table row is a head row. table_head_row: bool, @@ -2203,8 +2202,7 @@ impl<'s> Parser<'s> { src, blocks: blocks.into_iter().peekable(), pre_pass, - block_attributes: Attributes::new(), - block_attributes_pos: None, + block_attributes: None, table_head_row: false, verbatim: false, inline_parser, @@ -2440,23 +2438,27 @@ impl<'s> Parser<'s> { let event = match ev.kind { block::EventKind::Atom(a) => match a { block::Atom::Blankline => { - self.block_attributes_pos = None; - self.block_attributes.clear(); + self.block_attributes = None; Event::Blankline } block::Atom::ThematicBreak => { - if let Some(pos) = self.block_attributes_pos.take() { + let attrs = if let Some((attrs, pos)) = self.block_attributes.take() { ev.span.start = pos; - } - Event::ThematicBreak(self.block_attributes.take()) + attrs + } else { + Attributes::new() + }; + Event::ThematicBreak(attrs) } block::Atom::Attributes => { - if self.block_attributes_pos.is_none() { - self.block_attributes_pos = Some(ev.span.start); - } - self.block_attributes + let (mut attrs, pos) = self + .block_attributes + .take() + .unwrap_or_else(|| (Attributes::new(), ev.span.start)); + attrs .parse(&self.src[ev.span.clone()]) .expect("should be valid"); + self.block_attributes = Some((attrs, pos)); continue; } }, @@ -2550,13 +2552,15 @@ impl<'s> Parser<'s> { }, }; if enter { - if let Some(pos) = self.block_attributes_pos.take() { + let attrs = if let Some((attrs, pos)) = self.block_attributes.take() { ev.span.start = pos; - } - Event::Start(cont, self.block_attributes.take()) + attrs + } else { + Attributes::new() + }; + Event::Start(cont, attrs) } else { - self.block_attributes = Attributes::new(); - self.block_attributes_pos = None; + self.block_attributes = None; Event::End(cont) } } From f28812ebebdff40602f3927a0b3d9c5eed7cf4c3 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 20:33:20 +0200 Subject: [PATCH 23/25] inline: emit Empty event for dangling attrs e.g. 'word {.a}' --- src/inline.rs | 150 ++++++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 5 ++ 2 files changed, 132 insertions(+), 23 deletions(-) diff --git a/src/inline.rs b/src/inline.rs index 9dacf24..58a3f52 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -61,6 +61,7 @@ pub enum EventKind<'s> { Exit(Container<'s>), Atom(Atom<'s>), Str, + Empty, // dummy to hold attributes Attributes { container: bool, attrs: AttributesIndex, @@ -557,6 +558,8 @@ impl<'s> Parser<'s> { } AttributesElementType::Word => { self.events.push_back(attr_event); + // push for now, pop later if attrs attached to word + self.push(EventKind::Empty); } } } @@ -988,18 +991,22 @@ impl<'s> Parser<'s> { } } else { let attr = self.events.pop_front().unwrap(); - self.events.push_front(Event { - kind: EventKind::Exit(Span), - span: attr.span.clone(), - }); - self.events.push_front(Event { - kind: EventKind::Str, - span: span_str.clone(), - }); - self.events.push_front(Event { - kind: EventKind::Enter(Span), - span: span_str.start..span_str.start, - }); + if !span_str.is_empty() { + let empty = self.events.pop_front(); + debug_assert_eq!(empty.unwrap().kind, EventKind::Empty); + self.events.push_front(Event { + kind: EventKind::Exit(Span), + span: attr.span.clone(), + }); + self.events.push_front(Event { + kind: EventKind::Str, + span: span_str.clone(), + }); + self.events.push_front(Event { + kind: EventKind::Enter(Span), + span: span_str.start..span_str.start, + }); + } attr } } @@ -1187,12 +1194,20 @@ impl<'s> Iterator for Parser<'s> { } self.events.pop_front().and_then(|e| match e.kind { - EventKind::Str if e.span.is_empty() => self.next(), + EventKind::Str + if e.span.is_empty() + && !matches!( + self.events.front().map(|ev| &ev.kind), + Some(EventKind::Attributes { + container: false, + .. + }) + ) => + { + self.next() + } EventKind::Str => Some(self.merge_str_events(e.span)), - EventKind::Placeholder - | EventKind::Attributes { - container: false, .. - } => self.next(), + EventKind::Placeholder => self.next(), _ => Some(e), }) } @@ -1574,7 +1589,19 @@ mod test { (Str, "abc"), (Exit(Span), "]{.def}"), ); - test_parse!("not a [span] {#id}.", (Str, "not a [span] "), (Str, ".")); + test_parse!( + "not a [span] {#id}.", + (Str, "not a [span] "), + ( + Attributes { + container: false, + attrs: 0, + }, + "{#id}", + ), + (Empty, "{#id}"), + (Str, "."), + ); } #[test] @@ -1765,6 +1792,14 @@ mod test { (Str, "abc def"), (Exit(Emphasis), "_{.a}{.b}{.c}"), (Str, " "), + ( + Attributes { + container: false, + attrs: 1, + }, + "{.d}", + ), + (Empty, "{.d}"), ); } @@ -1819,10 +1854,71 @@ mod test { #[test] fn attr_whitespace() { - test_parse!("word {%comment%}", (Str, "word ")); - test_parse!("word {%comment%} word", (Str, "word "), (Str, " word")); - test_parse!("word {a=b}", (Str, "word ")); - test_parse!("word {.d}", (Str, "word ")); + test_parse!( + "word {%comment%}", + (Str, "word "), + ( + Attributes { + container: false, + attrs: 0, + }, + "{%comment%}", + ), + (Empty, "{%comment%}"), + ); + test_parse!( + "word {%comment%} word", + (Str, "word "), + ( + Attributes { + container: false, + attrs: 0 + }, + "{%comment%}", + ), + (Empty, "{%comment%}"), + (Str, " word"), + ); + test_parse!( + "word {a=b}", + (Str, "word "), + ( + Attributes { + container: false, + attrs: 0, + }, + "{a=b}", + ), + (Empty, "{a=b}"), + ); + test_parse!( + " {a=b}", + (Str, " "), + ( + Attributes { + container: false, + attrs: 0, + }, + "{a=b}", + ), + (Empty, "{a=b}"), + ); + } + + #[test] + fn attr_start() { + test_parse!( + "{a=b} word", + ( + Attributes { + container: false, + attrs: 0, + }, + "{a=b}", + ), + (Empty, "{a=b}"), + (Str, " word"), + ); } #[test] @@ -1904,7 +2000,15 @@ mod test { left: false }), "'" - ) + ), + ( + Attributes { + container: false, + attrs: 0, + }, + "{.b}", + ), + (Empty, "{.b}"), ); } } diff --git a/src/lib.rs b/src/lib.rs index 38097dd..36a0bd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2416,6 +2416,11 @@ impl<'s> Parser<'s> { inline::Atom::Hardbreak => Event::Hardbreak, inline::Atom::Escape => Event::Escape, }, + inline::EventKind::Empty => { + debug_assert!(!attributes.is_empty()); + attributes.clear(); + Event::Escape // TODO replace dummy + } inline::EventKind::Str => Event::Str(self.src[inline.span.clone()].into()), inline::EventKind::Attributes { .. } | inline::EventKind::Placeholder => { panic!("{:?}", inline) From 43a9464988eb6a9d1ec00be472f8c291b152b061 Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sat, 10 Aug 2024 20:47:59 +0200 Subject: [PATCH 24/25] lib: emit Attributes event for dangling attrs --- src/html.rs | 2 +- src/lib.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 156 insertions(+), 14 deletions(-) diff --git a/src/html.rs b/src/html.rs index 073aa97..3bfe56b 100644 --- a/src/html.rs +++ b/src/html.rs @@ -409,7 +409,7 @@ impl<'s> Writer<'s> { Event::NonBreakingSpace => out.write_str(" ")?, Event::Hardbreak => out.write_str("
\n")?, Event::Softbreak => out.write_char('\n')?, - Event::Escape | Event::Blankline => {} + Event::Escape | Event::Blankline | Event::Attributes(..) => {} Event::ThematicBreak(attrs) => { if self.not_first_line { out.write_char('\n')?; diff --git a/src/lib.rs b/src/lib.rs index 36a0bd8..227ef49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -629,6 +629,52 @@ pub enum Event<'s> { /// assert_eq!(&html::render_to_string(events.into_iter()), html); /// ``` ThematicBreak(Attributes<'s>), + /// Dangling attributes not attached to anything. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "{#a}\n", + /// "\n", + /// "inline {#b}\n", + /// "\n", + /// "{#c}\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Attributes( + /// [(AttributeKind::Id, "a".into())] + /// .into_iter() + /// .collect(), + /// ), + /// Event::Blankline, + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("inline ".into()), + /// Event::Attributes( + /// [(AttributeKind::Id, "b".into())] + /// .into_iter() + /// .collect(), + /// ), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::Attributes( + /// [(AttributeKind::Id, "c".into())] + /// .into_iter() + /// .collect(), + /// ), + /// ], + /// ); + /// let html = concat!( + /// "\n", + /// "

inline

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` + Attributes(Attributes<'s>), } /// A container that may contain other elements. @@ -1965,8 +2011,8 @@ pub struct Parser<'s> { /// Contents obtained by the prepass. pre_pass: PrePass<'s>, - /// Last parsed block attributes, and its starting offset. - block_attributes: Option<(Attributes<'s>, usize)>, + /// Last parsed block attributes, and its span. + block_attributes: Option<(Attributes<'s>, Range)>, /// Current table row is a head row. table_head_row: bool, @@ -2418,8 +2464,7 @@ impl<'s> Parser<'s> { }, inline::EventKind::Empty => { debug_assert!(!attributes.is_empty()); - attributes.clear(); - Event::Escape // TODO replace dummy + Event::Attributes(attributes.take()) } inline::EventKind::Str => Event::Str(self.src[inline.span.clone()].into()), inline::EventKind::Attributes { .. } | inline::EventKind::Placeholder => { @@ -2443,12 +2488,12 @@ impl<'s> Parser<'s> { let event = match ev.kind { block::EventKind::Atom(a) => match a { block::Atom::Blankline => { - self.block_attributes = None; + debug_assert_eq!(self.block_attributes, None); Event::Blankline } block::Atom::ThematicBreak => { - let attrs = if let Some((attrs, pos)) = self.block_attributes.take() { - ev.span.start = pos; + let attrs = if let Some((attrs, span)) = self.block_attributes.take() { + ev.span.start = span.start; attrs } else { Attributes::new() @@ -2456,14 +2501,20 @@ impl<'s> Parser<'s> { Event::ThematicBreak(attrs) } block::Atom::Attributes => { - let (mut attrs, pos) = self + let (mut attrs, span) = self .block_attributes .take() - .unwrap_or_else(|| (Attributes::new(), ev.span.start)); + .unwrap_or_else(|| (Attributes::new(), ev.span.clone())); attrs .parse(&self.src[ev.span.clone()]) .expect("should be valid"); - self.block_attributes = Some((attrs, pos)); + if matches!( + self.blocks.peek().map(|e| &e.kind), + Some(block::EventKind::Atom(block::Atom::Blankline)) + ) { + return Some((Event::Attributes(attrs), span)); + } + self.block_attributes = Some((attrs, span)); continue; } }, @@ -2557,8 +2608,8 @@ impl<'s> Parser<'s> { }, }; if enter { - let attrs = if let Some((attrs, pos)) = self.block_attributes.take() { - ev.span.start = pos; + let attrs = if let Some((attrs, span)) = self.block_attributes.take() { + ev.span.start = span.start; attrs } else { Attributes::new() @@ -2591,7 +2642,11 @@ impl<'s> Parser<'s> { } fn next_span(&mut self) -> Option<(Event<'s>, Range)> { - self.inline().or_else(|| self.block()) + self.inline().or_else(|| self.block()).or_else(|| { + self.block_attributes + .take() + .map(|(attrs, span)| (Event::Attributes(attrs), span)) + }) } } @@ -3695,11 +3750,41 @@ mod test { ); } + #[test] + fn attr_block_dangling() { + test_parse!( + "{.a}", + ( + Attributes([(AttributeKind::Class, "a")].into_iter().collect()), + "{.a}", + ), + ); + test_parse!( + "para\n\n{.a}", + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Attributes([(AttributeKind::Class, "a")].into_iter().collect()), + "{.a}", + ), + ); + } + #[test] fn attr_block_blankline() { test_parse!( "{.a}\n\n{.b}\n\n{.c}\npara", + ( + Attributes([(AttributeKind::Class, "a")].into_iter().collect()), + "{.a}\n", + ), (Blankline, "\n"), + ( + Attributes([(AttributeKind::Class, "b")].into_iter().collect()), + "{.b}\n", + ), (Blankline, "\n"), ( Start( @@ -3977,6 +4062,63 @@ mod test { ); } + #[test] + fn attr_inline_dangling() { + test_parse!( + "*a\n{%}", + (Start(Paragraph, Attributes::new()), ""), + (Str("*a".into()), "*a"), + (Softbreak, "\n"), + ( + Attributes([(AttributeKind::Comment, "")].into_iter().collect()), + "{%}", + ), + (End(Paragraph), ""), + ); + test_parse!( + "a {.b} c", + (Start(Paragraph, Attributes::new()), ""), + (Str("a ".into()), "a "), + ( + Attributes([(AttributeKind::Class, "b")].into_iter().collect()), + "{.b}", + ), + (Str(" c".into()), " c"), + (End(Paragraph), ""), + ); + test_parse!( + "a {%cmt} c", + (Start(Paragraph, Attributes::new()), ""), + (Str("a ".into()), "a "), + ( + Attributes([(AttributeKind::Comment, "cmt")].into_iter().collect()), + "{%cmt}", + ), + (Str(" c".into()), " c"), + (End(Paragraph), ""), + ); + test_parse!( + "a {%cmt}", + (Start(Paragraph, Attributes::new()), ""), + (Str("a ".into()), "a "), + ( + Attributes([(AttributeKind::Comment, "cmt")].into_iter().collect()), + "{%cmt}", + ), + (End(Paragraph), ""), + ); + test_parse!( + "{%cmt} a", + (Start(Paragraph, Attributes::new()), ""), + ( + Attributes([(AttributeKind::Comment, "cmt")].into_iter().collect()), + "{%cmt}", + ), + (Str(" a".into()), " a"), + (End(Paragraph), ""), + ); + } + #[test] fn list_item_unordered() { test_parse!( From 0c4fa46dfac78e2f035f4d69c262d498f79f4c9f Mon Sep 17 00:00:00 2001 From: Noah Hellman Date: Sun, 18 Aug 2024 21:19:42 +0200 Subject: [PATCH 25/25] html: match class-other order of ref impl --- src/html.rs | 97 +++++++++++++++++--------------- tests/html-ut/skip | 1 - tests/html-ut/ut/attributes.test | 13 ++++- 3 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/html.rs b/src/html.rs index 3bfe56b..d2e454d 100644 --- a/src/html.rs +++ b/src/html.rs @@ -208,9 +208,19 @@ impl<'s> Writer<'s> { Container::LinkDefinition { .. } => return Ok(()), } - for (a, v) in attrs.unique_pairs().filter(|(a, _)| *a != "class") { + let mut id_written = false; + let mut class_written = false; + for (a, v) in attrs.unique_pairs() { write!(out, r#" {}=""#, a)?; v.parts().try_for_each(|part| write_attr(part, &mut out))?; + match a { + "class" => { + class_written = true; + write_class(c, true, &mut out)?; + } + "id" => id_written = true, + _ => {} + } out.write_char('"')?; } @@ -221,59 +231,25 @@ impl<'s> Writer<'s> { } | Container::Section { id } = &c { - if !attrs.unique_pairs().any(|(a, _)| a == "id") { + if !id_written { out.write_str(r#" id=""#)?; write_attr(id, &mut out)?; out.write_char('"')?; } - } - - if attrs.unique_pairs().any(|(a, _)| a == "class") + } else if (matches!(c, Container::Div { class } if !class.is_empty()) || matches!( c, - Container::Div { class } if !class.is_empty()) - || matches!(c, |Container::Math { .. }| Container::List { - kind: ListKind::Task, - .. - } | Container::TaskListItem { .. }) + Container::Math { .. } + | Container::List { + kind: ListKind::Task, + .. + } + | Container::TaskListItem { .. } + )) + && !class_written { out.write_str(r#" class=""#)?; - let mut first_written = false; - if let Some(cls) = match c { - Container::List { - kind: ListKind::Task, - .. - } => Some("task-list"), - Container::TaskListItem { checked: false } => Some("unchecked"), - Container::TaskListItem { checked: true } => Some("checked"), - Container::Math { display: false } => Some("math inline"), - Container::Math { display: true } => Some("math display"), - _ => None, - } { - first_written = true; - out.write_str(cls)?; - } - for cls in attrs - .unique_pairs() - .filter(|(a, _)| a == &"class") - .map(|(_, cls)| cls) - { - if first_written { - out.write_char(' ')?; - } - first_written = true; - cls.parts() - .try_for_each(|part| write_attr(part, &mut out))?; - } - // div class goes after classes from attrs - if let Container::Div { class } = c { - if !class.is_empty() { - if first_written { - out.write_char(' ')?; - } - out.write_str(class)?; - } - } + write_class(c, false, &mut out)?; out.write_char('"')?; } @@ -473,6 +449,35 @@ impl<'s> Writer<'s> { } } +fn write_class(c: &Container, mut first_written: bool, out: &mut W) -> std::fmt::Result +where + W: std::fmt::Write, +{ + if let Some(cls) = match c { + Container::List { + kind: ListKind::Task, + .. + } => Some("task-list"), + Container::TaskListItem { checked: false } => Some("unchecked"), + Container::TaskListItem { checked: true } => Some("checked"), + Container::Math { display: false } => Some("math inline"), + Container::Math { display: true } => Some("math display"), + _ => None, + } { + first_written = true; + out.write_str(cls)?; + } + if let Container::Div { class } = c { + if !class.is_empty() { + if first_written { + out.write_char(' ')?; + } + out.write_str(class)?; + } + } + Ok(()) +} + fn write_text(s: &str, out: W) -> std::fmt::Result where W: std::fmt::Write, diff --git a/tests/html-ut/skip b/tests/html-ut/skip index 52e70b0..b25d688 100644 --- a/tests/html-ut/skip +++ b/tests/html-ut/skip @@ -1,6 +1,5 @@ 38d85f9:multi-line block attributes 6c14561:multi-line block attributes -f4f22fc:attribute key class order ae6fc15:bugged left/right quote 168469a:bugged left/right quote 2fa94d1:bugged left/right quote diff --git a/tests/html-ut/ut/attributes.test b/tests/html-ut/ut/attributes.test index 8dc1033..07a64f3 100644 --- a/tests/html-ut/ut/attributes.test +++ b/tests/html-ut/ut/attributes.test @@ -3,5 +3,16 @@ Classes should be concatenated ``` word{.a #a class=b #b .c} . -

word

+

word

+``` + +Automatic and explicit classes should be merged correctly + +``` +{.a} +::: b +::: +. +
+
```