b8a88e35d0396e0b83df322a4f9169409942ca8b
[nit.git] / lib / markdown2 / markdown_html_rendering.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 # HTML rendering of Markdown documents
16 module markdown_html_rendering
17
18 import markdown_rendering
19
20 # Markdown document renderer to HTML
21 class HtmlRenderer
22 super MdRenderer
23
24 # HTML output under construction
25 private var html: Buffer is noinit
26
27 # Render `document` as HTML
28 redef fun render(document) do
29 reset
30 enter_visit(document)
31 return html.write_to_string
32 end
33
34 redef fun visit(node) do node.render_html(self)
35
36 # Reset `headings` and internal state
37 fun reset do
38 html = new Buffer
39 if enable_heading_ids then headings.clear
40 end
41
42 # Last char visited
43 #
44 # Used to avoid double blank lines.
45 private var last_char: nullable Char = null
46
47 # Add `string` to `html`
48 private fun add(string: String) do
49 html.append(string)
50 if not html.is_empty then
51 last_char = html.last
52 end
53 end
54
55 # Add a raw `html` string to the output
56 #
57 # Raw means that the string will not be escaped.
58 fun add_raw(html: String) do add html
59
60 # Add `text` string to the output
61 #
62 # The string will be escaped.
63 fun add_text(text: String) do add html_escape(text, true)
64
65 # Add a blank line to the output
66 fun add_line do
67 if last_char != null and last_char != '\n' then
68 add "\n"
69 end
70 end
71
72 # Escape `string` to HTML
73 #
74 # When `keep_entities`, HTML entities will not be escaped.
75 fun html_escape(string: String, keep_entities: Bool): String do
76 var buf: nullable Buffer = null
77 for i in [0..string.length[ do
78 var c = string.chars[i]
79 var sub = null
80 if c == '&' and (not keep_entities or string.search_from(re_entity, i) == null) then
81 sub = "&"
82 else if c == '<' then
83 sub = "&lt;"
84 else if c == '>' then
85 sub = "&gt;"
86 else if c == '"' then
87 sub = "&quot;"
88 else
89 if buf != null then buf.add c
90 continue
91 end
92 if buf == null then
93 buf = new Buffer
94 for j in [0..i[ do buf.add string.chars[j]
95 end
96 buf.append sub
97 end
98
99 if buf == null then return string
100 return buf.to_s
101 end
102
103 # HTML entity pattern
104 private var re_entity: Regex = "^&(#x[a-f0-9]\{1,8\}|#[0-9]\{1,8\}|[a-z][a-z0-9]\{1,31\});".to_re
105
106 # Encode the `uri` string
107 fun encode_uri(uri: String): String do
108 var buf = new Buffer
109
110 var i = 0
111 while i < uri.length do
112 var c = uri.chars[i]
113 if (c >= '0' and c <= '9') or
114 (c >= 'a' and c <= 'z') or
115 (c >= 'A' and c <= 'Z') or
116 c == ';' or c == ',' or c == '/' or c == '?' or
117 c == ':' or c == '@' or c == '=' or c == '+' or
118 c == '$' or c == '-' or c == '_' or c == '.' or
119 c == '!' or c == '~' or c == '*' or c == '(' or
120 c == ')' or c == '#' or c == '\''
121 then
122 buf.add c
123 else if c == '&' then
124 buf.append "&amp;"
125 else if c == '%' and uri.search_from(re_uri_code, i) != null then
126 buf.append uri.substring(i, 3)
127 i += 2
128 else
129 var bytes = c.to_s.bytes
130 for b in bytes do buf.append "%{b.to_i.to_hex}".to_upper
131 end
132 i += 1
133 end
134
135 return buf.to_s
136 end
137
138 # URI encode pattern
139 private var re_uri_code: Regex = "^%[a-zA-Z0-9]\{2\}".to_re
140
141 # Add `id` tags to headings
142 var enable_heading_ids = false is optional, writable
143
144 # Associate headings ids to blocks
145 var headings = new ArrayMap[String, MdHeading]
146
147 # Strip heading id
148 fun strip_id(text: String): String do
149 # strip id
150 var b = new FlatBuffer
151 for c in text do
152 if c == ' ' then
153 b.add '_'
154 else
155 if not c.is_letter and
156 not c.is_digit and
157 not allowed_id_chars.has(c) then continue
158 b.add c
159 end
160 end
161 var res = b.to_s
162 if res.is_empty then res = "_"
163 var key = res
164 # check for multiple id definitions
165 if headings.has_key(key) then
166 var i = 1
167 key = "{res}_{i}"
168 while headings.has_key(key) do
169 i += 1
170 key = "{res}_{i}"
171 end
172 end
173 return key
174 end
175
176 # Allowed characters in ids
177 var allowed_id_chars: Array[Char] = ['-', '_', ':', '.']
178 end
179
180 redef class MdNode
181
182 # Render `self` as HTML
183 fun render_html(v: HtmlRenderer) do visit_all(v)
184 end
185
186 # Blocks
187
188 redef class MdBlockQuote
189 redef fun render_html(v) do
190 v.add_line
191 v.add_raw "<blockquote>"
192 v.add_line
193 visit_all(v)
194 v.add_line
195 v.add_raw "</blockquote>"
196 v.add_line
197 end
198 end
199
200 redef class MdCodeBlock
201 redef fun render_html(v) do
202 var info = self.info
203 v.add_line
204 v.add_raw "<pre>"
205 v.add_raw "<code"
206 if info != null and not info.is_empty then
207 v.add_raw " class=\"language-{info.split(" ").first}\""
208 end
209 v.add_raw ">"
210 var literal = self.literal or else ""
211 var lines = literal.split("\n")
212 for i in [0..lines.length[ do
213 var line = lines[i]
214 v.add_raw v.html_escape(line, false)
215 if i < lines.length - 1 then
216 v.add_raw "\n"
217 end
218 end
219 v.add_raw "</code>"
220 v.add_raw "</pre>"
221 v.add_line
222 end
223 end
224
225 redef class MdHeading
226 redef fun render_html(v) do
227 v.add_line
228 if v.enable_heading_ids then
229 var id = self.id
230 if id == null then
231 id = v.strip_id(title)
232 v.headings[id] = self
233 self.id = id
234 end
235 v.add_raw "<h{level} id=\"{id}\">"
236 else
237 v.add_raw "<h{level}>"
238 end
239 visit_all(v)
240 v.add_raw "</h{level}>"
241 v.add_line
242 end
243
244 #
245 var id: nullable String = null
246
247 #
248 fun title: String do
249 var v = new RawTextVisitor
250 return v.render(self)
251 end
252 end
253
254 redef class MdUnorderedList
255 redef fun render_html(v) do
256 v.add_line
257 v.add_raw "<ul>"
258 v.add_line
259 visit_all(v)
260 v.add_line
261 v.add_raw "</ul>"
262 v.add_line
263 end
264 end
265
266 redef class MdOrderedList
267 redef fun render_html(v) do
268 var start = self.start_number
269 v.add_line
270 v.add_raw "<ol"
271 if start != 1 then
272 v.add_raw " start=\"{start}\""
273 end
274 v.add_raw ">"
275 v.add_line
276 visit_all(v)
277 v.add_line
278 v.add_raw "</ol>"
279 v.add_line
280 end
281 end
282
283 redef class MdListItem
284 redef fun render_html(v) do
285 v.add_raw "<li>"
286 visit_all(v)
287 v.add_raw "</li>"
288 v.add_line
289 end
290 end
291
292 redef class MdParagraph
293 redef fun render_html(v) do
294 var is_tight = is_in_tight_list
295 if not is_tight then
296 v.add_line
297 v.add_raw "<p>"
298 end
299 visit_all(v)
300 if not is_tight then
301 v.add_raw "</p>"
302 v.add_line
303 end
304 end
305 end
306
307 redef class MdThematicBreak
308 redef fun render_html(v) do
309 v.add_line
310 v.add_raw "<hr />"
311 v.add_line
312 end
313 end
314
315 redef class MdHtmlBlock
316 redef fun render_html(v) do
317 v.add_line
318 var literal = self.literal or else ""
319 var lines = literal.split("\n")
320 for i in [0..lines.length[ do
321 var line = lines[i]
322 if not line.trim.is_empty then
323 v.add_raw line
324 end
325 if i < lines.length - 1 then
326 v.add_raw "\n"
327 end
328 end
329 v.add_line
330 end
331 end
332
333 # Inlines
334
335 redef class MdHardLineBreak
336 redef fun render_html(v) do
337 v.add_raw "<br />"
338 v.add_line
339 end
340 end
341
342 redef class MdSoftLineBreak
343 redef fun render_html(v) do
344 v.add_raw "\n"
345 end
346 end
347
348 redef class MdCode
349 redef fun render_html(v) do
350 v.add_raw "<code>"
351 v.add_raw v.html_escape(literal, false)
352 v.add_raw "</code>"
353 end
354 end
355
356 redef class MdEmphasis
357 redef fun render_html(v) do
358 v.add_raw "<em>"
359 visit_all(v)
360 v.add_raw "</em>"
361 end
362 end
363
364 redef class MdStrongEmphasis
365 redef fun render_html(v) do
366 v.add_raw "<strong>"
367 visit_all(v)
368 v.add_raw "</strong>"
369 end
370 end
371
372 redef class MdHtmlInline
373 redef fun render_html(v) do
374 v.add_raw literal
375 end
376 end
377
378 redef class MdImage
379 redef fun render_html(v) do
380 var url = self.destination
381 var title = self.title
382 v.add_raw "<img"
383 v.add_raw " src=\"{v.encode_uri(url)}\""
384
385 var alt_text = self.alt_text
386 v.add_raw " alt=\"{alt_text}\""
387
388 if title != null and not title.is_empty then
389 v.add_raw " title=\""
390 v.add_text title
391 v.add_raw "\""
392 end
393
394 v.add_raw " />"
395 end
396
397 private fun alt_text: String do
398 var v = new RawTextVisitor
399 return v.render(self)
400 end
401 end
402
403 redef class MdLink
404 redef fun render_html(v) do
405 var url = self.destination
406 var title = self.title
407 v.add_raw "<a"
408 v.add_raw " href=\"{v.encode_uri(url)}\""
409 if title != null and not title.is_empty then
410 v.add_raw " title=\""
411 v.add_text title
412 v.add_raw "\""
413 end
414 v.add_raw ">"
415 visit_all(v)
416 v.add_raw "</a>"
417 end
418 end
419
420 redef class MdText
421 redef fun render_html(v) do
422 v.add_text literal
423 end
424 end