diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 976bb10c3..6ce62f926 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,7 +105,7 @@ jobs: run: make build-theme - name: Install Playwright WebKit run: npx playwright install --with-deps webkit - - name: Run FOUC guard (QuantEcon/quantecon-theme-src#66) + - name: Run FOUC guard run: npm run test:fouc env: THEME_TEMPLATE: ${{ github.workspace }}/.deploy/quantecon-theme diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 727fb84e8..f1690eae1 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -1,16 +1,15 @@ # PR preview on GitHub Pages — self-contained (GITHUB_TOKEN only, no org secrets). # # Builds a real lecture site (QuantEcon/lecture-python-programming) with the -# PR's theme via a static `myst build --html` — the production export path the -# Playwright harness (live `myst start`) never exercises — and deploys it to -# the gh-pages branch under pr-preview/pr-/. A sticky PR comment links the -# preview; closing the PR tears the subdirectory down. +# PR's theme via a static `myst build --html` — the production export path, +# here run on real lecture content rather than the Playwright fixture — and +# deploys it to the gh-pages branch under pr-preview/pr-/. A sticky PR +# comment links the preview; closing the PR tears the subdirectory down. # # The content repo is a legacy Jupyter Book (_config.yml/_toc.yml): `myst init` -# performs the JB→MyST upgrade at build time, so the preview doubles as a -# continuous migration-readiness check for the lecture content (PLAN.md open -# question 4). Content is cloned at full depth so the git-metadata plugin -# (plugins/git-metadata.mjs, PR #83) renders real per-page history. +# performs the JB→MyST upgrade at build time, so every preview also exercises +# that upgrade on real lecture content. Content is cloned at full depth so the +# git-metadata plugin (plugins/git-metadata.mjs) renders real per-page history. name: PR Preview on: @@ -76,8 +75,7 @@ jobs: test -f myst.yml yq -i '.site.template = env(THEME_DIR)' myst.yml yq -i '.project.github = env(CONTENT_GITHUB)' myst.yml - # The git-metadata plugin ships with the theme from PR #83 onward; - # guard so previews still build on branches that predate it. + # Guarded so the preview still builds on a branch without the plugin. if [ -f "$PLUGIN" ]; then yq -i '.project.plugins = [env(PLUGIN)]' myst.yml fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a82e92da9..3c57cb010 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release -# Tag-triggered build → zip → GitHub Release pipeline (PLAN.md Phase 0, #71). +# Tag-triggered build → zip → GitHub Release pipeline. # # Pushing a `vX.Y.Z` tag builds the theme, assembles the same bundle that # `make build-theme` ships (adapted from upstream jupyter-book/myst-theme's @@ -10,7 +10,7 @@ name: Release # # The zip contains: # build/, public/ (from `npm run prod:build`) -# template.yml (version stamped from package.json — #71) +# template.yml (version stamped from package.json) # server.js, package.json, LICENSE, README.md, thumbnail.png, qe-logo.png # (from template/, with VERSION substituted) # package-lock.json (generated; node_modules is NOT shipped) @@ -19,10 +19,6 @@ name: Release # Guards (both fail the release): # - the tag must match `package.json` `version` # - CHANGELOG.md must have a `## [X.Y.Z]` section — it becomes the release notes -# -# This replaces the Changesets release.yml (removed in #72). The legacy -# `make deploy` push to QuantEcon/quantecon-theme remains only until consumers -# are repointed to pinned release URLs (PLAN.md Phase 0 consumer migration). on: push: @@ -88,8 +84,8 @@ jobs: # the published artifact. - run: npm run test:unit - name: Build - # esbuild occasionally deadlocks (QuantEcon/quantecon-theme-src — known - # flake); time out fast so the run can simply be retried. + # esbuild occasionally deadlocks: the build hangs rather than failing. + # Time out fast so the run can simply be retried. timeout-minutes: 20 run: npm run prod:build @@ -103,7 +99,7 @@ jobs: cp -r build "$DIST/build" cp CHANGELOG.md "$DIST/CHANGELOG.md" # Stamp both version fields from package.json so the shipped bundle - # cannot drift (#71). + # cannot drift. sed -i "s/VERSION/$VERSION/g" "$DIST/package.json" sed -E "s/^version: .*/version: $VERSION/" template.yml > "$DIST/template.yml" # Lockfile for reproducible consumer installs, without shipping node_modules. diff --git a/.npmrc b/.npmrc index df082807d..06e2b823c 100644 --- a/.npmrc +++ b/.npmrc @@ -1,8 +1,8 @@ # @myst-theme/site (1.3.x) declares a peer dependency on @remix-run/* "^1.19", # but this theme deliberately pins Remix to ~1.17.0 to match upstream -# (jupyter-book/myst-theme themes are still on ~1.17.0). Remix 1.19's client +# (jupyter-book/myst-theme's themes are on ~1.17.0). Remix 1.19's client # hard-reloads when window.__remixContext.url is undefined — which it always is # under the mystmd CLI's SSR — producing an infinite reload loop ("flashing") -# and breaking the in-page outline. 1.17 hydrates gracefully, like v1.1.1. +# and breaking the in-page outline. 1.17 hydrates gracefully. # legacy-peer-deps lets npm install/ci accept the intentional version mismatch. legacy-peer-deps=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42389ce99..f5f740a15 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,6 +149,64 @@ Sphinx-era repos), but the theme treats both styles identically. Making gated cells genuinely root-level in the AST would be an upstream mystmd transform change, not a theme or content fix. +## Code comments + +Comments explain the code as it stands: what it does, and why it is written +that way when the code alone does not say. How it came to be written belongs +in git — the commit message, the PR and `CHANGELOG.md` — where it stays +accurate; a comment that narrates a project phase goes stale once the work is +done. + +This applies to comments in code — including the comments in YAML, shell and +fixture files. Prose documentation (`README.md`, `docs/`, `PLAN.md`, the test +suite's own `README.md`) is written for a reader who wants the project's +history, and keeps it. + +- **No project framing.** No phase numbers, `PLAN.md` items or milestones, and + no issue number used as a label for the work that produced the code. + `git log -L` traces any line back through the PRs that shaped it. +- **Link an issue only when the thread carries more than the comment can.** + An upstream bug being worked around, a field a fork adds, the regression a + test exists to catch, the measurements a claim rests on, an open decision + that will change the code. State the point itself in the comment as well, so + the comment stands on its own if the link rots. +- **Reasons, not comparisons.** "Matches the Sphinx build" on its own does not + tell the next reader what breaks if the value changes. Give the reason: the + measurement, the contrast ratio, the layout constraint. + + A constraint from another system *is* a reason, and naming it is not project + framing — this theme reproduces the deployed Sphinx lecture sites, so for a + value derived by measuring them, that derivation is the reason and belongs in + the comment: `/* 14.4px: the lecture builds' .8rem of an 18px root */`. What + the rule rules out is the bare comparison that leaves the number unexplained. + + Code ported from another project gets one line of provenance where the port + starts. + +### Matching the lecture builds, and when not to + +Most values in `styles/quantecon.css` are measurements of the deployed Sphinx +lecture sites, and most inherited colours come from `quantecon-book-theme`'s +own palettes. The rule the theme follows is: **match them, except where +matching them would fail WCAG.** Where it diverges it says so, with the +measurement, so the next reader does not "restore parity" and reintroduce the +problem. The existing divergences are the model: + +- the outline's resting entries stay undimmed at 10.3:1, where the Sphinx panel + composites its own to 4.47:1; +- the outline's active entry carries weight and a rule as well as colour, so the + state is not colour-only (WCAG 1.4.1); +- the dark footer link is blue-300, because the inherited `#0072bc` composites + to 2.2:1 at the footer's opacity; +- content is a flat 18px off a `rem` root, rather than the Sphinx builds' + px root, which is a WCAG 1.4.4 problem. + +Changing an inherited value for any *other* reason is a two-theme decision, not +a fix this repo makes alone — it would reintroduce a difference against the live +sites. Open an issue instead (see #172 for the code palette, #201 for the +footer). When you compute a contrast ratio, composite any `opacity` on the +element first: the declared colour is not what the reader sees. + ## Commit Convention We use conventional commits: diff --git a/Makefile b/Makefile index c9974e7b8..67b069530 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,6 @@ # Releases ship via the tag-triggered GitHub Release workflow # (.github/workflows/release.yml) — see CONTRIBUTING.md "Releases". -# The old `make deploy` flow (pushing the bundle to the now-archived -# QuantEcon/quantecon-theme build repo) was retired with it. THEME = quantecon-theme VERSION = $(shell cat package.json | jq -r '.version') diff --git a/app/backend/loaders.server.ts b/app/backend/loaders.server.ts index 2c6abf00e..f2c94b815 100644 --- a/app/backend/loaders.server.ts +++ b/app/backend/loaders.server.ts @@ -134,11 +134,10 @@ export async function getMystSearchJson(): Promise { } /** - * The QuantEcon lectures favicon (byte-identical to the Sphinx sites' - * `_static/lectures-favicon.ico`), served when no `favicon` option is set. + * The QuantEcon lectures favicon, served when no `favicon` option is set. * It lives under public/logos/ rather than at public/favicon.ico: a static - * file at that path is served ahead of the `[favicon.ico]` route, which is - * why the `favicon` site option never took effect before #173. + * file at that path is served ahead of the `[favicon.ico]` route, so the + * `favicon` site option would never take effect. */ const DEFAULT_FAVICON = "public/logos/lectures-favicon.png"; diff --git a/app/components/Document.tsx b/app/components/Document.tsx index 530d5f4b6..ed82689c1 100644 --- a/app/components/Document.tsx +++ b/app/components/Document.tsx @@ -34,13 +34,14 @@ import classNames from 'classnames'; * * No Radix `DirectionProvider` is needed: the floating positioning under * dropdown menus and tooltips reads the computed CSS `direction`, which - * `dir` on sets, so they mirror on their own. (A provider was tried - * and found inert: the theme's copy of `@radix-ui/react-direction` was a - * different instance from the one the menu and select packages pin, so its - * context never reached them.) + * `dir` on sets, so they mirror on their own. (A provider would not + * reach them anyway: in this lockfile `@radix-ui/react-menu`, + * `react-roving-focus` and `react-select` each nest their own copy of + * `@radix-ui/react-direction`, so a context from any other copy is invisible + * to them.) * * Everything else is upstream's, unchanged, so a future upstream `lang` / - * `dir` prop (see UPSTREAM-CANDIDATES.yml) lets this file be deleted again. + * `dir` prop (see UPSTREAM-CANDIDATES.yml) lets this file be deleted. * Kept in step with @myst-theme/site 1.3.0. */ export function Document({ diff --git a/app/components/Outline.tsx b/app/components/Outline.tsx index 855d166c1..0f3cce9fd 100644 --- a/app/components/Outline.tsx +++ b/app/components/Outline.tsx @@ -18,18 +18,17 @@ export function BackToTop() { } )} > - {/* Label matches the Sphinx build's back-to-top button exactly: - U+2191 + space + "Top". `aria-label` is deliberately kept -- the - Sphinx original leans on `title` alone, which screen readers - announce inconsistently, so the visible label can shorten without - the accessible name going with it. + {/* Visible label: U+2191 + space + "Top". `aria-label` is kept + deliberately -- `title` alone is announced inconsistently by + screen readers, so the visible label can stay short without the + accessible name going with it. A plain fragment link, not the provider `Link`: this control is - rendered server-side, and the provider resolved `#top` against - the un-slashed SSR pathname while the outline (built after mount) - resolved against the slashed one -- so on the deployed lecture - sites "Top" was the one in-page control that left the document - (301 + full reload, #186). A bare `href="#top"` is what the Sphinx - build renders and never needs path resolution. */} + rendered server-side, where the provider would resolve `#top` + against the un-slashed SSR pathname rather than the slashed one + the outline (built after mount) sees -- so on a static host that + redirects to the slashed URL, "Top" would leave the document + (301 + full reload) instead of scrolling. A bare `href="#top"` + never needs path resolution. */} ↑ Top @@ -40,34 +39,28 @@ export function BackToTop() { } /** - * The right-hand "On this page" panel (#182). Pinned, scroll-tracking and - * nested, matching the Sphinx build's `sticky_contents` panel: + * The right-hand "On this page" panel -- pinned, scroll-tracking and nested: * - * - The current entry is chosen by `useActiveHeading`, a port of the Sphinx - * build's scrollspy rule (the last heading whose top has passed 120px - * from the viewport top; the final one within 50px of the page bottom), - * rather than by `useHeaders`' own `activeId`. That id is computed from an - * IntersectionObserver pass against the theme's navbar offset, and in this - * theme the hook reads that offset as 0 (measured on the fixture: the - * topmost heading in the upper third wins, even one hidden under the - * navbar), so the deployed sites' rule is both more predictable and the - * parity target. + * - The current entry is chosen by `useActiveHeading` (the last heading whose + * top has passed 120px from the viewport top; the final one within 50px of + * the page bottom) rather than by `useHeaders`' own `activeId`. That id is + * computed from an IntersectionObserver pass against the theme's navbar + * offset, and in this theme the hook reads that offset as 0 (measured on the + * fixture: the topmost heading in the upper third wins, even one hidden + * under the navbar), so a fixed scroll offset is the more predictable rule. * - Pinning is `position: fixed` in styles/quantecon.css (`.qe-outline`), not * `sticky`: the wrapper is `self-start`, so a sticky child would have zero * travel, and the grid declares no rows for it to span. `left`/`right` stay * `auto`, so the panel keeps its static position in the margin track. - * - h3 entries nest under their h2 and collapse to the active branch, as the - * Sphinx panel does under `contents_autoexpand` (on by default there and - * on every lecture site): at the top of the page only the sections show; - * scrolling into a section expands its subsections, the current one - * (section or subsection) is marked, and the parent of a current - * subsection is expanded but not marked. Past `max-height` the panel - * scrolls internally. + * - h3 entries nest under their h2 and collapse to the active branch: at the + * top of the page only the sections show; scrolling into a section expands + * its subsections, the current one (section or subsection) is marked, and + * the parent of a current subsection is expanded but not marked. Past + * `max-height` the panel scrolls internally. * - Enumerators come from the heading itself (`span.select-none`, "3.1"), - * plus the period the h1 and the Sphinx panel use -- never a number - * computed from the list index, which was wrong as soon as h3s existed. - * - The logo sits below the list and above "Powered by", where the Sphinx - * panel keeps it (decided on review of #196); smaller, per #96. + * plus the period the h1 uses -- never a number computed from the list + * index, which miscounts as soon as h3s are listed. + * - The logo sits below the list and above "Powered by". */ export function Outline({ containerClassName, @@ -81,8 +74,8 @@ export function Outline({ const { headings } = useHeaders('main h2, main h3', 3); const currentId = useActiveHeading(headings); const tree = nest(headings); - // Sphinx's autoexpand: the current item's own sub-list, and every ancestor - // of the current item, are expanded; nothing else is. + // The current item's own sub-list, and every ancestor of the current item, + // are expanded; nothing else is. const expandedId = tree.find( (branch) => branch.id === currentId || branch.children.some((c) => c.id === currentId) )?.id; @@ -162,8 +155,8 @@ function Entry({ currentId?: string; Link: ReturnType; }) { - // The heading's own enumerator ("3.1"), plus the period the h1 and the - // Sphinx panel use; the bare title when the project sets no numbering. + // The heading's own enumerator ("3.1"), plus the period the h1 uses; the + // bare title when the project sets no numbering. const enumerator = heading.element.querySelector('span.select-none')?.textContent?.trim(); return ( @@ -172,17 +165,17 @@ function Entry({ ); } -/** Sphinx's `scrollspy.js` constants: a section is current once its heading - * has passed this many px from the top; the last section is forced current - * this close to the page bottom. */ +/** From quantecon-book-theme's `scrollspy.js`, as is `useActiveHeading`: a + * section is current once its heading has passed this many px from the top; + * the last section is forced current this close to the page bottom. */ const ACTIVATION_OFFSET_PX = 120; const BOTTOM_THRESHOLD_PX = 50; /** - * The id of the heading the reader is in, by the Sphinx rule: the last heading - * whose top is at or above the activation line, or the last heading of all - * once the page is scrolled to its bottom. Nothing is current above the first - * heading. Recomputed on scroll and resize, one read per animation frame. + * The id of the heading the reader is in: the last heading whose top is at or + * above the activation line, or the last heading of all once the page is + * scrolled to its bottom. Nothing is current above the first heading. + * Recomputed on scroll and resize, one read per animation frame. */ function useActiveHeading(headings: { id: string; element: HTMLElement }[]) { const [currentId, setCurrentId] = useState(undefined); diff --git a/app/components/PageContent.tsx b/app/components/PageContent.tsx index 9873d8986..ec237d0e8 100644 --- a/app/components/PageContent.tsx +++ b/app/components/PageContent.tsx @@ -37,9 +37,9 @@ export const PageContent = React.memo(function ({ article }: { article: PageLoad const keywords = article.frontmatter?.keywords ?? []; const parts = extractKnownParts(tree, article.frontmatter?.parts); const projectParts = config?.parts ?? {}; - // Translator credit (#143): the site option, overridden per page under - // `site:` in the page's frontmatter (replace, never merge; an explicit empty - // value suppresses). See app/i18n.ts for the rules and the string form. + // Translator credit: the site option, overridden per page under `site:` in + // the page's frontmatter (replace, never merge; an explicit empty value + // suppresses). See app/i18n.ts for the rules and the string form. const siteOptions = ((config as any)?.options ?? {}) as Record; const pageOptions = ((article.frontmatter as any)?.site ?? undefined) as | Record diff --git a/app/components/PageHeaderHistory.tsx b/app/components/PageHeaderHistory.tsx index 4bbd597a7..f477b7377 100644 --- a/app/components/PageHeaderHistory.tsx +++ b/app/components/PageHeaderHistory.tsx @@ -55,8 +55,7 @@ function relativeTime(iso: string, now: number) { const COPY = "text-[0.85rem]"; /** - * "Last changed" page-header control that expands an inline changelog, - * mirroring the quantecon-book-theme header. + * "Last changed" page-header control that expands an inline changelog. * * Layout: the root is `display: contents`, so the trigger and the panel become * flex items of the header's author row rather than nesting inside a box. The @@ -70,8 +69,8 @@ const COPY = "text-[0.85rem]"; * 1. `site.git_metadata` in the page frontmatter (manual override, and how * the visual fixture pins deterministic data) -- a declared template * option, so it arrives as a string holding a YAML block (template - * options are scalar-only; see app/i18n.ts) or, from older content, as - * an object; then + * options are scalar-only; see app/i18n.ts), though a real object is + * accepted too; then * 2. `mdast.data.git_metadata` injected at build time by * plugins/git-metadata.mjs. * @@ -148,8 +147,8 @@ export function PageHeaderHistory({ alignEnd = true }: { alignEnd?: boolean } = return (
{ if (event.key === "Escape" && open) { setOpen(false); diff --git a/app/components/ProjectFrontmatter.tsx b/app/components/ProjectFrontmatter.tsx index 9db6133c2..fc018a31f 100644 --- a/app/components/ProjectFrontmatter.tsx +++ b/app/components/ProjectFrontmatter.tsx @@ -47,7 +47,7 @@ export function ProjectFrontmatter({ pageTitle?: string; authors?: Contributor[]; affiliations?: Affiliation[]; - /** Translators of this page's edition (#143); nothing renders when empty. */ + /** Translators of this page's edition; nothing renders when empty. */ translators?: Person[]; /** Label introducing the translators, in the edition's language; '' omits it. */ translatorsLabel?: string; @@ -60,8 +60,8 @@ export function ProjectFrontmatter({ className={classNames( // When the changelog is open its panel is the last thing in this // block, and the bottom padding is dropped so the blue divider becomes - // the panel's own bottom edge (the tidier "integrated" look of the - // Sphinx book-theme header, keeping the blue on the bottom). + // the panel's own bottom edge: the panel reads as part of the header, + // with the blue still on the bottom. `col-body border-b-[5px] border-b-qeborder-blue space-y-1 pb-4 has-[[data-qe-history-panel]]:pb-0`, className @@ -119,9 +119,8 @@ export function ProjectFrontmatter({ }, '')}
)} - {/* Translators (#143). Same row as the "Last changed" control, at the - end, as the book theme places them since v0.22.0 -- a fourth - stacked header line read as clutter there. This block takes the + {/* Translators: same row as the "Last changed" control, at the end -- + an extra stacked header line reads as clutter. This block takes the `ms-auto` and the history control sits beside it; on narrow viewports the row wraps. Distinct class from the authors line and no `rel="author"` on the links, so the two are never confused. A diff --git a/app/components/SiteFooter.tsx b/app/components/SiteFooter.tsx index e7a0bf97a..065573edc 100644 --- a/app/components/SiteFooter.tsx +++ b/app/components/SiteFooter.tsx @@ -13,9 +13,9 @@ export function SiteFooter({ content, className }: { content: GenericParent; cla )} >
- {/* col-body, not col-screen: the Sphinx footer is content-column width, and - the 5px rule above is already col-body -- col-screen here left the text - running wider than its own rule. */} + {/* col-body, not col-screen: the footer text keeps to the content column, + the same width as the 5px rule above -- col-screen would run the text + wider than its own rule. */}
); diff --git a/app/components/toolbar/LanguageSwitcher.tsx b/app/components/toolbar/LanguageSwitcher.tsx index adb12d38a..8233afa4f 100644 --- a/app/components/toolbar/LanguageSwitcher.tsx +++ b/app/components/toolbar/LanguageSwitcher.tsx @@ -10,10 +10,9 @@ import { Tooltip } from './Tooltip'; /** * Globe-icon dropdown that switches between the translated editions of a - * site (Phase 4, #90), the MyST port of the book theme's language switcher. - * Each entry links to the *same page* in the other edition -- that edition's - * site root plus this page's path -- and the entry whose code matches - * `current_language` is marked as the current one. + * site. Each entry links to the *same page* in the other edition -- that + * edition's site root plus this page's path -- and the entry whose code + * matches `current_language` is marked as the current one. * * Renders nothing unless `languages` lists two or more editions; a single * edition has nothing to switch to. diff --git a/app/components/toolbar/Toolbar.tsx b/app/components/toolbar/Toolbar.tsx index d486307eb..99fedb979 100644 --- a/app/components/toolbar/Toolbar.tsx +++ b/app/components/toolbar/Toolbar.tsx @@ -34,8 +34,8 @@ export function Toolbar() { edge. Keep the tighter spacing for that band when adding controls here. `gap-x-*`, not `space-x-*`: the latter is a physical `margin-left` on - every item after the first, which a right-to-left edition (#91) does - not mirror, so the first two icons touch and the margin lands on the + every item after the first, which a right-to-left edition does not + mirror, so the first two icons touch and the margin lands on the outer edge. `gap` is direction-neutral. An empty
  • still takes a gap, hence the `empty:hidden` on the two slots that may render nothing. */} @@ -84,13 +84,12 @@ export function Toolbar() {
  • - {/* Language switcher (#90): far end of the toolbar, as in the book - theme, and at every width -- it is the one action a reader of a - translated edition reaches for, so it stays out of the overflow - menu. Renders nothing unless two or more editions are configured, - and `empty:hidden` keeps the wrapper from taking a gap when it does - not -- every single-edition site would otherwise carry dead space - here. */} + {/* Language switcher: far end of the toolbar, and at every width -- it + is the one action a reader of a translated edition reaches for, so + it stays out of the overflow menu. Renders nothing unless two or + more editions are configured, and `empty:hidden` keeps the wrapper + from taking a gap when it does not -- every single-edition site + would otherwise carry dead space here. */}
  • diff --git a/app/components/toolbar/launchUrls.ts b/app/components/toolbar/launchUrls.ts index 667ec00d2..91a51ed9b 100644 --- a/app/components/toolbar/launchUrls.ts +++ b/app/components/toolbar/launchUrls.ts @@ -1,17 +1,15 @@ // Pure URL/path builders for the notebook launcher. // // Kept free of React so the logic can be unit-tested in isolation -// (see tests/unit/launch-urls.test.mjs). This is the MyST-theme port of the -// book-theme `launch.py` URL construction (`nb_path_to_notebooks`, -// `path_to_docs` stripping, branch/repo handling). Defaults reproduce the -// historical hardcoded behaviour, so projects that set no config are unchanged. +// (see tests/unit/launch-urls.test.mjs). Ported from the URL construction in +// quantecon-book-theme's `launch.py` (`nb_path_to_notebooks`, `path_to_docs`). export interface LaunchConfig { repoUrl?: string; // launch_repo_url — explicit notebook repo, overrides the derived one repoSuffix?: string; // launch_repo_suffix — appended to the source repo (default ".notebooks") branch?: string; // launch_branch — notebook repo branch (default "main") - notebooksPath?: string; // launch_notebooks_path — subdir within the notebook repo (book-theme nb_path_to_notebooks) - sourcePath?: string; // launch_source_path — prefix stripped from the page path (book-theme path_to_docs) + notebooksPath?: string; // launch_notebooks_path — subdir within the notebook repo + sourcePath?: string; // launch_source_path — prefix stripped from the page path } export const DEFAULT_REPO_SUFFIX = '.notebooks'; @@ -52,13 +50,13 @@ export function notebookRelPath(location: string, config: LaunchConfig = {}): st // Strip leading slash and the trailing source extension only (not every dot). let path = location.replace(/^\/+/, '').replace(/\.[^/.]+$/, ''); - // Strip the source_path (path_to_docs) prefix if the page lives under it. + // Strip the source_path prefix if the page lives under it. const sourcePath = trimSlashes(config.sourcePath ?? ''); if (sourcePath && (path === sourcePath || path.startsWith(`${sourcePath}/`))) { path = trimSlashes(path.slice(sourcePath.length)); } - // Prepend the notebooks_path (nb_path_to_notebooks) subdir. + // Prepend the notebooks_path subdir. const notebooksPath = trimSlashes(config.notebooksPath ?? ''); const prefix = notebooksPath ? `${notebooksPath}/` : ''; return `${prefix}${path}.ipynb`; diff --git a/app/i18n.ts b/app/i18n.ts index a1426fe15..eb9dea247 100644 --- a/app/i18n.ts +++ b/app/i18n.ts @@ -1,8 +1,8 @@ /** - * Multilingual editions: the language switcher and `hreflang` alternates - * (Phase 4, #90), document direction (Phase 5, #91) and translator credit - * (#143). Pure TypeScript with no React, so tests/unit/i18n.test.mjs runs it - * under `node --test` with type stripping, the way launchUrls.ts is tested. + * Multilingual editions: the language switcher and `hreflang` alternates, + * document direction and translator credit. Pure TypeScript with no React, so + * tests/unit/i18n.test.mjs runs it under `node --test` with type stripping, + * the way launchUrls.ts is tested. * * WHY THE LISTS ARE STRINGS * @@ -22,10 +22,9 @@ * page's `site:` frontmatter goes through the same validation, so the same * form applies there. * - * The shapes and the resolution rules are quantecon-book-theme's + * The shapes and the resolution rules are ported from quantecon-book-theme * (`_process_languages`, `_normalise_people`, `_resolve_people`, - * `_resolve_label` in its __init__.py), so a translated edition configures the - * two themes with the same values. + * `_resolve_label` in its __init__.py). */ import { parse as parseYaml } from 'yaml'; @@ -77,7 +76,7 @@ function asString(value: unknown): string { /** * The languages to offer. Entries need all of `code`, `name` and `url`; * trailing slashes come off the URL so page paths join cleanly. Fewer than two - * valid entries means no switcher and no alternates, as in the book theme. + * valid entries means no switcher and no alternates. */ export function normaliseLanguages(raw: unknown): Language[] { const value = parseStructured(raw); diff --git a/app/links.ts b/app/links.ts index 5b1f19b85..9bd2efbd7 100644 --- a/app/links.ts +++ b/app/links.ts @@ -21,8 +21,8 @@ import ptSerif700ItalicCss from '@fontsource/pt-serif/700-italic.css'; * blocked the maths markup still renders but is completely unstyled — * fractions, radicals and matrices collapse into run-together text — so * the lectures become unreadable exactly where they matter most. - * 2. The critical path. It was a render-blocking stylesheet on a third - * origin, so first paint waited on a DNS lookup and TLS handshake. + * 2. The critical path. Upstream's is a render-blocking stylesheet on a + * third origin, so first paint waits on a DNS lookup and TLS handshake. * * Remix fingerprints this import and emits it, plus the 60 font files it * references, into `public/build/_assets/` — served at `/myst_assets_folder/` @@ -41,16 +41,16 @@ export const KatexCSS: HtmlLinkDescriptor = { }; /** - * Self-hosted Source Sans 3 (variable), replacing the - * `@import url('https://fonts.googleapis.com/css2?family=Source+Sans+3…')` that - * used to open `styles/app.css`. + * Self-hosted Source Sans 3 (variable), rather than an + * `@import url('https://fonts.googleapis.com/css2?family=Source+Sans+3…')` at + * the top of `styles/app.css`. * * Same two reasons as KaTeX above, both sharper here. Google Fonts is blocked * in mainland China, and this is the *body* font on every page rather than the * maths on some of them. And an `@import` is the worst shape a critical-path * request can have: it is discovered only once `app.css` has downloaded and - * parsed, so it cannot be preloaded, and the chain ran app.css → Google's CSS → - * gstatic woff2 across two extra origins. + * parsed, so it cannot be preloaded, and the chain would run app.css → + * Google's CSS → gstatic woff2 across two extra origins. * * The import lives here rather than in `styles/app.css` because Tailwind does * not rebase `url()` inside an `@import`ed stylesheet — the font paths would be @@ -74,17 +74,15 @@ export const SourceSans3CSS: HtmlLinkDescriptor[] = [ ]; /** - * Self-hosted PT Serif, the heading face of the Sphinx lecture builds - * (python-programming.quantecon.org sets `h1,h2,h3` in `"PT Serif",serif`). - * `styles/quantecon.css` applies it to `.article` headings here. + * Self-hosted PT Serif, the lecture heading face: `styles/quantecon.css` + * applies it to the `.article` h1-h3 headings. * * Self-hosted for the same reasons as Source Sans 3 above, and routed through * the same Remix/esbuild pipeline. The static (non-variable) package: PT Serif * only ships 400 and 700. Four stylesheets because the package splits every * weight/style pair; each is a handful of `@font-face` rules and the browser - * only downloads the faces a page actually uses. The Sphinx build asks for - * weight 900 on headings, which resolves to the 700 face -- the same face - * these files provide. + * only downloads the faces a page actually uses. Heading weights from 600 up + * resolve to the 700 face these files provide. */ export const PTSerifCSS: HtmlLinkDescriptor[] = [ { rel: 'stylesheet', href: ptSerif400Css }, diff --git a/app/renderers.tsx b/app/renderers.tsx index dd9cca95b..55a6ee582 100644 --- a/app/renderers.tsx +++ b/app/renderers.tsx @@ -9,7 +9,7 @@ import { OUTPUT_RENDERERS } from '@myst-theme/jupyter'; * (paren | parens) for Pandoc fancy_lists markers such as `a.`, `iv.`, `(i)`. * * myst-to-react's `list` renderer drops both fields, so `(a)` / `(i)` lists - * render with decimal markers (#100). This override maps `style` to the + * render with decimal markers. This override maps `style` to the * `
      ` attribute and exposes `delimiter` as a `delimiter-paren` / * `delimiter-parens` class consumed by styles/lists.css — the same hook the * fork's `myst-to-html` emits, so one CSS spec serves both renderers. @@ -68,10 +68,9 @@ export const LIST_RENDERERS: NodeRenderers = { }; /** - * Collapsible stderr (Phase 6, #92). The Sphinx build's `stderr-warnings.js` - * folds a cell's stderr streams behind a "Code warnings" button after page - * load; upstream @myst-theme/jupyter renders them as a plain `
      `. Same fold, done at render: a stderr stream
      + * Collapsible stderr. A cell's stderr streams fold behind a "Code warnings"
      + * button, where upstream @myst-theme/jupyter renders them as a plain
      + * `
      `. The fold is done at render: a stderr stream
        * `output` node is wrapped in a native `
      `, closed by default, so it * works in the server-rendered HTML with no script and no DOM surgery. Every * other output goes to upstream's renderer untouched. Styled by the diff --git a/app/revalidate.ts b/app/revalidate.ts index f0f594029..58b996e3e 100644 --- a/app/revalidate.ts +++ b/app/revalidate.ts @@ -2,7 +2,7 @@ import type { ShouldRevalidateFunction } from '@remix-run/react'; /** * `shouldRevalidate` for the three route modules that render UI (`root`, - * `routes/$`, `routes/_index`) -- #186. + * `routes/$`, `routes/_index`). * * A static build (`myst build --html`) still hydrates a live data router, but * its loaders are only servable by a running Remix server: on a static host a @@ -15,8 +15,8 @@ import type { ShouldRevalidateFunction } from '@remix-run/react'; * * These three loaders are pure functions of pathname and search, so declining * to revalidate when neither has changed is correct in `app` mode too, not - * merely a patch for static builds: a repeated click on the active link no - * longer refetches. All three modules must export it -- patching `routes/$` + * merely a patch for static builds: a repeated click on the active link does + * not refetch. All three modules must export it -- patching `routes/$` * alone leaves `?_data=root` firing, which silently replaces the root loader * data (`MODE`, `BASE_URL`, `theme`) with an HTML string. * diff --git a/app/root.tsx b/app/root.tsx index 38731c156..a40bce39f 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -26,14 +26,14 @@ import { htmlDir, htmlLang } from './i18n'; import type { TemplateOptions } from './types'; export { AppErrorBoundary as ErrorBoundary } from '@myst-theme/site'; // Never re-run the loader on a navigation that changes neither pathname nor -// search (Back off an in-page anchor on a static build) -- #186. +// search (Back off an in-page anchor on a static build). export { shouldRevalidate } from '~/revalidate'; const RENDERERS: NodeRenderers = mergeRenderers([ defaultRenderers, JUPYTER_RENDERERS, LIST_RENDERERS, - // After JUPYTER_RENDERERS: wraps upstream's `output` renderer (#92). + // After JUPYTER_RENDERERS: wraps upstream's `output` renderer. STDERR_RENDERERS, ]); @@ -56,8 +56,8 @@ export const meta: V2_MetaFunction = ({ data }) => { * unstyled frame arrives ~200ms AFTER first paint, when React hydration fails * (minified #418/#423) and the recovery client render re-patches whatever * diverged between server and client markup. A divergence in the makes - * that pass re-create head nodes (#126 measured it re-inserting a missing - *