-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add syntax for defining settings on images (#43)
* Add modifier to support setting image sizes * Fix eslint issues in plugin * Add image sizing to main test * Add plugin to readme + changelog * Allow the units to be customised * Switch image_size to image_settings, add alignment support * Don't actually use float for left/right alignment * Tweak README wording for plugin * Wording tweaks
- Loading branch information
Showing
9 changed files
with
316 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/* | ||
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 modifiers/image_settings | ||
*/ | ||
|
||
const safeObject = require('../util/safe_object'); | ||
const regexEscape = require('../util/regex_escape'); | ||
|
||
/** | ||
* @typedef {Object} ImageSettingsOptions | ||
* @property {string[]} [sizeUnits=['', 'px', '%']] Image size units to allow. | ||
*/ | ||
|
||
/** | ||
* Add support for defining settings on images, such as size and alignment. | ||
* | ||
* The syntax for this is `{ width=<width> height=<height> align=<alignment> }`, at the end of the image markup. | ||
* E.g. `{ width=100 height=200 align=left }`. | ||
* All settings are optional, and the order does not matter. | ||
* | ||
* By default, the width and height can be plain number (`100`), pixels (`100px`), or percentage (`100%`). | ||
* Other units can be supported by passing an array of unit strings via the `sizeUnits` option. | ||
* | ||
* Alignment can be left unset, which will center the image, or can be set to either `left` or `right`. | ||
* | ||
* @example | ||
* { width=100 height=200 align=left } | ||
* | ||
* <p><img src="test.png" alt="alt" title="title" width="100" height="200" align="left"></p> | ||
* | ||
* @type {import('markdown-it').PluginWithOptions<ImageSettingsOptions>} | ||
*/ | ||
module.exports = (md, options) => { | ||
// Get the correct options | ||
const optsObj = safeObject(options); | ||
|
||
// Get the units to allow | ||
const units = Array.isArray(optsObj.sizeUnits) && optsObj.sizeUnits.length ? optsObj.sizeUnits : [ '', 'px', '%' ]; | ||
const nonEmptyUnits = units.filter(unit => unit !== ''); | ||
const unitPattern = `^\\d+${nonEmptyUnits.length ? `(?:${nonEmptyUnits.map(regexEscape).join('|')})${units.includes('') ? '?' : ''}` : ''}$`; | ||
|
||
/** | ||
* Wrap the link parsing rule to allow for parsing settings syntax after the image. | ||
* | ||
* @param {import('markdown-it/lib/parser_inline').RuleInline} original Original parse function (`image`). | ||
* @returns {import('markdown-it/lib/parser_inline').RuleInline} | ||
* @private | ||
*/ | ||
const imageWithSettings = original => (state, silent) => { | ||
// Run the original image rule | ||
const originalResult = original(state, silent); | ||
if (!originalResult) return originalResult; | ||
|
||
// If the image rule succeeded, start looking for our extra settings syntax | ||
// If at any point we fail, we just return the original result as to not break regular images | ||
|
||
// Check we have space for opening and closing tags | ||
if (state.pos + 2 > state.posMax) return originalResult; | ||
|
||
// Check we're on an opening marker | ||
if (state.src[state.pos] !== '{') return originalResult; | ||
|
||
// Look for closing marker | ||
const closeIdx = state.src.indexOf('}', state.pos + 1); | ||
if (closeIdx === -1 || closeIdx > state.posMax - 1) return originalResult; | ||
|
||
// Get the settings string | ||
const parts = state.src.slice(state.pos + 1, closeIdx).trim().split(/\s+/); | ||
const settings = {}; | ||
for (const part of parts) { | ||
// Check the setting has a key and value | ||
const split = part.indexOf('='); | ||
if (split === -1 || split === 0 || split === part.length - 1) return originalResult; | ||
|
||
// Check this is a valid setting | ||
const key = part.slice(0, split); | ||
const value = part.slice(split + 1); | ||
switch (key) { | ||
// If we have a size, check it's using a permitted unit | ||
case 'width': | ||
case 'height': | ||
if (!value.match(unitPattern)) return originalResult; | ||
break; | ||
|
||
// If we have an alignment, check it's a valid value | ||
case 'align': | ||
if (![ 'left', 'right' ].includes(value)) return originalResult; | ||
break; | ||
|
||
// We're not expecting any other settings | ||
default: | ||
return originalResult; | ||
} | ||
|
||
// Store the setting | ||
settings[key] = value; | ||
} | ||
|
||
// Apply the settings to the token | ||
const imageToken = state.tokens[state.tokens.length - 1]; | ||
if (settings.width) imageToken.attrSet('width', settings.width); | ||
if (settings.height) imageToken.attrSet('height', settings.height); | ||
if (settings.align) imageToken.attrSet('align', settings.align); | ||
|
||
// Update the position | ||
state.pos = closeIdx + 1; | ||
|
||
// Return the original result | ||
return originalResult; | ||
}; | ||
|
||
// eslint-disable-next-line no-underscore-dangle | ||
md.inline.ruler.at('image', imageWithSettings(md.inline.ruler.__rules__.find(rule => rule.name === 'image').fn)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/* | ||
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('./image_settings')); | ||
|
||
it('handles an image with alt text and a title', () => { | ||
expect(md.renderInline('')).toBe('<img src="test.png" alt="alt" title="title">'); | ||
}); | ||
|
||
it('handles an image with a size (width + height)', () => { | ||
expect(md.renderInline('{ width=100 height=200 }')).toBe('<img src="test.png" alt="" width="100" height="200">'); | ||
}); | ||
|
||
it('handles an image with a size (width + height) with a unit', () => { | ||
expect(md.renderInline('{ width=100px height=200px }')).toBe('<img src="test.png" alt="" width="100px" height="200px">'); | ||
}); | ||
|
||
it('handles an image with a size (width only)', () => { | ||
expect(md.renderInline('{ width=100 }')).toBe('<img src="test.png" alt="" width="100">'); | ||
}); | ||
|
||
it('handles an image with a size (height only)', () => { | ||
expect(md.renderInline('{ height=200 }')).toBe('<img src="test.png" alt="" height="200">'); | ||
}); | ||
|
||
it('handles an image with alt text, a title and size', () => { | ||
expect(md.renderInline('{ width=100 height=200 }')).toBe('<img src="test.png" alt="alt" title="title" width="100" height="200">'); | ||
}); | ||
|
||
it('handles an image with an invalid size (no value)', () => { | ||
expect(md.renderInline('{ width= }')).toBe('<img src="test.png" alt="">{ width= }'); | ||
}); | ||
|
||
it('handles an image with an invalid size (bad unit)', () => { | ||
expect(md.renderInline('{ width=10em height=20em }')).toBe('<img src="test.png" alt="">{ width=10em height=20em }'); | ||
}); | ||
|
||
it('handles an image with an alignment', () => { | ||
expect(md.renderInline('{ align=left }')).toBe('<img src="test.png" alt="" align="left">'); | ||
}); | ||
|
||
it('handles an image with an invalid alignment', () => { | ||
expect(md.renderInline('{ align=top }')).toBe('<img src="test.png" alt="">{ align=top }'); | ||
}); | ||
|
||
it('handles an image with a size and alignment', () => { | ||
expect(md.renderInline('{ width=100 height=200 align=left }')).toBe('<img src="test.png" alt="" width="100" height="200" align="left">'); | ||
}); | ||
|
||
it('handles an image with a size and alignment in a different order', () => { | ||
expect(md.renderInline('{ height=200 align=left width=100 }')).toBe('<img src="test.png" alt="" width="100" height="200" align="left">'); | ||
}); | ||
|
||
it('handles an image with an invalid setting', () => { | ||
expect(md.renderInline('{ hello=world }')).toBe('<img src="test.png" alt="">{ hello=world }'); | ||
}); | ||
|
||
it('handles an image with a valid and invalid settings', () => { | ||
expect(md.renderInline('{ width=100 hello=world }')).toBe('<img src="test.png" alt="">{ width=100 hello=world }'); | ||
}); | ||
|
||
const mdNoUnit = require('markdown-it')().use(require('./image_settings'), { sizeUnits: [ '' ] }); | ||
|
||
it('handles an image with a size (width + height) with no unit, with no units allowed', () => { | ||
expect(mdNoUnit.renderInline('{ width=100 height=200 }')).toBe('<img src="test.png" alt="" width="100" height="200">'); | ||
}); | ||
|
||
it('handles an image with a size (width + height) with a unit, with no units allowed', () => { | ||
expect(mdNoUnit.renderInline('{ width=100px height=200px }')).toBe('<img src="test.png" alt="">{ width=100px height=200px }'); | ||
}); | ||
|
||
const mdCustomUnit = require('markdown-it')().use(require('./image_settings'), { sizeUnits: [ 'em' ] }); | ||
|
||
it('handles an image with a size (width + height) with no unit, with a custom unit allowed', () => { | ||
expect(mdCustomUnit.renderInline('{ width=100 height=200 }')).toBe('<img src="test.png" alt="">{ width=100 height=200 }'); | ||
}); | ||
|
||
it('handles an image with a size (width + height) with a custom unit, with a custom unit allowed', () => { | ||
expect(mdCustomUnit.renderInline('{ width=100em height=200em }')).toBe('<img src="test.png" alt="" width="100em" height="200em">'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
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'; | ||
|
||
/** | ||
* Escape a string for use in a RegExp. | ||
* | ||
* @param {string} string String to escape. | ||
* @returns {string} | ||
*/ | ||
module.exports = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string |