242 lines
5.9 KiB
JavaScript
242 lines
5.9 KiB
JavaScript
import { monotonicInterpolate } from "./util/monotonic-interpolate.mjs";
|
|
|
|
export function applyMetricOverride(para, mo, argv) {
|
|
const bindings = initBindings(para, argv);
|
|
for (const [field, expr] of Object.entries(mo)) {
|
|
if (!validMetricOverrideFields.has(field)) {
|
|
console.error(`Field ${field} cannot be get overridden.`);
|
|
continue;
|
|
}
|
|
if (typeof expr === "number") {
|
|
para[field] = expr;
|
|
} else if (typeof expr === "string") {
|
|
const e = RootExpression(new State(expr), bindings);
|
|
if (typeof e !== "number")
|
|
throw new TypeError(`Expression ${e} do not evaluate to a number`);
|
|
para[field] = e;
|
|
} else {
|
|
throw new SyntaxError(`Invalid expression ${JSON.stringify(expr)}`);
|
|
}
|
|
}
|
|
}
|
|
const validMetricOverrideFields = new Set([
|
|
"cap",
|
|
"ascender",
|
|
"xHeight",
|
|
"sb",
|
|
"accentWidth",
|
|
"accentClearance",
|
|
"accentHeight",
|
|
"accentStackOffset",
|
|
"dotSize",
|
|
"periodSize",
|
|
"leading",
|
|
"winMetricAscenderPad",
|
|
"winMetricDescenderPad",
|
|
"symbolMid",
|
|
"parenSize",
|
|
"powerlineScaleY",
|
|
"powerlineScaleX",
|
|
"powerlineShiftY",
|
|
"powerlineShiftX",
|
|
"onumZeroHeightRatio",
|
|
"essRatioUpper",
|
|
"essRatioLower",
|
|
"essRatioQuestion",
|
|
"essRatio",
|
|
"archDepth",
|
|
"smallArchDepth"
|
|
]);
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Function bindings
|
|
|
|
function initBindings(para, argv) {
|
|
const valueBindings = new Map();
|
|
for (const k of validMetricOverrideFields) {
|
|
valueBindings.set("default_" + k, para[k]);
|
|
}
|
|
valueBindings.set("weight", argv.shape.weight);
|
|
valueBindings.set("width", argv.shape.width);
|
|
valueBindings.set("slopeAngle", argv.shape.slopeAngle);
|
|
const functionBindings = new Map();
|
|
functionBindings.set("blend", blend);
|
|
return { val: valueBindings, functions: functionBindings };
|
|
}
|
|
|
|
function blend(against, ...pairs) {
|
|
const xs = [],
|
|
ys = [];
|
|
for (const [x, y] of pairs) {
|
|
xs.push(x), ys.push(y);
|
|
}
|
|
return monotonicInterpolate(xs, ys)(against);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Simple expression parser
|
|
|
|
class State {
|
|
constructor(input) {
|
|
this.input = input;
|
|
this.cp = 0;
|
|
}
|
|
fetch(ch) {
|
|
return this.input[this.cp];
|
|
}
|
|
test(ch) {
|
|
return this.input[this.cp] === ch;
|
|
}
|
|
testCk(f) {
|
|
return f(this.input[this.cp]);
|
|
}
|
|
advance() {
|
|
return this.input[this.cp++];
|
|
}
|
|
expectAndAdvance(ch) {
|
|
if (!this.test(ch)) this.fail();
|
|
this.advance();
|
|
}
|
|
expectEnd() {
|
|
if (this.cp < this.input.length) this.fail();
|
|
}
|
|
fail() {
|
|
throw new SyntaxError("Failed to parse expression: " + this.input + "@" + this.cp);
|
|
}
|
|
}
|
|
|
|
function RootExpression(state, bindings) {
|
|
const e = Expression(state, bindings);
|
|
state.expectEnd();
|
|
return e;
|
|
}
|
|
function Expression(state, bindings) {
|
|
skipSpaces(state);
|
|
const e = Sum(state, bindings);
|
|
skipSpaces(state);
|
|
return e;
|
|
}
|
|
function Sum(state, bindings) {
|
|
let f = Term(state, bindings);
|
|
skipSpaces(state);
|
|
while (state.test("+") || state.test("-")) {
|
|
let op = state.advance();
|
|
skipSpaces(state);
|
|
const g = Term(state, bindings);
|
|
skipSpaces(state);
|
|
switch (op) {
|
|
case "+":
|
|
f = f + g;
|
|
break;
|
|
case "-":
|
|
f = f - g;
|
|
break;
|
|
}
|
|
}
|
|
return f;
|
|
}
|
|
function Term(state, bindings) {
|
|
let f = Factor(state, bindings);
|
|
skipSpaces(state);
|
|
while (state.test("*") || state.test("/")) {
|
|
let op = state.advance();
|
|
skipSpaces(state);
|
|
const g = Factor(state, bindings);
|
|
skipSpaces(state);
|
|
switch (op) {
|
|
case "*":
|
|
f = f * g;
|
|
break;
|
|
case "/":
|
|
f = f / g;
|
|
break;
|
|
}
|
|
}
|
|
return f;
|
|
}
|
|
function Factor(state, bindings) {
|
|
if (state.test("+")) {
|
|
state.advance();
|
|
skipSpaces(state);
|
|
return Factor(state, bindings);
|
|
} else if (state.test("-")) {
|
|
state.advance();
|
|
skipSpaces(state);
|
|
return -Factor(state, bindings);
|
|
} else {
|
|
return Primitive(state, bindings);
|
|
}
|
|
}
|
|
function Primitive(state, bindings) {
|
|
if (state.testCk(isDigit)) return Lit(state, bindings);
|
|
if (state.testCk(isAlpha)) return BindingOrCall(state, bindings);
|
|
if (state.test("(")) return Group(state, bindings);
|
|
if (state.test("[")) return List("[", "]", state, bindings);
|
|
state.fail();
|
|
}
|
|
function Lit(state, bindings) {
|
|
let integerPart = 0;
|
|
let fractionPart = 0;
|
|
let fractionScale = 1;
|
|
while (state.testCk(isDigit)) {
|
|
const digit = state.advance().codePointAt(0) - "0".codePointAt(0);
|
|
integerPart = integerPart * 10 + digit;
|
|
}
|
|
if (state.test(".")) {
|
|
state.advance();
|
|
while (state.testCk(isDigit)) {
|
|
fractionScale /= 10;
|
|
const digit = state.advance().codePointAt(0) - "0".codePointAt(0);
|
|
fractionPart += digit * fractionScale;
|
|
}
|
|
}
|
|
return integerPart + fractionPart;
|
|
}
|
|
function BindingOrCall(state, bindings) {
|
|
let symbolName = "";
|
|
while (state.testCk(isAlpha)) symbolName += state.advance();
|
|
if (state.test("(")) {
|
|
const args = List("(", ")", state, bindings);
|
|
if (bindings.functions.has(symbolName)) return bindings.functions.get(symbolName)(...args);
|
|
else throw new TypeError(`Unknown function ${symbolName}.`);
|
|
} else {
|
|
if (bindings.val.has(symbolName)) return bindings.val.get(symbolName);
|
|
else throw new TypeError(`Unknown identifier ${symbolName}.`);
|
|
}
|
|
}
|
|
function Group(state, bindings) {
|
|
state.expectAndAdvance("(");
|
|
skipSpaces(state);
|
|
const e = Expression(state, bindings);
|
|
skipSpaces(state);
|
|
state.expectAndAdvance(")");
|
|
return e;
|
|
}
|
|
function List(start, end, state, bindings) {
|
|
let results = [];
|
|
state.expectAndAdvance(start);
|
|
skipSpaces(state);
|
|
results.push(Expression(state, bindings));
|
|
skipSpaces(state);
|
|
while (state.test(",")) {
|
|
state.expectAndAdvance(",");
|
|
skipSpaces(state);
|
|
results.push(Expression(state, bindings));
|
|
skipSpaces(state);
|
|
}
|
|
state.expectAndAdvance(end);
|
|
return results;
|
|
}
|
|
|
|
function skipSpaces(state) {
|
|
while (state.testCk(isSpace)) state.advance();
|
|
}
|
|
function isSpace(ch) {
|
|
return ch === " " || ch === "\t";
|
|
}
|
|
function isDigit(ch) {
|
|
return ch >= "0" && ch <= "9";
|
|
}
|
|
function isAlpha(ch) {
|
|
return (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || ch === "_";
|
|
}
|