- Export text and basic shapes as editable PowerPoint elements - Rasterize unsupported geometry, effects, masks, and clipped content for fidelity - Support app and CLI exports across multiple pages Co-authored-by: TKman <102001532+greekr4@users.noreply.github.com>
264 lines
6.8 KiB
TypeScript
264 lines
6.8 KiB
TypeScript
import { expect, setDefaultTimeout, test } from 'bun:test'
|
|
import { mkdtemp } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
import { unzipSync } from 'fflate'
|
|
|
|
import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core/io'
|
|
|
|
import { runOpenPencilCLI } from '#tests/helpers/cli'
|
|
import { createRect, firstPageId, makeSceneGraph } from '#tests/helpers/scene'
|
|
|
|
setDefaultTimeout(30_000)
|
|
|
|
const io = new IORegistry(BUILTIN_IO_FORMATS)
|
|
|
|
async function createFigFixture() {
|
|
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-export-cli-'))
|
|
const figPath = join(dir, 'card.fig')
|
|
const graph = makeSceneGraph('Export Page')
|
|
const firstFrame = graph.createNode('FRAME', firstPageId(graph), {
|
|
name: 'First slide',
|
|
width: 1280,
|
|
height: 720
|
|
})
|
|
const rect = createRect(graph, firstFrame.id, {
|
|
name: 'Export Card',
|
|
x: 0,
|
|
y: 0,
|
|
width: 160,
|
|
height: 80
|
|
})
|
|
rect.layoutMode = 'HORIZONTAL'
|
|
rect.itemSpacing = 8
|
|
rect.paddingLeft = 16
|
|
rect.paddingRight = 16
|
|
rect.paddingTop = 16
|
|
rect.paddingBottom = 16
|
|
rect.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 } }]
|
|
|
|
const secondPage = graph.addPage('Second Page')
|
|
const secondFrame = graph.createNode('FRAME', secondPage.id, {
|
|
name: 'Second slide',
|
|
width: 1280,
|
|
height: 720
|
|
})
|
|
createRect(graph, secondFrame.id, {
|
|
name: 'Second Card',
|
|
x: 0,
|
|
y: 0,
|
|
width: 120,
|
|
height: 60
|
|
})
|
|
|
|
const result = await io.writeDocument('fig', graph)
|
|
await Bun.write(figPath, result.data as Uint8Array)
|
|
return { dir, figPath }
|
|
}
|
|
|
|
test('FIG export preserves the whole document by default', async () => {
|
|
const { dir, figPath } = await createFigFixture()
|
|
const output = join(dir, 'whole.fig')
|
|
|
|
const { stdout, stderr, exitCode } = await runOpenPencilCLI([
|
|
'export',
|
|
figPath,
|
|
'--format',
|
|
'fig',
|
|
'--output',
|
|
output
|
|
])
|
|
|
|
expect(stderr).toBe('')
|
|
expect(exitCode).toBe(0)
|
|
expect(stdout).toContain('Target: whole document')
|
|
|
|
const { graph } = await io.readDocument({
|
|
name: output,
|
|
data: new Uint8Array(await Bun.file(output).arrayBuffer())
|
|
})
|
|
expect(graph.getPages()).toHaveLength(3)
|
|
expect(graph.getPages().map((page) => page.name)).toContain('Second Page')
|
|
})
|
|
|
|
test('PPTX export includes slides from every page by default', async () => {
|
|
const { dir, figPath } = await createFigFixture()
|
|
const output = join(dir, 'whole.pptx')
|
|
|
|
const { stdout, stderr, exitCode } = await runOpenPencilCLI([
|
|
'export',
|
|
figPath,
|
|
'--format',
|
|
'pptx',
|
|
'--output',
|
|
output
|
|
])
|
|
|
|
expect(stderr).toBe('')
|
|
expect(exitCode).toBe(0)
|
|
expect(stdout).toContain('Target: whole document')
|
|
|
|
const files = unzipSync(new Uint8Array(await Bun.file(output).arrayBuffer()))
|
|
expect(files['ppt/slides/slide1.xml']).toBeDefined()
|
|
expect(files['ppt/slides/slide2.xml']).toBeDefined()
|
|
expect(files['ppt/slides/slide3.xml']).toBeUndefined()
|
|
})
|
|
|
|
test('FIG export requires an explicit page for a partial archive', async () => {
|
|
const { dir, figPath } = await createFigFixture()
|
|
const output = join(dir, 'page.fig')
|
|
|
|
const { stderr, exitCode } = await runOpenPencilCLI([
|
|
'export',
|
|
figPath,
|
|
'--format',
|
|
'fig',
|
|
'--page',
|
|
'Second Page',
|
|
'--output',
|
|
output
|
|
])
|
|
|
|
expect(stderr).toBe('')
|
|
expect(exitCode).toBe(0)
|
|
|
|
const { graph } = await io.readDocument({
|
|
name: output,
|
|
data: new Uint8Array(await Bun.file(output).arrayBuffer())
|
|
})
|
|
expect(graph.getPages().map((page) => page.name)).toEqual(['Second Page'])
|
|
})
|
|
|
|
test('export CLI writes HTML with inline styles by default', async () => {
|
|
const { dir, figPath } = await createFigFixture()
|
|
const output = join(dir, 'card.html')
|
|
|
|
const { stderr, exitCode } = await runOpenPencilCLI([
|
|
'export',
|
|
figPath,
|
|
'--format',
|
|
'html',
|
|
'--output',
|
|
output
|
|
])
|
|
|
|
expect(stderr).toBe('')
|
|
expect(exitCode).toBe(0)
|
|
|
|
const html = await Bun.file(output).text()
|
|
expect(html).toContain('data-open-pencil-node-id')
|
|
expect(html).toContain('style=')
|
|
expect(html).toContain('display: flex')
|
|
})
|
|
|
|
test('export CLI can write HTML styles as Tailwind classes', async () => {
|
|
const { dir, figPath } = await createFigFixture()
|
|
const output = join(dir, 'card-tailwind.html')
|
|
|
|
const { stderr, exitCode } = await runOpenPencilCLI([
|
|
'export',
|
|
figPath,
|
|
'--format',
|
|
'html',
|
|
'--css',
|
|
'tailwind',
|
|
'--output',
|
|
output
|
|
])
|
|
|
|
expect(stderr).toBe('')
|
|
expect(exitCode).toBe(0)
|
|
|
|
const html = await Bun.file(output).text()
|
|
expect(html).toContain('data-open-pencil-node-id')
|
|
expect(html).toContain('class="')
|
|
expect(html).toContain('flex')
|
|
expect(html).not.toContain('style=')
|
|
})
|
|
|
|
test('export CLI can write standalone HTML', async () => {
|
|
const { dir, figPath } = await createFigFixture()
|
|
const output = join(dir, 'card-standalone.html')
|
|
|
|
const { stderr, exitCode } = await runOpenPencilCLI([
|
|
'export',
|
|
figPath,
|
|
'--format',
|
|
'html',
|
|
'--html',
|
|
'standalone',
|
|
'--output',
|
|
output
|
|
])
|
|
|
|
expect(stderr).toBe('')
|
|
expect(exitCode).toBe(0)
|
|
|
|
const html = await Bun.file(output).text()
|
|
expect(html).toContain('<!doctype html>')
|
|
expect(html).toContain('data-open-pencil-html="standalone"')
|
|
expect(html).toContain('position:relative')
|
|
expect(html).toContain('position: absolute')
|
|
expect(html).not.toContain('@tailwindcss/browser@4')
|
|
})
|
|
|
|
test('export CLI precompiles Tailwind CSS for standalone Tailwind HTML', async () => {
|
|
const { dir, figPath } = await createFigFixture()
|
|
const output = join(dir, 'card-standalone-tailwind.html')
|
|
|
|
const { stderr, exitCode } = await runOpenPencilCLI([
|
|
'export',
|
|
figPath,
|
|
'--format',
|
|
'html',
|
|
'--html',
|
|
'standalone',
|
|
'--css',
|
|
'tailwind',
|
|
'--output',
|
|
output
|
|
])
|
|
|
|
expect(stderr).toBe('')
|
|
expect(exitCode).toBe(0)
|
|
|
|
const html = await Bun.file(output).text()
|
|
expect(html).toContain('<!doctype html>')
|
|
expect(html).toContain('<style>')
|
|
expect(html).toContain('class="')
|
|
expect(html).toContain('.flex')
|
|
expect(html).not.toContain('@tailwindcss/browser@4')
|
|
})
|
|
|
|
test('export CLI can write external standalone HTML assets', async () => {
|
|
const { dir, figPath } = await createFigFixture()
|
|
const output = join(dir, 'card-external.html')
|
|
|
|
const { stderr, exitCode } = await runOpenPencilCLI([
|
|
'export',
|
|
figPath,
|
|
'--format',
|
|
'html',
|
|
'--html',
|
|
'standalone',
|
|
'--css',
|
|
'tailwind',
|
|
'--assets',
|
|
'external',
|
|
'--output',
|
|
output
|
|
])
|
|
|
|
expect(stderr).toBe('')
|
|
expect(exitCode).toBe(0)
|
|
|
|
const html = await Bun.file(output).text()
|
|
const cssPath = join(dir, 'card-external.assets', 'openpencil.css')
|
|
const css = await Bun.file(cssPath).text()
|
|
expect(html).toContain('<link rel="stylesheet" href="card-external.assets/openpencil.css">')
|
|
expect(html).not.toContain('<style>')
|
|
expect(css).toContain('.flex')
|
|
expect(css).toContain('.op-stage')
|
|
})
|