@juspay/svelte-ui-components 2.136.1 → 2.136.8

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.
@@ -0,0 +1,105 @@
1
+ import { Marked } from 'marked';
2
+ /**
3
+ * Chat content comes from models and users, not from the app's own templates,
4
+ * so the output must be safe without asking consumers to run a sanitizer:
5
+ * raw HTML never passes through (it renders as escaped text), and only these
6
+ * URL protocols survive on links. Everything else in the output is built by
7
+ * marked from markdown syntax alone.
8
+ */
9
+ const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
10
+ const SAFE_IMAGE_PROTOCOLS = new Set(['http:', 'https:']);
11
+ /**
12
+ * Character references that decode to an ASCII control character, plus the two
13
+ * named forms HTML defines for tab and newline. These are the smuggling vector:
14
+ * `jav	ascript:` is not a scheme to `new URL()`, so it reads as a harmless
15
+ * relative path -- but the browser decodes the reference back to a tab when it
16
+ * parses the attribute, and the WHATWG URL parser strips tabs and newlines from
17
+ * INSIDE a scheme, leaving `javascript:`. The validator and the browser
18
+ * therefore disagree about the same string, which is the whole bug.
19
+ */
20
+ const NUMERIC_REFERENCE = /&#(x[0-9a-f]+|[0-9]+);?/gi;
21
+ const NAMED_WHITESPACE_REFERENCE = /&(tab|newline);/gi;
22
+ // Matching control characters is the entire point: these are exactly what the
23
+ // URL parser silently removes, and what an attacker smuggles in as a character
24
+ // reference to hide a scheme.
25
+ // eslint-disable-next-line no-control-regex
26
+ const STRIPPED_BY_URL_PARSER = /[\u0000-\u0020\u007f]/g;
27
+ /**
28
+ * Normalise an href the way the BROWSER will see it, then check the protocol.
29
+ * Decoding first and stripping second is what closes the gap: whatever survives
30
+ * this is what the URL parser is actually handed at click time.
31
+ */
32
+ function normaliseForProtocolCheck(href) {
33
+ return href
34
+ .replace(NUMERIC_REFERENCE, (_match, code) => String.fromCodePoint(code[0].toLowerCase() === 'x' ? parseInt(code.slice(1), 16) : parseInt(code, 10)))
35
+ .replace(NAMED_WHITESPACE_REFERENCE, ' ')
36
+ .replace(STRIPPED_BY_URL_PARSER, '');
37
+ }
38
+ /**
39
+ * Relative URLs resolve against the placeholder base and come out `https:`, so
40
+ * they are allowed without a special case.
41
+ */
42
+ function hasSafeProtocol(href, allowList) {
43
+ try {
44
+ return allowList.has(new URL(normaliseForProtocolCheck(href), 'https://relative.invalid').protocol);
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ function escapeHtml(value) {
51
+ return value
52
+ .replace(/&/g, '&')
53
+ .replace(/</g, '&lt;')
54
+ .replace(/>/g, '&gt;')
55
+ .replace(/"/g, '&quot;')
56
+ .replace(/'/g, '&#39;');
57
+ }
58
+ const sanitizingRenderer = {
59
+ html(token) {
60
+ return escapeHtml(token.text);
61
+ },
62
+ link(token) {
63
+ if (hasSafeProtocol(token.href, SAFE_LINK_PROTOCOLS)) {
64
+ return false;
65
+ }
66
+ return escapeHtml(token.text);
67
+ },
68
+ image(token) {
69
+ if (hasSafeProtocol(token.href, SAFE_IMAGE_PROTOCOLS)) {
70
+ return false;
71
+ }
72
+ return escapeHtml(token.text);
73
+ }
74
+ };
75
+ /* Safe to share across SSR requests: each instance's configuration (renderer,
76
+ gfm, breaks) is fixed at construction and parse() takes no per-request state,
77
+ so the cache only ever holds config-immutable parsers keyed by option shape. */
78
+ const instances = new Map();
79
+ /* External links open in a new tab with `rel="noopener noreferrer"` — the same
80
+ default the library's Button/Card apply to `target="_blank"` anchors. The
81
+ default renderer never emits `target`, so the regex can only annotate, never
82
+ duplicate; relative, mailto: and tel: links keep same-tab navigation. */
83
+ const EXTERNAL_ANCHOR_PATTERN = /<a href="(https?:\/\/[^"]*)"/g;
84
+ function annotateExternalLinks(html) {
85
+ return html.replace(EXTERNAL_ANCHOR_PATTERN, '<a href="$1" target="_blank" rel="noopener noreferrer"');
86
+ }
87
+ function instanceFor(options) {
88
+ const breaks = options.breaks === true;
89
+ const key = breaks ? 'breaks' : 'default';
90
+ let instance = instances.get(key);
91
+ if (!instance) {
92
+ instance = new Marked({ gfm: true, breaks, renderer: sanitizingRenderer });
93
+ instances.set(key, instance);
94
+ }
95
+ return instance;
96
+ }
97
+ /**
98
+ * Markdown → HTML with the sanitizing pipeline above. Pure string transform —
99
+ * no DOM involved — so it renders identically on server and client.
100
+ */
101
+ export function renderMarkdown(markdown, options = {}) {
102
+ const instance = instanceFor(options);
103
+ const output = options.inline === true ? instance.parseInline(markdown) : instance.parse(markdown);
104
+ return typeof output === 'string' ? annotateExternalLinks(output) : '';
105
+ }
@@ -0,0 +1,27 @@
1
+ export type MarkdownTextProperties = OptionalMarkdownTextProperties & MandatoryMarkdownTextProperties;
2
+ export type MandatoryMarkdownTextProperties = {
3
+ /**
4
+ * Markdown source. Rendered through the library's sanitized-by-construction
5
+ * pipeline: raw HTML (block and inline) is escaped and shown as text, and
6
+ * link/image URLs outside the safe-protocol allow-list are stripped while
7
+ * their text is kept.
8
+ */
9
+ markdown: string;
10
+ };
11
+ export type OptionalMarkdownTextProperties = {
12
+ /** Render single newlines as `<br>` (GFM "breaks" mode). */
13
+ breaks?: boolean;
14
+ testId?: string;
15
+ classes?: string;
16
+ };
17
+ export type RenderMarkdownOptions = {
18
+ /** Render single newlines as `<br>` (GFM "breaks" mode). */
19
+ breaks?: boolean;
20
+ /**
21
+ * Parse as inline markdown: no block elements (`<p>`, lists, tables) are
22
+ * produced, so the output can sit inside an existing `<p>` or `<span>`.
23
+ * The same sanitization applies — inline raw HTML is escaped and unsafe
24
+ * link/image protocols are stripped.
25
+ */
26
+ inline?: boolean;
27
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -86,6 +86,8 @@ export { default as Chat } from './Chat/Chat.svelte';
86
86
  export { default as ChatHeader } from './ChatHeader/ChatHeader.svelte';
87
87
  export { default as ChatMessage } from './ChatMessage/ChatMessage.svelte';
88
88
  export { default as ChatMessageList } from './ChatMessageList/ChatMessageList.svelte';
89
+ export { default as MarkdownText } from './MarkdownText/MarkdownText.svelte';
90
+ export { renderMarkdown } from './MarkdownText/markdown';
89
91
  export { default as ChatComposer } from './ChatComposer/ChatComposer.svelte';
90
92
  export { default as ChatSuggestions } from './ChatSuggestions/ChatSuggestions.svelte';
91
93
  export { default as ChatToolStatus } from './ChatToolStatus/ChatToolStatus.svelte';
@@ -152,7 +154,7 @@ export type * from './ThemeSwitcher/properties';
152
154
  export type * from './ThinkingIndicator/properties';
153
155
  export type * from './ToolCallLog/properties';
154
156
  export type * from './TaskList/properties';
155
- export type * from './soundKit/properties';
157
+ export type * from './SoundKit/properties';
156
158
  export type * from './Book/properties';
157
159
  export type * from './Browser/properties';
158
160
  export type * from './Phone/properties';
@@ -187,6 +189,7 @@ export type * from './SpeechToText/types';
187
189
  export type * from './ChatHeader/properties';
188
190
  export type * from './ChatMessage/properties';
189
191
  export type * from './ChatMessageList/properties';
192
+ export type * from './MarkdownText/properties';
190
193
  export type * from './ChatComposer/properties';
191
194
  export type * from './ChatSuggestions/properties';
192
195
  export type * from './ChatToolStatus/properties';
@@ -197,6 +200,6 @@ export type * from './Draggable/properties';
197
200
  export type * from './MediaPlayer/properties';
198
201
  export type * from './MediaUpload/properties';
199
202
  export type * from './Gallery/properties';
200
- export { createSoundKit } from './soundKit/soundKit';
203
+ export { createSoundKit } from './SoundKit/SoundKit';
201
204
  export { validateInput, lockBodyScroll, unlockBodyScroll } from './utils';
202
205
  export { formatNumberIndian } from './_chart/format';
package/dist/index.js CHANGED
@@ -86,6 +86,8 @@ export { default as Chat } from './Chat/Chat.svelte';
86
86
  export { default as ChatHeader } from './ChatHeader/ChatHeader.svelte';
87
87
  export { default as ChatMessage } from './ChatMessage/ChatMessage.svelte';
88
88
  export { default as ChatMessageList } from './ChatMessageList/ChatMessageList.svelte';
89
+ export { default as MarkdownText } from './MarkdownText/MarkdownText.svelte';
90
+ export { renderMarkdown } from './MarkdownText/markdown';
89
91
  export { default as ChatComposer } from './ChatComposer/ChatComposer.svelte';
90
92
  export { default as ChatSuggestions } from './ChatSuggestions/ChatSuggestions.svelte';
91
93
  export { default as ChatToolStatus } from './ChatToolStatus/ChatToolStatus.svelte';
@@ -100,6 +102,6 @@ export { default as Gallery } from './Gallery/Gallery.svelte';
100
102
  export { ChatController } from './Chat/controller.svelte';
101
103
  export { partyOf } from './Chat/roles';
102
104
  export { SpeechToTextController } from './SpeechToText/controller.svelte';
103
- export { createSoundKit } from './soundKit/soundKit';
105
+ export { createSoundKit } from './SoundKit/SoundKit';
104
106
  export { validateInput, lockBodyScroll, unlockBodyScroll } from './utils';
105
107
  export { formatNumberIndian } from './_chart/format';