From 08477f11ed7b40e89d0aceac3980e77d31de3ddb Mon Sep 17 00:00:00 2001 From: diegomura Date: Sat, 2 Jul 2022 02:20:50 -0300 Subject: [PATCH 1/3] refactor: build textkit with rollup & define public api --- packages/layout/src/svg/layoutText.js | 32 ++++++++------- packages/layout/src/text/fromFragments.js | 27 ++++++++++++ .../layout/src/text/getAttributedString.js | 2 +- packages/layout/src/text/layoutText.js | 13 +++--- packages/layout/src/text/linesWidth.js | 5 +-- .../layout/tests/text/fromFragments.test.js | 41 +++++++++++++++++++ packages/layout/tests/text/layoutText.test.js | 5 +-- .../render/src/primitives/renderSvgText.js | 7 +--- packages/render/src/primitives/renderText.js | 20 ++++----- packages/textkit/babel.config.js | 4 +- packages/textkit/package.json | 7 ++-- packages/textkit/rollup.config.js | 37 +++++++++++++++++ packages/textkit/src/index.js | 6 +-- .../textkit/src/layout/finalizeFragments.js | 34 +++++++++++++++ 14 files changed, 186 insertions(+), 54 deletions(-) create mode 100644 packages/layout/src/text/fromFragments.js create mode 100644 packages/layout/tests/text/fromFragments.test.js create mode 100644 packages/textkit/rollup.config.js diff --git a/packages/layout/src/svg/layoutText.js b/packages/layout/src/svg/layoutText.js index 6aa63ab48..aabf49549 100644 --- a/packages/layout/src/svg/layoutText.js +++ b/packages/layout/src/svg/layoutText.js @@ -1,12 +1,13 @@ import * as P from '@react-pdf/primitives'; -import layoutEngine from '@react-pdf/textkit/lib/layout'; -import linebreaker from '@react-pdf/textkit/lib/engines/linebreaker'; -import justification from '@react-pdf/textkit/lib/engines/justification'; -import scriptItemizer from '@react-pdf/textkit/lib/engines/scriptItemizer'; -import wordHyphenation from '@react-pdf/textkit/lib/engines/wordHyphenation'; -import decorationEngine from '@react-pdf/textkit/lib/engines/textDecoration'; -import fromFragments from '@react-pdf/textkit/lib/attributedString/fromFragments'; +import layoutEngine, { + linebreaker, + justification, + scriptItemizer, + wordHyphenation, + textDecoration, +} from '@react-pdf/textkit'; +import fromFragments from '../text/fromFragments'; import transformText from '../text/transformText'; import fontSubstitution from '../text/fontSubstitution'; @@ -15,10 +16,10 @@ const isTextInstance = node => node.type === P.TextInstance; const engines = { linebreaker, justification, + textDecoration, scriptItemizer, wordHyphenation, fontSubstitution, - textDecoration: decorationEngine, }; const engine = layoutEngine(engines); @@ -34,13 +35,14 @@ const getFragments = (fontStore, instance) => { fontWeight, fontStyle, fontSize = 18, - textDecoration, textDecorationColor, textDecorationStyle, textTransform, opacity, } = instance.props; + const _textDecoration = instance.props.textDecoration; + const obj = fontStore ? fontStore.getFont({ fontFamily, fontWeight, fontStyle }) : null; @@ -53,14 +55,14 @@ const getFragments = (fontStore, instance) => { color: fill, underlineStyle: textDecorationStyle, underline: - textDecoration === 'underline' || - textDecoration === 'underline line-through' || - textDecoration === 'line-through underline', + _textDecoration === 'underline' || + _textDecoration === 'underline line-through' || + _textDecoration === 'line-through underline', underlineColor: textDecorationColor || fill, strike: - textDecoration === 'line-through' || - textDecoration === 'underline line-through' || - textDecoration === 'line-through underline', + _textDecoration === 'line-through' || + _textDecoration === 'underline line-through' || + _textDecoration === 'line-through underline', strikeStyle: textDecorationStyle, strikeColor: textDecorationColor || fill, }; diff --git a/packages/layout/src/text/fromFragments.js b/packages/layout/src/text/fromFragments.js new file mode 100644 index 000000000..cca715ebe --- /dev/null +++ b/packages/layout/src/text/fromFragments.js @@ -0,0 +1,27 @@ +/** + * Create attributed string from text fragments + * + * @param {Array} fragments + * @return {Object} attributed string + */ +const fromFragments = fragments => { + let offset = 0; + let string = ''; + const runs = []; + + fragments.forEach(fragment => { + string += fragment.string; + + runs.push({ + start: offset, + end: offset + fragment.string.length, + attributes: fragment.attributes || {}, + }); + + offset += fragment.string.length; + }); + + return { string, runs }; +}; + +export default fromFragments; diff --git a/packages/layout/src/text/getAttributedString.js b/packages/layout/src/text/getAttributedString.js index 135655ede..cce85ba0f 100644 --- a/packages/layout/src/text/getAttributedString.js +++ b/packages/layout/src/text/getAttributedString.js @@ -1,8 +1,8 @@ import * as P from '@react-pdf/primitives'; -import fromFragments from '@react-pdf/textkit/lib/attributedString/fromFragments'; import { embedEmojis } from './emoji'; import ignoreChars from './ignoreChars'; +import fromFragments from './fromFragments'; import transformText from './transformText'; const PREPROCESSORS = [ignoreChars, embedEmojis]; diff --git a/packages/layout/src/text/layoutText.js b/packages/layout/src/text/layoutText.js index f00913636..35a822183 100644 --- a/packages/layout/src/text/layoutText.js +++ b/packages/layout/src/text/layoutText.js @@ -1,9 +1,10 @@ -import layoutEngine from '@react-pdf/textkit/lib/layout'; -import linebreaker from '@react-pdf/textkit/lib/engines/linebreaker'; -import justification from '@react-pdf/textkit/lib/engines/justification'; -import textDecoration from '@react-pdf/textkit/lib/engines/textDecoration'; -import scriptItemizer from '@react-pdf/textkit/lib/engines/scriptItemizer'; -import wordHyphenation from '@react-pdf/textkit/lib/engines/wordHyphenation'; +import layoutEngine, { + linebreaker, + justification, + scriptItemizer, + wordHyphenation, + textDecoration, +} from '@react-pdf/textkit'; import fontSubstitution from './fontSubstitution'; import getAttributedString from './getAttributedString'; diff --git a/packages/layout/src/text/linesWidth.js b/packages/layout/src/text/linesWidth.js index 0b4d6eaaf..775b352fa 100644 --- a/packages/layout/src/text/linesWidth.js +++ b/packages/layout/src/text/linesWidth.js @@ -1,5 +1,3 @@ -import advanceWidth from '@react-pdf/textkit/lib/attributedString/advanceWidth'; - /** * Get lines width (if any) * @@ -8,7 +6,8 @@ import advanceWidth from '@react-pdf/textkit/lib/attributedString/advanceWidth'; */ const linesWidth = node => { if (!node.lines) return 0; - return Math.max(0, ...node.lines.map(line => advanceWidth(line))); + + return Math.max(0, ...node.lines.map(line => line.xAdvance)); }; export default linesWidth; diff --git a/packages/layout/tests/text/fromFragments.test.js b/packages/layout/tests/text/fromFragments.test.js new file mode 100644 index 000000000..7e4d88b74 --- /dev/null +++ b/packages/layout/tests/text/fromFragments.test.js @@ -0,0 +1,41 @@ +import fromFragments from '../../src/text/fromFragments'; + +describe('attributeString fromFragments operator', () => { + test('should return empty attributed string for no fragments', () => { + const attributedString = fromFragments([]); + + expect(attributedString.string).toBe(''); + expect(attributedString.runs).toHaveLength(0); + }); + + test('should be constructed by one fragment', () => { + const attributedString = fromFragments([{ string: 'Hey' }]); + + expect(attributedString.string).toBe('Hey'); + expect(attributedString.runs[0]).toHaveProperty('start', 0); + expect(attributedString.runs[0]).toHaveProperty('end', 3); + }); + + test('should be constructed by fragments', () => { + const attributedString = fromFragments([ + { string: 'Hey' }, + { string: ' ho' }, + ]); + + expect(attributedString.string).toBe('Hey ho'); + expect(attributedString.runs[0]).toHaveProperty('start', 0); + expect(attributedString.runs[0]).toHaveProperty('end', 3); + expect(attributedString.runs[1]).toHaveProperty('start', 3); + expect(attributedString.runs[1]).toHaveProperty('end', 6); + }); + + test('should preserve fragment attributes', () => { + const attributedString = fromFragments([ + { string: 'Hey', attributes: { attr: 1 } }, + { string: ' ho', attributes: { attr: 2 } }, + ]); + + expect(attributedString.runs[0]).toHaveProperty('attributes', { attr: 1 }); + expect(attributedString.runs[1]).toHaveProperty('attributes', { attr: 2 }); + }); +}); diff --git a/packages/layout/tests/text/layoutText.test.js b/packages/layout/tests/text/layoutText.test.js index 816df8b1c..1fdeecc64 100644 --- a/packages/layout/tests/text/layoutText.test.js +++ b/packages/layout/tests/text/layoutText.test.js @@ -1,5 +1,4 @@ import * as P from '@react-pdf/primitives'; -import runWidth from '@react-pdf/textkit/lib/run/advanceWidth'; import layoutText from '../../src/text/layoutText'; @@ -38,7 +37,7 @@ describe('text layoutText', () => { test('Should render aligned right text', async () => { const node = createTextNode(TEXT, { textAlign: 'right' }); const lines = layoutText(node, 1500, 30, null); - const textWidth = runWidth(lines[0].runs[0]); + const textWidth = lines[0].runs[0].xAdvance; expect(lines[0].box.x).toBe(1500 - textWidth); }); @@ -46,7 +45,7 @@ describe('text layoutText', () => { test('Should render aligned center text', async () => { const node = createTextNode(TEXT, { textAlign: 'center' }); const lines = layoutText(node, 1500, 30, null); - const textWidth = runWidth(lines[0].runs[0]); + const textWidth = lines[0].runs[0].xAdvance; expect(lines[0].box.x).toBe((1500 - textWidth) / 2); }); diff --git a/packages/render/src/primitives/renderSvgText.js b/packages/render/src/primitives/renderSvgText.js index 94f766e0f..f4da66f8c 100644 --- a/packages/render/src/primitives/renderSvgText.js +++ b/packages/render/src/primitives/renderSvgText.js @@ -1,10 +1,7 @@ -import runWidth from '@react-pdf/textkit/lib/run/advanceWidth'; -import lineWidth from '@react-pdf/textkit/lib/attributedString/advanceWidth'; - import renderGlyphs from './renderGlyphs'; const renderRun = (ctx, run) => { - const runAdvanceWidth = runWidth(run); + const runAdvanceWidth = run.xAdvance; const { font, fontSize, color, opacity } = run.attributes; ctx.fillColor(color); @@ -48,7 +45,7 @@ const renderSpan = (ctx, line, textAnchor, dominantBaseline) => { const y = line.box?.y || 0; const font = line.runs[0]?.attributes.font; const scale = line.runs[0]?.attributes?.scale || 1; - const width = lineWidth(line); + const width = line.xAdvance; const ascent = font.ascent * scale; const xHeight = font.xHeight * scale; diff --git a/packages/render/src/primitives/renderText.js b/packages/render/src/primitives/renderText.js index 8d83177a9..0f3e8c7f0 100644 --- a/packages/render/src/primitives/renderText.js +++ b/packages/render/src/primitives/renderText.js @@ -1,9 +1,5 @@ /* eslint-disable no-param-reassign */ import { isNil } from '@react-pdf/fns'; -import runHeight from '@react-pdf/textkit/lib/run/height'; -import runDescent from '@react-pdf/textkit/lib/run/descent'; -import advanceWidth from '@react-pdf/textkit/lib/run/advanceWidth'; -import ascent from '@react-pdf/textkit/lib/attributedString/ascent'; import renderGlyphs from './renderGlyphs'; import parseColor from '../utils/parseColor'; @@ -56,12 +52,10 @@ const renderRun = (ctx, run, options) => { ? color.opacity : run.attributes.opacity; - const height = runHeight(run); - const descent = runDescent(run); - const runAdvanceWidth = advanceWidth(run); + const { height, descent, xAdvance } = run; if (options.outlineRuns) { - ctx.rect(0, -height, runAdvanceWidth, height).stroke(); + ctx.rect(0, -height, xAdvance, height).stroke(); } ctx.fillColor(color.value); @@ -69,9 +63,9 @@ const renderRun = (ctx, run, options) => { if (link) { if (isSrcId(link)) { - ctx.goTo(0, -height - descent, runAdvanceWidth, height, link.slice(1)); + ctx.goTo(0, -height - descent, xAdvance, height, link.slice(1)); } else { - ctx.link(0, -height - descent, runAdvanceWidth, height, link); + ctx.link(0, -height - descent, xAdvance, height, link); } } @@ -105,7 +99,7 @@ const renderRun = (ctx, run, options) => { } } - ctx.translate(runAdvanceWidth, 0); + ctx.translate(xAdvance, 0); }; const renderBackground = (ctx, rect, backgroundColor) => { @@ -174,7 +168,7 @@ const renderDecorationLine = (ctx, line) => { }; const renderLine = (ctx, line, options) => { - const lineAscent = ascent(line); + const lineAscent = line.ascent; if (options.outlineLines) { ctx.rect(line.box.x, line.box.y, line.box.width, line.box.height).stroke(); @@ -194,7 +188,7 @@ const renderLine = (ctx, line, options) => { x: 0, y: -lineAscent, height: line.box.height, - width: advanceWidth(run) - overflowRight, + width: run.xAdvance - overflowRight, }; renderBackground(ctx, backgroundRect, run.attributes.backgroundColor); diff --git a/packages/textkit/babel.config.js b/packages/textkit/babel.config.js index 27f83582f..831adcadc 100644 --- a/packages/textkit/babel.config.js +++ b/packages/textkit/babel.config.js @@ -1 +1,3 @@ -module.exports = { extends: '../../babel.config.js' }; +module.exports = { + extends: '../../babel.config.js', +}; diff --git a/packages/textkit/package.json b/packages/textkit/package.json index f164d6725..d52018d89 100644 --- a/packages/textkit/package.json +++ b/packages/textkit/package.json @@ -3,7 +3,8 @@ "license": "MIT", "version": "3.0.0", "description": "An advanced text layout framework", - "main": "./lib/index.js", + "main": "./lib/textkit.cjs.js", + "module": "./lib/textkit.es.js", "repository": { "type": "git", "url": "https://github.com/diegomura/react-pdf.git", @@ -15,8 +16,8 @@ ], "scripts": { "test": "jest", - "build": "rimraf ./lib && babel src --out-dir lib", - "watch": "rimraf ./lib && babel src --out-dir lib --watch" + "build": "rimraf ./lib && rollup -c", + "watch": "rimraf ./lib && rollup -c -w" }, "files": [ "lib" diff --git a/packages/textkit/rollup.config.js b/packages/textkit/rollup.config.js new file mode 100644 index 000000000..774f22e11 --- /dev/null +++ b/packages/textkit/rollup.config.js @@ -0,0 +1,37 @@ +import babel from '@rollup/plugin-babel'; +import localResolve from 'rollup-plugin-local-resolve'; +import pkg from './package.json'; + +const cjs = { + exports: 'named', + format: 'cjs', +}; + +const esm = { + format: 'es', +}; + +const getCJS = override => Object.assign({}, cjs, override); +const getESM = override => Object.assign({}, esm, override); + +const configBase = { + input: './src/index.js', + external: Object.keys(pkg.dependencies), + plugins: [ + localResolve(), + babel({ + babelrc: true, + babelHelpers: 'runtime', + exclude: 'node_modules/**', + }), + ], +}; + +const config = Object.assign({}, configBase, { + output: [ + getESM({ file: 'lib/textkit.es.js' }), + getCJS({ file: 'lib/textkit.cjs.js' }), + ], +}); + +export default config; diff --git a/packages/textkit/src/index.js b/packages/textkit/src/index.js index 21935e2d5..08593be16 100644 --- a/packages/textkit/src/index.js +++ b/packages/textkit/src/index.js @@ -6,7 +6,7 @@ import scriptItemizer from './engines/scriptItemizer'; import wordHyphenation from './engines/wordHyphenation'; import fontSubstitution from './engines/fontSubstitution'; -const engines = { +export { linebreaker, justification, textDecoration, @@ -15,6 +15,4 @@ const engines = { fontSubstitution, }; -const engine = layoutEngine(engines); - -export default engine; +export default layoutEngine; diff --git a/packages/textkit/src/layout/finalizeFragments.js b/packages/textkit/src/layout/finalizeFragments.js index 1c426c75c..462107128 100644 --- a/packages/textkit/src/layout/finalizeFragments.js +++ b/packages/textkit/src/layout/finalizeFragments.js @@ -1,5 +1,9 @@ import { last, compose } from '@react-pdf/fns'; +import runHeight from '../run/height'; +import runAscent from '../run/ascent'; +import runDescent from '../run/descent'; +import runAdvanceWidth from '../run/advanceWidth'; import advanceWidth from '../attributedString/advanceWidth'; import leadingOffset from '../attributedString/leadingOffset'; import trailingOffset from '../attributedString/trailingOffset'; @@ -64,6 +68,35 @@ const justifyLine = (engines, options, align) => line => { return shouldJustify ? engines.justification(options)(newLine) : newLine; }; +const finalizeLine = line => { + let lineAscent = 0; + let lineDescent = 0; + let lineHeight = 0; + let lineXAdvance = 0; + + const runs = line.runs.map(run => { + const height = runHeight(run); + const ascent = runAscent(run); + const descent = runDescent(run); + const xAdvance = runAdvanceWidth(run); + + lineHeight = Math.max(lineHeight, height); + lineAscent = Math.max(lineAscent, ascent); + lineDescent = Math.max(lineDescent, descent); + lineXAdvance += xAdvance; + + return Object.assign({}, run, { height, ascent, descent, xAdvance }); + }); + + return Object.assign({}, line, { + runs, + height: lineHeight, + ascent: lineAscent, + descent: lineDescent, + xAdvance: lineXAdvance, + }); +}; + /** * Finalize line by performing line justification * and text decoration (using appropiate engines) @@ -81,6 +114,7 @@ const finalizeBlock = (engines = {}, options) => (line, i, lines) => { const align = isLastFragment ? style.alignLastLine : style.align; return compose( + finalizeLine, engines.textDecoration(options), justifyLine(engines, options, align), adjustOverflow, From c350bf1a0eeafb7a2b340e40602a8daa56d26e6e Mon Sep 17 00:00:00 2001 From: diegomura Date: Sat, 2 Jul 2022 02:32:58 -0300 Subject: [PATCH 2/3] chore: add changeset --- .changeset/strange-pots-push.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/strange-pots-push.md diff --git a/.changeset/strange-pots-push.md b/.changeset/strange-pots-push.md new file mode 100644 index 000000000..46bdc55d8 --- /dev/null +++ b/.changeset/strange-pots-push.md @@ -0,0 +1,7 @@ +--- +'@react-pdf/textkit': major +'@react-pdf/layout': patch +'@react-pdf/render': patch +--- + +refactor: build textkit with rollup & define public api From 51e06d2bf26d18d6bebfe723ca6e4e7f8f04e892 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Sat, 2 Jul 2022 02:34:08 -0300 Subject: [PATCH 3/3] Update strange-pots-push.md --- .changeset/strange-pots-push.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/strange-pots-push.md b/.changeset/strange-pots-push.md index 46bdc55d8..40ca829a8 100644 --- a/.changeset/strange-pots-push.md +++ b/.changeset/strange-pots-push.md @@ -4,4 +4,4 @@ '@react-pdf/render': patch --- -refactor: build textkit with rollup & define public api +feat: build textkit with rollup & define public api