ext-apps/scripts/typedoc-plugin-mcpstyle.mjs
Den Delimarsky a870456893
fix(readme): use picture element for theme-aware logo (#545)
* fix(readme): use picture element for theme-aware logo

The #gh-light-mode-only / #gh-dark-mode-only URL fragments only work
on github.com. On npm, VS Code preview, and other markdown renderers
both img tags render, showing two stacked logos.

Switch to <picture> with prefers-color-scheme, which GitHub recommends
and which degrades gracefully to a single logo elsewhere.

* ci: add Cloudflare Pages preview for docs PRs

Deploys a per-PR preview of the TypeDoc site to Cloudflare Pages project
mcp-ext-apps-docs-preview when docs, src JSDoc, README, or typedoc config
change. Uses the shared modelcontextprotocol/actions composite action that
injects noindex headers, posts a sticky PR comment with preview URLs, and
cleans up deployments on PR close. Fork PRs are skipped (no secret access).

* fix(docs): sync README logo with TypeDoc theme switcher

The README <picture> logo uses prefers-color-scheme, which only follows
the OS setting. TypeDoc's theme switcher sets data-theme on <html>, so
manually picking Light/Dark would show the wrong logo (e.g. white logo
on light background if OS is dark but user picked Light).

Rewrite the <picture> in the generated docs into two class-tagged <img>s
via the mcpstyle plugin, and hide the inactive one via CSS keyed on both
data-theme (explicit selection) and prefers-color-scheme (OS mode). The
README source is unchanged so GitHub/npm still get <picture>.

Also: prettier normalized mcp-theme.css (tabs to spaces, quote style).
2026-03-10 17:33:40 +00:00

67 lines
3.5 KiB
JavaScript

/**
* TypeDoc plugin that applies MCP-specific styling tweaks.
*
* - Moves custom.css to load last so overrides win the cascade
* - Replaces breadcrumbs with the document group name (e.g. "Security")
* - Marks the current sidebar nav link for CSS highlighting
* - Rewrites the README <picture> logo to a pair of theme-tagged <img>s so
* the logo follows the TypeDoc theme switcher (data-theme), not just the
* OS prefers-color-scheme media query
*/
import { Renderer } from "typedoc";
/**
* TypeDoc plugin entry point.
* @param {import('typedoc').Application} app
*/
export function load(app) {
app.renderer.on(Renderer.EVENT_END_PAGE, (page) => {
if (!page.contents) return;
// Move custom.css to load after all theme stylesheets so overrides win the cascade
const customCssLink = page.contents.match(
/<link rel="stylesheet" href="[^"]*custom\.css"\/>/,
);
if (customCssLink) {
page.contents = page.contents.replace(customCssLink[0], "");
page.contents = page.contents.replace(
"</head>",
customCssLink[0] + "\n</head>",
);
}
// For document pages, replace the breadcrumb with the group name
// (e.g. "Security", "Getting Started"). The page title is in the H1.
if (page.model?.isDocument?.() && page.model.frontmatter?.group) {
const group = String(page.model.frontmatter.group);
page.contents = page.contents.replace(
/<ul class="tsd-breadcrumb"[^>]*>.*?<\/ul>/,
`<ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><span>${group}</span></li></ul>`,
);
}
// The README logo uses <picture> with prefers-color-scheme, which follows
// the OS setting and ignores the TypeDoc theme switcher (data-theme attr).
// Rewrite it as two <img> tags — CSS in mcp-theme.css picks the correct
// one based on both data-theme and prefers-color-scheme (for "OS" mode).
page.contents = page.contents.replace(
/<picture>\s*<source media="\(prefers-color-scheme: dark\)" srcset="([^"]+)">\s*<source media="\(prefers-color-scheme: light\)" srcset="([^"]+)">\s*<img src="[^"]+"([^>]*)>\s*<\/picture>/,
(_, darkSrc, lightSrc, imgAttrs) =>
`<img src="${lightSrc}"${imgAttrs} class="mcp-logo-light">` +
`<img src="${darkSrc}"${imgAttrs} class="mcp-logo-dark">`,
);
// Inject script to mark the current sidebar nav link with a "current" class.
// TypeDoc does not natively add this class for document pages.
// The sidebar is populated asynchronously from compressed navigation data,
// so we use a MutationObserver to detect when links appear.
// Pathname comparison strips trailing slashes and .html extensions to handle
// servers with clean-URL mode (e.g. `serve` drops .html).
const currentNavScript = `<script>(function(){function norm(s){return s.replace(/\\/$/,"").replace(/\\.html$/,"");}function mark(){var p=norm(location.pathname);var links=document.querySelectorAll(".site-menu .tsd-navigation a[href]");for(var i=0;i<links.length;i++){var h=norm(new URL(links[i].href,location.href).pathname);if(h===p){links[i].classList.add("current");return true;}}return false;}function init(){if(!mark()){var c=document.getElementById("tsd-nav-container");if(c){new MutationObserver(function(m,o){if(mark())o.disconnect();}).observe(c,{childList:true,subtree:true});}}}if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",init);}else{init();}})();</script>`;
page.contents = page.contents.replace(
"</body>",
currentNavScript + "\n</body>",
);
});
}