md-to-mowen 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 momolibrary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { config } from 'dotenv';
4
+ import { resolve, dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
7
+ import { homedir } from 'os';
8
+ import { createInterface } from 'readline';
9
+ import { processFile } from '../publish/process-file.js';
10
+ import { MowenClient } from '../mowen/client.js';
11
+ import { noteAtomToMast } from '../noteatom/to-mast.js';
12
+ import { mastToMarkdown } from '../mast/to-markdown.js';
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ /** 按优先级搜索 .env 文件 */
15
+ function searchEnvPaths() {
16
+ const projectRoot = resolve(__dirname, '../../');
17
+ return [
18
+ { label: '项目根目录', path: join(projectRoot, '.env') },
19
+ { label: '当前工作目录', path: resolve(process.cwd(), '.env') },
20
+ { label: '用户主目录', path: join(homedir(), '.md-to-mowen.env') },
21
+ ];
22
+ }
23
+ /** 加载 .env,返回找到的路径列表 */
24
+ function loadEnvConfig() {
25
+ const locations = searchEnvPaths();
26
+ const found = [];
27
+ for (const loc of locations) {
28
+ if (existsSync(loc.path)) {
29
+ config({ path: loc.path });
30
+ found.push(loc.label);
31
+ }
32
+ }
33
+ return found;
34
+ }
35
+ function getApiKey() {
36
+ return process.env.MOWEN_API_KEY;
37
+ }
38
+ function getEnvWritePath() {
39
+ // 写入项目根目录 .env
40
+ return join(resolve(__dirname, '../../'), '.env');
41
+ }
42
+ async function promptApiKey() {
43
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
44
+ console.log('');
45
+ console.log('获取 API Key 的步骤:');
46
+ console.log(' 1. 打开微信,搜索"墨问"小程序');
47
+ console.log(' 2. 进入个人主页');
48
+ console.log(' 3. 点击「开发者」');
49
+ console.log(' 4. 进入「我的 API Key」页面');
50
+ console.log(' 5. 复制 API Key');
51
+ console.log('');
52
+ return new Promise((resolve) => {
53
+ rl.question('请粘贴你的 MOWEN_API_KEY: ', (answer) => {
54
+ rl.close();
55
+ resolve(answer.trim());
56
+ });
57
+ });
58
+ }
59
+ function saveApiKey(apiKey) {
60
+ const envPath = getEnvWritePath();
61
+ const content = `MOWEN_API_KEY=${apiKey}\n`;
62
+ // 如果文件已存在,读取并替换;否则创建新文件
63
+ if (existsSync(envPath)) {
64
+ const existing = readFileSync(envPath, 'utf8');
65
+ const lines = existing.split('\n');
66
+ const idx = lines.findIndex((l) => l.startsWith('MOWEN_API_KEY='));
67
+ if (idx !== -1) {
68
+ lines[idx] = `MOWEN_API_KEY=${apiKey}`;
69
+ }
70
+ else {
71
+ lines.push(`MOWEN_API_KEY=${apiKey}`);
72
+ }
73
+ writeFileSync(envPath, lines.filter((l) => l.trim()).join('\n') + '\n', 'utf8');
74
+ }
75
+ else {
76
+ mkdirSync(dirname(envPath), { recursive: true });
77
+ writeFileSync(envPath, content, 'utf8');
78
+ }
79
+ console.log(`\n✅ API Key 已保存到 ${envPath}`);
80
+ console.log(' 下次运行无需再输入。');
81
+ }
82
+ // ── 加载配置 ──────────────────────────────────────────────────────────────────
83
+ loadEnvConfig();
84
+ // ── CLI ───────────────────────────────────────────────────────────────────────
85
+ const program = new Command();
86
+ program.name('md-to-mowen').description('将 Markdown(GFM)转换为墨问笔记').version('0.0.0');
87
+ // ── config ────────────────────────────────────────────────────────────────────
88
+ program
89
+ .command('config')
90
+ .description('设置墨问 API Key')
91
+ .action(async () => {
92
+ const existingKey = getApiKey();
93
+ if (existingKey) {
94
+ console.log(`当前 MOWEN_API_KEY: ${existingKey.slice(0, 8)}...${existingKey.slice(-4)}`);
95
+ }
96
+ const apiKey = await promptApiKey();
97
+ if (!apiKey) {
98
+ console.error('错误:API Key 不能为空。');
99
+ process.exit(1);
100
+ }
101
+ saveApiKey(apiKey);
102
+ });
103
+ // ── publish ────────────────────────────────────────────────────────────────────
104
+ program
105
+ .command('publish')
106
+ .description('发布 Markdown 文件为墨问笔记')
107
+ .requiredOption('-i, --input <file>', 'Markdown 文件路径')
108
+ .option('--note-id <id>', '已有笔记 ID(编辑模式,全量替换)')
109
+ .option('--tags <tags>', '标签,逗号分隔(如 "tech,ai")')
110
+ .option('--auto-publish', '自动发布(非草稿)', false)
111
+ .option('--dry-run', '走完流水线但不调用墨问 API,仅打印统计', false)
112
+ .option('--cache-dir <dir>', '保存各阶段产物的目录(调试用)', 'out/pipeline-cache')
113
+ .action(async (opts) => {
114
+ const apiKey = getApiKey();
115
+ if (!apiKey && !opts.dryRun) {
116
+ console.error('错误:未设置 MOWEN_API_KEY。');
117
+ console.error('');
118
+ console.error('请先运行以下命令配置 API Key:');
119
+ console.error(' md-to-mowen config');
120
+ console.error('');
121
+ console.error('获取方式:微信小程序"墨问" → 个人主页 → 开发者 → 我的 API Key');
122
+ process.exit(1);
123
+ }
124
+ const client = new MowenClient(apiKey ?? 'dry-run-placeholder');
125
+ const tags = opts.tags ? opts.tags.split(',').map((t) => t.trim()) : undefined;
126
+ try {
127
+ const result = await processFile(opts.input, client, {
128
+ ...(opts.noteId ? { noteId: opts.noteId } : {}),
129
+ ...(tags ? { tags } : {}),
130
+ autoPublish: opts.autoPublish,
131
+ dryRun: opts.dryRun,
132
+ ...(opts.cacheDir ? { cacheDir: opts.cacheDir } : {}),
133
+ });
134
+ if (!result.dryRun) {
135
+ console.log(`\n✅ 发布成功`);
136
+ console.log(` 笔记 ID:${result.noteId}`);
137
+ console.log(` 访问地址:${result.noteUrl}\n`);
138
+ }
139
+ }
140
+ catch (err) {
141
+ console.error('发布失败:', err instanceof Error ? err.message : err);
142
+ process.exit(1);
143
+ }
144
+ });
145
+ // ── to-markdown ────────────────────────────────────────────────────────────────
146
+ program
147
+ .command('to-markdown')
148
+ .description('将 NoteAtom JSON 转换为 Markdown')
149
+ .requiredOption('-i, --input <file>', 'NoteAtom JSON 文件路径')
150
+ .option('-o, --output <file>', '输出 Markdown 文件路径(不指定则输出到 stdout)')
151
+ .action(async (opts) => {
152
+ try {
153
+ const raw = (await import('fs/promises')).readFile;
154
+ const data = await raw(opts.input, 'utf8');
155
+ const noteAtom = JSON.parse(data);
156
+ if (!noteAtom.type || !Array.isArray(noteAtom.content)) {
157
+ console.error('错误:无效的 NoteAtom JSON,缺少 type 或 content 字段。');
158
+ process.exit(1);
159
+ }
160
+ const mast = noteAtomToMast(noteAtom);
161
+ const md = mastToMarkdown(mast);
162
+ if (opts.output) {
163
+ await (await import('fs/promises')).writeFile(opts.output, md, 'utf8');
164
+ console.log(`✅ 已输出到 ${opts.output}`);
165
+ }
166
+ else {
167
+ process.stdout.write(md);
168
+ }
169
+ }
170
+ catch (err) {
171
+ console.error('转换失败:', err instanceof Error ? err.message : err);
172
+ process.exit(1);
173
+ }
174
+ });
175
+ program.parse();
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 将 MASTDocument 序列化为 Markdown 文本。
3
+ */
4
+ export function mastToMarkdown(doc) {
5
+ const parts = [];
6
+ let prevType;
7
+ for (const id of doc.topLevel) {
8
+ const block = doc.blocks[id];
9
+ if (!block)
10
+ continue;
11
+ // 空行分隔:连续空 paragraph 合并为一个空行
12
+ if (block.type === 'paragraph' && block.content.length === 0) {
13
+ if (prevType === 'empty')
14
+ continue;
15
+ parts.push('');
16
+ prevType = 'empty';
17
+ continue;
18
+ }
19
+ // 块之间加空行(段落、引用块之间)
20
+ if (prevType && prevType !== 'empty') {
21
+ parts.push('');
22
+ }
23
+ prevType = block.type;
24
+ parts.push(serializeBlock(block, doc));
25
+ }
26
+ return parts.join('\n');
27
+ }
28
+ function serializeBlock(block, doc) {
29
+ switch (block.type) {
30
+ case 'paragraph':
31
+ return serializeParagraph(block);
32
+ case 'quote':
33
+ return serializeQuote(block, doc);
34
+ case 'image':
35
+ return serializeImage(block);
36
+ case 'audio':
37
+ return serializeAudio(block);
38
+ }
39
+ }
40
+ function serializeParagraph(block) {
41
+ if (block.content.length === 0)
42
+ return '';
43
+ return block.content.map(serializeTextRun).join('');
44
+ }
45
+ function serializeQuote(block, doc) {
46
+ const lines = [];
47
+ for (const childId of block.children) {
48
+ const child = doc.blocks[childId];
49
+ if (child && child.type === 'paragraph') {
50
+ const text = serializeParagraph(child);
51
+ // 段落内嵌换行,每行都要加 `> ` 前缀
52
+ for (const line of text.split('\n')) {
53
+ lines.push(`> ${line}`);
54
+ }
55
+ }
56
+ }
57
+ return lines.join('\n');
58
+ }
59
+ function serializeImage(block) {
60
+ const src = block.uuid ? block.src : block.src;
61
+ return `![${block.alt}](${src})`;
62
+ }
63
+ function serializeAudio(block) {
64
+ const src = block.uuid ? block.src : block.src;
65
+ return `![audio: ${block.showNote}](${src})`;
66
+ }
67
+ function serializeTextRun(run) {
68
+ if (!run.marks)
69
+ return run.text;
70
+ let text = run.text;
71
+ // code 最先包裹(innermost),遇到 code 时只保留 code
72
+ if (run.marks.code) {
73
+ text = `\`${text}\``;
74
+ // code 不与其他标记叠加
75
+ if (run.marks.link)
76
+ text = `[${text}](${run.marks.link})`;
77
+ return text;
78
+ }
79
+ // strikethrough
80
+ if (run.marks.strikethrough)
81
+ text = `~~${text}~~`;
82
+ // bold
83
+ if (run.marks.bold)
84
+ text = `**${text}**`;
85
+ // italic
86
+ if (run.marks.italic)
87
+ text = `*${text}*`;
88
+ // link 最后包裹(outermost)
89
+ if (run.marks.link)
90
+ text = `[${text}](${run.marks.link})`;
91
+ return text;
92
+ }
@@ -0,0 +1,3 @@
1
+ // MAST — Mowen AST(中间表示)
2
+ // HAST 与 NoteAtom 之间的标准中间格式
3
+ export {};
@@ -0,0 +1,72 @@
1
+ import { withRetry } from '../shared/retry.js';
2
+ const BASE_URL = 'https://open.mowen.cn';
3
+ /**
4
+ * 墨问 API 客户端
5
+ */
6
+ export class MowenClient {
7
+ apiKey;
8
+ constructor(apiKey) {
9
+ if (!apiKey)
10
+ throw new Error('MOWEN_API_KEY is required');
11
+ this.apiKey = apiKey;
12
+ }
13
+ get headers() {
14
+ return {
15
+ Authorization: `Bearer ${this.apiKey}`,
16
+ 'Content-Type': 'application/json',
17
+ };
18
+ }
19
+ async request(path, body) {
20
+ const res = await withRetry(() => fetch(`${BASE_URL}${path}`, {
21
+ method: 'POST',
22
+ headers: this.headers,
23
+ body: JSON.stringify(body),
24
+ }), {
25
+ shouldRetry: (err) => {
26
+ // 网络错误重试;4xx 不重试
27
+ if (err instanceof MowenApiError && err.code < 500)
28
+ return false;
29
+ return true;
30
+ },
31
+ });
32
+ const data = (await res.json());
33
+ if (!res.ok || data.code) {
34
+ throw new MowenApiError(data.code ?? res.status, data.reason ?? res.statusText);
35
+ }
36
+ return data;
37
+ }
38
+ /** 获取 OSS 上传凭证(本地上传第一步) */
39
+ async uploadPrepare(fileType, fileName) {
40
+ const res = await this.request('/api/open/api/v1/upload/prepare', { fileType, fileName });
41
+ return res.form;
42
+ }
43
+ /** 基于远程 URL 上传文件(一步完成) */
44
+ async uploadViaUrl(fileType, url, fileName) {
45
+ const res = await this.request('/api/open/api/v1/upload/url', {
46
+ fileType,
47
+ url,
48
+ ...(fileName ? { fileName } : {}),
49
+ });
50
+ return res.file.fileId;
51
+ }
52
+ /** 创建笔记,返回 noteId */
53
+ async createNote(body, settings) {
54
+ const res = await this.request('/api/open/api/v1/note/create', {
55
+ body,
56
+ settings: settings ?? {},
57
+ });
58
+ return res.noteId;
59
+ }
60
+ /** 编辑已有笔记(全量替换) */
61
+ async editNote(noteId, body) {
62
+ await this.request('/api/open/api/v1/note/edit', { noteId, body });
63
+ }
64
+ }
65
+ export class MowenApiError extends Error {
66
+ code;
67
+ constructor(code, message) {
68
+ super(`[${code}] ${message}`);
69
+ this.code = code;
70
+ this.name = 'MowenApiError';
71
+ }
72
+ }
@@ -0,0 +1,83 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { basename, extname } from 'path';
3
+ import { withRetry } from '../shared/retry.js';
4
+ const MIME_MAP = {
5
+ gif: 'image/gif',
6
+ jpeg: 'image/jpeg',
7
+ jpg: 'image/jpeg',
8
+ png: 'image/png',
9
+ webp: 'image/webp',
10
+ mp3: 'audio/mpeg',
11
+ m4a: 'audio/mp4',
12
+ mp4: 'audio/mp4',
13
+ pdf: 'application/pdf',
14
+ };
15
+ function mimeFromFileName(fileName) {
16
+ const ext = extname(fileName).slice(1).toLowerCase();
17
+ return MIME_MAP[ext] ?? 'application/octet-stream';
18
+ }
19
+ /**
20
+ * 上传本地图片文件,返回 fileId。
21
+ * 使用两步 OSS 上传流程:prepare → multipart POST。
22
+ */
23
+ export async function uploadLocalFile(filePath, client) {
24
+ const fileName = basename(filePath);
25
+ const fileBuffer = await readFile(filePath);
26
+ const form = await client.uploadPrepare(1, fileName);
27
+ await ossUpload(form, fileBuffer, fileName);
28
+ return form['x:file_id'];
29
+ }
30
+ /**
31
+ * 上传远程 URL 图片,返回 fileId。
32
+ */
33
+ export async function uploadRemoteUrl(url, client) {
34
+ return client.uploadViaUrl(1, url);
35
+ }
36
+ /**
37
+ * 上传 Data URI 图片,返回 fileId。
38
+ * 支持 data:image/png;base64,... 格式。
39
+ */
40
+ export async function uploadDataUri(dataUri, client) {
41
+ const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
42
+ if (!match)
43
+ throw new Error(`Invalid data URI: ${dataUri.slice(0, 40)}`);
44
+ const mime = match[1] ?? 'image/png';
45
+ const b64 = match[2] ?? '';
46
+ const ext = mime.split('/')[1] ?? 'png';
47
+ const fileName = `image.${ext}`;
48
+ const buffer = Buffer.from(b64, 'base64');
49
+ const form = await client.uploadPrepare(1, fileName);
50
+ await ossUpload(form, buffer, fileName);
51
+ return form['x:file_id'];
52
+ }
53
+ /**
54
+ * OSS multipart 直传(第二步)。
55
+ */
56
+ async function ossUpload(form, fileBuffer, fileName) {
57
+ await withRetry(async () => {
58
+ const formData = new FormData();
59
+ // 按 API 文档顺序添加所有表单字段(endpoint 除外)
60
+ const fields = [
61
+ 'key',
62
+ 'policy',
63
+ 'callback',
64
+ 'success_action_status',
65
+ 'x-oss-credential',
66
+ 'x-oss-date',
67
+ 'x-oss-meta-mo-uid',
68
+ 'x-oss-signature',
69
+ 'x-oss-signature-version',
70
+ 'x:file_id',
71
+ 'x:file_name',
72
+ 'x:file_uid',
73
+ ];
74
+ for (const field of fields) {
75
+ formData.append(field, form[field]);
76
+ }
77
+ formData.append('file', new Blob([fileBuffer], { type: mimeFromFileName(fileName) }), fileName);
78
+ const res = await fetch(form.endpoint, { method: 'POST', body: formData });
79
+ if (!res.ok) {
80
+ throw new Error(`OSS upload failed: ${res.status} ${res.statusText}`);
81
+ }
82
+ });
83
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * 将 MASTDocument 序列化为 NoteAtom JSON。
3
+ * 所有 MASTImageBlock 必须已有 uuid(资源阶段完成后)。
4
+ */
5
+ export function mastToNoteAtom(doc) {
6
+ const content = [];
7
+ for (const id of doc.topLevel) {
8
+ const block = doc.blocks[id];
9
+ if (block) {
10
+ const nodes = convertBlock(block, doc);
11
+ content.push(...nodes);
12
+ }
13
+ }
14
+ return { type: 'doc', content };
15
+ }
16
+ // ── 块节点转换 ─────────────────────────────────────────────────────────────────
17
+ function convertBlock(block, doc) {
18
+ switch (block.type) {
19
+ case 'paragraph':
20
+ return [convertParagraph(block)];
21
+ case 'quote':
22
+ return [convertQuote(block, doc)];
23
+ case 'image':
24
+ return [convertImage(block)];
25
+ case 'audio':
26
+ return [convertAudio(block)];
27
+ }
28
+ }
29
+ function convertParagraph(block) {
30
+ return {
31
+ type: 'paragraph',
32
+ content: block.content.map(convertTextRun),
33
+ };
34
+ }
35
+ function convertQuote(block, doc) {
36
+ const children = [];
37
+ for (const childId of block.children) {
38
+ const child = doc.blocks[childId];
39
+ if (!child)
40
+ continue;
41
+ if (child.type === 'paragraph') {
42
+ children.push(convertParagraph(child));
43
+ }
44
+ // quote 内嵌套 quote:展平为段落
45
+ else if (child.type === 'quote') {
46
+ const quoteChild = child;
47
+ for (const grandChildId of quoteChild.children) {
48
+ const grandChild = doc.blocks[grandChildId];
49
+ if (grandChild && grandChild.type === 'paragraph') {
50
+ children.push(convertParagraph(grandChild));
51
+ }
52
+ }
53
+ }
54
+ }
55
+ return { type: 'quote', content: children };
56
+ }
57
+ function convertImage(block) {
58
+ if (!block.uuid) {
59
+ throw new Error(`MASTImageBlock ${block.id} has no uuid — run asset processing before serialization`);
60
+ }
61
+ return {
62
+ type: 'image',
63
+ attrs: {
64
+ uuid: block.uuid,
65
+ alt: block.alt,
66
+ align: block.align,
67
+ },
68
+ };
69
+ }
70
+ function convertAudio(block) {
71
+ if (!block.uuid) {
72
+ throw new Error(`MASTAudioBlock ${block.id} has no uuid — run asset processing before serialization`);
73
+ }
74
+ return {
75
+ type: 'audio',
76
+ attrs: {
77
+ 'audio-uuid': block.uuid,
78
+ 'show-note': block.showNote,
79
+ },
80
+ };
81
+ }
82
+ // ── 行内节点转换 ───────────────────────────────────────────────────────────────
83
+ function convertTextRun(run) {
84
+ const node = { type: 'text', text: run.text };
85
+ if (!run.marks)
86
+ return node;
87
+ const marks = [];
88
+ // 按优先级顺序:code → strikethrough → bold → italic → link
89
+ if (run.marks.code)
90
+ marks.push({ type: 'code' });
91
+ if (run.marks.strikethrough)
92
+ marks.push({ type: 'strikethrough' });
93
+ if (run.marks.bold)
94
+ marks.push({ type: 'bold' });
95
+ if (run.marks.italic)
96
+ marks.push({ type: 'italic' });
97
+ if (run.marks.link)
98
+ marks.push({ type: 'link', attrs: { href: run.marks.link } });
99
+ if (marks.length > 0)
100
+ node.marks = marks;
101
+ return node;
102
+ }
@@ -0,0 +1,2 @@
1
+ export { mastToNoteAtom } from './from-mast.js';
2
+ export { noteAtomToMast } from './to-mast.js';
@@ -0,0 +1,103 @@
1
+ let _idCounter = 0;
2
+ function newId() {
3
+ return `b_${++_idCounter}`;
4
+ }
5
+ /**
6
+ * 将 NoteAtom JSON 解析为 MASTDocument。
7
+ */
8
+ export function noteAtomToMast(doc) {
9
+ _idCounter = 0;
10
+ const blocks = {};
11
+ const topLevel = [];
12
+ for (const node of doc.content) {
13
+ const id = convertNode(node, blocks);
14
+ if (id)
15
+ topLevel.push(id);
16
+ }
17
+ return { blocks, topLevel };
18
+ }
19
+ // ── 块节点转换 ─────────────────────────────────────────────────────────────────
20
+ function convertNode(node, blocks) {
21
+ switch (node.type) {
22
+ case 'paragraph':
23
+ return convertParagraph(node, blocks);
24
+ case 'quote':
25
+ return convertQuote(node, blocks);
26
+ case 'image':
27
+ return convertImage(node, blocks);
28
+ case 'audio':
29
+ return convertAudio(node, blocks);
30
+ }
31
+ }
32
+ function convertParagraph(block, blocks) {
33
+ const id = newId();
34
+ const content = (block.content ?? []).map(convertTextRun);
35
+ const mast = { id, type: 'paragraph', content };
36
+ blocks[id] = mast;
37
+ return id;
38
+ }
39
+ function convertQuote(block, blocks) {
40
+ const id = newId();
41
+ const children = [];
42
+ for (const child of block.content) {
43
+ const childId = convertParagraph(child, blocks);
44
+ children.push(childId);
45
+ }
46
+ const mast = { id, type: 'quote', children };
47
+ blocks[id] = mast;
48
+ return id;
49
+ }
50
+ function convertImage(block, blocks) {
51
+ const id = newId();
52
+ const mast = {
53
+ id,
54
+ type: 'image',
55
+ src: `mowen://file/${block.attrs.uuid}`,
56
+ uuid: block.attrs.uuid,
57
+ alt: block.attrs.alt,
58
+ align: block.attrs.align ?? 'center',
59
+ };
60
+ blocks[id] = mast;
61
+ return id;
62
+ }
63
+ function convertAudio(block, blocks) {
64
+ const id = newId();
65
+ const mast = {
66
+ id,
67
+ type: 'audio',
68
+ src: `mowen://file/${block.attrs['audio-uuid']}`,
69
+ uuid: block.attrs['audio-uuid'],
70
+ showNote: block.attrs['show-note'] ?? '',
71
+ };
72
+ blocks[id] = mast;
73
+ return id;
74
+ }
75
+ // ── 行内节点转换 ───────────────────────────────────────────────────────────────
76
+ function convertTextRun(run) {
77
+ const mast = { type: 'text', text: run.text };
78
+ if (!run.marks || run.marks.length === 0)
79
+ return mast;
80
+ const marks = {};
81
+ for (const mark of run.marks) {
82
+ switch (mark.type) {
83
+ case 'bold':
84
+ marks.bold = true;
85
+ break;
86
+ case 'italic':
87
+ marks.italic = true;
88
+ break;
89
+ case 'code':
90
+ marks.code = true;
91
+ break;
92
+ case 'strikethrough':
93
+ marks.strikethrough = true;
94
+ break;
95
+ case 'link':
96
+ marks.link = mark.attrs.href;
97
+ break;
98
+ }
99
+ }
100
+ if (Object.keys(marks).length > 0)
101
+ mast.marks = marks;
102
+ return mast;
103
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * NoteAtom 类型定义
3
+ * 墨问的 ProseMirror 风格文档格式
4
+ */
5
+ export {};