plugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /**
  2. * TinyMCE version 8.0.2 (2025-08-14)
  3. */
  4. (function () {
  5. 'use strict';
  6. var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager');
  7. /* eslint-disable @typescript-eslint/no-wrapper-object-types */
  8. const hasProto = (v, constructor, predicate) => {
  9. var _a;
  10. if (predicate(v, constructor.prototype)) {
  11. return true;
  12. }
  13. else {
  14. // String-based fallback time
  15. return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name;
  16. }
  17. };
  18. const typeOf = (x) => {
  19. const t = typeof x;
  20. if (x === null) {
  21. return 'null';
  22. }
  23. else if (t === 'object' && Array.isArray(x)) {
  24. return 'array';
  25. }
  26. else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
  27. return 'string';
  28. }
  29. else {
  30. return t;
  31. }
  32. };
  33. const isType = (type) => (value) => typeOf(value) === type;
  34. const eq = (t) => (a) => t === a;
  35. const isString = isType('string');
  36. const isUndefined = eq(undefined);
  37. const isNullable = (a) => a === null || a === undefined;
  38. const isNonNullable = (a) => !isNullable(a);
  39. const not = (f) => (t) => !f(t);
  40. const hasOwnProperty = Object.hasOwnProperty;
  41. const has = (obj, key) => hasOwnProperty.call(obj, key);
  42. const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr;
  43. const contains = (str, substr, start = 0, end) => {
  44. const idx = str.indexOf(substr, start);
  45. if (idx !== -1) {
  46. return isUndefined(end) ? true : idx + substr.length <= end;
  47. }
  48. else {
  49. return false;
  50. }
  51. };
  52. /** Does 'str' start with 'prefix'?
  53. * Note: all strings start with the empty string.
  54. * More formally, for all strings x, startsWith(x, "").
  55. * This is so that for all strings x and y, startsWith(y + x, y)
  56. */
  57. const startsWith = (str, prefix) => {
  58. return checkRange(str, prefix, 0);
  59. };
  60. const zeroWidth = '\uFEFF';
  61. const isZwsp = (char) => char === zeroWidth;
  62. const removeZwsp = (s) => s.replace(/\uFEFF/g, '');
  63. /*
  64. The RegEx parses the following components (https://www.rfc-editor.org/rfc/rfc3986.txt):
  65. scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
  66. foo://example.com:8042/over/there?name=ferret#nose
  67. \_/ \______________/\_________/ \_________/ \__/
  68. | | | | |
  69. scheme authority path query fragment
  70. Originally from:
  71. http://blog.mattheworiordan.com/post/13174566389/url-regular-expression-for-links-with-or-without-the
  72. Modified to:
  73. - include port numbers
  74. - allow full stops in email addresses
  75. - allow [-.~*+=!&;:'%@?^${}(),\/\w] after the #
  76. - allow [-.~*+=!&;:'%@?^${}(),\/\w] after the ?
  77. - move allow -_.~*+=!&;:'%@?^${}() in email usernames to the first @ match (TBIO-4809)
  78. - enforce domains to be [A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)* so they can't end in a period (TBIO-4809)
  79. - removed a bunch of escaping, made every group non-capturing (during TBIO-4809)
  80. - colons are only valid when followed directly by // or some text and then @ (TBIO-4867)
  81. - only include the fragment '#' if it has 1 or more trailing matches
  82. - only include the query '?' if it has 1 or more trailing matches
  83. - allow commas in URL path
  84. - exclude trailing comma and period in URL path
  85. - allow up to 15 character schemes including all valid characters from the spec https://url.spec.whatwg.org/#url-scheme-string (TINY-5074)
  86. - changed instances of 0-9 to be \d (TINY-5074)
  87. - reduced duplication (TINY-5074)
  88. - allow [*!;:'@$] in the path segment as they are valid characters per the spec: https://url.spec.whatwg.org/#url-path-segment-string (TINY-8069)
  89. (?:
  90. (?:
  91. [A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?
  92. | www\.
  93. | [-;:&=+$,.\w]+@
  94. )
  95. [A-Za-z\d-]+
  96. (?:\.[A-Za-z\d-]+)*
  97. )
  98. (?::\d+)?
  99. (?:
  100. \/
  101. (?:
  102. [-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w]
  103. )?
  104. )?
  105. (?:
  106. \?
  107. (?:
  108. [-.~*+=!&;:'%@?^${}(),\/\w]+
  109. )
  110. )?
  111. (?:
  112. #
  113. (?:
  114. [-.~*+=!&;:'%@?^${}(),\/\w]+
  115. )
  116. )?
  117. */
  118. const link = () =>
  119. // eslint-disable-next-line max-len
  120. /(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g;
  121. const option = (name) => (editor) => editor.options.get(name);
  122. const register = (editor) => {
  123. const registerOption = editor.options.register;
  124. registerOption('autolink_pattern', {
  125. processor: 'regexp',
  126. // Use the Polaris link detection, however for autolink we need to make it be an exact match
  127. default: new RegExp('^' + link().source + '$', 'i')
  128. });
  129. registerOption('link_default_target', {
  130. processor: 'string'
  131. });
  132. registerOption('link_default_protocol', {
  133. processor: 'string',
  134. default: 'https'
  135. });
  136. };
  137. const getAutoLinkPattern = option('autolink_pattern');
  138. const getDefaultLinkTarget = option('link_default_target');
  139. const getDefaultLinkProtocol = option('link_default_protocol');
  140. const allowUnsafeLinkTarget = option('allow_unsafe_link_target');
  141. var global = tinymce.util.Tools.resolve('tinymce.dom.TextSeeker');
  142. const isTextNode = (node) => node.nodeType === 3;
  143. const isElement = (node) => node.nodeType === 1;
  144. const isBracketOrSpace = (char) => /^[(\[{ \u00a0]$/.test(char);
  145. // Note: This is similar to the Polaris protocol detection, except it also handles `mailto` and any length scheme
  146. const hasProtocol = (url) => /^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(url);
  147. // A limited list of punctuation characters that might be used after a link
  148. const isPunctuation = (char) => /[?!,.;:]/.test(char);
  149. const findChar = (text, index, predicate) => {
  150. for (let i = index - 1; i >= 0; i--) {
  151. const char = text.charAt(i);
  152. if (!isZwsp(char) && predicate(char)) {
  153. return i;
  154. }
  155. }
  156. return -1;
  157. };
  158. const freefallRtl = (container, offset) => {
  159. let tempNode = container;
  160. let tempOffset = offset;
  161. while (isElement(tempNode) && tempNode.childNodes[tempOffset]) {
  162. tempNode = tempNode.childNodes[tempOffset];
  163. tempOffset = isTextNode(tempNode) ? tempNode.data.length : tempNode.childNodes.length;
  164. }
  165. return { container: tempNode, offset: tempOffset };
  166. };
  167. const parseCurrentLine = (editor, offset) => {
  168. var _a;
  169. const voidElements = editor.schema.getVoidElements();
  170. const autoLinkPattern = getAutoLinkPattern(editor);
  171. const { dom, selection } = editor;
  172. // Never create a link when we are inside a link
  173. if (dom.getParent(selection.getNode(), 'a[href]') !== null || editor.mode.isReadOnly()) {
  174. return null;
  175. }
  176. const rng = selection.getRng();
  177. const textSeeker = global(dom, (node) => {
  178. return dom.isBlock(node) || has(voidElements, node.nodeName.toLowerCase()) || dom.getContentEditable(node) === 'false' || dom.getParent(node, 'a[href]') !== null;
  179. });
  180. // Descend down the end container to find the text node
  181. const { container: endContainer, offset: endOffset } = freefallRtl(rng.endContainer, rng.endOffset);
  182. // Find the root container to use when walking
  183. const root = (_a = dom.getParent(endContainer, dom.isBlock)) !== null && _a !== void 0 ? _a : dom.getRoot();
  184. // Move the selection backwards to the start of the potential URL to account for the pressed character
  185. // while also excluding the last full stop from a word like "www.site.com."
  186. const endSpot = textSeeker.backwards(endContainer, endOffset + offset, (node, offset) => {
  187. const text = node.data;
  188. const idx = findChar(text, offset, not(isBracketOrSpace));
  189. // Move forward one so the offset is after the found character unless the found char is a punctuation char
  190. return idx === -1 || isPunctuation(text[idx]) ? idx : idx + 1;
  191. }, root);
  192. if (!endSpot) {
  193. return null;
  194. }
  195. // Walk backwards until we find a boundary or a bracket/space
  196. let lastTextNode = endSpot.container;
  197. const startSpot = textSeeker.backwards(endSpot.container, endSpot.offset, (node, offset) => {
  198. lastTextNode = node;
  199. const idx = findChar(node.data, offset, isBracketOrSpace);
  200. // Move forward one so that the offset is after the bracket/space
  201. return idx === -1 ? idx : idx + 1;
  202. }, root);
  203. const newRng = dom.createRng();
  204. if (!startSpot) {
  205. newRng.setStart(lastTextNode, 0);
  206. }
  207. else {
  208. newRng.setStart(startSpot.container, startSpot.offset);
  209. }
  210. newRng.setEnd(endSpot.container, endSpot.offset);
  211. const rngText = removeZwsp(newRng.toString());
  212. const matches = rngText.match(autoLinkPattern);
  213. if (matches) {
  214. let url = matches[0];
  215. if (startsWith(url, 'www.')) {
  216. const protocol = getDefaultLinkProtocol(editor);
  217. url = protocol + '://' + url;
  218. }
  219. else if (contains(url, '@') && !hasProtocol(url)) {
  220. url = 'mailto:' + url;
  221. }
  222. return { rng: newRng, url };
  223. }
  224. else {
  225. return null;
  226. }
  227. };
  228. const convertToLink = (editor, result) => {
  229. const { dom, selection } = editor;
  230. const { rng, url } = result;
  231. const bookmark = selection.getBookmark();
  232. selection.setRng(rng);
  233. // Needs to be a native createlink command since this is executed in a keypress event handler
  234. // so the pending character that is to be inserted needs to be inserted after the link. That will not
  235. // happen if we use the formatter create link version. Since we're using the native command
  236. // then we also need to ensure the exec command events are fired for backwards compatibility.
  237. const command = 'createlink';
  238. const args = { command, ui: false, value: url };
  239. const beforeExecEvent = editor.dispatch('BeforeExecCommand', args);
  240. if (!beforeExecEvent.isDefaultPrevented()) {
  241. editor.getDoc().execCommand(command, false, url);
  242. editor.dispatch('ExecCommand', args);
  243. const defaultLinkTarget = getDefaultLinkTarget(editor);
  244. if (isString(defaultLinkTarget)) {
  245. const anchor = selection.getNode();
  246. dom.setAttrib(anchor, 'target', defaultLinkTarget);
  247. // Ensure noopener is added for blank targets to prevent window opener attacks
  248. if (defaultLinkTarget === '_blank' && !allowUnsafeLinkTarget(editor)) {
  249. dom.setAttrib(anchor, 'rel', 'noopener');
  250. }
  251. }
  252. }
  253. selection.moveToBookmark(bookmark);
  254. editor.nodeChanged();
  255. };
  256. const handleSpacebar = (editor) => {
  257. const result = parseCurrentLine(editor, -1);
  258. if (isNonNullable(result)) {
  259. convertToLink(editor, result);
  260. }
  261. };
  262. const handleBracket = handleSpacebar;
  263. const handleEnter = (editor) => {
  264. const result = parseCurrentLine(editor, 0);
  265. if (isNonNullable(result)) {
  266. convertToLink(editor, result);
  267. }
  268. };
  269. const setup = (editor) => {
  270. editor.on('keydown', (e) => {
  271. if (e.keyCode === 13 && !e.isDefaultPrevented()) {
  272. handleEnter(editor);
  273. }
  274. });
  275. editor.on('keyup', (e) => {
  276. if (e.keyCode === 32) {
  277. handleSpacebar(editor);
  278. // One of the closing bracket keys: ), ] or }
  279. }
  280. else if (e.keyCode === 48 && e.shiftKey || e.keyCode === 221) {
  281. handleBracket(editor);
  282. }
  283. });
  284. };
  285. var Plugin = () => {
  286. global$1.add('autolink', (editor) => {
  287. register(editor);
  288. setup(editor);
  289. });
  290. };
  291. Plugin();
  292. /** *****
  293. * DO NOT EXPORT ANYTHING
  294. *
  295. * IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
  296. *******/
  297. })();