From f7169bd8c462497cb6cb71aef9097e91ae1e9cf1 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Wed, 24 Jan 2024 10:12:48 -0800 Subject: [PATCH 01/13] add blocks to global config --- src/config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 43c8bad95..0551d6ff0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,6 +31,7 @@ export interface Config { output: string; // defaults to dist title?: string; pages: (Page | Section)[]; // TODO rename to sidebar? + blocks: boolean; // defaults to false pager: boolean; // defaults to true footer: string; toc: TableOfContents; @@ -91,14 +92,15 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro if (style === null) style = null; else if (style !== undefined) style = {path: String(style)}; else style = {theme: (theme = normalizeTheme(theme))}; - let {title, pages = await readPages(root), pager = true, toc = true} = spec; + let {title, pages = await readPages(root), pager = true, toc = true, blocks = false} = spec; if (title !== undefined) title = String(title); pages = Array.from(pages, normalizePageOrSection); pager = Boolean(pager); + blocks = Boolean(blocks); footer = String(footer); toc = normalizeToc(toc); deploy = deploy ? {workspace: String(deploy.workspace).replace(/^@+/, ""), project: String(deploy.project)} : null; - return {root, output, title, pages, pager, footer, toc, style, deploy}; + return {root, output, title, pages, blocks, pager, footer, toc, style, deploy}; } function normalizeTheme(spec: any): string[] { From ca07700aba9d1c130cded05b765884cfac3a6021 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Wed, 24 Jan 2024 10:32:55 -0800 Subject: [PATCH 02/13] pass "blocks" into readMarkdown --- src/preview.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/preview.ts b/src/preview.ts index 9e57d6f6f..c8e07ea8a 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -276,7 +276,7 @@ export function getPreviewStylesheet(path: string, data: ParseResult["data"], st : relativeUrl(path, `/_observablehq/theme-${style.theme.join(",")}.css`); } -function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style: defaultStyle}: Config) { +function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style: defaultStyle, blocks: boolean}: Config) { let path: string | null = null; let current: ReadMarkdownResult | null = null; let stylesheets: Set | null = null; @@ -322,7 +322,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style: defa break; } case "change": { - const updated = await readMarkdown(path, root); + const updated = await readMarkdown(path, root, blocks); if (current.parse.hash === updated.parse.hash) break; const updatedStylesheets = await getStylesheets(updated.parse); for (const href of difference(stylesheets, updatedStylesheets)) send({type: "remove-stylesheet", href}); @@ -344,7 +344,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style: defa if (!(path = normalize(path)).startsWith("/")) throw new Error("Invalid path: " + initialPath); if (path.endsWith("/")) path += "index"; path += ".md"; - current = await readMarkdown(path, root); + current = await readMarkdown(path, root, false); if (current.parse.hash !== initialHash) return void send({type: "reload"}); stylesheets = await getStylesheets(current.parse); attachmentWatcher = await FileWatchers.of(root, path, getWatchPaths(current.parse), refreshAttachment); From d4ee52818da718d8789c4ad436ab7cbaf5a5e8b5 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Wed, 24 Jan 2024 10:41:28 -0800 Subject: [PATCH 03/13] fix typing --- src/preview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/preview.ts b/src/preview.ts index c8e07ea8a..f442cbe12 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -276,7 +276,7 @@ export function getPreviewStylesheet(path: string, data: ParseResult["data"], st : relativeUrl(path, `/_observablehq/theme-${style.theme.join(",")}.css`); } -function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style: defaultStyle, blocks: boolean}: Config) { +function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style: defaultStyle, blocks}: Config) { let path: string | null = null; let current: ReadMarkdownResult | null = null; let stylesheets: Set | null = null; From 2425b1f4d5c8f4aa3563915f9c932b292f54dcf3 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Thu, 25 Jan 2024 11:47:35 -0800 Subject: [PATCH 04/13] add global and front-matter blocks show option --- src/config.ts | 6 +++--- src/markdown.ts | 18 ++++++++++++------ src/preview.ts | 4 ++-- src/render.ts | 5 +++-- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/config.ts b/src/config.ts index 0551d6ff0..a819c05fb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,7 +31,7 @@ export interface Config { output: string; // defaults to dist title?: string; pages: (Page | Section)[]; // TODO rename to sidebar? - blocks: boolean; // defaults to false + blocks: null | string; // defaults to null pager: boolean; // defaults to true footer: string; toc: TableOfContents; @@ -92,11 +92,11 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro if (style === null) style = null; else if (style !== undefined) style = {path: String(style)}; else style = {theme: (theme = normalizeTheme(theme))}; - let {title, pages = await readPages(root), pager = true, toc = true, blocks = false} = spec; + let {title, pages = await readPages(root), pager = true, toc = true, blocks = null} = spec; if (title !== undefined) title = String(title); pages = Array.from(pages, normalizePageOrSection); pager = Boolean(pager); - blocks = Boolean(blocks); + blocks = (blocks !== null) ? String(blocks) : null; footer = String(footer); toc = normalizeToc(toc); deploy = deploy ? {workspace: String(deploy.workspace).replace(/^@+/, ""), project: String(deploy.project)} : null; diff --git a/src/markdown.ts b/src/markdown.ts index 4477c5480..566ce9f8a 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -88,6 +88,10 @@ function isFalse(attribute: string | undefined): boolean { return attribute?.toLowerCase() === "false"; } +function isShow(attribute: string | undefined): boolean { + return attribute?.toLowerCase?.() === "show"; +} + function getLiveSource(content: string, tag: string): string | undefined { return tag === "js" ? content @@ -104,7 +108,7 @@ function getLiveSource(content: string, tag: string): string | undefined { : undefined; } -function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string): RenderRule { +function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string, showBlocks: boolean): RenderRule { return (tokens, idx, options, context: ParseContext, self) => { const token = tokens[idx]; const {tag, attributes} = parseInfo(token.info); @@ -129,7 +133,8 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s }">\n`; count++; } - if (attributes.echo == null ? source == null : !isFalse(attributes.echo)) { + // Conditionally render fanced block source. + if (attributes.echo == null ? showBlocks || source == null : !isFalse(attributes.echo)) { result += baseRenderer(tokens, idx, options, context, self); count++; } @@ -420,14 +425,15 @@ async function toParseCells(pieces: RenderPiece[]): Promise { // TODO We need to know what line in the source the markdown starts on and pass // that as startLine in the parse context below. -export async function parseMarkdown(source: string, root: string, sourcePath: string): Promise { +export async function parseMarkdown(source: string, root: string, sourcePath: string, blocks: null | string): Promise { const parts = matter(source, {}); const md = MarkdownIt({html: true}); + const showBlocks = parts.data?.blocks == null ? isShow(blocks) : isShow(parts.data?.blocks); md.use(MarkdownItAnchor, {permalink: MarkdownItAnchor.permalink.headerLink({class: "observablehq-header-anchor"})}); md.inline.ruler.push("placeholder", transformPlaceholderInline); md.core.ruler.before("linkify", "placeholder", transformPlaceholderCore); md.renderer.rules.placeholder = makePlaceholderRenderer(root, sourcePath); - md.renderer.rules.fence = makeFenceRenderer(root, md.renderer.rules.fence!, sourcePath); + md.renderer.rules.fence = makeFenceRenderer(root, md.renderer.rules.fence!, sourcePath, showBlocks); md.renderer.rules.softbreak = makeSoftbreakRenderer(md.renderer.rules.softbreak!); md.renderer.render = renderIntoPieces(md.renderer, root, sourcePath); const context: ParseContext = {files: [], imports: [], pieces: [], startLine: 0, currentLine: 0}; @@ -538,8 +544,8 @@ export function diffMarkdown({parse: prevParse}: ReadMarkdownResult, {parse: nex .map(diffReducer); } -export async function readMarkdown(path: string, root: string): Promise { +export async function readMarkdown(path: string, root: string, blocks: null | string): Promise { const contents = await readFile(join(root, path), "utf-8"); - const parse = await parseMarkdown(contents, root, path); + const parse = await parseMarkdown(contents, root, path, blocks); return {contents, parse}; } diff --git a/src/preview.ts b/src/preview.ts index f442cbe12..9c0d9383b 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -276,7 +276,7 @@ export function getPreviewStylesheet(path: string, data: ParseResult["data"], st : relativeUrl(path, `/_observablehq/theme-${style.theme.join(",")}.css`); } -function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style: defaultStyle, blocks}: Config) { +function handleWatch(socket: WebSocket, req: IncomingMessage, {root, blocks, style: defaultStyle}: Config) { let path: string | null = null; let current: ReadMarkdownResult | null = null; let stylesheets: Set | null = null; @@ -344,7 +344,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, {root, style: defa if (!(path = normalize(path)).startsWith("/")) throw new Error("Invalid path: " + initialPath); if (path.endsWith("/")) path += "index"; path += ".md"; - current = await readMarkdown(path, root, false); + current = await readMarkdown(path, root, blocks); if (current.parse.hash !== initialHash) return void send({type: "reload"}); stylesheets = await getStylesheets(current.parse); attachmentWatcher = await FileWatchers.of(root, path, getWatchPaths(current.parse), refreshAttachment); diff --git a/src/render.ts b/src/render.ts index 420973a53..7b47eb1a4 100644 --- a/src/render.ts +++ b/src/render.ts @@ -23,10 +23,11 @@ export interface Render { export interface RenderOptions extends Config { root: string; path: string; + blocks: string | null; } export async function renderPreview(source: string, options: RenderOptions): Promise { - const parseResult = await parseMarkdown(source, options.root, options.path); + const parseResult = await parseMarkdown(source, options.root, options.path, options.blocks); return { html: await render(parseResult, {...options, preview: true}), files: parseResult.files, @@ -36,7 +37,7 @@ export async function renderPreview(source: string, options: RenderOptions): Pro } export async function renderServerless(source: string, options: RenderOptions): Promise { - const parseResult = await parseMarkdown(source, options.root, options.path); + const parseResult = await parseMarkdown(source, options.root, options.path, options.blocks); return { html: await render(parseResult, options), files: parseResult.files, From c24925cbaad8454345e4a584833f3ba9a5ea889b Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Thu, 25 Jan 2024 12:03:01 -0800 Subject: [PATCH 05/13] fix ts issues --- src/config.ts | 2 +- src/markdown.ts | 2 +- test/markdown-test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index a819c05fb..d71464256 100644 --- a/src/config.ts +++ b/src/config.ts @@ -61,7 +61,7 @@ async function readPages(root: string): Promise { const pages: Page[] = []; for await (const file of visitFiles(root)) { if (file === "index.md" || file === "404.md" || extname(file) !== ".md") continue; - const parsed = await parseMarkdown(await readFile(join(root, file), "utf-8"), root, file); + const parsed = await parseMarkdown(await readFile(join(root, file), "utf-8"), root, file, null); const name = basename(file, ".md"); const page = {path: join("/", dirname(file), name), name: parsed.title ?? "Untitled"}; if (name === "index") pages.unshift(page); diff --git a/src/markdown.ts b/src/markdown.ts index 566ce9f8a..102c33439 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -88,7 +88,7 @@ function isFalse(attribute: string | undefined): boolean { return attribute?.toLowerCase() === "false"; } -function isShow(attribute: string | undefined): boolean { +function isShow(attribute: string | undefined | null): boolean { return attribute?.toLowerCase?.() === "show"; } diff --git a/test/markdown-test.ts b/test/markdown-test.ts index c17cf8162..678ca0e9d 100644 --- a/test/markdown-test.ts +++ b/test/markdown-test.ts @@ -22,7 +22,7 @@ describe("parseMarkdown(input)", () => { const outname = only || skip ? name.slice(5) : name; (only ? it.only : skip ? it.skip : it)(`test/input/${name}`, async () => { - const snapshot = await parseMarkdown(await readFile(path, "utf8"), "test/input", name); + const snapshot = await parseMarkdown(await readFile(path, "utf8"), "test/input", name, null); let allequal = true; for (const ext of ["html", "json"]) { const actual = ext === "json" ? jsonMeta(snapshot) : snapshot[ext]; From 920194f4424ff4cc92323d8b2fc868a2a7cc042c Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Thu, 25 Jan 2024 12:06:56 -0800 Subject: [PATCH 06/13] prettify --- src/config.ts | 2 +- src/markdown.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index d71464256..2e6b5bf59 100644 --- a/src/config.ts +++ b/src/config.ts @@ -96,7 +96,7 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro if (title !== undefined) title = String(title); pages = Array.from(pages, normalizePageOrSection); pager = Boolean(pager); - blocks = (blocks !== null) ? String(blocks) : null; + blocks = blocks !== null ? String(blocks) : null; footer = String(footer); toc = normalizeToc(toc); deploy = deploy ? {workspace: String(deploy.workspace).replace(/^@+/, ""), project: String(deploy.project)} : null; diff --git a/src/markdown.ts b/src/markdown.ts index 102c33439..54f5f206c 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -108,7 +108,12 @@ function getLiveSource(content: string, tag: string): string | undefined { : undefined; } -function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string, showBlocks: boolean): RenderRule { +function makeFenceRenderer( + root: string, + baseRenderer: RenderRule, + sourcePath: string, + showBlocks: boolean +): RenderRule { return (tokens, idx, options, context: ParseContext, self) => { const token = tokens[idx]; const {tag, attributes} = parseInfo(token.info); @@ -425,7 +430,12 @@ async function toParseCells(pieces: RenderPiece[]): Promise { // TODO We need to know what line in the source the markdown starts on and pass // that as startLine in the parse context below. -export async function parseMarkdown(source: string, root: string, sourcePath: string, blocks: null | string): Promise { +export async function parseMarkdown( + source: string, + root: string, + sourcePath: string, + blocks: null | string +): Promise { const parts = matter(source, {}); const md = MarkdownIt({html: true}); const showBlocks = parts.data?.blocks == null ? isShow(blocks) : isShow(parts.data?.blocks); From d4e55e7ab1dc4af4eb3d2e38849aad48de7d71b6 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Thu, 25 Jan 2024 12:13:27 -0800 Subject: [PATCH 07/13] fix existing tests --- test/config-test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/config-test.ts b/test/config-test.ts index 04ba3a7cb..b98c6a963 100644 --- a/test/config-test.ts +++ b/test/config-test.ts @@ -21,6 +21,7 @@ describe("readConfig(undefined, root)", () => { pager: true, footer: 'Built with Observable on Jan 11, 2024.', + blocks: null, deploy: { workspace: "acme", project: "bi" @@ -38,6 +39,7 @@ describe("readConfig(undefined, root)", () => { pager: true, footer: 'Built with Observable on Jan 11, 2024.', + blocks: null, deploy: null }); }); From 8e9ed0573b2f85217707291e5474fb12e6395907 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Thu, 25 Jan 2024 14:43:44 -0800 Subject: [PATCH 08/13] add front-matter level test --- test/input/fenced-code-options-with-blocks.md | 39 ++++++++ .../fenced-code-options-with-blocks.html | 18 ++++ .../fenced-code-options-with-blocks.json | 99 +++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 test/input/fenced-code-options-with-blocks.md create mode 100644 test/output/fenced-code-options-with-blocks.html create mode 100644 test/output/fenced-code-options-with-blocks.json diff --git a/test/input/fenced-code-options-with-blocks.md b/test/input/fenced-code-options-with-blocks.md new file mode 100644 index 000000000..69c947c15 --- /dev/null +++ b/test/input/fenced-code-options-with-blocks.md @@ -0,0 +1,39 @@ +--- +blocks: show +--- + +# Fenced code options + +```js echo +function add(a, b) { + return a + b; +} +``` + +```js +function bareJs() { + return 1; +} +``` + +```js echo +function langOutside() { + return 1; +} +``` + +```js echo=false whatever +function langAndAttributes() { + return 1; +} +``` + +```js echo=false run=false +function langAndAttributes() { + return 1; +} +``` + +```py echo=false +1 + 2 +``` diff --git a/test/output/fenced-code-options-with-blocks.html b/test/output/fenced-code-options-with-blocks.html new file mode 100644 index 000000000..3f0b41a3b --- /dev/null +++ b/test/output/fenced-code-options-with-blocks.html @@ -0,0 +1,18 @@ +

Fenced code options

+
+
function add(a, b) {
+  return a + b;
+}
+
+
+
function bareJs() {
+  return 1;
+}
+
+
+
function langOutside() {
+  return 1;
+}
+
+
+ \ No newline at end of file diff --git a/test/output/fenced-code-options-with-blocks.json b/test/output/fenced-code-options-with-blocks.json new file mode 100644 index 000000000..2109a8c0c --- /dev/null +++ b/test/output/fenced-code-options-with-blocks.json @@ -0,0 +1,99 @@ +{ + "data": { + "blocks": "show" + }, + "title": "Fenced code options", + "files": [], + "imports": [], + "pieces": [ + { + "type": "html", + "id": "", + "cellIds": [], + "html": "

Fenced code options

\n" + }, + { + "type": "html", + "id": "", + "cellIds": [ + "fe9e095e" + ], + "html": "
\n
function add(a, b) {\n  return a + b;\n}\n
\n
" + }, + { + "type": "html", + "id": "", + "cellIds": [ + "4bf815ab" + ], + "html": "
\n
function bareJs() {\n  return 1;\n}\n
\n
" + }, + { + "type": "html", + "id": "", + "cellIds": [ + "d41a5631" + ], + "html": "
\n
function langOutside() {\n  return 1;\n}\n
\n
" + }, + { + "type": "html", + "id": "", + "cellIds": [ + "2f4eff36" + ], + "html": "
\n" + }, + { + "type": "html", + "id": "", + "cellIds": [], + "html": "" + }, + { + "type": "html", + "id": "", + "cellIds": [], + "html": "" + } + ], + "cells": [ + { + "type": "cell", + "id": "fe9e095e", + "expression": false, + "outputs": [ + "add" + ], + "body": "() => {\nfunction add(a, b) {\n return a + b;\n}\nreturn {add};\n}" + }, + { + "type": "cell", + "id": "4bf815ab", + "expression": false, + "outputs": [ + "bareJs" + ], + "body": "() => {\nfunction bareJs() {\n return 1;\n}\nreturn {bareJs};\n}" + }, + { + "type": "cell", + "id": "d41a5631", + "expression": false, + "outputs": [ + "langOutside" + ], + "body": "() => {\nfunction langOutside() {\n return 1;\n}\nreturn {langOutside};\n}" + }, + { + "type": "cell", + "id": "2f4eff36", + "expression": false, + "outputs": [ + "langAndAttributes" + ], + "body": "() => {\nfunction langAndAttributes() {\n return 1;\n}\nreturn {langAndAttributes};\n}" + } + ], + "hash": "ec47fece52c6cf6f693413531f76f7d94d94950abe774b2a79c2ae14bef6cb26" +} \ No newline at end of file From 95508cc6607c9ece3285f8e8df123d0359cc37c9 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Thu, 25 Jan 2024 15:37:05 -0800 Subject: [PATCH 09/13] add tests with custom config --- test/build-test.ts | 7 +- .../blocks/fenced-code-options-with-blocks.md | 39 +++++++++ .../input/build/blocks/fenced-code-options.md | 35 ++++++++ .../input/build/blocks/observablehq.config.ts | 3 + .../fenced-code-options-with-blocks.html | 87 +++++++++++++++++++ .../build/blocks/fenced-code-options.html | 87 +++++++++++++++++++ test/preview/preview-test.ts | 2 +- 7 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 test/input/build/blocks/fenced-code-options-with-blocks.md create mode 100644 test/input/build/blocks/fenced-code-options.md create mode 100644 test/input/build/blocks/observablehq.config.ts create mode 100644 test/output/build/blocks/fenced-code-options-with-blocks.html create mode 100644 test/output/build/blocks/fenced-code-options.html diff --git a/test/build-test.ts b/test/build-test.ts index f31e5d052..a80a7c424 100644 --- a/test/build-test.ts +++ b/test/build-test.ts @@ -12,7 +12,7 @@ const silentEffects = { output: {write() {}} }; -describe("build", async () => { +describe.only("build", async () => { before(() => setCurrentDate(new Date("2024-01-10T16:00:00"))); mockJsDelivr(); @@ -35,6 +35,11 @@ describe("build", async () => { await rm(actualDir, {recursive: true, force: true}); if (generate) console.warn(`! generating ${expectedDir}`); const config = Object.assign(await readConfig(undefined, path), {output: outputDir}); + if (name === "blocks") { + console.log({ name }); + console.log({ path }); + console.log({ config }); + } await build({config, addPublic}, new TestEffects(outputDir)); // In the addPublic case, we don’t want to test the contents of the public diff --git a/test/input/build/blocks/fenced-code-options-with-blocks.md b/test/input/build/blocks/fenced-code-options-with-blocks.md new file mode 100644 index 000000000..69c947c15 --- /dev/null +++ b/test/input/build/blocks/fenced-code-options-with-blocks.md @@ -0,0 +1,39 @@ +--- +blocks: show +--- + +# Fenced code options + +```js echo +function add(a, b) { + return a + b; +} +``` + +```js +function bareJs() { + return 1; +} +``` + +```js echo +function langOutside() { + return 1; +} +``` + +```js echo=false whatever +function langAndAttributes() { + return 1; +} +``` + +```js echo=false run=false +function langAndAttributes() { + return 1; +} +``` + +```py echo=false +1 + 2 +``` diff --git a/test/input/build/blocks/fenced-code-options.md b/test/input/build/blocks/fenced-code-options.md new file mode 100644 index 000000000..75215c155 --- /dev/null +++ b/test/input/build/blocks/fenced-code-options.md @@ -0,0 +1,35 @@ +# Fenced code options + +```js echo +function add(a, b) { + return a + b; +} +``` + +```js +function bareJs() { + return 1; +} +``` + +```js echo +function langOutside() { + return 1; +} +``` + +```js echo=false whatever +function langAndAttributes() { + return 1; +} +``` + +```js echo=false run=false +function langAndAttributes() { + return 1; +} +``` + +```py echo=false +1 + 2 +``` diff --git a/test/input/build/blocks/observablehq.config.ts b/test/input/build/blocks/observablehq.config.ts new file mode 100644 index 000000000..dbe67bf28 --- /dev/null +++ b/test/input/build/blocks/observablehq.config.ts @@ -0,0 +1,3 @@ +export default { + blocks: "show" +}; diff --git a/test/output/build/blocks/fenced-code-options-with-blocks.html b/test/output/build/blocks/fenced-code-options-with-blocks.html new file mode 100644 index 000000000..31f208d24 --- /dev/null +++ b/test/output/build/blocks/fenced-code-options-with-blocks.html @@ -0,0 +1,87 @@ + + + +Fenced code options + + + + + + + + + + + + + + +
+
+

Fenced code options

+
+
function add(a, b) {
+  return a + b;
+}
+
+
+
function bareJs() {
+  return 1;
+}
+
+
+
function langOutside() {
+  return 1;
+}
+
+
+
+ +
diff --git a/test/output/build/blocks/fenced-code-options.html b/test/output/build/blocks/fenced-code-options.html new file mode 100644 index 000000000..1ef91bda1 --- /dev/null +++ b/test/output/build/blocks/fenced-code-options.html @@ -0,0 +1,87 @@ + + + +Fenced code options + + + + + + + + + + + + + + +
+
+

Fenced code options

+
+
function add(a, b) {
+  return a + b;
+}
+
+
+
function bareJs() {
+  return 1;
+}
+
+
+
function langOutside() {
+  return 1;
+}
+
+
+
+ +
diff --git a/test/preview/preview-test.ts b/test/preview/preview-test.ts index 0ccd1aecb..139e6fdd2 100644 --- a/test/preview/preview-test.ts +++ b/test/preview/preview-test.ts @@ -10,7 +10,7 @@ const testHostName = process.env.TEST_HOSTNAME ?? "127.0.0.1"; const testPort = +(process.env.TEST_PORT ?? 8080); const testServerOptions: PreviewOptions = { - config: await normalizeConfig({root: testHostRoot}), + config: await normalizeConfig({root: testHostRoot, blocks: "show"}), hostname: testHostName, port: testPort, verbose: false From 4cdc0c9c9d9db9600b6c16d7c37c56184abec69d Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Thu, 25 Jan 2024 15:42:34 -0800 Subject: [PATCH 10/13] remove testing cruft --- test/build-test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/build-test.ts b/test/build-test.ts index a80a7c424..f31e5d052 100644 --- a/test/build-test.ts +++ b/test/build-test.ts @@ -12,7 +12,7 @@ const silentEffects = { output: {write() {}} }; -describe.only("build", async () => { +describe("build", async () => { before(() => setCurrentDate(new Date("2024-01-10T16:00:00"))); mockJsDelivr(); @@ -35,11 +35,6 @@ describe.only("build", async () => { await rm(actualDir, {recursive: true, force: true}); if (generate) console.warn(`! generating ${expectedDir}`); const config = Object.assign(await readConfig(undefined, path), {output: outputDir}); - if (name === "blocks") { - console.log({ name }); - console.log({ path }); - console.log({ config }); - } await build({config, addPublic}, new TestEffects(outputDir)); // In the addPublic case, we don’t want to test the contents of the public From 256edc2c881a86d40f4360b1d284afbde94e15d1 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Thu, 25 Jan 2024 15:45:31 -0800 Subject: [PATCH 11/13] remove spuriouse config --- test/preview/preview-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/preview/preview-test.ts b/test/preview/preview-test.ts index 139e6fdd2..0ccd1aecb 100644 --- a/test/preview/preview-test.ts +++ b/test/preview/preview-test.ts @@ -10,7 +10,7 @@ const testHostName = process.env.TEST_HOSTNAME ?? "127.0.0.1"; const testPort = +(process.env.TEST_PORT ?? 8080); const testServerOptions: PreviewOptions = { - config: await normalizeConfig({root: testHostRoot, blocks: "show"}), + config: await normalizeConfig({root: testHostRoot}), hostname: testHostName, port: testPort, verbose: false From 0046f9834bc069da162f23aed33490a5adea4e7e Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Mon, 29 Jan 2024 10:26:10 -0800 Subject: [PATCH 12/13] move to echo --- observablehq.config.ts | 1 + src/config.ts | 8 ++--- src/markdown.ts | 33 ++++++++++--------- src/preview.ts | 6 ++-- src/render.ts | 6 ++-- test/config-test.ts | 4 +-- .../fenced-code-options-with-echo.md} | 2 +- .../{blocks => echo}/fenced-code-options.md | 0 .../{blocks => echo}/observablehq.config.ts | 2 +- .../fenced-code-options-with-echo.html} | 2 +- .../{blocks => echo}/fenced-code-options.html | 4 +-- .../fenced-code-options-with-blocks.html | 8 ++--- .../fenced-code-options-with-blocks.json | 2 +- 13 files changed, 38 insertions(+), 40 deletions(-) rename test/input/build/{blocks/fenced-code-options-with-blocks.md => echo/fenced-code-options-with-echo.md} (96%) rename test/input/build/{blocks => echo}/fenced-code-options.md (100%) rename test/input/build/{blocks => echo}/observablehq.config.ts (54%) rename test/output/build/{blocks/fenced-code-options-with-blocks.html => echo/fenced-code-options-with-echo.html} (98%) rename test/output/build/{blocks => echo}/fenced-code-options.html (96%) diff --git a/observablehq.config.ts b/observablehq.config.ts index a3d966128..0adf38aa0 100644 --- a/observablehq.config.ts +++ b/observablehq.config.ts @@ -1,5 +1,6 @@ export default { title: "Observable CLI", + echo: false, pages: [ {name: "Getting started", path: "/getting-started"}, {name: "Routing", path: "/routing"}, diff --git a/src/config.ts b/src/config.ts index 2e6b5bf59..5f08a282a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,7 +31,7 @@ export interface Config { output: string; // defaults to dist title?: string; pages: (Page | Section)[]; // TODO rename to sidebar? - blocks: null | string; // defaults to null + echo: null | string; // defaults to null pager: boolean; // defaults to true footer: string; toc: TableOfContents; @@ -92,15 +92,15 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro if (style === null) style = null; else if (style !== undefined) style = {path: String(style)}; else style = {theme: (theme = normalizeTheme(theme))}; - let {title, pages = await readPages(root), pager = true, toc = true, blocks = null} = spec; + let {title, pages = await readPages(root), pager = true, toc = true, echo = null} = spec; if (title !== undefined) title = String(title); pages = Array.from(pages, normalizePageOrSection); pager = Boolean(pager); - blocks = blocks !== null ? String(blocks) : null; + echo = echo !== null ? String(echo) : null; footer = String(footer); toc = normalizeToc(toc); deploy = deploy ? {workspace: String(deploy.workspace).replace(/^@+/, ""), project: String(deploy.project)} : null; - return {root, output, title, pages, blocks, pager, footer, toc, style, deploy}; + return {root, output, title, pages, echo, pager, footer, toc, style, deploy}; } function normalizeTheme(spec: any): string[] { diff --git a/src/markdown.ts b/src/markdown.ts index 54f5f206c..807e84bef 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -88,10 +88,6 @@ function isFalse(attribute: string | undefined): boolean { return attribute?.toLowerCase() === "false"; } -function isShow(attribute: string | undefined | null): boolean { - return attribute?.toLowerCase?.() === "show"; -} - function getLiveSource(content: string, tag: string): string | undefined { return tag === "js" ? content @@ -108,12 +104,8 @@ function getLiveSource(content: string, tag: string): string | undefined { : undefined; } -function makeFenceRenderer( - root: string, - baseRenderer: RenderRule, - sourcePath: string, - showBlocks: boolean -): RenderRule { +function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string, echo: boolean): RenderRule { + console.log({makeFenceRendererEcho: echo}); return (tokens, idx, options, context: ParseContext, self) => { const token = tokens[idx]; const {tag, attributes} = parseInfo(token.info); @@ -139,7 +131,7 @@ function makeFenceRenderer( count++; } // Conditionally render fanced block source. - if (attributes.echo == null ? showBlocks || source == null : !isFalse(attributes.echo)) { + if (attributes.echo == null ? echo || source == null : !isFalse(attributes.echo)) { result += baseRenderer(tokens, idx, options, context, self); count++; } @@ -428,22 +420,31 @@ async function toParseCells(pieces: RenderPiece[]): Promise { return cellPieces; } +// Combine project and page level echo configuration +function isEcho(project: null | string, page: undefined | boolean): boolean { + return page == null ? project != null && !isFalse(project) : page; +} + // TODO We need to know what line in the source the markdown starts on and pass // that as startLine in the parse context below. export async function parseMarkdown( source: string, root: string, sourcePath: string, - blocks: null | string + echo: null | string ): Promise { const parts = matter(source, {}); const md = MarkdownIt({html: true}); - const showBlocks = parts.data?.blocks == null ? isShow(blocks) : isShow(parts.data?.blocks); md.use(MarkdownItAnchor, {permalink: MarkdownItAnchor.permalink.headerLink({class: "observablehq-header-anchor"})}); md.inline.ruler.push("placeholder", transformPlaceholderInline); md.core.ruler.before("linkify", "placeholder", transformPlaceholderCore); md.renderer.rules.placeholder = makePlaceholderRenderer(root, sourcePath); - md.renderer.rules.fence = makeFenceRenderer(root, md.renderer.rules.fence!, sourcePath, showBlocks); + md.renderer.rules.fence = makeFenceRenderer( + root, + md.renderer.rules.fence!, + sourcePath, + isEcho(echo, parts.data?.echo) + ); md.renderer.rules.softbreak = makeSoftbreakRenderer(md.renderer.rules.softbreak!); md.renderer.render = renderIntoPieces(md.renderer, root, sourcePath); const context: ParseContext = {files: [], imports: [], pieces: [], startLine: 0, currentLine: 0}; @@ -554,8 +555,8 @@ export function diffMarkdown({parse: prevParse}: ReadMarkdownResult, {parse: nex .map(diffReducer); } -export async function readMarkdown(path: string, root: string, blocks: null | string): Promise { +export async function readMarkdown(path: string, root: string, echo: null | string): Promise { const contents = await readFile(join(root, path), "utf-8"); - const parse = await parseMarkdown(contents, root, path, blocks); + const parse = await parseMarkdown(contents, root, path, echo); return {contents, parse}; } diff --git a/src/preview.ts b/src/preview.ts index 9c0d9383b..4f1dbfa44 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -276,7 +276,7 @@ export function getPreviewStylesheet(path: string, data: ParseResult["data"], st : relativeUrl(path, `/_observablehq/theme-${style.theme.join(",")}.css`); } -function handleWatch(socket: WebSocket, req: IncomingMessage, {root, blocks, style: defaultStyle}: Config) { +function handleWatch(socket: WebSocket, req: IncomingMessage, {root, echo, style: defaultStyle}: Config) { let path: string | null = null; let current: ReadMarkdownResult | null = null; let stylesheets: Set | null = null; @@ -322,7 +322,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, {root, blocks, sty break; } case "change": { - const updated = await readMarkdown(path, root, blocks); + const updated = await readMarkdown(path, root, echo); if (current.parse.hash === updated.parse.hash) break; const updatedStylesheets = await getStylesheets(updated.parse); for (const href of difference(stylesheets, updatedStylesheets)) send({type: "remove-stylesheet", href}); @@ -344,7 +344,7 @@ function handleWatch(socket: WebSocket, req: IncomingMessage, {root, blocks, sty if (!(path = normalize(path)).startsWith("/")) throw new Error("Invalid path: " + initialPath); if (path.endsWith("/")) path += "index"; path += ".md"; - current = await readMarkdown(path, root, blocks); + current = await readMarkdown(path, root, echo); if (current.parse.hash !== initialHash) return void send({type: "reload"}); stylesheets = await getStylesheets(current.parse); attachmentWatcher = await FileWatchers.of(root, path, getWatchPaths(current.parse), refreshAttachment); diff --git a/src/render.ts b/src/render.ts index 6c9aeb9c8..9a4578b96 100644 --- a/src/render.ts +++ b/src/render.ts @@ -23,11 +23,11 @@ export interface Render { export interface RenderOptions extends Config { root: string; path: string; - blocks: string | null; + echo: string | null; } export async function renderPreview(source: string, options: RenderOptions): Promise { - const parseResult = await parseMarkdown(source, options.root, options.path, options.blocks); + const parseResult = await parseMarkdown(source, options.root, options.path, options.echo); return { html: await render(parseResult, {...options, preview: true}), files: parseResult.files, @@ -37,7 +37,7 @@ export async function renderPreview(source: string, options: RenderOptions): Pro } export async function renderServerless(source: string, options: RenderOptions): Promise { - const parseResult = await parseMarkdown(source, options.root, options.path, options.blocks); + const parseResult = await parseMarkdown(source, options.root, options.path, options.echo); return { html: await render(parseResult, options), files: parseResult.files, diff --git a/test/config-test.ts b/test/config-test.ts index b98c6a963..f2394098d 100644 --- a/test/config-test.ts +++ b/test/config-test.ts @@ -21,7 +21,7 @@ describe("readConfig(undefined, root)", () => { pager: true, footer: 'Built with Observable on Jan 11, 2024.', - blocks: null, + echo: null, deploy: { workspace: "acme", project: "bi" @@ -39,7 +39,7 @@ describe("readConfig(undefined, root)", () => { pager: true, footer: 'Built with Observable on Jan 11, 2024.', - blocks: null, + echo: null, deploy: null }); }); diff --git a/test/input/build/blocks/fenced-code-options-with-blocks.md b/test/input/build/echo/fenced-code-options-with-echo.md similarity index 96% rename from test/input/build/blocks/fenced-code-options-with-blocks.md rename to test/input/build/echo/fenced-code-options-with-echo.md index 69c947c15..8cfa18054 100644 --- a/test/input/build/blocks/fenced-code-options-with-blocks.md +++ b/test/input/build/echo/fenced-code-options-with-echo.md @@ -1,5 +1,5 @@ --- -blocks: show +echo: true --- # Fenced code options diff --git a/test/input/build/blocks/fenced-code-options.md b/test/input/build/echo/fenced-code-options.md similarity index 100% rename from test/input/build/blocks/fenced-code-options.md rename to test/input/build/echo/fenced-code-options.md diff --git a/test/input/build/blocks/observablehq.config.ts b/test/input/build/echo/observablehq.config.ts similarity index 54% rename from test/input/build/blocks/observablehq.config.ts rename to test/input/build/echo/observablehq.config.ts index dbe67bf28..fce7dc401 100644 --- a/test/input/build/blocks/observablehq.config.ts +++ b/test/input/build/echo/observablehq.config.ts @@ -1,3 +1,3 @@ export default { - blocks: "show" + echo: "show" }; diff --git a/test/output/build/blocks/fenced-code-options-with-blocks.html b/test/output/build/echo/fenced-code-options-with-echo.html similarity index 98% rename from test/output/build/blocks/fenced-code-options-with-blocks.html rename to test/output/build/echo/fenced-code-options-with-echo.html index 31f208d24..390f5b42d 100644 --- a/test/output/build/blocks/fenced-code-options-with-blocks.html +++ b/test/output/build/echo/fenced-code-options-with-echo.html @@ -51,7 +51,7 @@
    - +
diff --git a/test/output/build/blocks/fenced-code-options.html b/test/output/build/echo/fenced-code-options.html similarity index 96% rename from test/output/build/blocks/fenced-code-options.html rename to test/output/build/echo/fenced-code-options.html index 1ef91bda1..2ff764b85 100644 --- a/test/output/build/blocks/fenced-code-options.html +++ b/test/output/build/echo/fenced-code-options.html @@ -51,7 +51,7 @@
    - +
@@ -81,7 +81,7 @@

diff --git a/test/output/fenced-code-options-with-blocks.html b/test/output/fenced-code-options-with-blocks.html index 3f0b41a3b..4f8f13b23 100644 --- a/test/output/fenced-code-options-with-blocks.html +++ b/test/output/fenced-code-options-with-blocks.html @@ -4,12 +4,8 @@

return a + b; } -
-
function bareJs() {
-  return 1;
-}
-
-
+
+
function langOutside() {
   return 1;
 }
diff --git a/test/output/fenced-code-options-with-blocks.json b/test/output/fenced-code-options-with-blocks.json
index 2109a8c0c..328a9e93f 100644
--- a/test/output/fenced-code-options-with-blocks.json
+++ b/test/output/fenced-code-options-with-blocks.json
@@ -26,7 +26,7 @@
       "cellIds": [
         "4bf815ab"
       ],
-      "html": "
\n
function bareJs() {\n  return 1;\n}\n
\n
" + "html": "
\n" }, { "type": "html", From 3cf37dec68e3aa7c8b8de5407fa8e7dfea1fab86 Mon Sep 17 00:00:00 2001 From: Robert Harris Date: Mon, 29 Jan 2024 10:26:56 -0800 Subject: [PATCH 13/13] remove console.log --- src/markdown.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/markdown.ts b/src/markdown.ts index 807e84bef..3b1ad11bc 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -105,7 +105,6 @@ function getLiveSource(content: string, tag: string): string | undefined { } function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string, echo: boolean): RenderRule { - console.log({makeFenceRendererEcho: echo}); return (tokens, idx, options, context: ParseContext, self) => { const token = tokens[idx]; const {tag, attributes} = parseInfo(token.info);