diff --git a/package.json b/package.json index aed1b4e0..24d83840 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@iconify-json/lucide": "^1.2.71", - "@nuxtjs/mdc": "0.18.0", + "@nuxtjs/mdc": "https://pkg.pr.new/@nuxtjs/mdc@bf09212", "defu": "^6.1.4", "destr": "^2.0.5", "unstorage": "^1.17.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 677fdc8e..4a078a04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^1.2.71 version: 1.2.71 '@nuxtjs/mdc': - specifier: 0.18.0 - version: 0.18.0(magicast@0.3.5) + specifier: https://pkg.pr.new/@nuxtjs/mdc@bf09212 + version: https://pkg.pr.new/@nuxtjs/mdc@bf09212(magicast@0.3.5) defu: specifier: ^6.1.4 version: 6.1.4 @@ -921,6 +921,10 @@ packages: '@nuxtjs/mdc@0.18.0': resolution: {integrity: sha512-/rWEOiLpD6oNx2FC/UsYxLn1pP31pvRmaX5y8GurBOogATKDWd3jlfKCGgshLnsWM6dCKgNkF0mCZQCMZMfpIQ==} + '@nuxtjs/mdc@https://pkg.pr.new/@nuxtjs/mdc@bf09212': + resolution: {tarball: https://pkg.pr.new/@nuxtjs/mdc@bf09212} + version: 0.18.0 + '@nuxtjs/robots@5.5.6': resolution: {integrity: sha512-PFp0sSaQs2ceEubvkiUPrWQ0GYTTu5bDH0lGVmJlm0h/Dqmt/e9TziXNKahL8HUV3VG22YzRyuyjd7p8+BaNgw==} @@ -8054,6 +8058,55 @@ snapshots: - magicast - supports-color + '@nuxtjs/mdc@https://pkg.pr.new/@nuxtjs/mdc@bf09212(magicast@0.3.5)': + dependencies: + '@nuxt/kit': 4.2.0(magicast@0.3.5) + '@shikijs/core': 3.14.0 + '@shikijs/langs': 3.14.0 + '@shikijs/themes': 3.14.0 + '@shikijs/transformers': 3.14.0 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@vue/compiler-core': 3.5.22 + consola: 3.4.2 + debug: 4.4.3 + defu: 6.1.4 + destr: 2.0.5 + detab: 3.0.2 + github-slugger: 2.0.0 + hast-util-format: 1.1.0 + hast-util-to-mdast: 10.1.2 + hast-util-to-string: 3.0.1 + mdast-util-to-hast: 13.2.0 + micromark-util-sanitize-uri: 2.0.1 + parse5: 8.0.0 + pathe: 2.0.3 + property-information: 7.1.0 + rehype-external-links: 3.0.0 + rehype-minify-whitespace: 6.0.2 + rehype-raw: 7.0.0 + rehype-remark: 10.0.1 + rehype-slug: 6.0.0 + rehype-sort-attribute-values: 5.0.1 + rehype-sort-attributes: 5.0.1 + remark-emoji: 5.0.2 + remark-gfm: 4.0.1 + remark-mdc: remark-mdc-edge@3.6.0-29333381.8558577 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-stringify: 11.0.0 + scule: 1.3.0 + shiki: 3.14.0 + ufo: 1.6.1 + unified: 11.0.5 + unist-builder: 4.0.0 + unist-util-visit: 5.0.0 + unwasm: 0.3.11 + vfile: 6.0.3 + transitivePeerDependencies: + - magicast + - supports-color + '@nuxtjs/robots@5.5.6(h3@1.15.4)(magicast@0.5.0)(vue@3.5.22(typescript@5.9.3))': dependencies: '@fingerprintjs/botd': 1.9.1 diff --git a/src/app/src/components/content/ContentEditorCode.vue b/src/app/src/components/content/ContentEditorCode.vue index 30672ae9..6ab3feb7 100644 --- a/src/app/src/components/content/ContentEditorCode.vue +++ b/src/app/src/components/content/ContentEditorCode.vue @@ -42,6 +42,7 @@ const language = computed(() => { }) const { editor, setContent: setEditorContent } = useMonaco(editorRef, { + uri: 'file://' + (document.value?.id || ''), language, readOnly: props.readOnly, colorMode: ui.colorMode, diff --git a/src/app/src/composables/useMonaco.ts b/src/app/src/composables/useMonaco.ts index 3aae3aab..6c1bbfd7 100644 --- a/src/app/src/composables/useMonaco.ts +++ b/src/app/src/composables/useMonaco.ts @@ -3,6 +3,7 @@ import type { editor as Editor } from 'modern-monaco/editor-core' import { setupMonaco } from '../utils/monaco' export interface UseMonacoOptions { + uri?: string language: Ref | string initialContent?: string readOnly?: boolean @@ -60,7 +61,9 @@ export function useMonaco(target: Ref, options: UseMona // Create and attach model const language = unref(options.language) - editor.value.setModel(monaco.editor.createModel(initialContent, language)) + const existingModel = options.uri && monaco.editor.getModel(options.uri) + const model = existingModel || monaco.editor.createModel(initialContent, language, options.uri) + editor.value.setModel(model) // Watch for color mode changes watch(options.colorMode, (newMode) => { diff --git a/src/app/src/utils/content.ts b/src/app/src/utils/content.ts index 3e44db9d..d08851b6 100644 --- a/src/app/src/utils/content.ts +++ b/src/app/src/utils/content.ts @@ -197,7 +197,7 @@ export async function generateContentFromMarkdownDocument(document: DatabasePage } }) - return await stringifyMarkdown(body, removeReservedKeysFromDocument(document), { + const markdown = await stringifyMarkdown(body, removeReservedKeysFromDocument(document), { plugins: { remarkMDC: { options: { @@ -206,4 +206,6 @@ export async function generateContentFromMarkdownDocument(document: DatabasePage }, }, }) + + return typeof markdown === 'string' ? markdown.replace(/*/g, '*') : markdown } diff --git a/src/app/src/utils/database.ts b/src/app/src/utils/database.ts index 3a2fbd16..c113ab53 100644 --- a/src/app/src/utils/database.ts +++ b/src/app/src/utils/database.ts @@ -3,6 +3,7 @@ import { type DatabasePageItem, ContentFileExtension } from '../types' import { stringify } from 'minimark/stringify' import type { MDCRoot } from '@nuxtjs/mdc' import type { MarkdownRoot } from '@nuxt/content' +import { isDeepEqual } from './object' export function isEqual(document1: DatabasePageItem, document2: DatabasePageItem) { function withoutLastStyles(body: MarkdownRoot) { @@ -35,7 +36,7 @@ export function isEqual(document1: DatabasePageItem, document2: DatabasePageItem } } - if (JSON.stringify(documentData1) !== JSON.stringify(documentData2)) { + if (!isDeepEqual(refineDocumentData(documentData1), refineDocumentData(documentData2))) { return false } @@ -49,3 +50,18 @@ export function isEqual(document1: DatabasePageItem, document2: DatabasePageItem return true } + +function refineDocumentData(doc: Record) { + if (doc.seo) { + const seo = doc.seo as Record + doc.seo = { + ...seo, + title: seo.title || doc.title, + description: seo.description || doc.description, + } + } + // documents with same id are being compared, so it is safe to remove `path` and `__hash__` + Reflect.deleteProperty(doc, '__hash__') + Reflect.deleteProperty(doc, 'path') + return doc +} diff --git a/src/app/src/utils/draft.ts b/src/app/src/utils/draft.ts index 0d7232ad..33b92066 100644 --- a/src/app/src/utils/draft.ts +++ b/src/app/src/utils/draft.ts @@ -2,7 +2,7 @@ import type { DatabaseItem, MediaItem, DatabasePageItem, DraftItem, BaseItem, Co import { DraftStatus, ContentFileExtension, TreeRootId } from '../types' import { isEqual } from './database' import { studioFlags } from '../composables/useStudio' -import { generateContentFromDocument } from './content' +import { generateContentFromDocument, generateDocumentFromContent } from './content' import { fromBase64ToUTF8 } from '../utils/string' export async function checkConflict(draftItem: DraftItem): Promise { @@ -28,6 +28,11 @@ export async function checkConflict(draftItem: DraftItem, keys: string | string[]) => { return Object.fromEntries(Object.entries(obj) .filter(([key]) => keys.includes(key))) } + +export function isDeepEqual(obj1: Record, obj2: Record) { + if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2 + + const keys1 = Object.keys(obj1).filter(k => obj1[k] != null) + const keys2 = Object.keys(obj2).filter(k => obj2[k] != null) + + if (keys1.length !== keys2.length) return false + + for (const key of keys1) { + if (!isDeepEqual(obj1[key] as Record, obj2[key] as Record)) return false + } + + return true +} diff --git a/src/module/src/runtime/utils/collection.ts b/src/module/src/runtime/utils/collection.ts index 18df26fd..fe6aa2d3 100644 --- a/src/module/src/runtime/utils/collection.ts +++ b/src/module/src/runtime/utils/collection.ts @@ -143,10 +143,19 @@ export function normalizeDocument(document: DatabaseItem) { // we can remove it to avoid duplication if (document.seo) { const seo = document.seo as Record - if ((!seo.title || seo.title === document.title) && (!seo.description || seo.description === document.description)) { + + if (!seo.title || seo.title === document.title) { + Reflect.deleteProperty(document.seo, 'title') + } + if (!seo.description || seo.description === document.description) { + Reflect.deleteProperty(document.seo, 'description') + } + + if (Object.keys(seo).length === 0) { Reflect.deleteProperty(document, 'seo') } } + return document }