Skip to content
78 changes: 36 additions & 42 deletions src/client/imageFigureNumber.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,48 @@
* Client module to automatically add figure numbers and captions to images
* based on their alt text in blog posts and docs.
*/
export function onRouteDidUpdate({ location, previousLocation }) {

/**
* Derive the locale from the current pathname so the caption prefix is always
* consistent with the page the user navigated *to*, not with whatever stale
* value `document.documentElement.lang` happens to hold during the transition.
*
* @param {string} pathname
* @returns {"zh" | "en"}
*/
function localeFromPathname(pathname) {
return pathname.startsWith("/zh/") || pathname === "/zh" ? "zh" : "en";
}

export function onRouteDidUpdate({ location }) {
// Only run on blog and doc pages
if (!location.pathname.match(/\/blog\//) && !location.pathname.match(/\/docs\//)) {
return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Wait for DOM to be ready with multiple attempts
setTimeout(() => addFigureNumbers(), 100);
setTimeout(() => addFigureNumbers(), 500);
setTimeout(() => addFigureNumbers(), 1000);
const locale = localeFromPathname(location.pathname);

// A single rAF tick is sufficient: Docusaurus has already committed the new
// page content to the DOM by the time onRouteDidUpdate fires. Using rAF
// instead of multiple staggered setTimeouts avoids duplicate caption
// injections when routes are navigated quickly.
requestAnimationFrame(() => addFigureNumbers(locale));
Comment thread
moezdil marked this conversation as resolved.
Comment thread
moezdil marked this conversation as resolved.
}

function addFigureNumbers() {
/**
* @param {"zh" | "en"} locale
*/
function addFigureNumbers(locale) {
// Find all images in markdown content
const articleContent = document.querySelector("article");
if (!articleContent) {
return;
}

// Find all images that are not in header or footer
const prefix = locale === "zh" ? "图" : "Figure";

// Find all images that are not logos or small icons
const images = Array.from(articleContent.querySelectorAll("img")).filter((img) => {
// Filter out logos, avatars, icons
const parentClass = img.parentElement?.className || "";
const isLogo = parentClass.includes("logo") || img.alt.includes("logo");
const isIcon = img.width < 100 || img.height < 100;
Expand All @@ -33,57 +53,31 @@ function addFigureNumbers() {
let figureCount = 0;

images.forEach((img) => {
// Get alt text
const altText = img.getAttribute("alt") || "";
if (!altText.trim()) {
return;
}

// Check if already has figcaption with figure number
const existingFigure = img.closest("figure");
if (existingFigure) {
const existingFigcaption = existingFigure.querySelector("figcaption");
if (existingFigcaption && existingFigcaption.textContent.match(/^(图|Figure)\d+:/)) {
figureCount++;
return;
}
}

// Increment counter
figureCount++;

// Create figure wrapper if it doesn't exist
// Wrap in <figure> if not already wrapped
let figure = img.closest("figure");
if (!figure) {
figure = document.createElement("figure");
img.parentNode.insertBefore(figure, img);
figure.appendChild(img);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Remove existing figcaption if any (but only if it doesn't have figure number)
const existingFigcaption = figure.querySelector("figcaption");
if (existingFigcaption && !existingFigcaption.textContent.match(/^(图|Figure)\d+:/)) {
existingFigcaption.remove();
} else if (existingFigcaption) {
// Update existing figcaption with figure number
const currentLang = document.documentElement.lang || "en";
const prefix = currentLang.startsWith("zh") ? "图" : "Figure";
existingFigcaption.textContent = `${prefix}${figureCount}: ${altText}`;
return;
// Find or create the <figcaption>, always overwriting its text so that
// navigating between locales corrects stale captions from a previous run.
let figcaption = figure.querySelector("figcaption");
if (!figcaption) {
figcaption = document.createElement("figcaption");
figure.appendChild(figcaption);
}

// Create figcaption with number and alt text
const figcaption = document.createElement("figcaption");
const currentLang = document.documentElement.lang || "en";

// Use appropriate prefix based on language
const prefix = currentLang.startsWith("zh") ? "图" : "Figure";
figcaption.textContent = `${prefix}${figureCount}: ${altText}`;

// Append figcaption to figure
figure.appendChild(figcaption);

// Add styling class
// Apply styles (idempotent — repeated assignment is harmless)
figure.style.cssText = `
margin: 2em 0;
text-align: center;
Expand Down
23 changes: 15 additions & 8 deletions src/plugins/changelog/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@ import {aliasedSitePath, docuHash, normalizeUrl} from '@docusaurus/utils';
/**
* Multiple versions may be published on the same day, causing the order to be
* the reverse. Therefore, our publish time has a "fake hour" to order them.
*
* NOTE: Both sets are passed in from loadContent() so that each build pass
* (en, zh, …) starts with a clean slate and locale runs cannot interfere with
* each other's timestamps or author maps.
*/
const publishTimes = new Set();
/**
* @type {Record<string, {name: string, url: string,alias: string, imageURL: string}>}
*/
const authorsMap = {};

/**
* @param {string} section
* @param {Set<string>} publishTimes per-invocation dedup set
* @param {Record<string, {name: string, url: string, alias: string, imageURL: string}>} authorsMap per-invocation authors accumulator
*/
function processSection(section) {
function processSection(section, publishTimes, authorsMap) {
const title = section
.match(/\n## .*/)?.[0]
.trim()
Expand Down Expand Up @@ -94,7 +95,7 @@ function processSection(section) {
publishTimes.add(`${date}T${hour}:00`);

return {
title: title.replace(/ \(.*\)/, ''),
title: title.replace(/ \(.*\)/, ""),
Comment thread
moezdil marked this conversation as resolved.
Outdated
content: `---
mdx:
format: md
Expand Down Expand Up @@ -138,10 +139,16 @@ export default async function ChangelogPlugin(context, options) {
...blogPlugin,
name: 'changelog-plugin',
async loadContent() {
// Create fresh state per loadContent() call so that sequential locale
// build passes (en → zh) cannot see each other's timestamps or authors.
const publishTimes = new Set();
/** @type {Record<string, {name: string, url: string, alias: string, imageURL: string}>} */
const authorsMap = {};

const fileContent = (await fs.readFile(changelogPath, 'utf-8')).replace(/\r\n/g, '\n');
const sections = fileContent
.split(/(?=\n## )/)
.map(processSection)
.map((section) => processSection(section, publishTimes, authorsMap))
.filter(Boolean);
await Promise.all(
sections.map((section) =>
Expand Down