joicomponents
Version:
Design patterns to build native web components
327 lines (289 loc) • 8.96 kB
JavaScript
function resolveVariable(key, map) {
let next;
while (next = map[key])
key = next;
return key;
}
function matchArgument(a, b, varMap) {
a = resolveVariable(a, varMap);
if (a.startsWith(":"))
return varMap[a] = b;
b = resolveVariable(b, varMap);
if (b.startsWith(":"))
return varMap[b] = a;
return a === b;
}
function matchArguments(as, bs, varMap) {
as = resolveVariable(as, varMap);
if (!Array.isArray(as))
return varMap[as] = bs;
bs = resolveVariable(bs, varMap);
if (!Array.isArray(bs))
return varMap[bs] = as;
if (as.length !== bs.length)
return false;
for (let i = 0; i < as.length; i++)
if (!matchArgument(as[i], bs[i], varMap))
return false;
return true;
}
class HashDot {
constructor(full, flat) {
this.tagName = full;
this.tagValue = flat;
this.args = [];
this.flatArgs = [];
}
addArgument(full, flat, varCounter) {
if (!this.args.length && full.startsWith("::"))
return this.args = full + "-" + varCounter;
if (this.args.length && (full.startsWith("::") || !Array.isArray(this.args)))
throw new Error(`DoubleDots '::' must be the only argument.`);
if (full.startsWith(":"))
full += "-" + varCounter;
this.args.push(full);
this.flatArgs.push(flat);
}
static _flattenArgs(args) {
return args.map(arg => {
if (arg.startsWith(":"))
return arg;
if (arg.startsWith(".'"))
return arg.substring(2, arg.length - 1).replace(/\\'/, "'");
if (arg.startsWith('."'))
return arg.substring(2, arg.length - 1).replace(/\\"/, '"');
return arg.substring(1);
});
}
flatten(varMap) {
let flat = new HashDot(this.tagName, this.tagValue);
flat.args = this.args;
if (!Array.isArray(flat.args))
flat.args = resolveVariable(flat.args, varMap);
if (!Array.isArray(flat.args)) {
flat.flatArgs = flat.args;
return flat;
}
flat.args = flat.args.map(arg => resolveVariable(arg, varMap));
flat.flatArgs = HashDot._flattenArgs(flat.args);
return flat;
}
//todo 1: make match return a new hybrid HashDot?
//todo 2: this hybrid uses the args of the left, but fills its flatArg values for variables with the right side?
//todo 3: or does this hybrid object use a varMap?
//todo make varMap immutable?
//todo should matchArguments use the flatValues??
match(otherDot, varMap) {
return this.tagValue === otherDot.tagValue && matchArguments(this.args, otherDot.args, varMap);
}
toString() {
return this.tagName + (Array.isArray(this.args) ? this.args.join("") : this.args);
}
}
class MatchResult {
constructor(input, hitSide, replaceSide, start, stop, varMap) {
this.input = input;
this.hitSide = hitSide;
this.replaceSide = replaceSide;
this.varMap = varMap;
this.start = start;
this.stop = stop;
}
static transform(thiz) {
let res = [].concat(thiz.input);
res.splice(thiz.start, thiz.stop, ...thiz.replaceSide);
return res.map(dot => dot.flatten(thiz.varMap));
}
static find(thiz) {
return thiz.hitSide.map(dot => dot.flatten(thiz.varMap));
}
static translate(thiz) {
return thiz.replaceSide.map(dot => dot.flatten(thiz.varMap));
}
}
let variableCounter = 0;
export class HashDots {
static parse(input) {
input = input.trim();
if (input.length === 0)
return [];
if (!(input.startsWith("#") || input.startsWith("/") || input.startsWith("!") || input.startsWith(";")))
throw new SyntaxError(`HashDot sequence must start with #,!,/ or ;.\nInput: ${input}\nError: ↑`);
const varCounter = variableCounter++;
const hashOrDot = /[#/!]+([\w]+)|\.([\w]+)|\."((\\.|[^"])*)"|\.'((\\.|[^'])*)'|::?[\w]+|=|;|\s+|(.+)/g;
const rules = [];
let rule = {left: []};
let dots = rule.left;
let dot;
for (let next; (next = hashOrDot.exec(input)) !== null;) {
if (next[7]) {
const errorPos = hashOrDot.lastIndex - next[7].length + 1;
throw new SyntaxError(`HashDot syntax error:\nInput: ${input}\nError: ${Array(errorPos).join(" ")}↑`);
}
let word = next[0];
let flat = next[1] || next[2] || (next[3] && next[3].replace(/\\"/, '"')) || (next[5] && next[5].replace(/\\'/, "'"));
if (word[0].match(/\s/))
continue;
if (word === "=") {
rule.right = [];
dots = rule.right;
dot = undefined;
continue;
}
if (word === ";") {
if (rule.left.length !== 0) {
rules.push(rule);
rule = {left: []};
dots = rule.left;
}
continue;
}
if (word.startsWith("#") || word.startsWith("/") || word.startsWith("!")) {
dots.push(dot = new HashDot(word, flat));
continue;
}
if (dot === undefined) {
const errorPos = hashOrDot.lastIndex - word.length + 1;
throw new SyntaxError(`HashDot syntax error. HashDot sequence must start with '#':\nInput: ${input}\nError: ${Array(errorPos).join(" ")}↑`);
}
try {
dot.addArgument(word, flat, varCounter);
} catch (err) {
const errorPos = hashOrDot.lastIndex - word.length + 1;
throw new SyntaxError(`HashDot syntax error: ${err.message}\nInput: ${input}\nError: ${Array(errorPos).join(" ")}↑`);
}
}
if (rule.left.length !== 0)
rules.push(rule);
return rules;
}
static subsetMatch(a, b, c) {
for (let i = 0; i < a.length; i++) {
if (a.length - i < b.length)
return null;
for (let j = 0; j < b.length; j++) {
let varMap = HashDots._matchImpl(a, b, i, j);
if (varMap)
return new MatchResult(a, b, c, i, b.length, varMap);
}
}
return null;
}
static supersetMatch(a, b, c) {
for (let i = 0; i < b.length; i++) {
if (b.length - i < a.length)
return null;
for (let j = 0; j < a.length; j++) {
let varMap = HashDots._matchImpl(b, a, i, j);
if (varMap)
return new MatchResult(a, b, c, i, a.length, varMap);
}
}
return null;
}
static exactMatch(a, b, c) {
if (a.length !== b.length)
return null;
let varMap = HashDots._matchImpl(a, b, 0, 0);
if (varMap)
return new MatchResult(a, b, c, 0, a.length, varMap);
return null;
}
static _matchImpl(a, b, i, j) {
let varMap = {};
for (let k = 0; k < b.length; k++) {
if (!a[i + k].match(b[j + k], varMap))
return null;
}
return varMap;
}
}
export class HashDotMap {
static make(routeMap) {
return new HashDotMap(routeMap);
}
constructor(routeMap) {
this._rules = HashDots.parse(routeMap);
}
//todo when rules are added or modified mid-flight, then they should do so on immutable this._rules object,
//todo so as to not alter the other .query() - iterators already out there.
query(input) {
if (typeof input === "string" || input instanceof String)
input = HashDots.parse(input)[0].left;
return new HashDotQuery(input, this._rules);
}
}
class HashDotQuery {
constructor(query, rules) {
this.original = query;
this.rules = rules;
this.query = query;
this.i = 0;
this.interpreter = MatchResult.translate;
this.matchFunction = HashDots.exactMatch;
this._recursive = false;
this._inverted = false;
}
[Symbol.iterator]() {
return this;
}
//front
find() { //todo rename to frontSide
this.interpreter = MatchResult.find;
return this;
}
translate() {
this.interpreter = MatchResult.translate; //todo convert?
return this;
}
transform() { //todo merge?
this.interpreter = MatchResult.transform;
return this;
}
//todo add the possibility of retrieving the whole rule?
ruleIsSubsetOfQuery() {
this.matchFunction = HashDots.subsetMatch;
return this;
}
queryIsSubsetOfRule() {
this.matchFunction = HashDots.supersetMatch;
return this;
}
queryIsExactRule() {
this.matchFunction = HashDots.exactMatch;
return this;
}
recursive() {
this._recursive = true;
return this;
}
reverse() {
this._inverted = !this._inverted;
return this;
}
next() {
while (this.i < this.rules.length) {
let rule = this.rules[this.i++];
let value = this._inverted ? this.matchFunction(this.query, rule.right, rule.left) : this.matchFunction(this.query, rule.left, rule.right);
if (!value)
continue;
if (this.interpreter)
value = this.interpreter(value);
if (this._recursive) {
this.i = 0;
this.query = value;
}
return {done: false, value: value};
}
return {done: true};
}
first() {
this.i = 0;
return this.next().value;
}
reset(){
this.i = 0;
this.input = this.original;
return this;
}
}