playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
421 lines (420 loc) • 11 kB
JavaScript
const EOF_TOKEN = 0;
const ERROR_TOKEN = 1;
const TEXT_TOKEN = 2;
const OPEN_BRACKET_TOKEN = 3;
const CLOSE_BRACKET_TOKEN = 4;
const EQUALS_TOKEN = 5;
const STRING_TOKEN = 6;
const IDENTIFIER_TOKEN = 7;
const WHITESPACE_TOKEN = 8;
const WHITESPACE_CHARS = " \n\r\v\f";
const IDENTIFIER_REGEX = /[\w|/]/;
class Scanner {
constructor(symbols) {
this._symbols = symbols;
this._index = 0;
this._last = 0;
this._cur = this._symbols.length > 0 ? this._symbols[0] : null;
this._buf = [];
this._mode = "text";
this._error = null;
}
// read the next token, ignore whitespace
read() {
let token = this._read();
while (token === WHITESPACE_TOKEN) {
token = this._read();
}
if (token !== EOF_TOKEN && token !== ERROR_TOKEN) {
this._last = this._index;
}
return token;
}
// returns the buffer for the last returned token
buf() {
return this._buf;
}
// returns the index of end of the last successful token extraction
last() {
return this._last;
}
// return the error message
error() {
return this._error;
}
// print the scanner output
debugPrint() {
const tokenStrings = ["EOF", "ERROR", "TEXT", "OPEN_BRACKET", "CLOSE_BRACKET", "EQUALS", "STRING", "IDENTIFIER", "WHITESPACE"];
let token = this.read();
let result = "";
while (true) {
result += `${(result.length > 0 ? "\n" : "") + tokenStrings[token]} '${this.buf().join("")}'`;
if (token === EOF_TOKEN || token === ERROR_TOKEN) {
break;
}
token = this.read();
}
return result;
}
// read the next token from the input stream and return the token
_read() {
this._buf = [];
if (this._eof()) {
return EOF_TOKEN;
}
return this._mode === "text" ? this._text() : this._tag();
}
// read text block until eof or start of tag
_text() {
while (true) {
switch (this._cur) {
case null:
return this._buf.length > 0 ? TEXT_TOKEN : EOF_TOKEN;
case "[":
this._mode = "tag";
return this._buf.length > 0 ? TEXT_TOKEN : this._tag();
case "\\":
this._next();
switch (this._cur) {
case "[":
this._store();
break;
default:
this._output("\\");
break;
}
break;
default:
this._store();
break;
}
}
}
// read tag block
_tag() {
switch (this._cur) {
case null:
this._error = "unexpected end of input reading tag";
return ERROR_TOKEN;
case "[":
this._store();
return OPEN_BRACKET_TOKEN;
case "]":
this._store();
this._mode = "text";
return CLOSE_BRACKET_TOKEN;
case "=":
this._store();
return EQUALS_TOKEN;
case " ":
case " ":
case "\n":
case "\r":
case "\v":
case "\f":
return this._whitespace();
case '"':
return this._string();
default:
if (!this._isIdentifierSymbol(this._cur)) {
this._error = "unrecognized character";
return ERROR_TOKEN;
}
return this._identifier();
}
}
_whitespace() {
this._store();
while (WHITESPACE_CHARS.indexOf(this._cur) !== -1) {
this._store();
}
return WHITESPACE_TOKEN;
}
_string() {
this._next();
while (true) {
switch (this._cur) {
case null:
this._error = "unexpected end of input reading string";
return ERROR_TOKEN;
case '"':
this._next();
return STRING_TOKEN;
default:
this._store();
break;
}
}
}
_identifier() {
this._store();
while (this._cur !== null && this._isIdentifierSymbol(this._cur)) {
this._store();
}
return IDENTIFIER_TOKEN;
}
_isIdentifierSymbol(s) {
return s.length === 1 && s.match(IDENTIFIER_REGEX) !== null;
}
_eof() {
return this._cur === null;
}
_next() {
if (!this._eof()) {
this._index++;
this._cur = this._index < this._symbols.length ? this._symbols[this._index] : null;
}
return this._cur;
}
_store() {
this._buf.push(this._cur);
return this._next();
}
_output(c) {
this._buf.push(c);
}
}
class Parser {
constructor(symbols) {
this._scanner = new Scanner(symbols);
this._error = null;
}
// parse the incoming symbols placing resulting symbols in symbols
// and tags in tags
// tags is an array of the following structure:
// {
// name: string; // tag name, for example 'color'
// value: string; // optional tag value, for example '#ff0000'
// attributes: { // list of attributes
// key: value; // optional key/value pairs
// }
// start: int; // first symbol to which this tag applies
// end: int; // last symbol to which this tag applies
// }
parse(symbols, tags) {
while (true) {
const token = this._scanner.read();
switch (token) {
case EOF_TOKEN:
return true;
case ERROR_TOKEN:
return false;
case TEXT_TOKEN:
Array.prototype.push.apply(symbols, this._scanner.buf());
break;
case OPEN_BRACKET_TOKEN:
if (!this._parseTag(symbols, tags)) {
return false;
}
break;
default:
return false;
}
}
}
// access an error message if the parser failed
error() {
return `Error evaluating markup at #${this._scanner.last().toString()} (${this._scanner.error() || this._error})`;
}
_parseTag(symbols, tags) {
let token = this._scanner.read();
if (token !== IDENTIFIER_TOKEN) {
this._error = "expected identifier";
return false;
}
const name = this._scanner.buf().join("");
if (name[0] === "/") {
for (let index = tags.length - 1; index >= 0; --index) {
if (name === `/${tags[index].name}` && tags[index].end === null) {
tags[index].end = symbols.length;
token = this._scanner.read();
if (token !== CLOSE_BRACKET_TOKEN) {
this._error = "expected close bracket";
return false;
}
return true;
}
}
this._error = "failed to find matching tag";
return false;
}
const tag = {
name,
value: null,
attributes: {},
start: symbols.length,
end: null
};
token = this._scanner.read();
if (token === EQUALS_TOKEN) {
token = this._scanner.read();
if (token !== STRING_TOKEN) {
this._error = "expected string";
return false;
}
tag.value = this._scanner.buf().join("");
token = this._scanner.read();
}
while (true) {
switch (token) {
case CLOSE_BRACKET_TOKEN:
tags.push(tag);
return true;
case IDENTIFIER_TOKEN: {
const identifier = this._scanner.buf().join("");
token = this._scanner.read();
if (token !== EQUALS_TOKEN) {
this._error = "expected equals";
return false;
}
token = this._scanner.read();
if (token !== STRING_TOKEN) {
this._error = "expected string";
return false;
}
const value = this._scanner.buf().join("");
tag.attributes[identifier] = value;
break;
}
default:
this._error = "expected close bracket or identifier";
return false;
}
token = this._scanner.read();
}
}
}
function merge(target, source) {
for (const key in source) {
if (!source.hasOwnProperty(key)) {
continue;
}
const value = source[key];
if (value instanceof Object) {
if (!target.hasOwnProperty(key)) {
target[key] = {};
}
merge(target[key], source[key]);
} else {
target[key] = value;
}
}
}
function combineTags(tags) {
if (tags.length === 0) {
return null;
}
const result = {};
for (let index = 0; index < tags.length; ++index) {
const tag = tags[index];
const tmp = {};
tmp[tag.name] = { value: tag.value, attributes: tag.attributes };
merge(result, tmp);
}
return result;
}
function resolveMarkupTags(tags, numSymbols) {
if (tags.length === 0) {
return null;
}
const edges = {};
for (let index = 0; index < tags.length; ++index) {
const tag = tags[index];
if (!edges.hasOwnProperty(tag.start)) {
edges[tag.start] = { open: [tag], close: null };
} else {
if (edges[tag.start].open === null) {
edges[tag.start].open = [tag];
} else {
edges[tag.start].open.push(tag);
}
}
if (!edges.hasOwnProperty(tag.end)) {
edges[tag.end] = { open: null, close: [tag] };
} else {
if (edges[tag.end].close === null) {
edges[tag.end].close = [tag];
} else {
edges[tag.end].close.push(tag);
}
}
}
let tagStack = [];
function removeTags(tags2) {
tagStack = tagStack.filter((tag) => {
return tags2.find((t) => {
return t === tag;
}) === void 0;
});
}
function addTags(tags2) {
for (let index = 0; index < tags2.length; ++index) {
tagStack.push(tags2[index]);
}
}
const edgeKeys = Object.keys(edges).sort((a, b) => {
return a - b;
});
const resolvedTags = [];
for (let index = 0; index < edgeKeys.length; ++index) {
const edge = edges[edgeKeys[index]];
if (edge.close !== null) {
removeTags(edge.close);
}
if (edge.open !== null) {
addTags(edge.open);
}
resolvedTags.push({
start: edgeKeys[index],
tags: combineTags(tagStack)
});
}
const result = [];
let prevTag = null;
for (let index = 0; index < resolvedTags.length; ++index) {
const resolvedTag = resolvedTags[index];
while (result.length < resolvedTag.start) {
result.push(prevTag ? prevTag.tags : null);
}
prevTag = resolvedTag;
}
while (result.length < numSymbols) {
result.push(null);
}
return result;
}
function evaluateMarkup(symbols) {
const parser = new Parser(symbols);
const stripped_symbols = [];
const tags = [];
if (!parser.parse(stripped_symbols, tags)) {
console.warn(parser.error());
return {
symbols,
tags: null
};
}
const invalidTag = tags.find((t) => {
return t.end === null;
});
if (invalidTag) {
console.warn(`Markup error: found unclosed tag='${invalidTag.name}'`);
return {
symbols,
tags: null
};
}
const resolved_tags = resolveMarkupTags(tags, stripped_symbols.length);
return {
symbols: stripped_symbols,
tags: resolved_tags
};
}
class Markup {
static evaluate(symbols) {
return evaluateMarkup(symbols);
}
}
export {
Markup
};