markdown: trim indenting spaces from code-blocks in MDoc
[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 import parser
19 import html
20 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 >= 4 then
72 curblock.add(text)
73 continue
74 end
75
76 # Was a codblock just before the current line ?
77 close_codeblock(n or else root)
78
79 # fence opening
80 if text.substring(0,3) == "~~~" then
81 var l = 3
82 while l < text.length and text.chars[l] == '~' do l += 1
83 in_fence = text.substring(0, l)
84 continue
85 end
86
87 # Cleanup the string
88 text = text.trim
89
90 # The HTML node of the last line, so we know if we continue the same block
91 var old = n
92
93 # No line or loss of indentation: reset
94 if text.is_empty or indent < lastindent then
95 n = null
96 ul = null
97 if text.is_empty then continue
98 end
99
100 # Special first word: new paragraph
101 if text.has_prefix("TODO") or text.has_prefix("FIXME") then
102 n = new HTMLTag("p")
103 root.add n
104 n.add_class("todo")
105 ul = null
106 else if text.has_prefix("REQUIRE") or text.has_prefix("Require") or text.has_prefix("ENSURE") or text.has_prefix("Ensure") then
107 n = new HTMLTag("p")
108 root.add n
109 n.add_class("contract")
110 ul = null
111 end
112
113 # List
114 if text.has_prefix("* ") or text.has_prefix("- ") then
115 text = text.substring_from(1).trim
116 if ul == null then
117 ul = new HTMLTag("ul")
118 root.add ul
119 end
120 n = new HTMLTag("li")
121 ul.add(n)
122 end
123
124 # Nothing? then paragraph
125 if n == null then
126 n = new HTMLTag("p")
127 root.add n
128 ul = null
129 end
130
131 if old == n then
132 # Because spaces and `\n` where trimmed
133 n.append("\n")
134 end
135
136 process_line(n, text)
137
138 # Special case, the fist line is the synopsys and is in its own paragraph
139 if is_first_line then
140 n.add_class("synopsys")
141 n = null
142 is_first_line = false
143 end
144 end
145
146 # If the codeblock was the last code sequence
147 close_codeblock(n or else root)
148
149 return root
150 end
151
152 fun short_work(mdoc: MDoc): HTMLTag
153 do
154 var text = mdoc.content.first
155 var n = new HTMLTag("span")
156 n.add_class("synopsys")
157 n.add_class("nitdoc")
158 process_line(n, text)
159 return n
160 end
161
162 fun process_line(n: HTMLTag, text: String)
163 do
164 # Loosly detect code parts
165 var parts = text.split("`")
166
167 # Process each code parts, thus alternate between text and code
168 var is_text = true
169 for part in parts do
170 if is_text then
171 # Text part
172 n.append part
173 else
174 # Code part
175 var n2 = new HTMLTag("code")
176 n.add(n2)
177 process_code(n2, part)
178 end
179 is_text = not is_text
180 end
181 end
182
183 fun close_codeblock(root: HTMLTag)
184 do
185 # Is there a codeblock to manage?
186 if not curblock.is_empty then
187 # determine the smalest indent
188 var minindent = -1
189 for text in curblock do
190 var indent = 0
191 while indent < text.length and text.chars[indent] == ' ' do indent += 1
192 if minindent == -1 or indent < minindent then
193 minindent = indent
194 end
195 end
196
197 # Generate the text
198 var btext = new FlatBuffer
199 for text in curblock do
200 btext.append text.substring_from(minindent)
201 btext.add '\n'
202 end
203
204 # add the node
205 var n = new HTMLTag("pre")
206 root.add(n)
207 process_code(n, btext.to_s)
208 curblock.clear
209 end
210 end
211
212 fun process_code(n: HTMLTag, text: String)
213 do
214 # Try to parse it
215 var ast = toolcontext.parse_something(text)
216
217 if ast isa AError then
218 n.append text
219 # n.attrs["title"] = ast.message
220 n.add_class("rawcode")
221 else
222 var v = new HighlightVisitor
223 v.enter_visit(ast)
224 n.add(v.html)
225 n.add_class("nitcode")
226 end
227 end
228 end
229
230 redef class MDoc
231 # Build a `<div>` element that contains the full documentation in HTML
232 fun full_markdown: HTMLTag
233 do
234 var res = full_markdown_cache
235 if res != null then return res
236 var tc = new ToolContext
237 var d2m = new Doc2Mdwn(tc)
238 res = d2m.work(self)
239 full_markdown_cache = res
240 return res
241 end
242
243 private var full_markdown_cache: nullable HTMLTag
244
245 # Build a `<span>` element that contains the synopsys in HTML
246 fun short_markdown: HTMLTag
247 do
248 var res = short_markdown_cache
249 if res != null then return res
250 var tc = new ToolContext
251 var d2m = new Doc2Mdwn(tc)
252 res = d2m.short_work(self)
253 short_markdown_cache = res
254 return res
255 end
256
257 private var short_markdown_cache: nullable HTMLTag
258 end