You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
12 KiB
373 lines
12 KiB
'use strict';
|
|
|
|
var isString = require('./helpers').isString;
|
|
var isNumber = require('./helpers').isNumber;
|
|
var isObject = require('./helpers').isObject;
|
|
var isArray = require('./helpers').isArray;
|
|
var isUndefined = require('./helpers').isUndefined;
|
|
var LineBreaker = require('@foliojs-fork/linebreak');
|
|
|
|
var LEADING = /^(\s)+/g;
|
|
var TRAILING = /(\s)+$/g;
|
|
|
|
/**
|
|
* Creates an instance of TextTools - text measurement utility
|
|
*
|
|
* @constructor
|
|
* @param {FontProvider} fontProvider
|
|
*/
|
|
function TextTools(fontProvider) {
|
|
this.fontProvider = fontProvider;
|
|
}
|
|
|
|
/**
|
|
* Converts an array of strings (or inline-definition-objects) into a collection
|
|
* of inlines and calculated minWidth/maxWidth.
|
|
* and their min/max widths
|
|
* @param {Object} textArray - an array of inline-definition-objects (or strings)
|
|
* @param {Object} styleContextStack current style stack
|
|
* @return {Object} collection of inlines, minWidth, maxWidth
|
|
*/
|
|
TextTools.prototype.buildInlines = function (textArray, styleContextStack) {
|
|
var measured = measure(this.fontProvider, textArray, styleContextStack);
|
|
|
|
var minWidth = 0,
|
|
maxWidth = 0,
|
|
currentLineWidth;
|
|
|
|
measured.forEach(function (inline) {
|
|
minWidth = Math.max(minWidth, inline.width - inline.leadingCut - inline.trailingCut);
|
|
|
|
if (!currentLineWidth) {
|
|
currentLineWidth = { width: 0, leadingCut: inline.leadingCut, trailingCut: 0 };
|
|
}
|
|
|
|
currentLineWidth.width += inline.width;
|
|
currentLineWidth.trailingCut = inline.trailingCut;
|
|
|
|
maxWidth = Math.max(maxWidth, getTrimmedWidth(currentLineWidth));
|
|
|
|
if (inline.lineEnd) {
|
|
currentLineWidth = null;
|
|
}
|
|
});
|
|
|
|
if (getStyleProperty({}, styleContextStack, 'noWrap', false)) {
|
|
minWidth = maxWidth;
|
|
}
|
|
|
|
return {
|
|
items: measured,
|
|
minWidth: minWidth,
|
|
maxWidth: maxWidth
|
|
};
|
|
|
|
function getTrimmedWidth(item) {
|
|
return Math.max(0, item.width - item.leadingCut - item.trailingCut);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns size of the specified string (without breaking it) using the current style
|
|
* @param {String} text text to be measured
|
|
* @param {Object} styleContextStack current style stack
|
|
* @return {Object} size of the specified string
|
|
*/
|
|
TextTools.prototype.sizeOfString = function (text, styleContextStack) {
|
|
text = text ? text.toString().replace(/\t/g, ' ') : '';
|
|
|
|
//TODO: refactor - extract from measure
|
|
var fontName = getStyleProperty({}, styleContextStack, 'font', 'Roboto');
|
|
var fontSize = getStyleProperty({}, styleContextStack, 'fontSize', 12);
|
|
var fontFeatures = getStyleProperty({}, styleContextStack, 'fontFeatures', null);
|
|
var bold = getStyleProperty({}, styleContextStack, 'bold', false);
|
|
var italics = getStyleProperty({}, styleContextStack, 'italics', false);
|
|
var lineHeight = getStyleProperty({}, styleContextStack, 'lineHeight', 1);
|
|
var characterSpacing = getStyleProperty({}, styleContextStack, 'characterSpacing', 0);
|
|
|
|
var font = this.fontProvider.provideFont(fontName, bold, italics);
|
|
|
|
return {
|
|
width: widthOfString(text, font, fontSize, characterSpacing, fontFeatures),
|
|
height: font.lineHeight(fontSize) * lineHeight,
|
|
fontSize: fontSize,
|
|
lineHeight: lineHeight,
|
|
ascender: font.ascender / 1000 * fontSize,
|
|
descender: font.descender / 1000 * fontSize
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Returns size of the specified rotated string (without breaking it) using the current style
|
|
*
|
|
* @param {string} text text to be measured
|
|
* @param {number} angle
|
|
* @param {object} styleContextStack current style stack
|
|
* @returns {object} size of the specified string
|
|
*/
|
|
TextTools.prototype.sizeOfRotatedText = function (text, angle, styleContextStack) {
|
|
var angleRad = angle * Math.PI / -180;
|
|
var size = this.sizeOfString(text, styleContextStack);
|
|
return {
|
|
width: Math.abs(size.height * Math.sin(angleRad)) + Math.abs(size.width * Math.cos(angleRad)),
|
|
height: Math.abs(size.width * Math.sin(angleRad)) + Math.abs(size.height * Math.cos(angleRad))
|
|
};
|
|
}
|
|
|
|
TextTools.prototype.widthOfString = function (text, font, fontSize, characterSpacing, fontFeatures) {
|
|
return widthOfString(text, font, fontSize, characterSpacing, fontFeatures);
|
|
};
|
|
|
|
function splitWords(text, noWrap) {
|
|
var results = [];
|
|
text = text.replace(/\t/g, ' ');
|
|
|
|
if (noWrap) {
|
|
results.push({ text: text });
|
|
return results;
|
|
}
|
|
|
|
var breaker = new LineBreaker(text);
|
|
var last = 0;
|
|
var bk;
|
|
|
|
while (bk = breaker.nextBreak()) {
|
|
var word = text.slice(last, bk.position);
|
|
|
|
if (bk.required || word.match(/\r?\n$|\r$/)) { // new line
|
|
word = word.replace(/\r?\n$|\r$/, '');
|
|
results.push({ text: word, lineEnd: true });
|
|
} else {
|
|
results.push({ text: word });
|
|
}
|
|
|
|
last = bk.position;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function copyStyle(source, destination) {
|
|
destination = destination || {};
|
|
source = source || {}; //TODO: default style
|
|
|
|
for (var key in source) {
|
|
if (key != 'text' && source.hasOwnProperty(key)) {
|
|
destination[key] = source[key];
|
|
}
|
|
}
|
|
|
|
return destination;
|
|
}
|
|
|
|
function normalizeTextArray(array, styleContextStack) {
|
|
function flatten(array) {
|
|
return array.reduce(function (prev, cur) {
|
|
var current = isArray(cur.text) ? flatten(cur.text) : cur;
|
|
var more = [].concat(current).some(Array.isArray);
|
|
return prev.concat(more ? flatten(current) : current);
|
|
}, []);
|
|
}
|
|
|
|
function getOneWord(index, words, noWrap) {
|
|
if (isUndefined(words[index])) {
|
|
return null;
|
|
}
|
|
|
|
if (words[index].lineEnd) {
|
|
return null;
|
|
}
|
|
|
|
var word = words[index].text;
|
|
|
|
if (noWrap) {
|
|
var tmpWords = splitWords(normalizeString(word), false);
|
|
if (isUndefined(tmpWords[tmpWords.length - 1])) {
|
|
return null;
|
|
}
|
|
word = tmpWords[tmpWords.length - 1].text;
|
|
}
|
|
|
|
return word;
|
|
}
|
|
|
|
var results = [];
|
|
|
|
if (!isArray(array)) {
|
|
array = [array];
|
|
}
|
|
|
|
array = flatten(array);
|
|
|
|
var lastWord = null;
|
|
for (var i = 0, l = array.length; i < l; i++) {
|
|
var item = array[i];
|
|
var style = null;
|
|
var words;
|
|
|
|
var noWrap = getStyleProperty(item || {}, styleContextStack, 'noWrap', false);
|
|
if (isObject(item)) {
|
|
if (item._textRef && item._textRef._textNodeRef.text) {
|
|
item.text = item._textRef._textNodeRef.text;
|
|
}
|
|
words = splitWords(normalizeString(item.text), noWrap);
|
|
style = copyStyle(item);
|
|
} else {
|
|
words = splitWords(normalizeString(item), noWrap);
|
|
}
|
|
|
|
if (lastWord && words.length) {
|
|
var firstWord = getOneWord(0, words, noWrap);
|
|
|
|
var wrapWords = splitWords(normalizeString(lastWord + firstWord), false);
|
|
if (wrapWords.length === 1) {
|
|
results[results.length - 1].noNewLine = true;
|
|
}
|
|
}
|
|
|
|
for (var i2 = 0, l2 = words.length; i2 < l2; i2++) {
|
|
var result = {
|
|
text: words[i2].text
|
|
};
|
|
|
|
if (words[i2].lineEnd) {
|
|
result.lineEnd = true;
|
|
}
|
|
|
|
copyStyle(style, result);
|
|
|
|
results.push(result);
|
|
}
|
|
|
|
lastWord = null;
|
|
if (i + 1 < l) {
|
|
lastWord = getOneWord(words.length - 1, words, noWrap);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
function normalizeString(value) {
|
|
if (value === undefined || value === null) {
|
|
return '';
|
|
} else if (isNumber(value)) {
|
|
return value.toString();
|
|
} else if (isString(value)) {
|
|
return value;
|
|
} else {
|
|
return value.toString();
|
|
}
|
|
}
|
|
|
|
function getStyleProperty(item, styleContextStack, property, defaultValue) {
|
|
var value;
|
|
|
|
if (item[property] !== undefined && item[property] !== null) {
|
|
// item defines this property
|
|
return item[property];
|
|
}
|
|
|
|
if (!styleContextStack) {
|
|
return defaultValue;
|
|
}
|
|
|
|
styleContextStack.auto(item, function () {
|
|
value = styleContextStack.getProperty(property);
|
|
});
|
|
|
|
if (value !== null && value !== undefined) {
|
|
return value;
|
|
} else {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
function measure(fontProvider, textArray, styleContextStack) {
|
|
var normalized = normalizeTextArray(textArray, styleContextStack);
|
|
|
|
if (normalized.length) {
|
|
var leadingIndent = getStyleProperty(normalized[0], styleContextStack, 'leadingIndent', 0);
|
|
|
|
if (leadingIndent) {
|
|
normalized[0].leadingCut = -leadingIndent;
|
|
normalized[0].leadingIndent = leadingIndent;
|
|
}
|
|
}
|
|
|
|
normalized.forEach(function (item) {
|
|
var fontName = getStyleProperty(item, styleContextStack, 'font', 'Roboto');
|
|
var fontSize = getStyleProperty(item, styleContextStack, 'fontSize', 12);
|
|
var fontFeatures = getStyleProperty(item, styleContextStack, 'fontFeatures', null);
|
|
var bold = getStyleProperty(item, styleContextStack, 'bold', false);
|
|
var italics = getStyleProperty(item, styleContextStack, 'italics', false);
|
|
var color = getStyleProperty(item, styleContextStack, 'color', 'black');
|
|
var decoration = getStyleProperty(item, styleContextStack, 'decoration', null);
|
|
var decorationColor = getStyleProperty(item, styleContextStack, 'decorationColor', null);
|
|
var decorationStyle = getStyleProperty(item, styleContextStack, 'decorationStyle', null);
|
|
var background = getStyleProperty(item, styleContextStack, 'background', null);
|
|
var lineHeight = getStyleProperty(item, styleContextStack, 'lineHeight', 1);
|
|
var characterSpacing = getStyleProperty(item, styleContextStack, 'characterSpacing', 0);
|
|
var link = getStyleProperty(item, styleContextStack, 'link', null);
|
|
var linkToPage = getStyleProperty(item, styleContextStack, 'linkToPage', null);
|
|
var linkToDestination = getStyleProperty(item, styleContextStack, 'linkToDestination', null);
|
|
var noWrap = getStyleProperty(item, styleContextStack, 'noWrap', null);
|
|
var preserveLeadingSpaces = getStyleProperty(item, styleContextStack, 'preserveLeadingSpaces', false);
|
|
var preserveTrailingSpaces = getStyleProperty(item, styleContextStack, 'preserveTrailingSpaces', false);
|
|
var opacity = getStyleProperty(item, styleContextStack, 'opacity', 1);
|
|
var sup = getStyleProperty(item, styleContextStack, 'sup', false);
|
|
var sub = getStyleProperty(item, styleContextStack, 'sub', false);
|
|
|
|
if ((sup || sub) && item.fontSize === undefined) {
|
|
// font size reduction taken from here: https://en.wikipedia.org/wiki/Subscript_and_superscript#Desktop_publishing
|
|
fontSize *= 0.58
|
|
}
|
|
|
|
var font = fontProvider.provideFont(fontName, bold, italics);
|
|
|
|
item.width = widthOfString(item.text, font, fontSize, characterSpacing, fontFeatures);
|
|
item.height = font.lineHeight(fontSize) * lineHeight;
|
|
|
|
if (!item.leadingCut) {
|
|
item.leadingCut = 0;
|
|
}
|
|
|
|
var leadingSpaces;
|
|
if (!preserveLeadingSpaces && (leadingSpaces = item.text.match(LEADING))) {
|
|
item.leadingCut += widthOfString(leadingSpaces[0], font, fontSize, characterSpacing, fontFeatures);
|
|
}
|
|
|
|
var trailingSpaces;
|
|
if (!preserveTrailingSpaces && (trailingSpaces = item.text.match(TRAILING))) {
|
|
item.trailingCut = widthOfString(trailingSpaces[0], font, fontSize, characterSpacing, fontFeatures);
|
|
} else {
|
|
item.trailingCut = 0;
|
|
}
|
|
|
|
item.alignment = getStyleProperty(item, styleContextStack, 'alignment', 'left');
|
|
item.font = font;
|
|
item.fontSize = fontSize;
|
|
item.fontFeatures = fontFeatures;
|
|
item.characterSpacing = characterSpacing;
|
|
item.color = color;
|
|
item.decoration = decoration;
|
|
item.decorationColor = decorationColor;
|
|
item.decorationStyle = decorationStyle;
|
|
item.background = background;
|
|
item.link = link;
|
|
item.linkToPage = linkToPage;
|
|
item.linkToDestination = linkToDestination;
|
|
item.noWrap = noWrap;
|
|
item.opacity = opacity;
|
|
item.sup = sup;
|
|
item.sub = sub;
|
|
});
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function widthOfString(text, font, fontSize, characterSpacing, fontFeatures) {
|
|
return font.widthOfString(text, fontSize, fontFeatures) + ((characterSpacing || 0) * (text.length - 1));
|
|
}
|
|
|
|
module.exports = TextTools;
|