From 06361e223902d5557cbd56f38cbaad83a2650ccb Mon Sep 17 00:00:00 2001 From: Elijah Hamovitz Date: Mon, 27 Aug 2018 13:35:26 -0700 Subject: [PATCH] Fix spreading of lists and list items Related to syntax-tree/mdast#4. Related to remarkjs/remark#349. Related to remarkjs/remark#350. Related to remarkjs/remark#364. Closes GH-23. Co-authored-by: Titus Wormer --- lib/footer.js | 31 ++-- lib/handlers/list-item.js | 58 +++++-- test/footnote-definition.js | 10 +- test/footnote.js | 291 ++++++++++++++++-------------------- test/image-reference.js | 13 ++ test/link-reference.js | 13 ++ test/list-item.js | 88 ++++++----- 7 files changed, 279 insertions(+), 225 deletions(-) diff --git a/lib/footer.js b/lib/footer.js index 3b5619b..17532a9 100644 --- a/lib/footer.js +++ b/lib/footer.js @@ -12,6 +12,9 @@ function generateFootnotes(h) { var index = -1 var listItems = [] var def + var backReference + var content + var tail if (!length) { return null @@ -19,16 +22,26 @@ function generateFootnotes(h) { while (++index < length) { def = footnotes[index] + content = def.children.concat() + tail = content[content.length - 1] + backReference = { + type: 'link', + url: '#fnref-' + def.identifier, + data: {hProperties: {className: ['footnote-backref']}}, + children: [{type: 'text', value: '↩'}] + } + + if (!tail || tail.type !== 'paragraph') { + tail = {type: 'paragraph', children: []} + content.push(tail) + } + + tail.children.push(backReference) listItems[index] = { type: 'listItem', data: {hProperties: {id: 'fn-' + def.identifier}}, - children: def.children.concat({ - type: 'link', - url: '#fnref-' + def.identifier, - data: {hProperties: {className: ['footnote-backref']}}, - children: [{type: 'text', value: '↩'}] - }), + children: content, position: def.position } } @@ -40,11 +53,7 @@ function generateFootnotes(h) { wrap( [ thematicBreak(h), - list(h, { - type: 'list', - ordered: true, - children: listItems - }) + list(h, {type: 'list', ordered: true, children: listItems}) ], true ) diff --git a/lib/handlers/list-item.js b/lib/handlers/list-item.js index e325894..24197ba 100644 --- a/lib/handlers/list-item.js +++ b/lib/handlers/list-item.js @@ -9,27 +9,40 @@ var all = require('../all') function listItem(h, node, parent) { var children = node.children var head = children[0] + var raw = all(h, node) + var loose = parent ? listLoose(parent) : listItemLoose(node) var props = {} - var single = false var result var container + var index + var length + var child - if ( - (!parent || !parent.loose) && - children.length === 1 && - head.type === 'paragraph' - ) { - single = true - } + /* Tight lists should not render 'paragraph' nodes as 'p' tags */ + if (loose) { + result = raw + } else { + result = [] + length = raw.length + index = -1 + + while (++index < length) { + child = raw[index] - result = all(h, single ? head : node) + if (child.tagName === 'p') { + result = result.concat(child.children) + } else { + result.push(child) + } + } + } if (typeof node.checked === 'boolean') { - if (!single && (!head || head.type !== 'paragraph')) { + if (loose && (!head || head.type !== 'paragraph')) { result.unshift(h(null, 'p', [])) } - container = single ? result : result[0].children + container = loose ? result[0].children : result if (container.length !== 0) { container.unshift(u('text', ' ')) @@ -47,9 +60,30 @@ function listItem(h, node, parent) { props.className = ['task-list-item'] } - if (!single && result.length !== 0) { + if (loose && result.length !== 0) { result = wrap(result, true) } return h(node, 'li', props, result) } + +function listLoose(node) { + var loose = node.spread + var children = node.children + var length = children.length + var index = -1 + + while (!loose && ++index < length) { + loose = listItemLoose(children[index]) + } + + return loose +} + +function listItemLoose(node) { + var spread = node.spread + + return spread === undefined || spread === null + ? node.children.length > 1 + : spread +} diff --git a/test/footnote-definition.js b/test/footnote-definition.js index 73c8e9c..3c8b387 100644 --- a/test/footnote-definition.js +++ b/test/footnote-definition.js @@ -7,13 +7,9 @@ var to = require('..') test('FootnoteDefinition', function(t) { t.equal( to( - u( - 'footnoteDefinition', - { - identifier: 'zulu' - }, - [u('paragraph', [u('text', 'alpha')])] - ) + u('footnoteDefinition', {identifier: 'zulu'}, [ + u('paragraph', [u('text', 'alpha')]) + ]) ), null, 'should ignore `footnoteDefinition`' diff --git a/test/footnote.js b/test/footnote.js index ae48210..fbe0a6a 100644 --- a/test/footnote.js +++ b/test/footnote.js @@ -8,72 +8,38 @@ test('Footnote', function(t) { t.deepEqual( to(u('root', [u('footnote', [u('text', 'bravo')])])), u('root', [ - u( - 'element', - { - tagName: 'sup', - properties: { - id: 'fnref-1' - } - }, - [ - u( - 'element', - { - tagName: 'a', - properties: { - href: '#fn-1', - className: ['footnote-ref'] - } - }, - [u('text', '1')] - ) - ] - ), + u('element', {tagName: 'sup', properties: {id: 'fnref-1'}}, [ + u( + 'element', + { + tagName: 'a', + properties: {href: '#fn-1', className: ['footnote-ref']} + }, + [u('text', '1')] + ) + ]), u('text', '\n'), - u( - 'element', - { - tagName: 'div', - properties: {className: ['footnotes']} - }, - [ + u('element', {tagName: 'div', properties: {className: ['footnotes']}}, [ + u('text', '\n'), + u('element', {tagName: 'hr', properties: {}}, []), + u('text', '\n'), + u('element', {tagName: 'ol', properties: {}}, [ u('text', '\n'), - u('element', {tagName: 'hr', properties: {}}, []), - u('text', '\n'), - u('element', {tagName: 'ol', properties: {}}, [ - u('text', '\n'), + u('element', {tagName: 'li', properties: {id: 'fn-1'}}, [ + u('text', 'bravo'), u( 'element', { - tagName: 'li', - properties: {id: 'fn-1'} + tagName: 'a', + properties: {href: '#fnref-1', className: ['footnote-backref']} }, - [ - u('text', '\n'), - u('element', {tagName: 'p', properties: {}}, [ - u('text', 'bravo') - ]), - u('text', '\n'), - u( - 'element', - { - tagName: 'a', - properties: { - href: '#fnref-1', - className: ['footnote-backref'] - } - }, - [u('text', '↩')] - ), - u('text', '\n') - ] - ), - u('text', '\n') + [u('text', '↩')] + ) ]), u('text', '\n') - ] - ) + ]), + u('text', '\n') + ]) ]), 'should render `footnote`s (#1)' ) @@ -81,129 +47,134 @@ test('Footnote', function(t) { t.deepEqual( to( u('root', [ - u('footnoteDefinition', {identifier: '1'}, [u('text', 'bravo')]), + u('footnoteDefinition', {identifier: '1'}, [ + u('paragraph', [u('text', 'bravo')]) + ]), u('footnoteReference', {identifier: '1'}), u('footnote', [u('text', 'charlie')]) ]) ), u('root', [ - u( - 'element', - { - tagName: 'sup', - properties: { - id: 'fnref-1' - } - }, - [ - u( - 'element', - { - tagName: 'a', - properties: { - href: '#fn-1', - className: ['footnote-ref'] - } - }, - [u('text', '1')] - ) - ] - ), + u('element', {tagName: 'sup', properties: {id: 'fnref-1'}}, [ + u( + 'element', + { + tagName: 'a', + properties: {href: '#fn-1', className: ['footnote-ref']} + }, + [u('text', '1')] + ) + ]), u('text', '\n'), - u( - 'element', - { - tagName: 'sup', - properties: { - id: 'fnref-2' - } - }, - [ - u( - 'element', - { - tagName: 'a', - properties: { - href: '#fn-2', - className: ['footnote-ref'] - } - }, - [u('text', '2')] - ) - ] - ), + u('element', {tagName: 'sup', properties: {id: 'fnref-2'}}, [ + u( + 'element', + { + tagName: 'a', + properties: {href: '#fn-2', className: ['footnote-ref']} + }, + [u('text', '2')] + ) + ]), u('text', '\n'), - u( - 'element', - { - tagName: 'div', - properties: {className: ['footnotes']} - }, - [ - u('text', '\n'), - u('element', {tagName: 'hr', properties: {}}, []), + u('element', {tagName: 'div', properties: {className: ['footnotes']}}, [ + u('text', '\n'), + u('element', {tagName: 'hr', properties: {}}, []), + u('text', '\n'), + u('element', {tagName: 'ol', properties: {}}, [ u('text', '\n'), - u('element', {tagName: 'ol', properties: {}}, [ - u('text', '\n'), + u('element', {tagName: 'li', properties: {id: 'fn-1'}}, [ + u('text', 'bravo'), u( 'element', { - tagName: 'li', - properties: {id: 'fn-1'} + tagName: 'a', + properties: {href: '#fnref-1', className: ['footnote-backref']} }, - [ - u('text', '\n'), - u('text', 'bravo'), - u('text', '\n'), - u( - 'element', - { - tagName: 'a', - properties: { - href: '#fnref-1', - className: ['footnote-backref'] - } - }, - [u('text', '↩')] - ), - u('text', '\n') - ] - ), - u('text', '\n'), + [u('text', '↩')] + ) + ]), + u('text', '\n'), + u('element', {tagName: 'li', properties: {id: 'fn-2'}}, [ + u('text', 'charlie'), u( 'element', { - tagName: 'li', - properties: {id: 'fn-2'} + tagName: 'a', + properties: {href: '#fnref-2', className: ['footnote-backref']} }, - [ - u('text', '\n'), - u('element', {tagName: 'p', properties: {}}, [ - u('text', 'charlie') - ]), - u('text', '\n'), - u( - 'element', - { - tagName: 'a', - properties: { - href: '#fnref-2', - className: ['footnote-backref'] - } - }, - [u('text', '↩')] - ), - u('text', '\n') - ] - ), - u('text', '\n') + [u('text', '↩')] + ) ]), u('text', '\n') - ] - ) + ]), + u('text', '\n') + ]) ]), 'should render `footnote`s (#2)' ) + t.deepEqual( + to( + u('root', [ + u('footnoteDefinition', {identifier: '1'}, [ + u('blockquote', [u('paragraph', [u('text', 'alpha')])]) + ]), + u('paragraph', [u('footnoteReference', {identifier: '1'})]) + ]) + ), + u('root', [ + u('element', {tagName: 'p', properties: {}}, [ + u('element', {tagName: 'sup', properties: {id: 'fnref-1'}}, [ + u( + 'element', + { + tagName: 'a', + properties: {href: '#fn-1', className: ['footnote-ref']} + }, + [u('text', '1')] + ) + ]) + ]), + u('text', '\n'), + u('element', {tagName: 'div', properties: {className: ['footnotes']}}, [ + u('text', '\n'), + u('element', {tagName: 'hr', properties: {}}, []), + u('text', '\n'), + u('element', {tagName: 'ol', properties: {}}, [ + u('text', '\n'), + u('element', {tagName: 'li', properties: {id: 'fn-1'}}, [ + u('text', '\n'), + u('element', {tagName: 'blockquote', properties: {}}, [ + u('text', '\n'), + u('element', {tagName: 'p', properties: {}}, [ + u('text', 'alpha') + ]), + u('text', '\n') + ]), + u('text', '\n'), + u('element', {tagName: 'p', properties: {}}, [ + u( + 'element', + { + tagName: 'a', + properties: { + href: '#fnref-1', + className: ['footnote-backref'] + } + }, + [u('text', '↩')] + ) + ]), + u('text', '\n') + ]), + u('text', '\n') + ]), + u('text', '\n') + ]) + ]), + 'should render `footnote`s (#3)' + ) + t.end() }) diff --git a/test/image-reference.js b/test/image-reference.js index 9eb5593..7b32948 100644 --- a/test/image-reference.js +++ b/test/image-reference.js @@ -66,6 +66,19 @@ test('ImageReference', function(t) { 'should transform `imageReference`s to `img`s' ) + t.deepEqual( + to( + u('paragraph', [ + u('imageReference', {identifier: 'november', alt: 'oscar'}), + u('definition', {identifier: 'november', url: ''}) + ]) + ), + u('element', {tagName: 'p', properties: {}}, [ + u('element', {tagName: 'img', properties: {src: '', alt: 'oscar'}}, []) + ]), + 'should transform `imageReference`s with an empty defined url to `img`s' + ) + t.deepEqual( to( u('imageReference', { diff --git a/test/link-reference.js b/test/link-reference.js index a3ee34b..8bc7131 100644 --- a/test/link-reference.js +++ b/test/link-reference.js @@ -69,6 +69,19 @@ test('LinkReference', function(t) { 'should transform `linkReference`s to `a`s' ) + t.deepEqual( + to( + u('paragraph', [ + u('linkReference', {identifier: 'juliett'}, [u('text', 'kilo')]), + u('definition', {identifier: 'juliett', url: ''}) + ]) + ), + u('element', {tagName: 'p', properties: {}}, [ + u('element', {tagName: 'a', properties: {href: ''}}, [u('text', 'kilo')]) + ]), + 'should transform `linkReference`s with an empty defined url to `a`s' + ) + t.deepEqual( to( u( diff --git a/test/list-item.js b/test/list-item.js index 4f3f1b7..70042f7 100644 --- a/test/list-item.js +++ b/test/list-item.js @@ -11,6 +11,18 @@ test('ListItem', function(t) { 'should transform tight `listItem`s to a `li` element' ) + t.deepEqual( + to( + u('listItem', {spread: true}, [u('paragraph', [u('text', 'november')])]) + ), + u('element', {tagName: 'li', properties: {}}, [ + u('text', '\n'), + u('element', {tagName: 'p', properties: {}}, [u('text', 'november')]), + u('text', '\n') + ]), + 'should transform a spreaded `listItem`s to a `li` element' + ) + t.deepEqual( to( u('listItem', [ @@ -35,11 +47,7 @@ test('ListItem', function(t) { 'element', { tagName: 'input', - properties: { - type: 'checkbox', - checked: true, - disabled: true - } + properties: {type: 'checkbox', checked: true, disabled: true} }, [] ), @@ -63,11 +71,7 @@ test('ListItem', function(t) { 'element', { tagName: 'input', - properties: { - type: 'checkbox', - checked: false, - disabled: true - } + properties: {type: 'checkbox', checked: false, disabled: true} }, [] ), @@ -83,6 +87,26 @@ test('ListItem', function(t) { t.deepEqual( to(u('listItem', {checked: true}, [u('html', '')])), + u('element', {tagName: 'li', properties: {className: ['task-list-item']}}, [ + u( + 'element', + { + tagName: 'input', + properties: {type: 'checkbox', checked: true, disabled: true} + }, + [] + ) + ]), + 'should support checkboxes in `listItem`s without paragraph' + ) + + t.deepEqual( + to( + u('listItem', {checked: false}, [ + u('blockquote', [u('paragraph', [u('text', 'romeo')])]), + u('paragraph', [u('text', 'sierra')]) + ]) + ), u('element', {tagName: 'li', properties: {className: ['task-list-item']}}, [ u('text', '\n'), u('element', {tagName: 'p', properties: {}}, [ @@ -90,18 +114,22 @@ test('ListItem', function(t) { 'element', { tagName: 'input', - properties: { - type: 'checkbox', - checked: true, - disabled: true - } + properties: {type: 'checkbox', checked: false, disabled: true} }, [] ) ]), + u('text', '\n'), + u('element', {tagName: 'blockquote', properties: {}}, [ + u('text', '\n'), + u('element', {tagName: 'p', properties: {}}, [u('text', 'romeo')]), + u('text', '\n') + ]), + u('text', '\n'), + u('element', {tagName: 'p', properties: {}}, [u('text', 'sierra')]), u('text', '\n') ]), - 'should support checkboxes in `listItem`s without paragraph' + 'should support checkboxes in loose `listItem`s without paragraphs' ) t.deepEqual( @@ -113,22 +141,14 @@ test('ListItem', function(t) { t.deepEqual( to(u('listItem', {checked: true}, [])), u('element', {tagName: 'li', properties: {className: ['task-list-item']}}, [ - u('text', '\n'), - u('element', {tagName: 'p', properties: {}}, [ - u( - 'element', - { - tagName: 'input', - properties: { - type: 'checkbox', - checked: true, - disabled: true - } - }, - [] - ) - ]), - u('text', '\n') + u( + 'element', + { + tagName: 'input', + properties: {type: 'checkbox', checked: true, disabled: true} + }, + [] + ) ]), 'should support checkboxes in `listItem`s without children' ) @@ -142,13 +162,11 @@ test('ListItem', function(t) { ]) ), u('element', {tagName: 'li', properties: {}}, [ - u('text', '\n'), u('element', {tagName: 'ul', properties: {}}, [ u('text', '\n'), u('element', {tagName: 'li', properties: {}}, [u('text', 'Alpha')]), u('text', '\n') - ]), - u('text', '\n') + ]) ]), 'should support lists in `listItem`s' )