From c7a3db44b6695658db9edb6498061ea0be8c58b2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Nov 2026 07:12:30 +0000 Subject: [PATCH 1/7] chore: upgrade OINK to 1.5.1 and enable Duo View - pin OINK and refresh the reviewed override inventory - enable native Duo View and isolate site runtimes inside panes - add real-page matrix, visual, staging and rollback validation --- assets/js/hugegraph-shell.js | 2 + assets/js/kapa-adapter.js | 2 + assets/js/language-preference.js | 2 + go.mod | 2 +- go.sum | 4 +- hugo.yaml | 2 + layouts/_partials/actions/manifest.html | 2 +- netlify.toml | 6 +- scripts/oink-features.md | 23 ++- scripts/oink-overrides.json | 49 ++--- scripts/oink-upgrade.md | 89 ++++++++- tests/e2e/duo-view.spec.js | 246 ++++++++++++++++++++++++ tests/e2e/package.json | 2 +- tests/e2e/visual.spec.js | 16 +- tests/ui-ai/ui-contract.test.cjs | 33 ++++ 15 files changed, 440 insertions(+), 40 deletions(-) create mode 100644 tests/e2e/duo-view.spec.js diff --git a/assets/js/hugegraph-shell.js b/assets/js/hugegraph-shell.js index 2cbf2e7028..5773d7e490 100644 --- a/assets/js/hugegraph-shell.js +++ b/assets/js/hugegraph-shell.js @@ -7,6 +7,8 @@ (function (global) { 'use strict'; + if (typeof window !== 'undefined' && window.OinkComparePane) return; + var versionExecutorRegistries = new WeakSet(); function readConfig(documentObject) { diff --git a/assets/js/kapa-adapter.js b/assets/js/kapa-adapter.js index 35a7d9f2fa..402786631c 100644 --- a/assets/js/kapa-adapter.js +++ b/assets/js/kapa-adapter.js @@ -7,6 +7,8 @@ (function (global) { 'use strict'; + if (typeof window !== 'undefined' && window.OinkComparePane) return; + var BUNDLE_URL = 'https://widget.kapa.ai/kapa-widget.bundle.js'; var TIMEOUT_MS = 5000; diff --git a/assets/js/language-preference.js b/assets/js/language-preference.js index 871ca2d968..89ff514fe1 100644 --- a/assets/js/language-preference.js +++ b/assets/js/language-preference.js @@ -2,6 +2,8 @@ (function () { 'use strict'; + if (typeof window !== 'undefined' && window.OinkComparePane) return; + var node = document.getElementById('hg-language-config'); if (!node) return; var config = JSON.parse(node.textContent); diff --git a/go.mod b/go.mod index ed7705a94f..89ea065047 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/apache/hugegraph-doc go 1.27.3 -require github.com/pgsty/oink v1.4.0 +require github.com/pgsty/oink v1.5.1 diff --git a/go.sum b/go.sum index e4f4337787..c0b266540e 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/pgsty/oink v1.4.0 h1:EAYCzReap9+nrXuZUogmrXEv62ulJUaSNK1zpGRwM5Y= -github.com/pgsty/oink v1.4.0/go.mod h1:GBKnERMfbKlRSFuKCa+kV7sehSAT7kAlYHxwxglg9Jw= +github.com/pgsty/oink v1.5.1 h1:PgOZ7gnbkoJKZMFZ4YvCVXNL7HDiMV0LJiNYYhxiRmw= +github.com/pgsty/oink v1.5.1/go.mod h1:GBKnERMfbKlRSFuKCa+kV7sehSAT7kAlYHxwxglg9Jw= diff --git a/hugo.yaml b/hugo.yaml index b941e19696..4757992598 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -159,6 +159,8 @@ params: reading_time: false feedback: false keyboard_nav: true + # OINK native Duo View is exposed only for real Docs/Book/Blog translations. + compare: true module: imports: diff --git a/layouts/_partials/actions/manifest.html b/layouts/_partials/actions/manifest.html index 3029720f3b..3030af534b 100644 --- a/layouts/_partials/actions/manifest.html +++ b/layouts/_partials/actions/manifest.html @@ -1,5 +1,5 @@ {{- $outputFormat := lower (.Store.Get "tdOutputFormat" | default "html") -}} -{{- $context := partialCached "actions/context.html" . .Site.BaseURL .Site.Language.Lang .RelPermalink $outputFormat -}} +{{- $context := partialCached "actions/context.html" . .Site.BaseURL .Site.Language.Lang (partial "compare/page-key.html" .) $outputFormat -}} {{- $versionOptions := partial "version-options.html" . -}} {{- $actions := slice -}} {{- range $context.actions -}} diff --git a/netlify.toml b/netlify.toml index 7de52a48aa..d329d623ad 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,9 +1,9 @@ [build] -command = "hugo --cleanDestinationDir --gc --minify --environment production --printPathWarnings --printI18nWarnings --panicOnWarning" +command = "hugo --cleanDestinationDir --gc --minify --environment production --printPathWarnings --printI18nWarnings" publish = "public" [build.environment] -GO_VERSION = "1.27.0" -HUGO_VERSION = "0.165.0" +GO_VERSION = "1.27.3" +HUGO_VERSION = "0.168.0" HUGO_ENVIRONMENT = "production" diff --git a/scripts/oink-features.md b/scripts/oink-features.md index 09e02b09bd..c7799c63f4 100644 --- a/scripts/oink-features.md +++ b/scripts/oink-features.md @@ -26,6 +26,24 @@ section's child list. Handwritten cards suit cross-section recommendations. Keep prerequisites and warnings visible; use a native `> [!DETAILS]- Title` callout for supplemental detail. +## Native Duo View + +OINK 1.5.1 calls the feature **Duo View** in English and **双列视图** in +Chinese. Use **Sync Scroll / 同步滚动** for its optional approximate heading +alignment. The configuration and action IDs remain `params.ui.compare` and +`compare_translations`; do not rename those API identifiers. + +HugeGraph enables the native feature globally. OINK discovers only real, +same-origin HTML translations of Docs, Book and Blog pages, loads the second +page on demand, and hides the entry when no valid translation exists. Do not +add a parallel route map, copied reader template, machine-generated placeholder +translation or site-specific compare controller. Page-level heading IDs may be +aligned incrementally where a real reading problem is observed. + +Site-level integrations loaded from hooks must return immediately when +`window.OinkComparePane` is true. Content runtimes remain owned by OINK so tabs, +diagrams, code controls and fragment links continue to work inside each pane. + ## Common authoring patterns Block attribute lines must touch the list, table or image they describe. @@ -225,7 +243,8 @@ The recorder is an authoring tool, not a build dependency. fallbacks. Check RSS where the component appears in a feed. A successful Hugo render alone does not establish compatibility. -The [1.4.0 release notes](https://oink.pgsty.com/zh/blog/release/1.4.0/) and -[tagged source](https://github.com/pgsty/oink/tree/v1.4.0) are the reference for +The [1.5.1 release](https://github.com/pgsty/oink/releases/tag/v1.5.1), +[changelog](https://github.com/pgsty/oink/blob/v1.5.1/CHANGELOG.md) and +[tagged source](https://github.com/pgsty/oink/tree/v1.5.1) are the reference for this upgrade. Consult the pinned release before extending a component; the upstream website can describe a newer version. diff --git a/scripts/oink-overrides.json b/scripts/oink-overrides.json index 1cf661b858..7474d26362 100644 --- a/scripts/oink-overrides.json +++ b/scripts/oink-overrides.json @@ -1,46 +1,47 @@ { - "version": "v1.4.0", + "version": "v1.5.1", "files": { "assets/js/appearance.js": "66fa6a6fea304c235a8b11ca714478b221d8fdc8db0e9fffdfe5f6a2524760ff", - "assets/js/command-palette.js": "f9c7b4aa5defca431b98984b78242fd6a7b55957b571ca3a07428c660879684f", + "assets/js/command-palette.js": "2d1bd325480e5d117fe9f4aca78569846fdb6b19372453c3b24b468b3d362528", "assets/js/dark-mode.js": "d0f19cd7011f05debab7ffadc4c268321d4845f12be781d090d2e461978c03b2", - "assets/js/docs-shell.js": "2f45ec115a1aef3b2e73bf8e36d54d50a6bca0bd6523fdc47f42e52e0fb399aa", - "assets/js/search-engine.js": "ef7796c7157a8a7351a9296c295bd95adb834258a29c3d955ee260712cd812a1", - "assets/js/search.js": "2afb45a6074881969a9ee1eb1357eea3b49fdcff3710940e7cd18f4d58e821f7", - "assets/js/sidebar-nav.js": "668a1b27e2dcc3311b4aa04982325e97b625ca2d160c8322d1151b0dd2387ca8", - "assets/js/sidebar-state.js": "cef728b2897f55bf93ec1d1702cdac87b0971fb458cbd2d378a8c66d71e1da8f", + "assets/js/docs-shell.js": "c44358e29573f32bc54165eed0e8234d4f7598bf4ad6dbd47af6d6b0d7ad827f", + "assets/js/search-engine.js": "9ca8b850f126ee817a348df97244a916b31611054930cf8e13894363391e87ae", + "assets/js/search.js": "69d9316572736644a9a46f915ab7576e7e3a56aed81a5151cdd1ffe0d5c17a93", + "assets/js/sidebar-nav.js": "8d848619d326934d92d87354d3d1511db5c65b5122af136f161d6af1fb070d39", + "assets/js/sidebar-state.js": "ae1dc392c52457fcd42765914ed01c3b25116101bb7d0c548b43ff9f0019d771", "assets/js/surface-coordinator.js": "fa9c633ed305449fe7009caaa6c3e5e24027050a9ac4bcb77261473dc9faedd2", "assets/scss/_styles_project.scss": "65a49b8298575f61a1b03ef0ab95e949ad147b62db9cb386170ecec09fe64e87", "assets/scss/_variables_project.scss": "65a49b8298575f61a1b03ef0ab95e949ad147b62db9cb386170ecec09fe64e87", "assets/scss/_variables_project_after_bs.scss": "65a49b8298575f61a1b03ef0ab95e949ad147b62db9cb386170ecec09fe64e87", "assets/scss/main.scss": "c740f25f00be4b3927233c295602dbfbb5550a701610bd0d383c2220ae9e5596", "assets/scss/redoc.scss": "70db4b7c1bd10d3dfe410badd298f08bee4f6a525c1ca70bbb7d349f931c8a5b", - "assets/scss/td/_accessibility.scss": "92d5c2de3df4273fd178579c63f8a28ace52d31de581c2195c3a62f67a06621b", + "assets/scss/td/_accessibility.scss": "d0c5234e9114c8d8867d9dfdbcf6bafe0ba3aad76bbcfc2fe4f87df3cd0382d7", "assets/scss/td/_alerts.scss": "46354249f30bd669474c7a81188eae309bb80cb28006dc4f8b1a7fd27b25a896", "assets/scss/td/_appearance.scss": "03c3048613f75915a9b11040cac5d43835a53e7f2928398a8af31ce625745c8c", "assets/scss/td/_backlinks.scss": "624a003051a3b2ffb18ba4f85ceba0d76c29574cd12eac9e4164c283a74663b1", "assets/scss/td/_blog.scss": "7e828f5677061536a9b7ff462757489426c4299e247f770adeb90cfbaa7ac92c", - "assets/scss/td/_book.scss": "327705bf2730b780d36b824c166f8dfdcfab32124bd72eebb50db372ceafda89", - "assets/scss/td/_brand.scss": "a38548973acdcd31f67fb9d32203f11c56c1974ccf12003afe3688262eaebbb8", + "assets/scss/td/_book.scss": "112cc86a9919d85521291b06bed36bf509f6d7c013bc1441832be58a81b52380", + "assets/scss/td/_brand.scss": "36295b57add2aacbedba0b9c9a3187f66351950b8668976ef3fc737d4c6bf515", "assets/scss/td/_breadcrumb.scss": "19bba95695f4a5f5c12642917649eeb62cb1740591aa42272227dca7b11e9318", "assets/scss/td/_byline.scss": "7e32a9f578aa01ab8c9a8179c2ee8f88dcbf05fedb440f6789f5f6eb5b3527a6", "assets/scss/td/_chroma.scss": "9c2c4ead60b5068d07cbd838f8ac36d04ef746874b2786b5f6b9fbceba3e347a", "assets/scss/td/_code.scss": "72bbf6c4ef6543bc1b7ae14e645491778c363c5f2a2adeed61989a8ab026e3ce", "assets/scss/td/_content-components.scss": "d5cc90a48bc1931a41534d62129647fd9152754ed959d85ad6ab0f8af49134eb", - "assets/scss/td/_content.scss": "9230a3e412e792a4f3ddb7a3c72856317d13f7c7de79a191e109838f4fe773ba", + "assets/scss/td/_content.scss": "a6446a901260e13b224d14bc2775f70f15cd6ad2f3838bd6243f40c95856a4e7", "assets/scss/td/_contributors.scss": "523b145aa37f3dae2130ab3720c0e53e44e6c738c5c9b72abd46fab324d50729", "assets/scss/td/_diagram.scss": "255034e69f1902186296c10c46a29202daffc12968245d2ceb24bd50a8599883", + "assets/scss/td/_document-compare.scss": "77eb2f4123395000a03fb27da20686250a5a03cf5d81e33cfcbe0369de748ba3", "assets/scss/td/_drawio.scss": "7cbb2e6b6798ca6f901b77fe6da019ee9074d7c2b1395f0ea2f3c11d92335fb5", - "assets/scss/td/_experimental-presets.scss": "801f402023446a8236803f90143ded0a53352b3e7bb91f223c83cecfdc24068a", - "assets/scss/td/_expo.scss": "0962f62c89562726ad950230766e91909dbee657b5e4a5a11a088d9fc8ed479e", + "assets/scss/td/_experimental-presets.scss": "66ca81547b0c0adbe88db23c0331786951e1918db8741927184141dfa2687400", + "assets/scss/td/_expo.scss": "eb8f6c5eeedd1077e6dbd5b62a055e1a1e161ab88d16d780d2d1a49bf3706483", "assets/scss/td/_filetree.scss": "202e739c9c43aa4297e246e852bfda1f112d25da212a27a3640b8252afd04be7", "assets/scss/td/_footer.scss": "fff8b7333d8d2e6f21e34d334863cbfd49d6c226101dbbc3d08a8ca020cec2b4", "assets/scss/td/_gallery.scss": "f8390c4a531efe78df2f8558605abdd2e83a033d5322faf9434cdc729e801944", "assets/scss/td/_giscus.scss": "56232f9d99e37e95394a270892fcf0ba69cee77961b64624f460bbc79ababc64", - "assets/scss/td/_landing.scss": "1342a989cc43c671e9e68ba08c945b46040afa75a03f734d3e15b4f59520a611", - "assets/scss/td/_language-selector.scss": "3fb539051f1a77f53281183d293163c49570e6548bf334d6a1c5ac4adff0f638", - "assets/scss/td/_main-container.scss": "516932e165600aef8c0a42fd196ee35847a6940bd977ce9244680f5611e01e46", - "assets/scss/td/_main.scss": "ed837956318b4a4e9d4c5ee4b5686d4a4729aae1d78879e65778f1bbdefb87cf", + "assets/scss/td/_landing.scss": "3902f5bef445b57fefe11c6bf20f036d585208d22f4cfe7918263ed4bd8505cb", + "assets/scss/td/_language-selector.scss": "a5d5a530923b8f88a440ffcd88937d815a55ce0bf235cc861b2c25d68d7d1594", + "assets/scss/td/_main-container.scss": "c51d91f52134243044643ff46133ad264414c644812ede04be7ac5f4fa38a15c", + "assets/scss/td/_main.scss": "fbc90e395f9e2292a87a02b07d7bc78907ae044dc5cd79c52006f1c55c530064", "assets/scss/td/_markers.scss": "ffe0affd0e951e33545f1eeae5239b827e1d4ff7e718f3b2642ef2c5b4d8ee88", "assets/scss/td/_pager.scss": "1d0cc461a67f02be83933e09218eed3e327b8d0ad550c3c56c18715dfe59b749", "assets/scss/td/_presets.scss": "09b75f292dbe73bdd475e29d4cf7a63d021d93a3f5996a7dfbbb1d3a223274fc", @@ -67,22 +68,22 @@ "assets/scss/td/shell/_blog.scss": "c86b2b9ca2567c62c3cb5b508e41176a4ab2e37eec819a516f3cca79cc2e0385", "assets/scss/td/shell/_featured.scss": "836efc4118719e5609f2f2b872303e03f0d5c80b18618927133a307cd939bd00", "assets/scss/td/shell/_kbd-nav.scss": "82d4fad6dadc9acb1ec3aeea10051a3aad8e21e0336fb0391c4abbd01ae9a4a4", - "assets/scss/td/shell/_layout.scss": "88a8b0834f1af9e00d1554f11f5139edffb05b69e533725fb67e6398ba635432", + "assets/scss/td/shell/_layout.scss": "2b5b206bb6ba42a6d7e31631158a374e0ebd10da1744b47e73812777a646cea7", "assets/scss/td/shell/_page-context.scss": "733e2a53ebf31f02b0cb7906688138798d989592e6ff81c9018ba8dc147328c5", "assets/scss/td/shell/_search.scss": "53296619c651a8e7702e4c3a2b9732941738c9319073498f9724d2cebf6b5a76", "assets/scss/td/shell/_sidebar.scss": "b7914a91e7f6e6028b089a24205f080547f5e3387ad7abc1b8369fc2e500d73a", "assets/scss/td/shell/_toc.scss": "2207c0df2a2f4831330e95a78b9a0418969afa1dea70026ad9cc5b7809b939f3", "assets/scss/td/shell/_tokens.scss": "eafe3c1c650377d7b790941953eec0ff4e1e6e05d715d0e34208c3765541b195", - "assets/scss/td/shortcodes/_content-primitives.scss": "44e330fdcbb1b23786f8a651a6d01105d6721d1dd5738f927b03f6cbdce6b72e", + "assets/scss/td/shortcodes/_content-primitives.scss": "efdbc0cfd1984a741c74121af6a49a0adc72adcc1c5a8a40318ca791d6921c7c", "assets/scss/td/shortcodes/_steps.scss": "24735dbdb6fc40de3e931870067c02a33a57b2a818ef9c4d0da0f78d13c9be85", "assets/scss/td/support/_bootstrap_vers_test.scss": "dd1830c809c6e2bf5e9c9df6954240e58fbfb70071ef4fa45494049234503949", "assets/scss/td/support/_mixins.scss": "116c992f1d40ef7b077de945f96689e8d843dcdb44b01294e681ed370d1ea300", "assets/scss/td/support/_rtl.scss": "d2928aa5dd933ff2514f5d59cfa92a489625fb0d65160f5fb7e11ee60ee80146", "assets/scss/td/support/_utilities.scss": "52f3f2fbb604692b776d6e84e8a71af523cff7fb22cfa02982281deca9c2f49e", - "i18n/en.yaml": "9391c4ef2f824281c6e734bcbac5b233a3c9a7d8068e54f95022e8cc0b3eeb10", - "i18n/zh-cn.yaml": "c934023b40c10dbf621ee018cf0ead6bbcd9f325e42b5a5d814ff51758044081", + "i18n/en.yaml": "d456d73384f10ec7997dedb3982d367fac607396885436ca66404a990e560210", + "i18n/zh-cn.yaml": "188311a8238449c73c0adc29de1ca0869a31e85d059164eafbed7565447a719b", "layouts/404.html": "8aa99a52d82914e85360df70e75b2b184c0e0ffd6389bad20e68ddaf3be8c934", - "layouts/_partials/actions/manifest.html": "8d706f3c0383da9a8f9e5db5297fe41b1d10e03cf0ebccc1feb78cebd02e1584", + "layouts/_partials/actions/manifest.html": "3aed378114d4cb8f46a9f9b163cd6483b9b37fb7c71817c324705dcc3e4aa49a", "layouts/_partials/backlinks-sources.html": "46ede594b89f971731dd5caf322bd13b0dc7b791f5e04294daa0d793bff1908b", "layouts/_partials/backlinks.html": "be94ac2f7917993128f603188045b69fb425bd2882b1888ad2d3db253ce72653", "layouts/_partials/content/image-zoom-config.html": "3ad22eed2f6317dab16429cd91dffa2b01ab433674409b9ac5b4be41498b6fba", @@ -91,7 +92,7 @@ "layouts/_partials/footer.html": "6cbf0f74e6591b3235d47ed5cdad537c795508bd4af27e789962ec41f4b03544", "layouts/_partials/hooks/body-end.html": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "layouts/_partials/hooks/head-end.html": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "layouts/_partials/navbar.html": "e39b7e1eada012ed68409b8413616a5e0370f930552d29d0d9e16d0af2095aff", + "layouts/_partials/navbar.html": "5c8063fc39a75b670eb24d3bdb255267ed4d7d7b3e1f11c91ca10ac09eeb3337", "layouts/_partials/preset-config.html": "3350bea094fcfdd11266d00505f64203a087f3b9d697142778ca10cffcc5ca78", "layouts/_partials/print/page-content.html": "0247138a2cb592716a54fd0cb2cd748c6fc23cec99055b808258da677624d8cf", "layouts/_partials/seo-alternates.html": "fa17d2bfa8f29884d13790f4a6fc9985f5a59833e1925906c8657bda082e86a3", @@ -99,7 +100,7 @@ "layouts/_partials/shell/appearance-menu.html": "883a454576b9dc4cfc50e95d438a864e4e301b3964cf4653b6a5e24b93541fd0", "layouts/_partials/shell/config.html": "ca839c2b502af37ec2fa58d5b77a7d10994ba402142b8fad1659b411e64fedb0", "layouts/_partials/shell/search-enabled.html": "0c55b2dfb3438942ff39eaf6b5b30829920af4871a71c88b0aa3d55c671a50d6", - "layouts/_partials/shell/sidebar-panel.html": "87ac2ef5b82c1328697be791c7638ee117cfe27e655712b71c51864912367ed3", + "layouts/_partials/shell/sidebar-panel.html": "32088fbf038706e7e45e21d35fbed906042594a3fc70bf0e18bf9a65512e0ec3", "layouts/_partials/shell/toc.html": "7f3bce19c87102e53bc87c1c6fca8dcf935bb72b53a1fe3cf8c1800f414622af", "layouts/landing.html": "073148bdbea02e8d4e5bd3cfc61c1efdc9213de73345a6027ba242eff014d8a7" } diff --git a/scripts/oink-upgrade.md b/scripts/oink-upgrade.md index 6d20ac897a..cf2042a7f9 100644 --- a/scripts/oink-upgrade.md +++ b/scripts/oink-upgrade.md @@ -10,13 +10,13 @@ source imports, so it can remove the theme). Use the Hugo Extended version pinned in `.github/workflows/hugo.yml`, the Go version in `go.mod`, Python 3.13, WebP's `cwebp` / `dwebp`, and Node satisfying -`tests/e2e/package.json`. Node is only for browser tests. OINK 1.4.0 requires +`tests/e2e/package.json`. Node is only for browser tests. OINK 1.5.1 requires Hugo Extended 0.168.0 and Go 1.27.3; Hugo's built-in LibSass compiles the site, so no standalone Dart Sass or npm site-build dependencies are needed. Set `HUGO_BIN` / `PYTHON_BIN` for task-local tools. Read the target release notes and the [official upgrade guide](https://oink.pgsty.com/zh/docs/admin/upgrade/). -For [1.4.0](https://oink.pgsty.com/zh/blog/release/1.4.0/), keep LibSass deprecation +For [OINK 1.5.1](https://github.com/pgsty/oink/releases/tag/v1.5.1), keep LibSass deprecation warnings visible: do not add `--panicOnWarning` or suppress the warning through `ignoreLogs`. The shared `scripts/hugo_build.py` runner streams the full output and permits only that exact LibSass warning; every other Hugo `WARN`, including @@ -46,7 +46,7 @@ scripts/hugo.sh build --destination "$upgrade_dir/before-site" SITE_ROOT="$upgrade_dir/before-site" VISUAL_RESULTS_DIR="$upgrade_dir/before" npm run test:visual ) # Select the target toolchain, then use an explicit release tag. -scripts/update-oink.sh v1.4.0 --work-dir "$upgrade_dir" +scripts/update-oink.sh v1.5.1 --work-dir "$upgrade_dir" ``` The single command checks target tool requirements and a real Hugo/LibSass @@ -59,7 +59,7 @@ When upstream changes affect local integrations, compare the old upstream, new upstream and local override. Adapt only affected behavior, then resume: ```sh -scripts/update-oink.sh v1.4.0 --resume --accept-reviewed --work-dir "$upgrade_dir" +scripts/update-oink.sh v1.5.1 --resume --accept-reviewed --work-dir "$upgrade_dir" ``` `--accept-reviewed` acknowledges the reported compatibility review; it skips no @@ -104,7 +104,7 @@ Normal preview commands retain local-development capabilities. Manual CI dispatches inspect the immutable candidate's theme pin before scheduling builds. OINK 1.3.0 manual candidates are outside this workflow's supported scope; OINK 1.3.1 requires standalone Dart Sass. Both pins are rejected by the current -workflow; upgrade such a candidate to OINK 1.4.0 first. +workflow; upgrade such a candidate to OINK 1.5.1 first. This restriction concerns the candidate's theme, not historical documentation: archived content continues to use the current site shell. @@ -115,6 +115,85 @@ runs reject reserved outputs instead of overwriting user files; existing baseline screenshots. A fresh `--resume` directory backs up the already-pinned target, which `review.json` explicitly records. +## Duo View acceptance matrix + +Use the product names **OINK**, **Duo View / 双列视图** and **Sync Scroll / +同步滚动** consistently. API names such as `params.ui.compare`, +`compare_translations` and `window.OinkComparePane` stay unchanged. + +The blocking browser suite uses real generated content and the aggregate +artifact. `tests/e2e/duo-view.spec.js` covers this route formula for every +selected version, both languages and both responsive layouts: + +| Version | English | Chinese | Desktop | Mobile | +| --- | --- | --- | --- | --- | +| latest | `/docs/` | `/cn/docs/` | two panes at 1440×900 | one visible pane at 390×844 | +| 1.7 | `/versions/1.7/docs/` | `/versions/1.7/cn/docs/` | same | same | +| 1.5 | `/versions/1.5/docs/` | `/versions/1.5/cn/docs/` | same | same | +| 1.3 | `/versions/1.3/docs/` | `/versions/1.3/cn/docs/` | same | same | +| 1.0 | `/versions/1.0/docs/` | `/versions/1.0/cn/docs/` | same | same | + +The detailed interaction path is the real HugeGraph Server quick-start pair; it +checks lazy loading, the page entry, Command Palette action, `B`, Sync Scroll, +continuous scrolling, fragment links, iframe isolation, reload, Back/Forward, +reading-position recovery and third-party request suppression. The real +ToplingDB Blog pair checks Blog/search/Print/Markdown/RSS coexistence. The +Chinese-only 0.10.4 release note checks that a missing translation has no entry +or command. A controlled failed pane checks the 15-second timeout contract, +accessible Retry and successful recovery. + +OINK supports Docs, Book and Blog through the same native discovery partial. +HugeGraph currently publishes no Book section, so do not add synthetic or +machine-translated Book content merely to manufacture coverage. The exact +OINK module lock and reviewed upstream inventory preserve Book support for the +first real bilingual Book page; upstream owns draft, non-HTML and `build.render` +filtering. Consumer tests concentrate on HugeGraph's real routes and hook +integrations rather than copying OINK's fixture suite. + +Visual CI captures English and Chinese Duo View at desktop and mobile sizes. +Review those images together with ordinary navigation, search, appearance and +content captures. No comparison runtime or peer page may be requested before a +user opens Duo View. + +## Staging, release and rollback + +A pull request builds the full aggregate but does not publish. After the same +commit exists on an ASF repository branch, run **Build and deploy site** from +ASF `master` with: + +```text +operation: staging-next +scope: full +candidate_branch: +confirmation: publish asf-staging-oink +``` + +Verify `https://hugegraph-oink.staged.apache.org/` with the complete matrix, +canonical/hreflang, version switching, search, Kapa consent, Print/RSS and the +visual artifact. Do not run `production-history-refresh` and do not publish +`asf-site` without separate authorization. An ORG-only branch cannot satisfy +this Staging gate because the privileged workflow resolves `candidate_branch` +inside the ASF repository. + +Record the pre-upgrade commit and the candidate commit before Staging. Recovery +is deliberately symmetric: + +1. **Before merge or publication:** restore `go.mod`, `go.sum`, `hugo.yaml`, + the reviewed overrides and compatibility changes from the recorded + pre-upgrade commit, then rerun the complete PR suite. +2. **After merge but before Production:** revert the focused upgrade commit on + an ASF branch and dispatch `staging-next` with `scope: full` for that revert. + Verify all five versions before merging the revert. +3. **After a future Production publication:** merge the tested revert through + the normal ASF review flow and let the existing `master` deployment restore + `asf-site`; never force-update a publish branch or reuse an unverified cached + artifact. + +The legacy `netlify.toml` is not consumed by the ASF/GitHub Actions historical +builder and is removed from archived assemblies. It remains aligned with the +locked Go/Hugo versions for optional local/Netlify previews, without +`--panicOnWarning`; the GitHub workflow is the deployment authority. + ## Customization review map | Integration | Local entry | Required evidence | diff --git a/tests/e2e/duo-view.spec.js b/tests/e2e/duo-view.spec.js new file mode 100644 index 0000000000..446413cab4 --- /dev/null +++ b/tests/e2e/duo-view.spec.js @@ -0,0 +1,246 @@ +const { test, expect } = require('./artifact-test'); + +const VERSION_IDS = ['latest', '1.7', '1.5', '1.3', '1.0']; +const EXPECTED_IDS = (process.env.EXPECTED_VERSIONS || VERSION_IDS.join(',')).split(','); + +function versionPrefix(version) { + return version === 'latest' ? '' : `/versions/${version}`; +} + +function docsRoute(version, locale) { + return `${versionPrefix(version)}${locale === 'cn' ? '/cn' : ''}/docs/`; +} + +function alternateDocsRoute(version, locale) { + return docsRoute(version, locale === 'cn' ? 'en' : 'cn'); +} + +async function config(page) { + return page.locator('#td-compare-config').evaluate(node => JSON.parse(node.textContent)); +} + +async function actionManifest(page) { + return page.locator('#td-action-manifest').evaluate(node => JSON.parse(node.textContent)); +} + +function paneFrames(page) { + return page.frames().filter(frame => { + try { + return new URL(frame.url()).searchParams.get('oink_compare_pane') === '1'; + } catch (_) { + return false; + } + }); +} + +async function waitForReadyPanes(page) { + const dialog = page.locator('dialog[data-td-compare]'); + await expect(dialog).toBeVisible(); + await expect(dialog.locator('[data-td-compare-frame]')).toHaveCount(2); + await expect.poll(() => paneFrames(page).length).toBe(2); + for (const frame of paneFrames(page)) { + await expect.poll(() => frame.evaluate(() => Boolean( + window.OinkComparePane && + document.documentElement.hasAttribute('data-td-compare-pane') && + document.querySelector('meta[name="oink-compare-page"]') && + document.querySelector('#td-main-content .td-content') + ))).toBe(true); + } + await expect(dialog.locator('[data-td-compare-sync]')).toBeEnabled(); + return dialog; +} + +async function visiblePaneCount(dialog) { + return dialog.locator('[data-td-compare-side]').evaluateAll(nodes => nodes.filter(node => { + const style = getComputedStyle(node); + return style.visibility !== 'hidden' && style.display !== 'none'; + }).length); +} + +for (const version of VERSION_IDS) { + for (const locale of ['en', 'cn']) { + test(`${version} ${locale} uses native Duo View on desktop and one-pane mobile`, async ({ page }) => { + test.skip(!EXPECTED_IDS.includes(version), 'version not selected for this artifact'); + await page.setViewportSize({ width: 1440, height: 900 }); + const route = docsRoute(version, locale); + await page.goto(route); + + const descriptor = await config(page); + expect(descriptor.current.url).toBe(route); + expect(descriptor.targets.map(target => target.url)).toContain(alternateDocsRoute(version, locale)); + expect(descriptor.shortcut).toBe('B'); + const manifest = await actionManifest(page); + expect(manifest.actions.find(action => action.id === 'compare_translations')).toMatchObject({ + available: true, + kind: 'invoke', + }); + + const entry = page.locator('[data-td-compare-open]'); + await expect(entry).toBeVisible(); + await expect(entry).toHaveAttribute('aria-keyshortcuts', 'B'); + await entry.click(); + const dialog = await waitForReadyPanes(page); + await expect.poll(() => visiblePaneCount(dialog)).toBe(2); + + await page.setViewportSize({ width: 390, height: 844 }); + const tabs = dialog.locator('.td-compare__tab'); + await expect(tabs).toHaveCount(2); + await expect(tabs.first()).toHaveAttribute('aria-pressed', 'true'); + await expect.poll(() => visiblePaneCount(dialog)).toBe(1); + await tabs.nth(1).click(); + await expect(tabs.nth(1)).toHaveAttribute('aria-pressed', 'true'); + await expect.poll(() => visiblePaneCount(dialog)).toBe(1); + + await dialog.locator('[data-td-compare-close]').click(); + await expect(page.locator('dialog[data-td-compare]')).toHaveCount(0); + }); + } +} + +test('real Docs pair is lazy, pane-isolated, synchronized and history-restorable', async ({ page }) => { + const runtimeRequests = []; + const thirdPartyRequests = []; + page.on('request', request => { + if (request.url().includes('document-compare')) runtimeRequests.push(request.url()); + if (/kapa\.ai|hcaptcha\.com|kapa-widget-proxy/.test(request.url())) thirdPartyRequests.push(request.url()); + }); + + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto('/docs/quickstart/hugegraph/hugegraph-server/'); + expect(runtimeRequests).toEqual([]); + const descriptor = await config(page); + expect(descriptor.targets.map(target => target.url)).toContain( + '/cn/docs/quickstart/hugegraph/hugegraph-server/' + ); + + await page.keyboard.press('b'); + let dialog = await waitForReadyPanes(page); + expect(runtimeRequests.length).toBeGreaterThan(0); + let frames = paneFrames(page); + for (const frame of frames) { + expect(await frame.evaluate(() => ({ + shell: typeof window.HugeGraphShell, + kapa: typeof window.HugeGraphKapa, + pane: window.OinkComparePane, + }))).toEqual({ shell: 'undefined', kapa: 'undefined', pane: true }); + } + expect(thirdPartyRequests).toEqual([]); + + const sync = dialog.locator('[data-td-compare-sync]'); + await expect(sync).toHaveAttribute('role', 'switch'); + await expect(sync).toHaveAttribute('aria-checked', 'true'); + await sync.click(); + await expect(sync).toHaveAttribute('aria-checked', 'false'); + await sync.click(); + await expect(sync).toHaveAttribute('aria-checked', 'true'); + + await frames[0].evaluate(() => window.scrollTo({ + top: Math.max(500, document.documentElement.scrollHeight * 0.55), + behavior: 'instant', + })); + await expect.poll(() => frames[1].evaluate(() => window.scrollY)).toBeGreaterThan(100); + const firstFollowerPosition = await frames[1].evaluate(() => window.scrollY); + await frames[0].evaluate(() => window.scrollTo({ + top: document.documentElement.scrollHeight * 0.8, + behavior: 'instant', + })); + await expect.poll(() => frames[1].evaluate(() => window.scrollY)) + .toBeGreaterThan(firstFollowerPosition + 20); + + const followerBeforeFragment = await frames[1].evaluate(() => window.scrollY); + const selfLink = frames[0].locator('#td-main-content .td-content a.td-heading-self-link').nth(2); + await expect(selfLink).toBeAttached(); + await selfLink.click(); + await expect.poll(() => new URL(frames[0].url()).hash.length).toBeGreaterThan(1); + await expect.poll(async () => Math.abs( + await frames[1].evaluate(() => window.scrollY) - followerBeforeFragment + )).toBeGreaterThan(20); + + await page.waitForTimeout(450); + await page.reload(); + dialog = await waitForReadyPanes(page); + frames = paneFrames(page); + await expect.poll(() => frames[0].evaluate(() => window.scrollY)).toBeGreaterThan(100); + await expect.poll(() => frames[1].evaluate(() => window.scrollY)).toBeGreaterThan(100); + + await page.keyboard.press('b'); + await expect(page.locator('dialog[data-td-compare]')).toHaveCount(0); + await expect.poll(() => page.evaluate(() => Boolean(history.state?.oinkCompareReader))).toBe(false); + await page.goForward(); + await waitForReadyPanes(page); + await page.goBack(); + await expect(page.locator('dialog[data-td-compare]')).toHaveCount(0); + + await page.locator('[data-td-shell-search-open]').first().click(); + const input = page.locator('.td-shell-search__input'); + await input.fill('>Duo View'); + const command = page.locator('[role="option"]').filter({ hasText: 'Duo View' }).first(); + await expect(command).toBeVisible(); + await command.click(); + await waitForReadyPanes(page); +}); + +test('real Blog translation coexists with search, Print, Markdown and RSS', async ({ page, request }) => { + const route = '/blog/2025/10/09/toplingdb-quick-start/'; + await page.goto(route); + const descriptor = await config(page); + expect(descriptor.targets.map(target => target.url)).toContain( + '/cn/blog/2025/10/09/toplingdb-quick-start/' + ); + await page.locator('[data-td-compare-open]').click(); + const dialog = await waitForReadyPanes(page); + await dialog.locator('[data-td-compare-close]').click(); + await page.locator('[data-td-shell-search-open]').first().click(); + await page.locator('.td-shell-search__input').fill('ToplingDB'); + await expect(page.locator('[role="option"]').first()).toBeVisible(); + + for (const output of ['/blog/index.xml', '/_print/docs/', `${route}index.md`]) { + const response = await request.get(output); + expect(response.ok(), output).toBeTruthy(); + const body = await response.text(); + expect(body, output).not.toContain('id="td-compare-config"'); + expect(body, output).not.toMatch(/]+data-td-compare-open/); + } +}); + +test('a real one-language page exposes no Duo View entry or command', async ({ page }) => { + await page.goto('/cn/docs/changelog/hugegraph-0.10.4-release-notes/'); + await expect(page.locator('#td-compare-config')).toHaveCount(0); + await expect(page.locator('[data-td-compare-open]')).toHaveCount(0); + const manifest = await actionManifest(page); + expect(manifest.actions.some(action => action.id === 'compare_translations')).toBe(false); + await page.keyboard.press('b'); + await expect(page.locator('dialog[data-td-compare]')).toHaveCount(0); +}); + +test('failed pane times out to an accessible retry and recovers', async ({ page }) => { + await page.addInitScript(() => { + const nativeSetTimeout = window.setTimeout.bind(window); + window.setTimeout = (callback, delay, ...args) => + nativeSetTimeout(callback, delay === 15000 ? 80 : delay, ...args); + }); + let held = false; + await page.route('**/*', async route => { + const url = new URL(route.request().url()); + if (!held && url.pathname === '/cn/docs/' && url.searchParams.get('oink_compare_pane') === '1') { + held = true; + await new Promise(resolve => setTimeout(resolve, 250)); + try { await route.abort('failed'); } catch (_) { /* The timed-out frame may already be gone. */ } + return; + } + await route.fallback(); + }); + + await page.goto('/docs/'); + await page.locator('[data-td-compare-open]').click(); + const dialog = page.locator('dialog[data-td-compare]'); + await expect(dialog).toBeVisible(); + const failure = dialog.locator('.td-compare__failure'); + await expect(failure).toBeVisible(); + await expect(failure).toContainText('Could not load this page.'); + const retry = failure.locator('.td-compare__retry'); + await expect(retry).toHaveAccessibleName('Retry'); + await expect(failure.locator('a[target="_blank"]')).toHaveAttribute('rel', 'noopener'); + await retry.click(); + await waitForReadyPanes(page); +}); diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 18ebee9a2a..5ec3d48035 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -6,7 +6,7 @@ }, "scripts": { "test": "playwright test", - "test:ci": "node --test ../ui-ai/*.test.cjs && node --test workflow-contract.test.cjs && playwright test versioning.spec.js search-ranking.spec.js platform.spec.js theme-upgrade.spec.js native-features.spec.js filetree-comments.spec.js pd-table.spec.js technical-catalogs.spec.js chart-tooltip.spec.js component-security.spec.js ai.spec.js accessibility.spec.js --reporter=line,html", + "test:ci": "node --test ../ui-ai/*.test.cjs && node --test workflow-contract.test.cjs && playwright test versioning.spec.js duo-view.spec.js search-ranking.spec.js platform.spec.js theme-upgrade.spec.js native-features.spec.js filetree-comments.spec.js pd-table.spec.js technical-catalogs.spec.js chart-tooltip.spec.js component-security.spec.js ai.spec.js accessibility.spec.js --reporter=line,html", "test:visual": "playwright test visual.spec.js --reporter=line" }, "devDependencies": { diff --git a/tests/e2e/visual.spec.js b/tests/e2e/visual.spec.js index 8c9183c8fc..d2802308d0 100644 --- a/tests/e2e/visual.spec.js +++ b/tests/e2e/visual.spec.js @@ -15,6 +15,10 @@ const states = [ ["cn-appearance-mobile-dark", "/cn/docs/introduction/", { width: 390, height: 844 }, "dark", "appearance"], ["en-paper-desktop-light", "/docs/introduction/", { width: 1440, height: 900 }, "light", "paper"], ["cn-paper-mobile-dark", "/cn/docs/introduction/", { width: 390, height: 844 }, "dark", "paper"], + ["en-duo-docs-desktop-light", "/docs/quickstart/hugegraph/hugegraph-server/", { width: 1440, height: 900 }, "light", "duo"], + ["cn-duo-docs-desktop-light", "/cn/docs/quickstart/hugegraph/hugegraph-server/", { width: 1440, height: 900 }, "light", "duo"], + ["en-duo-docs-mobile-light", "/docs/quickstart/hugegraph/hugegraph-server/", { width: 390, height: 844 }, "light", "duo"], + ["cn-duo-docs-mobile-light", "/cn/docs/quickstart/hugegraph/hugegraph-server/", { width: 390, height: 844 }, "light", "duo"], ...["en", "cn"].flatMap(language => { const prefix = language === "cn" ? "/cn" : ""; return ["slate", "paper"].flatMap(preset => ["light", "dark"].flatMap(theme => { @@ -176,6 +180,16 @@ for (const [name, url, viewport, theme, state, preset = "slate"] of states) { await expect(page.locator("html")).toHaveAttribute("data-td-preset", "paper"); await page.keyboard.press("Escape"); } + } else if (state === "duo") { + await page.locator("[data-td-compare-open]").click(); + const dialog = page.locator("dialog[data-td-compare]"); + await expect(dialog).toBeVisible(); + await expect(dialog.locator("[data-td-compare-frame]")).toHaveCount(2); + await expect.poll(() => page.frames().filter(frame => { + try { return new URL(frame.url()).searchParams.get("oink_compare_pane") === "1"; } + catch (_) { return false; } + }).length).toBe(2); + await expect(dialog.locator("[data-td-compare-sync]")).toBeEnabled(); } else if (state === "search") { await page.locator("[data-td-shell-search-open]").first().click(); await page.locator(".td-shell-search__input").fill(url.startsWith("/cn/") ? "服务端" : "server"); @@ -212,7 +226,7 @@ for (const [name, url, viewport, theme, state, preset = "slate"] of states) { await page.screenshot({ animations: "disabled", path: path.join(directory, `${name}.png`), - fullPage: !["gallery", "chart", "longcode", "markmap", "terminal", "math", "pd", "computer", "config-api", "helm", "footer", "hubble-import", "hubble-nodes"].includes(state) + fullPage: !["gallery", "chart", "longcode", "markmap", "terminal", "math", "pd", "computer", "config-api", "helm", "footer", "hubble-import", "hubble-nodes", "duo"].includes(state) }); if (sectionHeading) { expect(await sectionHeading.evaluate(node => Math.abs(node.getBoundingClientRect().top - 108))).toBeLessThan(2); diff --git a/tests/ui-ai/ui-contract.test.cjs b/tests/ui-ai/ui-contract.test.cjs index 90f4f3f1ee..05e8a22c51 100644 --- a/tests/ui-ai/ui-contract.test.cjs +++ b/tests/ui-ai/ui-contract.test.cjs @@ -128,3 +128,36 @@ test('social fallback preserves the supplied HugeGraph and Apache image dimensio assert.equal(image.readUInt32BE(16), 1681); assert.equal(image.readUInt32BE(20), 936); }); + +test('OINK Duo View stays native and HugeGraph site runtimes stand down in panes', () => { + const config = read('hugo.yaml'); + const manifest = read('layouts/_partials/actions/manifest.html'); + assert.match(config, /\n\s+compare: true\n/); + assert.match(manifest, /partial "compare\/page-key\.html" \./); + assert.doesNotMatch(manifest, /\.RelPermalink \$outputFormat/); + for (const file of [ + 'assets/js/hugegraph-shell.js', + 'assets/js/language-preference.js', + 'assets/js/kapa-adapter.js', + ]) { + assert.match( + read(file), + /if \(typeof window !== 'undefined' && window\.OinkComparePane\) return;/, + `${file} must not initialize site-level behavior inside native Duo View panes`, + ); + } + assert.equal(fs.existsSync(path.join(root, 'assets/js/document-compare.js')), false); + assert.equal(fs.existsSync(path.join(root, 'layouts/_partials/compare')), false); +}); + +test('OINK lock and reviewed override inventory advance together', () => { + const goMod = read('go.mod'); + const goSum = read('go.sum'); + const inventory = JSON.parse(read('scripts/oink-overrides.json')); + assert.match(goMod, /github\.com\/pgsty\/oink v1\.5\.1/); + assert.match(goSum, /github\.com\/pgsty\/oink v1\.5\.1 h1:/); + assert.match(goSum, /github\.com\/pgsty\/oink v1\.5\.1\/go\.mod h1:/); + assert.equal(inventory.version, 'v1.5.1'); + assert.ok(inventory.files['assets/scss/td/_document-compare.scss']); + assert.ok(inventory.files['layouts/_partials/actions/manifest.html']); +}); From 78ab4cd41360702742122983cf961ad43be08481 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sat, 14 Nov 2026 23:44:41 +0800 Subject: [PATCH 2/7] fix: complete OINK 1.5.1 Duo View compatibility - preserve content runtimes while isolating site-level iframe integrations - align local OINK overrides and upgrade inventory with 1.5.1 contracts - strengthen real-page, historical, visual, staging and rollback validation --- assets/js/hugegraph-shell.js | 8 +- assets/js/kapa-adapter.js | 3 +- assets/js/language-preference.js | 2 +- assets/scss/_styles_project.scss | 10 + contribution.md | 6 +- layouts/_partials/navbar.html | 14 +- layouts/_partials/shell/sidebar-panel.html | 4 +- netlify.toml | 4 +- scripts/oink-features.md | 40 +- scripts/oink-overrides.json | 16 +- scripts/oink-upgrade.md | 342 ++++++++--------- scripts/update_oink.py | 11 + tests/e2e/duo-view.spec.js | 407 +++++++++++---------- tests/e2e/visual.spec.js | 29 +- tests/ui-ai/duo-view.test.cjs | 124 +++++++ tests/ui-ai/ui-contract.test.cjs | 25 +- 16 files changed, 598 insertions(+), 447 deletions(-) create mode 100644 tests/ui-ai/duo-view.test.cjs diff --git a/assets/js/hugegraph-shell.js b/assets/js/hugegraph-shell.js index 5773d7e490..38402d12d2 100644 --- a/assets/js/hugegraph-shell.js +++ b/assets/js/hugegraph-shell.js @@ -7,8 +7,6 @@ (function (global) { 'use strict'; - if (typeof window !== 'undefined' && window.OinkComparePane) return; - var versionExecutorRegistries = new WeakSet(); function readConfig(documentObject) { @@ -276,6 +274,12 @@ } function init(windowObject, documentObject) { + if (windowObject.OinkComparePane) { + // Duo View panes are ordinary documents: keep article table behavior, + // but do not bind navigation, storage, search or version controllers twice. + initScrollableTables(documentObject); + return; + } var config = readConfig(documentObject); consumeVersionFallback(windowObject, documentObject, config); initVersionSwitching(windowObject, documentObject); diff --git a/assets/js/kapa-adapter.js b/assets/js/kapa-adapter.js index 402786631c..3422d0fb31 100644 --- a/assets/js/kapa-adapter.js +++ b/assets/js/kapa-adapter.js @@ -7,8 +7,6 @@ (function (global) { 'use strict'; - if (typeof window !== 'undefined' && window.OinkComparePane) return; - var BUNDLE_URL = 'https://widget.kapa.ai/kapa-widget.bundle.js'; var TIMEOUT_MS = 5000; @@ -429,6 +427,7 @@ } function init(windowObject, documentObject) { + if (windowObject.OinkComparePane) return null; var config = readConfig(documentObject); if (!config) return null; var controller = createController(windowObject, documentObject, config); diff --git a/assets/js/language-preference.js b/assets/js/language-preference.js index 89ff514fe1..5837da1982 100644 --- a/assets/js/language-preference.js +++ b/assets/js/language-preference.js @@ -2,7 +2,7 @@ (function () { 'use strict'; - if (typeof window !== 'undefined' && window.OinkComparePane) return; + if (window.OinkComparePane) return; var node = document.getElementById('hg-language-config'); if (!node) return; diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 8135ec2cf5..28838bb997 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -931,3 +931,13 @@ width: 100%; max-width: 36rem; } + +// OINK Duo View loads ordinary pages in same-origin iframes. Keep content-level +// interactions, but remove HugeGraph's top-level AI chrome from both panes. +html[data-td-compare-pane] { + .hg-ask-ai-launcher, + .hg-ai-consent, + .hg-ai-status { + display: none !important; + } +} diff --git a/contribution.md b/contribution.md index 1feba1a30b..e2591895fe 100644 --- a/contribution.md +++ b/contribution.md @@ -66,7 +66,7 @@ effect. Use `--baseURL` or `HG_DOC_SITE_ORIGIN` when changing the rendered origin (the flag wins when both are set); a server `--port` is reflected in the generated local origin. -OINK 1.4 uses Hugo's built-in LibSass. Its deprecation warning must remain visible; +The pinned OINK release uses Hugo's built-in LibSass. Its deprecation warning must remain visible; do not add `--panicOnWarning` or suppress it with `ignoreLogs`. The shared `scripts/hugo_build.py` gate permits only that exact warning and fails builds on all other Hugo warnings, including missing translations and duplicate target @@ -91,7 +91,7 @@ validate commands so older candidate branches do not need the new CI helpers. Candidates pinned to OINK 1.3.0 are outside this workflow's supported scope. OINK 1.3.1 requires standalone Dart Sass, while this workflow installs native LibSass only. Both pins are rejected before scheduling builds; upgrade such a -candidate to OINK 1.4.0 before dispatching it. +candidate to the current OINK pin before dispatching it. Historical documentation versions still use the current site's shell and are unaffected by this candidate-theme restriction. Cache hits still run the full version validator, and aggregation validates every selected version again. @@ -124,7 +124,7 @@ latest 文档使用 SeaTunnel 3.0+ 等最低版本说明,并引用上游 lates Use the focused [OINK feature reference](scripts/oink-features.md) for component selection, copyable syntax and real HugeGraph source examples. It covers the -native components used by this site, OINK 1.4.0's table reader and release cards, +native components used by this site, including the table reader, release cards and Duo View, and the public interfaces used by the site shell. Read the relevant section before adding a renderer, shortcode, script or stylesheet. diff --git a/layouts/_partials/navbar.html b/layouts/_partials/navbar.html index 751432de74..bd95e827eb 100644 --- a/layouts/_partials/navbar.html +++ b/layouts/_partials/navbar.html @@ -1,5 +1,5 @@ -{{- /* OINK v1.3.1 override: keep keyboard help in the header and expose - version links inside the landing-page mobile drawer. */ -}} +{{- /* OINK v1.5.1 override: keep keyboard help in the header and expose + HugeGraph version links inside the native navbar-only mobile drawer. */ -}} {{- $config := partialCached "shell/config.html" . .Site.Language.Lang -}} {{- $targets := partialCached "language-targets.html" . .Site.BaseURL .RelPermalink .Site.Language.Lang -}} {{- $localSearch := partial "shell/search-enabled.html" . -}} @@ -8,12 +8,10 @@ aside). */ -}} {{- $landingLayout := eq .Layout "landing" -}} {{- $drawerMode := and (not $landingLayout) (partial "shell/chrome-enabled.html" .) (partial "shell/sidebar-enabled.html" .) -}} -{{- /* Home and explicit Landing layouts render the drawer entry: these are - the navbar-only surfaces with no sidebar drawer to fall back on. CSS - gates the button to the below-md tier, where it joins search as the - whole right-hand contract; the centered link tree stays at every - width, so the drawer only restores the full labelled hierarchy. */ -}} -{{- $landingSurface := and (not $drawerMode) (or .IsHome $landingLayout) -}} +{{- /* Every navbar-only surface needs the native mobile drawer. OINK 1.5.1 + widened this from home/landing pages so utility controls stay reachable + on taxonomy, list and other pages without the docs sidebar. */ -}} +{{- $landingSurface := not $drawerMode -}} {{- /* The navbar owns this drawer even with a local homepage renderer. */ -}} {{- if $landingSurface }}{{ .Page.Store.Set "hasLanding" true }}{{ end -}} {{- $githubStars := partial "landing/github-stars.html" . -}} diff --git a/layouts/_partials/shell/sidebar-panel.html b/layouts/_partials/shell/sidebar-panel.html index 0b5e2e9cd5..58fe90191b 100644 --- a/layouts/_partials/shell/sidebar-panel.html +++ b/layouts/_partials/shell/sidebar-panel.html @@ -1,4 +1,4 @@ -{{- /* OINK override (reviewed against v1.1.0): the mobile Docs drawer owns global controls that +{{- /* OINK override (reviewed against v1.5.1): the mobile Docs drawer owns global controls that disappear from the compact header and no longer repeat in the footer. */ -}} {{- $config := partialCached "shell/config.html" . .Site.Language.Lang -}} {{- $hasAlgolia := partialCached "search/algolia-enabled.html" . .Site.Language.Lang .Site.BaseURL -}} @@ -9,7 +9,7 @@
- + {{- if $config.wordmarkURL }} diff --git a/netlify.toml b/netlify.toml index d329d623ad..678f7a0bb4 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,9 +1,9 @@ +# Legacy/ad-hoc preview only. ASF staging and production use .github/workflows/hugo.yml. [build] -command = "hugo --cleanDestinationDir --gc --minify --environment production --printPathWarnings --printI18nWarnings" +command = "bash scripts/hugo.sh build" publish = "public" [build.environment] GO_VERSION = "1.27.3" HUGO_VERSION = "0.168.0" HUGO_ENVIRONMENT = "production" - diff --git a/scripts/oink-features.md b/scripts/oink-features.md index c7799c63f4..44b8123911 100644 --- a/scripts/oink-features.md +++ b/scripts/oink-features.md @@ -26,24 +26,6 @@ section's child list. Handwritten cards suit cross-section recommendations. Keep prerequisites and warnings visible; use a native `> [!DETAILS]- Title` callout for supplemental detail. -## Native Duo View - -OINK 1.5.1 calls the feature **Duo View** in English and **双列视图** in -Chinese. Use **Sync Scroll / 同步滚动** for its optional approximate heading -alignment. The configuration and action IDs remain `params.ui.compare` and -`compare_translations`; do not rename those API identifiers. - -HugeGraph enables the native feature globally. OINK discovers only real, -same-origin HTML translations of Docs, Book and Blog pages, loads the second -page on demand, and hides the entry when no valid translation exists. Do not -add a parallel route map, copied reader template, machine-generated placeholder -translation or site-specific compare controller. Page-level heading IDs may be -aligned incrementally where a real reading problem is observed. - -Site-level integrations loaded from hooks must return immediately when -`window.OinkComparePane` is true. Content runtimes remain owned by OINK so tabs, -diagrams, code controls and fragment links continue to work inside each pane. - ## Common authoring patterns Block attribute lines must touch the list, table or image they describe. @@ -194,7 +176,7 @@ Expo's generated previews, neighboring-original preload and album layout work do not apply to Gallery. Supply intrinsic `width` and `height` on standalone Markdown images to reserve space; those attributes do not compress the file. Inspect generated HTML and measure bytes/timing before claiming an improvement. -See the [tagged Expo implementation](https://github.com/pgsty/oink/blob/v1.4.0/layouts/_partials/expo/items.html). +See the [tagged Expo implementation](https://github.com/pgsty/oink/blob/v1.5.1/layouts/_partials/expo/items.html). ### Terminal recordings @@ -225,6 +207,24 @@ The recorder is an authoring tool, not a build dependency. Sidebar persistence uses `OinkSidebar`; AI integration uses the public search-tail API. Keep HugeGraph consent, version/language scope and local availability behavior in their existing hooks/adapters. +## Native Duo View + +OINK 1.5.1 calls the feature **Duo View** in English and **双列视图** in +Chinese. Use **Sync Scroll / 同步滚动** for its optional approximate heading +alignment. The configuration and action IDs remain `params.ui.compare` and +`compare_translations`; do not rename those public identifiers. + +HugeGraph enables the native feature globally. OINK discovers only real, +same-origin HTML translations of Docs, Book and Blog pages, loads the peer page +on demand, and hides the entry when no valid translation exists. Do not add a +parallel route map, copied reader template, machine-generated placeholder +translation or site-specific comparison controller. Align heading IDs only on +important pages where a real Sync Scroll problem is observed. + +Site-level integrations loaded from hooks must stand down when +`window.OinkComparePane` is true. Content runtimes remain active so tables, tabs, +diagrams, code controls and fragment links continue to work inside each pane. + - Local search uses `params.offline_search_index: content` so configuration names, error messages and other body-only terms are searchable in both languages. OINK loads the current language's index on demand; full content increases its @@ -246,5 +246,5 @@ The recorder is an authoring tool, not a build dependency. The [1.5.1 release](https://github.com/pgsty/oink/releases/tag/v1.5.1), [changelog](https://github.com/pgsty/oink/blob/v1.5.1/CHANGELOG.md) and [tagged source](https://github.com/pgsty/oink/tree/v1.5.1) are the reference for -this upgrade. Consult the pinned release before extending a component; the +the current pin. Consult the pinned release before extending a component; the upstream website can describe a newer version. diff --git a/scripts/oink-overrides.json b/scripts/oink-overrides.json index 7474d26362..dfb48f3c5e 100644 --- a/scripts/oink-overrides.json +++ b/scripts/oink-overrides.json @@ -1,10 +1,12 @@ { - "version": "v1.5.1", "files": { "assets/js/appearance.js": "66fa6a6fea304c235a8b11ca714478b221d8fdc8db0e9fffdfe5f6a2524760ff", "assets/js/command-palette.js": "2d1bd325480e5d117fe9f4aca78569846fdb6b19372453c3b24b468b3d362528", + "assets/js/compare-entry.js": "063313bec498b35329b4f553f70eecf40035b625bf264a4785266068809bdb80", + "assets/js/compare-scroll.js": "156f95de0df0c3fc042294ecc1ef83a8fdf785d59bfafc2fd08bb9dee9a7ddc8", "assets/js/dark-mode.js": "d0f19cd7011f05debab7ffadc4c268321d4845f12be781d090d2e461978c03b2", "assets/js/docs-shell.js": "c44358e29573f32bc54165eed0e8234d4f7598bf4ad6dbd47af6d6b0d7ad827f", + "assets/js/document-compare.js": "fa56ddea93cc55cb162e19575b1cc29025c4fc7a396a50356e2597d009ee2f21", "assets/js/search-engine.js": "9ca8b850f126ee817a348df97244a916b31611054930cf8e13894363391e87ae", "assets/js/search.js": "69d9316572736644a9a46f915ab7576e7e3a56aed81a5151cdd1ffe0d5c17a93", "assets/js/sidebar-nav.js": "8d848619d326934d92d87354d3d1511db5c65b5122af136f161d6af1fb070d39", @@ -83,13 +85,22 @@ "i18n/en.yaml": "d456d73384f10ec7997dedb3982d367fac607396885436ca66404a990e560210", "i18n/zh-cn.yaml": "188311a8238449c73c0adc29de1ca0869a31e85d059164eafbed7565447a719b", "layouts/404.html": "8aa99a52d82914e85360df70e75b2b184c0e0ffd6389bad20e68ddaf3be8c934", + "layouts/_partials/actions/context.html": "0b1359f19b2270e2c24e01837539c625ed41bc3c07a63c48daaf5cb55bd84996", "layouts/_partials/actions/manifest.html": "3aed378114d4cb8f46a9f9b163cd6483b9b37fb7c71817c324705dcc3e4aa49a", "layouts/_partials/backlinks-sources.html": "46ede594b89f971731dd5caf322bd13b0dc7b791f5e04294daa0d793bff1908b", "layouts/_partials/backlinks.html": "be94ac2f7917993128f603188045b69fb425bd2882b1888ad2d3db253ce72653", + "layouts/_partials/compare/analytics.html": "227e4407f3307d3f156e0eec5d347989fcc4a174d6383f002c62874df82eda88", + "layouts/_partials/compare/bootstrap.html": "c2ad9725c0a1de6f498dac9d53f5cdef87a95d2867d2e64e389e701c842a7e29", + "layouts/_partials/compare/enabled.html": "8f278b044ee95b52fb214c9b336e7771f88c9ad52babd68af5f44922996e4512", + "layouts/_partials/compare/entry.html": "9278c555c0993b61e7d35a605417336f4cc64259ebf0b7f824575f801a43dda4", + "layouts/_partials/compare/page-key.html": "a69595b47434c15fd907a35bb9a952fc94b1e60b0dec794b03a3a48b9e15b83c", + "layouts/_partials/compare/scripts.html": "216e18f92409589ba17d5a3e89111ab679cf335ea3d7f327cc600f86c04ed0b8", + "layouts/_partials/compare/targets.html": "171b3f818dffb8092e6018228c7b84e5594b3e57c4be6d58896fe2eac84d05e8", "layouts/_partials/content/image-zoom-config.html": "3ad22eed2f6317dab16429cd91dffa2b01ab433674409b9ac5b4be41498b6fba", "layouts/_partials/content/render.html": "573629e404d2eb48dc53908c7b4540bf58c05de160f8274415482169a391411e", "layouts/_partials/dark-mode-config.html": "c7c07026dfe91e812ef94d9827dbfe6e756bd0e74dd619b43d146b5825410243", "layouts/_partials/footer.html": "6cbf0f74e6591b3235d47ed5cdad537c795508bd4af27e789962ec41f4b03544", + "layouts/_partials/head.html": "30e9ca8acd5d450ed8b034308e6eb5e7e493af0a6974a6801f86ea98a3abea9c", "layouts/_partials/hooks/body-end.html": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "layouts/_partials/hooks/head-end.html": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "layouts/_partials/navbar.html": "5c8063fc39a75b670eb24d3bdb255267ed4d7d7b3e1f11c91ca10ac09eeb3337", @@ -103,5 +114,6 @@ "layouts/_partials/shell/sidebar-panel.html": "32088fbf038706e7e45e21d35fbed906042594a3fc70bf0e18bf9a65512e0ec3", "layouts/_partials/shell/toc.html": "7f3bce19c87102e53bc87c1c6fca8dcf935bb72b53a1fe3cf8c1800f414622af", "layouts/landing.html": "073148bdbea02e8d4e5bd3cfc61c1efdc9213de73345a6027ba242eff014d8a7" - } + }, + "version": "v1.5.1" } diff --git a/scripts/oink-upgrade.md b/scripts/oink-upgrade.md index cf2042a7f9..55d6c57ee1 100644 --- a/scripts/oink-upgrade.md +++ b/scripts/oink-upgrade.md @@ -1,40 +1,59 @@ # OINK upgrade SOP -OINK is the site's sole theme module. Its version and two checksums live in -`go.mod` / `go.sum`; CI and historical builds use the same locks through -`oink_module.py`. Keep site changes in configuration, data, hooks and public APIs. -Do not edit the module cache, vendor a fork or run `go mod tidy` (there are no Go -source imports, so it can remove the theme). +OINK is the site's sole theme module. Its exact release and checksums live in `go.mod` / `go.sum`; latest and historical builds consume the same locks +through `oink_module.py`. Keep HugeGraph customizations in configuration, data, hooks and public OINK APIs. Do not edit the module cache, vendor a fork, +copy the Duo View implementation, or run `go mod tidy`: this repository has no Go source imports, so `tidy` can remove the theme module. + +This document uses the upstream product names consistently: + +- **OINK 1.5.1** is the theme release. +- **Duo View** is the English feature name; the Chinese UI label is **双列视图**. +- **Sync Scroll** is the English control; the Chinese UI label is **同步滚动**. ## Toolchain and upstream guidance -Use the Hugo Extended version pinned in `.github/workflows/hugo.yml`, the Go -version in `go.mod`, Python 3.13, WebP's `cwebp` / `dwebp`, and Node satisfying -`tests/e2e/package.json`. Node is only for browser tests. OINK 1.5.1 requires -Hugo Extended 0.168.0 and Go 1.27.3; Hugo's built-in LibSass compiles the site, -so no standalone Dart Sass or npm site-build dependencies are needed. -Set `HUGO_BIN` / `PYTHON_BIN` for task-local tools. - -Read the target release notes and the [official upgrade guide](https://oink.pgsty.com/zh/docs/admin/upgrade/). -For [OINK 1.5.1](https://github.com/pgsty/oink/releases/tag/v1.5.1), keep LibSass deprecation -warnings visible: do not add `--panicOnWarning` or suppress the warning through -`ignoreLogs`. The shared `scripts/hugo_build.py` runner streams the full output -and permits only that exact LibSass warning; every other Hugo `WARN`, including -missing translations and duplicate target paths, fails production builds. The -same gate covers local, versioned and AI fixture builds. Build errors, -source/output validation and browser contracts remain blocking. - -The official Module sequence is `hugo mod get`, `hugo mod tidy`, `hugo mod graph` -and a production build. Our runner adds HugeGraph's module identity, historical -navigation, search, consent and browser checks to that workflow. Its `go get` -and checksum cleanup preserve the site's sole-theme contract. Use the existing -runner; the upstream multi-consumer updater is unnecessary for this one site. +OINK 1.5.1 requires Hugo Extended 0.168.0 and Go 1.27.3. Hugo's built-in LibSass compiles the site, so no standalone Dart Sass or npm site-build +dependency is needed. Use Python 3.13, WebP's `cwebp` / `dwebp`, and a Node version satisfying `tests/e2e/package.json`; Node is used only by browser tests. +The pinned versions live in `.github/workflows/hugo.yml`, `go.mod`, and the local/legacy preview configuration. Set `HUGO_BIN`, `GO_BIN`, or `PYTHON_BIN` +only for task-local tools. + +Read the [OINK 1.5 upgrade guide](https://oink.pgsty.com/zh/docs/admin/upgrade/#preparing-1-5) and the +[OINK 1.5.1 release notes](https://github.com/pgsty/oink/releases/tag/v1.5.1) before changing the lock. Keep LibSass deprecation warnings visible: do not add +`--panicOnWarning` or suppress them through `ignoreLogs`. The shared `scripts/hugo_build.py` runner permits only the known LibSass warning; every other Hugo +`WARN`, including missing translations and duplicate target paths, remains blocking. + +The upstream generic Module sequence includes `hugo mod get`, `go mod tidy`, `hugo mod graph`, and a production build. HugeGraph's updater deliberately replaces +that sequence: it changes the exact pin, preserves the theme requirement, prunes only stale OINK checksums, verifies the published module graph, runs the full +site/version/browser checks, and updates the reviewed override inventory. + +## Duo View scope and route matrix + +Set `params.ui.compare: true` once. OINK then discovers only real, same-origin HTML translations for page types `docs`, `book`, and `blog`. It supplies the page +entry button, Command Palette action, `B` shortcut, lazy reader runtime, Sync Scroll, history/state restoration, mobile pane switcher, failure/retry UI, and +accessibility behavior. HugeGraph must not add a second comparison component or route map. + +HugeGraph currently publishes translated Docs and Blog pages but no real Book content tree. Book remains enabled through the native OINK type support; browser +verification starts when actual translated Book pages exist. Do not create fixture-like production Book pages merely to satisfy a matrix. + +| Release | English Docs | Chinese Docs | Blog | Book | +| --- | --- | --- | --- | --- | +| latest | `/docs/…` | `/cn/docs/…` | `/blog/…` and `/cn/blog/…` | No published HugeGraph Book route | +| 1.7 | `/versions/1.7/docs/…` | `/versions/1.7/cn/docs/…` | Shared latest Blog, not archived | No published HugeGraph Book route | +| 1.5 | `/versions/1.5/docs/…` | `/versions/1.5/cn/docs/…` | Shared latest Blog, not archived | No published HugeGraph Book route | +| 1.3 | `/versions/1.3/docs/…` | `/versions/1.3/cn/docs/…` | Shared latest Blog, not archived | No published HugeGraph Book route | +| 1.0 | `/versions/1.0/docs/…` | `/versions/1.0/cn/docs/…` | Shared latest Blog, not archived | No published HugeGraph Book route | + +For each real pair, validate both language entry routes. Desktop widths at or above OINK's 900 px breakpoint show two panes; narrower viewports show one +pane and native language tabs. A page with no real peer, a draft peer, a `build.render: link/never/false` peer, a non-HTML peer, or a later Blog archive page +must not show Duo View. Print, Markdown and RSS outputs must never carry the reader. + +Sync Scroll intentionally uses approximate alignment when heading IDs differ. Improve explicit shared heading IDs only on important pages where evidence shows a +poor reading experience; do not rewrite the documentation tree as part of a theme upgrade. ## Upgrade and review -Start with a clean worktree and capture the current site with its matching -toolchain. Network access is needed for modules, archived content, npm and -Chromium; ports 4173–4175 must be free. +Start from a clean worktree and capture the pre-upgrade OINK 1.4.0 site with its matching toolchain. Network access is needed for modules, archived content, npm +and Chromium; ports 4173-4175 must be free. ```sh upgrade_dir=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-oink-review.XXXXXX") @@ -45,184 +64,121 @@ scripts/hugo.sh build --destination "$upgrade_dir/before-site" npx playwright install chromium SITE_ROOT="$upgrade_dir/before-site" VISUAL_RESULTS_DIR="$upgrade_dir/before" npm run test:visual ) -# Select the target toolchain, then use an explicit release tag. + scripts/update-oink.sh v1.5.1 --work-dir "$upgrade_dir" ``` -The single command checks target tool requirements and a real Hugo/LibSass -compilation before changing locks. It then updates the pin, removes old -checksums, verifies Go/Hugo module identity and reports watched upstream changes -in `review.json`. With no review impacts it runs all checks and records the -new override baseline. It never installs tools, commits, pushes or publishes. +The updater first resolves target metadata and performs a real Hugo Extended/LibSass smoke compilation without changing locks. It then updates `go.mod` and +`go.sum`, verifies that `github.com/apache/hugegraph-doc` imports only published `github.com/pgsty/oink@v1.5.1`, and compares the reviewed upstream inventory in +`scripts/oink-overrides.json`. -When upstream changes affect local integrations, compare the old upstream, new -upstream and local override. Adapt only affected behavior, then resume: +When `review.json` reports an affected local integration, compare the old upstream implementation, OINK 1.5.1, and the HugeGraph override. Adapt only behavior +that is actually affected, then resume in the same work directory: ```sh scripts/update-oink.sh v1.5.1 --resume --accept-reviewed --work-dir "$upgrade_dir" ``` -`--accept-reviewed` acknowledges the reported compatibility review; it skips no -validation. A failed run retains the target pin, artifacts, Playwright report and -traces. Fix the cause and resume in the same directory. A same-version run also -validates changes to local overrides. The baseline advances only after all -checks pass. Inspect bilingual desktop/mobile before/after captures and obtain -an independent combined-diff review before publishing. - -## Validation and reuse - -The runner reuses existing link checks, Python source tests, production build, -bounded two-worker version builder, aggregate validator, blocking Chromium suite -and visual harness. It checks every selected version. Latest always rebuilds; -archived artifacts can be reused only when source, shared shell, toolchain, -origins and year fingerprints plus file digests match. Restored artifacts are -validated again. A theme or shell change invalidates those receipts automatically; -a browser-only repair can resume without recompiling unchanged archives. - -CI derives build groups from `versions.json`: adding a version needs no copied -job/action. A compatible historical bundle uses one warm group even when only -some per-version keys match; a fully cold shell uses at most three groups of -two workers. Compatibility lookup uses a shared-input prefix, while each -artifact still needs its exact version key, digest and validation before reuse. -Theme, Hugo, branding or version-menu changes correctly rebuild archives: -their static HTML shares the current shell. Updating ordinary latest Docs -content or browser/source tests leaves historical content reusable. - -Validation includes current `content/` and `static/` changes, including -non-ignored new files and deletions, using a temporary index and (if needed) an -unreferenced validation commit. It does not stage files or move HEAD/branches. -`validation-snapshot.json` records that scope; historical sources remain pinned -by `versions.json`. - -Release checks disable inherited Hugo replacements and preview themes, set -`GOWORK=off` / `HUGO_MODULE_WORKSPACE=off`, and reject `_vendor`. A correct -`go.mod` alone cannot prove Hugo used the selected theme. Read-only -`--check-baseline` uses disposable locks in a sibling workspace so Hugo's graph -initialization cannot rewrite the checkout; the parent directory must be writable. -Normal preview commands retain local-development capabilities. - -Manual CI dispatches inspect the immutable candidate's theme pin before scheduling -builds. OINK 1.3.0 manual candidates are outside this workflow's supported scope; -OINK 1.3.1 requires standalone Dart Sass. Both pins are rejected by the current -workflow; upgrade such a candidate to OINK 1.5.1 first. -This restriction concerns the candidate's theme, not historical documentation: -archived content continues to use the current site shell. - -Reuse the same external work directory to preserve the first lock/inventory -backup, `review.json`, `toolchain.json`, build artifacts and captures. Initial -runs reject reserved outputs instead of overwriting user files; existing -`before/` captures and `before-site/` are allowed. Resume never regenerates -baseline screenshots. A fresh `--resume` directory backs up the already-pinned -target, which `review.json` explicitly records. - -## Duo View acceptance matrix - -Use the product names **OINK**, **Duo View / 双列视图** and **Sync Scroll / -同步滚动** consistently. API names such as `params.ui.compare`, -`compare_translations` and `window.OinkComparePane` stay unchanged. - -The blocking browser suite uses real generated content and the aggregate -artifact. `tests/e2e/duo-view.spec.js` covers this route formula for every -selected version, both languages and both responsive layouts: - -| Version | English | Chinese | Desktop | Mobile | -| --- | --- | --- | --- | --- | -| latest | `/docs/` | `/cn/docs/` | two panes at 1440×900 | one visible pane at 390×844 | -| 1.7 | `/versions/1.7/docs/` | `/versions/1.7/cn/docs/` | same | same | -| 1.5 | `/versions/1.5/docs/` | `/versions/1.5/cn/docs/` | same | same | -| 1.3 | `/versions/1.3/docs/` | `/versions/1.3/cn/docs/` | same | same | -| 1.0 | `/versions/1.0/docs/` | `/versions/1.0/cn/docs/` | same | same | - -The detailed interaction path is the real HugeGraph Server quick-start pair; it -checks lazy loading, the page entry, Command Palette action, `B`, Sync Scroll, -continuous scrolling, fragment links, iframe isolation, reload, Back/Forward, -reading-position recovery and third-party request suppression. The real -ToplingDB Blog pair checks Blog/search/Print/Markdown/RSS coexistence. The -Chinese-only 0.10.4 release note checks that a missing translation has no entry -or command. A controlled failed pane checks the 15-second timeout contract, -accessible Retry and successful recovery. - -OINK supports Docs, Book and Blog through the same native discovery partial. -HugeGraph currently publishes no Book section, so do not add synthetic or -machine-translated Book content merely to manufacture coverage. The exact -OINK module lock and reviewed upstream inventory preserve Book support for the -first real bilingual Book page; upstream owns draft, non-HTML and `build.render` -filtering. Consumer tests concentrate on HugeGraph's real routes and hook -integrations rather than copying OINK's fixture suite. - -Visual CI captures English and Chinese Duo View at desktop and mobile sizes. -Review those images together with ordinary navigation, search, appearance and -content captures. No comparison runtime or peer page may be requested before a -user opens Duo View. - -## Staging, release and rollback - -A pull request builds the full aggregate but does not publish. After the same -commit exists on an ASF repository branch, run **Build and deploy site** from -ASF `master` with: +`--accept-reviewed` acknowledges the compatibility review; it skips no validation. A failed run retains locks, logs, Playwright traces, captures and the first +pre-upgrade backup. A same-version run validates later fixes against the same OINK release. The override baseline advances only after the required checks pass. + +## Compatibility review map + +| Integration | Local entry | Required evidence | +| --- | --- | --- | +| Navbar, mobile drawer and keyboard help | `layouts/_partials/navbar.html` | OINK 1.5.1 navbar semantics, desktop/mobile utilities and version links | +| Page actions and Command Palette | `layouts/_partials/actions/manifest.html` | Page-scoped `compare_translations` availability and no cache leakage | +| Language preference / historical shell | `language-preference.js`, `hugegraph-shell.js`, `layouts/` | No duplicate controllers; language/version/history | +| Kapa AI and consent | `kapa-adapter.js`, body/footer hooks, project SCSS | No duplicate widget, consent, request or floating launcher inside panes | +| SEO and language targets | `seo-alternates.html`, `language-targets.html` | Canonical/hreflang only for real translations across current and archived Docs | +| Branding and appearance | `assets/scss/`, `data/`, `i18n/` | Light/dark, presets, Print and mobile; pane content remains usable | +| Historical assembly and cache | `versioning.py`, `version_cache.py`, `versions.json` | Five-version aggregate; theme/toolchain invalidates archived caches | +| Native content outputs | `content/`, output validator | Docs/Blog, draft/missing peers, HTML-only entry, Print/Markdown/RSS isolation | + +A Duo View iframe is an ordinary same-origin page. Preserve content-level runtimes such as tables, tabs and diagrams, but suppress HugeGraph's top-level shell, +language preference controller and Kapa initialization inside `window.OinkComparePane`. Do not disable all scripts in the pane. + +`scripts/oink-overrides.json` stores upstream hashes, not copied source. Its inventory includes same-name local overrides, upstream SCSS selectors consumed by +project styles, and watched public interfaces. Added, removed, or changed hashes require review; unchanged hashes alone do not prove interaction compatibility. + +## Required validation + +Run the updater's complete validation and retain its report. At minimum, the evidence must include: + +1. Exact locks: OINK `v1.5.1`, both `go.sum` entries, Hugo Extended 0.168.0, Go 1.27.3, and built-in LibSass. +2. Source/link tests and a production Hugo build with no new warning class. +3. The five-version aggregate with English and Chinese Docs routes shown in the matrix above. +4. Real latest Docs and Blog pairs opened from both languages; the published Chinese 0.12.0 release notes page must not offer Duo View because its English peer + is still a draft. +5. Page button, Command Palette, `B`, close/reopen, Back/Forward, refresh restoration, reading-position restoration, fragment navigation, Sync Scroll and + continuous scrolling. +6. Desktop two-pane and mobile single-pane/tab behavior, light/dark themes, Print, RSS, Markdown, search, version switching and Kapa coexistence. +7. Lazy loading: before opening, no translated page or `document-compare.js` request; inside panes, no duplicate Kapa load or HugeGraph top-level controllers. +8. Failure, Retry and direct-link fallback, keyboard focus, dialog name, pane titles and reduced-motion behavior. +9. Before/after desktop Duo View captures, mobile captures, and a short Sync Scroll GIF. Use real HugeGraph pages, not only generated fixtures. +10. An independent combined-diff review covering correctness, native reuse, security/CSP, performance and over-design. + +OINK's release fixtures cover Docs/Book/Blog discovery, draft/non-HTML filtering, output isolation, lazy assets, pane runtime classification and reader +geometry. HugeGraph's Playwright suite adds real content, historical paths, local override behavior and product integration. Review both layers; neither +replaces the other. + +## Cache and build behavior + +Latest always rebuilds. Historical artifacts may be reused only when their source, current shared shell, theme lock, Hugo/Go toolchain, origins, year +fingerprints and file digests match. A change to `go.sum`, OINK inventory, shared layouts, styles or shell scripts invalidates the relevant cache receipts; +every restored artifact is validated again. Browser-only fixes may reuse unchanged compiled archives. + +Release checks clear Hugo replacements and preview themes, set `GOWORK=off` and `HUGO_MODULE_WORKSPACE=off`, and reject `_vendor`. A correct `go.mod` is not +proof that Hugo used the selected module; the module graph and rendered artifacts are both checked. + +`netlify.toml` is retained only for legacy/ad-hoc previews. ASF staging and production use `.github/workflows/hugo.yml`; Netlify settings must not become a +second release authority. + +## Staging release gate + +Do not publish Production as part of an upgrade PR. After the candidate branch exists in the ASF repository and PR CI passes, dispatch **Build and deploy site** +from ASF `master` with the immutable candidate branch and these inputs: ```text operation: staging-next scope: full -candidate_branch: +candidate_branch: confirmation: publish asf-staging-oink ``` -Verify `https://hugegraph-oink.staged.apache.org/` with the complete matrix, -canonical/hreflang, version switching, search, Kapa consent, Print/RSS and the -visual artifact. Do not run `production-history-refresh` and do not publish -`asf-site` without separate authorization. An ORG-only branch cannot satisfy -this Staging gate because the privileged workflow resolves `candidate_branch` -inside the ASF repository. - -Record the pre-upgrade commit and the candidate commit before Staging. Recovery -is deliberately symmetric: - -1. **Before merge or publication:** restore `go.mod`, `go.sum`, `hugo.yaml`, - the reviewed overrides and compatibility changes from the recorded - pre-upgrade commit, then rerun the complete PR suite. -2. **After merge but before Production:** revert the focused upgrade commit on - an ASF branch and dispatch `staging-next` with `scope: full` for that revert. - Verify all five versions before merging the revert. -3. **After a future Production publication:** merge the tested revert through - the normal ASF review flow and let the existing `master` deployment restore - `asf-site`; never force-update a publish branch or reuse an unverified cached - artifact. - -The legacy `netlify.toml` is not consumed by the ASF/GitHub Actions historical -builder and is removed from archived assemblies. It remains aligned with the -locked Go/Hugo versions for optional local/Netlify previews, without -`--panicOnWarning`; the GitHub workflow is the deployment authority. - -## Customization review map +`scope: full` is required for this upgrade because the current theme is applied to latest plus 1.7, 1.5, 1.3 and 1.0. Verify the staged origin's English/Chinese +route matrix, same-origin iframe policy, canonical/hreflang, search, Kapa consent, Print/RSS/Markdown outputs, desktop/mobile behavior and captures. Record the +candidate commit and the workflow run; a local build or ORG-only branch is not an ASF Staging pass. -| Integration | Local entry | Required evidence | -| --- | --- | --- | -| Version/language navigation, historical shell | `layouts/`, `scripts/versioning.py` | All-version aggregate and navigation/browser contracts | -| Language preference and sidebar memory | `language-preference.js`, `hugegraph-shell.js`, head hook | Storage, focus, keyboard and public OinkSidebar API | -| Ask AI consent and community entry | `hugo.yaml`, AI config partial, `kapa-adapter.js`, body/footer hooks | Enabled/disabled AI fixtures, public search-tail API | -| Branding and appearance | `assets/scss/`, `data/`, `i18n/` | Four presets, light/dark, bilingual desktop/mobile and Print | -| Native content | `content/`, output validator | Component behavior, actual payload sinks, Print/Markdown fallbacks | - -`scripts/oink-overrides.json` stores upstream hashes, not copied source. The -inventory covers same-name local layouts/assets/translations/data, watched -sidebar/search/surface/appearance interfaces and upstream SCSS selectors used -locally. Added/removed/changed entries require review; unchanged hashes do not -prove interaction compatibility. After adding/removing an override, use -`scripts/update-oink.sh --resume --accept-reviewed` to validate -and record the inventory. - -Use [OINK features](oink-features.md) for native component selection and real -site examples. Prefer native configuration/components over a copied template, -CSS patch or second DOM controller. When removing shared styles, also check -archived content in its temporary assembly; do not rewrite release branches. -Visible virtual Docs groups must omit `toc_hide`, which hides whole subtrees. - -## Recovery - -Keep upgrade/adaptations/baseline in a dedicated commit. Revert that commit to -undo a committed upgrade, then validate the restored version with `--resume`. -For an uncommitted run, inspect the diff and restore only upgrade files from the -known pre-upgrade revision or the first work-directory backup. Do not reset -unrelated work. The runner does not roll back automatically. Publication uses -the existing release workflow. +Production remains a separate authorized operation. Do not dispatch `production-history-refresh` and do not update `asf-site` until Staging evidence is accepted +and explicit Production approval is given. + +## Rollback + +Keep the version lock, compatibility fixes, tests and reviewed baseline in one focused upgrade commit or a small contiguous commit series. + +### Before Production + +1. Revert the complete upgrade commit/series; do not reset unrelated work. +2. Confirm `go.mod`, `go.sum`, `hugo.yaml` and `scripts/oink-overrides.json` are back to the OINK 1.4.0 state. +3. From a clean worktree, run a same-version validation with a new work directory: + + ```sh + rollback_dir=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-oink-rollback.XXXXXX") + scripts/update-oink.sh v1.4.0 --work-dir "$rollback_dir" + ``` + +4. Run the full five-version browser matrix and publish only to Staging with `scope: full`. +5. Compare the staged rollback against the retained pre-upgrade captures before any Production decision. + +### Uncommitted recovery + +Use the first updater work-directory backup or `git restore --source=` for only the upgrade-owned files. Do not use a broad hard +reset. +The updater does not roll back automatically. + +### After Production + +Use the repository's normal git revert and release workflow; never patch generated `asf-site` content by hand. Rebuild all five versions from the reverted +source, validate Staging, then publish Production only with the same explicit authorization used for the original release. Retain both release workflow run +IDs and the rollback commit for auditability. diff --git a/scripts/update_oink.py b/scripts/update_oink.py index 40bd6dd358..508715ae98 100644 --- a/scripts/update_oink.py +++ b/scripts/update_oink.py @@ -28,6 +28,17 @@ "assets/js/docs-shell.js", "assets/js/search.js", "assets/js/search-engine.js", "assets/js/surface-coordinator.js", "assets/js/command-palette.js", "assets/js/appearance.js", "assets/js/dark-mode.js", + "assets/js/compare-entry.js", "assets/js/compare-scroll.js", + "assets/js/document-compare.js", + "layouts/_partials/actions/context.html", + "layouts/_partials/head.html", + "layouts/_partials/compare/analytics.html", + "layouts/_partials/compare/bootstrap.html", + "layouts/_partials/compare/enabled.html", + "layouts/_partials/compare/entry.html", + "layouts/_partials/compare/page-key.html", + "layouts/_partials/compare/scripts.html", + "layouts/_partials/compare/targets.html", "layouts/_partials/shell/appearance-menu.html", "layouts/_partials/preset-config.html", "layouts/_partials/dark-mode-config.html") diff --git a/tests/e2e/duo-view.spec.js b/tests/e2e/duo-view.spec.js index 446413cab4..26f1ac1ef0 100644 --- a/tests/e2e/duo-view.spec.js +++ b/tests/e2e/duo-view.spec.js @@ -1,246 +1,277 @@ const { test, expect } = require('./artifact-test'); const VERSION_IDS = ['latest', '1.7', '1.5', '1.3', '1.0']; -const EXPECTED_IDS = (process.env.EXPECTED_VERSIONS || VERSION_IDS.join(',')).split(','); +const EXPECTED_IDS = new Set((process.env.EXPECTED_VERSIONS || VERSION_IDS.join(',')) + .split(',').map(value => value.trim()).filter(Boolean)); -function versionPrefix(version) { - return version === 'latest' ? '' : `/versions/${version}`; +function docsRoute(version, locale, relative = '') { + const versionPrefix = version === 'latest' ? '' : `/versions/${version}`; + const languagePrefix = locale === 'cn' ? '/cn' : ''; + return `${versionPrefix}${languagePrefix}/docs/${relative}`; } -function docsRoute(version, locale) { - return `${versionPrefix(version)}${locale === 'cn' ? '/cn' : ''}/docs/`; -} - -function alternateDocsRoute(version, locale) { - return docsRoute(version, locale === 'cn' ? 'en' : 'cn'); +async function compareConfig(page) { + return page.locator('#td-compare-config').evaluate(node => JSON.parse(node.textContent)); } -async function config(page) { - return page.locator('#td-compare-config').evaluate(node => JSON.parse(node.textContent)); +function resolvedURL(page, value) { + return new URL(value, page.url()); } async function actionManifest(page) { return page.locator('#td-action-manifest').evaluate(node => JSON.parse(node.textContent)); } -function paneFrames(page) { - return page.frames().filter(frame => { - try { - return new URL(frame.url()).searchParams.get('oink_compare_pane') === '1'; - } catch (_) { - return false; - } - }); -} - -async function waitForReadyPanes(page) { +async function paneFrames(page) { const dialog = page.locator('dialog[data-td-compare]'); - await expect(dialog).toBeVisible(); - await expect(dialog.locator('[data-td-compare-frame]')).toHaveCount(2); - await expect.poll(() => paneFrames(page).length).toBe(2); - for (const frame of paneFrames(page)) { - await expect.poll(() => frame.evaluate(() => Boolean( - window.OinkComparePane && - document.documentElement.hasAttribute('data-td-compare-pane') && - document.querySelector('meta[name="oink-compare-page"]') && - document.querySelector('#td-main-content .td-content') - ))).toBe(true); + await expect(dialog).toBeVisible({ timeout: 15_000 }); + const locators = dialog.locator('iframe[data-td-compare-frame]'); + await expect(locators).toHaveCount(2); + const frames = []; + for (let index = 0; index < 2; index += 1) { + const handle = await locators.nth(index).elementHandle(); + const frame = await handle.contentFrame(); + await frame.waitForFunction(() => window.OinkComparePane === true && + Boolean(document.querySelector('meta[name="oink-compare-page"]')) && + Boolean(document.querySelector('#td-main-content .td-content'))); + frames.push(frame); } - await expect(dialog.locator('[data-td-compare-sync]')).toBeEnabled(); - return dialog; + return { dialog, locators, frames }; } -async function visiblePaneCount(dialog) { - return dialog.locator('[data-td-compare-side]').evaluateAll(nodes => nodes.filter(node => { - const style = getComputedStyle(node); - return style.visibility !== 'hidden' && style.display !== 'none'; - }).length); +async function closeReader(page) { + const dialog = page.locator('dialog[data-td-compare]'); + if (await dialog.count()) { + await dialog.locator('[data-td-compare-close]').click(); + await expect(dialog).toHaveCount(0); + await expect.poll(() => page.evaluate(() => history.state?.oinkCompareReader || null)).toBe(null); + } } for (const version of VERSION_IDS) { for (const locale of ['en', 'cn']) { - test(`${version} ${locale} uses native Duo View on desktop and one-pane mobile`, async ({ page }) => { - test.skip(!EXPECTED_IDS.includes(version), 'version not selected for this artifact'); - await page.setViewportSize({ width: 1440, height: 900 }); + test(`${version} ${locale} Duo View covers desktop and mobile Docs`, async ({ page }) => { + test.skip(!EXPECTED_IDS.has(version), 'version absent from this aggregate artifact'); + test.setTimeout(60_000); const route = docsRoute(version, locale); - await page.goto(route); - - const descriptor = await config(page); - expect(descriptor.current.url).toBe(route); - expect(descriptor.targets.map(target => target.url)).toContain(alternateDocsRoute(version, locale)); - expect(descriptor.shortcut).toBe('B'); - const manifest = await actionManifest(page); - expect(manifest.actions.find(action => action.id === 'compare_translations')).toMatchObject({ - available: true, - kind: 'invoke', - }); - - const entry = page.locator('[data-td-compare-open]'); - await expect(entry).toBeVisible(); - await expect(entry).toHaveAttribute('aria-keyshortcuts', 'B'); - await entry.click(); - const dialog = await waitForReadyPanes(page); - await expect.poll(() => visiblePaneCount(dialog)).toBe(2); - - await page.setViewportSize({ width: 390, height: 844 }); - const tabs = dialog.locator('.td-compare__tab'); - await expect(tabs).toHaveCount(2); - await expect(tabs.first()).toHaveAttribute('aria-pressed', 'true'); - await expect.poll(() => visiblePaneCount(dialog)).toBe(1); - await tabs.nth(1).click(); - await expect(tabs.nth(1)).toHaveAttribute('aria-pressed', 'true'); - await expect.poll(() => visiblePaneCount(dialog)).toBe(1); - - await dialog.locator('[data-td-compare-close]').click(); - await expect(page.locator('dialog[data-td-compare]')).toHaveCount(0); + const expectedTarget = docsRoute(version, locale === 'en' ? 'cn' : 'en'); + + for (const viewport of [{ width: 1440, height: 900 }, { width: 390, height: 844 }]) { + await page.setViewportSize(viewport); + await page.goto(route); + const darkMobile = version === 'latest' && locale === 'cn' && viewport.width < 900; + if (darkMobile) { + await page.evaluate(() => localStorage.setItem('td-color-theme', 'dark')); + await page.reload({ waitUntil: 'domcontentloaded' }); + await expect(page.locator('html')).toHaveAttribute('data-bs-theme', 'dark'); + } + const entry = page.locator('[data-td-compare-open]'); + await expect(entry).toBeVisible(); + const config = await compareConfig(page); + expect(config.current.url).toBeTruthy(); + expect(config.targets).toHaveLength(1); + expect(resolvedURL(page, config.targets[0].url).pathname).toBe(expectedTarget); + const manifest = await actionManifest(page); + expect(manifest.actions.some(action => action.id === 'compare_translations')).toBe(true); + expect(manifest.actions.some(action => action.id === 'switch_version')).toBe(true); + + const targetLocale = locale === 'en' ? 'zh-CN' : 'en-US'; + const alternate = page.locator(`link[rel="alternate"][hreflang="${targetLocale}"]`); + await expect(alternate).toHaveCount(1); + expect(new URL(await alternate.getAttribute('href')).pathname).toBe(expectedTarget); + expect(new URL(await page.locator('link[rel="canonical"]').getAttribute('href')).pathname).toBe(route); + + await entry.click(); + const { dialog, locators, frames } = await paneFrames(page); + await expect(dialog).toHaveAccessibleName(locale === 'cn' ? '双列视图' : 'Duo View'); + await expect(locators.nth(0)).toHaveAttribute('title', config.current.label); + await expect(locators.nth(1)).toHaveAttribute('title', config.targets[0].label); + await expect(dialog.locator('[data-td-compare-close]')).toHaveAttribute('aria-keyshortcuts', /Escape B/); + + const panes = dialog.locator('[data-td-compare-side]'); + if (viewport.width >= 900) { + await expect(panes.nth(0)).toHaveAttribute('aria-hidden', 'false'); + await expect(panes.nth(1)).toHaveAttribute('aria-hidden', 'false'); + } else { + await expect(dialog.locator('.td-compare__tab')).toHaveCount(2); + await expect(panes.nth(0)).toHaveAttribute('aria-hidden', 'false'); + await expect(panes.nth(1)).toHaveAttribute('aria-hidden', 'true'); + await dialog.locator('.td-compare__tab').nth(1).click(); + await expect(panes.nth(0)).toHaveAttribute('aria-hidden', 'true'); + await expect(panes.nth(1)).toHaveAttribute('aria-hidden', 'false'); + } + + for (const [index, frame] of frames.entries()) { + expect(await frame.evaluate(() => new URL(location.href).searchParams.get('oink_compare_pane'))).toBe('1'); + const canonicalPath = await frame.locator('link[rel="canonical"]').evaluate(node => new URL(node.href).pathname); + expect(canonicalPath).toBe(index === 0 ? route : expectedTarget); + expect(await frame.evaluate(() => document.querySelector('.hg-ask-ai-launcher')?.dataset.hgAiBound)).toBeUndefined(); + if (darkMobile) { + expect(await frame.evaluate(() => document.documentElement.getAttribute('data-bs-theme'))).toBe('dark'); + } + const launcher = frame.locator('.hg-ask-ai-launcher'); + if (await launcher.count()) await expect(launcher).toBeHidden(); + } + await closeReader(page); + } }); } } -test('real Docs pair is lazy, pane-isolated, synchronized and history-restorable', async ({ page }) => { - const runtimeRequests = []; - const thirdPartyRequests = []; - page.on('request', request => { - if (request.url().includes('document-compare')) runtimeRequests.push(request.url()); - if (/kapa\.ai|hcaptcha\.com|kapa-widget-proxy/.test(request.url())) thirdPartyRequests.push(request.url()); - }); - +test('real Docs page lazily opens, synchronizes, restores and exposes all native entries', async ({ page }) => { + test.setTimeout(90_000); + const requested = []; + page.on('request', request => requested.push(request.url())); await page.setViewportSize({ width: 1440, height: 900 }); + await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto('/docs/quickstart/hugegraph/hugegraph-server/'); - expect(runtimeRequests).toEqual([]); - const descriptor = await config(page); - expect(descriptor.targets.map(target => target.url)).toContain( - '/cn/docs/quickstart/hugegraph/hugegraph-server/' - ); + expect(await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches)).toBe(true); - await page.keyboard.press('b'); - let dialog = await waitForReadyPanes(page); - expect(runtimeRequests.length).toBeGreaterThan(0); - let frames = paneFrames(page); - for (const frame of frames) { - expect(await frame.evaluate(() => ({ - shell: typeof window.HugeGraphShell, - kapa: typeof window.HugeGraphKapa, - pane: window.OinkComparePane, - }))).toEqual({ shell: 'undefined', kapa: 'undefined', pane: true }); - } - expect(thirdPartyRequests).toEqual([]); + const config = await compareConfig(page); + const targetPath = resolvedURL(page, config.targets[0].url).pathname; + expect(requested.some(url => url.includes('document-compare'))).toBe(false); + expect(requested.some(url => new URL(url).pathname === targetPath)).toBe(false); + + const readingHeading = page.locator('#td-main-content .td-content h2[id]').nth(2); + await readingHeading.scrollIntoViewIfNeeded(); + const sourceScroll = await page.evaluate(() => scrollY); + expect(sourceScroll).toBeGreaterThan(0); + await page.keyboard.press('b'); + let { dialog, frames } = await paneFrames(page); + expect(requested.some(url => url.includes('document-compare'))).toBe(true); + expect(requested.some(url => new URL(url).pathname === targetPath)).toBe(true); const sync = dialog.locator('[data-td-compare-sync]'); - await expect(sync).toHaveAttribute('role', 'switch'); + await expect(sync).toBeEnabled(); await expect(sync).toHaveAttribute('aria-checked', 'true'); await sync.click(); await expect(sync).toHaveAttribute('aria-checked', 'false'); await sync.click(); await expect(sync).toHaveAttribute('aria-checked', 'true'); + expect(await frames[0].evaluate(() => scrollY)).toBeGreaterThan(0); + await expect.poll(() => frames[1].evaluate(() => scrollY)).toBeGreaterThan(0); - await frames[0].evaluate(() => window.scrollTo({ - top: Math.max(500, document.documentElement.scrollHeight * 0.55), - behavior: 'instant', - })); - await expect.poll(() => frames[1].evaluate(() => window.scrollY)).toBeGreaterThan(100); - const firstFollowerPosition = await frames[1].evaluate(() => window.scrollY); - await frames[0].evaluate(() => window.scrollTo({ - top: document.documentElement.scrollHeight * 0.8, - behavior: 'instant', - })); - await expect.poll(() => frames[1].evaluate(() => window.scrollY)) - .toBeGreaterThan(firstFollowerPosition + 20); - - const followerBeforeFragment = await frames[1].evaluate(() => window.scrollY); - const selfLink = frames[0].locator('#td-main-content .td-content a.td-heading-self-link').nth(2); - await expect(selfLink).toBeAttached(); - await selfLink.click(); - await expect.poll(() => new URL(frames[0].url()).hash.length).toBeGreaterThan(1); - await expect.poll(async () => Math.abs( - await frames[1].evaluate(() => window.scrollY) - followerBeforeFragment - )).toBeGreaterThan(20); - - await page.waitForTimeout(450); - await page.reload(); - dialog = await waitForReadyPanes(page); - frames = paneFrames(page); - await expect.poll(() => frames[0].evaluate(() => window.scrollY)).toBeGreaterThan(100); - await expect.poll(() => frames[1].evaluate(() => window.scrollY)).toBeGreaterThan(100); + const targetBefore = await frames[1].evaluate(() => scrollY); + await frames[0].evaluate(() => { + document.dispatchEvent(new WheelEvent('wheel', { bubbles: true })); + window.scrollTo({ top: Math.min(document.documentElement.scrollHeight - innerHeight, scrollY + 700), behavior: 'instant' }); + }); + await expect.poll(() => frames[1].evaluate(() => scrollY)).not.toBe(targetBefore); + const targetAfterFirstMove = await frames[1].evaluate(() => scrollY); + await frames[0].evaluate(() => { + document.dispatchEvent(new WheelEvent('wheel', { bubbles: true })); + window.scrollBy({ top: 500, behavior: 'instant' }); + }); + await expect.poll(() => frames[1].evaluate(() => scrollY)).not.toBe(targetAfterFirstMove); + + const fragment = await frames[0].evaluate(() => { + const heading = [...document.querySelectorAll('#td-main-content .td-content h2[id], #td-main-content .td-content h3[id]')] + .find(node => node.getBoundingClientRect().height > 0 && node.id); + if (!heading) return null; + const link = [...document.querySelectorAll('a[href]')].find(anchor => { + try { return new URL(anchor.href).hash === `#${heading.id}`; } catch (_) { return false; } + }); + if (!link) return null; + link.click(); + return `#${heading.id}`; + }); + expect(fragment).toBeTruthy(); + await expect.poll(() => frames[0].evaluate(() => location.hash)).toBe(fragment); await page.keyboard.press('b'); await expect(page.locator('dialog[data-td-compare]')).toHaveCount(0); - await expect.poll(() => page.evaluate(() => Boolean(history.state?.oinkCompareReader))).toBe(false); - await page.goForward(); - await waitForReadyPanes(page); - await page.goBack(); - await expect(page.locator('dialog[data-td-compare]')).toHaveCount(0); + await expect.poll(() => page.evaluate(() => history.state?.oinkCompareReader || null)).toBe(null); await page.locator('[data-td-shell-search-open]').first().click(); const input = page.locator('.td-shell-search__input'); - await input.fill('>Duo View'); - const command = page.locator('[role="option"]').filter({ hasText: 'Duo View' }).first(); - await expect(command).toBeVisible(); - await command.click(); - await waitForReadyPanes(page); -}); - -test('real Blog translation coexists with search, Print, Markdown and RSS', async ({ page, request }) => { - const route = '/blog/2025/10/09/toplingdb-quick-start/'; - await page.goto(route); - const descriptor = await config(page); - expect(descriptor.targets.map(target => target.url)).toContain( - '/cn/blog/2025/10/09/toplingdb-quick-start/' - ); - await page.locator('[data-td-compare-open]').click(); - const dialog = await waitForReadyPanes(page); - await dialog.locator('[data-td-compare-close]').click(); - await page.locator('[data-td-shell-search-open]').first().click(); - await page.locator('.td-shell-search__input').fill('ToplingDB'); - await expect(page.locator('[role="option"]').first()).toBeVisible(); - - for (const output of ['/blog/index.xml', '/_print/docs/', `${route}index.md`]) { - const response = await request.get(output); - expect(response.ok(), output).toBeTruthy(); - const body = await response.text(); - expect(body, output).not.toContain('id="td-compare-config"'); - expect(body, output).not.toMatch(/]+data-td-compare-open/); - } -}); + await input.fill('Duo View'); + await page.getByRole('option').filter({ hasText: /^Duo View$/ }).first().click(); + ({ dialog } = await paneFrames(page)); -test('a real one-language page exposes no Duo View entry or command', async ({ page }) => { - await page.goto('/cn/docs/changelog/hugegraph-0.10.4-release-notes/'); - await expect(page.locator('#td-compare-config')).toHaveCount(0); - await expect(page.locator('[data-td-compare-open]')).toHaveCount(0); - const manifest = await actionManifest(page); - expect(manifest.actions.some(action => action.id === 'compare_translations')).toBe(false); - await page.keyboard.press('b'); + await page.reload({ waitUntil: 'domcontentloaded' }); + ({ dialog, frames } = await paneFrames(page)); + expect(await frames[0].evaluate(() => scrollY)).toBeGreaterThan(0); + await page.goBack(); await expect(page.locator('dialog[data-td-compare]')).toHaveCount(0); + await page.goForward(); + await expect(page.locator('dialog[data-td-compare]')).toBeVisible({ timeout: 15_000 }); + await closeReader(page); }); -test('failed pane times out to an accessible retry and recovers', async ({ page }) => { - await page.addInitScript(() => { - const nativeSetTimeout = window.setTimeout.bind(window); +test('Duo View times out a stalled pane, retries and keeps an explicit direct link', async ({ page }) => { + test.setTimeout(45_000); + await page.goto('/docs/quickstart/hugegraph/hugegraph-server/'); + const config = await compareConfig(page); + const target = resolvedURL(page, config.targets[0].url); + await page.evaluate(() => { + const nativeTimeout = window.setTimeout.bind(window); window.setTimeout = (callback, delay, ...args) => - nativeSetTimeout(callback, delay === 15000 ? 80 : delay, ...args); + nativeTimeout(callback, delay === 15_000 ? 100 : delay, ...args); }); - let held = false; - await page.route('**/*', async route => { - const url = new URL(route.request().url()); - if (!held && url.pathname === '/cn/docs/' && url.searchParams.get('oink_compare_pane') === '1') { - held = true; - await new Promise(resolve => setTimeout(resolve, 250)); - try { await route.abort('failed'); } catch (_) { /* The timed-out frame may already be gone. */ } - return; + let attempts = 0; + await page.route(url => url.pathname === target.pathname && url.searchParams.get('oink_compare_pane') === '1', async route => { + attempts += 1; + if (attempts === 1) { + await new Promise(resolve => setTimeout(resolve, 300)); + try { await route.continue(); } catch (_) { /* Timed-out iframe was disposed. */ } + } else { + await route.continue(); } - await route.fallback(); }); - await page.goto('/docs/'); await page.locator('[data-td-compare-open]').click(); const dialog = page.locator('dialog[data-td-compare]'); await expect(dialog).toBeVisible(); - const failure = dialog.locator('.td-compare__failure'); - await expect(failure).toBeVisible(); - await expect(failure).toContainText('Could not load this page.'); - const retry = failure.locator('.td-compare__retry'); - await expect(retry).toHaveAccessibleName('Retry'); - await expect(failure.locator('a[target="_blank"]')).toHaveAttribute('rel', 'noopener'); + const retry = dialog.locator('.td-compare__retry'); + await expect(retry).toBeVisible({ timeout: 15_000 }); + const direct = retry.locator('xpath=following-sibling::a'); + await expect(direct).toHaveAttribute('href', target.href); + await expect(direct).toHaveAttribute('target', '_blank'); + await expect(direct).toHaveAttribute('rel', 'noopener'); + await retry.click(); - await waitForReadyPanes(page); + const { frames } = await paneFrames(page); + expect(attempts).toBe(2); + expect(await frames[1].evaluate(() => document.querySelector('meta[name="oink-compare-page"]')?.content)).toBe(target.pathname); + await closeReader(page); +}); + +test('draft/missing peers and non-HTML outputs never expose Duo View', async ({ page, request }) => { + await page.goto('/cn/docs/changelog/hugegraph-0.12.0-release-notes/'); + await expect(page.locator('[data-td-compare-open]')).toHaveCount(0); + await expect(page.locator('#td-compare-config')).toHaveCount(0); + const manifest = await actionManifest(page); + expect(manifest.actions.some(action => action.id === 'compare_translations')).toBe(false); + + for (const route of [ + '/docs/quickstart/hugegraph/hugegraph-server/index.md', + '/_print/docs/', + '/docs/index.xml', + ]) { + const response = await request.get(route); + expect(response.ok(), route).toBeTruthy(); + const body = await response.text(); + expect(body, route).not.toContain('td-compare-config'); + expect(body, route).not.toContain('data-td-compare-open'); + } }); + +for (const [locale, route] of [ + ['en', '/blog/2025/10/09/toplingdb-quick-start/'], + ['cn', '/cn/blog/2025/10/09/toplingdb-quick-start/'], +]) { + test(`${locale} real Blog page uses native Duo View without preloading the peer`, async ({ page }) => { + const requested = []; + page.on('request', request => requested.push(request.url())); + await page.goto(route); + const config = await compareConfig(page); + const target = resolvedURL(page, config.targets[0].url); + await expect(page.locator('[data-td-compare-open]')).toBeVisible(); + expect(requested.some(url => new URL(url).pathname === target.pathname)).toBe(false); + await page.locator('[data-td-compare-open]').click(); + await paneFrames(page); + expect(requested.some(url => new URL(url).pathname === target.pathname)).toBe(true); + expect(requested.filter(url => /(?:kapa\.ai|hcaptcha\.com|a\.run\.app)/.test(url))).toEqual([]); + await closeReader(page); + }); +} diff --git a/tests/e2e/visual.spec.js b/tests/e2e/visual.spec.js index d2802308d0..fb947185f2 100644 --- a/tests/e2e/visual.spec.js +++ b/tests/e2e/visual.spec.js @@ -5,6 +5,10 @@ const path = require("node:path"); const states = [ ["en-docs-desktop-light", "/docs/", { width: 1440, height: 900 }, "light", "default"], ["cn-docs-desktop-dark", "/cn/docs/", { width: 1440, height: 900 }, "dark", "default"], + ["en-duo-desktop-light", "/docs/quickstart/hugegraph/hugegraph-server/", { width: 1440, height: 900 }, "light", "duo"], + ["cn-duo-desktop-light", "/cn/docs/quickstart/hugegraph/hugegraph-server/", { width: 1440, height: 900 }, "light", "duo"], + ["en-duo-mobile-light", "/docs/quickstart/hugegraph/hugegraph-server/", { width: 390, height: 844 }, "light", "duo-mobile"], + ["cn-duo-mobile-dark", "/cn/docs/quickstart/hugegraph/hugegraph-server/", { width: 390, height: 844 }, "dark", "duo-mobile"], ["en-search-desktop-light", "/docs/", { width: 1440, height: 900 }, "light", "search"], ["cn-search-mobile-dark", "/cn/docs/", { width: 390, height: 844 }, "dark", "search"], ["en-sidebar-desktop-light", "/docs/introduction/", { width: 1440, height: 900 }, "light", "collapse"], @@ -15,10 +19,6 @@ const states = [ ["cn-appearance-mobile-dark", "/cn/docs/introduction/", { width: 390, height: 844 }, "dark", "appearance"], ["en-paper-desktop-light", "/docs/introduction/", { width: 1440, height: 900 }, "light", "paper"], ["cn-paper-mobile-dark", "/cn/docs/introduction/", { width: 390, height: 844 }, "dark", "paper"], - ["en-duo-docs-desktop-light", "/docs/quickstart/hugegraph/hugegraph-server/", { width: 1440, height: 900 }, "light", "duo"], - ["cn-duo-docs-desktop-light", "/cn/docs/quickstart/hugegraph/hugegraph-server/", { width: 1440, height: 900 }, "light", "duo"], - ["en-duo-docs-mobile-light", "/docs/quickstart/hugegraph/hugegraph-server/", { width: 390, height: 844 }, "light", "duo"], - ["cn-duo-docs-mobile-light", "/cn/docs/quickstart/hugegraph/hugegraph-server/", { width: 390, height: 844 }, "light", "duo"], ...["en", "cn"].flatMap(language => { const prefix = language === "cn" ? "/cn" : ""; return ["slate", "paper"].flatMap(preset => ["light", "dark"].flatMap(theme => { @@ -180,16 +180,21 @@ for (const [name, url, viewport, theme, state, preset = "slate"] of states) { await expect(page.locator("html")).toHaveAttribute("data-td-preset", "paper"); await page.keyboard.press("Escape"); } - } else if (state === "duo") { + } else if (state === "duo" || state === "duo-mobile") { await page.locator("[data-td-compare-open]").click(); const dialog = page.locator("dialog[data-td-compare]"); await expect(dialog).toBeVisible(); - await expect(dialog.locator("[data-td-compare-frame]")).toHaveCount(2); - await expect.poll(() => page.frames().filter(frame => { - try { return new URL(frame.url()).searchParams.get("oink_compare_pane") === "1"; } - catch (_) { return false; } - }).length).toBe(2); - await expect(dialog.locator("[data-td-compare-sync]")).toBeEnabled(); + await expect(dialog.locator("iframe[data-td-compare-frame]")).toHaveCount(2); + for (const frame of await dialog.locator("iframe[data-td-compare-frame]").all()) { + const handle = await frame.elementHandle(); + const content = await handle.contentFrame(); + await content.waitForFunction(() => window.OinkComparePane === true && + Boolean(document.querySelector("#td-main-content .td-content"))); + } + if (state === "duo-mobile") { + await dialog.locator(".td-compare__tab").nth(1).click(); + await expect(dialog.locator("[data-td-compare-side='1']")).toHaveAttribute("aria-hidden", "false"); + } } else if (state === "search") { await page.locator("[data-td-shell-search-open]").first().click(); await page.locator(".td-shell-search__input").fill(url.startsWith("/cn/") ? "服务端" : "server"); @@ -226,7 +231,7 @@ for (const [name, url, viewport, theme, state, preset = "slate"] of states) { await page.screenshot({ animations: "disabled", path: path.join(directory, `${name}.png`), - fullPage: !["gallery", "chart", "longcode", "markmap", "terminal", "math", "pd", "computer", "config-api", "helm", "footer", "hubble-import", "hubble-nodes", "duo"].includes(state) + fullPage: !["gallery", "chart", "longcode", "markmap", "terminal", "math", "pd", "computer", "config-api", "helm", "footer", "hubble-import", "hubble-nodes"].includes(state) }); if (sectionHeading) { expect(await sectionHeading.evaluate(node => Math.abs(node.getBoundingClientRect().top - 108))).toBeLessThan(2); diff --git a/tests/ui-ai/duo-view.test.cjs b/tests/ui-ai/duo-view.test.cjs new file mode 100644 index 0000000000..bea898426d --- /dev/null +++ b/tests/ui-ai/duo-view.test.cjs @@ -0,0 +1,124 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const vm = require('node:vm'); + +const root = path.resolve(__dirname, '../..'); +const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); +const exists = (file) => fs.existsSync(path.join(root, file)); + +const shell = require('../../assets/js/hugegraph-shell.js'); +const kapa = require('../../assets/js/kapa-adapter.js'); + +test('OINK 1.5.1 is locked exactly and native Duo View is enabled', () => { + assert.match(read('go.mod'), /github\.com\/pgsty\/oink v1\.5\.1\s*$/m); + const sums = read('go.sum').trim().split('\n'); + assert.deepEqual(sums, [ + 'github.com/pgsty/oink v1.5.1 h1:PgOZ7gnbkoJKZMFZ4YvCVXNL7HDiMV0LJiNYYhxiRmw=', + 'github.com/pgsty/oink v1.5.1/go.mod h1:GBKnERMfbKlRSFuKCa+kV7sehSAT7kAlYHxwxglg9Jw=', + ]); + assert.match(read('hugo.yaml'), /^\s*compare: true\s*$/m); + assert.equal(JSON.parse(read('scripts/oink-overrides.json')).version, 'v1.5.1'); +}); + +test('local and legacy preview toolchains match the OINK 1.5.1 contract', () => { + assert.match(read('go.mod'), /^go 1\.27\.3$/m); + assert.match(read('.github/workflows/hugo.yml'), /^\s*HUGO_VERSION:\s*0\.168\.0$/m); + const netlify = read('netlify.toml'); + assert.match(netlify, /^command = "bash scripts\/hugo\.sh build"$/m); + assert.match(netlify, /^GO_VERSION = "1\.27\.3"$/m); + assert.match(netlify, /^HUGO_VERSION = "0\.168\.0"$/m); +}); + +test('the site reuses OINK Duo View instead of carrying a second reader or router', () => { + for (const file of [ + 'assets/js/compare-entry.js', + 'assets/js/compare-scroll.js', + 'assets/js/document-compare.js', + 'layouts/_partials/compare/bootstrap.html', + 'layouts/_partials/compare/entry.html', + 'layouts/_partials/compare/targets.html', + ]) { + assert.equal(exists(file), false, `${file} must remain owned by OINK`); + } + + const updater = read('scripts/update_oink.py'); + for (const file of [ + 'assets/js/compare-entry.js', + 'assets/js/compare-scroll.js', + 'assets/js/document-compare.js', + 'layouts/_partials/actions/context.html', + 'layouts/_partials/head.html', + 'layouts/_partials/compare/analytics.html', + 'layouts/_partials/compare/bootstrap.html', + 'layouts/_partials/compare/enabled.html', + 'layouts/_partials/compare/entry.html', + 'layouts/_partials/compare/page-key.html', + 'layouts/_partials/compare/scripts.html', + 'layouts/_partials/compare/targets.html', + ]) { + assert.match(updater, new RegExp(file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + } +}); + +test('local template overrides retain OINK 1.5.1 page and mobile contracts', () => { + const manifest = read('layouts/_partials/actions/manifest.html'); + const navbar = read('layouts/_partials/navbar.html'); + const sidebar = read('layouts/_partials/shell/sidebar-panel.html'); + assert.match(manifest, /partial "compare\/page-key\.html" \./); + assert.doesNotMatch(manifest, /\.RelPermalink \$outputFormat/); + assert.match(navbar, /\$landingSurface := not \$drawerMode/); + assert.doesNotMatch(navbar, /\$landingSurface := and \(not \$drawerMode\)/); + assert.match(sidebar, /T "ui_home"/); + assert.doesNotMatch(sidebar, /brandName }} home/); +}); + +test('Duo View panes keep article tables but do not initialize HugeGraph shell controllers', () => { + let keydown; + const table = { + dataset: {}, + addEventListener(name, callback) { + assert.equal(name, 'keydown'); + keydown = callback; + }, + }; + const documentObject = new Proxy({ + querySelectorAll(selector) { + assert.equal(selector, '[data-td-asset-table-scroll]'); + return [table]; + }, + }, { + get(target, property) { + if (property in target) return target[property]; + throw new Error(`unexpected pane shell access: ${String(property)}`); + }, + }); + + assert.equal(shell.init({ OinkComparePane: true }, documentObject), undefined); + assert.equal(table.dataset.hgTableScrollBound, ''); + assert.equal(typeof keydown, 'function'); +}); + +test('language preference and Kapa return before touching a Duo View pane document', () => { + const forbiddenDocument = new Proxy({}, { + get(_, property) { + throw new Error(`unexpected pane document access: ${String(property)}`); + }, + }); + + assert.equal(kapa.init({ OinkComparePane: true }, forbiddenDocument), null); + vm.runInNewContext(read('assets/js/language-preference.js'), { + window: { OinkComparePane: true }, + document: forbiddenDocument, + }); +}); + +test('pane-only HugeGraph chrome is hidden without disabling content runtimes', () => { + const styles = read('assets/scss/_styles_project.scss'); + assert.match(styles, /html\[data-td-compare-pane\]/); + for (const selector of ['.hg-ask-ai-launcher', '.hg-ai-consent', '.hg-ai-status']) { + assert.match(styles, new RegExp(selector.replace('.', '\\.'))); + } + assert.doesNotMatch(styles, /html\[data-td-compare-pane\][^{]*\{[^}]*script/i); +}); diff --git a/tests/ui-ai/ui-contract.test.cjs b/tests/ui-ai/ui-contract.test.cjs index 05e8a22c51..2f5a5eb865 100644 --- a/tests/ui-ai/ui-contract.test.cjs +++ b/tests/ui-ai/ui-contract.test.cjs @@ -129,23 +129,24 @@ test('social fallback preserves the supplied HugeGraph and Apache image dimensio assert.equal(image.readUInt32BE(20), 936); }); -test('OINK Duo View stays native and HugeGraph site runtimes stand down in panes', () => { +test('OINK Duo View stays native and HugeGraph pane runtimes remain content-scoped', () => { const config = read('hugo.yaml'); const manifest = read('layouts/_partials/actions/manifest.html'); + const shell = read('assets/js/hugegraph-shell.js'); assert.match(config, /\n\s+compare: true\n/); assert.match(manifest, /partial "compare\/page-key\.html" \./); assert.doesNotMatch(manifest, /\.RelPermalink \$outputFormat/); - for (const file of [ - 'assets/js/hugegraph-shell.js', - 'assets/js/language-preference.js', - 'assets/js/kapa-adapter.js', - ]) { - assert.match( - read(file), - /if \(typeof window !== 'undefined' && window\.OinkComparePane\) return;/, - `${file} must not initialize site-level behavior inside native Duo View panes`, - ); - } + assert.match(shell, /if \(windowObject\.OinkComparePane\) \{/); + assert.match(shell, /if \(windowObject\.OinkComparePane\)[\s\S]*?initScrollableTables\(documentObject\);[\s\S]*?return;/); + assert.doesNotMatch(shell, /if \(typeof window !== 'undefined' && window\.OinkComparePane\) return;/); + assert.match( + read('assets/js/language-preference.js'), + /if \(window\.OinkComparePane\) return;/, + ); + assert.match( + read('assets/js/kapa-adapter.js'), + /if \(windowObject\.OinkComparePane\) return null;/, + ); assert.equal(fs.existsSync(path.join(root, 'assets/js/document-compare.js')), false); assert.equal(fs.existsSync(path.join(root, 'layouts/_partials/compare')), false); }); From 5f6bf3b795d1ac486d55ebf5a14f6397b0797689 Mon Sep 17 00:00:00 2001 From: Imba Jin Date: Sat, 14 Nov 2026 15:53:02 +0000 Subject: [PATCH 3/7] fix: scope Duo View URLs in historical artifacts --- scripts/test_versioning.py | 54 ++++++++++++++++++++++++++++++++++++++ scripts/versioning.py | 30 ++++++++++++++++++++- tests/e2e/duo-view.spec.js | 2 +- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/scripts/test_versioning.py b/scripts/test_versioning.py index c92c4582f2..dd6022df77 100644 --- a/scripts/test_versioning.py +++ b/scripts/test_versioning.py @@ -1625,6 +1625,60 @@ def test_language_fallback_scopes_to_each_artifact_base(self) -> None: switch = scoped["actions"][0] self.assertEqual(switch["options"][0]["url"], expected_url) + def test_scopes_duo_view_config_urls_for_archived_artifact(self) -> None: + manifest = versioning.load_manifest(versioning.ROOT / "versions.json") + archived = next( + entry for entry in manifest["versions"] if entry["id"] == "1.7" + ) + with tempfile.TemporaryDirectory() as temp_name: + output = Path(temp_name) + page = output / "docs/index.html" + page.parent.mkdir(parents=True) + counterpart = output / "cn/docs/index.html" + counterpart.parent.mkdir(parents=True) + counterpart.write_text("

中文

", encoding="utf-8") + config = { + "current": {"label": "English", "url": "/docs/"}, + "src": "/js/chunks/document-compare.js", + "targets": [ + { + "code": "zh-CN", + "label": "简体中文", + "url": "/cn/docs/", + } + ], + "labels": {"title": "Duo View"}, + } + page.write_text( + '", + encoding="utf-8", + ) + + stats = versioning.scope_version_artifact( + output, + manifest, + archived, + ORIGIN, + ) + + rendered = page.read_text(encoding="utf-8") + scoped = json.loads( + versioning.COMPARE_CONFIG_RE.search(rendered).group("body") + ) + self.assertEqual(scoped["current"]["url"], "/versions/1.7/docs/") + self.assertEqual( + scoped["targets"][0]["url"], + "/versions/1.7/cn/docs/", + ) + self.assertEqual( + scoped["src"], + "/versions/1.7/js/chunks/document-compare.js", + ) + self.assertEqual(scoped["labels"], {"title": "Duo View"}) + self.assertEqual(stats["compareConfigs"], 1) + def test_latest_language_fallback_preserves_root_relative_url(self) -> None: relative = "cn/docs/changelog/hugegraph-0.12.0-release-notes/index.html" current_url = f"{ORIGIN}cn/docs/changelog/hugegraph-0.12.0-release-notes/" diff --git a/scripts/versioning.py b/scripts/versioning.py index f221fd8b64..687f8bc45e 100644 --- a/scripts/versioning.py +++ b/scripts/versioning.py @@ -113,6 +113,12 @@ r"(?P)", re.IGNORECASE | re.DOTALL, ) +COMPARE_CONFIG_RE = re.compile( + r"(?P]*\bid=[\"']?td-compare-config[\"']?[^>]*>)" + r"(?P.*?)" + r"(?P)", + re.IGNORECASE | re.DOTALL, +) HREFLANG_LINK_RE = re.compile( r"]*\brel=[\"']?alternate(?:[\"'\s>]|$))" r"(?=[^>]*\bhreflang=)[^>]*>", @@ -3093,7 +3099,13 @@ def rewrite(value: str) -> str: ) return rewritten - stats = {"files": 0, "urls": 0, "manifests": 0, "searchRefs": 0} + stats = { + "files": 0, + "urls": 0, + "manifests": 0, + "compareConfigs": 0, + "searchRefs": 0, + } for path in sorted(output.rglob("*")): if not path.is_file(): continue @@ -3145,8 +3157,24 @@ def replace_manifest(match: re.Match) -> str: + match.group("close") ) + def replace_compare_config(match: re.Match) -> str: + # Hugo renders version-local RelPermalinks such as /docs/. Archived + # artifacts are later mounted below /versions//, so the native + # Duo View reader's JSON URLs need the same scoping as links and + # action manifests. Otherwise an archived page silently opens the + # latest documentation in both panes. + data = json.loads(match.group("body")) + rewritten = rewrite_json_urls(data, rewrite) + stats["compareConfigs"] += 1 + return ( + match.group("open") + + json.dumps(rewritten, ensure_ascii=False, separators=(",", ":")) + + match.group("close") + ) + if path.suffix == ".html": rendered = ACTION_MANIFEST_RE.sub(replace_manifest, rendered) + rendered = COMPARE_CONFIG_RE.sub(replace_compare_config, rendered) if rendered != original: path.write_text(rendered, encoding="utf-8") stats["files"] += 1 diff --git a/tests/e2e/duo-view.spec.js b/tests/e2e/duo-view.spec.js index 26f1ac1ef0..8eb9f9d4f1 100644 --- a/tests/e2e/duo-view.spec.js +++ b/tests/e2e/duo-view.spec.js @@ -68,7 +68,7 @@ for (const version of VERSION_IDS) { const entry = page.locator('[data-td-compare-open]'); await expect(entry).toBeVisible(); const config = await compareConfig(page); - expect(config.current.url).toBeTruthy(); + expect(resolvedURL(page, config.current.url).pathname).toBe(route); expect(config.targets).toHaveLength(1); expect(resolvedURL(page, config.targets[0].url).pathname).toBe(expectedTarget); const manifest = await actionManifest(page); From cc7396c818dfba2080b0f51419ee55a782f106e0 Mon Sep 17 00:00:00 2001 From: Imba Jin Date: Sat, 14 Nov 2026 15:57:22 +0000 Subject: [PATCH 4/7] test: make Duo View timeout recovery deterministic --- tests/e2e/duo-view.spec.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/e2e/duo-view.spec.js b/tests/e2e/duo-view.spec.js index 8eb9f9d4f1..6a33495dba 100644 --- a/tests/e2e/duo-view.spec.js +++ b/tests/e2e/duo-view.spec.js @@ -205,8 +205,17 @@ test('Duo View times out a stalled pane, retries and keeps an explicit direct li const target = resolvedURL(page, config.targets[0].url); await page.evaluate(() => { const nativeTimeout = window.setTimeout.bind(window); - window.setTimeout = (callback, delay, ...args) => - nativeTimeout(callback, delay === 15_000 ? 100 : delay, ...args); + let paneTimeouts = 0; + window.setTimeout = (callback, delay, ...args) => { + if (delay === 15_000) { + paneTimeouts += 1; + // OINK loads the source pane first and the target pane second. Fail + // only the target's first attempt; the source and retry keep the real + // timeout so this test cannot manufacture a two-pane race. + if (paneTimeouts === 2) return nativeTimeout(callback, 100, ...args); + } + return nativeTimeout(callback, delay, ...args); + }; }); let attempts = 0; await page.route(url => url.pathname === target.pathname && url.searchParams.get('oink_compare_pane') === '1', async route => { From ca3cbc169323332fb3642bd3443303402ca20260 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 15 Nov 2026 00:23:08 +0800 Subject: [PATCH 5/7] fix: restore Duo View on archived pages - Mount the OINK pane marker at the historical page path. - Verify both panes are readable and select the palette action by its title. - Keep the upgrade checks compatible with baseline promotion after validation. --- scripts/oink-features.md | 20 +++++++++---------- scripts/test_versioning.py | 23 +++++++++++++++++++++ scripts/versioning.py | 17 ++++++++++++++++ tests/e2e/duo-view.spec.js | 9 +++++---- tests/ui-ai/duo-view.test.cjs | 19 ++++++------------ tests/ui-ai/ui-contract.test.cjs | 34 -------------------------------- 6 files changed, 61 insertions(+), 61 deletions(-) diff --git a/scripts/oink-features.md b/scripts/oink-features.md index 44b8123911..bed22d946b 100644 --- a/scripts/oink-features.md +++ b/scripts/oink-features.md @@ -197,16 +197,6 @@ only for presentation. Resize the terminal before recording: overriding player `cols`/`rows` can crop it. Check desktop and mobile playback. The recorder is an authoring tool, not a build dependency. -## Site integration and output checks - -- `hugo.yaml` owns appearance, search and output configuration. Four style - presets are selected with `params.ui.preset_menu: [paper, slate, ink, terminal]`; - boolean `true` offers only Paper/Slate. Ink and Terminal are experimental. - Style and light/dark mode are independent; OINK owns saved preferences. -- Header and mobile drawers use the public `shell/appearance-menu.html` partial. - Sidebar persistence uses `OinkSidebar`; AI integration uses the public - search-tail API. Keep HugeGraph consent, version/language scope and local - availability behavior in their existing hooks/adapters. ## Native Duo View OINK 1.5.1 calls the feature **Duo View** in English and **双列视图** in @@ -225,6 +215,16 @@ Site-level integrations loaded from hooks must stand down when `window.OinkComparePane` is true. Content runtimes remain active so tables, tabs, diagrams, code controls and fragment links continue to work inside each pane. +## Site integration and output checks + +- `hugo.yaml` owns appearance, search and output configuration. Four style + presets are selected with `params.ui.preset_menu: [paper, slate, ink, terminal]`; + boolean `true` offers only Paper/Slate. Ink and Terminal are experimental. + Style and light/dark mode are independent; OINK owns saved preferences. +- Header and mobile drawers use the public `shell/appearance-menu.html` partial. + Sidebar persistence uses `OinkSidebar`; AI integration uses the public + search-tail API. Keep HugeGraph consent, version/language scope and local + availability behavior in their existing hooks/adapters. - Local search uses `params.offline_search_index: content` so configuration names, error messages and other body-only terms are searchable in both languages. OINK loads the current language's index on demand; full content increases its diff --git a/scripts/test_versioning.py b/scripts/test_versioning.py index dd6022df77..653e1ba22a 100644 --- a/scripts/test_versioning.py +++ b/scripts/test_versioning.py @@ -1679,6 +1679,29 @@ def test_scopes_duo_view_config_urls_for_archived_artifact(self) -> None: self.assertEqual(scoped["labels"], {"title": "Duo View"}) self.assertEqual(stats["compareConfigs"], 1) + def test_duo_view_page_marker_uses_the_mounted_path(self) -> None: + manifest = versioning.load_manifest(versioning.ROOT / "versions.json") + examples = { + "docs/deep/page/index.html": '', + "cn/docs/图/index.html": "", + } + other_meta = '' + for version in ("latest", "1.7"): + entry = next(item for item in manifest["versions"] if item["id"] == version) + prefix = "/" + entry["publishPath"] if entry["publishPath"] else "" + with self.subTest(version=version), tempfile.TemporaryDirectory() as temp_name: + output = Path(temp_name) + for relative, marker in examples.items(): + page = output / relative + page.parent.mkdir(parents=True) + page.write_text(marker.format(mount="") + other_meta, encoding="utf-8") + for _ in range(2): + versioning.scope_version_artifact(output, manifest, entry, ORIGIN) + for relative, marker in examples.items(): + self.assertEqual( + (output / relative).read_text(), marker.format(mount=prefix) + other_meta + ) + def test_latest_language_fallback_preserves_root_relative_url(self) -> None: relative = "cn/docs/changelog/hugegraph-0.12.0-release-notes/index.html" current_url = f"{ORIGIN}cn/docs/changelog/hugegraph-0.12.0-release-notes/" diff --git a/scripts/versioning.py b/scripts/versioning.py index 687f8bc45e..e09d719d98 100644 --- a/scripts/versioning.py +++ b/scripts/versioning.py @@ -119,6 +119,11 @@ r"(?P)", re.IGNORECASE | re.DOTALL, ) +COMPARE_PAGE_PATH_RE = re.compile( + r"""(?P]*\sname=["']?oink-compare-page(?:["'\s>]|$))""" + r"""[^>]*?\scontent=["']?)(?P/[^"'\s>]*)""", + re.IGNORECASE, +) HREFLANG_LINK_RE = re.compile( r"]*\brel=[\"']?alternate(?:[\"'\s>]|$))" r"(?=[^>]*\bhreflang=)[^>]*>", @@ -3017,6 +3022,16 @@ def scope_version_artifact( reviewed_selector_urls = reviewed_version_route_urls( manifest, route_map, origin, historical_origin ) + mount = "/" + entry["publishPath"] if entry["publishPath"] else "" + + def mount_compare_page(match: re.Match) -> str: + # OINK checks this page's identity against location.pathname inside + # the iframe. Keep its encoded permalink, without navigation fallbacks. + pathname = match["path"] + if pathname == mount or pathname.startswith(mount + "/"): + return match[0] + return match["open"] + mount + pathname + historical_selector_urls: set[str] = set() if historical_origin is not None: archived_ids = { @@ -3173,6 +3188,8 @@ def replace_compare_config(match: re.Match) -> str: ) if path.suffix == ".html": + if mount: + rendered = COMPARE_PAGE_PATH_RE.sub(mount_compare_page, rendered) rendered = ACTION_MANIFEST_RE.sub(replace_manifest, rendered) rendered = COMPARE_CONFIG_RE.sub(replace_compare_config, rendered) if rendered != original: diff --git a/tests/e2e/duo-view.spec.js b/tests/e2e/duo-view.spec.js index 6a33495dba..27a2e2219b 100644 --- a/tests/e2e/duo-view.spec.js +++ b/tests/e2e/duo-view.spec.js @@ -25,14 +25,15 @@ async function actionManifest(page) { async function paneFrames(page) { const dialog = page.locator('dialog[data-td-compare]'); await expect(dialog).toBeVisible({ timeout: 15_000 }); - const locators = dialog.locator('iframe[data-td-compare-frame]'); - await expect(locators).toHaveCount(2); + // OINK keeps aria-busy while loading and removes failed frames on error. + const locators = dialog.locator('.td-compare__body:not([aria-busy]) > iframe[data-td-compare-frame]'); + await expect(locators).toHaveCount(2, { timeout: 15_000 }); const frames = []; for (let index = 0; index < 2; index += 1) { const handle = await locators.nth(index).elementHandle(); const frame = await handle.contentFrame(); await frame.waitForFunction(() => window.OinkComparePane === true && - Boolean(document.querySelector('meta[name="oink-compare-page"]')) && + document.querySelector('meta[name="oink-compare-page"]')?.content === location.pathname && Boolean(document.querySelector('#td-main-content .td-content'))); frames.push(frame); } @@ -185,7 +186,7 @@ test('real Docs page lazily opens, synchronizes, restores and exposes all native await page.locator('[data-td-shell-search-open]').first().click(); const input = page.locator('.td-shell-search__input'); await input.fill('Duo View'); - await page.getByRole('option').filter({ hasText: /^Duo View$/ }).first().click(); + await page.getByRole('group', { name: 'Actions' }).getByText('Duo View', { exact: true }).click(); ({ dialog } = await paneFrames(page)); await page.reload({ waitUntil: 'domcontentloaded' }); diff --git a/tests/ui-ai/duo-view.test.cjs b/tests/ui-ai/duo-view.test.cjs index bea898426d..fbab3b2f84 100644 --- a/tests/ui-ai/duo-view.test.cjs +++ b/tests/ui-ai/duo-view.test.cjs @@ -11,24 +11,17 @@ const exists = (file) => fs.existsSync(path.join(root, file)); const shell = require('../../assets/js/hugegraph-shell.js'); const kapa = require('../../assets/js/kapa-adapter.js'); -test('OINK 1.5.1 is locked exactly and native Duo View is enabled', () => { - assert.match(read('go.mod'), /github\.com\/pgsty\/oink v1\.5\.1\s*$/m); - const sums = read('go.sum').trim().split('\n'); - assert.deepEqual(sums, [ - 'github.com/pgsty/oink v1.5.1 h1:PgOZ7gnbkoJKZMFZ4YvCVXNL7HDiMV0LJiNYYhxiRmw=', - 'github.com/pgsty/oink v1.5.1/go.mod h1:GBKnERMfbKlRSFuKCa+kV7sehSAT7kAlYHxwxglg9Jw=', - ]); +test('the site enables native Duo View', () => { assert.match(read('hugo.yaml'), /^\s*compare: true\s*$/m); - assert.equal(JSON.parse(read('scripts/oink-overrides.json')).version, 'v1.5.1'); }); -test('local and legacy preview toolchains match the OINK 1.5.1 contract', () => { - assert.match(read('go.mod'), /^go 1\.27\.3$/m); - assert.match(read('.github/workflows/hugo.yml'), /^\s*HUGO_VERSION:\s*0\.168\.0$/m); +test('the legacy preview uses the same build command and toolchain as the site', () => { + const goVersion = read('go.mod').match(/^go (\S+)$/m)[1]; + const hugoVersion = read('.github/workflows/hugo.yml').match(/^ +HUGO_VERSION: (\S+)$/m)[1]; const netlify = read('netlify.toml'); assert.match(netlify, /^command = "bash scripts\/hugo\.sh build"$/m); - assert.match(netlify, /^GO_VERSION = "1\.27\.3"$/m); - assert.match(netlify, /^HUGO_VERSION = "0\.168\.0"$/m); + assert.equal(netlify.match(/^GO_VERSION = "([^"]+)"$/m)[1], goVersion); + assert.equal(netlify.match(/^HUGO_VERSION = "([^"]+)"$/m)[1], hugoVersion); }); test('the site reuses OINK Duo View instead of carrying a second reader or router', () => { diff --git a/tests/ui-ai/ui-contract.test.cjs b/tests/ui-ai/ui-contract.test.cjs index 2f5a5eb865..90f4f3f1ee 100644 --- a/tests/ui-ai/ui-contract.test.cjs +++ b/tests/ui-ai/ui-contract.test.cjs @@ -128,37 +128,3 @@ test('social fallback preserves the supplied HugeGraph and Apache image dimensio assert.equal(image.readUInt32BE(16), 1681); assert.equal(image.readUInt32BE(20), 936); }); - -test('OINK Duo View stays native and HugeGraph pane runtimes remain content-scoped', () => { - const config = read('hugo.yaml'); - const manifest = read('layouts/_partials/actions/manifest.html'); - const shell = read('assets/js/hugegraph-shell.js'); - assert.match(config, /\n\s+compare: true\n/); - assert.match(manifest, /partial "compare\/page-key\.html" \./); - assert.doesNotMatch(manifest, /\.RelPermalink \$outputFormat/); - assert.match(shell, /if \(windowObject\.OinkComparePane\) \{/); - assert.match(shell, /if \(windowObject\.OinkComparePane\)[\s\S]*?initScrollableTables\(documentObject\);[\s\S]*?return;/); - assert.doesNotMatch(shell, /if \(typeof window !== 'undefined' && window\.OinkComparePane\) return;/); - assert.match( - read('assets/js/language-preference.js'), - /if \(window\.OinkComparePane\) return;/, - ); - assert.match( - read('assets/js/kapa-adapter.js'), - /if \(windowObject\.OinkComparePane\) return null;/, - ); - assert.equal(fs.existsSync(path.join(root, 'assets/js/document-compare.js')), false); - assert.equal(fs.existsSync(path.join(root, 'layouts/_partials/compare')), false); -}); - -test('OINK lock and reviewed override inventory advance together', () => { - const goMod = read('go.mod'); - const goSum = read('go.sum'); - const inventory = JSON.parse(read('scripts/oink-overrides.json')); - assert.match(goMod, /github\.com\/pgsty\/oink v1\.5\.1/); - assert.match(goSum, /github\.com\/pgsty\/oink v1\.5\.1 h1:/); - assert.match(goSum, /github\.com\/pgsty\/oink v1\.5\.1\/go\.mod h1:/); - assert.equal(inventory.version, 'v1.5.1'); - assert.ok(inventory.files['assets/scss/td/_document-compare.scss']); - assert.ok(inventory.files['layouts/_partials/actions/manifest.html']); -}); From 67352ae6ea8b8abb071f5e8c127b87ddebd228c1 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 15 Nov 2026 00:28:39 +0800 Subject: [PATCH 6/7] fix: capture Duo View at its viewport size - Capture the desktop reader at its two-column viewport. - Keep the mobile capture on the selected translation. - Exclude the underlying article height from reader screenshots. --- tests/e2e/visual.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/visual.spec.js b/tests/e2e/visual.spec.js index fb947185f2..7c158e91c0 100644 --- a/tests/e2e/visual.spec.js +++ b/tests/e2e/visual.spec.js @@ -231,7 +231,8 @@ for (const [name, url, viewport, theme, state, preset = "slate"] of states) { await page.screenshot({ animations: "disabled", path: path.join(directory, `${name}.png`), - fullPage: !["gallery", "chart", "longcode", "markmap", "terminal", "math", "pd", "computer", "config-api", "helm", "footer", "hubble-import", "hubble-nodes"].includes(state) + fullPage: !["duo", "duo-mobile", "gallery", "chart", "longcode", "markmap", "terminal", "math", + "pd", "computer", "config-api", "helm", "footer", "hubble-import", "hubble-nodes"].includes(state) }); if (sectionHeading) { expect(await sectionHeading.evaluate(node => Math.abs(node.getBoundingClientRect().top - 108))).toBeLessThan(2); From 22a5761ca8cdd456c82b09eb839d3fa3e2b44a51 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 15 Nov 2026 00:55:23 +0800 Subject: [PATCH 7/7] fix: keep long configuration keys on one line - Size the key column to fit in the RocksDB and PD references. - Apply the same markup to the matching archived configuration tables. - Verify that archive assembly preserves authored table attributes. --- assets/scss/_styles_project.scss | 7 ++ .../cn/docs/config/config-backend-rocksdb.md | 2 +- content/cn/docs/config/config-option.md | 2 +- .../docs/quickstart/hugegraph/hugegraph-pd.md | 2 +- .../en/docs/config/config-backend-rocksdb.md | 2 +- content/en/docs/config/config-option.md | 2 +- .../docs/quickstart/hugegraph/hugegraph-pd.md | 2 +- scripts/test_versioning.py | 75 +++++++++++++++++++ scripts/versioning.py | 35 +++++++-- 9 files changed, 118 insertions(+), 11 deletions(-) diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 28838bb997..aea5710a0e 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -932,6 +932,13 @@ max-width: 36rem; } +@media screen { + // Let configuration keys set the width of their own column. + .td-content .td-table-scroll:not(.td-table-scroll--static) > .hg-config-keys > tbody > tr > td:first-child { + max-width: none; + } +} + // OINK Duo View loads ordinary pages in same-origin iframes. Keep content-level // interactions, but remove HugeGraph's top-level AI chrome from both panes. html[data-td-compare-pane] { diff --git a/content/cn/docs/config/config-backend-rocksdb.md b/content/cn/docs/config/config-backend-rocksdb.md index 22a188d11f..f54a0b8667 100644 --- a/content/cn/docs/config/config-backend-rocksdb.md +++ b/content/cn/docs/config/config-backend-rocksdb.md @@ -172,7 +172,7 @@ rocksdb-data/ | rocksdb.use_direct_io_for_flush_and_compaction | `false` | flush 和 compaction 时使用直接读写。 | | rocksdb.use_fsync | `false` | 为 true 时每次持久化都会执行 fsync。 | | rocksdb.atomic_flush | `false` | 为 true 时多个列族的 flush 结果会原子地提交到 MANIFEST。WAL 一直开启的情况下不必设置该项。 | -{.td-table--wide} +{.wide .hg-config-keys} ### SST 表格式与块缓存配置项 diff --git a/content/cn/docs/config/config-option.md b/content/cn/docs/config/config-option.md index 5a51412e3e..03d78f5223 100644 --- a/content/cn/docs/config/config-option.md +++ b/content/cn/docs/config/config-option.md @@ -302,7 +302,7 @@ HStore 后端还会从图配置文件 `{graph-name}.properties` 中读取以下 | rocksdb.partition_filters_and_indexes | false | If bloom filter is enabled, set this option true to use partitioned full filters and indexes for each sst file. This option is incompatible with block-based filters. | | rocksdb.pin_top_level_index_and_filter | true | If partition_filters_and_indexes is set true, set this option true if we'd pin top-level index of partitioned filter and index blocks to the block cache. | | rocksdb.prefix_extractor_n_bytes | 0 | The prefix-extractor uses the first N bytes of a key as its prefix, it will use the full key when a key is shorter than the N. 0 means unset prefix-extractor. | -{.td-table--wide} +{.wide .hg-config-keys} > [!DETAILS]- **K8s 配置项 (可选)** > 对应配置文件`rest-server.properties` diff --git a/content/cn/docs/quickstart/hugegraph/hugegraph-pd.md b/content/cn/docs/quickstart/hugegraph/hugegraph-pd.md index 0ee64df8ec..c2c53d8bd7 100644 --- a/content/cn/docs/quickstart/hugegraph/hugegraph-pd.md +++ b/content/cn/docs/quickstart/hugegraph/hugegraph-pd.md @@ -207,7 +207,7 @@ PD 启动脚本读取安装目录中的 `conf/application.yml`。请使用与安 | `management.metrics.export.prometheus.enabled` | `true` | Spring Boot 默认值 | 是否暴露 `/actuator/prometheus`。 | | `management.endpoints.web.exposure.include` | `health,metrics,prometheus` | Spring Boot 默认只暴露 `health` | Actuator 暴露白名单;这些端点不经过 PD REST Basic 拦截器。 | | `logging.config` | `file:./conf/log4j2.xml` | 无 | Log4j2 配置文件,会写出 `logs/hugegraph-pd.log`、`logs/hugegraph-pd_raft.log` 和 `logs/audit-hugegraph-pd.log`。 | -{.td-table--wide} +{.wide .hg-config-keys} **线程池** diff --git a/content/en/docs/config/config-backend-rocksdb.md b/content/en/docs/config/config-backend-rocksdb.md index dc0fad035d..dd54111f24 100644 --- a/content/en/docs/config/config-backend-rocksdb.md +++ b/content/en/docs/config/config-backend-rocksdb.md @@ -184,7 +184,7 @@ Other points to keep in mind: | rocksdb.use_direct_io_for_flush_and_compaction | `false` | Enable the OS to use direct read/writes in flush and compaction. | | rocksdb.use_fsync | `false` | If true, then every store to stable storage will issue a fsync. | | rocksdb.atomic_flush | `false` | If true, flushing multiple column families and committing their results atomically to MANIFEST. Note that it's not necessary to set atomic_flush=true if WAL is always enabled. | -{.td-table--wide} +{.wide .hg-config-keys} ### SST table format and block cache options diff --git a/content/en/docs/config/config-option.md b/content/en/docs/config/config-option.md index 8ccda4a40c..9ad3a0eb75 100644 --- a/content/en/docs/config/config-option.md +++ b/content/en/docs/config/config-option.md @@ -304,7 +304,7 @@ Basic Config Options and Backend Config Options correspond to configuration file | rocksdb.partition_filters_and_indexes | false | If bloom filter is enabled, set this option true to use partitioned full filters and indexes for each sst file. This option is incompatible with block-based filters. | | rocksdb.pin_top_level_index_and_filter | true | If partition_filters_and_indexes is set true, set this option true if we'd pin top-level index of partitioned filter and index blocks to the block cache. | | rocksdb.prefix_extractor_n_bytes | 0 | The prefix-extractor uses the first N bytes of a key as its prefix, it will use the full key when a key is shorter than the N. 0 means unset prefix-extractor. | -{.td-table--wide} +{.wide .hg-config-keys} > [!DETAILS]- **K8s Config Options (Optional)** > Corresponding configuration file `rest-server.properties` diff --git a/content/en/docs/quickstart/hugegraph/hugegraph-pd.md b/content/en/docs/quickstart/hugegraph/hugegraph-pd.md index 9bf9a27fef..778b28ccde 100644 --- a/content/en/docs/quickstart/hugegraph/hugegraph-pd.md +++ b/content/en/docs/quickstart/hugegraph/hugegraph-pd.md @@ -211,7 +211,7 @@ initial partitions = store count * partition.store-max-shard-count / partition.d | `management.metrics.export.prometheus.enabled` | `true` | Spring Boot default | Exposes `/actuator/prometheus`. | | `management.endpoints.web.exposure.include` | `health,metrics,prometheus` | Spring Boot exposes only `health` by default | Actuator allowlist; these endpoints bypass the PD REST Basic interceptor. | | `logging.config` | `file:./conf/log4j2.xml` | none | Log4j2 configuration. Writes `logs/hugegraph-pd.log`, `logs/hugegraph-pd_raft.log` and `logs/audit-hugegraph-pd.log`. | -{.td-table--wide} +{.wide .hg-config-keys} **Thread pools** diff --git a/scripts/test_versioning.py b/scripts/test_versioning.py index 653e1ba22a..5aee7c386f 100644 --- a/scripts/test_versioning.py +++ b/scripts/test_versioning.py @@ -1254,6 +1254,81 @@ def test_historical_catalog_preserves_already_wide_attributes(self) -> None: self.assertEqual(versioning.ensure_legacy_config_tables_wide(page), 0) self.assertEqual(page.read_text(encoding="utf-8"), source) + def test_historical_config_option_marks_only_the_rocksdb_key_table(self) -> None: + key_row = "rocksdb.use_direct_io_for_flush_and_compaction" + other_table = ( + "| config option | default value | description |\n|---|---|---|\n" + "| gremlin.graph | graph.yaml | Graph config file. |\n" + "{#other .td-table--wide data-label=\"keep } here\"}\n" + ) + key_table = ( + "| config option | default value | description |\n|---|---|---|\n" + f"| {key_row} | false | RocksDB option. |\n" + '{#rocksdb class="td-table--wide authored" data-label="preserve } here"}\n' + ) + + with tempfile.TemporaryDirectory() as temporary: + page = Path(temporary) / "config-option.md" + source = other_table + "\n" + key_table + page.write_text(source, encoding="utf-8") + + self.assertEqual( + versioning.ensure_legacy_config_tables_wide( + page, config_key_row=key_row + ), + 1, + ) + rendered = page.read_text(encoding="utf-8") + self.assertIn('{#other .td-table--wide data-label="keep } here"}\n', rendered) + self.assertIn( + '{#rocksdb class="td-table--wide authored" ' + 'data-label="preserve } here" .hg-config-keys}\n', + rendered, + ) + self.assertEqual( + versioning.ensure_legacy_config_tables_wide( + page, config_key_row=key_row + ), + 0, + ) + self.assertEqual(page.read_text(encoding="utf-8"), rendered) + + def test_historical_adapter_adds_rocksdb_key_class_only_to_archive(self) -> None: + key_row = "rocksdb.use_direct_io_for_flush_and_compaction" + table = ( + "| config option | default value | description |\n|---|---|---|\n" + "| gremlin.graph | graph.yaml | Graph config file. |\n" + "\n" + "| config option | default value | description |\n|---|---|---|\n" + f"| {key_row} | false | RocksDB option. |\n" + ) + for version in ("1.3", "latest"): + with self.subTest(version=version), tempfile.TemporaryDirectory() as temporary: + assembly = Path(temporary) + pages = [] + for language in ("en", "cn"): + page = assembly / f"content/{language}/docs/config/config-option.md" + page.parent.mkdir(parents=True, exist_ok=True) + page.write_text(table, encoding="utf-8") + pages.append(page) + + with ( + mock.patch.object(versioning, "apply_exact_legacy_content_fixes", return_value=4), + mock.patch.object(versioning, "ensure_search_metadata", return_value=0), + mock.patch.object(versioning, "normalize_historical_server_headings", return_value=0), + mock.patch.object(versioning, "first_existing_path", return_value=assembly / "server.md"), + ): + versioning.apply_known_legacy_fixes(assembly, version) + + for page in pages: + rendered = page.read_text(encoding="utf-8") + if version == "latest": + self.assertEqual(rendered, table) + else: + self.assertEqual(rendered.count(".hg-config-keys"), 1) + self.assertIn("{.td-table--wide .hg-config-keys}\n", rendered) + self.assertIn("{.td-table--wide}\n", rendered) + def test_known_legacy_catalog_adapter_is_bound_to_versions_languages_and_paths(self) -> None: source = ( "---\ntitle: Config\n---\n" diff --git a/scripts/versioning.py b/scripts/versioning.py index e09d719d98..e10274425c 100644 --- a/scripts/versioning.py +++ b/scripts/versioning.py @@ -1606,6 +1606,7 @@ def ensure_legacy_config_tables_wide( path: pathlib.Path, headers: tuple[tuple[str, ...], ...] = (("config option", "default value", "description"),), required_row: str | None = None, + config_key_row: str | None = None, ) -> int: """Opt frozen technical catalogs into OINK's native horizontal table layout.""" source = path.read_text(encoding="utf-8") @@ -1644,14 +1645,27 @@ def parts(line: str) -> tuple[str, str]: while end < len(lines) and parts(lines[end])[0].strip() == prefix.strip() and parts(lines[end])[1].startswith("|"): end += 1 result.extend(lines[index:end]) - if required_row and not any(required_row in line for line in lines[index + 2:end]): + table_rows = lines[index + 2:end] + if required_row and not any(required_row in line for line in table_rows): index = end continue + classes = ["td-table--wide"] + if config_key_row and any(config_key_row in line for line in table_rows): + classes.append("hg-config-keys") attribute = parts(lines[end])[1].strip() if end < len(lines) and parts(lines[end])[0].strip() == prefix.strip() else "" if attribute.startswith("{") and attribute.endswith("}"): - if not re.search(r"(? tuple[str, str]: else: if not result[-1].endswith("\n"): result[-1] += "\n" - result.append(prefix + "{.td-table--wide}\n") + class_markers = " ".join(f".{class_name}" for class_name in classes) + result.append(prefix + "{" + class_markers + "}\n") fixed += 1 index = end continue @@ -1758,7 +1773,17 @@ def apply_known_legacy_fixes(assembly: pathlib.Path, version: str) -> int: for relative, headers, required_row in catalogs: page = assembly / f"content/{language}/docs/{relative}" if page.is_file(): - fixed += ensure_legacy_config_tables_wide(page, headers, required_row) + config_key_row = ( + "rocksdb.use_direct_io_for_flush_and_compaction" + if relative == "config/config-option.md" + else None + ) + fixed += ensure_legacy_config_tables_wide( + page, + headers, + required_row, + config_key_row=config_key_row, + ) summary_path = assembly / f"content/{language}/docs/SUMMARY.md" if version in {"1.7", "1.5"}: fixed += repair_historical_performance_routes(summary_path)