@liustack/mdpress 0.1.0 → 0.2.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/dist/clipboard-pLkenrrh.js +7 -0
- package/dist/main.js +75 -43
- package/package.json +2 -1
package/dist/main.js
CHANGED
|
@@ -294,6 +294,8 @@ const rehypeSanitizeTags = () => {
|
|
|
294
294
|
};
|
|
295
295
|
};
|
|
296
296
|
const MAX_SIZE = 2 * 1024 * 1024;
|
|
297
|
+
const MIN_FILE_SIZE = 1 * 1024;
|
|
298
|
+
const MIN_DIMENSION = 120;
|
|
297
299
|
function classifySource(src) {
|
|
298
300
|
if (src.startsWith("data:")) return "data-uri";
|
|
299
301
|
if (src.startsWith("http://") || src.startsWith("https://")) return "remote";
|
|
@@ -314,17 +316,31 @@ function detectFormat(ext) {
|
|
|
314
316
|
return map[ext] || "image/png";
|
|
315
317
|
}
|
|
316
318
|
async function compressToDataUri(buffer, mime, maxSize, sourcePath) {
|
|
319
|
+
if (buffer.length < MIN_FILE_SIZE) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Image file too small: ${sourcePath} (${buffer.length} bytes, minimum ${MIN_FILE_SIZE} bytes)`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const meta = await sharp(buffer).metadata();
|
|
325
|
+
const width = meta.width ?? 0;
|
|
326
|
+
const height = meta.height ?? 0;
|
|
327
|
+
if (width < MIN_DIMENSION || height < MIN_DIMENSION) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Image too small: ${sourcePath} (${width}x${height}px, minimum ${MIN_DIMENSION}x${MIN_DIMENSION}px)`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
317
332
|
if (mime === "image/svg+xml") {
|
|
318
|
-
|
|
333
|
+
const pngBuffer = await sharp(buffer).png().toBuffer();
|
|
334
|
+
return `data:image/png;base64,${pngBuffer.toString("base64")}`;
|
|
319
335
|
}
|
|
320
336
|
if (mime === "image/gif") {
|
|
321
337
|
let result2 = buffer;
|
|
322
338
|
if (result2.length > maxSize) {
|
|
323
|
-
let
|
|
324
|
-
while (
|
|
325
|
-
result2 = await sharp(buffer, { animated: true }).resize({ width:
|
|
339
|
+
let gifWidth = 1080;
|
|
340
|
+
while (gifWidth >= 100) {
|
|
341
|
+
result2 = await sharp(buffer, { animated: true }).resize({ width: gifWidth, withoutEnlargement: true }).gif().toBuffer();
|
|
326
342
|
if (result2.length <= maxSize) break;
|
|
327
|
-
|
|
343
|
+
gifWidth = Math.floor(gifWidth * 0.7);
|
|
328
344
|
}
|
|
329
345
|
}
|
|
330
346
|
if (result2.length > maxSize) {
|
|
@@ -334,14 +350,13 @@ async function compressToDataUri(buffer, mime, maxSize, sourcePath) {
|
|
|
334
350
|
}
|
|
335
351
|
return `data:image/gif;base64,${result2.toString("base64")}`;
|
|
336
352
|
}
|
|
337
|
-
const
|
|
338
|
-
const width = metadata.width || 1920;
|
|
353
|
+
const origWidth = width || 1920;
|
|
339
354
|
let result;
|
|
340
355
|
result = await sharp(buffer).png({ compressionLevel: 6 }).toBuffer();
|
|
341
356
|
if (result.length <= maxSize) {
|
|
342
357
|
return `data:image/png;base64,${result.toString("base64")}`;
|
|
343
358
|
}
|
|
344
|
-
let currentWidth =
|
|
359
|
+
let currentWidth = origWidth;
|
|
345
360
|
while (currentWidth >= 100) {
|
|
346
361
|
currentWidth = Math.floor(currentWidth * 0.8);
|
|
347
362
|
result = await sharp(buffer).resize({ width: currentWidth, withoutEnlargement: true }).png({ compressionLevel: 6 }).toBuffer();
|
|
@@ -349,7 +364,7 @@ async function compressToDataUri(buffer, mime, maxSize, sourcePath) {
|
|
|
349
364
|
return `data:image/png;base64,${result.toString("base64")}`;
|
|
350
365
|
}
|
|
351
366
|
}
|
|
352
|
-
for (let jpegWidth =
|
|
367
|
+
for (let jpegWidth = origWidth; jpegWidth >= 100; jpegWidth = Math.floor(jpegWidth * 0.8)) {
|
|
353
368
|
for (const quality of [85, 70, 50]) {
|
|
354
369
|
result = await sharp(buffer).flatten({ background: { r: 255, g: 255, b: 255 } }).resize({ width: jpegWidth, withoutEnlargement: true }).jpeg({ quality }).toBuffer();
|
|
355
370
|
if (result.length <= maxSize) {
|
|
@@ -405,7 +420,7 @@ function protectWhitespace(children) {
|
|
|
405
420
|
}
|
|
406
421
|
let text = parts[i];
|
|
407
422
|
text = text.replace(/\t/g, " ");
|
|
408
|
-
text = text.replace(
|
|
423
|
+
text = text.replace(/ /g, " ");
|
|
409
424
|
if (text) {
|
|
410
425
|
result.push({ type: "text", value: text });
|
|
411
426
|
}
|
|
@@ -435,22 +450,22 @@ const rehypeCodeHighlight = () => {
|
|
|
435
450
|
});
|
|
436
451
|
};
|
|
437
452
|
};
|
|
438
|
-
function
|
|
453
|
+
function classifyLink(href) {
|
|
439
454
|
if (href.startsWith("//")) {
|
|
440
|
-
return
|
|
455
|
+
return "external";
|
|
441
456
|
}
|
|
442
457
|
if (href.startsWith("#") || href.startsWith("/") || href.startsWith("./") || href.startsWith("../")) {
|
|
443
|
-
return
|
|
458
|
+
return "anchor";
|
|
444
459
|
}
|
|
445
460
|
try {
|
|
446
461
|
const url = new URL(href);
|
|
447
462
|
if (url.hostname === "mp.weixin.qq.com" || url.hostname.endsWith(".mp.weixin.qq.com")) {
|
|
448
|
-
return
|
|
463
|
+
return "wechat";
|
|
449
464
|
}
|
|
450
465
|
} catch {
|
|
451
|
-
return
|
|
466
|
+
return "anchor";
|
|
452
467
|
}
|
|
453
|
-
return
|
|
468
|
+
return "external";
|
|
454
469
|
}
|
|
455
470
|
const rehypeFootnoteLinks = () => {
|
|
456
471
|
return (tree) => {
|
|
@@ -461,7 +476,12 @@ const rehypeFootnoteLinks = () => {
|
|
|
461
476
|
if (node.tagName !== "a" || index === void 0 || !parent) return;
|
|
462
477
|
const href = node.properties?.href;
|
|
463
478
|
if (typeof href !== "string") return;
|
|
464
|
-
|
|
479
|
+
const linkType = classifyLink(href);
|
|
480
|
+
if (linkType === "wechat") return;
|
|
481
|
+
if (linkType === "anchor") {
|
|
482
|
+
parent.children.splice(index, 1, ...node.children);
|
|
483
|
+
return [SKIP, index + node.children.length];
|
|
484
|
+
}
|
|
465
485
|
let num = urlMap.get(href);
|
|
466
486
|
if (num === void 0) {
|
|
467
487
|
counter++;
|
|
@@ -529,35 +549,39 @@ function extractText(node) {
|
|
|
529
549
|
}
|
|
530
550
|
return text;
|
|
531
551
|
}
|
|
552
|
+
const FONT_BODY = '"mp-quote",PingFang SC,system-ui,-apple-system,BlinkMacSystemFont,Helvetica Neue,Hiragino Sans GB,Microsoft YaHei UI,Microsoft YaHei,Arial,sans-serif';
|
|
553
|
+
const FONT_MONO = 'Menlo,Consolas,Monaco,"Courier New",monospace';
|
|
554
|
+
const F = `font-family: ${FONT_BODY};`;
|
|
555
|
+
const FM = `font-family: ${FONT_MONO};`;
|
|
532
556
|
const defaultStyles = {
|
|
533
557
|
// Headings — Apple style: 600 weight, tight letter-spacing
|
|
534
|
-
h1:
|
|
535
|
-
h2:
|
|
536
|
-
h3:
|
|
537
|
-
h4:
|
|
538
|
-
h5:
|
|
539
|
-
h6:
|
|
540
|
-
// Paragraphs —
|
|
541
|
-
p:
|
|
558
|
+
h1: `${F} font-size: 24px; font-weight: 600; margin: 0 0 16px; line-height: 1.2; letter-spacing: -0.02em; color: #1d1d1f;`,
|
|
559
|
+
h2: `${F} font-size: 20px; font-weight: 600; margin: 32px 0 12px; line-height: 1.25; color: #1d1d1f;`,
|
|
560
|
+
h3: `${F} font-size: 17px; font-weight: 600; margin: 24px 0 8px; line-height: 1.3; color: #1d1d1f;`,
|
|
561
|
+
h4: `${F} font-size: 15px; font-weight: 600; margin: 20px 0 6px; line-height: 1.4; color: #6e6e73;`,
|
|
562
|
+
h5: `${F} font-size: 14px; font-weight: 600; margin: 16px 0 4px; line-height: 1.4; color: #6e6e73;`,
|
|
563
|
+
h6: `${F} font-size: 13px; font-weight: 600; margin: 16px 0 4px; line-height: 1.4; color: #86868b;`,
|
|
564
|
+
// Paragraphs — 16px body, 1.75 line-height (comfortable for Chinese)
|
|
565
|
+
p: `${F} font-size: 16px; line-height: 1.75; margin: 0 0 1.25em; color: #1d1d1f;`,
|
|
542
566
|
// Blockquote — restrained: left border + italic only, no background
|
|
543
|
-
blockquote:
|
|
544
|
-
// Inline code — subtle gray background
|
|
545
|
-
code:
|
|
546
|
-
// Code block — GitHub-style light gray bg, no border
|
|
547
|
-
"pre code":
|
|
567
|
+
blockquote: `${F} margin: 1.5em 0; padding: 0 0 0 1em; border-left: 3px solid #d2d2d7; color: #6e6e73; font-style: italic;`,
|
|
568
|
+
// Inline code — subtle gray background, monospace
|
|
569
|
+
code: `${FM} font-size: 0.875em; background: #f5f5f7; color: #1d1d1f; padding: 0.15em 0.4em; border-radius: 4px;`,
|
|
570
|
+
// Code block — GitHub-style light gray bg, no border, monospace
|
|
571
|
+
"pre code": `${FM} display: block; font-size: 13px; background: #f6f8fa; color: #1d1d1f; padding: 20px 24px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; line-height: 1.6; text-align: left;`,
|
|
548
572
|
pre: "margin: 1.5em 0;",
|
|
549
573
|
// Lists
|
|
550
|
-
ul:
|
|
551
|
-
ol:
|
|
552
|
-
li:
|
|
553
|
-
// Table — clean: bottom borders only, no vertical lines
|
|
554
|
-
table: "width: 100%; margin: 1.5em 0; font-size: 14px; border-collapse: collapse;",
|
|
555
|
-
th: "padding: 12px 16px; text-align: left; font-weight: 600; color: #1d1d1f; font-size: 14px; border-bottom: 1px solid #d2d2d7;",
|
|
556
|
-
td: "padding: 12px 16px; border-bottom: 1px solid #e5e5ea; color: #1d1d1f;",
|
|
574
|
+
ul: `${F} margin: 1em 0; padding-left: 1.5em; font-size: 16px; line-height: 1.75; color: #1d1d1f;`,
|
|
575
|
+
ol: `${F} margin: 1em 0; padding-left: 1.5em; font-size: 16px; line-height: 1.75; color: #1d1d1f;`,
|
|
576
|
+
li: `${F} margin: 0.4em 0; color: #1d1d1f;`,
|
|
557
577
|
// Links — WeChat blue
|
|
558
578
|
a: "color: #576b95; text-decoration: none;",
|
|
559
579
|
// Images
|
|
560
|
-
img: "max-width: 100%; height: auto; display: block; margin: 1.5em auto; border-radius:
|
|
580
|
+
img: "max-width: 100%; height: auto; display: block; margin: 1.5em auto; border-radius: 10px;",
|
|
581
|
+
// Tables — minimalist: collapse borders, thin lines
|
|
582
|
+
table: `${F} width: 100%; border-collapse: collapse; margin: 1.5em 0; font-size: 16px; border-radius: 2px;`,
|
|
583
|
+
th: `${F} padding: 10px 12px; border-bottom: 1px solid #d2d2d7; font-weight: 600; text-align: left; color: #1d1d1f;`,
|
|
584
|
+
td: `${F} padding: 10px 12px; border-bottom: 1px solid #e5e5ea; color: #1d1d1f;`,
|
|
561
585
|
// Horizontal rule
|
|
562
586
|
hr: "border: none; height: 1px; background: #d2d2d7; margin: 2em 0;",
|
|
563
587
|
// Inline formatting
|
|
@@ -569,8 +593,8 @@ const defaultStyles = {
|
|
|
569
593
|
sup: "font-size: 0.75em; vertical-align: super; color: #576b95;",
|
|
570
594
|
sub: "font-size: 0.75em; vertical-align: sub;",
|
|
571
595
|
// Section / figure
|
|
572
|
-
section:
|
|
573
|
-
figcaption:
|
|
596
|
+
section: `${F} margin: 0.5em 0;`,
|
|
597
|
+
figcaption: `${F} font-size: 13px; color: #86868b; text-align: center; margin-top: 8px;`,
|
|
574
598
|
figure: "margin: 1.5em 0; text-align: center;",
|
|
575
599
|
// Mark
|
|
576
600
|
mark: "background: #fff3b0; padding: 0.1em 0.3em; border-radius: 2px;",
|
|
@@ -867,18 +891,26 @@ async function render(options) {
|
|
|
867
891
|
const outputPath = path.resolve(output);
|
|
868
892
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
869
893
|
fs.writeFileSync(outputPath, html, "utf-8");
|
|
894
|
+
let copied = false;
|
|
895
|
+
if (options.copy) {
|
|
896
|
+
const { copyToClipboard } = await import("./clipboard-pLkenrrh.js");
|
|
897
|
+
await copyToClipboard(html);
|
|
898
|
+
copied = true;
|
|
899
|
+
}
|
|
870
900
|
return {
|
|
871
901
|
input: inputPath,
|
|
872
902
|
output: outputPath,
|
|
873
|
-
size: Buffer.byteLength(html, "utf-8")
|
|
903
|
+
size: Buffer.byteLength(html, "utf-8"),
|
|
904
|
+
...copied ? { copied } : {}
|
|
874
905
|
};
|
|
875
906
|
}
|
|
876
907
|
const program = new Command();
|
|
877
|
-
program.name("mdpress").description("Convert Markdown into WeChat MP-ready HTML").version("0.
|
|
908
|
+
program.name("mdpress").description("Convert Markdown into WeChat MP-ready HTML").version("0.2.0").requiredOption("-i, --input <path>", "Input Markdown file path").requiredOption("-o, --output <path>", "Output HTML file path").option("-c, --copy", "Copy rendered HTML to clipboard via Playwright").action(async (options) => {
|
|
878
909
|
try {
|
|
879
910
|
const result = await render({
|
|
880
911
|
input: options.input,
|
|
881
|
-
output: options.output
|
|
912
|
+
output: options.output,
|
|
913
|
+
copy: options.copy
|
|
882
914
|
});
|
|
883
915
|
console.log(JSON.stringify(result, null, 2));
|
|
884
916
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liustack/mdpress",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI for AI agents to convert Markdown into WeChat MP-ready HTML with inline styles, base64 images, and tag sanitization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"node": ">=18"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@crosscopy/clipboard": "^0.3.6",
|
|
35
36
|
"@types/hast": "^3.0.4",
|
|
36
37
|
"commander": "^13.1.0",
|
|
37
38
|
"rehype-highlight": "^7.0.2",
|