slamdown 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/changelog.md +7 -0
- data/lib/slamdown/conversion/configuration.rb +35 -0
- data/lib/slamdown/conversion/configuration_validator.rb +235 -0
- data/lib/slamdown/conversion/converter.rb +98 -0
- data/lib/slamdown/conversion/normalisation.rb +21 -0
- data/lib/slamdown/conversion/normalised_node.rb +40 -0
- data/lib/slamdown/conversion/normaliser/breaks.rb +104 -0
- data/lib/slamdown/conversion/normaliser/content.rb +174 -0
- data/lib/slamdown/conversion/normaliser/correction_diagnostics.rb +29 -0
- data/lib/slamdown/conversion/normaliser/element_actions.rb +423 -0
- data/lib/slamdown/conversion/normaliser/heading_levels.rb +45 -0
- data/lib/slamdown/conversion/normaliser/inline_semantics.rb +218 -0
- data/lib/slamdown/conversion/normaliser/node_factory.rb +112 -0
- data/lib/slamdown/conversion/normaliser/url_rewriter.rb +124 -0
- data/lib/slamdown/conversion/normaliser.rb +58 -0
- data/lib/slamdown/conversion/policy.rb +178 -0
- data/lib/slamdown/conversion/prepared_node.rb +23 -0
- data/lib/slamdown/conversion/preparer.rb +122 -0
- data/lib/slamdown/conversion/renderer.rb +432 -0
- data/lib/slamdown/conversion/rendering/entity_encoder.rb +56 -0
- data/lib/slamdown/conversion/rendering/escaper.rb +58 -0
- data/lib/slamdown/conversion/rendering/html.rb +183 -0
- data/lib/slamdown/conversion/rendering/result.rb +23 -0
- data/lib/slamdown/conversion/rendering/strategy.rb +157 -0
- data/lib/slamdown/conversion/semantic_extractor.rb +97 -0
- data/lib/slamdown/conversion/tables.rb +676 -0
- data/lib/slamdown/conversion.rb +34 -0
- data/lib/slamdown/diagnostic.rb +34 -0
- data/lib/slamdown/document.rb +107 -0
- data/lib/slamdown/errors.rb +25 -0
- data/lib/slamdown/immutable.rb +23 -0
- data/lib/slamdown/output.rb +30 -0
- data/lib/slamdown/parsing/html.rb +227 -0
- data/lib/slamdown/parsing/node.rb +50 -0
- data/lib/slamdown/parsing/parsed_node_adapter.rb +144 -0
- data/lib/slamdown/parsing.rb +17 -0
- data/lib/slamdown/version.rb +3 -0
- data/lib/slamdown.rb +10 -0
- data/readme.md +121 -0
- metadata +174 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slamdown
|
|
4
|
+
module Conversion
|
|
5
|
+
module Rendering
|
|
6
|
+
|
|
7
|
+
# Reconstructs canonical HTML islands solely from normalised nodes and post-policy attributes.
|
|
8
|
+
class Html
|
|
9
|
+
SEMANTIC_TAGS = {
|
|
10
|
+
:paragraph => "p",
|
|
11
|
+
:blockquote => "blockquote",
|
|
12
|
+
:ordered_list => "ol",
|
|
13
|
+
:unordered_list => "ul",
|
|
14
|
+
:list_item => "li",
|
|
15
|
+
:definition_list => "dl",
|
|
16
|
+
:definition_term => "dt",
|
|
17
|
+
:definition_description => "dd",
|
|
18
|
+
:horizontal_rule => "hr",
|
|
19
|
+
:line_break => "br",
|
|
20
|
+
:code_span => "code",
|
|
21
|
+
:code_block => "pre",
|
|
22
|
+
:link => "a",
|
|
23
|
+
:image => "img",
|
|
24
|
+
:emphasis => "em",
|
|
25
|
+
:strong => "strong",
|
|
26
|
+
:deletion => "del",
|
|
27
|
+
:table => "table",
|
|
28
|
+
:table_head => "thead",
|
|
29
|
+
:table_body => "tbody",
|
|
30
|
+
:table_foot => "tfoot",
|
|
31
|
+
:table_row => "tr",
|
|
32
|
+
:table_cell => "td",
|
|
33
|
+
:table_caption => "caption",
|
|
34
|
+
:table_colgroup => "colgroup",
|
|
35
|
+
:table_column => "col"
|
|
36
|
+
}.freeze
|
|
37
|
+
BLOCK_TAGS = %w[address article aside blockquote div dl fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr li main nav ol p pre section table thead tbody tfoot tr td th ul].freeze
|
|
38
|
+
VOID_TAGS = %w[area base br col embed hr img input link meta param source track wbr].freeze
|
|
39
|
+
|
|
40
|
+
def initialize(configuration, entity_encoder)
|
|
41
|
+
@indent = configuration.html_indent
|
|
42
|
+
@inline = configuration.inline
|
|
43
|
+
@entity_encoder = entity_encoder
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render(node)
|
|
47
|
+
block_node?(node) ? render_block(node, 0) : render_inline(node)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_inline(node)
|
|
51
|
+
return escape_text(node.value) if node.kind == :text
|
|
52
|
+
return render_entity(node) if entity_leaf?(node)
|
|
53
|
+
|
|
54
|
+
tag = tag_for(node)
|
|
55
|
+
return node.children.map { |child| render_inline(child) }.join unless tag
|
|
56
|
+
return start_tag(node, tag, true) if void_tag?(tag)
|
|
57
|
+
return render_code_span(node, tag) if node.semantic_kind == :code_span
|
|
58
|
+
|
|
59
|
+
"#{start_tag(node, tag)}#{node.children.map { |child| render_inline(child) }.join}</#{tag}>"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def render_block(node, depth)
|
|
65
|
+
tag = tag_for(node)
|
|
66
|
+
return render_block_children(node.children, depth) unless tag
|
|
67
|
+
return indent(depth) + start_tag(node, tag, true) if void_tag?(tag)
|
|
68
|
+
return render_preserved_code_block(node, tag, depth) if node.semantic_kind == :code_block && node.representation == :html
|
|
69
|
+
return render_code_block(node, depth) if node.semantic_kind == :code_block && node.representation != :html
|
|
70
|
+
|
|
71
|
+
if node.children.none? { |child| block_node?(child) }
|
|
72
|
+
return "#{indent(depth)}#{start_tag(node, tag)}#{node.children.map { |child| render_inline(child) }.join}</#{tag}>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
lines = [indent(depth) + start_tag(node, tag)]
|
|
76
|
+
lines.concat(render_mixed_children(node.children, depth + 1))
|
|
77
|
+
lines << "#{indent(depth)}</#{tag}>"
|
|
78
|
+
lines.join("\n")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_block_children(children, depth)
|
|
82
|
+
render_mixed_children(children, depth).join("\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def render_mixed_children(children, depth)
|
|
86
|
+
lines = []
|
|
87
|
+
inline_run = []
|
|
88
|
+
flush = lambda do
|
|
89
|
+
unless inline_run.empty?
|
|
90
|
+
lines << indent(depth) + inline_run.map { |child| render_inline(child) }.join
|
|
91
|
+
inline_run = []
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
children.each do |child|
|
|
96
|
+
if block_node?(child)
|
|
97
|
+
flush.call
|
|
98
|
+
lines << render_block(child, depth)
|
|
99
|
+
else
|
|
100
|
+
inline_run << child
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
flush.call
|
|
104
|
+
lines
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def render_code_span(node, tag)
|
|
108
|
+
value = node.value.to_s
|
|
109
|
+
value = value.gsub(/\s+/, " ") if @inline
|
|
110
|
+
"#{start_tag(node, tag)}#{escape_text(value)}</#{tag}>"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def render_code_block(node, depth)
|
|
114
|
+
tag = tag_for(node)
|
|
115
|
+
language = node.semantic_values[:code_language]
|
|
116
|
+
code_attributes = language ? " class=\"#{escape_attribute("language-#{language}")}\"" : ""
|
|
117
|
+
"#{indent(depth)}#{start_tag(node, tag)}<code#{code_attributes}>#{escape_text(node.value.to_s)}</code></#{tag}>"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_preserved_code_block(node, tag, depth)
|
|
121
|
+
"#{indent(depth)}#{start_tag(node, tag)}#{escape_text(node.value.to_s)}</#{tag}>"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def start_tag(node, tag, void = false)
|
|
125
|
+
ending = void ? " />" : ">"
|
|
126
|
+
"<#{tag}#{render_attributes(node)}#{ending}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def render_attributes(node)
|
|
130
|
+
pairs = []
|
|
131
|
+
pairs << ["id", node.id] if node.id
|
|
132
|
+
pairs << ["class", node.classes.join(" ")] unless node.classes.empty?
|
|
133
|
+
pairs.concat(node.attributes.reject { |name, _value| ["id", "class"].include?(name) }.sort_by(&:first))
|
|
134
|
+
return "" if pairs.empty?
|
|
135
|
+
|
|
136
|
+
" " + pairs.map { |name, value| "#{name}=\"#{escape_attribute(value)}\"" }.join(" ")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def tag_for(node)
|
|
140
|
+
return node.source_name if node.representation == :html && node.source_name
|
|
141
|
+
return "th" if node.semantic_kind == :table_cell && node.source_name == "th"
|
|
142
|
+
return "h#{node.properties[:level]}" if node.semantic_kind == :heading
|
|
143
|
+
|
|
144
|
+
SEMANTIC_TAGS[node.semantic_kind]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def block_node?(node)
|
|
148
|
+
return true if node.category == :block
|
|
149
|
+
|
|
150
|
+
tag = tag_for(node)
|
|
151
|
+
tag && BLOCK_TAGS.include?(tag)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def void_tag?(tag)
|
|
155
|
+
VOID_TAGS.include?(tag)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def entity_leaf?(node)
|
|
159
|
+
[:entity, :typographic_symbol, :smart_quote].include?(node.kind)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def render_entity(node)
|
|
163
|
+
value = @entity_encoder.encode(node)
|
|
164
|
+
@entity_encoder.character_mode? ? escape_text(value) : value
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def escape_text(value)
|
|
168
|
+
value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def escape_attribute(value)
|
|
172
|
+
value = value.to_s.gsub(/[\r\n]+/, " ") if @inline
|
|
173
|
+
escape_text(value).gsub('"', """)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def indent(depth)
|
|
177
|
+
@indent * depth
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slamdown
|
|
4
|
+
module Conversion
|
|
5
|
+
module Rendering
|
|
6
|
+
|
|
7
|
+
# Carries one rendered document together with node-local failures contained during traversal.
|
|
8
|
+
class Result
|
|
9
|
+
attr_reader :output, :diagnostics
|
|
10
|
+
|
|
11
|
+
def initialize(output, diagnostics)
|
|
12
|
+
raise ArgumentError, "output must be a string" unless output.is_a?(String)
|
|
13
|
+
raise ArgumentError, "diagnostics must contain only Slamdown diagnostics" unless diagnostics.is_a?(Array) && diagnostics.all? { |diagnostic| diagnostic.is_a?(Diagnostic) }
|
|
14
|
+
|
|
15
|
+
@output = output.dup.freeze
|
|
16
|
+
@diagnostics = diagnostics.dup.freeze
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slamdown
|
|
4
|
+
module Conversion
|
|
5
|
+
module Rendering
|
|
6
|
+
|
|
7
|
+
# Supplies the small set of output-format differences used by the shared renderer.
|
|
8
|
+
module Strategy
|
|
9
|
+
class << self
|
|
10
|
+
def build(format)
|
|
11
|
+
format == "kramdown" ? Kramdown.new : Markdown.new
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Defines common format hooks and groups definition-list terms with their following descriptions.
|
|
16
|
+
class Base
|
|
17
|
+
def pad_space_bounded_code_span?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def html_for_space_bounded_code_span?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def escape_link_ampersands?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def escape_definition_marker?
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def decorate_inline(_node, syntax)
|
|
34
|
+
syntax
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def decorate_block(_node, syntax)
|
|
38
|
+
syntax
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def definition_groups(node)
|
|
44
|
+
groups = []
|
|
45
|
+
terms = []
|
|
46
|
+
descriptions = []
|
|
47
|
+
flush = lambda do
|
|
48
|
+
groups << [terms, descriptions] unless terms.empty? && descriptions.empty?
|
|
49
|
+
terms = []
|
|
50
|
+
descriptions = []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
node.children.each do |child|
|
|
54
|
+
if child.semantic_kind == :definition_term
|
|
55
|
+
flush.call unless descriptions.empty?
|
|
56
|
+
terms << child
|
|
57
|
+
elsif child.semantic_kind == :definition_description
|
|
58
|
+
descriptions << child
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
flush.call
|
|
62
|
+
groups
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def prefix_lines(value, first_prefix, continuation_prefix)
|
|
66
|
+
lines = value.split("\n", -1)
|
|
67
|
+
lines.each_with_index.map do |line, index|
|
|
68
|
+
prefix = index.zero? ? first_prefix : continuation_prefix
|
|
69
|
+
line.empty? ? prefix.rstrip : prefix + line
|
|
70
|
+
end.join("\n")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Implements definition-list degradation for the restricted Markdown profile.
|
|
75
|
+
class Markdown < Base
|
|
76
|
+
def pad_space_bounded_code_span?
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_definition_list(renderer, node)
|
|
81
|
+
groups = definition_groups(node).map do |terms, descriptions|
|
|
82
|
+
term = terms.map do |item|
|
|
83
|
+
renderer.render_inlines(item.children, :suppress_strong => true, :line_start => false).strip
|
|
84
|
+
end.join("<br />")
|
|
85
|
+
lines = ["* **#{term}**"]
|
|
86
|
+
descriptions.each do |description|
|
|
87
|
+
content = renderer.render_container_contents(description)
|
|
88
|
+
lines << prefix_lines(content, " * ", " ")
|
|
89
|
+
end
|
|
90
|
+
lines.join("\n")
|
|
91
|
+
end
|
|
92
|
+
groups.join("\n")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Adds Kramdown IALs and emits native definition-list syntax while sharing all ordinary rendering primitives.
|
|
97
|
+
class Kramdown < Base
|
|
98
|
+
IAL_TOKEN = /\A[A-Za-z_][A-Za-z0-9_-]*\z/.freeze
|
|
99
|
+
|
|
100
|
+
def html_for_space_bounded_code_span?
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def escape_definition_marker?
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def decorate_inline(node, syntax)
|
|
109
|
+
attribute_list = ial(node)
|
|
110
|
+
attribute_list ? syntax + attribute_list : syntax
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def decorate_block(node, syntax)
|
|
114
|
+
attribute_list = ial(node)
|
|
115
|
+
attribute_list ? "#{syntax}\n#{attribute_list}" : syntax
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def render_definition_list(renderer, node)
|
|
119
|
+
groups = definition_groups(node).map do |terms, descriptions|
|
|
120
|
+
lines = terms.map { |term| renderer.render_inlines(term.children, :line_start => true).strip }
|
|
121
|
+
descriptions.each do |description|
|
|
122
|
+
content = renderer.render_container_contents(description)
|
|
123
|
+
lines << prefix_lines(content, ": ", " ")
|
|
124
|
+
end
|
|
125
|
+
lines.join("\n")
|
|
126
|
+
end
|
|
127
|
+
decorate_block(node, groups.join("\n\n"))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def ial(node)
|
|
133
|
+
return if !node.id && node.classes.empty?
|
|
134
|
+
|
|
135
|
+
parts = []
|
|
136
|
+
parts << ial_id(node.id) if node.id
|
|
137
|
+
if node.classes.all? { |name| IAL_TOKEN.match(name) }
|
|
138
|
+
parts.concat(node.classes.map { |name| ".#{name}" })
|
|
139
|
+
elsif !node.classes.empty?
|
|
140
|
+
parts << "class=\"#{quoted(node.classes.join(" "))}\""
|
|
141
|
+
end
|
|
142
|
+
"{: #{parts.join(" ")}}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ial_id(value)
|
|
146
|
+
IAL_TOKEN.match(value) ? "##{value}" : "id=\"#{quoted(value)}\""
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def quoted(value)
|
|
150
|
+
value.gsub("\\", "\\\\").gsub('"', '\\"')
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slamdown
|
|
4
|
+
module Conversion
|
|
5
|
+
|
|
6
|
+
# Reads the small registered set of semantic source attributes before retention policy can discard those attributes.
|
|
7
|
+
class SemanticExtractor
|
|
8
|
+
HTML_WHITESPACE = /[\t\n\f\r ]+/.freeze
|
|
9
|
+
LANGUAGE_CLASS = /\Alanguage-([A-Za-z0-9+_.-]+)\z/.freeze
|
|
10
|
+
RECOGNISED_STYLE_PROPERTIES = %w[font-weight font-style text-decoration].freeze
|
|
11
|
+
|
|
12
|
+
def extract(node)
|
|
13
|
+
attributes = Hash[node.attributes]
|
|
14
|
+
values = {}
|
|
15
|
+
|
|
16
|
+
extract_image_fallback(node, attributes, values)
|
|
17
|
+
extract_span_styles(node, attributes, values)
|
|
18
|
+
extract_code_language(node, attributes, values)
|
|
19
|
+
extract_table_spans(node, attributes, values)
|
|
20
|
+
|
|
21
|
+
values
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Language classes become fence metadata and must not also survive as presentation classes.
|
|
25
|
+
def consumed_class?(node, token)
|
|
26
|
+
["code", "pre"].include?(node.source_name) && LANGUAGE_CLASS.match(token)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def extract_image_fallback(node, attributes, values)
|
|
32
|
+
return unless node.source_name == "img" && !attributes.key?("alt")
|
|
33
|
+
|
|
34
|
+
title = attributes["title"]
|
|
35
|
+
values[:image_fallback_alt] = title if title.is_a?(String) && !title.strip.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extract_span_styles(node, attributes, values)
|
|
39
|
+
return unless node.source_name == "span" && attributes["style"].is_a?(String)
|
|
40
|
+
|
|
41
|
+
declarations = recognised_declarations(attributes["style"])
|
|
42
|
+
styles = []
|
|
43
|
+
styles << :strong if bold?(declarations["font-weight"])
|
|
44
|
+
styles << :emphasis if declarations["font-style"] == "italic"
|
|
45
|
+
|
|
46
|
+
decorations = declarations.fetch("text-decoration", "").split(HTML_WHITESPACE)
|
|
47
|
+
styles << :deletion if decorations.include?("line-through")
|
|
48
|
+
styles << :underline if decorations.include?("underline")
|
|
49
|
+
values[:styled_as] = styles unless styles.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def recognised_declarations(style)
|
|
53
|
+
style.split(";").each_with_object({}) do |declaration, result|
|
|
54
|
+
property, value = declaration.split(":", 2)
|
|
55
|
+
next unless property && value
|
|
56
|
+
|
|
57
|
+
property = property.strip.downcase
|
|
58
|
+
value = value.strip.downcase
|
|
59
|
+
next unless RECOGNISED_STYLE_PROPERTIES.include?(property) && !value.empty?
|
|
60
|
+
|
|
61
|
+
# Every syntactically usable declaration replaces the previous value, even when its value has no Slamdown semantic equivalent.
|
|
62
|
+
result[property] = value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def bold?(value)
|
|
67
|
+
return true if ["bold", "bolder"].include?(value)
|
|
68
|
+
return false unless /\A\d+\z/.match(value)
|
|
69
|
+
|
|
70
|
+
value.to_i > 400
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def extract_code_language(node, attributes, values)
|
|
74
|
+
return unless ["code", "pre"].include?(node.source_name)
|
|
75
|
+
|
|
76
|
+
attributes.fetch("class", "").split(HTML_WHITESPACE).each do |token|
|
|
77
|
+
match = LANGUAGE_CLASS.match(token)
|
|
78
|
+
next unless match
|
|
79
|
+
|
|
80
|
+
values[:code_language] = match[1]
|
|
81
|
+
break
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def extract_table_spans(node, attributes, values)
|
|
86
|
+
return unless ["td", "th"].include?(node.source_name)
|
|
87
|
+
|
|
88
|
+
spans = ["colspan", "rowspan"].each_with_object({}) do |name, result|
|
|
89
|
+
value = attributes[name]
|
|
90
|
+
result[name] = value if value.is_a?(String)
|
|
91
|
+
end
|
|
92
|
+
values[:table_spans] = spans unless spans.empty?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
end
|
|
97
|
+
end
|