Found in DrDrij's design review of the v2.5.0 deploy (review item 2), and reproduced end to end against the reviewed deployment at https://6a9b93b9108dd4beb6bebadc--epic-agnesi-957267.netlify.app. On the intro page's {tableofcontents} block, every lecture entry renders as 1 About These Lectures — enumerator, space, no period. The same enumerator, for the same page, renders as 1. About These Lectures everywhere else the theme puts one on screen.
Both strings are in the same served document. The in-content list emits <a class="hover-link" href="/about-py">1 About These Lectures</a>; the contents drawer, earlier in the same HTML, emits <a href="/about-py">1. <!-- -->About These Lectures</a>. (The hover-link / link class split visible in that first snippet is the subject of the content-links item from the same review, not this one.)
This is an internal inconsistency before it is a Sphinx question
The theme hardcodes ${enumerator}. — with the period — in three of its own components. The {tableofcontents} block is the one place on the page where the string is not built by the theme at all, and it is the only one without the period.
| Where the enumerator is rendered |
Site |
Separator |
Built by |
| Contents drawer |
app/components/ContentsSidebar.tsx:29, :34 |
. |
theme |
Page <h1> |
app/components/PageContent.tsx:73 |
. |
theme |
| "On this page" outline |
app/components/Outline.tsx:64 |
. |
theme |
{tableofcontents} block |
none — baked into a text node at build time |
space |
mystmd |
Outline.tsx:64 is cited here only for the separator it uses. The line reads {pageEnumerator ? \${pageEnumerator}.${i + 1}. ${h.title}` : h.title}, and the ${i + 1}half — synthesising the sub-number from the loop index rather than from each heading's own enumerator — is separately unsound for any page withh3s. That is the subject of the outline/scroll-spy item from the same review; it does not weaken the separator evidence, and the two issues should not be read as disagreeing about that line. If the outline fix deletes or rewrites that line, the table above still holds — the drawer and
` sites carry the argument on their own.
So the period is the right value on consistency grounds alone. Sphinx renders it the same way (1. About These Lectures inside the anchor, measured below), which makes it a safe default rather than the reason for it.
Measured against the Sphinx build
Fetched from https://python-programming.quantecon.org/intro.html (26 lecture entries, 6 parts) and compared with the rendered DOM of the reviewed deployment. Three differences, one of which the designer reported.
| Characteristic |
Sphinx intro.html |
This theme, v2.5.0 deploy |
| Enumerator separator inside the anchor |
1. About These Lectures (line 389) |
1 About These Lectures |
| Part title markup |
<p aria-level="2" class="caption" role="heading"> (line 387) |
a bare <li> text node with a sibling <ul> |
role="heading" elements on the page |
12 |
0 |
| List element |
<ul>, one per part, each inside <div class="toctree-wrapper compound"> |
one <ul>, parts nested as <li> + inner <ul> |
| Numbering across part boundaries |
continuous — 8. then 9. (lines 396, 402) |
continuous — same |
| The current page listed in its own contents |
no (href="intro.html" appears 0 times in the toctree) |
yes, first — <li><a class="link" href="/">Python Programming for Economics and Finance</a></li> |
The theme's rendered structure for the first part, attributes trimmed:
<ul>
<li><a class="link" href="/">Python Programming for Economics and Finance</a></li>
<li>Introduction to Python<ul>
<li><span data-state="closed"><a class="hover-link" href="/about-py">1 About These Lectures</a></span></li>
<li><span data-state="closed"><a class="hover-link" href="/getting-started">2 Getting Started</a></span></li>
<!-- … remaining entries and closing tags elided -->
The equivalent from Sphinx:
<div class="toctree-wrapper compound">
<p aria-level="2" class="caption" role="heading"><span class="caption-text">Introduction to Python</span></p>
<ul>
<li class="toctree-l1"><a class="reference internal" href="about_py.html">1. About These Lectures</a></li>
<li class="toctree-l1"><a class="reference internal" href="getting_started.html">2. Getting Started</a></li>
Root cause: the string is frozen at build time, one layer below the theme
mystmd's buildTocTransform concatenates the enumerator into the link's text node with a single space and no period, so by the time the theme renders the page the whole label is one immutable string. Two sites, in the vendored myst-transforms@1.3.49 in this repo's node_modules:
node_modules/myst-transforms/dist/toc.js:49, inside transformPage() — the toc:project path that {tableofcontents} resolves to:
value: `${enumerator ? `${enumerator} ` : ''}${title}`,
node_modules/myst-transforms/dist/toc.js:114, inside transformHeading() — the identical defect for the toc:page and toc:section contexts:
value: `${enumerator ? `${enumerator} ` : ''}${toText(children)}`,
{tableofcontents} with no options reaches the first of these: node_modules/myst-directives/dist/toc.js:7 lists tableofcontents among the toc directive's aliases, and line 30 defaults its context to 'project'.
The QuantEcon fork has not diverged here. packages/myst-transforms/src/toc.ts:69 and :132 on QuantEcon/mystmd main are byte-identical to the two lines above once TypeScript annotations are removed — fetched and diffed against the vendored build.
No stylesheet in this repo can repair it. The numeral sits mid-string inside a text node, so no ::before can reach it, and there is no CSS hook on the block either: node_modules/myst-to-react/dist/block.js:7 builds the block's className from className, node.class and node.data.class only, and never surfaces node.data.part, which is where toc:project lives.
The <ol> suggestion, on its merits
The designer asked whether this could be an <ol>. The instinct is right — the list semantics are wrong — but <ol> is the wrong repair, and would make two things worse.
<ol> would number the wrong level. mystmd emits one nested list per part inside a single outer list, and makeList (toc.js:3-5) sets neither ordered nor start. So an outer <ol> numbers the parts 1–6, and an inner <ol> restarts at 1 in each part — where the real numbering runs continuously, 1–8 then 9–12 then 13–16. Nothing in the AST carries the offset that would fix that.
<ol> markers also cannot render the enumerators this project actually produces. With numbering.headings: true the toc:page context yields dotted multi-level enumerators such as 3.4.1, which no CSS list-counter format expresses.
And it would lose information for screen-reader users. The numeral currently sits inside the anchor, so it is part of the link's accessible name — "1. About These Lectures". List markers are generally not announced, so moving the numeral into an <ol> marker removes it from the announcement while leaving it on screen.
What the structure ought to be instead. Sphinx's answer — role="heading" aria-level="2" bolted onto a <p> — is an ARIA patch over non-semantic markup, and this is exactly the kind of place where parity in idea should beat parity in implementation. The part title should be a real heading element, and each part's <ul> should be associated with it, inside a named navigation landmark:
- one
<nav aria-label="Table of contents"> (or aria-labelledby the directive's own argument heading, which the toc directive already accepts) wrapping the whole block;
- each part title as a heading element at the depth that follows the page's own hierarchy, rather than a hardcoded
aria-level="2";
- each part's entries in a
<ul> carrying aria-labelledby pointing at that heading, so the list announces which part it belongs to;
- the numeral kept inside the anchor text, as both builds do today.
That is strictly better than either current build, and it is what makes the block navigable by heading in a screen reader — which is the affordance the designer is actually missing when the flat bullet list reads wrong. Worth noting the theme already does the landmark half correctly in the drawer: <nav aria-labelledby="qe-toc-heading"> with each part title as a styled <p>. The in-content block is behind its own sibling component.
Decision 1 — where the separator fix lands
Option 1 — QuantEcon/mystmd (recommended). Change packages/myst-transforms/src/toc.ts:69 and :132 from ${enumerator} to ${enumerator}. , guarded so an enumerator that already ends in punctuation is not double-punctuated (see Option 3 for why that guard is reachable, not hypothetical). Covers all four contexts, including the two the theme cannot safely reach. Also corrects the search-index text and the myst build --html static output, since both read the same AST. Cost: one PR to the fork, then a pinned-SHA bump in each consuming lecture repo. That bump is one line, not an npm release: .github/workflows/ci.yml on QuantEcon/lecture-python-programming@jb2 installs the fork by git fetch --depth 1 origin 96ee78f550ff2e0410730f328352e98d3e29d338 and npm install -g from source. I verified that for lecture-python-programming only; whether every lecture repo pins the same way is an open question below. It should also be offered upstream at jupyter-book/mystmd — the fork is byte-identical here, so it is a clean upstream candidate — and recorded in UPSTREAM-CANDIDATES.yml.
Option 2 — theme-local block renderer override (stopgap only, and narrower than it first looks). Precedent exists: app/renderers.tsx already overrides list for fancy ordered lists, and mergeRenderers (node_modules/@myst-theme/providers/dist/renderers.js:21-31) does a shallow per-node-type Object.assign, so replacing block's base composes cleanly. Three real costs. First, it must replace the base renderer and therefore runs for every block node on every page — a selector-keyed entry is not available, because unist-util-select cannot match nested attribute paths (matches('block[data.part=toc:project]', node) throws Expected "=" but "." found., run in this repo's node_modules). Second, it is only safe for toc:project and toc:children, where the enumerator can be re-derived from the site manifest that ContentsSidebar.tsx already uses; for toc:page / toc:section there is no manifest entry, and the only remaining move is string surgery on the already-concatenated text. The regex proposed for that during triage, /^((?:\d+|[A-Za-z])(?:\.(?:\d+|[A-Za-z]))*)\s(?=\S)/ → '$1. ', corrupts real content in this corpus — I ran it:
| Input |
Output |
1 About These Lectures |
1. About These Lectures |
3.4.1 A Version with a For Loop |
3.4.1. A Version with a For Loop |
A Version with a For Loop |
A. Version with a For Loop |
A Comment on Indentation |
A. Comment on Indentation |
The last two are real headings — 3.4.1 and 3.4.4 in python_by_example.html — and they are unprefixed exactly when numbering.headings is off, which is the state of this repo's own visual fixture (tests/visual/fixture/myst.yml.in has no numbering: key). Third, and this is the cost most likely to be under-counted: lectures/myst.yml:96 on jb2 pins the theme by release URL, so a theme-only fix also needs a theme release plus a per-repo pin bump. "Theme-local is cheaper" is not true on the release axis; it is only cheaper in that the change lives in a repo this team already ships from.
Option 3 — configuration only, in each lecture repo. Rejected, with a measurement. numbering.title.enumerator: "%s." would work at the mystmd layer: formatHeadingEnumerator (node_modules/myst-transforms/dist/enumerate.js:168) applies prefix.replace(/%s/g, enumerator), and enumerate.js:240 reads numbering.title?.enumerator for the page-title counter. I executed it — toc.js:49 then yields exactly 1. About These Lectures. But the same enumerator flows into the theme's three hardcoded . sites, which append a second period: PageContent.tsx:73 renders 1.. About These Lectures in the <h1>, and the drawer and outline do the same. So this option is unusable today. It is worth recording anyway for two reasons: it is why Option 1's guard is necessary rather than defensive, and it exposes a small latent defect in this repo — the theme's three components should skip their own . when the enumerator already ends in punctuation. That guard is the only part of this issue that lands here, and it is a three-line change.
Option 4 — accept as is and close. The cost is not the missing period on its own; it is that the front door of every migrated lecture site will read differently from the front door of the site it replaces, in the first block below the intro paragraph, on a detail the theme itself gets right three other times on the same page.
Decision 2 — whether the structural half is in scope
Fixing the separator leaves the second and third rows of the comparison table standing, and those are what the <ol> request was reaching for. Both are shaped by the same transform (listFromPages / listItemFromPages, toc.js:9-38), so the same PR can address them, but the AST shape change is materially larger than the two-character separator change and affects every consumer of those ASTs.
- Fold the part-title heading semantics into the same fork change, and land the whole block as described under "What the structure ought to be instead".
- Land the separator alone now — it is two characters and unblocks the reported complaint — and file the structural work separately, in the fork, referencing this issue.
- Leave the structure alone. Defensible only if the accessibility budget is being spent elsewhere; note that the design review already has open accessibility-adjacent items and nobody has summed them.
The third row — the current page appearing first in its own contents list — may not need code at all. intro.md is the first entry of lectures/myst.yml's toc: on jb2, and {tableofcontents} faithfully lists it; Sphinx's toctree excludes the current document by construction. Whether that is a lecture-repo authoring change or a transform change is an open question.
Testing and baselines
No existing visual baseline moves, in either direction. tests/visual/fixture/myst.yml.in has a flat four-file toc: and no numbering: key, and a grep for tableofcontents, numbering and enumerator across tests/visual/ returns nothing — so the current darwin and linux snapshot sets contain zero enumerators and zero in-content TOC.
New coverage therefore means new PNGs in all four directories (desktop-chrome-{darwin,linux}, mobile-chrome-{darwin,linux}), refreshed locally with --update-snapshots=all and on CI via an /update-snapshots comment. Do not get that coverage by adding numbering: to the existing fixture: PageContent.tsx:73 would then prefix every fixture page's <h1> with N. and churn every existing baseline in all four sets. Add a small dedicated fixture with a parts-based toc:, numbering: {titles: true, headings: true, heading_1: false} and an index page carrying {tableofcontents} — the jb2 shape — in the style of the RTL fixture #174 introduces.
Assert the rendered text with a DOM assertion, not the screenshot: #113 records that the 1% pixel-diff budget hides structural changes, and adding a period to a handful of rows is exactly the size of change it hides.
One harness limitation to plan around: this repo's CI installs the upstream CLI (npm install -g mystmd, unpinned, at .github/workflows/ci.yml:40 and :102, preview.yml:51, update-snapshots.yml:67), not the QuantEcon fork. A fork-only fix cannot be regression-tested here until it also lands upstream and a new mystmd npm release ships, or until this repo's workflows switch to the fork. That is an argument for filing upstream in the same pass, not for preferring the theme-local route.
Where this sits
This is a visible regression against the sites the migration replaces, on the intro page of every lecture series, so it matters to the cutover even though it is the most cosmetic defect in the review. The implementation lives in QuantEcon/mystmd, so this issue is the tracking home and the companion implementation issue belongs in the fork, cross-referenced with QuantEcon/mystmd#13 (the counter-abstraction design issue), which is too large a vehicle for a two-line separator change. It is not Phase 3 (#89): nothing here is colour or highlighting. Nothing about it belongs on #92.
It conflicts with nothing on baselines — none of the PNGs in the combined CSS PR's refresh (the #171-first plan) is a TOC fixture, and a JS/AST change moves no existing snapshot. Option 2 would conflict textually with #174, which edits app/root.tsx where RENDERERS is assembled at lines 27-31; Option 1 would not.
Sequencing: this has the longest lead time in the review set despite being the most cosmetic defect, because a fork change plus per-repo SHA bumps is a slower path than any CSS PR. It should start in parallel now rather than queue behind #171 and #174.
Open questions
- Do the other lecture repos pin
QuantEcon/mystmd by SHA the way lecture-python-programming@jb2 does? That number sets the real cost of Option 1, and I verified only the one repo.
- Should the current page list itself in its own
{tableofcontents}? Lecture-repo toc: authoring, or transform behaviour?
- Does the same change want to go upstream to
jupyter-book/mystmd immediately, or sit in the fork until this repo's CI can exercise it?
Next action
Take Decision 1. If it is Option 1, open the two-line PR against QuantEcon/mystmd (packages/myst-transforms/src/toc.ts:69 and :132, guarded), file the matching upstream issue, add the UPSTREAM-CANDIDATES.yml entry, and open the small companion PR here that stops ContentsSidebar.tsx, PageContent.tsx and Outline.tsx double-punctuating an enumerator that already ends in a period.
Found in DrDrij's design review of the v2.5.0 deploy (review item 2), and reproduced end to end against the reviewed deployment at
https://6a9b93b9108dd4beb6bebadc--epic-agnesi-957267.netlify.app. On the intro page's{tableofcontents}block, every lecture entry renders as1 About These Lectures— enumerator, space, no period. The same enumerator, for the same page, renders as1. About These Lectureseverywhere else the theme puts one on screen.Both strings are in the same served document. The in-content list emits
<a class="hover-link" href="/about-py">1 About These Lectures</a>; the contents drawer, earlier in the same HTML, emits<a href="/about-py">1. <!-- -->About These Lectures</a>. (Thehover-link/linkclass split visible in that first snippet is the subject of the content-links item from the same review, not this one.)This is an internal inconsistency before it is a Sphinx question
The theme hardcodes
${enumerator}.— with the period — in three of its own components. The{tableofcontents}block is the one place on the page where the string is not built by the theme at all, and it is the only one without the period.app/components/ContentsSidebar.tsx:29,:34.<h1>app/components/PageContent.tsx:73.app/components/Outline.tsx:64.{tableofcontents}blockOutline.tsx:64is cited here only for the separator it uses. The line reads{pageEnumerator ? \${pageEnumerator}.${i + 1}. ${h.title}` : h.title}, and the${i + 1}half — synthesising the sub-number from the loop index rather than from each heading's own enumerator — is separately unsound for any page withh3s. That is the subject of the outline/scroll-spy item from the same review; it does not weaken the separator evidence, and the two issues should not be read as disagreeing about that line. If the outline fix deletes or rewrites that line, the table above still holds — the drawer and` sites carry the argument on their own.
So the period is the right value on consistency grounds alone. Sphinx renders it the same way (
1. About These Lecturesinside the anchor, measured below), which makes it a safe default rather than the reason for it.Measured against the Sphinx build
Fetched from
https://python-programming.quantecon.org/intro.html(26 lecture entries, 6 parts) and compared with the rendered DOM of the reviewed deployment. Three differences, one of which the designer reported.intro.html1. About These Lectures(line 389)1 About These Lectures<p aria-level="2" class="caption" role="heading">(line 387)<li>text node with a sibling<ul>role="heading"elements on the page<ul>, one per part, each inside<div class="toctree-wrapper compound"><ul>, parts nested as<li>+ inner<ul>8.then9.(lines 396, 402)href="intro.html"appears 0 times in the toctree)<li><a class="link" href="/">Python Programming for Economics and Finance</a></li>The theme's rendered structure for the first part, attributes trimmed:
The equivalent from Sphinx:
Root cause: the string is frozen at build time, one layer below the theme
mystmd's
buildTocTransformconcatenates the enumerator into the link's text node with a single space and no period, so by the time the theme renders the page the whole label is one immutable string. Two sites, in the vendoredmyst-transforms@1.3.49in this repo'snode_modules:node_modules/myst-transforms/dist/toc.js:49, insidetransformPage()— thetoc:projectpath that{tableofcontents}resolves to:node_modules/myst-transforms/dist/toc.js:114, insidetransformHeading()— the identical defect for thetoc:pageandtoc:sectioncontexts:{tableofcontents}with no options reaches the first of these:node_modules/myst-directives/dist/toc.js:7liststableofcontentsamong thetocdirective's aliases, and line 30 defaults its context to'project'.The QuantEcon fork has not diverged here.
packages/myst-transforms/src/toc.ts:69and:132onQuantEcon/mystmdmain are byte-identical to the two lines above once TypeScript annotations are removed — fetched and diffed against the vendored build.No stylesheet in this repo can repair it. The numeral sits mid-string inside a text node, so no
::beforecan reach it, and there is no CSS hook on the block either:node_modules/myst-to-react/dist/block.js:7builds the block's className fromclassName,node.classandnode.data.classonly, and never surfacesnode.data.part, which is wheretoc:projectlives.The
<ol>suggestion, on its meritsThe designer asked whether this could be an
<ol>. The instinct is right — the list semantics are wrong — but<ol>is the wrong repair, and would make two things worse.<ol>would number the wrong level. mystmd emits one nestedlistper part inside a single outer list, andmakeList(toc.js:3-5) sets neitherorderednorstart. So an outer<ol>numbers the parts 1–6, and an inner<ol>restarts at 1 in each part — where the real numbering runs continuously, 1–8 then 9–12 then 13–16. Nothing in the AST carries the offset that would fix that.<ol>markers also cannot render the enumerators this project actually produces. Withnumbering.headings: truethetoc:pagecontext yields dotted multi-level enumerators such as3.4.1, which no CSS list-counter format expresses.And it would lose information for screen-reader users. The numeral currently sits inside the anchor, so it is part of the link's accessible name — "1. About These Lectures". List markers are generally not announced, so moving the numeral into an
<ol>marker removes it from the announcement while leaving it on screen.What the structure ought to be instead. Sphinx's answer —
role="heading" aria-level="2"bolted onto a<p>— is an ARIA patch over non-semantic markup, and this is exactly the kind of place where parity in idea should beat parity in implementation. The part title should be a real heading element, and each part's<ul>should be associated with it, inside a named navigation landmark:<nav aria-label="Table of contents">(oraria-labelledbythe directive's own argument heading, which thetocdirective already accepts) wrapping the whole block;aria-level="2";<ul>carryingaria-labelledbypointing at that heading, so the list announces which part it belongs to;That is strictly better than either current build, and it is what makes the block navigable by heading in a screen reader — which is the affordance the designer is actually missing when the flat bullet list reads wrong. Worth noting the theme already does the landmark half correctly in the drawer:
<nav aria-labelledby="qe-toc-heading">with each part title as a styled<p>. The in-content block is behind its own sibling component.Decision 1 — where the separator fix lands
Option 1 — QuantEcon/mystmd (recommended). Change
packages/myst-transforms/src/toc.ts:69and:132from${enumerator}to${enumerator}., guarded so an enumerator that already ends in punctuation is not double-punctuated (see Option 3 for why that guard is reachable, not hypothetical). Covers all four contexts, including the two the theme cannot safely reach. Also corrects the search-index text and themyst build --htmlstatic output, since both read the same AST. Cost: one PR to the fork, then a pinned-SHA bump in each consuming lecture repo. That bump is one line, not an npm release:.github/workflows/ci.ymlonQuantEcon/lecture-python-programming@jb2installs the fork bygit fetch --depth 1 origin 96ee78f550ff2e0410730f328352e98d3e29d338andnpm install -gfrom source. I verified that forlecture-python-programmingonly; whether every lecture repo pins the same way is an open question below. It should also be offered upstream atjupyter-book/mystmd— the fork is byte-identical here, so it is a clean upstream candidate — and recorded inUPSTREAM-CANDIDATES.yml.Option 2 — theme-local
blockrenderer override (stopgap only, and narrower than it first looks). Precedent exists:app/renderers.tsxalready overrideslistfor fancy ordered lists, andmergeRenderers(node_modules/@myst-theme/providers/dist/renderers.js:21-31) does a shallow per-node-typeObject.assign, so replacingblock's base composes cleanly. Three real costs. First, it must replace the base renderer and therefore runs for everyblocknode on every page — a selector-keyed entry is not available, becauseunist-util-selectcannot match nested attribute paths (matches('block[data.part=toc:project]', node)throwsExpected "=" but "." found., run in this repo'snode_modules). Second, it is only safe fortoc:projectandtoc:children, where the enumerator can be re-derived from the site manifest thatContentsSidebar.tsxalready uses; fortoc:page/toc:sectionthere is no manifest entry, and the only remaining move is string surgery on the already-concatenated text. The regex proposed for that during triage,/^((?:\d+|[A-Za-z])(?:\.(?:\d+|[A-Za-z]))*)\s(?=\S)/→'$1. ', corrupts real content in this corpus — I ran it:1 About These Lectures1. About These Lectures3.4.1 A Version with a For Loop3.4.1. A Version with a For LoopA Version with a For LoopA. Version with a For LoopA Comment on IndentationA. Comment on IndentationThe last two are real headings —
3.4.1and3.4.4inpython_by_example.html— and they are unprefixed exactly whennumbering.headingsis off, which is the state of this repo's own visual fixture (tests/visual/fixture/myst.yml.inhas nonumbering:key). Third, and this is the cost most likely to be under-counted:lectures/myst.yml:96onjb2pins the theme by release URL, so a theme-only fix also needs a theme release plus a per-repo pin bump. "Theme-local is cheaper" is not true on the release axis; it is only cheaper in that the change lives in a repo this team already ships from.Option 3 — configuration only, in each lecture repo. Rejected, with a measurement.
numbering.title.enumerator: "%s."would work at the mystmd layer:formatHeadingEnumerator(node_modules/myst-transforms/dist/enumerate.js:168) appliesprefix.replace(/%s/g, enumerator), andenumerate.js:240readsnumbering.title?.enumeratorfor the page-title counter. I executed it —toc.js:49then yields exactly1. About These Lectures. But the same enumerator flows into the theme's three hardcoded.sites, which append a second period:PageContent.tsx:73renders1.. About These Lecturesin the<h1>, and the drawer and outline do the same. So this option is unusable today. It is worth recording anyway for two reasons: it is why Option 1's guard is necessary rather than defensive, and it exposes a small latent defect in this repo — the theme's three components should skip their own.when the enumerator already ends in punctuation. That guard is the only part of this issue that lands here, and it is a three-line change.Option 4 — accept as is and close. The cost is not the missing period on its own; it is that the front door of every migrated lecture site will read differently from the front door of the site it replaces, in the first block below the intro paragraph, on a detail the theme itself gets right three other times on the same page.
Decision 2 — whether the structural half is in scope
Fixing the separator leaves the second and third rows of the comparison table standing, and those are what the
<ol>request was reaching for. Both are shaped by the same transform (listFromPages/listItemFromPages,toc.js:9-38), so the same PR can address them, but the AST shape change is materially larger than the two-character separator change and affects every consumer of those ASTs.The third row — the current page appearing first in its own contents list — may not need code at all.
intro.mdis the first entry oflectures/myst.yml'stoc:onjb2, and{tableofcontents}faithfully lists it; Sphinx'stoctreeexcludes the current document by construction. Whether that is a lecture-repo authoring change or a transform change is an open question.Testing and baselines
No existing visual baseline moves, in either direction.
tests/visual/fixture/myst.yml.inhas a flat four-filetoc:and nonumbering:key, and a grep fortableofcontents,numberingandenumeratoracrosstests/visual/returns nothing — so the current darwin and linux snapshot sets contain zero enumerators and zero in-content TOC.New coverage therefore means new PNGs in all four directories (
desktop-chrome-{darwin,linux},mobile-chrome-{darwin,linux}), refreshed locally with--update-snapshots=alland on CI via an/update-snapshotscomment. Do not get that coverage by addingnumbering:to the existing fixture:PageContent.tsx:73would then prefix every fixture page's<h1>withN.and churn every existing baseline in all four sets. Add a small dedicated fixture with a parts-basedtoc:,numbering: {titles: true, headings: true, heading_1: false}and an index page carrying{tableofcontents}— thejb2shape — in the style of the RTL fixture #174 introduces.Assert the rendered text with a DOM assertion, not the screenshot: #113 records that the 1% pixel-diff budget hides structural changes, and adding a period to a handful of rows is exactly the size of change it hides.
One harness limitation to plan around: this repo's CI installs the upstream CLI (
npm install -g mystmd, unpinned, at.github/workflows/ci.yml:40and:102,preview.yml:51,update-snapshots.yml:67), not the QuantEcon fork. A fork-only fix cannot be regression-tested here until it also lands upstream and a newmystmdnpm release ships, or until this repo's workflows switch to the fork. That is an argument for filing upstream in the same pass, not for preferring the theme-local route.Where this sits
This is a visible regression against the sites the migration replaces, on the intro page of every lecture series, so it matters to the cutover even though it is the most cosmetic defect in the review. The implementation lives in
QuantEcon/mystmd, so this issue is the tracking home and the companion implementation issue belongs in the fork, cross-referenced withQuantEcon/mystmd#13(the counter-abstraction design issue), which is too large a vehicle for a two-line separator change. It is not Phase 3 (#89): nothing here is colour or highlighting. Nothing about it belongs on #92.It conflicts with nothing on baselines — none of the PNGs in the combined CSS PR's refresh (the #171-first plan) is a TOC fixture, and a JS/AST change moves no existing snapshot. Option 2 would conflict textually with #174, which edits
app/root.tsxwhereRENDERERSis assembled at lines 27-31; Option 1 would not.Sequencing: this has the longest lead time in the review set despite being the most cosmetic defect, because a fork change plus per-repo SHA bumps is a slower path than any CSS PR. It should start in parallel now rather than queue behind #171 and #174.
Open questions
QuantEcon/mystmdby SHA the waylecture-python-programming@jb2does? That number sets the real cost of Option 1, and I verified only the one repo.{tableofcontents}? Lecture-repotoc:authoring, or transform behaviour?jupyter-book/mystmdimmediately, or sit in the fork until this repo's CI can exercise it?Next action
Take Decision 1. If it is Option 1, open the two-line PR against
QuantEcon/mystmd(packages/myst-transforms/src/toc.ts:69and:132, guarded), file the matching upstream issue, add theUPSTREAM-CANDIDATES.ymlentry, and open the small companion PR here that stopsContentsSidebar.tsx,PageContent.tsxandOutline.tsxdouble-punctuating an enumerator that already ends in a period.