@wildwinter/toolkit 0.2.3 → 0.2.4
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/CHANGELOG.md +9 -0
- package/README.md +14 -0
- package/dist/index.cjs +39 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @wildwinter/toolkit
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Find by field value**: `parseFieldQuery`, `fieldNameMatches`, `fieldValueMatches` and `fieldValueText`, so the
|
|
8
|
+
Field tabs in Storyletter's and Patterpad's Find windows read `mood = tense` by one rule. A value matches by its
|
|
9
|
+
field's type (text, enum and quality without case, flags by membership, numbers numerically, booleans by word),
|
|
10
|
+
`!=` negates, and a bare field name matches any value.
|
|
11
|
+
|
|
3
12
|
## 0.2.3
|
|
4
13
|
|
|
5
14
|
### Added
|
package/README.md
CHANGED
|
@@ -73,6 +73,20 @@ matcher, or `null` for an empty query. The query is **escaped**: somebody typing
|
|
|
73
73
|
`a.b` means those three characters, not "a, anything, b". Always global, so
|
|
74
74
|
every occurrence in a string is replaced rather than only the first.
|
|
75
75
|
|
|
76
|
+
Find by field value, the Field tab both families' Find windows have. Each app
|
|
77
|
+
walks its own project for the values; these say what a query means, so
|
|
78
|
+
`mood = tense` reads the same in both:
|
|
79
|
+
|
|
80
|
+
- `parseFieldQuery(query)`: `mood`, `mood = tense`, `mood != tense`,
|
|
81
|
+
`ext.field = "two words"`, or `null` when it is not a field name.
|
|
82
|
+
- `fieldNameMatches(name, query)`: names compare without case.
|
|
83
|
+
- `fieldValueMatches(type, value, query)`: by the field's type. Text, enum and
|
|
84
|
+
quality compare without case, flags match when the list includes the flag
|
|
85
|
+
(`= ""` is none set), numbers numerically, booleans as `true` or `false`.
|
|
86
|
+
`!=` is the opposite, and no operator matches any value. Pass the value that
|
|
87
|
+
holds, the default where the node sets none.
|
|
88
|
+
- `fieldValueText(type, value)`: the value as a row shows it.
|
|
89
|
+
|
|
76
90
|
## Archive
|
|
77
91
|
|
|
78
92
|
A separate, node-only entry point, because it imports `node:path`:
|
package/dist/index.cjs
CHANGED
|
@@ -48,10 +48,49 @@ function findMatcher(opts) {
|
|
|
48
48
|
const body = opts.wholeWord ? `\\b${esc}\\b` : esc;
|
|
49
49
|
return new RegExp(body, opts.caseSensitive ? "g" : "gi");
|
|
50
50
|
}
|
|
51
|
+
function parseFieldQuery(query) {
|
|
52
|
+
const m = /^\s*([a-z_][\w-]*(?:\.[a-z_][\w-]*){0,2})\s*(?:(!=|=)\s*(.*?))?\s*$/i.exec(query);
|
|
53
|
+
if (!m) return null;
|
|
54
|
+
const field = m[1].toLowerCase();
|
|
55
|
+
if (m[2] === void 0) return { field };
|
|
56
|
+
let value = m[3];
|
|
57
|
+
const q = /^(["'])(.*)\1$/.exec(value);
|
|
58
|
+
if (q) value = q[2];
|
|
59
|
+
return { field, op: m[2], value };
|
|
60
|
+
}
|
|
61
|
+
var fieldNameMatches = (name, query) => name.toLowerCase() === query.field;
|
|
62
|
+
function fieldValueMatches(type, value, query) {
|
|
63
|
+
if (query.op === void 0 || query.value === void 0) return true;
|
|
64
|
+
const holds = valueIs(type, value, query.value);
|
|
65
|
+
return query.op === "=" ? holds : !holds;
|
|
66
|
+
}
|
|
67
|
+
function valueIs(type, value, wanted) {
|
|
68
|
+
const want = wanted.trim().toLowerCase();
|
|
69
|
+
if (type === "flags") {
|
|
70
|
+
const flags = Array.isArray(value) ? value.map((f) => String(f).toLowerCase()) : [];
|
|
71
|
+
return want === "" ? flags.length === 0 : flags.includes(want);
|
|
72
|
+
}
|
|
73
|
+
if (type === "number") {
|
|
74
|
+
if (want === "") return value === void 0 || value === null || value === "";
|
|
75
|
+
const n = Number(want);
|
|
76
|
+
return !Number.isNaN(n) && typeof value === "number" && value === n;
|
|
77
|
+
}
|
|
78
|
+
if (type === "boolean") return value === true && want === "true" || value === false && want === "false";
|
|
79
|
+
return fieldValueText(type, value).trim().toLowerCase() === want;
|
|
80
|
+
}
|
|
81
|
+
function fieldValueText(type, value) {
|
|
82
|
+
if (value === void 0 || value === null) return "";
|
|
83
|
+
if (type === "flags" && Array.isArray(value)) return value.join(", ");
|
|
84
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
85
|
+
}
|
|
51
86
|
|
|
87
|
+
exports.fieldNameMatches = fieldNameMatches;
|
|
88
|
+
exports.fieldValueMatches = fieldValueMatches;
|
|
89
|
+
exports.fieldValueText = fieldValueText;
|
|
52
90
|
exports.findMatcher = findMatcher;
|
|
53
91
|
exports.fnv32 = fnv32;
|
|
54
92
|
exports.newId = newId;
|
|
93
|
+
exports.parseFieldQuery = parseFieldQuery;
|
|
55
94
|
exports.parseSource = parseSource;
|
|
56
95
|
exports.slug = slug;
|
|
57
96
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ids.ts","../src/hash.ts","../src/source.ts","../src/find.ts"],"names":["JSON5"],"mappings":";;;;;;;;;AASA,IAAM,QAAA,GAAW,sCAAA;AAGV,SAAS,KAAA,CAAM,MAAA,GAAS,EAAA,EAAI,MAAA,GAAS,CAAA,EAAW;AAGrD,EAAA,MAAM,KAAA,GAAQ,GAAA,GAAO,GAAA,GAAM,QAAA,CAAS,MAAA;AACpC,EAAA,IAAI,KAAA,GAAQ,EAAA;AACZ,EAAA,OAAO,KAAA,CAAM,SAAS,MAAA,EAAQ;AAC5B,IAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAA,GAAS,CAAC,CAAA;AACvC,IAAA,UAAA,CAAW,MAAA,CAAO,gBAAgB,KAAK,CAAA;AACvC,IAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,MAAA,IAAI,KAAK,KAAA,EAAO;AAChB,MAAA,KAAA,IAAS,QAAA,CAAS,CAAA,GAAI,QAAA,CAAS,MAAM,CAAA;AACrC,MAAA,IAAI,KAAA,CAAM,WAAW,MAAA,EAAQ;AAAA,IAC/B;AAAA,EACF;AACA,EAAA,OAAO,MAAA,GAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,GAAK,KAAA;AACzC;AAGO,SAAS,IAAA,CAAK,IAAA,EAAc,QAAA,GAAW,SAAA,EAAmB;AAC/D,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAY,CAAE,OAAA,CAAQ,aAAA,EAAe,GAAG,CAAA,CAAE,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA;AACtF,EAAA,OAAO,CAAA,IAAK,QAAA;AACd;;;ACxBO,SAAS,MAAM,KAAA,EAAuB;AAC3C,EAAA,IAAI,CAAA,GAAI,UAAA;AACR,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,CAAA,IAAK,KAAA,CAAM,WAAW,CAAC,CAAA;AACvB,IAAA,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,CAAA,EAAG,QAAU,CAAA;AAAA,EAC7B;AACA,EAAA,OAAO,CAAA,KAAM,CAAA;AACf;ACLO,SAAS,YAAY,IAAA,EAAuB;AACjD,EAAA,MAAM,IAAA,GAAO,KAAK,UAAA,CAAW,CAAC,MAAM,KAAA,GAAS,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,GAAI,IAAA;AAC7D,EAAA,OAAOA,sBAAA,CAAM,MAAM,IAAI,CAAA;AACzB;;;ACGO,SAAS,YAAY,IAAA,EAAkC;AAC5D,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,OAAO,IAAA;AACxB,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,OAAA,CAAQ,uBAAuB,MAAM,CAAA;AAC5D,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,GAAY,CAAA,GAAA,EAAM,GAAG,CAAA,GAAA,CAAA,GAAQ,GAAA;AAC/C,EAAA,OAAO,IAAI,MAAA,CAAO,IAAA,EAAM,IAAA,CAAK,aAAA,GAAgB,MAAM,IAAI,CAAA;AACzD","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Stable, immutable, opaque ids.\n//\n// Generated at creation, never derived from content or position, so an id\n// survives moving, renaming and reordering the thing it names. A short,\n// collision-resistant base-36 token with an optional type prefix that aids\n// debugging but carries no meaning.\n// ---------------------------------------------------------------------------\n\nconst ALPHABET = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\n/** Generate a new opaque id, e.g. `newId(\"scn\") -> \"scn_8f3kq2z1\"`. */\nexport function newId(prefix = \"\", length = 8): string {\n // Rejection-sample so every alphabet character is equally likely: a plain\n // `byte % 36` over-weights the first four characters.\n const limit = 256 - (256 % ALPHABET.length); // 252\n let token = \"\";\n while (token.length < length) {\n const bytes = new Uint8Array(length * 2);\n globalThis.crypto.getRandomValues(bytes);\n for (const b of bytes) {\n if (b >= limit) continue;\n token += ALPHABET[b % ALPHABET.length];\n if (token.length === length) break;\n }\n }\n return prefix ? `${prefix}_${token}` : token;\n}\n\n/** A url-ish slug from a human label; `\"project\"` when nothing usable is left. */\nexport function slug(name: string, fallback = \"project\"): string {\n const s = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n return s || fallback;\n}\n","// ---------------------------------------------------------------------------\n// FNV-1a, 32-bit.\n//\n// A published, fixed algorithm. Not cryptographic: it is for short stable\n// handles and change detection, where the properties that matter are that it is\n// the same everywhere and does not move between runs.\n// ---------------------------------------------------------------------------\n\n/** FNV-1a over a string's UTF-16 code units, as an unsigned 32-bit integer. */\nexport function fnv32(input: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n","// ---------------------------------------------------------------------------\n// Reading authored source files.\n// ---------------------------------------------------------------------------\n\nimport JSON5 from \"json5\";\n\n/**\n * Parse JSON5 source text. A leading byte-order mark is tolerated, because\n * editors on Windows add one and JSON5 will not accept it. Throws on malformed\n * input.\n */\nexport function parseSource(text: string): unknown {\n const body = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n return JSON5.parse(body);\n}\n","// ---------------------------------------------------------------------------\n// Find and replace over authored text.\n// ---------------------------------------------------------------------------\n\nexport interface FindOptions {\n query: string;\n caseSensitive?: boolean;\n wholeWord?: boolean;\n}\n\n/**\n * Build the find matcher, or null for an empty query. Global, so every\n * occurrence in a string is replaced rather than only the first.\n *\n * The query is escaped: a person typing `a.b` means those three characters, not\n * \"a, anything, b\".\n */\nexport function findMatcher(opts: FindOptions): RegExp | null {\n if (!opts.query) return null;\n const esc = opts.query.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const body = opts.wholeWord ? `\\\\b${esc}\\\\b` : esc;\n return new RegExp(body, opts.caseSensitive ? \"g\" : \"gi\");\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/ids.ts","../src/hash.ts","../src/source.ts","../src/find.ts"],"names":["JSON5"],"mappings":"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","file":"index.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Stable, immutable, opaque ids.\n//\n// Generated at creation, never derived from content or position, so an id\n// survives moving, renaming and reordering the thing it names. A short,\n// collision-resistant base-36 token with an optional type prefix that aids\n// debugging but carries no meaning.\n// ---------------------------------------------------------------------------\n\nconst ALPHABET = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\n/** Generate a new opaque id, e.g. `newId(\"scn\") -> \"scn_8f3kq2z1\"`. */\nexport function newId(prefix = \"\", length = 8): string {\n // Rejection-sample so every alphabet character is equally likely: a plain\n // `byte % 36` over-weights the first four characters.\n const limit = 256 - (256 % ALPHABET.length); // 252\n let token = \"\";\n while (token.length < length) {\n const bytes = new Uint8Array(length * 2);\n globalThis.crypto.getRandomValues(bytes);\n for (const b of bytes) {\n if (b >= limit) continue;\n token += ALPHABET[b % ALPHABET.length];\n if (token.length === length) break;\n }\n }\n return prefix ? `${prefix}_${token}` : token;\n}\n\n/** A url-ish slug from a human label; `\"project\"` when nothing usable is left. */\nexport function slug(name: string, fallback = \"project\"): string {\n const s = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n return s || fallback;\n}\n","// ---------------------------------------------------------------------------\n// FNV-1a, 32-bit.\n//\n// A published, fixed algorithm. Not cryptographic: it is for short stable\n// handles and change detection, where the properties that matter are that it is\n// the same everywhere and does not move between runs.\n// ---------------------------------------------------------------------------\n\n/** FNV-1a over a string's UTF-16 code units, as an unsigned 32-bit integer. */\nexport function fnv32(input: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n","// ---------------------------------------------------------------------------\n// Reading authored source files.\n// ---------------------------------------------------------------------------\n\nimport JSON5 from \"json5\";\n\n/**\n * Parse JSON5 source text. A leading byte-order mark is tolerated, because\n * editors on Windows add one and JSON5 will not accept it. Throws on malformed\n * input.\n */\nexport function parseSource(text: string): unknown {\n const body = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n return JSON5.parse(body);\n}\n","// ---------------------------------------------------------------------------\n// Find and replace over authored text.\n// ---------------------------------------------------------------------------\n\nexport interface FindOptions {\n query: string;\n caseSensitive?: boolean;\n wholeWord?: boolean;\n}\n\n/**\n * Build the find matcher, or null for an empty query. Global, so every\n * occurrence in a string is replaced rather than only the first.\n *\n * The query is escaped: a person typing `a.b` means those three characters, not\n * \"a, anything, b\".\n */\nexport function findMatcher(opts: FindOptions): RegExp | null {\n if (!opts.query) return null;\n const esc = opts.query.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const body = opts.wholeWord ? `\\\\b${esc}\\\\b` : esc;\n return new RegExp(body, opts.caseSensitive ? \"g\" : \"gi\");\n}\n\n// ---------------------------------------------------------------------------\n// Find by field value: the Field tab of both families' Find windows\n// (patterkit design/family-parity-2026-10.md item 16). Each app walks its own\n// project for the values (Storylets' card and outcome fields, Patter's\n// gameData) and asks these two what a query means, so `mood = tense` reads the\n// same in both.\n// ---------------------------------------------------------------------------\n\n/** A parsed Field query: a field, and optionally a value it must (`=`) or must\n * not (`!=`) hold. With no `op` the query lists every value of the field. */\nexport interface FieldQuery {\n /** As typed, folded to lower case: `mood`, or `extension.field`. */\n field: string;\n op?: \"=\" | \"!=\";\n /** As typed, trimmed, one pair of surrounding quotes taken off. May be `\"\"`\n * (`mood =` asks for the field left empty). */\n value?: string;\n}\n\n/**\n * `mood`, `mood = tense`, `mood != tense`, `ext.field = \"two words\"`. Null when\n * the text before the operator is not a field name (a letter or `_`, then\n * letters, digits, `_` or `-`; up to three of those joined by dots).\n */\nexport function parseFieldQuery(query: string): FieldQuery | null {\n const m = /^\\s*([a-z_][\\w-]*(?:\\.[a-z_][\\w-]*){0,2})\\s*(?:(!=|=)\\s*(.*?))?\\s*$/i.exec(query);\n if (!m) return null;\n const field = m[1]!.toLowerCase();\n if (m[2] === undefined) return { field };\n let value = m[3]!;\n const q = /^([\"'])(.*)\\1$/.exec(value);\n if (q) value = q[2]!;\n return { field, op: m[2] as \"=\" | \"!=\", value };\n}\n\n/** Does this field name answer the query's? Names compare without case. */\nexport const fieldNameMatches = (name: string, query: FieldQuery): boolean => name.toLowerCase() === query.field;\n\n/**\n * Does a field's value satisfy the query, read by its type (the family's list:\n * text, multiline, number, boolean, enum, flags, quality; `string` is text)?\n * Pass the value that HOLDS: the default when the node sets none, so\n * `mood = calm` finds the cards that leave a calm mood alone.\n *\n * - text, multiline, enum, quality, and any type this does not know: equal,\n * ignoring case and the ends' whitespace\n * - flags: the list includes that flag (`= \"\"` is no flags set)\n * - number: numerically equal (`= 1.0` finds 1)\n * - boolean: `true` or `false`, ignoring case\n *\n * `!=` is the opposite of `=`. A query with no operator matches any value.\n */\nexport function fieldValueMatches(type: string, value: unknown, query: FieldQuery): boolean {\n if (query.op === undefined || query.value === undefined) return true;\n const holds = valueIs(type, value, query.value);\n return query.op === \"=\" ? holds : !holds;\n}\n\nfunction valueIs(type: string, value: unknown, wanted: string): boolean {\n const want = wanted.trim().toLowerCase();\n if (type === \"flags\") {\n const flags = Array.isArray(value) ? value.map((f) => String(f).toLowerCase()) : [];\n return want === \"\" ? flags.length === 0 : flags.includes(want);\n }\n if (type === \"number\") {\n if (want === \"\") return value === undefined || value === null || value === \"\";\n const n = Number(want);\n return !Number.isNaN(n) && typeof value === \"number\" && value === n;\n }\n if (type === \"boolean\") return (value === true && want === \"true\") || (value === false && want === \"false\");\n return fieldValueText(type, value).trim().toLowerCase() === want;\n}\n\n/** A field's value as a row shows it: flags joined with commas, everything\n * else as written, nothing for no value. */\nexport function fieldValueText(type: string, value: unknown): string {\n if (value === undefined || value === null) return \"\";\n if (type === \"flags\" && Array.isArray(value)) return value.join(\", \");\n return typeof value === \"string\" ? value : JSON.stringify(value);\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -26,5 +26,41 @@ interface FindOptions {
|
|
|
26
26
|
* "a, anything, b".
|
|
27
27
|
*/
|
|
28
28
|
declare function findMatcher(opts: FindOptions): RegExp | null;
|
|
29
|
+
/** A parsed Field query: a field, and optionally a value it must (`=`) or must
|
|
30
|
+
* not (`!=`) hold. With no `op` the query lists every value of the field. */
|
|
31
|
+
interface FieldQuery {
|
|
32
|
+
/** As typed, folded to lower case: `mood`, or `extension.field`. */
|
|
33
|
+
field: string;
|
|
34
|
+
op?: "=" | "!=";
|
|
35
|
+
/** As typed, trimmed, one pair of surrounding quotes taken off. May be `""`
|
|
36
|
+
* (`mood =` asks for the field left empty). */
|
|
37
|
+
value?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `mood`, `mood = tense`, `mood != tense`, `ext.field = "two words"`. Null when
|
|
41
|
+
* the text before the operator is not a field name (a letter or `_`, then
|
|
42
|
+
* letters, digits, `_` or `-`; up to three of those joined by dots).
|
|
43
|
+
*/
|
|
44
|
+
declare function parseFieldQuery(query: string): FieldQuery | null;
|
|
45
|
+
/** Does this field name answer the query's? Names compare without case. */
|
|
46
|
+
declare const fieldNameMatches: (name: string, query: FieldQuery) => boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Does a field's value satisfy the query, read by its type (the family's list:
|
|
49
|
+
* text, multiline, number, boolean, enum, flags, quality; `string` is text)?
|
|
50
|
+
* Pass the value that HOLDS: the default when the node sets none, so
|
|
51
|
+
* `mood = calm` finds the cards that leave a calm mood alone.
|
|
52
|
+
*
|
|
53
|
+
* - text, multiline, enum, quality, and any type this does not know: equal,
|
|
54
|
+
* ignoring case and the ends' whitespace
|
|
55
|
+
* - flags: the list includes that flag (`= ""` is no flags set)
|
|
56
|
+
* - number: numerically equal (`= 1.0` finds 1)
|
|
57
|
+
* - boolean: `true` or `false`, ignoring case
|
|
58
|
+
*
|
|
59
|
+
* `!=` is the opposite of `=`. A query with no operator matches any value.
|
|
60
|
+
*/
|
|
61
|
+
declare function fieldValueMatches(type: string, value: unknown, query: FieldQuery): boolean;
|
|
62
|
+
/** A field's value as a row shows it: flags joined with commas, everything
|
|
63
|
+
* else as written, nothing for no value. */
|
|
64
|
+
declare function fieldValueText(type: string, value: unknown): string;
|
|
29
65
|
|
|
30
|
-
export { type FindOptions, findMatcher, fnv32, newId, parseSource, slug };
|
|
66
|
+
export { type FieldQuery, type FindOptions, fieldNameMatches, fieldValueMatches, fieldValueText, findMatcher, fnv32, newId, parseFieldQuery, parseSource, slug };
|
package/dist/index.d.ts
CHANGED
|
@@ -26,5 +26,41 @@ interface FindOptions {
|
|
|
26
26
|
* "a, anything, b".
|
|
27
27
|
*/
|
|
28
28
|
declare function findMatcher(opts: FindOptions): RegExp | null;
|
|
29
|
+
/** A parsed Field query: a field, and optionally a value it must (`=`) or must
|
|
30
|
+
* not (`!=`) hold. With no `op` the query lists every value of the field. */
|
|
31
|
+
interface FieldQuery {
|
|
32
|
+
/** As typed, folded to lower case: `mood`, or `extension.field`. */
|
|
33
|
+
field: string;
|
|
34
|
+
op?: "=" | "!=";
|
|
35
|
+
/** As typed, trimmed, one pair of surrounding quotes taken off. May be `""`
|
|
36
|
+
* (`mood =` asks for the field left empty). */
|
|
37
|
+
value?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `mood`, `mood = tense`, `mood != tense`, `ext.field = "two words"`. Null when
|
|
41
|
+
* the text before the operator is not a field name (a letter or `_`, then
|
|
42
|
+
* letters, digits, `_` or `-`; up to three of those joined by dots).
|
|
43
|
+
*/
|
|
44
|
+
declare function parseFieldQuery(query: string): FieldQuery | null;
|
|
45
|
+
/** Does this field name answer the query's? Names compare without case. */
|
|
46
|
+
declare const fieldNameMatches: (name: string, query: FieldQuery) => boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Does a field's value satisfy the query, read by its type (the family's list:
|
|
49
|
+
* text, multiline, number, boolean, enum, flags, quality; `string` is text)?
|
|
50
|
+
* Pass the value that HOLDS: the default when the node sets none, so
|
|
51
|
+
* `mood = calm` finds the cards that leave a calm mood alone.
|
|
52
|
+
*
|
|
53
|
+
* - text, multiline, enum, quality, and any type this does not know: equal,
|
|
54
|
+
* ignoring case and the ends' whitespace
|
|
55
|
+
* - flags: the list includes that flag (`= ""` is no flags set)
|
|
56
|
+
* - number: numerically equal (`= 1.0` finds 1)
|
|
57
|
+
* - boolean: `true` or `false`, ignoring case
|
|
58
|
+
*
|
|
59
|
+
* `!=` is the opposite of `=`. A query with no operator matches any value.
|
|
60
|
+
*/
|
|
61
|
+
declare function fieldValueMatches(type: string, value: unknown, query: FieldQuery): boolean;
|
|
62
|
+
/** A field's value as a row shows it: flags joined with commas, everything
|
|
63
|
+
* else as written, nothing for no value. */
|
|
64
|
+
declare function fieldValueText(type: string, value: unknown): string;
|
|
29
65
|
|
|
30
|
-
export { type FindOptions, findMatcher, fnv32, newId, parseSource, slug };
|
|
66
|
+
export { type FieldQuery, type FindOptions, fieldNameMatches, fieldValueMatches, fieldValueText, findMatcher, fnv32, newId, parseFieldQuery, parseSource, slug };
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,42 @@ function findMatcher(opts) {
|
|
|
38
38
|
const body = opts.wholeWord ? `\\b${esc}\\b` : esc;
|
|
39
39
|
return new RegExp(body, opts.caseSensitive ? "g" : "gi");
|
|
40
40
|
}
|
|
41
|
+
function parseFieldQuery(query) {
|
|
42
|
+
const m = /^\s*([a-z_][\w-]*(?:\.[a-z_][\w-]*){0,2})\s*(?:(!=|=)\s*(.*?))?\s*$/i.exec(query);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
const field = m[1].toLowerCase();
|
|
45
|
+
if (m[2] === void 0) return { field };
|
|
46
|
+
let value = m[3];
|
|
47
|
+
const q = /^(["'])(.*)\1$/.exec(value);
|
|
48
|
+
if (q) value = q[2];
|
|
49
|
+
return { field, op: m[2], value };
|
|
50
|
+
}
|
|
51
|
+
var fieldNameMatches = (name, query) => name.toLowerCase() === query.field;
|
|
52
|
+
function fieldValueMatches(type, value, query) {
|
|
53
|
+
if (query.op === void 0 || query.value === void 0) return true;
|
|
54
|
+
const holds = valueIs(type, value, query.value);
|
|
55
|
+
return query.op === "=" ? holds : !holds;
|
|
56
|
+
}
|
|
57
|
+
function valueIs(type, value, wanted) {
|
|
58
|
+
const want = wanted.trim().toLowerCase();
|
|
59
|
+
if (type === "flags") {
|
|
60
|
+
const flags = Array.isArray(value) ? value.map((f) => String(f).toLowerCase()) : [];
|
|
61
|
+
return want === "" ? flags.length === 0 : flags.includes(want);
|
|
62
|
+
}
|
|
63
|
+
if (type === "number") {
|
|
64
|
+
if (want === "") return value === void 0 || value === null || value === "";
|
|
65
|
+
const n = Number(want);
|
|
66
|
+
return !Number.isNaN(n) && typeof value === "number" && value === n;
|
|
67
|
+
}
|
|
68
|
+
if (type === "boolean") return value === true && want === "true" || value === false && want === "false";
|
|
69
|
+
return fieldValueText(type, value).trim().toLowerCase() === want;
|
|
70
|
+
}
|
|
71
|
+
function fieldValueText(type, value) {
|
|
72
|
+
if (value === void 0 || value === null) return "";
|
|
73
|
+
if (type === "flags" && Array.isArray(value)) return value.join(", ");
|
|
74
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
75
|
+
}
|
|
41
76
|
|
|
42
|
-
export { findMatcher, fnv32, newId, slug };
|
|
77
|
+
export { fieldNameMatches, fieldValueMatches, fieldValueText, findMatcher, fnv32, newId, parseFieldQuery, slug };
|
|
43
78
|
//# sourceMappingURL=index.js.map
|
|
44
79
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ids.ts","../src/hash.ts","../src/find.ts"],"names":[],"mappings":";;;AASA,IAAM,QAAA,GAAW,sCAAA;AAGV,SAAS,KAAA,CAAM,MAAA,GAAS,EAAA,EAAI,MAAA,GAAS,CAAA,EAAW;AAGrD,EAAA,MAAM,KAAA,GAAQ,GAAA,GAAO,GAAA,GAAM,QAAA,CAAS,MAAA;AACpC,EAAA,IAAI,KAAA,GAAQ,EAAA;AACZ,EAAA,OAAO,KAAA,CAAM,SAAS,MAAA,EAAQ;AAC5B,IAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAA,GAAS,CAAC,CAAA;AACvC,IAAA,UAAA,CAAW,MAAA,CAAO,gBAAgB,KAAK,CAAA;AACvC,IAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,MAAA,IAAI,KAAK,KAAA,EAAO;AAChB,MAAA,KAAA,IAAS,QAAA,CAAS,CAAA,GAAI,QAAA,CAAS,MAAM,CAAA;AACrC,MAAA,IAAI,KAAA,CAAM,WAAW,MAAA,EAAQ;AAAA,IAC/B;AAAA,EACF;AACA,EAAA,OAAO,MAAA,GAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,GAAK,KAAA;AACzC;AAGO,SAAS,IAAA,CAAK,IAAA,EAAc,QAAA,GAAW,SAAA,EAAmB;AAC/D,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,IAAA,EAAK,CAAE,WAAA,EAAY,CAAE,OAAA,CAAQ,aAAA,EAAe,GAAG,CAAA,CAAE,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA;AACtF,EAAA,OAAO,CAAA,IAAK,QAAA;AACd;;;ACxBO,SAAS,MAAM,KAAA,EAAuB;AAC3C,EAAA,IAAI,CAAA,GAAI,UAAA;AACR,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,CAAA,IAAK,KAAA,CAAM,WAAW,CAAC,CAAA;AACvB,IAAA,CAAA,GAAI,IAAA,CAAK,IAAA,CAAK,CAAA,EAAG,QAAU,CAAA;AAAA,EAC7B;AACA,EAAA,OAAO,CAAA,KAAM,CAAA;AACf;;;ACCO,SAAS,YAAY,IAAA,EAAkC;AAC5D,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,OAAO,IAAA;AACxB,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,OAAA,CAAQ,uBAAuB,MAAM,CAAA;AAC5D,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,GAAY,CAAA,GAAA,EAAM,GAAG,CAAA,GAAA,CAAA,GAAQ,GAAA;AAC/C,EAAA,OAAO,IAAI,MAAA,CAAO,IAAA,EAAM,IAAA,CAAK,aAAA,GAAgB,MAAM,IAAI,CAAA;AACzD","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Stable, immutable, opaque ids.\n//\n// Generated at creation, never derived from content or position, so an id\n// survives moving, renaming and reordering the thing it names. A short,\n// collision-resistant base-36 token with an optional type prefix that aids\n// debugging but carries no meaning.\n// ---------------------------------------------------------------------------\n\nconst ALPHABET = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\n/** Generate a new opaque id, e.g. `newId(\"scn\") -> \"scn_8f3kq2z1\"`. */\nexport function newId(prefix = \"\", length = 8): string {\n // Rejection-sample so every alphabet character is equally likely: a plain\n // `byte % 36` over-weights the first four characters.\n const limit = 256 - (256 % ALPHABET.length); // 252\n let token = \"\";\n while (token.length < length) {\n const bytes = new Uint8Array(length * 2);\n globalThis.crypto.getRandomValues(bytes);\n for (const b of bytes) {\n if (b >= limit) continue;\n token += ALPHABET[b % ALPHABET.length];\n if (token.length === length) break;\n }\n }\n return prefix ? `${prefix}_${token}` : token;\n}\n\n/** A url-ish slug from a human label; `\"project\"` when nothing usable is left. */\nexport function slug(name: string, fallback = \"project\"): string {\n const s = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n return s || fallback;\n}\n","// ---------------------------------------------------------------------------\n// FNV-1a, 32-bit.\n//\n// A published, fixed algorithm. Not cryptographic: it is for short stable\n// handles and change detection, where the properties that matter are that it is\n// the same everywhere and does not move between runs.\n// ---------------------------------------------------------------------------\n\n/** FNV-1a over a string's UTF-16 code units, as an unsigned 32-bit integer. */\nexport function fnv32(input: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n","// ---------------------------------------------------------------------------\n// Find and replace over authored text.\n// ---------------------------------------------------------------------------\n\nexport interface FindOptions {\n query: string;\n caseSensitive?: boolean;\n wholeWord?: boolean;\n}\n\n/**\n * Build the find matcher, or null for an empty query. Global, so every\n * occurrence in a string is replaced rather than only the first.\n *\n * The query is escaped: a person typing `a.b` means those three characters, not\n * \"a, anything, b\".\n */\nexport function findMatcher(opts: FindOptions): RegExp | null {\n if (!opts.query) return null;\n const esc = opts.query.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const body = opts.wholeWord ? `\\\\b${esc}\\\\b` : esc;\n return new RegExp(body, opts.caseSensitive ? \"g\" : \"gi\");\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/ids.ts","../src/hash.ts","../src/find.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Stable, immutable, opaque ids.\n//\n// Generated at creation, never derived from content or position, so an id\n// survives moving, renaming and reordering the thing it names. A short,\n// collision-resistant base-36 token with an optional type prefix that aids\n// debugging but carries no meaning.\n// ---------------------------------------------------------------------------\n\nconst ALPHABET = \"0123456789abcdefghijklmnopqrstuvwxyz\";\n\n/** Generate a new opaque id, e.g. `newId(\"scn\") -> \"scn_8f3kq2z1\"`. */\nexport function newId(prefix = \"\", length = 8): string {\n // Rejection-sample so every alphabet character is equally likely: a plain\n // `byte % 36` over-weights the first four characters.\n const limit = 256 - (256 % ALPHABET.length); // 252\n let token = \"\";\n while (token.length < length) {\n const bytes = new Uint8Array(length * 2);\n globalThis.crypto.getRandomValues(bytes);\n for (const b of bytes) {\n if (b >= limit) continue;\n token += ALPHABET[b % ALPHABET.length];\n if (token.length === length) break;\n }\n }\n return prefix ? `${prefix}_${token}` : token;\n}\n\n/** A url-ish slug from a human label; `\"project\"` when nothing usable is left. */\nexport function slug(name: string, fallback = \"project\"): string {\n const s = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n return s || fallback;\n}\n","// ---------------------------------------------------------------------------\n// FNV-1a, 32-bit.\n//\n// A published, fixed algorithm. Not cryptographic: it is for short stable\n// handles and change detection, where the properties that matter are that it is\n// the same everywhere and does not move between runs.\n// ---------------------------------------------------------------------------\n\n/** FNV-1a over a string's UTF-16 code units, as an unsigned 32-bit integer. */\nexport function fnv32(input: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n","// ---------------------------------------------------------------------------\n// Find and replace over authored text.\n// ---------------------------------------------------------------------------\n\nexport interface FindOptions {\n query: string;\n caseSensitive?: boolean;\n wholeWord?: boolean;\n}\n\n/**\n * Build the find matcher, or null for an empty query. Global, so every\n * occurrence in a string is replaced rather than only the first.\n *\n * The query is escaped: a person typing `a.b` means those three characters, not\n * \"a, anything, b\".\n */\nexport function findMatcher(opts: FindOptions): RegExp | null {\n if (!opts.query) return null;\n const esc = opts.query.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const body = opts.wholeWord ? `\\\\b${esc}\\\\b` : esc;\n return new RegExp(body, opts.caseSensitive ? \"g\" : \"gi\");\n}\n\n// ---------------------------------------------------------------------------\n// Find by field value: the Field tab of both families' Find windows\n// (patterkit design/family-parity-2026-10.md item 16). Each app walks its own\n// project for the values (Storylets' card and outcome fields, Patter's\n// gameData) and asks these two what a query means, so `mood = tense` reads the\n// same in both.\n// ---------------------------------------------------------------------------\n\n/** A parsed Field query: a field, and optionally a value it must (`=`) or must\n * not (`!=`) hold. With no `op` the query lists every value of the field. */\nexport interface FieldQuery {\n /** As typed, folded to lower case: `mood`, or `extension.field`. */\n field: string;\n op?: \"=\" | \"!=\";\n /** As typed, trimmed, one pair of surrounding quotes taken off. May be `\"\"`\n * (`mood =` asks for the field left empty). */\n value?: string;\n}\n\n/**\n * `mood`, `mood = tense`, `mood != tense`, `ext.field = \"two words\"`. Null when\n * the text before the operator is not a field name (a letter or `_`, then\n * letters, digits, `_` or `-`; up to three of those joined by dots).\n */\nexport function parseFieldQuery(query: string): FieldQuery | null {\n const m = /^\\s*([a-z_][\\w-]*(?:\\.[a-z_][\\w-]*){0,2})\\s*(?:(!=|=)\\s*(.*?))?\\s*$/i.exec(query);\n if (!m) return null;\n const field = m[1]!.toLowerCase();\n if (m[2] === undefined) return { field };\n let value = m[3]!;\n const q = /^([\"'])(.*)\\1$/.exec(value);\n if (q) value = q[2]!;\n return { field, op: m[2] as \"=\" | \"!=\", value };\n}\n\n/** Does this field name answer the query's? Names compare without case. */\nexport const fieldNameMatches = (name: string, query: FieldQuery): boolean => name.toLowerCase() === query.field;\n\n/**\n * Does a field's value satisfy the query, read by its type (the family's list:\n * text, multiline, number, boolean, enum, flags, quality; `string` is text)?\n * Pass the value that HOLDS: the default when the node sets none, so\n * `mood = calm` finds the cards that leave a calm mood alone.\n *\n * - text, multiline, enum, quality, and any type this does not know: equal,\n * ignoring case and the ends' whitespace\n * - flags: the list includes that flag (`= \"\"` is no flags set)\n * - number: numerically equal (`= 1.0` finds 1)\n * - boolean: `true` or `false`, ignoring case\n *\n * `!=` is the opposite of `=`. A query with no operator matches any value.\n */\nexport function fieldValueMatches(type: string, value: unknown, query: FieldQuery): boolean {\n if (query.op === undefined || query.value === undefined) return true;\n const holds = valueIs(type, value, query.value);\n return query.op === \"=\" ? holds : !holds;\n}\n\nfunction valueIs(type: string, value: unknown, wanted: string): boolean {\n const want = wanted.trim().toLowerCase();\n if (type === \"flags\") {\n const flags = Array.isArray(value) ? value.map((f) => String(f).toLowerCase()) : [];\n return want === \"\" ? flags.length === 0 : flags.includes(want);\n }\n if (type === \"number\") {\n if (want === \"\") return value === undefined || value === null || value === \"\";\n const n = Number(want);\n return !Number.isNaN(n) && typeof value === \"number\" && value === n;\n }\n if (type === \"boolean\") return (value === true && want === \"true\") || (value === false && want === \"false\");\n return fieldValueText(type, value).trim().toLowerCase() === want;\n}\n\n/** A field's value as a row shows it: flags joined with commas, everything\n * else as written, nothing for no value. */\nexport function fieldValueText(type: string, value: unknown): string {\n if (value === undefined || value === null) return \"\";\n if (type === \"flags\" && Array.isArray(value)) return value.join(\", \");\n return typeof value === \"string\" ? value : JSON.stringify(value);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wildwinter/toolkit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Small shared pieces of the authoring tooling: opaque ids, a stable hash, JSON5 source parsing, find/replace matching, the archive guards, and (in ./extensions) the spec and merge for Patter extensions and Storylet extensions. Nothing here runs in a game.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|