diff --git a/CHANGELOG.md b/CHANGELOG.md index 6232c77d..b0df9261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Added +- Add [shorthand for setting text color via image syntax](https://marpit.marp.app/image-syntax?id=shorthand-for-setting-colors) ([#159](https://github.com/marp-team/marpit/pull/159)) - Add [documentation of fragmented list](https://marpit.marp.app/fragmented-list) ([#152](https://github.com/marp-team/marpit/pull/152)) - Test with Node 12 (Erbium) ([#160](https://github.com/marp-team/marpit/pull/160)) diff --git a/docs/image-syntax.md b/docs/image-syntax.md index 5e4fec19..c15f697c 100644 --- a/docs/image-syntax.md +++ b/docs/image-syntax.md @@ -2,19 +2,22 @@ Marpit has extended Markdown image syntax `![](image.jpg)` to be helpful creating beautiful slides. -| Features | Inline image | Slide BG | Advanced BG | -| :--------------------------------: | :----------: | :------: | :---------: | -| [Resizing by keywords][resizing] | `auto` only | ✅ | ✅ | -| [Resizing by percentage][resizing] | ❌ | ✅ | ✅ | -| [Resizing by length][resizing] | ✅ | ✅ | ✅ | -| [Image filters][filters] | ✅ | ❌ | ✅ | -| [Background color][bgcolor] | - | ✅ | ✅ | -| [Multiple backgrounds][multiple] | - | ❌ | ✅ | -| [Split backgrounds][split] | - | ❌ | ✅ | +| Features | Inline image | [Slide BG][slide-bg] | [Advanced BG][advanced-bg] | +| :---------------------------------: | :----------------: | :------------------: | :------------------------: | +| [Resizing by keywords][resizing] | `auto` only | :heavy_check_mark: | :heavy_check_mark: | +| [Resizing by percentage][resizing] | :x: | :heavy_check_mark: | :heavy_check_mark: | +| [Resizing by length][resizing] | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| [Image filters][filters] | :heavy_check_mark: | :x: | :heavy_check_mark: | +| [Multiple backgrounds][multiple] | - | :x: | :heavy_check_mark: | +| [Split backgrounds][split] | - | :x: | :heavy_check_mark: | +| [Setting text color][textcolor] | :heavy_check_mark: | - | - | +| [Setting background color][bgcolor] | - | :heavy_check_mark: | :heavy_check_mark: | [resizing]: #resizing-image [filters]: #image-filters -[bgcolor]: #background-color +[textcolor]: #shorthand-for-setting-colors +[bgcolor]: #shorthand-for-setting-colors +[slide-bg]: #slide-backgrounds [advanced-bg]: #advanced-backgrounds [multiple]: #multiple-backgrounds [split]: #split-backgrounds @@ -97,29 +100,9 @@ You can resize the background image by keywords. The keyword value basically fol You also can continue to use [`width` (`w`) and `height` (`h`) option keywords][resizing] to specify size by length. -### Background color - -Through Markdown image syntax, Marpit allows the definition of the background [color value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) instead of the image URL. - -```markdown -![bg](#fff) - ---- - -![bg](rebeccapurple) - ---- - -![bg]() -``` - -It is same as defining [`` spot directive](/directives#backgrounds). - -?> In this example, `rgb` function is [formed by angle brackets](https://spec.commonmark.org/0.28/#example-470). Normally `![bg](rgb(255,128,0))` may have [no problems](https://spec.commonmark.org/0.28/#example-468). But it does not allow including spaces. - ## Advanced backgrounds -!> 📐 It will work only in experimental [inline SVG slide](/inline-svg). +!> :triangular_ruler: It will work only in experimental [inline SVG slide](/inline-svg). The advanced backgrounds support [multiple backgrounds][multiple], [split backgrounds][split], and [image filters for background][filters]. @@ -187,3 +170,36 @@ The space of a slide content will shrink to the left side. This feature is similar to [Deckset's Split Slides](https://docs.decksetapp.com/English.lproj/Media/01-background-images.html#split-slides). ?> Marpit uses a last defined keyword in a slide when `left` and `right` keyword is mixed in the same slide by using multiple backgrounds. + +## Shorthand for setting colors + +Through Markdown image syntax, Marpit allows the definition of [color value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) instead of the image URL. + + + +```markdown +# Hex color (White BG + Black text) + +![bg](#fff) +![](#000) + +--- + +# Named color (rebeccapurple BG + White text) + +![bg](rebeccapurple) +![](white) + +--- + +# RGB values (Orange BG + White text) + +![bg](rgb(255,128,0)) +![](rgb(255,255,255)) +``` + + + +It is same as defining [`color` and `backgroundColor` spot directive](/directives?id=local-directives-1). + +!> By the spec of CommonMark, it should not allow including spaces without escape if you want using color function like `rgb()`. diff --git a/src/markdown/background_image/apply.js b/src/markdown/background_image/apply.js index 18d42792..ad095f58 100644 --- a/src/markdown/background_image/apply.js +++ b/src/markdown/background_image/apply.js @@ -64,9 +64,9 @@ function backgroundImageApply(md) { const { background, backgroundDirection, - backgroundColor, backgroundSize, backgroundSplit, + color, filter, height, size, @@ -75,11 +75,11 @@ function backgroundImageApply(md) { } = t.meta.marpitImage if (background && !url.match(/^\s*$/)) { - if (backgroundColor) { + if (color) { // Background color current.open.meta.marpitDirectives = { ...(current.open.meta.marpitDirectives || {}), - backgroundColor, + backgroundColor: color, } } else { // Background image diff --git a/src/markdown/background_image/parse.js b/src/markdown/background_image/parse.js index 441b273c..d7c66e40 100644 --- a/src/markdown/background_image/parse.js +++ b/src/markdown/background_image/parse.js @@ -1,5 +1,4 @@ /** @module */ -import colorString from 'color-string' import marpitPlugin from '../marpit_plugin' const bgSizeKeywords = { @@ -33,13 +32,6 @@ function backgroundImageParse(md) { marpitImage.background = true t.hidden = true - // Background color - const isColor = - !!colorString.get(marpitImage.url) || - marpitImage.url.toLowerCase() === 'currentcolor' - - if (isColor) marpitImage.backgroundColor = marpitImage.url - for (const opt of marpitImage.options) { // Background size keyword if (bgSizeKeywords[opt]) diff --git a/src/markdown/image.js b/src/markdown/image.js new file mode 100644 index 00000000..40e3aeb0 --- /dev/null +++ b/src/markdown/image.js @@ -0,0 +1,17 @@ +/** @module */ +import marpitPlugin from './marpit_plugin' +import apply from './image/apply' +import parse from './image/parse' + +/** + * Marpit image plugin. + * + * @alias module:markdown/image + * @param {MarkdownIt} md markdown-it instance. + */ +function image(md) { + parse(md) + apply(md) +} + +export default marpitPlugin(image) diff --git a/src/markdown/image/apply.js b/src/markdown/image/apply.js new file mode 100644 index 00000000..66e7989d --- /dev/null +++ b/src/markdown/image/apply.js @@ -0,0 +1,73 @@ +/** @module */ +import marpitPlugin from '../marpit_plugin' +import InlineStyle from '../../helpers/inline_style' + +/** + * Marpit image apply plugin. + * + * Apply image style and color spot directive based on parsed meta. + * + * @alias module:markdown/image/apply + * @param {MarkdownIt} md markdown-it instance. + */ +function applyImage(md) { + // Build and apply image style + md.inline.ruler2.push('marpit_apply_image', ({ tokens }) => { + for (const token of tokens) { + if (token.type === 'image') { + const { filters, height, width } = token.meta.marpitImage + const style = new InlineStyle(token.attrGet('style')) + + if (width && !width.endsWith('%')) style.set('width', width) + if (height && !height.endsWith('%')) style.set('height', height) + + if (filters) { + const filterStyle = [] + + for (const fltrs of filters) + filterStyle.push(`${fltrs[0]}(${fltrs[1]})`) + + token.meta.marpitImage.filter = filterStyle.join(' ') + style.set('filter', token.meta.marpitImage.filter) + } + + const stringified = style.toString() + if (stringified) token.attrSet('style', stringified) + } + } + }) + + // Shorthand for color spot directive + md.core.ruler.after( + 'marpit_inline_svg', + 'marpit_apply_color', + ({ inlineMode, tokens }) => { + if (inlineMode) return + + let current + + for (const t of tokens) { + if (t.type === 'marpit_slide_open') current = t + if (t.type === 'marpit_slide_close') current = undefined + + // Collect parsed inline image meta + if (current && t.type === 'inline') { + for (const tc of t.children) { + if (tc.type === 'image') { + const { background, color } = tc.meta.marpitImage + + if (!background && color) { + current.meta.marpitDirectives = { + ...(current.meta.marpitDirectives || {}), + color, + } + } + } + } + } + } + } + ) +} + +export default marpitPlugin(applyImage) diff --git a/src/markdown/image/parse.js b/src/markdown/image/parse.js new file mode 100644 index 00000000..f7f5a2dc --- /dev/null +++ b/src/markdown/image/parse.js @@ -0,0 +1,131 @@ +/** @module */ +import colorString from 'color-string' +import marpitPlugin from '../marpit_plugin' + +const escape = target => + target.replace( + /[\\;:()]/g, + matched => `\\${matched[0].codePointAt(0).toString(16)} ` + ) + +const optionMatchers = new Map() + +// The scale percentage for resize background +optionMatchers.set(/^(\d*\.)?\d+%$/, matches => ({ size: matches[0] })) + +// width and height +const normalizeLength = v => `${v}${/^(\d*\.)?\d+$/.test(v) ? 'px' : ''}` + +optionMatchers.set( + /^w(?:idth)?:((?:\d*\.)?\d+(?:%|ch|cm|em|ex|in|mm|pc|pt|px)?|auto)$/, + matches => ({ width: normalizeLength(matches[1]) }) +) + +optionMatchers.set( + /^h(?:eight)?:((?:\d*\.)?\d+(?:%|ch|cm|em|ex|in|mm|pc|pt|px)?|auto)$/, + matches => ({ height: normalizeLength(matches[1]) }) +) + +// CSS filters +optionMatchers.set(/^blur(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['blur', escape(matches[1] || '10px')]], +})) +optionMatchers.set(/^brightness(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['brightness', escape(matches[1] || '1.5')]], +})) +optionMatchers.set(/^contrast(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['contrast', escape(matches[1] || '2')]], +})) +optionMatchers.set( + /^drop-shadow(?::(.+?),(.+?)(?:,(.+?))?(?:,(.+?))?)?$/, + (matches, meta) => { + const args = [] + + for (const arg of matches.slice(1)) { + if (arg) { + const colorFunc = arg.match(/^(rgba?|hsla?)\((.*)\)$/) + + args.push( + colorFunc ? `${colorFunc[1]}(${escape(colorFunc[2])})` : escape(arg) + ) + } + } + + return { + filters: [ + ...meta.filters, + ['drop-shadow', args.join(' ') || '0 5px 10px rgba(0,0,0,.4)'], + ], + } + } +) +optionMatchers.set(/^grayscale(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['grayscale', escape(matches[1] || '1')]], +})) +optionMatchers.set(/^hue-rotate(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['hue-rotate', escape(matches[1] || '180deg')]], +})) +optionMatchers.set(/^invert(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['invert', escape(matches[1] || '1')]], +})) +optionMatchers.set(/^opacity(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['opacity', escape(matches[1] || '.5')]], +})) +optionMatchers.set(/^saturate(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['saturate', escape(matches[1] || '2')]], +})) +optionMatchers.set(/^sepia(?::(.+))?$/, (matches, meta) => ({ + filters: [...meta.filters, ['sepia', escape(matches[1] || '1')]], +})) + +/** + * Marpit image parse plugin. + * + * Parse image tokens and store the result into `marpitImage` meta. It has an + * image url and options. The alternative text is regarded as space-separated + * options. + * + * @alias module:markdown/image/parse + * @param {MarkdownIt} md markdown-it instance. + */ +function parseImage(md) { + md.inline.ruler2.push('marpit_parse_image', ({ tokens }) => { + for (const token of tokens) { + if (token.type === 'image') { + const options = token.content.split(/\s+/).filter(s => s.length > 0) + const url = token.attrGet('src') + + token.meta = token.meta || {} + token.meta.marpitImage = { + ...(token.meta.marpitImage || {}), + url, + options, + } + + // Detect shorthand for setting color + if (!!colorString.get(url) || url.toLowerCase() === 'currentcolor') { + token.meta.marpitImage.color = url + token.hidden = true + } + + // Parse keyword through matchers + for (const opt of options) { + for (const [regexp, mergeFunc] of optionMatchers) { + const matched = opt.match(regexp) + + if (matched) + token.meta.marpitImage = { + ...token.meta.marpitImage, + ...mergeFunc(matched, { + filters: [], + ...token.meta.marpitImage, + }), + } + } + } + } + } + }) +} + +export default marpitPlugin(parseImage) diff --git a/src/markdown/parse_image.js b/src/markdown/parse_image.js deleted file mode 100644 index 8d96d7f5..00000000 --- a/src/markdown/parse_image.js +++ /dev/null @@ -1,147 +0,0 @@ -/** @module */ -import marpitPlugin from './marpit_plugin' -import InlineStyle from '../helpers/inline_style' - -const escape = target => - target.replace( - /[\\;:()]/g, - matched => `\\${matched[0].codePointAt(0).toString(16)} ` - ) - -/** - * Marpit parse image plugin. - * - * Parse image tokens and store the result into `marpitImage` meta. It has an - * image url and options. The alternative text is regarded as space-separated - * options. - * - * @alias module:markdown/parse_image - * @param {MarkdownIt} md markdown-it instance. - */ -function parseImage(md) { - const optionMatchers = new Map() - - // The scale percentage for resize background - optionMatchers.set(/^(\d*\.)?\d+%$/, matches => ({ size: matches[0] })) - - // width and height - optionMatchers.set( - /^w(?:idth)?:((?:\d*\.)?\d+(?:%|ch|cm|em|ex|in|mm|pc|pt|px)?|auto)$/, - matches => ({ width: matches[1] }) - ) - - optionMatchers.set( - /^h(?:eight)?:((?:\d*\.)?\d+(?:%|ch|cm|em|ex|in|mm|pc|pt|px)?|auto)$/, - matches => ({ height: matches[1] }) - ) - - // CSS filters - optionMatchers.set(/^blur(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['blur', escape(matches[1] || '10px')]], - })) - optionMatchers.set(/^brightness(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['brightness', escape(matches[1] || '1.5')]], - })) - optionMatchers.set(/^contrast(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['contrast', escape(matches[1] || '2')]], - })) - optionMatchers.set( - /^drop-shadow(?::(.+?),(.+?)(?:,(.+?))?(?:,(.+?))?)?$/, - (matches, meta) => { - const args = [] - - for (const arg of matches.slice(1)) { - if (arg) { - const colorFunc = arg.match(/^(rgba?|hsla?)\((.*)\)$/) - - args.push( - colorFunc ? `${colorFunc[1]}(${escape(colorFunc[2])})` : escape(arg) - ) - } - } - - return { - filters: [ - ...meta.filters, - ['drop-shadow', args.join(' ') || '0 5px 10px rgba(0,0,0,.4)'], - ], - } - } - ) - optionMatchers.set(/^grayscale(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['grayscale', escape(matches[1] || '1')]], - })) - optionMatchers.set(/^hue-rotate(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['hue-rotate', escape(matches[1] || '180deg')]], - })) - optionMatchers.set(/^invert(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['invert', escape(matches[1] || '1')]], - })) - optionMatchers.set(/^opacity(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['opacity', escape(matches[1] || '.5')]], - })) - optionMatchers.set(/^saturate(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['saturate', escape(matches[1] || '2')]], - })) - optionMatchers.set(/^sepia(?::(.+))?$/, (matches, meta) => ({ - filters: [...meta.filters, ['sepia', escape(matches[1] || '1')]], - })) - - md.inline.ruler2.push('marpit_parse_image', ({ tokens }) => { - for (const token of tokens) { - if (token.type === 'image') { - const options = token.content.split(/\s+/).filter(s => s.length > 0) - - token.meta = token.meta || {} - token.meta.marpitImage = { - ...(token.meta.marpitImage || {}), - url: token.attrGet('src'), - options, - } - - for (const opt of options) - for (const [regexp, mergeFunc] of optionMatchers) { - const matched = opt.match(regexp) - - if (matched) - token.meta.marpitImage = { - ...token.meta.marpitImage, - ...mergeFunc(matched, { - filters: [], - ...token.meta.marpitImage, - }), - } - } - - // Build and apply styles - const { filters, height, width } = token.meta.marpitImage - const style = new InlineStyle(token.attrGet('style')) - - const assign = (decl, value) => { - const normalize = `${value}${/^(\d*\.)?\d+$/.test(value) ? 'px' : ''}` - token.meta.marpitImage[decl] = normalize - - if (!value.endsWith('%')) style.set(decl, normalize) - } - - if (width) assign('width', width) - if (height) assign('height', height) - - if (filters) { - const filterStyle = [] - - for (const fltrs of filters) - filterStyle.push(`${fltrs[0]}(${fltrs[1]})`) - - token.meta.marpitImage.filter = filterStyle.join(' ') - style.set('filter', token.meta.marpitImage.filter) - } - - const stringified = style.toString() - if (stringified) token.attrSet('style', stringified) - } - } - }) -} - -export default marpitPlugin(parseImage) diff --git a/src/marpit.js b/src/marpit.js index b267c07f..a192ee70 100644 --- a/src/marpit.js +++ b/src/marpit.js @@ -12,7 +12,7 @@ import marpitHeaderAndFooter from './markdown/header_and_footer' import marpitHeadingDivider from './markdown/heading_divider' import marpitInlineSVG from './markdown/inline_svg' import marpitParseDirectives from './markdown/directives/parse' -import marpitParseImage from './markdown/parse_image' +import marpitImage from './markdown/image' import marpitSlide from './markdown/slide' import marpitSlideContainer from './markdown/slide_container' import marpitStyleAssign from './markdown/style/assign' @@ -138,11 +138,11 @@ class Marpit { .use(marpitHeadingDivider) .use(marpitSlideContainer) .use(marpitContainerPlugin) - .use(marpitParseImage) - .use(marpitSweep) .use(marpitInlineSVG) - .use(marpitStyleAssign) + .use(marpitImage) .use(marpitBackgroundImage) + .use(marpitSweep) + .use(marpitStyleAssign) .use(marpitFragment) .use(marpitCollect) } diff --git a/src/theme_set.js b/src/theme_set.js index 801f09bc..fb1979ff 100644 --- a/src/theme_set.js +++ b/src/theme_set.js @@ -17,6 +17,9 @@ import scaffold from './theme/scaffold' * Marpit theme set class. */ class ThemeSet { + /** + * Create a ThemeSet instance. + */ constructor() { /** * An instance of default theme. diff --git a/test/markdown/background_image.js b/test/markdown/background_image.js index e591f7d4..4c07eed1 100644 --- a/test/markdown/background_image.js +++ b/test/markdown/background_image.js @@ -5,7 +5,7 @@ import backgroundImage from '../../src/markdown/background_image' import comment from '../../src/markdown/comment' import inlineSVG from '../../src/markdown/inline_svg' import parseDirectives from '../../src/markdown/directives/parse' -import parseImage from '../../src/markdown/parse_image' +import image from '../../src/markdown/image' import slide from '../../src/markdown/slide' const splitBackgroundKeywords = ['left', 'right'] @@ -28,7 +28,7 @@ describe('Marpit background image plugin', () => { .use(parseDirectives) .use(applyDirectives) .use(inlineSVG) - .use(parseImage) + .use(image) .use(backgroundImage) } diff --git a/test/markdown/image.js b/test/markdown/image.js new file mode 100644 index 00000000..2c914a45 --- /dev/null +++ b/test/markdown/image.js @@ -0,0 +1,105 @@ +import cheerio from 'cheerio' +import MarkdownIt from 'markdown-it' +import applyDirectives from '../../src/markdown/directives/apply' +import backgroundImage from '../../src/markdown/background_image' +import comment from '../../src/markdown/comment' +import inlineSVG from '../../src/markdown/inline_svg' +import parseDirectives from '../../src/markdown/directives/parse' +import image from '../../src/markdown/image' +import slide from '../../src/markdown/slide' + +describe('Marpit image plugin', () => { + const md = (svg = false) => + new MarkdownIt('commonmark') + .use(instance => { + instance.marpit = { + customDirectives: { global: {}, local: {} }, + options: { inlineSVG: svg }, + } + }) + .use(comment) + .use(slide) + .use(parseDirectives) + .use(applyDirectives) + .use(inlineSVG) + .use(image) + .use(backgroundImage) + + describe('Style for inline image', () => { + const style = opts => { + const $ = cheerio.load( + md().render(`![${opts}](https://example.com/example.jpg)`) + ) + return $('img').attr('style') + } + + it('renders image width style', () => { + // Number & floats + expect(style('w:100')).toBe('width:100px;') + expect(style('width:23.4')).toBe('width:23.4px;') + expect(style('w:.5')).toBe('width:.5px;') + + // CSS units + expect(style('w:1ch')).toBe('width:1ch;') + expect(style('w:2cm')).toBe('width:2cm;') + expect(style('w:3em')).toBe('width:3em;') + expect(style('w:4ex')).toBe('width:4ex;') + expect(style('w:5in')).toBe('width:5in;') + expect(style('w:6mm')).toBe('width:6mm;') + expect(style('w:7pc')).toBe('width:7pc;') + expect(style('w:8pt')).toBe('width:8pt;') + expect(style('w:9px')).toBe('width:9px;') + + // Percentage and not supported keyword in inline image will ignore + expect(style('w:100%')).not.toBe('width:100%;') + expect(style('w:12.345%')).not.toBe('width:12.345%;') + expect(style('w:.678%')).not.toBe('width:.678%;') + expect(style('w:unexpected')).not.toBe('width:unexpected;') + }) + + it('renders image height style', () => { + expect(style('h:100')).toBe('height:100px;') + expect(style('height:23.4')).toBe('height:23.4px;') + expect(style('h:.5')).toBe('height:.5px;') + + expect(style('h:1ch')).toBe('height:1ch;') + expect(style('h:2cm')).toBe('height:2cm;') + expect(style('h:3em')).toBe('height:3em;') + expect(style('h:4ex')).toBe('height:4ex;') + expect(style('h:5in')).toBe('height:5in;') + expect(style('h:6mm')).toBe('height:6mm;') + expect(style('h:7pc')).toBe('height:7pc;') + expect(style('h:8pt')).toBe('height:8pt;') + expect(style('h:9px')).toBe('height:9px;') + + expect(style('h:100%')).not.toBe('height:100%;') + expect(style('h:12.345%')).not.toBe('height:12.345%;') + expect(style('h:.678%')).not.toBe('height:.678%;') + expect(style('h:unexpected')).not.toBe('height:unexpected;') + }) + }) + + describe('Shorthand for text color', () => { + const colorMd = (src, opts = '') => `![${opts}](${src})` + const colorDirective = markdown => { + const [firstSlide] = md().parse(markdown) + return firstSlide.meta.marpitDirectives.color + } + + it('assigns color directive', () => { + expect(colorDirective(colorMd('#123abc'))).toBe('#123abc') + expect(colorDirective(colorMd('#def'))).toBe('#def') + expect(colorDirective(colorMd('transparent'))).toBe('transparent') + expect(colorDirective(colorMd('currentColor'))).toBe('currentColor') + expect(colorDirective(colorMd('rgb(255,128,0)'))).toBe('rgb(255,128,0)') + expect(colorDirective(colorMd('rgba(16,32,64,0.5)'))).toBe( + 'rgba(16,32,64,0.5)' + ) + }) + + it('does not assign color directive when options have bg keyword', () => { + expect(colorDirective(colorMd('#123abc', 'bg'))).toBeUndefined() + expect(colorDirective('![bg](red) ![](blue)')).toBe('blue') + }) + }) +}) diff --git a/test/markdown/parse_image.js b/test/markdown/parse_image.js deleted file mode 100644 index 430fcebf..00000000 --- a/test/markdown/parse_image.js +++ /dev/null @@ -1,64 +0,0 @@ -import cheerio from 'cheerio' -import MarkdownIt from 'markdown-it' -import parseImage from '../../src/markdown/parse_image' - -describe('Marpit parse image plugin', () => { - const md = () => { - const instance = new MarkdownIt('commonmark') - instance.marpit = { options: {} } - - return instance.use(parseImage) - } - - const style = opts => { - const $ = cheerio.load( - md().render(`![${opts}](https://example.com/example.jpg)`) - ) - return $('img').attr('style') - } - - it('renders image width style', () => { - // Number & floats - expect(style('w:100')).toBe('width:100px;') - expect(style('width:23.4')).toBe('width:23.4px;') - expect(style('w:.5')).toBe('width:.5px;') - - // CSS units - expect(style('w:1ch')).toBe('width:1ch;') - expect(style('w:2cm')).toBe('width:2cm;') - expect(style('w:3em')).toBe('width:3em;') - expect(style('w:4ex')).toBe('width:4ex;') - expect(style('w:5in')).toBe('width:5in;') - expect(style('w:6mm')).toBe('width:6mm;') - expect(style('w:7pc')).toBe('width:7pc;') - expect(style('w:8pt')).toBe('width:8pt;') - expect(style('w:9px')).toBe('width:9px;') - - // Percentage and not supported keyword in inline image will ignore - expect(style('w:100%')).not.toBe('width:100%;') - expect(style('w:12.345%')).not.toBe('width:12.345%;') - expect(style('w:.678%')).not.toBe('width:.678%;') - expect(style('w:unexpected')).not.toBe('width:unexpected;') - }) - - it('renders image height style', () => { - expect(style('h:100')).toBe('height:100px;') - expect(style('height:23.4')).toBe('height:23.4px;') - expect(style('h:.5')).toBe('height:.5px;') - - expect(style('h:1ch')).toBe('height:1ch;') - expect(style('h:2cm')).toBe('height:2cm;') - expect(style('h:3em')).toBe('height:3em;') - expect(style('h:4ex')).toBe('height:4ex;') - expect(style('h:5in')).toBe('height:5in;') - expect(style('h:6mm')).toBe('height:6mm;') - expect(style('h:7pc')).toBe('height:7pc;') - expect(style('h:8pt')).toBe('height:8pt;') - expect(style('h:9px')).toBe('height:9px;') - - expect(style('h:100%')).not.toBe('height:100%;') - expect(style('h:12.345%')).not.toBe('height:12.345%;') - expect(style('h:.678%')).not.toBe('height:.678%;') - expect(style('h:unexpected')).not.toBe('height:unexpected;') - }) -}) diff --git a/test/markdown/sweep.js b/test/markdown/sweep.js index b92c5a95..3747fdaf 100644 --- a/test/markdown/sweep.js +++ b/test/markdown/sweep.js @@ -6,7 +6,7 @@ import backgroundImage from '../../src/markdown/background_image' import comment from '../../src/markdown/comment' import inlineSVG from '../../src/markdown/inline_svg' import parseDirectives from '../../src/markdown/directives/parse' -import parseImage from '../../src/markdown/parse_image' +import image from '../../src/markdown/image' import slide from '../../src/markdown/slide' import sweep from '../../src/markdown/sweep' @@ -33,7 +33,7 @@ describe('Marpit sweep plugin', () => { .use(parseDirectives) .use(applyDirectives) .use(inlineSVG) - .use(parseImage) + .use(image) .use(backgroundImage) const $ = cheerio.load( diff --git a/test/marpit.js b/test/marpit.js index 111966de..a4304533 100644 --- a/test/marpit.js +++ b/test/marpit.js @@ -228,6 +228,17 @@ describe('Marpit', () => { }) }) + describe('Color shorthand', () => { + it('applies color to the current slide', () => { + const $ = cheerio.load(new Marpit().render('![](red)![bg](blue)').html) + expect($('section').html()).toBe('') + + const style = $('section').attr('style') + expect(style).toContain('color:red;') + expect(style).toContain('background-color:blue;') + }) + }) + describe('Inline style', () => { const instance = () => { const marpit = new Marpit()