bliss-svg-builder
Version:
Generates SVG code for Bliss (Blissymbolics) text using a Domain-Specific Language.
427 lines (365 loc) • 16.3 kB
JavaScript
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { blissElementDefinitions } from "./bliss-element-definitions.js";
import { BlissParser } from "./bliss-parser.js";
const DEFAULT_STROKE_WIDTH = 0.5;
const MAX_STROKE_WIDTH = 1.5;
const MIN_STROKE_WIDTH = 0.1;
const DEFAULT_CHAR_SPACING = 2;
const DEFAULT_FULL_SPACE = 8;
const DEFAULT_HALF_SPACE = 4;
const DEFAULT_QUARTER_SPACE = 2;
const DEFAULT_SENTENCE_SPACING = DEFAULT_CHAR_SPACING + DEFAULT_FULL_SPACE;
const DEFAULT_WORD_SPACING = DEFAULT_CHAR_SPACING + DEFAULT_HALF_SPACE;
const DEFAULT_PUNCTUATION_SPACING = DEFAULT_CHAR_SPACING + DEFAULT_QUARTER_SPACE;
export class BlissElement {
//#region Private Properties
#level
#extraPathOptions
#isCharacter
#isAtomic
#isIndicator
#isExternalGlyph
#width
#height
#children
#relativeToRootX
#relativeToParentX
#relativeToParentY
//#codeString
#codeName
#charSpacing
#wordSpacing
#sentenceSpacing
#punctuationSpacing
//#endregion
constructor(blissObj, { parentElement = null, previousElement = null, level = 0 } = {}) {
this.#level = level;
//this.#codeString = blissObj;
this.#codeName = "";
//this.#relativeToRootX = relativeToRootX;
//måste ta reda på this.#relativeToParentX utifrån parts. för infon saknas på charactern
this.#relativeToParentX = 0;
this.#relativeToParentY = 0;
this.#children = [];
this.#charSpacing = DEFAULT_CHAR_SPACING;
this.#wordSpacing = DEFAULT_WORD_SPACING;
this.#sentenceSpacing = DEFAULT_SENTENCE_SPACING;
this.#punctuationSpacing = DEFAULT_PUNCTUATION_SPACING;
//this.#relativeToRootX = (level === 2) ? parentElement.x : previousElement ? previousElement.x + previousElement.width + 2 : parentElement ? parentElement.x : 0; //space would be added differently depending on level and type etc
this.#relativeToRootX = (level === 2) ? parentElement.x : previousElement ? previousElement.x + previousElement.width + 2 : parentElement ? parentElement.x : 0; //space would be added differently depending on level and type etc
//this.#relativeToRootX = (previousElement ? previousElement.x : parentElement ? parentElement.x : 0) + (blissObj ? blissObj.x || 0 : 0);
this.parentElement = parentElement;
this.previousElement = previousElement;
if (blissObj.words) {
//let relativeToRootX = this.#relativeToRootX + this.#relativeToParentX;
for (const word of blissObj.words) {
//const child = new BlissElement(word, relativeToRootX);
const child = new BlissElement(word, { parentElement: this, previousElement: this.#children[this.#children.length - 1], level: this.#level + 1 });
//relativeToRootX = this.#relativeToParentX + child.width + this.#wordSpacing;
//child.type = "word";
this.#children.push(child);
}
this.getPath = (x = 0, y = 0) => this.#children.map(child => child.getPath(this.#relativeToRootX + x, this.#relativeToParentY + y)).join(' ');
} else if (blissObj.characters) {
if (this.#level < 1) this.#level = 1;
let relativeToLocalX = 0;
for (const character of blissObj.characters) {
//const child = new BlissElement(character, this.#relativeToParentX + relativeToLocalX);
const child = new BlissElement(character, { parentElement: this, previousElement: this.#children[this.#children.length - 1], level: this.#level + 1 });
/* handle this through querying previousElement instead.
if (child.isExternalGlyph) {
const defaultExternalGlyphSpacing = 0.1;
relativeToLocalX += child.width + defaultExternalGlyphSpacing;
} else {
relativeToLocalX += child.width + this.#charSpacing; //TODO take indicators into consideration
}*/
//child.type = "character";
this.#children.push(child);
}
this.getPath = (x = 0, y = 0) => this.#children.map(child => child.getPath(this.#relativeToParentX + x , this.#relativeToParentY + y)).join(' ');
} else {
if (this.#level < 2) this.#level = 2;
const codeName = blissObj.code;
if (level === 2) {
//detta funkar Inte, eller? nu ändrade jag så att x() returnerar relativeToParentX och inte (relativeToParentX+relativeToRootX)
this.#relativeToParentX = previousElement ? previousElement.x + previousElement.width + this.#charSpacing : 0;
} else {
this.#relativeToParentX = blissObj.x || 0;
//this.#relativeToParentX = (blissObj.x || 0) + previousElement ? previousElement.x + previousElement.width : 0;
}
this.#relativeToParentY = blissObj.y || 0;
const definition = blissElementDefinitions[codeName];
if (definition) {
this.#codeName = codeName; //default: empty string
this.#extraPathOptions = definition.extraPathOptions || {}; //default: empty object
this.#isCharacter = !!definition.isCharacter; //default: false
this.#isAtomic = !!definition.isAtomic; //default: false
this.#isExternalGlyph = !!definition.isExternalGlyph; //default: false
//hur ska jag göra så att isExternalGlyph hamnar på character, när den i själva verket läggs som en part?u
//if parent has only one child, och childen är isExternalGlyph, så är parenten ExternalGlyph?
//och isIndicator ligger ju på parenten, inte på definition
this.#width = definition.width;
this.#height = definition.height;
if (definition.getPath) {
this.getPath = (x = 0, y = 0) => definition.getPath(this.#relativeToParentX + x, this.#relativeToParentY + y, this.#extraPathOptions);
}
}
if (blissObj.parts) {
for (const part of blissObj.parts) {
//const child = new BlissElement(part, this.#relativeToParentX);
const child = new BlissElement(part, { parentElement: this, previousElement: this.#children[this.#children.length - 1], level: this.#level + 1 });
//child.type = "part";
this.#children.push(child);
}
if (blissObj?.isIndicator) {
const centerOfBaseCharacter = previousElement.width / 2; //should ask for center directly instead of calculating it.
const centerOfIndicator = blissObj.center;
const widthOfIndicator = blissObj.width;
const offsetX = centerOfBaseCharacter + centerOfIndicator - widthOfIndicator / 2;
//const offsetX = 2;
this.getPath = (x = 0, y = 0, level = 0) => {
// If level is 0, add the offsetX, else don't add it
const appliedOffsetX = (level === 0) ? x + this.#relativeToParentX + offsetX : x + this.#relativeToParentX;
const appliedOffsetY = y + this.#relativeToParentY;
return this.#children.map(child => child.getPath(appliedOffsetX, appliedOffsetY, level + 1)).join(' ');
}
//this.getPath = (x = 0, y = 0, level = 0) => this.#children.map(child => child.getPath(this.#relativeToParentX + x, this.#relativeToParentY + y)).join(' ');
//this.getPath = (x = 0, y = 0) => this.#children.map(child => child.getPath(this.#relativeToParentX + x, this.#relativeToParentY + y)).join(' ');
} else {
this.getPath = (x = 0, y = 0) => this.#children.map(child => child.getPath(this.#relativeToParentX + x, this.#relativeToParentY + y)).join(' ');
}
}
if (!definition?.getPath && !blissObj.parts) {
throw new Error(`Code ${blissObj.code} did not correspond to a definition with a getPath function.`);
}
}
}
//get codeString() {
// return this.#codeString;
// }
get codeName() {
return this.#codeName || "";
}
get width() {
if (this.#width !== undefined) {
return this.#width;
}
if (this.#children) {
let minRelativeX = Number.POSITIVE_INFINITY;
let maxRelativeXPlusWidth = Number.NEGATIVE_INFINITY;
for(let child of this.#children) {
minRelativeX = Math.min(minRelativeX, child.x);
maxRelativeXPlusWidth = Math.max(maxRelativeXPlusWidth, child.x + child.width);
}
return maxRelativeXPlusWidth - minRelativeX;
}
}
get height() {
if (this.#height !== undefined) {
return this.#height;
}
if (this.#isCharacter) {
return 20;
}
if (this.#children) {
return Math.max(...this.#children.map(child => this.y + child.y + child.height));
}
}
get x() {
return this.#relativeToParentX;// + this.#relativeToRootX;
}
get y() {
return this.#relativeToParentY;
}
//get #theOnlyChild() {
// return (this.#children.length === 1 && this.#children[0]) || {};
//}
get isCharacter() {
return this.#isCharacter; //this.#theOnlyChild.#isCharacter;
}
get isAtomic() {
return this.#isAtomic;
}
get isExternalGlyph() {
const isCharacterLevel = (this.#level === 2);
return isCharacterLevel ? !!(this.#children[0]?.isExternalGlyph) : false;
}
get includeGrid() {
return this.optio
}
toStringOldNotWorking() {
//return this.toJSON().elements.map(({ code, x = 0, y = 0 }) => (x === 0 && y === 0) ? code : `${code}:${x},${y}`).join(';')
const obj = this.toJSON();
if (obj.atomicElements) {
return obj.atomicElements.map(({ code, x = 0, y = 0 }) => (x === 0 && y === 0) ? code : `${code}:${x},${y}`).join(';')
} else {
//return obj.elements.map(({ code, x = 0, y = 0 }) => (x === 0 && y === 0) ? code : `${code}:${x},${y}`).join('/')
return (obj.elements || [])
.filter(e => e !== undefined && e !== null)
.map(({ code, x = 0, y = 0 }) => (x === 0 && y === 0) ? code : `${code}:${x},${y}`)
.join('/');
}
}
toStringOld2() {
const obj = this.toJSON();
const joinWith = this.#level === 0 ? "//" : this.#level === 1 ? "/" : ";";
return (obj.elements || [])
.filter(e => e !== undefined && e !== null)
.map(({ code, x = 0, y = 0 }) => (x === 0 && y === 0) ? code : `${code}:${x},${y}`)
.join(joinWith);
}
toStringOld3() {
const obj = this.toJSON();
let joinWith = "";
switch (this.#level) {
case 0: joinWith = "//"; break; // Sentence level
case 1: joinWith = "/"; break; // Word level
case 2: // Character level and beyond
default: joinWith = ";"; break;
}
return (obj.elements || [])
.filter(e => e !== undefined && e !== null)
.map(({ code, x = 0, y = 0 }) => (x === 0 && y === 0) ? code : `${code}:${x},${y}`)
.join(joinWith);
}
toStringDDD() {
const obj = this.toJSON();
return (obj.elements || [])
.filter(e => e !== undefined && e !== null)
.map(({ code, x = 0, y = 0, level }) => { // use the level of the child element
let joinWith = "";
switch (level) {
case 2: joinWith = "//"; break; // Sentence level
case 3: joinWith = "/"; break; // Word level
case 4: // Character level and beyond
default: joinWith = ";"; break;
}
return (x === 0 && y === 0) ? code : `${code}:${x},${y}${joinWith}`; // use joinWith here
})
.join('');
}
toStringj() {
const obj = this.toJSON();
}
toStringO() {
const obj = this.toJSON();
const processElement = ({ code, x = 0, y = 0, level, elements = [] }) => {
let joinWith = "";
switch (level) {
case 2: joinWith = "//"; break; // Sentence level
case 3: joinWith = "/"; break; // Word level
case 4: // Character level and beyond
default: joinWith = ";"; break;
}
const elementString = (x === 0 && y === 0) ? code : `${code}:${x},${y}`;
const childStrings = elements.map(processElement).join('');
return `${elementString}${joinWith}${childStrings}`;
};
return (obj.elements || []).map(processElement).join('');
}
toString() {
const obj = this.toJSON();
//const obj = {"elements":[{"elements":[{"elements":[{"code":"Xa","width":2.999438202247191,"x":null,"y":0,"level":3},{"code":"Xa","width":2.999438202247191,"x":null,"y":0,"level":3}]},{"elements":[{"code":"Xb","width":2.999438202247191,"x":null,"y":0,"level":3}]},{"elements":[{"code":"Xc","width":2.676404494382022,"x":null,"y":0,"level":3}]}]}]};
function traverse(obj, level = 1) {
if (!obj.elements) {
let str = obj.code;
if (obj.x !== 0 || obj.y !== 0) {
str = `${str}:${obj.x},${obj.y}`;
}
return str;
}
let delimiterMap = {
1: '//',
2: '/',
3: ';',
};
let results = obj.elements.map(subObj => traverse(subObj, level + 1)).join(delimiterMap[level]);
return results;
}
return traverse(obj);
}
toJSON() {
let obj = {};
if (this.#codeName && this.#isAtomic) {
const element = {};
element.code = this.codeName;
element.width = this.width;
element.x = this.#relativeToParentX;//this.x;
element.y = this.#relativeToParentY;//this.y;
//element.level = this.#level; // include the level in the returned object
obj = element;
} else if (this.#children) {
obj.elements = []
for (const child of this.#children) {
obj.elements.push(child.toJSON());
}
}
return obj;
}
toJSONOld4() {
const obj = {};
if (this.#codeName) {
const element = {};
element.code = this.codeName;
element.width = this.width;
element.x = this.x;
element.y = this.y;
element.level = this.#level;
obj.elements = [];
obj.elements.push(element);
} else if (this.#children) {
const elements = this.#children.map(child => child.toJSON().elements);
obj.elements = [];
obj.elements = obj.elements.concat(...elements);
}
return obj;
}
toJSONOldNotWorking() {
const obj = {};
if (this.#codeName && this.#isAtomic) {
const element = {};
element.code = this.codeName;
element.width = this.width;
element.x = this.x;
element.y = this.y;
obj.atomicElements = [];
obj.atomicElements.push(element);
} else if (this.#children) {
const elements = this.#children.map(child => child.toJSON().atomicElements); //doesn't work
obj.elements = [];
obj.elements = obj.elements.concat(...elements);
}
return obj;
}
#initExplicitElement(definition) {
this.#width = definition.width;
this.#height = definition.height;
this.getPath = (x = 0, y = 0) => definition.getPath(this.#relativeToParentX + x, this.#relativeToParentY + y, this.#extraPathOptions);
}
#initImplicitElement(parts) {
for (const part of parts) {
const child = new BlissElement(part, this.#relativeToParentX);
this.#children.push(child);
}
this.getPath = (x = 0, y = 0) => this.#children.map(child => child.getPath(x + this.#relativeToParentX, y + this.#relativeToParentY)).join(' ');
}
}
export class BlissCharacter extends BlissElement {
calculatePosition() {
// logic for calculating position of Character
}
}
export class BlissWord extends BlissElement {
calculatePosition() {
// logic for calculating position of Word
}
}
export class BlissShape extends BlissElement {
calculatePosition() {
// logic for calculating position of Shape
}
}