Skip to content

Instantly share code, notes, and snippets.

@lifeart
Last active May 11, 2021 21:06
Show Gist options
  • Save lifeart/3ed9885f3cf56b041ea85f84a3c300e4 to your computer and use it in GitHub Desktop.
Save lifeart/3ed9885f3cf56b041ea85f84a3c300e4 to your computer and use it in GitHub Desktop.
simple html parser
[
  [ 'open-element', 'div ' ],
  [ 'open-mustache', 'foo-bar' ],
  [ 'close-mustache', ' ' ],
  [ 'self-close-element', '' ],
  [ 'in-element', '' ],
  [ 'open-element', 'a' ],
  [ 'in-element', '' ],
  [ 'open-mustache', 'boo' ],
  [ 'close-mustache', '' ],
  [ 'close-element', 'a' ],
  [ 'in-element', '' ],
  [ 'open-mustache-block', 'foo' ],
  [ 'open-element', 'b ' ],
  [ 'self-close-element', '' ],
  [ 'in-element', '' ],
  [ 'close-mustache-block', 'foo' ]
]
{
  "type": "Template",
  "content": [
    {
      "type": "element",
      "tagName": "div ",
      "content": [],
      "modifiers": [
        {
          "type": "mustache",
          "tagName": "foo-bar",
          "content": [],
          "fillContent": false,
          "isClosed": false,
          "selfClosed": false
        }
      ],
      "fillContent": false,
      "isClosed": true,
      "selfClosed": true
    },
    {
      "type": "element",
      "tagName": "a",
      "content": [
        {
          "type": "mustache",
          "tagName": "boo",
          "content": [],
          "fillContent": false,
          "isClosed": false,
          "selfClosed": false
        }
      ],
      "modifiers": [],
      "fillContent": true,
      "isClosed": true,
      "selfClosed": false
    },
    {
      "type": "mustache-block",
      "tagName": "foo",
      "content": [
        {
          "type": "element",
          "tagName": "b ",
          "content": [],
          "modifiers": [],
          "fillContent": false,
          "isClosed": true,
          "selfClosed": true
        }
      ],
      "fillContent": true,
      "isClosed": false,
      "selfClosed": false
    }
  ],
  "fillContent": true
}
// '<div /><div>a<my-name /><span><h1>Gekko</h1></span>b</div>';
let input = '<div names={{boo-foo}} age="12" name="{{boo}}12" ...attributes {{foo-bar}} a=2/><a>{{boo}}</a>{{#foo}}<b />{{/foo}}';
function parse(input, isNested = false) {
let stack = [['entry', '']];
let context = 'entry';
let chain = [];
let str = input.split('').reverse();
let stackValue = [];
let createStack = (name) => {
if (name.startsWith('open')) {
chain.push(name.replace('open-',''));
} else if (name.startsWith('close')) {
let found = false;
let sname = name.replace('close-', '');
chain = chain.reverse().filter(e => {
if (found) {
return true;
} else {
if (e === sname) {
found = true;
return false;
} else {
return true;
}
}
}).reverse();
}
let prevStack = context;
context = name;
if (stack.length) {
let vals = stackValue.join('');
if (isNested) {
stack.push([prevStack, vals]);
} else {
let maybeValue = normalize(parse(vals, true));
if (maybeValue.length === 0) {
maybeValue = vals;
}
stack.push([prevStack, prevStack === 'attribute-value' ? maybeValue : vals]);
}
}
//stack.push(['open', context]);
stackValue = [];
}
let addValue = (value) => {
stackValue.push(value);
}
while (str.length) {
let char = str.pop();
let nextChar = str[str.length - 1];
if (context === 'close-mustache' && char === ' ') {
if (chain.includes('element')) {
createStack('element-attribute');
}
}
if (context === 'attribute-value' && char === ' ') {
createStack('element-attribute');
} else if (char === '=' && context === 'element-attribute') {
createStack('attribute-value');
} else if (char === ' ' && context === 'open-element') {
createStack('element-attribute')
} else if (char === '{') {
if (context !== 'open-mustache' && context !== 'attribute-value') {
createStack('open-mustache');
} else {
if (context === 'attribute-value') {
addValue(char);
}
}
} else if (char === '}') {
if (context === 'open-mustache-block' || context === 'close-mustache-block' || context === 'close-mustache' || context === 'attribute-value') {
if (context === 'attribute-value') {
addValue(char);
}
} else {
createStack('close-mustache');
}
} else if (char === '<' && nextChar !== '/') {
createStack('open-element');
} else if (char === '<' && nextChar === '/') {
createStack('close-element');
} else if (char === '/' && nextChar === '>') {
createStack('self-close-element');
} else if (char === '>') {
createStack('in-element');
} else if (context === 'open-mustache' && char === '#') {
context = 'open-mustache-block';
} else if (context === 'open-mustache' && char === '/') {
context = 'close-mustache-block';
} else {
if (char !== '/') {
addValue(char);
}
}
}
createStack('in-element');
return stack;
}
function normalize(stack) {
let newStack = [];
stack.forEach(([name, value]) => {
if (name !== 'entry') {
newStack.push([name, value]);
}
});
return newStack;
}
function domFromStack(stack) {
let cursor = {};
let cursors = [
{
type: 'Template',
content: [],
}
];
let lastCursor = null;
let last = () => {
return cursors[cursors.length - 1];
}
stack.forEach(([kind, value]) => {
if (kind === 'element-attribute') {
last().attributes.push({
type: 'attribute',
name: value,
value: null,
})
} else if (kind === 'attribute-value') {
last().attributes[last().attributes.length-1].value = Array.isArray(value) ? domFromStack(value).content : value;
} else if (kind === 'open-element') {
cursor = {
type: 'element',
value: value,
content: [],
attributes: [],
modifiers: [],
fillContent: false,
isClosed: false,
selfClosed: false,
}
last().content.push(cursor);
cursors.push(cursor);
} else if (kind === 'open-mustache') {
cursor = {
type: 'mustache',
value: value,
content: [],
attributes: [],
fillContent: false,
isClosed: false,
selfClosed: false,
}
if (last().isClosed) {
last().content.push(cursor);
cursors.push(cursor);
} else {
if (last().fillContent) {
last().content.push(cursor);
} else {
if (last().modifiers) {
last().modifiers.push(cursor);
} else {
// element attribute concat case
last().content.push(cursor);
}
}
}
} else if (kind === 'open-mustache-block') {
cursor = {
type: 'mustache-block',
value: value,
content: [],
fillContent: false,
isClosed: false,
selfClosed: false,
}
last().content.push(cursor);
cursors.push(cursor);
} else if (kind === 'close-mustache') {
if (last().isClosed) {
lastCursor = cursors.pop();
}
} else if (kind === 'close-mustache-block') {
lastCursor = cursors.pop();
} else if (kind === 'close-element') {
lastCursor = cursors.pop();
lastCursor.isClosed = true;
} else if (kind === 'in-element') {
last().fillContent = true;
if (value !== '') {
cursors[cursors.length - 1].content.push({
type: 'text',
value: value
});
}
} else if (kind === 'self-close-element') {
lastCursor = cursors.pop();
lastCursor.selfClosed = true;
lastCursor.isClosed = true;
}
});
return cursors.find((e) => e.type === 'Template') || lastCursor;
}
console.log(JSON.stringify(normalize(parse(input)), null, 1));
console.log(JSON.stringify(domFromStack(normalize(parse(input))), null, 2));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment