@jacobbubu/md-to-lark 1.5.1 → 1.7.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
@@ -174,13 +174,17 @@ Guardrails:
174
174
  - Single-file and recursive directory publish
175
175
  - Title derivation, title prefix, and single-H1 promotion
176
176
  - Local attachment and image detection with real upload
177
+ - Programmatic image display width control through `imageSizeResolver`
178
+ - Versioned `article-render/v1` contracts with inheritance, capability negotiation, strict pre-publish validation, and render reports
179
+ - Semantic figures, tables, equations, callouts, and GFM footnotes rendered to native Lark block structures
180
+ - Automatic `assets/manifest.yml` image sizing in renderer-contract mode
177
181
  - Remote image download and standalone URL preparation
178
182
  - Mermaid `text-drawing` and `board` output paths
179
183
  - Opt-in single-dollar inline math parsing for LaTeX-heavy papers
180
184
  - Table width heuristics and numeric-column right alignment
181
185
  - Chinese Markdown formatting preset (`zh-format`)
182
186
  - Ordered preset composition from CLI and programmatic usage
183
- - Stage cache output from `00-source` to `05-publish`
187
+ - Legacy stage cache output from `00-source` to `05-publish`; protocol mode adds contract and semantic stages through `07-publish`
184
188
  - Programmatic access through `publishMdToLark`
185
189
 
186
190
  ## Where To Read Next
@@ -1,6 +1,6 @@
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>] [--resource-base-dir <dir>] [--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>] [--single-dollar-text-math] [--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> [--renderer lark] [--renderer-contract <path>] [--strict] [--render-report <path>] [--image-size-manifest <path>] [other options]',
4
4
  '',
5
5
  'Options:',
6
6
  ' --input Markdown file path, or directory path (publish all *.md recursively).',
@@ -10,6 +10,12 @@ function usage() {
10
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
  ' --resource-base-dir Base directory used to resolve relative local image/file paths. Default: the current markdown file directory.',
13
+ ' --renderer Target renderer. Default: lark.',
14
+ ' --renderer-contract Explicit article-render/v1 contract path.',
15
+ ' --renderer-default-contract Fallback renderer contract path when no article contract is found.',
16
+ ' --strict Require a valid renderer contract and fail before publish on semantic errors.',
17
+ ' --render-report Write the article-render/v1 JSON report to this path.',
18
+ ' --image-size-manifest Explicit assets/manifest.yml path for image display sizing.',
13
19
  ' --folder Feishu folder token. Default: LARK_FOLDER_TOKEN from .env',
14
20
  ' --doc Existing Feishu document id (single-file only). If set, publish directly into this doc (and clear content first).',
15
21
  ' --download-remote-images Enable prepare-stage remote image pre-download + link rewrite.',
@@ -58,6 +64,12 @@ export function parsePublishMdArgs(argv, env = process.env) {
58
64
  const presetPaths = [];
59
65
  let documentBaseUrl = '';
60
66
  let resourceBaseDir = '';
67
+ let renderer = '';
68
+ let rendererContractPath = '';
69
+ let rendererDefaultContractPath = '';
70
+ let strict;
71
+ let renderReportPath = '';
72
+ let imageSizeManifestPath = '';
61
73
  let folderToken = (env.LARK_FOLDER_TOKEN ?? '').trim();
62
74
  let documentId;
63
75
  let downloadRemoteImages;
@@ -137,6 +149,50 @@ export function parsePublishMdArgs(argv, env = process.env) {
137
149
  i += 1;
138
150
  continue;
139
151
  }
152
+ if (arg === '--renderer') {
153
+ const value = argv[i + 1];
154
+ if (!value)
155
+ throw new Error('Missing value for --renderer.');
156
+ renderer = value;
157
+ i += 1;
158
+ continue;
159
+ }
160
+ if (arg === '--renderer-contract') {
161
+ const value = argv[i + 1];
162
+ if (!value)
163
+ throw new Error('Missing value for --renderer-contract.');
164
+ rendererContractPath = value;
165
+ i += 1;
166
+ continue;
167
+ }
168
+ if (arg === '--renderer-default-contract') {
169
+ const value = argv[i + 1];
170
+ if (!value)
171
+ throw new Error('Missing value for --renderer-default-contract.');
172
+ rendererDefaultContractPath = value;
173
+ i += 1;
174
+ continue;
175
+ }
176
+ if (arg === '--strict') {
177
+ strict = true;
178
+ continue;
179
+ }
180
+ if (arg === '--render-report') {
181
+ const value = argv[i + 1];
182
+ if (!value)
183
+ throw new Error('Missing value for --render-report.');
184
+ renderReportPath = value;
185
+ i += 1;
186
+ continue;
187
+ }
188
+ if (arg === '--image-size-manifest') {
189
+ const value = argv[i + 1];
190
+ if (!value)
191
+ throw new Error('Missing value for --image-size-manifest.');
192
+ imageSizeManifestPath = value;
193
+ i += 1;
194
+ continue;
195
+ }
140
196
  if (arg === '--doc') {
141
197
  const value = argv[i + 1];
142
198
  if (!value)
@@ -245,6 +301,12 @@ export function parsePublishMdArgs(argv, env = process.env) {
245
301
  : {}),
246
302
  ...(documentBaseUrl.trim() ? { documentBaseUrl: documentBaseUrl.trim() } : {}),
247
303
  ...(resourceBaseDir.trim() ? { resourceBaseDir: resourceBaseDir.trim() } : {}),
304
+ ...(renderer.trim() ? { renderer: renderer.trim() } : {}),
305
+ ...(rendererContractPath.trim() ? { rendererContractPath: rendererContractPath.trim() } : {}),
306
+ ...(rendererDefaultContractPath.trim() ? { rendererDefaultContractPath: rendererDefaultContractPath.trim() } : {}),
307
+ ...(strict === undefined ? {} : { strict }),
308
+ ...(renderReportPath.trim() ? { renderReportPath: renderReportPath.trim() } : {}),
309
+ ...(imageSizeManifestPath.trim() ? { imageSizeManifestPath: imageSizeManifestPath.trim() } : {}),
248
310
  folderToken,
249
311
  ...(documentId ? { documentId: documentId.trim() } : {}),
250
312
  ...(downloadRemoteImages === undefined ? {} : { downloadRemoteImages }),
@@ -1,10 +1,11 @@
1
- import { getPublishMdUsage, hasPublishMdHelpFlag, parsePublishMdArgs } from './args.js';
1
+ import { getPublishMdUsage, hasPublishMdHelpFlag, parsePublishMdArgs, } from './args.js';
2
2
  import { resolvePublishInputSet } from './input-resolver.js';
3
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
+ import { getRendererCapabilities, validateRendererContract } from '../../protocol/index.js';
8
9
  export { getPublishMdUsage, parsePublishMdArgs };
9
10
  function resolveMarkdownPresetRefs(options) {
10
11
  if (options.presetPaths && options.presetPaths.length > 0) {
@@ -65,19 +66,29 @@ function createFolderDocumentResolver(runtime, options) {
65
66
  };
66
67
  }
67
68
  export async function publishMdToLark(options, env = process.env) {
68
- const inputSet = await resolvePublishInputSet(options.inputPath);
69
- const markdownPresets = await loadMarkdownPresets(resolveMarkdownPresetRefs(options));
70
- if (options.documentId && inputSet.markdownFiles.length !== 1) {
69
+ const publishOptions = {
70
+ ...options,
71
+ folderToken: options.folderToken?.trim() || env.LARK_FOLDER_TOKEN?.trim() || '',
72
+ dryRun: options.dryRun ?? false,
73
+ };
74
+ if (!publishOptions.documentId && !publishOptions.folderToken) {
75
+ throw new Error('Folder token is required when documentId is not provided.');
76
+ }
77
+ const inputSet = await resolvePublishInputSet(publishOptions.inputPath);
78
+ const markdownPresets = await loadMarkdownPresets(resolveMarkdownPresetRefs(publishOptions));
79
+ if (publishOptions.documentId && inputSet.markdownFiles.length !== 1) {
71
80
  throw new Error('--doc only supports single markdown input file.');
72
81
  }
73
- const runtime = buildPublishRuntime(options, env, markdownPresets);
82
+ const runtime = buildPublishRuntime(publishOptions, env, markdownPresets);
74
83
  logPublishRuntimeSummary(runtime, inputSet.markdownFiles.length, inputSet.mode);
75
- const normalizedDocumentId = options.documentId ? normalizeDocumentId(options.documentId) : undefined;
76
- const resolveTargetDocumentId = options.dryRun || normalizedDocumentId ? undefined : createFolderDocumentResolver(runtime, options);
84
+ const normalizedDocumentId = publishOptions.documentId ? normalizeDocumentId(publishOptions.documentId) : undefined;
85
+ const resolveTargetDocumentId = publishOptions.dryRun || normalizedDocumentId ? undefined : createFolderDocumentResolver(runtime, publishOptions);
77
86
  const results = [];
78
87
  for (let index = 0; index < inputSet.markdownFiles.length; index += 1) {
79
88
  const markdownPath = inputSet.markdownFiles[index];
80
- const perFileOptions = normalizedDocumentId ? { ...options, documentId: normalizedDocumentId } : options;
89
+ const perFileOptions = normalizedDocumentId
90
+ ? { ...publishOptions, documentId: normalizedDocumentId }
91
+ : publishOptions;
81
92
  const result = await processSingleMarkdownFile({
82
93
  runtime,
83
94
  inputSet,
@@ -92,7 +103,7 @@ export async function publishMdToLark(options, env = process.env) {
92
103
  status: result.status,
93
104
  documentUrl: result.documentUrl,
94
105
  });
95
- if (!options.dryRun && index < inputSet.markdownFiles.length - 1 && runtime.publishCooldownMs > 0) {
106
+ if (!publishOptions.dryRun && index < inputSet.markdownFiles.length - 1 && runtime.publishCooldownMs > 0) {
96
107
  console.error(`[${index + 1}/${inputSet.markdownFiles.length}] Cooldown ${runtime.publishCooldownMs}ms before next markdown...`);
97
108
  await sleep(runtime.publishCooldownMs);
98
109
  }
@@ -104,6 +115,21 @@ export async function runPublishMdToLarkCli(argv, env = process.env) {
104
115
  console.log(getPublishMdUsage());
105
116
  return;
106
117
  }
118
+ if (argv.includes('--print-capabilities')) {
119
+ const rendererIndex = argv.indexOf('--renderer');
120
+ const renderer = rendererIndex >= 0 ? argv[rendererIndex + 1] : undefined;
121
+ console.log(JSON.stringify(getRendererCapabilities(renderer), null, 2));
122
+ return;
123
+ }
124
+ const validateIndex = argv.indexOf('--validate-contract');
125
+ if (validateIndex >= 0) {
126
+ const contractPath = argv[validateIndex + 1];
127
+ if (!contractPath)
128
+ throw new Error('Missing value for --validate-contract.');
129
+ const resolved = await validateRendererContract(contractPath);
130
+ console.log(JSON.stringify(resolved, null, 2));
131
+ return;
132
+ }
107
133
  const options = parsePublishMdArgs(argv, env);
108
134
  const results = await publishMdToLark(options, env);
109
135
  process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { publishMdToLark } from './commands/publish-md/index.js';
2
+ export { getRendererCapabilities, parseArticleFrontmatter, resolveRendererContract, validateRendererContract, } from './protocol/index.js';