@gjsify/adwaita-web 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adwaita-web.css +255 -27
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/blueprint-layout.spec.d.ts +1 -0
- package/lib/types/blueprint-markup.spec.d.ts +4 -0
- package/lib/types/blueprint-tree.spec.d.ts +1 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
- package/lib/types/elements/adw-carousel.d.ts +5 -0
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
- package/lib/types/elements/adw-navigation-view.d.ts +1 -0
- package/lib/types/elements/adw-sidebar.d.ts +10 -0
- package/lib/types/elements/adw-tab-view.d.ts +1 -0
- package/lib/types/elements/adw-toggle-group.d.ts +11 -0
- package/lib/types/elements/gtk-action-bar.d.ts +19 -0
- package/lib/types/elements/gtk-box.d.ts +17 -0
- package/lib/types/elements/gtk-button.d.ts +2 -0
- package/lib/types/elements/gtk-label.d.ts +122 -0
- package/lib/types/gio-menu.spec.d.ts +1 -0
- package/lib/types/gtk-box.spec.d.ts +1 -0
- package/lib/types/gtk-label.spec.d.ts +1 -0
- package/lib/types/gtk-value-doors.spec.d.ts +1 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/gtk.d.ts +5 -0
- package/lib/types/shared-tree-builder.d.ts +64 -0
- package/lib/types/source-view/asm6502.d.ts +1 -20
- package/lib/types/source-view/index.d.ts +3 -2
- package/lib/types/source-view/theme.d.ts +1 -1
- package/lib/types/string-list-slot.d.ts +4 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tags.spec.d.ts +1 -0
- package/lib/types/value-lists.spec.d.ts +1 -0
- package/package.json +8 -6
- package/scss/_action_bar.scss +51 -0
- package/scss/_banner.scss +8 -1
- package/scss/_bottom_sheet.scss +57 -0
- package/scss/_box.scss +23 -0
- package/scss/_button.scss +29 -7
- package/scss/_button_content.scss +6 -1
- package/scss/_button_row.scss +5 -2
- package/scss/_carousel.scss +6 -0
- package/scss/_labels.scss +126 -12
- package/scss/_navigation_split_view.scss +18 -0
- package/scss/_overlay_split_view.scss +5 -0
- package/scss/_reset.scss +29 -4
- package/scss/_split_button.scss +5 -2
- package/scss/_status_page.scss +31 -17
- package/scss/_theme.scss +11 -3
- package/scss/_toolbar_view.scss +10 -0
- package/scss/_variables.scss +1 -0
- package/scss/_widget.scss +55 -0
- package/scss/_window_title.scss +9 -0
- package/scss/adwaita-skin.scss +8 -2
- package/src/adw-banner.spec.ts +16 -0
- package/src/adw-button-row.spec.ts +8 -0
- package/src/adw-header-bar.spec.ts +17 -0
- package/src/blueprint-layout.spec.ts +223 -0
- package/src/blueprint-markup.spec.ts +155 -0
- package/src/blueprint-tree.spec.ts +174 -0
- package/src/bottom-sheet.spec.ts +166 -1
- package/src/chrome.spec.ts +5 -2
- package/src/elements/adw-bottom-sheet.ts +166 -59
- package/src/elements/adw-carousel.ts +10 -5
- package/src/elements/adw-clamp.ts +12 -0
- package/src/elements/adw-combo-row.ts +13 -4
- package/src/elements/adw-header-bar.ts +46 -3
- package/src/elements/adw-inline-view-switcher.ts +11 -0
- package/src/elements/adw-navigation-view.ts +10 -0
- package/src/elements/adw-sidebar.ts +22 -0
- package/src/elements/adw-spin-row.ts +44 -5
- package/src/elements/adw-status-page.ts +12 -6
- package/src/elements/adw-tab-view.ts +20 -3
- package/src/elements/adw-toggle-group.ts +45 -4
- package/src/elements/adw-toolbar-view.ts +9 -0
- package/src/elements/gtk-action-bar.ts +90 -0
- package/src/elements/gtk-box.ts +87 -0
- package/src/elements/gtk-button.ts +60 -13
- package/src/elements/gtk-drop-down.ts +10 -1
- package/src/elements/gtk-label.ts +415 -0
- package/src/gio-menu.spec.ts +125 -0
- package/src/globals.d.ts +7 -0
- package/src/gtk-box.spec.ts +168 -0
- package/src/gtk-button.spec.ts +123 -1
- package/src/gtk-label.spec.ts +595 -0
- package/src/gtk-value-doors.spec.ts +104 -0
- package/src/index.ts +14 -1
- package/src/keyboard-operable.spec.ts +2 -1
- package/src/namespace/gio.ts +19 -0
- package/src/namespace/gtk.ts +14 -0
- package/src/shared-tree-builder.ts +268 -0
- package/src/shared-trees.spec.ts +296 -32
- package/src/source-view/adw-source-view.ts +3 -3
- package/src/source-view/asm6502.ts +11 -191
- package/src/source-view/index.ts +17 -11
- package/src/source-view/theme.ts +16 -16
- package/src/split-button.spec.ts +7 -0
- package/src/string-list-slot.ts +21 -0
- package/src/styles.generated.ts +1 -1
- package/src/tags.spec.ts +51 -0
- package/src/test.browser.mts +18 -0
- package/src/value-lists.spec.ts +93 -0
- package/lib/types/source-view/hex.d.ts +0 -21
- package/src/source-view/hex.ts +0 -37
package/src/shared-trees.spec.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
// no per-block case — a `switch` on a widget name here would be the per-surface branch
|
|
17
17
|
// § 9 forbids, and the reason the corpus admits a block only when it needs no alias.
|
|
18
18
|
//
|
|
19
|
+
// `mountSharedTree` is SHIPPED code, not this driver's own — the renderer-specific half of
|
|
20
|
+
// ADR 0051 that any consumer of this package may need (the same move #1726 made for
|
|
21
|
+
// `gtk-host`), so it lives in `./shared-tree-builder.ts` rather than here. Its header
|
|
22
|
+
// explains why THIS renderer needs a mount step the other two do not: these elements build
|
|
23
|
+
// on connect.
|
|
24
|
+
//
|
|
19
25
|
// The readers below go through the REAL DOM the element rendered, never a state object of
|
|
20
26
|
// its own: an element asserting against its own bookkeeping agrees with itself while the
|
|
21
27
|
// page is wrong.
|
|
@@ -27,59 +33,43 @@
|
|
|
27
33
|
|
|
28
34
|
import { describe, expect, it } from '@gjsify/unit';
|
|
29
35
|
|
|
36
|
+
import { parseBlueprint, projectToSharedNode } from '@gjsify/blueprint';
|
|
30
37
|
import {
|
|
38
|
+
ADJUSTMENT_AUTHORED_VECTORS,
|
|
31
39
|
authoredTags,
|
|
32
40
|
sharedTreeExpectations,
|
|
41
|
+
sharedTreePlacements,
|
|
33
42
|
subjectIndexOf,
|
|
43
|
+
TOGGLE_ACTIVE_NAME_VECTORS,
|
|
44
|
+
withoutPlacements,
|
|
34
45
|
type SharedTreeExpectation,
|
|
35
46
|
type SharedTreeNode,
|
|
36
47
|
} from '@gjsify/adwaita-core/conformance';
|
|
48
|
+
import { attributeOf, hostTagOf } from '@gjsify/adwaita-core/tags';
|
|
37
49
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
import {
|
|
43
|
-
ADWAITA_GALLERY_SHARED_TREES,
|
|
44
|
-
attributeOf,
|
|
45
|
-
hostTagOf,
|
|
46
|
-
} from '../../../../scripts/adwaita-gallery-shared-trees.mjs';
|
|
50
|
+
// The corpus data itself, not the transforms: `scripts/` still owns the ONE gallery corpus
|
|
51
|
+
// (ADR 0051), and this driver reads it here rather than transcribing it, the same as the
|
|
52
|
+
// GTK and NativeScript drivers do.
|
|
53
|
+
import { ADWAITA_GALLERY_SHARED_TREES } from '../../../../scripts/adwaita-gallery-shared-trees.mjs';
|
|
47
54
|
|
|
48
55
|
import '@gjsify/adwaita-web';
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
* The whole renderer-specific half of this driver: an element, its authored properties as
|
|
52
|
-
* attributes, its children, in that order.
|
|
53
|
-
*
|
|
54
|
-
* A boolean is the ATTRIBUTE'S PRESENCE, which is what every element here reads
|
|
55
|
-
* (`hasAttribute('revealed')`, `hasAttribute('expanded')`) — spelling `"true"` would set a
|
|
56
|
-
* present attribute for `false` as well.
|
|
57
|
-
*/
|
|
58
|
-
function build(node: SharedTreeNode): HTMLElement {
|
|
59
|
-
const el = document.createElement(hostTagOf(node.tag));
|
|
60
|
-
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
61
|
-
if (typeof value === 'boolean') el.toggleAttribute(attributeOf(prop), value);
|
|
62
|
-
else el.setAttribute(attributeOf(prop), String(value));
|
|
63
|
-
}
|
|
64
|
-
for (const child of node.children ?? []) el.append(build(child));
|
|
65
|
-
return el;
|
|
66
|
-
}
|
|
57
|
+
import { mountSharedTree } from './shared-tree-builder.js';
|
|
67
58
|
|
|
68
59
|
/**
|
|
69
60
|
* Connect the authored root — these elements build on connect — run, and take it down again.
|
|
70
61
|
*
|
|
71
62
|
* The teardown is in a `finally` because a RED test must not leave a mounted tree behind:
|
|
72
63
|
* the next test would then be reading a document two blocks deep, and the failure it
|
|
73
|
-
* reported would name the wrong renderer.
|
|
64
|
+
* reported would name the wrong renderer. `mountSharedTree` is the shipped instantiation
|
|
65
|
+
* half; the `finally` around it is this suite's own isolation POLICY, not part of it.
|
|
74
66
|
*/
|
|
75
67
|
function mounted<T>(node: SharedTreeNode, use: (root: Element) => T): T {
|
|
76
|
-
const
|
|
77
|
-
host.append(build(node));
|
|
78
|
-
document.body.append(host);
|
|
68
|
+
const { root, unmount } = mountSharedTree(node);
|
|
79
69
|
try {
|
|
80
|
-
return use(
|
|
70
|
+
return use(root);
|
|
81
71
|
} finally {
|
|
82
|
-
|
|
72
|
+
unmount();
|
|
83
73
|
}
|
|
84
74
|
}
|
|
85
75
|
|
|
@@ -116,6 +106,26 @@ function realised(root: Element, wanted: readonly string[]): Element[] {
|
|
|
116
106
|
return [root, ...root.querySelectorAll('*')].filter((el) => set.has(el.tagName.toLowerCase()));
|
|
117
107
|
}
|
|
118
108
|
|
|
109
|
+
/**
|
|
110
|
+
* A Blueprint source as the `?shared-tree` door hands it over: projected by the one
|
|
111
|
+
* projection `@gjsify/vite-plugin-blueprint` serves, and refused when it dropped anything,
|
|
112
|
+
* as that door refuses it. Projected here rather than imported as a `.blp` file so these
|
|
113
|
+
* fixtures stay out of ADR 0053's census of shipped `.blp` files. The NativeScript driver
|
|
114
|
+
* (`packages/nativescript-bridge/adwaita/src/shared-trees.spec.ts`) builds the same sources.
|
|
115
|
+
*/
|
|
116
|
+
function blueprintTree(body: string): SharedTreeNode {
|
|
117
|
+
const { node, lost } = projectToSharedNode(
|
|
118
|
+
parseBlueprint(`using Gtk 4.0;\nusing Adw 1;\n\n${body}\n`, 'shared-trees.spec.blp'),
|
|
119
|
+
);
|
|
120
|
+
if (lost.length > 0) {
|
|
121
|
+
throw new Error(`the projection dropped ${lost.map((loss) => `${loss.kind} at line ${loss.line}`).join(', ')}`);
|
|
122
|
+
}
|
|
123
|
+
return node as SharedTreeNode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The ids of an element's children, in DOM order. */
|
|
127
|
+
const childIds = (el: Element | null): string[] => Array.from(el?.children ?? [], (child) => child.id);
|
|
128
|
+
|
|
119
129
|
export const AdwSharedTreesTest = async () => {
|
|
120
130
|
await describe('the shared corpus builds through adwaita-web', async () => {
|
|
121
131
|
for (const block of ADWAITA_GALLERY_SHARED_TREES) {
|
|
@@ -130,6 +140,260 @@ export const AdwSharedTreesTest = async () => {
|
|
|
130
140
|
}
|
|
131
141
|
});
|
|
132
142
|
|
|
143
|
+
// THE TAGS A REAL `.blp` WRITES, through the real projection — the same sources the
|
|
144
|
+
// NativeScript driver builds, so the two renderers answer the same trees.
|
|
145
|
+
await describe('a .blp tree with the tags the builders learned', async () => {
|
|
146
|
+
await it('Gtk.Adjustment at `adjustment:` becomes the spin row range, and the value the row shows', () => {
|
|
147
|
+
const tree = blueprintTree(
|
|
148
|
+
'Adw.SpinRow { title: "Size"; adjustment: Adjustment { lower: 2; upper: 10; value: 4; step-increment: 2; }; }',
|
|
149
|
+
);
|
|
150
|
+
mounted(tree, (root) => {
|
|
151
|
+
expect((root as unknown as { adjustment: unknown }).adjustment).toStrictEqual({
|
|
152
|
+
value: 4,
|
|
153
|
+
lower: 2,
|
|
154
|
+
upper: 10,
|
|
155
|
+
stepIncrement: 2,
|
|
156
|
+
pageIncrement: 2,
|
|
157
|
+
pageSize: 0,
|
|
158
|
+
});
|
|
159
|
+
expect(root.querySelector('input')?.value).toBe('4');
|
|
160
|
+
// Consumed as data, as GtkBuilder leaves no adjustment in the widget tree.
|
|
161
|
+
expect(root.querySelector('gtk-adjustment')).toBe(null);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
for (const vector of ADJUSTMENT_AUTHORED_VECTORS) {
|
|
166
|
+
await it(`ADJUSTMENT_AUTHORED_VECTORS through a .blp — ${vector.rule}`, () => {
|
|
167
|
+
const fields = Object.entries(vector.input)
|
|
168
|
+
.map(([field, value]) => `${attributeOf(field)}: ${String(value)};`)
|
|
169
|
+
.join(' ');
|
|
170
|
+
mounted(blueprintTree(`Adw.SpinRow { adjustment: Adjustment { ${fields} }; }`), (root) => {
|
|
171
|
+
expect((root as unknown as { adjustment: unknown }).adjustment).toStrictEqual(vector.adjustment);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const vector of TOGGLE_ACTIVE_NAME_VECTORS) {
|
|
177
|
+
await it(`TOGGLE_ACTIVE_NAME_VECTORS — ${vector.rule}`, () => {
|
|
178
|
+
const toggles = vector.names
|
|
179
|
+
.map(
|
|
180
|
+
(name, index) =>
|
|
181
|
+
`Adw.Toggle { ${name === null ? '' : `name: "${name}"; `}label: "T${index}"; }`,
|
|
182
|
+
)
|
|
183
|
+
.join(' ');
|
|
184
|
+
const tree = blueprintTree(`Adw.ToggleGroup { active-name: "${vector.activeName}"; ${toggles} }`);
|
|
185
|
+
mounted(tree, (root) => {
|
|
186
|
+
// The ACTIVE BUTTON, read off the DOM — not the element's index.
|
|
187
|
+
const buttons = Array.from(root.querySelectorAll('button.adw-toggle'));
|
|
188
|
+
expect(buttons.length).toBe(vector.kept);
|
|
189
|
+
expect(buttons.findIndex((button) => button.classList.contains('active'))).toBe(vector.active);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await it('Adw.Toggle carries its label into the button it becomes', () => {
|
|
195
|
+
const tree = blueprintTree(
|
|
196
|
+
'Adw.ToggleGroup { active: 1; Adw.Toggle { label: "List"; icon-name: "view-list-symbolic"; } Adw.Toggle { label: "Grid"; } }',
|
|
197
|
+
);
|
|
198
|
+
mounted(tree, (root) => {
|
|
199
|
+
const buttons = Array.from(root.querySelectorAll('button.adw-toggle'));
|
|
200
|
+
expect(buttons.map((button) => button.textContent)).toStrictEqual(['List', 'Grid']);
|
|
201
|
+
expect(buttons[1]?.classList.contains('active')).toBe(true);
|
|
202
|
+
expect(root.querySelector('gtk-image') !== null).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await it('Adw.NavigationPage brings its tag, title, can-pop and child into the navigation view', () => {
|
|
207
|
+
const tree = blueprintTree(`Adw.NavigationView {
|
|
208
|
+
Adw.NavigationPage { tag: "home"; title: "Home"; child: Gtk.Label { label: "Welcome"; }; }
|
|
209
|
+
Adw.NavigationPage { tag: "details"; title: "Details"; can-pop: false; child: Gtk.Label { label: "More"; }; }
|
|
210
|
+
}`);
|
|
211
|
+
mounted(tree, (root) => {
|
|
212
|
+
const view = root as unknown as {
|
|
213
|
+
pages: readonly Element[];
|
|
214
|
+
visiblePageTag: string | null;
|
|
215
|
+
pushByTag(tag: string): boolean;
|
|
216
|
+
canGoBack: boolean;
|
|
217
|
+
};
|
|
218
|
+
expect(view.pages.length).toBe(2);
|
|
219
|
+
expect(view.visiblePageTag).toBe('home');
|
|
220
|
+
expect(view.pages[0]?.querySelector('gtk-label')?.getAttribute('label')).toBe('Welcome');
|
|
221
|
+
expect(view.pushByTag('details')).toBe(true);
|
|
222
|
+
expect(view.visiblePageTag).toBe('details');
|
|
223
|
+
// `can-pop: false` is a GTK default of TRUE authored false — the case
|
|
224
|
+
// absence cannot spell, so the builder writes it through `canPop`.
|
|
225
|
+
expect(view.canGoBack).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await it('Adw.SidebarSection and Adw.SidebarItem become the sidebar model and its rows', () => {
|
|
230
|
+
const tree = blueprintTree(`Adw.Sidebar {
|
|
231
|
+
Adw.SidebarSection { title: "Places"; Adw.SidebarItem { title: "Home"; icon-name: "go-home-symbolic"; } Adw.SidebarItem { title: "Trash"; enabled: false; } }
|
|
232
|
+
Adw.SidebarSection { Adw.SidebarItem { title: "Hidden"; visible: false; } Adw.SidebarItem { title: "Music"; subtitle: "Library"; } }
|
|
233
|
+
}`);
|
|
234
|
+
mounted(tree, (root) => {
|
|
235
|
+
const sections = (
|
|
236
|
+
root as unknown as {
|
|
237
|
+
sections: readonly {
|
|
238
|
+
title?: string;
|
|
239
|
+
items: readonly { title: string; enabled?: boolean; visible?: boolean }[];
|
|
240
|
+
}[];
|
|
241
|
+
}
|
|
242
|
+
).sections;
|
|
243
|
+
expect(sections.map((section) => section.title)).toStrictEqual(['Places', '']);
|
|
244
|
+
expect(sections.flatMap((section) => section.items.map((item) => item.title))).toStrictEqual([
|
|
245
|
+
'Home',
|
|
246
|
+
'Trash',
|
|
247
|
+
'Hidden',
|
|
248
|
+
'Music',
|
|
249
|
+
]);
|
|
250
|
+
expect(sections[0]?.items[1]?.enabled).toBe(false);
|
|
251
|
+
expect(sections[1]?.items[0]?.visible).toBe(false);
|
|
252
|
+
// The ROWS: the hidden item's row is hidden, the disabled one insensitive.
|
|
253
|
+
const rows = Array.from(root.querySelectorAll<HTMLButtonElement>('button.adw-sidebar-item'));
|
|
254
|
+
expect(rows.map((row) => row.querySelector('.adw-sidebar-item-title')?.textContent)).toStrictEqual([
|
|
255
|
+
'Home',
|
|
256
|
+
'Trash',
|
|
257
|
+
'Hidden',
|
|
258
|
+
'Music',
|
|
259
|
+
]);
|
|
260
|
+
expect(rows.map((row) => row.hidden)).toStrictEqual([false, false, true, false]);
|
|
261
|
+
expect(rows.map((row) => row.disabled)).toStrictEqual([false, true, false, false]);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await it('Adw.TabPage becomes a titled page around its child; a bare widget an untitled one', () => {
|
|
266
|
+
const tree = blueprintTree(`Adw.TabView {
|
|
267
|
+
Adw.TabPage { title: "Inbox"; child: Gtk.Label inbox { label: "3 new"; }; }
|
|
268
|
+
Adw.TabPage { title: "Drafts"; child: Gtk.Label drafts { label: "None"; }; }
|
|
269
|
+
Gtk.Label bare { label: "Loose"; }
|
|
270
|
+
}`);
|
|
271
|
+
mounted(tree, (root) => {
|
|
272
|
+
const pages = (root as unknown as { pages: readonly { title: string; content?: Element }[] }).pages;
|
|
273
|
+
expect(pages.map((page) => page.title)).toStrictEqual(['Inbox', 'Drafts', '']);
|
|
274
|
+
expect(
|
|
275
|
+
pages.map((page) => page.content?.querySelector('gtk-label')?.id ?? page.content?.id),
|
|
276
|
+
).toStrictEqual(['inbox', 'drafts', 'bare']);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await it('Gtk.ActionBar packs [start], [end], [center] and an untyped child as GtkBuildable does', () => {
|
|
281
|
+
const tree = blueprintTree(`Gtk.ActionBar {
|
|
282
|
+
revealed: false;
|
|
283
|
+
[start] Gtk.Button a {}
|
|
284
|
+
[end] Gtk.Button c {}
|
|
285
|
+
[start] Gtk.Button b {}
|
|
286
|
+
[end] Gtk.Button d {}
|
|
287
|
+
Gtk.Button e {}
|
|
288
|
+
[center] Gtk.Label centre { label: "Centre"; }
|
|
289
|
+
}`);
|
|
290
|
+
mounted(tree, (root) => {
|
|
291
|
+
expect(childIds(root.querySelector('.adw-action-bar-start'))).toStrictEqual(['a', 'b', 'e']);
|
|
292
|
+
expect(childIds(root.querySelector('.adw-action-bar-end'))).toStrictEqual(['d', 'c']);
|
|
293
|
+
expect(childIds(root.querySelector('.adw-action-bar-center'))).toStrictEqual(['centre']);
|
|
294
|
+
expect((root as HTMLElement).hidden).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await it('an authored false on a getter-only property keeps the presence rule instead of throwing', () => {
|
|
299
|
+
// `<adw-split-button>` exposes `active` as a read-only getter; assigning through it
|
|
300
|
+
// threw a bare TypeError out of the builder before the writability check.
|
|
301
|
+
mounted({ tag: 'AdwSplitButton', props: { active: false } }, (root) => {
|
|
302
|
+
expect(root.hasAttribute('active')).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await it('`center-widget:` is refused — GTK has no such property, so Gtk.Builder refuses it too', () => {
|
|
307
|
+
expect(() => mountSharedTree(blueprintTree('Gtk.ActionBar { center-widget: Gtk.Label {}; }'))).toThrow(
|
|
308
|
+
'has no slot "center-widget"',
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await describe('the shared corpus is placed where it says', async () => {
|
|
314
|
+
// A CONTROL, not a table of where each slot lands. `withoutPlacements` is the tree a
|
|
315
|
+
// builder that never read `slot` hands this renderer — which is what all three
|
|
316
|
+
// builders did — so the two realised DOMs being identical is exactly that
|
|
317
|
+
// regression. NESTING AND ELEMENT NAME ONLY, because the `slot=` attribute is itself
|
|
318
|
+
// part of what the placed build writes: comparing markup would differ on the
|
|
319
|
+
// attribute alone and pass while the widget sat beside its destination.
|
|
320
|
+
const structure = (el: Element): string => `${el.localName}[${Array.from(el.children, structure).join(',')}]`;
|
|
321
|
+
|
|
322
|
+
for (const block of ADWAITA_GALLERY_SHARED_TREES) {
|
|
323
|
+
const placements = sharedTreePlacements(block.root);
|
|
324
|
+
if (placements.length === 0) continue;
|
|
325
|
+
const named = placements.map(({ path, slot }) => `${path} -> ${slot}`).join(', ');
|
|
326
|
+
await it(`${block.widget} — ${named}: the mounted DOM is not the unplaced one`, () => {
|
|
327
|
+
const placed = mounted(block.root, structure);
|
|
328
|
+
const unplaced = mounted(withoutPlacements(block.root), structure);
|
|
329
|
+
|
|
330
|
+
expect(placed === unplaced).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Otherwise the loop above is green from emptiness — the corpus authored no slot at
|
|
335
|
+
// all until a `.blp` forced the question.
|
|
336
|
+
await it('the corpus authors a placement at all', () => {
|
|
337
|
+
expect(ADWAITA_GALLERY_SHARED_TREES.some((block) => sharedTreePlacements(block.root).length > 0)).toBe(
|
|
338
|
+
true,
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// THE OTHER HALF OF READING A SLOT, and the corpus cannot carry it: a name this
|
|
343
|
+
// renderer has no destination for. `bindSlottedChildren` copies the NATIVE rule — an
|
|
344
|
+
// unmatched name is assigned nowhere and the child stays put — which is right for
|
|
345
|
+
// hand-written markup and says nothing to a builder realising an authored tree.
|
|
346
|
+
await it('a placement this renderer has no destination for is refused BY NAME', () => {
|
|
347
|
+
expect(() =>
|
|
348
|
+
mountSharedTree({ tag: 'AdwToolbarView', children: [{ tag: 'AdwBanner', slot: 'middle' }] }),
|
|
349
|
+
).toThrow('has no slot "middle"');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// `child: Gtk.Label {…}` in a `.blp` authors `slot: 'child'` — the widget's own
|
|
353
|
+
// GObject property name — for every single-child widget the corpus never places
|
|
354
|
+
// one on. `<adw-clamp>` has no separate box to route into (every child is already
|
|
355
|
+
// the placement, clamped in place), which is why it took no `bindSlottedChildren`
|
|
356
|
+
// call at all until this slot needed one — and why it stayed unnoticed: the corpus
|
|
357
|
+
// above authors zero placements on it.
|
|
358
|
+
await it("AdwClamp accepts its GIR child property, 'child', as a named slot", () => {
|
|
359
|
+
mounted(
|
|
360
|
+
{ tag: 'AdwClamp', children: [{ tag: 'GtkLabel', slot: 'child', props: { label: 'Clamped' } }] },
|
|
361
|
+
(root) => {
|
|
362
|
+
expect(root.querySelector('gtk-label')?.getAttribute('label')).toBe('Clamped');
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await it("AdwStatusPage accepts its GIR child property, 'child', as a named slot", () => {
|
|
368
|
+
mounted(
|
|
369
|
+
{ tag: 'AdwStatusPage', children: [{ tag: 'GtkButton', slot: 'child', props: { label: 'Retry' } }] },
|
|
370
|
+
(root) => {
|
|
371
|
+
expect(root.querySelector('.adw-status-page-child gtk-button')?.getAttribute('label')).toBe(
|
|
372
|
+
'Retry',
|
|
373
|
+
);
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
await it('the PROPERTY spelling of the title slot reaches the centre', () => {
|
|
379
|
+
// `title-widget:` is what a Blueprint source writes the centre at, and this
|
|
380
|
+
// element spells its own centre `center`; naming both is the element's own
|
|
381
|
+
// declaration, not a table in the builder.
|
|
382
|
+
//
|
|
383
|
+
// READ BY THE AUTHORED VALUE, not by the tag: an unrouted child is destroyed by
|
|
384
|
+
// the bar's own `replaceChildren`, and the bar then DERIVES an `<adw-window-title>`
|
|
385
|
+
// into the same centre — so a test asking only where an `<adw-window-title>` sits
|
|
386
|
+
// passed against the derived one with the authored one gone. Measured, by dropping
|
|
387
|
+
// the builder's `slot=` write: green.
|
|
388
|
+
const authored = { tag: 'AdwWindowTitle', slot: 'title-widget', props: { title: 'Placed' } };
|
|
389
|
+
mounted({ tag: 'AdwHeaderBar', children: [authored] }, (root) => {
|
|
390
|
+
const title = root.querySelector('adw-window-title[title="Placed"]');
|
|
391
|
+
|
|
392
|
+
expect(title?.parentElement?.className).toBe('adw-header-bar-center');
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
133
397
|
await describe('the shared corpus against the adwaita-core vectors it reaches', async () => {
|
|
134
398
|
for (const block of ADWAITA_GALLERY_SHARED_TREES) {
|
|
135
399
|
for (const expectation of sharedTreeExpectations(block.root)) {
|
|
@@ -33,7 +33,7 @@ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
|
|
33
33
|
|
|
34
34
|
import { asm6502 } from './asm6502.js';
|
|
35
35
|
import { adwaitaEditorTheme, adwaitaHighlightStyle, ensureSourceViewStyleInjected } from './theme.js';
|
|
36
|
-
import { formatHexAddress, formatLineNumber,
|
|
36
|
+
import { formatHexAddress, formatLineNumber, stripSourceWhitespace } from '@gjsify/adwaita-core';
|
|
37
37
|
import { createGtkImage } from '../elements/gtk-image.js';
|
|
38
38
|
|
|
39
39
|
/** A recognisable copy glyph rendered inline so the subpath needs no icon CSS. */
|
|
@@ -379,7 +379,7 @@ export class AdwSourceView extends HTMLElement {
|
|
|
379
379
|
const { from, to } = view.state.selection.main;
|
|
380
380
|
if (from === to) return false;
|
|
381
381
|
const selected = view.state.sliceDoc(from, to);
|
|
382
|
-
event.clipboardData?.setData('text/plain',
|
|
382
|
+
event.clipboardData?.setData('text/plain', stripSourceWhitespace(selected));
|
|
383
383
|
event.preventDefault();
|
|
384
384
|
return true;
|
|
385
385
|
},
|
|
@@ -391,7 +391,7 @@ export class AdwSourceView extends HTMLElement {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
private _onCopyButtonClick(): void {
|
|
394
|
-
const text = this._hexAddresses ?
|
|
394
|
+
const text = this._hexAddresses ? stripSourceWhitespace(this.code) : this.code;
|
|
395
395
|
const clipboard = (globalThis.navigator as Navigator | undefined)?.clipboard;
|
|
396
396
|
if (clipboard?.writeText) void clipboard.writeText(text).catch(() => undefined);
|
|
397
397
|
this.dispatchEvent(new CustomEvent('copy', { bubbles: true, detail: { code: this.code } }));
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
//
|
|
1
|
+
// The CodeMirror 6 binding for the 6502 assembly mode — the web twin of the
|
|
2
2
|
// GtkSourceView `6502-assembler` language definition. A stream tokenizer is
|
|
3
3
|
// enough here: 6502 asm is line-oriented and needs no nested grammar, so we
|
|
4
4
|
// avoid a full Lezer grammar (per the task brief).
|
|
5
5
|
//
|
|
6
|
+
// The LANGUAGE itself — the opcode/register/directive tables and the tokenizer —
|
|
7
|
+
// is not here. It lives in `@gjsify/adwaita-core` (`source.ts`), typed on an
|
|
8
|
+
// eight-member stream interface that CodeMirror's `StringStream` satisfies
|
|
9
|
+
// structurally, so a second renderer can highlight 6502 without CodeMirror and
|
|
10
|
+
// this file stays what it claims to be: a binding.
|
|
11
|
+
//
|
|
6
12
|
// Token classes (mapped to @lezer/highlight tags via `asm6502TokenTable`):
|
|
7
13
|
// opcode — the 56 official mnemonics (LDA, STA, JMP, …) → keyword
|
|
8
14
|
// register — the accumulator/index registers A, X, Y → atom
|
|
@@ -13,197 +19,11 @@
|
|
|
13
19
|
// label — a definition (`loop:` or a bare name at column 0) → labelName
|
|
14
20
|
// operator — the immediate `#`, arithmetic + addressing punct → operator
|
|
15
21
|
// identifier — any other symbol reference (a named operand) → variableName
|
|
16
|
-
//
|
|
17
|
-
// Reference: refs/gjs-guide + the GtkSourceView `.lang` grammar shape.
|
|
18
22
|
|
|
19
|
-
import { StreamLanguage, type StreamParser
|
|
23
|
+
import { StreamLanguage, type StreamParser } from '@codemirror/language';
|
|
20
24
|
import { type Extension } from '@codemirror/state';
|
|
21
25
|
import { tags, type Tag } from '@lezer/highlight';
|
|
22
|
-
|
|
23
|
-
/** The 56 official NMOS 6502 mnemonics (uppercased for case-insensitive match). */
|
|
24
|
-
export const OPCODES: ReadonlySet<string> = new Set([
|
|
25
|
-
'ADC',
|
|
26
|
-
'AND',
|
|
27
|
-
'ASL',
|
|
28
|
-
'BCC',
|
|
29
|
-
'BCS',
|
|
30
|
-
'BEQ',
|
|
31
|
-
'BIT',
|
|
32
|
-
'BMI',
|
|
33
|
-
'BNE',
|
|
34
|
-
'BPL',
|
|
35
|
-
'BRK',
|
|
36
|
-
'BVC',
|
|
37
|
-
'BVS',
|
|
38
|
-
'CLC',
|
|
39
|
-
'CLD',
|
|
40
|
-
'CLI',
|
|
41
|
-
'CLV',
|
|
42
|
-
'CMP',
|
|
43
|
-
'CPX',
|
|
44
|
-
'CPY',
|
|
45
|
-
'DEC',
|
|
46
|
-
'DEX',
|
|
47
|
-
'DEY',
|
|
48
|
-
'EOR',
|
|
49
|
-
'INC',
|
|
50
|
-
'INX',
|
|
51
|
-
'INY',
|
|
52
|
-
'JMP',
|
|
53
|
-
'JSR',
|
|
54
|
-
'LDA',
|
|
55
|
-
'LDX',
|
|
56
|
-
'LDY',
|
|
57
|
-
'LSR',
|
|
58
|
-
'NOP',
|
|
59
|
-
'ORA',
|
|
60
|
-
'PHA',
|
|
61
|
-
'PHP',
|
|
62
|
-
'PLA',
|
|
63
|
-
'PLP',
|
|
64
|
-
'ROL',
|
|
65
|
-
'ROR',
|
|
66
|
-
'RTI',
|
|
67
|
-
'RTS',
|
|
68
|
-
'SBC',
|
|
69
|
-
'SEC',
|
|
70
|
-
'SED',
|
|
71
|
-
'SEI',
|
|
72
|
-
'STA',
|
|
73
|
-
'STX',
|
|
74
|
-
'STY',
|
|
75
|
-
'TAX',
|
|
76
|
-
'TAY',
|
|
77
|
-
'TSX',
|
|
78
|
-
'TXA',
|
|
79
|
-
'TXS',
|
|
80
|
-
'TYA',
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
|
-
/** The registers that appear as bare operands (`ASL A`, `LDA $00,X`). */
|
|
84
|
-
export const REGISTERS: ReadonlySet<string> = new Set(['A', 'X', 'Y']);
|
|
85
|
-
|
|
86
|
-
/** Assembler pseudo-ops. `define`/`dcb` are the easy6502 dialect; the rest are
|
|
87
|
-
* common cross-assembler directives (with the leading dot handled separately). */
|
|
88
|
-
export const DIRECTIVE_WORDS: ReadonlySet<string> = new Set([
|
|
89
|
-
'DEFINE',
|
|
90
|
-
'DCB',
|
|
91
|
-
'ORG',
|
|
92
|
-
'BYTE',
|
|
93
|
-
'WORD',
|
|
94
|
-
'DB',
|
|
95
|
-
'DW',
|
|
96
|
-
'DS',
|
|
97
|
-
'ASCII',
|
|
98
|
-
'ASCIIZ',
|
|
99
|
-
'TEXT',
|
|
100
|
-
'INCLUDE',
|
|
101
|
-
'INCBIN',
|
|
102
|
-
'EQU',
|
|
103
|
-
'END',
|
|
104
|
-
'PROC',
|
|
105
|
-
'ENDPROC',
|
|
106
|
-
'MACRO',
|
|
107
|
-
'ENDMACRO',
|
|
108
|
-
]);
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Classify an identifier-shaped token. Pure + side-effect-free so it is unit
|
|
112
|
-
* testable without a CodeMirror stream. `atLineStart` decides whether an
|
|
113
|
-
* unknown word is a label DEFINITION (column 0) or a symbol REFERENCE.
|
|
114
|
-
*/
|
|
115
|
-
export function classifyWord(word: string, atLineStart: boolean): string {
|
|
116
|
-
const upper = word.toUpperCase();
|
|
117
|
-
if (OPCODES.has(upper)) return 'opcode';
|
|
118
|
-
if (DIRECTIVE_WORDS.has(upper)) return 'directive';
|
|
119
|
-
if (REGISTERS.has(upper)) return 'register';
|
|
120
|
-
if (atLineStart) return 'label';
|
|
121
|
-
return 'identifier';
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Tokenize one token from the stream. Exported for unit tests (feed it a
|
|
126
|
-
* `StringStream`). Returns a token-class name (see file header) or `null` for
|
|
127
|
-
* whitespace/unclassified punctuation.
|
|
128
|
-
*/
|
|
129
|
-
export function token6502(stream: StringStream): string | null {
|
|
130
|
-
const atLineStart = stream.sol();
|
|
131
|
-
if (stream.eatSpace()) return null;
|
|
132
|
-
|
|
133
|
-
const ch = stream.peek();
|
|
134
|
-
if (ch === null || ch === undefined) return null;
|
|
135
|
-
|
|
136
|
-
// `; …` line comment.
|
|
137
|
-
if (ch === ';') {
|
|
138
|
-
stream.skipToEnd();
|
|
139
|
-
return 'comment';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Quoted string (used by `.ascii "…"` / `.byte "…"`).
|
|
143
|
-
if (ch === '"' || ch === "'") {
|
|
144
|
-
const quote = ch;
|
|
145
|
-
stream.next();
|
|
146
|
-
let escaped = false;
|
|
147
|
-
while (!stream.eol()) {
|
|
148
|
-
const c = stream.next();
|
|
149
|
-
if (c === undefined) break;
|
|
150
|
-
if (c === quote && !escaped) break;
|
|
151
|
-
escaped = c === '\\' && !escaped;
|
|
152
|
-
}
|
|
153
|
-
return 'string';
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Immediate-addressing prefix.
|
|
157
|
-
if (ch === '#') {
|
|
158
|
-
stream.next();
|
|
159
|
-
return 'operator';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Hex literal `$1a2b`.
|
|
163
|
-
if (ch === '$') {
|
|
164
|
-
stream.next();
|
|
165
|
-
stream.eatWhile(/[0-9a-fA-F]/);
|
|
166
|
-
return 'number';
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Binary literal `%1010`.
|
|
170
|
-
if (ch === '%') {
|
|
171
|
-
stream.next();
|
|
172
|
-
stream.eatWhile(/[01]/);
|
|
173
|
-
return 'number';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Decimal literal.
|
|
177
|
-
if (/[0-9]/.test(ch)) {
|
|
178
|
-
stream.eatWhile(/[0-9]/);
|
|
179
|
-
return 'number';
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Dot-directive `.org` / `.byte`.
|
|
183
|
-
if (ch === '.') {
|
|
184
|
-
stream.next();
|
|
185
|
-
if (stream.eatWhile(/[A-Za-z0-9_]/)) return 'directive';
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Identifier → opcode / register / directive / label / reference.
|
|
190
|
-
if (/[A-Za-z_]/.test(ch)) {
|
|
191
|
-
stream.eatWhile(/[A-Za-z0-9_]/);
|
|
192
|
-
const word = stream.current();
|
|
193
|
-
// An explicit `name:` is always a label definition.
|
|
194
|
-
if (stream.peek() === ':') return 'label';
|
|
195
|
-
return classifyWord(word, atLineStart);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Arithmetic / addressing punctuation.
|
|
199
|
-
if ('+-*/=<>,()[]&|^~'.includes(ch)) {
|
|
200
|
-
stream.next();
|
|
201
|
-
return 'operator';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
stream.next();
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
26
|
+
import { ADW_SOURCE_6502_LINE_COMMENT, tokenizeAsm6502 } from '@gjsify/adwaita-core';
|
|
207
27
|
|
|
208
28
|
/** Maps this mode's token-class names to @lezer/highlight tags. */
|
|
209
29
|
export const asm6502TokenTable: Record<string, Tag> = {
|
|
@@ -221,9 +41,9 @@ export const asm6502TokenTable: Record<string, Tag> = {
|
|
|
221
41
|
/** The StreamParser powering the 6502 mode. */
|
|
222
42
|
export const asm6502Parser: StreamParser<unknown> = {
|
|
223
43
|
name: '6502',
|
|
224
|
-
token: (stream) =>
|
|
44
|
+
token: (stream) => tokenizeAsm6502(stream),
|
|
225
45
|
tokenTable: asm6502TokenTable,
|
|
226
|
-
languageData: { commentTokens: { line:
|
|
46
|
+
languageData: { commentTokens: { line: ADW_SOURCE_6502_LINE_COMMENT } },
|
|
227
47
|
};
|
|
228
48
|
|
|
229
49
|
/** The 6502 assembly language extension for a CodeMirror EditorState. */
|
package/src/source-view/index.ts
CHANGED
|
@@ -8,16 +8,7 @@ export { AdwSourceView } from './adw-source-view.js';
|
|
|
8
8
|
|
|
9
9
|
// Building blocks, exported so apps/tests can reuse them (e.g. embed the 6502
|
|
10
10
|
// mode or the Adwaita theme in a bespoke EditorView).
|
|
11
|
-
export {
|
|
12
|
-
asm6502,
|
|
13
|
-
asm6502Parser,
|
|
14
|
-
asm6502TokenTable,
|
|
15
|
-
classifyWord,
|
|
16
|
-
token6502,
|
|
17
|
-
OPCODES,
|
|
18
|
-
REGISTERS,
|
|
19
|
-
DIRECTIVE_WORDS,
|
|
20
|
-
} from './asm6502.js';
|
|
11
|
+
export { asm6502, asm6502Parser, asm6502TokenTable } from './asm6502.js';
|
|
21
12
|
export {
|
|
22
13
|
adwaitaEditorTheme,
|
|
23
14
|
adwaitaHighlightStyle,
|
|
@@ -25,4 +16,19 @@ export {
|
|
|
25
16
|
SOURCE_VIEW_CSS,
|
|
26
17
|
SOURCE_VIEW_STYLE_ID,
|
|
27
18
|
} from './theme.js';
|
|
28
|
-
|
|
19
|
+
|
|
20
|
+
// The renderer-free half — language tables, tokenizer, gutter formatters — is
|
|
21
|
+
// owned by `@gjsify/adwaita-core`. Re-exported here under the spellings this
|
|
22
|
+
// subpath has always published, so nothing that imports them has to move.
|
|
23
|
+
export {
|
|
24
|
+
ADW_SOURCE_6502_DIRECTIVES as DIRECTIVE_WORDS,
|
|
25
|
+
ADW_SOURCE_6502_OPCODES as OPCODES,
|
|
26
|
+
ADW_SOURCE_6502_REGISTERS as REGISTERS,
|
|
27
|
+
ADW_SOURCE_HEX_STRIDE as HEX_ADDRESS_STRIDE,
|
|
28
|
+
classifyAsm6502Word as classifyWord,
|
|
29
|
+
formatHexAddress,
|
|
30
|
+
formatLineNumber,
|
|
31
|
+
stripSourceWhitespace as stripWhitespace,
|
|
32
|
+
tokenizeAsm6502 as token6502,
|
|
33
|
+
} from '@gjsify/adwaita-core';
|
|
34
|
+
export type { AdwSourceTokenClass, AdwSourceTokenStream } from '@gjsify/adwaita-core';
|