diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f24d5..53296cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v9.0.0 - Unreleased + +- Added support for inserts, deletes, and marks. +- Added support for nested lists. +- Added support for ordered lists. +- Improved support for spans with attributes. + ## v8.0.0 - 2025-11-28 - Added support for inline attributes on links and images. diff --git a/README.md b/README.md index 8a7d038..29ded53 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ Further documentation can be found at . This project is a work in progress. So far it supports: +- [x] Autolinks (email and URL) - [x] Block attributes -- [x] Bullet lists without nesting -- [x] Code blocks - [x] Block quotes +- [x] Code blocks - [x] Content escaping - [x] Div - [x] Emphasis and strong @@ -48,15 +48,16 @@ This project is a work in progress. So far it supports: - [x] Headings - [x] Images (with attributes support) - [x] Inline code +- [x] Inserts, deletes, and marks. - [x] Links (inline and reference, with attributes support) -- [x] Autolinks (email and URL) -- [x] Lists (without nesting) - [x] Manual line breaks - [x] Maths (inline and display) +- [x] Non-breaking spaces +- [x] Ordered lists with the `1.`, `1)`, and `(1)` syntaxes - [x] Paragraphs - [x] Raw blocks +- [x] Smart replacing of `...` with ellipsis +- [x] Smart replacing of hyphens with dashes - [x] Span with attributes - [x] Thematic breaks -- [x] Smart replacing of hyphens with dashes -- [x] Smart replacing of `...` with ellipsis -- [x] Non-breaking spaces +- [x] Unordered lists diff --git a/gleam.toml b/gleam.toml index cf7f29d..bca9ef4 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "jot" -version = "8.0.0" +version = "9.0.0" gleam = ">= 1.5.0" description = "A parser for Djot, a markdown-like language" licences = ["Apache-2.0"] diff --git a/src/jot.gleam b/src/jot.gleam index 0ee33dd..076dba5 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -46,11 +46,45 @@ pub type Container { content: String, ) RawBlock(content: String) - BulletList(layout: ListLayout, style: String, items: List(List(Container))) + BulletList( + layout: ListLayout, + style: BulletStyle, + items: List(List(Container)), + ) + OrderedList( + layout: ListLayout, + punctuation: OrdinalPunctuation, + ordinal: OrdinalStyle, + start: Int, + items: List(List(Container)), + ) BlockQuote(attributes: Dict(String, String), items: List(Container)) Div(attributes: Dict(String, String), items: List(Container)) } +pub type BulletStyle { + BulletDash + BulletStar + BulletPlus +} + +pub type OrdinalPunctuation { + FullStop + SingleParen + DoubleParen +} + +pub type OrdinalStyle { + NumericOrdinal + LowerAlphaOrdinal + UpperAlphaOrdinal +} + +type ListStyle { + Bullet(BulletStyle) + Ordered(start: Int, punctuation: OrdinalPunctuation, style: OrdinalStyle) +} + pub type Inline { Linebreak NonBreakingSpace @@ -68,6 +102,9 @@ pub type Inline { Span(attributes: Dict(String, String), content: List(Inline)) Emphasis(content: List(Inline)) Strong(content: List(Inline)) + Delete(content: List(Inline)) + Insert(content: List(Inline)) + Mark(content: List(Inline)) Footnote(reference: String) Code(content: String) MathInline(content: String) @@ -145,6 +182,10 @@ pub fn parse(djot: String) -> Document { "--", "...", "<", + "{-", + "{+", + "{=", + "{", ]), link_destination: splitter.new([")", "]", "\n"]), math_end: splitter.new(["`"]), @@ -363,11 +404,17 @@ fn parse_container( #(in, refs, Some(block_quote), dict.new()) } - "-" as style <> in2 | "*" as style <> in2 -> { + "-" as style <> in2 | "*" as style <> in2 | "+" as style <> in2 -> { case parse_thematic_break(1, in2), in2 { None, " " <> in2 | None, "\n" <> in2 -> { + let bullet_style = case style { + "-" -> BulletDash + "*" -> BulletStar + _ -> BulletPlus + } + let style = Bullet(bullet_style) let #(list, in) = - parse_bullet_list(in2, refs, attrs, style, Tight, [], splitters) + parse_list(in2, refs, attrs, style, Tight, [], splitters) #(in, refs, Some(list), dict.new()) } None, _ -> { @@ -404,15 +451,12 @@ fn parse_container( #(in, refs, Some(paragraph), dict.new()) } Some(#(id, url, in)) -> { - let refs = - Refs( - ..refs, - urls: dict.insert(refs.urls, id, url), - url_attributes: case dict.is_empty(attrs) { - True -> refs.url_attributes - False -> dict.insert(refs.url_attributes, id, attrs) - }, - ) + let url_attributes = case dict.is_empty(attrs) { + True -> refs.url_attributes + False -> dict.insert(refs.url_attributes, id, attrs) + } + let urls = dict.insert(refs.urls, id, url) + let refs = Refs(..refs, urls:, url_attributes:) #(in, refs, None, dict.new()) } } @@ -425,20 +469,139 @@ fn parse_container( parse_paragraph(in, attrs, splitters, div_close_size) #(in, refs, Some(paragraph), dict.new()) } - Some(#(in, attrs, content)) -> #( - in, - refs, - Some(Div(attrs, content)), - dict.new(), - ) + Some(#(in, attrs, content)) -> { + let div = Some(Div(attrs, content)) + #(in, refs, div, dict.new()) + } + } + } + + "(" <> rest -> { + case parse_maybe_list(rest, refs, attrs, splitters, True) { + Some(#(in, refs, list)) -> #(in, refs, Some(list), dict.new()) + None -> { + let #(paragraph, in) = + parse_paragraph(in, attrs, splitters, div_close_size) + #(in, refs, Some(paragraph), dict.new()) + } } } _ -> { - let #(paragraph, in) = - parse_paragraph(in, attrs, splitters, div_close_size) - #(in, refs, Some(paragraph), dict.new()) + case parse_maybe_list(in, refs, attrs, splitters, False) { + Some(#(in, refs, list)) -> #(in, refs, Some(list), dict.new()) + None -> { + let #(paragraph, in) = + parse_paragraph(in, attrs, splitters, div_close_size) + #(in, refs, Some(paragraph), dict.new()) + } + } + } + } +} + +fn parse_maybe_list( + in: String, + refs: Refs, + attrs: Dict(String, String), + splitters: Splitters, + paren: Bool, +) -> Option(#(String, Refs, Container)) { + case in { + "0" <> _ + | "1" <> _ + | "2" <> _ + | "3" <> _ + | "4" <> _ + | "5" <> _ + | "6" <> _ + | "7" <> _ + | "8" <> _ + | "9" <> _ -> { + case parse_number_list(in, 0, paren) { + Some(#(punctuation, style, start, in)) -> { + let style = Ordered(start:, punctuation:, style:) + let #(list, in) = + parse_list(in, refs, attrs, style, Tight, [], splitters) + Some(#(in, refs, list)) + } + None -> None + } } + + "a" <> _ + | "b" <> _ + | "c" <> _ + | "d" <> _ + | "e" <> _ + | "f" <> _ + | "g" <> _ + | "h" <> _ + | "i" <> _ + | "j" <> _ + | "k" <> _ + | "l" <> _ + | "m" <> _ + | "n" <> _ + | "o" <> _ + | "p" <> _ + | "q" <> _ + | "r" <> _ + | "s" <> _ + | "t" <> _ + | "u" <> _ + | "v" <> _ + | "w" <> _ + | "x" <> _ + | "y" <> _ + | "z" <> _ -> + case parse_lower_list(in, 0, paren) { + Some(#(punctuation, style, start, in)) -> { + let style = Ordered(start:, punctuation:, style:) + let #(list, in) = + parse_list(in, refs, attrs, style, Tight, [], splitters) + Some(#(in, refs, list)) + } + None -> None + } + + "A" <> _ + | "B" <> _ + | "C" <> _ + | "D" <> _ + | "E" <> _ + | "F" <> _ + | "G" <> _ + | "H" <> _ + | "I" <> _ + | "J" <> _ + | "K" <> _ + | "L" <> _ + | "M" <> _ + | "N" <> _ + | "O" <> _ + | "P" <> _ + | "Q" <> _ + | "R" <> _ + | "S" <> _ + | "T" <> _ + | "U" <> _ + | "V" <> _ + | "W" <> _ + | "X" <> _ + | "Y" <> _ + | "Z" <> _ -> + case parse_upper_list(in, 0, paren) { + Some(#(punctuation, style, start, in)) -> { + let style = Ordered(start:, punctuation:, style:) + let #(list, in) = + parse_list(in, refs, attrs, style, Tight, [], splitters) + Some(#(in, refs, list)) + } + None -> None + } + + _ -> None } } @@ -843,7 +1006,7 @@ fn parse_attributes_end( "" -> Some(#(attrs, "")) "\n" <> in -> Some(#(attrs, in)) " " <> in -> parse_attributes_end(in, attrs) - _ -> None + _ -> Some(#(attrs, in)) } } @@ -1210,6 +1373,46 @@ fn parse_inline( } } + // Delete + #(a, "{-", in) -> { + let text = text <> a + case parse_insert_delete_mark(in, splitters, "-}") { + None -> parse_inline(in, splitters, text <> "{-", acc) + Some(#(inner, in)) -> + parse_inline(in, splitters, "", [Delete(inner), Text(text), ..acc]) + } + } + + // Insert + #(a, "{+", in) -> { + let text = text <> a + case parse_insert_delete_mark(in, splitters, "+}") { + None -> parse_inline(in, splitters, text <> "{+", acc) + Some(#(inner, in)) -> + parse_inline(in, splitters, "", [Insert(inner), Text(text), ..acc]) + } + } + + // Mark + #(a, "{=", in) -> { + let text = text <> a + case parse_insert_delete_mark(in, splitters, "=}") { + None -> parse_inline(in, splitters, text <> "{=", acc) + Some(#(inner, in)) -> + parse_inline(in, splitters, "", [Mark(inner), Text(text), ..acc]) + } + } + + // Standalone attributes (they are discarded) + #(a, "{", in) -> { + let text = text <> a + case parse_attributes(in, dict.new()) { + None -> parse_inline(in, splitters, text <> "{", acc) + Some(#(_attrs, in)) -> + parse_inline(in, splitters, "", [Text(text), ..acc]) + } + } + #(text2, text3, in) -> case text <> text2 <> text3 { "" -> #(list.reverse(acc), in) @@ -1363,6 +1566,21 @@ fn take_emphasis_chars( } } +fn parse_insert_delete_mark( + in: String, + splitters: Splitters, + close: String, +) -> Option(#(List(Inline), String)) { + case string.split_once(in, close) { + Error(_) -> None + Ok(#(inline_in, rest)) -> { + let #(inline, inline_in_remaining) = + parse_inline(inline_in, splitters, "", []) + Some(#(inline, inline_in_remaining <> rest)) + } + } +} + fn parse_link_or_recover( in: String, splitters: Splitters, @@ -1581,8 +1799,11 @@ fn take_inline_text(inlines: List(Inline), acc: String) -> String { NonBreakingSpace -> take_inline_text(rest, acc <> " ") Text(text) | Code(text) | MathInline(text) | MathDisplay(text) -> take_inline_text(rest, acc <> text) - Strong(inlines) | Emphasis(inlines) -> - take_inline_text(list.append(inlines, rest), acc) + Strong(inlines) + | Emphasis(inlines) + | Delete(inlines) + | Insert(inlines) + | Mark(inlines) -> take_inline_text(list.append(inlines, rest), acc) Link(_, nested, _) | Image(_, nested, _) | Span(_, nested) -> { let acc = take_inline_text(nested, acc) take_inline_text(rest, acc) @@ -1606,21 +1827,29 @@ fn parse_paragraph( #(Paragraph(attrs, inline), inline_in_remaining <> in) } -fn parse_bullet_list( +fn parse_list( in: String, refs: Refs, attrs: Dict(String, String), - style: String, + style: ListStyle, layout: ListLayout, items: List(List(Container)), splitters: Splitters, ) -> #(Container, String) { - let #(inline_in, in, end) = take_list_item_chars(in, "", style) + let #(inline_in, in, layout) = take_list_item_chars(in, "", style, layout) let item = parse_list_item(inline_in, refs, attrs, splitters, []) let items = [item, ..items] - case end { - True -> #(BulletList(layout:, style:, items: list.reverse(items)), in) - False -> parse_bullet_list(in, refs, attrs, style, layout, items, splitters) + case continue_list(in, style) { + Some(in) -> parse_list(in, refs, attrs, style, layout, items, splitters) + None -> { + let items = list.reverse(items) + let container = case style { + Bullet(style) -> BulletList(layout:, style:, items:) + Ordered(start:, punctuation:, style: ordinal) -> + OrderedList(layout:, punctuation:, ordinal:, start:, items:) + } + #(container, in) + } } } @@ -1646,20 +1875,339 @@ fn parse_list_item( fn take_list_item_chars( in: String, acc: String, - style: String, -) -> #(String, String, Bool) { - let #(in, acc) = case string.split_once(in, "\n") { - Ok(#(content, in)) -> #(in, acc <> content) - Error(_) -> #("", acc <> in) + style: ListStyle, + layout: ListLayout, +) -> #(String, String, ListLayout) { + let #(line, in) = case string.split_once(in, "\n") { + Ok(split) -> split + Error(_) -> #(in, "") } + let acc = acc <> line case in { - " " <> in -> take_list_item_chars(in, acc <> "\n ", style) - "- " <> in if style == "-" -> #(acc, in, False) - "\n- " <> in if style == "-" -> #(acc, in, False) - "* " <> in if style == "*" -> #(acc, in, False) - "\n* " <> in if style == "*" -> #(acc, in, False) - _ -> #(acc, in, True) + "" -> #(acc, "", layout) + " " <> _ -> take_list_item_chars(in, acc <> "\n", style, layout) + + // A blank line followed by indented content, meaning this is + // content for the current list item. + "\n " <> rest -> { + let #(rest, indent) = count_drop_spaces(rest, 1) + let layout = case parse_list_marker(rest) { + Some(_) -> layout + None -> Loose + } + let acc = acc <> "\n\n" + take_list_item_chars_indented(rest, acc, style, layout, indent) + } + + // A blank line followed by un-indented content, so the end of this + // current list item. + "\n" <> in -> { + let layout = case continue_list(in, style) { + Some(_) -> Loose + None -> layout + } + #(acc, in, layout) + } + + _ -> { + case parse_list_marker(in) { + Some(_) -> #(acc, in, layout) + None -> take_list_item_chars(in, acc <> "\n", style, layout) + } + } + } +} + +fn parse_list_marker(in: String) -> Option(#(ListStyle, String)) { + case in { + "- " <> in | "-\n" <> in -> Some(#(Bullet(BulletDash), in)) + "* " <> in | "*\n" <> in -> Some(#(Bullet(BulletStar), in)) + "+ " <> in | "+\n" <> in -> Some(#(Bullet(BulletPlus), in)) + "(" <> in -> parse_list_marker_maybe_paren(in, True) + _ -> parse_list_marker_maybe_paren(in, False) + } +} + +fn parse_list_marker_maybe_paren( + in: String, + paren: Bool, +) -> Option(#(ListStyle, String)) { + case in { + "0" <> _ + | "1" <> _ + | "2" <> _ + | "3" <> _ + | "4" <> _ + | "5" <> _ + | "6" <> _ + | "7" <> _ + | "8" <> _ + | "9" <> _ -> + case parse_number_list(in, 0, paren) { + Some(#(punctuation, style, start, in)) -> + Some(#(Ordered(start:, style:, punctuation:), in)) + None -> None + } + + "a" <> _ + | "b" <> _ + | "c" <> _ + | "d" <> _ + | "e" <> _ + | "f" <> _ + | "g" <> _ + | "h" <> _ + | "i" <> _ + | "j" <> _ + | "k" <> _ + | "l" <> _ + | "m" <> _ + | "n" <> _ + | "o" <> _ + | "p" <> _ + | "q" <> _ + | "r" <> _ + | "s" <> _ + | "t" <> _ + | "u" <> _ + | "v" <> _ + | "w" <> _ + | "x" <> _ + | "y" <> _ + | "z" <> _ -> + case parse_lower_list(in, 0, paren) { + Some(#(punctuation, style, start, in)) -> + Some(#(Ordered(start:, style:, punctuation:), in)) + None -> None + } + + "A" <> _ + | "B" <> _ + | "C" <> _ + | "D" <> _ + | "E" <> _ + | "F" <> _ + | "G" <> _ + | "H" <> _ + | "I" <> _ + | "J" <> _ + | "K" <> _ + | "L" <> _ + | "M" <> _ + | "N" <> _ + | "O" <> _ + | "P" <> _ + | "Q" <> _ + | "R" <> _ + | "S" <> _ + | "T" <> _ + | "U" <> _ + | "V" <> _ + | "W" <> _ + | "X" <> _ + | "Y" <> _ + | "Z" <> _ -> + case parse_upper_list(in, 0, paren) { + Some(#(punctuation, style, start, in)) -> + Some(#(Ordered(start:, style:, punctuation:), in)) + None -> None + } + + _ -> None + } +} + +fn take_list_item_chars_indented( + in: String, + acc: String, + style: ListStyle, + layout: ListLayout, + indent: Int, +) -> #(String, String, ListLayout) { + let in = drop_n_spaces(in, indent) + let #(line, in) = case string.split_once(in, "\n") { + Ok(split) -> split + Error(_) -> #(in, "") + } + let acc = acc <> line + + case in { + "" -> #(acc, "", layout) + + " " <> _ -> + take_list_item_chars_indented(in, acc <> "\n", style, layout, indent) + + "\n " <> rest -> { + let layout = case parse_list_marker(drop_spaces(rest)) { + Some(_) -> layout + None -> Loose + } + let acc = acc <> "\n\n" + let in = string.drop_start(in, 1) + take_list_item_chars_indented(in, acc, style, layout, indent) + } + + // A blank line followed by un-indented content, so this is the end of this + // current list item. + "\n" <> rest2 -> #(acc, rest2, layout) + + _ -> { + case continue_list(in, style) { + Some(_) -> #(acc, in, layout) + None -> + take_list_item_chars_indented(in, acc <> "\n", style, layout, indent) + } + } + } +} + +fn continue_list(in: String, style: ListStyle) -> Option(String) { + case parse_list_marker(in) { + Some(#(next, in)) -> + case style, next { + Ordered(punctuation: p1, style: s1, start: _), + Ordered(punctuation: p2, style: s2, start: _) + if p1 == p2 && s1 == s2 + -> Some(in) + + _, _ if style == next -> Some(in) + _, _ -> None + } + None -> None + } +} + +fn drop_n_spaces(in: String, count: Int) -> String { + case in { + _ if count == 0 -> in + " " <> rest -> drop_n_spaces(rest, count - 1) + _ -> in + } +} + +fn parse_number_list( + in: String, + num: Int, + // Whether the ordinal started with a paren + paren: Bool, +) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { + case in { + "0" <> rest -> parse_number_list(rest, num * 10 + 0, paren) + "1" <> rest -> parse_number_list(rest, num * 10 + 1, paren) + "2" <> rest -> parse_number_list(rest, num * 10 + 2, paren) + "3" <> rest -> parse_number_list(rest, num * 10 + 3, paren) + "4" <> rest -> parse_number_list(rest, num * 10 + 4, paren) + "5" <> rest -> parse_number_list(rest, num * 10 + 5, paren) + "6" <> rest -> parse_number_list(rest, num * 10 + 6, paren) + "7" <> rest -> parse_number_list(rest, num * 10 + 7, paren) + "8" <> rest -> parse_number_list(rest, num * 10 + 8, paren) + "9" <> rest -> parse_number_list(rest, num * 10 + 9, paren) + ". " <> rest | ".\n" <> rest -> Some(#(FullStop, NumericOrdinal, num, rest)) + ") " <> rest | ")\n" <> rest -> { + let punctuation = case paren { + True -> DoubleParen + False -> SingleParen + } + Some(#(punctuation, NumericOrdinal, num, rest)) + } + _ -> None + } +} + +fn parse_lower_list( + in: String, + num: Int, + // Whether the ordinal started with a paren + paren: Bool, +) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { + case in { + "a" <> in -> parse_lower_list(in, num * 26 + 1, paren) + "b" <> in -> parse_lower_list(in, num * 26 + 2, paren) + "c" <> in -> parse_lower_list(in, num * 26 + 3, paren) + "d" <> in -> parse_lower_list(in, num * 26 + 4, paren) + "e" <> in -> parse_lower_list(in, num * 26 + 5, paren) + "f" <> in -> parse_lower_list(in, num * 26 + 6, paren) + "g" <> in -> parse_lower_list(in, num * 26 + 7, paren) + "h" <> in -> parse_lower_list(in, num * 26 + 8, paren) + "i" <> in -> parse_lower_list(in, num * 26 + 9, paren) + "j" <> in -> parse_lower_list(in, num * 26 + 10, paren) + "k" <> in -> parse_lower_list(in, num * 26 + 11, paren) + "l" <> in -> parse_lower_list(in, num * 26 + 12, paren) + "m" <> in -> parse_lower_list(in, num * 26 + 13, paren) + "n" <> in -> parse_lower_list(in, num * 26 + 14, paren) + "o" <> in -> parse_lower_list(in, num * 26 + 15, paren) + "p" <> in -> parse_lower_list(in, num * 26 + 16, paren) + "q" <> in -> parse_lower_list(in, num * 26 + 17, paren) + "r" <> in -> parse_lower_list(in, num * 26 + 18, paren) + "s" <> in -> parse_lower_list(in, num * 26 + 19, paren) + "t" <> in -> parse_lower_list(in, num * 26 + 20, paren) + "u" <> in -> parse_lower_list(in, num * 26 + 21, paren) + "v" <> in -> parse_lower_list(in, num * 26 + 22, paren) + "w" <> in -> parse_lower_list(in, num * 26 + 23, paren) + "x" <> in -> parse_lower_list(in, num * 26 + 24, paren) + "y" <> in -> parse_lower_list(in, num * 26 + 25, paren) + "z" <> in -> parse_lower_list(in, num * 26 + 26, paren) + + ". " <> rest | ".\n" <> rest if !paren -> + Some(#(FullStop, LowerAlphaOrdinal, num, rest)) + + ") " <> rest | ")\n" <> rest -> { + let punctuation = case paren { + True -> DoubleParen + False -> SingleParen + } + Some(#(punctuation, LowerAlphaOrdinal, num, rest)) + } + _ -> None + } +} + +fn parse_upper_list( + in: String, + num: Int, + // Whether the ordinal started with a paren + paren: Bool, +) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { + case in { + "A" <> in -> parse_upper_list(in, num * 26 + 1, paren) + "B" <> in -> parse_upper_list(in, num * 26 + 2, paren) + "C" <> in -> parse_upper_list(in, num * 26 + 3, paren) + "D" <> in -> parse_upper_list(in, num * 26 + 4, paren) + "E" <> in -> parse_upper_list(in, num * 26 + 5, paren) + "F" <> in -> parse_upper_list(in, num * 26 + 6, paren) + "G" <> in -> parse_upper_list(in, num * 26 + 7, paren) + "H" <> in -> parse_upper_list(in, num * 26 + 8, paren) + "I" <> in -> parse_upper_list(in, num * 26 + 9, paren) + "J" <> in -> parse_upper_list(in, num * 26 + 10, paren) + "K" <> in -> parse_upper_list(in, num * 26 + 11, paren) + "L" <> in -> parse_upper_list(in, num * 26 + 12, paren) + "M" <> in -> parse_upper_list(in, num * 26 + 13, paren) + "N" <> in -> parse_upper_list(in, num * 26 + 14, paren) + "O" <> in -> parse_upper_list(in, num * 26 + 15, paren) + "P" <> in -> parse_upper_list(in, num * 26 + 16, paren) + "Q" <> in -> parse_upper_list(in, num * 26 + 17, paren) + "R" <> in -> parse_upper_list(in, num * 26 + 18, paren) + "S" <> in -> parse_upper_list(in, num * 26 + 19, paren) + "T" <> in -> parse_upper_list(in, num * 26 + 20, paren) + "U" <> in -> parse_upper_list(in, num * 26 + 21, paren) + "V" <> in -> parse_upper_list(in, num * 26 + 22, paren) + "W" <> in -> parse_upper_list(in, num * 26 + 23, paren) + "X" <> in -> parse_upper_list(in, num * 26 + 24, paren) + "Y" <> in -> parse_upper_list(in, num * 26 + 25, paren) + "Z" <> in -> parse_upper_list(in, num * 26 + 26, paren) + + ". " <> rest | ".\n" <> rest if !paren -> + Some(#(FullStop, UpperAlphaOrdinal, num, rest)) + + ") " <> rest | ")\n" <> rest -> { + let punctuation = case paren { + True -> DoubleParen + False -> SingleParen + } + Some(#(punctuation, UpperAlphaOrdinal, num, rest)) + } + _ -> None } } @@ -1667,7 +2215,7 @@ fn take_paragraph_chars( in: String, div_close_size: Option(Int), ) -> #(String, String) { - let #(paragraph, rest) = case string.split_once(in, "\n\n") { + let #(paragraph, in) = case string.split_once(in, "\n\n") { Ok(#(content, in)) -> #(content, in) Error(_) -> case string.ends_with(in, "\n") { @@ -1678,16 +2226,16 @@ fn take_paragraph_chars( case div_close_size { Some(size) -> { - let #(split_paragraph, paragraph_rest) = + let #(split_paragraph, paragraph_in) = search_paragraph_for_div_end(paragraph, [], size) - case split_paragraph, paragraph_rest { - "", "" -> #(paragraph, rest) - _, "" -> #(split_paragraph, rest) - _, _ -> #(split_paragraph, paragraph_rest <> "\n\n" <> rest) + case split_paragraph, paragraph_in { + "", "" -> #(paragraph, in) + _, "" -> #(split_paragraph, in) + _, _ -> #(split_paragraph, paragraph_in <> "\n\n" <> in) } } - None -> #(paragraph, rest) + None -> #(paragraph, in) } } @@ -1872,6 +2420,23 @@ fn container_to_html( |> close_tag("ul") } + OrderedList(layout:, punctuation: _, ordinal:, start:, items:) -> { + let attrs = case start { + 1 -> dict.new() + _ -> dict.from_list([#("start", int.to_string(start))]) + } + let attrs = case ordinal { + NumericOrdinal -> attrs + LowerAlphaOrdinal -> dict.insert(attrs, "type", "a") + UpperAlphaOrdinal -> dict.insert(attrs, "type", "A") + } + html + |> open_tag("ol", attrs) + |> append_to_html("\n") + |> list_items_to_html(layout, items, refs) + |> close_tag("ol") + } + BlockQuote(attrs, items) -> html |> open_tag("blockquote", attrs) @@ -2034,12 +2599,25 @@ fn list_items_to_html( |> list_items_to_html(layout, rest, refs) } + [[Paragraph(_, inlines), nested_list, ..item_rest], ..rest] + if layout == Tight + -> { + html + |> open_tag("li", dict.new()) + |> append_to_html("\n") + |> inlines_to_html(inlines, refs, TrimLast) + |> append_to_html("\n") + |> containers_to_html([nested_list, ..item_rest], refs, _) + |> close_tag("li") + |> append_to_html("\n") + |> list_items_to_html(layout, rest, refs) + } + [item, ..rest] -> { html |> open_tag("li", dict.new()) |> append_to_html("\n") |> containers_to_html(item, refs, _) - |> append_to_html("\n") |> close_tag("li") |> append_to_html("\n") |> list_items_to_html(layout, rest, refs) @@ -2123,6 +2701,24 @@ fn inline_to_html( |> inlines_to_html(inlines, refs, trim) |> close_tag("em") } + Delete(inlines) -> { + html + |> open_tag("del", dict.new()) + |> inlines_to_html(inlines, refs, NoTrim) + |> close_tag("del") + } + Insert(inlines) -> { + html + |> open_tag("ins", dict.new()) + |> inlines_to_html(inlines, refs, NoTrim) + |> close_tag("ins") + } + Mark(inlines) -> { + html + |> open_tag("mark", dict.new()) + |> inlines_to_html(inlines, refs, NoTrim) + |> close_tag("mark") + } Link(attributes, text, destination) -> { // Merge: reference attrs <- href <- inline attrs let ref_attrs = get_reference_attributes(destination, refs) diff --git a/test/cases_unimplemented/insert_delete_mark.test b/test/cases/insert_delete_mark.test similarity index 100% rename from test/cases_unimplemented/insert_delete_mark.test rename to test/cases/insert_delete_mark.test diff --git a/test/cases/lists.test b/test/cases/lists.test index a59c1b3..e810930 100644 --- a/test/cases/lists.test +++ b/test/cases/lists.test @@ -53,3 +53,420 @@ one ``` + +``` +- one +lazy +- two +. + +``` + +``` +- a +- b ++ c +. + + +``` + +``` +- a + +- b +. + +``` + +``` +- one + + - two + + - three +. + +``` + +``` +- one + and + + another paragraph + + - a list + +- two +. + +``` + + +``` +- a + - b + + - c +- d +. + +``` + +``` +- a + - b + + - c + +- d +. + +``` + +``` +- a + + b +- c +. + +``` + +``` +- a + + - b + - c +- d +. + +``` + +``` +- a + + - b + - c + +- d +. + +``` + +``` +- a + + * b +cd +. + +``` + +``` +- - - a +. + +``` + +``` +1. one +1. two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + +``` +1. one + + 1. two +. +
    +
  1. +one +
      +
    1. +two +
    2. +
    +
  2. +
+``` + +``` +4. one +5. two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + +``` +1) one +2) two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + +``` +(1) one +(2) two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + +``` +(a) one +(b) two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + +``` +(D) one +(E) two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + +``` +a. one +b. two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + +``` +101) one +102) two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + +``` +C) one +D) two +E) three +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
  5. +three +
  6. +
+``` + +``` +d. one +e. two +. +
    +
  1. +one +
  2. +
  3. +two +
  4. +
+``` + diff --git a/test/cases_unimplemented/spans.test b/test/cases/spans.test similarity index 100% rename from test/cases_unimplemented/spans.test rename to test/cases/spans.test diff --git a/test/cases_unimplemented/lists.test b/test/cases_unimplemented/lists.test index f98b91e..dffbd49 100644 --- a/test/cases_unimplemented/lists.test +++ b/test/cases_unimplemented/lists.test @@ -1,372 +1,3 @@ -``` -- one - - - two - - - three -. - -``` - -``` -- one - and - - another paragraph - - - a list - -- two -. - -``` - -``` -- one -lazy -- two -. - -``` - -``` -- a -- b -+ c -. - - -``` - -``` -- a - -- b -. - -``` - -``` -- a - - b - - - c -- d -. - -``` - -``` -- a - - b - - - c - -- d -. - -``` - -``` -- a - - b -- c -. - -``` - -``` -- a - - - b - - c -- d -. - -``` - -``` -- a - - - b - - c - -- d -. - -``` - -``` -- a - - * b -cd -. - -``` - -``` -- - - a -. - -``` - -``` -1. one -1. two -. -
    -
  1. -one -
  2. -
  3. -two -
  4. -
-``` - -``` -1. one - - 1. two -. -
    -
  1. -one -
      -
    1. -two -
    2. -
    -
  2. -
-``` - -``` -4. one -5. two -. -
    -
  1. -one -
  2. -
  3. -two -
  4. -
-``` - -``` -1) one -2) two -. -
    -
  1. -one -
  2. -
  3. -two -
  4. -
-``` - -``` -(1) one -(2) two -. -
    -
  1. -one -
  2. -
  3. -two -
  4. -
-``` - -``` -(a) one -(b) two -. -
    -
  1. -one -
  2. -
  3. -two -
  4. -
-``` - -``` -(D) one -(E) two -. -
    -
  1. -one -
  2. -
  3. -two -
  4. -
-``` - -``` -a. one -b. two -. -
    -
  1. -one -
  2. -
  3. -two -
  4. -
-``` - ``` i. one ii. two