@jacobbubu/md-to-lark 1.5.0 → 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) {
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',
@@ -155,9 +223,23 @@ function createImageBlock(ctx, parentId, sourceUrl) {
155
223
  children: [],
156
224
  payload: createDefaultImagePayload(parentBlock?.type),
157
225
  };
226
+ const selectorAttrs = {};
158
227
  if (sourceUrl) {
159
- blockBase.selector = { attrs: { sourceUrl } };
228
+ selectorAttrs.sourceUrl = sourceUrl;
229
+ }
230
+ if (alt) {
231
+ selectorAttrs.alt = alt;
232
+ }
233
+ if (title) {
234
+ selectorAttrs.title = title;
160
235
  }
236
+ if (linkHref) {
237
+ selectorAttrs.linkHref = linkHref;
238
+ }
239
+ if (Object.keys(selectorAttrs).length > 0) {
240
+ blockBase.selector = { attrs: selectorAttrs };
241
+ }
242
+ applyImageDisplaySize(ctx, image, blockBase);
161
243
  addBlock(ctx, blockBase);
162
244
  return blockId;
163
245
  }
@@ -358,14 +440,35 @@ function isWhitespaceTextNode(node) {
358
440
  function getMeaningfulChildren(nodes) {
359
441
  return nodes.filter((child) => !isWhitespaceTextNode(child));
360
442
  }
361
- function findStandaloneImageSrcInParagraph(paragraph) {
443
+ function extractImageSourceFromElement(element) {
444
+ if (element.tagName === 'img') {
445
+ return {
446
+ sourceUrl: getStringProp(element, 'src'),
447
+ alt: getStringProp(element, 'alt'),
448
+ title: getStringProp(element, 'title'),
449
+ linkHref: null,
450
+ };
451
+ }
452
+ if (element.tagName !== 'a')
453
+ return null;
454
+ const href = getStringProp(element, 'href');
455
+ const meaningfulChildren = getMeaningfulChildren(getChildren(element));
456
+ if (meaningfulChildren.length !== 1)
457
+ return null;
458
+ const only = meaningfulChildren[0];
459
+ if (!only || !isElement(only))
460
+ return null;
461
+ const imageSource = extractImageSourceFromElement(only);
462
+ return imageSource ? { ...imageSource, linkHref: href } : null;
463
+ }
464
+ function findStandaloneImageInParagraph(paragraph) {
362
465
  const meaningfulChildren = getMeaningfulChildren(getChildren(paragraph));
363
466
  if (meaningfulChildren.length !== 1)
364
467
  return null;
365
468
  const only = meaningfulChildren[0];
366
- if (!only || !isElement(only) || only.tagName !== 'img')
469
+ if (!only || !isElement(only))
367
470
  return null;
368
- return getStringProp(only, 'src');
471
+ return extractImageSourceFromElement(only);
369
472
  }
370
473
  function parseHttpUrl(url) {
371
474
  try {
@@ -472,10 +575,11 @@ function findStandaloneRichItemInTableCell(cell) {
472
575
  }
473
576
  if (!only || !isElement(only))
474
577
  return null;
475
- if (only.tagName === 'img') {
578
+ const imageSource = extractImageSourceFromElement(only);
579
+ if (imageSource) {
476
580
  return {
477
581
  kind: 'image',
478
- sourceUrl: getStringProp(only, 'src'),
582
+ ...imageSource,
479
583
  };
480
584
  }
481
585
  if (only.tagName !== 'a')
@@ -667,7 +771,7 @@ function convertTable(ctx, table, parentId) {
667
771
  const cellBlock = ctx.blocks[cellId];
668
772
  const richItem = cell ? findStandaloneRichItemInTableCell(cell) : null;
669
773
  if (cellBlock?.type === 'table_cell' && richItem?.kind === 'image') {
670
- const imageId = createImageBlock(ctx, cellId, richItem.sourceUrl);
774
+ const imageId = createImageBlock(ctx, cellId, richItem.sourceUrl, richItem.alt, richItem.title, richItem.linkHref);
671
775
  cellBlock.children = [imageId];
672
776
  cells.push(cellId);
673
777
  continue;
@@ -733,6 +837,46 @@ function convertUnknownElement(ctx, element, parentId) {
733
837
  ]),
734
838
  ];
735
839
  }
840
+ function hasNonWhitespaceInline(inlines) {
841
+ return inlines.some((inline) => toSearchText(inline).text.trim().length > 0);
842
+ }
843
+ function convertParagraph(ctx, paragraph, parentId) {
844
+ const standaloneImage = findStandaloneImageInParagraph(paragraph);
845
+ if (standaloneImage?.sourceUrl) {
846
+ return [
847
+ createImageBlock(ctx, parentId, standaloneImage.sourceUrl, standaloneImage.alt, standaloneImage.title, standaloneImage.linkHref),
848
+ ];
849
+ }
850
+ const standaloneIframe = findStandaloneIframePayloadInParagraph(paragraph);
851
+ if (standaloneIframe) {
852
+ return [createIframeBlock(ctx, parentId, standaloneIframe.url, standaloneIframe.iframeType)];
853
+ }
854
+ const ids = [];
855
+ let pendingInlineNodes = [];
856
+ const flushText = () => {
857
+ if (pendingInlineNodes.length === 0)
858
+ return;
859
+ const inlines = parseInlineNodes(ctx, pendingInlineNodes);
860
+ pendingInlineNodes = [];
861
+ if (!hasNonWhitespaceInline(inlines))
862
+ return;
863
+ ids.push(createTextualBlock(ctx, 'text', parentId, inlines));
864
+ };
865
+ for (const child of getChildren(paragraph)) {
866
+ const image = isElement(child) ? extractImageSourceFromElement(child) : null;
867
+ if (image?.sourceUrl) {
868
+ flushText();
869
+ ids.push(createImageBlock(ctx, parentId, image.sourceUrl, image.alt, image.title, image.linkHref));
870
+ continue;
871
+ }
872
+ pendingInlineNodes.push(child);
873
+ }
874
+ flushText();
875
+ if (ids.length > 0) {
876
+ return ids;
877
+ }
878
+ return [createTextualBlock(ctx, 'text', parentId, parseInlineNodes(ctx, getChildren(paragraph)))];
879
+ }
736
880
  function convertBlock(ctx, node, parentId) {
737
881
  if (isWhitespaceTextNode(node)) {
738
882
  return [];
@@ -753,17 +897,8 @@ function convertBlock(ctx, node, parentId) {
753
897
  return [];
754
898
  }
755
899
  switch (node.tagName) {
756
- case 'p': {
757
- const standaloneImageSrc = findStandaloneImageSrcInParagraph(node);
758
- if (standaloneImageSrc) {
759
- return [createImageBlock(ctx, parentId, standaloneImageSrc)];
760
- }
761
- const standaloneIframe = findStandaloneIframePayloadInParagraph(node);
762
- if (standaloneIframe) {
763
- return [createIframeBlock(ctx, parentId, standaloneIframe.url, standaloneIframe.iframeType)];
764
- }
765
- return [createTextualBlock(ctx, 'text', parentId, parseInlineNodes(ctx, getChildren(node)))];
766
- }
900
+ case 'p':
901
+ return convertParagraph(ctx, node, parentId);
767
902
  case 'h1':
768
903
  case 'h2':
769
904
  case 'h3':
@@ -789,7 +924,9 @@ function convertBlock(ctx, node, parentId) {
789
924
  case 'table':
790
925
  return convertTable(ctx, node, parentId);
791
926
  case 'img':
792
- return [createImageBlock(ctx, parentId, getStringProp(node, 'src'))];
927
+ return [
928
+ createImageBlock(ctx, parentId, getStringProp(node, 'src'), getStringProp(node, 'alt'), getStringProp(node, 'title')),
929
+ ];
793
930
  case 'br':
794
931
  return [
795
932
  createTextualBlock(ctx, 'text', parentId, [
@@ -902,7 +1039,7 @@ function deepCloneBlock(value) {
902
1039
  return JSON.parse(JSON.stringify(value));
903
1040
  }
904
1041
  export function hastToLAST(hast, options) {
905
- const ctx = createContext();
1042
+ const ctx = createContext(options);
906
1043
  const mode = options?.mode ?? 'fragment';
907
1044
  const rootId = createTextualBlock(ctx, 'page', null, []);
908
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.0",
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",