diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..8b8456521 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,76 @@ +name: Build & push docker image + +# 推 fork/main 时自动 build + push docker.io/nickwilde18/pkgsitex 单 image +# 含 worker / frontend / pkgsitex-init 三 entrypoint。 +# +# Tag 策略: +# - fork-main:fork/main 分支 HEAD 永远指向最新(compose.prod.yaml 默认拉这个) +# - :每次 push 一份 immutable,方便 rollback +# +# 触发条件:fork/main 推送、PR 推送(PR 只 build 不 push 验证 Dockerfile)。 +# +# 用户操作: +# 1) https://hub.docker.com/settings/security 生成 access token +# 2) GitHub repo Settings → Secrets and variables → Actions 加两个 secret: +# DOCKERHUB_USERNAME = nickwilde18 +# DOCKERHUB_TOKEN = +# 3) push fork/main 触发本 workflow + +on: + push: + branches: + - fork/main + pull_request: + branches: + - master + - fork/main + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + # PR 触发时跳过 login(forks PR 拿不到 secret,build 跑通验证 Dockerfile 即可) + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: nickwilde18/pkgsitex + # tags:fork-main 浮动指向最新 / 不可变 + tags: | + type=raw,value=fork-main,enable=${{ github.ref == 'refs/heads/fork/main' }} + type=sha,format=short,prefix= + + - name: Set up QEMU + # 跨 arch build 需要 binfmt——QEMU emulation 让 amd64 runner 能交叉 + # 编出 arm64 二进制(buildx + Go 交叉编译速度差距很小) + uses: docker/setup-qemu-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: deploy/prod/Dockerfile + # PR 时只 build 不 push(验证 Dockerfile 不坏) + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # 多 arch:macOS arm64 / 国产 CPU / 树莓派都能直接 docker pull + # native,省 Rosetta emulation;amd64 K8s prod 节点也照样 native + platforms: linux/amd64,linux/arm64 + # 用 GitHub Actions cache 加速重 build + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 17e5a31db..0b74a38fd 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,7 @@ __diff_output__ #### Cloud Build Artifacts #### _BUILD_TAG _ID_TOKEN + +#### pkgsitex prod stack 配置 #### +# .env 含 GITHUB_TOKEN 明文,绝对不能提交 +deploy/prod/.env diff --git a/README.md b/README.md index cfbd5618e..70dd52be4 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,168 @@ -# golang.org/x/pkgsite +# pkgsitex -This repository hosts the source code of the [pkg.go.dev](https://pkg.go.dev) website, -and [`pkgsite`](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite), a documentation -server program. +> English | [简体中文](README.zh-CN.md) -[![Go Reference](https://pkg.go.dev/badge/golang.org/x/pkgsite.svg)](https://pkg.go.dev/golang.org/x/pkgsite) +A fork of [`golang/pkgsite`](https://github.com/golang/pkgsite) tuned for self-hosting Go module docs in private/internal environments. Adds URL subpath mounting, godoc comment markdown (mermaid / inline code / bold), unexported symbol display + browser toggle, view source via locally mounted files, and config-driven private GitHub repo support. -## pkg.go.dev: a site for discovering Go packages +> **Fork strategy**: master always equals upstream master; all patches live on the long-lived [`fork/main`](https://github.com/NickWilde18/pkgsitex/tree/fork/main) branch. GitHub "Sync fork" stays fast-forward, zero conflict. All usage / setup / patch docs live on `fork/main` — including this README. -Pkg.go.dev is a website for discovering and evaluating Go packages and modules. +```sh +git clone https://github.com/NickWilde18/pkgsitex.git ~/Repo/pkgsitex +cd ~/Repo/pkgsitex +git checkout fork/main # switch to the fork patches branch +cat README.md # ← what you're reading +``` -You can check it out at [https://pkg.go.dev](https://pkg.go.dev). +## Quick start (local, sub-second) -## pkgsite: a documentation server +Requires Go 1.25+. Clone the repos you want to browse to `~/Repo/`. -`pkgsite` program extracts and generates documentation for Go projects. +```sh +go run ./cmd/pkgsite -base-path=/pkgsitex -show-unexported -http=:8089 \ + ~/Repo/Chat \ + ~/Repo/Doubao-Speech-Service \ + ~/Repo/UniAuth/uniauth-gf \ + ~/Repo/UniAuth/ittools_sync \ + ~/Repo/open-platform -Example usage: +open http://localhost:8089/pkgsitex/ +``` + +Or compile once and reuse locally: +```sh +go install ./cmd/pkgsite +pkgsite -base-path=/pkgsitex -show-unexported -http=:8089 ~/Repo/Chat ... ``` -$ go install golang.org/x/pkgsite/cmd/pkgsite@latest -$ cd myproject -$ pkgsite -open . + +The first `go run` fetches `esbuild` / `safehtml` deps via GOPROXY (~30s); subsequent runs are instant. + +## Configuration + +| flag | purpose | default | +|---|---|---| +| `-base-path=/pkgsitex` | mount the site under a URL subpath (reverse-proxy / shared-domain scenarios). Empty = mount at root (pkg.go.dev behavior) | `""` | +| `-show-unexported` | godoc renders unexported declarations; the browser uses a toggle to control visibility | `false` | +| `-http=:8089` | listen address | `localhost:8080` | +| `-cache` | use GOMODCACHE | `false` | +| `-proxy` | use GOPROXY to fetch remote modules | `false` | +| `...` | Go module paths to index (multiple) | `.` | + +Full flags: `go run ./cmd/pkgsite -h`. + +## Adding / removing repos + +Edit the path list at the end of the `go run` / `pkgsite` command. + +Convention: clone internal repos to `~/Repo/`. UniAuth is a monorepo with two Go modules (`uniauth-gf/` + `ittools_sync/`) — list each separately; pkgsite doesn't auto-discover nested modules. + +## Browse experience + +| Element | Behavior | +|---|---| +| URL `/pkgsitex/` | module overview | +| URL `/pkgsitex//` | subpackage | +| URL `/pkgsitex/@` | specific git tag (local mode reads from module cache; the prod worker mode has full tag history) | +| **Show unexported** button | next to the Index header — toggles visibility of private declarations / sidebar / index links; state persists in localStorage across pages | +| **Show internal directories** button | upstream feature — toggles `internal/` subdirectory display. Auto-enabled when `data-local=true` | +| ` `code` ` / `**bold**` / ` ```mermaid ` | godoc comments render these markdown forms (the fork's dochtml ext) | +| **View Source** | a link next to each declaration on the package detail page — jumps to the locally mounted source file (no GitHub access required) | + +## Patches (what the fork changes) + +- **`-base-path`**: URL subpath prefix — all mux patterns / template helpers / godoc cross-references / view source links are auto-prefixed +- **`-show-unexported`**: makes `internal/fetch/load.go` keep unexported `FuncDecl`s during AST processing, and `doc.NewFromFiles` uses `doc.AllDecls` +- **godoc markdown ext** (`internal/godoc/dochtml/internal/render/markdown_ext.go`): post-processes HTML to recognize inline code / bold / mermaid fences +- **mermaid client lazy-load** (`static/frontend/frontend.tmpl`): dynamically imports `mermaid@10` only when a page contains `code.language-mermaid` +- **unexported toggle** (`static/frontend/unit/main/main.ts`): client-side hide + button + localStorage +- **view source local file mux** + base path prefix fix +- **multi-repo command line**: list multiple module paths in one command +- **trailing-slash redirect-loop fix**: `internal/frontend/details.go` distinguishes "mounted at root" from "the base path itself" in base-path mode +- **prod 4-piece stack**: postgres / athens / worker / frontend `compose.prod.yaml` plus a `cmd/pkgsitex-init` bootstrap container that writes athens netrc + enqueues modules to the worker + +## Prod deployment + +Prod mode differs from dev — it runs the same 4-component architecture as pkg.go.dev itself: + +- **postgres**: caches module index / package docs +- **athens**: GOPROXY cache + private repo fetch via GitHub PAT +- **worker**: async fetches modules into the DB +- **frontend**: renders from the DB (port 8089) + +The image is published to docker.io: [`nickwilde18/pkgsitex:fork-main`](https://hub.docker.com/r/nickwilde18/pkgsitex) (multi-arch `linux/amd64` + `linux/arm64`, auto-pushed by GitHub Actions). Users don't need to build — pull the image and run. + +```sh +git clone https://github.com/NickWilde18/pkgsitex.git +cd pkgsitex +git checkout fork/main + +# 1) configure PAT +cp deploy/prod/.env.example deploy/prod/.env +$EDITOR deploy/prod/.env # GITHUB_TOKEN=ghp_xxx + +# 2) edit the module list (5 sample CUHKSZ internal repos pre-listed) +$EDITOR deploy/prod/config.yaml + +# 3) start the stack (image is auto-pulled) +docker compose -f compose.prod.yaml --env-file deploy/prod/.env up -d + +# 4) browse +open http://localhost:8089/pkgsitex/ ``` -For more information, see the [pkgsite documentation](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite). +Full ops (add/remove modules, periodic cron refresh, troubleshooting): [`deploy/prod/README.md`](deploy/prod/README.md). -## API +Supports multi-version (git tag switching), license-permissive rendering (proprietary too), and config-driven module list. -`pkgsite` provides a REST API for retrieving package and module information. -The API endpoints include: +## Maintenance strategy -- `/v1/package/{path}`: Information about the package at `{path}`. -- `/v1/module/{path}`: Information about the module at `{path}`. -- `/v1/versions/{path}`: Versions of the module at `{path}`. -- `/v1/packages/{path}`: Information about packages of the module at `{path}`. -- `/v1/search`: Search results. -- `/v1/symbols/{path}`: List of symbols for the package at `{path}`. -- `/v1/imported-by/{path}`: Paths of packages importing the package at `{path}`. -- `/v1/vulns/{path}`: Vulnerabilities of the module at `{path}`. +- **master always equals upstream master** — never merge `fork/main` into master. GitHub "Sync fork" stays fast-forward, zero conflict +- **`fork/main` is a long-lived patch branch** — all fork changes accumulate here +- **Monthly rebase**: AI runs `git rebase upstream/master` so `fork/main` follows new upstream development. Predicted conflict hot spots: + - `internal/frontend/server.go` mux list (when upstream adds new routes) + - `static/**/*.tmpl` / `*.ts` (when upstream changes styles / interactions) + - other fork-only files (e.g. `markdown_ext.go`, `base-path/base-path.ts`) almost never conflict (fork-exclusive) -For detailed documentation of parameters and response schemas, see the documentation page served at `/api`, or refer directly to the annotated source code in `internal/api/api.go`. +Patch history: PR [#2](https://github.com/NickWilde18/pkgsitex/pull/2). -## Issues +## Troubleshooting -If you want to report a bug or have a feature suggestion, please first check -the [known issues](https://github.com/golang/go/labels/pkgsite) to see if your -issue is already being discussed. If an issue does not already exist, feel free -to [file an issue](https://golang.org/s/pkgsite-feedback). +| Symptom | Diagnosis | +|---|---| +| `go run` fails to fetch deps | use a closer GOPROXY: `GOPROXY=https://goproxy.cn,direct go run ...` | +| `/pkgsitex/...` static asset 404 | you edited `static/`/templates without rebuilding: run `go run ./devtools/cmd/static` to regenerate the bundle | +| module resolution timeout | point GOPROXY at the company Athens: `export GOPROXY=https://athens.corp.com,direct` | +| mermaid doesn't render | check the browser console for `mermaid load failed` — your network blocks jsdelivr CDN; vendor mermaid locally (`third_party/mermaid/` pending) | +| Sidebar / Index has no "Show unexported" button | localStorage may have an old toggle key (`gogodocs:showUnexported`); click the button once to reset to the new key | -For answers to frequently asked questions, see [pkg.go.dev/about](https://pkg.go.dev/about). +--- -You can also chat with us on the -[#pkgsite Slack channel](https://gophers.slack.com/archives/C0166L4QGJV) on the -[Gophers Slack](https://invite.slack.golangbridge.org). +## Upstream pkgsite -## Contributing +A downstream fork has to follow upstream development — keeping the upstream README content below for cross-checking features / locating differences during upgrades. + +# golang.org/x/pkgsite + +This repository hosts the source code of the [pkg.go.dev](https://pkg.go.dev) website, +and [`pkgsite`](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite), a documentation +server program. -We would love your help! +[![Go Reference](https://pkg.go.dev/badge/golang.org/x/pkgsite.svg)](https://pkg.go.dev/golang.org/x/pkgsite) -Our canonical Git repository is located at -[go.googlesource.com/pkgsite](https://go.googlesource.com/pkgsite). -There is a mirror of the repository at -[github.com/golang/pkgsite](https://github.com/golang/pkgsite). +Full upstream README: [golang/pkgsite README](https://github.com/golang/pkgsite#readme). -To contribute, please read our [contributing guide](CONTRIBUTING.md). +## Issues -## License +Fork issues (base-path / markdown ext / etc): [NickWilde18/pkgsitex/issues](https://github.com/NickWilde18/pkgsitex/issues). -Unless otherwise noted, the Go source files are distributed under the BSD-style -license found in the [LICENSE](LICENSE) file. +Upstream pkgsite issues go to [`golang/go`](https://golang.org/issues), prefixed `x/pkgsite:` — see the upstream README. -## Links +## Contributing + +Fork-internal PRs target the `fork/main` branch (not master). + +Upstream pkgsite contribution flow (Gerrit code review): [Contribution Guide](https://golang.org/doc/contribute.html) + [upstream README contributing section](https://github.com/golang/pkgsite#contributing). + +## License -- [Homepage](https://pkg.go.dev) -- [Feedback](https://golang.org/s/pkgsite-feedback) -- [Issue Tracker](https://golang.org/s/pkgsite-issues) +Unless otherwise noted, the source files are distributed under the BSD-style license found in the LICENSE file. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 000000000..7c289c120 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,167 @@ +# pkgsitex + +> [English](README.md) | 简体中文 + +[`golang/pkgsite`](https://github.com/golang/pkgsite) 的 fork,加了内网部署 / 私有仓库浏览常用能力——`-base-path` 子路径、未导出符号显示 + 浏览器 toggle、godoc 注释 markdown 扩展(` `code` ` / `**bold**` / mermaid 围栏)、view source 走本地 file mux 等。 + +> **Fork 维护策略**:master 永远 = upstream master,patches 落在 long-lived branch [`fork/main`](https://github.com/NickWilde18/pkgsitex/tree/fork/main)。GitHub "Sync fork" 永远 fast-forward 0 conflict。所有用法 / 启动 / patch 详见 fork/main 分支的 README。 + +```sh +git clone https://github.com/NickWilde18/pkgsitex.git ~/Repo/pkgsitex +cd ~/Repo/pkgsitex +git checkout fork/main # 切到 fork patches 分支 +cat README.md # ← 你看到的 fork 说明 +``` + +## 快速启动(本地,秒级) + +前置:Go 1.25+,本地把要浏览的 repo clone 到 `~/Repo/`。 + +```sh +go run ./cmd/pkgsite -base-path=/pkgsitex -show-unexported -http=:8089 \ + ~/Repo/Chat \ + ~/Repo/Doubao-Speech-Service \ + ~/Repo/UniAuth/uniauth-gf \ + ~/Repo/UniAuth/ittools_sync \ + ~/Repo/open-platform + +open http://localhost:8089/pkgsitex/ +``` + +或者编译一次本地复用: + +```sh +go install ./cmd/pkgsite +pkgsite -base-path=/pkgsitex -show-unexported -http=:8089 ~/Repo/Chat ... +``` + +首次 `go run` 拉 esbuild / safehtml 等 dep 走 GOPROXY,约 30 秒,后续秒启。 + +## 配置选项 + +| flag | 用途 | 默认 | +|---|---|---| +| `-base-path=/pkgsitex` | 站点挂子路径下,反代 / 共享域名场景。空 = 挂根(pkg.go.dev 行为) | `""` | +| `-show-unexported` | godoc 渲染保留未导出符号;浏览器用 toggle 控制显隐 | `false` | +| `-http=:8089` | 监听端口 | `localhost:8080` | +| `-cache` | 走 GOMODCACHE | `false` | +| `-proxy` | 走 GOPROXY 拉远程 module | `false` | +| `...` | 要索引的 Go module 路径(多个) | `.` | + +完整 flag:`go run ./cmd/pkgsite -h`。 + +## 增 / 减仓库 + +直接改 `go run` / `pkgsite` 命令末尾的 path 列表。 + +约定:内部仓库都 clone 在 `~/Repo/`。UniAuth 是 monorepo 含 `uniauth-gf/` + `ittools_sync/` 两个 Go module,各列一份——pkgsite 不支持 nested module 自动发现。 + +## 浏览体验 + +| 元素 | 行为 | +|---|---| +| URL `/pkgsitex/` | module overview | +| URL `/pkgsitex//` | 子包 | +| URL `/pkgsitex/@` | 指定 git tag(local mode 走 module cache,prod worker 模式才有完整 tag 历史) | +| **Show unexported** button | Index 标题旁,切私有 declaration / 侧边栏 / index 链接的显隐,状态 localStorage 跨页保留 | +| **Show internal directories** button | 上游内置,切 `internal/` 子目录显示。`data-local=true` 时自动开启 | +| ` `code` ` / `**bold**` / ` ```mermaid ` | godoc 注释里这些 markdown 写法都渲染(fork 加的 dochtml ext) | +| **View Source** | 包详情页每个声明右侧链接,跳本地 mount 的源码文件(不依赖 GitHub 在线访问) | + +## Patch 集合(fork 改了什么) + +- **`-base-path`**:URL 子路径前缀,所有 mux pattern / template helper / godoc cross-reference / view source 自动 prefix +- **`-show-unexported`**:让 `internal/fetch/load.go` 在 AST 阶段保留 unexported FuncDecl + `doc.NewFromFiles` 用 `doc.AllDecls` +- **godoc markdown ext**(`internal/godoc/dochtml/internal/render/markdown_ext.go`):post-process HTML 加 inline code / bold / mermaid fence 识别 +- **mermaid client lazy-load**(`static/frontend/frontend.tmpl`):页面有 `code.language-mermaid` 才动态 import mermaid@10 +- **unexported toggle**(`static/frontend/unit/main/main.ts`):client-side hide + button + localStorage +- **view source 本地 file mux** + base path 拼接修 +- **multi-repo command line**:直接 list 多个 module path +- **trailing slash 死循环修**:`internal/frontend/details.go` 在 base path 模式下区分"挂根"和"base path 自身" + +## Prod 部署 + +Prod 模式跟 dev 不同——跟 pkg.go.dev 自身架构一致 4 件套: + +- **postgres**:缓存 module 索引 / 包文档 +- **athens**:GOPROXY 缓存 + 用 GitHub PAT 拉私有 repo +- **worker**:异步 fetch module 入 DB +- **frontend**:从 DB 读渲染(端口 8089) + +镜像已发布到 docker.io [`nickwilde18/pkgsitex:fork-main`](https://hub.docker.com/r/nickwilde18/pkgsitex)(GitHub Actions 自动 push)。用户不需要 build——拉 image 即用。 + +```sh +git clone https://github.com/NickWilde18/pkgsitex.git +cd pkgsitex +git checkout fork/main + +# 1) 配 PAT +cp deploy/prod/.env.example deploy/prod/.env +$EDITOR deploy/prod/.env # GITHUB_TOKEN=ghp_xxx + +# 2) 编辑要监控的 module 列表(内置 5 个 CUHKSZ 内部 repo 示例) +$EDITOR deploy/prod/config.yaml + +# 3) 起 stack(自动拉镜像) +docker compose -f compose.prod.yaml --env-file deploy/prod/.env up -d + +# 4) 浏览 +open http://localhost:8089/pkgsitex/ +``` + +完整运维(加 / 删 module、周期 cron 刷新、故障排查):[`deploy/prod/README.md`](deploy/prod/README.md)。 + +支持 multi-version(git tag 切换)、license permissive(私有 / proprietary 也渲)、配置文件驱动。 + +## 维护策略 + +- **master 永远 = upstream master**——不 merge `fork/main` 进 master。GitHub "Sync fork" 永远 fast-forward 0 conflict +- **`fork/main` 是 long-lived patch branch**——所有 fork 改动累积在此分支 +- **月度 rebase**:AI 跑 `git rebase upstream/master` 让 `fork/main` 跟随上游新进展。预测 conflict 集中在: + - `internal/frontend/server.go` 的 mux 列表(上游新加路由时) + - `static/**/*.tmpl` / `*.ts`(上游改样式 / 加交互时) + - 其他 fork 文件(如 `markdown_ext.go`、`base-path/base-path.ts`)几乎不会 conflict(fork 独有) + +历次 patch 详见 PR [#2](https://github.com/NickWilde18/pkgsitex/pull/2)。 + +## 故障排查 + +| 现象 | 排查 | +|---|---| +| `go run` 拉 dep 失败 | 走更近的 GOPROXY:`GOPROXY=https://goproxy.cn,direct go run ...` | +| 浏览 `/pkgsitex/...` 静态资源 404 | 改了 `static/`/模板后没重 build:`go run ./devtools/cmd/static` 重生 bundle | +| 解析 module 超时 | 把 GOPROXY 指向公司 Athens:`export GOPROXY=https://athens.corp.com,direct` | +| mermaid 不渲染 | 浏览器 console 看 `mermaid load failed`——内网拦了 jsdelivr CDN,需要本地 host mermaid(`third_party/mermaid/` 待 vendor) | +| Sidebar / Index 没"Show unexported"按钮 | 可能 localStorage 之前 toggle key 是老名字(`gogodocs:showUnexported`),点一次按钮即可重置成新 key | + +--- + +## 上游 pkgsite + +下游 fork 需要跟随上游进展,保留上游 README 内容如下,方便核对功能 / 升级时定位差异。 + +# golang.org/x/pkgsite + +This repository hosts the source code of the [pkg.go.dev](https://pkg.go.dev) website, +and [`pkgsite`](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite), a documentation +server program. + +[![Go Reference](https://pkg.go.dev/badge/golang.org/x/pkgsite.svg)](https://pkg.go.dev/golang.org/x/pkgsite) + +完整上游 README:[golang/pkgsite README](https://github.com/golang/pkgsite#readme)。 + +## Issues + +Fork 自身 issues(base-path / markdown ext / etc):[NickWilde18/pkgsitex/issues](https://github.com/NickWilde18/pkgsitex/issues)。 + +上游 pkgsite issues 报到 [`golang/go`](https://golang.org/issues),前缀 `x/pkgsite:`,详见上游 README。 + +## Contributing + +Fork 内部 PR 投到 `fork/main` 分支(不进 master)。 + +上游 pkgsite contribution flow(gerrit code review)见 [Contribution Guide](https://golang.org/doc/contribute.html) + [上游 README contributing 段](https://github.com/golang/pkgsite#contributing)。 + +## License + +Unless otherwise noted, the source files are distributed under the BSD-style license found in the LICENSE file. diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index 1684981f8..88a3fc9dd 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -8,8 +8,10 @@ package main import ( "context" "flag" + "fmt" "net/http" "os" + "strings" "time" "cloud.google.com/go/profiler" @@ -26,6 +28,8 @@ import ( "golang.org/x/pkgsite/internal/fetchdatasource" "golang.org/x/pkgsite/internal/frontend" "golang.org/x/pkgsite/internal/frontend/fetchserver" + "golang.org/x/pkgsite/internal/frontend/versions" + "golang.org/x/pkgsite/internal/godoc" "golang.org/x/pkgsite/internal/log" "golang.org/x/pkgsite/internal/middleware" "golang.org/x/pkgsite/internal/middleware/timeout" @@ -56,11 +60,45 @@ var ( bypassLicenseCheck = flag.Bool("bypass_license_check", false, "display all information, even for non-redistributable paths") hostAddr = flag.String("host", "localhost:8080", "Host address for the server") queueType = flag.String("queue", "inmemory", `queue implementation when not on GCP: "inmemory" or "postgres"`) + // fork: 把 frontend 整体挂在 URL 子路径下(如 -base-path=/pkgsitex), + // 跟 cmd/pkgsite local mode 同款语义。空 = 挂根(上游默认行为)。 + basePath = flag.String("base-path", "", "URL prefix to mount the site under (e.g. /pkgsitex). Must start with / and not end with /.") + // fork: godoc 渲染保留未导出符号(doc.AllDecls 模式),跟 cmd/pkgsite 同款。 + // 内网部署常见诉求——自家代码完整展示比 public-only 视图更有用。 + showUnexported = flag.Bool("show-unexported", false, "Render documentation including unexported declarations (doc.AllDecls mode).") ) +// validateBasePath 强制 base-path 形如 "/foo" 或空——空表示挂根。 +// 不允许尾部斜杠(mux pattern 拼起来会双斜杠引发匹配失败)。复制自 +// cmd/pkgsite/main.go——两个 cmd 各自校验避免引入 cmd/internal 共享层。 +func validateBasePath(p string) error { + if p == "" { + return nil + } + if !strings.HasPrefix(p, "/") { + return fmt.Errorf("-base-path %q must start with /", p) + } + if strings.HasSuffix(p, "/") { + return fmt.Errorf("-base-path %q must not end with /", p) + } + return nil +} + func main() { flag.Parse() ctx := context.Background() + + if err := validateBasePath(*basePath); err != nil { + log.Fatalf(ctx, "%v", err) + } + // 全局开关——godoc.DocPackage / dochtml 模板读包级 var 决定 unexported + // 显隐 + URL 子路径前缀。同 cmd/pkgsite 一种行为,没必要加 ServerConfig 字段层层透传。 + godoc.IncludeUnexported = *showUnexported + godoc.BasePath = *basePath + // versions 包的 ConstructUnitURL 是所有 unit / package / module 详情页链接 + // 的核心 URL builder——空 BasePath 时 no-op 跟上游一致。 + versions.BasePath = *basePath + cfg, err := serverconfig.Init(ctx) if err != nil { log.Fatal(ctx, err) @@ -201,6 +239,7 @@ func main() { VulndbClient: vc, HTTPClient: &http.Client{Transport: new(ochttp.Transport)}, RecordCodeWikiMetrics: dcensus.RecordClick, + BasePath: *basePath, }) if err != nil { log.Fatalf(ctx, "frontend.NewServer: %v", err) diff --git a/cmd/internal/pkgsite/server.go b/cmd/internal/pkgsite/server.go index d1fb19d79..191e56bf7 100644 --- a/cmd/internal/pkgsite/server.go +++ b/cmd/internal/pkgsite/server.go @@ -44,6 +44,10 @@ type ServerConfig struct { GoDocMode bool RecordCodeWikiMetrics frontend.RecordClickFunc + // BasePath:URL 前缀(如 "/pkgsitex")让站点挂在子路径下。空 = 挂根。 + // 详见 cmd/pkgsite/main.go 的 -base-path flag。 + BasePath string + Proxy *proxy.Client // client, or nil; controlled by the -proxy flag } @@ -115,7 +119,7 @@ func BuildServer(ctx context.Context, serverCfg ServerConfig) (*frontend.Server, return allModules[i].ModulePath < allModules[j].ModulePath }) - return newServer(getters, allModules, cfg.proxy, serverCfg.GoDocMode, serverCfg.DevMode, serverCfg.DevModeStaticDir) + return newServer(getters, allModules, cfg.proxy, serverCfg.GoDocMode, serverCfg.DevMode, serverCfg.DevModeStaticDir, serverCfg.BasePath) } // getModuleDirs returns the set of workspace modules for each directory, @@ -276,7 +280,7 @@ func buildGetters(ctx context.Context, cfg getterConfig) ([]fetch.ModuleGetter, return getters, nil } -func newServer(getters []fetch.ModuleGetter, localModules []frontend.LocalModule, prox *proxy.Client, goDocMode bool, devMode bool, staticFlag string) (*frontend.Server, error) { +func newServer(getters []fetch.ModuleGetter, localModules []frontend.LocalModule, prox *proxy.Client, goDocMode bool, devMode bool, staticFlag string, basePath string) (*frontend.Server, error) { lds := fetchdatasource.Options{ Getters: getters, ProxyClientForLatest: prox, @@ -307,6 +311,7 @@ func newServer(getters []fetch.ModuleGetter, localModules []frontend.LocalModule LocalMode: true, LocalModules: localModules, ThirdPartyFS: thirdparty.FS, + BasePath: basePath, }) if err != nil { return nil, err diff --git a/cmd/pkgsite/main.go b/cmd/pkgsite/main.go index 49b99a597..b924b7e40 100644 --- a/cmd/pkgsite/main.go +++ b/cmd/pkgsite/main.go @@ -62,6 +62,8 @@ import ( "golang.org/x/pkgsite/cmd/internal/pkgsite" "golang.org/x/pkgsite/internal/browser" + "golang.org/x/pkgsite/internal/frontend/versions" + "golang.org/x/pkgsite/internal/godoc" "golang.org/x/pkgsite/internal/log" "golang.org/x/pkgsite/internal/middleware/timeout" "golang.org/x/pkgsite/internal/proxy" @@ -75,6 +77,13 @@ var ( goRepoPath = flag.String("gorepo", "", "path to Go repo on local filesystem") useProxy = flag.Bool("proxy", false, "fetch from GOPROXY if not found locally") openFlag = flag.Bool("open", false, "open a browser window to the server's address") + // basePath:把整个站点挂在 URL 子路径下(如 -base-path=/pkgsitex, + // 站点入口 http://host/pkgsitex/)。空字符串 = 默认挂根路径,跟上游一致。 + // fork 加入这个 flag 是为了让 pkgsite 能跟主网关共用域名(反代而非 subdomain)。 + basePath = flag.String("base-path", "", "URL prefix to mount the site under (e.g. /pkgsitex). Must start with / and not end with /.") + // showUnexported:godoc 显示 unexported 符号(doc.AllDecls 模式)。fork + // 内网部署常见诉求——自家代码完整展示比 public-only 视图更有用。 + showUnexported = flag.Bool("show-unexported", false, "Render documentation including unexported declarations (doc.AllDecls mode).") // other flags are bound to ServerConfig below ) @@ -97,10 +106,29 @@ func main() { } flag.Parse() + if err := validateBasePath(*basePath); err != nil { + dief("%v", err) + } + + // 全局开关——godoc.DocPackage 读包级 var 决定是否传 doc.AllDecls。 + // 单进程 pkgsite 一种行为,没必要加到 ServerConfig 里再透传一层。 + godoc.IncludeUnexported = *showUnexported + // view source / file link 在 local mode 走 "/files/..." 路径——base path + // 挂子路径时 godoc.renderOptions 的 localPrefix 闭包读这个 var 加前缀。 + godoc.BasePath = *basePath + + // versions 包级 BasePath——pkgsite 内部 ConstructUnitURL 是所有 unit / + // package / module 详情页链接的核心 URL builder(subdir 列表、search + // 结果、breadcrumb 都走它),它生成的绝对 URL 必须带 base path 前缀, + // 否则点子目录会跳出 base path 外 404。同 godoc.IncludeUnexported 模式 + // 用包级 var 而非加 ServerConfig 字段层层透传。 + versions.BasePath = *basePath + serverCfg.UseLocalStdlib = true serverCfg.GoRepoPath = *goRepoPath serverCfg.Paths = collectPaths(flag.Args()) serverCfg.RecordCodeWikiMetrics = nil + serverCfg.BasePath = *basePath if serverCfg.UseCache || *useProxy { fmt.Fprintf(os.Stderr, "BYPASSING LICENSE CHECKING: MAY DISPLAY NON-REDISTRIBUTABLE INFORMATION\n") @@ -169,3 +197,18 @@ func collectPaths(args []string) []string { } return paths } + +// validateBasePath 强制 base-path 形如 "/foo" 或空——空表示挂根。 +// 不允许尾部斜杠(mux pattern 拼起来会双斜杠引发匹配失败)。 +func validateBasePath(p string) error { + if p == "" { + return nil + } + if !strings.HasPrefix(p, "/") { + return fmt.Errorf("-base-path %q must start with /", p) + } + if strings.HasSuffix(p, "/") { + return fmt.Errorf("-base-path %q must not end with /", p) + } + return nil +} diff --git a/cmd/pkgsitex-init/main.go b/cmd/pkgsitex-init/main.go new file mode 100644 index 000000000..a8564f1e0 --- /dev/null +++ b/cmd/pkgsitex-init/main.go @@ -0,0 +1,302 @@ +// Copyright 2026 Yechi Yang. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file of the upstream pkgsite. + +// Command pkgsitex-init 是 prod stack 的 init container: +// +// 1. 从 /etc/pkgsitex/config.yaml 读 GitHub PAT + 要监控的 module 列表 +// 2. 写 /etc/athens/.netrc 让 athens 内部 git 操作走 PAT 认证拉私有 repo +// 3. 等 worker http 就绪 +// 4. 对每个 module:可选 git ls-remote --tags 拿所有版本(multi-version 模式) +// + POST worker /fetch/{module}/@v/{version} 触发 worker 异步入 DB +// +// 进程跑完即退出(Kubernetes init container 模式 / docker compose 一次性 service)。 +// 周期 refresh 由独立 cron container(同一镜像,--mode=refresh)做——首次启动 +// 拉 latest + 全 tag,后续 cron 拉新增 tag。 +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +// Config 是 prod stack 的配置文件 schema,对应 /etc/pkgsitex/config.yaml。 +// +// 用户视角只需关心 github.token + modules——其他字段在 docker-compose 模板里 +// 已固定指向同 stack 内部 service 名,不需要改。 +type Config struct { + GitHub struct { + // Token 是 classic GitHub PAT(repo scope full)——athens 内部 git + // clone 用它拉私有 repo。明文存入 .netrc,因此 config.yaml + .netrc + // 都需 600 权限。生产环境通过 env var GITHUB_TOKEN 注入更佳,配置里 + // 写 ${GITHUB_TOKEN} 启动时 expand。 + Token string `yaml:"token"` + } `yaml:"github"` + + // Modules:prod 启动时 enqueue 给 worker 的 module 列表。每条 path 是 + // Go module path(如 github.com/CUHKSZ-ITSO-Dev/Chat 或 monorepo 子模块 + // github.com/CUHKSZ-ITSO-Dev/UniAuth/uniauth-gf)。 + Modules []ModuleSpec `yaml:"modules"` + + // Worker 内部 service URL;docker-compose 默认 "http://worker:8000"。 + Worker struct { + URL string `yaml:"url"` + } `yaml:"worker"` + + // Athens 配置:netrc 输出位置,让 athens 内部 git 拿到 PAT。 + Athens struct { + NetrcPath string `yaml:"netrcPath"` + } `yaml:"athens"` +} + +// ModuleSpec 一个待索引 module 的配置。 +type ModuleSpec struct { + // Path 是 Go module path(pkg.go.dev URL 的 module 段) + Path string `yaml:"path"` + + // Versions 控制初始化时 enqueue 哪些版本: + // - "latest"(默认):只 enqueue @latest,worker resolve 成 default branch + // - "all-tags":git ls-remote --tags 拿所有 tag + enqueue 每个 + 还有 + // latest(fork 内网部署常用——团队希望看每个 release 的文档) + // + // 不在配置里枚举具体版本字符串——上游 pkgsite 已经支持任意 module + // version URL 直接访问会触发 worker 按需 fetch;这里只是启动预热。 + Versions string `yaml:"versions"` + + // RepoURL 可选——module path 跟 git repo URL 不一致时用(如 monorepo + // 子模块 github.com/CUHKSZ-ITSO-Dev/UniAuth/uniauth-gf 的 repo URL 是 + // github.com/CUHKSZ-ITSO-Dev/UniAuth)。默认从 module path 推断: + // 取 github.com// 前三段。 + RepoURL string `yaml:"repoUrl"` +} + +func main() { + configPath := flag.String("config", "/etc/pkgsitex/config.yaml", "path to pkgsitex config yaml") + mode := flag.String("mode", "init", + "netrc-only = 只写 netrc 退出(在 athens 启动前跑,避免 athens 等 worker 等 athens 循环依赖);"+ + "init = 等 worker 就绪 + enqueue 全 module(首次启动);"+ + "refresh = 跳过 netrc / wait,仅重 ls-remote tags + enqueue 新版本(周期 cron)") + flag.Parse() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + cfg, err := loadConfig(*configPath) + if err != nil { + log.Fatalf("load config %s: %v", *configPath, err) + } + cfg.GitHub.Token = os.ExpandEnv(cfg.GitHub.Token) + if cfg.GitHub.Token == "" { + log.Fatal("github.token 为空(环境变量 GITHUB_TOKEN 未设置 / config 没填)") + } + + switch *mode { + case "netrc-only": + if err := writeNetrc(cfg.Athens.NetrcPath, cfg.GitHub.Token); err != nil { + log.Fatalf("写 netrc 失败: %v", err) + } + log.Printf("[netrc-only] netrc 写入 %s", cfg.Athens.NetrcPath) + + case "init": + if err := writeNetrc(cfg.Athens.NetrcPath, cfg.GitHub.Token); err != nil { + log.Fatalf("写 netrc 失败: %v", err) + } + log.Printf("[init] netrc 写入 %s", cfg.Athens.NetrcPath) + + if err := waitWorkerReady(ctx, cfg.Worker.URL); err != nil { + log.Fatalf("等 worker 就绪超时: %v", err) + } + log.Printf("[init] worker %s 就绪", cfg.Worker.URL) + + enqueueAll(ctx, cfg) + log.Printf("[init] 完成") + + case "refresh": + // refresh 模式跳过 netrc / worker wait(cron 跑时 stack 已稳定运行) + enqueueAll(ctx, cfg) + log.Printf("[refresh] 完成") + + default: + log.Fatalf("未知 mode=%q(支持 netrc-only / init / refresh)", *mode) + } +} + +func loadConfig(path string) (*Config, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cfg Config + if err := yaml.Unmarshal(b, &cfg); err != nil { + return nil, fmt.Errorf("yaml unmarshal: %w", err) + } + if cfg.Worker.URL == "" { + cfg.Worker.URL = "http://worker:8000" + } + if cfg.Athens.NetrcPath == "" { + cfg.Athens.NetrcPath = "/etc/athens/.netrc" + } + return &cfg, nil +} + +// writeNetrc 输出 athens 用的 .netrc 让 git 走 PAT 认证。 +// +// 文件格式(git 标准): +// +// machine github.com +// login x-access-token +// password +// +// "x-access-token" 是 GitHub 的 PAT username 占位(HTTPS Basic Auth 协议要求 +// username 非空,PAT 走 password 字段)。 +// +// 同时确保 dir 存在 + 权限 600(含 PAT 明文)。 +func writeNetrc(path, token string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + content := fmt.Sprintf("machine github.com\nlogin x-access-token\npassword %s\n", token) + return os.WriteFile(path, []byte(content), 0o600) +} + +// waitWorkerReady 轮询 worker /healthz 端点直到就绪或 ctx 超时。 +// +// 间隔 2 秒;总超时由 caller ctx 控制。docker-compose 的 healthcheck 应该 +// 已让 worker 在 init 启动前就 ready,但这层兜底防容器启动顺序 race。 +func waitWorkerReady(ctx context.Context, workerURL string) error { + tick := time.NewTicker(2 * time.Second) + defer tick.Stop() + for { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, workerURL+"/healthz", nil) + resp, err := http.DefaultClient.Do(req) + if err == nil { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + if resp.StatusCode < 400 { + return nil + } + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-tick.C: + } + } +} + +// enqueueAll 遍历 cfg.Modules,对每个 module 触发 worker 拉相应 version。 +// +// versions=all-tags 时先 git ls-remote --tags 拿全 tag 再 enqueue 每个;否则只 +// enqueue @latest(worker 自己 resolve)。错误只 log 不 fatal——单 module 失败 +// 不该阻塞其他。 +func enqueueAll(ctx context.Context, cfg *Config) { + for _, mod := range cfg.Modules { + versions := []string{"latest"} + if mod.Versions == "all-tags" { + tags, err := lsRemoteTags(ctx, mod, cfg.GitHub.Token) + if err != nil { + log.Printf("ls-remote %s: %v(跳过,仅 enqueue @latest)", mod.Path, err) + } else { + versions = append(versions, tags...) + } + } + for _, v := range versions { + if err := enqueueOne(ctx, cfg.Worker.URL, mod.Path, v); err != nil { + log.Printf("enqueue %s@%s: %v", mod.Path, v, err) + continue + } + log.Printf("enqueue %s@%s 成功", mod.Path, v) + } + } +} + +// lsRemoteTags 跑 git ls-remote --tags 拿 module 对应 repo 的所有 git tag。 +// +// 用 https://x-access-token:@github.com/ 形式让 git 走 PAT 认证。 +// PAT 出现在 cmd 行参数里——子进程不会 leak(exec.Command 不写 shell history), +// 但 ps 临时可见——内网容器内部环境可接受。 +// +// repoURL 推断:默认取 module path 前三段(github.com//), +// monorepo 子模块走显式 RepoURL 字段。 +// +// 返回值过滤:peeled tag refs(refs/tags/v1.0^{})跳掉,pkgsite 只认 vX.Y.Z +// 形式的 SemVer tag——非 SemVer tag 也 enqueue 但 worker 大概率会拒(log 警告 +// 但不 fatal,让 caller 看到详细错误码)。 +func lsRemoteTags(ctx context.Context, mod ModuleSpec, token string) ([]string, error) { + repoURL := mod.RepoURL + if repoURL == "" { + // 从 module path 推断:取前三段 + parts := strings.Split(mod.Path, "/") + if len(parts) < 3 || parts[0] != "github.com" { + return nil, fmt.Errorf("无法推断 repo URL(仅支持 github.com/owner/repo... 形式,得到 %q)", mod.Path) + } + repoURL = strings.Join(parts[:3], "/") + } + + authedURL := fmt.Sprintf("https://x-access-token:%s@%s", url.PathEscape(token), repoURL) + cmd := exec.CommandContext(ctx, "git", "ls-remote", "--tags", authedURL) + out, err := cmd.Output() + if err != nil { + // 不 leak token 到错误信息:只显示 repoURL 不含 token + if errors.Is(err, exec.ErrNotFound) { + return nil, fmt.Errorf("git 二进制不在 PATH(请检查 init container image)") + } + return nil, fmt.Errorf("git ls-remote %s 失败: %w", repoURL, err) + } + + var tags []string + for _, line := range strings.Split(string(out), "\n") { + idx := strings.Index(line, "refs/tags/") + if idx < 0 { + continue + } + tag := line[idx+len("refs/tags/"):] + // peeled tag refs(如 "refs/tags/v1.0.0^{}")跳过——它们指向 commit + // 而不是 tag object 本身,重复出现 + if strings.HasSuffix(tag, "^{}") { + continue + } + tags = append(tags, tag) + } + return tags, nil +} + +// enqueueOne 调 worker /fetch//@v/ 端点触发异步 fetch。 +// +// 该端点是 pkgsite 上游 worker 内置——worker.handleFetch 处理:先看 DB 是否 +// 已有此版本数据,没有就 enqueue 进 pgqueue / inmem queue 走 ProxyClient +// 拉模块(我们 stack 里 ProxyClient 指 athens,athens 走 git + PAT 拉私有 +// repo)。 +// +// HTTP 200 = 已 enqueue(不代表 fetch 成功,那是异步事),4xx/5xx 才是真 +// 错误(典型:module path 错 / version 格式不合 SemVer)。 +func enqueueOne(ctx context.Context, workerURL, modulePath, version string) error { + u := fmt.Sprintf("%s/fetch/%s/@v/%s", workerURL, modulePath, version) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) + return fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + return nil +} diff --git a/compose.prod.yaml b/compose.prod.yaml new file mode 100644 index 000000000..c96e94f4d --- /dev/null +++ b/compose.prod.yaml @@ -0,0 +1,176 @@ +# pkgsitex prod 部署 stack —— 4 件套 + 一次性 init / migrate: +# +# postgres: pkgsite schema 缓存 module 索引(disk volume 持久) +# athens: GOPROXY 缓存层 + 用 PAT 拉私有 GitHub repo(disk 持久) +# worker: 周期 fetch module → 入 postgres +# frontend: 用户浏览的 web,base-path / show-unexported / license +# permissive 全开,端口 8089 暴露 +# migrate: 一次性,启动时跑 migrations/ 下 SQL up(基于 migrate/migrate) +# init: 一次性,写 athens netrc + 用 PAT git ls-remote 拿 tag + +# 调 worker /fetch enqueue 各 module 各版本 +# +# 启动: +# cp deploy/prod/.env.example deploy/prod/.env +# vi deploy/prod/.env # 填 GITHUB_TOKEN +# vi deploy/prod/config.yaml # 填要监控的 module 列表 +# docker compose -f compose.prod.yaml --env-file deploy/prod/.env up -d +# open http://localhost:8089/pkgsitex/ +# +# 加新 module:编辑 deploy/prod/config.yaml;触发刷新: +# docker compose -f compose.prod.yaml --env-file deploy/prod/.env run --rm init --mode=refresh +# +# Prod (k8s) 部署:以本 yaml 为底稿改写 helm chart,DB / athens 用 cluster 内 +# 共享实例,init 改成 CronJob 周期 refresh。 + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: pkgsitex + POSTGRES_PASSWORD: pkgsitex + POSTGRES_DB: discovery_db + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pkgsitex -d discovery_db"] + interval: 5s + timeout: 3s + retries: 20 + restart: unless-stopped + + athens: + image: gomods/athens:v0.15.0 + environment: + ATHENS_STORAGE_TYPE: disk + ATHENS_DISK_STORAGE_ROOT: /var/lib/athens + ATHENS_DOWNLOAD_MODE: sync + ATHENS_NETRC_PATH: /etc/athens/.netrc + ATHENS_LOG_LEVEL: info + # athens 内部 git clone 时通过 .netrc 走 PAT 认证拉私有 repo; + # GOPROXY=direct 让 athens 直接 git 拉而不是再代理给上游 + GOPROXY: direct + volumes: + - athens-data:/var/lib/athens + - athens-config:/etc/athens + expose: + - "3000" + healthcheck: + test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:3000/healthz"] + interval: 10s + timeout: 5s + retries: 30 + depends_on: + init: + # 等 init 写好 netrc 再起 athens + condition: service_completed_successfully + restart: unless-stopped + + migrate: + image: migrate/migrate:v4.18.1 + volumes: + - ../migrations:/migrations:ro + command: + - "-path=/migrations" + - "-database=postgres://pkgsitex:pkgsitex@postgres:5432/discovery_db?sslmode=disable" + - "up" + depends_on: + postgres: + condition: service_healthy + + worker: + image: ${PKGSITEX_IMAGE:-nickwilde18/pkgsitex:fork-main} + pull_policy: ${PKGSITEX_PULL_POLICY:-always} + entrypoint: ["/usr/local/bin/worker"] + environment: + GO_DISCOVERY_DATABASE_HOST: postgres + GO_DISCOVERY_DATABASE_USER: pkgsitex + GO_DISCOVERY_DATABASE_PASSWORD: pkgsitex + GO_DISCOVERY_DATABASE_NAME: discovery_db + GO_MODULE_PROXY_URL: http://athens:3000 + PORT: "8000" + command: + - "-queue=postgres" + - "-static=/app/static" + expose: + - "8000" + depends_on: + migrate: + condition: service_completed_successfully + athens: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8000/healthz"] + interval: 10s + timeout: 5s + retries: 30 + restart: unless-stopped + + frontend: + image: ${PKGSITEX_IMAGE:-nickwilde18/pkgsitex:fork-main} + pull_policy: ${PKGSITEX_PULL_POLICY:-always} + entrypoint: ["/usr/local/bin/frontend"] + environment: + GO_DISCOVERY_DATABASE_HOST: postgres + GO_DISCOVERY_DATABASE_USER: pkgsitex + GO_DISCOVERY_DATABASE_PASSWORD: pkgsitex + GO_DISCOVERY_DATABASE_NAME: discovery_db + PORT: "8080" + command: + - "-static=/app/static" + - "-third_party=/app/third_party" + # fork 加的子路径 / unexported / license——直接走 cmd/frontend flag。 + # license 用上游已有 -bypass_license_check 不再单独包装。 + - "-base-path=/pkgsitex" + - "-show-unexported" + - "-bypass_license_check" + ports: + - "8089:8080" + depends_on: + migrate: + condition: service_completed_successfully + restart: unless-stopped + + # init 一次性跑:写 athens netrc + 等 worker 起来 + enqueue 全 module。 + # athens 依赖 init(要 netrc 文件),所以 init 要先于 athens 跑——但 + # init 又要等 worker /healthz —— worker 又依赖 athens —— 循环依赖。 + # + # 解法:分两阶段—— + # 阶段 1(service netrc-writer):仅写 netrc 然后退出,依赖 0。athens 等它 + # 阶段 2(service init):等 worker 起来后 enqueue。worker 依赖 athens + # + # 用同一镜像不同 mode 跑两次。 + netrc-writer: + image: ${PKGSITEX_IMAGE:-nickwilde18/pkgsitex:fork-main} + pull_policy: ${PKGSITEX_PULL_POLICY:-always} + entrypoint: ["/usr/local/bin/pkgsitex-init"] + environment: + GITHUB_TOKEN: ${GITHUB_TOKEN} + volumes: + - ./deploy/prod/config.yaml:/etc/pkgsitex/config.yaml:ro + - athens-config:/etc/athens + command: + - "--config=/etc/pkgsitex/config.yaml" + - "--mode=netrc-only" + + init: + image: ${PKGSITEX_IMAGE:-nickwilde18/pkgsitex:fork-main} + pull_policy: ${PKGSITEX_PULL_POLICY:-always} + entrypoint: ["/usr/local/bin/pkgsitex-init"] + environment: + GITHUB_TOKEN: ${GITHUB_TOKEN} + volumes: + - ./deploy/prod/config.yaml:/etc/pkgsitex/config.yaml:ro + - athens-config:/etc/athens + command: + - "--config=/etc/pkgsitex/config.yaml" + - "--mode=init" + depends_on: + netrc-writer: + condition: service_completed_successfully + worker: + condition: service_healthy + +volumes: + postgres-data: + athens-data: + athens-config: diff --git a/deploy/prod/.env.example b/deploy/prod/.env.example new file mode 100644 index 000000000..6f4ec4ff4 --- /dev/null +++ b/deploy/prod/.env.example @@ -0,0 +1,15 @@ +# pkgsitex prod stack 环境变量 +# +# 拷成 .env(同目录)后填实际值: +# cp deploy/prod/.env.example deploy/prod/.env +# vi deploy/prod/.env +# +# .env 含 PAT 明文,**不要 git commit**——已在 .gitignore 屏蔽。 + +# Classic GitHub PAT(repo scope full)。athens 内部用 git + PAT 拉私有 repo。 +# 生成:https://github.com/settings/tokens(Classic 不是 fine-grained) +GITHUB_TOKEN=ghp_PASTE_YOUR_TOKEN_HERE + +# GOPROXY for go build——空会走 https://goproxy.cn(fork 默认 build 用) +# 公司有 Athens 私有镜像就改这里: +# GOPROXY=https://athens.corp.com,https://goproxy.cn,direct diff --git a/deploy/prod/Dockerfile b/deploy/prod/Dockerfile new file mode 100644 index 000000000..e32ff2df8 --- /dev/null +++ b/deploy/prod/Dockerfile @@ -0,0 +1,46 @@ +# pkgsitex prod 单 image 三 entrypoint(worker / frontend / init)。 +# +# 单 image 设计:减少 docker.io push 频次和拉取流量——用户拉一次就拿到三个 +# 二进制;compose 里通过 entrypoint: 选择跑哪个。三种角色共享一份 ca-certs +# 和 git,本来分开 image 也是同 base,单 image 没浪费。 +# +# 跟仓库根 `Dockerfile`(cmd/pkgsite local mode)不同:那个跑 fetchdatasource +# 直读文件系统,不走 DB 不走 GOPROXY;本镜像跑 cmd/worker / cmd/frontend, +# 需要 PostgreSQL + athens GOPROXY,跟 pkg.go.dev 自身架构一致。 + +FROM golang:1.25 AS builder +WORKDIR /src + +COPY go.mod go.sum ./ +ARG GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct +ENV GOPROXY=${GOPROXY} +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /bin/worker ./cmd/worker +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /bin/frontend ./cmd/frontend +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /bin/pkgsitex-init ./cmd/pkgsitex-init + +FROM debian:bookworm-slim +LABEL org.opencontainers.image.source="https://github.com/NickWilde18/pkgsitex" +LABEL org.opencontainers.image.description="Self-hosted pkgsite fork (worker/frontend/init in one image)" +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates git wget && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /bin/worker /usr/local/bin/worker +COPY --from=builder /bin/frontend /usr/local/bin/frontend +COPY --from=builder /bin/pkgsitex-init /usr/local/bin/pkgsitex-init + +# 静态资源 / migrations / third_party 都内置——cmd/worker / cmd/frontend 都 +# 通过 -static / -third_party flag 指 /app 下,cmd/worker 内部跑 migrations +# 时也能找到 /app/migrations +COPY --from=builder /src/static /app/static +COPY --from=builder /src/third_party /app/third_party +COPY --from=builder /src/migrations /app/migrations +WORKDIR /app + +# 默认 entrypoint = frontend(用户最常用);compose 里其他 service 用 +# entrypoint: ["/usr/local/bin/worker"] / ["/usr/local/bin/pkgsitex-init"] 覆盖 +EXPOSE 8000 8080 +ENTRYPOINT ["/usr/local/bin/frontend"] diff --git a/deploy/prod/README.md b/deploy/prod/README.md new file mode 100644 index 000000000..7437954d3 --- /dev/null +++ b/deploy/prod/README.md @@ -0,0 +1,146 @@ +# pkgsitex prod 部署 + +完整 4 件套 stack(postgres + athens + worker + frontend)+ init / migrate 一次性容器,跑配置驱动 + 私有 repo 自动 fetch + multi-version 支持。 + +跟仓库根的 `compose.yaml`(cmd/pkgsite local mode,单进程,文件 mount)不同——本目录是 prod 模式,跟 pkg.go.dev 自身架构一致。 + +镜像 `nickwilde18/pkgsitex:fork-main` 由 GitHub Actions 自动 build & push 到 docker.io(推 `fork/main` 分支触发);用户拉 image 即用,不需要本地 build。 + +## 启动步骤 + +```sh +cd ~/Repo/pkgsitex # 仓库根,不是 deploy/prod/ + +# 1) 配 PAT +cp deploy/prod/.env.example deploy/prod/.env +$EDITOR deploy/prod/.env # GITHUB_TOKEN=ghp_xxx + +# 2) 配要监控的 module 列表 +$EDITOR deploy/prod/config.yaml # modules: [...] + +# 3) 起 stack(首次 docker pull 镜像 ~30 秒) +docker compose -f compose.prod.yaml --env-file deploy/prod/.env up -d + +# 4) 看日志确认 worker 在拉 module +docker compose -f compose.prod.yaml logs -f worker + +# 5) 浏览 +open http://localhost:8089/pkgsitex/ +``` + +## 切镜像版本 / 用本地 build + +`compose.prod.yaml` 默认用 `nickwilde18/pkgsitex:fork-main`,可通过 env 切: + +```sh +# 钉死某个 git sha(不可变 tag) +PKGSITEX_IMAGE=nickwilde18/pkgsitex:abc1234 docker compose -f compose.prod.yaml ... up -d + +# 用本地刚 build 的(开发 fork patch 时用) +docker build -t pkgsitex:local -f deploy/prod/Dockerfile . +PKGSITEX_IMAGE=pkgsitex:local PKGSITEX_PULL_POLICY=never \ + docker compose -f compose.prod.yaml ... up -d +``` + +## 运维操作 + +### 加 / 删 module + +编辑 [`config.yaml`](config.yaml) 后触发 init 一次性容器重新 enqueue: + +```sh +docker compose -f compose.prod.yaml --env-file deploy/prod/.env \ + run --rm init --mode=refresh +``` + +`refresh` 模式跳过 netrc 写 + worker wait(stack 已稳定运行),仅重 ls-remote tag + enqueue 新版本给 worker。 + +### 周期刷新(cron) + +每天凌晨 3 点跑 `init --mode=refresh` 增量拉新版本。host crontab: + +``` +0 3 * * * cd ~/Repo/pkgsitex && docker compose -f compose.prod.yaml \ + --env-file deploy/prod/.env run --rm init --mode=refresh +``` + +或者用 host systemd timer。 + +### 看 athens 缓存了哪些 module + +```sh +docker compose -f compose.prod.yaml exec athens ls /var/lib/athens +``` + +### 重置(清 DB + athens 缓存) + +```sh +docker compose -f compose.prod.yaml down -v # -v 删 volumes +``` + +## 架构 + +``` + ┌──────────────────┐ + 配置文件 │ config.yaml │ modules + token + worker URL + └────────┬─────────┘ + │ read + ▼ + ┌──────────────────┐ + │ init container │ 写 netrc + git ls-remote --tags + + │ (one-shot) │ POST worker /fetch//@v/ + └────────┬─────────┘ + │ + ▼ + ┌──────┐ GOPROXY ┌────────────┐ fetch+parse ┌────────┐ + │worker├──────────►│ athens │──── git ────► │ GitHub │ + └──┬───┘ │ (PAT auth) │ PAT │ 私有 │ + │ store └────────────┘ └────────┘ + ▼ + ┌────────┐ + │postgres│ ◄────── frontend(端口 8089)─── 用户浏览 + └────────┘ +``` + +数据流(首次访问 `module@v1.2.3`): + +1. 用户浏览器 → frontend 端口 8089 +2. frontend 查 postgres 没有此 module 数据 → 返 "fetching" 页面 +3. (并行)init 容器启动时已 enqueue(worker `/fetch/...`),worker 异步处理 +4. worker 调 athens GOPROXY 协议要 module zip +5. athens 第一次见 → git clone 用 PAT → 缓存到 disk → 返 zip +6. worker 解析 zip → 入 postgres +7. 用户刷新 → frontend 命中 postgres → 渲 godoc + +## 配置文件 schema + +[`config.yaml`](config.yaml) 字段: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `github.token` | string | classic GitHub PAT(repo scope full);建议写 `${GITHUB_TOKEN}` 走 env | +| `worker.url` | string | worker 内部 URL,docker-compose 默认 `http://worker:8000` | +| `athens.netrcPath` | string | init 写 netrc 给 athens 用,默认 `/etc/athens/.netrc` | +| `modules[].path` | string | Go module path | +| `modules[].versions` | string | `latest`(默认) / `all-tags` | +| `modules[].repoUrl` | string | 可选——monorepo 子模块时指定 git repo URL(不含子目录) | + +## License + +prod stack frontend 默认开 `licenses.permissive`——所有 module 不论 license 类型都渲染(包括 `All Rights Reserved` / unknown / proprietary)。这是内网部署诉求;不希望渲染某些 module 时把它从 `config.yaml` 删除即可。 + +## 故障排查 + +| 现象 | 排查 | +|---|---| +| `worker` 容器拉 module 401 | `docker logs athens` 看是否 git clone 401,PAT 错或 scope 不够 | +| init 容器 git ls-remote `Permission denied` | 同上,PAT 没 `repo` scope | +| frontend 页面 "this package is not redistributable" | 检查 frontend 容器 env `PKGSITEX_LICENSE_PERMISSIVE=true`(cmd/frontend flag 集成完后生效)| +| migrate 失败 | `docker compose logs migrate` 看 SQL 错;通常是 schema 跟上游 mismatch(rebase 后 migrations 文件冲突)| +| athens 拉私有 repo 超时 | 内网 git 出口受限——把 athens 的 git config 走公司代理:编辑 athens 容器 `~/.gitconfig` 加 `http.proxy` | + +## TODO + +- [ ] cmd/frontend / cmd/worker 加 `-base-path` / `-show-unexported` / `-license-permissive` flag(当前用 env var 占位,未通到代码层) +- [ ] init container 周期 cron 内置(不靠 host crontab) +- [ ] athens config volume 改成 ConfigMap 形式让 helm chart 复用更方便 diff --git a/deploy/prod/config.yaml b/deploy/prod/config.yaml new file mode 100644 index 000000000..fd0316831 --- /dev/null +++ b/deploy/prod/config.yaml @@ -0,0 +1,46 @@ +# pkgsitex prod 配置——cmd/pkgsitex-init 启动时读,决定要监控的 module 列表。 +# +# 编辑此文件后跑: +# docker compose -f compose.prod.yaml --env-file deploy/prod/.env \ +# run --rm init --mode=refresh +# 让 init 容器重新枚举 tag + enqueue 给 worker。 + +github: + # ${GITHUB_TOKEN} 在容器里走 env var 注入,不要在这里写明文 PAT + token: ${GITHUB_TOKEN} + +# Worker 内部 service URL——docker-compose 默认 service 名是 worker,端口 8000 +worker: + url: http://worker:8000 + +athens: + # init 写 netrc 给 athens 内部 git 使用 + netrcPath: /etc/athens/.netrc + +# 要监控的 Go module 列表。每条目支持: +# path: Go module path(pkg.go.dev URL 段)—— 必填 +# versions: "latest"(默认,仅拉默认分支)/ "all-tags"(git ls-remote 拿 +# 所有 tag 全部 enqueue,团队希望看每个 release 文档时用) +# repoUrl: 可选——module path 跟 git repo URL 不一致时(如 monorepo 子模块) +# +# 加新 module:append 一行 + 跑 init --mode=refresh。删 module 不需要从 DB 清, +# 后续不再 fetch 自然过期。 +modules: + - path: github.com/CUHKSZ-ITSO-Dev/Chat + versions: all-tags + + - path: github.com/CUHKSZ-ITSO-Dev/Doubao-Speech-Service + versions: all-tags + + # UniAuth 是 monorepo 含两个 Go module(uniauth-gf / ittools_sync)。 + # path 是 module path(含子目录),repoUrl 指 git repo 不含子目录。 + - path: github.com/CUHKSZ-ITSO-Dev/UniAuth/uniauth-gf + versions: all-tags + repoUrl: github.com/CUHKSZ-ITSO-Dev/UniAuth + + - path: github.com/CUHKSZ-ITSO-Dev/UniAuth/ittools_sync + versions: all-tags + repoUrl: github.com/CUHKSZ-ITSO-Dev/UniAuth + + - path: github.com/CUHKSZ-ITSO-Dev/open-platform + versions: all-tags diff --git a/internal/fetch/fetch_test.go b/internal/fetch/fetch_test.go index 5bc060188..3f02aa8dc 100644 --- a/internal/fetch/fetch_test.go +++ b/internal/fetch/fetch_test.go @@ -40,7 +40,7 @@ var ( type fetchFunc func(t *testing.T, withLicenseDetector bool, ctx context.Context, mod *proxytest.Module, fetchVersion string) (*FetchResult, *licenses.Detector) func TestMain(m *testing.M) { - dochtml.LoadTemplates(templateFS) + dochtml.LoadTemplates(templateFS, "") testModules = proxytest.LoadTestModules("../proxy/testdata") licenses.OmitExceptions = true os.Exit(m.Run()) diff --git a/internal/fetch/load.go b/internal/fetch/load.go index e8f03316c..5fede8539 100644 --- a/internal/fetch/load.go +++ b/internal/fetch/load.go @@ -303,6 +303,13 @@ func loadPackageForBuildContext(ctx context.Context, files map[string][]byte, in if modulePath == stdlib.ModulePath && innerPath == "builtin" { removeNodes = false } + // fork:-show-unexported flag 设全局 godoc.IncludeUnexported=true + // 时,AST 阶段也保留未导出 FuncDecl(默认 [removeUnusedASTNodes] + // 整个剥掉),后续 [DocPackage] 才有 nodes 可读 + doc.AllDecls 才生效。 + // 否则 doc.AllDecls 在 AST 已经被剔光的输入上无效。 + if godoc.IncludeUnexported { + removeNodes = false + } docPkg.AddFile(pf, removeNodes) } diff --git a/internal/fetchdatasource/fetchdatasource_test.go b/internal/fetchdatasource/fetchdatasource_test.go index 223c38bd9..8cfb2b488 100644 --- a/internal/fetchdatasource/fetchdatasource_test.go +++ b/internal/fetchdatasource/fetchdatasource_test.go @@ -35,7 +35,7 @@ var ( ) func TestMain(m *testing.M) { - dochtml.LoadTemplates(template.TrustedFSFromTrustedSource(template.TrustedSourceFromConstant("../../static"))) + dochtml.LoadTemplates(template.TrustedFSFromTrustedSource(template.TrustedSourceFromConstant("../../static")), "") defaultTestModules = proxytest.LoadTestModules("../proxy/testdata") licenses.OmitExceptions = true os.Exit(m.Run()) diff --git a/internal/frontend/details.go b/internal/frontend/details.go index 0323f0407..a6be4c1fd 100644 --- a/internal/frontend/details.go +++ b/internal/frontend/details.go @@ -30,10 +30,13 @@ func (s *Server) serveDetails(w http.ResponseWriter, r *http.Request, ds interna if r.Method != http.MethodGet && r.Method != http.MethodHead { return &serrors.ServerError{Status: http.StatusMethodNotAllowed} } - if r.URL.Path == "/" { + // 站点根:原版只判 "/",挂 -base-path 后还要判 basePath 自身(带或不带尾斜杠)。 + if r.URL.Path == "/" || r.URL.Path == s.basePath || r.URL.Path == s.basePath+"/" { s.serveHomepage(ctx, w, r) return nil } + // trailing-slash 规范化:去尾再 301,但不要把 basePath 自身的尾去掉 + // (否则跟 mux 自动 redirect 形成 301 ↔ 307 死循环)。 if strings.HasSuffix(r.URL.Path, "/") { url := *r.URL url.Path = strings.TrimSuffix(r.URL.Path, "/") @@ -47,7 +50,13 @@ func (s *Server) serveDetails(w http.ResponseWriter, r *http.Request, ds interna ctx = setExperimentsFromQueryParam(ctx, r) } - urlInfo, err := urlinfo.ExtractURLPathInfo(r.URL.Path) + // 给 ExtractURLPathInfo 看到的 path 剥掉 basePath——它假设挂根, + // path 形如 "/[@]",basePath 不属于 module path 一部分。 + parsePath := r.URL.Path + if s.basePath != "" { + parsePath = strings.TrimPrefix(parsePath, s.basePath) + } + urlInfo, err := urlinfo.ExtractURLPathInfo(parsePath) if err != nil { var epage *page.ErrorPage if uerr := new(urlinfo.UserError); errors.As(err, &uerr) { @@ -63,7 +72,8 @@ func (s *Server) serveDetails(w http.ResponseWriter, r *http.Request, ds interna return serrors.InvalidVersionError(urlInfo.FullPath, urlInfo.RequestedVersion) } if urlPath := stdlibRedirectURL(urlInfo.FullPath); urlPath != "" { - http.Redirect(w, r, urlPath, http.StatusMovedPermanently) + // stdlibRedirectURL 返回 "/std" 之类挂根 path,反代场景要补 basePath。 + http.Redirect(w, r, s.basePath+urlPath, http.StatusMovedPermanently) return } if err := checkExcluded(ctx, ds, urlInfo.FullPath, urlInfo.RequestedVersion); err != nil { diff --git a/internal/frontend/fetchserver/404.go b/internal/frontend/fetchserver/404.go index 7841e2731..6bdce2adc 100644 --- a/internal/frontend/fetchserver/404.go +++ b/internal/frontend/fetchserver/404.go @@ -46,7 +46,7 @@ func (s *FetchServer) ServePathNotFoundPage(w http.ResponseWriter, r *http.Reque log.Error(ctx, err) } if path != "" { - http.Redirect(w, r, fmt.Sprintf("/%s", path), http.StatusFound) + http.Redirect(w, r, fmt.Sprintf("%s/%s", s.BasePath, path), http.StatusFound) return } @@ -132,7 +132,7 @@ func (s *FetchServer) ServePathNotFoundPage(w http.ResponseWriter, r *http.Reque // See https://golang.org/issue/43725 for context. nm, err := db.GetNestedModules(ctx, fullPath) if err == nil && len(nm) > 0 { - http.Redirect(w, r, "/search?q="+url.QueryEscape(fullPath), http.StatusFound) + http.Redirect(w, r, s.BasePath+"/search?q="+url.QueryEscape(fullPath), http.StatusFound) return nil } return &serrors.ServerError{ diff --git a/internal/frontend/fetchserver/fetch.go b/internal/frontend/fetchserver/fetch.go index 27f0fda06..baff89d41 100644 --- a/internal/frontend/fetchserver/fetch.go +++ b/internal/frontend/fetchserver/fetch.go @@ -89,6 +89,10 @@ var ( type FetchServer struct { Queue queue.Queue TaskIDChangeInterval time.Duration + // BasePath:站点 URL 子路径前缀(如 "/pkgsitex")。空 = 挂根。 + // 用于 [ServePathNotFoundPage] 内部 http.Redirect 拼绝对 URL,否则 + // /search?q=... 之类的重定向会跳到 reverse proxy 之外。 + BasePath string } // ServeFetch checks if a requested path and version exists in the database. diff --git a/internal/frontend/server.go b/internal/frontend/server.go index 89f63a9c1..8504ea72f 100644 --- a/internal/frontend/server.go +++ b/internal/frontend/server.go @@ -63,6 +63,11 @@ type Server struct { instanceID string HTTPClient *http.Client recordCodeWikiMetrics RecordClickFunc + // basePath:URL 前缀(如 "/pkgsitex"),空字符串 = 默认挂根。 + // 由调用方通过 ServerConfig.BasePath 设置,并贯穿 mux pattern / StripPrefix / + // template "abs" 助手 / 内部 redirect / dochtml 链接拼接,使整站能整体迁移到子路径。 + // 关于不变式:basePath 要么空、要么形如 "/foo"——尾部不带斜杠(拼 mux 时双斜杠会失配)。 + basePath string mu sync.Mutex // Protects all fields below templates map[string]*template.Template @@ -101,16 +106,18 @@ type ServerConfig struct { VulndbClient *vuln.Client HTTPClient *http.Client RecordCodeWikiMetrics RecordClickFunc + // BasePath:URL 子路径前缀(如 "/pkgsitex"),空 = 挂根。详见 [Server.basePath]。 + BasePath string } // NewServer creates a new Server for the given database and template directory. func NewServer(scfg ServerConfig) (_ *Server, err error) { defer derrors.Wrap(&err, "NewServer(...)") - ts, err := templates.ParsePageTemplates(scfg.TemplateFS) + ts, err := templates.ParsePageTemplates(scfg.TemplateFS, scfg.BasePath) if err != nil { return nil, fmt.Errorf("error parsing templates: %v", err) } - dochtml.LoadTemplates(scfg.TemplateFS) + dochtml.LoadTemplates(scfg.TemplateFS, scfg.BasePath) s := &Server{ fetchServer: scfg.FetchServer, getDataSource: scfg.DataSourceGetter, @@ -128,6 +135,7 @@ func NewServer(scfg ServerConfig) (_ *Server, err error) { vulnClient: scfg.VulndbClient, HTTPClient: scfg.HTTPClient, recordCodeWikiMetrics: scfg.RecordCodeWikiMetrics, + basePath: scfg.BasePath, } if s.HTTPClient == nil { s.HTTPClient = http.DefaultClient @@ -177,7 +185,21 @@ type Cacher interface { // Install registers server routes using the given handler registration func. // authValues is the set of values that can be set on authHeader to bypass the // cache. +// +// fork 注:handle 入参在函数体内被 shadow 成 base-path-aware 版本——所有 +// "GET /static/"、"/v1/api" 之类的 pattern 都自动通过 [Server.prefixPattern] +// 加上 [Server.basePath],无需改下面任何具体路由声明。空 basePath 时 prefix +// 是 no-op,行为与上游完全一致。internal StripPrefix 仍需手动加 s.basePath +// 让 file server 能剥到正确的剩余 path。 func (s *Server) Install(handle func(string, http.Handler), cacher Cacher, authValues []string) { + // shadow 入参 handle:所有 handle("GET /static/", ...) 自动变成 + // realHandle("GET /pkgsitex/static/", ...),保持原代码零改动。 + // installDebugHandlers(handle) 透传时也用包装版本——_debug/pprof 等 + // 调试端点同样落在 base-path 下。 + realHandle := handle + handle = func(pattern string, h http.Handler) { + realHandle(s.prefixPattern(pattern), h) + } var ( detailHandler http.Handler = s.errorHandler(s.serveDetails) fetchHandler http.Handler @@ -208,12 +230,12 @@ func (s *Server) Install(handle func(string, http.Handler), cacher Cacher, authV log.Infof(r.Context(), "Request made to %q", r.URL.Path) })) handle("GET /static/", s.staticHandler()) - handle("GET /third_party/", http.StripPrefix("/third_party", http.FileServer(http.FS(s.thirdPartyFS)))) + handle("GET /third_party/", http.StripPrefix(s.basePath+"/third_party", http.FileServer(http.FS(s.thirdPartyFS)))) handle("GET /favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serveFileFS(w, r, s.staticFS, "shared/icon/favicon.ico") })) - handle("/sitemap/", http.StripPrefix("/sitemap/", http.FileServer(http.Dir("private/sitemap")))) + handle("/sitemap/", http.StripPrefix(s.basePath+"/sitemap/", http.FileServer(http.Dir("private/sitemap")))) handle("GET /mod/", http.HandlerFunc(s.handleModuleDetailsRedirect)) handle("GET /pkg/", http.HandlerFunc(s.handlePackageDetailsRedirect)) if fetchHandler != nil { @@ -230,11 +252,11 @@ func (s *Server) Install(handle func(string, http.Handler), cacher Cacher, authV handle("GET /C", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Package "C" is a special case: redirect to /cmd/cgo. // (This is what golang.org/C does.) - http.Redirect(w, r, "/cmd/cgo", http.StatusMovedPermanently) + http.Redirect(w, r, s.basePath+"/cmd/cgo", http.StatusMovedPermanently) })) handle("GET /codewiki", http.HandlerFunc(s.handleCodeWikiRedirect)) handle("GET /golang.org/x", s.staticPageHandler("subrepo", "Sub-repositories")) - handle("GET /files/", http.StripPrefix("/files", s.fileMux)) + handle("GET /files/", http.StripPrefix(s.basePath+"/files", s.fileMux)) handle("GET /vuln/", vulnHandler) handle("GET /v1/package/", s.apiHandler(api.ServePackage)) handle("GET /v1/symbols/", s.apiHandler(api.ServePackageSymbols)) @@ -246,10 +268,10 @@ func (s *Server) Install(handle func(string, http.Handler), cacher Cacher, authV handle("GET /v1/vulns/", s.apiHandler(api.ServeVulnerabilities(s.vulnClient))) handle("GET /v1/api", s.apiDocHandler()) handle("GET /api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/v1/api", http.StatusMovedPermanently) + http.Redirect(w, r, s.basePath+"/v1/api", http.StatusMovedPermanently) })) handle("GET /v1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/v1/api", http.StatusMovedPermanently) + http.Redirect(w, r, s.basePath+"/v1/api", http.StatusMovedPermanently) })) handle("/v1/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { api.ServeError(w, r, api.BadRequest("Unknown API endpoint. For documentation, GET /v1/api at this host and port.")) @@ -260,9 +282,9 @@ func (s *Server) Install(handle func(string, http.Handler), cacher Cacher, authV handle("/", detailHandler) if s.serveStats { handle("/detail-stats/", - stats.Stats()(http.StripPrefix("/detail-stats", s.errorHandler(s.serveDetails)))) + stats.Stats()(http.StripPrefix(s.basePath+"/detail-stats", s.errorHandler(s.serveDetails)))) handle("/search-stats/", - stats.Stats()(http.StripPrefix("/search-stats", s.errorHandler(s.serveSearch)))) + stats.Stats()(http.StripPrefix(s.basePath+"/search-stats", s.errorHandler(s.serveSearch)))) } handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") @@ -353,10 +375,38 @@ func (s *Server) installDebugHandlers(handle func(string, http.Handler)) { } // InstallFS adds path under the /files handler, serving the files in fsys. +// +// 注:fileMux 是子 mux,外层 [Server.Install] 在 /files/ 这一层已经 StripPrefix +// 掉了 [Server.basePath]+"/files",所以 fileMux 看到的请求 path 就是 raw path +// (从 path 段开始)。这里的 StripPrefix(path) 不需要带 basePath。 func (s *Server) InstallFS(path string, fsys fs.FS) { s.fileMux.Handle("GET "+path+"/", http.StripPrefix(path, http.FileServer(http.FS(fsys)))) } +// prefixPattern 把 mux pattern 里的 path 部分加 [Server.basePath] 前缀。 +// +// pattern 形式(go 1.22 ServeMux 语法): +// - "GET /static/" — 带 method +// - "/sitemap/" — 不带 method +// - "POST /play/share" — 任意 method +// +// basePath 为空时返回原 pattern(与上游零差异);非空时把 path 段前置 basePath。 +// +// 注:basePath 已在 [validateBasePath] 校验过形如 "/foo" 不带尾斜杠, +// 拼上去保证不会出现双斜杠(如 basePath="/x" + pattern="/static/" = "/x/static/")。 +func (s *Server) prefixPattern(pattern string) string { + if s.basePath == "" { + return pattern + } + // 拆 method 和 path:" " 分隔,最多一处。 + method, p, ok := strings.Cut(pattern, " ") + if !ok { + // 没空格——pattern 整段就是 path + return s.basePath + pattern + } + return method + " " + s.basePath + p +} + const ( // defaultTTL is used when details tab contents are subject to change, or when // there is a problem confirming that the details can be permanently cached. @@ -718,7 +768,7 @@ func (s *Server) findTemplate(templateName string) (*template.Template, error) { s.mu.Lock() defer s.mu.Unlock() var err error - s.templates, err = templates.ParsePageTemplates(s.templateFS) + s.templates, err = templates.ParsePageTemplates(s.templateFS, s.basePath) if err != nil { return nil, fmt.Errorf("error parsing templates: %v", err) } @@ -740,7 +790,7 @@ func executeTemplate(ctx context.Context, templateName string, tmpl *template.Te } func (s *Server) staticHandler() http.Handler { - return http.StripPrefix("/static/", http.FileServer(http.FS(s.staticFS))) + return http.StripPrefix(s.basePath+"/static/", http.FileServer(http.FS(s.staticFS))) } // serveFileFS serves a file from the given filesystem. diff --git a/internal/frontend/templates/templates.go b/internal/frontend/templates/templates.go index 035c5d36b..bb1994f9d 100644 --- a/internal/frontend/templates/templates.go +++ b/internal/frontend/templates/templates.go @@ -10,7 +10,9 @@ import ( "path" "strings" + "github.com/google/safehtml" "github.com/google/safehtml/template" + "github.com/google/safehtml/uncheckedconversions" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -39,15 +41,79 @@ func stripScheme(url string) string { return url } +// funcsWithBasePath 在内置 templateFuncs 之上叠两个 base-path 助手: +// +// - `{{abs "/static/foo.svg"}}` → 静态绝对路径前置 BasePath, +// 站点挂根时输出 `/static/foo.svg`,挂 -base-path=/pkgsitex 时输出 +// `/pkgsitex/static/foo.svg`。必须以 / 开头;否则原样(让作者改时一目了然)。 +// 返回 [safehtml.TrustedResourceURL]——safehtml/template 不会校验,允许 +// 模板里跟 `?version={{.AppVersionLabel}}` 等动态 query 拼接(普通 +// string return 会被 safehtml 拒绝,认为 ?version= 不是合法 URL prefix)。 +// - `{{basepath}}` → 返回 BasePath 字符串本身(不带尾斜杠),用于模板里 +// 拼动态 path 例如 ``——abs 不能拼带 +// 变量的 path(template 函数实参不能嵌套表达式)。返回 [safehtml.URL] +// 让 safehtml 信任这是已校验的 URL 段。 +// +// 安全说明:用 [uncheckedconversions] 绕过 safehtml 的 TrustedResourceURL +// 校验是 deliberate——abs/basepath 的入参在 fork 内是模板里的字面常量 +// (已经过 git review),不是用户输入;basePath 来自 -base-path flag 被 +// validateBasePath 校验形如 "/foo",不会注入恶意内容。 +// +// templateFuncs 是个全局只读 map,本函数 copy 一份再叠 helper, +// 避免不同 Server 实例(理论上多 BasePath 共存)相互覆盖 funcMap。 +func funcsWithBasePath(basePath string) template.FuncMap { + out := template.FuncMap{} + for k, v := range templateFuncs { + out[k] = v + } + // abs 返回普通 string——safehtml/template 会按上下文自动 escape: + // - → URL escape + // - → JS string escape + // - {{abs ...}} 在文本节点 → HTML escape + // 用 string 而非 safehtml.TrustedResourceURL 是因为后者无法在 {{end}} diff --git a/static/frontend/frontend.js b/static/frontend/frontend.js index 392707036..a37504179 100644 --- a/static/frontend/frontend.js +++ b/static/frontend/frontend.js @@ -1,21 +1,27 @@ -function P(){let n=document.querySelector(".js-header");document.querySelectorAll(".js-desktop-menu-hover").forEach(a=>{a.addEventListener("mouseenter",c=>{let l=c.target,s=document.querySelector(".forced-open");s&&s!==a&&(s.blur(),s.classList.remove("forced-open")),l.classList.remove("forced-closed"),l.classList.add("forced-open")});let u=c=>{var f,p;let l=c.target,s=l==null?void 0:l.classList.contains("forced-open"),o=c.currentTarget;s?(o.removeEventListener("blur",()=>o.classList.remove("forced-open")),o.classList.remove("forced-open"),o.classList.add("forced-closed"),o.blur(),(f=o==null?void 0:o.parentNode)==null||f.addEventListener("mouseout",()=>{o.classList.remove("forced-closed")})):(o.classList.remove("forced-closed"),o.classList.add("forced-open"),o.focus(),o.addEventListener("blur",()=>o.classList.remove("forced-open")),(p=o==null?void 0:o.parentNode)==null||p.removeEventListener("mouseout",()=>{o.classList.remove("forced-closed")})),o.focus()};a.addEventListener("click",u),a.addEventListener("focus",c=>{let l=c.target;l.classList.add("forced-closed"),l.classList.remove("forced-open")});let d=c=>{let l=c,s=c.target;if(l.key==="Escape"){let o=document.querySelector(".forced-open");o&&(o.classList.remove("forced-open"),o.blur(),o.classList.add("forced-closed"),s==null||s.focus())}};document.addEventListener("keydown",d)});let t=document.querySelectorAll(".js-headerMenuButton");t.forEach(a=>{a.addEventListener("click",u=>{u.preventDefault();let d=n==null?void 0:n.classList.contains("is-active");d?v(n):w(n),a.setAttribute("aria-expanded",d?"true":"false")})});let i=document.querySelector(".js-scrim");i==null||i.addEventListener("click",a=>{a.preventDefault(),document.querySelectorAll(".go-NavigationDrawer-submenuItem.is-active").forEach(d=>v(d)),v(n),t.forEach(d=>{d.setAttribute("aria-expanded",n!=null&&n.classList.contains("is-active")?"true":"false")})});let r=a=>{if(!a)return[];let u=Array.from(a.querySelectorAll(":scope > .go-NavigationDrawer-nav > .go-NavigationDrawer-list > .go-NavigationDrawer-listItem > a, :scope > .go-NavigationDrawer-nav > .go-NavigationDrawer-list > .go-NavigationDrawer-listItem > .go-Header-socialIcons > a")||[]),d=a.querySelector(".go-NavigationDrawer-header > a");return d&&u.unshift(d),u},h=a=>{if(a)return a.classList.contains("go-NavigationDrawer-submenuItem")},v=a=>{var c,l;if(!a)return;let u=r(a);a.classList.remove("is-active");let d=(c=a.closest(".go-NavigationDrawer-listItem"))==null?void 0:c.querySelector(":scope > a");d==null||d.focus(),u==null||u.forEach(s=>s==null?void 0:s.setAttribute("tabindex","-1")),u&&u[0]&&(u[0].removeEventListener("keydown",M(a)),u[u.length-1].removeEventListener("keydown",L(a))),a===n&&t&&((l=t[0])==null||l.focus())},w=a=>{let u=r(a);a.classList.add("is-active"),u.forEach(d=>d.setAttribute("tabindex","0")),u[0].focus(),u[0].addEventListener("keydown",M(a)),u[u.length-1].addEventListener("keydown",L(a))},M=a=>u=>{u.key==="Tab"&&u.shiftKey&&(u.preventDefault(),v(a))},L=a=>u=>{u.key==="Tab"&&!u.shiftKey&&(u.preventDefault(),v(a))},b=a=>{var c;let u=h(a),d=r(a);a.addEventListener("keyup",l=>{l.key==="Escape"&&v(a)}),d.forEach(l=>{let s=l.closest("li");if(s&&s.classList.contains("js-mobile-subnav-trigger")){let o=s.querySelector(".go-NavigationDrawer-submenuItem");l.addEventListener("click",()=>{w(o)})}}),u&&(v(a),(c=a==null?void 0:a.querySelector(".go-NavigationDrawer-header"))==null||c.addEventListener("click",l=>{l.preventDefault(),v(a)}))};document.querySelectorAll(".go-NavigationDrawer").forEach(a=>b(a)),v(n)}function U(){let n=document.querySelector(".js-searchForm"),e=document.querySelector(".js-expandSearch"),t=n==null?void 0:n.querySelector("input"),i=document.querySelector(".js-headerLogo"),r=document.querySelector(".js-headerMenuButton");e==null||e.addEventListener("click",()=>{n==null||n.classList.add("go-SearchForm--expanded"),i==null||i.classList.add("go-Header-logo--hidden"),r==null||r.classList.add("go-Header-navOpen--hidden"),t==null||t.focus()}),document==null||document.addEventListener("click",h=>{n!=null&&n.contains(h.target)||(n==null||n.classList.remove("go-SearchForm--expanded"),i==null||i.classList.remove("go-Header-logo--hidden"),r==null||r.classList.remove("go-Header-navOpen--hidden"))})}var k=class{constructor(e){this.el=e;this.setActive=e=>{this.activeIndex=(e+this.slides.length)%this.slides.length,this.el.setAttribute("data-slide-index",String(this.activeIndex));for(let t of this.dots)t.classList.remove("go-Carousel-dot--active");this.dots[this.activeIndex].classList.add("go-Carousel-dot--active");for(let t of this.slides)t.setAttribute("aria-hidden","true");this.slides[this.activeIndex].removeAttribute("aria-hidden"),this.liveRegion.textContent="Slide "+(this.activeIndex+1)+" of "+this.slides.length};var t;this.slides=Array.from(e.querySelectorAll(".go-Carousel-slide")),this.dots=[],this.liveRegion=document.createElement("div"),this.activeIndex=Number((t=e.getAttribute("data-slide-index"))!=null?t:0),this.initSlides(),this.initArrows(),this.initDots(),this.initLiveRegion()}initSlides(){for(let[e,t]of this.slides.entries())e!==this.activeIndex&&t.setAttribute("aria-hidden","true")}initArrows(){var t,i;let e=document.createElement("ul");e.classList.add("go-Carousel-arrows"),e.innerHTML=` +function $(){let t=document.querySelector(".js-header");document.querySelectorAll(".js-desktop-menu-hover").forEach(a=>{a.addEventListener("mouseenter",c=>{let l=c.target,s=document.querySelector(".forced-open");s&&s!==a&&(s.blur(),s.classList.remove("forced-open")),l.classList.remove("forced-closed"),l.classList.add("forced-open")});let u=c=>{var f,p;let l=c.target,s=l==null?void 0:l.classList.contains("forced-open"),o=c.currentTarget;s?(o.removeEventListener("blur",()=>o.classList.remove("forced-open")),o.classList.remove("forced-open"),o.classList.add("forced-closed"),o.blur(),(f=o==null?void 0:o.parentNode)==null||f.addEventListener("mouseout",()=>{o.classList.remove("forced-closed")})):(o.classList.remove("forced-closed"),o.classList.add("forced-open"),o.focus(),o.addEventListener("blur",()=>o.classList.remove("forced-open")),(p=o==null?void 0:o.parentNode)==null||p.removeEventListener("mouseout",()=>{o.classList.remove("forced-closed")})),o.focus()};a.addEventListener("click",u),a.addEventListener("focus",c=>{let l=c.target;l.classList.add("forced-closed"),l.classList.remove("forced-open")});let d=c=>{let l=c,s=c.target;if(l.key==="Escape"){let o=document.querySelector(".forced-open");o&&(o.classList.remove("forced-open"),o.blur(),o.classList.add("forced-closed"),s==null||s.focus())}};document.addEventListener("keydown",d)});let n=document.querySelectorAll(".js-headerMenuButton");n.forEach(a=>{a.addEventListener("click",u=>{u.preventDefault();let d=t==null?void 0:t.classList.contains("is-active");d?v(t):w(t),a.setAttribute("aria-expanded",d?"true":"false")})});let i=document.querySelector(".js-scrim");i==null||i.addEventListener("click",a=>{a.preventDefault(),document.querySelectorAll(".go-NavigationDrawer-submenuItem.is-active").forEach(d=>v(d)),v(t),n.forEach(d=>{d.setAttribute("aria-expanded",t!=null&&t.classList.contains("is-active")?"true":"false")})});let r=a=>{if(!a)return[];let u=Array.from(a.querySelectorAll(":scope > .go-NavigationDrawer-nav > .go-NavigationDrawer-list > .go-NavigationDrawer-listItem > a, :scope > .go-NavigationDrawer-nav > .go-NavigationDrawer-list > .go-NavigationDrawer-listItem > .go-Header-socialIcons > a")||[]),d=a.querySelector(".go-NavigationDrawer-header > a");return d&&u.unshift(d),u},h=a=>{if(a)return a.classList.contains("go-NavigationDrawer-submenuItem")},v=a=>{var c,l;if(!a)return;let u=r(a);a.classList.remove("is-active");let d=(c=a.closest(".go-NavigationDrawer-listItem"))==null?void 0:c.querySelector(":scope > a");d==null||d.focus(),u==null||u.forEach(s=>s==null?void 0:s.setAttribute("tabindex","-1")),u&&u[0]&&(u[0].removeEventListener("keydown",M(a)),u[u.length-1].removeEventListener("keydown",L(a))),a===t&&n&&((l=n[0])==null||l.focus())},w=a=>{let u=r(a);a.classList.add("is-active"),u.forEach(d=>d.setAttribute("tabindex","0")),u[0].focus(),u[0].addEventListener("keydown",M(a)),u[u.length-1].addEventListener("keydown",L(a))},M=a=>u=>{u.key==="Tab"&&u.shiftKey&&(u.preventDefault(),v(a))},L=a=>u=>{u.key==="Tab"&&!u.shiftKey&&(u.preventDefault(),v(a))},b=a=>{var c;let u=h(a),d=r(a);a.addEventListener("keyup",l=>{l.key==="Escape"&&v(a)}),d.forEach(l=>{let s=l.closest("li");if(s&&s.classList.contains("js-mobile-subnav-trigger")){let o=s.querySelector(".go-NavigationDrawer-submenuItem");l.addEventListener("click",()=>{w(o)})}}),u&&(v(a),(c=a==null?void 0:a.querySelector(".go-NavigationDrawer-header"))==null||c.addEventListener("click",l=>{l.preventDefault(),v(a)}))};document.querySelectorAll(".go-NavigationDrawer").forEach(a=>b(a)),v(t)}function W(){let t=document.querySelector(".js-searchForm"),e=document.querySelector(".js-expandSearch"),n=t==null?void 0:t.querySelector("input"),i=document.querySelector(".js-headerLogo"),r=document.querySelector(".js-headerMenuButton");e==null||e.addEventListener("click",()=>{t==null||t.classList.add("go-SearchForm--expanded"),i==null||i.classList.add("go-Header-logo--hidden"),r==null||r.classList.add("go-Header-navOpen--hidden"),n==null||n.focus()}),document==null||document.addEventListener("click",h=>{t!=null&&t.contains(h.target)||(t==null||t.classList.remove("go-SearchForm--expanded"),i==null||i.classList.remove("go-Header-logo--hidden"),r==null||r.classList.remove("go-Header-navOpen--hidden"))})}function O(){var t;return(t=document.documentElement.dataset.basePath)!=null?t:""}function J(t){return t.startsWith("/")?O()+t:t}var k=class{constructor(e){this.el=e;this.setActive=e=>{this.activeIndex=(e+this.slides.length)%this.slides.length,this.el.setAttribute("data-slide-index",String(this.activeIndex));for(let n of this.dots)n.classList.remove("go-Carousel-dot--active");this.dots[this.activeIndex].classList.add("go-Carousel-dot--active");for(let n of this.slides)n.setAttribute("aria-hidden","true");this.slides[this.activeIndex].removeAttribute("aria-hidden"),this.liveRegion.textContent="Slide "+(this.activeIndex+1)+" of "+this.slides.length};var n;this.slides=Array.from(e.querySelectorAll(".go-Carousel-slide")),this.dots=[],this.liveRegion=document.createElement("div"),this.activeIndex=Number((n=e.getAttribute("data-slide-index"))!=null?n:0),this.initSlides(),this.initArrows(),this.initDots(),this.initLiveRegion()}initSlides(){for(let[e,n]of this.slides.entries())e!==this.activeIndex&&n.setAttribute("aria-hidden","true")}initArrows(){var i,r;let e=document.createElement("ul");e.classList.add("go-Carousel-arrows");let n=O();e.innerHTML=`
  • - `,(t=e.querySelector(".go-Carousel-prevSlide"))==null||t.addEventListener("click",()=>this.setActive(this.activeIndex-1)),(i=e.querySelector(".go-Carousel-nextSlide"))==null||i.addEventListener("click",()=>this.setActive(this.activeIndex+1)),this.el.append(e)}initDots(){let e=document.createElement("ul");e.classList.add("go-Carousel-dots");for(let t=0;tSlide ${t+1}`,r.addEventListener("click",()=>this.setActive(t)),i.append(r),e.append(i),this.dots.push(r)}this.el.append(e)}initLiveRegion(){this.liveRegion.setAttribute("aria-live","polite"),this.liveRegion.setAttribute("aria-atomic","true"),this.liveRegion.setAttribute("class","go-Carousel-obscured"),this.liveRegion.textContent=`Slide ${this.activeIndex+1} of ${this.slides.length}`,this.el.appendChild(this.liveRegion)}};var A=class{constructor(e){this.el=e;var t,i,r,h,v;this.data=(t=e.dataset.toCopy)!=null?t:e.innerText,!this.data&&((i=e.parentElement)!=null&&i.classList.contains("go-InputGroup"))&&(this.data=(v=this.data||((h=(r=e.parentElement)==null?void 0:r.querySelector("input"))==null?void 0:h.value))!=null?v:""),e.addEventListener("click",w=>this.handleCopyClick(w))}handleCopyClick(e){e.preventDefault();let t=1e3;if(!navigator.clipboard){this.showTooltipText("Unable to copy",t);return}navigator.clipboard.writeText(this.data).then(()=>{this.showTooltipText("Copied!",t)}).catch(()=>{this.showTooltipText("Unable to copy",t)})}showTooltipText(e,t){this.el.setAttribute("data-tooltip",e),setTimeout(()=>this.el.setAttribute("data-tooltip",""),t)}};var x=class{constructor(e){this.el=e;document.addEventListener("click",t=>{this.el.contains(t.target)||this.el.removeAttribute("open")}),this.el.addEventListener("keydown",t=>{t.key==="Escape"&&(this.el.open=!1)})}};var C=class{constructor(e){this.el=e;this.el.addEventListener("change",t=>{let i=t.target,r=i.value;i.value.startsWith("/")||(r="/"+r),window.location.href=r})}};var q=class{constructor(e){this.el=e;window.dialogPolyfill&&window.dialogPolyfill.registerDialog(e),this.init()}init(){let e=document.querySelector(`[aria-controls="${this.el.id}"]`);e&&e.addEventListener("click",()=>{var t;this.el.showModal?this.el.showModal():this.el.setAttribute("opened","true"),(t=this.el.querySelector("input"))==null||t.focus()});for(let t of this.el.querySelectorAll("[data-modal-close]"))t.addEventListener("click",()=>{this.el.close?this.el.close():this.el.removeAttribute("opened")})}};function I(n,e,t,i){var r;(r=window.dataLayer)!=null||(window.dataLayer=[]),typeof n=="string"?window.dataLayer.push({event:n,event_category:e,event_action:t,event_label:i}):window.dataLayer.push(n)}function W(n){var e;(e=window.dataLayer)!=null||(window.dataLayer=[]),window.dataLayer.push(n)}var O=class{constructor(){this.handlers={},document.addEventListener("keydown",e=>this.handleKeyPress(e))}on(e,t,i,r){var h,v;return(v=(h=this.handlers)[e])!=null||(h[e]=new Set),this.handlers[e].add({description:t,callback:i,...r}),this}handleKeyPress(e){var t;for(let i of(t=this.handlers[e.key.toLowerCase()])!=null?t:new Set){if(i.target&&i.target!==e.target)return;let r=e.target;if(!i.target&&((r==null?void 0:r.tagName)==="INPUT"||(r==null?void 0:r.tagName)==="SELECT"||(r==null?void 0:r.tagName)==="TEXTAREA")||r!=null&&r.isContentEditable||i.withMeta&&!(e.ctrlKey||e.metaKey)||!i.withMeta&&(e.ctrlKey||e.metaKey))return;I("keypress","hotkeys",`${e.key} pressed`,i.description),i.callback(e)}}},H=new O;function $(){var l;let n=document.querySelector(".JumpDialog"),e=n==null?void 0:n.querySelector(".JumpDialog-body"),t=n==null?void 0:n.querySelector(".JumpDialog-list"),i=n==null?void 0:n.querySelector(".JumpDialog-input"),r=document.querySelector(".js-documentation"),h;function v(){let s=[];if(r){for(let o of r.querySelectorAll("[data-kind]"))s.push(w(o));for(let o of s)o.link.addEventListener("click",function(){n==null||n.close()});return s.sort(function(o,f){return o.lower.localeCompare(f.lower)}),s}}function w(s){var E;let o=document.createElement("a"),f=s.getAttribute("id");o.setAttribute("href","#"+f),o.setAttribute("tabindex","-1"),o.setAttribute("data-gtmc","jump to link");let p=s.getAttribute("data-kind");return{link:o,name:f!=null?f:"",kind:p!=null?p:"",lower:(E=f==null?void 0:f.toLowerCase())!=null?E:""}}let M,L=-1;function b(s){for(M=s,h||(h=v()),a(-1);t!=null&&t.firstChild;)t.firstChild.remove();if(s){let o=s.toLowerCase(),f=[],p=[],E=[],S=(g,y,T)=>g.name.substring(0,y)+""+g.name.substring(y,T)+""+g.name.substring(T);for(let g of h!=null?h:[]){let y=g.name.toLowerCase();if(y===o)g.link.innerHTML=S(g,0,g.name.length),f.push(g);else if(y.startsWith(o))g.link.innerHTML=S(g,0,s.length),p.push(g);else{let T=y.indexOf(o);T>-1&&(g.link.innerHTML=S(g,T,T+s.length),E.push(g))}}for(let g of f.concat(p).concat(E))t==null||t.appendChild(g.link)}else{if(!h||h.length===0){let o=document.createElement("i");o.innerHTML="There are no symbols on this page.",t==null||t.appendChild(o)}for(let o of h!=null?h:[])o.link.innerHTML=o.name+" "+o.kind+"",t==null||t.appendChild(o.link)}e&&(e.scrollTop=0),h!=null&&h.length&&t&&t.children.length>0&&a(0)}function a(s){let o=t==null?void 0:t.children;if(!(!o||!e)){if(L>=0&&o[L].classList.remove("JumpDialog-active"),s>=o.length&&(s=o.length-1),s>=0){o[s].classList.add("JumpDialog-active");let f=o[s].offsetTop-o[0].offsetTop,p=f+o[s].clientHeight;fe.scrollTop+e.clientHeight&&(e.scrollTop=p-e.clientHeight)}L=s}}function u(s){if(L<0)return;let o=L+s;o<0&&(o=0),a(o)}i==null||i.addEventListener("keyup",function(){i.value.toUpperCase()!=M.toUpperCase()&&b(i.value)}),i==null||i.addEventListener("keydown",function(s){switch(s.which){case 38:u(-1),s.preventDefault();break;case 40:u(1),s.preventDefault();break;case 13:L>=0&&t&&(t.children[L].click(),s.preventDefault());break}});let d=document.querySelector(".ShortcutsDialog");H.on("f","open jump to modal",s=>{var o;n!=null&&n.open||d!=null&&d.open||(s.preventDefault(),i&&(i.value=""),(o=n==null?void 0:n.showModal)==null||o.call(n),i==null||i.focus(),b(""))}).on("?","open shortcuts modal",()=>{var s;n!=null&&n.open||d!=null&&d.open||(s=d==null?void 0:d.showModal)==null||s.call(d)});let c=document.querySelector(".js-jumpToInput");c&&c.addEventListener("click",()=>{var s;i&&(i.value=""),b(""),!(n!=null&&n.open||d!=null&&d.open)&&((s=n==null?void 0:n.showModal)==null||s.call(n),i==null||i.focus())}),(l=document.querySelector(".js-openShortcuts"))==null||l.addEventListener("click",()=>{var s;(s=d==null?void 0:d.showModal)==null||s.call(d)})}var G=async function(){if(!["/about"].includes(window.location.pathname))return;let e="h2, h3, h4",t=".LeftNav a",i=document.querySelector(".LeftNav"),r=document.querySelector(".go-Content"),h=!1;function v(c="",l={},...s){if(!c)throw new Error("Provide `type` to create document element.");let o=Object.assign(document.createElement(c),l);return s.forEach(f=>{typeof f=="string"?o.appendChild(document.createTextNode(f)):Array.isArray(f)?f.forEach(p=>o.appendChild(p)):f instanceof HTMLElement&&o.appendChild(f)}),o}function w(){return new Promise((c,l)=>{var f,p,E,S,g,y,T,J,R,_;let s=[],o=[];if(!r||!i)return l(".SiteContent not found.");if(i instanceof HTMLElement&&!((f=i==null?void 0:i.dataset)!=null&&f.hydrate))return c(!0);for(let m of r.querySelectorAll(e))if(m instanceof HTMLElement&&!((p=m==null?void 0:m.dataset)!=null&&p.ignore))switch(m.tagName){case"H2":s=[...s,{id:m.id,label:(E=m==null?void 0:m.dataset)!=null&&E.title?m.dataset.title:(S=m.textContent)!=null?S:""}];break;case"H3":case"H4":(g=s[s.length-1])!=null&&g.subnav?s[s.length-1].subnav&&((_=s[s.length-1].subnav)==null||_.push({id:m.id,label:(J=m==null?void 0:m.dataset)!=null&&J.title?m.dataset.title:(R=m.textContent)!=null?R:""})):s[s.length-1].subnav=[{id:m.id,label:(y=m==null?void 0:m.dataset)!=null&&y.title?m.dataset.title:(T=m.textContent)!=null?T:""}];break}for(let m of s){let V=v("a",{href:"#"+m.id},v("span",{},m.label));if(o=[...o,V],m!=null&&m.subnav){let N=[];for(let K of m.subnav){let z=v("li",{},v("a",{href:"#"+K.id},v("img",{src:"/static/frontend/about/dot.svg",width:"5",height:"5"}),v("span",{},K.label)));N=[...N,z]}let X=v("ul",{className:"LeftSubnav"},N);o=[...o,X]}}return o.forEach(m=>i.appendChild(m)),c(!0)})}function M(){return new Promise(c=>{if(!document.querySelectorAll(t))return c(!0);for(let l of document.querySelectorAll(t))if(l instanceof HTMLAnchorElement&&l.href===location.href){b(l);break}c(!0)})}function L(){return new Promise(c=>{if(!document.querySelectorAll(t))return c(!0);for(let l of document.querySelectorAll(t))l.classList.remove("active");c(!0)})}function b(c){c instanceof HTMLAnchorElement&&L().then(()=>{var s,o,f;c.classList.add("active");let l=(s=c==null?void 0:c.parentNode)==null?void 0:s.parentNode;l instanceof HTMLElement&&((o=l==null?void 0:l.classList)!=null&&o.contains("LeftSubnav"))&&((f=l.previousElementSibling)==null||f.classList.add("active"))})}function a(){u();let c=document.querySelector('[href="'+location.hash+'"]');c instanceof HTMLAnchorElement&&b(c)}function u(){h=!0,setTimeout(()=>{h=!1},200)}function d(){var c;if(window.addEventListener("hashchange",a),r!=null&&r.querySelectorAll(e)){let l=o=>{if(!h&&Array.isArray(o)&&o.length>0){for(let f of o)if(f.isIntersecting&&f.target instanceof HTMLElement){let{id:p}=f.target,E=document.querySelector('[href="#'+p+'"]');E instanceof HTMLAnchorElement&&b(E);break}}},s=new IntersectionObserver(l,{threshold:0,rootMargin:"0px 0px -50% 0px"});for(let o of r.querySelectorAll(e))o instanceof HTMLElement&&!((c=o==null?void 0:o.dataset)!=null&&c.ignore)&&s.observe(o)}}try{await w(),await M(),location.hash&&u(),d()}catch(c){c instanceof Error?console.error(c.message):console.error(c)}};window.addEventListener("load",()=>{var n;for(let e of document.querySelectorAll(".js-clipboard"))new A(e);for(let e of document.querySelectorAll(".js-modal"))new q(e);for(let e of document.querySelectorAll(".js-tooltip"))new x(e);for(let e of document.querySelectorAll(".js-selectNav"))new C(e);for(let e of document.querySelectorAll(".js-carousel"))new k(e);for(let e of document.querySelectorAll(".js-toggleTheme"))e.addEventListener("click",()=>{Y()});(n=document.querySelector(".js-gtmID"))!=null&&n.dataset.gtmid&&window.dataLayer?W(function(){B()}):B(),P(),U(),$(),G(),Z()});H.on("/","focus search",n=>{let e=Array.from(document.querySelectorAll(".js-searchFocus")).pop();e&&!window.navigator.userAgent.includes("Firefox")&&(n.preventDefault(),e.focus())});H.on("y","set canonical url",()=>{var e;let n=(e=document.querySelector(".js-canonicalURLPath"))==null?void 0:e.dataset.canonicalUrlPath;if(n&&n!==""){let t=window.location.hash;t&&(n+=t),window.history.replaceState(null,"",n)}});(function(){I({"gtm.start":new Date().getTime(),event:"gtm.js"})})();function B(){let n=new URLSearchParams(window.location.search),e=n.get("utm_source");if(e!=="gopls"&&e!=="godoc"&&e!=="pkggodev")return;let t=new URL(window.location.href);n.delete("utm_source"),t.search=n.toString(),window.history.replaceState(null,"",t.toString())}function Y(){let n="dark",e=document.documentElement.getAttribute("data-theme");e==="dark"?n="light":e==="light"&&(n="auto");let t="";(location.hostname==="go.dev"||location.hostname.endsWith(".go.dev"))&&(t="domain=.go.dev;"),document.documentElement.setAttribute("data-theme",n),document.cookie=`prefers-color-scheme=${n};${t}path=/;max-age=31536000;`}function Z(){if(!document.cookie.match(/cookie-consent=true/)){let e=document.querySelector(".js-cookieNotice"),t=e==null?void 0:e.querySelector("button");e==null||e.classList.add("Cookie-notice--visible"),t==null||t.addEventListener("click",()=>{let i="";(location.hostname==="go.dev"||location.hostname.endsWith(".go.dev"))&&(i="domain=.go.dev;"),document.cookie=`cookie-consent=true;${i}path=/;max-age=31536000`,e==null||e.remove()})}} + `,(i=e.querySelector(".go-Carousel-prevSlide"))==null||i.addEventListener("click",()=>this.setActive(this.activeIndex-1)),(r=e.querySelector(".go-Carousel-nextSlide"))==null||r.addEventListener("click",()=>this.setActive(this.activeIndex+1)),this.el.append(e)}initDots(){let e=document.createElement("ul");e.classList.add("go-Carousel-dots");for(let n=0;nSlide ${n+1}`,r.addEventListener("click",()=>this.setActive(n)),i.append(r),e.append(i),this.dots.push(r)}this.el.append(e)}initLiveRegion(){this.liveRegion.setAttribute("aria-live","polite"),this.liveRegion.setAttribute("aria-atomic","true"),this.liveRegion.setAttribute("class","go-Carousel-obscured"),this.liveRegion.textContent=`Slide ${this.activeIndex+1} of ${this.slides.length}`,this.el.appendChild(this.liveRegion)}};var A=class{constructor(e){this.el=e;var n,i,r,h,v;this.data=(n=e.dataset.toCopy)!=null?n:e.innerText,!this.data&&((i=e.parentElement)!=null&&i.classList.contains("go-InputGroup"))&&(this.data=(v=this.data||((h=(r=e.parentElement)==null?void 0:r.querySelector("input"))==null?void 0:h.value))!=null?v:""),e.addEventListener("click",w=>this.handleCopyClick(w))}handleCopyClick(e){e.preventDefault();let n=1e3;if(!navigator.clipboard){this.showTooltipText("Unable to copy",n);return}navigator.clipboard.writeText(this.data).then(()=>{this.showTooltipText("Copied!",n)}).catch(()=>{this.showTooltipText("Unable to copy",n)})}showTooltipText(e,n){this.el.setAttribute("data-tooltip",e),setTimeout(()=>this.el.setAttribute("data-tooltip",""),n)}};var x=class{constructor(e){this.el=e;document.addEventListener("click",n=>{this.el.contains(n.target)||this.el.removeAttribute("open")}),this.el.addEventListener("keydown",n=>{n.key==="Escape"&&(this.el.open=!1)})}};var C=class{constructor(e){this.el=e;this.el.addEventListener("change",n=>{let i=n.target,r=i.value;i.value.startsWith("/")||(r="/"+r),window.location.href=r})}};var q=class{constructor(e){this.el=e;window.dialogPolyfill&&window.dialogPolyfill.registerDialog(e),this.init()}init(){let e=document.querySelector(`[aria-controls="${this.el.id}"]`);e&&e.addEventListener("click",()=>{var n;this.el.showModal?this.el.showModal():this.el.setAttribute("opened","true"),(n=this.el.querySelector("input"))==null||n.focus()});for(let n of this.el.querySelectorAll("[data-modal-close]"))n.addEventListener("click",()=>{this.el.close?this.el.close():this.el.removeAttribute("opened")})}};function I(t,e,n,i){var r;(r=window.dataLayer)!=null||(window.dataLayer=[]),typeof t=="string"?window.dataLayer.push({event:t,event_category:e,event_action:n,event_label:i}):window.dataLayer.push(t)}function B(t){var e;(e=window.dataLayer)!=null||(window.dataLayer=[]),window.dataLayer.push(t)}var R=class{constructor(){this.handlers={},document.addEventListener("keydown",e=>this.handleKeyPress(e))}on(e,n,i,r){var h,v;return(v=(h=this.handlers)[e])!=null||(h[e]=new Set),this.handlers[e].add({description:n,callback:i,...r}),this}handleKeyPress(e){var n;for(let i of(n=this.handlers[e.key.toLowerCase()])!=null?n:new Set){if(i.target&&i.target!==e.target)return;let r=e.target;if(!i.target&&((r==null?void 0:r.tagName)==="INPUT"||(r==null?void 0:r.tagName)==="SELECT"||(r==null?void 0:r.tagName)==="TEXTAREA")||r!=null&&r.isContentEditable||i.withMeta&&!(e.ctrlKey||e.metaKey)||!i.withMeta&&(e.ctrlKey||e.metaKey))return;I("keypress","hotkeys",`${e.key} pressed`,i.description),i.callback(e)}}},H=new R;function G(){var l;let t=document.querySelector(".JumpDialog"),e=t==null?void 0:t.querySelector(".JumpDialog-body"),n=t==null?void 0:t.querySelector(".JumpDialog-list"),i=t==null?void 0:t.querySelector(".JumpDialog-input"),r=document.querySelector(".js-documentation"),h;function v(){let s=[];if(r){for(let o of r.querySelectorAll("[data-kind]"))s.push(w(o));for(let o of s)o.link.addEventListener("click",function(){t==null||t.close()});return s.sort(function(o,f){return o.lower.localeCompare(f.lower)}),s}}function w(s){var E;let o=document.createElement("a"),f=s.getAttribute("id");o.setAttribute("href","#"+f),o.setAttribute("tabindex","-1"),o.setAttribute("data-gtmc","jump to link");let p=s.getAttribute("data-kind");return{link:o,name:f!=null?f:"",kind:p!=null?p:"",lower:(E=f==null?void 0:f.toLowerCase())!=null?E:""}}let M,L=-1;function b(s){for(M=s,h||(h=v()),a(-1);n!=null&&n.firstChild;)n.firstChild.remove();if(s){let o=s.toLowerCase(),f=[],p=[],E=[],S=(g,y,T)=>g.name.substring(0,y)+""+g.name.substring(y,T)+""+g.name.substring(T);for(let g of h!=null?h:[]){let y=g.name.toLowerCase();if(y===o)g.link.innerHTML=S(g,0,g.name.length),f.push(g);else if(y.startsWith(o))g.link.innerHTML=S(g,0,s.length),p.push(g);else{let T=y.indexOf(o);T>-1&&(g.link.innerHTML=S(g,T,T+s.length),E.push(g))}}for(let g of f.concat(p).concat(E))n==null||n.appendChild(g.link)}else{if(!h||h.length===0){let o=document.createElement("i");o.innerHTML="There are no symbols on this page.",n==null||n.appendChild(o)}for(let o of h!=null?h:[])o.link.innerHTML=o.name+" "+o.kind+"",n==null||n.appendChild(o.link)}e&&(e.scrollTop=0),h!=null&&h.length&&n&&n.children.length>0&&a(0)}function a(s){let o=n==null?void 0:n.children;if(!(!o||!e)){if(L>=0&&o[L].classList.remove("JumpDialog-active"),s>=o.length&&(s=o.length-1),s>=0){o[s].classList.add("JumpDialog-active");let f=o[s].offsetTop-o[0].offsetTop,p=f+o[s].clientHeight;fe.scrollTop+e.clientHeight&&(e.scrollTop=p-e.clientHeight)}L=s}}function u(s){if(L<0)return;let o=L+s;o<0&&(o=0),a(o)}i==null||i.addEventListener("keyup",function(){i.value.toUpperCase()!=M.toUpperCase()&&b(i.value)}),i==null||i.addEventListener("keydown",function(s){switch(s.which){case 38:u(-1),s.preventDefault();break;case 40:u(1),s.preventDefault();break;case 13:L>=0&&n&&(n.children[L].click(),s.preventDefault());break}});let d=document.querySelector(".ShortcutsDialog");H.on("f","open jump to modal",s=>{var o;t!=null&&t.open||d!=null&&d.open||(s.preventDefault(),i&&(i.value=""),(o=t==null?void 0:t.showModal)==null||o.call(t),i==null||i.focus(),b(""))}).on("?","open shortcuts modal",()=>{var s;t!=null&&t.open||d!=null&&d.open||(s=d==null?void 0:d.showModal)==null||s.call(d)});let c=document.querySelector(".js-jumpToInput");c&&c.addEventListener("click",()=>{var s;i&&(i.value=""),b(""),!(t!=null&&t.open||d!=null&&d.open)&&((s=t==null?void 0:t.showModal)==null||s.call(t),i==null||i.focus())}),(l=document.querySelector(".js-openShortcuts"))==null||l.addEventListener("click",()=>{var s;(s=d==null?void 0:d.showModal)==null||s.call(d)})}var V=async function(){if(![J("/about")].includes(window.location.pathname))return;let e="h2, h3, h4",n=".LeftNav a",i=document.querySelector(".LeftNav"),r=document.querySelector(".go-Content"),h=!1;function v(c="",l={},...s){if(!c)throw new Error("Provide `type` to create document element.");let o=Object.assign(document.createElement(c),l);return s.forEach(f=>{typeof f=="string"?o.appendChild(document.createTextNode(f)):Array.isArray(f)?f.forEach(p=>o.appendChild(p)):f instanceof HTMLElement&&o.appendChild(f)}),o}function w(){return new Promise((c,l)=>{var f,p,E,S,g,y,T,P,_,K;let s=[],o=[];if(!r||!i)return l(".SiteContent not found.");if(i instanceof HTMLElement&&!((f=i==null?void 0:i.dataset)!=null&&f.hydrate))return c(!0);for(let m of r.querySelectorAll(e))if(m instanceof HTMLElement&&!((p=m==null?void 0:m.dataset)!=null&&p.ignore))switch(m.tagName){case"H2":s=[...s,{id:m.id,label:(E=m==null?void 0:m.dataset)!=null&&E.title?m.dataset.title:(S=m.textContent)!=null?S:""}];break;case"H3":case"H4":(g=s[s.length-1])!=null&&g.subnav?s[s.length-1].subnav&&((K=s[s.length-1].subnav)==null||K.push({id:m.id,label:(P=m==null?void 0:m.dataset)!=null&&P.title?m.dataset.title:(_=m.textContent)!=null?_:""})):s[s.length-1].subnav=[{id:m.id,label:(y=m==null?void 0:m.dataset)!=null&&y.title?m.dataset.title:(T=m.textContent)!=null?T:""}];break}for(let m of s){let z=v("a",{href:"#"+m.id},v("span",{},m.label));if(o=[...o,z],m!=null&&m.subnav){let N=[];for(let U of m.subnav){let Y=v("li",{},v("a",{href:"#"+U.id},v("img",{src:J("/static/frontend/about/dot.svg"),width:"5",height:"5"}),v("span",{},U.label)));N=[...N,Y]}let Q=v("ul",{className:"LeftSubnav"},N);o=[...o,Q]}}return o.forEach(m=>i.appendChild(m)),c(!0)})}function M(){return new Promise(c=>{if(!document.querySelectorAll(n))return c(!0);for(let l of document.querySelectorAll(n))if(l instanceof HTMLAnchorElement&&l.href===location.href){b(l);break}c(!0)})}function L(){return new Promise(c=>{if(!document.querySelectorAll(n))return c(!0);for(let l of document.querySelectorAll(n))l.classList.remove("active");c(!0)})}function b(c){c instanceof HTMLAnchorElement&&L().then(()=>{var s,o,f;c.classList.add("active");let l=(s=c==null?void 0:c.parentNode)==null?void 0:s.parentNode;l instanceof HTMLElement&&((o=l==null?void 0:l.classList)!=null&&o.contains("LeftSubnav"))&&((f=l.previousElementSibling)==null||f.classList.add("active"))})}function a(){u();let c=document.querySelector('[href="'+location.hash+'"]');c instanceof HTMLAnchorElement&&b(c)}function u(){h=!0,setTimeout(()=>{h=!1},200)}function d(){var c;if(window.addEventListener("hashchange",a),r!=null&&r.querySelectorAll(e)){let l=o=>{if(!h&&Array.isArray(o)&&o.length>0){for(let f of o)if(f.isIntersecting&&f.target instanceof HTMLElement){let{id:p}=f.target,E=document.querySelector('[href="#'+p+'"]');E instanceof HTMLAnchorElement&&b(E);break}}},s=new IntersectionObserver(l,{threshold:0,rootMargin:"0px 0px -50% 0px"});for(let o of r.querySelectorAll(e))o instanceof HTMLElement&&!((c=o==null?void 0:o.dataset)!=null&&c.ignore)&&s.observe(o)}}try{await w(),await M(),location.hash&&u(),d()}catch(c){c instanceof Error?console.error(c.message):console.error(c)}};window.addEventListener("load",()=>{var t;for(let e of document.querySelectorAll(".js-clipboard"))new A(e);for(let e of document.querySelectorAll(".js-modal"))new q(e);for(let e of document.querySelectorAll(".js-tooltip"))new x(e);for(let e of document.querySelectorAll(".js-selectNav"))new C(e);for(let e of document.querySelectorAll(".js-carousel"))new k(e);for(let e of document.querySelectorAll(".js-toggleTheme"))e.addEventListener("click",()=>{D()});(t=document.querySelector(".js-gtmID"))!=null&&t.dataset.gtmid&&window.dataLayer?B(function(){X()}):X(),$(),W(),G(),V(),F()});H.on("/","focus search",t=>{let e=Array.from(document.querySelectorAll(".js-searchFocus")).pop();e&&!window.navigator.userAgent.includes("Firefox")&&(t.preventDefault(),e.focus())});H.on("y","set canonical url",()=>{var e;let t=(e=document.querySelector(".js-canonicalURLPath"))==null?void 0:e.dataset.canonicalUrlPath;if(t&&t!==""){let n=window.location.hash;n&&(t+=n),window.history.replaceState(null,"",t)}});(function(){I({"gtm.start":new Date().getTime(),event:"gtm.js"})})();function X(){let t=new URLSearchParams(window.location.search),e=t.get("utm_source");if(e!=="gopls"&&e!=="godoc"&&e!=="pkggodev")return;let n=new URL(window.location.href);t.delete("utm_source"),n.search=t.toString(),window.history.replaceState(null,"",n.toString())}function D(){let t="dark",e=document.documentElement.getAttribute("data-theme");e==="dark"?t="light":e==="light"&&(t="auto");let n="";(location.hostname==="go.dev"||location.hostname.endsWith(".go.dev"))&&(n="domain=.go.dev;"),document.documentElement.setAttribute("data-theme",t),document.cookie=`prefers-color-scheme=${t};${n}path=/;max-age=31536000;`}function F(){if(!document.cookie.match(/cookie-consent=true/)){let e=document.querySelector(".js-cookieNotice"),n=e==null?void 0:e.querySelector("button");e==null||e.classList.add("Cookie-notice--visible"),n==null||n.addEventListener("click",()=>{let i="";(location.hostname==="go.dev"||location.hostname.endsWith(".go.dev"))&&(i="domain=.go.dev;"),document.cookie=`cookie-consent=true;${i}path=/;max-age=31536000`,e==null||e.remove()})}} /** * @license * Copyright 2021 The Go Authors. All rights reserved. * Use of this source code is governed by a BSD-style * license that can be found in the LICENSE file. */ +/** + * @license + * Copyright 2024 The Go Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ /*! * @license * Copyright 2019-2020 The Go Authors. All rights reserved. diff --git a/static/frontend/frontend.js.map b/static/frontend/frontend.js.map index aad914663..0b262a0fd 100644 --- a/static/frontend/frontend.js.map +++ b/static/frontend/frontend.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../shared/header/header.ts", "../shared/carousel/carousel.ts", "../shared/clipboard/clipboard.ts", "../shared/tooltip/tooltip.ts", "../shared/outline/select.ts", "../shared/modal/modal.ts", "../shared/analytics/analytics.ts", "../shared/keyboard/keyboard.ts", "../shared/jump/jump.ts", "about/index.ts", "frontend.ts"], - "sourcesContent": ["/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nexport function registerHeaderListeners(): void {\n const header = document.querySelector('.js-header') as HTMLElement;\n\n // Desktop menu hover state\n const menuItemHovers = document.querySelectorAll('.js-desktop-menu-hover');\n menuItemHovers.forEach(menuItemHover => {\n // when user clicks on the dropdown menu item on desktop or mobile,\n // force the menu to stay open until the user clicks off of it.\n menuItemHover.addEventListener('mouseenter', e => {\n const target = e.target as HTMLElement;\n const forced = document.querySelector('.forced-open') as HTMLElement;\n if (forced && forced !== menuItemHover) {\n forced.blur();\n forced.classList.remove('forced-open');\n }\n // prevents menus that have been tabbed into from staying open\n // when you hover over another menu\n target.classList.remove('forced-closed');\n target.classList.add('forced-open');\n });\n\n const toggleForcedOpen = (e: Event) => {\n const target = e.target as HTMLElement;\n const isForced = target?.classList.contains('forced-open');\n const currentTarget = e.currentTarget as HTMLElement;\n if (isForced) {\n currentTarget.removeEventListener('blur', () =>\n currentTarget.classList.remove('forced-open')\n );\n currentTarget.classList.remove('forced-open');\n currentTarget.classList.add('forced-closed');\n currentTarget.blur();\n currentTarget?.parentNode?.addEventListener('mouseout', () => {\n currentTarget.classList.remove('forced-closed');\n });\n } else {\n currentTarget.classList.remove('forced-closed');\n currentTarget.classList.add('forced-open');\n currentTarget.focus();\n currentTarget.addEventListener('blur', () => currentTarget.classList.remove('forced-open'));\n currentTarget?.parentNode?.removeEventListener('mouseout', () => {\n currentTarget.classList.remove('forced-closed');\n });\n }\n currentTarget.focus();\n };\n menuItemHover.addEventListener('click', toggleForcedOpen);\n menuItemHover.addEventListener('focus', e => {\n const target = e.target as HTMLElement;\n target.classList.add('forced-closed');\n target.classList.remove('forced-open');\n });\n\n // ensure desktop submenus are closed when esc is pressed\n const closeSubmenuOnEsc = (e: Event) => {\n const event = e as KeyboardEvent;\n const target = e.target as HTMLElement;\n if (event.key === 'Escape') {\n const forcedOpenItem = document.querySelector('.forced-open') as HTMLElement;\n if (forcedOpenItem) {\n forcedOpenItem.classList.remove('forced-open');\n forcedOpenItem.blur();\n forcedOpenItem.classList.add('forced-closed');\n target?.focus();\n }\n }\n };\n document.addEventListener('keydown', closeSubmenuOnEsc);\n });\n\n // Mobile menu subnav menus\n const headerbuttons = document.querySelectorAll('.js-headerMenuButton');\n headerbuttons.forEach(button => {\n button.addEventListener('click', e => {\n e.preventDefault();\n const isActive = header?.classList.contains('is-active');\n if (isActive) {\n handleNavigationDrawerInactive(header);\n } else {\n handleNavigationDrawerActive(header);\n }\n button.setAttribute('aria-expanded', isActive ? 'true' : 'false');\n });\n });\n\n const scrim = document.querySelector('.js-scrim');\n scrim?.addEventListener('click', e => {\n e.preventDefault();\n\n // find any active submenus and close them\n const activeSubnavs = document.querySelectorAll('.go-NavigationDrawer-submenuItem.is-active');\n activeSubnavs.forEach(subnav => handleNavigationDrawerInactive(subnav as HTMLElement));\n\n handleNavigationDrawerInactive(header);\n\n headerbuttons.forEach(button => {\n button.setAttribute(\n 'aria-expanded',\n header?.classList.contains('is-active') ? 'true' : 'false'\n );\n });\n });\n\n const getNavigationDrawerMenuItems = (navigationDrawer: HTMLElement): HTMLElement[] => {\n if (!navigationDrawer) {\n return [];\n }\n\n const menuItems = Array.from(\n navigationDrawer.querySelectorAll(\n ':scope > .go-NavigationDrawer-nav > .go-NavigationDrawer-list > .go-NavigationDrawer-listItem > a, :scope > .go-NavigationDrawer-nav > .go-NavigationDrawer-list > .go-NavigationDrawer-listItem > .go-Header-socialIcons > a'\n ) || []\n );\n\n const anchorEl = navigationDrawer.querySelector('.go-NavigationDrawer-header > a');\n if (anchorEl) {\n menuItems.unshift(anchorEl);\n }\n return menuItems as HTMLElement[];\n };\n\n const getNavigationDrawerIsSubnav = (navigationDrawer: HTMLElement) => {\n if (!navigationDrawer) {\n return;\n }\n return navigationDrawer.classList.contains('go-NavigationDrawer-submenuItem');\n };\n\n const handleNavigationDrawerInactive = (navigationDrawer: HTMLElement) => {\n if (!navigationDrawer) {\n return;\n }\n const menuItems = getNavigationDrawerMenuItems(navigationDrawer);\n navigationDrawer.classList.remove('is-active');\n const parentMenuItem = navigationDrawer\n .closest('.go-NavigationDrawer-listItem')\n ?.querySelector(':scope > a') as HTMLElement;\n parentMenuItem?.focus();\n menuItems?.forEach(item => item?.setAttribute('tabindex', '-1'));\n if (menuItems && menuItems[0]) {\n menuItems[0].removeEventListener('keydown', handleMenuItemTabLeftFactory(navigationDrawer));\n menuItems[menuItems.length - 1].removeEventListener(\n 'keydown',\n handleMenuItemTabRightFactory(navigationDrawer)\n );\n }\n\n if (navigationDrawer === header) {\n headerbuttons && (headerbuttons[0] as HTMLElement)?.focus();\n }\n };\n\n const handleNavigationDrawerActive = (navigationDrawer: HTMLElement) => {\n const menuItems = getNavigationDrawerMenuItems(navigationDrawer);\n\n navigationDrawer.classList.add('is-active');\n menuItems.forEach(item => item.setAttribute('tabindex', '0'));\n menuItems[0].focus();\n\n menuItems[0].addEventListener('keydown', handleMenuItemTabLeftFactory(navigationDrawer));\n menuItems[menuItems.length - 1].addEventListener(\n 'keydown',\n handleMenuItemTabRightFactory(navigationDrawer)\n );\n };\n\n const handleMenuItemTabLeftFactory = (navigationDrawer: HTMLElement) => {\n return (e: KeyboardEvent) => {\n if (e.key === 'Tab' && e.shiftKey) {\n e.preventDefault();\n handleNavigationDrawerInactive(navigationDrawer);\n }\n };\n };\n\n const handleMenuItemTabRightFactory = (navigationDrawer: HTMLElement) => {\n return (e: KeyboardEvent) => {\n if (e.key === 'Tab' && !e.shiftKey) {\n e.preventDefault();\n handleNavigationDrawerInactive(navigationDrawer);\n }\n };\n };\n\n const prepMobileNavigationDrawer = (navigationDrawer: HTMLElement) => {\n const isSubnav = getNavigationDrawerIsSubnav(navigationDrawer);\n const menuItems = getNavigationDrawerMenuItems(navigationDrawer);\n navigationDrawer.addEventListener('keyup', e => {\n if (e.key === 'Escape') {\n handleNavigationDrawerInactive(navigationDrawer);\n }\n });\n\n menuItems.forEach(item => {\n const parentLi = item.closest('li');\n if (parentLi && parentLi.classList.contains('js-mobile-subnav-trigger')) {\n const submenu = parentLi.querySelector('.go-NavigationDrawer-submenuItem') as HTMLElement;\n item.addEventListener('click', () => {\n handleNavigationDrawerActive(submenu);\n });\n }\n });\n if (isSubnav) {\n handleNavigationDrawerInactive(navigationDrawer);\n navigationDrawer\n ?.querySelector('.go-NavigationDrawer-header')\n ?.addEventListener('click', e => {\n e.preventDefault();\n handleNavigationDrawerInactive(navigationDrawer);\n });\n }\n };\n\n document\n .querySelectorAll('.go-NavigationDrawer')\n .forEach(drawer => prepMobileNavigationDrawer(drawer as HTMLElement));\n\n handleNavigationDrawerInactive(header);\n}\n\nexport function registerSearchFormListeners(): void {\n const searchForm = document.querySelector('.js-searchForm');\n const expandSearch = document.querySelector('.js-expandSearch');\n const input = searchForm?.querySelector('input');\n const headerLogo = document.querySelector('.js-headerLogo');\n const menuButton = document.querySelector('.js-headerMenuButton');\n expandSearch?.addEventListener('click', () => {\n searchForm?.classList.add('go-SearchForm--expanded');\n headerLogo?.classList.add('go-Header-logo--hidden');\n menuButton?.classList.add('go-Header-navOpen--hidden');\n input?.focus();\n });\n document?.addEventListener('click', e => {\n if (!searchForm?.contains(e.target as Node)) {\n searchForm?.classList.remove('go-SearchForm--expanded');\n headerLogo?.classList.remove('go-Header-logo--hidden');\n menuButton?.classList.remove('go-Header-navOpen--hidden');\n }\n });\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * Carousel Controller adds event listeners, accessibility enhancements, and\n * control elements to a carousel component.\n */\nexport class CarouselController {\n /**\n * slides is a collection of slides in the carousel.\n */\n private slides: HTMLLIElement[];\n /**\n * dots is a collection of dot navigation controls, added to the carousel\n * by this controller.\n */\n private dots: HTMLElement[];\n /**\n * liveRegion is a visually hidden element that notifies assitive devices\n * of visual changes to the carousel. They are added to the carousel by\n * this controller.\n */\n private liveRegion: HTMLElement;\n /**\n * activeIndex is the 0-index of the currently active slide.\n */\n private activeIndex: number;\n\n constructor(private el: HTMLElement) {\n this.slides = Array.from(el.querySelectorAll('.go-Carousel-slide'));\n this.dots = [];\n this.liveRegion = document.createElement('div');\n this.activeIndex = Number(el.getAttribute('data-slide-index') ?? 0);\n\n this.initSlides();\n this.initArrows();\n this.initDots();\n this.initLiveRegion();\n }\n\n private initSlides() {\n for (const [i, v] of this.slides.entries()) {\n if (i === this.activeIndex) continue;\n v.setAttribute('aria-hidden', 'true');\n }\n }\n\n private initArrows() {\n const arrows = document.createElement('ul');\n arrows.classList.add('go-Carousel-arrows');\n arrows.innerHTML = `\n
  • \n \n
  • \n
  • \n \n
  • \n `;\n arrows\n .querySelector('.go-Carousel-prevSlide')\n ?.addEventListener('click', () => this.setActive(this.activeIndex - 1));\n arrows\n .querySelector('.go-Carousel-nextSlide')\n ?.addEventListener('click', () => this.setActive(this.activeIndex + 1));\n this.el.append(arrows);\n }\n\n private initDots() {\n const dots = document.createElement('ul');\n dots.classList.add('go-Carousel-dots');\n for (let i = 0; i < this.slides.length; i++) {\n const li = document.createElement('li');\n const button = document.createElement('button');\n button.classList.add('go-Carousel-dot');\n if (i === this.activeIndex) {\n button.classList.add('go-Carousel-dot--active');\n }\n button.innerHTML = `Slide ${i + 1}`;\n button.addEventListener('click', () => this.setActive(i));\n li.append(button);\n dots.append(li);\n this.dots.push(button);\n }\n this.el.append(dots);\n }\n\n private initLiveRegion() {\n this.liveRegion.setAttribute('aria-live', 'polite');\n this.liveRegion.setAttribute('aria-atomic', 'true');\n this.liveRegion.setAttribute('class', 'go-Carousel-obscured');\n this.liveRegion.textContent = `Slide ${this.activeIndex + 1} of ${this.slides.length}`;\n this.el.appendChild(this.liveRegion);\n }\n\n private setActive = (index: number) => {\n this.activeIndex = (index + this.slides.length) % this.slides.length;\n this.el.setAttribute('data-slide-index', String(this.activeIndex));\n for (const d of this.dots) {\n d.classList.remove('go-Carousel-dot--active');\n }\n this.dots[this.activeIndex].classList.add('go-Carousel-dot--active');\n for (const s of this.slides) {\n s.setAttribute('aria-hidden', 'true');\n }\n this.slides[this.activeIndex].removeAttribute('aria-hidden');\n this.liveRegion.textContent = 'Slide ' + (this.activeIndex + 1) + ' of ' + this.slides.length;\n };\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * This class decorates an element to copy arbitrary data attached via a data-\n * attribute to the clipboard.\n */\nexport class ClipboardController {\n /**\n * The data to be copied to the clipboard.\n */\n private data: string;\n\n /**\n * @param el The element that will trigger copying text to the clipboard. The text is\n * expected to be within its data-to-copy attribute.\n */\n constructor(private el: HTMLButtonElement) {\n this.data = el.dataset['toCopy'] ?? el.innerText;\n // if data-to-copy is empty and the button is part of an input group\n // capture the value of the input.\n if (!this.data && el.parentElement?.classList.contains('go-InputGroup')) {\n this.data = (this.data || el.parentElement?.querySelector('input')?.value) ?? '';\n }\n el.addEventListener('click', e => this.handleCopyClick(e));\n }\n\n /**\n * Handles when the primary element is clicked.\n */\n handleCopyClick(e: MouseEvent): void {\n e.preventDefault();\n const TOOLTIP_SHOW_DURATION_MS = 1000;\n\n // This API is not available on iOS.\n if (!navigator.clipboard) {\n this.showTooltipText('Unable to copy', TOOLTIP_SHOW_DURATION_MS);\n return;\n }\n navigator.clipboard\n .writeText(this.data)\n .then(() => {\n this.showTooltipText('Copied!', TOOLTIP_SHOW_DURATION_MS);\n })\n .catch(() => {\n this.showTooltipText('Unable to copy', TOOLTIP_SHOW_DURATION_MS);\n });\n }\n\n /**\n * Shows the given text in a tooltip for a specified amount of time, in milliseconds.\n */\n showTooltipText(text: string, durationMs: number): void {\n this.el.setAttribute('data-tooltip', text);\n setTimeout(() => this.el.setAttribute('data-tooltip', ''), durationMs);\n }\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * ToolTipController handles closing tooltips on external clicks.\n */\nexport class ToolTipController {\n constructor(private el: HTMLDetailsElement) {\n document.addEventListener('click', e => {\n const insideTooltip = this.el.contains(e.target as Element);\n if (!insideTooltip) {\n this.el.removeAttribute('open');\n }\n });\n\n // Add event listener for \"Escape\" keydown to close tooltip\n this.el.addEventListener('keydown', e => {\n if (e.key === 'Escape') {\n this.el.open = false;\n }\n });\n }\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { TreeNavController } from './tree.js';\n\nexport class SelectNavController {\n constructor(private el: Element) {\n this.el.addEventListener('change', e => {\n const target = e.target as HTMLSelectElement;\n let href = target.value;\n if (!target.value.startsWith('/')) {\n href = '/' + href;\n }\n window.location.href = href;\n });\n }\n}\n\nexport function makeSelectNav(tree: TreeNavController): HTMLLabelElement {\n const label = document.createElement('label');\n label.classList.add('go-Label');\n label.setAttribute('aria-label', 'Menu');\n const select = document.createElement('select');\n select.classList.add('go-Select', 'js-selectNav');\n label.appendChild(select);\n const outline = document.createElement('optgroup');\n outline.label = 'Outline';\n select.appendChild(outline);\n const groupMap: Record = {};\n let group: HTMLOptGroupElement;\n for (const t of tree.treeitems) {\n if (Number(t.depth) > 4) continue;\n if (t.groupTreeitem) {\n group = groupMap[t.groupTreeitem.label];\n if (!group) {\n group = groupMap[t.groupTreeitem.label] = document.createElement('optgroup');\n group.label = t.groupTreeitem.label;\n select.appendChild(group);\n }\n } else {\n group = outline;\n }\n const o = document.createElement('option');\n o.label = t.label;\n o.textContent = t.label;\n o.value = (t.el as HTMLAnchorElement).href.replace(window.location.origin, '').replace('/', '');\n group.appendChild(o);\n }\n tree.addObserver(t => {\n const hash = (t.el as HTMLAnchorElement).hash;\n const value = select.querySelector(`[value$=\"${hash}\"]`)?.value;\n if (value) {\n select.value = value;\n }\n }, 50);\n return label;\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\ninterface Window {\n dialogPolyfill?: {\n registerDialog: (el: HTMLDialogElement) => void;\n };\n}\n\ndeclare const window: Window;\n\n/**\n * ModalController registers a dialog element with the polyfill if\n * necessary for the current browser, add adds event listeners to\n * close and open modals.\n */\nexport class ModalController {\n constructor(private el: HTMLDialogElement) {\n if (window.dialogPolyfill) {\n window.dialogPolyfill.registerDialog(el);\n }\n this.init();\n }\n\n init() {\n const button = document.querySelector(`[aria-controls=\"${this.el.id}\"]`);\n if (button) {\n button.addEventListener('click', () => {\n if (this.el.showModal) {\n this.el.showModal();\n } else {\n this.el.setAttribute('opened', 'true');\n }\n this.el.querySelector('input')?.focus();\n });\n }\n for (const btn of this.el.querySelectorAll('[data-modal-close]')) {\n btn.addEventListener('click', () => {\n if (this.el.close) {\n this.el.close();\n } else {\n this.el.removeAttribute('opened');\n }\n });\n }\n }\n}\n", "interface TagManagerEvent {\n /**\n * event is the name of the event, used to filter events in\n * Google Analytics.\n */\n event: string;\n\n /**\n * event_category is a name that you supply as a way to group objects\n * that to analyze. Typically, you will use the same category name\n * multiple times over related UI elements (buttons, links, etc).\n */\n event_category?: string;\n\n /**\n * event_action is used to name the type of event or interaction you\n * want to measure for a particular web object. For example, with a\n * single \"form\" category, you can analyze a number of specific events\n * with this parameter, such as: form entered, form submitted.\n */\n event_action?: string;\n\n /**\n * event_label provide additional information for events that you want\n * to analyze, such as the text label of a link.\n */\n event_label?: string;\n\n /**\n * gtm.start is used to initialize Google Tag Manager.\n */\n 'gtm.start'?: number;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\ndeclare global {\n interface Window {\n dataLayer?: (TagManagerEvent | VoidFunction)[];\n ga?: unknown;\n }\n}\n\n/**\n * track sends events to Google Tag Manager.\n */\nexport function track(\n event: string | TagManagerEvent,\n category?: string,\n action?: string,\n label?: string\n): void {\n window.dataLayer ??= [];\n if (typeof event === 'string') {\n window.dataLayer.push({\n event,\n event_category: category,\n event_action: action,\n event_label: label,\n });\n } else {\n window.dataLayer.push(event);\n }\n}\n\n/**\n * func adds functions to run sequentionally after\n * Google Tag Manager is ready.\n */\nexport function func(fn: () => void): void {\n window.dataLayer ??= [];\n window.dataLayer.push(fn);\n}\n", "/*!\n * @license\n * Copyright 2019-2020 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { track } from '../analytics/analytics';\n\n/**\n * Options are keyhandler callback options.\n */\ninterface Options {\n /**\n * target is the element the key event should filter on. The\n * default target is the document.\n */\n target?: Element;\n\n /**\n * withMeta specifies if the event callback should fire when\n * the key is pressed with a meta key (ctrl, alt, etc). By\n * default meta keypresses are ignored.\n */\n withMeta?: boolean;\n}\n\n/**\n * KeyHandler is the config for a keyboard event callback.\n */\ninterface KeyHandler extends Options {\n description: string;\n callback: (e: KeyboardEvent) => void;\n}\n\n/**\n * KeyboardController controls event callbacks for sitewide\n * keyboard events. Multiple callbacks can be registered for\n * a single key and by default the controller ignores events\n * for text input targets.\n */\nclass KeyboardController {\n handlers: Record>;\n\n constructor() {\n this.handlers = {};\n document.addEventListener('keydown', e => this.handleKeyPress(e));\n }\n\n /**\n * on registers keyboard event callbacks.\n * @param key the key to register.\n * @param description name of the event.\n * @param callback event callback.\n * @param options set target and withMeta options to override the default behaviors.\n */\n on(key: string, description: string, callback: (e: KeyboardEvent) => void, options?: Options) {\n this.handlers[key] ??= new Set();\n this.handlers[key].add({ description, callback, ...options });\n return this;\n }\n\n private handleKeyPress(e: KeyboardEvent) {\n for (const handler of this.handlers[e.key.toLowerCase()] ?? new Set()) {\n if (handler.target && handler.target !== e.target) {\n return;\n }\n const t = e.target as HTMLElement | null;\n if (\n !handler.target &&\n (t?.tagName === 'INPUT' || t?.tagName === 'SELECT' || t?.tagName === 'TEXTAREA')\n ) {\n return;\n }\n if (t?.isContentEditable) {\n return;\n }\n if (\n (handler.withMeta && !(e.ctrlKey || e.metaKey)) ||\n (!handler.withMeta && (e.ctrlKey || e.metaKey))\n ) {\n return;\n }\n track('keypress', 'hotkeys', `${e.key} pressed`, handler.description);\n handler.callback(e);\n }\n }\n}\n\nexport const keyboard = new KeyboardController();\n", "/*!\n * @license\n * Copyright 2019-2020 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n// This file implements the behavior of the \"jump to symbol\" dialog for Go\n// package documentation, as well as the simple dialog that displays keyboard\n// shortcuts.\n\n// The DOM for the dialogs is at the bottom of static/frontend/unit/main/_modals.tmpl.\n// The CSS is in static/frontend/unit/main/_modals.css.\n\n// The dialog is activated by pressing the 'f' key. It presents a list\n// (#JumpDialog-list) of all Go symbols displayed in the documentation.\n// Entering text in the dialog's text box (#JumpDialog-filter) restricts the\n// list to symbols containing the text. Clicking on an symbol jumps to\n// its documentation.\n\n// This code is based on\n// https://go.googlesource.com/gddo/+/refs/heads/master/gddo-server/assets/site.js.\n// It was modified to remove the dependence on jquery and bootstrap.\n\nimport { keyboard } from '../keyboard/keyboard';\n\nexport function initModals(): void {\n const jumpDialog = document.querySelector('.JumpDialog');\n const jumpBody = jumpDialog?.querySelector('.JumpDialog-body');\n const jumpList = jumpDialog?.querySelector('.JumpDialog-list');\n const jumpFilter = jumpDialog?.querySelector('.JumpDialog-input');\n const doc = document.querySelector('.js-documentation');\n\n interface JumpListItem {\n link: HTMLAnchorElement;\n name: string;\n kind: string;\n lower: string;\n }\n\n let jumpListItems: JumpListItem[] | undefined; // All the symbols in the doc; computed only once.\n\n // collectJumpListItems returns a list of items, one for each symbol in the\n // documentation on the current page.\n //\n // It uses the data-kind attribute generated in the documentation HTML to find\n // the symbols and their id attributes.\n //\n // If there are no data-kind attributes, then we have older doc; fall back to\n // a less precise method.\n function collectJumpListItems() {\n const items = [];\n if (!doc) return;\n for (const el of doc.querySelectorAll('[data-kind]')) {\n items.push(newJumpListItem(el));\n }\n\n // Clicking on any of the links closes the dialog.\n for (const item of items) {\n item.link.addEventListener('click', function () {\n jumpDialog?.close();\n });\n }\n // Sort case-insensitively by symbol name.\n items.sort(function (a, b) {\n return a.lower.localeCompare(b.lower);\n });\n return items;\n }\n\n // newJumpListItem creates a new item for the DOM element el.\n // An item is an object with:\n // - name: the element's id (which is the symbol name)\n // - kind: the element's kind (function, variable, etc.),\n // - link: a link ('a' tag) to the element\n // - lower: the name in lower case, just for sorting\n function newJumpListItem(el: Element): JumpListItem {\n const a = document.createElement('a');\n const name = el.getAttribute('id');\n a.setAttribute('href', '#' + name);\n a.setAttribute('tabindex', '-1');\n a.setAttribute('data-gtmc', 'jump to link');\n const kind = el.getAttribute('data-kind');\n return {\n link: a,\n name: name ?? '',\n kind: kind ?? '',\n lower: name?.toLowerCase() ?? '', // for sorting\n };\n }\n\n let lastFilterValue: string; // The last contents of the filter text box.\n let activeJumpItem = -1; // The index of the currently active item in the list.\n\n // updateJumpList sets the elements of the dialog list to\n // everything whose name contains filter.\n function updateJumpList(filter: string) {\n lastFilterValue = filter;\n if (!jumpListItems) {\n jumpListItems = collectJumpListItems();\n }\n setActiveJumpItem(-1);\n\n // Remove all children from list.\n while (jumpList?.firstChild) {\n jumpList.firstChild.remove();\n }\n\n if (filter) {\n // A filter is set. We treat the filter as a substring that can appear in\n // an item name (case insensitive), and find the following matches - in\n // order of priority:\n //\n // 1. Exact matches (the filter matches the item's name exactly)\n // 2. Prefix matches (the item's name starts with filter)\n // 3. Infix matches (the filter is a substring of the item's name)\n const filterLowerCase = filter.toLowerCase();\n\n const exactMatches = [];\n const prefixMatches = [];\n const infixMatches = [];\n\n // makeLinkHtml creates the link name HTML for a list item. item is the DOM\n // item. item.name.substr(boldStart, boldEnd) will be bolded.\n const makeLinkHtml = (item: JumpListItem, boldStart: number, boldEnd: number) => {\n return (\n item.name.substring(0, boldStart) +\n '' +\n item.name.substring(boldStart, boldEnd) +\n '' +\n item.name.substring(boldEnd)\n );\n };\n\n for (const item of jumpListItems ?? []) {\n const nameLowerCase = item.name.toLowerCase();\n\n if (nameLowerCase === filterLowerCase) {\n item.link.innerHTML = makeLinkHtml(item, 0, item.name.length);\n exactMatches.push(item);\n } else if (nameLowerCase.startsWith(filterLowerCase)) {\n item.link.innerHTML = makeLinkHtml(item, 0, filter.length);\n prefixMatches.push(item);\n } else {\n const index = nameLowerCase.indexOf(filterLowerCase);\n if (index > -1) {\n item.link.innerHTML = makeLinkHtml(item, index, index + filter.length);\n infixMatches.push(item);\n }\n }\n }\n\n for (const item of exactMatches.concat(prefixMatches).concat(infixMatches)) {\n jumpList?.appendChild(item.link);\n }\n } else {\n if (!jumpListItems || jumpListItems.length === 0) {\n const msg = document.createElement('i');\n msg.innerHTML = 'There are no symbols on this page.';\n jumpList?.appendChild(msg);\n }\n // No filter set; display all items in their existing order.\n for (const item of jumpListItems ?? []) {\n item.link.innerHTML = item.name + ' ' + item.kind + '';\n jumpList?.appendChild(item.link);\n }\n }\n\n if (jumpBody) {\n jumpBody.scrollTop = 0;\n }\n if (jumpListItems?.length && jumpList && jumpList.children.length > 0) {\n setActiveJumpItem(0);\n }\n }\n\n // Set the active jump item to n.\n function setActiveJumpItem(n: number) {\n const cs = jumpList?.children as HTMLCollectionOf | null | undefined;\n if (!cs || !jumpBody) {\n return;\n }\n if (activeJumpItem >= 0) {\n cs[activeJumpItem].classList.remove('JumpDialog-active');\n }\n if (n >= cs.length) {\n n = cs.length - 1;\n }\n if (n >= 0) {\n cs[n].classList.add('JumpDialog-active');\n\n // Scroll so the active item is visible.\n // For some reason cs[n].scrollIntoView() doesn't behave as I'd expect:\n // it moves the entire dialog box in the viewport.\n\n // Get the top and bottom of the active item relative to jumpBody.\n const activeTop = cs[n].offsetTop - cs[0].offsetTop;\n const activeBottom = activeTop + cs[n].clientHeight;\n if (activeTop < jumpBody.scrollTop) {\n // Off the top; scroll up.\n jumpBody.scrollTop = activeTop;\n } else if (activeBottom > jumpBody.scrollTop + jumpBody.clientHeight) {\n // Off the bottom; scroll down.\n jumpBody.scrollTop = activeBottom - jumpBody.clientHeight;\n }\n }\n activeJumpItem = n;\n }\n\n // Increment the activeJumpItem by delta.\n function incActiveJumpItem(delta: number) {\n if (activeJumpItem < 0) {\n return;\n }\n let n = activeJumpItem + delta;\n if (n < 0) {\n n = 0;\n }\n setActiveJumpItem(n);\n }\n\n // Pressing a key in the filter updates the list (if the filter actually changed).\n jumpFilter?.addEventListener('keyup', function () {\n if (jumpFilter.value.toUpperCase() != lastFilterValue.toUpperCase()) {\n updateJumpList(jumpFilter.value);\n }\n });\n\n // Pressing enter in the filter selects the first element in the list.\n jumpFilter?.addEventListener('keydown', function (event) {\n const upArrow = 38;\n const downArrow = 40;\n const enterKey = 13;\n switch (event.which) {\n case upArrow:\n incActiveJumpItem(-1);\n event.preventDefault();\n break;\n case downArrow:\n incActiveJumpItem(1);\n event.preventDefault();\n break;\n case enterKey:\n if (activeJumpItem >= 0) {\n if (jumpList) {\n (jumpList.children[activeJumpItem] as HTMLElement).click();\n event.preventDefault();\n }\n }\n break;\n }\n });\n\n const shortcutsDialog = document.querySelector('.ShortcutsDialog');\n\n // - Pressing 'f' or 'F' opens the jump-to-symbol dialog.\n // - Pressing '?' opens up the shortcut dialog.\n // Ignore a keypress if a dialog is already open, or if it is pressed on a\n // component that wants to consume it.\n keyboard\n .on('f', 'open jump to modal', e => {\n if (jumpDialog?.open || shortcutsDialog?.open) {\n return;\n }\n e.preventDefault();\n if (jumpFilter) {\n jumpFilter.value = '';\n }\n jumpDialog?.showModal?.();\n jumpFilter?.focus();\n updateJumpList('');\n })\n .on('?', 'open shortcuts modal', () => {\n if (jumpDialog?.open || shortcutsDialog?.open) {\n return;\n }\n shortcutsDialog?.showModal?.();\n });\n\n const jumpOutlineInput = document.querySelector('.js-jumpToInput');\n if (jumpOutlineInput) {\n jumpOutlineInput.addEventListener('click', () => {\n if (jumpFilter) {\n jumpFilter.value = '';\n }\n updateJumpList('');\n if (jumpDialog?.open || shortcutsDialog?.open) {\n return;\n }\n jumpDialog?.showModal?.();\n jumpFilter?.focus();\n });\n }\n\n document.querySelector('.js-openShortcuts')?.addEventListener('click', () => {\n shortcutsDialog?.showModal?.();\n });\n}\n", "/**\n * @license\n * Copyright 2022 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * Left Navigation.\n */\nexport const initJumpLinks = async function () {\n const pagesWithJumpLinks = ['/about'];\n if (!pagesWithJumpLinks.includes(window.location.pathname)) {\n // stop the file from doing anything else if the page doesn't have jumplinks\n return;\n }\n\n // these might be generated or not so don't grab references to the elements until actually need them.\n const titles = 'h2, h3, h4';\n const nav = '.LeftNav a';\n // these are always in the dom so we can get them now and throw errors if they're not.\n const leftNav = document.querySelector('.LeftNav');\n const siteContent = document.querySelector('.go-Content');\n let isObserverDisabled = false;\n\n /**\n * El function\n * @example el('h1', {className: 'title'}, 'Welcome to the site');\n * @example el('ul', {className: 'list'}, el('li', {}, 'Item one'), el('li', {}, 'Item two'), el('li', {}, 'Item three'));\n * @example el('img', {src: '/url.svg'});\n */\n function el(\n type = '',\n props: { [key: string]: string } = {},\n ...children: (HTMLElement | HTMLElement[] | string | undefined)[]\n ) {\n // Error, no type declared.\n if (!type) {\n throw new Error('Provide `type` to create document element.');\n }\n\n // Create element with optional attribute props\n const docEl = Object.assign(document.createElement(type), props);\n\n // Children: array containing strings or elements\n children.forEach(child => {\n if (typeof child === 'string') {\n docEl.appendChild(document.createTextNode(child));\n } else if (Array.isArray(child)) {\n child.forEach(c => docEl.appendChild(c));\n } else if (child instanceof HTMLElement) {\n docEl.appendChild(child);\n }\n });\n\n return docEl;\n }\n /** Build Nav if data hydrate is present. */\n function buildNav() {\n return new Promise((resolve, reject) => {\n let navItems: { id: string; label: string; subnav?: { id: string; label: string }[] }[] = [];\n let elements: HTMLElement[] = [];\n\n if (!siteContent || !leftNav) {\n return reject('.SiteContent not found.');\n }\n if (leftNav instanceof HTMLElement && !leftNav?.dataset?.hydrate) {\n return resolve(true);\n }\n\n for (const title of siteContent.querySelectorAll(titles)) {\n if (title instanceof HTMLElement && !title?.dataset?.ignore) {\n switch (title.tagName) {\n case 'H2':\n navItems = [\n ...navItems,\n {\n id: title.id,\n label: title?.dataset?.title ? title.dataset.title : title.textContent ?? '',\n },\n ];\n break;\n\n case 'H3':\n case 'H4':\n if (!navItems[navItems.length - 1]?.subnav) {\n navItems[navItems.length - 1].subnav = [\n {\n id: title.id,\n label: title?.dataset?.title ? title.dataset.title : title.textContent ?? '',\n },\n ];\n } else if (navItems[navItems.length - 1].subnav) {\n navItems[navItems.length - 1].subnav?.push({\n id: title.id,\n label: title?.dataset?.title ? title.dataset.title : title.textContent ?? '',\n });\n }\n break;\n }\n }\n }\n\n for (const navItem of navItems) {\n const link = el('a', { href: '#' + navItem.id }, el('span', {}, navItem.label));\n elements = [...elements, link];\n if (navItem?.subnav) {\n let subLinks: HTMLElement[] = [];\n for (const subnavItem of navItem.subnav) {\n const subItem = el(\n 'li',\n {},\n el(\n 'a',\n { href: '#' + subnavItem.id },\n el('img', { src: '/static/frontend/about/dot.svg', width: '5', height: '5' }),\n el('span', {}, subnavItem.label)\n )\n );\n subLinks = [...subLinks, subItem];\n }\n const list = el('ul', { className: 'LeftSubnav' }, subLinks);\n elements = [...elements, list];\n }\n }\n\n elements.forEach(element => leftNav.appendChild(element));\n\n return resolve(true);\n });\n }\n /**\n * Set the correct active element.\n */\n function setNav() {\n return new Promise(resolve => {\n if (!document.querySelectorAll(nav)) return resolve(true);\n for (const a of document.querySelectorAll(nav)) {\n if (a instanceof HTMLAnchorElement && a.href === location.href) {\n setElementActive(a);\n break;\n }\n }\n resolve(true);\n });\n }\n /** resetNav: removes all .active from nav elements */\n function resetNav() {\n return new Promise(resolve => {\n if (!document.querySelectorAll(nav)) return resolve(true);\n for (const a of document.querySelectorAll(nav)) {\n a.classList.remove('active');\n }\n resolve(true);\n });\n }\n /** setElementActive: controls resetting nav and highlighting the appropriate nav items */\n function setElementActive(element: HTMLAnchorElement) {\n if (element instanceof HTMLAnchorElement) {\n resetNav().then(() => {\n element.classList.add('active');\n const parent = element?.parentNode?.parentNode;\n if (parent instanceof HTMLElement && parent?.classList?.contains('LeftSubnav')) {\n parent.previousElementSibling?.classList.add('active');\n }\n });\n }\n }\n /** setLinkManually: disables observer and selects the clicked nav item. */\n function setLinkManually() {\n delayObserver();\n const link = document.querySelector('[href=\"' + location.hash + '\"]');\n if (link instanceof HTMLAnchorElement) {\n setElementActive(link);\n }\n }\n /** delayObserver: Quick on off switch for intersection observer. */\n function delayObserver() {\n isObserverDisabled = true;\n setTimeout(() => {\n isObserverDisabled = false;\n }, 200);\n }\n /** observeSections: kicks off observation of titles as well as manual clicks with hashchange */\n function observeSections() {\n window.addEventListener('hashchange', setLinkManually);\n\n if (siteContent?.querySelectorAll(titles)) {\n const callback: IntersectionObserverCallback = entries => {\n if (!isObserverDisabled && Array.isArray(entries) && entries.length > 0) {\n for (const entry of entries) {\n if (entry.isIntersecting && entry.target instanceof HTMLElement) {\n const { id } = entry.target;\n const link = document.querySelector('[href=\"#' + id + '\"]');\n if (link instanceof HTMLAnchorElement) {\n setElementActive(link);\n }\n break;\n }\n }\n }\n };\n // rootMargin is important when multiple sections are in the observable area **on page load**.\n // they will still be highlighted on scroll because of the root margin.\n const ob = new IntersectionObserver(callback, {\n threshold: 0,\n rootMargin: '0px 0px -50% 0px',\n });\n for (const title of siteContent.querySelectorAll(titles)) {\n if (title instanceof HTMLElement && !title?.dataset?.ignore) {\n ob.observe(title);\n }\n }\n }\n }\n\n try {\n await buildNav();\n await setNav();\n if (location.hash) {\n delayObserver();\n }\n observeSections();\n } catch (e) {\n if (e instanceof Error) {\n console.error(e.message);\n } else {\n console.error(e);\n }\n }\n};\n", "/**\n * @license\n * Copyright 2020 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { registerHeaderListeners, registerSearchFormListeners } from 'static/shared/header/header';\nimport { CarouselController } from 'static/shared/carousel/carousel';\nimport { ClipboardController } from 'static/shared/clipboard/clipboard';\nimport { ToolTipController } from 'static/shared/tooltip/tooltip';\nimport { SelectNavController } from 'static/shared/outline/select';\nimport { ModalController } from 'static/shared/modal/modal';\nimport { initModals } from 'static/shared/jump/jump';\n\nimport { keyboard } from 'static/shared/keyboard/keyboard';\nimport * as analytics from 'static/shared/analytics/analytics';\nimport { initJumpLinks } from './about/index';\n\nwindow.addEventListener('load', () => {\n for (const el of document.querySelectorAll('.js-clipboard')) {\n new ClipboardController(el);\n }\n\n for (const el of document.querySelectorAll('.js-modal')) {\n new ModalController(el);\n }\n\n for (const t of document.querySelectorAll('.js-tooltip')) {\n new ToolTipController(t);\n }\n\n for (const el of document.querySelectorAll('.js-selectNav')) {\n new SelectNavController(el);\n }\n\n for (const el of document.querySelectorAll('.js-carousel')) {\n new CarouselController(el);\n }\n\n for (const el of document.querySelectorAll('.js-toggleTheme')) {\n el.addEventListener('click', () => {\n toggleTheme();\n });\n }\n\n if (document.querySelector('.js-gtmID')?.dataset.gtmid && window.dataLayer) {\n analytics.func(function () {\n removeUTMSource();\n });\n } else {\n removeUTMSource();\n }\n\n registerHeaderListeners();\n registerSearchFormListeners();\n initModals();\n initJumpLinks();\n registerCookieNotice();\n});\n\n// Pressing '/' focuses the search box\nkeyboard.on('/', 'focus search', e => {\n const searchInput = Array.from(\n document.querySelectorAll('.js-searchFocus')\n ).pop();\n // Favoring the Firefox quick find feature over search input\n // focus. See: https://github.com/golang/go/issues/41093.\n if (searchInput && !window.navigator.userAgent.includes('Firefox')) {\n e.preventDefault();\n searchInput.focus();\n }\n});\n\n// Pressing 'y' changes the browser URL to the canonical URL\n// without triggering a reload.\nkeyboard.on('y', 'set canonical url', () => {\n let canonicalURLPath = document.querySelector('.js-canonicalURLPath')?.dataset[\n 'canonicalUrlPath'\n ];\n if (canonicalURLPath && canonicalURLPath !== '') {\n const fragment = window.location.hash;\n if (fragment) {\n canonicalURLPath += fragment;\n }\n window.history.replaceState(null, '', canonicalURLPath);\n }\n});\n\n/**\n * setupGoogleTagManager initializes Google Tag Manager.\n */\n(function setupGoogleTagManager() {\n analytics.track({\n 'gtm.start': new Date().getTime(),\n event: 'gtm.js',\n });\n})();\n\n/**\n * removeUTMSource removes the utm_source GET parameter if present.\n * This is done using JavaScript, so that the utm_source is still\n * captured by Google Analytics.\n */\nfunction removeUTMSource() {\n const urlParams = new URLSearchParams(window.location.search);\n const utmSource = urlParams.get('utm_source');\n if (utmSource !== 'gopls' && utmSource !== 'godoc' && utmSource !== 'pkggodev') {\n return;\n }\n\n /** Strip the utm_source query parameter and replace the URL. **/\n const newURL = new URL(window.location.href);\n urlParams.delete('utm_source');\n newURL.search = urlParams.toString();\n window.history.replaceState(null, '', newURL.toString());\n}\n\n/**\n * toggleTheme switches the preferred color scheme between auto, light, and dark.\n */\nfunction toggleTheme() {\n let nextTheme = 'dark';\n const theme = document.documentElement.getAttribute('data-theme');\n if (theme === 'dark') {\n nextTheme = 'light';\n } else if (theme === 'light') {\n nextTheme = 'auto';\n }\n let domain = '';\n if (location.hostname === 'go.dev' || location.hostname.endsWith('.go.dev')) {\n domain = 'domain=.go.dev;';\n }\n document.documentElement.setAttribute('data-theme', nextTheme);\n document.cookie = `prefers-color-scheme=${nextTheme};${domain}path=/;max-age=31536000;`;\n}\n\n/**\n * registerCookieNotice makes the cookie notice visible and adds listeners to dismiss it\n * if it has not yet been acknowledge by the user.\n */\nfunction registerCookieNotice() {\n const themeCookie = document.cookie.match(/cookie-consent=true/);\n if (!themeCookie) {\n const notice = document.querySelector('.js-cookieNotice');\n const button = notice?.querySelector('button');\n notice?.classList.add('Cookie-notice--visible');\n button?.addEventListener('click', () => {\n let domain = '';\n if (location.hostname === 'go.dev' || location.hostname.endsWith('.go.dev')) {\n // Apply the cookie to *.go.dev.\n domain = 'domain=.go.dev;';\n }\n document.cookie = `cookie-consent=true;${domain}path=/;max-age=31536000`;\n notice?.remove();\n });\n }\n}\n"], - "mappings": "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", - "names": ["registerHeaderListeners", "header", "menuItemHover", "e", "target", "forced", "toggleForcedOpen", "_a", "_b", "isForced", "currentTarget", "closeSubmenuOnEsc", "event", "forcedOpenItem", "headerbuttons", "button", "isActive", "handleNavigationDrawerInactive", "handleNavigationDrawerActive", "scrim", "subnav", "getNavigationDrawerMenuItems", "navigationDrawer", "menuItems", "anchorEl", "getNavigationDrawerIsSubnav", "parentMenuItem", "item", "handleMenuItemTabLeftFactory", "handleMenuItemTabRightFactory", "prepMobileNavigationDrawer", "isSubnav", "parentLi", "submenu", "drawer", "registerSearchFormListeners", "searchForm", "expandSearch", "input", "headerLogo", "menuButton", "CarouselController", "el", "index", "d", "s", "_a", "i", "v", "_b", "arrows", "dots", "li", "button", "ClipboardController", "el", "_a", "_b", "_c", "_d", "_e", "e", "TOOLTIP_SHOW_DURATION_MS", "text", "durationMs", "ToolTipController", "el", "e", "SelectNavController", "el", "e", "target", "href", "ModalController", "el", "button", "_a", "btn", "track", "event", "category", "action", "label", "_a", "func", "fn", "KeyboardController", "key", "description", "callback", "options", "_a", "_b", "handler", "t", "track", "keyboard", "initModals", "_a", "jumpDialog", "jumpBody", "jumpList", "jumpFilter", "doc", "jumpListItems", "collectJumpListItems", "items", "el", "newJumpListItem", "item", "a", "b", "name", "kind", "lastFilterValue", "activeJumpItem", "updateJumpList", "filter", "setActiveJumpItem", "filterLowerCase", "exactMatches", "prefixMatches", "infixMatches", "makeLinkHtml", "boldStart", "boldEnd", "nameLowerCase", "index", "msg", "n", "cs", "activeTop", "activeBottom", "incActiveJumpItem", "delta", "event", "shortcutsDialog", "keyboard", "e", "jumpOutlineInput", "initJumpLinks", "titles", "nav", "leftNav", "siteContent", "isObserverDisabled", "el", "type", "props", "children", "docEl", "child", "c", "buildNav", "resolve", "reject", "_a", "_b", "_c", "_d", "_e", "_f", "_g", "_h", "_i", "_j", "navItems", "elements", "title", "navItem", "link", "subLinks", "subnavItem", "subItem", "list", "element", "setNav", "a", "setElementActive", "resetNav", "parent", "setLinkManually", "delayObserver", "observeSections", "callback", "entries", "entry", "id", "ob", "e", "_a", "el", "ClipboardController", "ModalController", "t", "ToolTipController", "SelectNavController", "CarouselController", "toggleTheme", "func", "removeUTMSource", "registerHeaderListeners", "registerSearchFormListeners", "initModals", "initJumpLinks", "registerCookieNotice", "keyboard", "e", "searchInput", "canonicalURLPath", "fragment", "track", "urlParams", "utmSource", "newURL", "nextTheme", "theme", "domain", "notice", "button"] + "sources": ["../shared/header/header.ts", "../shared/base-path/base-path.ts", "../shared/carousel/carousel.ts", "../shared/clipboard/clipboard.ts", "../shared/tooltip/tooltip.ts", "../shared/outline/select.ts", "../shared/modal/modal.ts", "../shared/analytics/analytics.ts", "../shared/keyboard/keyboard.ts", "../shared/jump/jump.ts", "about/index.ts", "frontend.ts"], + "sourcesContent": ["/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nexport function registerHeaderListeners(): void {\n const header = document.querySelector('.js-header') as HTMLElement;\n\n // Desktop menu hover state\n const menuItemHovers = document.querySelectorAll('.js-desktop-menu-hover');\n menuItemHovers.forEach(menuItemHover => {\n // when user clicks on the dropdown menu item on desktop or mobile,\n // force the menu to stay open until the user clicks off of it.\n menuItemHover.addEventListener('mouseenter', e => {\n const target = e.target as HTMLElement;\n const forced = document.querySelector('.forced-open') as HTMLElement;\n if (forced && forced !== menuItemHover) {\n forced.blur();\n forced.classList.remove('forced-open');\n }\n // prevents menus that have been tabbed into from staying open\n // when you hover over another menu\n target.classList.remove('forced-closed');\n target.classList.add('forced-open');\n });\n\n const toggleForcedOpen = (e: Event) => {\n const target = e.target as HTMLElement;\n const isForced = target?.classList.contains('forced-open');\n const currentTarget = e.currentTarget as HTMLElement;\n if (isForced) {\n currentTarget.removeEventListener('blur', () =>\n currentTarget.classList.remove('forced-open')\n );\n currentTarget.classList.remove('forced-open');\n currentTarget.classList.add('forced-closed');\n currentTarget.blur();\n currentTarget?.parentNode?.addEventListener('mouseout', () => {\n currentTarget.classList.remove('forced-closed');\n });\n } else {\n currentTarget.classList.remove('forced-closed');\n currentTarget.classList.add('forced-open');\n currentTarget.focus();\n currentTarget.addEventListener('blur', () => currentTarget.classList.remove('forced-open'));\n currentTarget?.parentNode?.removeEventListener('mouseout', () => {\n currentTarget.classList.remove('forced-closed');\n });\n }\n currentTarget.focus();\n };\n menuItemHover.addEventListener('click', toggleForcedOpen);\n menuItemHover.addEventListener('focus', e => {\n const target = e.target as HTMLElement;\n target.classList.add('forced-closed');\n target.classList.remove('forced-open');\n });\n\n // ensure desktop submenus are closed when esc is pressed\n const closeSubmenuOnEsc = (e: Event) => {\n const event = e as KeyboardEvent;\n const target = e.target as HTMLElement;\n if (event.key === 'Escape') {\n const forcedOpenItem = document.querySelector('.forced-open') as HTMLElement;\n if (forcedOpenItem) {\n forcedOpenItem.classList.remove('forced-open');\n forcedOpenItem.blur();\n forcedOpenItem.classList.add('forced-closed');\n target?.focus();\n }\n }\n };\n document.addEventListener('keydown', closeSubmenuOnEsc);\n });\n\n // Mobile menu subnav menus\n const headerbuttons = document.querySelectorAll('.js-headerMenuButton');\n headerbuttons.forEach(button => {\n button.addEventListener('click', e => {\n e.preventDefault();\n const isActive = header?.classList.contains('is-active');\n if (isActive) {\n handleNavigationDrawerInactive(header);\n } else {\n handleNavigationDrawerActive(header);\n }\n button.setAttribute('aria-expanded', isActive ? 'true' : 'false');\n });\n });\n\n const scrim = document.querySelector('.js-scrim');\n scrim?.addEventListener('click', e => {\n e.preventDefault();\n\n // find any active submenus and close them\n const activeSubnavs = document.querySelectorAll('.go-NavigationDrawer-submenuItem.is-active');\n activeSubnavs.forEach(subnav => handleNavigationDrawerInactive(subnav as HTMLElement));\n\n handleNavigationDrawerInactive(header);\n\n headerbuttons.forEach(button => {\n button.setAttribute(\n 'aria-expanded',\n header?.classList.contains('is-active') ? 'true' : 'false'\n );\n });\n });\n\n const getNavigationDrawerMenuItems = (navigationDrawer: HTMLElement): HTMLElement[] => {\n if (!navigationDrawer) {\n return [];\n }\n\n const menuItems = Array.from(\n navigationDrawer.querySelectorAll(\n ':scope > .go-NavigationDrawer-nav > .go-NavigationDrawer-list > .go-NavigationDrawer-listItem > a, :scope > .go-NavigationDrawer-nav > .go-NavigationDrawer-list > .go-NavigationDrawer-listItem > .go-Header-socialIcons > a'\n ) || []\n );\n\n const anchorEl = navigationDrawer.querySelector('.go-NavigationDrawer-header > a');\n if (anchorEl) {\n menuItems.unshift(anchorEl);\n }\n return menuItems as HTMLElement[];\n };\n\n const getNavigationDrawerIsSubnav = (navigationDrawer: HTMLElement) => {\n if (!navigationDrawer) {\n return;\n }\n return navigationDrawer.classList.contains('go-NavigationDrawer-submenuItem');\n };\n\n const handleNavigationDrawerInactive = (navigationDrawer: HTMLElement) => {\n if (!navigationDrawer) {\n return;\n }\n const menuItems = getNavigationDrawerMenuItems(navigationDrawer);\n navigationDrawer.classList.remove('is-active');\n const parentMenuItem = navigationDrawer\n .closest('.go-NavigationDrawer-listItem')\n ?.querySelector(':scope > a') as HTMLElement;\n parentMenuItem?.focus();\n menuItems?.forEach(item => item?.setAttribute('tabindex', '-1'));\n if (menuItems && menuItems[0]) {\n menuItems[0].removeEventListener('keydown', handleMenuItemTabLeftFactory(navigationDrawer));\n menuItems[menuItems.length - 1].removeEventListener(\n 'keydown',\n handleMenuItemTabRightFactory(navigationDrawer)\n );\n }\n\n if (navigationDrawer === header) {\n headerbuttons && (headerbuttons[0] as HTMLElement)?.focus();\n }\n };\n\n const handleNavigationDrawerActive = (navigationDrawer: HTMLElement) => {\n const menuItems = getNavigationDrawerMenuItems(navigationDrawer);\n\n navigationDrawer.classList.add('is-active');\n menuItems.forEach(item => item.setAttribute('tabindex', '0'));\n menuItems[0].focus();\n\n menuItems[0].addEventListener('keydown', handleMenuItemTabLeftFactory(navigationDrawer));\n menuItems[menuItems.length - 1].addEventListener(\n 'keydown',\n handleMenuItemTabRightFactory(navigationDrawer)\n );\n };\n\n const handleMenuItemTabLeftFactory = (navigationDrawer: HTMLElement) => {\n return (e: KeyboardEvent) => {\n if (e.key === 'Tab' && e.shiftKey) {\n e.preventDefault();\n handleNavigationDrawerInactive(navigationDrawer);\n }\n };\n };\n\n const handleMenuItemTabRightFactory = (navigationDrawer: HTMLElement) => {\n return (e: KeyboardEvent) => {\n if (e.key === 'Tab' && !e.shiftKey) {\n e.preventDefault();\n handleNavigationDrawerInactive(navigationDrawer);\n }\n };\n };\n\n const prepMobileNavigationDrawer = (navigationDrawer: HTMLElement) => {\n const isSubnav = getNavigationDrawerIsSubnav(navigationDrawer);\n const menuItems = getNavigationDrawerMenuItems(navigationDrawer);\n navigationDrawer.addEventListener('keyup', e => {\n if (e.key === 'Escape') {\n handleNavigationDrawerInactive(navigationDrawer);\n }\n });\n\n menuItems.forEach(item => {\n const parentLi = item.closest('li');\n if (parentLi && parentLi.classList.contains('js-mobile-subnav-trigger')) {\n const submenu = parentLi.querySelector('.go-NavigationDrawer-submenuItem') as HTMLElement;\n item.addEventListener('click', () => {\n handleNavigationDrawerActive(submenu);\n });\n }\n });\n if (isSubnav) {\n handleNavigationDrawerInactive(navigationDrawer);\n navigationDrawer\n ?.querySelector('.go-NavigationDrawer-header')\n ?.addEventListener('click', e => {\n e.preventDefault();\n handleNavigationDrawerInactive(navigationDrawer);\n });\n }\n };\n\n document\n .querySelectorAll('.go-NavigationDrawer')\n .forEach(drawer => prepMobileNavigationDrawer(drawer as HTMLElement));\n\n handleNavigationDrawerInactive(header);\n}\n\nexport function registerSearchFormListeners(): void {\n const searchForm = document.querySelector('.js-searchForm');\n const expandSearch = document.querySelector('.js-expandSearch');\n const input = searchForm?.querySelector('input');\n const headerLogo = document.querySelector('.js-headerLogo');\n const menuButton = document.querySelector('.js-headerMenuButton');\n expandSearch?.addEventListener('click', () => {\n searchForm?.classList.add('go-SearchForm--expanded');\n headerLogo?.classList.add('go-Header-logo--hidden');\n menuButton?.classList.add('go-Header-navOpen--hidden');\n input?.focus();\n });\n document?.addEventListener('click', e => {\n if (!searchForm?.contains(e.target as Node)) {\n searchForm?.classList.remove('go-SearchForm--expanded');\n headerLogo?.classList.remove('go-Header-logo--hidden');\n menuButton?.classList.remove('go-Header-navOpen--hidden');\n }\n });\n}\n", "/**\n * @license\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * \u7AD9\u70B9 URL \u5B50\u8DEF\u5F84\u524D\u7F00\uFF0C\u4ECE \u8BFB\u51FA\u3002\n *\n * fork \u7528 -base-path=/pkgsitex \u542F\u52A8\u65F6\uFF0Cserver \u7AEF\u6A21\u677F\u628A attribute \u5199\u6210\n * \"/pkgsitex\"\uFF1B\u4E0A\u6E38\u516C\u7F51 pkg.go.dev \u9ED8\u8BA4\u6302\u6839\uFF0Cattribute \u4E3A\u7A7A\u5B57\u7B26\u4E32\u3002\n *\n * \u5728 \u4E4B\u5916\u7684\u811A\u672C\u521D\u59CB\u5316\u9636\u6BB5\u8C03\u7528\u90FD\u5B89\u5168\u2014\u2014documentElement \u4E00\u5B9A\u5B58\u5728\u3002\n */\nexport function getBasePath(): string {\n return document.documentElement.dataset.basePath ?? '';\n}\n\n/**\n * \u7ED9\u4EE5 / \u5F00\u5934\u7684\u7EDD\u5BF9\u8DEF\u5F84\u52A0 BasePath \u524D\u7F00\u3002\n *\n * \u7528\u4E8E fetch URL / innerHTML src / location \u6BD4\u8F83\u7B49\u4EFB\u4F55\u9700\u8981\u62FC\u7AD9\u70B9\u7EDD\u5BF9 path\n * \u7684\u5730\u65B9\u3002\u975E\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u4E0D\u4EE5 / \u5F00\u5934\uFF09\u539F\u6837\u8FD4\u56DE\u2014\u2014caller \u81EA\u5DF1\u4FDD\u8BC1\u8BED\u4E49\u3002\n *\n * \u4F8B\uFF1A\n * abs('/play/share') // \u2192 '/pkgsitex/play/share' \u6216 '/play/share'\n * abs('/static/foo.svg') // \u2192 '/pkgsitex/static/foo.svg' \u6216 '/static/foo.svg'\n * abs('relative/x') // \u2192 'relative/x'\uFF08\u4E0D\u52A8\uFF09\n */\nexport function abs(p: string): string {\n if (!p.startsWith('/')) return p;\n return getBasePath() + p;\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { getBasePath } from '../base-path/base-path';\n\n/**\n * Carousel Controller adds event listeners, accessibility enhancements, and\n * control elements to a carousel component.\n */\nexport class CarouselController {\n /**\n * slides is a collection of slides in the carousel.\n */\n private slides: HTMLLIElement[];\n /**\n * dots is a collection of dot navigation controls, added to the carousel\n * by this controller.\n */\n private dots: HTMLElement[];\n /**\n * liveRegion is a visually hidden element that notifies assitive devices\n * of visual changes to the carousel. They are added to the carousel by\n * this controller.\n */\n private liveRegion: HTMLElement;\n /**\n * activeIndex is the 0-index of the currently active slide.\n */\n private activeIndex: number;\n\n constructor(private el: HTMLElement) {\n this.slides = Array.from(el.querySelectorAll('.go-Carousel-slide'));\n this.dots = [];\n this.liveRegion = document.createElement('div');\n this.activeIndex = Number(el.getAttribute('data-slide-index') ?? 0);\n\n this.initSlides();\n this.initArrows();\n this.initDots();\n this.initLiveRegion();\n }\n\n private initSlides() {\n for (const [i, v] of this.slides.entries()) {\n if (i === this.activeIndex) continue;\n v.setAttribute('aria-hidden', 'true');\n }\n }\n\n private initArrows() {\n const arrows = document.createElement('ul');\n arrows.classList.add('go-Carousel-arrows');\n // base path \u901A\u8FC7 [getBasePath]() \u6CE8\u5165\uFF1B\u6302\u6839\u65F6\u8FD4\u7A7A\u5B57\u7B26\u4E32\uFF0C\u5BF9\u4E0A\u6E38\u96F6\u5DEE\u5F02\u3002\n const bp = getBasePath();\n arrows.innerHTML = `\n
  • \n \n
  • \n
  • \n \n
  • \n `;\n arrows\n .querySelector('.go-Carousel-prevSlide')\n ?.addEventListener('click', () => this.setActive(this.activeIndex - 1));\n arrows\n .querySelector('.go-Carousel-nextSlide')\n ?.addEventListener('click', () => this.setActive(this.activeIndex + 1));\n this.el.append(arrows);\n }\n\n private initDots() {\n const dots = document.createElement('ul');\n dots.classList.add('go-Carousel-dots');\n for (let i = 0; i < this.slides.length; i++) {\n const li = document.createElement('li');\n const button = document.createElement('button');\n button.classList.add('go-Carousel-dot');\n if (i === this.activeIndex) {\n button.classList.add('go-Carousel-dot--active');\n }\n button.innerHTML = `Slide ${i + 1}`;\n button.addEventListener('click', () => this.setActive(i));\n li.append(button);\n dots.append(li);\n this.dots.push(button);\n }\n this.el.append(dots);\n }\n\n private initLiveRegion() {\n this.liveRegion.setAttribute('aria-live', 'polite');\n this.liveRegion.setAttribute('aria-atomic', 'true');\n this.liveRegion.setAttribute('class', 'go-Carousel-obscured');\n this.liveRegion.textContent = `Slide ${this.activeIndex + 1} of ${this.slides.length}`;\n this.el.appendChild(this.liveRegion);\n }\n\n private setActive = (index: number) => {\n this.activeIndex = (index + this.slides.length) % this.slides.length;\n this.el.setAttribute('data-slide-index', String(this.activeIndex));\n for (const d of this.dots) {\n d.classList.remove('go-Carousel-dot--active');\n }\n this.dots[this.activeIndex].classList.add('go-Carousel-dot--active');\n for (const s of this.slides) {\n s.setAttribute('aria-hidden', 'true');\n }\n this.slides[this.activeIndex].removeAttribute('aria-hidden');\n this.liveRegion.textContent = 'Slide ' + (this.activeIndex + 1) + ' of ' + this.slides.length;\n };\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * This class decorates an element to copy arbitrary data attached via a data-\n * attribute to the clipboard.\n */\nexport class ClipboardController {\n /**\n * The data to be copied to the clipboard.\n */\n private data: string;\n\n /**\n * @param el The element that will trigger copying text to the clipboard. The text is\n * expected to be within its data-to-copy attribute.\n */\n constructor(private el: HTMLButtonElement) {\n this.data = el.dataset['toCopy'] ?? el.innerText;\n // if data-to-copy is empty and the button is part of an input group\n // capture the value of the input.\n if (!this.data && el.parentElement?.classList.contains('go-InputGroup')) {\n this.data = (this.data || el.parentElement?.querySelector('input')?.value) ?? '';\n }\n el.addEventListener('click', e => this.handleCopyClick(e));\n }\n\n /**\n * Handles when the primary element is clicked.\n */\n handleCopyClick(e: MouseEvent): void {\n e.preventDefault();\n const TOOLTIP_SHOW_DURATION_MS = 1000;\n\n // This API is not available on iOS.\n if (!navigator.clipboard) {\n this.showTooltipText('Unable to copy', TOOLTIP_SHOW_DURATION_MS);\n return;\n }\n navigator.clipboard\n .writeText(this.data)\n .then(() => {\n this.showTooltipText('Copied!', TOOLTIP_SHOW_DURATION_MS);\n })\n .catch(() => {\n this.showTooltipText('Unable to copy', TOOLTIP_SHOW_DURATION_MS);\n });\n }\n\n /**\n * Shows the given text in a tooltip for a specified amount of time, in milliseconds.\n */\n showTooltipText(text: string, durationMs: number): void {\n this.el.setAttribute('data-tooltip', text);\n setTimeout(() => this.el.setAttribute('data-tooltip', ''), durationMs);\n }\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * ToolTipController handles closing tooltips on external clicks.\n */\nexport class ToolTipController {\n constructor(private el: HTMLDetailsElement) {\n document.addEventListener('click', e => {\n const insideTooltip = this.el.contains(e.target as Element);\n if (!insideTooltip) {\n this.el.removeAttribute('open');\n }\n });\n\n // Add event listener for \"Escape\" keydown to close tooltip\n this.el.addEventListener('keydown', e => {\n if (e.key === 'Escape') {\n this.el.open = false;\n }\n });\n }\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { TreeNavController } from './tree.js';\n\nexport class SelectNavController {\n constructor(private el: Element) {\n this.el.addEventListener('change', e => {\n const target = e.target as HTMLSelectElement;\n let href = target.value;\n if (!target.value.startsWith('/')) {\n href = '/' + href;\n }\n window.location.href = href;\n });\n }\n}\n\nexport function makeSelectNav(tree: TreeNavController): HTMLLabelElement {\n const label = document.createElement('label');\n label.classList.add('go-Label');\n label.setAttribute('aria-label', 'Menu');\n const select = document.createElement('select');\n select.classList.add('go-Select', 'js-selectNav');\n label.appendChild(select);\n const outline = document.createElement('optgroup');\n outline.label = 'Outline';\n select.appendChild(outline);\n const groupMap: Record = {};\n let group: HTMLOptGroupElement;\n for (const t of tree.treeitems) {\n if (Number(t.depth) > 4) continue;\n if (t.groupTreeitem) {\n group = groupMap[t.groupTreeitem.label];\n if (!group) {\n group = groupMap[t.groupTreeitem.label] = document.createElement('optgroup');\n group.label = t.groupTreeitem.label;\n select.appendChild(group);\n }\n } else {\n group = outline;\n }\n const o = document.createElement('option');\n o.label = t.label;\n o.textContent = t.label;\n o.value = (t.el as HTMLAnchorElement).href.replace(window.location.origin, '').replace('/', '');\n group.appendChild(o);\n }\n tree.addObserver(t => {\n const hash = (t.el as HTMLAnchorElement).hash;\n const value = select.querySelector(`[value$=\"${hash}\"]`)?.value;\n if (value) {\n select.value = value;\n }\n }, 50);\n return label;\n}\n", "/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\ninterface Window {\n dialogPolyfill?: {\n registerDialog: (el: HTMLDialogElement) => void;\n };\n}\n\ndeclare const window: Window;\n\n/**\n * ModalController registers a dialog element with the polyfill if\n * necessary for the current browser, add adds event listeners to\n * close and open modals.\n */\nexport class ModalController {\n constructor(private el: HTMLDialogElement) {\n if (window.dialogPolyfill) {\n window.dialogPolyfill.registerDialog(el);\n }\n this.init();\n }\n\n init() {\n const button = document.querySelector(`[aria-controls=\"${this.el.id}\"]`);\n if (button) {\n button.addEventListener('click', () => {\n if (this.el.showModal) {\n this.el.showModal();\n } else {\n this.el.setAttribute('opened', 'true');\n }\n this.el.querySelector('input')?.focus();\n });\n }\n for (const btn of this.el.querySelectorAll('[data-modal-close]')) {\n btn.addEventListener('click', () => {\n if (this.el.close) {\n this.el.close();\n } else {\n this.el.removeAttribute('opened');\n }\n });\n }\n }\n}\n", "interface TagManagerEvent {\n /**\n * event is the name of the event, used to filter events in\n * Google Analytics.\n */\n event: string;\n\n /**\n * event_category is a name that you supply as a way to group objects\n * that to analyze. Typically, you will use the same category name\n * multiple times over related UI elements (buttons, links, etc).\n */\n event_category?: string;\n\n /**\n * event_action is used to name the type of event or interaction you\n * want to measure for a particular web object. For example, with a\n * single \"form\" category, you can analyze a number of specific events\n * with this parameter, such as: form entered, form submitted.\n */\n event_action?: string;\n\n /**\n * event_label provide additional information for events that you want\n * to analyze, such as the text label of a link.\n */\n event_label?: string;\n\n /**\n * gtm.start is used to initialize Google Tag Manager.\n */\n 'gtm.start'?: number;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\ndeclare global {\n interface Window {\n dataLayer?: (TagManagerEvent | VoidFunction)[];\n ga?: unknown;\n }\n}\n\n/**\n * track sends events to Google Tag Manager.\n */\nexport function track(\n event: string | TagManagerEvent,\n category?: string,\n action?: string,\n label?: string\n): void {\n window.dataLayer ??= [];\n if (typeof event === 'string') {\n window.dataLayer.push({\n event,\n event_category: category,\n event_action: action,\n event_label: label,\n });\n } else {\n window.dataLayer.push(event);\n }\n}\n\n/**\n * func adds functions to run sequentionally after\n * Google Tag Manager is ready.\n */\nexport function func(fn: () => void): void {\n window.dataLayer ??= [];\n window.dataLayer.push(fn);\n}\n", "/*!\n * @license\n * Copyright 2019-2020 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { track } from '../analytics/analytics';\n\n/**\n * Options are keyhandler callback options.\n */\ninterface Options {\n /**\n * target is the element the key event should filter on. The\n * default target is the document.\n */\n target?: Element;\n\n /**\n * withMeta specifies if the event callback should fire when\n * the key is pressed with a meta key (ctrl, alt, etc). By\n * default meta keypresses are ignored.\n */\n withMeta?: boolean;\n}\n\n/**\n * KeyHandler is the config for a keyboard event callback.\n */\ninterface KeyHandler extends Options {\n description: string;\n callback: (e: KeyboardEvent) => void;\n}\n\n/**\n * KeyboardController controls event callbacks for sitewide\n * keyboard events. Multiple callbacks can be registered for\n * a single key and by default the controller ignores events\n * for text input targets.\n */\nclass KeyboardController {\n handlers: Record>;\n\n constructor() {\n this.handlers = {};\n document.addEventListener('keydown', e => this.handleKeyPress(e));\n }\n\n /**\n * on registers keyboard event callbacks.\n * @param key the key to register.\n * @param description name of the event.\n * @param callback event callback.\n * @param options set target and withMeta options to override the default behaviors.\n */\n on(key: string, description: string, callback: (e: KeyboardEvent) => void, options?: Options) {\n this.handlers[key] ??= new Set();\n this.handlers[key].add({ description, callback, ...options });\n return this;\n }\n\n private handleKeyPress(e: KeyboardEvent) {\n for (const handler of this.handlers[e.key.toLowerCase()] ?? new Set()) {\n if (handler.target && handler.target !== e.target) {\n return;\n }\n const t = e.target as HTMLElement | null;\n if (\n !handler.target &&\n (t?.tagName === 'INPUT' || t?.tagName === 'SELECT' || t?.tagName === 'TEXTAREA')\n ) {\n return;\n }\n if (t?.isContentEditable) {\n return;\n }\n if (\n (handler.withMeta && !(e.ctrlKey || e.metaKey)) ||\n (!handler.withMeta && (e.ctrlKey || e.metaKey))\n ) {\n return;\n }\n track('keypress', 'hotkeys', `${e.key} pressed`, handler.description);\n handler.callback(e);\n }\n }\n}\n\nexport const keyboard = new KeyboardController();\n", "/*!\n * @license\n * Copyright 2019-2020 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n// This file implements the behavior of the \"jump to symbol\" dialog for Go\n// package documentation, as well as the simple dialog that displays keyboard\n// shortcuts.\n\n// The DOM for the dialogs is at the bottom of static/frontend/unit/main/_modals.tmpl.\n// The CSS is in static/frontend/unit/main/_modals.css.\n\n// The dialog is activated by pressing the 'f' key. It presents a list\n// (#JumpDialog-list) of all Go symbols displayed in the documentation.\n// Entering text in the dialog's text box (#JumpDialog-filter) restricts the\n// list to symbols containing the text. Clicking on an symbol jumps to\n// its documentation.\n\n// This code is based on\n// https://go.googlesource.com/gddo/+/refs/heads/master/gddo-server/assets/site.js.\n// It was modified to remove the dependence on jquery and bootstrap.\n\nimport { keyboard } from '../keyboard/keyboard';\n\nexport function initModals(): void {\n const jumpDialog = document.querySelector('.JumpDialog');\n const jumpBody = jumpDialog?.querySelector('.JumpDialog-body');\n const jumpList = jumpDialog?.querySelector('.JumpDialog-list');\n const jumpFilter = jumpDialog?.querySelector('.JumpDialog-input');\n const doc = document.querySelector('.js-documentation');\n\n interface JumpListItem {\n link: HTMLAnchorElement;\n name: string;\n kind: string;\n lower: string;\n }\n\n let jumpListItems: JumpListItem[] | undefined; // All the symbols in the doc; computed only once.\n\n // collectJumpListItems returns a list of items, one for each symbol in the\n // documentation on the current page.\n //\n // It uses the data-kind attribute generated in the documentation HTML to find\n // the symbols and their id attributes.\n //\n // If there are no data-kind attributes, then we have older doc; fall back to\n // a less precise method.\n function collectJumpListItems() {\n const items = [];\n if (!doc) return;\n for (const el of doc.querySelectorAll('[data-kind]')) {\n items.push(newJumpListItem(el));\n }\n\n // Clicking on any of the links closes the dialog.\n for (const item of items) {\n item.link.addEventListener('click', function () {\n jumpDialog?.close();\n });\n }\n // Sort case-insensitively by symbol name.\n items.sort(function (a, b) {\n return a.lower.localeCompare(b.lower);\n });\n return items;\n }\n\n // newJumpListItem creates a new item for the DOM element el.\n // An item is an object with:\n // - name: the element's id (which is the symbol name)\n // - kind: the element's kind (function, variable, etc.),\n // - link: a link ('a' tag) to the element\n // - lower: the name in lower case, just for sorting\n function newJumpListItem(el: Element): JumpListItem {\n const a = document.createElement('a');\n const name = el.getAttribute('id');\n a.setAttribute('href', '#' + name);\n a.setAttribute('tabindex', '-1');\n a.setAttribute('data-gtmc', 'jump to link');\n const kind = el.getAttribute('data-kind');\n return {\n link: a,\n name: name ?? '',\n kind: kind ?? '',\n lower: name?.toLowerCase() ?? '', // for sorting\n };\n }\n\n let lastFilterValue: string; // The last contents of the filter text box.\n let activeJumpItem = -1; // The index of the currently active item in the list.\n\n // updateJumpList sets the elements of the dialog list to\n // everything whose name contains filter.\n function updateJumpList(filter: string) {\n lastFilterValue = filter;\n if (!jumpListItems) {\n jumpListItems = collectJumpListItems();\n }\n setActiveJumpItem(-1);\n\n // Remove all children from list.\n while (jumpList?.firstChild) {\n jumpList.firstChild.remove();\n }\n\n if (filter) {\n // A filter is set. We treat the filter as a substring that can appear in\n // an item name (case insensitive), and find the following matches - in\n // order of priority:\n //\n // 1. Exact matches (the filter matches the item's name exactly)\n // 2. Prefix matches (the item's name starts with filter)\n // 3. Infix matches (the filter is a substring of the item's name)\n const filterLowerCase = filter.toLowerCase();\n\n const exactMatches = [];\n const prefixMatches = [];\n const infixMatches = [];\n\n // makeLinkHtml creates the link name HTML for a list item. item is the DOM\n // item. item.name.substr(boldStart, boldEnd) will be bolded.\n const makeLinkHtml = (item: JumpListItem, boldStart: number, boldEnd: number) => {\n return (\n item.name.substring(0, boldStart) +\n '' +\n item.name.substring(boldStart, boldEnd) +\n '' +\n item.name.substring(boldEnd)\n );\n };\n\n for (const item of jumpListItems ?? []) {\n const nameLowerCase = item.name.toLowerCase();\n\n if (nameLowerCase === filterLowerCase) {\n item.link.innerHTML = makeLinkHtml(item, 0, item.name.length);\n exactMatches.push(item);\n } else if (nameLowerCase.startsWith(filterLowerCase)) {\n item.link.innerHTML = makeLinkHtml(item, 0, filter.length);\n prefixMatches.push(item);\n } else {\n const index = nameLowerCase.indexOf(filterLowerCase);\n if (index > -1) {\n item.link.innerHTML = makeLinkHtml(item, index, index + filter.length);\n infixMatches.push(item);\n }\n }\n }\n\n for (const item of exactMatches.concat(prefixMatches).concat(infixMatches)) {\n jumpList?.appendChild(item.link);\n }\n } else {\n if (!jumpListItems || jumpListItems.length === 0) {\n const msg = document.createElement('i');\n msg.innerHTML = 'There are no symbols on this page.';\n jumpList?.appendChild(msg);\n }\n // No filter set; display all items in their existing order.\n for (const item of jumpListItems ?? []) {\n item.link.innerHTML = item.name + ' ' + item.kind + '';\n jumpList?.appendChild(item.link);\n }\n }\n\n if (jumpBody) {\n jumpBody.scrollTop = 0;\n }\n if (jumpListItems?.length && jumpList && jumpList.children.length > 0) {\n setActiveJumpItem(0);\n }\n }\n\n // Set the active jump item to n.\n function setActiveJumpItem(n: number) {\n const cs = jumpList?.children as HTMLCollectionOf | null | undefined;\n if (!cs || !jumpBody) {\n return;\n }\n if (activeJumpItem >= 0) {\n cs[activeJumpItem].classList.remove('JumpDialog-active');\n }\n if (n >= cs.length) {\n n = cs.length - 1;\n }\n if (n >= 0) {\n cs[n].classList.add('JumpDialog-active');\n\n // Scroll so the active item is visible.\n // For some reason cs[n].scrollIntoView() doesn't behave as I'd expect:\n // it moves the entire dialog box in the viewport.\n\n // Get the top and bottom of the active item relative to jumpBody.\n const activeTop = cs[n].offsetTop - cs[0].offsetTop;\n const activeBottom = activeTop + cs[n].clientHeight;\n if (activeTop < jumpBody.scrollTop) {\n // Off the top; scroll up.\n jumpBody.scrollTop = activeTop;\n } else if (activeBottom > jumpBody.scrollTop + jumpBody.clientHeight) {\n // Off the bottom; scroll down.\n jumpBody.scrollTop = activeBottom - jumpBody.clientHeight;\n }\n }\n activeJumpItem = n;\n }\n\n // Increment the activeJumpItem by delta.\n function incActiveJumpItem(delta: number) {\n if (activeJumpItem < 0) {\n return;\n }\n let n = activeJumpItem + delta;\n if (n < 0) {\n n = 0;\n }\n setActiveJumpItem(n);\n }\n\n // Pressing a key in the filter updates the list (if the filter actually changed).\n jumpFilter?.addEventListener('keyup', function () {\n if (jumpFilter.value.toUpperCase() != lastFilterValue.toUpperCase()) {\n updateJumpList(jumpFilter.value);\n }\n });\n\n // Pressing enter in the filter selects the first element in the list.\n jumpFilter?.addEventListener('keydown', function (event) {\n const upArrow = 38;\n const downArrow = 40;\n const enterKey = 13;\n switch (event.which) {\n case upArrow:\n incActiveJumpItem(-1);\n event.preventDefault();\n break;\n case downArrow:\n incActiveJumpItem(1);\n event.preventDefault();\n break;\n case enterKey:\n if (activeJumpItem >= 0) {\n if (jumpList) {\n (jumpList.children[activeJumpItem] as HTMLElement).click();\n event.preventDefault();\n }\n }\n break;\n }\n });\n\n const shortcutsDialog = document.querySelector('.ShortcutsDialog');\n\n // - Pressing 'f' or 'F' opens the jump-to-symbol dialog.\n // - Pressing '?' opens up the shortcut dialog.\n // Ignore a keypress if a dialog is already open, or if it is pressed on a\n // component that wants to consume it.\n keyboard\n .on('f', 'open jump to modal', e => {\n if (jumpDialog?.open || shortcutsDialog?.open) {\n return;\n }\n e.preventDefault();\n if (jumpFilter) {\n jumpFilter.value = '';\n }\n jumpDialog?.showModal?.();\n jumpFilter?.focus();\n updateJumpList('');\n })\n .on('?', 'open shortcuts modal', () => {\n if (jumpDialog?.open || shortcutsDialog?.open) {\n return;\n }\n shortcutsDialog?.showModal?.();\n });\n\n const jumpOutlineInput = document.querySelector('.js-jumpToInput');\n if (jumpOutlineInput) {\n jumpOutlineInput.addEventListener('click', () => {\n if (jumpFilter) {\n jumpFilter.value = '';\n }\n updateJumpList('');\n if (jumpDialog?.open || shortcutsDialog?.open) {\n return;\n }\n jumpDialog?.showModal?.();\n jumpFilter?.focus();\n });\n }\n\n document.querySelector('.js-openShortcuts')?.addEventListener('click', () => {\n shortcutsDialog?.showModal?.();\n });\n}\n", "/**\n * @license\n * Copyright 2022 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { abs } from '../../shared/base-path/base-path';\n\n/**\n * Left Navigation.\n */\nexport const initJumpLinks = async function () {\n // pathname \u5728\u6302 -base-path=/pkgsitex \u65F6\u662F \"/pkgsitex/about\"\u2014\u2014\u6BD4\u8F83\u5217\u8868\u4E5F\u5F97\u5E26\u524D\u7F00\n const pagesWithJumpLinks = [abs('/about')];\n if (!pagesWithJumpLinks.includes(window.location.pathname)) {\n // stop the file from doing anything else if the page doesn't have jumplinks\n return;\n }\n\n // these might be generated or not so don't grab references to the elements until actually need them.\n const titles = 'h2, h3, h4';\n const nav = '.LeftNav a';\n // these are always in the dom so we can get them now and throw errors if they're not.\n const leftNav = document.querySelector('.LeftNav');\n const siteContent = document.querySelector('.go-Content');\n let isObserverDisabled = false;\n\n /**\n * El function\n * @example el('h1', {className: 'title'}, 'Welcome to the site');\n * @example el('ul', {className: 'list'}, el('li', {}, 'Item one'), el('li', {}, 'Item two'), el('li', {}, 'Item three'));\n * @example el('img', {src: '/url.svg'});\n */\n function el(\n type = '',\n props: { [key: string]: string } = {},\n ...children: (HTMLElement | HTMLElement[] | string | undefined)[]\n ) {\n // Error, no type declared.\n if (!type) {\n throw new Error('Provide `type` to create document element.');\n }\n\n // Create element with optional attribute props\n const docEl = Object.assign(document.createElement(type), props);\n\n // Children: array containing strings or elements\n children.forEach(child => {\n if (typeof child === 'string') {\n docEl.appendChild(document.createTextNode(child));\n } else if (Array.isArray(child)) {\n child.forEach(c => docEl.appendChild(c));\n } else if (child instanceof HTMLElement) {\n docEl.appendChild(child);\n }\n });\n\n return docEl;\n }\n /** Build Nav if data hydrate is present. */\n function buildNav() {\n return new Promise((resolve, reject) => {\n let navItems: { id: string; label: string; subnav?: { id: string; label: string }[] }[] = [];\n let elements: HTMLElement[] = [];\n\n if (!siteContent || !leftNav) {\n return reject('.SiteContent not found.');\n }\n if (leftNav instanceof HTMLElement && !leftNav?.dataset?.hydrate) {\n return resolve(true);\n }\n\n for (const title of siteContent.querySelectorAll(titles)) {\n if (title instanceof HTMLElement && !title?.dataset?.ignore) {\n switch (title.tagName) {\n case 'H2':\n navItems = [\n ...navItems,\n {\n id: title.id,\n label: title?.dataset?.title ? title.dataset.title : title.textContent ?? '',\n },\n ];\n break;\n\n case 'H3':\n case 'H4':\n if (!navItems[navItems.length - 1]?.subnav) {\n navItems[navItems.length - 1].subnav = [\n {\n id: title.id,\n label: title?.dataset?.title ? title.dataset.title : title.textContent ?? '',\n },\n ];\n } else if (navItems[navItems.length - 1].subnav) {\n navItems[navItems.length - 1].subnav?.push({\n id: title.id,\n label: title?.dataset?.title ? title.dataset.title : title.textContent ?? '',\n });\n }\n break;\n }\n }\n }\n\n for (const navItem of navItems) {\n const link = el('a', { href: '#' + navItem.id }, el('span', {}, navItem.label));\n elements = [...elements, link];\n if (navItem?.subnav) {\n let subLinks: HTMLElement[] = [];\n for (const subnavItem of navItem.subnav) {\n const subItem = el(\n 'li',\n {},\n el(\n 'a',\n { href: '#' + subnavItem.id },\n el('img', { src: abs('/static/frontend/about/dot.svg'), width: '5', height: '5' }),\n el('span', {}, subnavItem.label)\n )\n );\n subLinks = [...subLinks, subItem];\n }\n const list = el('ul', { className: 'LeftSubnav' }, subLinks);\n elements = [...elements, list];\n }\n }\n\n elements.forEach(element => leftNav.appendChild(element));\n\n return resolve(true);\n });\n }\n /**\n * Set the correct active element.\n */\n function setNav() {\n return new Promise(resolve => {\n if (!document.querySelectorAll(nav)) return resolve(true);\n for (const a of document.querySelectorAll(nav)) {\n if (a instanceof HTMLAnchorElement && a.href === location.href) {\n setElementActive(a);\n break;\n }\n }\n resolve(true);\n });\n }\n /** resetNav: removes all .active from nav elements */\n function resetNav() {\n return new Promise(resolve => {\n if (!document.querySelectorAll(nav)) return resolve(true);\n for (const a of document.querySelectorAll(nav)) {\n a.classList.remove('active');\n }\n resolve(true);\n });\n }\n /** setElementActive: controls resetting nav and highlighting the appropriate nav items */\n function setElementActive(element: HTMLAnchorElement) {\n if (element instanceof HTMLAnchorElement) {\n resetNav().then(() => {\n element.classList.add('active');\n const parent = element?.parentNode?.parentNode;\n if (parent instanceof HTMLElement && parent?.classList?.contains('LeftSubnav')) {\n parent.previousElementSibling?.classList.add('active');\n }\n });\n }\n }\n /** setLinkManually: disables observer and selects the clicked nav item. */\n function setLinkManually() {\n delayObserver();\n const link = document.querySelector('[href=\"' + location.hash + '\"]');\n if (link instanceof HTMLAnchorElement) {\n setElementActive(link);\n }\n }\n /** delayObserver: Quick on off switch for intersection observer. */\n function delayObserver() {\n isObserverDisabled = true;\n setTimeout(() => {\n isObserverDisabled = false;\n }, 200);\n }\n /** observeSections: kicks off observation of titles as well as manual clicks with hashchange */\n function observeSections() {\n window.addEventListener('hashchange', setLinkManually);\n\n if (siteContent?.querySelectorAll(titles)) {\n const callback: IntersectionObserverCallback = entries => {\n if (!isObserverDisabled && Array.isArray(entries) && entries.length > 0) {\n for (const entry of entries) {\n if (entry.isIntersecting && entry.target instanceof HTMLElement) {\n const { id } = entry.target;\n const link = document.querySelector('[href=\"#' + id + '\"]');\n if (link instanceof HTMLAnchorElement) {\n setElementActive(link);\n }\n break;\n }\n }\n }\n };\n // rootMargin is important when multiple sections are in the observable area **on page load**.\n // they will still be highlighted on scroll because of the root margin.\n const ob = new IntersectionObserver(callback, {\n threshold: 0,\n rootMargin: '0px 0px -50% 0px',\n });\n for (const title of siteContent.querySelectorAll(titles)) {\n if (title instanceof HTMLElement && !title?.dataset?.ignore) {\n ob.observe(title);\n }\n }\n }\n }\n\n try {\n await buildNav();\n await setNav();\n if (location.hash) {\n delayObserver();\n }\n observeSections();\n } catch (e) {\n if (e instanceof Error) {\n console.error(e.message);\n } else {\n console.error(e);\n }\n }\n};\n", "/**\n * @license\n * Copyright 2020 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { registerHeaderListeners, registerSearchFormListeners } from 'static/shared/header/header';\nimport { CarouselController } from 'static/shared/carousel/carousel';\nimport { ClipboardController } from 'static/shared/clipboard/clipboard';\nimport { ToolTipController } from 'static/shared/tooltip/tooltip';\nimport { SelectNavController } from 'static/shared/outline/select';\nimport { ModalController } from 'static/shared/modal/modal';\nimport { initModals } from 'static/shared/jump/jump';\n\nimport { keyboard } from 'static/shared/keyboard/keyboard';\nimport * as analytics from 'static/shared/analytics/analytics';\nimport { initJumpLinks } from './about/index';\n\nwindow.addEventListener('load', () => {\n for (const el of document.querySelectorAll('.js-clipboard')) {\n new ClipboardController(el);\n }\n\n for (const el of document.querySelectorAll('.js-modal')) {\n new ModalController(el);\n }\n\n for (const t of document.querySelectorAll('.js-tooltip')) {\n new ToolTipController(t);\n }\n\n for (const el of document.querySelectorAll('.js-selectNav')) {\n new SelectNavController(el);\n }\n\n for (const el of document.querySelectorAll('.js-carousel')) {\n new CarouselController(el);\n }\n\n for (const el of document.querySelectorAll('.js-toggleTheme')) {\n el.addEventListener('click', () => {\n toggleTheme();\n });\n }\n\n if (document.querySelector('.js-gtmID')?.dataset.gtmid && window.dataLayer) {\n analytics.func(function () {\n removeUTMSource();\n });\n } else {\n removeUTMSource();\n }\n\n registerHeaderListeners();\n registerSearchFormListeners();\n initModals();\n initJumpLinks();\n registerCookieNotice();\n});\n\n// Pressing '/' focuses the search box\nkeyboard.on('/', 'focus search', e => {\n const searchInput = Array.from(\n document.querySelectorAll('.js-searchFocus')\n ).pop();\n // Favoring the Firefox quick find feature over search input\n // focus. See: https://github.com/golang/go/issues/41093.\n if (searchInput && !window.navigator.userAgent.includes('Firefox')) {\n e.preventDefault();\n searchInput.focus();\n }\n});\n\n// Pressing 'y' changes the browser URL to the canonical URL\n// without triggering a reload.\nkeyboard.on('y', 'set canonical url', () => {\n let canonicalURLPath = document.querySelector('.js-canonicalURLPath')?.dataset[\n 'canonicalUrlPath'\n ];\n if (canonicalURLPath && canonicalURLPath !== '') {\n const fragment = window.location.hash;\n if (fragment) {\n canonicalURLPath += fragment;\n }\n window.history.replaceState(null, '', canonicalURLPath);\n }\n});\n\n/**\n * setupGoogleTagManager initializes Google Tag Manager.\n */\n(function setupGoogleTagManager() {\n analytics.track({\n 'gtm.start': new Date().getTime(),\n event: 'gtm.js',\n });\n})();\n\n/**\n * removeUTMSource removes the utm_source GET parameter if present.\n * This is done using JavaScript, so that the utm_source is still\n * captured by Google Analytics.\n */\nfunction removeUTMSource() {\n const urlParams = new URLSearchParams(window.location.search);\n const utmSource = urlParams.get('utm_source');\n if (utmSource !== 'gopls' && utmSource !== 'godoc' && utmSource !== 'pkggodev') {\n return;\n }\n\n /** Strip the utm_source query parameter and replace the URL. **/\n const newURL = new URL(window.location.href);\n urlParams.delete('utm_source');\n newURL.search = urlParams.toString();\n window.history.replaceState(null, '', newURL.toString());\n}\n\n/**\n * toggleTheme switches the preferred color scheme between auto, light, and dark.\n */\nfunction toggleTheme() {\n let nextTheme = 'dark';\n const theme = document.documentElement.getAttribute('data-theme');\n if (theme === 'dark') {\n nextTheme = 'light';\n } else if (theme === 'light') {\n nextTheme = 'auto';\n }\n let domain = '';\n if (location.hostname === 'go.dev' || location.hostname.endsWith('.go.dev')) {\n domain = 'domain=.go.dev;';\n }\n document.documentElement.setAttribute('data-theme', nextTheme);\n document.cookie = `prefers-color-scheme=${nextTheme};${domain}path=/;max-age=31536000;`;\n}\n\n/**\n * registerCookieNotice makes the cookie notice visible and adds listeners to dismiss it\n * if it has not yet been acknowledge by the user.\n */\nfunction registerCookieNotice() {\n const themeCookie = document.cookie.match(/cookie-consent=true/);\n if (!themeCookie) {\n const notice = document.querySelector('.js-cookieNotice');\n const button = notice?.querySelector('button');\n notice?.classList.add('Cookie-notice--visible');\n button?.addEventListener('click', () => {\n let domain = '';\n if (location.hostname === 'go.dev' || location.hostname.endsWith('.go.dev')) {\n // Apply the cookie to *.go.dev.\n domain = 'domain=.go.dev;';\n }\n document.cookie = `cookie-consent=true;${domain}path=/;max-age=31536000`;\n notice?.remove();\n });\n }\n}\n"], + "mappings": "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", + "names": ["registerHeaderListeners", "header", "menuItemHover", "e", "target", "forced", "toggleForcedOpen", "_a", "_b", "isForced", "currentTarget", "closeSubmenuOnEsc", "event", "forcedOpenItem", "headerbuttons", "button", "isActive", "handleNavigationDrawerInactive", "handleNavigationDrawerActive", "scrim", "subnav", "getNavigationDrawerMenuItems", "navigationDrawer", "menuItems", "anchorEl", "getNavigationDrawerIsSubnav", "parentMenuItem", "item", "handleMenuItemTabLeftFactory", "handleMenuItemTabRightFactory", "prepMobileNavigationDrawer", "isSubnav", "parentLi", "submenu", "drawer", "registerSearchFormListeners", "searchForm", "expandSearch", "input", "headerLogo", "menuButton", "getBasePath", "_a", "abs", "p", "CarouselController", "el", "index", "d", "s", "_a", "i", "v", "_b", "arrows", "bp", "getBasePath", "dots", "li", "button", "ClipboardController", "el", "_a", "_b", "_c", "_d", "_e", "e", "TOOLTIP_SHOW_DURATION_MS", "text", "durationMs", "ToolTipController", "el", "e", "SelectNavController", "el", "e", "target", "href", "ModalController", "el", "button", "_a", "btn", "track", "event", "category", "action", "label", "_a", "func", "fn", "KeyboardController", "key", "description", "callback", "options", "_a", "_b", "handler", "t", "track", "keyboard", "initModals", "_a", "jumpDialog", "jumpBody", "jumpList", "jumpFilter", "doc", "jumpListItems", "collectJumpListItems", "items", "el", "newJumpListItem", "item", "a", "b", "name", "kind", "lastFilterValue", "activeJumpItem", "updateJumpList", "filter", "setActiveJumpItem", "filterLowerCase", "exactMatches", "prefixMatches", "infixMatches", "makeLinkHtml", "boldStart", "boldEnd", "nameLowerCase", "index", "msg", "n", "cs", "activeTop", "activeBottom", "incActiveJumpItem", "delta", "event", "shortcutsDialog", "keyboard", "e", "jumpOutlineInput", "initJumpLinks", "abs", "titles", "nav", "leftNav", "siteContent", "isObserverDisabled", "el", "type", "props", "children", "docEl", "child", "c", "buildNav", "resolve", "reject", "_a", "_b", "_c", "_d", "_e", "_f", "_g", "_h", "_i", "_j", "navItems", "elements", "title", "navItem", "link", "subLinks", "subnavItem", "subItem", "list", "element", "setNav", "a", "setElementActive", "resetNav", "parent", "setLinkManually", "delayObserver", "observeSections", "callback", "entries", "entry", "id", "ob", "e", "_a", "el", "ClipboardController", "ModalController", "t", "ToolTipController", "SelectNavController", "CarouselController", "toggleTheme", "func", "removeUTMSource", "registerHeaderListeners", "registerSearchFormListeners", "initModals", "initJumpLinks", "registerCookieNotice", "keyboard", "e", "searchInput", "canonicalURLPath", "fragment", "track", "urlParams", "utmSource", "newURL", "nextTheme", "theme", "domain", "notice", "button"] } diff --git a/static/frontend/frontend.tmpl b/static/frontend/frontend.tmpl index e2a7600fa..6e3d91c5d 100644 --- a/static/frontend/frontend.tmpl +++ b/static/frontend/frontend.tmpl @@ -5,7 +5,7 @@ --> - + + + {{template "header" .}} {{template "main" .}} diff --git a/static/frontend/homepage/homepage.tmpl b/static/frontend/homepage/homepage.tmpl index 384c996b3..37818e3c2 100644 --- a/static/frontend/homepage/homepage.tmpl +++ b/static/frontend/homepage/homepage.tmpl @@ -5,16 +5,16 @@ --> {{define "pre-content"}} - + {{end}} {{define "main"}}
    + src="{{abs `/static/shared/gopher/package-search-700x300.jpeg`}}" alt="Cartoon gopher typing">
    diff --git a/static/frontend/search/search.tmpl b/static/frontend/search/search.tmpl index f699dcd82..8eff9a2c2 100644 --- a/static/frontend/search/search.tmpl +++ b/static/frontend/search/search.tmpl @@ -13,12 +13,12 @@ {{end}} {{define "pre-content"}} - + {{end}} {{define "post-content"}} {{end}} @@ -42,7 +42,7 @@ {{define "search_symbol"}}
    Showing {{len $.Results}} matching {{.SearchModeSymbol}}s. - Search help + Search help
    {{if eq (len .Results) 0}} {{template "search_no_results" .}} @@ -54,7 +54,7 @@ {{define "search_no_results"}} {{template "gopher-airplane" "It looks like there are no matches for your search."}}

    - Need help? Check out tips for searching on pkg.go.dev. + Need help? Check out tips for searching on pkg.go.dev.

    {{end}} @@ -73,7 +73,7 @@ {{.SymbolName}} in - {{$r.PackagePath}} {{with $r.ChipText}}{{.}}{{end}} @@ -88,7 +88,7 @@ {{define "search_package"}}
    - Showing {{len .Results}} modules with matching packages. Search help + Showing {{len .Results}} modules with matching packages. Search help
    {{if eq (len .Results) 0}} {{template "search_no_results" .}} @@ -106,7 +106,7 @@

    - {{$v.Name}} ({{$v.PackagePath}}) @@ -126,7 +126,7 @@ {{.Heading}} {{$numLinks := (len .Links)}} {{range $i, $v := .Links}} - + {{$v.Body}}{{if lt $i (subtract $numLinks 1)}},{{end}} {{end}} @@ -139,7 +139,7 @@ {{range $i, $v := .Links}} {{if and (lt $i 5) (ne $v.Body "")}} {{$v.Body}} @@ -162,7 +162,7 @@ {{define "search_metadata"}}
    - + Imported by {{.NumImportedBy}} | @@ -172,7 +172,7 @@ | {{if .Licenses}} - + {{commaseparate .Licenses}} {{else}} @@ -190,7 +190,7 @@ {{- if and (lt $p.Limit $p.MaxLimit) (eq $p.Limit (len .Results)) -}} Show more results. {{- else -}} - See search help. + See search help. {{- end -}}
    {{end}} @@ -215,12 +215,12 @@
    diff --git a/static/frontend/subrepo/subrepo.tmpl b/static/frontend/subrepo/subrepo.tmpl index 2cf0c6d49..f01171bd0 100644 --- a/static/frontend/subrepo/subrepo.tmpl +++ b/static/frontend/subrepo/subrepo.tmpl @@ -10,7 +10,7 @@ {{end}} {{define "pre-content"}} - + {{end}} {{define "main"}} @@ -28,91 +28,91 @@

    diff --git a/static/frontend/unit/_header.tmpl b/static/frontend/unit/_header.tmpl index 641678b64..16a3c34c5 100644 --- a/static/frontend/unit/_header.tmpl +++ b/static/frontend/unit/_header.tmpl @@ -42,7 +42,7 @@ class="go-Icon go-Icon--accented" height="24" width="24" - src="/static/shared/icon/content_copy_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/content_copy_gm_grey_24dp.svg`}}" alt="" > @@ -56,7 +56,7 @@ {{define "unit-header-title"}}

    {{.Title}}

    {{range .PageLabels}} @@ -76,7 +76,7 @@ class="go-Icon go-Icon--accented" height="24" width="24" - src="/static/shared/icon/content_copy_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/content_copy_gm_grey_24dp.svg`}}" alt="" /> @@ -150,14 +150,14 @@ {{if $i}}, {{end}} {{$e.Type}} {{- end -}} - not legal advice {{end}} {{else}} None detected - not legal advice @@ -199,7 +199,7 @@
    ``` @@ -55,7 +55,7 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/search_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/search_gm_grey_24dp.svg`}}" alt="" /> @@ -82,7 +82,7 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/filter_list_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/filter_list_gm_grey_24dp.svg`}}" alt="" /> @@ -94,7 +94,7 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/search_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/search_gm_grey_24dp.svg`}}" alt="" /> @@ -110,7 +110,7 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/filter_list_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/filter_list_gm_grey_24dp.svg`}}" alt="" /> @@ -119,7 +119,7 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/search_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/search_gm_grey_24dp.svg`}}" alt="" /> diff --git a/static/shared/gopher/gopher.tmpl b/static/shared/gopher/gopher.tmpl index 456b79abd..850e0bfc9 100644 --- a/static/shared/gopher/gopher.tmpl +++ b/static/shared/gopher/gopher.tmpl @@ -6,7 +6,7 @@ {{define "gopher-airplane"}}
    - The Go Gopher {{if .}}

    {{.}}

    {{end}}
    diff --git a/static/shared/header/header.tmpl b/static/shared/header/header.tmpl index 4ae963b25..2978831cf 100644 --- a/static/shared/header/header.tmpl +++ b/static/shared/header/header.tmpl @@ -10,7 +10,7 @@
    @@ -161,42 +161,42 @@ aria-label="Get connected with google-groups (Opens in new window)" title="Get connected with google-groups (Opens in new window)" href="https://groups.google.com/g/golang-nuts"> - + - + - + - + - + - +

    @@ -213,7 +213,7 @@
    @@ -24,7 +24,7 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/alert_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/alert_gm_grey_24dp.svg`}}" alt="Warning" />  Retracted: This version of Syntax has been retracted. @@ -38,7 +38,7 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/alert_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/alert_gm_grey_24dp.svg`}}" alt="Alert" />  Critical error diff --git a/static/shared/modal/modal.md b/static/shared/modal/modal.md index cd1f8d413..473ebc24b 100644 --- a/static/shared/modal/modal.md +++ b/static/shared/modal/modal.md @@ -22,7 +22,7 @@ The size modifer class is optional. The base modal will grow to fit the inner co class="go-Icon" height="24" width="24" - src="/static/shared/icon/close_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/close_gm_grey_24dp.svg`}}" alt="" /> @@ -56,7 +56,7 @@ The size modifer class is optional. The base modal will grow to fit the inner co class="go-Icon" height="24" width="24" - src="/static/shared/icon/close_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/close_gm_grey_24dp.svg`}}" alt="" /> @@ -90,7 +90,7 @@ The size modifer class is optional. The base modal will grow to fit the inner co class="go-Icon" height="24" width="24" - src="/static/shared/icon/close_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/close_gm_grey_24dp.svg`}}" alt="" /> diff --git a/static/shared/playground/playground.ts b/static/shared/playground/playground.ts index c0dd0cf4b..dfc662fd2 100644 --- a/static/shared/playground/playground.ts +++ b/static/shared/playground/playground.ts @@ -11,6 +11,8 @@ // The CSS is in static/frontend/unit/main/_doc.css +import { abs } from '../base-path/base-path'; + /** * CSS classes used by PlaygroundExampleController */ @@ -232,7 +234,7 @@ require ${moduleVars.modulepath} ${moduleVars.version} this.setOutputText('Waiting for remote server…'); - fetch('/play/share', { + fetch(abs('/play/share'), { method: 'POST', body: this.getCodeWithModFile(), }) @@ -255,7 +257,7 @@ require ${moduleVars.modulepath} ${moduleVars.version} const body = new FormData(); body.append('body', this.inputEl?.value ?? ''); - fetch('/play/fmt', { + fetch(abs('/play/fmt'), { method: 'POST', body: body, }) @@ -278,7 +280,7 @@ require ${moduleVars.modulepath} ${moduleVars.version} private handleRunButtonClick() { this.setOutputText('Waiting for remote server…'); - fetch('/play/compile', { + fetch(abs('/play/compile'), { method: 'POST', body: JSON.stringify({ body: this.getCodeWithModFile(), version: 2 }), }) diff --git a/static/shared/tooltip/tooltip.md b/static/shared/tooltip/tooltip.md index 7c6dc2cec..bcd92fa52 100644 --- a/static/shared/tooltip/tooltip.md +++ b/static/shared/tooltip/tooltip.md @@ -10,7 +10,7 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/help_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/help_gm_grey_24dp.svg`}}" alt="" /> diff --git a/static/shared/vuln/vuln.tmpl b/static/shared/vuln/vuln.tmpl index 71df6074e..e4d865469 100644 --- a/static/shared/vuln/vuln.tmpl +++ b/static/shared/vuln/vuln.tmpl @@ -10,10 +10,10 @@ class="go-Icon" height="24" width="24" - src="/static/shared/icon/alert_gm_grey_24dp.svg" + src="{{abs `/static/shared/icon/alert_gm_grey_24dp.svg`}}" alt="Alert" />  - {{.ID}}: {{.Details}} + {{.ID}}: {{.Details}} {{end}} diff --git a/static/worker/excluded.tmpl b/static/worker/excluded.tmpl index 91e8ae7d0..36b1b1543 100644 --- a/static/worker/excluded.tmpl +++ b/static/worker/excluded.tmpl @@ -7,7 +7,7 @@ - + {{.Env}} Worker Excluded diff --git a/static/worker/index.tmpl b/static/worker/index.tmpl index 432177520..06b1b6d6c 100644 --- a/static/worker/index.tmpl +++ b/static/worker/index.tmpl @@ -7,7 +7,7 @@ - + {{.Env}} Worker @@ -30,39 +30,39 @@

    - Modules | - Traces | - RPCs | - Metrics | - Excluded + Modules | + Traces | + RPCs | + Metrics | + Excluded

    - + -
    +
    -
    +
    -
    +
    -
    + @@ -152,7 +152,7 @@ {{.RequestInfo.State.Load}} {{with .RequestInfo.TraceID}} Logs - Cancel + Cancel {{end}} {{end}} @@ -179,7 +179,7 @@ {{.State.Load}} {{with .TraceID}} Logs - Cancel + Cancel {{end}} {{end}} @@ -224,5 +224,5 @@ s.src = src; document.head.appendChild(s); } - loadScript("/static/worker/worker.js"); + loadScript((document.documentElement.dataset.basePath || "") + "/static/worker/worker.js"); diff --git a/static/worker/versions.tmpl b/static/worker/versions.tmpl index 1e70bef42..2faa0a0e9 100644 --- a/static/worker/versions.tmpl +++ b/static/worker/versions.tmpl @@ -40,13 +40,13 @@ - + {{.Env}} Worker

    {{.Env}} Worker

    All times in America/New_York.

    -

    Home

    +

    Home

    Statistics