@handled-ai/design-system 0.20.21 → 0.20.23

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.
@@ -2,12 +2,14 @@ import { describe, expect, it } from "vitest"
2
2
  import { htmlToTextSnippet, sanitizeHtml } from "../safe-html"
3
3
 
4
4
  describe("sanitizeHtml", () => {
5
- it("removes executable tags, event handlers, styles, and unsafe urls", () => {
5
+ it("removes executable tags, event handlers, unsafe styles, and unsafe urls", () => {
6
6
  const html = sanitizeHtml(
7
- '<p style="color:red" onclick="alert(1)">Hi<script>alert(1)</script><iframe src="https://evil.test"></iframe><a href="java&#x3a;script:alert(1)">bad</a><img src="data:text/html,boom" onerror="alert(1)"></p>',
7
+ '<p style="color:red; background-image: url(https://evil.test/x)" onclick="alert(1)">Hi<script>alert(1)</script><iframe src="https://evil.test"></iframe><a href="java&#x3a;script:alert(1)">bad</a><img src="data:text/html,boom" onerror="alert(1)"></p>',
8
8
  )
9
9
 
10
- expect(html).toBe('<p>Hi<a>bad</a><img></p>')
10
+ // Presentational color survives (signature fidelity); the url()-bearing
11
+ // declaration and everything executable is gone.
12
+ expect(html).toBe('<p style="color: red">Hi<a>bad</a><img></p>')
11
13
  })
12
14
 
13
15
  it("removes svg, math, and other active embedded content", () => {
@@ -60,12 +62,29 @@ describe("sanitizeHtml", () => {
60
62
  expect(html).toContain('class="gmail_signature"')
61
63
  expect(html).toContain('<table><tbody><tr><td><img src="https://example.com/logo.png" width="120" height="48" alt="Acme &amp; Co"></td>')
62
64
  expect(html).toContain('<sup>TM</sup><sub>LLC</sub>')
63
- expect(html).toContain('style="vertical-align: super; font-size: 10px"')
65
+ // Authored presentational styles now survive (signature Gmail-look
66
+ // fidelity); url()-bearing declarations are still dropped.
67
+ expect(html).toContain('style="vertical-align: super; font-size: 10px; color: red"')
64
68
  expect(html).not.toContain("onerror")
65
- expect(html).not.toContain("color: red")
66
69
  expect(html).not.toContain("background-image")
67
70
  })
68
71
 
72
+ it("preserves authored signature typography and spacing styles, dropping unknown properties", () => {
73
+ const html = sanitizeHtml(
74
+ '<p style="font-family: \'pt serif\', serif; font-style: italic; font-size: 12px; color: rgb(102, 102, 102); line-height: 1.38; margin: 0 0 8px 0; position: fixed; z-index: 9999">2261 Market Street</p>',
75
+ )
76
+
77
+ expect(html).toContain("font-family: 'pt serif', serif")
78
+ expect(html).toContain("font-style: italic")
79
+ expect(html).toContain("font-size: 12px")
80
+ expect(html).toContain("color: rgb(102, 102, 102)")
81
+ expect(html).toContain("line-height: 1.38")
82
+ expect(html).toContain("margin: 0 0 8px 0")
83
+ // Layout-escape properties are not allowlisted.
84
+ expect(html).not.toContain("position")
85
+ expect(html).not.toContain("z-index")
86
+ })
87
+
69
88
  it("bounds image dimensions and inline font-size while preserving subscript alignment", () => {
70
89
  const html = sanitizeHtml(
71
90
  '<span style="vertical-align: sub; font-size: 500px">H2O</span><img src="https://example.com/x.png" width="99999" height="64" alt="x">',
@@ -147,6 +147,39 @@ function sanitizeFontSize(value: string): string | null {
147
147
  return `${amount}${unit}`
148
148
  }
149
149
 
150
+ // Presentational properties preserved so email signatures keep their authored
151
+ // Gmail look (serif/italic/gray etc.) when rendered in the conversation panel.
152
+ // Values are validated per-property; anything containing url()/expression()
153
+ // (or any property not listed) is still dropped — these are text-presentation
154
+ // only, no layout escape or resource loading.
155
+ const COLOR_VALUE = /^(#[0-9a-f]{3,8}|rgba?\([\d\s,./%]+\)|[a-z]+)$/
156
+ const LENGTH_BOX_VALUE = /^(-?[\d.]+(px|em|rem|%)?|auto|0)(\s+(-?[\d.]+(px|em|rem|%)?|auto|0)){0,3}$/
157
+ const SAFE_TEXT_STYLE_PROPS: Record<string, RegExp> = {
158
+ color: COLOR_VALUE,
159
+ "background-color": COLOR_VALUE,
160
+ "font-family": /^[a-z0-9\s,'"-]+$/,
161
+ "font-style": /^(italic|normal|oblique)$/,
162
+ "font-weight": /^(bold|bolder|lighter|normal|[1-9]00)$/,
163
+ "font-variant": /^[a-z\s-]+$/,
164
+ "text-decoration": /^[a-z\s-]+$/,
165
+ "text-decoration-line": /^[a-z\s-]+$/,
166
+ "text-transform": /^(none|capitalize|uppercase|lowercase)$/,
167
+ "text-align": /^(left|right|center|justify)$/,
168
+ "letter-spacing": /^(normal|-?[\d.]+(px|em|rem)?)$/,
169
+ "line-height": /^(normal|[\d.]+(px|em|rem|%)?)$/,
170
+ "white-space": /^[a-z-]+$/,
171
+ margin: LENGTH_BOX_VALUE,
172
+ "margin-top": LENGTH_BOX_VALUE,
173
+ "margin-right": LENGTH_BOX_VALUE,
174
+ "margin-bottom": LENGTH_BOX_VALUE,
175
+ "margin-left": LENGTH_BOX_VALUE,
176
+ padding: LENGTH_BOX_VALUE,
177
+ "padding-top": LENGTH_BOX_VALUE,
178
+ "padding-right": LENGTH_BOX_VALUE,
179
+ "padding-bottom": LENGTH_BOX_VALUE,
180
+ "padding-left": LENGTH_BOX_VALUE,
181
+ }
182
+
150
183
  function sanitizeStyle(value: string): string | null {
151
184
  const declarations: string[] = []
152
185
 
@@ -166,6 +199,12 @@ function sanitizeStyle(value: string): string | null {
166
199
  if (property === "font-size") {
167
200
  const fontSize = sanitizeFontSize(rawValue)
168
201
  if (fontSize) declarations.push(`font-size: ${fontSize}`)
202
+ continue
203
+ }
204
+
205
+ const allowedValue = SAFE_TEXT_STYLE_PROPS[property]
206
+ if (allowedValue?.test(rawValue)) {
207
+ declarations.push(`${property}: ${rawValue}`)
169
208
  }
170
209
  }
171
210