Rename REAMDE to README.md
[nit.git] / src / docdown.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 docdown
17
18 private import parser
19 import html
20 private import highlight
21 private import parser_util
22
23 # The class that does the convertion from a `ADoc` to HTML
24 private class Doc2Mdwn
25 var toolcontext: ToolContext
26
27 # The lines of the current code block, empty is no current code block
28 var curblock = new Array[String]
29
30 # Count empty lines between code blocks
31 var empty_lines = 0
32
33 # Optional tag for a fence
34 var fence_tag = ""
35
36 fun work(mdoc: MDoc): HTMLTag
37 do
38 var root = new HTMLTag("div")
39 root.add_class("nitdoc")
40
41 # Indent level of the previous line
42 var lastindent = 0
43
44 # Indent level of the current line
45 var indent = 0
46
47 # Expected fencing closing tag (if any)
48 var in_fence: nullable String = null
49
50 # The current element (p, li, etc.) if any
51 var n: nullable HTMLTag = null
52
53 # The current ul element (if any)
54 var ul: nullable HTMLTag = null
55
56 var is_first_line = true
57 # Local variable to benefit adaptive typing
58 for text in mdoc.content do
59 # Count the number of spaces
60 lastindent = indent
61 indent = 0
62 while text.length > indent and text.chars[indent] == ' ' do indent += 1
63
64 # In a fence
65 if in_fence != null then
66 # fence closing
67 if text.substring(0,in_fence.length) == in_fence then
68 close_codeblock(n or else root)
69 in_fence = null
70 continue
71 end
72 # else fence content
73 curblock.add(text)
74 continue
75 end
76
77 # Is codeblock? Then just collect them
78 if indent >= 3 then
79 for i in [0..empty_lines[ do curblock.add("")
80 empty_lines = 0
81 # to allows 4 spaces including the one that follows the #
82 curblock.add(text)
83 fence_tag = ""
84 continue
85 end
86
87 # fence opening
88 if text.substring(0,3) == "~~~" then
89 # Was a codblock just before the current line ?
90 close_codeblock(n or else root)
91
92 var l = 3
93 while l < text.length and text.chars[l] == '~' do l += 1
94 in_fence = text.substring(0, l)
95 while l < text.length and (text.chars[l] == '.' or text.chars[l] == ' ') do l += 1
96 fence_tag = text.substring_from(l)
97 continue
98 end
99
100 # Cleanup the string
101 text = text.trim
102
103 # The HTML node of the last line, so we know if we continue the same block
104 var old = n
105
106 # No line or loss of indentation: reset
107 if text.is_empty or indent < lastindent then
108 n = null
109 ul = null
110 if text.is_empty then
111 if not curblock.is_empty then empty_lines += 1
112 continue
113 end
114 end
115
116 # Was a codblock just before the current line ?
117 close_codeblock(n or else root)
118
119 # Special first word: new paragraph
120 if text.has_prefix("TODO") or text.has_prefix("FIXME") then
121 n = new HTMLTag("p")
122 root.add n
123 n.add_class("todo")
124 ul = null
125 else if text.has_prefix("REQUIRE") or text.has_prefix("Require") or text.has_prefix("ENSURE") or text.has_prefix("Ensure") then
126 n = new HTMLTag("p")
127 root.add n
128 n.add_class("contract")
129 ul = null
130 end
131
132 # List
133 if text.has_prefix("* ") or text.has_prefix("- ") then
134 text = text.substring_from(1).trim
135 if ul == null then
136 ul = new HTMLTag("ul")
137 root.add ul
138 end
139 n = new HTMLTag("li")
140 ul.add(n)
141 end
142
143 # Nothing? then paragraph
144 if n == null then
145 n = new HTMLTag("p")
146 root.add n
147 ul = null
148 end
149
150 if old == n then
151 # Because spaces and `\n` where trimmed
152 n.append("\n")
153 end
154
155 process_line(n, text)
156
157 # Special case, the fist line is the synopsys and is in its own paragraph
158 if is_first_line then
159 n.add_class("synopsys")
160 n = null
161 is_first_line = false
162 end
163 end
164
165 # If the codeblock was the last code sequence
166 close_codeblock(n or else root)
167
168 return root
169 end
170
171 fun short_work(mdoc: MDoc): HTMLTag
172 do
173 var text = mdoc.content.first
174 var n = new HTMLTag("span")
175 n.add_class("synopsys")
176 n.add_class("nitdoc")
177 process_line(n, text)
178 return n
179 end
180
181 fun process_line(n: HTMLTag, text: String)
182 do
183 # Loosly detect code parts
184 var parts = text.split("`")
185
186 # Process each code parts, thus alternate between text and code
187 var is_text = true
188 for part in parts do
189 if is_text then
190 # Text part
191 n.append part
192 else
193 # Code part
194 var n2 = new HTMLTag("code")
195 n.add(n2)
196 process_code(n2, part, null)
197 end
198 is_text = not is_text
199 end
200 end
201
202 fun close_codeblock(root: HTMLTag)
203 do
204 # Is there a codeblock to manage?
205 if not curblock.is_empty then
206 empty_lines = 0
207
208 # determine the smalest indent
209 var minindent = -1
210 for text in curblock do
211 var indent = 0
212 while indent < text.length and text.chars[indent] == ' ' do indent += 1
213 # skip white lines
214 if indent >= text.length then continue
215 if minindent == -1 or indent < minindent then
216 minindent = indent
217 end
218 end
219 if minindent < 0 then minindent = 0
220
221 # Generate the text
222 var btext = new FlatBuffer
223 for text in curblock do
224 btext.append text.substring_from(minindent)
225 btext.add '\n'
226 end
227
228 # add the node
229 var n = new HTMLTag("pre")
230 root.add(n)
231 process_code(n, btext.to_s, fence_tag)
232 curblock.clear
233 end
234 end
235
236 fun process_code(n: HTMLTag, text: String, tag: nullable String)
237 do
238 # Do not try to highlight non-nit code.
239 if tag != null and tag != "" and tag != "nit" and tag != "nitish" then
240 n.append text
241 n.add_class("rawcode")
242 return
243 end
244
245 # Try to parse it
246 var ast = toolcontext.parse_something(text)
247
248 if ast isa AError then
249 n.append text
250 # n.attrs["title"] = ast.message
251 n.add_class("rawcode")
252 else
253 var v = new HighlightVisitor
254 v.enter_visit(ast)
255 n.add(v.html)
256 n.add_class("nitcode")
257 end
258 end
259 end
260
261 redef class MDoc
262 # Build a `<div>` element that contains the full documentation in HTML
263 fun full_markdown: HTMLTag
264 do
265 var res = full_markdown_cache
266 if res != null then return res
267 var tc = new ToolContext
268 var d2m = new Doc2Mdwn(tc)
269 res = d2m.work(self)
270 full_markdown_cache = res
271 return res
272 end
273
274 private var full_markdown_cache: nullable HTMLTag
275
276 # Build a `<span>` element that contains the synopsys in HTML
277 fun short_markdown: HTMLTag
278 do
279 var res = short_markdown_cache
280 if res != null then return res
281 var tc = new ToolContext
282 var d2m = new Doc2Mdwn(tc)
283 res = d2m.short_work(self)
284 short_markdown_cache = res
285 return res
286 end
287
288 private var short_markdown_cache: nullable HTMLTag
289 end