X-Git-Url: http://nitlanguage.org diff --git a/lib/markdown/markdown.nit b/lib/markdown/markdown.nit index fd33c59..2426d53 100644 --- a/lib/markdown/markdown.nit +++ b/lib/markdown/markdown.nit @@ -30,10 +30,7 @@ import template # SEE: `String::md_to_html` for a shortcut. class MarkdownProcessor - # `MarkdownEmitter` used for ouput. - var emitter: MarkdownEmitter is noinit - - # Work in extended mode. + # Work in extended mode (default). # # Behavior changes when using extended mode: # @@ -41,54 +38,108 @@ class MarkdownProcessor # # In normal markdown the following: # - # This is a paragraph - # * and this is not a list + # ~~~md + # This is a paragraph + # * and this is not a list + # ~~~ # # Will produce: # - #

This is a paragraph - # * and this is not a list

+ # ~~~html + #

This is a paragraph + # * and this is not a list

+ # ~~~ # - # When using extended mode this changes to: + # When using extended mode this changes to: # - #

This is a paragraph

- # + # ~~~html + #

This is a paragraph

+ # + # ~~~ # # * Fences code blocks # # If you don't want to indent your all your code with 4 spaces, # you can wrap your code in ``` ``` ``` or `~~~`. # - # Here's an example: + # Here's an example: + # + # ~~~md + # fun test do + # print "Hello World!" + # end + # ~~~ + # + # * Code blocks meta + # + # If you want to use syntax highlighting tools, most of them need to know what kind + # of language they are highlighting. + # You can add an optional language identifier after the fence declaration to output + # it in the HTML render. + # + # ```nit + # import markdown + # + # print "# Hello World!".md_to_html + # ``` + # + # Becomes + # + # ~~~html + #
import markdown
 	#
-	#		```
-	#		fun test do
-	#			print "Hello World!"
-	#		end
-	#		```
+	# print "Hello World!".md_to_html
+	# 
+ # ~~~ # # * Underscores (Emphasis) # # Underscores in the middle of a word like: # - # Con_cat_this + # ~~~md + # Con_cat_this + # ~~~ # - # normally produces this: + # normally produces this: # - #

Concatthis

+ # ~~~html + #

Concatthis

+ # ~~~ # # With extended mode they don't result in emphasis. # - #

Con_cat_this

+ # ~~~html + #

Con_cat_this

+ # ~~~ # - var ext_mode = false + # * Strikethrough + # + # Like in [GFM](https://help.github.com/articles/github-flavored-markdown), + # strikethrought span is marked with `~~`. + # + # ~~~md + # ~~Mistaken text.~~ + # ~~~ + # + # becomes + # + # ~~~html + # Mistaken text. + # ~~~ + var ext_mode = true - init do self.emitter = new MarkdownEmitter(self) + # Disable attaching MDLocation to Tokens + # + # Locations are useful for some tools but they may + # cause an important time and space overhead. + # + # Default = `false` + var no_location = false is writable # Process the mardown `input` string and return the processed output. - fun process(input: String): Streamable do + fun process(input: String): Writable do # init processor link_refs.clear last_link_ref = null @@ -99,41 +150,49 @@ class MarkdownProcessor parent.remove_surrounding_empty_lines recurse(parent, false) # output processed text - return emitter.emit(parent.kind) + decorator.headlines.clear + return emit(parent.kind) end # Split `input` string into `MDLines` and create a parent `MDBlock` with it. private fun read_lines(input: String): MDBlock do - var block = new MDBlock + var block = new MDBlock(new MDLocation(1, 1, 1, 1)) var value = new FlatBuffer var i = 0 + + var line_pos = 0 + var col_pos = 0 + while i < input.length do value.clear var pos = 0 var eol = false while not eol and i < input.length do + col_pos += 1 var c = input[i] if c == '\n' then - i += 1 eol = true + else if c == '\r' then else if c == '\t' then - var np = pos + (4 - (pos.bin_and(3))) + var np = pos + (4 - (pos & 3)) while pos < np do value.add ' ' pos += 1 end - i += 1 else pos += 1 value.add c - i += 1 end + i += 1 end + line_pos += 1 - var line = new MDLine(value.write_to_string) + var loc = new MDLocation(line_pos, 1, line_pos, col_pos) + var line = new MDLine(loc, value.write_to_string) var is_link_ref = check_link_ref(line) # Skip link refs if not is_link_ref then block.add_line line + col_pos = 0 end return block end @@ -150,15 +209,15 @@ class MarkdownProcessor if not line.is_empty and line.leading < 4 and line.value[line.leading] == '[' then pos = line.leading + 1 pos = md.read_until(id, pos, ']') - if not id.is_empty and pos + 2 < line.value.length then + if not id.is_empty and pos >= 0 and pos + 2 < line.value.length then if line.value[pos + 1] == ':' then pos += 2 pos = md.skip_spaces(pos) - if line.value[pos] == '<' then + if pos >= 0 and line.value[pos] == '<' then pos += 1 pos = md.read_until(link, pos, '>') pos += 1 - else + else if pos >= 0 then pos = md.read_until(link, pos, ' ', '\n') end if not link.is_empty then @@ -199,7 +258,10 @@ class MarkdownProcessor pos = md.read_until(comment, pos, c) end end - if not comment.is_empty then last_link_ref.title = comment.write_to_string + var last_link_ref = self.last_link_ref + if not comment.is_empty and last_link_ref != null then + last_link_ref.title = comment.write_to_string + end end if comment.is_empty then return false return true @@ -214,8 +276,10 @@ class MarkdownProcessor # # Markdown allows link refs to be defined over two lines: # - # [id]: http://example.com/longish/path/to/resource/here - # "Optional Title Here" + # ~~~md + # [id]: http://example.com/longish/path/to/resource/here + # "Optional Title Here" + # ~~~ # private var last_link_ref: nullable LinkRef = null @@ -340,65 +404,81 @@ class MarkdownProcessor c2 = ' ' end + var loc + if no_location then + loc = null + else + loc = new MDLocation( + current_loc.line_start, + current_loc.column_start + pos, + current_loc.line_start, + current_loc.column_start + pos) + end + if c == '*' then if c1 == '*' then if c0 != ' ' or c2 != ' ' then - return new TokenStrongStar(pos, c) + return new TokenStrongStar(loc, pos, c) else - return new TokenEmStar(pos, c) + return new TokenEmStar(loc, pos, c) end end if c0 != ' ' or c1 != ' ' then - return new TokenEmStar(pos, c) + return new TokenEmStar(loc, pos, c) else - return new TokenNone(pos, c) + return new TokenNone(loc, pos, c) end else if c == '_' then if c1 == '_' then - if c0 != ' ' or c2 != ' 'then - return new TokenStrongUnderscore(pos, c) + if c0 != ' ' or c2 != ' ' then + return new TokenStrongUnderscore(loc, pos, c) else - return new TokenEmUnderscore(pos, c) + return new TokenEmUnderscore(loc, pos, c) end end if ext_mode then if (c0.is_letter or c0.is_digit) and c0 != '_' and (c1.is_letter or c1.is_digit) then - return new TokenNone(pos, c) + return new TokenNone(loc, pos, c) else - return new TokenEmUnderscore(pos, c) + return new TokenEmUnderscore(loc, pos, c) end end if c0 != ' ' or c1 != ' ' then - return new TokenEmUnderscore(pos, c) + return new TokenEmUnderscore(loc, pos, c) else - return new TokenNone(pos, c) + return new TokenNone(loc, pos, c) end else if c == '!' then - if c1 == '[' then return new TokenImage(pos, c) - return new TokenNone(pos, c) + if c1 == '[' then return new TokenImage(loc, pos, c) + return new TokenNone(loc, pos, c) else if c == '[' then - return new TokenLink(pos, c) + return new TokenLink(loc, pos, c) else if c == ']' then - return new TokenNone(pos, c) + return new TokenNone(loc, pos, c) else if c == '`' then if c1 == '`' then - return new TokenCodeDouble(pos, c) + return new TokenCodeDouble(loc, pos, c) else - return new TokenCodeSingle(pos, c) + return new TokenCodeSingle(loc, pos, c) end else if c == '\\' then 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 - return new TokenEscape(pos, c) + return new TokenEscape(loc, pos, c) else - return new TokenNone(pos, c) + return new TokenNone(loc, pos, c) end else if c == '<' then - return new TokenHTML(pos, c) + return new TokenHTML(loc, pos, c) else if c == '&' then - return new TokenEntity(pos, c) + return new TokenEntity(loc, pos, c) else - return new TokenNone(pos, c) + if ext_mode then + if c == '~' and c1 == '~' then + return new TokenStrike(loc, pos, c) + end + end + return new TokenNone(loc, pos, c) end end @@ -413,24 +493,18 @@ class MarkdownProcessor end return -1 end -end -# Emit output corresponding to blocks content. -# -# Blocks are created by a previous pass in `MarkdownProcessor`. -# The emitter use a `Decorator` to select the output format. -class MarkdownEmitter - - # Processor containing link refs. - var processor: MarkdownProcessor + # Kind of decorator used for decoration. + type DECORATOR: Decorator # Decorator used for output. # Default is `HTMLDecorator` - var decorator: Decorator = new HTMLDecorator is writable + var decorator: DECORATOR is writable, lazy do + return new HTMLDecorator + end # Create a new `MarkdownEmitter` using a custom `decorator`. - init with_decorator(processor: MarkdownProcessor, decorator: Decorator) do - init processor + init with_decorator(decorator: DECORATOR) do self.decorator = decorator end @@ -446,20 +520,22 @@ class MarkdownEmitter fun emit_in(block: Block) do block.emit_in(self) # Transform and emit mardown text - fun emit_text(text: Text) do - emit_text_until(text, 0, null) - end + fun emit_text(text: Text) do emit_text_until(text, 0, null) - # Transform and emit mardown text starting at `from` and + # Transform and emit mardown text starting at `start` and # until a token with the same type as `token` is found. - # Go until the end of text if `token` is null. + # Go until the end of `text` if `token` is null. fun emit_text_until(text: Text, start: Int, token: nullable Token): Int do var old_text = current_text var old_pos = current_pos current_text = text current_pos = start while current_pos < text.length do - var mt = processor.token_at(text, current_pos) + if text[current_pos] == '\n' then + current_loc.line_start += 1 + current_loc.column_start = -current_pos + end + var mt = token_at(text, current_pos) if (token != null and not token isa TokenNone) and (mt.is_same_type(token) or (token isa TokenEmStar and mt isa TokenStrongStar) or @@ -501,8 +577,23 @@ class MarkdownEmitter return buffer_stack.last end + # Stacked locations. + private var loc_stack = new List[MDLocation] + + # Push a new MDLocation on the stack. + private fun push_loc(location: MDLocation) do loc_stack.add location + + # Pop the last buffer. + private fun pop_loc: MDLocation do return loc_stack.pop + + # Current output buffer. + private fun current_loc: MDLocation do + assert not loc_stack.is_empty + return loc_stack.last + end + # Append `e` to current buffer. - fun add(e: Streamable) do + fun add(e: Writable) do if e isa Text then current_buffer.append e else @@ -511,18 +602,20 @@ class MarkdownEmitter end # Append `c` to current buffer. - fun addc(c: Char) do current_buffer.add c + fun addc(c: Char) do + current_buffer.add c + end # Append a "\n" line break. - fun addn do current_buffer.add '\n' + fun addn do addc '\n' end # A Link Reference. # Links that are specified somewhere in the mardown document to be reused as shortcuts. # -# Example: -# -# [1]: http://example.com/ "Optional title" +# ~~~raw +# [1]: http://example.com/ "Optional title" +# ~~~ class LinkRef # Link href @@ -536,7 +629,7 @@ class LinkRef # Create a link with a title. init with_title(link: String, title: nullable String) do - self.link = link + init(link) self.title = title end end @@ -545,59 +638,72 @@ end # Default decorator used is `HTMLDecorator`. interface Decorator + # Kind of processor used + type PROCESSOR: MarkdownProcessor + + # Render a single plain char. + # + # Redefine this method to add special escaping for plain text. + fun add_char(v: PROCESSOR, c: Char) do v.addc c + # Render a ruler block. - fun add_ruler(v: MarkdownEmitter, block: BlockRuler) is abstract + fun add_ruler(v: PROCESSOR, block: BlockRuler) is abstract # Render a headline block with corresponding level. - fun add_headline(v: MarkdownEmitter, block: BlockHeadline) is abstract + fun add_headline(v: PROCESSOR, block: BlockHeadline) is abstract # Render a paragraph block. - fun add_paragraph(v: MarkdownEmitter, block: BlockParagraph) is abstract + fun add_paragraph(v: PROCESSOR, block: BlockParagraph) is abstract # Render a code or fence block. - fun add_code(v: MarkdownEmitter, block: BlockCode) is abstract + fun add_code(v: PROCESSOR, block: BlockCode) is abstract # Render a blockquote. - fun add_blockquote(v: MarkdownEmitter, block: BlockQuote) is abstract + fun add_blockquote(v: PROCESSOR, block: BlockQuote) is abstract # Render an unordered list. - fun add_unorderedlist(v: MarkdownEmitter, block: BlockUnorderedList) is abstract + fun add_unorderedlist(v: PROCESSOR, block: BlockUnorderedList) is abstract # Render an ordered list. - fun add_orderedlist(v: MarkdownEmitter, block: BlockOrderedList) is abstract + fun add_orderedlist(v: PROCESSOR, block: BlockOrderedList) is abstract # Render a list item. - fun add_listitem(v: MarkdownEmitter, block: BlockListItem) is abstract + fun add_listitem(v: PROCESSOR, block: BlockListItem) is abstract # Render an emphasis text. - fun add_em(v: MarkdownEmitter, text: Text) is abstract + fun add_em(v: PROCESSOR, text: Text) is abstract # Render a strong text. - fun add_strong(v: MarkdownEmitter, text: Text) is abstract + fun add_strong(v: PROCESSOR, text: Text) is abstract + + # Render a strike text. + # + # Extended mode only (see `MarkdownProcessor::ext_mode`) + fun add_strike(v: PROCESSOR, text: Text) is abstract # Render a link. - fun add_link(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract + fun add_link(v: PROCESSOR, link: Text, name: Text, comment: nullable Text) is abstract # Render an image. - fun add_image(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract + fun add_image(v: PROCESSOR, link: Text, name: Text, comment: nullable Text) is abstract # Render an abbreviation. - fun add_abbr(v: MarkdownEmitter, name: Text, comment: Text) is abstract + fun add_abbr(v: PROCESSOR, name: Text, comment: Text) is abstract # Render a code span reading from a buffer. - fun add_span_code(v: MarkdownEmitter, buffer: Text, from, to: Int) is abstract + fun add_span_code(v: PROCESSOR, buffer: Text, from, to: Int) is abstract # Render a text and escape it. - fun append_value(v: MarkdownEmitter, value: Text) is abstract + fun append_value(v: PROCESSOR, value: Text) is abstract # Render code text from buffer and escape it. - fun append_code(v: MarkdownEmitter, buffer: Text, from, to: Int) is abstract + fun append_code(v: PROCESSOR, buffer: Text, from, to: Int) is abstract # Render a character escape. - fun escape_char(v: MarkdownEmitter, char: Char) is abstract + fun escape_char(v: PROCESSOR, char: Char) is abstract # Render a line break - fun add_line_break(v: MarkdownEmitter) is abstract + fun add_line_break(v: PROCESSOR) is abstract # Generate a new html valid id from a `String`. fun strip_id(txt: String): String is abstract @@ -630,7 +736,9 @@ class HTMLDecorator redef fun add_headline(v, block) do # save headline - var txt = block.block.first_line.value + var line = block.block.first_line + if line == null then return + var txt = line.value var id = strip_id(txt) var lvl = block.depth headlines[id] = new HeadLine(id, txt, lvl) @@ -647,7 +755,14 @@ class HTMLDecorator end redef fun add_code(v, block) do - v.add "
"
+		var meta = block.meta
+		if meta != null then
+			v.add "
"
+		else
+			v.add "
"
+		end
 		v.emit_in block
 		v.add "
\n" end @@ -688,6 +803,12 @@ class HTMLDecorator v.add "" end + redef fun add_strike(v, text) do + v.add "" + v.add text + v.add "" + end + redef fun add_image(v, link, name, comment) do v.add " 0 then return var level = 0 var line = block.first_line + if line == null then return if line.is_empty then return var start = line.leading while start < line.value.length and line.value[start] == '#' do @@ -1116,6 +1314,7 @@ class BlockHeadline line.leading = 0 line.trailing = 0 end + self.start = start depth = level.min(6) end end @@ -1135,6 +1334,7 @@ abstract class BlockList # Split list block into list items sub-blocks. private fun init_block(v: MarkdownProcessor) do var line = block.first_line + if line == null then return line = line.next while line != null do var t = v.line_kind(line) @@ -1230,6 +1430,9 @@ end # A markdown line. class MDLine + # Location of `self` in the original input. + var location: MDLocation + # Text contained in this line. var value: String is writable @@ -1264,8 +1467,8 @@ class MDLine leading = 0 trailing = 0 is_empty = true - if prev != null then prev.next_empty = true - if next != null then next.prev_empty = true + if prev != null then prev.as(not null).next_empty = true + if next != null then next.as(not null).prev_empty = true end # Number or leading spaces on this line. @@ -1399,8 +1602,8 @@ class MDLine # Used by `check_html`. private fun read_xml_comment(first_line: MDLine, start: Int): Int do var line: nullable MDLine = first_line - if start + 3 < line.value.length then - if line.value[2] == '-' and line.value[3] == '-' then + if start + 3 < line.as(not null).value.length then + if line.as(not null).value[2] == '-' and line.as(not null).value[3] == '-' then var pos = start + 4 while line != null do while pos < line.value.length and line.value[pos] != '-' do @@ -1441,7 +1644,7 @@ class LineEmpty super Line redef fun process(v) do - v.current_line = v.current_line.next + v.current_line = v.current_line.as(not null).next end end @@ -1453,7 +1656,7 @@ class LineOther redef fun process(v) do var line = v.current_line # go to block end - var was_empty = line.prev_empty + var was_empty = line.as(not null).prev_empty while line != null and not line.is_empty do var t = v.line_kind(line) if (v.in_list or v.ext_mode) and t isa LineList then @@ -1469,29 +1672,30 @@ class LineOther line = line.next end # build block + var current_block = v.current_block.as(not null) if line != null and not line.is_empty then - var block = v.current_block.split(line.prev.as(not null)) + var block = current_block.split(line.prev.as(not null)) if v.in_list and not was_empty then block.kind = new BlockNone(block) else block.kind = new BlockParagraph(block) end - v.current_block.remove_leading_empty_lines + current_block.remove_leading_empty_lines else var block: MDBlock if line != null then - block = v.current_block.split(line) + block = current_block.split(line) else - block = v.current_block.split(v.current_block.last_line.as(not null)) + block = current_block.split(current_block.last_line.as(not null)) end if v.in_list and (line == null or not line.is_empty) and not was_empty then block.kind = new BlockNone(block) else block.kind = new BlockParagraph(block) end - v.current_block.remove_leading_empty_lines + current_block.remove_leading_empty_lines end - v.current_line = v.current_block.first_line + v.current_line = current_block.first_line end end @@ -1506,15 +1710,16 @@ class LineCode line = line.next end # split at block end line + var current_block = v.current_block.as(not null) var block: MDBlock if line != null then - block = v.current_block.split(line.prev.as(not null)) + block = current_block.split(line.prev.as(not null)) else - block = v.current_block.split(v.current_block.last_line.as(not null)) + block = current_block.split(current_block.last_line.as(not null)) end block.kind = new BlockCode(block) block.remove_surrounding_empty_lines - v.current_line = v.current_block.first_line + v.current_line = current_block.first_line end end @@ -1524,12 +1729,14 @@ class LineXML redef fun process(v) do var line = v.current_line + if line == null then return + var current_block = v.current_block.as(not null) var prev = line.prev - if prev != null then v.current_block.split(prev) - var block = v.current_block.split(line.xml_end_line.as(not null)) + if prev != null then current_block.split(prev) + var block = current_block.split(line.xml_end_line.as(not null)) block.kind = new BlockXML(block) - v.current_block.remove_leading_empty_lines - v.current_line = v.current_block.first_line + current_block.remove_leading_empty_lines + v.current_line = current_block.first_line end end @@ -1539,6 +1746,7 @@ class LineBlockquote redef fun process(v) do var line = v.current_line + var current_block = v.current_block.as(not null) # go to bquote end while line != null do if not line.is_empty and (line.prev_empty and @@ -1549,9 +1757,9 @@ class LineBlockquote # build sub block var block: MDBlock if line != null then - block = v.current_block.split(line.prev.as(not null)) + block = current_block.split(line.prev.as(not null)) else - block = v.current_block.split(v.current_block.last_line.as(not null)) + block = current_block.split(current_block.last_line.as(not null)) end var kind = new BlockQuote(block) block.kind = kind @@ -1559,7 +1767,7 @@ class LineBlockquote kind.remove_block_quote_prefix(block) v.current_line = line v.recurse(block, false) - v.current_line = v.current_block.first_line + v.current_line = current_block.first_line end end @@ -1569,11 +1777,13 @@ class LineHR redef fun process(v) do var line = v.current_line - if line.prev != null then v.current_block.split(line.prev.as(not null)) - var block = v.current_block.split(line.as(not null)) + if line == null then return + var current_block = v.current_block.as(not null) + if line.prev != null then current_block.split(line.prev.as(not null)) + var block = current_block.split(line) block.kind = new BlockRuler(block) - v.current_block.remove_leading_empty_lines - v.current_line = v.current_block.first_line + current_block.remove_leading_empty_lines + v.current_line = current_block.first_line end end @@ -1583,7 +1793,8 @@ class LineFence redef fun process(v) do # go to fence end - var line = v.current_line.next + var line = v.current_line.as(not null).next + var current_block = v.current_block.as(not null) while line != null do if v.line_kind(line) isa LineFence then break line = line.next @@ -1594,15 +1805,17 @@ class LineFence # build fence block var block: MDBlock if line != null then - block = v.current_block.split(line.prev.as(not null)) + block = current_block.split(line.prev.as(not null)) else - block = v.current_block.split(v.current_block.last_line.as(not null)) + block = current_block.split(current_block.last_line.as(not null)) end - block.kind = new BlockFence(block) - block.first_line.clear + block.remove_surrounding_empty_lines + var meta = block.first_line.as(not null).value.meta_from_fence + block.kind = new BlockFence(block, meta) + block.first_line.as(not null).clear var last = block.last_line if last != null and v.line_kind(last) isa LineFence then - block.last_line.clear + block.last_line.as(not null).clear end block.remove_surrounding_empty_lines v.current_line = line @@ -1615,14 +1828,16 @@ class LineHeadline redef fun process(v) do var line = v.current_line + if line == null then return + var current_block = v.current_block.as(not null) var lprev = line.prev - if lprev != null then v.current_block.split(lprev) - var block = v.current_block.split(line.as(not null)) + if lprev != null then current_block.split(lprev) + var block = current_block.split(line) var kind = new BlockHeadline(block) block.kind = kind kind.transform_headline(block) - v.current_block.remove_leading_empty_lines - v.current_line = v.current_block.first_line + current_block.remove_leading_empty_lines + v.current_line = current_block.first_line end end @@ -1632,16 +1847,18 @@ class LineHeadline1 redef fun process(v) do var line = v.current_line + if line == null then return + var current_block = v.current_block.as(not null) var lprev = line.prev - if lprev != null then v.current_block.split(lprev) - line.next.clear - var block = v.current_block.split(line.as(not null)) + if lprev != null then current_block.split(lprev) + line.next.as(not null).clear + var block = current_block.split(line) var kind = new BlockHeadline(block) kind.depth = 1 kind.transform_headline(block) block.kind = kind - v.current_block.remove_leading_empty_lines - v.current_line = v.current_block.first_line + current_block.remove_leading_empty_lines + v.current_line = current_block.first_line end end @@ -1651,22 +1868,24 @@ class LineHeadline2 redef fun process(v) do var line = v.current_line + if line == null then return + var current_block = v.current_block.as(not null) var lprev = line.prev - if lprev != null then v.current_block.split(lprev) - line.next.clear - var block = v.current_block.split(line.as(not null)) + if lprev != null then current_block.split(lprev) + line.next.as(not null).clear + var block = current_block.split(line) var kind = new BlockHeadline(block) kind.depth = 2 kind.transform_headline(block) block.kind = kind - v.current_block.remove_leading_empty_lines - v.current_line = v.current_block.first_line + current_block.remove_leading_empty_lines + v.current_line = current_block.first_line end end # A markdown list line. # Mainly used to factorize code between ordered and unordered lists. -class LineList +abstract class LineList super Line redef fun process(v) do @@ -1679,19 +1898,20 @@ class LineList line = line.next end # build list block + var current_block = v.current_block.as(not null) var list: MDBlock if line != null then - list = v.current_block.split(line.prev.as(not null)) + list = current_block.split(line.prev.as(not null)) else - list = v.current_block.split(v.current_block.last_line.as(not null)) + list = current_block.split(current_block.last_line.as(not null)) end var kind = block_kind(list) list.kind = kind - list.first_line.prev_empty = false - list.last_line.next_empty = false + list.first_line.as(not null).prev_empty = false + list.last_line.as(not null).next_empty = false list.remove_surrounding_empty_lines - list.first_line.prev_empty = false - list.last_line.next_empty = false + list.first_line.as(not null).prev_empty = false + list.last_line.as(not null).next_empty = false kind.init_block(v) var block = list.first_block while block != null do @@ -1736,14 +1956,17 @@ end # Some tokens have a specific markup behaviour that is handled here. abstract class Token - # Position of `self` in markdown input. + # Location of `self` in the original input. + var location: nullable MDLocation + + # Position of `self` in input independant from lines. var pos: Int # Character found at `pos` in the markdown input. var char: Char # Output that token using `MarkdownEmitter::decorator`. - fun emit(v: MarkdownEmitter) do v.addc char + fun emit(v: MarkdownProcessor) do v.decorator.add_char(v, char) end # A token without a specific meaning. @@ -1811,14 +2034,15 @@ abstract class TokenCode super Token redef fun emit(v) do + var current_text = v.current_text.as(not null) var a = pos + next_pos + 1 - var b = v.processor.find_token(v.current_text.as(not null), a, self) + var b = v.find_token(current_text, a, self) if b > 0 then v.current_pos = b + next_pos - while a < b and v.current_text[a] == ' ' do a += 1 + while a < b and current_text[a] == ' ' do a += 1 if a < b then - while v.current_text[b - 1] == ' ' do b -= 1 - v.decorator.add_span_code(v, v.current_text.as(not null), a, b) + while current_text[b - 1] == ' ' do b -= 1 + v.decorator.add_span_code(v, current_text, a, b) end else v.addc char @@ -1871,11 +2095,12 @@ abstract class TokenLinkOrImage end # Emit the hyperlink as link or image. - private fun emit_hyper(v: MarkdownEmitter) is abstract + private fun emit_hyper(v: MarkdownProcessor) is abstract # Check if the link is a valid link. - private fun check_link(v: MarkdownEmitter, out: FlatBuffer, start: Int, token: Token): Int do + private fun check_link(v: MarkdownProcessor, out: FlatBuffer, start: Int, token: Token): Int do var md = v.current_text + if md == null then return -1 var pos if token isa TokenLink then pos = start + 1 @@ -1890,9 +2115,9 @@ abstract class TokenLinkOrImage pos += 1 pos = md.skip_spaces(pos) if pos < start then - var tid = name.write_to_string.to_lower - if v.processor.link_refs.has_key(tid) then - var lr = v.processor.link_refs[tid] + var tid = name.as(not null).write_to_string.to_lower + if v.link_refs.has_key(tid) then + var lr = v.link_refs[tid] is_abbrev = lr.is_abbrev link = lr.link comment = lr.title @@ -1927,6 +2152,7 @@ abstract class TokenLinkOrImage if pos == -1 then return -1 end end + if pos < start then return -1 if md[pos] != ')' then return -1 else if md[pos] == '[' then pos += 1 @@ -1939,16 +2165,16 @@ abstract class TokenLinkOrImage else id = name end - var tid = id.write_to_string.to_lower - if v.processor.link_refs.has_key(tid) then - var lr = v.processor.link_refs[tid] + var tid = id.as(not null).write_to_string.to_lower + if v.link_refs.has_key(tid) then + var lr = v.link_refs[tid] link = lr.link comment = lr.title end else - var tid = name.write_to_string.replace("\n", " ").to_lower - if v.processor.link_refs.has_key(tid) then - var lr = v.processor.link_refs[tid] + var tid = name.as(not null).write_to_string.replace("\n", " ").to_lower + if v.link_refs.has_key(tid) then + var lr = v.link_refs[tid] link = lr.link comment = lr.title pos = old_pos @@ -2000,7 +2226,7 @@ class TokenHTML # Is the HTML valid? # Also take care of link and mailto shortcuts. - private fun check_html(v: MarkdownEmitter, out: FlatBuffer, md: Text, start: Int): Int do + private fun check_html(v: MarkdownProcessor, out: FlatBuffer, md: Text, start: Int): Int do # check for auto links var tmp = new FlatBuffer var pos = md.read_until(tmp, start + 1, ':', ' ', '>', '\n') @@ -2081,7 +2307,26 @@ class TokenEscape redef fun emit(v) do v.current_pos += 1 - v.addc v.current_text[v.current_pos] + v.addc v.current_text.as(not null)[v.current_pos] + end +end + +# A markdown strike token. +# +# Extended mode only (see `MarkdownProcessor::ext_mode`) +class TokenStrike + super Token + + redef fun emit(v) do + var tmp = v.push_buffer + var b = v.emit_text_until(v.current_text.as(not null), pos + 2, self) + v.pop_buffer + if b > 0 then + v.decorator.add_strike(v, tmp) + v.current_pos = b + 1 + else + v.addc char + end end end @@ -2106,18 +2351,11 @@ redef class Text if c == '\\' and pos + 1 < length then pos = escape(out, self[pos + 1], pos) else - var end_reached = false - for n in nend do - if c == n then - end_reached = true - break - end - end - if end_reached then break + for n in nend do if c == n then break label out.add c end pos += 1 - end + end label if pos == length then return -1 return pos end @@ -2193,6 +2431,7 @@ redef class Text # Safe mode can be activated to limit reading to valid xml. private fun read_xml(out: FlatBuffer, start: Int, safe_mode: Bool): Int do var pos = 0 + var is_valid = true var is_close_tag = false if start + 1 >= length then return -1 if self[start + 1] == '/' then @@ -2210,7 +2449,11 @@ redef class Text pos = read_xml_until(tmp, pos, ' ', '/', '>') if pos == -1 then return -1 var tag = tmp.write_to_string.trim.to_lower - if tag.is_html_unsafe then + if not tag.is_valid_html_tag then + out.append "<" + pos = -1 + else if tag.is_html_unsafe then + is_valid = false out.append "<" if is_close_tag then out.add '/' out.append tmp @@ -2233,7 +2476,11 @@ redef class Text if pos == -1 then return -1 end if self[pos] == '>' then - out.add '>' + if is_valid then + out.add '>' + else + out.append ">" + end return pos end return -1 @@ -2305,6 +2552,14 @@ redef class Text return tpl.write_to_string.to_lower end + private fun is_valid_html_tag: Bool do + if is_empty then return false + for c in self do + if not c.is_alpha then return false + end + return true + end + # Read and escape the markdown contained in `self`. private fun escape(out: FlatBuffer, c: Char, pos: Int): Int do if c == '\\' or c == '[' or c == ']' or c == '(' or c == ')' or c == '{' or @@ -2318,6 +2573,17 @@ redef class Text return pos end + # Extract string found at end of fence opening. + private fun meta_from_fence: nullable Text do + for i in [0..chars.length[ do + var c = chars[i] + if c != ' ' and c != '`' and c != '~' then + return substring_from(i).trim + end + end + return null + end + # Is `self` an unsafe HTML element? private fun is_html_unsafe: Bool do return html_unsafe_tags.has(self.write_to_string) @@ -2341,7 +2607,7 @@ redef class String # var md = "**Hello World!**" # var html = md.md_to_html # assert html == "

Hello World!

\n" - fun md_to_html: Streamable do + fun md_to_html: Writable do var processor = new MarkdownProcessor return processor.process(self) end