Skip to content

Commit cea3e64

Browse files
iCharlesHuQuietMisdreavus
authored andcommitted
Add custom attributes using ^[foo][N] syntax
rdar://79015293
1 parent 7586471 commit cea3e64

File tree

6 files changed

+224
-10
lines changed

6 files changed

+224
-10
lines changed

api_test/main.c

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,7 +1222,7 @@ static void check_markdown_attributes_node(test_batch_runner *runner, char *mark
12221222
cmark_node_free(doc);
12231223
}
12241224

1225-
static void verify_custome_attributes_node(test_batch_runner *runner) {
1225+
static void verify_custom_attributes_node(test_batch_runner *runner) {
12261226
// Should produce a TEXT node since there's no `()` to signify attributes
12271227
check_markdown_attributes_node(runner, "^[]", CMARK_NODE_TEXT, NULL);
12281228
check_markdown_attributes_node(runner, "^[](", CMARK_NODE_TEXT, NULL);
@@ -1234,6 +1234,103 @@ static void verify_custome_attributes_node(test_batch_runner *runner) {
12341234
check_markdown_attributes_node(runner, "^[](rainbow: 'extreme')", CMARK_NODE_ATTRIBUTE, "rainbow: 'extreme'");
12351235
}
12361236

1237+
static cmark_node* parse_custom_attributues_footnote(test_batch_runner *runner, const char *markdown, cmark_node **retdoc) {
1238+
cmark_node *doc = cmark_parse_document(markdown, strlen(markdown), CMARK_OPT_DEFAULT);
1239+
cmark_node *pg = cmark_node_first_child(doc);
1240+
INT_EQ(runner, cmark_node_get_type(pg), CMARK_NODE_PARAGRAPH, "markdown '%s' did not produce a paragraph node", markdown);
1241+
*retdoc = doc;
1242+
return cmark_node_first_child(pg);
1243+
}
1244+
1245+
static void verify_custom_attributes_footnote_basic(test_batch_runner *runner) {
1246+
static const char markdown[] =
1247+
"^[caffe][1]\n"
1248+
"\n"
1249+
"^[1]: rainbow: 'extreme', colors: { r: 255, g: 0, b: 0 }, corgicopter: true";
1250+
cmark_node *doc;
1251+
cmark_node *attributeNode = parse_custom_attributues_footnote(runner, markdown, &doc);
1252+
INT_EQ(runner, cmark_node_get_type(attributeNode), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1253+
STR_EQ(runner, cmark_node_get_attributes(attributeNode),
1254+
"rainbow: 'extreme', colors: { r: 255, g: 0, b: 0 }, corgicopter: true",
1255+
"markdown '%s' did not produce the right attribute in footnote", markdown);
1256+
1257+
cmark_node_free(doc);
1258+
}
1259+
1260+
static void verify_custom_attributes_footnote_multiple_footnotes(test_batch_runner *runner) {
1261+
static const char markdown[] =
1262+
"^[food][1] and ^[drinks][2]\n"
1263+
"\n"
1264+
"^[1]: rainbow: 'fun'\n"
1265+
"^[2]: magic: 42";
1266+
cmark_node *doc;
1267+
cmark_node *attributeNode1 = parse_custom_attributues_footnote(runner, markdown, &doc);
1268+
INT_EQ(runner, cmark_node_get_type(attributeNode1), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1269+
STR_EQ(runner, cmark_node_get_attributes(attributeNode1), "rainbow: 'fun'", "markdown '%s' did not produce the right attribute in footnote", markdown);
1270+
cmark_node *textNode = cmark_node_next(attributeNode1); // "and"
1271+
cmark_node *attributeNode2 = cmark_node_next(textNode);
1272+
INT_EQ(runner, cmark_node_get_type(attributeNode2), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1273+
STR_EQ(runner, cmark_node_get_attributes(attributeNode2), "magic: 42", "markdown '%s' did not produce the right attribute in footnote", markdown);
1274+
1275+
cmark_node_free(doc);
1276+
}
1277+
1278+
static void verify_custom_attributes_footnote_reuse(test_batch_runner *runner) {
1279+
static const char markdown[] =
1280+
"^[pizza][1], ^[sandwich][2], ^[ice cream][2], and ^[salad][1]\n"
1281+
"\n"
1282+
"^[1]: has_tomato: true\n"
1283+
"^[2]: price: 12";
1284+
cmark_node *doc;
1285+
cmark_node *pizzaNode = parse_custom_attributues_footnote(runner, markdown, &doc);
1286+
INT_EQ(runner, cmark_node_get_type(pizzaNode), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1287+
STR_EQ(runner, cmark_node_get_attributes(pizzaNode), "has_tomato: true", "markdown '%s' did not produce the right attribute in footnote", markdown);
1288+
cmark_node *sandwichNode = cmark_node_next(cmark_node_next(pizzaNode));
1289+
INT_EQ(runner, cmark_node_get_type(sandwichNode), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1290+
STR_EQ(runner, cmark_node_get_attributes(sandwichNode), "price: 12", "markdown '%s' did not produce the right attribute in footnote", markdown);
1291+
cmark_node *icecreamNode = cmark_node_next(cmark_node_next(sandwichNode));
1292+
INT_EQ(runner, cmark_node_get_type(icecreamNode), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1293+
STR_EQ(runner, cmark_node_get_attributes(icecreamNode), "price: 12", "markdown '%s' did not produce the right attribute in footnote", markdown);
1294+
cmark_node *saladNode = cmark_node_next(cmark_node_next(icecreamNode));
1295+
INT_EQ(runner, cmark_node_get_type(saladNode), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1296+
STR_EQ(runner, cmark_node_get_attributes(saladNode), "has_tomato: true", "markdown '%s' did not produce the right attribute in footnote", markdown);
1297+
1298+
cmark_node_free(doc);
1299+
}
1300+
1301+
static void verify_custom_attributes_footnote_mixed_content(test_batch_runner *runner) {
1302+
static const char markdown[] =
1303+
"^[attribute1][1], [a link][2], ^[attribute2][3], ^[attribute3][1]\n"
1304+
"\n"
1305+
"^[1]: rainbow: 'fun'\n"
1306+
"[2]: https://www.example.com\n"
1307+
"Lorem ipsum\n"
1308+
"\n"
1309+
"^[3]: universe: 42\n";
1310+
cmark_node *doc;
1311+
cmark_node *attributeNode1 = parse_custom_attributues_footnote(runner, markdown, &doc);
1312+
INT_EQ(runner, cmark_node_get_type(attributeNode1), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1313+
STR_EQ(runner, cmark_node_get_attributes(attributeNode1), "rainbow: 'fun'", "markdown '%s' did not produce the right attribute in footnote", markdown);
1314+
cmark_node *linkNode = cmark_node_next(cmark_node_next(attributeNode1));
1315+
INT_EQ(runner, cmark_node_get_type(linkNode), CMARK_NODE_LINK, "markdown '%s' did not produce an link node", markdown);
1316+
STR_EQ(runner, cmark_node_get_url(linkNode), "https://www.example.com", "markdown '%s' did not produce the right link in footnote", markdown);
1317+
cmark_node *attributeNode2 = cmark_node_next(cmark_node_next(linkNode));
1318+
INT_EQ(runner, cmark_node_get_type(attributeNode2), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1319+
STR_EQ(runner, cmark_node_get_attributes(attributeNode2), "universe: 42", "markdown '%s' did not produce the right attribute in footnote", markdown);
1320+
cmark_node *attributeNode3 = cmark_node_next(cmark_node_next(attributeNode2));
1321+
INT_EQ(runner, cmark_node_get_type(attributeNode3), CMARK_NODE_ATTRIBUTE, "markdown '%s' did not produce an attribute node", markdown);
1322+
STR_EQ(runner, cmark_node_get_attributes(attributeNode3), "rainbow: 'fun'", "markdown '%s' did not produce the right attribute in footnote", markdown);
1323+
1324+
cmark_node_free(doc);
1325+
}
1326+
1327+
static void verify_custom_attributes_node_with_footnote(test_batch_runner *runner) {
1328+
verify_custom_attributes_footnote_basic(runner);
1329+
verify_custom_attributes_footnote_multiple_footnotes(runner);
1330+
verify_custom_attributes_footnote_reuse(runner);
1331+
verify_custom_attributes_footnote_mixed_content(runner);
1332+
}
1333+
12371334
typedef void (*reentrant_call_func) (void);
12381335

12391336
static cmark_node *reentrant_parse_inline_ext(cmark_syntax_extension *self, cmark_parser *parser,
@@ -1327,7 +1424,8 @@ int main() {
13271424
ref_source_pos(runner);
13281425
inline_only_opt(runner);
13291426
preserve_whitespace_opt(runner);
1330-
verify_custome_attributes_node(runner);
1427+
verify_custom_attributes_node(runner);
1428+
verify_custom_attributes_node_with_footnote(runner);
13311429
parser_interrupt(runner);
13321430

13331431
test_print_summary(runner);

src/blocks.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,10 @@ static bool resolve_reference_link_definitions(
284284
bufsize_t pos;
285285
cmark_strbuf *node_content = &b->content;
286286
cmark_chunk chunk = {node_content->ptr, node_content->size, 0};
287-
while (chunk.len && chunk.data[0] == '[' &&
288-
(pos = cmark_parse_reference_inline(parser->mem, &chunk,
289-
parser->refmap))) {
287+
while ((chunk.len && chunk.data[0] == '[' &&
288+
(pos = cmark_parse_reference_inline(parser->mem, &chunk, parser->refmap))) ||
289+
(chunk.len && chunk.data[0] == '^' && chunk.data[1] == '[' &&
290+
(pos = cmark_parse_reference_attributes_inline(parser->mem, &chunk, parser->refmap)))) {
290291

291292
chunk.data += pos;
292293
chunk.len -= pos;

src/include/inlines.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ extern "C" {
1111

1212
cmark_chunk cmark_clean_url(cmark_mem *mem, cmark_chunk *url);
1313
cmark_chunk cmark_clean_title(cmark_mem *mem, cmark_chunk *title);
14+
cmark_chunk cmark_clean_attributes(cmark_mem *mem, cmark_chunk *attributes);
1415

1516
CMARK_GFM_EXPORT
1617
void cmark_parse_inlines(cmark_parser *parser,
@@ -21,6 +22,9 @@ void cmark_parse_inlines(cmark_parser *parser,
2122
bufsize_t cmark_parse_reference_inline(cmark_mem *mem, cmark_chunk *input,
2223
cmark_map *refmap);
2324

25+
bufsize_t cmark_parse_reference_attributes_inline(cmark_mem *mem, cmark_chunk *input,
26+
cmark_map *refmap);
27+
2428
void cmark_inlines_add_special_character(cmark_parser *parser, unsigned char c, bool emphasis);
2529
void cmark_inlines_remove_special_character(cmark_parser *parser, unsigned char c, bool emphasis);
2630

src/include/references.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ extern "C" {
99

1010
struct cmark_reference {
1111
cmark_map_entry entry;
12+
bool is_attributes_reference;
1213
cmark_chunk url;
1314
cmark_chunk title;
15+
cmark_chunk attributes;
1416
};
1517

1618
typedef struct cmark_reference cmark_reference;
1719

1820
void cmark_reference_create(cmark_map *map, cmark_chunk *label,
1921
cmark_chunk *url, cmark_chunk *title);
22+
void cmark_reference_create_attributes(cmark_map *map, cmark_chunk *label,
23+
cmark_chunk *attributes);
2024
cmark_map *cmark_reference_map_new(cmark_mem *mem);
2125

2226
#ifdef __cplusplus

src/inlines.c

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,12 @@ cmark_chunk cmark_clean_title(cmark_mem *mem, cmark_chunk *title) {
878878
return cmark_chunk_buf_detach(&buf);
879879
}
880880

881+
// Clean custom attributes. This function uses `cmark_clean_url` internaly
882+
// because the requirements are the same
883+
cmark_chunk cmark_clean_attributes(cmark_mem *mem, cmark_chunk *attributes) {
884+
return cmark_clean_url(mem, attributes);
885+
}
886+
881887
// Parse an autolink or HTML tag.
882888
// Assumes the subject has a '<' character at the current position.
883889
static cmark_node *handle_pointy_brace(subject *subj, int options) {
@@ -933,10 +939,19 @@ static cmark_node *handle_pointy_brace(subject *subj, int options) {
933939
// Note: unescaped brackets are not allowed in labels.
934940
// The label begins with `[` and ends with the first `]` character
935941
// encountered. Backticks in labels do not start code spans.
936-
static int link_label(subject *subj, cmark_chunk *raw_label) {
942+
static int link_label(subject *subj, cmark_chunk *raw_label, bool parse_attribute_label) {
937943
bufsize_t startpos = subj->pos;
938944
int length = 0;
939945
unsigned char c;
946+
947+
// If we are parsing attribute label, advance past ^
948+
if (parse_attribute_label) {
949+
if (peek_char(subj) == '^') {
950+
advance(subj);
951+
} else {
952+
return 0;
953+
}
954+
}
940955

941956
// advance past [
942957
if (peek_char(subj) == '[') {
@@ -963,8 +978,9 @@ static int link_label(subject *subj, cmark_chunk *raw_label) {
963978
}
964979

965980
if (c == ']') { // match found
981+
bufsize_t position = parse_attribute_label ? startpos + 2 : startpos + 1;
966982
*raw_label =
967-
cmark_chunk_dup(&subj->input, startpos + 1, subj->pos - (startpos + 1));
983+
cmark_chunk_dup(&subj->input, position, subj->pos - position);
968984
cmark_chunk_trim(raw_label);
969985
advance(subj); // advance past ]
970986
return 1;
@@ -1089,6 +1105,7 @@ static cmark_node *handle_close_bracket_attribute(cmark_parser *parser, subject
10891105
cmark_chunk raw_label;
10901106
int found_label;
10911107
cmark_node *tmp, *tmpnext;
1108+
cmark_reference *ref = NULL;
10921109
bool isAttributesNode = false;
10931110

10941111
// ^name[content](attributes)
@@ -1112,6 +1129,19 @@ static cmark_node *handle_close_bracket_attribute(cmark_parser *parser, subject
11121129
}
11131130
}
11141131
}
1132+
1133+
// If we can't match direct link, look for [link label] that matches in refmap
1134+
raw_label = cmark_chunk_literal("");
1135+
found_label = link_label(subj, &raw_label, false);
1136+
if (found_label) {
1137+
ref = (cmark_reference *)cmark_map_lookup(subj->refmap, &raw_label);
1138+
cmark_chunk_free(subj->mem, &raw_label);
1139+
1140+
if (ref && ref->is_attributes_reference) {
1141+
isAttributesNode = true;
1142+
attributes = chunk_clone(subj->mem, &ref->attributes);
1143+
}
1144+
}
11151145

11161146
if (!isAttributesNode) {
11171147
// The current node can't be parsed as attribute node, turn it to a TEXT node instead.
@@ -1220,7 +1250,7 @@ static cmark_node *handle_close_bracket(cmark_parser *parser, subject *subj) {
12201250
// Next, look for a following [link label] that matches in refmap.
12211251
// skip spaces
12221252
raw_label = cmark_chunk_literal("");
1223-
found_label = link_label(subj, &raw_label);
1253+
found_label = link_label(subj, &raw_label, false);
12241254
if (!found_label) {
12251255
// If we have a shortcut reference link, back up
12261256
// to before the spacse we skipped.
@@ -1239,7 +1269,7 @@ static cmark_node *handle_close_bracket(cmark_parser *parser, subject *subj) {
12391269
cmark_chunk_free(subj->mem, &raw_label);
12401270
}
12411271

1242-
if (ref != NULL) { // found
1272+
if (ref != NULL && !ref->is_attributes_reference) { // found
12431273
url = chunk_clone(subj->mem, &ref->url);
12441274
title = chunk_clone(subj->mem, &ref->title);
12451275
goto match;
@@ -1568,7 +1598,7 @@ bufsize_t cmark_parse_reference_inline(cmark_mem *mem, cmark_chunk *input,
15681598
subject_from_buf(mem, -1, 0, &subj, input, NULL);
15691599

15701600
// parse label:
1571-
if (!link_label(&subj, &lab) || lab.len == 0)
1601+
if (!link_label(&subj, &lab, false) || lab.len == 0)
15721602
return 0;
15731603

15741604
// colon:
@@ -1616,6 +1646,56 @@ bufsize_t cmark_parse_reference_inline(cmark_mem *mem, cmark_chunk *input,
16161646
return subj.pos;
16171647
}
16181648

1649+
bufsize_t cmark_parse_reference_attributes_inline(cmark_mem *mem, cmark_chunk *input,
1650+
cmark_map *refmap) {
1651+
subject subj;
1652+
1653+
cmark_chunk lab;
1654+
cmark_chunk attributes;
1655+
1656+
bufsize_t matchlen = 0;
1657+
unsigned char c;
1658+
1659+
subject_from_buf(mem, -1, 0, &subj, input, NULL);
1660+
1661+
// parse attribute label:
1662+
if (!link_label(&subj, &lab, true) || lab.len == 0) {
1663+
return 0;
1664+
}
1665+
1666+
// Colon:
1667+
if (peek_char(&subj) == ':') {
1668+
advance(&subj);
1669+
} else {
1670+
return 0;
1671+
}
1672+
1673+
// parse attributes
1674+
spnl(&subj);
1675+
// read until next newline
1676+
bufsize_t startpos = subj.pos;
1677+
while ((c = peek_char(&subj)) && !S_is_line_end_char(c)) {
1678+
advance(&subj);
1679+
matchlen++;
1680+
}
1681+
1682+
if (matchlen == 0) {
1683+
return 0;
1684+
}
1685+
1686+
attributes = cmark_chunk_dup(&subj.input, startpos, matchlen);
1687+
1688+
// parse final spaces and newline:
1689+
skip_spaces(&subj);
1690+
if (!skip_line_end(&subj)) {
1691+
return 0;
1692+
}
1693+
1694+
// insert reference into refmap
1695+
cmark_reference_create_attributes(refmap, &lab, &attributes);
1696+
return subj.pos;
1697+
}
1698+
16191699
unsigned char cmark_inline_parser_peek_char(cmark_inline_parser *parser) {
16201700
return peek_char(parser);
16211701
}

src/references.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ static void reference_free(cmark_map *map, cmark_map_entry *_ref) {
1111
mem->free(ref->entry.label);
1212
cmark_chunk_free(mem, &ref->url);
1313
cmark_chunk_free(mem, &ref->title);
14+
cmark_chunk_free(mem, &ref->attributes);
1415
mem->free(ref);
1516
}
1617
}
@@ -28,15 +29,41 @@ void cmark_reference_create(cmark_map *map, cmark_chunk *label,
2829

2930
ref = (cmark_reference *)map->mem->calloc(1, sizeof(*ref));
3031
ref->entry.label = reflabel;
32+
ref->is_attributes_reference = false;
3133
ref->url = cmark_clean_url(map->mem, url);
3234
ref->title = cmark_clean_title(map->mem, title);
35+
ref->attributes = cmark_chunk_literal("");
3336
ref->entry.age = map->size;
3437
ref->entry.next = map->refs;
3538

3639
map->refs = (cmark_map_entry *)ref;
3740
map->size++;
3841
}
3942

43+
void cmark_reference_create_attributes(cmark_map *map, cmark_chunk *label,
44+
cmark_chunk *attributes) {
45+
cmark_reference *ref;
46+
unsigned char *reflabel = normalize_map_label(map->mem, label);
47+
48+
/* empty reference name, or composed from only whitespace */
49+
if (reflabel == NULL)
50+
return;
51+
52+
assert(map->sorted == NULL);
53+
54+
ref = (cmark_reference *)map->mem->calloc(1, sizeof(*ref));
55+
ref->entry.label = reflabel;
56+
ref->is_attributes_reference = true;
57+
ref->url = cmark_chunk_literal("");
58+
ref->title = cmark_chunk_literal("");
59+
ref->attributes = cmark_clean_attributes(map->mem, attributes);
60+
ref->entry.age = map->size;
61+
ref->entry.next = map->refs;
62+
63+
map->refs = (cmark_map_entry *)ref;
64+
map->size++;
65+
}
66+
4067
cmark_map *cmark_reference_map_new(cmark_mem *mem) {
4168
return cmark_map_new(mem, reference_free);
4269
}

0 commit comments

Comments
 (0)