Skip to content

Commit e087dea

Browse files
committed
rewrite header
1 parent 429b1f3 commit e087dea

File tree

8 files changed

+66
-29
lines changed

8 files changed

+66
-29
lines changed

observablehq.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export default {
103103
</svg>
104104
</a>
105105
<div style="display: flex; flex-grow: 1; justify-content: space-between; align-items: baseline;">
106-
<a href="https://observablehq.com/framework/">
106+
<a href="/">
107107
<span class="hide-if-small">Observable</span> Framework
108108
</a>
109109
<span style="display: flex; align-items: baseline; gap: 0.5rem; font-size: 14px;">

src/html.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {DOMWindow} from "jsdom";
55
import {JSDOM, VirtualConsole} from "jsdom";
66
import {isAssetPath, relativePath, resolveLocalPath} from "./path.js";
77

8-
const ASSET_PROPERTIES: readonly [selector: string, src: string][] = [
8+
const ASSET_ATTRIBUTES: readonly [selector: string, src: string][] = [
99
["a[href][download]", "href"],
1010
["audio source[src]", "src"],
1111
["audio[src]", "src"],
@@ -17,6 +17,18 @@ const ASSET_PROPERTIES: readonly [selector: string, src: string][] = [
1717
["video[src]", "src"]
1818
];
1919

20+
const PATH_ATTRIBUTES: readonly [selector: string, src: string][] = [
21+
["a[href]", "href"],
22+
["audio source[src]", "src"],
23+
["audio[src]", "src"],
24+
["img[src]", "src"],
25+
["img[srcset]", "srcset"],
26+
["link[href]", "href"],
27+
["picture source[srcset]", "srcset"],
28+
["video source[src]", "src"],
29+
["video[src]", "src"]
30+
];
31+
2032
export function isJavaScript({type}: HTMLScriptElement): boolean {
2133
if (!type) return true;
2234
type = type.toLowerCase();
@@ -48,7 +60,7 @@ export function findAssets(html: string, path: string): Assets {
4860
files.add(relativePath(path, localPath));
4961
};
5062

51-
for (const [selector, src] of ASSET_PROPERTIES) {
63+
for (const [selector, src] of ASSET_ATTRIBUTES) {
5264
for (const element of document.querySelectorAll(selector)) {
5365
const source = decodeURI(element.getAttribute(src)!);
5466
if (src === "srcset") {
@@ -85,19 +97,39 @@ export function findAssets(html: string, path: string): Assets {
8597
return {files, localImports, globalImports, staticImports};
8698
}
8799

88-
interface HtmlResolvers {
89-
resolveFile?: (specifier: string) => string;
90-
resolveScript?: (specifier: string) => string;
100+
export function rewriteHtmlPaths(html: string, path: string): string {
101+
const {document} = parseHtml(html);
102+
103+
const resolvePath = (specifier: string): string => {
104+
return isAssetPath(specifier) ? relativePath(path, specifier) : specifier;
105+
};
106+
107+
for (const [selector, src] of PATH_ATTRIBUTES) {
108+
for (const element of document.querySelectorAll(selector)) {
109+
const source = decodeURI(element.getAttribute(src)!);
110+
element.setAttribute(src, src === "srcset" ? resolveSrcset(source, resolvePath) : resolvePath(source));
111+
}
112+
}
113+
114+
return document.body.innerHTML;
115+
}
116+
117+
export interface HtmlResolvers {
118+
resolveFile: (specifier: string) => string;
119+
resolveScript: (specifier: string) => string;
91120
}
92121

93-
export function rewriteHtml(html: string, {resolveFile = String, resolveScript = String}: HtmlResolvers): string {
122+
export function rewriteHtml(
123+
html: string,
124+
{resolveFile = String, resolveScript = String}: Partial<HtmlResolvers>
125+
): string {
94126
const {document} = parseHtml(html);
95127

96128
const maybeResolveFile = (specifier: string): string => {
97129
return isAssetPath(specifier) ? resolveFile(specifier) : specifier;
98130
};
99131

100-
for (const [selector, src] of ASSET_PROPERTIES) {
132+
for (const [selector, src] of ASSET_ATTRIBUTES) {
101133
for (const element of document.querySelectorAll(selector)) {
102134
const source = decodeURI(element.getAttribute(src)!);
103135
element.setAttribute(src, src === "srcset" ? resolveSrcset(source, maybeResolveFile) : maybeResolveFile(source));

src/markdown.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {RenderRule} from "markdown-it/lib/renderer.js";
1010
import MarkdownItAnchor from "markdown-it-anchor";
1111
import type {Config} from "./config.js";
1212
import {mergeStyle} from "./config.js";
13+
import {rewriteHtmlPaths} from "./html.js";
1314
import {parseInfo} from "./info.js";
1415
import type {JavaScriptNode} from "./javascript/parse.js";
1516
import {parseJavaScript} from "./javascript/parse.js";
@@ -26,7 +27,8 @@ export interface MarkdownCode {
2627

2728
export interface MarkdownPage {
2829
title: string | null;
29-
html: string;
30+
body: string;
31+
header: string | null;
3032
data: {[key: string]: any} | null;
3133
style: string | null;
3234
code: MarkdownCode[];
@@ -307,6 +309,7 @@ export function makeLinkNormalizer(baseNormalize: (url: string) => string, clean
307309
export interface ParseOptions {
308310
md: MarkdownIt;
309311
path: string;
312+
header?: Config["header"];
310313
style?: Config["style"];
311314
}
312315

@@ -329,15 +332,16 @@ export function createMarkdownIt({
329332
return markdownIt === undefined ? md : markdownIt(md);
330333
}
331334

332-
export function parseMarkdown(input: string, {md, path, style: configStyle}: ParseOptions): MarkdownPage {
335+
export function parseMarkdown(input: string, {md, path, header, style: configStyle}: ParseOptions): MarkdownPage {
333336
const {content, data} = matter(input, {});
334337
const code: MarkdownCode[] = [];
335338
const context: ParseContext = {code, startLine: 0, currentLine: 0, path};
336339
const tokens = md.parse(content, context);
337-
const html = md.renderer.render(tokens, md.options, context); // Note: mutates code!
340+
const body = md.renderer.render(tokens, md.options, context); // Note: mutates code!
338341
const style = getStylesheet(path, data, configStyle);
339342
return {
340-
html,
343+
header: data.header != null ? String(data.header) : header != null ? rewriteHtmlPaths(header, path) : null,
344+
body,
341345
data: isEmpty(data) ? null : data,
342346
title: data?.title ?? findTitle(tokens) ?? null,
343347
style,

src/preview.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, config: Config) {
290290
const source = await readFile(join(root, path), "utf8");
291291
const page = parseMarkdown(source, {path, ...config});
292292
// delay to avoid a possibly-empty file
293-
if (!force && page.html === "") {
293+
if (!force && page.body === "") {
294294
if (!emptyTimeout) {
295295
emptyTimeout = setTimeout(() => {
296296
emptyTimeout = null;
@@ -390,8 +390,8 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, config: Config) {
390390
}
391391
}
392392

393-
function getHtml({html}: MarkdownPage, resolvers: Resolvers): string[] {
394-
return Array.from(parseHtml(rewriteHtml(html, resolvers)).document.body.children, (d) => d.outerHTML);
393+
function getHtml({body}: MarkdownPage, resolvers: Resolvers): string[] {
394+
return Array.from(parseHtml(rewriteHtml(body, resolvers)).document.body.children, (d) => d.outerHTML);
395395
}
396396

397397
function getCode({code}: MarkdownPage, resolvers: Resolvers): Map<string, string> {

src/render.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import mime from "mime";
22
import type {Config, Page, Script, Section} from "./config.js";
33
import {mergeToc} from "./config.js";
44
import {getClientPath} from "./files.js";
5-
import type {Html} from "./html.js";
6-
import {html, parseHtml, rewriteHtml} from "./html.js";
5+
import type {Html, HtmlResolvers} from "./html.js";
6+
import {html, parseHtml, rewriteHtml, rewriteHtmlPaths} from "./html.js";
77
import {transpileJavaScript} from "./javascript/transpile.js";
88
import type {MarkdownPage} from "./markdown.js";
99
import type {PageLink} from "./pager.js";
@@ -86,9 +86,9 @@ ${preview ? `\nopen({hash: ${JSON.stringify(resolvers.hash)}, eval: (body) => ev
8686
</script>${sidebar ? html`\n${await renderSidebar(title, pages, root, path, search, normalizeLink)}` : ""}${
8787
toc.show ? html`\n${renderToc(findHeaders(page), toc.label)}` : ""
8888
}
89-
<div id="observablehq-center">${renderHeader(options, data)}
89+
<div id="observablehq-center">${renderHeader(page.header, resolvers)}
9090
<main id="observablehq-main" class="observablehq${draft ? " observablehq--draft" : ""}">
91-
${html.unsafe(rewriteHtml(page.html, resolvers))}</main>${renderFooter(path, options, data, normalizeLink)}
91+
${html.unsafe(rewriteHtml(page.body, resolvers))}</main>${renderFooter(path, options, data, normalizeLink)}
9292
</div>
9393
`);
9494
}
@@ -171,7 +171,7 @@ interface Header {
171171
const tocSelector = "h1:not(:first-of-type), h2:first-child, :not(h1) + h2";
172172

173173
function findHeaders(page: MarkdownPage): Header[] {
174-
return Array.from(parseHtml(page.html).document.querySelectorAll(tocSelector))
174+
return Array.from(parseHtml(page.body).document.querySelectorAll(tocSelector))
175175
.map((node) => ({label: node.textContent, href: node.firstElementChild?.getAttribute("href")}))
176176
.filter((d): d is Header => !!d.label && !!d.href);
177177
}
@@ -236,9 +236,10 @@ function renderModulePreload(href: string): Html {
236236
return html`\n<link rel="modulepreload" href="${href}">`;
237237
}
238238

239-
function renderHeader({header}: Pick<Config, "header">, data: MarkdownPage["data"]): Html | null {
240-
if (data?.header !== undefined) header = data?.header;
241-
return header ? html`\n<header id="observablehq-header">\n${html.unsafe(header)}\n</header>` : null;
239+
function renderHeader(header: MarkdownPage["header"], resolvers: HtmlResolvers): Html | null {
240+
return header
241+
? html`\n<header id="observablehq-header">\n${html.unsafe(rewriteHtml(header, resolvers))}\n</header>`
242+
: null;
242243
}
243244

244245
function renderFooter(

src/resolvers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export async function getResolvers(
7474
page: MarkdownPage,
7575
{root, path, loaders}: {root: string; path: string; loaders: LoaderResolver}
7676
): Promise<Resolvers> {
77-
const hash = createHash("sha256").update(page.html).update(JSON.stringify(page.data));
77+
const hash = createHash("sha256").update(page.body).update(JSON.stringify(page.data));
7878
const assets = new Set<string>();
7979
const files = new Set<string>();
8080
const fileMethods = new Set<string>();
@@ -85,7 +85,7 @@ export async function getResolvers(
8585
const resolutions = new Map<string, string>();
8686

8787
// Add assets.
88-
const info = findAssets(page.html, path);
88+
const info = findAssets(page.body, path);
8989
for (const f of info.files) assets.add(f);
9090
for (const i of info.localImports) localImports.add(i);
9191
for (const i of info.globalImports) globalImports.add(i);

src/search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export async function searchIndex(config: Config, effects = defaultEffects): Pro
4444
const sourcePath = join(root, file);
4545
const source = await readFile(sourcePath, "utf8");
4646
const path = `/${join(dirname(file), basename(file, ".md"))}`;
47-
const {html, title, data} = parseMarkdown(source, {...config, path});
47+
const {body, title, data} = parseMarkdown(source, {...config, path});
4848

4949
// Skip pages that opt-out of indexing, and skip unlisted pages unless
5050
// opted-in. We only log the first case.
@@ -58,7 +58,7 @@ export async function searchIndex(config: Config, effects = defaultEffects): Pro
5858
// eslint-disable-next-line import/no-named-as-default-member
5959
const text = he
6060
.decode(
61-
html
61+
body
6262
.replaceAll(/[\n\r]/g, " ")
6363
.replaceAll(/<style\b.*<\/style\b[^>]*>/gi, " ")
6464
.replaceAll(/<[^>]+>/g, " ")

test/markdown-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe("parseMarkdown(input)", () => {
2626
const snapshot = parseMarkdown(source, {path: name, md});
2727
let allequal = true;
2828
for (const ext of ["html", "json"]) {
29-
const actual = ext === "json" ? jsonMeta(snapshot) : snapshot[ext];
29+
const actual = ext === "json" ? jsonMeta(snapshot) : snapshot.body;
3030
const outfile = resolve(outputRoot, `${ext === "json" ? outname : basename(outname, ".md")}.${ext}`);
3131
const diffile = resolve(outputRoot, `${ext === "json" ? outname : basename(outname, ".md")}-changed.${ext}`);
3232
let expected;
@@ -196,7 +196,7 @@ describe("makeLinkNormalizer(normalize, true)", () => {
196196
});
197197
});
198198

199-
function jsonMeta({html, ...rest}: MarkdownPage): string {
199+
function jsonMeta({body, header, ...rest}: MarkdownPage): string {
200200
return JSON.stringify(rest, null, 2);
201201
}
202202

0 commit comments

Comments
 (0)