accented 0.0.0-20250124142030
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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/accented.d.ts +27 -0
- package/dist/accented.d.ts.map +1 -0
- package/dist/accented.js +85 -0
- package/dist/accented.js.map +1 -0
- package/dist/dom-updater.d.ts +2 -0
- package/dist/dom-updater.d.ts.map +1 -0
- package/dist/dom-updater.js +96 -0
- package/dist/dom-updater.js.map +1 -0
- package/dist/elements/accented-container.d.ts +350 -0
- package/dist/elements/accented-container.d.ts.map +1 -0
- package/dist/elements/accented-container.js +131 -0
- package/dist/elements/accented-container.js.map +1 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +20 -0
- package/dist/logger.js.map +1 -0
- package/dist/scanner.d.ts +3 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +56 -0
- package/dist/scanner.js.map +1 -0
- package/dist/state.d.ts +5 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +8 -0
- package/dist/state.js.map +1 -0
- package/dist/task-queue.d.ts +10 -0
- package/dist/task-queue.d.ts.map +1 -0
- package/dist/task-queue.js +44 -0
- package/dist/task-queue.js.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/are-issue-sets-equal.d.ts +3 -0
- package/dist/utils/are-issue-sets-equal.d.ts.map +1 -0
- package/dist/utils/are-issue-sets-equal.js +6 -0
- package/dist/utils/are-issue-sets-equal.js.map +1 -0
- package/dist/utils/deep-merge.d.ts +4 -0
- package/dist/utils/deep-merge.d.ts.map +1 -0
- package/dist/utils/deep-merge.js +18 -0
- package/dist/utils/deep-merge.js.map +1 -0
- package/dist/utils/transform-violations.d.ts +4 -0
- package/dist/utils/transform-violations.d.ts.map +1 -0
- package/dist/utils/transform-violations.js +39 -0
- package/dist/utils/transform-violations.js.map +1 -0
- package/dist/utils/update-elements-with-issues.d.ts +5 -0
- package/dist/utils/update-elements-with-issues.d.ts.map +1 -0
- package/dist/utils/update-elements-with-issues.js +46 -0
- package/dist/utils/update-elements-with-issues.js.map +1 -0
- package/package.json +38 -0
- package/src/accented.test.ts +24 -0
- package/src/accented.ts +99 -0
- package/src/dom-updater.ts +104 -0
- package/src/elements/accented-container.ts +147 -0
- package/src/logger.ts +21 -0
- package/src/scanner.ts +68 -0
- package/src/state.ts +12 -0
- package/src/task-queue.test.ts +135 -0
- package/src/task-queue.ts +59 -0
- package/src/types.ts +97 -0
- package/src/utils/are-issue-sets-equal.test.ts +49 -0
- package/src/utils/are-issue-sets-equal.ts +10 -0
- package/src/utils/deep-merge.test.ts +27 -0
- package/src/utils/deep-merge.ts +18 -0
- package/src/utils/transform-violations.test.ts +124 -0
- package/src/utils/transform-violations.ts +45 -0
- package/src/utils/update-elements-with-issues.test.ts +209 -0
- package/src/utils/update-elements-with-issues.ts +55 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export default class TaskQueue {
|
|
2
|
+
#throttle;
|
|
3
|
+
#asyncCallback = null;
|
|
4
|
+
#items = new Set();
|
|
5
|
+
#inRunLoop = false;
|
|
6
|
+
constructor(asyncCallback, throttle) {
|
|
7
|
+
this.#asyncCallback = asyncCallback;
|
|
8
|
+
this.#throttle = throttle;
|
|
9
|
+
}
|
|
10
|
+
async #preRun() {
|
|
11
|
+
if (this.#inRunLoop) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
this.#inRunLoop = true;
|
|
15
|
+
if (!this.#throttle.leading) {
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, this.#throttle.wait));
|
|
17
|
+
}
|
|
18
|
+
await this.#run();
|
|
19
|
+
}
|
|
20
|
+
async #run() {
|
|
21
|
+
if (this.#items.size === 0) {
|
|
22
|
+
this.#inRunLoop = false;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
this.#items.clear();
|
|
26
|
+
if (this.#asyncCallback) {
|
|
27
|
+
await this.#asyncCallback();
|
|
28
|
+
}
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, this.#throttle.wait));
|
|
30
|
+
await this.#run();
|
|
31
|
+
}
|
|
32
|
+
addMultiple(items) {
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
this.#items.add(item);
|
|
35
|
+
}
|
|
36
|
+
if (this.#items.size > 0) {
|
|
37
|
+
this.#preRun();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
add(item) {
|
|
41
|
+
this.addMultiple([item]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=task-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-queue.js","sourceRoot":"","sources":["../src/task-queue.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,OAAO,OAAO,SAAS;IAC5B,SAAS,CAAW;IACpB,cAAc,GAAwB,IAAI,CAAC;IAE3C,MAAM,GAAG,IAAI,GAAG,EAAK,CAAC;IACtB,UAAU,GAAG,KAAK,CAAC;IAEnB,YAAY,aAA2B,EAAE,QAA4B;QACnE,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;QACpC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QAEvB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3E,CAAC;QAED,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YACxB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAEpB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAC9B,CAAC;QAED,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAEzE,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IAED,WAAW,CAAC,KAAe;QACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC;IACH,CAAC;IAED,GAAG,CAAC,IAAO;QACT,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,CAAC;CACF"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type axe from 'axe-core';
|
|
2
|
+
import type { Signal } from '@preact/signals-core';
|
|
3
|
+
import type { AccentedContainer } from './elements/accented-container';
|
|
4
|
+
export type DeepRequired<T> = T extends object ? {
|
|
5
|
+
[P in keyof T]-?: DeepRequired<T[P]>;
|
|
6
|
+
} : T;
|
|
7
|
+
export type Throttle = {
|
|
8
|
+
/**
|
|
9
|
+
* The minimal time between scans.
|
|
10
|
+
*
|
|
11
|
+
* Default: 1000.
|
|
12
|
+
* */
|
|
13
|
+
wait?: number;
|
|
14
|
+
/**
|
|
15
|
+
* When to run the scan on Accented initialization or on a mutation.
|
|
16
|
+
*
|
|
17
|
+
* If true, the scan will run immediately. If false, the scan will run after the first throttle delay.
|
|
18
|
+
*
|
|
19
|
+
* Default: true.
|
|
20
|
+
* */
|
|
21
|
+
leading?: boolean;
|
|
22
|
+
};
|
|
23
|
+
type CallbackParams = {
|
|
24
|
+
/**
|
|
25
|
+
* The most current array of elements with issues.
|
|
26
|
+
* */
|
|
27
|
+
elementsWithIssues: Array<ElementWithIssues>;
|
|
28
|
+
/**
|
|
29
|
+
* How long the scan took in milliseconds.
|
|
30
|
+
* */
|
|
31
|
+
scanDuration: number;
|
|
32
|
+
};
|
|
33
|
+
export type Callback = (params: CallbackParams) => void;
|
|
34
|
+
export type AccentedOptions = {
|
|
35
|
+
/**
|
|
36
|
+
* The character sequence that’s used in various elements, attributes and stylesheets that Accented adds to the page.
|
|
37
|
+
* * The data attribute that’s added to elements with issues (default: "data-accented").
|
|
38
|
+
* * The custom element that encapsulates the button and dialog attached to each element with issues (default: "accented-container").
|
|
39
|
+
* * The CSS cascade layer containing page-wide Accented-specific styles (default: "accented").
|
|
40
|
+
* * The prefix for some of the CSS custom properties used by Accented (default: "--accented-").
|
|
41
|
+
*
|
|
42
|
+
* Default: "accented".
|
|
43
|
+
*/
|
|
44
|
+
name?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to output the issues to the console.
|
|
47
|
+
*
|
|
48
|
+
* Default: true.
|
|
49
|
+
* */
|
|
50
|
+
outputToConsole?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Scan throttling options object.
|
|
53
|
+
* */
|
|
54
|
+
throttle?: Throttle;
|
|
55
|
+
/**
|
|
56
|
+
* A callback that will be called after each scan.
|
|
57
|
+
*
|
|
58
|
+
* Default: () => {}.
|
|
59
|
+
* */
|
|
60
|
+
callback?: Callback;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A function that fully disables Accented,
|
|
64
|
+
* stopping the scanning and removing all highlights from the page.
|
|
65
|
+
*/
|
|
66
|
+
export type DisableAccented = () => void;
|
|
67
|
+
export type Issue = {
|
|
68
|
+
id: string;
|
|
69
|
+
title: string;
|
|
70
|
+
description: string;
|
|
71
|
+
url: string;
|
|
72
|
+
impact: axe.ImpactValue;
|
|
73
|
+
};
|
|
74
|
+
export type ElementWithIssues = {
|
|
75
|
+
element: HTMLElement;
|
|
76
|
+
issues: Array<Issue>;
|
|
77
|
+
};
|
|
78
|
+
export type ExtendedElementWithIssues = Omit<ElementWithIssues, 'issues'> & {
|
|
79
|
+
issues: Signal<ElementWithIssues['issues']>;
|
|
80
|
+
accentedContainer: AccentedContainer;
|
|
81
|
+
id: number;
|
|
82
|
+
};
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG;KAC9C,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAI,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,GAAG,CAAC,CAAC;AAEN,MAAM,MAAM,QAAQ,GAAG;IACrB;;;;SAIK;IACL,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;;SAMK;IACL,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,KAAK,cAAc,GAAG;IACpB;;SAEK;IACL,kBAAkB,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAE7C;;SAEK;IACL,YAAY,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAE5B;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;SAIK;IACL,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;SAEK;IACL,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAEpB;;;;SAIK;IACL,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC;AAEzC,MAAM,MAAM,KAAK,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,GAAG,CAAC,WAAW,CAAA;CACxB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,GAAG;IAC1E,MAAM,EAAE,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5C,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,EAAE,EAAE,MAAM,CAAA;CACX,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"are-issue-sets-equal.d.ts","sourceRoot":"","sources":["../../src/utils/are-issue-sets-equal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAItC,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,WAKrF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const issueProps = ['id', 'title', 'description', 'url', 'impact'];
|
|
2
|
+
export default function areIssueSetsEqual(issues1, issues2) {
|
|
3
|
+
return issues1.length === issues2.length &&
|
|
4
|
+
issues1.every(issue1 => Boolean(issues2.find(issue2 => issueProps.every(prop => issue2[prop] === issue1[prop]))));
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=are-issue-sets-equal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"are-issue-sets-equal.js","sourceRoot":"","sources":["../../src/utils/are-issue-sets-equal.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,GAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAEvF,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,OAAqB,EAAE,OAAqB;IACpF,OAAO,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM;QACtC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CACpD,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,CACxD,CAAC,CAAC,CAAC;AACR,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deep-merge.d.ts","sourceRoot":"","sources":["../../src/utils/deep-merge.ts"],"names":[],"mappings":"AAAA,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAErC,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,GAAG,SAAS,CAejF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default function deepMerge(target, source) {
|
|
2
|
+
const output = { ...target };
|
|
3
|
+
for (const key of Object.keys(source)) {
|
|
4
|
+
if (typeof source[key] === 'object' && source[key] !== null) {
|
|
5
|
+
if (!(key in target)) {
|
|
6
|
+
output[key] = source[key];
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
output[key] = source[key];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return output;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=deep-merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deep-merge.js","sourceRoot":"","sources":["../../src/utils/deep-merge.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,MAAiB,EAAE,MAAiB;IACpE,MAAM,MAAM,GAAG,EAAC,GAAG,MAAM,EAAC,CAAC;IAC3B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,IAAI,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YAC5D,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,EAAE,CAAC;gBACrB,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5B,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACpD,CAAC;QACH,CAAC;aACI,CAAC;YACJ,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform-violations.d.ts","sourceRoot":"","sources":["../../src/utils/transform-violations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAS,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAEzD,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,UAAU,EAAE,OAAO,UAAU,CAAC,UAAU,uBAyCnF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default function transformViolations(violations) {
|
|
2
|
+
const elementsWithIssues = [];
|
|
3
|
+
for (const violation of violations) {
|
|
4
|
+
for (const node of violation.nodes) {
|
|
5
|
+
const { element, target } = node;
|
|
6
|
+
// Although axe-core can perform iframe scanning, I haven't succeeded in it,
|
|
7
|
+
// and the docs suggest that the axe-core script should be explicitly included
|
|
8
|
+
// in each of the iframed documents anyway.
|
|
9
|
+
// It seems preferable to disallow iframe scanning and not report issues in elements within iframes
|
|
10
|
+
// in the case that such issues are for some reason reported by axe-core.
|
|
11
|
+
// A consumer of Accented can instead scan the iframed document by calling Accented initialization from that document.
|
|
12
|
+
const isInIframe = target.length > 1;
|
|
13
|
+
// Highlighting elements in shadow DOM is not yet supported, see https://github.com/pomerantsev/accented/issues/25
|
|
14
|
+
// Until then, we don’t want such elements to be added to the set.
|
|
15
|
+
const isInShadowDOM = Array.isArray(target[0]);
|
|
16
|
+
if (element && !isInIframe && !isInShadowDOM) {
|
|
17
|
+
const issue = {
|
|
18
|
+
id: violation.id,
|
|
19
|
+
title: violation.help,
|
|
20
|
+
description: node.failureSummary ?? violation.description,
|
|
21
|
+
url: violation.helpUrl,
|
|
22
|
+
impact: violation.impact ?? null
|
|
23
|
+
};
|
|
24
|
+
const existingElementIndex = elementsWithIssues.findIndex(elementWithIssues => elementWithIssues.element === element);
|
|
25
|
+
if (existingElementIndex === -1) {
|
|
26
|
+
elementsWithIssues.push({
|
|
27
|
+
element,
|
|
28
|
+
issues: [issue]
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
elementsWithIssues[existingElementIndex].issues.push(issue);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return elementsWithIssues;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=transform-violations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform-violations.js","sourceRoot":"","sources":["../../src/utils/transform-violations.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,UAAwC;IAClF,MAAM,kBAAkB,GAA6B,EAAE,CAAC;IAExD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,KAAK,EAAE,CAAC;YACnC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;YAEjC,4EAA4E;YAC5E,8EAA8E;YAC9E,2CAA2C;YAC3C,mGAAmG;YACnG,yEAAyE;YACzE,sHAAsH;YACtH,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YAErC,kHAAkH;YAClH,kEAAkE;YAClE,MAAM,aAAa,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAE/C,IAAI,OAAO,IAAI,CAAC,UAAU,IAAI,CAAC,aAAa,EAAE,CAAC;gBAC7C,MAAM,KAAK,GAAU;oBACnB,EAAE,EAAE,SAAS,CAAC,EAAE;oBAChB,KAAK,EAAE,SAAS,CAAC,IAAI;oBACrB,WAAW,EAAE,IAAI,CAAC,cAAc,IAAI,SAAS,CAAC,WAAW;oBACzD,GAAG,EAAE,SAAS,CAAC,OAAO;oBACtB,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,IAAI;iBACjC,CAAC;gBACF,MAAM,oBAAoB,GAAG,kBAAkB,CAAC,SAAS,CAAC,iBAAiB,CAAC,EAAE,CAAC,iBAAiB,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC;gBACtH,IAAI,oBAAoB,KAAK,CAAC,CAAC,EAAE,CAAC;oBAChC,kBAAkB,CAAC,IAAI,CAAC;wBACtB,OAAO;wBACP,MAAM,EAAE,CAAC,KAAK,CAAC;qBAChB,CAAC,CAAC;gBACL,CAAC;qBAAM,CAAC;oBACN,kBAAkB,CAAC,oBAAoB,CAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC/D,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,kBAAkB,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AxeResults } from 'axe-core';
|
|
2
|
+
import type { Signal } from '@preact/signals-core';
|
|
3
|
+
import type { ExtendedElementWithIssues } from '../types';
|
|
4
|
+
export default function updateElementsWithIssues(extendedElementsWithIssues: Signal<Array<ExtendedElementWithIssues>>, violations: typeof AxeResults.violations, win: Window, name: string): void;
|
|
5
|
+
//# sourceMappingURL=update-elements-with-issues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-elements-with-issues.d.ts","sourceRoot":"","sources":["../../src/utils/update-elements-with-issues.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAO1D,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAAC,0BAA0B,EAAE,MAAM,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,UAAU,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QA4CzL"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { batch, signal } from '@preact/signals-core';
|
|
2
|
+
import transformViolations from './transform-violations.js';
|
|
3
|
+
import areIssueSetsEqual from './are-issue-sets-equal.js';
|
|
4
|
+
let count = 0;
|
|
5
|
+
export default function updateElementsWithIssues(extendedElementsWithIssues, violations, win, name) {
|
|
6
|
+
const updatedElementsWithIssues = transformViolations(violations);
|
|
7
|
+
batch(() => {
|
|
8
|
+
for (const updatedElementWithIssues of updatedElementsWithIssues) {
|
|
9
|
+
const existingElementIndex = extendedElementsWithIssues.value.findIndex(extendedElementWithIssues => extendedElementWithIssues.element === updatedElementWithIssues.element);
|
|
10
|
+
if (existingElementIndex > -1 && extendedElementsWithIssues.value[existingElementIndex] && !areIssueSetsEqual(extendedElementsWithIssues.value[existingElementIndex].issues.value, updatedElementWithIssues.issues)) {
|
|
11
|
+
extendedElementsWithIssues.value[existingElementIndex].issues.value = updatedElementWithIssues.issues;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const addedElementsWithIssues = updatedElementsWithIssues.filter(updatedElementWithIssues => {
|
|
15
|
+
return !extendedElementsWithIssues.value.some(extendedElementWithIssues => extendedElementWithIssues.element === updatedElementWithIssues.element);
|
|
16
|
+
});
|
|
17
|
+
const removedElementsWithIssues = extendedElementsWithIssues.value.filter(extendedElementWithIssues => {
|
|
18
|
+
return !updatedElementsWithIssues.some(updatedElementWithIssues => updatedElementWithIssues.element === extendedElementWithIssues.element);
|
|
19
|
+
});
|
|
20
|
+
if (addedElementsWithIssues.length > 0 || removedElementsWithIssues.length > 0) {
|
|
21
|
+
extendedElementsWithIssues.value = [...extendedElementsWithIssues.value]
|
|
22
|
+
.filter(extendedElementWithIssues => {
|
|
23
|
+
return !removedElementsWithIssues.some(removedElementWithIssues => removedElementWithIssues.element === extendedElementWithIssues.element);
|
|
24
|
+
})
|
|
25
|
+
.concat(addedElementsWithIssues.map(addedElementWithIssues => {
|
|
26
|
+
const id = count++;
|
|
27
|
+
const accentedContainer = win.document.createElement(`${name}-container`);
|
|
28
|
+
const elementZIndex = parseInt(win.getComputedStyle(addedElementWithIssues.element).zIndex, 10);
|
|
29
|
+
if (!isNaN(elementZIndex)) {
|
|
30
|
+
accentedContainer.style.setProperty('z-index', (elementZIndex + 1).toString());
|
|
31
|
+
}
|
|
32
|
+
accentedContainer.style.setProperty('position-anchor', `--${name}-anchor-${id}`);
|
|
33
|
+
accentedContainer.dataset.id = id.toString();
|
|
34
|
+
const issues = signal(addedElementWithIssues.issues);
|
|
35
|
+
accentedContainer.issues = issues;
|
|
36
|
+
return {
|
|
37
|
+
id,
|
|
38
|
+
element: addedElementWithIssues.element,
|
|
39
|
+
accentedContainer,
|
|
40
|
+
issues
|
|
41
|
+
};
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=update-elements-with-issues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-elements-with-issues.js","sourceRoot":"","sources":["../../src/utils/update-elements-with-issues.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "accented",
|
|
3
|
+
"version": "0.0.0-20250124142030",
|
|
4
|
+
"description": "Continuous accessibility testing and issue highlighting for web development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/accented.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/pomerantsev/accented.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"accessibility",
|
|
17
|
+
"a11y",
|
|
18
|
+
"axe",
|
|
19
|
+
"axe-core"
|
|
20
|
+
],
|
|
21
|
+
"author": "Pavel Pomerantsev",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/pomerantsev/accented/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/pomerantsev/accented#readme",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@preact/signals-core": "^1.8.0",
|
|
29
|
+
"axe-core": "^4.10.2"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"checkBuiltFiles": "node --import tsx ./scripts/check-built-files.ts",
|
|
34
|
+
"checkImportsInBuiltFiles": "node ./dist/accented.js",
|
|
35
|
+
"watch": "tsc --watch",
|
|
36
|
+
"test": "node --test --import tsx \"./**/*.test.ts\""
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {suite, test} from 'node:test';
|
|
3
|
+
import type { Mock } from 'node:test';
|
|
4
|
+
|
|
5
|
+
import accented from './accented.js';
|
|
6
|
+
|
|
7
|
+
suite('Accented', () => {
|
|
8
|
+
test('runs without errors in NodeJS and issues a warning and a trace (it’s meant to be a no-op on the server side)', (t) => {
|
|
9
|
+
t.mock.method(console, 'warn', () => {});
|
|
10
|
+
t.mock.method(console, 'trace', () => {});
|
|
11
|
+
accented();
|
|
12
|
+
assert.equal((console.warn as Mock<() => void>).mock.callCount(), 1);
|
|
13
|
+
assert.equal((console.trace as Mock<() => void>).mock.callCount(), 1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
suite('argument validation', () => {
|
|
17
|
+
// @ts-expect-error
|
|
18
|
+
assert.throws(() => accented({ throttle: null }));
|
|
19
|
+
// @ts-expect-error
|
|
20
|
+
assert.throws(() => accented({ throttle: 1000 }));
|
|
21
|
+
|
|
22
|
+
assert.throws(() => accented({ throttle: { wait: -1 } }));
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/accented.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
|
|
2
|
+
import createDomUpdater from './dom-updater.js';
|
|
3
|
+
import createLogger from './logger.js';
|
|
4
|
+
import createScanner from './scanner.js';
|
|
5
|
+
import { enabled, extendedElementsWithIssues } from './state.js';
|
|
6
|
+
import deepMerge from './utils/deep-merge.js';
|
|
7
|
+
import getAccentedContainer from './elements/accented-container.js'
|
|
8
|
+
import type { DeepRequired, AccentedOptions, DisableAccented } from './types';
|
|
9
|
+
|
|
10
|
+
export type { AccentedOptions, DisableAccented };
|
|
11
|
+
|
|
12
|
+
// IMPORTANT: when changing any of the properties or values, also do the following:
|
|
13
|
+
// * update the default value in the type documentation accordingly;
|
|
14
|
+
// * update validations and validation tests if necessary;
|
|
15
|
+
// * update examples in the accented() function JSDoc;
|
|
16
|
+
// * update examples in the Readme.
|
|
17
|
+
const defaultOptions: DeepRequired<AccentedOptions> = {
|
|
18
|
+
name: 'accented',
|
|
19
|
+
outputToConsole: true,
|
|
20
|
+
throttle: {
|
|
21
|
+
wait: 1000,
|
|
22
|
+
leading: true
|
|
23
|
+
},
|
|
24
|
+
callback: () => {}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Enables highlighting of elements with accessibility issues.
|
|
29
|
+
*
|
|
30
|
+
* @param {AccentedOptions} options - The options object.
|
|
31
|
+
*
|
|
32
|
+
* @returns A `disable` function that can be called to stop the scanning and highlighting.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* accented();
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const disableAccented = accented({
|
|
39
|
+
* outputToConsole: false,
|
|
40
|
+
* throttle: {
|
|
41
|
+
* wait: 500,
|
|
42
|
+
* leading: false
|
|
43
|
+
* },
|
|
44
|
+
* callback: ({ elementsWithIssues, scanDuration }) => {
|
|
45
|
+
* console.log('Elements with issues:', elementsWithIssues);
|
|
46
|
+
* console.log('Scan duration:', scanDuration);
|
|
47
|
+
* }
|
|
48
|
+
* });
|
|
49
|
+
*/
|
|
50
|
+
export default function accented(options: AccentedOptions = {}): DisableAccented {
|
|
51
|
+
|
|
52
|
+
// Argument validation
|
|
53
|
+
if (options.throttle !== undefined) {
|
|
54
|
+
if (typeof options.throttle !== 'object' || options.throttle === null) {
|
|
55
|
+
throw new TypeError(`Invalid argument: \`throttle\` option must be an object if provided. It’s currently set to ${options.throttle}.`);
|
|
56
|
+
}
|
|
57
|
+
if (options.throttle.wait !== undefined && (typeof options.throttle.wait !== 'number' || options.throttle.wait < 0)) {
|
|
58
|
+
throw new TypeError(`Invalid argument: \`throttle.wait\` option must be a non-negative number if provided. It’s currently set to ${options.throttle.wait}.`);
|
|
59
|
+
}
|
|
60
|
+
if (options.callback !== undefined && typeof options.callback !== 'function') {
|
|
61
|
+
throw new TypeError(`Invalid argument: \`callback\` option must be a function if provided. It’s currently set to ${options.callback}.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
66
|
+
console.warn('Accented: this script can only run in the browser, and it’s likely running on the server now. Exiting.');
|
|
67
|
+
console.trace();
|
|
68
|
+
return () => {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const {name, outputToConsole, throttle, callback} = deepMerge(defaultOptions, options);
|
|
72
|
+
|
|
73
|
+
const AccentedContainer = getAccentedContainer(name);
|
|
74
|
+
if (!customElements.get(`${name}-container`)) {
|
|
75
|
+
customElements.define(`${name}-container`, AccentedContainer);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (enabled.value) {
|
|
79
|
+
// TODO: add link to relevant docs
|
|
80
|
+
console.warn(
|
|
81
|
+
'You are trying to run the Accented library more than once. ' +
|
|
82
|
+
'This will likely lead to errors.'
|
|
83
|
+
);
|
|
84
|
+
console.trace();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
enabled.value = true;
|
|
88
|
+
const cleanupScanner = createScanner(name, throttle, callback);
|
|
89
|
+
const cleanupDomUpdater = createDomUpdater(name);
|
|
90
|
+
const cleanupLogger = outputToConsole ? createLogger() : () => {};
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
enabled.value = false;
|
|
94
|
+
extendedElementsWithIssues.value = [];
|
|
95
|
+
cleanupScanner();
|
|
96
|
+
cleanupDomUpdater();
|
|
97
|
+
cleanupLogger();
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { effect } from '@preact/signals-core';
|
|
2
|
+
import { extendedElementsWithIssues } from './state.js';
|
|
3
|
+
import type { ExtendedElementWithIssues } from './types';
|
|
4
|
+
|
|
5
|
+
export default function createDomUpdater(name: string) {
|
|
6
|
+
const attrName = `data-${name}`;
|
|
7
|
+
|
|
8
|
+
function supportsAnchorPositioning () {
|
|
9
|
+
return CSS.supports('anchor-name: --foo') && CSS.supports('position-anchor: --foo');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function setAnchorName (element: HTMLElement, id: number) {
|
|
13
|
+
const anchorNameValue = element.style.getPropertyValue('anchor-name') || window.getComputedStyle(element).getPropertyValue('anchor-name');
|
|
14
|
+
const anchorNames = anchorNameValue
|
|
15
|
+
.split(/,\s*/)
|
|
16
|
+
.filter(anchorName => anchorName.startsWith('--'));
|
|
17
|
+
if (anchorNames.length > 0) {
|
|
18
|
+
element.style.setProperty('anchor-name', `${anchorNameValue}, --${name}-anchor-${id}`);
|
|
19
|
+
} else {
|
|
20
|
+
element.style.setProperty('anchor-name', `--${name}-anchor-${id}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function removeAnchorName (element: HTMLElement, id: number) {
|
|
25
|
+
const anchorNameValue = element.style.getPropertyValue('anchor-name');
|
|
26
|
+
const anchorNames = anchorNameValue
|
|
27
|
+
.split(/,\s*/)
|
|
28
|
+
.filter(anchorName => anchorName.startsWith('--'));
|
|
29
|
+
const index = anchorNames.indexOf(`--${name}-anchor-${id}`);
|
|
30
|
+
if (anchorNames.length === 1 && index === 0) {
|
|
31
|
+
element.style.removeProperty('anchor-name');
|
|
32
|
+
} else if (anchorNames.length > 1 && index > -1) {
|
|
33
|
+
element.style.setProperty('anchor-name', anchorNames.filter((_, i) => i !== index).join(', '));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
|
|
38
|
+
const displayAccentedContainers = supportsAnchorPositioning();
|
|
39
|
+
for (const elementWithIssues of extendedElementsWithIssues) {
|
|
40
|
+
elementWithIssues.element.setAttribute(attrName, elementWithIssues.id.toString());
|
|
41
|
+
if (displayAccentedContainers) {
|
|
42
|
+
setAnchorName(elementWithIssues.element, elementWithIssues.id);
|
|
43
|
+
if (elementWithIssues.element.parentElement) {
|
|
44
|
+
elementWithIssues.element.insertAdjacentElement('afterend', elementWithIssues.accentedContainer);
|
|
45
|
+
} else {
|
|
46
|
+
elementWithIssues.element.insertAdjacentElement('beforeend', elementWithIssues.accentedContainer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeIssues (extendedElementsWithIssues: Array<ExtendedElementWithIssues>) {
|
|
53
|
+
for (const elementWithIssues of extendedElementsWithIssues) {
|
|
54
|
+
elementWithIssues.element.removeAttribute(attrName);
|
|
55
|
+
removeAnchorName(elementWithIssues.element, elementWithIssues.id);
|
|
56
|
+
elementWithIssues.accentedContainer.remove();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const stylesheet = new CSSStyleSheet();
|
|
61
|
+
stylesheet.replaceSync(`
|
|
62
|
+
@layer ${name} {
|
|
63
|
+
:root {
|
|
64
|
+
--${name}-primary-color: red;
|
|
65
|
+
--${name}-secondary-color: white;
|
|
66
|
+
--${name}-outline-width: 2px;
|
|
67
|
+
--${name}-outline-style: solid;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
[${attrName}]:not(:focus-visible) {
|
|
71
|
+
outline-width: var(--${name}-outline-width) !important;
|
|
72
|
+
outline-offset: calc(-1 * var(--${name}-outline-width)) !important;
|
|
73
|
+
outline-color: var(--${name}-primary-color) !important;
|
|
74
|
+
outline-style: var(--${name}-outline-style) !important;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
let previousExtendedElementsWithIssues: Array<ExtendedElementWithIssues> = [];
|
|
80
|
+
|
|
81
|
+
document.adoptedStyleSheets.push(stylesheet);
|
|
82
|
+
const removeStylesheet = () => {
|
|
83
|
+
if (document.adoptedStyleSheets.includes(stylesheet)) {
|
|
84
|
+
document.adoptedStyleSheets.splice(document.adoptedStyleSheets.indexOf(stylesheet), 1);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const disposeOfElementsEffect = effect(() => {
|
|
89
|
+
const added = extendedElementsWithIssues.value.filter(elementWithIssues => {
|
|
90
|
+
return !previousExtendedElementsWithIssues.some(previousElementWithIssues => previousElementWithIssues.element === elementWithIssues.element);
|
|
91
|
+
});
|
|
92
|
+
const removed = previousExtendedElementsWithIssues.filter(previousElementWithIssues => {
|
|
93
|
+
return !extendedElementsWithIssues.value.some(elementWithIssues => elementWithIssues.element === previousElementWithIssues.element);
|
|
94
|
+
});
|
|
95
|
+
removeIssues(removed);
|
|
96
|
+
setIssues(added);
|
|
97
|
+
previousExtendedElementsWithIssues = [...extendedElementsWithIssues.value];
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
removeStylesheet();
|
|
102
|
+
disposeOfElementsEffect();
|
|
103
|
+
};
|
|
104
|
+
}
|