@cyber-dash-tech/revela 0.5.0 → 0.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
@@ -114,7 +114,7 @@ Before the agent writes `decks/humanoid-robotics.html`, it must update `DECKS.js
114
114
  /revela review humanoid-robotics
115
115
  ```
116
116
 
117
- Export when needed:
117
+ Export when needed, either manually or by asking the agent to export:
118
118
 
119
119
  ```text
120
120
  /revela pdf decks/humanoid-robotics.html
@@ -600,13 +600,17 @@ PDF export:
600
600
  /revela pdf decks/my-deck.html
601
601
  ```
602
602
 
603
+ LLM tool equivalent: `revela-pdf` with `{ "file": "decks/my-deck.html" }`.
604
+
603
605
  Editable PPTX export:
604
606
 
605
607
  ```text
606
608
  /revela pptx decks/my-deck.html
607
609
  ```
608
610
 
609
- Both commands write output beside the source HTML deck.
611
+ LLM tool equivalent: `revela-pptx` with `{ "file": "decks/my-deck.html" }`.
612
+
613
+ Both commands and tools write output beside the source HTML deck. Use the tools when you want the agent to run export as part of the deck workflow instead of asking the user to invoke `/revela pdf` or `/revela pptx` manually.
610
614
 
611
615
  ---
612
616
 
package/README.zh-CN.md CHANGED
@@ -113,7 +113,7 @@ Create a 6-slide HTML deck on humanoid robotics supply chains. Cite the main mar
113
113
  /revela review humanoid-robotics
114
114
  ```
115
115
 
116
- 需要导出时:
116
+ 需要导出时,可以手动调用,也可以让 agent 直接导出:
117
117
 
118
118
  ```text
119
119
  /revela pdf decks/humanoid-robotics.html
@@ -565,13 +565,17 @@ PDF 导出:
565
565
  /revela pdf decks/my-deck.html
566
566
  ```
567
567
 
568
+ 对应的 LLM tool:`revela-pdf`,参数为 `{ "file": "decks/my-deck.html" }`。
569
+
568
570
  可编辑 PPTX 导出:
569
571
 
570
572
  ```text
571
573
  /revela pptx decks/my-deck.html
572
574
  ```
573
575
 
574
- 两种导出都会把结果写到源 HTML deck 同目录。
576
+ 对应的 LLM tool:`revela-pptx`,参数为 `{ "file": "decks/my-deck.html" }`。
577
+
578
+ 命令和 tool 都会把结果写到源 HTML deck 同目录。如果希望 agent 在 deck 工作流中自动完成导出,可以让它调用 tool,而不需要用户手动执行 `/revela pdf` 或 `/revela pptx`。
575
579
 
576
580
  ---
577
581
 
package/lib/pdf/export.ts CHANGED
@@ -57,6 +57,18 @@ const MIME_TO_EXT: Record<string, string> = {
57
57
  "image/avif": ".avif",
58
58
  }
59
59
 
60
+ const IMAGE_EXTS = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".avif", ".bmp"])
61
+ const EXT_TO_MIME: Record<string, string> = {
62
+ ".jpg": "image/jpeg",
63
+ ".jpeg": "image/jpeg",
64
+ ".png": "image/png",
65
+ ".gif": "image/gif",
66
+ ".webp": "image/webp",
67
+ ".svg": "image/svg+xml",
68
+ ".avif": "image/avif",
69
+ ".bmp": "image/bmp",
70
+ }
71
+
60
72
  // ── Helpers ──────────────────────────────────────────────────────────────────
61
73
 
62
74
  function findChromePath(): string {
@@ -149,6 +161,72 @@ async function localizeExternalImages(
149
161
  return patched
150
162
  }
151
163
 
164
+ function isLocalImageRef(ref: string): boolean {
165
+ const pathPart = ref.split(/[?#]/)[0]
166
+ return IMAGE_EXTS.has(extname(pathPart).toLowerCase())
167
+ }
168
+
169
+ export function extractImageAssetRefsForPdf(htmlContent: string): string[] {
170
+ const assetRefPattern = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))|url\(\s*(?:"([^"]*)"|'([^']*)'|([^\s)]+))\s*\)/g
171
+ const refs = new Set<string>()
172
+ let match: RegExpExecArray | null
173
+
174
+ while ((match = assetRefPattern.exec(htmlContent)) !== null) {
175
+ const ref = match.slice(1).find((value): value is string => value !== undefined)
176
+ if (ref) refs.add(ref.trim())
177
+ }
178
+
179
+ return Array.from(refs)
180
+ }
181
+
182
+ async function toDataUrlFromRef(ref: string, baseDir: string): Promise<string | null> {
183
+ if (!ref || ref.startsWith("data:") || ref.startsWith("blob:") || ref.startsWith("#")) {
184
+ return null
185
+ }
186
+
187
+ try {
188
+ if (ref.startsWith("http://") || ref.startsWith("https://") || ref.startsWith("//") || ref.startsWith("file://")) {
189
+ return null
190
+ }
191
+
192
+ let filePath: string | null = null
193
+ if (isLocalImageRef(ref)) {
194
+ filePath = resolve(baseDir, decodeURI(ref.split(/[?#]/)[0]))
195
+ }
196
+
197
+ if (!filePath || !existsSync(filePath)) return null
198
+ const ext = extname(filePath).toLowerCase()
199
+ const mime = EXT_TO_MIME[ext]
200
+ if (!mime) return null
201
+ const buf = readFileSync(filePath)
202
+ return `data:${mime};base64,${buf.toString("base64")}`
203
+ } catch {
204
+ return null
205
+ }
206
+ }
207
+
208
+ export async function inlineImageAssetsForPdf(htmlContent: string, htmlFilePath: string): Promise<string> {
209
+ const baseDir = dirname(resolve(htmlFilePath))
210
+ const refs = extractImageAssetRefsForPdf(htmlContent)
211
+
212
+ if (refs.length === 0) return htmlContent
213
+
214
+ const replacements = new Map<string, string>()
215
+ await Promise.allSettled(
216
+ refs.map(async (ref) => {
217
+ const dataUrl = await toDataUrlFromRef(ref, baseDir)
218
+ if (dataUrl) replacements.set(ref, dataUrl)
219
+ })
220
+ )
221
+
222
+ let patched = htmlContent
223
+ for (const [original, replacement] of replacements) {
224
+ const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
225
+ patched = patched.replace(new RegExp(escaped, "g"), replacement)
226
+ }
227
+ return patched
228
+ }
229
+
152
230
  // ── Main export ──────────────────────────────────────────────────────────────
153
231
 
154
232
  export interface ExportResult {
@@ -185,7 +263,8 @@ export async function exportToPdf(htmlFilePath: string): Promise<ExportResult> {
185
263
  let tmpHtmlPath: string
186
264
  try {
187
265
  const originalHtml = readFileSync(abs, "utf-8")
188
- const patchedHtml = await localizeExternalImages(originalHtml, tmpDir)
266
+ const localizedHtml = await localizeExternalImages(originalHtml, tmpDir)
267
+ const patchedHtml = await inlineImageAssetsForPdf(localizedHtml, abs)
189
268
  tmpHtmlPath = join(tmpDir, "index.html")
190
269
  writeFileSync(tmpHtmlPath, patchedHtml, "utf-8")
191
270
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/plugin.ts CHANGED
@@ -82,6 +82,8 @@ import researchSaveTool from "./tools/research-save"
82
82
  import workspaceScanTool from "./tools/workspace-scan"
83
83
  import extractDocumentMaterialsTool from "./tools/extract-document-materials"
84
84
  import qaTool from "./tools/qa"
85
+ import pdfTool from "./tools/pdf"
86
+ import pptxTool from "./tools/pptx"
85
87
  import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research-prompt"
86
88
  import { runQA, formatReport } from "./lib/qa"
87
89
  import { extractDesignClasses } from "./lib/design/designs"
@@ -331,6 +333,8 @@ const server: Plugin = (async (pluginCtx) => {
331
333
  "revela-workspace-scan": workspaceScanTool,
332
334
  "revela-extract-document-materials": extractDocumentMaterialsTool,
333
335
  "revela-qa": qaTool,
336
+ "revela-pdf": pdfTool,
337
+ "revela-pptx": pptxTool,
334
338
  },
335
339
 
336
340
  // ── chat.message: intercept @-referenced / pasted binary files ────────
package/tools/pdf.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * tools/pdf.ts
3
+ *
4
+ * revela-pdf — Export a Revela HTML slide deck to PDF.
5
+ */
6
+
7
+ import { tool } from "@opencode-ai/plugin"
8
+ import { existsSync } from "fs"
9
+ import { resolve } from "path"
10
+ import { exportToPdf } from "../lib/pdf/export"
11
+
12
+ export default tool({
13
+ description:
14
+ "Export a Revela-generated HTML slide deck to PDF. " +
15
+ "Use this after the deck HTML has been written and layout QA has passed. " +
16
+ "Output is written beside the input file with the same basename and a .pdf extension.",
17
+ args: {
18
+ file: tool.schema
19
+ .string()
20
+ .describe(
21
+ "Path to the HTML slide file to export. " +
22
+ "Can be absolute or relative to the current working directory."
23
+ ),
24
+ },
25
+ async execute({ file }, { directory }) {
26
+ const filePath = resolve(directory || process.cwd(), file)
27
+
28
+ if (!existsSync(filePath)) {
29
+ return JSON.stringify({ ok: false, error: `File not found: ${filePath}` })
30
+ }
31
+
32
+ if (!/\.html?$/i.test(filePath)) {
33
+ return JSON.stringify({ ok: false, error: `File must be an HTML file: ${filePath}` })
34
+ }
35
+
36
+ try {
37
+ const result = await exportToPdf(filePath)
38
+ return JSON.stringify({ ok: true, ...result }, null, 2)
39
+ } catch (e: any) {
40
+ return JSON.stringify({ ok: false, error: e?.message ?? String(e) })
41
+ }
42
+ },
43
+ })
package/tools/pptx.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * tools/pptx.ts
3
+ *
4
+ * revela-pptx — Export a Revela HTML slide deck to editable PPTX.
5
+ */
6
+
7
+ import { tool } from "@opencode-ai/plugin"
8
+ import { existsSync } from "fs"
9
+ import { resolve } from "path"
10
+ import { exportToPptx } from "../lib/pptx/export"
11
+
12
+ export default tool({
13
+ description:
14
+ "Export a Revela-generated HTML slide deck to editable PPTX. " +
15
+ "Use this after the deck HTML has been written and layout QA has passed. " +
16
+ "Output is written beside the input file with the same basename and a .pptx extension.",
17
+ args: {
18
+ file: tool.schema
19
+ .string()
20
+ .describe(
21
+ "Path to the HTML slide file to export. " +
22
+ "Can be absolute or relative to the current working directory."
23
+ ),
24
+ },
25
+ async execute({ file }, { directory }) {
26
+ const filePath = resolve(directory || process.cwd(), file)
27
+
28
+ if (!existsSync(filePath)) {
29
+ return JSON.stringify({ ok: false, error: `File not found: ${filePath}` })
30
+ }
31
+
32
+ if (!/\.html?$/i.test(filePath)) {
33
+ return JSON.stringify({ ok: false, error: `File must be an HTML file: ${filePath}` })
34
+ }
35
+
36
+ const progress: string[] = []
37
+
38
+ try {
39
+ const result = await exportToPptx(filePath, {
40
+ onProgress: (event) => {
41
+ progress.push(event.message)
42
+ },
43
+ })
44
+ return JSON.stringify({ ok: true, ...result, progress }, null, 2)
45
+ } catch (e: any) {
46
+ return JSON.stringify({ ok: false, error: e?.message ?? String(e), progress })
47
+ }
48
+ },
49
+ })