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 # `MarkdownEmitter` used for ouput.
34 var emitter
: MarkdownEmitter is noinit
36 init do self.emitter
= new MarkdownEmitter(self)
38 # Process the mardown `input` string and return the processed output.
39 fun process
(input
: String): Streamable do
46 var parent
= read_lines
(input
)
47 parent
.remove_surrounding_empty_lines
48 recurse
(parent
, false)
49 # output processed text
50 return emitter
.emit
(parent
.kind
)
53 # Split `input` string into `MDLines` and create a parent `MDBlock` with it.
54 private fun read_lines
(input
: String): MDBlock do
55 var block
= new MDBlock
56 var value
= new FlatBuffer
58 while i
< input
.length
do
62 while not eol
and i
< input
.length
do
67 else if c
== '\t' then
68 var np
= pos
+ (4 - (pos
.bin_and
(3)))
81 var line
= new MDLine(value
.write_to_string
)
82 var is_link_ref
= check_link_ref
(line
)
84 if not is_link_ref
then block
.add_line line
89 # Check if line is a block link definition.
90 # Return `true` if line contains a valid link ref and save it into `link_refs`.
91 private fun check_link_ref
(line
: MDLine): Bool do
93 var is_link_ref
= false
94 var id
= new FlatBuffer
95 var link
= new FlatBuffer
96 var comment
= new FlatBuffer
98 if not line
.is_empty
and line
.leading
< 4 and line
.value
[line
.leading
] == '[' then
99 pos
= line
.leading
+ 1
100 pos
= md
.read_until
(id
, pos
, ']')
101 if not id
.is_empty
and pos
+ 2 < line
.value
.length
then
102 if line
.value
[pos
+ 1] == ':' then
104 pos
= md
.skip_spaces
(pos
)
105 if line
.value
[pos
] == '<' then
107 pos
= md
.read_until
(link
, pos
, '>')
110 pos
= md
.read_until
(link
, pos
, ' ', '\n')
112 if not link
.is_empty
then
113 pos
= md
.skip_spaces
(pos
)
114 if pos
> 0 and pos
< line
.value
.length
then
115 var c
= line
.value
[pos
]
116 if c
== '\"' or c
== '\'' or c == '(' then
119 pos = md.read_until(comment, pos, ')')
121 pos = md.read_until(comment, pos, c)
123 if pos > 0 then is_link_ref = true
132 if is_link_ref and not id.is_empty and not link.is_empty then
133 var lr = new LinkRef.with_title(link.write_to_string, comment.write_to_string)
134 add_link_ref(id.write_to_string, lr)
135 if comment.is_empty then last_link_ref = lr
138 comment = new FlatBuffer
139 if not line.is_empty and last_link_ref != null then
141 var c = line.value[pos]
142 if c == '\
"' or c == '\'' or c == '(' then
145 pos = md.read_until(comment, pos, ')')
147 pos = md.read_until(comment, pos, c)
150 if not comment.is_empty then last_link_ref.title = comment.write_to_string
152 if comment.is_empty then return false
158 # This list will be needed during output to expand links.
159 var link_refs: Map[String, LinkRef] = new HashMap[String, LinkRef]
161 # Last encountered link ref (for multiline definitions)
163 # Markdown allows link refs to be defined over two lines:
165 # [id]: http://example.com/longish/path/to/resource/here
166 # "Optional Title Here"
168 private var last_link_ref: nullable LinkRef = null
170 # Add a link ref to the list
171 fun add_link_ref(key: String, ref: LinkRef) do link_refs[key.to_lower] = ref
173 # Recursively split a `block`.
175 # The block is splitted according to the type of lines it contains.
176 # Some blocks can be splited again recursively like lists.
177 # The `in_list` mode is used to recurse on list and build
178 # nested paragraphs or code blocks.
179 fun recurse(root: MDBlock, in_list: Bool) do
180 var old_mode = self.in_list
181 var old_root = self.current_block
182 self.in_list = in_list
184 var line = root.first_line
185 while line != null and line.is_empty do
187 if line == null then return
192 while current_line != null do
193 line_kind(current_line.as(not null)).process(self)
195 self.in_list = old_mode
196 self.current_block = old_root
199 # Currently processed line.
200 # Used when visiting blocks with `recurse`.
201 var current_line: nullable MDLine = null is writable
203 # Currently processed block.
204 # Used when visiting blocks with `recurse`.
205 var current_block: nullable MDBlock = null is writable
207 # Is the current recursion in list mode?
208 # Used when visiting blocks with `recurse`
209 private var in_list = false
213 fun line_kind(md: MDLine): Line do
215 var leading = md.leading
216 var trailing = md.trailing
217 if md.is_empty then return new LineEmpty
218 if md.leading > 3 then return new LineCode
219 if value[leading] == '#' then return new LineHeadline
220 if value[leading] == '>' then return new LineBlockquote
222 if value.length - leading - trailing > 2 then
223 if value[leading] == '`' and md.count_chars_start('`') >= 3 then
226 if value[leading] == '~' and md.count_chars_start('~') >= 3 then
231 if value.length - leading - trailing > 2 and
232 (value[leading] == '*' or value[leading] == '-' or value[leading] == '_') then
233 if md.count_chars(value[leading]) >= 3 then
238 if value.length - leading >= 2 and value[leading + 1] == ' ' then
239 var c = value[leading]
240 if c == '*' or c == '-' or c == '+' then return new LineUList
243 if value.length - leading >= 3 and value[leading].is_digit then
245 while i < value.length and value[i].is_digit do i += 1
246 if i + 1 < value.length and value[i] == '.' and value[i + 1] == ' ' then
251 if value[leading] == '<' and md.check_html then return new LineXML
254 if next != null and not next.is_empty then
255 if next.count_chars('=') > 0 then
256 return new LineHeadline1
258 if next.count_chars('-') > 0 then
259 return new LineHeadline2
265 # Get the token kind at `pos`.
266 fun token_at(text: Text, pos: Int): Token do
278 if pos + 1 < text.length then
283 if pos + 2 < text.length then
291 if c0 != ' ' or c2 != ' ' then
292 return new TokenStrongStar(pos, c)
294 return new TokenEmStar(pos, c)
297 if c0 != ' ' or c1 != ' ' then
298 return new TokenEmStar(pos, c)
300 return new TokenNone(pos, c)
302 else if c == '_' then
304 if c0 != ' ' or c2 != ' 'then
305 return new TokenStrongUnderscore(pos, c)
307 return new TokenEmUnderscore(pos, c)
310 if c0 != ' ' or c1 != ' ' then
311 return new TokenEmUnderscore(pos, c)
313 return new TokenNone(pos, c)
315 else if c == '!' then
316 if c1 == '[' then return new TokenImage(pos, c)
317 return new TokenNone(pos, c)
318 else if c == '[' then
319 return new TokenLink(pos, c)
320 else if c == ']' then
321 return new TokenNone(pos, c)
322 else if c == '`' then
324 return new TokenCodeDouble(pos, c)
326 return new TokenCodeSingle(pos, c)
328 else if c == '\\' then
329 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
330 return new TokenEscape(pos
, c
)
332 return new TokenNone(pos
, c
)
334 else if c
== '<' then
335 return new TokenHTML(pos
, c
)
336 else if c
== '&' then
337 return new TokenEntity(pos
, c
)
338 else if c
== '^' then
339 if c0
== '^' or c1
== '^' then
340 return new TokenNone(pos
, c
)
342 return new TokenSuper(pos
, c
)
345 return new TokenNone(pos
, c
)
349 # Find the position of a `token` in `self`.
350 fun find_token
(text
: Text, start
: Int, token
: Token): Int do
352 while pos
< text
.length
do
353 if token_at
(text
, pos
).is_same_type
(token
) then
362 # Emit output corresponding to blocks content.
364 # Blocks are created by a previous pass in `MarkdownProcessor`.
365 # The emitter use a `Decorator` to select the output format.
366 class MarkdownEmitter
368 # Processor containing link refs.
369 var processor
: MarkdownProcessor
371 # Decorator used for output.
372 # Default is `HTMLDecorator`
373 var decorator
: Decorator = new HTMLDecorator is writable
375 # Create a new `MarkdownEmitter` using a custom `decorator`.
376 init with_decorator
(processor
: MarkdownProcessor, decorator
: Decorator) do
378 self.decorator
= decorator
381 # Output `block` using `decorator` in the current buffer.
382 fun emit
(block
: Block): Text do
383 var buffer
= push_buffer
389 # Output the content of `block`.
390 fun emit_in
(block
: Block) do block
.emit_in
(self)
392 # Transform and emit mardown text
393 fun emit_text
(text
: Text) do
394 emit_text_until
(text
, 0, null)
397 # Transform and emit mardown text starting at `from` and
398 # until a token with the same type as `token` is found.
399 # Go until the end of text if `token` is null.
400 fun emit_text_until
(text
: Text, start
: Int, token
: nullable Token): Int do
401 var old_text
= current_text
402 var old_pos
= current_pos
405 while current_pos
< text
.length
do
406 var mt
= processor
.token_at
(text
, current_pos
)
407 if (token
!= null and not token
isa TokenNone) and
408 (mt
.is_same_type
(token
) or
409 (token
isa TokenEmStar and mt
isa TokenStrongStar) or
410 (token
isa TokenEmUnderscore and mt
isa TokenStrongUnderscore)) then
416 current_text
= old_text
417 current_pos
= old_pos
421 # Currently processed position in `current_text`.
422 # Used when visiting inline production with `emit_text_until`.
423 private var current_pos
: Int = -1
425 # Currently processed text.
426 # Used when visiting inline production with `emit_text_until`.
427 private var current_text
: nullable Text = null
430 private var buffer_stack
= new List[FlatBuffer]
432 # Push a new buffer on the stack.
433 private fun push_buffer
: FlatBuffer do
434 var buffer
= new FlatBuffer
435 buffer_stack
.add buffer
439 # Pop the last buffer.
440 private fun pop_buffer
do buffer_stack
.pop
442 # Current output buffer.
443 private fun current_buffer
: FlatBuffer do
444 assert not buffer_stack
.is_empty
445 return buffer_stack
.last
448 # Append `e` to current buffer.
449 fun add
(e
: Streamable) do
451 current_buffer
.append e
453 current_buffer
.append e
.write_to_string
457 # Append `c` to current buffer.
458 fun addc
(c
: Char) do current_buffer
.add c
460 # Append a "\n" line break.
461 fun addn
do current_buffer
.add
'\n'
465 # Links that are specified somewhere in the mardown document to be reused as shortcuts.
469 # [1]: http://example.com/ "Optional title"
475 # Optional link title
476 var title
: nullable String = null
478 # Is the link an abreviation?
479 var is_abbrev
= false
481 # Create a link with a title.
482 init with_title
(link
: String, title
: nullable String) do
488 # A `Decorator` is used to emit mardown into a specific format.
489 # Default decorator used is `HTMLDecorator`.
492 # Render a ruler block.
493 fun add_ruler
(v
: MarkdownEmitter, block
: BlockRuler) is abstract
495 # Render a headline block with corresponding level.
496 fun add_headline
(v
: MarkdownEmitter, block
: BlockHeadline) is abstract
498 # Render a paragraph block.
499 fun add_paragraph
(v
: MarkdownEmitter, block
: BlockParagraph) is abstract
501 # Render a code or fence block.
502 fun add_code
(v
: MarkdownEmitter, block
: BlockCode) is abstract
504 # Render a blockquote.
505 fun add_blockquote
(v
: MarkdownEmitter, block
: BlockQuote) is abstract
507 # Render an unordered list.
508 fun add_unorderedlist
(v
: MarkdownEmitter, block
: BlockUnorderedList) is abstract
510 # Render an ordered list.
511 fun add_orderedlist
(v
: MarkdownEmitter, block
: BlockOrderedList) is abstract
513 # Render a list item.
514 fun add_listitem
(v
: MarkdownEmitter, block
: BlockListItem) is abstract
516 # Render an emphasis text.
517 fun add_em
(v
: MarkdownEmitter, text
: Text) is abstract
519 # Render a strong text.
520 fun add_strong
(v
: MarkdownEmitter, text
: Text) is abstract
522 # Render a super text.
523 fun add_super
(v
: MarkdownEmitter, text
: Text) is abstract
526 fun add_link
(v
: MarkdownEmitter, link
: Text, name
: Text, comment
: nullable Text) is abstract
529 fun add_image
(v
: MarkdownEmitter, link
: Text, name
: Text, comment
: nullable Text) is abstract
531 # Render an abbreviation.
532 fun add_abbr
(v
: MarkdownEmitter, name
: Text, comment
: Text) is abstract
534 # Render a code span reading from a buffer.
535 fun add_span_code
(v
: MarkdownEmitter, buffer
: Text, from
, to
: Int) is abstract
537 # Render a text and escape it.
538 fun append_value
(v
: MarkdownEmitter, value
: Text) is abstract
540 # Render code text from buffer and escape it.
541 fun append_code
(v
: MarkdownEmitter, buffer
: Text, from
, to
: Int) is abstract
543 # Render a character escape.
544 fun escape_char
(v
: MarkdownEmitter, char
: Char) is abstract
546 # Render a line break
547 fun add_line_break
(v
: MarkdownEmitter) is abstract
549 # Generate a new html valid id from a `String`.
550 fun strip_id
(txt
: String): String is abstract
552 # Found headlines during the processing labeled by their ids.
553 fun headlines
: ArrayMap[String, HeadLine] is abstract
556 # Class representing a markdown headline.
558 # Unique identifier of this headline.
561 # Text of the headline.
564 # Level of this headline.
566 # According toe the markdown specification, level must be in `[1..6]`.
570 # `Decorator` that outputs HTML.
574 redef var headlines
= new ArrayMap[String, HeadLine]
576 redef fun add_ruler
(v
, block
) do v
.add
"<hr/>\n"
578 redef fun add_headline
(v
, block
) do
580 var txt
= block
.block
.first_line
.value
581 var id
= strip_id
(txt
)
582 var lvl
= block
.depth
583 headlines
[id
] = new HeadLine(id
, txt
, lvl
)
585 v
.add
"<h{lvl} id=\"{id}\
">"
590 redef fun add_paragraph
(v
, block
) do
596 redef fun add_code
(v
, block
) do
599 v
.add
"</code></pre>\n"
602 redef fun add_blockquote
(v
, block
) do
603 v
.add
"<blockquote>\n"
605 v
.add
"</blockquote>\n"
608 redef fun add_unorderedlist
(v
, block
) do
614 redef fun add_orderedlist
(v
, block
) do
620 redef fun add_listitem
(v
, block
) do
626 redef fun add_em
(v
, text
) do
632 redef fun add_strong
(v
, text
) do
638 redef fun add_super
(v
, text
) do
644 redef fun add_image
(v
, link
, name
, comment
) do
646 append_value(v, link)
648 append_value(v, name)
650 if comment
!= null and not comment
.is_empty
then
652 append_value(v, comment)
658 redef fun add_link
(v
, link
, name
, comment
) do
660 append_value(v, link)
662 if comment
!= null and not comment
.is_empty
then
664 append_value(v, comment)
672 redef fun add_abbr
(v
, name
, comment
) do
673 v
.add
"<abbr title=\""
674 append_value(v, comment)
680 redef fun add_span_code
(v
, text
, from
, to
) do
682 append_code
(v
, text
, from
, to
)
686 redef fun add_line_break
(v
) do
690 redef fun append_value
(v
, text
) do for c
in text
do escape_char
(v
, c
)
692 redef fun escape_char
(v
, c
) do
695 else if c
== '<' then
697 else if c
== '>' then
699 else if c
== '"' then
701 else if c
== '\'' then
708 redef fun append_code(v, buffer, from, to) do
709 for i in [from..to[ do
713 else if c == '<' then
715 else if c == '>' then
723 redef fun strip_id(txt) do
725 var b = new FlatBuffer
730 if not c.is_letter and
732 not allowed_id_chars.has(c) then continue
738 # check for multiple id definitions
739 if headlines.has_key(key) then
742 while headlines.has_key(key) do
750 private var allowed_id_chars: Array[Char] = ['-', '_
', ':', '.']
753 # A block of markdown lines.
754 # A `MDBlock` can contains lines and/or sub-blocks.
758 var kind: Block = new BlockNone(self) is writable
761 var first_line: nullable MDLine = null is writable
764 var last_line: nullable MDLine = null is writable
766 # First sub-block if any.
767 var first_block: nullable MDBlock = null is writable
769 # Last sub-block if any.
770 var last_block: nullable MDBlock = null is writable
772 # Previous block if any.
773 var prev: nullable MDBlock = null is writable
776 var next: nullable MDBlock = null is writable
778 # Does this block contain subblocks?
779 fun has_blocks: Bool do return first_block != null
782 fun count_blocks: Int do
784 var block = first_block
785 while block != null do
792 # Does this block contain lines?
793 fun has_lines: Bool do return first_line != null
796 fun count_lines: Int do
798 var line = first_line
799 while line != null do
806 # Split `self` creating a new sub-block having `line` has `last_line`.
807 fun split(line: MDLine): MDBlock do
808 var block = new MDBlock
809 block.first_line = first_line
810 block.last_line = line
811 first_line = line.next
813 if first_line == null then
816 first_line.prev = null
818 if first_block == null then
822 last_block.next = block
828 # Add a `line` to this block.
829 fun add_line(line: MDLine) do
830 if last_line == null then
834 last_line.next_empty = line.is_empty
835 line.prev_empty = last_line.is_empty
836 line.prev = last_line
837 last_line.next = line
842 # Remove `line` from this block.
843 fun remove_line(line: MDLine) do
844 if line.prev == null then
845 first_line = line.next
847 line.prev.next = line.next
849 if line.next == null then
850 last_line = line.prev
852 line.next.prev = line.prev
858 # Remove leading empty lines.
859 fun remove_leading_empty_lines: Bool do
860 var was_empty = false
861 var line = first_line
862 while line != null and line.is_empty do
870 # Remove trailing empty lines.
871 fun remove_trailing_empty_lines: Bool do
872 var was_empty = false
874 while line != null and line.is_empty do
882 # Remove leading and trailing empty lines.
883 fun remove_surrounding_empty_lines: Bool do
884 var was_empty = false
885 if remove_leading_empty_lines then was_empty = true
886 if remove_trailing_empty_lines then was_empty = true
890 # Remove list markers and up to 4 leading spaces.
891 # Used to clean nested lists.
892 fun remove_list_indent(v: MarkdownProcessor) do
893 var line = first_line
894 while line != null do
895 if not line.is_empty then
896 var kind = v.line_kind(line)
897 if kind isa LineList then
898 line.value = kind.extract_value(line)
900 line.value = line.value.substring_from(line.leading.min(4))
902 line.leading = line.process_leading
908 # Collect block line text.
910 var text = new FlatBuffer
911 var line = first_line
912 while line != null do
913 if not line.is_empty then
914 text.append line.text
919 return text.write_to_string
923 # Representation of a markdown block in the AST.
924 # Each `Block` is linked to a `MDBlock` that contains mardown code.
927 # The markdown block `self` is related to.
930 # Output `self` using `v.decorator`.
931 fun emit(v: MarkdownEmitter) do v.emit_in(self)
933 # Emit the containts of `self`, lines or blocks.
934 fun emit_in(v: MarkdownEmitter) do
935 block.remove_surrounding_empty_lines
936 if block.has_lines then
943 # Emit lines contained in `block`.
944 fun emit_lines(v: MarkdownEmitter) do
945 var tpl = v.push_buffer
946 var line = block.first_line
947 while line != null do
948 if not line.is_empty then
949 v.add line.value.substring(line.leading, line.value.length - line.trailing)
950 if line.trailing >= 2 then v.decorator.add_line_break(v)
952 if line.next != null then
961 # Emit sub-blocks contained in `block`.
962 fun emit_blocks(v: MarkdownEmitter) do
963 var block = self.block.first_block
964 while block != null do
971 # A block without any markdown specificities.
973 # Actually use the same implementation than `BlockCode`,
974 # this class is only used for typing purposes.
979 # A markdown blockquote.
983 redef fun emit(v) do v.decorator.add_blockquote(v, self)
985 # Remove blockquote markers.
986 private fun remove_block_quote_prefix(block: MDBlock) do
987 var line = block.first_line
988 while line != null do
989 if not line.is_empty then
990 if line.value[line.leading] == '>' then
991 var rem = line.leading + 1
992 if line.leading + 1 < line.value.length and
993 line.value[line.leading + 1] == ' ' then
996 line.value = line.value.substring_from(rem)
997 line.leading = line.process_leading
1005 # A markdown code block.
1009 # Number of char to skip at the beginning of the line.
1011 # Block code lines start at 4 spaces.
1012 protected var line_start = 4
1014 redef fun emit(v) do v.decorator.add_code(v, self)
1016 redef fun emit_lines(v) do
1017 var line = block.first_line
1018 while line != null do
1019 if not line.is_empty then
1020 v.decorator.append_code(v, line.value, line_start, line.value.length)
1028 # A markdown code-fence block.
1030 # Actually use the same implementation than `BlockCode`,
1031 # this class is only used for typing purposes.
1035 # Fence code lines start at 0 spaces.
1036 redef var line_start = 0
1039 # A markdown headline.
1043 redef fun emit(v) do v.decorator.add_headline(v, self)
1045 # Depth of the headline used to determine the headline level.
1048 # Remove healine marks from lines contained in `self`.
1049 private fun transform_headline(block: MDBlock) do
1050 if depth > 0 then return
1052 var line = block.first_line
1053 if line.is_empty then return
1054 var start = line.leading
1055 while start < line.value.length and line.value[start] == '#' do
1059 while start
< line
.value
.length
and line
.value
[start
] == ' ' do
1062 if start
>= line
.value
.length
then
1063 line
.is_empty
= true
1065 var nend
= line
.value
.length
- line
.trailing
- 1
1066 while line
.value
[nend
] == '#' do nend
-= 1
1067 while line
.value
[nend
] == ' ' do nend
-= 1
1068 line
.value
= line
.value
.substring
(start
, nend
- start
+ 1)
1072 depth
= level
.min
(6)
1076 # A markdown list item block.
1080 redef fun emit
(v
) do v
.decorator
.add_listitem
(v
, self)
1083 # A markdown list block.
1084 # Can be either an ordered or unordered list, this class is mainly used to factorize code.
1085 abstract class BlockList
1088 # Split list block into list items sub-blocks.
1089 private fun init_block
(v
: MarkdownProcessor) do
1090 var line
= block
.first_line
1092 while line
!= null do
1093 var t
= v
.line_kind
(line
)
1094 if t
isa LineList or
1095 (not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
1096 not (t
isa LineList))) then
1097 var sblock
= block
.split
(line
.prev
.as(not null))
1098 sblock
.kind
= new BlockListItem(sblock
)
1102 var sblock
= block
.split
(block
.last_line
.as(not null))
1103 sblock
.kind
= new BlockListItem(sblock
)
1106 # Expand list items as paragraphs if needed.
1107 private fun expand_paragraphs
(block
: MDBlock) do
1108 var outer
= block
.first_block
1109 var inner
: nullable MDBlock
1110 var has_paragraph
= false
1111 while outer
!= null and not has_paragraph
do
1112 if outer
.kind
isa BlockListItem then
1113 inner
= outer
.first_block
1114 while inner
!= null and not has_paragraph
do
1115 if inner
.kind
isa BlockParagraph then
1116 has_paragraph
= true
1123 if has_paragraph
then
1124 outer
= block
.first_block
1125 while outer
!= null do
1126 if outer
.kind
isa BlockListItem then
1127 inner
= outer
.first_block
1128 while inner
!= null do
1129 if inner
.kind
isa BlockNone then
1130 inner
.kind
= new BlockParagraph(inner
)
1141 # A markdown ordered list.
1142 class BlockOrderedList
1145 redef fun emit
(v
) do v
.decorator
.add_orderedlist
(v
, self)
1148 # A markdown unordred list.
1149 class BlockUnorderedList
1152 redef fun emit
(v
) do v
.decorator
.add_unorderedlist
(v
, self)
1155 # A markdown paragraph block.
1156 class BlockParagraph
1159 redef fun emit
(v
) do v
.decorator
.add_paragraph
(v
, self)
1166 redef fun emit
(v
) do v
.decorator
.add_ruler
(v
, self)
1169 # Xml blocks that can be found in markdown markup.
1173 redef fun emit_lines
(v
) do
1174 var line
= block
.first_line
1175 while line
!= null do
1176 if not line
.is_empty
then v
.add line
.value
1186 # Text contained in this line.
1187 var value
: String is writable
1189 # Is this line empty?
1190 # Lines containing only spaces are considered empty.
1191 var is_empty
: Bool = true is writable
1193 # Previous line in `MDBlock` or null if first line.
1194 var prev
: nullable MDLine = null is writable
1196 # Next line in `MDBlock` or null if last line.
1197 var next
: nullable MDLine = null is writable
1199 # Is the previous line empty?
1200 var prev_empty
: Bool = false is writable
1202 # Is the next line empty?
1203 var next_empty
: Bool = false is writable
1205 # Initialize a new MDLine from its string value
1207 self.leading
= process_leading
1208 if leading
!= value
.length
then
1209 self.is_empty
= false
1210 self.trailing
= process_trailing
1214 # Set `value` as an empty String and update `leading`, `trailing` and is_`empty`.
1220 if prev
!= null then prev
.next_empty
= true
1221 if next
!= null then next
.prev_empty
= true
1224 # Number or leading spaces on this line.
1225 var leading
: Int = 0 is writable
1227 # Compute `leading` depending on `value`.
1228 fun process_leading
: Int do
1230 var value
= self.value
1231 while count
< value
.length
and value
[count
] == ' ' do count
+= 1
1232 if leading
== value
.length
then clear
1236 # Number of trailing spaces on this line.
1237 var trailing
: Int = 0 is writable
1239 # Compute `trailing` depending on `value`.
1240 fun process_trailing
: Int do
1242 var value
= self.value
1243 while value
[value
.length
- count
- 1] == ' ' do
1249 # Count the amount of `ch` in this line.
1250 # Return A value > 0 if this line only consists of `ch` end spaces.
1251 fun count_chars
(ch
: Char): Int do
1267 # Count the amount of `ch` at the start of this line ignoring spaces.
1268 fun count_chars_start
(ch
: Char): Int do
1283 # Last XML line if any.
1284 private var xml_end_line
: nullable MDLine = null
1286 # Does `value` contains valid XML markup?
1287 private fun check_html
: Bool do
1288 var tags
= new Array[String]
1289 var tmp
= new FlatBuffer
1291 if pos
+ 1 < value
.length
and value
[pos
+ 1] == '!' then
1292 if read_xml_comment
(self, pos
) > 0 then return true
1294 pos
= value
.read_xml
(tmp
, pos
, false)
1298 if not tag
.is_html_block
then
1306 var line
: nullable MDLine = self
1307 while line
!= null do
1308 while pos
< line
.value
.length
and line
.value
[pos
] != '<' do
1311 if pos
>= line
.value
.length
then
1312 if pos
- 2 >= 0 and line
.value
[pos
- 2] == '/' then
1314 if tags
.is_empty
then
1322 tmp
= new FlatBuffer
1323 var new_pos
= line
.value
.read_xml
(tmp
, pos
, false)
1326 if tag
.is_html_block
and not tag
== "hr" then
1327 if tmp
[1] == '/' then
1328 if tags
.last
!= tag
then
1336 if tags
.is_empty
then
1346 return tags
.is_empty
1351 # Read a XML comment.
1352 # Used by `check_html`.
1353 private fun read_xml_comment
(first_line
: MDLine, start
: Int): Int do
1354 var line
: nullable MDLine = first_line
1355 if start
+ 3 < line
.value
.length
then
1356 if line
.value
[2] == '-' and line
.value
[3] == '-' then
1358 while line
!= null do
1359 while pos
< line
.value
.length
and line
.value
[pos
] != '-' do
1362 if pos
== line
.value
.length
then
1366 if pos
+ 2 < line
.value
.length
then
1367 if line
.value
[pos
+ 1] == '-' and line
.value
[pos
+ 2] == '>' then
1368 first_line
.xml_end_line
= line
1380 # Extract the text of `self` without leading and trailing.
1381 fun text
: String do return value
.substring
(leading
, value
.length
- trailing
)
1388 # See `MarkdownProcessor::recurse`.
1389 fun process
(v
: MarkdownProcessor) is abstract
1392 # An empty markdown line.
1396 redef fun process
(v
) do
1397 v
.current_line
= v
.current_line
.next
1401 # A non-specific markdown construction.
1402 # Mainly used as part of another line construct such as paragraphs or lists.
1406 redef fun process
(v
) do
1407 var line
= v
.current_line
1409 var was_empty
= line
.prev_empty
1410 while line
!= null and not line
.is_empty
do
1411 var t
= v
.line_kind
(line
)
1412 if v
.in_list
and t
isa LineList then
1415 if t
isa LineCode or t
isa LineFence then
1418 if t
isa LineHeadline or t
isa LineHeadline1 or t
isa LineHeadline2 or
1419 t
isa LineHR or t
isa LineBlockquote or t
isa LineXML then
1425 if line
!= null and not line
.is_empty
then
1426 var block
= v
.current_block
.split
(line
.prev
.as(not null))
1427 if v
.in_list
and not was_empty
then
1428 block
.kind
= new BlockNone(block
)
1430 block
.kind
= new BlockParagraph(block
)
1432 v
.current_block
.remove_leading_empty_lines
1435 if line
!= null then
1436 block
= v
.current_block
.split
(line
)
1438 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1440 if v
.in_list
and (line
== null or not line
.is_empty
) and not was_empty
then
1441 block
.kind
= new BlockNone(block
)
1443 block
.kind
= new BlockParagraph(block
)
1445 v
.current_block
.remove_leading_empty_lines
1447 v
.current_line
= v
.current_block
.first_line
1451 # A line of markdown code.
1455 redef fun process
(v
) do
1456 var line
= v
.current_line
1458 while line
!= null and (line
.is_empty
or v
.line_kind
(line
) isa LineCode) do
1461 # split at block end line
1463 if line
!= null then
1464 block
= v
.current_block
.split
(line
.prev
.as(not null))
1466 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1468 block
.kind
= new BlockCode(block
)
1469 block
.remove_surrounding_empty_lines
1470 v
.current_line
= v
.current_block
.first_line
1474 # A line of raw XML.
1478 redef fun process
(v
) do
1479 var line
= v
.current_line
1480 var prev
= line
.prev
1481 if prev
!= null then v
.current_block
.split
(prev
)
1482 var block
= v
.current_block
.split
(line
.xml_end_line
.as(not null))
1483 block
.kind
= new BlockXML(block
)
1484 v
.current_block
.remove_leading_empty_lines
1485 v
.current_line
= v
.current_block
.first_line
1489 # A markdown blockquote line.
1490 class LineBlockquote
1493 redef fun process
(v
) do
1494 var line
= v
.current_line
1496 while line
!= null do
1497 if not line
.is_empty
and (line
.prev_empty
and
1498 line
.leading
== 0 and
1499 not v
.line_kind
(line
) isa LineBlockquote) then break
1504 if line
!= null then
1505 block
= v
.current_block
.split
(line
.prev
.as(not null))
1507 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1509 var kind
= new BlockQuote(block
)
1511 block
.remove_surrounding_empty_lines
1512 kind
.remove_block_quote_prefix
(block
)
1513 v
.current_line
= line
1514 v
.recurse
(block
, false)
1515 v
.current_line
= v
.current_block
.first_line
1519 # A markdown ruler line.
1523 redef fun process
(v
) do
1524 var line
= v
.current_line
1525 if line
.prev
!= null then v
.current_block
.split
(line
.prev
.as(not null))
1526 var block
= v
.current_block
.split
(line
.as(not null))
1527 block
.kind
= new BlockRuler(block
)
1528 v
.current_block
.remove_leading_empty_lines
1529 v
.current_line
= v
.current_block
.first_line
1533 # A markdown fence code line.
1537 redef fun process
(v
) do
1539 var line
= v
.current_line
.next
1540 while line
!= null do
1541 if v
.line_kind
(line
) isa LineFence then break
1544 if line
!= null then
1549 if line
!= null then
1550 block
= v
.current_block
.split
(line
.prev
.as(not null))
1552 block
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1554 block
.kind
= new BlockFence(block
)
1555 block
.first_line
.clear
1556 var last
= block
.last_line
1557 if last
!= null and v
.line_kind
(last
) isa LineFence then
1558 block
.last_line
.clear
1560 block
.remove_surrounding_empty_lines
1561 v
.current_line
= line
1565 # A markdown headline.
1569 redef fun process
(v
) do
1570 var line
= v
.current_line
1571 var lprev
= line
.prev
1572 if lprev
!= null then v
.current_block
.split
(lprev
)
1573 var block
= v
.current_block
.split
(line
.as(not null))
1574 var kind
= new BlockHeadline(block
)
1576 kind
.transform_headline
(block
)
1577 v
.current_block
.remove_leading_empty_lines
1578 v
.current_line
= v
.current_block
.first_line
1582 # A markdown headline of level 1.
1586 redef fun process
(v
) do
1587 var line
= v
.current_line
1588 var lprev
= line
.prev
1589 if lprev
!= null then v
.current_block
.split
(lprev
)
1591 var block
= v
.current_block
.split
(line
.as(not null))
1592 var kind
= new BlockHeadline(block
)
1594 kind
.transform_headline
(block
)
1596 v
.current_block
.remove_leading_empty_lines
1597 v
.current_line
= v
.current_block
.first_line
1601 # A markdown headline of level 2.
1605 redef fun process
(v
) do
1606 var line
= v
.current_line
1607 var lprev
= line
.prev
1608 if lprev
!= null then v
.current_block
.split
(lprev
)
1610 var block
= v
.current_block
.split
(line
.as(not null))
1611 var kind
= new BlockHeadline(block
)
1613 kind
.transform_headline
(block
)
1615 v
.current_block
.remove_leading_empty_lines
1616 v
.current_line
= v
.current_block
.first_line
1620 # A markdown list line.
1621 # Mainly used to factorize code between ordered and unordered lists.
1625 redef fun process
(v
) do
1626 var line
= v
.current_line
1628 while line
!= null do
1629 var t
= v
.line_kind
(line
)
1630 if not line
.is_empty
and (line
.prev_empty
and line
.leading
== 0 and
1631 not t
isa LineList) then break
1636 if line
!= null then
1637 list
= v
.current_block
.split
(line
.prev
.as(not null))
1639 list
= v
.current_block
.split
(v
.current_block
.last_line
.as(not null))
1641 var kind
= block_kind
(list
)
1643 list
.first_line
.prev_empty
= false
1644 list
.last_line
.next_empty
= false
1645 list
.remove_surrounding_empty_lines
1646 list
.first_line
.prev_empty
= false
1647 list
.last_line
.next_empty
= false
1649 var block
= list
.first_block
1650 while block
!= null do
1651 block
.remove_list_indent
(v
)
1652 v
.recurse
(block
, true)
1655 kind
.expand_paragraphs
(list
)
1656 v
.current_line
= line
1659 # Create a new block kind based on this line.
1660 protected fun block_kind
(block
: MDBlock): BlockList is abstract
1662 # Extract string value from `MDLine`.
1663 protected fun extract_value
(line
: MDLine): String is abstract
1666 # An ordered list line.
1670 redef fun block_kind
(block
) do return new BlockOrderedList(block
)
1672 redef fun extract_value
(line
) do
1673 return line
.value
.substring_from
(line
.value
.index_of
('.') + 2)
1677 # An unordered list line.
1681 redef fun block_kind
(block
) do return new BlockUnorderedList(block
)
1683 redef fun extract_value
(line
) do
1684 return line
.value
.substring_from
(line
.leading
+ 2)
1688 # A token represent a character in the markdown input.
1689 # Some tokens have a specific markup behaviour that is handled here.
1690 abstract class Token
1692 # Position of `self` in markdown input.
1695 # Character found at `pos` in the markdown input.
1698 # Output that token using `MarkdownEmitter::decorator`.
1699 fun emit
(v
: MarkdownEmitter) do v
.addc char
1702 # A token without a specific meaning.
1707 # An emphasis token.
1708 abstract class TokenEm
1711 redef fun emit
(v
) do
1712 var tmp
= v
.push_buffer
1713 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
1716 v
.decorator
.add_em
(v
, tmp
)
1724 # An emphasis star token.
1729 # An emphasis underscore token.
1730 class TokenEmUnderscore
1735 abstract class TokenStrong
1738 redef fun emit
(v
) do
1739 var tmp
= v
.push_buffer
1740 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 2, self)
1743 v
.decorator
.add_strong
(v
, tmp
)
1744 v
.current_pos
= b
+ 1
1751 # A strong star token.
1752 class TokenStrongStar
1756 # A strong underscore token.
1757 class TokenStrongUnderscore
1762 # This class is mainly used to factorize work between single and double quoted span codes.
1763 abstract class TokenCode
1766 redef fun emit
(v
) do
1767 var a
= pos
+ next_pos
+ 1
1768 var b
= v
.processor
.find_token
(v
.current_text
.as(not null), a
, self)
1770 v
.current_pos
= b
+ next_pos
1771 while a
< b
and v
.current_text
[a
] == ' ' do a
+= 1
1773 while v
.current_text
[b
- 1] == ' ' do b
-= 1
1774 v
.decorator
.add_span_code
(v
, v
.current_text
.as(not null), a
, b
)
1781 private fun next_pos
: Int is abstract
1784 # A span code token.
1785 class TokenCodeSingle
1788 redef fun next_pos
do return 0
1791 # A doubled span code token.
1792 class TokenCodeDouble
1795 redef fun next_pos
do return 1
1798 # A link or image token.
1799 # This class is mainly used to factorize work between images and links.
1800 abstract class TokenLinkOrImage
1804 var link
: nullable Text = null
1807 var name
: nullable Text = null
1810 var comment
: nullable Text = null
1812 # Is the link construct an abbreviation?
1813 var is_abbrev
= false
1815 redef fun emit
(v
) do
1816 var tmp
= new FlatBuffer
1817 var b
= check_link
(v
, tmp
, pos
, self)
1826 # Emit the hyperlink as link or image.
1827 private fun emit_hyper
(v
: MarkdownEmitter) is abstract
1829 # Check if the link is a valid link.
1830 private fun check_link
(v
: MarkdownEmitter, out
: FlatBuffer, start
: Int, token
: Token): Int do
1831 var md
= v
.current_text
1833 if token
isa TokenLink then
1838 var tmp
= new FlatBuffer
1839 pos
= md
.read_md_link_id
(tmp
, pos
)
1840 if pos
< start
then return -1
1844 pos
= md
.skip_spaces
(pos
)
1846 var tid
= name
.write_to_string
.to_lower
1847 if v
.processor
.link_refs
.has_key
(tid
) then
1848 var lr
= v
.processor
.link_refs
[tid
]
1849 is_abbrev
= lr
.is_abbrev
1856 else if md
[pos
] == '(' then
1858 pos
= md
.skip_spaces
(pos
)
1859 if pos
< start
then return -1
1860 tmp
= new FlatBuffer
1861 var use_lt
= md
[pos
] == '<'
1863 pos
= md
.read_until
(tmp
, pos
+ 1, '>')
1865 pos
= md
.read_md_link
(tmp
, pos
)
1867 if pos
< start
then return -1
1868 if use_lt
then pos
+= 1
1869 link
= tmp
.write_to_string
1870 if md
[pos
] == ' ' then
1871 pos
= md
.skip_spaces
(pos
)
1872 if pos
> start
and md
[pos
] == '"' then
1874 tmp
= new FlatBuffer
1875 pos
= md
.read_until
(tmp
, pos
, '"')
1876 if pos
< start
then return -1
1877 comment
= tmp
.write_to_string
1879 pos
= md
.skip_spaces
(pos
)
1880 if pos
== -1 then return -1
1883 if md
[pos
] != ')' then return -1
1884 else if md
[pos
] == '[' then
1886 tmp
= new FlatBuffer
1887 pos
= md
.read_raw_until
(tmp
, pos
, ']')
1888 if pos
< start
then return -1
1890 if tmp
.length
> 0 then
1895 var tid
= id
.write_to_string
.to_lower
1896 if v
.processor
.link_refs
.has_key
(tid
) then
1897 var lr
= v
.processor
.link_refs
[tid
]
1902 var tid
= name
.write_to_string
.replace
("\n", " ").to_lower
1903 if v
.processor
.link_refs
.has_key
(tid
) then
1904 var lr
= v
.processor
.link_refs
[tid
]
1912 if link
== null then return -1
1917 # A markdown link token.
1919 super TokenLinkOrImage
1921 redef fun emit_hyper
(v
) do
1922 if is_abbrev
and comment
!= null then
1923 v
.decorator
.add_abbr
(v
, name
.as(not null), comment
.as(not null))
1925 v
.decorator
.add_link
(v
, link
.as(not null), name
.as(not null), comment
)
1930 # A markdown image token.
1932 super TokenLinkOrImage
1934 redef fun emit_hyper
(v
) do
1935 v
.decorator
.add_image
(v
, link
.as(not null), name
.as(not null), comment
)
1943 redef fun emit
(v
) do
1944 var tmp
= new FlatBuffer
1945 var b
= check_html
(v
, tmp
, v
.current_text
.as(not null), v
.current_pos
)
1950 v
.decorator
.escape_char
(v
, char
)
1954 # Is the HTML valid?
1955 # Also take care of link and mailto shortcuts.
1956 private fun check_html
(v
: MarkdownEmitter, out
: FlatBuffer, md
: Text, start
: Int): Int do
1957 # check for auto links
1958 var tmp
= new FlatBuffer
1959 var pos
= md
.read_until
(tmp
, start
+ 1, ':', ' ', '>', '\n')
1960 if pos
!= -1 and md
[pos
] == ':' and tmp
.is_link_prefix
then
1961 pos
= md
.read_until
(tmp
, pos
, '>')
1963 var link
= tmp
.write_to_string
1964 v
.decorator
.add_link
(v
, link
, link
, null)
1968 # TODO check for mailto
1969 # check for inline html
1970 if start
+ 2 < md
.length
then
1971 return md
.read_xml
(out
, start
, true)
1977 # An HTML entity token.
1981 redef fun emit
(v
) do
1982 var tmp
= new FlatBuffer
1983 var b
= check_entity
(tmp
, v
.current_text
.as(not null), pos
)
1988 v
.decorator
.escape_char
(v
, char
)
1992 # Is the entity valid?
1993 private fun check_entity
(out
: FlatBuffer, md
: Text, start
: Int): Int do
1994 var pos
= md
.read_until
(out
, start
, ';')
1995 if pos
< 0 or out
.length
< 3 then
1998 if out
[1] == '#' then
1999 if out
[2] == 'x' or out
[2] == 'X' then
2000 if out
.length
< 4 then return -1
2001 for i
in [3..out
.length
[ do
2003 if (c
< '0' or c
> '9') and (c
< 'a' and c
> 'f') and (c
< 'A' and c
> 'F') then
2008 for i
in [2..out
.length
[ do
2010 if c
< '0' or c
> '9' then return -1
2015 for i
in [1..out
.length
[ do
2017 if not c
.is_digit
and not c
.is_letter
then return -1
2020 # TODO check entity is valid
2021 # if out.is_entity then
2031 # A markdown escape token.
2035 redef fun emit
(v
) do
2037 v
.addc v
.current_text
[v
.current_pos
]
2041 # A markdown super token.
2045 redef fun emit
(v
) do
2046 var tmp
= v
.push_buffer
2047 var b
= v
.emit_text_until
(v
.current_text
.as(not null), pos
+ 1, self)
2050 v
.decorator
.add_super
(v
, tmp
)
2060 # Get the position of the next non-space character.
2061 private fun skip_spaces
(start
: Int): Int do
2063 while pos
> -1 and pos
< length
and (self[pos
] == ' ' or self[pos
] == '\n') do
2066 if pos
< length
then return pos
2070 # Read `self` until `nend` and append it to the `out` buffer.
2071 # Escape markdown special chars.
2072 private fun read_until
(out
: FlatBuffer, start
: Int, nend
: Char...): Int do
2074 while pos
< length
do
2076 if c
== '\\' and pos
+ 1 < length
then
2077 pos
= escape
(out
, self[pos
+ 1], pos
)
2079 var end_reached
= false
2086 if end_reached
then break
2091 if pos
== length
then return -1
2095 # Read `self` as raw text until `nend` and append it to the `out` buffer.
2096 # No escape is made.
2097 private fun read_raw_until
(out
: FlatBuffer, start
: Int, nend
: Char...): Int do
2099 while pos
< length
do
2101 var end_reached
= false
2108 if end_reached
then break
2112 if pos
== length
then return -1
2116 # Read `self` as XML until `to` and append it to the `out` buffer.
2117 # Escape HTML special chars.
2118 private fun read_xml_until
(out
: FlatBuffer, from
: Int, to
: Char...): Int do
2121 var str_char
: nullable Char = null
2122 while pos
< length
do
2128 if pos
< length
then
2134 if c
== str_char
then
2141 if c
== '"' or c
== '\'' then
2146 var end_reached = false
2147 for n in [0..to.length[ do
2153 if end_reached then break
2158 if pos == length then return -1
2162 # Read `self` as XML and append it to the `out` buffer.
2163 # Safe mode can be activated to limit reading to valid xml.
2164 private fun read_xml(out: FlatBuffer, start: Int, safe_mode: Bool): Int do
2166 var is_close_tag = false
2167 if start + 1 >= length then return -1
2168 if self[start + 1] == '/' then
2171 else if self[start + 1] == '!' then
2175 is_close_tag = false
2179 var tmp = new FlatBuffer
2180 pos = read_xml_until(tmp, pos, ' ', '/', '>')
2181 if pos == -1 then return -1
2182 var tag = tmp.write_to_string.trim.to_lower
2183 if tag.is_html_unsafe then
2185 if is_close_tag then out.add '/'
2189 if is_close_tag then out.add '/'
2194 if is_close_tag then out.add '/'
2195 pos = read_xml_until(out, pos, ' ', '/', '>')
2197 if pos == -1 then return -1
2198 pos = read_xml_until(out, pos, '/', '>')
2199 if pos == -1 then return -1
2200 if self[pos] == '/' then
2202 pos = self.read_xml_until(out, pos + 1, '>')
2203 if pos == -1 then return -1
2205 if self[pos] == '>' then
2212 # Read a markdown link address and append it to the `out` buffer.
2213 private fun read_md_link(out: FlatBuffer, start: Int): Int do
2216 while pos < length do
2218 if c == '\\
' and pos + 1 < length then
2219 pos = escape(out, self[pos + 1], pos)
2221 var end_reached = false
2224 else if c == ' ' then
2225 if counter == 1 then end_reached = true
2226 else if c == ')' then
2228 if counter == 0 then end_reached = true
2230 if end_reached then break
2235 if pos == length then return -1
2239 # Read a markdown link text and append it to the `out` buffer.
2240 private fun read_md_link_id(out: FlatBuffer, start: Int): Int do
2243 while pos < length do
2245 var end_reached = false
2249 else if c == ']' then
2251 if counter == 0 then
2259 if end_reached then break
2262 if pos == length then return -1
2266 # Extract the XML tag name from a XML tag.
2267 private fun xml_tag: String do
2268 var tpl = new FlatBuffer
2270 if pos < length and self[1] == '/' then pos += 1
2271 while pos < length - 1 and (self[pos].is_digit or self[pos].is_letter) do
2275 return tpl.write_to_string.to_lower
2278 # Read and escape the markdown contained in `self`.
2279 private fun escape(out: FlatBuffer, c: Char, pos: Int): Int do
2280 if c == '\\
' or c == '[' or c == ']' or c == '(' or c == ')' or c == '{' or
2281 c == '}' or c == '#' or c == '"' or c == '\'' or c == '.' or c == '<' or
2282 c
== '>' or c
== '*' or c
== '+' or c
== '-' or c
== '_' or c
== '!' or
2283 c
== '`' or c
== '~' or c
== '^' then
2291 # Is `self` an unsafe HTML element?
2292 private fun is_html_unsafe
: Bool do return html_unsafe_tags
.has
(self.write_to_string
)
2294 # Is `self` a HRML block element?
2295 private fun is_html_block
: Bool do return html_block_tags
.has
(self.write_to_string
)
2297 # Is `self` a link prefix?
2298 private fun is_link_prefix
: Bool do return html_link_prefixes
.has
(self.write_to_string
)
2300 private fun html_unsafe_tags
: Array[String] do return once
["applet", "head", "body", "frame", "frameset", "iframe", "script", "object"]
2302 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"]
2304 private fun html_link_prefixes
: Array[String] do return once
["http", "https", "ftp", "ftps"]
2309 # Parse `self` as markdown and return the HTML representation
2311 # var md = "**Hello World!**"
2312 # var html = md.md_to_html
2313 # assert html == "<p><strong>Hello World!</strong></p>\n"
2314 fun md_to_html
: Streamable do
2315 var processor
= new MarkdownProcessor
2316 return processor
.process
(self)