Compare commits

...

7 commits

Author SHA1 Message Date
the-djmaze cfbc47488a cleanHtml use allowedTags instead of disallowedTags and improved CSS handling 2024-09-16 14:37:26 +02:00
the-djmaze 05812c6be1 Updated custom Squire 2.3.2 2024-09-16 14:30:54 +02:00
the-djmaze 18452cc53c Also prevent error for #1733 2024-09-16 14:28:19 +02:00
the-djmaze e0236ea52d Resolve #1733 2024-09-16 14:25:09 +02:00
the-djmaze 6f4f6bfd03 Improved attempt for #1746 2024-09-16 13:49:59 +02:00
the-djmaze f8520c27a2 Bump version number 2024-09-16 10:17:26 +02:00
the-djmaze 61e7c00b62 Resolve #1746 2024-09-16 10:15:30 +02:00
9 changed files with 241 additions and 270 deletions

View file

@ -17,14 +17,34 @@ const
"'": '''
},
disallowedTags = [
'svg','script','title','link','base','meta',
'input','output','select','button','textarea',
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
// not supported by <template> element
// ,'html','head','body'
keepTagContent = 'form,button,data', // font
allowedTags = [
// Structural Elements:
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
'article','aside','header','footer','main','section',
'details','summary',
// List Elements
'dd','dl','dt','li','ol','ul',
// Text Formatting Elements
'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
'span','strong','sub','sup','time','u','var',
// Deprecated by HTML Standard
'acronym','big','center','dir','font','marquee',
'nobr','noembed','noframes','plaintext','rb','rtc','strike','tt',
// Media Elements
'img',//'picture','source',
// Table Elements
'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
// Disallowed but converted later
'style','xmp'
].join(','),
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG'
],
blockquoteSwitcher = () => {
SettingsUserStore.collapseBlockquotes() &&
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
@ -102,8 +122,10 @@ const
},
cleanCSS = source =>
source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+(;|$)/g, '')
source.trim()
.replace(/;\s*-[^;]+/g, '')
.replace(/^\s*-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+/g, '')
// Drop Microsoft Office style properties
// .replace(/mso-[^:;]+:[^;]+/gi, '')
,
@ -145,14 +167,14 @@ const
if (source) {
source = source
// strip comments
.replace(/\/\*[\s\S]*?\*\/|<!--|-->/gi, '')
// strip import statements
.replace(/@import .*?;/gi , '')
// strip keyframe statements
.replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, '');
.replace(/\/\*[\s\S]*?\*\//gi, '')
// strip MS Word comments
.replace(/<!--[\s\S]*?-->/gi, '');
// strip HTML
// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
// unified regex to match css & media queries together
let unified = /((\s*?(?:\/\*[\s\S]*?\*\/)?\s*?@media[\s\S]*?){([\s\S]*?)}\s*?})|(([\s\S]*?){([\s\S]*?)})/gi,
let unified = /(?:(\s*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
arr;
while (true) {
@ -161,7 +183,7 @@ const
break;
}
let selector = arr[arr[2] === undefined ? 5 : 2].split('\r\n').join('\n').trim()
let selector = arr[arr[2] === undefined ? 3 : 1].split('\r\n').join('\n').trim()
// Never have more than a single line break in a row
.replace(/\n+/, "\n")
// Remove :root and html
@ -173,13 +195,14 @@ const
css.push({
selector: selector,
type: 'media',
subStyles: parseCSS(arr[3] + '\n}') //recursively parse media query inner css
subStyles: parseCSS(arr[2] + '\n}') //recursively parse media query inner css
});
} else if (selector && !selector.includes('@')) {
// we have standard css
// ignores @import, @keyframe, @font-face statements
css.push({
selector: selector,
rules: cleanCSS(arr[6])
rules: cleanCSS(arr[4])
});
}
}
@ -258,9 +281,6 @@ export const
'abbr', 'scope',
// td
'colspan', 'rowspan', 'headers'
],
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG'
];
if (SettingsUserStore.allowStyles()) {
@ -307,13 +327,21 @@ export const
}
});
// https://github.com/the-djmaze/snappymail/issues/1125
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
tmpl.content.querySelectorAll(
disallowedTags
':not('+allowedTags+')'
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
).forEach(oElement => oElement.remove());
// https://github.com/the-djmaze/snappymail/issues/1125
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
/* // Is this slower or faster?
).forEach(oElement => {
if (!node || !node.contains(oElement)) {
oElement.remove();
node = oElement;
}
});
*/
// https://github.com/the-djmaze/snappymail/issues/1641
let body = tmpl.content.querySelector('.mail-body');
@ -346,6 +374,13 @@ export const
return;
}
if ('XMP' === name) {
const pre = createElement('pre');
pre.innerHTML = encodeHtml(oElement.innerHTML);
oElement.replaceWith(pre);
return;
}
// \MailSo\Base\HtmlUtils::ClearTags()
if ('none' == oStyle.display
|| 'hidden' == oStyle.visibility
@ -432,7 +467,7 @@ export const
}
// if (['CENTER','FORM'].includes(name)) {
if ('O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
return;
}

View file

@ -90,26 +90,23 @@ class SnappyMailHelper
}
*/
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
$isOIDC = \str_starts_with($aCredentials[2], 'oidc_login|');
try {
$ocSession = \OC::$server->getSession();
if ($ocSession->get('is_oidc')) {
$pwd = new \SnappyMail\SensitiveString($aCredentials[1]);
$oAccount = $oActions->LoginProcess($aCredentials[1], $pwd);
if ($oAccount) {
$oActions->SetSignMeToken($oAccount);
}
} else {
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
if ($oAccount && $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn) {
$oActions->SetSignMeToken($oAccount);
}
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
if (!$isOIDC && $oAccount
&& $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn
) {
$oActions->SetSignMeToken($oAccount);
}
} catch (\Throwable $e) {
// Login failure, reset password to prevent more attempts
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
\OC::$server->getSession()['snappymail-passphrase'] = '';
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
if (!$isOIDC) {
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
\OC::$server->getSession()['snappymail-passphrase'] = '';
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
}
}
}
}
@ -126,6 +123,32 @@ class SnappyMailHelper
}
}
// Check if OpenID Connect (OIDC) is enabled and used for login
// https://apps.nextcloud.com/apps/oidc_login
public static function isOIDCLogin() : bool
{
$config = \OC::$server->getConfig();
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
// Check if the OIDC Login app is enabled
if (\OC::$server->getAppManager()->isEnabledForUser('oidc_login')) {
// Check if session is an OIDC Login
$ocSession = \OC::$server->getSession();
if ($ocSession->get('is_oidc')) {
// IToken->getPassword() ???
if ($ocSession->get('oidc_access_token')) {
return true;
}
\SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
} else {
\SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
}
} else {
\SnappyMail\Log::debug('Nextcloud', 'OIDC login disabled');
}
}
return false;
}
private static function getLoginCredentials() : array
{
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
@ -151,18 +174,9 @@ class SnappyMailHelper
if ($ocSession['snappymail-nc-uid'] == $sUID) {
// If OpenID Connect (OIDC) is enabled and used for login, use this.
// https://apps.nextcloud.com/apps/oidc_login
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
if ($ocSession->get('is_oidc')) {
// IToken->getPassword() ???
if ($sAccessToken = $ocSession->get('oidc_access_token')) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
return [$sUID, $sEmail, $sAccessToken];
}
\SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
} else {
\SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
}
if (static::isOIDCLogin()) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
return [$sUID, $sEmail, "oidc_login|{$sUID}"];
}
// Only use the user's password in the current session if they have

View file

@ -4,8 +4,8 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Nextcloud',
VERSION = '2.37.0',
RELEASE = '2024-08-11',
VERSION = '2.37.1',
RELEASE = '2024-09-16',
CATEGORY = 'Integrations',
DESCRIPTION = 'Integrate with Nextcloud v20+',
REQUIRED = '2.36.2';
@ -90,32 +90,17 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
public function beforeLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void
{
// https://apps.nextcloud.com/apps/oidc_login
$config = \OC::$server->getConfig();
$oUser = \OC::$server->getUserSession()->getUser();
$sUID = $oUser->getUID();
$sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email');
$sPassword = $config->getUserValue($sUID, 'snappymail', 'passphrase')
?: $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
$bAccountDefinedExplicitly = ($sEmail && $sPassword) && $sEmail === $oSettings->username;
$sNcEmail = $oUser->getEMailAddress() ?: $oUser->getPrimaryEMailAddress();
// Only login with OIDC access token if
// it is enabled in config, the user is currently logged in with OIDC,
// the current snappymail account is the OIDC account and no account defined explicitly
if (\OC::$server->getConfig()->getAppValue('snappymail', 'snappymail-autologin-oidc', false)
&& \OC::$server->getSession()->get('is_oidc')
&& $sNcEmail === $oSettings->username
&& !$bAccountDefinedExplicitly
if ($oAccount instanceof \RainLoop\Model\MainAccount
&& \OCA\SnappyMail\Util\SnappyMailHelper::isOIDCLogin()
// && $oClient->supportsAuthType('OAUTHBEARER') // v2.28
&& \str_starts_with($oSettings->passphrase, 'oidc_login|')
) {
$sAccessToken = \OC::$server->getSession()->get('oidc_access_token');
if ($sAccessToken) {
$oSettings->passphrase = $sAccessToken;
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER');
}
// $oSettings->passphrase = \OC::$server->getSession()->get('snappymail-passphrase');
$oSettings->passphrase = \OC::$server->getSession()->get('oidc_access_token');
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER');
}
}

View file

@ -749,9 +749,14 @@ class MailClient
*/
public function MessageList(MessageListParams $oParams) : MessageCollection
{
if (0 > $oParams->iOffset || 0 > $oParams->iLimit || 999 < $oParams->iLimit) {
if (0 > $oParams->iOffset || 0 > $oParams->iLimit) {
throw new \ValueError;
}
if (10 > $oParams->iLimit) {
$oParams->iLimit = 10;
} else if (999 < $oParams->iLimit) {
$oParams->iLimit = 50;
}
$sSearch = \trim($oParams->sSearch);

View file

@ -62,6 +62,12 @@ input {
}
}
//input:invalid,
input:user-invalid {
background-color: var(--error-bg-clr, #f2dede);
border-color: var(--error-border-clr, #eed3d7);
color: var(--error-clr, #b94a48);
}
// Position radios and checkboxes better
input[type="radio"],

View file

@ -51,6 +51,7 @@ ko.utils = {
: node => node.cloneNode(true)),
setDomNodeChildren: (domNode, childNodes) => {
// domNode.replaceChildren(...childNodes);
ko.utils.emptyDomNode(domNode);
childNodes && domNode.append(...childNodes);
},
@ -2821,7 +2822,7 @@ ko.bindingHandlers['textInput'] = {
elementValueBeforeEvent = timeoutHandle = undefined;
var elementValue = element.value;
if (previousElementValue !== elementValue) {
if (element.checkValidity() && previousElementValue !== elementValue) {
// Provide a way for tests to know exactly which event was processed
previousElementValue = elementValue;
ko.expressionRewriting.writeValueToProperty(valueAccessor(), allBindings, 'textInput', elementValue);

View file

@ -53,8 +53,8 @@ var ba={};c.i.options={init:a=>{if(!a.matches("SELECT"))throw Error("options bin
e?f=b().map(c.C.M):0<=a.selectedIndex&&f.push(c.C.M(a.options[a.selectedIndex]));if(l){Array.isArray(l)||(l=[l]);var n=l.filter(m=>m??1)}var p=!1;l=k;d.has("optionsAfterRender")&&"function"==typeof d.get("optionsAfterRender")&&(l=(m,q)=>{k(m,q);c.u.I(d.get("optionsAfterRender"),null,[q[0],m!==ba?m:void 0])});c.g.Ab(a,n,(m,q,r)=>{r.length&&(f=r[0].selected?[c.C.M(r[0])]:[],p=!0);q=a.ownerDocument.createElement("option");m===ba?(c.g.Za(q),c.C.Fa(q,void 0)):(r=h(m,d.get("optionsValue"),m),c.C.Fa(q,c.g.h(r)),
m=h(m,d.get("optionsText"),r),c.g.Za(q,m));return[q]},{},l);n=f.length;(e?n&&b().length<n:n&&0<=a.selectedIndex?c.C.M(a.options[a.selectedIndex])!==f[0]:n||0<=a.selectedIndex)&&c.u.I(c.g.Db,null,[a,"change"]);c.u.Ca()&&c.j.notify(a,c.j.F);g&&20<Math.abs(g-a.scrollTop)&&(a.scrollTop=g)}};c.i.options.Wa=c.g.l.Z();c.i.style={update:(a,b)=>{c.g.K(c.g.h(b()||{}),(d,e)=>{e=c.g.h(e);if(null==e||!1===e)e="";if(/^--/.test(d))a.style.setProperty(d,e);else{d=d.replace(/-(\w)/g,(l,f)=>f.toUpperCase());var g=
a.style[d];a.style[d]=e;e===g||a.style[d]!=g||isNaN(e)||(a.style[d]=e+"px")}})}};c.i.submit={init:(a,b,d,e,g)=>{if("function"!=typeof b())throw Error("The value for a submit binding must be a function");a.addEventListener("submit",l=>{var f=b();try{var h=f.call(g.$data,a)}finally{!0!==h&&l.preventDefault()}})}};c.i.text={init:()=>({controlsDescendantBindings:!0}),update:(a,b)=>{8===a.nodeType&&(a.text||a.after(a.text=J.createTextNode("")),a=a.text);c.g.Za(a,b())}};c.m.aa.text=!0;c.i.textInput={init:(a,
b,d)=>{var e=a.value,g,l,f=()=>{clearTimeout(g);l=g=void 0;var k=a.value;e!==k&&(e=k,c.la.Ga(b(),d,"textInput",k))},h=()=>{var k=c.g.h(b())??"";void 0!==l&&k===l?setTimeout(h,4):a.value!==k&&(a.value=k,e=a.value)};a.addEventListener("input",f);a.addEventListener("change",f);c.o(h,{s:a})}};c.i.value={init:(a,b,d)=>{var e=a.matches("SELECT"),g=a.matches("INPUT");if(!g||"checkbox"!=a.type&&"radio"!=a.type){var l=new Set,f=d.get("valueUpdate"),h=null,k=()=>{h=null;var m=b(),q=c.C.M(a);c.la.Ga(m,d,"value",
q)};f&&("string"==typeof f?l.add(f):f.forEach(m=>l.add(m)),l.delete("change"));l.forEach(m=>{var q=k;(m||"").startsWith("after")&&(q=()=>{h=c.C.M(a);setTimeout(k,0)},m=m.slice(5));a.addEventListener(m,q)});var n=g&&"file"==a.type?()=>{var m=c.g.h(b());null==m||""===m?a.value="":c.u.I(k)}:()=>{var m=c.g.h(b()),q=c.C.M(a);if(null!==h&&m===h)setTimeout(n,0);else if(m!==q||void 0===q)e?(c.C.Fa(a,m),m!==c.C.M(a)&&c.u.I(k)):c.C.Fa(a,m)};if(e){var p;c.j.subscribe(a,c.j.F,()=>{p?d.get("valueAllowUnset")?
b,d)=>{var e=a.value,g,l,f=()=>{clearTimeout(g);l=g=void 0;var k=a.value;a.checkValidity()&&e!==k&&(e=k,c.la.Ga(b(),d,"textInput",k))},h=()=>{var k=c.g.h(b())??"";void 0!==l&&k===l?setTimeout(h,4):a.value!==k&&(a.value=k,e=a.value)};a.addEventListener("input",f);a.addEventListener("change",f);c.o(h,{s:a})}};c.i.value={init:(a,b,d)=>{var e=a.matches("SELECT"),g=a.matches("INPUT");if(!g||"checkbox"!=a.type&&"radio"!=a.type){var l=new Set,f=d.get("valueUpdate"),h=null,k=()=>{h=null;var m=b(),q=c.C.M(a);
c.la.Ga(m,d,"value",q)};f&&("string"==typeof f?l.add(f):f.forEach(m=>l.add(m)),l.delete("change"));l.forEach(m=>{var q=k;(m||"").startsWith("after")&&(q=()=>{h=c.C.M(a);setTimeout(k,0)},m=m.slice(5));a.addEventListener(m,q)});var n=g&&"file"==a.type?()=>{var m=c.g.h(b());null==m||""===m?a.value="":c.u.I(k)}:()=>{var m=c.g.h(b()),q=c.C.M(a);if(null!==h&&m===h)setTimeout(n,0);else if(m!==q||void 0===q)e?(c.C.Fa(a,m),m!==c.C.M(a)&&c.u.I(k)):c.C.Fa(a,m)};if(e){var p;c.j.subscribe(a,c.j.F,()=>{p?d.get("valueAllowUnset")?
n():k():(a.addEventListener("change",k),p=c.o(n,{s:a}))},null,{notifyImmediately:!0})}else a.addEventListener("change",k),c.o(n,{s:a})}else c.applyBindingAccessorsToNode(a,{checkedValue:b})},update:()=>{}};c.i.visible={update:(a,b)=>{b=c.g.h(b());var d="none"!=a.style.display;b&&!d?a.style.display="":d&&!b&&(a.style.display="none")}};c.i.hidden={update:(a,b)=>a.hidden=!!c.g.h(b())};(function(a){c.i[a]={init:function(b,d,e,g,l){return c.i.event.init.call(this,b,()=>({[a]:d()}),e,g,l)}}})("click");
(()=>{let a=c.g.l.Z();class b{constructor(e){this.Na=e}Ua(...e){let g=this.Na;if(!e.length)return c.g.l.get(g,a)||(11===this.H?g.content:1===this.H?g:void 0);c.g.l.set(g,a,e[0])}}class d extends b{constructor(e){super(e);e&&(this.H=e.matches("TEMPLATE")&&e.content?e.content.nodeType:1)}}c.bb={Na:d,lb:b}})();(()=>{const a=(h,k,n)=>{var p;for(k=c.m.nextSibling(k);h&&(p=h)!==k;)h=c.m.nextSibling(p),n(p,h)},b=(h,k)=>{if(h.length){var n=h[0],p=n.parentNode;a(n,h[h.length-1],m=>(1===m.nodeType||8===m.nodeType)&&
c.Ib(k,m));c.g.xa(h,p)}},d=(h,k,n,p)=>{var m=(h&&(h.nodeType?h:0<h.length?h[0]:null)||n||{}).ownerDocument;if("string"==typeof n){m=m||J;m=m.getElementById(n);if(!m)throw Error("Cannot find template with ID "+n);n=new c.bb.Na(m)}else if([1,8].includes(n.nodeType))n=new c.bb.lb(n);else throw Error("Unknown template type: "+n);n=(n=n.Ua?n.Ua():null)?[...n.cloneNode(!0).childNodes]:null;if(!Array.isArray(n)||0<n.length&&"number"!=typeof n[0].nodeType)throw Error("Template engine must return an array of DOM nodes");

View file

@ -10,7 +10,7 @@ ko.bindingHandlers['textInput'] = {
elementValueBeforeEvent = timeoutHandle = undefined;
var elementValue = element.value;
if (previousElementValue !== elementValue) {
if (element.checkValidity() && previousElementValue !== elementValue) {
// Provide a way for tests to know exactly which event was processed
previousElementValue = elementValue;
ko.expressionRewriting.writeValueToProperty(valueAccessor(), allBindings, 'textInput', elementValue);

View file

@ -6,26 +6,39 @@
var SHOW_ELEMENT_OR_TEXT = 5;
// source/node/TreeWalker.ts
var FILTER_ACCEPT = NodeFilter.FILTER_ACCEPT;
TreeWalker.prototype.previousPONode = function() {
const root = this.root;
let current = this.currentNode;
let node = current.lastChild;
while (!node && current) {
if (current === this.root) {
break;
let node;
while (true) {
node = current.lastChild;
while (!node && current) {
if (current === root) {
break;
}
node = current.previousSibling;
if (!node) {
current = current.parentNode;
}
}
node = this.previousSibling();
if (!node) {
current = this.parentNode();
return null;
}
const nodeType = node.nodeType;
const nodeFilterType = nodeType === Node.ELEMENT_NODE ? NodeFilter.SHOW_ELEMENT : nodeType === Node.TEXT_NODE ? NodeFilter.SHOW_TEXT : 0;
if (!!(nodeFilterType & this.whatToShow) && FILTER_ACCEPT === this.filter.acceptNode(node)) {
this.currentNode = node;
return node;
}
current = node;
}
node && (this.currentNode = node);
return node;
};
var createTreeWalker = (root, whatToShow, filter) => document.createTreeWalker(
root,
whatToShow,
{
acceptNode: (node) => !filter || filter(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
acceptNode: (node) => !filter || filter(node) ? FILTER_ACCEPT : NodeFilter.FILTER_SKIP
}
);
@ -46,7 +59,7 @@
// source/node/Category.ts
var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/;
var leafNodeNames = /* @__PURE__ */ new Set(["BR", "HR", "IFRAME", "IMG", "INPUT"]);
var leafNodeNames = /* @__PURE__ */ new Set(["BR", "HR", "IMG"]);
var UNKNOWN = 0;
var INLINE = 1;
var BLOCK = 2;
@ -55,9 +68,7 @@
var resetNodeCategoryCache = () => {
cache = /* @__PURE__ */ new WeakMap();
};
var isLeaf = (node) => {
return leafNodeNames.has(node.nodeName);
};
var isLeaf = (node) => leafNodeNames.has(node.nodeName);
var getNodeCategory = (node) => {
switch (node.nodeType) {
case TEXT_NODE:
@ -82,15 +93,9 @@
cache.set(node, nodeCategory);
return nodeCategory;
};
var isInline = (node) => {
return getNodeCategory(node) === INLINE;
};
var isBlock = (node) => {
return getNodeCategory(node) === BLOCK;
};
var isContainer = (node) => {
return getNodeCategory(node) === CONTAINER;
};
var isInline = (node) => getNodeCategory(node) === INLINE;
var isBlock = (node) => getNodeCategory(node) === BLOCK;
var isContainer = (node) => getNodeCategory(node) === CONTAINER;
// source/node/Node.ts
var createElement = (tag, props, children) => {
@ -161,31 +166,17 @@
}
return returnNode;
};
var getLength = (node) => {
return node instanceof Element || node instanceof DocumentFragment ? node.childNodes.length : node instanceof CharacterData ? node.length : 0;
};
var getLength = (node) => node instanceof Element || node instanceof DocumentFragment ? node.childNodes.length : node instanceof CharacterData ? node.length : 0;
var empty = (node) => {
const frag = document.createDocumentFragment();
let child = node.firstChild;
while (child) {
frag.append(child);
child = node.firstChild;
}
frag.append(...node.childNodes);
return frag;
};
var detach = (node) => {
const parent = node.parentNode;
if (parent) {
parent.removeChild(node);
}
node.parentNode?.removeChild(node);
return node;
};
var replaceWith = (node, node2) => {
const parent = node.parentNode;
if (parent) {
parent.replaceChild(node2, node);
}
};
var replaceWith = (node, node2) => node.parentNode?.replaceChild(node2, node);
var getClosest = (node, root, selector) => {
node = (node && !node.closest ? node.parentElement : node)?.closest(selector);
return node && root.contains(node) ? node : null;
@ -203,12 +194,10 @@
};
// source/node/Whitespace.ts
var notWSTextNode = (node) => {
return node instanceof Element ? node.nodeName === "BR" : (
// okay if data is 'undefined' here.
notWS.test(node.data)
);
};
var notWSTextNode = (node) => node instanceof Element ? node.nodeName === "BR" : (
// okay if data is 'undefined' here.
notWS.test(node.data)
);
var isLineBreak = (br, isLBIfEmptyBlock) => {
let block = br.parentNode;
while (isInline(block)) {
@ -291,7 +280,7 @@
const child = endContainer.childNodes[endOffset - 1];
if (!child || isLeaf(child)) {
if (child && child.nodeName === "BR" && !isLineBreak(child, false)) {
endOffset -= 1;
--endOffset;
continue;
}
break;
@ -335,7 +324,7 @@
break;
}
if (endContainer.nodeType !== TEXT_NODE && endContainer.childNodes[endOffset] && endContainer.childNodes[endOffset].nodeName === "BR" && !isLineBreak(endContainer.childNodes[endOffset], false)) {
endOffset += 1;
++endOffset;
}
if (endOffset !== getLength(endContainer)) {
break;
@ -400,32 +389,24 @@
};
var fixContainer = (container, root) => {
let wrapper = null;
Array.from(container.childNodes).forEach((child) => {
[...container.childNodes].forEach((child) => {
const isBR = child.nodeName === "BR";
if (!isBR && isInline(child)) {
if (!wrapper) {
wrapper = createElement("DIV");
}
if (!isBR && child.parentNode == root && isInline(child)) {
wrapper || (wrapper = createElement("DIV"));
wrapper.append(child);
} else if (isBR || wrapper) {
if (!wrapper) {
wrapper = createElement("DIV");
}
wrapper || (wrapper = createElement("DIV"));
fixCursor(wrapper);
if (isBR) {
container.replaceChild(wrapper, child);
child.replaceWith(wrapper);
} else {
container.insertBefore(wrapper, child);
}
wrapper = null;
}
if (isContainer(child)) {
fixContainer(child, root);
}
isContainer(child) && fixContainer(child, root);
});
if (wrapper) {
container.append(fixCursor(wrapper));
}
wrapper && container.append(fixCursor(wrapper));
return container;
};
var split = (node, offset, stopNode, root) => {
@ -454,7 +435,7 @@
}
fixCursor(node);
fixCursor(clone);
parent.insertBefore(clone, node.nextSibling);
node.after(clone);
return split(parent, clone, stopNode, root);
};
var _mergeInlines = (node, fakeRange) => {
@ -475,7 +456,7 @@
}
if (fakeRange.startContainer === node) {
if (fakeRange.startOffset > l) {
fakeRange.startOffset -= 1;
--fakeRange.startOffset;
} else if (fakeRange.startOffset === l) {
fakeRange.startContainer = prev;
fakeRange.startOffset = getLength(prev);
@ -483,7 +464,7 @@
}
if (fakeRange.endContainer === node) {
if (fakeRange.endOffset > l) {
fakeRange.endOffset -= 1;
--fakeRange.endOffset;
} else if (fakeRange.endOffset === l) {
fakeRange.endContainer = prev;
fakeRange.endOffset = getLength(prev);
@ -529,8 +510,8 @@
offset = block.childNodes.length;
const last = block.lastChild;
if (last && last.nodeName === "BR") {
block.removeChild(last);
offset -= 1;
last.remove();
--offset;
}
block.append(empty(next));
range.setStart(block, offset);
@ -546,23 +527,18 @@
}
if (prev && areAlike(prev, node)) {
if (!isContainer(prev)) {
if (isListItem) {
const block = createElement("DIV");
block.append(empty(prev));
prev.append(block);
} else {
if (!isListItem) {
return;
}
const block = createElement("DIV");
block.append(empty(prev));
prev.append(block);
}
detach(node);
const needsFix = !isContainer(node);
prev.append(empty(node));
if (needsFix) {
fixContainer(prev, root);
}
if (first) {
mergeContainers(first, root);
}
needsFix && fixContainer(prev, root);
first && mergeContainers(first, root);
} else if (isListItem) {
const block = createElement("DIV");
node.insertBefore(block, first);
@ -645,7 +621,7 @@
return (node, parent) => {
const el = createElement(tag);
const attributes = node.attributes;
for (let i = 0, l = attributes.length; i < l; i += 1) {
for (let i = 0, l = attributes.length; i < l; ++i) {
const attribute = attributes[i];
el.setAttribute(attribute.name, attribute.value);
}
@ -655,13 +631,15 @@
};
};
var fontSizes = {
"1": "10",
"2": "13",
"3": "16",
"4": "18",
"5": "24",
"6": "32",
"7": "48"
"1": "x-small",
"2": "small",
"3": "medium",
"4": "large",
"5": "x-large",
"6": "xx-large",
"7": "xxx-large",
"-1": "smaller",
"+1": "larger"
};
var stylesRewriters = {
STRONG: replaceWithTag("B"),
@ -674,62 +652,31 @@
const face = font.face;
const size = font.size;
let color = font.color;
const classNames = config.classNames;
let fontSpan;
let sizeSpan;
let colorSpan;
let newTreeBottom;
let newTreeTop;
let newTag = createElement("SPAN");
let css = newTag.style;
newTag.style.cssText = node.style.cssText;
if (face) {
fontSpan = createElement("SPAN", {
class: classNames.fontFamily,
style: "font-family:" + face
});
newTreeTop = fontSpan;
newTreeBottom = fontSpan;
css.fontFamily = face;
}
if (size) {
sizeSpan = createElement("SPAN", {
class: classNames.fontSize,
style: "font-size:" + fontSizes[size] + "px"
});
if (!newTreeTop) {
newTreeTop = sizeSpan;
}
if (newTreeBottom) {
newTreeBottom.append(sizeSpan);
}
newTreeBottom = sizeSpan;
css.fontSize = fontSizes[size];
}
if (color && /^#?([\dA-F]{3}){1,2}$/i.test(color)) {
if (color.charAt(0) !== "#") {
color = "#" + color;
}
colorSpan = createElement("SPAN", {
class: classNames.color,
style: "color:" + color
});
if (!newTreeTop) {
newTreeTop = colorSpan;
}
if (newTreeBottom) {
newTreeBottom.append(colorSpan);
}
newTreeBottom = colorSpan;
css.color = color;
}
if (!newTreeTop || !newTreeBottom) {
newTreeTop = newTreeBottom = createElement("SPAN");
}
parent.replaceChild(newTreeTop, font);
newTreeBottom.append(empty(font));
return newTreeBottom;
replaceWith(node, newTag);
newTag.append(empty(node));
return newTag;
},
TT: (node, parent, config) => {
const el = createElement("SPAN", {
class: config.classNames.fontFamily,
style: 'font-family:menlo,consolas,"courier new",monospace'
});
parent.replaceChild(el, node);
replaceWith(node, el);
el.append(empty(node));
return el;
}
@ -788,7 +735,7 @@
const brs = node.querySelectorAll("BR");
const brBreaksLine = [];
let l = brs.length;
for (let i = 0; i < l; i += 1) {
for (let i = 0; i < l; ++i) {
brBreaksLine[i] = isLineBreak(brs[i], keepForBlankLine);
}
while (l--) {
@ -869,7 +816,7 @@
let nodeAfterCursor;
if (startContainer instanceof Text) {
const text = startContainer.data;
for (let i = startOffset; i > 0; i -= 1) {
for (let i = startOffset; i > 0; --i) {
if (text.charAt(i - 1) !== ZWS) {
return false;
}
@ -906,7 +853,7 @@
if (endContainer instanceof Text) {
const text = endContainer.data;
const length = text.length;
for (let i = endOffset; i < length; i += 1) {
for (let i = endOffset; i < length; ++i) {
if (text.charAt(i) !== ZWS) {
return false;
}
@ -969,7 +916,7 @@
endOffset -= startOffset;
endContainer = afterSplit;
} else if (endContainer === parent) {
endOffset += 1;
++endOffset;
}
startContainer = afterSplit;
}
@ -1223,7 +1170,7 @@
let textContent = "";
let addedTextInBlock = false;
let value;
if (!(node instanceof Element) && !(node instanceof Text) || NodeFilter.FILTER_ACCEPT !== walker.filter.acceptNode(node)) {
if (!(node instanceof Element) && !(node instanceof Text) || FILTER_ACCEPT !== walker.filter.acceptNode(node)) {
node = walker.nextNode();
}
while (node) {
@ -2271,7 +2218,7 @@
);
let endOffset = Array.from(endContainer.childNodes).indexOf(end);
if (startContainer === endContainer) {
endOffset -= 1;
--endOffset;
}
start.remove();
end.remove();
@ -2509,7 +2456,7 @@
}
const html = this._getRawHTML();
if (replace) {
undoIndex -= 1;
--undoIndex;
}
if (undoThreshold > -1 && html.length * 2 > undoThreshold) {
if (undoLimit > -1 && undoIndex > undoLimit) {
@ -2520,15 +2467,13 @@
}
undoStack[undoIndex] = html;
this._undoIndex = undoIndex;
this._undoStackLength += 1;
++this._undoStackLength;
this._isInUndoState = true;
}
return this;
}
saveUndoState(range) {
if (!range) {
range = this.getSelection();
}
range || (range = this.getSelection());
this._recordUndoState(range, this._isInUndoState);
this._getRangeAndRemoveBookmark(range);
return this;
@ -2536,7 +2481,7 @@
undo() {
if (this._undoIndex !== 0 || !this._isInUndoState) {
this._recordUndoState(this.getSelection(), false);
this._undoIndex -= 1;
--this._undoIndex;
this._setRawHTML(this._undoStack[this._undoIndex]);
const range = this._getRangeAndRemoveBookmark();
if (range) {
@ -2555,7 +2500,7 @@
const undoIndex = this._undoIndex;
const undoStackLength = this._undoStackLength;
if (undoIndex + 1 < undoStackLength && this._isInUndoState) {
this._undoIndex += 1;
++this._undoIndex;
this._setRawHTML(this._undoStack[this._undoIndex]);
const range = this._getRangeAndRemoveBookmark();
if (range) {
@ -2577,23 +2522,15 @@
return this._root.innerHTML;
}
_setRawHTML(html) {
const root = this._root;
root.innerHTML = html;
let node = root;
const child = node.firstChild;
if (!child || child.nodeName === "BR") {
const block = this.createDefaultBlock();
if (child) {
node.replaceChild(block, child);
} else {
node.append(block);
}
} else {
while (node = getNextBlock(node, root)) {
if (html !== void 0) {
const root = this._root;
let node = root;
root.innerHTML = html;
do {
fixCursor(node);
}
} while (node = getNextBlock(node, root));
this._ignoreChange = true;
}
this._ignoreChange = true;
return this;
}
getHTML(withBookmark) {
@ -2798,7 +2735,7 @@
openBlock += " " + attr + '="' + escapeHTML(attributes[attr]) + '"';
}
openBlock += ">";
for (let i = 0, l = lines.length; i < l; i += 1) {
for (let i = 0, l = lines.length; i < l; ++i) {
let line = lines[i];
line = escapeHTML(line).replace(/ (?=(?: |$))/g, "&nbsp;");
if (i) {
@ -2839,22 +2776,22 @@
const color = style.color;
if (!fontInfo.color && color) {
fontInfo.color = color;
seenAttributes += 1;
++seenAttributes;
}
const backgroundColor = style.backgroundColor;
if (!fontInfo.backgroundColor && backgroundColor) {
fontInfo.backgroundColor = backgroundColor;
seenAttributes += 1;
++seenAttributes;
}
const fontFamily = style.fontFamily;
if (!fontInfo.fontFamily && fontFamily) {
fontInfo.fontFamily = fontFamily;
seenAttributes += 1;
++seenAttributes;
}
const fontSize = style.fontSize;
if (!fontInfo.fontSize && fontSize) {
fontInfo.fontSize = fontSize;
seenAttributes += 1;
++seenAttributes;
}
}
element = element.parentNode;
@ -2868,12 +2805,8 @@
*/
hasFormat(tag, attributes, range) {
tag = tag.toUpperCase();
if (!attributes) {
attributes = {};
}
if (!range) {
range = this.getSelection();
}
attributes || (attributes = {});
range || (range = this.getSelection());
if (!range.collapsed && range.startContainer instanceof Text && range.startOffset === range.startContainer.length && range.startContainer.nextSibling) {
range.setStartBefore(range.startContainer.nextSibling);
}
@ -2888,9 +2821,11 @@
if (common instanceof Text) {
return false;
}
const walker = createTreeWalker(common, SHOW_TEXT, (node2) => {
return isNodeContainedInRange(range, node2, true);
});
const walker = createTreeWalker(
common,
SHOW_TEXT,
(node2) => isNodeContainedInRange(range, node2, true)
);
let seenNode = false;
let node;
while (node = walker.nextNode()) {
@ -2949,7 +2884,7 @@
);
let { startContainer, startOffset, endContainer, endOffset } = range;
walker.currentNode = startContainer;
if (!(startContainer instanceof Element) && !(startContainer instanceof Text) || NodeFilter.FILTER_ACCEPT !== walker.filter.acceptNode(startContainer)) {
if (!(startContainer instanceof Element) && !(startContainer instanceof Text) || FILTER_ACCEPT !== walker.filter.acceptNode(startContainer)) {
const next = walker.nextNode();
if (!next) {
return range;
@ -2970,7 +2905,7 @@
endContainer = node;
endOffset -= startOffset;
} else if (endContainer === startContainer.parentNode) {
endOffset += 1;
++endOffset;
}
startContainer = node;
startOffset = 0;
@ -3041,33 +2976,23 @@
).filter((el) => {
return isNodeContainedInRange(range, el, true) && hasTagAttributes(el, tag, attributes);
});
if (!partial) {
formatTags.forEach((node) => {
examineNode(node, node);
});
}
partial || formatTags.forEach((node) => examineNode(node, node));
toWrap.forEach(([el, node]) => {
el = el.cloneNode(false);
replaceWith(node, el);
el.append(node);
});
formatTags.forEach((el) => {
replaceWith(el, empty(el));
});
formatTags.forEach((el) => replaceWith(el, empty(el)));
if (cantFocusEmptyTextNodes && fixer) {
fixer = fixer.parentNode;
let block = fixer;
while (block && isInline(block)) {
block = block.parentNode;
}
if (block) {
removeZWS(block, fixer);
}
block && removeZWS(block, fixer);
}
this._getRangeAndRemoveBookmark(range);
if (fixer) {
range.collapse(false);
}
fixer && range.collapse(false);
mergeInlines(root, range);
return range;
}
@ -3097,7 +3022,7 @@
let protocolEnd = url.indexOf(":") + 1;
if (protocolEnd) {
while (url[protocolEnd] === "/") {
protocolEnd += 1;
++protocolEnd;
}
}
insertNodeInRange(
@ -3574,13 +3499,13 @@
const lists = frag.querySelectorAll("UL, OL");
const items = frag.querySelectorAll("LI");
const root = this._root;
for (let i = 0, l = lists.length; i < l; i += 1) {
for (let i = 0, l = lists.length; i < l; ++i) {
const list = lists[i];
const listFrag = empty(list);
fixContainer(listFrag, root);
replaceWith(list, listFrag);
}
for (let i = 0, l = items.length; i < l; i += 1) {
for (let i = 0, l = items.length; i < l; ++i) {
const item = items[i];
if (isBlock(item)) {
replaceWith(item, this.createDefaultBlock([empty(item)]));
@ -3656,7 +3581,7 @@
let nodes = node.querySelectorAll("BR");
const brBreaksLine = [];
let l = nodes.length;
for (let i = 0; i < l; i += 1) {
for (let i = 0; i < l; ++i) {
brBreaksLine[i] = isLineBreak(nodes[i], false);
}
while (l--) {