UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

421 lines (420 loc) 9.98 kB
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 };