feat(creator-studio): bottom status bar, editor-rendered math/footnote, code language picker, block delete

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-07 00:38:39 +08:00
1 parent 6928127bfa
commit 4a119358ca
11 files changed
+754 -63

No files matched your search

@@ -0,0 +1,83 @@
/**
* 编辑器内容 ↔ 保存格式的转换。
*
* 编辑器里把公式、注释、代码语言标签渲染成可读的样子(方便写作时看到最终效果),
* 但落库必须回到短代码/纯标记,服务端才能稳定渲染。这里在两个方向上都做一次转换。
*
* @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;
};
/**
* 保存前:把编辑器里的可视化块换回短代码 / 纯标记。
*/
export function serializeBlocks(html: string): string {
if (!html || (html.indexOf('data-jio-') === -1 && html.indexOf('jio-code-lang') === -1)) {
return html;
}
const container = document.createElement('div');
container.innerHTML = html;
// 公式
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]`));
});
// 代码块语言角标只是编辑期提示
container.querySelectorAll('.jio-code-lang').forEach((node) => node.remove());
return container.innerHTML;
}
/**
* 打开已有内容时:把短代码还原成编辑器里的可视化块。
*/
export function hydrateBlocks(html: string): string {
if (!html || html.indexOf('[jio_') === -1) {
return html;
}
return html
.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>');
};