@jacobbubu/md-to-lark 1.3.1 → 1.4.1

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
@@ -86,6 +86,8 @@ Progress logs and exceptions are written to stderr.
86
86
  - Otherwise use `LARK_DOCUMENT_BASE_URL`
87
87
  - Otherwise fall back to the current compatibility derivation from `LARK_BASE_URL`
88
88
 
89
+ Relative local assets such as `./img-001.png` still resolve against the Markdown file directory by default. If your caller generates a temporary Markdown file elsewhere, use `--resource-base-dir` to keep local asset resolution pinned to the original content directory.
90
+
89
91
  ## Common Commands
90
92
 
91
93
  Basic publish:
@@ -110,6 +112,7 @@ Presets, Mermaid, and stage artifacts:
110
112
  npm run publish:md -- --input ./test-md/comp/comp.md --preset medium --dry-run
111
113
  npm run publish:md -- --input ./test-md/comp/comp.md --preset zh-format --dry-run
112
114
  npm run publish:md -- --input ./test-md/comp/comp.md --preset zh-format --preset ./my-preset.mjs --dry-run
115
+ npm run publish:md -- --input ./tmp/generated/article.md --resource-base-dir ./source-assets --dry-run
113
116
  npm run publish:md -- --input ./test-md/mermaid.md --mermaid-target board --dry-run
114
117
  npm run publish:md -- --input ./test-md/comp/comp.md --pipeline-cache-dir ./out/debug-cache --dry-run
115
118
  ```
@@ -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>] [--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>] [--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>] [--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).',
@@ -9,6 +9,7 @@ function usage() {
9
9
  ' --no-date-prefix Disable date prefix in final title.',
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
+ ' --resource-base-dir Base directory used to resolve relative local image/file paths. Default: the current markdown file directory.',
12
13
  ' --folder Feishu folder token. Default: LARK_FOLDER_TOKEN from .env',
13
14
  ' --doc Existing Feishu document id (single-file only). If set, publish directly into this doc (and clear content first).',
14
15
  ' --download-remote-images Enable prepare-stage remote image pre-download + link rewrite.',
@@ -33,12 +34,14 @@ function usage() {
33
34
  ' 7) Prepare stage can pre-download remote markdown images and optional yt-dlp URL lines.',
34
35
  ' 8) Leading YAML/TOML frontmatter is rewritten as fenced code block (yaml/toml), so it stays visible and will not be parsed as headings.',
35
36
  ' 9) Missing local asset files are skipped/degraded to text fallback; publish will not fail only because a referenced local path is absent.',
37
+ ' 10) Relative local asset paths resolve against the markdown file directory by default; use --resource-base-dir to override that base.',
36
38
  '',
37
39
  'Examples:',
38
40
  ' npm run publish:md -- --input ./docs/a.md',
39
41
  ' npm run publish:md -- --input ./docs --title Weekly --folder <token>',
40
42
  ' npm run publish:md -- --input ./docs/a.md --doc <document_id>',
41
43
  ' npm run publish:md -- --input ./docs/a.md --dry-run',
44
+ ' npm run publish:md -- --input ./tmp/generated/article.md --resource-base-dir ./original-assets --dry-run',
42
45
  ].join('\n');
43
46
  }
44
47
  export function getPublishMdUsage() {
@@ -53,6 +56,7 @@ export function parsePublishMdArgs(argv, env = process.env) {
53
56
  let titleDatePrefix;
54
57
  const presetPaths = [];
55
58
  let documentBaseUrl = '';
59
+ let resourceBaseDir = '';
56
60
  let folderToken = (env.LARK_FOLDER_TOKEN ?? '').trim();
57
61
  let documentId;
58
62
  let downloadRemoteImages;
@@ -123,6 +127,14 @@ export function parsePublishMdArgs(argv, env = process.env) {
123
127
  i += 1;
124
128
  continue;
125
129
  }
130
+ if (arg === '--resource-base-dir') {
131
+ const value = argv[i + 1];
132
+ if (!value)
133
+ throw new Error('Missing value for --resource-base-dir.');
134
+ resourceBaseDir = value;
135
+ i += 1;
136
+ continue;
137
+ }
126
138
  if (arg === '--doc') {
127
139
  const value = argv[i + 1];
128
140
  if (!value)
@@ -226,6 +238,7 @@ export function parsePublishMdArgs(argv, env = process.env) {
226
238
  }
227
239
  : {}),
228
240
  ...(documentBaseUrl.trim() ? { documentBaseUrl: documentBaseUrl.trim() } : {}),
241
+ ...(resourceBaseDir.trim() ? { resourceBaseDir: resourceBaseDir.trim() } : {}),
229
242
  folderToken,
230
243
  ...(documentId ? { documentId: documentId.trim() } : {}),
231
244
  ...(downloadRemoteImages === undefined ? {} : { downloadRemoteImages }),
@@ -511,7 +511,7 @@ export function isRelationMismatchError(error) {
511
511
  return false;
512
512
  return /1770013|relation mismatch/i.test(error.message);
513
513
  }
514
- export async function replaceImageBlock(client, documentId, blockId, imageToken, authOptions, limiter) {
514
+ export async function replaceImageBlock(client, documentId, blockId, imageToken, imageOptions, authOptions, limiter) {
515
515
  await limiter.wait();
516
516
  const response = await withRetry('docx.documentBlock.batchUpdate(replace_image)', async () => client.docx.documentBlock.batchUpdate({
517
517
  path: {
@@ -523,6 +523,11 @@ export async function replaceImageBlock(client, documentId, blockId, imageToken,
523
523
  block_id: blockId,
524
524
  replace_image: {
525
525
  token: imageToken,
526
+ ...(typeof imageOptions.width === 'number' ? { width: imageOptions.width } : {}),
527
+ ...(typeof imageOptions.height === 'number' ? { height: imageOptions.height } : {}),
528
+ ...(typeof imageOptions.align === 'number' ? { align: imageOptions.align } : {}),
529
+ ...(imageOptions.caption ? { caption: imageOptions.caption } : {}),
530
+ ...(typeof imageOptions.scale === 'number' ? { scale: imageOptions.scale } : {}),
526
531
  },
527
532
  },
528
533
  ],
@@ -25,16 +25,23 @@ export async function applyCreatedImageBlock(client, documentId, createdBlockId,
25
25
  const localPath = image && typeof image.local_path === 'string' ? image.local_path : '';
26
26
  if (!localPath)
27
27
  return null;
28
+ const replaceOptions = {
29
+ ...(image && typeof image.width === 'number' ? { width: image.width } : {}),
30
+ ...(image && typeof image.height === 'number' ? { height: image.height } : {}),
31
+ ...(image && typeof image.align === 'number' ? { align: image.align } : {}),
32
+ ...(image && toObjectRecord(image.caption) ? { caption: toObjectRecord(image.caption) } : {}),
33
+ ...(image && typeof image.scale === 'number' ? { scale: image.scale } : {}),
34
+ };
28
35
  let imageToken = await uploadBinaryToNode(client, 'docx_image', createdBlockId, localPath, authOptions, mediaLimiter);
29
36
  try {
30
- await replaceImageBlock(client, documentId, createdBlockId, imageToken, authOptions, docxLimiter);
37
+ await replaceImageBlock(client, documentId, createdBlockId, imageToken, replaceOptions, authOptions, docxLimiter);
31
38
  }
32
39
  catch (error) {
33
40
  if (!isRelationMismatchError(error)) {
34
41
  throw error;
35
42
  }
36
43
  imageToken = await uploadBinaryToNode(client, 'docx_image', createdBlockId, localPath, authOptions, mediaLimiter);
37
- await replaceImageBlock(client, documentId, createdBlockId, imageToken, authOptions, docxLimiter);
44
+ await replaceImageBlock(client, documentId, createdBlockId, imageToken, replaceOptions, authOptions, docxLimiter);
38
45
  }
39
46
  return {
40
47
  kind: 'image',
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_IMAGE_WIDTH = 1000;
2
+ export const DEFAULT_TABLE_CELL_IMAGE_WIDTH = 240;
3
+ export function createDefaultImagePayload(parentType) {
4
+ return {
5
+ width: parentType === 'table_cell' ? DEFAULT_TABLE_CELL_IMAGE_WIDTH : DEFAULT_IMAGE_WIDTH,
6
+ token: '',
7
+ align: 'left',
8
+ };
9
+ }
@@ -1,4 +1,5 @@
1
1
  import { toString } from 'hast-util-to-string';
2
+ import { createDefaultImagePayload } from '../last/image-defaults.js';
2
3
  import { LAST_TEXTUAL_BLOCK_TYPE_SET } from '../last/textual-block-types.js';
3
4
  const BLOCK_CONTAINER_TAGS = new Set([
4
5
  'article',
@@ -146,17 +147,13 @@ function createDividerBlock(ctx, parentId) {
146
147
  }
147
148
  function createImageBlock(ctx, parentId, sourceUrl) {
148
149
  const blockId = nextBlockId(ctx);
150
+ const parentBlock = ctx.blocks[parentId];
149
151
  const blockBase = {
150
152
  id: blockId,
151
153
  type: 'image',
152
154
  parentId,
153
155
  children: [],
154
- payload: {
155
- width: 0,
156
- height: 0,
157
- token: '',
158
- align: 'left',
159
- },
156
+ payload: createDefaultImagePayload(parentBlock?.type),
160
157
  };
161
158
  if (sourceUrl) {
162
159
  blockBase.selector = { attrs: { sourceUrl } };
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createDefaultImagePayload } from '../last/image-defaults.js';
3
4
  import { createDefaultMarks, extractTextFromInlines, firstInlineLinkUrl, getPathExtension, inferMediaKind, isTextualBlock, resolveLocalPathFromSource, shouldUsePreviewView, stripQueryAndHash, } from './common.js';
4
5
  export function applyStandaloneAttachmentTransforms(last, baseDir) {
5
6
  const localAssetByBlockId = new Map();
@@ -26,18 +27,14 @@ export function applyStandaloneAttachmentTransforms(last, baseDir) {
26
27
  const displayName = extractTextFromInlines(block.payload.inlines).trim();
27
28
  const fileName = displayName.length > 0 ? displayName : path.basename(stripQueryAndHash(linkUrl));
28
29
  if (mediaKind === 'image') {
30
+ const parentBlock = block.parentId ? last.blocks[block.parentId] : undefined;
29
31
  const transformed = {
30
32
  id: block.id,
31
33
  ...(block.bttId ? { bttId: block.bttId } : {}),
32
34
  type: 'image',
33
35
  parentId: block.parentId,
34
36
  children: [],
35
- payload: {
36
- width: 0,
37
- height: 0,
38
- token: '',
39
- align: 'left',
40
- },
37
+ payload: createDefaultImagePayload(parentBlock?.type),
41
38
  selector: {
42
39
  attrs: {
43
40
  sourceUrl: linkUrl,
@@ -69,6 +69,7 @@ export async function processSingleMarkdownFile(params) {
69
69
  const stagePaths = buildPipelineStagePaths(runtime.pipelineCacheRootDir, markdownPath);
70
70
  const startedAt = new Date().toISOString();
71
71
  const sourceMarkdown = await readFile(markdownPath, 'utf8');
72
+ const resourceBaseDir = runtime.resourceBaseDir ?? path.dirname(markdownPath);
72
73
  let markdown = sourceMarkdown;
73
74
  for (let presetIndex = 0; presetIndex < runtime.markdownPresets.length; presetIndex += 1) {
74
75
  const preset = runtime.markdownPresets[presetIndex];
@@ -82,6 +83,7 @@ export async function processSingleMarkdownFile(params) {
82
83
  }
83
84
  await writeSourceStage(stagePaths, sourceMarkdown, markdown, {
84
85
  sourcePath: path.resolve(markdownPath),
86
+ resourceBaseDir,
85
87
  preset: runtime.markdownPresets.length === 1 ? runtime.markdownPresets[0].displayPath : null,
86
88
  presets: runtime.markdownPresets.map((preset) => preset.displayPath),
87
89
  startedAt,
@@ -106,8 +108,7 @@ export async function processSingleMarkdownFile(params) {
106
108
  });
107
109
  await writeLastStage(stagePaths, last);
108
110
  ensureLastBlockBttIds(last);
109
- const baseDir = path.dirname(markdownPath);
110
- const localAssetByBlockId = applyStandaloneAttachmentTransforms(last, baseDir);
111
+ const localAssetByBlockId = applyStandaloneAttachmentTransforms(last, resourceBaseDir);
111
112
  applyTableColumnWidthHeuristics(last);
112
113
  const mermaidByBlockId = collectMermaidPatches(last);
113
114
  const btt = convertLASTToBTT(last, {
@@ -77,6 +77,7 @@ export function buildPublishRuntime(options, env, markdownPresets) {
77
77
  ? env.LARK_DOCUMENT_BASE_URL.trim()
78
78
  : deriveLarkDocumentBaseUrl(config.baseUrl);
79
79
  const documentBaseUrl = normalizeLarkDocumentBaseUrl(documentBaseUrlCandidate);
80
+ const resourceBaseDir = options.resourceBaseDir?.trim() ? path.resolve(options.resourceBaseDir.trim()) : null;
80
81
  const prepareTimeoutMs = toPositiveInt(Number((env.PREPARE_TIMEOUT_MS ?? '').trim())) ?? 15_000;
81
82
  const prepareMaxRetries = toNonNegativeInt(Number((env.PREPARE_MAX_RETRIES ?? '').trim())) ?? 3;
82
83
  const prepareBackoffBaseMs = toPositiveInt(Number((env.PREPARE_BACKOFF_BASE_MS ?? '').trim())) ?? 500;
@@ -103,6 +104,7 @@ export function buildPublishRuntime(options, env, markdownPresets) {
103
104
  markdownPresets,
104
105
  markdownPreset: markdownPresets[0] ?? null,
105
106
  documentBaseUrl,
107
+ resourceBaseDir,
106
108
  documentUrlFor: (documentId) => buildLarkDocumentUrl(documentBaseUrl, documentId),
107
109
  authOptions,
108
110
  sdkClient,
@@ -137,6 +139,9 @@ export function logPublishRuntimeSummary(runtime, inputCount, inputMode) {
137
139
  ? `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)')}`
138
140
  : 'Mermaid: target=text-drawing');
139
141
  console.error(`Document URL base: ${runtime.documentBaseUrl}`);
142
+ if (runtime.resourceBaseDir) {
143
+ console.error(`Local asset base override: ${runtime.resourceBaseDir}`);
144
+ }
140
145
  if (runtime.markdownPresets.length > 1) {
141
146
  console.error(`Presets: ${runtime.markdownPresets.map((preset) => preset.displayPath).join(' -> ')}`);
142
147
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobbubu/md-to-lark",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
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",