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

add options to show fenced block source #607

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions observablehq.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default {
title: "Observable CLI",
echo: false,
pages: [
{name: "Getting started", path: "/getting-started"},
{name: "Routing", path: "/routing"},
Expand Down
8 changes: 5 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface Config {
output: string; // defaults to dist
title?: string;
pages: (Page | Section)[]; // TODO rename to sidebar?
echo: null | string; // defaults to null
pager: boolean; // defaults to true
footer: string;
toc: TableOfContents;
Expand Down Expand Up @@ -60,7 +61,7 @@ async function readPages(root: string): Promise<Page[]> {
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);
Expand Down Expand Up @@ -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, echo = null} = spec;
if (title !== undefined) title = String(title);
pages = Array.from(pages, normalizePageOrSection);
pager = Boolean(pager);
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, pager, footer, toc, style, deploy};
return {root, output, title, pages, echo, pager, footer, toc, style, deploy};
}

function normalizeTheme(spec: any): string[] {
Expand Down
28 changes: 22 additions & 6 deletions src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,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, echo: boolean): RenderRule {
return (tokens, idx, options, context: ParseContext, self) => {
const token = tokens[idx];
const {tag, attributes} = parseInfo(token.info);
Expand All @@ -129,7 +129,8 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s
}"></div>\n`;
count++;
}
if (attributes.echo == null ? source == null : !isFalse(attributes.echo)) {
// Conditionally render fanced block source.
if (attributes.echo == null ? echo || source == null : !isFalse(attributes.echo)) {
result += baseRenderer(tokens, idx, options, context, self);
count++;
}
Expand Down Expand Up @@ -418,16 +419,31 @@ async function toParseCells(pieces: RenderPiece[]): Promise<CellPiece[]> {
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): Promise<ParseResult> {
export async function parseMarkdown(
source: string,
root: string,
sourcePath: string,
echo: null | string
): Promise<ParseResult> {
const parts = matter(source, {});
const md = MarkdownIt({html: true});
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,
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};
Expand Down Expand Up @@ -538,8 +554,8 @@ export function diffMarkdown({parse: prevParse}: ReadMarkdownResult, {parse: nex
.map(diffReducer);
}

export async function readMarkdown(path: string, root: string): Promise<ReadMarkdownResult> {
export async function readMarkdown(path: string, root: string, echo: null | string): Promise<ReadMarkdownResult> {
const contents = await readFile(join(root, path), "utf-8");
const parse = await parseMarkdown(contents, root, path);
const parse = await parseMarkdown(contents, root, path, echo);
return {contents, parse};
}
6 changes: 3 additions & 3 deletions src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, echo, style: defaultStyle}: Config) {
let path: string | null = null;
let current: ReadMarkdownResult | null = null;
let stylesheets: Set<string> | null = null;
Expand Down Expand Up @@ -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, 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});
Expand All @@ -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, 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);
Expand Down
5 changes: 3 additions & 2 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ export interface Render {
export interface RenderOptions extends Config {
root: string;
path: string;
echo: string | null;
}

export async function renderPreview(source: string, options: RenderOptions): Promise<Render> {
const parseResult = await parseMarkdown(source, options.root, options.path);
const parseResult = await parseMarkdown(source, options.root, options.path, options.echo);
return {
html: await render(parseResult, {...options, preview: true}),
files: parseResult.files,
Expand All @@ -36,7 +37,7 @@ export async function renderPreview(source: string, options: RenderOptions): Pro
}

export async function renderServerless(source: string, options: RenderOptions): Promise<Render> {
const parseResult = await parseMarkdown(source, options.root, options.path);
const parseResult = await parseMarkdown(source, options.root, options.path, options.echo);
return {
html: await render(parseResult, options),
files: parseResult.files,
Expand Down
2 changes: 2 additions & 0 deletions test/config-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("readConfig(undefined, root)", () => {
pager: true,
footer:
'Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="2024-01-11T01:02:03">Jan 11, 2024</a>.',
echo: null,
deploy: {
workspace: "acme",
project: "bi"
Expand All @@ -38,6 +39,7 @@ describe("readConfig(undefined, root)", () => {
pager: true,
footer:
'Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="2024-01-11T01:02:03">Jan 11, 2024</a>.',
echo: null,
deploy: null
});
});
Expand Down
39 changes: 39 additions & 0 deletions test/input/build/echo/fenced-code-options-with-echo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
echo: true
---

# 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
```
35 changes: 35 additions & 0 deletions test/input/build/echo/fenced-code-options.md
Original file line number Diff line number Diff line change
@@ -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
```
3 changes: 3 additions & 0 deletions test/input/build/echo/observablehq.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
echo: "show"
};
39 changes: 39 additions & 0 deletions test/input/fenced-code-options-with-blocks.md
Original file line number Diff line number Diff line change
@@ -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
```
2 changes: 1 addition & 1 deletion test/markdown-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
87 changes: 87 additions & 0 deletions test/output/build/echo/fenced-code-options-with-echo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Fenced code options</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight.css">
<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="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight.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="modulepreload" href="./_observablehq/client.js">
<link rel="modulepreload" href="./_observablehq/runtime.js">
<link rel="modulepreload" href="./_observablehq/stdlib.js">
<script type="module">

import {define} from "./_observablehq/client.js";

define({id: "fe9e095e", outputs: ["add"], body: () => {
function add(a, b) {
return a + b;
}
return {add};
}});

define({id: "4bf815ab", outputs: ["bareJs"], body: () => {
function bareJs() {
return 1;
}
return {bareJs};
}});

define({id: "d41a5631", outputs: ["langOutside"], body: () => {
function langOutside() {
return 1;
}
return {langOutside};
}});

define({id: "2f4eff36", outputs: ["langAndAttributes"], body: () => {
function langAndAttributes() {
return 1;
}
return {langAndAttributes};
}});

</script>
<input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar">
<label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label>
<nav id="observablehq-sidebar">
<ol>
<label id="observablehq-sidebar-close" for="observablehq-sidebar-toggle"></label>
<li class="observablehq-link"><a href="./">Home</a></li>
</ol>
<ol>
<li class="observablehq-link observablehq-link-active"><a href="./fenced-code-options-with-echo">Fenced code options</a></li>
<li class="observablehq-link"><a href="./fenced-code-options">Fenced code options</a></li>
</ol>
</nav>
<script>{/* redacted init script */}</script>
<aside id="observablehq-toc" data-selector="#observablehq-main h1:not(:first-of-type), #observablehq-main h2:not(h1 + h2)">
<nav>
</nav>
</aside>
<div id="observablehq-center">
<main id="observablehq-main" class="observablehq">
<h1 id="fenced-code-options" tabindex="-1"><a class="observablehq-header-anchor" href="#fenced-code-options">Fenced code options</a></h1>
<div><div id="cell-fe9e095e" class="observablehq observablehq--block"></div>
<pre data-language="js"><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">add</span>(<span class="hljs-params">a, b</span>) {
<span class="hljs-keyword">return</span> a + b;
}
</code></pre>
</div><div><div id="cell-4bf815ab" class="observablehq observablehq--block"></div>
<pre data-language="js"><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">bareJs</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
}
</code></pre>
</div><div><div id="cell-d41a5631" class="observablehq observablehq--block"></div>
<pre data-language="js"><code class="language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">langOutside</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
}
</code></pre>
</div><div id="cell-2f4eff36" class="observablehq observablehq--block"></div>
<span></span><span></span></main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a><a rel="next" href="./fenced-code-options"><span>Fenced code options</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="2024-01-10T16:00:00">Jan 10, 2024</a>.</div>
</footer>
</div>
Loading