@uniweb/semantic-parser 1.1.6 → 1.1.7
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 +1 -1
- package/src/builders/doc.js +204 -0
- package/src/index.js +2 -1
- package/src/processors/sequence.js +18 -17
package/package.json
CHANGED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse conversion: content structure → TipTap document.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the forward parser (processors/sequence.js + processors/groups.js)
|
|
5
|
+
* so that parseContent(buildDoc(content)) roundtrips cleanly.
|
|
6
|
+
*
|
|
7
|
+
* Starter content uses plain strings (no HTML marks), so the conversion
|
|
8
|
+
* is straightforward — no need to reverse inline HTML formatting.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// --- TipTap node builders ---
|
|
12
|
+
|
|
13
|
+
function textNode(text) {
|
|
14
|
+
return { type: 'text', text }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function heading(level, text) {
|
|
18
|
+
if (!text) return null
|
|
19
|
+
// Multi-line title: string[] → multiple headings at same level
|
|
20
|
+
if (Array.isArray(text)) {
|
|
21
|
+
return text.map(t => heading(level, t)).filter(Boolean)
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
type: 'heading',
|
|
25
|
+
attrs: { level },
|
|
26
|
+
content: [textNode(text)],
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function paragraph(text) {
|
|
31
|
+
if (!text) return null
|
|
32
|
+
return {
|
|
33
|
+
type: 'paragraph',
|
|
34
|
+
content: [textNode(text)],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function linkParagraph({ text, href, target }) {
|
|
39
|
+
if (!text || !href) return null
|
|
40
|
+
const mark = { type: 'link', attrs: { href } }
|
|
41
|
+
if (target) mark.attrs.target = target
|
|
42
|
+
return {
|
|
43
|
+
type: 'paragraph',
|
|
44
|
+
content: [{ type: 'text', text, marks: [mark] }],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function imageBlock({ src, alt = '', caption = '', direction, role, width, height }) {
|
|
49
|
+
const attrs = { url: src, alt }
|
|
50
|
+
if (caption) attrs.caption = caption
|
|
51
|
+
if (direction) attrs.direction = direction
|
|
52
|
+
if (role) attrs.role = role
|
|
53
|
+
if (width && height) {
|
|
54
|
+
attrs.aspect_ratio = { width, height, ratio: (height / width) * 100 }
|
|
55
|
+
}
|
|
56
|
+
return { type: 'ImageBlock', attrs }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function iconNode({ src, svg, library, name, size, color }) {
|
|
60
|
+
// UniwebIcon supports multiple source types
|
|
61
|
+
const attrs = {}
|
|
62
|
+
if (svg || src) attrs.svg = svg || src
|
|
63
|
+
if (library) attrs.library = library
|
|
64
|
+
if (name) attrs.name = name
|
|
65
|
+
if (size) attrs.size = size
|
|
66
|
+
if (color) attrs.color = color
|
|
67
|
+
return { type: 'UniwebIcon', attrs }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function videoNode({ src, caption, direction, coverImg }) {
|
|
71
|
+
const attrs = { src }
|
|
72
|
+
if (caption) attrs.caption = caption
|
|
73
|
+
if (direction) attrs.direction = direction
|
|
74
|
+
if (coverImg) attrs.coverImg = coverImg
|
|
75
|
+
return { type: 'Video', attrs }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function dividerBlock() {
|
|
79
|
+
return { type: 'DividerBlock' }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function bulletList(items) {
|
|
83
|
+
if (!items || !items.length) return null
|
|
84
|
+
return {
|
|
85
|
+
type: 'bulletList',
|
|
86
|
+
content: items.map(item => ({
|
|
87
|
+
type: 'listItem',
|
|
88
|
+
content: [paragraph(item)].filter(Boolean),
|
|
89
|
+
})),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Group builder ---
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build TipTap nodes from a content group (main or item).
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} group - Content structure: { pretitle, title, subtitle, paragraphs, images, ... }
|
|
99
|
+
* @param {number} titleLevel - Heading level for title (1 for main, 2 for items)
|
|
100
|
+
* @returns {Array} Array of TipTap nodes
|
|
101
|
+
*/
|
|
102
|
+
function buildGroupNodes(group, titleLevel = 1) {
|
|
103
|
+
const nodes = []
|
|
104
|
+
|
|
105
|
+
// 1. Headings: pretitle → title → subtitle
|
|
106
|
+
// Pretitle uses a higher level number (less important) than title
|
|
107
|
+
// e.g., H3 before H1 — mirrors isPreTitle() in groups.js
|
|
108
|
+
if (group.pretitle) {
|
|
109
|
+
const pre = heading(titleLevel + 2, group.pretitle)
|
|
110
|
+
if (Array.isArray(pre)) nodes.push(...pre)
|
|
111
|
+
else if (pre) nodes.push(pre)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (group.title) {
|
|
115
|
+
const t = heading(titleLevel, group.title)
|
|
116
|
+
if (Array.isArray(t)) nodes.push(...t)
|
|
117
|
+
else if (t) nodes.push(t)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Subtitle is one level below title
|
|
121
|
+
if (group.subtitle) {
|
|
122
|
+
const sub = heading(titleLevel + 1, group.subtitle)
|
|
123
|
+
if (Array.isArray(sub)) nodes.push(...sub)
|
|
124
|
+
else if (sub) nodes.push(sub)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Body fields in document order
|
|
128
|
+
if (group.paragraphs) {
|
|
129
|
+
for (const p of group.paragraphs) {
|
|
130
|
+
const node = paragraph(p)
|
|
131
|
+
if (node) nodes.push(node)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (group.images) {
|
|
136
|
+
for (const img of group.images) {
|
|
137
|
+
nodes.push(imageBlock(img))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (group.links) {
|
|
142
|
+
for (const link of group.links) {
|
|
143
|
+
const node = linkParagraph(link)
|
|
144
|
+
if (node) nodes.push(node)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (group.icons) {
|
|
149
|
+
for (const icon of group.icons) {
|
|
150
|
+
nodes.push(iconNode(icon))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (group.videos) {
|
|
155
|
+
for (const video of group.videos) {
|
|
156
|
+
nodes.push(videoNode(video))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (group.lists) {
|
|
161
|
+
for (const list of group.lists) {
|
|
162
|
+
const node = bulletList(list)
|
|
163
|
+
if (node) nodes.push(node)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return nodes
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- Main export ---
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build a TipTap document from a content structure.
|
|
174
|
+
*
|
|
175
|
+
* This is the reverse of parseContent(): given a flat content object
|
|
176
|
+
* (title, paragraphs, items, etc.), produce a TipTap document that
|
|
177
|
+
* roundtrips through parseContent() to yield the same structure.
|
|
178
|
+
*
|
|
179
|
+
* @param {Object} content - Content structure (same shape as parseContent output / starter)
|
|
180
|
+
* @returns {Object|null} TipTap document { type: 'doc', content: [...] }, or null if empty
|
|
181
|
+
*/
|
|
182
|
+
function buildDoc(content) {
|
|
183
|
+
if (!content) return null
|
|
184
|
+
|
|
185
|
+
const nodes = []
|
|
186
|
+
|
|
187
|
+
// Main group content (title level 1)
|
|
188
|
+
nodes.push(...buildGroupNodes(content, 1))
|
|
189
|
+
|
|
190
|
+
// Items: separated by DividerBlock (mirrors divider-based grouping in groups.js)
|
|
191
|
+
if (content.items && content.items.length > 0) {
|
|
192
|
+
for (const item of content.items) {
|
|
193
|
+
nodes.push(dividerBlock())
|
|
194
|
+
// Item headings use level 2 (one below main H1)
|
|
195
|
+
nodes.push(...buildGroupNodes(item, 2))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (nodes.length === 0) return null
|
|
200
|
+
|
|
201
|
+
return { type: 'doc', content: nodes }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { buildDoc }
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { processSequence } from "./processors/sequence.js";
|
|
2
2
|
import { processGroups } from "./processors/groups.js";
|
|
3
|
+
import { buildDoc } from "./builders/doc.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Parse ProseMirror/TipTap content into semantic structure
|
|
@@ -29,4 +30,4 @@ function parseContent(doc, options = {}) {
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export { parseContent };
|
|
33
|
+
export { parseContent, buildDoc };
|
|
@@ -412,21 +412,25 @@ function processInlineElements(content) {
|
|
|
412
412
|
return items;
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
|
|
416
|
-
let url = "";
|
|
417
|
-
|
|
418
|
-
let src = info?.src || info?.url || "";
|
|
415
|
+
const ASSET_BASE_URL = "https://assets.uniweb.app/";
|
|
419
416
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
417
|
+
/**
|
|
418
|
+
* Resolve an asset identifier ({version}/{filename}) to a direct URL.
|
|
419
|
+
* Assets are hosted at assets.uniweb.app under dist/{version}/base.{ext}.
|
|
420
|
+
*/
|
|
421
|
+
function resolveAssetIdentifier(identifier) {
|
|
422
|
+
if (!identifier || typeof identifier !== "string") return "";
|
|
423
|
+
const [version, filename] = identifier.split("/");
|
|
424
|
+
if (!filename) return "";
|
|
425
|
+
const ext = filename.substring(filename.lastIndexOf(".") + 1);
|
|
426
|
+
return `${ASSET_BASE_URL}dist/${version}/base.${ext}`;
|
|
427
|
+
}
|
|
428
428
|
|
|
429
|
-
|
|
429
|
+
function makeAssetUrl(info) {
|
|
430
|
+
const src = info?.src || info?.url || "";
|
|
431
|
+
if (src) return src;
|
|
432
|
+
if (info?.identifier) return resolveAssetIdentifier(info.identifier);
|
|
433
|
+
return "";
|
|
430
434
|
}
|
|
431
435
|
|
|
432
436
|
function parseCardBlock(itemAttrs) {
|
|
@@ -467,10 +471,7 @@ function parseDocumentBlock(itemAttrs) {
|
|
|
467
471
|
const { identifier = "" } = info;
|
|
468
472
|
|
|
469
473
|
if (identifier) {
|
|
470
|
-
ele.downloadUrl =
|
|
471
|
-
`docufolio/profile`,
|
|
472
|
-
"_template"
|
|
473
|
-
).getAssetInfo(identifier)?.href;
|
|
474
|
+
ele.downloadUrl = resolveAssetIdentifier(identifier);
|
|
474
475
|
}
|
|
475
476
|
}
|
|
476
477
|
|