@tsplc/debug 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ui/dom.ts CHANGED
@@ -1,142 +1,197 @@
1
1
  import type { DeviceState } from "../api.ts";
2
2
  import type { DebugDevice, SourceRange } from "../manifest.ts";
3
3
  import {
4
- formatReadAt,
5
- formatValue,
6
- recordLocation,
7
- sourceLocation,
8
- valueStateLabels,
4
+ formatReadAt,
5
+ formatValue,
6
+ recordLocation,
7
+ sourceLocation,
8
+ valueStateLabels,
9
9
  } from "./format.ts";
10
10
  import type { Store, Topic } from "./store.ts";
11
11
  import type { TreeNode } from "./tree.ts";
12
12
 
13
13
  type Child = Node | string | null | undefined | false;
14
- type Attribute = string | number | boolean | undefined | ((event: Event) => void);
14
+ type Attribute =
15
+ string | number | boolean | undefined | ((event: Event) => void);
15
16
 
16
17
  export const h = <K extends keyof HTMLElementTagNameMap>(
17
- tag: K,
18
- attributes: Record<string, Attribute> = {},
19
- ...children: Child[]
18
+ tag: K,
19
+ attributes: Record<string, Attribute> = {},
20
+ ...children: Child[]
20
21
  ): HTMLElementTagNameMap[K] => {
21
- const element = document.createElement(tag);
22
- for (const [name, value] of Object.entries(attributes)) {
23
- if (value === undefined || value === false) continue;
24
- if (typeof value === "function") element.addEventListener(name.slice(2), value);
25
- else element.setAttribute(name, value === true ? "" : String(value));
26
- }
27
- return fill(element, ...children);
22
+ const element = document.createElement(tag);
23
+ for (const [name, value] of Object.entries(attributes)) {
24
+ if (value === undefined || value === false) {
25
+ continue;
26
+ }
27
+ if (typeof value === "function") {
28
+ element.addEventListener(name.slice(2), value);
29
+ } else {
30
+ element.setAttribute(name, value === true ? "" : String(value));
31
+ }
32
+ }
33
+ return fill(element, ...children);
28
34
  };
29
35
 
30
36
  /** Replaces an element's children, skipping the absent ones. */
31
37
  export const fill = <E extends Element>(element: E, ...children: Child[]) => {
32
- element.replaceChildren(...children.filter((child): child is Node | string => Boolean(child)));
33
- return element;
38
+ element.replaceChildren(
39
+ ...children.filter((child): child is Node | string => Boolean(child)),
40
+ );
41
+ return element;
34
42
  };
35
43
 
36
44
  /** A page: built once per route, then told which store topics changed each frame. */
37
45
  export interface View {
38
- element: HTMLElement;
39
- update(topics: Set<Topic>): void;
40
- dispose?(): void;
46
+ element: HTMLElement;
47
+ update(topics: Set<Topic>): void;
48
+ dispose?(): void;
41
49
  }
42
50
 
43
51
  export const empty = (text: string) => h("p", { class: "empty" }, text);
44
52
 
53
+ /** A table of `class` (`grid`, styled as a panel) with a header row of `columns`. */
54
+ export const table = (
55
+ attributes: { class: string },
56
+ columns: string[],
57
+ body: HTMLElement,
58
+ ) =>
59
+ h(
60
+ "table",
61
+ attributes,
62
+ h("thead", {}, h("tr", {}, ...columns.map((label) => h("th", {}, label)))),
63
+ body,
64
+ );
65
+
45
66
  const locationLink = (
46
- { label, href }: { label: string; href?: string },
47
- title: string,
48
- text?: string,
67
+ { label, href }: { label: string; href?: string },
68
+ title: string,
69
+ text?: string,
49
70
  ) =>
50
- href
51
- ? h("a", { class: "src", href, title: `Open ${title} in VS Code` }, text ?? label)
52
- : h("span", { class: "src", title }, text ?? label);
71
+ href
72
+ ? h(
73
+ "a",
74
+ { class: "src", href, title: `Open ${title} in VS Code` },
75
+ text ?? label,
76
+ )
77
+ : h("span", { class: "src", title }, text ?? label);
53
78
 
54
79
  export const sourceLink = (store: Store, range: SourceRange, text?: string) => {
55
- const location = sourceLocation(store.manifest, store.session.projectRoot, range);
56
- return locationLink(location, location.label, text);
80
+ const location = sourceLocation(
81
+ store.manifest,
82
+ store.session.projectRoot,
83
+ range,
84
+ );
85
+ return locationLink(location, location.label, text);
57
86
  };
58
87
 
59
88
  /** A telemetry record's `source`, a project-relative `file:line`. */
60
89
  export const recordSourceLink = (store: Store, source: string) =>
61
- locationLink(recordLocation(store.session.projectRoot, source), source);
90
+ locationLink(recordLocation(store.session.projectRoot, source), source);
62
91
 
63
- const variableHref = (path: string) => `#/variables?path=${encodeURIComponent(path)}`;
92
+ export const variableHref = (path: string) =>
93
+ `#/variables?path=${encodeURIComponent(path)}`;
94
+
95
+ export const deviceHref = (id: string) =>
96
+ `#/hardware/${encodeURIComponent(id)}`;
64
97
 
65
98
  export const pathLink = (path: string, text: Node | string = path) =>
66
- h("a", { class: "path", href: variableHref(path), title: `Show ${path} in Variables` }, text);
99
+ h(
100
+ "a",
101
+ {
102
+ class: "path",
103
+ href: variableHref(path),
104
+ title: `Show ${path} in Variables`,
105
+ },
106
+ text,
107
+ );
67
108
 
68
109
  /** A path that wraps only between its segments. */
69
110
  export const wrappingPath = (path: string) =>
70
- h("span", {}, ...path.split(/(?=[.[])/).flatMap((segment) => [h("wbr"), segment]));
111
+ h(
112
+ "span",
113
+ {},
114
+ ...path.split(/(?=[.[])/).flatMap((segment) => [h("wbr"), segment]),
115
+ );
71
116
 
72
117
  const setText = (element: HTMLElement, text: string) => {
73
- if (element.textContent !== text) element.textContent = text;
118
+ if (element.textContent !== text) {
119
+ element.textContent = text;
120
+ }
74
121
  };
75
122
  const setClass = (element: HTMLElement, name: string) => {
76
- if (element.className !== name) element.className = name;
123
+ if (element.className !== name) {
124
+ element.className = name;
125
+ }
77
126
  };
78
127
 
79
128
  /** A variable's value and its quality and age, kept current by `update()` from the store. */
80
129
  export class LiveValue {
81
- readonly value = h("span");
82
- readonly meta = h("span");
83
-
84
- constructor(
85
- private readonly store: Store,
86
- readonly node: TreeNode,
87
- ) {
88
- this.update();
89
- }
90
-
91
- /** Whether the stream should carry this path. */
92
- get subscribed() {
93
- return this.node.readable && !this.node.constant;
94
- }
95
-
96
- update() {
97
- const { node, store } = this;
98
- if (node.constant)
99
- return this.show(
100
- "constant",
101
- formatValue(store.manifest, node.type, node.constant.value),
102
- "constant",
103
- );
104
- if (!node.readable)
105
- return this.show(
106
- "unavailable",
107
- "—",
108
- "not readable",
109
- "This build can't read it from the controller",
110
- );
111
- const { sample, state } = store.live(node.path);
112
- if (!sample) return this.show("unavailable", "—", "not read", "Not read yet");
113
- const meta =
114
- state === "unavailable" || state === "bad"
115
- ? valueStateLabels[state]
116
- : `${valueStateLabels[state]} · ${formatReadAt(sample.receivedAt)}`;
117
- this.show(
118
- state,
119
- formatValue(store.manifest, node.type, sample.value),
120
- meta,
121
- `${sample.status}\nread ${sample.receivedAt}`,
122
- );
123
- }
124
-
125
- private show(state: string, value: string, meta: string, title?: string) {
126
- setClass(this.value, `value s-${state}`);
127
- setClass(this.meta, `meta s-${state}`);
128
- setText(this.value, value);
129
- setText(this.meta, meta);
130
- if (title !== undefined && this.meta.title !== title) this.meta.title = title;
131
- }
130
+ readonly value = h("span");
131
+ readonly meta = h("span");
132
+
133
+ constructor(
134
+ private readonly store: Store,
135
+ readonly node: TreeNode,
136
+ ) {
137
+ this.update();
138
+ }
139
+
140
+ /** Whether the stream should carry this path. */
141
+ get subscribed() {
142
+ return this.node.readable && !this.node.constant;
143
+ }
144
+
145
+ update() {
146
+ const { node, store } = this;
147
+ if (node.constant) {
148
+ return this.show(
149
+ "constant",
150
+ formatValue(store.manifest, node.type, node.constant.value),
151
+ "constant",
152
+ );
153
+ }
154
+ if (!node.readable) {
155
+ return this.show(
156
+ "unavailable",
157
+ "—",
158
+ "not readable",
159
+ "This build can't read it from the controller",
160
+ );
161
+ }
162
+ const { sample, state } = store.live(node.path);
163
+ if (!sample) {
164
+ return this.show("unavailable", "—", "not read", "Not read yet");
165
+ }
166
+ const meta =
167
+ state === "unavailable" || state === "bad"
168
+ ? valueStateLabels[state]
169
+ : `${valueStateLabels[state]} · ${formatReadAt(sample.receivedAt)}`;
170
+ this.show(
171
+ state,
172
+ formatValue(store.manifest, node.type, sample.value),
173
+ meta,
174
+ `${sample.status}\nread ${sample.receivedAt}`,
175
+ );
176
+ }
177
+
178
+ private show(state: string, value: string, meta: string, title?: string) {
179
+ setClass(this.value, `value s-${state}`);
180
+ setClass(this.meta, `meta s-${state}`);
181
+ setText(this.value, value);
182
+ setText(this.meta, meta);
183
+ if (title !== undefined && this.meta.title !== title) {
184
+ this.meta.title = title;
185
+ }
186
+ }
132
187
  }
133
188
 
134
189
  export type Health = "ok" | "fault" | "stale" | "unknown";
135
190
 
136
191
  interface HealthView {
137
- health: Health;
138
- text: string;
139
- detail: string;
192
+ health: Health;
193
+ text: string;
194
+ detail: string;
140
195
  }
141
196
 
142
197
  /**
@@ -144,25 +199,51 @@ interface HealthView {
144
199
  * login" while nothing can read it, never "OK" by default.
145
200
  */
146
201
  export const deviceHealth = (store: Store, device: DebugDevice): HealthView => {
147
- const state: DeviceState | undefined = store.devices.get(device.id);
148
- const { connection } = store.session;
149
- if (!device.health)
150
- return {
151
- health: "unknown",
152
- text: "unknown",
153
- detail: "This build reads no health for this device",
154
- };
155
- if (connection.state === "unavailable")
156
- return { health: "unknown", text: "needs login", detail: connection.error ?? "" };
157
- if (!state || state.health.state === "unknown")
158
- return { health: "unknown", text: "unknown", detail: "The driver's state has not been read" };
159
- const { health } = state;
160
- const detail = [health.name, health.message].filter(Boolean).join(" · ");
161
- if (health.state === "stale") return { health: "stale", text: "stale", detail };
162
- if (!store.connected || health.quality !== "good")
163
- return { health: "stale", text: `stale, was ${health.state}`, detail };
164
- return { health: health.state, text: health.state === "ok" ? "OK" : health.state, detail };
202
+ const state: DeviceState | undefined = store.devices.get(device.id);
203
+ const { connection } = store.session;
204
+ if (!device.health) {
205
+ return {
206
+ health: "unknown",
207
+ text: "unknown",
208
+ detail: "This build reads no health for this device",
209
+ };
210
+ }
211
+ if (connection.state === "unavailable") {
212
+ return {
213
+ health: "unknown",
214
+ text: "needs login",
215
+ detail: connection.error ?? "",
216
+ };
217
+ }
218
+ if (!state || state.health.state === "unknown") {
219
+ return {
220
+ health: "unknown",
221
+ text: "unknown",
222
+ detail: "The driver's state has not been read",
223
+ };
224
+ }
225
+ const { health } = state;
226
+ const detail = [health.name, health.message].filter(Boolean).join(" · ");
227
+ if (health.state === "stale") {
228
+ return { health: "stale", text: "stale", detail };
229
+ }
230
+ if (!store.connected || health.quality !== "good") {
231
+ return { health: "stale", text: `stale, was ${health.state}`, detail };
232
+ }
233
+ return {
234
+ health: health.state,
235
+ text: health.state === "ok" ? "OK" : health.state,
236
+ detail,
237
+ };
165
238
  };
166
239
 
240
+ /** The devices whose driver state this build reads: the ones a page listing health asks for. */
241
+ export const devicesWithHealth = (store: Store) =>
242
+ store.manifest.devices.filter((device) => device.health).map(({ id }) => id);
243
+
167
244
  export const healthBadge = (health: HealthView) =>
168
- h("span", { class: `badge h-${health.health}`, title: health.detail }, health.text);
245
+ h(
246
+ "span",
247
+ { class: `badge h-${health.health}`, title: health.detail },
248
+ health.text,
249
+ );
package/src/ui/format.ts CHANGED
@@ -3,123 +3,178 @@ import type { DebugManifest, SourceRange } from "../manifest.ts";
3
3
  import { typeOf } from "../paths.ts";
4
4
 
5
5
  /** How a value is shown: each state has its own styling and label, so none passes for live. */
6
- export type ValueState = "live" | "uncertain" | "bad" | "stale" | "unavailable" | "constant";
6
+ export type ValueState =
7
+ "live" | "uncertain" | "bad" | "stale" | "unavailable" | "constant";
7
8
 
8
9
  export const valueStateLabels: Record<ValueState, string> = {
9
- live: "good",
10
- uncertain: "uncertain",
11
- bad: "bad",
12
- stale: "stale",
13
- unavailable: "unavailable",
14
- constant: "constant",
10
+ live: "good",
11
+ uncertain: "uncertain",
12
+ bad: "bad",
13
+ stale: "stale",
14
+ unavailable: "unavailable",
15
+ constant: "constant",
15
16
  };
16
17
 
17
18
  /** A sample is stale when the controller connection is down or the sample is older than three sample intervals. */
18
19
  export const valueState = (
19
- quality: Quality,
20
- ageMs: number,
21
- context: { connected: boolean; intervalMs: number },
20
+ quality: Quality,
21
+ ageMs: number,
22
+ context: { connected: boolean; intervalMs: number },
22
23
  ): ValueState => {
23
- if (quality === "unavailable") return "unavailable";
24
- if (!context.connected || ageMs > 3 * context.intervalMs) return "stale";
25
- if (quality === "bad") return "bad";
26
- if (quality === "uncertain") return "uncertain";
27
- return "live";
24
+ if (quality === "unavailable") {
25
+ return "unavailable";
26
+ }
27
+ if (!context.connected || ageMs > 3 * context.intervalMs) {
28
+ return "stale";
29
+ }
30
+ if (quality === "bad") {
31
+ return "bad";
32
+ }
33
+ if (quality === "uncertain") {
34
+ return "uncertain";
35
+ }
36
+ return "live";
28
37
  };
29
38
 
30
39
  export const formatAge = (ms: number) => {
31
- if (ms < 1000) return "now";
32
- if (ms < 60_000) return `${Math.floor(ms / 1000)} s ago`;
33
- if (ms < 3_600_000) return `${Math.floor(ms / 60_000)} min ago`;
34
- return `${Math.floor(ms / 3_600_000)} h ago`;
40
+ if (ms < 1000) {
41
+ return "now";
42
+ }
43
+ if (ms < 60_000) {
44
+ return `${Math.floor(ms / 1000)} s ago`;
45
+ }
46
+ if (ms < 3_600_000) {
47
+ return `${Math.floor(ms / 60_000)} min ago`;
48
+ }
49
+ return `${Math.floor(ms / 3_600_000)} h ago`;
35
50
  };
36
51
 
37
52
  export const formatDuration = (ms: number) => {
38
- if (ms < 1000) return `${Math.round(ms)} ms`;
39
- if (ms < 60_000) return `${(ms / 1000).toFixed(1)} s`;
40
- return `${Math.floor(ms / 60_000)} min ${Math.floor((ms % 60_000) / 1000)} s`;
53
+ if (ms < 1000) {
54
+ return `${Math.round(ms)} ms`;
55
+ }
56
+ if (ms < 60_000) {
57
+ return `${(ms / 1000).toFixed(1)} s`;
58
+ }
59
+ return `${Math.floor(ms / 60_000)} min ${Math.floor((ms % 60_000) / 1000)} s`;
41
60
  };
42
61
 
43
62
  /** How often values are read: `250 ms`, `1 s`, `1.5 s`. */
44
- export const formatInterval = (ms: number) => (ms < 1000 ? `${ms} ms` : `${ms / 1000} s`);
63
+ export const formatInterval = (ms: number) =>
64
+ ms < 1000 ? `${ms} ms` : `${ms / 1000} s`;
45
65
 
46
66
  /** Local time of day with milliseconds, for ordering events by eye. */
47
67
  export const formatClock = (iso: string) => {
48
- const date = new Date(iso);
49
- const pad = (value: number, width = 2) => String(value).padStart(width, "0");
50
- return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
68
+ const date = new Date(iso);
69
+ const pad = (value: number, width = 2) => String(value).padStart(width, "0");
70
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
51
71
  };
52
72
 
53
73
  /** When a value was read, to the second: `read 14:03:27`. */
54
- export const formatReadAt = (iso: string) => `read ${formatClock(iso).slice(0, 8)}`;
74
+ export const formatReadAt = (iso: string) =>
75
+ `read ${formatClock(iso).slice(0, 8)}`;
55
76
 
56
77
  /** A value of unknown type: booleans as IEC literals, strings quoted. */
57
78
  export const formatScalar = (value: unknown): string => {
58
- if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
59
- if (typeof value === "number")
60
- return Number.isInteger(value) ? String(value) : String(Number(value.toPrecision(6)));
61
- if (typeof value === "string") return JSON.stringify(value);
62
- if (value === null || value === undefined) return "—";
63
- return JSON.stringify(value);
79
+ if (typeof value === "boolean") {
80
+ return value ? "TRUE" : "FALSE";
81
+ }
82
+ if (typeof value === "number") {
83
+ return Number.isInteger(value)
84
+ ? String(value)
85
+ : String(Number(value.toPrecision(6)));
86
+ }
87
+ if (typeof value === "string") {
88
+ return JSON.stringify(value);
89
+ }
90
+ if (value === null || value === undefined) {
91
+ return "—";
92
+ }
93
+ return JSON.stringify(value);
64
94
  };
65
95
 
66
96
  /** A value in the program's terms: enum members by name, whether the host decoded them already or not. */
67
- export const formatValue = (manifest: DebugManifest, typeId: string, value: unknown): string => {
68
- const type = typeOf(manifest, typeId);
69
- if (type.kind !== "enum") return formatScalar(value);
70
- if (typeof value === "string" && Object.hasOwn(type.members, value)) return value;
71
- if (typeof value !== "number") return formatScalar(value);
72
- const member = Object.entries(type.members).find(([, number]) => number === value)?.[0];
73
- return member ?? `${value} (not a ${type.name})`;
97
+ export const formatValue = (
98
+ manifest: DebugManifest,
99
+ typeId: string,
100
+ value: unknown,
101
+ ): string => {
102
+ const type = typeOf(manifest, typeId);
103
+ if (type.kind !== "enum") {
104
+ return formatScalar(value);
105
+ }
106
+ if (typeof value === "string" && Object.hasOwn(type.members, value)) {
107
+ return value;
108
+ }
109
+ if (typeof value !== "number") {
110
+ return formatScalar(value);
111
+ }
112
+ const member = Object.entries(type.members).find(
113
+ ([, number]) => number === value,
114
+ )?.[0];
115
+ return member ?? `${value} (not a ${type.name})`;
74
116
  };
75
117
 
76
118
  export const typeLabel = (manifest: DebugManifest, typeId: string): string => {
77
- const type = typeOf(manifest, typeId);
78
- switch (type.kind) {
79
- case "scalar":
80
- return type.iec;
81
- case "enum":
82
- case "record":
83
- return type.name;
84
- case "array":
85
- return `${typeLabel(manifest, type.element)}[${type.length}]`;
86
- }
119
+ const type = typeOf(manifest, typeId);
120
+ switch (type.kind) {
121
+ case "scalar":
122
+ return type.iec;
123
+ case "enum":
124
+ case "record":
125
+ return type.name;
126
+ case "array":
127
+ return `${typeLabel(manifest, type.element)}[${type.length}]`;
128
+ }
87
129
  };
88
130
 
89
131
  const PROPERTY_TEXT = 16;
90
132
 
91
133
  /** A record's properties as `barcode=CON-MD3-884… scanner=3`, long strings cut short. */
92
134
  export const formatProperties = (properties: Record<string, unknown>) =>
93
- Object.entries(properties)
94
- .map(([key, value]) => {
95
- const text = typeof value === "string" ? value : formatScalar(value);
96
- return `${key}=${text.length > PROPERTY_TEXT ? `${text.slice(0, PROPERTY_TEXT)}…` : text}`;
97
- })
98
- .join(" ");
135
+ Object.entries(properties)
136
+ .map(([key, value]) => {
137
+ const text = typeof value === "string" ? value : formatScalar(value);
138
+ return `${key}=${text.length > PROPERTY_TEXT ? `${text.slice(0, PROPERTY_TEXT)}…` : text}`;
139
+ })
140
+ .join(" ");
99
141
 
100
142
  /** A VS Code link to a project file, when the project root on the debugger host is known. */
101
- const fileHref = (projectRoot: string | undefined, file: string, line: number, column: number) => {
102
- if (projectRoot === undefined) return undefined;
103
- const root = projectRoot.replaceAll("\\", "/").replace(/\/$/, "");
104
- const absolute = `${root}/${file}`.replace(/^\/?/, "/");
105
- return `vscode://file${absolute}:${line}:${column}`;
143
+ const fileHref = (
144
+ projectRoot: string | undefined,
145
+ file: string,
146
+ line: number,
147
+ column: number,
148
+ ) => {
149
+ if (projectRoot === undefined) {
150
+ return undefined;
151
+ }
152
+ const root = projectRoot.replaceAll("\\", "/").replace(/\/$/, "");
153
+ const absolute = `${root}/${file}`.replace(/^\/?/, "/");
154
+ return `vscode://file${absolute}:${line}:${column}`;
106
155
  };
107
156
 
108
157
  /** `file:line`, and a VS Code link when the project root on the debugger host is known. */
109
158
  export const sourceLocation = (
110
- manifest: DebugManifest,
111
- projectRoot: string | undefined,
112
- range: SourceRange,
159
+ manifest: DebugManifest,
160
+ projectRoot: string | undefined,
161
+ range: SourceRange,
113
162
  ) => {
114
- const file = manifest.sources[range.file] ?? `source ${range.file}`;
115
- const href = fileHref(projectRoot, file, range.line, range.column);
116
- return { label: `${file}:${range.line}`, ...(href && { href }) };
163
+ const file = manifest.sources[range.file] ?? `source ${range.file}`;
164
+ const href = fileHref(projectRoot, file, range.line, range.column);
165
+ return { label: `${file}:${range.line}`, ...(href && { href }) };
117
166
  };
118
167
 
119
168
  /** A record's project-relative `file:line` as `basename:line`, with the same VS Code link. */
120
- export const recordLocation = (projectRoot: string | undefined, source: string) => {
121
- const [, file = source, line] = /^(.*):(\d+)$/.exec(source) ?? [];
122
- const label = `${file.slice(file.lastIndexOf("/") + 1)}${line ? `:${line}` : ""}`;
123
- const href = line === undefined ? undefined : fileHref(projectRoot, file, Number(line), 1);
124
- return { label, ...(href && { href }) };
169
+ export const recordLocation = (
170
+ projectRoot: string | undefined,
171
+ source: string,
172
+ ) => {
173
+ const [, file = source, line] = /^(.*):(\d+)$/.exec(source) ?? [];
174
+ const label = `${file.slice(file.lastIndexOf("/") + 1)}${line ? `:${line}` : ""}`;
175
+ const href =
176
+ line === undefined
177
+ ? undefined
178
+ : fileHref(projectRoot, file, Number(line), 1);
179
+ return { label, ...(href && { href }) };
125
180
  };