fast-publicodes 0.0.0 → 1.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,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Troopay
4
+
5
+ fast-publicodes is a port of publicodes (https://github.com/publicodes/publicodes),
6
+ distributed under the following notice:
7
+
8
+ Copyright (c) 2018-2021 beta.gouv.fr
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # fast-publicodes
2
+
3
+ A port of [publicodes](https://github.com/publicodes/publicodes) 1.x with its engine rewritten in Rust: the same API and the same behaviour, faster. Node loads it as a native addon, prebuilt for Linux, macOS and Windows; browsers, other runtimes and other platforms load it as WebAssembly, from the same package.
4
+
5
+ > **Not affiliated with the publicodes project.** fast-publicodes is an independent port. The language, its features and every discussion about them belong to [publicodes](https://github.com/publicodes/publicodes): propose features and report language issues there, not here. If publicodes is useful to you, support its authors: star the repository, contribute, and credit them.
6
+
7
+ ## When to use it
8
+
9
+ Use fast-publicodes where evaluating publicodes rules is a bottleneck:
10
+
11
+ - **On a server**: APIs, batch jobs or simulations that evaluate many situations, where each evaluation costs CPU time and latency.
12
+ - **In a client**: large rule sets recomputed on every input, or slower devices, where evaluation delays the interface.
13
+
14
+ Stay with publicodes when its speed is enough for you, or when you need what fast-publicodes does not provide (see [Limitations](#limitations)), such as the explanations publicodes-react renders. In a browser, also weigh the size of the WebAssembly module against the time it saves.
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ npm install fast-publicodes
20
+ ```
21
+
22
+ Then replace the import; nothing else changes.
23
+
24
+ ```ts
25
+ import Engine from "fast-publicodes";
26
+
27
+ const engine = new Engine(rules);
28
+ engine.setSituation({ "salaire brut": 3000 }).evaluate("salaire net");
29
+ ```
30
+
31
+ ## Performance
32
+
33
+ On the [modele-social](https://www.npmjs.com/package/modele-social) rules (`pnpm bench`, Node, one machine):
34
+
35
+ | Operation | publicodes | fast-publicodes | Speed-up |
36
+ | --- | --- | --- | --- |
37
+ | Parse the rules | 366 ms | 68 ms | 5× |
38
+ | Set a situation and evaluate one rule | 23.7 ms | 0.75 ms | 32× |
39
+ | Set a situation and evaluate every rule | 313 ms | 18 ms | 18× |
40
+ | `shallowCopy()` | 6.2 ms | 0.42 ms | 15× |
41
+
42
+ ## Limitations
43
+
44
+ - **No AST.** `evaluate()` returns `nodeValue`, `unit` and `missingVariables` without `explanation`, and rules returned by `getRule()` and `getParsedRules()` carry their metadata without their node tree; their `suggestions` are opaque values that only `evaluate()` reads. `parsePublicodes`, `reduceAST`, `transformAST`, `traverseASTNode`, `utils.cyclicDependencies`, `utils.disambiguateReferenceNode` and `utils.updateReferencesMapsFromReferenceNode` are not provided, and `evaluate()` accepts no AST node other than these suggestions.
45
+ - **Internal fields.** Of the members `publicodes` marks as internal, only `context.nodesTypes` exists, with the type inferred for each rule `getRule()` returns; `baseContext`, `publicParsedRules`, `cache`, `evaluateNode` and the rest of `context` do not.
46
+ - **`undefined` inside rules.** A property of a rule set to `undefined` counts as absent, where `publicodes` reads some of them as an empty value.
47
+
48
+ ## Development
49
+
50
+ ### Requirements
51
+
52
+ - Node 22+
53
+ - pnpm 10.33
54
+ - Rust via rustup (the pinned toolchain installs itself)
55
+ - `wasm-bindgen-cli` 0.2.129, for the WebAssembly module: `cargo install wasm-bindgen-cli --version 0.2.129 --locked`
56
+
57
+ ### Setup
58
+
59
+ ```bash
60
+ pnpm install
61
+ pnpm build:native
62
+ ```
63
+
64
+ ### Commands
65
+
66
+ | Command | Purpose |
67
+ | --- | --- |
68
+ | `pnpm build:native` | Build the debug addon, needed before `pnpm test` |
69
+ | `pnpm build:wasm` | Build the WebAssembly module, needed before `pnpm test:wasm` |
70
+ | `pnpm build` | Release addon, WebAssembly module and compiled package |
71
+ | `pnpm lint` | Lint and type check Rust and TypeScript |
72
+ | `pnpm format` | Format Rust and TypeScript |
73
+ | `pnpm test` | TypeScript tests, including the comparison against publicodes |
74
+ | `pnpm test:wasm` | The same tests against the WebAssembly module |
75
+ | `FUZZ_SEED=100 pnpm test test/compat/fuzz.test.ts` | Fuzz new seeds; see [docs/testing.md](docs/testing.md) |
76
+ | `cargo test` | Rust tests |
77
+ | `pnpm bench` | Benchmarks against publicodes |
78
+
79
+ To publish a version, see [docs/releasing.md](docs/releasing.md).
80
+
81
+ ## License
82
+
83
+ [MIT](LICENSE), including the notice of publicodes, from which this port derives.
package/dist/api.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Engine } from "./engine.js";
2
+ import * as utils from "./ruleUtils.js";
3
+ import type { ExprAST } from "./types.js";
4
+ export { isPublicodesError, PublicodesError } from "./error.js";
5
+ export { serializeEvaluation, simplifyNodeUnit } from "./evaluation.js";
6
+ export { capitalise0, formatValue } from "./format.js";
7
+ export type * from "./types.js";
8
+ export { parseUnit, serializeUnit, type Unit } from "./units.js";
9
+ export { Engine, utils };
10
+ export default Engine;
11
+ export declare function parseExpression(expression: string, dottedName?: string): ExprAST;
package/dist/api.js ADDED
@@ -0,0 +1,17 @@
1
+ import { binding } from "./binding.js";
2
+ import { Engine } from "./engine.js";
3
+ import { fromNative } from "./error.js";
4
+ import * as utils from "./ruleUtils.js";
5
+ export { isPublicodesError, PublicodesError } from "./error.js";
6
+ export { serializeEvaluation, simplifyNodeUnit } from "./evaluation.js";
7
+ export { capitalise0, formatValue } from "./format.js";
8
+ export { parseUnit, serializeUnit } from "./units.js";
9
+ export { Engine, utils };
10
+ export default Engine;
11
+ export function parseExpression(expression, dottedName = "<expression>") {
12
+ const result = binding().parseExpression(expression, dottedName);
13
+ if ("error" in result) {
14
+ throw fromNative(JSON.parse(result.error));
15
+ }
16
+ return JSON.parse(result.expression);
17
+ }
@@ -0,0 +1,29 @@
1
+ /** What the Node addon and the WebAssembly module both export. */
2
+ export interface Binding {
3
+ Engine: new (rules: string, options: string, getToday: () => string, getUnitKey?: (unit: string) => string | {
4
+ message?: string;
5
+ }) => NativeEngine;
6
+ parseExpression(expression: string, dottedName: string): object;
7
+ }
8
+ export interface NativeEngine {
9
+ setSituation(situation: string, keepPrevious: boolean, strict: boolean): object;
10
+ evaluate(text: string, json: boolean): object;
11
+ evaluateSuggestion(rule: string, name: string): object;
12
+ evaluations(): unknown;
13
+ missingVariables(evaluations: unknown, index: number): object;
14
+ takeWarnings(): object;
15
+ inversionFail(): boolean;
16
+ resetCache(): void;
17
+ shallowCopy(copyCache: boolean): NativeEngine;
18
+ ruleNames(): string[];
19
+ rule(name: string): object;
20
+ applicablePossibilities(name: string): object;
21
+ }
22
+ /** Sets the binding the package uses; its entry points call it before anything else runs. */
23
+ export declare function useBinding(binding: Binding): void;
24
+ export declare function binding(): Binding;
25
+ /**
26
+ * `value` as JSON for the binding, writing what JSON cannot as `{ "\u0000": String(it) }`:
27
+ * non-finite numbers, and `undefined` as `value` or as one of its properties.
28
+ */
29
+ export declare function toJson(value: unknown): string;
@@ -0,0 +1,27 @@
1
+ let current;
2
+ /** Sets the binding the package uses; its entry points call it before anything else runs. */
3
+ export function useBinding(binding) {
4
+ current = binding;
5
+ }
6
+ export function binding() {
7
+ if (!current)
8
+ throw new Error("fast-publicodes was loaded without its engine");
9
+ return current;
10
+ }
11
+ /**
12
+ * `value` as JSON for the binding, writing what JSON cannot as `{ "\u0000": String(it) }`:
13
+ * non-finite numbers, and `undefined` as `value` or as one of its properties.
14
+ */
15
+ export function toJson(value) {
16
+ if (value === undefined)
17
+ return UNDEFINED;
18
+ return JSON.stringify(value, function (_key, item) {
19
+ if (typeof item === "number" && !Number.isFinite(item)) {
20
+ return { "\u0000": String(item) };
21
+ }
22
+ return item === undefined && this === value
23
+ ? { "\u0000": "undefined" }
24
+ : item;
25
+ });
26
+ }
27
+ const UNDEFINED = JSON.stringify({ "\u0000": "undefined" });
@@ -0,0 +1,42 @@
1
+ import type { EngineOptions, EvaluatedNode, InferedType, ParsedRules, Possibility, PublicodesExpression, RawPublicodes, RuleNode, Situation } from "./types.js";
2
+ export declare class Engine<RuleNames extends string = string> {
3
+ private native;
4
+ private logger;
5
+ private publicSituation;
6
+ private parsedRules;
7
+ private rules;
8
+ /** `nodesTypes` alone, for the rule nodes `getRule()` returns. */
9
+ context: {
10
+ nodesTypes: WeakMap<RuleNode, InferedType>;
11
+ };
12
+ private warningTexts;
13
+ private unitKey;
14
+ /** Results by evaluated value, until the cache is reset. */
15
+ private evaluations;
16
+ /** The engine's handle on the evaluations of the current cache. */
17
+ private evaluationsHandle;
18
+ private units;
19
+ constructor(rules?: RawPublicodes<RuleNames>, options?: EngineOptions);
20
+ private logWarnings;
21
+ setSituation(situation?: Situation<RuleNames>, { keepPreviousSituation, strict, }?: {
22
+ keepPreviousSituation?: boolean;
23
+ strict?: boolean;
24
+ }): this;
25
+ private accept;
26
+ getSituation(): Situation<RuleNames>;
27
+ evaluate(value: PublicodesExpression): EvaluatedNode;
28
+ private evaluateNative;
29
+ getRule(dottedName: RuleNames): RuleNode<RuleNames>;
30
+ getParsedRules(): ParsedRules<RuleNames>;
31
+ getPossibilitiesFor(dottedName: RuleNames, { filterNotApplicable }?: {
32
+ filterNotApplicable?: boolean;
33
+ }): Possibility[] | null;
34
+ inversionFail(): boolean;
35
+ resetCache(): void;
36
+ private resetEvaluations;
37
+ private unitOf;
38
+ shallowCopy({ copyCache, }?: {
39
+ copyCache?: boolean;
40
+ inherit?: boolean;
41
+ }): Engine<RuleNames>;
42
+ }
package/dist/engine.js ADDED
@@ -0,0 +1,246 @@
1
+ import { binding, toJson } from "./binding.js";
2
+ import { fromNative, PublicodesError, restoreSurrogates, } from "./error.js";
3
+ import { resolveOptions } from "./options.js";
4
+ function unwrap(result) {
5
+ if ("error" in result) {
6
+ throw fromNative(JSON.parse(result.error));
7
+ }
8
+ return result;
9
+ }
10
+ function today() {
11
+ const date = new Date();
12
+ const pad = (n) => String(n).padStart(2, "0");
13
+ return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()}`;
14
+ }
15
+ /**
16
+ * `getUnitKey` as the binding calls it: it returns the message of an `Error` it throws, and
17
+ * keeps any other value it throws for the engine to throw once the call that ran it returns.
18
+ */
19
+ class UnitKey {
20
+ getUnitKey;
21
+ thrown;
22
+ constructor(getUnitKey) {
23
+ this.getUnitKey = getUnitKey;
24
+ }
25
+ call = (unit) => {
26
+ try {
27
+ return this.getUnitKey(unit);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof Error)
31
+ return { message: error.message };
32
+ this.thrown = { value: error };
33
+ return {};
34
+ }
35
+ };
36
+ rethrow() {
37
+ const thrown = this.thrown;
38
+ if (thrown) {
39
+ this.thrown = undefined;
40
+ throw thrown.value;
41
+ }
42
+ }
43
+ }
44
+ function createNative(rules, options, logger, unitKey) {
45
+ if (typeof rules === "string") {
46
+ throw new PublicodesError("EngineError", "Publicodes does not parse yaml rule sets itself anymore. Please provide a parsed js object. E.g. the `eemeli/yaml` package.", {});
47
+ }
48
+ try {
49
+ return new (binding().Engine)(toJson(rules), JSON.stringify(resolveOptions(options)), today, unitKey?.call);
50
+ }
51
+ catch (error) {
52
+ unitKey?.rethrow();
53
+ let details;
54
+ try {
55
+ details = JSON.parse(error.message);
56
+ }
57
+ catch {
58
+ throw error;
59
+ }
60
+ for (const warning of details.warnings) {
61
+ logger.warn(restoreSurrogates(warning));
62
+ }
63
+ throw fromNative(details);
64
+ }
65
+ }
66
+ /** The rule and name of each suggestion `getRule()` returns, which any engine evaluates. */
67
+ const suggestionSources = new WeakMap();
68
+ export class Engine {
69
+ native;
70
+ logger;
71
+ publicSituation = {};
72
+ parsedRules;
73
+ rules = new Map();
74
+ /** `nodesTypes` alone, for the rule nodes `getRule()` returns. */
75
+ context = {
76
+ nodesTypes: new WeakMap(),
77
+ };
78
+ warningTexts = [];
79
+ unitKey;
80
+ /** Results by evaluated value, until the cache is reset. */
81
+ evaluations = new Map();
82
+ /** The engine's handle on the evaluations of the current cache. */
83
+ evaluationsHandle;
84
+ units = [];
85
+ constructor(rules = {}, options = {}) {
86
+ this.logger = options.logger ?? console;
87
+ this.unitKey = options.getUnitKey && new UnitKey(options.getUnitKey);
88
+ this.native = createNative(rules, options, this.logger, this.unitKey);
89
+ this.logWarnings(this.native.takeWarnings());
90
+ }
91
+ logWarnings({ warnings, warningsReset }) {
92
+ if (!warnings)
93
+ return;
94
+ if (warningsReset)
95
+ this.warningTexts = [];
96
+ for (const warning of warnings) {
97
+ if (typeof warning === "string") {
98
+ const text = restoreSurrogates(warning);
99
+ this.warningTexts.push(text);
100
+ this.logger.warn(text);
101
+ }
102
+ else {
103
+ this.logger.warn(this.warningTexts[warning]);
104
+ }
105
+ }
106
+ }
107
+ setSituation(situation = {}, { keepPreviousSituation = false, strict = false, } = {}) {
108
+ this.resetEvaluations();
109
+ const result = this.native.setSituation(toJson(situation), Boolean(keepPreviousSituation), Boolean(strict));
110
+ this.logWarnings(result);
111
+ const { accepted, error } = result;
112
+ if (error !== undefined) {
113
+ if (result.situationReset && !keepPreviousSituation) {
114
+ this.publicSituation = {};
115
+ }
116
+ if (accepted.length) {
117
+ this.accept(situation, accepted, keepPreviousSituation);
118
+ }
119
+ this.unitKey?.rethrow();
120
+ throw fromNative(JSON.parse(error));
121
+ }
122
+ this.accept(situation, accepted, keepPreviousSituation);
123
+ return this;
124
+ }
125
+ accept(situation, accepted, keepPreviousSituation) {
126
+ if (!keepPreviousSituation) {
127
+ this.publicSituation = {};
128
+ }
129
+ for (const name of accepted) {
130
+ this.publicSituation[name] = situation[name];
131
+ }
132
+ }
133
+ getSituation() {
134
+ return this.publicSituation;
135
+ }
136
+ evaluate(value) {
137
+ const evaluated = this.evaluations.get(value);
138
+ if (evaluated)
139
+ return evaluated;
140
+ if (typeof value === "object" && value !== null && "nodeKind" in value) {
141
+ throw new PublicodesError("EngineError", "fast-publicodes n'évalue pas les nœuds d'AST", {});
142
+ }
143
+ const result = this.evaluateNative(value);
144
+ this.logWarnings(result);
145
+ this.unitKey?.rethrow();
146
+ const { nodeValue, index, ...nativeUnit } = unwrap(result);
147
+ const unit = this.unitOf(nativeUnit);
148
+ const native = this.native;
149
+ this.evaluationsHandle ??= native.evaluations();
150
+ const handle = this.evaluationsHandle;
151
+ let missingVariables;
152
+ const evaluation = {
153
+ nodeValue,
154
+ ...(unit && { unit }),
155
+ // Built on first read: most callers only need the value.
156
+ get missingVariables() {
157
+ missingVariables ??= native.missingVariables(handle, index);
158
+ return missingVariables;
159
+ },
160
+ };
161
+ this.evaluations.set(value, evaluation);
162
+ return evaluation;
163
+ }
164
+ evaluateNative(value) {
165
+ const suggestion = typeof value === "object" ? suggestionSources.get(value) : undefined;
166
+ if (suggestion) {
167
+ return this.native.evaluateSuggestion(suggestion.rule, suggestion.name);
168
+ }
169
+ const json = typeof value !== "string";
170
+ const text = json ? toJson(value) : value;
171
+ return this.native.evaluate(text, json);
172
+ }
173
+ getRule(dottedName) {
174
+ let rule = this.rules.get(dottedName);
175
+ if (!rule) {
176
+ const result = this.native.rule(dottedName);
177
+ const { node, type, suggestions } = JSON.parse(unwrap(result).rule);
178
+ rule = { ...node, suggestions: {} };
179
+ for (const name of suggestions) {
180
+ const suggestion = Object.freeze({});
181
+ suggestionSources.set(suggestion, { rule: dottedName, name });
182
+ rule.suggestions[name] = suggestion;
183
+ }
184
+ this.rules.set(dottedName, rule);
185
+ this.context.nodesTypes.set(rule, {
186
+ isNullable: type.isNullable ?? undefined,
187
+ type: type.type ?? undefined,
188
+ });
189
+ }
190
+ return rule;
191
+ }
192
+ getParsedRules() {
193
+ if (!this.parsedRules) {
194
+ const rules = {};
195
+ for (const name of this.native.ruleNames()) {
196
+ rules[name] = this.getRule(name);
197
+ }
198
+ this.parsedRules = rules;
199
+ }
200
+ return this.parsedRules;
201
+ }
202
+ getPossibilitiesFor(dottedName, { filterNotApplicable = false } = {}) {
203
+ if (!filterNotApplicable) {
204
+ const { possibilities } = this.getRule(dottedName);
205
+ return possibilities ? possibilities.explanation : null;
206
+ }
207
+ const result = this.native.applicablePossibilities(dottedName);
208
+ this.logWarnings(result);
209
+ return JSON.parse(unwrap(result).possibilities);
210
+ }
211
+ inversionFail() {
212
+ return this.native.inversionFail();
213
+ }
214
+ resetCache() {
215
+ this.resetEvaluations();
216
+ this.native.resetCache();
217
+ }
218
+ resetEvaluations() {
219
+ this.evaluations = new Map();
220
+ this.evaluationsHandle = undefined;
221
+ }
222
+ unitOf({ unit, newUnit, unitsReset }) {
223
+ if (unitsReset)
224
+ this.units = [];
225
+ if (newUnit) {
226
+ this.units.push(newUnit);
227
+ return newUnit;
228
+ }
229
+ return unit === undefined ? undefined : this.units[unit];
230
+ }
231
+ shallowCopy({ copyCache = true, } = {}) {
232
+ const copy = Object.create(Engine.prototype);
233
+ copy.native = this.native.shallowCopy(Boolean(copyCache));
234
+ copy.logger = this.logger;
235
+ copy.publicSituation = { ...this.publicSituation };
236
+ copy.parsedRules = this.parsedRules;
237
+ copy.rules = this.rules;
238
+ copy.context = this.context;
239
+ copy.warningTexts = [...this.warningTexts];
240
+ copy.unitKey = this.unitKey;
241
+ copy.evaluations = copyCache ? new Map(this.evaluations) : new Map();
242
+ copy.evaluationsHandle = undefined;
243
+ copy.units = [...this.units];
244
+ return copy;
245
+ }
246
+ }
@@ -0,0 +1,39 @@
1
+ export interface PublicodesErrorTypes {
2
+ InternalError: {
3
+ dottedName?: string;
4
+ };
5
+ EngineError: Record<string, never>;
6
+ SyntaxError: {
7
+ dottedName: string;
8
+ };
9
+ EvaluationError: {
10
+ dottedName: string;
11
+ };
12
+ SituationError: {
13
+ dottedName: string;
14
+ };
15
+ UnknownRule: {
16
+ dottedName: string;
17
+ };
18
+ PrivateRule: {
19
+ dottedName: string;
20
+ };
21
+ TypeError: {
22
+ dottedName: string;
23
+ };
24
+ }
25
+ export declare class PublicodesError<Name extends keyof PublicodesErrorTypes = keyof PublicodesErrorTypes> extends Error {
26
+ name: Name;
27
+ info: PublicodesErrorTypes[Name];
28
+ constructor(name: Name, message: string, info: PublicodesErrorTypes[Name], originalError?: Error);
29
+ }
30
+ export declare function isPublicodesError<Name extends keyof PublicodesErrorTypes>(error: unknown, name?: Name): error is PublicodesError<Name>;
31
+ export interface NativeError {
32
+ name: keyof PublicodesErrorTypes | "Error" | "TypeError";
33
+ message: string;
34
+ dottedName: string;
35
+ }
36
+ /** Restores the lone high surrogates the engine writes as private use characters. */
37
+ export declare function restoreSurrogates(text: string): string;
38
+ /** The error the engine reports, with the message it already wrote. */
39
+ export declare function fromNative({ name, message: written, dottedName, }: NativeError): Error;
package/dist/error.js ADDED
@@ -0,0 +1,45 @@
1
+ const titles = {
2
+ SyntaxError: "Erreur syntaxique",
3
+ EvaluationError: "Erreur d'évaluation",
4
+ SituationError: "Erreur lors de la mise à jour de la situation",
5
+ UnknownRule: "Règle inconnue",
6
+ PrivateRule: "Règle privée",
7
+ };
8
+ function buildMessage(name, message, info, originalError) {
9
+ const isError = /error/i.test(name);
10
+ return (`\n[ ${titles[name] ?? name} ]` +
11
+ (info?.dottedName ? `\n➡️ Dans la règle "${info.dottedName}"` : "") +
12
+ `\n${isError ? "✖️" : "⚠️"} ${message}` +
13
+ (originalError
14
+ ? `\n${isError ? " " : "ℹ️ "}${originalError.message}`
15
+ : ""));
16
+ }
17
+ export class PublicodesError extends Error {
18
+ name;
19
+ info;
20
+ constructor(name, message, info, originalError) {
21
+ super(buildMessage(name, message, info, originalError));
22
+ this.name = name;
23
+ this.info = info;
24
+ }
25
+ }
26
+ export function isPublicodesError(error, name) {
27
+ return (error instanceof PublicodesError &&
28
+ (name === undefined || error.name === name));
29
+ }
30
+ /** Restores the lone high surrogates the engine writes as private use characters. */
31
+ export function restoreSurrogates(text) {
32
+ return text.replace(/[\u{F0000}-\u{F03FF}]/gu, (c) => String.fromCharCode(0xd800 + c.codePointAt(0) - 0xf0000));
33
+ }
34
+ /** The error the engine reports, with the message it already wrote. */
35
+ export function fromNative({ name, message: written, dottedName, }) {
36
+ const message = restoreSurrogates(written);
37
+ if (name === "Error")
38
+ return new Error(message);
39
+ if (name === "TypeError")
40
+ return new TypeError(message);
41
+ const info = (dottedName ? { dottedName } : {});
42
+ const error = new PublicodesError(name, "", info);
43
+ error.message = message;
44
+ return error;
45
+ }
@@ -0,0 +1,5 @@
1
+ import type { EvaluatedNode } from "./types.js";
2
+ /** The node with its unit simplified, if it has one, and its value converted to it. */
3
+ export declare function simplifyNodeUnit<Node extends EvaluatedNode = EvaluatedNode>(node: Node): Node;
4
+ /** The node's value as a publicodes expression. */
5
+ export declare function serializeEvaluation(node: EvaluatedNode): string | undefined;
@@ -0,0 +1,28 @@
1
+ import { convertUnit, serializeUnit, simplifyUnit } from "./units.js";
2
+ /** The node with its unit simplified, if it has one, and its value converted to it. */
3
+ export function simplifyNodeUnit(node) {
4
+ if (!node.unit) {
5
+ return node;
6
+ }
7
+ const unit = simplifyUnit(node.unit);
8
+ return {
9
+ ...node,
10
+ nodeValue: typeof node.nodeValue === "number"
11
+ ? convertUnit(node.unit, unit, node.nodeValue)
12
+ : node.nodeValue,
13
+ unit,
14
+ };
15
+ }
16
+ /** The node's value as a publicodes expression. */
17
+ export function serializeEvaluation(node) {
18
+ if (typeof node.nodeValue === "number") {
19
+ const unit = serializeUnit(node.unit);
20
+ return `${node.nodeValue}${unit ? unit.replace(/\s/g, "") : ""}`;
21
+ }
22
+ if (typeof node.nodeValue === "boolean") {
23
+ return node.nodeValue ? "oui" : "non";
24
+ }
25
+ if (typeof node.nodeValue === "string") {
26
+ return `'${node.nodeValue}'`;
27
+ }
28
+ }
@@ -0,0 +1,14 @@
1
+ import { type FormatUnit, type Unit } from "./units.js";
2
+ export declare function capitalise0(name: undefined): undefined;
3
+ export declare function capitalise0(name: string): string;
4
+ type Formattable = number | {
5
+ nodeValue: number | string | boolean | null | undefined;
6
+ unit?: Unit;
7
+ } | undefined;
8
+ export declare function formatValue(value: Formattable, { language, displayedUnit, formatUnit, precision, }?: {
9
+ language?: string;
10
+ displayedUnit?: string;
11
+ precision?: number;
12
+ formatUnit?: FormatUnit;
13
+ }): string | undefined;
14
+ export {};