|
|
|
@ -12,8 +12,6 @@ const max = require("../util/max");
|
|
|
|
|
const last = require("../util/last");
|
|
|
|
|
|
|
|
|
|
function drawDebugLine(context, lineNumber, y, width, color = "red") {
|
|
|
|
|
//context.globalAlpha = 1;
|
|
|
|
|
|
|
|
|
|
context.strokeStyle = color;
|
|
|
|
|
context.beginPath();
|
|
|
|
|
|
|
|
|
@ -35,164 +33,129 @@ function drawDebugLine(context, lineNumber, y, width, color = "red") {
|
|
|
|
|
|
|
|
|
|
context.font = "12px sans-serif";
|
|
|
|
|
context.fillStyle = "white";
|
|
|
|
|
context.fillText(lineNumber, 5 + boxOffset * 18, y + 15)
|
|
|
|
|
context.fillText(lineNumber, 5 + boxOffset * 18, y + 15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function layoutItems(lines, options = {}) {
|
|
|
|
|
let currentYOffset = options.initialY;
|
|
|
|
|
let currentXOffset = 0;
|
|
|
|
|
let debugYOffset = 0;
|
|
|
|
|
let debugLines = [];
|
|
|
|
|
|
|
|
|
|
let positionedItems = lines.reduce((items, line, i) => {
|
|
|
|
|
debugLines.push({color: "red", y: debugYOffset, lineNumber: i});
|
|
|
|
|
debugLines.push({color: "green", y: currentYOffset, lineNumber: i});
|
|
|
|
|
debugLines.push({color: "orange", y: currentYOffset + line.adjustedHeight, lineNumber: i});
|
|
|
|
|
|
|
|
|
|
let newItems = items.concat(line.items.map((item) => {
|
|
|
|
|
let x = currentXOffset;
|
|
|
|
|
let y = currentYOffset - (line.minAscender / 2) + (line.adjustedHeight / 2)
|
|
|
|
|
|
|
|
|
|
debugLines.push({color: "blue", y: y, x1: x, x2: x + item.measurements.width, lineNumber: i});
|
|
|
|
|
|
|
|
|
|
currentXOffset += item.measurements.width;
|
|
|
|
|
|
|
|
|
|
return Object.assign({
|
|
|
|
|
x: x,
|
|
|
|
|
y: y
|
|
|
|
|
}, item);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
currentYOffset += line.adjustedHeight;
|
|
|
|
|
debugYOffset += line.adjustedHeight;
|
|
|
|
|
currentXOffset = 0;
|
|
|
|
|
|
|
|
|
|
return newItems;
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
//context.globalAlpha = 1;
|
|
|
|
|
return {
|
|
|
|
|
positionedItems: positionedItems,
|
|
|
|
|
debugLines: debugLines
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = function layoutFormattedText(text, options) {
|
|
|
|
|
if (options.tags === true) {
|
|
|
|
|
let lines = parser(text).map((lineItems) => {
|
|
|
|
|
let measuredItems = lineItems.filter(item => item.text !== "").map((item) => {
|
|
|
|
|
let generatedStyle = generateStyle(item, {
|
|
|
|
|
classes: options.classes,
|
|
|
|
|
defaultStyle: options.defaultStyle
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return Object.assign({
|
|
|
|
|
measurements: measureText(item.text, generatedStyle),
|
|
|
|
|
style: generatedStyle
|
|
|
|
|
}, item);
|
|
|
|
|
});
|
|
|
|
|
let lines;
|
|
|
|
|
|
|
|
|
|
if (measuredItems.length > 0) {
|
|
|
|
|
return {
|
|
|
|
|
alignment: (lineItems.length > 0) ? findAlignment(lineItems[0]) : null,
|
|
|
|
|
items: measuredItems,
|
|
|
|
|
height: max(measuredItems.map(item => item.measurements.height)),
|
|
|
|
|
width: sum(measuredItems.map(item => item.measurements.width)),
|
|
|
|
|
minAscender: min(measuredItems.map(item => item.measurements.ascender)),
|
|
|
|
|
maxDescender: max(measuredItems.map(item => item.measurements.descender))
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
/* This line does not contain any real elements, so we'll omit it. */
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}).filter(line => line != null);
|
|
|
|
|
|
|
|
|
|
let adjustedLineHeights = adjustLineHeights(lines, options.lineHeight);
|
|
|
|
|
|
|
|
|
|
/* We start out by moving everything up half the first line's height - this
|
|
|
|
|
* compensates for the middle-of-the-line-oriented line spacing of the first
|
|
|
|
|
* line. Otherwise, everything would be shifted off the canvas.*/
|
|
|
|
|
let startHeightCorrection, endHeightCorrection;
|
|
|
|
|
|
|
|
|
|
if (options.trimVerticalWhitespace) {
|
|
|
|
|
startHeightCorrection = -(adjustedLineHeights[0] / 2) - (lines[0].minAscender / 2);
|
|
|
|
|
endHeightCorrection = -(last(adjustedLineHeights) / 2) - (last(lines).minAscender / 2);
|
|
|
|
|
} else {
|
|
|
|
|
startHeightCorrection = 0;
|
|
|
|
|
endHeightCorrection = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let currentLineOffset = startHeightCorrection;
|
|
|
|
|
|
|
|
|
|
let debugLineOffset = 0;
|
|
|
|
|
let currentComponentOffset = 0;
|
|
|
|
|
let debugLines = [];
|
|
|
|
|
|
|
|
|
|
let positionedItems = lines.reduce((items, line, i) => {
|
|
|
|
|
debugLines.push({color: "red", y: debugLineOffset, lineNumber: i});
|
|
|
|
|
debugLines.push({color: "green", y: currentLineOffset, lineNumber: i});
|
|
|
|
|
debugLines.push({color: "orange", y: currentLineOffset + adjustedLineHeights[i], lineNumber: i});
|
|
|
|
|
|
|
|
|
|
let newItems = items.concat(line.items.map((item) => {
|
|
|
|
|
let positionedItem = Object.assign({
|
|
|
|
|
x: currentComponentOffset,
|
|
|
|
|
y: currentLineOffset - (line.minAscender / 2) + (adjustedLineHeights[i] / 2)
|
|
|
|
|
}, item);
|
|
|
|
|
|
|
|
|
|
debugLines.push({color: "blue", y: positionedItem.y, x1: positionedItem.x, x2: positionedItem.x + item.measurements.width, lineNumber: i});
|
|
|
|
|
|
|
|
|
|
currentComponentOffset += item.measurements.width;
|
|
|
|
|
return positionedItem;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
currentLineOffset += adjustedLineHeights[i];
|
|
|
|
|
debugLineOffset += adjustedLineHeights[i];
|
|
|
|
|
currentComponentOffset = 0;
|
|
|
|
|
|
|
|
|
|
return newItems;
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
width: Math.ceil(max(lines.map(line => line.width))),
|
|
|
|
|
height: Math.ceil(sum(adjustedLineHeights) + startHeightCorrection + endHeightCorrection + (last(lines).maxDescender)),
|
|
|
|
|
items: positionedItems,
|
|
|
|
|
drawDebugLines: function drawDebugLines(context) {
|
|
|
|
|
debugLines.forEach((debugLine) => {
|
|
|
|
|
let width;
|
|
|
|
|
|
|
|
|
|
if (debugLine.x1 != null) {
|
|
|
|
|
width = [debugLine.x1, debugLine.x2];
|
|
|
|
|
} else {
|
|
|
|
|
width = this.width;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawDebugLine(context, debugLine.lineNumber, debugLine.y, width, debugLine.color);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (options.tags === true) {
|
|
|
|
|
lines = parser(text).map((lineItems) => {
|
|
|
|
|
return {
|
|
|
|
|
items: lineItems
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
let debugLines = [];
|
|
|
|
|
lines = text.split("\n").map((line) => {
|
|
|
|
|
let lineMeasurements = measureText(line, options.defaultStyle);
|
|
|
|
|
|
|
|
|
|
let lines = text.split("\n").map((line) => {
|
|
|
|
|
return {
|
|
|
|
|
text: line,
|
|
|
|
|
measurements: measureText(line, options.defaultStyle)
|
|
|
|
|
items: [{
|
|
|
|
|
text: line
|
|
|
|
|
}]
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let adjustedLineHeights = adjustLineHeights(lines.map((line) => {
|
|
|
|
|
return {
|
|
|
|
|
text: line.text,
|
|
|
|
|
height: line.measurements.height
|
|
|
|
|
let processedLines = lines.map((line) => {
|
|
|
|
|
let processedItems = line.items.map((item) => {
|
|
|
|
|
let style;
|
|
|
|
|
|
|
|
|
|
if (item.tags != null) {
|
|
|
|
|
style = Object.assign({}, options.defaultStyle, generateStyle(item.tags, options.classes));
|
|
|
|
|
} else {
|
|
|
|
|
style = options.defaultStyle;
|
|
|
|
|
}
|
|
|
|
|
}), options.lineHeight);
|
|
|
|
|
|
|
|
|
|
let startHeightCorrection, endHeightCorrection;
|
|
|
|
|
return Object.assign(item, {
|
|
|
|
|
style: style,
|
|
|
|
|
measurements: measureText(item.text, style)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (options.trimVerticalWhitespace) {
|
|
|
|
|
startHeightCorrection = -(adjustedLineHeights[0] / 2) - (lines[0].measurements.ascender / 2);
|
|
|
|
|
endHeightCorrection = -(last(adjustedLineHeights) / 2) - (last(lines).measurements.ascender / 2);
|
|
|
|
|
} else {
|
|
|
|
|
startHeightCorrection = 0;
|
|
|
|
|
endHeightCorrection = 0;
|
|
|
|
|
}
|
|
|
|
|
let height = max(processedItems.map(item => item.measurements.height));
|
|
|
|
|
|
|
|
|
|
let currentLineOffset = startHeightCorrection;
|
|
|
|
|
return Object.assign(line, {
|
|
|
|
|
alignment: (line.items.length > 0) ? findAlignment(line.items[0]) : null,
|
|
|
|
|
items: processedItems,
|
|
|
|
|
height: height,
|
|
|
|
|
adjustedHeight: height * options.lineHeight * 1.13,
|
|
|
|
|
width: sum(processedItems.map(item => item.measurements.width)),
|
|
|
|
|
minAscender: min(processedItems.map(item => item.measurements.ascender)),
|
|
|
|
|
maxDescender: max(processedItems.map(item => item.measurements.descender))
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let debugLineOffset = 0;
|
|
|
|
|
let startHeightCorrection, endHeightCorrection;
|
|
|
|
|
|
|
|
|
|
let positionedItems = lines.map((line, i) => {
|
|
|
|
|
let newItem = {
|
|
|
|
|
text: line.text,
|
|
|
|
|
measurements: line.measurements,
|
|
|
|
|
style: options.defaultStyle,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: currentLineOffset - (line.measurements.ascender / 2) + (adjustedLineHeights[i] / 2)
|
|
|
|
|
}
|
|
|
|
|
if (options.trimVerticalWhitespace) {
|
|
|
|
|
startHeightCorrection = -(lines[0].adjustedHeight / 2) - (lines[0].minAscender / 2);
|
|
|
|
|
endHeightCorrection = -(last(lines).adjustedHeight / 2) - (last(lines).minAscender / 2);
|
|
|
|
|
} else {
|
|
|
|
|
startHeightCorrection = 0;
|
|
|
|
|
endHeightCorrection = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debugLines.push({color: "red", y: debugLineOffset, lineNumber: i});
|
|
|
|
|
debugLines.push({color: "green", y: currentLineOffset, lineNumber: i});
|
|
|
|
|
debugLines.push({color: "blue", y: newItem.y, lineNumber: i});
|
|
|
|
|
debugLines.push({color: "orange", y: currentLineOffset + adjustedLineHeights[i], lineNumber: i});
|
|
|
|
|
let {positionedItems, debugLines} = layoutItems(lines, {
|
|
|
|
|
initialY: startHeightCorrection
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
currentLineOffset += adjustedLineHeights[i];
|
|
|
|
|
debugLineOffset += adjustedLineHeights[i];
|
|
|
|
|
let combinedLineHeights = sum(lines.map(line => line.adjustedHeight));
|
|
|
|
|
|
|
|
|
|
return newItem;
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
width: Math.ceil(max(lines.map(line => line.width))),
|
|
|
|
|
height: Math.ceil(combinedLineHeights + startHeightCorrection + endHeightCorrection + last(lines).maxDescender),
|
|
|
|
|
items: positionedItems,
|
|
|
|
|
drawDebugLines: function drawDebugLines(context) {
|
|
|
|
|
debugLines.forEach((debugLine) => {
|
|
|
|
|
let width;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
width: Math.ceil(max(lines.map(line => line.measurements.width))),
|
|
|
|
|
height: Math.ceil(sum(adjustedLineHeights) + startHeightCorrection + endHeightCorrection + (last(lines).measurements.descender)),
|
|
|
|
|
items: positionedItems,
|
|
|
|
|
drawDebugLines: function drawDebugLines(context) {
|
|
|
|
|
debugLines.forEach((debugLine) => {
|
|
|
|
|
drawDebugLine(context, debugLine.lineNumber, debugLine.y, this.width, debugLine.color);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (debugLine.x1 != null) {
|
|
|
|
|
width = [debugLine.x1, debugLine.x2];
|
|
|
|
|
} else {
|
|
|
|
|
width = this.width;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drawDebugLine(context, debugLine.lineNumber, debugLine.y, width, debugLine.color);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|