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
402 # Generate a new html valid id from a `String`.
403 fun strip_id(txt: String): String is abstract
405 # Found headlines during the processing labeled by their ids.
406 fun headlines: ArrayMap[String, HeadLine] is abstract
409 # Class representing a markdown headline.
411 # Unique identifier of this headline.
414 # Text of the headline.
417 # Level of this headline.
419 # According toe the markdown specification, level must be in `[1..6]`.
423 # `Decorator` that outputs HTML.
427 redef var headlines = new ArrayMap[String, HeadLine]
429 redef fun add_ruler(v, block) do v.add "<hr
/>\n
"
431 redef fun add_headline(v, block) do
433 var txt = block.block.first_line.value
434 var id = strip_id(txt)
435 var lvl = block.depth
436 headlines[id] = new HeadLine(id, txt, lvl)
438 v.add "<h
{lvl} id
=\
"{id}\">"
443 redef fun add_paragraph(v, block) do
449 redef fun add_code(v, block) do
452 v.add "</code
></pre
>\n
"
455 redef fun add_blockquote(v, block) do
456 v.add "<blockquote
>\n
"
458 v.add "</blockquote
>\n
"
461 redef fun add_unorderedlist(v, block) do
467 redef fun add_orderedlist(v, block) do
473 redef fun add_listitem(v, block) do
479 redef fun add_em(v, text) do
485 redef fun add_strong(v, text) do
491 redef fun add_super(v, text) do
497 redef fun add_image(v, link, name, comment) do
499 append_value
(v
, link
)
501 append_value
(v
, name
)
503 if comment != null and not comment.is_empty then
505 append_value
(v
, comment
)
511 redef fun add_link(v, link, name, comment) do
513 append_value
(v
, link
)
515 if comment != null and not comment.is_empty then
517 append_value
(v
, comment
)
525 redef fun add_abbr(v, name, comment) do
526 v.add "<abbr title
=\
""
527 append_value
(v
, comment
)
533 redef fun add_span_code(v, text, from, to) do
535 append_code(v, text, from, to)
539 redef fun add_line_break(v) do
543 redef fun append_value(v, text) do for c in text do escape_char(v, c)
545 redef fun escape_char(v, c) do
548 else if c == '<' then
550 else if c == '>' then
552 else if c == '"' then
554 else if c == '\
'' then
561 redef fun append_code
(v
, buffer
, from
, to
) do
562 for i
in [from
..to
[ do
566 else if c
== '<' then
568 else if c
== '>' then
576 redef fun strip_id
(txt
) do
578 var b
= new FlatBuffer
583 if not c
.is_letter
and
585 not allowed_id_chars
.has
(c
) then continue
591 # check for multiple id definitions
592 if headlines
.has_key
(key
) then
595 while headlines
.has_key
(key
) do
603 private var allowed_id_chars
: Array[Char] = ['-', '_', ':', '.']
606 # A block of markdown lines.
607 # A `MDBlock` can contains lines and/or sub-blocks.
611 var kind
: Block = new BlockNone(self) is writable
614 var first_line
: nullable MDLine = null is writable
617 var last_line
: nullable MDLine = null is writable
619 # First sub-block if any.
620 var first_block
: nullable MDBlock = null is writable
622 # Last sub-block if any.
623 var last_block
: nullable MDBlock = null is writable
625 # Previous block if any.
626 var prev
: nullable MDBlock = null is writable
629 var next
: nullable MDBlock = null is writable
631 # Does this block contain subblocks?
632 fun has_blocks
: Bool do return first_block
!= null
635 fun count_blocks
: Int do
637 var block
= first_block
638 while block
!= null do
645 # Does this block contain lines?
646 fun has_lines
: Bool do return first_line
!= null
649 fun count_lines
: Int do
651 var line
= first_line
652 while line
!= null do
659 # Split `self` creating a new sub-block having `line` has `last_line`.
660 fun split
(line
: MDLine): MDBlock do
661 var block
= new MDBlock
662 block
.first_line
= first_line
663 block
.last_line
= line
664 first_line
= line
.next
666 if first_line
== null then
669 first_line
.prev
= null
671 if first_block
== null then
675 last_block
.next
= block
681 # Add a `line` to this block.
682 fun add_line
(line
: MDLine) do
683 if last_line
== null then
687 last_line
.next_empty
= line
.is_empty
688 line
.prev_empty
= last_line
.is_empty
689 line
.prev
= last_line
690 last_line
.next
= line
695 # Remove `line` from this block.
696 fun remove_line
(line
: MDLine) do
697 if line
.prev
== null then
698 first_line
= line
.next
700 line
.prev
.next
= line
.next
702 if line
.next
== null then
703 last_line
= line
.prev
705 line
.next
.prev
= line
.prev
711 # Remove leading empty lines.
712 fun remove_leading_empty_lines
: Bool do
713 var was_empty
= false
714 var line
= first_line
715 while line
!= null and line
.is_empty
do
723 # Remove trailing empty lines.
724 fun remove_trailing_empty_lines
: Bool do
725 var was_empty
= false
727 while line
!= null and line
.is_empty
do
735 # Remove leading and trailing empty lines.
736 fun remove_surrounding_empty_lines
: Bool do
737 var was_empty
= false
738 if remove_leading_empty_lines
then was_empty
= true
739 if remove_trailing_empty_lines
then was_empty
= true
743 # Remove list markers and up to 4 leading spaces.
744 # Used to clean nested lists.
745 fun remove_list_indent
(v
: MarkdownProcessor) do
746 var line
= first_line
747 while line
!= null do
748 if not line
.is_empty
then
749 var kind
= line
.kind
(v
)
750 if kind
isa LineList then
751 line
.value
= kind
.extract_value
(line
)
753 line
.value
= line
.value
.substring_from
(line
.leading
.min
(4))
755 line
.leading
= line
.process_leading
761 # Collect block line text.
763 var text
= new FlatBuffer
764 var line
= first_line
765 while line
!= null do
766 if not line
.is_empty
then
767 text
.append line
.text
772 return text
.write_to_string
776 # Representation of a markdown block in the AST.
777 # Each `Block` is linked to a `MDBlock` that contains mardown code.
780 # The markdown block `self` is related to.
783 # Output `self` using `v.decorator`.
784 fun emit
(v
: MarkdownEmitter) do v
.emit_in
(self)
786 # Emit the containts of `self`, lines or blocks.
787 fun emit_in
(v
: MarkdownEmitter) do
788 block
.remove_surrounding_empty_lines
789 if block
.has_lines
then
796 # Emit lines contained in `block`.
797 fun emit_lines
(v
: MarkdownEmitter) do
798 var tpl
= v
.push_buffer
799 var line
= block
.first_line
800 while line
!= null do
801 if not line
.is_empty
then
802 v
.add line
.value
.substring
(line
.leading
, line
.value
.length
- line
.trailing
)
803 if line
.trailing
>= 2 then v
.decorator
.add_line_break
(v
)
805 if line
.next
!= null then
814 # Emit sub-blocks contained in `block`.
815 fun emit_blocks
(v
: MarkdownEmitter) do
816 var block
= self.block
.first_block
817 while block
!= null do
824 # A block without any markdown specificities.
826 # Actually use the same implementation than `BlockCode`,
827 # this class is only used for typing purposes.
832 # A markdown blockquote.
836 redef fun emit
(v
) do v
.decorator
.add_blockquote
(v
, self)
838 # Remove blockquote markers.
839 private fun remove_block_quote_prefix
(block
: MDBlock) do
840 var line
= block
.first_line
841 while line
!= null do
842 if not line
.is_empty
then
843 if line
.value
[line
.leading
] == '>' then
844 var rem
= line
.leading
+ 1
845 if line
.leading
+ 1 < line
.value
.length
and
846 line
.value
[line
.leading
+ 1] == ' ' then
849 line
.value
= line
.value
.substring_from
(rem
)
850 line
.leading
= line
.process_leading
858 # A markdown code block.
862 redef fun emit
(v
) do v
.decorator
.add_code
(v
, self)
864 redef fun emit_lines
(v
) do
865 var line
= block
.first_line
866 while line
!= null do
867 if not line
.is_empty
then
868 v
.decorator
.append_code
(v
, line
.value
, 4, line
.value
.length
)
876 # A markdown code-fence block.
878 # Actually use the same implementation than `BlockCode`,
879 # this class is only used for typing purposes.
884 # A markdown headline.
888 redef fun emit
(v
) do v
.decorator
.add_headline
(v
, self)
890 # Depth of the headline used to determine the headline level.
893 # Remove healine marks from lines contained in `self`.
894 private fun transform_headline
(block
: MDBlock) do
895 if depth
> 0 then return
897 var line
= block
.first_line
898 if line
.is_empty
then return
899 var start
= line
.leading
900 while start
< line
.value
.length
and line
.value
[start
] == '#' do
904 while start
< line
.value
.length
and line
.value
[start
] == ' ' do
907 if start
>= line
.value
.length
then
910 var nend
= line
.value
.length
- line
.trailing
- 1
911 while line
.value
[nend
] == '#' do nend
-= 1
912 while line
.value
[nend
] == ' ' do nend
-= 1
913 line
.value
= line
.value
.substring
(start
, nend
- start
+ 1)
921 # A markdown list item block.
925 redef fun emit
(v
) do v
.decorator
.add_listitem
(v
, self)
928 # A markdown list block.
929 # Can be either an ordered or unordered list, this class is mainly used to factorize code.
930 abstract class BlockList
933 # Split list block into list items sub-blocks.
934 private fun init_block
(v
: MarkdownProcessor) do
935 var line
= block
.first_line
937 while line
!= null do
940 (not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
941 not (t
isa LineList))) then
942 var sblock
= block
.split
(line
.prev
.as(not null))
943 sblock
.kind
= new BlockListItem(sblock
)
947 var sblock
= block
.split
(block
.last_line
.as(not null))
948 sblock
.kind
= new BlockListItem(sblock
)
951 # Expand list items as paragraphs if needed.
952 private fun expand_paragraphs
(block
: MDBlock) do
953 var outer
= block
.first_block
954 var inner
: nullable MDBlock
955 var has_paragraph
= false
956 while outer
!= null and not has_paragraph
do
957 if outer
.kind
isa BlockListItem then
958 inner
= outer
.first_block
959 while inner
!= null and not has_paragraph
do
960 if inner
.kind
isa BlockParagraph then
968 if has_paragraph
then
969 outer
= block
.first_block
970 while outer
!= null do
971 if outer
.kind
isa BlockListItem then
972 inner
= outer
.first_block
973 while inner
!= null do
974 if inner
.kind
isa BlockNone then
975 inner
.kind
= new BlockParagraph(inner
)
986 # A markdown ordered list.
987 class BlockOrderedList
990 redef fun emit
(v
) do v
.decorator
.add_orderedlist
(v
, self)
993 # A markdown unordred list.
994 class BlockUnorderedList
997 redef fun emit
(v
) do v
.decorator
.add_unorderedlist
(v
, self)
1000 # A markdown paragraph block.
1001 class BlockParagraph
1004 redef fun emit
(v
) do v
.decorator
.add_paragraph
(v
, self)
1011 redef fun emit
(v
) do v
.decorator
.add_ruler
(v
, self)
1014 # Xml blocks that can be found in markdown markup.
1018 redef fun emit_lines
(v
) do
1019 var line
= block
.first_line
1020 while line
!= null do
1021 if not line
.is_empty
then v
.add line
.value
1031 # Text contained in this line.
1032 var value
: String is writable
1034 # Is this line empty?
1035 # Lines containing only spaces are considered empty.
1036 var is_empty
: Bool = true is writable
1038 # Previous line in `MDBlock` or null if first line.
1039 var prev
: nullable MDLine = null is writable
1041 # Next line in `MDBlock` or null if last line.
1042 var next
: nullable MDLine = null is writable
1044 # Is the previous line empty?
1045 var prev_empty
: Bool = false is writable
1047 # Is the next line empty?
1048 var next_empty
: Bool = false is writable
1050 init(value
: String) do
1052 self.leading
= process_leading
1053 if leading
!= value
.length
then
1054 self.is_empty
= false
1055 self.trailing
= process_trailing
1059 # Set `value` as an empty String and update `leading`, `trailing` and is_`empty`.
1065 if prev
!= null then prev
.next_empty
= true
1066 if next
!= null then next
.prev_empty
= true
1071 fun kind
(v
: MarkdownProcessor): Line do
1072 var value
= self.value
1073 if is_empty
then return new LineEmpty
1074 if leading
> 3 then return new LineCode
1075 if value
[leading
] == '#' then return new LineHeadline
1076 if value
[leading
] == '>' then return new LineBlockquote
1078 if value
.length
- leading
- trailing
> 2 then
1079 if value
[leading
] == '`' and count_chars_start
('`') >= 3 then
1080 return new LineFence
1082 if value
[leading
] == '~' and count_chars_start
('~') >= 3 then
1083 return new LineFence
1087 if value
.length
- leading
- trailing
> 2 and
1088 (value
[leading
] == '*' or value
[leading
] == '-' or value
[leading
] == '_') then
1089 if count_chars
(value
[leading
]) >= 3 then
1094 if value
.length
- leading
>= 2 and value
[leading
+ 1] == ' ' then
1095 var c
= value
[leading
]
1096 if c
== '*' or c
== '-' or c
== '+' then return new LineUList
1099 if value
.length
- leading
>= 3 and value
[leading
].is_digit
then
1101 while i
< value
.length
and value
[i
].is_digit
do i
+= 1
1102 if i
+ 1 < value
.length
and value
[i
] == '.' and value
[i
+ 1] == ' ' then
1103 return new LineOList
1107 if value
[leading
] == '<' and check_html
then return new LineXML
1109 if next
!= null and not next
.is_empty
then
1110 if next
.count_chars
('=') > 0 then
1111 return new LineHeadline1
1113 if next
.count_chars
('-') > 0 then
1114 return new LineHeadline2
1117 return new LineOther
1120 # Number or leading spaces on this line.
1121 var leading
: Int = 0 is writable
1123 # Compute `leading` depending on `value`.
1124 fun process_leading
: Int do
1126 var value
= self.value
1127 while count
< value
.length
and value
[count
] == ' ' do count
+= 1
1128 if leading
== value
.length
then clear
1132 # Number of trailing spaces on this line.
1133 var trailing
: Int = 0 is writable
1135 # Compute `trailing` depending on `value`.
1136 fun process_trailing
: Int do
1138 var value
= self.value
1139 while value
[value
.length
- count
- 1] == ' ' do
1145 # Count the amount of `ch` in this line.
1146 # Return A value > 0 if this line only consists of `ch` end spaces.
1147 fun count_chars
(ch
: Char): Int do
1163 # Count the amount of `ch` at the start of this line ignoring spaces.
1164 fun count_chars_start
(ch
: Char): Int do
1179 # Last XML line if any.
1180 private var xml_end_line
: nullable MDLine = null
1182 # Does `value` contains valid XML markup?
1183 private fun check_html
: Bool do
1184 var tags
= new Array[String]
1185 var tmp
= new FlatBuffer
1187 if pos
+ 1 < value
.length
and value
[pos
+ 1] == '!' then
1188 if read_xml_comment
(self, pos
) > 0 then return true
1190 pos
= value
.read_xml
(tmp
, pos
, false)
1194 if not tag
.is_html_block
then
1202 var line
: nullable MDLine = self
1203 while line
!= null do
1204 while pos
< line
.value
.length
and line
.value
[pos
] != '<' do
1207 if pos
>= line
.value
.length
then
1208 if pos
- 2 >= 0 and line
.value
[pos
- 2] == '/' then
1210 if tags
.is_empty
then
1218 tmp
= new FlatBuffer
1219 var new_pos
= line
.value
.read_xml
(tmp
, pos
, false)
1222 if tag
.is_html_block
and not tag
== "hr" then
1223 if tmp
[1] == '/' then
1224 if tags
.last
!= tag
then
1232 if tags
.is_empty
then
1242 return tags
.is_empty
1247 # Read a XML comment.
1248 # Used by `check_html`.
1249 private fun read_xml_comment
(first_line
: MDLine, start
: Int): Int do
1250 var line
: nullable MDLine = first_line
1251 if start
+ 3 < line
.value
.length
then
1252 if line
.value
[2] == '-' and line
.value
[3] == '-' then
1254 while line
!= null do
1255 while pos
< line
.value
.length
and line
.value
[pos
] != '-' do
1258 if pos
== line
.value
.length
then
1262 if pos
+ 2 < line
.value
.length
then
1263 if line
.value
[pos
+ 1] == '-' and line
.value
[pos
+ 2] == '>' then
1264 first_line
.xml_end_line
= line
1276 # Extract the text of `self` without leading and trailing.
1277 fun text
: String do return value
.substring
(leading
, value
.length
- trailing
)
1284 # See `MarkdownProcessor::recurse`.
1285 fun process
(v
: MarkdownProcessor) is abstract
1288 # An empty markdown line.
1292 redef fun process
(v
) do
1293 v
.current_line
= v
.current_line
.next
1297 # A non-specific markdown construction.
1298 # Mainly used as part of another line construct such as paragraphs or lists.
1302 redef fun process
(v
) do
1303 var line
= v
.current_line
1305 var was_empty
= line
.prev_empty
1306 while line
!= null and not line
.is_empty
do
1307 var t
= line
.kind
(v
)
1308 if v
.in_list
and t
isa LineList then
1311 if t
isa LineCode or t
isa LineFence then
1314 if t
isa LineHeadline or t
isa LineHeadline1 or t
isa LineHeadline2 or
1315 t
isa LineHR or t
isa LineBlockquote or t
isa LineXML then
1322 if line
!= null and not line
.is_empty
then
1323 var block
= v
.current_block
.split
(line
.prev
.as(not null))
1324 if v
.in_list
and not was_empty
then
1325 block
.kind
= new BlockNone(block
)
1327 block
.kind
= new BlockParagraph(block
)
1329 v
.current_block
.remove_leading_empty_lines
1332 if line
!= null then
1333 block
= v
.current_block
.split
(line
)
1335 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1337 if v
.in_list
and (line
== null or not line
.is_empty
) and not was_empty
then
1338 block
.kind
= new BlockNone(block
)
1340 block
.kind
= new BlockParagraph(block
)
1342 v
.current_block
.remove_leading_empty_lines
1344 v
.current_line
= v
.current_block
.first_line
1348 # A line of markdown code.
1352 redef fun process
(v
) do
1353 var line
= v
.current_line
1355 while line
!= null and (line
.is_empty
or line
.kind
(v
) isa LineCode) do
1358 # split at block end line
1360 if line
!= null then
1361 block
= v
.current_block
.split
(line
.prev
.as(not null))
1363 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1365 block
.kind
= new BlockCode(block
)
1366 block
.remove_surrounding_empty_lines
1367 v
.current_line
= v
.current_block
.first_line
1371 # A line of raw XML.
1375 redef fun process
(v
) do
1376 var line
= v
.current_line
1377 var prev
= line
.prev
1378 if prev
!= null then v
.current_block
.split
(prev
)
1379 var block
= v
.current_block
.split
(line
.xml_end_line
.as(not null))
1380 block
.kind
= new BlockXML(block
)
1381 v
.current_block
.remove_leading_empty_lines
1382 v
.current_line
= v
.current_block
.first_line
1386 # A markdown blockquote line.
1387 class LineBlockquote
1390 redef fun process
(v
) do
1391 var line
= v
.current_line
1393 while line
!= null do
1394 if not line
.is_empty
and (line
.prev_empty
and
1395 line
.leading
== 0 and
1396 not line
.kind
(v
) isa LineBlockquote) then break
1401 if line
!= null then
1402 block
= v
.current_block
.split
(line
.prev
.as(not null))
1404 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1406 var kind
= new BlockQuote(block
)
1408 block
.remove_surrounding_empty_lines
1409 kind
.remove_block_quote_prefix
(block
)
1410 v
.current_line
= line
1411 v
.recurse
(block
, false)
1412 v
.current_line
= v
.current_block
.first_line
1416 # A markdown ruler line.
1420 redef fun process
(v
) do
1421 var line
= v
.current_line
1422 if line
.prev
!= null then v
.current_block
.split
(line
.prev
.as(not null))
1423 var block
= v
.current_block
.split
(line
.as(not null))
1424 block
.kind
= new BlockRuler(block
)
1425 v
.current_block
.remove_leading_empty_lines
1426 v
.current_line
= v
.current_block
.first_line
1430 # A markdown fence code line.
1434 redef fun process
(v
) do
1436 var line
= v
.current_line
.next
1437 while line
!= null do
1438 if line
.kind
(v
) isa LineFence then break
1441 if line
!= null then
1446 if line
!= null then
1447 block
= v
.current_block
.split
(line
.prev
.as(not null))
1449 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1451 block
.kind
= new BlockFence(block
)
1452 block
.first_line
.clear
1453 if block
.last_line
.kind
(v
) isa LineFence then
1454 block
.last_line
.clear
1456 block
.remove_surrounding_empty_lines
1457 v
.current_line
= line
1461 # A markdown headline.
1465 redef fun process
(v
) do
1466 var line
= v
.current_line
1467 var lprev
= line
.prev
1468 if lprev
!= null then v
.current_block
.split
(lprev
)
1469 var block
= v
.current_block
.split
(line
.as(not null))
1470 var kind
= new BlockHeadline(block
)
1472 kind
.transform_headline
(block
)
1473 v
.current_block
.remove_leading_empty_lines
1474 v
.current_line
= v
.current_block
.first_line
1478 # A markdown headline of level 1.
1482 redef fun process
(v
) do
1483 var line
= v
.current_line
1484 var lprev
= line
.prev
1485 if lprev
!= null then v
.current_block
.split
(lprev
)
1487 var block
= v
.current_block
.split
(line
.as(not null))
1488 var kind
= new BlockHeadline(block
)
1490 kind
.transform_headline
(block
)
1492 v
.current_block
.remove_leading_empty_lines
1493 v
.current_line
= v
.current_block
.first_line
1497 # A markdown headline of level 2.
1501 redef fun process
(v
) do
1502 var line
= v
.current_line
1503 var lprev
= line
.prev
1504 if lprev
!= null then v
.current_block
.split
(lprev
)
1506 var block
= v
.current_block
.split
(line
.as(not null))
1507 var kind
= new BlockHeadline(block
)
1509 kind
.transform_headline
(block
)
1511 v
.current_block
.remove_leading_empty_lines
1512 v
.current_line
= v
.current_block
.first_line
1516 # A markdown list line.
1517 # Mainly used to factorize code between ordered and unordered lists.
1521 redef fun process
(v
) do
1522 var line
= v
.current_line
1524 while line
!= null do
1525 var t
= line
.kind
(v
)
1526 if not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
1527 not t
isa LineList) then break
1532 if line
!= null then
1533 list
= v
.current_block
.split
(line
.prev
.as(not null))
1535 list
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1537 var kind
= block_kind
(list
)
1539 list
.first_line
.prev_empty
= false
1540 list
.last_line
.next_empty
= false
1541 list
.remove_surrounding_empty_lines
1542 list
.first_line
.prev_empty
= false
1543 list
.last_line
.next_empty
= false
1545 var block
= list
.first_block
1546 while block
!= null do
1547 block
.remove_list_indent
(v
)
1548 v
.recurse
(block
, true)
1551 kind
.expand_paragraphs
(list
)
1552 v
.current_line
= line
1555 # Create a new block kind based on this line.
1556 protected fun block_kind
(block
: MDBlock): BlockList is abstract
1558 protected fun extract_value
(line
: MDLine): String is abstract
1561 # An ordered list line.
1565 redef fun block_kind
(block
) do return new BlockOrderedList(block
)
1567 redef fun extract_value
(line
) do
1568 return line
.value
.substring_from
(line
.value
.index_of
('.') + 2)
1572 # An unordered list line.
1576 redef fun block_kind
(block
) do return new BlockUnorderedList(block
)
1578 redef fun extract_value
(line
) do
1579 return line
.value
.substring_from
(line
.leading
+ 2)
1583 # A token represent a character in the markdown input.
1584 # Some tokens have a specific markup behaviour that is handled here.
1585 abstract class Token
1587 # Position of `self` in markdown input.
1590 # Character found at `pos` in the markdown input.
1593 # Output that token using `MarkdownEmitter::decorator`.
1594 fun emit
(v
: MarkdownEmitter) do v
.addc char
1597 # A token without a specific meaning.
1602 # An emphasis token.
1603 abstract class TokenEm
1606 redef fun emit
(v
) do
1607 var tmp
= v
.push_buffer
1608 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1611 v
.decorator
.add_em
(v
, tmp
)
1619 # An emphasis star token.
1624 # An emphasis underscore token.
1625 class TokenEmUnderscore
1630 abstract class TokenStrong
1633 redef fun emit
(v
) do
1634 var tmp
= v
.push_buffer
1635 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 2, self)
1638 v
.decorator
.add_strong
(v
, tmp
)
1639 v
.current_pos
= b
+ 1
1646 # A strong star token.
1647 class TokenStrongStar
1651 # A strong underscore token.
1652 class TokenStrongUnderscore
1657 # This class is mainly used to factorize work between single and double quoted span codes.
1658 abstract class TokenCode
1661 redef fun emit
(v
) do
1662 var a
= pos
+ next_pos
+ 1
1663 var b
= v
.current_text
.find_token
(a
, self)
1665 v
.current_pos
= b
+ next_pos
1666 while a
< b
and v
.current_text
[a
] == ' ' do a
+= 1
1668 while v
.current_text
[b
- 1] == ' ' do b
-= 1
1669 v
.decorator
.add_span_code
(v
, v
.current_text
.as(not null), a
, b
)
1676 private fun next_pos
: Int is abstract
1679 # A span code token.
1680 class TokenCodeSingle
1683 redef fun next_pos
do return 0
1686 # A doubled span code token.
1687 class TokenCodeDouble
1690 redef fun next_pos
do return 1
1693 # A link or image token.
1694 # This class is mainly used to factorize work between images and links.
1695 abstract class TokenLinkOrImage
1699 var link
: nullable Text = null
1702 var name
: nullable Text = null
1705 var comment
: nullable Text = null
1707 # Is the link construct an abbreviation?
1708 var is_abbrev
= false
1710 redef fun emit
(v
) do
1711 var tmp
= new FlatBuffer
1712 var b
= check_link
(v
, tmp
, pos
, self)
1721 # Emit the hyperlink as link or image.
1722 private fun emit_hyper
(v
: MarkdownEmitter) is abstract
1724 # Check if the link is a valid link.
1725 private fun check_link
(v
: MarkdownEmitter, out
: FlatBuffer, start
: Int, token
: Token): Int do
1726 var md
= v
.current_text
1728 if token
isa TokenLink then
1733 var tmp
= new FlatBuffer
1734 pos
= md
.read_md_link_id
(tmp
, pos
)
1735 if pos
< start
then return -1
1739 pos
= md
.skip_spaces
(pos
)
1741 var tid
= name
.write_to_string
.to_lower
1742 if v
.processor
.link_refs
.has_key
(tid
) then
1743 var lr
= v
.processor
.link_refs
[tid
]
1744 is_abbrev
= lr
.is_abbrev
1751 else if md
[pos
] == '(' then
1753 pos
= md
.skip_spaces
(pos
)
1754 if pos
< start
then return -1
1755 tmp
= new FlatBuffer
1756 var use_lt
= md
[pos
] == '<'
1758 pos
= md
.read_until
(tmp
, pos
+ 1, '>')
1760 pos
= md
.read_md_link
(tmp
, pos
)
1762 if pos
< start
then return -1
1763 if use_lt
then pos
+= 1
1764 link
= tmp
.write_to_string
1765 if md
[pos
] == ' ' then
1766 pos
= md
.skip_spaces
(pos
)
1767 if pos
> start
and md
[pos
] == '"' then
1769 tmp
= new FlatBuffer
1770 pos
= md
.read_until
(tmp
, pos
, '"')
1771 if pos
< start
then return -1
1772 comment
= tmp
.write_to_string
1774 pos
= md
.skip_spaces
(pos
)
1775 if pos
== -1 then return -1
1778 if md
[pos
] != ')' then return -1
1779 else if md
[pos
] == '[' then
1781 tmp
= new FlatBuffer
1782 pos
= md
.read_raw_until
(tmp
, pos
, ']')
1783 if pos
< start
then return -1
1785 if tmp
.length
> 0 then
1790 var tid
= id
.write_to_string
.to_lower
1791 if v
.processor
.link_refs
.has_key
(tid
) then
1792 var lr
= v
.processor
.link_refs
[tid
]
1797 var tid
= name
.write_to_string
.replace
("\n", " ").to_lower
1798 if v
.processor
.link_refs
.has_key
(tid
) then
1799 var lr
= v
.processor
.link_refs
[tid
]
1807 if link
== null then return -1
1812 # A markdown link token.
1814 super TokenLinkOrImage
1816 redef fun emit_hyper
(v
) do
1817 if is_abbrev
and comment
!= null then
1818 v
.decorator
.add_abbr
(v
, name
.as(not null), comment
.as(not null))
1820 v
.decorator
.add_link
(v
, link
.as(not null), name
.as(not null), comment
)
1825 # A markdown image token.
1827 super TokenLinkOrImage
1829 redef fun emit_hyper
(v
) do
1830 v
.decorator
.add_image
(v
, link
.as(not null), name
.as(not null), comment
)
1838 redef fun emit
(v
) do
1839 var tmp
= new FlatBuffer
1840 var b
= check_html
(v
, tmp
, v
.current_text
.as(not null), v
.current_pos
)
1845 v
.decorator
.escape_char
(v
, char
)
1849 # Is the HTML valid?
1850 # Also take care of link and mailto shortcuts.
1851 private fun check_html
(v
: MarkdownEmitter, out
: FlatBuffer, md
: Text, start
: Int): Int do
1852 # check for auto links
1853 var tmp
= new FlatBuffer
1854 var pos
= md
.read_until
(tmp
, start
+ 1, ':', ' ', '>', '\n')
1855 if pos
!= -1 and md
[pos
] == ':' and tmp
.is_link_prefix
then
1856 pos
= md
.read_until
(tmp
, pos
, '>')
1858 var link
= tmp
.write_to_string
1859 v
.decorator
.add_link
(v
, link
, link
, null)
1863 # TODO check for mailto
1864 # check for inline html
1865 if start
+ 2 < md
.length
then
1866 return md
.read_xml
(out
, start
, true)
1872 # An HTML entity token.
1876 redef fun emit
(v
) do
1877 var tmp
= new FlatBuffer
1878 var b
= check_entity
(tmp
, v
.current_text
.as(not null), pos
)
1883 v
.decorator
.escape_char
(v
, char
)
1887 # Is the entity valid?
1888 private fun check_entity
(out
: FlatBuffer, md
: Text, start
: Int): Int do
1889 var pos
= md
.read_until
(out
, start
, ';')
1890 if pos
< 0 or out
.length
< 3 then
1893 if out
[1] == '#' then
1894 if out
[2] == 'x' or out
[2] == 'X' then
1895 if out
.length
< 4 then return -1
1896 for i
in [3..out
.length
[ do
1898 if (c
< '0' or c
> '9') and (c
< 'a' and c
> 'f') and (c
< 'A' and c
> 'F') then
1903 for i
in [2..out
.length
[ do
1905 if c
< '0' or c
> '9' then return -1
1910 for i
in [1..out
.length
[ do
1912 if not c
.is_digit
and not c
.is_letter
then return -1
1915 # TODO check entity is valid
1916 # if out.is_entity then
1926 # A markdown escape token.
1930 redef fun emit
(v
) do
1932 v
.addc v
.current_text
[v
.current_pos
]
1936 # A markdown super token.
1940 redef fun emit
(v
) do
1941 var tmp
= v
.push_buffer
1942 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1945 v
.decorator
.add_super
(v
, tmp
)
1955 # Get the token kind at `pos`.
1956 private fun token_at
(pos
: Int): Token do
1969 if pos
+ 1 < length
then
1974 if pos
+ 2 < length
then
1979 if pos
+ 3 < length
then
1987 if c0
!= ' ' or c2
!= ' ' then
1988 return new TokenStrongStar(pos
, c
)
1990 return new TokenEmStar(pos
, c
)
1993 if c0
!= ' ' or c1
!= ' ' then
1994 return new TokenEmStar(pos
, c
)
1996 return new TokenNone(pos
, c
)
1998 else if c
== '_' then
2000 if c0
!= ' ' or c2
!= ' 'then
2001 return new TokenStrongUnderscore(pos
, c
)
2003 return new TokenEmUnderscore(pos
, c
)
2006 if c0
!= ' ' or c1
!= ' ' then
2007 return new TokenEmUnderscore(pos
, c
)
2009 return new TokenNone(pos
, c
)
2011 else if c
== '!' then
2012 if c1
== '[' then return new TokenImage(pos
, c
)
2013 return new TokenNone(pos
, c
)
2014 else if c
== '[' then
2015 return new TokenLink(pos
, c
)
2016 else if c
== ']' then
2017 return new TokenNone(pos
, c
)
2018 else if c
== '`' then
2020 return new TokenCodeDouble(pos
, c
)
2022 return new TokenCodeSingle(pos
, c
)
2024 else if c
== '\\' then
2025 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
2026 return new TokenEscape(pos, c)
2028 return new TokenNone(pos, c)
2030 else if c == '<' then
2031 return new TokenHTML(pos, c)
2032 else if c == '&' then
2033 return new TokenEntity(pos, c)
2034 else if c == '^' then
2035 if c0 == '^' or c1 == '^' then
2036 return new TokenNone(pos, c)
2038 return new TokenSuper(pos, c)
2041 return new TokenNone(pos, c)
2045 # Find the position of a `token
` in `self`.
2046 private fun find_token(start: Int, token: Token): Int do
2048 while pos < length do
2049 if token_at(pos).is_same_type(token) then
2057 # Get the position of the next non-space character.
2058 private fun skip_spaces(start: Int): Int do
2060 while pos > -1 and pos < length and (self[pos] == ' ' or self[pos] == '\n') do
2063 if pos < length then return pos
2067 # Read `self` until `nend
` and append it to the `out
` buffer.
2068 # Escape markdown special chars.
2069 private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do
2071 while pos < length do
2073 if c == '\\' and pos + 1 < length then
2074 pos = escape(out, self[pos + 1], pos)
2076 var end_reached = false
2083 if end_reached then break
2088 if pos == length then return -1
2092 # Read `self` as raw text until `nend
` and append it to the `out
` buffer.
2093 # No escape is made.
2094 private fun read_raw_until(out: FlatBuffer, start: Int, nend: Char...): Int do
2096 while pos < length do
2098 var end_reached = false
2105 if end_reached then break
2109 if pos == length then return -1
2113 # Read `self` as XML until `to
` and append it to the `out
` buffer.
2114 # Escape HTML special chars.
2115 private fun read_xml_until(out: FlatBuffer, from: Int, to: Char...): Int do
2118 var str_char: nullable Char = null
2119 while pos < length do
2125 if pos < length then
2131 if c == str_char then
2138 if c == '"' or c == '\'' then
2143 var end_reached = false
2144 for n in [0..to.length[ do
2150 if end_reached then break
2155 if pos == length then return -1
2159 # Read `self` as XML and append it to the `out
` buffer.
2160 # Safe mode can be activated to limit reading to valid xml.
2161 private fun read_xml(out: FlatBuffer, start: Int, safe_mode: Bool): Int do
2163 var is_close_tag = false
2164 if start + 1 >= length then return -1
2165 if self[start + 1] == '/' then
2168 else if self[start + 1] == '!' then
2172 is_close_tag = false
2176 var tmp = new FlatBuffer
2177 pos = read_xml_until(tmp, pos, ' ', '/', '>')
2178 if pos == -1 then return -1
2179 var tag = tmp.write_to_string.trim.to_lower
2180 if tag.is_html_unsafe then
2182 if is_close_tag then out.add '/'
2186 if is_close_tag then out.add '/'
2191 if is_close_tag then out.add '/'
2192 pos = read_xml_until(out, pos, ' ', '/', '>')
2194 if pos == -1 then return -1
2195 pos = read_xml_until(out, pos, '/', '>')
2196 if pos == -1 then return -1
2197 if self[pos] == '/' then
2199 pos = self.read_xml_until(out, pos + 1, '>')
2200 if pos == -1 then return -1
2202 if self[pos] == '>' then
2209 # Read a markdown link address and append it to the `out
` buffer.
2210 private fun read_md_link(out: FlatBuffer, start: Int): Int do
2213 while pos < length do
2215 if c == '\\' and pos + 1 < length then
2216 pos = escape(out, self[pos + 1], pos)
2218 var end_reached = false
2221 else if c == ' ' then
2222 if counter == 1 then end_reached = true
2223 else if c == ')' then
2225 if counter == 0 then end_reached = true
2227 if end_reached then break
2232 if pos == length then return -1
2236 # Read a markdown link text and append it to the `out
` buffer.
2237 private fun read_md_link_id(out: FlatBuffer, start: Int): Int do
2240 while pos < length do
2242 var end_reached = false
2246 else if c == ']' then
2248 if counter == 0 then
2256 if end_reached then break
2259 if pos == length then return -1
2263 # Extract the XML tag name from a XML tag.
2264 private fun xml_tag: String do
2265 var tpl = new FlatBuffer
2267 if pos < length and self[1] == '/' then pos += 1
2268 while pos < length - 1 and (self[pos].is_digit or self[pos].is_letter) do
2272 return tpl.write_to_string.to_lower
2275 # Read and escape the markdown contained in `self`.
2276 private fun escape(out: FlatBuffer, c: Char, pos: Int): Int do
2277 if c == '\\' or c == '[' or c == ']' or c == '(' or c == ')' or c == '{' or
2278 c == '}' or c == '#' or c == '"' or c == '\'' or c == '.' or c == '<' or
2279 c == '>' or c == '*' or c == '+' or c == '-' or c == '_' or c == '!' or
2280 c == '`' or c == '~
' or c == '^
' then
2288 # Is `self` an unsafe HTML element?
2289 private fun is_html_unsafe: Bool do return html_unsafe_tags.has(self.write_to_string)
2291 # Is `self` a HRML block element?
2292 private fun is_html_block: Bool do return html_block_tags.has(self.write_to_string)
2294 # Is `self` a link prefix?
2295 private fun is_link_prefix: Bool do return html_link_prefixes.has(self.write_to_string)
2297 private fun html_unsafe_tags: Array[String] do return once ["applet", "head", "body", "frame", "frameset", "iframe", "script", "object"]
2299 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"]
2301 private fun html_link_prefixes: Array[String] do return once ["http", "https", "ftp", "ftps"]
2306 # Parse `self` as markdown and return the HTML representation
2308 # var md = "**Hello World!**"
2309 # var html = md.md_to_html
2310 # assert html == "<p><strong>Hello World!</strong></p>\n"
2311 fun md_to_html: Streamable do
2312 var processor = new MarkdownProcessor
2313 return processor.process(self)