Skip to content

Commit e61c1b1

Browse files
committed
incremental implicit stylesheets
1 parent 029d4e8 commit e61c1b1

File tree

4 files changed

+38
-5
lines changed

4 files changed

+38
-5
lines changed

src/client/preview.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ export function open({hash, eval: compile} = {}) {
1919
location.reload();
2020
break;
2121
}
22-
case "refresh":
22+
case "refresh": {
2323
message.cellIds.forEach((id) => {
2424
const cell = cellsById.get(id);
2525
if (cell) define(cell.cell);
2626
});
2727
break;
28+
}
2829
case "update": {
2930
const root = document.querySelector("main");
3031
if (message.previousHash !== hash) {
@@ -92,6 +93,19 @@ export function open({hash, eval: compile} = {}) {
9293
enableCopyButtons();
9394
break;
9495
}
96+
case "add-stylesheet": {
97+
const link = document.createElement("link");
98+
link.rel = "stylesheet";
99+
link.type = "text/css";
100+
link.crossOrigin = "";
101+
link.href = message.href;
102+
document.head.appendChild(link);
103+
break;
104+
}
105+
case "remove-stylesheet": {
106+
document.head.querySelector(`link[href="${message.href}"]`)?.remove();
107+
break;
108+
}
95109
}
96110
};
97111

src/libraries.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export function getImplicitSpecifiers(inputs: Set<string>): Set<string> {
2+
return addImplicitSpecifiers(new Set(), inputs);
3+
}
4+
15
export function addImplicitSpecifiers(specifiers: Set<string>, inputs: Set<string>): typeof specifiers {
26
if (inputs.has("d3") || inputs.has("Plot")) specifiers.add("npm:d3");
37
if (inputs.has("Plot")) specifiers.add("npm:@observablehq/plot");

src/markdown.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ const SUPPORTED_PROPERTIES: readonly {query: string; src: "href" | "src" | "srcs
332332
{query: "video[src]", src: "src"},
333333
{query: "video source[src]", src: "src"}
334334
]);
335+
335336
export function normalizePieceHtml(html: string, sourcePath: string, context: ParseContext): string {
336337
const {document} = parseHTML(html);
337338

@@ -419,11 +420,10 @@ function toParseCells(pieces: RenderPiece[]): CellPiece[] {
419420
return cellPieces;
420421
}
421422

423+
// TODO We need to know what line in the source the markdown starts on and pass
424+
// that as startLine in the parse context below.
422425
export async function parseMarkdown(source: string, root: string, sourcePath: string): Promise<ParseResult> {
423426
const parts = matter(source, {});
424-
425-
// TODO: We need to know what line in the source the markdown starts on and pass that
426-
// as startLine in the parse context below.
427427
const md = MarkdownIt({html: true});
428428
md.use(MarkdownItAnchor, {permalink: MarkdownItAnchor.permalink.headerLink({class: "observablehq-header-anchor"})});
429429
md.inline.ruler.push("placeholder", transformPlaceholderInline);
@@ -497,12 +497,13 @@ function diffReducer(patch: PatchItem<ParsePiece>) {
497497
if (patch.type === "remove") {
498498
return {
499499
...patch,
500+
type: "remove",
500501
items: patch.items.map((item) => ({
501502
type: item.type,
502503
id: item.id,
503504
...("cellIds" in item ? {cellIds: item.cellIds} : null)
504505
}))
505-
};
506+
} as const;
506507
}
507508
return patch;
508509
}

src/preview.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {createServer} from "node:http";
66
import type {IncomingMessage, RequestListener, Server, ServerResponse} from "node:http";
77
import {basename, dirname, extname, join, normalize} from "node:path";
88
import {fileURLToPath} from "node:url";
9+
import {difference} from "d3-array";
910
import send from "send";
1011
import {type WebSocket, WebSocketServer} from "ws";
1112
import {version} from "../package.json";
@@ -14,6 +15,7 @@ import {Loader} from "./dataloader.js";
1415
import {HttpError, isEnoent, isHttpError, isSystemError} from "./error.js";
1516
import {FileWatchers} from "./fileWatchers.js";
1617
import {createImportResolver, rewriteModule} from "./javascript/imports.js";
18+
import {getImplicitSpecifiers, getImplicitStylesheets} from "./libraries.js";
1719
import {diffMarkdown, readMarkdown} from "./markdown.js";
1820
import type {ParseResult, ReadMarkdownResult} from "./markdown.js";
1921
import {renderPreview} from "./render.js";
@@ -246,10 +248,17 @@ function getWatchPaths(parseResult: ParseResult): string[] {
246248
return paths;
247249
}
248250

251+
function getStylesheets({cells}: ParseResult): Set<string> {
252+
const inputs = new Set<string>();
253+
for (const cell of cells) cell.inputs?.forEach(inputs.add, inputs);
254+
return getImplicitStylesheets(getImplicitSpecifiers(inputs));
255+
}
256+
249257
function handleWatch(socket: WebSocket, req: IncomingMessage, options: {root: string}) {
250258
const {root} = options;
251259
let path: string | null = null;
252260
let current: ReadMarkdownResult | null = null;
261+
let stylesheets: Set<string> | null = null;
253262
let markdownWatcher: FSWatcher | null = null;
254263
let attachmentWatcher: FileWatchers | null = null;
255264
console.log(faint("socket open"), req.url);
@@ -285,6 +294,10 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, options: {root: st
285294
case "change": {
286295
const updated = await readMarkdown(path, root);
287296
if (current.parse.hash === updated.parse.hash) break;
297+
const updatedStylesheets = getStylesheets(updated.parse);
298+
for (const href of difference(stylesheets, updatedStylesheets)) send({type: "remove-stylesheet", href});
299+
for (const href of difference(updatedStylesheets, stylesheets)) send({type: "add-stylesheet", href});
300+
stylesheets = updatedStylesheets;
288301
const diff = diffMarkdown(current, updated);
289302
send({type: "update", diff, previousHash: current.parse.hash, updatedHash: updated.parse.hash});
290303
current = updated;
@@ -303,6 +316,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, options: {root: st
303316
path += ".md";
304317
current = await readMarkdown(path, root);
305318
if (current.parse.hash !== initialHash) return void send({type: "reload"});
319+
stylesheets = getStylesheets(current.parse);
306320
attachmentWatcher = await FileWatchers.of(root, path, getWatchPaths(current.parse), refreshAttachment);
307321
markdownWatcher = watch(join(root, path), watcher);
308322
}

0 commit comments

Comments
 (0)