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

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.
| Plugin | What it gives you | Works in |
|---|---|---|
| piebird-kagi-search | Kagi 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 search | omp, Claude Code, Codex, ChatGPT, Claude |
| piebird-agent-variants | The agent_variant_setup tool: temporary project-scoped variants of an agent, with explicit model routing | omp |
| piebird-sessions | The 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 id | omp, Claude Code, Codex, ChatGPT, Claude |
| piebird-tern-bridge | The tern_lua tool, running Lua with Tern's window API in the Tern window holding omp's pane, and the tern skill | omp in Tern |
| piebird-threads | Your 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 agent | omp, Claude Code, Codex, ChatGPT |
| piebird-workflow-rules | Rules on how to work (proportion, thinking before building, fixing corrections as classes, project tooling, changelogs, asking for decisions) and the failure-classes skill | omp, Claude Code, Codex, ChatGPT |
| piebird-tern-ghostty-style | omp'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 replies | omp in Tern |
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.
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.
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/.
MIT. reference/agent-plugins/ is the Agent Plugins specification and schemas, redistributed under their own licenses (NOTICE).
hooks/register.ts 25 lines1// 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 lines1// 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 lines1// 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 lines1// 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 lines1// 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 lines1// 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 lines1/**
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 lines1import { 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 lines1// 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 lines1/**
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 lines1/**
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 lines1export 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