commonmarker 0.23.0 → 0.23.4

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.

@@ -38,3 +38,26 @@ void cmark_footnote_create(cmark_map *map, cmark_node *node) {
38
38
  cmark_map *cmark_footnote_map_new(cmark_mem *mem) {
39
39
  return cmark_map_new(mem, footnote_free);
40
40
  }
41
+
42
+ // Before calling `cmark_map_free` on a map with `cmark_footnotes`, first
43
+ // unlink all of the footnote nodes before freeing their memory.
44
+ //
45
+ // Sometimes, two (unused) footnote nodes can end up referencing each other,
46
+ // which as they get freed up by calling `cmark_map_free` -> `footnote_free` ->
47
+ // etc, can lead to a use-after-free error.
48
+ //
49
+ // Better to `unlink` every footnote node first, setting their next, prev, and
50
+ // parent pointers to NULL, and only then walk thru & free them up.
51
+ void cmark_unlink_footnotes_map(cmark_map *map) {
52
+ cmark_map_entry *ref;
53
+ cmark_map_entry *next;
54
+
55
+ ref = map->refs;
56
+ while(ref) {
57
+ next = ref->next;
58
+ if (((cmark_footnote *)ref)->node) {
59
+ cmark_node_unlink(((cmark_footnote *)ref)->node);
60
+ }
61
+ ref = next;
62
+ }
63
+ }
@@ -18,6 +18,8 @@ typedef struct cmark_footnote cmark_footnote;
18
18
  void cmark_footnote_create(cmark_map *map, cmark_node *node);
19
19
  cmark_map *cmark_footnote_map_new(cmark_mem *mem);
20
20
 
21
+ void cmark_unlink_footnotes_map(cmark_map *map);
22
+
21
23
  #ifdef __cplusplus
22
24
  }
23
25
  #endif
@@ -59,16 +59,30 @@ static void filter_html_block(cmark_html_renderer *renderer, uint8_t *data, size
59
59
  cmark_strbuf_put(html, data, (bufsize_t)len);
60
60
  }
61
61
 
62
- static bool S_put_footnote_backref(cmark_html_renderer *renderer, cmark_strbuf *html) {
62
+ static bool S_put_footnote_backref(cmark_html_renderer *renderer, cmark_strbuf *html, cmark_node *node) {
63
63
  if (renderer->written_footnote_ix >= renderer->footnote_ix)
64
64
  return false;
65
65
  renderer->written_footnote_ix = renderer->footnote_ix;
66
66
 
67
- cmark_strbuf_puts(html, "<a href=\"#fnref");
68
- char n[32];
69
- snprintf(n, sizeof(n), "%d", renderer->footnote_ix);
70
- cmark_strbuf_puts(html, n);
71
- cmark_strbuf_puts(html, "\" class=\"footnote-backref\">↩</a>");
67
+ cmark_strbuf_puts(html, "<a href=\"#fnref-");
68
+ houdini_escape_href(html, node->as.literal.data, node->as.literal.len);
69
+ cmark_strbuf_puts(html, "\" class=\"footnote-backref\" data-footnote-backref aria-label=\"Back to content\">↩</a>");
70
+
71
+ if (node->footnote.def_count > 1)
72
+ {
73
+ for(int i = 2; i <= node->footnote.def_count; i++) {
74
+ char n[32];
75
+ snprintf(n, sizeof(n), "%d", i);
76
+
77
+ cmark_strbuf_puts(html, " <a href=\"#fnref-");
78
+ houdini_escape_href(html, node->as.literal.data, node->as.literal.len);
79
+ cmark_strbuf_puts(html, "-");
80
+ cmark_strbuf_puts(html, n);
81
+ cmark_strbuf_puts(html, "\" class=\"footnote-backref\" data-footnote-backref aria-label=\"Back to content\">↩<sup class=\"footnote-ref\">");
82
+ cmark_strbuf_puts(html, n);
83
+ cmark_strbuf_puts(html, "</sup></a>");
84
+ }
85
+ }
72
86
 
73
87
  return true;
74
88
  }
@@ -273,7 +287,7 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node,
273
287
  } else {
274
288
  if (parent->type == CMARK_NODE_FOOTNOTE_DEFINITION && node->next == NULL) {
275
289
  cmark_strbuf_putc(html, ' ');
276
- S_put_footnote_backref(renderer, html);
290
+ S_put_footnote_backref(renderer, html, parent);
277
291
  }
278
292
  cmark_strbuf_puts(html, "</p>\n");
279
293
  }
@@ -392,16 +406,15 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node,
392
406
  case CMARK_NODE_FOOTNOTE_DEFINITION:
393
407
  if (entering) {
394
408
  if (renderer->footnote_ix == 0) {
395
- cmark_strbuf_puts(html, "<section class=\"footnotes\">\n<ol>\n");
409
+ cmark_strbuf_puts(html, "<section class=\"footnotes\" data-footnotes>\n<ol>\n");
396
410
  }
397
411
  ++renderer->footnote_ix;
398
- cmark_strbuf_puts(html, "<li id=\"fn");
399
- char n[32];
400
- snprintf(n, sizeof(n), "%d", renderer->footnote_ix);
401
- cmark_strbuf_puts(html, n);
412
+
413
+ cmark_strbuf_puts(html, "<li id=\"fn-");
414
+ houdini_escape_href(html, node->as.literal.data, node->as.literal.len);
402
415
  cmark_strbuf_puts(html, "\">\n");
403
416
  } else {
404
- if (S_put_footnote_backref(renderer, html)) {
417
+ if (S_put_footnote_backref(renderer, html, node)) {
405
418
  cmark_strbuf_putc(html, '\n');
406
419
  }
407
420
  cmark_strbuf_puts(html, "</li>\n");
@@ -410,12 +423,20 @@ static int S_render_node(cmark_html_renderer *renderer, cmark_node *node,
410
423
 
411
424
  case CMARK_NODE_FOOTNOTE_REFERENCE:
412
425
  if (entering) {
413
- cmark_strbuf_puts(html, "<sup class=\"footnote-ref\"><a href=\"#fn");
414
- cmark_strbuf_put(html, node->as.literal.data, node->as.literal.len);
415
- cmark_strbuf_puts(html, "\" id=\"fnref");
416
- cmark_strbuf_put(html, node->as.literal.data, node->as.literal.len);
417
- cmark_strbuf_puts(html, "\">");
418
- cmark_strbuf_put(html, node->as.literal.data, node->as.literal.len);
426
+ cmark_strbuf_puts(html, "<sup class=\"footnote-ref\"><a href=\"#fn-");
427
+ houdini_escape_href(html, node->parent_footnote_def->as.literal.data, node->parent_footnote_def->as.literal.len);
428
+ cmark_strbuf_puts(html, "\" id=\"fnref-");
429
+ houdini_escape_href(html, node->parent_footnote_def->as.literal.data, node->parent_footnote_def->as.literal.len);
430
+
431
+ if (node->footnote.ref_ix > 1) {
432
+ char n[32];
433
+ snprintf(n, sizeof(n), "%d", node->footnote.ref_ix);
434
+ cmark_strbuf_puts(html, "-");
435
+ cmark_strbuf_puts(html, n);
436
+ }
437
+
438
+ cmark_strbuf_puts(html, "\" data-footnote-ref>");
439
+ houdini_escape_href(html, node->as.literal.data, node->as.literal.len);
419
440
  cmark_strbuf_puts(html, "</a></sup>");
420
441
  }
421
442
  break;
@@ -1137,19 +1137,77 @@ noMatch:
1137
1137
  // What if we're a footnote link?
1138
1138
  if (parser->options & CMARK_OPT_FOOTNOTES &&
1139
1139
  opener->inl_text->next &&
1140
- opener->inl_text->next->type == CMARK_NODE_TEXT &&
1141
- !opener->inl_text->next->next) {
1140
+ opener->inl_text->next->type == CMARK_NODE_TEXT) {
1141
+
1142
1142
  cmark_chunk *literal = &opener->inl_text->next->as.literal;
1143
- if (literal->len > 1 && literal->data[0] == '^') {
1144
- inl = make_simple(subj->mem, CMARK_NODE_FOOTNOTE_REFERENCE);
1145
- inl->as.literal = cmark_chunk_dup(literal, 1, literal->len - 1);
1146
- inl->start_line = inl->end_line = subj->line;
1147
- inl->start_column = opener->inl_text->start_column;
1148
- inl->end_column = subj->pos + subj->column_offset + subj->block_offset;
1149
- cmark_node_insert_before(opener->inl_text, inl);
1150
- cmark_node_free(opener->inl_text->next);
1151
- cmark_node_free(opener->inl_text);
1143
+
1144
+ // look back to the opening '[', and skip ahead to the next character
1145
+ // if we're looking at a '[^' sequence, and there is other text or nodes
1146
+ // after the ^, let's call it a footnote reference.
1147
+ if ((literal->len > 0 && literal->data[0] == '^') && (literal->len > 1 || opener->inl_text->next->next)) {
1148
+
1149
+ // Before we got this far, the `handle_close_bracket` function may have
1150
+ // advanced the current state beyond our footnote's actual closing
1151
+ // bracket, ie if it went looking for a `link_label`.
1152
+ // Let's just rewind the subject's position:
1153
+ subj->pos = initial_pos;
1154
+
1155
+ cmark_node *fnref = make_simple(subj->mem, CMARK_NODE_FOOTNOTE_REFERENCE);
1156
+
1157
+ // the start and end of the footnote ref is the opening and closing brace
1158
+ // i.e. the subject's current position, and the opener's start_column
1159
+ int fnref_end_column = subj->pos + subj->column_offset + subj->block_offset;
1160
+ int fnref_start_column = opener->inl_text->start_column;
1161
+
1162
+ // any given node delineates a substring of the line being processed,
1163
+ // with the remainder of the line being pointed to thru its 'literal'
1164
+ // struct member.
1165
+ // here, we copy the literal's pointer, moving it past the '^' character
1166
+ // for a length equal to the size of footnote reference text.
1167
+ // i.e. end_col minus start_col, minus the [ and the ^ characters
1168
+ //
1169
+ // this copies the footnote reference string, even if between the
1170
+ // `opener` and the subject's current position there are other nodes
1171
+ //
1172
+ // (first, check for underflows)
1173
+ if ((fnref_start_column + 2) <= fnref_end_column) {
1174
+ fnref->as.literal = cmark_chunk_dup(literal, 1, (fnref_end_column - fnref_start_column) - 2);
1175
+ } else {
1176
+ fnref->as.literal = cmark_chunk_dup(literal, 1, 0);
1177
+ }
1178
+
1179
+ fnref->start_line = fnref->end_line = subj->line;
1180
+ fnref->start_column = fnref_start_column;
1181
+ fnref->end_column = fnref_end_column;
1182
+
1183
+ // we then replace the opener with this new fnref node, the net effect
1184
+ // being replacing the opening '[' text node with a `^footnote-ref]` node.
1185
+ cmark_node_insert_before(opener->inl_text, fnref);
1186
+
1152
1187
  process_emphasis(parser, subj, opener->previous_delimiter);
1188
+ // sometimes, the footnote reference text gets parsed into multiple nodes
1189
+ // i.e. '[^example]' parsed into '[', '^exam', 'ple]'.
1190
+ // this happens for ex with the autolink extension. when the autolinker
1191
+ // finds the 'w' character, it will split the text into multiple nodes
1192
+ // in hopes of being able to match a 'www.' substring.
1193
+ //
1194
+ // because this function is called one character at a time via the
1195
+ // `parse_inlines` function, and the current subj->pos is pointing at the
1196
+ // closing ] brace, and because we copy all the text between the [ ]
1197
+ // braces, we should be able to safely ignore and delete any nodes after
1198
+ // the opener->inl_text->next.
1199
+ //
1200
+ // therefore, here we walk thru the list and free them all up
1201
+ cmark_node *next_node;
1202
+ cmark_node *current_node = opener->inl_text->next;
1203
+ while(current_node) {
1204
+ next_node = current_node->next;
1205
+ cmark_node_free(current_node);
1206
+ current_node = next_node;
1207
+ }
1208
+
1209
+ cmark_node_free(opener->inl_text);
1210
+
1153
1211
  pop_bracket(subj);
1154
1212
  return NULL;
1155
1213
  }
@@ -76,6 +76,13 @@ struct cmark_node {
76
76
 
77
77
  cmark_syntax_extension *extension;
78
78
 
79
+ union {
80
+ int ref_ix;
81
+ int def_count;
82
+ } footnote;
83
+
84
+ cmark_node *parent_footnote_def;
85
+
79
86
  union {
80
87
  cmark_chunk literal;
81
88
  cmark_list list;
@@ -114,60 +114,94 @@ static cmark_strbuf *unescape_pipes(cmark_mem *mem, unsigned char *string, bufsi
114
114
  static table_row *row_from_string(cmark_syntax_extension *self,
115
115
  cmark_parser *parser, unsigned char *string,
116
116
  int len) {
117
+ // Parses a single table row. It has the following form:
118
+ // `delim? table_cell (delim table_cell)* delim? newline`
119
+ // Note that cells are allowed to be empty.
120
+ //
121
+ // From the GitHub-flavored Markdown specification:
122
+ //
123
+ // > Each row consists of cells containing arbitrary text, in which inlines
124
+ // > are parsed, separated by pipes (|). A leading and trailing pipe is also
125
+ // > recommended for clarity of reading, and if there’s otherwise parsing
126
+ // > ambiguity.
127
+
117
128
  table_row *row = NULL;
118
129
  bufsize_t cell_matched = 1, pipe_matched = 1, offset;
119
- int cell_end_offset;
130
+ int expect_more_cells = 1;
131
+ int row_end_offset = 0;
132
+ int int_overflow_abort = 0;
120
133
 
121
134
  row = (table_row *)parser->mem->calloc(1, sizeof(table_row));
122
135
  row->n_columns = 0;
123
136
  row->cells = NULL;
124
137
 
138
+ // Scan past the (optional) leading pipe.
125
139
  offset = scan_table_cell_end(string, len, 0);
126
140
 
127
141
  // Parse the cells of the row. Stop if we reach the end of the input, or if we
128
142
  // cannot detect any more cells.
129
- while (offset < len && (cell_matched || pipe_matched)) {
143
+ while (offset < len && expect_more_cells) {
130
144
  cell_matched = scan_table_cell(string, len, offset);
131
145
  pipe_matched = scan_table_cell_end(string, len, offset + cell_matched);
132
146
 
133
147
  if (cell_matched || pipe_matched) {
134
- cell_end_offset = offset + cell_matched - 1;
148
+ // We are guaranteed to have a cell, since (1) either we found some
149
+ // content and cell_matched, or (2) we found an empty cell followed by a
150
+ // pipe.
151
+ cmark_strbuf *cell_buf = unescape_pipes(parser->mem, string + offset,
152
+ cell_matched);
153
+ cmark_strbuf_trim(cell_buf);
154
+
155
+ node_cell *cell = (node_cell *)parser->mem->calloc(1, sizeof(*cell));
156
+ cell->buf = cell_buf;
157
+ cell->start_offset = offset;
158
+ cell->end_offset = offset + cell_matched - 1;
159
+
160
+ while (cell->start_offset > 0 && string[cell->start_offset - 1] != '|') {
161
+ --cell->start_offset;
162
+ ++cell->internal_offset;
163
+ }
164
+
165
+ // make sure we never wrap row->n_columns
166
+ // offset will != len and our exit will clean up as intended
167
+ if (row->n_columns == UINT16_MAX) {
168
+ int_overflow_abort = 1;
169
+ break;
170
+ }
171
+ row->n_columns += 1;
172
+ row->cells = cmark_llist_append(parser->mem, row->cells, cell);
173
+ }
135
174
 
136
- if (string[cell_end_offset] == '\n' || string[cell_end_offset] == '\r') {
137
- row->paragraph_offset = cell_end_offset;
175
+ offset += cell_matched + pipe_matched;
176
+
177
+ if (pipe_matched) {
178
+ expect_more_cells = 1;
179
+ } else {
180
+ // We've scanned the last cell. Check if we have reached the end of the row
181
+ row_end_offset = scan_table_row_end(string, len, offset);
182
+ offset += row_end_offset;
183
+
184
+ // If the end of the row is not the end of the input,
185
+ // the row is not a real row but potentially part of the paragraph
186
+ // preceding the table.
187
+ if (row_end_offset && offset != len) {
188
+ row->paragraph_offset = offset;
138
189
 
139
190
  cmark_llist_free_full(parser->mem, row->cells, (cmark_free_func)free_table_cell);
140
191
  row->cells = NULL;
141
192
  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
193
 
157
- row->n_columns += 1;
158
- row->cells = cmark_llist_append(parser->mem, row->cells, cell);
159
- }
160
- }
161
-
162
- offset += cell_matched + pipe_matched;
194
+ // Scan past the (optional) leading pipe.
195
+ offset += scan_table_cell_end(string, len, offset);
163
196
 
164
- if (!pipe_matched) {
165
- pipe_matched = scan_table_row_end(string, len, offset);
166
- offset += pipe_matched;
197
+ expect_more_cells = 1;
198
+ } else {
199
+ expect_more_cells = 0;
200
+ }
167
201
  }
168
202
  }
169
203
 
170
- if (offset != len || !row->n_columns) {
204
+ if (offset != len || row->n_columns == 0 || int_overflow_abort) {
171
205
  free_table_row(parser->mem, row);
172
206
  row = NULL;
173
207
  }
@@ -199,8 +233,6 @@ static cmark_node *try_opening_table_header(cmark_syntax_extension *self,
199
233
  cmark_parser *parser,
200
234
  cmark_node *parent_container,
201
235
  unsigned char *input, int len) {
202
- bufsize_t matched =
203
- scan_table_start(input, len, cmark_parser_get_first_nonspace(parser));
204
236
  cmark_node *table_header;
205
237
  table_row *header_row = NULL;
206
238
  table_row *marker_row = NULL;
@@ -208,41 +240,48 @@ static cmark_node *try_opening_table_header(cmark_syntax_extension *self,
208
240
  const char *parent_string;
209
241
  uint16_t i;
210
242
 
211
- if (!matched)
212
- return parent_container;
213
-
214
- parent_string = cmark_node_get_string_content(parent_container);
215
-
216
- cmark_arena_push();
217
-
218
- header_row = row_from_string(self, parser, (unsigned char *)parent_string,
219
- (int)strlen(parent_string));
220
-
221
- if (!header_row) {
222
- free_table_row(parser->mem, header_row);
223
- cmark_arena_pop();
243
+ if (!scan_table_start(input, len, cmark_parser_get_first_nonspace(parser))) {
224
244
  return parent_container;
225
245
  }
226
246
 
247
+ // Since scan_table_start was successful, we must have a marker row.
227
248
  marker_row = row_from_string(self, parser,
228
249
  input + cmark_parser_get_first_nonspace(parser),
229
250
  len - cmark_parser_get_first_nonspace(parser));
230
-
251
+ // assert may be optimized out, don't rely on it for security boundaries
252
+ if (!marker_row) {
253
+ return parent_container;
254
+ }
255
+
231
256
  assert(marker_row);
232
257
 
233
- if (header_row->n_columns != marker_row->n_columns) {
234
- free_table_row(parser->mem, header_row);
258
+ cmark_arena_push();
259
+
260
+ // Check for a matching header row. We call `row_from_string` with the entire
261
+ // (potentially long) parent container as input, but this should be safe since
262
+ // `row_from_string` bails out early if it does not find a row.
263
+ parent_string = cmark_node_get_string_content(parent_container);
264
+ header_row = row_from_string(self, parser, (unsigned char *)parent_string,
265
+ (int)strlen(parent_string));
266
+ if (!header_row || header_row->n_columns != marker_row->n_columns) {
235
267
  free_table_row(parser->mem, marker_row);
268
+ free_table_row(parser->mem, header_row);
236
269
  cmark_arena_pop();
237
270
  return parent_container;
238
271
  }
239
272
 
240
273
  if (cmark_arena_pop()) {
274
+ marker_row = row_from_string(
275
+ self, parser, input + cmark_parser_get_first_nonspace(parser),
276
+ len - cmark_parser_get_first_nonspace(parser));
241
277
  header_row = row_from_string(self, parser, (unsigned char *)parent_string,
242
278
  (int)strlen(parent_string));
243
- marker_row = row_from_string(self, parser,
244
- input + cmark_parser_get_first_nonspace(parser),
245
- len - cmark_parser_get_first_nonspace(parser));
279
+ // row_from_string can return NULL, add additional check to ensure n_columns match
280
+ if (!marker_row || !header_row || header_row->n_columns != marker_row->n_columns) {
281
+ free_table_row(parser->mem, marker_row);
282
+ free_table_row(parser->mem, header_row);
283
+ return parent_container;
284
+ }
246
285
  }
247
286
 
248
287
  if (!cmark_node_set_type(parent_container, CMARK_NODE_TABLE)) {
@@ -257,13 +296,13 @@ static cmark_node *try_opening_table_header(cmark_syntax_extension *self,
257
296
  }
258
297
 
259
298
  cmark_node_set_syntax_extension(parent_container, self);
260
-
261
299
  parent_container->as.opaque = parser->mem->calloc(1, sizeof(node_table));
262
-
263
300
  set_n_table_columns(parent_container, header_row->n_columns);
264
301
 
302
+ // allocate alignments based on marker_row->n_columns
303
+ // since we populate the alignments array based on marker_row->cells
265
304
  uint8_t *alignments =
266
- (uint8_t *)parser->mem->calloc(header_row->n_columns, sizeof(uint8_t));
305
+ (uint8_t *)parser->mem->calloc(marker_row->n_columns, sizeof(uint8_t));
267
306
  cmark_llist *it = marker_row->cells;
268
307
  for (i = 0; it; it = it->next, ++i) {
269
308
  node_cell *node = (node_cell *)it->data;
@@ -332,6 +371,12 @@ static cmark_node *try_opening_table_row(cmark_syntax_extension *self,
332
371
  row = row_from_string(self, parser, input + cmark_parser_get_first_nonspace(parser),
333
372
  len - cmark_parser_get_first_nonspace(parser));
334
373
 
374
+ if (!row) {
375
+ // clean up the dangling node
376
+ cmark_node_free(table_row_block);
377
+ return NULL;
378
+ }
379
+
335
380
  {
336
381
  cmark_llist *tmp;
337
382
  int i, table_columns = get_n_table_columns(parent_container);
@@ -7,23 +7,28 @@ module CommonMarker
7
7
  OPTS = {
8
8
  parse: {
9
9
  DEFAULT: 0,
10
+ SOURCEPOS: (1 << 1),
11
+ UNSAFE: (1 << 17),
10
12
  VALIDATE_UTF8: (1 << 9),
11
13
  SMART: (1 << 10),
12
14
  LIBERAL_HTML_TAG: (1 << 12),
13
15
  FOOTNOTES: (1 << 13),
14
- STRIKETHROUGH_DOUBLE_TILDE: (1 << 14),
15
- UNSAFE: (1 << 17)
16
+ STRIKETHROUGH_DOUBLE_TILDE: (1 << 14)
16
17
  }.freeze,
17
18
  render: {
18
19
  DEFAULT: 0,
19
20
  SOURCEPOS: (1 << 1),
20
21
  HARDBREAKS: (1 << 2),
22
+ UNSAFE: (1 << 17),
21
23
  NOBREAKS: (1 << 4),
24
+ VALIDATE_UTF8: (1 << 9),
25
+ SMART: (1 << 10),
22
26
  GITHUB_PRE_LANG: (1 << 11),
27
+ LIBERAL_HTML_TAG: (1 << 12),
28
+ FOOTNOTES: (1 << 13),
29
+ STRIKETHROUGH_DOUBLE_TILDE: (1 << 14),
23
30
  TABLE_PREFER_STYLE_ATTRIBUTES: (1 << 15),
24
- FULL_INFO_STRING: (1 << 16),
25
- UNSAFE: (1 << 17),
26
- FOOTNOTES: (1 << 13)
31
+ FULL_INFO_STRING: (1 << 16)
27
32
  }.freeze,
28
33
  format: %i[html xml commonmark plaintext].freeze
29
34
  }.freeze
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commonmarker/node/inspect'
4
+
5
+ module CommonMarker
6
+ class RenderError < StandardError
7
+ PREAMBLE = 'There was an error rendering'
8
+ def initialize(error)
9
+ super("#{PREAMBLE}: #{error.class} #{error.message}")
10
+ end
11
+ end
12
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CommonMarker
4
- VERSION = '0.23.0'
4
+ VERSION = '0.23.4'
5
5
  end
data/lib/commonmarker.rb CHANGED
@@ -23,9 +23,7 @@ module CommonMarker
23
23
  raise TypeError, "text must be a String; got a #{text.class}!" unless text.is_a?(String)
24
24
 
25
25
  opts = Config.process_options(options, :render)
26
- text = text.encode('UTF-8')
27
- html = Node.markdown_to_html(text, opts, extensions)
28
- html.force_encoding('UTF-8')
26
+ Node.markdown_to_html(text.encode('UTF-8'), opts, extensions)
29
27
  end
30
28
 
31
29
  # Public: Parses a Markdown string into a `document` node.
data/test/benchmark.rb CHANGED
@@ -1,32 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'benchmark/ips'
3
4
  require 'commonmarker'
4
- require 'github/markdown'
5
5
  require 'redcarpet'
6
6
  require 'kramdown'
7
7
  require 'benchmark'
8
8
 
9
- def dobench(name, &blk)
10
- puts name
11
- puts Benchmark.measure(&blk)
12
- end
9
+ benchinput = File.read('test/benchinput.md').freeze
13
10
 
14
- benchinput = File.open('test/benchinput.md', 'r').read
11
+ printf("input size = %<bytes>d bytes\n\n", { bytes: benchinput.bytesize })
15
12
 
16
- printf("input size = %<bytes>d bytes\n\n", benchinput.bytesize)
13
+ Benchmark.ips do |x|
14
+ x.report('redcarpet') do
15
+ Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: false, tables: false).render(benchinput)
16
+ end
17
17
 
18
- dobench('redcarpet') do
19
- Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: false, tables: false).render(benchinput)
20
- end
18
+ x.report('commonmarker with to_html') do
19
+ CommonMarker.render_html(benchinput)
20
+ end
21
21
 
22
- dobench('commonmarker with to_html') do
23
- CommonMarker.render_html(benchinput)
24
- end
22
+ x.report('commonmarker with to_xml') do
23
+ CommonMarker.render_html(benchinput)
24
+ end
25
25
 
26
- dobench('commonmarker with ruby HtmlRenderer') do
27
- CommonMarker::HtmlRenderer.new.render(CommonMarker.render_doc(benchinput))
28
- end
26
+ x.report('commonmarker with ruby HtmlRenderer') do
27
+ CommonMarker::HtmlRenderer.new.render(CommonMarker.render_doc(benchinput))
28
+ end
29
+
30
+ x.report('commonmarker with render_doc.to_html') do
31
+ CommonMarker.render_doc(benchinput, :DEFAULT, [:autolink]).to_html(:DEFAULT, [:autolink])
32
+ end
33
+
34
+ x.report('kramdown') do
35
+ Kramdown::Document.new(benchinput).to_html(benchinput)
36
+ end
29
37
 
30
- dobench('kramdown') do
31
- Kramdown::Document.new(benchinput).to_html(benchinput)
38
+ x.compare!
32
39
  end
data/test/test_basics.rb CHANGED
@@ -15,4 +15,21 @@ class TestBasics < Minitest::Test
15
15
  html = CommonMarker.render_html('Hi *there*')
16
16
  assert_equal "<p>Hi <em>there</em></p>\n", html
17
17
  end
18
+
19
+ # basic test that just checks if every option is accepted & no errors are thrown
20
+ def test_accept_every_option
21
+ text = "Hello **world** -- how are _you_ today? I'm ~~fine~~, ~yourself~?"
22
+ parse_opt = %i[SOURCEPOS UNSAFE VALIDATE_UTF8 SMART LIBERAL_HTML_TAG FOOTNOTES STRIKETHROUGH_DOUBLE_TILDE]
23
+ render_opt = parse_opt + %i[HARDBREAKS NOBREAKS GITHUB_PRE_LANG TABLE_PREFER_STYLE_ATTRIBUTES FULL_INFO_STRING]
24
+
25
+ extensions = %i[table tasklist strikethrough autolink tagfilter]
26
+
27
+ assert_equal "<p>Hello <strong>world</strong> – how are <em>you</em> today? I’m <del>fine</del>, ~yourself~?</p>\n", CommonMarker.render_doc(text, parse_opt, extensions).to_html
28
+
29
+ # NOTE: how tho the doc returned has sourcepos info, by default the renderer
30
+ # won't emit it. for that we need to pass in the render opt
31
+ assert_equal "<p data-sourcepos=\"1:1-1:65\">Hello <strong>world</strong> – how are <em>you</em> today? I’m <del>fine</del>, ~yourself~?</p>\n", CommonMarker.render_doc(text, parse_opt, extensions).to_html(render_opt, extensions)
32
+
33
+ assert_equal "<p data-sourcepos=\"1:1-1:65\">Hello <strong>world</strong> – how are <em>you</em> today? I’m <del>fine</del>, ~yourself~?</p>\n", CommonMarker.render_html(text, parse_opt, extensions)
34
+ end
18
35
  end
@@ -29,6 +29,9 @@ class TestExtensions < Minitest::Test
29
29
  doc = CommonMarker.render_doc('~a~ ~~b~~ ~~~c~~~', :STRIKETHROUGH_DOUBLE_TILDE, [:strikethrough])
30
30
  assert_equal("<p>~a~ <del>b</del> ~~~c~~~</p>\n", doc.to_html)
31
31
 
32
+ html = CommonMarker.render_html('~a~ ~~b~~ ~~~c~~~', :STRIKETHROUGH_DOUBLE_TILDE, [:strikethrough])
33
+ assert_equal("<p>~a~ <del>b</del> ~~~c~~~</p>\n", html)
34
+
32
35
  CommonMarker.render_html(@markdown, :DEFAULT, %i[table strikethrough]).tap do |out|
33
36
  refute_includes out, '| a'
34
37
  refute_includes out, '| <strong>x</strong>'