Converse converse.js

Source: shared/styling.js

  1. /**
  2. * @copyright 2022, the Converse.js contributors
  3. * @license Mozilla Public License (MPLv2)
  4. * @description Utility functions to help with parsing XEP-393 message styling hints
  5. * @todo Other parsing helpers can be made more abstract and placed here.
  6. */
  7. import { html } from 'lit';
  8. import { renderStylingDirectiveBody } from 'shared/directives/styling.js';
  9. const bracketing_directives = ['*', '_', '~', '`'];
  10. const styling_directives = [...bracketing_directives, '```', '>'];
  11. const styling_map = {
  12. '*': {'name': 'strong', 'type': 'span'},
  13. '_': {'name': 'emphasis', 'type': 'span'},
  14. '~': {'name': 'strike', 'type': 'span'},
  15. '`': {'name': 'preformatted', 'type': 'span'},
  16. '```': {'name': 'preformatted_block', 'type': 'block'},
  17. '>': {'name': 'quote', 'type': 'block'}
  18. };
  19. const dont_escape = ['_', '>', '`', '~'];
  20. const styling_templates = {
  21. // m is the chatbox model
  22. // i is the offset of this directive relative to the start of the original message
  23. 'emphasis': (txt, i, options) => html`<span class="styling-directive">_</span><i>${renderStylingDirectiveBody(txt, i, options)}</i><span class="styling-directive">_</span>`,
  24. 'preformatted': txt => html`<span class="styling-directive">\`</span><code>${txt}</code><span class="styling-directive">\`</span>`,
  25. 'preformatted_block': txt => html`<div class="styling-directive">\`\`\`</div><code class="block">${txt}</code><div class="styling-directive">\`\`\`</div>`,
  26. 'quote': (txt, i, options) => html`<blockquote>${renderStylingDirectiveBody(txt, i, options)}</blockquote>`,
  27. 'strike': (txt, i, options) => html`<span class="styling-directive">~</span><del>${renderStylingDirectiveBody(txt, i, options)}</del><span class="styling-directive">~</span>`,
  28. 'strong': (txt, i, options) => html`<span class="styling-directive">*</span><b>${renderStylingDirectiveBody(txt, i, options)}</b><span class="styling-directive">*</span>`,
  29. };
  30. /**
  31. * Checks whether a given character "d" at index "i" of "text" is a valid opening or closing directive.
  32. * @param { String } d - The potential directive
  33. * @param { String } text - The text in which the directive appears
  34. * @param { Number } i - The directive index
  35. * @param { Boolean } opening - Check for a valid opening or closing directive
  36. */
  37. function isValidDirective (d, text, i, opening) {
  38. // Ignore directives that are parts of words
  39. // More info on the Regexes used here: https://javascript.info/regexp-unicode#unicode-properties-p
  40. if (opening) {
  41. const regex = RegExp(dont_escape.includes(d) ? `^(\\p{L}|\\p{N})${d}` : `^(\\p{L}|\\p{N})\\${d}`, 'u');
  42. if (i > 1 && regex.test(text.slice(i-1))) {
  43. return false;
  44. }
  45. const is_quote = isQuoteDirective(d);
  46. if (is_quote && i > 0 && text[i-1] !== '\n') {
  47. // Quote directives must be on newlines
  48. return false;
  49. } else if (bracketing_directives.includes(d) && (text[i+1] === d)) {
  50. // Don't consider empty bracketing directives as valid (e.g. **, `` etc.)
  51. return false;
  52. }
  53. } else {
  54. const regex = RegExp(dont_escape.includes(d) ? `^${d}(\\p{L}|\\p{N})` : `^\\${d}(\\p{L}|\\p{N})`, 'u');
  55. if (i < text.length-1 && regex.test(text.slice(i))) {
  56. return false;
  57. }
  58. if (bracketing_directives.includes(d) && (text[i-1] === d)) {
  59. // Don't consider empty directives as valid (e.g. **, `` etc.)
  60. return false;
  61. }
  62. }
  63. return true;
  64. }
  65. /**
  66. * Given a specific index "i" of "text", return the directive it matches or null otherwise.
  67. * @param { String } text - The text in which the directive appears
  68. * @param { Number } i - The directive index
  69. * @param { Boolean } opening - Whether we're looking for an opening or closing directive
  70. */
  71. function getDirective (text, i, opening=true) {
  72. let d;
  73. if (
  74. (/(^```[\s,\u200B]*\n)|(^```[\s,\u200B]*$)/).test(text.slice(i)) &&
  75. (i === 0 || text[i-1] === '>' || (/\n\u200B{0,2}$/).test(text.slice(0, i)))
  76. ) {
  77. d = text.slice(i, i+3);
  78. } else if (styling_directives.includes(text.slice(i, i+1))) {
  79. d = text.slice(i, i+1);
  80. if (!isValidDirective(d, text, i, opening)) return null;
  81. } else {
  82. return null;
  83. }
  84. return d;
  85. }
  86. /**
  87. * Given a directive "d", which occurs in "text" at index "i", check that it
  88. * has a valid closing directive and return the length from start to end of the
  89. * directive.
  90. * @param { String } d -The directive
  91. * @param { Number } i - The directive index
  92. * @param { String } text -The text in which the directive appears
  93. */
  94. function getDirectiveLength (d, text, i) {
  95. if (!d) return 0;
  96. const begin = i;
  97. i += d.length;
  98. if (isQuoteDirective(d)) {
  99. i += text.slice(i).split(/\n[^>]/).shift().length;
  100. return i-begin;
  101. } else if (styling_map[d].type === 'span') {
  102. const line = text.slice(i).split('\n').shift();
  103. let j = 0;
  104. let idx = line.indexOf(d);
  105. while (idx !== -1) {
  106. if (getDirective(text, i+idx, false) === d) {
  107. return idx+2*d.length;
  108. }
  109. idx = line.indexOf(d, j++);
  110. }
  111. return 0;
  112. } else {
  113. // block directives
  114. const substring = text.slice(i+1);
  115. let j = 0;
  116. let idx = substring.indexOf(d);
  117. while (idx !== -1) {
  118. if (getDirective(text, i+1+idx, false) === d) {
  119. return idx+1+2*d.length;
  120. }
  121. idx = substring.indexOf(d, j++);
  122. }
  123. return 0;
  124. }
  125. }
  126. export function getDirectiveAndLength (text, i) {
  127. const d = getDirective(text, i);
  128. const length = d ? getDirectiveLength(d, text, i) : 0;
  129. return length > 0 ? { d, length } : {};
  130. }
  131. export const isQuoteDirective = (d) => ['>', '&gt;'].includes(d);
  132. export function getDirectiveTemplate (d, text, offset, options) {
  133. const template = styling_templates[styling_map[d].name];
  134. if (isQuoteDirective(d)) {
  135. const newtext = text
  136. // Don't show the directive itself
  137. .replace(/\n>\s/g, '\n\u200B\u200B')
  138. .replace(/\n>/g, '\n\u200B')
  139. .replace(/\n$/, ''); // Trim line-break at the end
  140. return template(newtext, offset, options);
  141. } else {
  142. return template(text, offset, options);
  143. }
  144. }
  145. export function containsDirectives (text) {
  146. for (let i=0; i<styling_directives.length; i++) {
  147. if (text.includes(styling_directives[i])) {
  148. return true;
  149. }
  150. }
  151. }