commit 81e0b20d2d9d168c578386867157614e6beaf78e Author: Johannes Kimmel Date: Thu Nov 9 12:20:50 2023 +0100 sub: init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef761f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# End of https://www.toptal.com/developers/gitignore/api/go + +subv4 diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..0714c3f --- /dev/null +++ b/Containerfile @@ -0,0 +1,16 @@ +FROM golang AS builder + +WORKDIR /src + +COPY go.* ./ +RUN go mod download -x && go mod verify + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -tags -netgo -o sub + +FROM scratch + +COPY --from=builder /src/sub /sub + +CMD ["/sub"] diff --git a/css/classless.css b/css/classless.css new file mode 100644 index 0000000..886e91b --- /dev/null +++ b/css/classless.css @@ -0,0 +1,382 @@ +/* Classless.css v1.0 + +Table of Contents: + 1. Theme Settings + 2. Reset + 3. Base Style + 4. Extras (remove unwanted) + 5. Classes (remove unwanted) +*/ + +/* 1. Theme Settings ––––––––––––––––––––-–––––––––––––– */ + + +:root, html[data-theme='light'] { + --rem: 12pt; + --width: 50rem; + --navpos: absolute; /* fixed | absolute */ + --font-p: 1em/1.7 'Open Sans', 'DejaVu Sans', FreeSans, Helvetica, sans-serif; + --font-h: .9em/1.5 'Open Sans', 'DejaVu Sans', FreeSans, Helvetica, sans-serif; + --font-c: .9em/1.4 'DejaVu Sans Mono', monospace; + --border: 1px solid var(--cmed); + --ornament: "‹‹‹ ›››"; + /* foreground | background color */ + --cfg: #433; --cbg: #fff; + --cdark: #888; --clight: #f5f6f7; + --cmed: #d1d1d1; + --clink: #07c; + --cemph: #088; --cemphbg: #0881; +} + + +/* 2. Reset –––––––––––––––––––––––––––––––––––––––––––– */ + +/* reset block elements */ +* { box-sizing: border-box; border-spacing: 0; margin: 0; padding: 0;} +header, footer, figure, table, video, details, blockquote, +ul, ol, dl, fieldset, pre, pre > code, caption { + display: block; + margin: 0.5rem 0rem 1rem; + width: 100%; + overflow: auto hidden; + text-align: left; +} +video, summary, input, select { outline:none; } + +/* reset clickable things (FF Bug: select:hover prevents usage) */ +a, button, select, summary { color: var(--clink); cursor: pointer; } + + +/* 3. Base Style ––––––––––––––––––––––––––––––––––––––– */ +html { font-size: var(--rem); background: var(--cbg); } +body { + position: relative; + margin: auto; + max-width: var(--width); + font: var(--font-p); + color: var(--cfg); + padding: 3.0rem 0.6rem 0; + overflow-x: hidden; +} +body > footer { margin: 10rem 0rem 0rem; font-size: 90%; } +p { margin: .6em 0; } + +/* links */ +a[href]{ text-decoration: underline solid var(--cmed); text-underline-position: under; } +a[href^="#"] {text-decoration: none; } +a:hover, button:not([disabled]):hover, summary:hover { + filter: brightness(92%); color: var(--cemph); border-color: var(--cemph); +} + +/* lists */ +ul, ol, dl { margin: 1rem 0; padding: 0 0 0 2em; } +li:not(:last-child), dd:not(:last-child) { margin-bottom: 0.5rem; } +dt { font-weight: bold; } + +/* headings */ +h1, h2, h3, h4, h5 { margin: 1.5em 0 .5rem; font: var(--font-h); line-height: 1.2em; clear: both; } +h1+h2, h2+h3, h3+h4, h4+h5 { margin-top: .5em; padding-top: 0; } /* non-clashing headings */ +h1 { font-size: 2.2em; font-weight: 300; } +h2 { font-size: 2.0em; font-weight: 300; font-variant: small-caps; } +h3 { font-size: 1.5em; font-weight: 400; } +h4 { font-size: 1.1em; font-weight: 700; } +h5 { font-size: 1.2em; font-weight: 400; color: var(--cfg); } +h6 { font-size: 1.0em; font-weight: 700; font-style: italic; display: inline; } +h6 + p { display: inline; } + +/* tables */ +td, th { + padding: 0.5em 0.8em; + text-align: right; + border-bottom: 0.1rem solid var(--cmed); + white-space: nowrap; + font-size: 95%; +} +thead th[colspan] { padding: .2em 0.8em; text-align: center; } +thead tr:not(:only-child) td { padding: .2em 0.8em; } +thead+tbody tr:first-child td { border-top: 0.1rem solid var(--cdark); } +td:first-child, th:first-child { text-align: left; } +tr:hover{ background-color: var(--clight); } +table img { display: block; } + +/* figures */ +img, svg { max-width: 100%; vertical-align: text-top; object-fit: cover; } +p>img:not(:only-child) { float: right; margin: 0 0 .5em .5em; } +figure > img { display: inline-block; width: auto; } +figure > img:only-of-type, figure > svg:only-of-type { max-width: 100%; display: block; margin: 0 auto 0.4em; } +figcaption, caption { font: var(--font-h); color: var(--cdark); width: 100%; } +figcaption > *:first-child, caption > *:first-child { display: inline-block; margin: 0; } +figure > *:not(:last-child) { margin-bottom: 0.4rem; } + +/* code */ +pre > code { + margin: 0; + position: relative; + padding: 0.8em; + border-left: .4rem solid var(--cemph); +} +code, kbd, samp { + padding: 0.2em; + font: var(--font-c); + background: var(--clight); + border-radius: 4px; +} +kbd { border: 1px solid var(--cmed); } + +/* misc */ +blockquote { border-left: 0.4rem solid var(--cmed); padding: 0 0 0 1rem; } +time{ color: var(--cdark); } +hr { border: 0; border-top: 0.1rem solid var(--cmed); } +nav { width: 100%; background-color: var(--clight); } +::selection, mark { background: var(--clink); color: var(--cbg); } + + +/* 4. Extra Style –––––––––––––––––––––––––––––––––––––– */ + +/* Auto Numbering: figure/tables/headings/cite */ +article { counter-reset: h2 0 h3 0 tab 0 fig 0 lst 0 ref 0 eq 0; } +article figure figcaption:before { + color: var(--cemph); + counter-increment: fig; + content: "Figure " counter(fig) ": "; +} + +/* subfigures */ +figure { counter-reset: subfig 0 } +article figure figure { counter-reset: none; } +article figure > figure { display: inline-grid; width: auto; } +figure > figure:not(:last-of-type) { padding-right: 1rem; } +article figure figure figcaption:before { + counter-increment: subfig 1; + content: counter(subfig, lower-alpha) ": "; +} + +/* listings */ +article figure pre + figcaption:before { + counter-increment: lst 1; + content: "Listing " counter(lst) ": "; +} + +/* tables */ +figure > table:only-of-type { display: table; margin: 0.5em auto !important; width: fit-content; } +article figure > table caption { display: table-caption; caption-side: bottom; } +article figure > table + figcaption:before, +article table caption:before { + color: var(--cemph); + counter-increment: tab 1; + content: "Table " counter(tab) ": "; +} + +/* headings */ +article h2, h3 { position: relative; } +article h2:before, +article h3:before { + display: inline-block; + position: relative; + font-size: 0.6em; + text-align: right; + vertical-align: baseline; + left: -1rem; + width: 2.5em; + margin-left: -2.5em; +} +article h1 { counter-set: h2; } +article h2:before { counter-increment: h2; content: counter(h2) ". "; counter-set: h3; } +article h3:before { counter-increment: h3; content: counter(h2) "." counter(h3) ". ";} +@media (max-width: 60rem) { h2:before, h3:before { display: none; } } + +/* tooltip + citation */ +article p>cite:before { + padding: 0 .5em 0 0; + counter-increment: ref; content: " [" counter(ref) "] "; + vertical-align: super; font-size: .6em; +} +article p>cite > *:only-child { display: none; } +article p>cite:hover > *:only-child, +[data-tooltip]:hover:before { + display: inline-block; z-index: 40; + white-space: pre-wrap; + position: absolute; left: 1rem; right: 1rem; + padding: 1em 2em; + text-align: center; + transform:translateY( calc(-100%) ); + content: attr(data-tooltip); + color: var(--cbg); + background-color: var(--cemph); + box-shadow: 0 2px 10px 0 black; +} +[data-tooltip], article p>cite:before { + color: var(--clink); + border: .8rem solid transparent; margin: -.8rem; +} +abbr[title], [data-tooltip] { cursor: help; } + +/* navbar */ +nav+* { margin-top: 3rem; } +body>nav, header nav { + position: var(--navpos); + top: 0; left: 0; right: 0; + z-index: 41; + box-shadow: 0vw -50vw 0 50vw var(--clight), 0 calc(-50vw + 2px) 4px 50vw var(--cdark); +} +nav ul { list-style-type: none; } +nav ul:first-child { margin: 0; padding: 0; overflow: visible; } +nav ul:first-child > li { + display: inline-block; + margin: 0; + padding: 0.8rem .6rem; +} +nav ul > li > ul { + display: none; + width: auto; + position: absolute; + margin: 0.5rem 0; + padding: 1rem 2rem; + background-color: var(--clight); + border: var(--border); + border-radius: 4px; + z-index: 42; +} +nav ul > li > ul > li { white-space: nowrap; } +nav ul > li:hover > ul { display: block; } +@media (max-width: 40rem) { + nav ul:first-child > li:first-child:after { content: " \25BE"; } + nav ul:first-child > li:not(:first-child):not(.sticky) { display: none; } + nav ul:first-child:hover > li:not(:first-child):not(.sticky) { display: block; float: none !important; } +} + +/* details/cards */ +summary>* { display: inline; } +.card, details { + display: block; + margin: 0.5rem 0rem 1rem; + padding: 0 .6rem; + border-radius: 4px; + overflow: hidden; +} +.card, details[open] { outline: 1px solid var(--cmed); } +.card>img:first-child { margin: -3px -.6rem; max-width: calc(100% + 1.2rem); } +summary:hover, details[open] summary, .card>p:first-child { + box-shadow: inset 0 0 0 2em var(--clight), 0 -.8rem 0 .8rem var(--clight); +} +.hint { --cmed: var(--cemph); --clight: var(--cemphbg); background-color: var(--clight); } +.warn { --cmed: #c11; --clight: #e221; background-color: var(--clight); } + +/* big first letter */ +article > section:first-of-type > h2:first-of-type + p:first-letter, +article > h2:first-of-type + p:first-letter, .lettrine { + float: left; + font-size: 3.5em; + padding: 0.1em 0.1em 0 0; + line-height: 0.68em; + color: var(--cemph); +} + +/* ornaments */ +section:after { + display: block; + margin: 1em 0; + color: var(--cmed); + text-align: center; + font-size: 1.5em; + content: var(--ornament); +} + +/* side menu (aside is not intended for use in a paragraph!) */ +main aside { + position: absolute; + width: 8rem; right: -8.6rem; + font-size: 0.8em; line-height: 1.4em; +} +@media (max-width: 70rem) { main aside { display: none; } } + +/* forms and inputs */ +textarea, input:not([type=range]), button, select { + font: var(--font-h); + border-radius: 4px; + border: 1.5px solid var(--cmed); + padding: 0.4em 0.8em; + color: var(--cfg); + background-color: var(--clight); +} +fieldset select, input:not([type=checkbox]):not([type=radio]) { + display: block; + width: 100%; + margin: 0 0 1rem; +} +button, select { + font-weight: bold; + margin: .5em; + border: 1.5px solid var(--clink); +} +button { padding: 0.4em 1em; font-size: 85%; letter-spacing: 0.1em; } +button[disabled]{ color: var(--cdark); border-color: var(--cmed); } +fieldset { border-radius: 4px; border: var(--border); padding: .5em 1em;} +textarea:hover, input:not([type=checkbox]):not([type*='ra']):hover, select:hover{ + border: 1.5px solid var(--cemph); +} +textarea:focus, input:not([type=checkbox]):not([type*='ra']):focus{ + border: 1.5px solid var(--clink); + box-shadow: 0 0 5px var(--clink); +} +p>button { padding: 0 .5em; margin: 0 .5em; } +p>select { padding: 0; margin: 0 .5em; } + + +/* 5. Bootstrap-compatible classes ––––––––––––––––––––– */ + +/* grid */ +.row { display: flex; margin: 0.5rem -0.6rem; align-items: stretch; } +.row [class*="col"] { padding: 0 0.6rem; } +.row .col { flex: 1 1 100%; } +.row .col-2 { flex: 0 0 16.66%; max-width: 16.66%;} +.row .col-3 { flex: 0 0 25%; max-width: 25%;} +.row .col-4 { flex: 0 0 33.33%; max-width: 33.33%; } +.row .col-5 { flex: 0 0 41.66%; max-width: 41.66%; } +.row .col-6 { flex: 0 0 50%; max-width: 50%; } +@media (max-width: 40rem) { .row { flex-direction: column; } } + +/* align */ +.text-left { text-align: left; } +.text-right { text-align: right; } +.text-center { text-align: center; } +.float-left { float: left !important; } +.float-right { float: right !important; } +.clearfix { clear: both; } + +/* colors */ +.text-black { color: #000; } +.text-white { color: #fff; } +.text-primary { color: var(--cemph); } +.text-secondary{ color: var(--cdark); } +.bg-white { background-color: #fff; } +.bg-light { background-color: var(--clight); } +.bg-primary { background-color: var(--cemph); } +.bg-secondary{ background-color: var(--cmed); } + +/* margins */ +.mx-auto { margin-left: auto; margin-right: auto; } +.m-0 { margin: 0 !important; } +.m-1, .mx-1, .mr-1 { margin-right: 1.0rem !important; } +.m-1, .mx-1, .ml-1 { margin-left: 1.0rem !important; } +.m-1, .my-1, .mt-1 { margin-top: 1.0rem !important; } +.m-1, .my-1, .mb-1 { margin-bottom: 1.0rem !important; } + +/* pading */ +.p-0 { padding: 0 !important; } +.p-1, .px-1, .pr-1 { padding-right: 1.0rem !important; } +.p-1, .px-1, .pl-1 { padding-left: 1.0rem !important; } +.p-1, .py-1, .pt-1 { padding-top: 1.0rem !important; } +.p-1, .py-1, .pb-1 { padding-bottom: 1.0rem !important; } + +/* be print-friendly */ +@media print { + @page { margin: 1.5cm 2cm; } + html {font-size: 9pt!important; } + body { max-width: 27cm; } + p { orphans: 2; widows: 2; } + caption, figcaption { page-break-before: avoid; } + h2, h3, h4, h5 { page-break-after: avoid;} + .noprint, body>nav, section:after { display: none; } + .row { flex-direction: row; } +} \ No newline at end of file diff --git a/css/subv4.css b/css/subv4.css new file mode 100644 index 0000000..6a8b945 --- /dev/null +++ b/css/subv4.css @@ -0,0 +1,29 @@ +#control { + display: flex; +} +#control button { + flex:auto; +} +#used-grouped td:first-child , +#used td:first-child { + text-align: right +} +#used-grouped td:first-child:after , +#used td:first-child:after { + content: "/" +} +#used-grouped td , +#used td { + padding-left: 0; + padding-right: 0; +} +#used-grouped { + display: flex; + flex-wrap: wrap; + + justify-content: space-evenly; + gap: 3rem; +} +#used-grouped > div { + flex: 0 1 auto; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9c75c8d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.freifunk-franken.de/jkimmel/sub + +go 1.21.3 diff --git a/index.html b/index.html new file mode 100644 index 0000000..7ac59c2 --- /dev/null +++ b/index.html @@ -0,0 +1,60 @@ + + + + + + Subnetzvergabe + + + + + + +

Subnetzvergabe

+ +
+ + + + + + + + + +
+ + {{ block "update" .Update }}
{{ with .Content }}{{ . }}{{ end }}
{{ end }} + + {{ block "used" .Api }} +
+ +

grouped

+
+ {{- range .UsedGrouped }} + {{ $first := index . 0 }} + {{ $bits := $first.Bits }} +
+

/{{ $bits }}

+ + {{ range . }} + {{ end }} +
{{.Addr}}{{.Bits}}
+
+ {{ end }} +
+ + + +

all

+
+ + {{- range .Used }} + {{ end }} +
{{.Addr}}{{.Bits}}
+ +
+
+ {{ end }} + + diff --git a/ipalloc/ipalloc.go b/ipalloc/ipalloc.go new file mode 100644 index 0000000..d552975 --- /dev/null +++ b/ipalloc/ipalloc.go @@ -0,0 +1,468 @@ +package ipalloc + +import ( + "bufio" + "fmt" + "log" + "net/netip" + "os" + "slices" + "sync" + "syscall" +) + +type Store interface { + Append(Operation) error + Operations() (Scanner, error) +} + +type Scanner interface { + Next() bool + Operation() Operation + Err() error +} + +type Operation interface { + Prefix() netip.Prefix +} + +type op struct { + prefix netip.Prefix +} + +func (o op) Prefix() netip.Prefix { + return o.prefix +} + +type OpProvision struct{ op } + +type OpAllocation struct{ op } + +type OpDeallocation struct{ op } + +type Memstore struct { + ops []Operation +} + +func (m *Memstore) Append(op Operation) error { + m.ops = append(m.ops, op) + return nil +} + +func (m *Memstore) Operations() (Scanner, error) { + return &memscanner{opQueue: slices.Clip(m.ops)}, nil +} + +type memscanner struct { + cur Operation + opQueue []Operation +} + +func (m *memscanner) Next() bool { + if len(m.opQueue) == 0 { + return false + } + + m.opQueue, m.cur = m.opQueue[1:], m.opQueue[0] + return true +} + +func (m *memscanner) Operation() Operation { + return m.cur +} + +func (m *memscanner) Err() error { + return nil +} + +type Filestore struct { + filepath string +} + +func (fs *Filestore) Append(op Operation) error { + f, err := os.OpenFile(fs.filepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return err + } + defer f.Close() + + if err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + return err + } + + _, err = fmt.Fprintln(f, op) + return err +} + +func (fs *Filestore) Operations() (Scanner, error) { + f, err := os.OpenFile(fs.filepath, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + + if err = syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil { + return nil, err + } + + return newFileScanner(f), nil +} + +type fileScanner struct { + err error + cur Operation + f *os.File + s bufio.Scanner +} + +func newFileScanner(f *os.File) *fileScanner { + return &fileScanner{ + err: nil, + cur: nil, + f: f, + s: *bufio.NewScanner(f), + } +} + +func (f *fileScanner) Next() bool { + if f.f == nil { + return false + } + if !f.s.Scan() { + f.err = f.s.Err() + if err := f.f.Close(); err != nil { + log.Println(err) + } + return false + } + log.Println(f.s.Text()) + f.cur = nil // TODO: parse + return true +} + +func (f *fileScanner) Operation() Operation { + return f.cur +} + +func (f *fileScanner) Err() error { + return f.err +} + +type tree struct { + prefix netip.Prefix + lo *tree + hi *tree + parent *tree +} + +func (t *tree) Prefix() netip.Prefix { + return t.prefix +} + +func (t *tree) Split() { + lo, hi := splitPrefix(t.prefix) + + t.lo = &tree{prefix: lo, parent: t} + t.hi = &tree{prefix: hi, parent: t} +} + +func (t *tree) BestMatch(bits int) *tree { + if t == nil { + return nil + } + if t.Leaf() { + // already taken + return nil + } + //log.Println("testing", t.prefix) + if t.prefix.Bits() >= bits { + //log.Println("too long", t.prefix) + return nil + } + + var best *tree + if !t.Full() { + best = t + } + + best = better(best, t.lo.BestMatch(bits)) + best = better(best, t.hi.BestMatch(bits)) + + return best +} + +func better(t1, t2 *tree) *tree { + switch { + case t1 == nil: + return t2 + case t2 == nil: + return t1 + case t1.prefix.Bits() >= t2.prefix.Bits(): + return t1 + default: + return t2 + } +} + +func (t *tree) Alloc(bits int) *tree { + //log.Println("Searching space for", bits) + best := t.BestMatch(bits) + if best == nil { + best = t + } + if best.Full() { + return nil + } + + for best.prefix.Bits() < bits { + lo, hi := splitPrefix(best.prefix) + //log.Println(lo, hi) + switch { + case best.lo == nil: + best.lo = &tree{prefix: lo, parent: best} + best = best.lo + case best.hi == nil: + best.hi = &tree{prefix: hi, parent: best} + best = best.hi + default: + panic("unreachable") + } + //log.Println("choosing", best.prefix) + } + + return best +} + +func (t *tree) Insert(p netip.Prefix) *tree { + var ret *tree + + for t.prefix.Bits() < p.Bits() { + lo, hi := splitPrefix(t.prefix) + //log.Println(lo, hi) + switch { + case lo.Overlaps(p): + if t.lo != nil { + t = t.lo + } else { + t.lo = &tree{prefix: lo, parent: t} + t = t.lo + ret = t + } + case hi.Overlaps(p): + if t.hi != nil { + t = t.hi + } else { + t.hi = &tree{prefix: hi, parent: t} + t = t.hi + ret = t + } + default: + return nil + } + //log.Println("splitting to", best.prefix) + } + return ret +} + +func (t *tree) FindBest(p netip.Prefix) *tree { + if t == nil { + return nil + } + if !t.prefix.Overlaps(p) { + return nil + } + if best := t.lo.FindBest(p); best != nil { + return best + } + if best := t.hi.FindBest(p); best != nil { + return best + } + return t +} + +type ErrNotFound struct{} + +func (e ErrNotFound) Error() string { + return "prefix not found" +} + +type ErrNotEmpty struct{} + +func (e ErrNotEmpty) Error() string { + return "prefix not empty" +} + +func (t *tree) Dealloc(p netip.Prefix) error { + match := t.Find(p) + if match == nil { + return ErrNotFound{} + } + if !match.Leaf() { + return ErrNotEmpty{} + } + + for match != nil { + if match.Leaf() && match.parent != nil { + if match.parent.lo == match { + match.parent.lo = nil + } + if match.parent.hi == match { + match.parent.hi = nil + } + } + + match = match.parent + } + + return nil +} +func (t *tree) Find(p netip.Prefix) *tree { + if t == nil { + return nil + } + //log.Println("searching for", p, "in", t.prefix) + if t.prefix == p { + return t + } + if !t.prefix.Overlaps(p) { + //log.Println(p, "does not overlap", t.prefix) + return nil + } + l := t.lo.Find(p) + if l != nil { + return l + } + return t.hi.Find(p) +} + +func (t *tree) Leaf() bool { + return t != nil && t.lo == nil && t.hi == nil +} +func (t *tree) Full() bool { + return t != nil && t.lo != nil && t.hi != nil +} + +func (t *tree) Walk(f func(*tree) bool) { + if t == nil { + return + } + t.lo.Walk(f) + if !f(t) { + return + } + t.hi.Walk(f) +} + +type DB struct { + sync.Mutex + provisions *tree +} + +func NewDB(prefix string) *DB { + return &DB{provisions: &tree{prefix: netip.MustParsePrefix(prefix)}} +} + +func (db *DB) Used() []netip.Prefix { + db.Lock() + defer db.Unlock() + var ret []netip.Prefix + db.provisions.Walk(func(t *tree) bool { + if t.Leaf() && t.parent != nil { + ret = append(ret, t.prefix) + } + return true + }) + + return ret +} +func (db *DB) UsedGrouped() [][]netip.Prefix { + db.Lock() + defer db.Unlock() + bins := make(map[int][]netip.Prefix) + db.provisions.Walk(func(t *tree) bool { + if t.Leaf() && t.parent != nil { + p := t.prefix + bins[p.Bits()] = append(bins[p.Bits()], p) + } + return true + }) + + var ret [][]netip.Prefix + for _, bin := range bins { + ret = append(ret, bin) + } + + slices.SortFunc(ret, func(ps1, ps2 []netip.Prefix) int { + return ps2[0].Bits() - ps1[0].Bits() + }) + + return ret +} +func (db *DB) All() []*tree { + db.Lock() + defer db.Unlock() + var ret []*tree + db.provisions.Walk(func(t *tree) bool { + ret = append(ret, t) + return true + }) + return ret +} +func (db *DB) Alloc(bits int) (*tree, error) { + db.Lock() + defer db.Unlock() + t := db.provisions.Alloc(bits) + if t == nil { + return nil, fmt.Errorf("Allocation not possible for size %d", bits) + } + return t, nil +} +func (db *DB) Insert(prefix netip.Prefix) error { + return nil +} +func (db *DB) Dealloc(prefix netip.Prefix) error { + db.Lock() + defer db.Unlock() + return db.provisions.Dealloc(prefix) +} +func (db *DB) Free() ([]netip.Prefix, error) { + db.Lock() + defer db.Unlock() + return nil, nil +} +func (db *DB) Find(p netip.Prefix) bool { + db.Lock() + defer db.Unlock() + p = p.Masked() + return db.provisions.Find(p) != nil +} + +//func (db *DB) Provision(p netip.Prefix) error { +// db.Lock() +// defer db.Unlock() +// p = p.Masked() +// if db.provisions.Provision(p) == nil { +// return fmt.Errorf("Overlapping provisions: %s", p) +// } +// return nil +//} + +func splitPrefix(p netip.Prefix) (netip.Prefix, netip.Prefix) { + if p.IsSingleIP() { + log.Fatal("can't split single ip prefix", p) + } + p = p.Masked() + + bs := p.Addr().AsSlice() + off := p.Bits() / 8 + bit := p.Bits() % 8 + bs[off] |= 0x80 >> bit + hiaddr, ok := netip.AddrFromSlice(bs) + if !ok { + log.Fatal("can't use slice as addr", bs) + } + + lo := netip.PrefixFrom(p.Addr(), p.Bits()+1) + hi := netip.PrefixFrom(hiaddr, p.Bits()+1) + + return lo, hi +} diff --git a/ipalloc/ipalloc_test.go b/ipalloc/ipalloc_test.go new file mode 100644 index 0000000..15abb2c --- /dev/null +++ b/ipalloc/ipalloc_test.go @@ -0,0 +1,177 @@ +package ipalloc + +import ( + "log" + "net/netip" + "testing" +) + +func TestSplitPrefix(t *testing.T) { + tests := []struct{ in, lo, hi string }{ + {"0.0.0.0/0", "0.0.0.0/1", "128.0.0.0/1"}, + {"0.0.0.0/29", "0.0.0.0/30", "0.0.0.4/30"}, + {"0.0.0.0/31", "0.0.0.0/32", "0.0.0.1/32"}, + {"0.0.0.1/0", "0.0.0.0/1", "128.0.0.0/1"}, + } + + for _, test := range tests { + lo, hi := splitPrefix(netip.MustParsePrefix(test.in)) + if lo != netip.MustParsePrefix(test.lo) || hi != netip.MustParsePrefix(test.hi) { + t.Fatal(test.in, lo, hi) + } + } +} + +func TestTreeAlloc(t *testing.T) { + tests := []struct { + bits int + best string + }{ + {32, "0.0.0.0/32"}, + {32, "0.0.0.1/32"}, + {1, "128.0.0.0/1"}, + {2, "64.0.0.0/2"}, + {31, "0.0.0.2/31"}, + {32, "0.0.0.4/32"}, + } + root := &tree{prefix: netip.MustParsePrefix("0.0.0.0/0")} + + for _, test := range tests { + best := root.Alloc(test.bits) + if best == nil { + t.Fatal(root, test.bits) + } + if best.prefix != netip.MustParsePrefix(test.best) { + t.Fatal(root, test.bits, best.prefix.String()) + } + } + +} + +func TestTreeDealloc(t *testing.T) { + tests := []struct { + bits int + best string + }{ + {32, "0.0.0.0/32"}, + {32, "0.0.0.1/32"}, + {1, "128.0.0.0/1"}, + {2, "64.0.0.0/2"}, + {31, "0.0.0.2/31"}, + {32, "0.0.0.4/32"}, + } + root := &tree{prefix: netip.MustParsePrefix("0.0.0.0/0")} + + for _, test := range tests { + best := root.Alloc(test.bits) + if best == nil { + t.Fatal(root, test.bits) + } + if best.prefix != netip.MustParsePrefix(test.best) { + t.Fatal(root, test.bits, best.prefix.String()) + } + } + + deallocs := []string{ + "0.0.0.0/32", + "0.0.0.1/32", + "128.0.0.0/1", + "64.0.0.0/2", + "0.0.0.2/31", + "0.0.0.4/32", + } + + for _, test := range deallocs { + if err := root.Dealloc(netip.MustParsePrefix(test)); err != nil { + t.Fatal(test, err) + } + } + + if !root.Leaf() { + root.Walk(func(t *tree) bool { + log.Println(t.prefix, t.Leaf()) + return true + }) + t.Fatalf("root not empty: %#v", root) + } + + first := root.Alloc(2).prefix + root.Alloc(2) + root.Dealloc(first) + if first != root.Alloc(2).prefix { + root.Walk(func(t *tree) bool { + log.Println(t.prefix, t.Leaf()) + return true + }) + t.Fatal(first) + } +} +func TestFindBest(t *testing.T) { + tests := []struct { + bits int + best string + }{ + {32, "0.0.0.0/32"}, + {32, "0.0.0.1/32"}, + {1, "128.0.0.0/1"}, + {2, "64.0.0.0/2"}, + {31, "0.0.0.2/31"}, + {32, "0.0.0.4/32"}, + } + root := &tree{prefix: netip.MustParsePrefix("0.0.0.0/0")} + + for _, test := range tests { + best := root.Alloc(test.bits) + if best == nil { + t.Fatal(root, test.bits) + } + if best.prefix != netip.MustParsePrefix(test.best) { + t.Fatal(root, test.bits, best.prefix.String()) + } + } + + best := [][2]string{ + {"128.0.0.0/32", "128.0.0.0/1"}, + {"128.0.0.1/32", "128.0.0.0/1"}, + } + + for _, test := range best { + best := root.FindBest(netip.MustParsePrefix(test[0])) + want := netip.MustParsePrefix(test[1]) + + if best.prefix != want { + t.Fatal(best, want) + } + } +} + +func TestInsert(t *testing.T) { + tests := []string{ + "0.0.0.1/32", + "0.0.0.0/32", + "128.0.0.0/1", + "64.0.0.0/2", + "0.0.0.4/32", + "0.0.0.2/31", + } + root := &tree{prefix: netip.MustParsePrefix("0.0.0.0/0")} + + for _, test := range tests { + p := netip.MustParsePrefix(test) + insert := root.Insert(p) + if insert == nil { + t.Fatal(root, test) + } + match := root.Find(p) + if match.prefix != p { + t.Fatal("unable to find", p, "in", root) + } + } + for _, test := range tests { + p := netip.MustParsePrefix(test) + insert := root.Insert(p) + if insert != nil { + t.Fatal(root, test) + } + } +} diff --git a/js/htmx.js b/js/htmx.js new file mode 100644 index 0000000..e69de29 diff --git a/js/htmx.min.js b/js/htmx.min.js new file mode 100644 index 0000000..daab078 --- /dev/null +++ b/js/htmx.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Y={onLoad:t,process:Dt,on:Z,off:K,trigger:fe,ajax:Cr,find:E,findAll:f,closest:d,values:function(e,t){var r=or(e,t||"post");return r.values},remove:B,addClass:F,removeClass:n,toggleClass:V,takeClass:j,defineExtension:Ar,removeExtension:Nr,logAll:X,logNone:U,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,scrollIntoViewOnBoost:true},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Y.config.wsBinaryType;return t},version:"1.9.7"};var r={addTriggerHandler:St,bodyContains:oe,canAccessLocalStorage:M,findThisElement:ve,filterValues:cr,hasAttribute:o,getAttributeValue:ee,getClosestAttributeValue:re,getClosestMatch:c,getExpressionVars:wr,getHeaders:fr,getInputValues:or,getInternalData:ie,getSwapSpecification:dr,getTriggerSpecs:Ze,getTarget:ge,makeFragment:l,mergeObjects:se,makeSettleInfo:T,oobSwap:ye,querySelectorExt:le,selectAndSwap:Ue,settleImmediately:Jt,shouldCancel:tt,triggerEvent:fe,triggerErrorEvent:ue,withExtensions:C};var b=["get","post","put","delete","patch"];var w=b.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function Q(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function ee(e,t){return Q(e,t)||Q(e,"data-"+t)}function u(e){return e.parentElement}function te(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function R(e,t,r){var n=ee(t,r);var i=ee(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function re(t,r){var n=null;c(t,function(e){return n=R(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=te().createDocumentFragment()}return i}function H(e){return e.match(/",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i(""+e+"
",1);case"col":return i(""+e+"
",2);case"tr":return i(""+e+"
",2);case"td":case"th":return i(""+e+"
",3);case"script":case"style":return i("
"+e+"
",1);default:return i(e,0)}}}function ne(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ie(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r=0}function oe(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return te().body.contains(e.getRootNode().host)}else{return te().body.contains(e)}}function P(e){return e.trim().split(/\s+/)}function se(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function S(e){try{return JSON.parse(e)}catch(e){y(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return xr(te().body,function(){return eval(e)})}function t(t){var e=Y.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){Y.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function U(){Y.logger=null}function E(e,t){if(t){return e.querySelector(t)}else{return E(te(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(te(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function F(e,t,r){e=s(e);if(r){setTimeout(function(){F(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);ae(e.parentElement.children,function(e){n(e,t)});F(e,t)}function d(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function _(e,t){return e.substring(e.length-t.length)===t}function z(e){var t=e.trim();if(g(t,"<")&&_(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[d(e,z(t.substr(8)))]}else if(t.indexOf("find ")===0){return[E(e,z(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[$(e,z(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[G(e,z(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return te().querySelectorAll(z(t))}}var $=function(e,t){var r=te().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function le(e,t){if(t){return W(e,t)[0]}else{return W(te().body,e)[0]}}function s(e){if(L(e,"String")){return E(e)}else{return e}}function J(e,t,r){if(A(t)){return{target:te().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function Z(t,r,n){Pr(function(){var e=J(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function K(t,r,n){Pr(function(){var e=J(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var he=te().createElement("output");function de(e,t){var r=re(e,t);if(r){if(r==="this"){return[ve(e,t)]}else{var n=W(e,r);if(n.length===0){y('The selector "'+r+'" on '+t+" returned no matches!");return[he]}else{return n}}}}function ve(e,t){return c(e,function(e){return ee(e,t)!=null})}function ge(e){var t=re(e,"hx-target");if(t){if(t==="this"){return ve(e,"hx-target")}else{return le(e,t)}}else{var r=ie(e);if(r.boosted){return te().body}else{return e}}}function me(e){var t=Y.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=te().querySelectorAll(t);if(r){ae(r,function(e){var t;var r=i.cloneNode(true);t=te().createDocumentFragment();t.appendChild(r);if(!xe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!fe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){De(o,e,e,t,a)}ae(a.elts,function(e){fe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ue(te().body,"htmx:oobErrorNoTarget",{content:i})}return e}function be(e,t,r){var n=re(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();pe(e,i);s.tasks.push(function(){pe(e,a)})}}})}function Ee(e){return function(){n(e,Y.config.addedClass);Dt(e);Ct(e);Ce(e);fe(e,"htmx:load")}}function Ce(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Se(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;F(i,Y.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Ee(i))}}}function Te(e,t){var r=0;while(r-1){var t=e.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Ue(e,t,r,n,i,a){i.title=Xe(n);var o=l(n);if(o){be(r,o,i);o=Me(r,o,a);we(o);return De(e,r,t,o,i)}}function Be(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=S(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!N(o)){o={value:o}}fe(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=xr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ue(te().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if($e(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function x(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var Je="input, textarea, select";function Ze(e){var t=ee(e,"hx-trigger");var r=[];if(t){var n=We(t);do{x(n,ze);var i=n.length;var a=x(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};x(n,ze);o.pollInterval=v(x(n,/[,\[\s]/));x(n,ze);var s=Ge(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=Ge(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){x(n,ze);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=v(x(n,p))}else if(u==="from"&&n[0]===":"){n.shift();var f=x(n,p);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();var c=x(n,p);if(c.length>0){f+=" "+c}}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=x(n,p)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=v(x(n,p))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=x(n,p)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=x(n,p)}else{ue(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ue(e,"htmx:syntax:error",{token:n.shift()})}x(n,ze)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,Je)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Ke(e){ie(e).cancelled=true}function Ye(e,t,r){var n=ie(e);n.timeout=setTimeout(function(){if(oe(e)&&n.cancelled!==true){if(!nt(r,e,Ut("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ye(e,t,r)}},r.pollInterval)}function Qe(e){return location.hostname===e.hostname&&Q(e,"href")&&Q(e,"href").indexOf("#")!==0}function et(t,r,e){if(t.tagName==="A"&&Qe(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=Q(t,"href")}else{var a=Q(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=Q(t,"action")}e.forEach(function(e){it(t,function(e,t){if(d(e,Y.config.disableSelector)){m(e);return}ce(n,i,e,t)},r,e,true)})}}function tt(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&d(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function rt(e,t){return ie(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function nt(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){ue(te().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function it(a,o,e,s,l){var u=ie(a);var t;if(s.from){t=W(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ie(e);t.lastValue=e.value})}ae(t,function(n){var i=function(e){if(!oe(a)){n.removeEventListener(s.trigger,i);return}if(rt(a,e)){return}if(l||tt(e,a)){e.preventDefault()}if(nt(s,a,e)){return}var t=ie(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ie(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{fe(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var at=false;var ot=null;function st(){if(!ot){ot=function(){at=true};window.addEventListener("scroll",ot);setInterval(function(){if(at){at=false;ae(te().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){lt(e)})}},200)}}function lt(t){if(!o(t,"data-hx-revealed")&&k(t)){t.setAttribute("data-hx-revealed","true");var e=ie(t);if(e.initHash){fe(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){fe(t,"revealed")},{once:true})}}}function ut(e,t,r){var n=P(r);for(var i=0;i=0){var t=dt(n);setTimeout(function(){ft(s,r,n+1)},t)}};t.onopen=function(e){n=0};ie(s).webSocket=t;t.addEventListener("message",function(e){if(ct(s)){return}var t=e.data;C(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=I(n.children);for(var a=0;a0){fe(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(tt(e,u)){e.preventDefault()}})}else{ue(u,"htmx:noWebSocketSourceError")}}function dt(e){var t=Y.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}y('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function vt(e,t,r){var n=P(r);for(var i=0;i0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Nt(o)}for(var l in r){It(e,l,r[l])}}}function Pt(t){Re(t);for(var e=0;eY.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ue(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function _t(e){if(!M()){return null}e=D(e);var t=S(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){fe(te().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Vt();var r=T(t);var n=Xe(this.response);if(n){var i=E("title");if(i){i.innerHTML=n}else{window.document.title=n}}Pe(t,e,r);Jt(r.tasks);Ft=a;fe(te().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ue(te().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function Kt(e){Wt();e=e||location.pathname+location.search;var t=_t(e);if(t){var r=l(t.content);var n=Vt();var i=T(n);Pe(n,r,i);Jt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Ft=e;fe(te().body,"htmx:historyRestore",{path:e,item:t})}else{if(Y.config.refreshOnHistoryMiss){window.location.reload(true)}else{Zt(e)}}}function Yt(e){var t=de(e,"hx-indicator");if(t==null){t=[e]}ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Y.config.requestClass)});return t}function Qt(e){var t=de(e,"hx-disabled-elt");if(t==null){t=[]}ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function er(e,t){ae(e,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Y.config.requestClass)}});ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function tr(e,t){for(var r=0;r=0}function dr(e,t){var r=t?t:re(e,"hx-swap");var n={swapStyle:ie(e).boosted?"innerHTML":Y.config.defaultSwapStyle,swapDelay:Y.config.defaultSwapDelay,settleDelay:Y.config.defaultSettleDelay};if(Y.config.scrollIntoViewOnBoost&&ie(e).boosted&&!hr(e)){n["show"]="top"}if(r){var i=P(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}else if(a==0){n["swapStyle"]=o}else{y("Unknown modifier in hx-swap: "+o)}}}}return n}function vr(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&Q(e,"enctype")==="multipart/form-data"}function gr(t,r,n){var i=null;C(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(vr(r)){return ur(n)}else{return lr(n)}}}function T(e){return{tasks:[],elts:[e]}}function mr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=le(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=le(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Y.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Y.config.scrollBehavior})}}}function pr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=ee(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=xr(e,function(){return Function("return ("+a+")")()},{})}else{s=S(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return pr(u(e),t,r,n)}function xr(e,t,r){if(Y.config.allowEval){return t()}else{ue(e,"htmx:evalDisallowedError");return r}}function yr(e,t){return pr(e,"hx-vars",true,t)}function br(e,t){return pr(e,"hx-vals",false,t)}function wr(e){return se(yr(e),br(e))}function Sr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Er(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ue(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return e.getAllResponseHeaders().match(t)}function Cr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||L(r,"String")){return ce(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return ce(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return ce(e,t,null,null,{returnPromise:true})}}function Tr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function Or(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Y.config.selfRequestsOnly){if(!n){return false}}return fe(e,"htmx:validateUrl",se({url:i,sameHost:n},r))}function ce(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=te().body}var M=a.handler||qr;if(!oe(n)){ne(o);return l}var u=a.targetOverride||ge(n);if(u==null||u==he){ue(n,"htmx:targetError",{target:ee(n,"hx-target")});ne(s);return l}var f=ie(n);var c=f.lastButtonClicked;if(c){var h=Q(c,"formaction");if(h!=null){r=h}var d=Q(c,"formmethod");if(d!=null){if(d.toLowerCase()!=="dialog"){t=d}}}var v=re(n,"hx-confirm");if(e===undefined){var D=function(e){return ce(t,r,n,i,a,!!e)};var X={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:v};if(fe(n,"htmx:confirm",X)===false){ne(o);return l}}var g=n;var m=re(n,"hx-sync");var p=null;var x=false;if(m){var U=m.split(":");var B=U[0].trim();if(B==="this"){g=ve(n,"hx-sync")}else{g=le(n,B)}m=(U[1]||"drop").trim();f=ie(g);if(m==="drop"&&f.xhr&&f.abortable!==true){ne(o);return l}else if(m==="abort"){if(f.xhr){ne(o);return l}else{x=true}}else if(m==="replace"){fe(g,"htmx:abort")}else if(m.indexOf("queue")===0){var F=m.split(" ");p=(F[1]||"last").trim()}}if(f.xhr){if(f.abortable){fe(g,"htmx:abort")}else{if(p==null){if(i){var y=ie(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){p=y.triggerSpec.queue}}if(p==null){p="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(p==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){ce(t,r,n,i,a)})}else if(p==="all"){f.queuedRequests.push(function(){ce(t,r,n,i,a)})}else if(p==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){ce(t,r,n,i,a)})}ne(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var V=re(n,"hx-prompt");if(V){var S=prompt(V);if(S===null||!fe(n,"htmx:prompt",{prompt:S,target:u})){ne(o);w();return l}}if(v&&!e){if(!confirm(v)){ne(o);w();return l}}var E=fr(n,u,S);if(a.headers){E=se(E,a.headers)}var j=or(n,t);var C=j.errors;var T=j.values;if(a.values){T=se(T,a.values)}var _=wr(n);var z=se(T,_);var O=cr(z,n);if(t!=="get"&&!vr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(Y.config.getCacheBusterParam&&t==="get"){O["org.htmx.cache-buster"]=Q(u,"id")||"true"}if(r==null||r===""){r=te().location.href}var R=pr(n,"hx-request");var W=ie(n).boosted;var q=Y.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:O,unfilteredParameters:z,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||R.credentials||Y.config.withCredentials,timeout:a.timeout||R.timeout||Y.config.timeout,path:r,triggeringEvent:i};if(!fe(n,"htmx:configRequest",H)){ne(o);w();return l}r=H.path;t=H.verb;E=H.headers;O=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){fe(n,"htmx:validation:halted",H);ne(o);w();return l}var $=r.split("#");var G=$[0];var L=$[1];var A=r;if(q){A=G;var J=Object.keys(O).length!==0;if(J){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=lr(O);if(L){A+="#"+L}}}if(!Or(n,A,H)){ue(n,"htmx:invalidPath",H);ne(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(R.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var Z=E[N];Sr(b,N,Z)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Tr(n);I.pathInfo.responsePath=Er(b);M(n,I);er(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:afterOnLoad",I);if(!oe(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(oe(r)){t=r}}if(t){fe(t,"htmx:afterRequest",I);fe(t,"htmx:afterOnLoad",I)}}ne(o);w()}catch(e){ue(n,"htmx:onLoadError",se({error:e},I));throw e}};b.onerror=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:sendError",I);ne(s);w()};b.onabort=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:sendAbort",I);ne(s);w()};b.ontimeout=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:timeout",I);ne(s);w()};if(!fe(n,"htmx:beforeRequest",I)){ne(o);w();return l}var k=Yt(n);var P=Qt(n);ae(["loadstart","loadend","progress","abort"],function(t){ae([b,b.upload],function(e){e.addEventListener(t,function(e){fe(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});fe(n,"htmx:beforeSend",I);var K=q?null:gr(b,n,O);b.send(K);return l}function Rr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=re(e,"hx-push-url");var l=re(e,"hx-replace-url");var u=ie(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function qr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;if(!fe(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){Be(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){Wt();var r=f.getResponseHeader("HX-Location");var h;if(r.indexOf("{")===0){h=S(r);r=h["path"];delete h["path"]}Cr("GET",r,h).then(function(){$t(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){u.target=te().querySelector(f.getResponseHeader("HX-Retarget"))}var d=Rr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var v=f.response;var a=f.status>=400;var g=Y.config.ignoreTitle;var o=se({shouldSwap:i,serverResponse:v,isError:a,ignoreTitle:g},u);if(!fe(c,"htmx:beforeSwap",o))return;c=o.target;v=o.serverResponse;a=o.isError;g=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){Ke(l)}C(l,function(e){v=e.transformResponse(v,f,l)});if(d.type){Wt()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var h=dr(l,s);if(h.hasOwnProperty("ignoreTitle")){g=h.ignoreTitle}c.classList.add(Y.config.swappingClass);var m=null;var p=null;var x=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}var n=T(c);Ue(h.swapStyle,c,l,v,n,r);if(t.elt&&!oe(t.elt)&&Q(t.elt,"id")){var i=document.getElementById(Q(t.elt,"id"));var a={preventScroll:h.focusScroll!==undefined?!h.focusScroll:!Y.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Y.config.swappingClass);ae(n.elts,function(e){if(e.classList){e.classList.add(Y.config.settlingClass)}fe(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!oe(l)){o=te().body}Be(f,"HX-Trigger-After-Swap",o)}var s=function(){ae(n.tasks,function(e){e.call()});ae(n.elts,function(e){if(e.classList){e.classList.remove(Y.config.settlingClass)}fe(e,"htmx:afterSettle",u)});if(d.type){fe(te().body,"htmx:beforeHistoryUpdate",se({history:d},u));if(d.type==="push"){$t(d.path);fe(te().body,"htmx:pushedIntoHistory",{path:d.path})}else{Gt(d.path);fe(te().body,"htmx:replacedInHistory",{path:d.path})}}if(u.pathInfo.anchor){var e=te().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!g){var t=E("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}mr(n.elts,h);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!oe(l)){r=te().body}Be(f,"HX-Trigger-After-Settle",r)}ne(m)};if(h.settleDelay>0){setTimeout(s,h.settleDelay)}else{s()}}catch(e){ue(l,"htmx:swapError",u);ne(p);throw e}};var y=Y.config.globalViewTransitions;if(h.hasOwnProperty("transition")){y=h.transition}if(y&&fe(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var b=new Promise(function(e,t){m=e;p=t});var w=x;x=function(){document.startViewTransition(function(){w();return b})}}if(h.swapDelay>0){setTimeout(x,h.swapDelay)}else{x()}}if(a){ue(l,"htmx:responseError",se({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Hr={};function Lr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ar(e,t){if(t.init){t.init(r)}Hr[e]=se(Lr(),t)}function Nr(e){delete Hr[e]}function Ir(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=ee(e,"hx-ext");if(t){ae(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Hr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Ir(u(e),r,n)}var kr=false;te().addEventListener("DOMContentLoaded",function(){kr=true});function Pr(e){if(kr||te().readyState==="complete"){e()}else{te().addEventListener("DOMContentLoaded",e)}}function Mr(){if(Y.config.includeIndicatorStyles!==false){te().head.insertAdjacentHTML("beforeend","")}}function Dr(){var e=te().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function Xr(){var e=Dr();if(e){Y.config=se(Y.config,e)}}Pr(function(){Xr();Mr();var e=te().body;Dt(e);var t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ie(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){Kt();ae(t,function(e){fe(e,"htmx:restored",{document:te(),triggerEvent:fe})})}else{if(r){r(e)}}};setTimeout(function(){fe(e,"htmx:load",{});e=null},0)});return Y}()}); \ No newline at end of file diff --git a/js/idiomorph-ext.min.js b/js/idiomorph-ext.min.js new file mode 100644 index 0000000..ff1d8cd --- /dev/null +++ b/js/idiomorph-ext.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else{e.Idiomorph=e.Idiomorph||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";let o=new Set;function e(e,t,n={}){if(e instanceof Document){e=e.documentElement}if(typeof t==="string"){t=S(t)}let l=y(t);let r=c(e,l,n);return d(e,l,r)}function d(r,i,o){if(o.head.block){let t=r.querySelector("head");let n=i.querySelector("head");if(t&&n){let e=s(n,t,o);Promise.all(e).then(function(){d(r,i,Object.assign(o,{head:{block:false,ignore:true}}))});return}}if(o.morphStyle==="innerHTML"){l(i,r,o);return r.children}else if(o.morphStyle==="outerHTML"||o.morphStyle==null){let e=M(i,r,o);let t=e?.previousSibling;let n=e?.nextSibling;let l=u(r,e,o);if(e){return A(t,l,n)}else{return[]}}else{throw"Do not understand how to morph style "+o.morphStyle}}function f(e,t){return t.ignoreActiveValue&&e===document.activeElement}function u(e,t,n){if(n.ignoreActive&&e===document.activeElement){}else if(t==null){if(n.callbacks.beforeNodeRemoved(e)===false)return;e.remove();n.callbacks.afterNodeRemoved(e);return null}else if(!m(e,t)){if(n.callbacks.beforeNodeRemoved(e)===false)return;if(n.callbacks.beforeNodeAdded(t)===false)return;e.parentElement.replaceChild(t,e);n.callbacks.afterNodeAdded(t);n.callbacks.afterNodeRemoved(e);return t}else{if(n.callbacks.beforeNodeMorphed(e,t)===false)return;if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){s(t,e,n)}else{r(t,e,n);if(!f(e,n)){l(t,e,n)}}n.callbacks.afterNodeMorphed(e,t);return e}}function l(n,l,r){let i=n.firstChild;let o=l.firstChild;let d;while(i){d=i;i=d.nextSibling;if(o==null){if(r.callbacks.beforeNodeAdded(d)===false)return;l.appendChild(d);r.callbacks.afterNodeAdded(d);x(r,d);continue}if(h(d,o,r)){u(o,d,r);o=o.nextSibling;x(r,d);continue}let e=g(n,l,d,o,r);if(e){o=b(o,e,r);u(e,d,r);x(r,d);continue}let t=v(n,l,d,o,r);if(t){o=b(o,t,r);u(t,d,r);x(r,d);continue}if(r.callbacks.beforeNodeAdded(d)===false)return;l.insertBefore(d,o);r.callbacks.afterNodeAdded(d);x(r,d)}while(o!==null){let e=o;o=o.nextSibling;k(e,r)}}function r(e,t,n){let l=e.nodeType;if(l===1){const r=e.attributes;const i=t.attributes;for(const o of r){if(o.name==="value"&&f(t,n)){continue}if(t.getAttribute(o.name)!==o.value){t.setAttribute(o.name,o.value)}}for(const d of i){if(!e.hasAttribute(d.name)){t.removeAttribute(d.name)}}}if(l===8||l===3){if(t.nodeValue!==e.nodeValue){t.nodeValue=e.nodeValue}}if(!f(t,n)){a(e,t)}}function t(e,t,n){if(e[n]!==t[n]){if(e[n]){t.setAttribute(n,e[n])}else{t.removeAttribute(n)}}}function a(n,l){if(n instanceof HTMLInputElement&&l instanceof HTMLInputElement&&n.type!=="file"){l.value=n.value||"";t(n,l,"value");t(n,l,"checked");t(n,l,"disabled")}else if(n instanceof HTMLOptionElement){t(n,l,"selected")}else if(n instanceof HTMLTextAreaElement&&l instanceof HTMLTextAreaElement){let e=n.value;let t=l.value;if(e!==t){l.value=e}if(l.firstChild&&l.firstChild.nodeValue!==e){l.firstChild.nodeValue=e}}}function s(e,t,l){let r=[];let i=[];let o=[];let d=[];let f=l.head.style;let u=new Map;for(const n of e.children){u.set(n.outerHTML,n)}for(const s of t.children){let e=u.has(s.outerHTML);let t=l.head.shouldReAppend(s);let n=l.head.shouldPreserve(s);if(e||n){if(t){i.push(s)}else{u.delete(s.outerHTML);o.push(s)}}else{if(f==="append"){if(t){i.push(s);d.push(s)}}else{if(l.head.shouldRemove(s)!==false){i.push(s)}}}}d.push(...u.values());p("to append: ",d);let a=[];for(const c of d){p("adding: ",c);let n=document.createRange().createContextualFragment(c.outerHTML).firstChild;p(n);if(l.callbacks.beforeNodeAdded(n)!==false){if(n.href||n.src){let t=null;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});a.push(e)}t.appendChild(n);l.callbacks.afterNodeAdded(n);r.push(n)}}for(const h of i){if(l.callbacks.beforeNodeRemoved(h)!==false){t.removeChild(h);l.callbacks.afterNodeRemoved(h)}}l.head.afterHeadMorphed(t,{added:r,kept:o,removed:i});return a}function p(){}function i(){}function c(e,t,n){return{target:e,newContent:t,config:n,morphStyle:n.morphStyle,ignoreActive:n.ignoreActive,ignoreActiveValue:n.ignoreActiveValue,idMap:L(e,t),deadIds:new Set,callbacks:Object.assign({beforeNodeAdded:i,afterNodeAdded:i,beforeNodeMorphed:i,afterNodeMorphed:i,beforeNodeRemoved:i,afterNodeRemoved:i},n.callbacks),head:Object.assign({style:"merge",shouldPreserve:function(e){return e.getAttribute("im-preserve")==="true"},shouldReAppend:function(e){return e.getAttribute("im-re-append")==="true"},shouldRemove:i,afterHeadMorphed:i},n.head)}}function h(e,t,n){if(e==null||t==null){return false}if(e.nodeType===t.nodeType&&e.tagName===t.tagName){if(e.id!==""&&e.id===t.id){return true}else{return E(n,e,t)>0}}return false}function m(e,t){if(e==null||t==null){return false}return e.nodeType===t.nodeType&&e.tagName===t.tagName}function b(t,e,n){while(t!==e){let e=t;t=t.nextSibling;k(e,n)}x(n,e);return e.nextSibling}function g(n,e,l,r,i){let o=E(i,l,e);let t=null;if(o>0){let e=r;let t=0;while(e!=null){if(h(l,e,i)){return e}t+=E(i,e,n);if(t>o){return null}e=e.nextSibling}}return t}function v(e,t,n,l,r){let i=l;let o=n.nextSibling;let d=0;while(i!=null){if(E(r,i,e)>0){return null}if(m(n,i)){return i}if(m(o,i)){d++;o=o.nextSibling;if(d>=2){return null}}i=i.nextSibling}return i}function S(n){let l=new DOMParser;let e=n.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=l.parseFromString(n,"text/html");if(e.match(/<\/html>/)){t.generatedByIdiomorph=true;return t}else{let e=t.firstChild;if(e){e.generatedByIdiomorph=true;return e}else{return null}}}else{let e=l.parseFromString("","text/html");let t=e.body.querySelector("template").content;t.generatedByIdiomorph=true;return t}}function y(e){if(e==null){const t=document.createElement("div");return t}else if(e.generatedByIdiomorph){return e}else if(e instanceof Node){const t=document.createElement("div");t.append(e);return t}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}function A(e,t,n){let l=[];let r=[];while(e!=null){l.push(e);e=e.previousSibling}while(l.length>0){let e=l.pop();r.push(e);t.parentElement.insertBefore(e,t)}r.push(t);while(n!=null){l.push(n);r.push(n);n=n.nextSibling}while(l.length>0){t.parentElement.insertBefore(l.pop(),t.nextSibling)}return r}function M(e,t,n){let l;l=e.firstChild;let r=l;let i=0;while(l){let e=N(l,t,n);if(e>i){r=l;i=e}l=l.nextSibling}return r}function N(e,t,n){if(m(e,t)){return.5+E(n,e,t)}return 0}function k(e,t){x(t,e);if(t.callbacks.beforeNodeRemoved(e)===false)return;e.remove();t.callbacks.afterNodeRemoved(e)}function w(e,t){return!e.deadIds.has(t)}function T(e,t,n){let l=e.idMap.get(n)||o;return l.has(t)}function x(e,t){let n=e.idMap.get(t)||o;for(const l of n){e.deadIds.add(l)}}function E(e,t,n){let l=e.idMap.get(t)||o;let r=0;for(const i of l){if(w(e,i)&&T(e,i,n)){++r}}return r}function H(e,n){let l=e.parentElement;let t=e.querySelectorAll("[id]");for(const r of t){let t=r;while(t!==l&&t!=null){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(r.id);t=t.parentElement}}}function L(e,t){let n=new Map;H(e,n);H(t,n);return n}return{morph:e}}()});htmx.defineExtension("morph",{isInlineSwap:function(e){return e==="morph"},handleSwap:function(e,t,n){if(e==="morph"||e==="morph:outerHTML"){return Idiomorph.morph(t,n.children)}else if(e==="morph:innerHTML"){return Idiomorph.morph(t,n.children,{morphStyle:"innerHTML"})}}}); \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..b094c7a --- /dev/null +++ b/main.go @@ -0,0 +1,157 @@ +package main + +import ( + "embed" + "fmt" + "html/template" + "log" + "net/http" + "net/netip" + "strconv" + "strings" + "subv4/ipalloc" +) + +type server struct { + mux *http.ServeMux + ipdb *ipalloc.DB + tmpl *template.Template +} + +func (s *server) setup() { + s.mux = http.NewServeMux() + s.mux.HandleFunc("/js/", s.static()) + s.mux.HandleFunc("/css/", s.static()) + s.mux.HandleFunc("/", s.template) + s.mux.HandleFunc("/api/prefix/", s.apiPrefix) + s.mux.HandleFunc("/api/alloc/", s.apiAlloc) + s.mux.HandleFunc("/api/dealloc/", s.apiDealloc) + + s.mux.HandleFunc("/api/used/", s.apiUsed) + s.mux.HandleFunc("/api/print/", s.apiPrint) + //s.mux.HandleFunc("/api/provision/", s.apiProvision) + + s.ipdb = ipalloc.NewDB("10.83.46.0/23") + s.tmpl = template.Must(template.ParseFS(webcontent, "index.html")) +} + +func (s *server) apiPrefix(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + switch r.Method { + case "GET": + prefixString := strings.TrimPrefix(r.URL.Path, "/api/prefix/") + prefix, err := netip.ParsePrefix(prefixString) + fmt.Fprintln(w, prefix, err) + case "POST": + } +} + +type PageContent struct { + Api *ipalloc.DB + Update UpdateMessage +} +type UpdateMessage struct { + Type string + Content any +} + +func (s *server) template(w http.ResponseWriter, r *http.Request) { + err := s.tmpl.Execute(w, PageContent{Api: s.ipdb}) + if err != nil { + log.Println(err) + } +} +func (s *server) apiUsed(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + for _, used := range s.ipdb.Used() { + fmt.Fprintln(w, used) + } +} +func (s *server) apiPrint(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + for _, p := range s.ipdb.All() { + fmt.Fprintln(w, p.Prefix(), p.Leaf()) + } +} +func (s *server) apiAlloc(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + var updmsg UpdateMessage + + bitsString := strings.TrimPrefix(r.URL.Path, "/api/alloc/") + bits, err := strconv.Atoi(bitsString) + if err != nil { + updmsg.Type = "warn" + updmsg.Content = err + if err := s.tmpl.ExecuteTemplate(w, "update", updmsg); err != nil { + log.Println(err) + } + return + } + + alloc, err := s.ipdb.Alloc(bits) + + if err != nil { + updmsg.Type = "warn" + updmsg.Content = err + } else { + updmsg.Type = "hint" + updmsg.Content = fmt.Sprintf("Added %s", alloc.Prefix()) + } + if err := s.tmpl.ExecuteTemplate(w, "update", updmsg); err != nil { + log.Println(err) + } + + if err := s.tmpl.ExecuteTemplate(w, "used", s.ipdb); err != nil { + log.Println(err) + } +} +func (s *server) apiDealloc(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + var updmsg UpdateMessage + + prefixString := strings.TrimPrefix(r.URL.Path, "/api/dealloc/") + prefix, err := netip.ParsePrefix(prefixString) + if err != nil { + updmsg.Type = "warn" + updmsg.Content = err + s.tmpl.ExecuteTemplate(w, "update", updmsg) + return + } + + err = s.ipdb.Dealloc(prefix) + + if err != nil { + updmsg.Type = "warn" + updmsg.Content = err + } else { + updmsg.Type = "hint" + updmsg.Content = fmt.Sprintf("Removed %s", prefix) + } + s.tmpl.ExecuteTemplate(w, "update", updmsg) + s.tmpl.ExecuteTemplate(w, "used", s.ipdb) +} + +//go:embed js css *.html +var webcontent embed.FS + +func (s *server) static() http.HandlerFunc { + fs := http.FS(webcontent) + fileserver := http.FileServer(fs) + return fileserver.ServeHTTP +} + +func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL) + s.mux.ServeHTTP(w, r) +} + +func main() { + var s server + s.setup() + http.ListenAndServe("[::1]:8080", &s) +}