highlight: move hightlightcode inside HighlightVisitor
[nit.git] / src / highlight.nit
index 757af93..7b2f718 100644 (file)
@@ -19,13 +19,68 @@ import frontend
 import html
 import pipeline
 import astutil
+import serialization
+
+# A standalone highlighted piece of code
+class HLCode
+       super Serializable
+
+       # The highlighter used
+       var hl: HighlightVisitor
+
+       # The raw code source
+       var content: String
+
+       # The pseudo source-file
+       var source: SourceFile
+
+       # JavaScript code to update an existing codemirror editor.
+       fun code_mirror_update: Template
+       do
+
+               var res = new Template
+               res.add """
+       function nitmessage() {
+               editor.operation(function(){
+                       for (var i = 0; i < widgets.length; ++i)
+                             editor.removeLineWidget(widgets[i]);
+                       widgets.length = 0;
+"""
+
+               for m in source.messages do
+                       res.add """
+                       var l = document.createElement("div");
+                       l.className = "lint-error"
+                       l.innerHTML = "<span class='glyphicon glyphicon-warning-sign lint-error-icon'></span> {{{m.text.html_escape}}}";
+                       var w = editor.addLineWidget({{{m.location.line_start-1}}}, l);
+                       widgets.push(w);
+"""
+               end
+               res.add """});}"""
+               return res
+       end
+
+       redef fun core_serialize_to(v)
+       do
+               v.serialize_attribute("code", hl.html.write_to_string)
+               var msgs = new Array[Map[String, Serializable]]
+               for m in source.messages do
+                       var o = new Map[String, Serializable]
+                       msgs.add o
+                       o["line"] = m.location.line_start-1
+                       o["message"] = m.text
+               end
+               v.serialize_attribute("messages", msgs)
+       end
+end
 
 # Visitor used to produce a HTML tree based on a AST on a `Source`
 class HighlightVisitor
        # The root of the HTML hierarchy
        var html = new HTMLTag("span")
 
-       # Is the HTML include a nested `<span class"{type_of_node}">` element for each `ANode` of the AST?
+       # Should the HTML include a nested `<span class"{type_of_node}">` element for each `ANode` of the AST?
+       #
        # Used to have a really huge and verbose HTML (mainly for debug)
        var with_ast = false is writable
 
@@ -60,11 +115,9 @@ class HighlightVisitor
        # It is used to refer to some specific entities when generating links.
        # If `null` is returned, then no link are generated and `<a>` elements become `<span>`.
        #
-       # Clients are encouraged to redefine the method in a subclass to control where entities should link to.
-       fun hrefto(entitiy: MEntity): nullable String
-       do
-               return entitiy.href
-       end
+       # By default, `null` is returned.
+       # Clients are therefore encouraged to redefine the method in a subclass to control where entities should link to.
+       fun hrefto(entity: MEntity): nullable String do return null
 
        init
        do
@@ -90,9 +143,8 @@ class HighlightVisitor
        # Default: false
        var include_whole_lines = false is writable
 
-       # The entry-point of the highlighting.
-       # Will fill `html` with the generated HTML content.
-       fun enter_visit(n: ANode)
+       # Highlight a AST element.
+       fun highlight_node(n: ANode)
        do
                n.parentize_tokens
 
@@ -115,12 +167,28 @@ class HighlightVisitor
                        if l.next_looses.not_empty then l = l.next_looses.last
                end
 
+               var line = first_line
+               if line != null then
+                       while f.location.line_start < line do
+                               f = f.next_token
+                               if f == null then return
+                       end
+               end
+
+               line = last_line
+               if line != null then
+                       while l.location.line_end > line do
+                               l = l.prev_token
+                               if l == null then return
+                       end
+               end
+
                if include_whole_lines then
                        f = f.first_real_token_in_line
                        l = l.last_real_token_in_line
                end
 
-               htmlize(f, l)
+               do_highlight(f, l)
        end
 
        private fun full_tag(anode: ANode, hv: HighlightVisitor): nullable HTMLTag
@@ -159,19 +227,19 @@ class HighlightVisitor
        # Highlight a full lexed source file.
        #
        # REQUIRE `source.first_token != null`
-       fun hightlight_source(source: SourceFile)
+       fun highlight_source(source: SourceFile)
        do
-               htmlize(source.first_token.as(not null), null)
+               do_highlight(source.first_token.as(not null), null)
        end
 
-       # Produce HTML between two tokens
-       protected fun htmlize(first_token: Token, last_token: nullable Token)
+       # Low-level highlighting between 2 tokens
+       protected fun do_highlight(first_token: Token, last_token: nullable Token)
        do
                var stack2 = new Array[HTMLTag]
                var stack = new Array[Prod]
                var line = 0
                var c: nullable Token = first_token
-               var hv = new HighlightVisitor
+               var hv = self
                while c != null do
                        var starting
 
@@ -222,7 +290,7 @@ class HighlightVisitor
                        end
 
                        # Add the token
-                       if c isa TEol then 
+                       if c isa TEol then
                                html.append "\n"
                        else
                                var tag = full_tag(c, hv)
@@ -259,8 +327,7 @@ class HighlightVisitor
 
                        c = n
                end
-               #assert stack.is_empty
-               #assert stack2.is_empty
+               if not stack2.is_empty then html = stack2.first
        end
 
        # Return a default CSS content related to CSS classes used in the `html` tree.
@@ -320,6 +387,51 @@ class HighlightVisitor
 <script src="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
 <script>$(".popupable").popover({html:true, placement:'top'})/*initialize bootstrap popover*/</script>"""
        end
+
+       # Fully process `content` as a Nit source file.
+       #
+       # Set `print_errors = true` to print errors in the code to the console.
+       fun highlightcode(content: String, print_errors: nullable Bool): HLCode
+       do
+               # Prepare a stand-alone tool context
+               var tc = new ToolContext
+               tc.nit_dir = tc.locate_nit_dir # still use the common lib to have core
+               tc.keep_going = true # no exit, obviously
+               if print_errors != true then tc.opt_warn.value = -1 # no output
+
+               # Prepare an stand-alone model and model builder.
+               # Unfortunately, models are enclosing and append-only.
+               # There is no way (yet?) to have a shared module `core` with
+               # isolated and throwable user modules.
+               var model = new Model
+               var mb = new ModelBuilder(model, tc)
+
+               # Parse the code
+               var source = new SourceFile.from_string("", content + "\n")
+               var lexer = new Lexer(source)
+               var parser = new Parser(lexer)
+               var tree = parser.parse
+
+               var hlcode = new HLCode(self, content, source)
+
+               # Check syntax error
+               var eof = tree.n_eof
+               if eof isa AError then
+                       mb.error(eof, eof.message)
+                       highlight_source(source)
+                       return hlcode
+               end
+               var amodule = tree.n_base.as(not null)
+
+               # Load the AST as a module in the model
+               # Then process it
+               mb.load_rt_module(null, amodule, "")
+               mb.run_phases
+
+               # Highlight the processed module
+               highlight_node(amodule)
+               return hlcode
+       end
 end
 
 redef class HTMLTag
@@ -364,12 +476,14 @@ class HInfoBox
        end
 
        # Append a new dropdown in the popuped content
-       fun new_dropdown(title, text: String): HTMLTag
+       fun new_dropdown(title, text: String, text_is_html: nullable Bool): HTMLTag
        do
                content.add_raw_html """<div class="dropdown"> <a data-toggle="dropdown" href="#"><b>"""
                content.append(title)
                content.add_raw_html "</b> "
-               content.append(text)
+               if text_is_html == true then
+                       content.add_raw_html(text)
+               else content.append(text)
                content.add_raw_html """<span class="caret"></span></a>"""
                var res = content.open("ul").add_class("dropdown-menu").attr("role", "menu").attr("aria-labelledby", "dLabel")
                content.add_raw_html "</div>"
@@ -428,8 +542,12 @@ redef class MEntity
                return (new HTMLTag("a")).attr("href", href).text(text)
        end
 
-       # Default link
-       private fun href: nullable String do return null
+       # Append an entry for the doc in the given infobox
+       private fun add_doc_to_infobox(res: HInfoBox)
+       do
+               var mdoc = mdoc_or_fallback
+               if mdoc != null then mdoc.fill_infobox(res)
+       end
 end
 
 redef class MModule
@@ -438,8 +556,7 @@ redef class MModule
                var res = new HInfoBox(v, "module {name}")
                res.href = v.hrefto(self)
                res.new_field("module").add(linkto(v))
-               var mdoc = self.mdoc
-               if mdoc != null then mdoc.fill_infobox(res)
+               add_doc_to_infobox(res)
                if in_importation.greaters.length > 1 then
                        var c = res.new_dropdown("imports", "{in_importation.greaters.length-1} modules")
                        for x in in_importation.greaters do
@@ -450,12 +567,6 @@ redef class MModule
                return res
        end
 
-       # The module HTML page
-       redef fun href: String
-       do
-               return c_name + ".html"
-       end
-
        redef fun linkto(v) do return linkto_text(v, name)
 end
 
@@ -470,10 +581,9 @@ redef class MClassDef
                        res.new_field("redef class").text(mclass.name)
                        res.new_field("intro").add mclass.intro.linkto_text(v, "in {mclass.intro_mmodule.to_s}")
                end
-               var mdoc = self.mdoc
-               if mdoc == null then mdoc = mclass.intro.mdoc
-               if mdoc != null then mdoc.fill_infobox(res)
+               add_doc_to_infobox(res)
 
+               var in_hierarchy = self.in_hierarchy
                if in_hierarchy == null then return res
 
                if in_hierarchy.greaters.length > 1 then
@@ -501,12 +611,6 @@ redef class MClassDef
                end
                return res
        end
-
-       # The class HTML page (an anchor in the module page)
-       redef fun href: String
-       do
-               return mmodule.href + "#" + to_s
-       end
 end
 
 redef class MPropDef
@@ -515,10 +619,13 @@ redef class MPropDef
                var res = new HInfoBox(v, to_s)
                res.href = v.hrefto(self)
                if self isa MMethodDef then
+                       var msignature = self.msignature
                        if msignature != null then res.new_field("fun").append(mproperty.name).add msignature.linkto(v)
                else if self isa MAttributeDef then
+                       var static_mtype = self.static_mtype
                        if static_mtype != null then res.new_field("fun").append(mproperty.name).add static_mtype.linkto(v)
                else if self isa MVirtualTypeDef then
+                       var bound = self.bound
                        if bound != null then res.new_field("add").append(mproperty.name).add bound.linkto(v)
                else
                        res.new_field("wat?").append(mproperty.name)
@@ -528,9 +635,7 @@ redef class MPropDef
                else
                        res.new_field("intro").add mproperty.intro.linkto_text(v, "in {mproperty.intro.mclassdef}")
                end
-               var mdoc = self.mdoc
-               if mdoc == null then mdoc = mproperty.intro.mdoc
-               if mdoc != null then mdoc.fill_infobox(res)
+               add_doc_to_infobox(res)
                if mproperty.mpropdefs.length > 1 then
                        var c = res.new_dropdown("redef", "redefinitions")
                        for x in mproperty.mpropdefs do
@@ -540,12 +645,6 @@ redef class MPropDef
 
                return res
        end
-
-       # The property HTML page (an anchor in the module page)
-       redef fun href: String
-       do
-               return self.mclassdef.mmodule.href + "#" + self.to_s
-       end
 end
 
 redef class MClassType
@@ -554,9 +653,7 @@ redef class MClassType
                var res = new HInfoBox(v, to_s)
                res.href = v.hrefto(self)
                res.new_field("class").add mclass.intro.linkto(v)
-               var mdoc = mclass.mdoc
-               if mdoc == null then mdoc = mclass.intro.mdoc
-               if mdoc != null then mdoc.fill_infobox(res)
+               add_doc_to_infobox(res)
                return res
        end
        redef fun linkto(v)
@@ -570,10 +667,8 @@ redef class MVirtualType
                var res = new HInfoBox(v, to_s)
                res.href = v.hrefto(mproperty)
                var p = mproperty
-               var pd = p.intro
-               res.new_field("virtual type").add pd.linkto(v)
-               var mdoc = pd.mdoc
-               if mdoc != null then mdoc.fill_infobox(res)
+               res.new_field("virtual type").add p.intro.linkto(v)
+               add_doc_to_infobox(res)
                return res
        end
        redef fun linkto(v)
@@ -668,9 +763,7 @@ redef class CallSite
                else
                        res.new_field("intro").add mproperty.intro.linkto_text(v, "in {mproperty.intro.mclassdef}")
                end
-               var mdoc = mpropdef.mdoc
-               if mdoc == null then mdoc = mproperty.intro.mdoc
-               if mdoc != null then mdoc.fill_infobox(res)
+               add_doc_to_infobox(res)
 
                return res
        end
@@ -719,6 +812,8 @@ redef class AQclassid
        redef fun decorate_tag(v, res, token)
        do
                if token != n_id then return null
+               var parent = self.parent
+               if parent == null then return null
                return parent.decorate_tag(v, res, token)
        end
 end
@@ -727,6 +822,8 @@ redef class AQid
        redef fun decorate_tag(v, res, token)
        do
                if token != n_id then return null
+               var parent = self.parent
+               if parent == null then return null
                return parent.decorate_tag(v, res, token)
        end
 end
@@ -781,7 +878,7 @@ end
 redef class Token
        # Produce an HTMLTag with the correct contents and CSS classes
        # Subclasses can redefine it to decorate the tag
-       redef fun make_tag(v: HighlightVisitor): HTMLTag
+       redef fun make_tag(v): HTMLTag
        do
                var res = new HTMLTag("span")
                res.text(text)
@@ -875,6 +972,7 @@ end
 redef class ASendExpr
        redef fun decorate_tag(v, res, token)
        do
+               var callsite = self.callsite
                if callsite == null then return null
                return callsite.infobox(v)
        end
@@ -883,6 +981,7 @@ end
 redef class ANewExpr
        redef fun decorate_tag(v, res, token)
        do
+               var callsite = self.callsite
                if callsite == null then return null
                return callsite.infobox(v)
        end
@@ -903,7 +1002,9 @@ end
 redef class AModuleName
        redef fun decorate_tag(v, res, token)
        do
-               return parent.decorate_tag(v, res, token)
+               var p = parent
+               if p == null then return null
+               return p.decorate_tag(v, res, token)
        end
 end
 
@@ -1016,6 +1117,7 @@ redef class AFormaldef
        do
                if not token isa TClassid then return null
                res.add_class("nc_vt")
+               var mtype = self.mtype
                if mtype == null then return null
                return mtype.infobox(v)
        end