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:
1 parent
6928127bfa
commit
4a119358ca
11 files changed
+754
-63
No files matched your search
@@ -51,19 +51,26 @@ export function codeLabel(language: string): string {
|
||||
return CODE_LANGUAGES.find((item) => item.value === language)?.label || language;
|
||||
}
|
||||
|
||||
/** 代码块:语言写在 data 属性里,前台 enlighter 与编辑器标签共用。 */
|
||||
/** 代码块右上角角标:编辑器内由 React 渲染,这里给出标识供 CSS 定位。 */
|
||||
export const CODE_LABEL_CLASS = 'jio-code-lang';
|
||||
|
||||
/**
|
||||
* 代码块:语言写在 data 属性里,前台 enlighter 与编辑器角标共用。
|
||||
*
|
||||
* `data-jio-code` 标记这是编辑器创建的块,编辑器据此保证回车不跳出代码区、
|
||||
* 并渲染可点击的语言角标。
|
||||
*/
|
||||
export function codeBlockMarkup(language = 'generic', code = ''): string {
|
||||
const body = code !== '' ? escapeHtml(code) : escapeHtml(__('在此输入代码', 'jio'));
|
||||
// data-jio-code-label 供编辑器角标直接显示,避免在 CSS 里映射语言标识。
|
||||
const label = escapeHtml(codeLabel(language));
|
||||
return (
|
||||
`<pre class="enlighter-pre" data-enlighter-language="${escapeHtml(language)}" data-jio-code-label="${label}">` +
|
||||
`<pre class="enlighter-pre" data-jio-code="1" data-enlighter-language="${escapeHtml(language)}">` +
|
||||
`<code class="gl" data-enlighter-language="${escapeHtml(language)}">` +
|
||||
body +
|
||||
'</code></pre><p></p>'
|
||||
);
|
||||
}
|
||||
|
||||
/** 把代码块的语言写成编辑器里的内联标签(纯文本,保存前由编辑器移除)。 */
|
||||
export type TableOptions = {
|
||||
rows: number;
|
||||
cols: number;
|
||||
@@ -102,21 +109,33 @@ export function tocMarkup(): string {
|
||||
return '<p>[jio_toc]</p><p></p>';
|
||||
}
|
||||
|
||||
/** 注释:用短代码落库,服务端生成上标序号与文末注释列表。 */
|
||||
export function footnoteMarkup(note: string): string {
|
||||
const text = note.trim() || __('注释内容', 'jio');
|
||||
// 短代码属性里再放 HTML 会被 kses 改写,这里只允许纯文本内容。
|
||||
return `<p>[jio_footnote]${escapeHtml(text)}[/jio_footnote]</p>`;
|
||||
}
|
||||
|
||||
export type MathOptions = { formula: string; display: boolean };
|
||||
|
||||
/** 公式:用短代码落库,服务端统一做上标 / 下标排版。 */
|
||||
/** 公式的上标 / 下标排版(与 ContentBlocks::typeset_math 保持一致)。 */
|
||||
export function typesetMath(raw: string): string {
|
||||
const escaped = escapeHtml(raw);
|
||||
return escaped
|
||||
.replace(/\^\{([^{}]*)\}|\^(\S)/gu, '<sup>$1$2</sup>')
|
||||
.replace(/_\{([^{}]*)\}|_(\S)/gu, '<sub>$1$2</sub>');
|
||||
}
|
||||
|
||||
/**
|
||||
* 公式:编辑器里直接显示排版结果,保存前由 `serializeBlocks` 换回短代码。
|
||||
*/
|
||||
export function mathMarkup({ formula, display }: MathOptions): string {
|
||||
const body = escapeHtml(formula.trim());
|
||||
const body = typesetMath(formula.trim());
|
||||
const attrs = `data-jio-math-source="${escapeHtml(formula.trim())}" data-jio-math-display="${display ? '1' : '0'}"`;
|
||||
return display
|
||||
? `<p>[jio_math display="1"]${body}[/jio_math]</p>`
|
||||
: `<p>[jio_math display="0"]${body}[/jio_math]</p>`;
|
||||
? `<p><span class="jio-math jio-math--block" ${attrs} contenteditable="false">${body}</span></p>`
|
||||
: `<span class="jio-math" ${attrs}>${body}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释:编辑器里显示上标序号,保存前由 `serializeBlocks` 换回短代码。
|
||||
*/
|
||||
export function footnoteMarkup(note: string): string {
|
||||
const text = note.trim() || __('注释内容', 'jio');
|
||||
return `<sup class="jio-footnote-ref" data-jio-footnote="${escapeHtml(text)}" contenteditable="false">${escapeHtml(text)}</sup>`;
|
||||
}
|
||||
|
||||
export type QuoteColor = '' | 'qe_wzk_c-red' | 'qe_wzk_lan' | 'qe_wzk_lv';
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 编辑器内容 ↔ 保存格式的转换。
|
||||
*
|
||||
* 编辑器里把公式、注释、代码语言标签渲染成可读的样子(方便写作时看到最终效果),
|
||||
* 但落库必须回到短代码/纯标记,服务端才能稳定渲染。这里在两个方向上都做一次转换。
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
const escapeAttr = (value: string): string =>
|
||||
value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return escaped
|
||||
.replace(/\^\{([^{}]*)\}|\^(\S)/gu, '<sup>$1$2</sup>')
|
||||
.replace(/_\{([^{}]*)\}|_(\S)/gu, '<sub>$1$2</sub>');
|
||||
};
|
||||
Reference in new issue
Block a user