@uniformdev/richtext 20.72.2-alpha.3 → 20.72.2

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,28 @@ 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
+ /** Derives the href string from a link parameter value. */
74
115
  declare function linkParamValueToHref(link: LinkParamValue): string | undefined;
75
116
 
76
117
  interface ListNode extends RichTextNode {
@@ -223,4 +264,4 @@ declare const emptyRichTextValue: {
223
264
  };
224
265
  declare const defaultParameterConfiguration: RichTextParamConfiguration;
225
266
 
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 };
267
+ 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, 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,28 @@ 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
+ /** Derives the href string from a link parameter value. */
74
115
  declare function linkParamValueToHref(link: LinkParamValue): string | undefined;
75
116
 
76
117
  interface ListNode extends RichTextNode {
@@ -223,4 +264,4 @@ declare const emptyRichTextValue: {
223
264
  };
224
265
  declare const defaultParameterConfiguration: RichTextParamConfiguration;
225
266
 
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 };
267
+ 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, 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,54 @@ 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 && !hasDangerousScheme(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
+ if (((_a = result["target"]) == null ? void 0 : _a.toLowerCase()) === "_blank" && !("rel" in result)) {
146
+ result.rel = "noopener noreferrer";
147
+ }
148
+ return result;
149
+ }
109
150
  function linkParamValueToHref(link) {
110
151
  switch (link == null ? void 0 : link.type) {
111
152
  case "email":
@@ -387,6 +428,17 @@ function renderChildrenToText(children, context) {
387
428
  return Array.isArray(children) ? children.map((node) => renderToText(node, context)).join("") : "";
388
429
  }
389
430
 
431
+ // src/types.ts
432
+ function resolveLinkAttrConfig(entry) {
433
+ var _a;
434
+ return {
435
+ key: entry.key,
436
+ options: entry.options,
437
+ label: (_a = entry.label) != null ? _a : entry.key,
438
+ helpText: entry.helpText
439
+ };
440
+ }
441
+
390
442
  // src/utils.ts
391
443
  var walkRichTextTree = (node, callback, parent) => {
392
444
  if (!isRichTextNode(node)) {
@@ -548,6 +600,7 @@ export {
548
600
  hasChildren,
549
601
  headingHtmlRenderer,
550
602
  isArrayWithLength,
603
+ isDangerousLinkAttributeKey,
551
604
  isPureDirection,
552
605
  isPureTextAlign,
553
606
  isRichTextNode,
@@ -556,6 +609,7 @@ export {
556
609
  isRichTextValueConsideredEmpty,
557
610
  linkHtmlRenderer,
558
611
  linkParamValueToHref,
612
+ linkParamValueToHtmlAttributes,
559
613
  listHtmlRenderer,
560
614
  listitemHtmlRenderer,
561
615
  paragraphHtmlRenderer,
@@ -567,6 +621,7 @@ export {
567
621
  renderToText,
568
622
  resolveDefaultRenderer as resolveDefaultHtmlRenderer,
569
623
  resolveDefaultRenderer2 as resolveDefaultTextRenderer,
624
+ resolveLinkAttrConfig,
570
625
  richTextBuiltInElements,
571
626
  richTextBuiltInFormats,
572
627
  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,
@@ -38,6 +39,7 @@ __export(index_exports, {
38
39
  isRichTextValueConsideredEmpty: () => isRichTextValueConsideredEmpty,
39
40
  linkHtmlRenderer: () => linkHtmlRenderer,
40
41
  linkParamValueToHref: () => linkParamValueToHref,
42
+ linkParamValueToHtmlAttributes: () => linkParamValueToHtmlAttributes,
41
43
  listHtmlRenderer: () => listHtmlRenderer,
42
44
  listitemHtmlRenderer: () => listitemHtmlRenderer,
43
45
  paragraphHtmlRenderer: () => paragraphHtmlRenderer,
@@ -49,6 +51,7 @@ __export(index_exports, {
49
51
  renderToText: () => renderToText,
50
52
  resolveDefaultHtmlRenderer: () => resolveDefaultRenderer,
51
53
  resolveDefaultTextRenderer: () => resolveDefaultRenderer2,
54
+ resolveLinkAttrConfig: () => resolveLinkAttrConfig,
52
55
  richTextBuiltInElements: () => richTextBuiltInElements,
53
56
  richTextBuiltInFormats: () => richTextBuiltInFormats,
54
57
  rootHtmlRenderer: () => rootHtmlRenderer,
@@ -91,7 +94,7 @@ var voidElements = /* @__PURE__ */ new Set([
91
94
  "wbr"
92
95
  ]);
93
96
  function renderHtmlElement(tag, attributes, children) {
94
- const attributesString = attributes && attributes.size > 0 ? " " + Array.from(attributes).reduce((result, [key, value]) => [...result, `${key}="${value}"`], []).join(" ") : "";
97
+ const attributesString = attributes && attributes.size > 0 ? " " + Array.from(attributes).reduce((result, [key, value]) => [...result, `${key}="${purifyText(value)}"`], []).join(" ") : "";
95
98
  return voidElements.has(tag) ? `<${tag}${attributesString}>` : `<${tag}${attributesString}>${children != null ? children : ""}</${tag}>`;
96
99
  }
97
100
 
@@ -161,13 +164,54 @@ var headingTextRenderer = ({ context, renderChildren }) => {
161
164
  // src/nodes/link.ts
162
165
  var linkHtmlRenderer = ({ context, renderChildren }) => {
163
166
  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
- }
167
+ const htmlAttrs = linkParamValueToHtmlAttributes(node.link);
168
+ const attributes = new Map(Object.entries(htmlAttrs));
169
169
  return renderHtmlElement("a", attributes, renderChildren(context.currentNode.children));
170
170
  };
171
+ var DENYLIST_EXACT = /* @__PURE__ */ new Set(["href", "src", "style", "formaction"]);
172
+ var EVENT_HANDLER_PATTERN = /^on/i;
173
+ var SAFE_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9:_.-]*$/;
174
+ var DANGEROUS_SCHEME_PATTERN = /^(?:javascript|data|vbscript):/i;
175
+ function hasDangerousScheme(value) {
176
+ const normalized = value.replace(/[\u0000-\u0020\u007f-\u00a0]+/g, "").toLowerCase();
177
+ return DANGEROUS_SCHEME_PATTERN.test(normalized);
178
+ }
179
+ function isDangerousLinkAttributeKey(key) {
180
+ return EVENT_HANDLER_PATTERN.test(key) || DENYLIST_EXACT.has(key.toLowerCase());
181
+ }
182
+ function linkParamValueToHtmlAttributes(link) {
183
+ var _a;
184
+ const result = {};
185
+ const href = linkParamValueToHref(link);
186
+ if (href && !hasDangerousScheme(href)) {
187
+ result.href = href;
188
+ }
189
+ if (!(link == null ? void 0 : link.attributes)) {
190
+ return result;
191
+ }
192
+ for (const [key, value] of Object.entries(link.attributes)) {
193
+ if (!key || typeof value !== "string") {
194
+ continue;
195
+ }
196
+ if (!SAFE_ATTRIBUTE_NAME_PATTERN.test(key)) {
197
+ continue;
198
+ }
199
+ if (EVENT_HANDLER_PATTERN.test(key)) {
200
+ continue;
201
+ }
202
+ if (DENYLIST_EXACT.has(key.toLowerCase())) {
203
+ continue;
204
+ }
205
+ if (hasDangerousScheme(value)) {
206
+ continue;
207
+ }
208
+ result[key] = value;
209
+ }
210
+ if (((_a = result["target"]) == null ? void 0 : _a.toLowerCase()) === "_blank" && !("rel" in result)) {
211
+ result.rel = "noopener noreferrer";
212
+ }
213
+ return result;
214
+ }
171
215
  function linkParamValueToHref(link) {
172
216
  switch (link == null ? void 0 : link.type) {
173
217
  case "email":
@@ -449,6 +493,17 @@ function renderChildrenToText(children, context) {
449
493
  return Array.isArray(children) ? children.map((node) => renderToText(node, context)).join("") : "";
450
494
  }
451
495
 
496
+ // src/types.ts
497
+ function resolveLinkAttrConfig(entry) {
498
+ var _a;
499
+ return {
500
+ key: entry.key,
501
+ options: entry.options,
502
+ label: (_a = entry.label) != null ? _a : entry.key,
503
+ helpText: entry.helpText
504
+ };
505
+ }
506
+
452
507
  // src/utils.ts
453
508
  var walkRichTextTree = (node, callback, parent) => {
454
509
  if (!isRichTextNode(node)) {
@@ -611,6 +666,7 @@ var defaultParameterConfiguration = {
611
666
  hasChildren,
612
667
  headingHtmlRenderer,
613
668
  isArrayWithLength,
669
+ isDangerousLinkAttributeKey,
614
670
  isPureDirection,
615
671
  isPureTextAlign,
616
672
  isRichTextNode,
@@ -619,6 +675,7 @@ var defaultParameterConfiguration = {
619
675
  isRichTextValueConsideredEmpty,
620
676
  linkHtmlRenderer,
621
677
  linkParamValueToHref,
678
+ linkParamValueToHtmlAttributes,
622
679
  listHtmlRenderer,
623
680
  listitemHtmlRenderer,
624
681
  paragraphHtmlRenderer,
@@ -630,6 +687,7 @@ var defaultParameterConfiguration = {
630
687
  renderToText,
631
688
  resolveDefaultHtmlRenderer,
632
689
  resolveDefaultTextRenderer,
690
+ resolveLinkAttrConfig,
633
691
  richTextBuiltInElements,
634
692
  richTextBuiltInFormats,
635
693
  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,54 @@ 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 && !hasDangerousScheme(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
+ if (((_a = result["target"]) == null ? void 0 : _a.toLowerCase()) === "_blank" && !("rel" in result)) {
146
+ result.rel = "noopener noreferrer";
147
+ }
148
+ return result;
149
+ }
109
150
  function linkParamValueToHref(link) {
110
151
  switch (link == null ? void 0 : link.type) {
111
152
  case "email":
@@ -387,6 +428,17 @@ function renderChildrenToText(children, context) {
387
428
  return Array.isArray(children) ? children.map((node) => renderToText(node, context)).join("") : "";
388
429
  }
389
430
 
431
+ // src/types.ts
432
+ function resolveLinkAttrConfig(entry) {
433
+ var _a;
434
+ return {
435
+ key: entry.key,
436
+ options: entry.options,
437
+ label: (_a = entry.label) != null ? _a : entry.key,
438
+ helpText: entry.helpText
439
+ };
440
+ }
441
+
390
442
  // src/utils.ts
391
443
  var walkRichTextTree = (node, callback, parent) => {
392
444
  if (!isRichTextNode(node)) {
@@ -548,6 +600,7 @@ export {
548
600
  hasChildren,
549
601
  headingHtmlRenderer,
550
602
  isArrayWithLength,
603
+ isDangerousLinkAttributeKey,
551
604
  isPureDirection,
552
605
  isPureTextAlign,
553
606
  isRichTextNode,
@@ -556,6 +609,7 @@ export {
556
609
  isRichTextValueConsideredEmpty,
557
610
  linkHtmlRenderer,
558
611
  linkParamValueToHref,
612
+ linkParamValueToHtmlAttributes,
559
613
  listHtmlRenderer,
560
614
  listitemHtmlRenderer,
561
615
  paragraphHtmlRenderer,
@@ -567,6 +621,7 @@ export {
567
621
  renderToText,
568
622
  resolveDefaultRenderer as resolveDefaultHtmlRenderer,
569
623
  resolveDefaultRenderer2 as resolveDefaultTextRenderer,
624
+ resolveLinkAttrConfig,
570
625
  richTextBuiltInElements,
571
626
  richTextBuiltInFormats,
572
627
  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.2",
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.2",
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": "8b65535fb3ec81e23f9eb8e0ed0b0be478f6e0e8"
57
57
  }