@uniweb/semantic-parser 1.1.17 → 1.1.19
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 +5 -5
- package/src/builders/doc.js +8 -3
- package/src/processors/sequence.js +94 -39
- package/vitest.config.js +9 -0
- package/.eslintrc.json +0 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/semantic-parser",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
4
4
|
"description": "Semantic parser for ProseMirror/TipTap content structures",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"author": "Proximify Inc.",
|
|
18
18
|
"license": "GPL-3.0-or-later",
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"
|
|
20
|
+
"vitest": "^4.1.7"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"yaml": "^2.8.2"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"test": "
|
|
38
|
-
"test-report": "
|
|
39
|
-
"test:groups": "
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test-report": "vitest run --reporter=json --outputFile=test-results.json",
|
|
39
|
+
"test:groups": "vitest run tests/processors/groups.test.js"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/builders/doc.js
CHANGED
|
@@ -57,11 +57,16 @@ function imageBlock({ src, alt = '', caption = '', direction, role, width, heigh
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function iconNode({ src, svg, library, name, size, color }) {
|
|
60
|
-
// UniwebIcon supports multiple source types
|
|
60
|
+
// UniwebIcon supports multiple source types.
|
|
61
|
+
//
|
|
62
|
+
// The family rides INSIDE `name` as `family:id`. The editor's node declares
|
|
63
|
+
// `{ name, svg, url, size, color, preserveColors, info }` and no `library`,
|
|
64
|
+
// and ProseMirror silently drops undeclared attrs on `fromJSON` — so emitting
|
|
65
|
+
// a separate `library` lost the family without a trace, on the path that
|
|
66
|
+
// builds starter content for every new section.
|
|
61
67
|
const attrs = {}
|
|
62
68
|
if (svg || src) attrs.svg = svg || src
|
|
63
|
-
if (
|
|
64
|
-
if (name) attrs.name = name
|
|
69
|
+
if (name) attrs.name = library ? `${library}:${name}` : name
|
|
65
70
|
if (size) attrs.size = size
|
|
66
71
|
if (color) attrs.color = color
|
|
67
72
|
return { type: 'UniwebIcon', attrs }
|
|
@@ -318,10 +318,80 @@ function createSequenceElement(node, options = {}) {
|
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// Extensions that make a link a download rather than a navigation.
|
|
322
|
+
const FILE_LINK_EXTENSIONS = [
|
|
323
|
+
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx",
|
|
324
|
+
"jpg", "jpeg", "png", "webp", "gif", "svg",
|
|
325
|
+
"mp4", "mp3", "wav", "mov", "zip",
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Find a node's link mark, if it carries one.
|
|
330
|
+
*/
|
|
331
|
+
function getLinkMark(node) {
|
|
332
|
+
return (node?.marks || []).find((mark) => mark.type === "link") || null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Whether two nodes belong to the same link.
|
|
337
|
+
*
|
|
338
|
+
* Marked-up text inside a label (`[read *this*](/x)`) arrives as several
|
|
339
|
+
* adjacent nodes that each carry the same link mark. They are one link and
|
|
340
|
+
* must produce one anchor, not one anchor apiece.
|
|
341
|
+
*/
|
|
342
|
+
function sameLink(a, b) {
|
|
343
|
+
if (!a || !b) return false;
|
|
344
|
+
return JSON.stringify(a.attrs || {}) === JSON.stringify(b.attrs || {});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Wrap already-styled inline content in an anchor.
|
|
349
|
+
*/
|
|
350
|
+
function wrapLink(inner, linkMark) {
|
|
351
|
+
const href = linkMark.attrs.href;
|
|
352
|
+
const target = linkMark.attrs.target || "_self";
|
|
353
|
+
const extension = href.split(".").pop()?.toLowerCase();
|
|
354
|
+
const isFileLink = FILE_LINK_EXTENSIONS.includes(extension);
|
|
355
|
+
|
|
356
|
+
return `<a href="${href}" target="${target}"${isFileLink ? " download" : ""}>${inner}</a>`;
|
|
357
|
+
}
|
|
358
|
+
|
|
321
359
|
function getTextContent(content, options = {}) {
|
|
322
360
|
if (!content) return "";
|
|
323
361
|
|
|
324
|
-
|
|
362
|
+
// Group consecutive nodes that share a link mark, so one link yields one
|
|
363
|
+
// anchor however many nodes its label tokenized into. Everything else is
|
|
364
|
+
// a group of one.
|
|
365
|
+
const groups = [];
|
|
366
|
+
for (const node of content) {
|
|
367
|
+
const link = node?.type === "text" ? getLinkMark(node) : null;
|
|
368
|
+
const last = groups[groups.length - 1];
|
|
369
|
+
|
|
370
|
+
if (link && last?.link && sameLink(last.link, link)) {
|
|
371
|
+
last.nodes.push(node);
|
|
372
|
+
} else {
|
|
373
|
+
groups.push({ link, nodes: [node] });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return groups
|
|
378
|
+
.reduce((prev, group) => {
|
|
379
|
+
const inner = group.nodes.reduce(
|
|
380
|
+
(acc, node) => acc + renderInlineNode(node),
|
|
381
|
+
"",
|
|
382
|
+
);
|
|
383
|
+
if (!inner && !group.link) return prev;
|
|
384
|
+
return prev + (group.link ? wrapLink(inner, group.link) : inner);
|
|
385
|
+
}, "")
|
|
386
|
+
.trim();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Render one inline node to HTML, applying every mark EXCEPT link — the
|
|
391
|
+
* anchor is applied per group by getTextContent, since it may span nodes.
|
|
392
|
+
*/
|
|
393
|
+
function renderInlineNode(curr) {
|
|
394
|
+
return [curr]
|
|
325
395
|
.reduce((prev, curr) => {
|
|
326
396
|
const { type, marks = [], text } = curr;
|
|
327
397
|
|
|
@@ -376,6 +446,13 @@ function getTextContent(content, options = {}) {
|
|
|
376
446
|
styledText = `<span${attrString}>${styledText}</span>`;
|
|
377
447
|
}
|
|
378
448
|
|
|
449
|
+
// code — a bare <code>, styled by the theme rather than here.
|
|
450
|
+
// The mark was previously dropped, so inline code rendered as
|
|
451
|
+
// ordinary prose with no way for a foundation to tell it apart.
|
|
452
|
+
if (marks.some((mark) => mark.type === "code")) {
|
|
453
|
+
styledText = `<code>${styledText}</code>`;
|
|
454
|
+
}
|
|
455
|
+
|
|
379
456
|
// bold
|
|
380
457
|
if (marks.some((mark) => mark.type === "bold")) {
|
|
381
458
|
styledText = `<strong>${styledText}</strong>`;
|
|
@@ -386,40 +463,9 @@ function getTextContent(content, options = {}) {
|
|
|
386
463
|
styledText = `<em>${styledText}</em>`;
|
|
387
464
|
}
|
|
388
465
|
|
|
389
|
-
// link
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const href = linkMark.attrs.href;
|
|
393
|
-
const target = linkMark.attrs.target || "_self";
|
|
394
|
-
|
|
395
|
-
// Check if it's a file link (add download attribute)
|
|
396
|
-
const fileExtensions = [
|
|
397
|
-
"pdf",
|
|
398
|
-
"doc",
|
|
399
|
-
"docx",
|
|
400
|
-
"xls",
|
|
401
|
-
"xlsx",
|
|
402
|
-
"ppt",
|
|
403
|
-
"pptx",
|
|
404
|
-
"jpg",
|
|
405
|
-
"jpeg",
|
|
406
|
-
"png",
|
|
407
|
-
"webp",
|
|
408
|
-
"gif",
|
|
409
|
-
"svg",
|
|
410
|
-
"mp4",
|
|
411
|
-
"mp3",
|
|
412
|
-
"wav",
|
|
413
|
-
"mov",
|
|
414
|
-
"zip",
|
|
415
|
-
];
|
|
416
|
-
const extension = href.split(".").pop()?.toLowerCase();
|
|
417
|
-
const isFileLink = fileExtensions.includes(extension);
|
|
418
|
-
|
|
419
|
-
styledText = `<a href="${href}" target="${target}"${
|
|
420
|
-
isFileLink ? " download" : ""
|
|
421
|
-
}>${styledText}</a>`;
|
|
422
|
-
}
|
|
466
|
+
// The link mark is deliberately NOT applied here — it is the
|
|
467
|
+
// outermost wrapper and may span several nodes, so
|
|
468
|
+
// getTextContent applies it once per group.
|
|
423
469
|
|
|
424
470
|
return prev + styledText;
|
|
425
471
|
} else if (type === "math_inline") {
|
|
@@ -455,8 +501,7 @@ function getTextContent(content, options = {}) {
|
|
|
455
501
|
// console.warn(`unhandled text content type: ${type}`, curr);
|
|
456
502
|
return prev;
|
|
457
503
|
}
|
|
458
|
-
}, "")
|
|
459
|
-
.trim();
|
|
504
|
+
}, "");
|
|
460
505
|
}
|
|
461
506
|
|
|
462
507
|
function processInlineElements(content) {
|
|
@@ -580,8 +625,18 @@ function parseUniwebIcon(itemAttrs) {
|
|
|
580
625
|
if (preserveColors) icon.preserveColors = preserveColors;
|
|
581
626
|
if (href) icon.href = href;
|
|
582
627
|
if (target) icon.target = target;
|
|
583
|
-
|
|
584
|
-
|
|
628
|
+
// The editor carries the family inside `name` as `family:id`; the markdown
|
|
629
|
+
// pipeline carries them separately. Normalize to the separate form, which
|
|
630
|
+
// is what `<Icon library name />` consumes.
|
|
631
|
+
let resolvedLibrary = library;
|
|
632
|
+
let resolvedName = name;
|
|
633
|
+
if (!resolvedLibrary && typeof name === "string" && name.includes(":")) {
|
|
634
|
+
const [family, ...rest] = name.split(":");
|
|
635
|
+
resolvedLibrary = family;
|
|
636
|
+
resolvedName = rest.join(":");
|
|
637
|
+
}
|
|
638
|
+
if (resolvedLibrary) icon.library = resolvedLibrary;
|
|
639
|
+
if (resolvedName) icon.name = resolvedName;
|
|
585
640
|
|
|
586
641
|
return icon;
|
|
587
642
|
}
|
package/vitest.config.js
ADDED
package/.eslintrc.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"env": {
|
|
3
|
-
"node": true,
|
|
4
|
-
"es2021": true,
|
|
5
|
-
"jest": true
|
|
6
|
-
},
|
|
7
|
-
"extends": [
|
|
8
|
-
"eslint:recommended"
|
|
9
|
-
],
|
|
10
|
-
"parserOptions": {
|
|
11
|
-
"ecmaVersion": 2022,
|
|
12
|
-
"sourceType": "module"
|
|
13
|
-
},
|
|
14
|
-
"rules": {
|
|
15
|
-
"indent": ["error", 2],
|
|
16
|
-
"linebreak-style": ["error", "unix"],
|
|
17
|
-
"quotes": ["error", "single", { "avoidEscape": true }],
|
|
18
|
-
"semi": ["error", "always"],
|
|
19
|
-
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
|
20
|
-
"no-console": "off",
|
|
21
|
-
"no-constant-condition": ["error", { "checkLoops": false }]
|
|
22
|
-
},
|
|
23
|
-
"ignorePatterns": [
|
|
24
|
-
"lib/",
|
|
25
|
-
"dist/",
|
|
26
|
-
"node_modules/"
|
|
27
|
-
]
|
|
28
|
-
}
|