@thyn/vite-plugin 0.0.312 → 0.0.314
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/dom.d.ts +35 -0
- package/dist/dom.js +142 -0
- package/dist/html-parser.d.ts +31 -0
- package/dist/html-parser.js +275 -0
- package/dist/index.js +72 -63
- package/package.json +2 -2
- package/src/html-parser.ts +332 -0
- package/src/index.ts +87 -79
- package/tsconfig.tsbuildinfo +1 -1
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare class Node {
|
|
2
|
+
nodeType: number;
|
|
3
|
+
childNodes: Node[];
|
|
4
|
+
constructor(nodeType: number);
|
|
5
|
+
get textContent(): string;
|
|
6
|
+
set textContent(value: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class Text extends Node {
|
|
9
|
+
private _text;
|
|
10
|
+
constructor(text: string);
|
|
11
|
+
get textContent(): string;
|
|
12
|
+
set textContent(value: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class Element extends Node {
|
|
15
|
+
tagName: string;
|
|
16
|
+
attributes: {
|
|
17
|
+
name: string;
|
|
18
|
+
value: string;
|
|
19
|
+
}[];
|
|
20
|
+
constructor(tagName: string);
|
|
21
|
+
getAttribute(name: string): string | null;
|
|
22
|
+
setAttribute(name: string, value: string): void;
|
|
23
|
+
removeAttribute(name: string): void;
|
|
24
|
+
hasAttribute(name: string): boolean;
|
|
25
|
+
get classList(): {
|
|
26
|
+
add: (className: string) => void;
|
|
27
|
+
};
|
|
28
|
+
get children(): Element[];
|
|
29
|
+
set innerHTML(html: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class DocumentFragment extends Node {
|
|
32
|
+
constructor();
|
|
33
|
+
get firstElementChild(): Element | null;
|
|
34
|
+
}
|
|
35
|
+
export declare function parseHTML(html: string): DocumentFragment;
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export class Node {
|
|
2
|
+
constructor(nodeType) {
|
|
3
|
+
this.childNodes = [];
|
|
4
|
+
this.nodeType = nodeType;
|
|
5
|
+
}
|
|
6
|
+
get textContent() {
|
|
7
|
+
return this.childNodes.map(node => node.textContent).join('');
|
|
8
|
+
}
|
|
9
|
+
set textContent(value) {
|
|
10
|
+
this.childNodes = [new Text(value)];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class Text extends Node {
|
|
14
|
+
constructor(text) {
|
|
15
|
+
super(3); // Node.TEXT_NODE
|
|
16
|
+
this._text = text;
|
|
17
|
+
}
|
|
18
|
+
get textContent() {
|
|
19
|
+
return this._text;
|
|
20
|
+
}
|
|
21
|
+
set textContent(value) {
|
|
22
|
+
this._text = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class Element extends Node {
|
|
26
|
+
constructor(tagName) {
|
|
27
|
+
super(1); // Node.ELEMENT_NODE
|
|
28
|
+
this.attributes = [];
|
|
29
|
+
this.tagName = tagName;
|
|
30
|
+
}
|
|
31
|
+
getAttribute(name) {
|
|
32
|
+
const attr = this.attributes.find(a => a.name === name);
|
|
33
|
+
return attr ? attr.value : null;
|
|
34
|
+
}
|
|
35
|
+
setAttribute(name, value) {
|
|
36
|
+
const attr = this.attributes.find(a => a.name === name);
|
|
37
|
+
if (attr) {
|
|
38
|
+
attr.value = value;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
this.attributes.push({ name, value });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
removeAttribute(name) {
|
|
45
|
+
this.attributes = this.attributes.filter(a => a.name !== name);
|
|
46
|
+
}
|
|
47
|
+
hasAttribute(name) {
|
|
48
|
+
return this.attributes.some(a => a.name === name);
|
|
49
|
+
}
|
|
50
|
+
get classList() {
|
|
51
|
+
return {
|
|
52
|
+
add: (className) => {
|
|
53
|
+
const existing = this.getAttribute('class') || '';
|
|
54
|
+
const classes = existing.split(/\s+/).filter(Boolean);
|
|
55
|
+
if (!classes.includes(className)) {
|
|
56
|
+
classes.push(className);
|
|
57
|
+
this.setAttribute('class', classes.join(' '));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
get children() {
|
|
63
|
+
return this.childNodes.filter(node => node.nodeType === 1);
|
|
64
|
+
}
|
|
65
|
+
set innerHTML(html) {
|
|
66
|
+
const fragment = parseHTML(html);
|
|
67
|
+
this.childNodes = fragment.childNodes;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class DocumentFragment extends Node {
|
|
71
|
+
constructor() {
|
|
72
|
+
super(11); // Node.DOCUMENT_FRAGMENT_NODE
|
|
73
|
+
}
|
|
74
|
+
get firstElementChild() {
|
|
75
|
+
return this.childNodes.find(node => node.nodeType === 1) || null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const voidElements = new Set([
|
|
79
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
80
|
+
]);
|
|
81
|
+
export function parseHTML(html) {
|
|
82
|
+
const fragment = new DocumentFragment();
|
|
83
|
+
const stack = [fragment];
|
|
84
|
+
const tagRegex = new RegExp('<(?:\\/([a-zA-Z0-9\\-:]+)|([a-zA-Z0-9\\-:]+)((?:[^>"\']|"[^"]*"|\'[^\']*\')*)(\\/)?)>|<!--[\\s\\S]*?-->', 'g');
|
|
85
|
+
let lastIndex = 0;
|
|
86
|
+
let match;
|
|
87
|
+
while ((match = tagRegex.exec(html)) !== null) {
|
|
88
|
+
const [fullMatch, closeTag, openTag, attrs, selfClosing] = match;
|
|
89
|
+
const index = match.index;
|
|
90
|
+
// Process text before the tag
|
|
91
|
+
if (index > lastIndex) {
|
|
92
|
+
const textContent = html.slice(lastIndex, index);
|
|
93
|
+
if (textContent) {
|
|
94
|
+
stack[stack.length - 1].childNodes.push(new Text(textContent));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
lastIndex = index + fullMatch.length;
|
|
98
|
+
// Ignore comments
|
|
99
|
+
if (fullMatch.startsWith('<!--')) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (openTag) {
|
|
103
|
+
const element = new Element(openTag);
|
|
104
|
+
if (attrs) {
|
|
105
|
+
// Match attributes: name="value", name='value', name=value, name
|
|
106
|
+
const attrRegex = /([a-zA-Z0-9\-:@\._#]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^"'\s]+)))?/g;
|
|
107
|
+
let attrMatch;
|
|
108
|
+
while ((attrMatch = attrRegex.exec(attrs)) !== null) {
|
|
109
|
+
const name = attrMatch[1];
|
|
110
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? '';
|
|
111
|
+
element.setAttribute(name, value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
stack[stack.length - 1].childNodes.push(element);
|
|
115
|
+
const isVoid = voidElements.has(openTag.toLowerCase());
|
|
116
|
+
const isSelfClosingTag = !!selfClosing;
|
|
117
|
+
if (!isVoid && !isSelfClosingTag) {
|
|
118
|
+
stack.push(element);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (closeTag) {
|
|
122
|
+
// Find the corresponding opening tag in the stack
|
|
123
|
+
// We search backwards to handle potential malformed HTML gracefully-ish
|
|
124
|
+
for (let i = stack.length - 1; i > 0; i--) {
|
|
125
|
+
const node = stack[i];
|
|
126
|
+
if (node instanceof Element && node.tagName.toLowerCase() === closeTag.toLowerCase()) {
|
|
127
|
+
// Found the matching tag, pop everything up to this point
|
|
128
|
+
stack.splice(i);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Process any remaining text
|
|
135
|
+
if (lastIndex < html.length) {
|
|
136
|
+
const textContent = html.slice(lastIndex);
|
|
137
|
+
if (textContent) {
|
|
138
|
+
stack[stack.length - 1].childNodes.push(new Text(textContent));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return fragment;
|
|
142
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface Node {
|
|
2
|
+
nodeType: number;
|
|
3
|
+
nodeName: string;
|
|
4
|
+
textContent: string;
|
|
5
|
+
childNodes: Node[];
|
|
6
|
+
}
|
|
7
|
+
interface Element extends Node {
|
|
8
|
+
tagName: string;
|
|
9
|
+
attributes: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}>;
|
|
13
|
+
children: Element[];
|
|
14
|
+
firstElementChild: Element | null;
|
|
15
|
+
hasAttribute(name: string): boolean;
|
|
16
|
+
getAttribute(name: string): string | null;
|
|
17
|
+
setAttribute(name: string, value: string): void;
|
|
18
|
+
removeAttribute(name: string): void;
|
|
19
|
+
classList: {
|
|
20
|
+
add(className: string): void;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface DocumentFragment {
|
|
24
|
+
childNodes: Node[];
|
|
25
|
+
firstElementChild: Element | null;
|
|
26
|
+
}
|
|
27
|
+
interface TemplateElement extends Element {
|
|
28
|
+
content: DocumentFragment;
|
|
29
|
+
}
|
|
30
|
+
export declare function parseHTML(html: string): TemplateElement;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
function parseAttributes(attrStr) {
|
|
2
|
+
const attrs = [];
|
|
3
|
+
let i = 0;
|
|
4
|
+
while (i < attrStr.length) {
|
|
5
|
+
// Skip whitespace
|
|
6
|
+
while (i < attrStr.length && /\s/.test(attrStr[i]))
|
|
7
|
+
i++;
|
|
8
|
+
if (i >= attrStr.length)
|
|
9
|
+
break;
|
|
10
|
+
// Parse attribute name
|
|
11
|
+
let name = "";
|
|
12
|
+
while (i < attrStr.length && !/[\s=]/.test(attrStr[i])) {
|
|
13
|
+
name += attrStr[i];
|
|
14
|
+
i++;
|
|
15
|
+
}
|
|
16
|
+
if (!name)
|
|
17
|
+
break;
|
|
18
|
+
// Skip whitespace
|
|
19
|
+
while (i < attrStr.length && /\s/.test(attrStr[i]))
|
|
20
|
+
i++;
|
|
21
|
+
let value = "";
|
|
22
|
+
if (i < attrStr.length && attrStr[i] === "=") {
|
|
23
|
+
i++; // skip '='
|
|
24
|
+
// Skip whitespace
|
|
25
|
+
while (i < attrStr.length && /\s/.test(attrStr[i]))
|
|
26
|
+
i++;
|
|
27
|
+
if (i < attrStr.length) {
|
|
28
|
+
const quote = attrStr[i];
|
|
29
|
+
if (quote === '"' || quote === "'") {
|
|
30
|
+
i++; // skip opening quote
|
|
31
|
+
while (i < attrStr.length && attrStr[i] !== quote) {
|
|
32
|
+
value += attrStr[i];
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
if (i < attrStr.length)
|
|
36
|
+
i++; // skip closing quote
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Unquoted value - take until whitespace
|
|
40
|
+
while (i < attrStr.length && !/\s/.test(attrStr[i])) {
|
|
41
|
+
value += attrStr[i];
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
attrs.push({ name, value });
|
|
48
|
+
}
|
|
49
|
+
return attrs;
|
|
50
|
+
}
|
|
51
|
+
function createTextNode(text) {
|
|
52
|
+
return {
|
|
53
|
+
nodeType: 3,
|
|
54
|
+
nodeName: "#text",
|
|
55
|
+
textContent: text,
|
|
56
|
+
childNodes: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function createElement(tagName, attributes = []) {
|
|
60
|
+
const children = [];
|
|
61
|
+
const childNodes = [];
|
|
62
|
+
const element = {
|
|
63
|
+
nodeType: 1,
|
|
64
|
+
nodeName: tagName.toUpperCase(),
|
|
65
|
+
tagName: tagName.toUpperCase(),
|
|
66
|
+
textContent: "",
|
|
67
|
+
attributes: [...attributes],
|
|
68
|
+
children,
|
|
69
|
+
childNodes,
|
|
70
|
+
firstElementChild: null,
|
|
71
|
+
hasAttribute(name) {
|
|
72
|
+
return this.attributes.some((attr) => attr.name === name);
|
|
73
|
+
},
|
|
74
|
+
getAttribute(name) {
|
|
75
|
+
const attr = this.attributes.find((attr) => attr.name === name);
|
|
76
|
+
return attr ? attr.value : null;
|
|
77
|
+
},
|
|
78
|
+
setAttribute(name, value) {
|
|
79
|
+
const existing = this.attributes.find((attr) => attr.name === name);
|
|
80
|
+
if (existing) {
|
|
81
|
+
existing.value = value;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.attributes.push({ name, value });
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
removeAttribute(name) {
|
|
88
|
+
this.attributes = this.attributes.filter((attr) => attr.name !== name);
|
|
89
|
+
},
|
|
90
|
+
classList: {
|
|
91
|
+
add: (className) => {
|
|
92
|
+
const existing = element.getAttribute("class");
|
|
93
|
+
const classes = existing ? existing.split(" ").filter(Boolean) : [];
|
|
94
|
+
if (!classes.includes(className)) {
|
|
95
|
+
classes.push(className);
|
|
96
|
+
element.setAttribute("class", classes.join(" "));
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
return element;
|
|
102
|
+
}
|
|
103
|
+
// Find next tag position, properly handling quoted strings
|
|
104
|
+
function findNextTag(html, startIndex) {
|
|
105
|
+
let i = startIndex;
|
|
106
|
+
while (i < html.length) {
|
|
107
|
+
// Find the next '<'
|
|
108
|
+
while (i < html.length && html[i] !== '<') {
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
if (i >= html.length)
|
|
112
|
+
return null;
|
|
113
|
+
const tagStart = i;
|
|
114
|
+
i++; // skip '<'
|
|
115
|
+
// Check if it's a closing tag
|
|
116
|
+
const isClose = i < html.length && html[i] === '/';
|
|
117
|
+
if (isClose)
|
|
118
|
+
i++;
|
|
119
|
+
// Parse tag name
|
|
120
|
+
let tagName = '';
|
|
121
|
+
while (i < html.length && /[a-zA-Z0-9-]/.test(html[i])) {
|
|
122
|
+
tagName += html[i];
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
if (!tagName) {
|
|
126
|
+
// Not a valid tag, continue searching
|
|
127
|
+
i = tagStart + 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Parse attributes, respecting quotes
|
|
131
|
+
let attrs = '';
|
|
132
|
+
let inQuote = null;
|
|
133
|
+
let tagEnd = -1;
|
|
134
|
+
while (i < html.length) {
|
|
135
|
+
const char = html[i];
|
|
136
|
+
if (inQuote) {
|
|
137
|
+
attrs += char;
|
|
138
|
+
if (char === inQuote) {
|
|
139
|
+
inQuote = null;
|
|
140
|
+
}
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
else if (char === '"' || char === "'") {
|
|
144
|
+
attrs += char;
|
|
145
|
+
inQuote = char;
|
|
146
|
+
i++;
|
|
147
|
+
}
|
|
148
|
+
else if (char === '>') {
|
|
149
|
+
tagEnd = i + 1; // Include the '>'
|
|
150
|
+
i++;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
attrs += char;
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (tagEnd === -1) {
|
|
159
|
+
// Malformed tag (no closing >), continue searching
|
|
160
|
+
i = tagStart + 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// Check for self-closing
|
|
164
|
+
const trimmedAttrs = attrs.trim();
|
|
165
|
+
const isSelfClose = trimmedAttrs.endsWith('/');
|
|
166
|
+
const finalAttrs = isSelfClose ? trimmedAttrs.slice(0, -1).trim() : trimmedAttrs;
|
|
167
|
+
return {
|
|
168
|
+
index: tagStart,
|
|
169
|
+
endIndex: tagEnd,
|
|
170
|
+
isClose,
|
|
171
|
+
tagName,
|
|
172
|
+
attrs: finalAttrs,
|
|
173
|
+
isSelfClose
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
export function parseHTML(html) {
|
|
179
|
+
const match = html.match(/<template([^>]*)>([\s\S]*)<\/template>/i);
|
|
180
|
+
if (!match) {
|
|
181
|
+
throw new Error("No <template> tag found in HTML");
|
|
182
|
+
}
|
|
183
|
+
const content = match[2].trim();
|
|
184
|
+
const stack = [];
|
|
185
|
+
const textChunks = [];
|
|
186
|
+
const fragmentChildren = [];
|
|
187
|
+
const fragmentElements = [];
|
|
188
|
+
let pos = 0;
|
|
189
|
+
const flushText = () => {
|
|
190
|
+
if (textChunks.length > 0) {
|
|
191
|
+
const text = textChunks.join("");
|
|
192
|
+
textChunks.length = 0;
|
|
193
|
+
const textNode = createTextNode(text);
|
|
194
|
+
if (stack.length > 0) {
|
|
195
|
+
const parent = stack[stack.length - 1];
|
|
196
|
+
parent.childNodes.push(textNode);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
fragmentChildren.push(textNode);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
while (pos < content.length) {
|
|
204
|
+
const tagInfo = findNextTag(content, pos);
|
|
205
|
+
if (!tagInfo) {
|
|
206
|
+
// No more tags, add remaining as text
|
|
207
|
+
if (pos < content.length) {
|
|
208
|
+
textChunks.push(content.slice(pos));
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
// Add text before this tag
|
|
213
|
+
if (tagInfo.index > pos) {
|
|
214
|
+
textChunks.push(content.slice(pos, tagInfo.index));
|
|
215
|
+
}
|
|
216
|
+
const { isClose, tagName, attrs, isSelfClose, endIndex } = tagInfo;
|
|
217
|
+
if (isClose) {
|
|
218
|
+
flushText();
|
|
219
|
+
if (stack.length > 0) {
|
|
220
|
+
const closedElement = stack.pop();
|
|
221
|
+
if (closedElement.tagName.toLowerCase() !== tagName.toLowerCase()) {
|
|
222
|
+
throw new Error(`Mismatched tags: expected </${closedElement.tagName}>, got </${tagName}>`);
|
|
223
|
+
}
|
|
224
|
+
// Update parent's firstElementChild if needed
|
|
225
|
+
const parent = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
226
|
+
if (parent && !parent.firstElementChild) {
|
|
227
|
+
parent.firstElementChild = closedElement;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
flushText();
|
|
233
|
+
const attributes = parseAttributes(attrs);
|
|
234
|
+
const element = createElement(tagName, attributes);
|
|
235
|
+
if (stack.length === 0) {
|
|
236
|
+
// Top-level element
|
|
237
|
+
fragmentChildren.push(element);
|
|
238
|
+
fragmentElements.push(element);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const parent = stack[stack.length - 1];
|
|
242
|
+
parent.children.push(element);
|
|
243
|
+
parent.childNodes.push(element);
|
|
244
|
+
if (!parent.firstElementChild) {
|
|
245
|
+
parent.firstElementChild = element;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!isSelfClose) {
|
|
249
|
+
stack.push(element);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Move position past this tag
|
|
253
|
+
pos = endIndex;
|
|
254
|
+
}
|
|
255
|
+
// Flush any remaining text
|
|
256
|
+
if (stack.length === 0) {
|
|
257
|
+
flushText();
|
|
258
|
+
}
|
|
259
|
+
if (stack.length > 0) {
|
|
260
|
+
throw new Error(`Unclosed tags remain: ${stack.map(e => e.tagName).join(', ')}`);
|
|
261
|
+
}
|
|
262
|
+
const fragment = {
|
|
263
|
+
childNodes: fragmentChildren,
|
|
264
|
+
firstElementChild: fragmentElements[0] || null,
|
|
265
|
+
};
|
|
266
|
+
const templateAttrs = parseAttributes(match[1].trim());
|
|
267
|
+
const templateElement = {
|
|
268
|
+
...createElement("template", templateAttrs),
|
|
269
|
+
content: fragment,
|
|
270
|
+
};
|
|
271
|
+
templateElement.childNodes = [...fragmentChildren];
|
|
272
|
+
templateElement.children = [...fragmentElements];
|
|
273
|
+
templateElement.firstElementChild = fragmentElements[0] || null;
|
|
274
|
+
return templateElement;
|
|
275
|
+
}
|