@texuli/core 0.1.0-next.1 → 0.1.0-next.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/README.md +14 -9
- package/dist/editor/history.js +6 -0
- package/dist/url/safe-href.js +6 -2
- package/dist/url-policy/shared.d.ts +8 -0
- package/dist/url-policy/shared.js +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# @texuli/core
|
|
2
2
|
|
|
3
3
|
**Layer L0: texels.** Framework-free TypeScript behavior and policy that every
|
|
4
|
-
Texuli component is built from. No Svelte, no runtime dependencies.
|
|
5
|
-
|
|
4
|
+
Texuli component is built from. No Svelte, no runtime dependencies.
|
|
5
|
+
Published to npm as `0.1.0-next.2` under the `next`
|
|
6
|
+
dist-tag (by CI, with provenance); no stable release (`latest` is reserved
|
|
7
|
+
for 1.0.0), so install it with `@next`. The workspace package stays
|
|
8
|
+
`"private": true`.
|
|
6
9
|
|
|
7
10
|
## What exists today
|
|
8
11
|
|
|
@@ -38,12 +41,12 @@ rest.
|
|
|
38
41
|
| URL policy (links) | `safeHref`, `type HrefTrust` | [core-url-policy](docs/core-url-policy.md) |
|
|
39
42
|
| URL policy (media) | `safeImageSrc`, `safeImageSrcset`, `safeMediaSrc`, `MAX_DATA_IMAGE_BYTES` | [core-url-policy](docs/core-url-policy.md) |
|
|
40
43
|
|
|
41
|
-
The editor texels are not in the root barrel: the barrel measures 22.
|
|
42
|
-
its 24 KiB budget. `pnpm size
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
KiB) and `editor-transforms` (10.36 KiB) are over
|
|
46
|
-
|
|
44
|
+
The editor texels are not in the root barrel: the barrel measures 22.64 of
|
|
45
|
+
its 24 KiB budget. `pnpm size` (2026-10-26) measures `editor-history` at
|
|
46
|
+
773 B and `editor-keymap` at 2.49 KiB, both within the 4 KiB per-texel
|
|
47
|
+
default. `editor-markdown` (7.05 of 7.50 KiB), `editor-model` (7.83 of
|
|
48
|
+
8.50 KiB) and `editor-transforms` (10.36 of 11 KiB) are over that default
|
|
49
|
+
and carry their own budget rows; every entry is within budget.
|
|
47
50
|
|
|
48
51
|
The tables above name the headline exports per texel, not the full surface:
|
|
49
52
|
read `src/index.ts` (and the per-area `index.ts`) for every exported symbol
|
|
@@ -86,7 +89,9 @@ isTabbable(stops[0]!); // true
|
|
|
86
89
|
- **`untrusted`** (default, for agent or user content): only `https:`,
|
|
87
90
|
`mailto:` and `#fragment`.
|
|
88
91
|
- **`developer`** (for app-authored props): anything except `javascript:`,
|
|
89
|
-
`vbscript:`, `data:` and
|
|
92
|
+
`vbscript:`, `data:` and any value opening with two slashes or
|
|
93
|
+
backslashes in any mix (`//host`, `/\host`, `\/host`, `\\host`) or with
|
|
94
|
+
any leading backslash, which browsers resolve off-origin.
|
|
90
95
|
- Whitespace and ASCII control characters are stripped before the scheme is
|
|
91
96
|
read, so `java\tscript:` is caught.
|
|
92
97
|
|
package/dist/editor/history.js
CHANGED
|
@@ -18,6 +18,12 @@ export function editorHistoryReducer(history, action) {
|
|
|
18
18
|
if (!action.result.ok)
|
|
19
19
|
return { ...history, errors: action.result.errors };
|
|
20
20
|
const state = action.result.state;
|
|
21
|
+
// A change that leaves the document as it was (a caret mark toggle) is
|
|
22
|
+
// no undo step, but like a selection change it ends the running typing
|
|
23
|
+
// step: undo after "a **b**" stops at the format change, not at "".
|
|
24
|
+
if (state.doc === history.present.doc) {
|
|
25
|
+
return { ...history, present: state, errors: [], last: null };
|
|
26
|
+
}
|
|
21
27
|
const kind = action.kind ?? 'other';
|
|
22
28
|
const at = action.at ?? 0;
|
|
23
29
|
const block = focusBlock(state);
|
package/dist/url/safe-href.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Two trust tiers:
|
|
5
5
|
* - `developer`: props written by the app's own developers. Every scheme is
|
|
6
6
|
* allowed except the ones that execute or smuggle content: `javascript:`,
|
|
7
|
-
* `vbscript:`, `data:` and protocol-relative
|
|
7
|
+
* `vbscript:`, `data:` and protocol-relative URLs: any value opening with
|
|
8
|
+
* two of `/` and `\` (`//host`, `/\host`, `\/host`, `\\host`) or with a
|
|
9
|
+
* backslash.
|
|
8
10
|
* - `untrusted`: content an agent or a user produced. Only `https:`,
|
|
9
11
|
* `mailto:` and same-document `#fragment` links pass, and additionally:
|
|
10
12
|
* - no invisible formatting characters anywhere (zero-width, bidi
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
* `untrusted` (`resolveTrust`). A rejected value returns `undefined`, so the
|
|
20
22
|
* caller renders inert text.
|
|
21
23
|
*/
|
|
24
|
+
import { hasAuthorityPrefix } from "../url-policy/shared.js";
|
|
22
25
|
import { resolveTrust } from "../url-policy/trust.js";
|
|
23
26
|
const BLOCKED_SCHEMES = new Set(['javascript:', 'vbscript:', 'data:']);
|
|
24
27
|
const UNTRUSTED_SCHEMES = new Set(['https:', 'mailto:']);
|
|
@@ -67,7 +70,8 @@ export function safeHref(value, requested = 'untrusted') {
|
|
|
67
70
|
return undefined;
|
|
68
71
|
if (href.startsWith('#'))
|
|
69
72
|
return href;
|
|
70
|
-
|
|
73
|
+
// `//`, `/\`, `\/` and `\\` all open another origin; so does any leading `\`.
|
|
74
|
+
if (hasAuthorityPrefix(href))
|
|
71
75
|
return undefined;
|
|
72
76
|
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href)?.[1]?.toLowerCase();
|
|
73
77
|
if (trust === 'untrusted') {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export declare function cleanSource(value: unknown): string | undefined;
|
|
2
|
+
/**
|
|
3
|
+
* Whether a URL opens with a backslash or with `//` or `/\`. Browsers read
|
|
4
|
+
* `\` as `/` in http(s) URLs, so `//host`, `/\host`, `\/host` and `\\host`
|
|
5
|
+
* are all protocol-relative and leave the page's origin (`/\evil.example`
|
|
6
|
+
* resolves to `https://evil.example/`). Callers pass the value after
|
|
7
|
+
* trimming and stripping control characters.
|
|
8
|
+
*/
|
|
9
|
+
export declare function hasAuthorityPrefix(value: string): boolean;
|
|
2
10
|
export declare function sourceScheme(value: string): string | undefined;
|
|
3
11
|
export declare function isBlockedSource(value: string): boolean;
|
|
4
12
|
export declare function isAllowedRelativeOrWebSource(value: string): boolean;
|
|
@@ -11,14 +11,22 @@ export function cleanSource(value) {
|
|
|
11
11
|
}
|
|
12
12
|
return source;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether a URL opens with a backslash or with `//` or `/\`. Browsers read
|
|
16
|
+
* `\` as `/` in http(s) URLs, so `//host`, `/\host`, `\/host` and `\\host`
|
|
17
|
+
* are all protocol-relative and leave the page's origin (`/\evil.example`
|
|
18
|
+
* resolves to `https://evil.example/`). Callers pass the value after
|
|
19
|
+
* trimming and stripping control characters.
|
|
20
|
+
*/
|
|
21
|
+
export function hasAuthorityPrefix(value) {
|
|
22
|
+
return /^(\\|\/[/\\])/.test(value);
|
|
23
|
+
}
|
|
14
24
|
export function sourceScheme(value) {
|
|
15
25
|
return /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
16
26
|
}
|
|
17
27
|
export function isBlockedSource(value) {
|
|
18
28
|
const scheme = sourceScheme(value);
|
|
19
|
-
return (
|
|
20
|
-
value.startsWith('//') ||
|
|
21
|
-
value.startsWith('\\'));
|
|
29
|
+
return (scheme !== undefined && BLOCKED_SCHEMES.has(`${scheme}:`)) || hasAuthorityPrefix(value);
|
|
22
30
|
}
|
|
23
31
|
export function isAllowedRelativeOrWebSource(value) {
|
|
24
32
|
const scheme = sourceScheme(value);
|