index.js000644 0000012101 14334777706007366 0ustar00000000 000000 !function(e){var t={};function o(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,o),i.l=!0,i.exports}o.m=e,o.c=t,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)o.d(n,i,function(t){return e[t]}.bind(null,i));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,o){"use strict";var n=this&&this.__awaiter||function(e,t,o,n){return new(o||(o=Promise))((function(i,r){function l(e){try{u(n.next(e))}catch(e){r(e)}}function a(e){try{u(n.throw(e))}catch(e){r(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof o?t:new o((function(e){e(t)}))).then(l,a)}u((n=n.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0});const i=o(1),r=o(2);i.default.plugins.register({onStart:function(){return n(this,void 0,void 0,(function*(){yield i.default.commands.register({name:"insert_spoiler_block",label:"Spoiler block",iconName:"fas fa-angle-right",execute:()=>n(this,void 0,void 0,(function*(){let e=(yield i.default.commands.execute("selectedText")).split("\n\n");e.length>1?yield i.default.commands.execute("replaceSelection",`:[\n${e[0]}\n\n${e.slice(1).join("\n\n")}\n\n]:`):yield i.default.commands.execute("insertText",":[\nTitle\n\nBody\n\n]:")}))}),yield i.default.views.toolbarButtons.create("insert_spoiler_block","insert_spoiler_block",r.ToolbarButtonLocation.EditorToolbar),yield i.default.views.menus.create("spoiler_block","Insert spoiler block",[{commandName:"insert_spoiler_block",accelerator:"Ctrl+Alt+P"}]),yield i.default.commands.register({name:"insert_inline_spoiler",label:"Spoiler",iconName:"fas fa-low-vision",execute:()=>n(this,void 0,void 0,(function*(){const e=yield i.default.commands.execute("selectedText");e?yield i.default.commands.execute("replaceSelection",`%%${e}%%`):yield i.default.commands.execute("insertText","%%spoiler%%")}))}),yield i.default.views.toolbarButtons.create("insert_inline_spoiler","insert_inline_spoiler",r.ToolbarButtonLocation.EditorToolbar),yield i.default.views.menus.create("inline_spoiler","Insert spoiler",[{commandName:"insert_inline_spoiler",accelerator:"Ctrl+Alt+O"}]),yield i.default.contentScripts.register(r.ContentScriptType.MarkdownItPlugin,"Spoilers","./spoilers.js")}))}})},function(e,t,o){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=joplin},function(e,t,o){"use strict";var n;Object.defineProperty(t,"__esModule",{value:!0}),t.ContentScriptType=t.SettingStorage=t.AppType=t.SettingItemSubType=t.SettingItemType=t.ToolbarButtonLocation=t.isContextMenuItemLocation=t.MenuItemLocation=t.ModelType=t.ImportModuleOutputFormat=t.FileSystemItem=void 0,function(e){e.File="file",e.Directory="directory"}(t.FileSystemItem||(t.FileSystemItem={})),function(e){e.Markdown="md",e.Html="html"}(t.ImportModuleOutputFormat||(t.ImportModuleOutputFormat={})),function(e){e[e.Note=1]="Note",e[e.Folder=2]="Folder",e[e.Setting=3]="Setting",e[e.Resource=4]="Resource",e[e.Tag=5]="Tag",e[e.NoteTag=6]="NoteTag",e[e.Search=7]="Search",e[e.Alarm=8]="Alarm",e[e.MasterKey=9]="MasterKey",e[e.ItemChange=10]="ItemChange",e[e.NoteResource=11]="NoteResource",e[e.ResourceLocalState=12]="ResourceLocalState",e[e.Revision=13]="Revision",e[e.Migration=14]="Migration",e[e.SmartFilter=15]="SmartFilter",e[e.Command=16]="Command"}(t.ModelType||(t.ModelType={})),function(e){e.File="file",e.Edit="edit",e.View="view",e.Note="note",e.Tools="tools",e.Help="help",e.Context="context",e.NoteListContextMenu="noteListContextMenu",e.EditorContextMenu="editorContextMenu",e.FolderContextMenu="folderContextMenu",e.TagContextMenu="tagContextMenu"}(n=t.MenuItemLocation||(t.MenuItemLocation={})),t.isContextMenuItemLocation=function(e){return[n.Context,n.NoteListContextMenu,n.EditorContextMenu,n.FolderContextMenu,n.TagContextMenu].includes(e)},function(e){e.NoteToolbar="noteToolbar",e.EditorToolbar="editorToolbar"}(t.ToolbarButtonLocation||(t.ToolbarButtonLocation={})),function(e){e[e.Int=1]="Int",e[e.String=2]="String",e[e.Bool=3]="Bool",e[e.Array=4]="Array",e[e.Object=5]="Object",e[e.Button=6]="Button"}(t.SettingItemType||(t.SettingItemType={})),function(e){e.FilePathAndArgs="file_path_and_args",e.FilePath="file_path",e.DirectoryPath="directory_path"}(t.SettingItemSubType||(t.SettingItemSubType={})),function(e){e.Desktop="desktop",e.Mobile="mobile",e.Cli="cli"}(t.AppType||(t.AppType={})),function(e){e[e.Database=1]="Database",e[e.File=2]="File"}(t.SettingStorage||(t.SettingStorage={})),function(e){e.MarkdownItPlugin="markdownItPlugin",e.CodeMirrorPlugin="codeMirrorPlugin"}(t.ContentScriptType||(t.ContentScriptType={}))}]);manifest.json000644 0000000735 14334777706010434 0ustar00000000 000000 { "manifest_version": 1, "id": "joplin.plugin.spoiler.cards", "app_min_version": "1.7", "version": "1.0.6", "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-spoilers", "repository_url": "https://github.com/martinkorelic/joplin-plugin-spoilers", "keywords": ["joplin", "plugin", "spoiler", "blocks"] }spoiler-style.css000644 0000002351 14334777706011254 0ustar00000000 000000 summary:focus{ outline: none; } summary:hover { opacity: 0.7; } details > summary { list-style-type: none; cursor: pointer; } details > summary::-webkit-details-marker { display: none; } @keyframes open { 0% { opacity: 0; } 100% { opacity: 1; } } details summary:before { margin-right: 1.3em; font-size: 1.5em; vertical-align: middle; content: "\2B9E "; display: inline-block; transform-origin: center; transition: 200ms linear; } details[open] summary ~ * { animation: open 0.3s ease-in-out; } details[open] > summary:before { transform: rotate(90deg); } details { 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; } .summary-title { user-select: none; padding: 0.8em; font-family: monospace; border-radius: 8px; font-size: 18px; } .summary-content { border-top: 1px solid #7a7a7a; cursor: default; padding: 1em; font-weight: 300; line-height: 1.5; } .spoiler-inline { color: red; }spoilers.js000644 0000040527 14334777706010134 0ustar00000000 000000 module.exports = { default: function(context) { const pluginId = context.pluginId; console.info(`${pluginId} : Content Plugin default function`); return { plugin: async function(markdownIt, _options) { const contentScriptId = _options.contentScriptId; const blocktypes = ['image', 'link_open'] 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); var idxi = idx var isBlocktype = false // Check if inline spoiler needs to be of "display:block" while (tokens[idxi].type !== 'spoiler_close') { if (blocktypes.includes(tokens[idxi].type)) { isBlocktype = true break; } idxi++ } // Generate a random id to distinguish between events let ranhex = genRanHex(8); // We use a checkbox hack to implement a clickable event return ``; }; 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 { 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; } } #spoiler-block-body { display: none; animation: open 0.3s ease-in-out; } #spoiler-block-title { cursor: pointer; } 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; } /* Styles for exporting */ @media print { #spoiler-block-body { display: block; animation: none; } .summary-title:before { content: ""; } } ` }, // Styling of inline spoilers { inline: true, mime: 'text/css', text: ` .spoiler-inline-block { display: block; max-width: max-content; max-height: max-content; } 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; overflow-wrap: anywhere; padding: 2px; } 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; padding: 2px; } ` } ]; }, } } } // 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) { console.info(`spoiler : Exit from 'spoiler_block' : no data`); return false; } // Check when it starts with ':[' if (state.src.slice(pos, pos+2) !== ':[') { console.info(`spoiler : Exit from 'spoiler_block' : no start token`); return false; } pos += 2; // We don't accept empty card formats if (state.src.slice(pos, pos+2) == ']:') { console.info(`spoiler : Exit from 'spoiler_block' : empty card`); return false; } pos += 2; if (silent) return true; console.info(`spoiler : Entry into 'spoiler_block' : real block at ${curLine} - ${end} (${state.level})`); 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' ]]; let bracketLevel = 1; // Content starts for (next = curLine; !found;) { next++; if (next >= end) { console.info(`spoiler : Loop break in 'spoiler_block' : at ${next} >= ${end}`); break; } pos = state.bMarks[next] + state.tShift[next]; max = state.eMarks[next]; if (pos < max && state.tShift[next] < state.blkIndent) { break; } if (state.src.slice(pos, max).length == 2 && state.src.slice(pos, max).trim().slice(-2) == ':[') { bracketLevel ++; } // Check if there's only ']:' on the line if (state.src.slice(pos, max).length == 2 && state.src.slice(pos, max).trim().slice(-2) == ']:') { if (bracketLevel == 1) found = true; else bracketLevel --; } } // 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); console.info(`spoiler : Exit from 'spoiler_block' : real exit at ${next}`); 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('');