A Markdown shard for the Crystal programming language
修訂 | f00309f94962bce86a760ecd7c103afa79ed9010 (tree) |
---|---|
時間 | 2023-12-28 12:19:51 |
作者 | supercell <stigma@disr...> |
Commiter | supercell |
Add support for AlertBlockSyntaxes
@@ -5,6 +5,7 @@ All notable changes to Luce will be documented in this file. | ||
5 | 5 | ## [Unreleased] |
6 | 6 | |
7 | 7 | * Support for [footnotes](https://pandoc.org/MANUAL.html#footnotes). |
8 | +* Support for [Github Alerts][gh-alerts] | |
8 | 9 | * Tables are now able to interrupt other blocks. |
9 | 10 | * Allow Element attributes to have no value ([#6][issue-6] - thanks [solvin]!). |
10 | 11 | * Fix an infinite loop occurring when using a link in table headers. |
@@ -16,6 +17,7 @@ All notable changes to Luce will be documented in this file. | ||
16 | 17 | ([#3][issue-3] - thanks [solvin]!) |
17 | 18 | * Fix crash in Crystal >=1.8 caused by exceeding the Regex Match Limit. |
18 | 19 | |
20 | +[gh-alerts]: https://docs.github.com/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts | |
19 | 21 | [issue-3]: https://codeberg.org/supercell/luce/pulls/3 |
20 | 22 | [issue-6]: https://codeberg.org/supercell/luce/pulls/6 |
21 | 23 | [solvin]: https://codeberg.org/solvin |
@@ -0,0 +1,94 @@ | ||
1 | +>>> type note | |
2 | +> [!NoTe] | |
3 | +> Test note alert. | |
4 | +<<< | |
5 | +<div class="markdown-alert markdown-alert-note"> | |
6 | +<p class="markdown-alert-title">Note</p> | |
7 | +<p>Test note alert.</p> | |
8 | +</div> | |
9 | +>>> type tip | |
10 | +> [!TiP] | |
11 | +> Test tip alert. | |
12 | +<<< | |
13 | +<div class="markdown-alert markdown-alert-tip"> | |
14 | +<p class="markdown-alert-title">Tip</p> | |
15 | +<p>Test tip alert.</p> | |
16 | +</div> | |
17 | +>>> type important | |
18 | +> [!ImpoRtanT] | |
19 | +> Test important alert. | |
20 | +<<< | |
21 | +<div class="markdown-alert markdown-alert-important"> | |
22 | +<p class="markdown-alert-title">Important</p> | |
23 | +<p>Test important alert.</p> | |
24 | +</div> | |
25 | +>>> type warning | |
26 | +> [!WarNinG] | |
27 | +> Test warning alert. | |
28 | +<<< | |
29 | +<div class="markdown-alert markdown-alert-warning"> | |
30 | +<p class="markdown-alert-title">Warning</p> | |
31 | +<p>Test warning alert.</p> | |
32 | +</div> | |
33 | +>>> type caution | |
34 | +> [!CauTioN] | |
35 | +> Test caution alert. | |
36 | +<<< | |
37 | +<div class="markdown-alert markdown-alert-caution"> | |
38 | +<p class="markdown-alert-title">Caution</p> | |
39 | +<p>Test caution alert.</p> | |
40 | +</div> | |
41 | +>>> invalid type | |
42 | +> [!foo] | |
43 | +> Test foo alert. | |
44 | +<<< | |
45 | +<blockquote> | |
46 | +<p>[!foo] | |
47 | +Test foo alert.</p> | |
48 | +</blockquote> | |
49 | +>>> contents can both contain/not contain starting quote | |
50 | +> [!NOTE] | |
51 | +Test note alert. | |
52 | +>Test note alert x2. | |
53 | +<<< | |
54 | +<div class="markdown-alert markdown-alert-note"> | |
55 | +<p class="markdown-alert-title">Note</p> | |
56 | +<p>Test note alert. | |
57 | +Test note alert x2.</p> | |
58 | +</div> | |
59 | +>>> spaces everywhere | |
60 | + > [!NOTE] | |
61 | +> Test note alert. | |
62 | + > Test note alert x2. | |
63 | +<<< | |
64 | +<div class="markdown-alert markdown-alert-note"> | |
65 | +<p class="markdown-alert-title">Note</p> | |
66 | +<p>Test note alert. | |
67 | +Test note alert x2.</p> | |
68 | +</div> | |
69 | +>>> title has 3 more spaces then fallback to blockquote | |
70 | +> [!NOTE] | |
71 | +> Test blockquote. | |
72 | +<<< | |
73 | +<blockquote> | |
74 | +<p>[!NOTE] | |
75 | +Test blockquote.</p> | |
76 | +</blockquote> | |
77 | +>>>nested blockquote | |
78 | +> [!NOTE] | |
79 | +>> Test nested blockquote. | |
80 | +<<< | |
81 | +<div class="markdown-alert markdown-alert-note"> | |
82 | +<p class="markdown-alert-title">Note</p> | |
83 | +<blockquote> | |
84 | +<p>Test nested blockquote.</p> | |
85 | +</blockquote> | |
86 | +</div> | |
87 | +>>>escape brackets | |
88 | +> \[!note\] | |
89 | +> Test escape brackets. | |
90 | +<<< | |
91 | +<div class="markdown-alert markdown-alert-note"> | |
92 | +<p class="markdown-alert-title">Note</p> | |
93 | +<p>Test escape brackets.</p> | |
94 | +</div> |
@@ -97,6 +97,8 @@ test_file "extensions/unordered_list_with_checkboxes.unit", | ||
97 | 97 | block_syntaxes: [Luce::UnorderedListWithCheckboxSyntax.new] |
98 | 98 | test_file "extensions/footnote_block.unit", |
99 | 99 | block_syntaxes: [Luce::FootnoteDefSyntax.new] |
100 | +test_file "extensions/alert_extension.unit", | |
101 | + block_syntaxes: [Luce::AlertBlockSyntax.new] | |
100 | 102 | |
101 | 103 | # Inline syntax extensions |
102 | 104 | test_file "extensions/autolink_extension.unit", |
@@ -5,6 +5,7 @@ | ||
5 | 5 | # |
6 | 6 | require "./luce/ast" |
7 | 7 | require "./luce/block_parser" |
8 | +require "./luce/block_syntaxes/alert_block_syntax" | |
8 | 9 | require "./luce/block_syntaxes/block_syntax" |
9 | 10 | require "./luce/block_syntaxes/blockquote_syntax" |
10 | 11 | require "./luce/block_syntaxes/code_block_syntax" |
@@ -0,0 +1,94 @@ | ||
1 | +# | |
2 | +# Copyright (c) 2023 supercell | |
3 | +# | |
4 | +# SPDX-License-Identifier: BSD-3-Clause | |
5 | +# | |
6 | +require "./block_syntax" | |
7 | + | |
8 | +# Parses GitHub Alert blocks | |
9 | +# | |
10 | +# See also: https://docs.github.com/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts | |
11 | +module Luce | |
12 | + class AlertBlockSyntax < BlockSyntax | |
13 | + # Whether this alert ends with a lazy continuation line. | |
14 | + # | |
15 | + # The definition of lazy continuation lines: | |
16 | + # https://spec.commonmark.org/0.30/#lazy-continuation-line | |
17 | + @lazy_continuation = false | |
18 | + @content_line_regexp = Regex.new(%q{>?\s?(.*)*}) | |
19 | + | |
20 | + def pattern : Regex | |
21 | + Luce.alert_pattern | |
22 | + end | |
23 | + | |
24 | + def can_parse?(parser : BlockParser) : Bool | |
25 | + pattern.matches?(parser.current.content) && parser.lines.any?(&.content.matches?(@content_line_regexp)) | |
26 | + end | |
27 | + | |
28 | + def parse_child_lines(parser : BlockParser) : Array(Line) | |
29 | + # Grab all of the lines that form the alert, stripping off the ">". | |
30 | + child_lines = [] of Line | |
31 | + @lazy_continuation = false | |
32 | + | |
33 | + until parser.done? | |
34 | + stripped_content = parser.current.content.sub(/^\s*>?\s*/, "") | |
35 | + match = @content_line_regexp.match(stripped_content) | |
36 | + unless match.nil? | |
37 | + child_lines << Line.new(stripped_content) | |
38 | + parser.advance | |
39 | + @lazy_continuation = false | |
40 | + next | |
41 | + end | |
42 | + | |
43 | + last_line = child_lines.last | |
44 | + | |
45 | + # A paragraph continuation is OK. This is content that cannot be parsed | |
46 | + # as any other syntax except Paragraph, and it doesn't match the bar in | |
47 | + # a Setext Header. | |
48 | + # Because indented code blocks cannot interrupt paragraphs, a line | |
49 | + # matched CodeBlockSyntax is also paragraph continuation text. | |
50 | + other_matched = parser.block_syntaxes.find(&.can_parse?(parser)) | |
51 | + if (other_matched.is_a?(ParagraphSyntax) && !last_line.is_blank_line? && !Luce.code_fence_pattern.matches?(last_line.content)) || | |
52 | + (other_matched.is_a?(CodeBlockSyntax) && !Luce.indent_pattern.matches?(last_line.content)) | |
53 | + child_lines << parser.current | |
54 | + @lazy_continuation = true | |
55 | + parser.advance | |
56 | + else | |
57 | + break | |
58 | + end | |
59 | + end | |
60 | + | |
61 | + child_lines | |
62 | + end | |
63 | + | |
64 | + def parse(parser : BlockParser) : Node? | |
65 | + # Parse the alert type from the first line. | |
66 | + type = pattern.match(parser.current.content).not_nil![1].not_nil!.downcase | |
67 | + parser.advance | |
68 | + child_lines = parse_child_lines(parser) | |
69 | + # Recursively parse the contents of the alert. | |
70 | + children = BlockParser.new(child_lines, parser.document).parse_lines( | |
71 | + # The setext heading underline cannot be a lazy continuation line in a | |
72 | + # block quote. | |
73 | + # https://spec.commonmark.org/0.30/#example-93 | |
74 | + disable_setext_heading: @lazy_continuation, | |
75 | + parent_syntax: self) | |
76 | + | |
77 | + # Mapping the alert title text. | |
78 | + type_text_map = { | |
79 | + "note" => "Note", | |
80 | + "tip" => "Tip", | |
81 | + "important" => "Important", | |
82 | + "caution" => "Caution", | |
83 | + "warning" => "Warning", | |
84 | + } | |
85 | + title_text = type_text_map[type] | |
86 | + title_element = Element.new("p", [Text.new(title_text)] of Node) | |
87 | + title_element.attributes["class"] = "markdown-alert-title" | |
88 | + element_class = "markdown-alert markdown-alert-#{type.downcase}" | |
89 | + element = Element.new("div", [title_element] + children) | |
90 | + element.attributes["class"] = element_class | |
91 | + element | |
92 | + end | |
93 | + end | |
94 | +end |
@@ -44,6 +44,7 @@ module Luce | ||
44 | 44 | UnorderedListWithCheckboxSyntax.new, |
45 | 45 | OrderedListWithCheckboxSyntax.new, |
46 | 46 | FootnoteDefSyntax.new, |
47 | + AlertBlockSyntax.new, | |
47 | 48 | ], |
48 | 49 | [ |
49 | 50 | InlineHTMLSyntax.new, |
@@ -85,8 +85,13 @@ module Luce | ||
85 | 85 | @@html_characters_pattern = Regex.new("&(?:([a-z0-9]+)|#([0-9]{1,7})|#x([a-f0-9]{1,6}));", |
86 | 86 | Regex::Options::IGNORE_CASE) |
87 | 87 | |
88 | + # :nodoc: | |
88 | 89 | @@link_reference_definition_pattern = Regex.new(%q{^[ ]{0,3}\[}) |
89 | 90 | |
91 | + # :nodoc: | |
92 | + @@alert_pattern = Regex.new(%q{^\s{0,3}>\s{0,3}\\?\[!(note|tip|important|caution|warning)\\?\]\s*$}, | |
93 | + Regex::Options::IGNORE_CASE) | |
94 | + | |
90 | 95 | # The line contains only whitespace or is empty |
91 | 96 | def self.empty_pattern : Regex |
92 | 97 | @@empty_pattern |
@@ -197,4 +202,12 @@ module Luce | ||
197 | 202 | def self.link_reference_definition_pattern : Regex |
198 | 203 | @@link_reference_definition_pattern |
199 | 204 | end |
205 | + | |
206 | + # Alert type patterns. | |
207 | + # A alert block is similar to a blockquote, | |
208 | + # starts with `> [!TYPE]`, and only 5 types are supported | |
209 | + # with case-insensitive. | |
210 | + def self.alert_pattern : Regex | |
211 | + @@alert_pattern | |
212 | + end | |
200 | 213 | end |