/** * 编辑器内容 ↔ 保存格式的转换。 * * 编辑器里把公式、注释渲染成可读的样子(方便写作时看到最终效果), * 但落库必须回到短代码,服务端才能稳定渲染。隐藏内容本身就是 * 短代码 / 前台结构,不需要转换。 * * 代码块例外:前台由 EnlighterJS 按「全局功能 → 代码高亮」统一高亮,认的是 * `pre > code`,并把源码里的换行当作分行;而 TinyMCE 把 `pre > code` 当成 * 行内边界,光标落不进去、输入会被拆成段落。因此编辑器里用裸 `
` 编辑,
 * 保存前套回 `pre > code` 并把 `
` 还原成换行符。 * * @package Jio */ const escapeAttr = (value: string): string => value.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); const decodeEntities = (value: string): string => { const area = document.createElement('textarea'); area.innerHTML = value; return area.value; }; /** * 读出代码块的源码行。 * * 两种形态都要认:编辑器里的裸 `
`(换行是 `
`),以及前台落库的 * `pre > code`(换行是文本里的 `\n`)。`` 只是容器,直接走进去读, * 不重复计数。 */ const readCodeLines = (pre: Element): string[] => { const lines: string[] = []; let current = ''; const flush = (): void => { lines.push(current); current = ''; }; const walk = (node: Node): void => { node.childNodes.forEach((child) => { if (child.nodeType === 3) { String(child.nodeValue || '') .replace(/ /g, ' ') .split('\n') .forEach((part, index) => { if (index > 0) { flush(); } current += part; }); return; } if (child.nodeType !== 1) { return; } if ((child as Element).nodeName === 'BR') { flush(); return; } walk(child); }); }; walk(pre); flush(); // 末尾的空行只是排版残留(裸 pre 收尾的
、落库时补的换行符)。 while (lines.length > 0 && lines[lines.length - 1].trim() === '') { lines.pop(); } return lines; }; /** * 编辑器里的裸 `
` → 前台的 `pre > code`。
 *
 * `
` 与文本里的换行都换成分行符:Enlighter 直接按文本切行,留着 `
` * 会把整块压成一行、长行溢出卡片。 */ const serializeCodeBlock = (pre: Element): void => { const lines = readCodeLines(pre); const code = document.createElement('code'); code.textContent = lines.length === 0 ? '' : `${lines.join('\n')}\n`; pre.replaceChildren(code); }; /** * 前台 `pre > code` → 编辑器里可直接输入的裸 `
`。
 */
const hydrateCodeBlock = (pre: Element): void => {
	const lines = readCodeLines(pre);
	pre.replaceChildren();
	if (lines.length === 0) {
		pre.appendChild(document.createElement('br'));
		return;
	}
	lines.forEach((line, index) => {
		if (index > 0) {
			pre.appendChild(document.createElement('br'));
		}
		if (line !== '') {
			pre.appendChild(document.createTextNode(line));
		}
	});
};

/**
 * 保存前:把编辑器里的可视化块换回短代码 / 纯标记。
 */
export function serializeBlocks(html: string): string {
	if (!html) {
		return html;
	}

	const container = document.createElement('div');
	container.innerHTML = html;

	// 代码块:裸 pre → 前台的 pre > code(换行写成换行符)。
	container.querySelectorAll('pre').forEach((pre) => serializeCodeBlock(pre));

	// 公式
	container.querySelectorAll('[data-jio-math-source]').forEach((node) => {
		const formula = node.getAttribute('data-jio-math-source') || '';
		const display = node.getAttribute('data-jio-math-display') !== '0';
		const shortcode = `[jio_math display="${display ? '1' : '0'}"]${formula}[/jio_math]`;
		const paragraph = node.closest('p');
		if (display && paragraph && paragraph.textContent?.trim() === node.textContent?.trim()) {
			paragraph.textContent = shortcode;
		} else {
			node.replaceWith(document.createTextNode(shortcode));
		}
	});

	// 注释
	container.querySelectorAll('[data-jio-footnote]').forEach((node) => {
		const note = node.getAttribute('data-jio-footnote') || '';
		node.replaceWith(document.createTextNode(`[jio_footnote]${note}[/jio_footnote]`));
	});

	return container.innerHTML;
}

/**
 * 打开已有内容时:把短代码 / 前台的代码块结构还原成编辑器里的可视化块。
 */
export function hydrateBlocks(html: string): string {
	if (!html) {
		return html;
	}

	const container = document.createElement('div');
	container.innerHTML = html;

	container.querySelectorAll('pre').forEach((pre) => hydrateCodeBlock(pre));

	return container.innerHTML
		.replace(
			/\[jio_math display="([01])"\]([\s\S]*?)\[\/jio_math\]/g,
			(_match, display: string, formula: string) =>
				display === '0'
					? `${typeset(formula)}`
					: `

${typeset(formula)}

` ) .replace( /\[jio_footnote\]([\s\S]*?)\[\/jio_footnote\]/g, (_match, note: string) => `${note}` ); } const typeset = (raw: string): string => { const escaped = raw.replace(/&/g, '&').replace(//g, '>'); return escaped .replace(/\^\{([^{}]*)\}|\^(\S)/gu, '$1$2') .replace(/_\{([^{}]*)\}|_(\S)/gu, '$1$2'); };