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

Add header and footer directives #22

Merged
merged 11 commits into from
May 14, 2018
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

* Add `header` and `footer` directives ([#22](https://github.com/marp-team/marpit/pull/22))

## v0.0.5 - 2018-05-12

* Add `paginate` local directive ([#17](https://github.com/marp-team/marpit/pull/17))
Expand Down
89 changes: 83 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,57 @@ Simply you have to move a definition of `paginate` directive to an inside of a s
It will paginate slide from a this page.
```

#### Header and footer

When you have to be shown the same content across multiple slides like a title of the slide deck, you can use `header` or `footer` local directives.

```markdown
---
header: "Header content"
footer: "Footer content"
---

# Page 1

---

## Page 2
```

In above case, it will render to HTML like this:

```html
<section>
<header>Header content</header>
<h1>Page 1</h1>
<footer>Footer content</footer>
</section>
<section>
<header>Header content</header>
<h2>Page 2</h2>
<footer>Footer content</footer>
</section>
```

The specified contents will wrap by a corresponding element, and insert to a right place of each slide.

If you want to place these contents in the marginals of the slide, **you have to use a theme that is supported it.** If not, you could simply see header and footer as the part of slide content.

##### Styling header and footer

In addition, you can format the header and footer content with inline styling through markdown syntax. You can also insert inline images.

```html
---
header: "**bold** _italic_"
footer: "![image](https://example.com/image.jpg)"
---
```

> :warning: Marpit uses YAML for parsing directives, so **you should wrap with quotes** when the value includes invalid chars in YAML.

> :information_source: Due to the parsing order of Markdown, you cannot use [slide background images](#slide-background) in `header` and `footer` directives.

### Slide backgrounds

We provide a background image syntax to specify slide's background through Markdown. Include `bg` to the alternate text.
Expand Down Expand Up @@ -119,8 +170,6 @@ This feature is available regardless of `backgroundSyntax` option in Marpit cons
<!-- _backgroundImage: "linear-gradient(to bottom, #67b8e3, #0288d1)" -->
```

Marpit uses YAML for parsing directives, so you should wrap by quote when the value includes space.

##### Directives

| Spot directive | Description | Default |
Expand Down Expand Up @@ -205,10 +254,6 @@ Naturally multiple filters can apply to a image.
![brightness:.8 sepia:50%](https://example.com/image.jpg)
```

### ToDo

* [ ] Header and footer directive

## Markup

### HTML output
Expand Down Expand Up @@ -295,6 +340,38 @@ Please refer to [the default style of `section::after` in a scaffold theme](src/

> :information_source: The root `section::after` has preserved a content of page number from Marpit. At present, you cannot use the root `section::after` selector for other use.

#### Header and footer

`header` element and `footer` element have a possible to be rendered by [local directives](#header-and-footer). _Marpit has no default style for these elements._

If you want to place to marginals of slide, using `position: absolute` would be a good solution.

```css
section {
padding: 50px;
}

header,
footer {
position: absolute;
left: 50px;
right: 50px;
height: 20px;
}

header {
top: 30px;
}

footer {
bottom: 30px;
}
```

Of course, you can use the other way as needed (Flexbox, Grid, etc...).

You can even hide by `display: none` when you are scared a corrupted layout caused by inserted elements. Poof!

#### Theme set

The `Marpit` instance has a `themeSet` member that manages usable themes in the `theme` directive of Marpit Markdown. You have to add theme CSS by using `themeSet.add(string)`.
Expand Down
6 changes: 6 additions & 0 deletions src/markdown/directives/apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ function apply(md, opts = {}) {
if (marpitDirectives.paginate)
token.attrSet('data-marpit-pagination', marpitSlide + 1)

if (marpitDirectives.header)
token.meta.marpitHeader = marpitDirectives.header

if (marpitDirectives.footer)
token.meta.marpitFooter = marpitDirectives.footer

const styleStr = style.toString()
if (styleStr !== '') token.attrSet('style', styleStr)
})
Expand Down
10 changes: 10 additions & 0 deletions src/markdown/directives/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export const globals = {
* @prop {Directive} backgroundSize Specify background-size style. The default
* value while setting backgroundImage is `cover`.
* @prop {Directive} class Specify HTML class of section element(s).
* @prop {Directive} footer Specify the content of slide footer. It will insert
* a `<footer>` element to the last of each slide contents.
* @prop {Directive} header Specify the content of slide header. It will insert
* a `<header>` element to the first of each slide contents.
* @prop {Directive} pagination Show page number on the slide if you set `true`.
*/
export const locals = {
Expand All @@ -63,6 +67,12 @@ export const locals = {
class(value) {
return { class: value }
},
footer(value) {
return { footer: value }
},
header(value) {
return { header: value }
},
paginate(value) {
return { paginate: (value || '').toLowerCase() === 'true' }
},
Expand Down
68 changes: 68 additions & 0 deletions src/markdown/header_and_footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/** @module */
import Token from 'markdown-it/lib/token'
import wrapTokens from '../helpers/wrap_tokens'

/**
* Marpit header and footer plugin.
*
* At each slide, add header and footer that are provided by directives.
*
* @alias module:markdown/header_and_footer
* @param {MarkdownIt} md markdown-it instance.
*/
function headerAndFooter(md) {
md.core.ruler.after(
'marpit_directives_apply',
'marpit_header_and_footer',
state => {
if (state.inlineMode) return

const renderedInlines = new Map()
const getRendered = markdown => {
let rendered = renderedInlines.get(markdown)

if (!rendered) {
rendered = md.renderInline(markdown, state.env)
renderedInlines.set(markdown, rendered)
}

return rendered
}

const createMarginalTokens = (tag, markdown) => {
const token = new Token('html_block', '', 0)
token.content = getRendered(markdown)

return wrapTokens(`marpit_${tag}`, { tag, close: { block: true } }, [
token,
])
}

let current

state.tokens = state.tokens.reduce((arr, token) => {
let concats = [token]

if (token.type === 'marpit_slide_open') {
current = token

if (current.meta && current.meta.marpitHeader)
concats = [
...concats,
...createMarginalTokens('header', current.meta.marpitHeader),
]
} else if (token.type === 'marpit_slide_close') {
if (current.meta && current.meta.marpitFooter)
concats = [
...createMarginalTokens('footer', current.meta.marpitFooter),
...concats,
]
}

return [...arr, ...concats]
}, [])
}
)
}

export default headerAndFooter
2 changes: 2 additions & 0 deletions src/marpit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import marpitApplyDirectives from './markdown/directives/apply'
import marpitBackgroundImage from './markdown/background_image'
import marpitComment from './markdown/comment'
import marpitContainerPlugin from './markdown/container'
import marpitHeaderAndFooter from './markdown/header_and_footer'
import marpitInlineSVG from './markdown/inline_svg'
import marpitParseDirectives from './markdown/directives/parse'
import marpitParseImage from './markdown/parse_image'
Expand Down Expand Up @@ -89,6 +90,7 @@ class Marpit {
.use(marpitSlide)
.use(marpitParseDirectives, this)
.use(marpitApplyDirectives)
.use(marpitHeaderAndFooter)
.use(marpitSlideContainer, this.slideContainers)
.use(marpitContainerPlugin, this.containers)
.use(marpitParseImage, { filters: this.options.filters })
Expand Down
97 changes: 97 additions & 0 deletions test/markdown/header_and_footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import assert from 'assert'
import cheerio from 'cheerio'
import MarkdownIt from 'markdown-it'
import applyDirectives from '../../src/markdown/directives/apply'
import comment from '../../src/markdown/comment'
import parseDirectives from '../../src/markdown/directives/parse'
import slide from '../../src/markdown/slide'
import headerAndFooter from '../../src/markdown/header_and_footer'

describe('Marpit header and footer plugin', () => {
const themeSet = new Map()
themeSet.set('test_theme', true)

const marpitStub = (props = {}) => ({
themeSet,
lastGlobalDirectives: {},
...props,
})

const md = (marpitInstance = marpitStub()) =>
new MarkdownIt('commonmark')
.use(comment)
.use(slide)
.use(parseDirectives, { themeSet: marpitInstance.themeSet })
.use(applyDirectives)
.use(headerAndFooter)

describe('Header local directive', () => {
const markdown = header =>
`<!-- header: "${header}" -->\n# Page 1\n\n---\n\n# Page 2`

it('appends <header> element to each slide', () => {
const $ = cheerio.load(md().render(markdown('text')))

$('section').each((i, elm) => {
const children = $(elm).children()
const firstChild = children.first()

assert(firstChild.get(0).tagName === 'header')
assert(firstChild.html() === 'text')
})
})

it('renders tags when it includes inline markdown syntax', () => {
const mdText = '**bold** _italic_ ![image](https://example.com/image.jpg)'
const $ = cheerio.load(md().render(markdown(mdText)))

$('section').each((i, elm) => {
const header = $(elm)
.children()
.first()

const img = header.find('img')

assert(header.find('strong').length === 1)
assert(header.find('em').length === 1)
assert(img.length === 1)
assert(img.attr('src') === 'https://example.com/image.jpg')
})
})
})

describe('Footer local directive', () => {
const markdown = footer =>
`<!-- footer: "${footer}" -->\n# Page 1\n\n---\n\n# Page 2`

it('prepends <footer> element to each slide', () => {
const $ = cheerio.load(md().render(markdown('text')))

$('section').each((i, elm) => {
const children = $(elm).children()
const lastChild = children.last()

assert(lastChild.get(0).tagName === 'footer')
assert(lastChild.html() === 'text')
})
})

it('renders tags when it includes inline markdown syntax', () => {
const mdText = '**bold** _italic_ ![image](https://example.com/image.jpg)'
const $ = cheerio.load(md().render(markdown(mdText)))

$('section').each((i, elm) => {
const footer = $(elm)
.children()
.last()

const img = footer.find('img')

assert(footer.find('strong').length === 1)
assert(footer.find('em').length === 1)
assert(img.length === 1)
assert(img.attr('src') === 'https://example.com/image.jpg')
})
})
})
})