Skip to content

Commit

Permalink
Add slideshow plugin (#57)
Browse files Browse the repository at this point in the history
* Add slideshow plugin

* Fix css code style issues

* Updates based on review

* Add tests for random size positions

* Add more size position tests

---------

Co-authored-by: szabi <[email protected]>
  • Loading branch information
MSzabi and MSzabi authored Mar 27, 2023
1 parent 30c6c76 commit 647d630
Show file tree
Hide file tree
Showing 9 changed files with 380 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 Slideshow embeds
- (minor) Add Instagram embeds
- (minor) Add Vimeo embeds

Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,35 @@ Set this property to `false` to disable this plugin.
_No options are available for this plugin._
</details>

### slideshow

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

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

**Example Markdown input:**

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

**Example HTML output:**

<div class="slideshow" style="height: 270px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<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>

**Options:**

Pass options for this plugin as the `slideshow` 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
7 changes: 7 additions & 0 deletions fixtures/full-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,13 @@ Like a few other embeds, you can also pass optional flags to customize the embed
- Add `left`, `center`, or `right` to set the alignment of the embed (default is `left`).
- Pass `caption` to include caption under the post (e.g. `[instagram https://www.instagram.com/p/CkQuv3_LRgS caption]`)

### Slideshow

You can also embed Slideshow (url1, url2, ...urls, height, width):

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

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

## Step 6 — Tutorials

Expand Down
8 changes: 8 additions & 0 deletions fixtures/full-output.html
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ <h3 id="instagram"><a class="hash-anchor" href="#instagram" aria-hidden="true"><
<li>Add <code>left</code>, <code>center</code>, or <code>right</code> to set the alignment of the embed (default is <code>left</code>).</li>
<li>Pass <code>caption</code> to include caption under the post (e.g. <code>[instagram https://www.instagram.com/p/CkQuv3_LRgS caption]</code>)</li>
</ul>
<h3 id="slideshow"><a class="hash-anchor" href="#slideshow" aria-hidden="true"></a>Slideshow</h3>
<p>You can also embed Slideshow (url1, url2, …urls, height, width):</p>
<div class="slideshow" style="height: 270px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<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>
<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 @@ -45,6 +45,7 @@ const safeObject = require('./util/safe_object');
* @property {false} [vimeo] Disable Vimeo embeds.
* @property {false} [twitter] Disable Twitter embeds.
* @property {false} [instagram] Disable Instagram embeds.
* @property {false} [slideshow] Disable Slideshow 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 @@ -154,6 +155,10 @@ module.exports = (md, options) => {
md.use(require('./rules/embeds/instagram'), safeObject(optsObj.instagram));
}

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

// Register modifiers

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

/**
* Add support for Slideshow in Markdown, as block syntax.
*
* The basic syntax is `[slideshow <url1> <url2> <...urls>]`. E.g., `[slideshow https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png https://assets.digitalocean.com/banners/nodejs.png]`.
* Height and width can optionally be set using `[slideshow <url1> <url2> <...urls> [height] [width]]`. E.g., `[slideshow https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png https://assets.digitalocean.com/banners/nodejs.png 380 560]`.
* The default value for height is 270 and for width is 480.
*
* @example
*
* [slideshow https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png https://assets.digitalocean.com/banners/nodejs.png]
*
* <div class="slideshow" style="height: 270px; width: 480px;">
* <div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
* <div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
* <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>
*
* @type {import('markdown-it').PluginSimple}
*/
module.exports = md => {
/**
* Parsing rule for Slideshow markup.
*
* @type {import('markdown-it/lib/parser_block').RuleBlock}
* @private
*/
const slideshowRule = (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; // [slideshow a]
if (currentLine.slice(0, 11) !== '[slideshow ') return false;
if (currentLine[currentLine.length - 1] !== ']') return false;

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

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

// Get the integers for dimensions
const numbers = options.split(' ').filter(o => o && Number.isInteger(parseInt(o, 10)));

// Get the height
const height = Number(numbers[0]) || 270;

// Get the width
const width = Number(numbers[1]) || 480;

// Get everything that is not a simple integer
const images = options.split(' ').filter(o => o && !Number.isInteger(parseInt(o, 10)));
if (!images.length) {
return false;
}

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

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

// Done
return true;
};

md.block.ruler.before('paragraph', 'slideshow', slideshowRule);

/**
* Rendering rule for slideshow markup.
*
* @type {import('markdown-it/lib/renderer').RenderRule}
* @private
*/
md.renderer.rules.slideshow = (tokens, index) => {
const token = tokens[index];
const slides = token.slideshow.images.map((image, idx) => `<img src="${md.utils.escapeHtml(image)}" alt="Slide #${idx + 1}" />`);
// Return the HTML
return `<div class="slideshow" style="height: ${md.utils.escapeHtml(token.slideshow.height)}px; width: ${md.utils.escapeHtml(token.slideshow.width)}px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= ${token.slideshow.width})()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += ${token.slideshow.width})()">&#8250;</div>
<div class="slides">${slides.join('')}</div>
</div>\n`;
};
};
137 changes: 137 additions & 0 deletions rules/embeds/slideshow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
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('./slideshow'));

it('handles slideshow embeds (not inline)', () => {
expect(md.render('[slideshow https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png 400 400]')).toBe(`<div class="slideshow" style="height: 400px; width: 400px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 400)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 400)()">&#8250;</div>
<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" /></div>
</div>
`);
});

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

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

it('handles slideshow embeds with one url', () => {
expect(md.render('[slideshow https://assets.digitalocean.com/banners/python.png]')).toBe(`<div class="slideshow" style="height: 270px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<div class="slides"><img src="https://assets.digitalocean.com/banners/python.png" alt="Slide #1" /></div>
</div>
`);
});

it('handles slideshow embeds without width or height', () => {
expect(md.render('[slideshow https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]')).toBe(`<div class="slideshow" style="height: 270px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<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" /></div>
</div>
`);
});

it('handles slideshow embeds without width', () => {
expect(md.render('[slideshow https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png 400]')).toBe(`<div class="slideshow" style="height: 400px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<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" /></div>
</div>
`);
});

it('handles slideshow embeds with height between urls', () => {
expect(md.render('[slideshow https://assets.digitalocean.com/banners/python.png 400 https://assets.digitalocean.com/banners/javascript.png]')).toBe(`<div class="slideshow" style="height: 400px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<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" /></div>
</div>
`);
});

it('handles slideshow embeds with height before urls', () => {
expect(md.render('[slideshow 400 https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]')).toBe(`<div class="slideshow" style="height: 400px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<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" /></div>
</div>
`);
});

it('handles slideshow embeds with height and width between urls', () => {
expect(md.render('[slideshow https://assets.digitalocean.com/banners/python.png 400 500 https://assets.digitalocean.com/banners/javascript.png]')).toBe(`<div class="slideshow" style="height: 400px; width: 500px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 500)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 500)()">&#8250;</div>
<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" /></div>
</div>
`);
});

it('handles slideshow embeds with height and width before urls', () => {
expect(md.render('[slideshow 400 500 https://assets.digitalocean.com/banners/python.png https://assets.digitalocean.com/banners/javascript.png]')).toBe(`<div class="slideshow" style="height: 400px; width: 500px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 500)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 500)()">&#8250;</div>
<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" /></div>
</div>
`);
});

it('handles slideshow embeds with height and width in random places', () => {
expect(md.render('[slideshow 400 https://assets.digitalocean.com/banners/python.png 500 https://assets.digitalocean.com/banners/javascript.png]')).toBe(`<div class="slideshow" style="height: 400px; width: 500px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 500)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 500)()">&#8250;</div>
<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" /></div>
</div>
`);
});

it('handles slideshow embeds attempting html injection', () => {
expect(md.render('[slideshow <script>alert();</script>]')).toBe(`<div class="slideshow" style="height: 270px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<div class="slides"><img src="&lt;script&gt;alert();&lt;/script&gt;" alt="Slide #1" /></div>
</div>
`);
});

it('handles slideshow embeds attempting js injection', () => {
expect(md.render('[slideshow " onload="alert();]')).toBe(`<div class="slideshow" style="height: 270px; width: 480px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 480)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 480)()">&#8250;</div>
<div class="slides"><img src="&quot;" alt="Slide #1" /><img src="onload=&quot;alert();" alt="Slide #2" /></div>
</div>
`);
});

it('handles slideshow embeds attempting url manipulation', () => {
expect(md.render('[slideshow a/../../b a/../../b 280 560]')).toBe(`<div class="slideshow" style="height: 280px; width: 560px;">
<div class="action left" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft -= 560)()">&#8249;</div>
<div class="action right" onclick="(() => this.parentNode.getElementsByClassName('slides')[0].scrollLeft += 560)()">&#8250;</div>
<div class="slides"><img src="a/../../b" alt="Slide #1" /><img src="a/../../b" alt="Slide #2" /></div>
</div>
`);
});
Loading

0 comments on commit 647d630

Please sign in to comment.