1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
20 # Parse a markdown string and split it in blocks.
22 # Blocks are then outputed by an `MarkdownEmitter`.
26 # var proc = new MarkdownProcessor
27 # var html = proc.process("**Hello World!**")
28 # assert html == "<p><strong>Hello World!</strong></p>\n"
30 # SEE: `String::md_to_html` for a shortcut.
31 class MarkdownProcessor
33 var emitter
: MarkdownEmitter is noinit
35 init do self.emitter
= new MarkdownEmitter(self)
37 # Process the mardown `input` string and return the processed output.
38 fun process
(input
: String): Streamable do
45 var parent
= read_lines
(input
)
46 parent
.remove_surrounding_empty_lines
47 recurse
(parent
, false)
48 # output processed text
49 return emitter
.emit
(parent
.kind
)
52 # Split `input` string into `MDLines` and create a parent `MDBlock` with it.
53 private fun read_lines
(input
: String): MDBlock do
54 var block
= new MDBlock
55 var value
= new FlatBuffer
57 while i
< input
.length
do
61 while not eol
and i
< input
.length
do
66 else if c
== '\t' then
67 var np
= pos
+ (4 - (pos
.bin_and
(3)))
80 var line
= new MDLine(value
.write_to_string
)
81 var is_link_ref
= check_link_ref
(line
)
83 if not is_link_ref
then block
.add_line line
88 # Check if line is a block link definition.
89 # Return `true` if line contains a valid link ref and save it into `link_refs`.
90 private fun check_link_ref
(line
: MDLine): Bool do
92 var is_link_ref
= false
93 var id
= new FlatBuffer
94 var link
= new FlatBuffer
95 var comment
= new FlatBuffer
97 if not line
.is_empty
and line
.leading
< 4 and line
.value
[line
.leading
] == '[' then
98 pos
= line
.leading
+ 1
99 pos
= md
.read_until
(id
, pos
, ']')
100 if not id
.is_empty
and pos
+ 2 < line
.value
.length
then
101 if line
.value
[pos
+ 1] == ':' then
103 pos
= md
.skip_spaces
(pos
)
104 if line
.value
[pos
] == '<' then
106 pos
= md
.read_until
(link
, pos
, '>')
109 pos
= md
.read_until
(link
, pos
, ' ', '\n')
111 if not link
.is_empty
then
112 pos
= md
.skip_spaces
(pos
)
113 if pos
> 0 and pos
< line
.value
.length
then
114 var c
= line
.value
[pos
]
115 if c
== '\"' or c
== '\'' or c == '(' then
118 pos = md.read_until(comment, pos, ')')
120 pos = md.read_until(comment, pos, c)
122 if pos > 0 then is_link_ref = true
131 if is_link_ref and not id.is_empty and not link.is_empty then
132 var lr = new LinkRef.with_title(link.write_to_string, comment.write_to_string)
133 add_link_ref(id.write_to_string, lr)
134 if comment.is_empty then last_link_ref = lr
137 comment = new FlatBuffer
138 if not line.is_empty and last_link_ref != null then
140 var c = line.value[pos]
141 if c == '\
"' or c == '\'' or c == '(' then
144 pos = md.read_until(comment, pos, ')')
146 pos = md.read_until(comment, pos, c)
149 if not comment.is_empty then last_link_ref.title = comment.write_to_string
151 if comment.is_empty then return false
157 # This list will be needed during output to expand links.
158 var link_refs: Map[String, LinkRef] = new HashMap[String, LinkRef]
160 # Last encountered link ref (for multiline definitions)
162 # Markdown allows link refs to be defined over two lines:
164 # [id]: http://example.com/longish/path/to/resource/here
165 # "Optional Title Here"
167 private var last_link_ref: nullable LinkRef = null
169 # Add a link ref to the list
170 fun add_link_ref(key: String, ref: LinkRef) do link_refs[key.to_lower] = ref
172 # Recursively split a `block`.
174 # The block is splitted according to the type of lines it contains.
175 # Some blocks can be splited again recursively like lists.
176 # The `in_list` mode is used to recurse on list and build
177 # nested paragraphs or code blocks.
178 fun recurse(root: MDBlock, in_list: Bool) do
179 var old_mode = self.in_list
180 var old_root = self.current_block
181 self.in_list = in_list
183 var line = root.first_line
184 while line != null and line.is_empty do
186 if line == null then return
191 while current_line != null do
192 current_line.kind(self).process(self)
194 self.in_list = old_mode
195 self.current_block = old_root
198 # Currently processed line.
199 # Used when visiting blocks with `recurse`.
200 var current_line: nullable MDLine = null is writable
202 # Currently processed block.
203 # Used when visiting blocks with `recurse`.
204 var current_block: nullable MDBlock = null is writable
206 # Is the current recursion in list mode?
207 # Used when visiting blocks with `recurse`
208 private var in_list = false
211 # Emit output corresponding to blocks content.
213 # Blocks are created by a previous pass in `MarkdownProcessor`.
214 # The emitter use a `Decorator` to select the output format.
215 class MarkdownEmitter
217 # Processor containing link refs.
218 var processor: MarkdownProcessor
220 # Decorator used for output.
221 # Default is `HTMLDecorator`
222 var decorator: Decorator = new HTMLDecorator is writable
224 # Create a new `MardownEmitter` using the default `HTMLDecorator`
225 init(processor: MarkdownProcessor) do
226 self.processor = processor
229 # Create a new `MarkdownEmitter` using a custom `decorator`.
230 init with_decorator(processor: MarkdownProcessor, decorator: Decorator) do
232 self.decorator = decorator
235 # Output `block` using `decorator` in the current buffer.
236 fun emit(block: Block): Text do
237 var buffer = push_buffer
243 # Output the content of `block`.
244 fun emit_in(block: Block) do block.emit_in(self)
246 # Transform and emit mardown text
247 fun emit_text(text: Text) do
248 emit_text_until(text, 0, null)
251 # Transform and emit mardown text starting at `from` and
252 # until a token with the same type as `token` is found.
253 # Go until the end of text if `token` is null.
254 fun emit_text_until(text: Text, start: Int, token: nullable Token): Int do
255 var old_text = current_text
256 var old_pos = current_pos
259 while current_pos < text.length do
260 var mt = text.token_at(current_pos)
261 if (token != null and not token isa TokenNone) and
262 (mt.is_same_type(token) or
263 (token isa TokenEmStar and mt isa TokenStrongStar) or
264 (token isa TokenEmUnderscore and mt isa TokenStrongUnderscore)) then
270 current_text = old_text
271 current_pos = old_pos
275 # Currently processed position in `current_text`.
276 # Used when visiting inline production with `emit_text_until`.
277 private var current_pos: Int = -1
279 # Currently processed text.
280 # Used when visiting inline production with `emit_text_until`.
281 private var current_text: nullable Text = null
284 private var buffer_stack = new List[FlatBuffer]
286 # Push a new buffer on the stack.
287 private fun push_buffer: FlatBuffer do
288 var buffer = new FlatBuffer
289 buffer_stack.add buffer
293 # Pop the last buffer.
294 private fun pop_buffer do buffer_stack.pop
296 # Current output buffer.
297 private fun current_buffer: FlatBuffer do
298 assert not buffer_stack.is_empty
299 return buffer_stack.last
302 # Append `e` to current buffer.
303 fun add(e: Streamable) do
305 current_buffer.append e
307 current_buffer.append e.write_to_string
311 # Append `c` to current buffer.
312 fun addc(c: Char) do current_buffer.add c
314 # Append a "\n
" line break.
315 fun addn do current_buffer.add '\n'
319 # Links that are specified somewhere in the mardown document to be reused as shortcuts.
323 # [1]: http://example.com/ "Optional title
"
329 # Optional link title
330 var title: nullable String = null
332 # Is the link an abreviation?
333 var is_abbrev = false
335 init with_title(link: String, title: nullable String) do
341 # A `Decorator` is used to emit mardown into a specific format.
342 # Default decorator used is `HTMLDecorator`.
345 # Render a ruler block.
346 fun add_ruler(v: MarkdownEmitter, block: BlockRuler) is abstract
348 # Render a headline block with corresponding level.
349 fun add_headline(v: MarkdownEmitter, block: BlockHeadline) is abstract
351 # Render a paragraph block.
352 fun add_paragraph(v: MarkdownEmitter, block: BlockParagraph) is abstract
354 # Render a code or fence block.
355 fun add_code(v: MarkdownEmitter, block: BlockCode) is abstract
357 # Render a blockquote.
358 fun add_blockquote(v: MarkdownEmitter, block: BlockQuote) is abstract
360 # Render an unordered list.
361 fun add_unorderedlist(v: MarkdownEmitter, block: BlockUnorderedList) is abstract
363 # Render an ordered list.
364 fun add_orderedlist(v: MarkdownEmitter, block: BlockOrderedList) is abstract
366 # Render a list item.
367 fun add_listitem(v: MarkdownEmitter, block: BlockListItem) is abstract
369 # Render an emphasis text.
370 fun add_em(v: MarkdownEmitter, text: Text) is abstract
372 # Render a strong text.
373 fun add_strong(v: MarkdownEmitter, text: Text) is abstract
375 # Render a super text.
376 fun add_super(v: MarkdownEmitter, text: Text) is abstract
379 fun add_link(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract
382 fun add_image(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract
384 # Render an abbreviation.
385 fun add_abbr(v: MarkdownEmitter, name: Text, comment: Text) is abstract
387 # Render a code span reading from a buffer.
388 fun add_span_code(v: MarkdownEmitter, buffer: Text, from, to: Int) is abstract
390 # Render a text and escape it.
391 fun append_value(v: MarkdownEmitter, value: Text) is abstract
393 # Render code text from buffer and escape it.
394 fun append_code(v: MarkdownEmitter, buffer: Text, from, to: Int) is abstract
396 # Render a character escape.
397 fun escape_char(v: MarkdownEmitter, char: Char) is abstract
399 # Render a line break
400 fun add_line_break(v: MarkdownEmitter) is abstract
403 # `Decorator` that outputs HTML.
407 redef fun add_ruler(v, block) do v.add "<hr
/>\n
"
409 redef fun add_headline(v, block) do
410 v.add "<h
{block.depth}>"
412 v.add "</h
{block.depth}>\n
"
415 redef fun add_paragraph(v, block) do
421 redef fun add_code(v, block) do
424 v.add "</code
></pre
>\n
"
427 redef fun add_blockquote(v, block) do
428 v.add "<blockquote
>\n
"
430 v.add "</blockquote
>\n
"
433 redef fun add_unorderedlist(v, block) do
439 redef fun add_orderedlist(v, block) do
445 redef fun add_listitem(v, block) do
451 redef fun add_em(v, text) do
457 redef fun add_strong(v, text) do
463 redef fun add_super(v, text) do
469 redef fun add_image(v, link, name, comment) do
471 append_value
(v
, link
)
473 append_value
(v
, name
)
475 if comment != null and not comment.is_empty then
477 append_value
(v
, comment
)
483 redef fun add_link(v, link, name, comment) do
485 append_value
(v
, link
)
487 if comment != null and not comment.is_empty then
489 append_value
(v
, comment
)
497 redef fun add_abbr(v, name, comment) do
498 v.add "<abbr title
=\
""
499 append_value
(v
, comment
)
505 redef fun add_span_code(v, text, from, to) do
507 append_code(v, text, from, to)
511 redef fun add_line_break(v) do
515 redef fun append_value(v, text) do for c in text do escape_char(v, c)
517 redef fun escape_char(v, c) do
520 else if c == '<' then
522 else if c == '>' then
524 else if c == '"' then
526 else if c == '\
'' then
533 redef fun append_code
(v
, buffer
, from
, to
) do
534 for i
in [from
..to
[ do
538 else if c
== '<' then
540 else if c
== '>' then
549 # A block of markdown lines.
550 # A `MDBlock` can contains lines and/or sub-blocks.
554 var kind
: Block = new BlockNone(self) is writable
557 var first_line
: nullable MDLine = null is writable
560 var last_line
: nullable MDLine = null is writable
562 # First sub-block if any.
563 var first_block
: nullable MDBlock = null is writable
565 # Last sub-block if any.
566 var last_block
: nullable MDBlock = null is writable
568 # Previous block if any.
569 var prev
: nullable MDBlock = null is writable
572 var next
: nullable MDBlock = null is writable
574 # Does this block contain subblocks?
575 fun has_blocks
: Bool do return first_block
!= null
578 fun count_blocks
: Int do
580 var block
= first_block
581 while block
!= null do
588 # Does this block contain lines?
589 fun has_lines
: Bool do return first_line
!= null
592 fun count_lines
: Int do
594 var line
= first_line
595 while line
!= null do
602 # Split `self` creating a new sub-block having `line` has `last_line`.
603 fun split
(line
: MDLine): MDBlock do
604 var block
= new MDBlock
605 block
.first_line
= first_line
606 block
.last_line
= line
607 first_line
= line
.next
609 if first_line
== null then
612 first_line
.prev
= null
614 if first_block
== null then
618 last_block
.next
= block
624 # Add a `line` to this block.
625 fun add_line
(line
: MDLine) do
626 if last_line
== null then
630 last_line
.next_empty
= line
.is_empty
631 line
.prev_empty
= last_line
.is_empty
632 line
.prev
= last_line
633 last_line
.next
= line
638 # Remove `line` from this block.
639 fun remove_line
(line
: MDLine) do
640 if line
.prev
== null then
641 first_line
= line
.next
643 line
.prev
.next
= line
.next
645 if line
.next
== null then
646 last_line
= line
.prev
648 line
.next
.prev
= line
.prev
654 # Remove leading empty lines.
655 fun remove_leading_empty_lines
: Bool do
656 var was_empty
= false
657 var line
= first_line
658 while line
!= null and line
.is_empty
do
666 # Remove trailing empty lines.
667 fun remove_trailing_empty_lines
: Bool do
668 var was_empty
= false
670 while line
!= null and line
.is_empty
do
678 # Remove leading and trailing empty lines.
679 fun remove_surrounding_empty_lines
: Bool do
680 var was_empty
= false
681 if remove_leading_empty_lines
then was_empty
= true
682 if remove_trailing_empty_lines
then was_empty
= true
686 # Remove list markers and up to 4 leading spaces.
687 # Used to clean nested lists.
688 fun remove_list_indent
(v
: MarkdownProcessor) do
689 var line
= first_line
690 while line
!= null do
691 if not line
.is_empty
then
692 var kind
= line
.kind
(v
)
693 if kind
isa LineList then
694 line
.value
= kind
.extract_value
(line
)
696 line
.value
= line
.value
.substring_from
(line
.leading
.min
(4))
698 line
.leading
= line
.process_leading
704 # Collect block line text.
706 var text
= new FlatBuffer
707 var line
= first_line
708 while line
!= null do
709 if not line
.is_empty
then
710 text
.append line
.text
715 return text
.write_to_string
719 # Representation of a markdown block in the AST.
720 # Each `Block` is linked to a `MDBlock` that contains mardown code.
723 # The markdown block `self` is related to.
726 # Output `self` using `v.decorator`.
727 fun emit
(v
: MarkdownEmitter) do v
.emit_in
(self)
729 # Emit the containts of `self`, lines or blocks.
730 fun emit_in
(v
: MarkdownEmitter) do
731 block
.remove_surrounding_empty_lines
732 if block
.has_lines
then
739 # Emit lines contained in `block`.
740 fun emit_lines
(v
: MarkdownEmitter) do
741 var tpl
= v
.push_buffer
742 var line
= block
.first_line
743 while line
!= null do
744 if not line
.is_empty
then
745 v
.add line
.value
.substring
(line
.leading
, line
.value
.length
- line
.trailing
)
746 if line
.trailing
>= 2 then v
.decorator
.add_line_break
(v
)
748 if line
.next
!= null then
757 # Emit sub-blocks contained in `block`.
758 fun emit_blocks
(v
: MarkdownEmitter) do
759 var block
= self.block
.first_block
760 while block
!= null do
767 # A block without any markdown specificities.
769 # Actually use the same implementation than `BlockCode`,
770 # this class is only used for typing purposes.
775 # A markdown blockquote.
779 redef fun emit
(v
) do v
.decorator
.add_blockquote
(v
, self)
781 # Remove blockquote markers.
782 private fun remove_block_quote_prefix
(block
: MDBlock) do
783 var line
= block
.first_line
784 while line
!= null do
785 if not line
.is_empty
then
786 if line
.value
[line
.leading
] == '>' then
787 var rem
= line
.leading
+ 1
788 if line
.leading
+ 1 < line
.value
.length
and
789 line
.value
[line
.leading
+ 1] == ' ' then
792 line
.value
= line
.value
.substring_from
(rem
)
793 line
.leading
= line
.process_leading
801 # A markdown code block.
805 redef fun emit
(v
) do v
.decorator
.add_code
(v
, self)
807 redef fun emit_lines
(v
) do
808 var line
= block
.first_line
809 while line
!= null do
810 if not line
.is_empty
then
811 v
.decorator
.append_code
(v
, line
.value
, 4, line
.value
.length
)
819 # A markdown code-fence block.
821 # Actually use the same implementation than `BlockCode`,
822 # this class is only used for typing purposes.
827 # A markdown headline.
831 redef fun emit
(v
) do v
.decorator
.add_headline
(v
, self)
833 # Depth of the headline used to determine the headline level.
836 # Remove healine marks from lines contained in `self`.
837 private fun transform_headline
(block
: MDBlock) do
838 if depth
> 0 then return
840 var line
= block
.first_line
841 if line
.is_empty
then return
842 var start
= line
.leading
843 while start
< line
.value
.length
and line
.value
[start
] == '#' do
847 while start
< line
.value
.length
and line
.value
[start
] == ' ' do
850 if start
>= line
.value
.length
then
853 var nend
= line
.value
.length
- line
.trailing
- 1
854 while line
.value
[nend
] == '#' do nend
-= 1
855 while line
.value
[nend
] == ' ' do nend
-= 1
856 line
.value
= line
.value
.substring
(start
, nend
- start
+ 1)
864 # A markdown list item block.
868 redef fun emit
(v
) do v
.decorator
.add_listitem
(v
, self)
871 # A markdown list block.
872 # Can be either an ordered or unordered list, this class is mainly used to factorize code.
873 abstract class BlockList
876 # Split list block into list items sub-blocks.
877 private fun init_block
(v
: MarkdownProcessor) do
878 var line
= block
.first_line
880 while line
!= null do
883 (not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
884 not (t
isa LineList))) then
885 var sblock
= block
.split
(line
.prev
.as(not null))
886 sblock
.kind
= new BlockListItem(sblock
)
890 var sblock
= block
.split
(block
.last_line
.as(not null))
891 sblock
.kind
= new BlockListItem(sblock
)
894 # Expand list items as paragraphs if needed.
895 private fun expand_paragraphs
(block
: MDBlock) do
896 var outer
= block
.first_block
897 var inner
: nullable MDBlock
898 var has_paragraph
= false
899 while outer
!= null and not has_paragraph
do
900 if outer
.kind
isa BlockListItem then
901 inner
= outer
.first_block
902 while inner
!= null and not has_paragraph
do
903 if inner
.kind
isa BlockParagraph then
911 if has_paragraph
then
912 outer
= block
.first_block
913 while outer
!= null do
914 if outer
.kind
isa BlockListItem then
915 inner
= outer
.first_block
916 while inner
!= null do
917 if inner
.kind
isa BlockNone then
918 inner
.kind
= new BlockParagraph(inner
)
929 # A markdown ordered list.
930 class BlockOrderedList
933 redef fun emit
(v
) do v
.decorator
.add_orderedlist
(v
, self)
936 # A markdown unordred list.
937 class BlockUnorderedList
940 redef fun emit
(v
) do v
.decorator
.add_unorderedlist
(v
, self)
943 # A markdown paragraph block.
947 redef fun emit
(v
) do v
.decorator
.add_paragraph
(v
, self)
954 redef fun emit
(v
) do v
.decorator
.add_ruler
(v
, self)
957 # Xml blocks that can be found in markdown markup.
961 redef fun emit_lines
(v
) do
962 var line
= block
.first_line
963 while line
!= null do
964 if not line
.is_empty
then v
.add line
.value
974 # Text contained in this line.
975 var value
: String is writable
977 # Is this line empty?
978 # Lines containing only spaces are considered empty.
979 var is_empty
: Bool = true is writable
981 # Previous line in `MDBlock` or null if first line.
982 var prev
: nullable MDLine = null is writable
984 # Next line in `MDBlock` or null if last line.
985 var next
: nullable MDLine = null is writable
987 # Is the previous line empty?
988 var prev_empty
: Bool = false is writable
990 # Is the next line empty?
991 var next_empty
: Bool = false is writable
993 init(value
: String) do
995 self.leading
= process_leading
996 if leading
!= value
.length
then
997 self.is_empty
= false
998 self.trailing
= process_trailing
1002 # Set `value` as an empty String and update `leading`, `trailing` and is_`empty`.
1008 if prev
!= null then prev
.next_empty
= true
1009 if next
!= null then next
.prev_empty
= true
1014 fun kind
(v
: MarkdownProcessor): Line do
1015 var value
= self.value
1016 if is_empty
then return new LineEmpty
1017 if leading
> 3 then return new LineCode
1018 if value
[leading
] == '#' then return new LineHeadline
1019 if value
[leading
] == '>' then return new LineBlockquote
1021 if value
.length
- leading
- trailing
> 2 then
1022 if value
[leading
] == '`' and count_chars_start
('`') >= 3 then
1023 return new LineFence
1025 if value
[leading
] == '~' and count_chars_start
('~') >= 3 then
1026 return new LineFence
1030 if value
.length
- leading
- trailing
> 2 and
1031 (value
[leading
] == '*' or value
[leading
] == '-' or value
[leading
] == '_') then
1032 if count_chars
(value
[leading
]) >= 3 then
1037 if value
.length
- leading
>= 2 and value
[leading
+ 1] == ' ' then
1038 var c
= value
[leading
]
1039 if c
== '*' or c
== '-' or c
== '+' then return new LineUList
1042 if value
.length
- leading
>= 3 and value
[leading
].is_digit
then
1044 while i
< value
.length
and value
[i
].is_digit
do i
+= 1
1045 if i
+ 1 < value
.length
and value
[i
] == '.' and value
[i
+ 1] == ' ' then
1046 return new LineOList
1050 if value
[leading
] == '<' and check_html
then return new LineXML
1052 if next
!= null and not next
.is_empty
then
1053 if next
.count_chars
('=') > 0 then
1054 return new LineHeadline1
1056 if next
.count_chars
('-') > 0 then
1057 return new LineHeadline2
1060 return new LineOther
1063 # Number or leading spaces on this line.
1064 var leading
: Int = 0 is writable
1066 # Compute `leading` depending on `value`.
1067 fun process_leading
: Int do
1069 var value
= self.value
1070 while count
< value
.length
and value
[count
] == ' ' do count
+= 1
1071 if leading
== value
.length
then clear
1075 # Number of trailing spaces on this line.
1076 var trailing
: Int = 0 is writable
1078 # Compute `trailing` depending on `value`.
1079 fun process_trailing
: Int do
1081 var value
= self.value
1082 while value
[value
.length
- count
- 1] == ' ' do
1088 # Count the amount of `ch` in this line.
1089 # Return A value > 0 if this line only consists of `ch` end spaces.
1090 fun count_chars
(ch
: Char): Int do
1106 # Count the amount of `ch` at the start of this line ignoring spaces.
1107 fun count_chars_start
(ch
: Char): Int do
1122 # Last XML line if any.
1123 private var xml_end_line
: nullable MDLine = null
1125 # Does `value` contains valid XML markup?
1126 private fun check_html
: Bool do
1127 var tags
= new Array[String]
1128 var tmp
= new FlatBuffer
1130 if pos
+ 1 < value
.length
and value
[pos
+ 1] == '!' then
1131 if read_xml_comment
(self, pos
) > 0 then return true
1133 pos
= value
.read_xml
(tmp
, pos
, false)
1137 if not tag
.is_html_block
then
1145 var line
: nullable MDLine = self
1146 while line
!= null do
1147 while pos
< line
.value
.length
and line
.value
[pos
] != '<' do
1150 if pos
>= line
.value
.length
then
1151 if line
.value
[pos
- 2] == '/' then
1153 if tags
.is_empty
then
1161 tmp
= new FlatBuffer
1162 var new_pos
= line
.value
.read_xml
(tmp
, pos
, false)
1165 if tag
.is_html_block
and not tag
== "hr" then
1166 if tmp
[1] == '/' then
1167 if tags
.last
!= tag
then
1175 if tags
.is_empty
then
1185 return tags
.is_empty
1190 # Read a XML comment.
1191 # Used by `check_html`.
1192 private fun read_xml_comment
(first_line
: MDLine, start
: Int): Int do
1193 var line
: nullable MDLine = first_line
1194 if start
+ 3 < line
.value
.length
then
1195 if line
.value
[2] == '-' and line
.value
[3] == '-' then
1197 while line
!= null do
1198 while pos
< line
.value
.length
and line
.value
[pos
] != '-' do
1201 if pos
== line
.value
.length
then
1205 if pos
+ 2 < line
.value
.length
then
1206 if line
.value
[pos
+ 1] == '-' and line
.value
[pos
+ 2] == '>' then
1207 first_line
.xml_end_line
= line
1219 # Extract the text of `self` without leading and trailing.
1220 fun text
: String do return value
.substring
(leading
, value
.length
- trailing
)
1227 # See `MarkdownProcessor::recurse`.
1228 fun process
(v
: MarkdownProcessor) is abstract
1231 # An empty markdown line.
1235 redef fun process
(v
) do
1236 v
.current_line
= v
.current_line
.next
1240 # A non-specific markdown construction.
1241 # Mainly used as part of another line construct such as paragraphs or lists.
1245 redef fun process
(v
) do
1246 var line
= v
.current_line
1248 var was_empty
= line
.prev_empty
1249 while line
!= null and not line
.is_empty
do
1250 var t
= line
.kind
(v
)
1251 if v
.in_list
and t
isa LineList then
1254 if t
isa LineCode or t
isa LineFence then
1257 if t
isa LineHeadline or t
isa LineHeadline1 or t
isa LineHeadline2 or
1258 t
isa LineHR or t
isa LineBlockquote or t
isa LineXML then
1265 if line
!= null and not line
.is_empty
then
1266 var block
= v
.current_block
.split
(line
.prev
.as(not null))
1267 if v
.in_list
and not was_empty
then
1268 block
.kind
= new BlockNone(block
)
1270 block
.kind
= new BlockParagraph(block
)
1272 v
.current_block
.remove_leading_empty_lines
1275 if line
!= null then
1276 block
= v
.current_block
.split
(line
)
1278 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1280 if v
.in_list
and (line
== null or not line
.is_empty
) and not was_empty
then
1281 block
.kind
= new BlockNone(block
)
1283 block
.kind
= new BlockParagraph(block
)
1285 v
.current_block
.remove_leading_empty_lines
1287 v
.current_line
= v
.current_block
.first_line
1291 # A line of markdown code.
1295 redef fun process
(v
) do
1296 var line
= v
.current_line
1298 while line
!= null and (line
.is_empty
or line
.kind
(v
) isa LineCode) do
1301 # split at block end line
1303 if line
!= null then
1304 block
= v
.current_block
.split
(line
.prev
.as(not null))
1306 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1308 block
.kind
= new BlockCode(block
)
1309 block
.remove_surrounding_empty_lines
1310 v
.current_line
= v
.current_block
.first_line
1314 # A line of raw XML.
1318 redef fun process
(v
) do
1319 var line
= v
.current_line
1320 var prev
= line
.prev
1321 if prev
!= null then v
.current_block
.split
(prev
)
1322 var block
= v
.current_block
.split
(line
.xml_end_line
.as(not null))
1323 block
.kind
= new BlockXML(block
)
1324 v
.current_block
.remove_leading_empty_lines
1325 v
.current_line
= v
.current_block
.first_line
1329 # A markdown blockquote line.
1330 class LineBlockquote
1333 redef fun process
(v
) do
1334 var line
= v
.current_line
1336 while line
!= null do
1337 if not line
.is_empty
and (line
.prev_empty
and
1338 line
.leading
== 0 and
1339 not line
.kind
(v
) isa LineBlockquote) then break
1344 if line
!= null then
1345 block
= v
.current_block
.split
(line
.prev
.as(not null))
1347 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1349 var kind
= new BlockQuote(block
)
1351 block
.remove_surrounding_empty_lines
1352 kind
.remove_block_quote_prefix
(block
)
1353 v
.current_line
= line
1354 v
.recurse
(block
, false)
1355 v
.current_line
= v
.current_block
.first_line
1359 # A markdown ruler line.
1363 redef fun process
(v
) do
1364 var line
= v
.current_line
1365 if line
.prev
!= null then v
.current_block
.split
(line
.prev
.as(not null))
1366 var block
= v
.current_block
.split
(line
.as(not null))
1367 block
.kind
= new BlockRuler(block
)
1368 v
.current_block
.remove_leading_empty_lines
1369 v
.current_line
= v
.current_block
.first_line
1373 # A markdown fence code line.
1377 redef fun process
(v
) do
1379 var line
= v
.current_line
.next
1380 while line
!= null do
1381 if line
.kind
(v
) isa LineFence then break
1384 if line
!= null then
1389 if line
!= null then
1390 block
= v
.current_block
.split
(line
.prev
.as(not null))
1392 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1394 block
.kind
= new BlockFence(block
)
1395 block
.first_line
.clear
1396 if block
.last_line
.kind
(v
) isa LineFence then
1397 block
.last_line
.clear
1399 block
.remove_surrounding_empty_lines
1400 v
.current_line
= line
1404 # A markdown headline.
1408 redef fun process
(v
) do
1409 var line
= v
.current_line
1410 var lprev
= line
.prev
1411 if lprev
!= null then v
.current_block
.split
(lprev
)
1412 var block
= v
.current_block
.split
(line
.as(not null))
1413 var kind
= new BlockHeadline(block
)
1416 # block.id = block.first_line.strip_id
1417 kind
.transform_headline
(block
)
1418 v
.current_block
.remove_leading_empty_lines
1419 v
.current_line
= v
.current_block
.first_line
1423 # A markdown headline of level 1.
1427 redef fun process
(v
) do
1428 var line
= v
.current_line
1429 var lprev
= line
.prev
1430 if lprev
!= null then v
.current_block
.split
(lprev
)
1432 var block
= v
.current_block
.split
(line
.as(not null))
1433 var kind
= new BlockHeadline(block
)
1436 # block.id = block.first_line.strip_id
1437 kind
.transform_headline
(block
)
1439 v
.current_block
.remove_leading_empty_lines
1440 v
.current_line
= v
.current_block
.first_line
1444 # A markdown headline of level 2.
1448 redef fun process
(v
) do
1449 var line
= v
.current_line
1450 var lprev
= line
.prev
1451 if lprev
!= null then v
.current_block
.split
(lprev
)
1453 var block
= v
.current_block
.split
(line
.as(not null))
1454 var kind
= new BlockHeadline(block
)
1457 # block.id = block.first_line.strip_id
1458 kind
.transform_headline
(block
)
1460 v
.current_block
.remove_leading_empty_lines
1461 v
.current_line
= v
.current_block
.first_line
1465 # A markdown list line.
1466 # Mainly used to factorize code between ordered and unordered lists.
1470 redef fun process
(v
) do
1471 var line
= v
.current_line
1473 while line
!= null do
1474 var t
= line
.kind
(v
)
1475 if not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
1476 not t
isa LineList) then break
1481 if line
!= null then
1482 list
= v
.current_block
.split
(line
.prev
.as(not null))
1484 list
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1486 var kind
= block_kind
(list
)
1488 list
.first_line
.prev_empty
= false
1489 list
.last_line
.next_empty
= false
1490 list
.remove_surrounding_empty_lines
1491 list
.first_line
.prev_empty
= false
1492 list
.last_line
.next_empty
= false
1494 var block
= list
.first_block
1495 while block
!= null do
1496 block
.remove_list_indent
(v
)
1497 v
.recurse
(block
, true)
1500 kind
.expand_paragraphs
(list
)
1501 v
.current_line
= line
1504 # Create a new block kind based on this line.
1505 protected fun block_kind
(block
: MDBlock): BlockList is abstract
1507 protected fun extract_value
(line
: MDLine): String is abstract
1510 # An ordered list line.
1514 redef fun block_kind
(block
) do return new BlockOrderedList(block
)
1516 redef fun extract_value
(line
) do
1517 return line
.value
.substring_from
(line
.value
.index_of
('.') + 2)
1521 # An unordered list line.
1525 redef fun block_kind
(block
) do return new BlockUnorderedList(block
)
1527 redef fun extract_value
(line
) do
1528 return line
.value
.substring_from
(line
.leading
+ 2)
1532 # A token represent a character in the markdown input.
1533 # Some tokens have a specific markup behaviour that is handled here.
1534 abstract class Token
1536 # Position of `self` in markdown input.
1539 # Character found at `pos` in the markdown input.
1542 # Output that token using `MarkdownEmitter::decorator`.
1543 fun emit
(v
: MarkdownEmitter) do v
.addc char
1546 # A token without a specific meaning.
1551 # An emphasis token.
1552 abstract class TokenEm
1555 redef fun emit
(v
) do
1556 var tmp
= v
.push_buffer
1557 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1560 v
.decorator
.add_em
(v
, tmp
)
1568 # An emphasis star token.
1573 # An emphasis underscore token.
1574 class TokenEmUnderscore
1579 abstract class TokenStrong
1582 redef fun emit
(v
) do
1583 var tmp
= v
.push_buffer
1584 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 2, self)
1587 v
.decorator
.add_strong
(v
, tmp
)
1588 v
.current_pos
= b
+ 1
1595 # A strong star token.
1596 class TokenStrongStar
1600 # A strong underscore token.
1601 class TokenStrongUnderscore
1606 # This class is mainly used to factorize work between single and double quoted span codes.
1607 abstract class TokenCode
1610 redef fun emit
(v
) do
1611 var a
= pos
+ next_pos
+ 1
1612 var b
= v
.current_text
.find_token
(a
, self)
1614 v
.current_pos
= b
+ next_pos
1615 while a
< b
and v
.current_text
[a
] == ' ' do a
+= 1
1617 while v
.current_text
[b
- 1] == ' ' do b
-= 1
1618 v
.decorator
.add_span_code
(v
, v
.current_text
.as(not null), a
, b
)
1625 private fun next_pos
: Int is abstract
1628 # A span code token.
1629 class TokenCodeSingle
1632 redef fun next_pos
do return 0
1635 # A doubled span code token.
1636 class TokenCodeDouble
1639 redef fun next_pos
do return 1
1642 # A link or image token.
1643 # This class is mainly used to factorize work between images and links.
1644 abstract class TokenLinkOrImage
1648 var link
: nullable Text = null
1651 var name
: nullable Text = null
1654 var comment
: nullable Text = null
1656 # Is the link construct an abbreviation?
1657 var is_abbrev
= false
1659 redef fun emit
(v
) do
1660 var tmp
= new FlatBuffer
1661 var b
= check_link
(v
, tmp
, pos
, self)
1670 # Emit the hyperlink as link or image.
1671 private fun emit_hyper
(v
: MarkdownEmitter) is abstract
1673 # Check if the link is a valid link.
1674 private fun check_link
(v
: MarkdownEmitter, out
: FlatBuffer, start
: Int, token
: Token): Int do
1675 var md
= v
.current_text
1677 if token
isa TokenLink then
1682 var tmp
= new FlatBuffer
1683 pos
= md
.read_md_link_id
(tmp
, pos
)
1684 if pos
< start
then return -1
1688 pos
= md
.skip_spaces
(pos
)
1690 var tid
= name
.write_to_string
.to_lower
1691 if v
.processor
.link_refs
.has_key
(tid
) then
1692 var lr
= v
.processor
.link_refs
[tid
]
1693 is_abbrev
= lr
.is_abbrev
1700 else if md
[pos
] == '(' then
1702 pos
= md
.skip_spaces
(pos
)
1703 if pos
< start
then return -1
1704 tmp
= new FlatBuffer
1705 var use_lt
= md
[pos
] == '<'
1707 pos
= md
.read_until
(tmp
, pos
+ 1, '>')
1709 pos
= md
.read_md_link
(tmp
, pos
)
1711 if pos
< start
then return -1
1712 if use_lt
then pos
+= 1
1713 link
= tmp
.write_to_string
1714 if md
[pos
] == ' ' then
1715 pos
= md
.skip_spaces
(pos
)
1716 if pos
> start
and md
[pos
] == '"' then
1718 tmp
= new FlatBuffer
1719 pos
= md
.read_until
(tmp
, pos
, '"')
1720 if pos
< start
then return -1
1721 comment
= tmp
.write_to_string
1723 pos
= md
.skip_spaces
(pos
)
1724 if pos
== -1 then return -1
1727 if md
[pos
] != ')' then return -1
1728 else if md
[pos
] == '[' then
1730 tmp
= new FlatBuffer
1731 pos
= md
.read_raw_until
(tmp
, pos
, ']')
1732 if pos
< start
then return -1
1734 if tmp
.length
> 0 then
1739 var tid
= id
.write_to_string
.to_lower
1740 if v
.processor
.link_refs
.has_key
(tid
) then
1741 var lr
= v
.processor
.link_refs
[tid
]
1746 var tid
= name
.write_to_string
.replace
("\n", " ").to_lower
1747 if v
.processor
.link_refs
.has_key
(tid
) then
1748 var lr
= v
.processor
.link_refs
[tid
]
1756 if link
== null then return -1
1761 # A markdown link token.
1763 super TokenLinkOrImage
1765 redef fun emit_hyper
(v
) do
1766 if is_abbrev
and comment
!= null then
1767 v
.decorator
.add_abbr
(v
, name
.as(not null), comment
.as(not null))
1769 v
.decorator
.add_link
(v
, link
.as(not null), name
.as(not null), comment
)
1774 # A markdown image token.
1776 super TokenLinkOrImage
1778 redef fun emit_hyper
(v
) do
1779 v
.decorator
.add_image
(v
, link
.as(not null), name
.as(not null), comment
)
1787 redef fun emit
(v
) do
1788 var tmp
= new FlatBuffer
1789 var b
= check_html
(v
, tmp
, v
.current_text
.as(not null), v
.current_pos
)
1794 v
.decorator
.escape_char
(v
, char
)
1798 # Is the HTML valid?
1799 # Also take care of link and mailto shortcuts.
1800 private fun check_html
(v
: MarkdownEmitter, out
: FlatBuffer, md
: Text, start
: Int): Int do
1801 # check for auto links
1802 var tmp
= new FlatBuffer
1803 var pos
= md
.read_until
(tmp
, start
+ 1, ':', ' ', '>', '\n')
1804 if pos
!= -1 and md
[pos
] == ':' and tmp
.is_link_prefix
then
1805 pos
= md
.read_until
(tmp
, pos
, '>')
1807 var link
= tmp
.write_to_string
1808 v
.decorator
.add_link
(v
, link
, link
, null)
1812 # TODO check for mailto
1813 # check for inline html
1814 if start
+ 2 < md
.length
then
1815 return md
.read_xml
(out
, start
, true)
1821 # An HTML entity token.
1825 redef fun emit
(v
) do
1826 var tmp
= new FlatBuffer
1827 var b
= check_entity
(tmp
, v
.current_text
.as(not null), pos
)
1832 v
.decorator
.escape_char
(v
, char
)
1836 # Is the entity valid?
1837 private fun check_entity
(out
: FlatBuffer, md
: Text, start
: Int): Int do
1838 var pos
= md
.read_until
(out
, start
, ';')
1839 if pos
< 0 or out
.length
< 3 then
1842 if out
[1] == '#' then
1843 if out
[2] == 'x' or out
[2] == 'X' then
1844 if out
.length
< 4 then return -1
1845 for i
in [3..out
.length
[ do
1847 if (c
< '0' or c
> '9') and (c
< 'a' and c
> 'f') and (c
< 'A' and c
> 'F') then
1852 for i
in [2..out
.length
[ do
1854 if c
< '0' or c
> '9' then return -1
1859 for i
in [1..out
.length
[ do
1861 if not c
.is_digit
and not c
.is_letter
then return -1
1864 # TODO check entity is valid
1865 # if out.is_entity then
1875 # A markdown escape token.
1879 redef fun emit
(v
) do
1881 v
.addc v
.current_text
[v
.current_pos
]
1885 # A markdown super token.
1889 redef fun emit
(v
) do
1890 var tmp
= v
.push_buffer
1891 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1894 v
.decorator
.add_super
(v
, tmp
)
1904 # Get the token kind at `pos`.
1905 private fun token_at
(pos
: Int): Token do
1918 if pos
+ 1 < length
then
1923 if pos
+ 2 < length
then
1928 if pos
+ 3 < length
then
1936 if c0
!= ' ' or c2
!= ' ' then
1937 return new TokenStrongStar(pos
, c
)
1939 return new TokenEmStar(pos
, c
)
1942 if c0
!= ' ' or c1
!= ' ' then
1943 return new TokenEmStar(pos
, c
)
1945 return new TokenNone(pos
, c
)
1947 else if c
== '_' then
1949 if c0
!= ' ' or c2
!= ' 'then
1950 return new TokenStrongUnderscore(pos
, c
)
1952 return new TokenEmUnderscore(pos
, c
)
1955 if c0
!= ' ' or c1
!= ' ' then
1956 return new TokenEmUnderscore(pos
, c
)
1958 return new TokenNone(pos
, c
)
1960 else if c
== '!' then
1961 if c1
== '[' then return new TokenImage(pos
, c
)
1962 return new TokenNone(pos
, c
)
1963 else if c
== '[' then
1964 return new TokenLink(pos
, c
)
1965 else if c
== ']' then
1966 return new TokenNone(pos
, c
)
1967 else if c
== '`' then
1969 return new TokenCodeDouble(pos
, c
)
1971 return new TokenCodeSingle(pos
, c
)
1973 else if c
== '\\' then
1974 if c1
== '\\' or c1
== '[' or c1
== ']' or c1
== '(' or c1
== ')' or c1
== '{' or c1 == '}' or c1
== '#' or c1
== '"' or c1
== '\'' or c1 == '.' or c1 == '<' or c1 == '>' or c1 == '*' or c1 == '+' or c1 == '-' or c1 == '_
' or c1 == '!' or c1 == '`' or c1 == '~' or c1 == '^' then
1975 return new TokenEscape(pos, c)
1977 return new TokenNone(pos, c)
1979 else if c == '<' then
1980 return new TokenHTML(pos, c)
1981 else if c == '&' then
1982 return new TokenEntity(pos, c)
1983 else if c == '^' then
1984 if c0 == '^' or c1 == '^' then
1985 return new TokenNone(pos, c)
1987 return new TokenSuper(pos, c)
1990 return new TokenNone(pos, c)
1994 # Find the position of a `token
` in `self`.
1995 private fun find_token(start: Int, token: Token): Int do
1997 while pos < length do
1998 if token_at(pos).is_same_type(token) then
2006 # Get the position of the next non-space character.
2007 private fun skip_spaces(start: Int): Int do
2009 while pos > -1 and pos < length and (self[pos] == ' ' or self[pos] == '\n') do
2012 if pos < length then return pos
2016 # Read `self` until `nend
` and append it to the `out
` buffer.
2017 # Escape markdown special chars.
2018 private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do
2020 while pos < length do
2022 if c == '\\' and pos + 1 < length then
2023 pos = escape(out, self[pos + 1], pos)
2025 var end_reached = false
2032 if end_reached then break
2037 if pos == length then return -1
2041 # Read `self` as raw text until `nend
` and append it to the `out
` buffer.
2042 # No escape is made.
2043 private fun read_raw_until(out: FlatBuffer, start: Int, nend: Char...): Int do
2045 while pos < length do
2047 var end_reached = false
2054 if end_reached then break
2058 if pos == length then return -1
2062 # Read `self` as XML until `to
` and append it to the `out
` buffer.
2063 # Escape HTML special chars.
2064 private fun read_xml_until(out: FlatBuffer, from: Int, to: Char...): Int do
2067 var str_char: nullable Char = null
2068 while pos < length do
2074 if pos < length then
2080 if c == str_char then
2087 if c == '"' or c == '\'' then
2092 var end_reached = false
2093 for n in [0..to.length[ do
2099 if end_reached then break
2104 if pos == length then return -1
2108 # Read `self` as XML and append it to the `out
` buffer.
2109 # Safe mode can be activated to limit reading to valid xml.
2110 private fun read_xml(out: FlatBuffer, start: Int, safe_mode: Bool): Int do
2112 var is_close_tag = false
2113 if start + 1 >= length then return -1
2114 if self[start + 1] == '/' then
2117 else if self[start + 1] == '!' then
2121 is_close_tag = false
2125 var tmp = new FlatBuffer
2126 pos = read_xml_until(tmp, pos, ' ', '/', '>')
2127 if pos == -1 then return -1
2128 var tag = tmp.write_to_string.trim.to_lower
2129 if tag.is_html_unsafe then
2131 if is_close_tag then out.add '/'
2135 if is_close_tag then out.add '/'
2140 if is_close_tag then out.add '/'
2141 pos = read_xml_until(out, pos, ' ', '/', '>')
2143 if pos == -1 then return -1
2144 pos = read_xml_until(out, pos, '/', '>')
2145 if pos == -1 then return -1
2146 if self[pos] == '/' then
2148 pos = self.read_xml_until(out, pos + 1, '>')
2149 if pos == -1 then return -1
2151 if self[pos] == '>' then
2158 # Read a markdown link address and append it to the `out
` buffer.
2159 private fun read_md_link(out: FlatBuffer, start: Int): Int do
2162 while pos < length do
2164 if c == '\\' and pos + 1 < length then
2165 pos = escape(out, self[pos + 1], pos)
2167 var end_reached = false
2170 else if c == ' ' then
2171 if counter == 1 then end_reached = true
2172 else if c == ')' then
2174 if counter == 0 then end_reached = true
2176 if end_reached then break
2181 if pos == length then return -1
2185 # Read a markdown link text and append it to the `out
` buffer.
2186 private fun read_md_link_id(out: FlatBuffer, start: Int): Int do
2189 while pos < length do
2191 var end_reached = false
2195 else if c == ']' then
2197 if counter == 0 then
2205 if end_reached then break
2208 if pos == length then return -1
2212 # Extract the XML tag name from a XML tag.
2213 private fun xml_tag: String do
2214 var tpl = new FlatBuffer
2216 if pos < length and self[1] == '/' then pos += 1
2217 while pos < length - 1 and (self[pos].is_digit or self[pos].is_letter) do
2221 return tpl.write_to_string.to_lower
2224 # Read and escape the markdown contained in `self`.
2225 private fun escape(out: FlatBuffer, c: Char, pos: Int): Int do
2226 if c == '\\' or c == '[' or c == ']' or c == '(' or c == ')' or c == '{' or
2227 c == '}' or c == '#' or c == '"' or c == '\'' or c == '.' or c == '<' or
2228 c == '>' or c == '*' or c == '+' or c == '-' or c == '_' or c == '!' or
2229 c == '`' or c == '~
' or c == '^
' then
2237 # Is `self` an unsafe HTML element?
2238 private fun is_html_unsafe: Bool do return html_unsafe_tags.has(self.write_to_string)
2240 # Is `self` a HRML block element?
2241 private fun is_html_block: Bool do return html_block_tags.has(self.write_to_string)
2243 # Is `self` a link prefix?
2244 private fun is_link_prefix: Bool do return html_link_prefixes.has(self.write_to_string)
2246 private fun html_unsafe_tags: Array[String] do return once ["applet", "head", "body", "frame", "frameset", "iframe", "script", "object"]
2248 private fun html_block_tags: Array[String] do return once ["address", "article", "aside", "audio", "blockquote", "canvas", "dd", "div", "dl", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "noscript", "ol", "output", "p", "pre", "section", "table", "tfoot", "ul", "video"]
2250 private fun html_link_prefixes: Array[String] do return once ["http", "https", "ftp", "ftps"]
2255 # Parse `self` as markdown and return the HTML representation
2257 # var md = "**Hello World!**"
2258 # var html = md.md_to_html
2259 # assert html == "<p><strong>Hello World!</strong></p>\n"
2260 fun md_to_html: Streamable do
2261 var processor = new MarkdownProcessor
2262 return processor.process(self)