Skip to content

Commit

Permalink
Merge pull request #41 from marp-team/heading-divider
Browse files Browse the repository at this point in the history
Support heading divider
  • Loading branch information
yhatt authored Jul 22, 2018
2 parents 03a1f90 + 8721fdb commit d08765f
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 40 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]

- Add the basic TypeScript definition ([#40](https://github.com/marp-team/marpit/pull/40))
- Support heading divider ([#41](https://github.com/marp-team/marpit/pull/41))

## v0.0.8 - 2018-06-28

Expand Down
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,72 @@ footer: "![image](https://example.com/image.jpg)"
> :information_source: Due to the parsing order of Markdown, you cannot use [slide background images](#slide-background) in `header` and `footer` directives.
#### Heading divider

This feature is similar to [Pandoc](https://pandoc.org/)'s [`--slide-level` option](https://pandoc.org/MANUAL.html#structuring-the-slide-show) and [Deckset 2](https://www.deckset.com/2/)'s "Slide Dividers" option.

By using `headingDivider` global directive, you can instruct to divide slide pages automatically at before of headings whose larger than or equal to specified level.

For example, the below 2 markdowns have the same output.

<table>
<thead>
<tr>
<th style="text-align:center;">Regular syntax</th>
<th style="text-align:center;">Heading divider</th>
</tr>
</thead>
<tbody>
<tr>
<td>

```markdown
# 1st page

The content of 1st page

---

## 2nd page

### The content of 2nd page

Hello, world!

---

# 3rd page

😃
```

</td><td>

```markdown
<!-- headingDivider: 2 -->

# 1st page

The content of 1st page

## 2nd page

### The content of 2nd page

Hello, world!

# 3rd page

😃
```

</td>
</tr>
</tbody>
</table>

It is useful when you want to create a slide deck from a plain Markdown. Even if you opened an example about `headingDivider` in general Markdown editor, it keeps a beautiful rendering without horizontal rulers.

### Slide backgrounds

We provide a background image syntax to specify slide's background through Markdown. Include `bg` to the alternate text.
Expand Down
3 changes: 3 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ declare module '@marp-team/marpit' {
backgroundSyntax?: boolean
container?: Element | Element[]
filters?: boolean
headingDivider?: false | MarpitHeadingDivider | MarpitHeadingDivider[]
inlineStyle?: boolean
markdown?: string | object | [string, object]
printable?: boolean
slideContainer?: Element | Element[]
inlineSVG?: boolean
}

type MarpitHeadingDivider = 1 | 2 | 3 | 4 | 5 | 6

type MarpitRenderResult = {
html: string
css: string
Expand Down
21 changes: 21 additions & 0 deletions src/helpers/parse_yaml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @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
6 changes: 6 additions & 0 deletions src/markdown/comment.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/** @module */
import parseYAML from '../helpers/parse_yaml'

const commentMatcher = /<!--+\s*([\s\S]*?)\s*--+>/
const commentMatcherOpening = /^<!--/
const commentMatcherClosing = /-->/
Expand Down Expand Up @@ -58,6 +60,10 @@ 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 }

return true
}
)
Expand Down
19 changes: 19 additions & 0 deletions src/markdown/directives/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,29 @@
* You can use prefix `$` as the name of a directive for the clarity (or
* compatibility with the old version of Marp).
*
* @prop {Directive} headingDivider Specify heading divider option.
* @prop {Directive} style Specify the CSS style to apply additionally.
* @prop {Directive} theme Specify theme of the slide deck.
*/
export const globals = {
headingDivider(value) {
const headings = [1, 2, 3, 4, 5, 6]
const toInt = v =>
Array.isArray(v) || Number.isNaN(v) ? v : Number.parseInt(v, 10)
const converted = toInt(value)

if (Array.isArray(converted)) {
const convertedArr = converted.map(toInt)
return {
headingDivider: headings.filter(v => convertedArr.includes(v)),
}
}

if (value === 'false') return { headingDivider: false }
if (headings.includes(converted)) return { headingDivider: converted }

return {}
},
style(value) {
return { style: value }
},
Expand Down
89 changes: 49 additions & 40 deletions src/markdown/directives/parse.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,8 @@
/** @module */
import MarkdownItFrontMatter from 'markdown-it-front-matter'
import YAML, { FAILSAFE_SCHEMA } from 'js-yaml'
import parseYAML from '../../helpers/parse_yaml'
import { globals, locals } from './directives'

// Parse text as YAML by using js-yaml's FAILSAFE_SCHEMA.
function parseYAMLObject(text) {
try {
const obj = YAML.safeLoad(text, { schema: FAILSAFE_SCHEMA })
if (obj === null || typeof obj !== 'object') return false

return obj
} catch (e) {
return false
}
}

/**
* Parse Marpit directives and store result to the slide token meta.
*
Expand All @@ -32,55 +20,76 @@ function parseYAMLObject(text) {
function parse(md, marpit, opts = {}) {
// Front-matter support
const frontMatter = opts.frontMatter === undefined ? true : !!opts.frontMatter
let frontMatterText
let frontMatterObject = {}

if (frontMatter) {
md.core.ruler.before('block', 'marpit_directives_front_matter', state => {
frontMatterText = undefined
frontMatterObject = {}
if (!state.inlineMode) marpit.lastGlobalDirectives = {}
})
md.use(MarkdownItFrontMatter, fm => {
frontMatterText = fm
frontMatterObject.text = fm

const yaml = parseYAML(fm)
if (yaml !== false) frontMatterObject.yaml = yaml
})
}

md.core.ruler.after('marpit_slide', 'marpit_directives_parse', state => {
// Parse global directives
md.core.ruler.after('block', 'marpit_directives_global_parse', state => {
if (state.inlineMode) return

const slides = []
const cursor = { slide: undefined, global: {}, local: {}, spot: {} }
let globalDirectives = {}
const applyDirectives = yaml => {
Object.keys(yaml).forEach(key => {
const globalKey = key.startsWith('$') ? key.slice(1) : key

const applyDirectives = text => {
const parsed = parseYAMLObject(text)
if (parsed === false) return
if (globals[globalKey])
globalDirectives = {
...globalDirectives,
...globals[globalKey](yaml[key], marpit),
}
})
}

Object.keys(parsed).forEach(key => {
const v = parsed[key]
if (frontMatterObject.yaml) applyDirectives(frontMatterObject.yaml)

// Global directives (Support prefix "$" for clarity/compatibility)
const globalKey = key.startsWith('$') ? key.slice(1) : key
if (globals[globalKey])
cursor.global = { ...cursor.global, ...globals[globalKey](v, marpit) }
state.tokens.forEach(token => {
if (token.type === 'marpit_comment' && token.meta.marpitParsedYAML)
applyDirectives(token.meta.marpitParsedYAML)
})

// Local directives
marpit.lastGlobalDirectives = { ...globalDirectives }
})

// Parse local directives and apply meta to slide
md.core.ruler.after('marpit_slide', 'marpit_directives_parse', state => {
if (state.inlineMode) return

const slides = []
const cursor = { slide: undefined, local: {}, spot: {} }

const applyDirectives = yaml => {
Object.keys(yaml).forEach(key => {
if (locals[key])
cursor.local = { ...cursor.local, ...locals[key](v, marpit) }
cursor.local = { ...cursor.local, ...locals[key](yaml[key], marpit) }

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

if (locals[spotKey])
cursor.spot = { ...cursor.spot, ...locals[spotKey](v, marpit) }
cursor.spot = {
...cursor.spot,
...locals[spotKey](yaml[key], marpit),
}
}
})
}

// At first, parse and apply YAML to cursor if assigned front-matter.
if (frontMatter && frontMatterText) applyDirectives(frontMatterText)
if (frontMatterObject.yaml) applyDirectives(frontMatterObject.yaml)

// Walk tokens to parse slides and comments.
state.tokens.forEach(token => {
if (token.meta && token.meta.marpitSlideElement === 1) {
// Initialize Marpit directives meta
Expand All @@ -97,21 +106,21 @@ function parse(md, marpit, opts = {}) {
}

cursor.spot = {}
} else if (token.type === 'marpit_comment') {
applyDirectives(token.content)
} else if (
token.type === 'marpit_comment' &&
token.meta.marpitParsedYAML
) {
applyDirectives(token.meta.marpitParsedYAML)
}
})

// Assign global directives to meta
slides.forEach(token => {
token.meta.marpitDirectives = {
...token.meta.marpitDirectives,
...cursor.global,
...marpit.lastGlobalDirectives,
}
})

// Store last parsed global directives to Marpit instance
marpit.lastGlobalDirectives = { ...cursor.global }
})
}

Expand Down
58 changes: 58 additions & 0 deletions src/markdown/heading_divider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** @module */
import Token from 'markdown-it/lib/token'
import split from '../helpers/split'

/**
* Marpit heading divider plugin.
*
* Start a new slide page at before of headings by prepending hidden `<hr>`
* elements.
*
* @alias module:markdown/heading_divider
* @param {MarkdownIt} md markdown-it instance.
* @param {Marpit} marpit Marpit instance.
*/
function headingDivider(md, marpit) {
md.core.ruler.before('marpit_slide', 'marpit_heading_divider', state => {
let target = marpit.options.headingDivider

if (
marpit.lastGlobalDirectives &&
Object.prototype.hasOwnProperty.call(
marpit.lastGlobalDirectives,
'headingDivider'
)
)
target = marpit.lastGlobalDirectives.headingDivider

if (state.inlineMode || target === false) return

if (Number.isInteger(target) && target >= 1 && target <= 6)
target = [...Array(target).keys()].map(i => i + 1)

if (!Array.isArray(target)) return

const splitTag = target.map(i => `h${i}`)
const splitFunc = t => t.type === 'heading_open' && splitTag.includes(t.tag)

state.tokens = split(state.tokens, splitFunc, true).reduce(
(arr, slideTokens) => {
const [firstToken] = slideTokens

if (
!(firstToken && splitFunc(firstToken)) ||
arr.filter(t => !t.hidden).length === 0
)
return [...arr, ...slideTokens]

const token = new Token('hr', '', 0)
token.hidden = true

return [...arr, token, ...slideTokens]
},
[]
)
})
}

export default headingDivider
Loading

0 comments on commit d08765f

Please sign in to comment.