@markii/ansi 0.0.1
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/dist/ansi.d.ts +79 -0
- package/dist/ansi.js +136 -0
- package/dist/box.d.ts +47 -0
- package/dist/box.js +209 -0
- package/dist/components/badge.d.ts +10 -0
- package/dist/components/badge.js +44 -0
- package/dist/components/callout.d.ts +20 -0
- package/dist/components/callout.js +71 -0
- package/dist/components/card.d.ts +12 -0
- package/dist/components/card.js +44 -0
- package/dist/components/cell.d.ts +18 -0
- package/dist/components/cell.js +31 -0
- package/dist/components/chart.d.ts +15 -0
- package/dist/components/chart.js +129 -0
- package/dist/components/details.d.ts +12 -0
- package/dist/components/details.js +25 -0
- package/dist/components/divider.d.ts +4 -0
- package/dist/components/divider.js +80 -0
- package/dist/components/figure.d.ts +20 -0
- package/dist/components/figure.js +53 -0
- package/dist/components/index.d.ts +34 -0
- package/dist/components/index.js +109 -0
- package/dist/components/kbd.d.ts +7 -0
- package/dist/components/kbd.js +8 -0
- package/dist/components/layout-wrapper.d.ts +35 -0
- package/dist/components/layout-wrapper.js +62 -0
- package/dist/components/progress.d.ts +15 -0
- package/dist/components/progress.js +78 -0
- package/dist/components/rating.d.ts +9 -0
- package/dist/components/rating.js +28 -0
- package/dist/components/row.d.ts +26 -0
- package/dist/components/row.js +67 -0
- package/dist/components/stat.d.ts +13 -0
- package/dist/components/stat.js +79 -0
- package/dist/components/tab.d.ts +13 -0
- package/dist/components/tab.js +17 -0
- package/dist/components/table-grid.d.ts +34 -0
- package/dist/components/table-grid.js +124 -0
- package/dist/components/table.d.ts +16 -0
- package/dist/components/table.js +101 -0
- package/dist/components/tabs.d.ts +18 -0
- package/dist/components/tabs.js +30 -0
- package/dist/failure-presentation.d.ts +58 -0
- package/dist/failure-presentation.js +124 -0
- package/dist/href-resolve.d.ts +21 -0
- package/dist/href-resolve.js +25 -0
- package/dist/image-resolve.d.ts +41 -0
- package/dist/image-resolve.js +28 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/layout.d.ts +73 -0
- package/dist/layout.js +144 -0
- package/dist/measure.d.ts +25 -0
- package/dist/measure.js +108 -0
- package/dist/registry.d.ts +227 -0
- package/dist/registry.js +121 -0
- package/dist/render.d.ts +69 -0
- package/dist/render.js +889 -0
- package/dist/resolve.d.ts +60 -0
- package/dist/resolve.js +152 -0
- package/dist/sanitize.d.ts +51 -0
- package/dist/sanitize.js +101 -0
- package/dist/style.d.ts +9 -0
- package/dist/style.js +13 -0
- package/dist/theme.d.ts +36 -0
- package/dist/theme.js +79 -0
- package/dist/url-resolve.d.ts +54 -0
- package/dist/url-resolve.js +82 -0
- package/dist/value-format.d.ts +13 -0
- package/dist/value-format.js +16 -0
- package/dist/value-types.d.ts +37 -0
- package/dist/value-types.js +17 -0
- package/package.json +61 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AnsiValueStore, AnsiVaultStore, FailureKind, ValueStatus } from './value-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* This engine's port of `@markii/html`'s `resolve.ts` (itself a port of
|
|
4
|
+
* `@markii/react`'s `store-path.ts`): resolves a `data=`/`:value[]` name
|
|
5
|
+
* (optionally dotted, optionally `@`-prefixed for the vault) against a
|
|
6
|
+
* value store. Ported rather than shared for the same reason `@markii/html`
|
|
7
|
+
* ports it rather than importing `@markii/react`: each platform renderer is
|
|
8
|
+
* an independent implementation of the same contract (docs/scripting.md),
|
|
9
|
+
* kept behaviorally identical by test coverage, not by a shared runtime
|
|
10
|
+
* dependency between renderers. The one difference from `@markii/html`'s
|
|
11
|
+
* copy is the store type: `./value-types.js`'s structural `AnsiValueStore`/
|
|
12
|
+
* `AnsiVaultStore` instead of `@markii/runtime`'s concrete types, so this
|
|
13
|
+
* package never needs `@markii/runtime` at runtime (see `value-types.ts`).
|
|
14
|
+
* The resolution semantics are otherwise unchanged, byte for byte.
|
|
15
|
+
*/
|
|
16
|
+
/** What resolving a (possibly dotted) name against a store produces. */
|
|
17
|
+
export interface StorePathResolution {
|
|
18
|
+
value: unknown;
|
|
19
|
+
status: ValueStatus;
|
|
20
|
+
/** The root entry's error message, if it has one — carried through regardless of how the rest of the path resolved. Always a `string` when present. */
|
|
21
|
+
error?: string;
|
|
22
|
+
/** The root entry's `failureKind`, carried through exactly like `error`. Absent when the root entry didn't carry one, or on a host-store fault (never invented). */
|
|
23
|
+
failureKind?: FailureKind;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Best-effort description of something host code threw, for the tooltip
|
|
27
|
+
* channel. Every step is itself guarded: `instanceof`, `.message`, and
|
|
28
|
+
* `String(...)` can all throw when the thrown value is a revoked `Proxy` or
|
|
29
|
+
* an object with hostile traps/getters.
|
|
30
|
+
*/
|
|
31
|
+
export declare function describeHostFault(err: unknown): string;
|
|
32
|
+
/** The one-character prefix that routes a `data=`/`:value[]` name at the vault store instead of the note store (docs/scripting.md). */
|
|
33
|
+
export declare const VAULT_NAME_PREFIX = "@";
|
|
34
|
+
/** Where a `resolveScopedPath` lookup may read from. Either half may be absent. */
|
|
35
|
+
export interface ValueScope {
|
|
36
|
+
store?: AnsiValueStore;
|
|
37
|
+
vault?: AnsiVaultStore;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolves a `data=`/`:value[]` name against `store`, walking a dotted path.
|
|
41
|
+
* Never throws, including against a hostile or buggy host store.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveStorePath(store: AnsiValueStore | undefined, dottedName: string): StorePathResolution;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves a `data=`/`:value[]` name against a `ValueScope`, routing an
|
|
46
|
+
* `@`-prefixed name at `scope.vault` instead of `scope.store` ("bare name =
|
|
47
|
+
* mine, `@name` = the vault's"). Never throws.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveScopedPath(scope: ValueScope, dottedName: string): StorePathResolution;
|
|
50
|
+
/** What `safeRead` produces: the extracted fields, plus the thrown message when the extraction had to be abandoned. */
|
|
51
|
+
export interface SafeRead<T> {
|
|
52
|
+
fields: T;
|
|
53
|
+
fault?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Runs `read` — an extraction that touches an untrusted bound `data` value —
|
|
57
|
+
* and falls back to `fallback()` if any part of it throws. Ported from
|
|
58
|
+
* `@markii/react`'s `safe-data.ts` by way of `@markii/html`'s `resolve.ts`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function safeRead<T>(read: () => T, fallback: () => T): SafeRead<T>;
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const MISSING = { value: undefined, status: 'missing' };
|
|
2
|
+
/** `ValueStatus`'s members, for validating a status read back off an untrusted entry. */
|
|
3
|
+
const VALUE_STATUSES = [
|
|
4
|
+
'fresh',
|
|
5
|
+
'stale',
|
|
6
|
+
'error',
|
|
7
|
+
'missing',
|
|
8
|
+
];
|
|
9
|
+
function isValueStatus(value) {
|
|
10
|
+
return (typeof value === 'string' &&
|
|
11
|
+
VALUE_STATUSES.includes(value));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The message a host-data fault falls back to when even describing the
|
|
15
|
+
* thrown value throws. Deliberately generic: it only ever reaches a tooltip.
|
|
16
|
+
*/
|
|
17
|
+
const HOST_FAULT_MESSAGE = 'value store threw while reading this name';
|
|
18
|
+
/**
|
|
19
|
+
* Best-effort description of something host code threw, for the tooltip
|
|
20
|
+
* channel. Every step is itself guarded: `instanceof`, `.message`, and
|
|
21
|
+
* `String(...)` can all throw when the thrown value is a revoked `Proxy` or
|
|
22
|
+
* an object with hostile traps/getters.
|
|
23
|
+
*/
|
|
24
|
+
export function describeHostFault(err) {
|
|
25
|
+
try {
|
|
26
|
+
if (err instanceof Error) {
|
|
27
|
+
const { message } = err;
|
|
28
|
+
if (typeof message === 'string' && message !== '')
|
|
29
|
+
return message;
|
|
30
|
+
}
|
|
31
|
+
const text = String(err);
|
|
32
|
+
return text === '' ? HOST_FAULT_MESSAGE : text;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return HOST_FAULT_MESSAGE;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The degraded resolution for a HOST-STORE FAULT: resolves to `'missing'`, carrying the thrown message as `error`, never inventing a `failureKind`. */
|
|
39
|
+
function hostFault(err) {
|
|
40
|
+
return { value: undefined, status: 'missing', error: describeHostFault(err) };
|
|
41
|
+
}
|
|
42
|
+
/** The one-character prefix that routes a `data=`/`:value[]` name at the vault store instead of the note store (docs/scripting.md). */
|
|
43
|
+
export const VAULT_NAME_PREFIX = '@';
|
|
44
|
+
/** Reads the four `StoredValue` fields off an entry a host store handed back. MAY THROW; every caller wraps it. */
|
|
45
|
+
function readEntry(entry) {
|
|
46
|
+
const value = entry.value;
|
|
47
|
+
const status = entry.status;
|
|
48
|
+
const error = entry.error;
|
|
49
|
+
const failureKind = entry.failureKind;
|
|
50
|
+
return {
|
|
51
|
+
value,
|
|
52
|
+
status: isValueStatus(status) ? status : 'missing',
|
|
53
|
+
error: typeof error === 'string' ? error : undefined,
|
|
54
|
+
failureKind: typeof failureKind === 'string'
|
|
55
|
+
? failureKind
|
|
56
|
+
: undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The partial-path degradation: `missing`, but still carrying the root entry's own error/kind. */
|
|
60
|
+
function partialMiss(entry) {
|
|
61
|
+
return {
|
|
62
|
+
value: undefined,
|
|
63
|
+
status: 'missing',
|
|
64
|
+
error: entry.error,
|
|
65
|
+
failureKind: entry.failureKind,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Walks `segments[1:]` into `entry.value`. Never throws: a hostile stored
|
|
70
|
+
* value (a revoked `Proxy`, throwing traps) degrades to `hostFault` rather
|
|
71
|
+
* than escaping. `Object.hasOwn` is the load-bearing guard against a segment
|
|
72
|
+
* like `__proto__`/`constructor` resolving through the prototype chain.
|
|
73
|
+
*/
|
|
74
|
+
function walkSegments(entry, segments) {
|
|
75
|
+
let current = entry.value;
|
|
76
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
77
|
+
const segment = segments[index];
|
|
78
|
+
if (!segment || current === null || typeof current !== 'object') {
|
|
79
|
+
return partialMiss(entry);
|
|
80
|
+
}
|
|
81
|
+
let next;
|
|
82
|
+
try {
|
|
83
|
+
if (!Object.hasOwn(current, segment))
|
|
84
|
+
return partialMiss(entry);
|
|
85
|
+
next = current[segment];
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return hostFault(err);
|
|
89
|
+
}
|
|
90
|
+
current = next;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
value: current,
|
|
94
|
+
status: entry.status,
|
|
95
|
+
error: entry.error,
|
|
96
|
+
failureKind: entry.failureKind,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** The shared never-throw shell around a store lookup. */
|
|
100
|
+
function resolveEntryPath(lookup, segments) {
|
|
101
|
+
let entry;
|
|
102
|
+
try {
|
|
103
|
+
const raw = lookup();
|
|
104
|
+
if (!raw)
|
|
105
|
+
return MISSING;
|
|
106
|
+
entry = readEntry(raw);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return hostFault(err);
|
|
110
|
+
}
|
|
111
|
+
return walkSegments(entry, segments);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolves a `data=`/`:value[]` name against `store`, walking a dotted path.
|
|
115
|
+
* Never throws, including against a hostile or buggy host store.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveStorePath(store, dottedName) {
|
|
118
|
+
const segments = dottedName.split('.');
|
|
119
|
+
const root = segments[0];
|
|
120
|
+
if (!root)
|
|
121
|
+
return MISSING;
|
|
122
|
+
return resolveEntryPath(() => store?.get(root), segments);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolves a `data=`/`:value[]` name against a `ValueScope`, routing an
|
|
126
|
+
* `@`-prefixed name at `scope.vault` instead of `scope.store` ("bare name =
|
|
127
|
+
* mine, `@name` = the vault's"). Never throws.
|
|
128
|
+
*/
|
|
129
|
+
export function resolveScopedPath(scope, dottedName) {
|
|
130
|
+
if (dottedName.startsWith(VAULT_NAME_PREFIX)) {
|
|
131
|
+
const remainder = dottedName.slice(VAULT_NAME_PREFIX.length);
|
|
132
|
+
const segments = remainder.split('.');
|
|
133
|
+
const root = segments[0];
|
|
134
|
+
if (!root)
|
|
135
|
+
return MISSING;
|
|
136
|
+
return resolveEntryPath(() => scope.vault?.get(root), segments);
|
|
137
|
+
}
|
|
138
|
+
return resolveStorePath(scope.store, dottedName);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Runs `read` — an extraction that touches an untrusted bound `data` value —
|
|
142
|
+
* and falls back to `fallback()` if any part of it throws. Ported from
|
|
143
|
+
* `@markii/react`'s `safe-data.ts` by way of `@markii/html`'s `resolve.ts`.
|
|
144
|
+
*/
|
|
145
|
+
export function safeRead(read, fallback) {
|
|
146
|
+
try {
|
|
147
|
+
return { fields: read() };
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
return { fields: fallback(), fault: describeHostFault(err) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place control characters are removed from author text before it
|
|
3
|
+
* reaches this engine's output (security critical). A Markii note is
|
|
4
|
+
* untrusted text: without this module, a note containing a raw ESC byte
|
|
5
|
+
* could move the terminal's cursor, recolor everything after it, or smuggle
|
|
6
|
+
* an OSC 8 hyperlink into the reader's terminal, all through nothing more
|
|
7
|
+
* exotic than a code fence or a link's visible text. The only escape
|
|
8
|
+
* sequences this engine's output ever carries are the ones `./ansi.ts`
|
|
9
|
+
* itself generates.
|
|
10
|
+
*
|
|
11
|
+
* Every piece of text that reaches the rendered output must pass through
|
|
12
|
+
* one of the three functions below before it is printed: an attribute
|
|
13
|
+
* value a component prints, a resolved stored value, a URL, alt text, code,
|
|
14
|
+
* and a code fence's language label all qualify. There is no fourth path.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Sanitizes INLINE author text (a paragraph, a heading, link text, alt
|
|
18
|
+
* text, an attribute value a component prints): every C0 control (ESC
|
|
19
|
+
* included) and DEL, and every C1 control, is removed. A tab, carriage
|
|
20
|
+
* return, or line feed collapses to a single space instead of vanishing
|
|
21
|
+
* outright, since those three are the ones a reader would otherwise
|
|
22
|
+
* perceive as "the words ran together" rather than "a character was
|
|
23
|
+
* stripped". Everything else passes through unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stripControlCharacters(value: string): string;
|
|
26
|
+
/** The fixed number of spaces one tab expands to inside a preformatted block. Matches the common terminal default. */
|
|
27
|
+
export declare const BLOCK_TAB_WIDTH = 4;
|
|
28
|
+
/**
|
|
29
|
+
* Sanitizes PREFORMATTED text (a code fence's body): CRLF and a lone CR
|
|
30
|
+
* both normalize to LF so a block's line breaks behave consistently
|
|
31
|
+
* regardless of the author's line-ending style, LF itself is kept (a code
|
|
32
|
+
* block is allowed to span lines), and a tab expands to `BLOCK_TAB_WIDTH`
|
|
33
|
+
* spaces so `./measure.ts`'s width accounting never has to special-case a
|
|
34
|
+
* tab stop. Every other C0 control (ESC included), DEL, and C1 control is
|
|
35
|
+
* dropped outright: unlike inline text, a code block has no "reads as a
|
|
36
|
+
* word boundary" fallback worth preserving for a stray control byte.
|
|
37
|
+
*/
|
|
38
|
+
export declare function sanitizeBlockText(value: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Sanitizes a URL immediately before it is embedded in an OSC 8 hyperlink
|
|
41
|
+
* sequence (`./ansi.ts`'s `hyperlink`). OSC 8's own terminator is BEL
|
|
42
|
+
* (U+0007) or ST, so a BEL or ESC byte smuggled into the URL text would
|
|
43
|
+
* prematurely close the engine's own escape sequence and let whatever
|
|
44
|
+
* follows be interpreted as raw terminal input; every C0 control, DEL, and
|
|
45
|
+
* C1 control is removed for that reason. A space is dropped too: OSC 8's
|
|
46
|
+
* URL parameter is not a place a real URL would ever legitimately contain
|
|
47
|
+
* one, and stripping it removes one more way to pad a payload without
|
|
48
|
+
* having to reason about whether some particular terminal treats a literal
|
|
49
|
+
* space specially inside the sequence.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sanitizeUrlText(value: string): string;
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place control characters are removed from author text before it
|
|
3
|
+
* reaches this engine's output (security critical). A Markii note is
|
|
4
|
+
* untrusted text: without this module, a note containing a raw ESC byte
|
|
5
|
+
* could move the terminal's cursor, recolor everything after it, or smuggle
|
|
6
|
+
* an OSC 8 hyperlink into the reader's terminal, all through nothing more
|
|
7
|
+
* exotic than a code fence or a link's visible text. The only escape
|
|
8
|
+
* sequences this engine's output ever carries are the ones `./ansi.ts`
|
|
9
|
+
* itself generates.
|
|
10
|
+
*
|
|
11
|
+
* Every piece of text that reaches the rendered output must pass through
|
|
12
|
+
* one of the three functions below before it is printed: an attribute
|
|
13
|
+
* value a component prints, a resolved stored value, a URL, alt text, code,
|
|
14
|
+
* and a code fence's language label all qualify. There is no fourth path.
|
|
15
|
+
*/
|
|
16
|
+
/** Every C0 control code point (U+0000 to U+001F) plus DEL (U+007F). */
|
|
17
|
+
function isC0OrDel(code) {
|
|
18
|
+
return (code >= 0x00 && code <= 0x1f) || code === 0x7f;
|
|
19
|
+
}
|
|
20
|
+
/** Every C1 control code point (U+0080 to U+009F), including the C1 CSI (U+009B). */
|
|
21
|
+
function isC1(code) {
|
|
22
|
+
return code >= 0x80 && code <= 0x9f;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sanitizes INLINE author text (a paragraph, a heading, link text, alt
|
|
26
|
+
* text, an attribute value a component prints): every C0 control (ESC
|
|
27
|
+
* included) and DEL, and every C1 control, is removed. A tab, carriage
|
|
28
|
+
* return, or line feed collapses to a single space instead of vanishing
|
|
29
|
+
* outright, since those three are the ones a reader would otherwise
|
|
30
|
+
* perceive as "the words ran together" rather than "a character was
|
|
31
|
+
* stripped". Everything else passes through unchanged.
|
|
32
|
+
*/
|
|
33
|
+
export function stripControlCharacters(value) {
|
|
34
|
+
let result = '';
|
|
35
|
+
for (const char of value) {
|
|
36
|
+
const code = char.codePointAt(0) ?? 0;
|
|
37
|
+
if (code === 0x09 || code === 0x0a || code === 0x0d) {
|
|
38
|
+
result += ' ';
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (isC0OrDel(code) || isC1(code))
|
|
42
|
+
continue;
|
|
43
|
+
result += char;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
/** The fixed number of spaces one tab expands to inside a preformatted block. Matches the common terminal default. */
|
|
48
|
+
export const BLOCK_TAB_WIDTH = 4;
|
|
49
|
+
/**
|
|
50
|
+
* Sanitizes PREFORMATTED text (a code fence's body): CRLF and a lone CR
|
|
51
|
+
* both normalize to LF so a block's line breaks behave consistently
|
|
52
|
+
* regardless of the author's line-ending style, LF itself is kept (a code
|
|
53
|
+
* block is allowed to span lines), and a tab expands to `BLOCK_TAB_WIDTH`
|
|
54
|
+
* spaces so `./measure.ts`'s width accounting never has to special-case a
|
|
55
|
+
* tab stop. Every other C0 control (ESC included), DEL, and C1 control is
|
|
56
|
+
* dropped outright: unlike inline text, a code block has no "reads as a
|
|
57
|
+
* word boundary" fallback worth preserving for a stray control byte.
|
|
58
|
+
*/
|
|
59
|
+
export function sanitizeBlockText(value) {
|
|
60
|
+
const normalized = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
61
|
+
let result = '';
|
|
62
|
+
for (const char of normalized) {
|
|
63
|
+
const code = char.codePointAt(0) ?? 0;
|
|
64
|
+
if (code === 0x0a) {
|
|
65
|
+
result += '\n';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (code === 0x09) {
|
|
69
|
+
result += ' '.repeat(BLOCK_TAB_WIDTH);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (isC0OrDel(code) || isC1(code))
|
|
73
|
+
continue;
|
|
74
|
+
result += char;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sanitizes a URL immediately before it is embedded in an OSC 8 hyperlink
|
|
80
|
+
* sequence (`./ansi.ts`'s `hyperlink`). OSC 8's own terminator is BEL
|
|
81
|
+
* (U+0007) or ST, so a BEL or ESC byte smuggled into the URL text would
|
|
82
|
+
* prematurely close the engine's own escape sequence and let whatever
|
|
83
|
+
* follows be interpreted as raw terminal input; every C0 control, DEL, and
|
|
84
|
+
* C1 control is removed for that reason. A space is dropped too: OSC 8's
|
|
85
|
+
* URL parameter is not a place a real URL would ever legitimately contain
|
|
86
|
+
* one, and stripping it removes one more way to pad a payload without
|
|
87
|
+
* having to reason about whether some particular terminal treats a literal
|
|
88
|
+
* space specially inside the sequence.
|
|
89
|
+
*/
|
|
90
|
+
export function sanitizeUrlText(value) {
|
|
91
|
+
let result = '';
|
|
92
|
+
for (const char of value) {
|
|
93
|
+
const code = char.codePointAt(0) ?? 0;
|
|
94
|
+
if (code === 0x20)
|
|
95
|
+
continue;
|
|
96
|
+
if (isC0OrDel(code) || isC1(code))
|
|
97
|
+
continue;
|
|
98
|
+
result += char;
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ColorLevel } from './ansi.js';
|
|
2
|
+
import type { AnsiTheme, Tier1Token } from './theme.js';
|
|
3
|
+
/**
|
|
4
|
+
* Applies `theme`'s color for `token` to `text` at `level`. Returns `text`
|
|
5
|
+
* unchanged at `level: 'none'` (no escapes to justify) and for a `null`
|
|
6
|
+
* theme entry (a token this engine does not treat as a color, such as a
|
|
7
|
+
* width preset) — both are "nothing to apply", not an error.
|
|
8
|
+
*/
|
|
9
|
+
export declare function style(text: string, token: Tier1Token, theme: AnsiTheme, level: ColorLevel): string;
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { colorize } from './ansi.js';
|
|
2
|
+
/**
|
|
3
|
+
* Applies `theme`'s color for `token` to `text` at `level`. Returns `text`
|
|
4
|
+
* unchanged at `level: 'none'` (no escapes to justify) and for a `null`
|
|
5
|
+
* theme entry (a token this engine does not treat as a color, such as a
|
|
6
|
+
* width preset) — both are "nothing to apply", not an error.
|
|
7
|
+
*/
|
|
8
|
+
export function style(text, token, theme, level) {
|
|
9
|
+
const color = theme[token];
|
|
10
|
+
if (!color)
|
|
11
|
+
return text;
|
|
12
|
+
return colorize(text, color, level);
|
|
13
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AnsiColor } from './ansi.js';
|
|
2
|
+
/**
|
|
3
|
+
* The terminal engine's theme: one entry per Tier 1 token `doc.css`
|
|
4
|
+
* declares (`packages/platforms/markii-react/src/doc.css`'s `.doc` block,
|
|
5
|
+
* the one that sets `--mk-bg`). Today that is the ten neutrals, the five
|
|
6
|
+
* semantic hues, and the four width presets — nineteen tokens in total.
|
|
7
|
+
*
|
|
8
|
+
* `theme-coverage.test.ts` parses that block the same way
|
|
9
|
+
* `apps/obsidian/src/theme-coverage.test.ts` parses it for the Obsidian
|
|
10
|
+
* theme layer, and fails whenever a Tier 1 token gets added to `doc.css`
|
|
11
|
+
* without a matching entry here: the same drift alarm every host theme
|
|
12
|
+
* layer carries (AGENTS.md's "New Tier 1 token" maintenance rule), applied
|
|
13
|
+
* to a THIRD kind of consumer (a color model, not a stylesheet).
|
|
14
|
+
*/
|
|
15
|
+
export type Tier1Token = '--mk-bg' | '--mk-raised' | '--mk-fg' | '--mk-surface' | '--mk-surface-strong' | '--mk-border' | '--mk-muted' | '--mk-faint' | '--mk-accent' | '--mk-on-accent' | '--mk-info' | '--mk-success' | '--mk-warning' | '--mk-danger' | '--mk-limit' | '--mk-width-fit' | '--mk-width-narrow' | '--mk-width-wide' | '--mk-width-full';
|
|
16
|
+
/**
|
|
17
|
+
* A token maps to a real `AnsiColor`, or to `null` when the token is not a
|
|
18
|
+
* color in a terminal at all. The four width presets are `null` for that
|
|
19
|
+
* reason: `--mk-width-narrow` etc. are CSS lengths in `doc.css`, and this
|
|
20
|
+
* engine consumes their equivalent as column arithmetic instead
|
|
21
|
+
* (`./layout.ts`'s `applyLayout`), not as a printable color.
|
|
22
|
+
*/
|
|
23
|
+
export type AnsiTheme = Readonly<Record<Tier1Token, AnsiColor | null>>;
|
|
24
|
+
/**
|
|
25
|
+
* The default theme, derived from `doc.css`'s LIGHT palette (the ten
|
|
26
|
+
* neutrals, the five semantic hues; the four width tokens are `null`, see
|
|
27
|
+
* `AnsiTheme`'s doc comment). Each entry's `ansi16` is a human judgment
|
|
28
|
+
* call, not a formula: the nearest of the eight standard terminal colors to
|
|
29
|
+
* the source hex, since 16-color terminals have no way to represent most of
|
|
30
|
+
* these hues exactly. `ansi256`/`truecolor` are derived mechanically from
|
|
31
|
+
* the same hex value, so only `ansi16` needed a human decision per token.
|
|
32
|
+
*
|
|
33
|
+
* A render option lets a caller pass a different `AnsiTheme` instead (see
|
|
34
|
+
* `render.ts`'s `RenderMarkOptions.theme`).
|
|
35
|
+
*/
|
|
36
|
+
export declare const defaultAnsiTheme: AnsiTheme;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Parses a `#rrggbb` literal into an `[r, g, b]` triple. Used only to build the theme's truecolor entries from the hex values `doc.css` itself carries. */
|
|
2
|
+
function hex(value) {
|
|
3
|
+
const r = Number.parseInt(value.slice(1, 3), 16);
|
|
4
|
+
const g = Number.parseInt(value.slice(3, 5), 16);
|
|
5
|
+
const b = Number.parseInt(value.slice(5, 7), 16);
|
|
6
|
+
return [r, g, b];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The standard xterm 216-color-cube plus grayscale-ramp formula for mapping
|
|
10
|
+
* a truecolor value down to the 256-color palette: an exact gray maps into
|
|
11
|
+
* the 24-step grayscale ramp (indices 232-255), everything else maps into
|
|
12
|
+
* the 6x6x6 color cube (indices 16-231). This is arithmetic, not a table
|
|
13
|
+
* lookup, so it introduces no dependency.
|
|
14
|
+
*/
|
|
15
|
+
function rgbToAnsi256(r, g, b) {
|
|
16
|
+
if (r === g && g === b) {
|
|
17
|
+
if (r < 8)
|
|
18
|
+
return 16;
|
|
19
|
+
if (r > 248)
|
|
20
|
+
return 231;
|
|
21
|
+
return Math.round(((r - 8) / 247) * 24) + 232;
|
|
22
|
+
}
|
|
23
|
+
const toCube = (channel) => Math.round((channel / 255) * 5);
|
|
24
|
+
return 16 + 36 * toCube(r) + 6 * toCube(g) + toCube(b);
|
|
25
|
+
}
|
|
26
|
+
/** Builds an `AnsiColor` from a `doc.css` hex literal and a hand-picked nearest ANSI-16 foreground code (the approximation this module's comment documents). */
|
|
27
|
+
function color(hexValue, ansi16) {
|
|
28
|
+
const truecolor = hex(hexValue);
|
|
29
|
+
return { ansi16, ansi256: rgbToAnsi256(...truecolor), truecolor };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The default theme, derived from `doc.css`'s LIGHT palette (the ten
|
|
33
|
+
* neutrals, the five semantic hues; the four width tokens are `null`, see
|
|
34
|
+
* `AnsiTheme`'s doc comment). Each entry's `ansi16` is a human judgment
|
|
35
|
+
* call, not a formula: the nearest of the eight standard terminal colors to
|
|
36
|
+
* the source hex, since 16-color terminals have no way to represent most of
|
|
37
|
+
* these hues exactly. `ansi256`/`truecolor` are derived mechanically from
|
|
38
|
+
* the same hex value, so only `ansi16` needed a human decision per token.
|
|
39
|
+
*
|
|
40
|
+
* A render option lets a caller pass a different `AnsiTheme` instead (see
|
|
41
|
+
* `render.ts`'s `RenderMarkOptions.theme`).
|
|
42
|
+
*/
|
|
43
|
+
export const defaultAnsiTheme = {
|
|
44
|
+
// #fff — near-white ground; nearest ANSI-16 is plain white.
|
|
45
|
+
'--mk-bg': color('#ffffff', 37),
|
|
46
|
+
// #fff — same as --mk-bg (raised surfaces are un-tinted in the light theme).
|
|
47
|
+
'--mk-raised': color('#ffffff', 37),
|
|
48
|
+
// #1a1a1a — near-black body text.
|
|
49
|
+
'--mk-fg': color('#1a1a1a', 30),
|
|
50
|
+
// #f4f4f5 — a hair off white.
|
|
51
|
+
'--mk-surface': color('#f4f4f5', 37),
|
|
52
|
+
// #f0f0f2 — a hair off white, slightly deeper than --mk-surface.
|
|
53
|
+
'--mk-surface-strong': color('#f0f0f2', 37),
|
|
54
|
+
// #e4e4e7 — a light hairline gray, closer to white than to black.
|
|
55
|
+
'--mk-border': color('#e4e4e7', 37),
|
|
56
|
+
// #52525b — a mid-dark gray; bright black (90) reads as "gray" in most terminals.
|
|
57
|
+
'--mk-muted': color('#52525b', 90),
|
|
58
|
+
// #94a3b8 — a light blue-gray, closer to white than to any saturated hue.
|
|
59
|
+
'--mk-faint': color('#94a3b8', 37),
|
|
60
|
+
// #3b82f6 — a clear blue.
|
|
61
|
+
'--mk-accent': color('#3b82f6', 34),
|
|
62
|
+
// #fff — ink meant to sit on a solid accent fill; white is the nearest ANSI-16.
|
|
63
|
+
'--mk-on-accent': color('#ffffff', 37),
|
|
64
|
+
// #3b82f6 — same blue as --mk-accent (doc.css deliberately reuses it).
|
|
65
|
+
'--mk-info': color('#3b82f6', 34),
|
|
66
|
+
// #15803d — a clear green.
|
|
67
|
+
'--mk-success': color('#15803d', 32),
|
|
68
|
+
// #d97706 — an amber; nearest ANSI-16 is plain yellow.
|
|
69
|
+
'--mk-warning': color('#d97706', 33),
|
|
70
|
+
// #dc2626 — a clear red.
|
|
71
|
+
'--mk-danger': color('#dc2626', 31),
|
|
72
|
+
// #7c3aed — a violet; nearest ANSI-16 is plain magenta.
|
|
73
|
+
'--mk-limit': color('#7c3aed', 35),
|
|
74
|
+
// Width presets: consumed as column arithmetic by ./layout.ts, never as a color.
|
|
75
|
+
'--mk-width-fit': null,
|
|
76
|
+
'--mk-width-narrow': null,
|
|
77
|
+
'--mk-width-wide': null,
|
|
78
|
+
'--mk-width-full': null,
|
|
79
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scheme-safety logic shared by every host-resolver seam on this
|
|
3
|
+
* engine's `RenderMarkOptions`: `resolveImageSrc` (`./image-resolve.js`)
|
|
4
|
+
* and `resolveHref` (`./href-resolve.js`). Both seams follow the identical
|
|
5
|
+
* shape: offer a host resolver a value that looks like ITS OWN relative
|
|
6
|
+
* path, then refuse a `javascript:`/`vbscript:` scheme in whatever the
|
|
7
|
+
* resolver hands back. That logic lives here ONCE and each seam's module
|
|
8
|
+
* only adds the one-line wrapper that names its own option
|
|
9
|
+
* (`resolveImageSrc` vs `resolveHref`). See `./image-resolve.js`'s top
|
|
10
|
+
* comment for the full rationale: why the result is checked against a
|
|
11
|
+
* narrow denylist here, not `@markii/core`'s author-facing `isSafeUrl`
|
|
12
|
+
* allowlist. Ported verbatim from `@markii/html`'s `url-resolve.ts` (itself
|
|
13
|
+
* mirroring `@markii/react`'s), engine-neutral, so all three engines cannot
|
|
14
|
+
* diverge on what counts as a dangerous scheme.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The scheme text before the first `:` when one is present in scheme
|
|
18
|
+
* position, lowercased; `undefined` for a schemeless value. Delimiter rule
|
|
19
|
+
* matches `@markii/core`'s `isSafeUrl`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function schemeOf(value: string): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* True for a value worth offering to a resolver at all: no scheme, no
|
|
24
|
+
* protocol-relative `//host/...` form, no bare `#fragment`, not
|
|
25
|
+
* empty/whitespace.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isResolvableRelativeValue(value: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Schemes that turn a resolved URL into a script-execution vector.
|
|
30
|
+
* Everything else a resolver returns (`https:`, `data:`, `app:`, a host's
|
|
31
|
+
* own custom scheme) is a legitimate resolved location, not a smuggled
|
|
32
|
+
* script.
|
|
33
|
+
*/
|
|
34
|
+
export declare const DANGEROUS_URL_SCHEMES: Set<string>;
|
|
35
|
+
/**
|
|
36
|
+
* `value` reduced to what a browser will actually parse a scheme out of:
|
|
37
|
+
* ASCII tab, line feed and carriage return removed wherever they appear,
|
|
38
|
+
* then leading C0 controls and spaces stripped. The URL parser ignores
|
|
39
|
+
* exactly these, so a tab or newline spliced into the middle of a scheme
|
|
40
|
+
* name, or leading whitespace/control characters before it, both still
|
|
41
|
+
* reach the page as that scheme. A scheme test that reads the raw text
|
|
42
|
+
* instead would call both of them schemeless and wave them through, which
|
|
43
|
+
* is the difference between a denylist that holds and one that only looks
|
|
44
|
+
* like it does.
|
|
45
|
+
*/
|
|
46
|
+
export declare function forSchemeTest(value: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* True unless `value` carries one of `DANGEROUS_URL_SCHEMES`, judged
|
|
49
|
+
* against `forSchemeTest`'s browser-equivalent reading rather than the raw
|
|
50
|
+
* string. Because this IS a denylist, an unrecognized scheme is allowed, so
|
|
51
|
+
* the parsing it rests on has to match the browser's exactly: an allowlist
|
|
52
|
+
* fails closed on a spelling it does not recognize, and this cannot.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isSafeResolvedUrl(value: string): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scheme-safety logic shared by every host-resolver seam on this
|
|
3
|
+
* engine's `RenderMarkOptions`: `resolveImageSrc` (`./image-resolve.js`)
|
|
4
|
+
* and `resolveHref` (`./href-resolve.js`). Both seams follow the identical
|
|
5
|
+
* shape: offer a host resolver a value that looks like ITS OWN relative
|
|
6
|
+
* path, then refuse a `javascript:`/`vbscript:` scheme in whatever the
|
|
7
|
+
* resolver hands back. That logic lives here ONCE and each seam's module
|
|
8
|
+
* only adds the one-line wrapper that names its own option
|
|
9
|
+
* (`resolveImageSrc` vs `resolveHref`). See `./image-resolve.js`'s top
|
|
10
|
+
* comment for the full rationale: why the result is checked against a
|
|
11
|
+
* narrow denylist here, not `@markii/core`'s author-facing `isSafeUrl`
|
|
12
|
+
* allowlist. Ported verbatim from `@markii/html`'s `url-resolve.ts` (itself
|
|
13
|
+
* mirroring `@markii/react`'s), engine-neutral, so all three engines cannot
|
|
14
|
+
* diverge on what counts as a dangerous scheme.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The scheme text before the first `:` when one is present in scheme
|
|
18
|
+
* position, lowercased; `undefined` for a schemeless value. Delimiter rule
|
|
19
|
+
* matches `@markii/core`'s `isSafeUrl`.
|
|
20
|
+
*/
|
|
21
|
+
export function schemeOf(value) {
|
|
22
|
+
const colon = value.indexOf(':');
|
|
23
|
+
if (colon === -1)
|
|
24
|
+
return undefined;
|
|
25
|
+
const slash = value.indexOf('/');
|
|
26
|
+
const questionMark = value.indexOf('?');
|
|
27
|
+
const numberSign = value.indexOf('#');
|
|
28
|
+
const hasSchemeBeforeDelimiter = (slash === -1 || colon < slash) &&
|
|
29
|
+
(questionMark === -1 || colon < questionMark) &&
|
|
30
|
+
(numberSign === -1 || colon < numberSign);
|
|
31
|
+
return hasSchemeBeforeDelimiter
|
|
32
|
+
? value.slice(0, colon).toLowerCase()
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True for a value worth offering to a resolver at all: no scheme, no
|
|
37
|
+
* protocol-relative `//host/...` form, no bare `#fragment`, not
|
|
38
|
+
* empty/whitespace.
|
|
39
|
+
*/
|
|
40
|
+
export function isResolvableRelativeValue(value) {
|
|
41
|
+
if (value.trim() === '')
|
|
42
|
+
return false;
|
|
43
|
+
if (value.startsWith('#'))
|
|
44
|
+
return false;
|
|
45
|
+
if (value.startsWith('//'))
|
|
46
|
+
return false;
|
|
47
|
+
return schemeOf(value) === undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Schemes that turn a resolved URL into a script-execution vector.
|
|
51
|
+
* Everything else a resolver returns (`https:`, `data:`, `app:`, a host's
|
|
52
|
+
* own custom scheme) is a legitimate resolved location, not a smuggled
|
|
53
|
+
* script.
|
|
54
|
+
*/
|
|
55
|
+
export const DANGEROUS_URL_SCHEMES = new Set(['javascript', 'vbscript']);
|
|
56
|
+
const TAB_NEWLINE_CR = new RegExp('[\\u0009\\u000a\\u000d]', 'g');
|
|
57
|
+
const LEADING_C0_OR_SPACE = new RegExp('^[\\u0000-\\u0020]+');
|
|
58
|
+
/**
|
|
59
|
+
* `value` reduced to what a browser will actually parse a scheme out of:
|
|
60
|
+
* ASCII tab, line feed and carriage return removed wherever they appear,
|
|
61
|
+
* then leading C0 controls and spaces stripped. The URL parser ignores
|
|
62
|
+
* exactly these, so a tab or newline spliced into the middle of a scheme
|
|
63
|
+
* name, or leading whitespace/control characters before it, both still
|
|
64
|
+
* reach the page as that scheme. A scheme test that reads the raw text
|
|
65
|
+
* instead would call both of them schemeless and wave them through, which
|
|
66
|
+
* is the difference between a denylist that holds and one that only looks
|
|
67
|
+
* like it does.
|
|
68
|
+
*/
|
|
69
|
+
export function forSchemeTest(value) {
|
|
70
|
+
return value.replace(TAB_NEWLINE_CR, '').replace(LEADING_C0_OR_SPACE, '');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* True unless `value` carries one of `DANGEROUS_URL_SCHEMES`, judged
|
|
74
|
+
* against `forSchemeTest`'s browser-equivalent reading rather than the raw
|
|
75
|
+
* string. Because this IS a denylist, an unrecognized scheme is allowed, so
|
|
76
|
+
* the parsing it rests on has to match the browser's exactly: an allowlist
|
|
77
|
+
* fails closed on a spelling it does not recognize, and this cannot.
|
|
78
|
+
*/
|
|
79
|
+
export function isSafeResolvedUrl(value) {
|
|
80
|
+
const scheme = schemeOf(forSchemeTest(value));
|
|
81
|
+
return scheme === undefined || !DANGEROUS_URL_SCHEMES.has(scheme);
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thin, unformatted wrapper around `@markii/stdlib`'s `formatValue`
|
|
3
|
+
* (called with no `format`, i.e. `'plain'`), mirroring `@markii/html`'s
|
|
4
|
+
* `stringifyStoredValue`: this engine's name for "the `:value[...]`
|
|
5
|
+
* default coercion", used wherever a caller has no `format=`/`decimals=`
|
|
6
|
+
* attribute to thread through. Every engine's `:value[...]` and every
|
|
7
|
+
* `format=`-aware component calls `formatValue` directly instead, so a
|
|
8
|
+
* number, date, or percentage reads identically across all three
|
|
9
|
+
* (docs/format.md).
|
|
10
|
+
*
|
|
11
|
+
* Never throws, for the same reason `formatValue` itself never throws.
|
|
12
|
+
*/
|
|
13
|
+
export declare function stringifyStoredValue(value: unknown): string;
|