FTML to HTML
<template>
  <div>
    <h1>FTML (type: page) to HTML</h1>
    <a
      href="https://github.com/scpwiki/wikijump/blob/develop/ftml/docs/Blocks.md"
      target="_blank"
    >
      @wikijump/ftml/docs/Blocks.md
    </a>
    <hr />
    <h3>Wiki Syntax</h3>
    <textarea ref="ftmlarea" v-model="ftml" @input="postMessage($event.target.value)"></textarea>
    <hr />
    <h3>Result</h3>
    <pre ref="result"></pre>
  </div>
</template>
 
<script>
const CN_ParentElementNode = class {
    constructor() {
        this.name = '_parent';
        this.init();
    }
    init() {
        this._content = document.createElement('div');
        this._content.classList.add('check_node_block');
    }
    get object() {
        return this._content;
    }
    append(cnnode) {
        this._content.appendChild(cnnode.object);
    }
};
 
const CN_ElementNode = class {
    constructor(name, ...attr) {
        this.name = name;
        this.attr = attr.map(a => 
            `<span class="check_node_attribute"><b>${a.nodeName}</b>=<i>"${a.nodeValue}"</i></span>`
        );
        this.init();
    }
    init() {
        this._wrap = document.createElement('div');
        this._label_open = document.createElement('a');
        this._label_open.classList.add('check_node_element_open');
        this._label_open.innerHTML  = this.name == 'wj-body'
          ? ''
          : `&lt;${['<span class="check_node_tagname">' + this.name + '</span>', ...this.attr].join(' ')}&gt;`;
        this._dummy = document.createElement('div');
        this._content = document.createElement('label');
        this.name == 'wj-body' || this._content.classList.add('check_node_block');
        const check = document.createElement('input');
        check.type = 'checkbox';
        this.name == 'wj-body' || check.classList.add('check_node_hidden');
        this._content.appendChild(check);
        this._label_close = document.createElement('a');
        this._label_close.classList.add('check_node_element_close');
        this._label_close.innerHTML  = this.name == 'wj-body'
          ? ''
          : `&lt;/<span class="check_node_tagname">${this.name}</span>&gt`;
        for(const e of [this._label_open, this._dummy, this._content, this._label_close]) this._wrap.appendChild(e);
    }
    get object() {
        return this._wrap;
    }
    append(cnnode) {
        this._content.appendChild(cnnode.object);
    }
};
 
//  <br>, <hr>, <img>, <input>, <link>, <base>, <meta>, <param>, <area>, <embed>, <col>, <track>, <source> 
const CN_VoidElementNode = class {
    constructor(name, ...attr) {
        this.name = name;
        this.attr = attr.map(a => 
            `<span class="check_node_attribute"><b>${a.nodeName}</b>=<i>"${a.nodeValue}"</i></span>`
        );
        this.init();
    }
    init() {
        this._span = document.createElement('a');
        this._span.classList.add('check_node_element_void', 'check_node_inline');
        this._span.innerHTML= `&lt;${['<span class="check_node_tagname">' + this.name + '</span>', ...this.attr, '/'].join(' ')}&gt;`;
    }
    get object() {
        return this._span;
    }
};
 
const CN_TextNode = class {
    constructor(name, value) {
        this.name = name;
        this.value = value;
        this.init();
    }
    init() {
        this._span = document.createElement('span');
        this._span.classList.add('check_node_inline');
        this._span.innerText = this.value;
        this._span.dataset.type = this.name;
    }
    get object() {
        return this._span;
    }
};
 
const CheckNode = (node, cnparent) => {
    const type = node.nodeName.toLowerCase();
    const isVoid = ['br', 'hr', 'img', 'input', 'link', 'base', 'meta', 'param', 'area', 'embed', 'col', 'track', 'source']
        .includes(type);
 
    if(type == '#text') {
        const cnnode = new CN_TextNode(type, node.nodeValue);
        cnparent.append(cnnode);
    }
    else if(type == '#comment') {
        const cnnode = new CN_TextNode(type, `<!-- ${node.nodeValue} -->`);
        cnparent.append(cnnode);
    }
    else if(isVoid) {
        const cnnode = new CN_VoidElementNode(type, ...node.attributes);
        cnparent.append(cnnode);
    }
    else {
        const cnnode = new CN_ElementNode(type, ...node.attributes);
        cnparent.append(cnnode);
        for(const child of node.childNodes) CheckNode(child, cnnode);
    }
    return cnparent.object;
};
 
module.exports = {
  data: function() {
    return {
      worker: null,
      ftml: '',
      result: ''
    }
  },
  methods: {
    createWorker: async function() {
      this.$refs.ftmlarea.disabled = true;
      if(this.worker) this.worker.terminate();
      const raw = await fetch('/local--files/7happy7-warehouse/ftml-base64.txt');
      const base64 = await raw.text();
      this.worker = new Worker(base64, {type: 'module'});
      this.worker.onmessage = e => {
        const { html, styles } = e.data;
        const dom = this.beautifyHTML(html, styles);
        if(dom) {
          this.$refs.result.innerHTML = '';
          this.$refs.result.appendChild(dom);
        }
      };
      this.$refs.ftmlarea.disabled = false;
    },
    postMessage: function(value) {
      this.worker.postMessage({ type: 'page', value });
    },
    beautifyHTML: function(html, styles) {
      try {
        const dom = new DOMParser().parseFromString(html, 'text/html');
        const body = dom.querySelector('wj-body');
        for(const s of styles) {
          const e = dom.createElement('style');
          e.innerHTML = s;
          body.appendChild(e);
        }
        return CheckNode(body, new CN_ParentElementNode());
      }catch(e) {
        console.log(e);
        return html;
      }
    }/*,
    format: function(node, level) {
      const bef = '  '.repeat(level++ + 1);
      const aft  = '  '.repeat(level - 1);
      let textNode;
 
      for(const child of node.children) {
        textNode = document.createTextNode(`\n${bef}`);
        node.insertBefore(textNode, child);
        this.format(child, level);
        if(node.lastElementChild == child) {
          textNode = document.createTextNode(`\n${aft}`);
          node.appendChild(textNode);
        }
      }
      return node;
    }*/
 
  },
  mounted: async function() {
    document.title = '7happy7\'s Warehouse - FTML to HTML';
 
    await this.createWorker();
  }
}
</script>
 
<style scoped>
textarea {
    border: 1px solid rgb(170, 170, 170);
    border-radius: 5px;
    font-family: "Ubuntu Mono", monospace;
    font-size: 20px;
    height: 250px;
    margin: 0 auto;
    position: relative;
    resize: vertical;
    transform: scale(0.65);
    width: 100%;
}
textarea:focus {
    outline: none;
}
pre {
    box-shadow: 1px 1px 3px #aaa;
    font-family: 'Ubuntu Mono', monospace;
    font-size: .8em;
    overflow-x: scroll;
    padding: 1em;
    position: relative;
    text-align: left;
    margin: 0 auto;
    max-width: calc(800px - 2em);
    width: calc(80% - 2em);
}
 
.check_node_block {
    display: block;
    padding: 0.5em 0 0.5em 0.5em;
    transition: all .2s;
}
.check_node_block:hover {
    box-shadow: 1px 1px 3px #aaa;
}
.check_node_block input {
    display: none;
}
.check_node_hidden {
    display: none;
}
[class^="check_node_element"] {
    color: #502;
    font-weight: normal;
}
.check_node_inline {
    display: block;
}
.check_node_inline[data-type="#comment"] {
    color: #589e58;
    font-style: italic;
}
.check_node_tagname {
    color: #568;
    font-weight: bold;
}
.check_node_attribute:hover {
    text-decoration: underline;
}
.check_node_attribute > b {
    color: #586;
}
.check_node_attribute > i {
    color: #c95;
    font-weight: bold;
}
 
.check_node_hidden:checked ~ * {
    display: inline-flex;
    height: 0.8em;
    visibility: hidden;
}
.check_node_hidden:checked + *::before {
    content: '...';
    visibility: visible;
}
.check_node_hidden:checked ~ * .check_node_hidden:checked + *::before {
    visibility: hidden;
}
</style>
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License