Explorar el Código

[add] removed MsgTree object
[add] highlights managment
[add] some options to generate emoji HTML
[add] option to filter text;

B Thibault hace 8 años
padre
commit
483cafb664
Se han modificado 1 ficheros con 193 adiciones y 110 borrados
  1. 193 110
      msgFormatter.js

+ 193 - 110
msgFormatter.js

@@ -1,17 +1,21 @@
-// 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);
-    }
+    /** @type {string} */
+    var text;
+
+    /** @type {MsgBranch} */
+    var root;
+
+    var opts = {
+        /** @type {Array<string>} */
+        highlights: [],
+
+        /** @type {function(string):string} */
+        emojiFormatFunction: identity,
+
+        /** @type {function(string):string} */
+        textFilter: identity
+    };
 
     /**
      * @constructor
@@ -27,7 +31,7 @@ var formatText = (function() {
 
     /**
      * @constructor
-     * @param {MsgBranch|MsgTree} _parent
+     * @param {MsgBranch} _parent
      * @param {number} triggerIndex
      * @param {string=} trigger
      */
@@ -71,14 +75,23 @@ var formatText = (function() {
         /** @type {boolean} */
         this.isEol = this.trigger === '\n';
 
-        /** @const @type {MsgBranch|MsgTree} */
+        /** @type {boolean} */
+        this.isHighlight = trigger !== undefined && opts.highlights.indexOf(trigger) !== -1;
+
+        /** @const @type {MsgBranch} */
         this._parent = _parent;
 
         /** @type {MsgBranch|null} */
         this.prevTwin = null;
 
         /** @type {boolean|number} */
-        this.terminated = this.isEol ? triggerIndex : false;
+        this.terminated = this.isEol || this.isHighlight ? (triggerIndex +trigger.length -1) : false;
+
+        if (this.isHighlight) {
+            this.lastNode = new MsgTextLeaf(this);
+            this.subNodes.push(this.lastNode);
+            this.lastNode.text = /** @type {string} */ (trigger);
+        }
     }
 
     /** @return {boolean} */
@@ -106,6 +119,11 @@ var formatText = (function() {
         return (this.isEmoji && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsEmoji());
     };
 
+    /** @return {boolean} */
+    MsgBranch.prototype.checkIsHighlight = function() {
+        return (this.isHighlight && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsHighlight());
+    };
+
     /** @return {boolean} */
     MsgBranch.prototype.checkIsCode = function() {
         return (this.isCode && !!this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsCode());
@@ -138,22 +156,26 @@ var formatText = (function() {
      *   | "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] === '>')
+    MsgBranch.prototype.finishWith = function(i) {
+        var prevIsAlphadec = isAlphadec(text[i -1]);
+        if (this.trigger === '<' && text[i] === '>' && prevIsAlphadec)
             return true;
 
-        if (str.substr(i, this.trigger.length) === this.trigger) {
+        if (!this.isQuote && text.substr(i, this.trigger.length) === this.trigger) {
+            if (!prevIsAlphadec && (this.isBold || this.isItalic || this.isStrike)) {
+                // previous char is not compatible with finishing now
+                return false;
+            }
             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
         }
-        if (str[i] === '\n' && this.isEol)
+        if (text[i] === '\n' && this.isQuote)
             return true;
         return false;
     };
@@ -188,15 +210,13 @@ var formatText = (function() {
 
     /**
      * 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'))
+    MsgBranch.prototype.isAcceptable = function(i) {
+        if (this.isEmoji && (text[i] === ' ' || text[i] === '\t'))
             return false;
-        if ((this.isEmoji || this.isLink || this.isBold || this.isItalic || this.isStrike || this.isCode) &&
-            str[i] === '\n')
+        if ((this.isEmoji || this.isLink || this.isBold || this.isItalic || this.isStrike || this.isCode) && text[i] === '\n')
             return false;
         return true;
     };
@@ -204,60 +224,102 @@ var formatText = (function() {
     /**
      * 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) {
+    MsgBranch.prototype.isNewToken = function(i) {
         if (this.isCode || this.isEmoji || this.isCodeBlock)
             return null;
-        if (!this.lastNode || this.lastNode.terminated || this.lastNode instanceof MsgTextLeaf) {
-            var nextIsAlphadec = isAlphadec(str[i +1])
-                ,prevIsAlphadec = isAlphadec(str[i -1]);
+        var nextIsAlphadec = isAlphadec(text[i +1]);
 
-            if (str.substr(i, 3) === '```')
-                return '```';
-            if (str.substr(i, 4) === "&gt;") // FIXME AND begining of line OR begining of QUOTED
+        if (text.substr(i, 3) === '```')
+            return '```';
+        if (isBeginingOfLine()) {
+            if (text.substr(i, 4) === "&gt;")
                 return "&gt;";
-            if (str[i] === '>') // FIXME AND begining of line OR begining of QUOTED
-                return str[i];
-            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];
+            if (text[i] === '>')
+                return text[i];
+        }
+        if (['`', '\n'].indexOf(text[i]) !== -1)
+            return text[i];
+        if (['*', '~', '-', '_' ].indexOf(text[i]) !== -1 && (nextIsAlphadec || ['*', '~', '-', '_', '<', '&'].indexOf(text[i+1]) !== -1))
+            return text[i];
+        if ([':', '<'].indexOf(text[i]) !== -1 && nextIsAlphadec)
+            return text[i];
+
+        for (var highlightIndex =0, nbHighlight =opts.highlights.length; highlightIndex < nbHighlight; highlightIndex++) {
+            var highlight = opts.highlights[highlightIndex];
+
+            // TODO compare with collators
+            if (text.substr(i, highlight.length) === highlight)
+                return highlight;
         }
         return null;
     };
 
+    /** @return {boolean|undefined} */
+    MsgTextLeaf.prototype.isBeginingOfLine = function() {
+        if (this.text.trim() !== '')
+            return false;
+        return undefined;
+    };
+
+    /** @return {boolean|undefined} */
+    MsgBranch.prototype.isBeginingOfLine = function() {
+        for (var i = this.subNodes.length -1; i >= 0; i--) {
+            var isNewLine = this.subNodes[i].isBeginingOfLine();
+            if (isNewLine !== undefined)
+                return isNewLine;
+        }
+        if (this.isEol || this.isQuote)
+            return true;
+        return undefined;
+    };
+
+    /**
+     * @param {string} c
+     * @return {boolean}
+     **/
+    function isAlphadec(c) {
+        return ((c >= 'A' && c <= 'Z') ||
+            (c >= 'a' && c <= 'z') ||
+            (c >= '0' && c <= '9') ||
+            "àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇߨøÅ寿œ".indexOf(c) !== -1);
+    }
+
+    /** @return {boolean} */
+    function isBeginingOfLine() {
+        var isNewLine = root.isBeginingOfLine();
+        return isNewLine === undefined ? true : isNewLine;
+    };
+
     /** @return {number} */
-    MsgTextLeaf.prototype.addChar = function(str, i) {
-        this.text += str[i];
+    MsgTextLeaf.prototype.addChar = function(i) {
+        this.text += text[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;
+    MsgBranch.prototype.addChar = function(i) {
+        var isFinished = (this.lastNode && !this.lastNode.terminated && this.lastNode.finishWith) ? this.lastNode.finishWith(i) : null;
 
         if (isFinished) {
             var lastTriggerLen = this.lastNode.trigger.length;
-            this.lastNode.terminate(this.lastNode.triggerIndex, i);
+            this.lastNode.terminate(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 (!this.lastNode || this.lastNode.terminated || this.lastNode instanceof MsgTextLeaf || this.lastNode.isAcceptable(i)) {
+                var isNewToken = this.isNewToken(i);
                 if (isNewToken) {
+                    // FIXME if this is a quote and newToken is also a quote, trim lastnode (and remove it if necessary)
                     this.lastNode = new MsgBranch(this, i, isNewToken);
                     this.subNodes.push(this.lastNode);
                     return this.lastNode.trigger.length;
@@ -266,17 +328,17 @@ var formatText = (function() {
                         this.lastNode = new MsgTextLeaf(this);
                         this.subNodes.push(this.lastNode);
                     }
-                    return this.lastNode.addChar(str, i);
+                    return this.lastNode.addChar(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);
+                root.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.lastNode.addChar(revertTo -1); // Add "escaped" trigger
                 this.subNodes.pop();
                 this.subNodes.push(this.lastNode);
 
@@ -286,7 +348,7 @@ var formatText = (function() {
         }
     };
 
-    MsgBranch.prototype.terminate = function(triggerIndex, terminateAtIndex) {
+    MsgBranch.prototype.terminate = function(terminateAtIndex) {
         var _this = this;
         while (_this) {
             _this.terminated = terminateAtIndex;
@@ -294,13 +356,6 @@ var formatText = (function() {
         }
     };
 
-    MsgBranch.prototype.getRoot = function() {
-        var branch = this;
-        while (branch._parent && branch._parent instanceof MsgBranch)
-            branch = branch._parent;
-        return branch;
-    };
-
     /**
      * @param {number} unTerminatedIndex
     **/
@@ -318,7 +373,17 @@ var formatText = (function() {
      * @return {string}
     **/
     MsgTextLeaf.prototype.innerHTML = function() {
-        return this._parent.checkIsEmoji() ? (':' +this.text +':') : (this.text);
+        if (this._parent.checkIsEmoji()) {
+            var _parent = this._parent;
+            while (_parent && !_parent.isEmoji)
+                _parent = _parent._parent;
+            if (_parent)
+                return opts.emojiFormatFunction(_parent.trigger +this.text +_parent.trigger);
+            return opts.emojiFormatFunction(this.text);
+        }
+        if (this._parent.checkIsCodeBlock())
+            return this.text.replace(/\n/g, '<br/>'); // TODO syntax highlight
+        return opts.textFilter(this.text);
     };
 
     /**
@@ -329,8 +394,6 @@ var formatText = (function() {
             ,classList = [];
 
         if (this._parent.checkIsCodeBlock()) {
-            // TODO syntax highlight
-            // TODO line breaks
             classList.push('codeblock');
         } else if (this._parent.checkIsCode()) {
             classList.push('code');
@@ -344,7 +407,9 @@ var formatText = (function() {
             if (this._parent.checkIsStrike())
                 classList.push('strike');
             if (this._parent.checkIsEmoji())
-                classList.push('emoji'); // FIXME emoji
+                classList.push('emoji');
+            if (this._parent.checkIsHighlight())
+                classList.push('highlight');
         }
         return '<' +tagName +(classList.length ? ' class="' +classList.join(' ') +'"' : '') +'>' +this.innerHTML() +'</' +tagName +'>';
     };
@@ -366,39 +431,17 @@ var formatText = (function() {
         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];
+    /** @param {MsgBranch=} _root */
+    function getFirstUnterminated(_root) {
+        _root = _root || 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);
+                    var unTerminatedChild = getFirstUnterminated(branch);
                     if (unTerminatedChild)
                         return unTerminatedChild;
                 }
@@ -411,51 +454,91 @@ var formatText = (function() {
      * @param {MsgBranch} branch
      * @param {boolean} next kill this branch or all the next ones ?
     **/
-    MsgTree.prototype.revertTree = function(branch, next) {
+    function revertTree(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);
+            revertTree(branch._parent, true);
+        }
+    };
+
+    MsgBranch.prototype.implicitClose = function(i) {
+        if (this.isQuote && !this.terminated) {
+            this.terminate(i);
         }
+        this.subNodes.forEach(function(node) {
+            if (node instanceof MsgBranch)
+                node.implicitClose(i);
+        });
     };
 
-    MsgTree.prototype.eof = function() {
-        // FIXME merge of same block-branches (opti + quoted)
-        var unterminated = this.getFirstUnterminated(this.root);
+    /**
+     * Try to close the tree.
+     * If a problem is found, return its index after reverting tree at this position
+     * @return {number|undefined}
+    **/
+    function eof() {
+        root.implicitClose(text.length);
+        var unterminated = getFirstUnterminated();
 
         if (unterminated) {
             // We have a first token, but never closed
             // kill that branch
-            this.revertTree(unterminated, false);
+            revertTree(unterminated, false);
 
             // "unterminate" all branch that will be closed in the future
-            this.root.cancelTerminate(unterminated.triggerIndex);
+            root.cancelTerminate(unterminated.triggerIndex);
 
             // Add a new text leaf containing trigger
             var textNode = new MsgTextLeaf(unterminated._parent);
-            textNode.addChar(this.text, unterminated.triggerIndex);
+            textNode.addChar(unterminated.triggerIndex);
             unterminated._parent.subNodes.push(textNode);
             unterminated._parent.lastNode = textNode;
 
             // Restart parsing
-            this.parseFrom(unterminated.triggerIndex +1);
+            return unterminated.triggerIndex +1;
+        } else {
+            // FIXME merge of same block-branches (opti)
         }
-        // Else no problem
     };
 
+    function identity(a) { return a; }
+
     /**
+     * @param {string} _text
+     * @param {({
+         * highlights: (Array<string>|undefined),
+         * emojiFormatFunction: (function(string):string|undefined),
+         * textFilter: (function(string):string|undefined)
+         * })=} _opts
      * @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());
+    function _parse(_text, _opts) {
+        if (!_opts)
+            _opts = {};
+        opts.highlights = _opts.highlights || [];
+        opts.emojiFormatFunction = _opts.emojiFormatFunction || identity;
+        opts.textFilter = _opts.textFilter || identity;
+        text = _text;
+
+        root = new MsgBranch(this, 0);
+        var i =0,
+            textLen = text.length;
+        do {
+            while (i < textLen)
+                i += root.addChar(i);
+            i = eof();
+        } while (i !== undefined);
+
+        return root.outerHTML();
     };
+    return _parse;
 })();
 
+window['_formatText'] = function(str, opts) {
+    return formatText(str, {
+        highlights: opts ? opts["highlights"] : undefined
+    });
+}
 if (typeof module !== "undefined") module.exports.formatText = formatText;