* refactor(codegen): share syntax-tree code generation between exporters dom-css printed Tailwind JSX with its own esrap JSX builders and kept TypeScript template helpers under its Storybook export. The OpenPencil JSX exporter needs the same JSX builders, and design-jsx and dom-css may not depend on each other. @open-pencil/codegen holds both: es for ESTree templates and modules (moved from dom-css) and jsx for JSX elements, attributes, text, and printing, including the literal rules that keep exported strings from being reinterpreted. dom-css no longer depends on acorn and esrap directly. * refactor(design-jsx): print JSX export as syntax trees sceneNodeToJSX concatenated strings with hand-written escaping and indentation. It now collects typed props (moved to export/props.ts) and prints them with @open-pencil/codegen's JSX builders. Output is unchanged except for text: special characters print as a string expression instead of entities, and multi-line text keeps its line breaks, which the old line-splitting lost on render. * docs(codegen): fix package metadata and dependency rules codegen's repository.directory still named design-jsx, the README mentioned es without showing it, and the design-jsx and dom-css guides still said they depend only on scene-graph. * test(core): move the JSX export round-trips to the design-jsx test home Covers tabs in layer names and text, which JSX keeps as written.
151 lines
5.5 KiB
TypeScript
151 lines
5.5 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
|
|
|
import { renderJSX } from '@open-pencil/core'
|
|
import { sceneNodeToJSX } from '@open-pencil/design-jsx'
|
|
|
|
import { getNodeOrThrow, childIdAt } from '#tests/helpers/assert'
|
|
import { makeSceneGraph } from '#tests/helpers/scene'
|
|
|
|
describe('text props round-trip', () => {
|
|
it('lineHeight renders and exports', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text color="#000" lineHeight={24}>Hello</Text>')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.lineHeight).toBe(24)
|
|
const jsx = sceneNodeToJSX(n.id, g)
|
|
expect(jsx).toContain('lineHeight={24}')
|
|
})
|
|
|
|
it('letterSpacing renders and exports', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text color="#000" letterSpacing={2}>Spaced</Text>')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.letterSpacing).toBe(2)
|
|
const jsx = sceneNodeToJSX(n.id, g)
|
|
expect(jsx).toContain('letterSpacing={2}')
|
|
})
|
|
|
|
it('textDecoration renders and exports', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text color="#000" textDecoration="underline">Link</Text>')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.textDecoration).toBe('UNDERLINE')
|
|
const jsx = sceneNodeToJSX(n.id, g)
|
|
expect(jsx).toContain('textDecoration="underline"')
|
|
})
|
|
|
|
it('textCase renders and exports', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text color="#000" textCase="upper">label</Text>')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.textCase).toBe('UPPER')
|
|
const jsx = sceneNodeToJSX(n.id, g)
|
|
expect(jsx).toContain('textCase="upper"')
|
|
})
|
|
|
|
it('maxLines renders with truncation', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text color="#000" maxLines={2}>Long text here</Text>')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.maxLines).toBe(2)
|
|
expect(n.textTruncation).toBe('ENDING')
|
|
const jsx = sceneNodeToJSX(n.id, g)
|
|
expect(jsx).toContain('maxLines={2}')
|
|
})
|
|
|
|
it('truncate without maxLines', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text color="#000" truncate>Overflow</Text>')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.textTruncation).toBe('ENDING')
|
|
const jsx = sceneNodeToJSX(n.id, g)
|
|
expect(jsx).toContain('truncate')
|
|
})
|
|
|
|
it('accepts Figma-style text aliases', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(
|
|
g,
|
|
'<Text text="Alias text" fontSize={24} fontFamily="Inter" fontWeight={700} fill="#123456" textHorizontalAlignment="CENTER" textVerticalAlignment="CENTER" />'
|
|
)
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.text).toBe('Alias text')
|
|
expect(n.fontSize).toBe(24)
|
|
expect(n.fontFamily).toBe('Inter')
|
|
expect(n.fontWeight).toBe(700)
|
|
expect(n.textAlignHorizontal).toBe('CENTER')
|
|
expect(n.textAlignVertical).toBe('CENTER')
|
|
expect(n.fills[0]?.type).toBe('SOLID')
|
|
})
|
|
|
|
it('accepts characters as a text content alias', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text characters="Characters text" color="#000" />')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.text).toBe('Characters text')
|
|
})
|
|
|
|
it('prefers text children over text prop alias', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text text="Prop text" color="#000">Child text</Text>')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.text).toBe('Child text')
|
|
})
|
|
|
|
it('defaults omit text props', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(g, '<Text color="#000">Plain</Text>')
|
|
const n = getNodeOrThrow(g, result.id)
|
|
expect(n.lineHeight).toBeNull()
|
|
expect(n.letterSpacing).toBe(0)
|
|
expect(n.textDecoration).toBe('NONE')
|
|
expect(n.textCase).toBe('ORIGINAL')
|
|
expect(n.maxLines).toBeNull()
|
|
const jsx = sceneNodeToJSX(n.id, g)
|
|
expect(jsx).not.toContain('lineHeight')
|
|
expect(jsx).not.toContain('letterSpacing')
|
|
expect(jsx).not.toContain('textDecoration')
|
|
expect(jsx).not.toContain('textCase')
|
|
expect(jsx).not.toContain('maxLines')
|
|
expect(jsx).not.toContain('truncate')
|
|
})
|
|
|
|
it('text w="fill" in flex="col" exports as w="fill" not w={computed}', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(
|
|
g,
|
|
`
|
|
<Frame name="Card" flex="col" w={300} p={20}>
|
|
<Text name="Title" size={22} weight="bold" color="#111" w="fill">Hello World</Text>
|
|
</Frame>
|
|
`
|
|
)
|
|
const card = getNodeOrThrow(g, result.id)
|
|
const title = getNodeOrThrow(g, childIdAt(card, 0))
|
|
expect(title.layoutAlignSelf).toBe('STRETCH')
|
|
expect(title.textAutoResize).toBe('HEIGHT')
|
|
const jsx = sceneNodeToJSX(title.id, g)
|
|
expect(jsx).toContain('w="fill"')
|
|
expect(jsx).not.toMatch(/w=\{/)
|
|
})
|
|
|
|
it('text grow={1} in flex="row" exports as grow not w={computed}', async () => {
|
|
const g = makeSceneGraph()
|
|
const [result] = await renderJSX(
|
|
g,
|
|
`
|
|
<Frame name="Row" flex="row" w={300}>
|
|
<Text name="Label" color="#999" w={60}>Label</Text>
|
|
<Text name="Value" color="#111" w="fill">Some value text</Text>
|
|
</Frame>
|
|
`
|
|
)
|
|
const row = getNodeOrThrow(g, result.id)
|
|
const value = getNodeOrThrow(g, childIdAt(row, 1))
|
|
expect(value.layoutGrow).toBe(1)
|
|
const jsx = sceneNodeToJSX(value.id, g)
|
|
expect(jsx).toContain('grow={1}')
|
|
expect(jsx).not.toMatch(/\bw=\{\d+\}/)
|
|
})
|
|
})
|