196 lines
10 KiB
JavaScript
196 lines
10 KiB
JavaScript
// Extracted from dww11-wechat-oneclick.user.js v1.0.9; package source is authoritative.
|
||
(() => {
|
||
'use strict';
|
||
const cleanText = value => String(value || '').replace(/\s+/g, ' ').trim();
|
||
function flattenParagraphs(body) {
|
||
const selector = 'p,div,section,article,figure,figcaption,h1,h2,h3,h4,h5,h6';
|
||
const marker = () => {
|
||
const br = body.ownerDocument.createElement('br');
|
||
br.dataset.dww11Boundary = '1'; return br;
|
||
};
|
||
for (const block of Array.from(body.querySelectorAll(selector)).reverse()) {
|
||
if (block.closest('table,ul,ol,blockquote')) continue;
|
||
block.replaceWith(marker(), ...block.childNodes, marker());
|
||
}
|
||
let previous = false, pending = [];
|
||
function finish() {
|
||
const actual = pending.filter(br => !br.hasAttribute('data-dww11-boundary'));
|
||
let retained = false;
|
||
for (const br of pending) {
|
||
if (!br.hasAttribute('data-dww11-boundary')) continue;
|
||
if (previous && !actual.length && !retained) { br.removeAttribute('data-dww11-boundary'); retained = true; }
|
||
else br.remove();
|
||
}
|
||
pending = [];
|
||
}
|
||
const walker = body.ownerDocument.createTreeWalker(body, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
||
const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode);
|
||
for (const node of nodes) {
|
||
if (node.nodeType === 1 && node.tagName === 'BR') pending.push(node);
|
||
else if ((node.nodeType === 3 && node.textContent.trim()) || (node.nodeType === 1 && ['IMG', 'VIDEO', 'HR', 'IFRAME'].includes(node.tagName))) {
|
||
finish(); previous = true;
|
||
}
|
||
}
|
||
// 末尾不保留由容器边界制造的额外换行。
|
||
for (const br of pending) if (br.hasAttribute('data-dww11-boundary')) br.remove();
|
||
}
|
||
function spaceImages(body) {
|
||
let previous = null, gapNodes = [], blankLines = 0;
|
||
const changes = [];
|
||
function content(kind, node) {
|
||
if (previous && previous.kind !== kind) {
|
||
let anchor = node;
|
||
while (anchor.parentElement && anchor.parentElement !== body &&
|
||
!['TD', 'TH', 'LI', 'BLOCKQUOTE'].includes(anchor.parentElement.tagName) && !anchor.parentElement.contains(previous.node)) anchor = anchor.parentElement;
|
||
changes.push({ anchor, nodes: gapNodes.slice(), count: Math.max(2, blankLines) });
|
||
}
|
||
previous = { kind, node }; gapNodes = []; blankLines = 0;
|
||
}
|
||
function visit(node) {
|
||
if (node.nodeType === 3) {
|
||
if (node.textContent.trim()) content('text', node); else gapNodes.push(node);
|
||
return;
|
||
}
|
||
if (node.nodeType !== 1 || ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.tagName)) return;
|
||
if (node.tagName === 'BR') { gapNodes.push(node); blankLines++; return; }
|
||
if (node.tagName === 'IMG') { content('image', node); return; }
|
||
for (const child of node.childNodes) visit(child);
|
||
}
|
||
for (const child of body.childNodes) visit(child);
|
||
for (const { anchor, nodes, count } of changes) {
|
||
for (const node of nodes) node.remove();
|
||
const fragment = body.ownerDocument.createDocumentFragment();
|
||
for (let i = 0; i < count; i++) fragment.append(body.ownerDocument.createElement('br'));
|
||
anchor.parentNode.insertBefore(fragment, anchor);
|
||
}
|
||
}
|
||
function formatBody(body, author, sourceText = null) {
|
||
const doc = body.ownerDocument;
|
||
body.querySelectorAll('[data-dww11-wx-source="1"]').forEach(el => el.remove());
|
||
// Discuz 会把内联 color 值直接转为 [color=…]。
|
||
// !important 不能出现在该值中;颜色和字号只用原生 FONT 属性。
|
||
for (const el of [body, ...body.querySelectorAll('*')]) {
|
||
if (['SCRIPT', 'STYLE'].includes(el.tagName)) continue;
|
||
for (const property of ['font', 'font-size', 'color', '-webkit-text-fill-color']) {
|
||
el.style.removeProperty(property);
|
||
}
|
||
// 其他排版属性也不保留 CSS 优先级,避免它泄漏进论坛标签。
|
||
for (const property of Array.from(el.style)) {
|
||
if (el.style.getPropertyPriority(property)) {
|
||
el.style.setProperty(property, el.style.getPropertyValue(property));
|
||
}
|
||
}
|
||
if (!el.style.cssText) el.removeAttribute('style');
|
||
}
|
||
// 去掉采集器或旧版脚本生成的嵌套 FONT,保留其中的内容和图片。
|
||
for (const font of Array.from(body.querySelectorAll('font'))) {
|
||
font.replaceWith(...font.childNodes);
|
||
}
|
||
flattenParagraphs(body);
|
||
spaceImages(body);
|
||
// 所有正文内容和换行放在同一 FONT 中。不能使用只有 BR 的空 FONT:
|
||
// 论坛转换器可能清除空格式标签,导致空行退回默认1号或直接丢失。
|
||
const articleFont = doc.createElement('font');
|
||
articleFont.size = '4'; articleFont.color = '#000000';
|
||
articleFont.dataset.dww11WxBody = '1';
|
||
articleFont.append(...body.childNodes);
|
||
body.append(articleFont);
|
||
// A 自身的浏览器默认蓝色会覆盖外层继承色,链接文字另设黑色。
|
||
for (const link of articleFont.querySelectorAll('a')) {
|
||
const linkFont = doc.createElement('font');
|
||
linkFont.size = '4'; linkFont.color = '#000000';
|
||
linkFont.append(...link.childNodes); link.append(linkFont);
|
||
}
|
||
// 只用于编辑器图片适配;帖子展示仍交给网站原有图片/附件规则。
|
||
for (const img of articleFont.querySelectorAll('img')) {
|
||
img.style.maxWidth = '100%'; img.style.height = 'auto';
|
||
}
|
||
const source = doc.createElement('p');
|
||
source.dataset.dww11WxSource = '1';
|
||
const font = doc.createElement('font');
|
||
font.size = '1'; font.color = '#c0c0c0';
|
||
const lines = (sourceText ?? `来源:${author},如有侵权,请联系删除。`).split('\n');
|
||
lines.forEach((line, index) => {
|
||
if (index) font.append(doc.createElement('br'));
|
||
font.append(doc.createTextNode(line));
|
||
});
|
||
source.append(font); body.append(source);
|
||
}
|
||
function existingSource(body) {
|
||
// 按段落和 BR 读取末尾逻辑行,兼容嵌套 FONT、纯文本及末尾空段落。
|
||
const lines = [];
|
||
let parts = [];
|
||
function flush() {
|
||
const text = cleanText(parts.map(part => part.node.textContent.slice(part.start, part.end)).join(''));
|
||
if (text) lines.push({ text, parts });
|
||
parts = [];
|
||
}
|
||
function visit(node) {
|
||
if (node.nodeType === 3) {
|
||
// 保留文本中的显式换行,并记录精确位置,避免删除同容器中的正文。
|
||
const value = node.textContent;
|
||
let start = 0;
|
||
for (let i = 0; i <= value.length; i++) {
|
||
if (i !== value.length && value[i] !== '\n') continue;
|
||
// HTML 缩进换行通常只是空白;只有后续是来源或版权行才分行。
|
||
if (i < value.length && !/^\s*(?:(?:来源|文章来源|本文来源|图文来源|文字来源|作者|作者信息|来源作者|转自|转载自|出处)\s*[::]|如有侵权|若有侵权|侵权请|转载请|版权归)/.test(value.slice(i + 1))) continue;
|
||
if (i > start) parts.push({ node, start, end: i });
|
||
if (i < value.length) flush();
|
||
start = i + 1;
|
||
}
|
||
return;
|
||
}
|
||
if (node.nodeType !== 1 || ['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.tagName)) return;
|
||
if (node.tagName === 'BR') { flush(); return; }
|
||
if (['IMG', 'VIDEO', 'AUDIO', 'IFRAME', 'HR'].includes(node.tagName)) {
|
||
flush(); lines.push({ text: '\ufffc', parts: [] }); return;
|
||
}
|
||
const block = /^(P|DIV|SECTION|ARTICLE|FIGURE|FIGCAPTION|H[1-6]|LI|TR|BLOCKQUOTE|PRE)$/.test(node.tagName);
|
||
if (block) flush();
|
||
for (const child of node.childNodes) visit(child);
|
||
if (block) flush();
|
||
}
|
||
for (const child of body.childNodes) visit(child);
|
||
flush();
|
||
let first = -1;
|
||
for (let i = lines.length - 1; i >= 0; i--) {
|
||
if (/^(?:来源|文章来源|本文来源|图文来源|文字来源|作者|作者信息|来源作者|转自|转载自|出处)\s*[::]\s*\S/.test(lines[i].text)) {
|
||
first = i;
|
||
} else if (!/^(?:如有侵权|若有侵权|侵权请|转载请|版权归)/.test(lines[i].text)) {
|
||
break;
|
||
}
|
||
}
|
||
if (first < 0) return null;
|
||
const start = lines[first].parts[0];
|
||
const range = body.ownerDocument.createRange();
|
||
range.setStart(start.node, start.start);
|
||
range.setEnd(body, body.childNodes.length);
|
||
return {
|
||
text: lines.slice(first).map(line => line.text).join('\n'),
|
||
remove: () => range.deleteContents()
|
||
};
|
||
}
|
||
|
||
window.DWW11Formatter = Object.freeze({
|
||
format(expectedSource) {
|
||
if (Number(window.wysiwyg) !== 1) throw new Error('请切换到可视化编辑器后排版');
|
||
const body = document.querySelector('#e_iframe')?.contentDocument?.body;
|
||
if (!body) throw new Error('编辑器未就绪');
|
||
const footer = existingSource(body);
|
||
const expected = `来源:${expectedSource},如有侵权,请联系删除。`;
|
||
if (!footer || footer.text !== expected) throw new Error('文末来源与文章包不一致,请人工检查');
|
||
const images = [...body.querySelectorAll('img')].map(x => x.id + ':' + x.getAttribute('src'));
|
||
const backup = body.innerHTML;
|
||
try {
|
||
footer.remove();
|
||
formatBody(body, expectedSource, expected);
|
||
const after = [...body.querySelectorAll('img')].map(x => x.id + ':' + x.getAttribute('src'));
|
||
if (JSON.stringify(images) !== JSON.stringify(after)) throw new Error('排版后图片标识或顺序改变');
|
||
if (body.querySelectorAll('[data-dww11-wx-source="1"]').length !== 1) throw new Error('来源尾注数量异常');
|
||
body.dispatchEvent(new Event('input', {bubbles:true}));
|
||
return {images: after, source: expected};
|
||
} catch (error) { body.innerHTML = backup; throw error; }
|
||
}
|
||
});
|
||
})();
|