@seafile/sea-email-editor 0.0.10-beta1 → 0.0.10-beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -149,9 +149,9 @@ const formatElementNodes = nodes => {
149
149
  return nodes;
150
150
  };
151
151
  const deserializeHtml = html => {
152
- const fragment = (0, _dom.sanitizeHTMLContent)(html);
153
- if (!fragment) return generateDefaultValue();
154
- const children = Array.from(fragment.childNodes);
152
+ const HTMLBody = (0, _dom.parseAndCleanHTML)(html);
153
+ if (!HTMLBody) return generateDefaultValue();
154
+ const children = Array.from(HTMLBody.childNodes);
155
155
  let nodes = [];
156
156
  nodes = deserializeElements(children, true);
157
157
  nodes = formatElementNodes(nodes);
@@ -13,7 +13,6 @@ const paragraphRule = (element, parseChild) => {
13
13
  nodeName,
14
14
  childNodes
15
15
  } = element;
16
- // article
17
16
  if ((nodeName === 'DIV' || nodeName === 'ARTICLE') && element.parentElement.nodeName !== 'LI') {
18
17
  if (childNodes.length === 0) {
19
18
  const node = {
package/dist/utils/dom.js CHANGED
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.addClass = addClass;
7
7
  exports.getTarget = exports.getEventClassName = exports.getDataAttr = exports.canUseDOM = exports.addClassName = void 0;
8
8
  exports.hasClass = hasClass;
9
- exports.isNearBottom = exports.isInputOrEditorActive = void 0;
9
+ exports.parseAndCleanHTML = exports.isNearBottom = exports.isInputOrEditorActive = void 0;
10
10
  exports.removeClass = removeClass;
11
- exports.sanitizeHTMLContent = exports.removeClassName = void 0;
11
+ exports.removeClassName = void 0;
12
12
  var _typeDetection = require("./type-detection");
13
13
  const canUseDOM = exports.canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
14
14
  const getEventClassName = e => {
@@ -132,27 +132,317 @@ const isNearBottom = function (element) {
132
132
  return distanceToBottom <= threshold;
133
133
  };
134
134
  exports.isNearBottom = isNearBottom;
135
- const removeCommentNodes = node => {
136
- for (let i = node.childNodes.length - 1; i >= 0; i--) {
137
- const child = node.childNodes[i];
135
+ const cleanWhiteSpace = function () {
136
+ let html = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
137
+ return html.replace(/\s*[\n\t\r]+\s*/g, '');
138
+ };
139
+ const removeInvalidNodes = root => {
140
+ if (!root || !root.querySelectorAll) return;
141
+ const elementsToRemove = root.querySelectorAll('style, title, script, link[rel="stylesheet"]');
142
+ elementsToRemove.forEach(element => {
143
+ element.remove();
144
+ });
145
+ for (let i = root.childNodes.length - 1; i >= 0; i--) {
146
+ const child = root.childNodes[i];
138
147
  if (child.nodeType === Node.COMMENT_NODE) {
139
- node.removeChild(child);
148
+ root.removeChild(child);
140
149
  } else if (child.nodeType === Node.ELEMENT_NODE) {
141
- removeCommentNodes(child);
150
+ removeInvalidNodes(child);
151
+ }
152
+ }
153
+ };
154
+ const stripCSSComments = function () {
155
+ let cssText = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
156
+ return cssText.replace(/\/\*[\s\S]*?\*\//g, '');
157
+ };
158
+ const splitByDelimiter = function () {
159
+ let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
160
+ let delimiter = arguments.length > 1 ? arguments[1] : undefined;
161
+ const parts = [];
162
+ let buffer = '';
163
+ let quote = '';
164
+ let roundDepth = 0;
165
+ let squareDepth = 0;
166
+ for (let i = 0; i < value.length; i++) {
167
+ const char = value[i];
168
+ const prevChar = value[i - 1];
169
+ if (quote) {
170
+ buffer += char;
171
+ if (char === quote && prevChar !== '\\') {
172
+ quote = '';
173
+ }
174
+ continue;
175
+ }
176
+ if (char === '"' || char === '\'') {
177
+ quote = char;
178
+ buffer += char;
179
+ continue;
180
+ }
181
+ if (char === '(') {
182
+ roundDepth += 1;
183
+ buffer += char;
184
+ continue;
185
+ }
186
+ if (char === ')') {
187
+ roundDepth = Math.max(0, roundDepth - 1);
188
+ buffer += char;
189
+ continue;
190
+ }
191
+ if (char === '[') {
192
+ squareDepth += 1;
193
+ buffer += char;
194
+ continue;
195
+ }
196
+ if (char === ']') {
197
+ squareDepth = Math.max(0, squareDepth - 1);
198
+ buffer += char;
199
+ continue;
142
200
  }
201
+ if (char === delimiter && roundDepth === 0 && squareDepth === 0) {
202
+ parts.push(buffer);
203
+ buffer = '';
204
+ continue;
205
+ }
206
+ buffer += char;
207
+ }
208
+ if (buffer) {
209
+ parts.push(buffer);
143
210
  }
211
+ return parts;
212
+ };
213
+ const findMatchingParenIndex = function () {
214
+ let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
215
+ let startIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
216
+ let quote = '';
217
+ let depth = 0;
218
+ for (let i = startIndex; i < value.length; i++) {
219
+ const char = value[i];
220
+ const prevChar = value[i - 1];
221
+ if (quote) {
222
+ if (char === quote && prevChar !== '\\') {
223
+ quote = '';
224
+ }
225
+ continue;
226
+ }
227
+ if (char === '"' || char === '\'') {
228
+ quote = char;
229
+ continue;
230
+ }
231
+ if (char === '(') {
232
+ depth += 1;
233
+ continue;
234
+ }
235
+ if (char === ')') {
236
+ depth -= 1;
237
+ if (depth === 0) {
238
+ return i;
239
+ }
240
+ }
241
+ }
242
+ return -1;
243
+ };
244
+ const getInlineStyleState = element => {
245
+ const properties = [];
246
+ for (let i = 0; i < element.style.length; i++) {
247
+ properties.push(element.style[i]);
248
+ }
249
+ return properties.reduce((state, property) => {
250
+ state[property] = {
251
+ value: element.style.getPropertyValue(property),
252
+ priority: element.style.getPropertyPriority(property),
253
+ specificity: 1000,
254
+ order: Number.MAX_SAFE_INTEGER
255
+ };
256
+ return state;
257
+ }, {});
258
+ };
259
+ const compareStylePriority = (currentStyle, nextStyle) => {
260
+ if (!currentStyle) return true;
261
+ const currentImportant = currentStyle.priority === 'important';
262
+ const nextImportant = nextStyle.priority === 'important';
263
+ if (currentImportant !== nextImportant) {
264
+ return nextImportant;
265
+ }
266
+ if (currentStyle.specificity !== nextStyle.specificity) {
267
+ return nextStyle.specificity > currentStyle.specificity;
268
+ }
269
+ return nextStyle.order >= currentStyle.order;
270
+ };
271
+ const applyStyleDeclarations = function (element) {
272
+ let declarationText = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
273
+ let metadata = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
274
+ let styleStateMap = arguments.length > 3 ? arguments[3] : undefined;
275
+ if (!element || !declarationText) return;
276
+ const styleState = styleStateMap.get(element) || getInlineStyleState(element);
277
+ splitByDelimiter(declarationText, ';').forEach(declaration => {
278
+ const separatorIndex = declaration.indexOf(':');
279
+ if (separatorIndex < 0) return;
280
+ const property = declaration.slice(0, separatorIndex).trim();
281
+ let value = declaration.slice(separatorIndex + 1).trim();
282
+ if (!property || !value) return;
283
+ let priority = '';
284
+ if (/!important\s*$/i.test(value)) {
285
+ priority = 'important';
286
+ value = value.replace(/\s*!important\s*$/i, '').trim();
287
+ }
288
+ const nextStyle = {
289
+ value,
290
+ priority,
291
+ specificity: metadata.specificity || 0,
292
+ order: metadata.order || 0
293
+ };
294
+ if (compareStylePriority(styleState[property], nextStyle)) {
295
+ styleState[property] = nextStyle;
296
+ }
297
+ });
298
+ styleStateMap.set(element, styleState);
299
+ };
300
+ const calculateSelectorSpecificity = function () {
301
+ let selector = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
302
+ const pseudoFunctionNames = ['is', 'not', 'has', 'where'];
303
+ let normalizedSelector = selector.replace(/::[\w-]+/g, ' ');
304
+ let pseudoSpecificity = 0;
305
+ pseudoFunctionNames.forEach(pseudoName => {
306
+ const token = `:${pseudoName}(`;
307
+ let tokenIndex = normalizedSelector.indexOf(token);
308
+ while (tokenIndex > -1) {
309
+ const contentStartIndex = tokenIndex + token.length - 1;
310
+ const contentEndIndex = findMatchingParenIndex(normalizedSelector, contentStartIndex);
311
+ if (contentEndIndex < 0) break;
312
+ const content = normalizedSelector.slice(contentStartIndex + 1, contentEndIndex);
313
+ if (pseudoName !== 'where') {
314
+ const specificityList = splitByDelimiter(content, ',').map(item => calculateSelectorSpecificity(item.trim())).filter(Boolean);
315
+ pseudoSpecificity += specificityList.length > 0 ? Math.max(...specificityList) : 0;
316
+ }
317
+ normalizedSelector = normalizedSelector.slice(0, tokenIndex) + ' ' + normalizedSelector.slice(contentEndIndex + 1);
318
+ tokenIndex = normalizedSelector.indexOf(token);
319
+ }
320
+ });
321
+ const idCount = (normalizedSelector.match(/#[\w-]+/g) || []).length;
322
+ const classCount = (normalizedSelector.match(/\.[\w-]+/g) || []).length;
323
+ const attributeCount = (normalizedSelector.match(/\[[^\]]+\]/g) || []).length;
324
+ const pseudoClassCount = (normalizedSelector.match(/:(?!:)[\w-]+(?:\([^)]*\))?/g) || []).length;
325
+ const elementSelector = normalizedSelector.replace(/#[\w-]+/g, ' ').replace(/\.[\w-]+/g, ' ').replace(/\[[^\]]+\]/g, ' ').replace(/:(?!:)[\w-]+(?:\([^)]*\))?/g, ' ').replace(/[>+~*]/g, ' ');
326
+ const typeCount = (elementSelector.match(/(^|\s)[a-zA-Z][\w-]*/g) || []).length;
327
+ return pseudoSpecificity + idCount * 100 + (classCount + attributeCount + pseudoClassCount) * 10 + typeCount;
328
+ };
329
+ const applyCollectedStyles = styleStateMap => {
330
+ styleStateMap.forEach((styleState, element) => {
331
+ const properties = Object.keys(styleState);
332
+ element.removeAttribute('style');
333
+ properties.forEach(property => {
334
+ const item = styleState[property];
335
+ if (!(item !== null && item !== void 0 && item.value)) return;
336
+ element.style.setProperty(property, item.value, item.priority);
337
+ });
338
+ });
339
+ };
340
+ const splitCSSBlocks = function () {
341
+ let cssText = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
342
+ const blocks = [];
343
+ let buffer = '';
344
+ let depth = 0;
345
+ let quote = '';
346
+ let roundDepth = 0;
347
+ for (let i = 0; i < cssText.length; i++) {
348
+ const char = cssText[i];
349
+ const prevChar = cssText[i - 1];
350
+ buffer += char;
351
+ if (quote) {
352
+ if (char === quote && prevChar !== '\\') {
353
+ quote = '';
354
+ }
355
+ continue;
356
+ }
357
+ if (char === '"' || char === '\'') {
358
+ quote = char;
359
+ continue;
360
+ }
361
+ if (char === '(') {
362
+ roundDepth += 1;
363
+ continue;
364
+ }
365
+ if (char === ')') {
366
+ roundDepth = Math.max(0, roundDepth - 1);
367
+ continue;
368
+ }
369
+ if (roundDepth > 0) {
370
+ continue;
371
+ }
372
+ if (char === '{') {
373
+ depth += 1;
374
+ continue;
375
+ }
376
+ if (char === '}') {
377
+ depth -= 1;
378
+ if (depth === 0) {
379
+ blocks.push(buffer.trim());
380
+ buffer = '';
381
+ }
382
+ }
383
+ }
384
+ return blocks;
385
+ };
386
+ const inlineCSSRules = function (root) {
387
+ let cssText = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
388
+ let styleStateMap = arguments.length > 2 ? arguments[2] : undefined;
389
+ let context = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {
390
+ order: 0
391
+ };
392
+ const normalizedCSS = stripCSSComments(cssText).trim();
393
+ if (!normalizedCSS) return;
394
+ splitCSSBlocks(normalizedCSS).forEach(block => {
395
+ const startIndex = block.indexOf('{');
396
+ const endIndex = block.lastIndexOf('}');
397
+ if (startIndex < 0 || endIndex < 0 || endIndex <= startIndex) return;
398
+ const selectorText = block.slice(0, startIndex).trim();
399
+ const declarationText = block.slice(startIndex + 1, endIndex).trim();
400
+ if (!selectorText || !declarationText) return;
401
+ if (selectorText.startsWith('@')) {
402
+ if (declarationText.includes('{')) {
403
+ inlineCSSRules(root, declarationText, styleStateMap, context);
404
+ }
405
+ return;
406
+ }
407
+ splitByDelimiter(selectorText, ',').forEach(selector => {
408
+ const normalizedSelector = selector.trim();
409
+ if (!normalizedSelector) return;
410
+ try {
411
+ const metadata = {
412
+ specificity: calculateSelectorSpecificity(normalizedSelector),
413
+ order: context.order++
414
+ };
415
+ root.querySelectorAll(normalizedSelector).forEach(element => {
416
+ applyStyleDeclarations(element, declarationText, metadata, styleStateMap);
417
+ });
418
+ } catch {
419
+ // Ignore selectors that cannot be queried, such as pseudo-elements.
420
+ }
421
+ });
422
+ });
423
+ };
424
+ const convertStyleToInlineStyle = parsed => {
425
+ const styleStateMap = new Map();
426
+ const context = {
427
+ order: 0
428
+ };
429
+ parsed.querySelectorAll('style').forEach(styleElement => {
430
+ inlineCSSRules(parsed, styleElement.textContent || '', styleStateMap, context);
431
+ });
432
+ applyCollectedStyles(styleStateMap);
144
433
  };
145
- const sanitizeHTMLContent = function () {
434
+ const parseAndCleanHTML = function () {
146
435
  let html = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
436
+ if (!html) return null;
147
437
  if ((0, _typeDetection.isNumber)(html)) {
148
438
  html = String(html);
149
439
  }
150
440
  if (!(0, _typeDetection.isString)(html)) return null;
151
- const sanitizedHTML = html.replace(/<!--([\s\S]*?)-->/g, '').replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '').replace(/<title\b[^>]*>[\s\S]*?<\/title>/gi, '').replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '').replace(/\s*[\n\t]\s*/g, '');
441
+ const sanitizedHTML = cleanWhiteSpace(html);
152
442
  const parsed = new DOMParser().parseFromString(sanitizedHTML, 'text/html');
443
+ convertStyleToInlineStyle(parsed);
153
444
  const body = parsed.body;
154
- body.querySelectorAll('style, title, script').forEach(el => el.remove());
155
- removeCommentNodes(body);
445
+ removeInvalidNodes(body);
156
446
  return body;
157
447
  };
158
- exports.sanitizeHTMLContent = sanitizeHTMLContent;
448
+ exports.parseAndCleanHTML = parseAndCleanHTML;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seafile/sea-email-editor",
3
- "version": "0.0.10-beta1",
3
+ "version": "0.0.10-beta2",
4
4
  "description": "A Slate-based rich email editor with HTML, markdown, tables, images, links, and block plugins.",
5
5
  "main": "dist/index.js",
6
6
  "dependencies": {