@sybilion/uilib 1.2.21 → 1.2.22

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.
@@ -56,6 +56,20 @@ const injectBold = (content) => {
56
56
  length: matches[0].length,
57
57
  };
58
58
  };
59
+ /** `_italic_` (underscore) — avoids clashing with `* ` bullet markers. */
60
+ const injectItalic = (content) => {
61
+ const matches = content.match(/(?<![A-Za-z0-9])_([^_\n]+?)_(?![A-Za-z0-9])/);
62
+ if (!matches || matches.index === undefined)
63
+ return null;
64
+ if (isInsideHtmlListOrTable(content, matches.index)) {
65
+ return null;
66
+ }
67
+ return {
68
+ elem: jsx("em", { children: matches[1] }),
69
+ index: matches.index,
70
+ length: matches[0].length,
71
+ };
72
+ };
59
73
  const injectBullet = (content) => {
60
74
  // Match bullet points: * or - followed by space
61
75
  // Match at start of string/line or after whitespace/colon (for nested bullets)
@@ -229,6 +243,33 @@ const isAllowedHref = (href) => {
229
243
  return true;
230
244
  return false;
231
245
  };
246
+ const normalizeWwwToHttps = (raw) => {
247
+ const t = raw.trim();
248
+ if (/^www\./i.test(t))
249
+ return `https://${t}`;
250
+ return t;
251
+ };
252
+ /** Strip ASCII closing punctuation often pasted after URLs. */
253
+ const stripTrailingUrlPunctuation = (s) => {
254
+ let u = s;
255
+ while (u.length > 0 && /[.,;:!?)}\]]$/u.test(u)) {
256
+ u = u.slice(0, -1);
257
+ }
258
+ return u;
259
+ };
260
+ function linkTargetRelForHref(href, explicitTarget) {
261
+ if (explicitTarget) {
262
+ const t = explicitTarget.trim();
263
+ if (t.toLowerCase() === '_blank') {
264
+ return { target: '_blank', rel: 'noopener noreferrer' };
265
+ }
266
+ return { target: t || undefined, rel: undefined };
267
+ }
268
+ if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) {
269
+ return { target: '_blank', rel: 'noopener noreferrer' };
270
+ }
271
+ return { target: undefined, rel: undefined };
272
+ }
232
273
  const runFormattingPipeline = (text, injectors) => {
233
274
  let result = [text];
234
275
  try {
@@ -260,6 +301,76 @@ const runFormattingPipeline = (text, injectors) => {
260
301
  }
261
302
  return result;
262
303
  };
304
+ const linkLabelInjectors = [
305
+ injectHTMLTags,
306
+ injectBold,
307
+ injectItalic,
308
+ injectNewlines,
309
+ ];
310
+ const injectMarkdownLink = (content) => {
311
+ const matches = content.match(/\[([^\]]+)\]\(([^)]+)\)/);
312
+ if (!matches || matches.index === undefined)
313
+ return null;
314
+ if (isInsideHtmlListOrTable(content, matches.index)) {
315
+ return null;
316
+ }
317
+ const href = normalizeWwwToHttps(matches[2].trim());
318
+ if (!isAllowedHref(href))
319
+ return null;
320
+ const { target, rel } = linkTargetRelForHref(href, undefined);
321
+ const label = matches[1];
322
+ return {
323
+ elem: (jsx("a", { href: href, target: target, rel: rel, children: runFormattingPipeline(label, linkLabelInjectors) })),
324
+ index: matches.index,
325
+ length: matches[0].length,
326
+ };
327
+ };
328
+ const injectAutolinkUrl = (content) => {
329
+ const candidates = [];
330
+ const reHttp = /https?:\/\/[^\s<>"']+/gi;
331
+ let hm;
332
+ while ((hm = reHttp.exec(content)) !== null) {
333
+ const href = stripTrailingUrlPunctuation(hm[0]);
334
+ if (href.length >= (href.startsWith('https://') ? 8 : 7) &&
335
+ isAllowedHref(href) &&
336
+ /^https?:\/\//i.test(href)) {
337
+ candidates.push({
338
+ index: hm.index,
339
+ length: href.length,
340
+ href,
341
+ display: href,
342
+ });
343
+ }
344
+ }
345
+ const reWww = /(^|[^A-Za-z0-9/])(www\.[^\s<>"']+)/gi;
346
+ while ((hm = reWww.exec(content)) !== null) {
347
+ const prefix = hm[1] ?? '';
348
+ const rawWww = hm[2];
349
+ const body = stripTrailingUrlPunctuation(rawWww);
350
+ const href = `https://${body}`;
351
+ if (body.length > 4 && isAllowedHref(href)) {
352
+ candidates.push({
353
+ index: hm.index + prefix.length,
354
+ length: body.length,
355
+ href,
356
+ display: body,
357
+ });
358
+ }
359
+ }
360
+ if (candidates.length === 0)
361
+ return null;
362
+ candidates.sort((a, b) => a.index - b.index);
363
+ const c = candidates[0];
364
+ if (isInsideHtmlListOrTable(content, c.index)) {
365
+ return null;
366
+ }
367
+ const { target, rel } = linkTargetRelForHref(c.href, undefined);
368
+ return {
369
+ elem: (jsx("a", { href: c.href, target: target, rel: rel, children: c.display })),
370
+ index: c.index,
371
+ length: c.length,
372
+ };
373
+ };
263
374
  const injectAnchor = (content) => {
264
375
  const regex = /<a\s+([^>]+)>([\s\S]*?)<\/a\s*>/i;
265
376
  const matches = content.match(regex);
@@ -281,15 +392,9 @@ const injectAnchor = (content) => {
281
392
  if (!href || !isAllowedHref(href)) {
282
393
  return null;
283
394
  }
284
- const opensNewTab = targetRaw.toLowerCase() === '_blank';
285
- const target = opensNewTab ? '_blank' : targetRaw || undefined;
286
- const rel = target === '_blank' ? 'noopener noreferrer' : undefined;
395
+ const { target, rel } = linkTargetRelForHref(href, targetRaw || undefined);
287
396
  return {
288
- elem: (jsx("a", { href: href, target: target, rel: rel, children: runFormattingPipeline(inner, [
289
- injectHTMLTags,
290
- injectBold,
291
- injectNewlines,
292
- ]) })),
397
+ elem: (jsx("a", { href: href, target: target, rel: rel, children: runFormattingPipeline(inner, linkLabelInjectors) })),
293
398
  index: matches.index,
294
399
  length: matches[0].length,
295
400
  };
@@ -297,16 +402,22 @@ const injectAnchor = (content) => {
297
402
  const applyFormatting = (text) => runFormattingPipeline(text, [
298
403
  injectHeaders,
299
404
  injectAnchor,
405
+ injectMarkdownLink,
300
406
  injectHTMLTags,
301
407
  injectBullet,
302
408
  injectBold,
409
+ injectItalic,
410
+ injectAutolinkUrl,
303
411
  injectNewlines,
304
412
  ]);
305
413
  const applyFormattingInline = (text) => runFormattingPipeline(text, [
306
414
  injectAnchor,
415
+ injectMarkdownLink,
307
416
  injectHTMLTags,
308
417
  injectBold,
418
+ injectItalic,
419
+ injectAutolinkUrl,
309
420
  injectNewlines,
310
421
  ]);
311
422
 
312
- export { applyFormatting, applyFormattingInline, convertMarkdownTableToHTML, injectAnchor, injectBold, injectBullet, injectHeaders, injectNewlines };
423
+ export { applyFormatting, applyFormattingInline, convertMarkdownTableToHTML, injectAnchor, injectBold, injectBullet, injectHeaders, injectItalic, injectNewlines };
@@ -1,5 +1,6 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import cn from 'classnames';
3
+ import { InteractiveContent } from '../../InteractiveContent/InteractiveContent.js';
3
4
  import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
4
5
  import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
5
6
  import { AgentMessageContent } from './AgentMessageContent.js';
@@ -9,7 +10,7 @@ import { UserCsvAttachmentBubble } from './UserCsvAttachmentBubble.js';
9
10
  function ChatMessage({ role, text, userCsvAttachment, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage = true, scriptContinue, onScriptContinue, renderMessageChart, }) {
10
11
  const isAssistant = role === MessageRole.ASSISTANT;
11
12
  const isSystem = role === MessageRole.SYSTEM;
12
- return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: text === GENERATING_DASHBOARD_SYSTEM_TEXT ? (jsx(TextShimmer, { as: "span", children: text })) : (text) })) : isAssistant ? (jsx(AgentMessageContent, { text: text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: S.text, children: text }), userCsvAttachment ? (jsx(UserCsvAttachmentBubble, { attachment: userCsvAttachment })) : null] })) }));
13
+ return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: text === GENERATING_DASHBOARD_SYSTEM_TEXT ? (jsx(TextShimmer, { as: "span", children: text })) : (text) })) : isAssistant ? (jsx(AgentMessageContent, { text: text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: S.text, children: jsx(InteractiveContent, { text: text }) }), userCsvAttachment ? (jsx(UserCsvAttachmentBubble, { attachment: userCsvAttachment })) : null] })) }));
13
14
  }
14
15
 
15
16
  export { ChatMessage };
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = ".InteractiveContent_root__FHnlY strong{font-weight:600}";
3
+ var css_248z = ".InteractiveContent_root__FHnlY strong{font-weight:600}.InteractiveContent_root__FHnlY em{font-style:italic}.InteractiveContent_root__FHnlY a{color:var(--sb-green-600);text-decoration:underline;text-underline-offset:2px}.InteractiveContent_root__FHnlY a:hover{color:var(--sb-green-700)}.dark .InteractiveContent_root__FHnlY a{color:var(--sb-green-400)}.dark .InteractiveContent_root__FHnlY a:hover{color:var(--sb-green-300)}";
4
4
  var S = {"root":"InteractiveContent_root__FHnlY"};
5
5
  styleInject(css_248z);
6
6
 
@@ -9,6 +9,12 @@ declare const injectBold: (content: string) => {
9
9
  index: number;
10
10
  length: number;
11
11
  };
12
+ /** `_italic_` (underscore) — avoids clashing with `* ` bullet markers. */
13
+ declare const injectItalic: (content: string) => {
14
+ elem: import("react/jsx-runtime").JSX.Element;
15
+ index: number;
16
+ length: number;
17
+ };
12
18
  declare const injectBullet: (content: string) => {
13
19
  elem: import("react/jsx-runtime").JSX.Element;
14
20
  index: number;
@@ -28,4 +34,4 @@ type Injector = (content: string) => {
28
34
  declare const injectAnchor: Injector;
29
35
  declare const applyFormatting: (text: string) => React.ReactNode[];
30
36
  declare const applyFormattingInline: (text: string) => React.ReactNode[];
31
- export { injectHeaders, injectAnchor, injectBold, injectBullet, injectNewlines, convertMarkdownTableToHTML, applyFormatting, applyFormattingInline, };
37
+ export { injectHeaders, injectAnchor, injectBold, injectItalic, injectBullet, injectNewlines, convertMarkdownTableToHTML, applyFormatting, applyFormattingInline, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.2.21",
3
+ "version": "1.2.22",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -77,6 +77,23 @@ const injectBold = (content: string) => {
77
77
  };
78
78
  };
79
79
 
80
+ /** `_italic_` (underscore) — avoids clashing with `* ` bullet markers. */
81
+ const injectItalic = (content: string) => {
82
+ const matches = content.match(/(?<![A-Za-z0-9])_([^_\n]+?)_(?![A-Za-z0-9])/);
83
+
84
+ if (!matches || matches.index === undefined) return null;
85
+
86
+ if (isInsideHtmlListOrTable(content, matches.index)) {
87
+ return null;
88
+ }
89
+
90
+ return {
91
+ elem: <em>{matches[1]}</em>,
92
+ index: matches.index,
93
+ length: matches[0].length,
94
+ };
95
+ };
96
+
80
97
  const injectBullet = (content: string) => {
81
98
  // Match bullet points: * or - followed by space
82
99
  // Match at start of string/line or after whitespace/colon (for nested bullets)
@@ -281,6 +298,38 @@ const isAllowedHref = (href: string): boolean => {
281
298
  return false;
282
299
  };
283
300
 
301
+ const normalizeWwwToHttps = (raw: string): string => {
302
+ const t = raw.trim();
303
+ if (/^www\./i.test(t)) return `https://${t}`;
304
+ return t;
305
+ };
306
+
307
+ /** Strip ASCII closing punctuation often pasted after URLs. */
308
+ const stripTrailingUrlPunctuation = (s: string): string => {
309
+ let u = s;
310
+ while (u.length > 0 && /[.,;:!?)}\]]$/u.test(u)) {
311
+ u = u.slice(0, -1);
312
+ }
313
+ return u;
314
+ };
315
+
316
+ function linkTargetRelForHref(
317
+ href: string,
318
+ explicitTarget?: string,
319
+ ): { target?: string; rel?: string } {
320
+ if (explicitTarget) {
321
+ const t = explicitTarget.trim();
322
+ if (t.toLowerCase() === '_blank') {
323
+ return { target: '_blank', rel: 'noopener noreferrer' };
324
+ }
325
+ return { target: t || undefined, rel: undefined };
326
+ }
327
+ if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) {
328
+ return { target: '_blank', rel: 'noopener noreferrer' };
329
+ }
330
+ return { target: undefined, rel: undefined };
331
+ }
332
+
284
333
  type Injector = (
285
334
  content: string,
286
335
  ) => { elem: React.ReactNode; index: number; length: number } | null;
@@ -325,6 +374,100 @@ const runFormattingPipeline = (
325
374
  return result;
326
375
  };
327
376
 
377
+ const linkLabelInjectors: Injector[] = [
378
+ injectHTMLTags,
379
+ injectBold,
380
+ injectItalic,
381
+ injectNewlines,
382
+ ];
383
+
384
+ const injectMarkdownLink: Injector = (content: string) => {
385
+ const matches = content.match(/\[([^\]]+)\]\(([^)]+)\)/);
386
+
387
+ if (!matches || matches.index === undefined) return null;
388
+
389
+ if (isInsideHtmlListOrTable(content, matches.index)) {
390
+ return null;
391
+ }
392
+
393
+ const href = normalizeWwwToHttps(matches[2].trim());
394
+ if (!isAllowedHref(href)) return null;
395
+
396
+ const { target, rel } = linkTargetRelForHref(href, undefined);
397
+ const label = matches[1];
398
+
399
+ return {
400
+ elem: (
401
+ <a href={href} target={target} rel={rel}>
402
+ {runFormattingPipeline(label, linkLabelInjectors)}
403
+ </a>
404
+ ),
405
+ index: matches.index,
406
+ length: matches[0].length,
407
+ };
408
+ };
409
+
410
+ const injectAutolinkUrl: Injector = (content: string) => {
411
+ type Cand = { index: number; length: number; href: string; display: string };
412
+
413
+ const candidates: Cand[] = [];
414
+
415
+ const reHttp = /https?:\/\/[^\s<>"']+/gi;
416
+ let hm: RegExpExecArray | null;
417
+ while ((hm = reHttp.exec(content)) !== null) {
418
+ const href = stripTrailingUrlPunctuation(hm[0]);
419
+ if (
420
+ href.length >= (href.startsWith('https://') ? 8 : 7) &&
421
+ isAllowedHref(href) &&
422
+ /^https?:\/\//i.test(href)
423
+ ) {
424
+ candidates.push({
425
+ index: hm.index,
426
+ length: href.length,
427
+ href,
428
+ display: href,
429
+ });
430
+ }
431
+ }
432
+
433
+ const reWww = /(^|[^A-Za-z0-9/])(www\.[^\s<>"']+)/gi;
434
+ while ((hm = reWww.exec(content)) !== null) {
435
+ const prefix = hm[1] ?? '';
436
+ const rawWww = hm[2];
437
+ const body = stripTrailingUrlPunctuation(rawWww);
438
+ const href = `https://${body}`;
439
+ if (body.length > 4 && isAllowedHref(href)) {
440
+ candidates.push({
441
+ index: hm.index + prefix.length,
442
+ length: body.length,
443
+ href,
444
+ display: body,
445
+ });
446
+ }
447
+ }
448
+
449
+ if (candidates.length === 0) return null;
450
+
451
+ candidates.sort((a, b) => a.index - b.index);
452
+ const c = candidates[0]!;
453
+
454
+ if (isInsideHtmlListOrTable(content, c.index)) {
455
+ return null;
456
+ }
457
+
458
+ const { target, rel } = linkTargetRelForHref(c.href, undefined);
459
+
460
+ return {
461
+ elem: (
462
+ <a href={c.href} target={target} rel={rel}>
463
+ {c.display}
464
+ </a>
465
+ ),
466
+ index: c.index,
467
+ length: c.length,
468
+ };
469
+ };
470
+
328
471
  const injectAnchor: Injector = (content: string) => {
329
472
  const regex = /<a\s+([^>]+)>([\s\S]*?)<\/a\s*>/i;
330
473
  const matches = content.match(regex);
@@ -356,18 +499,12 @@ const injectAnchor: Injector = (content: string) => {
356
499
  return null;
357
500
  }
358
501
 
359
- const opensNewTab = targetRaw.toLowerCase() === '_blank';
360
- const target = opensNewTab ? '_blank' : targetRaw || undefined;
361
- const rel = target === '_blank' ? 'noopener noreferrer' : undefined;
502
+ const { target, rel } = linkTargetRelForHref(href, targetRaw || undefined);
362
503
 
363
504
  return {
364
505
  elem: (
365
506
  <a href={href} target={target} rel={rel}>
366
- {runFormattingPipeline(inner, [
367
- injectHTMLTags,
368
- injectBold,
369
- injectNewlines,
370
- ])}
507
+ {runFormattingPipeline(inner, linkLabelInjectors)}
371
508
  </a>
372
509
  ),
373
510
  index: matches.index!,
@@ -379,17 +516,23 @@ const applyFormatting = (text: string): React.ReactNode[] =>
379
516
  runFormattingPipeline(text, [
380
517
  injectHeaders,
381
518
  injectAnchor,
519
+ injectMarkdownLink,
382
520
  injectHTMLTags,
383
521
  injectBullet,
384
522
  injectBold,
523
+ injectItalic,
524
+ injectAutolinkUrl,
385
525
  injectNewlines,
386
526
  ]);
387
527
 
388
528
  const applyFormattingInline = (text: string): React.ReactNode[] =>
389
529
  runFormattingPipeline(text, [
390
530
  injectAnchor,
531
+ injectMarkdownLink,
391
532
  injectHTMLTags,
392
533
  injectBold,
534
+ injectItalic,
535
+ injectAutolinkUrl,
393
536
  injectNewlines,
394
537
  ]);
395
538
 
@@ -397,6 +540,7 @@ export {
397
540
  injectHeaders,
398
541
  injectAnchor,
399
542
  injectBold,
543
+ injectItalic,
400
544
  injectBullet,
401
545
  injectNewlines,
402
546
  convertMarkdownTableToHTML,
@@ -1,5 +1,7 @@
1
1
  import cn from 'classnames';
2
2
 
3
+ import { InteractiveContent } from '#uilib/components/ui/InteractiveContent';
4
+
3
5
  import { TextShimmer } from '../../TextShimmer';
4
6
  import {
5
7
  type ChatMessageProps,
@@ -48,7 +50,9 @@ export function ChatMessage({
48
50
  />
49
51
  ) : (
50
52
  <div className={S.userColumn}>
51
- <div className={S.text}>{text}</div>
53
+ <div className={S.text}>
54
+ <InteractiveContent text={text} />
55
+ </div>
52
56
  {userCsvAttachment ? (
53
57
  <UserCsvAttachmentBubble attachment={userCsvAttachment} />
54
58
  ) : null}
@@ -1,3 +1,20 @@
1
1
  .root
2
2
  strong
3
3
  font-weight 600
4
+
5
+ em
6
+ font-style italic
7
+
8
+ a
9
+ color var(--sb-green-600)
10
+ text-decoration underline
11
+ text-underline-offset 2px
12
+
13
+ &:hover
14
+ color var(--sb-green-700)
15
+
16
+ :global(.dark) &
17
+ color var(--sb-green-400)
18
+
19
+ &:hover
20
+ color var(--sb-green-300)