diff --git a/.gitignore b/.gitignore index 69aab94..a0cf010 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ dist/ node_modules/ publish/ -WIP/ \ No newline at end of file +WIP/ diff --git a/.npmignore b/.npmignore index 3dfd160..109de0c 100644 --- a/.npmignore +++ b/.npmignore @@ -6,4 +6,4 @@ /dist tsconfig.json webpack.config.js -/WIP \ No newline at end of file +/WIP diff --git a/README.md b/README.md index dfa7ef1..769f3df 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,67 @@ -# Joplin Plugin - Spoiler cards +# Joplin Plugin - Spoilers -This Joplin plugin allows you to create cards with title and extendable body. +This Joplin plugin allows you to create inline spoilers and spoiler blocks with title and extendable body. -**Note**: Requires Joplin 1.7.0+ +**Note**: Requires Joplin 1.7.11+ -**Version**: 0.3.1 +**Version**: 1.0.0 - +**Spoilers (inline)** + + + +**Spoiler blocks** + + + + + +## Installation + +- Open Joplin and navigate to `Preferences > Plugins` +- Search for `Spoilers` and click install +- Restart Joplin + +### Uninstall + +- Open Joplin and navigate to `Tools > Options > Plugins` +- Search for `Spoilers` plugin +- Press `Delete` to remove the plugin or `Disable` to disable it +- Restart Joplin ## Usage -In order to create a card, you need to write in this format: +### Spoiler (inline) + +In order to create a spoiler (inline), you can: +- press on the `Spoiler` button or +- use the shortcut `Ctrl + Alt + O` +- or write in the following format: + +``` +%%spoiler%% +``` + +### Spoiler block + +In order to create a spoiler block, you can: +- press on the `Spoiler block` button or +- use the shortcut `Ctrl + Alt + P` +- or write in the following format: ``` :[ -Card name here... +Spoiler title here... -Card body text here... +Spoiler body text here... ]: ``` -Please note, that the empty line above and below card body text is **needed**. -Card body supports markdown formatting as well. - -### Example +Please note, that the empty line above and below spoiler body text is **needed**. +Spoiler title and body supports markdown formatting as well. +**Example**: ``` :[ 3 ways to check if an Object has a property in JS @@ -46,31 +82,58 @@ hero.toString; // => function() {...} hero.hasOwnProperty('toString'); // => false ~~~ * * * -.... ]: ``` -## Custom styling of cards +## Custom styles -If you would like to style the spoiler cards to your preference, use the following in your `userstyle.css` file: +If you would like to style the spoiler blocks to your preference, use the following in your `userstyle.css` file: ```css -/* Styling of the card title */ +/* Styling of the spoiler block title */ .summary-title { } -/* Styling of the card body */ +/* Styling of the spoiler block body */ .summary-content { } ``` +### Exporting styles + +By default when exporting with spoiler blocks, the blocks get extended, show the body and hides the arrows. Inline spoilers stay hidden. + +Alternately, if you would like to style the spoiler blocks to your liking when exporting, use the following in you `userstyle.css` file: +```css +@media print { + + /* Hides the side arrow */ + .summary-title:before { + content: ""; + } + + /* Container for spoiler blocks */ + .spoiler-block {} + + /* Container for spoiler title */ + #spoiler-block-title {} + + /* Container for spoiler body */ + #spoiler-block-body { + /* Shows the body contents */ + display: block; + animation: none; + } + +} +``` + ## Notes -- I have not thoroughly tested the plugin, so note that **there might be bugs**. -- I might have to change formatting in the future to be more convenient, but nothing significant from now on. +- **There might be bugs**, [report them here](https://github.com/martinkorelic/joplin-plugin-spoilers/issues) and I'll try to fix them when I'll find time. > Created on 12th April 2021 \ No newline at end of file diff --git a/docs/cards-plugin-preview.gif b/docs/cards-plugin-preview.gif deleted file mode 100644 index d1ff015..0000000 Binary files a/docs/cards-plugin-preview.gif and /dev/null differ diff --git a/docs/inline-spoiler-preview.gif b/docs/inline-spoiler-preview.gif new file mode 100644 index 0000000..dcdb234 Binary files /dev/null and b/docs/inline-spoiler-preview.gif differ diff --git a/docs/spoiler-block-preview.gif b/docs/spoiler-block-preview.gif new file mode 100644 index 0000000..7e0fff6 Binary files /dev/null and b/docs/spoiler-block-preview.gif differ diff --git a/package-lock.json b/package-lock.json index a29d239..e0b0134 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,9 +47,9 @@ "dev": true }, "@types/node": { - "version": "14.14.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", - "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", + "version": "14.14.44", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.44.tgz", + "integrity": "sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA==", "dev": true }, "@webassemblyjs/ast": { @@ -694,9 +694,9 @@ "dev": true }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "requires": { "ansi-styles": "^4.1.0", diff --git a/package.json b/package.json index 9b0c995..1e20b20 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "joplin-plugin-spoiler-cards", - "version": "0.3.1", - "description": "Joplin plugin allows you to create cards with title and extendable body.", + "version": "1.0.0", + "description": "Joplin plugin for creating inline spoilers and spoiler blocks.", "author": "martinkorelic", - "homepage": "https://github.com/martinkorelic/joplin-plugin-spoiler-cards", + "homepage": "https://github.com/martinkorelic/joplin-plugin-spoilers", "bugs": { - "url": "https://github.com/martinkorelic/joplin-plugin-spoiler-cards/issues" + "url": "https://github.com/martinkorelic/joplin-plugin-spoilers/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/martinkorelic/joplin-plugin-spoiler-cards.git" + "url": "git+https://github.com/martinkorelic/joplin-plugin-spoilers.git" }, "scripts": { "dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive", @@ -18,11 +18,14 @@ }, "license": "MIT", "keywords": [ - "joplin-plugin" + "joplin", + "joplin-plugin", + "markdown", + "markdownit" ], "devDependencies": { - "@types/node": "^14.0.14", - "chalk": "^4.1.0", + "@types/node": "^14.14.44", + "chalk": "^4.1.1", "copy-webpack-plugin": "^6.1.0", "fs-extra": "^9.0.1", "glob": "^7.1.6", diff --git a/src/cards.js b/src/cards.js deleted file mode 100644 index bd3cc9b..0000000 --- a/src/cards.js +++ /dev/null @@ -1,144 +0,0 @@ -module.exports = { - default: function(context) { - return { - plugin: async function(markdownIt, _options) { - const pluginId = context.pluginId; - const contentScriptId = _options.contentScriptId; - - markdownIt.block.ruler.after('fence', 'spoiler_card', spoiler_card, {alt: ['paragraph', 'reference', 'blockquote', 'list']}); - - /* - const defaultRender = markdownIt.renderer.rules.fence || function(tokens, idx, options, env, self) { - return self.renderToken(tokens, idx, options, env, self); - }; - markdownIt.renderer.rules.fence = function (tokens, idx, options, env, self) { - let token = tokens[idx]; - - // We detect if card block - if (token.info !== 'card') return defaultRender(tokens, idx, options, env, self); - - // Split the card title and body by :[]: - let card = token.content.match(/(?<title>.+)\n:\[(?<body>(?:.|\n)*?)\]:/i); - - // Return default renderer if formatted wrong - if (!card) return defaultRender(tokens, idx, options, env, self); - - let { title, body } = card.groups; - - if (!title || !body) return defaultRender(tokens, idx, options, env, self); - - // Re render markdown content within - return ` - <details> \ - <summary class="summary-title">${title}</summary> - <div class="summary-content"> - ${markdownIt.render(body)} - </div> - </details>`; - }*/ - }, - // Assests such as JS or CSS that should be loaded in the rendered HTML document - assets: function() { - return [ - { - name: "./cards.css" - } - ]; - }, - } - } -} - -function spoiler_card(state, start, end, silent) { - - let lastPos, found = false, - pos = state.bMarks[start]+ state.tShift[start], - max = state.eMarks[start], - next; - - var token; - let curLine = start; - - if (pos + 2 > max) return false; - - // Check when it starts with ':[' - if (state.src.slice(pos, pos+2) !== ':[') return false; - pos += 2; - - // We don't accept empty card formats - if (state.src.slice(pos, pos+2) == ']:') return false; - pos += 2; - - if (silent) return true; - - curLine++; - // Correct formatting of the title - if (state.isEmpty(curLine)) return false; - - let title = curLine; - - curLine++; - // Needs to be atleast one empty line in between for better formatting - if (!state.isEmpty(curLine)) return false; - - curLine++; - // Now there needs to be atleast some content before we render the card - if (state.isEmpty(curLine)) return false; - - // If the formatting is okay, we create new tokens - /* - 1 means the tag is opening - 0 means the tag is self-closing - -1 means the tag is closing - */ - - state.push('details_open', 'details', 1); - - token = state.push('summary_open', 'summary', 1); - token.attrs = [[ 'class', 'summary-title' ]]; - - token = state.push('inline', '', 0); - token.map = [ title, title ]; - token.content = state.getLines(title, title+1, state.tShift[title], false).trim(); - token.children = []; - - state.push('summary_close', 'summary', -1); - - token = state.push('spoiler_card_body', 'div', 1); - token.attrs = token.attr = [[ 'class', 'summary-content' ]]; - - // Content starts - for (next = curLine; !found;) { - next++; - - if (next >= end) { - break; - } - - pos = state.bMarks[next] + state.tShift[next]; - max = state.eMarks[next]; - - if (pos < max && state.tShift[next] < state.blkIndent) { - break; - } - - // Check if there's only ']:' on the line - if (state.src.slice(pos, max).length == 2 && state.src.slice(pos, max).trim().slice(-2) == ']:') { - lastPos = state.src.slice(0, max).lastIndexOf(']:'); - lastLine = state.src.slice(pos, lastPos); - found = true; - } - } - - // We use to render markdown within - state.md.block.tokenize(state, curLine, next, false); - - state.line = next + 1; - - // Finalize body - state.push('spoiler_body_close', 'div', -1); - - // Finalize details - state.push('details_close', 'details', -1); - return true; -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9d10381..ae25bbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,134 +1,57 @@ import joplin from 'api'; import { ContentScriptType, ToolbarButtonLocation } from 'api/types'; -// const uslug = require('uslug'); - joplin.plugins.register({ onStart: async function() { - /* - // Panel legacy (might upgrade and create table of cards panel) - - const panels = joplin.views.panels; - const view = await panels.create('tod-panel'); - - // Create a toolbar button + // Create a spoiler block command await joplin.commands.register({ - name: 'toggleTOD', - label: 'Toggle Table of definitions', - iconName: 'fas fa-scroll', + name: 'insert_spoiler_block', + label: 'Spoiler block', + iconName: 'fas fa-angle-right', execute: async () => { - const visible = await panels.visible(view); - panels.show(view, !visible); + await joplin.commands.execute('insertText',':[\nTitle...\n\nBody...\n\n]:'); } }); - await joplin.views.toolbarButtons.create('todButton', 'toggleTOD', ToolbarButtonLocation.NoteToolbar); + // Create a spoiler block toolbar button + await joplin.views.toolbarButtons.create('insert_spoiler_block', 'insert_spoiler_block', ToolbarButtonLocation.EditorToolbar); - // Building the panel - await joplin.views.panels.setHtml(view, 'Loading...'); - await joplin.views.panels.addScript(view, './webview.css'); - await joplin.views.panels.addScript(view, './table-tod.js'); - - // Message handler - panels.onMessage(view, async (message: any) => { - console.info(message); - - if (message.name == "fetchData") { - joplin.views.panels.setHtml(view, "Loud and clear!"); - return await fetchDefCards(); - } else if (message.name == "tokens") { - console.info(message.token); - console.info(message.tokens); - return; + // Create a spoiler block command shortcut + await joplin.views.menus.create('spoiler_block', 'Insert spoiler block', [ + { + commandName: 'insert_spoiler_block', + accelerator: 'Ctrl+Alt+P' } + ]); - }); - - // Structure the panel - await panels.setHtml(view, ` - <div class="container">Click to generate cards</div> - `); - - // Scans and fetches all the definitions made - async function fetchDefCards() { - - const { body } = await joplin.workspace.selectedNote(); - - if (!body) return body; - - var cards = body.match(/(.+)\n:\[((?:.|\n)*?)\]:/gi); - - const allCards = []; - for (var card in cards) { - allCards.push(cards[card].match(/(?<title>.+)\n:\[(?<body>(?:.|\n)*?)\]:/i).groups); + // Create a inline spoiler command + await joplin.commands.register({ + name: 'insert_inline_spoiler', + label: 'Spoiler', + iconName: 'fas fa-low-vision', + execute: async () => { + await joplin.commands.execute('insertText','%%spoiler%%'); } - - return allCards; - } - - // This event will be triggered when the user selects a different note - await joplin.workspace.onNoteSelectionChange(() => { - updateToDView(); }); - // This event will be triggered when the content of the note changes - // as you also want to update the TOC in this case. - await joplin.workspace.onNoteChange(() => { - updateToDView(); - }); - - // Also update the TOC when the plugin starts - updateToDView(); - - async function updateToDView() { - - // Get the current note in the workspace - const cards = await fetchDefCards(); - slugs = {}; + // Create a inline spoiler toolbar button + await joplin.views.toolbarButtons.create('insert_inline_spoiler', 'insert_inline_spoiler', ToolbarButtonLocation.EditorToolbar); - // Keep in mind that it can be `null` if nothing is currently selected! - if (cards) { - - console.info(cards); - console.info('Note content has changed!'); - - } else { - console.info('No note is selected.'); + // Create a inline spoiler command shortcut + await joplin.views.menus.create('inline_spoiler', 'Insert spoiler', [ + { + commandName: 'insert_inline_spoiler', + accelerator: 'Ctrl+Alt+O' } - - } - */ + ]); // Here we register new Markdown plugin await joplin.contentScripts.register( ContentScriptType.MarkdownItPlugin, - 'table-tod', - './cards.js' + 'Spoilers', + './spoilers.js' ); }, -}); - -/* Helper functions - -let slugs = {}; - -function headerSlug(headerText) { - const s = uslug(headerText); - let num = slugs[s] ? slugs[s] : 1; - const output = [s]; - if (num > 1) output.push(num); - slugs[s] = num + 1; - return output.join('-'); -} - -function escapeHtml(unsafe:string) { - return unsafe - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} -*/ \ No newline at end of file +}); \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 7038fe6..4627797 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,11 +2,11 @@ "manifest_version": 1, "id": "joplin.plugin.spoiler.cards", "app_min_version": "1.7", - "version": "0.3.1", - "name": "Spoiler cards", - "description": "Create spoiler cards with title and extendable body.", + "version": "1.0.0", + "name": "Spoilers", + "description": "Create inline spoilers and spoiler blocks with title and extendable body.", "author": "Martin Korelič", - "homepage_url": "https://github.com/martinkorelic/joplin-plugin-spoiler-cards", - "repository_url": "https://github.com/martinkorelic/joplin-plugin-spoiler-cards", - "keywords": ["joplin", "plugin", "cards", "spoiler"] + "homepage_url": "https://github.com/martinkorelic/joplin-plugin-spoilers", + "repository_url": "https://github.com/martinkorelic/joplin-plugin-spoilers", + "keywords": ["joplin", "plugin", "spoiler", "blocks"] } \ No newline at end of file diff --git a/src/cards.css b/src/spoiler-style.css similarity index 91% rename from src/cards.css rename to src/spoiler-style.css index 5ba0bb9..5dca960 100644 --- a/src/cards.css +++ b/src/spoiler-style.css @@ -70,4 +70,8 @@ details { padding: 1em; font-weight: 300; line-height: 1.5; +} + +.spoiler-inline { + color: red; } \ No newline at end of file diff --git a/src/spoilers.js b/src/spoilers.js new file mode 100644 index 0000000..5c55706 --- /dev/null +++ b/src/spoilers.js @@ -0,0 +1,428 @@ +module.exports = { + default: function(context) { + return { + plugin: async function(markdownIt, _options) { + const pluginId = context.pluginId; + const contentScriptId = _options.contentScriptId; + + markdownIt.block.ruler.after('fence', 'spoiler_block', spoiler_block, {alt: ['paragraph', 'reference', 'blockquote', 'list']}); + + markdownIt.inline.ruler.after('escape', 'spoiler_inline', tokenize_spoiler); + markdownIt.inline.ruler2.after('emphasis', 'spoiler_inline', function (state) { + var curr, + tokens_meta = state.tokens_meta, + max = (state.tokens_meta || []).length; + + postProcess(state, state.delimiters); + + for (curr = 0; curr < max; curr++) { + if (tokens_meta[curr] && tokens_meta[curr].delimiters) { + postProcess(state, tokens_meta[curr].delimiters); + } + } + }); + + // Inline spoiler open renderer + const spoiler_open_defaultRender = markdownIt.renderer.rules.spoiler_open || function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options, env, self); + }; + + const spoiler_inline_open = function(tokens, idx, options, env, self) { + + const token = tokens[idx]; + + if (token.type !== 'spoiler_open') return spoiler_open_defaultRender(tokens, idx, options, env, self); + + // Generate a random id to distinguish between events + let ranhex = genRanHex(8); + // We use a checkbox hack to implement a clickable event + return `<input type="checkbox" class="spoiler-inline" id=${ranhex}><label class="spoiler-inline" for=${ranhex}><span class="spoiler-inline">`; + }; + + markdownIt.renderer.rules.spoiler_open = spoiler_inline_open; + + // Inline spoiler close renderer + const spoiler_close_defaultRender = markdownIt.renderer.rules.spoiler_close || function(tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options, env, self); + }; + + const spoiler_inline_close = function(tokens, idx, options, env, self) { + + const token = tokens[idx]; + + if (token.type !== 'spoiler_close') return spoiler_close_defaultRender(tokens, idx, options, env, self); + + return `</span></label>`; + }; + + markdownIt.renderer.rules.spoiler_close = spoiler_inline_close; + + }, + // Assests such as JS or CSS that should be loaded in the rendered HTML document + assets: function() { + return [ + + /* For some reason this fails when exporting (cannot find the css file in assets) + { + name: 'spoiler-style.css' + } + */ + + // Styling of spoiler blocks + { + inline: true, + mime: 'text/css', + text: ` + .spoiler-block { + font-size: 1rem; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 10px 10px -5px rgba(0, 0, 0, 0.04); + width: 100%; + color: black; + background: #c2c2c2; + border-radius: 8px; + border: 1px solid #7a7a7a; + position: relative; + margin-top: 1em; + margin-bottom: 1em; + display: grid; + } + + .spoiler-block-input { + display: none; + } + + label.spoiler-block-title { + cursor: pointer; + } + + .summary-title { + user-select: none; + padding: 0.8em; + font-family: monospace; + border-radius: 8px; + font-size: 18px; + } + + .summary-title:hover { + opacity: 0.7; + } + + .summary-title:before { + margin-right: 0.3em; + font-size: 1.6em; + vertical-align: text-top; + content: "\u2B9E "; + display: inline-block; + transform-origin: center; + transition: 200ms linear; + } + + @keyframes open { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + div#spoiler-block-body { + display: none; + animation: open 0.3s ease-in-out; + } + + input.spoiler-block-input:checked ~ label ~ div#spoiler-block-body { + display: block; + } + + input.spoiler-block-input:checked ~ label#spoiler-block-title:before { + transform: rotate(90deg); + } + + .summary-content { + border-top: 1px solid #7a7a7a; + cursor: default; + padding: 1em; + font-weight: 300; + line-height: 1.5; + } + + /* Styles for exporting */ + @media print { + + div#spoiler-block-body { + display: block; + animation: none; + } + + .summary-title:before { + content: ""; + } + } + ` + }, + + // Styling of inline spoilers + { + inline: true, + mime: 'text/css', + text: ` + input.spoiler-inline { + display: none; + } + + input.spoiler-inline + label.spoiler-inline { + cursor: pointer; + background: #000; + border-radius: 3px; + box-shadow: 0 0 1px #ffffff; + color: #000; + user-select: none; + display: inline-flex; + } + + input.spoiler-inline + label.spoiler-inline > span.spoiler-inline { + opacity: 0; + } + + input.spoiler-inline:checked + label.spoiler-inline > span.spoiler-inline { + background: #0001; + color: inherit; + box-shadow: none; + user-select: text; + opacity: 1; + } + + input.spoiler-inline:checked + label.spoiler-inline { + background: #0001; + color: inherit; + box-shadow: none; + user-select: text; + display: inline-flex; + } + ` + } + ]; + }, + } + } +} + +// Tokenizing the spoiler blocks +function spoiler_block(state, start, end, silent) { + + let found = false, + pos = state.bMarks[start]+ state.tShift[start], + max = state.eMarks[start], + next; + + var token; + let curLine = start; + + if (pos + 2 > max) return false; + + // Check when it starts with ':[' + if (state.src.slice(pos, pos+2) !== ':[') return false; + pos += 2; + + // We don't accept empty card formats + if (state.src.slice(pos, pos+2) == ']:') return false; + pos += 2; + + if (silent) return true; + + curLine++; + // Correct formatting of the title + if (state.isEmpty(curLine)) return false; + + let title = curLine; + + curLine++; + // Needs to be atleast one empty line in between for better formatting + if (!state.isEmpty(curLine)) return false; + + curLine++; + // Now there needs to be atleast some content before we render the card + if (state.isEmpty(curLine)) return false; + + // If the formatting is okay, we create new tokens + /* + 1 means the tag is opening + 0 means the tag is self-closing + -1 means the tag is closing + */ + + // Spoiler block + token = state.push('spoiler_block_open', 'div', 1); + token.attrs = [[ 'class', 'spoiler-block']]; + + // We generate a random id to distinguish between events + let ranhex = genRanHex(8); + + // Input + token = state.push('spoiler_title_input', 'input', 0); + token.attrs = [[ 'class', 'spoiler-block-input' ], [ 'type', 'checkbox' ], [ 'id', ranhex ]]; + + // Spoiler title - label + token = state.push('spoiler_title_open', 'label', 1); + token.attrs = [[ 'class', 'summary-title' ], [ 'id', 'spoiler-block-title' ], [ 'for', ranhex ]]; + + token = state.push('inline', '', 0); + token.map = [ title, title ]; + token.content = state.getLines(title, title+1, state.tShift[title], false).trim(); + token.children = []; + + token = state.push('spoiler_title_close', 'label', -1); + + // Spoiler body - div + token = state.push('spoiler_body_open', 'div', 1); + token.attrs = [[ 'class', 'summary-content' ], [ 'id', 'spoiler-block-body' ]]; + + // Content starts + for (next = curLine; !found;) { + next++; + + if (next >= end) { + break; + } + + pos = state.bMarks[next] + state.tShift[next]; + max = state.eMarks[next]; + + if (pos < max && state.tShift[next] < state.blkIndent) { + break; + } + + // Check if there's only ']:' on the line + if (state.src.slice(pos, max).length == 2 && state.src.slice(pos, max).trim().slice(-2) == ']:') { + found = true; + } + } + + // We use to render markdown within + state.md.block.tokenize(state, curLine, next, false); + + state.line = next + 1; + + // Finalize body + state.push('spoiler_body_close', 'div', -1); + + // Finalize details + state.push('spoiler_block_close', 'div', -1); + + return true; +} + +// Insert each marker as a separate text token, and add it to delimiter list +function tokenize_spoiler(state, silent) { + var i, scanned, token, len, ch, + start = state.pos, + marker = state.src.charCodeAt(start); + + if (silent) { return false; } + + if (marker !== 0x25/* % */) { return false; } + + scanned = state.scanDelims(state.pos, true); + len = scanned.length; + ch = String.fromCharCode(marker); + + if (len < 2) { return false; } + + if (len % 2) { + token = state.push('text', '', 0); + token.content = ch; + len--; + } + + for (i = 0; i < len; i += 2) { + token = state.push('text', '', 0); + token.content = ch + ch; + + if (!scanned.can_open && !scanned.can_close) { continue; } + + state.delimiters.push({ + marker: marker, + length: 0, // disable "rule of 3" length checks meant for emphasis + jump: i / 2, // 1 delimiter = 2 characters + token: state.tokens.length - 1, + end: -1, + open: scanned.can_open, + close: scanned.can_close + }); + } + + state.pos += scanned.length; + + return true; +} + +// Walk through delimiter list and replace text tokens with tags +function postProcess(state, delimiters) { + var i, j, + startDelim, + endDelim, + token, + loneMarkers = [], + max = delimiters.length; + + for (i = 0; i < max; i++) { + startDelim = delimiters[i]; + + if (startDelim.marker !== 0x25/* % */) { + continue; + } + + if (startDelim.end === -1) { + continue; + } + + endDelim = delimiters[startDelim.end]; + + token = state.tokens[startDelim.token]; + token.type = 'spoiler_open'; + token.tag = 'spoiler'; + token.nesting = 1; + token.markup = '%%'; + token.content = ''; + + token = state.tokens[endDelim.token]; + token.type = 'spoiler_close'; + token.tag = 'spoiler'; + token.nesting = -1; + token.markup = '%'; + token.content = ''; + + if (state.tokens[endDelim.token - 1].type === 'text' && + state.tokens[endDelim.token - 1].content === '%') { + + loneMarkers.push(endDelim.token - 1); + } + } + + // If a marker sequence has an odd number of characters, it's splitted + // like this: `%%%%` -> `%` + `%%` + `%%`, leaving one marker at the + // start of the sequence. + // + // So, we have to move all those markers after subsequent s_close tags. + // + while (loneMarkers.length) { + i = loneMarkers.pop(); + j = i + 1; + + while (j < state.tokens.length && state.tokens[j].type === 'spoiler_close') { + j++; + } + + j--; + + if (i !== j) { + token = state.tokens[j]; + state.tokens[j] = state.tokens[i]; + state.tokens[i] = token; + } + } +} + +const genRanHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 4474cab..85284e9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,4 +7,4 @@ "allowJs": true, "baseUrl": "." } -} +} \ No newline at end of file