xml-to-html-converter 0.3.1 → 0.4.1

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/README.md CHANGED
@@ -6,72 +6,49 @@
6
6
  ![XML](https://img.shields.io/badge/input-XML-orange)
7
7
  ![HTML](https://img.shields.io/badge/output-HTML-red)
8
8
 
9
- A zero-dependency Node.js package for converting XML to HTML. Currently in pre-1.0.0 development, building the foundation one functional part at a time. Full XML-to-HTML conversion is the goal of `v1.0.0`.
9
+ A zero-dependency Node.js package for converting XML to HTML.
10
10
 
11
- ---
11
+ - **`minify(xml)`** strips inter-tag whitespace from prettified XML before parsing. Text content is left untouched
12
+ - **`scaffold(xml)`** reads any XML string and returns a nested node tree
13
+ - **`walk(nodes, visitor)`** traverses the full node tree depth-first, visiting every node
14
+ - **`render(nodes)`** converts a node tree to an HTML string. Every XML element becomes a `<div>` with `data-tag` and `data-attrs-*` attributes
12
15
 
13
- ## XML Node Extraction & Scaffolding
16
+ ---
14
17
 
15
- The `scaffold` function walks an XML string and produces an array of `XmlNode` objects, each carrying its role, its raw source text, and its position in the document, both globally across the full document and locally within its parent.
18
+ ## Install
16
19
 
17
- ```ts
18
- interface XmlAttribute {
19
- name: string;
20
- value: string;
21
- }
22
-
23
- interface XmlNode {
24
- role: XmlNodeRole;
25
- raw: string;
26
- xmlTag?: string;
27
- xmlInner?: string;
28
- xmlAttributes?: XmlAttribute[];
29
- globalIndex: number;
30
- localIndex: number;
31
- children?: XmlNode[];
32
- malformed?: true;
33
- }
34
-
35
- type XmlNodeRole =
36
- | "closeTag"
37
- | "comment"
38
- | "doctype"
39
- | "openTag"
40
- | "processingInstruction"
41
- | "selfTag"
42
- | "textLeaf";
20
+ ```bash
21
+ npm install xml-to-html-converter
43
22
  ```
44
23
 
45
- This scaffold is the foundation everything else will be built on. No transformation, no HTML output, no opinions about content, just an accurate, traversable representation of what the XML says.
46
-
47
24
  ---
48
25
 
49
- > **Where I am right now**
50
- >
51
- > `v0.x` is building the scaffold and the first render pass.
52
- >
53
- > - **`minify(xml)`** strips inter-tag whitespace from prettified XML before parsing — text content is left untouched
54
- > - **`scaffold(xml)`** reads any XML string and returns a nested node tree
55
- > - Every node knows its `role`, its `raw` source string, its `globalIndex` in the document, and its `localIndex` within its parent
56
- > - Tag nodes (`openTag`, `selfTag`) also carry `xmlTag`, `xmlInner`, and `xmlAttributes` — the parsed tag name, raw attribute string, and structured attribute array
57
- > - Broken XML is never thrown — malformed nodes are flagged with `malformed: true` in place and the tree is built regardless
58
- > - **`render(nodes)`** takes the scaffold output and converts it to an HTML string — every XML element becomes a `<div>` with `data-tag` and `data-attrs-*` attributes
59
- >
60
- > `v1.0.0` is when this package becomes what it says it is: a full XML-to-HTML converter. Everything before that is the work to get there.
26
+ ## Usage
61
27
 
62
- ---
28
+ ### minify
63
29
 
64
- ## Install
30
+ When your XML comes from a file or an API it is usually indented and line-broken. `minify` strips the whitespace between tags before parsing. Text content is left completely untouched.
65
31
 
66
- ```bash
67
- npm install xml-to-html-converter
32
+ ```js
33
+ import { minify } from "xml-to-html-converter";
34
+
35
+ const clean = minify(`
36
+ <bookstore>
37
+ <book category="cooking">
38
+ <title lang="en">Everyday Italian</title>
39
+ </book>
40
+ </bookstore>
41
+ `);
42
+ // <bookstore><book category="cooking"><title lang="en">Everyday Italian</title></book></bookstore>
68
43
  ```
69
44
 
45
+ `minify` is opt-in. Skip it if whitespace inside your content is meaningful.
46
+
70
47
  ---
71
48
 
72
- ## Usage
49
+ ### scaffold
73
50
 
74
- ### Parsing XML into a node tree
51
+ `scaffold` parses an XML string into a structured tree of `XmlNode` objects. Each node carries its role, its raw source text, and its position in the document both globally across the full document and locally within its parent.
75
52
 
76
53
  ```js
77
54
  import { scaffold } from "xml-to-html-converter";
@@ -135,7 +112,43 @@ const tree = scaffold(`
135
112
  ]
136
113
  ```
137
114
 
138
- ### Converting the tree to HTML
115
+ `scaffold` never throws. Malformed structures are flagged with `malformed: true` in place and the tree is built regardless. See [Malformed XML](#malformed-xml) for details.
116
+
117
+ ---
118
+
119
+ ### walk
120
+
121
+ `walk` traverses the full node tree depth-first, calling a visitor function on every node including all descendants. The visitor decides what to collect or do. `walk` has no opinions.
122
+
123
+ ```js
124
+ import { scaffold, walk } from "xml-to-html-converter";
125
+
126
+ const tree = scaffold(xml);
127
+
128
+ // collect all text content
129
+ const text = [];
130
+ walk(tree, (node) => {
131
+ if (node.role === "textLeaf") text.push(node.raw);
132
+ });
133
+
134
+ // find all nodes with a specific tag
135
+ const titles = [];
136
+ walk(tree, (node) => {
137
+ if (node.xmlTag === "title") titles.push(node);
138
+ });
139
+
140
+ // check for malformed nodes anywhere in the tree
141
+ const broken = [];
142
+ walk(tree, (node) => {
143
+ if (node.malformed) broken.push(node);
144
+ });
145
+ ```
146
+
147
+ ---
148
+
149
+ ### render
150
+
151
+ `render` walks the node tree and converts every XML element to a `<div>`. The original tag name is preserved in `data-tag` and each attribute becomes its own `data-attrs-*` attribute.
139
152
 
140
153
  ```js
141
154
  import { scaffold, render } from "xml-to-html-converter";
@@ -151,8 +164,6 @@ const html = render(
151
164
  );
152
165
  ```
153
166
 
154
- `render` walks the node tree and converts every XML element to a `<div>`. The original tag name is preserved in `data-tag` and each attribute becomes its own `data-attrs-*` attribute:
155
-
156
167
  ```html
157
168
  <div data-tag="bookstore">
158
169
  <div data-tag="book" data-attrs-category="cooking">
@@ -161,21 +172,23 @@ const html = render(
161
172
  </div>
162
173
  ```
163
174
 
164
- Processing instructions and doctypes are dropped. Comments are passed through unchanged.
175
+ Processing instructions and doctypes are dropped. Comments are passed through unchanged. The output is a raw HTML string — if you are inserting it into a web page, treat it accordingly.
165
176
 
166
177
  ---
167
178
 
168
- ### Minifying prettified XML
169
-
170
- When your XML comes from a file or an API it is usually indented and line-broken. `minify` strips the whitespace between tags before parsing, leaving text content completely untouched.
179
+ ### Full pipeline
171
180
 
172
181
  ```js
173
- import { minify, scaffold, render } from "xml-to-html-converter";
182
+ import { minify, scaffold, walk, render } from "xml-to-html-converter";
174
183
 
175
- const html = render(scaffold(minify(xml)));
176
- ```
184
+ const tree = scaffold(minify(xml));
177
185
 
178
- `minify` is opt-in. Skip it if whitespace inside your content is meaningful.
186
+ walk(tree, (node) => {
187
+ if (node.malformed) console.warn("malformed node", node.raw);
188
+ });
189
+
190
+ const html = render(tree);
191
+ ```
179
192
 
180
193
  ---
181
194
 
@@ -187,7 +200,7 @@ Every node in the tree has the following fields:
187
200
  | --------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
188
201
  | `role` | `XmlNodeRole` | What kind of node this is |
189
202
  | `raw` | `string` | The exact source string, untouched |
190
- | `xmlTag` | `string` | Tag name only, e.g. `"book"` or `"env:Envelope"`. Present on `openTag`, `selfTag`, `closeTag` |
203
+ | `xmlTag` | `string` | Tag name only, e.g. `"book"` or `"env:Envelope"`. Present on `openTag`, `selfTag`, and `closeTag` |
191
204
  | `xmlInner` | `string` | Everything after the tag name inside the brackets, verbatim. Present on `openTag` and `selfTag` when attributes exist |
192
205
  | `xmlAttributes` | `XmlAttribute[]` | Parsed array of `{ name, value }` attribute objects. Present on `openTag` and `selfTag` when attributes exist |
193
206
  | `globalIndex` | `number` | Position in the entire document (never resets) |
@@ -215,11 +228,15 @@ Every node in the tree has the following fields:
215
228
 
216
229
  `scaffold` never throws. No matter what the input looks like, it always returns a complete tree. Malformed structures are flagged with `malformed: true` in place and the walk continues.
217
230
 
218
- Four cases are handled:
231
+ Eight cases are handled:
219
232
 
220
233
  - **Unclosed tags** - opens but never closes, gets `malformed: true`, children are still collected
221
234
  - **Stray closing tags** - a `</tag>` with no matching open surfaces as a `closeTag` token with `malformed: true`
222
235
  - **Unclosed brackets** - a `<` with no matching `>` captures the remainder as a malformed token
236
+ - **Unquoted attributes** - `<tag attr=unquoted>` flags the node `malformed: true`, any valid attributes parsed before the error are preserved
237
+ - **Unclosed processing instructions** - `<?xml ...` with no `?>` captures the remainder as a malformed token
238
+ - **Unclosed comments** - `<!-- ...` with no `-->` captures the remainder as a malformed token
239
+ - **Unclosed CDATA** - `<![CDATA[ ...` with no `]]>` captures the remainder as a malformed token
223
240
  - **Excessive nesting** - documents nested beyond 500 levels have the deepest open tag flagged `malformed: true` to prevent a stack overflow
224
241
 
225
242
  ```js
@@ -268,7 +285,13 @@ const tree = scaffold("<root><unclosed><valid>text</valid></root>");
268
285
  ## Exports
269
286
 
270
287
  ```ts
271
- import { scaffold, render, minify, isMalformed } from "xml-to-html-converter";
288
+ import {
289
+ minify,
290
+ scaffold,
291
+ walk,
292
+ render,
293
+ isMalformed,
294
+ } from "xml-to-html-converter";
272
295
  import type {
273
296
  XmlNode,
274
297
  XmlNodeRole,
@@ -277,16 +300,17 @@ import type {
277
300
  } from "xml-to-html-converter";
278
301
  ```
279
302
 
280
- | Export | Kind | Description |
281
- | ------------------ | -------- | --------------------------------------------------- |
282
- | `minify` | function | Strips inter-tag whitespace from an XML string |
283
- | `scaffold` | function | Parses an XML string and returns a node tree |
284
- | `render` | function | Converts a node tree to an HTML string |
285
- | `isMalformed` | function | Type guard, narrows `XmlNode` to `MalformedXmlNode` |
286
- | `XmlNode` | type | The shape of every node in the tree |
287
- | `XmlNodeRole` | type | Union of all valid role strings |
288
- | `XmlAttribute` | type | Shape of a parsed attribute `{ name, value }` |
289
- | `MalformedXmlNode` | type | `XmlNode` narrowed to `{ malformed: true }` |
303
+ | Export | Kind | Description |
304
+ | ------------------ | -------- | ------------------------------------------------------- |
305
+ | `minify` | function | Strips inter-tag whitespace from an XML string |
306
+ | `scaffold` | function | Parses an XML string and returns a node tree |
307
+ | `walk` | function | Traverses a node tree depth-first with a visitor |
308
+ | `render` | function | Converts a node tree to an HTML string |
309
+ | `isMalformed` | function | Type guard that narrows `XmlNode` to `MalformedXmlNode` |
310
+ | `XmlNode` | type | The shape of every node in the tree |
311
+ | `XmlNodeRole` | type | Union of all valid role strings |
312
+ | `XmlAttribute` | type | Shape of a parsed attribute `{ name, value }` |
313
+ | `MalformedXmlNode` | type | `XmlNode` narrowed to nodes where `malformed` is `true` |
290
314
 
291
315
  ---
292
316
 
package/dist/index.cjs CHANGED
@@ -23,13 +23,14 @@ __export(src_exports, {
23
23
  isMalformed: () => isMalformed,
24
24
  minify: () => minify,
25
25
  render: () => render,
26
- scaffold: () => scaffold
26
+ scaffold: () => scaffold,
27
+ walk: () => walk
27
28
  });
28
29
  module.exports = __toCommonJS(src_exports);
29
30
 
30
31
  // src/modules/minify/minify.ts
31
32
  function minify(xml) {
32
- return xml.replace(/>(\s+)</g, (_, gap) => gap.trim() === "" ? "><" : `>${gap}<`).trim();
33
+ return xml.replace(/>(\s+)</g, "><").trim();
33
34
  }
34
35
 
35
36
  // src/modules/render/render.ts
@@ -73,7 +74,12 @@ function parseXmlAttributes(xmlInner) {
73
74
  i++;
74
75
  while (i < s.length && /\s/.test(s[i])) i++;
75
76
  const quote = s[i];
76
- if (quote !== '"' && quote !== "'") break;
77
+ if (quote !== '"' && quote !== "'") {
78
+ return {
79
+ attributes: attributes.length > 0 ? attributes : void 0,
80
+ malformed: true
81
+ };
82
+ }
77
83
  i++;
78
84
  const valueStart = i;
79
85
  while (i < s.length && s[i] !== quote) i++;
@@ -81,7 +87,10 @@ function parseXmlAttributes(xmlInner) {
81
87
  i++;
82
88
  attributes.push({ name, value });
83
89
  }
84
- return attributes.length > 0 ? attributes : void 0;
90
+ return {
91
+ attributes: attributes.length > 0 ? attributes : void 0,
92
+ malformed: false
93
+ };
85
94
  }
86
95
  var MAX_DEPTH = 500;
87
96
  function scaffold(xml) {
@@ -184,7 +193,8 @@ function extractXmlNodes(xml, position) {
184
193
  raw: xml.slice(position),
185
194
  role: "processingInstruction",
186
195
  tag: "",
187
- end: xml.length
196
+ end: xml.length,
197
+ malformed: true
188
198
  } : {
189
199
  raw: xml.slice(position, end2 + 2),
190
200
  role: "processingInstruction",
@@ -194,7 +204,7 @@ function extractXmlNodes(xml, position) {
194
204
  }
195
205
  if (xml[position + 1] === "!" && xml[position + 2] === "[") {
196
206
  const end2 = xml.indexOf("]]>", position + 3);
197
- return end2 === -1 ? { raw: xml.slice(position), role: "textLeaf", tag: "", end: xml.length } : {
207
+ return end2 === -1 ? { raw: xml.slice(position), role: "textLeaf", tag: "", end: xml.length, malformed: true } : {
198
208
  raw: xml.slice(position, end2 + 3),
199
209
  role: "textLeaf",
200
210
  tag: "",
@@ -203,7 +213,7 @@ function extractXmlNodes(xml, position) {
203
213
  }
204
214
  if (xml[position + 1] === "!" && xml[position + 2] === "-" && xml[position + 3] === "-") {
205
215
  const end2 = xml.indexOf("-->", position + 4);
206
- return end2 === -1 ? { raw: xml.slice(position), role: "comment", tag: "", end: xml.length } : {
216
+ return end2 === -1 ? { raw: xml.slice(position), role: "comment", tag: "", end: xml.length, malformed: true } : {
207
217
  raw: xml.slice(position, end2 + 3),
208
218
  role: "comment",
209
219
  tag: "",
@@ -243,23 +253,32 @@ function extractXmlNodes(xml, position) {
243
253
  const trimmed = inner.slice(0, -1).trim();
244
254
  const tag2 = trimmed.split(/\s/)[0] ?? "";
245
255
  const xmlInner2 = trimmed.slice(tag2.length).trim() || void 0;
246
- const xmlAttributes2 = xmlInner2 ? parseXmlAttributes(xmlInner2) : void 0;
247
- return { raw, role: "selfTag", tag: tag2, xmlInner: xmlInner2, xmlAttributes: xmlAttributes2, end };
256
+ const parsed2 = xmlInner2 ? parseXmlAttributes(xmlInner2) : void 0;
257
+ return { raw, role: "selfTag", tag: tag2, xmlInner: xmlInner2, xmlAttributes: parsed2?.attributes, end, malformed: parsed2?.malformed ? true : void 0 };
248
258
  }
249
259
  const tag = inner.split(/\s/)[0] ?? "";
250
260
  const xmlInner = inner.slice(tag.length).trim() || void 0;
251
- const xmlAttributes = xmlInner ? parseXmlAttributes(xmlInner) : void 0;
252
- return { raw, role: "openTag", tag, xmlInner, xmlAttributes, end };
261
+ const parsed = xmlInner ? parseXmlAttributes(xmlInner) : void 0;
262
+ return { raw, role: "openTag", tag, xmlInner, xmlAttributes: parsed?.attributes, end, malformed: parsed?.malformed ? true : void 0 };
253
263
  }
254
264
 
255
265
  // src/modules/scaffold/types.ts
256
266
  function isMalformed(node) {
257
267
  return node.malformed === true;
258
268
  }
269
+
270
+ // src/modules/walk/walk.ts
271
+ function walk(nodes, visitor) {
272
+ for (const node of nodes) {
273
+ visitor(node);
274
+ if (node.children) walk(node.children, visitor);
275
+ }
276
+ }
259
277
  // Annotate the CommonJS export names for ESM import in node:
260
278
  0 && (module.exports = {
261
279
  isMalformed,
262
280
  minify,
263
281
  render,
264
- scaffold
282
+ scaffold,
283
+ walk
265
284
  });
package/dist/index.d.cts CHANGED
@@ -25,4 +25,6 @@ declare function render(nodes: XmlNode[]): string;
25
25
 
26
26
  declare function scaffold(xml: string): XmlNode[];
27
27
 
28
- export { type MalformedXmlNode, type XmlAttribute, type XmlNode, type XmlNodeRole, isMalformed, minify, render, scaffold };
28
+ declare function walk(nodes: XmlNode[], visitor: (node: XmlNode) => void): void;
29
+
30
+ export { type MalformedXmlNode, type XmlAttribute, type XmlNode, type XmlNodeRole, isMalformed, minify, render, scaffold, walk };
package/dist/index.d.ts CHANGED
@@ -25,4 +25,6 @@ declare function render(nodes: XmlNode[]): string;
25
25
 
26
26
  declare function scaffold(xml: string): XmlNode[];
27
27
 
28
- export { type MalformedXmlNode, type XmlAttribute, type XmlNode, type XmlNodeRole, isMalformed, minify, render, scaffold };
28
+ declare function walk(nodes: XmlNode[], visitor: (node: XmlNode) => void): void;
29
+
30
+ export { type MalformedXmlNode, type XmlAttribute, type XmlNode, type XmlNodeRole, isMalformed, minify, render, scaffold, walk };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/modules/minify/minify.ts
2
2
  function minify(xml) {
3
- return xml.replace(/>(\s+)</g, (_, gap) => gap.trim() === "" ? "><" : `>${gap}<`).trim();
3
+ return xml.replace(/>(\s+)</g, "><").trim();
4
4
  }
5
5
 
6
6
  // src/modules/render/render.ts
@@ -44,7 +44,12 @@ function parseXmlAttributes(xmlInner) {
44
44
  i++;
45
45
  while (i < s.length && /\s/.test(s[i])) i++;
46
46
  const quote = s[i];
47
- if (quote !== '"' && quote !== "'") break;
47
+ if (quote !== '"' && quote !== "'") {
48
+ return {
49
+ attributes: attributes.length > 0 ? attributes : void 0,
50
+ malformed: true
51
+ };
52
+ }
48
53
  i++;
49
54
  const valueStart = i;
50
55
  while (i < s.length && s[i] !== quote) i++;
@@ -52,7 +57,10 @@ function parseXmlAttributes(xmlInner) {
52
57
  i++;
53
58
  attributes.push({ name, value });
54
59
  }
55
- return attributes.length > 0 ? attributes : void 0;
60
+ return {
61
+ attributes: attributes.length > 0 ? attributes : void 0,
62
+ malformed: false
63
+ };
56
64
  }
57
65
  var MAX_DEPTH = 500;
58
66
  function scaffold(xml) {
@@ -155,7 +163,8 @@ function extractXmlNodes(xml, position) {
155
163
  raw: xml.slice(position),
156
164
  role: "processingInstruction",
157
165
  tag: "",
158
- end: xml.length
166
+ end: xml.length,
167
+ malformed: true
159
168
  } : {
160
169
  raw: xml.slice(position, end2 + 2),
161
170
  role: "processingInstruction",
@@ -165,7 +174,7 @@ function extractXmlNodes(xml, position) {
165
174
  }
166
175
  if (xml[position + 1] === "!" && xml[position + 2] === "[") {
167
176
  const end2 = xml.indexOf("]]>", position + 3);
168
- return end2 === -1 ? { raw: xml.slice(position), role: "textLeaf", tag: "", end: xml.length } : {
177
+ return end2 === -1 ? { raw: xml.slice(position), role: "textLeaf", tag: "", end: xml.length, malformed: true } : {
169
178
  raw: xml.slice(position, end2 + 3),
170
179
  role: "textLeaf",
171
180
  tag: "",
@@ -174,7 +183,7 @@ function extractXmlNodes(xml, position) {
174
183
  }
175
184
  if (xml[position + 1] === "!" && xml[position + 2] === "-" && xml[position + 3] === "-") {
176
185
  const end2 = xml.indexOf("-->", position + 4);
177
- return end2 === -1 ? { raw: xml.slice(position), role: "comment", tag: "", end: xml.length } : {
186
+ return end2 === -1 ? { raw: xml.slice(position), role: "comment", tag: "", end: xml.length, malformed: true } : {
178
187
  raw: xml.slice(position, end2 + 3),
179
188
  role: "comment",
180
189
  tag: "",
@@ -214,22 +223,31 @@ function extractXmlNodes(xml, position) {
214
223
  const trimmed = inner.slice(0, -1).trim();
215
224
  const tag2 = trimmed.split(/\s/)[0] ?? "";
216
225
  const xmlInner2 = trimmed.slice(tag2.length).trim() || void 0;
217
- const xmlAttributes2 = xmlInner2 ? parseXmlAttributes(xmlInner2) : void 0;
218
- return { raw, role: "selfTag", tag: tag2, xmlInner: xmlInner2, xmlAttributes: xmlAttributes2, end };
226
+ const parsed2 = xmlInner2 ? parseXmlAttributes(xmlInner2) : void 0;
227
+ return { raw, role: "selfTag", tag: tag2, xmlInner: xmlInner2, xmlAttributes: parsed2?.attributes, end, malformed: parsed2?.malformed ? true : void 0 };
219
228
  }
220
229
  const tag = inner.split(/\s/)[0] ?? "";
221
230
  const xmlInner = inner.slice(tag.length).trim() || void 0;
222
- const xmlAttributes = xmlInner ? parseXmlAttributes(xmlInner) : void 0;
223
- return { raw, role: "openTag", tag, xmlInner, xmlAttributes, end };
231
+ const parsed = xmlInner ? parseXmlAttributes(xmlInner) : void 0;
232
+ return { raw, role: "openTag", tag, xmlInner, xmlAttributes: parsed?.attributes, end, malformed: parsed?.malformed ? true : void 0 };
224
233
  }
225
234
 
226
235
  // src/modules/scaffold/types.ts
227
236
  function isMalformed(node) {
228
237
  return node.malformed === true;
229
238
  }
239
+
240
+ // src/modules/walk/walk.ts
241
+ function walk(nodes, visitor) {
242
+ for (const node of nodes) {
243
+ visitor(node);
244
+ if (node.children) walk(node.children, visitor);
245
+ }
246
+ }
230
247
  export {
231
248
  isMalformed,
232
249
  minify,
233
250
  render,
234
- scaffold
251
+ scaffold,
252
+ walk
235
253
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xml-to-html-converter",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Zero dependency XML to HTML converter for Node environments",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",