@uniweb/semantic-parser 1.1.17 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.1.17",
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
- "jest": "^29.7.0"
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": "NODE_OPTIONS=--experimental-vm-modules jest",
38
- "test-report": "NODE_OPTIONS=--experimental-vm-modules jest --json > test-results.json 2>&1",
39
- "test:groups": "NODE_OPTIONS=--experimental-vm-modules jest tests/processors/groups.test.js"
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
  }
@@ -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
- return content
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 (outermost)
390
- if (marks.some((mark) => mark.type === "link")) {
391
- const linkMark = marks.find((mark) => mark.type === "link");
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) {
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.js']
8
+ }
9
+ })
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
- }