linegauge 0.3.3 → 0.4.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/README.md +2 -2
- package/dist/check.d.ts +14 -0
- package/dist/check.js +57 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +9 -0
- package/dist/plugin.d.ts +66 -0
- package/dist/plugin.js +125 -0
- package/dist/schema.json +1 -0
- package/dist/width.d.ts +2 -0
- package/dist/width.js +9 -0
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -154,10 +154,10 @@ its own suite — which this package passes. The runner reports that as a failur
|
|
|
154
154
|
to the incumbent an unexpected pass means a stale annotation; it is counted here as the
|
|
155
155
|
pass it is, and marked rather than left to look like the ones beside it.
|
|
156
156
|
|
|
157
|
-
Weight, installed and tree-inclusive: **
|
|
157
|
+
Weight, installed and tree-inclusive: **77,124 bytes** against **170,342** for the incumbents it replaces — a ratio of **0.4528**.
|
|
158
158
|
## Where it sits
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
Plugins register under the `widths` key, against the one schema the whole family shares.
|
|
161
161
|
|
|
162
162
|
`burgee`, `caique`, `flagstaff` build on it, and it builds on nothing in this family.
|
|
163
163
|
## Licence
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1, restated rather than imported: linegauge is an independent product and depends on nobody in
|
|
3
|
+
* the family for its exit contract. `scripts/exit-code-lock.test.ts` keeps the restatements in
|
|
4
|
+
* step.
|
|
5
|
+
*/
|
|
6
|
+
export declare const EXIT_OK = 0;
|
|
7
|
+
export declare const EXIT_RUNTIME = 1;
|
|
8
|
+
export declare const EXIT_USAGE = 2;
|
|
9
|
+
/**
|
|
10
|
+
* Every refusal leaves through here, wherever it was raised: `register()`, or a plugin file that
|
|
11
|
+
* registers itself on import — which throws inside the `import()`, before any line of `inspect`
|
|
12
|
+
* could catch it, and used to reach the bin as a bare message with no code and no fix (R8).
|
|
13
|
+
*/
|
|
14
|
+
export declare function check(argv: readonly string[], write: (s: string) => void): Promise<number>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { overrides, PluginError, register, setOverrides, validate } from './plugin.js';
|
|
4
|
+
import { width } from './width.js';
|
|
5
|
+
export const EXIT_OK = 0;
|
|
6
|
+
export const EXIT_RUNTIME = 1;
|
|
7
|
+
export const EXIT_USAGE = 2;
|
|
8
|
+
function refuse(code, message, fix, write) {
|
|
9
|
+
write(`${code}: ${message}\n fix: ${fix}\n`);
|
|
10
|
+
return EXIT_RUNTIME;
|
|
11
|
+
}
|
|
12
|
+
const HEX = 16;
|
|
13
|
+
const MIN_DIGITS = 4;
|
|
14
|
+
const codePoint = (n) => `U+${n.toString(HEX).toUpperCase().padStart(MIN_DIGITS, '0')}`;
|
|
15
|
+
const overridesOf = (plugin) => {
|
|
16
|
+
const widths = plugin?.widths;
|
|
17
|
+
return typeof widths === 'object' && widths !== null ? Object.entries(widths) : [];
|
|
18
|
+
};
|
|
19
|
+
const firstOf = (plugin) => overridesOf(plugin).flatMap(([, o]) => (o.ranges ?? []).map(([low]) => low));
|
|
20
|
+
const shadows = (names) => (names.length === 0 ? '' : ` (replaces ${names.join(', ')})`);
|
|
21
|
+
export async function check(argv, write) {
|
|
22
|
+
try {
|
|
23
|
+
return await inspect(argv, write);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (!(error instanceof PluginError))
|
|
27
|
+
throw error;
|
|
28
|
+
return refuse(error.code, error.message, error.fix, write);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function inspect(argv, write) {
|
|
32
|
+
const file = argv[0];
|
|
33
|
+
if (file === undefined) {
|
|
34
|
+
write('usage: linegauge check <plugin-file>\n');
|
|
35
|
+
return EXIT_USAGE;
|
|
36
|
+
}
|
|
37
|
+
const loaded = (await import(pathToFileURL(resolve(file)).href));
|
|
38
|
+
const plugin = loaded.default ?? loaded;
|
|
39
|
+
for (const registered of [...overrides().keys()])
|
|
40
|
+
setOverrides(registered, {});
|
|
41
|
+
const builtIn = new Map(firstOf(plugin).map((code) => [code, width(String.fromCodePoint(code))]));
|
|
42
|
+
validate(plugin);
|
|
43
|
+
register(plugin);
|
|
44
|
+
const name = plugin.name;
|
|
45
|
+
const rows = overridesOf(plugin).flatMap(([label, o]) => (o.ranges ?? []).map(([low, high]) => {
|
|
46
|
+
const span = low === high ? codePoint(low) : `${codePoint(low)}..${codePoint(high)}`;
|
|
47
|
+
return `${label} ${span} built-in ${String(builtIn.get(low) ?? '?')} → ${String(width(String.fromCodePoint(low)))} — ${o.why}`;
|
|
48
|
+
}));
|
|
49
|
+
write(`${name} — ${String(rows.length)} widths\n`);
|
|
50
|
+
if (rows.length === 0) {
|
|
51
|
+
return refuse('E_NO_CONTRIBUTION', `${name} registers, but contributes nothing linegauge reads`, 'add a `widths` section — a key another package in the family reads is allowed in the same object, but `linegauge check` cannot show it', write);
|
|
52
|
+
}
|
|
53
|
+
for (const row of rows)
|
|
54
|
+
write(` ${row}\n`);
|
|
55
|
+
write(`${name}: ok\n`);
|
|
56
|
+
return EXIT_OK;
|
|
57
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { check, EXIT_RUNTIME } from './check.js';
|
|
3
|
+
const [, , command, ...rest] = process.argv;
|
|
4
|
+
check(command === 'check' ? rest : [command, ...rest].filter((a) => a !== undefined), (s) => void process.stdout.write(s)).then((code) => {
|
|
5
|
+
process.exitCode = code;
|
|
6
|
+
}, (error) => {
|
|
7
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
8
|
+
process.exitCode = EXIT_RUNTIME;
|
|
9
|
+
});
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One plugin-supplied width override: the column count a set of code-point ranges occupies.
|
|
3
|
+
*/
|
|
4
|
+
export interface WidthOverride {
|
|
5
|
+
/** Inclusive `[low, high]` code-point pairs. */
|
|
6
|
+
ranges: readonly (readonly [number, number])[];
|
|
7
|
+
/** 0 for a zero-width mark, 1 narrow, 2 wide. */
|
|
8
|
+
columns: number;
|
|
9
|
+
/** Which terminal or font disagrees, and how it was measured. Required; see the file comment. */
|
|
10
|
+
why: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Later registrations win, which is the point: the user is the authority on their terminal.
|
|
14
|
+
*
|
|
15
|
+
* An empty table **removes** the plugin rather than recording that it contributes nothing.
|
|
16
|
+
* `overrides()` is what `linegauge check` prints and what a caller inspects, and a name in it
|
|
17
|
+
* with no ranges under it reads as *this plugin is active* when the truth is the opposite.
|
|
18
|
+
*
|
|
19
|
+
* The seam in `width.ts` is installed only while something is registered, so the last plugin
|
|
20
|
+
* leaving puts `measure` back on the path with no call in it at all.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setOverrides(name: string, widths: Record<string, WidthOverride>): void;
|
|
23
|
+
/** Every override currently registered, by plugin name — what `linegauge check` prints. */
|
|
24
|
+
export declare function overrides(): ReadonlyMap<string, Record<string, WidthOverride>>;
|
|
25
|
+
/**
|
|
26
|
+
* The plugin contract version. One number for the family — the same `1` flagstaff, caique,
|
|
27
|
+
* closeout and paratext declare, written out rather than imported for the reason above.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CONTRACT = 1;
|
|
30
|
+
/**
|
|
31
|
+
* The keys linegauge reads. Declared structurally: any object with these fields is a plugin
|
|
32
|
+
* here, whatever else it carries.
|
|
33
|
+
*/
|
|
34
|
+
export interface Plugin {
|
|
35
|
+
name: string;
|
|
36
|
+
contract?: number;
|
|
37
|
+
/** Width overrides by name. The key is the author's label; see {@link validate}. */
|
|
38
|
+
widths?: Record<string, WidthOverride>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Two codes, both already in the family's vocabulary
|
|
42
|
+
* (`scripts/plugin-error-vocabulary-lock.test.ts`).
|
|
43
|
+
*/
|
|
44
|
+
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_CONTRIBUTION';
|
|
45
|
+
/** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
|
|
46
|
+
export declare class PluginError extends Error {
|
|
47
|
+
readonly code: PluginErrorCode;
|
|
48
|
+
readonly fix: string;
|
|
49
|
+
constructor(code: PluginErrorCode, message: string, fix: string);
|
|
50
|
+
}
|
|
51
|
+
type Problem = {
|
|
52
|
+
code: PluginErrorCode;
|
|
53
|
+
line: string;
|
|
54
|
+
fix: string;
|
|
55
|
+
};
|
|
56
|
+
/** Everything wrong with a plugin document, in the family's vocabulary. Empty means it registers. */
|
|
57
|
+
export declare function problems(plugin: unknown): Problem[];
|
|
58
|
+
/** Throws on the first problem, in the family's vocabulary. */
|
|
59
|
+
export declare function validate(plugin: unknown): asserts plugin is Plugin;
|
|
60
|
+
/**
|
|
61
|
+
* Register a plugin's width overrides. Later registrations win over earlier ones, and over the
|
|
62
|
+
* built-in tables — which is the point: the built-in answer is right for most terminals and the
|
|
63
|
+
* user is the authority on theirs.
|
|
64
|
+
*/
|
|
65
|
+
export declare function register(plugin: unknown): void;
|
|
66
|
+
export {};
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { setClaim } from './width.js';
|
|
2
|
+
let flattened = [];
|
|
3
|
+
const byPlugin = new Map();
|
|
4
|
+
function overridden(code) {
|
|
5
|
+
let found;
|
|
6
|
+
for (let i = 0; i < flattened.length; i += 3) {
|
|
7
|
+
if (code >= flattened[i] && code <= flattened[i + 1])
|
|
8
|
+
found = flattened[i + 2];
|
|
9
|
+
}
|
|
10
|
+
return found;
|
|
11
|
+
}
|
|
12
|
+
export function setOverrides(name, widths) {
|
|
13
|
+
if (Object.keys(widths).length === 0)
|
|
14
|
+
byPlugin.delete(name);
|
|
15
|
+
else
|
|
16
|
+
byPlugin.set(name, widths);
|
|
17
|
+
const rows = [];
|
|
18
|
+
for (const table of byPlugin.values()) {
|
|
19
|
+
for (const { ranges, columns } of Object.values(table)) {
|
|
20
|
+
for (const [low, high] of ranges)
|
|
21
|
+
rows.push(low, high, columns);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
flattened = rows;
|
|
25
|
+
setClaim(rows.length === 0 ? undefined : overridden);
|
|
26
|
+
}
|
|
27
|
+
export function overrides() {
|
|
28
|
+
return byPlugin;
|
|
29
|
+
}
|
|
30
|
+
export const CONTRACT = 1;
|
|
31
|
+
export class PluginError extends Error {
|
|
32
|
+
code;
|
|
33
|
+
fix;
|
|
34
|
+
constructor(code, message, fix) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.fix = fix;
|
|
38
|
+
this.name = 'PluginError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
42
|
+
const MAX_CODE_POINT = 0x10_ff_ff;
|
|
43
|
+
const MAX_COLUMNS = 2;
|
|
44
|
+
function rangeProblem(label, range) {
|
|
45
|
+
if (!Array.isArray(range) || range.length !== 2)
|
|
46
|
+
return `${label} is not a [low, high] pair`;
|
|
47
|
+
const [low, high] = range;
|
|
48
|
+
if (!Number.isInteger(low) || !Number.isInteger(high))
|
|
49
|
+
return `${label} has a non-integer bound`;
|
|
50
|
+
const lo = low;
|
|
51
|
+
const hi = high;
|
|
52
|
+
if (lo < 0 || hi > MAX_CODE_POINT)
|
|
53
|
+
return `${label} is outside U+0000..U+10FFFF`;
|
|
54
|
+
if (lo > hi)
|
|
55
|
+
return `${label} runs backwards: [${String(lo)}, ${String(hi)}]`;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function headerProblems(plugin) {
|
|
59
|
+
const out = [];
|
|
60
|
+
if (typeof plugin['name'] !== 'string' || plugin['name'] === '') {
|
|
61
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: 'a plugin needs a non-empty `name`', fix: 'add `name: "my-widths"` — it is what `linegauge check` and a later registration call it' });
|
|
62
|
+
}
|
|
63
|
+
const contract = plugin['contract'];
|
|
64
|
+
if (contract !== undefined && contract !== CONTRACT) {
|
|
65
|
+
out.push({
|
|
66
|
+
code: 'E_PLUGIN_CONTRACT',
|
|
67
|
+
line: `contract ${String(contract)} is not ${String(CONTRACT)}`,
|
|
68
|
+
fix: `set \`contract: ${String(CONTRACT)}\`, or drop the field — the family is on one version`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function overrideProblems(name, value) {
|
|
74
|
+
if (!isRecord(value))
|
|
75
|
+
return [{ code: 'E_PLUGIN_SCHEMA', line: `widths.${name} is not an object`, fix: 'each override is { ranges, columns, why }' }];
|
|
76
|
+
const out = [];
|
|
77
|
+
const ranges = value['ranges'];
|
|
78
|
+
if (!Array.isArray(ranges) || ranges.length === 0) {
|
|
79
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: `widths.${name}.ranges is a non-empty array of [low, high] pairs`, fix: 'ranges: [[0xE000, 0xF8FF]]' });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
for (const [i, range] of ranges.entries()) {
|
|
83
|
+
const problem = rangeProblem(`widths.${name}.ranges[${String(i)}]`, range);
|
|
84
|
+
if (problem !== undefined)
|
|
85
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: problem, fix: 'each range is [low, high], both integers in U+0000..U+10FFFF, low first' });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const columns = value['columns'];
|
|
89
|
+
if (!Number.isInteger(columns) || columns < 0 || columns > MAX_COLUMNS) {
|
|
90
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: `widths.${name}.columns is 0, 1 or 2`, fix: '0 for a zero-width mark, 1 narrow, 2 wide' });
|
|
91
|
+
}
|
|
92
|
+
if (typeof value['why'] !== 'string' || value['why'] === '') {
|
|
93
|
+
out.push({
|
|
94
|
+
code: 'E_PLUGIN_SCHEMA',
|
|
95
|
+
line: `widths.${name}.why is required`,
|
|
96
|
+
fix: 'say which terminal or font disagrees, and how you measured it — ambiguous width is wrong often enough that the next reader needs the provenance',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
export function problems(plugin) {
|
|
102
|
+
if (!isRecord(plugin)) {
|
|
103
|
+
return [{ code: 'E_PLUGIN_SCHEMA', line: 'a plugin is an object', fix: 'export default { name: "my-widths", widths: { … } }' }];
|
|
104
|
+
}
|
|
105
|
+
const out = headerProblems(plugin);
|
|
106
|
+
const widths = plugin['widths'];
|
|
107
|
+
if (widths === undefined)
|
|
108
|
+
return out;
|
|
109
|
+
if (!isRecord(widths)) {
|
|
110
|
+
out.push({ code: 'E_PLUGIN_SCHEMA', line: '`widths` is an object keyed by name', fix: 'widths: { "nerd-font-icons": { ranges: [[0xE000, 0xF8FF]], columns: 2, why: "…" } }' });
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
for (const [name, value] of Object.entries(widths))
|
|
114
|
+
out.push(...overrideProblems(name, value));
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
export function validate(plugin) {
|
|
118
|
+
const [first] = problems(plugin);
|
|
119
|
+
if (first !== undefined)
|
|
120
|
+
throw new PluginError(first.code, first.line, first.fix);
|
|
121
|
+
}
|
|
122
|
+
export function register(plugin) {
|
|
123
|
+
validate(plugin);
|
|
124
|
+
setOverrides(plugin.name, plugin.widths ?? {});
|
|
125
|
+
}
|
package/dist/schema.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"propertyNames":{"enum":["error","warn","ok","hint","muted","command","flag","value","heading","ground"],"description":"roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}},"capabilities":{"$ref":"#/$defs/capabilities"},"widths":{"$ref":"#/$defs/widths"}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}},"capability":{"type":"object","required":["name","osc","when","encode","fallback"],"additionalProperties":false,"description":"One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.","properties":{"name":{"type":"string","minLength":1,"description":"How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."},"osc":{"description":"The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.","oneOf":[{"type":"integer","minimum":0},{"const":"BEL"}]},"when":{"type":"object","description":"When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.","additionalProperties":false,"properties":{"tty":{"type":"boolean","description":"Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."},"termProgram":{"type":"array","items":{"type":"string"},"description":"Any one of these TERM_PROGRAM values."},"envAny":{"type":"array","items":{"type":"string"},"description":"Any one of these environment variables merely being set, as VTE announces itself."},"term":{"type":"string","description":"An exact TERM — Kitty is xterm-kitty."}}},"encode":{"type":"string","minLength":1,"description":"The bytes, as a template, for a terminal that does understand."},"fallback":{"type":"string","description":"What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."}},"examples":[{"name":"kitty-image","osc":"BEL","when":{"tty":true,"term":"xterm-kitty"},"encode":"\u001b_Ga=T,f=100;{base64}\u001b\\","fallback":"{caption}"}]},"capabilities":{"type":"object","description":"paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/capability"}},"capabilityDocument":{"description":"What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.","oneOf":[{"description":"The family shape: a plugin carrying its capabilities under `capabilities`.","type":"object","required":["name","capabilities"],"properties":{"name":{"type":"string","minLength":1},"contract":{"type":"integer","minimum":1},"capabilities":{"$ref":"#/$defs/capabilities"}}},{"deprecated":true,"description":"Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.","$ref":"#/$defs/capability"}]},"widthRange":{"type":"array","description":"One inclusive code-point range, as [low, high]. A single code point is written [n, n].","items":{"type":"integer","minimum":0,"maximum":1114111},"minItems":2,"maxItems":2},"widthOverride":{"type":"object","description":"A linegauge width override: the column count a named set of code-point ranges occupies, for a terminal that disagrees with the Unicode tables. Data only — no function, so it can be written in a config file, diffed, and printed by `linegauge check` without running anyone code.","required":["ranges","columns","why"],"additionalProperties":false,"properties":{"ranges":{"type":"array","description":"The code points this override applies to.","items":{"$ref":"#/$defs/widthRange"},"minItems":1},"columns":{"type":"integer","description":"Columns each cluster in those ranges occupies: 0 for a zero-width mark, 1 narrow, 2 wide.","minimum":0,"maximum":2},"why":{"type":"string","description":"The terminal or the reason. Required, because a width table with no provenance is one nobody can audit when it turns out to be wrong — which is the normal outcome for ambiguous width.","minLength":1}}},"widths":{"type":"object","description":"linegauge width overrides by name — the measurement section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/widthOverride"}}}}
|
package/dist/width.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Installed by `plugin.ts` when the first override registers, and cleared when the last one goes. */
|
|
2
|
+
export declare function setClaim(fn: ((code: number) => number | undefined) | undefined): void;
|
|
1
3
|
/**
|
|
2
4
|
* Columns a string of *plain* text occupies — no escape scan. The wrapper below has
|
|
3
5
|
* already split its input into text runs and complete sequences, so rescanning would only
|
package/dist/width.js
CHANGED
|
@@ -178,9 +178,18 @@ function hangulColumns(visible, ambiguousIsWide) {
|
|
|
178
178
|
}
|
|
179
179
|
return columns;
|
|
180
180
|
}
|
|
181
|
+
let claim;
|
|
182
|
+
export function setClaim(fn) {
|
|
183
|
+
claim = fn;
|
|
184
|
+
}
|
|
181
185
|
export function measure(text, ambiguousIsWide = false) {
|
|
182
186
|
let columns = 0;
|
|
183
187
|
for (const { segment } of segmenter().segment(text)) {
|
|
188
|
+
const claimed = claim?.(segment.codePointAt(0) ?? 0);
|
|
189
|
+
if (claimed !== undefined) {
|
|
190
|
+
columns += claimed;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
184
193
|
if (ZERO_WIDTH_CLUSTER().test(segment))
|
|
185
194
|
continue;
|
|
186
195
|
if (RGI_EMOJI().test(segment) || isUnqualifiedEmojiSequence(segment)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linegauge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A printer's line gauge \u2014 the steel rule marked in picas and points. Measuring, wrapping, truncating and slicing styled terminal text without the edge fraying \u2014 grapheme-correct over Intl.Segmenter. Drop-in paths for string-width, wrap-ansi, strip-ansi and slice-ansi. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,13 @@
|
|
|
37
37
|
"types": "./dist/strip.d.ts",
|
|
38
38
|
"import": "./dist/strip.js",
|
|
39
39
|
"default": "./dist/strip.js"
|
|
40
|
-
}
|
|
40
|
+
},
|
|
41
|
+
"./plugin": {
|
|
42
|
+
"types": "./dist/plugin.d.ts",
|
|
43
|
+
"import": "./dist/plugin.js",
|
|
44
|
+
"default": "./dist/plugin.js"
|
|
45
|
+
},
|
|
46
|
+
"./schema.json": "./dist/schema.json"
|
|
41
47
|
},
|
|
42
48
|
"files": [
|
|
43
49
|
"dist",
|
|
@@ -45,7 +51,7 @@
|
|
|
45
51
|
"!dist/**/*.test.*"
|
|
46
52
|
],
|
|
47
53
|
"scripts": {
|
|
48
|
-
"build": "tsc -p tsconfig.build.json && node ../../scripts/strip-comments.mjs dist",
|
|
54
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/schema-to-dist.mjs && node ../../scripts/strip-comments.mjs dist",
|
|
49
55
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
50
56
|
"test": "vitest run --passWithNoTests",
|
|
51
57
|
"coverage": "vitest run --coverage.enabled",
|
|
@@ -76,5 +82,8 @@
|
|
|
76
82
|
],
|
|
77
83
|
"devDependencies": {
|
|
78
84
|
"vitest": "^5.0.1"
|
|
85
|
+
},
|
|
86
|
+
"bin": {
|
|
87
|
+
"linegauge": "./dist/cli.js"
|
|
79
88
|
}
|
|
80
89
|
}
|