);
}
function DiffSummary({ diff }: { diff: PlistDiff }) {
- const hasChanges =
- diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
-
- if (!hasChanges) {
- return (
-
{diff.added.length > 0 && (
diff --git a/src/lib/plist.ts b/src/lib/plist.ts
index 94dd01e..8ff07fd 100644
--- a/src/lib/plist.ts
+++ b/src/lib/plist.ts
@@ -1,57 +1,112 @@
-export function parsePlist(xml: string): Document {
+type PlistValue =
+ | string
+ | number
+ | boolean
+ | PlistValue[]
+ | { [key: string]: PlistValue };
+
+export function plistToJson(xml: string): { [key: string]: PlistValue } {
const parser = new DOMParser();
- return parser.parseFromString(xml, "application/xml");
-}
-
-interface PlistEntry {
- key: string;
- value: string;
-}
-
-export function normalizePlist(xml: string): string {
- const doc = parsePlist(xml);
+ const doc = parser.parseFromString(xml, "application/xml");
const rootDict = doc.querySelector("plist > dict");
- if (!rootDict) return xml;
-
- const entries: PlistEntry[] = [];
- const children = Array.from(rootDict.children);
+ if (!rootDict) return {};
+ return parseDict(rootDict);
+}
+function parseDict(dict: Element): { [key: string]: PlistValue } {
+ const result: { [key: string]: PlistValue } = {};
+ const children = Array.from(dict.children);
for (let i = 0; i < children.length; i += 2) {
const keyEl = children[i];
const valueEl = children[i + 1];
if (keyEl?.tagName === "key" && valueEl) {
- entries.push({
- key: keyEl.textContent || "",
- value: new XMLSerializer().serializeToString(valueEl),
- });
+ result[keyEl.textContent || ""] = parseValue(valueEl);
}
}
+ return result;
+}
- entries.sort((a, b) => a.key.localeCompare(b.key));
+function parseValue(el: Element): PlistValue {
+ switch (el.tagName) {
+ case "string":
+ return el.textContent || "";
+ case "integer":
+ return parseInt(el.textContent || "0", 10);
+ case "real":
+ return parseFloat(el.textContent || "0");
+ case "true":
+ return true;
+ case "false":
+ return false;
+ case "array":
+ return Array.from(el.children).map(parseValue);
+ case "dict":
+ return parseDict(el);
+ case "data":
+ return el.textContent || "";
+ default:
+ return el.textContent || "";
+ }
+}
- const lines = [
- '',
- '
',
- " ",
- ...entries.map((e) => ` ${e.key}\n ${indentValue(e.value)}`),
- " ",
- "",
- ];
+export function jsonToPlistXml(
+ obj: { [key: string]: PlistValue },
+ indent = 0,
+): string {
+ const pad = " ".repeat(indent);
+ const keys = Object.keys(obj).sort();
+ const lines: string[] = [];
+
+ for (const key of keys) {
+ lines.push(`${pad}
${escapeXml(key)}`);
+ lines.push(valueToXml(obj[key], indent));
+ }
return lines.join("\n");
}
-function indentValue(xml: string): string {
- // Simple indentation for single-line values
- if (!xml.includes("\n") && !xml.includes("><")) {
- return xml;
+function valueToXml(val: PlistValue, indent: number): string {
+ const pad = " ".repeat(indent);
+
+ if (val === true) return `${pad}
`;
+ if (val === false) return `${pad}
`;
+ if (typeof val === "string") return `${pad}
${escapeXml(val)}`;
+ if (typeof val === "number") {
+ return Number.isInteger(val)
+ ? `${pad}
${val}`
+ : `${pad}
${val}`;
}
- // For complex values, add indentation after each closing >
- return xml
- .replace(/>\n <")
- .split("\n")
- .map((line, i) => (i === 0 ? line : " " + line))
- .join("\n");
+ if (Array.isArray(val)) {
+ if (val.length === 0) return `${pad}
`;
+ const items = val.map((v) => valueToXml(v, indent + 1)).join("\n");
+ return `${pad}
\n${items}\n${pad}`;
+ }
+ if (typeof val === "object") {
+ const inner = jsonToPlistXml(val, indent + 1);
+ if (!inner) return `${pad}
`;
+ return `${pad}
\n${inner}\n${pad}`;
+ }
+ return `${pad}
${escapeXml(String(val))}`;
+}
+
+function escapeXml(s: string): string {
+ return s
+ .replace(/&/g, "&")
+ .replace(//g, ">");
+}
+
+export function normalizePlist(xml: string): string {
+ const json = plistToJson(xml);
+ const body = jsonToPlistXml(json, 2);
+ return [
+ '',
+ '
',
+ " ",
+ body,
+ " ",
+ "",
+ ].join("\n");
}
export interface PlistDiff {
@@ -62,11 +117,11 @@ export interface PlistDiff {
}
export function diffPlistKeys(oldXml: string, newXml: string): PlistDiff {
- const oldKeys = extractRootKeys(oldXml);
- const newKeys = extractRootKeys(newXml);
+ const oldJson = plistToJson(oldXml);
+ const newJson = plistToJson(newXml);
- const oldDoc = parsePlist(oldXml);
- const newDoc = parsePlist(newXml);
+ const oldKeys = new Set(Object.keys(oldJson));
+ const newKeys = new Set(Object.keys(newJson));
const added: string[] = [];
const removed: string[] = [];
@@ -84,9 +139,7 @@ export function diffPlistKeys(oldXml: string, newXml: string): PlistDiff {
} else if (inOld && !inNew) {
removed.push(key);
} else {
- const oldValue = getKeyValue(oldDoc, key);
- const newValue = getKeyValue(newDoc, key);
- if (oldValue === newValue) {
+ if (JSON.stringify(oldJson[key]) === JSON.stringify(newJson[key])) {
unchanged.push(key);
} else {
changed.push(key);
@@ -96,61 +149,3 @@ export function diffPlistKeys(oldXml: string, newXml: string): PlistDiff {
return { added, removed, changed, unchanged };
}
-
-function extractRootKeys(xml: string): Set
{
- const doc = parsePlist(xml);
- const keys = new Set();
- const rootDict = doc.querySelector("plist > dict");
- if (!rootDict) return keys;
-
- const keyElements = rootDict.querySelectorAll(":scope > key");
- keyElements.forEach((el) => {
- if (el.textContent) keys.add(el.textContent);
- });
- return keys;
-}
-
-function getKeyValue(doc: Document, keyName: string): string {
- const rootDict = doc.querySelector("plist > dict");
- if (!rootDict) return "";
-
- const keys = rootDict.querySelectorAll(":scope > key");
- for (const key of keys) {
- if (key.textContent === keyName) {
- const value = key.nextElementSibling;
- if (value) {
- return value.outerHTML;
- }
- }
- }
- return "";
-}
-
-export function prettifyXml(src: string): string {
- // Remove DOCTYPE to avoid DTD loading issues
- const cleanSrc = src.replace(/]*>/i, "");
-
- const xmlDoc = new DOMParser().parseFromString(cleanSrc, "application/xml");
- if (xmlDoc.querySelector("parsererror")) {
- return src;
- }
-
- const xsltDoc = new DOMParser().parseFromString(
- `
-
-
-
-
- `,
- "application/xml",
- );
-
- try {
- const xsltProcessor = new XSLTProcessor();
- xsltProcessor.importStylesheet(xsltDoc);
- const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
- return new XMLSerializer().serializeToString(resultDoc);
- } catch {
- return src;
- }
-}