@markuplint/ml-spec 5.0.0 → 5.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.
- package/CHANGELOG.md +15 -0
- package/lib/algorithm/aria/accname/element-names.js +9 -8
- package/lib/algorithm/aria/accname/helpers.d.ts +13 -2
- package/lib/algorithm/aria/accname/helpers.js +57 -19
- package/lib/algorithm/aria/accname/label-steps.js +2 -2
- package/lib/algorithm/aria/accname/svg-helpers.d.ts +7 -0
- package/lib/algorithm/aria/accname/svg-helpers.js +7 -0
- package/lib/algorithm/aria/accname/types.d.ts +11 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [5.1.0](https://github.com/markuplint/markuplint/compare/v5.0.1...v5.1.0) (2026-10-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **ml-spec:** read option, SVG title and control text through the accname resolver ([4c66d1f](https://github.com/markuplint/markuplint/commit/4c66d1fb960e746dc9f3c6cec0c8b3c9284a657e))
|
|
11
|
+
- **ml-spec:** read the SVG title and select options through the accname resolver ([0a7c162](https://github.com/markuplint/markuplint/commit/0a7c162a83a553d7872388184ea0020735def6fb)), closes [#4067](https://github.com/markuplint/markuplint/issues/4067)
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- **ml-spec:** let the accname resolver supply the child nodes to traverse ([caa8263](https://github.com/markuplint/markuplint/commit/caa82633dcecb604b32e8e9438b1096c116f059c)), closes [#4056](https://github.com/markuplint/markuplint/issues/4056)
|
|
16
|
+
|
|
17
|
+
## [5.0.1](https://github.com/markuplint/markuplint/compare/v5.0.0...v5.0.1) (2026-09-22)
|
|
18
|
+
|
|
19
|
+
**Note:** Version bump only for package @markuplint/ml-spec
|
|
20
|
+
|
|
6
21
|
# [5.0.0](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.7...v5.0.0) (2026-09-11)
|
|
7
22
|
|
|
8
23
|
**Note:** Version bump only for package @markuplint/ml-spec
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
2
|
import { DEFAULT_IMAGE_LABEL, DEFAULT_RESET_LABEL, DEFAULT_SUBMIT_LABEL, TEXT_INPUT_TYPES, } from '../../../const/index.js';
|
|
3
|
-
import { findChildByLocalName, getInputType, isSvgElement, makeResult, resolveNameFromContent } from './helpers.js';
|
|
3
|
+
import { findChildByLocalName, getInputType, getTextContent, isSvgElement, makeResult, resolveNameFromContent, } from './helpers.js';
|
|
4
4
|
import { resolveLabelText } from './label-steps.js';
|
|
5
5
|
/**
|
|
6
6
|
* Implements AccName 1.2 §4.3.2 Step 2E: for elements that have a native
|
|
@@ -15,7 +15,7 @@ import { resolveLabelText } from './label-steps.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export function getElementSpecificName(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
17
17
|
if (isSvgElement(el)) {
|
|
18
|
-
return handleSvgElement(el);
|
|
18
|
+
return handleSvgElement(el, resolver);
|
|
19
19
|
}
|
|
20
20
|
const { localName } = el;
|
|
21
21
|
switch (localName) {
|
|
@@ -185,7 +185,7 @@ function handleButton(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
|
185
185
|
* @see https://www.w3.org/TR/html-aam-1.0/#el-fieldset
|
|
186
186
|
*/
|
|
187
187
|
function handleFieldset(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
188
|
-
const legend = findChildByLocalName(el, 'legend');
|
|
188
|
+
const legend = findChildByLocalName(el, 'legend', resolver);
|
|
189
189
|
if (legend) {
|
|
190
190
|
const content = resolveNameFromContent(legend, resolver, visited, computeFn, inLabelledbyTraversal);
|
|
191
191
|
if (content.trim()) {
|
|
@@ -204,7 +204,7 @@ function handleFieldset(el, resolver, visited, computeFn, inLabelledbyTraversal)
|
|
|
204
204
|
* @see https://www.w3.org/TR/html-aam-1.0/#el-table
|
|
205
205
|
*/
|
|
206
206
|
function handleTable(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
207
|
-
const caption = findChildByLocalName(el, 'caption');
|
|
207
|
+
const caption = findChildByLocalName(el, 'caption', resolver);
|
|
208
208
|
if (caption) {
|
|
209
209
|
const content = resolveNameFromContent(caption, resolver, visited, computeFn, inLabelledbyTraversal);
|
|
210
210
|
if (content.trim()) {
|
|
@@ -310,10 +310,11 @@ function handleTitleOnly(el) {
|
|
|
310
310
|
* SVG-AAM: SVG elements get name from `<title>` child element.
|
|
311
311
|
* @see https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd — SVG-AAM §8.1
|
|
312
312
|
*/
|
|
313
|
-
function handleSvgElement(el) {
|
|
314
|
-
const titleEl = findChildByLocalName(el, 'title');
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
function handleSvgElement(el, resolver) {
|
|
314
|
+
const titleEl = findChildByLocalName(el, 'title', resolver);
|
|
315
|
+
const title = titleEl ? getTextContent(titleEl, resolver) : '';
|
|
316
|
+
if (title.trim()) {
|
|
317
|
+
return makeResult(title, 'svg-title');
|
|
317
318
|
}
|
|
318
319
|
return null;
|
|
319
320
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccnameElement, AccnameResolver, AccnameResult, AccnameSource } from './types.js';
|
|
1
|
+
import type { AccnameElement, AccnameNode, AccnameResolver, AccnameResult, AccnameSource } from './types.js';
|
|
2
2
|
export { EMBEDDED_CONTROL_ROLES } from '../../../const/index.js';
|
|
3
3
|
export declare function makeResult(name: string, source: AccnameSource | null): AccnameResult;
|
|
4
4
|
export declare function flattenText(text: string): string;
|
|
@@ -22,7 +22,18 @@ export declare function flattenText(text: string): string;
|
|
|
22
22
|
* @see https://www.w3.org/TR/accname-1.2/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
|
|
23
23
|
*/
|
|
24
24
|
export declare function resolveNameFromContent(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult, inLabelledbyTraversal: boolean): string;
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The child nodes to traverse for `el`: what the resolver says it renders,
|
|
27
|
+
* or `el.childNodes` when it says nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getChildNodes(el: AccnameElement, resolver: AccnameResolver): Iterable<AccnameNode>;
|
|
30
|
+
/**
|
|
31
|
+
* `el.textContent` as the resolver sees the element: the text of the nodes it
|
|
32
|
+
* says the element renders, at any depth. Text written inside a pretended
|
|
33
|
+
* component that never renders its children is not among them.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getTextContent(el: AccnameElement, resolver: AccnameResolver): string;
|
|
36
|
+
export declare function findChildByLocalName(el: AccnameElement, localName: string, resolver?: AccnameResolver): AccnameElement | null;
|
|
26
37
|
export declare function findAncestorLabel(el: AccnameElement): AccnameElement | null;
|
|
27
38
|
export declare function resolveLabel(el: AccnameElement, resolver: AccnameResolver): readonly AccnameElement[];
|
|
28
39
|
export declare function getInputType(el: AccnameElement): string;
|
|
@@ -32,7 +32,7 @@ export function flattenText(text) {
|
|
|
32
32
|
*/
|
|
33
33
|
export function resolveNameFromContent(el, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
34
34
|
const parts = [];
|
|
35
|
-
for (const child of el
|
|
35
|
+
for (const child of getChildNodes(el, resolver)) {
|
|
36
36
|
if (child.nodeType === TEXT_NODE) {
|
|
37
37
|
parts.push(child.textContent ?? '');
|
|
38
38
|
}
|
|
@@ -40,7 +40,7 @@ export function resolveNameFromContent(el, resolver, visited, computeFn, inLabel
|
|
|
40
40
|
const childEl = child;
|
|
41
41
|
if (resolver.isEmbeddedControl(childEl)) {
|
|
42
42
|
// Step 2C: Embedded controls — use value directly, aria-label is ignored
|
|
43
|
-
parts.push(getEmbeddedControlValue(childEl));
|
|
43
|
+
parts.push(getEmbeddedControlValue(childEl, resolver));
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
46
|
const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
|
|
@@ -78,14 +78,14 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
|
|
|
78
78
|
return '';
|
|
79
79
|
}
|
|
80
80
|
const parts = [];
|
|
81
|
-
for (const child of el
|
|
81
|
+
for (const child of getChildNodes(el, resolver)) {
|
|
82
82
|
if (child.nodeType === TEXT_NODE) {
|
|
83
83
|
parts.push(child.textContent ?? '');
|
|
84
84
|
}
|
|
85
85
|
else if (child.nodeType === ELEMENT_NODE) {
|
|
86
86
|
const childEl = child;
|
|
87
87
|
if (resolver.isEmbeddedControl(childEl)) {
|
|
88
|
-
parts.push(getEmbeddedControlValue(childEl));
|
|
88
|
+
parts.push(getEmbeddedControlValue(childEl, resolver));
|
|
89
89
|
}
|
|
90
90
|
else {
|
|
91
91
|
const result = computeFn(childEl, resolver, inLabelledbyTraversal, visited);
|
|
@@ -110,7 +110,7 @@ function collectTextContent(el, resolver, visited, computeFn, inLabelledbyTraver
|
|
|
110
110
|
*
|
|
111
111
|
* @see https://www.w3.org/TR/accname-1.2/#comp_embedded_control — AccName 1.2 §4.3.2 Step 2C
|
|
112
112
|
*/
|
|
113
|
-
function getEmbeddedControlValue(el) {
|
|
113
|
+
function getEmbeddedControlValue(el, resolver) {
|
|
114
114
|
const role = el.getAttribute('role')?.trim().split(/\s+/)[0];
|
|
115
115
|
// Range controls: slider, spinbutton, input[type=range]
|
|
116
116
|
if (role === 'slider' || role === 'spinbutton' || (el.localName === 'input' && getInputType(el) === 'range')) {
|
|
@@ -126,7 +126,7 @@ function getEmbeddedControlValue(el) {
|
|
|
126
126
|
if (value?.trim()) {
|
|
127
127
|
return value;
|
|
128
128
|
}
|
|
129
|
-
return el
|
|
129
|
+
return getTextContent(el, resolver);
|
|
130
130
|
}
|
|
131
131
|
// Textbox/combobox: value attr -> textContent
|
|
132
132
|
if (role === 'textbox' ||
|
|
@@ -138,13 +138,13 @@ function getEmbeddedControlValue(el) {
|
|
|
138
138
|
if (value != null) {
|
|
139
139
|
return value;
|
|
140
140
|
}
|
|
141
|
-
return el
|
|
141
|
+
return getTextContent(el, resolver);
|
|
142
142
|
}
|
|
143
143
|
// Listbox/select: selected option text
|
|
144
144
|
if (el.localName === 'select') {
|
|
145
|
-
return getSelectedOptionText(el);
|
|
145
|
+
return getSelectedOptionText(el, resolver);
|
|
146
146
|
}
|
|
147
|
-
return el
|
|
147
|
+
return getTextContent(el, resolver);
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
150
|
* Gets the text content of the selected `<option>` elements within a `<select>`.
|
|
@@ -170,16 +170,16 @@ function getEmbeddedControlValue(el) {
|
|
|
170
170
|
*
|
|
171
171
|
* @see https://github.com/markuplint/markuplint/issues/2069 — `<select>` and `<selectedcontent>` support
|
|
172
172
|
*/
|
|
173
|
-
function getSelectedOptionText(el) {
|
|
174
|
-
const options = collectOptions(el);
|
|
173
|
+
function getSelectedOptionText(el, resolver) {
|
|
174
|
+
const options = collectOptions(el, resolver);
|
|
175
175
|
const selected = options.filter(opt => opt.hasAttribute('selected'));
|
|
176
176
|
if (selected.length > 0) {
|
|
177
|
-
return selected.map(opt => opt.
|
|
177
|
+
return selected.map(opt => getTextContent(opt, resolver).trim()).join(' ');
|
|
178
178
|
}
|
|
179
179
|
// No explicit selected attr: HTML spec says first non-disabled option is selected
|
|
180
180
|
// (only for non-multiple selects, but we approximate for all)
|
|
181
181
|
const first = options.find(opt => !opt.hasAttribute('disabled'));
|
|
182
|
-
return first
|
|
182
|
+
return first ? getTextContent(first, resolver).trim() : '';
|
|
183
183
|
}
|
|
184
184
|
/**
|
|
185
185
|
* **Note (#2069):** Customizable `<select>` allows non-option children
|
|
@@ -189,14 +189,14 @@ function getSelectedOptionText(el) {
|
|
|
189
189
|
*
|
|
190
190
|
* @see https://github.com/markuplint/markuplint/issues/2069
|
|
191
191
|
*/
|
|
192
|
-
function collectOptions(el) {
|
|
192
|
+
function collectOptions(el, resolver) {
|
|
193
193
|
const result = [];
|
|
194
|
-
for (const child of el
|
|
194
|
+
for (const child of getChildElements(el, resolver)) {
|
|
195
195
|
if (child.localName === 'option') {
|
|
196
196
|
result.push(child);
|
|
197
197
|
}
|
|
198
198
|
else if (child.localName === 'optgroup') {
|
|
199
|
-
for (const grandchild of child
|
|
199
|
+
for (const grandchild of getChildElements(child, resolver)) {
|
|
200
200
|
if (grandchild.localName === 'option') {
|
|
201
201
|
result.push(grandchild);
|
|
202
202
|
}
|
|
@@ -208,9 +208,47 @@ function collectOptions(el) {
|
|
|
208
208
|
function isTextLikeInput(el) {
|
|
209
209
|
return TEXT_LIKE_INPUT_TYPES.has(getInputType(el));
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
/**
|
|
212
|
+
* The child nodes to traverse for `el`: what the resolver says it renders,
|
|
213
|
+
* or `el.childNodes` when it says nothing.
|
|
214
|
+
*/
|
|
215
|
+
export function getChildNodes(el, resolver) {
|
|
216
|
+
return resolver.getChildNodes?.(el) ?? el.childNodes;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* `el.textContent` as the resolver sees the element: the text of the nodes it
|
|
220
|
+
* says the element renders, at any depth. Text written inside a pretended
|
|
221
|
+
* component that never renders its children is not among them.
|
|
222
|
+
*/
|
|
223
|
+
export function getTextContent(el, resolver) {
|
|
224
|
+
if (!resolver.getChildNodes) {
|
|
225
|
+
return el.textContent ?? '';
|
|
226
|
+
}
|
|
227
|
+
let text = '';
|
|
228
|
+
for (const child of resolver.getChildNodes(el)) {
|
|
229
|
+
if (child.nodeType === TEXT_NODE) {
|
|
230
|
+
text += child.textContent ?? '';
|
|
231
|
+
}
|
|
232
|
+
else if (child.nodeType === ELEMENT_NODE) {
|
|
233
|
+
text += getTextContent(child, resolver);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return text;
|
|
237
|
+
}
|
|
238
|
+
function getChildElements(el, resolver) {
|
|
239
|
+
return [...getChildNodes(el, resolver)].filter((child) => child.nodeType === ELEMENT_NODE);
|
|
240
|
+
}
|
|
241
|
+
export function findChildByLocalName(el, localName, resolver) {
|
|
242
|
+
if (!resolver?.getChildNodes) {
|
|
243
|
+
for (const child of el.children) {
|
|
244
|
+
if (child.localName === localName) {
|
|
245
|
+
return child;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
for (const child of resolver.getChildNodes(el)) {
|
|
251
|
+
if (child.nodeType === ELEMENT_NODE && child.localName === localName) {
|
|
214
252
|
return child;
|
|
215
253
|
}
|
|
216
254
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
|
|
2
2
|
import { ELEMENT_NODE, TEXT_NODE } from '../../../const/index.js';
|
|
3
|
-
import { flattenText, makeResult, resolveLabel } from './helpers.js';
|
|
3
|
+
import { flattenText, getChildNodes, makeResult, resolveLabel } from './helpers.js';
|
|
4
4
|
/**
|
|
5
5
|
* Part of AccName 1.2 §4.3.2 Step 2E — for labelable elements, the HTML label
|
|
6
6
|
* association is checked before other element-specific rules.
|
|
@@ -33,7 +33,7 @@ export function resolveLabelText(el, resolver, visited, computeFn, inLabelledbyT
|
|
|
33
33
|
*/
|
|
34
34
|
function collectLabelText(label, labeledElement, resolver, visited, computeFn, inLabelledbyTraversal) {
|
|
35
35
|
const parts = [];
|
|
36
|
-
for (const child of label
|
|
36
|
+
for (const child of getChildNodes(label, resolver)) {
|
|
37
37
|
if (child.nodeType === TEXT_NODE) {
|
|
38
38
|
parts.push(child.textContent ?? '');
|
|
39
39
|
}
|
|
@@ -10,6 +10,13 @@ import type { AccnameElement } from './types.js';
|
|
|
10
10
|
*
|
|
11
11
|
* Checks (in order): `aria-label`, `aria-labelledby`, `<title>` child, `<desc>` child.
|
|
12
12
|
*
|
|
13
|
+
* Known limitation: this reads `el.children`, not `AccnameResolver.getChildNodes`,
|
|
14
|
+
* because `getComputedRole` has no resolver to pass. For a pretended component it
|
|
15
|
+
* therefore answers from the children written at the usage site, while the name
|
|
16
|
+
* computation reads the ones the component renders. A `<title>` that comes only
|
|
17
|
+
* from the `contents` of a pretender names nothing in either: those entries are
|
|
18
|
+
* childless, so it has no text.
|
|
19
|
+
*
|
|
13
20
|
* @param el - The SVG element to check for accessible name sources
|
|
14
21
|
* @returns True if the element has aria-label, aria-labelledby, or a title/desc child
|
|
15
22
|
* @see https://www.w3.org/TR/svg-aam-1.0/#include_elements — SVG-AAM §5.1.1
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Checks (in order): `aria-label`, `aria-labelledby`, `<title>` child, `<desc>` child.
|
|
12
12
|
*
|
|
13
|
+
* Known limitation: this reads `el.children`, not `AccnameResolver.getChildNodes`,
|
|
14
|
+
* because `getComputedRole` has no resolver to pass. For a pretended component it
|
|
15
|
+
* therefore answers from the children written at the usage site, while the name
|
|
16
|
+
* computation reads the ones the component renders. A `<title>` that comes only
|
|
17
|
+
* from the `contents` of a pretender names nothing in either: those entries are
|
|
18
|
+
* childless, so it has no text.
|
|
19
|
+
*
|
|
13
20
|
* @param el - The SVG element to check for accessible name sources
|
|
14
21
|
* @returns True if the element has aria-label, aria-labelledby, or a title/desc child
|
|
15
22
|
* @see https://www.w3.org/TR/svg-aam-1.0/#include_elements — SVG-AAM §5.1.1
|
|
@@ -48,6 +48,17 @@ export interface AccnameResolver {
|
|
|
48
48
|
* accessible names through configuration.
|
|
49
49
|
*/
|
|
50
50
|
getPrecomputedName?(el: AccnameElement): string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the child nodes to traverse for `el`, in place of `el.childNodes`.
|
|
53
|
+
* Used by ml-core for a pretended component: it renders its own elements around
|
|
54
|
+
* the children given at the usage site (`contents` of the pretender), and those
|
|
55
|
+
* can carry the accessible name (`<button><img alt="Save"></button>`).
|
|
56
|
+
* Consulted by name-from-content, label text, the legend/caption/SVG `title`
|
|
57
|
+
* lookups, the `<select>` options, and wherever the computation reads an
|
|
58
|
+
* element's text (`getTextContent`): an option, an SVG `title`, the value of
|
|
59
|
+
* an embedded control. `hasSvgAccessibleNameSource` is the exception.
|
|
60
|
+
*/
|
|
61
|
+
getChildNodes?(el: AccnameElement): Iterable<AccnameNode>;
|
|
51
62
|
}
|
|
52
63
|
/**
|
|
53
64
|
* Identifies the source of an element's accessible name per HTML-AAM §4.1.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/ml-spec",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Types and schema that specs of the Markup languages for markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"schema:aria": "npx json2ts ./schemas/aria.schema.json --cwd ./schemas > ./src/types/aria.ts"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@markuplint/ml-ast": "5.
|
|
40
|
-
"@markuplint/types": "5.
|
|
39
|
+
"@markuplint/ml-ast": "5.1.0",
|
|
40
|
+
"@markuplint/types": "5.1.0",
|
|
41
41
|
"is-plain-object": "5.0.0",
|
|
42
42
|
"type-fest": "5.6.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@markuplint/test-tools": "5.
|
|
45
|
+
"@markuplint/test-tools": "5.1.0",
|
|
46
46
|
"json-schema-to-typescript": "15.0.4"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "3a061784a8211034ad0c1cbd64798c0213414f65"
|
|
49
49
|
}
|