misc/vim: inform the user when no results are found
[nit.git] / lib / markdown / markdown.nit
index fd33c59..10dbcc6 100644 (file)
@@ -31,9 +31,9 @@ import template
 class MarkdownProcessor
 
        # `MarkdownEmitter` used for ouput.
-       var emitter: MarkdownEmitter is noinit
+       var emitter: MarkdownEmitter is noinit, protected writable
 
-       # Work in extended mode.
+       # Work in extended mode (default).
        #
        # Behavior changes when using extended mode:
        #
@@ -69,6 +69,26 @@ class MarkdownProcessor
        #               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
+       #
+       #               <pre class="nit"><code>import markdown
+       #
+       #               print "Hello World!".md_to_html
+       #               </code></pre>
+       #
        # * Underscores (Emphasis)
        #
        #   Underscores in the middle of a word like:
@@ -83,12 +103,22 @@ class MarkdownProcessor
        #
        #               <p>Con_cat_this</p>
        #
-       var ext_mode = false
+       # * Strikethrough
+       #
+       #   Like in [GFM](https://help.github.com/articles/github-flavored-markdown),
+       #   strikethrought span is marked with `~~`.
+       #
+       #               ~~Mistaken text.~~
+       #
+       #   becomes
+       #
+       #               <del>Mistaken text.</del>
+       var ext_mode = true
 
        init do self.emitter = new MarkdownEmitter(self)
 
        # 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
@@ -398,6 +428,11 @@ class MarkdownProcessor
                else if c == '&' then
                        return new TokenEntity(pos, c)
                else
+                       if ext_mode then
+                               if c == '~' and c1 == '~' then
+                                       return new TokenStrike(pos, c)
+                               end
+                       end
                        return new TokenNone(pos, c)
                end
        end
@@ -502,7 +537,7 @@ class MarkdownEmitter
        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
@@ -520,9 +555,9 @@ 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
@@ -575,6 +610,11 @@ interface Decorator
        # Render a strong text.
        fun add_strong(v: MarkdownEmitter, text: Text) is abstract
 
+       # Render a strike text.
+       #
+       # Extended mode only (see `MarkdownProcessor::ext_mode`)
+       fun add_strike(v: MarkdownEmitter, text: Text) is abstract
+
        # Render a link.
        fun add_link(v: MarkdownEmitter, link: Text, name: Text, comment: nullable Text) is abstract
 
@@ -647,7 +687,11 @@ class HTMLDecorator
        end
 
        redef fun add_code(v, block) do
-               v.add "<pre><code>"
+               if block isa BlockFence and block.meta != null then
+                       v.add "<pre class=\"{block.meta.to_s}\"><code>"
+               else
+                       v.add "<pre><code>"
+               end
                v.emit_in block
                v.add "</code></pre>\n"
        end
@@ -688,6 +732,12 @@ class HTMLDecorator
                v.add "</strong>"
        end
 
+       redef fun add_strike(v, text) do
+               v.add "<del>"
+               v.add text
+               v.add "</del>"
+       end
+
        redef fun add_image(v, link, name, comment) do
                v.add "<img src=\""
                append_value(v, link)
@@ -1079,6 +1129,9 @@ 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
@@ -1598,7 +1651,8 @@ class LineFence
                else
                        block = v.current_block.split(v.current_block.last_line.as(not null))
                end
-               block.kind = new BlockFence(block)
+               var meta = block.first_line.value.meta_from_fence
+               block.kind = new BlockFence(block, meta)
                block.first_line.clear
                var last = block.last_line
                if last != null and v.line_kind(last) isa LineFence then
@@ -2085,6 +2139,25 @@ class TokenEscape
        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
+
 redef class Text
 
        # Get the position of the next non-space character.
@@ -2193,6 +2266,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 +2284,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 "&lt;"
+                               pos = -1
+                       else if tag.is_html_unsafe then
+                               is_valid = false
                                out.append "&lt;"
                                if is_close_tag then out.add '/'
                                out.append tmp
@@ -2233,7 +2311,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 "&gt;"
+                       end
                        return pos
                end
                return -1
@@ -2305,6 +2387,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 +2408,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 +2442,7 @@ redef class String
        #    var md = "**Hello World!**"
        #    var html = md.md_to_html
        #    assert html == "<p><strong>Hello World!</strong></p>\n"
-       fun md_to_html: Streamable do
+       fun md_to_html: Writable do
                var processor = new MarkdownProcessor
                return processor.process(self)
        end