B Thibault 8 år sedan
förälder
incheckning
f6e36df1b7
1 ändrade filer med 448 tillägg och 0 borttagningar
  1. 448 0
      msgFormatter.js

+ 448 - 0
msgFormatter.js

@@ -0,0 +1,448 @@
+// FIXME eol and quote ( > ) with quote level
+
+/** @type {function(string):string} */
+var formatText = (function() {
+    /**
+     * @param {string} c
+     * @return {boolean}
+     **/
+    function isAlphadec(c) {
+        return ((c >= 'A' && c <= 'Z') ||
+            (c >= 'a' && c <= 'z') ||
+            (c >= '0' && c <= '9') ||
+            "àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇߨøÅ寿œ".indexOf(c) !== -1);
+    }
+
+    /**
+     * @constructor
+     * @param {MsgBranch!} _parent
+    **/
+    function MsgTextLeaf(_parent) {
+        /** @type {string} */
+        this.text = "";
+
+        /** @const @type {MsgBranch} */
+        this._parent = _parent;
+    }
+
+    /**
+     * @constructor
+     * @param {MsgBranch|MsgTree} _parent
+     * @param {number} triggerIndex
+     * @param {string=} trigger
+     */
+    function MsgBranch(_parent, triggerIndex, trigger) {
+        /** @const @type {number} */
+        this.triggerIndex = triggerIndex;
+
+        /** @type {MsgBranch|MsgTextLeaf|null} */
+        this.lastNode = null;
+
+        /** @type {Array<MsgBranch|MsgTextLeaf>} */
+        this.subNodes = [ ];
+
+        /** @const @type {string} */
+        this.trigger = trigger || '';
+
+        /** @type {boolean} */
+        this.isLink = this.trigger === '<';
+
+        /** @type {boolean} */
+        this.isBold = this.trigger === '*';
+
+        /** @type {boolean} */
+        this.isItalic = this.trigger === '_';
+
+        /** @type {boolean} */
+        this.isStrike = this.trigger === '~' || this.trigger === '-';
+
+        /** @type {boolean} */
+        this.isQuote = this.trigger === '>';
+
+        /** @type {boolean} */
+        this.isEmoji = this.trigger === ':';
+
+        /** @type {boolean} */
+        this.isCode = this.trigger === '`';
+
+        /** @type {boolean} */
+        this.isCodeBlock = this.trigger === '```';
+
+        /** @type {boolean} */
+        this.isEol = this.trigger === '\n';
+
+        /** @const @type {MsgBranch|MsgTree} */
+        this._parent = _parent;
+
+        /** @type {MsgBranch|null} */
+        this.prevTwin = null;
+
+        /** @type {boolean|number} */
+        this.terminated = false;
+    }
+
+    /** @return {boolean} */
+    MsgBranch.prototype.checkIsBold = function() {
+        return (this.isBold && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsBold());
+    };
+
+    /** @return {boolean} */
+    MsgBranch.prototype.checkIsItalic = function() {
+        return (this.isItalic && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsItalic());
+    };
+
+    /** @return {boolean} */
+    MsgBranch.prototype.checkIsStrike = function() {
+        return (this.isStrike && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsStrike());
+    };
+
+    /** @return {boolean} */
+    MsgBranch.prototype.checkIsQuote = function() {
+        return (this.isQuote && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsQuote());
+    };
+
+    /** @return {boolean} */
+    MsgBranch.prototype.checkIsEmoji = function() {
+        return (this.isEmoji && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsEmoji());
+    };
+
+    /** @return {boolean} */
+    MsgBranch.prototype.checkIsCode = function() {
+        return (this.isCode && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsCode());
+    };
+
+    /** @return {boolean} */
+    MsgBranch.prototype.checkIsCodeBlock = function() {
+        return (this.isCodeBlock && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsCodeBlock());
+    };
+
+    MsgBranch.prototype.containsUnterminatedBranch = function() {
+        for (var i =0, nbBranches = this.subNodes.length; i < nbBranches; i++) {
+            if (this.subNodes[i] instanceof MsgBranch && (
+                    !this.subNodes[i].terminated || this.subNodes[i].containsUnterminatedBranch()))
+                return true;
+        }
+        return false;
+    };
+
+    /**
+     * Check if this token closes the branch
+     * In this case, finishWith should return the new leave
+     * Exemple: `_text *bold_ bold continue*` should be
+     * + root
+     * | italic
+     *   | "text"
+     *   | bold
+     *     | "bold"
+     * | bold
+     *   | "bold continue"
+     * and this function will return a "bold" branch
+     *
+     * @param {string} str
+     * @param {number} i
+     * @return {boolean|MsgBranch}
+    **/
+    MsgBranch.prototype.finishWith = function(str, i) {
+        if (this.trigger === '<' && str[i] === '>')
+            return true;
+
+        if (str.substr(i, this.trigger.length) === this.trigger) {
+            if (this.lastNode && this.containsUnterminatedBranch()) // sub nodes contains at least one branch wich should be replicated
+                return this.lastNode.makeNewBranchFromThis();
+            else if (this.hasContent()) // self or any prevTwin has Content
+                return true;
+            // else no content, should not be finished now
+        }
+        return false;
+    };
+
+    /**
+     * @return {boolean}
+    **/
+    MsgBranch.prototype.hasContent = function() {
+        var _this = this;
+        while (_this) {
+            for (var i = 0, nbNodes = _this.subNodes.length; i < nbNodes; i++)
+                if (_this.subNodes[i] instanceof MsgBranch || _this.subNodes[i].text.length)
+                    return true;
+            // self or any prevTwin has Content
+            _this = _this.prevTwin;
+        }
+        return false;
+    };
+
+    /**
+     * @return {MsgBranch}
+    **/
+    MsgBranch.prototype.makeNewBranchFromThis = function() {
+        var other = new MsgBranch(this._parent, this.triggerIndex, this.trigger);
+        other.prevTwin = this;
+        if (this.lastNode && this.lastNode instanceof MsgBranch) {
+            other.lastNode = this.lastNode.makeNewBranchFromThis();
+            other.subNodes = [ other.lastNode ];
+        }
+        return other;
+    };
+
+    /**
+     * Check if this token is compatible with this branch
+     * @param {string} str
+     * @param {number} i
+     * @return {boolean}
+    **/
+    MsgBranch.prototype.isAcceptable = function(str, i) {
+        if (this.isEmoji && (str[i] === ' ' || str[i] === '\t'))
+            return false;
+        if ((this.isEmoji || this.isLink || this.isBold || this.isItalic || this.isStrike || this.isCode) &&
+            str[i] === '\n')
+            return false;
+        return true;
+    };
+
+    /**
+     * Check if str[i] is a trigger for a new node
+     * if true, return the trigger
+     * @param {string} str
+     * @param {number} i
+     * @return {string|null}
+    **/
+    MsgBranch.prototype.isNewToken = function(str, i) {
+        if (this.isCode || this.isCodeBlock || this.isEmoji)
+            return null;
+        if (!this.lastNode || this.lastNode instanceof MsgTextLeaf) {
+            var nextIsAlphadec = isAlphadec(str[i +1]);
+
+            if (str.substr(i, 3) === '```')
+                return '```';
+            if (['`', '\n'].indexOf(str[i]) !== -1)
+                return str[i];
+            if (['*', '~', '-', '_' ].indexOf(str[i]) !== -1 && (nextIsAlphadec || ['*', '`', '~', '-', '_', ':', '<'].indexOf(str[i+1]) !== -1))
+                return str[i];
+            if ([':', '<'].indexOf(str[i]) !== -1 && nextIsAlphadec)
+                return str[i];
+        }
+        return null;
+    };
+
+    /** @return {number} */
+    MsgTextLeaf.prototype.addChar = function(str, i) {
+        this.text += str[i];
+        return 1;
+    };
+
+    /**
+     * Parse next char
+     * @param {string} str
+     * @param {number} i
+     * @return {number}
+    **/
+    MsgBranch.prototype.addChar = function(str, i) {
+        var isFinished = (this.lastNode && this.lastNode.finishWith) ? this.lastNode.finishWith(str, i) : null;
+
+        if (isFinished) {
+            var lastTriggerLen = this.lastNode.trigger.length;
+            this.lastNode.terminate(this.lastNode.triggerIndex, i);
+            if (isFinished instanceof MsgBranch) {
+                this.lastNode = isFinished;
+                this.subNodes.push(isFinished);
+            }
+            return lastTriggerLen;
+        } else {
+            if (!this.lastNode || this.lastNode.terminated || this.lastNode instanceof MsgTextLeaf || this.lastNode.isAcceptable(str, i)) {
+                var isNewToken = this.isNewToken(str, i);
+                if (isNewToken) {
+                    this.lastNode = new MsgBranch(this, i, isNewToken);
+                    this.subNodes.push(this.lastNode);
+                    return this.lastNode.trigger.length;
+                } else {
+                    if (!this.lastNode || this.lastNode.terminated) {
+                        this.lastNode = new MsgTextLeaf(this);
+                        this.subNodes.push(this.lastNode);
+                    }
+                    return this.lastNode.addChar(str, i);
+                }
+            } else {
+                // last branch child is not compatible with this token.
+                // So, lastBranch is not a branch
+                var revertTo = this.lastNode.triggerIndex +1;
+
+                this.getRoot().cancelTerminate(this.lastNode.triggerIndex);
+                // Add a new "escaped" node to replace trigger
+                this.lastNode = new MsgTextLeaf(this);
+                this.lastNode.addChar(str, revertTo -1); // Add "escaped" trigger
+                this.subNodes.pop();
+                this.subNodes.push(this.lastNode);
+
+                // return negative value to start parsing again
+                return revertTo -i;
+            }
+        }
+    };
+
+    MsgBranch.prototype.terminate = function(triggerIndex, terminateAtIndex) {
+        var _this = this;
+        while (_this) {
+            _this.terminated = terminateAtIndex;
+            _this = _this.prevTwin;
+        }
+    };
+
+    MsgBranch.prototype.getRoot = function() {
+        var branch = this;
+        while (branch._parent && branch._parent instanceof MsgBranch)
+            branch = branch._parent;
+        return branch;
+    };
+
+    /**
+     * @param {number} unTerminatedIndex
+    **/
+    MsgBranch.prototype.cancelTerminate = function(unTerminatedIndex) {
+        if (this.terminated && this.terminated >= unTerminatedIndex)
+            this.terminated = false;
+        this.subNodes.forEach(function(node) {
+            if (node instanceof MsgBranch) {
+                node.cancelTerminate(unTerminatedIndex);
+            }
+        });
+    };
+
+    /**
+     * @return {string}
+    **/
+    MsgTextLeaf.prototype.innerHTML = function() {
+        return this._parent.checkIsEmoji() ? (':' +this.text +':') : (this.text);
+    };
+
+    /**
+     * @return {string}
+    **/
+    MsgTextLeaf.prototype.outerHTML = function() {
+        var tagName = 'span'
+            ,classList = [];
+
+        if (this._parent.checkIsCodeBlock()) {
+            // TODO syntax highlight
+            classList.push('codeblock');
+        } else if (this._parent.checkIsCode()) {
+            classList.push('code');
+        } else {
+            if (this.isLink)
+                tagName = 'a';
+            if (this._parent.checkIsBold())
+                classList.push('bold');
+            if (this._parent.checkIsItalic())
+                classList.push('italic');
+            if (this._parent.checkIsStrike())
+                classList.push('strike');
+            if (this._parent.checkIsEmoji())
+                classList.push('emoji'); // FIXME emoji
+        }
+        return '<' +tagName +(classList.length ? ' class="' +classList.join(' ') +'"' : '') +'>' +this.innerHTML() +'</' +tagName +'>';
+    };
+
+    MsgBranch.prototype.outerHTML = function() {
+        var html = "";
+
+        if (this.isQuote) {
+            html += '<span class="quote">';
+        }
+        this.subNodes.forEach(function(node) {
+            html += node.outerHTML();
+        });
+        if (this.isQuote) {
+            html += '</span>';
+        }
+        return html;
+    };
+
+    /**
+     * @constructor
+     * @param {string} text
+     */
+    function MsgTree(text) {
+        /** @const @type {string} */
+        this.text = text;
+
+        /** @type {MsgBranch|null} */
+        this.root = null;
+    }
+
+    MsgTree.prototype.parseFrom = function(i) {
+        for (var textLen = this.text.length; i < textLen;)
+            i += this.root.addChar(this.text, i);
+        this.eof();
+    };
+
+    MsgTree.prototype.parse = function() {
+        this.root = new MsgBranch(this, 0);
+        this.parseFrom(0);
+    };
+
+    /** @param {MsgBranch} root */
+    MsgTree.prototype.getFirstUnterminated = function(root) {
+        for (var i =0, nbBranches = root.subNodes.length; i < nbBranches; i++) {
+            var branch = root.subNodes[i];
+
+            if (branch instanceof MsgBranch) {
+                if (!branch.terminated) {
+                    return branch;
+                } else {
+                    var unTerminatedChild = this.getFirstUnterminated(branch);
+                    if (unTerminatedChild)
+                        return unTerminatedChild;
+                }
+            }
+        }
+        return null;
+    };
+
+    /**
+     * @param {MsgBranch} branch
+     * @param {boolean} next kill this branch or all the next ones ?
+    **/
+    MsgTree.prototype.revertTree = function(branch, next) {
+        if (branch._parent instanceof MsgBranch) {
+            branch._parent.subNodes.splice(branch._parent.subNodes.indexOf(branch) +(next ? 1 : 0));
+            branch._parent.lastNode = branch._parent.subNodes[branch._parent.subNodes.length -1];
+            this.revertTree(branch._parent, true);
+        }
+    };
+
+    MsgTree.prototype.eof = function() {
+        var unterminated = this.getFirstUnterminated(this.root);
+
+        if (unterminated) {
+            // We have a first token, but never closed
+            // kill that branch
+            this.revertTree(unterminated, false);
+
+            // "unterminate" all branch that will be closed in the future
+            this.root.cancelTerminate(unterminated.triggerIndex);
+
+            // Add a new text leaf containing trigger
+            var textNode = new MsgTextLeaf(unterminated._parent);
+            textNode.addChar(this.text, unterminated.triggerIndex);
+            unterminated._parent.subNodes.push(textNode);
+            unterminated._parent.lastNode = textNode;
+
+            // Restart parsing
+            this.parseFrom(unterminated.triggerIndex +1);
+        }
+        // Else no problem
+    };
+
+    /**
+     * @return {string}
+    **/
+    MsgTree.prototype.toHTML = function() {
+        return this.root ? this.root.outerHTML() : "";
+    };
+
+    return function(text) {
+        var root = new MsgTree(text);
+        root.parse();
+        return (root.toHTML());
+    };
+})();
+