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
89 changes: 43 additions & 46 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 }) {
// Only run on blog and doc pages
if (!location.pathname.match(/\/blog\//) && !location.pathname.match(/\/docs\//)) {

/**
* 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 (including root doc paths like /docs or /zh/docs)
if (!/(?:^|\/)(?:blog|docs)(?:$|\/)/.test(location.pathname)) {
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));
}

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,34 @@ 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. If the image is inside an anchor link,
// move the anchor element into the figure to preserve clickable links.
let figure = img.closest("figure");
if (!figure) {
figure = document.createElement("figure");
img.parentNode.insertBefore(figure, img);
figure.appendChild(img);
const targetElement =
img.parentElement && img.parentElement.tagName === "A" ? img.parentElement : img;
targetElement.parentNode.insertBefore(figure, targetElement);
figure.appendChild(targetElement);
}

// 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(/ \(.*\)/, ""),
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