@jacobbubu/md-to-lark 1.2.0 → 1.3.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/README.md CHANGED
@@ -23,7 +23,7 @@ Notes:
23
23
  - Recursively publishing multiple `.md` files from a directory
24
24
  - Preparing local assets, remote images, and standalone URLs before publish
25
25
  - Running a full dry-run without writing to Feishu
26
- - Rewriting Markdown before publish with presets
26
+ - Rewriting Markdown before publish with one or more ordered presets
27
27
 
28
28
  ## Quick Start
29
29
 
@@ -109,6 +109,7 @@ Presets, Mermaid, and stage artifacts:
109
109
  ```bash
110
110
  npm run publish:md -- --input ./test-md/comp/comp.md --preset medium --dry-run
111
111
  npm run publish:md -- --input ./test-md/comp/comp.md --preset zh-format --dry-run
112
+ npm run publish:md -- --input ./test-md/comp/comp.md --preset zh-format --preset ./my-preset.mjs --dry-run
112
113
  npm run publish:md -- --input ./test-md/mermaid.md --mermaid-target board --dry-run
113
114
  npm run publish:md -- --input ./test-md/comp/comp.md --pipeline-cache-dir ./out/debug-cache --dry-run
114
115
  ```
@@ -173,6 +174,7 @@ Guardrails:
173
174
  - Mermaid `text-drawing` and `board` output paths
174
175
  - Table width heuristics and numeric-column right alignment
175
176
  - Chinese Markdown formatting preset (`zh-format`)
177
+ - Ordered preset composition from CLI and programmatic usage
176
178
  - Stage cache output from `00-source` to `05-publish`
177
179
  - Programmatic access through `publishMdToLark`
178
180
 
@@ -1,13 +1,13 @@
1
1
  function usage() {
2
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>] [--document-base-url <base_url>] [--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]',
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>]... [--document-base-url <base_url>] [--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
4
  '',
5
5
  'Options:',
6
6
  ' --input Markdown file path, or directory path (publish all *.md recursively).',
7
7
  ' --title Single-file title. In directory mode this is used as title prefix.',
8
8
  ' --date-prefix Enable date prefix in final title: YYYYMMDD-<title>. Default: enabled.',
9
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.',
10
+ ' --preset Optional preset module path (js/mjs/cjs/ts) or built-in name (e.g. medium). Repeatable; presets run in the given order before publish pipeline.',
11
11
  ' --document-base-url Base URL used to build documentUrl results (for example https://li.feishu.cn).',
12
12
  ' --folder Feishu folder token. Default: LARK_FOLDER_TOKEN from .env',
13
13
  ' --doc Existing Feishu document id (single-file only). If set, publish directly into this doc (and clear content first).',
@@ -51,7 +51,7 @@ export function parsePublishMdArgs(argv, env = process.env) {
51
51
  let inputPath = '';
52
52
  let title = '';
53
53
  let titleDatePrefix;
54
- let presetPath = '';
54
+ const presetPaths = [];
55
55
  let documentBaseUrl = '';
56
56
  let folderToken = (env.LARK_FOLDER_TOKEN ?? '').trim();
57
57
  let documentId;
@@ -111,7 +111,7 @@ export function parsePublishMdArgs(argv, env = process.env) {
111
111
  const value = argv[i + 1];
112
112
  if (!value)
113
113
  throw new Error('Missing value for --preset.');
114
- presetPath = value;
114
+ presetPaths.push(value);
115
115
  i += 1;
116
116
  continue;
117
117
  }
@@ -214,11 +214,17 @@ export function parsePublishMdArgs(argv, env = process.env) {
214
214
  if (!documentId && !folderToken) {
215
215
  throw new Error('Folder token is required when --doc is not provided. Use --folder or set LARK_FOLDER_TOKEN.');
216
216
  }
217
+ const normalizedPresetPaths = presetPaths.map((presetPath) => presetPath.trim()).filter(Boolean);
217
218
  return {
218
219
  inputPath: inputPath.trim(),
219
220
  ...(title.trim() ? { title: title.trim() } : {}),
220
221
  ...(titleDatePrefix === undefined ? {} : { titleDatePrefix }),
221
- ...(presetPath.trim() ? { presetPath: presetPath.trim() } : {}),
222
+ ...(normalizedPresetPaths.length > 0
223
+ ? {
224
+ ...(normalizedPresetPaths.length === 1 ? { presetPath: normalizedPresetPaths[0] } : {}),
225
+ presetPaths: normalizedPresetPaths,
226
+ }
227
+ : {}),
222
228
  ...(documentBaseUrl.trim() ? { documentBaseUrl: documentBaseUrl.trim() } : {}),
223
229
  folderToken,
224
230
  ...(documentId ? { documentId: documentId.trim() } : {}),
@@ -1,11 +1,17 @@
1
1
  import { getPublishMdUsage, hasPublishMdHelpFlag, parsePublishMdArgs } from './args.js';
2
2
  import { resolvePublishInputSet } from './input-resolver.js';
3
- import { loadMarkdownPreset } from './preset-loader.js';
3
+ import { loadMarkdownPresets } from './preset-loader.js';
4
4
  import { createDocument, listFolderChildren, normalizeDocumentId } from '../../lark/docx/ops.js';
5
5
  import { processSingleMarkdownFile } from '../../publish/process-file.js';
6
6
  import { buildPublishRuntime, logPublishRuntimeSummary } from '../../publish/runtime.js';
7
7
  import { sleep } from '../../shared/rate-limiter.js';
8
8
  export { getPublishMdUsage, parsePublishMdArgs };
9
+ function resolveMarkdownPresetRefs(options) {
10
+ if (options.presetPaths && options.presetPaths.length > 0) {
11
+ return options.presetPaths.map((presetPath) => presetPath.trim()).filter(Boolean);
12
+ }
13
+ return options.presetPath?.trim() ? [options.presetPath.trim()] : [];
14
+ }
9
15
  function buildFolderDocIndex(entries) {
10
16
  const byTitle = new Map();
11
17
  for (const entry of entries) {
@@ -60,11 +66,11 @@ function createFolderDocumentResolver(runtime, options) {
60
66
  }
61
67
  export async function publishMdToLark(options, env = process.env) {
62
68
  const inputSet = await resolvePublishInputSet(options.inputPath);
63
- const markdownPreset = await loadMarkdownPreset(options.presetPath);
69
+ const markdownPresets = await loadMarkdownPresets(resolveMarkdownPresetRefs(options));
64
70
  if (options.documentId && inputSet.markdownFiles.length !== 1) {
65
71
  throw new Error('--doc only supports single markdown input file.');
66
72
  }
67
- const runtime = buildPublishRuntime(options, env, markdownPreset);
73
+ const runtime = buildPublishRuntime(options, env, markdownPresets);
68
74
  logPublishRuntimeSummary(runtime, inputSet.markdownFiles.length, inputSet.mode);
69
75
  const normalizedDocumentId = options.documentId ? normalizeDocumentId(options.documentId) : undefined;
70
76
  const resolveTargetDocumentId = options.dryRun || normalizedDocumentId
@@ -66,7 +66,7 @@ function resolveBuiltInPreset(rawPreset) {
66
66
  export function listBuiltinMarkdownPresetNames() {
67
67
  return Object.keys(BUILTIN_MARKDOWN_PRESETS);
68
68
  }
69
- export async function loadMarkdownPreset(rawPath) {
69
+ async function loadSingleMarkdownPreset(rawPath) {
70
70
  const trimmed = (rawPath ?? '').trim();
71
71
  if (!trimmed)
72
72
  return null;
@@ -111,3 +111,20 @@ export async function loadMarkdownPreset(rawPath) {
111
111
  transform: wrappedTransform,
112
112
  };
113
113
  }
114
+ export async function loadMarkdownPresets(rawPaths) {
115
+ const normalized = (rawPaths ?? []).map((rawPath) => rawPath.trim()).filter(Boolean);
116
+ if (normalized.length === 0)
117
+ return [];
118
+ const presets = [];
119
+ for (const rawPath of normalized) {
120
+ const preset = await loadSingleMarkdownPreset(rawPath);
121
+ if (preset) {
122
+ presets.push(preset);
123
+ }
124
+ }
125
+ return presets;
126
+ }
127
+ export async function loadMarkdownPreset(rawPath) {
128
+ const presets = await loadMarkdownPresets(rawPath ? [rawPath] : []);
129
+ return presets[0] ?? null;
130
+ }
@@ -70,18 +70,20 @@ export async function processSingleMarkdownFile(params) {
70
70
  const startedAt = new Date().toISOString();
71
71
  const sourceMarkdown = await readFile(markdownPath, 'utf8');
72
72
  let markdown = sourceMarkdown;
73
- if (runtime.markdownPreset) {
74
- markdown = await runtime.markdownPreset.transform(markdown, {
73
+ for (let presetIndex = 0; presetIndex < runtime.markdownPresets.length; presetIndex += 1) {
74
+ const preset = runtime.markdownPresets[presetIndex];
75
+ markdown = await preset.transform(markdown, {
75
76
  inputPath: markdownPath,
76
77
  index,
77
78
  total: inputSet.markdownFiles.length,
78
79
  env: runtime.env,
79
- log: (...args) => console.error(`[preset ${index + 1}/${inputSet.markdownFiles.length}]`, ...args.map((arg) => String(arg))),
80
+ log: (...args) => console.error(`[preset ${presetIndex + 1}/${runtime.markdownPresets.length} file ${index + 1}/${inputSet.markdownFiles.length}] [${preset.displayPath}]`, ...args.map((arg) => String(arg))),
80
81
  });
81
82
  }
82
83
  await writeSourceStage(stagePaths, sourceMarkdown, markdown, {
83
84
  sourcePath: path.resolve(markdownPath),
84
- preset: runtime.markdownPreset ? runtime.markdownPreset.displayPath : null,
85
+ preset: runtime.markdownPresets.length === 1 ? runtime.markdownPresets[0].displayPath : null,
86
+ presets: runtime.markdownPresets.map((preset) => preset.displayPath),
85
87
  startedAt,
86
88
  });
87
89
  const prepareResult = await prepareMarkdownBeforePublish(markdownPath, markdown, {
@@ -53,7 +53,7 @@ function normalizeOptionalPath(value) {
53
53
  }
54
54
  return trimmed;
55
55
  }
56
- export function buildPublishRuntime(options, env, markdownPreset) {
56
+ export function buildPublishRuntime(options, env, markdownPresets) {
57
57
  const config = createLarkClientConfigFromEnv(env);
58
58
  const sdkClient = new lark.Client({
59
59
  appId: config.appId,
@@ -100,7 +100,8 @@ export function buildPublishRuntime(options, env, markdownPreset) {
100
100
  };
101
101
  return {
102
102
  env,
103
- markdownPreset,
103
+ markdownPresets,
104
+ markdownPreset: markdownPresets[0] ?? null,
104
105
  documentBaseUrl,
105
106
  documentUrlFor: (documentId) => buildLarkDocumentUrl(documentBaseUrl, documentId),
106
107
  authOptions,
@@ -136,6 +137,10 @@ export function logPublishRuntimeSummary(runtime, inputCount, inputMode) {
136
137
  ? `Mermaid: target=board syntax_type=${String(runtime.mermaidRenderConfig.board.syntaxType)} style_type=${String(runtime.mermaidRenderConfig.board.styleType ?? '(default)')} diagram_type=${String(runtime.mermaidRenderConfig.board.diagramType ?? '(default)')}`
137
138
  : 'Mermaid: target=text-drawing');
138
139
  console.error(`Document URL base: ${runtime.documentBaseUrl}`);
140
+ if (runtime.markdownPresets.length > 1) {
141
+ console.error(`Presets: ${runtime.markdownPresets.map((preset) => preset.displayPath).join(' -> ')}`);
142
+ return;
143
+ }
139
144
  if (runtime.markdownPreset) {
140
145
  console.error(`Preset: ${runtime.markdownPreset.displayPath}`);
141
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobbubu/md-to-lark",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Publish Markdown to Feishu docs with a stable pipeline.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",