-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcomposer.ts
165 lines (142 loc) · 4.69 KB
/
composer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/**
* @file Attribute Composing
*/
import type {
AttrMap,
AttrName,
AttrPair,
AttrValueEscaper,
AttrValueFilter,
AttrMapComparator,
} from './types.js';
import {
assertValidAttributeName
} from './name/is-valid-attribute-name.js';
/**
* HTML attributes composer.
*
* If a {@see this.compareAttributes comparator function} is assigned,
* attributes are sorted according to its return value.
*
* If comparator function is not supplied, attributes will not be sorted.
*/
export class Composer
{
/**
* @public
* @readonly
* @type {?AttrValueFilter}
*/
public readonly filterAttributeValue?: AttrValueFilter;
/**
* @public
* @readonly
* @type {?AttrValueEscaper}
*/
public readonly escapeAttributeValue?: AttrValueEscaper;
/**
* @public
* @readonly
* @type {?AttrMapComparator}
*/
public readonly compareAttributes?: AttrMapComparator;
/**
* Creates a new HTML attributes composer.
*
* @param {?AttrValueFilter} [attributeValueFilter]
* @param {?AttrValueEscaper} [attributeValueEscaper]
* @param {?AttrMapComparator} [attributesComparator]
* @throws {TypeError} If an argument is not a function.
*/
constructor(
attributeValueFilter?: AttrValueFilter,
attributeValueEscaper?: AttrValueEscaper,
attributesComparator?: AttrMapComparator
) {
const filterAttributeValueDescriptor: PropertyDescriptor = {};
const escapeAttributeValueDescriptor: PropertyDescriptor = {};
const compareAttributesDescriptor: PropertyDescriptor = {};
if (attributeValueFilter) {
if (typeof attributeValueFilter === 'function') {
filterAttributeValueDescriptor.value = attributeValueFilter.bind(this);
} else {
throw new TypeError(
`${this.constructor.name} expected a filter function or nil`
);
}
}
if (attributeValueEscaper) {
if (typeof attributeValueEscaper === 'function') {
escapeAttributeValueDescriptor.value = attributeValueEscaper.bind(this);
} else {
throw new TypeError(
`${this.constructor.name} expected an escaper function or nil`
);
}
}
if (attributesComparator) {
if (typeof attributesComparator === 'function') {
compareAttributesDescriptor.value = attributesComparator.bind(this);
} else {
throw new TypeError(
`${this.constructor.name} expected a comparator function or nil`
);
}
}
Object.defineProperty(this, 'filterAttributeValue', filterAttributeValueDescriptor);
Object.defineProperty(this, 'escapeAttributeValue', escapeAttributeValueDescriptor);
Object.defineProperty(this, 'compareAttributes', compareAttributesDescriptor);
}
/**
* Generates a string of many HTML attributes.
*
* @param {AttrMap} attributes
* @returns {?string} Returns a string of many HTML attributes
* or `null` if all attributes are empty.
*/
composeAttributes(attributes: AttrMap): string | null
{
const attrs = Object.entries(attributes) as AttrPair[];
if (this.compareAttributes) {
attrs.sort(this.compareAttributes);
}
const composedAttributes: string[] = [];
for (const [ name, value ] of attrs) {
const composedAttribute = this.composeAttribute(name, value);
if (composedAttribute != null) {
composedAttributes.push(composedAttribute);
}
}
if (!composedAttributes.length) {
return null;
}
return composedAttributes.join(' ');
}
/**
* Generates a string of a single HTML attribute.
*
* @param {AttrName} name
* @param {unknown} value
* @returns {?string} Returns a string of a single HTML attribute
* or `null` if the attribute is empty.
*/
composeAttribute(name: AttrName, value: unknown): string | null
{
assertValidAttributeName(name);
if (this.filterAttributeValue) {
value = this.filterAttributeValue(value, name);
}
switch (typeof value) {
case 'boolean': {
return value ? name : null;
}
case 'string': {
if (this.escapeAttributeValue) {
value = this.escapeAttributeValue(value, name);
}
return `${name}="${value}"`;
}
}
return null;
}
}