msgFormatter.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. "use strict";
  2. /**
  3. * replace all :emoji: codes with corresponding image
  4. * @param {string} inputString
  5. * @return {string}
  6. **/
  7. function formatEmojis(inputString) {
  8. return inputString.replace(/:([^ \t:]+):/g, function(returnFailed, emoji) {
  9. var emojiDom = makeEmojiDom(emoji);
  10. if (emojiDom) {
  11. var domParent = document.createElement("span");
  12. domParent.className = returnFailed === inputString ? R.klass.emoji.medium : R.klass.emoji.small;
  13. domParent.appendChild(emojiDom);
  14. return domParent.outerHTML;
  15. }
  16. return returnFailed;
  17. });
  18. }
  19. /** @type {function(string):string} */
  20. var formatText = (function() {
  21. /**
  22. * @param {string} c
  23. * @return {boolean}
  24. **/
  25. function isAlphadec(c) {
  26. return ((c >= 'A' && c <= 'Z') ||
  27. (c >= 'a' && c <= 'z') ||
  28. (c >= '0' && c <= '9') ||
  29. "àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇߨøÅ寿œ".indexOf(c) !== -1);
  30. }
  31. /**
  32. * @constructor
  33. * @param {MsgBranch!} _parent
  34. **/
  35. function MsgTextLeaf(_parent) {
  36. /** @type {string} */
  37. this.text = "";
  38. /** @const @type {MsgBranch} */
  39. this._parent = _parent;
  40. }
  41. /**
  42. * @constructor
  43. * @param {MsgBranch|MsgTree} _parent
  44. * @param {number} triggerIndex
  45. * @param {string=} trigger
  46. */
  47. function MsgBranch(_parent, triggerIndex, trigger) {
  48. /** @const @type {number} */
  49. this.triggerIndex = triggerIndex;
  50. /** @type {MsgBranch|MsgTextLeaf} */
  51. this.lastNode = new MsgTextLeaf(this);
  52. /** @type {Array<MsgBranch|MsgTextLeaf>} */
  53. this.subNodes = [ this.lastNode ];
  54. /** @const @type {string} */
  55. this.trigger = trigger || '';
  56. /** @type {boolean} */
  57. this.isLink = this.trigger === '<';
  58. /** @type {boolean} */
  59. this.isBold = this.trigger === '*';
  60. /** @type {boolean} */
  61. this.isItalic = this.trigger === '_';
  62. /** @type {boolean} */
  63. this.isStrike = this.trigger === '~' || this.trigger === '-';
  64. /** @type {boolean} */
  65. this.isQuote = this.trigger === '>';
  66. /** @type {boolean} */
  67. this.isEmoji = this.trigger === ':';
  68. /** @type {boolean} */
  69. this.isCode = this.trigger === '`';
  70. /** @type {boolean} */
  71. this.isCodeBlock = this.trigger === '```';
  72. /** @type {boolean} */
  73. this.isEol = this.trigger === '\n';
  74. /** @const @type {MsgBranch|MsgTree} */
  75. this._parent = _parent;
  76. /** @type {boolean} */
  77. this.terminated = false;
  78. }
  79. /** @return {boolean} */
  80. MsgBranch.prototype.checkIsBold = function() {
  81. return (this.isBold && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsBold());
  82. };
  83. /** @return {boolean} */
  84. MsgBranch.prototype.checkIsItalic = function() {
  85. return (this.isItalic && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsItalic());
  86. };
  87. /** @return {boolean} */
  88. MsgBranch.prototype.checkIsStrike = function() {
  89. return (this.isStrike && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsStrike());
  90. };
  91. /** @return {boolean} */
  92. MsgBranch.prototype.checkIsQuote = function() {
  93. return (this.isQuote && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsQuote());
  94. };
  95. /** @return {boolean} */
  96. MsgBranch.prototype.checkIsEmoji = function() {
  97. return (this.isEmoji && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsEmoji());
  98. };
  99. /** @return {boolean} */
  100. MsgBranch.prototype.checkIsCode = function() {
  101. return (this.isCode && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsCode());
  102. };
  103. /** @return {boolean} */
  104. MsgBranch.prototype.checkIsCodeBlock = function() {
  105. return (this.isCodeBlock && this.terminated) || (this._parent instanceof MsgBranch && this._parent.checkIsCodeBlock());
  106. };
  107. /**
  108. * Check if this token closes the branch
  109. * FIXME it is possible that an inner branch still lives
  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. }
  129. if (str.substr(i, this.trigger.length) === this.trigger) {
  130. return new MsgTextLeaf(/** @type {MsgBranch!} */ (this._parent));
  131. }
  132. // FIXME
  133. // check if branch terminates here
  134. // If yes return a branche containing all unfinished children
  135. // If yes but no children, return a new MsgTextLeaf
  136. // If not return null
  137. return null;
  138. };
  139. /**
  140. * Check if this token is compatible with this branch
  141. * @param {string} str
  142. * @param {number} i
  143. * @return {boolean}
  144. **/
  145. MsgBranch.prototype.isAcceptable = function(str, i) {
  146. if (this.isEmoji && (str[i] === ' ' || str[i] === '\t'))
  147. return false;
  148. return true;
  149. };
  150. /**
  151. * Check if str[i] is a trigger for a new node
  152. * if true, return the trigger
  153. * @param {string} str
  154. * @param {number} i
  155. * @return {string|null}
  156. **/
  157. MsgBranch.prototype.isNewToken = function(str, i) {
  158. if (this.lastNode instanceof MsgTextLeaf) {
  159. if (str.substr(i, 3) === '```')
  160. return '```';
  161. if ('`' === str[i])
  162. return str[i];
  163. if (['*', '~', '-', '_' ].indexOf(str[i]) !== -1 && (isAlphadec(str[i +1]) || ['*', '`', '~', '-', '_', ':', '<'].indexOf(str[i+1]) !== -1))
  164. return str[i];
  165. if ([':', '<'].indexOf(str[i]) !== -1 && isAlphadec(str[i +1]))
  166. return str[i];
  167. }
  168. return null;
  169. };
  170. /** @return {number} */
  171. MsgTextLeaf.prototype.addChar = function(str, i) {
  172. this.text += str[i];
  173. return 1;
  174. };
  175. /**
  176. * Parse next char
  177. * @param {string} str
  178. * @param {number} i
  179. * @return {number}
  180. **/
  181. MsgBranch.prototype.addChar = function(str, i) {
  182. var isFinished = this.lastNode.finishWith ? this.lastNode.finishWith(str, i) : null;
  183. if (isFinished) {
  184. // terminated token, move pointer to a new leaf
  185. this.lastNode.terminated = true;
  186. this.lastNode = isFinished;
  187. this.subNodes.push(isFinished);
  188. return 1;
  189. } else {
  190. if (this.lastNode instanceof MsgTextLeaf || this.lastNode.isAcceptable(str, i)) {
  191. var isNewToken = this.isNewToken(str, i);
  192. if (isNewToken) {
  193. this.lastNode = new MsgBranch(this, i, isNewToken);
  194. this.subNodes.push(this.lastNode);
  195. return this.lastNode.trigger.length;
  196. } else {
  197. return this.lastNode.addChar(str, i);
  198. }
  199. } else {
  200. // last branch child is not compatible with this token.
  201. // So, lastBranch is not a branch
  202. // Add a new "escaped" node to replace trigger
  203. var textNode = new MsgTextLeaf(this);
  204. textNode.addChar(str, this.lastNode.trigger);
  205. this.subNodes.pop();
  206. this.subNodes.push(textNode);
  207. // new root
  208. var newBranch = new MsgBranch(this, this.lastNode.triggerIndex +1);
  209. for (var charIndex = this.lastNode.triggerIndex +1; charIndex <= i;)
  210. charIndex += newBranch.addChar(str, charIndex);
  211. this.lastNode = this.subNodes[this.subNodes.length -1];
  212. return 1;
  213. }
  214. }
  215. };
  216. /**
  217. * @return {boolean} true if still contains stuff
  218. **/
  219. MsgBranch.prototype.prune = function() {
  220. var branches = [];
  221. this.subNodes.forEach(function(i) {
  222. if (i instanceof MsgTextLeaf) {
  223. if (i.text !== '')
  224. branches.push(i);
  225. } else if (i.prune()) {
  226. branches.push(i);
  227. }
  228. });
  229. this.subNodes = branches;
  230. this.lastNode = branches[branches.length -1];
  231. return !!this.subNodes.length;
  232. };
  233. /**
  234. * @return {string}
  235. **/
  236. MsgTextLeaf.prototype.innerHTML = function() {
  237. return this.text;
  238. };
  239. /**
  240. * @return {string}
  241. **/
  242. MsgTextLeaf.prototype.outerHTML = function() {
  243. var tagName = 'span'
  244. ,classList = [];
  245. if (this._parent.checkIsCodeBlock()) {
  246. classList.push('codeblock');
  247. } else if (this._parent.checkIsCode()) {
  248. classList.push('code');
  249. } else {
  250. if (this.isLink)
  251. tagName = 'a';
  252. if (this._parent.checkIsBold())
  253. classList.push('bold');
  254. if (this._parent.checkIsItalic())
  255. classList.push('italic');
  256. if (this._parent.checkIsStrike())
  257. classList.push('strike');
  258. if (this._parent.checkIsQuote()) // FIXME nope.
  259. classList.push('quote');
  260. if (this._parent.checkIsEmoji())
  261. classList.push('emoji');
  262. }
  263. return '<' +tagName +(classList.length ? ' class="' +classList.join(' ') +'"' : '') +'>' +this.innerHTML() +'</' +tagName +'>';
  264. };
  265. MsgBranch.prototype.outerHTML = function() {
  266. var html = "";
  267. this.subNodes.forEach(function(node) {
  268. html += node.outerHTML();
  269. });
  270. return html;
  271. };
  272. MsgBranch.prototype.eof = function() {
  273. // FIXME check for any unterminated branches to reinterpret them
  274. };
  275. /**
  276. * @constructor
  277. * @param {string} text
  278. */
  279. function MsgTree(text) {
  280. /** @const @type {string} */
  281. this.text = text;
  282. /** @type {MsgBranch|null} */
  283. this.root = null;
  284. }
  285. MsgTree.prototype.parse = function() {
  286. this.root = new MsgBranch(this, 0);
  287. for (var i =0, textLen = this.text.length; i < textLen;)
  288. i += this.root.addChar(this.text, i);
  289. this.root.eof();
  290. if (!this.root.prune()) {
  291. this.root = null;
  292. }
  293. };
  294. /**
  295. * @return {string}
  296. **/
  297. MsgTree.prototype.toHTML = function() {
  298. return this.root ? this.root.outerHTML() : "";
  299. };
  300. return function(text) {
  301. var root = new MsgTree(text);
  302. root.parse();
  303. return (root.toHTML());
  304. };
  305. })();