@mdaemon/html-editor 1.1.0 → 1.2.0

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.
package/README.md CHANGED
@@ -103,6 +103,7 @@ const editor = new HTMLEditor(container, {
103
103
  | `entity_encoding` | 'raw' \| 'named' \| 'numeric' | 'raw' | HTML entity encoding mode |
104
104
  | `convert_unsafe_embeds` | boolean | true | Sanitize embedded content |
105
105
  | `format_empty_lines` | boolean | true | Format empty lines |
106
+ | `paste_from_office` | boolean | true | Clean and preserve formatting when pasting from Microsoft Word and Excel |
106
107
  | `setup` | (editor) => void | - | Callback invoked before init — use to register custom buttons |
107
108
 
108
109
  ### Toolbar Toggle
package/dist/index.d.ts CHANGED
@@ -138,6 +138,7 @@ export declare interface EditorConfig {
138
138
  quickbars_selection_toolbar?: string;
139
139
  quickbars_image_toolbar?: boolean;
140
140
  quickbars_insert_toolbar?: boolean;
141
+ paste_from_office?: boolean;
141
142
  browser_spellcheck?: boolean;
142
143
  elementpath?: boolean;
143
144
  entity_encoding?: 'raw' | 'named' | 'numeric';
@@ -373,6 +374,16 @@ export declare interface MDHTMLEditor {
373
374
 
374
375
  export declare const Mention: Node_2<any, any>;
375
376
 
377
+ export declare const PasteFromOffice: Extension<PasteFromOfficeOptions, any>;
378
+
379
+ export declare interface PasteFromOfficeOptions {
380
+ /**
381
+ * Enable/disable paste-from-office cleaning.
382
+ * When false, pasted content passes through unchanged.
383
+ */
384
+ enabled: boolean;
385
+ }
386
+
376
387
  /**
377
388
  * Reset the global translation function to the default identity function.
378
389
  * This also clears the customized flag so that the next editor created
package/dist/index.js CHANGED
@@ -44033,6 +44033,384 @@ const Mention = Node3.create({
44033
44033
  ];
44034
44034
  }
44035
44035
  });
44036
+ const WORD_MARKERS = [
44037
+ 'xmlns:o="urn:schemas-microsoft-com:office:office"',
44038
+ 'xmlns:w="urn:schemas-microsoft-com:office:word"',
44039
+ 'class="Mso',
44040
+ "class=Mso",
44041
+ 'style="mso-',
44042
+ "style='mso-"
44043
+ ];
44044
+ const EXCEL_MARKERS = [
44045
+ 'xmlns:x="urn:schemas-microsoft-com:office:excel"',
44046
+ "ProgId content=Excel.Sheet",
44047
+ 'ProgId content="Excel.Sheet"'
44048
+ ];
44049
+ function isWordContent(html) {
44050
+ return WORD_MARKERS.some((marker) => html.includes(marker));
44051
+ }
44052
+ function isExcelContent(html) {
44053
+ return EXCEL_MARKERS.some((marker) => html.includes(marker));
44054
+ }
44055
+ function isOfficeContent(html) {
44056
+ return isWordContent(html) || isExcelContent(html);
44057
+ }
44058
+ function removeConditionalComments(html) {
44059
+ return html.replace(/<!--\[if[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi, "");
44060
+ }
44061
+ const OFFICE_NS_TAG_PATTERN = /(?:<\/?)(?:o|w|v|m|x|st\d):/i;
44062
+ function removeOfficeElements(doc2) {
44063
+ const allElements = Array.from(doc2.body.querySelectorAll("*"));
44064
+ for (const el2 of allElements) {
44065
+ const tagName = el2.tagName.toLowerCase();
44066
+ if (OFFICE_NS_TAG_PATTERN.test(`<${tagName}`)) {
44067
+ const parent = el2.parentNode;
44068
+ if (parent) {
44069
+ while (el2.firstChild) {
44070
+ parent.insertBefore(el2.firstChild, el2);
44071
+ }
44072
+ parent.removeChild(el2);
44073
+ }
44074
+ }
44075
+ }
44076
+ const remaining = Array.from(doc2.body.querySelectorAll("*"));
44077
+ for (const el2 of remaining) {
44078
+ const attrs = Array.from(el2.attributes);
44079
+ for (const attr of attrs) {
44080
+ if (attr.name.startsWith("xmlns:") || attr.name === "xmlns") {
44081
+ el2.removeAttribute(attr.name);
44082
+ }
44083
+ }
44084
+ }
44085
+ }
44086
+ function parseListStyles(doc2) {
44087
+ const styleMap = /* @__PURE__ */ new Map();
44088
+ const styleEls = doc2.querySelectorAll("style");
44089
+ for (const styleEl of Array.from(styleEls)) {
44090
+ const css2 = styleEl.textContent ?? "";
44091
+ const listRulePattern = /@list\s+(l\d+):level(\d+)\s*\{([^}]*)\}/gi;
44092
+ let match;
44093
+ while ((match = listRulePattern.exec(css2)) !== null) {
44094
+ const listId = match[1];
44095
+ const level = match[2];
44096
+ const body = match[3];
44097
+ const key = `${listId}:level${level}`;
44098
+ const formatMatch = /mso-level-number-format:\s*([^;]+)/i.exec(body);
44099
+ if (formatMatch) {
44100
+ const format = formatMatch[1].trim().toLowerCase();
44101
+ styleMap.set(key, format !== "bullet");
44102
+ } else {
44103
+ styleMap.set(key, true);
44104
+ }
44105
+ }
44106
+ }
44107
+ return styleMap;
44108
+ }
44109
+ function parseListStyle(style2) {
44110
+ const match = /mso-list:\s*(l\d+)\s+level(\d+)\s+/i.exec(style2);
44111
+ if (!match) return null;
44112
+ return {
44113
+ listId: match[1],
44114
+ level: parseInt(match[2], 10),
44115
+ isOrdered: false
44116
+ // Will be resolved from style rules
44117
+ };
44118
+ }
44119
+ function convertWordLists(doc2) {
44120
+ const listStyles = parseListStyles(doc2);
44121
+ const listParagraphs = Array.from(doc2.body.querySelectorAll('[class*="MsoList"]'));
44122
+ if (listParagraphs.length === 0) return;
44123
+ const groups = [];
44124
+ let currentGroup = [];
44125
+ for (const p of listParagraphs) {
44126
+ const el2 = p;
44127
+ const style2 = el2.getAttribute("style") ?? "";
44128
+ if (!parseListStyle(style2)) continue;
44129
+ if (currentGroup.length > 0) {
44130
+ const lastEl = currentGroup[currentGroup.length - 1];
44131
+ let nextSibling = lastEl.nextElementSibling;
44132
+ while (nextSibling && nextSibling !== el2 && nextSibling.textContent?.trim() === "") {
44133
+ nextSibling = nextSibling.nextElementSibling;
44134
+ }
44135
+ if (nextSibling !== el2) {
44136
+ groups.push(currentGroup);
44137
+ currentGroup = [];
44138
+ }
44139
+ }
44140
+ currentGroup.push(el2);
44141
+ }
44142
+ if (currentGroup.length > 0) {
44143
+ groups.push(currentGroup);
44144
+ }
44145
+ for (const group of groups) {
44146
+ convertListGroup(doc2, group, listStyles);
44147
+ }
44148
+ }
44149
+ function convertListGroup(doc2, paragraphs, listStyles) {
44150
+ if (paragraphs.length === 0) return;
44151
+ const rootInfo = parseListStyle(paragraphs[0].getAttribute("style") ?? "");
44152
+ if (!rootInfo) return;
44153
+ const parent = paragraphs[0].parentNode;
44154
+ if (!parent) return;
44155
+ const rootKey = `${rootInfo.listId}:level${rootInfo.level}`;
44156
+ const isRootOrdered = listStyles.get(rootKey) ?? false;
44157
+ const rootList = doc2.createElement(isRootOrdered ? "ol" : "ul");
44158
+ const stack = [{ element: rootList, level: rootInfo.level }];
44159
+ for (const p of paragraphs) {
44160
+ const style2 = p.getAttribute("style") ?? "";
44161
+ const info = parseListStyle(style2);
44162
+ if (!info) continue;
44163
+ const key = `${info.listId}:level${info.level}`;
44164
+ const isOrdered = listStyles.get(key) ?? false;
44165
+ while (stack.length > 1 && stack[stack.length - 1].level >= info.level) {
44166
+ stack.pop();
44167
+ }
44168
+ if (info.level > stack[stack.length - 1].level) {
44169
+ const currentList = stack[stack.length - 1].element;
44170
+ let lastLi = currentList.lastElementChild;
44171
+ if (!lastLi || lastLi.tagName.toLowerCase() !== "li") {
44172
+ lastLi = doc2.createElement("li");
44173
+ currentList.appendChild(lastLi);
44174
+ }
44175
+ const subList = doc2.createElement(isOrdered ? "ol" : "ul");
44176
+ lastLi.appendChild(subList);
44177
+ stack.push({ element: subList, level: info.level });
44178
+ }
44179
+ const li = doc2.createElement("li");
44180
+ removeListMarkers(p);
44181
+ while (p.firstChild) {
44182
+ li.appendChild(p.firstChild);
44183
+ }
44184
+ const cleanedStyle = cleanMsoStyles(style2);
44185
+ if (cleanedStyle) {
44186
+ li.setAttribute("style", cleanedStyle);
44187
+ }
44188
+ stack[stack.length - 1].element.appendChild(li);
44189
+ }
44190
+ parent.insertBefore(rootList, paragraphs[0]);
44191
+ for (const p of paragraphs) {
44192
+ p.remove();
44193
+ }
44194
+ }
44195
+ function removeListMarkers(el2) {
44196
+ const spans = Array.from(el2.querySelectorAll("span"));
44197
+ for (const span of spans) {
44198
+ const style2 = span.getAttribute("style") ?? "";
44199
+ if (/mso-list:\s*Ignore/i.test(style2)) {
44200
+ span.remove();
44201
+ }
44202
+ }
44203
+ }
44204
+ const MSO_PROPERTY_PATTERN = /\bmso-[^:]+:[^;]+;?\s*/gi;
44205
+ const MSO_CLASS_PATTERN = /\bMso\w*|\bxl\d+/g;
44206
+ function cleanMsoStyles(style2) {
44207
+ let cleaned = style2.replace(MSO_PROPERTY_PATTERN, "");
44208
+ cleaned = cleaned.replace(/;\s*;/g, ";").replace(/^\s*;\s*/, "").replace(/\s*;\s*$/, "").trim();
44209
+ return cleaned || "";
44210
+ }
44211
+ function cleanFontFamily(value) {
44212
+ const first2 = value.split(",")[0].trim();
44213
+ return first2.replace(/^["']|["']$/g, "");
44214
+ }
44215
+ function cleanElementStyles(doc2) {
44216
+ const allElements = Array.from(doc2.body.querySelectorAll("*"));
44217
+ for (const el2 of allElements) {
44218
+ const className = el2.getAttribute("class");
44219
+ if (className) {
44220
+ const cleaned = className.replace(MSO_CLASS_PATTERN, "").trim();
44221
+ if (cleaned) {
44222
+ el2.setAttribute("class", cleaned);
44223
+ } else {
44224
+ el2.removeAttribute("class");
44225
+ }
44226
+ }
44227
+ const style2 = el2.getAttribute("style");
44228
+ if (style2) {
44229
+ let cleaned = cleanMsoStyles(style2);
44230
+ const fontFamilyMatch = /font-family:\s*([^;]+)/i.exec(cleaned);
44231
+ if (fontFamilyMatch) {
44232
+ const cleanedFont = cleanFontFamily(fontFamilyMatch[1]);
44233
+ cleaned = cleaned.replace(fontFamilyMatch[0], `font-family: ${cleanedFont}`);
44234
+ }
44235
+ if (cleaned) {
44236
+ el2.setAttribute("style", cleaned);
44237
+ } else {
44238
+ el2.removeAttribute("style");
44239
+ }
44240
+ }
44241
+ const attrsToRemove = Array.from(el2.attributes).filter(
44242
+ (attr) => attr.name.startsWith("v:") || attr.name.startsWith("o:") || attr.name === "lang"
44243
+ );
44244
+ for (const attr of attrsToRemove) {
44245
+ el2.removeAttribute(attr.name);
44246
+ }
44247
+ }
44248
+ }
44249
+ function removeEmptyWrappers(doc2) {
44250
+ let changed = true;
44251
+ while (changed) {
44252
+ changed = false;
44253
+ const spans = Array.from(doc2.body.querySelectorAll("span"));
44254
+ for (const span of spans) {
44255
+ if (span.attributes.length === 0) {
44256
+ const parent = span.parentNode;
44257
+ if (parent) {
44258
+ while (span.firstChild) {
44259
+ parent.insertBefore(span.firstChild, span);
44260
+ }
44261
+ parent.removeChild(span);
44262
+ changed = true;
44263
+ }
44264
+ }
44265
+ }
44266
+ }
44267
+ const emptyParas = Array.from(doc2.body.querySelectorAll("p"));
44268
+ for (const p of emptyParas) {
44269
+ if (p.innerHTML.trim() === "" && doc2.body.children.length > 1) {
44270
+ p.remove();
44271
+ }
44272
+ }
44273
+ }
44274
+ function normalizeExcelTables(doc2) {
44275
+ const tables = Array.from(doc2.body.querySelectorAll("table"));
44276
+ for (const table of tables) {
44277
+ removeExcelAttributes(table);
44278
+ const cells = Array.from(table.querySelectorAll("td, th"));
44279
+ for (const cell of cells) {
44280
+ removeExcelAttributes(cell);
44281
+ const style2 = cell.getAttribute("style");
44282
+ if (style2) {
44283
+ const cleaned = cleanMsoStyles(style2);
44284
+ if (cleaned) {
44285
+ cell.setAttribute("style", cleaned);
44286
+ } else {
44287
+ cell.removeAttribute("style");
44288
+ }
44289
+ }
44290
+ }
44291
+ const rows = Array.from(table.querySelectorAll("tr"));
44292
+ for (const row of rows) {
44293
+ removeExcelAttributes(row);
44294
+ }
44295
+ }
44296
+ }
44297
+ const EXCEL_ATTRS = [
44298
+ "x:num",
44299
+ "x:str",
44300
+ "x:fmla",
44301
+ "x:autofilter",
44302
+ "mso-number-format"
44303
+ ];
44304
+ function removeExcelAttributes(el2) {
44305
+ for (const attr of EXCEL_ATTRS) {
44306
+ el2.removeAttribute(attr);
44307
+ }
44308
+ const className = el2.getAttribute("class");
44309
+ if (className) {
44310
+ const cleaned = className.replace(MSO_CLASS_PATTERN, "").trim();
44311
+ if (cleaned) {
44312
+ el2.setAttribute("class", cleaned);
44313
+ } else {
44314
+ el2.removeAttribute("class");
44315
+ }
44316
+ }
44317
+ }
44318
+ const DANGEROUS_TAGS = [
44319
+ "script",
44320
+ "iframe",
44321
+ "object",
44322
+ "embed",
44323
+ "applet",
44324
+ "form",
44325
+ "input",
44326
+ "textarea",
44327
+ "select",
44328
+ "button"
44329
+ ];
44330
+ const EVENT_ATTR_PATTERN = /^on/i;
44331
+ const DANGEROUS_URL_PATTERN = /^\s*(javascript|vbscript|data\s*:(?!image))/i;
44332
+ function sanitize(doc2) {
44333
+ for (const tag of DANGEROUS_TAGS) {
44334
+ const elements = Array.from(doc2.body.querySelectorAll(tag));
44335
+ for (const el2 of elements) {
44336
+ el2.remove();
44337
+ }
44338
+ }
44339
+ const allElements = Array.from(doc2.body.querySelectorAll("*"));
44340
+ for (const el2 of allElements) {
44341
+ const attrs = Array.from(el2.attributes);
44342
+ for (const attr of attrs) {
44343
+ if (EVENT_ATTR_PATTERN.test(attr.name)) {
44344
+ el2.removeAttribute(attr.name);
44345
+ continue;
44346
+ }
44347
+ if ((attr.name === "href" || attr.name === "src" || attr.name === "action") && DANGEROUS_URL_PATTERN.test(attr.value)) {
44348
+ el2.removeAttribute(attr.name);
44349
+ }
44350
+ }
44351
+ const src = el2.getAttribute("src");
44352
+ if (src && src.startsWith("file://")) {
44353
+ el2.removeAttribute("src");
44354
+ }
44355
+ const href = el2.getAttribute("href");
44356
+ if (href && href.startsWith("file://")) {
44357
+ el2.removeAttribute("href");
44358
+ }
44359
+ }
44360
+ }
44361
+ function removeStyleBlocks(doc2) {
44362
+ const styles = Array.from(doc2.querySelectorAll("style"));
44363
+ for (const s of styles) {
44364
+ s.remove();
44365
+ }
44366
+ }
44367
+ function removeHeadElements(doc2) {
44368
+ const selectors = ["meta", "link", "title", "xml"];
44369
+ for (const selector of selectors) {
44370
+ const elements = Array.from(doc2.body.querySelectorAll(selector));
44371
+ for (const el2 of elements) {
44372
+ el2.remove();
44373
+ }
44374
+ }
44375
+ }
44376
+ function transformOfficeHTML(html) {
44377
+ if (!isOfficeContent(html)) {
44378
+ return html;
44379
+ }
44380
+ let cleaned = removeConditionalComments(html);
44381
+ const parser = new DOMParser();
44382
+ const doc2 = parser.parseFromString(cleaned, "text/html");
44383
+ removeHeadElements(doc2);
44384
+ if (isWordContent(html)) {
44385
+ convertWordLists(doc2);
44386
+ }
44387
+ removeOfficeElements(doc2);
44388
+ if (isExcelContent(html)) {
44389
+ normalizeExcelTables(doc2);
44390
+ }
44391
+ cleanElementStyles(doc2);
44392
+ removeStyleBlocks(doc2);
44393
+ removeEmptyWrappers(doc2);
44394
+ sanitize(doc2);
44395
+ return doc2.body.innerHTML;
44396
+ }
44397
+ const PasteFromOffice = Extension.create({
44398
+ name: "pasteFromOffice",
44399
+ addOptions() {
44400
+ return {
44401
+ enabled: true
44402
+ };
44403
+ },
44404
+ addStorage() {
44405
+ return {};
44406
+ },
44407
+ transformPastedHTML(html) {
44408
+ if (!this.options.enabled) {
44409
+ return html;
44410
+ }
44411
+ return transformOfficeHTML(html);
44412
+ }
44413
+ });
44036
44414
  const en = {
44037
44415
  "Bold": "Bold",
44038
44416
  "Italic": "Italic",
@@ -46403,7 +46781,8 @@ class HTMLEditor {
46403
46781
  convert_unsafe_embeds: config.convert_unsafe_embeds ?? true,
46404
46782
  format_empty_lines: config.format_empty_lines ?? true,
46405
46783
  toolbar_narrow_breakpoint: config.toolbar_narrow_breakpoint,
46406
- toolbar_priority: config.toolbar_priority
46784
+ toolbar_priority: config.toolbar_priority,
46785
+ paste_from_office: config.paste_from_office ?? true
46407
46786
  };
46408
46787
  }
46409
46788
  createEditor() {
@@ -46606,6 +46985,9 @@ class HTMLEditor {
46606
46985
  }),
46607
46986
  TextDirection
46608
46987
  ];
46988
+ if (this.config.paste_from_office !== false) {
46989
+ extensions.push(PasteFromOffice);
46990
+ }
46609
46991
  if (!this.config.basicEditor) {
46610
46992
  extensions.push(
46611
46993
  Image.configure({
@@ -46830,6 +47212,7 @@ exports.HTMLEditor = HTMLEditor;
46830
47212
  exports.LineHeight = LineHeight;
46831
47213
  exports.LinkEditor = LinkEditor;
46832
47214
  exports.Mention = Mention;
47215
+ exports.PasteFromOffice = PasteFromOffice;
46833
47216
  exports.SearchReplace = SearchReplace;
46834
47217
  exports.SignatureBlock = SignatureBlock;
46835
47218
  exports.SourceEditor = SourceEditor;
package/dist/index.mjs CHANGED
@@ -44031,6 +44031,384 @@ const Mention = Node3.create({
44031
44031
  ];
44032
44032
  }
44033
44033
  });
44034
+ const WORD_MARKERS = [
44035
+ 'xmlns:o="urn:schemas-microsoft-com:office:office"',
44036
+ 'xmlns:w="urn:schemas-microsoft-com:office:word"',
44037
+ 'class="Mso',
44038
+ "class=Mso",
44039
+ 'style="mso-',
44040
+ "style='mso-"
44041
+ ];
44042
+ const EXCEL_MARKERS = [
44043
+ 'xmlns:x="urn:schemas-microsoft-com:office:excel"',
44044
+ "ProgId content=Excel.Sheet",
44045
+ 'ProgId content="Excel.Sheet"'
44046
+ ];
44047
+ function isWordContent(html) {
44048
+ return WORD_MARKERS.some((marker) => html.includes(marker));
44049
+ }
44050
+ function isExcelContent(html) {
44051
+ return EXCEL_MARKERS.some((marker) => html.includes(marker));
44052
+ }
44053
+ function isOfficeContent(html) {
44054
+ return isWordContent(html) || isExcelContent(html);
44055
+ }
44056
+ function removeConditionalComments(html) {
44057
+ return html.replace(/<!--\[if[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi, "");
44058
+ }
44059
+ const OFFICE_NS_TAG_PATTERN = /(?:<\/?)(?:o|w|v|m|x|st\d):/i;
44060
+ function removeOfficeElements(doc2) {
44061
+ const allElements = Array.from(doc2.body.querySelectorAll("*"));
44062
+ for (const el2 of allElements) {
44063
+ const tagName = el2.tagName.toLowerCase();
44064
+ if (OFFICE_NS_TAG_PATTERN.test(`<${tagName}`)) {
44065
+ const parent = el2.parentNode;
44066
+ if (parent) {
44067
+ while (el2.firstChild) {
44068
+ parent.insertBefore(el2.firstChild, el2);
44069
+ }
44070
+ parent.removeChild(el2);
44071
+ }
44072
+ }
44073
+ }
44074
+ const remaining = Array.from(doc2.body.querySelectorAll("*"));
44075
+ for (const el2 of remaining) {
44076
+ const attrs = Array.from(el2.attributes);
44077
+ for (const attr of attrs) {
44078
+ if (attr.name.startsWith("xmlns:") || attr.name === "xmlns") {
44079
+ el2.removeAttribute(attr.name);
44080
+ }
44081
+ }
44082
+ }
44083
+ }
44084
+ function parseListStyles(doc2) {
44085
+ const styleMap = /* @__PURE__ */ new Map();
44086
+ const styleEls = doc2.querySelectorAll("style");
44087
+ for (const styleEl of Array.from(styleEls)) {
44088
+ const css2 = styleEl.textContent ?? "";
44089
+ const listRulePattern = /@list\s+(l\d+):level(\d+)\s*\{([^}]*)\}/gi;
44090
+ let match;
44091
+ while ((match = listRulePattern.exec(css2)) !== null) {
44092
+ const listId = match[1];
44093
+ const level = match[2];
44094
+ const body = match[3];
44095
+ const key = `${listId}:level${level}`;
44096
+ const formatMatch = /mso-level-number-format:\s*([^;]+)/i.exec(body);
44097
+ if (formatMatch) {
44098
+ const format = formatMatch[1].trim().toLowerCase();
44099
+ styleMap.set(key, format !== "bullet");
44100
+ } else {
44101
+ styleMap.set(key, true);
44102
+ }
44103
+ }
44104
+ }
44105
+ return styleMap;
44106
+ }
44107
+ function parseListStyle(style2) {
44108
+ const match = /mso-list:\s*(l\d+)\s+level(\d+)\s+/i.exec(style2);
44109
+ if (!match) return null;
44110
+ return {
44111
+ listId: match[1],
44112
+ level: parseInt(match[2], 10),
44113
+ isOrdered: false
44114
+ // Will be resolved from style rules
44115
+ };
44116
+ }
44117
+ function convertWordLists(doc2) {
44118
+ const listStyles = parseListStyles(doc2);
44119
+ const listParagraphs = Array.from(doc2.body.querySelectorAll('[class*="MsoList"]'));
44120
+ if (listParagraphs.length === 0) return;
44121
+ const groups = [];
44122
+ let currentGroup = [];
44123
+ for (const p of listParagraphs) {
44124
+ const el2 = p;
44125
+ const style2 = el2.getAttribute("style") ?? "";
44126
+ if (!parseListStyle(style2)) continue;
44127
+ if (currentGroup.length > 0) {
44128
+ const lastEl = currentGroup[currentGroup.length - 1];
44129
+ let nextSibling = lastEl.nextElementSibling;
44130
+ while (nextSibling && nextSibling !== el2 && nextSibling.textContent?.trim() === "") {
44131
+ nextSibling = nextSibling.nextElementSibling;
44132
+ }
44133
+ if (nextSibling !== el2) {
44134
+ groups.push(currentGroup);
44135
+ currentGroup = [];
44136
+ }
44137
+ }
44138
+ currentGroup.push(el2);
44139
+ }
44140
+ if (currentGroup.length > 0) {
44141
+ groups.push(currentGroup);
44142
+ }
44143
+ for (const group of groups) {
44144
+ convertListGroup(doc2, group, listStyles);
44145
+ }
44146
+ }
44147
+ function convertListGroup(doc2, paragraphs, listStyles) {
44148
+ if (paragraphs.length === 0) return;
44149
+ const rootInfo = parseListStyle(paragraphs[0].getAttribute("style") ?? "");
44150
+ if (!rootInfo) return;
44151
+ const parent = paragraphs[0].parentNode;
44152
+ if (!parent) return;
44153
+ const rootKey = `${rootInfo.listId}:level${rootInfo.level}`;
44154
+ const isRootOrdered = listStyles.get(rootKey) ?? false;
44155
+ const rootList = doc2.createElement(isRootOrdered ? "ol" : "ul");
44156
+ const stack = [{ element: rootList, level: rootInfo.level }];
44157
+ for (const p of paragraphs) {
44158
+ const style2 = p.getAttribute("style") ?? "";
44159
+ const info = parseListStyle(style2);
44160
+ if (!info) continue;
44161
+ const key = `${info.listId}:level${info.level}`;
44162
+ const isOrdered = listStyles.get(key) ?? false;
44163
+ while (stack.length > 1 && stack[stack.length - 1].level >= info.level) {
44164
+ stack.pop();
44165
+ }
44166
+ if (info.level > stack[stack.length - 1].level) {
44167
+ const currentList = stack[stack.length - 1].element;
44168
+ let lastLi = currentList.lastElementChild;
44169
+ if (!lastLi || lastLi.tagName.toLowerCase() !== "li") {
44170
+ lastLi = doc2.createElement("li");
44171
+ currentList.appendChild(lastLi);
44172
+ }
44173
+ const subList = doc2.createElement(isOrdered ? "ol" : "ul");
44174
+ lastLi.appendChild(subList);
44175
+ stack.push({ element: subList, level: info.level });
44176
+ }
44177
+ const li = doc2.createElement("li");
44178
+ removeListMarkers(p);
44179
+ while (p.firstChild) {
44180
+ li.appendChild(p.firstChild);
44181
+ }
44182
+ const cleanedStyle = cleanMsoStyles(style2);
44183
+ if (cleanedStyle) {
44184
+ li.setAttribute("style", cleanedStyle);
44185
+ }
44186
+ stack[stack.length - 1].element.appendChild(li);
44187
+ }
44188
+ parent.insertBefore(rootList, paragraphs[0]);
44189
+ for (const p of paragraphs) {
44190
+ p.remove();
44191
+ }
44192
+ }
44193
+ function removeListMarkers(el2) {
44194
+ const spans = Array.from(el2.querySelectorAll("span"));
44195
+ for (const span of spans) {
44196
+ const style2 = span.getAttribute("style") ?? "";
44197
+ if (/mso-list:\s*Ignore/i.test(style2)) {
44198
+ span.remove();
44199
+ }
44200
+ }
44201
+ }
44202
+ const MSO_PROPERTY_PATTERN = /\bmso-[^:]+:[^;]+;?\s*/gi;
44203
+ const MSO_CLASS_PATTERN = /\bMso\w*|\bxl\d+/g;
44204
+ function cleanMsoStyles(style2) {
44205
+ let cleaned = style2.replace(MSO_PROPERTY_PATTERN, "");
44206
+ cleaned = cleaned.replace(/;\s*;/g, ";").replace(/^\s*;\s*/, "").replace(/\s*;\s*$/, "").trim();
44207
+ return cleaned || "";
44208
+ }
44209
+ function cleanFontFamily(value) {
44210
+ const first2 = value.split(",")[0].trim();
44211
+ return first2.replace(/^["']|["']$/g, "");
44212
+ }
44213
+ function cleanElementStyles(doc2) {
44214
+ const allElements = Array.from(doc2.body.querySelectorAll("*"));
44215
+ for (const el2 of allElements) {
44216
+ const className = el2.getAttribute("class");
44217
+ if (className) {
44218
+ const cleaned = className.replace(MSO_CLASS_PATTERN, "").trim();
44219
+ if (cleaned) {
44220
+ el2.setAttribute("class", cleaned);
44221
+ } else {
44222
+ el2.removeAttribute("class");
44223
+ }
44224
+ }
44225
+ const style2 = el2.getAttribute("style");
44226
+ if (style2) {
44227
+ let cleaned = cleanMsoStyles(style2);
44228
+ const fontFamilyMatch = /font-family:\s*([^;]+)/i.exec(cleaned);
44229
+ if (fontFamilyMatch) {
44230
+ const cleanedFont = cleanFontFamily(fontFamilyMatch[1]);
44231
+ cleaned = cleaned.replace(fontFamilyMatch[0], `font-family: ${cleanedFont}`);
44232
+ }
44233
+ if (cleaned) {
44234
+ el2.setAttribute("style", cleaned);
44235
+ } else {
44236
+ el2.removeAttribute("style");
44237
+ }
44238
+ }
44239
+ const attrsToRemove = Array.from(el2.attributes).filter(
44240
+ (attr) => attr.name.startsWith("v:") || attr.name.startsWith("o:") || attr.name === "lang"
44241
+ );
44242
+ for (const attr of attrsToRemove) {
44243
+ el2.removeAttribute(attr.name);
44244
+ }
44245
+ }
44246
+ }
44247
+ function removeEmptyWrappers(doc2) {
44248
+ let changed = true;
44249
+ while (changed) {
44250
+ changed = false;
44251
+ const spans = Array.from(doc2.body.querySelectorAll("span"));
44252
+ for (const span of spans) {
44253
+ if (span.attributes.length === 0) {
44254
+ const parent = span.parentNode;
44255
+ if (parent) {
44256
+ while (span.firstChild) {
44257
+ parent.insertBefore(span.firstChild, span);
44258
+ }
44259
+ parent.removeChild(span);
44260
+ changed = true;
44261
+ }
44262
+ }
44263
+ }
44264
+ }
44265
+ const emptyParas = Array.from(doc2.body.querySelectorAll("p"));
44266
+ for (const p of emptyParas) {
44267
+ if (p.innerHTML.trim() === "" && doc2.body.children.length > 1) {
44268
+ p.remove();
44269
+ }
44270
+ }
44271
+ }
44272
+ function normalizeExcelTables(doc2) {
44273
+ const tables = Array.from(doc2.body.querySelectorAll("table"));
44274
+ for (const table of tables) {
44275
+ removeExcelAttributes(table);
44276
+ const cells = Array.from(table.querySelectorAll("td, th"));
44277
+ for (const cell of cells) {
44278
+ removeExcelAttributes(cell);
44279
+ const style2 = cell.getAttribute("style");
44280
+ if (style2) {
44281
+ const cleaned = cleanMsoStyles(style2);
44282
+ if (cleaned) {
44283
+ cell.setAttribute("style", cleaned);
44284
+ } else {
44285
+ cell.removeAttribute("style");
44286
+ }
44287
+ }
44288
+ }
44289
+ const rows = Array.from(table.querySelectorAll("tr"));
44290
+ for (const row of rows) {
44291
+ removeExcelAttributes(row);
44292
+ }
44293
+ }
44294
+ }
44295
+ const EXCEL_ATTRS = [
44296
+ "x:num",
44297
+ "x:str",
44298
+ "x:fmla",
44299
+ "x:autofilter",
44300
+ "mso-number-format"
44301
+ ];
44302
+ function removeExcelAttributes(el2) {
44303
+ for (const attr of EXCEL_ATTRS) {
44304
+ el2.removeAttribute(attr);
44305
+ }
44306
+ const className = el2.getAttribute("class");
44307
+ if (className) {
44308
+ const cleaned = className.replace(MSO_CLASS_PATTERN, "").trim();
44309
+ if (cleaned) {
44310
+ el2.setAttribute("class", cleaned);
44311
+ } else {
44312
+ el2.removeAttribute("class");
44313
+ }
44314
+ }
44315
+ }
44316
+ const DANGEROUS_TAGS = [
44317
+ "script",
44318
+ "iframe",
44319
+ "object",
44320
+ "embed",
44321
+ "applet",
44322
+ "form",
44323
+ "input",
44324
+ "textarea",
44325
+ "select",
44326
+ "button"
44327
+ ];
44328
+ const EVENT_ATTR_PATTERN = /^on/i;
44329
+ const DANGEROUS_URL_PATTERN = /^\s*(javascript|vbscript|data\s*:(?!image))/i;
44330
+ function sanitize(doc2) {
44331
+ for (const tag of DANGEROUS_TAGS) {
44332
+ const elements = Array.from(doc2.body.querySelectorAll(tag));
44333
+ for (const el2 of elements) {
44334
+ el2.remove();
44335
+ }
44336
+ }
44337
+ const allElements = Array.from(doc2.body.querySelectorAll("*"));
44338
+ for (const el2 of allElements) {
44339
+ const attrs = Array.from(el2.attributes);
44340
+ for (const attr of attrs) {
44341
+ if (EVENT_ATTR_PATTERN.test(attr.name)) {
44342
+ el2.removeAttribute(attr.name);
44343
+ continue;
44344
+ }
44345
+ if ((attr.name === "href" || attr.name === "src" || attr.name === "action") && DANGEROUS_URL_PATTERN.test(attr.value)) {
44346
+ el2.removeAttribute(attr.name);
44347
+ }
44348
+ }
44349
+ const src = el2.getAttribute("src");
44350
+ if (src && src.startsWith("file://")) {
44351
+ el2.removeAttribute("src");
44352
+ }
44353
+ const href = el2.getAttribute("href");
44354
+ if (href && href.startsWith("file://")) {
44355
+ el2.removeAttribute("href");
44356
+ }
44357
+ }
44358
+ }
44359
+ function removeStyleBlocks(doc2) {
44360
+ const styles = Array.from(doc2.querySelectorAll("style"));
44361
+ for (const s of styles) {
44362
+ s.remove();
44363
+ }
44364
+ }
44365
+ function removeHeadElements(doc2) {
44366
+ const selectors = ["meta", "link", "title", "xml"];
44367
+ for (const selector of selectors) {
44368
+ const elements = Array.from(doc2.body.querySelectorAll(selector));
44369
+ for (const el2 of elements) {
44370
+ el2.remove();
44371
+ }
44372
+ }
44373
+ }
44374
+ function transformOfficeHTML(html) {
44375
+ if (!isOfficeContent(html)) {
44376
+ return html;
44377
+ }
44378
+ let cleaned = removeConditionalComments(html);
44379
+ const parser = new DOMParser();
44380
+ const doc2 = parser.parseFromString(cleaned, "text/html");
44381
+ removeHeadElements(doc2);
44382
+ if (isWordContent(html)) {
44383
+ convertWordLists(doc2);
44384
+ }
44385
+ removeOfficeElements(doc2);
44386
+ if (isExcelContent(html)) {
44387
+ normalizeExcelTables(doc2);
44388
+ }
44389
+ cleanElementStyles(doc2);
44390
+ removeStyleBlocks(doc2);
44391
+ removeEmptyWrappers(doc2);
44392
+ sanitize(doc2);
44393
+ return doc2.body.innerHTML;
44394
+ }
44395
+ const PasteFromOffice = Extension.create({
44396
+ name: "pasteFromOffice",
44397
+ addOptions() {
44398
+ return {
44399
+ enabled: true
44400
+ };
44401
+ },
44402
+ addStorage() {
44403
+ return {};
44404
+ },
44405
+ transformPastedHTML(html) {
44406
+ if (!this.options.enabled) {
44407
+ return html;
44408
+ }
44409
+ return transformOfficeHTML(html);
44410
+ }
44411
+ });
44034
44412
  const en = {
44035
44413
  "Bold": "Bold",
44036
44414
  "Italic": "Italic",
@@ -46401,7 +46779,8 @@ class HTMLEditor {
46401
46779
  convert_unsafe_embeds: config.convert_unsafe_embeds ?? true,
46402
46780
  format_empty_lines: config.format_empty_lines ?? true,
46403
46781
  toolbar_narrow_breakpoint: config.toolbar_narrow_breakpoint,
46404
- toolbar_priority: config.toolbar_priority
46782
+ toolbar_priority: config.toolbar_priority,
46783
+ paste_from_office: config.paste_from_office ?? true
46405
46784
  };
46406
46785
  }
46407
46786
  createEditor() {
@@ -46604,6 +46983,9 @@ class HTMLEditor {
46604
46983
  }),
46605
46984
  TextDirection
46606
46985
  ];
46986
+ if (this.config.paste_from_office !== false) {
46987
+ extensions.push(PasteFromOffice);
46988
+ }
46607
46989
  if (!this.config.basicEditor) {
46608
46990
  extensions.push(
46609
46991
  Image.configure({
@@ -46829,6 +47211,7 @@ export {
46829
47211
  LineHeight,
46830
47212
  LinkEditor,
46831
47213
  Mention,
47214
+ PasteFromOffice,
46832
47215
  SearchReplace,
46833
47216
  SignatureBlock,
46834
47217
  SourceEditor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdaemon/html-editor",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A TinyMCE-compatible HTML editor built on TipTap",
5
5
  "homepage": "https://github.com/mdaemon-technologies/MDHTMLEditor",
6
6
  "repository": {
@@ -60,24 +60,24 @@
60
60
  },
61
61
  "license": "LGPL-3.0-or-later",
62
62
  "dependencies": {
63
- "@tiptap/core": "^3.23.1",
64
- "@tiptap/extension-character-count": "^3.23.1",
65
- "@tiptap/extension-code-block-lowlight": "^3.23.1",
66
- "@tiptap/extension-color": "^3.23.1",
67
- "@tiptap/extension-font-family": "^3.23.1",
68
- "@tiptap/extension-highlight": "^3.23.1",
69
- "@tiptap/extension-image": "^3.23.1",
70
- "@tiptap/extension-link": "^3.23.1",
71
- "@tiptap/extension-placeholder": "^3.23.1",
72
- "@tiptap/extension-table": "^3.23.1",
73
- "@tiptap/extension-table-cell": "^3.23.1",
74
- "@tiptap/extension-table-header": "^3.23.1",
75
- "@tiptap/extension-table-row": "^3.23.1",
76
- "@tiptap/extension-text-align": "^3.23.1",
77
- "@tiptap/extension-text-style": "^3.23.1",
78
- "@tiptap/extension-underline": "^3.23.1",
79
- "@tiptap/pm": "^3.23.1",
80
- "@tiptap/starter-kit": "^3.23.1",
63
+ "@tiptap/core": "^3.23.4",
64
+ "@tiptap/extension-character-count": "^3.23.4",
65
+ "@tiptap/extension-code-block-lowlight": "^3.23.4",
66
+ "@tiptap/extension-color": "^3.23.4",
67
+ "@tiptap/extension-font-family": "^3.23.4",
68
+ "@tiptap/extension-highlight": "^3.23.4",
69
+ "@tiptap/extension-image": "^3.23.4",
70
+ "@tiptap/extension-link": "^3.23.4",
71
+ "@tiptap/extension-placeholder": "^3.23.4",
72
+ "@tiptap/extension-table": "^3.23.4",
73
+ "@tiptap/extension-table-cell": "^3.23.4",
74
+ "@tiptap/extension-table-header": "^3.23.4",
75
+ "@tiptap/extension-table-row": "^3.23.4",
76
+ "@tiptap/extension-text-align": "^3.23.4",
77
+ "@tiptap/extension-text-style": "^3.23.4",
78
+ "@tiptap/extension-underline": "^3.23.4",
79
+ "@tiptap/pm": "^3.23.4",
80
+ "@tiptap/starter-kit": "^3.23.4",
81
81
  "lowlight": "^3.3.0"
82
82
  },
83
83
  "devDependencies": {