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

globalStylesheets config option #1597

Merged
merged 9 commits into from
Aug 22, 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
4 changes: 4 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,7 @@ The set of replacements for straight double and single quotes used when the [**t
## linkify <a href="https://github.com/observablehq/framework/releases/tag/v1.7.0" class="observablehq-version-badge" data-version="^1.7.0" title="Added in 1.7.0"></a>

If true (the default), automatically convert URL-like text to links in Markdown.

## globalStylesheets <a href="https://github.com/observablehq/framework/pull/1597" class="observablehq-version-badge" data-version="prerelease" title="Added in #1597"></a>

An array of links to global stylesheets to add to every page’s head, in addition to the [page stylesheet](#style). Defaults to loading [Source Serif 4](https://fonts.google.com/specimen/Source+Serif+4) from Google Fonts.
7 changes: 4 additions & 3 deletions observablehq.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ export default {
{name: "Contributing", path: "/contributing", pager: false}
],
base: "/framework",
head: `<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Spline+Sans+Mono:ital,wght@0,300..700;1,300..700&display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Spline+Sans+Mono:ital,wght@0,300..700;1,300..700&display=swap" crossorigin>
<link rel="apple-touch-icon" href="/observable.png">
globalStylesheets: [
"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Spline+Sans+Mono:ital,wght@0,300..700;1,300..700&display=swap"
],
head: `<link rel="apple-touch-icon" href="/observable.png">
<link rel="icon" type="image/png" href="/observable.png" sizes="32x32">${
process.env.CI
? `
Expand Down
4 changes: 2 additions & 2 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function build(
{config}: BuildOptions,
effects: BuildEffects = new FileBuildEffects(config.output, join(config.root, ".observablehq", "cache"))
): Promise<void> {
const {root, loaders, normalizePath} = config;
const {root, loaders} = config;
Telemetry.record({event: "build", step: "start"});

// Make sure all files are readable before starting to write output files.
Expand Down Expand Up @@ -79,7 +79,7 @@ export async function build(
effects.logger.log(faint("(skipped)"));
continue;
}
const resolvers = await getResolvers(page, {root, path: sourceFile, normalizePath, loaders});
const resolvers = await getResolvers(page, {path: sourceFile, ...config});
const elapsed = Math.floor(performance.now() - start);
for (const f of resolvers.assets) files.add(resolvePath(sourceFile, f));
for (const f of resolvers.files) files.add(resolvePath(sourceFile, f));
Expand Down
31 changes: 26 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interface Config {
footer: PageFragmentFunction | string | null; // defaults to “Built with Observable on [date].”
toc: TableOfContents;
style: null | Style; // defaults to {theme: ["light", "dark"]}
globalStylesheets: string[]; // defaults to Source Serif from Google Fonts
search: SearchConfig | null; // default to null
md: MarkdownIt;
normalizePath: (path: string) => string;
Expand All @@ -99,6 +100,7 @@ export interface ConfigSpec {
base?: unknown;
sidebar?: unknown;
style?: unknown;
globalStylesheets?: unknown;
theme?: unknown;
search?: unknown;
scripts?: unknown;
Expand Down Expand Up @@ -224,6 +226,10 @@ export function normalizeConfig(spec: ConfigSpec = {}, defaultRoot?: string, wat
: spec.style !== undefined
? {path: String(spec.style)}
: {theme: normalizeTheme(spec.theme === undefined ? "default" : spec.theme)};
const globalStylesheets =
spec.globalStylesheets === undefined
? defaultGlobalStylesheets()
: normalizeGlobalStylesheets(spec.globalStylesheets);
const md = createMarkdownIt({
linkify: spec.linkify === undefined ? undefined : Boolean(spec.linkify),
typographer: spec.typographer === undefined ? undefined : Boolean(spec.typographer),
Expand Down Expand Up @@ -255,6 +261,7 @@ export function normalizeConfig(spec: ConfigSpec = {}, defaultRoot?: string, wat
footer,
toc,
style,
globalStylesheets,
search,
md,
normalizePath: getPathNormalizer(spec.cleanUrls),
Expand Down Expand Up @@ -282,6 +289,12 @@ function pageFragment(spec: unknown): PageFragmentFunction | string | null {
return typeof spec === "function" ? (spec as PageFragmentFunction) : stringOrNull(spec);
}

function defaultGlobalStylesheets(): string[] {
return [
"https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap"
];
}

function defaultFooter(): string {
const date = currentDate ?? new Date();
return `Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="${formatIsoDate(
Expand Down Expand Up @@ -309,20 +322,28 @@ function findDefaultRoot(defaultRoot?: string): string {
return root;
}

function normalizeArray<T>(spec: unknown, f: (spec: unknown) => T): T[] {
return spec == null ? [] : Array.from(spec as ArrayLike<unknown>, f);
}

function normalizeBase(spec: unknown): string {
let base = String(spec);
if (!base.startsWith("/")) throw new Error(`base must start with slash: ${base}`);
if (!base.endsWith("/")) base += "/";
return base;
}

function normalizeGlobalStylesheets(spec: unknown): string[] {
return normalizeArray(spec, String);
}

export function normalizeTheme(spec: unknown): string[] {
return resolveTheme(typeof spec === "string" ? [spec] : spec === null ? [] : Array.from(spec as any, String));
return resolveTheme(typeof spec === "string" ? [spec] : normalizeArray(spec, String));
}

function normalizeScripts(spec: unknown): Script[] {
console.warn(`${yellow("Warning:")} the ${bold("scripts")} option is deprecated; use ${bold("head")} instead.`);
return Array.from(spec as any, normalizeScript);
return normalizeArray(spec, normalizeScript);
}

function normalizeScript(spec: unknown): Script {
Expand All @@ -334,7 +355,7 @@ function normalizeScript(spec: unknown): Script {
}

function normalizePages(spec: unknown): Config["pages"] {
return Array.from(spec as any, (spec: SectionSpec | PageSpec) =>
return normalizeArray(spec, (spec: any) =>
"pages" in spec ? normalizeSection(spec, normalizePage) : normalizePage(spec)
);
}
Expand All @@ -348,7 +369,7 @@ function normalizeSection<T>(
const open = collapsible ? Boolean(spec.open) : true;
const pager = spec.pager === undefined ? "main" : stringOrNull(spec.pager);
const path = spec.path == null ? null : normalizePath(spec.path);
const pages = Array.from(spec.pages as any, (spec: PageSpec) => normalizePage(spec, pager));
const pages = normalizeArray(spec.pages, (spec: any) => normalizePage(spec, pager));
return {name, collapsible, open, path, pager, pages};
}

Expand Down Expand Up @@ -380,7 +401,7 @@ function normalizePath(spec: unknown): string {
function normalizeInterpreters(spec: {[key: string]: unknown} = {}): {[key: string]: string[] | null} {
return Object.fromEntries(
Object.entries(spec).map(([key, value]): [string, string[] | null] => {
return [String(key), value == null ? null : Array.from(value as any, String)];
return [String(key), normalizeArray(value, String)];
})
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, configPromise: Pro

async function watcher(event: WatchEventType, force = false) {
if (!path || !config) throw new Error("not initialized");
const {root, loaders, normalizePath} = config;
const {root, loaders} = config;
switch (event) {
case "rename": {
markdownWatcher?.close();
Expand Down Expand Up @@ -336,7 +336,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, configPromise: Pro
clearTimeout(emptyTimeout);
emptyTimeout = null;
}
const resolvers = await getResolvers(page, {root, path, loaders, normalizePath});
const resolvers = await getResolvers(page, {path, ...config});
if (hash === resolvers.hash) break;
const previousHash = hash!;
const previousHtml = html!;
Expand Down
9 changes: 8 additions & 1 deletion src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ function renderListItem(page: Page, path: string, resolveLink: (href: string) =>

function renderHead(head: MarkdownPage["head"], resolvers: Resolvers): Html {
const {stylesheets, staticImports, resolveImport, resolveStylesheet} = resolvers;
return html`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>${
return html`${
hasGoogleFonts(stylesheets) ? html`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>` : null
}${
Array.from(new Set(Array.from(stylesheets, resolveStylesheet)), renderStylesheetPreload) // <link rel=preload as=style>
}${
Array.from(new Set(Array.from(stylesheets, resolveStylesheet)), renderStylesheet) // <link rel=stylesheet>
Expand Down Expand Up @@ -266,3 +268,8 @@ function renderPager({prev, next}: PageLink, resolveLink: (href: string) => stri
function renderRel(page: Page, rel: "prev" | "next", resolveLink: (href: string) => string): Html {
return html`<a rel="${rel}" href="${encodeURI(resolveLink(page.path))}"><span>${page.name}</span></a>`;
}

function hasGoogleFonts(stylesheets: Set<string>): boolean {
for (const s of stylesheets) if (s.startsWith("https://fonts.googleapis.com/")) return true;
return false;
}
9 changes: 4 additions & 5 deletions src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ResolversConfig {
root: string;
path: string;
normalizePath: (path: string) => string;
globalStylesheets?: string[];
loaders: LoaderResolver;
}

Expand Down Expand Up @@ -83,7 +84,7 @@ export const builtins = new Map<string, string>([
*/
export async function getResolvers(
page: MarkdownPage,
{root, path, normalizePath, loaders}: ResolversConfig
{root, path, normalizePath, globalStylesheets: defaultStylesheets, loaders}: ResolversConfig
): Promise<Resolvers> {
const hash = createHash("sha256").update(page.body).update(JSON.stringify(page.data));
const assets = new Set<string>();
Expand All @@ -92,7 +93,7 @@ export async function getResolvers(
const localImports = new Set<string>();
const globalImports = new Set<string>(defaultImports);
const staticImports = new Set<string>(defaultImports);
const stylesheets = new Set<string>();
const stylesheets = new Set<string>(defaultStylesheets);
const resolutions = new Map<string, string>();

// Add assets.
Expand All @@ -105,9 +106,7 @@ export async function getResolvers(
for (const i of info.staticImports) staticImports.add(i);
}

// Add stylesheets. TODO Instead of hard-coding Source Serif Pro, parse the
// page’s stylesheet to look for external imports.
stylesheets.add("https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap"); // prettier-ignore
// Add stylesheets.
if (page.style) stylesheets.add(page.style);

// Collect directly-attached files, local imports, and static imports.
Expand Down
4 changes: 2 additions & 2 deletions src/style/global.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
:root {
--monospace: Menlo, Consolas, monospace;
--monospace-font: 14px/1.5 var(--monospace);
--serif: "Source Serif Pro", "Iowan Old Style", "Apple Garamond", "Palatino Linotype", "Times New Roman",
"Droid Serif", Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--serif: "Source Serif 4", "Iowan Old Style", "Apple Garamond", "Palatino Linotype", "Times New Roman", "Droid Serif",
Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--sans-serif: -apple-system, BlinkMacSystemFont, "avenir next", avenir, helvetica, "helvetica neue", ubuntu, roboto,
noto, "segoe ui", arial, sans-serif;
--theme-blue: #4269d0;
Expand Down
6 changes: 6 additions & 0 deletions test/config-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ describe("readConfig(undefined, root)", () => {
output: "dist",
base: "/",
style: {theme: ["air", "near-midnight"]},
globalStylesheets: [
"https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap"
],
sidebar: true,
pages: [
{path: "/index", name: "Index", pager: "main"},
Expand Down Expand Up @@ -52,6 +55,9 @@ describe("readConfig(undefined, root)", () => {
output: "dist",
base: "/",
style: {theme: ["air", "near-midnight"]},
globalStylesheets: [
"https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap"
],
sidebar: true,
pages: [{name: "Build test case", path: "/simple", pager: "main"}],
title: undefined,
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/404/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Page not found</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/archives.posix/tar.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Tar</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/archives.posix/zip.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Zip</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/archives.win32/tar.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Tar</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/archives.win32/zip.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Zip</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
Expand Down
4 changes: 2 additions & 2 deletions test/output/build/config/closed/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>A page…</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="../_observablehq/theme-air,near-midnight.00000004.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="../_observablehq/theme-air,near-midnight.00000004.css">
<link rel="modulepreload" href="../_observablehq/client.00000001.js">
<link rel="modulepreload" href="../_observablehq/runtime.00000002.js">
Expand Down
Loading