@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
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jacobbubu
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# md-to-lark
|
|
2
|
+
|
|
3
|
+
[中文说明](./README_zh.md)
|
|
4
|
+
|
|
5
|
+
`md-to-lark` publishes Markdown (GFM) content to Feishu docs through a repeatable pipeline.
|
|
6
|
+
|
|
7
|
+
It is not a one-off rendering script. The pipeline covers input preparation, title policy, asset detection and upload, Mermaid rendering, table enhancement, dry-run, and stage-by-stage artifacts for debugging.
|
|
8
|
+
|
|
9
|
+
## Repository And Package Name
|
|
10
|
+
|
|
11
|
+
- GitHub repository: [jacobbubu/md-to-lark](https://github.com/jacobbubu/md-to-lark)
|
|
12
|
+
- The npm package metadata is configured as `@jacobbubu/md-to-lark`
|
|
13
|
+
- The dependency `@jacobbubu/md-zh-format` remains unchanged
|
|
14
|
+
|
|
15
|
+
Notes:
|
|
16
|
+
|
|
17
|
+
- The commands in this README are still focused on local development and verification.
|
|
18
|
+
- Once the package is actually published to npm, it can be installed as `@jacobbubu/md-to-lark`.
|
|
19
|
+
|
|
20
|
+
## What It Is Good For
|
|
21
|
+
|
|
22
|
+
- Publishing a single Markdown file to a Feishu doc
|
|
23
|
+
- Recursively publishing multiple `.md` files from a directory
|
|
24
|
+
- Preparing local assets, remote images, and standalone URLs before publish
|
|
25
|
+
- Running a full dry-run without writing to Feishu
|
|
26
|
+
- Rewriting Markdown before publish with presets
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
Install dependencies first:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then prepare `.env`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cp .env.sample .env
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
At minimum, make sure these values are valid:
|
|
43
|
+
|
|
44
|
+
```env
|
|
45
|
+
LARK_APP_ID="xxx"
|
|
46
|
+
LARK_APP_SECRET="xxx"
|
|
47
|
+
LARK_TOKEN_TYPE=tenant
|
|
48
|
+
LARK_FOLDER_TOKEN="xxx"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Notes:
|
|
52
|
+
|
|
53
|
+
- `--dry-run` still validates Feishu configuration first. It is not a zero-config mode.
|
|
54
|
+
- As long as `--doc` is not provided, `LARK_FOLDER_TOKEN` is required for single-file, directory, dry-run, and real publish modes.
|
|
55
|
+
|
|
56
|
+
The first run should use a built-in sample:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run publish:md -- --input ./test-md/comp/comp.md --dry-run
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This runs the full pipeline without actually writing to Feishu. After that looks correct, remove `--dry-run`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run publish:md -- --input ./test-md/comp/comp.md
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Common Commands
|
|
69
|
+
|
|
70
|
+
Basic publish:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm run publish:md -- --input ./test-md/comp/comp.md
|
|
74
|
+
npm run publish:md -- --input ./test-md/comp/comp.md --dry-run
|
|
75
|
+
npm run publish:md -- --input ./test-md
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Target document and title:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm run publish:md -- --input ./test-md/comp/comp.md --doc <document_id>
|
|
82
|
+
npm run publish:md -- --input ./test-md --title "Team Notes"
|
|
83
|
+
npm run publish:md -- --input ./test-md/comp/comp.md --no-date-prefix
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Presets, Mermaid, and stage artifacts:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm run publish:md -- --input ./test-md/comp/comp.md --preset medium --dry-run
|
|
90
|
+
npm run publish:md -- --input ./test-md/comp/comp.md --preset zh-format --dry-run
|
|
91
|
+
npm run publish:md -- --input ./test-md/mermaid.md --mermaid-target board --dry-run
|
|
92
|
+
npm run publish:md -- --input ./test-md/comp/comp.md --pipeline-cache-dir ./out/debug-cache --dry-run
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Debugging and helper scripts:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm run dev:playground
|
|
99
|
+
npm run example:module
|
|
100
|
+
npm run fetch:board-data -- --doc <document_id> --index 1
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Testing
|
|
104
|
+
|
|
105
|
+
Default local verification:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run check
|
|
109
|
+
npm test
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Live Feishu end-to-end tests:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm run test:e2e
|
|
116
|
+
npm run test:e2e:watch
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Notes:
|
|
120
|
+
|
|
121
|
+
- `npm test` only runs local tests and never writes to Feishu.
|
|
122
|
+
- `npm run test:e2e` runs real Feishu end-to-end tests and requires a local `.env-test`.
|
|
123
|
+
- `.env-test` is already ignored by Git and can be prepared from `.env-test.example`.
|
|
124
|
+
|
|
125
|
+
## Release Process
|
|
126
|
+
|
|
127
|
+
Releases are now driven by `semantic-release`.
|
|
128
|
+
|
|
129
|
+
- Only pushes to `main` can trigger a real release
|
|
130
|
+
- Version numbers are calculated from commit messages
|
|
131
|
+
- GitHub Releases and npm publishing are both handled automatically
|
|
132
|
+
- `CHANGELOG.md` is maintained by CI
|
|
133
|
+
|
|
134
|
+
Required repository setup:
|
|
135
|
+
|
|
136
|
+
- GitHub Actions must be enabled
|
|
137
|
+
- The repository must have an `NPM_TOKEN` secret with publish permission for `@jacobbubu/md-to-lark`
|
|
138
|
+
- Commits merged into `main` should continue to use Conventional Commit style such as `feat:` and `fix:`
|
|
139
|
+
|
|
140
|
+
Guardrails:
|
|
141
|
+
|
|
142
|
+
- Non-`main` branches do not trigger the release workflow
|
|
143
|
+
- Non-`main` branches can still run local checks, tests, and build verification
|
|
144
|
+
- npm publishing is expected to happen only through the `main` branch CI flow
|
|
145
|
+
|
|
146
|
+
## Core Capabilities
|
|
147
|
+
|
|
148
|
+
- Single-file and recursive directory publish
|
|
149
|
+
- Title derivation, title prefix, and single-H1 promotion
|
|
150
|
+
- Local attachment and image detection with real upload
|
|
151
|
+
- Remote image download and standalone URL preparation
|
|
152
|
+
- Mermaid `text-drawing` and `board` output paths
|
|
153
|
+
- Table width heuristics and numeric-column right alignment
|
|
154
|
+
- Chinese Markdown formatting preset (`zh-format`)
|
|
155
|
+
- Stage cache output from `00-source` to `05-publish`
|
|
156
|
+
- Programmatic access through `publishMdToLark`
|
|
157
|
+
|
|
158
|
+
## Where To Read Next
|
|
159
|
+
|
|
160
|
+
README is only the entry point. It does not repeat the full parameter reference or implementation details.
|
|
161
|
+
|
|
162
|
+
1. [docs/README.md](./docs/README.md)
|
|
163
|
+
2. [overview.md](./docs/01-getting-started/overview.md)
|
|
164
|
+
3. [quickstart.md](./docs/01-getting-started/quickstart.md)
|
|
165
|
+
4. [presets.md](./docs/02-guides/presets.md)
|
|
166
|
+
5. [cli-reference.md](./docs/03-reference/cli-reference.md)
|
|
167
|
+
6. [architecture-overview.md](./docs/04-internals/architecture-overview.md)
|
|
168
|
+
|
|
169
|
+
If this is your first time using the project, read in this order:
|
|
170
|
+
|
|
171
|
+
`01-getting-started -> 02-guides -> 03-reference -> 04-internals`
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getLarkBlockTypeName } from '../lark/block-types.js';
|
|
2
|
+
function pickRootBlockId(documentId, blocks, blocksById) {
|
|
3
|
+
if (blocks.length === 0) {
|
|
4
|
+
throw new Error(`No blocks returned for document "${documentId}".`);
|
|
5
|
+
}
|
|
6
|
+
const pageRoot = blocks.find((block) => block.block_type === 1);
|
|
7
|
+
if (pageRoot) {
|
|
8
|
+
return pageRoot.block_id;
|
|
9
|
+
}
|
|
10
|
+
const orphan = blocks.find((block) => !blocksById.has(block.parent_id));
|
|
11
|
+
if (orphan) {
|
|
12
|
+
return orphan.block_id;
|
|
13
|
+
}
|
|
14
|
+
const first = blocks[0];
|
|
15
|
+
if (!first) {
|
|
16
|
+
throw new Error(`No root block found for document "${documentId}".`);
|
|
17
|
+
}
|
|
18
|
+
return first.block_id;
|
|
19
|
+
}
|
|
20
|
+
function cloneBlock(block) {
|
|
21
|
+
return JSON.parse(JSON.stringify(block));
|
|
22
|
+
}
|
|
23
|
+
function buildNode(blockId, ctx, visiting) {
|
|
24
|
+
const block = ctx.blocksById.get(blockId);
|
|
25
|
+
if (!block) {
|
|
26
|
+
throw new Error(`Block "${blockId}" is missing while building BTT.`);
|
|
27
|
+
}
|
|
28
|
+
if (visiting.has(blockId)) {
|
|
29
|
+
throw new Error(`Cycle detected in block tree at "${blockId}".`);
|
|
30
|
+
}
|
|
31
|
+
visiting.add(blockId);
|
|
32
|
+
const children = [];
|
|
33
|
+
for (const childId of block.children ?? []) {
|
|
34
|
+
if (!ctx.blocksById.has(childId)) {
|
|
35
|
+
ctx.missingChildren.add(childId);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
children.push(buildNode(childId, ctx, visiting));
|
|
39
|
+
}
|
|
40
|
+
visiting.delete(blockId);
|
|
41
|
+
return {
|
|
42
|
+
blockId: block.block_id,
|
|
43
|
+
parentId: block.parent_id,
|
|
44
|
+
blockType: block.block_type,
|
|
45
|
+
blockTypeName: getLarkBlockTypeName(block.block_type),
|
|
46
|
+
rawBlock: cloneBlock(block),
|
|
47
|
+
children,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function toFlatBlocks(blocks) {
|
|
51
|
+
const output = {};
|
|
52
|
+
for (const block of blocks) {
|
|
53
|
+
output[block.block_id] = cloneBlock(block);
|
|
54
|
+
}
|
|
55
|
+
return output;
|
|
56
|
+
}
|
|
57
|
+
export function buildBTT(documentId, blocks) {
|
|
58
|
+
const blocksById = new Map();
|
|
59
|
+
for (const block of blocks) {
|
|
60
|
+
blocksById.set(block.block_id, block);
|
|
61
|
+
}
|
|
62
|
+
const rootBlockId = pickRootBlockId(documentId, blocks, blocksById);
|
|
63
|
+
const ctx = {
|
|
64
|
+
blocksById,
|
|
65
|
+
missingChildren: new Set(),
|
|
66
|
+
};
|
|
67
|
+
const root = buildNode(rootBlockId, ctx, new Set());
|
|
68
|
+
return {
|
|
69
|
+
schema: 'BTT',
|
|
70
|
+
version: '1.0.0',
|
|
71
|
+
documentId,
|
|
72
|
+
generatedAt: new Date().toISOString(),
|
|
73
|
+
rootBlockId,
|
|
74
|
+
totalBlocks: blocks.length,
|
|
75
|
+
missingChildren: Array.from(ctx.missingChildren),
|
|
76
|
+
root,
|
|
77
|
+
flatBlocks: toFlatBlocks(blocks),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildBTT } from './build-tree.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { getPublishMdUsage, runPublishMdToLarkCli } from '../commands/publish-md/index.js';
|
|
4
|
+
async function main() {
|
|
5
|
+
try {
|
|
6
|
+
await runPublishMdToLarkCli(process.argv.slice(2), process.env);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
10
|
+
console.error('');
|
|
11
|
+
console.error(getPublishMdUsage());
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
void main();
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
function usage() {
|
|
2
|
+
return [
|
|
3
|
+
'Usage: npm run publish:md -- --input <file.md|dir> [--title <doc_title_or_prefix>] [--date-prefix|--no-date-prefix] [--preset <preset_name_or_module_path>] [--folder <folder_token>] [--doc <document_id>] [--download-remote-images|--no-download-remote-images] [--yt-dlp-path <path>] [--yt-dlp-cookies-path <path>] [--pipeline-cache-dir <dir>] [--mermaid-target <text-drawing|board>] [--mermaid-board-syntax-type <int>] [--mermaid-board-style-type <int>] [--mermaid-board-diagram-type <int>] [--dry-run] [--help|-h]',
|
|
4
|
+
'',
|
|
5
|
+
'Options:',
|
|
6
|
+
' --input Markdown file path, or directory path (publish all *.md recursively).',
|
|
7
|
+
' --title Single-file title. In directory mode this is used as title prefix.',
|
|
8
|
+
' --date-prefix Enable date prefix in final title: YYYYMMDD-<title>. Default: enabled.',
|
|
9
|
+
' --no-date-prefix Disable date prefix in final title.',
|
|
10
|
+
' --preset Optional preset module path (js/mjs/cjs/ts) or built-in name (e.g. medium). Used to transform markdown before publish pipeline.',
|
|
11
|
+
' --folder Feishu folder token. Default: LARK_FOLDER_TOKEN from .env',
|
|
12
|
+
' --doc Existing Feishu document id (single-file only). If set, publish directly into this doc (and clear content first).',
|
|
13
|
+
' --download-remote-images Enable prepare-stage remote image pre-download + link rewrite.',
|
|
14
|
+
' --no-download-remote-images Disable prepare-stage remote image pre-download.',
|
|
15
|
+
' --yt-dlp-path Optional yt-dlp executable path for standalone URL extraction.',
|
|
16
|
+
' --yt-dlp-cookies-path Optional cookie file path passed to yt-dlp --cookies.',
|
|
17
|
+
' --pipeline-cache-dir Pipeline cache root directory. Default: ./out/pipeline-cache',
|
|
18
|
+
' --mermaid-target Mermaid render target: text-drawing (default) or board.',
|
|
19
|
+
' --mermaid-board-syntax-type Optional integer syntax_type for board createPlantuml (default: 2).',
|
|
20
|
+
' --mermaid-board-style-type Optional integer style_type for board createPlantuml.',
|
|
21
|
+
' --mermaid-board-diagram-type Optional integer diagram_type for board createPlantuml.',
|
|
22
|
+
' --dry-run Build pipeline and print patch stats only, do not call Feishu API.',
|
|
23
|
+
' --help, -h Show this help message and exit.',
|
|
24
|
+
'',
|
|
25
|
+
'Notes:',
|
|
26
|
+
' 1) Local image/file paths are uploaded to Feishu and replaced by token.',
|
|
27
|
+
' 2) Mermaid code fences can be rendered as text-drawing (block_type=40) or board (block_type=43).',
|
|
28
|
+
' 3) Tables use width heuristics + numeric-column right align + row/column expansion strategy when size > 9.',
|
|
29
|
+
' 4) Final title uses date prefix by default (YYYYMMDD-<title>); disable with --no-date-prefix or LARK_TITLE_DATE_PREFIX=false.',
|
|
30
|
+
' 5) If markdown has exactly one h1 and --title is not provided, that h1 becomes doc title, then removed from content, and remaining headings are promoted by one level.',
|
|
31
|
+
' 6) Stage cache layout per markdown: 00-source, 01-prepare, 02-hast, 03-last, 04-btt, 05-publish.',
|
|
32
|
+
' 7) Prepare stage can pre-download remote markdown images and optional yt-dlp URL lines.',
|
|
33
|
+
' 8) Leading YAML/TOML frontmatter is rewritten as fenced code block (yaml/toml), so it stays visible and will not be parsed as headings.',
|
|
34
|
+
' 9) Missing local asset files are skipped/degraded to text fallback; publish will not fail only because a referenced local path is absent.',
|
|
35
|
+
'',
|
|
36
|
+
'Examples:',
|
|
37
|
+
' npm run publish:md -- --input ./docs/a.md',
|
|
38
|
+
' npm run publish:md -- --input ./docs --title Weekly --folder <token>',
|
|
39
|
+
' npm run publish:md -- --input ./docs/a.md --doc <document_id>',
|
|
40
|
+
' npm run publish:md -- --input ./docs/a.md --dry-run',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
export function getPublishMdUsage() {
|
|
44
|
+
return usage();
|
|
45
|
+
}
|
|
46
|
+
export function hasPublishMdHelpFlag(argv) {
|
|
47
|
+
return argv.includes('--help') || argv.includes('-h');
|
|
48
|
+
}
|
|
49
|
+
export function parsePublishMdArgs(argv, env = process.env) {
|
|
50
|
+
let inputPath = '';
|
|
51
|
+
let title = '';
|
|
52
|
+
let titleDatePrefix;
|
|
53
|
+
let presetPath = '';
|
|
54
|
+
let folderToken = (env.LARK_FOLDER_TOKEN ?? '').trim();
|
|
55
|
+
let documentId;
|
|
56
|
+
let downloadRemoteImages;
|
|
57
|
+
let ytDlpPath = '';
|
|
58
|
+
let ytDlpCookiesPath = '';
|
|
59
|
+
let pipelineCacheDir = '';
|
|
60
|
+
let mermaidTarget;
|
|
61
|
+
let mermaidBoardSyntaxType;
|
|
62
|
+
let mermaidBoardStyleType;
|
|
63
|
+
let mermaidBoardDiagramType;
|
|
64
|
+
let dryRun = false;
|
|
65
|
+
const parseNonNegativeInt = (name, value) => {
|
|
66
|
+
const parsed = Number.parseInt(value, 10);
|
|
67
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
68
|
+
throw new Error(`${name} must be a non-negative integer, received: ${value}`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
};
|
|
72
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
73
|
+
const arg = argv[i];
|
|
74
|
+
if (!arg)
|
|
75
|
+
continue;
|
|
76
|
+
if (arg === '--input') {
|
|
77
|
+
const value = argv[i + 1];
|
|
78
|
+
if (!value)
|
|
79
|
+
throw new Error('Missing value for --input.');
|
|
80
|
+
inputPath = value;
|
|
81
|
+
i += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg === '--title') {
|
|
85
|
+
const value = argv[i + 1];
|
|
86
|
+
if (!value)
|
|
87
|
+
throw new Error('Missing value for --title.');
|
|
88
|
+
title = value;
|
|
89
|
+
i += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg === '--date-prefix') {
|
|
93
|
+
titleDatePrefix = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (arg === '--no-date-prefix') {
|
|
97
|
+
titleDatePrefix = false;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === '--folder') {
|
|
101
|
+
const value = argv[i + 1];
|
|
102
|
+
if (!value)
|
|
103
|
+
throw new Error('Missing value for --folder.');
|
|
104
|
+
folderToken = value;
|
|
105
|
+
i += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg === '--preset') {
|
|
109
|
+
const value = argv[i + 1];
|
|
110
|
+
if (!value)
|
|
111
|
+
throw new Error('Missing value for --preset.');
|
|
112
|
+
presetPath = value;
|
|
113
|
+
i += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (arg === '--doc') {
|
|
117
|
+
const value = argv[i + 1];
|
|
118
|
+
if (!value)
|
|
119
|
+
throw new Error('Missing value for --doc.');
|
|
120
|
+
documentId = value;
|
|
121
|
+
i += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (arg === '--download-remote-images') {
|
|
125
|
+
downloadRemoteImages = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (arg === '--no-download-remote-images') {
|
|
129
|
+
downloadRemoteImages = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (arg === '--yt-dlp-path') {
|
|
133
|
+
const value = argv[i + 1];
|
|
134
|
+
if (!value)
|
|
135
|
+
throw new Error('Missing value for --yt-dlp-path.');
|
|
136
|
+
ytDlpPath = value;
|
|
137
|
+
i += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === '--yt-dlp-cookies-path') {
|
|
141
|
+
const value = argv[i + 1];
|
|
142
|
+
if (!value)
|
|
143
|
+
throw new Error('Missing value for --yt-dlp-cookies-path.');
|
|
144
|
+
ytDlpCookiesPath = value;
|
|
145
|
+
i += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (arg === '--pipeline-cache-dir') {
|
|
149
|
+
const value = argv[i + 1];
|
|
150
|
+
if (!value)
|
|
151
|
+
throw new Error('Missing value for --pipeline-cache-dir.');
|
|
152
|
+
pipelineCacheDir = value;
|
|
153
|
+
i += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (arg === '--mermaid-target') {
|
|
157
|
+
const value = argv[i + 1];
|
|
158
|
+
if (!value)
|
|
159
|
+
throw new Error('Missing value for --mermaid-target.');
|
|
160
|
+
mermaidTarget = value;
|
|
161
|
+
i += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (arg === '--mermaid-board-syntax-type') {
|
|
165
|
+
const value = argv[i + 1];
|
|
166
|
+
if (!value)
|
|
167
|
+
throw new Error('Missing value for --mermaid-board-syntax-type.');
|
|
168
|
+
mermaidBoardSyntaxType = parseNonNegativeInt('--mermaid-board-syntax-type', value);
|
|
169
|
+
i += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (arg === '--mermaid-board-style-type') {
|
|
173
|
+
const value = argv[i + 1];
|
|
174
|
+
if (!value)
|
|
175
|
+
throw new Error('Missing value for --mermaid-board-style-type.');
|
|
176
|
+
mermaidBoardStyleType = parseNonNegativeInt('--mermaid-board-style-type', value);
|
|
177
|
+
i += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (arg === '--mermaid-board-diagram-type') {
|
|
181
|
+
const value = argv[i + 1];
|
|
182
|
+
if (!value)
|
|
183
|
+
throw new Error('Missing value for --mermaid-board-diagram-type.');
|
|
184
|
+
mermaidBoardDiagramType = parseNonNegativeInt('--mermaid-board-diagram-type', value);
|
|
185
|
+
i += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (arg === '--dry-run') {
|
|
189
|
+
dryRun = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (arg.startsWith('-')) {
|
|
193
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
194
|
+
}
|
|
195
|
+
if (!inputPath) {
|
|
196
|
+
inputPath = arg;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Unexpected extra argument: ${arg}`);
|
|
200
|
+
}
|
|
201
|
+
if (!inputPath.trim()) {
|
|
202
|
+
throw new Error('Input path is required. Use --input <file.md|dir>.');
|
|
203
|
+
}
|
|
204
|
+
if (!documentId && !folderToken) {
|
|
205
|
+
throw new Error('Folder token is required when --doc is not provided. Use --folder or set LARK_FOLDER_TOKEN.');
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
inputPath: inputPath.trim(),
|
|
209
|
+
...(title.trim() ? { title: title.trim() } : {}),
|
|
210
|
+
...(titleDatePrefix === undefined ? {} : { titleDatePrefix }),
|
|
211
|
+
...(presetPath.trim() ? { presetPath: presetPath.trim() } : {}),
|
|
212
|
+
folderToken,
|
|
213
|
+
...(documentId ? { documentId: documentId.trim() } : {}),
|
|
214
|
+
...(downloadRemoteImages === undefined ? {} : { downloadRemoteImages }),
|
|
215
|
+
...(ytDlpPath.trim() ? { ytDlpPath: ytDlpPath.trim() } : {}),
|
|
216
|
+
...(ytDlpCookiesPath.trim() ? { ytDlpCookiesPath: ytDlpCookiesPath.trim() } : {}),
|
|
217
|
+
...(pipelineCacheDir.trim() ? { pipelineCacheDir: pipelineCacheDir.trim() } : {}),
|
|
218
|
+
...(mermaidTarget ? { mermaidTarget } : {}),
|
|
219
|
+
...(mermaidBoardSyntaxType === undefined ? {} : { mermaidBoardSyntaxType }),
|
|
220
|
+
...(mermaidBoardStyleType === undefined ? {} : { mermaidBoardStyleType }),
|
|
221
|
+
...(mermaidBoardDiagramType === undefined ? {} : { mermaidBoardDiagramType }),
|
|
222
|
+
dryRun,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getPublishMdUsage, hasPublishMdHelpFlag, parsePublishMdArgs } from './args.js';
|
|
2
|
+
import { resolvePublishInputSet } from './input-resolver.js';
|
|
3
|
+
import { loadMarkdownPreset } from './preset-loader.js';
|
|
4
|
+
import { createDocument, listFolderChildren, normalizeDocumentId } from '../../lark/docx/ops.js';
|
|
5
|
+
import { processSingleMarkdownFile } from '../../publish/process-file.js';
|
|
6
|
+
import { buildPublishRuntime, logPublishRuntimeSummary } from '../../publish/runtime.js';
|
|
7
|
+
import { sleep } from '../../shared/rate-limiter.js';
|
|
8
|
+
export { getPublishMdUsage, parsePublishMdArgs };
|
|
9
|
+
function buildFolderDocIndex(entries) {
|
|
10
|
+
const byTitle = new Map();
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
if (entry.type !== 'docx')
|
|
13
|
+
continue;
|
|
14
|
+
const title = entry.name;
|
|
15
|
+
const token = entry.token;
|
|
16
|
+
if (!title || !token)
|
|
17
|
+
continue;
|
|
18
|
+
const current = byTitle.get(title);
|
|
19
|
+
if (current) {
|
|
20
|
+
current.push(token);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
byTitle.set(title, [token]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return byTitle;
|
|
27
|
+
}
|
|
28
|
+
function prependDocIntoFolderIndex(index, title, documentId) {
|
|
29
|
+
const current = index.get(title);
|
|
30
|
+
if (current) {
|
|
31
|
+
if (!current.includes(documentId)) {
|
|
32
|
+
current.unshift(documentId);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
index.set(title, [documentId]);
|
|
37
|
+
}
|
|
38
|
+
function createFolderDocumentResolver(runtime, options) {
|
|
39
|
+
let folderDocIndex = null;
|
|
40
|
+
const ensureFolderDocIndex = async () => {
|
|
41
|
+
if (folderDocIndex)
|
|
42
|
+
return folderDocIndex;
|
|
43
|
+
if (!options.folderToken) {
|
|
44
|
+
throw new Error('Folder token is required when publishing without --doc.');
|
|
45
|
+
}
|
|
46
|
+
const files = await listFolderChildren(runtime.sdkClient, options.folderToken, runtime.authOptions, runtime.docxLimiter);
|
|
47
|
+
folderDocIndex = buildFolderDocIndex(files);
|
|
48
|
+
return folderDocIndex;
|
|
49
|
+
};
|
|
50
|
+
return async (title) => {
|
|
51
|
+
const byTitle = await ensureFolderDocIndex();
|
|
52
|
+
const sameNameDocs = byTitle.get(title) ?? [];
|
|
53
|
+
if (sameNameDocs.length > 0) {
|
|
54
|
+
return sameNameDocs[0] ?? '';
|
|
55
|
+
}
|
|
56
|
+
const documentId = await createDocument(runtime.sdkClient, options.folderToken, title, runtime.authOptions, runtime.docxLimiter);
|
|
57
|
+
prependDocIntoFolderIndex(byTitle, title, documentId);
|
|
58
|
+
return documentId;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function publishMdToLark(options, env = process.env) {
|
|
62
|
+
const inputSet = await resolvePublishInputSet(options.inputPath);
|
|
63
|
+
const markdownPreset = await loadMarkdownPreset(options.presetPath);
|
|
64
|
+
if (options.documentId && inputSet.markdownFiles.length !== 1) {
|
|
65
|
+
throw new Error('--doc only supports single markdown input file.');
|
|
66
|
+
}
|
|
67
|
+
const runtime = buildPublishRuntime(options, env, markdownPreset);
|
|
68
|
+
logPublishRuntimeSummary(runtime, inputSet.markdownFiles.length, inputSet.mode);
|
|
69
|
+
const normalizedDocumentId = options.documentId ? normalizeDocumentId(options.documentId) : undefined;
|
|
70
|
+
const resolveTargetDocumentId = options.dryRun || normalizedDocumentId
|
|
71
|
+
? undefined
|
|
72
|
+
: createFolderDocumentResolver(runtime, options);
|
|
73
|
+
for (let index = 0; index < inputSet.markdownFiles.length; index += 1) {
|
|
74
|
+
const markdownPath = inputSet.markdownFiles[index];
|
|
75
|
+
const perFileOptions = normalizedDocumentId ? { ...options, documentId: normalizedDocumentId } : options;
|
|
76
|
+
await processSingleMarkdownFile({
|
|
77
|
+
runtime,
|
|
78
|
+
inputSet,
|
|
79
|
+
options: perFileOptions,
|
|
80
|
+
markdownPath,
|
|
81
|
+
index,
|
|
82
|
+
...(resolveTargetDocumentId ? { resolveTargetDocumentId } : {}),
|
|
83
|
+
});
|
|
84
|
+
if (!options.dryRun && index < inputSet.markdownFiles.length - 1 && runtime.publishCooldownMs > 0) {
|
|
85
|
+
console.log(`[${index + 1}/${inputSet.markdownFiles.length}] Cooldown ${runtime.publishCooldownMs}ms before next markdown...`);
|
|
86
|
+
await sleep(runtime.publishCooldownMs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export async function runPublishMdToLarkCli(argv, env = process.env) {
|
|
91
|
+
if (hasPublishMdHelpFlag(argv)) {
|
|
92
|
+
console.log(getPublishMdUsage());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const options = parsePublishMdArgs(argv, env);
|
|
96
|
+
await publishMdToLark(options, env);
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getPublishMdUsage, parsePublishMdArgs, publishMdToLark, runPublishMdToLarkCli } from './command.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function isMarkdownFilePath(filePath) {
|
|
4
|
+
return /\.md$/i.test(filePath);
|
|
5
|
+
}
|
|
6
|
+
async function collectMarkdownFilesRecursive(dirPath) {
|
|
7
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
8
|
+
const files = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
const nested = await collectMarkdownFilesRecursive(fullPath);
|
|
13
|
+
files.push(...nested);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (entry.isFile() && isMarkdownFilePath(entry.name)) {
|
|
17
|
+
files.push(fullPath);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
files.sort((a, b) => a.localeCompare(b, 'en'));
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
export async function resolvePublishInputSet(inputPath) {
|
|
24
|
+
const absolute = path.resolve(inputPath);
|
|
25
|
+
const stats = await stat(absolute);
|
|
26
|
+
if (stats.isFile()) {
|
|
27
|
+
if (!isMarkdownFilePath(absolute)) {
|
|
28
|
+
throw new Error(`Input file is not .md: ${absolute}`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
mode: 'single',
|
|
32
|
+
rootPath: path.dirname(absolute),
|
|
33
|
+
markdownFiles: [absolute],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (stats.isDirectory()) {
|
|
37
|
+
const markdownFiles = await collectMarkdownFilesRecursive(absolute);
|
|
38
|
+
if (markdownFiles.length === 0) {
|
|
39
|
+
throw new Error(`No .md files found under directory: ${absolute}`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
mode: 'directory',
|
|
43
|
+
rootPath: absolute,
|
|
44
|
+
markdownFiles,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Input path is neither file nor directory: ${absolute}`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DEFAULT_MERMAID_RENDER_CONFIG, } from '../../lark/docx/render-types.js';
|
|
2
|
+
export { DEFAULT_MERMAID_BOARD_SYNTAX_TYPE } from '../../lark/docx/render-types.js';
|
|
3
|
+
const TEXT_DRAWING_TARGET_ALIASES = new Set(['text-drawing', 'text_drawing', 'textdrawing', 'text']);
|
|
4
|
+
const BOARD_TARGET_ALIASES = new Set(['board', 'whiteboard', 'canvas']);
|
|
5
|
+
export function normalizeMermaidRenderTarget(raw) {
|
|
6
|
+
const normalized = raw?.trim().toLowerCase() ?? '';
|
|
7
|
+
if (!normalized) {
|
|
8
|
+
return DEFAULT_MERMAID_RENDER_CONFIG.target;
|
|
9
|
+
}
|
|
10
|
+
if (TEXT_DRAWING_TARGET_ALIASES.has(normalized)) {
|
|
11
|
+
return 'text-drawing';
|
|
12
|
+
}
|
|
13
|
+
if (BOARD_TARGET_ALIASES.has(normalized)) {
|
|
14
|
+
return 'board';
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Invalid mermaid target "${raw}". Expected one of: text-drawing, board (aliases: text_drawing, textdrawing, whiteboard, canvas).`);
|
|
17
|
+
}
|