Skip to content

Commit

Permalink
Merge pull request #159 from marp-team/text-color-syntax
Browse files Browse the repository at this point in the history
Add image syntax for text color
  • Loading branch information
yhatt authored May 5, 2019
2 parents 3c267ca + 498ec9c commit 273fc51
Show file tree
Hide file tree
Showing 15 changed files with 399 additions and 261 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
78 changes: 47 additions & 31 deletions docs/image-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) <!-- Hex color -->

---

![bg](rebeccapurple) <!-- Named color -->

---

![bg](<rgb(255,128,0)>) <!-- RGB values -->
```

It is same as defining [`<!-- _backgroundColor: "#fff" -->` 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].

Expand Down Expand Up @@ -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.

<!-- prettier-ignore-start -->

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

<!-- prettier-ignore-end -->

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()`.
6 changes: 3 additions & 3 deletions src/markdown/background_image/apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ function backgroundImageApply(md) {
const {
background,
backgroundDirection,
backgroundColor,
backgroundSize,
backgroundSplit,
color,
filter,
height,
size,
Expand All @@ -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
Expand Down
8 changes: 0 additions & 8 deletions src/markdown/background_image/parse.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/** @module */
import colorString from 'color-string'
import marpitPlugin from '../marpit_plugin'

const bgSizeKeywords = {
Expand Down Expand Up @@ -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])
Expand Down
17 changes: 17 additions & 0 deletions src/markdown/image.js
Original file line number Diff line number Diff line change
@@ -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)
73 changes: 73 additions & 0 deletions src/markdown/image/apply.js
Original file line number Diff line number Diff line change
@@ -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)
131 changes: 131 additions & 0 deletions src/markdown/image/parse.js
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 273fc51

Please sign in to comment.