Skip to content

Commit

Permalink
Merge pull request #10727 from quarto-dev/feature/brand-yml-html-fonts
Browse files Browse the repository at this point in the history
_brand.yml: fonts in `html`-like formats, font colors, schema updates, etc
  • Loading branch information
cscheid authored Sep 20, 2024
2 parents d847388 + 7bac8f0 commit 1399d2e
Show file tree
Hide file tree
Showing 101 changed files with 14,233 additions and 903 deletions.
3 changes: 1 addition & 2 deletions src/command/render/output-typst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ export function typstPdfOutputRecipe(
const pdfOutput = join(inputDir, inputStem + ".pdf");
const typstOptions: TypstCompileOptions = {
quiet: options.flags?.quiet,
// use recipe that may have been modified, not format which has not
fontPaths: asArray(recipe.format.metadata?.[kFontPaths]) as string[],
fontPaths: asArray(format.metadata?.[kFontPaths]) as string[],
};
if (project?.dir) {
typstOptions.rootDir = project.dir;
Expand Down
19 changes: 0 additions & 19 deletions src/command/render/pandoc-dependencies-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,6 @@ export function readAndInjectDependencies(
});
}

// this should be resolveMetadata returning an object
// like {'output-recipe': metadata}
export function resolveTypstFontPaths(
dependenciesFile: string,
) {
const dependencyJsonStream = Deno.readTextFileSync(dependenciesFile);
const fontPaths: string[] = [];
lines(dependencyJsonStream).forEach((json) => {
if (json) {
const dependency = JSON.parse(json);
if (dependency.type === "typst-font-path") {
const path = dependency?.content?.path;
fontPaths.push(path);
}
}
});
return fontPaths;
}

export function resolveDependencies(
extras: FormatExtras,
inputDir: string,
Expand Down
27 changes: 25 additions & 2 deletions src/command/render/pandoc-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function resolveSassBundles(
extras: FormatExtras,
format: Format,
temp: TempContext,
project?: ProjectContext,
project: ProjectContext,
) {
extras = cloneDeep(extras);

Expand Down Expand Up @@ -286,6 +286,29 @@ async function resolveQuartoSyntaxHighlighting(
if (themeDescriptor) {
// Other variables that need to be injected (if any)
const extraVariables = extras.html?.[kQuartoCssVariables] || [];
for (let i = 0; i < extraVariables.length; ++i) {
// For the same reason as outlined in https://github.com/rstudio/bslib/issues/1104,
// we need to patch the text to include a semicolon inside the declaration
// if it doesn't have one.
// This happens because scss-parser is brittle, and will fail to parse a declaration
// if it doesn't end with a semicolon.
//
// In addition, we know that some our variables come from the output
// of sassCompile which
// - misses the last semicolon
// - emits a :root declaration
// - triggers the scss-parser bug
// So we'll attempt to target the last declaration in the :root
// block specifically and add a semicolon if it doesn't have one.
let variable = extraVariables[i].trim();
if (
variable.endsWith("}") && variable.startsWith(":root") &&
!variable.match(/.*;\s?}$/)
) {
variable = variable.slice(0, -1) + ";}";
extraVariables[i] = variable;
}
}

// The text highlighting CSS variables
const highlightCss = generateThemeCssVars(themeDescriptor.json);
Expand All @@ -308,7 +331,7 @@ async function resolveQuartoSyntaxHighlighting(
// Add this string literal to the rule set, which prevents pandoc
// from inlining this style sheet
// See https://github.com/jgm/pandoc/commit/7c0a80c323f81e6262848bfcfc922301e3f406e0
rules.push(".prevent-inlining { content: '</' }");
rules.push(".prevent-inlining { content: '</'; }");

// Compile the scss
const highlightCssPath = await compileSass(
Expand Down
148 changes: 133 additions & 15 deletions src/command/render/pandoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { basename, dirname, isAbsolute, join } from "../../deno_ral/path.ts";

import { info } from "../../deno_ral/log.ts";

import { existsSync, expandGlobSync } from "fs/mod.ts";
import { ensureDir, existsSync, expandGlobSync } from "fs/mod.ts";

import { stringify } from "yaml/mod.ts";
import { encodeBase64 } from "encoding/base64.ts";
Expand Down Expand Up @@ -139,7 +139,6 @@ import { kDefaultHighlightStyle } from "./constants.ts";
import {
HtmlPostProcessor,
HtmlPostProcessResult,
OutputRecipe,
PandocOptions,
RunPandocResult,
} from "./types.ts";
Expand Down Expand Up @@ -176,7 +175,6 @@ import { resolveAndFormatDate, resolveDate } from "../../core/date.ts";
import { katexPostProcessor } from "../../format/html/format-html-math.ts";
import {
readAndInjectDependencies,
resolveTypstFontPaths,
writeDependencies,
} from "./pandoc-dependencies-html.ts";
import {
Expand Down Expand Up @@ -420,13 +418,13 @@ export async function runPandoc(
);

const extras = await resolveExtras(
options.source,
inputExtras,
options.format,
cwd,
options.libDir,
options.services.temp,
dependenciesFile,
options.recipe,
options.project,
);

Expand Down Expand Up @@ -1283,14 +1281,14 @@ function cleanupPandocMetadata(metadata: Metadata) {
}

async function resolveExtras(
input: string,
extras: FormatExtras, // input format extras (project, format, brand)
format: Format,
inputDir: string,
libDir: string,
temp: TempContext,
dependenciesFile: string,
recipe: OutputRecipe,
project?: ProjectContext,
project: ProjectContext,
) {
// resolve format resources
await writeFormatResources(
Expand Down Expand Up @@ -1346,15 +1344,135 @@ async function resolveExtras(

// perform typst-specific merging
if (isTypstOutput(format.pandoc)) {
extras.postprocessors = extras.postprocessors || [];
extras.postprocessors.push(async () => {
// gw: IMO this could be way more general as resolveMetadata
// returning all metadata found in the file
// then apply output-recipe and any others found using mergeConfigs
// would not be format-specific
const fontPaths = await resolveTypstFontPaths(dependenciesFile);
recipe.format.metadata[kFontPaths] = fontPaths;
});
const brand = await project.resolveBrand(input);
const fontdirs: Set<string> = new Set();
const base_urls = {
google: "https://fonts.googleapis.com/css",
bunny: "https://fonts.bunny.net/css",
};
const ttf_urls = [], woff_urls: Array<string> = [];
if (brand?.data.typography) {
const fonts = brand.data.typography.fonts || [];
for (const font of fonts) {
if (font.source === "file") {
for (const file of font.files || []) {
const path = typeof file === "object" ? file.path : file;
fontdirs.add(dirname(join(brand.brandDir, path)));
}
} else if (font.source === "bunny") {
console.log(
"Font bunny is not yet supported for Typst, skipping",
font.family,
);
} else if (font.source === "google" /* || font.source === "bunny" */) {
let { family, style, weight } = font;
const parts = [family!];
if (style) {
style = Array.isArray(style) ? style : [style];
parts.push(style.join(","));
}
if (weight) {
weight = Array.isArray(weight) ? weight : [weight];
parts.push(weight.join(","));
}
const response = await fetch(
`${base_urls[font.source]}?family=${parts.join(":")}`,
);
const lines = (await response.text()).split("\n");
for (const line of lines) {
const sourcelist = line.match(/^ *src: (.*); *$/);
if (sourcelist) {
const sources = sourcelist[1].split(",").map((s) => s.trim());
const failed_formats = [];
for (const source of sources) {
const match = source.match(
/url\(([^)]*)\) *format\('([^)]*)'\)/,
);
if (match) {
const [_, url, format] = match;
if (["truetype", "opentype"].includes(format)) {
ttf_urls.push(url);
break;
}
// else if (["woff", "woff2"].includes(format)) {
// woff_urls.push(url);
// break;
// }
failed_formats.push(format);
}
}
console.log(
"skipping",
family,
"\nnot currently able to use formats",
failed_formats.join(", "),
);
}
}
}
}
}
if (ttf_urls.length || woff_urls.length) {
const font_cache = join(brand!.projectDir, ".quarto", "typst-font-cache");
const url_to_path = (url: string) => url.replace(/^https?:\/\//, "");
const cached = async (url: string) => {
const path = url_to_path(url);
try {
await Deno.lstat(join(font_cache, path));
return true;
} catch (err) {
if (!(err instanceof Deno.errors.NotFound)) {
throw err;
}
return false;
}
};
const download = async (url: string) => {
const path = url_to_path(url);
await ensureDir(
join(font_cache, dirname(path)),
);

const response = await fetch(url);
const blob = await response.blob();
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
await Deno.writeFile(join(font_cache, path), bytes);
};
const woff2ttf = async (url: string) => {
const path = url_to_path(url);
await Deno.run({ cmd: ["ttx", join(font_cache, path)] });
await Deno.run({
cmd: ["ttx", join(font_cache, path.replace(/woff2?$/, "ttx"))],
});
};
const ttf_urls2: Array<string> = [], woff_urls2: Array<string> = [];
await Promise.all(ttf_urls.map(async (url) => {
if (!await cached(url)) {
ttf_urls2.push(url);
}
}));

await woff_urls.reduce((cur, next) => {
return cur.then(() => woff2ttf(next));
}, Promise.resolve());
// await Promise.all(woff_urls.map(async (url) => {
// if (!await cached(url)) {
// woff_urls2.push(url);
// }
// }));
await Promise.all(ttf_urls2.concat(woff_urls2).map(download));
if (woff_urls2.length) {
await Promise.all(woff_urls2.map(woff2ttf));
}
fontdirs.add(font_cache);
}
let fontPaths = format.metadata[kFontPaths] as Array<string> || [];
if (typeof fontPaths === "string") {
fontPaths = [fontPaths];
}
fontPaths.push(...fontdirs);
format.metadata[kFontPaths] = fontPaths;
}

// Process format resources
Expand Down
1 change: 0 additions & 1 deletion src/command/render/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ export async function renderPandoc(
metadata: executeResult.metadata,
quiet,
flags: context.options.flags,
recipe,
};

// add offset if we are in a project
Expand Down
3 changes: 0 additions & 3 deletions src/command/render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,6 @@ export interface PandocOptions {

// optional offset from file to project dir
offset?: string;

// output recipe (this makes many of above options redundant)
recipe: OutputRecipe;
}

// command line flags that we need to inspect
Expand Down
Loading

0 comments on commit 1399d2e

Please sign in to comment.