nova-frontend
Version:
Nova is an alternative to all those gigantic front-end frameworks, that often do more than is necessary when it comes to building simple UIs. Pure Vanilla Javascript is performance-wise the best way to build your front-end in a SPA, but it can be hard to
286 lines (254 loc) • 10.7 kB
JavaScript
const Element = require('./Element');
const Component = require('./Component');
/**
* @name Generator
* @class
* @desc
* The generator is a powerful way to generate HTML without writing actual HTML!
* It's meant to be very straightforward and to give your SPA a nice structure. No more angle brackets!
* @example
* import { Generator } from 'nova';
* const generator = new Generator();
* const header = generator.createTree(`
* header className: 'header'
* h1 className: 'header__title' innerText: 'Hello World!'
* h2 className: 'header__subtitle' innerText: 'This is my site.'
* end`)
*
* header.render();
*/
module.exports = class Generator {
constructor() {
this.tokens = [];
this.treeObjectArray = [];
this.indentationRule = 2
this.endToken = 'end'
this.rules = {
typeExpected: true,
valueExpected: false,
checkNextTypeIsValue: false
}
}
get elements() {
return this.elementsArray;
}
get tags() {
return ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bb", "bdo", "big", "blockquote", "body", "br /", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "command", "datagrid", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "eventsource", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr /", "html", "i", "iframe", "img", "input", "ins", "isindex", "kbd", "keygen", "label", "legend", "li", "link", "map", "mark", "menu", "meta", "meter", "nav", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strike", "strong", "style", "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]
}
_generateElementsFromTree() {
const root = document.getElementById('root');
const elementsArray = [];
let priorityArray = [];
let lowestIndentation = 0;
let previousPriority = null;
this.treeObjectArray.forEach((object, index) => {
if (index === 0)
lowestIndentation = object.priority
else
if (object.priority < lowestIndentation)
lowestIndentation = object.priority;
})
let iterations = 0;
let grandParent = false;
this.treeObjectArray.forEach((object) => {
let element;
if (object.priority === lowestIndentation) {
if (grandParent) {
throw new Error('Only one grandparent allowed!');
}
grandParent = true;
element = new Element(object.tag, root, object.propertyObject);
priorityArray = priorityArray.filter(elem => elem.priority <= object.priority - 2);
} else if (object.priority === (previousPriority + 2)) {
const parentObject = priorityArray.find(elem => elem.priority === object.priority - 2);
element = new Element(object.tag, parentObject.element, object.propertyObject);
} else if (object.priority <= (previousPriority - 2)) {
//Filter out everything that isn't grandparent
priorityArray = priorityArray.filter(elem => elem.priority <= object.priority - 2);
let parentObject = priorityArray.find(elem => elem.priority === object.priority - 2);
element = new Element(object.tag, parentObject.element, object.propertyObject);
} else if (object.priority === previousPriority) {
/* Removing previous priority so it doesn't interfere with paternity*/
priorityArray.splice(iterations - 1, 1);
iterations--;
const parentObject = priorityArray.find(elem => elem.priority === object.priority - 2);
element = new Element(object.tag, parentObject.element, object.propertyObject);
}
iterations++;
elementsArray.push(element);
priorityArray.push({ element, priority: object.priority });
previousPriority = object.priority;
})
this.elementsArray = elementsArray;
}
_createTreeObjectFromTokens() {
this.tokens.forEach((line, index) => {
const indentation = line[0].indentation;
const treeObject = {};
const propertyObject = {};
let tokenValue = false;
for (let tokenIndex = 0; tokenIndex < line.length; tokenIndex++) {
if (line[tokenIndex].type === 'token_tag') {
treeObject.tag = line[tokenIndex].token;
treeObject.priority = indentation;
continue;
}
if (line[tokenIndex].type === 'token_property') {
propertyObject[line[tokenIndex].token] = line[tokenIndex + 1].token;
tokenIndex++;
}
}
treeObject.propertyObject = propertyObject;
this.treeObjectArray.push(treeObject);
})
}
_checkGrammar(currentToken, lineNumber, currentLineTokens) {
/** Grammars
* token_tag ex. div section form button
* token_property
* token_value
*/
const tags = this.tags;
let returnType = '';
if (this.rules.typeExpected) {
if (tags.includes(currentToken)) {
returnType = 'token_tag';
} else if (currentToken === this.endToken) {
return 0;
} else {
console.table(currentToken);
throw new Error(`Invalid type, check if applied HTML tag on line ${lineNumber + 1} is valid. To see available tags, console.log(generator.tags)`)
}
}
if (this.rules.valueExpected) {
const lineType = currentLineTokens[0].token;
const testNode = document.createElement(lineType)
for (const elem in testNode) {
if (elem == currentToken)
returnType = 'token_property'
}
if (returnType !== 'token_property')
throw new Error(`Invalid property value ${currentToken} for ${lineType}`)
return 'token_property';
}
if (this.rules.checkNextTypeIsValue) {
if (currentToken[0] === '\'' && currentToken[currentToken.length - 1] === '\'') {
currentToken = currentToken.replace(/'/g, '');
returnType = 'token_value';
}
}
return returnType;
}
_generateTokens(inputArr) {
const arrayOfLines = inputArr.filter(elem => elem.length !== 0);
arrayOfLines.forEach((line, lineNumber) => {
const currentLineTokens = [];
this.rules.typeExpected = true;
let indentation = 0;
for (let i = 0; line[i] == ' '; i++) {
indentation++;
}
//Rule: Value Expected -- If ':' is found, next grammarcheck type needs to be 'value' else throw error
let currentToken = '';
let lastChar = ''
for (let lineIndex = indentation; lineIndex <= line.length; lineIndex++) {
if ((line[lineIndex] !== ' ' && lineIndex !== line.length) && line[lineIndex]) {
if (this.rules.valueExpected && line[lineIndex] !== '\'')
throw new Error('Missing \' after :');
if (line[lineIndex] === ':' && !this.rules.checkNextTypeIsValue) {
this.rules.valueExpected = true;
continue;
}
currentToken += line[lineIndex];
lastChar = line[lineIndex];
} else if (this.rules.checkNextTypeIsValue && lastChar !== '\'') {
currentToken += line[lineIndex];
lastChar = line[lineIndex];
} else {
//Validate token
const type = this._checkGrammar(currentToken, lineNumber, currentLineTokens)
if (this.rules.typeExpected && type !== 'token_tag' && currentToken !== this.endToken)
throw new Error("type expected as first token! ex. div");
this.rules.typeExpected = false;
if (this.rules.checkNextTypeIsValue && type !== 'token_value')
throw new Error("'value' expected after after token_property")
this.rules.checkNextTypeIsValue = false;
if (this.rules.valueExpected) {
this.rules.checkNextTypeIsValue = true;
this.rules.valueExpected = false;
}
if (type === 'token_value')
currentToken = currentToken.replace(/'/g, '');
currentLineTokens.push({ token: currentToken, type });
currentToken = '';
}
}
currentLineTokens.unshift({ indentation })
this.tokens.push(currentLineTokens);
})
}
/**
* 'createTree' is the method you can use to generate HTML stored in a Component as Elements.
* The string has to be in a certain format where indentations are very important.
* Indentations are what dictates if an element is a parent/child. Always use even indentations, 2 spaces per child.
* The structure is always the same: `[indentation][htmlTag][property]: '[value]'`.
* Note that you need to to use only one grandparent for all the element generated, else it will throw an error!.
* like:
* @example
* ` h1 innerText: 'helo'`
* Valid properties are for specific a htmlTag. For example, you can use 'type' on 'input' but not on 'h1'
* @example
* ` form
* input type: 'text'
* end`
*
* Indentation dictates children/parent
* @example
* `
* div className: 'grandparent'
* main className: 'parent'
* p className: 'child' innerText: 'I am a child of main'
* end`
*
* Always end the string on a new line with the word 'end'
*
* Full example
* @example
* import { Generator } from 'nova';
* const generator = new Generator();
* const header = generator.createTree(`
* header className: 'header'
* h1 className: 'header__title' innerText: 'Hello World!'
* h2 className: 'header__subtitle' innerText: 'This is my site.'
* nav id: 'menu'
* ul className: 'menu__items'
* li innerText: 'First item'
* li innerText: 'Second item'
* end`)
* @param {string} input
* @returns {Component}
*/
createTree(input, indentationRule = 2) {
this.indentationRule = indentationRule;
const splitByNewLines = input.split('\n');
this._generateTokens(splitByNewLines);
this._createTreeObjectFromTokens();
this._generateElementsFromTree();
this.elementsArray.pop();
const elementsArray = [...this.elementsArray]
this._defaultGenerator()
return new Component(elementsArray)
}
_defaultGenerator() {
this.tokens = [];
this.treeObjectArray = [];
this.indentationRule = 2
this.endToken = 'end'
this.rules = {
typeExpected: true,
valueExpected: false,
checkNextTypeIsValue: false
}
this.elementsArray = [];
}
}