@uniformdev/richtext 20.72.2-alpha.3 → 20.72.3-alpha.14

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/dist/index.d.mts CHANGED
@@ -28,6 +28,29 @@ type NodeStringRenderer = (props: NodeStringRendererProps) => string;
28
28
  type ParameterRichTextValue = SerializedEditorState | undefined | null;
29
29
  type RichTextBuiltInFormat = 'code' | 'bold' | 'italic' | 'underline' | 'strikethrough' | 'superscript' | 'subscript';
30
30
  type RichTextBuiltInElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'unorderedList' | 'orderedList' | 'link' | 'quote' | 'code' | 'variable' | 'table' | 'asset';
31
+ /**
32
+ * One entry in the link-attributes config list.
33
+ * `options`, `label`, and `helpText` are optional presentation hints.
34
+ * The React SDK translates the literal `class` key to `className` on output.
35
+ */
36
+ interface LinkAttrConfig {
37
+ key: string;
38
+ /** When present, the editor renders a select; the value must be one of these. */
39
+ options?: Array<{
40
+ name: string;
41
+ value: string;
42
+ }>;
43
+ label?: string;
44
+ helpText?: string;
45
+ }
46
+ interface LinkAttributesConfiguration {
47
+ /** When true, the editor sees an Anchor input; the fragment lives on value.path. */
48
+ enableAnchor?: boolean;
49
+ /** Flat, ordered list of attribute entries a project wants on its links. */
50
+ entries?: Array<LinkAttrConfig>;
51
+ }
52
+ /** Normalize a stored entry for rendering — fills in a default label. */
53
+ declare function resolveLinkAttrConfig(entry: LinkAttrConfig): Required<Pick<LinkAttrConfig, 'key' | 'label'>> & Pick<LinkAttrConfig, 'options' | 'helpText'>;
31
54
  interface RichTextParamConfiguration {
32
55
  required?: boolean;
33
56
  formatting?: {
@@ -40,6 +63,8 @@ interface RichTextParamConfiguration {
40
63
  asset?: {
41
64
  allowedTypes?: AssetDefinitionType[];
42
65
  };
66
+ /** Optional anchor (fragment) toggle and custom attribute config for the link element. */
67
+ link?: LinkAttributesConfiguration;
43
68
  };
44
69
  }
45
70
 
@@ -65,12 +90,34 @@ type ProjectMapLinkParamValue = {
65
90
  nodeId: string;
66
91
  path: string;
67
92
  dynamicInputValues?: Record<string, string>;
93
+ attributes?: Record<string, string>;
68
94
  };
69
95
  type NonProjectMapLinkParamValue = {
70
96
  type: Exclude<LinkParameterType, 'projectMapNode'>;
71
97
  path: string;
98
+ attributes?: Record<string, string>;
72
99
  };
73
100
  type LinkParamValue = ProjectMapLinkParamValue | NonProjectMapLinkParamValue | undefined;
101
+ /**
102
+ * True for attribute keys the Uniform web SDKs strip at render time: inline event
103
+ * handlers (`on*`) and dangerous attribute names (href, src, style, formaction).
104
+ * Authoring UIs should use this to warn before such keys are ever persisted.
105
+ */
106
+ declare function isDangerousLinkAttributeKey(key: string): boolean;
107
+ /**
108
+ * Converts a link parameter value into a plain HTML attribute bag suitable for
109
+ * an `<a>` element. Applies a web-channel security denylist (event handlers,
110
+ * script-executing URL schemes on the href and any value, dangerous attribute names) and auto-adds
111
+ * `rel="noopener noreferrer"` for any case variant of `target="_blank"`.
112
+ */
113
+ declare function linkParamValueToHtmlAttributes(link: LinkParamValue): Record<string, string>;
114
+ /**
115
+ * Converts a link parameter value into a React-compatible anchor prop bag.
116
+ * Delegates to `linkParamValueToHtmlAttributes` for the security denylist /
117
+ * auto-rel logic, then renames the literal `class` key to `className` for React.
118
+ */
119
+ declare function linkParamValueToAnchorProps(link: LinkParamValue): Record<string, string>;
120
+ /** Derives the href string from a link parameter value. */
74
121
  declare function linkParamValueToHref(link: LinkParamValue): string | undefined;
75
122
 
76
123
  interface ListNode extends RichTextNode {
@@ -223,4 +270,4 @@ declare const emptyRichTextValue: {
223
270
  };
224
271
  declare const defaultParameterConfiguration: RichTextParamConfiguration;
225
272
 
226
- export { type AssetNode, type HeadingNode, type LinkNode, type ListItemNode, type ListNode, type NodeStringRenderer, type NodeStringRendererProps, type ParagraphNode, type ParameterRichTextValue, type ResolveStringRenderer, type RichTextBuiltInElement, type RichTextBuiltInFormat, type RichTextNode, type RichTextNodeWithChildren, type RichTextParamConfiguration, type StringRenderContext, type TableCellNode, type TableNode, type TableRowNode, type TextNode, assetHtmlRenderer, defaultParameterConfiguration, emptyRichTextValue, getLabelForElement, getLabelForFormat, getRichTextTagFromTableCellHeaderState, getRichTextTagsFromTextFormat, hasChildren, headingHtmlRenderer, isArrayWithLength, isPureDirection, isPureTextAlign, isRichTextNode, isRichTextNodeType, isRichTextValue, isRichTextValueConsideredEmpty, linkHtmlRenderer, linkParamValueToHref, listHtmlRenderer, listitemHtmlRenderer, paragraphHtmlRenderer, purifyText, renderChildrenToHtml, renderChildrenToText, renderHtmlElement, renderToHtml, renderToText, resolveDefaultRenderer$1 as resolveDefaultHtmlRenderer, resolveDefaultRenderer as resolveDefaultTextRenderer, richTextBuiltInElements, richTextBuiltInFormats, rootHtmlRenderer, tableHtmlRenderer, tablecellHtmlRenderer, tablerowHtmlRenderer, textHtmlRenderer, walkRichTextTree };
273
+ export { type AssetNode, type HeadingNode, type LinkAttrConfig, type LinkAttributesConfiguration, type LinkNode, type ListItemNode, type ListNode, type NodeStringRenderer, type NodeStringRendererProps, type ParagraphNode, type ParameterRichTextValue, type ResolveStringRenderer, type RichTextBuiltInElement, type RichTextBuiltInFormat, type RichTextNode, type RichTextNodeWithChildren, type RichTextParamConfiguration, type StringRenderContext, type TableCellNode, type TableNode, type TableRowNode, type TextNode, assetHtmlRenderer, defaultParameterConfiguration, emptyRichTextValue, getLabelForElement, getLabelForFormat, getRichTextTagFromTableCellHeaderState, getRichTextTagsFromTextFormat, hasChildren, headingHtmlRenderer, isArrayWithLength, isDangerousLinkAttributeKey, isPureDirection, isPureTextAlign, isRichTextNode, isRichTextNodeType, isRichTextValue, isRichTextValueConsideredEmpty, linkHtmlRenderer, linkParamValueToAnchorProps, linkParamValueToHref, linkParamValueToHtmlAttributes, listHtmlRenderer, listitemHtmlRenderer, paragraphHtmlRenderer, purifyText, renderChildrenToHtml, renderChildrenToText, renderHtmlElement, renderToHtml, renderToText, resolveDefaultRenderer$1 as resolveDefaultHtmlRenderer, resolveDefaultRenderer as resolveDefaultTextRenderer, resolveLinkAttrConfig, richTextBuiltInElements, richTextBuiltInFormats, rootHtmlRenderer, tableHtmlRenderer, tablecellHtmlRenderer, tablerowHtmlRenderer, textHtmlRenderer, walkRichTextTree };
package/dist/index.d.ts CHANGED
@@ -28,6 +28,29 @@ type NodeStringRenderer = (props: NodeStringRendererProps) => string;
28
28
  type ParameterRichTextValue = SerializedEditorState | undefined | null;
29
29
  type RichTextBuiltInFormat = 'code' | 'bold' | 'italic' | 'underline' | 'strikethrough' | 'superscript' | 'subscript';
30
30
  type RichTextBuiltInElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'unorderedList' | 'orderedList' | 'link' | 'quote' | 'code' | 'variable' | 'table' | 'asset';
31
+ /**
32
+ * One entry in the link-attributes config list.
33
+ * `options`, `label`, and `helpText` are optional presentation hints.
34
+ * The React SDK translates the literal `class` key to `className` on output.
35
+ */
36
+ interface LinkAttrConfig {
37
+ key: string;
38
+ /** When present, the editor renders a select; the value must be one of these. */
39
+ options?: Array<{
40
+ name: string;
41
+ value: string;
42
+ }>;
43
+ label?: string;
44
+ helpText?: string;
45
+ }
46
+ interface LinkAttributesConfiguration {
47
+ /** When true, the editor sees an Anchor input; the fragment lives on value.path. */
48
+ enableAnchor?: boolean;
49
+ /** Flat, ordered list of attribute entries a project wants on its links. */
50
+ entries?: Array<LinkAttrConfig>;
51
+ }
52
+ /** Normalize a stored entry for rendering — fills in a default label. */
53
+ declare function resolveLinkAttrConfig(entry: LinkAttrConfig): Required<Pick<LinkAttrConfig, 'key' | 'label'>> & Pick<LinkAttrConfig, 'options' | 'helpText'>;
31
54
  interface RichTextParamConfiguration {
32
55
  required?: boolean;
33
56
  formatting?: {
@@ -40,6 +63,8 @@ interface RichTextParamConfiguration {
40
63
  asset?: {
41
64
  allowedTypes?: AssetDefinitionType[];
42
65
  };
66
+ /** Optional anchor (fragment) toggle and custom attribute config for the link element. */
67
+ link?: LinkAttributesConfiguration;
43
68
  };
44
69
  }
45
70
 
@@ -65,12 +90,34 @@ type ProjectMapLinkParamValue = {
65
90
  nodeId: string;
66
91
  path: string;
67
92
  dynamicInputValues?: Record<string, string>;
93
+ attributes?: Record<string, string>;
68
94
  };
69
95
  type NonProjectMapLinkParamValue = {
70
96
  type: Exclude<LinkParameterType, 'projectMapNode'>;
71
97
  path: string;
98
+ attributes?: Record<string, string>;
72
99
  };
73
100
  type LinkParamValue = ProjectMapLinkParamValue | NonProjectMapLinkParamValue | undefined;
101
+ /**
102
+ * True for attribute keys the Uniform web SDKs strip at render time: inline event
103
+ * handlers (`on*`) and dangerous attribute names (href, src, style, formaction).
104
+ * Authoring UIs should use this to warn before such keys are ever persisted.
105
+ */
106
+ declare function isDangerousLinkAttributeKey(key: string): boolean;
107
+ /**
108
+ * Converts a link parameter value into a plain HTML attribute bag suitable for
109
+ * an `<a>` element. Applies a web-channel security denylist (event handlers,
110
+ * script-executing URL schemes on the href and any value, dangerous attribute names) and auto-adds
111
+ * `rel="noopener noreferrer"` for any case variant of `target="_blank"`.
112
+ */
113
+ declare function linkParamValueToHtmlAttributes(link: LinkParamValue): Record<string, string>;
114
+ /**
115
+ * Converts a link parameter value into a React-compatible anchor prop bag.
116
+ * Delegates to `linkParamValueToHtmlAttributes` for the security denylist /
117
+ * auto-rel logic, then renames the literal `class` key to `className` for React.
118
+ */
119
+ declare function linkParamValueToAnchorProps(link: LinkParamValue): Record<string, string>;
120
+ /** Derives the href string from a link parameter value. */
74
121
  declare function linkParamValueToHref(link: LinkParamValue): string | undefined;
75
122
 
76
123
  interface ListNode extends RichTextNode {
@@ -223,4 +270,4 @@ declare const emptyRichTextValue: {
223
270
  };
224
271
  declare const defaultParameterConfiguration: RichTextParamConfiguration;
225
272
 
226
- export { type AssetNode, type HeadingNode, type LinkNode, type ListItemNode, type ListNode, type NodeStringRenderer, type NodeStringRendererProps, type ParagraphNode, type ParameterRichTextValue, type ResolveStringRenderer, type RichTextBuiltInElement, type RichTextBuiltInFormat, type RichTextNode, type RichTextNodeWithChildren, type RichTextParamConfiguration, type StringRenderContext, type TableCellNode, type TableNode, type TableRowNode, type TextNode, assetHtmlRenderer, defaultParameterConfiguration, emptyRichTextValue, getLabelForElement, getLabelForFormat, getRichTextTagFromTableCellHeaderState, getRichTextTagsFromTextFormat, hasChildren, headingHtmlRenderer, isArrayWithLength, isPureDirection, isPureTextAlign, isRichTextNode, isRichTextNodeType, isRichTextValue, isRichTextValueConsideredEmpty, linkHtmlRenderer, linkParamValueToHref, listHtmlRenderer, listitemHtmlRenderer, paragraphHtmlRenderer, purifyText, renderChildrenToHtml, renderChildrenToText, renderHtmlElement, renderToHtml, renderToText, resolveDefaultRenderer$1 as resolveDefaultHtmlRenderer, resolveDefaultRenderer as resolveDefaultTextRenderer, richTextBuiltInElements, richTextBuiltInFormats, rootHtmlRenderer, tableHtmlRenderer, tablecellHtmlRenderer, tablerowHtmlRenderer, textHtmlRenderer, walkRichTextTree };
273
+ export { type AssetNode, type HeadingNode, type LinkAttrConfig, type LinkAttributesConfiguration, type LinkNode, type ListItemNode, type ListNode, type NodeStringRenderer, type NodeStringRendererProps, type ParagraphNode, type ParameterRichTextValue, type ResolveStringRenderer, type RichTextBuiltInElement, type RichTextBuiltInFormat, type RichTextNode, type RichTextNodeWithChildren, type RichTextParamConfiguration, type StringRenderContext, type TableCellNode, type TableNode, type TableRowNode, type TextNode, assetHtmlRenderer, defaultParameterConfiguration, emptyRichTextValue, getLabelForElement, getLabelForFormat, getRichTextTagFromTableCellHeaderState, getRichTextTagsFromTextFormat, hasChildren, headingHtmlRenderer, isArrayWithLength, isDangerousLinkAttributeKey, isPureDirection, isPureTextAlign, isRichTextNode, isRichTextNodeType, isRichTextValue, isRichTextValueConsideredEmpty, linkHtmlRenderer, linkParamValueToAnchorProps, linkParamValueToHref, linkParamValueToHtmlAttributes, listHtmlRenderer, listitemHtmlRenderer, paragraphHtmlRenderer, purifyText, renderChildrenToHtml, renderChildrenToText, renderHtmlElement, renderToHtml, renderToText, resolveDefaultRenderer$1 as resolveDefaultHtmlRenderer, resolveDefaultRenderer as resolveDefaultTextRenderer, resolveLinkAttrConfig, richTextBuiltInElements, richTextBuiltInFormats, rootHtmlRenderer, tableHtmlRenderer, tablecellHtmlRenderer, tablerowHtmlRenderer, textHtmlRenderer, walkRichTextTree };
package/dist/index.esm.js CHANGED
@@ -29,7 +29,7 @@ var voidElements = /* @__PURE__ */ new Set([
29
29
  "wbr"
30
30
  ]);
31
31
  function renderHtmlElement(tag, attributes, children) {
32
- const attributesString = attributes && attributes.size > 0 ? " " + Array.from(attributes).reduce((result, [key, value]) => [...result, `${key}="${value}"`], []).join(" ") : "";
32
+ const attributesString = attributes && attributes.size > 0 ? " " + Array.from(attributes).reduce((result, [key, value]) => [...result, `${key}="${purifyText(value)}"`], []).join(" ") : "";
33
33
  return voidElements.has(tag) ? `<${tag}${attributesString}>` : `<${tag}${attributesString}>${children != null ? children : ""}</${tag}>`;
34
34
  }
35
35
 
@@ -99,13 +99,68 @@ var headingTextRenderer = ({ context, renderChildren }) => {
99
99
  // src/nodes/link.ts
100
100
  var linkHtmlRenderer = ({ context, renderChildren }) => {
101
101
  const node = context.currentNode;
102
- const attributes = /* @__PURE__ */ new Map();
103
- const href = linkParamValueToHref(node.link);
104
- if (href) {
105
- attributes.set("href", href);
106
- }
102
+ const htmlAttrs = linkParamValueToHtmlAttributes(node.link);
103
+ const attributes = new Map(Object.entries(htmlAttrs));
107
104
  return renderHtmlElement("a", attributes, renderChildren(context.currentNode.children));
108
105
  };
106
+ var DENYLIST_EXACT = /* @__PURE__ */ new Set(["href", "src", "style", "formaction"]);
107
+ var EVENT_HANDLER_PATTERN = /^on/i;
108
+ var SAFE_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9:_.-]*$/;
109
+ var DANGEROUS_SCHEME_PATTERN = /^(?:javascript|data|vbscript):/i;
110
+ function hasDangerousScheme(value) {
111
+ const normalized = value.replace(/[\u0000-\u0020\u007f-\u00a0]+/g, "").toLowerCase();
112
+ return DANGEROUS_SCHEME_PATTERN.test(normalized);
113
+ }
114
+ function isDangerousLinkAttributeKey(key) {
115
+ return EVENT_HANDLER_PATTERN.test(key) || DENYLIST_EXACT.has(key.toLowerCase());
116
+ }
117
+ function linkParamValueToHtmlAttributes(link) {
118
+ var _a;
119
+ const result = {};
120
+ const href = linkParamValueToHref(link);
121
+ if (href) {
122
+ result.href = href;
123
+ }
124
+ if (!(link == null ? void 0 : link.attributes)) {
125
+ return result;
126
+ }
127
+ for (const [key, value] of Object.entries(link.attributes)) {
128
+ if (!key || typeof value !== "string") {
129
+ continue;
130
+ }
131
+ if (!SAFE_ATTRIBUTE_NAME_PATTERN.test(key)) {
132
+ continue;
133
+ }
134
+ if (EVENT_HANDLER_PATTERN.test(key)) {
135
+ continue;
136
+ }
137
+ if (DENYLIST_EXACT.has(key.toLowerCase())) {
138
+ continue;
139
+ }
140
+ if (hasDangerousScheme(value)) {
141
+ continue;
142
+ }
143
+ result[key] = value;
144
+ }
145
+ const targetEntry = Object.entries(result).find(([key]) => key.toLowerCase() === "target");
146
+ const hasRel = Object.keys(result).some((key) => key.toLowerCase() === "rel");
147
+ if (((_a = targetEntry == null ? void 0 : targetEntry[1]) == null ? void 0 : _a.toLowerCase()) === "_blank" && !hasRel) {
148
+ result.rel = "noopener noreferrer";
149
+ }
150
+ return result;
151
+ }
152
+ function linkParamValueToAnchorProps(link) {
153
+ const raw = linkParamValueToHtmlAttributes(link);
154
+ const result = {};
155
+ for (const [key, value] of Object.entries(raw)) {
156
+ if (key === "class") {
157
+ result["className"] = value;
158
+ } else {
159
+ result[key] = value;
160
+ }
161
+ }
162
+ return result;
163
+ }
109
164
  function linkParamValueToHref(link) {
110
165
  switch (link == null ? void 0 : link.type) {
111
166
  case "email":
@@ -113,8 +168,11 @@ function linkParamValueToHref(link) {
113
168
  case "tel":
114
169
  return `tel:${link.path}`;
115
170
  case "url":
116
- return link.path;
117
171
  case "projectMapNode":
172
+ if (hasDangerousScheme(link.path)) {
173
+ console.warn(`[richtext] Dropped disallowed link URL scheme: "${link.path}"`);
174
+ return void 0;
175
+ }
118
176
  return link.path;
119
177
  }
120
178
  }
@@ -387,6 +445,17 @@ function renderChildrenToText(children, context) {
387
445
  return Array.isArray(children) ? children.map((node) => renderToText(node, context)).join("") : "";
388
446
  }
389
447
 
448
+ // src/types.ts
449
+ function resolveLinkAttrConfig(entry) {
450
+ var _a;
451
+ return {
452
+ key: entry.key,
453
+ options: entry.options,
454
+ label: (_a = entry.label) != null ? _a : entry.key,
455
+ helpText: entry.helpText
456
+ };
457
+ }
458
+
390
459
  // src/utils.ts
391
460
  var walkRichTextTree = (node, callback, parent) => {
392
461
  if (!isRichTextNode(node)) {
@@ -548,6 +617,7 @@ export {
548
617
  hasChildren,
549
618
  headingHtmlRenderer,
550
619
  isArrayWithLength,
620
+ isDangerousLinkAttributeKey,
551
621
  isPureDirection,
552
622
  isPureTextAlign,
553
623
  isRichTextNode,
@@ -555,7 +625,9 @@ export {
555
625
  isRichTextValue,
556
626
  isRichTextValueConsideredEmpty,
557
627
  linkHtmlRenderer,
628
+ linkParamValueToAnchorProps,
558
629
  linkParamValueToHref,
630
+ linkParamValueToHtmlAttributes,
559
631
  listHtmlRenderer,
560
632
  listitemHtmlRenderer,
561
633
  paragraphHtmlRenderer,
@@ -567,6 +639,7 @@ export {
567
639
  renderToText,
568
640
  resolveDefaultRenderer as resolveDefaultHtmlRenderer,
569
641
  resolveDefaultRenderer2 as resolveDefaultTextRenderer,
642
+ resolveLinkAttrConfig,
570
643
  richTextBuiltInElements,
571
644
  richTextBuiltInFormats,
572
645
  rootHtmlRenderer,
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  hasChildren: () => hasChildren,
31
31
  headingHtmlRenderer: () => headingHtmlRenderer,
32
32
  isArrayWithLength: () => isArrayWithLength,
33
+ isDangerousLinkAttributeKey: () => isDangerousLinkAttributeKey,
33
34
  isPureDirection: () => isPureDirection,
34
35
  isPureTextAlign: () => isPureTextAlign,
35
36
  isRichTextNode: () => isRichTextNode,
@@ -37,7 +38,9 @@ __export(index_exports, {
37
38
  isRichTextValue: () => isRichTextValue,
38
39
  isRichTextValueConsideredEmpty: () => isRichTextValueConsideredEmpty,
39
40
  linkHtmlRenderer: () => linkHtmlRenderer,
41
+ linkParamValueToAnchorProps: () => linkParamValueToAnchorProps,
40
42
  linkParamValueToHref: () => linkParamValueToHref,
43
+ linkParamValueToHtmlAttributes: () => linkParamValueToHtmlAttributes,
41
44
  listHtmlRenderer: () => listHtmlRenderer,
42
45
  listitemHtmlRenderer: () => listitemHtmlRenderer,
43
46
  paragraphHtmlRenderer: () => paragraphHtmlRenderer,
@@ -49,6 +52,7 @@ __export(index_exports, {
49
52
  renderToText: () => renderToText,
50
53
  resolveDefaultHtmlRenderer: () => resolveDefaultRenderer,
51
54
  resolveDefaultTextRenderer: () => resolveDefaultRenderer2,
55
+ resolveLinkAttrConfig: () => resolveLinkAttrConfig,
52
56
  richTextBuiltInElements: () => richTextBuiltInElements,
53
57
  richTextBuiltInFormats: () => richTextBuiltInFormats,
54
58
  rootHtmlRenderer: () => rootHtmlRenderer,
@@ -91,7 +95,7 @@ var voidElements = /* @__PURE__ */ new Set([
91
95
  "wbr"
92
96
  ]);
93
97
  function renderHtmlElement(tag, attributes, children) {
94
- const attributesString = attributes && attributes.size > 0 ? " " + Array.from(attributes).reduce((result, [key, value]) => [...result, `${key}="${value}"`], []).join(" ") : "";
98
+ const attributesString = attributes && attributes.size > 0 ? " " + Array.from(attributes).reduce((result, [key, value]) => [...result, `${key}="${purifyText(value)}"`], []).join(" ") : "";
95
99
  return voidElements.has(tag) ? `<${tag}${attributesString}>` : `<${tag}${attributesString}>${children != null ? children : ""}</${tag}>`;
96
100
  }
97
101
 
@@ -161,13 +165,68 @@ var headingTextRenderer = ({ context, renderChildren }) => {
161
165
  // src/nodes/link.ts
162
166
  var linkHtmlRenderer = ({ context, renderChildren }) => {
163
167
  const node = context.currentNode;
164
- const attributes = /* @__PURE__ */ new Map();
165
- const href = linkParamValueToHref(node.link);
166
- if (href) {
167
- attributes.set("href", href);
168
- }
168
+ const htmlAttrs = linkParamValueToHtmlAttributes(node.link);
169
+ const attributes = new Map(Object.entries(htmlAttrs));
169
170
  return renderHtmlElement("a", attributes, renderChildren(context.currentNode.children));
170
171
  };
172
+ var DENYLIST_EXACT = /* @__PURE__ */ new Set(["href", "src", "style", "formaction"]);
173
+ var EVENT_HANDLER_PATTERN = /^on/i;
174
+ var SAFE_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9:_.-]*$/;
175
+ var DANGEROUS_SCHEME_PATTERN = /^(?:javascript|data|vbscript):/i;
176
+ function hasDangerousScheme(value) {
177
+ const normalized = value.replace(/[\u0000-\u0020\u007f-\u00a0]+/g, "").toLowerCase();
178
+ return DANGEROUS_SCHEME_PATTERN.test(normalized);
179
+ }
180
+ function isDangerousLinkAttributeKey(key) {
181
+ return EVENT_HANDLER_PATTERN.test(key) || DENYLIST_EXACT.has(key.toLowerCase());
182
+ }
183
+ function linkParamValueToHtmlAttributes(link) {
184
+ var _a;
185
+ const result = {};
186
+ const href = linkParamValueToHref(link);
187
+ if (href) {
188
+ result.href = href;
189
+ }
190
+ if (!(link == null ? void 0 : link.attributes)) {
191
+ return result;
192
+ }
193
+ for (const [key, value] of Object.entries(link.attributes)) {
194
+ if (!key || typeof value !== "string") {
195
+ continue;
196
+ }
197
+ if (!SAFE_ATTRIBUTE_NAME_PATTERN.test(key)) {
198
+ continue;
199
+ }
200
+ if (EVENT_HANDLER_PATTERN.test(key)) {
201
+ continue;
202
+ }
203
+ if (DENYLIST_EXACT.has(key.toLowerCase())) {
204
+ continue;
205
+ }
206
+ if (hasDangerousScheme(value)) {
207
+ continue;
208
+ }
209
+ result[key] = value;
210
+ }
211
+ const targetEntry = Object.entries(result).find(([key]) => key.toLowerCase() === "target");
212
+ const hasRel = Object.keys(result).some((key) => key.toLowerCase() === "rel");
213
+ if (((_a = targetEntry == null ? void 0 : targetEntry[1]) == null ? void 0 : _a.toLowerCase()) === "_blank" && !hasRel) {
214
+ result.rel = "noopener noreferrer";
215
+ }
216
+ return result;
217
+ }
218
+ function linkParamValueToAnchorProps(link) {
219
+ const raw = linkParamValueToHtmlAttributes(link);
220
+ const result = {};
221
+ for (const [key, value] of Object.entries(raw)) {
222
+ if (key === "class") {
223
+ result["className"] = value;
224
+ } else {
225
+ result[key] = value;
226
+ }
227
+ }
228
+ return result;
229
+ }
171
230
  function linkParamValueToHref(link) {
172
231
  switch (link == null ? void 0 : link.type) {
173
232
  case "email":
@@ -175,8 +234,11 @@ function linkParamValueToHref(link) {
175
234
  case "tel":
176
235
  return `tel:${link.path}`;
177
236
  case "url":
178
- return link.path;
179
237
  case "projectMapNode":
238
+ if (hasDangerousScheme(link.path)) {
239
+ console.warn(`[richtext] Dropped disallowed link URL scheme: "${link.path}"`);
240
+ return void 0;
241
+ }
180
242
  return link.path;
181
243
  }
182
244
  }
@@ -449,6 +511,17 @@ function renderChildrenToText(children, context) {
449
511
  return Array.isArray(children) ? children.map((node) => renderToText(node, context)).join("") : "";
450
512
  }
451
513
 
514
+ // src/types.ts
515
+ function resolveLinkAttrConfig(entry) {
516
+ var _a;
517
+ return {
518
+ key: entry.key,
519
+ options: entry.options,
520
+ label: (_a = entry.label) != null ? _a : entry.key,
521
+ helpText: entry.helpText
522
+ };
523
+ }
524
+
452
525
  // src/utils.ts
453
526
  var walkRichTextTree = (node, callback, parent) => {
454
527
  if (!isRichTextNode(node)) {
@@ -611,6 +684,7 @@ var defaultParameterConfiguration = {
611
684
  hasChildren,
612
685
  headingHtmlRenderer,
613
686
  isArrayWithLength,
687
+ isDangerousLinkAttributeKey,
614
688
  isPureDirection,
615
689
  isPureTextAlign,
616
690
  isRichTextNode,
@@ -618,7 +692,9 @@ var defaultParameterConfiguration = {
618
692
  isRichTextValue,
619
693
  isRichTextValueConsideredEmpty,
620
694
  linkHtmlRenderer,
695
+ linkParamValueToAnchorProps,
621
696
  linkParamValueToHref,
697
+ linkParamValueToHtmlAttributes,
622
698
  listHtmlRenderer,
623
699
  listitemHtmlRenderer,
624
700
  paragraphHtmlRenderer,
@@ -630,6 +706,7 @@ var defaultParameterConfiguration = {
630
706
  renderToText,
631
707
  resolveDefaultHtmlRenderer,
632
708
  resolveDefaultTextRenderer,
709
+ resolveLinkAttrConfig,
633
710
  richTextBuiltInElements,
634
711
  richTextBuiltInFormats,
635
712
  rootHtmlRenderer,
package/dist/index.mjs CHANGED
@@ -29,7 +29,7 @@ var voidElements = /* @__PURE__ */ new Set([
29
29
  "wbr"
30
30
  ]);
31
31
  function renderHtmlElement(tag, attributes, children) {
32
- const attributesString = attributes && attributes.size > 0 ? " " + Array.from(attributes).reduce((result, [key, value]) => [...result, `${key}="${value}"`], []).join(" ") : "";
32
+ const attributesString = attributes && attributes.size > 0 ? " " + Array.from(attributes).reduce((result, [key, value]) => [...result, `${key}="${purifyText(value)}"`], []).join(" ") : "";
33
33
  return voidElements.has(tag) ? `<${tag}${attributesString}>` : `<${tag}${attributesString}>${children != null ? children : ""}</${tag}>`;
34
34
  }
35
35
 
@@ -99,13 +99,68 @@ var headingTextRenderer = ({ context, renderChildren }) => {
99
99
  // src/nodes/link.ts
100
100
  var linkHtmlRenderer = ({ context, renderChildren }) => {
101
101
  const node = context.currentNode;
102
- const attributes = /* @__PURE__ */ new Map();
103
- const href = linkParamValueToHref(node.link);
104
- if (href) {
105
- attributes.set("href", href);
106
- }
102
+ const htmlAttrs = linkParamValueToHtmlAttributes(node.link);
103
+ const attributes = new Map(Object.entries(htmlAttrs));
107
104
  return renderHtmlElement("a", attributes, renderChildren(context.currentNode.children));
108
105
  };
106
+ var DENYLIST_EXACT = /* @__PURE__ */ new Set(["href", "src", "style", "formaction"]);
107
+ var EVENT_HANDLER_PATTERN = /^on/i;
108
+ var SAFE_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9:_.-]*$/;
109
+ var DANGEROUS_SCHEME_PATTERN = /^(?:javascript|data|vbscript):/i;
110
+ function hasDangerousScheme(value) {
111
+ const normalized = value.replace(/[\u0000-\u0020\u007f-\u00a0]+/g, "").toLowerCase();
112
+ return DANGEROUS_SCHEME_PATTERN.test(normalized);
113
+ }
114
+ function isDangerousLinkAttributeKey(key) {
115
+ return EVENT_HANDLER_PATTERN.test(key) || DENYLIST_EXACT.has(key.toLowerCase());
116
+ }
117
+ function linkParamValueToHtmlAttributes(link) {
118
+ var _a;
119
+ const result = {};
120
+ const href = linkParamValueToHref(link);
121
+ if (href) {
122
+ result.href = href;
123
+ }
124
+ if (!(link == null ? void 0 : link.attributes)) {
125
+ return result;
126
+ }
127
+ for (const [key, value] of Object.entries(link.attributes)) {
128
+ if (!key || typeof value !== "string") {
129
+ continue;
130
+ }
131
+ if (!SAFE_ATTRIBUTE_NAME_PATTERN.test(key)) {
132
+ continue;
133
+ }
134
+ if (EVENT_HANDLER_PATTERN.test(key)) {
135
+ continue;
136
+ }
137
+ if (DENYLIST_EXACT.has(key.toLowerCase())) {
138
+ continue;
139
+ }
140
+ if (hasDangerousScheme(value)) {
141
+ continue;
142
+ }
143
+ result[key] = value;
144
+ }
145
+ const targetEntry = Object.entries(result).find(([key]) => key.toLowerCase() === "target");
146
+ const hasRel = Object.keys(result).some((key) => key.toLowerCase() === "rel");
147
+ if (((_a = targetEntry == null ? void 0 : targetEntry[1]) == null ? void 0 : _a.toLowerCase()) === "_blank" && !hasRel) {
148
+ result.rel = "noopener noreferrer";
149
+ }
150
+ return result;
151
+ }
152
+ function linkParamValueToAnchorProps(link) {
153
+ const raw = linkParamValueToHtmlAttributes(link);
154
+ const result = {};
155
+ for (const [key, value] of Object.entries(raw)) {
156
+ if (key === "class") {
157
+ result["className"] = value;
158
+ } else {
159
+ result[key] = value;
160
+ }
161
+ }
162
+ return result;
163
+ }
109
164
  function linkParamValueToHref(link) {
110
165
  switch (link == null ? void 0 : link.type) {
111
166
  case "email":
@@ -113,8 +168,11 @@ function linkParamValueToHref(link) {
113
168
  case "tel":
114
169
  return `tel:${link.path}`;
115
170
  case "url":
116
- return link.path;
117
171
  case "projectMapNode":
172
+ if (hasDangerousScheme(link.path)) {
173
+ console.warn(`[richtext] Dropped disallowed link URL scheme: "${link.path}"`);
174
+ return void 0;
175
+ }
118
176
  return link.path;
119
177
  }
120
178
  }
@@ -387,6 +445,17 @@ function renderChildrenToText(children, context) {
387
445
  return Array.isArray(children) ? children.map((node) => renderToText(node, context)).join("") : "";
388
446
  }
389
447
 
448
+ // src/types.ts
449
+ function resolveLinkAttrConfig(entry) {
450
+ var _a;
451
+ return {
452
+ key: entry.key,
453
+ options: entry.options,
454
+ label: (_a = entry.label) != null ? _a : entry.key,
455
+ helpText: entry.helpText
456
+ };
457
+ }
458
+
390
459
  // src/utils.ts
391
460
  var walkRichTextTree = (node, callback, parent) => {
392
461
  if (!isRichTextNode(node)) {
@@ -548,6 +617,7 @@ export {
548
617
  hasChildren,
549
618
  headingHtmlRenderer,
550
619
  isArrayWithLength,
620
+ isDangerousLinkAttributeKey,
551
621
  isPureDirection,
552
622
  isPureTextAlign,
553
623
  isRichTextNode,
@@ -555,7 +625,9 @@ export {
555
625
  isRichTextValue,
556
626
  isRichTextValueConsideredEmpty,
557
627
  linkHtmlRenderer,
628
+ linkParamValueToAnchorProps,
558
629
  linkParamValueToHref,
630
+ linkParamValueToHtmlAttributes,
559
631
  listHtmlRenderer,
560
632
  listitemHtmlRenderer,
561
633
  paragraphHtmlRenderer,
@@ -567,6 +639,7 @@ export {
567
639
  renderToText,
568
640
  resolveDefaultRenderer as resolveDefaultHtmlRenderer,
569
641
  resolveDefaultRenderer2 as resolveDefaultTextRenderer,
642
+ resolveLinkAttrConfig,
570
643
  richTextBuiltInElements,
571
644
  richTextBuiltInFormats,
572
645
  rootHtmlRenderer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniformdev/richtext",
3
- "version": "20.72.2-alpha.3+a1f072e7b4",
3
+ "version": "20.72.3-alpha.14+1e232e59cd",
4
4
  "description": "Common functionality and types for Uniform Rich Text parameters",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "main": "./dist/index.js",
@@ -44,7 +44,7 @@
44
44
  "@lexical/link": "0.45.0",
45
45
  "@lexical/list": "0.45.0",
46
46
  "@lexical/rich-text": "0.45.0",
47
- "@uniformdev/assets": "20.72.2-alpha.3+a1f072e7b4",
47
+ "@uniformdev/assets": "20.72.3-alpha.14+1e232e59cd",
48
48
  "lexical": "0.45.0"
49
49
  },
50
50
  "files": [
@@ -53,5 +53,5 @@
53
53
  "publishConfig": {
54
54
  "access": "public"
55
55
  },
56
- "gitHead": "a1f072e7b42ac4b3c694ebdd2e75216efd74a9c2"
56
+ "gitHead": "1e232e59cd3ded97b85b063986cc95e954b8c937"
57
57
  }