commonmarker 0.20.2 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of commonmarker might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +1 -3
- data/Rakefile +16 -4
- data/commonmarker.gemspec +8 -11
- data/ext/commonmarker/cmark-gfm-core-extensions.h +24 -1
- data/ext/commonmarker/commonmarker.c +120 -78
- data/ext/commonmarker/node.h +1 -0
- data/ext/commonmarker/table.c +52 -13
- data/ext/commonmarker/tasklist.c +33 -13
- data/lib/commonmarker.rb +5 -4
- data/lib/commonmarker/config.rb +5 -4
- data/lib/commonmarker/node.rb +1 -1
- data/lib/commonmarker/node/inspect.rb +11 -11
- data/lib/commonmarker/renderer.rb +7 -5
- data/lib/commonmarker/renderer/html_renderer.rb +21 -34
- data/lib/commonmarker/version.rb +1 -1
- data/test/benchmark.rb +1 -1
- data/test/test_attributes.rb +2 -2
- data/test/test_commonmark.rb +11 -11
- data/test/test_encoding.rb +0 -1
- data/test/test_extensions.rb +39 -40
- data/test/test_footnotes.rb +9 -9
- data/test/test_gc.rb +2 -0
- data/test/test_helper.rb +7 -8
- data/test/test_maliciousness.rb +192 -190
- data/test/test_node.rb +10 -12
- data/test/test_options.rb +15 -15
- data/test/test_pathological_inputs.rb +2 -2
- data/test/test_plaintext.rb +21 -21
- data/test/test_renderer.rb +9 -9
- data/test/test_spec.rb +3 -2
- data/test/test_tasklists.rb +16 -6
- metadata +42 -42
data/ext/commonmarker/node.h
CHANGED
data/ext/commonmarker/table.c
CHANGED
@@ -16,6 +16,7 @@ cmark_node_type CMARK_NODE_TABLE, CMARK_NODE_TABLE_ROW,
|
|
16
16
|
|
17
17
|
typedef struct {
|
18
18
|
uint16_t n_columns;
|
19
|
+
int paragraph_offset;
|
19
20
|
cmark_llist *cells;
|
20
21
|
} table_row;
|
21
22
|
|
@@ -115,6 +116,7 @@ static table_row *row_from_string(cmark_syntax_extension *self,
|
|
115
116
|
int len) {
|
116
117
|
table_row *row = NULL;
|
117
118
|
bufsize_t cell_matched = 1, pipe_matched = 1, offset;
|
119
|
+
int cell_end_offset;
|
118
120
|
|
119
121
|
row = (table_row *)parser->mem->calloc(1, sizeof(table_row));
|
120
122
|
row->n_columns = 0;
|
@@ -129,20 +131,32 @@ static table_row *row_from_string(cmark_syntax_extension *self,
|
|
129
131
|
pipe_matched = scan_table_cell_end(string, len, offset + cell_matched);
|
130
132
|
|
131
133
|
if (cell_matched || pipe_matched) {
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
134
|
+
cell_end_offset = offset + cell_matched - 1;
|
135
|
+
|
136
|
+
if (string[cell_end_offset] == '\n' || string[cell_end_offset] == '\r') {
|
137
|
+
row->paragraph_offset = cell_end_offset;
|
138
|
+
|
139
|
+
cmark_llist_free_full(parser->mem, row->cells, (cmark_free_func)free_table_cell);
|
140
|
+
row->cells = NULL;
|
141
|
+
row->n_columns = 0;
|
142
|
+
} else {
|
143
|
+
cmark_strbuf *cell_buf = unescape_pipes(parser->mem, string + offset,
|
144
|
+
cell_matched);
|
145
|
+
cmark_strbuf_trim(cell_buf);
|
146
|
+
|
147
|
+
node_cell *cell = (node_cell *)parser->mem->calloc(1, sizeof(*cell));
|
148
|
+
cell->buf = cell_buf;
|
149
|
+
cell->start_offset = offset;
|
150
|
+
cell->end_offset = cell_end_offset;
|
151
|
+
|
152
|
+
while (cell->start_offset > 0 && string[cell->start_offset - 1] != '|') {
|
153
|
+
--cell->start_offset;
|
154
|
+
++cell->internal_offset;
|
155
|
+
}
|
156
|
+
|
157
|
+
row->n_columns += 1;
|
158
|
+
row->cells = cmark_llist_append(parser->mem, row->cells, cell);
|
143
159
|
}
|
144
|
-
row->n_columns += 1;
|
145
|
-
row->cells = cmark_llist_append(parser->mem, row->cells, cell);
|
146
160
|
}
|
147
161
|
|
148
162
|
offset += cell_matched + pipe_matched;
|
@@ -161,6 +175,26 @@ static table_row *row_from_string(cmark_syntax_extension *self,
|
|
161
175
|
return row;
|
162
176
|
}
|
163
177
|
|
178
|
+
static void try_inserting_table_header_paragraph(cmark_parser *parser,
|
179
|
+
cmark_node *parent_container,
|
180
|
+
unsigned char *parent_string,
|
181
|
+
int paragraph_offset) {
|
182
|
+
cmark_node *paragraph;
|
183
|
+
cmark_strbuf *paragraph_content;
|
184
|
+
|
185
|
+
paragraph = cmark_node_new_with_mem(CMARK_NODE_PARAGRAPH, parser->mem);
|
186
|
+
|
187
|
+
paragraph_content = unescape_pipes(parser->mem, parent_string, paragraph_offset);
|
188
|
+
cmark_strbuf_trim(paragraph_content);
|
189
|
+
cmark_node_set_string_content(paragraph, (char *) paragraph_content->ptr);
|
190
|
+
cmark_strbuf_free(paragraph_content);
|
191
|
+
parser->mem->free(paragraph_content);
|
192
|
+
|
193
|
+
if (!cmark_node_insert_before(parent_container, paragraph)) {
|
194
|
+
parser->mem->free(paragraph);
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
164
198
|
static cmark_node *try_opening_table_header(cmark_syntax_extension *self,
|
165
199
|
cmark_parser *parser,
|
166
200
|
cmark_node *parent_container,
|
@@ -217,6 +251,11 @@ static cmark_node *try_opening_table_header(cmark_syntax_extension *self,
|
|
217
251
|
return parent_container;
|
218
252
|
}
|
219
253
|
|
254
|
+
if (header_row->paragraph_offset) {
|
255
|
+
try_inserting_table_header_paragraph(parser, parent_container, (unsigned char *)parent_string,
|
256
|
+
header_row->paragraph_offset);
|
257
|
+
}
|
258
|
+
|
220
259
|
cmark_node_set_syntax_extension(parent_container, self);
|
221
260
|
|
222
261
|
parent_container->as.opaque = parser->mem->calloc(1, sizeof(node_table));
|
data/ext/commonmarker/tasklist.c
CHANGED
@@ -9,19 +9,33 @@ typedef enum {
|
|
9
9
|
CMARK_TASKLIST_CHECKED,
|
10
10
|
} cmark_tasklist_type;
|
11
11
|
|
12
|
+
// Local constants
|
13
|
+
static const char *TYPE_STRING = "tasklist";
|
14
|
+
|
12
15
|
static const char *get_type_string(cmark_syntax_extension *extension, cmark_node *node) {
|
13
|
-
return
|
16
|
+
return TYPE_STRING;
|
14
17
|
}
|
15
18
|
|
16
|
-
|
17
|
-
|
19
|
+
|
20
|
+
// Return 1 if state was set, 0 otherwise
|
21
|
+
int cmark_gfm_extensions_set_tasklist_item_checked(cmark_node *node, bool is_checked) {
|
22
|
+
// The node has to exist, and be an extension, and actually be the right type in order to get the value.
|
23
|
+
if (!node || !node->extension || strcmp(cmark_node_get_type_string(node), TYPE_STRING))
|
18
24
|
return 0;
|
19
25
|
|
20
|
-
|
21
|
-
|
26
|
+
node->as.list.checked = is_checked;
|
27
|
+
return 1;
|
28
|
+
}
|
29
|
+
|
30
|
+
bool cmark_gfm_extensions_get_tasklist_item_checked(cmark_node *node) {
|
31
|
+
if (!node || !node->extension || strcmp(cmark_node_get_type_string(node), TYPE_STRING))
|
32
|
+
return false;
|
33
|
+
|
34
|
+
if (node->as.list.checked) {
|
35
|
+
return true;
|
22
36
|
}
|
23
37
|
else {
|
24
|
-
return
|
38
|
+
return false;
|
25
39
|
}
|
26
40
|
}
|
27
41
|
|
@@ -75,11 +89,7 @@ static cmark_node *open_tasklist_item(cmark_syntax_extension *self,
|
|
75
89
|
cmark_parser_advance_offset(parser, (char *)input, 3, false);
|
76
90
|
|
77
91
|
// Either an upper or lower case X means the task is completed.
|
78
|
-
|
79
|
-
parent_container->as.opaque = (void *)CMARK_TASKLIST_CHECKED;
|
80
|
-
} else {
|
81
|
-
parent_container->as.opaque = (void *)CMARK_TASKLIST_NOCHECKED;
|
82
|
-
}
|
92
|
+
parent_container->as.list.checked = (strstr((char*)input, "[x]") || strstr((char*)input, "[X]"));
|
83
93
|
|
84
94
|
return NULL;
|
85
95
|
}
|
@@ -90,7 +100,7 @@ static void commonmark_render(cmark_syntax_extension *extension,
|
|
90
100
|
bool entering = (ev_type == CMARK_EVENT_ENTER);
|
91
101
|
if (entering) {
|
92
102
|
renderer->cr(renderer);
|
93
|
-
if (
|
103
|
+
if (node->as.list.checked) {
|
94
104
|
renderer->out(renderer, node, "- [x] ", false, LITERAL);
|
95
105
|
} else {
|
96
106
|
renderer->out(renderer, node, "- [ ] ", false, LITERAL);
|
@@ -111,7 +121,7 @@ static void html_render(cmark_syntax_extension *extension,
|
|
111
121
|
cmark_strbuf_puts(renderer->html, "<li");
|
112
122
|
cmark_html_render_sourcepos(node, renderer->html, options);
|
113
123
|
cmark_strbuf_putc(renderer->html, '>');
|
114
|
-
if (
|
124
|
+
if (node->as.list.checked) {
|
115
125
|
cmark_strbuf_puts(renderer->html, "<input type=\"checkbox\" checked=\"\" disabled=\"\" /> ");
|
116
126
|
} else {
|
117
127
|
cmark_strbuf_puts(renderer->html, "<input type=\"checkbox\" disabled=\"\" /> ");
|
@@ -121,6 +131,15 @@ static void html_render(cmark_syntax_extension *extension,
|
|
121
131
|
}
|
122
132
|
}
|
123
133
|
|
134
|
+
static const char *xml_attr(cmark_syntax_extension *extension,
|
135
|
+
cmark_node *node) {
|
136
|
+
if (node->as.list.checked) {
|
137
|
+
return " completed=\"true\"";
|
138
|
+
} else {
|
139
|
+
return " completed=\"false\"";
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
124
143
|
cmark_syntax_extension *create_tasklist_extension(void) {
|
125
144
|
cmark_syntax_extension *ext = cmark_syntax_extension_new("tasklist");
|
126
145
|
|
@@ -131,6 +150,7 @@ cmark_syntax_extension *create_tasklist_extension(void) {
|
|
131
150
|
cmark_syntax_extension_set_commonmark_render_func(ext, commonmark_render);
|
132
151
|
cmark_syntax_extension_set_plaintext_render_func(ext, commonmark_render);
|
133
152
|
cmark_syntax_extension_set_html_render_func(ext, html_render);
|
153
|
+
cmark_syntax_extension_set_xml_attr_func(ext, xml_attr);
|
134
154
|
|
135
155
|
return ext;
|
136
156
|
}
|
data/lib/commonmarker.rb
CHANGED
@@ -10,8 +10,7 @@ require 'commonmarker/version'
|
|
10
10
|
|
11
11
|
begin
|
12
12
|
require 'awesome_print'
|
13
|
-
rescue LoadError; end
|
14
|
-
|
13
|
+
rescue LoadError; end # rubocop:disable Lint/SuppressedException
|
15
14
|
module CommonMarker
|
16
15
|
# Public: Parses a Markdown string into an HTML string.
|
17
16
|
#
|
@@ -21,7 +20,8 @@ module CommonMarker
|
|
21
20
|
#
|
22
21
|
# Returns a {String} of converted HTML.
|
23
22
|
def self.render_html(text, options = :DEFAULT, extensions = [])
|
24
|
-
|
23
|
+
raise TypeError, "text must be a String; got a #{text.class}!" unless text.is_a?(String)
|
24
|
+
|
25
25
|
opts = Config.process_options(options, :render)
|
26
26
|
text = text.encode('UTF-8')
|
27
27
|
html = Node.markdown_to_html(text, opts, extensions)
|
@@ -36,7 +36,8 @@ module CommonMarker
|
|
36
36
|
#
|
37
37
|
# Returns the `document` node.
|
38
38
|
def self.render_doc(text, options = :DEFAULT, extensions = [])
|
39
|
-
|
39
|
+
raise TypeError, "text must be a String; got a #{text.class}!" unless text.is_a?(String)
|
40
|
+
|
40
41
|
opts = Config.process_options(options, :parse)
|
41
42
|
text = text.encode('UTF-8')
|
42
43
|
Node.parse_document(text, text.bytesize, opts, extensions)
|
data/lib/commonmarker/config.rb
CHANGED
@@ -37,16 +37,17 @@ module CommonMarker
|
|
37
37
|
elsif option.is_a?(Array)
|
38
38
|
option = [nil] if option.empty?
|
39
39
|
# neckbearding around. the map will both check the opts and then bitwise-OR it
|
40
|
-
option.map
|
40
|
+
option.map do |o|
|
41
|
+
check_option(o, type)
|
42
|
+
type.to_h[o]
|
43
|
+
end.inject(0, :|)
|
41
44
|
else
|
42
45
|
raise TypeError, "option type must be a valid symbol or array of symbols within the #{type} context"
|
43
46
|
end
|
44
47
|
end
|
45
48
|
|
46
49
|
def self.check_option(option, type)
|
47
|
-
unless type.key?(option)
|
48
|
-
raise TypeError, "option ':#{option}' does not exist for #{type}"
|
49
|
-
end
|
50
|
+
raise TypeError, "option ':#{option}' does not exist for #{type}" unless type.key?(option)
|
50
51
|
end
|
51
52
|
end
|
52
53
|
end
|
data/lib/commonmarker/node.rb
CHANGED
@@ -8,13 +8,13 @@ module CommonMarker
|
|
8
8
|
PP_INDENT_SIZE = 2
|
9
9
|
|
10
10
|
def inspect
|
11
|
-
PP.pp(self,
|
11
|
+
PP.pp(self, +'', Float::INFINITY)
|
12
12
|
end
|
13
13
|
|
14
14
|
# @param [PrettyPrint] pp
|
15
|
-
def pretty_print(
|
16
|
-
|
17
|
-
|
15
|
+
def pretty_print(printer)
|
16
|
+
printer.group(PP_INDENT_SIZE, "#<#{self.class}(#{type}):", '>') do
|
17
|
+
printer.breakable
|
18
18
|
|
19
19
|
attrs = %i[
|
20
20
|
sourcepos
|
@@ -34,22 +34,22 @@ module CommonMarker
|
|
34
34
|
end
|
35
35
|
end.compact
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
printer.seplist(attrs) do |name, value|
|
38
|
+
printer.text "#{name}="
|
39
|
+
printer.pp value
|
40
40
|
end
|
41
41
|
|
42
42
|
if first_child
|
43
|
-
|
44
|
-
|
43
|
+
printer.breakable
|
44
|
+
printer.group(PP_INDENT_SIZE) do
|
45
45
|
children = []
|
46
46
|
node = first_child
|
47
47
|
while node
|
48
48
|
children << node
|
49
49
|
node = node.next
|
50
50
|
end
|
51
|
-
|
52
|
-
|
51
|
+
printer.text 'children='
|
52
|
+
printer.pp children
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
@@ -8,7 +8,7 @@ module CommonMarker
|
|
8
8
|
attr_accessor :in_tight, :warnings, :in_plain
|
9
9
|
def initialize(options: :DEFAULT, extensions: [])
|
10
10
|
@opts = Config.process_options(options, :render)
|
11
|
-
@stream = StringIO.new(''
|
11
|
+
@stream = StringIO.new(+'')
|
12
12
|
@need_blocksep = false
|
13
13
|
@warnings = Set.new []
|
14
14
|
@in_tight = false
|
@@ -34,7 +34,7 @@ module CommonMarker
|
|
34
34
|
@node = node
|
35
35
|
if node.type == :document
|
36
36
|
document(node)
|
37
|
-
|
37
|
+
@stream.string
|
38
38
|
elsif @in_plain && node.type != :text && node.type != :softbreak
|
39
39
|
node.each { |child| render(child) }
|
40
40
|
else
|
@@ -55,11 +55,11 @@ module CommonMarker
|
|
55
55
|
code_block(node)
|
56
56
|
end
|
57
57
|
|
58
|
-
def reference_def(_node)
|
59
|
-
end
|
58
|
+
def reference_def(_node); end
|
60
59
|
|
61
60
|
def cr
|
62
61
|
return if @stream.string.empty? || @stream.string[-1] == "\n"
|
62
|
+
|
63
63
|
out("\n")
|
64
64
|
end
|
65
65
|
|
@@ -111,7 +111,8 @@ module CommonMarker
|
|
111
111
|
)
|
112
112
|
(?=\s|>|/>)
|
113
113
|
}xi,
|
114
|
-
'<\1'
|
114
|
+
'<\1'
|
115
|
+
)
|
115
116
|
else
|
116
117
|
str
|
117
118
|
end
|
@@ -119,6 +120,7 @@ module CommonMarker
|
|
119
120
|
|
120
121
|
def sourcepos(node)
|
121
122
|
return '' unless option_enabled?(:SOURCEPOS)
|
123
|
+
|
122
124
|
s = node.sourcepos
|
123
125
|
" data-sourcepos=\"#{s[:start_line]}:#{s[:start_column]}-" \
|
124
126
|
"#{s[:end_line]}:#{s[:end_column]}\""
|
@@ -2,15 +2,9 @@
|
|
2
2
|
|
3
3
|
module CommonMarker
|
4
4
|
class HtmlRenderer < Renderer
|
5
|
-
def render(node)
|
6
|
-
super(node)
|
7
|
-
end
|
8
|
-
|
9
5
|
def document(_)
|
10
6
|
super
|
11
|
-
if @written_footnote_ix
|
12
|
-
out("</ol>\n</section>\n")
|
13
|
-
end
|
7
|
+
out("</ol>\n</section>\n") if @written_footnote_ix
|
14
8
|
end
|
15
9
|
|
16
10
|
def header(node)
|
@@ -71,12 +65,13 @@ module CommonMarker
|
|
71
65
|
|
72
66
|
def tasklist(node)
|
73
67
|
return '' unless tasklist?(node)
|
68
|
+
|
74
69
|
state = if checked?(node)
|
75
|
-
|
76
|
-
|
77
|
-
|
70
|
+
'checked="" disabled=""'
|
71
|
+
else
|
72
|
+
'disabled=""'
|
78
73
|
end
|
79
|
-
|
74
|
+
"><input type=\"checkbox\" #{state} /"
|
80
75
|
end
|
81
76
|
|
82
77
|
def blockquote(node)
|
@@ -97,9 +92,7 @@ module CommonMarker
|
|
97
92
|
block do
|
98
93
|
if option_enabled?(:GITHUB_PRE_LANG)
|
99
94
|
out("<pre#{sourcepos(node)}")
|
100
|
-
if node.fence_info && !node.fence_info.empty?
|
101
|
-
out(' lang="', node.fence_info.split(/\s+/)[0], '"')
|
102
|
-
end
|
95
|
+
out(' lang="', node.fence_info.split(/\s+/)[0], '"') if node.fence_info && !node.fence_info.empty?
|
103
96
|
out('><code>')
|
104
97
|
else
|
105
98
|
out("<pre#{sourcepos(node)}><code")
|
@@ -142,9 +135,7 @@ module CommonMarker
|
|
142
135
|
|
143
136
|
def link(node)
|
144
137
|
out('<a href="', node.url.nil? ? '' : escape_href(node.url), '"')
|
145
|
-
if node.title && !node.title.empty?
|
146
|
-
out(' title="', escape_html(node.title), '"')
|
147
|
-
end
|
138
|
+
out(' title="', escape_html(node.title), '"') if node.title && !node.title.empty?
|
148
139
|
out('>', :children, '</a>')
|
149
140
|
end
|
150
141
|
|
@@ -153,9 +144,7 @@ module CommonMarker
|
|
153
144
|
plain do
|
154
145
|
out(' alt="', :children, '"')
|
155
146
|
end
|
156
|
-
if node.title && !node.title.empty?
|
157
|
-
out(' title="', escape_html(node.title), '"')
|
158
|
-
end
|
147
|
+
out(' title="', escape_html(node.title), '"') if node.title && !node.title.empty?
|
159
148
|
out(' />')
|
160
149
|
end
|
161
150
|
|
@@ -169,7 +158,7 @@ module CommonMarker
|
|
169
158
|
out('</code>')
|
170
159
|
end
|
171
160
|
|
172
|
-
def linebreak(
|
161
|
+
def linebreak(_node)
|
173
162
|
out("<br />\n")
|
174
163
|
end
|
175
164
|
|
@@ -210,9 +199,9 @@ module CommonMarker
|
|
210
199
|
|
211
200
|
def table_cell(node)
|
212
201
|
align = case @alignments[@column_index]
|
213
|
-
when :left
|
214
|
-
when :right
|
215
|
-
when :center
|
202
|
+
when :left then ' align="left"'
|
203
|
+
when :right then ' align="right"'
|
204
|
+
when :center then ' align="center"'
|
216
205
|
else; ''
|
217
206
|
end
|
218
207
|
out(@in_header ? "<th#{align}#{sourcepos(node)}>" : "<td#{align}#{sourcepos(node)}>", :children, @in_header ? "</th>\n" : "</td>\n")
|
@@ -228,28 +217,27 @@ module CommonMarker
|
|
228
217
|
end
|
229
218
|
|
230
219
|
def footnote_definition(_)
|
231
|
-
|
220
|
+
unless @footnote_ix
|
232
221
|
out("<section class=\"footnotes\">\n<ol>\n")
|
233
222
|
@footnote_ix = 0
|
234
223
|
end
|
235
224
|
|
236
225
|
@footnote_ix += 1
|
237
|
-
out("<li id=\"fn
|
238
|
-
if out_footnote_backref
|
239
|
-
out("\n")
|
240
|
-
end
|
226
|
+
out("<li id=\"fn#{@footnote_ix}\">\n", :children)
|
227
|
+
out("\n") if out_footnote_backref
|
241
228
|
out("</li>\n")
|
242
|
-
|
243
|
-
|
229
|
+
# </ol>
|
230
|
+
# </section>
|
244
231
|
end
|
245
232
|
|
246
233
|
private
|
247
234
|
|
248
235
|
def out_footnote_backref
|
249
236
|
return false if @written_footnote_ix == @footnote_ix
|
237
|
+
|
250
238
|
@written_footnote_ix = @footnote_ix
|
251
239
|
|
252
|
-
out("<a href=\"#fnref
|
240
|
+
out("<a href=\"#fnref#{@footnote_ix}\" class=\"footnote-backref\">↩</a>")
|
253
241
|
true
|
254
242
|
end
|
255
243
|
|
@@ -258,8 +246,7 @@ module CommonMarker
|
|
258
246
|
end
|
259
247
|
|
260
248
|
def checked?(node)
|
261
|
-
node.
|
249
|
+
node.tasklist_item_checked?
|
262
250
|
end
|
263
|
-
|
264
251
|
end
|
265
252
|
end
|