Skip to content
Merged
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
1 change: 1 addition & 0 deletions news/changelog-1.11.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ All changes included in 1.11:
### `pdf`

- ([#14982](https://github.com/quarto-dev/quarto-cli/issues/14982)): Fix a footnote in document metadata, such as `title` or `author`, making PDF and Typst output shift heading levels, which turned the first level-1 heading into the document title.
- ([#15027](https://github.com/quarto-dev/quarto-cli/issues/15027)): Fix PDF rendering with regional German variants such as `lang: de-CH` failing when German hyphenation patterns are not installed, instead of installing them automatically.

### `revealjs`

Expand Down
123 changes: 101 additions & 22 deletions src/command/render/latexmk/parse-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,36 +156,115 @@ export function findPdfAccessibilityWarnings(
return result;
}

// Finds missing hyphenation files (these appear as warnings in the log file)
export function findMissingHyphenationFiles(logText: string) {
//ngerman gets special cased
const filterLang = (lang: string) => {
// It seems some languages have no hyphenation files, so we just filter them out
// e.g. `lang: zh` has no hyphenation files
// https://github.com/quarto-dev/quarto-cli/issues/10291
const noHyphen = ["chinese-hans", "chinese"];
if (noHyphen.includes(lang)) {
return;
}
// Hyphenation pattern names (in language.dat) whose package is not
// hyphen-<name>, according to `execute AddHyphen` in texlive.tlpdb
const kHyphenationPackageNames: Record<string, string> = {
ngerman: "german",
swissgerman: "german",
pinyin: "chinese",
ukenglish: "english",
british: "english",
usenglishmax: "english",
bokmal: "norwegian",
norsk: "norwegian",
nynorsk: "norwegian",
schoolfinnish: "finnish",
monogreek: "greek",
ibycus: "ancientgreek",
classiclatin: "latin",
liturgicallatin: "latin",
mongolianlmc: "mongolian",
serbianc: "serbian",
// older names (synonyms in texlive.tlpdb)
amharic: "ethiopic",
geez: "ethiopic",
persian: "farsi",
patois: "french",
francais: "french",
polygreek: "greek",
portuges: "portuguese",
slovene: "slovenian",
espanol: "spanish",
// all of these are in the hyphen-indic package
assamese: "indic",
bengali: "indic",
gujarati: "indic",
hindi: "indic",
kannada: "indic",
malayalam: "indic",
marathi: "indic",
oriya: "indic",
pali: "indic",
panjabi: "indic",
tamil: "indic",
telugu: "indic",
};

// Maps a babel language name to the TeX Live package providing its
// hyphenation patterns (undefined when there is no such package)
function hyphenationPackageForLanguage(lang: string) {
// Names in language.dat may be in mixed case (e.g. UKenglish)
lang = lang.toLowerCase();

// It seems some languages have no hyphenation files, so we just filter them out
// e.g. `lang: zh` has no hyphenation files
// https://github.com/quarto-dev/quarto-cli/issues/10291
const noHyphen = ["chinese-hans", "chinese"];
if (noHyphen.includes(lang)) {
return;
}

// NOTE Although the names of the corresponding lfd files match those in this list,
// there are some exceptions, particularly in German and Serbian. So, ngerman is
// called here german, which is the name in the CLDR and, actually, the most logical.
//
// See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf
const name = Object.hasOwn(kHyphenationPackageNames, lang)
? kHyphenationPackageNames[lang]
: lang;
const pkg = `hyphen-${name}`;
// The name comes from the log and is passed to tlmgr (through a .bat file
// on Windows), so only keep what looks like a package name (all
// hyphenation names in texlive.tlpdb are lowercase letters, digits and hyphens)
if (/^hyphen-[a-z0-9-]+$/.test(pkg)) {
return pkg;
}
}

// NOTE Although the names of the corresponding lfd files match those in this list,
// there are some exceptions, particularly in German and Serbian. So, ngerman is
// called here german, which is the name in the CLDR and, actually, the most logical.
//
// See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf
if (lang === "ngerman") {
return "hyphen-german";
// babel raises a fatal error (rather than the warning handled by
// findMissingHyphenationFiles) when a language is referenced but has neither
// hyphenation patterns in the format nor a dialect fallback. For instance,
// babel-german checks `\iflanguage{ngerman}` at begin document, which fails for
// `lang: de-CH` or `lang: de-AT` when hyphen-german is not installed:
// ! Package babel Error: You haven't defined the language 'ngerman' yet.
// https://github.com/rstudio/tinytex/issues/515
// Older babel versions use `ngerman' or no quotes at all.
const kBabelUndefinedLanguageRegex =
/Package babel Error: You haven't defined the language [`']?([^`'\s]+)[`']? yet/g;

// Finds hyphenation packages whose absence makes the compilation fail
// (to be installed directly by name, as opposed to the file search terms
// returned by findMissingFontsAndPackages)
export function findMissingHyphenationPackages(logText: string): string[] {
const packages: string[] = [];
for (const match of logText.matchAll(kBabelUndefinedLanguageRegex)) {
const pkg = hyphenationPackageForLanguage(match[1]);
if (pkg) {
packages.push(pkg);
}
return `hyphen-${lang.toLowerCase()}`;
};
}
return ld.uniq(packages);
}

// Finds missing hyphenation files (these appear as warnings in the log file)
export function findMissingHyphenationFiles(logText: string) {
const babelWarningRegex = /^Package babel Warning:/m;
const hasWarning = logText.match(babelWarningRegex);
if (hasWarning) {
const languageRegex = /^\(babel\).* language [`'](\S+)[`'].*$/m;
const languageMatch = logText.match(languageRegex);
if (languageMatch) {
return filterLang(languageMatch[1]);
return hyphenationPackageForLanguage(languageMatch[1]);
}
}

Expand All @@ -196,7 +275,7 @@ export function findMissingHyphenationFiles(logText: string) {
if (match) {
const language = match[1];
if (language) {
return filterLang(language);
return hyphenationPackageForLanguage(language);
}
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/command/render/latexmk/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import {
findLatexError,
findMissingFontsAndPackages,
findMissingHyphenationFiles,
findMissingHyphenationPackages,
findPdfAccessibilityWarnings,
kMissingFontLog,
needsRecompilation,
} from "./parse-error.ts";
import { error, info, warning } from "../../../deno_ral/log.ts";
import { logProgress } from "../../../core/log.ts";
import { isWindows } from "../../../deno_ral/platform.ts";
import * as ld from "../../../core/lodash.ts";

export async function generatePdf(mkOptions: LatexmkOptions): Promise<string> {
if (!mkOptions.quiet) {
Expand Down Expand Up @@ -470,8 +472,15 @@ async function findAndInstallPackages(
const logText = Deno.readTextFileSync(logFile);

const searchTerms = findMissingFontsAndPackages(logText, dirname(logFile));
if (searchTerms.length > 0) {
const packages = await pkgMgr.searchPackages(searchTerms);
// Packages known by name (no tlmgr file search needed)
const namedPackages = findMissingHyphenationPackages(logText);
if (searchTerms.length > 0 || namedPackages.length > 0) {
const packages = ld.uniq([
...namedPackages,
...(searchTerms.length > 0
? await pkgMgr.searchPackages(searchTerms)
: []),
]);
if (packages.length > 0) {
const packagesInstalled = await pkgMgr.installPackages(
packages,
Expand Down
28 changes: 28 additions & 0 deletions tests/docs/smoke-all/2026/10/12/15027.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: "Swiss German hyphenation auto-install (#15027)"
format: pdf
lang: de-CH
_quarto:
tests:
pdf: null
---

**Only remove packages on CI**

```{r}
#| eval: !expr isTRUE(as.logical(Sys.getenv("CI", "false")))
#| include: false
#| messages: "NA"

# Remove the German babel and hyphenation packages to test auto-installation.
# Without hyphen-german, babel fails with a fatal error for `lang: de-CH`:
# "You haven't defined the language 'ngerman' yet."
for (pkg in c("hyphen-german", "babel-german")) {
if (tinytex::check_installed(pkg)) {
message("Removing '", pkg, "' package for the render")
tinytex::tlmgr_remove(pkg)
}
}
```

Dieses Dokument prüft, dass Quarto `babel-german` und `hyphen-german` automatisch installiert, wenn `lang: de-CH` verwendet wird (siehe auch rstudio/tinytex#515).
Loading
Loading