styling.js 6.3 KB

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