Initial commit

master
Sven Slootweg 7 years ago
commit 80d0fc828d

2
.gitignore vendored

@ -0,0 +1,2 @@
/node_modules/
/lib/

@ -0,0 +1 @@
/node_modules/

@ -0,0 +1,32 @@
var gulp = require('gulp');
var presetES2015 = require("@joepie91/gulp-preset-es2015");
var presetPegjs = require("@joepie91/gulp-preset-pegjs")
var sources = {
babel: ["src/**/*.js"],
pegjs: ["src/**/*.pegjs"]
}
gulp.task('babel', function() {
return gulp.src(sources.babel)
.pipe(presetES2015({
basePath: __dirname
}))
.pipe(gulp.dest("lib/"));
});
gulp.task('pegjs', function() {
return gulp.src(sources.pegjs)
.pipe(presetPegjs({
basePath: __dirname
}))
.pipe(gulp.dest("lib/"));
})
gulp.task('watch', function () {
gulp.watch(sources.babel, ['babel']);
gulp.watch(sources.pegjs, ['pegjs']);
});
gulp.task('default', ['pegjs', 'babel', 'watch']);

@ -0,0 +1 @@
module.exports = require("./lib");

@ -0,0 +1,30 @@
{
"name": "canvassed-text",
"version": "1.0.0",
"description": "Text shape for `canvassed`",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@git.cryto.net:joepie91/canvassed-text.git"
},
"author": "Sven Slootweg",
"license": "WTFPL",
"devDependencies": {
"@joepie91/gulp-preset-es2015": "^1.0.1",
"@joepie91/gulp-preset-pegjs": "^1.0.0",
"babel-preset-es2015": "^6.22.0",
"chalk": "^1.1.3",
"default-value": "^1.0.0",
"gulp": "^3.9.1",
"pad": "^1.1.0"
},
"dependencies": {
"default-value": "^1.0.0",
"measure-font": "^1.0.0",
"memoizee": "^0.4.3",
"object.pick": "^1.2.0"
}
}

@ -0,0 +1,104 @@
'use strict';
const canvassed = require("canvassed");
const objectPick = require("object.pick");
const tagParser = require("./tag-parser");
const layout = require("./layout");
const fromTree = require("./text-components/from-tree");
const setTextStyles = require("./render/set-text-styles");
const measureText = require("./render/measure-text");
function getTextProperties(item) {
return objectPick(item, ["fontSize", "fontFamily", "fontStyle", "fontWeight"]);
}
module.exports = function createTextShape(options) {
let textObject = canvassed.createObject(Object.assign({
_layout: null,
_lines: null,
type: "text",
cacheBustingProperties: ["fillColor", "strokeColor", "strokeWidth", "text", "fontSize", "fontFamily", "fontStyle", "fontWeight"],
sizeBustingProperties: ["text", "fontSize", "fontFamily", "fontStyle", "fontWeight"],
fillColor: "red",
strokeColor: "red",
strokeWidth: 0,
fontFamily: "sans-serif",
fontSize: 16,
fontWeight: "normal",
lineHeight: 1.16,
onRender: function onRender(context) {
if (this.tags === true) {
let currentLineOffset = 0;
let currentComponentOffset = 0;
this._layout.forEach((line) => {
line.items.forEach((item) => {
setTextStyles(context, item.style);
context.fillText(item.text, currentComponentOffset, currentLineOffset - line.lineMinAscender);
currentComponentOffset += item.measurements.width;
});
currentLineOffset += line.lineHeight * this.lineHeight;
currentComponentOffset = 0;
});
} else {
let currentLineOffset = 0;
setTextStyles(context, getTextProperties(this));
console.log(this._lines, this.renderWidth, this.renderHeight);
this._lines.forEach((line) => {
context.fillText(line.text, 0, currentLineOffset - line.lineMinAscender);
currentLineOffset += line.measurements.height * this.lineHeight;
});
}
},
onRecalculateSize: function onRecalculateSize() {
if (this.tags === true) {
this._layout = layout(this.text, {
classes: options.classes,
defaultStyle: getTextProperties(this)
});
let combinedLineHeight = this._layout.reduce((total, line, i) => {
if (i !== this._layout.length - 1) {
return total + (line.lineHeight * this.lineHeight);
} else {
/* The last line doesn't get a lineHeight multiplier... */
return total + line.lineHeight;
}
}, 0);
return {
height: Math.ceil(combinedLineHeight),
width: Math.ceil(Math.max.apply(null, this._layout.map(line => line.lineWidth)))
}
} else {
this._lines = this.text.split("\n").map((line) => {
return {
text: line,
measurements: measureText(line, getTextProperties(this))
}
});
console.log("line", this._lines)
function sum(values) {
return values.reduce((total, value) => {
return total + value;
}, 0);
}
let multipliedLineHeights = this._lines.slice(0, -1).map(line => line.measurements.height * this.lineHeight);
let combinedLineHeight = sum(multipliedLineHeights) + this._lines[this._lines.length - 1].measurements.height;
return {
height: Math.ceil(combinedLineHeight),
width: Math.ceil(Math.max.apply(null, this._lines.map(line => line.measurements.width)))
}
}
}
}, options));
return textObject;
}

@ -0,0 +1,49 @@
'use strict';
const componentsToLines = require("./parser/to-lines");
const findAlignment = require("./parser/find-alignment");
const generateStyle = require("./parser/generate-style");
const fromTree = require("./parser/from-tree");
const tagParser = require("./parser/tag-parser");
const measureText = require("./render/measure-text");
module.exports = function layoutFormattedText(text, options) {
if (options.tags === true) {
let tree = tagParser.parse(text);
let lines = componentsToLines(fromTree(tree)).map((lineItems) => {
let measuredItems = lineItems.map((item) => {
let generatedStyle = generateStyle(item, {
classes: options.classes,
defaultStyle: options.defaultStyle
});
return Object.assign({
measurements: measureText(item.text, generatedStyle),
style: generatedStyle
}, item);
});
let combinedWidth = measuredItems.reduce((total, item) => {
return total + item.measurements.width;
}, 0);
let lineHeight = Math.max.apply(null, measuredItems.map(item => item.measurements.height));
let lineMinAscender = Math.min.apply(null, measuredItems.map(item => item.measurements.ascender));
let lineMaxDescender = Math.max.apply(null, measuredItems.map(item => item.measurements.descender));
return {
alignment: (lineItems.length > 0) ? findAlignment(lineItems[0]) : null,
items: measuredItems,
height: lineHeight,
width: combinedWidth,
minAscender: lineMinAscender,
maxDescender: lineMaxDescender
}
});
} else {
}
};

@ -0,0 +1,17 @@
'use strict';
module.exports = function findAlignment(item) {
if (item.tags == null) {
return null;
} else {
let alignmentTags = item.tags.filter(tag => tag.type === "alignment");
if (alignmentTags.length > 0) {
/* We always want the last-specified alignment tag. */
// FIXME: Throw an error upon encountering nested alignment tags...
return alignmentTags[alignmentTags.length - 1].alignment;
} else {
return null;
}
}
};

@ -0,0 +1,28 @@
'use strict';
module.exports = function splitTextComponents(treeList) {
function flatten(treeList, tagStack) {
return treeList.reduce((all, item) => {
if (item.type === "text") {
let splitItems = item.text.split("\n").reduce((items, line) => {
return items.concat([{
type: "text",
text: line,
tags: tagStack
}, {
type: "newline"
}]);
}, []);
/* Remove the last newline, because it's not delimiting anything. */
splitItems.pop();
return all.concat(splitItems);
} else {
return all.concat(flatten(item.contents, tagStack.concat([item])));
}
}, []);
}
return flatten(treeList, []);
}

@ -0,0 +1,19 @@
'use strict';
module.exports = function generateItemStyle(item, options) {
let style = Object.assign({}, options.defaultStyle);
item.tags.forEach((tag) => {
if (tag.type === "class") {
tag.classNames.forEach((className) => {
if (options.classes[className] == null) {
throw new Error(`Encountered unrecognized class ${className} in text`);
} else {
Object.assign(style, options.classes[className]);
}
});
}
});
return style;
}

@ -0,0 +1,129 @@
{
function filterTerminators(values) {
return values.map((value) => {
return value[1];
});
}
function collapseText(contents) {
/* To make the closing-tag parsing work, we interrupt a 'text' block
* every time we encounter a brace. Unfortunately, for non-closing-tag
* braces, this means things get broken up into consecutive text items.
* This function stitches consecutive text items back together. */
let textBuffer = "";
let newContents = [];
function completeTextItem() {
if (textBuffer.length > 0) {
newContents.push({
type: "text",
text: textBuffer
});
textBuffer = "";
}
}
contents.forEach((item) => {
if (item.type === "text") {
textBuffer += item.text;
} else {
completeTextItem();
newContents.push(item);
}
});
completeTextItem();
return newContents;
}
function processContents(contents) {
return collapseText(filterTerminators(contents));
}
function combineDelimitedItems(firstItem, nextItems, nextItemIndex = 1) {
return [firstItem].concat(nextItems.map((item) => {
return item[nextItemIndex];
}));
}
}
start
= contents:thing* {
return collapseText(contents);
}
text
= firstCharacter:. otherCharacters:[^{]* {
return {type: "text", text: firstCharacter + otherCharacters.join("")};
}
thing
= classTag
/*/ colorTag
/ boldTag
/ italicTag
/ weightTag
/ fontTag
/ sizeTag*/
/ alignmentTag
/ text
tagParameter
= characters:[^}:]+ { return characters.join(""); }
alignmentParameter
= "left"
/ "center"
/ "right"
/ "justify"
weightParameter
= "bold"
/ "normal"
/ [0-9]+
classParameter
= characters:[^}:,]+ { return characters.join(""); }
classTag
= "{class:" firstClassName:classParameter nextClassNames:("," classParameter)* "}" contents:(!"{/class}" thing)* "{/class}" {
return {type: "class", classNames: combineDelimitedItems(firstClassName, nextClassNames), contents: processContents(contents)};
}
/*colorTag
= "{color:" color:tagParameter "}" contents:(!"{/color}" thing)* "{/color}" {
return {type: "color", color: color, contents: processContents(contents)};
}
fontTag
= "{font:" font:tagParameter "}" contents:(!"{/font}" thing)* "{/font}" {
return {type: "font", font: font, contents: processContents(contents)};
}
sizeTag
= "{size:" size:tagParameter "}" contents:(!"{/size}" thing)* "{/size}" {
return {type: "size", size: size, contents: processContents(contents)};
}*/
alignmentTag
= "{align:" alignment:alignmentParameter "}" contents:(!"{/align}" thing)* "{/align}" {
return {type: "alignment", alignment: alignment, contents: processContents(contents)};
}
/*weightTag
= "{weight:" weight:tagParameter "}" contents:(!"{/weight}" thing)* "{/weight}" {
return {type: "weight", weight: weight, contents: processContents(contents)};
}
boldTag
= "{bold}" contents:(!"{/bold}" thing)* "{/bold}" {
return {type: "bold", contents: processContents(contents)};
}
italicTag
= "{italic}" contents:(!"{/italic}" thing)* "{/italic}" {
return {type: "italic", contents: processContents(contents)};
}*/

@ -0,0 +1,36 @@
'use strict';
const findAlignment = require("../tags/find-alignment");
module.exports = function componentsToLines(items) {
let currentLineItems = [];
let lines = [];
let lastAlignment = null;
function closeLine() {
lines.push(currentLineItems);
currentLineItems = [];
lastAlignment = null;
}
items.forEach((item) => {
if (item.type === "newline") {
closeLine();
} else {
let itemAlignment = findAlignment(item);
if (itemAlignment !== lastAlignment && currentLineItems.length > 0) {
/* We consider an alignment tag to be a 'block-level' element, therefore if it's
* encountered as anything but the first element, it automatically results in a
* new line. */
closeLine();
}
currentLineItems.push(item);
lastAlignment = itemAlignment;
}
});
closeLine();
return lines;
};

@ -0,0 +1,33 @@
'use strict';
const memoizee = require("memoizee");
const setTextStyles = require("./set-text-styles");
//const measureFont = memoizee(require("./measure-font"));
const measureFont = memoizee(require("measure-font"));
module.exports = function measureText(text, options) {
let offscreenCanvas = document.createElement("canvas");
let context = offscreenCanvas.getContext("2d");
setTextStyles(context, options);
let fontMeasurements = measureFont(options.fontFamily);
return Object.assign(context.measureText(text), {
/* FIXME: The following is a dirty hack until the Canvas v5 API is
* widely supported in major browsers. The 1.13 multiplier comes
* from the fabric.js source code - don't ask me why it's 1.13.
* See also:
* https://kangax.github.io/jstests/canvas-v5/
* https://lists.w3.org/Archives/Public/public-whatwg-archive/2012Mar/0269.htm
*/
//height: options.fontSize * 1.13,
height: options.fontSize * (fontMeasurements.descender - fontMeasurements.topBounding),
ascender: options.fontSize * fontMeasurements.ascender,
descender: options.fontSize * fontMeasurements.descender,
capHeight: options.fontSize * fontMeasurements.capHeight,
median: options.fontSize * fontMeasurements.median,
topBounding: options.fontSize * fontMeasurements.topBounding,
});
}

@ -0,0 +1,28 @@
'use strict';
const canvassed = require("canvassed");
const defaultValue = require("default-value");
module.exports = function setTextStyles(context, options) {
canvassed.validateSync(options, {
fontFamily: "required",
fontSize: "required"
});
let fontSegments = [
options.fontStyle,
options.fontVariant,
options.fontWeight,
`${options.fontSize}px`, // FIXME: Other units?
options.fontFamily
];
context.font = fontSegments.filter(segment => (segment != null)).join(" ");
context.textBaseline = defaultValue(options.textBaseline, "alphabetic");
if (options.isStroke) {
context.strokeStyle = options.color;
} else {
context.fillStyle = options.color;
}
}

@ -0,0 +1,25 @@
'use strict';
const pegjs = require("pegjs");
const util = require("util");
const fs = require("fs");
const createBetterPegTracer = require("better-peg-tracer");
//let toParse = "hello {bold}world {italic}{color:red}people{/color}{/italic}{/bold}!";
let toParse = `
foo{{{}}
{{align:center}bar
qux {class:test1,test2}baz{/class}{/align}}
`.trim();
let parser = pegjs.buildParser(fs.readFileSync("./src/parser/tag-parser.pegjs").toString(), {
trace: true
});
let results = parser.parse(toParse, {
tracer: createBetterPegTracer(toParse)
});
console.log("\n\n");
console.log(util.inspect(results, {colors: true, depth: null}));
Loading…
Cancel
Save