diff --git a/LICENSE b/LICENSE index 6a36a84..678789e 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ YIXIONG Tech.ltd 公司内部专属软件许可证 一、适用范围 -本许可证适用于“Markdown PDF 导出器”项目中由 YIXIONG Tech.ltd 拥有 +本许可证适用于“墨呈(MorphDoc)”项目中由 YIXIONG Tech.ltd 拥有 著作权的源代码、原创主题、原创文档、构建脚本和其他原创材料(以下简称 “本软件”)。 diff --git a/NOTICE b/NOTICE index ef22488..3fae3b1 100644 --- a/NOTICE +++ b/NOTICE @@ -1,4 +1,4 @@ -Markdown PDF 导出器 +墨呈(MorphDoc) Copyright 2026 YIXIONG Tech.ltd This product includes software developed by YIXIONG Tech.ltd and diff --git a/README.md b/README.md index e86c46d..984408f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,25 @@ -# Markdown PDF 导出器 +# 墨呈 -一个同时提供 Web 与 Windows 桌面端的 Markdown 排版、分页预览和 PDF -导出工具。预览与 PDF 复用同一份安全 HTML、`#write` DOM、主题 CSS 和 -分页运行时,适合内网部署,也可以作为不依赖本地服务的桌面应用使用。 +> 一稿多式,即写即呈 -当前版本:`v0.5.1`。 +面向结构化 Markdown 的主题化文档创作与发布工具,同时提供 Web 与 +Windows 桌面端。预览与 PDF 复用同一份安全 HTML、`#write` DOM、主题 +CSS 和分页运行时,DOCX 使用同源语义文档、主题令牌和可编辑 OOXML。 -## v0.5.1 主要能力 +当前版本:`v0.6.0`。 + +## v0.6.0 核心更新 + +- 新增可编辑 DOCX 导出,使用固定 Pandoc、动态 Word 样式、原生分节、 + 页眉页脚、页码和高分辨率 PNG 媒体管线; +- 主题 CSS 通过通用语义槽位翻译为 Word 样式,支持 14 套内置主题、 + 公文结构、独立封面、代码、表格、图片、Mermaid 与 ECharts; +- 新增 CodeMirror 6 Markdown 工具栏,支持标题、行内格式、引用、列表、 + 代码块、分隔线和尺寸表格; +- 产品品牌升级为“墨呈”,Windows 主程序和发行产物使用英文名 + `MorphDoc`。 + +## v0.5.1 主题与公文能力 - 新增政企标准红头、函件红头、企业简版红头、简报红头等 4 套红头主题, 以及 3 套正式文档主题和 3 套标书主题; @@ -39,6 +52,7 @@ | 远程图片 | 服务端受限下载并内嵌 | 应用服务受限下载并内嵌 | | 自定义主题 | Compose 只读挂载目录 | “更多 → 打开自定义主题目录” | | PDF 引擎 | 固定版本 Playwright Chromium | Electron 自带 Chromium | +| DOCX 引擎 | 固定版本 Pandoc | 内置固定版本 Pandoc | Web 端不提供本地素材目录能力。需要包含本地相对资源的文档时,请使用 Desktop;Web 端可使用 Base64/Data URL 或受限公网图片。 @@ -126,8 +140,8 @@ npm run make -w @md-to-pdf/desktop ``` 桌面 `package` 和 `make` 会先完整重建内嵌 Web 及全部共享依赖,不能 -复用陈旧的 `apps/web/dist`。产物位于 `apps/desktop/out/v0.5.1/`。 -正式安装包命名为 `md-to-pdf-0.5.1-x86_64-Setup.exe`,同时生成免安装 ZIP;当前发行包 +复用陈旧的 `apps/web/dist`。产物位于 `apps/desktop/out/v0.6.0/`。 +正式安装包命名为 `MorphDoc-0.6.0-x86_64-Setup.exe`,同时生成免安装 ZIP;当前发行包 未配置代码签名。 ## 文档与媒体 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 9420003..dad7676 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -46,8 +46,8 @@ Markdown ECharts、Core、Renderer、Application、Preview Engine 和 Web, 然后才复制 `apps/web/dist`。不得绕过该链路直接调用 electron-builder。 版本化目录包、NSIS `Setup.exe` 和 ZIP 输出到 -`apps/desktop/out/v0.5.1/`。该目录被 Git 忽略。公司内部分发以 -`md-to-pdf-0.5.1-x86_64-Setup.exe` 为正式安装包,ZIP 作为免安装 +`apps/desktop/out/v0.6.0/`。该目录被 Git 忽略。公司内部分发以 +`MorphDoc-0.6.0-x86_64-Setup.exe` 为正式安装包,ZIP 作为免安装 辅助包;当前未配置代码签名,Windows 首次运行可能显示“未知发布者” 提示。 @@ -56,11 +56,12 @@ NSIS 使用标准辅助安装模式,支持选择当前用户/所有用户安 发布,不提交进 Git 历史。 桌面和开始菜单快捷方式由 `build/installer.nsh` 显式创建,安装完成后的 -“运行”操作直接启动 `md-to-pdf.exe`,不依赖快捷方式文件。 +“运行”操作直接启动 `MorphDoc.exe`,不依赖快捷方式文件。 -产品名称、窗口标题、安装包名称和开始菜单名称保持为 -“Markdown PDF 导出器”,实际 Windows 主程序与进程文件固定为 -`md-to-pdf.exe`。 +界面、快捷方式与卸载项使用中文名“墨呈”,窗口标题为 +“墨呈 · MorphDoc”,默认安装目录和 Windows 主程序分别为 `MorphDoc` +和 `MorphDoc.exe`。NSIS appId 保持 `com.md-to-pdf.desktop`,确保 +v0.5.1 覆盖升级沿用原安装身份。 当前目录包只包含一套 Electron Chromium,不携带 Playwright Chromium。 Web 静态资源作为只读资源放在 Electron `resources/dist` 下,生产环境通过 diff --git a/apps/desktop/build/installer.nsh b/apps/desktop/build/installer.nsh index 08c82c7..f55965d 100644 --- a/apps/desktop/build/installer.nsh +++ b/apps/desktop/build/installer.nsh @@ -1,3 +1,8 @@ +!define MORPHDOC_DISPLAY_NAME "墨呈" +!define MORPHDOC_EXECUTABLE "MorphDoc.exe" +!define MORPHDOC_LEGACY_DISPLAY_NAME "Markdown PDF 导出器" +!define MORPHDOC_LEGACY_EXECUTABLE "md-to-pdf.exe" + !ifdef FONT_PACK_COMPANION_FILE !ifndef BUILD_UNINSTALLER !include "FileFunc.nsh" @@ -36,7 +41,7 @@ Pop $R0 StrCmp $R0 "error" 0 +2 Abort - ${NSD_CreateLabel} 0 0 100% 28u "检测到经过完整性校验的可选字体包。安装后,政企公文主题可使用与 PDF 相同的原生中文字体资产。" + ${NSD_CreateLabel} 0 0 100% 28u "检测到经过完整性校验的墨呈可选字体包。安装后,政企公文主题可使用与 PDF 相同的原生中文字体资产。" Pop $R0 ${NSD_CreateCheckbox} 0 38u 100% 14u "安装 ${FONT_PACK_COMPANION_NAME}" Pop $fontPackCompanionCheckbox @@ -70,11 +75,11 @@ WriteRegStr SHCTX \ "Software\Classes\MarkdownPdfExporter.Markdown\DefaultIcon" \ "" \ - '"$INSTDIR\md-to-pdf.exe",0' + '"$INSTDIR\${MORPHDOC_EXECUTABLE}",0' WriteRegStr SHCTX \ "Software\Classes\MarkdownPdfExporter.Markdown\shell\open\command" \ "" \ - '"$INSTDIR\md-to-pdf.exe" "%1"' + '"$INSTDIR\${MORPHDOC_EXECUTABLE}" "%1"' WriteRegStr SHCTX \ "Software\Classes\.md\OpenWithProgids" \ "MarkdownPdfExporter.Markdown" \ @@ -84,29 +89,34 @@ "MarkdownPdfExporter.Markdown" \ "" WriteRegStr SHCTX \ - "Software\Classes\Applications\md-to-pdf.exe\shell\open\command" \ + "Software\Classes\Applications\${MORPHDOC_EXECUTABLE}\shell\open\command" \ "" \ - '"$INSTDIR\md-to-pdf.exe" "%1"' + '"$INSTDIR\${MORPHDOC_EXECUTABLE}" "%1"' WriteRegStr SHCTX \ - "Software\Classes\Applications\md-to-pdf.exe\SupportedTypes" \ + "Software\Classes\Applications\${MORPHDOC_EXECUTABLE}\SupportedTypes" \ ".md" \ "" WriteRegStr SHCTX \ - "Software\Classes\Applications\md-to-pdf.exe\SupportedTypes" \ + "Software\Classes\Applications\${MORPHDOC_EXECUTABLE}\SupportedTypes" \ ".markdown" \ "" - CreateDirectory "$SMPROGRAMS\Markdown PDF 导出器" + DeleteRegKey SHCTX \ + "Software\Classes\Applications\${MORPHDOC_LEGACY_EXECUTABLE}" + Delete "$SMPROGRAMS\${MORPHDOC_LEGACY_DISPLAY_NAME}\${MORPHDOC_LEGACY_DISPLAY_NAME}.lnk" + RMDir "$SMPROGRAMS\${MORPHDOC_LEGACY_DISPLAY_NAME}" + Delete "$DESKTOP\${MORPHDOC_LEGACY_DISPLAY_NAME}.lnk" + CreateDirectory "$SMPROGRAMS\${MORPHDOC_DISPLAY_NAME}" CreateShortCut \ - "$SMPROGRAMS\Markdown PDF 导出器\Markdown PDF 导出器.lnk" \ - "$INSTDIR\md-to-pdf.exe" \ + "$SMPROGRAMS\${MORPHDOC_DISPLAY_NAME}\${MORPHDOC_DISPLAY_NAME}.lnk" \ + "$INSTDIR\${MORPHDOC_EXECUTABLE}" \ "" \ - "$INSTDIR\md-to-pdf.exe" + "$INSTDIR\${MORPHDOC_EXECUTABLE}" CreateShortCut \ - "$DESKTOP\Markdown PDF 导出器.lnk" \ - "$INSTDIR\md-to-pdf.exe" \ + "$DESKTOP\${MORPHDOC_DISPLAY_NAME}.lnk" \ + "$INSTDIR\${MORPHDOC_EXECUTABLE}" \ "" \ - "$INSTDIR\md-to-pdf.exe" - StrCpy $launchLink "$INSTDIR\md-to-pdf.exe" + "$INSTDIR\${MORPHDOC_EXECUTABLE}" + StrCpy $launchLink "$INSTDIR\${MORPHDOC_EXECUTABLE}" !ifdef FONT_PACK_COMPANION_FILE StrCmp $fontPackCompanionAvailable "1" 0 font_pack_install_done StrCmp $fontPackCompanionSelected "1" 0 font_pack_install_done @@ -136,11 +146,16 @@ "Software\Classes\.markdown\OpenWithProgids" \ "MarkdownPdfExporter.Markdown" DeleteRegKey SHCTX \ - "Software\Classes\Applications\md-to-pdf.exe" + "Software\Classes\Applications\${MORPHDOC_EXECUTABLE}" + DeleteRegKey SHCTX \ + "Software\Classes\Applications\${MORPHDOC_LEGACY_EXECUTABLE}" DeleteRegKey SHCTX \ "Software\Classes\MarkdownPdfExporter.Markdown" - Delete "$SMPROGRAMS\Markdown PDF 导出器\Markdown PDF 导出器.lnk" - RMDir "$SMPROGRAMS\Markdown PDF 导出器" - Delete "$DESKTOP\Markdown PDF 导出器.lnk" + Delete "$SMPROGRAMS\${MORPHDOC_DISPLAY_NAME}\${MORPHDOC_DISPLAY_NAME}.lnk" + RMDir "$SMPROGRAMS\${MORPHDOC_DISPLAY_NAME}" + Delete "$DESKTOP\${MORPHDOC_DISPLAY_NAME}.lnk" + Delete "$SMPROGRAMS\${MORPHDOC_LEGACY_DISPLAY_NAME}\${MORPHDOC_LEGACY_DISPLAY_NAME}.lnk" + RMDir "$SMPROGRAMS\${MORPHDOC_LEGACY_DISPLAY_NAME}" + Delete "$DESKTOP\${MORPHDOC_LEGACY_DISPLAY_NAME}.lnk" System::Call 'shell32::SHChangeNotify(i 0x08000000, i 0, i 0, i 0)' !macroend diff --git a/apps/desktop/electron-builder.config.cjs b/apps/desktop/electron-builder.config.cjs index d59018b..023ff88 100644 --- a/apps/desktop/electron-builder.config.cjs +++ b/apps/desktop/electron-builder.config.cjs @@ -1,5 +1,6 @@ const path = require("node:path"); const { version } = require("./package.json"); +const brand = require("../../product.json"); const windowsIcon = path.resolve( __dirname, @@ -9,8 +10,8 @@ const nsisInclude = process.env.MD_TO_PDF_NSIS_INCLUDE?.trim() || "build/installer.nsh"; module.exports = { - appId: "com.md-to-pdf.desktop", - productName: "Markdown PDF 导出器", + appId: brand.appId, + productName: brand.englishName, copyright: "Copyright © YIXIONG Tech.ltd", asar: true, electronLanguages: ["zh-CN", "en-US"], @@ -58,8 +59,8 @@ module.exports = { } ], icon: windowsIcon, - executableName: "md-to-pdf", - artifactName: `md-to-pdf-${version}-x86_64.\${ext}` + executableName: brand.executableName, + artifactName: `${brand.artifactPrefix}-${version}-x86_64.\${ext}` }, nsis: { oneClick: false, @@ -68,12 +69,12 @@ module.exports = { allowElevation: true, createDesktopShortcut: false, createStartMenuShortcut: false, - shortcutName: "Markdown PDF 导出器", - uninstallDisplayName: "Markdown PDF 导出器", + shortcutName: brand.displayName, + uninstallDisplayName: brand.displayName, installerIcon: windowsIcon, uninstallerIcon: windowsIcon, installerLanguages: ["zh_CN"], include: nsisInclude, - artifactName: `md-to-pdf-${version}-x86_64-Setup.\${ext}` + artifactName: `${brand.artifactPrefix}-${version}-x86_64-Setup.\${ext}` } }; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b2c7968..f3f599a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,9 +1,9 @@ { "name": "@md-to-pdf/desktop", - "version": "0.5.1", + "version": "0.6.0", "private": true, - "productName": "Markdown PDF 导出器", - "description": "Markdown PDF 导出器桌面端", + "productName": "MorphDoc", + "description": "墨呈桌面端:面向结构化 Markdown 的主题化文档创作与发布工具", "author": "YIXIONG Tech.ltd", "type": "module", "main": "dist/main.js", diff --git a/apps/desktop/scripts/build-main.mjs b/apps/desktop/scripts/build-main.mjs index d58b9f4..dadd87d 100644 --- a/apps/desktop/scripts/build-main.mjs +++ b/apps/desktop/scripts/build-main.mjs @@ -1,7 +1,14 @@ import { build } from "esbuild"; -import { copyFile } from "node:fs/promises"; +import { copyFile, readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; +const productBrand = JSON.parse( + await readFile( + fileURLToPath(new URL("../../../product.json", import.meta.url)), + "utf8" + ) +); + await build({ entryPoints: ["src/main.ts"], bundle: true, @@ -9,6 +16,15 @@ await build({ format: "esm", external: ["electron"], outfile: "dist/main.js", + define: { + __APP_ID__: JSON.stringify(productBrand.appId), + __APP_ENGLISH_NAME__: JSON.stringify(productBrand.englishName), + __APP_DISPLAY_NAME__: JSON.stringify(productBrand.displayName), + __APP_WINDOW_TITLE__: JSON.stringify(productBrand.windowTitle), + __APP_LEGACY_DISPLAY_NAME__: JSON.stringify( + productBrand.legacyDisplayName + ) + }, banner: { js: [ 'import { createRequire as __mdToPdfCreateRequire } from "node:module";', diff --git a/apps/desktop/src/build-globals.d.ts b/apps/desktop/src/build-globals.d.ts new file mode 100644 index 0000000..3caf2a5 --- /dev/null +++ b/apps/desktop/src/build-globals.d.ts @@ -0,0 +1,5 @@ +declare const __APP_ID__: string; +declare const __APP_ENGLISH_NAME__: string; +declare const __APP_DISPLAY_NAME__: string; +declare const __APP_WINDOW_TITLE__: string; +declare const __APP_LEGACY_DISPLAY_NAME__: string; diff --git a/apps/desktop/src/desktop-application-controller.ts b/apps/desktop/src/desktop-application-controller.ts index 4b06519..4140924 100644 --- a/apps/desktop/src/desktop-application-controller.ts +++ b/apps/desktop/src/desktop-application-controller.ts @@ -233,7 +233,7 @@ export class DesktopApplicationController { : undefined; const initialBounds = this.#resolveInitialBounds(isPrimary); const window = new BrowserWindow({ - title: `Markdown PDF 导出器 v${app.getVersion()}`, + title: `${__APP_WINDOW_TITLE__} v${app.getVersion()}`, icon: this.#iconPath, ...initialBounds, minWidth: Math.min( diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 7bf7c4f..503e8ca 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -6,6 +6,7 @@ import { session } from "electron"; import { access, mkdir } from "node:fs/promises"; +import { existsSync } from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { @@ -25,6 +26,7 @@ import { findMarkdownFileArgument, isMarkdownFilePath } from "./markdown-file.js"; +import { migrateLegacyUserData } from "./user-data-migration.js"; const APP_SCHEME = "mdpdf"; const APP_HOST = "bundle"; @@ -37,6 +39,28 @@ let pendingExternalMarkdownPath = findMarkdownFileArgument( process.cwd() ); +function configurePackagedUserDataDirectory() { + if (!app.isPackaged) { + return Promise.resolve(); + } + const appDataDirectory = app.getPath("appData"); + const targetDirectory = path.join( + appDataDirectory, + __APP_ENGLISH_NAME__ + ); + const targetAlreadyExists = existsSync(targetDirectory); + app.setPath("userData", targetDirectory); + if (targetAlreadyExists) { + return Promise.resolve(); + } + return migrateLegacyUserData( + [path.join(appDataDirectory, __APP_LEGACY_DISPLAY_NAME__)], + targetDirectory + ).then(() => undefined); +} + +const userDataMigration = configurePackagedUserDataDirectory(); + protocol.registerSchemesAsPrivileged([ { scheme: APP_SCHEME, @@ -300,9 +324,10 @@ if (!hasSingleInstanceLock) { app .whenReady() .then(async () => { + await userDataMigration; Menu.setApplicationMenu(null); if (process.platform === "win32") { - app.setAppUserModelId("com.md-to-pdf.desktop"); + app.setAppUserModelId(__APP_ID__); } const themeDirectory = getDesktopThemeDirectory(); await mkdir(themeDirectory, { recursive: true }); diff --git a/apps/desktop/src/user-data-migration.ts b/apps/desktop/src/user-data-migration.ts new file mode 100644 index 0000000..d774936 --- /dev/null +++ b/apps/desktop/src/user-data-migration.ts @@ -0,0 +1,70 @@ +import { cp, lstat, mkdir } from "node:fs/promises"; +import path from "node:path"; + +const MIGRATED_ENTRIES = [ + "window-state.json", + "save-location.json", + "themes", + "Local Storage", + "Session Storage", + "IndexedDB" +] as const; + +async function readOrdinaryStatus(target: string) { + try { + const status = await lstat(target); + return status.isSymbolicLink() ? undefined : status; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return undefined; + } + throw error; + } +} + +export interface LegacyUserDataMigrationResult { + migrated: boolean; + sourceDirectory?: string; + copiedEntries: string[]; +} + +export async function migrateLegacyUserData( + legacyDirectories: readonly string[], + targetDirectory: string +): Promise { + if (await readOrdinaryStatus(targetDirectory)) { + return { migrated: false, copiedEntries: [] }; + } + + for (const legacyDirectory of legacyDirectories) { + if (path.resolve(legacyDirectory) === path.resolve(targetDirectory)) { + continue; + } + const legacyStatus = await readOrdinaryStatus(legacyDirectory); + if (!legacyStatus?.isDirectory()) { + continue; + } + + await mkdir(targetDirectory, { recursive: true }); + const copiedEntries: string[] = []; + for (const entry of MIGRATED_ENTRIES) { + const source = path.join(legacyDirectory, entry); + if (!(await readOrdinaryStatus(source))) { + continue; + } + await cp(source, path.join(targetDirectory, entry), { + recursive: true, + errorOnExist: true, + force: false + }); + copiedEntries.push(entry); + } + return { + migrated: true, + sourceDirectory: legacyDirectory, + copiedEntries + }; + } + + return { migrated: false, copiedEntries: [] }; +} diff --git a/apps/desktop/tests/release-build.test.ts b/apps/desktop/tests/release-build.test.ts index c060d47..6ecfd95 100644 --- a/apps/desktop/tests/release-build.test.ts +++ b/apps/desktop/tests/release-build.test.ts @@ -3,14 +3,25 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; interface PackageManifest { + version?: string; + productName?: string; scripts?: Record; } +interface ProductBrand { + appId: string; + englishName: string; + displayName: string; + windowTitle: string; + executableName: string; + artifactPrefix: string; +} + const desktopRoot = fileURLToPath(new URL("../", import.meta.url)); const projectRoot = fileURLToPath(new URL("../../../", import.meta.url)); -function readManifest(path: string): PackageManifest { - return JSON.parse(readFileSync(path, "utf8")) as PackageManifest; +function readManifest(path: string): T { + return JSON.parse(readFileSync(path, "utf8")) as T; } describe("桌面发行构建链", () => { @@ -58,6 +69,49 @@ describe("桌面发行构建链", () => { expect(builderConfig).toContain('to: "samples"'); }); + it("保持安装身份并从统一配置生成 MorphDoc 发行名称", () => { + const brand = readManifest( + `${projectRoot}/product.json` + ); + const desktopManifest = readManifest( + `${desktopRoot}/package.json` + ); + const builderConfig = readFileSync( + `${desktopRoot}/electron-builder.config.cjs`, + "utf8" + ); + const installerInclude = readFileSync( + `${desktopRoot}/build/installer.nsh`, + "utf8" + ); + + expect(brand).toMatchObject({ + appId: "com.md-to-pdf.desktop", + englishName: "MorphDoc", + displayName: "墨呈", + windowTitle: "墨呈 · MorphDoc", + executableName: "MorphDoc", + artifactPrefix: "MorphDoc" + }); + expect(desktopManifest).toMatchObject({ + version: "0.6.0", + productName: "MorphDoc" + }); + expect(builderConfig).toContain("appId: brand.appId"); + expect(builderConfig).toContain("productName: brand.englishName"); + expect(builderConfig).toContain("executableName: brand.executableName"); + expect(builderConfig).toContain("shortcutName: brand.displayName"); + expect(installerInclude).toContain( + '!define MORPHDOC_EXECUTABLE "MorphDoc.exe"' + ); + expect(installerInclude).toContain( + '!define MORPHDOC_DISPLAY_NAME "墨呈"' + ); + expect(installerInclude).toContain( + "Software\\Classes\\Applications\\${MORPHDOC_LEGACY_EXECUTABLE}" + ); + }); + it("仅在组合构建时接入经过哈希校验的同目录字体包", () => { const builderConfig = readFileSync( `${desktopRoot}/electron-builder.config.cjs`, diff --git a/apps/desktop/tests/user-data-migration.test.ts b/apps/desktop/tests/user-data-migration.test.ts new file mode 100644 index 0000000..e31077c --- /dev/null +++ b/apps/desktop/tests/user-data-migration.test.ts @@ -0,0 +1,84 @@ +import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { rm } from "node:fs/promises"; +import { migrateLegacyUserData } from "../src/user-data-migration.js"; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((directory) => + rm(directory, { recursive: true, force: true }) + ) + ); +}); + +async function createRoot() { + const root = await mkdtemp(path.join(tmpdir(), "morphdoc-migration-")); + temporaryDirectories.push(root); + return root; +} + +describe("品牌升级用户数据迁移", () => { + it("首次启动时复制状态、主题和浏览器本地存储但保留旧目录", async () => { + const root = await createRoot(); + const legacy = path.join(root, "Markdown PDF 导出器"); + const target = path.join(root, "MorphDoc"); + await mkdir(path.join(legacy, "themes", "custom"), { + recursive: true + }); + await mkdir(path.join(legacy, "Local Storage"), { + recursive: true + }); + await writeFile( + path.join(legacy, "window-state.json"), + '{"width":1280}', + "utf8" + ); + await writeFile( + path.join(legacy, "themes", "custom", "theme.css"), + "#write{}", + "utf8" + ); + + const result = await migrateLegacyUserData([legacy], target); + + expect(result).toMatchObject({ + migrated: true, + sourceDirectory: legacy, + copiedEntries: ["window-state.json", "themes", "Local Storage"] + }); + expect( + await readFile(path.join(target, "window-state.json"), "utf8") + ).toBe('{"width":1280}'); + expect( + await readFile( + path.join(target, "themes", "custom", "theme.css"), + "utf8" + ) + ).toBe("#write{}"); + expect( + await readFile(path.join(legacy, "window-state.json"), "utf8") + ).toBe('{"width":1280}'); + }); + + it("目标目录已经存在时不覆盖新版本数据", async () => { + const root = await createRoot(); + const legacy = path.join(root, "Markdown PDF 导出器"); + const target = path.join(root, "MorphDoc"); + await mkdir(legacy, { recursive: true }); + await mkdir(target, { recursive: true }); + await writeFile(path.join(legacy, "window-state.json"), "legacy", "utf8"); + await writeFile(path.join(target, "window-state.json"), "current", "utf8"); + + expect(await migrateLegacyUserData([legacy], target)).toEqual({ + migrated: false, + copiedEntries: [] + }); + expect( + await readFile(path.join(target, "window-state.json"), "utf8") + ).toBe("current"); + }); +}); diff --git a/apps/server/scripts/verify-docx-http.mjs b/apps/server/scripts/verify-docx-http.mjs index 6985b9f..394b911 100644 --- a/apps/server/scripts/verify-docx-http.mjs +++ b/apps/server/scripts/verify-docx-http.mjs @@ -39,7 +39,7 @@ function temporaryDirectories() { const markdown = `--- title: Server HTTP DOCX 验收 -author: Markdown PDF 导出器研发组 +author: 墨呈研发组 --- # HTTP 导出保持可编辑 diff --git a/apps/web/index.html b/apps/web/index.html index 4d2b3e4..252c261 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -17,9 +17,9 @@ - Markdown PDF 导出器 + 墨呈 · MorphDoc
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 67e9d5a..1ea2ea6 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -44,6 +44,10 @@ import { type PdfExportRequest } from "./pdf-export"; import { APP_VERSION_LABEL } from "./app-version"; +import { + APP_DISPLAY_NAME, + APP_SLOGAN +} from "./app-brand"; import { getSyncedScrollTop } from "./scroll-sync"; import { loadPreviewZoom, @@ -1487,9 +1491,9 @@ export function App() {
-

内网文档工具

+

{APP_SLOGAN}

-

Markdown PDF 导出器

+

{APP_DISPLAY_NAME}

{APP_VERSION_LABEL}
diff --git a/apps/web/src/app-brand.ts b/apps/web/src/app-brand.ts new file mode 100644 index 0000000..804c31c --- /dev/null +++ b/apps/web/src/app-brand.ts @@ -0,0 +1,4 @@ +export const APP_DISPLAY_NAME = __APP_DISPLAY_NAME__; +export const APP_WINDOW_TITLE = __APP_WINDOW_TITLE__; +export const APP_SLOGAN = __APP_SLOGAN__; +export const APP_DESCRIPTION = __APP_DESCRIPTION__; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 87d4d97..628fe07 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,8 +1,12 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { App } from "./App"; +import { APP_WINDOW_TITLE } from "./app-brand"; +import { APP_VERSION_LABEL } from "./app-version"; import "./styles.css"; +document.title = `${APP_WINDOW_TITLE} ${APP_VERSION_LABEL}`; + const rootElement = document.getElementById("root"); if (!rootElement) { diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index 06d4d66..d7e5f04 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -16,6 +16,10 @@ import type { declare global { const __APP_VERSION__: string; + const __APP_DISPLAY_NAME__: string; + const __APP_WINDOW_TITLE__: string; + const __APP_SLOGAN__: string; + const __APP_DESCRIPTION__: string; interface DesktopPdfResult { pdf: Uint8Array; diff --git a/apps/web/tests/app-brand.test.ts b/apps/web/tests/app-brand.test.ts new file mode 100644 index 0000000..eae1674 --- /dev/null +++ b/apps/web/tests/app-brand.test.ts @@ -0,0 +1,34 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { + APP_DESCRIPTION, + APP_DISPLAY_NAME, + APP_SLOGAN, + APP_WINDOW_TITLE +} from "../src/app-brand"; + +const webRoot = fileURLToPath(new URL("../", import.meta.url)); + +describe("产品品牌", () => { + it("在主界面使用中文名和产品标语", () => { + expect(APP_DISPLAY_NAME).toBe("墨呈"); + expect(APP_SLOGAN).toBe("一稿多式,即写即呈"); + expect(APP_WINDOW_TITLE).toBe("墨呈 · MorphDoc"); + expect(APP_DESCRIPTION).toBe( + "面向结构化 Markdown 的主题化文档创作与发布工具" + ); + }); + + it("在静态入口中移除旧工具名称", () => { + const indexHtml = readFileSync(`${webRoot}/index.html`, "utf8"); + const mainSource = readFileSync(`${webRoot}/src/main.tsx`, "utf8"); + + expect(indexHtml).toContain("墨呈 · MorphDoc"); + expect(mainSource).toContain( + "document.title = `${APP_WINDOW_TITLE} ${APP_VERSION_LABEL}`" + ); + expect(indexHtml).not.toContain("Markdown PDF 导出器"); + expect(indexHtml).not.toContain("内网文档工具"); + }); +}); diff --git a/apps/web/tests/app-version.test.ts b/apps/web/tests/app-version.test.ts index 4228199..958b793 100644 --- a/apps/web/tests/app-version.test.ts +++ b/apps/web/tests/app-version.test.ts @@ -10,8 +10,8 @@ const webRoot = fileURLToPath(new URL("../", import.meta.url)); describe("应用版本", () => { it("从统一构建版本生成标题徽标", () => { - expect(APP_VERSION).toBe("0.5.1"); - expect(APP_VERSION_LABEL).toBe("v0.5.1"); + expect(APP_VERSION).toBe("0.6.0"); + expect(APP_VERSION_LABEL).toBe("v0.6.0"); }); it("允许浏览器加载同源 Web Manifest", () => { diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 05580f1..f30ee24 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -9,6 +9,17 @@ const rootPackage = JSON.parse( "utf8" ) ) as { version: string }; +const productBrand = JSON.parse( + readFileSync( + fileURLToPath(new URL("../../product.json", import.meta.url)), + "utf8" + ) +) as { + displayName: string; + windowTitle: string; + slogan: string; + description: string; +}; export default defineConfig({ plugins: [react()], @@ -16,7 +27,11 @@ export default defineConfig({ new URL("../../logos/web", import.meta.url) ), define: { - __APP_VERSION__: JSON.stringify(rootPackage.version) + __APP_VERSION__: JSON.stringify(rootPackage.version), + __APP_DISPLAY_NAME__: JSON.stringify(productBrand.displayName), + __APP_WINDOW_TITLE__: JSON.stringify(productBrand.windowTitle), + __APP_SLOGAN__: JSON.stringify(productBrand.slogan), + __APP_DESCRIPTION__: JSON.stringify(productBrand.description) }, build: { rollupOptions: { diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index a6bdc2b..2b4d9f1 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,6 +1,6 @@ -# Markdown PDF 导出器进度 +# 墨呈进度 -最后更新:2026-08-01 +最后更新:2026-08-02 ## 1. 当前概况 @@ -77,6 +77,12 @@ Windows ZIP 和 Linux amd64 tarball 的 SHA-256 均已核验;Linux 静态程 复制主题允许继续用于公司内部源码共享、部署和安装包分发,但不受项目 许可证重新授权;公开 GitHub 前必须移除、替换或取得公开分发授权。 +`v0.6.0` 发布收口阶段 A 已完成品牌与版本迁移:用户界面中文名为 +“墨呈”,产品标语为“一稿多式,即写即呈”,窗口标题为 +“墨呈 · MorphDoc v0.6.0”;Desktop 安装目录、可执行文件与发行产物统一 +使用 `MorphDoc`。Electron `appId` 和 Markdown 文件关联 ProgID 保持不变, +并为旧版用户数据、快捷方式和应用注册项提供兼容迁移与清理。 + `v0.6.0` 阶段 3 已完成 DOCX 共享协议:`packages/core` 统一定义请求 Schema、资源、Pandoc capability、错误码、结果、诊断、耗时、MIME 和 安全文件名;`packages/application` 新增 `prepareDocxExport()`,复用 @@ -1412,6 +1418,8 @@ ECharts 第二阶段浏览器与 PDF 验证结果: - 阶段 12D-R4 布局视觉矩阵:已完成六组主题、纸张、方向与页边距组合的 Chromium/Word/WPS 元素级严格门禁,并重跑 14 套主题生产矩阵;分页流 差异保留为诊断项,不再误判为样式失败; +- 发布收口阶段 A:已完成 v0.6.0 版本、墨呈/MorphDoc 品牌、NSIS 名称、 + 文件关联兼容和旧版用户数据迁移; - 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。 每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入 diff --git a/font-packs/windows/font-pack-installer.nsi b/font-packs/windows/font-pack-installer.nsi index d657d2d..5a4ad00 100644 --- a/font-packs/windows/font-pack-installer.nsi +++ b/font-packs/windows/font-pack-installer.nsi @@ -31,12 +31,12 @@ SetCompressorDictSize 64 !define MANAGER_ROOT "$LOCALAPPDATA\md-to-pdf\font-pack-installers" !endif -Name "Markdown PDF 导出器字体包 - ${PACK_NAME}" +Name "墨呈字体包 - ${PACK_NAME}" OutFile "${OUTPUT_FILE}" InstallDir "${MANAGER_ROOT}\${PACK_ID}" ShowInstDetails show ShowUninstDetails show -BrandingText "Markdown PDF 导出器" +BrandingText "墨呈" !ifdef ICON_FILE Icon "${ICON_FILE}" @@ -55,7 +55,7 @@ BrandingText "Markdown PDF 导出器" !insertmacro MUI_LANGUAGE "SimpChinese" VIProductVersion "1.0.0.0" -VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "ProductName" "Markdown PDF 导出器字体包" +VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "ProductName" "墨呈字体包" VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "FileDescription" "${PACK_NAME}" VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "FileVersion" "${PACK_VERSION}" VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "LegalCopyright" "Copyright © YIXIONG Tech.ltd" @@ -96,7 +96,7 @@ Section "安装字体包" SEC_FONT_PACK FileClose $R3 !ifdef REGISTER_UNINSTALL - WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "DisplayName" "Markdown PDF 导出器字体包 - ${PACK_NAME}" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "DisplayName" "墨呈字体包 - ${PACK_NAME}" WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "DisplayVersion" "${PACK_VERSION}" WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "Publisher" "YIXIONG Tech.ltd" WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "InstallLocation" "$INSTDIR" diff --git a/logos/web/site.webmanifest b/logos/web/site.webmanifest index 51bdd7c..6552854 100644 --- a/logos/web/site.webmanifest +++ b/logos/web/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "Markdown PDF 导出器", - "short_name": "Markdown PDF", + "name": "墨呈 · MorphDoc", + "short_name": "墨呈", "start_url": "./", "display": "standalone", "background_color": "#f4f2ed", diff --git a/package-lock.json b/package-lock.json index 3db25a8..b4450a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "md-to-pdf", - "version": "0.5.1", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "md-to-pdf", - "version": "0.5.1", + "version": "0.6.0", "license": "UNLICENSED", "workspaces": [ "apps/*", @@ -22,7 +22,7 @@ }, "apps/desktop": { "name": "@md-to-pdf/desktop", - "version": "0.5.1", + "version": "0.6.0", "devDependencies": { "@md-to-pdf/application": "0.4.1", "@md-to-pdf/docx-engine": "0.1.0", diff --git a/package.json b/package.json index 4862b89..4b055c7 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "md-to-pdf", - "version": "0.5.1", + "version": "0.6.0", "private": true, "license": "UNLICENSED", - "description": "可配置、可主题化的 Markdown PDF 导出工具", + "description": "面向结构化 Markdown 的主题化文档创作与发布工具", "workspaces": [ "apps/*", "packages/*" diff --git a/packages/docx-engine/fixtures/docx-acceptance.md b/packages/docx-engine/fixtures/docx-acceptance.md index 838c195..de0d41f 100644 --- a/packages/docx-engine/fixtures/docx-acceptance.md +++ b/packages/docx-engine/fixtures/docx-acceptance.md @@ -1,6 +1,6 @@ --- title: v0.6.0 DOCX 自动验收 -author: Markdown PDF 导出器研发组 +author: 墨呈研发组 subject: Word 与 WPS 可编辑性验收 keywords: [DOCX, Pandoc, OOXML] lang: zh-CN diff --git a/packages/docx-engine/fixtures/docx-media-visual.md b/packages/docx-engine/fixtures/docx-media-visual.md index ea500a2..f59770b 100644 --- a/packages/docx-engine/fixtures/docx-media-visual.md +++ b/packages/docx-engine/fixtures/docx-media-visual.md @@ -1,6 +1,6 @@ --- title: DOCX 媒体视觉专项验收 -author: Markdown PDF 导出器研发组 +author: 墨呈研发组 lang: zh-CN --- diff --git a/packages/docx-engine/fixtures/pandoc-conversion.md b/packages/docx-engine/fixtures/pandoc-conversion.md index b6c651e..bafada9 100644 --- a/packages/docx-engine/fixtures/pandoc-conversion.md +++ b/packages/docx-engine/fixtures/pandoc-conversion.md @@ -1,6 +1,6 @@ --- title: Pandoc 媒体转换验收 -author: Markdown PDF 导出器研发组 +author: 墨呈研发组 --- # 可编辑正文 diff --git a/packages/docx-engine/fixtures/pandoc-reference.md b/packages/docx-engine/fixtures/pandoc-reference.md index 6a785de..50b4b47 100644 --- a/packages/docx-engine/fixtures/pandoc-reference.md +++ b/packages/docx-engine/fixtures/pandoc-reference.md @@ -1,6 +1,6 @@ --- title: 阶段六动态模板验收 -author: Markdown PDF 导出器研发组 +author: 墨呈研发组 --- # 一级标题 diff --git a/packages/docx-engine/scripts/verify-docx-acceptance.mjs b/packages/docx-engine/scripts/verify-docx-acceptance.mjs index 36fb0a9..01652d3 100644 --- a/packages/docx-engine/scripts/verify-docx-acceptance.mjs +++ b/packages/docx-engine/scripts/verify-docx-acceptance.mjs @@ -406,7 +406,7 @@ for (const variant of variants) { const fileName = `${variant.id}.md`; const metadata = { title: "v0.6.0 DOCX 自动验收", - author: "Markdown PDF 导出器研发组", + author: "墨呈研发组", subject: "Word 与 WPS 可编辑性验收", keywords: ["DOCX", "Pandoc", "OOXML"], language: "zh-CN" diff --git a/packages/docx-engine/scripts/verify-pandoc-conversion.mjs b/packages/docx-engine/scripts/verify-pandoc-conversion.mjs index 1f2e71c..69a11c9 100644 --- a/packages/docx-engine/scripts/verify-pandoc-conversion.mjs +++ b/packages/docx-engine/scripts/verify-pandoc-conversion.mjs @@ -104,7 +104,7 @@ const conversionInput = { themeTokens, metadata: { title: "Pandoc 媒体转换验收", - author: "Markdown PDF 导出器研发组", + author: "墨呈研发组", subject: "", keywords: [], language: "zh-CN" diff --git a/packages/docx-engine/scripts/verify-pandoc-reference.mjs b/packages/docx-engine/scripts/verify-pandoc-reference.mjs index 1ec7a06..f29ebb4 100644 --- a/packages/docx-engine/scripts/verify-pandoc-reference.mjs +++ b/packages/docx-engine/scripts/verify-pandoc-reference.mjs @@ -147,7 +147,7 @@ function createVariant( fileName: `${variant.id}.md`, metadata: { title: "阶段六动态模板验收", - author: "Markdown PDF 导出器研发组" + author: "墨呈研发组" } }); const referencePath = path.join( diff --git a/packages/preview-engine/README.md b/packages/preview-engine/README.md index 6854723..555a824 100644 --- a/packages/preview-engine/README.md +++ b/packages/preview-engine/README.md @@ -1,6 +1,6 @@ # @md-to-pdf/preview-engine -Markdown PDF 导出器的共享预览与分页引擎。Web 连续/快速预览、服务端 +墨呈的共享预览与分页引擎。Web 连续/快速预览、服务端 Playwright PDF 和 Electron PDF 均通过同一套运行时完成媒体渲染、尺寸 适配与 Paged.js 分页。 diff --git a/product.json b/product.json new file mode 100644 index 0000000..6aed499 --- /dev/null +++ b/product.json @@ -0,0 +1,12 @@ +{ + "appId": "com.md-to-pdf.desktop", + "englishName": "MorphDoc", + "displayName": "墨呈", + "windowTitle": "墨呈 · MorphDoc", + "slogan": "一稿多式,即写即呈", + "description": "面向结构化 Markdown 的主题化文档创作与发布工具", + "executableName": "MorphDoc", + "artifactPrefix": "MorphDoc", + "legacyDisplayName": "Markdown PDF 导出器", + "legacyExecutableName": "md-to-pdf" +} diff --git a/samples/tutorials/echarts-tutorial.md b/samples/tutorials/echarts-tutorial.md index 08451c2..ca07580 100644 --- a/samples/tutorials/echarts-tutorial.md +++ b/samples/tutorials/echarts-tutorial.md @@ -1,6 +1,6 @@ --- title: Markdown ECharts 图表教程 -author: Markdown PDF 导出器 +author: 墨呈 keywords: [ECharts, Markdown, 图表, 教程] --- diff --git a/samples/tutorials/formal-document-tutorial.md b/samples/tutorials/formal-document-tutorial.md index df74890..d6fc022 100644 --- a/samples/tutorials/formal-document-tutorial.md +++ b/samples/tutorials/formal-document-tutorial.md @@ -1,6 +1,6 @@ --- title: 公文与正式文档主题使用教程 -author: Markdown PDF 导出器 +author: 墨呈 keywords: [公文, 红头文件, 正式文档, 标书, 教程] ---