@jacobbubu/md-to-lark 1.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 +15 -0
- package/README.md +171 -0
- package/dist/btt/build-tree.js +79 -0
- package/dist/btt/index.js +1 -0
- package/dist/btt/types.js +1 -0
- package/dist/cli/publish-md-to-lark.js +15 -0
- package/dist/commands/publish-md/args.js +224 -0
- package/dist/commands/publish-md/command.js +97 -0
- package/dist/commands/publish-md/index.js +1 -0
- package/dist/commands/publish-md/input-resolver.js +48 -0
- package/dist/commands/publish-md/mermaid-render.js +17 -0
- package/dist/commands/publish-md/pipeline-transform.js +4 -0
- package/dist/commands/publish-md/preset-loader.js +113 -0
- package/dist/commands/publish-md/presets/medium.js +7 -0
- package/dist/commands/publish-md/presets/zh-format.js +8 -0
- package/dist/commands/publish-md/title-policy.js +93 -0
- package/dist/index.js +1 -0
- package/dist/interop/btt-to-last.js +79 -0
- package/dist/interop/codec-btt-to-last.js +435 -0
- package/dist/interop/codec-last-to-btt.js +383 -0
- package/dist/interop/codec-shared.js +722 -0
- package/dist/interop/index.js +2 -0
- package/dist/interop/last-to-btt.js +17 -0
- package/dist/lark/block-types.js +42 -0
- package/dist/lark/client.js +36 -0
- package/dist/lark/docx/ops.js +596 -0
- package/dist/lark/docx/render-btt.js +156 -0
- package/dist/lark/docx/render-models.js +1 -0
- package/dist/lark/docx/render-payload.js +338 -0
- package/dist/lark/docx/render-post-process.js +98 -0
- package/dist/lark/docx/render-table.js +87 -0
- package/dist/lark/docx/render-types.js +7 -0
- package/dist/lark/index.js +2 -0
- package/dist/lark/types.js +1 -0
- package/dist/last/api.js +1687 -0
- package/dist/last/index.js +3 -0
- package/dist/last/preview-terminal.js +296 -0
- package/dist/last/textual-block-types.js +19 -0
- package/dist/last/to-markdown.js +303 -0
- package/dist/last/types.js +11 -0
- package/dist/pipeline/hast-to-last.js +946 -0
- package/dist/pipeline/index.js +3 -0
- package/dist/pipeline/markdown/md-to-hast.js +34 -0
- package/dist/pipeline/markdown/prepare-markdown.js +1049 -0
- package/dist/preview/index.js +1 -0
- package/dist/preview/markdown-terminal.js +350 -0
- package/dist/publish/asset-adapter.js +123 -0
- package/dist/publish/btt-patch.js +65 -0
- package/dist/publish/common.js +139 -0
- package/dist/publish/ids.js +9 -0
- package/dist/publish/index.js +7 -0
- package/dist/publish/last-normalize.js +327 -0
- package/dist/publish/process-file.js +228 -0
- package/dist/publish/runtime.js +133 -0
- package/dist/publish/stage-cache.js +56 -0
- package/dist/shared/rate-limiter.js +18 -0
- package/dist/shared/retry.js +141 -0
- package/package.json +78 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function buildPrepareDirForSource(prepareRootDir, sourcePath) {
|
|
5
|
+
const sourceHash = createHash('sha1').update(path.resolve(sourcePath)).digest('hex').slice(0, 12);
|
|
6
|
+
const baseName = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
7
|
+
return path.join(prepareRootDir, `${baseName || 'md'}-${sourceHash}`);
|
|
8
|
+
}
|
|
9
|
+
export function buildPipelineStagePaths(cacheRootDir, sourcePath) {
|
|
10
|
+
const rootDir = buildPrepareDirForSource(cacheRootDir, sourcePath);
|
|
11
|
+
return {
|
|
12
|
+
rootDir,
|
|
13
|
+
sourceDir: path.join(rootDir, '00-source'),
|
|
14
|
+
prepareDir: path.join(rootDir, '01-prepare'),
|
|
15
|
+
hastDir: path.join(rootDir, '02-hast'),
|
|
16
|
+
lastDir: path.join(rootDir, '03-last'),
|
|
17
|
+
bttDir: path.join(rootDir, '04-btt'),
|
|
18
|
+
publishDir: path.join(rootDir, '05-publish'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function ensureDir(dirPath) {
|
|
22
|
+
await mkdir(dirPath, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
export async function writeJson(filePath, value) {
|
|
25
|
+
await ensureDir(path.dirname(filePath));
|
|
26
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
export async function writeSourceStage(stagePaths, originalMarkdown, presetMarkdown, meta) {
|
|
29
|
+
await ensureDir(stagePaths.sourceDir);
|
|
30
|
+
await writeFile(path.join(stagePaths.sourceDir, 'original.md'), originalMarkdown, 'utf8');
|
|
31
|
+
await writeFile(path.join(stagePaths.sourceDir, 'preset.md'), presetMarkdown, 'utf8');
|
|
32
|
+
await writeJson(path.join(stagePaths.sourceDir, 'meta.json'), meta);
|
|
33
|
+
}
|
|
34
|
+
export async function writePrepareStage(stagePaths, preparedMarkdown, prepareResult) {
|
|
35
|
+
await ensureDir(stagePaths.prepareDir);
|
|
36
|
+
await writeFile(path.join(stagePaths.prepareDir, 'prepared.md'), preparedMarkdown, 'utf8');
|
|
37
|
+
const { preparedContent: _ignoredPreparedContent, logFileContent: _ignoredLogFileContent, ...prepareMeta } = prepareResult;
|
|
38
|
+
await writeJson(path.join(stagePaths.prepareDir, 'result.json'), prepareMeta);
|
|
39
|
+
await writePrepareLogFile(path.join(stagePaths.prepareDir, 'download.log.json'), prepareResult.logFileContent);
|
|
40
|
+
}
|
|
41
|
+
export async function writePrepareLogFile(filePath, value) {
|
|
42
|
+
await writeJson(filePath, value);
|
|
43
|
+
}
|
|
44
|
+
export async function writeHastStage(stagePaths, hast) {
|
|
45
|
+
await writeJson(path.join(stagePaths.hastDir, 'hast.json'), hast);
|
|
46
|
+
}
|
|
47
|
+
export async function writeLastStage(stagePaths, last) {
|
|
48
|
+
await writeJson(path.join(stagePaths.lastDir, 'last.json'), last);
|
|
49
|
+
}
|
|
50
|
+
export async function writeBttStage(stagePaths, btt, meta) {
|
|
51
|
+
await writeJson(path.join(stagePaths.bttDir, 'btt.json'), btt);
|
|
52
|
+
await writeJson(path.join(stagePaths.bttDir, 'meta.json'), meta);
|
|
53
|
+
}
|
|
54
|
+
export async function writePublishStageArtifact(stagePaths, artifact) {
|
|
55
|
+
await writeJson(path.join(stagePaths.publishDir, 'result.json'), artifact);
|
|
56
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class RateLimiter {
|
|
2
|
+
nextAllowedAt = 0;
|
|
3
|
+
minIntervalMs;
|
|
4
|
+
constructor(minIntervalMs) {
|
|
5
|
+
this.minIntervalMs = minIntervalMs;
|
|
6
|
+
}
|
|
7
|
+
async wait() {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const delay = this.nextAllowedAt - now;
|
|
10
|
+
if (delay > 0) {
|
|
11
|
+
await sleep(delay);
|
|
12
|
+
}
|
|
13
|
+
this.nextAllowedAt = Date.now() + this.minIntervalMs;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function sleep(ms) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { sleep } from './rate-limiter.js';
|
|
2
|
+
function toObjectRecord(value) {
|
|
3
|
+
return value && typeof value === 'object' ? value : null;
|
|
4
|
+
}
|
|
5
|
+
function getString(record, key) {
|
|
6
|
+
const value = record[key];
|
|
7
|
+
return typeof value === 'string' ? value : '';
|
|
8
|
+
}
|
|
9
|
+
function collectErrorRecords(error) {
|
|
10
|
+
const queue = [error];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const records = [];
|
|
13
|
+
while (queue.length > 0 && records.length < 20) {
|
|
14
|
+
const current = queue.shift();
|
|
15
|
+
if (current == null)
|
|
16
|
+
continue;
|
|
17
|
+
if (typeof current === 'object') {
|
|
18
|
+
if (seen.has(current))
|
|
19
|
+
continue;
|
|
20
|
+
seen.add(current);
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(current)) {
|
|
23
|
+
for (const item of current) {
|
|
24
|
+
queue.push(item);
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const row = toObjectRecord(current);
|
|
29
|
+
if (!row)
|
|
30
|
+
continue;
|
|
31
|
+
records.push(row);
|
|
32
|
+
queue.push(row.response, row.data, row.error, row.cause);
|
|
33
|
+
}
|
|
34
|
+
return records;
|
|
35
|
+
}
|
|
36
|
+
function toNumberFromUnknown(value) {
|
|
37
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
38
|
+
return value;
|
|
39
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim()))
|
|
40
|
+
return Number(value.trim());
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
function getErrorHttpStatus(error) {
|
|
44
|
+
const records = collectErrorRecords(error);
|
|
45
|
+
for (const row of records) {
|
|
46
|
+
const status = toNumberFromUnknown(row.status);
|
|
47
|
+
if (status)
|
|
48
|
+
return status;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
function getErrorApiCode(error) {
|
|
53
|
+
const records = collectErrorRecords(error);
|
|
54
|
+
for (const row of records) {
|
|
55
|
+
const code = row.code;
|
|
56
|
+
if (typeof code === 'number' || typeof code === 'string')
|
|
57
|
+
return code;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function getRetryAfterMs(error) {
|
|
62
|
+
const records = collectErrorRecords(error);
|
|
63
|
+
for (const row of records) {
|
|
64
|
+
const headers = toObjectRecord(row.headers);
|
|
65
|
+
if (!headers)
|
|
66
|
+
continue;
|
|
67
|
+
const rawRetryAfter = headers['retry-after'] ?? headers['Retry-After'];
|
|
68
|
+
let value = rawRetryAfter;
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
value = value[0];
|
|
71
|
+
}
|
|
72
|
+
const asNumber = toNumberFromUnknown(value);
|
|
73
|
+
if (asNumber && asNumber > 0) {
|
|
74
|
+
return asNumber * 1000;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function getErrorText(error) {
|
|
80
|
+
const textParts = [];
|
|
81
|
+
if (error instanceof Error && error.message) {
|
|
82
|
+
textParts.push(error.message);
|
|
83
|
+
}
|
|
84
|
+
const records = collectErrorRecords(error);
|
|
85
|
+
for (const row of records) {
|
|
86
|
+
const message = getString(row, 'message');
|
|
87
|
+
const msg = getString(row, 'msg');
|
|
88
|
+
if (message)
|
|
89
|
+
textParts.push(message);
|
|
90
|
+
if (msg)
|
|
91
|
+
textParts.push(msg);
|
|
92
|
+
}
|
|
93
|
+
return textParts.join(' | ');
|
|
94
|
+
}
|
|
95
|
+
function isRetryableError(error) {
|
|
96
|
+
const status = getErrorHttpStatus(error);
|
|
97
|
+
if (status && [429, 502, 503, 504].includes(status)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const apiCode = getErrorApiCode(error);
|
|
101
|
+
if (String(apiCode) === '99991400') {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
const text = getErrorText(error).toLowerCase();
|
|
105
|
+
return /429|too many requests|rate|frequency|request trigger frequency limit|timeout|timed out|econnreset|eai_again|502|503|504/.test(text);
|
|
106
|
+
}
|
|
107
|
+
function formatErrorForLog(error) {
|
|
108
|
+
const status = getErrorHttpStatus(error);
|
|
109
|
+
const apiCode = getErrorApiCode(error);
|
|
110
|
+
const text = getErrorText(error) || String(error);
|
|
111
|
+
const prefixes = [];
|
|
112
|
+
if (status)
|
|
113
|
+
prefixes.push(`status=${status}`);
|
|
114
|
+
if (apiCode != null)
|
|
115
|
+
prefixes.push(`code=${String(apiCode)}`);
|
|
116
|
+
const prefix = prefixes.length > 0 ? `${prefixes.join(' ')} ` : '';
|
|
117
|
+
return `${prefix}${text}`;
|
|
118
|
+
}
|
|
119
|
+
export async function withRetry(label, run, attempts = 7) {
|
|
120
|
+
let lastError;
|
|
121
|
+
let usedAttempts = 0;
|
|
122
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
123
|
+
usedAttempts = i + 1;
|
|
124
|
+
try {
|
|
125
|
+
return await run();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
lastError = error;
|
|
129
|
+
if (!isRetryableError(error) || i === attempts - 1) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const retryAfterMs = getRetryAfterMs(error);
|
|
133
|
+
const backoffMs = retryAfterMs ?? Math.min(30_000, 800 * 2 ** i);
|
|
134
|
+
const jitterMs = Math.floor(Math.random() * 300);
|
|
135
|
+
const delayMs = backoffMs + jitterMs;
|
|
136
|
+
console.warn(`[retry] ${label} attempt ${i + 1}/${attempts} failed: ${formatErrorForLog(error)}; retrying in ${delayMs}ms`);
|
|
137
|
+
await sleep(delayMs);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`${label} failed after ${usedAttempts} attempt(s): ${formatErrorForLog(lastError)}`);
|
|
141
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jacobbubu/md-to-lark",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Publish Markdown to Feishu docs with a stable pipeline.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"publish-md-to-lark": "dist/cli/publish-md-to-lark.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test --import tsx \"tests/**/*.test.ts\"",
|
|
17
|
+
"test:e2e": "node --test --test-concurrency=1 --import tsx \"tests-e2e/**/*.test.ts\"",
|
|
18
|
+
"test:e2e:watch": "node --test --test-concurrency=1 --watch --import tsx \"tests-e2e/**/*.test.ts\"",
|
|
19
|
+
"test:watch": "node --test --watch --import tsx \"tests/**/*.test.ts\"",
|
|
20
|
+
"clean": "node scripts/clean-dist.mjs",
|
|
21
|
+
"build": "npm run clean && tsc -p tsconfig.json && node scripts/ensure-cli-executable.mjs",
|
|
22
|
+
"release": "semantic-release",
|
|
23
|
+
"dev": "tsx src/cli/publish-md-to-lark.ts",
|
|
24
|
+
"dev:playground": "tsx devserver/jquery-playground.ts",
|
|
25
|
+
"publish:md": "tsx src/cli/publish-md-to-lark.ts",
|
|
26
|
+
"fetch:board-data": "tsx scripts/print-board-content.ts",
|
|
27
|
+
"example:module": "tsx examples/module-usage.ts",
|
|
28
|
+
"start": "node dist/cli/publish-md-to-lark.js",
|
|
29
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"format": "prettier . --write",
|
|
31
|
+
"format:check": "prettier . --check"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [],
|
|
34
|
+
"author": "jacobbubu <rong.shen@gmail.com>",
|
|
35
|
+
"license": "ISC",
|
|
36
|
+
"type": "module",
|
|
37
|
+
"private": false,
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/jacobbubu/md-to-lark.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/jacobbubu/md-to-lark#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/jacobbubu/md-to-lark/issues"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@jacobbubu/md-zh-format": "^1.0.1",
|
|
51
|
+
"dotenv": "^17.2.2",
|
|
52
|
+
"hast-util-to-string": "^3.0.1",
|
|
53
|
+
"rehype-parse": "^9.0.1",
|
|
54
|
+
"rehype-stringify": "^10.0.1",
|
|
55
|
+
"remark-gfm": "^4.0.1",
|
|
56
|
+
"remark-math": "^6.0.0",
|
|
57
|
+
"remark-parse": "^11.0.0",
|
|
58
|
+
"remark-rehype": "^11.1.2",
|
|
59
|
+
"unified": "^11.0.5",
|
|
60
|
+
"unist-util-visit": "^5.1.0",
|
|
61
|
+
"yaml": "^2.8.2"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
65
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
66
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
67
|
+
"@semantic-release/git": "^10.0.1",
|
|
68
|
+
"@semantic-release/github": "^11.0.6",
|
|
69
|
+
"@semantic-release/npm": "^12.0.2",
|
|
70
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
71
|
+
"@types/node": "^25.3.3",
|
|
72
|
+
"playwright": "^1.58.2",
|
|
73
|
+
"prettier": "^3.8.1",
|
|
74
|
+
"semantic-release": "^24.2.9",
|
|
75
|
+
"tsx": "^4.21.0",
|
|
76
|
+
"typescript": "^5.9.3"
|
|
77
|
+
}
|
|
78
|
+
}
|