@wafflebase/cli 0.4.7 → 0.4.9
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/client/http-client.d.ts +1 -0
- package/dist/client/http-client.js +3 -0
- package/dist/commands/slides.js +191 -2
- package/dist/docs/paginate.js +1 -1
- package/dist/schema/registry.js +96 -0
- package/dist/slides/content.d.ts +41 -0
- package/dist/slides/content.js +122 -0
- package/dist/slides/pptx-export.d.ts +6 -0
- package/dist/slides/pptx-export.js +10 -0
- package/package.json +4 -3
|
@@ -30,6 +30,7 @@ export declare class HttpClient {
|
|
|
30
30
|
deleteDocument(id: string): Promise<ApiResponse<unknown>>;
|
|
31
31
|
getDocContent(docId: string): Promise<ApiResponse<Document>>;
|
|
32
32
|
putDocContent(docId: string, doc: Document): Promise<ApiResponse<Document>>;
|
|
33
|
+
getSlidesContent(docId: string): Promise<ApiResponse<SlidesDocument>>;
|
|
33
34
|
putSlidesContent(docId: string, deck: SlidesDocument): Promise<ApiResponse<SlidesDocument>>;
|
|
34
35
|
listTabs(docId: string): Promise<ApiResponse<unknown[]>>;
|
|
35
36
|
getCells(docId: string, tabId: string, range?: string): Promise<ApiResponse<unknown>>;
|
|
@@ -119,6 +119,9 @@ export class HttpClient {
|
|
|
119
119
|
// Slides content — same endpoint as docs; the backend dispatches on
|
|
120
120
|
// the persisted document type, picking the docs writer for `'doc'`
|
|
121
121
|
// and the slides writer for `'slides'`.
|
|
122
|
+
getSlidesContent(docId) {
|
|
123
|
+
return this.request('GET', `/documents/${docId}/content`);
|
|
124
|
+
}
|
|
122
125
|
putSlidesContent(docId, deck) {
|
|
123
126
|
return this.request('PUT', `/documents/${docId}/content`, deck);
|
|
124
127
|
}
|
package/dist/commands/slides.js
CHANGED
|
@@ -1,12 +1,201 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { getGlobalOpts, getClient, getConfig } from './root.js';
|
|
3
|
+
import { output, outputError } from '../output/formatter.js';
|
|
4
|
+
import { printDryRun } from '../client/dry-run.js';
|
|
3
5
|
import { runSlidesImport } from '../slides/import.js';
|
|
6
|
+
import { parseSlidesContentFormat, runSlidesContent, } from '../slides/content.js';
|
|
7
|
+
import { writeBinary } from '../output/binary.js';
|
|
8
|
+
import { createImageFetcher } from '../docs/image-fetcher.js';
|
|
9
|
+
import { exportPptxCli } from '../slides/pptx-export.js';
|
|
4
10
|
export function registerSlidesCommand(program) {
|
|
5
11
|
const slides = program
|
|
6
12
|
.command('slides')
|
|
7
13
|
.alias('slide')
|
|
8
14
|
.alias('deck')
|
|
9
15
|
.description('Manage slide decks');
|
|
16
|
+
slides
|
|
17
|
+
.command('list')
|
|
18
|
+
.description('List slide decks in workspace')
|
|
19
|
+
.action(async function () {
|
|
20
|
+
const opts = getGlobalOpts(this);
|
|
21
|
+
try {
|
|
22
|
+
const res = await getClient(opts).listDocuments();
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
throw new Error(`HTTP ${res.status}`);
|
|
25
|
+
let data = res.data;
|
|
26
|
+
if (Array.isArray(data)) {
|
|
27
|
+
data = data.filter((d) => d.type === 'slides');
|
|
28
|
+
}
|
|
29
|
+
output(data, opts.format, opts.quiet);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
outputError(e, opts.quiet);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
slides
|
|
36
|
+
.command('create <title>')
|
|
37
|
+
.description('Create a new slide deck')
|
|
38
|
+
.action(async function (title) {
|
|
39
|
+
const opts = getGlobalOpts(this);
|
|
40
|
+
try {
|
|
41
|
+
if (opts.dryRun) {
|
|
42
|
+
printDryRun(getConfig(opts), 'POST', '/documents', {
|
|
43
|
+
title,
|
|
44
|
+
type: 'slides',
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const res = await getClient(opts).createDocument(title, 'slides');
|
|
49
|
+
if (!res.ok)
|
|
50
|
+
throw new Error(`HTTP ${res.status}`);
|
|
51
|
+
output(res.data, opts.format, opts.quiet);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
outputError(e, opts.quiet);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
slides
|
|
58
|
+
.command('get <doc-id>')
|
|
59
|
+
.description('Show slide deck metadata')
|
|
60
|
+
.action(async function (docId) {
|
|
61
|
+
const opts = getGlobalOpts(this);
|
|
62
|
+
try {
|
|
63
|
+
const res = await getClient(opts).getDocument(docId);
|
|
64
|
+
if (!res.ok)
|
|
65
|
+
throw new Error(`HTTP ${res.status}`);
|
|
66
|
+
output(res.data, opts.format, opts.quiet);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
outputError(e, opts.quiet);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
slides
|
|
73
|
+
.command('rename <doc-id> <title>')
|
|
74
|
+
.description('Rename a slide deck')
|
|
75
|
+
.action(async function (docId, title) {
|
|
76
|
+
const opts = getGlobalOpts(this);
|
|
77
|
+
if (opts.dryRun) {
|
|
78
|
+
printDryRun(getConfig(opts), 'PATCH', `/documents/${docId}`, { title });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const res = await getClient(opts).updateDocument(docId, title);
|
|
83
|
+
if (!res.ok)
|
|
84
|
+
throw new Error(`HTTP ${res.status}`);
|
|
85
|
+
output(res.data, opts.format, opts.quiet);
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
outputError(e, opts.quiet);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
slides
|
|
92
|
+
.command('delete <doc-id>')
|
|
93
|
+
.description('Delete a slide deck')
|
|
94
|
+
.action(async function (docId) {
|
|
95
|
+
const opts = getGlobalOpts(this);
|
|
96
|
+
if (opts.dryRun) {
|
|
97
|
+
printDryRun(getConfig(opts), 'DELETE', `/documents/${docId}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const res = await getClient(opts).deleteDocument(docId);
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
throw new Error(`HTTP ${res.status}`);
|
|
104
|
+
output(res.data, opts.format, opts.quiet);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
outputError(e, opts.quiet);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
slides
|
|
111
|
+
.command('content <doc-id>')
|
|
112
|
+
.description('Read deck content as JSON, Markdown, or plain text')
|
|
113
|
+
// NOTE: `--format` is intentionally not redeclared here — the global
|
|
114
|
+
// `--format` option catches the user's value (see the same comment
|
|
115
|
+
// on `docs content`). We read `opts.format` and validate it through
|
|
116
|
+
// `parseSlidesContentFormat`.
|
|
117
|
+
.option('--notes', 'Include speaker notes (md/text)', false)
|
|
118
|
+
.option('--out <file>', 'Output file (- for stdout)')
|
|
119
|
+
.option('--force', 'Overwrite existing output file', false)
|
|
120
|
+
.action(async function (docId) {
|
|
121
|
+
const opts = getGlobalOpts(this);
|
|
122
|
+
const local = this.opts();
|
|
123
|
+
try {
|
|
124
|
+
const format = parseSlidesContentFormat(opts.format);
|
|
125
|
+
if (opts.dryRun) {
|
|
126
|
+
printDryRun(getConfig(opts), 'GET', `/documents/${docId}/content`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const res = await getClient(opts).getSlidesContent(docId);
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
const body = res.data;
|
|
132
|
+
if (body?.error) {
|
|
133
|
+
// Surface backend-shaped errors (e.g., TYPE_MISMATCH) verbatim
|
|
134
|
+
// so agents reading stderr can act on the `code` field.
|
|
135
|
+
console.error(JSON.stringify(body, null, 2));
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`HTTP ${res.status}`);
|
|
140
|
+
}
|
|
141
|
+
runSlidesContent({
|
|
142
|
+
deck: res.data,
|
|
143
|
+
format,
|
|
144
|
+
notes: local.notes,
|
|
145
|
+
out: local.out,
|
|
146
|
+
force: local.force,
|
|
147
|
+
quiet: opts.quiet,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
outputError(e, opts.quiet);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
slides
|
|
155
|
+
.command('export <doc-id> <file>')
|
|
156
|
+
.description('Export a slide deck to PPTX')
|
|
157
|
+
// NOTE: `--format` is intentionally not redeclared here — the global
|
|
158
|
+
// `--format` option catches the user's value (see the same comment on
|
|
159
|
+
// `docs export`). We read `opts.format` via `getOptionValueSourceWithGlobals`
|
|
160
|
+
// to distinguish an explicit CLI flag from the default, then validate
|
|
161
|
+
// that only "pptx" is accepted.
|
|
162
|
+
.option('--force', 'Overwrite existing output file', false)
|
|
163
|
+
.action(async function (docId, file) {
|
|
164
|
+
const opts = getGlobalOpts(this);
|
|
165
|
+
const local = this.opts();
|
|
166
|
+
try {
|
|
167
|
+
const formatSource = this.getOptionValueSourceWithGlobals('format');
|
|
168
|
+
// Widen to `string` — `opts.format` is the global `OutputFormat`
|
|
169
|
+
// union (json|table|csv|yaml), which has no overlap with the
|
|
170
|
+
// export-only `pptx` value, so a direct comparison is a tsc error.
|
|
171
|
+
const fmt = formatSource === 'cli' ? opts.format : undefined;
|
|
172
|
+
if (fmt && fmt !== 'pptx')
|
|
173
|
+
throw new Error(`Invalid --format "${fmt}". Only "pptx" is supported.`);
|
|
174
|
+
if (!fmt && extname(file).toLowerCase() !== '.pptx') {
|
|
175
|
+
throw new Error(`Cannot infer format from "${file}". Use a .pptx extension or --format pptx.`);
|
|
176
|
+
}
|
|
177
|
+
if (opts.dryRun) {
|
|
178
|
+
printDryRun(getConfig(opts), 'GET', `/documents/${docId}/content`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const res = await getClient(opts).getSlidesContent(docId);
|
|
182
|
+
if (!res.ok) {
|
|
183
|
+
const body = res.data;
|
|
184
|
+
if (body?.error) {
|
|
185
|
+
console.error(JSON.stringify(body, null, 2));
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`HTTP ${res.status}`);
|
|
190
|
+
}
|
|
191
|
+
const imageFetcher = createImageFetcher({ serverBase: getConfig(opts).server });
|
|
192
|
+
const bytes = await exportPptxCli(res.data, { imageFetcher });
|
|
193
|
+
writeBinary(bytes, file, { force: local.force, quiet: opts.quiet });
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
outputError(e, opts.quiet);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
10
199
|
registerSlidesImportCommand(slides);
|
|
11
200
|
}
|
|
12
201
|
export function registerSlidesImportCommand(slides) {
|
package/dist/docs/paginate.js
CHANGED
|
@@ -14,6 +14,6 @@ export function paginateForCli(doc, measurer) {
|
|
|
14
14
|
const pageSetup = resolvePageSetup(doc.pageSetup);
|
|
15
15
|
const { width: paperWidth } = getEffectiveDimensions(pageSetup);
|
|
16
16
|
const contentWidth = paperWidth - pageSetup.margins.left - pageSetup.margins.right;
|
|
17
|
-
const { layout } = computeLayout(doc.blocks, measurer, contentWidth);
|
|
17
|
+
const { layout } = computeLayout(doc.blocks, measurer, contentWidth, undefined, undefined, undefined, doc.styles);
|
|
18
18
|
return paginateLayout(layout, pageSetup);
|
|
19
19
|
}
|
package/dist/schema/registry.js
CHANGED
|
@@ -141,6 +141,102 @@ const registry = [
|
|
|
141
141
|
],
|
|
142
142
|
aliases: ['doc.import', 'document.import', 'documents.import'],
|
|
143
143
|
},
|
|
144
|
+
// Slides (presentation) namespace
|
|
145
|
+
{
|
|
146
|
+
name: 'slides.list',
|
|
147
|
+
description: 'List slide decks in workspace',
|
|
148
|
+
safety: 'read-only',
|
|
149
|
+
parameters: {},
|
|
150
|
+
response: { type: 'array', items: { id: 'string', title: 'string', type: 'string', createdAt: 'string' } },
|
|
151
|
+
aliases: ['slide.list', 'deck.list', 'decks.list'],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'slides.create',
|
|
155
|
+
description: 'Create a new slide deck',
|
|
156
|
+
safety: 'write',
|
|
157
|
+
parameters: {
|
|
158
|
+
title: { type: 'string', required: true, description: 'Deck title' },
|
|
159
|
+
},
|
|
160
|
+
response: { id: 'string', title: 'string', type: 'string' },
|
|
161
|
+
aliases: ['slide.create', 'deck.create', 'decks.create'],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'slides.get',
|
|
165
|
+
description: 'Show slide deck metadata',
|
|
166
|
+
safety: 'read-only',
|
|
167
|
+
parameters: {
|
|
168
|
+
'doc-id': { type: 'string', required: true, description: 'Document ID' },
|
|
169
|
+
},
|
|
170
|
+
response: { id: 'string', title: 'string', type: 'string', createdAt: 'string' },
|
|
171
|
+
aliases: ['slide.get', 'deck.get', 'decks.get'],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'slides.rename',
|
|
175
|
+
description: 'Rename a slide deck',
|
|
176
|
+
safety: 'write',
|
|
177
|
+
parameters: {
|
|
178
|
+
'doc-id': { type: 'string', required: true, description: 'Document ID' },
|
|
179
|
+
title: { type: 'string', required: true, description: 'New title' },
|
|
180
|
+
},
|
|
181
|
+
response: { id: 'string', title: 'string' },
|
|
182
|
+
aliases: ['slide.rename', 'deck.rename', 'decks.rename'],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'slides.delete',
|
|
186
|
+
description: 'Delete a slide deck',
|
|
187
|
+
safety: 'destructive',
|
|
188
|
+
parameters: {
|
|
189
|
+
'doc-id': { type: 'string', required: true, description: 'Document ID' },
|
|
190
|
+
},
|
|
191
|
+
response: { id: 'string' },
|
|
192
|
+
aliases: ['slide.delete', 'deck.delete', 'decks.delete'],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'slides.content',
|
|
196
|
+
description: 'Read deck content as JSON, Markdown, or plain text',
|
|
197
|
+
safety: 'read-only',
|
|
198
|
+
parameters: {
|
|
199
|
+
'doc-id': { type: 'string', required: true, description: 'Document ID' },
|
|
200
|
+
'--format': { type: 'string', required: false, description: 'Output format (json|md|text)', default: 'json' },
|
|
201
|
+
'--notes': { type: 'boolean', required: false, description: 'Include speaker notes (md/text)', default: 'false' },
|
|
202
|
+
'--out': { type: 'string', required: false, description: 'Output file (- for stdout)' },
|
|
203
|
+
'--force': { type: 'boolean', required: false, description: 'Overwrite existing output file', default: 'false' },
|
|
204
|
+
},
|
|
205
|
+
response: { type: 'object', description: 'Deck JSON, Markdown text, or plaintext per --format' },
|
|
206
|
+
aliases: ['slide.content', 'deck.content', 'decks.content'],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'slides.import',
|
|
210
|
+
description: 'Import a .pptx file as a new (or replacement) slide deck',
|
|
211
|
+
// Default safety is `write` (create new deck); `--replace` flips it
|
|
212
|
+
// to destructive, mirroring `docs.import`.
|
|
213
|
+
safety: 'write',
|
|
214
|
+
parameters: {
|
|
215
|
+
file: { type: 'string', required: true, description: 'Source .pptx path or - for stdin' },
|
|
216
|
+
'--title': { type: 'string', required: false, description: 'Deck title (default: file basename)' },
|
|
217
|
+
'--replace': { type: 'string', required: false, description: 'Existing deck ID to replace' },
|
|
218
|
+
'--yes': { type: 'boolean', required: false, description: 'Skip --replace confirmation', default: 'false' },
|
|
219
|
+
},
|
|
220
|
+
response: { id: 'string', title: 'string', replaced: 'boolean' },
|
|
221
|
+
variants: [
|
|
222
|
+
{ when: 'default', safety: 'write', creates: 'new slide deck' },
|
|
223
|
+
{ when: '--replace given', safety: 'destructive', modifies: 'existing deck content' },
|
|
224
|
+
],
|
|
225
|
+
aliases: ['slide.import', 'deck.import', 'decks.import'],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'slides.export',
|
|
229
|
+
description: 'Export a slide deck to PPTX',
|
|
230
|
+
safety: 'read-only',
|
|
231
|
+
parameters: {
|
|
232
|
+
'doc-id': { type: 'string', required: true, description: 'Document ID' },
|
|
233
|
+
file: { type: 'string', required: true, description: 'Output path or - for stdout' },
|
|
234
|
+
'--format': { type: 'string', required: false, description: 'Output format (pptx); default from extension' },
|
|
235
|
+
'--force': { type: 'boolean', required: false, description: 'Overwrite existing output file', default: 'false' },
|
|
236
|
+
},
|
|
237
|
+
response: { type: 'binary', description: 'PPTX bytes' },
|
|
238
|
+
aliases: ['slide.export', 'deck.export', 'decks.export'],
|
|
239
|
+
},
|
|
144
240
|
// Sheets namespace — canonical names live under sheets.*
|
|
145
241
|
{
|
|
146
242
|
name: 'sheets.tabs.list',
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type SlidesDocument } from '@wafflebase/slides/node';
|
|
2
|
+
export declare const VALID_SLIDES_CONTENT_FORMATS: readonly ["json", "md", "text"];
|
|
3
|
+
export type SlidesContentFormat = (typeof VALID_SLIDES_CONTENT_FORMATS)[number];
|
|
4
|
+
export declare const SLIDES_LOSSY_NOTICE: string;
|
|
5
|
+
export declare function parseSlidesContentFormat(value: string): SlidesContentFormat;
|
|
6
|
+
/**
|
|
7
|
+
* Surface for `runSlidesContent` to talk back to the CLI without taking
|
|
8
|
+
* a direct dependency on `process.stdout` / `process.stderr` / `fs`.
|
|
9
|
+
* Mirrors `ContentIO` in `../docs/content.ts`; the command wires the
|
|
10
|
+
* real implementations, tests inject in-memory collectors.
|
|
11
|
+
*/
|
|
12
|
+
export interface SlidesContentIO {
|
|
13
|
+
stdout: (text: string) => void;
|
|
14
|
+
stderr: (line: string) => void;
|
|
15
|
+
/** Write `text` to `path`. Throws if the file exists and `force` is false. */
|
|
16
|
+
writeFile: (path: string, text: string, force: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const defaultSlidesContentIO: SlidesContentIO;
|
|
19
|
+
export interface RunSlidesContentArgs {
|
|
20
|
+
deck: SlidesDocument;
|
|
21
|
+
format: SlidesContentFormat;
|
|
22
|
+
/** Include speaker notes (`Slide.notes`) after each slide's body. */
|
|
23
|
+
notes?: boolean;
|
|
24
|
+
out?: string;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
quiet?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pure orchestration for `slides content`: takes an already-fetched
|
|
30
|
+
* `SlidesDocument` plus user flags, produces the rendered output, and
|
|
31
|
+
* routes it through the supplied IO surface.
|
|
32
|
+
*
|
|
33
|
+
* - `json`: the raw deck JSON (lossless — themes, layouts, geometry,
|
|
34
|
+
* styling all preserved).
|
|
35
|
+
* - `md` / `text`: per-slide text extraction only. Slides have no single
|
|
36
|
+
* linear text stream, so we walk each slide's elements in document
|
|
37
|
+
* order, pull every `TextBody`'s blocks (text boxes, shape labels,
|
|
38
|
+
* table cells), and serialize them with the docs serializers. Shapes,
|
|
39
|
+
* images, connectors, positioning and theming are dropped.
|
|
40
|
+
*/
|
|
41
|
+
export declare function runSlidesContent(args: RunSlidesContentArgs, io?: SlidesContentIO): void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { serializeMarkdown, serializeText, } from '@wafflebase/docs';
|
|
3
|
+
import { flattenElements, } from '@wafflebase/slides/node';
|
|
4
|
+
export const VALID_SLIDES_CONTENT_FORMATS = ['json', 'md', 'text'];
|
|
5
|
+
export const SLIDES_LOSSY_NOTICE = 'Lossy conversion: only text is extracted (shapes, images, connectors, ' +
|
|
6
|
+
'layout, theming, and positioning are dropped). See cli.md for details.';
|
|
7
|
+
export function parseSlidesContentFormat(value) {
|
|
8
|
+
if (!VALID_SLIDES_CONTENT_FORMATS.includes(value)) {
|
|
9
|
+
throw new Error(`Invalid --format "${value}". Use one of: ${VALID_SLIDES_CONTENT_FORMATS.join(', ')}.`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export const defaultSlidesContentIO = {
|
|
14
|
+
stdout: (text) => {
|
|
15
|
+
process.stdout.write(text);
|
|
16
|
+
if (!text.endsWith('\n'))
|
|
17
|
+
process.stdout.write('\n');
|
|
18
|
+
},
|
|
19
|
+
stderr: (line) => {
|
|
20
|
+
console.error(line);
|
|
21
|
+
},
|
|
22
|
+
writeFile: (path, text, force) => {
|
|
23
|
+
if (existsSync(path) && !force) {
|
|
24
|
+
throw new Error(`Refusing to overwrite "${path}". Pass --force to allow overwrite.`);
|
|
25
|
+
}
|
|
26
|
+
writeFileSync(path, text.endsWith('\n') ? text : text + '\n', 'utf-8');
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Pure orchestration for `slides content`: takes an already-fetched
|
|
31
|
+
* `SlidesDocument` plus user flags, produces the rendered output, and
|
|
32
|
+
* routes it through the supplied IO surface.
|
|
33
|
+
*
|
|
34
|
+
* - `json`: the raw deck JSON (lossless — themes, layouts, geometry,
|
|
35
|
+
* styling all preserved).
|
|
36
|
+
* - `md` / `text`: per-slide text extraction only. Slides have no single
|
|
37
|
+
* linear text stream, so we walk each slide's elements in document
|
|
38
|
+
* order, pull every `TextBody`'s blocks (text boxes, shape labels,
|
|
39
|
+
* table cells), and serialize them with the docs serializers. Shapes,
|
|
40
|
+
* images, connectors, positioning and theming are dropped.
|
|
41
|
+
*/
|
|
42
|
+
export function runSlidesContent(args, io = defaultSlidesContentIO) {
|
|
43
|
+
const { deck, format, notes = false, out, force = false, quiet = false } = args;
|
|
44
|
+
let text;
|
|
45
|
+
if (format === 'json') {
|
|
46
|
+
text = JSON.stringify(deck, null, 2);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (!quiet)
|
|
50
|
+
io.stderr(SLIDES_LOSSY_NOTICE);
|
|
51
|
+
text = serializeDeck(deck, format, notes);
|
|
52
|
+
}
|
|
53
|
+
if (!out || out === '-') {
|
|
54
|
+
io.stdout(text);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
io.writeFile(out, text, force);
|
|
58
|
+
if (!quiet)
|
|
59
|
+
io.stderr(`Wrote to ${out}`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Render every slide as a `## Slide N` (md) / `Slide N` (text) section
|
|
63
|
+
* followed by the slide's extracted text. Sections are joined with a
|
|
64
|
+
* blank line so the stream reads as one document.
|
|
65
|
+
*/
|
|
66
|
+
function serializeDeck(deck, format, notes) {
|
|
67
|
+
const sections = deck.slides.map((slide, i) => serializeSlide(slide, i + 1, format, notes));
|
|
68
|
+
return sections.join('\n\n');
|
|
69
|
+
}
|
|
70
|
+
function serializeSlide(slide, number, format, notes) {
|
|
71
|
+
const blocks = collectSlideBlocks(slide);
|
|
72
|
+
const body = serializeBlocks(blocks, format);
|
|
73
|
+
const parts = [];
|
|
74
|
+
parts.push(format === 'md' ? `## Slide ${number}` : `Slide ${number}`);
|
|
75
|
+
if (body.length > 0)
|
|
76
|
+
parts.push(body);
|
|
77
|
+
if (notes && slide.notes.length > 0) {
|
|
78
|
+
const notesBody = serializeBlocks(slide.notes, format);
|
|
79
|
+
if (notesBody.length > 0) {
|
|
80
|
+
parts.push(format === 'md' ? '### Notes' : 'Notes:');
|
|
81
|
+
parts.push(notesBody);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return parts.join('\n\n');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Collect every text block from a slide's elements, in document order.
|
|
88
|
+
* Groups are flattened so nested text is included.
|
|
89
|
+
*/
|
|
90
|
+
function collectSlideBlocks(slide) {
|
|
91
|
+
const blocks = [];
|
|
92
|
+
for (const el of flattenElements(slide.elements)) {
|
|
93
|
+
for (const body of textBodiesOf(el)) {
|
|
94
|
+
blocks.push(...body.blocks);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return blocks;
|
|
98
|
+
}
|
|
99
|
+
/** Extract every `TextBody` carried by a single element. */
|
|
100
|
+
function textBodiesOf(el) {
|
|
101
|
+
switch (el.type) {
|
|
102
|
+
case 'text':
|
|
103
|
+
// `TextElement.data` is a `TextBody` (intersection with fill/stroke).
|
|
104
|
+
return [el.data];
|
|
105
|
+
case 'shape':
|
|
106
|
+
return el.data.text ? [el.data.text] : [];
|
|
107
|
+
case 'table':
|
|
108
|
+
return el.data.rows.flatMap((row) => row.cells.map((cell) => cell.body));
|
|
109
|
+
default:
|
|
110
|
+
// image / connector carry no text; group is handled by flatten.
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Serialize a flat `Block[]` via the docs serializers by wrapping them
|
|
116
|
+
* in a synthetic `Document`. The serializers read only `doc.blocks`
|
|
117
|
+
* (header/footer are opt-in and never set here), so the cast is safe.
|
|
118
|
+
*/
|
|
119
|
+
function serializeBlocks(blocks, format) {
|
|
120
|
+
const doc = { blocks };
|
|
121
|
+
return format === 'md' ? serializeMarkdown(doc) : serializeText(doc);
|
|
122
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type SlidesDocument } from '@wafflebase/slides/node';
|
|
2
|
+
export interface CliPptxExportOptions {
|
|
3
|
+
/** Blob-returning fetcher (the CLI's shared image fetcher). */
|
|
4
|
+
imageFetcher?: (url: string) => Promise<Blob>;
|
|
5
|
+
}
|
|
6
|
+
export declare function exportPptxCli(deck: SlidesDocument, opts?: CliPptxExportOptions): Promise<Uint8Array>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { exportPptx } from '@wafflebase/slides/node';
|
|
2
|
+
export async function exportPptxCli(deck, opts = {}) {
|
|
3
|
+
const fetchImage = opts.imageFetcher
|
|
4
|
+
? async (src) => {
|
|
5
|
+
const blob = await opts.imageFetcher(src);
|
|
6
|
+
return { bytes: new Uint8Array(await blob.arrayBuffer()), mime: blob.type || 'image/png' };
|
|
7
|
+
}
|
|
8
|
+
: undefined;
|
|
9
|
+
return exportPptx(deck, { fetchImage });
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wafflebase/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "CLI for Wafflebase spreadsheet API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -20,14 +20,15 @@
|
|
|
20
20
|
"open": "^11.0.0",
|
|
21
21
|
"pdf-lib": "^1.17.1",
|
|
22
22
|
"yaml": "^2.0.0",
|
|
23
|
-
"@wafflebase/
|
|
24
|
-
"@wafflebase/
|
|
23
|
+
"@wafflebase/docs": "0.4.9",
|
|
24
|
+
"@wafflebase/slides": "0.4.9"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/fontkit": "^2.0.0",
|
|
28
28
|
"@types/node": "^25.4.0",
|
|
29
29
|
"@vitest/coverage-v8": "4.1.8",
|
|
30
30
|
"eslint": "^9.18.0",
|
|
31
|
+
"jszip": "^3.10.1",
|
|
31
32
|
"tsx": "^4.19.0",
|
|
32
33
|
"typescript": "^5.9.3",
|
|
33
34
|
"vitest": "^4.1.8"
|