@uniweb/semantic-parser 1.4.0 → 1.5.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.
package/AGENTS.md CHANGED
@@ -53,7 +53,7 @@ The parser returns a flat content structure:
53
53
  ```js
54
54
  {
55
55
  title: '', // Main heading
56
- pretitle: '', // `#>` label line(s) or smaller headings above the title
56
+ pretitle: '', // `#>` label line(s); also smaller headings above the title
57
57
  subtitle: '', // Line(s) one step below the title (string or array)
58
58
  paragraphs: [],
59
59
  links: [], // All link-like entities (including buttons, documents)
@@ -161,7 +161,7 @@ Lists maintain hierarchy through nested structure. The `processListItems()` func
161
161
 
162
162
  Key patterns — the staircase rule (each heading relates to the one before it):
163
163
 
164
- - **Pretitle**: `#>` label lines (headings with `role: "pretitle"`), and smaller headings stacked directly above a more important one (H3→H1, H6→H5, …). String or array.
164
+ - **Pretitle**: `#>` label lines (headings with `role: "pretitle"`) — what to write. Also smaller headings stacked directly above a more important one (H3→H1, H6→H5, …), the older spelling, read for compatibility. String or array.
165
165
  - **Subtitle**: one step below the title, directly adjacent — and each further one-step descent (or same-size repeat) is another subtitle line. String or array.
166
166
  - **Items**: a heading two or more steps below the previous one, a step back up, or any heading after body content starts a new group.
167
167
  - **Banner Pattern**: an image (with banner role or followed by a heading) at the start of the first group stays with the headline.
package/README.md CHANGED
@@ -66,7 +66,8 @@ Main content fields are at the top level. The `items` array contains additional
66
66
  result = {
67
67
  // Header fields (from headings) — each is a string, or an array when the
68
68
  // part spans several lines
69
- pretitle: "", // `#>` label line(s), or smaller headings above the title
69
+ pretitle: "", // `#>` label line(s) — also smaller headings above
70
+ // the title, the older spelling
70
71
  title: "Welcome", // Main heading
71
72
  subtitle: "", // Line(s) one step below the title
72
73
 
@@ -180,7 +181,7 @@ This is the mechanism that creates repeating content groups (cards, features, FA
180
181
  Two spellings fill the `pretitle` slot:
181
182
 
182
183
  - **A `#>` label line** — a heading with `role: "pretitle"`. It names the block that starts next, at any depth, with no level arithmetic; the hash count is carried for round-tripping and means nothing.
183
- - **Position** — smaller headings stacked above the most important heading of a run (H3 before H1, H6 before H5, …), in any order. `#### / ### / # Title` and `## / ### / # Title` both yield a two-line pretitle.
184
+ - **Position** — smaller headings stacked above the most important heading of a run (H3 before H1, H6 before H5, …), in any order. `#### / ### / # Title` and `## / ### / # Title` both yield a two-line pretitle. ⚖️ This is the older spelling and is read for compatibility; a label line is what to WRITE, because it carries its meaning alone rather than by what follows it.
184
185
 
185
186
  ### Banner image
186
187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Semantic parser for ProseMirror/TipTap content structures",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -6,6 +6,34 @@
6
6
  *
7
7
  * Starter content uses plain strings (no HTML marks), so the conversion
8
8
  * is straightforward — no need to reverse inline HTML formatting.
9
+ *
10
+ * ## ⭐ IT ACCEPTS THE PARSER'S OWN VOCABULARY TOO — and that is a contract
11
+ *
12
+ * This builder was written with its own spelling for several concepts, and
13
+ * `parseContent` emits a DIFFERENT one for the same thing: a link's text is
14
+ * `text` here and `label` there; an image's address is `src` here and `url`
15
+ * there; a video's cover is a string here and `{ src | url | identifier }`
16
+ * there; a list item is a plain string here and a whole content GROUP there.
17
+ *
18
+ * ⛔ EVERY ONE OF THOSE MISMATCHES FAILED SILENTLY, which is why the rule is
19
+ * now written down. Measured 2026-09-16, all four on the shapes a caller would
20
+ * most naturally reach for:
21
+ *
22
+ * { links: [{ label, href }] } → buildDoc returns **null** outright
23
+ * { images: [{ url, alt }] } → an ImageBlock with NO url attr
24
+ * { videos: [{ coverImg: '/p.jpg' }] } → the cover parses back as ""
25
+ * { lists: parsed.lists } → a text node whose `text` is an
26
+ * OBJECT — an invalid document
27
+ *
28
+ * None of them threw. A generator author reaches for `label` and `url` because
29
+ * those are the names in `docs/reference/component-metadata.md` and the names a
30
+ * component actually reads off `content` — so the natural spelling was the
31
+ * broken one, and it produced an empty section rather than an error.
32
+ *
33
+ * ⇒ For every concept where the two sides disagree, ACCEPT BOTH SPELLINGS. The
34
+ * emitted document is unchanged — this only widens what may come in — so the
35
+ * round-trip assertions above still pin the output. A new field whose name
36
+ * differs from the parser's needs the same treatment, or it joins this list.
9
37
  */
10
38
 
11
39
  // --- TipTap node builders ---
@@ -27,6 +55,33 @@ function heading(level, text) {
27
55
  }
28
56
  }
29
57
 
58
+ /**
59
+ * A label line — `#> Text` — which is how a pretitle is written.
60
+ *
61
+ * ⛔ NOT A SMALLER HEADING BEFORE THE TITLE. That positional form still parses,
62
+ * and it is what this builder emitted until 2026-09-16, but it is the old
63
+ * spelling and it is fragile in a way the label line is not: it means "pretitle"
64
+ * only by sitting before a bigger heading, so the same line moved, or left
65
+ * alone, becomes something else. `#>` says what it is wherever it lands.
66
+ *
67
+ * ⭐ THE `#` COUNT MEANS NOTHING. `#>`, `##>` and `###>` are the same thing —
68
+ * content-reader keeps the authored count in `level` purely so a file
69
+ * round-trips byte-for-byte. We match the level of the title it labels, which is
70
+ * what an author writing by hand does (`##>` above an `##` item title).
71
+ *
72
+ * On the wire it is an ordinary heading carrying `role: 'pretitle'`, so every
73
+ * consumer that already understands headings keeps working.
74
+ */
75
+ function labelLine(level, text) {
76
+ if (!text) return null
77
+ if (Array.isArray(text)) return text.map(t => labelLine(level, t)).filter(Boolean)
78
+ return {
79
+ type: 'heading',
80
+ attrs: { level, role: 'pretitle' },
81
+ content: [textNode(text)],
82
+ }
83
+ }
84
+
30
85
  function paragraph(text) {
31
86
  if (!text) return null
32
87
  return {
@@ -35,18 +90,23 @@ function paragraph(text) {
35
90
  }
36
91
  }
37
92
 
38
- function linkParagraph({ text, href, target }) {
39
- if (!text || !href) return null
93
+ function linkParagraph({ text, label, href, target }) {
94
+ // `label` is what parseContent emits and what a component reads off
95
+ // `content.links[]`; `text` is this builder's original spelling.
96
+ const caption = text || label
97
+ if (!caption || !href) return null
40
98
  const mark = { type: 'link', attrs: { href } }
41
99
  if (target) mark.attrs.target = target
42
100
  return {
43
101
  type: 'paragraph',
44
- content: [{ type: 'text', text, marks: [mark] }],
102
+ content: [{ type: 'text', text: caption, marks: [mark] }],
45
103
  }
46
104
  }
47
105
 
48
- function imageBlock({ src, alt = '', caption = '', direction, role, width, height }) {
49
- const attrs = { url: src, alt }
106
+ function imageBlock({ src, url, alt = '', caption = '', direction, role, width, height }) {
107
+ // The node attr is `url` and so is the parsed field; `src` is this builder's
108
+ // original spelling. Either one in, `url` out.
109
+ const attrs = { url: src || url, alt }
50
110
  if (caption) attrs.caption = caption
51
111
  if (direction) attrs.direction = direction
52
112
  if (role) attrs.role = role
@@ -56,6 +116,18 @@ function imageBlock({ src, alt = '', caption = '', direction, role, width, heigh
56
116
  return { type: 'ImageBlock', attrs }
57
117
  }
58
118
 
119
+ // A document is STORED as the image row with `role: 'pdf'` — the node the
120
+ // markdown reader writes and the parser reads as a `document` — so that is what
121
+ // this emits. `url` (the parser's spelling) or `src` (this builder's) in.
122
+ function documentNode({ url, src, alt = '', caption = '', preview, author, description }) {
123
+ const attrs = { src: url || src, alt, role: 'pdf' }
124
+ if (caption) attrs.caption = caption
125
+ if (preview) attrs.preview = preview
126
+ if (author) attrs.author = author
127
+ if (description) attrs.description = description
128
+ return { type: 'image', attrs }
129
+ }
130
+
59
131
  function iconNode({ src, url, svg, library, name, size, color }) {
60
132
  // UniwebIcon supports multiple source types.
61
133
  //
@@ -81,11 +153,16 @@ function iconNode({ src, url, svg, library, name, size, color }) {
81
153
  return { type: 'UniwebIcon', attrs }
82
154
  }
83
155
 
84
- function videoNode({ src, caption, direction, coverImg }) {
85
- const attrs = { src }
156
+ function videoNode({ src, url, caption, direction, coverImg }) {
157
+ const attrs = { src: src || url }
86
158
  if (caption) attrs.caption = caption
87
159
  if (direction) attrs.direction = direction
88
- if (coverImg) attrs.coverImg = coverImg
160
+ // ⛔ The editor's `Video` node holds `coverImg` as an OBJECT — the reader is
161
+ // `makeAssetUrl`, which looks for `.src` / `.url` / `.identifier`. A bare
162
+ // string passed straight through parses back as "": `info?.src` on a string
163
+ // is undefined. Normalize here so a caller may write either.
164
+ const cover = typeof coverImg === 'string' ? { src: coverImg } : coverImg
165
+ if (cover && (cover.src || cover.url || cover.identifier)) attrs.coverImg = cover
89
166
  return { type: 'Video', attrs }
90
167
  }
91
168
 
@@ -93,17 +170,75 @@ function dividerBlock() {
93
170
  return { type: 'DividerBlock' }
94
171
  }
95
172
 
173
+ /**
174
+ * A code block for DISPLAY — what lands in `content.snippets`.
175
+ *
176
+ * `parseContent` groups one as `{ language, code }`; the element before
177
+ * grouping carries `{ text, attrs }`. Accept either spelling, and a bare string
178
+ * for a snippet with no language.
179
+ */
180
+ function codeBlock(snippet) {
181
+ const spec = typeof snippet === 'string' ? { code: snippet } : snippet
182
+ if (!spec || typeof spec !== 'object') return null
183
+ const text = spec.code ?? spec.text
184
+ if (typeof text !== 'string' || text === '') return null
185
+ const attrs = {}
186
+ if (spec.language) attrs.language = spec.language
187
+ if (spec.filename) attrs.filename = spec.filename
188
+ return { type: 'codeBlock', attrs, content: [textNode(text)] }
189
+ }
190
+
191
+ /**
192
+ * A tagged data block — what lands in `content.data[tag]`.
193
+ *
194
+ * ⛔ IT IS A `dataBlock`, NOT A TAGGED `codeBlock`, and the difference is not
195
+ * cosmetic. content-reader emits `dataBlock` when a tagged fence PARSED, with
196
+ * the parsed value on `attrs.data`; it falls back to a tagged `codeBlock`
197
+ * carrying raw TEXT only when parsing failed. A builder handed a real object
198
+ * has parsed data by definition, so emitting the failure shape would hand a
199
+ * component a string where it expects a record.
200
+ *
201
+ * ⭐ `language` IS LOAD-BEARING, not decoration. content-writer reads it back to
202
+ * choose the fence, and the parsed value alone cannot say whether the author
203
+ * wrote YAML or JSON — without it an author's YAML silently becomes JSON on the
204
+ * next editor sync. A builder has no author to echo, so it states one.
205
+ */
206
+ function dataBlock(tag, value, language) {
207
+ // A null value is the same absence as no key at all — a component reading
208
+ // `content.data[tag]` gets null either way — so emit no fence rather than one
209
+ // whose whole payload is `null`.
210
+ if (!tag || value === undefined || value === null) return null
211
+ return { type: 'dataBlock', attrs: { tag, language, data: value } }
212
+ }
213
+
96
214
  function bulletList(items) {
97
215
  if (!items || !items.length) return null
216
+ const entries = items.map(listItemText).filter(Boolean)
217
+ if (!entries.length) return null
98
218
  return {
99
219
  type: 'bulletList',
100
- content: items.map(item => ({
220
+ content: entries.map(text => ({
101
221
  type: 'listItem',
102
- content: [paragraph(item)].filter(Boolean),
222
+ content: [paragraph(text)].filter(Boolean),
103
223
  })),
104
224
  }
105
225
  }
106
226
 
227
+ /**
228
+ * One list entry's text. A generator writes plain strings; `parseContent`
229
+ * emits a full content GROUP per entry ({ paragraphs, links, … }), and feeding
230
+ * that back produced a text node whose `text` was an object — an invalid
231
+ * document that threw nowhere.
232
+ */
233
+ function listItemText(item) {
234
+ if (typeof item === 'string') return item
235
+ if (!item || typeof item !== 'object') return ''
236
+ if (Array.isArray(item.paragraphs) && item.paragraphs.length) {
237
+ return item.paragraphs.join(' ')
238
+ }
239
+ return item.title || ''
240
+ }
241
+
107
242
  // --- Group builder ---
108
243
 
109
244
  /**
@@ -113,14 +248,13 @@ function bulletList(items) {
113
248
  * @param {number} titleLevel - Heading level for title (1 for main, 2 for items)
114
249
  * @returns {Array} Array of TipTap nodes
115
250
  */
116
- function buildGroupNodes(group, titleLevel = 1) {
251
+ function buildGroupNodes(group, titleLevel = 1, options = {}) {
117
252
  const nodes = []
118
253
 
119
- // 1. Headings: pretitle → title → subtitle
120
- // Pretitle uses a higher level number (less important) than title
121
- // e.g., H3 before H1 — mirrors isPreTitle() in groups.js
254
+ // 1. Headings: pretitle → title → subtitle.
255
+ // A pretitle is a LABEL LINE (`#> Text`), at the level of the title it labels.
122
256
  if (group.pretitle) {
123
- const pre = heading(titleLevel + 2, group.pretitle)
257
+ const pre = labelLine(titleLevel, group.pretitle)
124
258
  if (Array.isArray(pre)) nodes.push(...pre)
125
259
  else if (pre) nodes.push(pre)
126
260
  }
@@ -171,6 +305,12 @@ function buildGroupNodes(group, titleLevel = 1) {
171
305
  }
172
306
  }
173
307
 
308
+ if (group.documents) {
309
+ for (const doc of group.documents) {
310
+ nodes.push(documentNode(doc))
311
+ }
312
+ }
313
+
174
314
  if (group.lists) {
175
315
  for (const list of group.lists) {
176
316
  const node = bulletList(list)
@@ -178,6 +318,29 @@ function buildGroupNodes(group, titleLevel = 1) {
178
318
  }
179
319
  }
180
320
 
321
+ if (group.snippets) {
322
+ for (const snippet of group.snippets) {
323
+ const node = codeBlock(snippet)
324
+ if (node) nodes.push(node)
325
+ }
326
+ }
327
+
328
+ // `data` is a MAP of tag → value, not a list: one tagged fence per key, in
329
+ // declaration order.
330
+ //
331
+ // ⚠️ A `concept_block` (```md:faq) also lands in `content.data`, as
332
+ // `{ items, sequence }` — a tagged PROSE fence, not data. This does not
333
+ // reconstruct one: it emits the data fence, whose payload round-trips to the
334
+ // same value. Prose that was parsed into a document does not come back as
335
+ // prose here, and a caller rebuilding a concept block wants the markdown.
336
+ if (group.data && typeof group.data === 'object' && !Array.isArray(group.data)) {
337
+ const language = options.dataLanguage || 'yaml'
338
+ for (const [tag, value] of Object.entries(group.data)) {
339
+ const node = dataBlock(tag, value, language)
340
+ if (node) nodes.push(node)
341
+ }
342
+ }
343
+
181
344
  return nodes
182
345
  }
183
346
 
@@ -191,22 +354,26 @@ function buildGroupNodes(group, titleLevel = 1) {
191
354
  * roundtrips through parseContent() to yield the same structure.
192
355
  *
193
356
  * @param {Object} content - Content structure (same shape as parseContent output / starter)
357
+ * @param {Object} [options]
358
+ * @param {string} [options.dataLanguage='yaml'] - the fence a tagged data block
359
+ * is written as. It is recorded on the node because content-writer reads it
360
+ * back to choose the fence, and a parsed value cannot say which one it was.
194
361
  * @returns {Object|null} TipTap document { type: 'doc', content: [...] }, or null if empty
195
362
  */
196
- function buildDoc(content) {
363
+ function buildDoc(content, options = {}) {
197
364
  if (!content) return null
198
365
 
199
366
  const nodes = []
200
367
 
201
368
  // Main group content (title level 1)
202
- nodes.push(...buildGroupNodes(content, 1))
369
+ nodes.push(...buildGroupNodes(content, 1, options))
203
370
 
204
371
  // Items: separated by DividerBlock (mirrors divider-based grouping in groups.js)
205
372
  if (content.items && content.items.length > 0) {
206
373
  for (const item of content.items) {
207
374
  nodes.push(dividerBlock())
208
375
  // Item headings use level 2 (one below main H1)
209
- nodes.push(...buildGroupNodes(item, 2))
376
+ nodes.push(...buildGroupNodes(item, 2, options))
210
377
  }
211
378
  }
212
379
 
@@ -15,6 +15,7 @@ function flattenGroup(group) {
15
15
  icons: group.body.icons || [],
16
16
  lists: group.body.lists || [],
17
17
  videos: group.body.videos || [],
18
+ documents: group.body.documents || [],
18
19
  insets: group.body.insets || [],
19
20
  snippets: group.body.snippets || [],
20
21
  data: group.body.data || {},
@@ -303,6 +304,7 @@ function processGroupContent(elements, options = {}) {
303
304
  images: [],
304
305
  icons: [],
305
306
  videos: [],
307
+ documents: [],
306
308
  insets: [],
307
309
  snippets: [],
308
310
  paragraphs: [],
@@ -387,6 +389,10 @@ function processGroupContent(elements, options = {}) {
387
389
  body.videos.push(preserveProps);
388
390
  break;
389
391
 
392
+ case "document":
393
+ body.documents.push(preserveProps);
394
+ break;
395
+
390
396
  case "link":
391
397
  if (element.children && Array.isArray(element.children))
392
398
  processInlineElements(element.children, body);
@@ -520,19 +526,6 @@ function processGroupContent(elements, options = {}) {
520
526
  });
521
527
  break;
522
528
 
523
- case "document-group":
524
- // Map documents to links with role=document
525
- element.documents.forEach(doc => {
526
- body.links.push({
527
- href: doc.href || doc.downloadUrl || '',
528
- label: doc.title || '',
529
- role: 'document',
530
- download: true,
531
- preview: doc.coverImg,
532
- fileType: doc.fileType,
533
- });
534
- });
535
- break;
536
529
  }
537
530
  }
538
531
  }
@@ -588,6 +581,9 @@ function processInlineElements(children, body) {
588
581
  // `{role=video}` belongs in videos[] for the same reason a hoisted
589
582
  // one does.
590
583
  body.videos.push(item.attrs);
584
+ } else if (item.type === "document") {
585
+ // And an inline `{role=pdf}` in documents[].
586
+ body.documents.push(item.attrs);
591
587
  } else if (item.type === "link") {
592
588
  // Handle inline links extracted from paragraph text nodes
593
589
  body.links.push(item.attrs);
@@ -210,7 +210,19 @@ function createSequenceElement(node, options = {}) {
210
210
  case "image":
211
211
  // `role` decides which content array this lands in — the contract
212
212
  // published in docs/reference/content-structure.md ("Media Assets"):
213
- // icon → icons, video → videos, everything else → images.
213
+ // icon → icons, video → videos, pdf → documents, everything else →
214
+ // images.
215
+ //
216
+ // A document is stored as this node, as a video is, and delivered as
217
+ // a kind of its own (`parseMarkdownDocument`). Until 2026-09-29 it
218
+ // landed in `images`, drawn as an image of the file, while the
219
+ // runtime had guaranteed an empty `content.documents` since January.
220
+ if (attrs?.role === "pdf") {
221
+ return {
222
+ type: "document",
223
+ attrs: parseMarkdownDocument(attrs, options),
224
+ };
225
+ }
214
226
  //
215
227
  // The video branch was missing until 2026-07-30: every
216
228
  // `{role=video}` node ran parseImgBlock, which does not carry
@@ -315,13 +327,16 @@ function createSequenceElement(node, options = {}) {
315
327
  };
316
328
 
317
329
  case "document-group":
318
- return {
319
- type: "document-group",
320
- documents:
321
- node.content
322
- ?.filter((c) => c.type === "document")
323
- .map((doc) => parseDocumentBlock(doc.attrs)) || [],
324
- };
330
+ // The editor's older container: one `document` element per document
331
+ // it holds, so a document is one kind of content whichever node
332
+ // wrote it (`documentFromEditorNode`). Until 2026-09-29 this was a
333
+ // `document-group` element whose documents became `content.links`.
334
+ return (node.content || [])
335
+ .filter((c) => c.type === "document")
336
+ .map((doc) => ({
337
+ type: "document",
338
+ attrs: documentFromEditorNode(doc.attrs),
339
+ }));
325
340
 
326
341
  // The editor renamed this node to `StructuredContent` (2026-07-31,
327
342
  // editor-internal). Both are accepted: the new name is what the editor
@@ -613,13 +628,15 @@ function processInlineElements(content, options = {}) {
613
628
  // image silently deleted. groups.js has carried the receiving
614
629
  // branch (and a comment describing exactly this failure) since it
615
630
  // was written, but nothing ever emitted the item it reads.
616
- const isVideo = item.attrs?.role === "video";
617
- items.push({
618
- type: isVideo ? "video" : "image",
619
- attrs: isVideo
620
- ? parseMarkdownVideo(item.attrs, options)
621
- : parseImgBlock(item.attrs || {}, options),
622
- });
631
+ // The role partitions it as the block case does.
632
+ const role = item.attrs?.role;
633
+ if (role === "video") {
634
+ items.push({ type: "video", attrs: parseMarkdownVideo(item.attrs, options) });
635
+ } else if (role === "pdf") {
636
+ items.push({ type: "document", attrs: parseMarkdownDocument(item.attrs, options) });
637
+ } else {
638
+ items.push({ type: "image", attrs: parseImgBlock(item.attrs || {}, options) });
639
+ }
623
640
  } else if (item.type === "math-inline" || item.type === "math_inline") {
624
641
  items.push(item);
625
642
  } else if (item.type === "inset_placeholder") {
@@ -802,6 +819,54 @@ function parseCardBlock(itemAttrs) {
802
819
  };
803
820
  }
804
821
 
822
+ // A document authored in markdown: `![Annual report](./report.pdf){role=pdf …}`.
823
+ //
824
+ // Read through `parseImgBlock`, which resolves the address and the preview's the
825
+ // way every image's are, and then narrowed to what a document IS: where the file
826
+ // is, what it is called, and the resource's own metadata. The image layout keys
827
+ // (direction, size, credit) describe a picture, not a file, and are left out.
828
+ function parseMarkdownDocument(itemAttrs, options) {
829
+ const { url, alt, caption, role, href, target, id, preview, author, description } =
830
+ parseImgBlock(itemAttrs || {}, options);
831
+ return {
832
+ url,
833
+ alt,
834
+ caption,
835
+ role,
836
+ ...(href && { href }),
837
+ ...(target && { target }),
838
+ ...(id && { id }),
839
+ ...(preview && { preview }),
840
+ ...(author && { author }),
841
+ ...(description && { description }),
842
+ };
843
+ }
844
+
845
+ // The editor's older `document` node — its `title`, `coverImg` and `src` (or
846
+ // asset identifier) — read into the same shape a markdown document has.
847
+ function documentFromEditorNode(itemAttrs) {
848
+ const {
849
+ href,
850
+ downloadUrl,
851
+ title = "",
852
+ alt = "",
853
+ coverImg,
854
+ author,
855
+ description,
856
+ fileType,
857
+ } = parseDocumentBlock(itemAttrs || {});
858
+ return {
859
+ url: href || downloadUrl || "",
860
+ alt: alt || title,
861
+ caption: title,
862
+ role: "pdf",
863
+ ...(coverImg && { preview: coverImg }),
864
+ ...(author && { author }),
865
+ ...(description && { description }),
866
+ ...(fileType && { fileType }),
867
+ };
868
+ }
869
+
805
870
  function parseDocumentBlock(itemAttrs) {
806
871
  const { src, info = {}, coverImg = null, ...others } = itemAttrs;
807
872