@pyreon/document-primitives 0.12.12 → 0.12.14
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/document-primitives",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.14",
|
|
4
4
|
"description": "Rocketstyle document components — render in browser, export to 18 formats",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,32 +35,34 @@
|
|
|
35
35
|
"build:watch": "bun run vl_rolldown_build-watch",
|
|
36
36
|
"lint": "oxlint .",
|
|
37
37
|
"test": "vitest run",
|
|
38
|
+
"test:browser": "vitest run --config ./vitest.browser.config.ts",
|
|
38
39
|
"test:coverage": "vitest run --coverage",
|
|
39
40
|
"test:watch": "vitest",
|
|
40
41
|
"typecheck": "tsc --noEmit"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@pyreon/connector-document": "^0.12.
|
|
44
|
+
"@pyreon/connector-document": "^0.12.14"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
|
-
"@pyreon/core": "^0.12.
|
|
47
|
-
"@pyreon/elements": "^0.12.
|
|
48
|
-
"@pyreon/reactivity": "^0.12.
|
|
49
|
-
"@pyreon/rocketstyle": "^0.12.
|
|
50
|
-
"@pyreon/runtime-dom": "^0.12.
|
|
51
|
-
"@pyreon/styler": "^0.12.
|
|
47
|
+
"@pyreon/core": "^0.12.14",
|
|
48
|
+
"@pyreon/elements": "^0.12.14",
|
|
49
|
+
"@pyreon/reactivity": "^0.12.14",
|
|
50
|
+
"@pyreon/rocketstyle": "^0.12.14",
|
|
51
|
+
"@pyreon/runtime-dom": "^0.12.14",
|
|
52
|
+
"@pyreon/styler": "^0.12.14",
|
|
52
53
|
"@pyreon/test-utils": "^0.12.10",
|
|
53
|
-
"@pyreon/typescript": "^0.12.
|
|
54
|
-
"@pyreon/ui-core": "^0.12.
|
|
54
|
+
"@pyreon/typescript": "^0.12.14",
|
|
55
|
+
"@pyreon/ui-core": "^0.12.14",
|
|
56
|
+
"@vitest/browser-playwright": "^4.1.4",
|
|
55
57
|
"@vitus-labs/tools-rolldown": "^1.15.4"
|
|
56
58
|
},
|
|
57
59
|
"peerDependencies": {
|
|
58
|
-
"@pyreon/core": "^0.12.
|
|
59
|
-
"@pyreon/document": "^0.12.
|
|
60
|
-
"@pyreon/elements": "^0.12.
|
|
61
|
-
"@pyreon/rocketstyle": "^0.12.
|
|
62
|
-
"@pyreon/styler": "^0.12.
|
|
63
|
-
"@pyreon/ui-core": "^0.12.
|
|
60
|
+
"@pyreon/core": "^0.12.14",
|
|
61
|
+
"@pyreon/document": "^0.12.14",
|
|
62
|
+
"@pyreon/elements": "^0.12.14",
|
|
63
|
+
"@pyreon/rocketstyle": "^0.12.14",
|
|
64
|
+
"@pyreon/styler": "^0.12.14",
|
|
65
|
+
"@pyreon/ui-core": "^0.12.14"
|
|
64
66
|
},
|
|
65
67
|
"engines": {
|
|
66
68
|
"node": ">= 22"
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { h } from '@pyreon/core'
|
|
2
|
+
import { extractDocumentTree } from '@pyreon/connector-document'
|
|
3
|
+
import { signal } from '@pyreon/reactivity'
|
|
4
|
+
import { mountInBrowser } from '@pyreon/test-utils/browser'
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
6
|
+
import DocCode from '../primitives/DocCode'
|
|
7
|
+
import DocDocument from '../primitives/DocDocument'
|
|
8
|
+
import DocHeading from '../primitives/DocHeading'
|
|
9
|
+
import DocImage from '../primitives/DocImage'
|
|
10
|
+
import DocLink from '../primitives/DocLink'
|
|
11
|
+
import DocList from '../primitives/DocList'
|
|
12
|
+
import DocListItem from '../primitives/DocListItem'
|
|
13
|
+
import DocSection from '../primitives/DocSection'
|
|
14
|
+
import DocTable from '../primitives/DocTable'
|
|
15
|
+
import DocText from '../primitives/DocText'
|
|
16
|
+
|
|
17
|
+
// Real-browser smoke suite for @pyreon/document-primitives.
|
|
18
|
+
//
|
|
19
|
+
// The contract under test here is the one PR #197 fixed: when you pass a real
|
|
20
|
+
// rocketstyle-wrapped primitive (not a hand-constructed mock vnode) through
|
|
21
|
+
// `extractDocumentTree`, the extractor must invoke the component to reach the
|
|
22
|
+
// post-attrs vnode where `_documentProps` actually lives. Before PR #197,
|
|
23
|
+
// every real primitive silently dropped its metadata during export.
|
|
24
|
+
//
|
|
25
|
+
// The existing unit tests in `connector-document/src/__tests__/` use a
|
|
26
|
+
// hand-constructed `DocDocLike` function. This suite closes the gap by
|
|
27
|
+
// using the ACTUAL `DocDocument` primitive with real rocketstyle runtime,
|
|
28
|
+
// in a real browser.
|
|
29
|
+
|
|
30
|
+
describe('document-primitives in real browser', () => {
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
// Each test cleans up its own mount; extract-only tests have nothing to do.
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('extracts title + author from a real DocDocument vnode (PR #197 regression)', () => {
|
|
36
|
+
const vnode = h(DocDocument, { title: 'Resume', author: 'Alice' })
|
|
37
|
+
const tree = extractDocumentTree(vnode)
|
|
38
|
+
|
|
39
|
+
expect(tree.type).toBe('document')
|
|
40
|
+
expect(tree.props.title).toBe('Resume')
|
|
41
|
+
expect(tree.props.author).toBe('Alice')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('resolves reactive accessor props at extraction time (live signal reads)', () => {
|
|
45
|
+
const name = signal('Alice')
|
|
46
|
+
const vnode = h(DocDocument, {
|
|
47
|
+
title: () => `${name()} — Resume`,
|
|
48
|
+
author: () => name(),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const first = extractDocumentTree(vnode)
|
|
52
|
+
expect(first.props.title).toBe('Alice — Resume')
|
|
53
|
+
expect(first.props.author).toBe('Alice')
|
|
54
|
+
|
|
55
|
+
name.set('Bob')
|
|
56
|
+
const second = extractDocumentTree(vnode)
|
|
57
|
+
expect(second.props.title).toBe('Bob — Resume')
|
|
58
|
+
expect(second.props.author).toBe('Bob')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('renders a nested DocDocument tree to real DOM and extracts the same tree', () => {
|
|
62
|
+
const vnode = h(
|
|
63
|
+
DocDocument,
|
|
64
|
+
{ title: 'Report' },
|
|
65
|
+
h(DocHeading, { level: 'h1' }, 'Q1 Results'),
|
|
66
|
+
h(DocText, null, 'Revenue grew 12%.'),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const { container, unmount } = mountInBrowser(vnode)
|
|
70
|
+
// Real browser renders the rocketstyle-wrapped elements.
|
|
71
|
+
expect(container.textContent).toContain('Q1 Results')
|
|
72
|
+
expect(container.textContent).toContain('Revenue grew 12%.')
|
|
73
|
+
|
|
74
|
+
// Same vnode drives the export path.
|
|
75
|
+
const tree = extractDocumentTree(vnode)
|
|
76
|
+
expect(tree.type).toBe('document')
|
|
77
|
+
expect(tree.props.title).toBe('Report')
|
|
78
|
+
expect(tree.children.length).toBeGreaterThanOrEqual(2)
|
|
79
|
+
const nodeChildren = tree.children.filter(
|
|
80
|
+
(c): c is Exclude<typeof c, string> => typeof c !== 'string',
|
|
81
|
+
)
|
|
82
|
+
expect(nodeChildren.some((c) => c.type === 'heading')).toBe(true)
|
|
83
|
+
expect(nodeChildren.some((c) => c.type === 'text')).toBe(true)
|
|
84
|
+
|
|
85
|
+
unmount()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('extracts heading level from real DocHeading primitive', () => {
|
|
89
|
+
const tree = extractDocumentTree(
|
|
90
|
+
h(DocDocument, { title: 't' }, h(DocHeading, { level: 'h2' }, 'Section')),
|
|
91
|
+
)
|
|
92
|
+
const heading = (tree.children.find((c) => typeof c !== 'string' && c.type === 'heading') ??
|
|
93
|
+
null) as { type: string; props: { level?: number } } | null
|
|
94
|
+
expect(heading).not.toBeNull()
|
|
95
|
+
// DocHeading converts the 'h2' level prop into a numeric `level: 2`.
|
|
96
|
+
expect(heading?.props.level).toBe(2)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('extracts href from real DocLink primitive', () => {
|
|
100
|
+
const tree = extractDocumentTree(
|
|
101
|
+
h(
|
|
102
|
+
DocDocument,
|
|
103
|
+
{ title: 't' },
|
|
104
|
+
h(DocLink, { href: 'https://example.com' }, 'click me'),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
const link = (tree.children.find((c) => typeof c !== 'string' && c.type === 'link') ??
|
|
108
|
+
null) as { type: string; props: { href?: string } } | null
|
|
109
|
+
expect(link).not.toBeNull()
|
|
110
|
+
expect(link?.props.href).toBe('https://example.com')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('extracts ordered flag from real DocList primitive', () => {
|
|
114
|
+
const tree = extractDocumentTree(
|
|
115
|
+
h(
|
|
116
|
+
DocDocument,
|
|
117
|
+
{ title: 't' },
|
|
118
|
+
h(
|
|
119
|
+
DocList,
|
|
120
|
+
{ ordered: true },
|
|
121
|
+
h(DocListItem, null, 'one'),
|
|
122
|
+
h(DocListItem, null, 'two'),
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
const list = (tree.children.find((c) => typeof c !== 'string' && c.type === 'list') ??
|
|
127
|
+
null) as { type: string; props: { ordered?: boolean }; children: unknown[] } | null
|
|
128
|
+
expect(list).not.toBeNull()
|
|
129
|
+
expect(list?.props.ordered).toBe(true)
|
|
130
|
+
expect(list?.children.length).toBe(2)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('extracts language from real DocCode primitive', () => {
|
|
134
|
+
const tree = extractDocumentTree(
|
|
135
|
+
h(
|
|
136
|
+
DocDocument,
|
|
137
|
+
{ title: 't' },
|
|
138
|
+
h(DocCode, { language: 'typescript' }, 'const x = 1'),
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
const code = (tree.children.find((c) => typeof c !== 'string' && c.type === 'code') ??
|
|
142
|
+
null) as { type: string; props: { language?: string } } | null
|
|
143
|
+
expect(code).not.toBeNull()
|
|
144
|
+
expect(code?.props.language).toBe('typescript')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('extracts src + alt from real DocImage primitive (no DOM forwarding crash)', () => {
|
|
148
|
+
// Image element has read-only `naturalWidth`/`naturalHeight` properties —
|
|
149
|
+
// a clean test that DocImage doesn't try to set them via property assignment.
|
|
150
|
+
const tree = extractDocumentTree(
|
|
151
|
+
h(
|
|
152
|
+
DocDocument,
|
|
153
|
+
{ title: 't' },
|
|
154
|
+
h(DocImage, { src: 'https://example.com/x.png', alt: 'logo' }),
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
const img = (tree.children.find((c) => typeof c !== 'string' && c.type === 'image') ??
|
|
158
|
+
null) as { type: string; props: { src?: string; alt?: string } } | null
|
|
159
|
+
expect(img).not.toBeNull()
|
|
160
|
+
expect(img?.props.src).toBe('https://example.com/x.png')
|
|
161
|
+
expect(img?.props.alt).toBe('logo')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('extracts table rows + columns without read-only-property crash', () => {
|
|
165
|
+
// DocTable uses `.attrs(callback, { filter: ['rows', 'columns', ...] })`
|
|
166
|
+
// to strip export-only props before they reach the DOM, because
|
|
167
|
+
// HTMLTableElement.rows is a read-only HTMLCollection getter — assigning
|
|
168
|
+
// to it would crash. This test exercises that filter path.
|
|
169
|
+
const tree = extractDocumentTree(
|
|
170
|
+
h(
|
|
171
|
+
DocDocument,
|
|
172
|
+
{ title: 't' },
|
|
173
|
+
h(DocTable, {
|
|
174
|
+
rows: [
|
|
175
|
+
['a1', 'b1'],
|
|
176
|
+
['a2', 'b2'],
|
|
177
|
+
],
|
|
178
|
+
columns: ['col-a', 'col-b'],
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
const table = (tree.children.find((c) => typeof c !== 'string' && c.type === 'table') ??
|
|
183
|
+
null) as
|
|
184
|
+
| {
|
|
185
|
+
type: string
|
|
186
|
+
props: {
|
|
187
|
+
rows?: unknown[]
|
|
188
|
+
columns?: unknown[]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
| null
|
|
192
|
+
expect(table).not.toBeNull()
|
|
193
|
+
expect(Array.isArray(table?.props.rows)).toBe(true)
|
|
194
|
+
expect(table?.props.rows).toHaveLength(2)
|
|
195
|
+
expect(table?.props.columns).toEqual(['col-a', 'col-b'])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('extracts deeply nested tree (DocSection wrapping multiple primitives)', () => {
|
|
199
|
+
const tree = extractDocumentTree(
|
|
200
|
+
h(
|
|
201
|
+
DocDocument,
|
|
202
|
+
{ title: 'Report' },
|
|
203
|
+
h(
|
|
204
|
+
DocSection,
|
|
205
|
+
null,
|
|
206
|
+
h(DocHeading, { level: 'h1' }, 'Intro'),
|
|
207
|
+
h(DocText, null, 'paragraph'),
|
|
208
|
+
h(DocSection, null, h(DocText, null, 'nested-paragraph')),
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
expect(tree.type).toBe('document')
|
|
213
|
+
|
|
214
|
+
// The traversal into nested sections should preserve depth.
|
|
215
|
+
const findByType = (node: unknown, type: string): unknown => {
|
|
216
|
+
if (typeof node !== 'object' || !node) return null
|
|
217
|
+
const n = node as { type?: string; children?: unknown[] }
|
|
218
|
+
if (n.type === type) return n
|
|
219
|
+
for (const c of n.children ?? []) {
|
|
220
|
+
const found = findByType(c, type)
|
|
221
|
+
if (found) return found
|
|
222
|
+
}
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
const heading = findByType(tree, 'heading')
|
|
226
|
+
expect(heading).not.toBeNull()
|
|
227
|
+
// 'nested-paragraph' should appear as a text node anywhere in the tree.
|
|
228
|
+
const treeStr = JSON.stringify(tree)
|
|
229
|
+
expect(treeStr).toContain('nested-paragraph')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('handles primitives that have NO _documentProps cleanly (DocText)', () => {
|
|
233
|
+
// DocText has `_documentProps: {}` — empty object. Confirms the
|
|
234
|
+
// extractor doesn't choke on absent metadata.
|
|
235
|
+
const tree = extractDocumentTree(
|
|
236
|
+
h(DocDocument, { title: 't' }, h(DocText, null, 'just text')),
|
|
237
|
+
)
|
|
238
|
+
const text = (tree.children.find((c) => typeof c !== 'string' && c.type === 'text') ??
|
|
239
|
+
null) as { type: string; children?: unknown[] } | null
|
|
240
|
+
expect(text).not.toBeNull()
|
|
241
|
+
expect(JSON.stringify(text)).toContain('just text')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('handles undefined / nullish optional metadata (omits, does not emit `key: undefined`)', () => {
|
|
245
|
+
// DocDocument explicitly only sets keys when non-null. Verify by
|
|
246
|
+
// omitting all optional props.
|
|
247
|
+
const tree = extractDocumentTree(h(DocDocument, {}))
|
|
248
|
+
expect(tree.type).toBe('document')
|
|
249
|
+
expect(tree.props).not.toHaveProperty('title')
|
|
250
|
+
expect(tree.props).not.toHaveProperty('author')
|
|
251
|
+
expect(tree.props).not.toHaveProperty('subject')
|
|
252
|
+
})
|
|
253
|
+
})
|