@uniweb/semantic-parser 1.5.0 → 1.5.1

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/README.md CHANGED
@@ -73,16 +73,22 @@ result = {
73
73
 
74
74
  // Body fields
75
75
  paragraphs: ["Get started today."],
76
- links: [], // All links (including buttons, documents)
76
+ links: [], // All links (including buttons)
77
77
  images: [],
78
78
  videos: [],
79
+ documents: [], // Files marked {role=pdf} — { url, name, mime, size?, alt,
80
+ // caption, preview?, … }
79
81
  icons: [],
80
82
  lists: [],
81
83
  quotes: [],
82
84
  insets: [], // Inline @Component references — { refId }
85
+ media: [], // Images, videos and insets on their own line, in the
86
+ // order written — each with its `kind`
83
87
  snippets: [], // Fenced code blocks — { language, code }
84
88
  data: {}, // Structured data (tagged data blocks, forms, cards)
85
89
  headings: [], // Only from nested content (quote/list bodies)
90
+ // tables: [], // Only when the content has one — { rows, attrs }
91
+ // math: [], // Only when the content has one — { id, latex, mathml }
86
92
 
87
93
  // Additional content groups (from headings after content)
88
94
  // Each item has the SAME flat structure as the top level — title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Semantic parser for ProseMirror/TipTap content structures",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ import { processSequence, resolveAssetUrl, ASSET_SLOTS } from "./processors/sequ
2
2
  import { processGroups } from "./processors/groups.js";
3
3
  import { buildDoc } from "./builders/doc.js";
4
4
  import { lintContent } from "./lint.js";
5
+ import { MIME_TYPES, mimeFor } from "./utils/mime.js";
5
6
 
6
7
  /**
7
8
  * Parse ProseMirror/TipTap content into semantic structure
@@ -54,4 +55,6 @@ function parseContent(doc, options = {}) {
54
55
  // `lintContent` diagnoses grouping near-misses (see src/lint.js). It never
55
56
  // affects a parse — parsing stays deterministic; the judgment lives here so
56
57
  // the CLI, the dev server, and an editor surface the same findings.
57
- export { parseContent, buildDoc, resolveAssetUrl, ASSET_SLOTS, lintContent };
58
+ // `MIME_TYPES` / `mimeFor` are the one extension → media-type table: a document's
59
+ // `mime` here, a file's in the build (src/utils/mime.js).
60
+ export { parseContent, buildDoc, resolveAssetUrl, ASSET_SLOTS, lintContent, MIME_TYPES, mimeFor };
@@ -17,6 +17,7 @@ function flattenGroup(group) {
17
17
  videos: group.body.videos || [],
18
18
  documents: group.body.documents || [],
19
19
  insets: group.body.insets || [],
20
+ media: group.body.media || [],
20
21
  snippets: group.body.snippets || [],
21
22
  data: group.body.data || {},
22
23
  quotes: group.body.quotes || [],
@@ -69,6 +70,7 @@ function processGroups(sequence, options = {}) {
69
70
  lists: [],
70
71
  videos: [],
71
72
  insets: [],
73
+ media: [],
72
74
  snippets: [],
73
75
  data: {},
74
76
  quotes: [],
@@ -113,6 +115,7 @@ function processGroups(sequence, options = {}) {
113
115
  lists: [],
114
116
  videos: [],
115
117
  insets: [],
118
+ media: [],
116
119
  data: {},
117
120
  quotes: [],
118
121
  headings: [],
@@ -306,6 +309,13 @@ function processGroupContent(elements, options = {}) {
306
309
  videos: [],
307
310
  documents: [],
308
311
  insets: [],
312
+ // ⭐ The group's visual media — each block-level image, video and inset, in the
313
+ // order written, tagged with its `kind`: what a media slot holds. `images`,
314
+ // `videos` and `insets` keep what they hold, and keep the author's order only
315
+ // within one kind; across kinds only this does (and `sequence`). An image
316
+ // inside a sentence belongs to the text, so it is not here — nor is a
317
+ // document, which is not visual media.
318
+ media: [],
309
319
  snippets: [],
310
320
  paragraphs: [],
311
321
  links: [],
@@ -382,11 +392,13 @@ function processGroupContent(elements, options = {}) {
382
392
  body.icons.push(element.attrs);
383
393
  } else {
384
394
  body.images.push(preserveProps);
395
+ body.media.push({ ...preserveProps, kind: "image" });
385
396
  }
386
397
  break;
387
398
 
388
399
  case "video":
389
400
  body.videos.push(preserveProps);
401
+ body.media.push({ ...preserveProps, kind: "video" });
390
402
  break;
391
403
 
392
404
  case "document":
@@ -499,6 +511,7 @@ function processGroupContent(elements, options = {}) {
499
511
 
500
512
  case "inset":
501
513
  body.insets.push({ refId: element.refId });
514
+ body.media.push({ refId: element.refId, kind: "inset" });
502
515
  break;
503
516
 
504
517
  case "form":
@@ -1,3 +1,5 @@
1
+ import { mimeFor, fileNameOf } from "../utils/mime.js";
2
+
1
3
  /**
2
4
  * HTML-attribute encoding for values placed inside double-quoted
3
5
  * attributes. Backslashes and braces (common in LaTeX) pass through
@@ -830,6 +832,7 @@ function parseMarkdownDocument(itemAttrs, options) {
830
832
  parseImgBlock(itemAttrs || {}, options);
831
833
  return {
832
834
  url,
835
+ ...fileFacts(url, itemAttrs),
833
836
  alt,
834
837
  caption,
835
838
  role,
@@ -842,6 +845,25 @@ function parseMarkdownDocument(itemAttrs, options) {
842
845
  };
843
846
  }
844
847
 
848
+ /**
849
+ * A document's file, as a file record's value names it: `name` (the file's name) and
850
+ * `mime` from its address, and `size` — known only where the file is — when a
851
+ * producer stamped one. A stamped `name` or `mime` wins over the address: the build
852
+ * copies a local file under a hashed name, so it stamps the one the author gave it.
853
+ * ✅ The field names are a file record's [Diego, 2026-09-29], so one component can
854
+ * render a downloads list from either.
855
+ */
856
+ function fileFacts(url, attrs = {}) {
857
+ const name = typeof attrs?.name === "string" && attrs.name ? attrs.name : fileNameOf(url);
858
+ const mime = typeof attrs?.mime === "string" && attrs.mime ? attrs.mime : name ? mimeFor(name) : "";
859
+ const size = Number.isFinite(attrs?.size) ? attrs.size : null;
860
+ return {
861
+ ...(name && { name }),
862
+ ...(mime && { mime }),
863
+ ...(size !== null && { size }),
864
+ };
865
+ }
866
+
845
867
  // The editor's older `document` node — its `title`, `coverImg` and `src` (or
846
868
  // asset identifier) — read into the same shape a markdown document has.
847
869
  function documentFromEditorNode(itemAttrs) {
@@ -855,8 +877,10 @@ function documentFromEditorNode(itemAttrs) {
855
877
  description,
856
878
  fileType,
857
879
  } = parseDocumentBlock(itemAttrs || {});
880
+ const url = href || downloadUrl || "";
858
881
  return {
859
- url: href || downloadUrl || "",
882
+ url,
883
+ ...fileFacts(url, itemAttrs),
860
884
  alt: alt || title,
861
885
  caption: title,
862
886
  role: "pdf",
@@ -0,0 +1,90 @@
1
+ /**
2
+ * What a file is, by its extension — the one extension → media-type table.
3
+ *
4
+ * ⭐ ONE TABLE, HERE, because two producers need it and this is the package both can
5
+ * reach: the parser gives a document its `mime` from its address, and the build gives
6
+ * a file record — and a document file it copies — theirs (`@uniweb/build`'s
7
+ * `site/file-records.js` imports `mimeFor` from here, as it imports `ASSET_SLOTS`).
8
+ * The parser has no dependencies, so the table cannot live in the build and be
9
+ * reached from here. ⛔ Do not write a second.
10
+ *
11
+ * Anything not here is `application/octet-stream`.
12
+ */
13
+ export const MIME_TYPES = {
14
+ pdf: "application/pdf",
15
+ doc: "application/msword",
16
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
17
+ xls: "application/vnd.ms-excel",
18
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
19
+ ppt: "application/vnd.ms-powerpoint",
20
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
21
+ odt: "application/vnd.oasis.opendocument.text",
22
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
23
+ odp: "application/vnd.oasis.opendocument.presentation",
24
+ rtf: "application/rtf",
25
+ epub: "application/epub+zip",
26
+ zip: "application/zip",
27
+ gz: "application/gzip",
28
+ tar: "application/x-tar",
29
+ csv: "text/csv",
30
+ tsv: "text/tab-separated-values",
31
+ txt: "text/plain",
32
+ md: "text/markdown",
33
+ json: "application/json",
34
+ xml: "application/xml",
35
+ yml: "application/yaml",
36
+ yaml: "application/yaml",
37
+ html: "text/html",
38
+ png: "image/png",
39
+ jpg: "image/jpeg",
40
+ jpeg: "image/jpeg",
41
+ gif: "image/gif",
42
+ svg: "image/svg+xml",
43
+ webp: "image/webp",
44
+ avif: "image/avif",
45
+ mp3: "audio/mpeg",
46
+ wav: "audio/wav",
47
+ ogg: "audio/ogg",
48
+ m4a: "audio/mp4",
49
+ mp4: "video/mp4",
50
+ webm: "video/webm",
51
+ mov: "video/quicktime",
52
+ };
53
+
54
+ /**
55
+ * The extension of a file name or path, lower-cased, without its dot — `''` for none,
56
+ * and for a dotfile (`.env`), as Node's `extname` reads them.
57
+ */
58
+ function extensionOf(name) {
59
+ const base = String(name || "").split(/[\\/]/).pop();
60
+ const dot = base.lastIndexOf(".");
61
+ return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
62
+ }
63
+
64
+ /**
65
+ * The media type of a file, by its extension.
66
+ *
67
+ * @param {string} name - a file name or path
68
+ * @returns {string}
69
+ */
70
+ export function mimeFor(name) {
71
+ return MIME_TYPES[extensionOf(name)] || "application/octet-stream";
72
+ }
73
+
74
+ /**
75
+ * The file name an address ends in — `/files/Annual%20report.pdf?v=2` →
76
+ * `Annual report.pdf` — or `''` when it names none.
77
+ *
78
+ * @param {string} url
79
+ * @returns {string}
80
+ */
81
+ export function fileNameOf(url) {
82
+ const text = String(url || "");
83
+ if (/^data:/i.test(text)) return "";
84
+ const segment = text.split(/[?#]/)[0].split("/").pop() || "";
85
+ try {
86
+ return decodeURIComponent(segment);
87
+ } catch {
88
+ return segment;
89
+ }
90
+ }