Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

_brand.yml: fonts in html-like formats, font colors, schema updates, etc #10727

Merged
merged 2 commits into from
Sep 20, 2024
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
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
Loading