Skip to content

Commit

Permalink
Add Image comparison plugin (#56)
Browse files Browse the repository at this point in the history
* Add Image comparison plugin

* Fix eslint problems

* Changes based on review

* Updates based on feedback

* Remove '\' from output

* Update default value docs

---------

Co-authored-by: szabi <[email protected]>
  • Loading branch information
MSzabi and MSzabi authored Mar 27, 2023
1 parent 647d630 commit 8ef90dc
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 0 deletions.
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 Image Compare embeds
- (minor) Add Slideshow embeds
- (minor) Add Instagram embeds
- (minor) Add Vimeo embeds
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,36 @@ Set this property to `false` to disable this plugin.
_No options are available for this plugin._
</details>

### compare

<details>
<summary>Add support for Image Comparison in Markdown, as block syntax.</summary>

The basic syntax is `[compare <url1> <url2>]`. E.g., `[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]`.
Height and width can optionally be set using `[compare <url1> <url2> [height] [width]]`. E.g., `[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png 500 560]`.
The default value for height is 270 and for width is 480.

**Example Markdown input:**

[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]

**Example HTML output:**

<div class="image-compare" style="--value:50%; height: 270px; width: 480px;">
<img class="image-left" src="https://assets.digitalocean.com/banners/python.png" alt="Image left"/>
<img class="image-right" src="https://assets.digitalocean.com/banners/javascript.png" alt="Image right"/>
<input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', `${this.value}%`)" />
<svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
</div>

**Options:**

Pass options for this plugin as the `compare` 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
8 changes: 8 additions & 0 deletions fixtures/full-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,14 @@ You can also embed Slideshow (url1, url2, ...urls, height, width):

_Both the width and height are optional, with the defaults being 480 and 270 respectively._

### Image compare

Compare two images side by side (url1, url2, height, width):

[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]

_Both the width and height are optional, with the defaults being 480 and 270 respectively._

## Step 6 — Tutorials

Certain features of our Markdown engine are designed specifically for our tutorial content-types.
Expand Down
9 changes: 9 additions & 0 deletions fixtures/full-output.html
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,15 @@ <h3 id="slideshow"><a class="hash-anchor" href="#slideshow" aria-hidden="true"><
<div class="slides"><img src="https://assets.digitalocean.com/banners/python.png" alt="Slide #1" /><img src="https://assets.digitalocean.com/banners/javascript.png" alt="Slide #2" /><img src="https://assets.digitalocean.com/banners/nodejs.png" alt="Slide #3" /></div>
</div>
<p><em>Both the width and height are optional, with the defaults being 480 and 270 respectively.</em></p>
<h3 id="image-compare"><a class="hash-anchor" href="#image-compare" aria-hidden="true"></a>Image compare</h3>
<p>Compare two images side by side (url1, url2, height, width):</p>
<div class="image-compare" style="--value:50%; height: 270px; width: 480px;">
<img class="image-left" src="https://assets.digitalocean.com/banners/python.png" alt="Image left"/>
<img class="image-right" src="https://assets.digitalocean.com/banners/javascript.png" alt="Image right"/>
<input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', `${this.value}%`)" />
<svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
</div>
<p><em>Both the width and height are optional, with the defaults being 480 and 270 respectively.</em></p>
<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 Down
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const safeObject = require('./util/safe_object');
* @property {false} [twitter] Disable Twitter embeds.
* @property {false} [instagram] Disable Instagram embeds.
* @property {false} [slideshow] Disable Slideshow embeds.
* @property {false} [compare] Disable Image Compare 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 @@ -159,6 +160,10 @@ module.exports = (md, options) => {
md.use(require('./rules/embeds/slideshow'), safeObject(optsObj.slideshow));
}

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

// Register modifiers

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

/**
* Add support for Image Comparison in Markdown, as block syntax.
*
* The basic syntax is `[compare <url1> <url2>]`. E.g., `[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]`.
* Height and width can optionally be set using `[compare <url1> <url2> [height] [width]]`. E.g., `[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png 500 560]`.
* The default value for height is 270 and for width is 480.
*
* @example
* [compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]
*
* <div class="image-compare" style="--value:50%; height: 270px; width: 480px;">
* <img class="image-left" src="https://assets.digitalocean.com/banners/python.png" alt="Image left"/>
* <img class="image-right" src="https://assets.digitalocean.com/banners/javascript.png" alt="Image right"/>
* <input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', `${this.value}%`)" />
* <svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
* </div>
*
* @type {import('markdown-it').PluginSimple}
*/
module.exports = md => {
/**
* Parsing rule for Image compare markup.
*
* @type {import('markdown-it/lib/parser_block').RuleBlock}
* @private
*/
const compareRule = (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; // [compare a b]
if (currentLine.slice(0, 9) !== '[compare ') return false;
if (currentLine[currentLine.length - 1] !== ']') return false;

// Check for compare match
const match = currentLine.match(/^\[compare (\S+) (\S+)(?: (\d+))?(?: (\d+))?]$/);
if (!match) return false;

// Get the first image
const imageLeft = match[1];
if (!imageLeft) return false;

// Get the second image
const imageRight = match[2];
if (!imageRight) return false;

// Get the height
const height = Number(match[3]) || 270;

// Get the width
const width = Number(match[4]) || 480;

// Update the pos for the parser
state.line = startLine + 1;

// Add token to state
const token = state.push('compare', 'compare', 0);
token.block = true;
token.markup = match[0];
token.compare = { imageLeft, imageRight, height, width };

// Done
return true;
};

md.block.ruler.before('paragraph', 'compare', compareRule);

/**
* Rendering rule for compare markup.
*
* @type {import('markdown-it/lib/renderer').RenderRule}
* @private
*/
md.renderer.rules.compare = (tokens, index) => {
const token = tokens[index];

// Return the HTML
return `<div class="image-compare" style="--value:50%; height: ${md.utils.escapeHtml(token.compare.height)}px; width: ${md.utils.escapeHtml(token.compare.width)}px;">
<img class="image-left" src="${md.utils.escapeHtml(token.compare.imageLeft)}" alt="Image left"/>
<img class="image-right" src="${md.utils.escapeHtml(token.compare.imageRight)}" alt="Image right"/>
<input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', \`\${this.value}%\`)" />
<svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
</div>\n`;
};
};
89 changes: 89 additions & 0 deletions rules/embeds/compare.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
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';

const md = require('markdown-it')().use(require('./compare'));

it('handles image compare embeds (not inline)', () => {
expect(md.render('[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png 400 400]')).toBe(`<div class="image-compare" style="--value:50%; height: 400px; width: 400px;">
<img class="image-left" src="https://assets.digitalocean.com/banners/python.png" alt="Image left"/>
<img class="image-right" src="https://assets.digitalocean.com/banners/javascript.png" alt="Image right"/>
<input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', \`\${this.value}%\`)" />
<svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
</div>
`);
});

it('handles image compare embeds with no urls (no embed)', () => {
expect(md.render('[compare ]')).toBe(`<p>[compare ]</p>
`);
});

it('handles image compare embeds with one url (no embed)', () => {
expect(md.render('[compare https://assets.digitalocean.com/banners/python.png]')).toBe(`<p>[compare https://assets.digitalocean.com/banners/python.png]</p>
`);
});

it('handles image compare embeds that are unclosed (no embed)', () => {
expect(md.render('[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png')).toBe(`<p>[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png</p>
`);
});

it('handles image compare embeds without width', () => {
expect(md.render('[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png 400]')).toBe(`<div class="image-compare" style="--value:50%; height: 400px; width: 480px;">
<img class="image-left" src="https://assets.digitalocean.com/banners/python.png" alt="Image left"/>
<img class="image-right" src="https://assets.digitalocean.com/banners/javascript.png" alt="Image right"/>
<input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', \`\${this.value}%\`)" />
<svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
</div>
`);
});

it('handles image compare embeds without width or height', () => {
expect(md.render('[compare https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]')).toBe(`<div class="image-compare" style="--value:50%; height: 270px; width: 480px;">
<img class="image-left" src="https://assets.digitalocean.com/banners/python.png" alt="Image left"/>
<img class="image-right" src="https://assets.digitalocean.com/banners/javascript.png" alt="Image right"/>
<input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', \`\${this.value}%\`)" />
<svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
</div>
`);
});

it('handles image compare embeds attempting html injection', () => {
expect(md.render('[compare <script>alert();</script>]')).toBe(`<p>[compare &lt;script&gt;alert();&lt;/script&gt;]</p>
`);
});

it('handles image compare embeds attempting js injection', () => {
expect(md.render('[compare " onload="alert();]')).toBe(`<div class="image-compare" style="--value:50%; height: 270px; width: 480px;">
<img class="image-left" src="&quot;" alt="Image left"/>
<img class="image-right" src="onload=&quot;alert();" alt="Image right"/>
<input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', \`\${this.value}%\`)" />
<svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
</div>
`);
});

it('handles compare embeds attempting url manipulation', () => {
expect(md.render('[compare a/../../b a/../../b 280 560]')).toBe(`<div class="image-compare" style="--value:50%; height: 280px; width: 560px;">
<img class="image-left" src="a/../../b" alt="Image left"/>
<img class="image-right" src="a/../../b" alt="Image right"/>
<input type="range" class="control" min="0" max="100" value="50" oninput="this.parentNode.style.setProperty('--value', \`\${this.value}%\`)" />
<svg class="control-arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504.3 273.6c4.9-4.5 7.7-10.9 7.7-17.6s-2.8-13-7.7-17.6l-112-104c-7-6.5-17.2-8.2-25.9-4.4s-14.4 12.5-14.4 22l0 56-192 0 0-56c0-9.5-5.7-18.2-14.4-22s-18.9-2.1-25.9 4.4l-112 104C2.8 243 0 249.3 0 256s2.8 13 7.7 17.6l112 104c7 6.5 17.2 8.2 25.9 4.4s14.4-12.5 14.4-22l0-56 192 0 0 56c0 9.5 5.7 18.2 14.4 22s18.9 2.1 25.9-4.4l112-104z"/></svg>
</div>
`);
});
Loading

0 comments on commit 8ef90dc

Please sign in to comment.