Files
auto_news_upload/browser/formatter.js
T
2026-10-31 16:15:04 +08:00

196 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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; }
}
});
})();