Class: Mdlint::Renderer::HtmlRenderer

Inherits:
Object
  • Object
show all
Defined in:
lib/mdlint/renderer/html_renderer.rb,
sig/internal.rbs

Constant Summary collapse

NAMED_ENTITIES =
{
  "amp" => "&", "apos" => "'", "gt" => ">", "lt" => "<", "quot" => '"',
  "nbsp" => "\u00a0", "copy" => "©", "reg" => "®", "trade" => "",
  "AElig" => "Æ", "aelig" => "æ", "Dcaron" => "Ď", "dcaron" => "ď",
  "ouml" => "ö", "Ouml" => "Ö", "auml" => "ä", "Auml" => "Ä", "uuml" => "ü", "Uuml" => "Ü",
  "frac12" => "½", "frac14" => "¼", "frac34" => "¾", "HilbertSpace" => "",
  "DifferentialD" => "", "ClockwiseContourIntegral" => "", "ngE" => "≧̸"
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ HtmlRenderer

Returns a new instance of HtmlRenderer.

Parameters:

  • options (Object) (defaults to: {})


18
19
20
21
22
23
# File 'lib/mdlint/renderer/html_renderer.rb', line 18

def initialize(options = {})
  @options = options
  @reference_definitions = {}
  @footnote_definitions = {}
  @footnote_order = []
end

Instance Method Details

#collapse_tight_item(item) ⇒ String

Parameters:

  • item (Object)

Returns:

  • (String)


200
201
202
203
204
205
206
# File 'lib/mdlint/renderer/html_renderer.rb', line 200

def collapse_tight_item(item)
  match = item.match(/\A<p>(.*)<\/p>\n\z/m)
  return match[1] if match
  return "\n#{item}" if item.start_with?("<hr />\n")

  item.sub(/\A<p>(.*)<\/p>\n(?=<(?:ul|ol)>)/m, "\\1\n")
end

#collect_reference_definitions(tokens) ⇒ Object

Parameters:

  • tokens (Object)

Returns:

  • (Object)


208
209
210
211
212
213
214
215
216
217
218
# File 'lib/mdlint/renderer/html_renderer.rb', line 208

def collect_reference_definitions(tokens)
  tokens.each do |token|
    next unless token.type == :reference_definition

    label = token.attrs[:label]
    @reference_definitions[label] ||= {
      url: token.attrs[:url],
      title: token.attrs[:title]
    }
  end
end

#decode_entities(value) ⇒ String

Parameters:

  • value (Object)

Returns:

  • (String)


320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/mdlint/renderer/html_renderer.rb', line 320

def decode_entities(value)
  value.gsub(/&#x([0-9a-f]+);|&#([0-9]+);|&([A-Za-z][A-Za-z0-9]+);/i) do
    codepoint = if Regexp.last_match(1)
                  Integer(Regexp.last_match(1).to_s, 16)
                elsif Regexp.last_match(2)
                  Regexp.last_match(2).to_i
                end
    if codepoint
      begin
        if codepoint.zero?
          "\uFFFD"
        elsif codepoint > 0x10ffff || (0xd800..0xdfff).cover?(codepoint)
          Regexp.last_match(0)
        else
          [codepoint].pack("U")
        end
      rescue RangeError
        Regexp.last_match(0)
      end
    else
      NAMED_ENTITIES.fetch(Regexp.last_match(3), Regexp.last_match(0))
    end
  end
end

#escape(value) ⇒ String

Parameters:

  • value (Object)

Returns:

  • (String)


301
302
303
# File 'lib/mdlint/renderer/html_renderer.rb', line 301

def escape(value)
  CGI.escapeHTML(decode_entities(value.to_s)).gsub("&#39;", "'")
end

#escape_attribute(value) ⇒ String

Parameters:

  • value (Object)

Returns:

  • (String)


305
306
307
# File 'lib/mdlint/renderer/html_renderer.rb', line 305

def escape_attribute(value)
  CGI.escapeHTML(decode_entities(value.to_s)).gsub("`", "&#39;")
end

#escape_raw(value) ⇒ String

Parameters:

  • value (Object)

Returns:

  • (String)


316
317
318
# File 'lib/mdlint/renderer/html_renderer.rb', line 316

def escape_raw(value)
  CGI.escapeHTML(value.to_s).gsub("&#39;", "'")
end

#escape_url_attribute(value, unescape: true) ⇒ String

Parameters:

  • value (Object)
  • unescape: (Boolean) (defaults to: true)

Returns:

  • (String)


309
310
311
312
313
314
# File 'lib/mdlint/renderer/html_renderer.rb', line 309

def escape_url_attribute(value, unescape: true)
  encoded = unescape ? value.to_s.gsub(/\\([!\"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])/, '\\1') : value.to_s
  encoded = decode_entities(encoded)
  encoded = URI::DEFAULT_PARSER.escape(encoded, /[^A-Za-z0-9\-._~;\/:?@&=+$,#%!()*']/)
  CGI.escapeHTML(encoded).gsub("`", "&#39;")
end

#fenced_code(token) ⇒ String

Parameters:

  • token (Object)

Returns:

  • (String)


276
277
278
279
280
281
# File 'lib/mdlint/renderer/html_renderer.rb', line 276

def fenced_code(token)
  language = token.info.to_s.split.first
  language = decode_entities(language.to_s).gsub(/\\([!\"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])/, '\\1')
  class_attribute = language && !language.empty? ? " class=\"language-#{escape_attribute(language)}\"" : ""
  "<pre><code#{class_attribute}>#{escape_raw(token.content)}</code></pre>\n"
end

#find_close(tokens, index, close_type) ⇒ Object

Parameters:

  • tokens (Object)
  • index (Object)
  • close_type (Object)

Returns:

  • (Object)


288
289
290
# File 'lib/mdlint/renderer/html_renderer.rb', line 288

def find_close(tokens, index, close_type)
  tokens[(index + 1)..]&.index { |token| token.type == close_type }&.+(index + 1) || tokens.length
end

#image_alt(value) ⇒ String

Parameters:

  • value (Object)

Returns:

  • (String)


264
265
266
267
268
269
270
271
272
273
274
# File 'lib/mdlint/renderer/html_renderer.rb', line 264

def image_alt(value)
  tokens = Parser::InlineParser.new(@options).parse(value.to_s)
  tokens.filter_map do |token|
    case token.type
    when :text, :code_inline, :math_inline
      token.content
    when :image
      token.attrs[:alt] || token.content
    end
  end.join
end

#reference_literal(token, inner_tokens) ⇒ String

Parameters:

  • token (Object)
  • inner_tokens (Object)

Returns:

  • (String)


292
293
294
295
296
297
298
299
# File 'lib/mdlint/renderer/html_renderer.rb', line 292

def reference_literal(token, inner_tokens)
  inner = inner_tokens.to_a.map { |inner_token| inner_token.content || "" }.join
  if token.attrs[:reference_kind] == :full
    "[#{inner}][#{token.attrs[:reference_label]}]"
  else
    "[#{inner}]"
  end
end

#render(tokens) ⇒ String

Parameters:

  • tokens (Object)

Returns:

  • (String)


25
26
27
28
29
30
31
32
33
# File 'lib/mdlint/renderer/html_renderer.rb', line 25

def render(tokens)
  @reference_definitions = {}
  @footnote_definitions = {}
  @footnote_order = []
  collect_reference_definitions(tokens)
  output = render_sequence(tokens, 0).first
  output << render_footnotes unless @footnote_order.empty?
  output
end

#render_cell(value) ⇒ String

Parameters:

  • value (Object)

Returns:

  • (String)


259
260
261
262
# File 'lib/mdlint/renderer/html_renderer.rb', line 259

def render_cell(value)
  inline_tokens = Parser::InlineParser.new(@options).parse(value.to_s)
  render_inline_tokens(inline_tokens)
end

#render_directive(token) ⇒ String

Parameters:

  • token (Object)

Returns:

  • (String)


236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/mdlint/renderer/html_renderer.rb', line 236

def render_directive(token)
  name = token.meta[:name].to_s.downcase
  return token.content unless %w[note tip important warning caution message details].include?(name)

  lines = token.content.gsub(/\r\n/, "\n").split("\n", -1)
  lines.shift
  lines.pop while lines.last.to_s.empty?
  lines.pop if lines.last.to_s.strip == ":::"
  inner_source = lines.join("\n")
  inner_source += "\n" unless inner_source.empty?
  inner_tokens = Parser.parse(inner_source, @options)
  inner_html = render_sequence(inner_tokens, 0).first
  title = token.meta[:title].to_s

  if name == "details"
    summary = title.empty? ? "Details" : title
    "<details>\n<summary>#{escape(summary)}</summary>\n#{inner_html}</details>\n"
  else
    title_html = title.empty? ? "" : "<p class=\"markdown-directive-title\">#{escape(title)}</p>\n"
    "<aside class=\"markdown-directive markdown-directive-#{escape_attribute(name)}\">\n#{title_html}#{inner_html}</aside>\n"
  end
end

#render_footnote_content(content) ⇒ String

Parameters:

  • content (Object)

Returns:

  • (String)


231
232
233
234
# File 'lib/mdlint/renderer/html_renderer.rb', line 231

def render_footnote_content(content)
  inner_tokens = Parser.parse("#{content}\n", @options)
  render_sequence(inner_tokens, 0).first
end

#render_footnotesString

Returns:

  • (String)


220
221
222
223
224
225
226
227
228
229
# File 'lib/mdlint/renderer/html_renderer.rb', line 220

def render_footnotes
  output = +"<section class=\"footnotes\">\n<ol>\n"
  @footnote_order.each do |label|
    token = @footnote_definitions[label]
    content = token ? render_footnote_content(token.attrs[:content]) : ""
    safe_label = escape_attribute(label)
    output << "<li id=\"fn-#{safe_label}\">#{content} <a href=\"#fnref-#{safe_label}\">↩︎</a></li>\n"
  end
  output << "</ol>\n</section>\n"
end

#render_inline(token) ⇒ String

Parameters:

  • token (Object)

Returns:

  • (String)


116
117
118
119
120
121
# File 'lib/mdlint/renderer/html_renderer.rb', line 116

def render_inline(token)
  return "" unless token
  return escape(token.content) if token.children.empty?

  render_inline_tokens(token.children)
end

#render_inline_tokens(tokens) ⇒ String

Parameters:

  • tokens (Object)

Returns:

  • (String)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/mdlint/renderer/html_renderer.rb', line 123

def render_inline_tokens(tokens)
  output = +""
  index = 0
  while index < tokens.length
    token = tokens[index]
    case token.type
    when :text
      output << escape(token.content)
    when :strong_open, :em_open, :s_open
      close_type = { strong_open: :strong_close, em_open: :em_close, s_open: :s_close }.fetch(token.type)
      tag = { strong_open: "strong", em_open: "em", s_open: "del" }.fetch(token.type)
      close_index = find_close(tokens, index, close_type)
      output << "<#{tag}>#{render_inline_tokens(tokens[(index + 1)...close_index])}</#{tag}>"
      index = close_index
    when :code_inline
      output << "<code>#{escape_raw(token.content)}</code>"
    when :math_inline
      output << "<span class=\"math-inline\">#{escape(token.content)}</span>"
    when :footnote_ref
      label = escape_attribute(token.attrs[:label])
      @footnote_order << token.attrs[:label] unless @footnote_order.include?(token.attrs[:label])
      number = @footnote_order.index(token.attrs[:label]) + 1
      output << "<sup id=\"fnref-#{label}\"><a href=\"#fn-#{label}\">#{number}</a></sup>"
    when :link_open
      close_index = find_close(tokens, index, :link_close)
      reference = @reference_definitions[token.attrs[:reference_label]]
      unless reference || token.markup != "reference"
        literal = reference_literal(token, tokens[(index + 1)...close_index])
        output << escape(literal)
        index = close_index
        next
      end
      href = token.attrs[:href] || reference&.fetch(:url, "") || ""
      title = token.attrs[:title] || reference&.fetch(:title, nil)
      title = title.to_s.gsub(/\\([!\"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~])/, '\\1') if title
      title_attribute = title ? " title=\"#{escape_attribute(title)}\"" : ""
      unescape_url = token.markup != "autolink"
      output << "<a href=\"#{escape_url_attribute(href, unescape: unescape_url)}\"#{title_attribute}>#{render_inline_tokens(tokens[(index + 1)...close_index])}</a>"
      index = close_index
    when :image
      reference = @reference_definitions[token.attrs[:reference_label]]
      source = token.attrs[:src] || reference&.fetch(:url, nil)
      unless source
        label = token.attrs[:reference_label]
        suffix = token.attrs[:reference_kind] == :full ? "[#{label}]" : ""
        output << escape("![#{token.attrs[:alt] || token.content}]#{suffix}")
        index += 1
        next
      end
      attrs = "src=\"#{escape_url_attribute(source, unescape: true)}\" alt=\"#{escape_attribute(image_alt(token.attrs[:alt] || token.content))}\""
      title = token.attrs[:title] || reference&.fetch(:title, nil)
      attrs += " title=\"#{escape_attribute(title)}\"" if title
      output << "<img #{attrs} />"
    when :softbreak
      output << "\n"
    when :hardbreak
      output << "<br />\n"
    when :html_inline
      output << token.content
    end
    index += 1
  end
  output
end

#render_sequence(tokens, index, closing_type = nil) ⇒ Object

Parameters:

  • tokens (Object)
  • index (Object)
  • closing_type (Object) (defaults to: nil)

Returns:

  • (Object)


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/mdlint/renderer/html_renderer.rb', line 37

def render_sequence(tokens, index, closing_type = nil)
  output = +""
  while index < tokens.length
    token = tokens[index]
    if closing_type && token.type == closing_type
      return [output, index + 1]
    end

    case token.type
    when :heading_open
      content, index = render_until_inline(tokens, index + 1, :heading_close)
      output << "<#{token.tag}>#{render_inline(content)}</#{token.tag}>\n"
    when :paragraph_open
      content, index = render_until_inline(tokens, index + 1, :paragraph_close)
      output << "<p>#{render_inline(content)}</p>\n"
    when :bullet_list_open, :ordered_list_open
      list, index = render_sequence(tokens, index + 1, token.type == :bullet_list_open ? :bullet_list_close : :ordered_list_close)
      tag = token.type == :bullet_list_open ? "ul" : "ol"
      start = token.type == :ordered_list_open ? token.attrs[:start].to_i : 1
      start_attribute = token.type == :ordered_list_open && start != 1 ? " start=\"#{start}\"" : ""
      output << "<#{tag}#{start_attribute}>\n#{list}</#{tag}>\n"
    when :list_item_open
      item, index = render_sequence(tokens, index + 1, :list_item_close)
      task = token.attrs[:task] ? task_checkbox(token.attrs[:checked]) : ""
      if token.attrs[:tight] == false
        output << "<li>#{task}\n#{item}</li>\n"
      else
        item = collapse_tight_item(item)
        output << "<li>#{task}#{item}</li>\n"
      end
    when :blockquote_open
      content, index = render_sequence(tokens, index + 1, :blockquote_close)
      if token.attrs[:alert]
        alert = escape_attribute(token.attrs[:alert])
        output << "<aside class=\"markdown-alert markdown-alert-#{alert}\">\n#{content}</aside>\n"
      else
        output << "<blockquote>\n#{content}</blockquote>\n"
      end
    when :fence
      output << fenced_code(token)
      index += 1
    when :code_block
      output << "<pre><code>#{escape_raw(token.content)}</code></pre>\n"
      index += 1
    when :math_block
      output << "<div class=\"math-block\">#{escape_raw(token.content)}</div>\n"
      index += 1
    when :footnote_definition
      @footnote_definitions[token.attrs[:label]] ||= token
      index += 1
    when :hr
      output << "<hr />\n"
      index += 1
    when :html_block
      output << token.content
      output << "\n" unless token.content.end_with?("\n")
      index += 1
    when :table
      output << render_table(token)
      index += 1
    when :directive
      output << render_directive(token)
      index += 1
    when :front_matter, :reference_definition
      index += 1
    else
      index += 1
    end
  end

  [output, index]
end

#render_table(token) ⇒ String

Parameters:

  • token (Object)

Returns:

  • (String)


188
189
190
191
192
193
194
195
196
197
198
# File 'lib/mdlint/renderer/html_renderer.rb', line 188

def render_table(token)
  rows = token.meta.fetch(:rows, [])
  return "" if rows.empty?

  header = rows.first.map { |cell| "<th>#{render_cell(cell)}</th>" }.join
  body = rows.drop(1).map do |row|
    cells = row.map { |cell| "<td>#{render_cell(cell)}</td>" }.join
    "<tr>#{cells}</tr>\n"
  end.join
  "<table>\n<thead><tr>#{header}</tr></thead>\n<tbody>\n#{body}</tbody>\n</table>\n"
end

#render_until_inline(tokens, index, closing_type) ⇒ Object

Parameters:

  • tokens (Object)
  • index (Object)
  • closing_type (Object)

Returns:

  • (Object)


110
111
112
113
114
# File 'lib/mdlint/renderer/html_renderer.rb', line 110

def render_until_inline(tokens, index, closing_type)
  inline = tokens[index..]&.find { |token| token.type == :inline }
  closing_index = tokens[index..]&.index { |token| token.type == closing_type }
  [inline, index + (closing_index || 0) + 1]
end

#task_checkbox(checked) ⇒ String

Parameters:

  • checked (Object)

Returns:

  • (String)


283
284
285
286
# File 'lib/mdlint/renderer/html_renderer.rb', line 283

def task_checkbox(checked)
  checked_attribute = checked ? " checked" : ""
  "<input type=\"checkbox\" disabled#{checked_attribute}> "
end