b8a88e35d0396e0b83df322a4f9169409942ca8b
1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # HTML rendering of Markdown documents
16 module markdown_html_rendering
18 import markdown_rendering
20 # Markdown document renderer to HTML
24 # HTML output under construction
25 private var html
: Buffer is noinit
27 # Render `document` as HTML
28 redef fun render
(document
) do
31 return html
.write_to_string
34 redef fun visit
(node
) do node
.render_html
(self)
36 # Reset `headings` and internal state
39 if enable_heading_ids
then headings
.clear
44 # Used to avoid double blank lines.
45 private var last_char
: nullable Char = null
47 # Add `string` to `html`
48 private fun add
(string
: String) do
50 if not html
.is_empty
then
55 # Add a raw `html` string to the output
57 # Raw means that the string will not be escaped.
58 fun add_raw
(html
: String) do add html
60 # Add `text` string to the output
62 # The string will be escaped.
63 fun add_text
(text
: String) do add html_escape
(text
, true)
65 # Add a blank line to the output
67 if last_char
!= null and last_char
!= '\n' then
72 # Escape `string` to HTML
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
]
80 if c
== '&' and (not keep_entities
or string
.search_from
(re_entity
, i
) == null) then
89 if buf
!= null then buf
.add c
94 for j
in [0..i
[ do buf
.add string
.chars
[j
]
99 if buf
== null then return string
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
106 # Encode the `uri` string
107 fun encode_uri
(uri
: String): String do
111 while i
< uri
.length
do
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
== '\''
123 else if c == '&' then
125 else if c == '%' and uri.search_from(re_uri_code, i) != null then
126 buf.append uri.substring(i, 3)
129 var bytes = c.to_s.bytes
130 for b in bytes do buf.append "%{b.to_i.to_hex}".to_upper
139 private var re_uri_code: Regex = "^%[a-zA-Z0-9]\{2\}".to_re
141 # Add `id` tags to headings
142 var enable_heading_ids = false is optional, writable
144 # Associate headings ids to blocks
145 var headings = new ArrayMap[String, MdHeading]
148 fun strip_id(text: String): String do
150 var b = new FlatBuffer
155 if not c.is_letter and
157 not allowed_id_chars.has(c) then continue
162 if res.is_empty then res = "_"
164 # check for multiple id definitions
165 if headings.has_key(key) then
168 while headings.has_key(key) do
176 # Allowed characters in ids
177 var allowed_id_chars: Array[Char] = ['-', '_
', ':', '.']
182 # Render `self` as HTML
183 fun render_html(v: HtmlRenderer) do visit_all(v)
188 redef class MdBlockQuote
189 redef fun render_html(v) do
191 v.add_raw "<blockquote>"
195 v.add_raw "</blockquote>"
200 redef class MdCodeBlock
201 redef fun render_html(v) do
206 if info != null and not info.is_empty then
207 v.add_raw " class=\"language-{info.split(" ").first}\""
210 var literal = self.literal or else ""
211 var lines = literal.split("\n")
212 for i in [0..lines.length[ do
214 v.add_raw v.html_escape(line, false)
215 if i < lines.length - 1 then
225 redef class MdHeading
226 redef fun render_html(v) do
228 if v.enable_heading_ids then
231 id = v.strip_id(title)
232 v.headings[id] = self
235 v.add_raw "<h{level} id=\"{id}\">"
237 v.add_raw "<h{level}>"
240 v.add_raw "</h{level}>"
245 var id: nullable String = null
249 var v = new RawTextVisitor
250 return v.render(self)
254 redef class MdUnorderedList
255 redef fun render_html(v) do
266 redef class MdOrderedList
267 redef fun render_html(v) do
268 var start = self.start_number
272 v.add_raw " start=\"{start}\""
283 redef class MdListItem
284 redef fun render_html(v) do
292 redef class MdParagraph
293 redef fun render_html(v) do
294 var is_tight = is_in_tight_list
307 redef class MdThematicBreak
308 redef fun render_html(v) do
315 redef class MdHtmlBlock
316 redef fun render_html(v) do
318 var literal = self.literal or else ""
319 var lines = literal.split("\n")
320 for i in [0..lines.length[ do
322 if not line.trim.is_empty then
325 if i < lines.length - 1 then
335 redef class MdHardLineBreak
336 redef fun render_html(v) do
342 redef class MdSoftLineBreak
343 redef fun render_html(v) do
349 redef fun render_html(v) do
351 v.add_raw v.html_escape(literal, false)
356 redef class MdEmphasis
357 redef fun render_html(v) do
364 redef class MdStrongEmphasis
365 redef fun render_html(v) do
368 v.add_raw "</strong>"
372 redef class MdHtmlInline
373 redef fun render_html(v) do
379 redef fun render_html(v) do
380 var url = self.destination
381 var title = self.title
383 v.add_raw " src=\"{v.encode_uri(url)}\""
385 var alt_text = self.alt_text
386 v.add_raw " alt=\"{alt_text}\""
388 if title != null and not title.is_empty then
389 v.add_raw " title=\""
397 private fun alt_text: String do
398 var v = new RawTextVisitor
399 return v.render(self)
404 redef fun render_html(v) do
405 var url = self.destination
406 var title = self.title
408 v.add_raw " href=\"{v.encode_uri(url)}\""
409 if title != null and not title.is_empty then
410 v.add_raw " title=\""
421 redef fun render_html(v) do