@orz/query 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright 2026 the orzui team
4
+ Copyright 2026 the todou team
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # @orz/query
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The grammar half of a search box and a slash-command composer, driven by a schema the app supplies:
4
+ value specs, a query parser with diagnostics, a command registry and line parser, shell-style words,
5
+ brace expansion and completion locators.
6
+
7
+ It has no runtime dependencies and does not use React or the DOM, so an app's server, CLI and web
8
+ client can import the same module and read a query or a command line the same way. The React and
9
+ CodeMirror halves live in `@orz/ui` (`QueryBox`, `HighlightedInput`) and `@orz/ui-codemirror`.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @orz/query
15
+ ```
16
+
17
+ ## Value specs
18
+
19
+ A qualifier value and a command argument are both a `ValueSpec`: an `enum` the schema knows, a
20
+ `named` value the app resolves through a provider (labels, members), or a `free` value. Providers
21
+ are passed at the point of use, so the schema itself stays static and shareable with a server:
22
+
23
+ ```ts
24
+ import { valueVerdict } from "@orz/query";
25
+
26
+ const providers = {
27
+ label: { options: [{ value: "kind:bug" }, { value: "area:web" }] },
28
+ member: { search: (prefix, signal) => fetchMembers(prefix, signal) },
29
+ };
30
+
31
+ valueVerdict({ kind: "named", source: "label" }, "kind:bug", providers.label); // "valid"
32
+ ```
33
+
34
+ ## Queries
35
+
36
+ ```ts
37
+ import { defineQuerySchema, parseQuery, queryFilters } from "@orz/query";
38
+
39
+ const schema = defineQuerySchema({
40
+ qualifiers: {
41
+ state: { value: { kind: "enum", values: ["open", "closed"] }, hint: "open or closed" },
42
+ label: { value: { kind: "named", source: "label" } },
43
+ assignee: { value: { kind: "named", source: "member", special: ["@me"] } },
44
+ },
45
+ braces: true,
46
+ });
47
+
48
+ const parts = parseQuery(schema, 'label:area:{web,ui} -state:closed "deploy failed"');
49
+ queryFilters(parts);
50
+ // [{ key: "label", negated: false, values: ["area:web", "area:ui"] },
51
+ // { key: "state", negated: true, values: ["closed"] }]
52
+ ```
53
+
54
+ Unknown keys are plain text, so `https://app.example` and `12:30` stay searchable. The parts tile
55
+ the input, which is what lets a highlighter paint under an `<input>`. `escapes: "shell"` switches
56
+ quoting to shell rules (`label:'a,b'`, `don\'t`); the default keeps backslashes ordinary.
57
+
58
+ ## Slash commands
59
+
60
+ ```ts
61
+ import { createCommandRegistry, parseCommandLines } from "@orz/query";
62
+
63
+ const registry = createCommandRegistry([
64
+ {
65
+ name: "close",
66
+ detail: "→ Closed",
67
+ args: [],
68
+ summarize: () => "close",
69
+ compile: () => ({ type: "close" }),
70
+ },
71
+ {
72
+ name: "label",
73
+ detail: "add labels",
74
+ args: [{ name: "label", value: { kind: "named", source: "label" }, rest: true, multiple: true }],
75
+ summarize: ([labels]) => `label ${[labels].flat().join(", ")}`,
76
+ compile: ([labels]) => ({ type: "label", names: [labels].flat() }),
77
+ },
78
+ ]);
79
+
80
+ parseCommandLines("Looks done.\n/label {kind:bug,area:web}\n/close", registry, providers);
81
+ // body: "Looks done.", commands: [{ type: "label", … }, { type: "close" }]
82
+ ```
83
+
84
+ A command is a whole line starting with `/name`; anything else, including `/close the door`, is
85
+ prose. Arguments are shell words, a `rest` argument takes the remainder of the line unquoted, and
86
+ brace expansion applies only to arguments declared `multiple`, which `compile` receives as a list.
87
+ A value with an `invalid` verdict, an unclosed quote or an expansion past 64 values breaks the line
88
+ before `compile` runs; the reason lands in `invalid`.
89
+
90
+ ## Completion
91
+
92
+ `queryCompletionSlot` and `commandCompletionSlot` say what is being typed at the caret: a key, a
93
+ value, a command or an argument, with its extent and value spec. Inside a brace group the slot is
94
+ only the current alternative. `slotValues` lists what fits (or answers `"fetch"` for a `search`
95
+ provider), `keyRows` and `valueRows` turn a query slot into rows that carry the rewritten query,
96
+ and `orderRows` keeps the submit row first unless something is genuinely being completed.
97
+
98
+ ## Building blocks
99
+
100
+ - `shellWords(text)` splits shell-style words, recording which characters were quoted;
101
+ `quoteShellWord(value)` quotes a value so it reads back as one word.
102
+ - `expandBraces(word)` expands bash's comma form (`{a,b}`, nested, at most 64 words);
103
+ quoted and escaped braces stay literal.
104
+ - `didYouMean(value, candidates)` suggests the closest candidate within a small edit distance.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Brace expansion, bash's comma form: `{a,b}` → `a`, `b`; adjacent text
3
+ * distributes; groups multiply left to right and nest. Text that merely
4
+ * contains braces is not changed: a group needs an unquoted comma at its own
5
+ * level, so `{}`, `{a}` and an unclosed `{` stay literal. Sequence
6
+ * expressions (`{1..5}`) are not supported and stay literal too.
7
+ */
8
+ import { type ShellWord } from "./shell-words.ts";
9
+ export type BraceExpansion = {
10
+ ok: true;
11
+ words: string[];
12
+ } | {
13
+ ok: false;
14
+ reason: string;
15
+ };
16
+ /** Fail rather than cut, the way a query with too many terms is rejected. */
17
+ export declare const BRACE_LIMIT = 64;
18
+ /** One character; only an active one can group or separate. */
19
+ export type BraceChar = {
20
+ ch: string;
21
+ active: boolean;
22
+ };
23
+ /** A plain string is treated as entirely unquoted. */
24
+ export declare function expandBraces(word: ShellWord | string, options?: {
25
+ limit?: number;
26
+ }): BraceExpansion;
27
+ /**
28
+ * For readings that are not shell words, such as plain mode's quoted
29
+ * alternatives, whose quotes carry no escapes.
30
+ */
31
+ export declare function expandBraceChars(chars: BraceChar[], limit: number): BraceExpansion;
32
+ //# sourceMappingURL=braces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"braces.d.ts","sourceRoot":"","sources":["../src/braces.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,kBAAkB,CAAC;AAEhE,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,6EAA6E;AAC7E,eAAO,MAAM,WAAW,KAAK,CAAC;AAE9B,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAqHxD,sDAAsD;AACtD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,GAAG,MAAM,EACxB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,cAAc,CAEhB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,EAAE,EAClB,KAAK,EAAE,MAAM,GACZ,cAAc,CAShB"}
package/dist/braces.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Brace expansion, bash's comma form: `{a,b}` → `a`, `b`; adjacent text
3
+ * distributes; groups multiply left to right and nest. Text that merely
4
+ * contains braces is not changed: a group needs an unquoted comma at its own
5
+ * level, so `{}`, `{a}` and an unclosed `{` stay literal. Sequence
6
+ * expressions (`{1..5}`) are not supported and stay literal too.
7
+ */
8
+ import { segmentValue } from "./shell-words.js";
9
+ /** Fail rather than cut, the way a query with too many terms is rejected. */
10
+ export const BRACE_LIMIT = 64;
11
+ /** Quoted and escaped characters are literal; only bare ones can group. */
12
+ function charsOf(word) {
13
+ if (typeof word === "string") {
14
+ return Array.from(word, (ch) => ({ ch, active: true }));
15
+ }
16
+ const chars = [];
17
+ for (const segment of word.segments) {
18
+ const active = segment.quoted === "none";
19
+ for (const ch of segmentValue(word, segment))
20
+ chars.push({ ch, active });
21
+ }
22
+ return chars;
23
+ }
24
+ const isActive = (chars, i, ch) => chars[i]?.active === true && chars[i]?.ch === ch;
25
+ /**
26
+ * Pairs each bare `{` with its `}`, then keeps the pairs that have a bare
27
+ * comma at their own level. Returns open index → close index, plus the
28
+ * own-level commas of each group.
29
+ */
30
+ function findGroups(chars) {
31
+ const pairs = [];
32
+ const stack = [];
33
+ for (let i = 0; i < chars.length; i += 1) {
34
+ if (isActive(chars, i, "{"))
35
+ stack.push(i);
36
+ else if (isActive(chars, i, "}") && stack.length > 0) {
37
+ pairs.push({ open: stack.pop(), close: i });
38
+ }
39
+ }
40
+ const closeOf = new Map(pairs.map((p) => [p.open, p.close]));
41
+ const groups = new Map();
42
+ for (const { open, close } of pairs) {
43
+ const commas = [];
44
+ for (let i = open + 1; i < close; i += 1) {
45
+ const nested = closeOf.get(i);
46
+ if (nested !== undefined) {
47
+ i = nested;
48
+ continue;
49
+ }
50
+ if (isActive(chars, i, ","))
51
+ commas.push(i);
52
+ }
53
+ if (commas.length > 0)
54
+ groups.set(open, { close, commas });
55
+ }
56
+ return groups;
57
+ }
58
+ class OverLimit extends Error {
59
+ }
60
+ /**
61
+ * Every kept group has at least two alternatives, so nesting deeper than the
62
+ * limit already expands past it; failing here keeps the recursion shallow.
63
+ * The 256 cap protects the stack from a large custom limit.
64
+ */
65
+ function parse(chars, groups, from, to, depth, limit) {
66
+ if (depth > Math.min(limit, 256))
67
+ throw new OverLimit();
68
+ const items = [];
69
+ let text = "";
70
+ for (let i = from; i < to; i += 1) {
71
+ const group = groups.get(i);
72
+ if (group === undefined) {
73
+ text += chars[i].ch;
74
+ continue;
75
+ }
76
+ if (text !== "")
77
+ items.push(text);
78
+ text = "";
79
+ const bounds = [i, ...group.commas, group.close];
80
+ const alternatives = [];
81
+ for (let b = 0; b + 1 < bounds.length; b += 1) {
82
+ alternatives.push(parse(chars, groups, bounds[b] + 1, bounds[b + 1], depth + 1, limit));
83
+ }
84
+ items.push({ alternatives });
85
+ i = group.close;
86
+ }
87
+ if (text !== "")
88
+ items.push(text);
89
+ return items;
90
+ }
91
+ function expand(items, limit) {
92
+ let words = [""];
93
+ for (const item of items) {
94
+ if (typeof item === "string") {
95
+ words = words.map((w) => w + item);
96
+ continue;
97
+ }
98
+ const tails = item.alternatives.flatMap((alt) => expand(alt, limit));
99
+ if (tails.length > limit || words.length * tails.length > limit) {
100
+ throw new OverLimit();
101
+ }
102
+ // Prefix-major, which is bash's order: `{a,b}{c,d}` → ac ad bc bd.
103
+ words = words.flatMap((w) => tails.map((t) => w + t));
104
+ }
105
+ return words;
106
+ }
107
+ /** A plain string is treated as entirely unquoted. */
108
+ export function expandBraces(word, options) {
109
+ return expandBraceChars(charsOf(word), options?.limit ?? BRACE_LIMIT);
110
+ }
111
+ /**
112
+ * For readings that are not shell words, such as plain mode's quoted
113
+ * alternatives, whose quotes carry no escapes.
114
+ */
115
+ export function expandBraceChars(chars, limit) {
116
+ const groups = findGroups(chars);
117
+ try {
118
+ const items = parse(chars, groups, 0, chars.length, 0, limit);
119
+ return { ok: true, words: expand(items, limit) };
120
+ }
121
+ catch (error) {
122
+ if (!(error instanceof OverLimit))
123
+ throw error;
124
+ return { ok: false, reason: `expands to more than ${limit} values` };
125
+ }
126
+ }
127
+ //# sourceMappingURL=braces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"braces.js","sourceRoot":"","sources":["../src/braces.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAkB,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAMhE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAO9B,2EAA2E;AAC3E,SAAS,OAAO,CAAC,IAAwB;IACvC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC;QACzC,KAAK,MAAM,EAAE,IAAI,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,KAAa,EAAE,CAAS,EAAE,EAAU,EAAW,EAAE,CACjE,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;AAEnD;;;;GAIG;AACH,SAAS,UAAU,CACjB,KAAa;IAEb,MAAM,KAAK,GAAsC,EAAE,CAAC;IACpD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;aACtC,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,EAAY,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,IAAI,GAAG,EAA+C,CAAC;IACtE,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,KAAK,EAAE,CAAC;QACpC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,CAAC,GAAG,MAAM,CAAC;gBACX,SAAS;YACX,CAAC;YACD,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,SAAU,SAAQ,KAAK;CAAG;AAEhC;;;;GAIG;AACH,SAAS,KAAK,CACZ,KAAa,EACb,MAAwD,EACxD,IAAY,EACZ,EAAU,EACV,KAAa,EACb,KAAa;IAEb,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC;QAAE,MAAM,IAAI,SAAS,EAAE,CAAC;IACxD,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,IAAK,KAAK,CAAC,CAAC,CAAU,CAAC,EAAE,CAAC;YAC9B,SAAS;QACX,CAAC;QACD,IAAI,IAAI,KAAK,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,GAAG,EAAE,CAAC;QACV,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,YAAY,GAAa,EAAE,CAAC;QAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9C,YAAY,CAAC,IAAI,CACf,KAAK,CACH,KAAK,EACL,MAAM,EACL,MAAM,CAAC,CAAC,CAAY,GAAG,CAAC,EACzB,MAAM,CAAC,CAAC,GAAG,CAAC,CAAW,EACvB,KAAK,GAAG,CAAC,EACT,KAAK,CACN,CACF,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC;QAC7B,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;IAClB,CAAC;IACD,IAAI,IAAI,KAAK,EAAE;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,MAAM,CAAC,KAAa,EAAE,KAAa;IAC1C,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;YACnC,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QACrE,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,KAAK,EAAE,CAAC;YAChE,MAAM,IAAI,SAAS,EAAE,CAAC;QACxB,CAAC;QACD,mEAAmE;QACnE,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,YAAY,CAC1B,IAAwB,EACxB,OAA4B;IAE5B,OAAO,gBAAgB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,IAAI,WAAW,CAAC,CAAC;AACxE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAkB,EAClB,KAAa;IAEb,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;QAC9D,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;IACnD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,SAAS,CAAC;YAAE,MAAM,KAAK,CAAC;QAC/C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,wBAAwB,KAAK,SAAS,EAAE,CAAC;IACvE,CAAC;AACH,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Slash commands (T-161), the text half. A command is an ordinary draft line
3
+ * that happens to parse: it lives in the document, deleting the line cancels
4
+ * it, and typing `/close` by hand is exactly as good as picking it from the
5
+ * panel — the panel is input assistance, not the carrier. Submitting strips
6
+ * the recognized lines out of the body and compiles each one to the caller's
7
+ * own value.
8
+ *
9
+ * Everything here is pure so both an editor (highlighting, the panel) and
10
+ * the caller (button label, submit) read the same verdict, and a server or a
11
+ * CLI can read a draft without an editor at all.
12
+ */
13
+ import { type ShellWord } from "./shell-words.ts";
14
+ import { type ValueProviders, type ValueSpec, type ValueVerdict } from "./values.ts";
15
+ export type ArgSpec = {
16
+ /** Shown in the panel: `<label>`. */
17
+ name: string;
18
+ value: ValueSpec;
19
+ /** Only trailing arguments may be optional. */
20
+ optional?: boolean;
21
+ /** The last argument may take the rest of the line. */
22
+ rest?: boolean;
23
+ /** Brace expansion applies, and the argument is a list. */
24
+ multiple?: boolean;
25
+ };
26
+ /** `string` for a plain argument, `readonly string[]` for a `multiple` one. */
27
+ export type ArgValue = string | readonly string[];
28
+ /**
29
+ * `args` handed to `summarize`, `compile` and `invalidReason` are positional:
30
+ * an optional argument that was not typed is absent, so the array is shorter.
31
+ */
32
+ export type CommandDef<T> = {
33
+ /** The word after the slash; must match `/^[a-z0-9-]+$/`. */
34
+ name: string;
35
+ /** One-line panel description, e.g. "→ Done". */
36
+ detail: string;
37
+ args: readonly ArgSpec[];
38
+ /** Summary for the submit button, e.g. "close" or "label area:web". */
39
+ summarize: (args: readonly ArgValue[]) => string;
40
+ /** Null when the arguments name nothing that exists. */
41
+ compile: (args: readonly ArgValue[]) => T | null;
42
+ /** Why these arguments compiled to nothing; a generic message otherwise. */
43
+ invalidReason?: (args: readonly ArgValue[]) => string;
44
+ };
45
+ export type CommandRegistry<T> = {
46
+ commands: readonly CommandDef<T>[];
47
+ byName: ReadonlyMap<string, CommandDef<T>>;
48
+ };
49
+ /**
50
+ * Throws on a name no line could ever spell and on a name given twice, which
51
+ * would silently shadow the first: both are mistakes in the caller's list,
52
+ * not in user input. So are a required argument after an optional one and a
53
+ * rest argument that is not last, since no line could reach them.
54
+ */
55
+ export declare function createCommandRegistry<T>(commands: readonly CommandDef<T>[]): CommandRegistry<T>;
56
+ /**
57
+ * `In Progress` → `in-progress`. Lowercase, whitespace folded to hyphens,
58
+ * everything a slash command cannot carry dropped: a status named `Won't
59
+ * fix` still gets a typeable command.
60
+ */
61
+ export declare function slugifyCommandName(name: string): string;
62
+ export type RecognizedLine<T> = {
63
+ command: CommandDef<T>;
64
+ /**
65
+ * One word per argument typed, in the order of `args`, with offsets in the
66
+ * line. An unquoted rest argument is one word over the raw remainder.
67
+ */
68
+ words: ShellWord[];
69
+ args: ArgValue[];
70
+ /**
71
+ * Each argument's verdict; a `multiple` one has the worst among its
72
+ * values, and an unclosed quote or a failed expansion is `invalid`.
73
+ */
74
+ verdicts: ValueVerdict[];
75
+ /** Null: recognized, but broken. */
76
+ compiled: T | null;
77
+ /** Why it is broken. */
78
+ reason?: string;
79
+ };
80
+ /** A rest argument's remainder, read with no quoting at all. */
81
+ export declare function rawWord(text: string, start: number): ShellWord;
82
+ /**
83
+ * Does this line, on its own, spell a command? Only whole lines count: a `/`
84
+ * mid-sentence is prose, and prose is the default — an unknown `/clsoe` is
85
+ * left exactly where it was typed rather than guessed at.
86
+ *
87
+ * A recognized line is broken, and `compile` is not called, when a quote is
88
+ * left open, a brace expansion goes over the limit, or a value has an
89
+ * `invalid` verdict. Otherwise `compile` runs and stays the authority.
90
+ */
91
+ export declare function recognizeCommandLine<T>(line: string, registry: CommandRegistry<T>, providers?: ValueProviders): RecognizedLine<T> | null;
92
+ export type ParsedDraft<T> = {
93
+ /** The draft with every recognized command line removed. */
94
+ body: string;
95
+ commands: T[];
96
+ /** Recognized but broken — an argument that names nothing, say. */
97
+ invalid: {
98
+ line: string;
99
+ reason: string;
100
+ }[];
101
+ /** Submit-button material: one summary per recognized command, in order. */
102
+ summaries: string[];
103
+ };
104
+ /** Line indices (0-based) that carry a recognized command. */
105
+ export declare function commandLinesOf<T>(text: string, registry: CommandRegistry<T>, providers?: ValueProviders): Map<number, RecognizedLine<T>>;
106
+ export declare function parseCommandLines<T>(text: string, registry: CommandRegistry<T>, providers?: ValueProviders): ParsedDraft<T>;
107
+ /** "close" / "close and label bug" — the tail of the submit button's label. */
108
+ export declare function summarizeCommands(summaries: readonly string[]): string;
109
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}