90cb227ae70131a377c7ea095b315ad207a6a74c
[nit.git] / src / markdown.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Transform Nit verbatim documentation into HTML
16 module markdown
17
18 private import parser
19 import html
20 private import highlight
21
22 # The class that does the convertion from a `ADoc` to HTML
23 private class Doc2Mdwn
24 var toolcontext: ToolContext
25
26 # The lines of the current code block, empty is no current code block
27 var curblock = new Array[String]
28
29 fun work(mdoc: MDoc): HTMLTag
30 do
31 var root = new HTMLTag("div")
32 root.add_class("nitdoc")
33
34 # Indent level of the previous line
35 var lastindent = 0
36
37 # Indent level of the current line
38 var indent = 0
39
40 # Expected fencing closing tag (if any)
41 var in_fence: nullable String = null
42
43 # The current element (p, li, etc.) if any
44 var n: nullable HTMLTag = null
45
46 # The current ul element (if any)
47 var ul: nullable HTMLTag = null
48
49 var is_first_line = true
50 # Local variable to benefit adaptive typing
51 for text in mdoc.content do
52 # Count the number of spaces
53 lastindent = indent
54 indent = 0
55 while text.length > indent and text.chars[indent] == ' ' do indent += 1
56
57 # In a fence
58 if in_fence != null then
59 # fence closing
60 if text.substring(0,in_fence.length) == in_fence then
61 close_codeblock(n or else root)
62 in_fence = null
63 continue
64 end
65 # else fence content
66 curblock.add(text)
67 continue
68 end
69
70 # Is codeblock? Then just collect them
71 if indent >= 3 then
72 # to allows 4 spaces including the one that follows the #
73 curblock.add(text)
74 continue
75 end
76
77 # Was a codblock just before the current line ?
78 close_codeblock(n or else root)
79
80 # fence opening
81 if text.substring(0,3) == "~~~" then
82 var l = 3
83 while l < text.length and text.chars[l] == '~' do l += 1
84 in_fence = text.substring(0, l)
85 continue
86 end
87
88 # Cleanup the string
89 text = text.trim
90
91 # The HTML node of the last line, so we know if we continue the same block
92 var old = n
93
94 # No line or loss of indentation: reset
95 if text.is_empty or indent < lastindent then
96 n = null
97 ul = null
98 if text.is_empty then continue
99 end
100
101 # Special first word: new paragraph
102 if text.has_prefix("TODO") or text.has_prefix("FIXME") then
103 n = new HTMLTag("p")
104 root.add n
105 n.add_class("todo")
106 ul = null
107 else if text.has_prefix("REQUIRE") or text.has_prefix("Require") or text.has_prefix("ENSURE") or text.has_prefix("Ensure") then
108 n = new HTMLTag("p")
109 root.add n
110 n.add_class("contract")
111 ul = null
112 end
113
114 # List
115 if text.has_prefix("* ") or text.has_prefix("- ") then
116 text = text.substring_from(1).trim
117 if ul == null then
118 ul = new HTMLTag("ul")
119 root.add ul
120 end
121 n = new HTMLTag("li")
122 ul.add(n)
123 end
124
125 # Nothing? then paragraph
126 if n == null then
127 n = new HTMLTag("p")
128 root.add n
129 ul = null
130 end
131
132 if old == n then
133 # Because spaces and `\n` where trimmed
134 n.append("\n")
135 end
136
137 process_line(n, text)
138
139 # Special case, the fist line is the synopsys and is in its own paragraph
140 if is_first_line then
141 n.add_class("synopsys")
142 n = null
143 is_first_line = false
144 end
145 end
146
147 # If the codeblock was the last code sequence
148 close_codeblock(n or else root)
149
150 return root
151 end
152
153 fun short_work(mdoc: MDoc): HTMLTag
154 do
155 var text = mdoc.content.first
156 var n = new HTMLTag("span")
157 n.add_class("synopsys")
158 n.add_class("nitdoc")
159 process_line(n, text)
160 return n
161 end
162
163 fun process_line(n: HTMLTag, text: String)
164 do
165 # Loosly detect code parts
166 var parts = text.split("`")
167
168 # Process each code parts, thus alternate between text and code
169 var is_text = true
170 for part in parts do
171 if is_text then
172 # Text part
173 n.append part
174 else
175 # Code part
176 var n2 = new HTMLTag("code")
177 n.add(n2)
178 process_code(n2, part)
179 end
180 is_text = not is_text
181 end
182 end
183
184 fun close_codeblock(root: HTMLTag)
185 do
186 # Is there a codeblock to manage?
187 if not curblock.is_empty then
188 # determine the smalest indent
189 var minindent = -1
190 for text in curblock do
191 var indent = 0
192 while indent < text.length and text.chars[indent] == ' ' do indent += 1
193 if minindent == -1 or indent < minindent then
194 minindent = indent
195 end
196 end
197
198 # Generate the text
199 var btext = new FlatBuffer
200 for text in curblock do
201 btext.append text.substring_from(minindent)
202 btext.add '\n'
203 end
204
205 # add the node
206 var n = new HTMLTag("pre")
207 root.add(n)
208 process_code(n, btext.to_s)
209 curblock.clear
210 end
211 end
212
213 fun process_code(n: HTMLTag, text: String)
214 do
215 # Try to parse it
216 var ast = toolcontext.parse_something(text)
217
218 if ast isa AError then
219 n.append text
220 # n.attrs["title"] = ast.message
221 n.add_class("rawcode")
222 else
223 var v = new HighlightVisitor
224 v.enter_visit(ast)
225 n.add(v.html)
226 n.add_class("nitcode")
227 end
228 end
229 end
230
231 redef class MDoc
232 # Build a `<div>` element that contains the full documentation in HTML
233 fun full_markdown: HTMLTag
234 do
235 var res = full_markdown_cache
236 if res != null then return res
237 var tc = new ToolContext
238 var d2m = new Doc2Mdwn(tc)
239 res = d2m.work(self)
240 full_markdown_cache = res
241 return res
242 end
243
244 private var full_markdown_cache: nullable HTMLTag
245
246 # Build a `<span>` element that contains the synopsys in HTML
247 fun short_markdown: HTMLTag
248 do
249 var res = short_markdown_cache
250 if res != null then return res
251 var tc = new ToolContext
252 var d2m = new Doc2Mdwn(tc)
253 res = d2m.short_work(self)
254 short_markdown_cache = res
255 return res
256 end
257
258 private var short_markdown_cache: nullable HTMLTag
259 end