msgFormatter.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. "use strict";
  2. // FIXME Error with _*a_* and _*a*_
  3. /**
  4. * replace all :emoji: codes with corresponding image
  5. * @param {string} inputString
  6. * @return {string}
  7. **/
  8. function formatEmojis(inputString) {
  9. return inputString.replace(/:([^ \t:]+):/g, function(returnFailed, emoji) {
  10. var emojiDom = makeEmojiDom(emoji);
  11. if (emojiDom) {
  12. var domParent = document.createElement("span");
  13. domParent.className = returnFailed === inputString ? R.klass.emoji.medium : R.klass.emoji.small;
  14. domParent.appendChild(emojiDom);
  15. return domParent.outerHTML;
  16. }
  17. return returnFailed;
  18. });
  19. }
  20. /** @type {function(string):string} */
  21. var formatText = (function() {
  22. /**
  23. * @param {string} c
  24. * @return {boolean}
  25. **/
  26. function isAlphadec(c) {
  27. return ((c >= 'A' && c <= 'Z') ||
  28. (c >= 'a' && c <= 'z') ||
  29. (c >= '0' && c <= '9') ||
  30. "àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇߨøÅ寿œ".indexOf(c) !== -1);
  31. }
  32. /**
  33. * @constructor
  34. * @param {MsgBranch!} _parent
  35. **/
  36. function MsgTextLeaf(_parent) {
  37. /** @type {string} */
  38. this.text = "";
  39. /** @const @type {MsgBranch} */
  40. this._parent = _parent;
  41. }
  42. /**
  43. * @constructor
  44. * @param {MsgBranch|MsgTree} _parent
  45. * @param {number} triggerIndex
  46. * @param {string=} trigger
  47. */
  48. function MsgBranch(_parent, triggerIndex, trigger) {
  49. /** @const @type {number} */
  50. this.triggerIndex = triggerIndex;
  51. /** @type {MsgBranch|MsgTextLeaf} */
  52. this.lastNode = new MsgTextLeaf(this);
  53. /** @type {Array<MsgBranch|MsgTextLeaf>} */
  54. this.subNodes = [ this.lastNode ];
  55. /** @const @type {string} */
  56. this.trigger = trigger || '';
  57. /** @type {boolean} */
  58. this.isLink = this.trigger === '<';
  59. /** @type {boolean} */
  60. this.isBold = this.trigger === '*';
  61. /** @type {boolean} */
  62. this.isItalic = this.trigger === '_';
  63. /** @type {boolean} */
  64. this.isStrike = this.trigger === '~' || this.trigger === '-';
  65. /** @type {boolean} */
  66. this.isQuote = this.trigger === '>';
  67. /** @type {boolean} */
  68. this.isEmoji = this.trigger === ':';
  69. /** @type {boolean} */
  70. this.isCode = this.trigger === '`';
  71. /** @type {boolean} */
  72. this.isCodeBlock = this.trigger === '```';
  73. /** @type {boolean} */
  74. this.isEol = this.trigger === '\n';
  75. /** @const @type {MsgBranch|MsgTree} */
  76. this._parent = _parent;
  77. /** @type {boolean} */
  78. this.terminated = false;
  79. }
  80. /** @return {boolean} */
  81. MsgBranch.prototype.checkIsBold = function() {
  82. return (this.isBold && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsBold());
  83. };
  84. /** @return {boolean} */
  85. MsgBranch.prototype.checkIsItalic = function() {
  86. return (this.isItalic && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsItalic());
  87. };
  88. /** @return {boolean} */
  89. MsgBranch.prototype.checkIsStrike = function() {
  90. return (this.isStrike && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsStrike());
  91. };
  92. /** @return {boolean} */
  93. MsgBranch.prototype.checkIsQuote = function() {
  94. return (this.isQuote && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsQuote());
  95. };
  96. /** @return {boolean} */
  97. MsgBranch.prototype.checkIsEmoji = function() {
  98. return (this.isEmoji && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsEmoji());
  99. };
  100. /** @return {boolean} */
  101. MsgBranch.prototype.checkIsCode = function() {
  102. return (this.isCode && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsCode());
  103. };
  104. /** @return {boolean} */
  105. MsgBranch.prototype.checkIsCodeBlock = function() {
  106. return (this.isCodeBlock && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsCodeBlock());
  107. };
  108. /**
  109. * Check if this token closes the branch
  110. * In this case, finishWith should return the new leave
  111. * Exemple: `_text *bold_ bold continue*` should be
  112. * + root
  113. * | italic
  114. * | "text"
  115. * | bold
  116. * | "bold"
  117. * | bold
  118. * | "bold continue"
  119. * and this function will return a "bold" branch
  120. *
  121. * @param {string} str
  122. * @param {number} i
  123. * @return {MsgTextLeaf|MsgBranch|null}
  124. **/
  125. MsgBranch.prototype.finishWith = function(str, i) {
  126. if (this.trigger === '<' && str[i] === '>')
  127. return new MsgTextLeaf(/** @type {MsgBranch!} */ (this._parent));
  128. if (str.substr(i, this.trigger.length) === this.trigger) {
  129. if (this.lastNode instanceof MsgBranch)
  130. return this.lastNode.makeNewBranchFromThis();
  131. else if (this.lastNode.text !== '')
  132. return new MsgTextLeaf(/** @type {MsgBranch!} */ (this._parent));
  133. }
  134. return null;
  135. };
  136. /**
  137. * @return {MsgBranch}
  138. **/
  139. MsgBranch.prototype.makeNewBranchFromThis = function() {
  140. var other = new MsgBranch(this._parent, this.triggerIndex, this.trigger);
  141. if (this.lastNode instanceof MsgBranch) {
  142. other.lastNode = this.lastNode.makeNewBranchFromThis();
  143. other.subNodes = [ other.lastNode ];
  144. }
  145. return other;
  146. };
  147. /**
  148. * Check if this token is compatible with this branch
  149. * @param {string} str
  150. * @param {number} i
  151. * @return {boolean}
  152. **/
  153. MsgBranch.prototype.isAcceptable = function(str, i) {
  154. if (this.isEmoji && (str[i] === ' ' || str[i] === '\t'))
  155. return false;
  156. if ((this.isEmoji || this.isLink || this.isBold || this.isItalic || this.isStrike || this.isCode) &&
  157. str[i] === '\n')
  158. return false;
  159. return true;
  160. };
  161. /**
  162. * Check if str[i] is a trigger for a new node
  163. * if true, return the trigger
  164. * @param {string} str
  165. * @param {number} i
  166. * @return {string|null}
  167. **/
  168. MsgBranch.prototype.isNewToken = function(str, i) {
  169. if (this.isCode || this.isCodeBlock || this.isEmoji)
  170. return null;
  171. if (this.lastNode instanceof MsgTextLeaf) {
  172. if (str.substr(i, 3) === '```')
  173. return '```';
  174. if (['`', '\n'].indexOf(str[i]) !== -1)
  175. return str[i];
  176. if (['*', '~', '-', '_' ].indexOf(str[i]) !== -1 && (isAlphadec(str[i +1]) || ['*', '`', '~', '-', '_', ':', '<'].indexOf(str[i+1]) !== -1))
  177. return str[i];
  178. if ([':', '<'].indexOf(str[i]) !== -1 && isAlphadec(str[i +1]))
  179. return str[i];
  180. }
  181. return null;
  182. };
  183. /** @return {number} */
  184. MsgTextLeaf.prototype.addChar = function(str, i) {
  185. this.text += str[i];
  186. return 1;
  187. };
  188. /**
  189. * Parse next char
  190. * @param {string} str
  191. * @param {number} i
  192. * @return {number}
  193. **/
  194. MsgBranch.prototype.addChar = function(str, i) {
  195. var isFinished = this.lastNode.finishWith ? this.lastNode.finishWith(str, i) : null;
  196. if (isFinished) {
  197. this.lastNode.terminated = true;
  198. this.lastNode = isFinished;
  199. this.subNodes.push(isFinished);
  200. this.getRoot().terminate(this.triggerIndex);
  201. return 1;
  202. } else {
  203. if (this.lastNode instanceof MsgTextLeaf || this.lastNode.isAcceptable(str, i)) {
  204. var isNewToken = this.isNewToken(str, i);
  205. if (isNewToken) {
  206. this.lastNode = new MsgBranch(this, i, isNewToken);
  207. this.subNodes.push(this.lastNode);
  208. return this.lastNode.trigger.length;
  209. } else {
  210. return this.lastNode.addChar(str, i);
  211. }
  212. } else {
  213. // last branch child is not compatible with this token.
  214. // So, lastBranch is not a branch
  215. // Add a new "escaped" node to replace trigger
  216. var textNode = new MsgTextLeaf(this);
  217. textNode.addChar(str, this.lastNode.triggerIndex);
  218. this.subNodes.pop();
  219. this.subNodes.push(textNode);
  220. // new root
  221. var newBranch = new MsgBranch(this, this.lastNode.triggerIndex +1);
  222. for (var charIndex = this.lastNode.triggerIndex +1; charIndex <= i;)
  223. charIndex += newBranch.addChar(str, charIndex);
  224. this.lastNode = this.subNodes[this.subNodes.length -1];
  225. return 1;
  226. }
  227. }
  228. };
  229. MsgBranch.prototype.terminate = function(triggerIndex) {
  230. if (this.triggerIndex === triggerIndex)
  231. this.terminated = true;
  232. this.subNodes.forEach(function(i) {
  233. if (i instanceof MsgBranch)
  234. i.terminate(triggerIndex);
  235. });
  236. };
  237. MsgBranch.prototype.getRoot = function() {
  238. var branch = this;
  239. while (branch._parent && branch._parent instanceof MsgBranch)
  240. branch = branch._parent;
  241. return branch;
  242. };
  243. /**
  244. * @return {boolean} true if still contains stuff
  245. **/
  246. MsgBranch.prototype.prune = function() {
  247. var branches = [];
  248. this.subNodes.forEach(function(i) {
  249. if (i instanceof MsgTextLeaf) {
  250. if (i.text !== '')
  251. branches.push(i);
  252. } else if (i.prune()) {
  253. branches.push(i);
  254. }
  255. });
  256. this.subNodes = branches;
  257. this.lastNode = branches[branches.length -1];
  258. return !!this.subNodes.length;
  259. };
  260. /**
  261. * @return {string}
  262. **/
  263. MsgTextLeaf.prototype.innerHTML = function() {
  264. return this.text;
  265. };
  266. /**
  267. * @return {string}
  268. **/
  269. MsgTextLeaf.prototype.outerHTML = function() {
  270. var tagName = 'span'
  271. ,classList = [];
  272. if (this._parent.checkIsCodeBlock()) {
  273. // TODO syntax highlight
  274. classList.push('codeblock');
  275. } else if (this._parent.checkIsCode()) {
  276. classList.push('code');
  277. } else {
  278. if (this.isLink)
  279. tagName = 'a';
  280. if (this._parent.checkIsBold())
  281. classList.push('bold');
  282. if (this._parent.checkIsItalic())
  283. classList.push('italic');
  284. if (this._parent.checkIsStrike())
  285. classList.push('strike');
  286. if (this._parent.checkIsEmoji())
  287. classList.push('emoji');
  288. }
  289. return '<' +tagName +(classList.length ? ' class="' +classList.join(' ') +'"' : '') +'>' +this.innerHTML() +'</' +tagName +'>';
  290. };
  291. MsgBranch.prototype.outerHTML = function() {
  292. var html = "";
  293. if (this.isQuote) {
  294. html += '<span class="quote">';
  295. }
  296. this.subNodes.forEach(function(node) {
  297. html += node.outerHTML();
  298. });
  299. if (this.isQuote) {
  300. html += '</span>';
  301. }
  302. return html;
  303. };
  304. /**
  305. * @constructor
  306. * @param {string} text
  307. */
  308. function MsgTree(text) {
  309. /** @const @type {string} */
  310. this.text = text;
  311. /** @type {MsgBranch|null} */
  312. this.root = null;
  313. }
  314. MsgTree.prototype.parseFrom = function(i) {
  315. for (var textLen = this.text.length; i < textLen;)
  316. i += this.root.addChar(this.text, i);
  317. this.eof();
  318. };
  319. MsgTree.prototype.parse = function() {
  320. this.root = new MsgBranch(this, 0);
  321. this.parseFrom(0);
  322. if (!this.root.prune()) {
  323. this.root = null;
  324. }
  325. };
  326. /** @param {MsgBranch} root */
  327. MsgTree.prototype.getFirstUnterminated = function(root) {
  328. for (var i =0, nbBranches = root.subNodes.length; i < nbBranches; i++) {
  329. var branch = root.subNodes[i];
  330. if (branch instanceof MsgBranch) {
  331. if (!branch.terminated) {
  332. return branch;
  333. } else {
  334. var unTerminatedChild = this.getFirstUnterminated(branch);
  335. if (unTerminatedChild)
  336. return unTerminatedChild;
  337. }
  338. }
  339. }
  340. return null;
  341. };
  342. /**
  343. * @param {MsgBranch} branch
  344. * @param {boolean} next kill this branch or the next one ?
  345. **/
  346. MsgTree.prototype.revertTree = function(branch, next) {
  347. if (branch._parent instanceof MsgBranch) {
  348. branch._parent.subNodes.splice(branch._parent.subNodes.indexOf(branch));
  349. branch._parent.lastNode = branch._parent.subNodes[branch._parent.subNodes.length -1];
  350. this.revertTree(branch._parent, true);
  351. }
  352. };
  353. MsgTree.prototype.eof = function() {
  354. var unterminated = this.getFirstUnterminated(this.root);
  355. if (unterminated) {
  356. // We have a first token, but never closed
  357. // kill that branch
  358. this.revertTree(unterminated, false);
  359. // Add a new text leaf containing trigger
  360. var textNode = new MsgTextLeaf(unterminated._parent);
  361. textNode.addChar(this.text, unterminated.triggerIndex);
  362. unterminated._parent.subNodes.push(textNode);
  363. unterminated._parent.lastNode = textNode;
  364. // Restart parsing
  365. this.parseFrom(unterminated.triggerIndex +1);
  366. }
  367. // Else no problem
  368. };
  369. /**
  370. * @return {string}
  371. **/
  372. MsgTree.prototype.toHTML = function() {
  373. return this.root ? this.root.outerHTML() : "";
  374. };
  375. return function(text) {
  376. var root = new MsgTree(text);
  377. root.parse();
  378. return (root.toHTML());
  379. };
  380. })();