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 # Create a link with a title.
391 init with_title(link: String, title: nullable String) do
397 # A `Decorator` is used to emit mardown into a specific format.
398 # Default decorator used is `HTMLDecorator`.
401 # Render a ruler block.
402 fun add_ruler(v: MarkdownEmitter, block: BlockRuler) is abstract
404 # Render a headline block with corresponding level.
405 fun add_headline(v: MarkdownEmitter, block: BlockHeadline) is abstract
407 # Render a paragraph block.
408 fun add_paragraph(v: MarkdownEmitter, block: BlockParagraph) is abstract
410 # Render a code or fence block.
411 fun add_code(v: MarkdownEmitter, block: BlockCode) is abstract
413 # Render a blockquote.
414 fun add_blockquote(v: MarkdownEmitter, block: BlockQuote) is abstract
416 # Render an unordered list.
417 fun add_unorderedlist(v: MarkdownEmitter, block: BlockUnorderedList) is abstract
419 # Render an ordered list.
420 fun add_orderedlist(v: MarkdownEmitter, block: BlockOrderedList) is abstract
422 # Render a list item.
423 fun add_listitem(v: MarkdownEmitter, block: BlockListItem) is abstract
425 # Render an emphasis text.
426 fun add_em(v: MarkdownEmitter, text: Text) is abstract
428 # Render a strong text.
429 fun add_strong(v: MarkdownEmitter, text: Text) is abstract
431 # Render a super text.
432 fun add_super(v: MarkdownEmitter, text: Text) is abstract
435 fun add_link(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract
438 fun add_image(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract
440 # Render an abbreviation.
441 fun add_abbr(v: MarkdownEmitter, name: Text, comment: Text) is abstract
443 # Render a code span reading from a buffer.
444 fun add_span_code(v: MarkdownEmitter, buffer: Text, from, to: Int) is abstract
446 # Render a text and escape it.
447 fun append_value(v: MarkdownEmitter, value: Text) is abstract
449 # Render code text from buffer and escape it.
450 fun append_code(v: MarkdownEmitter, buffer: Text, from, to: Int) is abstract
452 # Render a character escape.
453 fun escape_char(v: MarkdownEmitter, char: Char) is abstract
455 # Render a line break
456 fun add_line_break(v: MarkdownEmitter) is abstract
458 # Generate a new html valid id from a `String`.
459 fun strip_id(txt: String): String is abstract
461 # Found headlines during the processing labeled by their ids.
462 fun headlines: ArrayMap[String, HeadLine] is abstract
465 # Class representing a markdown headline.
467 # Unique identifier of this headline.
470 # Text of the headline.
473 # Level of this headline.
475 # According toe the markdown specification, level must be in `[1..6]`.
479 # `Decorator` that outputs HTML.
483 redef var headlines = new ArrayMap[String, HeadLine]
485 redef fun add_ruler(v, block) do v.add "<hr
/>\n
"
487 redef fun add_headline(v, block) do
489 var txt = block.block.first_line.value
490 var id = strip_id(txt)
491 var lvl = block.depth
492 headlines[id] = new HeadLine(id, txt, lvl)
494 v.add "<h
{lvl} id
=\
"{id}\">"
499 redef fun add_paragraph(v, block) do
505 redef fun add_code(v, block) do
508 v.add "</code
></pre
>\n
"
511 redef fun add_blockquote(v, block) do
512 v.add "<blockquote
>\n
"
514 v.add "</blockquote
>\n
"
517 redef fun add_unorderedlist(v, block) do
523 redef fun add_orderedlist(v, block) do
529 redef fun add_listitem(v, block) do
535 redef fun add_em(v, text) do
541 redef fun add_strong(v, text) do
547 redef fun add_super(v, text) do
553 redef fun add_image(v, link, name, comment) do
555 append_value
(v
, link
)
557 append_value
(v
, name
)
559 if comment != null and not comment.is_empty then
561 append_value
(v
, comment
)
567 redef fun add_link(v, link, name, comment) do
569 append_value
(v
, link
)
571 if comment != null and not comment.is_empty then
573 append_value
(v
, comment
)
581 redef fun add_abbr(v, name, comment) do
582 v.add "<abbr title
=\
""
583 append_value
(v
, comment
)
589 redef fun add_span_code(v, text, from, to) do
591 append_code(v, text, from, to)
595 redef fun add_line_break(v) do
599 redef fun append_value(v, text) do for c in text do escape_char(v, c)
601 redef fun escape_char(v, c) do
604 else if c == '<' then
606 else if c == '>' then
608 else if c == '"' then
610 else if c == '\
'' then
617 redef fun append_code
(v
, buffer
, from
, to
) do
618 for i
in [from
..to
[ do
622 else if c
== '<' then
624 else if c
== '>' then
632 redef fun strip_id
(txt
) do
634 var b
= new FlatBuffer
639 if not c
.is_letter
and
641 not allowed_id_chars
.has
(c
) then continue
647 # check for multiple id definitions
648 if headlines
.has_key
(key
) then
651 while headlines
.has_key
(key
) do
659 private var allowed_id_chars
: Array[Char] = ['-', '_', ':', '.']
662 # A block of markdown lines.
663 # A `MDBlock` can contains lines and/or sub-blocks.
667 var kind
: Block = new BlockNone(self) is writable
670 var first_line
: nullable MDLine = null is writable
673 var last_line
: nullable MDLine = null is writable
675 # First sub-block if any.
676 var first_block
: nullable MDBlock = null is writable
678 # Last sub-block if any.
679 var last_block
: nullable MDBlock = null is writable
681 # Previous block if any.
682 var prev
: nullable MDBlock = null is writable
685 var next
: nullable MDBlock = null is writable
687 # Does this block contain subblocks?
688 fun has_blocks
: Bool do return first_block
!= null
691 fun count_blocks
: Int do
693 var block
= first_block
694 while block
!= null do
701 # Does this block contain lines?
702 fun has_lines
: Bool do return first_line
!= null
705 fun count_lines
: Int do
707 var line
= first_line
708 while line
!= null do
715 # Split `self` creating a new sub-block having `line` has `last_line`.
716 fun split
(line
: MDLine): MDBlock do
717 var block
= new MDBlock
718 block
.first_line
= first_line
719 block
.last_line
= line
720 first_line
= line
.next
722 if first_line
== null then
725 first_line
.prev
= null
727 if first_block
== null then
731 last_block
.next
= block
737 # Add a `line` to this block.
738 fun add_line
(line
: MDLine) do
739 if last_line
== null then
743 last_line
.next_empty
= line
.is_empty
744 line
.prev_empty
= last_line
.is_empty
745 line
.prev
= last_line
746 last_line
.next
= line
751 # Remove `line` from this block.
752 fun remove_line
(line
: MDLine) do
753 if line
.prev
== null then
754 first_line
= line
.next
756 line
.prev
.next
= line
.next
758 if line
.next
== null then
759 last_line
= line
.prev
761 line
.next
.prev
= line
.prev
767 # Remove leading empty lines.
768 fun remove_leading_empty_lines
: Bool do
769 var was_empty
= false
770 var line
= first_line
771 while line
!= null and line
.is_empty
do
779 # Remove trailing empty lines.
780 fun remove_trailing_empty_lines
: Bool do
781 var was_empty
= false
783 while line
!= null and line
.is_empty
do
791 # Remove leading and trailing empty lines.
792 fun remove_surrounding_empty_lines
: Bool do
793 var was_empty
= false
794 if remove_leading_empty_lines
then was_empty
= true
795 if remove_trailing_empty_lines
then was_empty
= true
799 # Remove list markers and up to 4 leading spaces.
800 # Used to clean nested lists.
801 fun remove_list_indent
(v
: MarkdownProcessor) do
802 var line
= first_line
803 while line
!= null do
804 if not line
.is_empty
then
805 var kind
= v
.line_kind
(line
)
806 if kind
isa LineList then
807 line
.value
= kind
.extract_value
(line
)
809 line
.value
= line
.value
.substring_from
(line
.leading
.min
(4))
811 line
.leading
= line
.process_leading
817 # Collect block line text.
819 var text
= new FlatBuffer
820 var line
= first_line
821 while line
!= null do
822 if not line
.is_empty
then
823 text
.append line
.text
828 return text
.write_to_string
832 # Representation of a markdown block in the AST.
833 # Each `Block` is linked to a `MDBlock` that contains mardown code.
836 # The markdown block `self` is related to.
839 # Output `self` using `v.decorator`.
840 fun emit
(v
: MarkdownEmitter) do v
.emit_in
(self)
842 # Emit the containts of `self`, lines or blocks.
843 fun emit_in
(v
: MarkdownEmitter) do
844 block
.remove_surrounding_empty_lines
845 if block
.has_lines
then
852 # Emit lines contained in `block`.
853 fun emit_lines
(v
: MarkdownEmitter) do
854 var tpl
= v
.push_buffer
855 var line
= block
.first_line
856 while line
!= null do
857 if not line
.is_empty
then
858 v
.add line
.value
.substring
(line
.leading
, line
.value
.length
- line
.trailing
)
859 if line
.trailing
>= 2 then v
.decorator
.add_line_break
(v
)
861 if line
.next
!= null then
870 # Emit sub-blocks contained in `block`.
871 fun emit_blocks
(v
: MarkdownEmitter) do
872 var block
= self.block
.first_block
873 while block
!= null do
880 # A block without any markdown specificities.
882 # Actually use the same implementation than `BlockCode`,
883 # this class is only used for typing purposes.
888 # A markdown blockquote.
892 redef fun emit
(v
) do v
.decorator
.add_blockquote
(v
, self)
894 # Remove blockquote markers.
895 private fun remove_block_quote_prefix
(block
: MDBlock) do
896 var line
= block
.first_line
897 while line
!= null do
898 if not line
.is_empty
then
899 if line
.value
[line
.leading
] == '>' then
900 var rem
= line
.leading
+ 1
901 if line
.leading
+ 1 < line
.value
.length
and
902 line
.value
[line
.leading
+ 1] == ' ' then
905 line
.value
= line
.value
.substring_from
(rem
)
906 line
.leading
= line
.process_leading
914 # A markdown code block.
918 # Number of char to skip at the beginning of the line.
920 # Block code lines start at 4 spaces.
921 protected var line_start
= 4
923 redef fun emit
(v
) do v
.decorator
.add_code
(v
, self)
925 redef fun emit_lines
(v
) do
926 var line
= block
.first_line
927 while line
!= null do
928 if not line
.is_empty
then
929 v
.decorator
.append_code
(v
, line
.value
, line_start
, line
.value
.length
)
937 # A markdown code-fence block.
939 # Actually use the same implementation than `BlockCode`,
940 # this class is only used for typing purposes.
944 # Fence code lines start at 0 spaces.
945 redef var line_start
= 0
948 # A markdown headline.
952 redef fun emit
(v
) do v
.decorator
.add_headline
(v
, self)
954 # Depth of the headline used to determine the headline level.
957 # Remove healine marks from lines contained in `self`.
958 private fun transform_headline
(block
: MDBlock) do
959 if depth
> 0 then return
961 var line
= block
.first_line
962 if line
.is_empty
then return
963 var start
= line
.leading
964 while start
< line
.value
.length
and line
.value
[start
] == '#' do
968 while start
< line
.value
.length
and line
.value
[start
] == ' ' do
971 if start
>= line
.value
.length
then
974 var nend
= line
.value
.length
- line
.trailing
- 1
975 while line
.value
[nend
] == '#' do nend
-= 1
976 while line
.value
[nend
] == ' ' do nend
-= 1
977 line
.value
= line
.value
.substring
(start
, nend
- start
+ 1)
985 # A markdown list item block.
989 redef fun emit
(v
) do v
.decorator
.add_listitem
(v
, self)
992 # A markdown list block.
993 # Can be either an ordered or unordered list, this class is mainly used to factorize code.
994 abstract class BlockList
997 # Split list block into list items sub-blocks.
998 private fun init_block
(v
: MarkdownProcessor) do
999 var line
= block
.first_line
1001 while line
!= null do
1002 var t
= v
.line_kind
(line
)
1003 if t
isa LineList or
1004 (not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
1005 not (t
isa LineList))) then
1006 var sblock
= block
.split
(line
.prev
.as(not null))
1007 sblock
.kind
= new BlockListItem(sblock
)
1011 var sblock
= block
.split
(block
.last_line
.as(not null))
1012 sblock
.kind
= new BlockListItem(sblock
)
1015 # Expand list items as paragraphs if needed.
1016 private fun expand_paragraphs
(block
: MDBlock) do
1017 var outer
= block
.first_block
1018 var inner
: nullable MDBlock
1019 var has_paragraph
= false
1020 while outer
!= null and not has_paragraph
do
1021 if outer
.kind
isa BlockListItem then
1022 inner
= outer
.first_block
1023 while inner
!= null and not has_paragraph
do
1024 if inner
.kind
isa BlockParagraph then
1025 has_paragraph
= true
1032 if has_paragraph
then
1033 outer
= block
.first_block
1034 while outer
!= null do
1035 if outer
.kind
isa BlockListItem then
1036 inner
= outer
.first_block
1037 while inner
!= null do
1038 if inner
.kind
isa BlockNone then
1039 inner
.kind
= new BlockParagraph(inner
)
1050 # A markdown ordered list.
1051 class BlockOrderedList
1054 redef fun emit
(v
) do v
.decorator
.add_orderedlist
(v
, self)
1057 # A markdown unordred list.
1058 class BlockUnorderedList
1061 redef fun emit
(v
) do v
.decorator
.add_unorderedlist
(v
, self)
1064 # A markdown paragraph block.
1065 class BlockParagraph
1068 redef fun emit
(v
) do v
.decorator
.add_paragraph
(v
, self)
1075 redef fun emit
(v
) do v
.decorator
.add_ruler
(v
, self)
1078 # Xml blocks that can be found in markdown markup.
1082 redef fun emit_lines
(v
) do
1083 var line
= block
.first_line
1084 while line
!= null do
1085 if not line
.is_empty
then v
.add line
.value
1095 # Text contained in this line.
1096 var value
: String is writable
1098 # Is this line empty?
1099 # Lines containing only spaces are considered empty.
1100 var is_empty
: Bool = true is writable
1102 # Previous line in `MDBlock` or null if first line.
1103 var prev
: nullable MDLine = null is writable
1105 # Next line in `MDBlock` or null if last line.
1106 var next
: nullable MDLine = null is writable
1108 # Is the previous line empty?
1109 var prev_empty
: Bool = false is writable
1111 # Is the next line empty?
1112 var next_empty
: Bool = false is writable
1114 # Initialize a new MDLine from its string value
1115 init(value
: String) do
1117 self.leading
= process_leading
1118 if leading
!= value
.length
then
1119 self.is_empty
= false
1120 self.trailing
= process_trailing
1124 # Set `value` as an empty String and update `leading`, `trailing` and is_`empty`.
1130 if prev
!= null then prev
.next_empty
= true
1131 if next
!= null then next
.prev_empty
= true
1134 # Number or leading spaces on this line.
1135 var leading
: Int = 0 is writable
1137 # Compute `leading` depending on `value`.
1138 fun process_leading
: Int do
1140 var value
= self.value
1141 while count
< value
.length
and value
[count
] == ' ' do count
+= 1
1142 if leading
== value
.length
then clear
1146 # Number of trailing spaces on this line.
1147 var trailing
: Int = 0 is writable
1149 # Compute `trailing` depending on `value`.
1150 fun process_trailing
: Int do
1152 var value
= self.value
1153 while value
[value
.length
- count
- 1] == ' ' do
1159 # Count the amount of `ch` in this line.
1160 # Return A value > 0 if this line only consists of `ch` end spaces.
1161 fun count_chars
(ch
: Char): Int do
1177 # Count the amount of `ch` at the start of this line ignoring spaces.
1178 fun count_chars_start
(ch
: Char): Int do
1193 # Last XML line if any.
1194 private var xml_end_line
: nullable MDLine = null
1196 # Does `value` contains valid XML markup?
1197 private fun check_html
: Bool do
1198 var tags
= new Array[String]
1199 var tmp
= new FlatBuffer
1201 if pos
+ 1 < value
.length
and value
[pos
+ 1] == '!' then
1202 if read_xml_comment
(self, pos
) > 0 then return true
1204 pos
= value
.read_xml
(tmp
, pos
, false)
1208 if not tag
.is_html_block
then
1216 var line
: nullable MDLine = self
1217 while line
!= null do
1218 while pos
< line
.value
.length
and line
.value
[pos
] != '<' do
1221 if pos
>= line
.value
.length
then
1222 if pos
- 2 >= 0 and line
.value
[pos
- 2] == '/' then
1224 if tags
.is_empty
then
1232 tmp
= new FlatBuffer
1233 var new_pos
= line
.value
.read_xml
(tmp
, pos
, false)
1236 if tag
.is_html_block
and not tag
== "hr" then
1237 if tmp
[1] == '/' then
1238 if tags
.last
!= tag
then
1246 if tags
.is_empty
then
1256 return tags
.is_empty
1261 # Read a XML comment.
1262 # Used by `check_html`.
1263 private fun read_xml_comment
(first_line
: MDLine, start
: Int): Int do
1264 var line
: nullable MDLine = first_line
1265 if start
+ 3 < line
.value
.length
then
1266 if line
.value
[2] == '-' and line
.value
[3] == '-' then
1268 while line
!= null do
1269 while pos
< line
.value
.length
and line
.value
[pos
] != '-' do
1272 if pos
== line
.value
.length
then
1276 if pos
+ 2 < line
.value
.length
then
1277 if line
.value
[pos
+ 1] == '-' and line
.value
[pos
+ 2] == '>' then
1278 first_line
.xml_end_line
= line
1290 # Extract the text of `self` without leading and trailing.
1291 fun text
: String do return value
.substring
(leading
, value
.length
- trailing
)
1298 # See `MarkdownProcessor::recurse`.
1299 fun process
(v
: MarkdownProcessor) is abstract
1302 # An empty markdown line.
1306 redef fun process
(v
) do
1307 v
.current_line
= v
.current_line
.next
1311 # A non-specific markdown construction.
1312 # Mainly used as part of another line construct such as paragraphs or lists.
1316 redef fun process
(v
) do
1317 var line
= v
.current_line
1319 var was_empty
= line
.prev_empty
1320 while line
!= null and not line
.is_empty
do
1321 var t
= v
.line_kind
(line
)
1322 if v
.in_list
and t
isa LineList then
1325 if t
isa LineCode or t
isa LineFence then
1328 if t
isa LineHeadline or t
isa LineHeadline1 or t
isa LineHeadline2 or
1329 t
isa LineHR or t
isa LineBlockquote or t
isa LineXML then
1335 if line
!= null and not line
.is_empty
then
1336 var block
= v
.current_block
.split
(line
.prev
.as(not null))
1337 if v
.in_list
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
1345 if line
!= null then
1346 block
= v
.current_block
.split
(line
)
1348 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1350 if v
.in_list
and (line
== null or not line
.is_empty
) and not was_empty
then
1351 block
.kind
= new BlockNone(block
)
1353 block
.kind
= new BlockParagraph(block
)
1355 v
.current_block
.remove_leading_empty_lines
1357 v
.current_line
= v
.current_block
.first_line
1361 # A line of markdown code.
1365 redef fun process
(v
) do
1366 var line
= v
.current_line
1368 while line
!= null and (line
.is_empty
or v
.line_kind
(line
) isa LineCode) do
1371 # split at block end line
1373 if line
!= null then
1374 block
= v
.current_block
.split
(line
.prev
.as(not null))
1376 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1378 block
.kind
= new BlockCode(block
)
1379 block
.remove_surrounding_empty_lines
1380 v
.current_line
= v
.current_block
.first_line
1384 # A line of raw XML.
1388 redef fun process
(v
) do
1389 var line
= v
.current_line
1390 var prev
= line
.prev
1391 if prev
!= null then v
.current_block
.split
(prev
)
1392 var block
= v
.current_block
.split
(line
.xml_end_line
.as(not null))
1393 block
.kind
= new BlockXML(block
)
1394 v
.current_block
.remove_leading_empty_lines
1395 v
.current_line
= v
.current_block
.first_line
1399 # A markdown blockquote line.
1400 class LineBlockquote
1403 redef fun process
(v
) do
1404 var line
= v
.current_line
1406 while line
!= null do
1407 if not line
.is_empty
and (line
.prev_empty
and
1408 line
.leading
== 0 and
1409 not v
.line_kind
(line
) isa LineBlockquote) then break
1414 if line
!= null then
1415 block
= v
.current_block
.split
(line
.prev
.as(not null))
1417 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1419 var kind
= new BlockQuote(block
)
1421 block
.remove_surrounding_empty_lines
1422 kind
.remove_block_quote_prefix
(block
)
1423 v
.current_line
= line
1424 v
.recurse
(block
, false)
1425 v
.current_line
= v
.current_block
.first_line
1429 # A markdown ruler line.
1433 redef fun process
(v
) do
1434 var line
= v
.current_line
1435 if line
.prev
!= null then v
.current_block
.split
(line
.prev
.as(not null))
1436 var block
= v
.current_block
.split
(line
.as(not null))
1437 block
.kind
= new BlockRuler(block
)
1438 v
.current_block
.remove_leading_empty_lines
1439 v
.current_line
= v
.current_block
.first_line
1443 # A markdown fence code line.
1447 redef fun process
(v
) do
1449 var line
= v
.current_line
.next
1450 while line
!= null do
1451 if v
.line_kind
(line
) isa LineFence then break
1454 if line
!= null then
1459 if line
!= null then
1460 block
= v
.current_block
.split
(line
.prev
.as(not null))
1462 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1464 block
.kind
= new BlockFence(block
)
1465 block
.first_line
.clear
1466 var last
= block
.last_line
1467 if last
!= null and v
.line_kind
(last
) isa LineFence then
1468 block
.last_line
.clear
1470 block
.remove_surrounding_empty_lines
1471 v
.current_line
= line
1475 # A markdown headline.
1479 redef fun process
(v
) do
1480 var line
= v
.current_line
1481 var lprev
= line
.prev
1482 if lprev
!= null then v
.current_block
.split
(lprev
)
1483 var block
= v
.current_block
.split
(line
.as(not null))
1484 var kind
= new BlockHeadline(block
)
1486 kind
.transform_headline
(block
)
1487 v
.current_block
.remove_leading_empty_lines
1488 v
.current_line
= v
.current_block
.first_line
1492 # A markdown headline of level 1.
1496 redef fun process
(v
) do
1497 var line
= v
.current_line
1498 var lprev
= line
.prev
1499 if lprev
!= null then v
.current_block
.split
(lprev
)
1501 var block
= v
.current_block
.split
(line
.as(not null))
1502 var kind
= new BlockHeadline(block
)
1504 kind
.transform_headline
(block
)
1506 v
.current_block
.remove_leading_empty_lines
1507 v
.current_line
= v
.current_block
.first_line
1511 # A markdown headline of level 2.
1515 redef fun process
(v
) do
1516 var line
= v
.current_line
1517 var lprev
= line
.prev
1518 if lprev
!= null then v
.current_block
.split
(lprev
)
1520 var block
= v
.current_block
.split
(line
.as(not null))
1521 var kind
= new BlockHeadline(block
)
1523 kind
.transform_headline
(block
)
1525 v
.current_block
.remove_leading_empty_lines
1526 v
.current_line
= v
.current_block
.first_line
1530 # A markdown list line.
1531 # Mainly used to factorize code between ordered and unordered lists.
1535 redef fun process
(v
) do
1536 var line
= v
.current_line
1538 while line
!= null do
1539 var t
= v
.line_kind
(line
)
1540 if not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
1541 not t
isa LineList) then break
1546 if line
!= null then
1547 list
= v
.current_block
.split
(line
.prev
.as(not null))
1549 list
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1551 var kind
= block_kind
(list
)
1553 list
.first_line
.prev_empty
= false
1554 list
.last_line
.next_empty
= false
1555 list
.remove_surrounding_empty_lines
1556 list
.first_line
.prev_empty
= false
1557 list
.last_line
.next_empty
= false
1559 var block
= list
.first_block
1560 while block
!= null do
1561 block
.remove_list_indent
(v
)
1562 v
.recurse
(block
, true)
1565 kind
.expand_paragraphs
(list
)
1566 v
.current_line
= line
1569 # Create a new block kind based on this line.
1570 protected fun block_kind
(block
: MDBlock): BlockList is abstract
1572 # Extract string value from `MDLine`.
1573 protected fun extract_value
(line
: MDLine): String is abstract
1576 # An ordered list line.
1580 redef fun block_kind
(block
) do return new BlockOrderedList(block
)
1582 redef fun extract_value
(line
) do
1583 return line
.value
.substring_from
(line
.value
.index_of
('.') + 2)
1587 # An unordered list line.
1591 redef fun block_kind
(block
) do return new BlockUnorderedList(block
)
1593 redef fun extract_value
(line
) do
1594 return line
.value
.substring_from
(line
.leading
+ 2)
1598 # A token represent a character in the markdown input.
1599 # Some tokens have a specific markup behaviour that is handled here.
1600 abstract class Token
1602 # Position of `self` in markdown input.
1605 # Character found at `pos` in the markdown input.
1608 # Output that token using `MarkdownEmitter::decorator`.
1609 fun emit
(v
: MarkdownEmitter) do v
.addc char
1612 # A token without a specific meaning.
1617 # An emphasis token.
1618 abstract class TokenEm
1621 redef fun emit
(v
) do
1622 var tmp
= v
.push_buffer
1623 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1626 v
.decorator
.add_em
(v
, tmp
)
1634 # An emphasis star token.
1639 # An emphasis underscore token.
1640 class TokenEmUnderscore
1645 abstract class TokenStrong
1648 redef fun emit
(v
) do
1649 var tmp
= v
.push_buffer
1650 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 2, self)
1653 v
.decorator
.add_strong
(v
, tmp
)
1654 v
.current_pos
= b
+ 1
1661 # A strong star token.
1662 class TokenStrongStar
1666 # A strong underscore token.
1667 class TokenStrongUnderscore
1672 # This class is mainly used to factorize work between single and double quoted span codes.
1673 abstract class TokenCode
1676 redef fun emit
(v
) do
1677 var a
= pos
+ next_pos
+ 1
1678 var b
= v
.current_text
.find_token
(a
, self)
1680 v
.current_pos
= b
+ next_pos
1681 while a
< b
and v
.current_text
[a
] == ' ' do a
+= 1
1683 while v
.current_text
[b
- 1] == ' ' do b
-= 1
1684 v
.decorator
.add_span_code
(v
, v
.current_text
.as(not null), a
, b
)
1691 private fun next_pos
: Int is abstract
1694 # A span code token.
1695 class TokenCodeSingle
1698 redef fun next_pos
do return 0
1701 # A doubled span code token.
1702 class TokenCodeDouble
1705 redef fun next_pos
do return 1
1708 # A link or image token.
1709 # This class is mainly used to factorize work between images and links.
1710 abstract class TokenLinkOrImage
1714 var link
: nullable Text = null
1717 var name
: nullable Text = null
1720 var comment
: nullable Text = null
1722 # Is the link construct an abbreviation?
1723 var is_abbrev
= false
1725 redef fun emit
(v
) do
1726 var tmp
= new FlatBuffer
1727 var b
= check_link
(v
, tmp
, pos
, self)
1736 # Emit the hyperlink as link or image.
1737 private fun emit_hyper
(v
: MarkdownEmitter) is abstract
1739 # Check if the link is a valid link.
1740 private fun check_link
(v
: MarkdownEmitter, out
: FlatBuffer, start
: Int, token
: Token): Int do
1741 var md
= v
.current_text
1743 if token
isa TokenLink then
1748 var tmp
= new FlatBuffer
1749 pos
= md
.read_md_link_id
(tmp
, pos
)
1750 if pos
< start
then return -1
1754 pos
= md
.skip_spaces
(pos
)
1756 var tid
= name
.write_to_string
.to_lower
1757 if v
.processor
.link_refs
.has_key
(tid
) then
1758 var lr
= v
.processor
.link_refs
[tid
]
1759 is_abbrev
= lr
.is_abbrev
1766 else if md
[pos
] == '(' then
1768 pos
= md
.skip_spaces
(pos
)
1769 if pos
< start
then return -1
1770 tmp
= new FlatBuffer
1771 var use_lt
= md
[pos
] == '<'
1773 pos
= md
.read_until
(tmp
, pos
+ 1, '>')
1775 pos
= md
.read_md_link
(tmp
, pos
)
1777 if pos
< start
then return -1
1778 if use_lt
then pos
+= 1
1779 link
= tmp
.write_to_string
1780 if md
[pos
] == ' ' then
1781 pos
= md
.skip_spaces
(pos
)
1782 if pos
> start
and md
[pos
] == '"' then
1784 tmp
= new FlatBuffer
1785 pos
= md
.read_until
(tmp
, pos
, '"')
1786 if pos
< start
then return -1
1787 comment
= tmp
.write_to_string
1789 pos
= md
.skip_spaces
(pos
)
1790 if pos
== -1 then return -1
1793 if md
[pos
] != ')' then return -1
1794 else if md
[pos
] == '[' then
1796 tmp
= new FlatBuffer
1797 pos
= md
.read_raw_until
(tmp
, pos
, ']')
1798 if pos
< start
then return -1
1800 if tmp
.length
> 0 then
1805 var tid
= id
.write_to_string
.to_lower
1806 if v
.processor
.link_refs
.has_key
(tid
) then
1807 var lr
= v
.processor
.link_refs
[tid
]
1812 var tid
= name
.write_to_string
.replace
("\n", " ").to_lower
1813 if v
.processor
.link_refs
.has_key
(tid
) then
1814 var lr
= v
.processor
.link_refs
[tid
]
1822 if link
== null then return -1
1827 # A markdown link token.
1829 super TokenLinkOrImage
1831 redef fun emit_hyper
(v
) do
1832 if is_abbrev
and comment
!= null then
1833 v
.decorator
.add_abbr
(v
, name
.as(not null), comment
.as(not null))
1835 v
.decorator
.add_link
(v
, link
.as(not null), name
.as(not null), comment
)
1840 # A markdown image token.
1842 super TokenLinkOrImage
1844 redef fun emit_hyper
(v
) do
1845 v
.decorator
.add_image
(v
, link
.as(not null), name
.as(not null), comment
)
1853 redef fun emit
(v
) do
1854 var tmp
= new FlatBuffer
1855 var b
= check_html
(v
, tmp
, v
.current_text
.as(not null), v
.current_pos
)
1860 v
.decorator
.escape_char
(v
, char
)
1864 # Is the HTML valid?
1865 # Also take care of link and mailto shortcuts.
1866 private fun check_html
(v
: MarkdownEmitter, out
: FlatBuffer, md
: Text, start
: Int): Int do
1867 # check for auto links
1868 var tmp
= new FlatBuffer
1869 var pos
= md
.read_until
(tmp
, start
+ 1, ':', ' ', '>', '\n')
1870 if pos
!= -1 and md
[pos
] == ':' and tmp
.is_link_prefix
then
1871 pos
= md
.read_until
(tmp
, pos
, '>')
1873 var link
= tmp
.write_to_string
1874 v
.decorator
.add_link
(v
, link
, link
, null)
1878 # TODO check for mailto
1879 # check for inline html
1880 if start
+ 2 < md
.length
then
1881 return md
.read_xml
(out
, start
, true)
1887 # An HTML entity token.
1891 redef fun emit
(v
) do
1892 var tmp
= new FlatBuffer
1893 var b
= check_entity
(tmp
, v
.current_text
.as(not null), pos
)
1898 v
.decorator
.escape_char
(v
, char
)
1902 # Is the entity valid?
1903 private fun check_entity
(out
: FlatBuffer, md
: Text, start
: Int): Int do
1904 var pos
= md
.read_until
(out
, start
, ';')
1905 if pos
< 0 or out
.length
< 3 then
1908 if out
[1] == '#' then
1909 if out
[2] == 'x' or out
[2] == 'X' then
1910 if out
.length
< 4 then return -1
1911 for i
in [3..out
.length
[ do
1913 if (c
< '0' or c
> '9') and (c
< 'a' and c
> 'f') and (c
< 'A' and c
> 'F') then
1918 for i
in [2..out
.length
[ do
1920 if c
< '0' or c
> '9' then return -1
1925 for i
in [1..out
.length
[ do
1927 if not c
.is_digit
and not c
.is_letter
then return -1
1930 # TODO check entity is valid
1931 # if out.is_entity then
1941 # A markdown escape token.
1945 redef fun emit
(v
) do
1947 v
.addc v
.current_text
[v
.current_pos
]
1951 # A markdown super token.
1955 redef fun emit
(v
) do
1956 var tmp
= v
.push_buffer
1957 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1960 v
.decorator
.add_super
(v
, tmp
)
1970 # Get the token kind at `pos`.
1971 private fun token_at
(pos
: Int): Token do
1983 if pos
+ 1 < length
then
1988 if pos
+ 2 < length
then
1996 if c0
!= ' ' or c2
!= ' ' then
1997 return new TokenStrongStar(pos
, c
)
1999 return new TokenEmStar(pos
, c
)
2002 if c0
!= ' ' or c1
!= ' ' then
2003 return new TokenEmStar(pos
, c
)
2005 return new TokenNone(pos
, c
)
2007 else if c
== '_' then
2009 if c0
!= ' ' or c2
!= ' 'then
2010 return new TokenStrongUnderscore(pos
, c
)
2012 return new TokenEmUnderscore(pos
, c
)
2015 if c0
!= ' ' or c1
!= ' ' then
2016 return new TokenEmUnderscore(pos
, c
)
2018 return new TokenNone(pos
, c
)
2020 else if c
== '!' then
2021 if c1
== '[' then return new TokenImage(pos
, c
)
2022 return new TokenNone(pos
, c
)
2023 else if c
== '[' then
2024 return new TokenLink(pos
, c
)
2025 else if c
== ']' then
2026 return new TokenNone(pos
, c
)
2027 else if c
== '`' then
2029 return new TokenCodeDouble(pos
, c
)
2031 return new TokenCodeSingle(pos
, c
)
2033 else if c
== '\\' then
2034 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
2035 return new TokenEscape(pos, c)
2037 return new TokenNone(pos, c)
2039 else if c == '<' then
2040 return new TokenHTML(pos, c)
2041 else if c == '&' then
2042 return new TokenEntity(pos, c)
2043 else if c == '^' then
2044 if c0 == '^' or c1 == '^' then
2045 return new TokenNone(pos, c)
2047 return new TokenSuper(pos, c)
2050 return new TokenNone(pos, c)
2054 # Find the position of a `token
` in `self`.
2055 private fun find_token(start: Int, token: Token): Int do
2057 while pos < length do
2058 if token_at(pos).is_same_type(token) then
2066 # Get the position of the next non-space character.
2067 private fun skip_spaces(start: Int): Int do
2069 while pos > -1 and pos < length and (self[pos] == ' ' or self[pos] == '\n') do
2072 if pos < length then return pos
2076 # Read `self` until `nend
` and append it to the `out
` buffer.
2077 # Escape markdown special chars.
2078 private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do
2080 while pos < length do
2082 if c == '\\' and pos + 1 < length then
2083 pos = escape(out, self[pos + 1], pos)
2085 var end_reached = false
2092 if end_reached then break
2097 if pos == length then return -1
2101 # Read `self` as raw text until `nend
` and append it to the `out
` buffer.
2102 # No escape is made.
2103 private fun read_raw_until(out: FlatBuffer, start: Int, nend: Char...): Int do
2105 while pos < length do
2107 var end_reached = false
2114 if end_reached then break
2118 if pos == length then return -1
2122 # Read `self` as XML until `to
` and append it to the `out
` buffer.
2123 # Escape HTML special chars.
2124 private fun read_xml_until(out: FlatBuffer, from: Int, to: Char...): Int do
2127 var str_char: nullable Char = null
2128 while pos < length do
2134 if pos < length then
2140 if c == str_char then
2147 if c == '"' or c == '\'' then
2152 var end_reached = false
2153 for n in [0..to.length[ do
2159 if end_reached then break
2164 if pos == length then return -1
2168 # Read `self` as XML and append it to the `out
` buffer.
2169 # Safe mode can be activated to limit reading to valid xml.
2170 private fun read_xml(out: FlatBuffer, start: Int, safe_mode: Bool): Int do
2172 var is_close_tag = false
2173 if start + 1 >= length then return -1
2174 if self[start + 1] == '/' then
2177 else if self[start + 1] == '!' then
2181 is_close_tag = false
2185 var tmp = new FlatBuffer
2186 pos = read_xml_until(tmp, pos, ' ', '/', '>')
2187 if pos == -1 then return -1
2188 var tag = tmp.write_to_string.trim.to_lower
2189 if tag.is_html_unsafe then
2191 if is_close_tag then out.add '/'
2195 if is_close_tag then out.add '/'
2200 if is_close_tag then out.add '/'
2201 pos = read_xml_until(out, pos, ' ', '/', '>')
2203 if pos == -1 then return -1
2204 pos = read_xml_until(out, pos, '/', '>')
2205 if pos == -1 then return -1
2206 if self[pos] == '/' then
2208 pos = self.read_xml_until(out, pos + 1, '>')
2209 if pos == -1 then return -1
2211 if self[pos] == '>' then
2218 # Read a markdown link address and append it to the `out
` buffer.
2219 private fun read_md_link(out: FlatBuffer, start: Int): Int do
2222 while pos < length do
2224 if c == '\\' and pos + 1 < length then
2225 pos = escape(out, self[pos + 1], pos)
2227 var end_reached = false
2230 else if c == ' ' then
2231 if counter == 1 then end_reached = true
2232 else if c == ')' then
2234 if counter == 0 then end_reached = true
2236 if end_reached then break
2241 if pos == length then return -1
2245 # Read a markdown link text and append it to the `out
` buffer.
2246 private fun read_md_link_id(out: FlatBuffer, start: Int): Int do
2249 while pos < length do
2251 var end_reached = false
2255 else if c == ']' then
2257 if counter == 0 then
2265 if end_reached then break
2268 if pos == length then return -1
2272 # Extract the XML tag name from a XML tag.
2273 private fun xml_tag: String do
2274 var tpl = new FlatBuffer
2276 if pos < length and self[1] == '/' then pos += 1
2277 while pos < length - 1 and (self[pos].is_digit or self[pos].is_letter) do
2281 return tpl.write_to_string.to_lower
2284 # Read and escape the markdown contained in `self`.
2285 private fun escape(out: FlatBuffer, c: Char, pos: Int): Int do
2286 if c == '\\' or c == '[' or c == ']' or c == '(' or c == ')' or c == '{' or
2287 c == '}' or c == '#' or c == '"' or c == '\'' or c == '.' or c == '<' or
2288 c == '>' or c == '*' or c == '+' or c == '-' or c == '_' or c == '!' or
2289 c == '`' or c == '~
' or c == '^
' then
2297 # Is `self` an unsafe HTML element?
2298 private fun is_html_unsafe: Bool do return html_unsafe_tags.has(self.write_to_string)
2300 # Is `self` a HRML block element?
2301 private fun is_html_block: Bool do return html_block_tags.has(self.write_to_string)
2303 # Is `self` a link prefix?
2304 private fun is_link_prefix: Bool do return html_link_prefixes.has(self.write_to_string)
2306 private fun html_unsafe_tags: Array[String] do return once ["applet", "head", "body", "frame", "frameset", "iframe", "script", "object"]
2308 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"]
2310 private fun html_link_prefixes: Array[String] do return once ["http", "https", "ftp", "ftps"]
2315 # Parse `self` as markdown and return the HTML representation
2317 # var md = "**Hello World!**"
2318 # var html = md.md_to_html
2319 # assert html == "<p><strong>Hello World!</strong></p>\n"
2320 fun md_to_html: Streamable do
2321 var processor = new MarkdownProcessor
2322 return processor.process(self)