Basic site editor

This commit is contained in:
Simon Martens
2026-01-14 18:52:57 +01:00
parent 3732b128db
commit f724cdf975
549 changed files with 324463 additions and 80 deletions

View File

@@ -0,0 +1,7 @@
// Exports the "autolink" plugin for usage with module loaders
// Usage:
// CommonJS:
// require('tinymce/plugins/autolink')
// ES2015:
// import 'tinymce/plugins/autolink'
require('./plugin.js');

View File

@@ -0,0 +1,315 @@
/**
* TinyMCE version 8.3.2 (2026-01-14)
*/
(function () {
'use strict';
var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager');
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
const hasProto = (v, constructor, predicate) => {
if (predicate(v, constructor.prototype)) {
return true;
}
else {
// String-based fallback time
return v.constructor?.name === constructor.name;
}
};
const typeOf = (x) => {
const t = typeof x;
if (x === null) {
return 'null';
}
else if (t === 'object' && Array.isArray(x)) {
return 'array';
}
else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
return 'string';
}
else {
return t;
}
};
const isType = (type) => (value) => typeOf(value) === type;
const eq = (t) => (a) => t === a;
const isString = isType('string');
const isUndefined = eq(undefined);
const isNullable = (a) => a === null || a === undefined;
const isNonNullable = (a) => !isNullable(a);
const not = (f) => (t) => !f(t);
const hasOwnProperty = Object.hasOwnProperty;
const has = (obj, key) => hasOwnProperty.call(obj, key);
const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr;
const contains = (str, substr, start = 0, end) => {
const idx = str.indexOf(substr, start);
if (idx !== -1) {
return isUndefined(end) ? true : idx + substr.length <= end;
}
else {
return false;
}
};
/** Does 'str' start with 'prefix'?
* Note: all strings start with the empty string.
* More formally, for all strings x, startsWith(x, "").
* This is so that for all strings x and y, startsWith(y + x, y)
*/
const startsWith = (str, prefix) => {
return checkRange(str, prefix, 0);
};
const zeroWidth = '\uFEFF';
const isZwsp = (char) => char === zeroWidth;
const removeZwsp = (s) => s.replace(/\uFEFF/g, '');
/*
The RegEx parses the following components (https://www.rfc-editor.org/rfc/rfc3986.txt):
scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]
foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/\_________/ \_________/ \__/
| | | | |
scheme authority path query fragment
Originally from:
http://blog.mattheworiordan.com/post/13174566389/url-regular-expression-for-links-with-or-without-the
Modified to:
- include port numbers
- allow full stops in email addresses
- allow [-.~*+=!&;:'%@?^${}(),\/\w] after the #
- allow [-.~*+=!&;:'%@?^${}(),\/\w] after the ?
- move allow -_.~*+=!&;:'%@?^${}() in email usernames to the first @ match (TBIO-4809)
- enforce domains to be [A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)* so they can't end in a period (TBIO-4809)
- removed a bunch of escaping, made every group non-capturing (during TBIO-4809)
- colons are only valid when followed directly by // or some text and then @ (TBIO-4867)
- only include the fragment '#' if it has 1 or more trailing matches
- only include the query '?' if it has 1 or more trailing matches
- allow commas in URL path
- exclude trailing comma and period in URL path
- allow up to 15 character schemes including all valid characters from the spec https://url.spec.whatwg.org/#url-scheme-string (TINY-5074)
- changed instances of 0-9 to be \d (TINY-5074)
- reduced duplication (TINY-5074)
- allow [*!;:'@$] in the path segment as they are valid characters per the spec: https://url.spec.whatwg.org/#url-path-segment-string (TINY-8069)
(?:
(?:
[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]+
)
)?
*/
const link = () =>
// eslint-disable-next-line max-len
/(?:[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;
const option = (name) => (editor) => editor.options.get(name);
const register = (editor) => {
const registerOption = editor.options.register;
registerOption('autolink_pattern', {
processor: 'regexp',
// Use the Polaris link detection, however for autolink we need to make it be an exact match
default: new RegExp('^' + link().source + '$', 'i')
});
registerOption('link_default_target', {
processor: 'string'
});
registerOption('link_default_protocol', {
processor: 'string',
default: 'https'
});
};
const getAutoLinkPattern = option('autolink_pattern');
const getDefaultLinkTarget = option('link_default_target');
const getDefaultLinkProtocol = option('link_default_protocol');
const allowUnsafeLinkTarget = option('allow_unsafe_link_target');
var global = tinymce.util.Tools.resolve('tinymce.dom.TextSeeker');
const isTextNode = (node) => node.nodeType === 3;
const isElement = (node) => node.nodeType === 1;
const isBracketOrSpace = (char) => /^[(\[{ \u00a0]$/.test(char);
// Note: This is similar to the Polaris protocol detection, except it also handles `mailto` and any length scheme
const hasProtocol = (url) => /^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(url);
// A limited list of punctuation characters that might be used after a link
const isPunctuation = (char) => /[?!,.;:]/.test(char);
const findChar = (text, index, predicate) => {
for (let i = index - 1; i >= 0; i--) {
const char = text.charAt(i);
if (!isZwsp(char) && predicate(char)) {
return i;
}
}
return -1;
};
const freefallRtl = (container, offset) => {
let tempNode = container;
let tempOffset = offset;
while (isElement(tempNode) && tempNode.childNodes[tempOffset]) {
tempNode = tempNode.childNodes[tempOffset];
tempOffset = isTextNode(tempNode) ? tempNode.data.length : tempNode.childNodes.length;
}
return { container: tempNode, offset: tempOffset };
};
const parseCurrentLine = (editor, offset) => {
const voidElements = editor.schema.getVoidElements();
const autoLinkPattern = getAutoLinkPattern(editor);
const { dom, selection } = editor;
// Never create a link when we are inside a link
if (dom.getParent(selection.getNode(), 'a[href]') !== null || editor.mode.isReadOnly()) {
return null;
}
const rng = selection.getRng();
const textSeeker = global(dom, (node) => {
return dom.isBlock(node) || has(voidElements, node.nodeName.toLowerCase()) || dom.getContentEditable(node) === 'false' || dom.getParent(node, 'a[href]') !== null;
});
// Descend down the end container to find the text node
const { container: endContainer, offset: endOffset } = freefallRtl(rng.endContainer, rng.endOffset);
// Find the root container to use when walking
const root = dom.getParent(endContainer, dom.isBlock) ?? dom.getRoot();
// Move the selection backwards to the start of the potential URL to account for the pressed character
// while also excluding the last full stop from a word like "www.site.com."
const endSpot = textSeeker.backwards(endContainer, endOffset + offset, (node, offset) => {
const text = node.data;
const idx = findChar(text, offset, not(isBracketOrSpace));
// Move forward one so the offset is after the found character unless the found char is a punctuation char
return idx === -1 || isPunctuation(text[idx]) ? idx : idx + 1;
}, root);
if (!endSpot) {
return null;
}
// Walk backwards until we find a boundary or a bracket/space
let lastTextNode = endSpot.container;
const startSpot = textSeeker.backwards(endSpot.container, endSpot.offset, (node, offset) => {
lastTextNode = node;
const idx = findChar(node.data, offset, isBracketOrSpace);
// Move forward one so that the offset is after the bracket/space
return idx === -1 ? idx : idx + 1;
}, root);
const newRng = dom.createRng();
if (!startSpot) {
newRng.setStart(lastTextNode, 0);
}
else {
newRng.setStart(startSpot.container, startSpot.offset);
}
newRng.setEnd(endSpot.container, endSpot.offset);
const rngText = removeZwsp(newRng.toString());
const matches = rngText.match(autoLinkPattern);
if (matches) {
let url = matches[0];
if (startsWith(url, 'www.')) {
const protocol = getDefaultLinkProtocol(editor);
url = protocol + '://' + url;
}
else if (contains(url, '@') && !hasProtocol(url)) {
url = 'mailto:' + url;
}
return { rng: newRng, url };
}
else {
return null;
}
};
const convertToLink = (editor, result) => {
const { dom, selection } = editor;
const { rng, url } = result;
const bookmark = selection.getBookmark();
selection.setRng(rng);
// Needs to be a native createlink command since this is executed in a keypress event handler
// so the pending character that is to be inserted needs to be inserted after the link. That will not
// happen if we use the formatter create link version. Since we're using the native command
// then we also need to ensure the exec command events are fired for backwards compatibility.
const command = 'createlink';
const args = { command, ui: false, value: url };
const beforeExecEvent = editor.dispatch('BeforeExecCommand', args);
if (!beforeExecEvent.isDefaultPrevented()) {
editor.getDoc().execCommand(command, false, url);
editor.dispatch('ExecCommand', args);
const defaultLinkTarget = getDefaultLinkTarget(editor);
if (isString(defaultLinkTarget)) {
const anchor = selection.getNode();
dom.setAttrib(anchor, 'target', defaultLinkTarget);
// Ensure noopener is added for blank targets to prevent window opener attacks
if (defaultLinkTarget === '_blank' && !allowUnsafeLinkTarget(editor)) {
dom.setAttrib(anchor, 'rel', 'noopener');
}
}
}
selection.moveToBookmark(bookmark);
editor.nodeChanged();
};
const handleSpacebar = (editor) => {
const result = parseCurrentLine(editor, -1);
if (isNonNullable(result)) {
convertToLink(editor, result);
}
};
const handleBracket = handleSpacebar;
const handleEnter = (editor) => {
const result = parseCurrentLine(editor, 0);
if (isNonNullable(result)) {
convertToLink(editor, result);
}
};
const setup = (editor) => {
editor.on('keydown', (e) => {
if (e.keyCode === 13 && !e.isDefaultPrevented()) {
handleEnter(editor);
}
});
editor.on('keyup', (e) => {
if (e.keyCode === 32) {
handleSpacebar(editor);
// One of the closing bracket keys: ), ] or }
}
else if (e.keyCode === 48 && e.shiftKey || e.keyCode === 221) {
handleBracket(editor);
}
});
};
var Plugin = () => {
global$1.add('autolink', (editor) => {
register(editor);
setup(editor);
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();

View File

@@ -0,0 +1 @@
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||o.constructor?.name===r.name)?"string":t;var n,o,r})(e);const n=e=>undefined===e;const o=e=>!(e=>null==e)(e),r=Object.hasOwnProperty,a=e=>"\ufeff"===e,s=e=>t=>t.options.get(e),l=s("autolink_pattern"),c=s("link_default_target"),i=s("link_default_protocol"),d=s("allow_unsafe_link_target");var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>/^[(\[{ \u00a0]$/.test(e),g=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!a(t)&&n(t))return o}return-1},m=(e,t)=>{const o=e.schema.getVoidElements(),a=l(e),{dom:s,selection:c}=e;if(null!==s.getParent(c.getNode(),"a[href]")||e.mode.isReadOnly())return null;const d=c.getRng(),m=u(s,(e=>{return s.isBlock(e)||(t=o,n=e.nodeName.toLowerCase(),r.call(t,n))||"false"===s.getContentEditable(e)||null!==s.getParent(e,"a[href]");var t,n})),{container:k,offset:p}=((e,t)=>{let n=e,o=t;for(;1===n.nodeType&&n.childNodes[o];)n=n.childNodes[o],o=3===n.nodeType?n.data.length:n.childNodes.length;return{container:n,offset:o}})(d.endContainer,d.endOffset),y=s.getParent(k,s.isBlock)??s.getRoot(),w=m.backwards(k,p+t,((e,t)=>{const n=e.data,o=g(n,t,(r=f,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1}),y);if(!w)return null;let h=w.container;const _=m.backwards(w.container,w.offset,((e,t)=>{h=e;const n=g(e.data,t,f);return-1===n?n:n+1}),y),v=s.createRng();_?v.setStart(_.container,_.offset):v.setStart(h,0),v.setEnd(w.container,w.offset);const A=v.toString().replace(/\uFEFF/g,"").match(a);if(A){let t=A[0];return b="www.",(C=t).length>=4&&C.substr(0,4)===b?t=i(e)+"://"+t:((e,t,o=0,r)=>{const a=e.indexOf(t,o);return-1!==a&&(!!n(r)||a+t.length<=r)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:v,url:t}}var C,b;return null},k=(e,n)=>{const{dom:o,selection:r}=e,{rng:a,url:s}=n,l=r.getBookmark();r.setRng(a);const i="createlink",u={command:i,ui:!1,value:s};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(i,!1,s),e.dispatch("ExecCommand",u);const n=c(e);if(t(n)){const t=r.getNode();o.setAttrib(t,"target",n),"_blank"!==n||d(e)||o.setAttrib(t,"rel","noopener")}}r.moveToBookmark(l),e.nodeChanged()},p=e=>{const t=m(e,-1);o(t)&&k(e,t)},y=p;e.add("autolink",(e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[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.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",(t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=m(e,0);o(t)&&k(e,t)})(e)})),e.on("keyup",(t=>{32===t.keyCode?p(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&y(e)}))})(e)}))}();