Repository: perry-mitchell/webdav-client (ts)

Issue:
Support xml attributes
## Xml example
```xml
<?xml version="1.0"?>
<d:multistatus
	xmlns:d="DAV:"
	xmlns:s="http://sabredav.org/ns"
	xmlns:oc="http://owncloud.org/ns"
	xmlns:nc="http://nextcloud.org/ns">
<d:response>
		<d:href>/remote.php/dav/files/admin/benjamin-lehman-xnTcEJm5IZw-unsplash.jpg</d:href>
		<d:propstat>
			<d:prop>
				<nc:system-tags>
					<nc:system-tag oc:can-assign="true" oc:id="321" oc:user-assignable="true" oc:user-visible="true">Test</nc:system-tag>
				</nc:system-tags>
			</d:prop>
			<d:status>HTTP/1.1 200 OK</d:status>
		</d:propstat>
	</d:response>
</d:multistatus>
```

## Issue
Currently, the `fast-xml-parser` ignore attributes.
A quick test, passing [the following options](https://github.com/perry-mitchell/webdav-client/blob/193268946a608708733a15612caaab13f93b156c/source/tools/dav.ts#L25-L30) would allow us to retrive the attributes like so
```js
        attributeNamePrefix: "",
        ignoreAttributes: false,
        textNodeName: "text",
```
![image](https://github.com/user-attachments/assets/632ee76f-bc79-4f9c-8fe1-8805f1fd28c2)


I think adding a proper method to process those would be best, so we can keep the namespaces and avoid conflicts?


Pull request:
feat: support attributes parsing
Fix #388 

What do you think of that [perry-mitchell](https://github.com/perry-mitchell/) :)

Patch:
diff --git a/source/tools/dav.ts b/source/tools/dav.ts
index 77bf395..69aae0f 100644
--- a/source/tools/dav.ts
+++ b/source/tools/dav.ts
@@ -23,11 +23,22 @@ enum PropertyType {
 
 function getParser(): XMLParser {
     return new XMLParser({
+        allowBooleanAttributes: true,
+        attributeNamePrefix: "",
+        textNodeName: "text",
+        ignoreAttributes: false,
         removeNSPrefix: true,
         numberParseOptions: {
             hex: true,
             leadingZeros: false
         },
+        attributeValueProcessor(attrName, attrValue, jPath) {
+            // handle boolean attributes
+            if (attrValue === "true" || attrValue === "false") {
+                return attrValue === "true";
+            }
+            return attrValue;
+        },
         tagValueProcessor(tagName, tagValue, jPath) {
             if (jPath.endsWith("propstat.prop.displayname")) {
                 // Do not parse the display name, because this causes e.g. '2024.10' to result in number 2024.1


Test patch:
diff --git a/test/node/operations/stat.spec.ts b/test/node/operations/stat.spec.ts
index a78bc03..7850d95 100644
--- a/test/node/operations/stat.spec.ts
+++ b/test/node/operations/stat.spec.ts
@@ -130,6 +130,38 @@ describe("stat", function () {
         });
     });
 
+    it("correctly parses the attributes property", async function () {
+        returnFakeResponse(
+            readFileSync(
+                new URL("../../responses/propfind-attributes.xml", import.meta.url)
+            ).toString()
+        );
+
+        await this.client.stat("/foo/", { details: true }).then(function (result) {
+            expect(result.data).to.have.property("props").that.is.an("object");
+            expect(result.data.props).to.have.property("system-tags").that.is.an("object");
+            expect(result.data.props["system-tags"]["system-tag"])
+                .to.be.an("array")
+                .and.have.lengthOf(2);
+            // <z:system-tag z:can-assign="true" z:id="321" z:checked>Tag1</z:system-tag>
+            // <z:system-tag z:can-assign="false" z:id="654" z:prop="">Tag2</z:system-tag>
+            expect(result.data.props["system-tags"]["system-tag"]).to.deep.equal([
+                {
+                    "can-assign": true,
+                    id: "321",
+                    checked: true,
+                    text: "Tag1"
+                },
+                {
+                    "can-assign": false,
+                    id: "654",
+                    prop: "",
+                    text: "Tag2"
+                }
+            ]);
+        });
+    });
+
     describe("with details: true", function () {
         it("returns data property", function () {
             return this.client.stat("/", { details: true }).then(function (result) {
diff --git a/test/responses/propfind-attributes.xml b/test/responses/propfind-attributes.xml
new file mode 100644
index 0000000..4848d08
--- /dev/null
+++ b/test/responses/propfind-attributes.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<d:multistatus
+	xmlns:d="DAV:"
+	xmlns:z="http://ns.example.com/standards/z1.2.3.4">
+	<d:response>
+		<d:href>/remote.php/dav/files/admin/foo/</d:href>
+		<d:propstat>
+			<d:prop>
+				<d:getetag>&quot;66a15a0171527&quot;</d:getetag>
+				<d:getlastmodified>Wed, 24 Jul 2024 19:46:09 GMT</d:getlastmodified>
+				<d:creationdate>1970-01-01T00:00:00+00:00</d:creationdate>
+				<z:system-tags>
+					<z:system-tag z:can-assign="true" z:id="321" z:checked>Tag1</z:system-tag>
+					<z:system-tag z:can-assign="false" z:id="654" z:prop="">Tag2</z:system-tag>
+				</z:system-tags>
+			</d:prop>
+			<d:status>HTTP/1.1 200 OK</d:status>
+		</d:propstat>
+		<d:propstat>
+			<d:prop>
+				<d:getcontentlength/>
+				<d:getcontenttype/>
+			</d:prop>
+			<d:status>HTTP/1.1 404 Not Found</d:status>
+		</d:propstat>
+	</d:response>
+</d:multistatus>