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,579 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createWriteStream, existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import markdownToc from 'markdown-toc';
|
|
6
|
+
import pako from 'pako';
|
|
7
|
+
const IMAGE_SIGN_KEY = 'UXO91eVnUveQn8suOJaYMvBcWs9KptS8N5HoP8ezSeU4vqApZpy1CkPaTpkpQEx2W2mlhxL8zwS8UePwBgksUM0CTtAODbTTTDFD';
|
|
8
|
+
export function normalizeDownloadOptions(flags) {
|
|
9
|
+
return {
|
|
10
|
+
distDir: typeof flags.distDir === 'string' ? flags.distDir : 'download',
|
|
11
|
+
ignoreImg: Boolean(flags.ignoreImg),
|
|
12
|
+
ignoreAttachments: normalizeIgnoreAttachments(flags.ignoreAttachments),
|
|
13
|
+
toc: Boolean(flags.toc),
|
|
14
|
+
incremental: Boolean(flags.incremental),
|
|
15
|
+
convertMarkdownVideoLinks: Boolean(flags.convertMarkdownVideoLinks),
|
|
16
|
+
hideFooter: Boolean(flags.hideFooter),
|
|
17
|
+
quiet: Boolean(flags.quiet)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function fixPath(value) {
|
|
21
|
+
return removeEmojis(value)
|
|
22
|
+
.replace(/[\\/:*?"<>|\n\r]/g, '_')
|
|
23
|
+
.replace(/\s+/g, ' ')
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
export function removeEmojis(value) {
|
|
27
|
+
return value.replace(/[\ud800-\udbff][\udc00-\udfff]/g, '');
|
|
28
|
+
}
|
|
29
|
+
export function formatDate(value) {
|
|
30
|
+
const date = new Date(value);
|
|
31
|
+
if (Number.isNaN(date.getTime()))
|
|
32
|
+
return '';
|
|
33
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
34
|
+
}
|
|
35
|
+
export function getMarkdownImageList(markdown) {
|
|
36
|
+
const mdImgReg = /!\[(.*?)\]\((.*?)\)/gm;
|
|
37
|
+
return Array.from(markdown.match(mdImgReg) || [])
|
|
38
|
+
.map((item) => item.replace(mdImgReg, '$2'))
|
|
39
|
+
.filter((url) => /^https?:\/\//.test(url));
|
|
40
|
+
}
|
|
41
|
+
export function fixLatex(markdown) {
|
|
42
|
+
const latexReg = /!\[(.*?)\]\((http.*?latex.*?)\)/gm;
|
|
43
|
+
const list = markdown.match(latexReg);
|
|
44
|
+
let fixed = markdown;
|
|
45
|
+
try {
|
|
46
|
+
list?.forEach((latexMd) => {
|
|
47
|
+
latexReg.lastIndex = 0;
|
|
48
|
+
const url = latexReg.exec(latexMd)?.[2] ?? '';
|
|
49
|
+
const { pathname, search } = new URL(url);
|
|
50
|
+
if (!pathname.endsWith('.svg') && search) {
|
|
51
|
+
fixed = fixed.replace(latexMd, decodeURIComponent(search).slice(1));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return markdown;
|
|
57
|
+
}
|
|
58
|
+
return fixed;
|
|
59
|
+
}
|
|
60
|
+
export function fixMarkdownImage(imgList, markdown, htmlData) {
|
|
61
|
+
if (!htmlData)
|
|
62
|
+
return markdown;
|
|
63
|
+
const htmlDataImgReg = /<card.*?name="image".*?value="data:(.*?)">(.*?)<\/card>/gm;
|
|
64
|
+
const htmlImgDataList = [];
|
|
65
|
+
for (const match of htmlData.matchAll(htmlDataImgReg)) {
|
|
66
|
+
if (!match[1])
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
const cardData = JSON.parse(decodeURIComponent(match[1]));
|
|
70
|
+
htmlImgDataList.push(cardData.src || '');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
htmlImgDataList.push('');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const replaceURLCountMap = new Map();
|
|
77
|
+
let fixed = markdown;
|
|
78
|
+
for (const imgUrl of imgList) {
|
|
79
|
+
let matchURL = '';
|
|
80
|
+
try {
|
|
81
|
+
const { origin, pathname } = new URL(imgUrl);
|
|
82
|
+
matchURL = `${origin}${pathname}`;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const targetURL = htmlImgDataList.find((item, index) => {
|
|
88
|
+
const isFind = item.startsWith(matchURL);
|
|
89
|
+
if (isFind)
|
|
90
|
+
htmlImgDataList.splice(index, 1);
|
|
91
|
+
return isFind;
|
|
92
|
+
});
|
|
93
|
+
if (!targetURL)
|
|
94
|
+
continue;
|
|
95
|
+
const count = replaceURLCountMap.get(imgUrl) || 0;
|
|
96
|
+
let current = 0;
|
|
97
|
+
fixed = fixed.replace(new RegExp(escapeRegExp(imgUrl), 'g'), (match) => {
|
|
98
|
+
const result = current === count ? targetURL : match;
|
|
99
|
+
current += 1;
|
|
100
|
+
return result;
|
|
101
|
+
});
|
|
102
|
+
replaceURLCountMap.set(imgUrl, count + 1);
|
|
103
|
+
}
|
|
104
|
+
return fixed;
|
|
105
|
+
}
|
|
106
|
+
export function fixInlineCode(markdown, htmlData) {
|
|
107
|
+
return markdown
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((line) => {
|
|
110
|
+
if (/^\s*```/.test(line))
|
|
111
|
+
return line;
|
|
112
|
+
return line.replace(/`([^`\n]+)`/g, (raw, value) => {
|
|
113
|
+
const hasHtmlTags = /<([a-z][\s\S]*?)>/i.test(value);
|
|
114
|
+
const hasMarkdownLabel = /(~~|\*\*|_)/g.test(value);
|
|
115
|
+
if (!hasHtmlTags && !hasMarkdownLabel)
|
|
116
|
+
return raw;
|
|
117
|
+
const escaped = value.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
118
|
+
if (htmlData.includes(escaped))
|
|
119
|
+
return raw;
|
|
120
|
+
return `<code>${value}</code>`;
|
|
121
|
+
});
|
|
122
|
+
})
|
|
123
|
+
.join('\n');
|
|
124
|
+
}
|
|
125
|
+
export function parseSheet(sheetStr) {
|
|
126
|
+
if (!sheetStr)
|
|
127
|
+
return '';
|
|
128
|
+
const input = sheetInflateInput(sheetStr);
|
|
129
|
+
const parseStr = pako.inflate(input, { to: 'string' });
|
|
130
|
+
const sheetList = JSON.parse(parseStr);
|
|
131
|
+
let markdown = '';
|
|
132
|
+
for (const item of sheetList) {
|
|
133
|
+
markdown += `\n## ${item.name}\n\n${genMarkdownTable(item.data)}`;
|
|
134
|
+
}
|
|
135
|
+
return markdown;
|
|
136
|
+
}
|
|
137
|
+
function sheetInflateInput(sheetStr) {
|
|
138
|
+
if (/^[\x00-\xff]*$/.test(sheetStr)) {
|
|
139
|
+
return Uint8Array.from(sheetStr, (char) => char.charCodeAt(0));
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return Buffer.from(sheetStr, 'base64');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return Buffer.from(sheetStr);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function genMarkdownTable(data) {
|
|
149
|
+
let rowList = Object.keys(data);
|
|
150
|
+
rowList = rowList.filter((rowKey) => {
|
|
151
|
+
const colList = Object.keys(data[rowKey] || {});
|
|
152
|
+
return colList.some((col) => data?.[rowKey]?.[col]?.v);
|
|
153
|
+
});
|
|
154
|
+
let colList = [];
|
|
155
|
+
rowList.forEach((rowKey) => {
|
|
156
|
+
colList = colList.concat(Object.keys(data[rowKey] || {}));
|
|
157
|
+
});
|
|
158
|
+
const rowMax = Math.max(...rowList.map((row) => Number(row)));
|
|
159
|
+
const colMax = Math.max(...colList.map((col) => Number(col)));
|
|
160
|
+
if (rowMax < 0 || colMax < 0 || !Number.isFinite(rowMax) || !Number.isFinite(colMax))
|
|
161
|
+
return '';
|
|
162
|
+
const title = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
163
|
+
const header = `| |${Array(colMax + 1).fill(' ').map((_v, i) => title[i % title.length]).join(' | ')}|`;
|
|
164
|
+
const divider = `|${Array(colMax + 2).fill('---').join(' |')}|`;
|
|
165
|
+
let table = `${header}\n${divider}\n`;
|
|
166
|
+
for (let row = 0; row < rowMax + 1; row += 1) {
|
|
167
|
+
const values = [];
|
|
168
|
+
for (let col = 0; col < colMax + 1; col += 1) {
|
|
169
|
+
values.push(sheetValueToMarkdown(data?.[row]?.[col]?.v));
|
|
170
|
+
}
|
|
171
|
+
table += `| ${row + 1} | ${values.join(' | ')}|\n`;
|
|
172
|
+
}
|
|
173
|
+
return table;
|
|
174
|
+
}
|
|
175
|
+
function sheetValueToMarkdown(value) {
|
|
176
|
+
if (!value)
|
|
177
|
+
return '';
|
|
178
|
+
if (typeof value === 'string')
|
|
179
|
+
return value.replace(/\|/g, '\\|');
|
|
180
|
+
if (typeof value !== 'object')
|
|
181
|
+
return String(value);
|
|
182
|
+
const data = value;
|
|
183
|
+
if (data.class === 'image' && data.src)
|
|
184
|
+
return ``;
|
|
185
|
+
if (data.class === 'checkbox')
|
|
186
|
+
return data.value ? '[x]' : '[ ]';
|
|
187
|
+
if (data.class === 'link')
|
|
188
|
+
return `[${data.text || data.url}](${data.url})`;
|
|
189
|
+
if (data.class === 'select' && Array.isArray(data.value))
|
|
190
|
+
return data.value.join(',');
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
export function handleMarkdownFooter(markdown, options) {
|
|
194
|
+
let data = markdown.replace(/<a\b[^>]*?>(\s*?)<\/a>/gm, '');
|
|
195
|
+
const header = options.articleTitle ? `# ${options.articleTitle}\n\n` : '';
|
|
196
|
+
const tocData = options.toc ? markdownToc(data).content : '';
|
|
197
|
+
const tocBlock = tocData ? `${tocData}\n\n---\n\n` : '';
|
|
198
|
+
let footer = '';
|
|
199
|
+
if (!options.hideFooter) {
|
|
200
|
+
footer = '\n\n';
|
|
201
|
+
if (options.articleUpdateTime)
|
|
202
|
+
footer += `> 更新: ${options.articleUpdateTime} \n`;
|
|
203
|
+
if (options.articleUrl)
|
|
204
|
+
footer += `> 原文: <${options.articleUrl}>`;
|
|
205
|
+
}
|
|
206
|
+
data = data.replace(/^# .*?\n+/, '');
|
|
207
|
+
const result = `${header}${tocBlock}${data}${footer}`;
|
|
208
|
+
return options.convertMarkdownVideoLinks ? convertMarkdownVideoLinks(result) : result;
|
|
209
|
+
}
|
|
210
|
+
function convertMarkdownVideoLinks(markdown) {
|
|
211
|
+
return markdown.replace(/\[(.*?)\]\((.*?)\.(mp4|mp3)\)/gm, (_match, alt, url, extType) => {
|
|
212
|
+
const htmlTag = extType === 'mp3' ? 'audio' : 'video';
|
|
213
|
+
return `<${htmlTag} controls width="800" alt="${alt}" src="${url}.${extType}"></${htmlTag}>`;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function escapeRegExp(value) {
|
|
217
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
218
|
+
}
|
|
219
|
+
export async function readProgress(bookPath) {
|
|
220
|
+
const file = path.join(bookPath, 'progress.json');
|
|
221
|
+
if (!existsSync(file))
|
|
222
|
+
return [];
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(await readFile(file, 'utf8'));
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export async function writeProgress(bookPath, items) {
|
|
231
|
+
await mkdir(bookPath, { recursive: true });
|
|
232
|
+
await writeFile(path.join(bookPath, 'progress.json'), `${JSON.stringify(items, null, 2)}\n`);
|
|
233
|
+
}
|
|
234
|
+
export async function buildResourceManifest(rootPath) {
|
|
235
|
+
const root = path.resolve(rootPath);
|
|
236
|
+
const files = [];
|
|
237
|
+
await collectResourceFiles(root, root, files);
|
|
238
|
+
files.sort((a, b) => a.path.localeCompare(b.path, 'zh-CN'));
|
|
239
|
+
return {
|
|
240
|
+
total: files.length,
|
|
241
|
+
by_type: {
|
|
242
|
+
image: files.filter((file) => file.type === 'image').length,
|
|
243
|
+
attachment: files.filter((file) => file.type === 'attachment').length
|
|
244
|
+
},
|
|
245
|
+
total_size: files.reduce((sum, file) => sum + file.size, 0),
|
|
246
|
+
files
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
export function shouldSkipByIncremental(current, previous, incremental) {
|
|
250
|
+
if (!incremental || !previous)
|
|
251
|
+
return false;
|
|
252
|
+
return current.contentUpdatedAt !== undefined && previous.contentUpdatedAt === current.contentUpdatedAt;
|
|
253
|
+
}
|
|
254
|
+
export async function downloadFile(params) {
|
|
255
|
+
await mkdir(path.dirname(params.file), { recursive: true });
|
|
256
|
+
const res = await fetch(params.url, { headers: params.headers });
|
|
257
|
+
if (!res.ok || !res.body)
|
|
258
|
+
throw new Error(`download failed: ${res.status} ${params.url}`);
|
|
259
|
+
const body = res.body;
|
|
260
|
+
const writer = createWriteStream(params.file);
|
|
261
|
+
await new Promise((resolve, reject) => {
|
|
262
|
+
body.pipeTo(new WritableStream({
|
|
263
|
+
write(chunk) {
|
|
264
|
+
writer.write(Buffer.from(chunk));
|
|
265
|
+
},
|
|
266
|
+
close() {
|
|
267
|
+
writer.end(resolve);
|
|
268
|
+
},
|
|
269
|
+
abort(reason) {
|
|
270
|
+
writer.destroy();
|
|
271
|
+
reject(reason);
|
|
272
|
+
}
|
|
273
|
+
})).catch(reject);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
export async function localizeImages(markdown, params) {
|
|
277
|
+
let data = markdown;
|
|
278
|
+
const urls = getMarkdownImageList(markdown);
|
|
279
|
+
for (const [index, rawUrl] of urls.entries()) {
|
|
280
|
+
const url = captureImageUrl(rawUrl, params.imageServiceDomains);
|
|
281
|
+
const ext = extensionFromUrl(rawUrl) || '.png';
|
|
282
|
+
const fileName = `${index + 1}${ext}`;
|
|
283
|
+
const relPath = `${params.imageDir}/${fileName}`;
|
|
284
|
+
const fullPath = path.resolve(params.savePath, relPath);
|
|
285
|
+
try {
|
|
286
|
+
await downloadFile({
|
|
287
|
+
url,
|
|
288
|
+
file: fullPath,
|
|
289
|
+
headers: {
|
|
290
|
+
...params.headers,
|
|
291
|
+
referer: params.referer
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
data = data.replace(rawUrl, relPath);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
params.warnings?.push({
|
|
298
|
+
type: 'image',
|
|
299
|
+
title: params.title,
|
|
300
|
+
url,
|
|
301
|
+
file: fullPath,
|
|
302
|
+
error: error instanceof Error ? error.message : String(error)
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return data;
|
|
307
|
+
}
|
|
308
|
+
export async function localizeAttachments(markdown, params) {
|
|
309
|
+
if (params.ignoreAttachments === true)
|
|
310
|
+
return markdown;
|
|
311
|
+
const attachmentReg = /\[(.*?)\]\((https?:\/\/.*?\.yuque\.com\/attachments.*?)\)/g;
|
|
312
|
+
let data = markdown;
|
|
313
|
+
const matches = Array.from(markdown.matchAll(attachmentReg));
|
|
314
|
+
for (const match of matches) {
|
|
315
|
+
const raw = match[0];
|
|
316
|
+
const label = match[1] || 'attachment';
|
|
317
|
+
const url = match[2];
|
|
318
|
+
if (!url || shouldIgnoreAttachment(url, params.ignoreAttachments))
|
|
319
|
+
continue;
|
|
320
|
+
const fileName = fixPath(label || path.basename(new URL(url).pathname) || 'attachment');
|
|
321
|
+
const relPath = `${params.attachmentsDir}/${fileName}`;
|
|
322
|
+
const fullPath = path.resolve(params.savePath, relPath);
|
|
323
|
+
try {
|
|
324
|
+
await downloadFile({ url, file: fullPath, headers: params.headers });
|
|
325
|
+
data = data.replace(raw, `[附件: ${fileName}](${relPath})`);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
params.warnings?.push({
|
|
329
|
+
type: 'attachment',
|
|
330
|
+
title: params.title,
|
|
331
|
+
url,
|
|
332
|
+
file: fullPath,
|
|
333
|
+
error: error instanceof Error ? error.message : String(error)
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return data;
|
|
338
|
+
}
|
|
339
|
+
export async function localizeFileCards(markdown, htmlData, params) {
|
|
340
|
+
if (params.ignoreAttachments === true)
|
|
341
|
+
return markdown;
|
|
342
|
+
let data = markdown;
|
|
343
|
+
const cards = parseHtmlFileCards(htmlData).filter((item) => {
|
|
344
|
+
return !shouldIgnoreAttachment(item.name || item.url, params.ignoreAttachments);
|
|
345
|
+
});
|
|
346
|
+
for (const card of cards) {
|
|
347
|
+
const fileName = fixPath(card.name || path.basename(new URL(card.url).pathname) || 'attachment');
|
|
348
|
+
const relPath = `${params.attachmentsDir}/${fileName}`;
|
|
349
|
+
const fullPath = path.resolve(params.savePath, relPath);
|
|
350
|
+
try {
|
|
351
|
+
await downloadFile({ url: card.url, file: fullPath, headers: params.headers });
|
|
352
|
+
const emptyLink = new RegExp(`\\[${escapeRegExp(card.name)}\\]\\(\\)`);
|
|
353
|
+
if (emptyLink.test(data))
|
|
354
|
+
data = data.replace(emptyLink, `[附件: ${fileName}](${relPath})`);
|
|
355
|
+
else
|
|
356
|
+
data += `\n\n[附件: ${fileName}](${relPath})\n`;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
params.warnings?.push({
|
|
360
|
+
type: 'attachment',
|
|
361
|
+
title: params.title,
|
|
362
|
+
url: card.url,
|
|
363
|
+
file: fullPath,
|
|
364
|
+
error: error instanceof Error ? error.message : String(error)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return data;
|
|
369
|
+
}
|
|
370
|
+
export async function localizeMedia(markdown, htmlData, params) {
|
|
371
|
+
if (params.ignoreAttachments === true)
|
|
372
|
+
return markdown;
|
|
373
|
+
let data = markdown;
|
|
374
|
+
const mediaItems = [
|
|
375
|
+
...parseMarkdownVideoCards(markdown),
|
|
376
|
+
...parseHtmlMediaCards(htmlData, 'audio'),
|
|
377
|
+
...parseHtmlMediaCards(htmlData, 'video')
|
|
378
|
+
].filter((item) => !shouldIgnoreAttachment(item.name || item.id, params.ignoreAttachments));
|
|
379
|
+
for (const item of mediaItems) {
|
|
380
|
+
const info = await params.getVideoInfo(item.id);
|
|
381
|
+
if (!info)
|
|
382
|
+
continue;
|
|
383
|
+
const media = normalizeMediaInfo(item, info);
|
|
384
|
+
const url = media.url;
|
|
385
|
+
const fileName = fixPath(media.fileName);
|
|
386
|
+
if (!url || !fileName)
|
|
387
|
+
continue;
|
|
388
|
+
const relPath = `${params.attachmentsDir}/${fileName}`;
|
|
389
|
+
const fullPath = path.resolve(params.savePath, relPath);
|
|
390
|
+
try {
|
|
391
|
+
await downloadFile({ url, file: fullPath, headers: params.headers });
|
|
392
|
+
if (item.raw)
|
|
393
|
+
data = data.replace(item.raw, `[音视频附件: ${fileName}](${relPath})`);
|
|
394
|
+
else
|
|
395
|
+
data += `\n\n[音视频附件: ${fileName}](${relPath})\n`;
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
params.warnings?.push({
|
|
399
|
+
type: 'media',
|
|
400
|
+
title: params.title,
|
|
401
|
+
url,
|
|
402
|
+
file: fullPath,
|
|
403
|
+
error: error instanceof Error ? error.message : String(error)
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return data;
|
|
408
|
+
}
|
|
409
|
+
function normalizeMediaInfo(item, info) {
|
|
410
|
+
const url = firstString(item.type === 'audio' ? info.audio : info.video, info.url, info.download_url, info.downloadUrl, info.src, info.play_url, info.playUrl, info.file?.url, info.file?.download_url, info.file?.downloadUrl);
|
|
411
|
+
const fileName = firstString(info.name, info.fileName, info.filename, info.file?.name, info.file?.fileName, item.name, path.basename(item.id));
|
|
412
|
+
return { url, fileName };
|
|
413
|
+
}
|
|
414
|
+
function firstString(...values) {
|
|
415
|
+
for (const value of values) {
|
|
416
|
+
if (typeof value === 'string' && value.trim())
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
return '';
|
|
420
|
+
}
|
|
421
|
+
export async function writeSummary(params) {
|
|
422
|
+
let content = `# ${params.bookName || path.basename(params.bookPath)}\n\n`;
|
|
423
|
+
if (params.bookDesc)
|
|
424
|
+
content += `> ${params.bookDesc}\n\n`;
|
|
425
|
+
const byParent = new Map();
|
|
426
|
+
for (const item of params.progressItems) {
|
|
427
|
+
const parent = item.toc.parent_uuid || '';
|
|
428
|
+
byParent.set(parent, [...(byParent.get(parent) || []), item]);
|
|
429
|
+
}
|
|
430
|
+
content += renderSummaryTree(byParent, '', 2);
|
|
431
|
+
await writeFile(path.join(params.bookPath, 'index.md'), content);
|
|
432
|
+
return content;
|
|
433
|
+
}
|
|
434
|
+
export function buildTocMaps(tocList) {
|
|
435
|
+
return new Map(tocList.map((toc) => [toc.uuid, toc]));
|
|
436
|
+
}
|
|
437
|
+
function renderSummaryTree(byParent, parent, level) {
|
|
438
|
+
let content = '';
|
|
439
|
+
for (const item of byParent.get(parent) || []) {
|
|
440
|
+
const title = fixPath(item.toc.title);
|
|
441
|
+
const type = item.toc.type?.toLowerCase();
|
|
442
|
+
if (type === 'title' && !item.path.endsWith('.md')) {
|
|
443
|
+
content += `\n${'#'.repeat(level)} ${title}\n\n`;
|
|
444
|
+
}
|
|
445
|
+
else if (type === 'link') {
|
|
446
|
+
content += `${level <= 2 ? '\n##' : '-'} [${title}](${item.toc.url || item.path})\n`;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
const link = item.path.replace(/\s/g, '%20');
|
|
450
|
+
content += `${level <= 2 ? '\n##' : '-'} [${title}](${link})\n`;
|
|
451
|
+
}
|
|
452
|
+
content += renderSummaryTree(byParent, item.toc.uuid, level + 1);
|
|
453
|
+
}
|
|
454
|
+
return content;
|
|
455
|
+
}
|
|
456
|
+
function normalizeIgnoreAttachments(value) {
|
|
457
|
+
if (value === undefined || value === false)
|
|
458
|
+
return false;
|
|
459
|
+
if (value === true)
|
|
460
|
+
return true;
|
|
461
|
+
return String(value);
|
|
462
|
+
}
|
|
463
|
+
async function collectResourceFiles(root, current, files) {
|
|
464
|
+
let entries;
|
|
465
|
+
try {
|
|
466
|
+
entries = await readdir(current);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
for (const entry of entries) {
|
|
472
|
+
const full = path.join(current, entry);
|
|
473
|
+
const info = await stat(full).catch(() => undefined);
|
|
474
|
+
if (!info)
|
|
475
|
+
continue;
|
|
476
|
+
if (info.isDirectory()) {
|
|
477
|
+
await collectResourceFiles(root, full, files);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (!info.isFile())
|
|
481
|
+
continue;
|
|
482
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
483
|
+
const parts = rel.split('/');
|
|
484
|
+
const type = parts.includes('img') ? 'image' : parts.includes('attachments') ? 'attachment' : undefined;
|
|
485
|
+
if (!type)
|
|
486
|
+
continue;
|
|
487
|
+
files.push({
|
|
488
|
+
type,
|
|
489
|
+
path: rel,
|
|
490
|
+
size: info.size
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function shouldIgnoreAttachment(url, ignore) {
|
|
495
|
+
if (ignore === true)
|
|
496
|
+
return true;
|
|
497
|
+
if (typeof ignore !== 'string')
|
|
498
|
+
return false;
|
|
499
|
+
const ext = extensionFromUrl(url).replace(/^\./, '') || path.extname(url).replace(/^\./, '');
|
|
500
|
+
return ignore.split(',').map((item) => item.trim()).includes(ext);
|
|
501
|
+
}
|
|
502
|
+
function parseMarkdownVideoCards(markdown) {
|
|
503
|
+
const result = [];
|
|
504
|
+
const linkReg = /\[([^\]]*)\]\(([^)]*_lake_card[^)]*)\)/g;
|
|
505
|
+
for (const match of markdown.matchAll(linkReg)) {
|
|
506
|
+
try {
|
|
507
|
+
const url = new URL(match[2]);
|
|
508
|
+
const encoded = url.searchParams.get('_lake_card');
|
|
509
|
+
if (!encoded)
|
|
510
|
+
continue;
|
|
511
|
+
const data = JSON.parse(decodeURIComponent(encoded));
|
|
512
|
+
if (data.videoId)
|
|
513
|
+
result.push({ type: 'video', id: data.videoId, name: data.name || match[1], raw: match[0] });
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// ignore
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
function parseHtmlMediaCards(htmlData, type) {
|
|
522
|
+
const reg = new RegExp(`name="${type}" value="data:(.*?${type}Id.*?)".*?><\\/card>`, 'gm');
|
|
523
|
+
const result = [];
|
|
524
|
+
for (const match of htmlData.matchAll(reg)) {
|
|
525
|
+
try {
|
|
526
|
+
const data = JSON.parse(decodeURIComponent(match[1]));
|
|
527
|
+
const id = type === 'audio' ? data.audioId : data.videoId;
|
|
528
|
+
if (id)
|
|
529
|
+
result.push({ type, id, name: data.name || data.fileName || path.basename(id), raw: '' });
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// ignore
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
function parseHtmlFileCards(htmlData) {
|
|
538
|
+
const reg = /<card\b[^>]*name="file"[^>]*value="data:(.*?)"[^>]*><\/card>/gm;
|
|
539
|
+
const result = [];
|
|
540
|
+
for (const match of htmlData.matchAll(reg)) {
|
|
541
|
+
try {
|
|
542
|
+
const data = JSON.parse(decodeURIComponent(match[1]));
|
|
543
|
+
const url = firstString(data.download_url, data.downloadUrl, data.attachment_url, data.attachmentUrl, data.url);
|
|
544
|
+
const name = firstString(data.name, data.filename, data.fileName, url ? path.basename(new URL(url).pathname) : '');
|
|
545
|
+
if (url)
|
|
546
|
+
result.push({ url, name: name || 'attachment', raw: match[0] });
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
export function captureImageUrl(url, imageServiceDomains) {
|
|
555
|
+
try {
|
|
556
|
+
const { host, pathname } = new URL(url);
|
|
557
|
+
if (imageServiceDomains.includes(host) || !pathname)
|
|
558
|
+
return url;
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return url;
|
|
562
|
+
}
|
|
563
|
+
const hash = crypto.createHash('sha256');
|
|
564
|
+
hash.update(`${IMAGE_SIGN_KEY}${url}`);
|
|
565
|
+
const sign = hash.digest('hex');
|
|
566
|
+
return `https://www.yuque.com/api/filetransfer/images?url=${encodeURIComponent(url)}&sign=${sign}`;
|
|
567
|
+
}
|
|
568
|
+
function extensionFromUrl(url) {
|
|
569
|
+
try {
|
|
570
|
+
const ext = path.extname(new URL(url).pathname);
|
|
571
|
+
return ext.slice(0, 12);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return '';
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function pad(num) {
|
|
578
|
+
return num.toString().padStart(2, '0');
|
|
579
|
+
}
|