@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.
Files changed (58) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +171 -0
  3. package/dist/btt/build-tree.js +79 -0
  4. package/dist/btt/index.js +1 -0
  5. package/dist/btt/types.js +1 -0
  6. package/dist/cli/publish-md-to-lark.js +15 -0
  7. package/dist/commands/publish-md/args.js +224 -0
  8. package/dist/commands/publish-md/command.js +97 -0
  9. package/dist/commands/publish-md/index.js +1 -0
  10. package/dist/commands/publish-md/input-resolver.js +48 -0
  11. package/dist/commands/publish-md/mermaid-render.js +17 -0
  12. package/dist/commands/publish-md/pipeline-transform.js +4 -0
  13. package/dist/commands/publish-md/preset-loader.js +113 -0
  14. package/dist/commands/publish-md/presets/medium.js +7 -0
  15. package/dist/commands/publish-md/presets/zh-format.js +8 -0
  16. package/dist/commands/publish-md/title-policy.js +93 -0
  17. package/dist/index.js +1 -0
  18. package/dist/interop/btt-to-last.js +79 -0
  19. package/dist/interop/codec-btt-to-last.js +435 -0
  20. package/dist/interop/codec-last-to-btt.js +383 -0
  21. package/dist/interop/codec-shared.js +722 -0
  22. package/dist/interop/index.js +2 -0
  23. package/dist/interop/last-to-btt.js +17 -0
  24. package/dist/lark/block-types.js +42 -0
  25. package/dist/lark/client.js +36 -0
  26. package/dist/lark/docx/ops.js +596 -0
  27. package/dist/lark/docx/render-btt.js +156 -0
  28. package/dist/lark/docx/render-models.js +1 -0
  29. package/dist/lark/docx/render-payload.js +338 -0
  30. package/dist/lark/docx/render-post-process.js +98 -0
  31. package/dist/lark/docx/render-table.js +87 -0
  32. package/dist/lark/docx/render-types.js +7 -0
  33. package/dist/lark/index.js +2 -0
  34. package/dist/lark/types.js +1 -0
  35. package/dist/last/api.js +1687 -0
  36. package/dist/last/index.js +3 -0
  37. package/dist/last/preview-terminal.js +296 -0
  38. package/dist/last/textual-block-types.js +19 -0
  39. package/dist/last/to-markdown.js +303 -0
  40. package/dist/last/types.js +11 -0
  41. package/dist/pipeline/hast-to-last.js +946 -0
  42. package/dist/pipeline/index.js +3 -0
  43. package/dist/pipeline/markdown/md-to-hast.js +34 -0
  44. package/dist/pipeline/markdown/prepare-markdown.js +1049 -0
  45. package/dist/preview/index.js +1 -0
  46. package/dist/preview/markdown-terminal.js +350 -0
  47. package/dist/publish/asset-adapter.js +123 -0
  48. package/dist/publish/btt-patch.js +65 -0
  49. package/dist/publish/common.js +139 -0
  50. package/dist/publish/ids.js +9 -0
  51. package/dist/publish/index.js +7 -0
  52. package/dist/publish/last-normalize.js +327 -0
  53. package/dist/publish/process-file.js +228 -0
  54. package/dist/publish/runtime.js +133 -0
  55. package/dist/publish/stage-cache.js +56 -0
  56. package/dist/shared/rate-limiter.js +18 -0
  57. package/dist/shared/retry.js +141 -0
  58. 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
+ }