bruschetta

A simple golang web based file browser
Log | Files | Refs | LICENSE

commit 47ba205af44ff590f48935478dea74c436e1f040
parent 19b356d368a3e14b10d5c2d4f821137b175c9498
Author: breadcat <breadcat@users.noreply.github.com>
Date:   Thu, 19 Mar 2026 16:39:36 +0000

Just embed the whole damn template in the source

Diffstat:
Mmain.go | 264++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mreadme.md | 2--
Dtemplates/listing.html | 259-------------------------------------------------------------------------------
3 files changed, 262 insertions(+), 263 deletions(-)

diff --git a/main.go b/main.go @@ -49,6 +49,266 @@ type PageData struct { TreeJSON template.JS } +const tmpl = `<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>{{ .CurrentPath }}</title> +<style> +*,::after,::before{box-sizing:border-box;margin:0;padding:0} +a:hover{text-decoration:underline} +body,html{height:100%;overflow:hidden} +body{background:#1a1a1a;color:#d4d4d4;font-family:Consolas,Menlo,'DejaVu Sans Mono',monospace;font-size:13px;display:flex;flex-direction:column} +footer{padding:6px 16px;color:#777} +header{border-bottom:1px solid #333;padding:10px 16px;display:flex;align-items:center;gap:12px} +@media (max-width: 640px) { .sidebar { display: none; } .file-table td.date { display: none; } .file-table td.size { min-width: 50px; } .thumb-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; padding: 8px; } } +.breadcrumb .current{color:#777} +.breadcrumb .sep{color:#777;margin:0 2px} +.breadcrumb a,.file-table thead th:hover,.thumb-label a{color:#d4d4d4} +.breadcrumb a:hover,.thumb-label a:hover,a{color:#5b9bd5;text-decoration:none} +.breadcrumb,.tree-item,.view-toggle{display:flex;gap:4px} +.breadcrumb{align-items:center;flex-wrap:wrap;flex:1} +.empty{padding:48px;text-align:center;color:#777} +.file-icon.dir,.thumb-preview .dir-icon{color:#c8a94e} +.file-icon.img{color:#7cbf7c} +.file-icon{margin-right:6px;color:#777} +.file-table tbody tr,.file-table thead th{border-bottom:1px solid #333} +.file-table tbody tr:hover,.tree-item.active,.tree-item:hover{background:#2e2e2e} +.file-table td.date{color:#777;min-width:130px} +.file-table td.name{width:100%;white-space:normal;word-break:break-all} +.file-table td.size{color:#777;text-align:right;min-width:70px} +.file-table td{padding:7px 12px;white-space:nowrap} +.file-table thead th,.tree-item{cursor:pointer;user-select:none;white-space:nowrap} +.file-table thead th.sort-asc::after{content:' ↑';color:#5b9bd5} +.file-table thead th.sort-desc::after{content:' ↓';color:#5b9bd5} +.file-table thead th{background:#222;padding:8px 12px;text-align:left;font-weight:400;color:#777;font-size:11px;letter-spacing:.08em;text-transform:uppercase} +.file-table thead{position:sticky;top:0} +.file-table{width:100%;border-collapse:collapse} +.layout{display:flex;flex:1;overflow:hidden} +.main{flex:1;overflow-y:auto} +.sidebar,.thumb-item,footer,header{background:#222} +.sidebar{width:220px;min-width:220px;border-right:1px solid #333;overflow-y:auto;overflow-x:hidden;padding:8px 0;flex-shrink:0} +.sidebar-title{font-size:10px;letter-spacing:.1em;color:#777;padding:4px 12px 8px;text-transform:uppercase} +.thumb-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;padding:16px} +.thumb-item:hover{border-color:#5b9bd5} +.thumb-item{border:1px solid #333;overflow:hidden} +.thumb-label,footer{border-top:1px solid #333;font-size:11px} +.thumb-label{padding:6px 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.thumb-meta{font-size:10px;color:#777;padding:0 8px 6px} +.thumb-preview .big-icon{font-size:36px;color:#777} +.thumb-preview img{width:100%;height:100%;object-fit:cover} +.thumb-preview{width:100%;height:110px;display:flex;align-items:center;justify-content:center;background:#2a2a2a;overflow:hidden} +.tree-children.open{display:block} +.tree-children{display:none;padding-left:12px} +.tree-icon{color:#777;flex-shrink:0} +.tree-item.active{color:#5b9bd5} +.tree-item{align-items:center;padding:3px 8px;overflow:hidden;color:#d4d4d4;font-size:12px} +.tree-label{overflow:hidden;text-overflow:ellipsis} +.tree-root{list-style:none} +.tree-toggle{width:12px;text-align:center;color:#777;flex-shrink:0;font-size:10px} +.view-toggle button.active{background:#1a1a1a;color:#5b9bd5;border-color:#5b9bd5} +.view-toggle button:hover{background:#2e2e2e;color:#d4d4d4} +.view-toggle button{background:#2a2a2a;border:1px solid #333;color:#777;padding:4px 10px;cursor:pointer;font-size:12px;font-family:inherit} +.view-toggle{margin-left:auto} +</style> +</head> +<body> + +<header> + <span>&#128193;</span> + <div class="breadcrumb"> + {{ range $i, $crumb := .Breadcrumbs }} + {{ if gt $i 0 }}<span class="sep">/</span>{{ end }} + {{ if eq $i (sub1 (len $.Breadcrumbs)) }} + <span class="current">{{ $crumb.Name }}</span> + {{ else }} + <a href="{{ $crumb.Path }}">{{ $crumb.Name }}</a> + {{ end }} + {{ end }} + </div> + <div class="view-toggle"> + <button id="btn-list" class="active" onclick="setView('list')">&#9776; List</button> + <button id="btn-thumb" onclick="setView('thumb')">&#9638; Thumbs</button> + </div> +</header> + +<div class="layout"> + <nav class="sidebar"> + <div class="sidebar-title">Folders</div> + <ul class="tree-root" id="tree-root"></ul> + </nav> + + <main class="main"> + <div id="view-list"> + {{ if .Files }} + <table class="file-table" id="file-table"> + <thead> + <tr> + <th class="name" onclick="sortTable(0)">Name</th> + <th class="size" onclick="sortTable(1)">Size</th> + <th class="date" onclick="sortTable(2)">Modified</th> + </tr> + </thead> + <tbody> + {{ if ne .CurrentPath "/" }} + <tr> + <td class="name"><span class="file-icon dir">&#128193;</span><a href="../">../</a></td> + <td class="size">—</td> + <td class="date">—</td> + </tr> + {{ end }} + {{ range .Files }} + <tr data-name="{{ .Name }}" data-size="{{ .Size }}" data-date="{{ .ModTime.Unix }}"> + <td class="name"> + {{ if .IsDir }} + <span class="file-icon dir">&#128193;</span><a href="{{ .Path }}">{{ .Name }}/</a> + {{ else if isImage .Ext }} + <span class="file-icon img">&#128247;</span><a href="{{ .Path }}">{{ .Name }}</a> + {{ else }} + <span class="file-icon">&#128196;</span><a href="{{ .Path }}">{{ .Name }}</a> + {{ end }} + </td> + <td class="size">{{ if .IsDir }}—{{ else }}{{ formatSize .Size }}{{ end }}</td> + <td class="date">{{ formatTime .ModTime }}</td> + </tr> + {{ end }} + </tbody> + </table> + {{ else }} + <div class="empty">Empty directory</div> + {{ end }} + </div> + + <div id="view-thumb" style="display:none"> + {{ if .Files }} + <div class="thumb-grid"> + {{ if ne .CurrentPath "/" }} + <div class="thumb-item"> + <a href="../" style="display:block"> + <div class="thumb-preview"><span class="big-icon dir-icon">&#8593;</span></div> + <div class="thumb-label">../</div> + </a> + </div> + {{ end }} + {{ range .Files }} + <div class="thumb-item"> + <a href="{{ .Path }}" style="display:block"> + <div class="thumb-preview"> + {{ if .IsDir }} + <span class="big-icon dir-icon">&#128193;</span> + {{ else if isImage .Ext }} + <img src="/__thumb{{ .Path }}" alt="{{ .Name }}" loading="lazy"> + {{ else }} + <span class="big-icon">&#128196;</span> + {{ end }} + </div> + <div class="thumb-label" title="{{ .Name }}">{{ .Name }}{{ if .IsDir }}/{{ end }}</div> + <div class="thumb-meta">{{ if .IsDir }}folder{{ else }}{{ formatSize .Size }}{{ end }}</div> + </a> + </div> + {{ end }} + </div> + {{ else }} + <div class="empty">Empty directory</div> + {{ end }} + </div> + </main> +</div> + +<footer>{{ len .Files }} item(s) &nbsp;·&nbsp; {{ .CurrentPath }}</footer> + +<script> +const TREE_DATA = {{ .TreeJSON }}; +const CURRENT_PATH = {{ printf "%q" .CurrentPath }}; + +function setView(v) { + const isList = v === 'list'; + document.getElementById('view-list').style.display = isList ? '' : 'none'; + document.getElementById('view-thumb').style.display = isList ? 'none' : ''; + document.getElementById('btn-list').className = isList ? 'active' : ''; + document.getElementById('btn-thumb').className = isList ? '' : 'active'; +} + +let sortCol = -1, sortDir = 1; +function sortTable(col) { + const tbody = document.querySelector('#file-table tbody'); + const rows = Array.from(tbody.querySelectorAll('tr')); + const parentRow = rows.find(r => r.querySelector('a[href="../"]')); + const dataRows = rows.filter(r => r !== parentRow); + + if (sortCol === col) sortDir *= -1; + else { sortCol = col; sortDir = 1; } + + dataRows.sort((a, b) => { + let av, bv; + if (col === 0) { av = (a.dataset.name||'').toLowerCase(); bv = (b.dataset.name||'').toLowerCase(); } + else if (col === 1) { av = parseInt(a.dataset.size||0); bv = parseInt(b.dataset.size||0); } + else { av = parseInt(a.dataset.date||0); bv = parseInt(b.dataset.date||0); } + return av < bv ? -sortDir : av > bv ? sortDir : 0; + }); + + document.querySelectorAll('#file-table thead th').forEach((th, i) => { + th.className = th.className.replace(/ ?sort-(asc|desc)/g, ''); + if (i === col) th.className += sortDir > 0 ? ' sort-asc' : ' sort-desc'; + }); + + tbody.innerHTML = ''; + if (parentRow) tbody.appendChild(parentRow); + dataRows.forEach(r => tbody.appendChild(r)); +} + +function buildTree(nodes, container, depth) { + if (!nodes || !nodes.length) return; + nodes.forEach(node => { + const li = document.createElement('li'); + const item = document.createElement('div'); + item.className = 'tree-item' + (node.path === CURRENT_PATH ? ' active' : ''); + item.style.paddingLeft = (8 + depth * 12) + 'px'; + + const toggle = document.createElement('span'); + toggle.className = 'tree-toggle'; + toggle.textContent = (node.children && node.children.length) ? '▶' : ' '; + + const icon = document.createElement('span'); + icon.className = 'tree-icon'; + icon.textContent = '📁'; + + const label = document.createElement('span'); + label.className = 'tree-label'; + label.textContent = node.name; + + item.append(toggle, icon, label); + li.appendChild(item); + + let childUl = null; + if (node.children && node.children.length) { + childUl = document.createElement('ul'); + childUl.className = 'tree-children'; + buildTree(node.children, childUl, depth + 1); + li.appendChild(childUl); + toggle.addEventListener('click', e => { + e.stopPropagation(); + toggle.textContent = childUl.classList.toggle('open') ? '▼' : '▶'; + }); + } + + item.addEventListener('click', () => { window.location.href = node.path; }); + + if (CURRENT_PATH.startsWith(node.path) && childUl) { + childUl.classList.add('open'); + toggle.textContent = '▼'; + } + + container.appendChild(li); + }); +} + +buildTree(TREE_DATA, document.getElementById('tree-root'), 0); +</script> +</body> +</html>` + func main() { var dir string var port int @@ -166,14 +426,14 @@ func listingHandler(w http.ResponseWriter, r *http.Request) { "sub1": func(n int) int { return n - 1 }, } - tmpl, err := template.New("listing.html").Funcs(funcMap).ParseFiles("templates/listing.html") + t, err := template.New("page").Funcs(funcMap).Parse(tmpl) if err != nil { http.Error(w, "Template error: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") - if err := tmpl.Execute(w, data); err != nil { + if err := t.Execute(w, data); err != nil { log.Printf("Template error: %v", err) } } diff --git a/readme.md b/readme.md @@ -15,8 +15,6 @@ Then access the servers IP address via a web browser on port `8080`. The application is also buildable via your usual `go build -o bruschetta .`. -Note: the `templates/` directory must be alongside the binary at runtime. - ## Features * List and thumbnail view * Thumbnails generated on the fly and cached diff --git a/templates/listing.html b/templates/listing.html @@ -1,259 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="UTF-8"> -<meta name="viewport" content="width=device-width, initial-scale=1.0"> -<title>{{ .CurrentPath }}</title> -<style> -*,::after,::before{box-sizing:border-box;margin:0;padding:0} -a:hover{text-decoration:underline} -body,html{height:100%;overflow:hidden} -body{background:#1a1a1a;color:#d4d4d4;font-family:Consolas,Menlo,'DejaVu Sans Mono',monospace;font-size:13px;display:flex;flex-direction:column} -footer{padding:6px 16px;color:#777} -header{border-bottom:1px solid #333;padding:10px 16px;display:flex;align-items:center;gap:12px} -@media (max-width: 640px) { .sidebar { display: none; } .file-table td.date { display: none; } .file-table td.size { min-width: 50px; } .thumb-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; padding: 8px; } } -.breadcrumb .current{color:#777} -.breadcrumb .sep{color:#777;margin:0 2px} -.breadcrumb a,.file-table thead th:hover,.thumb-label a{color:#d4d4d4} -.breadcrumb a:hover,.thumb-label a:hover,a{color:#5b9bd5;text-decoration:none} -.breadcrumb,.tree-item,.view-toggle{display:flex;gap:4px} -.breadcrumb{align-items:center;flex-wrap:wrap;flex:1} -.empty{padding:48px;text-align:center;color:#777} -.file-icon.dir,.thumb-preview .dir-icon{color:#c8a94e} -.file-icon.img{color:#7cbf7c} -.file-icon{margin-right:6px;color:#777} -.file-table tbody tr,.file-table thead th{border-bottom:1px solid #333} -.file-table tbody tr:hover,.tree-item.active,.tree-item:hover{background:#2e2e2e} -.file-table td.date{color:#777;min-width:130px} -.file-table td.name{width:100%;white-space:normal;word-break:break-all} -.file-table td.size{color:#777;text-align:right;min-width:70px} -.file-table td{padding:7px 12px;white-space:nowrap} -.file-table thead th,.tree-item{cursor:pointer;user-select:none;white-space:nowrap} -.file-table thead th.sort-asc::after{content:' ↑';color:#5b9bd5} -.file-table thead th.sort-desc::after{content:' ↓';color:#5b9bd5} -.file-table thead th{background:#222;padding:8px 12px;text-align:left;font-weight:400;color:#777;font-size:11px;letter-spacing:.08em;text-transform:uppercase} -.file-table thead{position:sticky;top:0} -.file-table{width:100%;border-collapse:collapse} -.layout{display:flex;flex:1;overflow:hidden} -.main{flex:1;overflow-y:auto} -.sidebar,.thumb-item,footer,header{background:#222} -.sidebar{width:220px;min-width:220px;border-right:1px solid #333;overflow-y:auto;overflow-x:hidden;padding:8px 0;flex-shrink:0} -.sidebar-title{font-size:10px;letter-spacing:.1em;color:#777;padding:4px 12px 8px;text-transform:uppercase} -.thumb-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;padding:16px} -.thumb-item:hover{border-color:#5b9bd5} -.thumb-item{border:1px solid #333;overflow:hidden} -.thumb-label,footer{border-top:1px solid #333;font-size:11px} -.thumb-label{padding:6px 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.thumb-meta{font-size:10px;color:#777;padding:0 8px 6px} -.thumb-preview .big-icon{font-size:36px;color:#777} -.thumb-preview img{width:100%;height:100%;object-fit:cover} -.thumb-preview{width:100%;height:110px;display:flex;align-items:center;justify-content:center;background:#2a2a2a;overflow:hidden} -.tree-children.open{display:block} -.tree-children{display:none;padding-left:12px} -.tree-icon{color:#777;flex-shrink:0} -.tree-item.active{color:#5b9bd5} -.tree-item{align-items:center;padding:3px 8px;overflow:hidden;color:#d4d4d4;font-size:12px} -.tree-label{overflow:hidden;text-overflow:ellipsis} -.tree-root{list-style:none} -.tree-toggle{width:12px;text-align:center;color:#777;flex-shrink:0;font-size:10px} -.view-toggle button.active{background:#1a1a1a;color:#5b9bd5;border-color:#5b9bd5} -.view-toggle button:hover{background:#2e2e2e;color:#d4d4d4} -.view-toggle button{background:#2a2a2a;border:1px solid #333;color:#777;padding:4px 10px;cursor:pointer;font-size:12px;font-family:inherit} -.view-toggle{margin-left:auto} -</style> -</head> -<body> - -<header> - <span>&#128193;</span> - <div class="breadcrumb"> - {{ range $i, $crumb := .Breadcrumbs }} - {{ if gt $i 0 }}<span class="sep">/</span>{{ end }} - {{ if eq $i (sub1 (len $.Breadcrumbs)) }} - <span class="current">{{ $crumb.Name }}</span> - {{ else }} - <a href="{{ $crumb.Path }}">{{ $crumb.Name }}</a> - {{ end }} - {{ end }} - </div> - <div class="view-toggle"> - <button id="btn-list" class="active" onclick="setView('list')">&#9776; List</button> - <button id="btn-thumb" onclick="setView('thumb')">&#9638; Thumbs</button> - </div> -</header> - -<div class="layout"> - <nav class="sidebar"> - <div class="sidebar-title">Folders</div> - <ul class="tree-root" id="tree-root"></ul> - </nav> - - <main class="main"> - <div id="view-list"> - {{ if .Files }} - <table class="file-table" id="file-table"> - <thead> - <tr> - <th class="name" onclick="sortTable(0)">Name</th> - <th class="size" onclick="sortTable(1)">Size</th> - <th class="date" onclick="sortTable(2)">Modified</th> - </tr> - </thead> - <tbody> - {{ if ne .CurrentPath "/" }} - <tr> - <td class="name"><span class="file-icon dir">&#128193;</span><a href="../">../</a></td> - <td class="size">—</td> - <td class="date">—</td> - </tr> - {{ end }} - {{ range .Files }} - <tr data-name="{{ .Name }}" data-size="{{ .Size }}" data-date="{{ .ModTime.Unix }}"> - <td class="name"> - {{ if .IsDir }} - <span class="file-icon dir">&#128193;</span><a href="{{ .Path }}">{{ .Name }}/</a> - {{ else if isImage .Ext }} - <span class="file-icon img">&#128247;</span><a href="{{ .Path }}">{{ .Name }}</a> - {{ else }} - <span class="file-icon">&#128196;</span><a href="{{ .Path }}">{{ .Name }}</a> - {{ end }} - </td> - <td class="size">{{ if .IsDir }}—{{ else }}{{ formatSize .Size }}{{ end }}</td> - <td class="date">{{ formatTime .ModTime }}</td> - </tr> - {{ end }} - </tbody> - </table> - {{ else }} - <div class="empty">Empty directory</div> - {{ end }} - </div> - - <div id="view-thumb" style="display:none"> - {{ if .Files }} - <div class="thumb-grid"> - {{ if ne .CurrentPath "/" }} - <div class="thumb-item"> - <a href="../" style="display:block"> - <div class="thumb-preview"><span class="big-icon dir-icon">&#8593;</span></div> - <div class="thumb-label">../</div> - </a> - </div> - {{ end }} - {{ range .Files }} - <div class="thumb-item"> - <a href="{{ .Path }}" style="display:block"> - <div class="thumb-preview"> - {{ if .IsDir }} - <span class="big-icon dir-icon">&#128193;</span> - {{ else if isImage .Ext }} - <img src="/__thumb{{ .Path }}" alt="{{ .Name }}" loading="lazy"> - {{ else }} - <span class="big-icon">&#128196;</span> - {{ end }} - </div> - <div class="thumb-label" title="{{ .Name }}">{{ .Name }}{{ if .IsDir }}/{{ end }}</div> - <div class="thumb-meta">{{ if .IsDir }}folder{{ else }}{{ formatSize .Size }}{{ end }}</div> - </a> - </div> - {{ end }} - </div> - {{ else }} - <div class="empty">Empty directory</div> - {{ end }} - </div> - </main> -</div> - -<footer>{{ len .Files }} item(s) &nbsp;·&nbsp; {{ .CurrentPath }}</footer> - -<script> -const TREE_DATA = {{ .TreeJSON }}; -const CURRENT_PATH = {{ printf "%q" .CurrentPath }}; - -function setView(v) { - const isList = v === 'list'; - document.getElementById('view-list').style.display = isList ? '' : 'none'; - document.getElementById('view-thumb').style.display = isList ? 'none' : ''; - document.getElementById('btn-list').className = isList ? 'active' : ''; - document.getElementById('btn-thumb').className = isList ? '' : 'active'; -} - -let sortCol = -1, sortDir = 1; -function sortTable(col) { - const tbody = document.querySelector('#file-table tbody'); - const rows = Array.from(tbody.querySelectorAll('tr')); - const parentRow = rows.find(r => r.querySelector('a[href="../"]')); - const dataRows = rows.filter(r => r !== parentRow); - - if (sortCol === col) sortDir *= -1; - else { sortCol = col; sortDir = 1; } - - dataRows.sort((a, b) => { - let av, bv; - if (col === 0) { av = (a.dataset.name||'').toLowerCase(); bv = (b.dataset.name||'').toLowerCase(); } - else if (col === 1) { av = parseInt(a.dataset.size||0); bv = parseInt(b.dataset.size||0); } - else { av = parseInt(a.dataset.date||0); bv = parseInt(b.dataset.date||0); } - return av < bv ? -sortDir : av > bv ? sortDir : 0; - }); - - document.querySelectorAll('#file-table thead th').forEach((th, i) => { - th.className = th.className.replace(/ ?sort-(asc|desc)/g, ''); - if (i === col) th.className += sortDir > 0 ? ' sort-asc' : ' sort-desc'; - }); - - tbody.innerHTML = ''; - if (parentRow) tbody.appendChild(parentRow); - dataRows.forEach(r => tbody.appendChild(r)); -} - -function buildTree(nodes, container, depth) { - if (!nodes || !nodes.length) return; - nodes.forEach(node => { - const li = document.createElement('li'); - const item = document.createElement('div'); - item.className = 'tree-item' + (node.path === CURRENT_PATH ? ' active' : ''); - item.style.paddingLeft = (8 + depth * 12) + 'px'; - - const toggle = document.createElement('span'); - toggle.className = 'tree-toggle'; - toggle.textContent = (node.children && node.children.length) ? '▶' : ' '; - - const icon = document.createElement('span'); - icon.className = 'tree-icon'; - icon.textContent = '📁'; - - const label = document.createElement('span'); - label.className = 'tree-label'; - label.textContent = node.name; - - item.append(toggle, icon, label); - li.appendChild(item); - - let childUl = null; - if (node.children && node.children.length) { - childUl = document.createElement('ul'); - childUl.className = 'tree-children'; - buildTree(node.children, childUl, depth + 1); - li.appendChild(childUl); - toggle.addEventListener('click', e => { - e.stopPropagation(); - toggle.textContent = childUl.classList.toggle('open') ? '▼' : '▶'; - }); - } - - item.addEventListener('click', () => { window.location.href = node.path; }); - - if (CURRENT_PATH.startsWith(node.path) && childUl) { - childUl.classList.add('open'); - toggle.textContent = '▼'; - } - - container.appendChild(li); - }); -} - -buildTree(TREE_DATA, document.getElementById('tree-root'), 0); -</script> -</body> -</html>