diff --git a/CLAUDE.md b/CLAUDE.md index 296d77da..0a6ebc68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,28 +87,35 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched) and meta read access; mirrors each applied repo into `agent-configs/` (core-only); agents are read-only collaborators on `core/meta` - src/dashboard.rs axum HTTP: /api/state JSON + actions + src/dashboard.rs axum HTTP: static shell + /api/state JSON + actions + journald viewer + bind-with-retry (SO_REUSEADDR) + deployed_sha chip per container + /dashboard/{stream,history} subscribing to the - unified DashboardEvent channel. Static assets - (HTML/CSS/JS/favicon) served by - tower_http::ServeDir from $HIVE_STATIC_DIR - (= `${frontend}/dashboard` per the c0re module). + unified DashboardEvent channel + assets/ index.html, dashboard.css, app.js (include_str!) -frontend/ npm workspaces (esbuild → static dist). Built - hermetically by `nix/frontend.nix` - (`packages.${system}.frontend`). - packages/shared/ @hive/shared: terminal pane + Catppuccin palette - + base typography (was hive-fr0nt). ES module - exporting { create, linkify }; pure JS, no IIFE - globals; consumed by dashboard + agent. - packages/dashboard/ @hive/dashboard SPA: src/{index.html, app.js, - dashboard.css} + build.mjs → dist/{index.html, - static/{app.js, dashboard.css}}. - packages/agent/ @hive/agent default per-container UI: src/ - {index, stats, screen}.html + {app, stats}.js - + agent.css → dist/{*.html, static/*}. +hive-fr0nt/ shared frontend-assets crate (browser only). + src/lib.rs pub const BASE_CSS / TERMINAL_CSS / TERMINAL_JS / + MARKED_JS re-exports; both binaries + `include_str!` them and prepend to their per- + page serving routes. + assets/base.css Catppuccin palette + body typography (one source + of truth, no per-page redeclaration). + assets/terminal.css `.terminal-wrap` + `.live` + `.tail-pill` + + `.row` / `details.row` styling for both + pages' lit log panes. Unified prefix-column + (padding-left + negative text-indent) so glyph + alignment is consistent across row kinds + a + `.md` block scope for marked-rendered bodies. + assets/terminal.js `window.HiveTerminal.create(opts)`: scroll- + sticky log + "↓ N new" pill + history + backfill + SSE subscribe-buffer-snapshot- + dedupe dance. Pages register a kind→renderer + map; the terminal owns the lifecycle. + assets/marked.umd.js vendored marked v4.0.2 UMD bundle. Per-agent + terminal uses the global `marked.parse` for + markdown bodies on send / recv / ask / answer + / assistant text rows. hive-ag3nt/ in-container harness crate; produces TWO binaries src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT @@ -132,10 +139,8 @@ hive-ag3nt/ in-container harness crate; produces TWO binaries src/login_session.rs drives `claude auth login` over stdio pipes src/bin/hive-ag3nt.rs sub-agent main (Serve + Mcp subcommands) src/bin/hive-m1nd.rs manager main (Serve + Mcp subcommands) - Static UI assets served by ServeDir from - $HIVE_STATIC_DIR (= hyperhive.frontend - .mergedDist — default agent dist + per-agent - extraFiles, set per the harness-base module). + assets/ index.html, agent.css, app.js, stats.html, + stats.js, screen.html (include_str!) prompts/ static role/tools/settings for claude (include_str!): agent.md — sub-agent system prompt manager.md — manager system prompt diff --git a/Cargo.lock b/Cargo.lock index 9e8e75be..3083bdb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,6 +565,7 @@ dependencies = [ "axum", "clap", "futures-util", + "hive-fr0nt", "hive-sh4re", "reqwest", "rmcp", @@ -574,7 +575,6 @@ dependencies = [ "serde_json", "tokio", "tokio-stream", - "tower-http", "tracing", "tracing-subscriber", ] @@ -587,6 +587,7 @@ dependencies = [ "axum", "base64", "clap", + "hive-fr0nt", "hive-sh4re", "libc", "reqwest", @@ -596,11 +597,14 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", - "tower-http", "tracing", "tracing-subscriber", ] +[[package]] +name = "hive-fr0nt" +version = "0.1.0" + [[package]] name = "hive-sh4re" version = "0.1.0" @@ -641,12 +645,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - [[package]] name = "httparse" version = "1.10.1" @@ -956,16 +954,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "mio" version = "1.2.0" @@ -1735,19 +1723,10 @@ checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", - "futures-core", "futures-util", "http", "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", "pin-project-lite", - "tokio", - "tokio-util", "tower", "tower-layer", "tower-service", @@ -1856,12 +1835,6 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index c3880048..a53b5b9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["hive-ag3nt", "hive-c0re", "hive-sh4re"] +members = ["hive-ag3nt", "hive-c0re", "hive-fr0nt", "hive-sh4re"] [workspace.package] edition = "2024" @@ -19,8 +19,8 @@ anyhow = "1" axum = { version = "0.8", features = ["ws"] } base64 = "0.22" clap = { version = "4", features = ["derive"] } +hive-fr0nt = { path = "hive-fr0nt" } hive-sh4re = { path = "hive-sh4re" } -tower-http = { version = "0.6", features = ["fs"] } rmcp = { version = "1.7", default-features = false, features = [ "server", "macros", diff --git a/docs/terminal-rendering.md b/docs/terminal-rendering.md index fce8d21a..c5ebaa73 100644 --- a/docs/terminal-rendering.md +++ b/docs/terminal-rendering.md @@ -2,11 +2,11 @@ Snapshot of how the per-agent web UI's live pane renders each event kind today. Source of truth lives in -`frontend/packages/agent/src/app.js` (`renderStream`, `fmtToolUse`, +`hive-ag3nt/assets/app.js` (`renderStream`, `fmtToolUse`, `renderRichToolUse`, `renderToolResult`, `renderTaskEvent`, `mdNode`, `detailsOpenMd`, `fmtArgsGeneric`) + -`frontend/packages/shared/src/terminal.css` (the shared -`.live .` styling) + the `marked` npm package (markdown). +`hive-fr0nt/assets/terminal.css` (the shared `.live .` +styling) + `hive-fr0nt/assets/marked.umd.js` (markdown). ## Layout contract @@ -82,8 +82,8 @@ parent's negative pull. ## Markdown -`mdNode(text)` wraps `marked.parse(text)` (the `marked` v4.x npm -dep, bundled by esbuild into the page's `app.js`) in a `
`. CSS in `terminal.css` scopes paragraph / code / list / blockquote / link styling under `.live .row .md` so the markdown body doesn't bleed into the row's own diff --git a/docs/web-ui.md b/docs/web-ui.md index a0a555df..61b4fa31 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -7,32 +7,22 @@ and the per-agent UIs (manager on :8000, sub-agents on a hashed ## Shape (shared by both) -- `GET /` → `index.html` from the bundled frontend dist (see - `frontend/`). Both binaries' routers declare their dynamic - endpoints first and then `fallback_service(ServeDir::new(...))` - pointed at `HIVE_STATIC_DIR` — anything not matched by an API or - action route is served from the dist. Dashboard dist lives at - `${frontend}/dashboard`; per-agent dist is the merged - `hyperhive.frontend.mergedDist` (default agent dist + per-agent - `extraFiles` overlay). -- `GET /static/*` → bundled CSS + JS produced by esbuild - (`frontend/packages/{dashboard,agent}/build.mjs`). Both pages - pull the shared terminal pane + Catppuccin palette + typography - from `@hive/shared` (was `hive-fr0nt`); the CSS bundle inlines - `base.css` + `terminal.css` via esbuild's `@import` resolution. - `terminal.js` exports `{ create, linkify }` as ES module - members (no more `window.HiveTerminal` global outside the - back-compat shim the IIFE bodies still use). The dashboard's +- `GET /` → `assets/index.html` (placeholders for state-driven + sections, shipped via `include_str!` so the binary has no runtime + file dependency). +- `GET /static/*.css` + `GET /static/*.js` → static assets. Both + pages prepend `hive_fr0nt::BASE_CSS` + `TERMINAL_CSS` to their + per-page stylesheet, and `GET /static/hive-fr0nt.js` serves the + shared `window.HiveTerminal.create` runtime. The dashboard's `#msgflow` and the per-agent `#live` log are both backed by this terminal — sticky-bottom auto-scroll, "↓ N new" pill, history backfill, SSE plumbing all live there. Each page registers a kind→renderer map; unknown kinds fall through to a JSON-dump note row. Bare `http(s)://` URLs in row text are - turned into clickable new-tab links by `linkify` (text-node - based, no `innerHTML` — XSS-safe); markdown bodies get the - same treatment via `marked`'s autolink (npm dep, replacing the - vendored UMD bundle), with the rendered ``s rewritten to - `target="_blank"` (issue #233). + turned into clickable new-tab links by `HiveTerminal.linkify` + (text-node based, no `innerHTML` — XSS-safe); markdown bodies + get the same treatment via `marked`'s autolink, with the + rendered ``s rewritten to `target="_blank"` (issue #233). - `GET /api/state` → JSON snapshot the JS app renders into the DOM. Includes a top-level `seq` (the dashboard event channel's high-water mark at the moment the snapshot was assembled); diff --git a/flake.nix b/flake.nix index 73d24bdd..db3efb4d 100644 --- a/flake.nix +++ b/flake.nix @@ -51,31 +51,18 @@ in { packages = forAllSystems ( - { pkgs, naersk-lib, ... }: + { naersk-lib, ... }: { default = naersk-lib.buildPackage { src = ./.; meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-m1nd)"; }; - # Bundled browser assets — see ./nix/frontend.nix. Output is - # $out/{dashboard,agent}/ which the Rust binaries serve via - # tower_http::ServeDir (wired up in Phase 4 of #273). - frontend = pkgs.callPackage ./nix/frontend.nix { - branding-svg = ./branding/hyperhive.svg; - }; } ); overlays = { default = final: prev: { hyperhive = self.packages.${prev.stdenv.hostPlatform.system}.default; - # Bundled frontend dist (see ./nix/frontend.nix). Output is - # $out/{dashboard,agent}/; consumers pick the surface they - # need. Exposed via the overlay so containers' nix evaluations - # can reach it as `pkgs.hyperhive-frontend` once the overlay - # is applied (manager + agent containers both apply it via - # `mkContainer` further down). - hyperhive-frontend = self.packages.${prev.stdenv.hostPlatform.system}.frontend; }; claude-unstable = final: prev: @@ -109,7 +96,6 @@ # builds (already applied internally in `nixosConfigurations`). hive-c0re = import ./nix/modules/hive-c0re.nix { hyperhivePackage = system: self.packages.${system}.default; - hyperhiveFrontend = system: self.packages.${system}.frontend; hyperhiveFlake = "${self}"; }; hive-forge = ./nix/modules/hive-forge.nix; diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 31994d81..00000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -packages/*/dist/ diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 5564ab0a..00000000 --- a/frontend/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# hyperhive frontend - -npm workspaces project for the hyperhive browser-facing assets: - -- `packages/shared/` — shared modules used by both surfaces (terminal - pane, Catppuccin palette + body typography). -- `packages/dashboard/` — the hive-c0re dashboard SPA. -- `packages/agent/` — the per-container web UI (default agent page, - stats, screen). - -## Build - -``` -npm install # one-off; uses the checked-in package-lock.json -npm run build # builds every workspace into packages/*/dist/ -``` - -The Rust binaries serve `packages/dashboard/dist/` and -`packages/agent/dist/` via `tower_http::ServeDir` at runtime; the -build derivation is wired up in `nix/modules/frontend.nix`. Per-agent -additions are layered on top of the default agent dist via the -`hyperhive.frontend.extraFiles` option in `agent.nix`. - -## Why npm + esbuild - -- **Hermetic**: dependencies vendored via the checked-in lockfile; - `buildNpmPackage` in nix uses it as the source-of-truth so the - output is reproducible without network access at build time. -- **esbuild**: vanilla-JS bundler, no framework runtime overhead. - Each workspace's `build.mjs` is ~30 lines. -- **Single-PR migration**: see issue #273 for the design proposal and - the four-commit shape (npm scaffold → nix derivations → container - plumbing → Rust cutover). diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index a2ffd71c..00000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,549 +0,0 @@ -{ - "name": "hyperhive-frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "hyperhive-frontend", - "version": "0.0.0", - "workspaces": [ - "packages/shared", - "packages/dashboard", - "packages/agent" - ], - "devDependencies": { - "esbuild": "0.25.5" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@hive/agent": { - "resolved": "packages/agent", - "link": true - }, - "node_modules/@hive/dashboard": { - "resolved": "packages/dashboard", - "link": true - }, - "node_modules/@hive/shared": { - "resolved": "packages/shared", - "link": true - }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "license": "MIT" - }, - "node_modules/chart.js": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", - "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } - }, - "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" - } - }, - "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "packages/agent": { - "name": "@hive/agent", - "version": "0.0.0", - "dependencies": { - "@hive/shared": "*", - "chart.js": "4.4.4", - "marked": "4.3.0" - } - }, - "packages/dashboard": { - "name": "@hive/dashboard", - "version": "0.0.0", - "dependencies": { - "@hive/shared": "*", - "marked": "4.3.0" - } - }, - "packages/shared": { - "name": "@hive/shared", - "version": "0.0.0" - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 282a6480..00000000 --- a/frontend/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "hyperhive-frontend", - "version": "0.0.0", - "private": true, - "description": "Frontend assets for the hyperhive dashboard and per-agent UIs. Built with esbuild into static dist directories that the Rust binaries serve via tower_http::ServeDir.", - "workspaces": [ - "packages/shared", - "packages/dashboard", - "packages/agent" - ], - "scripts": { - "build": "npm run build --workspaces --if-present", - "clean": "rm -rf packages/*/dist" - }, - "devDependencies": { - "esbuild": "0.25.5" - } -} diff --git a/frontend/packages/agent/build.mjs b/frontend/packages/agent/build.mjs deleted file mode 100644 index 6b231f38..00000000 --- a/frontend/packages/agent/build.mjs +++ /dev/null @@ -1,60 +0,0 @@ -// esbuild build for @hive/agent. Output layout (`dist/`): -// -// dist/index.html served at GET / -// dist/stats.html served at GET /stats -// dist/screen.html served at GET /screen -// dist/static/app.js served at /static/app.js (ESM bundle, -// pulls in @hive/shared + marked) -// dist/static/app.js.map source map sibling -// dist/static/stats.js served at /static/stats.js (pulls in -// chart.js/auto) -// dist/static/stats.js.map source map sibling -// dist/static/agent.css served at /static/agent.css (@import -// resolved from @hive/shared) -// -// The in-container Rust binary mounts `dist/` (with per-agent -// `hyperhive.frontend.extraFiles` layered on top) as a -// `tower_http::ServeDir` fallback; the layout above keeps every URL -// the HTML references reachable without rewriting paths. - -import { build } from 'esbuild'; -import { mkdirSync, copyFileSync, rmSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const here = dirname(fileURLToPath(import.meta.url)); -const src = (p) => resolve(here, 'src', p); -const dist = (p) => resolve(here, 'dist', p); -const staticDir = (p) => resolve(here, 'dist', 'static', p); - -rmSync(dist(''), { recursive: true, force: true }); -mkdirSync(staticDir(''), { recursive: true }); - -// Two JS entries: the main app + the stats page. Both bundle their -// own deps so each page can be loaded independently. -await build({ - entryPoints: [src('app.js'), src('stats.js')], - outdir: staticDir(''), - bundle: true, - format: 'esm', - platform: 'browser', - target: ['es2022'], - sourcemap: true, - logLevel: 'info', -}); - -// Bundle the CSS — the @import lines pull in shared/base.css and -// shared/terminal.css from the @hive/shared workspace dep. -await build({ - entryPoints: [src('agent.css')], - outfile: staticDir('agent.css'), - bundle: true, - loader: { '.css': 'css' }, - logLevel: 'info', -}); - -for (const html of ['index.html', 'stats.html', 'screen.html']) { - copyFileSync(src(html), dist(html)); -} - -console.log('agent build ok →', dist('')); diff --git a/frontend/packages/agent/package.json b/frontend/packages/agent/package.json deleted file mode 100644 index 0d0c1759..00000000 --- a/frontend/packages/agent/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@hive/agent", - "version": "0.0.0", - "private": true, - "description": "hive-ag3nt per-container web UI. Bundled by esbuild into a static dist; served by the in-container Rust binary at runtime via tower_http::ServeDir. Per-agent additions are layered on top via the hyperhive.frontend.extraFiles nix option.", - "type": "module", - "scripts": { - "build": "node ./build.mjs" - }, - "dependencies": { - "@hive/shared": "*", - "marked": "4.3.0", - "chart.js": "4.4.4" - } -} diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs deleted file mode 100644 index 07097fa6..00000000 --- a/frontend/packages/dashboard/build.mjs +++ /dev/null @@ -1,53 +0,0 @@ -// esbuild build for @hive/dashboard. Output layout (`dist/`): -// -// dist/index.html served by the Rust router at GET / -// dist/static/app.js served at /static/app.js (ESM bundle, -// pulls in @hive/shared + marked) -// dist/static/app.js.map source map sibling -// dist/static/dashboard.css served at /static/dashboard.css -// (@import resolved from @hive/shared) -// -// The Rust binary mounts `dist/` as a `tower_http::ServeDir` fallback; -// the layout above keeps every URL the index.html references reachable -// without rewriting paths in the HTML. - -import { build } from 'esbuild'; -import { mkdirSync, copyFileSync, rmSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const here = dirname(fileURLToPath(import.meta.url)); -const src = (p) => resolve(here, 'src', p); -const dist = (p) => resolve(here, 'dist', p); -const staticDir = (p) => resolve(here, 'dist', 'static', p); - -rmSync(dist(''), { recursive: true, force: true }); -mkdirSync(staticDir(''), { recursive: true }); - -// Bundle the JS entry. ES-module output, browser target, no minify -// (line-aligned source aids debugging; minification belongs in a later -// follow-up once asset sizes warrant it). -await build({ - entryPoints: [src('app.js')], - outfile: staticDir('app.js'), - bundle: true, - format: 'esm', - platform: 'browser', - target: ['es2022'], - sourcemap: true, - logLevel: 'info', -}); - -// Bundle the CSS — esbuild resolves @import including the package -// re-exports from @hive/shared. -await build({ - entryPoints: [src('dashboard.css')], - outfile: staticDir('dashboard.css'), - bundle: true, - loader: { '.css': 'css' }, - logLevel: 'info', -}); - -copyFileSync(src('index.html'), dist('index.html')); - -console.log('dashboard build ok →', dist('')); diff --git a/frontend/packages/dashboard/package.json b/frontend/packages/dashboard/package.json deleted file mode 100644 index de91e6d7..00000000 --- a/frontend/packages/dashboard/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@hive/dashboard", - "version": "0.0.0", - "private": true, - "description": "hive-c0re dashboard SPA. Bundled by esbuild into a static dist; served by the hive-c0re Rust binary at runtime via tower_http::ServeDir.", - "type": "module", - "scripts": { - "build": "node ./build.mjs" - }, - "dependencies": { - "@hive/shared": "*", - "marked": "4.3.0" - } -} diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json deleted file mode 100644 index 795ed42c..00000000 --- a/frontend/packages/shared/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@hive/shared", - "version": "0.0.0", - "private": true, - "description": "Shared frontend modules used by both the dashboard and the per-agent UI: terminal log pane, Catppuccin palette, base typography. Imported by sibling workspaces; not bundled standalone.", - "type": "module", - "main": "./src/index.js", - "exports": { - ".": "./src/index.js", - "./terminal.js": "./src/terminal.js", - "./base.css": "./src/base.css", - "./terminal.css": "./src/terminal.css" - }, - "files": [ - "src/" - ] -} diff --git a/frontend/packages/shared/src/index.js b/frontend/packages/shared/src/index.js deleted file mode 100644 index 49f5c1dc..00000000 --- a/frontend/packages/shared/src/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// Convenience re-export so consumers can `import { create, linkify } -// from '@hive/shared'` without naming the sub-module path. -export { create, linkify } from './terminal.js'; diff --git a/frontend/packages/shared/src/terminal.js b/frontend/packages/shared/src/terminal.js deleted file mode 100644 index 0726a8cc..00000000 --- a/frontend/packages/shared/src/terminal.js +++ /dev/null @@ -1,342 +0,0 @@ -// Shared terminal pane: sticky-bottom log + "↓ N new" pill + history -// backfill + live SSE. Pages provide a kind→renderer map; this module -// owns scroll behaviour, animation suppression on backfill, and the -// EventSource lifecycle. -// -// Usage: -// -// import { create, linkify } from '@hive/shared/terminal.js'; -// -// create({ -// logEl: document.getElementById('msgflow'), -// historyUrl: '/messages/history?limit=200', // optional -// streamUrl: '/messages/stream', -// renderers: { -// sent: (ev, api) => api.row('msgrow sent', ...), -// delivered: (ev, api) => api.row('msgrow delivered', ...), -// _default: (ev, api) => api.row('note', JSON.stringify(ev)), -// }, -// onLiveEvent: (ev) => { /* live-only side effects (notif, state pokes) */ }, -// onAnyEvent: (ev, { fromHistory }) => { /* runs for every event in -// both backfill replay and live — use for derived views that need -// the full picture (e.g. a per-recipient inbox built from broker -// events) */ }, -// onBackfillDone: (count) => { /* one-shot after history replay */ }, -// onStreamOpen: () => { /* fires on every EventSource (re)connect — -// use to re-sync snapshot-derived state after a reconnect gap */ }, -// pillAnchor: document.getElementById('msgflow').parentElement, -// }); -// -// Renderers receive (ev, api) where api exposes: -// -// api.row(cls, text) → appends a flat
-// api.details(cls, summary, body) → appends
-// with a -// api.detailsDiff(cls, summary, body) → ditto but body is line-coloured by -// leading "+ " / "- " prefix -// api.placeholder(text) → replaces log content with a single -// muted "(placeholder)" row, cleared -// on the next real row -// api.fromHistory → true while backfill is replaying -// -// Each kind is dispatched to `renderers[ev.kind]`; unknown kinds fall -// through to `renderers._default` (which itself defaults to a JSON-dump -// note row). The convention is that the SSE/history endpoints emit -// objects with a `kind` field. -// -// Backfill is best-effort: if `historyUrl` is unset or the fetch fails, -// we skip straight to SSE. The optional `onBackfillDone(count)` hook -// fires after replay finishes (or after a failed/skipped fetch with -// count=0); pages use it to set state flags from the replayed history. - -const NEAR_BOTTOM_PX = 48; - -export function create(opts) { - const log = opts.logEl; - if (!log) throw new Error('HiveTerminal.create: logEl is required'); - const renderers = opts.renderers || {}; - const defaultRender = renderers._default - || ((ev, api) => api.row('note', JSON.stringify(ev))); - const pillAnchor = opts.pillAnchor || log.parentElement || log; - - let placeholderEl = null; - let pill = null; - let unseen = 0; - let currentNoAnim = false; - - function isNearBottom() { - return log.scrollHeight - log.scrollTop - log.clientHeight <= NEAR_BOTTOM_PX; - } - function ensurePill() { - if (pill) return pill; - pill = document.createElement('button'); - pill.type = 'button'; - pill.className = 'tail-pill'; - pill.addEventListener('click', () => { log.scrollTop = log.scrollHeight; }); - pillAnchor.appendChild(pill); - return pill; - } - function updatePill() { - if (unseen <= 0) { - if (pill) pill.classList.remove('visible'); - return; - } - ensurePill(); - pill.textContent = '↓ ' + unseen + ' new'; - pill.classList.add('visible'); - } - log.addEventListener('scroll', () => { - if (isNearBottom()) { unseen = 0; updatePill(); } - }); - function afterAppend() { - if (currentNoAnim || isNearBottom()) { - log.scrollTop = log.scrollHeight; - } else { - unseen += 1; - updatePill(); - } - } - function clearPlaceholder() { - if (placeholderEl && placeholderEl.parentElement === log) { - log.removeChild(placeholderEl); - } - placeholderEl = null; - } - function placeholder(text) { - clearPlaceholder(); - const e = document.createElement('div'); - e.className = 'row note'; - e.textContent = text; - log.appendChild(e); - placeholderEl = e; - } - function row(cls, text) { - clearPlaceholder(); - const e = document.createElement('div'); - e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); - e.appendChild(linkify(text)); - log.appendChild(e); - afterAppend(); - return e; - } - function details(cls, summary, body) { - clearPlaceholder(); - const d = document.createElement('details'); - d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); - const s = document.createElement('summary'); - s.textContent = summary; - d.appendChild(s); - const pre = document.createElement('pre'); - pre.className = 'tool-body'; - pre.appendChild(linkify(body)); - d.appendChild(pre); - log.appendChild(d); - afterAppend(); - return d; - } - function detailsDiff(cls, summary, body) { - clearPlaceholder(); - const d = document.createElement('details'); - d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); - const s = document.createElement('summary'); - s.textContent = summary; - d.appendChild(s); - const pre = document.createElement('pre'); - pre.className = 'tool-body diff-body'; - for (const line of String(body).split('\n')) { - const span = document.createElement('span'); - if (line.startsWith('+ ')) span.className = 'diff-add'; - else if (line.startsWith('- ')) span.className = 'diff-del'; - else span.className = 'diff-ctx'; - span.textContent = line + '\n'; - pre.appendChild(span); - } - d.appendChild(pre); - log.appendChild(d); - afterAppend(); - return d; - } - - function api(extra) { - return Object.assign({ - row, details, detailsDiff, placeholder, linkify, - fromHistory: false, - }, extra || {}); - } - function dispatch(ev, fromHistory) { - const r = renderers[ev.kind] || defaultRender; - try { - r(ev, api({ fromHistory })); - } catch (err) { - console.error('terminal renderer threw', ev, err); - row('note', '[render err] ' + (err && err.message ? err.message : err)); - } - if (opts.onAnyEvent) { - try { opts.onAnyEvent(ev, { fromHistory }); } - catch (err) { console.error('onAnyEvent threw', err); } - } - } - - // Subscribe → buffer → fetch history → dedupe → apply. - // - // Race the SSE subscription opens before the history fetch starts. - // Live events that land before history resolves are buffered, not - // rendered. Once the history response (`{ seq, events }`) arrives we: - // 1. Replay `events` (fromHistory=true). - // 2. Drop buffered events with `seq <= history.seq` — they're - // already reflected in the history rows above. - // 3. Apply remaining buffered events (fromHistory=false). - // 4. Switch to live mode: each new SSE event dispatches immediately. - // - // Without this dance an event that fires between history-fetch and - // SSE-subscribe goes missing; without seq dedupe the same event - // shows twice (once via history, once via live buffer). Both bugs - // were latent before. - // - // If `historyUrl` is unset we skip the dance: buffered events apply - // as live the moment the buffer flushes (no dedupe possible without - // a boundary seq). - function start() { - let live = false; - let buffered = []; - - const es = new EventSource(opts.streamUrl); - es.onmessage = (e) => { - let ev; - try { ev = JSON.parse(e.data); } - catch (err) { row('note', '[parse err] ' + e.data); return; } - if (!live) { buffered.push(ev); return; } - dispatch(ev, false); - if (opts.onLiveEvent) { - try { opts.onLiveEvent(ev); } - catch (err) { console.error('onLiveEvent threw', err); } - } - }; - es.onerror = () => { - if (es.readyState === EventSource.CONNECTING) row('note', '[reconnecting…]'); - else row('note', '[disconnected]'); - }; - es.onopen = () => { - // Fires on the initial connect and on every automatic - // reconnect. EventSource never replays events that fired - // during a disconnect window, so a consumer with - // snapshot-derived state (the dashboard's /api/state stores) - // must re-sync here or it shows stale state until a manual - // reload (issue #163). - if (opts.onStreamOpen) { - try { opts.onStreamOpen(); } - catch (err) { console.error('onStreamOpen threw', err); } - } - }; - - function flushBuffered(boundarySeq, historyKinds) { - const drained = buffered; - buffered = []; - live = true; - for (const ev of drained) { - // Seq-dedupe only events of a kind that actually appeared in - // the history replay — those are the only ones that could - // double (once via history, once via the live buffer). - // Mutation events (approval/question/container/…) are never - // carried by the history endpoint; deduping them against the - // broker-history seq would wrongly drop ones that fired - // between a consumer's own snapshot read and this history - // fetch (issue #163). ev.seq absent/0 → no dedupe possible. - if (boundarySeq != null - && typeof ev.seq === 'number' && ev.seq <= boundarySeq - && historyKinds && historyKinds.has(ev.kind)) { - continue; - } - dispatch(ev, false); - if (opts.onLiveEvent) { - try { opts.onLiveEvent(ev); } - catch (err) { console.error('onLiveEvent threw', err); } - } - } - } - - async function backfill() { - if (!opts.historyUrl) { - flushBuffered(null); - if (opts.onBackfillDone) opts.onBackfillDone(0); - return; - } - try { - const resp = await fetch(opts.historyUrl); - if (!resp.ok) { - flushBuffered(null); - if (opts.onBackfillDone) opts.onBackfillDone(0); - return; - } - const body = await resp.json(); - // Accept the envelope `{ seq, events }`. A bare array means - // the server hasn't been updated to include seq yet — treat - // it as "no dedupe possible." - const events = Array.isArray(body) ? body : (body.events || []); - const boundarySeq = Array.isArray(body) ? null : (body.seq ?? null); - // Kinds present in the history replay — the only kinds that - // can double and therefore the only ones to seq-dedupe. - const historyKinds = new Set(events.map((ev) => ev.kind)); - currentNoAnim = true; - for (const ev of events) dispatch(ev, true); - currentNoAnim = false; - if (events.length) row('note', '─── live (older above) ───'); - else placeholder('(connected — waiting for events)'); - flushBuffered(boundarySeq, historyKinds); - if (opts.onBackfillDone) opts.onBackfillDone(events.length); - } catch (err) { - console.warn('history backfill failed', err); - flushBuffered(null); - if (opts.onBackfillDone) opts.onBackfillDone(0); - } - } - return backfill(); - } - - const ready = start(); - return { row, details, detailsDiff, placeholder, ready }; -} - -// Build a DocumentFragment from `text`, turning bare http(s) URLs into -// clickable links that open in a new tab. Non-URL text stays as plain -// text nodes — no innerHTML, so this is XSS-safe. Trailing sentence -// punctuation is kept out of the link. (issue #233) -const LINKIFY_URL_RE = /https?:\/\/[^\s<>"']+/g; -export function linkify(text) { - const str = text == null ? '' : String(text); - const frag = document.createDocumentFragment(); - if (str.indexOf('://') === -1) { // fast path: no URLs - if (str) frag.appendChild(document.createTextNode(str)); - return frag; - } - let last = 0; - let m; - LINKIFY_URL_RE.lastIndex = 0; - while ((m = LINKIFY_URL_RE.exec(str)) !== null) { - let url = m[0]; - // Don't swallow trailing punctuation that's really sentence text. - const trail = url.match(/[.,;:!?)\]}'"]+$/); - const tail = trail ? trail[0] : ''; - if (tail) url = url.slice(0, -tail.length); - if (m.index > last) { - frag.appendChild(document.createTextNode(str.slice(last, m.index))); - } - if (!url.slice(url.indexOf('://') + 3)) { - // Nothing past the scheme — not a real URL, emit verbatim. - frag.appendChild(document.createTextNode(m[0])); - } else { - const a = document.createElement('a'); - a.href = url; // regex only matches https?:// — safe - a.textContent = url; - a.target = '_blank'; - a.rel = 'noopener noreferrer'; - frag.appendChild(a); - if (tail) frag.appendChild(document.createTextNode(tail)); - } - last = m.index + m[0].length; - } - if (last < str.length) { - frag.appendChild(document.createTextNode(str.slice(last))); - } - return frag; -} diff --git a/hive-ag3nt/Cargo.toml b/hive-ag3nt/Cargo.toml index 3f0fba3e..e2b4a2b6 100644 --- a/hive-ag3nt/Cargo.toml +++ b/hive-ag3nt/Cargo.toml @@ -12,6 +12,7 @@ axum.workspace = true reqwest.workspace = true futures-util = "0.3" clap.workspace = true +hive-fr0nt.workspace = true hive-sh4re.workspace = true rmcp.workspace = true rusqlite.workspace = true @@ -20,7 +21,6 @@ serde.workspace = true serde_json.workspace = true tokio.workspace = true tokio-stream.workspace = true -tower-http.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/frontend/packages/agent/src/agent.css b/hive-ag3nt/assets/agent.css similarity index 98% rename from frontend/packages/agent/src/agent.css rename to hive-ag3nt/assets/agent.css index 68103202..8b66609b 100644 --- a/frontend/packages/agent/src/agent.css +++ b/hive-ag3nt/assets/agent.css @@ -1,8 +1,5 @@ -/* Shared Catppuccin palette + body typography + terminal pane styles. - Bundled in front of the agent-only rules below via esbuild. */ -@import "@hive/shared/base.css"; -@import "@hive/shared/terminal.css"; - +/* Palette + base body typography live in hive-fr0nt::BASE_CSS, prepended + to this stylesheet by `serve_css` at runtime. */ body { max-width: 110em; margin: 1.5em auto; diff --git a/frontend/packages/agent/src/app.js b/hive-ag3nt/assets/app.js similarity index 98% rename from frontend/packages/agent/src/app.js rename to hive-ag3nt/assets/app.js index c2a56c55..eb478932 100644 --- a/frontend/packages/agent/src/app.js +++ b/hive-ag3nt/assets/app.js @@ -2,18 +2,6 @@ // tails `/events/stream` for live claude events, drives async-form // actions (send / login/* / dashboard rebuild). -import { create as termCreate, linkify as termLinkify } from '@hive/shared/terminal.js'; -import { marked } from 'marked'; - -// Expose the previously-script-tag-provided globals so the IIFE below -// keeps working unchanged. Pre-split these were attached by -// `/static/hive-fr0nt.js` (HiveTerminal) and `/static/marked.js` -// (marked) loading before app.js. The bundle now pulls them in via ES -// imports; once the IIFE is opened up these aliases can be dropped in -// favour of direct named imports. -window.HiveTerminal = { create: termCreate, linkify: termLinkify }; -window.marked = marked; - (() => { // ─── helpers ──────────────────────────────────────────────────────────── const $ = (id) => document.getElementById(id); diff --git a/frontend/packages/agent/src/index.html b/hive-ag3nt/assets/index.html similarity index 91% rename from frontend/packages/agent/src/index.html rename to hive-ag3nt/assets/index.html index dcc0d2b9..8721ef9c 100644 --- a/frontend/packages/agent/src/index.html +++ b/hive-ag3nt/assets/index.html @@ -45,8 +45,8 @@
- - + + + diff --git a/frontend/packages/agent/src/screen.html b/hive-ag3nt/assets/screen.html similarity index 100% rename from frontend/packages/agent/src/screen.html rename to hive-ag3nt/assets/screen.html diff --git a/frontend/packages/agent/src/stats.html b/hive-ag3nt/assets/stats.html similarity index 91% rename from frontend/packages/agent/src/stats.html rename to hive-ag3nt/assets/stats.html index 0795310d..446491fc 100644 --- a/frontend/packages/agent/src/stats.html +++ b/hive-ag3nt/assets/stats.html @@ -90,9 +90,11 @@

result mix

- - + + + diff --git a/frontend/packages/agent/src/stats.js b/hive-ag3nt/assets/stats.js similarity index 97% rename from frontend/packages/agent/src/stats.js rename to hive-ag3nt/assets/stats.js index 546ab190..1b524d24 100644 --- a/frontend/packages/agent/src/stats.js +++ b/hive-ag3nt/assets/stats.js @@ -2,14 +2,6 @@ // once on load, then /api/stats?window=... for the chart data — re-fetches // when the operator clicks a window tab. -import Chart from 'chart.js/auto'; - -// Expose for the IIFE below — pre-split this was a window global from -// the jsDelivr CDN script tag. esbuild now bundles chart.js into -// stats.js; once the IIFE opens up we can use the imported `Chart` -// directly. -window.Chart = Chart; - (function () { 'use strict'; diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 14e02e39..3868c826 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -24,7 +24,6 @@ use axum::{ use serde::{Deserialize, Serialize}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio_stream::{Stream, StreamExt, wrappers::BroadcastStream}; -use tower_http::services::ServeDir; use crate::client; use crate::events::Bus; @@ -89,19 +88,6 @@ pub async fn serve( turn_lock: TurnLock, ) -> Result<()> { let gui_vnc_port = read_gui_json(); - let static_dir: PathBuf = std::env::var_os("HIVE_STATIC_DIR") - .map(PathBuf::from) - .context( - "HIVE_STATIC_DIR env var not set — point it at the merged \ - per-agent dist (see hyperhive.frontend.mergedDist in nix)", - )?; - if !static_dir.is_dir() { - anyhow::bail!( - "HIVE_STATIC_DIR ({}) is not a directory", - static_dir.display() - ); - } - tracing::info!(static_dir = %static_dir.display(), "web UI static dir resolved"); let state = AppState { label, login, @@ -113,6 +99,11 @@ pub async fn serve( gui_vnc_port, }; let app = Router::new() + .route("/", get(serve_index)) + .route("/static/agent.css", get(serve_css)) + .route("/static/app.js", get(serve_app_js)) + .route("/static/hive-fr0nt.js", get(serve_shared_js)) + .route("/static/marked.js", get(serve_marked_js)) .route("/api/state", get(api_state)) .route("/events/stream", get(events_stream)) .route("/events/history", get(events_history)) @@ -125,17 +116,12 @@ pub async fn serve( .route("/api/model", post(post_set_model)) .route("/api/new-session", post(post_new_session)) .route("/api/loose-ends", get(api_loose_ends)) + .route("/stats", get(serve_stats)) + .route("/static/stats.js", get(serve_stats_js)) .route("/api/stats", get(api_stats)) + .route("/screen", get(serve_screen)) .route("/screen/ws", get(screen_ws)) .route("/icon", get(serve_icon)) - // Anything else (`/`, `/stats`, `/screen`, `/static/*`) - // falls through to the merged dist. ServeDir auto-appends - // `.html` when the URL is a bare path that matches a file - // (so `/stats` → `dist/stats.html`, `/screen` → `dist/ - // screen.html`). Per-agent `extraFiles` additions are - // already layered into this same directory (see - // hyperhive.frontend.mergedDist in nix). - .fallback_service(ServeDir::new(&static_dir)) .with_state(state); let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = bind_with_retry(addr, "web UI").await?; @@ -215,6 +201,68 @@ fn try_bind(addr: SocketAddr) -> std::io::Result { sock.listen(1024) } +async fn serve_index() -> impl IntoResponse { + ( + [("content-type", "text/html; charset=utf-8")], + include_str!("../assets/index.html"), + ) +} + +async fn serve_css() -> impl IntoResponse { + // Prepend the shared palette/typography so per-page styles only need + // to declare what's actually page-specific. One HTTP request, no + // per-asset cache to invalidate. + let body = format!( + "{}\n{}\n{}", + hive_fr0nt::BASE_CSS, + hive_fr0nt::TERMINAL_CSS, + include_str!("../assets/agent.css"), + ); + ([("content-type", "text/css")], body) +} + +async fn serve_app_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript")], + include_str!("../assets/app.js"), + ) +} + +async fn serve_shared_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript")], + hive_fr0nt::TERMINAL_JS, + ) +} + +async fn serve_marked_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript")], + hive_fr0nt::MARKED_JS, + ) +} + +async fn serve_stats() -> impl IntoResponse { + ( + [("content-type", "text/html; charset=utf-8")], + include_str!("../assets/stats.html"), + ) +} + +async fn serve_stats_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript")], + include_str!("../assets/stats.js"), + ) +} + +async fn serve_screen() -> impl IntoResponse { + ( + [("content-type", "text/html; charset=utf-8")], + include_str!("../assets/screen.html"), + ) +} + /// This agent's icon. Serves the operator-configured SVG from /// `/etc/hyperhive/icon.svg` (set via the `hyperhive.icon` agent.nix /// option) when present, otherwise the bundled default hyperhive logo. @@ -537,13 +585,8 @@ async fn api_state(State(state): State) -> axum::Json { fn agent_links(label: &str, gui_enabled: bool) -> Vec { let mut links = Vec::new(); - // Note: the URLs are the actual HTML files served out of the - // frontend dist (`stats.html` / `screen.html`); after the #273 - // backend/frontend split the harness serves these as static - // files via ServeDir rather than via Rust routes, so the URL - // has to be the on-disk filename. links.push(AgentLink { - url: "/stats.html".to_owned(), + url: "/stats".to_owned(), icon: "📊".to_owned(), label: "stats".to_owned(), kind: AgentLinkKind::Container, @@ -551,7 +594,7 @@ fn agent_links(label: &str, gui_enabled: bool) -> Vec { if gui_enabled { links.push(AgentLink { - url: "/screen.html".to_owned(), + url: "/screen".to_owned(), icon: "🖥".to_owned(), label: "screen".to_owned(), kind: AgentLinkKind::Container, diff --git a/hive-c0re/Cargo.toml b/hive-c0re/Cargo.toml index d57a7772..169a50ea 100644 --- a/hive-c0re/Cargo.toml +++ b/hive-c0re/Cargo.toml @@ -12,6 +12,7 @@ axum.workspace = true base64.workspace = true reqwest.workspace = true clap.workspace = true +hive-fr0nt.workspace = true hive-sh4re.workspace = true libc = "0.2" rusqlite.workspace = true @@ -19,7 +20,6 @@ serde.workspace = true serde_json.workspace = true tokio.workspace = true tokio-stream.workspace = true -tower-http.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/frontend/packages/dashboard/src/app.js b/hive-c0re/assets/app.js similarity index 99% rename from frontend/packages/dashboard/src/app.js rename to hive-c0re/assets/app.js index d7b912b2..53012bde 100644 --- a/frontend/packages/dashboard/src/app.js +++ b/hive-c0re/assets/app.js @@ -2,18 +2,6 @@ // up async-form submission (URL-encoded POST + spinner + state refresh), // and tails the unified dashboard event channel over `/dashboard/stream`. -import { create as termCreate, linkify as termLinkify } from '@hive/shared/terminal.js'; -import { marked } from 'marked'; - -// Expose the previously-script-tag-provided globals so the IIFE below -// keeps working unchanged. Pre-split these were attached by -// `/static/hive-fr0nt.js` (HiveTerminal) and `/static/marked.js` -// (marked) loading before app.js. The bundle now pulls them in via ES -// imports; once the IIFE is opened up these aliases can be dropped in -// favour of direct named imports. -window.HiveTerminal = { create: termCreate, linkify: termLinkify }; -window.marked = marked; - (() => { // ─── constants ────────────────────────────────────────────────────────── // Context-window badge thresholds. Preferred source is each container's diff --git a/frontend/packages/dashboard/src/dashboard.css b/hive-c0re/assets/dashboard.css similarity index 99% rename from frontend/packages/dashboard/src/dashboard.css rename to hive-c0re/assets/dashboard.css index 39d6cbeb..568729db 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -1,8 +1,5 @@ -/* Shared Catppuccin palette + body typography + terminal pane styles. - Bundled in front of the dashboard-only rules below via esbuild. */ -@import "@hive/shared/base.css"; -@import "@hive/shared/terminal.css"; - +/* Palette + base body typography live in hive-fr0nt::BASE_CSS, prepended + to this stylesheet by `serve_css` at runtime. */ body { max-width: 70em; margin: 1.5em auto; diff --git a/frontend/packages/dashboard/src/index.html b/hive-c0re/assets/index.html similarity index 96% rename from frontend/packages/dashboard/src/index.html rename to hive-c0re/assets/index.html index be998339..846c86e9 100644 --- a/frontend/packages/dashboard/src/index.html +++ b/hive-c0re/assets/index.html @@ -109,9 +109,8 @@ - - + + + diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 9871fc54..725ac1b3 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -4,7 +4,7 @@ use std::convert::Infallible; use std::net::SocketAddr; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use anyhow::{Context, Result}; @@ -14,7 +14,7 @@ use axum::{ extract::{Path as AxumPath, State}, http::{HeaderMap, StatusCode}, response::{ - IntoResponse, Response, + Html, IntoResponse, Response, sse::{Event, KeepAlive, Sse}, }, routing::{get, post}, @@ -23,7 +23,6 @@ use hive_sh4re::Approval; use serde::{Deserialize, Serialize}; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::{Stream, StreamExt}; -use tower_http::services::ServeDir; use crate::actions; use crate::container_view::{ContainerView, claude_has_session}; @@ -38,20 +37,11 @@ struct AppState { } pub async fn serve(port: u16, coord: Arc) -> Result<()> { - let static_dir: PathBuf = std::env::var_os("HIVE_STATIC_DIR") - .map(PathBuf::from) - .context( - "HIVE_STATIC_DIR env var not set — point it at the bundled \ - dashboard dist (see services.hive-c0re.frontend in nix)", - )?; - if !static_dir.is_dir() { - anyhow::bail!( - "HIVE_STATIC_DIR ({}) is not a directory", - static_dir.display() - ); - } - tracing::info!(static_dir = %static_dir.display(), "dashboard static dir resolved"); let app = Router::new() + .route("/", get(serve_index)) + .route("/static/dashboard.css", get(serve_css)) + .route("/static/app.js", get(serve_app_js)) + .route("/favicon.svg", get(serve_favicon)) .route("/api/state", get(api_state)) .route("/approve/{id}", post(post_approve)) .route("/deny/{id}", post(post_deny)) @@ -76,11 +66,8 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { .route("/meta-update", post(post_meta_update)) .route("/dashboard/stream", get(dashboard_stream)) .route("/dashboard/history", get(dashboard_history)) - // Anything not matched by the dynamic routes above falls - // through to the bundled dashboard dist (GET / → - // dist/index.html, /favicon.svg → dist/favicon.svg, - // /static/dashboard.css → dist/static/dashboard.css, etc.). - .fallback_service(ServeDir::new(&static_dir)) + .route("/static/hive-fr0nt.js", get(serve_shared_js)) + .route("/static/marked.js", get(serve_marked_js)) .with_state(AppState { coord }); let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = bind_with_retry(addr).await?; @@ -90,14 +77,11 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { } // --------------------------------------------------------------------------- -// The dashboard is an SPA. Its HTML shell + bundled JS / CSS / favicon -// live in the directory pointed at by `HIVE_STATIC_DIR` (set by the -// hive-c0re NixOS module to `${frontend}/dashboard`), served by the -// `tower_http::ServeDir` fallback declared in `serve()`. The dynamic -// surface — `/api/state` and the action endpoints — is owned here. -// The JS app fetches state on load, re-fetches after every async-form -// submit, and listens on `/dashboard/stream` for the unified live event -// channel. +// Static asset handlers: the dashboard is an SPA. `GET /` returns the +// (static) shell; `GET /static/*` serves the CSS + JS app; `GET /api/state` +// returns the current snapshot as JSON. The JS app fetches state on load, +// re-fetches after every async-form submit, and listens on +// `/dashboard/stream` for the unified live event channel. // --------------------------------------------------------------------------- /// `SO_REUSEADDR` bind with retry. Mirrors the per-agent variant in @@ -158,6 +142,56 @@ fn try_bind(addr: SocketAddr) -> std::io::Result { sock.listen(1024) } +async fn serve_index() -> impl IntoResponse { + Html(include_str!("../assets/index.html")) +} + +async fn serve_css() -> impl IntoResponse { + // Prepend the shared palette/typography so per-page styles only need + // to declare what's actually page-specific. One HTTP request, no + // per-asset cache to invalidate. + let body = format!( + "{}\n{}\n{}", + hive_fr0nt::BASE_CSS, + hive_fr0nt::TERMINAL_CSS, + include_str!("../assets/dashboard.css"), + ); + ([("content-type", "text/css")], body) +} + +async fn serve_app_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript")], + include_str!("../assets/app.js"), + ) +} + +/// Dashboard favicon — the hyperhive mark. Static: the dashboard +/// represents the whole hive, so it always uses the project logo +/// (per-agent pages serve their own configurable `/icon` instead). +async fn serve_favicon() -> impl IntoResponse { + ( + [("content-type", "image/svg+xml")], + include_str!("../../branding/hyperhive.svg"), + ) +} + +async fn serve_shared_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript")], + hive_fr0nt::TERMINAL_JS, + ) +} + +/// Vendored `marked` bundle — the side panel renders markdown file +/// previews with it. +async fn serve_marked_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript")], + hive_fr0nt::MARKED_JS, + ) +} + #[derive(Serialize)] struct StateSnapshot { /// Broker seq at the moment this snapshot was assembled. Clients diff --git a/hive-fr0nt/Cargo.toml b/hive-fr0nt/Cargo.toml new file mode 100644 index 00000000..1bc5f00f --- /dev/null +++ b/hive-fr0nt/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "hive-fr0nt" +edition.workspace = true +version.workspace = true + +[lints] +workspace = true diff --git a/frontend/packages/shared/src/base.css b/hive-fr0nt/assets/base.css similarity index 100% rename from frontend/packages/shared/src/base.css rename to hive-fr0nt/assets/base.css diff --git a/hive-fr0nt/assets/marked.umd.js b/hive-fr0nt/assets/marked.umd.js new file mode 100644 index 00000000..2f2ca9ca --- /dev/null +++ b/hive-fr0nt/assets/marked.umd.js @@ -0,0 +1,2913 @@ +/** + * marked - a markdown parser + * Copyright (c) 2011-2021, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.marked = {})); +})(this, (function (exports) { 'use strict'; + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); + } + + function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; + } + + function _createForOfIteratorHelperLoose(o, allowArrayLike) { + var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; + if (it) return (it = it.call(o)).next.bind(it); + + if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { + if (it) o = it; + var i = 0; + return function () { + if (i >= o.length) return { + done: true + }; + return { + done: false, + value: o[i++] + }; + }; + } + + throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + function getDefaults() { + return { + baseUrl: null, + breaks: false, + extensions: null, + gfm: true, + headerIds: true, + headerPrefix: '', + highlight: null, + langPrefix: 'language-', + mangle: true, + pedantic: false, + renderer: null, + sanitize: false, + sanitizer: null, + silent: false, + smartLists: false, + smartypants: false, + tokenizer: null, + walkTokens: null, + xhtml: false + }; + } + exports.defaults = getDefaults(); + function changeDefaults(newDefaults) { + exports.defaults = newDefaults; + } + + /** + * Helpers + */ + var escapeTest = /[&<>"']/; + var escapeReplace = /[&<>"']/g; + var escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; + var escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; + var escapeReplacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + var getEscapeReplacement = function getEscapeReplacement(ch) { + return escapeReplacements[ch]; + }; + + function escape(html, encode) { + if (encode) { + if (escapeTest.test(html)) { + return html.replace(escapeReplace, getEscapeReplacement); + } + } else { + if (escapeTestNoEncode.test(html)) { + return html.replace(escapeReplaceNoEncode, getEscapeReplacement); + } + } + + return html; + } + var unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; + function unescape(html) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(unescapeTest, function (_, n) { + n = n.toLowerCase(); + if (n === 'colon') return ':'; + + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' ? String.fromCharCode(parseInt(n.substring(2), 16)) : String.fromCharCode(+n.substring(1)); + } + + return ''; + }); + } + var caret = /(^|[^\[])\^/g; + function edit(regex, opt) { + regex = regex.source || regex; + opt = opt || ''; + var obj = { + replace: function replace(name, val) { + val = val.source || val; + val = val.replace(caret, '$1'); + regex = regex.replace(name, val); + return obj; + }, + getRegex: function getRegex() { + return new RegExp(regex, opt); + } + }; + return obj; + } + var nonWordAndColonTest = /[^\w:]/g; + var originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; + function cleanUrl(sanitize, base, href) { + if (sanitize) { + var prot; + + try { + prot = decodeURIComponent(unescape(href)).replace(nonWordAndColonTest, '').toLowerCase(); + } catch (e) { + return null; + } + + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { + return null; + } + } + + if (base && !originIndependentUrl.test(href)) { + href = resolveUrl(base, href); + } + + try { + href = encodeURI(href).replace(/%25/g, '%'); + } catch (e) { + return null; + } + + return href; + } + var baseUrls = {}; + var justDomain = /^[^:]+:\/*[^/]*$/; + var protocol = /^([^:]+:)[\s\S]*$/; + var domain = /^([^:]+:\/*[^/]*)[\s\S]*$/; + function resolveUrl(base, href) { + if (!baseUrls[' ' + base]) { + // we can ignore everything in base after the last slash of its path component, + // but we might need to add _that_ + // https://tools.ietf.org/html/rfc3986#section-3 + if (justDomain.test(base)) { + baseUrls[' ' + base] = base + '/'; + } else { + baseUrls[' ' + base] = rtrim(base, '/', true); + } + } + + base = baseUrls[' ' + base]; + var relativeBase = base.indexOf(':') === -1; + + if (href.substring(0, 2) === '//') { + if (relativeBase) { + return href; + } + + return base.replace(protocol, '$1') + href; + } else if (href.charAt(0) === '/') { + if (relativeBase) { + return href; + } + + return base.replace(domain, '$1') + href; + } else { + return base + href; + } + } + var noopTest = { + exec: function noopTest() {} + }; + function merge(obj) { + var i = 1, + target, + key; + + for (; i < arguments.length; i++) { + target = arguments[i]; + + for (key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + obj[key] = target[key]; + } + } + } + + return obj; + } + function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + var row = tableRow.replace(/\|/g, function (match, offset, str) { + var escaped = false, + curr = offset; + + while (--curr >= 0 && str[curr] === '\\') { + escaped = !escaped; + } + + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } else { + // add space before unescaped | + return ' |'; + } + }), + cells = row.split(/ \|/); + var i = 0; // First/last cell in a row cannot be empty if it has no leading/trailing pipe + + if (!cells[0].trim()) { + cells.shift(); + } + + if (!cells[cells.length - 1].trim()) { + cells.pop(); + } + + if (cells.length > count) { + cells.splice(count); + } else { + while (cells.length < count) { + cells.push(''); + } + } + + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(/\\\|/g, '|'); + } + + return cells; + } // Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + // /c*$/ is vulnerable to REDOS. + // invert: Remove suffix of non-c chars instead. Default falsey. + + function rtrim(str, c, invert) { + var l = str.length; + + if (l === 0) { + return ''; + } // Length of suffix matching the invert condition. + + + var suffLen = 0; // Step left until we fail to match the invert condition. + + while (suffLen < l) { + var currChar = str.charAt(l - suffLen - 1); + + if (currChar === c && !invert) { + suffLen++; + } else if (currChar !== c && invert) { + suffLen++; + } else { + break; + } + } + + return str.substr(0, l - suffLen); + } + function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + + var l = str.length; + var level = 0, + i = 0; + + for (; i < l; i++) { + if (str[i] === '\\') { + i++; + } else if (str[i] === b[0]) { + level++; + } else if (str[i] === b[1]) { + level--; + + if (level < 0) { + return i; + } + } + } + + return -1; + } + function checkSanitizeDeprecation(opt) { + if (opt && opt.sanitize && !opt.silent) { + console.warn('marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options'); + } + } // copied from https://stackoverflow.com/a/5450113/806777 + + function repeatString(pattern, count) { + if (count < 1) { + return ''; + } + + var result = ''; + + while (count > 1) { + if (count & 1) { + result += pattern; + } + + count >>= 1; + pattern += pattern; + } + + return result + pattern; + } + + function outputLink(cap, link, raw, lexer) { + var href = link.href; + var title = link.title ? escape(link.title) : null; + var text = cap[1].replace(/\\([\[\]])/g, '$1'); + + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + var token = { + type: 'link', + raw: raw, + href: href, + title: title, + text: text, + tokens: lexer.inlineTokens(text, []) + }; + lexer.state.inLink = false; + return token; + } else { + return { + type: 'image', + raw: raw, + href: href, + title: title, + text: escape(text) + }; + } + } + + function indentCodeCompensation(raw, text) { + var matchIndentToCode = raw.match(/^(\s+)(?:```)/); + + if (matchIndentToCode === null) { + return text; + } + + var indentToCode = matchIndentToCode[1]; + return text.split('\n').map(function (node) { + var matchIndentInNode = node.match(/^\s+/); + + if (matchIndentInNode === null) { + return node; + } + + var indentInNode = matchIndentInNode[0]; + + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + + return node; + }).join('\n'); + } + /** + * Tokenizer + */ + + + var Tokenizer = /*#__PURE__*/function () { + function Tokenizer(options) { + this.options = options || exports.defaults; + } + + var _proto = Tokenizer.prototype; + + _proto.space = function space(src) { + var cap = this.rules.block.newline.exec(src); + + if (cap) { + if (cap[0].length > 1) { + return { + type: 'space', + raw: cap[0] + }; + } + + return { + raw: '\n' + }; + } + }; + + _proto.code = function code(src) { + var cap = this.rules.block.code.exec(src); + + if (cap) { + var text = cap[0].replace(/^ {1,4}/gm, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic ? rtrim(text, '\n') : text + }; + } + }; + + _proto.fences = function fences(src) { + var cap = this.rules.block.fences.exec(src); + + if (cap) { + var raw = cap[0]; + var text = indentCodeCompensation(raw, cap[3] || ''); + return { + type: 'code', + raw: raw, + lang: cap[2] ? cap[2].trim() : cap[2], + text: text + }; + } + }; + + _proto.heading = function heading(src) { + var cap = this.rules.block.heading.exec(src); + + if (cap) { + var text = cap[2].trim(); // remove trailing #s + + if (/#$/.test(text)) { + var trimmed = rtrim(text, '#'); + + if (this.options.pedantic) { + text = trimmed.trim(); + } else if (!trimmed || / $/.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + + var token = { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text: text, + tokens: [] + }; + this.lexer.inline(token.text, token.tokens); + return token; + } + }; + + _proto.hr = function hr(src) { + var cap = this.rules.block.hr.exec(src); + + if (cap) { + return { + type: 'hr', + raw: cap[0] + }; + } + }; + + _proto.blockquote = function blockquote(src) { + var cap = this.rules.block.blockquote.exec(src); + + if (cap) { + var text = cap[0].replace(/^ *> ?/gm, ''); + return { + type: 'blockquote', + raw: cap[0], + tokens: this.lexer.blockTokens(text, []), + text: text + }; + } + }; + + _proto.list = function list(src) { + var cap = this.rules.block.list.exec(src); + + if (cap) { + var raw, istask, ischecked, indent, i, blankLine, endsWithBlankLine, line, lines, itemContents; + var bull = cap[1].trim(); + var isordered = bull.length > 1; + var list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [] + }; + bull = isordered ? "\\d{1,9}\\" + bull.slice(-1) : "\\" + bull; + + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } // Get next list item + + + var itemRegex = new RegExp("^( {0,3}" + bull + ")((?: [^\\n]*| *)(?:\\n[^\\n]*)*(?:\\n|$))"); // Get each top-level item + + while (src) { + if (this.rules.block.hr.test(src)) { + // End list if we encounter an HR (possibly move into itemRegex?) + break; + } + + if (!(cap = itemRegex.exec(src))) { + break; + } + + lines = cap[2].split('\n'); + + if (this.options.pedantic) { + indent = 2; + itemContents = lines[0].trimLeft(); + } else { + indent = cap[2].search(/[^ ]/); // Find first non-space char + + indent = cap[1].length + (indent > 4 ? 1 : indent); // intented code blocks after 4 spaces; indent is always 1 + + itemContents = lines[0].slice(indent - cap[1].length); + } + + blankLine = false; + raw = cap[0]; + + if (!lines[0] && /^ *$/.test(lines[1])) { + // items begin with at most one blank line + raw = cap[1] + lines.slice(0, 2).join('\n') + '\n'; + list.loose = true; + lines = []; + } + + var nextBulletRegex = new RegExp("^ {0," + Math.min(3, indent - 1) + "}(?:[*+-]|\\d{1,9}[.)])"); + + for (i = 1; i < lines.length; i++) { + line = lines[i]; + + if (this.options.pedantic) { + // Re-align to follow commonmark nesting rules + line = line.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' '); + } // End list item if found start of new bullet + + + if (nextBulletRegex.test(line)) { + raw = cap[1] + lines.slice(0, i).join('\n') + '\n'; + break; + } // Until we encounter a blank line, item contents do not need indentation + + + if (!blankLine) { + if (!line.trim()) { + // Check if current line is empty + blankLine = true; + } // Dedent if possible + + + if (line.search(/[^ ]/) >= indent) { + itemContents += '\n' + line.slice(indent); + } else { + itemContents += '\n' + line; + } + + continue; + } // Dedent this line + + + if (line.search(/[^ ]/) >= indent || !line.trim()) { + itemContents += '\n' + line.slice(indent); + continue; + } else { + // Line was not properly indented; end of this item + raw = cap[1] + lines.slice(0, i).join('\n') + '\n'; + break; + } + } + + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } else if (/\n *\n *$/.test(raw)) { + endsWithBlankLine = true; + } + } // Check for task list items + + + if (this.options.gfm) { + istask = /^\[[ xX]\] /.exec(itemContents); + + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(/^\[[ xX]\] +/, ''); + } + } + + list.items.push({ + type: 'list_item', + raw: raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents + }); + list.raw += raw; + src = src.slice(raw.length); + } // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + + + list.items[list.items.length - 1].raw = raw.trimRight(); + list.items[list.items.length - 1].text = itemContents.trimRight(); + list.raw = list.raw.trimRight(); + var l = list.items.length; // Item child tokens handled here at end because we needed to have the final item to trim it first + + for (i = 0; i < l; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + + if (list.items[i].tokens.some(function (t) { + return t.type === 'space'; + })) { + list.loose = true; + list.items[i].loose = true; + } + } + + return list; + } + }; + + _proto.html = function html(src) { + var cap = this.rules.block.html.exec(src); + + if (cap) { + var token = { + type: 'html', + raw: cap[0], + pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), + text: cap[0] + }; + + if (this.options.sanitize) { + token.type = 'paragraph'; + token.text = this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]); + token.tokens = []; + this.lexer.inline(token.text, token.tokens); + } + + return token; + } + }; + + _proto.def = function def(src) { + var cap = this.rules.block.def.exec(src); + + if (cap) { + if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1); + var tag = cap[1].toLowerCase().replace(/\s+/g, ' '); + return { + type: 'def', + tag: tag, + raw: cap[0], + href: cap[2], + title: cap[3] + }; + } + }; + + _proto.table = function table(src) { + var cap = this.rules.block.table.exec(src); + + if (cap) { + var item = { + type: 'table', + header: splitCells(cap[1]).map(function (c) { + return { + text: c + }; + }), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + rows: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] + }; + + if (item.header.length === item.align.length) { + item.raw = cap[0]; + var l = item.align.length; + var i, j, k, row; + + for (i = 0; i < l; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + l = item.rows.length; + + for (i = 0; i < l; i++) { + item.rows[i] = splitCells(item.rows[i], item.header.length).map(function (c) { + return { + text: c + }; + }); + } // parse child tokens inside headers and cells + // header child tokens + + + l = item.header.length; + + for (j = 0; j < l; j++) { + item.header[j].tokens = []; + this.lexer.inlineTokens(item.header[j].text, item.header[j].tokens); + } // cell child tokens + + + l = item.rows.length; + + for (j = 0; j < l; j++) { + row = item.rows[j]; + + for (k = 0; k < row.length; k++) { + row[k].tokens = []; + this.lexer.inlineTokens(row[k].text, row[k].tokens); + } + } + + return item; + } + } + }; + + _proto.lheading = function lheading(src) { + var cap = this.rules.block.lheading.exec(src); + + if (cap) { + var token = { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: [] + }; + this.lexer.inline(token.text, token.tokens); + return token; + } + }; + + _proto.paragraph = function paragraph(src) { + var cap = this.rules.block.paragraph.exec(src); + + if (cap) { + var token = { + type: 'paragraph', + raw: cap[0], + text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1], + tokens: [] + }; + this.lexer.inline(token.text, token.tokens); + return token; + } + }; + + _proto.text = function text(src) { + var cap = this.rules.block.text.exec(src); + + if (cap) { + var token = { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: [] + }; + this.lexer.inline(token.text, token.tokens); + return token; + } + }; + + _proto.escape = function escape$1(src) { + var cap = this.rules.inline.escape.exec(src); + + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: escape(cap[1]) + }; + } + }; + + _proto.tag = function tag(src) { + var cap = this.rules.inline.tag.exec(src); + + if (cap) { + if (!this.lexer.state.inLink && /^/i.test(cap[0])) { + this.lexer.state.inLink = false; + } + + if (!this.lexer.state.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } else if (this.lexer.state.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + + return { + type: this.options.sanitize ? 'text' : 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + text: this.options.sanitize ? this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]) : cap[0] + }; + } + }; + + _proto.link = function link(src) { + var cap = this.rules.inline.link.exec(src); + + if (cap) { + var trimmedUrl = cap[2].trim(); + + if (!this.options.pedantic && /^$/.test(trimmedUrl)) { + return; + } // ending angle bracket cannot be escaped + + + var rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } else { + // find closing parenthesis + var lastParenIndex = findClosingBracket(cap[2], '()'); + + if (lastParenIndex > -1) { + var start = cap[0].indexOf('!') === 0 ? 5 : 4; + var linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + + var href = cap[2]; + var title = ''; + + if (this.options.pedantic) { + // split pedantic href and title + var link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + + if (link) { + href = link[1]; + title = link[3]; + } + } else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + + href = href.trim(); + + if (/^$/.test(trimmedUrl)) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } else { + href = href.slice(1, -1); + } + } + + return outputLink(cap, { + href: href ? href.replace(this.rules.inline._escapes, '$1') : href, + title: title ? title.replace(this.rules.inline._escapes, '$1') : title + }, cap[0], this.lexer); + } + }; + + _proto.reflink = function reflink(src, links) { + var cap; + + if ((cap = this.rules.inline.reflink.exec(src)) || (cap = this.rules.inline.nolink.exec(src))) { + var link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = links[link.toLowerCase()]; + + if (!link || !link.href) { + var text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text: text + }; + } + + return outputLink(cap, link, cap[0], this.lexer); + } + }; + + _proto.emStrong = function emStrong(src, maskedSrc, prevChar) { + if (prevChar === void 0) { + prevChar = ''; + } + + var match = this.rules.inline.emStrong.lDelim.exec(src); + if (!match) return; // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + + if (match[3] && prevChar.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDF70-\uDF81\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE70-\uDEBE\uDEC0-\uDEC9\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/)) return; + var nextChar = match[1] || match[2] || ''; + + if (!nextChar || nextChar && (prevChar === '' || this.rules.inline.punctuation.exec(prevChar))) { + var lLength = match[0].length - 1; + var rDelim, + rLength, + delimTotal = lLength, + midDelimTotal = 0; + var endReg = match[0][0] === '*' ? this.rules.inline.emStrong.rDelimAst : this.rules.inline.emStrong.rDelimUnd; + endReg.lastIndex = 0; // Clip maskedSrc to same section of string as src (move to lexer?) + + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + if (!rDelim) continue; // skip single * in __abc*abc__ + + rLength = rDelim.length; + + if (match[3] || match[4]) { + // found another Left Delim + delimTotal += rLength; + continue; + } else if (match[5] || match[6]) { + // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + + delimTotal -= rLength; + if (delimTotal > 0) continue; // Haven't found enough closing delimiters + // Remove extra characters. *a*** -> *a* + + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); // Create `em` if smallest delimiter has odd char count. *a*** + + if (Math.min(lLength, rLength) % 2) { + var _text = src.slice(1, lLength + match.index + rLength); + + return { + type: 'em', + raw: src.slice(0, lLength + match.index + rLength + 1), + text: _text, + tokens: this.lexer.inlineTokens(_text, []) + }; + } // Create 'strong' if smallest delimiter has even char count. **a*** + + + var text = src.slice(2, lLength + match.index + rLength - 1); + return { + type: 'strong', + raw: src.slice(0, lLength + match.index + rLength + 1), + text: text, + tokens: this.lexer.inlineTokens(text, []) + }; + } + } + }; + + _proto.codespan = function codespan(src) { + var cap = this.rules.inline.code.exec(src); + + if (cap) { + var text = cap[2].replace(/\n/g, ' '); + var hasNonSpaceChars = /[^ ]/.test(text); + var hasSpaceCharsOnBothEnds = /^ /.test(text) && / $/.test(text); + + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + + text = escape(text, true); + return { + type: 'codespan', + raw: cap[0], + text: text + }; + } + }; + + _proto.br = function br(src) { + var cap = this.rules.inline.br.exec(src); + + if (cap) { + return { + type: 'br', + raw: cap[0] + }; + } + }; + + _proto.del = function del(src) { + var cap = this.rules.inline.del.exec(src); + + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2], []) + }; + } + }; + + _proto.autolink = function autolink(src, mangle) { + var cap = this.rules.inline.autolink.exec(src); + + if (cap) { + var text, href; + + if (cap[2] === '@') { + text = escape(this.options.mangle ? mangle(cap[1]) : cap[1]); + href = 'mailto:' + text; + } else { + text = escape(cap[1]); + href = text; + } + + return { + type: 'link', + raw: cap[0], + text: text, + href: href, + tokens: [{ + type: 'text', + raw: text, + text: text + }] + }; + } + }; + + _proto.url = function url(src, mangle) { + var cap; + + if (cap = this.rules.inline.url.exec(src)) { + var text, href; + + if (cap[2] === '@') { + text = escape(this.options.mangle ? mangle(cap[0]) : cap[0]); + href = 'mailto:' + text; + } else { + // do extended autolink path validation + var prevCapZero; + + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])[0]; + } while (prevCapZero !== cap[0]); + + text = escape(cap[0]); + + if (cap[1] === 'www.') { + href = 'http://' + text; + } else { + href = text; + } + } + + return { + type: 'link', + raw: cap[0], + text: text, + href: href, + tokens: [{ + type: 'text', + raw: text, + text: text + }] + }; + } + }; + + _proto.inlineText = function inlineText(src, smartypants) { + var cap = this.rules.inline.text.exec(src); + + if (cap) { + var text; + + if (this.lexer.state.inRawBlock) { + text = this.options.sanitize ? this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]) : cap[0]; + } else { + text = escape(this.options.smartypants ? smartypants(cap[0]) : cap[0]); + } + + return { + type: 'text', + raw: cap[0], + text: text + }; + } + }; + + return Tokenizer; + }(); + + /** + * Block-Level Grammar + */ + + var block = { + newline: /^(?: *(?:\n|$))+/, + code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/, + fences: /^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?=\n|$)|$)/, + hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/, + heading: /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/, + blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, + list: /^( {0,3}bull)( [^\n]+?)?(?:\n|$)/, + html: '^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) closing tag + + ')', + def: /^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/, + table: noopTest, + lheading: /^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/, + // regex template, placeholders will be replaced according to different paragraph + // interruption rules of commonmark and the original markdown spec: + _paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/, + text: /^[^\n]+/ + }; + block._label = /(?!\s*\])(?:\\[\[\]]|[^\[\]])+/; + block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; + block.def = edit(block.def).replace('label', block._label).replace('title', block._title).getRegex(); + block.bullet = /(?:[*+-]|\d{1,9}[.)])/; + block.listItemStart = edit(/^( *)(bull) */).replace('bull', block.bullet).getRegex(); + block.list = edit(block.list).replace(/bull/g, block.bullet).replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))').replace('def', '\\n+(?=' + block.def.source + ')').getRegex(); + block._tag = 'address|article|aside|base|basefont|blockquote|body|caption' + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr' + '|track|ul'; + block._comment = /|$)/; + block.html = edit(block.html, 'i').replace('comment', block._comment).replace('tag', block._tag).replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(); + block.paragraph = edit(block._paragraph).replace('hr', block.hr).replace('heading', ' {0,3}#{1,6} ').replace('|lheading', '') // setex headings don't interrupt commonmark paragraphs + .replace('blockquote', ' {0,3}>').replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n').replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)').replace('tag', block._tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + block.blockquote = edit(block.blockquote).replace('paragraph', block.paragraph).getRegex(); + /** + * Normal Block Grammar + */ + + block.normal = merge({}, block); + /** + * GFM Block Grammar + */ + + block.gfm = merge({}, block.normal, { + table: '^ *([^\\n ].*\\|.*)\\n' // Header + + ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)' // Cells + + }); + block.gfm.table = edit(block.gfm.table).replace('hr', block.hr).replace('heading', ' {0,3}#{1,6} ').replace('blockquote', ' {0,3}>').replace('code', ' {4}[^\\n]').replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n').replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)').replace('tag', block._tag) // tables can be interrupted by type (6) html blocks + .getRegex(); + /** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ + + block.pedantic = merge({}, block.normal, { + html: edit('^ *(?:comment *(?:\\n|\\s*$)' + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))').replace('comment', block._comment).replace(/tag/g, '(?!(?:' + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b').getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, + // fences not supported + paragraph: edit(block.normal._paragraph).replace('hr', block.hr).replace('heading', ' *#{1,6} *[^\n]').replace('lheading', block.lheading).replace('blockquote', ' {0,3}>').replace('|fences', '').replace('|list', '').replace('|html', '').getRegex() + }); + /** + * Inline-Level Grammar + */ + + var inline = { + escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, + autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/, + url: noopTest, + tag: '^comment' + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^', + // CDATA section + link: /^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/, + reflink: /^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/, + nolink: /^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/, + reflinkSearch: 'reflink|nolink(?!\\()', + emStrong: { + lDelim: /^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/, + // (1) and (2) can only be a Right Delimiter. (3) and (4) can only be Left. (5) and (6) can be either Left or Right. + // () Skip orphan delim inside strong (1) #*** (2) a***#, a*** (3) #***a, ***a (4) ***# (5) #***# (6) a***a + rDelimAst: /^[^_*]*?\_\_[^_*]*?\*[^_*]*?(?=\_\_)|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/, + rDelimUnd: /^[^_*]*?\*\*[^_*]*?\_[^_*]*?(?=\*\*)|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/ // ^- Not allowed for _ + + }, + code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, + br: /^( {2,}|\\)\n(?!\s*$)/, + del: noopTest, + text: /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~'; + inline.punctuation = edit(inline.punctuation).replace(/punctuation/g, inline._punctuation).getRegex(); // sequences em should skip over [title](link), `code`, + + inline.blockSkip = /\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g; + inline.escapedEmSt = /\\\*|\\_/g; + inline._comment = edit(block._comment).replace('(?:-->|$)', '-->').getRegex(); + inline.emStrong.lDelim = edit(inline.emStrong.lDelim).replace(/punct/g, inline._punctuation).getRegex(); + inline.emStrong.rDelimAst = edit(inline.emStrong.rDelimAst, 'g').replace(/punct/g, inline._punctuation).getRegex(); + inline.emStrong.rDelimUnd = edit(inline.emStrong.rDelimUnd, 'g').replace(/punct/g, inline._punctuation).getRegex(); + inline._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g; + inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; + inline._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/; + inline.autolink = edit(inline.autolink).replace('scheme', inline._scheme).replace('email', inline._email).getRegex(); + inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; + inline.tag = edit(inline.tag).replace('comment', inline._comment).replace('attribute', inline._attribute).getRegex(); + inline._label = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; + inline._href = /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/; + inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; + inline.link = edit(inline.link).replace('label', inline._label).replace('href', inline._href).replace('title', inline._title).getRegex(); + inline.reflink = edit(inline.reflink).replace('label', inline._label).getRegex(); + inline.reflinkSearch = edit(inline.reflinkSearch, 'g').replace('reflink', inline.reflink).replace('nolink', inline.nolink).getRegex(); + /** + * Normal Inline Grammar + */ + + inline.normal = merge({}, inline); + /** + * Pedantic Inline Grammar + */ + + inline.pedantic = merge({}, inline.normal, { + strong: { + start: /^__|\*\*/, + middle: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + endAst: /\*\*(?!\*)/g, + endUnd: /__(?!_)/g + }, + em: { + start: /^_|\*/, + middle: /^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/, + endAst: /\*(?!\*)/g, + endUnd: /_(?!_)/g + }, + link: edit(/^!?\[(label)\]\((.*?)\)/).replace('label', inline._label).getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace('label', inline._label).getRegex() + }); + /** + * GFM Inline Grammar + */ + + inline.gfm = merge({}, inline.normal, { + escape: edit(inline.escape).replace('])', '~|])').getRegex(), + _extended_email: /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/, + url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, + _backpedal: /(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\ 0.5) { + ch = 'x' + ch.toString(16); + } + + out += '&#' + ch + ';'; + } + + return out; + } + /** + * Block Lexer + */ + + + var Lexer = /*#__PURE__*/function () { + function Lexer(options) { + this.tokens = []; + this.tokens.links = Object.create(null); + this.options = options || exports.defaults; + this.options.tokenizer = this.options.tokenizer || new Tokenizer(); + this.tokenizer = this.options.tokenizer; + this.tokenizer.options = this.options; + this.tokenizer.lexer = this; + this.inlineQueue = []; + this.state = { + inLink: false, + inRawBlock: false, + top: true + }; + var rules = { + block: block.normal, + inline: inline.normal + }; + + if (this.options.pedantic) { + rules.block = block.pedantic; + rules.inline = inline.pedantic; + } else if (this.options.gfm) { + rules.block = block.gfm; + + if (this.options.breaks) { + rules.inline = inline.breaks; + } else { + rules.inline = inline.gfm; + } + } + + this.tokenizer.rules = rules; + } + /** + * Expose Rules + */ + + + /** + * Static Lex Method + */ + Lexer.lex = function lex(src, options) { + var lexer = new Lexer(options); + return lexer.lex(src); + } + /** + * Static Lex Inline Method + */ + ; + + Lexer.lexInline = function lexInline(src, options) { + var lexer = new Lexer(options); + return lexer.inlineTokens(src); + } + /** + * Preprocessing + */ + ; + + var _proto = Lexer.prototype; + + _proto.lex = function lex(src) { + src = src.replace(/\r\n|\r/g, '\n').replace(/\t/g, ' '); + this.blockTokens(src, this.tokens); + var next; + + while (next = this.inlineQueue.shift()) { + this.inlineTokens(next.src, next.tokens); + } + + return this.tokens; + } + /** + * Lexing + */ + ; + + _proto.blockTokens = function blockTokens(src, tokens) { + var _this = this; + + if (tokens === void 0) { + tokens = []; + } + + if (this.options.pedantic) { + src = src.replace(/^ +$/gm, ''); + } + + var token, lastToken, cutSrc, lastParagraphClipped; + + while (src) { + if (this.options.extensions && this.options.extensions.block && this.options.extensions.block.some(function (extTokenizer) { + if (token = extTokenizer.call({ + lexer: _this + }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + + return false; + })) { + continue; + } // newline + + + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + + if (token.type) { + tokens.push(token); + } + + continue; + } // code + + + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; // An indented code block cannot interrupt a paragraph. + + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } else { + tokens.push(token); + } + + continue; + } // fences + + + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // heading + + + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // hr + + + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // blockquote + + + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // list + + + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // html + + + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // def + + + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title + }; + } + + continue; + } // table (gfm) + + + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // lheading + + + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + + + cutSrc = src; + + if (this.options.extensions && this.options.extensions.startBlock) { + (function () { + var startIndex = Infinity; + var tempSrc = src.slice(1); + var tempStart = void 0; + + _this.options.extensions.startBlock.forEach(function (getStartIndex) { + tempStart = getStartIndex.call({ + lexer: this + }, tempSrc); + + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + })(); + } + + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + lastToken = tokens[tokens.length - 1]; + + if (lastParagraphClipped && lastToken.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } else { + tokens.push(token); + } + + lastParagraphClipped = cutSrc.length !== src.length; + src = src.substring(token.raw.length); + continue; + } // text + + + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + + if (lastToken && lastToken.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } else { + tokens.push(token); + } + + continue; + } + + if (src) { + var errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + + if (this.options.silent) { + console.error(errMsg); + break; + } else { + throw new Error(errMsg); + } + } + } + + this.state.top = true; + return tokens; + }; + + _proto.inline = function inline(src, tokens) { + this.inlineQueue.push({ + src: src, + tokens: tokens + }); + } + /** + * Lexing/Compiling + */ + ; + + _proto.inlineTokens = function inlineTokens(src, tokens) { + var _this2 = this; + + if (tokens === void 0) { + tokens = []; + } + + var token, lastToken, cutSrc; // String with links masked to avoid interference with em and strong + + var maskedSrc = src; + var match; + var keepPrevChar, prevChar; // Mask out reflinks + + if (this.tokens.links) { + var links = Object.keys(this.tokens.links); + + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + repeatString('a', match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } // Mask out other blocks + + + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + repeatString('a', match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } // Mask out escaped em & strong delimiters + + + while ((match = this.tokenizer.rules.inline.escapedEmSt.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex); + } + + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + + keepPrevChar = false; // extensions + + if (this.options.extensions && this.options.extensions.inline && this.options.extensions.inline.some(function (extTokenizer) { + if (token = extTokenizer.call({ + lexer: _this2 + }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + + return false; + })) { + continue; + } // escape + + + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // tag + + + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } else { + tokens.push(token); + } + + continue; + } // link + + + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // reflink, nolink + + + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } else { + tokens.push(token); + } + + continue; + } // em & strong + + + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // code + + + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // br + + + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // del (gfm) + + + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // autolink + + + if (token = this.tokenizer.autolink(src, mangle)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // url (gfm) + + + if (!this.state.inLink && (token = this.tokenizer.url(src, mangle))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + + + cutSrc = src; + + if (this.options.extensions && this.options.extensions.startInline) { + (function () { + var startIndex = Infinity; + var tempSrc = src.slice(1); + var tempStart = void 0; + + _this2.options.extensions.startInline.forEach(function (getStartIndex) { + tempStart = getStartIndex.call({ + lexer: this + }, tempSrc); + + if (typeof tempStart === 'number' && tempStart >= 0) { + startIndex = Math.min(startIndex, tempStart); + } + }); + + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + })(); + } + + if (token = this.tokenizer.inlineText(cutSrc, smartypants)) { + src = src.substring(token.raw.length); + + if (token.raw.slice(-1) !== '_') { + // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + + keepPrevChar = true; + lastToken = tokens[tokens.length - 1]; + + if (lastToken && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } else { + tokens.push(token); + } + + continue; + } + + if (src) { + var errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + + if (this.options.silent) { + console.error(errMsg); + break; + } else { + throw new Error(errMsg); + } + } + } + + return tokens; + }; + + _createClass(Lexer, null, [{ + key: "rules", + get: function get() { + return { + block: block, + inline: inline + }; + } + }]); + + return Lexer; + }(); + + /** + * Renderer + */ + + var Renderer = /*#__PURE__*/function () { + function Renderer(options) { + this.options = options || exports.defaults; + } + + var _proto = Renderer.prototype; + + _proto.code = function code(_code, infostring, escaped) { + var lang = (infostring || '').match(/\S*/)[0]; + + if (this.options.highlight) { + var out = this.options.highlight(_code, lang); + + if (out != null && out !== _code) { + escaped = true; + _code = out; + } + } + + _code = _code.replace(/\n$/, '') + '\n'; + + if (!lang) { + return '
' + (escaped ? _code : escape(_code, true)) + '
\n'; + } + + return '
' + (escaped ? _code : escape(_code, true)) + '
\n'; + }; + + _proto.blockquote = function blockquote(quote) { + return '
\n' + quote + '
\n'; + }; + + _proto.html = function html(_html) { + return _html; + }; + + _proto.heading = function heading(text, level, raw, slugger) { + if (this.options.headerIds) { + return '' + text + '\n'; + } // ignore IDs + + + return '' + text + '\n'; + }; + + _proto.hr = function hr() { + return this.options.xhtml ? '
\n' : '
\n'; + }; + + _proto.list = function list(body, ordered, start) { + var type = ordered ? 'ol' : 'ul', + startatt = ordered && start !== 1 ? ' start="' + start + '"' : ''; + return '<' + type + startatt + '>\n' + body + '\n'; + }; + + _proto.listitem = function listitem(text) { + return '
  • ' + text + '
  • \n'; + }; + + _proto.checkbox = function checkbox(checked) { + return ' '; + }; + + _proto.paragraph = function paragraph(text) { + return '

    ' + text + '

    \n'; + }; + + _proto.table = function table(header, body) { + if (body) body = '' + body + ''; + return '\n' + '\n' + header + '\n' + body + '
    \n'; + }; + + _proto.tablerow = function tablerow(content) { + return '\n' + content + '\n'; + }; + + _proto.tablecell = function tablecell(content, flags) { + var type = flags.header ? 'th' : 'td'; + var tag = flags.align ? '<' + type + ' align="' + flags.align + '">' : '<' + type + '>'; + return tag + content + '\n'; + } // span level renderer + ; + + _proto.strong = function strong(text) { + return '' + text + ''; + }; + + _proto.em = function em(text) { + return '' + text + ''; + }; + + _proto.codespan = function codespan(text) { + return '' + text + ''; + }; + + _proto.br = function br() { + return this.options.xhtml ? '
    ' : '
    '; + }; + + _proto.del = function del(text) { + return '' + text + ''; + }; + + _proto.link = function link(href, title, text) { + href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + + if (href === null) { + return text; + } + + var out = '
    '; + return out; + }; + + _proto.image = function image(href, title, text) { + href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + + if (href === null) { + return text; + } + + var out = '' + text + '' : '>'; + return out; + }; + + _proto.text = function text(_text) { + return _text; + }; + + return Renderer; + }(); + + /** + * TextRenderer + * returns only the textual part of the token + */ + var TextRenderer = /*#__PURE__*/function () { + function TextRenderer() {} + + var _proto = TextRenderer.prototype; + + // no need for block level renderers + _proto.strong = function strong(text) { + return text; + }; + + _proto.em = function em(text) { + return text; + }; + + _proto.codespan = function codespan(text) { + return text; + }; + + _proto.del = function del(text) { + return text; + }; + + _proto.html = function html(text) { + return text; + }; + + _proto.text = function text(_text) { + return _text; + }; + + _proto.link = function link(href, title, text) { + return '' + text; + }; + + _proto.image = function image(href, title, text) { + return '' + text; + }; + + _proto.br = function br() { + return ''; + }; + + return TextRenderer; + }(); + + /** + * Slugger generates header id + */ + var Slugger = /*#__PURE__*/function () { + function Slugger() { + this.seen = {}; + } + + var _proto = Slugger.prototype; + + _proto.serialize = function serialize(value) { + return value.toLowerCase().trim() // remove html tags + .replace(/<[!\/a-z].*?>/ig, '') // remove unwanted chars + .replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '').replace(/\s/g, '-'); + } + /** + * Finds the next safe (unique) slug to use + */ + ; + + _proto.getNextSafeSlug = function getNextSafeSlug(originalSlug, isDryRun) { + var slug = originalSlug; + var occurenceAccumulator = 0; + + if (this.seen.hasOwnProperty(slug)) { + occurenceAccumulator = this.seen[originalSlug]; + + do { + occurenceAccumulator++; + slug = originalSlug + '-' + occurenceAccumulator; + } while (this.seen.hasOwnProperty(slug)); + } + + if (!isDryRun) { + this.seen[originalSlug] = occurenceAccumulator; + this.seen[slug] = 0; + } + + return slug; + } + /** + * Convert string to unique id + * @param {object} options + * @param {boolean} options.dryrun Generates the next unique slug without updating the internal accumulator. + */ + ; + + _proto.slug = function slug(value, options) { + if (options === void 0) { + options = {}; + } + + var slug = this.serialize(value); + return this.getNextSafeSlug(slug, options.dryrun); + }; + + return Slugger; + }(); + + /** + * Parsing & Compiling + */ + + var Parser = /*#__PURE__*/function () { + function Parser(options) { + this.options = options || exports.defaults; + this.options.renderer = this.options.renderer || new Renderer(); + this.renderer = this.options.renderer; + this.renderer.options = this.options; + this.textRenderer = new TextRenderer(); + this.slugger = new Slugger(); + } + /** + * Static Parse Method + */ + + + Parser.parse = function parse(tokens, options) { + var parser = new Parser(options); + return parser.parse(tokens); + } + /** + * Static Parse Inline Method + */ + ; + + Parser.parseInline = function parseInline(tokens, options) { + var parser = new Parser(options); + return parser.parseInline(tokens); + } + /** + * Parse Loop + */ + ; + + var _proto = Parser.prototype; + + _proto.parse = function parse(tokens, top) { + if (top === void 0) { + top = true; + } + + var out = '', + i, + j, + k, + l2, + l3, + row, + cell, + header, + body, + token, + ordered, + start, + loose, + itemBody, + item, + checked, + task, + checkbox, + ret; + var l = tokens.length; + + for (i = 0; i < l; i++) { + token = tokens[i]; // Run any renderer extensions + + if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) { + ret = this.options.extensions.renderers[token.type].call({ + parser: this + }, token); + + if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'paragraph', 'text'].includes(token.type)) { + out += ret || ''; + continue; + } + } + + switch (token.type) { + case 'space': + { + continue; + } + + case 'hr': + { + out += this.renderer.hr(); + continue; + } + + case 'heading': + { + out += this.renderer.heading(this.parseInline(token.tokens), token.depth, unescape(this.parseInline(token.tokens, this.textRenderer)), this.slugger); + continue; + } + + case 'code': + { + out += this.renderer.code(token.text, token.lang, token.escaped); + continue; + } + + case 'table': + { + header = ''; // header + + cell = ''; + l2 = token.header.length; + + for (j = 0; j < l2; j++) { + cell += this.renderer.tablecell(this.parseInline(token.header[j].tokens), { + header: true, + align: token.align[j] + }); + } + + header += this.renderer.tablerow(cell); + body = ''; + l2 = token.rows.length; + + for (j = 0; j < l2; j++) { + row = token.rows[j]; + cell = ''; + l3 = row.length; + + for (k = 0; k < l3; k++) { + cell += this.renderer.tablecell(this.parseInline(row[k].tokens), { + header: false, + align: token.align[k] + }); + } + + body += this.renderer.tablerow(cell); + } + + out += this.renderer.table(header, body); + continue; + } + + case 'blockquote': + { + body = this.parse(token.tokens); + out += this.renderer.blockquote(body); + continue; + } + + case 'list': + { + ordered = token.ordered; + start = token.start; + loose = token.loose; + l2 = token.items.length; + body = ''; + + for (j = 0; j < l2; j++) { + item = token.items[j]; + checked = item.checked; + task = item.task; + itemBody = ''; + + if (item.task) { + checkbox = this.renderer.checkbox(checked); + + if (loose) { + if (item.tokens.length > 0 && item.tokens[0].type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text; + } + } else { + item.tokens.unshift({ + type: 'text', + text: checkbox + }); + } + } else { + itemBody += checkbox; + } + } + + itemBody += this.parse(item.tokens, loose); + body += this.renderer.listitem(itemBody, task, checked); + } + + out += this.renderer.list(body, ordered, start); + continue; + } + + case 'html': + { + // TODO parse inline content if parameter markdown=1 + out += this.renderer.html(token.text); + continue; + } + + case 'paragraph': + { + out += this.renderer.paragraph(this.parseInline(token.tokens)); + continue; + } + + case 'text': + { + body = token.tokens ? this.parseInline(token.tokens) : token.text; + + while (i + 1 < l && tokens[i + 1].type === 'text') { + token = tokens[++i]; + body += '\n' + (token.tokens ? this.parseInline(token.tokens) : token.text); + } + + out += top ? this.renderer.paragraph(body) : body; + continue; + } + + default: + { + var errMsg = 'Token with "' + token.type + '" type was not found.'; + + if (this.options.silent) { + console.error(errMsg); + return; + } else { + throw new Error(errMsg); + } + } + } + } + + return out; + } + /** + * Parse Inline Tokens + */ + ; + + _proto.parseInline = function parseInline(tokens, renderer) { + renderer = renderer || this.renderer; + var out = '', + i, + token, + ret; + var l = tokens.length; + + for (i = 0; i < l; i++) { + token = tokens[i]; // Run any renderer extensions + + if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) { + ret = this.options.extensions.renderers[token.type].call({ + parser: this + }, token); + + if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(token.type)) { + out += ret || ''; + continue; + } + } + + switch (token.type) { + case 'escape': + { + out += renderer.text(token.text); + break; + } + + case 'html': + { + out += renderer.html(token.text); + break; + } + + case 'link': + { + out += renderer.link(token.href, token.title, this.parseInline(token.tokens, renderer)); + break; + } + + case 'image': + { + out += renderer.image(token.href, token.title, token.text); + break; + } + + case 'strong': + { + out += renderer.strong(this.parseInline(token.tokens, renderer)); + break; + } + + case 'em': + { + out += renderer.em(this.parseInline(token.tokens, renderer)); + break; + } + + case 'codespan': + { + out += renderer.codespan(token.text); + break; + } + + case 'br': + { + out += renderer.br(); + break; + } + + case 'del': + { + out += renderer.del(this.parseInline(token.tokens, renderer)); + break; + } + + case 'text': + { + out += renderer.text(token.text); + break; + } + + default: + { + var errMsg = 'Token with "' + token.type + '" type was not found.'; + + if (this.options.silent) { + console.error(errMsg); + return; + } else { + throw new Error(errMsg); + } + } + } + } + + return out; + }; + + return Parser; + }(); + + /** + * Marked + */ + + function marked(src, opt, callback) { + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + throw new Error('marked(): input parameter is undefined or null'); + } + + if (typeof src !== 'string') { + throw new Error('marked(): input parameter is of type ' + Object.prototype.toString.call(src) + ', string expected'); + } + + if (typeof opt === 'function') { + callback = opt; + opt = null; + } + + opt = merge({}, marked.defaults, opt || {}); + checkSanitizeDeprecation(opt); + + if (callback) { + var highlight = opt.highlight; + var tokens; + + try { + tokens = Lexer.lex(src, opt); + } catch (e) { + return callback(e); + } + + var done = function done(err) { + var out; + + if (!err) { + try { + if (opt.walkTokens) { + marked.walkTokens(tokens, opt.walkTokens); + } + + out = Parser.parse(tokens, opt); + } catch (e) { + err = e; + } + } + + opt.highlight = highlight; + return err ? callback(err) : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + if (!tokens.length) return done(); + var pending = 0; + marked.walkTokens(tokens, function (token) { + if (token.type === 'code') { + pending++; + setTimeout(function () { + highlight(token.text, token.lang, function (err, code) { + if (err) { + return done(err); + } + + if (code != null && code !== token.text) { + token.text = code; + token.escaped = true; + } + + pending--; + + if (pending === 0) { + done(); + } + }); + }, 0); + } + }); + + if (pending === 0) { + done(); + } + + return; + } + + try { + var _tokens = Lexer.lex(src, opt); + + if (opt.walkTokens) { + marked.walkTokens(_tokens, opt.walkTokens); + } + + return Parser.parse(_tokens, opt); + } catch (e) { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + + if (opt.silent) { + return '

    An error occurred:

    ' + escape(e.message + '', true) + '
    '; + } + + throw e; + } + } + /** + * Options + */ + + marked.options = marked.setOptions = function (opt) { + merge(marked.defaults, opt); + changeDefaults(marked.defaults); + return marked; + }; + + marked.getDefaults = getDefaults; + marked.defaults = exports.defaults; + /** + * Use Extension + */ + + marked.use = function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var opts = merge.apply(void 0, [{}].concat(args)); + var extensions = marked.defaults.extensions || { + renderers: {}, + childTokens: {} + }; + var hasExtensions; + args.forEach(function (pack) { + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + hasExtensions = true; + pack.extensions.forEach(function (ext) { + if (!ext.name) { + throw new Error('extension name required'); + } + + if (ext.renderer) { + // Renderer extensions + var prevRenderer = extensions.renderers ? extensions.renderers[ext.name] : null; + + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + var ret = ext.renderer.apply(this, args); + + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + + return ret; + }; + } else { + extensions.renderers[ext.name] = ext.renderer; + } + } + + if (ext.tokenizer) { + // Tokenizer Extensions + if (!ext.level || ext.level !== 'block' && ext.level !== 'inline') { + throw new Error("extension level must be 'block' or 'inline'"); + } + + if (extensions[ext.level]) { + extensions[ext.level].unshift(ext.tokenizer); + } else { + extensions[ext.level] = [ext.tokenizer]; + } + + if (ext.start) { + // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } else { + extensions.startBlock = [ext.start]; + } + } else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } else { + extensions.startInline = [ext.start]; + } + } + } + } + + if (ext.childTokens) { + // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + } // ==-- Parse "overwrite" extensions --== // + + + if (pack.renderer) { + (function () { + var renderer = marked.defaults.renderer || new Renderer(); + + var _loop = function _loop(prop) { + var prevRenderer = renderer[prop]; // Replace renderer with func to run extension, but fall back if false + + renderer[prop] = function () { + for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + + var ret = pack.renderer[prop].apply(renderer, args); + + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + + return ret; + }; + }; + + for (var prop in pack.renderer) { + _loop(prop); + } + + opts.renderer = renderer; + })(); + } + + if (pack.tokenizer) { + (function () { + var tokenizer = marked.defaults.tokenizer || new Tokenizer(); + + var _loop2 = function _loop2(prop) { + var prevTokenizer = tokenizer[prop]; // Replace tokenizer with func to run extension, but fall back if false + + tokenizer[prop] = function () { + for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + args[_key4] = arguments[_key4]; + } + + var ret = pack.tokenizer[prop].apply(tokenizer, args); + + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + + return ret; + }; + }; + + for (var prop in pack.tokenizer) { + _loop2(prop); + } + + opts.tokenizer = tokenizer; + })(); + } // ==-- Parse WalkTokens extensions --== // + + + if (pack.walkTokens) { + var _walkTokens = marked.defaults.walkTokens; + + opts.walkTokens = function (token) { + pack.walkTokens.call(this, token); + + if (_walkTokens) { + _walkTokens.call(this, token); + } + }; + } + + if (hasExtensions) { + opts.extensions = extensions; + } + + marked.setOptions(opts); + }); + }; + /** + * Run callback for every token + */ + + + marked.walkTokens = function (tokens, callback) { + var _loop3 = function _loop3() { + var token = _step.value; + callback.call(marked, token); + + switch (token.type) { + case 'table': + { + for (var _iterator2 = _createForOfIteratorHelperLoose(token.header), _step2; !(_step2 = _iterator2()).done;) { + var cell = _step2.value; + marked.walkTokens(cell.tokens, callback); + } + + for (var _iterator3 = _createForOfIteratorHelperLoose(token.rows), _step3; !(_step3 = _iterator3()).done;) { + var row = _step3.value; + + for (var _iterator4 = _createForOfIteratorHelperLoose(row), _step4; !(_step4 = _iterator4()).done;) { + var _cell = _step4.value; + marked.walkTokens(_cell.tokens, callback); + } + } + + break; + } + + case 'list': + { + marked.walkTokens(token.items, callback); + break; + } + + default: + { + if (marked.defaults.extensions && marked.defaults.extensions.childTokens && marked.defaults.extensions.childTokens[token.type]) { + // Walk any extensions + marked.defaults.extensions.childTokens[token.type].forEach(function (childTokens) { + marked.walkTokens(token[childTokens], callback); + }); + } else if (token.tokens) { + marked.walkTokens(token.tokens, callback); + } + } + } + }; + + for (var _iterator = _createForOfIteratorHelperLoose(tokens), _step; !(_step = _iterator()).done;) { + _loop3(); + } + }; + /** + * Parse Inline + */ + + + marked.parseInline = function (src, opt) { + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + throw new Error('marked.parseInline(): input parameter is undefined or null'); + } + + if (typeof src !== 'string') { + throw new Error('marked.parseInline(): input parameter is of type ' + Object.prototype.toString.call(src) + ', string expected'); + } + + opt = merge({}, marked.defaults, opt || {}); + checkSanitizeDeprecation(opt); + + try { + var tokens = Lexer.lexInline(src, opt); + + if (opt.walkTokens) { + marked.walkTokens(tokens, opt.walkTokens); + } + + return Parser.parseInline(tokens, opt); + } catch (e) { + e.message += '\nPlease report this to https://github.com/markedjs/marked.'; + + if (opt.silent) { + return '

    An error occurred:

    ' + escape(e.message + '', true) + '
    '; + } + + throw e; + } + }; + /** + * Expose + */ + + + marked.Parser = Parser; + marked.parser = Parser.parse; + marked.Renderer = Renderer; + marked.TextRenderer = TextRenderer; + marked.Lexer = Lexer; + marked.lexer = Lexer.lex; + marked.Tokenizer = Tokenizer; + marked.Slugger = Slugger; + marked.parse = marked; + var options = marked.options; + var setOptions = marked.setOptions; + var use = marked.use; + var walkTokens = marked.walkTokens; + var parseInline = marked.parseInline; + var parse = marked; + var parser = Parser.parse; + var lexer = Lexer.lex; + + exports.Lexer = Lexer; + exports.Parser = Parser; + exports.Renderer = Renderer; + exports.Slugger = Slugger; + exports.TextRenderer = TextRenderer; + exports.Tokenizer = Tokenizer; + exports.getDefaults = getDefaults; + exports.lexer = lexer; + exports.marked = marked; + exports.options = options; + exports.parse = parse; + exports.parseInline = parseInline; + exports.parser = parser; + exports.setOptions = setOptions; + exports.use = use; + exports.walkTokens = walkTokens; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/frontend/packages/shared/src/terminal.css b/hive-fr0nt/assets/terminal.css similarity index 100% rename from frontend/packages/shared/src/terminal.css rename to hive-fr0nt/assets/terminal.css diff --git a/hive-fr0nt/assets/terminal.js b/hive-fr0nt/assets/terminal.js new file mode 100644 index 00000000..fd30ea21 --- /dev/null +++ b/hive-fr0nt/assets/terminal.js @@ -0,0 +1,344 @@ +// Shared terminal pane: sticky-bottom log + "↓ N new" pill + history +// backfill + live SSE. Pages provide a kind→renderer map; this module +// owns scroll behaviour, animation suppression on backfill, and the +// EventSource lifecycle. +// +// Usage: +// +// HiveTerminal.create({ +// logEl: document.getElementById('msgflow'), +// historyUrl: '/messages/history?limit=200', // optional +// streamUrl: '/messages/stream', +// renderers: { +// sent: (ev, api) => api.row('msgrow sent', ...), +// delivered: (ev, api) => api.row('msgrow delivered', ...), +// _default: (ev, api) => api.row('note', JSON.stringify(ev)), +// }, +// onLiveEvent: (ev) => { /* live-only side effects (notif, state pokes) */ }, +// onAnyEvent: (ev, { fromHistory }) => { /* runs for every event in +// both backfill replay and live — use for derived views that need +// the full picture (e.g. a per-recipient inbox built from broker +// events) */ }, +// onBackfillDone: (count) => { /* one-shot after history replay */ }, +// onStreamOpen: () => { /* fires on every EventSource (re)connect — +// use to re-sync snapshot-derived state after a reconnect gap */ }, +// pillAnchor: document.getElementById('msgflow').parentElement, +// }); +// +// Renderers receive (ev, api) where api exposes: +// +// api.row(cls, text) → appends a flat
    +// api.details(cls, summary, body) → appends
    +// with a +// api.detailsDiff(cls, summary, body) → ditto but body is line-coloured by +// leading "+ " / "- " prefix +// api.placeholder(text) → replaces log content with a single +// muted "(placeholder)" row, cleared +// on the next real row +// api.fromHistory → true while backfill is replaying +// +// Each kind is dispatched to `renderers[ev.kind]`; unknown kinds fall +// through to `renderers._default` (which itself defaults to a JSON-dump +// note row). The convention is that the SSE/history endpoints emit +// objects with a `kind` field. +// +// Backfill is best-effort: if `historyUrl` is unset or the fetch fails, +// we skip straight to SSE. The optional `onBackfillDone(count)` hook +// fires after replay finishes (or after a failed/skipped fetch with +// count=0); pages use it to set state flags from the replayed history. + +(function () { + const NEAR_BOTTOM_PX = 48; + + function create(opts) { + const log = opts.logEl; + if (!log) throw new Error('HiveTerminal.create: logEl is required'); + const renderers = opts.renderers || {}; + const defaultRender = renderers._default + || ((ev, api) => api.row('note', JSON.stringify(ev))); + const pillAnchor = opts.pillAnchor || log.parentElement || log; + + let placeholderEl = null; + let pill = null; + let unseen = 0; + let currentNoAnim = false; + + function isNearBottom() { + return log.scrollHeight - log.scrollTop - log.clientHeight <= NEAR_BOTTOM_PX; + } + function ensurePill() { + if (pill) return pill; + pill = document.createElement('button'); + pill.type = 'button'; + pill.className = 'tail-pill'; + pill.addEventListener('click', () => { log.scrollTop = log.scrollHeight; }); + pillAnchor.appendChild(pill); + return pill; + } + function updatePill() { + if (unseen <= 0) { + if (pill) pill.classList.remove('visible'); + return; + } + ensurePill(); + pill.textContent = '↓ ' + unseen + ' new'; + pill.classList.add('visible'); + } + log.addEventListener('scroll', () => { + if (isNearBottom()) { unseen = 0; updatePill(); } + }); + function afterAppend() { + if (currentNoAnim || isNearBottom()) { + log.scrollTop = log.scrollHeight; + } else { + unseen += 1; + updatePill(); + } + } + function clearPlaceholder() { + if (placeholderEl && placeholderEl.parentElement === log) { + log.removeChild(placeholderEl); + } + placeholderEl = null; + } + function placeholder(text) { + clearPlaceholder(); + const e = document.createElement('div'); + e.className = 'row note'; + e.textContent = text; + log.appendChild(e); + placeholderEl = e; + } + function row(cls, text) { + clearPlaceholder(); + const e = document.createElement('div'); + e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); + e.appendChild(linkify(text)); + log.appendChild(e); + afterAppend(); + return e; + } + function details(cls, summary, body) { + clearPlaceholder(); + const d = document.createElement('details'); + d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); + const s = document.createElement('summary'); + s.textContent = summary; + d.appendChild(s); + const pre = document.createElement('pre'); + pre.className = 'tool-body'; + pre.appendChild(linkify(body)); + d.appendChild(pre); + log.appendChild(d); + afterAppend(); + return d; + } + function detailsDiff(cls, summary, body) { + clearPlaceholder(); + const d = document.createElement('details'); + d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); + const s = document.createElement('summary'); + s.textContent = summary; + d.appendChild(s); + const pre = document.createElement('pre'); + pre.className = 'tool-body diff-body'; + for (const line of String(body).split('\n')) { + const span = document.createElement('span'); + if (line.startsWith('+ ')) span.className = 'diff-add'; + else if (line.startsWith('- ')) span.className = 'diff-del'; + else span.className = 'diff-ctx'; + span.textContent = line + '\n'; + pre.appendChild(span); + } + d.appendChild(pre); + log.appendChild(d); + afterAppend(); + return d; + } + + function api(extra) { + return Object.assign({ + row, details, detailsDiff, placeholder, linkify, + fromHistory: false, + }, extra || {}); + } + function dispatch(ev, fromHistory) { + const r = renderers[ev.kind] || defaultRender; + try { + r(ev, api({ fromHistory })); + } catch (err) { + console.error('terminal renderer threw', ev, err); + row('note', '[render err] ' + (err && err.message ? err.message : err)); + } + if (opts.onAnyEvent) { + try { opts.onAnyEvent(ev, { fromHistory }); } + catch (err) { console.error('onAnyEvent threw', err); } + } + } + + // Subscribe → buffer → fetch history → dedupe → apply. + // + // Race the SSE subscription opens before the history fetch starts. + // Live events that land before history resolves are buffered, not + // rendered. Once the history response (`{ seq, events }`) arrives we: + // 1. Replay `events` (fromHistory=true). + // 2. Drop buffered events with `seq <= history.seq` — they're + // already reflected in the history rows above. + // 3. Apply remaining buffered events (fromHistory=false). + // 4. Switch to live mode: each new SSE event dispatches immediately. + // + // Without this dance an event that fires between history-fetch and + // SSE-subscribe goes missing; without seq dedupe the same event + // shows twice (once via history, once via live buffer). Both bugs + // were latent before. + // + // If `historyUrl` is unset we skip the dance: buffered events apply + // as live the moment the buffer flushes (no dedupe possible without + // a boundary seq). + function start() { + let live = false; + let buffered = []; + + const es = new EventSource(opts.streamUrl); + es.onmessage = (e) => { + let ev; + try { ev = JSON.parse(e.data); } + catch (err) { row('note', '[parse err] ' + e.data); return; } + if (!live) { buffered.push(ev); return; } + dispatch(ev, false); + if (opts.onLiveEvent) { + try { opts.onLiveEvent(ev); } + catch (err) { console.error('onLiveEvent threw', err); } + } + }; + es.onerror = () => { + if (es.readyState === EventSource.CONNECTING) row('note', '[reconnecting…]'); + else row('note', '[disconnected]'); + }; + es.onopen = () => { + // Fires on the initial connect and on every automatic + // reconnect. EventSource never replays events that fired + // during a disconnect window, so a consumer with + // snapshot-derived state (the dashboard's /api/state stores) + // must re-sync here or it shows stale state until a manual + // reload (issue #163). + if (opts.onStreamOpen) { + try { opts.onStreamOpen(); } + catch (err) { console.error('onStreamOpen threw', err); } + } + }; + + function flushBuffered(boundarySeq, historyKinds) { + const drained = buffered; + buffered = []; + live = true; + for (const ev of drained) { + // Seq-dedupe only events of a kind that actually appeared in + // the history replay — those are the only ones that could + // double (once via history, once via the live buffer). + // Mutation events (approval/question/container/…) are never + // carried by the history endpoint; deduping them against the + // broker-history seq would wrongly drop ones that fired + // between a consumer's own snapshot read and this history + // fetch (issue #163). ev.seq absent/0 → no dedupe possible. + if (boundarySeq != null + && typeof ev.seq === 'number' && ev.seq <= boundarySeq + && historyKinds && historyKinds.has(ev.kind)) { + continue; + } + dispatch(ev, false); + if (opts.onLiveEvent) { + try { opts.onLiveEvent(ev); } + catch (err) { console.error('onLiveEvent threw', err); } + } + } + } + + async function backfill() { + if (!opts.historyUrl) { + flushBuffered(null); + if (opts.onBackfillDone) opts.onBackfillDone(0); + return; + } + try { + const resp = await fetch(opts.historyUrl); + if (!resp.ok) { + flushBuffered(null); + if (opts.onBackfillDone) opts.onBackfillDone(0); + return; + } + const body = await resp.json(); + // Accept the envelope `{ seq, events }`. A bare array means + // the server hasn't been updated to include seq yet — treat + // it as "no dedupe possible." + const events = Array.isArray(body) ? body : (body.events || []); + const boundarySeq = Array.isArray(body) ? null : (body.seq ?? null); + // Kinds present in the history replay — the only kinds that + // can double and therefore the only ones to seq-dedupe. + const historyKinds = new Set(events.map((ev) => ev.kind)); + currentNoAnim = true; + for (const ev of events) dispatch(ev, true); + currentNoAnim = false; + if (events.length) row('note', '─── live (older above) ───'); + else placeholder('(connected — waiting for events)'); + flushBuffered(boundarySeq, historyKinds); + if (opts.onBackfillDone) opts.onBackfillDone(events.length); + } catch (err) { + console.warn('history backfill failed', err); + flushBuffered(null); + if (opts.onBackfillDone) opts.onBackfillDone(0); + } + } + return backfill(); + } + + const ready = start(); + return { row, details, detailsDiff, placeholder, ready }; + } + + // Build a DocumentFragment from `text`, turning bare http(s) URLs into + // clickable links that open in a new tab. Non-URL text stays as plain + // text nodes — no innerHTML, so this is XSS-safe. Trailing sentence + // punctuation is kept out of the link. (issue #233) + const LINKIFY_URL_RE = /https?:\/\/[^\s<>"']+/g; + function linkify(text) { + const str = text == null ? '' : String(text); + const frag = document.createDocumentFragment(); + if (str.indexOf('://') === -1) { // fast path: no URLs + if (str) frag.appendChild(document.createTextNode(str)); + return frag; + } + let last = 0; + let m; + LINKIFY_URL_RE.lastIndex = 0; + while ((m = LINKIFY_URL_RE.exec(str)) !== null) { + let url = m[0]; + // Don't swallow trailing punctuation that's really sentence text. + const trail = url.match(/[.,;:!?)\]}'"]+$/); + const tail = trail ? trail[0] : ''; + if (tail) url = url.slice(0, -tail.length); + if (m.index > last) { + frag.appendChild(document.createTextNode(str.slice(last, m.index))); + } + if (!url.slice(url.indexOf('://') + 3)) { + // Nothing past the scheme — not a real URL, emit verbatim. + frag.appendChild(document.createTextNode(m[0])); + } else { + const a = document.createElement('a'); + a.href = url; // regex only matches https?:// — safe + a.textContent = url; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + frag.appendChild(a); + if (tail) frag.appendChild(document.createTextNode(tail)); + } + last = m.index + m[0].length; + } + if (last < str.length) { + frag.appendChild(document.createTextNode(str.slice(last))); + } + return frag; + } + + window.HiveTerminal = { create, linkify }; +})(); diff --git a/hive-fr0nt/src/lib.rs b/hive-fr0nt/src/lib.rs new file mode 100644 index 00000000..e791dee1 --- /dev/null +++ b/hive-fr0nt/src/lib.rs @@ -0,0 +1,47 @@ +//! Shared frontend assets for the hive-c0re dashboard and the hive-ag3nt +//! per-container web UI. Both surfaces live in different binaries (and +//! different containers at runtime) but should feel like one product — +//! same colour tokens, same terminal-style live stream, same compose-box +//! ergonomics. Keeping the CSS + JS in one crate is the dumbest way to +//! make that true: both binaries `include_str!` from +//! `hive_fr0nt::assets::*` instead of growing their own copy. +//! +//! There is no Rust code beyond these `const` re-exports. The crate is a +//! container for text files and a place to write down the contract +//! between the two surfaces. +//! +//! Conventions for sharing: +//! - **CSS variables** live in [`BASE_CSS`] (colour palette, typography). +//! Page-specific stylesheets append to it; nothing else should declare +//! `--bg` / `--purple` / etc. +//! - **Terminal pane** (sticky-bottom log + `↓ N new` pill + fade-in +//! rows) lives in [`TERMINAL_CSS`] and [`TERMINAL_JS`]. Pages provide +//! a kind→renderer map; the JS owns the scroll + backfill + SSE plumbing. +//! - **Compose box** (textarea + slash-command palette + sticky +//! recipient + `@`-mention autocomplete) lives in [`COMPOSER_JS`]. +//! Pages pass a config flagging which features they want; the dashboard +//! ships `@`-mentions without slash commands, the agent page ships +//! slash commands without `@`-mentions. Both render through the same +//! component so the keystrokes, error flashes, and async-form +//! behaviour stay identical. +//! +//! Loading new shared assets: add the file under `assets/`, expose it as +//! a `pub const`, and `include_str!` it from whichever +//! `dashboard.rs` / `web_ui.rs` route needs it. + +pub const BASE_CSS: &str = include_str!("../assets/base.css"); +pub const TERMINAL_CSS: &str = include_str!("../assets/terminal.css"); +pub const TERMINAL_JS: &str = include_str!("../assets/terminal.js"); +/// Vendored [marked](https://github.com/markedjs/marked) v4.0.2 **UMD** +/// bundle (`lib/marked.umd.js`). The UMD wrapper assigns a browser global +/// `window.marked` with `.parse(src, opts)` / `.setOptions(...)`. Used by +/// the per-agent terminal and the dashboard file-preview flyout to render +/// markdown (paragraphs, lists, fenced + inline code, bold/italic, links). +/// Vendored rather than CDN-loaded so the pages work on operator machines +/// without internet egress (the container itself never fetches it). +/// +/// NB: must be the **UMD** build, not `marked.min.js` / `lib/marked.cjs` — +/// those are `CommonJS` (`exports.parse = …`, no wrapper) and throw +/// `exports is not defined` in a `