Skip to content

Commit

Permalink
feat: build textkit with rollup & define public api (#1906)
Browse files Browse the repository at this point in the history
  • Loading branch information
diegomura authored Jul 2, 2022
1 parent 37a9a74 commit 884695b
Show file tree
Hide file tree
Showing 15 changed files with 193 additions and 54 deletions.
7 changes: 7 additions & 0 deletions .changeset/strange-pots-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@react-pdf/textkit': major
'@react-pdf/layout': patch
'@react-pdf/render': patch
---

feat: build textkit with rollup & define public api
32 changes: 17 additions & 15 deletions packages/layout/src/svg/layoutText.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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,
};
Expand Down
27 changes: 27 additions & 0 deletions packages/layout/src/text/fromFragments.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion packages/layout/src/text/getAttributedString.js
Original file line number Diff line number Diff line change
@@ -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];
Expand Down
13 changes: 7 additions & 6 deletions packages/layout/src/text/layoutText.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
5 changes: 2 additions & 3 deletions packages/layout/src/text/linesWidth.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import advanceWidth from '@react-pdf/textkit/lib/attributedString/advanceWidth';

/**
* Get lines width (if any)
*
Expand All @@ -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;
41 changes: 41 additions & 0 deletions packages/layout/tests/text/fromFragments.test.js
Original file line number Diff line number Diff line change
@@ -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 });
});
});
5 changes: 2 additions & 3 deletions packages/layout/tests/text/layoutText.test.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -38,15 +37,15 @@ 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);
});

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);
});
Expand Down
7 changes: 2 additions & 5 deletions packages/render/src/primitives/renderSvgText.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 7 additions & 13 deletions packages/render/src/primitives/renderText.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -56,22 +52,20 @@ 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);
ctx.fillOpacity(opacity);

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);
}
}

Expand Down Expand Up @@ -105,7 +99,7 @@ const renderRun = (ctx, run, options) => {
}
}

ctx.translate(runAdvanceWidth, 0);
ctx.translate(xAdvance, 0);
};

const renderBackground = (ctx, rect, backgroundColor) => {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion packages/textkit/babel.config.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
module.exports = { extends: '../../babel.config.js' };
module.exports = {
extends: '../../babel.config.js',
};
7 changes: 4 additions & 3 deletions packages/textkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
37 changes: 37 additions & 0 deletions packages/textkit/rollup.config.js
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 2 additions & 4 deletions packages/textkit/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,6 +15,4 @@ const engines = {
fontSubstitution,
};

const engine = layoutEngine(engines);

export default engine;
export default layoutEngine;
Loading

0 comments on commit 884695b

Please sign in to comment.