mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-10-23 13:45:06 +00:00
91 lines
4.6 KiB
JavaScript
91 lines
4.6 KiB
JavaScript
// Clipboard HTML is parsed in an inert document, never inserted into the live DOM.
|
|
export function isWordHtml(html) {
|
|
return /\bmso-|class=["']?Mso|urn:schemas-microsoft-com:office|Microsoft Word/i.test(html);
|
|
}
|
|
|
|
export function cleanWordHtml(html) {
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
doc.querySelectorAll('script,style,meta,link,iframe,object,embed,svg,math,form,input,button').forEach(node => node.remove());
|
|
const comments = doc.createTreeWalker(doc.body, NodeFilter.SHOW_COMMENT);
|
|
const remove = [];
|
|
while (comments.nextNode()) { remove.push(comments.currentNode); }
|
|
remove.forEach(node => node.remove());
|
|
|
|
doc.querySelectorAll('p').forEach(node => {
|
|
const level = (node.className || '').match(/\bMsoHeading([1-6])\b/i) || (node.getAttribute('style') || '').match(/mso-outline-level:\s*([1-6])\b/i);
|
|
if (!level) { return; }
|
|
const heading = doc.createElement('h' + level[1]);
|
|
while (node.firstChild) { heading.appendChild(node.firstChild); }
|
|
node.replaceWith(heading);
|
|
});
|
|
|
|
// Desktop Word exports list items as paragraphs with mso-list and marker spans.
|
|
const lists = new Map();
|
|
doc.querySelectorAll('p').forEach(node => {
|
|
const style = node.getAttribute('style') || '';
|
|
const match = style.match(/mso-list:\s*(l\d+)\s+level(\d+)\s+(lfo\d+)/i);
|
|
if (!match) { return; }
|
|
const marker = Array.from(node.querySelectorAll('span')).find(span => /mso-list:\s*ignore/i.test(span.getAttribute('style') || ''));
|
|
const label = marker ? marker.textContent.trim() : '';
|
|
const ordered = /^(?:\d+|[a-z]+)[.)]/i.test(label);
|
|
lists.set(node, { key: match[1] + match[3], level: Number(match[2]), type: ordered ? 'ol' : 'ul', start: /^\d+/.test(label) ? parseInt(label, 10) : 1 });
|
|
if (marker) { marker.remove(); }
|
|
});
|
|
const parents = new Set(Array.from(lists.keys()).map(node => node.parentNode));
|
|
parents.forEach(parent => {
|
|
let stack = [];
|
|
let key = '';
|
|
Array.from(parent.childNodes).forEach(node => {
|
|
if (node.nodeType === 3 && !node.textContent.trim()) { return; }
|
|
const item = lists.get(node);
|
|
if (!item) { stack = []; key = ''; return; }
|
|
if (key !== item.key) { stack = []; key = item.key; }
|
|
while (stack.length && stack[stack.length - 1].level > item.level) { stack.pop(); }
|
|
if (stack.length && stack[stack.length - 1].level === item.level && stack[stack.length - 1].type !== item.type) { stack.pop(); }
|
|
if (!stack.length || stack[stack.length - 1].level < item.level) {
|
|
const list = doc.createElement(item.type);
|
|
if (item.type === 'ol' && item.start > 1) { list.setAttribute('start', item.start); }
|
|
if (stack.length) { stack[stack.length - 1].last.appendChild(list); }
|
|
else { parent.insertBefore(list, node); }
|
|
stack.push({ level: item.level, type: item.type, list: list, last: null });
|
|
}
|
|
const li = doc.createElement('li');
|
|
const entry = stack[stack.length - 1];
|
|
entry.list.appendChild(li);
|
|
li.appendChild(node);
|
|
entry.last = li;
|
|
});
|
|
});
|
|
|
|
const allowed = new Set('p div br h1 h2 h3 h4 h5 h6 strong b em i u s strike del blockquote pre code ul ol li a table thead tbody tfoot tr td th hr'.split(' '));
|
|
Array.from(doc.body.querySelectorAll('*')).reverse().forEach(node => {
|
|
if (!node.parentNode) { return; }
|
|
const tag = node.tagName.toLowerCase();
|
|
// Convert meaningful inline formatting before stripping presentation attributes.
|
|
const marks = [];
|
|
const weight = node.style.fontWeight;
|
|
if (weight === 'bold' || Number(weight) >= 600) { marks.push('strong'); }
|
|
if (node.style.fontStyle === 'italic') { marks.push('em'); }
|
|
const decoration = node.style.textDecoration;
|
|
if (/underline/.test(decoration)) { marks.push('u'); }
|
|
if (/line-through/.test(decoration)) { marks.push('s'); }
|
|
marks.forEach(mark => {
|
|
const wrapper = doc.createElement(mark);
|
|
while (node.firstChild) { wrapper.appendChild(node.firstChild); }
|
|
node.appendChild(wrapper);
|
|
});
|
|
Array.from(node.attributes).forEach(attr => {
|
|
const name = attr.name.toLowerCase();
|
|
const value = attr.value.trim();
|
|
const safeLink = tag === 'a' && name === 'href' && /^(https?:\/\/|mailto:|tel:|\/|#)/i.test(value) && !/[\u0000-\u0020]/.test(value);
|
|
const span = /^(td|th)$/.test(tag) && /^(colspan|rowspan)$/.test(name) && /^[1-9]\d{0,2}$/.test(value);
|
|
const start = tag === 'ol' && name === 'start' && /^[1-9]\d{0,5}$/.test(value);
|
|
if (!safeLink && !span && !start) { node.removeAttribute(attr.name); }
|
|
});
|
|
if (!allowed.has(tag)) {
|
|
node.replaceWith(...Array.from(node.childNodes));
|
|
}
|
|
});
|
|
return doc.body.innerHTML;
|
|
}
|