Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom directives support #125

Merged
merged 5 commits into from
Jan 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Make custom directives definable via `customDirectives` property ([#124](https://github.com/marp-team/marpit/issues/124), [#125](https://github.com/marp-team/marpit/pull/125))

## v0.6.1 - 2019-01-25

### Fixed
Expand Down
20 changes: 10 additions & 10 deletions src/markdown/directives/apply.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
/** @module */
import kebabCase from 'lodash.kebabcase'
import { globals, locals } from './directives'
import builtInDirectives from './directives'
import InlineStyle from '../../helpers/inline_style'

const publicDirectives = [...Object.keys(globals), ...Object.keys(locals)]

/**
* Apply parsed Marpit directives to markdown-it tokens.
*
* @alias module:markdown/directives/apply
* @param {MarkdownIt} md markdown-it instance.
* @param {Marpit} marpit Marpit instance.
* @param {Object} [opts]
* @param {boolean} [opts.dataset=true] Assigns directives as HTML data
* attributes of each section tag.
* @param {boolean} [opts.css=true] Assigns directives as CSS Custom Properties
* of each section tag.
* @param {boolean} [opts.includeInternal=false] Whether include internal
* directives (Undefined in {@link module:markdown/directives/directives}.)
* In default, internal directives are not applied to HTML/CSS.
*/
function apply(md, opts = {}) {
function apply(md, marpit, opts = {}) {
const dataset = opts.dataset === undefined ? true : !!opts.dataset
const css = opts.css === undefined ? true : !!opts.css

const filterFunc = key =>
!!opts.includeInternal || publicDirectives.includes(key)
const { global, local } = marpit.customDirectives
const directives = [
...Object.keys(global),
...Object.keys(local),
...builtInDirectives,
]

md.core.ruler.after(
'marpit_directives_parse',
Expand All @@ -39,7 +39,7 @@ function apply(md, opts = {}) {
const style = new InlineStyle(token.attrGet('style'))

for (const dir of Object.keys(marpitDirectives)) {
if (filterFunc(dir)) {
if (directives.includes(dir)) {
const value = marpitDirectives[dir]

if (value) {
Expand Down
55 changes: 14 additions & 41 deletions src/markdown/directives/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* @prop {Directive} theme Specify theme of the slide deck.
*/
export const globals = {
headingDivider(value) {
headingDivider: value => {
const headings = [1, 2, 3, 4, 5, 6]
const toInt = v =>
Array.isArray(v) || Number.isNaN(v) ? v : Number.parseInt(v, 10)
Expand All @@ -42,13 +42,8 @@ export const globals = {

return {}
},
style(value) {
return { style: value }
},
theme(value, marpit) {
if (!marpit.themeSet.has(value)) return {}
return { theme: value }
},
style: v => ({ style: v }),
theme: (v, marpit) => (marpit.themeSet.has(v) ? { theme: v } : {}),
}

/**
Expand Down Expand Up @@ -77,38 +72,16 @@ export const globals = {
* @prop {Directive} paginate Show page number on the slide if you set `true`.
*/
export const locals = {
backgroundColor(value) {
return { backgroundColor: value }
},
backgroundImage(value) {
return { backgroundImage: value }
},
backgroundPosition(value) {
return { backgroundPosition: value }
},
backgroundRepeat(value) {
return { backgroundRepeat: value }
},
backgroundSize(value) {
return { backgroundSize: value }
},
class(value) {
return { class: Array.isArray(value) ? value.join(' ') : value }
},
color(value) {
return { color: value }
},
footer(value) {
return typeof value === 'string' ? { footer: value } : {}
},
header(value) {
return typeof value === 'string' ? { header: value } : {}
},
paginate(value) {
return { paginate: (value || '').toLowerCase() === 'true' }
},
backgroundColor: v => ({ backgroundColor: v }),
backgroundImage: v => ({ backgroundImage: v }),
backgroundPosition: v => ({ backgroundPosition: v }),
backgroundRepeat: v => ({ backgroundRepeat: v }),
backgroundSize: v => ({ backgroundSize: v }),
class: v => ({ class: Array.isArray(v) ? v.join(' ') : v }),
color: v => ({ color: v }),
footer: v => (typeof v === 'string' ? { footer: v } : {}),
header: v => (typeof v === 'string' ? { header: v } : {}),
paginate: v => ({ paginate: (v || '').toLowerCase() === 'true' }),
}

const directiveNames = [...Object.keys(globals), ...Object.keys(locals)]

export default directiveNames
export default [...Object.keys(globals), ...Object.keys(locals)]
52 changes: 45 additions & 7 deletions src/markdown/directives/parse.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @module */
import MarkdownItFrontMatter from 'markdown-it-front-matter'
import yaml from './yaml'
import { globals, locals } from './directives'
import * as directives from './directives'

/**
* Parse Marpit directives and store result to the slide token meta.
Expand Down Expand Up @@ -44,6 +44,17 @@ function parse(md, marpit, opts = {}) {
token.meta.marpitCommentParsed = 'directive'
}

const filterBuiltinDirective = newProps => {
const ret = {}

for (const prop of Object.keys(newProps).filter(
p => !directives.default.includes(p)
))
ret[prop] = newProps[prop]

return ret
}

// Parse global directives
md.core.ruler.after('inline', 'marpit_directives_global_parse', state => {
if (state.inlineMode) return
Expand All @@ -55,11 +66,19 @@ function parse(md, marpit, opts = {}) {
for (const key of Object.keys(obj)) {
const globalKey = key.startsWith('$') ? key.slice(1) : key

if (globals[globalKey]) {
if (directives.globals[globalKey]) {
recognized = true
globalDirectives = {
...globalDirectives,
...directives.globals[globalKey](obj[key], marpit),
}
} else if (marpit.customDirectives.global[globalKey]) {
recognized = true
globalDirectives = {
...globalDirectives,
...globals[globalKey](obj[key], marpit),
...filterBuiltinDirective(
marpit.customDirectives.global[globalKey](obj[key], marpit)
),
}
}
}
Expand Down Expand Up @@ -97,21 +116,40 @@ function parse(md, marpit, opts = {}) {
let recognized = false

for (const key of Object.keys(obj)) {
if (locals[key]) {
if (directives.locals[key]) {
recognized = true
cursor.local = { ...cursor.local, ...locals[key](obj[key], marpit) }
cursor.local = {
...cursor.local,
...directives.locals[key](obj[key], marpit),
}
} else if (marpit.customDirectives.local[key]) {
recognized = true
cursor.local = {
...cursor.local,
...filterBuiltinDirective(
marpit.customDirectives.local[key](obj[key], marpit)
),
}
}

// Spot directives
// (Apply local directive to only current slide by prefix "_")
if (key.startsWith('_')) {
const spotKey = key.slice(1)

if (locals[spotKey]) {
if (directives.locals[spotKey]) {
recognized = true
cursor.spot = {
...cursor.spot,
...directives.locals[spotKey](obj[key], marpit),
}
} else if (marpit.customDirectives.local[spotKey]) {
recognized = true
cursor.spot = {
...cursor.spot,
...locals[spotKey](obj[key], marpit),
...filterBuiltinDirective(
marpit.customDirectives.local[spotKey](obj[key], marpit)
),
}
}
}
Expand Down
21 changes: 19 additions & 2 deletions src/marpit.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,31 @@ class Marpit {
* value of options after creating instance.
*
* @member {Object} options
* @memberOf Marpit#
* @memberOf Marpit
* @readonly
*/
Object.defineProperty(this, 'options', {
enumerable: true,
value: Object.freeze({ ...defaultOptions, ...opts }),
})

/**
* Definitions of the custom directive.
*
* It has the assignable `global` and `local` object. They have consisted of
* the directive name as a key, and parser function as a value. The parser
* should return the validated object for updating meta of markdown-it
* token.
*
* @member {Object} customDirectives
* @memberOf Marpit
* @readonly
*/
Object.defineProperty(this, 'customDirectives', {
value: Object.seal({ global: {}, local: {} }),
})

// Internal members
Object.defineProperties(this, {
containers: { value: [...wrapArray(this.options.container)] },
slideContainers: { value: [...wrapArray(this.options.slideContainer)] },
Expand Down Expand Up @@ -121,7 +138,7 @@ class Marpit {
.use(marpitStyleParse, this)
.use(marpitSlide)
.use(marpitParseDirectives, this, { looseYAML })
.use(marpitApplyDirectives)
.use(marpitApplyDirectives, this)
.use(marpitHeaderAndFooter)
.use(marpitHeadingDivider, this)
.use(marpitSlideContainer, this.slideContainers)
Expand Down
3 changes: 2 additions & 1 deletion test/markdown/background_image.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const splitBackgroundKeywords = ['left', 'right']

describe('Marpit background image plugin', () => {
const marpitStub = svg => ({
customDirectives: { global: {}, local: {} },
lastGlobalDirectives: {},
themeSet: { getThemeProp: () => 100 },
options: { inlineSVG: svg },
Expand All @@ -22,7 +23,7 @@ describe('Marpit background image plugin', () => {
.use(comment)
.use(slide)
.use(parseDirectives, marpitStub(svg))
.use(applyDirectives)
.use(applyDirectives, marpitStub(svg))
.use(inlineSVG, marpitStub(svg))
.use(parseImage, { filters })
.use(backgroundImage)
Expand Down
8 changes: 6 additions & 2 deletions test/markdown/collect.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('Marpit collect plugin', () => {

const marpitStub = (svg = false) => ({
themeSet,
customDirectives: { global: {}, local: {} },
lastGlobalDirectives: {},
options: { inlineSVG: svg },
})
Expand All @@ -21,8 +22,11 @@ describe('Marpit collect plugin', () => {
new MarkdownIt('commonmark')
.use(comment)
.use(slide)
.use(parseDirectives, { themeSet: marpitInstance.themeSet })
.use(applyDirectives)
.use(parseDirectives, {
customDirectives: marpitInstance.customDirectives,
themeSet: marpitInstance.themeSet,
})
.use(applyDirectives, marpitInstance)
.use(collect, marpitInstance)
.use(inlineSVG, marpitInstance)

Expand Down
24 changes: 7 additions & 17 deletions test/markdown/directives/apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ describe('Marpit directives apply plugin', () => {
const themeSetStub = new Map()
themeSetStub.set('test_theme', true)

const md = (...args) =>
new MarkdownIt('commonmark')
const md = (...args) => {
const customDirectives = { global: {}, local: {} }

return new MarkdownIt('commonmark')
.use(comment)
.use(slide)
.use(parseDirectives, { themeSet: themeSetStub })
.use(applyDirectives, ...args)
.use(parseDirectives, { customDirectives, themeSet: themeSetStub })
.use(applyDirectives, { customDirectives }, ...args)
}

const mdForTest = (...args) =>
md(...args).use(mdInstance => {
Expand Down Expand Up @@ -86,19 +89,6 @@ describe('Marpit directives apply plugin', () => {
})
})

context('with includeInternal option as true', () => {
const opts = { includeInternal: true }

it('applies together with unknown (internal) directive', () => {
const $ = cheerio.load(mdForTest(opts).render(basicDirs))
const section = $('section').first()
const style = toObjStyle(section.attr('style'))

expect(section.attr('data-unknown-dir')).toBe('directive')
expect(style['--unknown-dir']).toBe('directive')
})
})

describe('Local directives', () => {
describe('Background image', () => {
const bgDirs = dedent`
Expand Down
5 changes: 4 additions & 1 deletion test/markdown/directives/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import slide from '../../../src/markdown/slide'

describe('Marpit directives parse plugin', () => {
const themeSetStub = new Map()
const marpitStub = { themeSet: themeSetStub }
const marpitStub = {
customDirectives: { global: {}, local: {} },
themeSet: themeSetStub,
}
themeSetStub.set('test_theme', true)

const md = (...args) =>
Expand Down
8 changes: 6 additions & 2 deletions test/markdown/header_and_footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('Marpit header and footer plugin', () => {

const marpitStub = (props = {}) => ({
themeSet,
customDirectives: { global: {}, local: {} },
lastGlobalDirectives: {},
...props,
})
Expand All @@ -20,8 +21,11 @@ describe('Marpit header and footer plugin', () => {
new MarkdownIt('commonmark')
.use(comment)
.use(slide)
.use(parseDirectives, { themeSet: marpitInstance.themeSet })
.use(applyDirectives)
.use(parseDirectives, {
customDirectives: marpitInstance.customDirectives,
themeSet: marpitInstance.themeSet,
})
.use(applyDirectives, marpitInstance)
.use(headerAndFooter)

describe('Header local directive', () => {
Expand Down
Loading