utils.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { html } from 'lit';
  2. import { bracketing_directives, dont_escape, styling_directives, styling_map } from './constants';
  3. /**
  4. * @param {any} s
  5. * @returns {boolean} - Returns true if the input is a string, otherwise false.
  6. */
  7. export function isString(s) {
  8. return typeof s === 'string';
  9. }
  10. /**
  11. * @param {string} url
  12. * @returns {boolean}
  13. */
  14. export function isSpotifyTrack(url) {
  15. try {
  16. const { hostname, pathname } = new URL(url);
  17. return hostname === 'open.spotify.com' && pathname.startsWith('/track/');
  18. } catch (e) {
  19. console.warn(`Could not create URL object from ${url}`);
  20. return false;
  21. }
  22. }
  23. /**
  24. * @param {string} url
  25. * @returns {Promise<Headers>}
  26. */
  27. export async function getHeaders(url) {
  28. try {
  29. const response = await fetch(url, { method: 'HEAD' });
  30. return response.headers;
  31. } catch (e) {
  32. console.warn(`Error calling HEAD on url ${url}: ${e}`);
  33. return null;
  34. }
  35. }
  36. /**
  37. * We don't render more than two line-breaks, replace extra line-breaks with
  38. * the zero-width whitespace character
  39. * This takes into account other characters that may have been removed by
  40. * being replaced with a zero-width space, such as '> ' in the case of
  41. * multi-line quotes.
  42. * @param {string} text
  43. */
  44. export function collapseLineBreaks(text) {
  45. return text.replace(/\n(\u200B*\n)+/g, (m) => `\n${'\u200B'.repeat(m.length - 2)}\n`);
  46. }
  47. export const tplMentionWithNick = (o) =>
  48. html`<span class="mention mention--self badge badge-info" data-uri="${o.uri}">${o.mention}</span>`;
  49. export function tplMention(o) {
  50. return html`<span class="mention" data-uri="${o.uri}">${o.mention}</span>`;
  51. }
  52. /**
  53. * Checks whether a given character "d" at index "i" of "text" is a valid opening or closing directive.
  54. * @param {String} d - The potential directive
  55. * @param {import('./texture').Texture} text - The text in which the directive appears
  56. * @param {Number} i - The directive index
  57. * @param {Boolean} opening - Check for a valid opening or closing directive
  58. * @returns {boolean}
  59. */
  60. function isValidDirective(d, text, i, opening) {
  61. // Ignore directives that are parts of words
  62. // More info on the Regexes used here: https://javascript.info/regexp-unicode#unicode-properties-p
  63. if (opening) {
  64. const regex = RegExp(dont_escape.includes(d) ? `^(\\p{L}|\\p{N})${d}` : `^(\\p{L}|\\p{N})\\${d}`, 'u');
  65. if (i > 1 && regex.test(text.slice(i - 1))) {
  66. return false;
  67. }
  68. const is_quote = isQuoteDirective(d);
  69. if (is_quote && i > 0 && text[i - 1] !== '\n') {
  70. // Quote directives must be on newlines
  71. return false;
  72. } else if (bracketing_directives.includes(d) && text[i + 1] === d) {
  73. // Don't consider empty bracketing directives as valid (e.g. **, `` etc.)
  74. return false;
  75. }
  76. } else {
  77. const regex = RegExp(dont_escape.includes(d) ? `^${d}(\\p{L}|\\p{N})` : `^\\${d}(\\p{L}|\\p{N})`, 'u');
  78. if (i < text.length - 1 && regex.test(text.slice(i))) {
  79. return false;
  80. }
  81. if (bracketing_directives.includes(d) && text[i - 1] === d) {
  82. // Don't consider empty directives as valid (e.g. **, `` etc.)
  83. return false;
  84. }
  85. }
  86. return true;
  87. }
  88. /**
  89. * Given a specific index "i" of "text", return the directive it matches or null otherwise.
  90. * @param {import('./texture').Texture} text - The text in which the directive appears
  91. * @param {Number} i - The directive index
  92. * @param {Boolean} opening - Whether we're looking for an opening or closing directive
  93. * @returns {string|null}
  94. */
  95. function getDirective(text, i, opening = true) {
  96. let d;
  97. if (
  98. /(^```[\s,\u200B]*\n)|(^```[\s,\u200B]*$)/.test(text.slice(i)) &&
  99. (i === 0 || text[i - 1] === '>' || /\n\u200B{0,2}$/.test(text.slice(0, i)))
  100. ) {
  101. d = text.slice(i, i + 3);
  102. } else if (styling_directives.includes(text.slice(i, i + 1))) {
  103. d = text.slice(i, i + 1);
  104. if (!isValidDirective(d, text, i, opening)) return null;
  105. } else {
  106. return null;
  107. }
  108. return d;
  109. }
  110. /**
  111. * @param {import('./texture').Texture} text
  112. * @param {number} i
  113. */
  114. export function getDirectiveAndLength(text, i) {
  115. const d = getDirective(text, i);
  116. const length = d ? getDirectiveLength(d, text, i) : 0;
  117. return length > 0 ? { d, length } : {};
  118. }
  119. /**
  120. * Given a directive "d", which occurs in "text" at index "i", check that it
  121. * has a valid closing directive and return the length from start to end of the
  122. * directive.
  123. * @param {String} d -The directive
  124. * @param {Number} i - The directive index
  125. * @param {import('./texture').Texture} text -The text in which the directive appears
  126. */
  127. function getDirectiveLength(d, text, i) {
  128. if (!d) return 0;
  129. const begin = i;
  130. i += d.length;
  131. if (isQuoteDirective(d)) {
  132. i += text
  133. .slice(i)
  134. .split(/\n\u200B*[^>\u200B]/)
  135. .shift().length;
  136. return i - begin;
  137. } else if (styling_map[d].type === 'span') {
  138. const line = text.slice(i).split('\n').shift();
  139. let j = 0;
  140. let idx = line.indexOf(d);
  141. while (idx !== -1) {
  142. if (getDirective(text, i + idx, false) === d) {
  143. return idx + 2 * d.length;
  144. }
  145. idx = line.indexOf(d, j++);
  146. }
  147. return 0;
  148. } else {
  149. // block directives
  150. const substring = text.slice(i + 1);
  151. let j = 0;
  152. let idx = substring.indexOf(d);
  153. while (idx !== -1) {
  154. if (getDirective(text, i + 1 + idx, false) === d) {
  155. return idx + 1 + 2 * d.length;
  156. }
  157. idx = substring.indexOf(d, j++);
  158. }
  159. return 0;
  160. }
  161. }
  162. /**
  163. * @param {string} d
  164. */
  165. export function isQuoteDirective(d) {
  166. return ['>', '&gt;'].includes(d);
  167. }
  168. /**
  169. * @param {import('./texture').Texture} text
  170. * @returns {boolean}
  171. */
  172. export function containsDirectives(text) {
  173. for (let i = 0; i < styling_directives.length; i++) {
  174. if (text.includes(styling_directives[i])) {
  175. return true;
  176. }
  177. }
  178. return false;
  179. }