@nan0web/ui-payload 3.3.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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Nan0ComponentNode
3
+ * Represents a NaN0Web component inside the Lexical tree
4
+ * (e.g. `App.Header`, `Card.Details`, `App.Deposits.Calculator`).
5
+ *
6
+ * Serialized shape:
7
+ * ```json
8
+ * {
9
+ * "type": "nan0-component",
10
+ * "version": 1,
11
+ * "component": "Card.Details",
12
+ * "props": { "card": "$" }
13
+ * }
14
+ * ```
15
+ *
16
+ * Renders in the editor as a read-only placeholder for now. A future
17
+ * phase adds React components + props forms for editing.
18
+ */
19
+ import { DecoratorNode, $applyNodeReplacement } from 'lexical'
20
+
21
+ export class Nan0ComponentNode extends DecoratorNode {
22
+ /** @type {string} */ __component
23
+ /** @type {Record<string, any>} */ __props
24
+
25
+ constructor({ component, props = {}, key }) {
26
+ super(key)
27
+ this.__component = component
28
+ this.__props = props
29
+ }
30
+
31
+ static clone(node) {
32
+ return new this({ component: node.__component, props: { ...node.__props }, key: node.__key })
33
+ }
34
+
35
+ static getType() {
36
+ return 'nan0-component'
37
+ }
38
+
39
+ static importJSON(serializedNode) {
40
+ return $createNan0ComponentNode({
41
+ component: serializedNode.component,
42
+ props: serializedNode.props || {},
43
+ })
44
+ }
45
+
46
+ exportJSON() {
47
+ return {
48
+ ...super.exportJSON(),
49
+ type: 'nan0-component',
50
+ version: 1,
51
+ component: this.__component,
52
+ props: this.__props,
53
+ }
54
+ }
55
+
56
+ getComponent() {
57
+ return this.getLatest().__component
58
+ }
59
+
60
+ getProps() {
61
+ return this.getLatest().__props
62
+ }
63
+
64
+ isInline() {
65
+ return false
66
+ }
67
+
68
+ createDOM() {
69
+ const el = document.createElement('div')
70
+ el.setAttribute('data-nan0-component', this.__component)
71
+ el.className = 'nan0-component-placeholder'
72
+ el.style.border = '1px dashed #c8a'
73
+ el.style.padding = '8px'
74
+ el.style.margin = '4px 0'
75
+ el.style.borderRadius = '4px'
76
+ el.style.background = '#fdf6ff'
77
+ el.style.fontSize = '0.85em'
78
+ el.style.color = '#a55'
79
+ el.textContent = `[component: ${this.__component}]`
80
+ return el
81
+ }
82
+
83
+ updateDOM() {
84
+ return false
85
+ }
86
+
87
+ decorate() {
88
+ return null
89
+ }
90
+ }
91
+
92
+ export function $createNan0ComponentNode({ component, props = {} }) {
93
+ return $applyNodeReplacement(new Nan0ComponentNode({ component, props }))
94
+ }
95
+
96
+ export function $isNan0ComponentNode(node) {
97
+ return node instanceof Nan0ComponentNode
98
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Nan0ElementNode
3
+ * Lossless wrapper for NaN0HTML elements that carry `$`-attributes.
4
+ *
5
+ * Serialized shape:
6
+ * ```json
7
+ * { "type": "nan0-element", "version": 1, "tag": "div", "attributes": { "class": "..." }, "children": [...] }
8
+ * ```
9
+ *
10
+ * This node is used when a native Lexical node cannot represent the attributes
11
+ * without data loss. Clean elements (no attributes) use native nodes directly.
12
+ */
13
+ import { ElementNode, $applyNodeReplacement } from 'lexical'
14
+
15
+ export class Nan0ElementNode extends ElementNode {
16
+ /** @type {string} */ __tag
17
+ /** @type {Record<string, any>} */ __attributes
18
+
19
+ constructor({ tag, attributes = {}, key }) {
20
+ super(key)
21
+ this.__tag = tag
22
+ this.__attributes = attributes
23
+ }
24
+
25
+ static clone(node) {
26
+ return new this({ tag: node.__tag, attributes: { ...node.__attributes }, key: node.__key })
27
+ }
28
+
29
+ static getType() {
30
+ return 'nan0-element'
31
+ }
32
+
33
+ static importJSON(serializedNode) {
34
+ const node = $createNan0ElementNode({
35
+ tag: serializedNode.tag,
36
+ attributes: serializedNode.attributes || {},
37
+ })
38
+ return node
39
+ }
40
+
41
+ exportJSON() {
42
+ return {
43
+ ...super.exportJSON(),
44
+ type: 'nan0-element',
45
+ version: 1,
46
+ tag: this.__tag,
47
+ attributes: this.__attributes,
48
+ }
49
+ }
50
+
51
+ getTag() {
52
+ return this.getLatest().__tag
53
+ }
54
+
55
+ getAttributes() {
56
+ return this.getLatest().__attributes
57
+ }
58
+
59
+ isInline() {
60
+ return false
61
+ }
62
+
63
+ canBeEmpty() {
64
+ return true
65
+ }
66
+
67
+ createDOM(config) {
68
+ const element = document.createElement(this.__tag || 'div')
69
+ const attrs = this.__attributes || {}
70
+ for (const [key, value] of Object.entries(attrs)) {
71
+ if (key === 'class') {
72
+ element.className = Array.isArray(value) ? value.join(' ') : String(value)
73
+ } else if (key === 'style' && typeof value === 'string') {
74
+ element.setAttribute('style', value)
75
+ } else if (typeof value === 'string' || typeof value === 'number') {
76
+ element.setAttribute(key, String(value))
77
+ }
78
+ }
79
+ return element
80
+ }
81
+
82
+ updateDOM() {
83
+ return false
84
+ }
85
+ }
86
+
87
+ export function $createNan0ElementNode({ tag, attributes = {} }) {
88
+ return $applyNodeReplacement(new Nan0ElementNode({ tag, attributes }))
89
+ }
90
+
91
+ export function $isNan0ElementNode(node) {
92
+ return node instanceof Nan0ElementNode
93
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Nan0HTMLFeature
3
+ * Payload Lexical feature that registers the NaN0 custom nodes
4
+ * (nan0-element, nan0-raw, nan0-component) for server + client.
5
+ *
6
+ * Usage in a Payload collection:
7
+ * ```js
8
+ * editor: lexicalEditor({
9
+ * features: ({ defaultFeatures }) => [
10
+ * ...defaultFeatures,
11
+ * Nan0HTMLFeature(),
12
+ * ],
13
+ * })
14
+ * ```
15
+ */
16
+ import { createServerFeature } from '@payloadcms/richtext-lexical'
17
+ import { convertLexicalNodesToHTML } from '@payloadcms/richtext-lexical'
18
+ import { createNode } from '@payloadcms/richtext-lexical'
19
+ import { Nan0ElementNode } from './Nan0ElementNode.js'
20
+ import { Nan0RawNode } from './Nan0RawNode.js'
21
+ import { Nan0ComponentNode } from './Nan0ComponentNode.js'
22
+
23
+ export const Nan0HTMLFeature = createServerFeature({
24
+ key: 'nan0html',
25
+ feature: {
26
+ ClientFeature: '@nan0web/ui-payload/richtext/client#Nan0HTMLFeatureClient',
27
+ nodes: [
28
+ createNode({
29
+ node: Nan0ElementNode,
30
+ converters: {
31
+ html: {
32
+ converter: async ({
33
+ converters,
34
+ currentDepth,
35
+ depth,
36
+ draft,
37
+ node,
38
+ overrideAccess,
39
+ parent,
40
+ req,
41
+ showHiddenFields,
42
+ }) => {
43
+ const childrenText = await convertLexicalNodesToHTML({
44
+ converters,
45
+ currentDepth,
46
+ depth,
47
+ draft,
48
+ lexicalNodes: node.children,
49
+ overrideAccess,
50
+ parent: { ...node, parent },
51
+ req,
52
+ showHiddenFields,
53
+ })
54
+ const attrs = node.getAttributes() || {}
55
+ const attrString = Object.entries(attrs)
56
+ .filter(([k]) => k !== 'class' && k !== 'style')
57
+ .map(([k, v]) => `${k}="${String(v).replaceAll('"', '&quot;')}"`)
58
+ .join(' ')
59
+ const classAttr = attrs.class ? ` class="${Array.isArray(attrs.class) ? attrs.class.join(' ') : attrs.class}"` : ''
60
+ const styleAttr = attrs.style ? ` style="${String(attrs.style).replaceAll('"', '&quot;')}"` : ''
61
+ return `<${node.getTag()}${classAttr}${styleAttr}${attrString ? ' ' + attrString : ''}>${childrenText}</${node.getTag()}>`
62
+ },
63
+ nodeTypes: [Nan0ElementNode.getType()],
64
+ },
65
+ },
66
+ }),
67
+ createNode({
68
+ node: Nan0RawNode,
69
+ converters: {
70
+ html: {
71
+ converter: ({ node }) => {
72
+ const source = node.getSource() || {}
73
+ return `<!-- nan0-raw:${source.tag || 'unknown'} -->`
74
+ },
75
+ nodeTypes: [Nan0RawNode.getType()],
76
+ },
77
+ },
78
+ }),
79
+ createNode({
80
+ node: Nan0ComponentNode,
81
+ converters: {
82
+ html: {
83
+ converter: ({ node }) => {
84
+ return `<!-- nan0-component:${node.getComponent()} -->`
85
+ },
86
+ nodeTypes: [Nan0ComponentNode.getType()],
87
+ },
88
+ },
89
+ }),
90
+ ],
91
+ },
92
+ })
93
+
94
+ export default Nan0HTMLFeature
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Nan0RawNode
3
+ * Fallback for unknown NaN0HTML tags and structures that do not yet have
4
+ * a native or nan0-element adapter.
5
+ *
6
+ * Serialized shape:
7
+ * ```json
8
+ * {
9
+ * "type": "nan0-raw",
10
+ * "version": 1,
11
+ * "source": {
12
+ * "tag": "legacy-widget",
13
+ * "attributes": { "data-mode": "legacy" },
14
+ * "children": []
15
+ * }
16
+ * }
17
+ * ```
18
+ *
19
+ * In the editor, this renders as a read-only block showing the tag name
20
+ * so the user knows something is stored but not yet editable natively.
21
+ * A future migration can replace nan0-raw with the appropriate node.
22
+ */
23
+ import { ElementNode, $applyNodeReplacement } from 'lexical'
24
+
25
+ export class Nan0RawNode extends ElementNode {
26
+ /** @type {{ tag: string, attributes: Record<string, any>, children: any[] }} */ __source
27
+
28
+ constructor({ source, key }) {
29
+ super(key)
30
+ this.__source = source
31
+ }
32
+
33
+ static clone(node) {
34
+ return new this({ source: { ...node.__source }, key: node.__key })
35
+ }
36
+
37
+ static getType() {
38
+ return 'nan0-raw'
39
+ }
40
+
41
+ static importJSON(serializedNode) {
42
+ return $createNan0RawNode({ source: serializedNode.source || { tag: 'unknown', attributes: {}, children: [] } })
43
+ }
44
+
45
+ exportJSON() {
46
+ return {
47
+ ...super.exportJSON(),
48
+ type: 'nan0-raw',
49
+ version: 1,
50
+ source: this.__source,
51
+ }
52
+ }
53
+
54
+ getSource() {
55
+ return this.getLatest().__source
56
+ }
57
+
58
+ isInline() {
59
+ return false
60
+ }
61
+
62
+ canBeEmpty() {
63
+ return true
64
+ }
65
+
66
+ createDOM(config) {
67
+ const el = document.createElement('div')
68
+ el.setAttribute('data-nan0-raw', this.__source.tag)
69
+ el.className = 'nan0-raw-fallback'
70
+ el.style.border = '1px dashed #ccc'
71
+ el.style.padding = '8px'
72
+ el.style.margin = '4px 0'
73
+ el.style.borderRadius = '4px'
74
+ el.style.background = '#fafafa'
75
+ el.style.fontSize = '0.85em'
76
+ el.style.color = '#888'
77
+ el.textContent = `[nan0-raw: ${this.__source.tag}]`
78
+ return el
79
+ }
80
+
81
+ updateDOM() {
82
+ return false
83
+ }
84
+ }
85
+
86
+ export function $createNan0RawNode({ source }) {
87
+ return $applyNodeReplacement(new Nan0RawNode({ source }))
88
+ }
89
+
90
+ export function $isNan0RawNode(node) {
91
+ return node instanceof Nan0RawNode
92
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Nan0HTMLFeatureClient
3
+ * Client-side registration of NaN0 custom nodes for the Lexical editor.
4
+ * Imported via the Payload import map from `@nan0web/ui-payload/richtext/client`.
5
+ */
6
+ 'use client'
7
+
8
+ import { Nan0ElementNode } from '../Nan0ElementNode.js'
9
+ import { Nan0RawNode } from '../Nan0RawNode.js'
10
+ import { Nan0ComponentNode } from '../Nan0ComponentNode.js'
11
+
12
+ export const Nan0HTMLFeatureClient = (props = {}) => ({
13
+ clientFeatureProps: props,
14
+ feature: () => ({
15
+ nodes: [Nan0ElementNode, Nan0RawNode, Nan0ComponentNode],
16
+ sanitizedClientFeatureProps: props,
17
+ }),
18
+ })
19
+
20
+ export default Nan0HTMLFeatureClient
@@ -0,0 +1,294 @@
1
+ /**
2
+ * fromNan0Html
3
+ * Converts a NaN0HTML AST (page.content / content) into a Payload Lexical state JSON.
4
+ *
5
+ * NaN0HTML AST shape:
6
+ * - array → sequence of blocks
7
+ * - string → text content
8
+ * - object → element with keys: `$attr` for attributes, lowercase tag keys for
9
+ * child elements, `Uppercase.With.Dot` keys for NaN0 components
10
+ * - `tag: true` → void element (e.g. `br`, `hr`)
11
+ *
12
+ * Strategy:
13
+ * - Clean elements (no attributes) map to native Lexical nodes where possible.
14
+ * - Elements carrying any `$`-attribute are preserved losslessly as a `nan0-element`
15
+ * node (tag + attributes + children) so round-trip back to NaN0HTML is lossless.
16
+ * - Unknown tags fall back to a `nan0-raw` node and are recorded in the inventory.
17
+ * - NaN0 components (`App.Header`, `Card.Details`, ...) become `nan0-component` nodes.
18
+ *
19
+ * The converter returns a plain structure; localization is handled at the Payload
20
+ * collection level, not here.
21
+ */
22
+
23
+ // Lexical text format bitmask
24
+ export const FORMAT = Object.freeze({
25
+ bold: 1 << 0,
26
+ italic: 1 << 1,
27
+ underline: 1 << 2,
28
+ strikethrough: 1 << 3,
29
+ })
30
+
31
+ // Native block-level tags we can represent without a custom node.
32
+ const BLOCK_TAGS = new Set([
33
+ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
34
+ 'ul', 'ol', 'li',
35
+ 'blockquote', 'hr', 'br',
36
+ 'table', 'thead', 'tbody', 'tr', 'td', 'th',
37
+ ])
38
+
39
+ // Tags that map to native Lexical nodes.
40
+ const NATIVE_TAG_MAP = {
41
+ p: 'paragraph',
42
+ h1: 'heading', h2: 'heading', h3: 'heading', h4: 'heading', h5: 'heading', h6: 'heading',
43
+ blockquote: 'blockquote',
44
+ hr: 'horizontalrule',
45
+ br: 'linebreak',
46
+ }
47
+
48
+ // Inline formatting tags → format bit added to descendant text nodes.
49
+ const INLINE_FORMAT = {
50
+ strong: FORMAT.bold,
51
+ b: FORMAT.bold,
52
+ em: FORMAT.italic,
53
+ i: FORMAT.italic,
54
+ u: FORMAT.underline,
55
+ }
56
+
57
+ /**
58
+ * Recursively walk the NaN0HTML AST and collect unknown constructs.
59
+ * @param {any} node
60
+ * @param {Array<{tag: string, attributes: Object, parent: string}>} out
61
+ * @param {string} parent
62
+ */
63
+ export function inventoryNan0Html(node, out = [], parent = 'root') {
64
+ if (node == null) return out
65
+ if (Array.isArray(node)) {
66
+ for (const child of node) inventoryNan0Html(child, out, parent)
67
+ return out
68
+ }
69
+ if (typeof node !== 'object') return out
70
+ for (const [key, value] of Object.entries(node)) {
71
+ if (key.startsWith('$')) continue
72
+ if (/^[a-z][a-z0-9]*$/.test(key)) {
73
+ if (!BLOCK_TAGS.has(key) && key !== 'a' && key !== 'img' && key !== 'span' && key !== 'div' && key !== 'wbr' && key !== 'small' && key !== 'sup') {
74
+ out.push({ tag: key, parent, attributes: collectAttrs(value) })
75
+ }
76
+ inventoryNan0Html(value, out, key)
77
+ } else if (/^[A-Z]/.test(key) && key.includes('.')) {
78
+ out.push({ component: key, parent })
79
+ inventoryNan0Html(value, out, key)
80
+ } else {
81
+ inventoryNan0Html(value, out, parent)
82
+ }
83
+ }
84
+ return out
85
+ }
86
+
87
+ function collectAttrs(value) {
88
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
89
+ const attrs = {}
90
+ for (const [k, v] of Object.entries(value)) {
91
+ if (k.startsWith('$')) attrs[k.slice(1)] = v
92
+ }
93
+ return attrs
94
+ }
95
+
96
+ /**
97
+ * Convert a NaN0HTML AST into a Payload Lexical root state.
98
+ * @param {any} content NaN0HTML AST (array of blocks, or a single block)
99
+ * @returns {{ root: { type: string, version: number, children: any[] } }}
100
+ */
101
+ export function fromNan0Html(content) {
102
+ const children = convertBlock(content, 0)
103
+ return {
104
+ root: {
105
+ type: 'root',
106
+ version: 1,
107
+ children,
108
+ },
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Convert a block (array of elements, or a single element) into Lexical children.
114
+ */
115
+ function convertBlock(node, format) {
116
+ if (node == null) return []
117
+ if (Array.isArray(node)) {
118
+ const out = []
119
+ for (const child of node) {
120
+ out.push(...convertBlock(child, format))
121
+ }
122
+ return out
123
+ }
124
+ if (typeof node === 'string') {
125
+ return [textNode(node, format)]
126
+ }
127
+ if (typeof node !== 'object') return []
128
+ // Object: split `$`-attributes from element keys.
129
+ const attrs = {}
130
+ const elements = []
131
+ for (const [key, value] of Object.entries(node)) {
132
+ if (key.startsWith('$')) {
133
+ attrs[key.slice(1)] = value
134
+ continue
135
+ }
136
+ elements.push([key, value])
137
+ }
138
+ const out = []
139
+ for (const [tag, value] of elements) {
140
+ out.push(...convertElement(tag, value, attrs, format))
141
+ }
142
+ return out
143
+ }
144
+
145
+ /**
146
+ * Convert a single tag element into Lexical nodes.
147
+ * `attrs` are the attributes collected at the same object level.
148
+ */
149
+ function convertElement(tag, value, attrs, format) {
150
+ // NaN0 component
151
+ if (/^[A-Z]/.test(tag) && tag.includes('.')) {
152
+ const props = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
153
+ return [componentNode(tag, props)]
154
+ }
155
+
156
+ // Inline formatting tags alter the text format of descendants.
157
+ if (INLINE_FORMAT[tag] != null) {
158
+ const childFormat = format | INLINE_FORMAT[tag]
159
+ return convertBlock(value, childFormat)
160
+ }
161
+
162
+ let children = []
163
+ if (value === true) {
164
+ // void element
165
+ children = []
166
+ } else if (Array.isArray(value) || typeof value === 'string') {
167
+ children = convertBlock(value, format)
168
+ } else if (value && typeof value === 'object') {
169
+ children = convertBlock(value, format)
170
+ }
171
+
172
+ const hasAttrs = Object.keys(attrs).length > 0
173
+
174
+ switch (tag) {
175
+ case 'p':
176
+ if (hasAttrs) return [elementNode(tag, attrs, children)]
177
+ return [blockNode('paragraph', children)]
178
+ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
179
+ if (hasAttrs) return [elementNode(tag, attrs, children)]
180
+ return [blockNode('heading', children, { tag })]
181
+ case 'blockquote':
182
+ if (hasAttrs) return [elementNode(tag, attrs, children)]
183
+ return [blockNode('blockquote', children)]
184
+ case 'hr':
185
+ if (hasAttrs) return [elementNode(tag, attrs, [])]
186
+ return [{ type: 'horizontalrule', version: 1 }]
187
+ case 'br':
188
+ if (hasAttrs) return [elementNode(tag, attrs, [])]
189
+ return [{ type: 'linebreak', version: 1 }]
190
+ case 'ul': case 'ol':
191
+ if (hasAttrs) return [elementNode(tag, attrs, children)]
192
+ return [listNode(tag, value, children)]
193
+ case 'li':
194
+ if (hasAttrs) return [elementNode(tag, attrs, children)]
195
+ return [{ type: 'listitem', version: 1, value: 1, children }]
196
+ case 'a': {
197
+ const url = attrs.href || '#'
198
+ const linkAttrs = { url, linkType: 'custom' }
199
+ if (attrs.target) linkAttrs.newTab = String(attrs.target) === '_blank'
200
+ if (attrs.rel) linkAttrs.rel = attrs.rel
201
+ const node = { type: 'link', version: 1, fields: linkAttrs, children }
202
+ // Preserve extra attributes losslessly.
203
+ return [wrapElementIfNeeded(tag, attrs, node)]
204
+ }
205
+ case 'img': {
206
+ const src = attrs.src || ''
207
+ const node = {
208
+ type: 'upload',
209
+ version: 1,
210
+ fields: { relationTo: 'media', value: src },
211
+ children: [],
212
+ }
213
+ return [wrapElementIfNeeded(tag, attrs, node)]
214
+ }
215
+ case 'table': case 'thead': case 'tbody':
216
+ if (hasAttrs) return [elementNode(tag, attrs, children)]
217
+ return [{ type: 'table', version: 1, children }]
218
+ case 'tr':
219
+ if (hasAttrs) return [elementNode(tag, attrs, children)]
220
+ return [{ type: 'tablerow', version: 1, children }]
221
+ case 'td': case 'th': {
222
+ const cell = { type: 'tablecell', version: 1, header: tag === 'th', children }
223
+ return [wrapElementIfNeeded(tag, attrs, cell)]
224
+ }
225
+ case 'span': case 'div': case 'section': case 'header': case 'nav':
226
+ case 'figure': case 'caption': case 'small': case 'sup': case 'wbr':
227
+ case 'picture': case 'source':
228
+ // These are either inline or block wrappers; preserve attributes if any.
229
+ if (hasAttrs) return [elementNode(tag, attrs, children)]
230
+ return [blockNode('paragraph', children)]
231
+ default:
232
+ // Unknown tag → preserve losslessly as nan0-raw.
233
+ return [rawNode(tag, attrs, children)]
234
+ }
235
+ }
236
+
237
+ /**
238
+ * If the element carries attributes beyond those consumed by the native node,
239
+ * wrap it in a lossless `nan0-element` node so nothing is dropped.
240
+ */
241
+ function wrapElementIfNeeded(tag, attrs, node) {
242
+ if (!attrs) return [node]
243
+ return [elementNode(tag, attrs, node.children || [])]
244
+ }
245
+
246
+ function textNode(text, format) {
247
+ return { type: 'text', text, format }
248
+ }
249
+
250
+ function blockNode(type, children, extra = {}) {
251
+ return { type, version: 1, ...extra, children }
252
+ }
253
+
254
+ function listNode(tag, value, children) {
255
+ const listType = tag === 'ol' ? 'number' : 'bullet'
256
+ let start = 1
257
+ if (value && typeof value === 'object' && !Array.isArray(value) && value.$start != null) {
258
+ start = Number(value.$start) || 1
259
+ }
260
+ return { type: 'list', version: 1, listType, start, tag, children }
261
+ }
262
+
263
+ function elementNode(tag, attrs, children) {
264
+ return {
265
+ type: 'nan0-element',
266
+ version: 1,
267
+ tag,
268
+ attributes: attrs,
269
+ children,
270
+ }
271
+ }
272
+
273
+ function componentNode(component, props) {
274
+ return {
275
+ type: 'nan0-component',
276
+ version: 1,
277
+ component,
278
+ props,
279
+ }
280
+ }
281
+
282
+ function rawNode(tag, attrs, children) {
283
+ return {
284
+ type: 'nan0-raw',
285
+ version: 1,
286
+ source: {
287
+ tag,
288
+ attributes: attrs,
289
+ children,
290
+ },
291
+ }
292
+ }
293
+
294
+ export default fromNan0Html
@@ -0,0 +1,10 @@
1
+ /**
2
+ * richtext package index
3
+ * NaN0HTML → Payload Lexical conversion toolkit.
4
+ *
5
+ * Pure functions (fromNan0Html, inventoryNan0Html) are safe to import
6
+ * from any context. Custom node classes (Nan0ElementNode, etc.) require
7
+ * `lexical` to be resolvable and are available via direct import paths.
8
+ */
9
+ export { default as fromNan0Html, inventoryNan0Html, FORMAT } from './fromNan0Html.js'
10
+ export { default as toNan0Html } from './toNan0Html.js'