uri.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import GLib from 'gi://GLib';
  5. /**
  6. * The same regular expression used in GNOME Shell
  7. *
  8. * http://daringfireball.net/2010/07/improved_regex_for_matching_urls
  9. */
  10. const _balancedParens = '\\((?:[^\\s()<>]+|(?:\\(?:[^\\s()<>]+\\)))*\\)';
  11. const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]';
  12. const _notTrailingJunk = '[^\\s`!()\\[\\]{};:\'\\".,<>?\u00AB\u00BB\u201C\u201D\u2018\u2019]';
  13. const _urlRegexp = new RegExp(
  14. '(^|' + _leadingJunk + ')' +
  15. '(' +
  16. '(?:' +
  17. '(?:http|https)://' + // scheme://
  18. '|' +
  19. 'www\\d{0,3}[.]' + // www.
  20. '|' +
  21. '[a-z0-9.\\-]+[.][a-z]{2,4}/' + // foo.xx/
  22. ')' +
  23. '(?:' + // one or more:
  24. '[^\\s()<>]+' + // run of non-space non-()
  25. '|' + // or
  26. _balancedParens + // balanced parens
  27. ')+' +
  28. '(?:' + // end with:
  29. _balancedParens + // balanced parens
  30. '|' + // or
  31. _notTrailingJunk + // last non-junk char
  32. ')' +
  33. ')', 'gi');
  34. /**
  35. * sms/tel URI RegExp (https://tools.ietf.org/html/rfc5724)
  36. *
  37. * A fairly lenient regexp for sms: URIs that allows tel: numbers with chars
  38. * from global-number, local-number (without phone-context) and single spaces.
  39. * This allows passing numbers directly from libfolks or GData without
  40. * pre-processing. It also makes an allowance for URIs passed from Gio.File
  41. * that always come in the form "sms:///".
  42. */
  43. const _smsParam = "[\\w.!~*'()-]+=(?:[\\w.!~*'()-]|%[0-9A-F]{2})*";
  44. const _telParam = ";[a-zA-Z0-9-]+=(?:[\\w\\[\\]/:&+$.!~*'()-]|%[0-9A-F]{2})+";
  45. const _lenientDigits = '[+]?(?:[0-9A-F*#().-]| (?! )|%20(?!%20))+';
  46. const _lenientNumber = `${_lenientDigits}(?:${_telParam})*`;
  47. const _smsRegex = new RegExp(
  48. '^' +
  49. 'sms:' + // scheme
  50. '(?:[/]{2,3})?' + // Gio.File returns ":///"
  51. '(' + // one or more...
  52. _lenientNumber + // phone numbers
  53. '(?:,' + _lenientNumber + ')*' + // separated by commas
  54. ')' +
  55. '(?:\\?(' + // followed by optional...
  56. _smsParam + // parameters...
  57. '(?:&' + _smsParam + ')*' + // separated by "&" (unescaped)
  58. '))?' +
  59. '$', 'g'); // fragments (#foo) not allowed
  60. const _numberRegex = new RegExp(
  61. '^' +
  62. '(' + _lenientDigits + ')' + // phone number digits
  63. '((?:' + _telParam + ')*)' + // followed by optional parameters
  64. '$', 'g');
  65. /**
  66. * Searches @str for URLs and returns an array of objects with %url
  67. * properties showing the matched URL string, and %pos properties indicating
  68. * the position within @str where the URL was found.
  69. *
  70. * @param {string} str - the string to search
  71. * @return {Object[]} the list of match objects, as described above
  72. */
  73. export function findUrls(str) {
  74. _urlRegexp.lastIndex = 0;
  75. const res = [];
  76. let match;
  77. while ((match = _urlRegexp.exec(str))) {
  78. const name = match[2];
  79. const url = GLib.uri_parse_scheme(name) ? name : `http://${name}`;
  80. res.push({name, url, pos: match.index + match[1].length});
  81. }
  82. return res;
  83. }
  84. /**
  85. * Return a string with URLs couched in <a> tags, parseable by Pango and
  86. * using the same RegExp as GNOME Shell.
  87. *
  88. * @param {string} str - The string to be modified
  89. * @param {string} [title] - An optional title (eg. alt text, tooltip)
  90. * @return {string} the modified text
  91. */
  92. export function linkify(str, title = null) {
  93. const text = GLib.markup_escape_text(str, -1);
  94. _urlRegexp.lastIndex = 0;
  95. if (title) {
  96. return text.replace(
  97. _urlRegexp,
  98. `$1<a href="$2" title="${title}">$2</a>`
  99. );
  100. } else {
  101. return text.replace(_urlRegexp, '$1<a href="$2">$2</a>');
  102. }
  103. }
  104. /**
  105. * A simple parsing class for sms: URI's (https://tools.ietf.org/html/rfc5724)
  106. */
  107. export default class URI {
  108. constructor(uri) {
  109. _smsRegex.lastIndex = 0;
  110. const [, recipients, query] = _smsRegex.exec(uri);
  111. this.recipients = recipients.split(',').map(recipient => {
  112. _numberRegex.lastIndex = 0;
  113. const [, number, params] = _numberRegex.exec(recipient);
  114. if (params) {
  115. for (const param of params.substr(1).split(';')) {
  116. const [key, value] = param.split('=');
  117. // add phone-context to beginning of
  118. if (key === 'phone-context' && value.startsWith('+'))
  119. return value + unescape(number);
  120. }
  121. }
  122. return unescape(number);
  123. });
  124. if (query) {
  125. for (const field of query.split('&')) {
  126. const [key, value] = field.split('=');
  127. if (key === 'body') {
  128. if (this.body)
  129. throw URIError('duplicate "body" field');
  130. this.body = value ? decodeURIComponent(value) : undefined;
  131. }
  132. }
  133. }
  134. }
  135. toString() {
  136. const uri = `sms:${this.recipients.join(',')}`;
  137. return this.body ? `${uri}?body=${escape(this.body)}` : uri;
  138. }
  139. }