diff --git a/CLAUDE.md b/CLAUDE.md index 0a6ebc68..296d77da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,35 +87,28 @@ 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: static shell + /api/state JSON + actions + src/dashboard.rs axum HTTP: /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 - assets/ index.html, dashboard.css, app.js (include_str!) + unified DashboardEvent channel. Static assets + (HTML/CSS/JS/favicon) served by + tower_http::ServeDir from $HIVE_STATIC_DIR + (= `${frontend}/dashboard` per the c0re module). -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. +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-ag3nt/ in-container harness crate; produces TWO binaries src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT @@ -139,8 +132,10 @@ 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) - assets/ index.html, agent.css, app.js, stats.html, - stats.js, screen.html (include_str!) + 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). 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 3083bdb0..9e8e75be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,7 +565,6 @@ dependencies = [ "axum", "clap", "futures-util", - "hive-fr0nt", "hive-sh4re", "reqwest", "rmcp", @@ -575,6 +574,7 @@ dependencies = [ "serde_json", "tokio", "tokio-stream", + "tower-http", "tracing", "tracing-subscriber", ] @@ -587,7 +587,6 @@ dependencies = [ "axum", "base64", "clap", - "hive-fr0nt", "hive-sh4re", "libc", "reqwest", @@ -597,14 +596,11 @@ 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" @@ -645,6 +641,12 @@ 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" @@ -954,6 +956,16 @@ 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" @@ -1723,10 +1735,19 @@ 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", @@ -1835,6 +1856,12 @@ 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 a53b5b9e..c3880048 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["hive-ag3nt", "hive-c0re", "hive-fr0nt", "hive-sh4re"] +members = ["hive-ag3nt", "hive-c0re", "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 c5ebaa73..fce8d21a 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 -`hive-ag3nt/assets/app.js` (`renderStream`, `fmtToolUse`, +`frontend/packages/agent/src/app.js` (`renderStream`, `fmtToolUse`, `renderRichToolUse`, `renderToolResult`, `renderTaskEvent`, `mdNode`, `detailsOpenMd`, `fmtArgsGeneric`) + -`hive-fr0nt/assets/terminal.css` (the shared `.live .` -styling) + `hive-fr0nt/assets/marked.umd.js` (markdown). +`frontend/packages/shared/src/terminal.css` (the shared +`.live .` styling) + the `marked` npm package (markdown). ## Layout contract @@ -82,8 +82,8 @@ parent's negative pull. ## Markdown -`mdNode(text)` wraps `window.marked.parse(text)` (vendored -v4.0.2 UMD via `hive-fr0nt::MARKED_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 61b4fa31..a0a555df 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -7,22 +7,32 @@ and the per-agent UIs (manager on :8000, sub-agents on a hashed ## Shape (shared by both) -- `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 +- `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 `#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 `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). + 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). - `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 db3efb4d..73d24bdd 100644 --- a/flake.nix +++ b/flake.nix @@ -51,18 +51,31 @@ in { packages = forAllSystems ( - { naersk-lib, ... }: + { pkgs, 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: @@ -96,6 +109,7 @@ # 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 new file mode 100644 index 00000000..31994d81 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +packages/*/dist/ diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..5564ab0a --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,33 @@ +# 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 new file mode 100644 index 00000000..a2ffd71c --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,549 @@ +{ + "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 new file mode 100644 index 00000000..282a6480 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,18 @@ +{ + "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 new file mode 100644 index 00000000..6b231f38 --- /dev/null +++ b/frontend/packages/agent/build.mjs @@ -0,0 +1,60 @@ +// 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 new file mode 100644 index 00000000..0d0c1759 --- /dev/null +++ b/frontend/packages/agent/package.json @@ -0,0 +1,15 @@ +{ + "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/hive-ag3nt/assets/agent.css b/frontend/packages/agent/src/agent.css similarity index 98% rename from hive-ag3nt/assets/agent.css rename to frontend/packages/agent/src/agent.css index 8b66609b..68103202 100644 --- a/hive-ag3nt/assets/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -1,5 +1,8 @@ -/* Palette + base body typography live in hive-fr0nt::BASE_CSS, prepended - to this stylesheet by `serve_css` at runtime. */ +/* 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"; + body { max-width: 110em; margin: 1.5em auto; diff --git a/hive-ag3nt/assets/app.js b/frontend/packages/agent/src/app.js similarity index 98% rename from hive-ag3nt/assets/app.js rename to frontend/packages/agent/src/app.js index eb478932..c2a56c55 100644 --- a/hive-ag3nt/assets/app.js +++ b/frontend/packages/agent/src/app.js @@ -2,6 +2,18 @@ // 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/hive-ag3nt/assets/index.html b/frontend/packages/agent/src/index.html similarity index 91% rename from hive-ag3nt/assets/index.html rename to frontend/packages/agent/src/index.html index 8721ef9c..dcc0d2b9 100644 --- a/hive-ag3nt/assets/index.html +++ b/frontend/packages/agent/src/index.html @@ -45,8 +45,8 @@
- - - + + diff --git a/hive-ag3nt/assets/screen.html b/frontend/packages/agent/src/screen.html similarity index 100% rename from hive-ag3nt/assets/screen.html rename to frontend/packages/agent/src/screen.html diff --git a/hive-ag3nt/assets/stats.html b/frontend/packages/agent/src/stats.html similarity index 91% rename from hive-ag3nt/assets/stats.html rename to frontend/packages/agent/src/stats.html index 446491fc..0795310d 100644 --- a/hive-ag3nt/assets/stats.html +++ b/frontend/packages/agent/src/stats.html @@ -90,11 +90,9 @@

result mix

- - - + + diff --git a/hive-ag3nt/assets/stats.js b/frontend/packages/agent/src/stats.js similarity index 97% rename from hive-ag3nt/assets/stats.js rename to frontend/packages/agent/src/stats.js index 1b524d24..546ab190 100644 --- a/hive-ag3nt/assets/stats.js +++ b/frontend/packages/agent/src/stats.js @@ -2,6 +2,14 @@ // 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/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs new file mode 100644 index 00000000..07097fa6 --- /dev/null +++ b/frontend/packages/dashboard/build.mjs @@ -0,0 +1,53 @@ +// 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 new file mode 100644 index 00000000..de91e6d7 --- /dev/null +++ b/frontend/packages/dashboard/package.json @@ -0,0 +1,14 @@ +{ + "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/hive-c0re/assets/app.js b/frontend/packages/dashboard/src/app.js similarity index 99% rename from hive-c0re/assets/app.js rename to frontend/packages/dashboard/src/app.js index 53012bde..d7b912b2 100644 --- a/hive-c0re/assets/app.js +++ b/frontend/packages/dashboard/src/app.js @@ -2,6 +2,18 @@ // 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/hive-c0re/assets/dashboard.css b/frontend/packages/dashboard/src/dashboard.css similarity index 99% rename from hive-c0re/assets/dashboard.css rename to frontend/packages/dashboard/src/dashboard.css index 568729db..39d6cbeb 100644 --- a/hive-c0re/assets/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -1,5 +1,8 @@ -/* Palette + base body typography live in hive-fr0nt::BASE_CSS, prepended - to this stylesheet by `serve_css` at runtime. */ +/* 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"; + body { max-width: 70em; margin: 1.5em auto; diff --git a/hive-c0re/assets/index.html b/frontend/packages/dashboard/src/index.html similarity index 96% rename from hive-c0re/assets/index.html rename to frontend/packages/dashboard/src/index.html index 846c86e9..be998339 100644 --- a/hive-c0re/assets/index.html +++ b/frontend/packages/dashboard/src/index.html @@ -109,8 +109,9 @@ - - - + + diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json new file mode 100644 index 00000000..795ed42c --- /dev/null +++ b/frontend/packages/shared/package.json @@ -0,0 +1,17 @@ +{ + "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/hive-fr0nt/assets/base.css b/frontend/packages/shared/src/base.css similarity index 100% rename from hive-fr0nt/assets/base.css rename to frontend/packages/shared/src/base.css diff --git a/frontend/packages/shared/src/index.js b/frontend/packages/shared/src/index.js new file mode 100644 index 00000000..49f5c1dc --- /dev/null +++ b/frontend/packages/shared/src/index.js @@ -0,0 +1,3 @@ +// 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/hive-fr0nt/assets/terminal.css b/frontend/packages/shared/src/terminal.css similarity index 100% rename from hive-fr0nt/assets/terminal.css rename to frontend/packages/shared/src/terminal.css diff --git a/frontend/packages/shared/src/terminal.js b/frontend/packages/shared/src/terminal.js new file mode 100644 index 00000000..0726a8cc --- /dev/null +++ b/frontend/packages/shared/src/terminal.js @@ -0,0 +1,342 @@ +// 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 e2b4a2b6..3f0fba3e 100644 --- a/hive-ag3nt/Cargo.toml +++ b/hive-ag3nt/Cargo.toml @@ -12,7 +12,6 @@ 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 @@ -21,6 +20,7 @@ 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/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 3868c826..14e02e39 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -24,6 +24,7 @@ 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; @@ -88,6 +89,19 @@ 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, @@ -99,11 +113,6 @@ 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)) @@ -116,12 +125,17 @@ 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?; @@ -201,68 +215,6 @@ 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. @@ -585,8 +537,13 @@ 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".to_owned(), + url: "/stats.html".to_owned(), icon: "📊".to_owned(), label: "stats".to_owned(), kind: AgentLinkKind::Container, @@ -594,7 +551,7 @@ fn agent_links(label: &str, gui_enabled: bool) -> Vec { if gui_enabled { links.push(AgentLink { - url: "/screen".to_owned(), + url: "/screen.html".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 169a50ea..d57a7772 100644 --- a/hive-c0re/Cargo.toml +++ b/hive-c0re/Cargo.toml @@ -12,7 +12,6 @@ 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 @@ -20,6 +19,7 @@ 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/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 725ac1b3..9871fc54 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; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Context, Result}; @@ -14,7 +14,7 @@ use axum::{ extract::{Path as AxumPath, State}, http::{HeaderMap, StatusCode}, response::{ - Html, IntoResponse, Response, + IntoResponse, Response, sse::{Event, KeepAlive, Sse}, }, routing::{get, post}, @@ -23,6 +23,7 @@ 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}; @@ -37,11 +38,20 @@ 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)) @@ -66,8 +76,11 @@ 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)) - .route("/static/hive-fr0nt.js", get(serve_shared_js)) - .route("/static/marked.js", get(serve_marked_js)) + // 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)) .with_state(AppState { coord }); let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = bind_with_retry(addr).await?; @@ -77,11 +90,14 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { } // --------------------------------------------------------------------------- -// 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. +// 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. // --------------------------------------------------------------------------- /// `SO_REUSEADDR` bind with retry. Mirrors the per-agent variant in @@ -142,56 +158,6 @@ 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 deleted file mode 100644 index 1bc5f00f..00000000 --- a/hive-fr0nt/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "hive-fr0nt" -edition.workspace = true -version.workspace = true - -[lints] -workspace = true diff --git a/hive-fr0nt/assets/marked.umd.js b/hive-fr0nt/assets/marked.umd.js deleted file mode 100644 index 2f2ca9ca..00000000 --- a/hive-fr0nt/assets/marked.umd.js +++ /dev/null @@ -1,2913 +0,0 @@ -/** - * 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/hive-fr0nt/assets/terminal.js b/hive-fr0nt/assets/terminal.js deleted file mode 100644 index fd30ea21..00000000 --- a/hive-fr0nt/assets/terminal.js +++ /dev/null @@ -1,344 +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: -// -// 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 deleted file mode 100644 index e791dee1..00000000 --- a/hive-fr0nt/src/lib.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! 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 `