@jacobbubu/md-to-lark 1.5.1 → 1.6.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,6 +174,7 @@ 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`
177
178
  - Remote image download and standalone URL preparation
178
179
  - Mermaid `text-drawing` and `board` output paths
179
180
  - Opt-in single-dollar inline math parsing for LaTeX-heavy papers
@@ -11,11 +11,14 @@ const BLOCK_CONTAINER_TAGS = new Set([
11
11
  'footer',
12
12
  ]);
13
13
  const DEFAULT_ALIGN = 'left';
14
- function createContext() {
14
+ function createContext(options = {}) {
15
15
  return {
16
16
  blocks: {},
17
17
  blockCounter: 1,
18
18
  inlineCounter: 1,
19
+ imageSizeContext: options.imageSizeContext ?? {},
20
+ warnedImageSizeKeys: new Set(),
21
+ ...(options.imageSizeResolver ? { imageSizeResolver: options.imageSizeResolver } : {}),
19
22
  };
20
23
  }
21
24
  function nextBlockId(ctx) {
@@ -145,9 +148,74 @@ function createDividerBlock(ctx, parentId) {
145
148
  });
146
149
  return blockId;
147
150
  }
148
- function createImageBlock(ctx, parentId, sourceUrl, alt = null) {
151
+ function warnInvalidImageDisplaySize(ctx, key, message) {
152
+ if (ctx.warnedImageSizeKeys.has(key))
153
+ return;
154
+ ctx.warnedImageSizeKeys.add(key);
155
+ console.warn(`[md-to-lark] ${message}`);
156
+ }
157
+ function buildImageSizeResolverContext(ctx, image) {
158
+ const context = {};
159
+ if (ctx.imageSizeContext.inputPath) {
160
+ context.inputPath = ctx.imageSizeContext.inputPath;
161
+ }
162
+ if (ctx.imageSizeContext.resourceBaseDir) {
163
+ context.resourceBaseDir = ctx.imageSizeContext.resourceBaseDir;
164
+ }
165
+ if (image.alt) {
166
+ context.alt = image.alt;
167
+ }
168
+ if (image.title) {
169
+ context.title = image.title;
170
+ }
171
+ return context;
172
+ }
173
+ function toPositiveRoundedWidth(value) {
174
+ if (!Number.isFinite(value) || value <= 0)
175
+ return undefined;
176
+ return Math.max(1, Math.round(value));
177
+ }
178
+ function applyImageDisplaySize(ctx, image, block) {
179
+ if (!ctx.imageSizeResolver || !image.sourceUrl) {
180
+ return;
181
+ }
182
+ const size = ctx.imageSizeResolver(image.sourceUrl, buildImageSizeResolverContext(ctx, image));
183
+ if (!size) {
184
+ return;
185
+ }
186
+ const hasWidthRatio = typeof size.widthRatio === 'number';
187
+ if (hasWidthRatio) {
188
+ const ratio = size.widthRatio;
189
+ if (Number.isFinite(ratio) && ratio > 0 && ratio <= 1) {
190
+ const baseWidth = block.payload.width ??
191
+ createDefaultImagePayload(block.parentId ? ctx.blocks[block.parentId]?.type : undefined).width ??
192
+ 0;
193
+ const width = toPositiveRoundedWidth(baseWidth * ratio);
194
+ if (width !== undefined) {
195
+ block.payload.width = width;
196
+ }
197
+ return;
198
+ }
199
+ warnInvalidImageDisplaySize(ctx, `widthRatio:${image.sourceUrl}:${String(size.widthRatio)}`, `Ignoring invalid image widthRatio for ${image.sourceUrl}: ${String(size.widthRatio)}. Expected 0 < widthRatio <= 1.`);
200
+ }
201
+ if (typeof size.widthPx === 'number') {
202
+ const width = toPositiveRoundedWidth(size.widthPx);
203
+ if (width !== undefined) {
204
+ block.payload.width = width;
205
+ return;
206
+ }
207
+ warnInvalidImageDisplaySize(ctx, `widthPx:${image.sourceUrl}:${String(size.widthPx)}`, `Ignoring invalid image widthPx for ${image.sourceUrl}: ${String(size.widthPx)}. Expected widthPx > 0.`);
208
+ }
209
+ }
210
+ function createImageBlock(ctx, parentId, sourceUrl, alt = null, title = null, linkHref = null) {
149
211
  const blockId = nextBlockId(ctx);
150
212
  const parentBlock = ctx.blocks[parentId];
213
+ const image = {
214
+ sourceUrl,
215
+ alt,
216
+ title,
217
+ linkHref,
218
+ };
151
219
  const blockBase = {
152
220
  id: blockId,
153
221
  type: 'image',
@@ -162,9 +230,16 @@ function createImageBlock(ctx, parentId, sourceUrl, alt = null) {
162
230
  if (alt) {
163
231
  selectorAttrs.alt = alt;
164
232
  }
233
+ if (title) {
234
+ selectorAttrs.title = title;
235
+ }
236
+ if (linkHref) {
237
+ selectorAttrs.linkHref = linkHref;
238
+ }
165
239
  if (Object.keys(selectorAttrs).length > 0) {
166
240
  blockBase.selector = { attrs: selectorAttrs };
167
241
  }
242
+ applyImageDisplaySize(ctx, image, blockBase);
168
243
  addBlock(ctx, blockBase);
169
244
  return blockId;
170
245
  }
@@ -370,17 +445,21 @@ function extractImageSourceFromElement(element) {
370
445
  return {
371
446
  sourceUrl: getStringProp(element, 'src'),
372
447
  alt: getStringProp(element, 'alt'),
448
+ title: getStringProp(element, 'title'),
449
+ linkHref: null,
373
450
  };
374
451
  }
375
452
  if (element.tagName !== 'a')
376
453
  return null;
454
+ const href = getStringProp(element, 'href');
377
455
  const meaningfulChildren = getMeaningfulChildren(getChildren(element));
378
456
  if (meaningfulChildren.length !== 1)
379
457
  return null;
380
458
  const only = meaningfulChildren[0];
381
459
  if (!only || !isElement(only))
382
460
  return null;
383
- return extractImageSourceFromElement(only);
461
+ const imageSource = extractImageSourceFromElement(only);
462
+ return imageSource ? { ...imageSource, linkHref: href } : null;
384
463
  }
385
464
  function findStandaloneImageInParagraph(paragraph) {
386
465
  const meaningfulChildren = getMeaningfulChildren(getChildren(paragraph));
@@ -692,7 +771,7 @@ function convertTable(ctx, table, parentId) {
692
771
  const cellBlock = ctx.blocks[cellId];
693
772
  const richItem = cell ? findStandaloneRichItemInTableCell(cell) : null;
694
773
  if (cellBlock?.type === 'table_cell' && richItem?.kind === 'image') {
695
- const imageId = createImageBlock(ctx, cellId, richItem.sourceUrl, richItem.alt);
774
+ const imageId = createImageBlock(ctx, cellId, richItem.sourceUrl, richItem.alt, richItem.title, richItem.linkHref);
696
775
  cellBlock.children = [imageId];
697
776
  cells.push(cellId);
698
777
  continue;
@@ -764,7 +843,9 @@ function hasNonWhitespaceInline(inlines) {
764
843
  function convertParagraph(ctx, paragraph, parentId) {
765
844
  const standaloneImage = findStandaloneImageInParagraph(paragraph);
766
845
  if (standaloneImage?.sourceUrl) {
767
- return [createImageBlock(ctx, parentId, standaloneImage.sourceUrl, standaloneImage.alt)];
846
+ return [
847
+ createImageBlock(ctx, parentId, standaloneImage.sourceUrl, standaloneImage.alt, standaloneImage.title, standaloneImage.linkHref),
848
+ ];
768
849
  }
769
850
  const standaloneIframe = findStandaloneIframePayloadInParagraph(paragraph);
770
851
  if (standaloneIframe) {
@@ -785,7 +866,7 @@ function convertParagraph(ctx, paragraph, parentId) {
785
866
  const image = isElement(child) ? extractImageSourceFromElement(child) : null;
786
867
  if (image?.sourceUrl) {
787
868
  flushText();
788
- ids.push(createImageBlock(ctx, parentId, image.sourceUrl, image.alt));
869
+ ids.push(createImageBlock(ctx, parentId, image.sourceUrl, image.alt, image.title, image.linkHref));
789
870
  continue;
790
871
  }
791
872
  pendingInlineNodes.push(child);
@@ -843,7 +924,9 @@ function convertBlock(ctx, node, parentId) {
843
924
  case 'table':
844
925
  return convertTable(ctx, node, parentId);
845
926
  case 'img':
846
- return [createImageBlock(ctx, parentId, getStringProp(node, 'src'), getStringProp(node, 'alt'))];
927
+ return [
928
+ createImageBlock(ctx, parentId, getStringProp(node, 'src'), getStringProp(node, 'alt'), getStringProp(node, 'title')),
929
+ ];
847
930
  case 'br':
848
931
  return [
849
932
  createTextualBlock(ctx, 'text', parentId, [
@@ -956,7 +1039,7 @@ function deepCloneBlock(value) {
956
1039
  return JSON.parse(JSON.stringify(value));
957
1040
  }
958
1041
  export function hastToLAST(hast, options) {
959
- const ctx = createContext();
1042
+ const ctx = createContext(options);
960
1043
  const mode = options?.mode ?? 'fragment';
961
1044
  const rootId = createTextualBlock(ctx, 'page', null, []);
962
1045
  const root = ctx.blocks[rootId];
@@ -105,6 +105,15 @@ export async function processSingleMarkdownFile(params) {
105
105
  const last = hastToLAST(hast, {
106
106
  documentId: documentKey,
107
107
  mode: 'fragment',
108
+ ...(runtime.imageSizeResolver
109
+ ? {
110
+ imageSizeResolver: runtime.imageSizeResolver,
111
+ imageSizeContext: {
112
+ inputPath: markdownPath,
113
+ resourceBaseDir,
114
+ },
115
+ }
116
+ : {}),
108
117
  });
109
118
  await writeLastStage(stagePaths, last);
110
119
  ensureLastBlockBttIds(last);
@@ -122,6 +122,7 @@ export function buildPublishRuntime(options, env, markdownPresets) {
122
122
  ...(ytDlpPath ? { ytDlpPath } : {}),
123
123
  mermaidRenderConfig,
124
124
  markdownParseConfig,
125
+ ...(options.imageSizeResolver ? { imageSizeResolver: options.imageSizeResolver } : {}),
125
126
  prepareConfig: {
126
127
  enabled: downloadRemoteImages,
127
128
  timeoutMs: prepareTimeoutMs,
@@ -145,6 +146,9 @@ export function logPublishRuntimeSummary(runtime, inputCount, inputMode) {
145
146
  if (runtime.markdownParseConfig.singleDollarTextMath) {
146
147
  console.error('Markdown parse: single_dollar_text_math=true');
147
148
  }
149
+ if (runtime.imageSizeResolver) {
150
+ console.error('Image display size resolver: enabled');
151
+ }
148
152
  console.error(`Document URL base: ${runtime.documentBaseUrl}`);
149
153
  if (runtime.resourceBaseDir) {
150
154
  console.error(`Local asset base override: ${runtime.resourceBaseDir}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobbubu/md-to-lark",
3
- "version": "1.5.1",
3
+ "version": "1.6.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",