catberry
Version:
Catberry is an isomorphic framework for building universal front-end apps using components, Flux architecture and progressive rendering.
201 lines (176 loc) • 3.96 kB
JavaScript
'use strict';
const STATES = {
ILLEGAL: -1,
INITIAL: 0,
CONTENT: 1,
COMPONENT: 2,
COMMENT: 3,
END: 4
};
Object.freeze(STATES);
const COMPONENT_TAG_NAME_REGEXP = /^<((cat-)|((document|head|body)[\s/>]))/i;
const COMPONENT_NAME_MIN_LENGTH = 10;
class HTMLTokenizer {
/**
* Creates a new instance of the simplified streaming HTML tokenizer.
*/
constructor() {
/**
* Current token buffer.
* @type {string}
* @private
*/
this._source = '';
/**
* Current index in buffer.
* @type {number}
* @private
*/
this._currentIndex = 0;
/**
* Current token identifier.
* @type {number}
* @private
*/
this._currentState = STATES.INITIAL;
}
/**
* Gets the state constants map.
*/
static get STATES() {
return STATES;
}
/**
* Sets a HTML string to the tokenizer.
* @param {string} html HTML string.
*/
setHTMLString(html) {
this._source = html;
this._currentIndex = 0;
this._currentState = STATES.INITIAL;
}
/**
* Gets a next token.
* @returns {{state: number, value: string}} Token descriptor.
*/
next() {
const start = this._currentIndex;
const state = this._currentState;
switch (this._currentState) {
case STATES.CONTENT:
this.content();
break;
case STATES.COMPONENT:
this.component();
break;
case STATES.COMMENT:
this.comment();
break;
case STATES.END:
return {
state,
value: null
};
case STATES.ILLEGAL:
this._currentState = STATES.INITIAL;
this._currentIndex++;
break;
default:
this.initial();
return this.next();
}
return {
state,
value: this._source.substring(start, this._currentIndex)
};
}
/**
* Switches machine to the "data" state.
*/
initial() {
if (this._currentIndex >= this._source.length) {
this._currentState = STATES.END;
return;
}
// maybe comment or component
if (this._source[this._currentIndex] === '<') {
// comment
if (this._source[this._currentIndex + 1] === '!') {
if (this._source[this._currentIndex + 2] === '-' &&
this._source[this._currentIndex + 3] === '-') {
this._currentState = STATES.COMMENT;
return;
}
this._currentState = STATES.CONTENT;
return;
}
if (this.checkIfComponent()) {
this._currentState = STATES.COMPONENT;
return;
}
}
this._currentState = STATES.CONTENT;
}
/**
* Switches machine to the "tag" state.
*/
component() {
this._currentIndex += 5;
while (this._currentIndex < this._source.length) {
if (this._source[this._currentIndex] === '>') {
this._currentIndex++;
this._currentState = STATES.INITIAL;
return;
}
this._currentIndex++;
}
this._currentState = STATES.ILLEGAL;
}
/**
* Switches machine to the "content" state.
*/
content() {
this._currentIndex++;
while (this._currentIndex < this._source.length) {
if (this._source[this._currentIndex] === '<') {
this._currentState = STATES.INITIAL;
return;
}
this._currentIndex++;
}
this._currentState = STATES.END;
}
/**
* Switches machine to the "comment" state.
*/
comment() {
this._currentIndex += 4;
while (this._currentIndex < this._source.length) {
if (this._source[this._currentIndex] === '-') {
if (this._currentIndex + 2 >= this._source.length) {
this._currentState = STATES.ILLEGAL;
return;
}
if (this._source[this._currentIndex + 1] === '-' &&
this._source[this._currentIndex + 2] === '>') {
this._currentIndex += 3;
this._currentState = STATES.INITIAL;
return;
}
}
this._currentIndex++;
}
this._currentState = STATES.ILLEGAL;
}
/**
* Checks if following HTML is a component.
* @returns {boolean} Is it a component?
*/
checkIfComponent() {
const testString = this._source.substr(
this._currentIndex, COMPONENT_NAME_MIN_LENGTH
);
return COMPONENT_TAG_NAME_REGEXP.test(testString);
}
}
module.exports = HTMLTokenizer;