@gjsify/adwaita-web 0.61.0 → 0.63.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/README.md +31 -0
- package/lib/types/canvas2d/canvas2d-bridge.d.ts +55 -0
- package/lib/types/canvas2d/index.d.ts +1 -0
- package/lib/types/elements/gtk-window.d.ts +33 -0
- package/lib/types/pango-markup.d.ts +10 -0
- package/lib/types/pango-markup.spec.d.ts +1 -0
- package/lib/types/source-view/copy-glyph.d.ts +6 -0
- package/lib/types/source-view/copy-glyph.spec.d.ts +1 -0
- package/package.json +15 -11
- package/src/application.spec.ts +33 -0
- package/src/application.ts +16 -0
- package/src/canvas2d/canvas2d-bridge.ts +218 -0
- package/src/canvas2d/index.ts +5 -0
- package/src/elements/adw-alert-dialog.ts +8 -5
- package/src/elements/adw-banner.ts +14 -9
- package/src/elements/gtk-window.ts +146 -11
- package/src/gtk-window.spec.ts +122 -0
- package/src/pango-markup.spec.ts +257 -0
- package/src/pango-markup.ts +266 -0
- package/src/source-view/adw-source-view.ts +2 -8
- package/src/source-view/copy-glyph.spec.ts +46 -0
- package/src/source-view/copy-glyph.ts +52 -0
- package/src/test.browser.mts +4 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Pango markup → DOM NODES. Never markup, never a string the browser parses.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS. `use-markup` used to be implemented as `element.innerHTML =
|
|
4
|
+
// markup`, in three elements: `<adw-banner>`'s title, `<adw-alert-dialog>`'s heading and
|
|
5
|
+
// body. That is an INJECTION SINK dressed as a renderer, and it is also wrong on the
|
|
6
|
+
// merits: Pango markup and HTML are different languages (eleven tags, `<span>`
|
|
7
|
+
// attributes, five entities, no scripting — versus the whole of HTML). Mozilla's add-on
|
|
8
|
+
// reviewer flagged it, correctly.
|
|
9
|
+
//
|
|
10
|
+
// THE THREE STEPS, and why they are in this order:
|
|
11
|
+
//
|
|
12
|
+
// 1. `parsePangoMarkup` (`@gjsify/adwaita-core`, headless) — the STRING becomes a tree
|
|
13
|
+
// of plain data, or `null`. It is shared with the search pipeline, so the two can
|
|
14
|
+
// never disagree about what pango accepts.
|
|
15
|
+
// 2. Anything the tree does not name becomes NOTHING. A tag outside pango's set is not
|
|
16
|
+
// markup at all: the parse fails, and {@link renderPangoMarkup} writes the RAW
|
|
17
|
+
// STRING as a text node — the same fallback `gtk_label_set_markup` takes. There is
|
|
18
|
+
// no branch anywhere below that creates an element from an unrecognised name, so
|
|
19
|
+
// `<img src=x onerror=…>` and `<script>` cannot become nodes at all.
|
|
20
|
+
// 3. `<span>` attributes become CSS DECLARATIONS, never HTML attributes. Each is matched
|
|
21
|
+
// against a whitelist below and written with `style.setProperty`, which REJECTS an
|
|
22
|
+
// invalid value outright — so an attribute pango accepts but CSS does not (an unknown
|
|
23
|
+
// `weight`, a `font_family` with a quote in it) leaves the run unstyled rather than
|
|
24
|
+
// smuggling a declaration past the parser. Nothing here reads an attribute name off
|
|
25
|
+
// the element.
|
|
26
|
+
//
|
|
27
|
+
// WHY ELEMENTS AND NOT THE TAG NAMES VERBATIM. Six of pango's eleven tags map onto an
|
|
28
|
+
// HTML element of the SAME name with the SAME meaning (`b`, `i`, `u`, `s`, `sub`, `sup`),
|
|
29
|
+
// and the remaining four (`tt`, `big`, `small`, and `<span>` itself) are all
|
|
30
|
+
// {@link TAG_STYLES} entries: a style declaration on a `<span>`, because a semantic
|
|
31
|
+
// element is a thing an author's CSS can then reach, and because `<b>`/`<i>` are what a
|
|
32
|
+
// reader of the DOM expects. `<markup>` is pango's IMPLICIT root and draws nothing, so it
|
|
33
|
+
// becomes a `display: contents` span: its children land where the root was, with no box of
|
|
34
|
+
// its own for a stylesheet to trip over.
|
|
35
|
+
//
|
|
36
|
+
// `<tt>`/`<big>`/`<small>` are real HTML elements but OBSOLETE ones: they carry no
|
|
37
|
+
// presentational meaning in a modern UA stylesheet, which is exactly why their styles are
|
|
38
|
+
// spelled out here.
|
|
39
|
+
//
|
|
40
|
+
// Reference: packages/web/adwaita-core/src/pango-markup.ts (the parse, and why it is
|
|
41
|
+
// shared with the search pipeline)
|
|
42
|
+
// Reference: refs/gtk/gtk/gtklabel.c (gtk_label_set_markup's warn-and-keep-the-string
|
|
43
|
+
// fallback, which is what the `null` branch reproduces)
|
|
44
|
+
// Reference: Mozilla add-on reviewer guidance on innerHTML (the report this answers)
|
|
45
|
+
// Copyright (c) 2025 gjsify contributors. MIT License.
|
|
46
|
+
|
|
47
|
+
import { parsePangoMarkup, type PangoMarkupNode } from '@gjsify/adwaita-core';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pango tag → the HTML element that draws it, with the CSS it needs because no
|
|
51
|
+
* presentational UA rule covers it. Every entry is a CLOSED set: a tag missing here is a
|
|
52
|
+
* parse error, so there is no default branch to fall through.
|
|
53
|
+
*/
|
|
54
|
+
const TAG_STYLES: Readonly<Record<string, { element: string; style?: Readonly<Record<string, string>> }>> = {
|
|
55
|
+
b: { element: 'b' },
|
|
56
|
+
i: { element: 'i' },
|
|
57
|
+
u: { element: 'u' },
|
|
58
|
+
s: { element: 's' },
|
|
59
|
+
tt: { element: 'span', style: { 'font-family': 'monospace' } },
|
|
60
|
+
big: { element: 'span', style: { 'font-size': 'larger' } },
|
|
61
|
+
small: { element: 'span', style: { 'font-size': 'smaller' } },
|
|
62
|
+
sub: { element: 'sub' },
|
|
63
|
+
sup: { element: 'sup' },
|
|
64
|
+
markup: { element: 'span', style: { display: 'contents' } },
|
|
65
|
+
span: { element: 'span' },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `<span>` attribute → the CSS property it becomes, and the values accepted for it. An
|
|
70
|
+
* attribute NOT listed here is dropped — pango accepts more of them than a stylesheet can
|
|
71
|
+
* express (`rise`, `letter_spacing`, `lang`, `variant`, `stretch`, …), and dropping one is
|
|
72
|
+
* what keeps this from becoming a general-purpose attribute→style bridge.
|
|
73
|
+
*/
|
|
74
|
+
const SPAN_STYLES: Readonly<Record<string, string>> = {
|
|
75
|
+
weight: 'font-weight',
|
|
76
|
+
style: 'font-style',
|
|
77
|
+
underline: 'text-decoration-line',
|
|
78
|
+
strikethrough: 'text-decoration-line',
|
|
79
|
+
font_family: 'font-family',
|
|
80
|
+
size: 'font-size',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* pango's alternative spellings for the attributes above, which `<span>` documents as
|
|
85
|
+
* equivalent (`pango_markup.html`: "the `font_weight` attribute is an alias for `weight`",
|
|
86
|
+
* and so on). Resolved BEFORE {@link SPAN_STYLES}, so an alias reaches the same whitelist
|
|
87
|
+
* as the name it aliases — reading them as separate properties would let one of the pair
|
|
88
|
+
* through as an unstyled run while the other worked.
|
|
89
|
+
*/
|
|
90
|
+
const SPAN_ALIASES: Readonly<Record<string, string>> = {
|
|
91
|
+
font_weight: 'weight',
|
|
92
|
+
font_style: 'style',
|
|
93
|
+
face: 'font_family',
|
|
94
|
+
font_size: 'size',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The pango attribute values each property accepts, by NAME. `font-weight` takes pango's
|
|
99
|
+
* weight names and, beside them, pango's own numbers — CSS numbers weights on the same
|
|
100
|
+
* 1–1000 scale, so `weight="600"` is the one value that needs no translation. `weight`
|
|
101
|
+
* and `style` are the only two with a table; `text-decoration-line` takes CSS keywords,
|
|
102
|
+
* which are NOT pango's underline / strikethrough booleans — `underline="single"` has to
|
|
103
|
+
* become `underline`, and that mapping is one line per attribute, not a parser.
|
|
104
|
+
*/
|
|
105
|
+
const ATTRIBUTE_VALUES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
106
|
+
weight: {
|
|
107
|
+
ultralight: '100',
|
|
108
|
+
light: '300',
|
|
109
|
+
normal: '400',
|
|
110
|
+
medium: '500',
|
|
111
|
+
semibold: '600',
|
|
112
|
+
bold: 'bold',
|
|
113
|
+
ultrabold: '700',
|
|
114
|
+
heavy: '900',
|
|
115
|
+
},
|
|
116
|
+
style: { normal: 'normal', oblique: 'oblique', italic: 'italic' },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One node of the parsed tree, built. Returns `null` for a tag this file does not draw —
|
|
121
|
+
* the fail-closed half: an unrecognised tag DROPS its element (and pango's parser would
|
|
122
|
+
* have refused the string entirely anyway), rather than falling back to some catch-all
|
|
123
|
+
* that would put an arbitrary element into the tree.
|
|
124
|
+
*/
|
|
125
|
+
function buildNode(node: PangoMarkupNode, doc: Document): Node | null {
|
|
126
|
+
if (node.kind === 'text') return doc.createTextNode(node.text);
|
|
127
|
+
|
|
128
|
+
const spec = TAG_STYLES[node.tag];
|
|
129
|
+
if (spec === undefined) return null;
|
|
130
|
+
|
|
131
|
+
const el = doc.createElement(spec.element);
|
|
132
|
+
if (spec.style !== undefined)
|
|
133
|
+
for (const [property, value] of Object.entries(spec.style)) el.style.setProperty(property, value);
|
|
134
|
+
if (node.tag === 'span') applySpanAttributes(el, node.attributes);
|
|
135
|
+
|
|
136
|
+
for (const child of node.children) {
|
|
137
|
+
const built = buildNode(child, doc);
|
|
138
|
+
if (built !== null) el.append(built);
|
|
139
|
+
}
|
|
140
|
+
return el;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Write the recognised `<span>` attributes onto `el` as style declarations. */
|
|
144
|
+
function applySpanAttributes(el: HTMLElement, attributes: Readonly<Record<string, string>>): void {
|
|
145
|
+
// Additive, so `<span underline="single" strikethrough="true">` draws BOTH —
|
|
146
|
+
// `text-decoration` is a shorthand that would drop the first one.
|
|
147
|
+
const decorations: string[] = [];
|
|
148
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
149
|
+
const attribute = SPAN_ALIASES[name] ?? name;
|
|
150
|
+
const property = SPAN_STYLES[attribute];
|
|
151
|
+
if (property === undefined) continue;
|
|
152
|
+
|
|
153
|
+
if (attribute === 'underline') {
|
|
154
|
+
// pango: any value but `none` underlines.
|
|
155
|
+
if (value !== 'none') decorations.push('underline');
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (attribute === 'strikethrough') {
|
|
159
|
+
if (value !== 'false' && value !== 'none') decorations.push('line-through');
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (attribute === 'font_family') {
|
|
163
|
+
const family = sanitizeFontFamily(value);
|
|
164
|
+
if (family !== null) el.style.setProperty(property, family);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (attribute === 'size') {
|
|
168
|
+
const size = sanitizeFontSize(value);
|
|
169
|
+
if (size !== null) el.style.setProperty(property, size);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// pango also takes a numeric weight; CSS reads the same integers from 1 to 1000.
|
|
174
|
+
if (attribute === 'weight' && /^[0-9]{1,4}$/.test(value) && +value >= 1 && +value <= 1000) {
|
|
175
|
+
el.style.setProperty(property, value);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const named = ATTRIBUTE_VALUES[attribute]?.[value];
|
|
180
|
+
// An unmapped value is DROPPED rather than passed through: `setProperty` would
|
|
181
|
+
// reject an invalid declaration anyway, but an unvalidated string reaching CSS is
|
|
182
|
+
// the shape of bug this file exists to end, so it never gets there.
|
|
183
|
+
if (named !== undefined) el.style.setProperty(property, named);
|
|
184
|
+
}
|
|
185
|
+
if (decorations.length > 0) el.style.setProperty('text-decoration-line', decorations.join(' '));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The generic families CSS parses unquoted; everything else must be a plain name. */
|
|
189
|
+
const CSS_GENERIC_FAMILIES: ReadonlySet<string> = new Set([
|
|
190
|
+
'serif',
|
|
191
|
+
'sans-serif',
|
|
192
|
+
'monospace',
|
|
193
|
+
'cursive',
|
|
194
|
+
'fantasy',
|
|
195
|
+
'system-ui',
|
|
196
|
+
'ui-serif',
|
|
197
|
+
'ui-sans-serif',
|
|
198
|
+
'ui-monospace',
|
|
199
|
+
'ui-rounded',
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A pango `font_family` list as a CSS `font-family`, or `null` when any entry cannot be
|
|
204
|
+
* one. Pango takes a bare comma-separated list; CSS additionally parses quotes and
|
|
205
|
+
* escapes, so an entry holding either is refused as a WHOLE list rather than escaped —
|
|
206
|
+
* refusing is the answer that cannot produce a family name CSS reads differently.
|
|
207
|
+
*/
|
|
208
|
+
function sanitizeFontFamily(value: string): string | null {
|
|
209
|
+
const families: string[] = [];
|
|
210
|
+
for (const raw of value.split(',')) {
|
|
211
|
+
const family = raw.trim();
|
|
212
|
+
if (family === '') continue;
|
|
213
|
+
const generic = CSS_GENERIC_FAMILIES.has(family.toLowerCase());
|
|
214
|
+
// A quoted CSS identifier is the general form; a bare one is safe only when it is
|
|
215
|
+
// a plain name, so anything carrying a quote, backslash or semicolon is refused.
|
|
216
|
+
if (!generic && !/^["']?[A-Za-z0-9 _-]+["']?$/.test(family)) return null;
|
|
217
|
+
families.push(family);
|
|
218
|
+
}
|
|
219
|
+
return families.length > 0 ? families.join(', ') : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A pango `size` as a CSS `font-size`, or `null` when it cannot be one. */
|
|
223
|
+
function sanitizeFontSize(value: string): string | null {
|
|
224
|
+
const trimmed = value.trim();
|
|
225
|
+
// pango's named sizes, which CSS ALSO names — the same absolute ladder, plus pango's
|
|
226
|
+
// two relative steps, so the whole set needs no conversion at all.
|
|
227
|
+
const named: Readonly<Record<string, string>> = {
|
|
228
|
+
'xx-small': 'xx-small',
|
|
229
|
+
'x-small': 'x-small',
|
|
230
|
+
small: 'small',
|
|
231
|
+
medium: 'medium',
|
|
232
|
+
large: 'large',
|
|
233
|
+
'x-large': 'x-large',
|
|
234
|
+
'xx-large': 'xx-large',
|
|
235
|
+
smaller: 'smaller',
|
|
236
|
+
larger: 'larger',
|
|
237
|
+
};
|
|
238
|
+
const namedSize = named[trimmed];
|
|
239
|
+
if (namedSize !== undefined) return namedSize;
|
|
240
|
+
// A bare pango size is in 1024ths of a point (`size="12288"` is 12pt), so it is scaled,
|
|
241
|
+
// never passed on: CSS would read a bare number as no length at all.
|
|
242
|
+
if (/^[0-9]+$/.test(trimmed)) return `${+trimmed / 1024}pt`;
|
|
243
|
+
// Since pango 1.50: points with their unit, and a percentage of the parent.
|
|
244
|
+
if (/^[0-9]+(\.[0-9]+)?pt$/.test(trimmed)) return trimmed;
|
|
245
|
+
if (/^[0-9]+(\.[0-9]+)?%$/.test(trimmed)) return trimmed;
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Render Pango markup into `target`, replacing its content — the one door every
|
|
251
|
+
* `use-markup` surface in this package goes through.
|
|
252
|
+
*
|
|
253
|
+
* The `null` branch is the C fallback and is the SECURITY property: markup that does not
|
|
254
|
+
* parse is written as TEXT, so an unrecognised tag can never become an element. Callers
|
|
255
|
+
* that want markup only when a caller has asked for it read that from their own flag and
|
|
256
|
+
* pass plain text here — this function never decides whether markup is meant.
|
|
257
|
+
*/
|
|
258
|
+
export function renderPangoMarkup(target: HTMLElement, markup: string): void {
|
|
259
|
+
const nodes = parsePangoMarkup(markup);
|
|
260
|
+
if (nodes === null) {
|
|
261
|
+
target.textContent = markup;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const doc = target.ownerDocument ?? document;
|
|
265
|
+
target.replaceChildren(...nodes.map((node) => buildNode(node, doc)).filter((node): node is Node => node !== null));
|
|
266
|
+
}
|
|
@@ -35,13 +35,7 @@ import { asm6502 } from './asm6502.js';
|
|
|
35
35
|
import { adwaitaEditorTheme, adwaitaHighlightStyle, ensureSourceViewStyleInjected } from './theme.js';
|
|
36
36
|
import { formatHexAddress, formatLineNumber, stripSourceWhitespace } from '@gjsify/adwaita-core';
|
|
37
37
|
import { createGtkImage } from '../elements/gtk-image.js';
|
|
38
|
-
|
|
39
|
-
/** A recognisable copy glyph rendered inline so the subpath needs no icon CSS. */
|
|
40
|
-
const COPY_SVG =
|
|
41
|
-
'<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">' +
|
|
42
|
-
'<path d="M4 1a2 2 0 0 0-2 2v7h2V3h6V1H4z"/>' +
|
|
43
|
-
'<path d="M7 4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H7zm0 2h5v7H7V6z"/>' +
|
|
44
|
-
'</svg>';
|
|
38
|
+
import { createCopyGlyph } from './copy-glyph.js';
|
|
45
39
|
|
|
46
40
|
/** Parse a boolean attribute: present (any value but "false") ⇒ true. */
|
|
47
41
|
function boolAttr(value: string | null): boolean {
|
|
@@ -400,7 +394,7 @@ export class AdwSourceView extends HTMLElement {
|
|
|
400
394
|
private _renderCopyIcon(): void {
|
|
401
395
|
const name = this._copyButtonIcon || 'edit-copy';
|
|
402
396
|
if (name === 'edit-copy' || name === 'copy' || name === '') {
|
|
403
|
-
this._copyButton.
|
|
397
|
+
this._copyButton.replaceChildren(createCopyGlyph(this._copyButton.ownerDocument));
|
|
404
398
|
return;
|
|
405
399
|
}
|
|
406
400
|
// A non-default icon uses the adwaita-web mask class (requires the main
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The copy glyph the `<adw-source-view>` copy button draws.
|
|
2
|
+
//
|
|
3
|
+
// The regression this holds: the glyph WAS a constant SVG string assigned with
|
|
4
|
+
// `innerHTML`. Constant and safe-until-edited, and the reviewer who flagged the two
|
|
5
|
+
// `use-markup` sinks in the same package flagged the shape, so it is built from nodes now.
|
|
6
|
+
// These rows are the ones a re-parsing implementation would fail: the namespace, the
|
|
7
|
+
// element names, and that nothing about it arrives as a string the browser parsed.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
10
|
+
|
|
11
|
+
import { createCopyGlyph } from './copy-glyph.js';
|
|
12
|
+
|
|
13
|
+
export const CopyGlyphTest = async () => {
|
|
14
|
+
await describe('createCopyGlyph — the copy button still has its icon', async () => {
|
|
15
|
+
await it('is an <svg> in the SVG namespace, sized and viewBox-ed like every other glyph', () => {
|
|
16
|
+
const glyph = createCopyGlyph();
|
|
17
|
+
expect(glyph.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
18
|
+
expect(glyph.tagName).toBe('svg');
|
|
19
|
+
expect(glyph.getAttribute('width')).toBe('16');
|
|
20
|
+
expect(glyph.getAttribute('height')).toBe('16');
|
|
21
|
+
expect(glyph.getAttribute('viewBox')).toBe('0 0 16 16');
|
|
22
|
+
// The button owns the accessible name; the glyph is decoration inside it.
|
|
23
|
+
expect(glyph.getAttribute('aria-hidden')).toBe('true');
|
|
24
|
+
expect(glyph.getAttribute('fill')).toBe('currentColor');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await it('draws the two sheets as <path> elements, each with a real outline', () => {
|
|
28
|
+
const glyph = createCopyGlyph();
|
|
29
|
+
const paths = [...glyph.children];
|
|
30
|
+
expect(paths.map((el) => el.tagName)).toStrictEqual(['path', 'path']);
|
|
31
|
+
for (const path of paths) {
|
|
32
|
+
expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
|
|
33
|
+
expect((path.getAttribute('d') ?? '').length).toBeGreaterThan(10);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await it('lands in a button as a drawable glyph', () => {
|
|
38
|
+
const button = document.createElement('button');
|
|
39
|
+
document.body.appendChild(button);
|
|
40
|
+
button.replaceChildren(createCopyGlyph(button.ownerDocument));
|
|
41
|
+
expect(button.querySelector('svg')).not.toBe(null);
|
|
42
|
+
expect(button.getBoundingClientRect().height).toBeGreaterThan(0);
|
|
43
|
+
button.remove();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The copy button's glyph, built as NODES.
|
|
2
|
+
//
|
|
3
|
+
// It was a constant SVG string handed to `innerHTML`, which is safe only until someone
|
|
4
|
+
// edits the string — and it was, next to two `use-markup` surfaces that did the same with
|
|
5
|
+
// AUTHOR-controlled markup, the reason Mozilla's add-on reviewer flagged this package.
|
|
6
|
+
// The glyph is a constant and the sink was not: `createElementNS` costs two lines more and
|
|
7
|
+
// cannot parse anything.
|
|
8
|
+
//
|
|
9
|
+
// It lives in its own module, not in `pango-markup.ts` (which is about Pango markup) and
|
|
10
|
+
// not inline in `adw-source-view.ts` (which would drag CodeMirror into any suite that
|
|
11
|
+
// wants to check the shape) — so a test can assert the glyph without pulling the editor in.
|
|
12
|
+
//
|
|
13
|
+
// The two paths are the two sheets of the `edit-copy` mark: the back sheet offset up-left,
|
|
14
|
+
// the front sheet inset. `aria-hidden` because the button carries the accessible name, the
|
|
15
|
+
// same split `gtk-image.ts` documents.
|
|
16
|
+
//
|
|
17
|
+
// Reference: refs/adwaita-icon-theme/Adwaita/scalable/status-icons/edit-copy-symbolic.svg
|
|
18
|
+
// (the same two-sheet mark, path data rounded to integers here)
|
|
19
|
+
// Copyright (c) 2025 gjsify contributors. MIT License.
|
|
20
|
+
|
|
21
|
+
/** The SVG namespace, spelled once — `createElementNS` needs it on every node. */
|
|
22
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
23
|
+
|
|
24
|
+
/** The two sheet outlines, in draw order (back, then front). */
|
|
25
|
+
const SHEETS = [
|
|
26
|
+
'M4 1a2 2 0 0 0-2 2v7h2V3h6V1H4z',
|
|
27
|
+
'M7 4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H7zm0 2h5v7H7V6z',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** The size both spinners and every symbolic icon in this package draw at. */
|
|
31
|
+
const GLYPH_SIZE = '16';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A recognisable copy glyph rendered inline so the opt-in `source-view` subpath needs no
|
|
35
|
+
* icon CSS. `doc` is a parameter so a caller outside the main document (a shadow root in
|
|
36
|
+
* another frame) gets nodes its own tree can adopt.
|
|
37
|
+
*/
|
|
38
|
+
export function createCopyGlyph(doc: Document = document): SVGSVGElement {
|
|
39
|
+
const svg = doc.createElementNS(SVG_NS, 'svg');
|
|
40
|
+
svg.setAttribute('width', GLYPH_SIZE);
|
|
41
|
+
svg.setAttribute('height', GLYPH_SIZE);
|
|
42
|
+
svg.setAttribute('viewBox', `0 0 ${GLYPH_SIZE} ${GLYPH_SIZE}`);
|
|
43
|
+
svg.setAttribute('fill', 'currentColor');
|
|
44
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
45
|
+
|
|
46
|
+
for (const d of SHEETS) {
|
|
47
|
+
const path = doc.createElementNS(SVG_NS, 'path');
|
|
48
|
+
path.setAttribute('d', d);
|
|
49
|
+
svg.append(path);
|
|
50
|
+
}
|
|
51
|
+
return svg;
|
|
52
|
+
}
|
package/src/test.browser.mts
CHANGED
|
@@ -59,6 +59,8 @@ import { GtkAboutDialogTest } from './gtk-about-dialog.spec.js';
|
|
|
59
59
|
import { GtkEmojiChooserTest } from './gtk-emoji-chooser.spec.js';
|
|
60
60
|
import { GtkPrintDialogsTest } from './gtk-print-dialogs.spec.js';
|
|
61
61
|
import { AdwBannerTest } from './adw-banner.spec.js';
|
|
62
|
+
import { PangoMarkupTest } from './pango-markup.spec.js';
|
|
63
|
+
import { CopyGlyphTest } from './source-view/copy-glyph.spec.js';
|
|
62
64
|
import { AdwButtonContentTest } from './adw-button-content.spec.js';
|
|
63
65
|
import { GtkImageTest } from './gtk-image.spec.js';
|
|
64
66
|
import { AdwIconRegistryTest } from './icon-registry.spec.js';
|
|
@@ -191,6 +193,8 @@ run({
|
|
|
191
193
|
AdwAboutDialogTest,
|
|
192
194
|
AdwAlertDialogTest,
|
|
193
195
|
AdwBannerTest,
|
|
196
|
+
PangoMarkupTest,
|
|
197
|
+
CopyGlyphTest,
|
|
194
198
|
AdwButtonContentTest,
|
|
195
199
|
GtkImageTest,
|
|
196
200
|
AdwIconRegistryTest,
|