From ebf404e4ae9261228dc899927eb4a42c884cd72b Mon Sep 17 00:00:00 2001 From: David Date: Fri, 22 Dec 2023 13:49:26 +0100 Subject: [PATCH] #1486 fix newline items in lists (#1487) * adds test case with line-breaks in list items reproduces #1486 * use LineBreaker per item and only draw `firstLine` once fixes #1486 * boyscout: consistent naming * boyscout: test multiple line break versions * update changelog for fix of #1486 --------- Co-authored-by: David --- CHANGELOG.md | 1 + lib/mixins/text.js | 123 +++++++++--------- ...-list-with-line-breaks-in-items-1-snap.png | Bin 0 -> 19096 bytes tests/visual/text.spec.js | 7 + 4 files changed, 73 insertions(+), 58 deletions(-) create mode 100644 tests/visual/__image_snapshots__/text-spec-js-text-list-with-line-breaks-in-items-1-snap.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 8484fc9a..a24e1063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Unreleased - Add subset for PDF/UA +- Fix for line breaks in list items (#1486) ### [v0.14.0] - 2023-11-09 diff --git a/lib/mixins/text.js b/lib/mixins/text.js index a1c7f425..d78f4465 100644 --- a/lib/mixins/text.js +++ b/lib/mixins/text.js @@ -153,73 +153,80 @@ export default { } }; - wrapper = new LineWrapper(this, options); - wrapper.on('line', this._line); + const drawListItem = function(listItem) { + wrapper = new LineWrapper(this, options); + wrapper.on('line', this._line); + + level = 1; + let i = 0; + wrapper.once('firstLine', () => { + let item, itemType, labelType, bodyType; + if (options.structParent) { + if (options.structTypes) { + [itemType, labelType, bodyType] = options.structTypes; + } else { + [itemType, labelType, bodyType] = ['LI', 'Lbl', 'LBody']; + } + } - level = 1; - let i = 0; - wrapper.on('firstLine', () => { - let item, itemType, labelType, bodyType; - if (options.structParent) { - if (options.structTypes) { - [ itemType, labelType, bodyType ] = options.structTypes; - } else { - [ itemType, labelType, bodyType ] = [ 'LI', 'Lbl', 'LBody' ]; + if (itemType) { + item = this.struct(itemType); + options.structParent.add(item); + } else if (options.structParent) { + item = options.structParent; } - } - if (itemType) { - item = this.struct(itemType); - options.structParent.add(item); - } else if (options.structParent) { - item = options.structParent; - } + let l; + if ((l = levels[i++]) !== level) { + const diff = itemIndent * (l - level); + this.x += diff; + wrapper.lineWidth -= diff; + level = l; + } - let l; - if ((l = levels[i++]) !== level) { - const diff = itemIndent * (l - level); - this.x += diff; - wrapper.lineWidth -= diff; - level = l; - } + if (item && (labelType || bodyType)) { + item.add(this.struct(labelType || bodyType, + [this.markStructureContent(labelType || bodyType)])); + } + switch (listType) { + case 'bullet': + this.circle(this.x - indent + r, this.y + midLine, r); + this.fill(); + break; + case 'numbered': + case 'lettered': + var text = label(numbers[i - 1]); + this._fragment(text, this.x - indent, this.y, options); + break; + } - if (item && (labelType || bodyType)) { - item.add(this.struct(labelType || bodyType, - [ this.markStructureContent(labelType || bodyType) ])); - } - switch (listType) { - case 'bullet': - this.circle(this.x - indent + r, this.y + midLine, r); - this.fill(); - break; - case 'numbered': - case 'lettered': - var text = label(numbers[i - 1]); - this._fragment(text, this.x - indent, this.y, options); - break; - } + if (item && labelType && bodyType) { + item.add(this.struct(bodyType, [this.markStructureContent(bodyType)])); + } + if (item && item !== options.structParent) { + item.end(); + } + }); - if (item && labelType && bodyType) { - item.add(this.struct(bodyType, [ this.markStructureContent(bodyType) ])); - } - if (item && item !== options.structParent) { - item.end(); - } - }); + wrapper.on('sectionStart', () => { + const pos = indent + itemIndent * (level - 1); + this.x += pos; + return (wrapper.lineWidth -= pos); + }); - wrapper.on('sectionStart', () => { - const pos = indent + itemIndent * (level - 1); - this.x += pos; - return (wrapper.lineWidth -= pos); - }); + wrapper.on('sectionEnd', () => { + const pos = indent + itemIndent * (level - 1); + this.x -= pos; + return (wrapper.lineWidth += pos); + }); + + wrapper.wrap(listItem, options); + }; - wrapper.on('sectionEnd', () => { - const pos = indent + itemIndent * (level - 1); - this.x -= pos; - return (wrapper.lineWidth += pos); - }); - wrapper.wrap(items.join('\n'), options); + for (let i = 0; i < items.length; i++) { + drawListItem.call(this, items[i]); + } return this; }, diff --git a/tests/visual/__image_snapshots__/text-spec-js-text-list-with-line-breaks-in-items-1-snap.png b/tests/visual/__image_snapshots__/text-spec-js-text-list-with-line-breaks-in-items-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..fb76940d0c9c37037205b797b14c91be516ffcb1 GIT binary patch literal 19096 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 HsZ0L{{K)PH literal 0 HcmV?d00001 diff --git a/tests/visual/text.spec.js b/tests/visual/text.spec.js index ec6de8e6..e4e089da 100644 --- a/tests/visual/text.spec.js +++ b/tests/visual/text.spec.js @@ -43,4 +43,11 @@ describe('text', function() { doc.fillColor('#000').list(['One', 'Two', 'Three'], 100, 150); }); }); + + test('list with line breaks in items', function() { + return runDocTest(function(doc) { + doc.font('tests/fonts/Roboto-Regular.ttf'); + doc.list(['Foo\nBar', 'Foo\rBar', 'Foo\r\nBar'], [100, 150]); + }) + }) });