@wafflebase/cli 0.4.8 → 0.5.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.
Files changed (83) hide show
  1. package/dist/bin.js +99693 -11
  2. package/package.json +7 -6
  3. package/dist/bin.d.ts +0 -2
  4. package/dist/client/dry-run.d.ts +0 -5
  5. package/dist/client/dry-run.js +0 -16
  6. package/dist/client/http-client.d.ts +0 -59
  7. package/dist/client/http-client.js +0 -178
  8. package/dist/commands/api-keys.d.ts +0 -2
  9. package/dist/commands/api-keys.js +0 -54
  10. package/dist/commands/cells.d.ts +0 -2
  11. package/dist/commands/cells.js +0 -108
  12. package/dist/commands/ctx.d.ts +0 -5
  13. package/dist/commands/ctx.js +0 -56
  14. package/dist/commands/docs.d.ts +0 -3
  15. package/dist/commands/docs.js +0 -320
  16. package/dist/commands/login.d.ts +0 -2
  17. package/dist/commands/login.js +0 -220
  18. package/dist/commands/logout.d.ts +0 -2
  19. package/dist/commands/logout.js +0 -10
  20. package/dist/commands/root.d.ts +0 -18
  21. package/dist/commands/root.js +0 -37
  22. package/dist/commands/schema.d.ts +0 -2
  23. package/dist/commands/schema.js +0 -28
  24. package/dist/commands/sheets-export.d.ts +0 -2
  25. package/dist/commands/sheets-export.js +0 -52
  26. package/dist/commands/sheets-import.d.ts +0 -2
  27. package/dist/commands/sheets-import.js +0 -88
  28. package/dist/commands/sheets.d.ts +0 -2
  29. package/dist/commands/sheets.js +0 -17
  30. package/dist/commands/slides.d.ts +0 -3
  31. package/dist/commands/slides.js +0 -227
  32. package/dist/commands/status.d.ts +0 -2
  33. package/dist/commands/status.js +0 -22
  34. package/dist/commands/tabs.d.ts +0 -2
  35. package/dist/commands/tabs.js +0 -20
  36. package/dist/config/config.d.ts +0 -38
  37. package/dist/config/config.js +0 -135
  38. package/dist/config/session.d.ts +0 -49
  39. package/dist/config/session.js +0 -74
  40. package/dist/docs/content.d.ts +0 -37
  41. package/dist/docs/content.js +0 -90
  42. package/dist/docs/docx-export.d.ts +0 -23
  43. package/dist/docs/docx-export.js +0 -14
  44. package/dist/docs/docx-import.d.ts +0 -36
  45. package/dist/docs/docx-import.js +0 -70
  46. package/dist/docs/dom-polyfill.d.ts +0 -1
  47. package/dist/docs/dom-polyfill.js +0 -29
  48. package/dist/docs/fontkit-measurer.d.ts +0 -41
  49. package/dist/docs/fontkit-measurer.js +0 -64
  50. package/dist/docs/image-fetcher.d.ts +0 -37
  51. package/dist/docs/image-fetcher.js +0 -38
  52. package/dist/docs/import.d.ts +0 -72
  53. package/dist/docs/import.js +0 -146
  54. package/dist/docs/page-range.d.ts +0 -29
  55. package/dist/docs/page-range.js +0 -74
  56. package/dist/docs/page-slice.d.ts +0 -45
  57. package/dist/docs/page-slice.js +0 -61
  58. package/dist/docs/paginate.d.ts +0 -13
  59. package/dist/docs/paginate.js +0 -19
  60. package/dist/docs/pdf-export.d.ts +0 -47
  61. package/dist/docs/pdf-export.js +0 -95
  62. package/dist/output/binary.d.ts +0 -37
  63. package/dist/output/binary.js +0 -47
  64. package/dist/output/csv.d.ts +0 -4
  65. package/dist/output/csv.js +0 -25
  66. package/dist/output/formatter.d.ts +0 -4
  67. package/dist/output/formatter.js +0 -41
  68. package/dist/output/json.d.ts +0 -1
  69. package/dist/output/json.js +0 -3
  70. package/dist/output/table.d.ts +0 -4
  71. package/dist/output/table.js +0 -14
  72. package/dist/schema/registry.d.ts +0 -42
  73. package/dist/schema/registry.js +0 -375
  74. package/dist/slides/content.d.ts +0 -41
  75. package/dist/slides/content.js +0 -122
  76. package/dist/slides/import.d.ts +0 -82
  77. package/dist/slides/import.js +0 -170
  78. package/dist/slides/pptx-export.d.ts +0 -6
  79. package/dist/slides/pptx-export.js +0 -10
  80. package/dist/slides/pptx-import.d.ts +0 -35
  81. package/dist/slides/pptx-import.js +0 -49
  82. package/dist/util/csv-parse.d.ts +0 -21
  83. package/dist/util/csv-parse.js +0 -99
@@ -1,146 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { basename, extname } from 'node:path';
3
- import { createInterface } from 'node:readline';
4
- import { importDocx, InvalidDocxError } from './docx-import.js';
5
- export const defaultImportIO = {
6
- stdout: (line) => {
7
- process.stdout.write(line);
8
- if (!line.endsWith('\n'))
9
- process.stdout.write('\n');
10
- },
11
- stderr: (line) => {
12
- console.error(line);
13
- },
14
- readBytes: async (path) => {
15
- if (path === '-') {
16
- return new Promise((resolve, reject) => {
17
- const chunks = [];
18
- process.stdin.on('data', (c) => chunks.push(c));
19
- process.stdin.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))));
20
- process.stdin.on('error', reject);
21
- });
22
- }
23
- return new Uint8Array(readFileSync(path));
24
- },
25
- confirm: (prompt) => {
26
- return new Promise((resolve) => {
27
- const rl = createInterface({ input: process.stdin, output: process.stderr });
28
- rl.question(prompt, (answer) => {
29
- rl.close();
30
- const a = answer.trim().toLowerCase();
31
- resolve(a === 'y' || a === 'yes');
32
- });
33
- });
34
- },
35
- isTTY: process.stdin.isTTY ?? false,
36
- };
37
- /**
38
- * Pure orchestration for `docs import`. Reads the source `.docx`,
39
- * parses it into a `Document`, then either creates a new document
40
- * (default) or overwrites an existing one (`--replace`).
41
- *
42
- * Why split out from the command action: the action's layered handling
43
- * of stdin / TTY / interactive prompt is the easiest path to test
44
- * regressions in. Mocking `process.stdin` at the harness level is
45
- * fragile; passing in an `ImportIO` lets us drive every branch
46
- * deterministically.
47
- */
48
- export async function runDocsImport(args, client, io = defaultImportIO) {
49
- const { file, replace, yes = false, imageUploader, quiet = false, dryRun = false } = args;
50
- const inferredTitle = args.title ?? defaultTitleFor(file);
51
- if (replace) {
52
- // `--replace` is destructive: confirm interactively unless `--yes`.
53
- if (!yes) {
54
- if (!io.isTTY) {
55
- io.stderr(JSON.stringify({
56
- error: {
57
- code: 'CONFIRMATION_REQ',
58
- message: `Pass --yes to confirm replacing document "${replace}".`,
59
- },
60
- }, null, 2));
61
- return { exitCode: 1 };
62
- }
63
- const ok = await io.confirm(`This will replace content of ${replace}. Continue? [y/N] `);
64
- if (!ok) {
65
- if (!quiet)
66
- io.stderr('Aborted.');
67
- return { exitCode: 0 };
68
- }
69
- }
70
- const buf = await io.readBytes(file);
71
- const parsed = await safeImportDocx(buf, imageUploader, io);
72
- if (parsed === null)
73
- return { exitCode: 1 };
74
- const doc = parsed;
75
- if (dryRun) {
76
- io.stdout(JSON.stringify({ method: 'PUT', path: `/documents/${replace}/content`, body: doc }, null, 2));
77
- return { exitCode: 0 };
78
- }
79
- const res = await client.putDocContent(replace, doc);
80
- if (!res.ok) {
81
- io.stderr(JSON.stringify(res.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
82
- return { exitCode: 1 };
83
- }
84
- io.stdout(JSON.stringify({ id: replace, replaced: true }, null, 2));
85
- return { exitCode: 0 };
86
- }
87
- // Default flow: POST + PUT.
88
- const buf = await io.readBytes(file);
89
- const parsedNew = await safeImportDocx(buf, imageUploader, io);
90
- if (parsedNew === null)
91
- return { exitCode: 1 };
92
- const doc = parsedNew;
93
- if (dryRun) {
94
- io.stdout(JSON.stringify({
95
- method: 'POST',
96
- path: '/documents',
97
- body: { title: inferredTitle, type: 'doc' },
98
- followUp: { method: 'PUT', path: '/documents/<new-id>/content' },
99
- }, null, 2));
100
- return { exitCode: 0 };
101
- }
102
- const created = await client.createDocument(inferredTitle, 'doc');
103
- if (!created.ok) {
104
- io.stderr(JSON.stringify(created.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
105
- return { exitCode: 1 };
106
- }
107
- const newId = created.data?.id;
108
- if (!newId) {
109
- io.stderr(JSON.stringify({ error: { code: 'INVALID_RESPONSE', message: 'Server did not return an id' } }, null, 2));
110
- return { exitCode: 1 };
111
- }
112
- const put = await client.putDocContent(newId, doc);
113
- if (!put.ok) {
114
- io.stderr(JSON.stringify(put.data ?? { error: { code: 'HTTP_ERROR' } }, null, 2));
115
- return { exitCode: 1 };
116
- }
117
- io.stdout(JSON.stringify({ id: newId, title: inferredTitle }, null, 2));
118
- return { exitCode: 0 };
119
- }
120
- /** Strip the directory and extension to produce a default doc title. */
121
- function defaultTitleFor(file) {
122
- if (file === '-')
123
- return 'Untitled';
124
- return basename(file, extname(file)) || 'Untitled';
125
- }
126
- /**
127
- * Run `importDocx` and turn `InvalidDocxError` into a structured
128
- * stderr body + `null` return so callers can short-circuit with
129
- * `exitCode: 1` while keeping the rest of the error envelope
130
- * (`HTTP_ERROR`, `INVALID_RESPONSE`, etc.) consistent. The action's
131
- * `outputError` would technically also surface the error code today
132
- * (post-B1), but catching here keeps the contract local to this file
133
- * and matches every other failure path in `runDocsImport`.
134
- */
135
- async function safeImportDocx(buf, imageUploader, io) {
136
- try {
137
- return await importDocx(buf, { imageUploader });
138
- }
139
- catch (e) {
140
- if (e instanceof InvalidDocxError) {
141
- io.stderr(JSON.stringify({ error: { code: e.code, message: e.message } }, null, 2));
142
- return null;
143
- }
144
- throw e;
145
- }
146
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Resolved page selection produced by {@link parsePageRange}.
3
- *
4
- * `pages` is the deduplicated set of 1-based page numbers requested by
5
- * the user, after clamping to the document's actual page count.
6
- *
7
- * `warnings` collects clamp/duplicate notices the caller should emit to
8
- * stderr (parsing succeeded; the user just asked for pages that don't
9
- * exist). Hard errors throw instead — see {@link parsePageRange}.
10
- */
11
- export interface PageRange {
12
- pages: ReadonlySet<number>;
13
- warnings: ReadonlyArray<string>;
14
- }
15
- /**
16
- * Parse a `--pages` flag value (e.g., `"1-3,5,7-9"`) against a known
17
- * page count.
18
- *
19
- * Throws `Error` on malformed input — empty strings, non-numeric tokens,
20
- * a `0` page (1-based), or a reversed range (`"3-1"`). Out-of-range
21
- * upper bounds are *not* errors: they get clamped to `totalPages` with a
22
- * stderr-bound warning string in the result. Tokens whose entire range
23
- * sits past `totalPages` collapse to no pages with a warning, never an
24
- * error — this lets `--pages 1-99` work on a 3-page doc.
25
- *
26
- * `totalPages` of 0 is a valid input (empty doc) and yields an empty set
27
- * with a single "document has no pages" warning, regardless of input.
28
- */
29
- export declare function parsePageRange(input: string, totalPages: number): PageRange;
@@ -1,74 +0,0 @@
1
- const SINGLE = /^\d+$/;
2
- const RANGE = /^(\d+)-(\d+)$/;
3
- /**
4
- * Parse a `--pages` flag value (e.g., `"1-3,5,7-9"`) against a known
5
- * page count.
6
- *
7
- * Throws `Error` on malformed input — empty strings, non-numeric tokens,
8
- * a `0` page (1-based), or a reversed range (`"3-1"`). Out-of-range
9
- * upper bounds are *not* errors: they get clamped to `totalPages` with a
10
- * stderr-bound warning string in the result. Tokens whose entire range
11
- * sits past `totalPages` collapse to no pages with a warning, never an
12
- * error — this lets `--pages 1-99` work on a 3-page doc.
13
- *
14
- * `totalPages` of 0 is a valid input (empty doc) and yields an empty set
15
- * with a single "document has no pages" warning, regardless of input.
16
- */
17
- export function parsePageRange(input, totalPages) {
18
- if (totalPages < 0) {
19
- throw new Error(`parsePageRange: totalPages must be ≥ 0, got ${totalPages}`);
20
- }
21
- const trimmed = input.trim();
22
- if (trimmed === '') {
23
- throw new Error('Page range is empty.');
24
- }
25
- const tokens = trimmed.split(',').map((t) => t.trim());
26
- for (const tok of tokens) {
27
- if (tok === '') {
28
- throw new Error(`Empty token in page range "${input}"`);
29
- }
30
- }
31
- const pages = new Set();
32
- const warnings = [];
33
- if (totalPages === 0) {
34
- warnings.push('Document has no pages.');
35
- return { pages, warnings };
36
- }
37
- for (const tok of tokens) {
38
- if (SINGLE.test(tok)) {
39
- const n = Number(tok);
40
- if (n < 1) {
41
- throw new Error(`Page numbers are 1-based; got "${tok}"`);
42
- }
43
- if (n > totalPages) {
44
- warnings.push(`Page ${n} is beyond document end (${totalPages}); ignored.`);
45
- continue;
46
- }
47
- pages.add(n);
48
- continue;
49
- }
50
- const m = RANGE.exec(tok);
51
- if (!m) {
52
- throw new Error(`Invalid page token "${tok}" in "${input}"`);
53
- }
54
- const lo = Number(m[1]);
55
- const hi = Number(m[2]);
56
- if (lo < 1 || hi < 1) {
57
- throw new Error(`Page numbers are 1-based; got "${tok}"`);
58
- }
59
- if (lo > hi) {
60
- throw new Error(`Reversed range "${tok}" — start must be ≤ end.`);
61
- }
62
- if (lo > totalPages) {
63
- warnings.push(`Range ${tok} starts beyond document end (${totalPages}); ignored.`);
64
- continue;
65
- }
66
- const clampedHi = Math.min(hi, totalPages);
67
- if (clampedHi < hi) {
68
- warnings.push(`Range ${tok} clamped to ${lo}-${clampedHi} (document has ${totalPages} pages).`);
69
- }
70
- for (let p = lo; p <= clampedHi; p++)
71
- pages.add(p);
72
- }
73
- return { pages, warnings };
74
- }
@@ -1,45 +0,0 @@
1
- import type { Block, BlockPageMeta, Document, PaginatedLayout } from '@wafflebase/docs';
2
- import type { PageRange } from './page-range.js';
3
- /**
4
- * Output formats supported by the CLI's `docs content --pages` flow.
5
- *
6
- * `'json'` keeps full structural fidelity and triggers `pageMeta`
7
- * emission so consumers can recover page boundaries without re-running
8
- * pagination. `'md'` and `'text'` drop the metadata since the
9
- * serializers themselves are page-blind — block selection alone is
10
- * enough.
11
- */
12
- export type SliceFormat = 'json' | 'md' | 'text';
13
- export interface PageSliceResult {
14
- /**
15
- * Subset of `doc.blocks` whose layout lines intersect the requested
16
- * pages, in original document order. A block that spans multiple
17
- * requested pages appears exactly once.
18
- */
19
- blocks: Block[];
20
- /**
21
- * Per-block 1-based page indices for the included blocks. Present
22
- * only when `format === 'json'` so the JSON serializer's `_pageMeta`
23
- * field stays accurate after slicing. Always in the same order as
24
- * `blocks`.
25
- */
26
- pageMeta?: BlockPageMeta[];
27
- }
28
- /**
29
- * Select the blocks of `doc` that participate in any of the pages named
30
- * by `range`, optionally producing per-block page metadata for the JSON
31
- * format. Block ordering matches `doc.blocks` — the slicer never
32
- * reorders content, only filters it.
33
- *
34
- * Selection rule (per `docs/design/docs-cli.md` §5.2): include any block
35
- * whose layout has at least one line on a requested page. Blocks emit
36
- * whole — there is no mid-block cut. A block that straddles the
37
- * boundary between a requested page and a non-requested one still
38
- * appears in the result, in full.
39
- *
40
- * Blocks with no layout lines (e.g., zero-height collapsed blocks the
41
- * paginator omitted) drop out entirely; if a future block type needs
42
- * "always include" semantics, the caller should pre-tag those blocks
43
- * before calling this function.
44
- */
45
- export declare function sliceBlocksByPages(doc: Document, layout: PaginatedLayout, range: PageRange, format: SliceFormat): PageSliceResult;
@@ -1,61 +0,0 @@
1
- /**
2
- * Select the blocks of `doc` that participate in any of the pages named
3
- * by `range`, optionally producing per-block page metadata for the JSON
4
- * format. Block ordering matches `doc.blocks` — the slicer never
5
- * reorders content, only filters it.
6
- *
7
- * Selection rule (per `docs/design/docs-cli.md` §5.2): include any block
8
- * whose layout has at least one line on a requested page. Blocks emit
9
- * whole — there is no mid-block cut. A block that straddles the
10
- * boundary between a requested page and a non-requested one still
11
- * appears in the result, in full.
12
- *
13
- * Blocks with no layout lines (e.g., zero-height collapsed blocks the
14
- * paginator omitted) drop out entirely; if a future block type needs
15
- * "always include" semantics, the caller should pre-tag those blocks
16
- * before calling this function.
17
- */
18
- export function sliceBlocksByPages(doc, layout, range, format) {
19
- const wantedPages = range.pages;
20
- // Group all line pageIndices by blockIndex so we can both decide
21
- // inclusion and reuse the lookup for `pageMeta`. Walking pages in
22
- // order keeps the per-block `lines` list in document order.
23
- //
24
- // Duplicates are intentional: `BlockPageMeta.lines` is contractually
25
- // "one entry per layout line, in order" — a block with three lines
26
- // all on page 1 yields `[1, 1, 1]`. See `serialize/json.ts` for the
27
- // canonical contract; the consumer is meant to count entries to
28
- // recover the per-block line count, not treat the array as a set.
29
- const linesByBlock = new Map();
30
- for (const page of layout.pages) {
31
- for (const pl of page.lines) {
32
- const bucket = linesByBlock.get(pl.blockIndex);
33
- if (bucket)
34
- bucket.push(pl.pageIndex);
35
- else
36
- linesByBlock.set(pl.blockIndex, [pl.pageIndex]);
37
- }
38
- }
39
- const blocks = [];
40
- const pageMeta = [];
41
- for (let i = 0; i < doc.blocks.length; i++) {
42
- const lines = linesByBlock.get(i);
43
- if (!lines || lines.length === 0)
44
- continue;
45
- let intersects = false;
46
- for (const p of lines) {
47
- if (wantedPages.has(p)) {
48
- intersects = true;
49
- break;
50
- }
51
- }
52
- if (!intersects)
53
- continue;
54
- const block = doc.blocks[i];
55
- blocks.push(block);
56
- if (format === 'json') {
57
- pageMeta.push({ blockId: block.id, lines });
58
- }
59
- }
60
- return format === 'json' ? { blocks, pageMeta } : { blocks };
61
- }
@@ -1,13 +0,0 @@
1
- import { type Document, type PaginatedLayout, type TextMeasurer } from '@wafflebase/docs';
2
- /**
3
- * Run `computeLayout` + `paginateLayout` for a fetched `Document` using
4
- * the supplied measurer. The CLI calls this only when `--pages` is set
5
- * (or any other flag that needs page boundaries) — the bare `docs
6
- * content` JSON path skips pagination entirely.
7
- *
8
- * Page setup falls back to `DEFAULT_PAGE_SETUP` (US Letter portrait, 1in
9
- * margins) when the document doesn't carry one. Content width is the
10
- * effective page width minus left/right margins, matching what the
11
- * editor's `DocCanvas` computes.
12
- */
13
- export declare function paginateForCli(doc: Document, measurer: TextMeasurer): PaginatedLayout;
@@ -1,19 +0,0 @@
1
- import { computeLayout, paginateLayout, resolvePageSetup, getEffectiveDimensions, } from '@wafflebase/docs';
2
- /**
3
- * Run `computeLayout` + `paginateLayout` for a fetched `Document` using
4
- * the supplied measurer. The CLI calls this only when `--pages` is set
5
- * (or any other flag that needs page boundaries) — the bare `docs
6
- * content` JSON path skips pagination entirely.
7
- *
8
- * Page setup falls back to `DEFAULT_PAGE_SETUP` (US Letter portrait, 1in
9
- * margins) when the document doesn't carry one. Content width is the
10
- * effective page width minus left/right margins, matching what the
11
- * editor's `DocCanvas` computes.
12
- */
13
- export function paginateForCli(doc, measurer) {
14
- const pageSetup = resolvePageSetup(doc.pageSetup);
15
- const { width: paperWidth } = getEffectiveDimensions(pageSetup);
16
- const contentWidth = paperWidth - pageSetup.margins.left - pageSetup.margins.right;
17
- const { layout } = computeLayout(doc.blocks, measurer, contentWidth);
18
- return paginateLayout(layout, pageSetup);
19
- }
@@ -1,47 +0,0 @@
1
- import { type Document, type DocxImageFetcher, type PdfFontsOptions } from '@wafflebase/docs';
2
- import type { PageRange } from './page-range.js';
3
- /**
4
- * Options for the CLI PDF export pipeline.
5
- *
6
- * `pages` is the resolved selection from `parsePageRange`; when present
7
- * we render the full PDF first and then strip non-selected pages via
8
- * `pdf-lib` (cheaper than re-implementing a partial paint path inside
9
- * `PdfExporter`). The future plan is to push a `pageIndices` option
10
- * down into `PdfExporter` itself; for now the post-process is correct
11
- * but pays for layout/paint work it then throws away.
12
- *
13
- * `fontSources` lets tests inject in-memory buffers via `PdfFonts` so
14
- * `exportPdf` can run without hitting jsdelivr.
15
- */
16
- export interface CliPdfExportOptions {
17
- pages?: PageRange;
18
- /** Currently informational — `PdfExporter` always paints
19
- * header/footer when `doc.header`/`doc.footer` are present. The CLI
20
- * surfaces the flag for parity with `docs export --include-header-footer`
21
- * and to leave room for a future "strip header/footer" mode. */
22
- includeHeaderFooter?: boolean;
23
- fontSources?: PdfFontsOptions['sources'];
24
- /** Resolve and download image inlines so they can be embedded in the
25
- * PDF. Required when the document contains image inlines —
26
- * `PdfExporter` throws otherwise to avoid silently dropping content.
27
- * The CLI command builds one via `createImageFetcher` from the
28
- * configured server base; tests inject a stub that returns
29
- * in-memory bytes. Typed as `DocxImageFetcher` (a structural
30
- * `(url) => Promise<Blob>` alias) since the bare `ImageFetcher`
31
- * symbol from `pdf-image-painter` isn't re-exported. */
32
- imageFetcher?: DocxImageFetcher;
33
- }
34
- /**
35
- * Render `doc` to a PDF byte buffer using the same `PdfExporter` the
36
- * editor uses, with a `fontkit`-backed measurer in place of the browser
37
- * Canvas measurer. Korean fonts referenced by the document are
38
- * pre-loaded through `PdfFonts` and registered with the measurer so
39
- * pagination matches what the painter then draws.
40
- *
41
- * Latin text falls back to the measurer's coarse 0.5em estimate
42
- * (Helvetica/Times metrics aren't bundled). Pure-ASCII docs may
43
- * therefore line-break differently in the CLI than in the browser; the
44
- * paint output stays correct because `PdfPainter` uses pdf-lib's
45
- * StandardFonts at draw time.
46
- */
47
- export declare function exportPdf(doc: Document, opts?: CliPdfExportOptions): Promise<Uint8Array>;
@@ -1,95 +0,0 @@
1
- import { PDFDocument } from 'pdf-lib';
2
- import { PdfExporter, PdfFonts, scanFontsUsed, } from '@wafflebase/docs';
3
- import { FontkitMeasurer } from './fontkit-measurer.js';
4
- /**
5
- * Render `doc` to a PDF byte buffer using the same `PdfExporter` the
6
- * editor uses, with a `fontkit`-backed measurer in place of the browser
7
- * Canvas measurer. Korean fonts referenced by the document are
8
- * pre-loaded through `PdfFonts` and registered with the measurer so
9
- * pagination matches what the painter then draws.
10
- *
11
- * Latin text falls back to the measurer's coarse 0.5em estimate
12
- * (Helvetica/Times metrics aren't bundled). Pure-ASCII docs may
13
- * therefore line-break differently in the CLI than in the browser; the
14
- * paint output stays correct because `PdfPainter` uses pdf-lib's
15
- * StandardFonts at draw time.
16
- */
17
- export async function exportPdf(doc, opts = {}) {
18
- const usage = scanFontsUsed(doc);
19
- const fonts = new PdfFonts({ sources: opts.fontSources });
20
- const measurer = new FontkitMeasurer();
21
- await preloadKoreanFonts(fonts, measurer, usage);
22
- const blob = await PdfExporter.export(doc, {
23
- measurer,
24
- fonts,
25
- imageFetcher: opts.imageFetcher,
26
- });
27
- const fullBytes = new Uint8Array(await blob.arrayBuffer());
28
- if (!opts.pages || opts.pages.pages.size === 0) {
29
- return fullBytes;
30
- }
31
- return extractPages(fullBytes, opts.pages);
32
- }
33
- /**
34
- * Load every Korean variant `usage` reports as needed and register each
35
- * loaded buffer with the measurer under both the canonical `'Noto …'`
36
- * family name and the editor-side aliases (`바탕`, `Batang`) so inline
37
- * styles that pick a non-canonical family still hit a real font instead
38
- * of the fallback estimate.
39
- */
40
- async function preloadKoreanFonts(fonts, measurer, usage) {
41
- const variants = [];
42
- if (usage.needsKR) {
43
- variants.push({
44
- key: 'kr-sans-regular',
45
- weight: 'normal',
46
- families: ['Noto Sans KR'],
47
- });
48
- }
49
- if (usage.needsKR && usage.needsBold) {
50
- variants.push({
51
- key: 'kr-sans-bold',
52
- weight: 'bold',
53
- families: ['Noto Sans KR'],
54
- });
55
- }
56
- if (usage.needsKRSerif) {
57
- variants.push({
58
- key: 'kr-serif-regular',
59
- weight: 'normal',
60
- families: ['Noto Serif KR', '바탕', 'Batang'],
61
- });
62
- }
63
- if (usage.needsKRSerif && usage.needsBold) {
64
- variants.push({
65
- key: 'kr-serif-bold',
66
- weight: 'bold',
67
- families: ['Noto Serif KR', '바탕', 'Batang'],
68
- });
69
- }
70
- for (const v of variants) {
71
- const buf = await fonts.load(v.key);
72
- for (const family of v.families) {
73
- measurer.register(family, v.weight, 'normal', buf);
74
- }
75
- }
76
- }
77
- /**
78
- * Keep only the pages named by `range` and return the new PDF bytes.
79
- *
80
- * Iterates from the highest index down so each `removePage(idx)` call
81
- * doesn't shift the indices of pages we haven't visited yet. Pages in
82
- * `range.pages` are 1-based; pdf-lib indexes from 0.
83
- */
84
- async function extractPages(bytes, range) {
85
- const pdf = await PDFDocument.load(bytes);
86
- const total = pdf.getPageCount();
87
- for (let i = total - 1; i >= 0; i--) {
88
- const oneBased = i + 1;
89
- if (!range.pages.has(oneBased)) {
90
- pdf.removePage(i);
91
- }
92
- }
93
- const out = await pdf.save();
94
- return out;
95
- }
@@ -1,37 +0,0 @@
1
- export interface WriteBinaryOptions {
2
- /** Allow overwriting an existing file. Without this flag, `writeBinary`
3
- * refuses to clobber and throws. Mirrors `--force` on the CLI. */
4
- force?: boolean;
5
- /** Suppress informational stderr output. The bytes themselves still
6
- * reach stdout/the file regardless of this flag. */
7
- quiet?: boolean;
8
- }
9
- /**
10
- * Write `bytes` to `target`, where `target` is either a filesystem path
11
- * or `'-'` for stdout. The default behavior refuses to overwrite an
12
- * existing file because exports are usually irreversible (the source
13
- * doc is the canonical store but a stale local copy can still mislead).
14
- *
15
- * The function is split out from the command action so binary output
16
- * paths can be unit-tested without spawning the CLI binary; the action
17
- * supplies real `process.stdout.write` and the IO surface forwards it.
18
- *
19
- * The default `writeFile` honors `force` via the `'w'` (overwrite) /
20
- * `'wx'` (exclusive create) `fs` flags, folding the existence check
21
- * and the write into a single syscall — avoids a TOCTOU window where
22
- * something else could create the file between the check and the
23
- * write.
24
- */
25
- export declare function writeBinary(bytes: Uint8Array, target: string, opts?: WriteBinaryOptions, io?: BinaryIO): void;
26
- export interface BinaryIO {
27
- stdout: (bytes: Uint8Array) => void;
28
- stderr: (line: string) => void;
29
- /**
30
- * Write `bytes` to `path`. When `force` is `false`, the
31
- * implementation MUST refuse to overwrite an existing file and
32
- * throw — the production `defaultBinaryIO` does this via the
33
- * exclusive-create `'wx'` flag.
34
- */
35
- writeFile: (path: string, bytes: Uint8Array, force: boolean) => void;
36
- }
37
- export declare const defaultBinaryIO: BinaryIO;
@@ -1,47 +0,0 @@
1
- import { writeFileSync } from 'node:fs';
2
- /**
3
- * Write `bytes` to `target`, where `target` is either a filesystem path
4
- * or `'-'` for stdout. The default behavior refuses to overwrite an
5
- * existing file because exports are usually irreversible (the source
6
- * doc is the canonical store but a stale local copy can still mislead).
7
- *
8
- * The function is split out from the command action so binary output
9
- * paths can be unit-tested without spawning the CLI binary; the action
10
- * supplies real `process.stdout.write` and the IO surface forwards it.
11
- *
12
- * The default `writeFile` honors `force` via the `'w'` (overwrite) /
13
- * `'wx'` (exclusive create) `fs` flags, folding the existence check
14
- * and the write into a single syscall — avoids a TOCTOU window where
15
- * something else could create the file between the check and the
16
- * write.
17
- */
18
- export function writeBinary(bytes, target, opts = {}, io = defaultBinaryIO) {
19
- if (target === '-') {
20
- io.stdout(bytes);
21
- return;
22
- }
23
- io.writeFile(target, bytes, opts.force === true);
24
- if (!opts.quiet)
25
- io.stderr(`Wrote ${bytes.length} bytes to ${target}`);
26
- }
27
- export const defaultBinaryIO = {
28
- stdout: (bytes) => {
29
- process.stdout.write(bytes);
30
- },
31
- stderr: (line) => {
32
- console.error(line);
33
- },
34
- writeFile: (path, bytes, force) => {
35
- try {
36
- writeFileSync(path, bytes, { flag: force ? 'w' : 'wx' });
37
- }
38
- catch (err) {
39
- if (err instanceof Error &&
40
- 'code' in err &&
41
- err.code === 'EEXIST') {
42
- throw new Error(`Refusing to overwrite "${path}". Pass --force to allow overwrite.`);
43
- }
44
- throw err;
45
- }
46
- },
47
- };
@@ -1,4 +0,0 @@
1
- /**
2
- * Format data as CSV. Accepts arrays or single objects.
3
- */
4
- export declare function formatCsv(data: unknown): string;
@@ -1,25 +0,0 @@
1
- /**
2
- * Format data as CSV. Accepts arrays or single objects.
3
- */
4
- export function formatCsv(data) {
5
- const rows = Array.isArray(data)
6
- ? data
7
- : data !== null && typeof data === 'object'
8
- ? [data]
9
- : [];
10
- if (rows.length === 0)
11
- return '';
12
- const keys = Object.keys(rows[0]);
13
- const csvEscape = (val) => {
14
- const s = val !== null && typeof val === 'object'
15
- ? JSON.stringify(val)
16
- : String(val ?? '');
17
- if (s.includes(',') || s.includes('"') || s.includes('\n')) {
18
- return `"${s.replace(/"/g, '""')}"`;
19
- }
20
- return s;
21
- };
22
- const header = keys.map(csvEscape).join(',');
23
- const lines = rows.map((row) => keys.map((k) => csvEscape(row[k])).join(','));
24
- return [header, ...lines].join('\n');
25
- }
@@ -1,4 +0,0 @@
1
- export type OutputFormat = 'json' | 'table' | 'csv';
2
- export declare function format(data: unknown, fmt: OutputFormat): string;
3
- export declare function output(data: unknown, fmt: OutputFormat, quiet: boolean): void;
4
- export declare function outputError(error: unknown, quiet: boolean): void;