@uniweb/kit 1.1.0 → 1.2.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"shiki": "^3.0.0",
|
|
44
44
|
"tailwind-merge": "^3.6.0",
|
|
45
45
|
"temml": "^0.13.2",
|
|
46
|
-
"@uniweb/core": "~1.
|
|
47
|
-
"@uniweb/projections": "~1.1.
|
|
48
|
-
"@uniweb/semantic-parser": "~1.
|
|
46
|
+
"@uniweb/core": "~1.2.0",
|
|
47
|
+
"@uniweb/projections": "~1.1.1",
|
|
48
|
+
"@uniweb/semantic-parser": "~1.6.0",
|
|
49
49
|
"@uniweb/scene": "~1.0.1",
|
|
50
|
-
"@uniweb/schemas": "~1.0
|
|
50
|
+
"@uniweb/schemas": "~1.1.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^19.0.0",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import React, { useState, useEffect, useMemo } from 'react'
|
|
14
14
|
import { getUniweb } from '@uniweb/core'
|
|
15
|
+
import { sanitizeSVG } from '@uniweb/semantic-parser/sanitize'
|
|
15
16
|
import { cn, parseIconRef } from '../../utils/index.js'
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -93,9 +94,13 @@ function parseSvg(svgContent) {
|
|
|
93
94
|
const strokeLinecap = getAttr(svgTag, 'stroke-linecap')
|
|
94
95
|
const strokeLinejoin = getAttr(svgTag, 'stroke-linejoin')
|
|
95
96
|
|
|
96
|
-
// Get inner content (everything between <svg> and </svg>)
|
|
97
|
+
// Get inner content (everything between <svg> and </svg>), rebuilt from
|
|
98
|
+
// the SVG allowlist before it is injected: an icon can come from content
|
|
99
|
+
// (an author's file, a pasted SVG) or from any URL, and markup from there
|
|
100
|
+
// may not run script. Built-in icons are constants and skip this.
|
|
101
|
+
// ⛔ Until 2026-10-14 the inner markup was injected as fetched.
|
|
97
102
|
const innerMatch = svgContent.match(/<svg\s[^>]*>([\s\S]*)<\/svg>/i)
|
|
98
|
-
const content = innerMatch ? innerMatch[1] : ''
|
|
103
|
+
const content = innerMatch ? sanitizeSVG(innerMatch[1]) : ''
|
|
99
104
|
|
|
100
105
|
return {
|
|
101
106
|
viewBox, content, width, height,
|
|
@@ -9,14 +9,20 @@
|
|
|
9
9
|
* The resolution itself lives in utils/href.js, shared with <Text>, so both
|
|
10
10
|
* prose renderers agree on what an authored href means.
|
|
11
11
|
*
|
|
12
|
-
* WHAT
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* WHAT MAKES IT SAFE — and what does not
|
|
13
|
+
* The value is rendered as HTML, as given; this component cleans nothing. A
|
|
14
|
+
* semantic-parser string is safe to give it because the parser builds it from
|
|
15
|
+
* structure and escapes every character of the text and every attribute value,
|
|
16
|
+
* and the only markup it lets through, a math node's MathML, it rebuilds from
|
|
17
|
+
* an allowlist (`@uniweb/semantic-parser`, since 2026-10-14).
|
|
18
|
+
*
|
|
19
|
+
* ⛔ So a string from anywhere else does not belong here — a field a member
|
|
20
|
+
* wrote, a response from an API. Render it as text. *(This said until
|
|
21
|
+
* 2026-10-14 that "nothing in the framework" sanitizes and that a site's
|
|
22
|
+
* author also writes its foundation, so there was no privilege boundary — a
|
|
23
|
+
* premise that fails wherever the person writing content is not the one
|
|
24
|
+
* writing code: an author using an editor, a member of a site with a
|
|
25
|
+
* backend.)*
|
|
20
26
|
*
|
|
21
27
|
* @module @uniweb/kit/SafeHtml
|
|
22
28
|
*/
|
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
* Handles single strings or arrays of paragraphs with automatic semantic
|
|
6
6
|
* tag selection and empty content filtering.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
8
|
+
* What it renders as HTML (`html`, true by default): a semantic-parser string.
|
|
9
|
+
* The parser builds it from structure and escapes every character of the text
|
|
10
|
+
* and every attribute value, so a `content.title` or a paragraph is safe to
|
|
11
|
+
* inject (`@uniweb/semantic-parser`, since 2026-10-14). Text renders it as
|
|
12
|
+
* given and adds no cleaning of its own.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ A string from anywhere else is not parser output: a field a member wrote,
|
|
15
|
+
* a value an API returned. Render it as text — `html={false}` (`<PlainText>`),
|
|
16
|
+
* or `{value}` in JSX — or a member's `<img onerror>` runs as the viewer.
|
|
12
17
|
*
|
|
13
18
|
* @module @uniweb/kit/Text
|
|
14
19
|
*/
|
|
@@ -360,8 +360,9 @@ export function SequenceElement({ element, block, components }) {
|
|
|
360
360
|
|
|
361
361
|
case 'math': {
|
|
362
362
|
// MathML is pre-compiled at parse time; the browser renders it natively,
|
|
363
|
-
// so no runtime math library ships.
|
|
364
|
-
//
|
|
363
|
+
// so no runtime math library ships. semantic-parser rebuilds it from the
|
|
364
|
+
// MathML allowlist before it gets here. Missing here until 2026-07-30,
|
|
365
|
+
// which is why math vanished from every <Article>.
|
|
365
366
|
if (!element.mathml) return null
|
|
366
367
|
const display = element.display !== false
|
|
367
368
|
const Tag = display ? 'div' : 'span'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Scene as SceneRenderer, composeScene } from '@uniweb/scene'
|
|
2
|
+
import { sanitizeSVG } from '@uniweb/semantic-parser/sanitize'
|
|
2
3
|
|
|
3
4
|
// The @uniweb/scene renderer reads `--vc-*` custom properties for its default
|
|
4
5
|
// colors (an explicit value in the scene JSON always wins). Text, font, and
|
|
@@ -22,14 +23,20 @@ const THEME_VARS = {
|
|
|
22
23
|
* Maps the site's `--primary` / `--accent` tokens onto the renderer's decorative
|
|
23
24
|
* defaults so an unstyled scene adopts the theme; text and fonts inherit the
|
|
24
25
|
* section context automatically. All other props (`mobileBreakpoint`,
|
|
25
|
-
* `initialWidth`, `
|
|
26
|
-
*
|
|
26
|
+
* `initialWidth`, `className`, `style`, and the editing hooks) forward straight
|
|
27
|
+
* through.
|
|
28
|
+
*
|
|
29
|
+
* An svg layer's markup is injected, so it is rebuilt from the SVG allowlist
|
|
30
|
+
* first (`sanitizeSVG`, `@uniweb/semantic-parser/sanitize`) — a scene is content,
|
|
31
|
+
* and whoever composed it may not be the person who wrote the foundation. Pass
|
|
32
|
+
* your own `sanitizeSvg` to replace it. ⛔ Until 2026-10-14 nothing was passed by
|
|
33
|
+
* default, so a layer's `<script>` or `onload` ran on the page.
|
|
27
34
|
*
|
|
28
35
|
* @example
|
|
29
36
|
* <Scene composition={content.data.scene} />
|
|
30
37
|
* <Scene composition={template} overrides={{ title: { content: content.title } }} />
|
|
31
38
|
*/
|
|
32
|
-
export function Scene({ composition, overrides, content, style, ...rest }) {
|
|
39
|
+
export function Scene({ composition, overrides, content, style, sanitizeSvg = sanitizeSVG, ...rest }) {
|
|
33
40
|
const data =
|
|
34
41
|
overrides || content
|
|
35
42
|
? composeScene(composition, { overrides, content })
|
|
@@ -39,6 +46,7 @@ export function Scene({ composition, overrides, content, style, ...rest }) {
|
|
|
39
46
|
<SceneRenderer
|
|
40
47
|
composition={data}
|
|
41
48
|
style={{ ...THEME_VARS, ...style }}
|
|
49
|
+
sanitizeSvg={sanitizeSvg}
|
|
42
50
|
{...rest}
|
|
43
51
|
/>
|
|
44
52
|
)
|
package/src/utils/index.js
CHANGED
|
@@ -146,8 +146,15 @@ export function cn(...inputs) {
|
|
|
146
146
|
return twMerge(clsx(inputs))
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
const REFERENCES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: '\u00a0' }
|
|
150
|
+
|
|
149
151
|
/**
|
|
150
|
-
* Strip HTML tags from a string
|
|
152
|
+
* Strip HTML tags from a string — the plain text of a parser string.
|
|
153
|
+
*
|
|
154
|
+
* semantic-parser escapes the text it puts into its strings (`a<b` arrives as
|
|
155
|
+
* `a<b`), so the plain text decodes the references as well as dropping the
|
|
156
|
+
* tags, in the browser and on the server alike.
|
|
157
|
+
*
|
|
151
158
|
* @param {string} html - HTML string
|
|
152
159
|
* @returns {string} Plain text
|
|
153
160
|
*/
|
|
@@ -160,8 +167,15 @@ export function stripTags(html) {
|
|
|
160
167
|
return doc.body.textContent || ''
|
|
161
168
|
}
|
|
162
169
|
|
|
163
|
-
// Fallback:
|
|
164
|
-
|
|
170
|
+
// Fallback (no DOM — the server): drop the tags, then decode the references
|
|
171
|
+
// a serializer writes. ⛔ Until 2026-10-14 it only dropped the tags.
|
|
172
|
+
return html
|
|
173
|
+
.replace(/<[^>]*>/g, '')
|
|
174
|
+
.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (match, ref) => {
|
|
175
|
+
if (ref[0] !== '#') return REFERENCES[ref] ?? match
|
|
176
|
+
const code = ref[1] === 'x' || ref[1] === 'X' ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10)
|
|
177
|
+
return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : match
|
|
178
|
+
})
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
// URL predicates live in url.js so href.js can use them without importing
|