@herb-tools/formatter 0.4.1 → 0.4.2
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/herb-format.js +213 -23
- package/dist/herb-format.js.map +1 -1
- package/dist/index.cjs +198 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +198 -11
- package/dist/index.esm.js.map +1 -1
- package/dist/types/printer.d.ts +14 -0
- package/package.json +2 -2
- package/src/cli.ts +11 -6
- package/src/printer.ts +240 -6
package/dist/types/printer.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare class Printer extends Visitor {
|
|
|
12
12
|
private lines;
|
|
13
13
|
private indentLevel;
|
|
14
14
|
private inlineMode;
|
|
15
|
+
private isInComplexNesting;
|
|
15
16
|
constructor(source: string, options: Required<FormatOptions>);
|
|
16
17
|
print(object: Node | Token, indentLevel?: number): string;
|
|
17
18
|
private push;
|
|
@@ -53,4 +54,17 @@ export declare class Printer extends Visitor {
|
|
|
53
54
|
private visitERBGeneric;
|
|
54
55
|
private renderInlineOpen;
|
|
55
56
|
renderAttribute(attribute: HTMLAttributeNode): string;
|
|
57
|
+
/**
|
|
58
|
+
* Try to render children inline if they are simple enough.
|
|
59
|
+
* Returns the inline string if possible, null otherwise.
|
|
60
|
+
*/
|
|
61
|
+
private tryRenderInline;
|
|
62
|
+
/**
|
|
63
|
+
* Calculate the maximum nesting depth in a subtree of nodes.
|
|
64
|
+
*/
|
|
65
|
+
private getMaxNestingDepth;
|
|
66
|
+
/**
|
|
67
|
+
* Render an HTML element's content inline (without the wrapping tags).
|
|
68
|
+
*/
|
|
69
|
+
private renderElementInline;
|
|
56
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/formatter",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://herb-tools.dev",
|
|
6
6
|
"bugs": "https://github.com/marcoroth/herb/issues/new?title=Package%20%60@herb-tools/formatter%60:%20",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@herb-tools/core": "0.4.
|
|
37
|
+
"@herb-tools/core": "0.4.2",
|
|
38
38
|
"glob": "^11.0.3"
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
package/src/cli.ts
CHANGED
|
@@ -105,13 +105,16 @@ export class CLI {
|
|
|
105
105
|
try {
|
|
106
106
|
const source = readFileSync(filePath, "utf-8")
|
|
107
107
|
const result = formatter.format(source)
|
|
108
|
-
|
|
108
|
+
const output = result.endsWith('\n') ? result : result + '\n'
|
|
109
|
+
|
|
110
|
+
if (output !== source) {
|
|
109
111
|
if (isCheckMode) {
|
|
110
112
|
unformattedFiles.push(filePath)
|
|
111
113
|
} else {
|
|
112
|
-
writeFileSync(filePath,
|
|
114
|
+
writeFileSync(filePath, output, "utf-8")
|
|
113
115
|
console.log(`Formatted: ${filePath}`)
|
|
114
116
|
}
|
|
117
|
+
|
|
115
118
|
formattedCount++
|
|
116
119
|
}
|
|
117
120
|
} catch (error) {
|
|
@@ -134,13 +137,14 @@ export class CLI {
|
|
|
134
137
|
} else {
|
|
135
138
|
const source = readFileSync(file, "utf-8")
|
|
136
139
|
const result = formatter.format(source)
|
|
140
|
+
const output = result.endsWith('\n') ? result : result + '\n'
|
|
137
141
|
|
|
138
|
-
if (
|
|
142
|
+
if (output !== source) {
|
|
139
143
|
if (isCheckMode) {
|
|
140
144
|
console.log(`File is not formatted: ${file}`)
|
|
141
145
|
process.exit(1)
|
|
142
146
|
} else {
|
|
143
|
-
writeFileSync(file,
|
|
147
|
+
writeFileSync(file, output, "utf-8")
|
|
144
148
|
console.log(`Formatted: ${file}`)
|
|
145
149
|
}
|
|
146
150
|
} else if (isCheckMode) {
|
|
@@ -169,12 +173,13 @@ export class CLI {
|
|
|
169
173
|
try {
|
|
170
174
|
const source = readFileSync(filePath, "utf-8")
|
|
171
175
|
const result = formatter.format(source)
|
|
176
|
+
const output = result.endsWith('\n') ? result : result + '\n'
|
|
172
177
|
|
|
173
|
-
if (
|
|
178
|
+
if (output !== source) {
|
|
174
179
|
if (isCheckMode) {
|
|
175
180
|
unformattedFiles.push(filePath)
|
|
176
181
|
} else {
|
|
177
|
-
writeFileSync(filePath,
|
|
182
|
+
writeFileSync(filePath, output, "utf-8")
|
|
178
183
|
console.log(`Formatted: ${filePath}`)
|
|
179
184
|
}
|
|
180
185
|
formattedCount++
|
package/src/printer.ts
CHANGED
|
@@ -68,6 +68,7 @@ export class Printer extends Visitor {
|
|
|
68
68
|
private lines: string[] = []
|
|
69
69
|
private indentLevel: number = 0
|
|
70
70
|
private inlineMode: boolean = false
|
|
71
|
+
private isInComplexNesting: boolean = false
|
|
71
72
|
|
|
72
73
|
constructor(source: string, options: Required<FormatOptions>) {
|
|
73
74
|
super()
|
|
@@ -85,6 +86,7 @@ export class Printer extends Visitor {
|
|
|
85
86
|
|
|
86
87
|
this.lines = []
|
|
87
88
|
this.indentLevel = indentLevel
|
|
89
|
+
this.isInComplexNesting = false // Reset for each top-level element
|
|
88
90
|
|
|
89
91
|
if (typeof (node as any).accept === 'function') {
|
|
90
92
|
node.accept(this)
|
|
@@ -144,6 +146,7 @@ export class Printer extends Visitor {
|
|
|
144
146
|
const attributes = open.children.filter((child): child is HTMLAttributeNode =>
|
|
145
147
|
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
146
148
|
)
|
|
149
|
+
|
|
147
150
|
const inlineNodes = open.children.filter(child =>
|
|
148
151
|
!(child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') &&
|
|
149
152
|
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')
|
|
@@ -177,6 +180,31 @@ export class Printer extends Visitor {
|
|
|
177
180
|
return
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
if (children.length >= 1) {
|
|
184
|
+
if (this.isInComplexNesting) {
|
|
185
|
+
if (children.length === 1) {
|
|
186
|
+
const child = children[0]
|
|
187
|
+
|
|
188
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
189
|
+
const textContent = (child as HTMLTextNode).content.trim()
|
|
190
|
+
const singleLine = `<${tagName}>${textContent}</${tagName}>`
|
|
191
|
+
|
|
192
|
+
if (!textContent.includes('\n') && (indent.length + singleLine.length) <= this.maxLineLength) {
|
|
193
|
+
this.push(indent + singleLine)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
const inlineResult = this.tryRenderInline(children, tagName)
|
|
200
|
+
|
|
201
|
+
if (inlineResult && (indent.length + inlineResult.length) <= this.maxLineLength) {
|
|
202
|
+
this.push(indent + inlineResult)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
180
208
|
this.push(indent + `<${tagName}>`)
|
|
181
209
|
|
|
182
210
|
this.withIndent(() => {
|
|
@@ -221,10 +249,18 @@ export class Printer extends Visitor {
|
|
|
221
249
|
(singleAttribute.value instanceof HTMLAttributeValueNode || (singleAttribute.value as any)?.type === 'AST_HTML_ATTRIBUTE_VALUE_NODE') &&
|
|
222
250
|
(singleAttribute.value as any)?.children.length === 0
|
|
223
251
|
|
|
252
|
+
const hasERBControlFlow = inlineNodes.some(node =>
|
|
253
|
+
node instanceof ERBIfNode || (node as any).type === 'AST_ERB_IF_NODE' ||
|
|
254
|
+
node instanceof ERBUnlessNode || (node as any).type === 'AST_ERB_UNLESS_NODE' ||
|
|
255
|
+
node instanceof ERBBlockNode || (node as any).type === 'AST_ERB_BLOCK_NODE' ||
|
|
256
|
+
node instanceof ERBCaseNode || (node as any).type === 'AST_ERB_CASE_NODE' ||
|
|
257
|
+
node instanceof ERBWhileNode || (node as any).type === 'AST_ERB_WHILE_NODE' ||
|
|
258
|
+
node instanceof ERBForNode || (node as any).type === 'AST_ERB_FOR_NODE'
|
|
259
|
+
)
|
|
260
|
+
|
|
224
261
|
const shouldKeepInline = (attributes.length <= 3 &&
|
|
225
|
-
!hasEmptyValue &&
|
|
226
262
|
inline.length + indent.length <= this.maxLineLength) ||
|
|
227
|
-
inlineNodes.length > 0
|
|
263
|
+
(inlineNodes.length > 0 && !hasERBControlFlow)
|
|
228
264
|
|
|
229
265
|
if (shouldKeepInline) {
|
|
230
266
|
if (children.length === 0) {
|
|
@@ -235,6 +271,7 @@ export class Printer extends Visitor {
|
|
|
235
271
|
} else {
|
|
236
272
|
this.push(indent + inline.replace('>', `></${tagName}>`))
|
|
237
273
|
}
|
|
274
|
+
|
|
238
275
|
return
|
|
239
276
|
}
|
|
240
277
|
|
|
@@ -255,7 +292,32 @@ export class Printer extends Visitor {
|
|
|
255
292
|
return
|
|
256
293
|
}
|
|
257
294
|
|
|
258
|
-
if (inlineNodes.length > 0) {
|
|
295
|
+
if (inlineNodes.length > 0 && hasERBControlFlow) {
|
|
296
|
+
this.push(indent + `<${tagName}`)
|
|
297
|
+
this.withIndent(() => {
|
|
298
|
+
open.children.forEach(child => {
|
|
299
|
+
if (child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
300
|
+
this.push(this.indent() + this.renderAttribute(child as HTMLAttributeNode))
|
|
301
|
+
} else if (!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE')) {
|
|
302
|
+
this.visit(child)
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
if (isSelfClosing) {
|
|
308
|
+
this.push(indent + "/>")
|
|
309
|
+
} else if (node.is_void) {
|
|
310
|
+
this.push(indent + ">")
|
|
311
|
+
} else if (children.length === 0) {
|
|
312
|
+
this.push(indent + ">" + `</${tagName}>`)
|
|
313
|
+
} else {
|
|
314
|
+
this.push(indent + ">")
|
|
315
|
+
this.withIndent(() => {
|
|
316
|
+
children.forEach(child => this.visit(child))
|
|
317
|
+
})
|
|
318
|
+
this.push(indent + `</${tagName}>`)
|
|
319
|
+
}
|
|
320
|
+
} else if (inlineNodes.length > 0) {
|
|
259
321
|
this.push(indent + this.renderInlineOpen(tagName, attributes, isSelfClosing, inlineNodes, open.children))
|
|
260
322
|
|
|
261
323
|
if (!isSelfClosing && !node.is_void && children.length > 0) {
|
|
@@ -336,7 +398,6 @@ export class Printer extends Visitor {
|
|
|
336
398
|
(singleAttribute.value as any)?.children.length === 0
|
|
337
399
|
|
|
338
400
|
const shouldKeepInline = attributes.length <= 3 &&
|
|
339
|
-
!hasEmptyValue &&
|
|
340
401
|
inline.length + indent.length <= this.maxLineLength
|
|
341
402
|
|
|
342
403
|
if (shouldKeepInline) {
|
|
@@ -539,21 +600,39 @@ export class Printer extends Visitor {
|
|
|
539
600
|
const open = node.tag_opening?.value ?? ""
|
|
540
601
|
const content = node.content?.value ?? ""
|
|
541
602
|
const close = node.tag_closing?.value ?? ""
|
|
603
|
+
|
|
542
604
|
this.lines.push(open + content + close)
|
|
543
605
|
|
|
544
|
-
node.statements.
|
|
606
|
+
if (node.statements.length > 0) {
|
|
607
|
+
this.lines.push(" ")
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
node.statements.forEach((child, index) => {
|
|
545
611
|
if (child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE') {
|
|
546
|
-
this.lines.push(
|
|
612
|
+
this.lines.push(this.renderAttribute(child as HTMLAttributeNode))
|
|
547
613
|
} else {
|
|
548
614
|
this.visit(child)
|
|
549
615
|
}
|
|
616
|
+
|
|
617
|
+
if (index < node.statements.length - 1) {
|
|
618
|
+
this.lines.push(" ")
|
|
619
|
+
}
|
|
550
620
|
})
|
|
551
621
|
|
|
622
|
+
if (node.statements.length > 0) {
|
|
623
|
+
this.lines.push(" ")
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (node.subsequent) {
|
|
627
|
+
this.visit(node.subsequent)
|
|
628
|
+
}
|
|
629
|
+
|
|
552
630
|
if (node.end_node) {
|
|
553
631
|
const endNode = node.end_node as any
|
|
554
632
|
const endOpen = endNode.tag_opening?.value ?? ""
|
|
555
633
|
const endContent = endNode.content?.value ?? ""
|
|
556
634
|
const endClose = endNode.tag_closing?.value ?? ""
|
|
635
|
+
|
|
557
636
|
this.lines.push(endOpen + endContent + endClose)
|
|
558
637
|
}
|
|
559
638
|
} else {
|
|
@@ -758,4 +837,159 @@ export class Printer extends Visitor {
|
|
|
758
837
|
|
|
759
838
|
return name + equals + value
|
|
760
839
|
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Try to render children inline if they are simple enough.
|
|
843
|
+
* Returns the inline string if possible, null otherwise.
|
|
844
|
+
*/
|
|
845
|
+
private tryRenderInline(children: Node[], tagName: string, depth: number = 0): string | null {
|
|
846
|
+
if (children.length > 10) {
|
|
847
|
+
return null
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const maxNestingDepth = this.getMaxNestingDepth(children, 0)
|
|
851
|
+
|
|
852
|
+
if (maxNestingDepth > 1) {
|
|
853
|
+
this.isInComplexNesting = true
|
|
854
|
+
return null
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
for (const child of children) {
|
|
858
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
859
|
+
const textContent = (child as HTMLTextNode).content
|
|
860
|
+
|
|
861
|
+
if (textContent.includes('\n')) {
|
|
862
|
+
return null
|
|
863
|
+
}
|
|
864
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
865
|
+
const element = child as HTMLElementNode
|
|
866
|
+
const openTag = element.open_tag as HTMLOpenTagNode
|
|
867
|
+
|
|
868
|
+
const attributes = openTag.children.filter((child): child is HTMLAttributeNode =>
|
|
869
|
+
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
if (attributes.length > 0) {
|
|
873
|
+
return null
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
877
|
+
// ERB content nodes are allowed in inline rendering
|
|
878
|
+
} else {
|
|
879
|
+
return null
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const oldLines = this.lines
|
|
884
|
+
const oldInlineMode = this.inlineMode
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
this.lines = []
|
|
888
|
+
this.inlineMode = true
|
|
889
|
+
|
|
890
|
+
let content = ''
|
|
891
|
+
|
|
892
|
+
for (const child of children) {
|
|
893
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
894
|
+
content += (child as HTMLTextNode).content
|
|
895
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
896
|
+
const element = child as HTMLElementNode
|
|
897
|
+
const openTag = element.open_tag as HTMLOpenTagNode
|
|
898
|
+
const childTagName = openTag?.tag_name?.value || ''
|
|
899
|
+
|
|
900
|
+
const attributes = openTag.children.filter((child): child is HTMLAttributeNode =>
|
|
901
|
+
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
const attributesString = attributes.length > 0
|
|
905
|
+
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
906
|
+
: ''
|
|
907
|
+
|
|
908
|
+
const elementContent = this.renderElementInline(element)
|
|
909
|
+
|
|
910
|
+
content += `<${childTagName}${attributesString}>${elementContent}</${childTagName}>`
|
|
911
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
912
|
+
const erbNode = child as ERBContentNode
|
|
913
|
+
const open = erbNode.tag_opening?.value ?? ""
|
|
914
|
+
const erbContent = erbNode.content?.value ?? ""
|
|
915
|
+
const close = erbNode.tag_closing?.value ?? ""
|
|
916
|
+
|
|
917
|
+
content += `${open} ${erbContent.trim()} ${close}`
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
content = content.replace(/\s+/g, ' ').trim()
|
|
922
|
+
|
|
923
|
+
return `<${tagName}>${content}</${tagName}>`
|
|
924
|
+
|
|
925
|
+
} finally {
|
|
926
|
+
this.lines = oldLines
|
|
927
|
+
this.inlineMode = oldInlineMode
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Calculate the maximum nesting depth in a subtree of nodes.
|
|
933
|
+
*/
|
|
934
|
+
private getMaxNestingDepth(children: Node[], currentDepth: number): number {
|
|
935
|
+
let maxDepth = currentDepth
|
|
936
|
+
|
|
937
|
+
for (const child of children) {
|
|
938
|
+
if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
939
|
+
const element = child as HTMLElementNode
|
|
940
|
+
const elementChildren = element.body.filter(
|
|
941
|
+
child =>
|
|
942
|
+
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE') &&
|
|
943
|
+
!((child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') && (child as any)?.content.trim() === ""),
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
const childDepth = this.getMaxNestingDepth(elementChildren, currentDepth + 1)
|
|
947
|
+
maxDepth = Math.max(maxDepth, childDepth)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return maxDepth
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Render an HTML element's content inline (without the wrapping tags).
|
|
956
|
+
*/
|
|
957
|
+
private renderElementInline(element: HTMLElementNode): string {
|
|
958
|
+
const children = element.body.filter(
|
|
959
|
+
child =>
|
|
960
|
+
!(child instanceof WhitespaceNode || (child as any).type === 'AST_WHITESPACE_NODE') &&
|
|
961
|
+
!((child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') && (child as any)?.content.trim() === ""),
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
let content = ''
|
|
965
|
+
for (const child of children) {
|
|
966
|
+
if (child instanceof HTMLTextNode || (child as any).type === 'AST_HTML_TEXT_NODE') {
|
|
967
|
+
content += (child as HTMLTextNode).content
|
|
968
|
+
} else if (child instanceof HTMLElementNode || (child as any).type === 'AST_HTML_ELEMENT_NODE') {
|
|
969
|
+
const childElement = child as HTMLElementNode
|
|
970
|
+
const openTag = childElement.open_tag as HTMLOpenTagNode
|
|
971
|
+
const childTagName = openTag?.tag_name?.value || ''
|
|
972
|
+
|
|
973
|
+
const attributes = openTag.children.filter((child): child is HTMLAttributeNode =>
|
|
974
|
+
child instanceof HTMLAttributeNode || (child as any).type === 'AST_HTML_ATTRIBUTE_NODE'
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
const attributesString = attributes.length > 0
|
|
978
|
+
? ' ' + attributes.map(attr => this.renderAttribute(attr)).join(' ')
|
|
979
|
+
: ''
|
|
980
|
+
|
|
981
|
+
const childContent = this.renderElementInline(childElement)
|
|
982
|
+
content += `<${childTagName}${attributesString}>${childContent}</${childTagName}>`
|
|
983
|
+
} else if (child instanceof ERBContentNode || (child as any).type === 'AST_ERB_CONTENT_NODE') {
|
|
984
|
+
const erbNode = child as ERBContentNode
|
|
985
|
+
const open = erbNode.tag_opening?.value ?? ""
|
|
986
|
+
const erbContent = erbNode.content?.value ?? ""
|
|
987
|
+
const close = erbNode.tag_closing?.value ?? ""
|
|
988
|
+
|
|
989
|
+
content += `${open} ${erbContent.trim()} ${close}`
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return content.replace(/\s+/g, ' ').trim()
|
|
994
|
+
}
|
|
761
995
|
}
|