@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Semantic parser for ProseMirror/TipTap content structures",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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
- function makeAssetUrl(info) {
416
- let url = "";
417
-
418
- let src = info?.src || info?.url || "";
415
+ const ASSET_BASE_URL = "https://assets.uniweb.app/";
419
416
 
420
- if (src) {
421
- url = src;
422
- } else if (info?.identifier) {
423
- url =
424
- new uniweb.Profile(`docufolio/profile`, "_template").getAssetInfo(
425
- info.identifier
426
- )?.src || "";
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
- return url;
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 = new uniweb.Profile(
471
- `docufolio/profile`,
472
- "_template"
473
- ).getAssetInfo(identifier)?.href;
474
+ ele.downloadUrl = resolveAssetIdentifier(identifier);
474
475
  }
475
476
  }
476
477