UNPKG

@digitalocean/do-markdownit

Version:

Markdown-It plugin for the DigitalOcean Community.

197 lines (164 loc) 6.71 kB
/* Copyright 2022 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ 'use strict'; /** * @module rules/embeds/callout */ const safeObject = require('../../util/safe_object'); const blockLines = require('../../util/block_lines'); /** * @typedef {Object} CalloutOptions * @property {string[]} [allowedClasses] List of case-sensitive classes that are allowed. If not an array, all classes are allowed. * @property {string[]} [extraClasses=['callout']] List of extra classes to apply to a callout div, alongside the given class. * @property {string} [labelClass='callout-label'] Class to use for the label. */ /** * Add support for callout embeds in Markdown, as block syntax. * * The basic syntax is `<$>[<class>]<text><$>`. E.g. `<$>[hello]world<$>`. * The class must be in square brackets, and must come immediately after the opening `<$>`. * Newlines are allowed in the text, as is any other Markdown syntax (both block and inline). * * Callouts can also have a label set within them. The label should be in the format `[label <text>]`. * The label must be on the first newline after the opening `<$>`. * The label cannot contain any newlines, but does support inline Markdown syntax. * * @example * <$>[info] * test * <$> * * <$>[info] * [label hello] * world * <$> * * <div class="callout info"> * <p>test</p> * </div> * * <div class="callout info"> * <p class="callout-label">hello</p> * <p>world</p> * </div> * * @type {import('markdown-it').PluginWithOptions<CalloutOptions>} */ module.exports = (md, options) => { // Get the correct options const optsObj = safeObject(options); /** * Parsing rule for callout markup. * * @type {import('markdown-it/lib/parser_block').RuleBlock} * @private */ const calloutRule = (state, startLine, endLine, silent) => { // If silent, don't replace if (silent) return false; // Get current string to consider (current line to end) const currentLines = blockLines(state, startLine, endLine).join('\n'); // Perform some non-regex checks for speed if (currentLines.length < 10) return false; // <$>[a]b<$> if (currentLines.slice(0, 4) !== '<$>[') return false; // Attempt to find closing mark (ensure there is a newline at the end if mark is at end of doc) const closingMark = `${currentLines}\n`.indexOf('<$>\n'); if (closingMark === -1) return false; // Check for callout match const match = currentLines.slice(0, closingMark + 4).match(/^<\$>\[([^[\]\n]+)]([\s\S]+?)\n?<\$>(?:$|\n)/); if (!match) return false; // Get the class name const className = match[1].trim(); if (!className) return false; // Check class name is allowed if (optsObj.allowedClasses && !optsObj.allowedClasses.includes(className)) return false; // Set the parent type const oldParentType = state.parentType; state.parentType = 'callout'; // Update the pos for the parser state.line = startLine + match[0].trim().split('\n').length; // Add opening token to state const tokenOpen = state.push('callout_open', 'div', 1); tokenOpen.block = true; tokenOpen.markup = `<$>[${match[1]}]`; tokenOpen.callout = { className }; tokenOpen.map = [ startLine, state.line ]; // Update the boundaries based on the markup state.bMarks[startLine] += `<$>[${match[1]}]`.length; state.eMarks[state.line - 1] -= '<$>'.length; // Check for a label let workingStartLint = startLine; const labelMatch = match[2].match(/^\n\[label ([^\n]+)](?:$|\n)/); if (labelMatch) { // Add opening token to state const labelOpenToken = state.push('callout_label_open', 'p', 1); labelOpenToken.block = true; labelOpenToken.markup = '[label '; labelOpenToken.map = [ workingStartLint + 1, workingStartLint + 2 ]; // Create an inline token for the content const labelToken = state.push('inline', '', 0); labelToken.content = labelMatch[1]; labelToken.map = [ workingStartLint + 1, workingStartLint + 2 ]; labelToken.children = []; // Add closing token to state const labelCloseToken = state.push('callout_label_close', 'p', -1); labelCloseToken.block = true; labelCloseToken.markup = ']'; // Change the start line workingStartLint += 2; } // Add content to state state.md.block.tokenize(state, workingStartLint, state.line); // Add closing token to state const tokenClose = state.push('callout_close', 'div', -1); tokenClose.block = true; tokenClose.markup = '<$>'; // Reset parent type state.parentType = oldParentType; // Done return true; }; md.block.ruler.before('paragraph', 'callout', calloutRule); /** * Rendering rule for the start of a callout. * * @type {import('markdown-it/lib/renderer').RenderRule} * @private */ md.renderer.rules.callout_open = (tokens, index) => { const token = tokens[index]; const classes = [ ...(optsObj.extraClasses || [ 'callout' ]), token.callout.className ].join(' '); return `<div class="${md.utils.escapeHtml(classes)}">\n`; }; /** * Rendering rule for the end of a callout. * * @type {import('markdown-it/lib/renderer').RenderRule} * @private */ md.renderer.rules.callout_close = () => '</div>\n'; /** * Rendering rule for the start of a callout label inside a callout. * * @type {import('markdown-it/lib/renderer').RenderRule} * @private */ md.renderer.rules.callout_label_open = () => `<p class="${md.utils.escapeHtml(optsObj.labelClass || 'callout-label')}">`; /** * Rendering rule for the end of a callout label inside a callout. * * @type {import('markdown-it/lib/renderer').RenderRule} * @private */ md.renderer.rules.callout_label_close = () => '</p>\n'; };