Xml Tree View
xml-tree-view ui Collapsible XML tree viewer with color-coded tags and attributes, click-to-copy, live search/filter, and expand/collapse-all controls. Parses XML strings and renders elements, text, comments, and CDATA with contained scrolling.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/xml-tree-view.json $ npx shadcn@latest add https://uipkge.dev/r/react/xml-tree-view.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/xml-tree-view.json $ bunx shadcn@latest add https://uipkge.dev/r/react/xml-tree-view.json Named registry:
npx shadcn@latest add @uipkge-react/xml-tree-view Installs to: components/ui/xml-tree-view/ Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
data Raw XML string to parse and display. | string | — | required |
expandDepth | number | — | optional |
maxDepth | number | — | optional |
showSearch | boolean | — | optional |
showToolbar | boolean | — | optional |
rootLabel Override path root label; defaults to the document element name. | string | — | optional |
onCopy | (value: string, path: string) => void | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
XmlAttr interface XmlAttr {
name: string
value: string
} XmlNode interface XmlNode {
type: XmlNodeType
/** Tag name for elements; empty for text / comment / cdata. */
name: string
attributes: XmlAttr[]
/** Character data for text / comment / cdata nodes. */
text: string
children: XmlNode[]
} ParseXmlResult interface ParseXmlResult {
root: XmlNode | null
error: string | null
} npm dependencies
Files installed (4)
-
components/ui/xml-tree-view/XmlTreeView.tsx 8.6 kB
import * as React from 'react' import { Search, CodeXml, FoldVertical, UnfoldVertical, AlertCircle } from 'lucide-react' import { cn } from '@/lib/utils' import { XmlTreeNode } from './XmlTreeNode' import { parseXml, countElements, pathKey, walkExpandable, type XmlNode } from './types' export type { XmlNode } from './types' export interface XmlTreeViewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'data' | 'onCopy'> { /** Raw XML string to parse and display. */ data: string expandDepth?: number maxDepth?: number showSearch?: boolean showToolbar?: boolean /** Override path root label; defaults to the document element name. */ rootLabel?: string onCopy?: (value: string, path: string) => void } const tagColor = 'text-violet-600 dark:text-violet-400' const attrNameColor = 'text-blue-600 dark:text-blue-400' const attrValueColor = 'text-emerald-600 dark:text-emerald-400' const textColor = 'text-emerald-600 dark:text-emerald-400' const commentColor = 'text-muted-foreground' const punctColor = 'text-muted-foreground' const XmlTreeView = React.forwardRef<HTMLDivElement, XmlTreeViewProps>(function XmlTreeView(props, ref) { const { data, expandDepth = 1, maxDepth = 100, showSearch = true, showToolbar = true, rootLabel, onCopy, className, ...rest } = props const [expanded, setExpanded] = React.useState<Set<string>>(() => new Set()) const [search, setSearch] = React.useState('') const [copiedPath, setCopiedPath] = React.useState<string | null>(null) const parsed = React.useMemo(() => parseXml(data), [data]) const root = parsed.root const parseError = parsed.error const expandDepthRef = React.useRef(expandDepth) expandDepthRef.current = expandDepth const maxDepthRef = React.useRef(maxDepth) maxDepthRef.current = maxDepth const rootRef = React.useRef(root) rootRef.current = root function defaultExpanded(): Set<string> { const next = new Set<string>() const r = rootRef.current if (!r) return next walkExpandable(r, [], 0, expandDepthRef.current, (path) => { next.add(pathKey(path)) }) return next } React.useEffect(() => { setExpanded(defaultExpanded()) // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, expandDepth]) function toggle(path: string[]) { const k = pathKey(path) setExpanded((prev) => { const next = new Set(prev) if (next.has(k)) next.delete(k) else next.add(k) return next }) } function isExpanded(path: string[]): boolean { return expanded.has(pathKey(path)) } function expandAll() { const next = new Set<string>() const r = rootRef.current if (r) { walkExpandable(r, [], 0, maxDepthRef.current, (path) => { next.add(pathKey(path)) }) } setExpanded(next) } function collapseAll() { setExpanded(new Set()) } function matchesSearch(node: XmlNode): boolean { if (!search) return true const term = search.toLowerCase() const walk = (n: XmlNode): boolean => { if (n.type === 'element') { if (n.name.toLowerCase().includes(term)) return true if (n.attributes.some((a) => a.name.toLowerCase().includes(term) || a.value.toLowerCase().includes(term))) return true return n.children.some(walk) } return n.text.toLowerCase().includes(term) } return walk(node) } React.useEffect(() => { if (!search) { setExpanded(defaultExpanded()) return } const next = new Set<string>() const r = rootRef.current if (r) { walkExpandable(r, [], 0, maxDepthRef.current, (path, node) => { if (matchesSearch(node)) next.add(pathKey(path)) }) } setExpanded(next) // eslint-disable-next-line react-hooks/exhaustive-deps }, [search]) async function copyValue(value: string, path: string[]) { const p = pathKey(path) try { await navigator.clipboard.writeText(value) setCopiedPath(p) onCopy?.(value, p) setTimeout(() => { setCopiedPath((cur) => (cur === p ? null : cur)) }, 1200) } catch { // clipboard unavailable } } const effectiveRootLabel = rootLabel ?? root?.name ?? 'xml' const summary = React.useMemo(() => { if (parseError) return 'Parse error' if (!root) return 'Empty' const n = countElements(root) return `${root.name} · ${n} element${n === 1 ? '' : 's'}` }, [parseError, root]) const searchMatchCount = React.useMemo(() => { if (!search || !root) return 0 let count = 0 const term = search.toLowerCase() const walk = (n: XmlNode) => { if (n.type === 'element') { if (n.name.toLowerCase().includes(term)) count++ for (const a of n.attributes) { if (a.name.toLowerCase().includes(term) || a.value.toLowerCase().includes(term)) count++ } n.children.forEach(walk) return } if (n.text.toLowerCase().includes(term)) count++ } walk(root) return count }, [search, root]) return ( <div ref={ref} data-uipkge="" data-slot="xml-tree-view" className={cn('bg-background flex flex-col overflow-hidden rounded-lg border font-mono text-sm', className)} {...rest} > {(showToolbar || showSearch) && ( <div className="border-border flex items-center gap-2 border-b px-3 py-2"> <div className="flex items-center gap-1.5"> <CodeXml className="text-muted-foreground size-4" /> <span className="text-muted-foreground text-xs">{summary}</span> </div> <div className="ml-auto flex items-center gap-1"> {showSearch && !parseError && ( <div className="relative"> <Search className="text-muted-foreground absolute top-1/2 left-2 size-3.5 -translate-y-1/2" /> <input type="text" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Filter..." aria-label="Filter XML tree" className="border-input bg-muted/40 focus:border-ring focus:ring-ring/30 h-7 w-32 rounded-md pr-2 pl-7 text-xs transition-[width] outline-none focus:w-44 focus:ring-2" /> </div> )} {search && !parseError && ( <span className="text-muted-foreground text-xs"> {searchMatchCount} match{searchMatchCount === 1 ? '' : 'es'} </span> )} {!parseError && ( <> <button type="button" className="text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-7 items-center justify-center rounded-md transition-colors" title="Expand all" aria-label="Expand all" onClick={expandAll} > <UnfoldVertical className="size-4" /> </button> <button type="button" className="text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-7 items-center justify-center rounded-md transition-colors" title="Collapse all" aria-label="Collapse all" onClick={collapseAll} > <FoldVertical className="size-4" /> </button> </> )} </div> </div> )} {parseError ? ( <div className="text-destructive flex items-start gap-2 p-4 text-sm"> <AlertCircle className="mt-0.5 size-4 shrink-0" /> <div className="min-w-0"> <p className="font-sans font-medium">Invalid XML</p> <p className="text-muted-foreground mt-1 font-mono text-xs break-words">{parseError}</p> </div> </div> ) : root ? ( <div className="min-h-0 flex-1 overflow-auto p-2" role="tree" aria-label={effectiveRootLabel}> <XmlTreeNode node={root} path={[]} isRoot search={search} maxDepth={maxDepth} matchesSearch={matchesSearch} isExpanded={isExpanded} toggle={toggle} tagColor={tagColor} attrNameColor={attrNameColor} attrValueColor={attrValueColor} textColor={textColor} commentColor={commentColor} punctColor={punctColor} copiedPath={copiedPath} onCopy={copyValue} /> </div> ) : null} </div> ) }) XmlTreeView.displayName = 'XmlTreeView' export { XmlTreeView } -
components/ui/xml-tree-view/XmlTreeNode.tsx 11.4 kB
import * as React from 'react' import { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react' import type { XmlNode } from './types' import { isExpandable, serializeXml, pathKey, childEntries } from './types' export interface XmlTreeNodeProps { node: XmlNode path: string[] isRoot?: boolean search?: string maxDepth?: number matchesSearch: (node: XmlNode) => boolean isExpanded: (path: string[]) => boolean toggle: (path: string[]) => void tagColor: string attrNameColor: string attrValueColor: string textColor: string commentColor: string punctColor: string copiedPath?: string | null onCopy?: (value: string, path: string[]) => void } function XmlTreeNode(props: XmlTreeNodeProps): React.ReactElement { const { node, path, isRoot = false, search = '', maxDepth = 100, matchesSearch, isExpanded, toggle, tagColor, attrNameColor, attrValueColor, textColor, commentColor, punctColor, copiedPath = null, onCopy, } = props const key = pathKey(path) const open = isExpanded(path) const expandable = isExpandable(node) const dimmed = !!search && !matchesSearch(node) const indent = isRoot ? 0 : 20 const parentKey = path.length ? pathKey(path.slice(0, -1)) : null const entries = React.useMemo(() => childEntries(node, path), [node, path]) const childCount = node.children.filter((c) => c.type === 'element').length const collapsedPreview = React.useMemo(() => { if (open || !expandable) return '' const tags = node.children.filter((c) => c.type === 'element').slice(0, 3) const parts = tags.map((c) => `<${c.name}${c.attributes.length ? ' …' : ''}>`) const suffix = childCount > 3 ? ' …' : '' return parts.join(' ') + suffix }, [open, expandable, node.children, childCount]) const textOnlyChild = node.type === 'element' && node.children.length === 1 && node.children[0]!.type === 'text' ? node.children[0]!.text : null function handleCopy() { const value = node.type === 'element' ? serializeXml(node) : node.text onCopy?.(value, path) } function getTreeRows(from: HTMLElement): HTMLElement[] { const tree = from.closest('[role="tree"]') if (!tree) return [] return Array.from(tree.querySelectorAll<HTMLElement>('[data-tree-row]')) } function focusRow(row: HTMLElement | null | undefined) { row?.focus() } function handleRowKeydown(e: React.KeyboardEvent) { const target = e.currentTarget as HTMLElement if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() if (expandable) toggle(path) else handleCopy() return } if (e.key === 'ArrowRight') { e.preventDefault() if (expandable && !open) { toggle(path) } else if (expandable && open) { const rows = getTreeRows(target) const idx = rows.indexOf(target) if (idx >= 0 && idx < rows.length - 1) focusRow(rows[idx + 1]) } return } if (e.key === 'ArrowLeft') { e.preventDefault() if (expandable && open) { toggle(path) } else if (parentKey) { const tree = target.closest('[role="tree"]') const parent = tree?.querySelector<HTMLElement>(`[data-tree-row][data-tree-id="${CSS.escape(parentKey)}"]`) focusRow(parent) } return } if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault() const rows = getTreeRows(target) const idx = rows.indexOf(target) if (idx < 0) return focusRow(e.key === 'ArrowDown' ? rows[idx + 1] : rows[idx - 1]) return } if (e.key === 'Home') { e.preventDefault() focusRow(getTreeRows(target)[0]) return } if (e.key === 'End') { e.preventDefault() const rows = getTreeRows(target) focusRow(rows[rows.length - 1]) } } const copyBtn = ( <button type="button" className="text-muted-foreground hover:text-foreground focus-visible:ring-ring ml-auto inline-flex size-5 shrink-0 items-center justify-center rounded opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1" title="Copy" aria-label="Copy" tabIndex={-1} onClick={(e) => { e.stopPropagation() handleCopy() }} > {copiedPath === key ? <Check className="size-3 text-emerald-500" /> : <Copy className="size-3" />} </button> ) const attrs = node.attributes.map((attr) => ( <React.Fragment key={attr.name}> <span className="select-none"> </span> <span className={`${attrNameColor} select-none`}>{attr.name}</span> <span className={`${punctColor} select-none`}>=</span> <span className={`${attrValueColor} select-none`}>"{attr.value}"</span> </React.Fragment> )) return ( <div data-dimmed={dimmed ? '' : undefined} className={dimmed ? 'opacity-30' : ''} role="treeitem" aria-expanded={expandable ? open : undefined} > {node.type === 'element' && expandable && ( <> <div data-tree-row data-tree-id={key} data-tree-parent={parentKey ?? undefined} tabIndex={0} className="group hover:bg-accent/40 focus-visible:ring-ring/50 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none" style={{ paddingLeft: `${indent}px` }} onClick={() => toggle(path)} onKeyDown={handleRowKeydown} > <button type="button" className="text-muted-foreground hover:text-foreground hover:bg-accent inline-flex size-4 shrink-0 items-center justify-center rounded" aria-expanded={open} aria-label={open ? 'Collapse' : 'Expand'} tabIndex={-1} onClick={(e) => { e.stopPropagation() toggle(path) }} > {open ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />} </button> <span className={`${punctColor} select-none`}><</span> <span className={`${tagColor} select-none`}>{node.name}</span> {attrs} <span className={`${punctColor} select-none`}>></span> {open ? ( <span className="text-muted-foreground ml-0.5 text-xs"> {childCount} {childCount === 1 ? 'child' : 'children'} </span> ) : ( <span className="text-muted-foreground ml-1 truncate text-xs select-none">{collapsedPreview}</span> )} {copyBtn} </div> {open && ( <div role="group"> {entries.map((entry) => ( <XmlTreeNode key={entry.segment} node={entry.child} path={entry.path} isRoot={false} search={search} maxDepth={maxDepth} matchesSearch={matchesSearch} isExpanded={isExpanded} toggle={toggle} tagColor={tagColor} attrNameColor={attrNameColor} attrValueColor={attrValueColor} textColor={textColor} commentColor={commentColor} punctColor={punctColor} copiedPath={copiedPath} onCopy={onCopy} /> ))} <div className="flex items-center gap-0.5 py-0.5 select-none" style={{ paddingLeft: `${indent}px` }}> <span className="inline-flex size-4 shrink-0" /> <span className={punctColor}></</span> <span className={tagColor}>{node.name}</span> <span className={punctColor}>></span> </div> </div> )} </> )} {node.type === 'element' && !expandable && ( <div data-tree-row data-tree-id={key} data-tree-parent={parentKey ?? undefined} tabIndex={0} className="group hover:bg-accent/40 focus-visible:ring-ring/50 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none" style={{ paddingLeft: `${indent}px` }} onClick={handleCopy} onKeyDown={handleRowKeydown} > <span className="inline-flex size-4 shrink-0" /> <span className={`${punctColor} select-none`}><</span> <span className={`${tagColor} select-none`}>{node.name}</span> {attrs} {textOnlyChild !== null ? ( <> <span className={`${punctColor} select-none`}>></span> <span className={`${textColor} truncate`}>{textOnlyChild}</span> <span className={`${punctColor} shrink-0 select-none`}></</span> <span className={`${tagColor} shrink-0 select-none`}>{node.name}</span> <span className={`${punctColor} shrink-0 select-none`}>></span> </> ) : ( <span className={`${punctColor} select-none`}> /></span> )} {copyBtn} </div> )} {node.type === 'comment' && ( <div data-tree-row data-tree-id={key} data-tree-parent={parentKey ?? undefined} tabIndex={0} className="group hover:bg-accent/40 focus-visible:ring-ring/50 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none" style={{ paddingLeft: `${indent}px` }} onClick={handleCopy} onKeyDown={handleRowKeydown} > <span className="inline-flex size-4 shrink-0" /> <span className={`${commentColor} truncate italic select-none`}><!--{node.text}--></span> {copyBtn} </div> )} {node.type === 'cdata' && ( <div data-tree-row data-tree-id={key} data-tree-parent={parentKey ?? undefined} tabIndex={0} className="group hover:bg-accent/40 focus-visible:ring-ring/50 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none" style={{ paddingLeft: `${indent}px` }} onClick={handleCopy} onKeyDown={handleRowKeydown} > <span className="inline-flex size-4 shrink-0" /> <span className={`${punctColor} select-none`}><![CDATA[</span> <span className={`${textColor} truncate`}>{node.text}</span> <span className={`${punctColor} shrink-0 select-none`}>]]></span> {copyBtn} </div> )} {node.type === 'text' && ( <div data-tree-row data-tree-id={key} data-tree-parent={parentKey ?? undefined} tabIndex={0} className="group hover:bg-accent/40 focus-visible:ring-ring/50 -mx-1 flex items-center gap-0.5 rounded px-1 py-0.5 transition-colors focus-visible:ring-2 focus-visible:outline-none" style={{ paddingLeft: `${indent}px` }} onClick={handleCopy} onKeyDown={handleRowKeydown} > <span className="inline-flex size-4 shrink-0" /> <span className={`${textColor} truncate`}>{node.text}</span> {copyBtn} </div> )} </div> ) } export { XmlTreeNode } -
components/ui/xml-tree-view/types.ts 6.3 kB
export type XmlNodeType = 'element' | 'text' | 'comment' | 'cdata' export interface XmlAttr { name: string value: string } export interface XmlNode { type: XmlNodeType /** Tag name for elements; empty for text / comment / cdata. */ name: string attributes: XmlAttr[] /** Character data for text / comment / cdata nodes. */ text: string children: XmlNode[] } export interface ParseXmlResult { root: XmlNode | null error: string | null } /** * Convert a DOM node into a lightweight tree for rendering. * Whitespace-only text nodes are dropped so the tree stays readable. */ function domToNode(node: Node): XmlNode | null { if (node.nodeType === Node.ELEMENT_NODE) { const el = node as Element const attributes: XmlAttr[] = Array.from(el.attributes).map((a) => ({ name: a.name, value: a.value, })) const children: XmlNode[] = [] for (const child of Array.from(el.childNodes)) { const n = domToNode(child) if (n) children.push(n) } return { type: 'element', name: el.tagName, attributes, text: '', children, } } if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent ?? '' if (!text.trim()) return null return { type: 'text', name: '', attributes: [], text, children: [] } } if (node.nodeType === Node.COMMENT_NODE) { return { type: 'comment', name: '', attributes: [], text: node.textContent ?? '', children: [], } } if (node.nodeType === Node.CDATA_SECTION_NODE) { return { type: 'cdata', name: '', attributes: [], text: node.textContent ?? '', children: [], } } return null } /** * Parse an XML string with the browser DOMParser. * Returns a structured tree or a human-readable error. */ export function parseXml(source: string): ParseXmlResult { const trimmed = source?.trim() ?? '' if (!trimmed) return { root: null, error: 'Empty XML' } if (typeof DOMParser === 'undefined') { return { root: null, error: 'DOMParser is not available in this environment' } } try { const doc = new DOMParser().parseFromString(trimmed, 'application/xml') const parseError = doc.querySelector('parsererror') if (parseError) { const msg = parseError.textContent?.replace(/\s+/g, ' ').trim() || 'Invalid XML' return { root: null, error: msg } } const el = doc.documentElement if (!el) return { root: null, error: 'Empty document' } const root = domToNode(el) if (!root) return { root: null, error: 'Could not read document element' } return { root, error: null } } catch (e) { return { root: null, error: e instanceof Error ? e.message : 'Failed to parse XML' } } } /** Serialize a tree node (and descendants) back to XML. */ export function serializeXml(node: XmlNode, indent = 0): string { const pad = ' '.repeat(indent) if (node.type === 'text') return node.text if (node.type === 'comment') return `${pad}<!--${node.text}-->` if (node.type === 'cdata') return `${pad}<![CDATA[${node.text}]]>` const attrs = node.attributes.length > 0 ? ' ' + node.attributes.map((a) => `${a.name}="${escapeAttr(a.value)}"`).join(' ') : '' if (node.children.length === 0) { return `${pad}<${node.name}${attrs} />` } // Single text child → keep on one line if (node.children.length === 1 && node.children[0]!.type === 'text') { return `${pad}<${node.name}${attrs}>${escapeText(node.children[0]!.text)}</${node.name}>` } const inner = node.children.map((c) => serializeXml(c, indent + 1)).join('\n') return `${pad}<${node.name}${attrs}>\n${inner}\n${pad}</${node.name}>` } function escapeAttr(s: string): string { return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<') } function escapeText(s: string): string { return s.replace(/&/g, '&').replace(/</g, '<') } /** True when an element expands (has non-trivial children). */ export function isExpandable(node: XmlNode): boolean { if (node.type !== 'element') return false if (node.children.length === 0) return false // Single text child stays inline — no expand if (node.children.length === 1 && node.children[0]!.type === 'text') return false return true } /** Count element descendants (for summary). */ export function countElements(node: XmlNode): number { let n = node.type === 'element' ? 1 : 0 for (const c of node.children) n += countElements(c) return n } /** Path key for expand/collapse set. */ export function pathKey(path: string[]): string { return path.length ? '/' + path.join('/') : '/' } /** Walk expandable nodes with consistent sibling-indexed path segments. */ export function walkExpandable( node: XmlNode, path: string[], depth: number, max: number, visit: (path: string[], node: XmlNode) => void, ) { if (depth >= max) return if (isExpandable(node)) { visit(path, node) const counts = new Map<string, number>() const totals = new Map<string, number>() for (const c of node.children) { if (c.type === 'element') totals.set(c.name, (totals.get(c.name) ?? 0) + 1) } node.children.forEach((child, i) => { let segment: string if (child.type === 'element') { const n = (counts.get(child.name) ?? 0) + 1 counts.set(child.name, n) const total = totals.get(child.name) ?? 1 segment = total > 1 ? `${child.name}[${n}]` : child.name } else if (child.type === 'comment') { segment = `comment()[${i}]` } else { segment = `text()[${i}]` } walkExpandable(child, [...path, segment], depth + 1, max, visit) }) } } export function childEntries(node: XmlNode, path: string[]) { const counts = new Map<string, number>() const totals = new Map<string, number>() for (const c of node.children) { if (c.type === 'element') totals.set(c.name, (totals.get(c.name) ?? 0) + 1) } return node.children.map((child, i) => { let segment: string if (child.type === 'element') { const n = (counts.get(child.name) ?? 0) + 1 counts.set(child.name, n) const total = totals.get(child.name) ?? 1 segment = total > 1 ? `${child.name}[${n}]` : child.name } else if (child.type === 'comment') { segment = `comment()[${i}]` } else { segment = `text()[${i}]` } return { child, segment, path: [...path, segment] as string[] } }) } -
components/ui/xml-tree-view/index.ts 0.3 kB
export { XmlTreeView, type XmlTreeViewProps } from './XmlTreeView' export { XmlTreeNode, type XmlTreeNodeProps } from './XmlTreeNode' export type { XmlNode, XmlAttr, XmlNodeType, ParseXmlResult } from './types' export { parseXml, serializeXml, isExpandable, countElements } from './types'
Raw manifest: https://uipkge.dev/r/react/xml-tree-view.json