diff --git a/src/go/Makefile b/src/go/Makefile index 942f8a0..b466568 100644 --- a/src/go/Makefile +++ b/src/go/Makefile @@ -41,6 +41,12 @@ install-deps-dev: ## install dependencies for development @# https://aquasecurity.github.io/trivy/v0.18.3/installation/#install-script @which trivy || curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b $(TOOLS_DIR) v$(TRIVY_VERSION) @which actionlint || echo "install actionlint https://github.com/rhysd/actionlint" + @# https://github.com/swaggo/swag — dev-only, not a runtime dependency + @which swag || go install github.com/swaggo/swag/v2/cmd/swag@latest + +.PHONY: openapi +openapi: ## regenerate the OpenAPI spec from handler annotations (requires swag v2) + swag init --v3.1 -g cmd/serve/doc.go -o cmd/serve/webui --ot yaml .PHONY: format-check format-check: ## format check diff --git a/src/go/README.md b/src/go/README.md index bcd90c6..87592f6 100644 --- a/src/go/README.md +++ b/src/go/README.md @@ -69,6 +69,31 @@ health check. Unversioned `/tasks` endpoints remain available for compatibility. Like `run`, `serve` defaults to read-only permission approval; pass `--yolo` to change the server default, or set `yolo=true` on an individual task request. +#### Browser UI (Scalar API reference) + +Once the server is running, open **http://127.0.0.1:8080/docs/** in your +browser to access the built-in Scalar API reference UI. From there you can +explore all endpoints and send requests interactively using the built-in +try-console. + +Key points: + +- The UI is **fully offline** — the Scalar bundle and the OpenAPI spec are + embedded in the binary; no network access is required. +- The OpenAPI 3.1 spec is also available at `GET /swagger.yaml`. +- `GET /` redirects to `/docs/`. + +To regenerate the spec after changing handler annotations, install `swag` (once) +and run `make openapi`: + +```shell +# install swag v2 (dev tool only, not a runtime dependency) +make install-deps-dev + +# regenerate cmd/serve/webui/swagger.yaml from handler annotations +make openapi +``` + ## Development instructions ### Local development diff --git a/src/go/cmd/serve/doc.go b/src/go/cmd/serve/doc.go new file mode 100644 index 0000000..b24a646 --- /dev/null +++ b/src/go/cmd/serve/doc.go @@ -0,0 +1,31 @@ +/* +Copyright © 2024 ks6088ts + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Package serve implements the HTTP task server command. +// +// This file is the swag general-info anchor: `swag init -g cmd/serve/doc.go`. +// +// @title Copilot Task API +// @version 1.0 +// @description REST API for submitting GitHub Copilot tasks and tracking their progress. +// @BasePath /v1 +package serve diff --git a/src/go/cmd/serve/serve.go b/src/go/cmd/serve/serve.go index c3f2cfb..7be0de6 100644 --- a/src/go/cmd/serve/serve.go +++ b/src/go/cmd/serve/serve.go @@ -151,11 +151,17 @@ Endpoints: GET /v1/tasks/{id} Get the status, progress, and result of a task. GET /v1/tasks List submitted tasks. GET /healthz Health check. + GET /docs/ Interactive API browser (Scalar UI, offline). + GET /swagger.yaml OpenAPI 3.1 spec. Tasks run asynchronously in the background. Poll GET /v1/tasks/{id} until the status is "done" or "failed". The unversioned /tasks endpoints are also available for compatibility. +Open http://127.0.0.1:8080/docs/ in a browser to explore and try the API +interactively. All web assets are embedded in the binary; no network access is +required. + By default, only read permission requests are approved automatically. Pass --yolo to make task execution approve every tool permission request. @@ -208,10 +214,9 @@ func GetCommand() *cobra.Command { return serveCmd } -// startServer wires up the HTTP routes, starts the listener, and blocks until -// ctx is cancelled (Ctrl+C). -func startServer(ctx context.Context, addr, cliURL string, defaultYolo bool) error { - store := newTaskStore() +// newMux builds and returns a configured ServeMux with all API and UI routes +// registered. Extracting this from startServer makes the routing unit-testable. +func newMux(store *taskStore, cliURL string, defaultYolo bool) *http.ServeMux { mux := http.NewServeMux() createTask := func(w http.ResponseWriter, r *http.Request) { @@ -235,9 +240,20 @@ func startServer(ctx context.Context, addr, cliURL string, defaultYolo bool) err // GET /tasks — list all tasks mux.HandleFunc("GET /tasks", listTasks) mux.HandleFunc("GET /v1/tasks", listTasks) - mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { - writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) - }) + + // GET /healthz — health check + mux.HandleFunc("GET /healthz", handleHealthz) + + registerWebUI(mux) + + return mux +} + +// startServer wires up the HTTP routes, starts the listener, and blocks until +// ctx is cancelled (Ctrl+C). +func startServer(ctx context.Context, addr, cliURL string, defaultYolo bool) error { + store := newTaskStore() + mux := newMux(store, cliURL, defaultYolo) srv := &http.Server{ Addr: addr, @@ -460,7 +476,29 @@ func uploadFileName(field string, index int, filename string) string { return fmt.Sprintf("%s-%d-%s", field, index, base) } +// handleHealthz handles GET /healthz. +// +// @Summary Health check +// @Description Returns the health status of the server. +// @Tags system +// @Produce json +// @Success 200 {object} map[string]string "ok" +// @Router /healthz [get] +func handleHealthz(w http.ResponseWriter, _ *http.Request) { + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + // handleCreateTask handles POST /tasks. +// +// @Summary Create a task +// @Description Submit a new Copilot task. Accepts JSON or multipart/form-data. +// @Tags tasks +// @Accept json +// @Produce json +// @Param task body taskRequest true "Task request" +// @Success 202 {object} taskCreatedResponse "Task created" +// @Failure 400 {object} map[string]string "Bad request" +// @Router /tasks [post] func handleCreateTask(w http.ResponseWriter, r *http.Request, store *taskStore, defaultCLIURL string, defaultYolo bool) { req, cleanup, err := parseTaskRequest(r) if err != nil { @@ -527,6 +565,15 @@ func handleCreateTask(w http.ResponseWriter, r *http.Request, store *taskStore, } // handleGetTask handles GET /tasks/{id}. +// +// @Summary Get a task +// @Description Get the current status, progress, and result of a task. +// @Tags tasks +// @Produce json +// @Param id path string true "Task ID" +// @Success 200 {object} Task "Task state" +// @Failure 404 {object} map[string]string "Task not found" +// @Router /tasks/{id} [get] func handleGetTask(w http.ResponseWriter, r *http.Request, store *taskStore) { id := r.PathValue("id") task, ok := store.get(id) @@ -538,6 +585,13 @@ func handleGetTask(w http.ResponseWriter, r *http.Request, store *taskStore) { } // handleListTasks handles GET /tasks. +// +// @Summary List tasks +// @Description List all submitted tasks. +// @Tags tasks +// @Produce json +// @Success 200 {array} Task "List of tasks" +// @Router /tasks [get] func handleListTasks(w http.ResponseWriter, _ *http.Request, store *taskStore) { store.mu.RLock() tasks := make([]Task, 0, len(store.tasks)) diff --git a/src/go/cmd/serve/serve_test.go b/src/go/cmd/serve/serve_test.go index 12bda94..de3d2d7 100644 --- a/src/go/cmd/serve/serve_test.go +++ b/src/go/cmd/serve/serve_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "os" + "strings" "testing" ) @@ -89,3 +90,73 @@ func assertFileExists(t *testing.T, path string) { t.Fatalf("os.Stat(%q) error = %v", path, err) } } + +// newTestMux creates a ServeMux backed by a fresh in-memory store. It is used +// by the route-level tests to exercise the full routing table without starting +// a real TCP server. +func newTestMux() *http.ServeMux { + return newMux(newTaskStore(), "", false) +} + +func TestDocsRouteReturnsHTML(t *testing.T) { + mux := newTestMux() + req := httptest.NewRequest(http.MethodGet, "/docs/", nil) + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("GET /docs/ status = %d, want 200", w.Code) + } + ct := w.Header().Get("Content-Type") + if !strings.HasPrefix(ct, "text/html") { + t.Fatalf("GET /docs/ Content-Type = %q, want text/html prefix", ct) + } +} + +func TestSwaggerYAMLRouteReturnsOpenAPISpec(t *testing.T) { + mux := newTestMux() + req := httptest.NewRequest(http.MethodGet, "/swagger.yaml", nil) + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("GET /swagger.yaml status = %d, want 200", w.Code) + } + body := w.Body.String() + if !strings.Contains(body, "openapi:") { + t.Fatalf("GET /swagger.yaml body does not contain 'openapi:' key") + } + if !strings.Contains(body, "paths:") { + t.Fatalf("GET /swagger.yaml body does not contain 'paths:' key") + } +} + +func TestRootRedirectsToDocs(t *testing.T) { + mux := newTestMux() + req := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + if w.Code != http.StatusFound && w.Code != http.StatusMovedPermanently && w.Code/100 != 3 { + t.Fatalf("GET / status = %d, want 3xx redirect", w.Code) + } + loc := w.Header().Get("Location") + if loc != "/docs/" { + t.Fatalf("GET / Location = %q, want /docs/", loc) + } +} + +func TestHealthzRoute(t *testing.T) { + mux := newTestMux() + req := httptest.NewRequest(http.MethodGet, "/healthz", nil) + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("GET /healthz status = %d, want 200", w.Code) + } + body := w.Body.String() + if !strings.Contains(body, `"ok"`) { + t.Fatalf("GET /healthz body = %q, want body containing \"ok\"", body) + } +} diff --git a/src/go/cmd/serve/webui.go b/src/go/cmd/serve/webui.go new file mode 100644 index 0000000..1037fb9 --- /dev/null +++ b/src/go/cmd/serve/webui.go @@ -0,0 +1,61 @@ +/* +Copyright © 2024 ks6088ts + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package serve + +import ( + "embed" + "io/fs" + "net/http" +) + +//go:embed webui +var webuiFS embed.FS + +// registerWebUI registers the static web UI and OpenAPI spec routes on mux. +// +// - GET /docs/ — serves the embedded Scalar API reference page. +// - GET /swagger.yaml — serves the embedded OpenAPI 3.1 spec. +// - GET /{$} — redirects the root path to /docs/. +func registerWebUI(mux *http.ServeMux) { + sub, err := fs.Sub(webuiFS, "webui") + if err != nil { + // This can only fail if the embed directive is wrong, which would be a + // compile-time/init-time programming error rather than a runtime error. + panic("webui: fs.Sub failed: " + err.Error()) + } + + fileServer := http.FileServerFS(sub) + + // Serve the full /docs/ tree (index.html, scalar.standalone.js, etc.) + mux.Handle("GET /docs/", http.StripPrefix("/docs/", fileServer)) + + // Expose the OpenAPI spec at the root level for convenience. + mux.HandleFunc("GET /swagger.yaml", func(w http.ResponseWriter, r *http.Request) { + r.URL.Path = "/swagger.yaml" + fileServer.ServeHTTP(w, r) + }) + + // Redirect root to the docs UI. + mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/docs/", http.StatusFound) + }) +} diff --git a/src/go/cmd/serve/webui/index.html b/src/go/cmd/serve/webui/index.html new file mode 100644 index 0000000..dde7478 --- /dev/null +++ b/src/go/cmd/serve/webui/index.html @@ -0,0 +1,21 @@ + + +
+ + +{let{slotScopeIds:u}=t;u&&(i=i?i.concat(u):u);let d=o(e),f=p(a(e),t,d,n,r,i,s);return f&&Fi(f)&&f.data===`]`?a(t.anchor=f):(Ni(),c(t.anchor=l(`]`),d,f),f)},h=(e,t,r,i,c,l)=>{if(Tr(e.parentElement,1)||Ni(),t.el=null,l){let t=g(e);for(;;){let n=a(e);if(n&&n!==t)s(n);else break}}let u=a(e),d=o(e);return s(e),n(null,t,d,u,r,i,Pi(d),c),r&&(r.vnode.el=t.el,Dne(r,t.el)),u},g=(e,t=`[`,n=`]`)=>{let r=0;for(;e;)if(e=a(e),e&&Fi(e)&&(e.data===t&&r++,e.data===n)){if(r===0)return a(e);r--}return e},_=(e,t,n)=>{let r=t.parentNode;r&&r.replaceChild(e,t);let i=n;for(;i;)i.vnode.el===t&&(i.vnode.el=i.subTree.el=e),i=i.parent},v=e=>e.nodeType===1&&e.tagName===`TEMPLATE`;return[u,d]}function Tr(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(Ii);)e=e.parentElement;let n=e&&e.getAttribute(Ii);if(n==null)return!1;if(n===``)return!0;{let e=n.split(`,`);return t===0&&e.includes(`children`)?!0:e.includes(sre[t])}}function ene(e,t){if(Fi(e)&&e.data===`[`){let n=1,r=e.nextSibling;for(;r;){if(r.nodeType===1){if(t(r)===!1)break}else if(Fi(r))if(r.data===`]`){if(--n===0)break}else r.data===`[`&&n++;r=r.nextSibling}}else t(e)}function tne(e){yt(e)&&(e={loader:e});let{loader:t,loadingComponent:n,errorComponent:r,delay:i=200,hydrate:a,timeout:o,suspensible:s=!0,onError:c}=e,l=null,u,d=0,f=()=>(d++,l=null,p()),p=()=>{let e;return l||(e=l=t().catch(e=>{if(e=e instanceof Error?e:Error(String(e)),c)return new Promise((t,n)=>{c(e,()=>t(f()),()=>n(e),d+1)});throw e}).then(t=>e!==l&&l?l:(t&&(t.__esModule||t[Symbol.toStringTag]===`Module`)&&(t=t.default),u=t,t)))};return N({name:`AsyncComponentWrapper`,__asyncLoader:p,__asyncHydrate(e,t,n){let r=!1;(t.bu||=[]).push(()=>r=!0);let i=()=>{r||n()},o=a?()=>{let n=a(i,t=>ene(e,t));n&&(t.bum||=[]).push(n)}:i;u?o():p().then(()=>!t.isUnmounted&&o())},get __asyncResolved(){return u},setup(){let e=va;if(Sr(e),u)return()=>Er(u,e);let t=t=>{l=null,rr(t,e,13,!r)};if(s&&e.suspense||wa)return p().then(t=>()=>Er(t,e)).catch(e=>(t(e),()=>r?H(r,{error:e}):null));let a=A(!1),c=A(),d=A(!!i);return i&&setTimeout(()=>{d.value=!1},i),o!=null&&setTimeout(()=>{if(!a.value&&!c.value){let e=Error(`Async component timed out after ${o}ms.`);t(e),c.value=e}},o),p().then(()=>{a.value=!0,e.parent&&Ri(e.parent.vnode)&&e.parent.update()}).catch(e=>{t(e),c.value=e}),()=>{if(a.value&&u)return Er(u,e);if(c.value&&r)return H(r,{error:c.value});if(n&&!d.value)return Er(n,e)}}})}function Er(e,t){let{ref:n,props:r,children:i,ce:a}=t.vnode,o=H(e,r,i);return o.ref=n,o.ce=a,delete t.vnode.ce,o}function nne(e,t){ine(e,`a`,t)}function rne(e,t){ine(e,`da`,t)}function ine(e,t,n=va){let r=e.__wdc||=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()};if(Dr(t,r,n),n){let e=n.parent;for(;e&&e.parent;)Ri(e.parent.vnode)&&ane(r,t,n,e),e=e.parent}}function ane(e,t,n,r){let i=Dr(t,e,r,!0);Wi(()=>{pt(r[t],i)},n)}function Dr(e,t,n=va,r=!1){if(n){let i=n[e]||(n[e]=[]),a=t.__weh||=(...r)=>{Xt();let i=Sa(n),a=nr(t,n,e,r);return i(),Zt(),a};return r?i.unshift(a):i.push(a),a}}function one(e,t=va){Dr(`ec`,e,t)}function Or(e,t){return sne(Ki,e,!0,t)||e}function kr(e){return bt(e)?sne(Ki,e,!1)||e:e||qi}function sne(e,t,n=!0,r=!1){let i=gi||va;if(i){let n=i.type;if(e===Ki){let e=Qne(n,!1);if(e&&(e===t||e===At(t)||e===Mt(At(t))))return n}let a=cne(i[e]||n[e],t)||cne(i.appContext[e],t);return!a&&r?n:a}}function cne(e,t){return e&&(e[t]||e[At(t)]||e[Mt(At(t))])}function Ar(e,t,n,r){let i,a=n&&n[r],o=ht(e);if(o||bt(e)){let n=o&&mn(e),r=!1,s=!1;n&&(r=!gn(e),s=hn(e),e=tn(e)),i=Array(e.length);for(let n=0,o=e.length;n t;){let n=!0,u=!1;if(!l||c>a[l-1].to){let e=Aj[c-1];e!=s&&(n=!1,u=e==16)}let d=!n&&s==1?[]:null,f=n?r:r+1,p=c;run:for(;;)if(l&&p==a[l-1].to){if(u)break run;let m=a[--l];if(!n)for(let e=m.from,n=l;;){if(e==t)break run;if(n&&a[n-1].to==e)e=a[--n].from;else if(Aj[e-1]==s)break run;else break}d?d.push(m):(m.toe.slice(1,-1).indexOf(`:`)===-1?void 0:(()=>{let t=e.slice(1,-1),n=t.indexOf(`:`),r=t.slice(0,n);return r?l_e+r:void 0})(),f_e=e=>{let{theme:t,classGroups:n}=e;return p_e(n,t)},p_e=(e,t)=>{let n=md();for(let r in e){let i=e[r];vd(i,n,r,t)}return n},vd=(e,t,n,r)=>{let i=e.length;for(let a=0;a{if(typeof e==`string`){h_e(e,t,n);return}if(typeof e==`function`){g_e(e,t,n,r);return}__e(e,t,n,r)},h_e=(e,t,n)=>{let r=e===``?t:yd(t,e);r.classGroupId=n},g_e=(e,t,n,r)=>{if(v_e(e)){vd(e(r),t,n,r);return}t.validators===null&&(t.validators=[]),t.validators.push(c_e(n,e))},__e=(e,t,n,r)=>{let i=Object.entries(e),a=i.length;for(let e=0;e{let n=e,r=t.split(hd),i=r.length;for(let e=0;e`isThemeGetter`in e&&e.isThemeGetter===!0,y_e=e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let t=0,n=Object.create(null),r=Object.create(null),i=(i,a)=>{n[i]=a,t++,t>e&&(t=0,r=n,n=Object.create(null))};return{get(e){let t=n[e];if(t!==void 0)return t;if((t=r[e])!==void 0)return i(e,t),t},set(e,t){e in n?n[e]=t:i(e,t)}}},bd=`!`,xd=`:`,b_e=[],Sd=(e,t,n,r,i)=>({modifiers:e,hasImportantModifier:t,baseClassName:n,maybePostfixModifierPosition:r,isExternal:i}),x_e=e=>{let{prefix:t,experimentalParseClassName:n}=e,r=e=>{let t=[],n=0,r=0,i=0,a,o=e.length;for(let s=0;si&&(r+=o.slice(Math.max(0,e-i),t-i)),i=s+1}return r}flatten(e){for(let t of this.text)e.push(t)}scanIdentical(){return 0}static split(t,n){let r=[],i=-1;for(let a of t)r.push(a),i+=a.length+1,r.length==32&&(n.push(new e(r,i)),r=[],i=-1);return i>-1&&n.push(new e(r,i)),n}},fk=class e extends uk{constructor(e,t){super(),this.children=e,this.length=t,this.lines=0;for(let t of e)this.lines+=t.lines}lineInner(e,t,n,r){for(let i=0;;i++){let a=this.children[i],o=r+a.length,s=n+a.lines-1;if((t?s:o)>=e)return a.lineInner(e,t,n,r);r=o+1,n=s+1}}decompose(e,t,n,r){for(let i=0,a=0;a<=t&&ia&&(r+=o.sliceString(e-a,t-a,n)),a=s+1}return r}flatten(e){for(let t of this.children)t.flatten(e)}scanIdentical(t,n){if(!(t instanceof e))return 0;let r=0,[i,a,o,s]=n>0?[0,0,this.children.length,t.children.length]:[this.children.length-1,t.children.length-1,-1,-1];for(;;i+=n,a+=n){if(i==o||a==s)return r;let e=this.children[i],c=t.children[a];if(e!=c)return r+e.scanIdentical(c,n);r+=e.length+1}}static from(t,n=t.reduce((e,t)=>e+t.length+1,-1)){let r=0;for(let e of t)r+=e.lines;if(r<32){let e=[];for(let n of t)n.flatten(e);return new dk(e,n)}let i=Math.max(32,r>>5),a=i<<1,o=i>>1,s=[],c=0,l=-1,u=[];function d(t){let n;if(t.lines>a&&t instanceof e)for(let e of t.children)d(e);else t.lines>o&&(c>o||!c)?(f(),s.push(t)):t instanceof dk&&c&&(n=u[u.length-1])instanceof dk&&t.lines+n.lines<=32?(c+=t.lines,l+=t.length+1,u[u.length-1]=new dk(n.text.concat(t.text),n.length+1+t.length)):(c+t.lines>i&&f(),c+=t.lines,l+=t.length+1,u.push(t))}function f(){c!=0&&(s.push(u.length==1?u[0]:e.from(u,l)),l=-1,c=u.length=0)}for(let e of t)d(e);return f(),s.length==1?s[0]:new e(s,n)}},uk.empty=new dk([``],0),pk=class{constructor(e,t=1){this.dir=t,this.done=!1,this.lineBreak=!1,this.value=``,this.nodes=[e],this.offsets=[t>0?1:(e instanceof dk?e.text.length:e.children.length)<<1]}nextInner(e,t){for(this.done=this.lineBreak=!1;;){let n=this.nodes.length-1,r=this.nodes[n],i=this.offsets[n],a=i>>1,o=r instanceof dk?r.text.length:r.children.length;if(a==(t>0?o:0)){if(n==0)return this.done=!0,this.value=``,this;t>0&&this.offsets[n-1]++,this.nodes.pop(),this.offsets.pop()}else if((i&1)==(t>0?0:1)){if(this.offsets[n]+=t,e==0)return this.lineBreak=!0,this.value=`
+`,this;e--}else if(r instanceof dk){let i=r.text[a+(t<0?-1:0)];if(this.offsets[n]+=t,i.length>Math.max(0,e))return this.value=e==0?i:t>0?i.slice(e):i.slice(0,i.length-e),this;e-=i.length}else{let i=r.children[a+(t<0?-1:0)];e>i.length?(e-=i.length,this.offsets[n]+=t):(t<0&&this.offsets[n]--,this.nodes.push(i),this.offsets.push(t>0?1:(i instanceof dk?i.text.length:i.children.length)<<1))}}}next(e=0){return e<0&&(this.nextInner(-e,-this.dir),e=this.value.length),this.nextInner(e,this.dir)}},mk=class{constructor(e,t,n){this.value=``,this.done=!1,this.cursor=new pk(e,t>n?-1:1),this.pos=t>n?e.length:0,this.from=Math.min(t,n),this.to=Math.max(t,n)}nextInner(e,t){if(t<0?this.pos<=this.from:this.pos>=this.to)return this.value=``,this.done=!0,this;e+=Math.max(0,t<0?this.pos-this.to:this.from-this.pos);let n=t<0?this.pos-this.from:this.to-this.pos;e>n&&(e=n),n-=e;let{value:r}=this.cursor.next(e);return this.pos+=(r.length+e)*t,this.value=r.length<=n?r:t<0?r.slice(r.length-n):r.slice(0,n),this.done=!this.value,this}next(e=0){return e<0?e=Math.max(e,this.from-this.pos):e>0&&(e=Math.min(e,this.to-this.pos)),this.nextInner(e,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=``}},hk=class{constructor(e){this.inner=e,this.afterBreak=!0,this.value=``,this.done=!1}next(e=0){let{done:t,lineBreak:n,value:r}=this.inner.next(e);return t&&this.afterBreak?(this.value=``,this.afterBreak=!1):t?(this.done=!0,this.value=``):n?this.afterBreak?this.value=``:(this.afterBreak=!0,this.next()):(this.value=r,this.afterBreak=!1),this}get lineBreak(){return!1}},typeof Symbol<`u`&&(uk.prototype[Symbol.iterator]=function(){return this.iter()},pk.prototype[Symbol.iterator]=mk.prototype[Symbol.iterator]=hk.prototype[Symbol.iterator]=function(){return this}),L4e=class{constructor(e,t,n,r){this.from=e,this.to=t,this.number=n,this.text=r}get length(){return this.to-this.from}},gk=/\r\n?|\n/,_k=(function(e){return e[e.Simple=0]=`Simple`,e[e.TrackDel=1]=`TrackDel`,e[e.TrackBefore=2]=`TrackBefore`,e[e.TrackAfter=3]=`TrackAfter`,e})(_k||={}),vk=class e{constructor(e){this.sections=e}get length(){let e=0;for(let t=0;t0&&qO(r,n,a.text),a.forward(e),o+=e}let c=t[e++];for(;o