@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 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. Private
5
- workspace package (`"private": true`), not published.
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.63 of
42
- its 24 KiB budget. `pnpm size --filter core/editor` (2026-10-25) measures
43
- `editor-history` at 754 B and `editor-keymap` at 2.49 KiB, both within the 4
44
- KiB per-texel default. `editor-markdown` (7.03 KiB), `editor-model` (7.81
45
- KiB) and `editor-transforms` (10.36 KiB) are over it, so their budget rows
46
- are pending with the owner.
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 protocol-relative `//host` URLs.
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
 
@@ -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);
@@ -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 `//host` URLs.
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
- if (href.startsWith('//') || href.startsWith('\\'))
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 ((scheme !== undefined && BLOCKED_SCHEMES.has(`${scheme}:`)) ||
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@texuli/core",
3
- "version": "0.1.0-next.1",
3
+ "version": "0.1.0-next.2",
4
4
  "description": "Texels: framework-free behavior and policy that every Texuli component is built from.",
5
5
  "keywords": [
6
6
  "svelte",