mirror of https://github.com/zulip/zulip.git
markdown: Add support for spoilers.
This adds support for a "spoiler" syntax in Zulip's markdown, which can be used to hide content that one doesn't want to be immediately visible without a click. We use our own spoiler block syntax inspired by Zulip's existing quote and math block markdown extensions, rather than requiring a token on every line, as is present in some other markdown spoiler implementations. Fixes #5802. Co-authored-by: Dylan Nugent <dylnuge@gmail.com>
This commit is contained in:
parent
54604257e0
commit
1cb040647b
|
@ -303,6 +303,7 @@
|
|||
"settings_ui": false,
|
||||
"settings_user_groups": false,
|
||||
"settings_users": false,
|
||||
"spoilers": false,
|
||||
"starred_messages": false,
|
||||
"stream_color": false,
|
||||
"stream_create": false,
|
||||
|
|
|
@ -70,6 +70,7 @@ const get_content_element = () => {
|
|||
$content.set_find_results('a.stream-topic', $array([]));
|
||||
$content.set_find_results('span.timestamp', $array([]));
|
||||
$content.set_find_results('.emoji', $array([]));
|
||||
$content.set_find_results('div.spoiler-header', $array([]));
|
||||
return $content;
|
||||
};
|
||||
|
||||
|
@ -197,4 +198,34 @@ run_test('emoji', () => {
|
|||
rm.update_elements($content);
|
||||
|
||||
assert(called);
|
||||
|
||||
// Set page paramaters back so that test run order is independent
|
||||
page_params.emojiset = 'apple';
|
||||
});
|
||||
|
||||
run_test('spoiler-header', () => {
|
||||
// Setup
|
||||
const $content = get_content_element();
|
||||
const $header = $.create('div.spoiler-header');
|
||||
$content.set_find_results('div.spoiler-header', $array([$header]));
|
||||
|
||||
// Test that button gets appened to a spoiler header
|
||||
const label = 'My Spoiler Header';
|
||||
const toggle_button_html = '<a class="spoiler-button" aria-expanded="false"><span class="spoiler-arrow"></span></a>';
|
||||
$header.html(label);
|
||||
rm.update_elements($content);
|
||||
assert.equal(toggle_button_html + label, $header.html());
|
||||
});
|
||||
|
||||
run_test('spoiler-header-empty-fill', () => {
|
||||
// Setup
|
||||
const $content = get_content_element();
|
||||
const $header = $.create('div.spoiler-header');
|
||||
$content.set_find_results('div.spoiler-header', $array([$header]));
|
||||
|
||||
// Test that an empty header gets the default text applied (through i18n filter)
|
||||
const toggle_button_html = '<a class="spoiler-button" aria-expanded="false"><span class="spoiler-arrow"></span></a>';
|
||||
$header.html('');
|
||||
rm.update_elements($content);
|
||||
assert.equal(toggle_button_html + '<p>translated: Spoiler</p>', $header.html());
|
||||
});
|
||||
|
|
|
@ -84,6 +84,7 @@ zrequire('color_data');
|
|||
zrequire('stream_data');
|
||||
zrequire('muting');
|
||||
zrequire('condense');
|
||||
zrequire('spoilers');
|
||||
zrequire('lightbox');
|
||||
zrequire('overlays');
|
||||
zrequire('invite');
|
||||
|
|
|
@ -144,6 +144,10 @@ exports.make_new_elem = function (selector, opts) {
|
|||
classes.set(class_name, true);
|
||||
return self;
|
||||
},
|
||||
append: function (arg) {
|
||||
html = html + arg;
|
||||
return self;
|
||||
},
|
||||
attr: function (name, val) {
|
||||
if (val === undefined) {
|
||||
return attrs.get(name);
|
||||
|
@ -284,6 +288,10 @@ exports.make_new_elem = function (selector, opts) {
|
|||
parents_selector + ' in ' + selector);
|
||||
return result;
|
||||
},
|
||||
prepend: function (arg) {
|
||||
html = arg + html;
|
||||
return self;
|
||||
},
|
||||
prop: function (name, val) {
|
||||
if (val === undefined) {
|
||||
return properties.get(name);
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -199,6 +199,7 @@ import "../settings_ui.js";
|
|||
import "../search_pill.js";
|
||||
import "../search_pill_widget.js";
|
||||
import "../stream_ui_updates.js";
|
||||
import "../spoilers.js";
|
||||
|
||||
// Import Styles
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ exports.initialize = function () {
|
|||
function is_clickable_message_element(target) {
|
||||
return target.is("a") || target.is("img.message_inline_image") || target.is("img.twitter-avatar") ||
|
||||
target.is("div.message_length_controller") || target.is("textarea") || target.is("input") ||
|
||||
target.is("i.edit_content_button") ||
|
||||
target.is("i.edit_content_button") || target.is(".spoiler-arrow") ||
|
||||
target.is(".highlight") && target.parent().is("a");
|
||||
}
|
||||
|
||||
|
|
|
@ -5,14 +5,20 @@
|
|||
// auto-completing code blocks missing a trailing close.
|
||||
|
||||
// See backend fenced_code.py:71 for associated regexp
|
||||
const fencestr = "^(~{3,}|`{3,})" + // Opening Fence
|
||||
const fencestr = "^(~{3,}|`{3,})" + // Opening Fence
|
||||
"[ ]*" + // Spaces
|
||||
"(" +
|
||||
"\\{?\\.?" +
|
||||
"([a-zA-Z0-9_+-./#]*)" + // Language
|
||||
"\\}?" +
|
||||
")" +
|
||||
"[ ]*" + // Spaces
|
||||
")$";
|
||||
"(" +
|
||||
"\\{?\\.?" +
|
||||
"([^~`]*)" + // Header (see fenced_code.py)
|
||||
"\\}?" +
|
||||
")" +
|
||||
"$";
|
||||
const fence_re = new RegExp(fencestr);
|
||||
|
||||
// Default stashing function does nothing
|
||||
|
@ -52,6 +58,20 @@ function wrap_tex(tex) {
|
|||
}
|
||||
}
|
||||
|
||||
function wrap_spoiler(header, text, stash_func) {
|
||||
const output = [];
|
||||
const header_div_open_html = '<div class="spoiler-block"><div class="spoiler-header">';
|
||||
const end_header_start_content_html = '</div><div class="spoiler-content" aria-hidden="true">';
|
||||
const footer_html = '</div></div>';
|
||||
|
||||
output.push(stash_func(header_div_open_html));
|
||||
output.push(header);
|
||||
output.push(stash_func(end_header_start_content_html));
|
||||
output.push(text);
|
||||
output.push(stash_func(footer_html));
|
||||
return output.join("\n\n");
|
||||
}
|
||||
|
||||
exports.set_stash_func = function (stash_handler) {
|
||||
stash_func = stash_handler;
|
||||
};
|
||||
|
@ -62,7 +82,7 @@ exports.process_fenced_code = function (content) {
|
|||
const handler_stack = [];
|
||||
let consume_line;
|
||||
|
||||
function handler_for_fence(output_lines, fence, lang) {
|
||||
function handler_for_fence(output_lines, fence, lang, header) {
|
||||
// lang is ignored except for 'quote', as we
|
||||
// don't do syntax highlighting yet
|
||||
return (function () {
|
||||
|
@ -108,6 +128,26 @@ exports.process_fenced_code = function (content) {
|
|||
};
|
||||
}
|
||||
|
||||
if (lang === 'spoiler') {
|
||||
return {
|
||||
handle_line: function (line) {
|
||||
if (line === fence) {
|
||||
this.done();
|
||||
} else {
|
||||
lines.push(line);
|
||||
}
|
||||
},
|
||||
|
||||
done: function () {
|
||||
const text = wrap_spoiler(header, lines.join('\n'), stash_func);
|
||||
output_lines.push('');
|
||||
output_lines.push(text);
|
||||
output_lines.push('');
|
||||
handler_stack.pop();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
handle_line: function (line) {
|
||||
if (line === fence) {
|
||||
|
@ -146,7 +186,8 @@ exports.process_fenced_code = function (content) {
|
|||
if (match) {
|
||||
const fence = match[1];
|
||||
const lang = match[3];
|
||||
const handler = handler_for_fence(output_lines, fence, lang);
|
||||
const header = match[5];
|
||||
const handler = handler_for_fence(output_lines, fence, lang, header);
|
||||
handler_stack.push(handler);
|
||||
} else {
|
||||
output_lines.push(line);
|
||||
|
|
|
@ -153,6 +153,19 @@ exports.update_elements = (content) => {
|
|||
}
|
||||
});
|
||||
|
||||
content.find('div.spoiler-header').each(function () {
|
||||
// If a spoiler block has no header content, it should have a default header
|
||||
// We do this client side to allow for i18n by the client
|
||||
if ($.trim($(this).html()).length === 0) {
|
||||
$(this).append(`<p>${i18n.t('Spoiler')}</p>`);
|
||||
}
|
||||
|
||||
// Add the expand/collapse button to spoiler blocks
|
||||
const toggle_button_html = '<a class="spoiler-button" aria-expanded="false"><span class="spoiler-arrow"></span></a>';
|
||||
$(this).prepend(toggle_button_html);
|
||||
});
|
||||
|
||||
|
||||
// Display emoji (including realm emoji) as text if
|
||||
// page_params.emojiset is 'text'.
|
||||
if (page_params.emojiset === 'text') {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
function collapse_spoiler(spoiler) {
|
||||
const spoiler_height = spoiler.prop('scrollHeight');
|
||||
|
||||
// Set height to rendered height on next frame, then to zero on following
|
||||
// frame to allow CSS transition animation to work
|
||||
requestAnimationFrame(function () {
|
||||
spoiler.height(spoiler_height + 'px');
|
||||
spoiler.removeClass("spoiler-content-open");
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
spoiler.height("0px");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function expand_spoiler(spoiler) {
|
||||
// Normally, the height of the spoiler block is not defined absolutely on
|
||||
// the `spoiler-content-open` class, but just set to `auto` (i.e. the height
|
||||
// of the content). CSS animations do not work with properties set to
|
||||
// `auto`, so we get the actual height of the content here and temporarily
|
||||
// put it explicitly on the element styling to allow the transition to work.
|
||||
const spoiler_height = spoiler.prop('scrollHeight');
|
||||
spoiler.height(spoiler_height + "px");
|
||||
// The `spoiler-content-open` class has CSS animations defined on it which
|
||||
// will trigger on the frame after this class change.
|
||||
spoiler.addClass("spoiler-content-open");
|
||||
|
||||
spoiler.on('transitionend', function () {
|
||||
spoiler.off('transitionend');
|
||||
// When the CSS transition is over, reset the height to auto
|
||||
// This keeps things working if, e.g., the viewport is resized
|
||||
spoiler.height("");
|
||||
});
|
||||
}
|
||||
|
||||
exports.initialize = function () {
|
||||
$("body").on("click", ".spoiler-button", function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const arrow = $(this).children('.spoiler-arrow');
|
||||
const spoiler_content = $(this).parent().siblings(".spoiler-content");
|
||||
|
||||
if (spoiler_content.hasClass("spoiler-content-open")) {
|
||||
// Content was open, we are collapsing
|
||||
arrow.removeClass("spoiler-button-open");
|
||||
|
||||
// Modify ARIA roles for screen readers
|
||||
$(this).attr("aria-expanded", "false");
|
||||
spoiler_content.attr("aria-hidden", "true");
|
||||
|
||||
collapse_spoiler(spoiler_content);
|
||||
} else {
|
||||
// Content was closed, we are expanding
|
||||
arrow.addClass("spoiler-button-open");
|
||||
|
||||
// Modify ARIA roles for screen readers
|
||||
$(this).attr("aria-expanded", "true");
|
||||
spoiler_content.attr("aria-hidden", "false");
|
||||
|
||||
expand_spoiler(spoiler_content);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.spoilers = exports;
|
|
@ -438,6 +438,7 @@ exports.initialize_everything = function () {
|
|||
subs.initialize();
|
||||
stream_list.initialize();
|
||||
condense.initialize();
|
||||
spoilers.initialize();
|
||||
lightbox.initialize();
|
||||
click_handlers.initialize();
|
||||
copy_and_paste.initialize();
|
||||
|
|
|
@ -163,6 +163,89 @@
|
|||
color: hsl(0, 0%, 50%);
|
||||
}
|
||||
|
||||
/* Spoiler styling */
|
||||
.spoiler-block {
|
||||
border: hsl(0, 0%, 50%) 1px solid;
|
||||
padding: 2px 8px 2px 10px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: block;
|
||||
margin: 5px 0 15px 0;
|
||||
|
||||
.spoiler-header {
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.spoiler-content {
|
||||
overflow: hidden;
|
||||
border-top: hsl(0, 0%, 50%) 0px solid;
|
||||
transition: height 0.4s ease-in-out, border-top 0.4s step-end, padding 0.4s step-end;
|
||||
padding: 0px;
|
||||
height: 0px;
|
||||
|
||||
&.spoiler-content-open {
|
||||
border-top: hsl(0, 0%, 50%) 1px solid;
|
||||
transition: height 0.4s ease-in-out, border-top 0.4s step-start, padding 0.4s step-start;
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.spoiler-button {
|
||||
float: right;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
&:hover .spoiler-arrow {
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: hsl(0, 0%, 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.spoiler-arrow {
|
||||
float: right;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
position: relative;
|
||||
bottom: -5px;
|
||||
left: -10px;
|
||||
cursor: pointer;
|
||||
transition: 0.4s ease;
|
||||
margin-top: 2px;
|
||||
text-align: left;
|
||||
transform: rotate(45deg);
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 3px;
|
||||
background-color: hsl(0, 0%, 83%);
|
||||
transition: 0.4s ease;
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
transform: rotate(90deg);
|
||||
top: -5px;
|
||||
left: 5px;
|
||||
}
|
||||
&.spoiler-button-open {
|
||||
transform: rotate(45deg) translate(-5px, -5px);
|
||||
&::before {
|
||||
transform: translate(10px, 0);
|
||||
}
|
||||
&::after {
|
||||
transform: rotate(90deg) translate(10px, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS for message content widgets */
|
||||
table.tictactoe {
|
||||
width: 80px;
|
||||
|
|
|
@ -128,6 +128,22 @@ Quoted block
|
|||
```</td>
|
||||
<td class="rendered_markdown"><blockquote><p>Quoted block</p></blockquote></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="preserve_spaces">```spoiler Always visible heading
|
||||
This text won't be visible until the user clicks.
|
||||
```</td>
|
||||
<td class="rendered_markdown">
|
||||
<div class="spoiler-block">
|
||||
<div class="spoiler-header"><a class="spoiler-button"><span class="spoiler-arrow"></span></a>
|
||||
<p>Always visible heading</p>
|
||||
</div>
|
||||
|
||||
<div class="spoiler-content">
|
||||
<p>This text won't be visible until the user clicks.</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Some inline math $$ e^{i \pi } + 1 = 0 $$</td>
|
||||
<td class="rendered_markdown">
|
||||
|
|
|
@ -13,6 +13,7 @@ to allow you to easily format your messages.
|
|||
* [Code blocks](#code)
|
||||
* [LaTeX](#latex)
|
||||
* [Quotes](#quotes)
|
||||
* [Spoilers](#spoilers)
|
||||
* [Emoji and emoticons](#emoji-and-emoticons)
|
||||
* [Mentions](#mentions)
|
||||
* [Status messages](#status-messages)
|
||||
|
@ -150,6 +151,28 @@ quote in two paragraphs
|
|||
|
||||
![](/static/images/help/markdown-quotes.png)
|
||||
|
||||
## Spoilers
|
||||
|
||||
You can use spoilers to hide content that you do not want to be visible until
|
||||
the user interacts with it.
|
||||
|
||||
|
||||
~~~
|
||||
Normal content in message
|
||||
|
||||
```spoiler Spoiler Header
|
||||
Spoiler content. These lines won't be visible until the user expands the spoiler.
|
||||
```
|
||||
~~~
|
||||
|
||||
The spoiler will initially display in a collapsed form:
|
||||
|
||||
![](/static/images/help/spoiler-collapsed.png)
|
||||
|
||||
Clicking the arrow will expand the spoiler content:
|
||||
|
||||
![](/static/images/help/spoiler-expanded.png)
|
||||
|
||||
## Emoji and emoticons
|
||||
|
||||
To translate emoticons into emoji, you'll need to
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"scala": 21,
|
||||
"scheme": 14,
|
||||
"sql": 32,
|
||||
"spoiler": 50,
|
||||
"swift": 41,
|
||||
"tex": 40,
|
||||
"text": 1,
|
||||
|
|
|
@ -128,6 +128,7 @@ EXEMPT_FILES = {
|
|||
'static/js/settings_ui.js',
|
||||
'static/js/settings_users.js',
|
||||
'static/js/setup.js',
|
||||
'static/js/spoilers.js',
|
||||
'static/js/starred_messages.js',
|
||||
'static/js/stream_color.js',
|
||||
'static/js/stream_create.js',
|
||||
|
|
|
@ -102,6 +102,13 @@ FENCE_RE = re.compile("""
|
|||
\\}?
|
||||
) # language, like ".py" or "{javascript}"
|
||||
[ ]* # spaces
|
||||
(
|
||||
\\{?\\.?
|
||||
(?P<header>
|
||||
[^~`]*
|
||||
)
|
||||
\\}?
|
||||
) # header for features that use fenced block header syntax (like spoilers)
|
||||
$
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
@ -155,13 +162,16 @@ class BaseHandler:
|
|||
raise NotImplementedError()
|
||||
|
||||
def generic_handler(processor: Any, output: MutableSequence[str],
|
||||
fence: str, lang: str,
|
||||
fence: str, lang: str, header: str,
|
||||
run_content_validators: bool=False,
|
||||
default_language: Optional[str]=None) -> BaseHandler:
|
||||
lang = lang.lower()
|
||||
if lang in ('quote', 'quoted'):
|
||||
return QuoteHandler(processor, output, fence, default_language)
|
||||
elif lang == 'math':
|
||||
return TexHandler(processor, output, fence)
|
||||
elif lang == 'spoiler':
|
||||
return SpoilerHandler(processor, output, fence, header)
|
||||
else:
|
||||
return CodeHandler(processor, output, fence, lang, run_content_validators)
|
||||
|
||||
|
@ -172,9 +182,11 @@ def check_for_new_fence(processor: Any, output: MutableSequence[str], line: str,
|
|||
if m:
|
||||
fence = m.group('fence')
|
||||
lang = m.group('lang')
|
||||
header = m.group('header')
|
||||
if not lang and default_language:
|
||||
lang = default_language
|
||||
handler = generic_handler(processor, output, fence, lang, run_content_validators, default_language)
|
||||
handler = generic_handler(processor, output, fence, lang, header,
|
||||
run_content_validators, default_language)
|
||||
processor.push(handler)
|
||||
else:
|
||||
output.append(line)
|
||||
|
@ -251,6 +263,37 @@ class QuoteHandler(BaseHandler):
|
|||
self.output.append('')
|
||||
self.processor.pop()
|
||||
|
||||
|
||||
class SpoilerHandler(BaseHandler):
|
||||
def __init__(self, processor: Any, output: MutableSequence[str],
|
||||
fence: str, spoiler_header: str) -> None:
|
||||
self.processor = processor
|
||||
self.output = output
|
||||
self.fence = fence
|
||||
self.spoiler_header = spoiler_header
|
||||
self.lines: List[str] = []
|
||||
|
||||
def handle_line(self, line: str) -> None:
|
||||
if line.rstrip() == self.fence:
|
||||
self.done()
|
||||
else:
|
||||
check_for_new_fence(self.processor, self.lines, line)
|
||||
|
||||
def done(self) -> None:
|
||||
if len(self.lines) == 0:
|
||||
# No content, do nothing
|
||||
return
|
||||
else:
|
||||
header = self.spoiler_header
|
||||
text = '\n'.join(self.lines)
|
||||
|
||||
text = self.processor.format_spoiler(header, text)
|
||||
processed_lines = text.split('\n')
|
||||
self.output.append('')
|
||||
self.output.extend(processed_lines)
|
||||
self.output.append('')
|
||||
self.processor.pop()
|
||||
|
||||
class TexHandler(BaseHandler):
|
||||
def __init__(self, processor: Any, output: MutableSequence[str], fence: str) -> None:
|
||||
self.processor = processor
|
||||
|
@ -359,6 +402,20 @@ class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor):
|
|||
quoted_paragraphs.append("\n".join("> " + line for line in lines if line != ''))
|
||||
return "\n\n".join(quoted_paragraphs)
|
||||
|
||||
def format_spoiler(self, header: str, text: str) -> str:
|
||||
output = []
|
||||
header_div_open_html = '<div class="spoiler-block"><div class="spoiler-header">'
|
||||
end_header_start_content_html = '</div><div class="spoiler-content"' \
|
||||
' aria-hidden="true">'
|
||||
footer_html = '</div></div>'
|
||||
|
||||
output.append(self.placeholder(header_div_open_html))
|
||||
output.append(header)
|
||||
output.append(self.placeholder(end_header_start_content_html))
|
||||
output.append(text)
|
||||
output.append(self.placeholder(footer_html))
|
||||
return "\n\n".join(output)
|
||||
|
||||
def format_tex(self, text: str) -> str:
|
||||
paragraphs = text.split("\n\n")
|
||||
tex_paragraphs = []
|
||||
|
|
|
@ -853,6 +853,38 @@
|
|||
"expected_output": "<ol>\n<li>\n<p>A</p>\n</li>\n<li>\n<p>B</p>\n</li>\n<li>\n<p>C</p>\n</li>\n<li>\n<p>D<br>\nordinary paragraph</p>\n</li>\n<li>\n<p>AA</p>\n</li>\n<li>\n<p>BB</p>\n</li>\n</ol>",
|
||||
"marked_expected_output": "<ol>\n<li><p>A</p>\n</li>\n<li><p>B</p>\n</li>\n<li><p>C</p>\n</li>\n<li><p>D<br>\nordinary paragraph</p>\n</li>\n<li><p>AA</p>\n</li>\n<li><p>BB</p>\n</li>\n</ol>",
|
||||
"text_content": "1. A\n2. B\n3. C\n4. D\nordinary paragraph\n5. AA\n6. BB"
|
||||
},
|
||||
{
|
||||
"name": "spoilers_fenced_spoiler",
|
||||
"input": "```spoiler header\ncontent\n```\noutside spoiler\n",
|
||||
"expected_output": "<div class=\"spoiler-block\"><div class=\"spoiler-header\">\n\n<p>header</p>\n</div><div class=\"spoiler-content\" aria-hidden=\"true\">\n\n<p>content</p>\n</div></div>\n\n<p>outside spoiler</p>"
|
||||
},
|
||||
{
|
||||
"name": "spoilers_empty_header",
|
||||
"input": "```spoiler\ncontent\n```\noutside spoiler\n",
|
||||
"expected_output": "<div class=\"spoiler-block\"><div class=\"spoiler-header\">\n\n</div><div class=\"spoiler-content\" aria-hidden=\"true\">\n\n<p>content</p>\n</div></div>\n\n<p>outside spoiler</p>"
|
||||
},
|
||||
{
|
||||
"name": "spoilers_script_tags",
|
||||
"input": "```spoiler <script>alert(1)</script>\n<script>alert(1)</script>\n```",
|
||||
"expected_output": "<div class=\"spoiler-block\"><div class=\"spoiler-header\">\n\n<p><script>alert(1)</script></p>\n</div><div class=\"spoiler-content\" aria-hidden=\"true\">\n\n<p><script>alert(1)</script></p>\n</div></div>",
|
||||
"marked_expected_output": "<div class=\"spoiler-block\"><div class=\"spoiler-header\">\n\n<p><script>alert(1)</script>\n\n</p>\n</div><div class=\"spoiler-content\" aria-hidden=\"true\">\n\n<p><script>alert(1)</script>\n\n</p>\n</div></div>"
|
||||
},
|
||||
{
|
||||
"name": "spoilers_block_quote",
|
||||
"input": "~~~quote\n```spoiler header\ncontent\n```\noutside spoiler\n~~~\noutside quote",
|
||||
"expected_output": "<blockquote>\n<div class=\"spoiler-block\"><div class=\"spoiler-header\">\n\n<p>header</p>\n</div><div class=\"spoiler-content\" aria-hidden=\"true\">\n\n<p>content</p>\n</div></div>\n\n<p>outside spoiler</p>\n</blockquote>\n<p>outside quote</p>"
|
||||
},
|
||||
{
|
||||
"name": "spoilers_with_header_markdown",
|
||||
"input": "```spoiler [Header](https://example.com) :smile:\ncontent\n```",
|
||||
"expected_output": "<div class=\"spoiler-block\"><div class=\"spoiler-header\">\n\n<p><a href=\"https://example.com\">Header</a> <span aria-label=\"smile\" class=\"emoji emoji-263a\" role=\"img\" title=\"smile\">:smile:</span></p>\n</div><div class=\"spoiler-content\" aria-hidden=\"true\">\n\n<p>content</p>\n</div></div>"
|
||||
},
|
||||
{
|
||||
"name": "spoiler_with_inline_image",
|
||||
"input": "```spoiler header\nContent http://example.com/image.png\n```",
|
||||
"expected_output": "<div class=\"spoiler-block\"><div class=\"spoiler-header\">\n\n<p>header</p>\n</div><div class=\"spoiler-content\" aria-hidden=\"true\">\n\n<p>Content <a href=\"http://example.com/image.png\">http://example.com/image.png</a></p>\n<div class=\"message_inline_image\"><a href=\"http://example.com/image.png\"><img data-src-fullsize=\"/thumbnail?url=http%3A%2F%2Fexample.com%2Fimage.png&size=full\" src=\"/thumbnail?url=http%3A%2F%2Fexample.com%2Fimage.png&size=thumbnail\"></a></div></div></div>",
|
||||
"marked_expected_output": "<div class=\"spoiler-block\"><div class=\"spoiler-header\">\n\n<p>header</p>\n</div><div class=\"spoiler-content\" aria-hidden=\"true\">\n\n<p>Content <a href=\"http://example.com/image.png\">http://example.com/image.png</a></p>\n</div></div>"
|
||||
}
|
||||
],
|
||||
"linkify_tests": [
|
||||
|
|
Loading…
Reference in New Issue