@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.
Files changed (73) hide show
  1. package/dist/ansi.d.ts +79 -0
  2. package/dist/ansi.js +136 -0
  3. package/dist/box.d.ts +47 -0
  4. package/dist/box.js +209 -0
  5. package/dist/components/badge.d.ts +10 -0
  6. package/dist/components/badge.js +44 -0
  7. package/dist/components/callout.d.ts +20 -0
  8. package/dist/components/callout.js +71 -0
  9. package/dist/components/card.d.ts +12 -0
  10. package/dist/components/card.js +44 -0
  11. package/dist/components/cell.d.ts +18 -0
  12. package/dist/components/cell.js +31 -0
  13. package/dist/components/chart.d.ts +15 -0
  14. package/dist/components/chart.js +129 -0
  15. package/dist/components/details.d.ts +12 -0
  16. package/dist/components/details.js +25 -0
  17. package/dist/components/divider.d.ts +4 -0
  18. package/dist/components/divider.js +80 -0
  19. package/dist/components/figure.d.ts +20 -0
  20. package/dist/components/figure.js +53 -0
  21. package/dist/components/index.d.ts +34 -0
  22. package/dist/components/index.js +109 -0
  23. package/dist/components/kbd.d.ts +7 -0
  24. package/dist/components/kbd.js +8 -0
  25. package/dist/components/layout-wrapper.d.ts +35 -0
  26. package/dist/components/layout-wrapper.js +62 -0
  27. package/dist/components/progress.d.ts +15 -0
  28. package/dist/components/progress.js +78 -0
  29. package/dist/components/rating.d.ts +9 -0
  30. package/dist/components/rating.js +28 -0
  31. package/dist/components/row.d.ts +26 -0
  32. package/dist/components/row.js +67 -0
  33. package/dist/components/stat.d.ts +13 -0
  34. package/dist/components/stat.js +79 -0
  35. package/dist/components/tab.d.ts +13 -0
  36. package/dist/components/tab.js +17 -0
  37. package/dist/components/table-grid.d.ts +34 -0
  38. package/dist/components/table-grid.js +124 -0
  39. package/dist/components/table.d.ts +16 -0
  40. package/dist/components/table.js +101 -0
  41. package/dist/components/tabs.d.ts +18 -0
  42. package/dist/components/tabs.js +30 -0
  43. package/dist/failure-presentation.d.ts +58 -0
  44. package/dist/failure-presentation.js +124 -0
  45. package/dist/href-resolve.d.ts +21 -0
  46. package/dist/href-resolve.js +25 -0
  47. package/dist/image-resolve.d.ts +41 -0
  48. package/dist/image-resolve.js +28 -0
  49. package/dist/index.d.ts +15 -0
  50. package/dist/index.js +17 -0
  51. package/dist/layout.d.ts +73 -0
  52. package/dist/layout.js +144 -0
  53. package/dist/measure.d.ts +25 -0
  54. package/dist/measure.js +108 -0
  55. package/dist/registry.d.ts +227 -0
  56. package/dist/registry.js +121 -0
  57. package/dist/render.d.ts +69 -0
  58. package/dist/render.js +889 -0
  59. package/dist/resolve.d.ts +60 -0
  60. package/dist/resolve.js +152 -0
  61. package/dist/sanitize.d.ts +51 -0
  62. package/dist/sanitize.js +101 -0
  63. package/dist/style.d.ts +9 -0
  64. package/dist/style.js +13 -0
  65. package/dist/theme.d.ts +36 -0
  66. package/dist/theme.js +79 -0
  67. package/dist/url-resolve.d.ts +54 -0
  68. package/dist/url-resolve.js +82 -0
  69. package/dist/value-format.d.ts +13 -0
  70. package/dist/value-format.js +16 -0
  71. package/dist/value-types.d.ts +37 -0
  72. package/dist/value-types.js +17 -0
  73. 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>;
@@ -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;
@@ -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
+ }
@@ -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
+ }
@@ -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;