From dce9a9b973fb8066cfb63fe6d23c272bbeecf9a7 Mon Sep 17 00:00:00 2001 From: QuietMisdreavus Date: Wed, 29 Dec 2021 12:27:49 -0700 Subject: [PATCH] optionally allow Reddit-style spoiler delimiters --- api_test/main.c | 68 +++++++++++++++++++++++++ bin/main.c | 5 ++ extensions/spoiler.c | 109 ++++++++++++++++++++++++++++++---------- src/include/cmark-gfm.h | 5 ++ test/spec.txt | 9 ++++ 5 files changed, 169 insertions(+), 27 deletions(-) diff --git a/api_test/main.c b/api_test/main.c index 9324eb499..bc7d39c6b 100644 --- a/api_test/main.c +++ b/api_test/main.c @@ -1455,6 +1455,74 @@ static void render_spoiler(test_batch_runner *runner) { " \n" "\n", "rendering spoilers without proper delimiters should appear correctly"); } + { + static const char markdown[] = "we have some >!incorrectly spicy text!< here"; + cmark_parser_feed(parser, markdown, sizeof(markdown) - 1); + + cmark_node *doc = cmark_parser_finish(parser); + char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT); + STR_EQ(runner, xml, "\n" + "\n" + "\n" + " \n" + " we have some >!incorrectly spicy text!< here\n" + " \n" + "\n", "rendering spoilers without proper delimiters should appear correctly"); + } +} + +static void render_spoiler_reddit(test_batch_runner *runner) { + cmark_gfm_core_extensions_ensure_registered(); + + cmark_parser *parser = cmark_parser_new(CMARK_OPT_SPOILER_REDDIT_STYLE); + cmark_parser_attach_syntax_extension(parser, cmark_find_syntax_extension("spoiler")); + + { + static const char markdown[] = "we have some >!spicy text!< here"; + cmark_parser_feed(parser, markdown, sizeof(markdown) - 1); + + cmark_node *doc = cmark_parser_finish(parser); + char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT); + STR_EQ(runner, xml, "\n" + "\n" + "\n" + " \n" + " we have some \n" + " \n" + " spicy text\n" + " \n" + " here\n" + " \n" + "\n", "rendering spoilers should appear correctly"); + } + { + static const char markdown[] = "we have some !non-spicy text! here"; + cmark_parser_feed(parser, markdown, sizeof(markdown) - 1); + + cmark_node *doc = cmark_parser_finish(parser); + char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT); + STR_EQ(runner, xml, "\n" + "\n" + "\n" + " \n" + " we have some !non-spicy text! here\n" + " \n" + "\n", "rendering spoilers without proper delimiters should appear correctly"); + } + { + static const char markdown[] = "we have some ||incorrectly spicy text|| here"; + cmark_parser_feed(parser, markdown, sizeof(markdown) - 1); + + cmark_node *doc = cmark_parser_finish(parser); + char *xml = cmark_render_xml(doc, CMARK_OPT_DEFAULT); + STR_EQ(runner, xml, "\n" + "\n" + "\n" + " \n" + " we have some ||incorrectly spicy text|| here\n" + " \n" + "\n", "rendering spoilers without proper delimiters should appear correctly"); + } } int main() { diff --git a/bin/main.c b/bin/main.c index 2bab06284..87aaf619b 100644 --- a/bin/main.c +++ b/bin/main.c @@ -64,6 +64,9 @@ void print_usage() { " instead of align attributes.\n"); printf(" --full-info-string Include remainder of code block info\n" " string in a separate attribute.\n"); + printf(" --spoiler-reddit-style Use Reddit-style spoiler delimiters,\n" + " i.e. >!spoilertext!< instead of\n" + " ||spoilertext||\n"); printf(" --help, -h Print usage information\n"); printf(" --version Print version\n"); } @@ -185,6 +188,8 @@ int main(int argc, char *argv[]) { options |= CMARK_OPT_VALIDATE_UTF8; } else if (strcmp(argv[i], "--liberal-html-tag") == 0) { options |= CMARK_OPT_LIBERAL_HTML_TAG; + } else if (strcmp(argv[i], "--spoiler-reddit-style") == 0) { + options |= CMARK_OPT_SPOILER_REDDIT_STYLE; } else if ((strcmp(argv[i], "--help") == 0) || (strcmp(argv[i], "-h") == 0)) { print_usage(); diff --git a/extensions/spoiler.c b/extensions/spoiler.c index 786b5219e..532ea7df8 100644 --- a/extensions/spoiler.c +++ b/extensions/spoiler.c @@ -11,25 +11,74 @@ static cmark_node *match(cmark_syntax_extension *self, cmark_parser *parser, int left_flanking, right_flanking, punct_before, punct_after, delims; char buffer[101]; - if (character != '|') - return NULL; - - delims = cmark_inline_parser_scan_delimiters( - inline_parser, sizeof(buffer) - 1, '|', - &left_flanking, - &right_flanking, &punct_before, &punct_after); - - memset(buffer, '|', delims); - buffer[delims] = 0; - - res = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem); - cmark_node_set_literal(res, buffer); - res->start_line = res->end_line = cmark_inline_parser_get_line(inline_parser); - res->start_column = cmark_inline_parser_get_column(inline_parser) - delims; - - if ((left_flanking || right_flanking) && (delims == 2)) { - cmark_inline_parser_push_delimiter(inline_parser, character, left_flanking, - right_flanking, res); + if ((parser->options & CMARK_OPT_SPOILER_REDDIT_STYLE)) { + // Reddit-style spoilers - flanked by angle brackets and exclamation marks, + // e.g. >!this is a spoiler!< + int pos = cmark_inline_parser_get_offset(inline_parser); + char *txt = NULL; + bool opener = false; + bool closer = false; + if (cmark_inline_parser_peek_at(inline_parser, pos) == '>' && + cmark_inline_parser_peek_at(inline_parser, pos + 1) == '!') { + txt = ">!"; + opener = true; + } else if (cmark_inline_parser_peek_at(inline_parser, pos) == '!' && + cmark_inline_parser_peek_at(inline_parser, pos + 1) == '<') { + txt = "!<"; + closer = true; + } + + if (opener && pos > 0 && !cmark_isspace(cmark_inline_parser_peek_at(inline_parser, pos - 1))) { + opener = false; + } + + if (closer) { + cmark_chunk *chunk = cmark_inline_parser_get_chunk(inline_parser); + bufsize_t len = chunk->len; + if (pos + 2 < len && !cmark_isspace(cmark_inline_parser_peek_at(inline_parser, pos + 2))) { + closer = false; + } + } + + if ((!opener && !closer) || !txt) + return NULL; + + res = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem); + cmark_node_set_literal(res, txt); + res->start_line = cmark_inline_parser_get_line(inline_parser); + res->start_column = cmark_inline_parser_get_column(inline_parser); + + cmark_inline_parser_set_offset(inline_parser, pos + 2); + + res->end_line = cmark_inline_parser_get_line(inline_parser); + res->end_column = cmark_inline_parser_get_column(inline_parser); + + // Set the character for this delimiter to `!`, since it's a heterogenous + // delimiter and the delimiter API assumes single repeated characters. + cmark_inline_parser_push_delimiter(inline_parser, '!', opener, closer, res); + } else { + // Discord-style spoilers - flanked on both sides by two pipes, + // e.g. ||this is a spoiler|| + if (character != '|') + return NULL; + + delims = cmark_inline_parser_scan_delimiters( + inline_parser, sizeof(buffer) - 1, '|', + &left_flanking, + &right_flanking, &punct_before, &punct_after); + + memset(buffer, '|', delims); + buffer[delims] = 0; + + res = cmark_node_new_with_mem(CMARK_NODE_TEXT, parser->mem); + cmark_node_set_literal(res, buffer); + res->start_line = res->end_line = cmark_inline_parser_get_line(inline_parser); + res->start_column = cmark_inline_parser_get_column(inline_parser) - delims; + + if ((left_flanking || right_flanking) && (delims == 2)) { + cmark_inline_parser_push_delimiter(inline_parser, character, left_flanking, + right_flanking, res); + } } return res; @@ -95,7 +144,16 @@ static int can_contain(cmark_syntax_extension *extension, cmark_node *node, static void commonmark_render(cmark_syntax_extension *extension, cmark_renderer *renderer, cmark_node *node, cmark_event_type ev_type, int options) { - renderer->out(renderer, node, "||", false, LITERAL); + if (options & CMARK_OPT_SPOILER_REDDIT_STYLE) { + bool entering = (ev_type == CMARK_EVENT_ENTER); + if (entering) { + renderer->out(renderer, node, ">!", false, LITERAL); + } else { + renderer->out(renderer, node, "!<", false, LITERAL); + } + } else { + renderer->out(renderer, node, "||", false, LITERAL); + } } static void html_render(cmark_syntax_extension *extension, @@ -109,12 +167,6 @@ static void html_render(cmark_syntax_extension *extension, } } -static void plaintext_render(cmark_syntax_extension *extension, - cmark_renderer *renderer, cmark_node *node, - cmark_event_type ev_type, int options) { - renderer->out(renderer, node, "~", false, LITERAL); -} - cmark_syntax_extension *create_spoiler_extension(void) { cmark_syntax_extension *ext = cmark_syntax_extension_new("spoiler"); cmark_llist *special_chars = NULL; @@ -123,7 +175,7 @@ cmark_syntax_extension *create_spoiler_extension(void) { cmark_syntax_extension_set_can_contain_func(ext, can_contain); cmark_syntax_extension_set_commonmark_render_func(ext, commonmark_render); cmark_syntax_extension_set_html_render_func(ext, html_render); - cmark_syntax_extension_set_plaintext_render_func(ext, plaintext_render); + cmark_syntax_extension_set_plaintext_render_func(ext, commonmark_render); CMARK_NODE_SPOILER = cmark_syntax_extension_add_node(1); cmark_syntax_extension_set_match_inline_func(ext, match); @@ -131,6 +183,9 @@ cmark_syntax_extension *create_spoiler_extension(void) { cmark_mem *mem = cmark_get_default_mem_allocator(); special_chars = cmark_llist_append(mem, special_chars, (void *)'|'); + special_chars = cmark_llist_append(mem, special_chars, (void *)'>'); + special_chars = cmark_llist_append(mem, special_chars, (void *)'<'); + special_chars = cmark_llist_append(mem, special_chars, (void *)'!'); cmark_syntax_extension_set_special_inline_chars(ext, special_chars); cmark_syntax_extension_set_emphasis(ext, 1); diff --git a/src/include/cmark-gfm.h b/src/include/cmark-gfm.h index 14ab95f4a..a4ee03c42 100644 --- a/src/include/cmark-gfm.h +++ b/src/include/cmark-gfm.h @@ -780,6 +780,11 @@ char *cmark_render_latex_with_mem(cmark_node *root, int options, int width, cmar */ #define CMARK_OPT_PRESERVE_WHITESPACE ((1 << 19) | CMARK_OPT_INLINE_ONLY) +/** Parse spoiler text with Reddit-style delimiters (`>!this is a spoiler!<`). Without + * this option, spoilers are parsed with Discord-style delimiters (`||this is a spoiler||`). + */ +#define CMARK_OPT_SPOILER_REDDIT_STYLE (1 << 20) + /** * ## Version information */ diff --git a/test/spec.txt b/test/spec.txt index 71e0215fa..ff93f3c77 100644 --- a/test/spec.txt +++ b/test/spec.txt @@ -7752,6 +7752,15 @@ Spoiler alert: ||Hello, world!||

Spoiler alert: Hello, world!

```````````````````````````````` +The `spoiler` extension also optionally allows alternate "Reddit-style" delimiters, +which use angle brackets and exclamation marks to mark spoiler text: + +```````````````````````````````` example disabled +Spoiler alert: >!Hello, world!!< +. +

Spoiler alert: Hello, world!

+```````````````````````````````` + ## Links