@excom/quark 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +6 -0
  11. package/index.ts +4 -0
  12. package/language.ts +7 -0
  13. package/package.json +45 -0
  14. package/rush-logs/quark.apply-exports.cache.log +1 -0
  15. package/rush-logs/quark.apply-exports.log +1 -0
  16. package/rush-logs/quark.build_package-metas.cache.log +1 -0
  17. package/rush-logs/quark.build_package-metas.log +1 -0
  18. package/src/actions.ts +445 -0
  19. package/src/ast.ts +187 -0
  20. package/src/bindings.ts +115 -0
  21. package/src/builtin-modules.ts +401 -0
  22. package/src/constants.ts +47 -0
  23. package/src/cycle-check.ts +121 -0
  24. package/src/devtools-hook.ts +303 -0
  25. package/src/element-api.ts +109 -0
  26. package/src/evaluator.ts +317 -0
  27. package/src/form-controls.ts +69 -0
  28. package/src/language-tables.ts +137 -0
  29. package/src/language.ts +1225 -0
  30. package/src/observer.ts +110 -0
  31. package/src/paint.ts +305 -0
  32. package/src/properties.ts +818 -0
  33. package/src/props.ts +94 -0
  34. package/src/quark-element.ts +7 -0
  35. package/src/quark-internal.ts +365 -0
  36. package/src/quark.ts +817 -0
  37. package/src/resolvers.ts +867 -0
  38. package/src/rule.ts +800 -0
  39. package/src/scope-id.ts +47 -0
  40. package/src/selector-utils.ts +430 -0
  41. package/src/settle.ts +117 -0
  42. package/src/types.ts +155 -0
  43. package/src/utils.ts +79 -0
  44. package/src/variables.ts +359 -0
  45. package/support/demos/css-variables.html +18 -0
  46. package/support/demos/dispatch.html +20 -0
  47. package/support/demos/events.html +9 -0
  48. package/support/demos/iterate.html +13 -0
  49. package/support/demos/js-api.html +17 -0
  50. package/support/demos/provider-list.html +16 -0
  51. package/support/demos/toggle-content.html +14 -0
  52. package/support/demos/unset.html +12 -0
  53. package/support/demos/view-transition.html +36 -0
  54. package/support/docs/ATTRIBUTES.md +37 -0
  55. package/support/docs/AT_RULES.md +45 -0
  56. package/support/docs/BUILTINS.md +57 -0
  57. package/support/docs/CONTENT.md +40 -0
  58. package/support/docs/CSS_VARIABLES.md +16 -0
  59. package/support/docs/DECLARATIONS.md +26 -0
  60. package/support/docs/DELAY.md +37 -0
  61. package/support/docs/DIAGNOSTICS.md +32 -0
  62. package/support/docs/DISPATCH.md +81 -0
  63. package/support/docs/ELEMENT_PROPERTIES.md +34 -0
  64. package/support/docs/EXPRESSIONS.md +31 -0
  65. package/support/docs/INTERNAL.md +8 -0
  66. package/support/docs/JS_API.md +53 -0
  67. package/support/docs/JS_WRITES.md +36 -0
  68. package/support/docs/LIMITATIONS.md +26 -0
  69. package/support/docs/LOOP_GUARD.md +30 -0
  70. package/support/docs/METHODS.md +43 -0
  71. package/support/docs/MODULES.md +111 -0
  72. package/support/docs/NO_REVERSION.md +18 -0
  73. package/support/docs/ON.md +87 -0
  74. package/support/docs/REACTIVITY.md +24 -0
  75. package/support/docs/README.md +143 -0
  76. package/support/docs/SELECTORS.md +87 -0
  77. package/support/docs/SHEETS.md +67 -0
  78. package/support/docs/SYNTAX.md +36 -0
  79. package/support/docs/USE.md +24 -0
  80. package/support/docs/VALUES.md +27 -0
  81. package/support/docs/VARIABLES.md +47 -0
  82. package/support/docs/VIEW_TRANSITION.md +64 -0
  83. package/support/docs-sections.json +65 -0
  84. package/support/package-meta.json +208 -0
  85. package/support/scripts/build-language-docs.mjs +196 -0
  86. package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
  87. package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
  88. package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
  89. package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
  90. package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
  91. package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
  92. package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
  93. package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
  94. package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
  95. package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
  96. package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
  97. package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
  98. package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
  99. package/support/tests/actions.test.ts +437 -0
  100. package/support/tests/builtin-modules.test.ts +337 -0
  101. package/support/tests/builtins.test.ts +189 -0
  102. package/support/tests/css-variables.view.test.ts +35 -0
  103. package/support/tests/delay.test.ts +244 -0
  104. package/support/tests/devtools-hook.test.ts +496 -0
  105. package/support/tests/diagnostics.test.ts +224 -0
  106. package/support/tests/dispatch.view.test.ts +43 -0
  107. package/support/tests/element-api.test.ts +202 -0
  108. package/support/tests/evaluator.test.ts +237 -0
  109. package/support/tests/events.view.test.ts +40 -0
  110. package/support/tests/form-controls.test.ts +452 -0
  111. package/support/tests/helpers.ts +354 -0
  112. package/support/tests/internals.test.ts +568 -0
  113. package/support/tests/iterate.view.test.ts +40 -0
  114. package/support/tests/js-api.view.test.ts +45 -0
  115. package/support/tests/language-docs.test.ts +147 -0
  116. package/support/tests/lifecycle.test.ts +440 -0
  117. package/support/tests/loop-guard.test.ts +323 -0
  118. package/support/tests/provider-list.view.test.ts +47 -0
  119. package/support/tests/quark-features.test.ts +2887 -0
  120. package/support/tests/quark-with-providers.test.ts +169 -0
  121. package/support/tests/quark.test.ts +359 -0
  122. package/support/tests/resolvers.test.ts +346 -0
  123. package/support/tests/selector-analysis.test.ts +281 -0
  124. package/support/tests/selectors.test.ts +670 -0
  125. package/support/tests/toggle-content.view.test.ts +40 -0
  126. package/support/tests/unset.view.test.ts +42 -0
  127. package/support/tests/view-helpers.ts +81 -0
  128. package/support/tests/view-transition.test.ts +1069 -0
  129. package/support/tests/view-transition.view.test.ts +76 -0
  130. package/tsconfig.json +5 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Runaway orchestration loops are cut by the shared loop guard
3
+ * (kit-utils): attr ↔ attr between rules, a self-feeding attr, a
4
+ * sync `$binding` cycle across sheets, content that re-matches its own
5
+ * paint, and a Quark ↔ Neutron cycle through an element's provision.
6
+ * Legitimate chains and repeated external triggers never trip. Trips
7
+ * reach DevTools as `quark/error`; write cycles between rules are
8
+ * warned at build.
9
+ */
10
+ import { Quark } from "../../index";
11
+ import { warnStaticCycles } from "../../src/cycle-check";
12
+ import { getQuarkInternal } from "../../src/quark-internal";
13
+ import { QuarkLogger } from "../../src/utils";
14
+ import {
15
+ type DevtoolsHook,
16
+ NUCLEUS_DEVTOOLS_HOOK_KEY,
17
+ pathMatches,
18
+ type PublicizeMeta,
19
+ type PublicizePath,
20
+ } from "@excom/kit-devtools";
21
+ import {
22
+ afterEach,
23
+ beforeEach,
24
+ describe,
25
+ expect,
26
+ fixture,
27
+ it,
28
+ vi,
29
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
30
+ import "@excom/provider-storage";
31
+ import { LoopGuard, type LoopGuardTrip } from "@excom/kit-utils";
32
+ import { flush, mount, unregisterAll } from "./helpers";
33
+
34
+ describe("Quark: loop guard", () => {
35
+ let trips: LoopGuardTrip[];
36
+ let off: () => void;
37
+
38
+ beforeEach(() => {
39
+ LoopGuard.reset();
40
+ LoopGuard.configure({ limit: 8, log: () => {} });
41
+ trips = [];
42
+ off = LoopGuard.onTrip((trip) => trips.push(trip));
43
+ });
44
+
45
+ afterEach(() => {
46
+ off();
47
+ LoopGuard.reset();
48
+ unregisterAll();
49
+ document.body.innerHTML = "";
50
+ localStorage.clear();
51
+ vi.restoreAllMocks();
52
+ });
53
+
54
+ /** Flush until `done()` holds (a runaway loop needs many rounds). */
55
+ const settle = async (done: () => boolean, rounds = 60) => {
56
+ for (let i = 0; i < rounds && !done(); i++) await flush();
57
+ };
58
+
59
+ const ruleRuns = (quark: Quark) =>
60
+ quark.rules.reduce((sum, rule) => sum + rule.numberOfRuns, 0);
61
+
62
+ it("never re-runs a declaration on the attribute that triggered it (built-in)", async () => {
63
+ // a rule writing its own gate cannot loop on its own: Quark skips the
64
+ // property whose key is the mutation that woke the rule
65
+ const { root, quark } = mount(
66
+ `<p data-tick="a"></p>`,
67
+ `p[data-tick="a"] { data-tick: "b"; }
68
+ p[data-tick="b"] { data-tick: "a"; }`
69
+ );
70
+ const p = root.querySelector("p")!;
71
+ await flush();
72
+ await flush();
73
+ expect(p.getAttribute("data-tick")).toBe("b");
74
+ expect(ruleRuns(quark)).toBeLessThanOrEqual(4);
75
+ expect(trips).toEqual([]);
76
+ });
77
+
78
+ it("cuts an attribute toggle cycling through two attributes", async () => {
79
+ const { root, quark } = mount(
80
+ `<p data-a="1"></p>`,
81
+ `p[data-a="1"] { data-b: "1"; }
82
+ p[data-b="1"] { data-a: "2"; }
83
+ p[data-a="2"] { data-b: "2"; }
84
+ p[data-b="2"] { data-a: "1"; }`
85
+ );
86
+ const p = root.querySelector("p")!;
87
+ await settle(() => trips.length > 0);
88
+ expect(trips.length).toBeGreaterThanOrEqual(1);
89
+ expect(trips[0]).toMatchObject({ kind: "depth", target: p, limit: 8 });
90
+ expect(["data-a", "data-b"]).toContain(trips[0].name);
91
+ // the loop is dead: nothing changes any more
92
+ const runs = ruleRuns(quark);
93
+ const value = [p.getAttribute("data-a"), p.getAttribute("data-b")];
94
+ await flush();
95
+ await flush();
96
+ expect([p.getAttribute("data-a"), p.getAttribute("data-b")]).toEqual(value);
97
+ expect(ruleRuns(quark)).toBe(runs);
98
+ expect(runs).toBeLessThanOrEqual((LoopGuard.limit + 2) * 4);
99
+ });
100
+
101
+ it("cuts two rules that toggle each other's class", async () => {
102
+ // `class:` is exempt from the "not re-run by its own trigger" skip,
103
+ // so this pair really cycles; map and string writes are guarded hops
104
+ const { root, quark } = mount(
105
+ `<p class="is-a"></p><i class="is-a"></i>`,
106
+ `p.is-a { class: (is-a: false, is-b: true); }
107
+ p.is-b { class: (is-b: false, is-a: true); }
108
+ i.is-a { class: "is-b"; }
109
+ i.is-b { class: "is-a"; }`
110
+ );
111
+ const p = root.querySelector("p")!;
112
+ const i = root.querySelector("i")!;
113
+ await settle(() => new Set(trips.map((trip) => trip.target)).size === 2);
114
+ expect(trips.map((trip) => trip.name)).toContain("class");
115
+ expect(new Set(trips.map((trip) => trip.target))).toEqual(new Set([p, i]));
116
+ const runs = ruleRuns(quark);
117
+ const value = [p.className, i.className];
118
+ await flush();
119
+ await flush();
120
+ expect([p.className, i.className]).toEqual(value);
121
+ expect(ruleRuns(quark)).toBe(runs);
122
+ });
123
+
124
+ it("cuts two rules that keep growing each other's attribute", async () => {
125
+ const { root } = mount(
126
+ `<p data-n=""></p>`,
127
+ `p[data-n] { data-m: attr("data-n") + "x"; }
128
+ p[data-m] { data-n: attr("data-m"); }`
129
+ );
130
+ const p = root.querySelector("p")!;
131
+ await settle(() => trips.length > 0);
132
+ expect(trips[0].kind).toBe("depth");
133
+ expect(["data-n", "data-m"]).toContain(trips[0].name);
134
+ expect(p.getAttribute("data-m")!.length).toBeLessThanOrEqual(
135
+ LoopGuard.limit + 1
136
+ );
137
+ });
138
+
139
+ it("does not trip a finite chain of dependent rules", async () => {
140
+ const steps = 12;
141
+ const src = Array.from(
142
+ { length: steps },
143
+ (_, i) => `p[data-s${i}] { data-s${i + 1}: "1"; }`
144
+ ).join("\n");
145
+ LoopGuard.configure({ limit: steps + 3 });
146
+ const { root } = mount(`<p data-s0="1"></p>`, src);
147
+ const p = root.querySelector("p")!;
148
+ await settle(() => p.hasAttribute(`data-s${steps}`));
149
+ expect(p.getAttribute(`data-s${steps}`)).toBe("1");
150
+ expect(trips).toEqual([]);
151
+ });
152
+
153
+ it("starts a fresh chain for every external trigger", async () => {
154
+ LoopGuard.configure({ limit: 3 });
155
+ const { root } = mount(
156
+ `<p data-n="0"></p>`,
157
+ `p[data-n] { data-copy: attr("data-n"); }`
158
+ );
159
+ const p = root.querySelector("p")!;
160
+ for (let i = 1; i <= 12; i++) {
161
+ p.setAttribute("data-n", String(i));
162
+ await flush();
163
+ }
164
+ expect(p.getAttribute("data-copy")).toBe("12");
165
+ expect(trips).toEqual([]);
166
+ });
167
+
168
+ it("cuts a synchronous $binding cycle across two sheets", () => {
169
+ const root = fixture<HTMLElement>(
170
+ `<section><div id="s1"></div><div id="s2"></div></section>`
171
+ );
172
+ const sheet1 = new Quark({
173
+ src: `:scope { $a: ($b or 0) + 1; }`,
174
+ options: { isScoped: true },
175
+ });
176
+ const sheet2 = new Quark({
177
+ src: `:scope { $b: ($a or 0) + 1; }`,
178
+ options: { isScoped: true },
179
+ });
180
+ sheet1.register({ sheetElement: root.querySelector("#s1") as HTMLElement });
181
+ // registering the second sheet starts the ping-pong synchronously:
182
+ // without the guard this is a stack overflow
183
+ expect(() =>
184
+ sheet2.register({
185
+ sheetElement: root.querySelector("#s2") as HTMLElement,
186
+ })
187
+ ).not.toThrow();
188
+ expect(trips.length).toBeGreaterThanOrEqual(1);
189
+ expect(trips[0].kind).toBe("depth");
190
+ expect(["$a", "$b"]).toContain(trips[0].name);
191
+ const vars = getQuarkInternal(root);
192
+ expect(vars.getVar("$a") as number).toBeLessThanOrEqual(LoopGuard.limit + 2);
193
+ expect(vars.getVar("$b") as number).toBeLessThanOrEqual(LoopGuard.limit + 2);
194
+ });
195
+
196
+ it("cuts content that re-matches its own paint", async () => {
197
+ const { root } = mount(
198
+ `<div id="d"></div>`,
199
+ `div { content: dangerous-html("<div></div>"); }`
200
+ );
201
+ await settle(() => trips.length > 0);
202
+ expect(trips[0]).toMatchObject({ kind: "depth", name: "content" });
203
+ // nesting is bounded by the limit instead of growing forever
204
+ let depth = 0;
205
+ for (let el = root.querySelector("#d"); el; el = el.firstElementChild) {
206
+ depth++;
207
+ }
208
+ expect(depth).toBeLessThanOrEqual(LoopGuard.limit + 2);
209
+ });
210
+
211
+ it("cuts a Quark ↔ Neutron cycle through an element's provision", async () => {
212
+ localStorage.setItem("ping", JSON.stringify({ next: "pong" }));
213
+ localStorage.setItem("pong", JSON.stringify({ next: "ping" }));
214
+ const { root } = mount(
215
+ `<provider-storage key-name="ping"></provider-storage>`,
216
+ `provider-storage { key-name: prop("provision").next; }`
217
+ );
218
+ const provider = root.querySelector("provider-storage") as any;
219
+ await settle(() => trips.length > 0);
220
+ expect(trips.length).toBeGreaterThanOrEqual(1);
221
+ expect(trips[0].kind).toBe("depth");
222
+ // the write was dropped, never half-applied: attribute and provision agree
223
+ const key = provider.getAttribute("key-name");
224
+ expect(["ping", "pong"]).toContain(key);
225
+ expect(provider.provision).toEqual(JSON.parse(localStorage.getItem(key)!));
226
+ const runsBefore = trips.length;
227
+ await flush();
228
+ await flush();
229
+ expect(provider.getAttribute("key-name")).toBe(key);
230
+ expect(trips.length).toBe(runsBefore);
231
+ });
232
+
233
+ it("publishes trips to DevTools as quark/error", () => {
234
+ const publications: { path: PublicizePath; meta: PublicizeMeta }[] = [];
235
+ const hook: DevtoolsHook = {
236
+ version: 1,
237
+ inject: () => {},
238
+ publicize: (path, meta) => {
239
+ publications.push({ path, meta });
240
+ },
241
+ };
242
+ Quark.attachDevtools(hook);
243
+ try {
244
+ const el = fixture<HTMLElement>(`<p></p>`);
245
+ LoopGuard.run(LoopGuard.limit, () =>
246
+ LoopGuard.write(el, "data-x", () => el.setAttribute("data-x", "1"))
247
+ );
248
+ expect(el.hasAttribute("data-x")).toBe(false);
249
+ const errors = publications.filter((p) =>
250
+ pathMatches(p.path, ["quark", "error"])
251
+ );
252
+ expect(errors).toHaveLength(1);
253
+ expect(errors[0].meta).toMatchObject({
254
+ tag: "p",
255
+ key: "data-x",
256
+ errorName: "LoopGuardDepth",
257
+ });
258
+ expect((errors[0].meta.weakElement as WeakRef<Element>).deref()).toBe(el);
259
+ expect(String(errors[0].meta.errorMessage)).toContain("Loop guard");
260
+ } finally {
261
+ delete (globalThis as any)[NUCLEUS_DEVTOOLS_HOOK_KEY];
262
+ }
263
+ });
264
+
265
+ describe("static write-cycle warning", () => {
266
+ const build = (src: string) =>
267
+ new Quark({ src, options: { isScoped: true } });
268
+
269
+ it("warns once about rules that gate on each other's writes", () => {
270
+ const warn = vi.spyOn(QuarkLogger, "warn").mockImplementation(() => {});
271
+ const quark = build(
272
+ `p[data-a="1"] { data-b: "1"; }
273
+ p[data-b="1"] { data-a: "2"; }
274
+ p[data-c] { data-d: "1"; }`
275
+ );
276
+ expect(warn).toHaveBeenCalledTimes(1);
277
+ const message = String(warn.mock.calls[0][0].message);
278
+ expect(message).toContain('`p[data-a="1"]` writes [data-b]');
279
+ expect(message).toContain('`p[data-b="1"]` writes [data-a]');
280
+ expect(message).not.toContain("data-c");
281
+ const cycles = warnStaticCycles(quark);
282
+ expect(cycles).toHaveLength(1);
283
+ expect(cycles[0].map((rule) => rule.selector).sort()).toEqual([
284
+ 'p[data-a="1"]',
285
+ 'p[data-b="1"]',
286
+ ]);
287
+ });
288
+
289
+ it("counts literal attr() reads as gates", () => {
290
+ const warn = vi.spyOn(QuarkLogger, "warn").mockImplementation(() => {});
291
+ build(
292
+ `p { data-b: attr("data-a"); }
293
+ p[data-b] { data-a: "1"; }`
294
+ );
295
+ expect(warn).toHaveBeenCalledTimes(1);
296
+ });
297
+
298
+ it("finds longer cycles", () => {
299
+ const warn = vi.spyOn(QuarkLogger, "warn").mockImplementation(() => {});
300
+ const quark = build(
301
+ `p[data-a] { data-b: "1"; }
302
+ p[data-b] { data-c: "1"; }
303
+ p[data-c] { data-a: "1"; }`
304
+ );
305
+ expect(warn).toHaveBeenCalledTimes(1);
306
+ expect(warnStaticCycles(quark)[0]).toHaveLength(3);
307
+ });
308
+
309
+ it("ignores self-edges, chains, non-attribute keys and @on blocks", () => {
310
+ const warn = vi.spyOn(QuarkLogger, "warn").mockImplementation(() => {});
311
+ const quark = build(
312
+ `input[value] { value: "1"; }
313
+ p[data-a] { data-b: "1"; }
314
+ p[data-b] { data-c: "1"; }
315
+ p[data-c] { content: "x"; --c: "1"; dataset: none; class: "z"; }
316
+ p[data-e] { @on click { data-f: "1"; } }
317
+ p[data-f] { data-e: "1"; }`
318
+ );
319
+ expect(warn).not.toHaveBeenCalled();
320
+ expect(warnStaticCycles(quark)).toEqual([]);
321
+ });
322
+ });
323
+ });
@@ -0,0 +1,47 @@
1
+ import "@excom/provider-fetch";
2
+ import "@excom/quark-sheet";
3
+ import {
4
+ afterEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ spyFetch,
9
+ waitForEvent,
10
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
11
+ import {
12
+ expectComplexity,
13
+ flush,
14
+ measureComplexity,
15
+ mountView,
16
+ readDemo,
17
+ } from "./view-helpers";
18
+
19
+ describe("provider-list view", () => {
20
+ afterEach(() => {
21
+ document.body.innerHTML = "";
22
+ });
23
+
24
+ it("iterates fetched todos", async () => {
25
+ spyFetch({
26
+ status: 200,
27
+ body: JSON.stringify([{ id: 1, title: "Alpha" }, { id: 2, title: "Beta" }]),
28
+ });
29
+ const { root, quark } = await mountView(
30
+ readDemo(import.meta.url, "provider-list"),
31
+ );
32
+ const provider = root.querySelector("provider-fetch")!;
33
+ if (!provider.hasAttribute("is-success")) {
34
+ await waitForEvent(provider, "provider-fetch-success");
35
+ await flush();
36
+ }
37
+ const meter = measureComplexity(quark!);
38
+ await flush();
39
+ const budget = meter.take();
40
+ meter.stop();
41
+ expect([...root.querySelectorAll("li")].map((el) => el.textContent)).toEqual([
42
+ "Alpha",
43
+ "Beta",
44
+ ]);
45
+ expectComplexity(budget);
46
+ });
47
+ });