@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 +7 -1
- package/package.json +1 -1
- package/src/index.js +4 -1
- package/src/processors/groups.js +13 -0
- package/src/processors/sequence.js +25 -1
- package/src/utils/mime.js +90 -0
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
|
|
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
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
|
-
|
|
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 };
|
package/src/processors/groups.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|