From 1b63d7e64fc5415d35d497559ca47014800d5463 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Tue, 13 Jan 2026 18:32:36 +0000 Subject: [PATCH 01/10] Standalone attributes --- CHANGELOG.md | 4 ++++ src/jot.gleam | 13 ++++++++++++- test/{cases_unimplemented => cases}/spans.test | 0 3 files changed, 16 insertions(+), 1 deletion(-) rename test/{cases_unimplemented => cases}/spans.test (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f24d5..4d258a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Improved support for spans with attributes. + ## v8.0.0 - 2025-11-28 - Added support for inline attributes on links and images. diff --git a/src/jot.gleam b/src/jot.gleam index 0ee33dd..597f7a3 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -145,6 +145,7 @@ pub fn parse(djot: String) -> Document { "--", "...", "<", + "{", ]), link_destination: splitter.new([")", "]", "\n"]), math_end: splitter.new(["`"]), @@ -843,7 +844,7 @@ fn parse_attributes_end( "" -> Some(#(attrs, "")) "\n" <> in -> Some(#(attrs, in)) " " <> in -> parse_attributes_end(in, attrs) - _ -> None + _ -> Some(#(attrs, in)) } } @@ -1210,6 +1211,16 @@ fn parse_inline( } } + // 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) 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 From 0c53712e0b4b4f54d16fb4a61a8229124519e28b Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Tue, 13 Jan 2026 18:45:11 +0000 Subject: [PATCH 02/10] Insert, delete, mark --- CHANGELOG.md | 1 + README.md | 11 +-- src/jot.gleam | 71 ++++++++++++++++++- .../insert_delete_mark.test | 0 4 files changed, 77 insertions(+), 6 deletions(-) rename test/{cases_unimplemented => cases}/insert_delete_mark.test (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d258a9..d147eb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Added support for inserts, deletes, and marks. - Improved support for spans with attributes. ## v8.0.0 - 2025-11-28 diff --git a/README.md b/README.md index 8a7d038..90f8c25 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,11 @@ 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] Block quotes - [x] Bullet lists without nesting - [x] Code blocks -- [x] Block quotes - [x] Content escaping - [x] Div - [x] Emphasis and strong @@ -48,15 +49,15 @@ 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] 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 diff --git a/src/jot.gleam b/src/jot.gleam index 597f7a3..4a4b40b 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -68,6 +68,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 +148,9 @@ pub fn parse(djot: String) -> Document { "--", "...", "<", + "{-", + "{+", + "{=", "{", ]), link_destination: splitter.new([")", "]", "\n"]), @@ -1211,6 +1217,36 @@ 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 @@ -1374,6 +1410,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, @@ -1592,7 +1643,7 @@ 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) -> + 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) @@ -2134,6 +2185,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 From af33ff9653abe538f0eb3c42309b2aed924a33a5 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Tue, 13 Jan 2026 19:11:55 +0000 Subject: [PATCH 03/10] Further list support --- CHANGELOG.md | 1 + src/jot.gleam | 49 ++++++++++++++++++--------- test/cases/lists.test | 51 +++++++++++++++++++++++++++++ test/cases_unimplemented/lists.test | 50 ---------------------------- 4 files changed, 86 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d147eb6..ebcd5fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added support for inserts, deletes, and marks. - Improved support for spans with attributes. +- Improved support for lists. ## v8.0.0 - 2025-11-28 diff --git a/src/jot.gleam b/src/jot.gleam index 4a4b40b..a71bc77 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -370,7 +370,7 @@ 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 #(list, in) = @@ -1643,8 +1643,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) | Delete(inlines) | Insert(inlines) | Mark(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) @@ -1677,7 +1680,8 @@ fn parse_bullet_list( items: List(List(Container)), splitters: Splitters, ) -> #(Container, String) { - let #(inline_in, in, end) = take_list_item_chars(in, "", style) + let #(inline_in, in, end, layout) = + take_list_item_chars(in, "", style, layout) let item = parse_list_item(inline_in, refs, attrs, splitters, []) let items = [item, ..items] case end { @@ -1709,19 +1713,35 @@ 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) + layout: ListLayout, +) -> #(String, String, Bool, 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, "", True, layout) + " " <> _ -> take_list_item_chars(in, acc <> "\n", style, layout) + + // Next item (tight, no line between them) + "- " <> in if style == "-" -> #(acc, in, False, layout) + "* " <> in if style == "*" -> #(acc, in, False, layout) + "+ " <> in if style == "+" -> #(acc, in, False, layout) + + // Next item (loose, a line between them) + "\n- " <> in if style == "-" -> #(acc, in, False, Loose) + "\n* " <> in if style == "*" -> #(acc, in, False, Loose) + "\n+ " <> in if style == "+" -> #(acc, in, False, Loose) + + // Blank line + "\n" <> _ as in -> #(acc, in, True, layout) + + // Different marker, so the start of a new list + "- " <> _ | "* " <> _ | "+ " <> _ -> #(acc, in, True, layout) + + _ -> take_list_item_chars(in, acc <> "\n", style, layout) } } @@ -2101,7 +2121,6 @@ fn list_items_to_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) diff --git a/test/cases/lists.test b/test/cases/lists.test index a59c1b3..e338ff0 100644 --- a/test/cases/lists.test +++ b/test/cases/lists.test @@ -53,3 +53,54 @@ one ``` + +``` +- one +lazy +- two +. +
    +
  • +one +lazy +
  • +
  • +two +
  • +
+``` + +``` +- a +- b ++ c +. +
    +
  • +a +
  • +
  • +b +
  • +
+
    +
  • +c +
  • +
+``` + +``` +- a + +- b +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
+``` diff --git a/test/cases_unimplemented/lists.test b/test/cases_unimplemented/lists.test index f98b91e..31da4f0 100644 --- a/test/cases_unimplemented/lists.test +++ b/test/cases_unimplemented/lists.test @@ -49,56 +49,6 @@ a list ``` -``` -- one -lazy -- two -. -
    -
  • -one -lazy -
  • -
  • -two -
  • -
-``` - -``` -- a -- b -+ c -. -
    -
  • -a -
  • -
  • -b -
  • -
-
    -
  • -c -
  • -
-``` - -``` -- a - -- b -. -
    -
  • -

    a

    -
  • -
  • -

    b

    -
  • -
-``` ``` - a From 495a0e98c77d6893e107ac34dea00b1be5d6b97a Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Tue, 13 Jan 2026 21:19:21 +0000 Subject: [PATCH 04/10] Nested lists --- CHANGELOG.md | 1 + README.md | 3 +- src/jot.gleam | 119 +++++++++++++++- test/cases/lists.test | 205 ++++++++++++++++++++++++++++ test/cases_unimplemented/lists.test | 204 --------------------------- 5 files changed, 324 insertions(+), 208 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebcd5fd..609e416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Added support for inserts, deletes, and marks. +- Added support for nested lists. - Improved support for spans with attributes. - Improved support for lists. diff --git a/README.md b/README.md index 90f8c25..6fbe2dd 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This project is a work in progress. So far it supports: - [x] Autolinks (email and URL) - [x] Block attributes - [x] Block quotes -- [x] Bullet lists without nesting +- [x] Bullet Lists - [x] Code blocks - [x] Content escaping - [x] Div @@ -51,7 +51,6 @@ This project is a work in progress. So far it supports: - [x] Inline code - [x] Inserts, deletes, and marks. - [x] Links (inline and reference, with attributes support) -- [x] Lists (without nesting) - [x] Manual line breaks - [x] Maths (inline and display) - [x] Non-breaking spaces diff --git a/src/jot.gleam b/src/jot.gleam index a71bc77..95b120b 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -1735,8 +1735,26 @@ fn take_list_item_chars( "\n* " <> in if style == "*" -> #(acc, in, False, Loose) "\n+ " <> in if style == "+" -> #(acc, in, False, Loose) - // Blank line - "\n" <> _ as in -> #(acc, in, True, layout) + // Blank line + "\n" <> in -> { + case in { + // The blank line was followed by indented content, so this is content + // for this particular list item. + " " <> in -> { + let #(in, indent) = count_drop_spaces(in, 1) + let layout = case starts_with_list_marker(in) { + True -> layout + False -> Loose + } + let acc = acc <> "\n\n" + take_list_item_chars_indented(in, acc, style, layout, indent) + } + + // The blank line was followed by non-indented content, meaning that + // the end of the list has been reached. + _ -> #(acc, in, True, layout) + } + } // Different marker, so the start of a new list "- " <> _ | "* " <> _ | "+ " <> _ -> #(acc, in, True, layout) @@ -1745,6 +1763,87 @@ fn take_list_item_chars( } } +fn starts_with_list_marker(in: String) -> Bool { + case in { + "-" <> rest | "*" <> rest | "+" <> rest -> + case rest { + "" | " " <> _ | "\n" <> _ -> True + _ -> False + } + _ -> False + } +} + +fn take_list_item_chars_indented( + in: String, + acc: String, + style: String, + layout: ListLayout, + indent: Int, +) -> #(String, String, Bool, ListLayout) { + // Strip the indent level from this line + let in = drop_n_spaces(in, indent) + let #(line, rest) = case string.split_once(in, "\n") { + Ok(split) -> split + Error(_) -> #(in, "") + } + let acc = acc <> line + + case rest { + "" -> #(acc, "", True, layout) + + // More indented content + " " <> _ -> + take_list_item_chars_indented(rest, acc <> "\n", style, layout, indent) + + // Blank line + "\n" <> rest2 -> { + case rest2 { + // Blank + indent -> continue indented block + " " <> _ -> { + let layout = case starts_with_list_marker(drop_spaces(rest2)) { + True -> layout + False -> Loose + } + take_list_item_chars_indented( + rest2, + acc <> "\n\n", + style, + layout, + indent, + ) + } + // Blank + same marker at base level -> next item in outer list + // (don't set Loose - the blank was within indented content, not between outer items) + "- " <> rest3 if style == "-" -> #(acc, rest3, False, layout) + "* " <> rest3 if style == "*" -> #(acc, rest3, False, layout) + "+ " <> rest3 if style == "+" -> #(acc, rest3, False, layout) + // Blank + other -> end of list + _ -> #(acc, "\n" <> rest2, True, layout) + } + } + + // Non-indented same marker -> next item in outer list + "- " <> rest2 if style == "-" -> #(acc, rest2, False, layout) + "* " <> rest2 if style == "*" -> #(acc, rest2, False, layout) + "+ " <> rest2 if style == "+" -> #(acc, rest2, False, layout) + + // Non-indented other -> lazy continuation of nested content + _ -> take_list_item_chars_indented(rest, acc <> "\n", style, layout, indent) + } +} + +fn drop_n_spaces(in: String, n: Int) -> String { + case n { + 0 -> in + _ -> + case in { + " " <> rest -> drop_n_spaces(rest, n - 1) + _ -> in + } + } +} + fn take_paragraph_chars( in: String, div_close_size: Option(Int), @@ -2105,6 +2204,7 @@ fn list_items_to_html( case items { [] -> html + // Tight list with single paragraph - no

tag [[Paragraph(_, inlines)], ..rest] if layout == Tight -> { html |> open_tag("li", dict.new()) @@ -2116,6 +2216,21 @@ fn list_items_to_html( |> list_items_to_html(layout, rest, refs) } + // Tight list with paragraph followed by nested list - no

tag for paragraph + [[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()) diff --git a/test/cases/lists.test b/test/cases/lists.test index e338ff0..d110142 100644 --- a/test/cases/lists.test +++ b/test/cases/lists.test @@ -104,3 +104,208 @@ c ``` + +``` +- one + + - two + + - three +. +

    +
  • +one +
      +
    • +two +
        +
      • +three +
      • +
      +
    • +
    +
  • +
+``` + +``` +- one + and + + another paragraph + + - a list + +- two +. +
    +
  • +

    one +and

    +

    another paragraph

    +
      +
    • +a list +
    • +
    +
  • +
  • +

    two

    +
  • +
+``` + + +``` +- a + - b + + - c +- d +. +
    +
  • +a +- b +
      +
    • +c +
    • +
    +
  • +
  • +d +
  • +
+``` + +``` +- a + - b + + - c + +- d +. +
    +
  • +a +- b +
      +
    • +c +
    • +
    +
  • +
  • +d +
  • +
+``` + +``` +- a + + b +- c +. +
    +
  • +

    a

    +

    b

    +
  • +
  • +

    c

    +
  • +
+``` + +``` +- a + + - b + - c +- d +. +
    +
  • +a +
      +
    • +b +
    • +
    • +c +
    • +
    +
  • +
  • +d +
  • +
+``` + +``` +- a + + - b + - c + +- d +. +
    +
  • +a +
      +
    • +b +
    • +
    • +c +
    • +
    +
  • +
  • +d +
  • +
+``` + +``` +- a + + * b +cd +. +
    +
  • +a +
      +
    • +b +cd +
    • +
    +
  • +
+``` + +``` +- - - a +. +
    +
  • +
      +
    • +
        +
      • +a +
      • +
      +
    • +
    +
  • +
+``` + diff --git a/test/cases_unimplemented/lists.test b/test/cases_unimplemented/lists.test index 31da4f0..66b05e0 100644 --- a/test/cases_unimplemented/lists.test +++ b/test/cases_unimplemented/lists.test @@ -1,207 +1,3 @@ -``` -- one - - - two - - - three -. -
    -
  • -one -
      -
    • -two -
        -
      • -three -
      • -
      -
    • -
    -
  • -
-``` - -``` -- one - and - - another paragraph - - - a list - -- two -. -
    -
  • -

    one -and

    -

    another paragraph

    -
      -
    • -a list -
    • -
    -
  • -
  • -

    two

    -
  • -
-``` - - -``` -- a - - b - - - c -- d -. -
    -
  • -a -- b -
      -
    • -c -
    • -
    -
  • -
  • -d -
  • -
-``` - -``` -- a - - b - - - c - -- d -. -
    -
  • -a -- b -
      -
    • -c -
    • -
    -
  • -
  • -d -
  • -
-``` - -``` -- a - - b -- c -. -
    -
  • -

    a

    -

    b

    -
  • -
  • -

    c

    -
  • -
-``` - -``` -- a - - - b - - c -- d -. -
    -
  • -a -
      -
    • -b -
    • -
    • -c -
    • -
    -
  • -
  • -d -
  • -
-``` - -``` -- a - - - b - - c - -- d -. -
    -
  • -a -
      -
    • -b -
    • -
    • -c -
    • -
    -
  • -
  • -d -
  • -
-``` - -``` -- a - - * b -cd -. -
    -
  • -a -
      -
    • -b -cd -
    • -
    -
  • -
-``` - -``` -- - - a -. -
    -
  • -
      -
    • -
        -
      • -a -
      • -
      -
    • -
    -
  • -
-``` - ``` 1. one 1. two From f1873c42858c49689e8b9bce1ba391d99a7f6946 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Tue, 13 Jan 2026 22:39:11 +0000 Subject: [PATCH 05/10] Ordered lists --- CHANGELOG.md | 1 + README.md | 3 +- src/jot.gleam | 370 +++++++++++++++++++++------- test/cases/lists.test | 44 ++++ test/cases_unimplemented/lists.test | 45 ---- 5 files changed, 325 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 609e416..9d70b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added support for inserts, deletes, and marks. - Added support for nested lists. +- Added support for ordered lists. - Improved support for spans with attributes. - Improved support for lists. diff --git a/README.md b/README.md index 6fbe2dd..75ec4fe 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ This project is a work in progress. So far it supports: - [x] Autolinks (email and URL) - [x] Block attributes - [x] Block quotes -- [x] Bullet Lists - [x] Code blocks - [x] Content escaping - [x] Div @@ -54,9 +53,11 @@ This project is a work in progress. So far it supports: - [x] Manual line breaks - [x] Maths (inline and display) - [x] Non-breaking spaces +- [x] Ordered lists with the `1. 2. 3.` syntax - [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] Unordered lists diff --git a/src/jot.gleam b/src/jot.gleam index 95b120b..ecc2e10 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -46,11 +46,22 @@ 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, 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 Inline { Linebreak NonBreakingSpace @@ -373,6 +384,11 @@ fn parse_container( "-" as style <> in2 | "*" as style <> in2 | "+" as style <> in2 -> { case parse_thematic_break(1, in2), in2 { None, " " <> in2 | None, "\n" <> in2 -> { + let style = case style { + "-" -> BulletDash + "*" -> BulletStar + _ -> BulletStar + } let #(list, in) = parse_bullet_list(in2, refs, attrs, style, Tight, [], splitters) #(in, refs, Some(list), dict.new()) @@ -388,6 +404,30 @@ fn parse_container( } } + "0" <> _ + | "1" <> _ + | "2" <> _ + | "3" <> _ + | "4" <> _ + | "5" <> _ + | "6" <> _ + | "7" <> _ + | "8" <> _ + | "9" <> _ -> { + case parse_ordered_list_start(in, 0) { + Some(#(start, in2)) -> { + let #(list, in) = + parse_ordered_list(in2, refs, attrs, start, Tight, [], splitters) + #(in, refs, Some(list), dict.new()) + } + None -> { + let #(paragraph, in) = + parse_paragraph(in, attrs, splitters, div_close_size) + #(in, refs, Some(paragraph), dict.new()) + } + } + } + "[^" <> in2 -> { case parse_footnote_def(in2, refs, splitters, "^") { None -> { @@ -1675,18 +1715,21 @@ fn parse_bullet_list( in: String, refs: Refs, attrs: Dict(String, String), - style: String, + style: BulletStyle, layout: ListLayout, items: List(List(Container)), splitters: Splitters, ) -> #(Container, String) { - let #(inline_in, in, end, layout) = - take_list_item_chars(in, "", style, layout) + 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 parse_bullet_marker(in) { + Some(#(next, in)) if next == style -> + parse_bullet_list(in, refs, attrs, style, layout, items, splitters) + _ -> { + let container = BulletList(layout:, style:, items: list.reverse(items)) + #(container, in) + } } } @@ -1712,9 +1755,9 @@ fn parse_list_item( fn take_list_item_chars( in: String, acc: String, - style: String, + style: BulletStyle, layout: ListLayout, -) -> #(String, String, Bool, ListLayout) { +) -> #(String, String, ListLayout) { let #(line, in) = case string.split_once(in, "\n") { Ok(split) -> split Error(_) -> #(in, "") @@ -1722,125 +1765,256 @@ fn take_list_item_chars( let acc = acc <> line case in { - "" -> #(acc, "", True, layout) + "" -> #(acc, "", layout) " " <> _ -> take_list_item_chars(in, acc <> "\n", style, layout) - // Next item (tight, no line between them) - "- " <> in if style == "-" -> #(acc, in, False, layout) - "* " <> in if style == "*" -> #(acc, in, False, layout) - "+ " <> in if style == "+" -> #(acc, in, False, 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 starts_with_list_marker(rest) { + True -> layout + False -> Loose + } + let acc = acc <> "\n\n" + take_list_item_chars_indented(rest, acc, style, layout, indent) + } - // Next item (loose, a line between them) - "\n- " <> in if style == "-" -> #(acc, in, False, Loose) - "\n* " <> in if style == "*" -> #(acc, in, False, Loose) - "\n+ " <> in if style == "+" -> #(acc, in, False, Loose) + // A blank line followed by un-indented content, so the end of this + // current list item. + "\n" <> rest -> { + let layout = case parse_bullet_marker(rest) { + Some(#(next, _)) if next == style -> Loose + _ -> layout + } + #(acc, rest, layout) + } - // Blank line - "\n" <> in -> { - case in { - // The blank line was followed by indented content, so this is content - // for this particular list item. - " " <> in -> { - let #(in, indent) = count_drop_spaces(in, 1) - let layout = case starts_with_list_marker(in) { - True -> layout - False -> Loose - } - let acc = acc <> "\n\n" - take_list_item_chars_indented(in, acc, style, layout, indent) - } + _ -> { + case parse_bullet_marker(in) { + Some(_) -> #(acc, in, layout) + None -> take_list_item_chars(in, acc <> "\n", style, layout) + } + } + } +} + +fn starts_with_list_marker(in: String) -> Bool { + case parse_bullet_marker(in) { + Some(_) -> True + None -> False + } +} + +fn parse_bullet_marker(in: String) -> Option(#(BulletStyle, String)) { + case in { + "- " <> rest | "-\n" <> rest -> Some(#(BulletDash, rest)) + "* " <> rest | "*\n" <> rest -> Some(#(BulletStar, rest)) + "+ " <> rest | "+\n" <> rest -> Some(#(BulletPlus, rest)) + _ -> None + } +} + +fn take_list_item_chars_indented( + in: String, + acc: String, + style: BulletStyle, + 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) - // The blank line was followed by non-indented content, meaning that - // the end of the list has been reached. - _ -> #(acc, in, True, layout) + "\n " <> rest -> { + let layout = case starts_with_list_marker(drop_spaces(rest)) { + True -> layout + False -> Loose } + let acc = acc <> "\n\n" + let in = string.drop_start(in, 1) + take_list_item_chars_indented(in, acc, style, layout, indent) } - // Different marker, so the start of a new list - "- " <> _ | "* " <> _ | "+ " <> _ -> #(acc, in, True, layout) + // A blank line followed by un-indented content, so this is the end of this + // current list item. + "\n" <> rest2 -> #(acc, rest2, layout) - _ -> take_list_item_chars(in, acc <> "\n", style, layout) + _ -> { + case parse_bullet_marker(in) { + Some(#(next, _)) if next == style -> #(acc, in, layout) + _ -> + take_list_item_chars_indented(in, acc <> "\n", style, layout, indent) + } + } } } -fn starts_with_list_marker(in: String) -> Bool { +fn drop_n_spaces(in: String, count: Int) -> String { case in { - "-" <> rest | "*" <> rest | "+" <> rest -> - case rest { - "" | " " <> _ | "\n" <> _ -> True - _ -> False + _ if count == 0 -> in + " " <> rest -> drop_n_spaces(rest, count - 1) + _ -> in + } +} + +fn parse_ordered_list_start(in: String, num: Int) -> Option(#(Int, String)) { + case in { + "0" <> rest -> parse_ordered_list_start(rest, num * 10 + 0) + "1" <> rest -> parse_ordered_list_start(rest, num * 10 + 1) + "2" <> rest -> parse_ordered_list_start(rest, num * 10 + 2) + "3" <> rest -> parse_ordered_list_start(rest, num * 10 + 3) + "4" <> rest -> parse_ordered_list_start(rest, num * 10 + 4) + "5" <> rest -> parse_ordered_list_start(rest, num * 10 + 5) + "6" <> rest -> parse_ordered_list_start(rest, num * 10 + 6) + "7" <> rest -> parse_ordered_list_start(rest, num * 10 + 7) + "8" <> rest -> parse_ordered_list_start(rest, num * 10 + 8) + "9" <> rest -> parse_ordered_list_start(rest, num * 10 + 9) + ". " <> rest | ".\n" <> rest -> Some(#(num, rest)) + _ -> None + } +} + +fn parse_ordered_list( + in: String, + refs: Refs, + attrs: Dict(String, String), + start: Int, + layout: ListLayout, + items: List(List(Container)), + splitters: Splitters, +) -> #(Container, String) { + let #(inline_in, in, layout) = take_ordered_list_item_chars(in, "", layout) + let item = parse_list_item(inline_in, refs, attrs, splitters, []) + let items = [item, ..items] + case drop_ordered_list_marker(in) { + Some(in) -> + parse_ordered_list(in, refs, attrs, start, layout, items, splitters) + None -> #(OrderedList(layout:, start:, items: list.reverse(items)), in) + } +} + +fn take_ordered_list_item_chars( + in: String, + acc: String, + 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 { + "" -> #(acc, "", layout) + " " <> _ -> take_ordered_list_item_chars(in, acc <> "\n", 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 starts_with_ordered_list_marker(rest) { + True -> layout + False -> Loose } - _ -> False + let acc = acc <> "\n\n" + take_ordered_list_item_chars_indented(rest, acc, layout, indent) + } + + // A blank line followed by un-indented content, so the end of this + // current list item. + "\n" <> rest -> { + let layout = case starts_with_ordered_list_marker(rest) { + True -> Loose + False -> layout + } + #(acc, rest, layout) + } + + // Next item marker or other content + _ -> { + case starts_with_ordered_list_marker(in) { + True -> #(acc, in, layout) + False -> take_ordered_list_item_chars(in, acc <> "\n", layout) + } + } } } -fn take_list_item_chars_indented( +fn take_ordered_list_item_chars_indented( in: String, acc: String, - style: String, layout: ListLayout, indent: Int, -) -> #(String, String, Bool, ListLayout) { - // Strip the indent level from this line +) -> #(String, String, ListLayout) { let in = drop_n_spaces(in, indent) - let #(line, rest) = case string.split_once(in, "\n") { + let #(line, in) = case string.split_once(in, "\n") { Ok(split) -> split Error(_) -> #(in, "") } let acc = acc <> line - case rest { - "" -> #(acc, "", True, layout) + case in { + "" -> #(acc, "", layout) - // More indented content " " <> _ -> - take_list_item_chars_indented(rest, acc <> "\n", style, layout, indent) - - // Blank line - "\n" <> rest2 -> { - case rest2 { - // Blank + indent -> continue indented block - " " <> _ -> { - let layout = case starts_with_list_marker(drop_spaces(rest2)) { - True -> layout - False -> Loose - } - take_list_item_chars_indented( - rest2, - acc <> "\n\n", - style, - layout, - indent, - ) - } - // Blank + same marker at base level -> next item in outer list - // (don't set Loose - the blank was within indented content, not between outer items) - "- " <> rest3 if style == "-" -> #(acc, rest3, False, layout) - "* " <> rest3 if style == "*" -> #(acc, rest3, False, layout) - "+ " <> rest3 if style == "+" -> #(acc, rest3, False, layout) - // Blank + other -> end of list - _ -> #(acc, "\n" <> rest2, True, layout) + take_ordered_list_item_chars_indented(in, acc <> "\n", layout, indent) + + // New line with indented content, it's content for the current list item. + "\n " <> rest -> { + let layout = case starts_with_ordered_list_marker(drop_spaces(rest)) { + True -> layout + False -> Loose } + let in = string.drop_start(in, 1) + take_ordered_list_item_chars_indented(in, acc <> "\n\n", layout, indent) } - // Non-indented same marker -> next item in outer list - "- " <> rest2 if style == "-" -> #(acc, rest2, False, layout) - "* " <> rest2 if style == "*" -> #(acc, rest2, False, layout) - "+ " <> rest2 if style == "+" -> #(acc, rest2, False, layout) + // New line with other content, the current list item has ended. + "\n" <> rest2 -> #(acc, rest2, layout) - // Non-indented other -> lazy continuation of nested content - _ -> take_list_item_chars_indented(rest, acc <> "\n", style, layout, indent) + _ -> { + case starts_with_ordered_list_marker(in) { + True -> #(acc, in, layout) + False -> { + let acc = acc <> "\n" + take_ordered_list_item_chars_indented(in, acc, layout, indent) + } + } + } } } -fn drop_n_spaces(in: String, n: Int) -> String { - case n { - 0 -> in - _ -> - case in { - " " <> rest -> drop_n_spaces(rest, n - 1) - _ -> in - } +fn drop_ordered_list_marker(in: String) -> Option(String) { + case in { + "0" <> rest + | "1" <> rest + | "2" <> rest + | "3" <> rest + | "4" <> rest + | "5" <> rest + | "6" <> rest + | "7" <> rest + | "8" <> rest + | "9" <> rest -> drop_ordered_list_marker(rest) + ". " <> rest | ".\n" <> rest -> Some(rest) + _ -> None + } +} + +fn starts_with_ordered_list_marker(in: String) -> Bool { + case drop_ordered_list_marker(in) { + Some(_) -> True + None -> False } } @@ -2053,6 +2227,18 @@ fn container_to_html( |> close_tag("ul") } + OrderedList(layout:, start:, items:) -> { + let attrs = case start { + 1 -> dict.new() + _ -> dict.from_list([#("start", int.to_string(start))]) + } + 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) diff --git a/test/cases/lists.test b/test/cases/lists.test index d110142..ada937d 100644 --- a/test/cases/lists.test +++ b/test/cases/lists.test @@ -309,3 +309,47 @@ 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. +
+``` diff --git a/test/cases_unimplemented/lists.test b/test/cases_unimplemented/lists.test index 66b05e0..ad0a3ce 100644 --- a/test/cases_unimplemented/lists.test +++ b/test/cases_unimplemented/lists.test @@ -1,48 +1,3 @@ -``` -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 From d05d251a4ae827734bd7189dc69b49c0bdec3444 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Wed, 14 Jan 2026 13:19:45 +0000 Subject: [PATCH 06/10] More ordered list ordinals --- README.md | 2 +- src/jot.gleam | 602 ++++++++++++++++++++-------- test/cases/lists.test | 85 ++++ test/cases_unimplemented/lists.test | 70 ---- 4 files changed, 515 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 75ec4fe..29ded53 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ This project is a work in progress. So far it supports: - [x] Manual line breaks - [x] Maths (inline and display) - [x] Non-breaking spaces -- [x] Ordered lists with the `1. 2. 3.` syntax +- [x] Ordered lists with the `1.`, `1)`, and `(1)` syntaxes - [x] Paragraphs - [x] Raw blocks - [x] Smart replacing of `...` with ellipsis diff --git a/src/jot.gleam b/src/jot.gleam index ecc2e10..770947c 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -51,7 +51,13 @@ pub type Container { style: BulletStyle, items: List(List(Container)), ) - OrderedList(layout: ListLayout, start: Int, items: List(List(Container))) + OrderedList( + layout: ListLayout, + style: 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)) } @@ -62,6 +68,81 @@ pub type BulletStyle { BulletPlus } +pub type OrdinalPunctuation { + FullStop + SingleParen + DoubleParen +} + +pub type OrdinalStyle { + NumericOrdinal + LowerAlphaOrdinal + UpperAlphaOrdinal +} + +fn letter_ordinal(c: String) -> Option(#(Int, OrdinalStyle)) { + case c { + "a" -> Some(#(1, LowerAlphaOrdinal)) + "b" -> Some(#(2, LowerAlphaOrdinal)) + "c" -> Some(#(3, LowerAlphaOrdinal)) + "d" -> Some(#(4, LowerAlphaOrdinal)) + "e" -> Some(#(5, LowerAlphaOrdinal)) + "f" -> Some(#(6, LowerAlphaOrdinal)) + "g" -> Some(#(7, LowerAlphaOrdinal)) + "h" -> Some(#(8, LowerAlphaOrdinal)) + "i" -> Some(#(9, LowerAlphaOrdinal)) + "j" -> Some(#(10, LowerAlphaOrdinal)) + "k" -> Some(#(11, LowerAlphaOrdinal)) + "l" -> Some(#(12, LowerAlphaOrdinal)) + "m" -> Some(#(13, LowerAlphaOrdinal)) + "n" -> Some(#(14, LowerAlphaOrdinal)) + "o" -> Some(#(15, LowerAlphaOrdinal)) + "p" -> Some(#(16, LowerAlphaOrdinal)) + "q" -> Some(#(17, LowerAlphaOrdinal)) + "r" -> Some(#(18, LowerAlphaOrdinal)) + "s" -> Some(#(19, LowerAlphaOrdinal)) + "t" -> Some(#(20, LowerAlphaOrdinal)) + "u" -> Some(#(21, LowerAlphaOrdinal)) + "v" -> Some(#(22, LowerAlphaOrdinal)) + "w" -> Some(#(23, LowerAlphaOrdinal)) + "x" -> Some(#(24, LowerAlphaOrdinal)) + "y" -> Some(#(25, LowerAlphaOrdinal)) + "z" -> Some(#(26, LowerAlphaOrdinal)) + "A" -> Some(#(1, UpperAlphaOrdinal)) + "B" -> Some(#(2, UpperAlphaOrdinal)) + "C" -> Some(#(3, UpperAlphaOrdinal)) + "D" -> Some(#(4, UpperAlphaOrdinal)) + "E" -> Some(#(5, UpperAlphaOrdinal)) + "F" -> Some(#(6, UpperAlphaOrdinal)) + "G" -> Some(#(7, UpperAlphaOrdinal)) + "H" -> Some(#(8, UpperAlphaOrdinal)) + "I" -> Some(#(9, UpperAlphaOrdinal)) + "J" -> Some(#(10, UpperAlphaOrdinal)) + "K" -> Some(#(11, UpperAlphaOrdinal)) + "L" -> Some(#(12, UpperAlphaOrdinal)) + "M" -> Some(#(13, UpperAlphaOrdinal)) + "N" -> Some(#(14, UpperAlphaOrdinal)) + "O" -> Some(#(15, UpperAlphaOrdinal)) + "P" -> Some(#(16, UpperAlphaOrdinal)) + "Q" -> Some(#(17, UpperAlphaOrdinal)) + "R" -> Some(#(18, UpperAlphaOrdinal)) + "S" -> Some(#(19, UpperAlphaOrdinal)) + "T" -> Some(#(20, UpperAlphaOrdinal)) + "U" -> Some(#(21, UpperAlphaOrdinal)) + "V" -> Some(#(22, UpperAlphaOrdinal)) + "W" -> Some(#(23, UpperAlphaOrdinal)) + "X" -> Some(#(24, UpperAlphaOrdinal)) + "Y" -> Some(#(25, UpperAlphaOrdinal)) + "Z" -> Some(#(26, UpperAlphaOrdinal)) + _ -> None + } +} + +type ListStyle { + Bullet(BulletStyle) + Ordered(OrdinalPunctuation, OrdinalStyle) +} + pub type Inline { Linebreak NonBreakingSpace @@ -384,13 +465,14 @@ fn parse_container( "-" as style <> in2 | "*" as style <> in2 | "+" as style <> in2 -> { case parse_thematic_break(1, in2), in2 { None, " " <> in2 | None, "\n" <> in2 -> { - let style = case style { + let bullet_style = case style { "-" -> BulletDash "*" -> BulletStar - _ -> 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, 0, Tight, [], splitters) #(in, refs, Some(list), dict.new()) } None, _ -> { @@ -415,9 +497,26 @@ fn parse_container( | "8" <> _ | "9" <> _ -> { case parse_ordered_list_start(in, 0) { - Some(#(start, in2)) -> { + Some(#(style, ordinal, start, in)) -> { + let style = Ordered(style, ordinal) let #(list, in) = - parse_ordered_list(in2, refs, attrs, start, Tight, [], splitters) + parse_list(in, refs, attrs, style, start, Tight, [], splitters) + #(in, refs, Some(list), dict.new()) + } + None -> { + let #(paragraph, in) = + parse_paragraph(in, attrs, splitters, div_close_size) + #(in, refs, Some(paragraph), dict.new()) + } + } + } + + "(" <> in -> { + case parse_ordered_list_start_parens(in) { + Some(#(style, ordinal, start, in)) -> { + let style = Ordered(style, ordinal) + let #(list, in) = + parse_list(in, refs, attrs, style, start, Tight, [], splitters) #(in, refs, Some(list), dict.new()) } None -> { @@ -451,15 +550,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()) } } @@ -472,15 +568,67 @@ 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()) + } } } + "a" as i <> rest + | "b" as i <> rest + | "c" as i <> rest + | "d" as i <> rest + | "e" as i <> rest + | "f" as i <> rest + | "g" as i <> rest + | "h" as i <> rest + | "i" as i <> rest + | "j" as i <> rest + | "k" as i <> rest + | "l" as i <> rest + | "m" as i <> rest + | "n" as i <> rest + | "o" as i <> rest + | "p" as i <> rest + | "q" as i <> rest + | "r" as i <> rest + | "s" as i <> rest + | "t" as i <> rest + | "u" as i <> rest + | "v" as i <> rest + | "w" as i <> rest + | "x" as i <> rest + | "y" as i <> rest + | "z" as i <> rest + | "A" as i <> rest + | "B" as i <> rest + | "C" as i <> rest + | "D" as i <> rest + | "E" as i <> rest + | "F" as i <> rest + | "G" as i <> rest + | "H" as i <> rest + | "I" as i <> rest + | "J" as i <> rest + | "K" as i <> rest + | "L" as i <> rest + | "M" as i <> rest + | "N" as i <> rest + | "O" as i <> rest + | "P" as i <> rest + | "Q" as i <> rest + | "R" as i <> rest + | "S" as i <> rest + | "T" as i <> rest + | "U" as i <> rest + | "V" as i <> rest + | "W" as i <> rest + | "X" as i <> rest + | "Y" as i <> rest + | "Z" as i <> rest -> + parse_alpha(in, rest, refs, attrs, splitters, div_close_size, i) + _ -> { let #(paragraph, in) = parse_paragraph(in, attrs, splitters, div_close_size) @@ -489,6 +637,38 @@ fn parse_container( } } +fn parse_alpha( + in: String, + rest: String, + refs: Refs, + attrs: Dict(String, String), + splitters: Splitters, + div_close_size: Option(Int), + letter: String, +) -> #(String, Refs, Option(Container), Dict(String, String)) { + case letter_ordinal(letter) { + Some(#(num, ordinal)) -> + case parse_ordered_list_alpha(rest, num, ordinal) { + Some(#(style, ordinal, start, in2)) -> { + let style = Ordered(style, ordinal) + let #(list, in) = + parse_list(in2, refs, attrs, style, start, Tight, [], splitters) + #(in, refs, Some(list), dict.new()) + } + None -> { + let #(paragraph, in) = + parse_paragraph(in, attrs, splitters, div_close_size) + #(in, refs, Some(paragraph), dict.new()) + } + } + None -> { + let #(paragraph, in) = + parse_paragraph(in, attrs, splitters, div_close_size) + #(in, refs, Some(paragraph), dict.new()) + } + } +} + /// Parse a div. fn parse_div( in: String, @@ -1711,11 +1891,12 @@ 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: BulletStyle, + style: ListStyle, + start: Int, layout: ListLayout, items: List(List(Container)), splitters: Splitters, @@ -1723,11 +1904,16 @@ fn parse_bullet_list( 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 parse_bullet_marker(in) { - Some(#(next, in)) if next == style -> - parse_bullet_list(in, refs, attrs, style, layout, items, splitters) + case parse_list_marker(in) { + Some(#(next_style, in)) if next_style == style -> + parse_list(in, refs, attrs, style, start, layout, items, splitters) _ -> { - let container = BulletList(layout:, style:, items: list.reverse(items)) + let items = list.reverse(items) + let container = case style { + Bullet(bullet_style) -> BulletList(layout:, style: bullet_style, items:) + Ordered(ordered_style, ordinal) -> + OrderedList(layout:, style: ordered_style, ordinal:, start:, items:) + } #(container, in) } } @@ -1755,7 +1941,7 @@ fn parse_list_item( fn take_list_item_chars( in: String, acc: String, - style: BulletStyle, + style: ListStyle, layout: ListLayout, ) -> #(String, String, ListLayout) { let #(line, in) = case string.split_once(in, "\n") { @@ -1772,9 +1958,9 @@ fn take_list_item_chars( // content for the current list item. "\n " <> rest -> { let #(rest, indent) = count_drop_spaces(rest, 1) - let layout = case starts_with_list_marker(rest) { - True -> layout - False -> Loose + 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) @@ -1783,15 +1969,15 @@ fn take_list_item_chars( // A blank line followed by un-indented content, so the end of this // current list item. "\n" <> rest -> { - let layout = case parse_bullet_marker(rest) { - Some(#(next, _)) if next == style -> Loose + let layout = case parse_list_marker(rest) { + Some(#(next_style, _)) if next_style == style -> Loose _ -> layout } #(acc, rest, layout) } _ -> { - case parse_bullet_marker(in) { + case parse_list_marker(in) { Some(_) -> #(acc, in, layout) None -> take_list_item_chars(in, acc <> "\n", style, layout) } @@ -1799,18 +1985,97 @@ fn take_list_item_chars( } } -fn starts_with_list_marker(in: String) -> Bool { - case parse_bullet_marker(in) { - Some(_) -> True - None -> False +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 -> + case parse_ordered_list_start_parens(in) { + Some(#(style, ordinal, _, in)) -> Some(#(Ordered(style, ordinal), in)) + None -> None + } + + "0" <> _ + | "1" <> _ + | "2" <> _ + | "3" <> _ + | "4" <> _ + | "5" <> _ + | "6" <> _ + | "7" <> _ + | "8" <> _ + | "9" <> _ -> + case parse_ordered_list_start(in, 0) { + Some(#(style, ordinal, _, in)) -> Some(#(Ordered(style, ordinal), in)) + None -> None + } + + "a" <> in + | "b" <> in + | "c" <> in + | "d" <> in + | "e" <> in + | "f" <> in + | "g" <> in + | "h" <> in + | "i" <> in + | "j" <> in + | "k" <> in + | "l" <> in + | "m" <> in + | "n" <> in + | "o" <> in + | "p" <> in + | "q" <> in + | "r" <> in + | "s" <> in + | "t" <> in + | "u" <> in + | "v" <> in + | "w" <> in + | "x" <> in + | "y" <> in -> parse_alpha_ordinal(in, LowerAlphaOrdinal) + + "z" <> in + | "A" <> in + | "B" <> in + | "C" <> in + | "D" <> in + | "E" <> in + | "F" <> in + | "G" <> in + | "H" <> in + | "I" <> in + | "J" <> in + | "K" <> in + | "L" <> in + | "M" <> in + | "N" <> in + | "O" <> in + | "P" <> in + | "Q" <> in + | "R" <> in + | "S" <> in + | "T" <> in + | "U" <> in + | "V" <> in + | "W" <> in + | "X" <> in + | "Y" <> in + | "Z" <> in -> parse_alpha_ordinal(in, UpperAlphaOrdinal) + _ -> None } } -fn parse_bullet_marker(in: String) -> Option(#(BulletStyle, String)) { +fn parse_alpha_ordinal( + in: String, + ordinal: OrdinalStyle, +) -> Option(#(ListStyle, String)) { case in { - "- " <> rest | "-\n" <> rest -> Some(#(BulletDash, rest)) - "* " <> rest | "*\n" <> rest -> Some(#(BulletStar, rest)) - "+ " <> rest | "+\n" <> rest -> Some(#(BulletPlus, rest)) + ". " <> rest | ".\n" <> rest -> Some(#(Ordered(FullStop, ordinal), rest)) + ") " <> rest | ")\n" <> rest -> Some(#(Ordered(SingleParen, ordinal), rest)) _ -> None } } @@ -1818,7 +2083,7 @@ fn parse_bullet_marker(in: String) -> Option(#(BulletStyle, String)) { fn take_list_item_chars_indented( in: String, acc: String, - style: BulletStyle, + style: ListStyle, layout: ListLayout, indent: Int, ) -> #(String, String, ListLayout) { @@ -1836,9 +2101,9 @@ fn take_list_item_chars_indented( take_list_item_chars_indented(in, acc <> "\n", style, layout, indent) "\n " <> rest -> { - let layout = case starts_with_list_marker(drop_spaces(rest)) { - True -> layout - False -> Loose + 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) @@ -1850,8 +2115,8 @@ fn take_list_item_chars_indented( "\n" <> rest2 -> #(acc, rest2, layout) _ -> { - case parse_bullet_marker(in) { - Some(#(next, _)) if next == style -> #(acc, in, layout) + case parse_list_marker(in) { + Some(#(next_style, _)) if next_style == style -> #(acc, in, layout) _ -> take_list_item_chars_indented(in, acc <> "\n", style, layout, indent) } @@ -1867,7 +2132,10 @@ fn drop_n_spaces(in: String, count: Int) -> String { } } -fn parse_ordered_list_start(in: String, num: Int) -> Option(#(Int, String)) { +fn parse_ordered_list_start( + in: String, + num: Int, +) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { case in { "0" <> rest -> parse_ordered_list_start(rest, num * 10 + 0) "1" <> rest -> parse_ordered_list_start(rest, num * 10 + 1) @@ -1879,150 +2147,133 @@ fn parse_ordered_list_start(in: String, num: Int) -> Option(#(Int, String)) { "7" <> rest -> parse_ordered_list_start(rest, num * 10 + 7) "8" <> rest -> parse_ordered_list_start(rest, num * 10 + 8) "9" <> rest -> parse_ordered_list_start(rest, num * 10 + 9) - ". " <> rest | ".\n" <> rest -> Some(#(num, rest)) + ". " <> rest | ".\n" <> rest -> Some(#(FullStop, NumericOrdinal, num, rest)) + ") " <> rest | ")\n" <> rest -> + Some(#(SingleParen, NumericOrdinal, num, rest)) _ -> None } } -fn parse_ordered_list( +fn parse_ordered_list_start_parens( in: String, - refs: Refs, - attrs: Dict(String, String), - start: Int, - layout: ListLayout, - items: List(List(Container)), - splitters: Splitters, -) -> #(Container, String) { - let #(inline_in, in, layout) = take_ordered_list_item_chars(in, "", layout) - let item = parse_list_item(inline_in, refs, attrs, splitters, []) - let items = [item, ..items] - case drop_ordered_list_marker(in) { - Some(in) -> - parse_ordered_list(in, refs, attrs, start, layout, items, splitters) - None -> #(OrderedList(layout:, start:, items: list.reverse(items)), in) +) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { + case in { + "0" <> in -> parse_number_ordinal(in, 0) + "1" <> in -> parse_number_ordinal(in, 1) + "2" <> in -> parse_number_ordinal(in, 2) + "3" <> in -> parse_number_ordinal(in, 3) + "4" <> in -> parse_number_ordinal(in, 4) + "5" <> in -> parse_number_ordinal(in, 5) + "6" <> in -> parse_number_ordinal(in, 6) + "7" <> in -> parse_number_ordinal(in, 7) + "8" <> in -> parse_number_ordinal(in, 8) + "9" <> in -> parse_number_ordinal(in, 9) + + // Alpha: (a), (A), etc. + "a" <> in -> parse_alpha_ordinal_parens(in, 1, LowerAlphaOrdinal) + "b" <> in -> parse_alpha_ordinal_parens(in, 2, LowerAlphaOrdinal) + "c" <> in -> parse_alpha_ordinal_parens(in, 3, LowerAlphaOrdinal) + "d" <> in -> parse_alpha_ordinal_parens(in, 4, LowerAlphaOrdinal) + "e" <> in -> parse_alpha_ordinal_parens(in, 5, LowerAlphaOrdinal) + "f" <> in -> parse_alpha_ordinal_parens(in, 6, LowerAlphaOrdinal) + "g" <> in -> parse_alpha_ordinal_parens(in, 7, LowerAlphaOrdinal) + "h" <> in -> parse_alpha_ordinal_parens(in, 8, LowerAlphaOrdinal) + "i" <> in -> parse_alpha_ordinal_parens(in, 9, LowerAlphaOrdinal) + "j" <> in -> parse_alpha_ordinal_parens(in, 10, LowerAlphaOrdinal) + "k" <> in -> parse_alpha_ordinal_parens(in, 11, LowerAlphaOrdinal) + "l" <> in -> parse_alpha_ordinal_parens(in, 12, LowerAlphaOrdinal) + "m" <> in -> parse_alpha_ordinal_parens(in, 13, LowerAlphaOrdinal) + "n" <> in -> parse_alpha_ordinal_parens(in, 14, LowerAlphaOrdinal) + "o" <> in -> parse_alpha_ordinal_parens(in, 15, LowerAlphaOrdinal) + "p" <> in -> parse_alpha_ordinal_parens(in, 16, LowerAlphaOrdinal) + "q" <> in -> parse_alpha_ordinal_parens(in, 17, LowerAlphaOrdinal) + "r" <> in -> parse_alpha_ordinal_parens(in, 18, LowerAlphaOrdinal) + "s" <> in -> parse_alpha_ordinal_parens(in, 19, LowerAlphaOrdinal) + "t" <> in -> parse_alpha_ordinal_parens(in, 20, LowerAlphaOrdinal) + "u" <> in -> parse_alpha_ordinal_parens(in, 21, LowerAlphaOrdinal) + "v" <> in -> parse_alpha_ordinal_parens(in, 22, LowerAlphaOrdinal) + "w" <> in -> parse_alpha_ordinal_parens(in, 23, LowerAlphaOrdinal) + "x" <> in -> parse_alpha_ordinal_parens(in, 24, LowerAlphaOrdinal) + "y" <> in -> parse_alpha_ordinal_parens(in, 25, LowerAlphaOrdinal) + "z" <> in -> parse_alpha_ordinal_parens(in, 26, LowerAlphaOrdinal) + "A" <> in -> parse_alpha_ordinal_parens(in, 1, UpperAlphaOrdinal) + "B" <> in -> parse_alpha_ordinal_parens(in, 2, UpperAlphaOrdinal) + "C" <> in -> parse_alpha_ordinal_parens(in, 3, UpperAlphaOrdinal) + "D" <> in -> parse_alpha_ordinal_parens(in, 4, UpperAlphaOrdinal) + "E" <> in -> parse_alpha_ordinal_parens(in, 5, UpperAlphaOrdinal) + "F" <> in -> parse_alpha_ordinal_parens(in, 6, UpperAlphaOrdinal) + "G" <> in -> parse_alpha_ordinal_parens(in, 7, UpperAlphaOrdinal) + "H" <> in -> parse_alpha_ordinal_parens(in, 8, UpperAlphaOrdinal) + "I" <> in -> parse_alpha_ordinal_parens(in, 9, UpperAlphaOrdinal) + "J" <> in -> parse_alpha_ordinal_parens(in, 10, UpperAlphaOrdinal) + "K" <> in -> parse_alpha_ordinal_parens(in, 11, UpperAlphaOrdinal) + "L" <> in -> parse_alpha_ordinal_parens(in, 12, UpperAlphaOrdinal) + "M" <> in -> parse_alpha_ordinal_parens(in, 13, UpperAlphaOrdinal) + "N" <> in -> parse_alpha_ordinal_parens(in, 14, UpperAlphaOrdinal) + "O" <> in -> parse_alpha_ordinal_parens(in, 15, UpperAlphaOrdinal) + "P" <> in -> parse_alpha_ordinal_parens(in, 16, UpperAlphaOrdinal) + "Q" <> in -> parse_alpha_ordinal_parens(in, 17, UpperAlphaOrdinal) + "R" <> in -> parse_alpha_ordinal_parens(in, 18, UpperAlphaOrdinal) + "S" <> in -> parse_alpha_ordinal_parens(in, 19, UpperAlphaOrdinal) + "T" <> in -> parse_alpha_ordinal_parens(in, 20, UpperAlphaOrdinal) + "U" <> in -> parse_alpha_ordinal_parens(in, 21, UpperAlphaOrdinal) + "V" <> in -> parse_alpha_ordinal_parens(in, 22, UpperAlphaOrdinal) + "W" <> in -> parse_alpha_ordinal_parens(in, 23, UpperAlphaOrdinal) + "X" <> in -> parse_alpha_ordinal_parens(in, 24, UpperAlphaOrdinal) + "Y" <> in -> parse_alpha_ordinal_parens(in, 25, UpperAlphaOrdinal) + "Z" <> in -> parse_alpha_ordinal_parens(in, 26, UpperAlphaOrdinal) + _ -> None } } -fn take_ordered_list_item_chars( +fn parse_number_ordinal( in: String, - acc: String, - layout: ListLayout, -) -> #(String, String, ListLayout) { - let #(line, in) = case string.split_once(in, "\n") { - Ok(split) -> split - Error(_) -> #(in, "") - } - let acc = acc <> line - + num: Int, +) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { case in { - "" -> #(acc, "", layout) - " " <> _ -> take_ordered_list_item_chars(in, acc <> "\n", 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 starts_with_ordered_list_marker(rest) { - True -> layout - False -> Loose - } - let acc = acc <> "\n\n" - take_ordered_list_item_chars_indented(rest, acc, layout, indent) - } - - // A blank line followed by un-indented content, so the end of this - // current list item. - "\n" <> rest -> { - let layout = case starts_with_ordered_list_marker(rest) { - True -> Loose - False -> layout - } - #(acc, rest, layout) - } - - // Next item marker or other content - _ -> { - case starts_with_ordered_list_marker(in) { - True -> #(acc, in, layout) - False -> take_ordered_list_item_chars(in, acc <> "\n", layout) - } - } + "0" <> in -> parse_number_ordinal(in, num * 10 + 0) + "1" <> in -> parse_number_ordinal(in, num * 10 + 1) + "2" <> in -> parse_number_ordinal(in, num * 10 + 2) + "3" <> in -> parse_number_ordinal(in, num * 10 + 3) + "4" <> in -> parse_number_ordinal(in, num * 10 + 4) + "5" <> in -> parse_number_ordinal(in, num * 10 + 5) + "6" <> in -> parse_number_ordinal(in, num * 10 + 6) + "7" <> in -> parse_number_ordinal(in, num * 10 + 7) + "8" <> in -> parse_number_ordinal(in, num * 10 + 8) + "9" <> in -> parse_number_ordinal(in, num * 10 + 9) + ") " <> in | ")\n" <> in -> Some(#(DoubleParen, NumericOrdinal, num, in)) + _ -> None } } -fn take_ordered_list_item_chars_indented( +fn parse_alpha_ordinal_parens( in: String, - acc: String, - 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 - + num: Int, + ordinal: OrdinalStyle, +) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { case in { - "" -> #(acc, "", layout) - - " " <> _ -> - take_ordered_list_item_chars_indented(in, acc <> "\n", layout, indent) - - // New line with indented content, it's content for the current list item. - "\n " <> rest -> { - let layout = case starts_with_ordered_list_marker(drop_spaces(rest)) { - True -> layout - False -> Loose - } - let in = string.drop_start(in, 1) - take_ordered_list_item_chars_indented(in, acc <> "\n\n", layout, indent) - } - - // New line with other content, the current list item has ended. - "\n" <> rest2 -> #(acc, rest2, layout) - - _ -> { - case starts_with_ordered_list_marker(in) { - True -> #(acc, in, layout) - False -> { - let acc = acc <> "\n" - take_ordered_list_item_chars_indented(in, acc, layout, indent) - } - } - } + ") " <> in | ")\n" <> in -> Some(#(DoubleParen, ordinal, num, in)) + _ -> None } } -fn drop_ordered_list_marker(in: String) -> Option(String) { +fn parse_ordered_list_alpha( + in: String, + num: Int, + ordinal: OrdinalStyle, +) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { case in { - "0" <> rest - | "1" <> rest - | "2" <> rest - | "3" <> rest - | "4" <> rest - | "5" <> rest - | "6" <> rest - | "7" <> rest - | "8" <> rest - | "9" <> rest -> drop_ordered_list_marker(rest) - ". " <> rest | ".\n" <> rest -> Some(rest) + ". " <> in | ".\n" <> in -> Some(#(FullStop, ordinal, num, in)) + ") " <> in | ")\n" <> in -> Some(#(SingleParen, ordinal, num, in)) _ -> None } } -fn starts_with_ordered_list_marker(in: String) -> Bool { - case drop_ordered_list_marker(in) { - Some(_) -> True - None -> False - } -} - 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") { @@ -2033,16 +2284,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) } } @@ -2227,11 +2478,16 @@ fn container_to_html( |> close_tag("ul") } - OrderedList(layout:, start:, items:) -> { + OrderedList(layout:, style: _, 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") diff --git a/test/cases/lists.test b/test/cases/lists.test index ada937d..5369a23 100644 --- a/test/cases/lists.test +++ b/test/cases/lists.test @@ -353,3 +353,88 @@ two ``` + +``` +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. +
+``` + diff --git a/test/cases_unimplemented/lists.test b/test/cases_unimplemented/lists.test index ad0a3ce..dffbd49 100644 --- a/test/cases_unimplemented/lists.test +++ b/test/cases_unimplemented/lists.test @@ -1,73 +1,3 @@ -``` -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 From b782f384a0635e04ae8a20ffd4c55bdb4c85d166 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Wed, 14 Jan 2026 13:21:08 +0000 Subject: [PATCH 07/10] Update version --- CHANGELOG.md | 3 +-- gleam.toml | 2 +- src/jot.gleam | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d70b59..53296cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,11 @@ # Changelog -## Unreleased +## 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. -- Improved support for lists. ## v8.0.0 - 2025-11-28 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 770947c..40edbe3 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -53,7 +53,7 @@ pub type Container { ) OrderedList( layout: ListLayout, - style: OrdinalPunctuation, + punctuation: OrdinalPunctuation, ordinal: OrdinalStyle, start: Int, items: List(List(Container)), @@ -1911,8 +1911,8 @@ fn parse_list( let items = list.reverse(items) let container = case style { Bullet(bullet_style) -> BulletList(layout:, style: bullet_style, items:) - Ordered(ordered_style, ordinal) -> - OrderedList(layout:, style: ordered_style, ordinal:, start:, items:) + Ordered(punctuation, ordinal) -> + OrderedList(layout:, punctuation:, ordinal:, start:, items:) } #(container, in) } @@ -2478,7 +2478,7 @@ fn container_to_html( |> close_tag("ul") } - OrderedList(layout:, style: _, ordinal:, start:, items:) -> { + OrderedList(layout:, punctuation: _, ordinal:, start:, items:) -> { let attrs = case start { 1 -> dict.new() _ -> dict.from_list([#("start", int.to_string(start))]) From 9551de557728491dd48c13b52089f6bf714277dd Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Wed, 14 Jan 2026 14:06:40 +0000 Subject: [PATCH 08/10] Correct ordinal counting --- src/jot.gleam | 344 ++++++++++++++++++++++++++++-------------- test/cases/lists.test | 32 ++++ 2 files changed, 264 insertions(+), 112 deletions(-) diff --git a/src/jot.gleam b/src/jot.gleam index 40edbe3..c305ddb 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -140,7 +140,7 @@ fn letter_ordinal(c: String) -> Option(#(Int, OrdinalStyle)) { type ListStyle { Bullet(BulletStyle) - Ordered(OrdinalPunctuation, OrdinalStyle) + Ordered(start: Int, punctuation: OrdinalPunctuation, style: OrdinalStyle) } pub type Inline { @@ -472,7 +472,7 @@ fn parse_container( } let style = Bullet(bullet_style) let #(list, in) = - parse_list(in2, refs, attrs, style, 0, Tight, [], splitters) + parse_list(in2, refs, attrs, style, Tight, [], splitters) #(in, refs, Some(list), dict.new()) } None, _ -> { @@ -496,11 +496,11 @@ fn parse_container( | "7" <> _ | "8" <> _ | "9" <> _ -> { - case parse_ordered_list_start(in, 0) { - Some(#(style, ordinal, start, in)) -> { - let style = Ordered(style, ordinal) + case parse_number_list(in, 0, False) { + Some(#(punctuation, style, start, in)) -> { + let style = Ordered(start:, punctuation:, style:) let #(list, in) = - parse_list(in, refs, attrs, style, start, Tight, [], splitters) + parse_list(in, refs, attrs, style, Tight, [], splitters) #(in, refs, Some(list), dict.new()) } None -> { @@ -513,10 +513,10 @@ fn parse_container( "(" <> in -> { case parse_ordered_list_start_parens(in) { - Some(#(style, ordinal, start, in)) -> { - let style = Ordered(style, ordinal) + Some(#(punctuation, style, start, in)) -> { + let style = Ordered(start:, style:, punctuation:) let #(list, in) = - parse_list(in, refs, attrs, style, start, Tight, [], splitters) + parse_list(in, refs, attrs, style, Tight, [], splitters) #(in, refs, Some(list), dict.new()) } None -> { @@ -649,10 +649,10 @@ fn parse_alpha( case letter_ordinal(letter) { Some(#(num, ordinal)) -> case parse_ordered_list_alpha(rest, num, ordinal) { - Some(#(style, ordinal, start, in2)) -> { - let style = Ordered(style, ordinal) + Some(#(punctuation, style, start, in2)) -> { + let style = Ordered(start:, style:, punctuation:) let #(list, in) = - parse_list(in2, refs, attrs, style, start, Tight, [], splitters) + parse_list(in2, refs, attrs, style, Tight, [], splitters) #(in, refs, Some(list), dict.new()) } None -> { @@ -1896,7 +1896,6 @@ fn parse_list( refs: Refs, attrs: Dict(String, String), style: ListStyle, - start: Int, layout: ListLayout, items: List(List(Container)), splitters: Splitters, @@ -1904,14 +1903,13 @@ fn parse_list( 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 parse_list_marker(in) { - Some(#(next_style, in)) if next_style == style -> - parse_list(in, refs, attrs, style, start, 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(bullet_style) -> BulletList(layout:, style: bullet_style, items:) - Ordered(punctuation, ordinal) -> + Bullet(style) -> BulletList(layout:, style:, items:) + Ordered(start:, punctuation:, style: ordinal) -> OrderedList(layout:, punctuation:, ordinal:, start:, items:) } #(container, in) @@ -1968,12 +1966,12 @@ fn take_list_item_chars( // A blank line followed by un-indented content, so the end of this // current list item. - "\n" <> rest -> { - let layout = case parse_list_marker(rest) { - Some(#(next_style, _)) if next_style == style -> Loose - _ -> layout + "\n" <> in -> { + let layout = case continue_list(in, style) { + Some(_) -> Loose + None -> layout } - #(acc, rest, layout) + #(acc, in, layout) } _ -> { @@ -1990,13 +1988,16 @@ fn parse_list_marker(in: String) -> Option(#(ListStyle, String)) { "- " <> 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) + } +} - "(" <> in -> - case parse_ordered_list_start_parens(in) { - Some(#(style, ordinal, _, in)) -> Some(#(Ordered(style, ordinal), in)) - None -> None - } - +fn parse_list_marker_maybe_paren( + in: String, + paren: Bool, +) -> Option(#(ListStyle, String)) { + case in { "0" <> _ | "1" <> _ | "2" <> _ @@ -2007,75 +2008,76 @@ fn parse_list_marker(in: String) -> Option(#(ListStyle, String)) { | "7" <> _ | "8" <> _ | "9" <> _ -> - case parse_ordered_list_start(in, 0) { - Some(#(style, ordinal, _, in)) -> Some(#(Ordered(style, ordinal), in)) + case parse_number_list(in, 0, paren) { + Some(#(punctuation, style, start, in)) -> + Some(#(Ordered(start:, style:, punctuation:), in)) None -> None } - "a" <> in - | "b" <> in - | "c" <> in - | "d" <> in - | "e" <> in - | "f" <> in - | "g" <> in - | "h" <> in - | "i" <> in - | "j" <> in - | "k" <> in - | "l" <> in - | "m" <> in - | "n" <> in - | "o" <> in - | "p" <> in - | "q" <> in - | "r" <> in - | "s" <> in - | "t" <> in - | "u" <> in - | "v" <> in - | "w" <> in - | "x" <> in - | "y" <> in -> parse_alpha_ordinal(in, LowerAlphaOrdinal) - - "z" <> in - | "A" <> in - | "B" <> in - | "C" <> in - | "D" <> in - | "E" <> in - | "F" <> in - | "G" <> in - | "H" <> in - | "I" <> in - | "J" <> in - | "K" <> in - | "L" <> in - | "M" <> in - | "N" <> in - | "O" <> in - | "P" <> in - | "Q" <> in - | "R" <> in - | "S" <> in - | "T" <> in - | "U" <> in - | "V" <> in - | "W" <> in - | "X" <> in - | "Y" <> in - | "Z" <> in -> parse_alpha_ordinal(in, UpperAlphaOrdinal) - _ -> 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 + } -fn parse_alpha_ordinal( - in: String, - ordinal: OrdinalStyle, -) -> Option(#(ListStyle, String)) { - case in { - ". " <> rest | ".\n" <> rest -> Some(#(Ordered(FullStop, ordinal), rest)) - ") " <> rest | ")\n" <> rest -> Some(#(Ordered(SingleParen, ordinal), rest)) _ -> None } } @@ -2115,15 +2117,31 @@ fn take_list_item_chars_indented( "\n" <> rest2 -> #(acc, rest2, layout) _ -> { - case parse_list_marker(in) { - Some(#(next_style, _)) if next_style == style -> #(acc, in, 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 @@ -2132,24 +2150,127 @@ fn drop_n_spaces(in: String, count: Int) -> String { } } -fn parse_ordered_list_start( +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_ordered_list_start(rest, num * 10 + 0) - "1" <> rest -> parse_ordered_list_start(rest, num * 10 + 1) - "2" <> rest -> parse_ordered_list_start(rest, num * 10 + 2) - "3" <> rest -> parse_ordered_list_start(rest, num * 10 + 3) - "4" <> rest -> parse_ordered_list_start(rest, num * 10 + 4) - "5" <> rest -> parse_ordered_list_start(rest, num * 10 + 5) - "6" <> rest -> parse_ordered_list_start(rest, num * 10 + 6) - "7" <> rest -> parse_ordered_list_start(rest, num * 10 + 7) - "8" <> rest -> parse_ordered_list_start(rest, num * 10 + 8) - "9" <> rest -> parse_ordered_list_start(rest, num * 10 + 9) + "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 -> - Some(#(SingleParen, 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 } } @@ -2169,7 +2290,6 @@ fn parse_ordered_list_start_parens( "8" <> in -> parse_number_ordinal(in, 8) "9" <> in -> parse_number_ordinal(in, 9) - // Alpha: (a), (A), etc. "a" <> in -> parse_alpha_ordinal_parens(in, 1, LowerAlphaOrdinal) "b" <> in -> parse_alpha_ordinal_parens(in, 2, LowerAlphaOrdinal) "c" <> in -> parse_alpha_ordinal_parens(in, 3, LowerAlphaOrdinal) diff --git a/test/cases/lists.test b/test/cases/lists.test index 5369a23..e810930 100644 --- a/test/cases/lists.test +++ b/test/cases/lists.test @@ -438,3 +438,35 @@ two ``` +``` +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. +
+``` + From e033f92db0f111d49781db66d2a5946f30e9b31c Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Wed, 14 Jan 2026 14:24:28 +0000 Subject: [PATCH 09/10] Remove redundant code --- src/jot.gleam | 402 ++++++++++++++------------------------------------ 1 file changed, 112 insertions(+), 290 deletions(-) diff --git a/src/jot.gleam b/src/jot.gleam index c305ddb..9c0537b 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -80,64 +80,6 @@ pub type OrdinalStyle { UpperAlphaOrdinal } -fn letter_ordinal(c: String) -> Option(#(Int, OrdinalStyle)) { - case c { - "a" -> Some(#(1, LowerAlphaOrdinal)) - "b" -> Some(#(2, LowerAlphaOrdinal)) - "c" -> Some(#(3, LowerAlphaOrdinal)) - "d" -> Some(#(4, LowerAlphaOrdinal)) - "e" -> Some(#(5, LowerAlphaOrdinal)) - "f" -> Some(#(6, LowerAlphaOrdinal)) - "g" -> Some(#(7, LowerAlphaOrdinal)) - "h" -> Some(#(8, LowerAlphaOrdinal)) - "i" -> Some(#(9, LowerAlphaOrdinal)) - "j" -> Some(#(10, LowerAlphaOrdinal)) - "k" -> Some(#(11, LowerAlphaOrdinal)) - "l" -> Some(#(12, LowerAlphaOrdinal)) - "m" -> Some(#(13, LowerAlphaOrdinal)) - "n" -> Some(#(14, LowerAlphaOrdinal)) - "o" -> Some(#(15, LowerAlphaOrdinal)) - "p" -> Some(#(16, LowerAlphaOrdinal)) - "q" -> Some(#(17, LowerAlphaOrdinal)) - "r" -> Some(#(18, LowerAlphaOrdinal)) - "s" -> Some(#(19, LowerAlphaOrdinal)) - "t" -> Some(#(20, LowerAlphaOrdinal)) - "u" -> Some(#(21, LowerAlphaOrdinal)) - "v" -> Some(#(22, LowerAlphaOrdinal)) - "w" -> Some(#(23, LowerAlphaOrdinal)) - "x" -> Some(#(24, LowerAlphaOrdinal)) - "y" -> Some(#(25, LowerAlphaOrdinal)) - "z" -> Some(#(26, LowerAlphaOrdinal)) - "A" -> Some(#(1, UpperAlphaOrdinal)) - "B" -> Some(#(2, UpperAlphaOrdinal)) - "C" -> Some(#(3, UpperAlphaOrdinal)) - "D" -> Some(#(4, UpperAlphaOrdinal)) - "E" -> Some(#(5, UpperAlphaOrdinal)) - "F" -> Some(#(6, UpperAlphaOrdinal)) - "G" -> Some(#(7, UpperAlphaOrdinal)) - "H" -> Some(#(8, UpperAlphaOrdinal)) - "I" -> Some(#(9, UpperAlphaOrdinal)) - "J" -> Some(#(10, UpperAlphaOrdinal)) - "K" -> Some(#(11, UpperAlphaOrdinal)) - "L" -> Some(#(12, UpperAlphaOrdinal)) - "M" -> Some(#(13, UpperAlphaOrdinal)) - "N" -> Some(#(14, UpperAlphaOrdinal)) - "O" -> Some(#(15, UpperAlphaOrdinal)) - "P" -> Some(#(16, UpperAlphaOrdinal)) - "Q" -> Some(#(17, UpperAlphaOrdinal)) - "R" -> Some(#(18, UpperAlphaOrdinal)) - "S" -> Some(#(19, UpperAlphaOrdinal)) - "T" -> Some(#(20, UpperAlphaOrdinal)) - "U" -> Some(#(21, UpperAlphaOrdinal)) - "V" -> Some(#(22, UpperAlphaOrdinal)) - "W" -> Some(#(23, UpperAlphaOrdinal)) - "X" -> Some(#(24, UpperAlphaOrdinal)) - "Y" -> Some(#(25, UpperAlphaOrdinal)) - "Z" -> Some(#(26, UpperAlphaOrdinal)) - _ -> None - } -} - type ListStyle { Bullet(BulletStyle) Ordered(start: Int, punctuation: OrdinalPunctuation, style: OrdinalStyle) @@ -486,47 +428,6 @@ fn parse_container( } } - "0" <> _ - | "1" <> _ - | "2" <> _ - | "3" <> _ - | "4" <> _ - | "5" <> _ - | "6" <> _ - | "7" <> _ - | "8" <> _ - | "9" <> _ -> { - case parse_number_list(in, 0, False) { - Some(#(punctuation, style, start, in)) -> { - let style = Ordered(start:, punctuation:, style:) - let #(list, in) = - parse_list(in, refs, attrs, style, Tight, [], splitters) - #(in, refs, Some(list), dict.new()) - } - None -> { - let #(paragraph, in) = - parse_paragraph(in, attrs, splitters, div_close_size) - #(in, refs, Some(paragraph), dict.new()) - } - } - } - - "(" <> in -> { - case parse_ordered_list_start_parens(in) { - Some(#(punctuation, style, start, in)) -> { - let style = Ordered(start:, style:, punctuation:) - let #(list, in) = - parse_list(in, refs, attrs, style, Tight, [], splitters) - #(in, refs, Some(list), dict.new()) - } - None -> { - let #(paragraph, in) = - parse_paragraph(in, attrs, splitters, div_close_size) - #(in, refs, Some(paragraph), dict.new()) - } - } - } - "[^" <> in2 -> { case parse_footnote_def(in2, refs, splitters, "^") { None -> { @@ -575,97 +476,132 @@ fn parse_container( } } - "a" as i <> rest - | "b" as i <> rest - | "c" as i <> rest - | "d" as i <> rest - | "e" as i <> rest - | "f" as i <> rest - | "g" as i <> rest - | "h" as i <> rest - | "i" as i <> rest - | "j" as i <> rest - | "k" as i <> rest - | "l" as i <> rest - | "m" as i <> rest - | "n" as i <> rest - | "o" as i <> rest - | "p" as i <> rest - | "q" as i <> rest - | "r" as i <> rest - | "s" as i <> rest - | "t" as i <> rest - | "u" as i <> rest - | "v" as i <> rest - | "w" as i <> rest - | "x" as i <> rest - | "y" as i <> rest - | "z" as i <> rest - | "A" as i <> rest - | "B" as i <> rest - | "C" as i <> rest - | "D" as i <> rest - | "E" as i <> rest - | "F" as i <> rest - | "G" as i <> rest - | "H" as i <> rest - | "I" as i <> rest - | "J" as i <> rest - | "K" as i <> rest - | "L" as i <> rest - | "M" as i <> rest - | "N" as i <> rest - | "O" as i <> rest - | "P" as i <> rest - | "Q" as i <> rest - | "R" as i <> rest - | "S" as i <> rest - | "T" as i <> rest - | "U" as i <> rest - | "V" as i <> rest - | "W" as i <> rest - | "X" as i <> rest - | "Y" as i <> rest - | "Z" as i <> rest -> - parse_alpha(in, rest, refs, attrs, splitters, div_close_size, i) + "(" <> 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_alpha( +fn parse_maybe_list( in: String, - rest: String, refs: Refs, attrs: Dict(String, String), splitters: Splitters, - div_close_size: Option(Int), - letter: String, -) -> #(String, Refs, Option(Container), Dict(String, String)) { - case letter_ordinal(letter) { - Some(#(num, ordinal)) -> - case parse_ordered_list_alpha(rest, num, ordinal) { - Some(#(punctuation, style, start, in2)) -> { - let style = Ordered(start:, style:, punctuation:) + 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(in2, refs, attrs, style, Tight, [], splitters) - #(in, refs, Some(list), dict.new()) - } - None -> { - let #(paragraph, in) = - parse_paragraph(in, attrs, splitters, div_close_size) - #(in, refs, Some(paragraph), dict.new()) + parse_list(in, refs, attrs, style, Tight, [], splitters) + Some(#(in, refs, list)) } + None -> None } - None -> { - let #(paragraph, in) = - parse_paragraph(in, attrs, splitters, div_close_size) - #(in, refs, Some(paragraph), dict.new()) } + + "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 } } @@ -2275,120 +2211,6 @@ fn parse_upper_list( } } -fn parse_ordered_list_start_parens( - in: String, -) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { - case in { - "0" <> in -> parse_number_ordinal(in, 0) - "1" <> in -> parse_number_ordinal(in, 1) - "2" <> in -> parse_number_ordinal(in, 2) - "3" <> in -> parse_number_ordinal(in, 3) - "4" <> in -> parse_number_ordinal(in, 4) - "5" <> in -> parse_number_ordinal(in, 5) - "6" <> in -> parse_number_ordinal(in, 6) - "7" <> in -> parse_number_ordinal(in, 7) - "8" <> in -> parse_number_ordinal(in, 8) - "9" <> in -> parse_number_ordinal(in, 9) - - "a" <> in -> parse_alpha_ordinal_parens(in, 1, LowerAlphaOrdinal) - "b" <> in -> parse_alpha_ordinal_parens(in, 2, LowerAlphaOrdinal) - "c" <> in -> parse_alpha_ordinal_parens(in, 3, LowerAlphaOrdinal) - "d" <> in -> parse_alpha_ordinal_parens(in, 4, LowerAlphaOrdinal) - "e" <> in -> parse_alpha_ordinal_parens(in, 5, LowerAlphaOrdinal) - "f" <> in -> parse_alpha_ordinal_parens(in, 6, LowerAlphaOrdinal) - "g" <> in -> parse_alpha_ordinal_parens(in, 7, LowerAlphaOrdinal) - "h" <> in -> parse_alpha_ordinal_parens(in, 8, LowerAlphaOrdinal) - "i" <> in -> parse_alpha_ordinal_parens(in, 9, LowerAlphaOrdinal) - "j" <> in -> parse_alpha_ordinal_parens(in, 10, LowerAlphaOrdinal) - "k" <> in -> parse_alpha_ordinal_parens(in, 11, LowerAlphaOrdinal) - "l" <> in -> parse_alpha_ordinal_parens(in, 12, LowerAlphaOrdinal) - "m" <> in -> parse_alpha_ordinal_parens(in, 13, LowerAlphaOrdinal) - "n" <> in -> parse_alpha_ordinal_parens(in, 14, LowerAlphaOrdinal) - "o" <> in -> parse_alpha_ordinal_parens(in, 15, LowerAlphaOrdinal) - "p" <> in -> parse_alpha_ordinal_parens(in, 16, LowerAlphaOrdinal) - "q" <> in -> parse_alpha_ordinal_parens(in, 17, LowerAlphaOrdinal) - "r" <> in -> parse_alpha_ordinal_parens(in, 18, LowerAlphaOrdinal) - "s" <> in -> parse_alpha_ordinal_parens(in, 19, LowerAlphaOrdinal) - "t" <> in -> parse_alpha_ordinal_parens(in, 20, LowerAlphaOrdinal) - "u" <> in -> parse_alpha_ordinal_parens(in, 21, LowerAlphaOrdinal) - "v" <> in -> parse_alpha_ordinal_parens(in, 22, LowerAlphaOrdinal) - "w" <> in -> parse_alpha_ordinal_parens(in, 23, LowerAlphaOrdinal) - "x" <> in -> parse_alpha_ordinal_parens(in, 24, LowerAlphaOrdinal) - "y" <> in -> parse_alpha_ordinal_parens(in, 25, LowerAlphaOrdinal) - "z" <> in -> parse_alpha_ordinal_parens(in, 26, LowerAlphaOrdinal) - "A" <> in -> parse_alpha_ordinal_parens(in, 1, UpperAlphaOrdinal) - "B" <> in -> parse_alpha_ordinal_parens(in, 2, UpperAlphaOrdinal) - "C" <> in -> parse_alpha_ordinal_parens(in, 3, UpperAlphaOrdinal) - "D" <> in -> parse_alpha_ordinal_parens(in, 4, UpperAlphaOrdinal) - "E" <> in -> parse_alpha_ordinal_parens(in, 5, UpperAlphaOrdinal) - "F" <> in -> parse_alpha_ordinal_parens(in, 6, UpperAlphaOrdinal) - "G" <> in -> parse_alpha_ordinal_parens(in, 7, UpperAlphaOrdinal) - "H" <> in -> parse_alpha_ordinal_parens(in, 8, UpperAlphaOrdinal) - "I" <> in -> parse_alpha_ordinal_parens(in, 9, UpperAlphaOrdinal) - "J" <> in -> parse_alpha_ordinal_parens(in, 10, UpperAlphaOrdinal) - "K" <> in -> parse_alpha_ordinal_parens(in, 11, UpperAlphaOrdinal) - "L" <> in -> parse_alpha_ordinal_parens(in, 12, UpperAlphaOrdinal) - "M" <> in -> parse_alpha_ordinal_parens(in, 13, UpperAlphaOrdinal) - "N" <> in -> parse_alpha_ordinal_parens(in, 14, UpperAlphaOrdinal) - "O" <> in -> parse_alpha_ordinal_parens(in, 15, UpperAlphaOrdinal) - "P" <> in -> parse_alpha_ordinal_parens(in, 16, UpperAlphaOrdinal) - "Q" <> in -> parse_alpha_ordinal_parens(in, 17, UpperAlphaOrdinal) - "R" <> in -> parse_alpha_ordinal_parens(in, 18, UpperAlphaOrdinal) - "S" <> in -> parse_alpha_ordinal_parens(in, 19, UpperAlphaOrdinal) - "T" <> in -> parse_alpha_ordinal_parens(in, 20, UpperAlphaOrdinal) - "U" <> in -> parse_alpha_ordinal_parens(in, 21, UpperAlphaOrdinal) - "V" <> in -> parse_alpha_ordinal_parens(in, 22, UpperAlphaOrdinal) - "W" <> in -> parse_alpha_ordinal_parens(in, 23, UpperAlphaOrdinal) - "X" <> in -> parse_alpha_ordinal_parens(in, 24, UpperAlphaOrdinal) - "Y" <> in -> parse_alpha_ordinal_parens(in, 25, UpperAlphaOrdinal) - "Z" <> in -> parse_alpha_ordinal_parens(in, 26, UpperAlphaOrdinal) - _ -> None - } -} - -fn parse_number_ordinal( - in: String, - num: Int, -) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { - case in { - "0" <> in -> parse_number_ordinal(in, num * 10 + 0) - "1" <> in -> parse_number_ordinal(in, num * 10 + 1) - "2" <> in -> parse_number_ordinal(in, num * 10 + 2) - "3" <> in -> parse_number_ordinal(in, num * 10 + 3) - "4" <> in -> parse_number_ordinal(in, num * 10 + 4) - "5" <> in -> parse_number_ordinal(in, num * 10 + 5) - "6" <> in -> parse_number_ordinal(in, num * 10 + 6) - "7" <> in -> parse_number_ordinal(in, num * 10 + 7) - "8" <> in -> parse_number_ordinal(in, num * 10 + 8) - "9" <> in -> parse_number_ordinal(in, num * 10 + 9) - ") " <> in | ")\n" <> in -> Some(#(DoubleParen, NumericOrdinal, num, in)) - _ -> None - } -} - -fn parse_alpha_ordinal_parens( - in: String, - num: Int, - ordinal: OrdinalStyle, -) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { - case in { - ") " <> in | ")\n" <> in -> Some(#(DoubleParen, ordinal, num, in)) - _ -> None - } -} - -fn parse_ordered_list_alpha( - in: String, - num: Int, - ordinal: OrdinalStyle, -) -> Option(#(OrdinalPunctuation, OrdinalStyle, Int, String)) { - case in { - ". " <> in | ".\n" <> in -> Some(#(FullStop, ordinal, num, in)) - ") " <> in | ")\n" <> in -> Some(#(SingleParen, ordinal, num, in)) - _ -> None - } -} - fn take_paragraph_chars( in: String, div_close_size: Option(Int), From 8e1a7ebf7ba3806a254187892caeabf7e3639c46 Mon Sep 17 00:00:00 2001 From: Louis Pilfold Date: Wed, 14 Jan 2026 14:32:03 +0000 Subject: [PATCH 10/10] Remove extra comment --- src/jot.gleam | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/jot.gleam b/src/jot.gleam index 9c0537b..076dba5 100644 --- a/src/jot.gleam +++ b/src/jot.gleam @@ -2588,7 +2588,6 @@ fn list_items_to_html( case items { [] -> html - // Tight list with single paragraph - no

tag [[Paragraph(_, inlines)], ..rest] if layout == Tight -> { html |> open_tag("li", dict.new()) @@ -2600,7 +2599,6 @@ fn list_items_to_html( |> list_items_to_html(layout, rest, refs) } - // Tight list with paragraph followed by nested list - no

tag for paragraph [[Paragraph(_, inlines), nested_list, ..item_rest], ..rest] if layout == Tight -> {