diff --git a/CHANGELOG.md b/CHANGELOG.md index 06872cf5..ce128210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - Bugfix: Pass `class` attribute to pseudo section on advanced background ([#48](https://github.com/marp-team/marpit/pull/48)) +- Lazy yaml support by `lazyYAML` option ([#49](https://github.com/marp-team/marpit/pull/49)) ## v0.0.10 - 2018-08-05 diff --git a/README.md b/README.md index 6bcc86b2..a40e9f6b 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Marpit will become a core of _the next version of **[Marp](https://github.com/yh #### Difference from [pre-released Marp](https://github.com/yhatt/marp/) - Removed directives about slide size. [Use `width` / `height` declaration of theme CSS.](#slide-size) -- Parse directives by YAML parser. ([js-yaml](https://github.com/nodeca/js-yaml) + [`FAILSAFE_SCHEMA`](http://www.yaml.org/spec/1.2/spec.html#id2802346)) +- Parse directives by YAML parser. ([js-yaml](https://github.com/nodeca/js-yaml) + [`FAILSAFE_SCHEMA`](http://www.yaml.org/spec/1.2/spec.html#id2802346), but we still support lazy YAML parser by `lazyYAML` option) - Support [Jekyll style front-matter](https://jekyllrb.com/docs/frontmatter/). - _[Global directives](https://github.com/yhatt/marp/blob/master/example.md#global-directives)_ is no longer requires `$` prefix. (but it still supports because of compatibility and clarity) - [Page directives](https://github.com/yhatt/marp/blob/master/example.md#page-directives) is renamed to _local directives_. @@ -118,6 +118,8 @@ footer: "![image](https://example.com/image.jpg)" ``` > :warning: Marpit uses YAML for parsing directives, so **you should wrap with (double-)quotes** when the value includes invalid chars in YAML. +> +> You can enable a lazy YAML parser by `lazyYAML` Marpit constructor option if you want to recognize string without quotes. > :information_source: Due to the parsing order of Markdown, you cannot use [slide background images](#slide-background) in `header` and `footer` directives. diff --git a/index.d.ts b/index.d.ts index c93632ea..a129c8e5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,6 +5,7 @@ declare module '@marp-team/marpit' { filters?: boolean headingDivider?: false | MarpitHeadingDivider | MarpitHeadingDivider[] inlineStyle?: boolean + lazyYAML?: boolean markdown?: string | object | [string, object] printable?: boolean slideContainer?: Element | Element[] diff --git a/src/helpers/parse_yaml.js b/src/helpers/parse_yaml.js deleted file mode 100644 index 666fec66..00000000 --- a/src/helpers/parse_yaml.js +++ /dev/null @@ -1,21 +0,0 @@ -/** @module */ -import YAML, { FAILSAFE_SCHEMA } from 'js-yaml' - -/** - * Parse text as YAML by using js-yaml's FAILSAFE_SCHEMA. - * - * @alias module:helpers/parse_yaml - * @param {String} text Target text. - */ -function parseYAML(text) { - try { - const obj = YAML.safeLoad(text, { schema: FAILSAFE_SCHEMA }) - if (obj === null || typeof obj !== 'object') return false - - return obj - } catch (e) { - return false - } -} - -export default parseYAML diff --git a/src/markdown/comment.js b/src/markdown/comment.js index 9e1d9d50..9c54be93 100644 --- a/src/markdown/comment.js +++ b/src/markdown/comment.js @@ -1,5 +1,5 @@ /** @module */ -import parseYAML from '../helpers/parse_yaml' +import yaml from './directives/yaml' const commentMatcher = // * * @alias module:markdown/comment * @param {MarkdownIt} md markdown-it instance. + * @param {Object} [opts] + * @param {boolean} [opts.lazyYAML=false] Allow lazy YAML for directives. */ -function comment(md) { +function comment(md, opts = {}) { /** * Based on markdown-it html_block rule * https://github.com/markdown-it/markdown-it/blob/master/lib/rules_block/html_block.js @@ -60,9 +62,9 @@ function comment(md) { const matchedContent = commentMatcher.exec(token.markup) token.content = matchedContent ? matchedContent[1].trim() : '' - // Parse YAML - const yaml = parseYAML(token.content) - token.meta = { marpitParsedYAML: yaml === false ? {} : yaml } + // Parse object + const parsed = yaml(token.content, !!opts.lazyYAML) + token.meta = { marpitParsedDirectives: parsed === false ? {} : parsed } return true } diff --git a/src/markdown/directives/directives.js b/src/markdown/directives/directives.js index 5ef8324d..b0aa03e7 100644 --- a/src/markdown/directives/directives.js +++ b/src/markdown/directives/directives.js @@ -109,4 +109,6 @@ export const locals = { }, } -export default { globals, locals } +const directiveNames = [...Object.keys(globals), ...Object.keys(locals)] + +export default directiveNames diff --git a/src/markdown/directives/parse.js b/src/markdown/directives/parse.js index 3a9933bd..66f74aab 100644 --- a/src/markdown/directives/parse.js +++ b/src/markdown/directives/parse.js @@ -1,6 +1,6 @@ /** @module */ import MarkdownItFrontMatter from 'markdown-it-front-matter' -import parseYAML from '../../helpers/parse_yaml' +import yaml from './yaml' import { globals, locals } from './directives' /** @@ -16,6 +16,7 @@ import { globals, locals } from './directives' * @param {boolean} [opts.frontMatter=true] Switch feature to support YAML * front-matter. If true, you can use Jekyll style directive setting to the * first page. + * @param {boolean} [opts.lazyYAML=false] Allow lazy YAML for directives. */ function parse(md, marpit, opts = {}) { // Front-matter support @@ -30,8 +31,8 @@ function parse(md, marpit, opts = {}) { md.use(MarkdownItFrontMatter, fm => { frontMatterObject.text = fm - const yaml = parseYAML(fm) - if (yaml !== false) frontMatterObject.yaml = yaml + const parsed = yaml(fm, !!opts.lazyYAML) + if (parsed !== false) frontMatterObject.yaml = parsed }) } @@ -40,14 +41,14 @@ function parse(md, marpit, opts = {}) { if (state.inlineMode) return let globalDirectives = {} - const applyDirectives = yaml => { - Object.keys(yaml).forEach(key => { + const applyDirectives = obj => { + Object.keys(obj).forEach(key => { const globalKey = key.startsWith('$') ? key.slice(1) : key if (globals[globalKey]) globalDirectives = { ...globalDirectives, - ...globals[globalKey](yaml[key], marpit), + ...globals[globalKey](obj[key], marpit), } }) } @@ -55,8 +56,8 @@ function parse(md, marpit, opts = {}) { if (frontMatterObject.yaml) applyDirectives(frontMatterObject.yaml) state.tokens.forEach(token => { - if (token.type === 'marpit_comment' && token.meta.marpitParsedYAML) - applyDirectives(token.meta.marpitParsedYAML) + if (token.type === 'marpit_comment' && token.meta.marpitParsedDirectives) + applyDirectives(token.meta.marpitParsedDirectives) }) marpit.lastGlobalDirectives = { ...globalDirectives } @@ -69,10 +70,10 @@ function parse(md, marpit, opts = {}) { const slides = [] const cursor = { slide: undefined, local: {}, spot: {} } - const applyDirectives = yaml => { - Object.keys(yaml).forEach(key => { + const applyDirectives = obj => { + Object.keys(obj).forEach(key => { if (locals[key]) - cursor.local = { ...cursor.local, ...locals[key](yaml[key], marpit) } + cursor.local = { ...cursor.local, ...locals[key](obj[key], marpit) } // Spot directives // (Apply local directive to only current slide by prefix "_") @@ -82,7 +83,7 @@ function parse(md, marpit, opts = {}) { if (locals[spotKey]) cursor.spot = { ...cursor.spot, - ...locals[spotKey](yaml[key], marpit), + ...locals[spotKey](obj[key], marpit), } } }) @@ -108,9 +109,9 @@ function parse(md, marpit, opts = {}) { cursor.spot = {} } else if ( token.type === 'marpit_comment' && - token.meta.marpitParsedYAML + token.meta.marpitParsedDirectives ) { - applyDirectives(token.meta.marpitParsedYAML) + applyDirectives(token.meta.marpitParsedDirectives) } }) diff --git a/src/markdown/directives/yaml.js b/src/markdown/directives/yaml.js new file mode 100644 index 00000000..421edce0 --- /dev/null +++ b/src/markdown/directives/yaml.js @@ -0,0 +1,58 @@ +/** @module */ +import YAML, { FAILSAFE_SCHEMA } from 'js-yaml' +import directives from './directives' + +/** + * Parse text as YAML by using js-yaml's FAILSAFE_SCHEMA. + * + * @alias module:markdown/directives/yaml + * @param {String} text Target text. + * @param {boolean} allowLazy By `true`, it try to parse lazy YAML at first. + * @returns {Object|false} Return parse result, or `false` when failed to parse. + */ + +const keyPattern = `[_$]?(?:${directives.join('|')})` +const directivesMatcher = new RegExp(`^\\s*(${keyPattern})\\s*:(.*)$`) +const specialChars = `["'{|>~&*` +const whitespaceMatcher = /^\s*$/ + +function strict(text) { + try { + const obj = YAML.safeLoad(text, { schema: FAILSAFE_SCHEMA }) + if (obj === null || typeof obj !== 'object') return false + + return obj + } catch (e) { + return false + } +} + +function lazy(text) { + const collected = {} + const lines = text.split(/\r?\n/) + + return lines.every(line => { + if (whitespaceMatcher.test(line)) return true + + const matched = directivesMatcher.exec(line) + if (!matched) return false + + const [, directive, originalValue] = matched + const value = originalValue.trim() + if (specialChars.includes(value[0])) return false + + collected[directive] = value + return true + }) + ? collected + : false +} + +export default function(text, allowLazy) { + if (allowLazy) { + const lazyResult = lazy(text) + if (lazyResult !== false) return lazyResult + } + + return strict(text) +} diff --git a/src/marpit.js b/src/marpit.js index b8014b69..6a3a35d5 100644 --- a/src/marpit.js +++ b/src/marpit.js @@ -24,6 +24,7 @@ const defaultOptions = { filters: true, headingDivider: false, inlineStyle: true, + lazyYAML: false, markdown: 'commonmark', printable: true, slideContainer: undefined, @@ -54,6 +55,7 @@ class Marpit { * @param {boolean} [opts.inlineStyle=true] Recognize `