playcanvas
Version:
PlayCanvas WebGL game engine
427 lines (425 loc) • 10.2 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 = ' \t\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() {
let token = this._read();
while(token === WHITESPACE_TOKEN){
token = this._read();
}
if (token !== EOF_TOKEN && token !== ERROR_TOKEN) {
this._last = this._index;
}
return token;
}
buf() {
return this._buf;
}
last() {
return this._last;
}
error() {
return this._error;
}
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() {
this._buf = [];
if (this._eof()) {
return EOF_TOKEN;
}
return this._mode === 'text' ? this._text() : this._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;
}
}
}
_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 '\t':
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(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;
}
}
}
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: 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(tags) {
tagStack = tagStack.filter((tag)=>{
return tags.find((t)=>{
return t === tag;
}) === undefined;
});
}
function addTags(tags) {
for(let index = 0; index < tags.length; ++index){
tagStack.push(tags[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: 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: 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 };