Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 56 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
name: CI

on:
schedule: # Build and deploy multilingual books weekly
- cron: '0 0 * * 1'
push: # Run CI for all branches except GitHub merge queue tmp branches
branches-ignore:
- "gh-readonly-queue/**"
pull_request: # Run CI for PRs on any branch
merge_group: # Run CI for the GitHub merge queue

env:
MDBOOK_VERSION: 0.5.1
MDBOOK_I18N_HELPERS_VERSION: 0.4.0

jobs:
build:
runs-on: ubuntu-24.04
Expand All @@ -17,38 +23,68 @@ jobs:
- name: Install Python dependencies
run: |
pip3 install --user python-dateutil linkchecker

- name: Put pip binary directory into path
run: echo "~/.local/bin" >> $GITHUB_PATH

- name: Cache Cargo installed binaries
uses: actions/cache@v4
id: cache-cargo
with:
path: ~/cargo-bin
key: cache-cargo
- name: Update rustup
run: rustup self update

- name: Install Rust
run: |
rustup set profile minimal
rustup toolchain install nightly -c rust-docs
rustup default nightly

- name: Install mdbook
if: steps.cache-cargo.outputs.cache-hit != 'true'
uses: actions-rs/install@v0.1
with:
crate: mdbook
version: latest
- name: Copy mdbook to cache directory
if: steps.cache-cargo.outputs.cache-hit != 'true'
run: |
mkdir ~/cargo-bin
cp ~/.cargo/bin/mdbook ~/cargo-bin
- name: Put new cargo binary directory into path
run: echo "~/cargo-bin" >> $GITHUB_PATH
cargo install mdbook --locked --version ${{ env.MDBOOK_VERSION }}

- name: Install mdbook-i18n-helpers
run: |
cargo install mdbook-i18n-helpers --locked --version ${{ env.MDBOOK_I18N_HELPERS_VERSION }}

- name: Download the .po files from localizethedocs/rust-embedded-book-l10n
run: |
git clone --branch po/master https://github.com/localizethedocs/rust-embedded-book-l10n.git po

- name: Build books
run: |
for langcode in $(jq -r '.languages[].code' switcher.json); do
echo "::group::Building $langcode"
langtag=$(jq -r --arg lang "$langcode" '.languages[] | select(.code == $lang) | .langtag' switcher.json)
MDBOOK_BOOK__LANGUAGE=$langcode mdbook build -d book/$langtag/
echo "::endgroup::"
done

- name: Build book
run: mdbook build
- name: Test book
run: mdbook test

- name: Check links
run: linkchecker book

- name: Create root index.html redirect
run: |
default_langtag=$(jq -r '.languages[] | select(.code == "en_US") | .langtag' switcher.json)
target="./${default_langtag}/"
cat > book/index.html << EOF
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=${target}" />
</head>
<body>
<p>Redirecting to <a href="${target}">${target}</a></p>
</body>
</html>
EOF

- name: Copy switcher.json to book root
run: cp switcher.json book/

- name: Deploy book
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) ||
( github.event_name == 'schedule' ) }}
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
10 changes: 10 additions & 0 deletions book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ title = "The Embedded Rust Book"

[output.html]
git-repository-url = "https://github.com/rust-embedded/book"
additional-css = [
"theme/css/switcher.css",
]
additional-js = [
"theme/js/switcher.js",
]

[preprocessor.gettext]
optional = true
after = ["links"]
7 changes: 7 additions & 0 deletions switcher.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"languages": [
{ "code": "en_US", "langtag": "en-us", "langname": "English" },
{ "code": "zh_CN", "langtag": "zh-cn", "langname": "简体中文" },
{ "code": "zh_TW", "langtag": "zh-tw", "langname": "繁體中文" }
]
}
50 changes: 50 additions & 0 deletions theme/css/switcher.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* Ensure right-buttons uses flex alignment (mirrors cairo-book chrome.css) */
.right-buttons {
display: flex;
align-items: center;
margin: 0 15px;
}

.right-buttons a {
text-decoration: none;
}

/* ---- Shared switcher styles ---- */

.language-switcher,
.version-switcher {
display: flex;
align-items: center;
gap: 0.5rem;
margin-right: 10px;
}

/* Deterministic order: both switchers always leftmost, language before version */
.language-switcher { order: -2; }
.version-switcher { order: -1; }

.language-switcher label,
.version-switcher label {
display: flex;
align-items: center;
font-size: 1.6rem;
color: var(--icons);
cursor: pointer;
}

.language-switcher select,
.version-switcher select {
padding: 0.25rem 0.5rem;
border-radius: 4px;
border: 1px solid var(--icons);
background: var(--bg);
color: var(--icons);
font-size: 1.6rem;
cursor: pointer;
}

.language-switcher select:hover,
.version-switcher select:hover {
border-color: var(--icons-hover);
color: var(--icons-hover);
}
129 changes: 129 additions & 0 deletions theme/js/switcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
(() => {
const fullPath = window.location.pathname;

// Derive siteRoot from a langtag segment (e.g. /xx-xx/) in the URL.
function deriveSiteRoot() {
const m = fullPath.match(/^(.+?)\/([a-z]{2}-[a-z]{2})\//);
return m ? m[1] : "";
}

const siteRoot = deriveSiteRoot();

function loadConfig() {
return fetch(siteRoot + "/switcher.json")
.then((r) => { if (!r.ok) throw new Error("not found"); return r.json(); })
.catch(() => null);
}

function whenReady(fn) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fn);
} else {
fn();
}
}

whenReady(() => {
loadConfig().then((data) => {
if (!data) return;

const afterSite = fullPath.substring(siteRoot.length);
const parts = afterSite.split("/").filter(Boolean);

if (data.languages) renderLanguageSwitcher(data.languages, parts);
if (data.versions) renderVersionSwitcher(data.versions, parts);
});
});

// ---- Language switcher ----

function renderLanguageSwitcher(languages, parts) {
const knownLangtags = new Set(languages.map((l) => l.langtag));

let currentLangtag = "";
let langtagIdx = -1;
for (let i = 0; i < parts.length; i++) {
if (knownLangtags.has(parts[i])) {
langtagIdx = i;
currentLangtag = parts[i];
break;
}
}
if (langtagIdx < 0) return;

const tail = "/" + parts.slice(langtagIdx + 1).join("/");

const container = document.createElement("div");
container.className = "language-switcher";
container.innerHTML =
'<label for="language-select" title="Language">' +
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1.25em" height="1.25em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/>' +
'<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>' +
'</svg></label>' +
'<select id="language-select">' +
languages.map((lang) =>
'<option value="' + lang.langtag + '"' + (lang.langtag === currentLangtag ? ' selected' : '') + '>' +
lang.langname +
'</option>'
).join("") +
'</select>';

insert(container);

document.getElementById("language-select").addEventListener("change", (e) => {
window.location.href = siteRoot + "/" + e.target.value + tail;
});
}

// ---- Version switcher ----

function renderVersionSwitcher(versions, parts) {
const knownVersions = new Set(versions.map((v) => v.version));
const defaultVersion = versions[0].version;

let currentVersion = defaultVersion;
let versionIdx = -1;
for (let i = 0; i < parts.length; i++) {
if (knownVersions.has(parts[i])) {
versionIdx = i;
currentVersion = parts[i];
break;
}
}
if (versionIdx < 0) return;

const prefix = "/" + parts.slice(0, versionIdx).join("/");
const pagePath = "/" + parts.slice(versionIdx + 1).join("/");

const container = document.createElement("div");
container.className = "version-switcher";
container.innerHTML =
'<label for="version-select" title="Version">' +
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1.25em" height="1.25em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/>' +
'<path d="M18 9a9 9 0 0 1-9 9"/>' +
'</svg></label>' +
'<select id="version-select">' +
versions.map((v) =>
'<option value="' + v.version + '"' + (v.version === currentVersion ? ' selected' : '') + '>' +
v.label +
'</option>'
).join("") +
'</select>';

insert(container);

document.getElementById("version-select").addEventListener("change", (e) => {
const newVersion = e.target.value;
window.location.href = siteRoot + prefix + "/" + newVersion + pagePath;
});
}

function insert(container) {
const menuBar = document.querySelector(".right-buttons");
if (menuBar) {
menuBar.insertBefore(container, menuBar.firstChild);
}
}
})();
Loading