commonmarker 0.20.1 → 0.22.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 +5 -5
- data/README.md +14 -14
- data/Rakefile +16 -4
- data/bin/commonmarker +83 -45
- data/commonmarker.gemspec +9 -13
- data/ext/commonmarker/cmark-gfm-core-extensions.h +24 -1
- data/ext/commonmarker/commonmarker.c +130 -80
- 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 +33 -39
- data/lib/commonmarker/node.rb +2 -2
- data/lib/commonmarker/node/inspect.rb +15 -17
- data/lib/commonmarker/renderer.rb +13 -9
- data/lib/commonmarker/renderer/html_renderer.rb +22 -35
- data/lib/commonmarker/version.rb +1 -1
- data/test/benchmark.rb +1 -5
- data/test/fixtures/strong.md +1 -0
- data/test/fixtures/table.md +10 -0
- data/test/test_attributes.rb +3 -3
- data/test/test_commands.rb +31 -0
- data/test/test_commonmark.rb +13 -13
- data/test/test_doc.rb +29 -29
- data/test/test_encoding.rb +4 -5
- data/test/test_extensions.rb +57 -69
- data/test/test_footnotes.rb +30 -9
- data/test/test_gc.rb +2 -0
- data/test/test_helper.rb +17 -10
- 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 +12 -12
- data/test/test_plaintext.rb +21 -21
- data/test/test_renderer.rb +27 -10
- data/test/test_spec.rb +3 -2
- data/test/test_tasklists.rb +16 -6
- metadata +54 -60
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
@@ -1,52 +1,46 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'ruby-enum'
|
4
3
|
module CommonMarker
|
5
4
|
# For Ruby::Enum, these must be classes, not modules
|
6
5
|
module Config
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
6
|
+
# See https://github.com/github/cmark-gfm/blob/master/src/cmark-gfm.h#L673
|
7
|
+
OPTS = {
|
8
|
+
parse: {
|
9
|
+
DEFAULT: 0,
|
10
|
+
VALIDATE_UTF8: (1 << 9),
|
11
|
+
SMART: (1 << 10),
|
12
|
+
LIBERAL_HTML_TAG: (1 << 12),
|
13
|
+
FOOTNOTES: (1 << 13),
|
14
|
+
STRIKETHROUGH_DOUBLE_TILDE: (1 << 14),
|
15
|
+
UNSAFE: (1 << 17)
|
16
|
+
}.freeze,
|
17
|
+
render: {
|
18
|
+
DEFAULT: 0,
|
19
|
+
SOURCEPOS: (1 << 1),
|
20
|
+
HARDBREAKS: (1 << 2),
|
21
|
+
NOBREAKS: (1 << 4),
|
22
|
+
GITHUB_PRE_LANG: (1 << 11),
|
23
|
+
TABLE_PREFER_STYLE_ATTRIBUTES: (1 << 15),
|
24
|
+
FULL_INFO_STRING: (1 << 16),
|
25
|
+
UNSAFE: (1 << 17),
|
26
|
+
FOOTNOTES: (1 << 13)
|
27
|
+
}.freeze
|
28
|
+
}.freeze
|
31
29
|
|
32
30
|
def self.process_options(option, type)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
case option
|
32
|
+
when Symbol
|
33
|
+
OPTS.fetch(type).fetch(option)
|
34
|
+
when Array
|
35
|
+
raise TypeError if option.none?
|
36
|
+
|
39
37
|
# neckbearding around. the map will both check the opts and then bitwise-OR it
|
40
|
-
|
38
|
+
OPTS.fetch(type).fetch_values(*option).inject(0, :|)
|
41
39
|
else
|
42
|
-
raise TypeError, "option type must be a valid symbol or array of symbols within the #{type} context"
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.check_option(option, type)
|
47
|
-
unless type.key?(option)
|
48
|
-
raise TypeError, "option ':#{option}' does not exist for #{type}"
|
40
|
+
raise TypeError, "option type must be a valid symbol or array of symbols within the #{name}::OPTS[:#{type}] context"
|
49
41
|
end
|
42
|
+
rescue KeyError => e
|
43
|
+
raise TypeError, "option ':#{e.key}' does not exist for #{name}::OPTS[:#{type}]"
|
50
44
|
end
|
51
45
|
end
|
52
46
|
end
|
data/lib/commonmarker/node.rb
CHANGED
@@ -11,7 +11,7 @@ module CommonMarker
|
|
11
11
|
#
|
12
12
|
# blk - A {Proc} representing the action to take for each child
|
13
13
|
def walk(&block)
|
14
|
-
return enum_for(:walk) unless
|
14
|
+
return enum_for(:walk) unless block
|
15
15
|
|
16
16
|
yield self
|
17
17
|
each do |child|
|
@@ -53,7 +53,7 @@ module CommonMarker
|
|
53
53
|
end
|
54
54
|
|
55
55
|
# Public: Iterate over the children (if any) of the current pointer.
|
56
|
-
def each
|
56
|
+
def each
|
57
57
|
return enum_for(:each) unless block_given?
|
58
58
|
|
59
59
|
child = first_child
|
@@ -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
|
-
# @param [PrettyPrint] pp
|
15
|
-
def pretty_print(
|
16
|
-
|
17
|
-
|
14
|
+
# @param printer [PrettyPrint] pp
|
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
|
@@ -27,29 +27,27 @@ module CommonMarker
|
|
27
27
|
list_tight
|
28
28
|
fence_info
|
29
29
|
].map do |name|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
nil
|
34
|
-
end
|
30
|
+
[name, __send__(name)]
|
31
|
+
rescue NodeError
|
32
|
+
nil
|
35
33
|
end.compact
|
36
34
|
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
printer.seplist(attrs) do |name, value|
|
36
|
+
printer.text "#{name}="
|
37
|
+
printer.pp value
|
40
38
|
end
|
41
39
|
|
42
40
|
if first_child
|
43
|
-
|
44
|
-
|
41
|
+
printer.breakable
|
42
|
+
printer.group(PP_INDENT_SIZE) do
|
45
43
|
children = []
|
46
44
|
node = first_child
|
47
45
|
while node
|
48
46
|
children << node
|
49
47
|
node = node.next
|
50
48
|
end
|
51
|
-
|
52
|
-
|
49
|
+
printer.text 'children='
|
50
|
+
printer.pp children
|
53
51
|
end
|
54
52
|
end
|
55
53
|
end
|
@@ -6,9 +6,10 @@ require 'stringio'
|
|
6
6
|
module CommonMarker
|
7
7
|
class Renderer
|
8
8
|
attr_accessor :in_tight, :warnings, :in_plain
|
9
|
+
|
9
10
|
def initialize(options: :DEFAULT, extensions: [])
|
10
11
|
@opts = Config.process_options(options, :render)
|
11
|
-
@stream = StringIO.new(''
|
12
|
+
@stream = StringIO.new(+'')
|
12
13
|
@need_blocksep = false
|
13
14
|
@warnings = Set.new []
|
14
15
|
@in_tight = false
|
@@ -18,11 +19,12 @@ module CommonMarker
|
|
18
19
|
|
19
20
|
def out(*args)
|
20
21
|
args.each do |arg|
|
21
|
-
|
22
|
+
case arg
|
23
|
+
when :children
|
22
24
|
@node.each { |child| out(child) }
|
23
|
-
|
25
|
+
when Array
|
24
26
|
arg.each { |x| render(x) }
|
25
|
-
|
27
|
+
when Node
|
26
28
|
render(arg)
|
27
29
|
else
|
28
30
|
@stream.write(arg)
|
@@ -34,7 +36,7 @@ module CommonMarker
|
|
34
36
|
@node = node
|
35
37
|
if node.type == :document
|
36
38
|
document(node)
|
37
|
-
|
39
|
+
@stream.string
|
38
40
|
elsif @in_plain && node.type != :text && node.type != :softbreak
|
39
41
|
node.each { |child| render(child) }
|
40
42
|
else
|
@@ -55,11 +57,11 @@ module CommonMarker
|
|
55
57
|
code_block(node)
|
56
58
|
end
|
57
59
|
|
58
|
-
def reference_def(_node)
|
59
|
-
end
|
60
|
+
def reference_def(_node); end
|
60
61
|
|
61
62
|
def cr
|
62
63
|
return if @stream.string.empty? || @stream.string[-1] == "\n"
|
64
|
+
|
63
65
|
out("\n")
|
64
66
|
end
|
65
67
|
|
@@ -111,7 +113,8 @@ module CommonMarker
|
|
111
113
|
)
|
112
114
|
(?=\s|>|/>)
|
113
115
|
}xi,
|
114
|
-
'<\1'
|
116
|
+
'<\1'
|
117
|
+
)
|
115
118
|
else
|
116
119
|
str
|
117
120
|
end
|
@@ -119,13 +122,14 @@ module CommonMarker
|
|
119
122
|
|
120
123
|
def sourcepos(node)
|
121
124
|
return '' unless option_enabled?(:SOURCEPOS)
|
125
|
+
|
122
126
|
s = node.sourcepos
|
123
127
|
" data-sourcepos=\"#{s[:start_line]}:#{s[:start_column]}-" \
|
124
128
|
"#{s[:end_line]}:#{s[:end_column]}\""
|
125
129
|
end
|
126
130
|
|
127
131
|
def option_enabled?(opt)
|
128
|
-
(@opts & CommonMarker::Config::
|
132
|
+
(@opts & CommonMarker::Config::OPTS.dig(:render, opt)) != 0
|
129
133
|
end
|
130
134
|
end
|
131
135
|
end
|