mirror of
https://github.com/ChiChou/entdb.git
synced 2026-06-10 23:07:47 +02:00
Add version history and side-by-side diff for binary entitlements
- Show all OS versions where the same binary path exists - Add side-by-side diff view comparing entitlements between versions - Normalize plist by sorting root-level keys before comparison - Add plist utilities for parsing and key extraction
This commit is contained in:
+102
-25
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { redirect, useSearchParams } from "next/navigation";
|
||||
import {
|
||||
createElement,
|
||||
@@ -9,36 +9,18 @@ import {
|
||||
import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { CopyButton } from "@/components/copy-button";
|
||||
import { DiffViewer } from "@/components/diff-viewer";
|
||||
|
||||
import { addBasePath } from "@/lib/env";
|
||||
import { createEngine } from "@/lib/engine";
|
||||
|
||||
function prettifyXml(src: string) {
|
||||
const xmlDoc = new DOMParser().parseFromString(src, "application/xml");
|
||||
const xsltDoc = new DOMParser().parseFromString(
|
||||
`<xsl:stylesheet version="1.0"
|
||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output omit-xml-declaration="yes" indent="yes"/>
|
||||
<xsl:template match="node()|@*">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates select="node()|@*"/>
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`,
|
||||
"application/xml",
|
||||
);
|
||||
|
||||
const xsltProcessor = new XSLTProcessor();
|
||||
xsltProcessor.importStylesheet(xsltDoc);
|
||||
const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
|
||||
const resultXml = new XMLSerializer().serializeToString(resultDoc);
|
||||
return resultXml;
|
||||
}
|
||||
import type { PathHistory } from "@/lib/engine/types";
|
||||
import { normalizePlist, prettifyXml } from "@/lib/plist";
|
||||
|
||||
export default function BinaryDetail() {
|
||||
const params = useSearchParams();
|
||||
const os = params.get("os");
|
||||
const path = params.get("path");
|
||||
const compareWith = params.get("compare");
|
||||
|
||||
const [group, build] = os ? os.split("/") : ["", ""];
|
||||
|
||||
@@ -55,6 +37,9 @@ export default function BinaryDetail() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [xml, setXML] = useState<string>("");
|
||||
const [xmlKeys, setXMLKeys] = useState<Set<string>>(new Set());
|
||||
const [history, setHistory] = useState<PathHistory[]>([]);
|
||||
const [compareXml, setCompareXml] = useState<string>("");
|
||||
const [compareLoading, setCompareLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
@@ -74,12 +59,43 @@ export default function BinaryDetail() {
|
||||
} catch {
|
||||
setXML(rawXml);
|
||||
}
|
||||
|
||||
const hist = await engine.getPathHistory(path!);
|
||||
setHistory(hist);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
load().finally(() => setLoading(false));
|
||||
}, [group, build, path]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compareWith || !group) return;
|
||||
|
||||
async function loadCompare() {
|
||||
const engine = await createEngine(group);
|
||||
const rawXml = await engine.getBinaryXML(compareWith!, path!);
|
||||
const prettified = prettifyXml(rawXml);
|
||||
setCompareXml(prettified);
|
||||
}
|
||||
|
||||
setCompareLoading(true);
|
||||
loadCompare().finally(() => setCompareLoading(false));
|
||||
}, [group, compareWith, path]);
|
||||
|
||||
const normalizedXml = useMemo(
|
||||
() => (xml ? normalizePlist(xml) : ""),
|
||||
[xml],
|
||||
);
|
||||
const normalizedCompareXml = useMemo(
|
||||
() => (compareXml ? normalizePlist(compareXml) : ""),
|
||||
[compareXml],
|
||||
);
|
||||
|
||||
const availableHistory = history.filter((h) => h.available);
|
||||
const currentOs = history.find(
|
||||
(h) => h.os.build === build || `${h.os.version}_${h.os.build}` === build,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<main className="space-y-6">
|
||||
@@ -87,7 +103,7 @@ export default function BinaryDetail() {
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Entitlements of</h2>
|
||||
<p>
|
||||
<code className="text-red-800 break-all font-thin text-sm">
|
||||
<code className="text-red-800 dark:text-red-400 break-all font-thin text-sm">
|
||||
{path}
|
||||
</code>
|
||||
</p>
|
||||
@@ -95,8 +111,67 @@ export default function BinaryDetail() {
|
||||
{!loading && xml && <CopyButton text={xml} />}
|
||||
</div>
|
||||
|
||||
{availableHistory.length > 1 && (
|
||||
<div className="border rounded-lg p-4 bg-gray-50 dark:bg-gray-900">
|
||||
<h3 className="font-semibold mb-2">Version History</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
This binary exists in {availableHistory.length} OS versions.
|
||||
Select a version to compare:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableHistory.map((h) => {
|
||||
const isCurrent =
|
||||
h.os.build === build ||
|
||||
`${h.os.version}_${h.os.build}` === build;
|
||||
const isComparing = compareWith === `${h.os.version}_${h.os.build}`;
|
||||
const versionTag = `${h.os.version}_${h.os.build}`;
|
||||
|
||||
if (isCurrent) {
|
||||
return (
|
||||
<span
|
||||
key={h.os.build}
|
||||
className="px-2 py-1 text-sm rounded bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 font-medium"
|
||||
>
|
||||
{h.os.version} (current)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const href = isComparing
|
||||
? addBasePath(`/os/bin?os=${encodeURIComponent(os!)}&path=${encodeURIComponent(path!)}`)
|
||||
: addBasePath(`/os/bin?os=${encodeURIComponent(os!)}&path=${encodeURIComponent(path!)}&compare=${encodeURIComponent(versionTag)}`);
|
||||
|
||||
return (
|
||||
<a
|
||||
key={h.os.build}
|
||||
href={href}
|
||||
className={`px-2 py-1 text-sm rounded transition-colors ${
|
||||
isComparing
|
||||
? "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
|
||||
: "bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
}`}
|
||||
>
|
||||
{h.os.version}
|
||||
{isComparing && " (comparing)"}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p>Loading...</p>}
|
||||
{!loading && xml && (
|
||||
|
||||
{!loading && compareWith && !compareLoading && normalizedCompareXml && (
|
||||
<DiffViewer
|
||||
oldXml={normalizedCompareXml}
|
||||
newXml={normalizedXml}
|
||||
oldLabel={`${compareWith}`}
|
||||
newLabel={currentOs ? `${currentOs.os.version}_${currentOs.os.build}` : build}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading && !compareWith && xml && (
|
||||
<SyntaxHighlighter
|
||||
language="xml"
|
||||
showLineNumbers={true}
|
||||
@@ -155,6 +230,8 @@ export default function BinaryDetail() {
|
||||
{xml}
|
||||
</SyntaxHighlighter>
|
||||
)}
|
||||
|
||||
{compareLoading && <p>Loading comparison...</p>}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { diffPlistKeys, type PlistDiff } from "@/lib/plist";
|
||||
|
||||
interface DiffLine {
|
||||
type: "unchanged" | "added" | "removed" | "changed";
|
||||
oldLine?: string;
|
||||
newLine?: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
function computeLineDiff(oldLines: string[], newLines: string[]): DiffLine[] {
|
||||
const result: DiffLine[] = [];
|
||||
const oldSet = new Set(oldLines);
|
||||
const newSet = new Set(newLines);
|
||||
|
||||
const maxLen = Math.max(oldLines.length, newLines.length);
|
||||
|
||||
let oi = 0;
|
||||
let ni = 0;
|
||||
|
||||
while (oi < oldLines.length || ni < newLines.length) {
|
||||
const oldLine = oldLines[oi];
|
||||
const newLine = newLines[ni];
|
||||
|
||||
if (oldLine === newLine) {
|
||||
result.push({ type: "unchanged", oldLine, newLine });
|
||||
oi++;
|
||||
ni++;
|
||||
} else if (oldLine && !newSet.has(oldLine)) {
|
||||
result.push({ type: "removed", oldLine, newLine: undefined });
|
||||
oi++;
|
||||
} else if (newLine && !oldSet.has(newLine)) {
|
||||
result.push({ type: "added", oldLine: undefined, newLine });
|
||||
ni++;
|
||||
} else {
|
||||
result.push({ type: "changed", oldLine, newLine });
|
||||
oi++;
|
||||
ni++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
interface DiffViewerProps {
|
||||
oldXml: string;
|
||||
newXml: string;
|
||||
oldLabel: string;
|
||||
newLabel: string;
|
||||
}
|
||||
|
||||
export function DiffViewer({
|
||||
oldXml,
|
||||
newXml,
|
||||
oldLabel,
|
||||
newLabel,
|
||||
}: DiffViewerProps) {
|
||||
const diff = useMemo(() => diffPlistKeys(oldXml, newXml), [oldXml, newXml]);
|
||||
|
||||
const oldLines = useMemo(
|
||||
() => oldXml.split("\n").filter((l) => l.trim()),
|
||||
[oldXml],
|
||||
);
|
||||
const newLines = useMemo(
|
||||
() => newXml.split("\n").filter((l) => l.trim()),
|
||||
[newXml],
|
||||
);
|
||||
|
||||
const lineDiff = useMemo(
|
||||
() => computeLineDiff(oldLines, newLines),
|
||||
[oldLines, newLines],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<DiffSummary diff={diff} />
|
||||
<div className="grid grid-cols-2 gap-2 font-mono text-xs">
|
||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-t px-3 py-2 font-semibold border-b">
|
||||
{oldLabel}
|
||||
</div>
|
||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-t px-3 py-2 font-semibold border-b">
|
||||
{newLabel}
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{lineDiff.map((line, i) => (
|
||||
<DiffLineRow key={i} line={line} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DiffLineRow({ line }: { line: DiffLine }) {
|
||||
const baseClasses = "px-3 py-0.5 font-mono text-xs whitespace-pre overflow-x-auto";
|
||||
|
||||
switch (line.type) {
|
||||
case "unchanged":
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClasses} bg-gray-50 dark:bg-gray-900`}>
|
||||
{line.oldLine}
|
||||
</div>
|
||||
<div className={`${baseClasses} bg-gray-50 dark:bg-gray-900`}>
|
||||
{line.newLine}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case "removed":
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClasses} bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200`}>
|
||||
{line.oldLine}
|
||||
</div>
|
||||
<div className={`${baseClasses} bg-gray-100 dark:bg-gray-800`} />
|
||||
</>
|
||||
);
|
||||
case "added":
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClasses} bg-gray-100 dark:bg-gray-800`} />
|
||||
<div className={`${baseClasses} bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200`}>
|
||||
{line.newLine}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case "changed":
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClasses} bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200`}>
|
||||
{line.oldLine}
|
||||
</div>
|
||||
<div className={`${baseClasses} bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200`}>
|
||||
{line.newLine}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function DiffSummary({ diff }: { diff: PlistDiff }) {
|
||||
const hasChanges =
|
||||
diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
|
||||
|
||||
if (!hasChanges) {
|
||||
return (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No changes in root-level keys
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
{diff.added.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded bg-green-500" />
|
||||
<span>
|
||||
<strong>{diff.added.length}</strong> added:{" "}
|
||||
<code className="text-xs">{diff.added.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{diff.removed.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded bg-red-500" />
|
||||
<span>
|
||||
<strong>{diff.removed.length}</strong> removed:{" "}
|
||||
<code className="text-xs">{diff.removed.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{diff.changed.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded bg-yellow-500" />
|
||||
<span>
|
||||
<strong>{diff.changed.length}</strong> changed:{" "}
|
||||
<code className="text-xs">{diff.changed.join(", ")}</code>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+31
-1
@@ -1,4 +1,4 @@
|
||||
import type { Engine } from "./types";
|
||||
import type { Engine, PathHistory } from "./types";
|
||||
import type { OS } from "@/lib/types";
|
||||
import { dataBaseURL } from "@/lib/env";
|
||||
import { fetchText, fetchLines } from "@/lib/client";
|
||||
@@ -42,6 +42,10 @@ class KVStore {
|
||||
});
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.#index.has(key);
|
||||
}
|
||||
|
||||
*keys(): IterableIterator<string> {
|
||||
yield* this.#index.keys();
|
||||
}
|
||||
@@ -93,6 +97,32 @@ export class KVEngine implements Engine {
|
||||
return lines.split("\n").filter(Boolean);
|
||||
}
|
||||
|
||||
async getPathHistory(path: string): Promise<PathHistory[]> {
|
||||
const osList = await this.listOS();
|
||||
const results: PathHistory[] = [];
|
||||
|
||||
const checks = osList.map(async (os) => {
|
||||
const tag = `${os.version}_${os.build}`;
|
||||
try {
|
||||
const reader = await this.openKV(`${this.#baseURL}/${tag}/blobs`);
|
||||
return { os, available: reader.has(path) };
|
||||
} catch {
|
||||
return { os, available: false };
|
||||
}
|
||||
});
|
||||
|
||||
const settled = await Promise.all(checks);
|
||||
return settled.sort((a, b) => {
|
||||
const vA = a.os.version.split(".").map(Number);
|
||||
const vB = b.os.version.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
|
||||
const diff = (vB[i] || 0) - (vA[i] || 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
#osCache: OS[] | null = null;
|
||||
|
||||
private async findOS(build: string): Promise<OS> {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { OS } from "@/lib/types";
|
||||
|
||||
export interface PathHistory {
|
||||
os: OS;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export interface Engine {
|
||||
listOS(): Promise<OS[]>;
|
||||
getPaths(build: string): Promise<string[]>;
|
||||
getBinaryXML(build: string, path: string): Promise<string>;
|
||||
getKeys(build: string): Promise<string[]>;
|
||||
getPathsForKey(build: string, key: string): Promise<string[]>;
|
||||
getPathHistory(path: string): Promise<PathHistory[]>;
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,4 +1,4 @@
|
||||
import type { Engine } from "./types";
|
||||
import type { Engine, PathHistory } from "./types";
|
||||
import type { OS } from "@/lib/types";
|
||||
import { dataBaseURL } from "@/lib/env";
|
||||
|
||||
@@ -197,4 +197,23 @@ export class WASMEngine implements Engine {
|
||||
});
|
||||
return rows.map((row) => row[0] as string);
|
||||
}
|
||||
|
||||
async getPathHistory(path: string): Promise<PathHistory[]> {
|
||||
const db = await getDB();
|
||||
const osList = await this.listOS();
|
||||
|
||||
const rows = db.exec({
|
||||
sql: `SELECT DISTINCT os.build FROM bin JOIN os ON bin.osid=os.id WHERE bin.path=?`,
|
||||
bind: [path],
|
||||
rowMode: "array",
|
||||
returnValue: "resultRows",
|
||||
});
|
||||
|
||||
const availableBuilds = new Set(rows.map((row) => row[0] as string));
|
||||
|
||||
return osList.map((os) => ({
|
||||
os,
|
||||
available: availableBuilds.has(os.build),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
export function parsePlist(xml: string): Document {
|
||||
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 rootDict = doc.querySelector("plist > dict");
|
||||
if (!rootDict) return xml;
|
||||
|
||||
const entries: PlistEntry[] = [];
|
||||
const children = Array.from(rootDict.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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
const lines = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<plist version="1.0">',
|
||||
"<dict>",
|
||||
...entries.map((e) => `<key>${e.key}</key>\n${e.value}`),
|
||||
"</dict>",
|
||||
"</plist>",
|
||||
];
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export interface PlistDiff {
|
||||
added: string[];
|
||||
removed: string[];
|
||||
changed: string[];
|
||||
unchanged: string[];
|
||||
}
|
||||
|
||||
export function diffPlistKeys(oldXml: string, newXml: string): PlistDiff {
|
||||
const oldKeys = extractRootKeys(oldXml);
|
||||
const newKeys = extractRootKeys(newXml);
|
||||
|
||||
const oldDoc = parsePlist(oldXml);
|
||||
const newDoc = parsePlist(newXml);
|
||||
|
||||
const added: string[] = [];
|
||||
const removed: string[] = [];
|
||||
const changed: string[] = [];
|
||||
const unchanged: string[] = [];
|
||||
|
||||
const allKeys = new Set([...oldKeys, ...newKeys]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
const inOld = oldKeys.has(key);
|
||||
const inNew = newKeys.has(key);
|
||||
|
||||
if (!inOld && inNew) {
|
||||
added.push(key);
|
||||
} else if (inOld && !inNew) {
|
||||
removed.push(key);
|
||||
} else {
|
||||
const oldValue = getKeyValue(oldDoc, key);
|
||||
const newValue = getKeyValue(newDoc, key);
|
||||
if (oldValue === newValue) {
|
||||
unchanged.push(key);
|
||||
} else {
|
||||
changed.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, changed, unchanged };
|
||||
}
|
||||
|
||||
function extractRootKeys(xml: string): Set<string> {
|
||||
const doc = parsePlist(xml);
|
||||
const keys = new Set<string>();
|
||||
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(/<!DOCTYPE[^>]*>/i, "");
|
||||
|
||||
const xmlDoc = new DOMParser().parseFromString(cleanSrc, "application/xml");
|
||||
if (xmlDoc.querySelector("parsererror")) {
|
||||
return src;
|
||||
}
|
||||
|
||||
const xsltDoc = new DOMParser().parseFromString(
|
||||
`<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output omit-xml-declaration="yes" indent="yes"/>
|
||||
<xsl:template match="node()|@*">
|
||||
<xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`,
|
||||
"application/xml",
|
||||
);
|
||||
|
||||
try {
|
||||
const xsltProcessor = new XSLTProcessor();
|
||||
xsltProcessor.importStylesheet(xsltDoc);
|
||||
const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
|
||||
return new XMLSerializer().serializeToString(resultDoc);
|
||||
} catch {
|
||||
return src;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user