@stacksjs/sanitizer 0.2.0 → 0.2.5
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/LICENSE.md +21 -0
- package/README.md +360 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +468 -0
- package/dist/index.js.map +12 -0
- package/dist/presets.d.ts +21 -0
- package/dist/sanitizer.d.ts +21 -0
- package/dist/types.d.ts +28 -0
- package/package.json +2 -1
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stacks.js
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![GitHub Actions][github-actions-src]][github-actions-href]
|
|
5
|
+
[](http://commitizen.github.io/cz-cli/)
|
|
6
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
7
|
+
|
|
8
|
+
# stx
|
|
9
|
+
|
|
10
|
+
A modern templating engine with Vue-like Single File Components, Laravel Blade directives, and Bun-powered performance.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Vue-like SFC** - `<script>`, `<template>`, `<style>` structure
|
|
15
|
+
- **Auto-imported Components** - Use `<Card />` directly, no imports needed
|
|
16
|
+
- **Two-way Binding** - `x-model` and `x-text` for reactive forms
|
|
17
|
+
- **Blade Directives** - `@if`, `@foreach`, `@layout`, `@section`
|
|
18
|
+
- **Props & Slots** - Pass data and content to components
|
|
19
|
+
- **200K+ Icons** - Built-in Iconify integration
|
|
20
|
+
- **Custom Directives** - Extend with your own directives
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bun add bun-plugin-stx
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
# bunfig.toml
|
|
30
|
+
preload = ["bun-plugin-stx"]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Single File Components
|
|
34
|
+
|
|
35
|
+
STX components use a Vue-like structure:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<!-- components/Greeting.stx -->
|
|
39
|
+
<script server>
|
|
40
|
+
// Server-side only - used for SSR, stripped from output
|
|
41
|
+
const name = props.name || 'World'
|
|
42
|
+
const time = new Date().toLocaleTimeString()
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="greeting">
|
|
47
|
+
<h1>Hello, {{ name }}!</h1>
|
|
48
|
+
<p>Current time: {{ time }}</p>
|
|
49
|
+
<slot />
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<style>
|
|
54
|
+
.greeting {
|
|
55
|
+
padding: 2rem;
|
|
56
|
+
background: #f5f5f5;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Script Types
|
|
62
|
+
|
|
63
|
+
| Type | Behavior |
|
|
64
|
+
|------|----------|
|
|
65
|
+
| `<script server>` | SSR only - extracted for variables, stripped from output |
|
|
66
|
+
| `<script client>` | Client only - preserved for browser, skips server evaluation |
|
|
67
|
+
| `<script>` | Both - runs on server AND preserved for client |
|
|
68
|
+
|
|
69
|
+
## Components
|
|
70
|
+
|
|
71
|
+
Components in `components/` are auto-imported using PascalCase:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<!-- pages/home.stx -->
|
|
75
|
+
<Header />
|
|
76
|
+
|
|
77
|
+
<main>
|
|
78
|
+
<UserCard name="John" role="Admin" />
|
|
79
|
+
<Card title="Welcome">
|
|
80
|
+
<p>This goes into the slot!</p>
|
|
81
|
+
</Card>
|
|
82
|
+
</main>
|
|
83
|
+
|
|
84
|
+
<Footer />
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Props
|
|
88
|
+
|
|
89
|
+
Pass data to components via attributes:
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<!-- String prop -->
|
|
93
|
+
<Card title="Hello" />
|
|
94
|
+
|
|
95
|
+
<!-- Expression binding with : -->
|
|
96
|
+
<Card :count="items.length" :active="isActive" />
|
|
97
|
+
|
|
98
|
+
<!-- Mustache interpolation -->
|
|
99
|
+
<Card title="{{ userName }}" />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Access props in components:
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<script server>
|
|
106
|
+
const title = props.title || 'Default'
|
|
107
|
+
const count = props.count || 0
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<h1>{{ title }}</h1>
|
|
112
|
+
<p>Count: {{ count }}</p>
|
|
113
|
+
</template>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Slots
|
|
117
|
+
|
|
118
|
+
Use `<slot />` to inject content:
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<!-- components/Card.stx -->
|
|
122
|
+
<template>
|
|
123
|
+
<div class="card">
|
|
124
|
+
<h2>{{ props.title }}</h2>
|
|
125
|
+
<slot />
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```html
|
|
131
|
+
<!-- Usage -->
|
|
132
|
+
<Card title="News">
|
|
133
|
+
<p>This content appears in the slot!</p>
|
|
134
|
+
</Card>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Explicit Imports
|
|
138
|
+
|
|
139
|
+
For components outside `components/`, use `@import`:
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
@import('layouts/Sidebar')
|
|
143
|
+
@import('shared/Button', 'shared/Modal')
|
|
144
|
+
|
|
145
|
+
<Sidebar />
|
|
146
|
+
<Button label="Click me" />
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Layouts
|
|
150
|
+
|
|
151
|
+
Wrap pages with common structure using `@layout`:
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<!-- layouts/default.stx -->
|
|
155
|
+
<!DOCTYPE html>
|
|
156
|
+
<html>
|
|
157
|
+
<head>
|
|
158
|
+
<title>{{ title || 'My App' }}</title>
|
|
159
|
+
</head>
|
|
160
|
+
<body>
|
|
161
|
+
<Header />
|
|
162
|
+
<main>
|
|
163
|
+
@yield('content')
|
|
164
|
+
</main>
|
|
165
|
+
<Footer />
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```html
|
|
171
|
+
<!-- pages/about.stx -->
|
|
172
|
+
@layout('default')
|
|
173
|
+
|
|
174
|
+
@section('content')
|
|
175
|
+
<h1>About Us</h1>
|
|
176
|
+
<p>Welcome to our site.</p>
|
|
177
|
+
@endsection
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Two-Way Binding (x-element)
|
|
181
|
+
|
|
182
|
+
For reactive forms, use x-element directives:
|
|
183
|
+
|
|
184
|
+
```html
|
|
185
|
+
<div x-data="{ message: '', count: 0 }">
|
|
186
|
+
<!-- Two-way binding -->
|
|
187
|
+
<input x-model="message" placeholder="Type here..." />
|
|
188
|
+
|
|
189
|
+
<!-- Reactive display -->
|
|
190
|
+
<p>You typed: <span x-text="message"></span></p>
|
|
191
|
+
|
|
192
|
+
<!-- Event handling -->
|
|
193
|
+
<button @click="count++">Increment</button>
|
|
194
|
+
<button @click="count--">Decrement</button>
|
|
195
|
+
<span x-text="count"></span>
|
|
196
|
+
</div>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
| Directive | Purpose |
|
|
200
|
+
|-----------|---------|
|
|
201
|
+
| `x-data` | Define reactive scope |
|
|
202
|
+
| `x-model` | Two-way binding for inputs |
|
|
203
|
+
| `x-text` | Reactive text content |
|
|
204
|
+
| `@click` | Event handling |
|
|
205
|
+
|
|
206
|
+
## Template Directives
|
|
207
|
+
|
|
208
|
+
### Conditionals
|
|
209
|
+
|
|
210
|
+
```html
|
|
211
|
+
@if (user.isAdmin)
|
|
212
|
+
<AdminPanel />
|
|
213
|
+
@elseif (user.isEditor)
|
|
214
|
+
<EditorTools />
|
|
215
|
+
@else
|
|
216
|
+
<UserView />
|
|
217
|
+
@endif
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Loops
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
@foreach (items as item)
|
|
224
|
+
<li>{{ item.name }}</li>
|
|
225
|
+
@endforeach
|
|
226
|
+
|
|
227
|
+
@for (let i = 0; i < 5; i++)
|
|
228
|
+
<li>Item {{ i }}</li>
|
|
229
|
+
@endfor
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Auth Guards
|
|
233
|
+
|
|
234
|
+
```html
|
|
235
|
+
@auth
|
|
236
|
+
<p>Welcome back, {{ user.name }}!</p>
|
|
237
|
+
@endauth
|
|
238
|
+
|
|
239
|
+
@guest
|
|
240
|
+
<a href="/login">Please log in</a>
|
|
241
|
+
@endguest
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Output
|
|
245
|
+
|
|
246
|
+
```html
|
|
247
|
+
<!-- Escaped (safe) -->
|
|
248
|
+
{{ userInput }}
|
|
249
|
+
|
|
250
|
+
<!-- Raw HTML (trusted content only) -->
|
|
251
|
+
{!! trustedHtml !!}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Custom Directives
|
|
255
|
+
|
|
256
|
+
Register custom directives in your build:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { stxPlugin, type CustomDirective } from 'bun-plugin-stx'
|
|
260
|
+
|
|
261
|
+
const uppercase: CustomDirective = {
|
|
262
|
+
name: 'uppercase',
|
|
263
|
+
handler: (content, params) => params[0]?.toUpperCase() || content.toUpperCase()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const wrap: CustomDirective = {
|
|
267
|
+
name: 'wrap',
|
|
268
|
+
hasEndTag: true,
|
|
269
|
+
handler: (content, params) => `<div class="${params[0] || 'wrapper'}">${content}</div>`
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
Bun.build({
|
|
273
|
+
entrypoints: ['./src/index.stx'],
|
|
274
|
+
plugins: [stxPlugin({
|
|
275
|
+
customDirectives: [uppercase, wrap]
|
|
276
|
+
})]
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<!-- Usage -->
|
|
282
|
+
<p>@uppercase('hello world')</p>
|
|
283
|
+
|
|
284
|
+
@wrap('container')
|
|
285
|
+
<p>Wrapped content</p>
|
|
286
|
+
@endwrap
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Icons
|
|
290
|
+
|
|
291
|
+
200K+ icons via Iconify:
|
|
292
|
+
|
|
293
|
+
```html
|
|
294
|
+
<HomeIcon size="24" />
|
|
295
|
+
<SearchIcon size="20" color="#333" />
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
bun stx iconify list
|
|
300
|
+
bun stx iconify generate material-symbols
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Complete Example
|
|
304
|
+
|
|
305
|
+
```html
|
|
306
|
+
<!-- components/TodoApp.stx -->
|
|
307
|
+
<script server>
|
|
308
|
+
const title = props.title || 'My Todos'
|
|
309
|
+
</script>
|
|
310
|
+
|
|
311
|
+
<template>
|
|
312
|
+
<div class="todo-app" x-data="{ todos: [], newTodo: '' }">
|
|
313
|
+
<h1>{{ title }}</h1>
|
|
314
|
+
|
|
315
|
+
<form @submit.prevent="todos.push({ text: newTodo, done: false }); newTodo = ''">
|
|
316
|
+
<input x-model="newTodo" placeholder="Add todo..." />
|
|
317
|
+
<button type="submit">Add</button>
|
|
318
|
+
</form>
|
|
319
|
+
|
|
320
|
+
@if (initialTodos)
|
|
321
|
+
<ul>
|
|
322
|
+
@foreach (initialTodos as todo)
|
|
323
|
+
<li>{{ todo.text }}</li>
|
|
324
|
+
@endforeach
|
|
325
|
+
</ul>
|
|
326
|
+
@endif
|
|
327
|
+
</div>
|
|
328
|
+
</template>
|
|
329
|
+
|
|
330
|
+
<style>
|
|
331
|
+
.todo-app {
|
|
332
|
+
max-width: 400px;
|
|
333
|
+
margin: 0 auto;
|
|
334
|
+
}
|
|
335
|
+
</style>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Documentation
|
|
339
|
+
|
|
340
|
+
- [Full Documentation](https://stx.sh)
|
|
341
|
+
- [Syntax Highlighting Guide](./docs/STX_SYNTAX_HIGHLIGHTING.md)
|
|
342
|
+
- [Examples](./examples/)
|
|
343
|
+
|
|
344
|
+
## Testing
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
bun test
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## License
|
|
351
|
+
|
|
352
|
+
MIT
|
|
353
|
+
|
|
354
|
+
<!-- Badges -->
|
|
355
|
+
[npm-version-src]: https://img.shields.io/npm/v/@stacksjs/stx?style=flat-square
|
|
356
|
+
[npm-version-href]: https://npmjs.com/package/@stacksjs/stx
|
|
357
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/@stacksjs/stx?style=flat-square
|
|
358
|
+
[npm-downloads-href]: https://npmjs.com/package/@stacksjs/stx
|
|
359
|
+
[github-actions-src]: https://img.shields.io/github/actions/workflow/status/stacksjs/stx/ci.yml?style=flat-square&branch=main
|
|
360
|
+
[github-actions-href]: https://github.com/stacksjs/stx/actions?query=workflow%3Aci
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { sanitize } from './sanitizer';
|
|
2
|
+
export * from './presets';
|
|
3
|
+
export { basic, getPreset, markdown, relaxed, strict } from './presets';
|
|
4
|
+
export * from './sanitizer';
|
|
5
|
+
export { escape, isSafe, sanitize, sanitizeWithInfo, stripTags } from './sanitizer';
|
|
6
|
+
export * from './types';
|
|
7
|
+
export default sanitize;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/presets.ts
|
|
3
|
+
var strict = {
|
|
4
|
+
allowedTags: [
|
|
5
|
+
"p",
|
|
6
|
+
"br",
|
|
7
|
+
"strong",
|
|
8
|
+
"em",
|
|
9
|
+
"u",
|
|
10
|
+
"span",
|
|
11
|
+
"h1",
|
|
12
|
+
"h2",
|
|
13
|
+
"h3",
|
|
14
|
+
"h4",
|
|
15
|
+
"h5",
|
|
16
|
+
"h6",
|
|
17
|
+
"ul",
|
|
18
|
+
"ol",
|
|
19
|
+
"li",
|
|
20
|
+
"code",
|
|
21
|
+
"pre"
|
|
22
|
+
],
|
|
23
|
+
allowedAttributes: {
|
|
24
|
+
"*": ["class", "id"]
|
|
25
|
+
},
|
|
26
|
+
allowedSchemes: ["http", "https", "mailto"],
|
|
27
|
+
allowDataAttributes: false,
|
|
28
|
+
allowAriaAttributes: true,
|
|
29
|
+
stripTags: true,
|
|
30
|
+
allowComments: false
|
|
31
|
+
};
|
|
32
|
+
var basic = {
|
|
33
|
+
allowedTags: [
|
|
34
|
+
"p",
|
|
35
|
+
"br",
|
|
36
|
+
"strong",
|
|
37
|
+
"em",
|
|
38
|
+
"u",
|
|
39
|
+
"span",
|
|
40
|
+
"div",
|
|
41
|
+
"h1",
|
|
42
|
+
"h2",
|
|
43
|
+
"h3",
|
|
44
|
+
"h4",
|
|
45
|
+
"h5",
|
|
46
|
+
"h6",
|
|
47
|
+
"ul",
|
|
48
|
+
"ol",
|
|
49
|
+
"li",
|
|
50
|
+
"a",
|
|
51
|
+
"img",
|
|
52
|
+
"code",
|
|
53
|
+
"pre",
|
|
54
|
+
"blockquote",
|
|
55
|
+
"table",
|
|
56
|
+
"thead",
|
|
57
|
+
"tbody",
|
|
58
|
+
"tr",
|
|
59
|
+
"th",
|
|
60
|
+
"td"
|
|
61
|
+
],
|
|
62
|
+
allowedAttributes: {
|
|
63
|
+
"*": ["class", "id"],
|
|
64
|
+
a: ["class", "id", "href", "title", "target", "rel"],
|
|
65
|
+
img: ["class", "id", "src", "alt", "title", "width", "height"],
|
|
66
|
+
td: ["class", "id", "colspan", "rowspan", "align"],
|
|
67
|
+
th: ["class", "id", "colspan", "rowspan", "align", "scope"]
|
|
68
|
+
},
|
|
69
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
70
|
+
allowDataAttributes: false,
|
|
71
|
+
allowAriaAttributes: true,
|
|
72
|
+
stripTags: true,
|
|
73
|
+
allowComments: false
|
|
74
|
+
};
|
|
75
|
+
var relaxed = {
|
|
76
|
+
allowedTags: [
|
|
77
|
+
"p",
|
|
78
|
+
"br",
|
|
79
|
+
"strong",
|
|
80
|
+
"em",
|
|
81
|
+
"u",
|
|
82
|
+
"span",
|
|
83
|
+
"div",
|
|
84
|
+
"section",
|
|
85
|
+
"article",
|
|
86
|
+
"aside",
|
|
87
|
+
"h1",
|
|
88
|
+
"h2",
|
|
89
|
+
"h3",
|
|
90
|
+
"h4",
|
|
91
|
+
"h5",
|
|
92
|
+
"h6",
|
|
93
|
+
"ul",
|
|
94
|
+
"ol",
|
|
95
|
+
"li",
|
|
96
|
+
"dl",
|
|
97
|
+
"dt",
|
|
98
|
+
"dd",
|
|
99
|
+
"a",
|
|
100
|
+
"img",
|
|
101
|
+
"figure",
|
|
102
|
+
"figcaption",
|
|
103
|
+
"code",
|
|
104
|
+
"pre",
|
|
105
|
+
"kbd",
|
|
106
|
+
"samp",
|
|
107
|
+
"var",
|
|
108
|
+
"blockquote",
|
|
109
|
+
"q",
|
|
110
|
+
"cite",
|
|
111
|
+
"table",
|
|
112
|
+
"thead",
|
|
113
|
+
"tbody",
|
|
114
|
+
"tfoot",
|
|
115
|
+
"tr",
|
|
116
|
+
"th",
|
|
117
|
+
"td",
|
|
118
|
+
"caption",
|
|
119
|
+
"del",
|
|
120
|
+
"ins",
|
|
121
|
+
"sub",
|
|
122
|
+
"sup",
|
|
123
|
+
"abbr",
|
|
124
|
+
"address",
|
|
125
|
+
"time",
|
|
126
|
+
"hr",
|
|
127
|
+
"video",
|
|
128
|
+
"audio",
|
|
129
|
+
"source",
|
|
130
|
+
"track"
|
|
131
|
+
],
|
|
132
|
+
allowedAttributes: {
|
|
133
|
+
"*": ["class", "id", "title"],
|
|
134
|
+
a: ["class", "id", "href", "title", "target", "rel"],
|
|
135
|
+
img: ["class", "id", "src", "srcset", "alt", "title", "width", "height", "loading"],
|
|
136
|
+
video: ["class", "id", "src", "width", "height", "controls", "preload", "poster"],
|
|
137
|
+
audio: ["class", "id", "src", "controls", "preload"],
|
|
138
|
+
source: ["src", "type"],
|
|
139
|
+
track: ["src", "kind", "srclang", "label"],
|
|
140
|
+
td: ["class", "id", "colspan", "rowspan", "align"],
|
|
141
|
+
th: ["class", "id", "colspan", "rowspan", "align", "scope"],
|
|
142
|
+
time: ["class", "id", "datetime"],
|
|
143
|
+
abbr: ["class", "id", "title"]
|
|
144
|
+
},
|
|
145
|
+
allowedSchemes: ["http", "https", "mailto", "tel", "data"],
|
|
146
|
+
allowDataAttributes: true,
|
|
147
|
+
allowAriaAttributes: true,
|
|
148
|
+
stripTags: true,
|
|
149
|
+
allowComments: false
|
|
150
|
+
};
|
|
151
|
+
var markdown = {
|
|
152
|
+
allowedTags: [
|
|
153
|
+
"p",
|
|
154
|
+
"br",
|
|
155
|
+
"strong",
|
|
156
|
+
"em",
|
|
157
|
+
"span",
|
|
158
|
+
"h1",
|
|
159
|
+
"h2",
|
|
160
|
+
"h3",
|
|
161
|
+
"h4",
|
|
162
|
+
"h5",
|
|
163
|
+
"h6",
|
|
164
|
+
"ul",
|
|
165
|
+
"ol",
|
|
166
|
+
"li",
|
|
167
|
+
"a",
|
|
168
|
+
"img",
|
|
169
|
+
"code",
|
|
170
|
+
"pre",
|
|
171
|
+
"blockquote",
|
|
172
|
+
"table",
|
|
173
|
+
"thead",
|
|
174
|
+
"tbody",
|
|
175
|
+
"tr",
|
|
176
|
+
"th",
|
|
177
|
+
"td",
|
|
178
|
+
"del",
|
|
179
|
+
"ins",
|
|
180
|
+
"hr",
|
|
181
|
+
"input"
|
|
182
|
+
],
|
|
183
|
+
allowedAttributes: {
|
|
184
|
+
"*": ["class", "id"],
|
|
185
|
+
a: ["class", "id", "href", "title"],
|
|
186
|
+
img: ["class", "id", "src", "alt", "title"],
|
|
187
|
+
code: ["class"],
|
|
188
|
+
td: ["align"],
|
|
189
|
+
th: ["align", "scope"],
|
|
190
|
+
input: ["type", "checked", "disabled"]
|
|
191
|
+
},
|
|
192
|
+
allowedSchemes: ["http", "https", "mailto"],
|
|
193
|
+
allowDataAttributes: false,
|
|
194
|
+
allowAriaAttributes: false,
|
|
195
|
+
stripTags: true,
|
|
196
|
+
allowComments: false
|
|
197
|
+
};
|
|
198
|
+
function getPreset(name) {
|
|
199
|
+
switch (name) {
|
|
200
|
+
case "strict":
|
|
201
|
+
return strict;
|
|
202
|
+
case "basic":
|
|
203
|
+
return basic;
|
|
204
|
+
case "relaxed":
|
|
205
|
+
return relaxed;
|
|
206
|
+
case "markdown":
|
|
207
|
+
return markdown;
|
|
208
|
+
default:
|
|
209
|
+
return basic;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/sanitizer.ts
|
|
214
|
+
var DEFAULT_OPTIONS = {
|
|
215
|
+
allowedTags: [],
|
|
216
|
+
allowedAttributes: {},
|
|
217
|
+
allowedSchemes: ["http", "https", "mailto"],
|
|
218
|
+
allowDataAttributes: false,
|
|
219
|
+
allowAriaAttributes: true,
|
|
220
|
+
stripTags: true,
|
|
221
|
+
allowComments: false
|
|
222
|
+
};
|
|
223
|
+
var DANGEROUS_TAGS = [
|
|
224
|
+
"script",
|
|
225
|
+
"iframe",
|
|
226
|
+
"object",
|
|
227
|
+
"embed",
|
|
228
|
+
"applet",
|
|
229
|
+
"base",
|
|
230
|
+
"link",
|
|
231
|
+
"meta",
|
|
232
|
+
"style"
|
|
233
|
+
];
|
|
234
|
+
var URL_ATTRIBUTES = [
|
|
235
|
+
"href",
|
|
236
|
+
"src",
|
|
237
|
+
"action",
|
|
238
|
+
"formaction",
|
|
239
|
+
"data",
|
|
240
|
+
"poster",
|
|
241
|
+
"cite",
|
|
242
|
+
"background",
|
|
243
|
+
"longdesc"
|
|
244
|
+
];
|
|
245
|
+
var EVENT_ATTRIBUTES_REGEX = /^on\w+/i;
|
|
246
|
+
function sanitize(html, options) {
|
|
247
|
+
const result = sanitizeWithInfo(html, options);
|
|
248
|
+
return result.html;
|
|
249
|
+
}
|
|
250
|
+
function sanitizeWithInfo(html, options) {
|
|
251
|
+
const opts = typeof options === "string" ? getPreset(options) : { ...DEFAULT_OPTIONS, ...options };
|
|
252
|
+
const removedTags = [];
|
|
253
|
+
const removedAttributes = [];
|
|
254
|
+
let result = html;
|
|
255
|
+
let modified = false;
|
|
256
|
+
if (!opts.allowComments) {
|
|
257
|
+
const commentRegex = /<!--[\s\S]*?-->/g;
|
|
258
|
+
if (commentRegex.test(result)) {
|
|
259
|
+
result = result.replace(commentRegex, "");
|
|
260
|
+
modified = true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const tagRegex = /<(\/?)([a-zA-Z][\w-]*)([^>]*)>/g;
|
|
264
|
+
result = result.replace(tagRegex, (match, closing, tagName, attributesStr) => {
|
|
265
|
+
const lowerTag = tagName.toLowerCase();
|
|
266
|
+
if (DANGEROUS_TAGS.includes(lowerTag)) {
|
|
267
|
+
if (!removedTags.includes(lowerTag)) {
|
|
268
|
+
removedTags.push(lowerTag);
|
|
269
|
+
}
|
|
270
|
+
modified = true;
|
|
271
|
+
return opts.stripTags ? "" : escapeHtml(match);
|
|
272
|
+
}
|
|
273
|
+
const allowedTags = opts.allowedTags || [];
|
|
274
|
+
if (allowedTags.length > 0 && !allowedTags.includes(lowerTag)) {
|
|
275
|
+
if (!removedTags.includes(lowerTag)) {
|
|
276
|
+
removedTags.push(lowerTag);
|
|
277
|
+
}
|
|
278
|
+
modified = true;
|
|
279
|
+
return opts.stripTags ? "" : escapeHtml(match);
|
|
280
|
+
}
|
|
281
|
+
if (closing) {
|
|
282
|
+
return match;
|
|
283
|
+
}
|
|
284
|
+
const sanitizedAttrs = sanitizeAttributes(attributesStr, lowerTag, opts, removedAttributes);
|
|
285
|
+
if (sanitizedAttrs !== attributesStr) {
|
|
286
|
+
modified = true;
|
|
287
|
+
}
|
|
288
|
+
if (opts.transformTag) {
|
|
289
|
+
const attributes = parseAttributes(sanitizedAttrs);
|
|
290
|
+
const transformed = opts.transformTag(lowerTag, attributes);
|
|
291
|
+
if (transformed === null) {
|
|
292
|
+
if (!removedTags.includes(lowerTag)) {
|
|
293
|
+
removedTags.push(lowerTag);
|
|
294
|
+
}
|
|
295
|
+
modified = true;
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
298
|
+
if (transformed.tagName !== lowerTag || JSON.stringify(transformed.attributes) !== JSON.stringify(attributes)) {
|
|
299
|
+
modified = true;
|
|
300
|
+
return buildTag(transformed.tagName, transformed.attributes);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return `<${tagName}${sanitizedAttrs}>`;
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
html: result,
|
|
307
|
+
modified,
|
|
308
|
+
removedTags: removedTags.length > 0 ? removedTags : undefined,
|
|
309
|
+
removedAttributes: removedAttributes.length > 0 ? removedAttributes : undefined
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function sanitizeAttributes(attributesStr, tagName, options, removedAttributes) {
|
|
313
|
+
if (!attributesStr.trim()) {
|
|
314
|
+
return attributesStr;
|
|
315
|
+
}
|
|
316
|
+
const attributes = parseAttributes(attributesStr);
|
|
317
|
+
const sanitized = {};
|
|
318
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
319
|
+
const lowerName = name.toLowerCase();
|
|
320
|
+
if (EVENT_ATTRIBUTES_REGEX.test(lowerName)) {
|
|
321
|
+
if (!removedAttributes.includes(lowerName)) {
|
|
322
|
+
removedAttributes.push(lowerName);
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (!isAttributeAllowed(lowerName, tagName, options)) {
|
|
327
|
+
if (!removedAttributes.includes(lowerName)) {
|
|
328
|
+
removedAttributes.push(lowerName);
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (URL_ATTRIBUTES.includes(lowerName)) {
|
|
333
|
+
if (!isUrlSafe(value, options)) {
|
|
334
|
+
if (!removedAttributes.includes(lowerName)) {
|
|
335
|
+
removedAttributes.push(lowerName);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (lowerName === "style" && options.allowedStyles) {
|
|
341
|
+
sanitized[name] = sanitizeStyle(value, options.allowedStyles);
|
|
342
|
+
} else {
|
|
343
|
+
sanitized[name] = value;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return buildAttributes(sanitized);
|
|
347
|
+
}
|
|
348
|
+
function isAttributeAllowed(attrName, tagName, options) {
|
|
349
|
+
const allowedAttrs = options.allowedAttributes;
|
|
350
|
+
if (!allowedAttrs) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
if (attrName.startsWith("data-")) {
|
|
354
|
+
return options.allowDataAttributes || false;
|
|
355
|
+
}
|
|
356
|
+
if (attrName.startsWith("aria-")) {
|
|
357
|
+
return options.allowAriaAttributes || false;
|
|
358
|
+
}
|
|
359
|
+
if (Array.isArray(allowedAttrs)) {
|
|
360
|
+
return allowedAttrs.includes(attrName);
|
|
361
|
+
}
|
|
362
|
+
if (allowedAttrs[tagName]?.includes(attrName)) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if (allowedAttrs["*"]?.includes(attrName)) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
function isUrlSafe(url, options) {
|
|
371
|
+
if (options.urlValidator) {
|
|
372
|
+
return options.urlValidator(url);
|
|
373
|
+
}
|
|
374
|
+
const trimmed = url.trim().toLowerCase();
|
|
375
|
+
const cleaned = trimmed.replace(/[\s\x00-\x1F]/g, "");
|
|
376
|
+
const dangerousProtocols = ["javascript:", "data:text/html", "vbscript:", "file:"];
|
|
377
|
+
for (const protocol of dangerousProtocols) {
|
|
378
|
+
if (cleaned.startsWith(protocol)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (options.allowedSchemes && options.allowedSchemes.length > 0) {
|
|
383
|
+
if (!cleaned.includes(":")) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const hasAllowedScheme = options.allowedSchemes.some((scheme) => {
|
|
387
|
+
return cleaned.startsWith(`${scheme}:`);
|
|
388
|
+
});
|
|
389
|
+
if (!hasAllowedScheme) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
function sanitizeStyle(style, allowedStyles) {
|
|
396
|
+
const properties = style.split(";").filter((p) => p.trim());
|
|
397
|
+
const sanitized = [];
|
|
398
|
+
for (const prop of properties) {
|
|
399
|
+
const colonIndex = prop.indexOf(":");
|
|
400
|
+
if (colonIndex === -1)
|
|
401
|
+
continue;
|
|
402
|
+
const name = prop.substring(0, colonIndex).trim().toLowerCase();
|
|
403
|
+
const value = prop.substring(colonIndex + 1).trim();
|
|
404
|
+
if (allowedStyles.includes(name)) {
|
|
405
|
+
if (!value.toLowerCase().includes("javascript:") && !value.toLowerCase().includes("expression(")) {
|
|
406
|
+
sanitized.push(`${name}: ${value}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return sanitized.join("; ");
|
|
411
|
+
}
|
|
412
|
+
function parseAttributes(attributesStr) {
|
|
413
|
+
const attributes = {};
|
|
414
|
+
const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
415
|
+
let match;
|
|
416
|
+
while (match = attrRegex.exec(attributesStr)) {
|
|
417
|
+
const name = match[1];
|
|
418
|
+
const value = match[2] || match[3] || match[4] || "";
|
|
419
|
+
attributes[name] = value;
|
|
420
|
+
}
|
|
421
|
+
return attributes;
|
|
422
|
+
}
|
|
423
|
+
function buildAttributes(attributes) {
|
|
424
|
+
const parts = [];
|
|
425
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
426
|
+
if (value === "") {
|
|
427
|
+
parts.push(name);
|
|
428
|
+
} else {
|
|
429
|
+
const escaped = value.replace(/"/g, """);
|
|
430
|
+
parts.push(`${name}="${escaped}"`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
434
|
+
}
|
|
435
|
+
function buildTag(tagName, attributes) {
|
|
436
|
+
return `<${tagName}${buildAttributes(attributes)}>`;
|
|
437
|
+
}
|
|
438
|
+
function escapeHtml(text) {
|
|
439
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
440
|
+
}
|
|
441
|
+
function isSafe(html, options) {
|
|
442
|
+
const result = sanitizeWithInfo(html, options);
|
|
443
|
+
return !result.modified;
|
|
444
|
+
}
|
|
445
|
+
function stripTags(html) {
|
|
446
|
+
return html.replace(/<[^>]*>/g, "");
|
|
447
|
+
}
|
|
448
|
+
function escape(text) {
|
|
449
|
+
return escapeHtml(text);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/index.ts
|
|
453
|
+
var src_default = sanitize;
|
|
454
|
+
export {
|
|
455
|
+
stripTags,
|
|
456
|
+
strict,
|
|
457
|
+
sanitizeWithInfo,
|
|
458
|
+
sanitize,
|
|
459
|
+
relaxed,
|
|
460
|
+
markdown,
|
|
461
|
+
isSafe,
|
|
462
|
+
getPreset,
|
|
463
|
+
escape,
|
|
464
|
+
src_default as default,
|
|
465
|
+
basic
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
//# debugId=11C850CF49E4930C64756E2164756E21
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/presets.ts", "../src/sanitizer.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { SanitizerOptions } from './types'\n\n/**\n * Strict preset - Only safe, basic formatting\n */\nexport const strict: SanitizerOptions = {\n allowedTags: [\n 'p',\n 'br',\n 'strong',\n 'em',\n 'u',\n 'span',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'code',\n 'pre',\n ],\n allowedAttributes: {\n '*': ['class', 'id'],\n },\n allowedSchemes: ['http', 'https', 'mailto'],\n allowDataAttributes: false,\n allowAriaAttributes: true,\n stripTags: true,\n allowComments: false,\n}\n\n/**\n * Basic preset - Common safe HTML elements\n */\nexport const basic: SanitizerOptions = {\n allowedTags: [\n 'p',\n 'br',\n 'strong',\n 'em',\n 'u',\n 'span',\n 'div',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'a',\n 'img',\n 'code',\n 'pre',\n 'blockquote',\n 'table',\n 'thead',\n 'tbody',\n 'tr',\n 'th',\n 'td',\n ],\n allowedAttributes: {\n '*': ['class', 'id'],\n 'a': ['class', 'id', 'href', 'title', 'target', 'rel'],\n 'img': ['class', 'id', 'src', 'alt', 'title', 'width', 'height'],\n 'td': ['class', 'id', 'colspan', 'rowspan', 'align'],\n 'th': ['class', 'id', 'colspan', 'rowspan', 'align', 'scope'],\n },\n allowedSchemes: ['http', 'https', 'mailto', 'tel'],\n allowDataAttributes: false,\n allowAriaAttributes: true,\n stripTags: true,\n allowComments: false,\n}\n\n/**\n * Relaxed preset - More permissive, includes media and interactive elements\n */\nexport const relaxed: SanitizerOptions = {\n allowedTags: [\n 'p',\n 'br',\n 'strong',\n 'em',\n 'u',\n 'span',\n 'div',\n 'section',\n 'article',\n 'aside',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'dl',\n 'dt',\n 'dd',\n 'a',\n 'img',\n 'figure',\n 'figcaption',\n 'code',\n 'pre',\n 'kbd',\n 'samp',\n 'var',\n 'blockquote',\n 'q',\n 'cite',\n 'table',\n 'thead',\n 'tbody',\n 'tfoot',\n 'tr',\n 'th',\n 'td',\n 'caption',\n 'del',\n 'ins',\n 'sub',\n 'sup',\n 'abbr',\n 'address',\n 'time',\n 'hr',\n 'video',\n 'audio',\n 'source',\n 'track',\n ],\n allowedAttributes: {\n '*': ['class', 'id', 'title'],\n 'a': ['class', 'id', 'href', 'title', 'target', 'rel'],\n 'img': ['class', 'id', 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading'],\n 'video': ['class', 'id', 'src', 'width', 'height', 'controls', 'preload', 'poster'],\n 'audio': ['class', 'id', 'src', 'controls', 'preload'],\n 'source': ['src', 'type'],\n 'track': ['src', 'kind', 'srclang', 'label'],\n 'td': ['class', 'id', 'colspan', 'rowspan', 'align'],\n 'th': ['class', 'id', 'colspan', 'rowspan', 'align', 'scope'],\n 'time': ['class', 'id', 'datetime'],\n 'abbr': ['class', 'id', 'title'],\n },\n allowedSchemes: ['http', 'https', 'mailto', 'tel', 'data'],\n allowDataAttributes: true,\n allowAriaAttributes: true,\n stripTags: true,\n allowComments: false,\n}\n\n/**\n * Markdown preset - Optimized for markdown-generated HTML\n */\nexport const markdown: SanitizerOptions = {\n allowedTags: [\n 'p',\n 'br',\n 'strong',\n 'em',\n 'span',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'li',\n 'a',\n 'img',\n 'code',\n 'pre',\n 'blockquote',\n 'table',\n 'thead',\n 'tbody',\n 'tr',\n 'th',\n 'td',\n 'del',\n 'ins',\n 'hr',\n 'input', // For task lists\n ],\n allowedAttributes: {\n '*': ['class', 'id'],\n 'a': ['class', 'id', 'href', 'title'],\n 'img': ['class', 'id', 'src', 'alt', 'title'],\n 'code': ['class'], // For syntax highlighting\n 'td': ['align'],\n 'th': ['align', 'scope'],\n 'input': ['type', 'checked', 'disabled'], // For task lists\n },\n allowedSchemes: ['http', 'https', 'mailto'],\n allowDataAttributes: false,\n allowAriaAttributes: false,\n stripTags: true,\n allowComments: false,\n}\n\n/**\n * Get preset by name\n */\nexport function getPreset(name: string): SanitizerOptions {\n switch (name) {\n case 'strict':\n return strict\n case 'basic':\n return basic\n case 'relaxed':\n return relaxed\n case 'markdown':\n return markdown\n default:\n return basic\n }\n}\n",
|
|
6
|
+
"import type { SanitizeResult, SanitizerOptions, SanitizerPreset } from './types'\nimport { getPreset } from './presets'\n\n/**\n * Fast, native HTML sanitizer optimized for Bun\n * Provides DOMPurify-like features with performance focus\n */\n\nconst DEFAULT_OPTIONS: SanitizerOptions = {\n allowedTags: [],\n allowedAttributes: {},\n allowedSchemes: ['http', 'https', 'mailto'],\n allowDataAttributes: false,\n allowAriaAttributes: true,\n stripTags: true,\n allowComments: false,\n}\n\n// Dangerous tags that should never be allowed\nconst DANGEROUS_TAGS = [\n 'script',\n 'iframe',\n 'object',\n 'embed',\n 'applet',\n 'base',\n 'link',\n 'meta',\n 'style',\n]\n\n// URL attributes that need validation\nconst URL_ATTRIBUTES = [\n 'href',\n 'src',\n 'action',\n 'formaction',\n 'data',\n 'poster',\n 'cite',\n 'background',\n 'longdesc',\n]\n\n// Event handler attributes (always removed)\nconst EVENT_ATTRIBUTES_REGEX = /^on\\w+/i\n\n/**\n * Sanitize HTML content\n */\nexport function sanitize(\n html: string,\n options?: SanitizerOptions | SanitizerPreset,\n): string {\n const result = sanitizeWithInfo(html, options)\n return result.html\n}\n\n/**\n * Sanitize HTML content with detailed information\n */\nexport function sanitizeWithInfo(\n html: string,\n options?: SanitizerOptions | SanitizerPreset,\n): SanitizeResult {\n // Load preset if string provided\n const opts: SanitizerOptions = typeof options === 'string'\n ? getPreset(options)\n : { ...DEFAULT_OPTIONS, ...options }\n\n const removedTags: string[] = []\n const removedAttributes: string[] = []\n\n // Use a simple regex-based parser for performance\n let result = html\n let modified = false\n\n // Remove comments if not allowed\n if (!opts.allowComments) {\n const commentRegex = /<!--[\\s\\S]*?-->/g\n if (commentRegex.test(result)) {\n result = result.replace(commentRegex, '')\n modified = true\n }\n }\n\n // Parse and sanitize tags\n // eslint-disable-next-line regexp/use-ignore-case\n const tagRegex = /<(\\/?)([a-zA-Z][\\w-]*)([^>]*)>/g\n result = result.replace(tagRegex, (match, closing, tagName, attributesStr) => {\n const lowerTag = tagName.toLowerCase()\n\n // Always remove dangerous tags\n if (DANGEROUS_TAGS.includes(lowerTag)) {\n if (!removedTags.includes(lowerTag)) {\n removedTags.push(lowerTag)\n }\n modified = true\n return opts.stripTags ? '' : escapeHtml(match)\n }\n\n // Check if tag is allowed\n const allowedTags = opts.allowedTags || []\n if (allowedTags.length > 0 && !allowedTags.includes(lowerTag)) {\n if (!removedTags.includes(lowerTag)) {\n removedTags.push(lowerTag)\n }\n modified = true\n return opts.stripTags ? '' : escapeHtml(match)\n }\n\n // For closing tags, just return as-is (already validated by opening tag)\n if (closing) {\n return match\n }\n\n // Sanitize attributes\n const sanitizedAttrs = sanitizeAttributes(\n attributesStr,\n lowerTag,\n opts,\n removedAttributes,\n )\n\n if (sanitizedAttrs !== attributesStr) {\n modified = true\n }\n\n // Transform tag if transformer provided\n if (opts.transformTag) {\n const attributes = parseAttributes(sanitizedAttrs)\n const transformed = opts.transformTag(lowerTag, attributes)\n\n if (transformed === null) {\n if (!removedTags.includes(lowerTag)) {\n removedTags.push(lowerTag)\n }\n modified = true\n return ''\n }\n\n if (transformed.tagName !== lowerTag || JSON.stringify(transformed.attributes) !== JSON.stringify(attributes)) {\n modified = true\n return buildTag(transformed.tagName, transformed.attributes)\n }\n }\n\n return `<${tagName}${sanitizedAttrs}>`\n })\n\n return {\n html: result,\n modified,\n removedTags: removedTags.length > 0 ? removedTags : undefined,\n removedAttributes: removedAttributes.length > 0 ? removedAttributes : undefined,\n }\n}\n\n/**\n * Sanitize attributes\n */\nfunction sanitizeAttributes(\n attributesStr: string,\n tagName: string,\n options: SanitizerOptions,\n removedAttributes: string[],\n): string {\n if (!attributesStr.trim()) {\n return attributesStr\n }\n\n const attributes = parseAttributes(attributesStr)\n const sanitized: Record<string, string> = {}\n\n for (const [name, value] of Object.entries(attributes)) {\n const lowerName = name.toLowerCase()\n\n // Remove event handlers\n if (EVENT_ATTRIBUTES_REGEX.test(lowerName)) {\n if (!removedAttributes.includes(lowerName)) {\n removedAttributes.push(lowerName)\n }\n continue\n }\n\n // Check if attribute is allowed\n if (!isAttributeAllowed(lowerName, tagName, options)) {\n if (!removedAttributes.includes(lowerName)) {\n removedAttributes.push(lowerName)\n }\n continue\n }\n\n // Validate URL attributes\n if (URL_ATTRIBUTES.includes(lowerName)) {\n if (!isUrlSafe(value, options)) {\n if (!removedAttributes.includes(lowerName)) {\n removedAttributes.push(lowerName)\n }\n continue\n }\n }\n\n // Sanitize style attribute\n if (lowerName === 'style' && options.allowedStyles) {\n sanitized[name] = sanitizeStyle(value, options.allowedStyles)\n }\n else {\n sanitized[name] = value\n }\n }\n\n return buildAttributes(sanitized)\n}\n\n/**\n * Check if attribute is allowed\n */\nfunction isAttributeAllowed(\n attrName: string,\n tagName: string,\n options: SanitizerOptions,\n): boolean {\n const allowedAttrs = options.allowedAttributes\n\n if (!allowedAttrs) {\n return false\n }\n\n // Check data attributes\n if (attrName.startsWith('data-')) {\n return options.allowDataAttributes || false\n }\n\n // Check aria attributes\n if (attrName.startsWith('aria-')) {\n return options.allowAriaAttributes || false\n }\n\n // Array format (global allowed attributes)\n if (Array.isArray(allowedAttrs)) {\n return allowedAttrs.includes(attrName)\n }\n\n // Object format (per-tag or global)\n if (allowedAttrs[tagName]?.includes(attrName)) {\n return true\n }\n\n // Check global attributes (*)\n if (allowedAttrs['*']?.includes(attrName)) {\n return true\n }\n\n return false\n}\n\n/**\n * Validate URL safety\n */\nfunction isUrlSafe(url: string, options: SanitizerOptions): boolean {\n // Use custom validator if provided\n if (options.urlValidator) {\n return options.urlValidator(url)\n }\n\n // Check for javascript: protocol and other dangerous schemes\n const trimmed = url.trim().toLowerCase()\n\n // Remove common whitespace/encoding tricks\n // eslint-disable-next-line no-control-regex\n const cleaned = trimmed.replace(/[\\s\\x00-\\x1F]/g, '')\n\n // Dangerous protocols\n const dangerousProtocols = ['javascript:', 'data:text/html', 'vbscript:', 'file:']\n for (const protocol of dangerousProtocols) {\n if (cleaned.startsWith(protocol)) {\n return false\n }\n }\n\n // Check allowed schemes\n if (options.allowedSchemes && options.allowedSchemes.length > 0) {\n // Relative URLs are allowed\n if (!cleaned.includes(':')) {\n return true\n }\n\n const hasAllowedScheme = options.allowedSchemes.some((scheme) => {\n return cleaned.startsWith(`${scheme}:`)\n })\n\n if (!hasAllowedScheme) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Sanitize inline styles\n */\nfunction sanitizeStyle(style: string, allowedStyles: string[]): string {\n const properties = style.split(';').filter(p => p.trim())\n const sanitized: string[] = []\n\n for (const prop of properties) {\n const colonIndex = prop.indexOf(':')\n if (colonIndex === -1)\n continue\n\n const name = prop.substring(0, colonIndex).trim().toLowerCase()\n const value = prop.substring(colonIndex + 1).trim()\n\n // Check if property is allowed\n if (allowedStyles.includes(name)) {\n // Additional validation for dangerous values\n if (!value.toLowerCase().includes('javascript:') && !value.toLowerCase().includes('expression(')) {\n sanitized.push(`${name}: ${value}`)\n }\n }\n }\n\n return sanitized.join('; ')\n}\n\n/**\n * Parse HTML attributes from string\n */\nfunction parseAttributes(attributesStr: string): Record<string, string> {\n const attributes: Record<string, string> = {}\n const attrRegex = /([\\w-]+)(?:=(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+)))?/g\n\n let match\n // biome-ignore lint/suspicious/noAssignInExpressions: needed for regex matching\n // eslint-disable-next-line no-cond-assign\n while ((match = attrRegex.exec(attributesStr))) {\n const name = match[1]\n const value = match[2] || match[3] || match[4] || ''\n attributes[name] = value\n }\n\n return attributes\n}\n\n/**\n * Build attributes string from object\n */\nfunction buildAttributes(attributes: Record<string, string>): string {\n const parts: string[] = []\n\n for (const [name, value] of Object.entries(attributes)) {\n if (value === '') {\n parts.push(name)\n }\n else {\n // Escape quotes in value\n const escaped = value.replace(/\"/g, '"')\n parts.push(`${name}=\"${escaped}\"`)\n }\n }\n\n return parts.length > 0 ? ` ${parts.join(' ')}` : ''\n}\n\n/**\n * Build tag string from name and attributes\n */\nfunction buildTag(tagName: string, attributes: Record<string, string>): string {\n return `<${tagName}${buildAttributes(attributes)}>`\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Check if HTML content is safe (quick check)\n */\nexport function isSafe(html: string, options?: SanitizerOptions | SanitizerPreset): boolean {\n const result = sanitizeWithInfo(html, options)\n return !result.modified\n}\n\n/**\n * Strip all HTML tags\n */\nexport function stripTags(html: string): string {\n return html.replace(/<[^>]*>/g, '')\n}\n\n/**\n * Escape HTML for safe display\n */\nexport function escape(text: string): string {\n return escapeHtml(text)\n}\n",
|
|
7
|
+
"/**\n * @stacksjs/sanitizer\n *\n * A fast, native Bun-powered HTML sanitizer with DOMPurify-like features.\n * Protection against XSS and malicious content.\n */\n\n// Default export\nimport { sanitize } from './sanitizer'\n\nexport * from './presets'\nexport { basic, getPreset, markdown, relaxed, strict } from './presets'\nexport * from './sanitizer'\n\n// Re-export for convenience\nexport { escape, isSafe, sanitize, sanitizeWithInfo, stripTags } from './sanitizer'\nexport * from './types'\n\nexport default sanitize\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "11C850CF49E4930C64756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SanitizerOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Get preset by name
|
|
4
|
+
*/
|
|
5
|
+
export declare function getPreset(name: string): SanitizerOptions;
|
|
6
|
+
/**
|
|
7
|
+
* Strict preset - Only safe, basic formatting
|
|
8
|
+
*/
|
|
9
|
+
export declare const strict: SanitizerOptions;
|
|
10
|
+
/**
|
|
11
|
+
* Basic preset - Common safe HTML elements
|
|
12
|
+
*/
|
|
13
|
+
export declare const basic: SanitizerOptions;
|
|
14
|
+
/**
|
|
15
|
+
* Relaxed preset - More permissive, includes media and interactive elements
|
|
16
|
+
*/
|
|
17
|
+
export declare const relaxed: SanitizerOptions;
|
|
18
|
+
/**
|
|
19
|
+
* Markdown preset - Optimized for markdown-generated HTML
|
|
20
|
+
*/
|
|
21
|
+
export declare const markdown: SanitizerOptions;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SanitizeResult, SanitizerOptions, SanitizerPreset } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Sanitize HTML content
|
|
4
|
+
*/
|
|
5
|
+
export declare function sanitize(html: string, options?: SanitizerOptions | SanitizerPreset): string;
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize HTML content with detailed information
|
|
8
|
+
*/
|
|
9
|
+
export declare function sanitizeWithInfo(html: string, options?: SanitizerOptions | SanitizerPreset): SanitizeResult;
|
|
10
|
+
/**
|
|
11
|
+
* Check if HTML content is safe (quick check)
|
|
12
|
+
*/
|
|
13
|
+
export declare function isSafe(html: string, options?: SanitizerOptions | SanitizerPreset): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Strip all HTML tags
|
|
16
|
+
*/
|
|
17
|
+
export declare function stripTags(html: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Escape HTML for safe display
|
|
20
|
+
*/
|
|
21
|
+
export declare function escape(text: string): string;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizer configuration options
|
|
3
|
+
*/
|
|
4
|
+
export declare interface SanitizerOptions {
|
|
5
|
+
allowedTags?: string[]
|
|
6
|
+
allowedAttributes?: Record<string, string[]> | string[]
|
|
7
|
+
allowedSchemes?: string[]
|
|
8
|
+
allowedStyles?: string[]
|
|
9
|
+
allowDataAttributes?: boolean
|
|
10
|
+
allowAriaAttributes?: boolean
|
|
11
|
+
urlValidator?: (url: string) => boolean
|
|
12
|
+
stripTags?: boolean
|
|
13
|
+
allowComments?: boolean
|
|
14
|
+
transformTag?: (tagName: string, attributes: Record<string, string>) => { tagName: string, attributes: Record<string, string> } | null
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* HTML sanitization result
|
|
18
|
+
*/
|
|
19
|
+
export declare interface SanitizeResult {
|
|
20
|
+
html: string
|
|
21
|
+
modified: boolean
|
|
22
|
+
removedTags?: string[]
|
|
23
|
+
removedAttributes?: string[]
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Preset configurations
|
|
27
|
+
*/
|
|
28
|
+
export type SanitizerPreset = 'strict' | 'basic' | 'relaxed' | 'markdown'
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/sanitizer",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.5",
|
|
5
5
|
"description": "A fast, native Bun-powered HTML sanitizer with DOMPurify-like features. Protection against XSS and malicious content.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "bun --bun build.ts",
|
|
43
|
+
"prepublishOnly": "bun run build",
|
|
43
44
|
"test": "bun test",
|
|
44
45
|
"test:watch": "bun test --watch",
|
|
45
46
|
"test:coverage": "bun test --coverage",
|