@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 +1 -0
- package/dist/pipeline/hast-to-last.js +159 -22
- package/dist/publish/process-file.js +9 -0
- package/dist/publish/runtime.js +4 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
469
|
+
if (!only || !isElement(only))
|
|
367
470
|
return null;
|
|
368
|
-
return
|
|
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
|
-
|
|
578
|
+
const imageSource = extractImageSourceFromElement(only);
|
|
579
|
+
if (imageSource) {
|
|
476
580
|
return {
|
|
477
581
|
kind: 'image',
|
|
478
|
-
|
|
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
|
-
|
|
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 [
|
|
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);
|
package/dist/publish/runtime.js
CHANGED
|
@@ -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}`);
|