From 2d3ffeb02197742f11c03a43b4db243dfa368f1c Mon Sep 17 00:00:00 2001
From: Mike Bostock <mbostock@gmail.com>
Date: Tue, 19 Dec 2023 19:52:38 -0800
Subject: [PATCH 1/3] DRY transpileJavaScript

---
 src/markdown.ts | 43 +++++++++++++++++--------------------------
 1 file changed, 17 insertions(+), 26 deletions(-)

diff --git a/src/markdown.ts b/src/markdown.ts
index 72fc03ab6..43f2f67ab 100644
--- a/src/markdown.ts
+++ b/src/markdown.ts
@@ -6,7 +6,7 @@ import equal from "fast-deep-equal";
 import matter from "gray-matter";
 import hljs from "highlight.js";
 import {parseHTML} from "linkedom";
-import MarkdownIt from "markdown-it";
+import MarkdownIt, {type Token} from "markdown-it";
 import {type RuleCore} from "markdown-it/lib/parser_core.js";
 import {type RuleInline} from "markdown-it/lib/parser_inline.js";
 import {type RenderRule, type default as Renderer} from "markdown-it/lib/renderer.js";
@@ -15,7 +15,7 @@ import {isEnoent} from "./error.js";
 import {fileReference, getLocalPath} from "./files.js";
 import {computeHash} from "./hash.js";
 import {parseInfo} from "./info.js";
-import type {FileReference, ImportReference, PendingTranspile, Transpile} from "./javascript.js";
+import type {FileReference, ImportReference, ParseOptions, PendingTranspile, Transpile} from "./javascript.js";
 import {transpileJavaScript} from "./javascript.js";
 import {transpileTag} from "./tag.js";
 import {resolvePath} from "./url.js";
@@ -101,6 +101,7 @@ function getLiveSource(content: string, tag: string): string | undefined {
 }
 
 function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: string): RenderRule {
+  const transform = makeJavaScriptTransformer(root, sourcePath);
   return (tokens, idx, options, context: ParseContext, self) => {
     const token = tokens[idx];
     const {tag, attributes} = parseInfo(token.info);
@@ -109,18 +110,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s
     let count = 0;
     const source = isFalse(attributes.run) ? undefined : getLiveSource(token.content, tag);
     if (source != null) {
-      const id = uniqueCodeId(context, token.content);
-      const sourceLine = context.startLine + context.currentLine;
-      const transpile = transpileJavaScript(source, {
-        id,
-        root,
-        sourcePath,
-        sourceLine
-      });
-      extendPiece(context, {code: [transpile]});
-      if (transpile.files) context.files.push(...transpile.files);
-      if (transpile.imports) context.imports.push(...transpile.imports);
-      result += `<div id="cell-${id}" class="observablehq observablehq--block"></div>\n`;
+      result += `<div id="cell-${transform(token, context)}" class="observablehq observablehq--block"></div>\n`;
       count++;
     }
     // TODO we could hide non-live code here with echo=false?
@@ -263,21 +253,22 @@ const transformPlaceholderCore: RuleCore = (state) => {
   state.tokens = output;
 };
 
-function makePlaceholderRenderer(root: string, sourcePath: string): RenderRule {
-  return (tokens, idx, options, context: ParseContext) => {
-    const id = uniqueCodeId(context, tokens[idx].content);
-    const token = tokens[idx];
-    const transpile = transpileJavaScript(token.content, {
-      id,
-      root,
-      sourcePath,
-      inline: true,
-      sourceLine: context.startLine + context.currentLine
-    });
+function makeJavaScriptTransformer(root: string, sourcePath: string, options: Partial<ParseOptions> = {}) {
+  return (token: Token, context: ParseContext): string => {
+    const id = uniqueCodeId(context, token.content);
+    const sourceLine = context.startLine + context.currentLine;
+    const transpile = transpileJavaScript(token.content, {id, root, sourcePath, sourceLine, ...options});
     extendPiece(context, {code: [transpile]});
     if (transpile.files) context.files.push(...transpile.files);
     if (transpile.imports) context.imports.push(...transpile.imports);
-    return `<span id="cell-${id}"></span>`;
+    return id;
+  };
+}
+
+function makePlaceholderRenderer(root: string, sourcePath: string): RenderRule {
+  const transform = makeJavaScriptTransformer(root, sourcePath, {inline: true});
+  return (tokens, idx, options, context: ParseContext) => {
+    return `<span id="cell-${transform(tokens[idx], context)}"></span>`;
   };
 }
 

From 4330749a964628fe76e06e95af9bf55b524867cc Mon Sep 17 00:00:00 2001
From: Mike Bostock <mbostock@gmail.com>
Date: Tue, 19 Dec 2023 19:53:19 -0800
Subject: [PATCH 2/3] template concat

---
 src/markdown.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/markdown.ts b/src/markdown.ts
index 43f2f67ab..cad813ad8 100644
--- a/src/markdown.ts
+++ b/src/markdown.ts
@@ -119,7 +119,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s
       count++;
     }
     // Tokens should always be rendered as a single block element.
-    if (count > 1) result = "<div>" + result + "</div>";
+    if (count > 1) result = `<div>${result}</div>`;
     return result;
   };
 }

From 5a0bfbcd741e6e5f402b4690ebc2f475d1380783 Mon Sep 17 00:00:00 2001
From: Mike Bostock <mbostock@gmail.com>
Date: Tue, 19 Dec 2023 20:00:49 -0800
Subject: [PATCH 3/3] eval ssr

---
 docs/ssr.md     |  5 +++++
 src/markdown.ts | 10 +++++++---
 2 files changed, 12 insertions(+), 3 deletions(-)
 create mode 100644 docs/ssr.md

diff --git a/docs/ssr.md b/docs/ssr.md
new file mode 100644
index 000000000..3a760d1f3
--- /dev/null
+++ b/docs/ssr.md
@@ -0,0 +1,5 @@
+# Server-side rendering
+
+```js echo server
+`<pre>hello ${process.env.USER}</pre>`
+```
diff --git a/src/markdown.ts b/src/markdown.ts
index cad813ad8..f9548d580 100644
--- a/src/markdown.ts
+++ b/src/markdown.ts
@@ -110,13 +110,17 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule, sourcePath: s
     let count = 0;
     const source = isFalse(attributes.run) ? undefined : getLiveSource(token.content, tag);
     if (source != null) {
-      result += `<div id="cell-${transform(token, context)}" class="observablehq observablehq--block"></div>\n`;
-      count++;
+      if (tag === "js" && (attributes.server === "" || attributes.server?.toLowerCase() === "true")) {
+        result += `<div>${eval(source)}</div>`;
+      } else {
+        result += `<div id="cell-${transform(token, context)}" class="observablehq observablehq--block"></div>\n`;
+      }
+      ++count;
     }
     // TODO we could hide non-live code here with echo=false?
     if (source == null || (attributes.echo != null && !isFalse(attributes.echo))) {
       result += baseRenderer(tokens, idx, options, context, self);
-      count++;
+      ++count;
     }
     // Tokens should always be rendered as a single block element.
     if (count > 1) result = `<div>${result}</div>`;