diff --git a/.babelrc b/.babelrc index e675c3f..aff8d33 100644 --- a/.babelrc +++ b/.babelrc @@ -1,10 +1,9 @@ { - "presets": [ - ["env", { "loose": true }] - ], + "presets": [["env", { "loose": true }]], "plugins": [ "transform-runtime", "transform-export-extensions", + "transform-object-rest-spread", ["transform-class-properties", { "loose": true }] ] } diff --git a/.gitignore b/.gitignore index 0781e10..c30b937 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ yarn.lock dist *.pdf lerna-debug.log +*.ttf +.vscode diff --git a/package.json b/package.json index 9a712b5..2488045 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "babel-eslint": "^8.2.2", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-export-extensions": "^6.22.0", + "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.7.0", "eslint": "^4.18.2", diff --git a/packages/bidi-engine/package.json b/packages/bidi-engine/package.json index 1f443ea..40cf906 100644 --- a/packages/bidi-engine/package.json +++ b/packages/bidi-engine/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" diff --git a/packages/core/package.json b/packages/core/package.json index 78785c4..0f82462 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,16 +1,19 @@ { "name": "@textkit/core", - "version": "0.1.16", + "version": "0.1.23", "description": "An advanced text layout framework", "main": "dist/index.js", "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel ./src --out-dir ./dist", + "build": "babel ./src --out-dir ./dist --source-maps", "build:watch": "babel ./src --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" }, + "publishConfig": { + "access": "public" + }, "files": [ "dist" ], @@ -18,8 +21,6 @@ "license": "MIT", "dependencies": { "cubic2quad": "^1.1.0", - "iconv-lite": "^0.4.13", - "svgpath": "^2.2.0", "unicode-properties": "^1.1.0" } } diff --git a/packages/core/src/geom/BBox.js b/packages/core/src/geom/BBox.js index dc17507..cb89e20 100644 --- a/packages/core/src/geom/BBox.js +++ b/packages/core/src/geom/BBox.js @@ -1,45 +1,17 @@ -/** - * Represents a glyph bounding box - */ +import Rect from './Rect'; + class BBox { constructor(minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity) { - /** - * The minimum X position in the bounding box - * @type {number} - */ this.minX = minX; - - /** - * The minimum Y position in the bounding box - * @type {number} - */ this.minY = minY; - - /** - * The maxmimum X position in the bounding box - * @type {number} - */ this.maxX = maxX; - - /** - * The maxmimum Y position in the bounding box - * @type {number} - */ this.maxY = maxY; } - /** - * The width of the bounding box - * @type {number} - */ get width() { return this.maxX - this.minX; } - /** - * The height of the bounding box - * @type {number} - */ get height() { return this.maxY - this.minY; } @@ -67,6 +39,10 @@ class BBox { this.addPoint(rect.maxX, rect.maxY); } + toRect() { + return new Rect(this.minX, this.minY, this.width, this.height); + } + copy() { return new BBox(this.minX, this.minY, this.maxX, this.maxY); } diff --git a/packages/core/src/geom/Rect.js b/packages/core/src/geom/Rect.js index 9b219bf..178c025 100644 --- a/packages/core/src/geom/Rect.js +++ b/packages/core/src/geom/Rect.js @@ -2,98 +2,43 @@ import Point from './Point'; const CORNERS = ['topLeft', 'topRight', 'bottomLeft', 'bottomRight']; -/** - * Represents a rectangle - */ class Rect { /** @public */ constructor(x = 0, y = 0, width = 0, height = 0) { - /** - * The x-coordinate of the rectangle - * @type {number} - */ this.x = x; - - /** - * The y-coordinate of the rectangle - * @type {number} - */ this.y = y; - - /** - * The width of the rectangle - * @type {number} - */ this.width = width; - - /** - * The height of the rectangle - * @type {number} - */ this.height = height; } - /** - * The maximum x-coordinate in the rectangle - * @type {number} - */ get maxX() { return this.x + this.width; } - /** - * The maximum y-coordinate in the rectangle - * @type {number} - */ get maxY() { return this.y + this.height; } - /** - * The area of the rectangle - * @type {number} - */ get area() { return this.width * this.height; } - /** - * The top left corner of the rectangle - * @type {Point} - */ get topLeft() { return new Point(this.x, this.y); } - /** - * The top right corner of the rectangle - * @type {Point} - */ get topRight() { return new Point(this.maxX, this.y); } - /** - * The bottom left corner of the rectangle - * @type {Point} - */ get bottomLeft() { return new Point(this.x, this.maxY); } - /** - * The bottom right corner of the rectangle - * @type {Point} - */ get bottomRight() { return new Point(this.maxX, this.maxY); } - /** - * Returns whether this rectangle intersects another rectangle - * @param {Rect} rect - The rectangle to check - * @return {boolean} - */ intersects(rect) { return ( this.x <= rect.x + rect.width && @@ -103,30 +48,14 @@ class Rect { ); } - /** - * Returns whether this rectangle fully contains another rectangle - * @param {Rect} rect - The rectangle to check - * @return {boolean} - */ containsRect(rect) { return this.x <= rect.x && this.y <= rect.y && this.maxX >= rect.maxX && this.maxY >= rect.maxY; } - /** - * Returns whether the rectangle contains the given point - * @param {Point} point - The point to check - * @return {boolean} - */ containsPoint(point) { return this.x <= point.x && this.y <= point.y && this.maxX >= point.x && this.maxY >= point.y; } - /** - * Returns the first corner of this rectangle (from top to bottom, left to right) - * that is contained in the given rectangle, or null of the rectangles do not intersect. - * @param {Rect} rect - The rectangle to check - * @return {string} - */ getCornerInRect(rect) { for (const key of CORNERS) { if (rect.containsPoint(this[key])) { @@ -154,10 +83,6 @@ class Rect { return this.width === size.width && this.height === size.height; } - /** - * Returns a copy of this rectangle - * @return {Rect} - */ copy() { return new Rect(this.x, this.y, this.width, this.height); } diff --git a/packages/core/src/index.js b/packages/core/src/index.js index caf8119..6a239bf 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -3,7 +3,6 @@ export { Run, Block, Range, - TabStop, RunStyle, GlyphRun, Container, diff --git a/packages/core/src/layout/GlyphGenerator.js b/packages/core/src/layout/GlyphGenerator.js deleted file mode 100644 index 4a6d4d5..0000000 --- a/packages/core/src/layout/GlyphGenerator.js +++ /dev/null @@ -1,149 +0,0 @@ -import GlyphRun from '../models/GlyphRun'; -import GlyphString from '../models/GlyphString'; -import Run from '../models/Run'; -import RunStyle from '../models/RunStyle'; -import flattenRuns from './flattenRuns'; - -/** - * A GlyphGenerator is responsible for mapping characters in - * an AttributedString to glyphs in a GlyphString. It resolves - * style attributes such as the font and Unicode script and - * directionality properties, and creates GlyphRuns using fontkit. - */ -export default class GlyphGenerator { - constructor(engines = {}) { - this.resolvers = [engines.fontSubstitutionEngine, engines.scriptItemizer]; - } - - generateGlyphs(attributedString) { - // Resolve runs - const runs = this.resolveRuns(attributedString); - - // Generate glyphs - let glyphIndex = 0; - const glyphRuns = runs.map(run => { - const str = attributedString.string.slice(run.start, run.end); - const glyphRun = run.attributes.font.layout( - str, - run.attributes.features, - run.attributes.script - ); - const end = glyphIndex + glyphRun.glyphs.length; - const glyphIndices = this.resolveGlyphIndices(str, glyphRun.stringIndices); - - const res = new GlyphRun( - glyphIndex, - end, - run.attributes, - glyphRun.glyphs, - glyphRun.positions, - glyphRun.stringIndices, - glyphIndices - ); - - this.resolveAttachments(res); - this.resolveYOffset(res); - - glyphIndex = end; - return res; - }); - - return new GlyphString(attributedString.string, glyphRuns); - } - - resolveGlyphIndices(string, stringIndices) { - const glyphIndices = []; - - for (let i = 0; i < string.length; i++) { - for (let j = 0; j < stringIndices.length; j++) { - if (stringIndices[j] >= i) { - glyphIndices[i] = j; - break; - } - - glyphIndices[i] = undefined; - } - } - - let lastValue = glyphIndices[glyphIndices.length - 1]; - for (let i = glyphIndices.length - 1; i >= 0; i--) { - if (glyphIndices[i] === undefined) { - glyphIndices[i] = lastValue; - } else { - lastValue = glyphIndices[i]; - } - } - - lastValue = glyphIndices[0]; - for (let i = 0; i < glyphIndices.length; i++) { - if (glyphIndices[i] === undefined) { - glyphIndices[i] = lastValue; - } else { - lastValue = glyphIndices[i]; - } - } - - return glyphIndices; - } - - resolveRuns(attributedString) { - // Map attributes to RunStyle objects - const r = attributedString.runs.map( - run => new Run(run.start, run.end, new RunStyle(run.attributes)) - ); - - // Resolve run ranges and additional attributes - const runs = []; - for (const resolver of this.resolvers) { - const resolved = resolver.getRuns(attributedString.string, r); - runs.push(...resolved); - } - - // Ignore resolved properties - const styles = attributedString.runs.map(run => { - const attrs = Object.assign({}, run.attributes); - delete attrs.font; - delete attrs.fontDescriptor; - return new Run(run.start, run.end, attrs); - }); - - // Flatten runs - const resolvedRuns = flattenRuns([...styles, ...runs]); - for (const run of resolvedRuns) { - run.attributes = new RunStyle(run.attributes); - } - - return resolvedRuns; - } - - resolveAttachments(glyphRun) { - const { font, attachment } = glyphRun.attributes; - - if (!attachment) { - return; - } - - const objectReplacement = font.glyphForCodePoint(0xfffc); - - for (let i = 0; i < glyphRun.length; i++) { - const glyph = glyphRun.glyphs[i]; - const position = glyphRun.positions[i]; - - if (glyph === objectReplacement) { - position.xAdvance = attachment.width; - } - } - } - - resolveYOffset(glyphRun) { - const { font, yOffset } = glyphRun.attributes; - - if (!yOffset) { - return; - } - - for (let i = 0; i < glyphRun.length; i++) { - glyphRun.positions[i].yOffset += yOffset * font.unitsPerEm; - } - } -} diff --git a/packages/core/src/layout/LayoutEngine.js b/packages/core/src/layout/LayoutEngine.js index 8f950f2..9a9482e 100644 --- a/packages/core/src/layout/LayoutEngine.js +++ b/packages/core/src/layout/LayoutEngine.js @@ -1,24 +1,12 @@ -import ParagraphStyle from '../models/ParagraphStyle'; -import Rect from '../geom/Rect'; -import Block from '../models/Block'; -import GlyphGenerator from './GlyphGenerator'; -import Typesetter from './Typesetter'; +import wrapWords from './wrapWords'; +import typesetter from './typesetter'; import injectEngines from './injectEngines'; - -// 1. split into paragraphs -// 2. get bidi runs and paragraph direction -// 3. font substitution - map to resolved font runs -// 4. script itemization -// 5. font shaping - text to glyphs -// 6. line breaking -// 7. bidi reordering -// 8. justification - -// 1. get a list of rectangles by intersecting path, line, and exclusion paths -// 2. perform line breaking to get acceptable break points for each fragment -// 3. ellipsize line if necessary -// 4. bidi reordering -// 5. justification +import generateGlyphs from './generateGlyphs'; +import resolveYOffset from './resolveYOffset'; +import preprocessRuns from './preprocessRuns'; +import splitParagraphs from './splitParagraphs'; +import resolveAttachments from './resolveAttachments'; +import applyDefaultStyles from './applyDefaultStyles'; /** * A LayoutEngine is the main object that performs text layout. @@ -27,114 +15,26 @@ import injectEngines from './injectEngines'; * various layout tasks. These objects can be overridden to customize * layout behavior. */ -export default class LayoutEngine { - constructor(engines) { - const injectedEngines = injectEngines(engines); - this.glyphGenerator = new GlyphGenerator(injectedEngines); - this.typesetter = new Typesetter(injectedEngines); - } - - layout(attributedString, containers) { - let start = 0; - for (let i = 0; i < containers.length && start < attributedString.length; i++) { - const container = containers[i]; - const { bbox, columns, columnGap } = container; - const isLastContainer = i === containers.length - 1; - const columnWidth = (bbox.width - columnGap * (columns - 1)) / columns; - const rect = new Rect(bbox.minX, bbox.minY, columnWidth, bbox.height); +const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x); - for (let j = 0; j < container.columns && start < attributedString.length; j++) { - start = this.layoutColumn(attributedString, start, container, rect.copy(), isLastContainer); - rect.x += columnWidth + container.columnGap; - } - } - } - - layoutColumn(attributedString, start, container, rect, isLastContainer) { - while (start < attributedString.length && rect.height > 0) { - let next = attributedString.string.indexOf('\n', start); - if (next === -1) next = attributedString.string.length; - - const paragraph = attributedString.slice(start, next); - const block = this.layoutParagraph(paragraph, container, rect, start, isLastContainer); - const paragraphHeight = block.bbox.height + block.style.paragraphSpacing; - - container.blocks.push(block); +const map = fn => (array, ...other) => array.map((e, index) => fn(e, ...other, index)); - rect.y += paragraphHeight; - rect.height -= paragraphHeight; - start += paragraph.length + 1; - - // If entire paragraph did not fit, move on to the next column or container. - if (start < next) break; - } - - return start; +export default class LayoutEngine { + constructor(engines) { + this.engines = injectEngines(engines); } - layoutParagraph(attributedString, container, rect, stringOffset, isLastContainer) { - const glyphString = this.glyphGenerator.generateGlyphs(attributedString); - const paragraphStyle = new ParagraphStyle(attributedString.runs[0].attributes); - const { marginLeft, marginRight, indent, maxLines, lineSpacing } = paragraphStyle; - - const lineRect = new Rect( - rect.x + marginLeft + indent, - rect.y, - rect.width - marginLeft - indent - marginRight, - glyphString.height - ); - - let pos = 0; - let lines = 0; - let firstLine = true; - const fragments = []; - - while (lineRect.y < rect.maxY && pos < glyphString.length && lines < maxLines) { - const lineFragments = this.typesetter.layoutLineFragments( - pos, - lineRect, - glyphString, - container, - paragraphStyle, - stringOffset - ); - - lineRect.y += lineRect.height + lineSpacing; - - if (lineFragments.length > 0) { - fragments.push(...lineFragments); - pos = lineFragments[lineFragments.length - 1].end; - lines++; - - if (firstLine) { - lineRect.x -= indent; - lineRect.width += indent; - firstLine = false; - } - } - } - - // Add empty line fragment for empty glyph strings - if (glyphString.length === 0) { - const newLineFragment = this.typesetter.layoutLineFragments( - pos, - lineRect, - glyphString, - container, - paragraphStyle - ); - - fragments.push(...newLineFragment); - } - - const isTruncated = isLastContainer && pos < glyphString.length; - fragments.forEach((fragment, i) => { - const isLastFragment = i === fragments.length - 1 && pos === glyphString.length; - - this.typesetter.finalizeLineFragment(fragment, paragraphStyle, isLastFragment, isTruncated); - }); - - return new Block(fragments, paragraphStyle); + layout(attributedString, containers) { + compose( + typesetter(this.engines)(containers), + map(resolveYOffset(this.engines)), + map(resolveAttachments(this.engines)), + map(generateGlyphs(this.engines)), + map(wrapWords(this.engines)), + splitParagraphs(this.engines), + preprocessRuns(this.engines), + applyDefaultStyles(this.engines) + )(attributedString); } } diff --git a/packages/core/src/layout/Typesetter.js b/packages/core/src/layout/Typesetter.js index d01a79c..bff0941 100644 --- a/packages/core/src/layout/Typesetter.js +++ b/packages/core/src/layout/Typesetter.js @@ -1,5 +1,8 @@ +import generateFragments from './fragmentGenerator'; +import Block from '../models/Block'; import LineFragment from '../models/LineFragment'; +const NEW_LINE = 10; const ALIGNMENT_FACTORS = { left: 0, center: 0.5, @@ -7,129 +10,117 @@ const ALIGNMENT_FACTORS = { justify: 0 }; -/** - * A Typesetter performs glyph line layout, including line breaking, - * hyphenation, justification, truncation, hanging punctuation, - * and text decoration. It uses several underlying objects to perform - * these tasks, which could be overridden in order to customize the - * typesetter's behavior. - */ -export default class Typesetter { - constructor(engines = {}) { - this.lineBreaker = engines.lineBreaker; - this.lineFragmentGenerator = engines.lineFragmentGenerator; - this.justificationEngine = engines.justificationEngine; - this.truncationEngine = engines.truncationEngine; - this.decorationEngine = engines.decorationEngine; - this.tabEngine = engines.tabEngine; - } - - layoutLineFragments(start, lineRect, glyphString, container, paragraphStyle, stringOffset) { - const lineString = glyphString.slice(start, glyphString.length); +const finalizeLineFragment = engines => (line, style, isLastFragment, isTruncated) => { + const align = isLastFragment && !isTruncated ? style.alignLastLine : style.align; - // Guess the line height using the full line before intersecting with the container. - lineRect.height = lineString.slice(0, lineString.glyphIndexAtOffset(lineRect.width)).height; + if (isLastFragment && isTruncated && style.truncationMode) { + engines.truncationEngine.truncate(line, style.truncationMode); + } - // Generate line fragment rectangles by intersecting with the container. - const fragmentRects = this.lineFragmentGenerator.generateFragments(lineRect, container); + let start = 0; + let end = line.length; - if (fragmentRects.length === 0) return []; + // Remove new line char at the end of line + if (line.codePointAtGlyphIndex(line.length - 1) === NEW_LINE) { + line.deleteGlyph(line.length - 1); + } - let pos = 0; - const lineFragments = []; - let lineHeight = paragraphStyle.lineHeight; + // Ignore whitespace at the start and end of a line for alignment + while (line.isWhiteSpace(start)) { + line.overflowLeft += line.getGlyphWidth(start++); + } - for (const fragmentRect of fragmentRects) { - const line = lineString.slice(pos, lineString.length); + while (line.isWhiteSpace(end - 1)) { + line.overflowRight += line.getGlyphWidth(--end); + } - if (this.tabEngine) { - this.tabEngine.processLineFragment(line, container); + // Adjust line rect for hanging punctuation + if (style.hangingPunctuation) { + if (align === 'left' || align === 'justify') { + if (line.isHangingPunctuationStart(start)) { + line.overflowLeft += line.getGlyphWidth(start++); } + } - const bk = this.lineBreaker.suggestLineBreak(line, fragmentRect.width, paragraphStyle); - - if (bk) { - bk.position += pos; - - const lineFragment = new LineFragment(fragmentRect, lineString.slice(pos, bk.position)); - - lineFragment.stringStart = - stringOffset + glyphString.stringIndexForGlyphIndex(lineFragment.start); - lineFragment.stringEnd = - stringOffset + glyphString.stringIndexForGlyphIndex(lineFragment.end); - - lineFragments.push(lineFragment); - lineHeight = Math.max(lineHeight, lineFragment.height); - - pos = bk.position; - if (pos >= lineString.length) { - break; - } + if (align === 'right' || align === 'justify') { + if (line.isHangingPunctuationEnd(end - 1)) { + line.overflowRight += line.getGlyphWidth(--end); } } + } - // Update the fragments on this line with the computed line height - if (lineHeight !== 0) lineRect.height = lineHeight; + line.rect.x -= line.overflowLeft; + line.rect.width += line.overflowLeft + line.overflowRight; - for (const fragment of lineFragments) { - fragment.rect.height = lineHeight; - } + // Adjust line offset for alignment + const remainingWidth = line.rect.width - line.advanceWidth; + line.rect.x += remainingWidth * ALIGNMENT_FACTORS[align]; - return lineFragments; + if (align === 'justify' || line.advanceWidth > line.rect.width) { + engines.justificationEngine.justify(line, { + factor: style.justificationFactor + }); } - finalizeLineFragment(lineFragment, paragraphStyle, isLastFragment, isTruncated) { - const align = - isLastFragment && !isTruncated ? paragraphStyle.alignLastLine : paragraphStyle.align; - - if (isLastFragment && isTruncated && paragraphStyle.truncationMode) { - this.truncationEngine.truncate(lineFragment, paragraphStyle.truncationMode); - } + engines.decorationEngine.createDecorationLines(line); +}; - this.adjustLineFragmentRectangle(lineFragment, paragraphStyle, align); +const layoutParagraph = engines => (paragraph, container, lineRect) => { + const { value, syllables } = paragraph; + const style = value.glyphRuns[0].attributes; - if (align === 'justify' || lineFragment.advanceWidth > lineFragment.rect.width) { - this.justificationEngine.justify(lineFragment, { - factor: paragraphStyle.justificationFactor - }); - } + // Guess the line height using the full line before intersecting with the container. + // Generate line fragment rectangles by intersecting with the container. + const lineHeight = value.slice(0, value.glyphIndexAtOffset(lineRect.width)).height; + const fragmentRects = generateFragments(lineRect, lineHeight, container); + const wrappingWidths = fragmentRects.map(rect => rect.width); + const lines = engines.lineBreaker.suggestLineBreak(value, syllables, wrappingWidths, style); - this.decorationEngine.createDecorationLines(lineFragment); - } + let currentY = lineRect.y; + const lineFragments = lines.map((line, i) => { + const lineBox = fragmentRects[Math.min(i, fragmentRects.length - 1)].copy(); + const fragmentHeight = Math.max(line.height, style.lineHeight); - adjustLineFragmentRectangle(lineFragment, paragraphStyle, align) { - let start = 0; - let end = lineFragment.length; + lineBox.y = currentY; + lineBox.height = fragmentHeight; + currentY += fragmentHeight; - // Ignore whitespace at the start and end of a line for alignment - while (lineFragment.isWhiteSpace(start)) { - lineFragment.overflowLeft += lineFragment.getGlyphWidth(start++); - } + return new LineFragment(lineBox, line); + }); - while (lineFragment.isWhiteSpace(end - 1)) { - lineFragment.overflowRight += lineFragment.getGlyphWidth(--end); - } + lineFragments.forEach((lineFragment, i) => { + finalizeLineFragment(engines)(lineFragment, style, i === lineFragments.length - 1); + }); - // Adjust line rect for hanging punctuation - if (paragraphStyle.hangingPunctuation) { - if (align === 'left' || align === 'justify') { - if (lineFragment.isHangingPunctuationStart(start)) { - lineFragment.overflowLeft += lineFragment.getGlyphWidth(start++); - } - } + return new Block(lineFragments); +}; - if (align === 'right' || align === 'justify') { - if (lineFragment.isHangingPunctuationEnd(end - 1)) { - lineFragment.overflowRight += lineFragment.getGlyphWidth(--end); - } +const typesetter = engines => containers => glyphStrings => { + const paragraphs = [...glyphStrings]; + + const layoutContainer = (rect, container) => { + let paragraphRect = rect.copy(); + let nextParagraph = paragraphs.shift(); + + while (nextParagraph) { + const block = layoutParagraph(engines)(nextParagraph, container, paragraphRect); + + if (paragraphRect.height >= block.height) { + container.blocks.push(block); + paragraphRect = paragraphRect.copy(); + paragraphRect.y += block.height; + paragraphRect.height -= block.height; + nextParagraph = paragraphs.shift(); + } else { + paragraphs.unshift(nextParagraph); + break; } } + }; - lineFragment.rect.x -= lineFragment.overflowLeft; - lineFragment.rect.width += lineFragment.overflowLeft + lineFragment.overflowRight; + return containers.forEach(container => { + layoutContainer(container.bbox.toRect(), container); + }); +}; - // Adjust line offset for alignment - const remainingWidth = lineFragment.rect.width - lineFragment.advanceWidth; - lineFragment.rect.x += remainingWidth * ALIGNMENT_FACTORS[align]; - } -} +export default typesetter; diff --git a/packages/core/src/layout/applyDefaultStyles.js b/packages/core/src/layout/applyDefaultStyles.js new file mode 100644 index 0000000..d0fc703 --- /dev/null +++ b/packages/core/src/layout/applyDefaultStyles.js @@ -0,0 +1,53 @@ +import FontDescriptor from '../models/FontDescriptor'; +import AttributedString from '../models/AttributedString'; + +const applyDefaultStyles = () => attributedString => { + const runs = attributedString.runs.map(({ start, end, attributes }) => ({ + start, + end, + attributes: { + align: attributes.align || 'left', + alignLastLine: + attributes.alignLastLine || (attributes.align === 'justify' ? 'left' : attributes.align), + attachment: attributes.attachment || null, + backgroundColor: attributes.backgroundColor || null, + bidiLevel: attributes.bidiLevel || null, + bullet: attributes.bullet || null, + characterSpacing: attributes.characterSpacing || 0, + color: attributes.color || 'black', + features: attributes.features || [], + fill: attributes.fill !== false, + font: attributes.font || null, + fontDescriptor: FontDescriptor.fromAttributes(attributes), + fontSize: attributes.fontSize || 12, + hangingPunctuation: attributes.hangingPunctuation || false, + hyphenationFactor: attributes.hyphenationFactor || 0, + indent: attributes.indent || 0, + justificationFactor: attributes.justificationFactor || 1, + lineHeight: attributes.lineHeight || null, + lineSpacing: attributes.lineSpacing || 0, + link: attributes.link || null, + marginLeft: attributes.marginLeft || attributes.margin || 0, + marginRight: attributes.marginRight || attributes.margin || 0, + maxLines: attributes.maxLines || Infinity, + paddingTop: attributes.paddingTop || attributes.padding || 0, + paragraphSpacing: attributes.paragraphSpacing || 0, + truncationMode: attributes.truncationMode || (attributes.truncate ? 'right' : null), + underline: attributes.underline || false, + underlineColor: attributes.underlineColor || attributes.color || 'black', + underlineStyle: attributes.underlineStyle || 'solid', + script: attributes.script || null, + shrinkFactor: attributes.shrinkFactor || 0, + strike: attributes.strike || false, + strikeColor: attributes.strikeColor || attributes.color || 'black', + strikeStyle: attributes.strikeStyle || 'solid', + stroke: attributes.stroke || false, + wordSpacing: attributes.wordSpacing || 0, + yOffset: attributes.yOffset || 0 + } + })); + + return new AttributedString(attributedString.string, runs); +}; + +export default applyDefaultStyles; diff --git a/packages/core/src/layout/fragmentGenerator.js b/packages/core/src/layout/fragmentGenerator.js new file mode 100644 index 0000000..550aa66 --- /dev/null +++ b/packages/core/src/layout/fragmentGenerator.js @@ -0,0 +1,220 @@ +import Rect from '../geom/Rect'; + +const LEFT = 0; +const RIGHT = 1; +const BELOW = 1; +const INSIDE = 2; +const ABOVE = 3; +const INTERIOR = 1; +const EXTERIOR = 2; + +const BELOW_TO_INSIDE = (BELOW << 4) | INSIDE; +const BELOW_TO_ABOVE = (BELOW << 4) | ABOVE; +const INSIDE_TO_BELOW = (INSIDE << 4) | BELOW; +const INSIDE_TO_ABOVE = (INSIDE << 4) | ABOVE; +const ABOVE_TO_INSIDE = (ABOVE << 4) | INSIDE; +const ABOVE_TO_BELOW = (ABOVE << 4) | BELOW; + +const xIntersection = (e, t, n) => { + const r = e - n.y; + const i = t.y - n.y; + return (r / i) * (t.x - n.x) + n.x; +}; + +const splitLineRect = (lineRect, polygon, type) => { + const minY = lineRect.y; + const maxY = lineRect.maxY; + const markers = []; + let wrapState = BELOW; + let min = Infinity; + let max = -Infinity; + + for (let i = 0; i < polygon.contours.length; i++) { + const contour = polygon.contours[i]; + let index = -1; + let state = -1; + + // Find the first point outside the line rect. + do { + const point = contour[++index]; + state = point.y <= minY ? BELOW : point.y >= maxY ? ABOVE : INSIDE; + } while (state === INSIDE && index < contour.length - 1); + + // Contour is entirely inside the line rect. Skip it. + if (state === INSIDE) { + continue; + } + + const dir = type === EXTERIOR ? 1 : -1; + let idx = type === EXTERIOR ? index : contour.length + index; + let currentPoint; + + for (let index = 0; index <= contour.length; index++, idx += dir) { + const point = contour[idx % contour.length]; + + if (index === 0) { + currentPoint = point; + state = point.y <= minY ? BELOW : point.y >= maxY ? ABOVE : INSIDE; + continue; + } + + const s = point.y <= minY ? BELOW : point.y >= maxY ? ABOVE : INSIDE; + const x = point.x; + + if (s !== state) { + const stateChangeType = (state << 4) | s; + switch (stateChangeType) { + case BELOW_TO_INSIDE: { + // console.log('BELOW_TO_INSIDE') + const xIntercept = xIntersection(minY, point, currentPoint); + min = Math.min(xIntercept, x); + max = Math.max(xIntercept, x); + wrapState = BELOW; + break; + } + + case BELOW_TO_ABOVE: { + // console.log('BELOW_TO_ABOVE') + const x1 = xIntersection(minY, point, currentPoint); + const x2 = xIntersection(maxY, point, currentPoint); + markers.push({ + type: LEFT, + position: Math.max(x1, x2) + }); + break; + } + + case ABOVE_TO_INSIDE: { + // console.log('ABOVE_TO_INSIDE') + const xIntercept = xIntersection(maxY, point, currentPoint); + min = Math.min(xIntercept, x); + max = Math.max(xIntercept, x); + wrapState = ABOVE; + break; + } + + case ABOVE_TO_BELOW: { + // console.log('ABOVE_TO_BELOW') + const x1 = xIntersection(minY, point, currentPoint); + const x2 = xIntersection(maxY, point, currentPoint); + markers.push({ + type: RIGHT, + position: Math.min(x1, x2) + }); + break; + } + + case INSIDE_TO_ABOVE: { + // console.log('INSIDE_TO_ABOVE') + const x1 = xIntersection(maxY, point, currentPoint); + max = Math.max(max, x1); + + markers.push({ type: LEFT, position: max }); + + if (wrapState === ABOVE) { + min = Math.min(min, x1); + markers.push({ type: RIGHT, position: min }); + } + + break; + } + + case INSIDE_TO_BELOW: { + // console.log('INSIDE_TO_BELOW') + const x1 = xIntersection(minY, point, currentPoint); + min = Math.min(min, x1); + + markers.push({ type: RIGHT, position: min }); + + if (wrapState === BELOW) { + max = Math.max(max, x1); + markers.push({ type: LEFT, position: max }); + } + + break; + } + + default: + throw new Error('Unknown state change'); + } + state = s; + } else if (s === INSIDE) { + min = Math.min(min, x); + max = Math.max(max, x); + } + + currentPoint = point; + } + } + + markers.sort((a, b) => a.position - b.position); + + let G = 0; + if (type === '' || (markers.length > 0 && markers[0].type === LEFT)) { + G++; + } + + let minX = lineRect.x; + const { maxX } = lineRect; + const { height } = lineRect; + const rects = []; + + for (const marker of markers) { + if (marker.type === RIGHT) { + if (G === 0) { + const p = Math.min(maxX, marker.position); + if (p >= minX) { + rects.push(new Rect(minX, minY, p - minX, height)); + } + } + + G++; + } else { + G--; + if (G === 0 && marker.position > minX) { + minX = marker.position; + } + } + } + + if (G === 0 && maxX >= minX) { + rects.push(new Rect(minX, minY, maxX - minX, height)); + } + + return rects; +}; + +/** + * A LineFragmentGenerator splits line rectangles into fragments, + * wrapping inside a container's polygon, and outside its exclusion polygon. + */ +const generateLineFragments = (lineRect, container) => { + const exclusion = container.exclusionPolygon; + const rects = splitLineRect(lineRect, container.polygon, INTERIOR); + + if (exclusion) { + const res = []; + for (const rect of rects) { + res.push(...splitLineRect(rect, exclusion, EXTERIOR)); + } + + return res; + } + + return rects; +}; + +const generateFragments = (paragraphRect, lineHeight, container) => { + const lineFragements = []; + let yCount = paragraphRect.y; + + while (paragraphRect.height + paragraphRect.y >= yCount + lineHeight) { + const lineRect = new Rect(paragraphRect.x, yCount, paragraphRect.width, lineHeight); + lineFragements.push(...generateLineFragments(lineRect, container)); + yCount += lineHeight; + } + + return lineFragements; +}; + +export default generateFragments; diff --git a/packages/core/src/layout/generateGlyphs.js b/packages/core/src/layout/generateGlyphs.js new file mode 100644 index 0000000..bc539ae --- /dev/null +++ b/packages/core/src/layout/generateGlyphs.js @@ -0,0 +1,70 @@ +import GlyphRun from '../models/GlyphRun'; +import GlyphString from '../models/GlyphString'; + +const resolveGlyphIndices = (string, stringIndices) => { + const glyphIndices = []; + + for (let i = 0; i < string.length; i++) { + for (let j = 0; j < stringIndices.length; j++) { + if (stringIndices[j] >= i) { + glyphIndices[i] = j; + break; + } + + glyphIndices[i] = undefined; + } + } + + let lastValue = glyphIndices[glyphIndices.length - 1]; + for (let i = glyphIndices.length - 1; i >= 0; i--) { + if (glyphIndices[i] === undefined) { + glyphIndices[i] = lastValue; + } else { + lastValue = glyphIndices[i]; + } + } + + lastValue = glyphIndices[0]; + for (let i = 0; i < glyphIndices.length; i++) { + if (glyphIndices[i] === undefined) { + glyphIndices[i] = lastValue; + } else { + lastValue = glyphIndices[i]; + } + } + + return glyphIndices; +}; + +const stringToGlyphs = attributedString => { + let glyphIndex = 0; + const glyphRuns = attributedString.runs.map(run => { + const { start, end, attributes } = run; + const str = attributedString.string.slice(start, end); + const glyphRun = run.attributes.font.layout(str, attributes.features, attributes.script); + const glyphEnd = glyphIndex + glyphRun.glyphs.length; + const glyphIndices = resolveGlyphIndices(str, glyphRun.stringIndices); + + const res = new GlyphRun( + glyphIndex, + glyphEnd, + attributes, + glyphRun.glyphs, + glyphRun.positions, + glyphRun.stringIndices, + glyphIndices + ); + + glyphIndex = glyphEnd; + return res; + }); + + return new GlyphString(attributedString.string, glyphRuns); +}; + +const generateGlyphs = () => paragraph => ({ + syllables: paragraph.syllables, + value: stringToGlyphs(paragraph.attributedString) +}); + +export default generateGlyphs; diff --git a/packages/core/src/layout/preprocessRuns.js b/packages/core/src/layout/preprocessRuns.js new file mode 100644 index 0000000..c9b887a --- /dev/null +++ b/packages/core/src/layout/preprocessRuns.js @@ -0,0 +1,26 @@ +import flattenRuns from './flattenRuns'; +import AttributedString from '../models/AttributedString'; + +const fontSubstitution = engines => attributedString => { + const { string, runs } = attributedString; + return engines.fontSubstitutionEngine.getRuns(string, runs); +}; + +const scriptItemization = engines => attributedString => + engines.scriptItemizer.getRuns(attributedString.string); + +const preprocessRuns = engines => attributedString => { + const fontRuns = fontSubstitution(engines)(attributedString); + const scriptRuns = scriptItemization(engines)(attributedString); + const stringRuns = attributedString.runs.map(run => { + const { + attributes: { font, ...attributes } + } = run; + return { ...run, attributes }; + }); + + const runs = flattenRuns([...stringRuns, ...fontRuns, ...scriptRuns]); + return new AttributedString(attributedString.string, runs); +}; + +export default preprocessRuns; diff --git a/packages/core/src/layout/resolveAttachments.js b/packages/core/src/layout/resolveAttachments.js new file mode 100644 index 0000000..b7ae920 --- /dev/null +++ b/packages/core/src/layout/resolveAttachments.js @@ -0,0 +1,18 @@ +const resolveAttachments = () => paragraph => { + for (const glyphRun of paragraph.value.glyphRuns) { + const { font, attachment } = glyphRun.attributes; + if (!attachment) continue; + const objectReplacement = font.glyphForCodePoint(0xfffc); + for (let i = 0; i < glyphRun.length; i++) { + const glyph = glyphRun.glyphs[i]; + const position = glyphRun.positions[i]; + if (glyph === objectReplacement) { + position.xAdvance = attachment.width; + } + } + } + + return paragraph; +}; + +export default resolveAttachments; diff --git a/packages/core/src/layout/resolveColumns.js b/packages/core/src/layout/resolveColumns.js new file mode 100644 index 0000000..eab6c61 --- /dev/null +++ b/packages/core/src/layout/resolveColumns.js @@ -0,0 +1,18 @@ +import Rect from '../geom/Rect'; + +const resolveColumns = container => { + const { bbox, columns, columnGap } = container; + const columnWidth = (bbox.width - columnGap * (columns - 1)) / columns; + + let x = bbox.minX; + const result = []; + + for (let index = 0; index < columns; index++) { + result.push(new Rect(x, bbox.minY, columnWidth, bbox.height)); + x += columnWidth + container.columnGap; + } + + return result; +}; + +export default resolveColumns; diff --git a/packages/core/src/layout/resolveYOffset.js b/packages/core/src/layout/resolveYOffset.js new file mode 100644 index 0000000..dd70cf6 --- /dev/null +++ b/packages/core/src/layout/resolveYOffset.js @@ -0,0 +1,13 @@ +const resolveYOffset = () => paragraph => { + for (const glyphRun of paragraph.value.glyphRuns) { + const { font, yOffset } = glyphRun.attributes; + if (!yOffset) continue; + for (let i = 0; i < glyphRun.length; i++) { + glyphRun.positions[i].yOffset += yOffset * font.unitsPerEm; + } + } + + return paragraph; +}; + +export default resolveYOffset; diff --git a/packages/core/src/layout/splitParagraphs.js b/packages/core/src/layout/splitParagraphs.js new file mode 100644 index 0000000..ff9bc96 --- /dev/null +++ b/packages/core/src/layout/splitParagraphs.js @@ -0,0 +1,20 @@ +const splitParagraphs = () => attributedString => { + const res = []; + + let start = 0; + let breakPoint = attributedString.string.indexOf('\n') + 1; + + while (breakPoint > 0) { + res.push(attributedString.slice(start, breakPoint)); + start = breakPoint; + breakPoint = attributedString.string.indexOf('\n', breakPoint) + 1; + } + + if (start < attributedString.length) { + res.push(attributedString.slice(start, attributedString.length)); + } + + return res; +}; + +export default splitParagraphs; diff --git a/packages/core/src/layout/wrapWords.js b/packages/core/src/layout/wrapWords.js new file mode 100644 index 0000000..c0cc541 --- /dev/null +++ b/packages/core/src/layout/wrapWords.js @@ -0,0 +1,26 @@ +import AttributedString from '../models/AttributedString'; + +const wrapWords = engines => attributedString => { + const syllables = []; + const fragments = []; + + for (const run of attributedString.runs) { + let string = ''; + const tokens = attributedString.string + .slice(run.start, run.end) + .split(/([ ]+)/g) + .filter(Boolean); + + for (const token of tokens) { + const parts = engines.wordHyphenation.hyphenateWord(token); + syllables.push(...parts); + string += parts.join(''); + } + + fragments.push({ string, attributes: run.attributes }); + } + + return { attributedString: AttributedString.fromFragments(fragments), syllables }; +}; + +export default wrapWords; diff --git a/packages/core/src/models/Container.js b/packages/core/src/models/Container.js index 1a717d4..2c5ce4e 100644 --- a/packages/core/src/models/Container.js +++ b/packages/core/src/models/Container.js @@ -3,12 +3,12 @@ import Path from '../geom/Path'; export default class Container { constructor(path, options = {}) { this.path = path; - this.exclusionPaths = options.exclusionPaths || []; - this.tabStops = options.tabStops || []; - this.tabStopInterval = options.tabStopInterval || 80; + this.blocks = []; this.columns = options.columns || 1; + this.tabStops = options.tabStops || []; this.columnGap = options.columnGap || 18; // 1/4 inch - this.blocks = []; + this.exclusionPaths = options.exclusionPaths || []; + this.tabStopInterval = options.tabStopInterval || 80; } get bbox() { diff --git a/packages/core/src/models/DecorationLine.js b/packages/core/src/models/DecorationLine.js index f1e4e76..d7304ca 100644 --- a/packages/core/src/models/DecorationLine.js +++ b/packages/core/src/models/DecorationLine.js @@ -9,7 +9,9 @@ export default class DecorationLine { merge(line) { if (this.rect.maxX === line.rect.x && this.rect.y === line.rect.y) { - this.rect.height = line.rect.height = Math.max(this.rect.height, line.rect.height); + const height = Math.max(this.rect.height, line.rect.height); + this.rect.height = height; + line.rect.height = height; if (this.color === line.color) { this.rect.width += line.rect.width; diff --git a/packages/core/src/models/GlyphRun.js b/packages/core/src/models/GlyphRun.js index ae5548a..71db9b2 100644 --- a/packages/core/src/models/GlyphRun.js +++ b/packages/core/src/models/GlyphRun.js @@ -11,16 +11,18 @@ class GlyphRun extends Run { this.scale = attributes.fontSize / attributes.font.unitsPerEm; if (!preScaled) { - this.positions.forEach((pos, index) => { + this.positions = this.positions.map((pos, index) => { const xAdvance = index === this.positions.length - 1 ? pos.xAdvance * this.scale : pos.xAdvance * this.scale + attributes.characterSpacing; - pos.xAdvance = xAdvance; - pos.yAdvance *= this.scale; - pos.xOffset *= this.scale; - pos.yOffset *= this.scale; + return { + xAdvance, + yAdvance: pos.yAdvance * this.scale, + xOffset: pos.xOffset * this.scale, + yOffset: pos.yOffset * this.scale + }; }); } } @@ -98,10 +100,10 @@ class GlyphRun extends Run { this.start, this.end, this.attributes, - this.glyphs, - this.positions, - this.stringIndices, - this.glyphIndices, + [...this.glyphs], + [...this.positions], + [...this.stringIndices], + [...this.glyphIndices], true ); } diff --git a/packages/core/src/models/GlyphString.js b/packages/core/src/models/GlyphString.js index 16676b7..1a9dc93 100644 --- a/packages/core/src/models/GlyphString.js +++ b/packages/core/src/models/GlyphString.js @@ -20,28 +20,71 @@ const HANGING_PUNCTUATION_END_CODEPOINTS = new Set([ 0x002d // HYPHEN ]); -class GlyphString { - constructor(string, glyphRuns, start, end) { - this.string = string; - this._glyphRuns = glyphRuns; - this.start = start || 0; - this._end = end; - this._glyphRunsCache = null; - this._glyphRunsCacheEnd = null; - } +const runIndexAtGlyphIndex = (glyphRuns, index) => { + let count = 0; - get end() { - if (this._glyphRuns.length === 0) { - return 0; + for (let i = 0; i < glyphRuns.length; i++) { + const run = glyphRuns[i]; + + if (count <= index && index < count + run.glyphs.length) { + return i; } - const glyphEnd = this._glyphRuns[this._glyphRuns.length - 1].end; + count += run.glyphs.length; + } + + return glyphRuns.length - 1; +}; - if (this._end) { - return Math.min(this._end, glyphEnd); +const sliceRuns = (glyphRuns, start, end) => { + if (glyphRuns.length === 0) return []; + + const startRunIndex = runIndexAtGlyphIndex(glyphRuns, start); + const endRunIndex = runIndexAtGlyphIndex(glyphRuns, end); + const startRun = glyphRuns[startRunIndex]; + const endRun = glyphRuns[endRunIndex]; + const runs = []; + + runs.push(startRun.slice(start - startRun.start, end - startRun.start)); + + if (endRunIndex !== startRunIndex) { + runs.push(...glyphRuns.slice(startRunIndex + 1, endRunIndex)); + + if (end - endRun.start !== 0) { + runs.push(endRun.slice(0, end - endRun.start)); } + } - return this._glyphRuns.length > 0 ? glyphEnd : 0; + for (const run of runs) { + run.start -= start; + run.end -= start; + run.stringIndices = run.stringIndices.map(s => s - start); + } + + return runs; +}; + +const normalizeStringIndices = glyphRuns => { + glyphRuns.forEach(run => { + run.stringIndices = run.stringIndices.map(index => index - run.stringIndices[0]); + }); + return glyphRuns; +}; + +class GlyphString { + constructor(string, glyphRuns = []) { + this.string = string; + this.glyphRuns = normalizeStringIndices(glyphRuns); + } + + get start() { + if (this.glyphRuns.length === 0) return 0; + return this.glyphRuns[0].start; + } + + get end() { + if (this.glyphRuns.length === 0) return 0; + return this.glyphRuns[this.glyphRuns.length - 1].end; } get length() { @@ -64,70 +107,37 @@ class GlyphString { return this.glyphRuns.reduce((acc, run) => Math.min(acc, run.descent), 0); } - get glyphRuns() { - if (this._glyphRunsCache && this._glyphRunsCacheEnd === this.end) { - return this._glyphRunsCache; - } - - if (this._glyphRuns.length === 0) { - this._glyphRunsCache = []; - this._glyphRunsCacheEnd = this.end; - return []; - } - - const startRunIndex = this.runIndexAtGlyphIndex(0); - const endRunIndex = this.runIndexAtGlyphIndex(this.length); - const startRun = this._glyphRuns[startRunIndex]; - const endRun = this._glyphRuns[endRunIndex]; - const runs = []; - - runs.push(startRun.slice(this.start - startRun.start, this.end - startRun.start)); - - if (endRunIndex !== startRunIndex) { - runs.push(...this._glyphRuns.slice(startRunIndex + 1, endRunIndex)); - - if (this.end - endRun.start !== 0) { - runs.push(endRun.slice(0, this.end - endRun.start)); - } - } - - this._glyphRunsCache = runs; - this._glyphRunsCacheEnd = this.end; - return runs; - } - slice(start, end) { const stringStart = this.stringIndexForGlyphIndex(start); const stringEnd = this.stringIndexForGlyphIndex(end); + const glyphRuns = sliceRuns(this.glyphRuns, start, end); - return new GlyphString( - this.string.slice(stringStart, stringEnd), - this._glyphRuns, - start + this.start, - end + this.start - ); - } + const result = new GlyphString(this.string.slice(stringStart, stringEnd), glyphRuns); - runIndexAtGlyphIndex(index) { - index += this.start; - let count = 0; + // Ligature splitting. If happens to slice in a ligature, we split create + const previousGlyph = this.glyphAtIndex(start - 1); + const lastGlyph = this.glyphAtIndex(end - 1); - for (let i = 0; i < this._glyphRuns.length; i++) { - const run = this._glyphRuns[i]; + if (lastGlyph && lastGlyph.isLigature) { + result.deleteGlyph(result.length - 1); + result.insertGlyph(result.length, lastGlyph.codePoints[0]); + } - if (count <= index && index < count + run.glyphs.length) { - return i; + // Add the ligature remaining chars to result + if (previousGlyph && previousGlyph.isLigature) { + for (let i = 1; i < previousGlyph.codePoints.length; i++) { + result.insertGlyph(i - 1, previousGlyph.codePoints[i]); } - - count += run.glyphs.length; } - return this._glyphRuns.length - 1; + return result; } - runAtGlyphIndex(index) { - index += this.start; + runIndexAtGlyphIndex(index) { + return runIndexAtGlyphIndex(this.glyphRuns, index); + } + runAtGlyphIndex(index) { for (let i = 0; i < this.glyphRuns.length; i++) { const run = this.glyphRuns[i]; @@ -152,7 +162,7 @@ class GlyphString { offset += run.stringEnd; } - return this._glyphRuns.length - 1; + return this.glyphRuns.length - 1; } runAtStringIndex(index) { @@ -210,15 +220,14 @@ class GlyphString { } stringIndexForGlyphIndex(index) { - let run; let count = 0; let offset = 0; for (let i = 0; i < this.glyphRuns.length; i++) { - run = this.glyphRuns[i]; + const run = this.glyphRuns[i]; if (offset <= index && offset + run.length > index) { - return count + run.stringIndices[index + this.start - run.start]; + return count + run.stringIndices[index - run.start]; } offset += run.length; @@ -277,9 +286,7 @@ class GlyphString { const stringIndex = this.stringIndexForGlyphIndex(index); const nextIndex = this.string.indexOf(string, stringIndex); - if (nextIndex === -1) { - return -1; - } + if (nextIndex === -1) return -1; return this.glyphIndexForStringIndex(nextIndex); } @@ -307,15 +314,13 @@ class GlyphString { insertGlyph(index, codePoint) { const runIndex = this.runIndexAtGlyphIndex(index); - const run = this._glyphRuns[runIndex]; + const run = this.glyphRuns[runIndex]; const { font, fontSize } = run.attributes; const glyph = run.attributes.font.glyphForCodePoint(codePoint); const scale = fontSize / font.unitsPerEm; const glyphIndex = this.start + index - run.start; - if (this._end) { - this._end += 1; - } + if (this._end) this._end += 1; run.glyphs.splice(glyphIndex, 0, glyph); run.stringIndices.splice(glyphIndex, 0, run.stringIndices[glyphIndex]); @@ -335,23 +340,23 @@ class GlyphString { run.end += 1; - for (let i = runIndex + 1; i < this._glyphRuns.length; i++) { - this._glyphRuns[i].start += 1; - this._glyphRuns[i].end += 1; + for (let i = runIndex + 1; i < this.glyphRuns.length; i++) { + this.glyphRuns[i].start += 1; + this.glyphRuns[i].end += 1; } - this._glyphRunsCache = null; + this.glyphRunsCache = null; } deleteGlyph(index) { - if (index < 0 || index >= this.length) { - return; - } + if (index < 0 || index >= this.length) return; const runIndex = this.runIndexAtGlyphIndex(index); - const run = this._glyphRuns[runIndex]; + const run = this.glyphRuns[runIndex]; const glyphIndex = this.start + index - run.start; + if (this._end) this._end -= 1; + run.glyphs.splice(glyphIndex, 1); run.positions.splice(glyphIndex, 1); run.stringIndices.splice(glyphIndex, 1); @@ -364,12 +369,12 @@ class GlyphString { run.end--; - for (let i = runIndex + 1; i < this._glyphRuns.length; i++) { - this._glyphRuns[i].start--; - this._glyphRuns[i].end--; + for (let i = runIndex + 1; i < this.glyphRuns.length; i++) { + this.glyphRuns[i].start--; + this.glyphRuns[i].end--; } - this._glyphRunsCache = null; + this.glyphRunsCache = null; } *[Symbol.iterator]() { diff --git a/packages/core/src/models/LineFragment.js b/packages/core/src/models/LineFragment.js index dad5145..aa574bd 100644 --- a/packages/core/src/models/LineFragment.js +++ b/packages/core/src/models/LineFragment.js @@ -2,7 +2,8 @@ import GlyphString from './GlyphString'; export default class LineFragment extends GlyphString { constructor(rect, glyphString) { - super(glyphString.string, glyphString._glyphRuns, glyphString.start, glyphString.end); + super(glyphString.string, glyphString.glyphRuns); + this.rect = rect; this.decorationLines = []; this.overflowLeft = 0; diff --git a/packages/core/src/models/ParagraphStyle.js b/packages/core/src/models/ParagraphStyle.js deleted file mode 100644 index 59ade54..0000000 --- a/packages/core/src/models/ParagraphStyle.js +++ /dev/null @@ -1,21 +0,0 @@ -export default class ParagraphStyle { - constructor(attributes = {}) { - this.indent = attributes.indent || 0; - this.bullet = attributes.bullet || null; - this.paddingTop = attributes.paddingTop || attributes.padding || 0; - this.paragraphSpacing = attributes.paragraphSpacing || 0; - this.marginLeft = attributes.marginLeft || attributes.margin || 0; - this.marginRight = attributes.marginRight || attributes.margin || 0; - this.align = attributes.align || 'left'; - this.alignLastLine = - attributes.alignLastLine || (this.align === 'justify' ? 'left' : this.align); - this.justificationFactor = attributes.justificationFactor || 1; - this.hyphenationFactor = attributes.hyphenationFactor || 0; - this.shrinkFactor = attributes.shrinkFactor || 0; - this.lineSpacing = attributes.lineSpacing || 0; - this.lineHeight = attributes.lineHeight || 0; - this.hangingPunctuation = attributes.hangingPunctuation || false; - this.truncationMode = attributes.truncationMode || (attributes.truncate ? 'right' : null); - this.maxLines = attributes.maxLines || Infinity; - } -} diff --git a/packages/core/src/models/Range.js b/packages/core/src/models/Range.js index 37be21d..cc5b8bc 100644 --- a/packages/core/src/models/Range.js +++ b/packages/core/src/models/Range.js @@ -1,75 +1,30 @@ -/** - * This class represents a numeric range between - * a starting and ending value, inclusive. - */ class Range { - /** - * Creates a new Range - * @param {number} start the starting index of the range - * @param {number} end the ending index of the range, inclusive - */ constructor(start, end) { - /** - * The starting index of the range - * @type {number} - */ this.start = start; - - /** - * The ending index of the range, inclusive - * @type {number} - */ this.end = end; } - /** - * The length of the range - * @type {number} - */ get length() { return this.end - this.start + 1; } - /** - * Returns whether this range is equal to the given range - * @param {Range} other the range to compare - * @return {boolean} - */ equals(other) { return other.start === this.start && other.end === this.end; } - /** - * Returns a copy of the range - * @return {Range} - */ copy() { return new Range(this.start, this.end); } - /** - * Returns whether the given value is in the range - * @param {number} index the index to check - * @return {boolean} - */ contains(index) { return index >= this.start && index <= this.end; } - /** - * Extends the range to include the given index - * @param {number} index the index to ad - */ extend(index) { this.start = Math.min(this.start, index); this.end = Math.max(this.end, index); } - /** - * Merge intersecting ranges - * @param {number} ranges array of valid Range objects - * @return {array} - */ static merge(ranges) { ranges.sort((a, b) => a.start - b.start); diff --git a/packages/core/src/models/RunStyle.js b/packages/core/src/models/RunStyle.js deleted file mode 100644 index 469eaf0..0000000 --- a/packages/core/src/models/RunStyle.js +++ /dev/null @@ -1,28 +0,0 @@ -import FontDescriptor from './FontDescriptor'; - -export default class RunStyle { - constructor(attributes = {}) { - this.color = attributes.color || 'black'; - this.backgroundColor = attributes.backgroundColor || null; - this.fontDescriptor = FontDescriptor.fromAttributes(attributes); - this.font = attributes.font || null; - this.fontSize = attributes.fontSize || 12; - this.lineHeight = attributes.lineHeight || null; - this.underline = attributes.underline || false; - this.underlineColor = attributes.underlineColor || this.color; - this.underlineStyle = attributes.underlineStyle || 'solid'; - this.strike = attributes.strike || false; - this.strikeColor = attributes.strikeColor || this.color; - this.strikeStyle = attributes.strikeStyle || 'solid'; - this.link = attributes.link || null; - this.fill = attributes.fill !== false; - this.stroke = attributes.stroke || false; - this.features = attributes.features || []; - this.wordSpacing = attributes.wordSpacing || 0; - this.yOffset = attributes.yOffset || 0; - this.characterSpacing = attributes.characterSpacing || 0; - this.attachment = attributes.attachment || null; - this.script = attributes.script || null; - this.bidiLevel = attributes.bidiLevel || null; - } -} diff --git a/packages/core/src/models/TabStop.js b/packages/core/src/models/TabStop.js deleted file mode 100644 index df337f9..0000000 --- a/packages/core/src/models/TabStop.js +++ /dev/null @@ -1,6 +0,0 @@ -export default class TabStop { - constructor(x, align = 'left') { - this.x = x; - this.align = align; - } -} diff --git a/packages/core/src/models/index.js b/packages/core/src/models/index.js index a37039d..4331679 100644 --- a/packages/core/src/models/index.js +++ b/packages/core/src/models/index.js @@ -1,14 +1,10 @@ export Run from './Run'; export Block from './Block'; export Range from './Range'; -export TabStop from './TabStop'; -export RunStyle from './RunStyle'; export GlyphRun from './GlyphRun'; export Container from './Container'; export Attachment from './Attachment'; export GlyphString from './GlyphString'; export LineFragment from './LineFragment'; -export ParagraphStyle from './ParagraphStyle'; export DecorationLine from './DecorationLine'; -export FontDescriptor from './FontDescriptor'; export AttributedString from './AttributedString'; diff --git a/packages/core/test/layout/LayoutEngine.test.js b/packages/core/test/layout/LayoutEngine.test.js index 83322a4..683f7ed 100644 --- a/packages/core/test/layout/LayoutEngine.test.js +++ b/packages/core/test/layout/LayoutEngine.test.js @@ -291,14 +291,6 @@ describe('LayoutEngine', () => { expect(layout.typesetter.lineBreaker.constructor.name).toBe(TestEngine.name); }); - test('should be able to inject custom lineFragmentGenerator engine', () => { - const layout = new LayoutEngine({ - lineFragmentGenerator: createEngine - }); - - expect(layout.typesetter.lineFragmentGenerator.constructor.name).toBe(TestEngine.name); - }); - test('should be able to inject custom justificationEngine engine', () => { const layout = new LayoutEngine({ justificationEngine: createEngine @@ -307,14 +299,6 @@ describe('LayoutEngine', () => { expect(layout.typesetter.justificationEngine.constructor.name).toBe(TestEngine.name); }); - test('should be able to inject custom truncationEngine engine', () => { - const layout = new LayoutEngine({ - truncationEngine: createEngine - }); - - expect(layout.typesetter.truncationEngine.constructor.name).toBe(TestEngine.name); - }); - test('should be able to inject custom decorationEngine engine', () => { const layout = new LayoutEngine({ decorationEngine: createEngine @@ -322,12 +306,4 @@ describe('LayoutEngine', () => { expect(layout.typesetter.decorationEngine.constructor.name).toBe(TestEngine.name); }); - - test('should be able to inject custom tabEngine engine', () => { - const layout = new LayoutEngine({ - tabEngine: createEngine - }); - - expect(layout.typesetter.tabEngine.constructor.name).toBe(TestEngine.name); - }); }); diff --git a/packages/core/test/models/GlyphString.test.js b/packages/core/test/models/GlyphString.test.js index 1854884..a8cba2c 100644 --- a/packages/core/test/models/GlyphString.test.js +++ b/packages/core/test/models/GlyphString.test.js @@ -9,18 +9,6 @@ describe('GlyphString', () => { expect(string.string).toBe('Lorem ipsum'); }); - test('should get string end', () => { - const string = createLatinTestString({ value: 'Lorem ipsum' }); - - expect(string.end).toBe(11); - }); - - test('should get string end (non latin)', () => { - const string = createCamboyanTestString({ value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន' }); - - expect(string.end).toBe(16); - }); - test('should get string length', () => { const string = createLatinTestString({ value: 'Lorem ipsum' }); @@ -92,10 +80,10 @@ describe('GlyphString', () => { const sliced = string.slice(2, 8); expect(sliced.glyphRuns).toHaveLength(2); - expect(sliced.glyphRuns[0].start).toBe(2); - expect(sliced.glyphRuns[0].end).toBe(6); - expect(sliced.glyphRuns[1].start).toBe(6); - expect(sliced.glyphRuns[1].end).toBe(8); + expect(sliced.glyphRuns[0].start).toBe(0); + expect(sliced.glyphRuns[0].end).toBe(4); + expect(sliced.glyphRuns[1].start).toBe(4); + expect(sliced.glyphRuns[1].end).toBe(6); }); test('should get glyphs run for sliced string (non latin)', () => { @@ -107,10 +95,10 @@ describe('GlyphString', () => { const sliced = string.slice(1, 15); expect(sliced.glyphRuns).toHaveLength(2); - expect(sliced.glyphRuns[0].start).toBe(1); - expect(sliced.glyphRuns[0].end).toBe(7); - expect(sliced.glyphRuns[1].start).toBe(7); - expect(sliced.glyphRuns[1].end).toBe(15); + expect(sliced.glyphRuns[0].start).toBe(0); + expect(sliced.glyphRuns[0].end).toBe(6); + expect(sliced.glyphRuns[1].start).toBe(6); + expect(sliced.glyphRuns[1].end).toBe(14); }); test('should isWhiteSpace return true if white space', () => { @@ -138,9 +126,10 @@ describe('GlyphString', () => { const string = createLatinTestString({ value: 'Lorem ipsum' }); const sliced = string.slice(2, 6); + expect(sliced.length).toBe(4); expect(sliced.string).toBe('rem '); - expect(sliced.glyphRuns[0].start).toBe(2); - expect(sliced.glyphRuns[0].end).toBe(6); + expect(sliced.glyphRuns[0].start).toBe(0); + expect(sliced.glyphRuns[0].end).toBe(4); expect(sliced.glyphRuns[0].glyphs.length).toBe(4); }); @@ -148,9 +137,10 @@ describe('GlyphString', () => { const string = createLatinTestString({ value: 'Lorem ipsum' }); const sliced = string.slice(2, 14); + expect(sliced.length).toBe(9); expect(sliced.string).toBe('rem ipsum'); - expect(sliced.glyphRuns[0].start).toBe(2); - expect(sliced.glyphRuns[0].end).toBe(11); + expect(sliced.glyphRuns[0].start).toBe(0); + expect(sliced.glyphRuns[0].end).toBe(9); expect(sliced.glyphRuns[0].glyphs.length).toBe(9); }); @@ -162,9 +152,10 @@ describe('GlyphString', () => { const sliced = string.slice(2, 6); + expect(sliced.length).toBe(4); expect(sliced.glyphRuns).toHaveLength(1); - expect(sliced.glyphRuns[0].start).toBe(2); - expect(sliced.glyphRuns[0].end).toBe(6); + expect(sliced.glyphRuns[0].start).toBe(0); + expect(sliced.glyphRuns[0].end).toBe(4); }); test('should ignore unnecesary leading runs when slice', () => { @@ -175,9 +166,11 @@ describe('GlyphString', () => { const sliced = string.slice(6, 9); + expect(sliced.length).toBe(3); + expect(sliced.string).toBe('ips'); expect(sliced.glyphRuns).toHaveLength(1); - expect(sliced.glyphRuns[0].start).toBe(6); - expect(sliced.glyphRuns[0].end).toBe(9); + expect(sliced.glyphRuns[0].start).toBe(0); + expect(sliced.glyphRuns[0].end).toBe(3); }); test('should return correct run index at glyph index', () => { @@ -250,16 +243,16 @@ describe('GlyphString', () => { const sliced = string.slice(4, 11); - expect(sliced.runAtGlyphIndex(0).start).toBe(4); - expect(sliced.runAtGlyphIndex(1).start).toBe(4); - expect(sliced.runAtGlyphIndex(2).start).toBe(6); - expect(sliced.runAtGlyphIndex(5).start).toBe(6); + expect(sliced.runAtGlyphIndex(0).start).toBe(0); + expect(sliced.runAtGlyphIndex(1).start).toBe(0); + expect(sliced.runAtGlyphIndex(2).start).toBe(2); + expect(sliced.runAtGlyphIndex(5).start).toBe(2); const sliced2 = string.slice(7, 11); - expect(sliced2.runAtGlyphIndex(0).start).toBe(7); - expect(sliced2.runAtGlyphIndex(1).start).toBe(7); - expect(sliced2.runAtGlyphIndex(2).start).toBe(7); + expect(sliced2.runAtGlyphIndex(0).start).toBe(0); + expect(sliced2.runAtGlyphIndex(1).start).toBe(0); + expect(sliced2.runAtGlyphIndex(2).start).toBe(0); }); test('should return correct run index at string index', () => { @@ -347,10 +340,12 @@ describe('GlyphString', () => { const sliced = string.slice(4, 11); - expect(sliced.runAtStringIndex(0).start).toBe(4); - expect(sliced.runAtStringIndex(1).start).toBe(4); - expect(sliced.runAtStringIndex(2).start).toBe(6); - expect(sliced.runAtStringIndex(5).start).toBe(6); + expect(sliced.string).toBe('m ipsum'); + expect(sliced.runAtStringIndex(0).start).toBe(0); + expect(sliced.runAtStringIndex(1).start).toBe(0); + expect(sliced.runAtStringIndex(2).start).toBe(2); + expect(sliced.runAtStringIndex(5).start).toBe(2); + expect(sliced.runAtStringIndex(5).end).toBe(7); }); test('should return correct run at string index for sliced strings (non latin)', () => { @@ -361,11 +356,11 @@ describe('GlyphString', () => { const sliced = string.slice(4, 11); - expect(sliced.runAtStringIndex(0).start).toBe(4); - expect(sliced.runAtStringIndex(1).start).toBe(4); - expect(sliced.runAtStringIndex(2).start).toBe(4); - expect(sliced.runAtStringIndex(3).start).toBe(7); - expect(sliced.runAtStringIndex(6).start).toBe(7); + expect(sliced.runAtStringIndex(0).start).toBe(0); + expect(sliced.runAtStringIndex(1).start).toBe(0); + expect(sliced.runAtStringIndex(2).start).toBe(0); + expect(sliced.runAtStringIndex(3).start).toBe(3); + expect(sliced.runAtStringIndex(6).start).toBe(3); }); test('should return correct glyph at index', () => { @@ -374,8 +369,8 @@ describe('GlyphString', () => { runs: [[0, 6], [6, 11]] }); - const firstRunGlyphs = string._glyphRuns[0].glyphs; - const secondRunGlyphs = string._glyphRuns[1].glyphs; + const firstRunGlyphs = string.glyphRuns[0].glyphs; + const secondRunGlyphs = string.glyphRuns[1].glyphs; expect(string.glyphAtIndex(2).id).toBe(firstRunGlyphs[2].id); expect(string.glyphAtIndex(9).id).toBe(secondRunGlyphs[3].id); @@ -387,8 +382,8 @@ describe('GlyphString', () => { runs: [[0, 8], [8, 21]] }); - const firstRunGlyphs = string._glyphRuns[0].glyphs; - const secondRunGlyphs = string._glyphRuns[1].glyphs; + const firstRunGlyphs = string.glyphRuns[0].glyphs; + const secondRunGlyphs = string.glyphRuns[1].glyphs; expect(string.glyphAtIndex(2).id).toBe(firstRunGlyphs[2].id); expect(string.glyphAtIndex(6).id).toBe(firstRunGlyphs[6].id); @@ -403,11 +398,11 @@ describe('GlyphString', () => { }); const sliced = string.slice(4, 11); - const firstRunGlyphs = sliced._glyphRuns[0].glyphs; - const secondRunGlyphs = sliced._glyphRuns[1].glyphs; + const firstRunGlyphs = sliced.glyphRuns[0].glyphs; + const secondRunGlyphs = sliced.glyphRuns[1].glyphs; - expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[4].id); - expect(sliced.glyphAtIndex(1).id).toBe(firstRunGlyphs[5].id); + expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[0].id); + expect(sliced.glyphAtIndex(1).id).toBe(firstRunGlyphs[1].id); expect(sliced.glyphAtIndex(2).id).toBe(secondRunGlyphs[0].id); expect(sliced.glyphAtIndex(5).id).toBe(secondRunGlyphs[3].id); }); @@ -419,11 +414,11 @@ describe('GlyphString', () => { }); const sliced = string.slice(4, 11); - const firstRunGlyphs = sliced._glyphRuns[0].glyphs; - const secondRunGlyphs = sliced._glyphRuns[1].glyphs; + const firstRunGlyphs = sliced.glyphRuns[0].glyphs; + const secondRunGlyphs = sliced.glyphRuns[1].glyphs; - expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[4].id); - expect(sliced.glyphAtIndex(2).id).toBe(firstRunGlyphs[6].id); + expect(sliced.glyphAtIndex(0).id).toBe(firstRunGlyphs[0].id); + expect(sliced.glyphAtIndex(2).id).toBe(firstRunGlyphs[2].id); expect(sliced.glyphAtIndex(3).id).toBe(secondRunGlyphs[0].id); }); @@ -433,8 +428,8 @@ describe('GlyphString', () => { runs: [[0, 6], [6, 11]] }); - const firstRunPositions = string._glyphRuns[0].positions; - const secondRunPositions = string._glyphRuns[1].positions; + const firstRunPositions = string.glyphRuns[0].positions; + const secondRunPositions = string.glyphRuns[1].positions; expect(string.getGlyphWidth(2)).toBe(firstRunPositions[2].xAdvance); expect(string.getGlyphWidth(9)).toBe(secondRunPositions[3].xAdvance); @@ -446,8 +441,8 @@ describe('GlyphString', () => { runs: [[0, 8], [8, 21]] }); - const firstRunPositions = string._glyphRuns[0].positions; - const secondRunPositions = string._glyphRuns[1].positions; + const firstRunPositions = string.glyphRuns[0].positions; + const secondRunPositions = string.glyphRuns[1].positions; expect(string.getGlyphWidth(0)).toBe(firstRunPositions[0].xAdvance); expect(string.getGlyphWidth(6)).toBe(firstRunPositions[6].xAdvance); @@ -462,11 +457,11 @@ describe('GlyphString', () => { }); const sliced = string.slice(4, 11); - const firstRunPositions = sliced._glyphRuns[0].positions; - const secondRunPositions = sliced._glyphRuns[1].positions; + const firstRunPositions = sliced.glyphRuns[0].positions; + const secondRunPositions = sliced.glyphRuns[1].positions; - expect(sliced.getGlyphWidth(0)).toBe(firstRunPositions[4].xAdvance); - expect(sliced.getGlyphWidth(1)).toBe(firstRunPositions[5].xAdvance); + expect(sliced.getGlyphWidth(0)).toBe(firstRunPositions[0].xAdvance); + expect(sliced.getGlyphWidth(1)).toBe(firstRunPositions[1].xAdvance); expect(sliced.getGlyphWidth(2)).toBe(secondRunPositions[0].xAdvance); expect(sliced.getGlyphWidth(5)).toBe(secondRunPositions[3].xAdvance); }); @@ -478,11 +473,11 @@ describe('GlyphString', () => { }); const sliced = string.slice(4, 11); - const firstRunPositions = sliced._glyphRuns[0].positions; - const secondRunPositions = sliced._glyphRuns[1].positions; + const firstRunPositions = sliced.glyphRuns[0].positions; + const secondRunPositions = sliced.glyphRuns[1].positions; - expect(sliced.getGlyphWidth(0)).toBe(firstRunPositions[4].xAdvance); - expect(sliced.getGlyphWidth(1)).toBe(firstRunPositions[5].xAdvance); + expect(sliced.getGlyphWidth(0)).toBe(firstRunPositions[0].xAdvance); + expect(sliced.getGlyphWidth(1)).toBe(firstRunPositions[1].xAdvance); expect(sliced.getGlyphWidth(3)).toBe(secondRunPositions[0].xAdvance); expect(sliced.getGlyphWidth(4)).toBe(secondRunPositions[1].xAdvance); }); @@ -660,21 +655,21 @@ describe('GlyphString', () => { expect(string.glyphIndexForStringIndex(10)).toBe(10); }); - test('should return correct glyph index for string index (not latin)', () => { - const string = createCamboyanTestString({ - value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', - runs: [[0, 8], [8, 21]] - }); + // test('should return correct glyph index for string index (not latin)', () => { + // const string = createCamboyanTestString({ + // value: 'ខ្ញុំអាចញ៉ាំកញ្ចក់បាន', + // runs: [[0, 8], [8, 21]] + // }); - // console.log(string.string); - // console.log(string.glyphRuns); + // // console.log(string.string); + // // console.log(string.glyphRuns); - // expect(string.glyphIndexForStringIndex(0)).toBe(0); - // expect(string.glyphIndexForStringIndex(4)).toBe(3); - // expect(string.glyphIndexForStringIndex(7)).toBe(6); - // expect(string.glyphIndexForStringIndex(8)).toBe(7); - // expect(string.glyphIndexForStringIndex(12)).toBe(8); - }); + // // expect(string.glyphIndexForStringIndex(0)).toBe(0); + // // expect(string.glyphIndexForStringIndex(4)).toBe(3); + // // expect(string.glyphIndexForStringIndex(7)).toBe(6); + // // expect(string.glyphIndexForStringIndex(8)).toBe(7); + // // expect(string.glyphIndexForStringIndex(12)).toBe(8); + // }); test('should return correct glyph index for string index for sliced strings', () => { const string = createLatinTestString({ @@ -846,8 +841,6 @@ describe('GlyphString', () => { string.insertGlyph(2, char); - expect(string.start).toBe(0); - expect(string.end).toBe(12); expect(string.glyphRuns[0].start).toBe(0); expect(string.glyphRuns[0].end).toBe(7); expect(string.glyphRuns[1].start).toBe(7); @@ -872,18 +865,13 @@ describe('GlyphString', () => { sliced.insertGlyph(2, char); - expect(sliced.start).toBe(4); - expect(sliced.end).toBe(12); - expect(sliced._glyphRuns[0].start).toBe(0); - expect(sliced._glyphRuns[0].end).toBe(6); - expect(sliced._glyphRuns[1].start).toBe(6); - expect(sliced._glyphRuns[1].end).toBe(12); - expect(sliced._glyphRuns[1].glyphs[0].id).toBe(45); - - // Test string indices. - // The new glyph shouldn't interfer with current indices - expect(string._glyphRuns[1].stringIndices[0]).toBe(0); - expect(string._glyphRuns[1].stringIndices[1]).toBe(0); + expect(sliced.start).toBe(0); + expect(sliced.end).toBe(8); + expect(sliced.glyphRuns[0].start).toBe(0); + expect(sliced.glyphRuns[0].end).toBe(2); + expect(sliced.glyphRuns[1].start).toBe(2); + expect(sliced.glyphRuns[1].end).toBe(8); + expect(sliced.glyphRuns[1].glyphs[0].id).toBe(45); }); test('should be able to iterate through glyph runs', () => { @@ -931,7 +919,7 @@ describe('GlyphString', () => { } expect(glyphs).toEqual([109, 32, 105, 112]); - expect(indices).toEqual([4, 5, 6, 7]); + expect(indices).toEqual([0, 1, 2, 3]); expect(positions).toEqual([6, 3, 6, 6]); expect(xs).toEqual([0, 6, 9, 15]); }); diff --git a/packages/core/test/models/TabStop.test.js b/packages/core/test/models/TabStop.test.js deleted file mode 100644 index 96f02da..0000000 --- a/packages/core/test/models/TabStop.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import TabStop from '../../src/models/TabStop'; - -describe('TabStop', () => { - test('should handle have left alignment by default', () => { - const tab = new TabStop(); - - expect(tab).toHaveProperty('align', 'left'); - }); - - test('should handle passed x', () => { - const tab = new TabStop(5); - - expect(tab).toHaveProperty('x', 5); - }); - - test('should handle passed align', () => { - const tab = new TabStop(5, 'center'); - - expect(tab).toHaveProperty('align', 'center'); - }); -}); diff --git a/packages/core/test/utils/glyphRuns.js b/packages/core/test/utils/glyphRuns.js index 2a4619d..83bd054 100644 --- a/packages/core/test/utils/glyphRuns.js +++ b/packages/core/test/utils/glyphRuns.js @@ -1,4 +1,3 @@ -import RunStyle from '../../src/models/RunStyle'; import GlyphRun from '../../src/models/GlyphRun'; import testFont from './font'; @@ -56,9 +55,7 @@ export const layout = value => { const glyphs = chars.map(char => ({ id: char.charCodeAt(0) })); const stringIndices = chars.map((_, index) => value.indexOf(chars[index], index)); const glyphIndices = resolveGlyphIndices(value, stringIndices); - const positions = chars.map(char => ({ - xAdvance: char === ' ' ? 512 : 1024 - })); + const positions = chars.map(char => ({ xAdvance: char === ' ' ? 512 : 1024 })); return { glyphs, @@ -79,7 +76,7 @@ export const createLatinTestRun = ({ end = value.length } = {}) => { const string = value.slice(start, end); - const attrs = new RunStyle(Object.assign({}, { font: testFont }, attributes)); + const attrs = { font: testFont, fontSize: 12, characterSpacing: 0, ...attributes }; const { glyphs, positions, stringIndices, glyphIndices } = layout(string); return new GlyphRun( @@ -110,7 +107,7 @@ export const createCamboyanTestRun = ({ const startGlyphIndex = getIndex(start); const endGlyphIndex = getIndex(end); const string = value.slice(startGlyphIndex, endGlyphIndex); - const attrs = new RunStyle(Object.assign({}, { font: testFont }, attributes)); + const attrs = { font: testFont, fontSize: 12, characterSpacing: 0, ...attributes }; const positions = [ { xAdvance: 1549 }, { xAdvance: 0 }, diff --git a/packages/core/test/utils/glyphStrings.js b/packages/core/test/utils/glyphStrings.js index b0a1711..4bc37ec 100644 --- a/packages/core/test/utils/glyphStrings.js +++ b/packages/core/test/utils/glyphStrings.js @@ -1,4 +1,3 @@ -import RunStyle from '../../src/models/RunStyle'; import GlyphRun from '../../src/models/GlyphRun'; import GlyphString from '../../src/models/GlyphString'; import { layout, createCamboyanTestRun } from './glyphRuns'; @@ -9,7 +8,7 @@ export const createLatinTestString = ({ runs = [[0, value.length]] } = {}) => { let glyphIndex = 0; - const attrs = new RunStyle(Object.assign({}, { font: testFont })); + const attrs = { font: testFont, fontSize: 12, characterSpacing: 0 }; const glyphRuns = runs.map(run => { const { glyphs, positions, stringIndices, glyphIndices } = layout(value.slice(run[0], run[1])); @@ -29,7 +28,7 @@ export const createLatinTestString = ({ return glyphRun; }); - return new GlyphString(value, glyphRuns, 0, value.length); + return new GlyphString(value, glyphRuns); }; export const createCamboyanTestString = ({ diff --git a/packages/font-substitution-engine/package.json b/packages/font-substitution-engine/package.json index 45a9c20..20feb79 100644 --- a/packages/font-substitution-engine/package.json +++ b/packages/font-substitution-engine/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest --config ./jest.config.js" @@ -17,7 +17,7 @@ "author": "Devon Govett ", "license": "MIT", "dependencies": { - "font-manager": "^0.2.2", + "font-manager": "^0.3.0", "fontkit": "^1.7.7" } } diff --git a/packages/justification-engine/package.json b/packages/justification-engine/package.json index 0f65a97..a0cba5d 100644 --- a/packages/justification-engine/package.json +++ b/packages/justification-engine/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" diff --git a/packages/line-fragment-generator/index.js b/packages/line-fragment-generator/index.js deleted file mode 100644 index 3e782d7..0000000 --- a/packages/line-fragment-generator/index.js +++ /dev/null @@ -1,211 +0,0 @@ -const BELOW = 1; -const INSIDE = 2; -const ABOVE = 3; - -const BELOW_TO_INSIDE = (BELOW << 4) | INSIDE; -const BELOW_TO_ABOVE = (BELOW << 4) | ABOVE; -const INSIDE_TO_BELOW = (INSIDE << 4) | BELOW; -const INSIDE_TO_ABOVE = (INSIDE << 4) | ABOVE; -const ABOVE_TO_INSIDE = (ABOVE << 4) | INSIDE; -const ABOVE_TO_BELOW = (ABOVE << 4) | BELOW; - -const LEFT = 0; -const RIGHT = 1; - -/** - * A LineFragmentGenerator splits line rectangles into fragments, - * wrapping inside a container's polygon, and outside its exclusion polygon. - */ -export default () => ({ Rect }) => - class LineFragmentGenerator { - generateFragments(lineRect, container) { - const rects = this.splitLineRect(lineRect, container.polygon, 'INTERIOR'); - const exclusion = container.exclusionPolygon; - - if (exclusion) { - const res = []; - for (const rect of rects) { - res.push(...this.splitLineRect(rect, exclusion, 'EXTERIOR')); - } - - return res; - } - - return rects; - } - - splitLineRect(lineRect, polygon, type) { - const minY = lineRect.y; - const maxY = lineRect.maxY; - const markers = []; - let wrapState = BELOW; - let min = Infinity; - let max = -Infinity; - - for (let i = 0; i < polygon.contours.length; i++) { - const contour = polygon.contours[i]; - let index = -1; - let state = -1; - - // Find the first point outside the line rect. - do { - const point = contour[++index]; - state = point.y <= minY ? BELOW : point.y >= maxY ? ABOVE : INSIDE; - } while (state === INSIDE && index < contour.length - 1); - - // Contour is entirely inside the line rect. Skip it. - if (state === INSIDE) { - continue; - } - - const dir = type === 'EXTERIOR' ? 1 : -1; - let idx = type === 'EXTERIOR' ? index : contour.length + index; - let currentPoint; - - for (let index = 0; index <= contour.length; index++, idx += dir) { - const point = contour[idx % contour.length]; - - if (index === 0) { - currentPoint = point; - state = point.y <= minY ? BELOW : point.y >= maxY ? ABOVE : INSIDE; - continue; - } - - const s = point.y <= minY ? BELOW : point.y >= maxY ? ABOVE : INSIDE; - const x = point.x; - - if (s !== state) { - const stateChangeType = (state << 4) | s; - switch (stateChangeType) { - case BELOW_TO_INSIDE: { - // console.log('BELOW_TO_INSIDE') - const xIntercept = xIntersection(minY, point, currentPoint); - min = Math.min(xIntercept, x); - max = Math.max(xIntercept, x); - wrapState = BELOW; - break; - } - - case BELOW_TO_ABOVE: { - // console.log('BELOW_TO_ABOVE') - const x1 = xIntersection(minY, point, currentPoint); - const x2 = xIntersection(maxY, point, currentPoint); - markers.push({ - type: LEFT, - position: Math.max(x1, x2) - }); - break; - } - - case ABOVE_TO_INSIDE: { - // console.log('ABOVE_TO_INSIDE') - const xIntercept = xIntersection(maxY, point, currentPoint); - min = Math.min(xIntercept, x); - max = Math.max(xIntercept, x); - wrapState = ABOVE; - break; - } - - case ABOVE_TO_BELOW: { - // console.log('ABOVE_TO_BELOW') - const x1 = xIntersection(minY, point, currentPoint); - const x2 = xIntersection(maxY, point, currentPoint); - markers.push({ - type: RIGHT, - position: Math.min(x1, x2) - }); - break; - } - - case INSIDE_TO_ABOVE: { - // console.log('INSIDE_TO_ABOVE') - const x1 = xIntersection(maxY, point, currentPoint); - max = Math.max(max, x1); - - markers.push({ type: LEFT, position: max }); - - if (wrapState === ABOVE) { - min = Math.min(min, x1); - markers.push({ type: RIGHT, position: min }); - } - - break; - } - - case INSIDE_TO_BELOW: { - // console.log('INSIDE_TO_BELOW') - const x1 = xIntersection(minY, point, currentPoint); - min = Math.min(min, x1); - - markers.push({ type: RIGHT, position: min }); - - if (wrapState === BELOW) { - max = Math.max(max, x1); - markers.push({ type: LEFT, position: max }); - } - - break; - } - - default: - throw new Error('Unknown state change'); - } - state = s; - } else if (s === INSIDE) { - min = Math.min(min, x); - max = Math.max(max, x); - } - - currentPoint = point; - } - } - - markers.sort((a, b) => a.position - b.position); - // console.log(markers); - - let G = 0; - if (type === 'INTERIOR' || (markers.length > 0 && markers[0].type === LEFT)) { - G++; - } - - // console.log(G) - - let minX = lineRect.x; - const { maxX } = lineRect; - const { height } = lineRect; - const rects = []; - - for (const marker of markers) { - if (marker.type === RIGHT) { - if (G === 0) { - const p = Math.min(maxX, marker.position); - if (p >= minX) { - rects.push(new Rect(minX, minY, p - minX, height)); - } - } - - G++; - } else { - G--; - if (G === 0 && marker.position > minX) { - minX = marker.position; - } - } - } - - // console.log(G, maxX, minX) - if (G === 0 && maxX >= minX) { - rects.push(new Rect(minX, minY, maxX - minX, height)); - } - - // console.log(rects) - return rects; - } - }; - -function xIntersection(e, t, n) { - const r = e - n.y; - const i = t.y - n.y; - - return r / i * (t.x - n.x) + n.x; -} diff --git a/packages/linebreaker/index.js b/packages/linebreaker/index.js index 5a01e11..c1389f2 100644 --- a/packages/linebreaker/index.js +++ b/packages/linebreaker/index.js @@ -1,8 +1,5 @@ import LineBreak from 'linebreak'; -import Hyphenator from 'hypher'; -import enUS from 'hyphenation.en-us'; -const hyphenator = new Hyphenator(enUS); const HYPHEN = 0x002d; const SHRINK_FACTOR = 0.04; @@ -12,95 +9,113 @@ const SHRINK_FACTOR = 0.04; */ export default () => () => class LineBreaker { - suggestLineBreak(glyphString, width, paragraphStyle) { + suggestLineBreak(glyphString, syllables, availableWidths, paragraphStyle) { + const width = availableWidths[0]; const hyphenationFactor = paragraphStyle.hyphenationFactor || 0; - const glyphIndex = glyphString.glyphIndexAtOffset(width); + const lines = []; - if (glyphIndex === -1) return null; + // let string = glyphString; + // let bk = this.findLineBreak(string, width, hyphenationFactor); - if (glyphIndex === glyphString.length) { - return { position: glyphString.length, required: true }; - } + // while (bk && bk.position !== 0) { + // lines.push(string.slice(0, bk.position)); + // string = string.slice(bk.position, string.length); + // bk = this.findLineBreak(string, width, hyphenationFactor); + // } - let stringIndex = glyphString.stringIndexForGlyphIndex(glyphIndex); - const bk = this.findBreakPreceeding(glyphString.string, stringIndex); + // console.log(lines); - if (bk) { - let breakIndex = glyphString.glyphIndexForStringIndex(bk.position); + return [glyphString]; + } - if ( - bk.next != null && - this.shouldHyphenate(glyphString, breakIndex, width, hyphenationFactor) - ) { - const lineWidth = glyphString.offsetAtGlyphIndex(glyphIndex); - const shrunk = lineWidth + lineWidth * SHRINK_FACTOR; + // findLineBreak(glyphString, width, hyphenationFactor) { + // const glyphIndex = glyphString.glyphIndexAtOffset(width); - const shrunkIndex = glyphString.glyphIndexAtOffset(shrunk); - stringIndex = Math.min(bk.next, glyphString.stringIndexForGlyphIndex(shrunkIndex)); + // if (glyphIndex === -1) return null; - const point = this.findHyphenationPoint( - glyphString.string.slice(bk.position, bk.next), - stringIndex - bk.position - ); + // if (glyphIndex === glyphString.length) { + // return { position: glyphString.length, required: true }; + // } - if (point > 0) { - bk.position += point; - breakIndex = glyphString.glyphIndexForStringIndex(bk.position); + // let stringIndex = glyphString.stringIndexForGlyphIndex(glyphIndex); + // const bk = this.findBreakPreceeding(glyphString.string, stringIndex); - if (bk.position < bk.next) { - glyphString.insertGlyph(breakIndex++, HYPHEN); - } - } - } + // if (bk) { + // let breakIndex = glyphString.glyphIndexForStringIndex(bk.position); - bk.position = breakIndex; - } + // if ( + // bk.next != null && + // this.shouldHyphenate(glyphString, breakIndex, width, hyphenationFactor) + // ) { + // const lineWidth = glyphString.offsetAtGlyphIndex(glyphIndex); + // const shrunk = lineWidth + lineWidth * SHRINK_FACTOR; - return bk; - } + // const shrunkIndex = glyphString.glyphIndexAtOffset(shrunk); + // stringIndex = Math.min(bk.next, glyphString.stringIndexForGlyphIndex(shrunkIndex)); - findBreakPreceeding(string, index) { - const breaker = new LineBreak(string); - let last = null; - let bk = null; + // const point = this.findHyphenationPoint( + // glyphString.string.slice(bk.position, bk.next), + // stringIndex - bk.position + // ); - while ((bk = breaker.nextBreak())) { - // console.log(bk); - if (bk.position > index) { - if (last) { - last.next = bk.position; - } + // if (point > 0) { + // bk.position += point; + // breakIndex = glyphString.glyphIndexForStringIndex(bk.position); - return last; - } + // if (bk.position < bk.next) { + // glyphString.insertGlyph(breakIndex++, HYPHEN); + // } + // } + // } - if (bk.required) { - return bk; - } + // bk.position = breakIndex; + // } - last = bk; - } + // return bk; + // } - return null; - } + // findBreakPreceeding(string, index) { + // const breaker = new LineBreak(string); + // let last = null; + // let bk = null; - shouldHyphenate(glyphString, glyphIndex, width, hyphenationFactor) { - const lineWidth = glyphString.offsetAtGlyphIndex(glyphIndex); - return lineWidth / width < hyphenationFactor; - } + // while ((bk = breaker.nextBreak())) { + // // console.log(bk); + // if (bk.position > index) { + // if (last) { + // last.next = bk.position; + // } - findHyphenationPoint(string, index) { - const parts = hyphenator.hyphenate(string); - let count = 0; + // return last; + // } - for (const part of parts) { - if (count + part.length > index) { - break; - } + // if (bk.required) { + // return bk; + // } - count += part.length; - } + // last = bk; + // } - return count; - } + // return null; + // } + + // shouldHyphenate(glyphString, glyphIndex, width, hyphenationFactor) { + // const lineWidth = glyphString.offsetAtGlyphIndex(glyphIndex); + // return lineWidth / width < hyphenationFactor; + // } + + // findHyphenationPoint(string, index) { + // const parts = hyphenator.hyphenate(string); + // let count = 0; + + // for (const part of parts) { + // if (count + part.length > index) { + // break; + // } + + // count += part.length; + // } + + // return count; + // } }; diff --git a/packages/linebreaker/package.json b/packages/linebreaker/package.json index f3acb75..7d011db 100644 --- a/packages/linebreaker/package.json +++ b/packages/linebreaker/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" diff --git a/packages/pdf-renderer/package.json b/packages/pdf-renderer/package.json index 4592cca..923809a 100644 --- a/packages/pdf-renderer/package.json +++ b/packages/pdf-renderer/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" diff --git a/packages/script-itemizer/index.js b/packages/script-itemizer/index.js index 25ff817..c44c213 100644 --- a/packages/script-itemizer/index.js +++ b/packages/script-itemizer/index.js @@ -14,9 +14,7 @@ export default () => ({ Run }) => let index = 0; const runs = []; - if (!string) { - return []; - } + if (!string) return []; for (const char of string) { const codePoint = char.codePointAt(); diff --git a/packages/script-itemizer/package.json b/packages/script-itemizer/package.json index 0ce9faf..cee0dad 100644 --- a/packages/script-itemizer/package.json +++ b/packages/script-itemizer/package.json @@ -1,16 +1,19 @@ { "name": "@textkit/script-itemizer", - "version": "0.1.9", + "version": "0.1.12", "description": "An advanced text layout framework", "main": "dist/index.js", "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" }, + "publishConfig": { + "access": "public" + }, "files": [ "dist" ], diff --git a/packages/tab-engine/package.json b/packages/tab-engine/package.json index 1cbdc87..76aa1a7 100644 --- a/packages/tab-engine/package.json +++ b/packages/tab-engine/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" diff --git a/packages/text-decoration-engine/package.json b/packages/text-decoration-engine/package.json index 35069e3..276ff27 100644 --- a/packages/text-decoration-engine/package.json +++ b/packages/text-decoration-engine/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" diff --git a/packages/textkit/index.js b/packages/textkit/index.js index e31527c..8d17b22 100644 --- a/packages/textkit/index.js +++ b/packages/textkit/index.js @@ -1,10 +1,10 @@ import tabEngine from '@textkit/tab-engine'; import lineBreaker from '@textkit/linebreaker'; +import wordHyphenation from '@textkit/word-hyphenation'; import scriptItemizer from '@textkit/script-itemizer'; import truncationEngine from '@textkit/truncation-engine'; import justificationEngine from '@textkit/justification-engine'; import textDecorationEngine from '@textkit/text-decoration-engine'; -import lineFragmentGenerator from '@textkit/line-fragment-generator'; import fontSubstitutionEngine from '@textkit/font-substitution-engine'; import { LayoutEngine as BaseLayoutEngine } from '@textkit/core'; @@ -12,10 +12,10 @@ const defaultEngines = { tabEngine: tabEngine(), lineBreaker: lineBreaker(), scriptItemizer: scriptItemizer(), + wordHyphenation: wordHyphenation(), truncationEngine: truncationEngine(), decorationEngine: textDecorationEngine(), justificationEngine: justificationEngine(), - lineFragmentGenerator: lineFragmentGenerator(), fontSubstitutionEngine: fontSubstitutionEngine() }; diff --git a/packages/textkit/package.json b/packages/textkit/package.json index 70f24fc..caa6615 100644 --- a/packages/textkit/package.json +++ b/packages/textkit/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" @@ -20,7 +20,7 @@ "@textkit/core": "^0.1.16", "@textkit/font-substitution-engine": "^0.1.9", "@textkit/justification-engine": "^0.1.9", - "@textkit/line-fragment-generator": "^0.1.9", + "@textkit/word-hyphenation": "^0.1.0", "@textkit/linebreaker": "^0.1.9", "@textkit/script-itemizer": "^0.1.9", "@textkit/tab-engine": "^0.1.9", diff --git a/packages/truncation-engine/package.json b/packages/truncation-engine/package.json index aa8ac50..616be5f 100644 --- a/packages/truncation-engine/package.json +++ b/packages/truncation-engine/package.json @@ -6,7 +6,7 @@ "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" diff --git a/packages/word-hyphenation/index.js b/packages/word-hyphenation/index.js new file mode 100644 index 0000000..f6defdb --- /dev/null +++ b/packages/word-hyphenation/index.js @@ -0,0 +1,24 @@ +import Hyphenator from 'hypher'; +import enUS from 'hyphenation.en-us'; + +const SOFT_HYPHEN_HEX = '\u00ad'; + +export default () => () => + class { + constructor() { + this.cache = {}; + this.hypher = new Hyphenator(enUS); + } + + hyphenateWord(word) { + if (this.cache[word]) return this.cache[word]; + + const parts = word.includes(SOFT_HYPHEN_HEX) + ? word.split(SOFT_HYPHEN_HEX) + : this.hypher.hyphenate(word); + + this.cache[word] = parts; + + return parts; + } + }; diff --git a/packages/line-fragment-generator/package.json b/packages/word-hyphenation/package.json similarity index 57% rename from packages/line-fragment-generator/package.json rename to packages/word-hyphenation/package.json index dd33d3e..1e3e19b 100644 --- a/packages/line-fragment-generator/package.json +++ b/packages/word-hyphenation/package.json @@ -1,19 +1,26 @@ { - "name": "@textkit/line-fragment-generator", - "version": "0.1.9", + "name": "@textkit/word-hyphenation", + "version": "0.1.0", "description": "An advanced text layout framework", "main": "dist/index.js", "scripts": { "prebuild": "rimraf dist", "prepublish": "npm run build", - "build": "babel index.js --out-dir ./dist", + "build": "babel index.js --out-dir ./dist --source-maps", "build:watch": "babel index.js --out-dir ./dist --watch", "precommit": "lint-staged", "test": "jest" }, + "publishConfig": { + "access": "public" + }, "files": [ "dist" ], "author": "Devon Govett ", - "license": "MIT" + "license": "MIT", + "dependencies": { + "hyphenation.en-us": "^0.2.1", + "hypher": "^0.2.4" + } } diff --git a/temp.js b/temp.js index 610127b..69abcfd 100644 --- a/temp.js +++ b/temp.js @@ -1,14 +1,8 @@ import fs from 'fs'; -import PDFDocument from 'pdfkit'; -import PDFRenderer from '@textkit/pdf-renderer'; -import { - Path, - Rect, - LayoutEngine, - AttributedString, - Container, - Attachment -} from '@textkit/textkit'; +import PDFDocument from '@react-pdf/pdfkit'; +import PDFRenderer from './packages/pdf-renderer'; +import fontkit from '@react-pdf/fontkit'; +import { Path, Rect, LayoutEngine, AttributedString, Container } from './packages/textkit'; const path = new Path(); @@ -26,11 +20,13 @@ path.toFunction()(doc); doc.stroke('green'); doc.stroke(); +const font = fontkit.openSync('./Roboto-Regular.ttf'); + const string = AttributedString.fromFragments([ { string: 'Lorem ipsum dolor sit amet, ', attributes: { - font: 'Arial', + font, fontSize: 14, bold: true, align: 'justify', @@ -43,24 +39,11 @@ const string = AttributedString.fromFragments([ { string: 'consectetur adipiscing elit, ', attributes: { - font: 'Arial', + font, fontSize: 18, color: 'red', align: 'justify' } - }, - { - string: - 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea volupt\u0301ate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?”', - attributes: { - font: 'Comic Sans MS', - fontSize: 14, - align: 'justify', - hyphenationFactor: 0.9, - hangingPunctuation: true, - lineSpacing: 5, - truncate: true - } } ]);