Files
wordpress-theme-aksrt/assets/creator-studio/src/utils/serialize.ts
T
kate522 ec582ac9f9 fix(submit): repair placeholder, code block, hidden block and settings toggles
- placeholder: hide the body ::before hint once the editor body has focus, so
  clicking an empty document no longer leaves the hint sitting under the caret
- code block: edit as a bare <pre> and convert to pre > code on save. TinyMCE
  treats pre > code as an inline boundary, so typing there leaked into the
  following paragraph; Enlighter also splits lines on newlines, so the <br>
  the editor produced rendered the whole block as one overflowing line
- code block: align the iframe rendering with Enlighter (Menlo 13px/1.4,
  #f8f9fa, 8px radius, 12px 17px padding, nowrap) in both content CSS files
- code block: move the caret into the block after insertion
- hidden block: hang the block-delete overlay on documentElement instead of
  body. data-mce-bogus only skips serialization, so the extra body child made
  mceInsertContent wipe the document and drop the first hidden block
- settings: give the toggle a real 44x24 hit area, align it to the top of the
  row, add aria-labels and show the disabled state
- styles: drop the duplicated @use/@import of publish-drawer and block-tools
- media insert/embed now serialize through serializeBlocks so code blocks are
  never stored in editor form
2026-11-10 16:04:49 +08:00

179 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 编辑器内容 ↔ 保存格式的转换。
*
* 编辑器里把公式、注释渲染成可读的样子(方便写作时看到最终效果),
* 但落库必须回到短代码,服务端才能稳定渲染。隐藏内容本身就是
* 短代码 / 前台结构,不需要转换。
*
* 代码块例外:前台由 EnlighterJS 按「全局功能 → 代码高亮」统一高亮,认的是
* `pre > code`,并把源码里的换行当作分行;而 TinyMCE 把 `pre > code` 当成
* 行内边界,光标落不进去、输入会被拆成段落。因此编辑器里用裸 `<pre>` 编辑,
* 保存前套回 `pre > code` 并把 `<br>` 还原成换行符。
*
* @package Jio
*/
const escapeAttr = (value: string): string =>
value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const decodeEntities = (value: string): string => {
const area = document.createElement('textarea');
area.innerHTML = value;
return area.value;
};
/**
* 读出代码块的源码行。
*
* 两种形态都要认:编辑器里的裸 `<pre>`(换行是 `<br>`),以及前台落库的
* `pre > code`(换行是文本里的 `\n`)。`<code>` 只是容器,直接走进去读,
* 不重复计数。
*/
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 收尾的 <br>、落库时补的换行符)。
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
lines.pop();
}
return lines;
};
/**
* 编辑器里的裸 `<pre>` → 前台的 `pre > code`。
*
* `<br>` 与文本里的换行都换成分行符:Enlighter 直接按文本切行,留着 `<br>`
* 会把整块压成一行、长行溢出卡片。
*/
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` → 编辑器里可直接输入的裸 `<pre>`。
*/
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<HTMLElement>('[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<HTMLElement>('[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'
? `<span class="jio-math" data-jio-math-source="${escapeAttr(formula)}" data-jio-math-display="0" contenteditable="false">${typeset(formula)}</span>`
: `<p><span class="jio-math jio-math--block" data-jio-math-source="${escapeAttr(formula)}" data-jio-math-display="1" contenteditable="false">${typeset(formula)}</span></p>`
)
.replace(
/\[jio_footnote\]([\s\S]*?)\[\/jio_footnote\]/g,
(_match, note: string) =>
`<sup class="jio-footnote-ref" data-jio-footnote="${escapeAttr(decodeEntities(note))}" contenteditable="false">${note}</sup>`
);
}
const typeset = (raw: string): string => {
const escaped = raw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return escaped
.replace(/\^\{([^{}]*)\}|\^(\S)/gu, '<sup>$1$2</sup>')
.replace(/_\{([^{}]*)\}|_(\S)/gu, '<sub>$1$2</sub>');
};