@fuman/plist 0.1.0

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.
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const utils = require("@fuman/utils");
4
+ const types = require("./types.cjs");
5
+ function getNextElement(iter) {
6
+ while (true) {
7
+ const node = iter.next();
8
+ if (node.done) return null;
9
+ if (node.value.nodeType === 1) return node.value;
10
+ }
11
+ }
12
+ function readXmlPlist(data, params) {
13
+ const { DOMParser = globalThis.DOMParser, parseUid = false } = params ?? {};
14
+ const parser = new DOMParser();
15
+ const doc = parser.parseFromString(data, "text/xml");
16
+ const root = doc.getElementsByTagName("plist")[0];
17
+ if (!root) throw new Error("<plist> not found, invalid plist?");
18
+ const topObject = root.childNodes.filter((node) => node.nodeType === 1);
19
+ if (topObject.length !== 1) throw new Error("expected exactly one top object");
20
+ function parseObject(node) {
21
+ switch (node.tagName) {
22
+ case "dict": {
23
+ const dict = {};
24
+ const childrenIter = node.childNodes[Symbol.iterator]();
25
+ while (true) {
26
+ const key = getNextElement(childrenIter);
27
+ if (!key) break;
28
+ if (key.tagName !== "key") throw new Error(`expected <key>, got <${key.tagName}>`);
29
+ const keyText = key.textContent ?? "";
30
+ const value = getNextElement(childrenIter);
31
+ if (!value) throw new Error(`value for ${keyText} not found`);
32
+ dict[keyText] = parseObject(value);
33
+ }
34
+ if (parseUid && (typeof dict.CF$UID === "number" || typeof dict.CF$UID === "bigint") && Object.keys(dict).length === 1) {
35
+ return new types.PlistValue("uid", dict.CF$UID);
36
+ }
37
+ return dict;
38
+ }
39
+ case "array": {
40
+ const array = [];
41
+ const childrenIter = node.childNodes[Symbol.iterator]();
42
+ while (true) {
43
+ const value = getNextElement(childrenIter);
44
+ if (!value) break;
45
+ array.push(parseObject(value));
46
+ }
47
+ return array;
48
+ }
49
+ case "string":
50
+ return node.textContent ?? "";
51
+ case "data":
52
+ return utils.base64.decode(node.textContent?.trim() ?? "");
53
+ case "integer": {
54
+ const value = node.textContent?.trim() ?? "";
55
+ const numValue = Number(value);
56
+ if (Number.isNaN(numValue)) throw new Error(`invalid integer: ${value}`);
57
+ if (numValue < Number.MIN_SAFE_INTEGER || numValue > Number.MAX_SAFE_INTEGER) {
58
+ return BigInt(value);
59
+ }
60
+ return numValue;
61
+ }
62
+ case "date": {
63
+ const value = new Date(node.textContent?.trim() ?? "");
64
+ if (Number.isNaN(value.getTime())) throw new Error(`invalid date: ${node.textContent}`);
65
+ return value;
66
+ }
67
+ case "false":
68
+ return false;
69
+ case "true":
70
+ return true;
71
+ case "real":
72
+ return Number(node.textContent?.trim() ?? "");
73
+ default:
74
+ throw new Error(`unexpected tag: <${node.tagName}>`);
75
+ }
76
+ }
77
+ return parseObject(topObject[0]);
78
+ }
79
+ exports.readXmlPlist = readXmlPlist;
@@ -0,0 +1,15 @@
1
+ export declare function readXmlPlist(data: string, params?: {
2
+ /**
3
+ * implementation of the DOMParser interface (e.g. `@xmldom/xmldom`)
4
+ *
5
+ * @default globalThis.DOMParser
6
+ */
7
+ DOMParser?: any;
8
+ /**
9
+ * in `xml1` plists, uids are serialized as a `<dict>` with a single integer key `CF$UID`.
10
+ * by default, we keep them as is, but you can opt into parsing them into a `PlistValue<'uid'>` instead.
11
+ *
12
+ * @default false
13
+ */
14
+ parseUid?: boolean;
15
+ }): unknown;
@@ -0,0 +1,15 @@
1
+ export declare function readXmlPlist(data: string, params?: {
2
+ /**
3
+ * implementation of the DOMParser interface (e.g. `@xmldom/xmldom`)
4
+ *
5
+ * @default globalThis.DOMParser
6
+ */
7
+ DOMParser?: any;
8
+ /**
9
+ * in `xml1` plists, uids are serialized as a `<dict>` with a single integer key `CF$UID`.
10
+ * by default, we keep them as is, but you can opt into parsing them into a `PlistValue<'uid'>` instead.
11
+ *
12
+ * @default false
13
+ */
14
+ parseUid?: boolean;
15
+ }): unknown;
@@ -0,0 +1,79 @@
1
+ import { base64 } from "@fuman/utils";
2
+ import { PlistValue } from "./types.js";
3
+ function getNextElement(iter) {
4
+ while (true) {
5
+ const node = iter.next();
6
+ if (node.done) return null;
7
+ if (node.value.nodeType === 1) return node.value;
8
+ }
9
+ }
10
+ function readXmlPlist(data, params) {
11
+ const { DOMParser = globalThis.DOMParser, parseUid = false } = params ?? {};
12
+ const parser = new DOMParser();
13
+ const doc = parser.parseFromString(data, "text/xml");
14
+ const root = doc.getElementsByTagName("plist")[0];
15
+ if (!root) throw new Error("<plist> not found, invalid plist?");
16
+ const topObject = root.childNodes.filter((node) => node.nodeType === 1);
17
+ if (topObject.length !== 1) throw new Error("expected exactly one top object");
18
+ function parseObject(node) {
19
+ switch (node.tagName) {
20
+ case "dict": {
21
+ const dict = {};
22
+ const childrenIter = node.childNodes[Symbol.iterator]();
23
+ while (true) {
24
+ const key = getNextElement(childrenIter);
25
+ if (!key) break;
26
+ if (key.tagName !== "key") throw new Error(`expected <key>, got <${key.tagName}>`);
27
+ const keyText = key.textContent ?? "";
28
+ const value = getNextElement(childrenIter);
29
+ if (!value) throw new Error(`value for ${keyText} not found`);
30
+ dict[keyText] = parseObject(value);
31
+ }
32
+ if (parseUid && (typeof dict.CF$UID === "number" || typeof dict.CF$UID === "bigint") && Object.keys(dict).length === 1) {
33
+ return new PlistValue("uid", dict.CF$UID);
34
+ }
35
+ return dict;
36
+ }
37
+ case "array": {
38
+ const array = [];
39
+ const childrenIter = node.childNodes[Symbol.iterator]();
40
+ while (true) {
41
+ const value = getNextElement(childrenIter);
42
+ if (!value) break;
43
+ array.push(parseObject(value));
44
+ }
45
+ return array;
46
+ }
47
+ case "string":
48
+ return node.textContent ?? "";
49
+ case "data":
50
+ return base64.decode(node.textContent?.trim() ?? "");
51
+ case "integer": {
52
+ const value = node.textContent?.trim() ?? "";
53
+ const numValue = Number(value);
54
+ if (Number.isNaN(numValue)) throw new Error(`invalid integer: ${value}`);
55
+ if (numValue < Number.MIN_SAFE_INTEGER || numValue > Number.MAX_SAFE_INTEGER) {
56
+ return BigInt(value);
57
+ }
58
+ return numValue;
59
+ }
60
+ case "date": {
61
+ const value = new Date(node.textContent?.trim() ?? "");
62
+ if (Number.isNaN(value.getTime())) throw new Error(`invalid date: ${node.textContent}`);
63
+ return value;
64
+ }
65
+ case "false":
66
+ return false;
67
+ case "true":
68
+ return true;
69
+ case "real":
70
+ return Number(node.textContent?.trim() ?? "");
71
+ default:
72
+ throw new Error(`unexpected tag: <${node.tagName}>`);
73
+ }
74
+ }
75
+ return parseObject(topObject[0]);
76
+ }
77
+ export {
78
+ readXmlPlist
79
+ };
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const utils = require("@fuman/utils");
4
+ const types = require("./types.cjs");
5
+ function escapeXml(str) {
6
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7
+ }
8
+ function writeXmlPlist(data, options) {
9
+ const {
10
+ indent: indentStr = " ",
11
+ lineBreak: lineBreakStr = "\n",
12
+ wrapDataAt = 0,
13
+ collapseEmpty = true
14
+ } = options ?? {};
15
+ const lines = [
16
+ '<?xml version="1.0" encoding="UTF-8"?>',
17
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
18
+ '<plist version="1.0">'
19
+ ];
20
+ let indent = "";
21
+ const pushIndent = () => {
22
+ indent += indentStr;
23
+ };
24
+ const popIndent = () => {
25
+ indent = indent.slice(0, -indentStr.length);
26
+ };
27
+ const pushLine = (line) => {
28
+ lines.push(indent + line);
29
+ };
30
+ function writeObject(object) {
31
+ switch (typeof object) {
32
+ case "object": {
33
+ if (object === null) {
34
+ pushLine("<null/>");
35
+ } else if (Array.isArray(object)) {
36
+ if (collapseEmpty && object.length === 0) {
37
+ pushLine("<array/>");
38
+ } else {
39
+ pushLine("<array>");
40
+ pushIndent();
41
+ for (const value of object) {
42
+ writeObject(value);
43
+ }
44
+ popIndent();
45
+ pushLine("</array>");
46
+ }
47
+ } else if (object instanceof Date) {
48
+ pushLine(`<date>${object.toISOString().replace(/\.\d+(Z|\+)$/, "$1")}</date>`);
49
+ } else if (object instanceof types.PlistValue) {
50
+ switch (object.type) {
51
+ case "float32":
52
+ case "float64":
53
+ pushLine(`<real>${object.value}</real>`);
54
+ break;
55
+ case "int":
56
+ pushLine(`<integer>${object.value}</integer>`);
57
+ break;
58
+ case "uid":
59
+ writeObject({ CF$UID: object.value });
60
+ break;
61
+ case "ascii":
62
+ case "utf16":
63
+ case "utf8":
64
+ pushLine(`<string>${escapeXml(object.value)}</string>`);
65
+ break;
66
+ default:
67
+ throw new Error(`unexpected type: ${object.type}`);
68
+ }
69
+ } else if (object instanceof Uint8Array) {
70
+ const b64 = utils.base64.encode(object);
71
+ if (wrapDataAt === 0) {
72
+ pushLine(`<data>${b64}</data>`);
73
+ } else {
74
+ const wrapLength = wrapDataAt - indent.replace(/\t/g, " ").length;
75
+ pushLine("<data>");
76
+ for (let i = 0; i < b64.length; i += wrapLength) {
77
+ pushLine(b64.slice(i, i + wrapLength));
78
+ }
79
+ pushLine("</data>");
80
+ }
81
+ } else {
82
+ const keys = Object.keys(object);
83
+ if (collapseEmpty && keys.length === 0) {
84
+ pushLine("<dict/>");
85
+ } else {
86
+ pushLine("<dict>");
87
+ pushIndent();
88
+ for (const key of keys) {
89
+ pushLine(`<key>${escapeXml(key)}</key>`);
90
+ writeObject(object[key]);
91
+ }
92
+ popIndent();
93
+ pushLine("</dict>");
94
+ }
95
+ }
96
+ break;
97
+ }
98
+ case "string":
99
+ pushLine(`<string>${escapeXml(object)}</string>`);
100
+ break;
101
+ case "number": {
102
+ const element = Number.isInteger(object) ? "integer" : "real";
103
+ pushLine(`<${element}>${object}</${element}>`);
104
+ break;
105
+ }
106
+ case "bigint":
107
+ pushLine(`<integer>${object.toString()}</integer>`);
108
+ break;
109
+ case "boolean":
110
+ pushLine(`<${object ? "true" : "false"}/>`);
111
+ break;
112
+ default:
113
+ throw new Error(`unexpected type: ${typeof object}`);
114
+ }
115
+ }
116
+ writeObject(data);
117
+ lines.push("</plist>");
118
+ return lines.join(lineBreakStr);
119
+ }
120
+ exports.writeXmlPlist = writeXmlPlist;
@@ -0,0 +1,27 @@
1
+ export declare function writeXmlPlist(data: unknown, options?: {
2
+ /**
3
+ * string to use for indentation
4
+ *
5
+ * @default '\t'
6
+ */
7
+ indent?: string;
8
+ /**
9
+ * string to use for line breaks
10
+ *
11
+ * @default '\n'
12
+ */
13
+ lineBreak?: string;
14
+ /**
15
+ * some implementations wrap `<data>` base64 strings at a certain length,
16
+ * this option allows you to set that length
17
+ *
18
+ * @default 0 (no wrapping)
19
+ */
20
+ wrapDataAt?: number;
21
+ /**
22
+ * whether we should collapse empty tags (e.g. `<dict/>`, <array/>`)
23
+ *
24
+ * @default true
25
+ */
26
+ collapseEmpty?: boolean;
27
+ }): string;
@@ -0,0 +1,27 @@
1
+ export declare function writeXmlPlist(data: unknown, options?: {
2
+ /**
3
+ * string to use for indentation
4
+ *
5
+ * @default '\t'
6
+ */
7
+ indent?: string;
8
+ /**
9
+ * string to use for line breaks
10
+ *
11
+ * @default '\n'
12
+ */
13
+ lineBreak?: string;
14
+ /**
15
+ * some implementations wrap `<data>` base64 strings at a certain length,
16
+ * this option allows you to set that length
17
+ *
18
+ * @default 0 (no wrapping)
19
+ */
20
+ wrapDataAt?: number;
21
+ /**
22
+ * whether we should collapse empty tags (e.g. `<dict/>`, <array/>`)
23
+ *
24
+ * @default true
25
+ */
26
+ collapseEmpty?: boolean;
27
+ }): string;
@@ -0,0 +1,120 @@
1
+ import { base64 } from "@fuman/utils";
2
+ import { PlistValue } from "./types.js";
3
+ function escapeXml(str) {
4
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5
+ }
6
+ function writeXmlPlist(data, options) {
7
+ const {
8
+ indent: indentStr = " ",
9
+ lineBreak: lineBreakStr = "\n",
10
+ wrapDataAt = 0,
11
+ collapseEmpty = true
12
+ } = options ?? {};
13
+ const lines = [
14
+ '<?xml version="1.0" encoding="UTF-8"?>',
15
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
16
+ '<plist version="1.0">'
17
+ ];
18
+ let indent = "";
19
+ const pushIndent = () => {
20
+ indent += indentStr;
21
+ };
22
+ const popIndent = () => {
23
+ indent = indent.slice(0, -indentStr.length);
24
+ };
25
+ const pushLine = (line) => {
26
+ lines.push(indent + line);
27
+ };
28
+ function writeObject(object) {
29
+ switch (typeof object) {
30
+ case "object": {
31
+ if (object === null) {
32
+ pushLine("<null/>");
33
+ } else if (Array.isArray(object)) {
34
+ if (collapseEmpty && object.length === 0) {
35
+ pushLine("<array/>");
36
+ } else {
37
+ pushLine("<array>");
38
+ pushIndent();
39
+ for (const value of object) {
40
+ writeObject(value);
41
+ }
42
+ popIndent();
43
+ pushLine("</array>");
44
+ }
45
+ } else if (object instanceof Date) {
46
+ pushLine(`<date>${object.toISOString().replace(/\.\d+(Z|\+)$/, "$1")}</date>`);
47
+ } else if (object instanceof PlistValue) {
48
+ switch (object.type) {
49
+ case "float32":
50
+ case "float64":
51
+ pushLine(`<real>${object.value}</real>`);
52
+ break;
53
+ case "int":
54
+ pushLine(`<integer>${object.value}</integer>`);
55
+ break;
56
+ case "uid":
57
+ writeObject({ CF$UID: object.value });
58
+ break;
59
+ case "ascii":
60
+ case "utf16":
61
+ case "utf8":
62
+ pushLine(`<string>${escapeXml(object.value)}</string>`);
63
+ break;
64
+ default:
65
+ throw new Error(`unexpected type: ${object.type}`);
66
+ }
67
+ } else if (object instanceof Uint8Array) {
68
+ const b64 = base64.encode(object);
69
+ if (wrapDataAt === 0) {
70
+ pushLine(`<data>${b64}</data>`);
71
+ } else {
72
+ const wrapLength = wrapDataAt - indent.replace(/\t/g, " ").length;
73
+ pushLine("<data>");
74
+ for (let i = 0; i < b64.length; i += wrapLength) {
75
+ pushLine(b64.slice(i, i + wrapLength));
76
+ }
77
+ pushLine("</data>");
78
+ }
79
+ } else {
80
+ const keys = Object.keys(object);
81
+ if (collapseEmpty && keys.length === 0) {
82
+ pushLine("<dict/>");
83
+ } else {
84
+ pushLine("<dict>");
85
+ pushIndent();
86
+ for (const key of keys) {
87
+ pushLine(`<key>${escapeXml(key)}</key>`);
88
+ writeObject(object[key]);
89
+ }
90
+ popIndent();
91
+ pushLine("</dict>");
92
+ }
93
+ }
94
+ break;
95
+ }
96
+ case "string":
97
+ pushLine(`<string>${escapeXml(object)}</string>`);
98
+ break;
99
+ case "number": {
100
+ const element = Number.isInteger(object) ? "integer" : "real";
101
+ pushLine(`<${element}>${object}</${element}>`);
102
+ break;
103
+ }
104
+ case "bigint":
105
+ pushLine(`<integer>${object.toString()}</integer>`);
106
+ break;
107
+ case "boolean":
108
+ pushLine(`<${object ? "true" : "false"}/>`);
109
+ break;
110
+ default:
111
+ throw new Error(`unexpected type: ${typeof object}`);
112
+ }
113
+ }
114
+ writeObject(data);
115
+ lines.push("</plist>");
116
+ return lines.join(lineBreakStr);
117
+ }
118
+ export {
119
+ writeXmlPlist
120
+ };
package/types.cjs ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ class PlistValue {
4
+ constructor(type, value) {
5
+ this.type = type;
6
+ this.value = value;
7
+ }
8
+ valueOf() {
9
+ return this.value;
10
+ }
11
+ toString() {
12
+ return String(this.value);
13
+ }
14
+ }
15
+ class KeyedArchiverValue {
16
+ constructor(header, value) {
17
+ this.header = header;
18
+ this.value = value;
19
+ }
20
+ valueOf() {
21
+ return this.value;
22
+ }
23
+ }
24
+ exports.KeyedArchiverValue = KeyedArchiverValue;
25
+ exports.PlistValue = PlistValue;
package/types.d.cts ADDED
@@ -0,0 +1,27 @@
1
+ export interface PlistValueType {
2
+ float32: number;
3
+ float64: number;
4
+ int: number | bigint;
5
+ uid: number | bigint;
6
+ ascii: string;
7
+ utf16: string;
8
+ utf8: string;
9
+ }
10
+ export declare class PlistValue<Type extends keyof PlistValueType = keyof PlistValueType> {
11
+ readonly type: Type;
12
+ readonly value: PlistValueType[Type];
13
+ constructor(type: Type, value: PlistValueType[Type]);
14
+ valueOf(): PlistValueType[Type];
15
+ toString(): string;
16
+ }
17
+ export interface KeyedArchiverValueHeader {
18
+ $classname: string;
19
+ $classes: string[];
20
+ $classhints?: string[];
21
+ }
22
+ export declare class KeyedArchiverValue {
23
+ readonly header: KeyedArchiverValueHeader;
24
+ readonly value: unknown;
25
+ constructor(header: KeyedArchiverValueHeader, value: unknown);
26
+ valueOf(): unknown;
27
+ }
package/types.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ export interface PlistValueType {
2
+ float32: number;
3
+ float64: number;
4
+ int: number | bigint;
5
+ uid: number | bigint;
6
+ ascii: string;
7
+ utf16: string;
8
+ utf8: string;
9
+ }
10
+ export declare class PlistValue<Type extends keyof PlistValueType = keyof PlistValueType> {
11
+ readonly type: Type;
12
+ readonly value: PlistValueType[Type];
13
+ constructor(type: Type, value: PlistValueType[Type]);
14
+ valueOf(): PlistValueType[Type];
15
+ toString(): string;
16
+ }
17
+ export interface KeyedArchiverValueHeader {
18
+ $classname: string;
19
+ $classes: string[];
20
+ $classhints?: string[];
21
+ }
22
+ export declare class KeyedArchiverValue {
23
+ readonly header: KeyedArchiverValueHeader;
24
+ readonly value: unknown;
25
+ constructor(header: KeyedArchiverValueHeader, value: unknown);
26
+ valueOf(): unknown;
27
+ }
package/types.js ADDED
@@ -0,0 +1,25 @@
1
+ class PlistValue {
2
+ constructor(type, value) {
3
+ this.type = type;
4
+ this.value = value;
5
+ }
6
+ valueOf() {
7
+ return this.value;
8
+ }
9
+ toString() {
10
+ return String(this.value);
11
+ }
12
+ }
13
+ class KeyedArchiverValue {
14
+ constructor(header, value) {
15
+ this.header = header;
16
+ this.value = value;
17
+ }
18
+ valueOf() {
19
+ return this.value;
20
+ }
21
+ }
22
+ export {
23
+ KeyedArchiverValue,
24
+ PlistValue
25
+ };