Skip to content

Commit

Permalink
Add Twitter embeds (#47)
Browse files Browse the repository at this point in the history
* Add Twitter plugin

* Add to changelog + readme + demo

* Add more tests for URL variants
  • Loading branch information
MattIPv4 authored Feb 16, 2023
1 parent bf724fb commit c7be411
Show file tree
Hide file tree
Showing 9 changed files with 545 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Any non-code changes should be prefixed with `(docs)`.
See `PUBLISH.md` for instructions on how to publish a new version.
-->

- (minor) Add Twitter embeds
- (minor) Add result tab flag to CodePen embeds
- (docs) Update CHANGELOG notes, add keywords, ignore Jest config for NPM
- (minor) Add hash links to heading_id plugin
Expand Down
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,53 @@ Set this property to `false` to disable this plugin.
_No options are available for this plugin._
</details>

### twitter

<details>
<summary>Add support for [Twitter](https://twitter.com/) embeds in Markdown, as block syntax.</summary>

The basic syntax is `[twitter <tweet>]`. E.g. `[twitter https://twitter.com/MattIPv4/status/1576415168426573825]`.
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 Markdown input:**

[twitter https://twitter.com/MattIPv4/status/1576415168426573825]

[twitter https://twitter.com/MattIPv4/status/1576415168426573825 left 400 dark]

**Example HTML output:**

<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>

**Options:**

Pass options for this plugin as the `twitter` property of the `do-markdownit` plugin options.
Set this property to `false` to disable this plugin.

_No options are available for this plugin._
</details>

### underline

<details>
Expand Down Expand Up @@ -1138,4 +1185,4 @@ behave in the pull request, through example Markdown syntax and what the resulta

This plugin is licensed under the [Apache License 2.0](LICENSE).

Copyright 2022 DigitalOcean.
Copyright 2023 DigitalOcean.
12 changes: 12 additions & 0 deletions fixtures/full-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,18 @@ Embedding a terminal recording from Asciinema (id, cols, rows):

[asciinema 239367 50 20]

### Twitter

You can also embed a tweet from Twitter by passing the URL for the tweet:

[twitter https://twitter.com/MattIPv4/status/1576415168426573825]

Like a few other embeds, you can also pass optional flags to customize the embed:

- Pass any integer value (between 250 and 550) to set a custom width for the embed (e.g. `[twitter https://twitter.com/MattIPv4/status/1576415168426573825 400]`)
- Pass `light` or `dark` to switch the theme of the embed (e.g. `[twitter https://twitter.com/MattIPv4/status/1576415168426573825 dark]`)
- Pass `left`, `center`, or `right` to align the embed (e.g. `[twitter https://twitter.com/MattIPv4/status/1576415168426573825 left]`)


## Step 6 — Tutorials

Expand Down
14 changes: 14 additions & 0 deletions fixtures/full-output.html
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,19 @@ <h3 id="asciinema"><a class="hash-anchor" href="#asciinema" aria-hidden="true"><
<noscript>
<a href="https://asciinema.org/a/239367" target="_blank">View asciinema recording</a>
</noscript>
<h3 id="twitter"><a class="hash-anchor" href="#twitter" aria-hidden="true"></a>Twitter</h3>
<p>You can also embed a tweet from Twitter by passing the URL for the tweet:</p>
<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>
<p>Like a few other embeds, you can also pass optional flags to customize the embed:</p>
<ul>
<li>Pass any integer value (between 250 and 550) to set a custom width for the embed (e.g. <code>[twitter https://twitter.com/MattIPv4/status/1576415168426573825 400]</code>)</li>
<li>Pass <code>light</code> or <code>dark</code> to switch the theme of the embed (e.g. <code>[twitter https://twitter.com/MattIPv4/status/1576415168426573825 dark]</code>)</li>
<li>Pass <code>left</code>, <code>center</code>, or <code>right</code> to align the embed (e.g. <code>[twitter https://twitter.com/MattIPv4/status/1576415168426573825 left]</code>)</li>
</ul>
<h2 id="step-6-tutorials"><a class="hash-anchor" href="#step-6-tutorials" aria-hidden="true"></a>Step 6 — Tutorials</h2>
<p>Certain features of our Markdown engine are designed specifically for our tutorial content-types.
These may not be enabled in all contexts in the DigitalOcean community, but are enabled by default in the do-markdownit plugin.</p>
Expand All @@ -336,3 +349,4 @@ <h2 id="conclusion"><a class="hash-anchor" href="#conclusion" aria-hidden="true"
<script async defer src="https://do-community.github.io/dns-tool-embed/bundle.js" type="text/javascript" onload="window.DNSToolEmbeds()"></script>
<script async defer src="https://static.codepen.io/assets/embed/ei.js" type="text/javascript"></script>
<script async defer src="https://cdn.jsdelivr.net/gh/ireade/[email protected]/public/caniuse-embed.min.js" type="text/javascript"></script>
<script async defer src="https://platform.twitter.com/widgets.js" type="text/javascript"></script>
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const safeObject = require('./util/safe_object');
* @property {false} [caniuse] Disable CanIUse embeds.
* @property {false} [youtube] Disable YouTube embeds.
* @property {false} [wistia] Disable Wistia embeds.
* @property {false} [twitter] Disable Twitter embeds.
* @property {false} [underline] Disable underline syntax.
* @property {false|import('./modifiers/fence_label').FenceLabelOptions} [fence_label] Disable fence labels, or set options for the feature.
* @property {false|import('./modifiers/fence_secondary_label').FenceSecondaryLabelOptions} [fence_secondary_label] Disable fence secondary labels, or set options for the feature.
Expand Down Expand Up @@ -139,6 +140,10 @@ module.exports = (md, options) => {
md.use(require('./rules/embeds/wistia'), safeObject(optsObj.wistia));
}

if (optsObj.twitter !== false) {
md.use(require('./rules/embeds/twitter'), safeObject(optsObj.twitter));
}

// Register modifiers

if (optsObj.underline !== false) {
Expand Down
178 changes: 178 additions & 0 deletions rules/embeds/twitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
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.
*/

'use strict';

/**
* @module rules/embeds/twitter
*/

const safeObject = require('../../util/safe_object');

/**
* Add support for [Twitter](https://twitter.com/) embeds in Markdown, as block syntax.
*
* The basic syntax is `[twitter <tweet>]`. E.g. `[twitter https://twitter.com/MattIPv4/status/1576415168426573825]`.
* 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]
*
* [twitter https://twitter.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
// https://www.twitter.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\\.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`;
};
};
Loading

0 comments on commit c7be411

Please sign in to comment.