SLOPSHOPPER

com.anthropic.claudecode

piebird-workflow-rules's mod: the rules in the context

newprompt
A shopper browsing a rack in a slop shop
README

piebird

Plugins for coding agents: omp first, Claude Code and Codex where a plugin works there too, and the Tern terminal.

Each plugin's README shows it working in a short video.

PluginWhat it gives youWorks in
piebird-kagi-searchKagi web search, quick answers and Kagi News with your own Kagi account, from the plugin's own program; the kagi skill; a setting for how much the agent relies on Kagi over its own web searchomp, Claude Code, Codex, ChatGPT, Claude
piebird-agent-variantsThe agent_variant_setup tool: temporary project-scoped variants of an agent, with explicit model routingomp
piebird-sessionsThe session_read tool: reads, pages and searches a session journal (omp's, Claude Code's or Codex's) by session id and line without opening it whole, with asks shown in full (each question, its options and what was picked or typed); and a line telling the agent its own session idomp, Claude Code, Codex, ChatGPT, Claude
piebird-tern-bridgeThe tern_lua tool, running Lua with Tern's window API in the Tern window holding omp's pane, and the tern skillomp in Tern
piebird-threadsYour project's open threads in front of the agent: the files in the folder its settings name, in omp before a prompt whenever the list changes and shown in the chat, in Claude Code at the start of a conversation; a rule pointing to the threads skill, which gives the few parts of a thread file the list reads and leaves the rest to the agentomp, Claude Code, Codex, ChatGPT
piebird-workflow-rulesRules on how to work (proportion, thinking before building, fixing corrections as classes, project tooling, changelogs, asking for decisions) and the failure-classes skillomp, Claude Code, Codex, ChatGPT
piebird-tern-ghostty-styleomp's chat in Tern drawn the way omp looks in Ghostty: one box per tool call, thoughts left open, bash as $ command, compact diffs and repliesomp in Tern

Install

On a Mac or a Windows PC with ChatGPT or Claude, download the installer from the releases page (piebird-<version>-macos-arm64.dmg for Apple silicon, -macos-x64.dmg for an Intel Mac, -windows-x64.exe for Windows) and open it. It lists the plugins and the apps on your computer they go to; pick the plugins and click Install. It finds omp, Claude Code and Codex too. docs/installer.md has what it writes where.

In a terminal, add this repository as a marketplace once, then install the plugins you want. piebird-kagi-search needs its vault, a program a marketplace can't carry, so it comes with the installer.

omp:

omp plugin marketplace add georgelubaretsi/piebird omp plugin install piebird-sessions@piebird

Claude Code:

claude plugin marketplace add georgelubaretsi/piebird claude plugin install piebird-threads@piebird

Codex:

codex plugin marketplace add georgelubaretsi/piebird codex plugin add piebird-threads@piebird

Codex runs a plugin's program (piebird-kagi-search's, piebird-sessions') only as the installer sets it up, so those two come to Codex through the installer.

A plugin that works in Tern carries a Tern plugin too (its so.stencil.tern/ folder), and omp brings it along: when omp starts in a Tern pane, it installs that Tern plugin if Tern lacks it, updates it after omp plugin upgrade, and says so in the chat. There is nothing to install in Tern yourself. A Tern plugin turned off in Tern's settings is left alone.

Contributing

Issues and pull requests are welcome. This repository is exported from a private workspace, so a pull request is applied there by hand, with credit to its author, and arrives here as a new commit rather than as a merge.

Development

Everything runs in the dev shell (Nix):

nix develop bun install bun run check # generated files, lint, format, typecheck, tests

AGENTS.md has the layout, commands and conventions. Tern's docs are not redistributed here; bun tools/sync-references.ts tern fetches them into reference/.

License

MIT. reference/agent-plugins/ is the Agent Plugins specification and schemas, redistributed under their own licenses (NOTICE).

Source 14 files
hooks/register.ts 25 lines
1// piebird-workflow-rules's Claude Code part, a mod: the plugin's rules (rules/), which omp reads
2// itself, in a block of the conversation's context (core/rules.ts). Claude Code computes it at the
3// start of a conversation and again after compaction or /clear.
4import type { Api, Register } from "claude-code";
5import type { Rule } from "../../kit/core/rules";
6import { contextBlock } from "../../kit/claude/context";
7import { parseRule } from "../../kit/core/rules";
8import { PLUGIN, rulesText } from "../../core/rules";
9
10/** The rules in the plugin's rules/, by file name. */
11async function rulesOf($: Api): Promise<Rule[]> {
12	const folder = `${$.plugin.root}/rules`;
13	const files = (await $.fs.list(folder)).filter(entry => entry.kind === "file" && entry.name.endsWith(".md"));
14	const names = files.map(entry => entry.name).toSorted();
15	return Promise.all(names.map(async name => parseRule(name.slice(0, -3), await $.fs.read(`${folder}/${name}`))));
16}
17
18export const register: Register = on => {
19	on("prompt.context", async ($, e, next) => {
20		const r = await next(e);
21		const block = contextBlock(PLUGIN, rulesText(await rulesOf($), $.plugin.root));
22		return { ...r, blocks: [...r.blocks, block] };
23	});
24};
25
../kit/core/rules.ts 56 lines
1// The rules a plugin ships in rules/, read as omp reads them, for a harness with no rules of its
2// own: each file's front matter says what it is about (`description`), whether it applies always
3// (`alwaysApply`) and to which agents (`agents`: main, reviewer, ...; every agent when absent).
4// Only the plain `key: value` lines omp's rules use are read. The build copies kit/ into each
5// plugin that needs it (plugins/<name>/kit/); edit it in kit/.
6
7export interface Rule {
8	/** The file's name without `.md`. */
9	readonly name: string;
10	readonly description: string;
11	readonly alwaysApply: boolean;
12	/** The agents it is for; undefined for every agent. */
13	readonly agents?: readonly string[];
14	/** The text after the front matter, trimmed: what omp gives an always-applied rule. */
15	readonly body: string;
16}
17
18const FRONT = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
19
20/** A front matter value, unquoted as YAML quotes one. */
21function value(raw: string): string {
22	const text = raw.trim();
23	const quoted = /^"(.*)"$/.exec(text) ?? /^'(.*)'$/.exec(text);
24	return quoted?.[1] ?? text;
25}
26
27/** The rule in `text`, the file `<name>.md`. */
28export function parseRule(name: string, text: string): Rule {
29	const front = FRONT.exec(text);
30	const fields = new Map<string, string>();
31	for (const line of (front?.[1] ?? "").split(/\r?\n/)) {
32		const at = line.indexOf(":");
33		if (at > 0) fields.set(line.slice(0, at).trim(), value(line.slice(at + 1)));
34	}
35	const agents = fields.get("agents");
36	return {
37		name,
38		description: fields.get("description") ?? "",
39		alwaysApply: fields.get("alwaysApply") === "true",
40		...(agents === undefined
41			? {}
42			: {
43					agents: agents
44						.replace(/^\[|\]$/g, "")
45						.split(",")
46						.map(a => value(a)),
47				}),
48		body: (front ? text.slice(front[0].length) : text).trim(),
49	};
50}
51
52/** Whether `rule` is for the agent `agent` (main, reviewer, ...). */
53export function ruleIsFor(rule: Rule, agent: string): boolean {
54	return rule.agents === undefined || rule.agents.includes(agent);
55}
56
../kit/claude/context.ts 17 lines
1// What a plugin tells the model at the start of a conversation, in a Claude Code mod: a block its
2// `prompt.context` hook adds to the context of the first user message, which Claude Code renders
3// under `# <name>` and computes again after compaction or /clear. The build copies kit/ into each
4// plugin that needs it (plugins/<name>/kit/); edit it in kit/.
5import type { ModelText } from "../core/model";
6
7/** One block of `prompt.context`; its name must be unique among the conversation's blocks. */
8export interface ContextBlock {
9	readonly name: string;
10	readonly text: string;
11}
12
13/** The block `name` with the model's text, for the hook to add to `(await next(e)).blocks`. */
14export function contextBlock(name: string, text: ModelText): ContextBlock {
15	return { name, text: text.text };
16}
17
../core/rules.ts 29 lines
1// The plugin's rules for an agent with no rules of its own: the always-applied ones for the main
2// agent in full, as omp puts them in its system prompt, then the others, each by its name, what it
3// is about and its file, for the agent to read when one fits. A rule for other agents only (a
4// reviewer's) is left out.
5import type { ModelText } from "../kit/core/model";
6import type { Rule } from "../kit/core/rules";
7import { joinText } from "../kit/core/model";
8import { ruleIsFor } from "../kit/core/rules";
9import { model } from "./model.gen";
10
11export const PLUGIN = "piebird-workflow-rules";
12
13/** The rules' text for the main agent; `plugin` is the plugin's folder, where the files are. */
14export function rulesText(rules: readonly Rule[], plugin: string): ModelText {
15	const mine = rules.filter(rule => ruleIsFor(rule, "main"));
16	const always = mine.filter(rule => rule.alwaysApply).map(rule => model.text("rule", { body: rule.body }));
17	const entries = mine
18		.filter(rule => !rule.alwaysApply)
19		.map(rule =>
20			model.text("rule-entry", {
21				name: rule.name,
22				description: rule.description,
23				file: `${plugin}/rules/${rule.name}.md`,
24			}),
25		);
26	const onDemand = entries.length === 0 ? [] : [joinText([model.text("rules-on-demand"), ...entries])];
27	return joinText([...always, ...onDemand], "paragraph");
28}
29
../kit/core/model.ts 100 lines
1// Everything a plugin sends a model comes from its Fluent catalog: only a Catalog can make
2// ModelText. Strict where Fluent would print a placeholder, and pure, so it runs in a Claude Code
3// mod too.
4import { FluentBundle, FluentNumber, FluentResource } from "./vendor/fluent/index.js";
5
6const MINT: unique symbol = Symbol("model text");
7
8/**
9 * Text for the model. Its brand keeps any other object with a `text` from passing as one, while
10 * every plugin's copy of the kit, and the tools that read them all, agree on the type; the build
11 * refuses the brand written anywhere but here (tools/build/purity.ts).
12 */
13export class ModelText {
14	readonly text: string;
15	readonly brand = "piebird-catalog";
16	constructor(mint: typeof MINT, text: string) {
17		if (mint !== MINT) throw new Error("ModelText comes from a Catalog");
18		this.text = text;
19	}
20	toString(): string {
21		return this.text;
22	}
23}
24
25/**
26 * Texts from the catalog one after another, each on its own line, or with an empty line between
27 * them as paragraphs. Line breaks are the one thing code adds: words come from the catalog.
28 */
29export function joinText(parts: readonly ModelText[], between: "line" | "paragraph" = "line"): ModelText {
30	return new ModelText(MINT, parts.map(p => p.text).join(between === "line" ? "\n" : "\n\n"));
31}
32
33/** The message ids formatted while a trace runs, for the "what the model sees" page. */
34let traced: string[] | undefined;
35
36/** Runs `run` and returns, with its value, the ids of the messages it formatted, in order, each once. */
37export async function traceMessages<T>(run: () => Promise<T>): Promise<{ value: T; ids: string[] }> {
38	const outer = traced;
39	traced = [];
40	try {
41		const value = await run();
42		return { value, ids: [...new Set(traced)] };
43	} finally {
44		traced = outer;
45	}
46}
47
48export type Variables = Readonly<Record<string, string | number>>;
49
50/** Numbers as written, not grouped the locale's way: a line number reads as a call takes it. */
51function plain(variables: Variables | undefined): Record<string, string | FluentNumber> | undefined {
52	if (variables === undefined) return undefined;
53	return Object.fromEntries(
54		Object.entries(variables).map(([k, v]) => [
55			k,
56			typeof v === "number" ? new FluentNumber(v, { useGrouping: false }) : v,
57		]),
58	);
59}
60
61type Args<M, K extends keyof M> = M[K] extends undefined ? [] : [variables: M[K]];
62
63/** `M` maps each message id, and `id.attribute`, to its variables, or `undefined` when it takes none. */
64export class Catalog<M extends Record<string, Variables | undefined>> {
65	readonly #bundle: FluentBundle;
66
67	constructor(source: string) {
68		// Bidi isolation marks around placeables would reach the model as text.
69		this.#bundle = new FluentBundle("en-US", { useIsolating: false });
70		// The parser skips an entry it can't read; the build refuses those (tools/build/model.ts).
71		const errors = this.#bundle.addResource(new FluentResource(source));
72		if (errors.length > 0) throw new Error(`the catalog: ${errors.map(e => e.message).join("; ")}`);
73	}
74
75	text<K extends keyof M & string>(id: K, ...args: Args<M, K>): ModelText {
76		return this.#format(id, args[0]);
77	}
78
79	/**
80	 * The message `id` chosen at run time, such as a vault's declared error. It throws, as `text`
81	 * does, when the catalog lacks the message or `variables` lacks one it needs.
82	 */
83	textById(id: string, variables?: Variables): ModelText {
84		return this.#format(id, variables);
85	}
86
87	#format(id: string, variables: Variables | undefined): ModelText {
88		const [name, attribute] = id.split(".", 2);
89		const message = this.#bundle.getMessage(name);
90		if (!message) throw new Error(`no message ${name} in the catalog`);
91		const pattern = attribute === undefined ? message.value : message.attributes[attribute];
92		if (!pattern) throw new Error(`message ${id} has no ${attribute === undefined ? "value" : "such attribute"}`);
93		const errors: Error[] = [];
94		const text = this.#bundle.formatPattern(pattern, plain(variables), errors);
95		if (errors.length > 0) throw new Error(`message ${id}: ${errors.map(e => e.message).join("; ")}`);
96		traced?.push(id);
97		return new ModelText(MINT, text);
98	}
99}
100
../core/model.gen.ts 20 lines
1// Generated from model.ftl by `bun run build`; edit model.ftl.
2import { Catalog } from "../kit/core/model";
3
4const SOURCE =
5	"## The rules, for an agent with no rules of its own: the always-applied ones in full, the rest by name.\n\nrule = { $body }\nrules-on-demand = Read one of these rules when its description fits your work:\nrule-entry = - { $name }: { $description } ({ $file })\n";
6
7export type Messages = {
8	rule: {
9		body: string | number;
10	};
11	"rule-entry": {
12		description: string | number;
13		file: string | number;
14		name: string | number;
15	};
16	"rules-on-demand": undefined;
17};
18
19export const model = new Catalog<Messages>(SOURCE);
20
../kit/core/vendor/fluent/index.js 10 lines
1/**
2 * A JavaScript implementation of Project Fluent, a localization
3 * framework designed to unleash the expressive power of the natural language.
4 *
5 * @module
6 */
7export { FluentBundle } from "./bundle.js";
8export { FluentResource } from "./resource.js";
9export { FluentType, FluentNone, FluentNumber, FluentDateTime, } from "./types.js";
10
../kit/core/vendor/fluent/bundle.js 154 lines
1import { resolveComplexPattern } from "./resolver.js";
2import { Scope } from "./scope.js";
3import { FluentNone } from "./types.js";
4import { NUMBER, DATETIME } from "./builtins.js";
5import { getMemoizerForLocale } from "./memoizer.js";
6/**
7 * Message bundles are single-language stores of translation resources. They are
8 * responsible for formatting message values and attributes to strings.
9 */
10export class FluentBundle {
11    /**
12     * Create an instance of `FluentBundle`.
13     *
14     * @example
15     * ```js
16     * let bundle = new FluentBundle(["en-US", "en"]);
17     *
18     * let bundle = new FluentBundle(locales, {useIsolating: false});
19     *
20     * let bundle = new FluentBundle(locales, {
21     *   useIsolating: true,
22     *   functions: {
23     *     NODE_ENV: () => process.env.NODE_ENV
24     *   }
25     * });
26     * ```
27     *
28     * @param locales - Used to instantiate `Intl` formatters used by translations.
29     * @param options - Optional configuration for the bundle.
30     */
31    constructor(locales, { functions, useIsolating = true, transform = (v) => v, } = {}) {
32        /** @ignore */
33        this._terms = new Map();
34        /** @ignore */
35        this._messages = new Map();
36        this.locales = Array.isArray(locales) ? locales : [locales];
37        this._functions = {
38            NUMBER,
39            DATETIME,
40            ...functions,
41        };
42        this._useIsolating = useIsolating;
43        this._transform = transform;
44        this._intls = getMemoizerForLocale(locales);
45    }
46    /**
47     * Check if a message is present in the bundle.
48     *
49     * @param id - The identifier of the message to check.
50     */
51    hasMessage(id) {
52        return this._messages.has(id);
53    }
54    /**
55     * Return a raw unformatted message object from the bundle.
56     *
57     * Raw messages are `{value, attributes}` shapes containing translation units
58     * called `Patterns`. `Patterns` are implementation-specific; they should be
59     * treated as black boxes and formatted with `FluentBundle.formatPattern`.
60     *
61     * @param id - The identifier of the message to check.
62     */
63    getMessage(id) {
64        return this._messages.get(id);
65    }
66    /**
67     * Add a translation resource to the bundle.
68     *
69     * @example
70     * ```js
71     * let res = new FluentResource("foo = Foo");
72     * bundle.addResource(res);
73     * bundle.getMessage("foo");
74     * // → {value: .., attributes: {..}}
75     * ```
76     *
77     * @param res
78     * @param options
79     */
80    addResource(res, { allowOverrides = false, } = {}) {
81        const errors = [];
82        for (let i = 0; i < res.body.length; i++) {
83            let entry = res.body[i];
84            if (entry.id.startsWith("-")) {
85                // Identifiers starting with a dash (-) define terms. Terms are private
86                // and cannot be retrieved from FluentBundle.
87                if (allowOverrides === false && this._terms.has(entry.id)) {
88                    errors.push(new Error(`Attempt to override an existing term: "${entry.id}"`));
89                    continue;
90                }
91                this._terms.set(entry.id, entry);
92            }
93            else {
94                if (allowOverrides === false && this._messages.has(entry.id)) {
95                    errors.push(new Error(`Attempt to override an existing message: "${entry.id}"`));
96                    continue;
97                }
98                this._messages.set(entry.id, entry);
99            }
100        }
101        return errors;
102    }
103    /**
104     * Format a `Pattern` to a string.
105     *
106     * Format a raw `Pattern` into a string. `args` will be used to resolve
107     * references to variables passed as arguments to the translation.
108     *
109     * In case of errors `formatPattern` will try to salvage as much of the
110     * translation as possible and will still return a string. For performance
111     * reasons, the encountered errors are not returned but instead are appended
112     * to the `errors` array passed as the third argument.
113     *
114     * If `errors` is omitted, the first encountered error will be thrown.
115     *
116     * @example
117     * ```js
118     * let errors = [];
119     * bundle.addResource(
120     *     new FluentResource("hello = Hello, {$name}!"));
121     *
122     * let hello = bundle.getMessage("hello");
123     * if (hello.value) {
124     *     bundle.formatPattern(hello.value, {name: "Jane"}, errors);
125     *     // Returns "Hello, Jane!" and `errors` is empty.
126     *
127     *     bundle.formatPattern(hello.value, undefined, errors);
128     *     // Returns "Hello, {$name}!" and `errors` is now:
129     *     // [<ReferenceError: Unknown variable: name>]
130     * }
131     * ```
132     */
133    formatPattern(pattern, args = null, errors = null) {
134        // Resolve a simple pattern without creating a scope. No error handling is
135        // required; by definition simple patterns don't have placeables.
136        if (typeof pattern === "string") {
137            return this._transform(pattern);
138        }
139        // Resolve a complex pattern.
140        let scope = new Scope(this, errors, args);
141        try {
142            let value = resolveComplexPattern(scope, pattern);
143            return value.toString(scope);
144        }
145        catch (err) {
146            if (scope.errors && err instanceof Error) {
147                scope.errors.push(err);
148                return new FluentNone().toString(scope);
149            }
150            throw err;
151        }
152    }
153}
154
../kit/core/vendor/fluent/resource.js 437 lines
1// This regex is used to iterate through the beginnings of messages and terms.
2// With the /m flag, the ^ matches at the beginning of every line.
3const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */gm;
4// Both Attributes and Variants are parsed in while loops. These regexes are
5// used to break out of them.
6const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y;
7const RE_VARIANT_START = /\*?\[/y;
8const RE_NUMBER_LITERAL = /(-?[0-9]+(?:\.([0-9]+))?)/y;
9const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y;
10const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y;
11const RE_FUNCTION_NAME = /^[A-Z][A-Z0-9_-]*$/;
12// A "run" is a sequence of text or string literal characters which don't
13// require any special handling. For TextElements such special characters are: {
14// (starts a placeable), and line breaks which require additional logic to check
15// if the next line is indented. For StringLiterals they are: \ (starts an
16// escape sequence), " (ends the literal), and line breaks which are not allowed
17// in StringLiterals. Note that string runs may be empty; text runs may not.
18const RE_TEXT_RUN = /([^{}\n\r]+)/y;
19const RE_STRING_RUN = /([^\\"\n\r]*)/y;
20// Escape sequences.
21const RE_STRING_ESCAPE = /\\([\\"])/y;
22const RE_UNICODE_ESCAPE = /\\u([a-fA-F0-9]{4})|\\U([a-fA-F0-9]{6})/y;
23// Used for trimming TextElements and indents.
24const RE_LEADING_NEWLINES = /^\n+/;
25const RE_TRAILING_SPACES = / +$/;
26// Used in makeIndent to strip spaces from blank lines and normalize CRLF to LF.
27const RE_BLANK_LINES = / *\r?\n/g;
28// Used in makeIndent to measure the indentation.
29const RE_INDENT = /( *)$/;
30// Common tokens.
31const TOKEN_BRACE_OPEN = /{\s*/y;
32const TOKEN_BRACE_CLOSE = /\s*}/y;
33const TOKEN_BRACKET_OPEN = /\[\s*/y;
34const TOKEN_BRACKET_CLOSE = /\s*] */y;
35const TOKEN_PAREN_OPEN = /\s*\(\s*/y;
36const TOKEN_ARROW = /\s*->\s*/y;
37const TOKEN_COLON = /\s*:\s*/y;
38// Note the optional comma. As a deviation from the Fluent EBNF, the parser
39// doesn't enforce commas between call arguments.
40const TOKEN_COMMA = /\s*,?\s*/y;
41const TOKEN_BLANK = /\s+/y;
42/**
43 * Fluent Resource is a structure storing parsed localization entries.
44 */
45export class FluentResource {
46    constructor(source) {
47        this.body = [];
48        RE_MESSAGE_START.lastIndex = 0;
49        let cursor = 0;
50        // Iterate over the beginnings of messages and terms to efficiently skip
51        // comments and recover from errors.
52        while (true) {
53            let next = RE_MESSAGE_START.exec(source);
54            if (next === null) {
55                break;
56            }
57            cursor = RE_MESSAGE_START.lastIndex;
58            try {
59                this.body.push(parseMessage(next[1]));
60            }
61            catch (err) {
62                if (err instanceof SyntaxError) {
63                    // Don't report any Fluent syntax errors. Skip directly to the
64                    // beginning of the next message or term.
65                    continue;
66                }
67                throw err;
68            }
69        }
70        // The parser implementation is inlined below for performance reasons,
71        // as well as for convenience of accessing `source` and `cursor`.
72        // The parser focuses on minimizing the number of false negatives at the
73        // expense of increasing the risk of false positives. In other words, it
74        // aims at parsing valid Fluent messages with a success rate of 100%, but it
75        // may also parse a few invalid messages which the reference parser would
76        // reject. The parser doesn't perform any validation and may produce entries
77        // which wouldn't make sense in the real world. For best results users are
78        // advised to validate translations with the fluent-syntax parser
79        // pre-runtime.
80        // The parser makes an extensive use of sticky regexes which can be anchored
81        // to any offset of the source string without slicing it. Errors are thrown
82        // to bail out of parsing of ill-formed messages.
83        function test(re) {
84            re.lastIndex = cursor;
85            return re.test(source);
86        }
87        // Advance the cursor by the char if it matches. May be used as a predicate
88        // (was the match found?) or, if errorClass is passed, as an assertion.
89        function consumeChar(char, errorClass) {
90            if (source[cursor] === char) {
91                cursor++;
92                return true;
93            }
94            if (errorClass) {
95                throw new errorClass(`Expected ${char}`);
96            }
97            return false;
98        }
99        // Advance the cursor by the token if it matches. May be used as a predicate
100        // (was the match found?) or, if errorClass is passed, as an assertion.
101        function consumeToken(re, errorClass) {
102            if (test(re)) {
103                cursor = re.lastIndex;
104                return true;
105            }
106            if (errorClass) {
107                throw new errorClass(`Expected ${re.toString()}`);
108            }
109            return false;
110        }
111        // Execute a regex, advance the cursor, and return all capture groups.
112        function match(re) {
113            re.lastIndex = cursor;
114            let result = re.exec(source);
115            if (result === null) {
116                throw new SyntaxError(`Expected ${re.toString()}`);
117            }
118            cursor = re.lastIndex;
119            return result;
120        }
121        // Execute a regex, advance the cursor, and return the capture group.
122        function match1(re) {
123            return match(re)[1];
124        }
125        function parseMessage(id) {
126            let value = parsePattern();
127            let attributes = parseAttributes();
128            if (value === null && Object.keys(attributes).length === 0) {
129                throw new SyntaxError("Expected message value or attributes");
130            }
131            return { id, value, attributes };
132        }
133        function parseAttributes() {
134            let attrs = Object.create(null);
135            while (test(RE_ATTRIBUTE_START)) {
136                let name = match1(RE_ATTRIBUTE_START);
137                let value = parsePattern();
138                if (value === null) {
139                    throw new SyntaxError("Expected attribute value");
140                }
141                attrs[name] = value;
142            }
143            return attrs;
144        }
145        function parsePattern() {
146            let first;
147            // First try to parse any simple text on the same line as the id.
148            if (test(RE_TEXT_RUN)) {
149                first = match1(RE_TEXT_RUN);
150            }
151            // If there's a placeable on the first line, parse a complex pattern.
152            if (source[cursor] === "{" || source[cursor] === "}") {
153                // Re-use the text parsed above, if possible.
154                return parsePatternElements(first ? [first] : [], Infinity);
155            }
156            // RE_TEXT_VALUE stops at newlines. Only continue parsing the pattern if
157            // what comes after the newline is indented.
158            let indent = parseIndent();
159            if (indent) {
160                if (first) {
161                    // If there's text on the first line, the blank block is part of the
162                    // translation content in its entirety.
163                    return parsePatternElements([first, indent], indent.length);
164                }
165                // Otherwise, we're dealing with a block pattern, i.e. a pattern which
166                // starts on a new line. Discrad the leading newlines but keep the
167                // inline indent; it will be used by the dedentation logic.
168                indent.value = trim(indent.value, RE_LEADING_NEWLINES);
169                return parsePatternElements([indent], indent.length);
170            }
171            if (first) {
172                // It was just a simple inline text after all.
173                return trim(first, RE_TRAILING_SPACES);
174            }
175            return null;
176        }
177        // Parse a complex pattern as an array of elements.
178        function parsePatternElements(elements = [], commonIndent) {
179            while (true) {
180                if (test(RE_TEXT_RUN)) {
181                    elements.push(match1(RE_TEXT_RUN));
182                    continue;
183                }
184                if (source[cursor] === "{") {
185                    elements.push(parsePlaceable());
186                    continue;
187                }
188                if (source[cursor] === "}") {
189                    throw new SyntaxError("Unbalanced closing brace");
190                }
191                let indent = parseIndent();
192                if (indent) {
193                    elements.push(indent);
194                    commonIndent = Math.min(commonIndent, indent.length);
195                    continue;
196                }
197                break;
198            }
199            let lastIndex = elements.length - 1;
200            let lastElement = elements[lastIndex];
201            // Trim the trailing spaces in the last element if it's a TextElement.
202            if (typeof lastElement === "string") {
203                elements[lastIndex] = trim(lastElement, RE_TRAILING_SPACES);
204            }
205            let baked = [];
206            for (let element of elements) {
207                if (element instanceof Indent) {
208                    // Dedent indented lines by the maximum common indent.
209                    element = element.value.slice(0, element.value.length - commonIndent);
210                }
211                if (element) {
212                    baked.push(element);
213                }
214            }
215            return baked;
216        }
217        function parsePlaceable() {
218            consumeToken(TOKEN_BRACE_OPEN, SyntaxError);
219            let selector = parseInlineExpression();
220            if (consumeToken(TOKEN_BRACE_CLOSE)) {
221                return selector;
222            }
223            if (consumeToken(TOKEN_ARROW)) {
224                let variants = parseVariants();
225                consumeToken(TOKEN_BRACE_CLOSE, SyntaxError);
226                return {
227                    type: "select",
228                    selector,
229                    ...variants,
230                };
231            }
232            throw new SyntaxError("Unclosed placeable");
233        }
234        function parseInlineExpression() {
235            if (source[cursor] === "{") {
236                // It's a nested placeable.
237                return parsePlaceable();
238            }
239            if (test(RE_REFERENCE)) {
240                let [, sigil, name, attr = null] = match(RE_REFERENCE);
241                if (sigil === "$") {
242                    return { type: "var", name };
243                }
244                if (consumeToken(TOKEN_PAREN_OPEN)) {
245                    let args = parseArguments();
246                    if (sigil === "-") {
247                        // A parameterized term: -term(...).
248                        return { type: "term", name, attr, args };
249                    }
250                    if (RE_FUNCTION_NAME.test(name)) {
251                        return { type: "func", name, args };
252                    }
253                    throw new SyntaxError("Function names must be all upper-case");
254                }
255                if (sigil === "-") {
256                    // A non-parameterized term: -term.
257                    return {
258                        type: "term",
259                        name,
260                        attr,
261                        args: [],
262                    };
263                }
264                return { type: "mesg", name, attr };
265            }
266            return parseLiteral();
267        }
268        function parseArguments() {
269            let args = [];
270            while (true) {
271                switch (source[cursor]) {
272                    case ")": // End of the argument list.
273                        cursor++;
274                        return args;
275                    case undefined: // EOF
276                        throw new SyntaxError("Unclosed argument list");
277                }
278                args.push(parseArgument());
279                // Commas between arguments are treated as whitespace.
280                consumeToken(TOKEN_COMMA);
281            }
282        }
283        function parseArgument() {
284            let expr = parseInlineExpression();
285            if (expr.type !== "mesg") {
286                return expr;
287            }
288            if (consumeToken(TOKEN_COLON)) {
289                // The reference is the beginning of a named argument.
290                return {
291                    type: "narg",
292                    name: expr.name,
293                    value: parseLiteral(),
294                };
295            }
296            // It's a regular message reference.
297            return expr;
298        }
299        function parseVariants() {
300            let variants = [];
301            let count = 0;
302            let star;
303            while (test(RE_VARIANT_START)) {
304                if (consumeChar("*")) {
305                    star = count;
306                }
307                let key = parseVariantKey();
308                let value = parsePattern();
309                if (value === null) {
310                    throw new SyntaxError("Expected variant value");
311                }
312                variants[count++] = { key, value };
313            }
314            if (count === 0) {
315                return null;
316            }
317            if (star === undefined) {
318                throw new SyntaxError("Expected default variant");
319            }
320            return { variants, star };
321        }
322        function parseVariantKey() {
323            consumeToken(TOKEN_BRACKET_OPEN, SyntaxError);
324            let key;
325            if (test(RE_NUMBER_LITERAL)) {
326                key = parseNumberLiteral();
327            }
328            else {
329                key = {
330                    type: "str",
331                    value: match1(RE_IDENTIFIER),
332                };
333            }
334            consumeToken(TOKEN_BRACKET_CLOSE, SyntaxError);
335            return key;
336        }
337        function parseLiteral() {
338            if (test(RE_NUMBER_LITERAL)) {
339                return parseNumberLiteral();
340            }
341            if (source[cursor] === '"') {
342                return parseStringLiteral();
343            }
344            throw new SyntaxError("Invalid expression");
345        }
346        function parseNumberLiteral() {
347            let [, value, fraction = ""] = match(RE_NUMBER_LITERAL);
348            let precision = fraction.length;
349            return {
350                type: "num",
351                value: parseFloat(value),
352                precision,
353            };
354        }
355        function parseStringLiteral() {
356            consumeChar('"', SyntaxError);
357            let value = "";
358            while (true) {
359                value += match1(RE_STRING_RUN);
360                if (source[cursor] === "\\") {
361                    value += parseEscapeSequence();
362                    continue;
363                }
364                if (consumeChar('"')) {
365                    return { type: "str", value };
366                }
367                // We've reached an EOL of EOF.
368                throw new SyntaxError("Unclosed string literal");
369            }
370        }
371        // Unescape known escape sequences.
372        function parseEscapeSequence() {
373            if (test(RE_STRING_ESCAPE)) {
374                return match1(RE_STRING_ESCAPE);
375            }
376            if (test(RE_UNICODE_ESCAPE)) {
377                let [, codepoint4, codepoint6] = match(RE_UNICODE_ESCAPE);
378                let codepoint = parseInt(codepoint4 || codepoint6, 16);
379                return codepoint <= 0xd7ff || 0xe000 <= codepoint
380                    ? // It's a Unicode scalar value.
381                        String.fromCodePoint(codepoint)
382                    : // Lonely surrogates can cause trouble when the parsing result is
383                        // saved using UTF-8. Use U+FFFD REPLACEMENT CHARACTER instead.
384                        "�";
385            }
386            throw new SyntaxError("Unknown escape sequence");
387        }
388        // Parse blank space. Return it if it looks like indent before a pattern
389        // line. Skip it othwerwise.
390        function parseIndent() {
391            let start = cursor;
392            consumeToken(TOKEN_BLANK);
393            // Check the first non-blank character after the indent.
394            switch (source[cursor]) {
395                case ".":
396                case "[":
397                case "*":
398                case "}":
399                case undefined: // EOF
400                    // A special character. End the Pattern.
401                    return false;
402                case "{":
403                    // Placeables don't require indentation (in EBNF: block-placeable).
404                    // Continue the Pattern.
405                    return makeIndent(source.slice(start, cursor));
406            }
407            // If the first character on the line is not one of the special characters
408            // listed above, it's a regular text character. Check if there's at least
409            // one space of indent before it.
410            if (source[cursor - 1] === " ") {
411                // It's an indented text character (in EBNF: indented-char). Continue
412                // the Pattern.
413                return makeIndent(source.slice(start, cursor));
414            }
415            // A not-indented text character is likely the identifier of the next
416            // message. End the Pattern.
417            return false;
418        }
419        // Trim blanks in text according to the given regex.
420        function trim(text, re) {
421            return text.replace(re, "");
422        }
423        // Normalize a blank block and extract the indent details.
424        function makeIndent(blank) {
425            let value = blank.replace(RE_BLANK_LINES, "\n");
426            let length = RE_INDENT.exec(blank)[1].length;
427            return new Indent(value, length);
428        }
429    }
430}
431class Indent {
432    constructor(value, length) {
433        this.value = value;
434        this.length = length;
435    }
436}
437
../kit/core/vendor/fluent/types.js 174 lines
1/**
2 * The `FluentType` class is the base of Fluent's type system.
3 *
4 * Fluent types wrap JavaScript values and store additional configuration for
5 * them, which can then be used in the `toString` method together with a proper
6 * `Intl` formatter.
7 */
8export class FluentType {
9    /**
10     * Create a `FluentType` instance.
11     *
12     * @param value The JavaScript value to wrap.
13     */
14    constructor(value) {
15        this.value = value;
16    }
17    /**
18     * Unwrap the raw value stored by this `FluentType`.
19     */
20    valueOf() {
21        return this.value;
22    }
23}
24/**
25 * A {@link FluentType} representing no correct value.
26 */
27export class FluentNone extends FluentType {
28    /**
29     * Create an instance of `FluentNone` with an optional fallback value.
30     * @param value The fallback value of this `FluentNone`.
31     */
32    constructor(value = "???") {
33        super(value);
34    }
35    /**
36     * Format this `FluentNone` to the fallback string.
37     */
38    toString(scope) {
39        return `{${this.value}}`;
40    }
41}
42/**
43 * A {@link FluentType} representing a number.
44 *
45 * A `FluentNumber` instance stores the number value of the number it
46 * represents. It may also store an option bag of options which will be passed
47 * to `Intl.NumerFormat` when the `FluentNumber` is formatted to a string.
48 */
49export class FluentNumber extends FluentType {
50    /**
51     * Create an instance of `FluentNumber` with options to the
52     * `Intl.NumberFormat` constructor.
53     *
54     * @param value The number value of this `FluentNumber`.
55     * @param opts Options which will be passed to `Intl.NumberFormat`.
56     */
57    constructor(value, opts = {}) {
58        super(value);
59        this.opts = opts;
60    }
61    /**
62     * Format this `FluentNumber` to a string.
63     */
64    toString(scope) {
65        if (scope) {
66            try {
67                const nf = scope.memoizeIntlObject(Intl.NumberFormat, this.opts);
68                return nf.format(this.value);
69            }
70            catch (err) {
71                scope.reportError(err);
72            }
73        }
74        return this.value.toString(10);
75    }
76}
77/**
78 * A {@link FluentType} representing a date and time.
79 *
80 * A `FluentDateTime` instance stores a Date object, Temporal object, or a number
81 * as a numerical timestamp in milliseconds. It may also store an
82 * option bag of options which will be passed to `Intl.DateTimeFormat` when the
83 * `FluentDateTime` is formatted to a string.
84 */
85export class FluentDateTime extends FluentType {
86    static supportsValue(value) {
87        if (typeof value === "number")
88            return true;
89        if (value instanceof Date)
90            return true;
91        if (value instanceof FluentType)
92            return FluentDateTime.supportsValue(value.valueOf());
93        // Temporary workaround to support environments without Temporal
94        if ("Temporal" in globalThis) {
95            // for TypeScript, which doesn't know about Temporal yet
96            const _Temporal = globalThis.Temporal;
97            if (value instanceof _Temporal.Instant ||
98                value instanceof _Temporal.PlainDateTime ||
99                value instanceof _Temporal.PlainDate ||
100                value instanceof _Temporal.PlainMonthDay ||
101                value instanceof _Temporal.PlainTime ||
102                value instanceof _Temporal.PlainYearMonth) {
103                return true;
104            }
105        }
106        return false;
107    }
108    /**
109     * Create an instance of `FluentDateTime` with options to the
110     * `Intl.DateTimeFormat` constructor.
111     *
112     * @param value The number value of this `FluentDateTime`, in milliseconds.
113     * @param opts Options which will be passed to `Intl.DateTimeFormat`.
114     */
115    constructor(value, opts = {}) {
116        // unwrap any FluentType value, but only retain the opts from FluentDateTime
117        if (value instanceof FluentDateTime) {
118            opts = { ...value.opts, ...opts };
119            value = value.value;
120        }
121        else if (value instanceof FluentType) {
122            value = value.valueOf();
123        }
124        // Intl.DateTimeFormat defaults to gregorian calendar, but Temporal defaults to iso8601
125        if (typeof value === "object" &&
126            "calendarId" in value &&
127            opts.calendar === undefined) {
128            opts = { ...opts, calendar: value.calendarId };
129        }
130        super(value);
131        this.opts = opts;
132    }
133    [Symbol.toPrimitive](hint) {
134        return hint === "string" ? this.toString() : this.toNumber();
135    }
136    /**
137     * Convert this `FluentDateTime` to a number.
138     * Note that this isn't always possible due to the nature of Temporal objects.
139     * In such cases, a TypeError will be thrown.
140     */
141    toNumber() {
142        const value = this.value;
143        if (typeof value === "number")
144            return value;
145        if (value instanceof Date)
146            return value.getTime();
147        if ("epochMilliseconds" in value) {
148            return value.epochMilliseconds;
149        }
150        if ("toZonedDateTime" in value) {
151            return value.toZonedDateTime("UTC").epochMilliseconds;
152        }
153        throw new TypeError("Unwrapping a non-number value as a number");
154    }
155    /**
156     * Format this `FluentDateTime` to a string.
157     */
158    toString(scope) {
159        if (scope) {
160            try {
161                const dtf = scope.memoizeIntlObject(Intl.DateTimeFormat, this.opts);
162                return dtf.format(this.value);
163            }
164            catch (err) {
165                scope.reportError(err);
166            }
167        }
168        if (typeof this.value === "number" || this.value instanceof Date) {
169            return new Date(this.value).toISOString();
170        }
171        return this.value.toString();
172    }
173}
174
../kit/core/vendor/fluent/resolver.js 273 lines
1/**
2 * The role of the Fluent resolver is to format a `Pattern` to an instance of
3 * `FluentValue`. For performance reasons, primitive strings are considered
4 * such instances, too.
5 *
6 * Translations can contain references to other messages or variables,
7 * conditional logic in form of select expressions, traits which describe their
8 * grammatical features, and can use Fluent builtins which make use of the
9 * `Intl` formatters to format numbers and dates into the bundle's languages.
10 * See the documentation of the Fluent syntax for more information.
11 *
12 * In case of errors the resolver will try to salvage as much of the
13 * translation as possible. In rare situations where the resolver didn't know
14 * how to recover from an error it will return an instance of `FluentNone`.
15 *
16 * All expressions resolve to an instance of `FluentValue`. The caller should
17 * use the `toString` method to convert the instance to a native value.
18 *
19 * Functions in this file pass around an instance of the `Scope` class, which
20 * stores the data required for successful resolution and error recovery.
21 */
22import { FluentType, FluentNone, FluentNumber, FluentDateTime, } from "./types.js";
23/**
24 * The maximum number of placeables which can be expanded in a single call to
25 * `formatPattern`. The limit protects against the Billion Laughs and Quadratic
26 * Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
27 */
28const MAX_PLACEABLES = 100;
29/** Unicode bidi isolation characters. */
30const FSI = "\u2068";
31const PDI = "\u2069";
32/** Helper: match a variant key to the given selector. */
33function match(scope, selector, key) {
34    if (key === selector) {
35        // Both are strings.
36        return true;
37    }
38    // XXX Consider comparing options too, e.g. minimumFractionDigits.
39    if (key instanceof FluentNumber &&
40        selector instanceof FluentNumber &&
41        key.value === selector.value) {
42        return true;
43    }
44    if (selector instanceof FluentNumber && typeof key === "string") {
45        let category = scope
46            .memoizeIntlObject(Intl.PluralRules, selector.opts)
47            .select(selector.value);
48        if (key === category) {
49            return true;
50        }
51    }
52    return false;
53}
54/** Helper: resolve the default variant from a list of variants. */
55function getDefault(scope, variants, star) {
56    if (variants[star]) {
57        return resolvePattern(scope, variants[star].value);
58    }
59    scope.reportError(new RangeError("No default"));
60    return new FluentNone();
61}
62/** Helper: resolve arguments to a call expression. */
63function getArguments(scope, args) {
64    const positional = [];
65    const named = Object.create(null);
66    for (const arg of args) {
67        if (arg.type === "narg") {
68            named[arg.name] = resolveExpression(scope, arg.value);
69        }
70        else {
71            positional.push(resolveExpression(scope, arg));
72        }
73    }
74    return { positional, named };
75}
76/** Resolve an expression to a Fluent type. */
77function resolveExpression(scope, expr) {
78    switch (expr.type) {
79        case "str":
80            return expr.value;
81        case "num":
82            return new FluentNumber(expr.value, {
83                minimumFractionDigits: expr.precision,
84            });
85        case "var":
86            return resolveVariableReference(scope, expr);
87        case "mesg":
88            return resolveMessageReference(scope, expr);
89        case "term":
90            return resolveTermReference(scope, expr);
91        case "func":
92            return resolveFunctionReference(scope, expr);
93        case "select":
94            return resolveSelectExpression(scope, expr);
95        default:
96            return new FluentNone();
97    }
98}
99/** Resolve a reference to a variable. */
100function resolveVariableReference(scope, { name }) {
101    let arg;
102    if (scope.params) {
103        // We're inside a TermReference. It's OK to reference undefined parameters.
104        if (Object.prototype.hasOwnProperty.call(scope.params, name)) {
105            arg = scope.params[name];
106        }
107        else {
108            return new FluentNone(`$${name}`);
109        }
110    }
111    else if (scope.args &&
112        Object.prototype.hasOwnProperty.call(scope.args, name)) {
113        // We're in the top-level Pattern or inside a MessageReference. Missing
114        // variables references produce ReferenceErrors.
115        arg = scope.args[name];
116    }
117    else {
118        scope.reportError(new ReferenceError(`Unknown variable: $${name}`));
119        return new FluentNone(`$${name}`);
120    }
121    // Return early if the argument already is an instance of FluentType.
122    if (arg instanceof FluentType) {
123        return arg;
124    }
125    // Convert the argument to a Fluent type.
126    switch (typeof arg) {
127        case "string":
128            return arg;
129        case "number":
130            return new FluentNumber(arg);
131        case "object":
132            if (FluentDateTime.supportsValue(arg)) {
133                return new FluentDateTime(arg);
134            }
135        // eslint-disable-next-line no-fallthrough
136        default:
137            scope.reportError(new TypeError(`Variable type not supported: $${name}, ${typeof arg}`));
138            return new FluentNone(`$${name}`);
139    }
140}
141/** Resolve a reference to another message. */
142function resolveMessageReference(scope, { name, attr }) {
143    const message = scope.bundle._messages.get(name);
144    if (!message) {
145        scope.reportError(new ReferenceError(`Unknown message: ${name}`));
146        return new FluentNone(name);
147    }
148    if (attr) {
149        const attribute = message.attributes[attr];
150        if (attribute) {
151            return resolvePattern(scope, attribute);
152        }
153        scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
154        return new FluentNone(`${name}.${attr}`);
155    }
156    if (message.value) {
157        return resolvePattern(scope, message.value);
158    }
159    scope.reportError(new ReferenceError(`No value: ${name}`));
160    return new FluentNone(name);
161}
162/** Resolve a call to a Term with key-value arguments. */
163function resolveTermReference(scope, { name, attr, args }) {
164    const id = `-${name}`;
165    const term = scope.bundle._terms.get(id);
166    if (!term) {
167        scope.reportError(new ReferenceError(`Unknown term: ${id}`));
168        return new FluentNone(id);
169    }
170    if (attr) {
171        const attribute = term.attributes[attr];
172        if (attribute) {
173            // Every TermReference has its own variables.
174            scope.params = getArguments(scope, args).named;
175            const resolved = resolvePattern(scope, attribute);
176            scope.params = null;
177            return resolved;
178        }
179        scope.reportError(new ReferenceError(`Unknown attribute: ${attr}`));
180        return new FluentNone(`${id}.${attr}`);
181    }
182    scope.params = getArguments(scope, args).named;
183    const resolved = resolvePattern(scope, term.value);
184    scope.params = null;
185    return resolved;
186}
187/** Resolve a call to a Function with positional and key-value arguments. */
188function resolveFunctionReference(scope, { name, args }) {
189    // Some functions are built-in. Others may be provided by the runtime via
190    // the `FluentBundle` constructor.
191    let func = scope.bundle._functions[name];
192    if (!func) {
193        scope.reportError(new ReferenceError(`Unknown function: ${name}()`));
194        return new FluentNone(`${name}()`);
195    }
196    if (typeof func !== "function") {
197        scope.reportError(new TypeError(`Function ${name}() is not callable`));
198        return new FluentNone(`${name}()`);
199    }
200    try {
201        let resolved = getArguments(scope, args);
202        return func(resolved.positional, resolved.named);
203    }
204    catch (err) {
205        scope.reportError(err);
206        return new FluentNone(`${name}()`);
207    }
208}
209/** Resolve a select expression to the member object. */
210function resolveSelectExpression(scope, { selector, variants, star }) {
211    let sel = resolveExpression(scope, selector);
212    if (sel instanceof FluentNone) {
213        return getDefault(scope, variants, star);
214    }
215    // Match the selector against keys of each variant, in order.
216    for (const variant of variants) {
217        const key = resolveExpression(scope, variant.key);
218        if (match(scope, sel, key)) {
219            return resolvePattern(scope, variant.value);
220        }
221    }
222    return getDefault(scope, variants, star);
223}
224/** Resolve a pattern (a complex string with placeables). */
225export function resolveComplexPattern(scope, ptn) {
226    if (scope.dirty.has(ptn)) {
227        scope.reportError(new RangeError("Cyclic reference"));
228        return new FluentNone();
229    }
230    // Tag the pattern as dirty for the purpose of the current resolution.
231    scope.dirty.add(ptn);
232    const result = [];
233    // Wrap interpolations with Directional Isolate Formatting characters
234    // only when the pattern has more than one element.
235    const useIsolating = scope.bundle._useIsolating && ptn.length > 1;
236    for (const elem of ptn) {
237        if (typeof elem === "string") {
238            result.push(scope.bundle._transform(elem));
239            continue;
240        }
241        scope.placeables++;
242        if (scope.placeables > MAX_PLACEABLES) {
243            scope.dirty.delete(ptn);
244            // This is a fatal error which causes the resolver to instantly bail out
245            // on this pattern. The length check protects against excessive memory
246            // usage, and throwing protects against eating up the CPU when long
247            // placeables are deeply nested.
248            throw new RangeError(`Too many placeables expanded: ${scope.placeables}, ` +
249                `max allowed is ${MAX_PLACEABLES}`);
250        }
251        if (useIsolating) {
252            result.push(FSI);
253        }
254        result.push(resolveExpression(scope, elem).toString(scope));
255        if (useIsolating) {
256            result.push(PDI);
257        }
258    }
259    scope.dirty.delete(ptn);
260    return result.join("");
261}
262/**
263 * Resolve a simple or a complex Pattern to a FluentString
264 * (which is really the string primitive).
265 */
266function resolvePattern(scope, value) {
267    // Resolve a simple pattern.
268    if (typeof value === "string") {
269        return scope.bundle._transform(value);
270    }
271    return resolveComplexPattern(scope, value);
272}
273
../kit/core/vendor/fluent/scope.js 42 lines
1export class Scope {
2    constructor(bundle, errors, args) {
3        /**
4         * The Set of patterns already encountered during this resolution.
5         * Used to detect and prevent cyclic resolutions.
6         * @ignore
7         */
8        this.dirty = new WeakSet();
9        /** A dict of parameters passed to a TermReference. */
10        this.params = null;
11        /**
12         * The running count of placeables resolved so far.
13         * Used to detect the Billion Laughs and Quadratic Blowup attacks.
14         * @ignore
15         */
16        this.placeables = 0;
17        this.bundle = bundle;
18        this.errors = errors;
19        this.args = args;
20    }
21    reportError(error) {
22        if (!this.errors || !(error instanceof Error)) {
23            throw error;
24        }
25        this.errors.push(error);
26    }
27    memoizeIntlObject(ctor, opts) {
28        let cache = this.bundle._intls.get(ctor);
29        if (!cache) {
30            cache = {};
31            this.bundle._intls.set(ctor, cache);
32        }
33        let id = JSON.stringify(opts);
34        if (!cache[id]) {
35            // @ts-expect-error This is fine.
36            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
37            cache[id] = new ctor(this.bundle.locales, opts);
38        }
39        return cache[id];
40    }
41}
42