lib/markdown: fix `text` for nested markdown blocks
[nit.git] / lib / markdown / markdown.nit
index eac11c2..ecf6064 100644 (file)
@@ -133,6 +133,14 @@ class MarkdownProcessor
        # ~~~
        var ext_mode = true
 
+       # 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
+
        init do self.emitter = new MarkdownEmitter(self)
 
        # Process the mardown `input` string and return the processed output.
@@ -168,8 +176,9 @@ class MarkdownProcessor
                                var c = input[i]
                                if c == '\n' then
                                        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
@@ -204,15 +213,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
@@ -396,7 +405,16 @@ class MarkdownProcessor
                        c2 = ' '
                end
 
-               var loc = text.pos_to_loc(pos)
+               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
@@ -413,7 +431,7 @@ class MarkdownProcessor
                        end
                else if c == '_' then
                        if c1 == '_' then
-                               if c0 != ' ' or c2 != ' 'then
+                               if c0 != ' ' or c2 != ' ' then
                                        return new TokenStrongUnderscore(loc, pos, c)
                                else
                                        return new TokenEmUnderscore(loc, pos, c)
@@ -476,6 +494,12 @@ class MarkdownProcessor
                end
                return -1
        end
+
+       # Location used for next parsed token.
+       #
+       # This location can be changed by the emitter to adjust with `\n` found
+       # in the input.
+       private fun current_loc: MDLocation do return emitter.current_loc
 end
 
 # Emit output corresponding to blocks content.
@@ -519,15 +543,19 @@ class MarkdownEmitter
        # Transform and emit mardown text
        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
+                       if text[current_pos] == '\n' then
+                               current_loc.line_start += 1
+                               current_loc.column_start = -current_pos
+                       end
                        var mt = processor.token_at(text, current_pos)
                        if (token != null and not token isa TokenNone) and
                        (mt.is_same_type(token) or
@@ -570,6 +598,21 @@ 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: Writable) do
                if e isa Text then
@@ -580,10 +623,12 @@ class MarkdownEmitter
        end
 
        # Append `c` to current buffer.
-       fun addc(c: Char) do add c.to_s
+       fun addc(c: Char) do
+               current_buffer.add c
+       end
 
        # Append a "\n" line break.
-       fun addn do add "\n"
+       fun addn do addc '\n'
 end
 
 # A Link Reference.
@@ -605,7 +650,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
@@ -617,6 +662,11 @@ interface Decorator
        # Kind of emitter used for decoration.
        type EMITTER: MarkdownEmitter
 
+       # Render a single plain char.
+       #
+       # Redefine this method to add special escaping for plain text.
+       fun add_char(v: EMITTER, c: Char) do v.addc c
+
        # Render a ruler block.
        fun add_ruler(v: EMITTER, block: BlockRuler) is abstract
 
@@ -724,8 +774,11 @@ class HTMLDecorator
        end
 
        redef fun add_code(v, block) do
-               if block isa BlockFence and block.meta != null then
-                       v.add "<pre class=\"{block.meta.to_s}\"><code>"
+               var meta = block.meta
+               if meta != null then
+                       v.add "<pre class=\""
+                       append_value(v, meta)
+                       v.add "\"><code>"
                else
                        v.add "<pre><code>"
                end
@@ -900,6 +953,11 @@ class MDLocation
        var column_end: Int
 
        redef fun to_s do return "{line_start},{column_start}--{line_end},{column_end}"
+
+       # Return a copy of `self`.
+       fun copy: MDLocation do
+               return new MDLocation(line_start, column_start, line_end, column_end)
+       end
 end
 
 # A block of markdown lines.
@@ -1082,6 +1140,12 @@ class MDBlock
                        text.append "\n"
                        line = line.next
                end
+               var block = first_block
+               while block != null do
+                       text.append block.text
+                       text.append "\n"
+                       block = block.next
+               end
                return text.write_to_string
        end
 end
@@ -1128,10 +1192,32 @@ abstract class Block
        fun emit_blocks(v: MarkdownEmitter) do
                var block = self.block.first_block
                while block != null do
+                       v.push_loc(block.location)
                        block.kind.emit(v)
+                       v.pop_loc
                        block = block.next
                end
        end
+
+       # The raw content of the block as a multi-line string.
+       fun raw_content: String do
+               var infence = self isa BlockFence
+               var text = new FlatBuffer
+               var line = self.block.first_line
+               while line != null do
+                       if not line.is_empty then
+                               var str = line.value
+                               if not infence and str.has_prefix("    ") then
+                                       text.append str.substring(4, str.length - line.trailing)
+                               else
+                                       text.append str
+                               end
+                       end
+                       text.append "\n"
+                       line = line.next
+               end
+               return text.write_to_string
+       end
 end
 
 # A block without any markdown specificities.
@@ -1172,6 +1258,9 @@ end
 class BlockCode
        super Block
 
+       # Any string found after fence token.
+       var meta: nullable Text
+
        # Number of char to skip at the beginning of the line.
        #
        # Block code lines start at 4 spaces.
@@ -1198,9 +1287,6 @@ end
 class BlockFence
        super BlockCode
 
-       # Any string found after fence token.
-       var meta: nullable Text
-
        # Fence code lines start at 0 spaces.
        redef var line_start = 0
 end
@@ -1209,7 +1295,15 @@ end
 class BlockHeadline
        super Block
 
-       redef fun emit(v) do v.decorator.add_headline(v, self)
+       redef fun emit(v) do
+               var loc = block.location.copy
+               loc.column_start += start
+               v.push_loc(loc)
+               v.decorator.add_headline(v, self)
+               v.pop_loc
+       end
+
+       private var start = 0
 
        # Depth of the headline used to determine the headline level.
        var depth = 0
@@ -1238,6 +1332,7 @@ class BlockHeadline
                        line.leading = 0
                        line.trailing = 0
                end
+               self.start = start
                depth = level.min(6)
        end
 end
@@ -1793,7 +1888,7 @@ 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
@@ -1864,7 +1959,7 @@ end
 abstract class Token
 
        # Location of `self` in the original input.
-       var location: MDLocation
+       var location: nullable MDLocation
 
        # Position of `self` in input independant from lines.
        var pos: Int
@@ -1873,7 +1968,7 @@ abstract class Token
        var char: Char
 
        # Output that token using `MarkdownEmitter::decorator`.
-       fun emit(v: MarkdownEmitter) do v.addc char
+       fun emit(v: MarkdownEmitter) do v.decorator.add_char(v, char)
 end
 
 # A token without a specific meaning.
@@ -2057,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
@@ -2255,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
@@ -2495,24 +2584,6 @@ redef class Text
                return null
        end
 
-       # Init a `MDLocation` instance at `pos` in `self`.
-       private fun pos_to_loc(pos: Int): MDLocation do
-               assert pos <= length
-               var line = 1
-               var col = 0
-               var i = 0
-               while i <= pos do
-                       col += 1
-                       var c = self[i]
-                       if c == '\n' then
-                               line +=1
-                               col = 0
-                       end
-                       i +=1
-               end
-               return new MDLocation(line, col, line, col)
-       end
-
        # Is `self` an unsafe HTML element?
        private fun is_html_unsafe: Bool do return html_unsafe_tags.has(self.write_to_string)