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