diff --git a/CHANGELOG.md b/CHANGELOG.md index 3223b8bf..b7ad3d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Parse lists in `*` and `1)` marker as fragmented list ([#145](https://github.com/marp-team/marpit/issues/145), [#148](https://github.com/marp-team/marpit/pull/148)) + ### Changed - Upgrade dependent packages to the latest version ([#143](https://github.com/marp-team/marpit/pull/143)) diff --git a/src/markdown/fragment.js b/src/markdown/fragment.js new file mode 100644 index 00000000..9c9e0294 --- /dev/null +++ b/src/markdown/fragment.js @@ -0,0 +1,52 @@ +/** @module */ +import marpitPlugin from './marpit_plugin' + +const fragmentedListMarkups = ['*', ')'] + +/** + * Marpit fragment plugin. + * + * @alias module:markdown/fragment + * @param {MarkdownIt} md markdown-it instance. + */ +function fragment(md) { + // Fragmented list + md.core.ruler.after('marpit_directives_parse', 'marpit_fragment', state => { + if (state.inlineMode) return + + for (const token of state.tokens) { + if ( + token.type === 'list_item_open' && + fragmentedListMarkups.includes(token.markup) + ) { + token.meta = token.meta || {} + token.meta.marpitFragment = true + } + } + }) + + // Add data-marpit-fragment(s) attributes to token + md.core.ruler.after('marpit_fragment', 'marpit_apply_fragment', state => { + if (state.inlineMode) return + + const fragments = { slide: undefined, count: 0 } + + for (const token of state.tokens) { + if (token.meta && token.meta.marpitSlideElement === 1) { + fragments.slide = token + fragments.count = 0 + } else if (token.meta && token.meta.marpitSlideElement === -1) { + if (fragments.slide && fragments.count > 0) { + fragments.slide.attrSet('data-marpit-fragments', fragments.count) + } + } else if (token.meta && token.meta.marpitFragment) { + fragments.count += 1 + + token.meta.marpitFragment = fragments.count + token.attrSet('data-marpit-fragment', fragments.count) + } + } + }) +} + +export default marpitPlugin(fragment) diff --git a/src/marpit.js b/src/marpit.js index 025ee675..9c3f7038 100644 --- a/src/marpit.js +++ b/src/marpit.js @@ -7,6 +7,7 @@ import marpitBackgroundImage from './markdown/background_image' import marpitCollect from './markdown/collect' import marpitComment from './markdown/comment' import marpitContainerPlugin from './markdown/container' +import marpitFragment from './markdown/fragment' import marpitHeaderAndFooter from './markdown/header_and_footer' import marpitHeadingDivider from './markdown/heading_divider' import marpitInlineSVG from './markdown/inline_svg' @@ -179,8 +180,9 @@ class Marpit { .use(marpitSweep) .use(marpitInlineSVG) .use(marpitStyleAssign) - .use(marpitCollect) .use(marpitBackgroundImage) + .use(marpitFragment) + .use(marpitCollect) } /** diff --git a/test/markdown/fragment.js b/test/markdown/fragment.js new file mode 100644 index 00000000..a5222029 --- /dev/null +++ b/test/markdown/fragment.js @@ -0,0 +1,84 @@ +import cheerio from 'cheerio' +import MarkdownIt from 'markdown-it' +import fragment from '../../src/markdown/fragment' +import slide from '../../src/markdown/slide' + +describe('Marpit fragment plugin', () => { + const md = () => { + const instance = new MarkdownIt('commonmark') + instance.marpit = {} + + return instance + .use(slide) + .use(m => m.core.ruler.push('marpit_directives_parse', () => {})) + .use(fragment) + } + + describe('Fragmented unordered list', () => { + context('when using "*" markup', () => { + const markdown = '* A\n* B\n* C' + const $ = cheerio.load(md().render(markdown)) + + it('adds data-marpit-fragment attribute to
  • with index', () => { + const li = $('ul > li[data-marpit-fragment]') + expect(li).toHaveLength(3) + + const indexes = li.map((_, el) => $(el).data('marpit-fragment')).get() + expect(indexes).toStrictEqual([1, 2, 3]) + }) + + it('adds data-marpit-fragments attribute to
    with count of fragments', () => { + const section = $('section[data-marpit-fragments]') + + expect(section).toHaveLength(1) + expect(section.data('marpit-fragments')).toBe(3) + }) + }) + + for (const char of ['-', '+']) { + context(`when using "${char}" markup`, () => { + const markdown = `${char} A\n${char} B\n${char} C` + const $ = cheerio.load(md().render(markdown)) + + it('does not add data-marpit-fragment attribute', () => + expect($('[data-marpit-fragment]')).toHaveLength(0)) + + it('does not add data-marpit-fragments attribute', () => + expect($('[data-marpit-fragments]')).toHaveLength(0)) + }) + } + }) + + describe('Fragmented ordered list', () => { + context('when using "1)" markup', () => { + const markdown = '1) A\n1) B\n1) C' + const $ = cheerio.load(md().render(markdown)) + + it('adds data-marpit-fragment attribute to
  • with index', () => { + const li = $('ol > li[data-marpit-fragment]') + expect(li).toHaveLength(3) + + const indexes = li.map((_, el) => $(el).data('marpit-fragment')).get() + expect(indexes).toStrictEqual([1, 2, 3]) + }) + + it('adds data-marpit-fragments attribute to
    with count of fragments', () => { + const section = $('section[data-marpit-fragments]') + + expect(section).toHaveLength(1) + expect(section.data('marpit-fragments')).toBe(3) + }) + }) + + context(`when using "1." markup`, () => { + const markdown = `1. A\n1. B\n1. C` + const $ = cheerio.load(md().render(markdown)) + + it('does not add data-marpit-fragment attribute', () => + expect($('[data-marpit-fragment]')).toHaveLength(0)) + + it('does not add data-marpit-fragments attribute', () => + expect($('[data-marpit-fragments]')).toHaveLength(0)) + }) + }) +})