Merge: nitlight as a service with embedded editor and ajax updates
authorJean Privat <jean@pryen.org>
Thu, 23 Jun 2016 20:21:52 +0000 (16:21 -0400)
committerJean Privat <jean@pryen.org>
Thu, 23 Jun 2016 20:21:52 +0000 (16:21 -0400)
It is less and less an example but nitlight_as_a_service now features a client-side editor (http://codemirror.net/) and update the page with some crappy ajax code.

http://test.nitlanguage.org/

Pull-Request: #2197

15 files changed:
contrib/nitcc/src/autom.nit
contrib/nitcc/src/nitcc.sablecc
contrib/nitcc/src/nitcc_parser_gen.nit
contrib/nitcc/src/re2nfa.nit
contrib/nitcc/tests/lexer-prefixes.sablecc [new file with mode: 0644]
contrib/nitcc/tests/sav/lexer-prefixes.input.res [new file with mode: 0644]
share/nitweb/index.html
share/nitweb/javascripts/docdown.js [new file with mode: 0644]
share/nitweb/javascripts/model.js
share/nitweb/javascripts/nitweb.js
share/nitweb/views/docdown.html [new file with mode: 0644]
src/doc/doc_commands.nit
src/nitweb.nit
src/web/api_docdown.nit [new file with mode: 0644]
src/web/web.nit

index 6971e7c..ac69b3b 100644 (file)
@@ -360,8 +360,8 @@ class Automaton
 
                # Remove their transitions
                for s in bads do
-                       for t in s.ins do t.delete
-                       for t in s.outs do t.delete
+                       for t in s.ins.to_a do t.delete
+                       for t in s.outs.to_a do t.delete
                end
 
                # Keep only the good stuff
@@ -373,14 +373,18 @@ class Automaton
        # REQUIRE: self is a DFA
        fun to_minimal_dfa: Automaton
        do
+               assert_valid
+
                trim
 
+               # Graph of known distinct states.
                var distincts = new HashMap[State, Set[State]]
                for s in states do
                        distincts[s] = new HashSet[State]
                end
 
-               # split accept states
+               # split accept states.
+               # An accept state is distinct with a non accept state.
                for s1 in states do
                        for s2 in states do
                                if distincts[s1].has(s2) then continue
@@ -390,7 +394,7 @@ class Automaton
                                        distincts[s2].add(s1)
                                        continue
                                end
-                               if tags[s1] != tags[s2] then
+                               if tags.get_or_null(s1) != tags.get_or_null(s2) then
                                        distincts[s1].add(s2)
                                        distincts[s2].add(s1)
                                        continue
@@ -398,24 +402,36 @@ class Automaton
                        end
                end
 
+               # Fixed point algorithm.
+               # * Get 2 states s1 and s2 not yet distinguished.
+               # * Get a symbol w.
+               # * If s1.trans(w) and s2.trans(w) are distinguished, then
+               #   distinguish s1 and s2.
                var changed = true
-               var ints = new Array[Int]
+               var ints = new Array[Int] # List of symbols to check
                while changed do
                        changed = false
                        for s1 in states do for s2 in states do
                                if distincts[s1].has(s2) then continue
+
+                               # The transitions use intervals. Therefore, for the states s1 and s2,
+                               # we need to check only the meaningful symbols. They are the `first`
+                               # symbol of each interval and the first one after the interval (`last+1`).
                                ints.clear
+                               # Check only `s1`; `s2` will be checked later when s1 and s2 are switched.
                                for t in s1.outs do
                                        var sym = t.symbol
                                        assert sym != null
                                        ints.add sym.first
                                        var l = sym.last
-                                       if l != null then ints.add l
+                                       if l != null then ints.add l + 1
                                end
+
+                               # Check each symbol
                                for i in ints do
                                        var ds1 = s1.trans(i)
                                        var ds2 = s2.trans(i)
-                                       if ds1 == null and ds2 == null then continue
+                                       if ds1 == ds2 then continue
                                        if ds1 != null and ds2 != null and not distincts[ds1].has(ds2) then continue
                                        distincts[s1].add(s2)
                                        distincts[s2].add(s1)
@@ -425,6 +441,8 @@ class Automaton
                        end
                end
 
+               # We need to unify not-distinguished states.
+               # Just add an epsilon-transition and DFAize the automaton.
                for s1 in states do for s2 in states do
                        if distincts[s1].has(s2) then continue
                        s1.add_trans(s2, null)
@@ -433,6 +451,21 @@ class Automaton
                return to_dfa
        end
 
+       # Assert that `self` is a valid automaton or abort
+       fun assert_valid
+       do
+               assert states.has(start)
+               assert states.has_all(accept)
+               for s in states do
+                       for t in s.outs do assert states.has(t.to)
+                       for t in s.ins do assert states.has(t.from)
+               end
+               assert states.has_all(tags.keys)
+               for t, ss in retrotags do
+                       assert states.has_all(ss)
+               end
+       end
+
        # Produce a graphvis file for the automaton
        fun to_dot(filepath: String)
        do
@@ -498,6 +531,8 @@ class Automaton
        # note: the DFA is not minimized.
        fun to_dfa: Automaton
        do
+               assert_valid
+
                trim
 
                var dfa = new Automaton.empty
index 18d52cc..d435c77 100644 (file)
@@ -64,6 +64,7 @@ re3 {-> re} =
        {plus:} re3 '+' |
        {shortest:} 'Shortest' '(' re ')' |
        {longest:} 'Longest' '(' re ')' |
+       {prefixes:} 'Prefixes' '(' re ')' |
        {id:} id |
        {par:} '(' re ')' |
        {class:} text '.' '.' text |
index bf3d57b..bfbdbb5 100644 (file)
@@ -85,6 +85,7 @@ var t_and = new Token("and")
 var t_except = new Token("except")
 var t_shortest = new Token("shortest")
 var t_longest = new Token("longest")
+var t_prefixes = new Token("prefixes")
 var t_ch_dec = new Token("ch_dec")
 var t_ch_hex = new Token("ch_hex")
 g.tokens.add_all([t_opar,
@@ -111,6 +112,7 @@ g.tokens.add_all([t_opar,
        t_except,
        t_shortest,
        t_longest,
+       t_prefixes,
        t_ch_dec,
        t_ch_hex])
 
@@ -139,6 +141,7 @@ p_re3.new_alt("re_ques", p_re3, t_ques)
 p_re3.new_alt("re_plus", p_re3, t_plus)
 p_re3.new_alt("re_shortest", t_shortest, t_opar, p_re, t_cpar)
 p_re3.new_alt("re_longest", t_longest, t_opar, p_re, t_cpar)
+p_re3.new_alt("re_prefixes", t_prefixes, t_opar, p_re, t_cpar)
 p_re3.new_alt("re_par", t_opar, p_re, t_cpar)
 p_re3.new_alt("re_class", p_text, t_dot, t_dot, p_text)
 p_re3.new_alt("re_openclass", p_text, t_dot, t_dot, t_dot)
index d523e51..12cd600 100644 (file)
@@ -173,6 +173,16 @@ redef class Nre_longest
        end
 end
 
+redef class Nre_prefixes
+       redef fun make_rfa
+       do
+               var a = children[2].make_rfa
+               a.trim
+               a.accept.add_all a.states
+               return a
+       end
+end
+
 redef class Nre_conc
        redef fun make_rfa
        do
diff --git a/contrib/nitcc/tests/lexer-prefixes.sablecc b/contrib/nitcc/tests/lexer-prefixes.sablecc
new file mode 100644 (file)
index 0000000..9e38697
--- /dev/null
@@ -0,0 +1,13 @@
+Grammar x;
+
+Lexer
+    m = 'abcd' | 'x'* 'y'+ 'z'?;
+    pm = Prefixes(m) Except '';
+    err = ('a'..'z') Except pm;
+
+blank = #10 | #13 | #32;
+Parser
+Ignored blank;
+
+    s = p+;
+    p = pm | err;
diff --git a/contrib/nitcc/tests/sav/lexer-prefixes.input.res b/contrib/nitcc/tests/sav/lexer-prefixes.input.res
new file mode 100644 (file)
index 0000000..108c10a
--- /dev/null
@@ -0,0 +1,52 @@
+Start
+  s
+    Nodes[Np]
+      p_0
+        pm@(1:1-1:2)='a'
+      p_1
+        err@(1:3-1:4)='b'
+      p_0
+        pm@(1:5-1:7)='ab'
+      p_0
+        pm@(1:8-1:11)='abc'
+      p_0
+        pm@(1:12-1:16)='abcd'
+      p_0
+        pm@(1:17-1:21)='abcd'
+      p_1
+        err@(1:21-1:22)='e'
+      p_0
+        pm@(1:23-1:24)='a'
+      p_0
+        pm@(1:24-1:26)='ab'
+      p_1
+        err@(1:26-1:27)='b'
+      p_1
+        err@(1:27-1:28)='c'
+      p_1
+        err@(1:28-1:29)='c'
+      p_1
+        err@(1:29-1:30)='d'
+      p_1
+        err@(1:30-1:31)='d'
+      p_0
+        pm@(2:1-2:2)='x'
+      p_0
+        pm@(2:3-2:4)='y'
+      p_1
+        err@(2:5-2:6)='z'
+      p_0
+        pm@(2:7-2:10)='xyz'
+      p_0
+        pm@(2:11-2:12)='x'
+      p_1
+        err@(2:12-2:13)='z'
+      p_0
+        pm@(2:14-2:16)='xy'
+      p_0
+        pm@(2:17-2:19)='yz'
+      p_0
+        pm@(2:20-2:25)='xxyyz'
+      p_1
+        err@(2:25-2:26)='z'
+  Eof@(3:1-3:1)=''
index 337750b..c615d41 100644 (file)
        <body>
                <nav class='navbar navbar-default navbar-fixed-top'>
                        <div class='container-fluid'>
-                               <div class='col-xs-3 navbar-header'>
-                                       <a class='navbar-brand' ng-href='/'>Nitdoc</a>
+                               <div class='col-xs-3'>
+                                       <div class='navbar-header'>
+                                               <a class='navbar-brand' ng-href='/'>Nitdoc</a>
+                                       </div>
+                                       <ul class="nav navbar-nav">
+                                               <li><a href="/docdown?edit=true">DocDown</a></li>
+                                       </ul>
                                </div>
                                <div class='col-xs-7'>
                                        <form ng-controller='SearchCtrl as searchCtrl' >
@@ -64,5 +69,6 @@
                <script src='/javascripts/entities.js'></script>
                <script src='/javascripts/ui.js'></script>
                <script src='/javascripts/index.js'></script>
+               <script src='/javascripts/docdown.js'></script>
        </body>
 </html>
diff --git a/share/nitweb/javascripts/docdown.js b/share/nitweb/javascripts/docdown.js
new file mode 100644 (file)
index 0000000..365b8c9
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       angular
+               .module('docdown', ['model', 'ngSanitize'])
+
+               .controller('DocdownCtrl', ['$routeParams', '$sce', '$scope', '$location', 'DocDown', function($routeParams, $sce, $scope, $location, DocDown) {
+
+                       this.updateSnippet = function() {
+                               this.updateLink();
+                               this.updateHtml();
+                       }
+
+                       this.updateLink = function() {
+                               $scope.link = $location.protocol()+ '://' + $location.host() + ':' +
+                                       $location.port() + $location.path() + '?snippet=' +
+                                       encodeURIComponent(btoa($scope.markdown));
+                       }
+
+                       this.updateHtml = function() {
+                               DocDown.postMarkdown($scope.markdown,
+                                       function(data) {
+                                               $scope.html = $sce.trustAsHtml(data);
+                                       }, function(err) {
+                                               $scope.error = err;
+                                       });
+                       };
+
+                       this.editMode = function(isEdit) {
+                               $scope.edit = isEdit;
+                       }
+
+                       $scope.markdown = 'Type some markdown...';
+                       if($location.search().snippet) {
+                               $scope.markdown = atob($location.search().snippet);
+                       }
+                       $scope.edit = false;
+                       if($location.search().edit) {
+                               $scope.edit = Boolean($location.search().edit);
+                       }
+
+                       this.updateSnippet();
+               }])
+})();
index 8812a7a..3accab2 100644 (file)
                                },
                        }
                }])
+
+               .factory('DocDown', [ '$http', function($http) {
+                       return {
+                               postMarkdown: function(md, cb, cbErr) {
+                                       $http.post(apiUrl + '/docdown', md)
+                                               .success(cb)
+                                               .error(cbErr);
+                               }
+                       }
+               }])
 })();
index 99034a7..7fa1142 100644 (file)
@@ -15,7 +15,7 @@
  */
 
 (function() {
-       angular.module('nitweb', ['ngRoute', 'ngSanitize', 'angular-loading-bar', 'entities', 'index'])
+       angular.module('nitweb', ['ngRoute', 'ngSanitize', 'angular-loading-bar', 'entities', 'docdown', 'index'])
        .config(['cfpLoadingBarProvider', function(cfpLoadingBarProvider) {
                cfpLoadingBarProvider.includeSpinner = false;
        }])
                                controller: 'IndexCtrl',
                                controllerAs: 'indexCtrl'
                        })
+                       .when('/docdown', {
+                               templateUrl: 'views/docdown.html',
+                               controller: 'DocdownCtrl',
+                               controllerAs: 'docdownCtrl'
+                       })
                        .when('/doc/:id', {
                                templateUrl: 'views/doc.html',
                                controller: 'EntityCtrl',
diff --git a/share/nitweb/views/docdown.html b/share/nitweb/views/docdown.html
new file mode 100644 (file)
index 0000000..75fe6df
--- /dev/null
@@ -0,0 +1,33 @@
+<div class='container-fluid'>
+       <div class='page-header'>
+               <h2>Docdown snippets</h2>
+               <p class='text-muted'>Sharable documentation snippets.</p>
+               <div class="input-group">
+                       <span ng-if='edit' class="input-group-btn">
+                               <button class='btn btn-success' ng-click='docdownCtrl.editMode(false)'>
+                                       <span class='glyphicon glyphicon-link' /> View
+                               </button>
+                       </span>
+                       <span ng-if='!edit' class="input-group-btn">
+                               <button class='btn btn-success' ng-click='docdownCtrl.editMode(true)'>
+                                       <span class='glyphicon glyphicon-edit' /> Edit
+                               </button>
+                       </span>
+                       <input class='form-control' type='text' ng-model='link' />
+               </div>
+       </div>
+       <div class='row'>
+               <div ng-show='edit' class='col-xs-6'>
+                       <div class='card'>
+                               <textarea ng-model='markdown' ng-model-options='{ debounce: 100 }' ng-change='docdownCtrl.updateSnippet()' class='form-control' rows='20'></textarea>
+                       </div>
+               </div>
+               <div ng-class='edit ? "col-xs-6" : "col-xs-12"'>
+                       <div class='card'>
+                               <div class='card-body nitdoc'>
+                                       <div ng-bind-html='html' />
+                               </div>
+                       </div>
+               </div>
+       </div>
+</div>
index e69bb38..1537704 100644 (file)
@@ -19,8 +19,6 @@
 # * `nitdoc` wikilinks like `[[doc: MEntity::name]]`
 module doc_commands
 
-import doc_base
-
 # A command aimed at a documentation tool like `nitdoc` or `nitx`.
 #
 # `DocCommand` are generally of the form `command: args`.
@@ -57,6 +55,8 @@ interface DocCommand
                        return new CallCommand(command_string)
                else if command_string.has_prefix("code:") then
                        return new CodeCommand(command_string)
+               else if command_string.has_prefix("graph:") then
+                       return new GraphCommand(command_string)
                end
                return new UnknownCommand(command_string)
        end
@@ -153,3 +153,11 @@ end
 class CodeCommand
        super AbstractDocCommand
 end
+
+# A `DocCommand` that display an graph for a `MEntity`.
+#
+# Syntax:
+# * `graph: MEntity::name`
+class GraphCommand
+       super AbstractDocCommand
+end
index 2e4e1d2..3897c5c 100644 (file)
@@ -103,6 +103,7 @@ class APIRouter
                use("/defs/:id", new APIEntityDefs(model, mainmodule))
                use("/inheritance/:id", new APIEntityInheritance(model, mainmodule))
                use("/graph/", new APIGraphRouter(model, mainmodule))
+               use("/docdown/", new APIDocdown(model, mainmodule, modelbuilder))
        end
 end
 
diff --git a/src/web/api_docdown.nit b/src/web/api_docdown.nit
new file mode 100644 (file)
index 0000000..3510b64
--- /dev/null
@@ -0,0 +1,272 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Nitdoc specific Markdown format handling for Nitweb
+module api_docdown
+
+import api_graph
+intrude import doc_down
+intrude import markdown::wikilinks
+import doc_commands
+
+# Docdown handler accept docdown as POST data and render it as HTML
+class APIDocdown
+       super APIHandler
+
+       # Modelbuilder used by the commands
+       var modelbuilder: ModelBuilder
+
+       # Specific Markdown processor to use within Nitweb
+       var md_processor: MarkdownProcessor is lazy do
+               var proc = new MarkdownProcessor
+               proc.emitter.decorator = new NitwebDecorator(view, modelbuilder)
+               return proc
+       end
+
+       redef fun post(req, res) do
+               res.html md_processor.process(req.body)
+       end
+end
+
+# Specific Markdown decorator for Nitweb
+#
+# We reuse all the implementation of the NitdocDecorator and add the wikilinks handling.
+class NitwebDecorator
+       super NitdocDecorator
+
+       # View used by wikilink commands to find model entities
+       var view: ModelView
+
+       # Modelbuilder used to access code
+       var modelbuilder: ModelBuilder
+
+       redef fun add_wikilink(v, token) do
+               var link = token.link
+               if link == null then return
+               var cmd = new DocCommand(link.write_to_string)
+               cmd.render(v, token, view)
+       end
+end
+
+# Same as `InlineDecorator` but with wikilink commands handling
+class NitwebInlineDecorator
+       super InlineDecorator
+
+       # View used by wikilink commands to find model entities
+       var view: ModelView
+
+       # Modelbuilder used to access code
+       var modelbuilder: ModelBuilder
+
+       redef fun add_wikilink(v, token) do
+               var link = token.link
+               if link == null then return
+               var cmd = new DocCommand(link.write_to_string)
+               cmd.render(v, token, view)
+       end
+end
+
+redef interface DocCommand
+
+       # Emit the HTML related to the execution of this doc command
+       fun render(v: MarkdownEmitter, token: TokenWikiLink, model: ModelView) do
+               write_error(v, "Not yet implemented command `{token.link or else "null"}`")
+       end
+
+       # Find the MEntity ` with `full_name`.
+       fun find_mentity(model: ModelView, full_name: nullable String): nullable MEntity do
+               if full_name == null then return null
+               return model.mentity_by_full_name(full_name.from_percent_encoding)
+       end
+
+       # Write a warning in the output
+       fun write_warning(v: MarkdownEmitter, text: String) do
+               v.emit_text "<p class='text-warning'>Warning: {text}</p>"
+       end
+
+       # Write an error in the output
+       fun write_error(v: MarkdownEmitter, text: String) do
+               v.emit_text "<p class='text-danger'>Error: {text}</p>"
+       end
+
+       # Write a link to a mentity in the output
+       fun write_mentity_link(v: MarkdownEmitter, mentity: MEntity) do
+               var link = mentity.web_url
+               var name = mentity.name
+               var mdoc = mentity.mdoc_or_fallback
+               var comment = null
+               if mdoc != null then comment = mdoc.synopsis
+               v.decorator.add_link(v, link, name, comment)
+       end
+end
+
+redef class UnknownCommand
+       redef fun render(v, token, model) do
+               var link = token.link
+               if link == null then
+                       write_error(v, "Empty command")
+                       return
+               end
+               var full_name = link.write_to_string
+               var mentity = find_mentity(model, full_name)
+               if mentity == null then
+                       write_error(v, "Unknown command `{link}`")
+                       return
+               end
+               write_mentity_link(v, mentity)
+       end
+end
+
+redef class ArticleCommand
+       redef fun render(v, token, model) do
+               if args.is_empty then
+                       write_error(v, "Expected one arg: the MEntity name")
+                       return
+               end
+               var name = args.first
+               var mentity = find_mentity(model, name)
+               if mentity == null then
+                       write_error(v, "No MEntity found for name `{name}`")
+                       return
+               end
+               var mdoc = mentity.mdoc_or_fallback
+               if mdoc == null then
+                       write_warning(v, "No MDoc for mentity `{name}`")
+                       return
+               end
+               v.add "<h3>"
+               write_mentity_link(v, mentity)
+               v.add " - "
+               v.emit_text mdoc.synopsis
+               v.add "</h3>"
+               v.add v.processor.process(mdoc.comment).write_to_string
+       end
+end
+
+redef class CommentCommand
+       redef fun render(v, token, model) do
+               if args.is_empty then
+                       write_error(v, "Expected one arg: the MEntity name")
+                       return
+               end
+               var name = args.first
+               var mentity = find_mentity(model, name)
+               if mentity == null then
+                       write_error(v, "No MEntity found for name `{name}`")
+                       return
+               end
+               var mdoc = mentity.mdoc_or_fallback
+               if mdoc == null then
+                       write_warning(v, "No MDoc for mentity `{name}`")
+                       return
+               end
+               v.add v.processor.process(mdoc.comment).write_to_string
+       end
+end
+
+redef class ListCommand
+       redef fun render(v, token, model) do
+               if args.is_empty then
+                       write_error(v, "Expected one arg: the MEntity name")
+                       return
+               end
+               var name = args.first
+               var mentity = find_mentity(model, name)
+               if mentity isa MPackage then
+                       write_list(v, mentity.mgroups)
+               else if mentity isa MGroup then
+                       var res = new Array[MEntity]
+                       res.add_all mentity.in_nesting.smallers
+                       res.add_all mentity.mmodules
+                       write_list(v, res)
+               else if mentity isa MModule then
+                       write_list(v, mentity.mclassdefs)
+               else if mentity isa MClass then
+                       write_list(v, mentity.collect_intro_mproperties(model))
+               else if mentity isa MClassDef then
+                       write_list(v, mentity.mpropdefs)
+               else if mentity isa MProperty then
+                       write_list(v, mentity.mpropdefs)
+               else
+                       write_error(v, "No list found for name `{name}`")
+               end
+       end
+
+       # Write a mentity list in the output
+       fun write_list(v: MarkdownEmitter, mentities: Collection[MEntity]) do
+               v.add "<ul>"
+               for mentity in mentities do
+                       var mdoc = mentity.mdoc_or_fallback
+                       v.add "<li>"
+                       write_mentity_link(v, mentity)
+                       if mdoc != null then
+                               v.add " - "
+                               v.emit_text mdoc.synopsis
+                       end
+                       v.add "</li>"
+               end
+               v.add "</ul>"
+       end
+end
+
+redef class CodeCommand
+       redef fun render(v, token, model) do
+               if args.is_empty then
+                       write_error(v, "Expected one arg: the MEntity name")
+                       return
+               end
+               var name = args.first
+               var mentity = find_mentity(model, name)
+               if mentity == null then
+                       write_error(v, "No MEntity found for name `{name}`")
+                       return
+               end
+               if mentity isa MClass then mentity = mentity.intro
+               if mentity isa MProperty then mentity = mentity.intro
+               var source = render_source(mentity, v.decorator.as(NitwebDecorator).modelbuilder)
+               if source == null then
+                       write_error(v, "No source for MEntity `{name}`")
+                       return
+               end
+               v.add "<pre>"
+               v.add source
+               v.add "</pre>"
+       end
+
+       # Highlight `mentity` source code.
+       private fun render_source(mentity: MEntity, modelbuilder: ModelBuilder): nullable HTMLTag do
+               var node = modelbuilder.mentity2node(mentity)
+               if node == null then return null
+               var hl = new HighlightVisitor
+               hl.enter_visit node
+               return hl.html
+       end
+end
+
+redef class GraphCommand
+       redef fun render(v, token, model) do
+               if args.is_empty then
+                       write_error(v, "Expected one arg: the MEntity name")
+                       return
+               end
+               var name = args.first
+               var mentity = find_mentity(model, name)
+               if mentity == null then
+                       write_error(v, "No MEntity found for name `{name}`")
+                       return
+               end
+               var g = new InheritanceGraph(mentity, model)
+               v.add g.draw(3, 3).to_svg
+       end
+end
index be5d2d1..e82a0f5 100644 (file)
@@ -18,3 +18,4 @@ module web
 import model_api
 import api_catalog
 import api_graph
+import api_docdown