@digitalocean/do-markdownit
Version:
Markdown-It plugin for the DigitalOcean Community.
189 lines (156 loc) • 8.35 kB
JavaScript
/*
Copyright 2023 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.
*/
;
/**
* @module rules/embeds/codepen
*/
const safeObject = require('../../util/safe_object');
/**
* Add support for [CodePen](https://codepen.io/) embeds in Markdown, as block syntax.
*
* The basic syntax is `[codepen <user> <hash>]`. E.g. `[codepen AlbertFeynman gjpgjN]`.
* After the user and hash, assorted space-separated flags can be added (in any combination/order):
*
* - Add `lazy` to set the CodePen embed to not run until the user interacts with it.
* - Add `light` or `dark` to set the CodePen embed theme (default is `light`).
* - Add `html`, `css`, or `js` to set the default tab for the CodePen embed.
* - Add `result` to set the CodePen embed to default to the Result tab (default, can be combined with other tabs).
* - Add `editable` to set the CodePen embed to allow the code to be edited (requires the embedded user to be Pro).
* - Add any set of digits to set the height of the embed (in pixels).
*
* If two or more tabs are selected (excluding `result`), `html` will be preferred, followed by `css`, then `js`.
* If the `result` tab is selected, it can be combined with any other tab to generate a split view.
*
* If both `light` and `dark` are selected, `dark` will be preferred.
*
* @example
* [codepen AlbertFeynman gjpgjN]
*
* [codepen AlbertFeynman gjpgjN lazy dark 512 html]
*
* <p class="codepen" data-height="256" data-theme-id="light" data-default-tab="result" data-user="AlbertFeynman" data-slug-hash="gjpgjN" style="height: 256px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
* <span>See the Pen <a href="https://codepen.io/AlbertFeynman/pen/gjpgjN">gjpgjN by AlbertFeynman</a> (<a href="https://codepen.io/AlbertFeynman">@AlbertFeynman</a>) on <a href='https://codepen.io'>CodePen</a>.</span>
* </p>
*
* <p class="codepen" data-height="512" data-theme-id="dark" data-default-tab="html" data-user="AlbertFeynman" data-slug-hash="gjpgjN" data-preview="true" style="height: 512px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
* <span>See the Pen <a href="https://codepen.io/AlbertFeynman/pen/gjpgjN">gjpgjN by AlbertFeynman</a> (<a href="https://codepen.io/AlbertFeynman">@AlbertFeynman</a>) on <a href='https://codepen.io'>CodePen</a>.</span>
* </p>
* <script async defer src="https://static.codepen.io/assets/embed/ei.js" type="text/javascript"></script>
*
* @type {import('markdown-it').PluginSimple}
*/
module.exports = md => {
/**
* Parsing rule for CodePen markup.
*
* @type {import('markdown-it/lib/parser_block').RuleBlock}
* @private
*/
const codepenRule = (state, startLine, endLine, silent) => {
// If silent, don't replace
if (silent) return false;
// Get current string to consider (just current line)
const pos = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
const currentLine = state.src.substring(pos, max);
// Perform some non-regex checks for speed
if (currentLine.length < 13) return false; // [codepen a b]
if (currentLine.slice(0, 9) !== '[codepen ') return false;
if (currentLine[currentLine.length - 1] !== ']') return false;
// Check for codepen match
const tabs = [ 'html', 'css', 'js', 'result' ];
const settings = [ 'lazy', 'light', 'dark', 'editable' ];
const match = currentLine.match(`^\\[codepen (\\S+) (\\S+)((?: (?:${tabs.concat(settings).join('|')}|\\d+))*)\\]$`);
if (!match) return false;
// Get the user
const user = match[1];
if (!user) return false;
// Get the hash
const hash = match[2];
if (!hash) return false;
// Get the raw flags
const flags = match[3];
// Get the height
const heightMatch = flags.match(/\d+/);
const height = heightMatch ? Number(heightMatch[0]) : 256;
// Defines the theme
const theme = flags.includes('dark') ? 'dark' : 'light';
// Defines if the embed should lazy load
const lazy = flags.includes('lazy');
// Defines if the embed should be editable
const editable = flags.includes('editable');
// Defines the default tab (preferring based on the order in the tabs array)
let tab = tabs.find(t => flags.includes(t)) || 'result';
// If the result tab was requested, it can be combined with any other tab
if (flags.includes('result') && tab !== 'result') tab = `${tab},result`;
// Update the pos for the parser
state.line = startLine + 1;
// Add token to state
const token = state.push('codepen', 'codepen', 0);
token.block = true;
token.markup = match[0];
token.codepen = { user, hash, height, theme, lazy, editable, tab };
// Track that we need the script
state.env.codepen = safeObject(state.env.codepen);
state.env.codepen.tokenized = true;
// Done
return true;
};
md.block.ruler.before('paragraph', 'codepen', codepenRule);
/**
* Parsing rule to inject the CodePen script.
*
* @type {import('markdown-it').RuleCore}
* @private
*/
const codepenScriptRule = state => {
if (state.inlineMode) return;
// Check if we need to inject the script
if (state.env.codepen && state.env.codepen.tokenized && !state.env.codepen.injected) {
// Set that we've injected it
state.env.codepen.injected = true;
// Inject the token
const token = new state.Token('html_block', '', 0);
token.content = '<script async defer src="https://static.codepen.io/assets/embed/ei.js" type="text/javascript"></script>\n';
state.tokens.push(token);
}
};
md.core.ruler.push('codepen_script', codepenScriptRule);
/**
* Rendering rule for CodePen markup.
*
* @type {import('markdown-it/lib/renderer').RenderRule}
* @private
*/
md.renderer.rules.codepen = (tokens, index) => {
const token = tokens[index];
// Construct the attrs
const attrHeight = ` data-height="${md.utils.escapeHtml(token.codepen.height)}"`;
const attrTheme = ` data-theme-id="${md.utils.escapeHtml(token.codepen.theme)}"`;
const attrTab = ` data-default-tab="${md.utils.escapeHtml(token.codepen.tab)}"`;
const attrUser = ` data-user="${md.utils.escapeHtml(token.codepen.user)}"`;
const attrHash = ` data-slug-hash="${md.utils.escapeHtml(token.codepen.hash)}"`;
const attrLazy = token.codepen.lazy ? ' data-preview="true"' : '';
const attrEditable = token.codepen.editable ? ' data-editable="true"' : '';
// Escape some HTML
const user = md.utils.escapeHtml(token.codepen.user);
const userUrl = encodeURIComponent(token.codepen.user);
const hash = md.utils.escapeHtml(token.codepen.hash);
const hashUrl = encodeURIComponent(token.codepen.hash);
const height = md.utils.escapeHtml(token.codepen.height);
// Return the HTML
return `<p class="codepen"${attrHeight}${attrTheme}${attrTab}${attrUser}${attrHash}${attrLazy}${attrEditable} style="height: ${height}px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/${userUrl}/pen/${hashUrl}">${hash} by ${user}</a> (<a href="https://codepen.io/${userUrl}">@${user}</a>) on <a href='https://codepen.io'>CodePen</a>.</span>
</p>\n`;
};
};