diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue index baee118d0..606ba625f 100644 --- a/client/components/editor/editor-markdown.vue +++ b/client/components/editor/editor-markdown.vue @@ -224,6 +224,7 @@ import mermaid from 'mermaid' // Helpers import katexHelper from './common/katex' +import spoiler from './markdown/spoiler' import tabsetHelper from './markdown/tabset' import cmFold from './common/cmFold' @@ -264,6 +265,7 @@ const md = new MarkdownIt({ .use(mdAttrs, { allowedAttributes: ['id', 'class', 'target'] }) + .use(spoiler) .use(mdDecorate) .use(underline) .use(mdEmoji) diff --git a/client/components/editor/markdown/spoiler.js b/client/components/editor/markdown/spoiler.js new file mode 100644 index 000000000..d742b1798 --- /dev/null +++ b/client/components/editor/markdown/spoiler.js @@ -0,0 +1,12 @@ +const container = require('markdown-it-container') + +module.exports = md => md.use(container, 'spoiler', { + validate: params => /^spoiler(?:\s+.*)?$/.test(params.trim()), + render(tokens, idx) { + if (tokens[idx].nesting === 1) { + const title = tokens[idx].info.trim().slice('spoiler'.length).trim() || 'Spoiler' + return `
${md.utils.escapeHtml(title)}\n` + } + return '
\n' + } +}) diff --git a/server/modules/rendering/markdown-core/renderer.js b/server/modules/rendering/markdown-core/renderer.js index 5723214e7..fc83c8823 100644 --- a/server/modules/rendering/markdown-core/renderer.js +++ b/server/modules/rendering/markdown-core/renderer.js @@ -2,6 +2,7 @@ const md = require('markdown-it') const mdAttrs = require('markdown-it-attrs') const mdDecorate = require('markdown-it-decorate') const _ = require('lodash') +const spoiler = require('./spoiler') const underline = require('./underline') const quoteStyles = { @@ -43,6 +44,7 @@ module.exports = { mkdown.use(mdAttrs, { allowedAttributes: ['id', 'class', 'target'] }) + mkdown.use(spoiler) mkdown.use(mdDecorate) for (let child of this.children) { diff --git a/server/modules/rendering/markdown-core/spoiler.js b/server/modules/rendering/markdown-core/spoiler.js new file mode 100644 index 000000000..d742b1798 --- /dev/null +++ b/server/modules/rendering/markdown-core/spoiler.js @@ -0,0 +1,12 @@ +const container = require('markdown-it-container') + +module.exports = md => md.use(container, 'spoiler', { + validate: params => /^spoiler(?:\s+.*)?$/.test(params.trim()), + render(tokens, idx) { + if (tokens[idx].nesting === 1) { + const title = tokens[idx].info.trim().slice('spoiler'.length).trim() || 'Spoiler' + return `
${md.utils.escapeHtml(title)}\n` + } + return '
\n' + } +}) diff --git a/server/test/modules/rendering/markdown-core.test.js b/server/test/modules/rendering/markdown-core.test.js new file mode 100644 index 000000000..4c0c02526 --- /dev/null +++ b/server/test/modules/rendering/markdown-core.test.js @@ -0,0 +1,38 @@ +const renderer = require('../../../modules/rendering/markdown-core/renderer') + +const render = input => renderer.render.call({ + input, + children: [], + config: { + allowHTML: true, + linebreaks: true, + linkify: true, + typographer: false, + quotes: 'English', + underline: false + } +}) + +describe('markdown-core spoiler container', () => { + it('renders a closed details element with a default title', async () => { + const output = await render('::: spoiler\nHidden **content**.\n:::') + + expect(output).toContain('
Spoiler') + expect(output).toContain('

Hidden content.

') + expect(output).toContain('
') + }) + + it('supports a custom escaped title', async () => { + const output = await render('::: spoiler Reveal \nSecret\n:::') + + expect(output).toContain('Reveal <script>alert(1)</script>') + expect(output).not.toContain('Reveal