@uniweb/semantic-parser 1.1.16 → 1.1.18
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/processors/groups.js +12 -0
- package/src/processors/sequence.js +82 -37
- 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.18",
|
|
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/processors/groups.js
CHANGED
|
@@ -480,6 +480,18 @@ function processInlineElements(children, body) {
|
|
|
480
480
|
children.forEach((item) => {
|
|
481
481
|
if (item.type === "icon") {
|
|
482
482
|
body.icons.push(item.attrs);
|
|
483
|
+
} else if (item.type === "image") {
|
|
484
|
+
// Inline image inside a paragraph (e.g. `` next to
|
|
485
|
+
// text or icons that didn't qualify for block-level hoisting
|
|
486
|
+
// in content-reader). Mirror the top-level partition:
|
|
487
|
+
// icon-role goes to icons[], everything else to images[].
|
|
488
|
+
// Without this branch, inline images vanish from body.images
|
|
489
|
+
// and components reading `item.images?.[0]` see nothing.
|
|
490
|
+
if (item.attrs?.role === "icon") {
|
|
491
|
+
body.icons.push(item.attrs);
|
|
492
|
+
} else {
|
|
493
|
+
body.images.push(item.attrs);
|
|
494
|
+
}
|
|
483
495
|
} else if (item.type === "link") {
|
|
484
496
|
// Handle inline links extracted from paragraph text nodes
|
|
485
497
|
body.links.push(item.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) {
|
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
|
-
}
|