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 line_kind(current_line.as(not null)).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
212 fun line_kind(md: MDLine): Line do
214 var leading = md.leading
215 var trailing = md.trailing
216 if md.is_empty then return new LineEmpty
217 if md.leading > 3 then return new LineCode
218 if value[leading] == '#' then return new LineHeadline
219 if value[leading] == '>' then return new LineBlockquote
221 if value.length - leading - trailing > 2 then
222 if value[leading] == '`' and md.count_chars_start('`') >= 3 then
225 if value[leading] == '~' and md.count_chars_start('~') >= 3 then
230 if value.length - leading - trailing > 2 and
231 (value[leading] == '*' or value[leading] == '-' or value[leading] == '_') then
232 if md.count_chars(value[leading]) >= 3 then
237 if value.length - leading >= 2 and value[leading + 1] == ' ' then
238 var c = value[leading]
239 if c == '*' or c == '-' or c == '+' then return new LineUList
242 if value.length - leading >= 3 and value[leading].is_digit then
244 while i < value.length and value[i].is_digit do i += 1
245 if i + 1 < value.length and value[i] == '.' and value[i + 1] == ' ' then
250 if value[leading] == '<' and md.check_html then return new LineXML
253 if next != null and not next.is_empty then
254 if next.count_chars('=') > 0 then
255 return new LineHeadline1
257 if next.count_chars('-') > 0 then
258 return new LineHeadline2
266 # Emit output corresponding to blocks content.
268 # Blocks are created by a previous pass in `MarkdownProcessor`.
269 # The emitter use a `Decorator` to select the output format.
270 class MarkdownEmitter
272 # Processor containing link refs.
273 var processor: MarkdownProcessor
275 # Decorator used for output.
276 # Default is `HTMLDecorator`
277 var decorator: Decorator = new HTMLDecorator is writable
279 # Create a new `MardownEmitter` using the default `HTMLDecorator`
280 init(processor: MarkdownProcessor) do
281 self.processor = processor
284 # Create a new `MarkdownEmitter` using a custom `decorator`.
285 init with_decorator(processor: MarkdownProcessor, decorator: Decorator) do
287 self.decorator = decorator
290 # Output `block` using `decorator` in the current buffer.
291 fun emit(block: Block): Text do
292 var buffer = push_buffer
298 # Output the content of `block`.
299 fun emit_in(block: Block) do block.emit_in(self)
301 # Transform and emit mardown text
302 fun emit_text(text: Text) do
303 emit_text_until(text, 0, null)
306 # Transform and emit mardown text starting at `from` and
307 # until a token with the same type as `token` is found.
308 # Go until the end of text if `token` is null.
309 fun emit_text_until(text: Text, start: Int, token: nullable Token): Int do
310 var old_text = current_text
311 var old_pos = current_pos
314 while current_pos < text.length do
315 var mt = text.token_at(current_pos)
316 if (token != null and not token isa TokenNone) and
317 (mt.is_same_type(token) or
318 (token isa TokenEmStar and mt isa TokenStrongStar) or
319 (token isa TokenEmUnderscore and mt isa TokenStrongUnderscore)) then
325 current_text = old_text
326 current_pos = old_pos
330 # Currently processed position in `current_text`.
331 # Used when visiting inline production with `emit_text_until`.
332 private var current_pos: Int = -1
334 # Currently processed text.
335 # Used when visiting inline production with `emit_text_until`.
336 private var current_text: nullable Text = null
339 private var buffer_stack = new List[FlatBuffer]
341 # Push a new buffer on the stack.
342 private fun push_buffer: FlatBuffer do
343 var buffer = new FlatBuffer
344 buffer_stack.add buffer
348 # Pop the last buffer.
349 private fun pop_buffer do buffer_stack.pop
351 # Current output buffer.
352 private fun current_buffer: FlatBuffer do
353 assert not buffer_stack.is_empty
354 return buffer_stack.last
357 # Append `e` to current buffer.
358 fun add(e: Streamable) do
360 current_buffer.append e
362 current_buffer.append e.write_to_string
366 # Append `c` to current buffer.
367 fun addc(c: Char) do current_buffer.add c
369 # Append a "\n
" line break.
370 fun addn do current_buffer.add '\n'
374 # Links that are specified somewhere in the mardown document to be reused as shortcuts.
378 # [1]: http://example.com/ "Optional title
"
384 # Optional link title
385 var title: nullable String = null
387 # Is the link an abreviation?
388 var is_abbrev = false
390 init with_title(link: String, title: nullable String) do
396 # A `Decorator` is used to emit mardown into a specific format.
397 # Default decorator used is `HTMLDecorator`.
400 # Render a ruler block.
401 fun add_ruler(v: MarkdownEmitter, block: BlockRuler) is abstract
403 # Render a headline block with corresponding level.
404 fun add_headline(v: MarkdownEmitter, block: BlockHeadline) is abstract
406 # Render a paragraph block.
407 fun add_paragraph(v: MarkdownEmitter, block: BlockParagraph) is abstract
409 # Render a code or fence block.
410 fun add_code(v: MarkdownEmitter, block: BlockCode) is abstract
412 # Render a blockquote.
413 fun add_blockquote(v: MarkdownEmitter, block: BlockQuote) is abstract
415 # Render an unordered list.
416 fun add_unorderedlist(v: MarkdownEmitter, block: BlockUnorderedList) is abstract
418 # Render an ordered list.
419 fun add_orderedlist(v: MarkdownEmitter, block: BlockOrderedList) is abstract
421 # Render a list item.
422 fun add_listitem(v: MarkdownEmitter, block: BlockListItem) is abstract
424 # Render an emphasis text.
425 fun add_em(v: MarkdownEmitter, text: Text) is abstract
427 # Render a strong text.
428 fun add_strong(v: MarkdownEmitter, text: Text) is abstract
430 # Render a super text.
431 fun add_super(v: MarkdownEmitter, text: Text) is abstract
434 fun add_link(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract
437 fun add_image(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract
439 # Render an abbreviation.
440 fun add_abbr(v: MarkdownEmitter, name: Text, comment: Text) is abstract
442 # Render a code span reading from a buffer.
443 fun add_span_code(v: MarkdownEmitter, buffer: Text, from, to: Int) is abstract
445 # Render a text and escape it.
446 fun append_value(v: MarkdownEmitter, value: Text) is abstract
448 # Render code text from buffer and escape it.
449 fun append_code(v: MarkdownEmitter, buffer: Text, from, to: Int) is abstract
451 # Render a character escape.
452 fun escape_char(v: MarkdownEmitter, char: Char) is abstract
454 # Render a line break
455 fun add_line_break(v: MarkdownEmitter) is abstract
457 # Generate a new html valid id from a `String`.
458 fun strip_id(txt: String): String is abstract
460 # Found headlines during the processing labeled by their ids.
461 fun headlines: ArrayMap[String, HeadLine] is abstract
464 # Class representing a markdown headline.
466 # Unique identifier of this headline.
469 # Text of the headline.
472 # Level of this headline.
474 # According toe the markdown specification, level must be in `[1..6]`.
478 # `Decorator` that outputs HTML.
482 redef var headlines = new ArrayMap[String, HeadLine]
484 redef fun add_ruler(v, block) do v.add "<hr
/>\n
"
486 redef fun add_headline(v, block) do
488 var txt = block.block.first_line.value
489 var id = strip_id(txt)
490 var lvl = block.depth
491 headlines[id] = new HeadLine(id, txt, lvl)
493 v.add "<h
{lvl} id
=\
"{id}\">"
498 redef fun add_paragraph(v, block) do
504 redef fun add_code(v, block) do
507 v.add "</code
></pre
>\n
"
510 redef fun add_blockquote(v, block) do
511 v.add "<blockquote
>\n
"
513 v.add "</blockquote
>\n
"
516 redef fun add_unorderedlist(v, block) do
522 redef fun add_orderedlist(v, block) do
528 redef fun add_listitem(v, block) do
534 redef fun add_em(v, text) do
540 redef fun add_strong(v, text) do
546 redef fun add_super(v, text) do
552 redef fun add_image(v, link, name, comment) do
554 append_value
(v
, link
)
556 append_value
(v
, name
)
558 if comment != null and not comment.is_empty then
560 append_value
(v
, comment
)
566 redef fun add_link(v, link, name, comment) do
568 append_value
(v
, link
)
570 if comment != null and not comment.is_empty then
572 append_value
(v
, comment
)
580 redef fun add_abbr(v, name, comment) do
581 v.add "<abbr title
=\
""
582 append_value
(v
, comment
)
588 redef fun add_span_code(v, text, from, to) do
590 append_code(v, text, from, to)
594 redef fun add_line_break(v) do
598 redef fun append_value(v, text) do for c in text do escape_char(v, c)
600 redef fun escape_char(v, c) do
603 else if c == '<' then
605 else if c == '>' then
607 else if c == '"' then
609 else if c == '\
'' then
616 redef fun append_code
(v
, buffer
, from
, to
) do
617 for i
in [from
..to
[ do
621 else if c
== '<' then
623 else if c
== '>' then
631 redef fun strip_id
(txt
) do
633 var b
= new FlatBuffer
638 if not c
.is_letter
and
640 not allowed_id_chars
.has
(c
) then continue
646 # check for multiple id definitions
647 if headlines
.has_key
(key
) then
650 while headlines
.has_key
(key
) do
658 private var allowed_id_chars
: Array[Char] = ['-', '_', ':', '.']
661 # A block of markdown lines.
662 # A `MDBlock` can contains lines and/or sub-blocks.
666 var kind
: Block = new BlockNone(self) is writable
669 var first_line
: nullable MDLine = null is writable
672 var last_line
: nullable MDLine = null is writable
674 # First sub-block if any.
675 var first_block
: nullable MDBlock = null is writable
677 # Last sub-block if any.
678 var last_block
: nullable MDBlock = null is writable
680 # Previous block if any.
681 var prev
: nullable MDBlock = null is writable
684 var next
: nullable MDBlock = null is writable
686 # Does this block contain subblocks?
687 fun has_blocks
: Bool do return first_block
!= null
690 fun count_blocks
: Int do
692 var block
= first_block
693 while block
!= null do
700 # Does this block contain lines?
701 fun has_lines
: Bool do return first_line
!= null
704 fun count_lines
: Int do
706 var line
= first_line
707 while line
!= null do
714 # Split `self` creating a new sub-block having `line` has `last_line`.
715 fun split
(line
: MDLine): MDBlock do
716 var block
= new MDBlock
717 block
.first_line
= first_line
718 block
.last_line
= line
719 first_line
= line
.next
721 if first_line
== null then
724 first_line
.prev
= null
726 if first_block
== null then
730 last_block
.next
= block
736 # Add a `line` to this block.
737 fun add_line
(line
: MDLine) do
738 if last_line
== null then
742 last_line
.next_empty
= line
.is_empty
743 line
.prev_empty
= last_line
.is_empty
744 line
.prev
= last_line
745 last_line
.next
= line
750 # Remove `line` from this block.
751 fun remove_line
(line
: MDLine) do
752 if line
.prev
== null then
753 first_line
= line
.next
755 line
.prev
.next
= line
.next
757 if line
.next
== null then
758 last_line
= line
.prev
760 line
.next
.prev
= line
.prev
766 # Remove leading empty lines.
767 fun remove_leading_empty_lines
: Bool do
768 var was_empty
= false
769 var line
= first_line
770 while line
!= null and line
.is_empty
do
778 # Remove trailing empty lines.
779 fun remove_trailing_empty_lines
: Bool do
780 var was_empty
= false
782 while line
!= null and line
.is_empty
do
790 # Remove leading and trailing empty lines.
791 fun remove_surrounding_empty_lines
: Bool do
792 var was_empty
= false
793 if remove_leading_empty_lines
then was_empty
= true
794 if remove_trailing_empty_lines
then was_empty
= true
798 # Remove list markers and up to 4 leading spaces.
799 # Used to clean nested lists.
800 fun remove_list_indent
(v
: MarkdownProcessor) do
801 var line
= first_line
802 while line
!= null do
803 if not line
.is_empty
then
804 var kind
= v
.line_kind
(line
)
805 if kind
isa LineList then
806 line
.value
= kind
.extract_value
(line
)
808 line
.value
= line
.value
.substring_from
(line
.leading
.min
(4))
810 line
.leading
= line
.process_leading
816 # Collect block line text.
818 var text
= new FlatBuffer
819 var line
= first_line
820 while line
!= null do
821 if not line
.is_empty
then
822 text
.append line
.text
827 return text
.write_to_string
831 # Representation of a markdown block in the AST.
832 # Each `Block` is linked to a `MDBlock` that contains mardown code.
835 # The markdown block `self` is related to.
838 # Output `self` using `v.decorator`.
839 fun emit
(v
: MarkdownEmitter) do v
.emit_in
(self)
841 # Emit the containts of `self`, lines or blocks.
842 fun emit_in
(v
: MarkdownEmitter) do
843 block
.remove_surrounding_empty_lines
844 if block
.has_lines
then
851 # Emit lines contained in `block`.
852 fun emit_lines
(v
: MarkdownEmitter) do
853 var tpl
= v
.push_buffer
854 var line
= block
.first_line
855 while line
!= null do
856 if not line
.is_empty
then
857 v
.add line
.value
.substring
(line
.leading
, line
.value
.length
- line
.trailing
)
858 if line
.trailing
>= 2 then v
.decorator
.add_line_break
(v
)
860 if line
.next
!= null then
869 # Emit sub-blocks contained in `block`.
870 fun emit_blocks
(v
: MarkdownEmitter) do
871 var block
= self.block
.first_block
872 while block
!= null do
879 # A block without any markdown specificities.
881 # Actually use the same implementation than `BlockCode`,
882 # this class is only used for typing purposes.
887 # A markdown blockquote.
891 redef fun emit
(v
) do v
.decorator
.add_blockquote
(v
, self)
893 # Remove blockquote markers.
894 private fun remove_block_quote_prefix
(block
: MDBlock) do
895 var line
= block
.first_line
896 while line
!= null do
897 if not line
.is_empty
then
898 if line
.value
[line
.leading
] == '>' then
899 var rem
= line
.leading
+ 1
900 if line
.leading
+ 1 < line
.value
.length
and
901 line
.value
[line
.leading
+ 1] == ' ' then
904 line
.value
= line
.value
.substring_from
(rem
)
905 line
.leading
= line
.process_leading
913 # A markdown code block.
917 redef fun emit
(v
) do v
.decorator
.add_code
(v
, self)
919 redef fun emit_lines
(v
) do
920 var line
= block
.first_line
921 while line
!= null do
922 if not line
.is_empty
then
923 v
.decorator
.append_code
(v
, line
.value
, 4, line
.value
.length
)
931 # A markdown code-fence block.
933 # Actually use the same implementation than `BlockCode`,
934 # this class is only used for typing purposes.
939 # A markdown headline.
943 redef fun emit
(v
) do v
.decorator
.add_headline
(v
, self)
945 # Depth of the headline used to determine the headline level.
948 # Remove healine marks from lines contained in `self`.
949 private fun transform_headline
(block
: MDBlock) do
950 if depth
> 0 then return
952 var line
= block
.first_line
953 if line
.is_empty
then return
954 var start
= line
.leading
955 while start
< line
.value
.length
and line
.value
[start
] == '#' do
959 while start
< line
.value
.length
and line
.value
[start
] == ' ' do
962 if start
>= line
.value
.length
then
965 var nend
= line
.value
.length
- line
.trailing
- 1
966 while line
.value
[nend
] == '#' do nend
-= 1
967 while line
.value
[nend
] == ' ' do nend
-= 1
968 line
.value
= line
.value
.substring
(start
, nend
- start
+ 1)
976 # A markdown list item block.
980 redef fun emit
(v
) do v
.decorator
.add_listitem
(v
, self)
983 # A markdown list block.
984 # Can be either an ordered or unordered list, this class is mainly used to factorize code.
985 abstract class BlockList
988 # Split list block into list items sub-blocks.
989 private fun init_block
(v
: MarkdownProcessor) do
990 var line
= block
.first_line
992 while line
!= null do
993 var t
= v
.line_kind
(line
)
995 (not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
996 not (t
isa LineList))) then
997 var sblock
= block
.split
(line
.prev
.as(not null))
998 sblock
.kind
= new BlockListItem(sblock
)
1002 var sblock
= block
.split
(block
.last_line
.as(not null))
1003 sblock
.kind
= new BlockListItem(sblock
)
1006 # Expand list items as paragraphs if needed.
1007 private fun expand_paragraphs
(block
: MDBlock) do
1008 var outer
= block
.first_block
1009 var inner
: nullable MDBlock
1010 var has_paragraph
= false
1011 while outer
!= null and not has_paragraph
do
1012 if outer
.kind
isa BlockListItem then
1013 inner
= outer
.first_block
1014 while inner
!= null and not has_paragraph
do
1015 if inner
.kind
isa BlockParagraph then
1016 has_paragraph
= true
1023 if has_paragraph
then
1024 outer
= block
.first_block
1025 while outer
!= null do
1026 if outer
.kind
isa BlockListItem then
1027 inner
= outer
.first_block
1028 while inner
!= null do
1029 if inner
.kind
isa BlockNone then
1030 inner
.kind
= new BlockParagraph(inner
)
1041 # A markdown ordered list.
1042 class BlockOrderedList
1045 redef fun emit
(v
) do v
.decorator
.add_orderedlist
(v
, self)
1048 # A markdown unordred list.
1049 class BlockUnorderedList
1052 redef fun emit
(v
) do v
.decorator
.add_unorderedlist
(v
, self)
1055 # A markdown paragraph block.
1056 class BlockParagraph
1059 redef fun emit
(v
) do v
.decorator
.add_paragraph
(v
, self)
1066 redef fun emit
(v
) do v
.decorator
.add_ruler
(v
, self)
1069 # Xml blocks that can be found in markdown markup.
1073 redef fun emit_lines
(v
) do
1074 var line
= block
.first_line
1075 while line
!= null do
1076 if not line
.is_empty
then v
.add line
.value
1086 # Text contained in this line.
1087 var value
: String is writable
1089 # Is this line empty?
1090 # Lines containing only spaces are considered empty.
1091 var is_empty
: Bool = true is writable
1093 # Previous line in `MDBlock` or null if first line.
1094 var prev
: nullable MDLine = null is writable
1096 # Next line in `MDBlock` or null if last line.
1097 var next
: nullable MDLine = null is writable
1099 # Is the previous line empty?
1100 var prev_empty
: Bool = false is writable
1102 # Is the next line empty?
1103 var next_empty
: Bool = false is writable
1105 init(value
: String) do
1107 self.leading
= process_leading
1108 if leading
!= value
.length
then
1109 self.is_empty
= false
1110 self.trailing
= process_trailing
1114 # Set `value` as an empty String and update `leading`, `trailing` and is_`empty`.
1120 if prev
!= null then prev
.next_empty
= true
1121 if next
!= null then next
.prev_empty
= true
1124 # Number or leading spaces on this line.
1125 var leading
: Int = 0 is writable
1127 # Compute `leading` depending on `value`.
1128 fun process_leading
: Int do
1130 var value
= self.value
1131 while count
< value
.length
and value
[count
] == ' ' do count
+= 1
1132 if leading
== value
.length
then clear
1136 # Number of trailing spaces on this line.
1137 var trailing
: Int = 0 is writable
1139 # Compute `trailing` depending on `value`.
1140 fun process_trailing
: Int do
1142 var value
= self.value
1143 while value
[value
.length
- count
- 1] == ' ' do
1149 # Count the amount of `ch` in this line.
1150 # Return A value > 0 if this line only consists of `ch` end spaces.
1151 fun count_chars
(ch
: Char): Int do
1167 # Count the amount of `ch` at the start of this line ignoring spaces.
1168 fun count_chars_start
(ch
: Char): Int do
1183 # Last XML line if any.
1184 private var xml_end_line
: nullable MDLine = null
1186 # Does `value` contains valid XML markup?
1187 private fun check_html
: Bool do
1188 var tags
= new Array[String]
1189 var tmp
= new FlatBuffer
1191 if pos
+ 1 < value
.length
and value
[pos
+ 1] == '!' then
1192 if read_xml_comment
(self, pos
) > 0 then return true
1194 pos
= value
.read_xml
(tmp
, pos
, false)
1198 if not tag
.is_html_block
then
1206 var line
: nullable MDLine = self
1207 while line
!= null do
1208 while pos
< line
.value
.length
and line
.value
[pos
] != '<' do
1211 if pos
>= line
.value
.length
then
1212 if pos
- 2 >= 0 and line
.value
[pos
- 2] == '/' then
1214 if tags
.is_empty
then
1222 tmp
= new FlatBuffer
1223 var new_pos
= line
.value
.read_xml
(tmp
, pos
, false)
1226 if tag
.is_html_block
and not tag
== "hr" then
1227 if tmp
[1] == '/' then
1228 if tags
.last
!= tag
then
1236 if tags
.is_empty
then
1246 return tags
.is_empty
1251 # Read a XML comment.
1252 # Used by `check_html`.
1253 private fun read_xml_comment
(first_line
: MDLine, start
: Int): Int do
1254 var line
: nullable MDLine = first_line
1255 if start
+ 3 < line
.value
.length
then
1256 if line
.value
[2] == '-' and line
.value
[3] == '-' then
1258 while line
!= null do
1259 while pos
< line
.value
.length
and line
.value
[pos
] != '-' do
1262 if pos
== line
.value
.length
then
1266 if pos
+ 2 < line
.value
.length
then
1267 if line
.value
[pos
+ 1] == '-' and line
.value
[pos
+ 2] == '>' then
1268 first_line
.xml_end_line
= line
1280 # Extract the text of `self` without leading and trailing.
1281 fun text
: String do return value
.substring
(leading
, value
.length
- trailing
)
1288 # See `MarkdownProcessor::recurse`.
1289 fun process
(v
: MarkdownProcessor) is abstract
1292 # An empty markdown line.
1296 redef fun process
(v
) do
1297 v
.current_line
= v
.current_line
.next
1301 # A non-specific markdown construction.
1302 # Mainly used as part of another line construct such as paragraphs or lists.
1306 redef fun process
(v
) do
1307 var line
= v
.current_line
1309 var was_empty
= line
.prev_empty
1310 while line
!= null and not line
.is_empty
do
1311 var t
= v
.line_kind
(line
)
1312 if v
.in_list
and t
isa LineList then
1315 if t
isa LineCode or t
isa LineFence then
1318 if t
isa LineHeadline or t
isa LineHeadline1 or t
isa LineHeadline2 or
1319 t
isa LineHR or t
isa LineBlockquote or t
isa LineXML then
1326 if line
!= null and not line
.is_empty
then
1327 var block
= v
.current_block
.split
(line
.prev
.as(not null))
1328 if v
.in_list
and not was_empty
then
1329 block
.kind
= new BlockNone(block
)
1331 block
.kind
= new BlockParagraph(block
)
1333 v
.current_block
.remove_leading_empty_lines
1336 if line
!= null then
1337 block
= v
.current_block
.split
(line
)
1339 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1341 if v
.in_list
and (line
== null or not line
.is_empty
) and not was_empty
then
1342 block
.kind
= new BlockNone(block
)
1344 block
.kind
= new BlockParagraph(block
)
1346 v
.current_block
.remove_leading_empty_lines
1348 v
.current_line
= v
.current_block
.first_line
1352 # A line of markdown code.
1356 redef fun process
(v
) do
1357 var line
= v
.current_line
1359 while line
!= null and (line
.is_empty
or v
.line_kind
(line
) isa LineCode) do
1362 # split at block end line
1364 if line
!= null then
1365 block
= v
.current_block
.split
(line
.prev
.as(not null))
1367 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1369 block
.kind
= new BlockCode(block
)
1370 block
.remove_surrounding_empty_lines
1371 v
.current_line
= v
.current_block
.first_line
1375 # A line of raw XML.
1379 redef fun process
(v
) do
1380 var line
= v
.current_line
1381 var prev
= line
.prev
1382 if prev
!= null then v
.current_block
.split
(prev
)
1383 var block
= v
.current_block
.split
(line
.xml_end_line
.as(not null))
1384 block
.kind
= new BlockXML(block
)
1385 v
.current_block
.remove_leading_empty_lines
1386 v
.current_line
= v
.current_block
.first_line
1390 # A markdown blockquote line.
1391 class LineBlockquote
1394 redef fun process
(v
) do
1395 var line
= v
.current_line
1397 while line
!= null do
1398 if not line
.is_empty
and (line
.prev_empty
and
1399 line
.leading
== 0 and
1400 not v
.line_kind
(line
) isa LineBlockquote) then break
1405 if line
!= null then
1406 block
= v
.current_block
.split
(line
.prev
.as(not null))
1408 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1410 var kind
= new BlockQuote(block
)
1412 block
.remove_surrounding_empty_lines
1413 kind
.remove_block_quote_prefix
(block
)
1414 v
.current_line
= line
1415 v
.recurse
(block
, false)
1416 v
.current_line
= v
.current_block
.first_line
1420 # A markdown ruler line.
1424 redef fun process
(v
) do
1425 var line
= v
.current_line
1426 if line
.prev
!= null then v
.current_block
.split
(line
.prev
.as(not null))
1427 var block
= v
.current_block
.split
(line
.as(not null))
1428 block
.kind
= new BlockRuler(block
)
1429 v
.current_block
.remove_leading_empty_lines
1430 v
.current_line
= v
.current_block
.first_line
1434 # A markdown fence code line.
1438 redef fun process
(v
) do
1440 var line
= v
.current_line
.next
1441 while line
!= null do
1442 if v
.line_kind
(line
) isa LineFence then break
1445 if line
!= null then
1450 if line
!= null then
1451 block
= v
.current_block
.split
(line
.prev
.as(not null))
1453 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1455 block
.kind
= new BlockFence(block
)
1456 block
.first_line
.clear
1457 var last
= block
.last_line
1458 if last
!= null and v
.line_kind
(last
) isa LineFence then
1459 block
.last_line
.clear
1461 block
.remove_surrounding_empty_lines
1462 v
.current_line
= line
1466 # A markdown headline.
1470 redef fun process
(v
) do
1471 var line
= v
.current_line
1472 var lprev
= line
.prev
1473 if lprev
!= null then v
.current_block
.split
(lprev
)
1474 var block
= v
.current_block
.split
(line
.as(not null))
1475 var kind
= new BlockHeadline(block
)
1477 kind
.transform_headline
(block
)
1478 v
.current_block
.remove_leading_empty_lines
1479 v
.current_line
= v
.current_block
.first_line
1483 # A markdown headline of level 1.
1487 redef fun process
(v
) do
1488 var line
= v
.current_line
1489 var lprev
= line
.prev
1490 if lprev
!= null then v
.current_block
.split
(lprev
)
1492 var block
= v
.current_block
.split
(line
.as(not null))
1493 var kind
= new BlockHeadline(block
)
1495 kind
.transform_headline
(block
)
1497 v
.current_block
.remove_leading_empty_lines
1498 v
.current_line
= v
.current_block
.first_line
1502 # A markdown headline of level 2.
1506 redef fun process
(v
) do
1507 var line
= v
.current_line
1508 var lprev
= line
.prev
1509 if lprev
!= null then v
.current_block
.split
(lprev
)
1511 var block
= v
.current_block
.split
(line
.as(not null))
1512 var kind
= new BlockHeadline(block
)
1514 kind
.transform_headline
(block
)
1516 v
.current_block
.remove_leading_empty_lines
1517 v
.current_line
= v
.current_block
.first_line
1521 # A markdown list line.
1522 # Mainly used to factorize code between ordered and unordered lists.
1526 redef fun process
(v
) do
1527 var line
= v
.current_line
1529 while line
!= null do
1530 var t
= v
.line_kind
(line
)
1531 if not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
1532 not t
isa LineList) then break
1537 if line
!= null then
1538 list
= v
.current_block
.split
(line
.prev
.as(not null))
1540 list
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1542 var kind
= block_kind
(list
)
1544 list
.first_line
.prev_empty
= false
1545 list
.last_line
.next_empty
= false
1546 list
.remove_surrounding_empty_lines
1547 list
.first_line
.prev_empty
= false
1548 list
.last_line
.next_empty
= false
1550 var block
= list
.first_block
1551 while block
!= null do
1552 block
.remove_list_indent
(v
)
1553 v
.recurse
(block
, true)
1556 kind
.expand_paragraphs
(list
)
1557 v
.current_line
= line
1560 # Create a new block kind based on this line.
1561 protected fun block_kind
(block
: MDBlock): BlockList is abstract
1563 protected fun extract_value
(line
: MDLine): String is abstract
1566 # An ordered list line.
1570 redef fun block_kind
(block
) do return new BlockOrderedList(block
)
1572 redef fun extract_value
(line
) do
1573 return line
.value
.substring_from
(line
.value
.index_of
('.') + 2)
1577 # An unordered list line.
1581 redef fun block_kind
(block
) do return new BlockUnorderedList(block
)
1583 redef fun extract_value
(line
) do
1584 return line
.value
.substring_from
(line
.leading
+ 2)
1588 # A token represent a character in the markdown input.
1589 # Some tokens have a specific markup behaviour that is handled here.
1590 abstract class Token
1592 # Position of `self` in markdown input.
1595 # Character found at `pos` in the markdown input.
1598 # Output that token using `MarkdownEmitter::decorator`.
1599 fun emit
(v
: MarkdownEmitter) do v
.addc char
1602 # A token without a specific meaning.
1607 # An emphasis token.
1608 abstract class TokenEm
1611 redef fun emit
(v
) do
1612 var tmp
= v
.push_buffer
1613 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1616 v
.decorator
.add_em
(v
, tmp
)
1624 # An emphasis star token.
1629 # An emphasis underscore token.
1630 class TokenEmUnderscore
1635 abstract class TokenStrong
1638 redef fun emit
(v
) do
1639 var tmp
= v
.push_buffer
1640 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 2, self)
1643 v
.decorator
.add_strong
(v
, tmp
)
1644 v
.current_pos
= b
+ 1
1651 # A strong star token.
1652 class TokenStrongStar
1656 # A strong underscore token.
1657 class TokenStrongUnderscore
1662 # This class is mainly used to factorize work between single and double quoted span codes.
1663 abstract class TokenCode
1666 redef fun emit
(v
) do
1667 var a
= pos
+ next_pos
+ 1
1668 var b
= v
.current_text
.find_token
(a
, self)
1670 v
.current_pos
= b
+ next_pos
1671 while a
< b
and v
.current_text
[a
] == ' ' do a
+= 1
1673 while v
.current_text
[b
- 1] == ' ' do b
-= 1
1674 v
.decorator
.add_span_code
(v
, v
.current_text
.as(not null), a
, b
)
1681 private fun next_pos
: Int is abstract
1684 # A span code token.
1685 class TokenCodeSingle
1688 redef fun next_pos
do return 0
1691 # A doubled span code token.
1692 class TokenCodeDouble
1695 redef fun next_pos
do return 1
1698 # A link or image token.
1699 # This class is mainly used to factorize work between images and links.
1700 abstract class TokenLinkOrImage
1704 var link
: nullable Text = null
1707 var name
: nullable Text = null
1710 var comment
: nullable Text = null
1712 # Is the link construct an abbreviation?
1713 var is_abbrev
= false
1715 redef fun emit
(v
) do
1716 var tmp
= new FlatBuffer
1717 var b
= check_link
(v
, tmp
, pos
, self)
1726 # Emit the hyperlink as link or image.
1727 private fun emit_hyper
(v
: MarkdownEmitter) is abstract
1729 # Check if the link is a valid link.
1730 private fun check_link
(v
: MarkdownEmitter, out
: FlatBuffer, start
: Int, token
: Token): Int do
1731 var md
= v
.current_text
1733 if token
isa TokenLink then
1738 var tmp
= new FlatBuffer
1739 pos
= md
.read_md_link_id
(tmp
, pos
)
1740 if pos
< start
then return -1
1744 pos
= md
.skip_spaces
(pos
)
1746 var tid
= name
.write_to_string
.to_lower
1747 if v
.processor
.link_refs
.has_key
(tid
) then
1748 var lr
= v
.processor
.link_refs
[tid
]
1749 is_abbrev
= lr
.is_abbrev
1756 else if md
[pos
] == '(' then
1758 pos
= md
.skip_spaces
(pos
)
1759 if pos
< start
then return -1
1760 tmp
= new FlatBuffer
1761 var use_lt
= md
[pos
] == '<'
1763 pos
= md
.read_until
(tmp
, pos
+ 1, '>')
1765 pos
= md
.read_md_link
(tmp
, pos
)
1767 if pos
< start
then return -1
1768 if use_lt
then pos
+= 1
1769 link
= tmp
.write_to_string
1770 if md
[pos
] == ' ' then
1771 pos
= md
.skip_spaces
(pos
)
1772 if pos
> start
and md
[pos
] == '"' then
1774 tmp
= new FlatBuffer
1775 pos
= md
.read_until
(tmp
, pos
, '"')
1776 if pos
< start
then return -1
1777 comment
= tmp
.write_to_string
1779 pos
= md
.skip_spaces
(pos
)
1780 if pos
== -1 then return -1
1783 if md
[pos
] != ')' then return -1
1784 else if md
[pos
] == '[' then
1786 tmp
= new FlatBuffer
1787 pos
= md
.read_raw_until
(tmp
, pos
, ']')
1788 if pos
< start
then return -1
1790 if tmp
.length
> 0 then
1795 var tid
= id
.write_to_string
.to_lower
1796 if v
.processor
.link_refs
.has_key
(tid
) then
1797 var lr
= v
.processor
.link_refs
[tid
]
1802 var tid
= name
.write_to_string
.replace
("\n", " ").to_lower
1803 if v
.processor
.link_refs
.has_key
(tid
) then
1804 var lr
= v
.processor
.link_refs
[tid
]
1812 if link
== null then return -1
1817 # A markdown link token.
1819 super TokenLinkOrImage
1821 redef fun emit_hyper
(v
) do
1822 if is_abbrev
and comment
!= null then
1823 v
.decorator
.add_abbr
(v
, name
.as(not null), comment
.as(not null))
1825 v
.decorator
.add_link
(v
, link
.as(not null), name
.as(not null), comment
)
1830 # A markdown image token.
1832 super TokenLinkOrImage
1834 redef fun emit_hyper
(v
) do
1835 v
.decorator
.add_image
(v
, link
.as(not null), name
.as(not null), comment
)
1843 redef fun emit
(v
) do
1844 var tmp
= new FlatBuffer
1845 var b
= check_html
(v
, tmp
, v
.current_text
.as(not null), v
.current_pos
)
1850 v
.decorator
.escape_char
(v
, char
)
1854 # Is the HTML valid?
1855 # Also take care of link and mailto shortcuts.
1856 private fun check_html
(v
: MarkdownEmitter, out
: FlatBuffer, md
: Text, start
: Int): Int do
1857 # check for auto links
1858 var tmp
= new FlatBuffer
1859 var pos
= md
.read_until
(tmp
, start
+ 1, ':', ' ', '>', '\n')
1860 if pos
!= -1 and md
[pos
] == ':' and tmp
.is_link_prefix
then
1861 pos
= md
.read_until
(tmp
, pos
, '>')
1863 var link
= tmp
.write_to_string
1864 v
.decorator
.add_link
(v
, link
, link
, null)
1868 # TODO check for mailto
1869 # check for inline html
1870 if start
+ 2 < md
.length
then
1871 return md
.read_xml
(out
, start
, true)
1877 # An HTML entity token.
1881 redef fun emit
(v
) do
1882 var tmp
= new FlatBuffer
1883 var b
= check_entity
(tmp
, v
.current_text
.as(not null), pos
)
1888 v
.decorator
.escape_char
(v
, char
)
1892 # Is the entity valid?
1893 private fun check_entity
(out
: FlatBuffer, md
: Text, start
: Int): Int do
1894 var pos
= md
.read_until
(out
, start
, ';')
1895 if pos
< 0 or out
.length
< 3 then
1898 if out
[1] == '#' then
1899 if out
[2] == 'x' or out
[2] == 'X' then
1900 if out
.length
< 4 then return -1
1901 for i
in [3..out
.length
[ do
1903 if (c
< '0' or c
> '9') and (c
< 'a' and c
> 'f') and (c
< 'A' and c
> 'F') then
1908 for i
in [2..out
.length
[ do
1910 if c
< '0' or c
> '9' then return -1
1915 for i
in [1..out
.length
[ do
1917 if not c
.is_digit
and not c
.is_letter
then return -1
1920 # TODO check entity is valid
1921 # if out.is_entity then
1931 # A markdown escape token.
1935 redef fun emit
(v
) do
1937 v
.addc v
.current_text
[v
.current_pos
]
1941 # A markdown super token.
1945 redef fun emit
(v
) do
1946 var tmp
= v
.push_buffer
1947 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1950 v
.decorator
.add_super
(v
, tmp
)
1960 # Get the token kind at `pos`.
1961 private fun token_at
(pos
: Int): Token do
1974 if pos
+ 1 < length
then
1979 if pos
+ 2 < length
then
1984 if pos
+ 3 < length
then
1992 if c0
!= ' ' or c2
!= ' ' then
1993 return new TokenStrongStar(pos
, c
)
1995 return new TokenEmStar(pos
, c
)
1998 if c0
!= ' ' or c1
!= ' ' then
1999 return new TokenEmStar(pos
, c
)
2001 return new TokenNone(pos
, c
)
2003 else if c
== '_' then
2005 if c0
!= ' ' or c2
!= ' 'then
2006 return new TokenStrongUnderscore(pos
, c
)
2008 return new TokenEmUnderscore(pos
, c
)
2011 if c0
!= ' ' or c1
!= ' ' then
2012 return new TokenEmUnderscore(pos
, c
)
2014 return new TokenNone(pos
, c
)
2016 else if c
== '!' then
2017 if c1
== '[' then return new TokenImage(pos
, c
)
2018 return new TokenNone(pos
, c
)
2019 else if c
== '[' then
2020 return new TokenLink(pos
, c
)
2021 else if c
== ']' then
2022 return new TokenNone(pos
, c
)
2023 else if c
== '`' then
2025 return new TokenCodeDouble(pos
, c
)
2027 return new TokenCodeSingle(pos
, c
)
2029 else if c
== '\\' then
2030 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
2031 return new TokenEscape(pos, c)
2033 return new TokenNone(pos, c)
2035 else if c == '<' then
2036 return new TokenHTML(pos, c)
2037 else if c == '&' then
2038 return new TokenEntity(pos, c)
2039 else if c == '^' then
2040 if c0 == '^' or c1 == '^' then
2041 return new TokenNone(pos, c)
2043 return new TokenSuper(pos, c)
2046 return new TokenNone(pos, c)
2050 # Find the position of a `token
` in `self`.
2051 private fun find_token(start: Int, token: Token): Int do
2053 while pos < length do
2054 if token_at(pos).is_same_type(token) then
2062 # Get the position of the next non-space character.
2063 private fun skip_spaces(start: Int): Int do
2065 while pos > -1 and pos < length and (self[pos] == ' ' or self[pos] == '\n') do
2068 if pos < length then return pos
2072 # Read `self` until `nend
` and append it to the `out
` buffer.
2073 # Escape markdown special chars.
2074 private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do
2076 while pos < length do
2078 if c == '\\' and pos + 1 < length then
2079 pos = escape(out, self[pos + 1], pos)
2081 var end_reached = false
2088 if end_reached then break
2093 if pos == length then return -1
2097 # Read `self` as raw text until `nend
` and append it to the `out
` buffer.
2098 # No escape is made.
2099 private fun read_raw_until(out: FlatBuffer, start: Int, nend: Char...): Int do
2101 while pos < length do
2103 var end_reached = false
2110 if end_reached then break
2114 if pos == length then return -1
2118 # Read `self` as XML until `to
` and append it to the `out
` buffer.
2119 # Escape HTML special chars.
2120 private fun read_xml_until(out: FlatBuffer, from: Int, to: Char...): Int do
2123 var str_char: nullable Char = null
2124 while pos < length do
2130 if pos < length then
2136 if c == str_char then
2143 if c == '"' or c == '\'' then
2148 var end_reached = false
2149 for n in [0..to.length[ do
2155 if end_reached then break
2160 if pos == length then return -1
2164 # Read `self` as XML and append it to the `out
` buffer.
2165 # Safe mode can be activated to limit reading to valid xml.
2166 private fun read_xml(out: FlatBuffer, start: Int, safe_mode: Bool): Int do
2168 var is_close_tag = false
2169 if start + 1 >= length then return -1
2170 if self[start + 1] == '/' then
2173 else if self[start + 1] == '!' then
2177 is_close_tag = false
2181 var tmp = new FlatBuffer
2182 pos = read_xml_until(tmp, pos, ' ', '/', '>')
2183 if pos == -1 then return -1
2184 var tag = tmp.write_to_string.trim.to_lower
2185 if tag.is_html_unsafe then
2187 if is_close_tag then out.add '/'
2191 if is_close_tag then out.add '/'
2196 if is_close_tag then out.add '/'
2197 pos = read_xml_until(out, pos, ' ', '/', '>')
2199 if pos == -1 then return -1
2200 pos = read_xml_until(out, pos, '/', '>')
2201 if pos == -1 then return -1
2202 if self[pos] == '/' then
2204 pos = self.read_xml_until(out, pos + 1, '>')
2205 if pos == -1 then return -1
2207 if self[pos] == '>' then
2214 # Read a markdown link address and append it to the `out
` buffer.
2215 private fun read_md_link(out: FlatBuffer, start: Int): Int do
2218 while pos < length do
2220 if c == '\\' and pos + 1 < length then
2221 pos = escape(out, self[pos + 1], pos)
2223 var end_reached = false
2226 else if c == ' ' then
2227 if counter == 1 then end_reached = true
2228 else if c == ')' then
2230 if counter == 0 then end_reached = true
2232 if end_reached then break
2237 if pos == length then return -1
2241 # Read a markdown link text and append it to the `out
` buffer.
2242 private fun read_md_link_id(out: FlatBuffer, start: Int): Int do
2245 while pos < length do
2247 var end_reached = false
2251 else if c == ']' then
2253 if counter == 0 then
2261 if end_reached then break
2264 if pos == length then return -1
2268 # Extract the XML tag name from a XML tag.
2269 private fun xml_tag: String do
2270 var tpl = new FlatBuffer
2272 if pos < length and self[1] == '/' then pos += 1
2273 while pos < length - 1 and (self[pos].is_digit or self[pos].is_letter) do
2277 return tpl.write_to_string.to_lower
2280 # Read and escape the markdown contained in `self`.
2281 private fun escape(out: FlatBuffer, c: Char, pos: Int): Int do
2282 if c == '\\' or c == '[' or c == ']' or c == '(' or c == ')' or c == '{' or
2283 c == '}' or c == '#' or c == '"' or c == '\'' or c == '.' or c == '<' or
2284 c == '>' or c == '*' or c == '+' or c == '-' or c == '_' or c == '!' or
2285 c == '`' or c == '~
' or c == '^
' then
2293 # Is `self` an unsafe HTML element?
2294 private fun is_html_unsafe: Bool do return html_unsafe_tags.has(self.write_to_string)
2296 # Is `self` a HRML block element?
2297 private fun is_html_block: Bool do return html_block_tags.has(self.write_to_string)
2299 # Is `self` a link prefix?
2300 private fun is_link_prefix: Bool do return html_link_prefixes.has(self.write_to_string)
2302 private fun html_unsafe_tags: Array[String] do return once ["applet", "head", "body", "frame", "frameset", "iframe", "script", "object"]
2304 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"]
2306 private fun html_link_prefixes: Array[String] do return once ["http", "https", "ftp", "ftps"]
2311 # Parse `self` as markdown and return the HTML representation
2313 # var md = "**Hello World!**"
2314 # var html = md.md_to_html
2315 # assert html == "<p><strong>Hello World!</strong></p>\n"
2316 fun md_to_html: Streamable do
2317 var processor = new MarkdownProcessor
2318 return processor.process(self)