feat: 完成 v0.6.0 墨呈品牌迁移
This commit is contained in:
@@ -5,7 +5,7 @@ YIXIONG Tech.ltd 公司内部专属软件许可证
|
||||
|
||||
一、适用范围
|
||||
|
||||
本许可证适用于“Markdown PDF 导出器”项目中由 YIXIONG Tech.ltd 拥有
|
||||
本许可证适用于“墨呈(MorphDoc)”项目中由 YIXIONG Tech.ltd 拥有
|
||||
著作权的源代码、原创主题、原创文档、构建脚本和其他原创材料(以下简称
|
||||
“本软件”)。
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Markdown PDF 导出器
|
||||
墨呈(MorphDoc)
|
||||
Copyright 2026 YIXIONG Tech.ltd
|
||||
|
||||
This product includes software developed by YIXIONG Tech.ltd and
|
||||
|
||||
@@ -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;当前发行包
|
||||
未配置代码签名。
|
||||
|
||||
## 文档与媒体
|
||||
|
||||
@@ -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` 下,生产环境通过
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";',
|
||||
|
||||
Vendored
+5
@@ -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;
|
||||
@@ -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(
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<LegacyUserDataMigrationResult> {
|
||||
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: [] };
|
||||
}
|
||||
@@ -3,14 +3,25 @@ import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
interface PackageManifest {
|
||||
version?: string;
|
||||
productName?: string;
|
||||
scripts?: Record<string, string>;
|
||||
}
|
||||
|
||||
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<T = PackageManifest>(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<ProductBrand>(
|
||||
`${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`,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,7 @@ function temporaryDirectories() {
|
||||
|
||||
const markdown = `---
|
||||
title: Server HTTP DOCX 验收
|
||||
author: Markdown PDF 导出器研发组
|
||||
author: 墨呈研发组
|
||||
---
|
||||
|
||||
# HTTP 导出保持可编辑
|
||||
|
||||
+2
-2
@@ -17,9 +17,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="可配置、可主题化的 Markdown PDF 导出工具"
|
||||
content="面向结构化 Markdown 的主题化文档创作与发布工具"
|
||||
/>
|
||||
<title>Markdown PDF 导出器</title>
|
||||
<title>墨呈 · MorphDoc</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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() {
|
||||
<header className="topbar">
|
||||
<div className="topbar-primary">
|
||||
<div className="topbar-brand">
|
||||
<p className="eyebrow">内网文档工具</p>
|
||||
<p className="eyebrow">{APP_SLOGAN}</p>
|
||||
<div className="topbar-brand-title">
|
||||
<h1>Markdown PDF 导出器</h1>
|
||||
<h1>{APP_DISPLAY_NAME}</h1>
|
||||
<span className="app-version">{APP_VERSION_LABEL}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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__;
|
||||
@@ -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) {
|
||||
|
||||
Vendored
+4
@@ -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;
|
||||
|
||||
@@ -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("<title>墨呈 · MorphDoc</title>");
|
||||
expect(mainSource).toContain(
|
||||
"document.title = `${APP_WINDOW_TITLE} ${APP_VERSION_LABEL}`"
|
||||
);
|
||||
expect(indexHtml).not.toContain("Markdown PDF 导出器");
|
||||
expect(indexHtml).not.toContain("内网文档工具");
|
||||
});
|
||||
});
|
||||
@@ -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", () => {
|
||||
|
||||
+16
-1
@@ -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: {
|
||||
|
||||
+10
-2
@@ -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 双向互存、外部主题兼容、体积和正式发布验收。
|
||||
|
||||
每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Markdown PDF 导出器",
|
||||
"short_name": "Markdown PDF",
|
||||
"name": "墨呈 · MorphDoc",
|
||||
"short_name": "墨呈",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#f4f2ed",
|
||||
|
||||
Generated
+3
-3
@@ -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",
|
||||
|
||||
+2
-2
@@ -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/*"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: v0.6.0 DOCX 自动验收
|
||||
author: Markdown PDF 导出器研发组
|
||||
author: 墨呈研发组
|
||||
subject: Word 与 WPS 可编辑性验收
|
||||
keywords: [DOCX, Pandoc, OOXML]
|
||||
lang: zh-CN
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: DOCX 媒体视觉专项验收
|
||||
author: Markdown PDF 导出器研发组
|
||||
author: 墨呈研发组
|
||||
lang: zh-CN
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Pandoc 媒体转换验收
|
||||
author: Markdown PDF 导出器研发组
|
||||
author: 墨呈研发组
|
||||
---
|
||||
|
||||
# 可编辑正文
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 阶段六动态模板验收
|
||||
author: Markdown PDF 导出器研发组
|
||||
author: 墨呈研发组
|
||||
---
|
||||
|
||||
# 一级标题
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -104,7 +104,7 @@ const conversionInput = {
|
||||
themeTokens,
|
||||
metadata: {
|
||||
title: "Pandoc 媒体转换验收",
|
||||
author: "Markdown PDF 导出器研发组",
|
||||
author: "墨呈研发组",
|
||||
subject: "",
|
||||
keywords: [],
|
||||
language: "zh-CN"
|
||||
|
||||
@@ -147,7 +147,7 @@ function createVariant(
|
||||
fileName: `${variant.id}.md`,
|
||||
metadata: {
|
||||
title: "阶段六动态模板验收",
|
||||
author: "Markdown PDF 导出器研发组"
|
||||
author: "墨呈研发组"
|
||||
}
|
||||
});
|
||||
const referencePath = path.join(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# @md-to-pdf/preview-engine
|
||||
|
||||
Markdown PDF 导出器的共享预览与分页引擎。Web 连续/快速预览、服务端
|
||||
墨呈的共享预览与分页引擎。Web 连续/快速预览、服务端
|
||||
Playwright PDF 和 Electron PDF 均通过同一套运行时完成媒体渲染、尺寸
|
||||
适配与 Paged.js 分页。
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Markdown ECharts 图表教程
|
||||
author: Markdown PDF 导出器
|
||||
author: 墨呈
|
||||
keywords: [ECharts, Markdown, 图表, 教程]
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 公文与正式文档主题使用教程
|
||||
author: Markdown PDF 导出器
|
||||
author: 墨呈
|
||||
keywords: [公文, 红头文件, 正式文档, 标书, 教程]
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user