From 429d90fa208ed008b049afd051571f4aa3691407 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 22 May 2026 15:26:39 -0400 Subject: [PATCH 1/4] feat(examples): add rust-doc example --- .gitignore | 1 + .readthedocs.yaml | 4 ++ eslint.config.mjs | 1 + examples/rustdoc/.cargo/config.toml | 5 ++ examples/rustdoc/Cargo.lock | 7 +++ examples/rustdoc/Cargo.toml | 15 ++++++ examples/rustdoc/package-lock.json | 60 +++++++++++++++++++++ examples/rustdoc/package.json | 11 ++++ examples/rustdoc/rustdoc/build.js | 28 ++++++++++ examples/rustdoc/rustdoc/shared-web.html | 20 +++++++ examples/rustdoc/src/lib.rs | 68 ++++++++++++++++++++++++ jsdoc.json | 7 +++ src/css/crowdin-rustdoc.scss | 30 +++++++++++ src/js/crowdin-rustdoc-css.js | 1 + src/js/crowdin.js | 21 ++++++-- tests/crowdin.test.js | 22 +++++++- webpack.config.js | 1 + 17 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 examples/rustdoc/.cargo/config.toml create mode 100644 examples/rustdoc/Cargo.lock create mode 100644 examples/rustdoc/Cargo.toml create mode 100644 examples/rustdoc/package-lock.json create mode 100644 examples/rustdoc/package.json create mode 100644 examples/rustdoc/rustdoc/build.js create mode 100644 examples/rustdoc/rustdoc/shared-web.html create mode 100644 examples/rustdoc/src/lib.rs create mode 100644 src/css/crowdin-rustdoc.scss create mode 100644 src/js/crowdin-rustdoc-css.js diff --git a/.gitignore b/.gitignore index fd133a2..923ebbc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ coverage/ _readthedocs/ build/ dist/ +target/ lizardbyte-shared-web*.tgz diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f94a8a8..e5e5541 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,6 +6,7 @@ build: nodejs: "22" python: "3.13" ruby: "3.3" + rust: "1.91" commands: - 'echo "output directory: ${READTHEDOCS_OUTPUT}html"' # shared-web build @@ -26,5 +27,8 @@ build: - cd examples/sphinx && npm ci # we need to include scripts for postinstall actions - cd examples/sphinx && npm run build - cd examples/sphinx && npm run lint + # rustdoc example build + - cd examples/rustdoc && npm ci --ignore-scripts + - cd examples/rustdoc && npm run build # debug output - cd ${READTHEDOCS_OUTPUT}html && ls -la -R diff --git a/eslint.config.mjs b/eslint.config.mjs index 0b121f1..22fbbc4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,6 +9,7 @@ export default [ "coverage/**", "dist/**", "docs/**", // generated JSDoc output + "examples/**/build/**", // generated example output ], }, { diff --git a/examples/rustdoc/.cargo/config.toml b/examples/rustdoc/.cargo/config.toml new file mode 100644 index 0000000..11faac5 --- /dev/null +++ b/examples/rustdoc/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +rustdocflags = [ + "--html-after-content", + "rustdoc/shared-web.html", +] diff --git a/examples/rustdoc/Cargo.lock b/examples/rustdoc/Cargo.lock new file mode 100644 index 0000000..c399348 --- /dev/null +++ b/examples/rustdoc/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "shared-web-rustdoc-example" +version = "0.0.0" diff --git a/examples/rustdoc/Cargo.toml b/examples/rustdoc/Cargo.toml new file mode 100644 index 0000000..b6f0717 --- /dev/null +++ b/examples/rustdoc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "shared-web-rustdoc-example" +version = "0.0.0" +edition = "2021" +description = "Example use of @lizardbyte/shared-web in rustdoc html." +license = "AGPL-3.0-only" +publish = false +exclude = [ + "build", + "node_modules", +] + +[lib] +name = "shared_web_rustdoc_example" +path = "src/lib.rs" diff --git a/examples/rustdoc/package-lock.json b/examples/rustdoc/package-lock.json new file mode 100644 index 0000000..86966b5 --- /dev/null +++ b/examples/rustdoc/package-lock.json @@ -0,0 +1,60 @@ +{ + "name": "shared-web-rustdoc-example", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shared-web-rustdoc-example", + "version": "0.0.0", + "dependencies": { + "@lizardbyte/shared-web": "file:../.." + } + }, + "../..": { + "name": "@lizardbyte/shared-web", + "version": "0.0.0", + "license": "AGPL-3.0-only", + "dependencies": { + "@fortawesome/fontawesome-free": "7.2.0", + "bootstrap": "5.3.8" + }, + "devDependencies": { + "@babel/core": "7.29.0", + "@babel/preset-env": "7.29.5", + "@codecov/webpack-plugin": "2.0.1", + "@eslint/js": "10.0.1", + "@jest/globals": "30.4.1", + "babel-loader": "10.1.1", + "clean-jsdoc-theme": "4.3.2", + "cross-env": "10.1.0", + "css-loader": "7.1.4", + "eslint": "10.4.0", + "eslint-plugin-jest": "29.15.2", + "globals": "17.6.0", + "jest": "30.4.2", + "jest-environment-jsdom": "30.4.1", + "jest-junit": "17.0.0", + "jsdoc": "4.0.5", + "mini-css-extract-plugin": "2.10.2", + "node-fetch": "3.3.2", + "npm-run-all2": "9.0.0", + "postcss": "8.5.15", + "postcss-loader": "8.2.1", + "postcss-preset-env": "11.3.0", + "sass": "1.100.0", + "sass-loader": "17.0.0", + "webpack": "5.107.1", + "webpack-cli": "7.0.2", + "webpack-dev-server": "5.2.4" + }, + "funding": { + "url": "https://app.lizardbyte.dev" + } + }, + "node_modules/@lizardbyte/shared-web": { + "resolved": "../..", + "link": true + } + } +} diff --git a/examples/rustdoc/package.json b/examples/rustdoc/package.json new file mode 100644 index 0000000..d82197a --- /dev/null +++ b/examples/rustdoc/package.json @@ -0,0 +1,11 @@ +{ + "name": "shared-web-rustdoc-example", + "version": "0.0.0", + "description": "Example use of @lizardbyte/shared-web in rustdoc html.", + "dependencies": { + "@lizardbyte/shared-web": "file:../.." + }, + "scripts": { + "build": "node rustdoc/build.js" + } +} diff --git a/examples/rustdoc/rustdoc/build.js b/examples/rustdoc/rustdoc/build.js new file mode 100644 index 0000000..e7c2915 --- /dev/null +++ b/examples/rustdoc/rustdoc/build.js @@ -0,0 +1,28 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const exampleDir = path.resolve(__dirname, '..'); +const readTheDocsOutput = process.env.READTHEDOCS_OUTPUT ? + path.resolve(process.env.READTHEDOCS_OUTPUT) : + path.join(exampleDir, 'build'); +const targetDir = path.join(readTheDocsOutput, 'html', 'rustdoc'); +const docDir = path.join(targetDir, 'doc'); +const sharedWebDist = path.join(exampleDir, 'node_modules', '@lizardbyte', 'shared-web', 'dist'); + +const cargoResult = spawnSync('cargo', ['doc', '--no-deps', '--target-dir', targetDir], { + stdio: 'inherit', +}); + +if (cargoResult.status !== 0) { + process.exit(cargoResult.status || 1); +} + +fs.mkdirSync(docDir, { recursive: true }); + +[ + 'crowdin.js', + 'crowdin-rustdoc-css.css', +].forEach((asset) => { + fs.copyFileSync(path.join(sharedWebDist, asset), path.join(docDir, asset)); +}); diff --git a/examples/rustdoc/rustdoc/shared-web.html b/examples/rustdoc/rustdoc/shared-web.html new file mode 100644 index 0000000..9b41cfd --- /dev/null +++ b/examples/rustdoc/rustdoc/shared-web.html @@ -0,0 +1,20 @@ + + + diff --git a/examples/rustdoc/src/lib.rs b/examples/rustdoc/src/lib.rs new file mode 100644 index 0000000..d7fad91 --- /dev/null +++ b/examples/rustdoc/src/lib.rs @@ -0,0 +1,68 @@ +//! # shared-web rustdoc sample +//! +//! This is a sample crate for rustdoc. It demonstrates how the shared-web +//! CrowdIn language selector appears in Rust API documentation. +//! +//! ## Widgets +//! +//! ### CrowdIn +//! +//! Install `@lizardbyte/shared-web`, then add a rustdoc HTML hook file: +//! +//! ```html +//! +//! +//! +//! ``` +//! +//! Configure Cargo to pass the hook to rustdoc: +//! +//! ```toml +//! [build] +//! rustdocflags = [ +//! "--html-after-content", +//! "rustdoc/shared-web.html", +//! ] +//! ``` +//! +//! Copy `crowdin.js` and `crowdin-rustdoc-css.css` from +//! `node_modules/@lizardbyte/shared-web/dist` into the generated rustdoc root +//! so the hook can load them with rustdoc's page-relative `rootPath`. + +/// Returns a greeting for the provided project name. +/// +/// # Examples +/// +/// ``` +/// let greeting = shared_web_rustdoc_example::greeting("shared-web"); +/// assert_eq!(greeting, "Hello, shared-web!"); +/// ``` +pub fn greeting(project: &str) -> String { + format!("Hello, {project}!") +} + +/// Example metadata rendered by rustdoc. +#[derive(Debug, Eq, PartialEq)] +pub struct Widget { + /// Human-readable widget name. + pub name: String, + /// Whether the widget is enabled in the rendered page. + pub enabled: bool, +} diff --git a/jsdoc.json b/jsdoc.json index 06e5364..0cec9c5 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -40,6 +40,13 @@ "class": "", "id": "sphinx-sample" }, + { + "title": "Rustdoc Sample", + "link": "rustdoc/doc/shared_web_rustdoc_example", + "target": "_blank", + "class": "", + "id": "rustdoc-sample" + }, { "title": "❤ Donate", "link": "https://app.lizardbyte.dev", diff --git a/src/css/crowdin-rustdoc.scss b/src/css/crowdin-rustdoc.scss new file mode 100644 index 0000000..946cba8 --- /dev/null +++ b/src/css/crowdin-rustdoc.scss @@ -0,0 +1,30 @@ +#crowdin-language-picker.rustdoc-crowdin-picker { + margin: 1rem; + width: calc(100% - 2rem); +} + +#crowdin-language-picker.rustdoc-crowdin-picker .cr-picker-button { + box-sizing: border-box; + width: 100%; +} + +#crowdin-language-picker .cr-picker-button, +#crowdin-language-picker .cr-picker-submenu { + background-color: var(--sidebar-background-color, var(--main-background-color, #ffffff)) !important; + border: 1px solid var(--border-color, #dddddd) !important; + color: var(--main-color, #111111) !important; +} + +#crowdin-language-picker .cr-picker-button:hover, +#crowdin-language-picker .cr-picker-submenu > a:hover { + background-color: var(--main-color, #111111) !important; + color: var(--main-background-color, #ffffff) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a { + color: var(--main-color, #111111) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a.cr-selected { + color: var(--link-color, #3873ad) !important; +} diff --git a/src/js/crowdin-rustdoc-css.js b/src/js/crowdin-rustdoc-css.js new file mode 100644 index 0000000..7669680 --- /dev/null +++ b/src/js/crowdin-rustdoc-css.js @@ -0,0 +1 @@ +import "../css/crowdin-rustdoc.scss"; diff --git a/src/js/crowdin.js b/src/js/crowdin.js index 69fadc4..1a40e80 100644 --- a/src/js/crowdin.js +++ b/src/js/crowdin.js @@ -49,7 +49,7 @@ function _installCrowdinFetchInterceptor() { /** * Initializes Crowdin translation widget based on project and UI platform. * @param {string} project - Project name ('LizardByte' or 'LizardByte-docs'). - * @param {string|null} platform - UI platform ('sphinx', or null). + * @param {string|null} platform - UI platform ('sphinx', 'rustdoc', or null). */ function initCrowdIn(project = 'LizardByte', platform = null) { // Input validation @@ -57,8 +57,8 @@ function initCrowdIn(project = 'LizardByte', platform = null) { console.error('Invalid project. Must be "LizardByte" or "LizardByte-docs"'); return; } - if (!['sphinx', null].includes(platform)) { - console.error('Invalid UI. Must be "sphinx", or null'); + if (!['sphinx', 'rustdoc', null].includes(platform)) { + console.error('Invalid UI. Must be "sphinx", "rustdoc", or null'); return; } @@ -143,6 +143,21 @@ function initCrowdIn(project = 'LizardByte', platform = null) { // move button to related pages sidebar.appendChild(container); } + + if (platform === 'rustdoc') { + const sidebar = document.querySelector('.sidebar .sidebar-elems') || document.querySelector('.sidebar'); + if (sidebar === null) { + return; + } + + container.classList.remove('cr-position-bottom-left'); + container.classList.add('rustdoc-crowdin-picker'); + container.style.position = 'static'; + container.style.left = 'auto'; + container.style.bottom = 'auto'; + + sidebar.appendChild(container); + } }); } diff --git a/tests/crowdin.test.js b/tests/crowdin.test.js index 437e16d..c079b84 100644 --- a/tests/crowdin.test.js +++ b/tests/crowdin.test.js @@ -29,6 +29,11 @@ describe('initCrowdIn', () => {
+ + + `; // Mock console.error @@ -60,7 +65,7 @@ describe('initCrowdIn', () => { it('should validate platform parameter', () => { initCrowdIn('LizardByte', 'invalidPlatform'); - expect(console.error).toHaveBeenCalledWith('Invalid UI. Must be "sphinx", or null'); + expect(console.error).toHaveBeenCalledWith('Invalid UI. Must be "sphinx", "rustdoc", or null'); }); it('should initialize proxyTranslator with LizardByte settings', () => { @@ -117,6 +122,21 @@ describe('initCrowdIn', () => { expect(container.style.position).toBe('relative'); expect(sidebar.contains(container)).toBe(true); }); + + it('should apply rustdoc styling', () => { + initCrowdIn('LizardByte', 'rustdoc'); + + // Simulate script loading and UI styling timeout + jest.runAllTimers(); + + const container = document.getElementById('crowdin-language-picker'); + const sidebar = document.getElementsByClassName('sidebar-elems')[0]; + + expect(container.classList.contains('cr-position-bottom-left')).toBe(false); + expect(container.classList.contains('rustdoc-crowdin-picker')).toBe(true); + expect(container.style.position).toBe('static'); + expect(sidebar.contains(container)).toBe(true); + }); }); describe('Crowdin fetch interceptor', () => { diff --git a/webpack.config.js b/webpack.config.js index d504356..2ffecf2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,6 +11,7 @@ let config = { 'crowdin-clean-jsdoc-css': './src/js/crowdin-clean-jsdoc-css', 'crowdin-doxygen-css': './src/js/crowdin-doxygen-css', 'crowdin-furo-css': './src/js/crowdin-furo-css', + 'crowdin-rustdoc-css': './src/js/crowdin-rustdoc-css', 'format-number': './src/js/format-number', 'levenshtein-distance': './src/js/levenshtein-distance', 'lizardbyte-css': './src/js/lizardbyte-css', From 1edb51b58f5a380cf6539940a204fbb476a03f2f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 22 May 2026 15:52:21 -0400 Subject: [PATCH 2/4] Add rustdoc asset copying, styling retries, and tests Copy crowdin assets beside every generated rustdoc HTML page and simplify the rustdoc hook to load those files directly. Add a recursive finder for generated HTML dirs in the example build script and update example docs to explain the hook location and behavior. Improve crowdin.js by adding retryable platform-specific styling logic for Sphinx and rustdoc, minor constants, and a small refactor to apply styling via a helper; also harden fetch interception edge cases. Add new and expanded Jest tests (platform styling retries, fetch-edge cases, global exposure in Node, and load-script no-callback handling) and tighten test coverage settings in package.json while excluding generated *-css.js files from coverage. --- examples/rustdoc/rustdoc/build.js | 33 +++++- examples/rustdoc/rustdoc/shared-web.html | 19 +--- examples/rustdoc/src/lib.rs | 37 ++----- package.json | 11 +- src/js/crowdin.js | 95 +++++++++++------ tests/crowdin.test.js | 127 +++++++++++++++++++++++ tests/global-exposure.test.js | 31 ++++++ tests/load-script.test.js | 22 ++++ 8 files changed, 295 insertions(+), 80 deletions(-) create mode 100644 tests/global-exposure.test.js diff --git a/examples/rustdoc/rustdoc/build.js b/examples/rustdoc/rustdoc/build.js index e7c2915..01ac9f2 100644 --- a/examples/rustdoc/rustdoc/build.js +++ b/examples/rustdoc/rustdoc/build.js @@ -9,6 +9,30 @@ const readTheDocsOutput = process.env.READTHEDOCS_OUTPUT ? const targetDir = path.join(readTheDocsOutput, 'html', 'rustdoc'); const docDir = path.join(targetDir, 'doc'); const sharedWebDist = path.join(exampleDir, 'node_modules', '@lizardbyte', 'shared-web', 'dist'); +const sharedWebAssets = [ + 'crowdin.js', + 'crowdin-rustdoc-css.css', +]; + +function findHtmlDirectories(dir) { + const directories = new Set(); + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + entries.forEach((entry) => { + const entryPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + findHtmlDirectories(entryPath).forEach((htmlDir) => directories.add(htmlDir)); + return; + } + + if (entry.isFile() && entry.name.endsWith('.html')) { + directories.add(dir); + } + }); + + return directories; +} const cargoResult = spawnSync('cargo', ['doc', '--no-deps', '--target-dir', targetDir], { stdio: 'inherit', @@ -20,9 +44,8 @@ if (cargoResult.status !== 0) { fs.mkdirSync(docDir, { recursive: true }); -[ - 'crowdin.js', - 'crowdin-rustdoc-css.css', -].forEach((asset) => { - fs.copyFileSync(path.join(sharedWebDist, asset), path.join(docDir, asset)); +findHtmlDirectories(docDir).forEach((htmlDir) => { + sharedWebAssets.forEach((asset) => { + fs.copyFileSync(path.join(sharedWebDist, asset), path.join(htmlDir, asset)); + }); }); diff --git a/examples/rustdoc/rustdoc/shared-web.html b/examples/rustdoc/rustdoc/shared-web.html index 9b41cfd..21b31c8 100644 --- a/examples/rustdoc/rustdoc/shared-web.html +++ b/examples/rustdoc/rustdoc/shared-web.html @@ -1,20 +1,7 @@ + + diff --git a/examples/rustdoc/src/lib.rs b/examples/rustdoc/src/lib.rs index d7fad91..d0921d5 100644 --- a/examples/rustdoc/src/lib.rs +++ b/examples/rustdoc/src/lib.rs @@ -7,30 +7,14 @@ //! //! ### CrowdIn //! -//! Install `@lizardbyte/shared-web`, then add a rustdoc HTML hook file: +//! Install `@lizardbyte/shared-web`, then create the rustdoc HTML hook at +//! `rustdoc/shared-web.html` in your crate root. The crate root is the +//! directory that contains `Cargo.toml`, so this example stores the hook at +//! `examples/rustdoc/rustdoc/shared-web.html`. //! -//! ```html -//! -//! -//! -//! ``` +//! The hook loads `crowdin.js` and `crowdin-rustdoc-css.css` from the same +//! directory as each generated HTML page. The example build script copies +//! those two files beside every generated `.html` file. //! //! Configure Cargo to pass the hook to rustdoc: //! @@ -42,9 +26,10 @@ //! ] //! ``` //! -//! Copy `crowdin.js` and `crowdin-rustdoc-css.css` from -//! `node_modules/@lizardbyte/shared-web/dist` into the generated rustdoc root -//! so the hook can load them with rustdoc's page-relative `rootPath`. +//! When adapting this outside the example, copy `crowdin.js` and +//! `crowdin-rustdoc-css.css` from `node_modules/@lizardbyte/shared-web/dist` +//! beside each generated rustdoc HTML page, or adjust the hook paths to point +//! at a location that every generated page can reach. /// Returns a greeting for the provided project name. /// diff --git a/package.json b/package.json index efce28d..a1beb25 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,17 @@ }, "jest": { "collectCoverageFrom": [ - "src/**/*.{js,jsx}" + "src/**/*.{js,jsx}", + "!src/js/*-css.js" ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, "testEnvironment": "jsdom" }, "scripts": { diff --git a/src/js/crowdin.js b/src/js/crowdin.js index 1a40e80..875be0c 100644 --- a/src/js/crowdin.js +++ b/src/js/crowdin.js @@ -9,6 +9,8 @@ const loadScript = require('./load-script'); * @type {string} */ const CROWDIN_DIST_MIRROR = 'https://cdn.jsdelivr.net/gh/LizardByte/i18n@dist'; +const CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS = 100; +const CROWDIN_PLATFORM_STYLING_RETRY_DELAY_MS = 50; /** * Monkey-patches globalThis.fetch to redirect Crowdin distribution requests to @@ -46,6 +48,65 @@ function _installCrowdinFetchInterceptor() { }; } +/** + * Re-attempts platform styling while Crowdin inserts the language picker. + * @param {string} platform - UI platform ('sphinx' or 'rustdoc'). + * @param {number} attempt - Current retry count. + */ +function _retryCrowdinPlatformStyling(platform, attempt) { + if (attempt >= CROWDIN_PLATFORM_STYLING_MAX_ATTEMPTS) { + return; + } + + globalThis.setTimeout(function() { + _applyCrowdinPlatformStyling(platform, attempt + 1); + }, CROWDIN_PLATFORM_STYLING_RETRY_DELAY_MS); +} + +/** + * Applies platform-specific placement after the Crowdin picker exists. + * @param {string} platform - UI platform ('sphinx' or 'rustdoc'). + * @param {number} attempt - Current retry count. + */ +function _applyCrowdinPlatformStyling(platform, attempt = 0) { + const container = document.getElementById('crowdin-language-picker'); + + if (platform === 'sphinx') { + const button = document.getElementsByClassName('cr-picker-button')[0]; + const sidebar = document.getElementsByClassName('sidebar-sticky')[0]; + + if (container === null || button === undefined || sidebar === undefined) { + _retryCrowdinPlatformStyling(platform, attempt); + return; + } + + container.classList.remove('cr-position-bottom-left'); + container.style.width = button.offsetWidth + 10 + 'px'; + container.style.position = 'relative'; + container.style.left = '10px'; + container.style.bottom = '10px'; + + // move button to related pages + sidebar.appendChild(container); + return; + } + + const sidebar = document.querySelector('.sidebar .sidebar-elems') || document.querySelector('.sidebar'); + + if (container === null || sidebar === null) { + _retryCrowdinPlatformStyling(platform, attempt); + return; + } + + container.classList.remove('cr-position-bottom-left'); + container.classList.add('rustdoc-crowdin-picker'); + container.style.position = 'static'; + container.style.left = 'auto'; + container.style.bottom = 'auto'; + + sidebar.appendChild(container); +} + /** * Initializes Crowdin translation widget based on project and UI platform. * @param {string} project - Project name ('LizardByte' or 'LizardByte-docs'). @@ -127,42 +188,12 @@ function initCrowdIn(project = 'LizardByte', platform = null) { return; } - const container = document.getElementById('crowdin-language-picker'); - const button = document.getElementsByClassName('cr-picker-button')[0]; - - if (platform === 'sphinx') { - container.classList.remove('cr-position-bottom-left') - container.style.width = button.offsetWidth + 10 + 'px'; - container.style.position = 'relative'; - container.style.left = '10px'; - container.style.bottom = '10px'; - - // get rst versions - const sidebar = document.getElementsByClassName('sidebar-sticky')[0]; - - // move button to related pages - sidebar.appendChild(container); - } - - if (platform === 'rustdoc') { - const sidebar = document.querySelector('.sidebar .sidebar-elems') || document.querySelector('.sidebar'); - if (sidebar === null) { - return; - } - - container.classList.remove('cr-position-bottom-left'); - container.classList.add('rustdoc-crowdin-picker'); - container.style.position = 'static'; - container.style.left = 'auto'; - container.style.bottom = 'auto'; - - sidebar.appendChild(container); - } + _applyCrowdinPlatformStyling(platform); }); } // Expose to the global scope -if (typeof globalThis !== 'undefined' && globalThis.window !== undefined) { +if (globalThis.window !== undefined) { globalThis.initCrowdIn = initCrowdIn; } diff --git a/tests/crowdin.test.js b/tests/crowdin.test.js index c079b84..5ee3e5d 100644 --- a/tests/crowdin.test.js +++ b/tests/crowdin.test.js @@ -137,6 +137,98 @@ describe('initCrowdIn', () => { expect(container.style.position).toBe('static'); expect(sidebar.contains(container)).toBe(true); }); + + it('should wait for sphinx language picker before applying styling', () => { + globalThis.document.body.innerHTML = ` + + `; + + initCrowdIn('LizardByte', 'sphinx'); + + expect(() => { + jest.advanceTimersByTime(0); + }).not.toThrow(); + + globalThis.document.body.insertAdjacentHTML('beforeend', ` +