diff --git a/apps/web-antdv-next/src/components/richText/KtTiptapHtmlEditor.spec.tsx b/apps/web-antdv-next/src/components/richText/KtTiptapHtmlEditor.spec.tsx index 3018cb8..fd8a89c 100644 --- a/apps/web-antdv-next/src/components/richText/KtTiptapHtmlEditor.spec.tsx +++ b/apps/web-antdv-next/src/components/richText/KtTiptapHtmlEditor.spec.tsx @@ -31,8 +31,11 @@ class FakeEditor { public commands = { focus: () => true, - setContent: (value: string) => { + setContent: (value: string, options?: { emitUpdate?: boolean }) => { this.html = value; + if (options?.emitUpdate !== false) { + this.options.onUpdate?.({ editor: this }); + } return true; }, }; @@ -316,6 +319,8 @@ describe('ktTiptapHtmlEditor', () => { ); await nextTick(); + expect(wrapper.emitted('update:modelValue')).toHaveLength(1); + expect(wrapper.emitted('change')).toHaveLength(1); expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([ '

新标题

新正文

', ]); diff --git a/apps/web-antdv-next/src/components/richText/KtTiptapHtmlEditor.tsx b/apps/web-antdv-next/src/components/richText/KtTiptapHtmlEditor.tsx index 877e969..e5991d9 100644 --- a/apps/web-antdv-next/src/components/richText/KtTiptapHtmlEditor.tsx +++ b/apps/web-antdv-next/src/components/richText/KtTiptapHtmlEditor.tsx @@ -540,7 +540,7 @@ export default defineComponent({ return; } - currentEditor.commands.setContent(value); + currentEditor.commands.setContent(value, { emitUpdate: false }); emitHtmlChange(readEditorHtml(currentEditor, value), previousHtml); } @@ -557,7 +557,7 @@ export default defineComponent({ if (nextHtml === currentHtml.value) return; currentHtml.value = nextHtml; - editor.value?.commands.setContent(nextHtml); + editor.value?.commands.setContent(nextHtml, { emitUpdate: false }); }, ); diff --git a/apps/web-antdv-next/src/views/blog/modules/article-form.spec.ts b/apps/web-antdv-next/src/views/blog/modules/article-form.spec.ts index 4601a25..fe37d39 100644 --- a/apps/web-antdv-next/src/views/blog/modules/article-form.spec.ts +++ b/apps/web-antdv-next/src/views/blog/modules/article-form.spec.ts @@ -19,7 +19,7 @@ describe('blog article form helpers', () => { const values = getBlogArticleEditFormValues({ categories: ['技术'], contentHtml: - '
select 1;
', + '
select 1;
', contentMarkdown: '```sql\nselect 1;\n```', excerpt: '摘要', id: '50', @@ -34,6 +34,43 @@ describe('blog article form helpers', () => { expect(values.content).toContain('hljs-codeblock'); }); + it('keeps existing markdown articles in markdown mode when rendered html is present', () => { + const values = getBlogArticleEditFormValues({ + categories: [], + contentHtml: '

标题

正文

', + contentMarkdown: '# 标题\n\n正文', + excerpt: '', + id: '52', + slug: 'markdown-post', + status: 'publish', + tags: [], + title: 'Markdown 文章', + }); + + expect(values.editorMode).toBe('markdown'); + expect(values.contentFormat).toBe('markdown'); + expect(values.content).toBe('# 标题\n\n正文'); + }); + + it('keeps markdown fenced code articles in markdown mode after Argon base rendering', () => { + const values = getBlogArticleEditFormValues({ + categories: [], + contentHtml: + '
const a = 1;
', + contentMarkdown: '```ts\nconst a = 1;\n```', + excerpt: '', + id: '53', + slug: 'markdown-code-post', + status: 'publish', + tags: [], + title: 'Markdown 代码文章', + }); + + expect(values.editorMode).toBe('markdown'); + expect(values.contentFormat).toBe('markdown'); + expect(values.content).toBe('```ts\nconst a = 1;\n```'); + }); + it('edits plain HTML articles with the rich HTML editor by default', () => { const values = getBlogArticleEditFormValues({ categories: [], diff --git a/apps/web-antdv-next/src/views/blog/modules/article-form.ts b/apps/web-antdv-next/src/views/blog/modules/article-form.ts index d98614d..dad373c 100644 --- a/apps/web-antdv-next/src/views/blog/modules/article-form.ts +++ b/apps/web-antdv-next/src/views/blog/modules/article-form.ts @@ -16,8 +16,9 @@ export const BLOG_ARTICLE_HTML_TEXTAREA_CLASS = 'blog-article-form__html-source'; export const BLOG_ARTICLE_MARKDOWN_MIN_HEIGHT = 560; -const ARGON_HTML_PATTERN = - /\b(?:wp-block-|hljs-codeblock|hljs-ln|hljs-control|fancybox-wrapper|lazyload|collapse-block)/; +const ARGON_BASE_CODEBLOCK_PATTERN = /\b(?:wp-block-code|hljs-codeblock)\b/; +const SOURCE_ONLY_HTML_PATTERN = + /\b(?:hljs-ln|hljs-control|fancybox-wrapper|lazyload|collapse-block|wp-block-(?!code\b)[\w-]+)/; const HTML_TAG_PATTERN = /<\/?[a-z][\s\S]*>/i; /** @@ -118,16 +119,18 @@ export function getContentFormatForEditorMode( } /** - * Chooses the editor mode for an existing article without forcing imported Argon HTML through Tiptap or Markdown. + * Chooses the editor mode for an existing article without forcing Markdown source or imported Argon HTML into the wrong editor. * @param article Article row returned by the local blog API. - * @returns Source HTML for Argon runtime snapshots, rich HTML for plain tags, otherwise Markdown. + * @returns Markdown for source-backed articles, source HTML for strong runtime DOM, rich HTML for plain tags. */ export function getBlogArticleEditorMode( article?: Partial, ): BlogArticleEditorMode { const html = getRenderedValue(article?.contentHtml || article?.content); if (!html) return 'markdown'; - if (ARGON_HTML_PATTERN.test(html)) return 'html-source'; + if (SOURCE_ONLY_HTML_PATTERN.test(html)) return 'html-source'; + if (hasMarkdownSource(article)) return 'markdown'; + if (ARGON_BASE_CODEBLOCK_PATTERN.test(html)) return 'html-source'; return HTML_TAG_PATTERN.test(html) ? 'html-rich' : 'markdown'; } @@ -241,6 +244,15 @@ function getRenderedValue(value?: string | WordpressBlogApi.RenderedField) { return value.raw || value.rendered || ''; } +/** + * Detects whether the article still owns an editable Markdown source snapshot. + * @param article Article row returned by the local blog API. + * @returns true when Admin should prefer Milkdown unless strong runtime-only HTML is present. + */ +function hasMarkdownSource(article?: Partial) { + return !!article?.contentMarkdown?.trim(); +} + /** * Chooses editable Markdown from an API rendered field and its explicit Markdown source. * @param value Rendered field or string value.