@digitalocean/do-markdownit
Version:
Markdown-It plugin for the DigitalOcean Community.
182 lines (152 loc) • 7.09 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/twitter
*/
const safeObject = require('../../util/safe_object');
/**
* Add support for [Twitter](https://twitter.com/) embeds in Markdown, as block syntax.
* Input may use either the legacy `twitter.com` or the new `x.com` domain; both are accepted.
*
* The basic syntax is `[twitter <tweet>]`.
* E.g. `[twitter https://twitter.com/MattIPv4/status/1576415168426573825]`.
* Rendered output always canonicalizes the link to `https://twitter.com/<user>/status/<id>`.
* After the tweet, assorted space-separated flags can be added (in any combination/order):
*
* - Add `light` or `dark` to set the card theme (default is `light`).
* - Add `left`, `center`, or `right` to set the alignment of the embed (default is `left`).
* - Add any set of digits to set the width of the embed (in pixels, between 250 and 550, default is 550).
*
* If two or more alignments are selected, `left` will be preferred, followed by `center`, then `right`.
*
* If both `light` and `dark` are selected, `dark` will be preferred.
*
* If a width outside the range of 250-550 is selected, a clamped value will be used.
*
* @example
* [twitter https://twitter.com/MattIPv4/status/1576415168426573825] Or [twitter https://x.com/MattIPv4/status/1576415168426573825]
*
* [twitter https://twitter.com/MattIPv4/status/1576415168426573825 left 400 dark] Or [twitter https://x.com/MattIPv4/status/1576415168426573825 left 400 dark]
*
* <div class="twitter">
* <blockquote class="twitter-tweet" data-dnt="true" data-width="550" data-theme="light">
* <a href="https://twitter.com/MattIPv4/status/1576415168426573825">View tweet by @MattIPv4</a>
* </blockquote>
* </div>
*
* <div class="twitter" align="left">
* <blockquote class="twitter-tweet" data-dnt="true" data-width="400" data-theme="dark">
* <a href="https://twitter.com/MattIPv4/status/1576415168426573825">View tweet by @MattIPv4</a>
* </blockquote>
* </div>
* <script async defer src="https://platform.twitter.com/widgets.js" type="text/javascript"></script>
*
* @type {import('markdown-it').PluginSimple}
*/
module.exports = md => {
/**
* Parsing rule for Twitter markup.
*
* @type {import('markdown-it/lib/parser_block').RuleBlock}
* @private
*/
const twitterRule = (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 < 11) return false; // [twitter a/status/b]
if (currentLine.slice(0, 9) !== '[twitter ') return false;
if (currentLine[currentLine.length - 1] !== ']') return false;
// Check for Twitter match (accept both twitter.com and x.com domains)
// https://www.twitter.com/<user>/status/<id> or https://www.x.com/<user>/status/<id> (treat everything prior to <user> as optional, ish)
const alignment = [ 'left', 'center', 'right' ];
const settings = [ 'light', 'dark' ];
const match = currentLine.match(`^\\[twitter (?:(?:(?:(?:https?:)?\\/\\/)?(?:www\\.)?(?:twitter|x)\\.com)?\\/)?(\\w+)\\/status\\/(\\d+)((?: (?:${alignment.concat(settings).join('|')}|\\d+))*)\\]$`);
if (!match) return false;
// Get the user
const user = match[1];
if (!user) return false;
// Get the id
const id = match[2];
if (!id) return false;
// Get the raw flags
const flags = match[3];
// Get the width
const widthMatch = flags.match(/\d+/);
const width = widthMatch ? Math.max(Math.min(Number(widthMatch[0]), 550), 250) : 550;
// Get the theme
const theme = flags.includes('dark') ? 'dark' : 'light';
// Get the alignment
const align = alignment.find(t => flags.includes(t)) || 'center';
// Update the pos for the parser
state.line = startLine + 1;
// Add token to state
const token = state.push('twitter', 'twitter', 0);
token.block = true;
token.markup = match[0];
token.twitter = { user, id, width, theme, align };
// Track that we need the script
state.env.twitter = safeObject(state.env.twitter);
state.env.twitter.tokenized = true;
// Done
return true;
};
md.block.ruler.before('paragraph', 'twitter', twitterRule);
/**
* Parsing rule to inject the Twitter script.
*
* @type {import('markdown-it').RuleCore}
* @private
*/
const twitterScriptRule = state => {
if (state.inlineMode) return;
// Check if we need to inject the script
if (state.env.twitter && state.env.twitter.tokenized && !state.env.twitter.injected) {
// Set that we've injected it
state.env.twitter.injected = true;
// Inject the token
const token = new state.Token('html_block', '', 0);
token.content = '<script async defer src="https://platform.twitter.com/widgets.js" type="text/javascript"></script>\n';
state.tokens.push(token);
}
};
md.core.ruler.push('twitter_script', twitterScriptRule);
/**
* Rendering rule for Twitter markup.
*
* @type {import('markdown-it/lib/renderer').RenderRule}
* @private
*/
md.renderer.rules.twitter = (tokens, index) => {
const token = tokens[index];
// Construct the attrs
const attrWidth = ` data-width="${md.utils.escapeHtml(token.twitter.width)}"`;
const attrTheme = ` data-theme="${md.utils.escapeHtml(token.twitter.theme)}"`;
const attrAlign = token.twitter.align !== 'center' ? ` align="${md.utils.escapeHtml(token.twitter.align)}"` : '';
// Escape some HTML
const user = md.utils.escapeHtml(token.twitter.user);
const id = md.utils.escapeHtml(token.twitter.id);
// Return the HTML
// Apply the alignment to the parent div, as Twitter does float-based alignment
return `<div class="twitter"${attrAlign}>
<blockquote class="twitter-tweet" data-dnt="true"${attrWidth}${attrTheme}>
<a href="https://twitter.com/${user}/status/${id}">View tweet by @${user}</a>
</blockquote>
</div>\n`;
};
};