yuque-cookie-plugin 0.1.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/.env.example +5 -0
- package/README.md +253 -0
- package/bin/yuque-local.mjs +3 -0
- package/dist/auth.js +439 -0
- package/dist/cli.js +692 -0
- package/dist/client-cookie.js +544 -0
- package/dist/download-utils.js +579 -0
- package/dist/downloader.js +388 -0
- package/dist/editor-bridge.js +240 -0
- package/dist/fs-utils.js +9 -0
- package/dist/lake-diff.js +113 -0
- package/dist/lake-heading-numbering.js +29 -0
- package/dist/lake-insert.js +34 -0
- package/dist/lake-markdown.js +54 -0
- package/dist/lake-parser.js +79 -0
- package/dist/lake-transform.js +12 -0
- package/dist/reports.js +10 -0
- package/dist/serve-book.js +134 -0
- package/dist/types.js +1 -0
- package/docs/docker-quickstart.md +96 -0
- package/docs/native-lake-capability.md +101 -0
- package/docs/npm-release.md +91 -0
- package/docs/real-acceptance.md +146 -0
- package/docs/usage-zh.md +580 -0
- package/package.json +71 -0
- package/skills/yuque-cookie-plugin/SKILL.md +58 -0
- package/skills/yuque-cookie-plugin/commands.yaml +61 -0
- package/skills/yuque-cookie-plugin/reference/agent-spec.md +51 -0
- package/vendor/lake-editor/CodeMirror.js +1 -0
- package/vendor/lake-editor/antd.4.24.13.css +26886 -0
- package/vendor/lake-editor/doc.css +23603 -0
- package/vendor/lake-editor/doc.umd.js +2 -0
- package/vendor/lake-editor/katex.js +18829 -0
- package/vendor/lake-editor/lake-editor-icon.js +1 -0
- package/vendor/lake-editor/react-dom.production.min.js +267 -0
- package/vendor/lake-editor/react.production.min.js +31 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeReport } from "./reports.js";
|
|
4
|
+
import { buildResourceManifest, buildTocMaps, fixInlineCode, fixLatex, fixMarkdownImage, fixPath, formatDate, getMarkdownImageList, handleMarkdownFooter, localizeAttachments, localizeFileCards, localizeImages, localizeMedia, parseSheet, readProgress, shouldSkipByIncremental, writeProgress, writeSummary } from "./download-utils.js";
|
|
5
|
+
export async function downloadBook(client, url, options) {
|
|
6
|
+
const book = await client.getBookInfo(url);
|
|
7
|
+
if (!book.tocList.length)
|
|
8
|
+
throw new Error('No found toc list');
|
|
9
|
+
const bookPath = path.resolve(options.distDir, book.bookName ? fixPath(book.bookName) : String(book.bookId));
|
|
10
|
+
await mkdir(bookPath, { recursive: true });
|
|
11
|
+
const previous = new Map((await readProgress(bookPath)).map((item) => [item.toc.uuid, item]));
|
|
12
|
+
const tocMap = buildTocMaps(book.tocList);
|
|
13
|
+
const progressItems = [];
|
|
14
|
+
const articleUrlPrefix = url.replace(new RegExp(`(.*?/${book.bookSlug}).*`), '$1');
|
|
15
|
+
const failures = [];
|
|
16
|
+
const warnings = [];
|
|
17
|
+
let downloaded = 0;
|
|
18
|
+
let skipped = 0;
|
|
19
|
+
let handledDocs = 0;
|
|
20
|
+
const docTotal = book.tocList.filter((item) => item.type?.toLowerCase() !== 'title' && item.type?.toLowerCase() !== 'link' && item.url).length;
|
|
21
|
+
logProgress(options, `Downloading book "${book.bookName || book.bookId}" (${docTotal} docs) -> ${bookPath}`);
|
|
22
|
+
for (const item of book.tocList) {
|
|
23
|
+
const progressItem = buildProgressItem(item, tocMap);
|
|
24
|
+
if (!progressItem)
|
|
25
|
+
continue;
|
|
26
|
+
progressItems.push(progressItem);
|
|
27
|
+
const itemType = item.type.toLowerCase();
|
|
28
|
+
if (itemType === 'title' && !item.url) {
|
|
29
|
+
await mkdir(path.resolve(bookPath, progressItem.path), { recursive: true });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (itemType === 'link') {
|
|
33
|
+
warnings.push({
|
|
34
|
+
type: 'link',
|
|
35
|
+
title: item.title,
|
|
36
|
+
url: item.url,
|
|
37
|
+
error: 'TOC link node is not a Yuque document and was not downloaded.'
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!item.url)
|
|
42
|
+
continue;
|
|
43
|
+
try {
|
|
44
|
+
const articleUrl = `${articleUrlPrefix}/${item.url}`;
|
|
45
|
+
logProgress(options, `[${handledDocs + 1}/${docTotal}] ${item.title}`);
|
|
46
|
+
const articleResult = await downloadArticleForTest(client, {
|
|
47
|
+
bookId: book.bookId,
|
|
48
|
+
itemUrl: item.url,
|
|
49
|
+
savePath: path.resolve(bookPath, progressItem.savePath || ''),
|
|
50
|
+
saveFilePath: path.resolve(bookPath, progressItem.path),
|
|
51
|
+
uuid: item.uuid,
|
|
52
|
+
articleTitle: item.title,
|
|
53
|
+
articleUrl,
|
|
54
|
+
host: book.host,
|
|
55
|
+
imageServiceDomains: book.imageServiceDomains,
|
|
56
|
+
options,
|
|
57
|
+
progressItem,
|
|
58
|
+
previousProgressItem: previous.get(item.uuid),
|
|
59
|
+
warnings
|
|
60
|
+
});
|
|
61
|
+
if (articleResult.skipped) {
|
|
62
|
+
skipped += 1;
|
|
63
|
+
logProgress(options, ` skipped unchanged: ${progressItem.path}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
downloaded += 1;
|
|
67
|
+
logProgress(options, ` saved: ${progressItem.path}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logProgress(options, ` failed: ${item.title}`);
|
|
72
|
+
failures.push({
|
|
73
|
+
title: item.title,
|
|
74
|
+
url: item.url,
|
|
75
|
+
retry_url: `${articleUrlPrefix}/${item.url}`,
|
|
76
|
+
error: error instanceof Error ? error.message : String(error)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
handledDocs += 1;
|
|
81
|
+
}
|
|
82
|
+
await writeProgress(bookPath, progressItems);
|
|
83
|
+
}
|
|
84
|
+
await writeSummary({
|
|
85
|
+
bookPath,
|
|
86
|
+
bookName: book.bookName,
|
|
87
|
+
bookDesc: book.bookDesc,
|
|
88
|
+
progressItems
|
|
89
|
+
});
|
|
90
|
+
await writeProgress(bookPath, progressItems);
|
|
91
|
+
const resourceManifest = await buildResourceManifest(bookPath);
|
|
92
|
+
const summary = {
|
|
93
|
+
ok: failures.length === 0,
|
|
94
|
+
book_path: bookPath,
|
|
95
|
+
total: progressItems.length,
|
|
96
|
+
docs: progressItems.filter((item) => {
|
|
97
|
+
const type = item.toc.type?.toLowerCase();
|
|
98
|
+
return type !== 'title' && type !== 'link' && item.path.endsWith('.md');
|
|
99
|
+
}).length,
|
|
100
|
+
downloaded,
|
|
101
|
+
skipped,
|
|
102
|
+
incremental: options.incremental,
|
|
103
|
+
options,
|
|
104
|
+
resources: resourceManifest,
|
|
105
|
+
warnings,
|
|
106
|
+
warning_summary: buildWarningSummary(warnings),
|
|
107
|
+
failures,
|
|
108
|
+
retry: buildRetryPlan('download-doc', failures.map((item) => item.retry_url || item.url).filter(isString), options)
|
|
109
|
+
};
|
|
110
|
+
const report = await writeReport('download-book', summary);
|
|
111
|
+
return {
|
|
112
|
+
...summary,
|
|
113
|
+
report
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export async function downloadDocs(client, urls, options) {
|
|
117
|
+
const distPath = path.resolve(options.distDir);
|
|
118
|
+
await mkdir(distPath, { recursive: true });
|
|
119
|
+
const failures = [];
|
|
120
|
+
const warnings = [];
|
|
121
|
+
const files = [];
|
|
122
|
+
let downloaded = 0;
|
|
123
|
+
logProgress(options, `Downloading ${urls.length} doc(s) -> ${distPath}`);
|
|
124
|
+
for (const [index, url] of urls.entries()) {
|
|
125
|
+
try {
|
|
126
|
+
const doc = await client.getDocInfoFromUrl(url);
|
|
127
|
+
if (!doc.docSlug || !doc.bookId)
|
|
128
|
+
throw new Error('Failed to get document info from URL');
|
|
129
|
+
const fileName = fixPath(doc.docTitle || doc.docSlug);
|
|
130
|
+
const saveFilePath = path.resolve(distPath, `${fileName}.md`);
|
|
131
|
+
logProgress(options, `[${index + 1}/${urls.length}] ${doc.docTitle || doc.docSlug}`);
|
|
132
|
+
const progressItem = {
|
|
133
|
+
path: `${fileName}.md`,
|
|
134
|
+
savePath: '',
|
|
135
|
+
pathTitleList: [fileName],
|
|
136
|
+
pathIdList: [String(doc.docId || doc.docSlug)],
|
|
137
|
+
toc: {
|
|
138
|
+
type: 'DOC',
|
|
139
|
+
title: doc.docTitle || doc.docSlug,
|
|
140
|
+
uuid: String(doc.docId || doc.docSlug),
|
|
141
|
+
url: doc.docSlug,
|
|
142
|
+
parent_uuid: '',
|
|
143
|
+
child_uuid: ''
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
await downloadArticleForTest(client, {
|
|
147
|
+
bookId: doc.bookId,
|
|
148
|
+
itemUrl: doc.docSlug,
|
|
149
|
+
savePath: distPath,
|
|
150
|
+
saveFilePath,
|
|
151
|
+
uuid: String(doc.docId || doc.docSlug),
|
|
152
|
+
articleTitle: doc.docTitle || doc.docSlug,
|
|
153
|
+
articleUrl: url,
|
|
154
|
+
host: doc.host,
|
|
155
|
+
imageServiceDomains: doc.imageServiceDomains,
|
|
156
|
+
options,
|
|
157
|
+
progressItem,
|
|
158
|
+
warnings
|
|
159
|
+
});
|
|
160
|
+
downloaded += 1;
|
|
161
|
+
files.push(saveFilePath);
|
|
162
|
+
logProgress(options, ` saved: ${saveFilePath}`);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
logProgress(options, ` failed: ${url}`);
|
|
166
|
+
failures.push({
|
|
167
|
+
url,
|
|
168
|
+
error: error instanceof Error ? error.message : String(error)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const resourceManifest = await buildResourceManifest(distPath);
|
|
173
|
+
const summary = {
|
|
174
|
+
ok: failures.length === 0,
|
|
175
|
+
dist_path: distPath,
|
|
176
|
+
files,
|
|
177
|
+
downloaded,
|
|
178
|
+
options,
|
|
179
|
+
resources: resourceManifest,
|
|
180
|
+
warnings,
|
|
181
|
+
warning_summary: buildWarningSummary(warnings),
|
|
182
|
+
failures,
|
|
183
|
+
retry: buildRetryPlan('download-doc', failures.map((item) => item.url).filter(isString), options)
|
|
184
|
+
};
|
|
185
|
+
const report = await writeReport('download-doc', summary);
|
|
186
|
+
return {
|
|
187
|
+
...summary,
|
|
188
|
+
report
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export async function downloadArticleForTest(client, params) {
|
|
192
|
+
const { response, apiUrl, httpStatus } = await client.getDocMarkdownData({
|
|
193
|
+
articleUrl: params.itemUrl,
|
|
194
|
+
bookId: params.bookId,
|
|
195
|
+
host: params.host,
|
|
196
|
+
isMarkdown: true
|
|
197
|
+
});
|
|
198
|
+
const data = response?.data || {};
|
|
199
|
+
params.progressItem.createAt = data.created_at || '';
|
|
200
|
+
params.progressItem.contentUpdatedAt = data.content_updated_at || '';
|
|
201
|
+
params.progressItem.publishedAt = data.published_at || '';
|
|
202
|
+
params.progressItem.firstPublishedAt = data.first_published_at || '';
|
|
203
|
+
if (shouldSkipByIncremental(params.progressItem, params.previousProgressItem, params.options.incremental)) {
|
|
204
|
+
return { skipped: true };
|
|
205
|
+
}
|
|
206
|
+
let markdown = '';
|
|
207
|
+
const type = String(data.type || '').toLowerCase();
|
|
208
|
+
if (type === 'sheet') {
|
|
209
|
+
const raw = await client.getDocMarkdownData({
|
|
210
|
+
articleUrl: params.itemUrl,
|
|
211
|
+
bookId: params.bookId,
|
|
212
|
+
host: params.host,
|
|
213
|
+
isMarkdown: false
|
|
214
|
+
});
|
|
215
|
+
const rawContent = raw.response?.data?.content;
|
|
216
|
+
const content = rawContent ? JSON.parse(rawContent) : {};
|
|
217
|
+
markdown = content?.sheet ? parseSheet(content.sheet) : '';
|
|
218
|
+
}
|
|
219
|
+
else if (type === 'board' || type === 'table') {
|
|
220
|
+
markdown = `[Unsupported Yuque document type: ${type}]\n`;
|
|
221
|
+
}
|
|
222
|
+
else if (typeof data.sourcecode === 'string') {
|
|
223
|
+
markdown = fixLatex(data.sourcecode);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
throw new Error(`download article Error: ${apiUrl}, http status ${httpStatus}`);
|
|
227
|
+
}
|
|
228
|
+
const raw = await client.getDocMarkdownData({
|
|
229
|
+
articleUrl: params.itemUrl,
|
|
230
|
+
bookId: params.bookId,
|
|
231
|
+
host: params.host,
|
|
232
|
+
isMarkdown: false
|
|
233
|
+
});
|
|
234
|
+
const htmlData = raw.response?.data?.content || '';
|
|
235
|
+
const imgList = getMarkdownImageList(markdown);
|
|
236
|
+
if (imgList.length && !params.options.ignoreImg) {
|
|
237
|
+
markdown = fixMarkdownImage(imgList, markdown, htmlData);
|
|
238
|
+
}
|
|
239
|
+
if (params.options.ignoreAttachments !== true) {
|
|
240
|
+
markdown = await localizeAttachments(markdown, {
|
|
241
|
+
savePath: params.savePath,
|
|
242
|
+
attachmentsDir: `attachments/${fixPath(params.uuid)}`,
|
|
243
|
+
headers: client.htmlHeaders(),
|
|
244
|
+
ignoreAttachments: params.options.ignoreAttachments,
|
|
245
|
+
warnings: params.warnings,
|
|
246
|
+
title: params.articleTitle
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (params.options.ignoreAttachments !== true) {
|
|
250
|
+
markdown = await localizeFileCards(markdown, htmlData, {
|
|
251
|
+
savePath: params.savePath,
|
|
252
|
+
attachmentsDir: `attachments/${fixPath(params.uuid)}`,
|
|
253
|
+
headers: client.htmlHeaders(),
|
|
254
|
+
ignoreAttachments: params.options.ignoreAttachments,
|
|
255
|
+
warnings: params.warnings,
|
|
256
|
+
title: params.articleTitle
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (params.options.ignoreAttachments !== true) {
|
|
260
|
+
markdown = await localizeMedia(markdown, htmlData, {
|
|
261
|
+
savePath: params.savePath,
|
|
262
|
+
attachmentsDir: `attachments/${fixPath(params.uuid)}`,
|
|
263
|
+
headers: client.htmlHeaders(),
|
|
264
|
+
ignoreAttachments: params.options.ignoreAttachments,
|
|
265
|
+
getVideoInfo: (videoId) => client.getVideoInfo(videoId),
|
|
266
|
+
warnings: params.warnings,
|
|
267
|
+
title: params.articleTitle
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (!params.options.ignoreImg) {
|
|
271
|
+
markdown = await localizeImages(markdown, {
|
|
272
|
+
savePath: params.savePath,
|
|
273
|
+
imageDir: `img/${fixPath(params.uuid)}`,
|
|
274
|
+
referer: params.articleUrl,
|
|
275
|
+
headers: client.htmlHeaders(),
|
|
276
|
+
imageServiceDomains: params.imageServiceDomains,
|
|
277
|
+
warnings: params.warnings,
|
|
278
|
+
title: params.articleTitle
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
markdown = fixInlineCode(markdown, htmlData);
|
|
282
|
+
markdown = handleMarkdownFooter(markdown, {
|
|
283
|
+
articleTitle: params.articleTitle,
|
|
284
|
+
articleUrl: params.articleUrl,
|
|
285
|
+
toc: params.options.toc,
|
|
286
|
+
articleUpdateTime: formatDate(data.content_updated_at || ''),
|
|
287
|
+
hideFooter: params.options.hideFooter,
|
|
288
|
+
convertMarkdownVideoLinks: params.options.convertMarkdownVideoLinks
|
|
289
|
+
});
|
|
290
|
+
await mkdir(path.dirname(params.saveFilePath), { recursive: true });
|
|
291
|
+
await writeFile(params.saveFilePath, markdown);
|
|
292
|
+
return { skipped: false };
|
|
293
|
+
}
|
|
294
|
+
function buildProgressItem(item, tocMap) {
|
|
295
|
+
if (!item.type)
|
|
296
|
+
return null;
|
|
297
|
+
const pathItems = getPathItems(item, tocMap);
|
|
298
|
+
const pathTitleList = pathItems.map((toc) => fixPath(toc.title));
|
|
299
|
+
const pathIdList = pathItems.map((toc) => toc.uuid);
|
|
300
|
+
const type = item.type.toLowerCase();
|
|
301
|
+
if (type === 'title' && !item.url) {
|
|
302
|
+
return {
|
|
303
|
+
path: pathTitleList.join('/'),
|
|
304
|
+
pathTitleList,
|
|
305
|
+
pathIdList,
|
|
306
|
+
toc: item
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (!item.url)
|
|
310
|
+
return null;
|
|
311
|
+
if (type === 'link') {
|
|
312
|
+
return {
|
|
313
|
+
path: pathTitleList.join('/'),
|
|
314
|
+
savePath: pathTitleList.slice(0, -1).join('/'),
|
|
315
|
+
pathTitleList,
|
|
316
|
+
pathIdList,
|
|
317
|
+
toc: item
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
let mdPath = `${pathTitleList.join('/')}.md`;
|
|
321
|
+
let savePath = pathTitleList.slice(0, -1).join('/');
|
|
322
|
+
if (type === 'doc' && item.child_uuid) {
|
|
323
|
+
mdPath = `${pathTitleList.join('/')}/index.md`;
|
|
324
|
+
savePath = pathTitleList.join('/');
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
path: mdPath,
|
|
328
|
+
savePath,
|
|
329
|
+
pathTitleList,
|
|
330
|
+
pathIdList,
|
|
331
|
+
toc: item
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function getPathItems(item, tocMap) {
|
|
335
|
+
const items = [item];
|
|
336
|
+
let current = item;
|
|
337
|
+
while (current.parent_uuid && tocMap.has(current.parent_uuid)) {
|
|
338
|
+
current = tocMap.get(current.parent_uuid);
|
|
339
|
+
items.unshift(current);
|
|
340
|
+
}
|
|
341
|
+
return items;
|
|
342
|
+
}
|
|
343
|
+
function logProgress(options, message) {
|
|
344
|
+
if (options.quiet)
|
|
345
|
+
return;
|
|
346
|
+
process.stderr.write(`${message}\n`);
|
|
347
|
+
}
|
|
348
|
+
function buildRetryPlan(command, urls, options) {
|
|
349
|
+
return {
|
|
350
|
+
command,
|
|
351
|
+
urls,
|
|
352
|
+
count: urls.length,
|
|
353
|
+
args: [
|
|
354
|
+
command,
|
|
355
|
+
...urls,
|
|
356
|
+
'--dist-dir',
|
|
357
|
+
options.distDir,
|
|
358
|
+
...(options.ignoreImg ? ['--ignore-img'] : []),
|
|
359
|
+
...(options.ignoreAttachments === true ? ['--ignore-attachments'] : []),
|
|
360
|
+
...(typeof options.ignoreAttachments === 'string' ? ['--ignore-attachments', options.ignoreAttachments] : []),
|
|
361
|
+
...(options.toc ? ['--toc'] : []),
|
|
362
|
+
...(options.hideFooter ? ['--hide-footer'] : []),
|
|
363
|
+
...(options.quiet ? ['--quiet'] : [])
|
|
364
|
+
]
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function isString(value) {
|
|
368
|
+
return typeof value === 'string' && value.length > 0;
|
|
369
|
+
}
|
|
370
|
+
function buildWarningSummary(warnings) {
|
|
371
|
+
const byType = {};
|
|
372
|
+
for (const warning of warnings) {
|
|
373
|
+
byType[warning.type] = (byType[warning.type] || 0) + 1;
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
total: warnings.length,
|
|
377
|
+
by_type: byType,
|
|
378
|
+
retryable_resources: warnings
|
|
379
|
+
.filter((warning) => warning.type !== 'link' && Boolean(warning.url))
|
|
380
|
+
.map((warning) => ({
|
|
381
|
+
type: warning.type,
|
|
382
|
+
title: warning.title,
|
|
383
|
+
url: warning.url,
|
|
384
|
+
file: warning.file,
|
|
385
|
+
error: warning.error
|
|
386
|
+
}))
|
|
387
|
+
};
|
|
388
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
|
+
const VENDOR_DIR = path.join(PACKAGE_ROOT, 'vendor/lake-editor');
|
|
7
|
+
export async function serializeHtmlToLake({ html, out, keepTemp = false }) {
|
|
8
|
+
const { chromium } = await importPlaywright();
|
|
9
|
+
const tempDir = await makeTempDir();
|
|
10
|
+
const pagePath = path.join(tempDir, 'editor-bridge.html');
|
|
11
|
+
const vendorAvailable = await hasOfficialEditorAssets();
|
|
12
|
+
await writeFile(pagePath, bridgeHtml({ vendorAvailable }), 'utf8');
|
|
13
|
+
const browser = await chromium.launch({ headless: true });
|
|
14
|
+
try {
|
|
15
|
+
const page = await browser.newPage();
|
|
16
|
+
await page.goto(pathToFileURL(pagePath).href);
|
|
17
|
+
const result = await page.evaluate(async (input) => {
|
|
18
|
+
return window.__yuqueBridge.serialize(input);
|
|
19
|
+
}, html);
|
|
20
|
+
if (!result?.lake?.trim())
|
|
21
|
+
throw new Error('Editor bridge returned empty Lake content.');
|
|
22
|
+
if (out) {
|
|
23
|
+
await mkdir(path.dirname(path.resolve(out)), { recursive: true });
|
|
24
|
+
await writeFile(out, result.lake, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
if (/Executable doesn't exist|browserType.launch/.test(message)) {
|
|
31
|
+
throw new Error(`${message}\nRun "npx playwright install chromium" and try again.`);
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await browser.close();
|
|
37
|
+
if (!keepTemp)
|
|
38
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function importPlaywright() {
|
|
42
|
+
try {
|
|
43
|
+
return await import('playwright');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error('Missing Playwright. Run "npm install --save-dev playwright" first.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function makeTempDir() {
|
|
50
|
+
await mkdir(path.join(os.tmpdir(), 'yuque-editor-bridge-'), { recursive: true });
|
|
51
|
+
return mkdtemp(path.join(os.tmpdir(), 'yuque-editor-bridge-'));
|
|
52
|
+
}
|
|
53
|
+
async function hasOfficialEditorAssets() {
|
|
54
|
+
try {
|
|
55
|
+
await access(path.join(VENDOR_DIR, 'doc.umd.js'));
|
|
56
|
+
await access(path.join(VENDOR_DIR, 'doc.css'));
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function bridgeHtml({ vendorAvailable }) {
|
|
64
|
+
const vendorUrl = pathToFileURL(VENDOR_DIR).href;
|
|
65
|
+
return `<!doctype html>
|
|
66
|
+
<html>
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="utf-8">
|
|
69
|
+
<title>Yuque Editor Bridge</title>
|
|
70
|
+
${vendorAvailable ? `<link rel="stylesheet" href="${vendorUrl}/antd.4.24.13.css"><link rel="stylesheet" href="${vendorUrl}/doc.css">` : ''}
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<div id="root"></div>
|
|
74
|
+
${vendorAvailable ? `<script>window.KITCHEN_URL = ${JSON.stringify(`${vendorUrl}/lake-editor-icon.js`)};</script><script src="${vendorUrl}/react.production.min.js"></script><script src="${vendorUrl}/react-dom.production.min.js"></script><script src="${vendorUrl}/doc.umd.js"></script>` : ''}
|
|
75
|
+
<script>
|
|
76
|
+
const vendorAvailable = ${JSON.stringify(vendorAvailable)};
|
|
77
|
+
const vendorUrl = ${JSON.stringify(vendorUrl)};
|
|
78
|
+
|
|
79
|
+
window.__yuqueBridge = {
|
|
80
|
+
async serialize(html) {
|
|
81
|
+
if (vendorAvailable && window.Doc && typeof window.Doc.createOpenEditor === 'function') {
|
|
82
|
+
return serializeWithOfficialEditor(html);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
serializer: 'fallback',
|
|
86
|
+
lake: fallbackSerialize(html)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
async function serializeWithOfficialEditor(html) {
|
|
92
|
+
const root = document.getElementById('root');
|
|
93
|
+
const editor = window.Doc.createOpenEditor(root, {
|
|
94
|
+
placeholder: '',
|
|
95
|
+
defaultFontsize: 14,
|
|
96
|
+
codeblock: {
|
|
97
|
+
codemirrorURL: vendorUrl + '/CodeMirror.js',
|
|
98
|
+
supportCustomStyle: true
|
|
99
|
+
},
|
|
100
|
+
math: {
|
|
101
|
+
KaTexURL: vendorUrl + '/katex.js'
|
|
102
|
+
},
|
|
103
|
+
image: {
|
|
104
|
+
isCaptureImageURL() {
|
|
105
|
+
return false;
|
|
106
|
+
},
|
|
107
|
+
async createUploadPromise(request) {
|
|
108
|
+
if (request.type === 'base64') {
|
|
109
|
+
return {
|
|
110
|
+
url: request.data,
|
|
111
|
+
size: request.data.length * 0.75,
|
|
112
|
+
name: 'image.png'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const reader = new FileReader();
|
|
117
|
+
reader.onload = () => resolve({
|
|
118
|
+
url: reader.result,
|
|
119
|
+
size: request.data.size,
|
|
120
|
+
name: request.data.name || 'image.png'
|
|
121
|
+
});
|
|
122
|
+
reader.onerror = reject;
|
|
123
|
+
reader.readAsDataURL(request.data);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
bookmark: {
|
|
128
|
+
recognizeYuque: true,
|
|
129
|
+
fetchDetailHandler: async (url) => ({ url, title: url })
|
|
130
|
+
},
|
|
131
|
+
link: {
|
|
132
|
+
isValidURL() {
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
sanitizeURL(url) {
|
|
136
|
+
return url;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
editor.setDocument('text/html', html);
|
|
141
|
+
let attempts = 0;
|
|
142
|
+
while (editor.canGetDocument && !editor.canGetDocument()) {
|
|
143
|
+
if (attempts > 100) throw new Error('Official editor was not ready after 10s.');
|
|
144
|
+
attempts += 1;
|
|
145
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
serializer: 'official-lake-editor',
|
|
149
|
+
lake: editor.getDocument('text/lake', { includeMeta: true })
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function fallbackSerialize(html) {
|
|
154
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
155
|
+
const blocks = [];
|
|
156
|
+
for (const node of Array.from(doc.body.childNodes)) {
|
|
157
|
+
const rendered = renderBlock(node);
|
|
158
|
+
if (rendered) blocks.push(rendered);
|
|
159
|
+
}
|
|
160
|
+
return '<meta name="serializer" content="yuque-cookie-plugin-fallback">' + blocks.join('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderBlock(node) {
|
|
164
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
165
|
+
const text = node.textContent.trim();
|
|
166
|
+
return text ? '<p>' + escapeHtml(text) + '</p>' : '';
|
|
167
|
+
}
|
|
168
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return '';
|
|
169
|
+
const tag = node.tagName.toLowerCase();
|
|
170
|
+
if (/^h[1-6]$/.test(tag)) return '<' + tag + '>' + renderInlineChildren(node) + '</' + tag + '>';
|
|
171
|
+
if (tag === 'p') return '<p>' + renderInlineChildren(node) + '</p>';
|
|
172
|
+
if (tag === 'blockquote') return '<blockquote>' + renderInlineChildren(node) + '</blockquote>';
|
|
173
|
+
if (tag === 'hr') return '<card name="hr" value="data:%7B%7D"></card>';
|
|
174
|
+
if (tag === 'pre') return renderCodeBlock(node);
|
|
175
|
+
if (tag === 'ul' || tag === 'ol') return renderList(node, tag);
|
|
176
|
+
if (tag === 'table') return renderTable(node);
|
|
177
|
+
if (tag === 'div' || tag === 'section' || tag === 'article') {
|
|
178
|
+
return Array.from(node.childNodes).map(renderBlock).filter(Boolean).join('');
|
|
179
|
+
}
|
|
180
|
+
return '<p>' + renderInline(node) + '</p>';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderInlineChildren(node) {
|
|
184
|
+
return Array.from(node.childNodes).map(renderInline).join('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderInline(node) {
|
|
188
|
+
if (node.nodeType === Node.TEXT_NODE) return escapeHtml(node.textContent);
|
|
189
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return '';
|
|
190
|
+
const tag = node.tagName.toLowerCase();
|
|
191
|
+
const content = renderInlineChildren(node);
|
|
192
|
+
if (tag === 'strong' || tag === 'b') return '<strong>' + content + '</strong>';
|
|
193
|
+
if (tag === 'em' || tag === 'i') return '<em>' + content + '</em>';
|
|
194
|
+
if (tag === 'del' || tag === 's') return '<del>' + content + '</del>';
|
|
195
|
+
if (tag === 'u') return '<u>' + content + '</u>';
|
|
196
|
+
if (tag === 'code') return '<code>' + content + '</code>';
|
|
197
|
+
if (tag === 'a') return '<a href="' + escapeAttr(node.getAttribute('href') || '') + '">' + content + '</a>';
|
|
198
|
+
if (tag === 'br') return '<br>';
|
|
199
|
+
if (tag === 'img') {
|
|
200
|
+
const data = {
|
|
201
|
+
src: node.getAttribute('src') || '',
|
|
202
|
+
name: node.getAttribute('alt') || 'image-placeholder'
|
|
203
|
+
};
|
|
204
|
+
return '<card name="image" value="data:' + encodeURIComponent(JSON.stringify(data)) + '"></card>';
|
|
205
|
+
}
|
|
206
|
+
return content;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function renderCodeBlock(node) {
|
|
210
|
+
const codeNode = node.querySelector('code') || node;
|
|
211
|
+
const className = codeNode.getAttribute('class') || '';
|
|
212
|
+
const language = (className.match(/language-([\\w-]+)/) || [])[1] || 'plain';
|
|
213
|
+
const data = { mode: language, code: codeNode.textContent || '', name: '' };
|
|
214
|
+
return '<card name="codeblock" value="data:' + encodeURIComponent(JSON.stringify(data)) + '"></card>';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderList(node, tag) {
|
|
218
|
+
return '<' + tag + '>' + Array.from(node.children).filter(child => child.tagName.toLowerCase() === 'li').map((li) => {
|
|
219
|
+
return '<li>' + renderInlineChildren(li) + '</li>';
|
|
220
|
+
}).join('') + '</' + tag + '>';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderTable(node) {
|
|
224
|
+
return '<table>' + node.innerHTML + '</table>';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function escapeHtml(value) {
|
|
228
|
+
return String(value)
|
|
229
|
+
.replace(/&/g, '&')
|
|
230
|
+
.replace(/</g, '<')
|
|
231
|
+
.replace(/>/g, '>');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function escapeAttr(value) {
|
|
235
|
+
return escapeHtml(value).replace(/"/g, '"');
|
|
236
|
+
}
|
|
237
|
+
</script>
|
|
238
|
+
</body>
|
|
239
|
+
</html>`;
|
|
240
|
+
}
|
package/dist/fs-utils.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function writeJson(file, data) {
|
|
4
|
+
await mkdir(path.dirname(path.resolve(file)), { recursive: true });
|
|
5
|
+
await writeFile(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
6
|
+
}
|
|
7
|
+
export function timestampForFile(date = new Date()) {
|
|
8
|
+
return date.toISOString().replace(/[:.]/g, '-');
|
|
9
|
+
}
|