diff --git a/.github/workflows/myst.yml b/.github/workflows/myst.yml index 39a28be07..c6cd09d18 100644 --- a/.github/workflows/myst.yml +++ b/.github/workflows/myst.yml @@ -32,8 +32,16 @@ jobs: # The PDF builds compile under LuaLaTeX and rely on # \DocumentMetadata, which the LaTeX kernel only ships with - # TeX Live 2022+. Ubuntu's apt packages still ship 2023, so - # install TeX Live 2025 from upstream via zauguin's action. + # TeX Live 2022+. Ubuntu's apt packages lag well behind, so + # install current TeX Live from upstream via zauguin's action. + # + # NOTE: `repository` pins the live tlnet mirror, which always + # serves the *current* TeX Live release — so texlive_version must + # match it. When TeX Live rolls over each spring, tlnet switches + # to the new year, the old version pin stops matching, and + # renamed/removed packages fail `tlmgr install` (this exact + # combination broke the 2025→2026 rollover). At rollover: bump + # texlive_version and re-check the package list for renames. # # Package list rationale: the *recommended* collections cover # nearly everything we need (geometry, hyperref, koma-script, @@ -43,10 +51,14 @@ jobs: # those collections. We deliberately avoid collection-latexextra # / collection-fontsextra (~2,700 extra packages, ~12 min, and # one of them tripped up format-file generation in TL 2025). - - name: Install TeX Live 2025 + # + # TL 2026 renames: pdfmanagement-testphase -> pdfmanagement, + # sourceserifpro -> sourceserif (ships a sourceserifpro.sty + # compatibility alias, so template.tex is unchanged). + - name: Install TeX Live 2026 uses: zauguin/install-texlive@v4 with: - texlive_version: 2025 + texlive_version: 2026 # Pin a known-reliable US CTAN mirror. zauguin's default # mirror probe has been failing intermittently with # "No mirror available"; hardcoding RIT bypasses it. @@ -64,8 +76,8 @@ jobs: framed iftex imakeidx - pdfmanagement-testphase - sourceserifpro + pdfmanagement + sourceserif tagpdf textpos titlesec diff --git a/_latex-template/template.tex b/_latex-template/template.tex index 9f817bb74..b51e9fc76 100644 --- a/_latex-template/template.tex +++ b/_latex-template/template.tex @@ -181,6 +181,16 @@ \snaporigincludegraphics[height=#1\baselineskip]{#2}% }% } +% myst-to-tex closes every image node with a blank line — a forced \par — +% which would break the paragraph after each inline image. The latex-shims +% plugin brackets inline images with \snapinlineopen/\snapinlineclose: +% inside the pair \par is a no-op, so the serializer's blank line is inert, +% and \unskip drops the stray inter-word glue from the newline the +% serializer emits right after \includegraphics (the following text node +% carries its own leading space when one belongs there). A real paragraph +% end is serialized after \snapinlineclose, so it still breaks normally. +\newcommand{\snapinlineopen}{\begingroup\let\par\relax} +\newcommand{\snapinlineclose}{\unskip\endgroup} \makeatletter \def\snap@inline@a{width=0.001\linewidth}% image-inline -> 1.5x \def\snap@inline@b{width=0.002\linewidth}% image-inline-tall -> 2.4x diff --git a/_support/plugins/latex-shims.mjs b/_support/plugins/latex-shims.mjs index e378cbe05..8f6c0127e 100644 --- a/_support/plugins/latex-shims.mjs +++ b/_support/plugins/latex-shims.mjs @@ -380,6 +380,38 @@ const latexShimsTransform = { imageNode.width = normalizeWidth(imageNode.width); } }); + + // 6. Keep inline images inline. myst-to-tex closes EVERY image node + // with a blank line (a forced \par in TeX), so an inline image + // mid-sentence would split its paragraph and put a line break + // after the image. We can't stop the serializer from emitting the + // blank line without forking myst-to-tex, but we can defuse it: + // bracket each inline image with raw-TeX siblings + // (\snapinlineopen / \snapinlineclose, defined in template.tex) + // that scope a no-op \par around the image. A genuine paragraph + // end still breaks normally because it is serialized after the + // closing marker. Nodes are tagged so re-runs of this transform + // (one per export pipeline) don't wrap twice. + walkAll(tree, (node) => { + if (!Array.isArray(node.children)) return; + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + if (child?.type !== 'image' || child._snapInlineWrapped) continue; + if (inlineSentinel(child) == null) continue; + child._snapInlineWrapped = true; + node.children.splice(i, 0, { + type: 'raw', + lang: 'tex', + tex: '\\snapinlineopen{}', + }); + node.children.splice(i + 2, 0, { + type: 'raw', + lang: 'tex', + tex: '\\snapinlineclose{}', + }); + i += 2; + } + }); }, };