
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
xml-tree-viewuiCollapsible 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 ->$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.jsonnpx shadcn@latest add @uipkge-react/xml-tree-viewInstalls to:components/ui/xml-tree-view/| Name | Type / Values | Default | Required |
|---|---|---|---|
dataRaw XML string to parse and display. | string | — | required |
expandDepth | number | — | optional |
maxDepth | number | — | optional |
showSearch | boolean | — | optional |
showToolbar | boolean | — | optional |
rootLabelOverride path root label; defaults to the document element name. | string | — | optional |
onCopy | (value: string, path: string) => void | — | optional |
Type aliases from this item's source — use them to shape the data you pass in.
XmlAttrinterface XmlAttr {
name: string
value: string
}XmlNodeinterface 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[]
}ParseXmlResultinterface ParseXmlResult {
root: XmlNode | null
error: string | null
}'use client'
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 }
'use client'
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 }
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[] }
})
}
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