From 6bde90360288bfd848b3f569dae440e101205c10 Mon Sep 17 00:00:00 2001
From: QuietMisdreavus <victoria@quietmisdreavus.net>
Date: Wed, 29 Dec 2021 12:12:33 -0700
Subject: [PATCH 1/6] allow extensions to use `!` and `^` as special characters

---
 src/inlines.c | 49 ++++++++++++++++++++++++++++---------------------
 1 file changed, 28 insertions(+), 21 deletions(-)

diff --git a/src/inlines.c b/src/inlines.c
index 8020a73e2..ec238e4be 100644
--- a/src/inlines.c
+++ b/src/inlines.c
@@ -1516,6 +1516,7 @@ static int parse_inline(cmark_parser *parser, subject *subj, cmark_node *parent,
   cmark_chunk contents;
   unsigned char c;
   bufsize_t startpos, endpos;
+  bufsize_t initpos = subj->pos;
   c = peek_char(subj);
   if (c == 0) {
     return 0;
@@ -1563,43 +1564,49 @@ static int parse_inline(cmark_parser *parser, subject *subj, cmark_node *parent,
     new_inl = handle_close_bracket(parser, subj);
     break;
   case '!':
-    advance(subj);
-    if (peek_char(subj) == '[' && peek_char_n(subj, 1) != '^') {
+    if (peek_char_n(subj, 1) == '[' && peek_char_n(subj, 2) != '^') {
+      advance(subj);
       advance(subj);
       new_inl = make_str(subj, subj->pos - 2, subj->pos - 1, cmark_chunk_literal("!["));
       push_bracket(subj, IMAGE, new_inl);
-    } else {
-      new_inl = make_str(subj, subj->pos - 1, subj->pos - 1, cmark_chunk_literal("!"));
     }
     break;
   case '^':
-    advance(subj);
     // TODO: Support a name between ^ and [
-    if (peek_char(subj) == '[') {
+    if (peek_char_n(subj, 1) == '[') {
+      advance(subj);
       advance(subj);
       new_inl = make_str(subj, subj->pos - 2, subj->pos - 1, cmark_chunk_literal("^["));
       push_bracket(subj, ATTRIBUTE, new_inl);
-    } else {
-      new_inl = make_str(subj, subj->pos - 1, subj->pos - 1, cmark_chunk_literal("^"));
     }
     break;
-  default:
-    new_inl = try_extensions(parser, parent, c, subj);
-    if (new_inl != NULL)
-      break;
+  }
 
-    endpos = subject_find_special_char(parser, subj, options);
-    contents = cmark_chunk_dup(&subj->input, subj->pos, endpos - subj->pos);
-    startpos = subj->pos;
-    subj->pos = endpos;
+  if (subj->pos == initpos) {
+    if (!new_inl)
+      new_inl = try_extensions(parser, parent, c, subj);
 
-    // if we're at a newline, strip trailing spaces.
-    if ((options & CMARK_OPT_PRESERVE_WHITESPACE) == 0 && S_is_line_end_char(peek_char(subj))) {
-      cmark_chunk_rtrim(&contents);
-    }
+    if (!new_inl) {
+      if (c == '^' || c == '!') {
+        advance(subj);
+        new_inl = make_str(subj, subj->pos - 1, subj->pos - 1, cmark_chunk_dup(&subj->input, subj->pos - 1, 1));
+      } else {
+        endpos = subject_find_special_char(parser, subj, options);
+        contents = cmark_chunk_dup(&subj->input, subj->pos, endpos - subj->pos);
+        startpos = subj->pos;
+        subj->pos = endpos;
+
+        // if we're at a newline, strip trailing spaces.
+        if ((options & CMARK_OPT_PRESERVE_WHITESPACE) == 0 && S_is_line_end_char(peek_char(subj))) {
+          cmark_chunk_rtrim(&contents);
+        }
 
-    new_inl = make_str(subj, startpos, endpos - 1, contents);
+        if (endpos > startpos)
+          new_inl = make_str(subj, startpos, endpos - 1, contents);
+      }
+    }
   }
+
   if (new_inl != NULL) {
     cmark_node_append_child(parent, new_inl);
   }

From 1e705e9e8718a080a5e358157d51d4a34d5b8751 Mon Sep 17 00:00:00 2001
From: QuietMisdreavus <victoria@quietmisdreavus.net>
Date: Thu, 30 Dec 2021 08:36:28 -0700
Subject: [PATCH 2/6] update Makefile with new include dir

---
 Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index a05ba97a6..b84e9e741 100644
--- a/Makefile
+++ b/Makefile
@@ -101,7 +101,7 @@ mingw:
 	cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain-mingw32.cmake -DCMAKE_INSTALL_PREFIX=$(MINGW_INSTALLDIR) ;\
 	$(MAKE) && $(MAKE) install
 
-man/man3/cmark-gfm.3: src/cmark-gfm.h | $(CMARK)
+man/man3/cmark-gfm.3: src/include/cmark-gfm.h | $(CMARK)
 	python man/make_man_page.py $< > $@ \
 
 archive:

From f678bae879337d6c82e0f2e676da9069b8412d3a Mon Sep 17 00:00:00 2001
From: QuietMisdreavus <victoria@quietmisdreavus.net>
Date: Sat, 25 Dec 2021 13:11:00 -0700
Subject: [PATCH 3/6] add new spoiler extension

---
 extensions/CMakeLists.txt    |   1 +
 extensions/core-extensions.c |   2 +
 extensions/spoiler.c         | 139 +++++++++++++++++++++++++++++++++++
 extensions/spoiler.h         |   9 +++
 4 files changed, 151 insertions(+)
 create mode 100644 extensions/spoiler.c
 create mode 100644 extensions/spoiler.h

diff --git a/extensions/CMakeLists.txt b/extensions/CMakeLists.txt
index 4977c98c3..62984af5c 100644
--- a/extensions/CMakeLists.txt
+++ b/extensions/CMakeLists.txt
@@ -11,6 +11,7 @@ set(LIBRARY_SOURCES
     ext_scanners.re
     ext_scanners.h
     tasklist.c
+    spoiler.c
    )
 
 include_directories(
diff --git a/extensions/core-extensions.c b/extensions/core-extensions.c
index 131cdf402..c9bff9471 100644
--- a/extensions/core-extensions.c
+++ b/extensions/core-extensions.c
@@ -2,6 +2,7 @@
 #include "autolink.h"
 #include "mutex.h"
 #include "strikethrough.h"
+#include "spoiler.h"
 #include "table.h"
 #include "tagfilter.h"
 #include "tasklist.h"
@@ -15,6 +16,7 @@ static int core_extensions_registration(cmark_plugin *plugin) {
   cmark_plugin_register_syntax_extension(plugin, create_autolink_extension());
   cmark_plugin_register_syntax_extension(plugin, create_tagfilter_extension());
   cmark_plugin_register_syntax_extension(plugin, create_tasklist_extension());
+  cmark_plugin_register_syntax_extension(plugin, create_spoiler_extension());
   return 1;
 }
 
diff --git a/extensions/spoiler.c b/extensions/spoiler.c
new file mode 100644
index 000000000..786b5219e
--- /dev/null
+++ b/extensions/spoiler.c
@@ -0,0 +1,139 @@
+#include "spoiler.h"
+#include <parser.h>
+#include <render.h>
+
+cmark_node_type CMARK_NODE_SPOILER;
+
+static cmark_node *match(cmark_syntax_extension *self, cmark_parser *parser,
+                         cmark_node *parent, unsigned char character,
+                         cmark_inline_parser *inline_parser) {
+  cmark_node *res = NULL;
+  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);
+  }
+
+  return res;
+}
+
+static delimiter *insert(cmark_syntax_extension *self, cmark_parser *parser,
+                         cmark_inline_parser *inline_parser, delimiter *opener,
+                         delimiter *closer) {
+  cmark_node *spoiler;
+  cmark_node *tmp, *next;
+  delimiter *delim, *tmp_delim;
+  delimiter *res = closer->next;
+
+  spoiler = opener->inl_text;
+
+  if (opener->inl_text->as.literal.len != closer->inl_text->as.literal.len)
+    goto done;
+
+  if (!cmark_node_set_type(spoiler, CMARK_NODE_SPOILER))
+    goto done;
+
+  cmark_node_set_syntax_extension(spoiler, self);
+
+  tmp = cmark_node_next(opener->inl_text);
+
+  while (tmp) {
+    if (tmp == closer->inl_text)
+      break;
+    next = cmark_node_next(tmp);
+    cmark_node_append_child(spoiler, tmp);
+    tmp = next;
+  }
+
+  spoiler->end_column = closer->inl_text->start_column + closer->inl_text->as.literal.len - 1;
+  cmark_node_free(closer->inl_text);
+
+  delim = closer;
+  while (delim != NULL && delim != opener) {
+    tmp_delim = delim->previous;
+    cmark_inline_parser_remove_delimiter(inline_parser, delim);
+    delim = tmp_delim;
+  }
+
+  cmark_inline_parser_remove_delimiter(inline_parser, opener);
+
+done:
+  return res;
+}
+
+static const char *get_type_string(cmark_syntax_extension *extension,
+                                   cmark_node *node) {
+  return node->type == CMARK_NODE_SPOILER ? "spoiler" : "<unknown>";
+}
+
+static int can_contain(cmark_syntax_extension *extension, cmark_node *node,
+                       cmark_node_type child_type) {
+  if (node->type != CMARK_NODE_SPOILER)
+    return false;
+
+  return CMARK_NODE_TYPE_INLINE_P(child_type);
+}
+
+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);
+}
+
+static void html_render(cmark_syntax_extension *extension,
+                        cmark_html_renderer *renderer, cmark_node *node,
+                        cmark_event_type ev_type, int options) {
+  bool entering = (ev_type == CMARK_EVENT_ENTER);
+  if (entering) {
+    cmark_strbuf_puts(renderer->html, "<span class=\"spoiler\">");
+  } else {
+    cmark_strbuf_puts(renderer->html, "</span>");
+  }
+}
+
+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;
+
+  cmark_syntax_extension_set_get_type_string_func(ext, get_type_string);
+  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_NODE_SPOILER = cmark_syntax_extension_add_node(1);
+
+  cmark_syntax_extension_set_match_inline_func(ext, match);
+  cmark_syntax_extension_set_inline_from_delim_func(ext, insert);
+
+  cmark_mem *mem = cmark_get_default_mem_allocator();
+  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);
+
+  return ext;
+}
diff --git a/extensions/spoiler.h b/extensions/spoiler.h
new file mode 100644
index 000000000..a86bbbf39
--- /dev/null
+++ b/extensions/spoiler.h
@@ -0,0 +1,9 @@
+#ifndef CMARK_GFM_SPOILER_H
+#define CMARK_GFM_SPOILER_H
+
+#include "cmark-gfm-core-extensions.h"
+
+extern cmark_node_type CMARK_NODE_SPOILER;
+cmark_syntax_extension *create_spoiler_extension(void);
+
+#endif /* CMARK_GFM_SPOILER_H */

From e70f2255cc9305c1fa3da3ba2f073abb9c424b0a Mon Sep 17 00:00:00 2001
From: QuietMisdreavus <victoria@quietmisdreavus.net>
Date: Sat, 25 Dec 2021 18:10:45 -0700
Subject: [PATCH 4/6] add tests for spoiler extension

---
 api_test/main.c | 41 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/api_test/main.c b/api_test/main.c
index 1051c2904..9324eb499 100644
--- a/api_test/main.c
+++ b/api_test/main.c
@@ -1417,6 +1417,46 @@ static void parser_interrupt(test_batch_runner *runner) {
   cmark_syntax_extension_free(cmark_get_default_mem_allocator(), my_ext);
 }
 
+static void render_spoiler(test_batch_runner *runner) {
+  cmark_gfm_core_extensions_ensure_registered();
+
+  cmark_parser *parser = cmark_parser_new(CMARK_OPT_DEFAULT);
+  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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+           "<!DOCTYPE document SYSTEM \"CommonMark.dtd\">\n"
+           "<document xmlns=\"http://commonmark.org/xml/1.0\">\n"
+           "  <paragraph>\n"
+           "    <text xml:space=\"preserve\">we have some </text>\n"
+           "    <spoiler>\n"
+           "      <text xml:space=\"preserve\">spicy text</text>\n"
+           "    </spoiler>\n"
+           "    <text xml:space=\"preserve\"> here</text>\n"
+           "  </paragraph>\n"
+           "</document>\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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+           "<!DOCTYPE document SYSTEM \"CommonMark.dtd\">\n"
+           "<document xmlns=\"http://commonmark.org/xml/1.0\">\n"
+           "  <paragraph>\n"
+           "    <text xml:space=\"preserve\">we have some |non-spicy text| here</text>\n"
+           "  </paragraph>\n"
+           "</document>\n", "rendering spoilers without proper delimiters should appear correctly");
+  }
+}
+
 int main() {
   int retval;
   test_batch_runner *runner = test_batch_runner_new();
@@ -1452,6 +1492,7 @@ int main() {
   verify_custom_attributes_node(runner);
   verify_custom_attributes_node_with_footnote(runner);
   parser_interrupt(runner);
+  render_spoiler(runner);
 
   test_print_summary(runner);
   retval = test_ok(runner) ? 0 : 1;

From f69ae9f0d1b94c44bbb373fadf26eed75fd3c85f Mon Sep 17 00:00:00 2001
From: QuietMisdreavus <victoria@quietmisdreavus.net>
Date: Tue, 28 Dec 2021 10:47:00 -0700
Subject: [PATCH 5/6] add spoiler extension to spec

---
 test/spec.txt | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/test/spec.txt b/test/spec.txt
index 582131d70..71e0215fa 100644
--- a/test/spec.txt
+++ b/test/spec.txt
@@ -7734,6 +7734,26 @@ new paragraph~~.
 
 </div>
 
+<div class="extension">
+
+## Spoiler text (extension)
+
+Swift-cmark GFM provides the `spoiler` extension, providing an additional emphasis
+type for text that should be hidden and revealed by user interaction.
+
+Spoiler text is wrapped in two pipes (`|`).
+
+As the use of spoiler text depends on user interaction, GFM only emits a generic
+`<span>` tag that must be styled by the client.
+
+```````````````````````````````` example spoiler
+Spoiler alert: ||Hello, world!||
+.
+<p>Spoiler alert: <span class="spoiler">Hello, world!</span></p>
+````````````````````````````````
+
+</div>
+
 ## Links
 
 A link contains [link text] (the visible text), a [link destination]

From 71a79e48f10865711710df25fd03f6d743dd8519 Mon Sep 17 00:00:00 2001
From: QuietMisdreavus <victoria@quietmisdreavus.net>
Date: Wed, 29 Dec 2021 12:27:49 -0700
Subject: [PATCH 6/6] 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) {
            "  </paragraph>\n"
            "</document>\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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+           "<!DOCTYPE document SYSTEM \"CommonMark.dtd\">\n"
+           "<document xmlns=\"http://commonmark.org/xml/1.0\">\n"
+           "  <paragraph>\n"
+           "    <text xml:space=\"preserve\">we have some &gt;!incorrectly spicy text!&lt; here</text>\n"
+           "  </paragraph>\n"
+           "</document>\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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+           "<!DOCTYPE document SYSTEM \"CommonMark.dtd\">\n"
+           "<document xmlns=\"http://commonmark.org/xml/1.0\">\n"
+           "  <paragraph>\n"
+           "    <text xml:space=\"preserve\">we have some </text>\n"
+           "    <spoiler>\n"
+           "      <text xml:space=\"preserve\">spicy text</text>\n"
+           "    </spoiler>\n"
+           "    <text xml:space=\"preserve\"> here</text>\n"
+           "  </paragraph>\n"
+           "</document>\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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+           "<!DOCTYPE document SYSTEM \"CommonMark.dtd\">\n"
+           "<document xmlns=\"http://commonmark.org/xml/1.0\">\n"
+           "  <paragraph>\n"
+           "    <text xml:space=\"preserve\">we have some !non-spicy text! here</text>\n"
+           "  </paragraph>\n"
+           "</document>\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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+           "<!DOCTYPE document SYSTEM \"CommonMark.dtd\">\n"
+           "<document xmlns=\"http://commonmark.org/xml/1.0\">\n"
+           "  <paragraph>\n"
+           "    <text xml:space=\"preserve\">we have some ||incorrectly spicy text|| here</text>\n"
+           "  </paragraph>\n"
+           "</document>\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!||
 <p>Spoiler alert: <span class="spoiler">Hello, world!</span></p>
 ````````````````````````````````
 
+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!!<
+.
+<p>Spoiler alert: <span class="spoiler">Hello, world!</span></p>
+````````````````````````````````
+
 </div>
 
 ## Links