@akairos/typeset-studio 0.2.1 → 0.2.2

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/index.html CHANGED
@@ -11,7 +11,7 @@
11
11
  rel="stylesheet"
12
12
  />
13
13
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-web/style.css" />
14
- <script type="module" crossorigin src="/assets/index-Dpnz-F2g.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-DDeEGeHD.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-CZ352D7X.css">
16
16
  </head>
17
17
  <body class="bg-[#fafafa] dark:bg-[#000000] text-[#171717] dark:text-[#ededed] antialiased selection:bg-[#eaeaea] selection:text-[#000000] dark:selection:bg-[#333333] dark:selection:text-[#ffffff] overflow-hidden">
@@ -67,22 +67,22 @@ function computeStageDimensions(spec, safeArea, contentBottomGap = 20) {
67
67
  function resolveCanvasFontFamily(fontName, isServer = false) {
68
68
  const rawFirst = (fontName || "").split(",")[0] || "";
69
69
  const trimmed = rawFirst.replace(/["'\\]/g, "").trim();
70
- if (trimmed.includes("Noto Sans") || trimmed === "Noto Sans SC") {
70
+ if (trimmed.includes("Noto Sans")) {
71
71
  return isServer ? '"Hiragino Sans GB", sans-serif' : '"Noto Sans SC", "PingFang SC", sans-serif';
72
72
  }
73
- if (trimmed.includes("PingFang") || trimmed === "PingFang SC") {
73
+ if (trimmed.includes("PingFang")) {
74
74
  return isServer ? '"Hiragino Sans GB", sans-serif' : '"PingFang SC", "Noto Sans SC", sans-serif';
75
75
  }
76
- if (trimmed.includes("Noto Serif") || trimmed === "Noto Serif SC") {
76
+ if (trimmed.includes("Noto Serif")) {
77
77
  return isServer ? '"Songti SC", serif' : '"Noto Serif SC", "Songti SC", serif';
78
78
  }
79
- if (trimmed.includes("Songti") || trimmed === "Songti SC" || trimmed === "STSong") {
79
+ if (trimmed.includes("Songti") || trimmed === "STSong") {
80
80
  return isServer ? '"Songti SC", serif' : '"Songti SC", "Noto Serif SC", serif';
81
81
  }
82
82
  if (trimmed.includes("Kai") || trimmed.includes("WenKai")) {
83
83
  return isServer ? '"STKaiti", "Kaiti SC", "Songti SC", serif' : '"LXGW WenKai GB Screen", "LXGW WenKai Screen", "Kaiti SC", "STKaiti", serif';
84
84
  }
85
- if (trimmed.includes("Yuan") || trimmed.includes("Yuanti")) {
85
+ if (trimmed.includes("Yuan")) {
86
86
  return isServer ? '"STYuanti", "Yuanti SC", "Hiragino Sans GB", sans-serif' : '"Yuanti SC", "STYuanti", "PingFang SC", sans-serif';
87
87
  }
88
88
  if (trimmed.includes("Inter")) {
@@ -92,7 +92,7 @@ function resolveCanvasFontFamily(fontName, isServer = false) {
92
92
  return isServer ? 'Georgia, "Songti SC", serif' : 'Georgia, "Songti SC", serif';
93
93
  }
94
94
  if (trimmed.toLowerCase().includes("serif") || trimmed.includes("Song")) {
95
- return isServer ? `"${trimmed}", "Songti SC", serif` : `"${trimmed}", "Songti SC", serif`;
95
+ return `"${trimmed}", "Songti SC", serif`;
96
96
  }
97
97
  return isServer ? `"${trimmed}", "Hiragino Sans GB", "PingFang SC", sans-serif` : `"${trimmed}", "PingFang SC", sans-serif`;
98
98
  }
@@ -114,14 +114,14 @@ async function drawPageToContext(ctx, options) {
114
114
  ctx.fillStyle = theme.bgColor;
115
115
  ctx.fillRect(0, 0, spec.width, spec.height);
116
116
  ctx.restore();
117
- const { stageWidth, stageHeight, stageX, stageY } = computeStageDimensions(spec, algorithm.safeArea, algorithm.contentBottomGap);
117
+ const { stageX, stageY } = computeStageDimensions(spec, algorithm.safeArea, algorithm.contentBottomGap);
118
118
  const isCover = config.aspectRatio === "wechat-cover-2.35";
119
119
  const showHeader = chrome?.showHeader ?? !isCover;
120
120
  const showWatermark = (chrome?.showWatermark ?? !isCover) && Boolean(config.watermarkText?.trim());
121
121
  const showPageNum = (chrome?.showPageNumber ?? (!isCover && totalPageCount > 1)) && totalPageCount > 1;
122
122
  if (showHeader && (showWatermark || showPageNum)) {
123
123
  const headerY = Math.max(24, Math.floor(algorithm.safeArea.top * 0.45));
124
- if (showWatermark && config.watermarkText?.trim()) {
124
+ if (showWatermark) {
125
125
  ctx.save();
126
126
  ctx.font = resolveCanvasFont("600 14px PingFang SC", isServer);
127
127
  ctx.fillStyle = theme.watermarkColor;
@@ -265,7 +265,7 @@ async function drawPageToContext(ctx, options) {
265
265
  ctx.stroke();
266
266
  ctx.restore();
267
267
  }
268
- if (showFooterText && config.footerText?.trim()) {
268
+ if (showFooterText) {
269
269
  ctx.save();
270
270
  ctx.font = resolveCanvasFont("500 13px PingFang SC", isServer);
271
271
  ctx.fillStyle = theme.watermarkColor || theme.captionColor;
@@ -6004,6 +6004,7 @@ function materializeRichInlineLineRange(prepared, line) {
6004
6004
  end: line.end
6005
6005
  };
6006
6006
  }
6007
+
6007
6008
  // src/core/pretext.ts
6008
6009
  var initialized = false;
6009
6010
  function ensurePretextRuntime() {
@@ -6583,21 +6584,21 @@ function runTypeset(params) {
6583
6584
  headingFont: config.typography.headingFont,
6584
6585
  bodyFont: config.typography.bodyFont
6585
6586
  });
6587
+ const createFlowBlocks = (text) => parseContentBlocks(text).map((block) => ({
6588
+ ...blockStyles[block.kind],
6589
+ kind: block.kind,
6590
+ text: block.text,
6591
+ runs: parseInlineRuns(block.text, highlightWords),
6592
+ bullet: block.kind === "list-item" ? block.bullet ?? "•" : undefined,
6593
+ textIndent: block.kind === "list-item" ? 28 : block.kind === "quote" ? 20 : 0,
6594
+ keepWithNext: ["heading", "subheading", "minor-heading"].includes(block.kind)
6595
+ }));
6586
6596
  const pageSegments = splitTextByPages(markdown);
6587
6597
  let pages = [];
6588
6598
  if (pageSegments.length > 1) {
6589
6599
  pages = pageSegments.map((segmentText, index) => {
6590
6600
  const pageNum = index + 1;
6591
- const rawBlocks = parseContentBlocks(segmentText);
6592
- const flowBlocks = rawBlocks.map((b) => ({
6593
- ...blockStyles[b.kind],
6594
- kind: b.kind,
6595
- text: b.text,
6596
- runs: parseInlineRuns(b.text, highlightWords),
6597
- bullet: b.kind === "list-item" ? b.bullet ?? "•" : undefined,
6598
- textIndent: b.kind === "list-item" ? 28 : b.kind === "quote" ? 20 : 0,
6599
- keepWithNext: ["heading", "subheading", "minor-heading"].includes(b.kind)
6600
- }));
6601
+ const flowBlocks = createFlowBlocks(segmentText);
6601
6602
  const pageImages = images.filter((img) => {
6602
6603
  if (img.pageIndex !== undefined && img.pageIndex !== null && img.pageIndex > 0) {
6603
6604
  return img.pageIndex === pageNum;
@@ -6624,16 +6625,7 @@ function runTypeset(params) {
6624
6625
  };
6625
6626
  });
6626
6627
  } else {
6627
- const rawBlocks = parseContentBlocks(markdown);
6628
- const flowBlocks = rawBlocks.map((b) => ({
6629
- ...blockStyles[b.kind],
6630
- kind: b.kind,
6631
- text: b.text,
6632
- runs: parseInlineRuns(b.text, highlightWords),
6633
- bullet: b.kind === "list-item" ? b.bullet ?? "•" : undefined,
6634
- textIndent: b.kind === "list-item" ? 28 : b.kind === "quote" ? 20 : 0,
6635
- keepWithNext: ["heading", "subheading", "minor-heading"].includes(b.kind)
6636
- }));
6628
+ const flowBlocks = createFlowBlocks(markdown);
6637
6629
  pages = paginateBlocks(flowBlocks, stageWidth, stageHeight, images, {
6638
6630
  obstacleGap: config.algorithm.obstacleGap,
6639
6631
  pageImageLimit: config.aspectRatio === "longform-3-4" ? 2 : 1,
@@ -7132,6 +7124,7 @@ function findTemplateByKeyOrName(key) {
7132
7124
  } catch {}
7133
7125
  }
7134
7126
  const files = readdirSync2(TEMPLATES_DIR).filter((f) => f.endsWith(".json"));
7127
+ const templates = [];
7135
7128
  for (const file of files) {
7136
7129
  try {
7137
7130
  const content = readFileSync2(join3(TEMPLATES_DIR, file), "utf-8");
@@ -7139,18 +7132,10 @@ function findTemplateByKeyOrName(key) {
7139
7132
  if (tpl.id?.toLowerCase() === lowerKey || tpl.name.toLowerCase() === lowerKey) {
7140
7133
  return tpl;
7141
7134
  }
7135
+ templates.push(tpl);
7142
7136
  } catch {}
7143
7137
  }
7144
- for (const file of files) {
7145
- try {
7146
- const content = readFileSync2(join3(TEMPLATES_DIR, file), "utf-8");
7147
- const tpl = JSON.parse(content);
7148
- if (tpl.name.toLowerCase().includes(lowerKey)) {
7149
- return tpl;
7150
- }
7151
- } catch {}
7152
- }
7153
- return null;
7138
+ return templates.find((tpl) => tpl.name.toLowerCase().includes(lowerKey)) ?? null;
7154
7139
  }
7155
7140
  var templatesRouter = new Hono3;
7156
7141
  templatesRouter.get("/", (c) => {
@@ -7442,7 +7427,7 @@ function mapBodyImages(bodyImages, pageSpecs) {
7442
7427
  });
7443
7428
  }
7444
7429
  var REMOTE_STUDIO_URL = process.env.TYPESET_STUDIO_URL?.replace(/\/$/, "");
7445
- async function renderRemoteBatch(studioUrl, body, outputDir, filenamePrefix, route, kind) {
7430
+ async function renderRemoteBatch(studioUrl, body, outputDir, route, kind) {
7446
7431
  const res = await fetch(`${studioUrl}/api/render/export`, {
7447
7432
  method: "POST",
7448
7433
  headers: { "Content-Type": "application/json" },
@@ -7454,12 +7439,11 @@ async function renderRemoteBatch(studioUrl, body, outputDir, filenamePrefix, rou
7454
7439
  const data = await res.json();
7455
7440
  const assets = [];
7456
7441
  const pagesSummary = [];
7457
- for (let i = 0;i < data.pages.length; i++) {
7458
- const pageItem = data.pages[i];
7442
+ for (const pageItem of data.pages) {
7459
7443
  const pageNum = pageItem.pageNumber;
7460
7444
  const base64Data = pageItem.dataUrl.replace(/^data:image\/png;base64,/, "");
7461
7445
  const buffer = Buffer.from(base64Data, "base64");
7462
- const filename = kind === "cover" ? "cover.png" : `${filenamePrefix}-${String(pageNum).padStart(2, "0")}.png`;
7446
+ const filename = kind === "cover" ? "cover.png" : `article-${String(pageNum).padStart(2, "0")}.png`;
7463
7447
  const filePath = join4(outputDir, filename);
7464
7448
  await writeFile(filePath, buffer);
7465
7449
  assets.push({
@@ -7476,9 +7460,33 @@ async function renderRemoteBatch(studioUrl, body, outputDir, filenamePrefix, rou
7476
7460
  }
7477
7461
  return { assets, pageCount: data.pageCount, pages: pagesSummary };
7478
7462
  }
7463
+ async function renderLocalBatch(body, outputDir, route, kind) {
7464
+ const { result } = resolveTypesetResult(body);
7465
+ const pngs = await renderTypesetResultToPngs(result);
7466
+ const outputs = kind === "cover" ? pngs.slice(0, 1) : pngs;
7467
+ if (kind === "cover" && outputs.length === 0) {
7468
+ throw new Error("Typeset Studio failed to render cover buffer");
7469
+ }
7470
+ const assets = [];
7471
+ const pages = [];
7472
+ for (const [index, output] of outputs.entries()) {
7473
+ const pageNumber = index + 1;
7474
+ const filename = kind === "cover" ? "cover.png" : `article-${String(pageNumber).padStart(2, "0")}.png`;
7475
+ const path = join4(outputDir, filename);
7476
+ await writeFile(path, output.buffer);
7477
+ assets.push({ kind, route, path, ...kind === "page" ? { index: pageNumber } : {} });
7478
+ const images = result.pages[index].images;
7479
+ pages.push({
7480
+ page: pageNumber,
7481
+ imageCount: images.length,
7482
+ imageSources: images.map((image) => image.src)
7483
+ });
7484
+ }
7485
+ return { assets, pageCount: outputs.length, pages };
7486
+ }
7479
7487
  var typesetStudioAdapter = {
7480
7488
  name: "typeset-studio",
7481
- version: "0.2.1",
7489
+ version: "0.2.2",
7482
7490
  async doctor() {
7483
7491
  const checks = [];
7484
7492
  if (REMOTE_STUDIO_URL) {
@@ -7512,7 +7520,7 @@ var typesetStudioAdapter = {
7512
7520
  } catch (err) {
7513
7521
  checks.push({
7514
7522
  name: "@napi-rs/canvas",
7515
- ok: !REMOTE_STUDIO_URL,
7523
+ ok: Boolean(REMOTE_STUDIO_URL),
7516
7524
  message: REMOTE_STUDIO_URL ? "@napi-rs/canvas not installed locally (using Cloud Typeset Studio microservice)" : `@napi-rs/canvas initialization failed: ${err}`
7517
7525
  });
7518
7526
  }
@@ -7533,201 +7541,41 @@ var typesetStudioAdapter = {
7533
7541
  return checks;
7534
7542
  },
7535
7543
  async render(input) {
7536
- const { route, outputDir, title, bodyText, state } = input;
7544
+ const { route, outputDir, bodyText, state } = input;
7537
7545
  await mkdir(outputDir, { recursive: true });
7546
+ if (route === "blog")
7547
+ return { assets: [], pageCount: 0, pages: [] };
7538
7548
  const account = state.route.account || "default";
7539
- const highlightWords = state.route.highlight_words || [];
7540
- const footerText = input.accountVisualParams?.footer;
7541
- const watermarkText = account ? `@${account}` : undefined;
7542
- const effectiveTitle = cleanTitle(title || state.metadata?.title || "");
7543
- if (REMOTE_STUDIO_URL) {
7544
- if (route === "wechat-article") {
7545
- const coverImages2 = mapBodyImages(input.bodyImages, input.pageSpecs);
7546
- return await renderRemoteBatch(REMOTE_STUDIO_URL, {
7547
- slot: "article_cover",
7548
- route: "wechat-article",
7549
- account,
7550
- title: effectiveTitle,
7551
- markdown: `# ${effectiveTitle}`,
7552
- template: input.template,
7553
- images: coverImages2,
7554
- highlightWords,
7555
- config: { footerText, watermarkText }
7556
- }, outputDir, "cover", "wechat-article", "cover");
7557
- }
7558
- if (route === "wechat-newspic") {
7559
- const newspicSpec2 = state.intent?.newspic_render;
7560
- const paginationMode2 = input.template === "longform-3-4" ? "multi" : input.template === "poster-3-4" ? "single" : newspicSpec2?.pagination_mode || "multi";
7561
- const isMultiPage2 = paginationMode2 === "multi";
7562
- const coverImages2 = mapBodyImages(input.bodyImages, input.pageSpecs);
7563
- const coverRes = await renderRemoteBatch(REMOTE_STUDIO_URL, {
7564
- slot: "newspic_cover",
7565
- route: "wechat-newspic",
7566
- kind: "cover",
7567
- account,
7568
- title: effectiveTitle,
7569
- markdown: `# ${effectiveTitle}`,
7570
- template: input.template,
7571
- images: coverImages2,
7572
- highlightWords,
7573
- config: { footerText, watermarkText }
7574
- }, outputDir, "cover", "wechat-newspic", "cover");
7575
- if (!isMultiPage2) {
7576
- return coverRes;
7577
- }
7578
- const contentImages2 = mapBodyImages(input.bodyImages, input.pageSpecs);
7579
- const contentRes = await renderRemoteBatch(REMOTE_STUDIO_URL, {
7580
- slot: "newspic_content",
7581
- route: "wechat-newspic",
7582
- kind: "content",
7583
- account,
7584
- title: effectiveTitle,
7585
- markdown: bodyText || `# ${effectiveTitle}`,
7586
- template: input.template,
7587
- images: contentImages2,
7588
- highlightWords,
7589
- config: { footerText, watermarkText }
7590
- }, outputDir, "article", "wechat-newspic", "page");
7591
- return {
7592
- assets: [...coverRes.assets, ...contentRes.assets],
7593
- pageCount: contentRes.pageCount,
7594
- pages: contentRes.pages
7595
- };
7596
- }
7597
- }
7598
- if (route === "wechat-article") {
7599
- const coverImages2 = mapBodyImages(input.bodyImages, input.pageSpecs);
7600
- const resolved = resolveTypesetResult({
7601
- slot: "article_cover",
7602
- route: "wechat-article",
7603
- account,
7604
- title: effectiveTitle,
7605
- markdown: `# ${effectiveTitle}`,
7606
- template: input.template,
7607
- images: coverImages2,
7608
- highlightWords,
7609
- config: {
7610
- footerText: footerText ?? undefined,
7611
- watermarkText: watermarkText ?? undefined
7612
- }
7613
- });
7614
- const pngOutputs = await renderTypesetResultToPngs(resolved.result);
7615
- const coverBuffer2 = pngOutputs[0]?.buffer;
7616
- if (!coverBuffer2) {
7617
- throw new Error("Typeset Studio failed to render cover buffer");
7618
- }
7619
- const coverPath2 = join4(outputDir, "cover.png");
7620
- await writeFile(coverPath2, coverBuffer2);
7621
- const asset = {
7622
- kind: "cover",
7623
- route: "wechat-article",
7624
- path: coverPath2
7625
- };
7626
- return {
7627
- assets: [asset],
7628
- pageCount: 1,
7629
- pages: [
7630
- {
7631
- page: 1,
7632
- imageCount: resolved.result.pages[0]?.images.length || 0,
7633
- imageSources: (resolved.result.pages[0]?.images || []).map((img) => img.src)
7634
- }
7635
- ]
7636
- };
7637
- }
7638
- if (route === "blog") {
7639
- return {
7640
- assets: [],
7641
- pageCount: 0,
7642
- pages: []
7643
- };
7644
- }
7645
- const newspicSpec = state.intent?.newspic_render;
7646
- const paginationMode = input.template === "longform-3-4" ? "multi" : input.template === "poster-3-4" ? "single" : newspicSpec?.pagination_mode || "multi";
7647
- const isMultiPage = paginationMode === "multi";
7648
- const assets = [];
7649
- const pagesSummary = [];
7650
- const coverImages = mapBodyImages(input.bodyImages, input.pageSpecs);
7651
- const resolvedCover = resolveTypesetResult({
7652
- slot: "newspic_cover",
7653
- route: "wechat-newspic",
7654
- kind: "cover",
7549
+ const title = cleanTitle(input.title || state.metadata?.title || "");
7550
+ const common = {
7655
7551
  account,
7656
- title: effectiveTitle,
7657
- markdown: `# ${effectiveTitle}`,
7552
+ title,
7658
7553
  template: input.template,
7659
- images: coverImages,
7660
- highlightWords,
7661
- config: {
7662
- footerText: footerText ?? undefined,
7663
- watermarkText: watermarkText ?? undefined
7664
- }
7665
- });
7666
- const coverPngs = await renderTypesetResultToPngs(resolvedCover.result);
7667
- const coverBuffer = coverPngs[0]?.buffer;
7668
- if (!coverBuffer) {
7669
- throw new Error("Typeset Studio failed to render newspic cover buffer");
7670
- }
7671
- const coverPath = join4(outputDir, "cover.png");
7672
- await writeFile(coverPath, coverBuffer);
7673
- assets.push({
7674
- kind: "cover",
7675
- route: "wechat-newspic",
7676
- path: coverPath
7677
- });
7678
- if (!isMultiPage) {
7679
- return {
7680
- assets,
7681
- pageCount: 1,
7682
- pages: [
7683
- {
7684
- page: 1,
7685
- imageCount: resolvedCover.result.pages[0]?.images.length || 0,
7686
- imageSources: (resolvedCover.result.pages[0]?.images || []).map((img) => img.src)
7687
- }
7688
- ]
7689
- };
7690
- }
7691
- const contentImages = mapBodyImages(input.bodyImages, input.pageSpecs);
7692
- const resolvedContent = resolveTypesetResult({
7693
- slot: "newspic_content",
7554
+ images: mapBodyImages(input.bodyImages, input.pageSpecs),
7555
+ highlightWords: state.route.highlight_words || [],
7556
+ config: { footerText: input.accountVisualParams?.footer, watermarkText: `@${account}` }
7557
+ };
7558
+ const articleCover = route === "wechat-article";
7559
+ const renderRoute = articleCover ? "wechat-article" : "wechat-newspic";
7560
+ const renderBatch = (body, kind) => REMOTE_STUDIO_URL && (route === "wechat-article" || route === "wechat-newspic") ? renderRemoteBatch(REMOTE_STUDIO_URL, body, outputDir, renderRoute, kind) : renderLocalBatch(body, outputDir, renderRoute, kind);
7561
+ const cover = await renderBatch({
7562
+ ...common,
7563
+ route: renderRoute,
7564
+ slot: articleCover ? "article_cover" : "newspic_cover",
7565
+ ...articleCover ? {} : { kind: "cover" },
7566
+ markdown: `# ${title}`
7567
+ }, "cover");
7568
+ const paginationMode = input.template === "longform-3-4" ? "multi" : input.template === "poster-3-4" ? "single" : state.intent?.newspic_render?.pagination_mode || "multi";
7569
+ if (articleCover || paginationMode !== "multi")
7570
+ return cover;
7571
+ const content = await renderBatch({
7572
+ ...common,
7694
7573
  route: "wechat-newspic",
7695
7574
  kind: "content",
7696
- account,
7697
- title: effectiveTitle,
7698
- markdown: bodyText || `# ${effectiveTitle}`,
7699
- template: input.template,
7700
- images: contentImages,
7701
- highlightWords,
7702
- config: {
7703
- footerText: footerText ?? undefined,
7704
- watermarkText: watermarkText ?? undefined
7705
- }
7706
- });
7707
- const contentPngs = await renderTypesetResultToPngs(resolvedContent.result);
7708
- for (let i = 0;i < contentPngs.length; i++) {
7709
- const pageNum = i + 1;
7710
- const fileName = `article-${String(pageNum).padStart(2, "0")}.png`;
7711
- const pagePath = join4(outputDir, fileName);
7712
- await writeFile(pagePath, contentPngs[i].buffer);
7713
- assets.push({
7714
- kind: "page",
7715
- route: "wechat-newspic",
7716
- index: pageNum,
7717
- path: pagePath
7718
- });
7719
- const flowPage = resolvedContent.result.pages[i];
7720
- pagesSummary.push({
7721
- page: pageNum,
7722
- imageCount: flowPage?.images.length || 0,
7723
- imageSources: (flowPage?.images || []).map((img) => img.src)
7724
- });
7725
- }
7726
- return {
7727
- assets,
7728
- pageCount: contentPngs.length,
7729
- pages: pagesSummary
7730
- };
7575
+ slot: "newspic_content",
7576
+ markdown: bodyText || `# ${title}`
7577
+ }, "page");
7578
+ return { ...content, assets: [...cover.assets, ...content.assets] };
7731
7579
  }
7732
7580
  };
7733
7581
  var plugin = typesetStudioAdapter;