feat(cli): add export font policy
This commit is contained in:
parent
c32ff03d03
commit
f580d8a7e8
|
|
@ -14,6 +14,7 @@ import {
|
|||
|
||||
import { isAppMode, requireFile, rpc } from '#cli/app-client'
|
||||
import { appTargetOptions, appTargetRPCArgs } from '#cli/app-target'
|
||||
import { applyExportFontPolicy, exportFontRoots, FONT_POLICIES } from '#cli/export-font-policy'
|
||||
import { ok, printError } from '#cli/format'
|
||||
import { loadDocument, populateDocumentPage, populateWholeDocument } from '#cli/headless'
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ interface ExportArgs {
|
|||
css: string
|
||||
assets: string
|
||||
fonts: string
|
||||
'font-policy': string
|
||||
thumbnail?: boolean
|
||||
width: string
|
||||
height: string
|
||||
|
|
@ -210,6 +212,17 @@ async function exportFromFile(format: string, args: ExportArgs) {
|
|||
const target = args.node
|
||||
? { scope: 'node' as const, nodeId: args.node }
|
||||
: { scope: 'page' as const, pageId: page.id }
|
||||
await applyExportFontPolicy(
|
||||
graph,
|
||||
exportFontRoots(
|
||||
args.node,
|
||||
page.id,
|
||||
pages.map((candidate) => candidate.id),
|
||||
wholeDocument
|
||||
),
|
||||
format,
|
||||
args['font-policy']
|
||||
)
|
||||
|
||||
if (args.thumbnail) {
|
||||
printError('Thumbnail export is not supported by the shared file export path yet.')
|
||||
|
|
@ -312,6 +325,11 @@ export default defineCommand({
|
|||
description: 'HTML font output: assets or none (default: none)',
|
||||
default: 'none'
|
||||
},
|
||||
'font-policy': {
|
||||
type: 'string',
|
||||
description: 'Raster/PDF font policy: warn, strict, or allow (default: warn)',
|
||||
default: 'warn'
|
||||
},
|
||||
thumbnail: { type: 'boolean', description: 'Export page thumbnail instead of full render' },
|
||||
width: { type: 'string', description: 'Thumbnail width (default: 1920)', default: '1920' },
|
||||
height: { type: 'string', description: 'Thumbnail height (default: 1080)', default: '1080' },
|
||||
|
|
@ -351,6 +369,11 @@ export default defineCommand({
|
|||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!FONT_POLICIES.has(args['font-policy'])) {
|
||||
printError(`Invalid font policy "${args['font-policy']}". Use warn, strict, or allow.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (isAppMode(args.file)) {
|
||||
await exportViaApp(format, args)
|
||||
} else {
|
||||
|
|
|
|||
34
packages/cli/src/export-font-policy.ts
Normal file
34
packages/cli/src/export-font-policy.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { prepareGraphFonts } from '@open-pencil/core/text'
|
||||
import type { SceneGraph } from '@open-pencil/scene-graph'
|
||||
|
||||
import { fail, printError } from '#cli/format'
|
||||
|
||||
export const FONT_POLICIES = new Set(['allow', 'strict', 'warn'])
|
||||
|
||||
export async function applyExportFontPolicy(
|
||||
graph: SceneGraph,
|
||||
roots: string[],
|
||||
format: string,
|
||||
policy: string
|
||||
): Promise<void> {
|
||||
if (policy === 'allow' || !['PNG', 'JPG', 'WEBP', 'PDF'].includes(format)) return
|
||||
const status = await prepareGraphFonts(graph, roots)
|
||||
if (status.faithful) return
|
||||
const names = status.issues.map((face) => `${face.family} ${face.style}`).join(', ')
|
||||
const message = `Font substitution: ${names}`
|
||||
if (policy === 'strict') {
|
||||
printError(message)
|
||||
process.exit(1)
|
||||
}
|
||||
console.error(fail(message))
|
||||
}
|
||||
|
||||
export function exportFontRoots(
|
||||
nodeId: string | undefined,
|
||||
pageId: string,
|
||||
pageIds: string[],
|
||||
wholeDocument: boolean
|
||||
): string[] {
|
||||
if (nodeId) return [nodeId]
|
||||
return wholeDocument ? pageIds : [pageId]
|
||||
}
|
||||
31
tests/engine/cli/export-font-policy.test.ts
Normal file
31
tests/engine/cli/export-font-policy.test.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { expect, test } from 'bun:test'
|
||||
|
||||
import { SceneGraph } from '@open-pencil/scene-graph'
|
||||
|
||||
import { FONT_POLICIES, applyExportFontPolicy, exportFontRoots } from '#cli/export-font-policy'
|
||||
|
||||
function graphWithMissingFont(): { graph: SceneGraph; pageId: string } {
|
||||
const graph = new SceneGraph()
|
||||
const page = graph.getPages()[0]
|
||||
graph.createNode('TEXT', page.id, {
|
||||
text: 'Hello',
|
||||
fontFamily: 'Definitely Missing Font',
|
||||
fontWeight: 400
|
||||
})
|
||||
return { graph, pageId: page.id }
|
||||
}
|
||||
|
||||
test('font policy roots match node, page, and document export scopes', () => {
|
||||
expect(exportFontRoots('node', 'page', ['page', 'page-2'], true)).toEqual(['node'])
|
||||
expect(exportFontRoots(undefined, 'page', ['page', 'page-2'], false)).toEqual(['page'])
|
||||
expect(exportFontRoots(undefined, 'page', ['page', 'page-2'], true)).toEqual(['page', 'page-2'])
|
||||
})
|
||||
|
||||
test('font policy values stay bounded', () => {
|
||||
expect([...FONT_POLICIES].sort()).toEqual(['allow', 'strict', 'warn'])
|
||||
})
|
||||
|
||||
test('allow policy skips font preparation', async () => {
|
||||
const { graph, pageId } = graphWithMissingFont()
|
||||
await expect(applyExportFontPolicy(graph, [pageId], 'PNG', 'allow')).resolves.toBeUndefined()
|
||||
})
|
||||
Loading…
Reference in a new issue