Skip to content

Commit be2d613

Browse files
authored
Fix snippet renderer on Cloudflare SSR (#19)
1 parent e0becd2 commit be2d613

2 files changed

Lines changed: 169 additions & 20 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, test } from 'bun:test';
2+
import { renderLabChainSnippet } from './lab-chain-snippet.server';
3+
4+
describe('renderLabChainSnippet', () => {
5+
test('returns null for non-chain code', async () => {
6+
expect(await renderLabChainSnippet('const x = 1;')).toBeNull();
7+
});
8+
9+
test('renders chain bodies with nested template expressions', async () => {
10+
const code = `const out = await lab.runChain([
11+
{
12+
name: "Nested",
13+
body: \`const label = \${input.ok ? \`ok-\${input.id}\` : "nope"};
14+
return { label };\`,
15+
capabilities: []
16+
}
17+
]);`;
18+
19+
const html = await renderLabChainSnippet(code);
20+
21+
expect(html).toContain('ok-');
22+
expect(html).toContain('return');
23+
expect(html).toContain('shiki-code-block');
24+
});
25+
});

src/lib/lab-chain-snippet.server.ts

Lines changed: 144 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import ts from 'typescript';
21
import {
32
highlightCode,
43
renderTokenizedCode,
@@ -19,34 +18,159 @@ type TokenSpan = {
1918
fontStyle?: number;
2019
};
2120

22-
function propertyNameText(name: ts.PropertyName): string | null {
23-
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
24-
return name.text;
21+
function isIdentifierStart(char: string): boolean {
22+
return /[A-Za-z_$]/.test(char);
23+
}
24+
25+
function isIdentifierChar(char: string): boolean {
26+
return /[A-Za-z0-9_$]/.test(char);
27+
}
28+
29+
function skipString(code: string, start: number, quote: "'" | '"'): number {
30+
let index = start + 1;
31+
while (index < code.length) {
32+
if (code[index] === '\\') {
33+
index += 2;
34+
continue;
35+
}
36+
if (code[index] === quote) return index + 1;
37+
index += 1;
38+
}
39+
return index;
40+
}
41+
42+
function skipLineComment(code: string, start: number): number {
43+
let index = start + 2;
44+
while (index < code.length && code[index] !== '\n') {
45+
index += 1;
46+
}
47+
return index;
48+
}
49+
50+
function skipBlockComment(code: string, start: number): number {
51+
let index = start + 2;
52+
while (index < code.length - 1) {
53+
if (code[index] === '*' && code[index + 1] === '/') return index + 2;
54+
index += 1;
55+
}
56+
return code.length;
57+
}
58+
59+
function skipTrivia(code: string, start: number): number {
60+
let index = start;
61+
while (index < code.length) {
62+
if (/\s/.test(code[index])) {
63+
index += 1;
64+
continue;
65+
}
66+
if (code[index] === '/' && code[index + 1] === '/') {
67+
index = skipLineComment(code, index);
68+
continue;
69+
}
70+
if (code[index] === '/' && code[index + 1] === '*') {
71+
index = skipBlockComment(code, index);
72+
continue;
73+
}
74+
return index;
75+
}
76+
return index;
77+
}
78+
79+
function skipTemplateExpression(code: string, start: number): number {
80+
let depth = 1;
81+
let index = start;
82+
while (index < code.length) {
83+
const char = code[index];
84+
if (char === "'" || char === '"') {
85+
index = skipString(code, index, char);
86+
continue;
87+
}
88+
if (char === '`') {
89+
index = skipTemplateLiteral(code, index);
90+
continue;
91+
}
92+
if (char === '/' && code[index + 1] === '/') {
93+
index = skipLineComment(code, index);
94+
continue;
95+
}
96+
if (char === '/' && code[index + 1] === '*') {
97+
index = skipBlockComment(code, index);
98+
continue;
99+
}
100+
if (char === '{') depth += 1;
101+
if (char === '}') {
102+
depth -= 1;
103+
if (depth === 0) return index + 1;
104+
}
105+
index += 1;
25106
}
26-
return null;
107+
return index;
108+
}
109+
110+
function skipTemplateLiteral(code: string, start: number): number {
111+
let index = start + 1;
112+
while (index < code.length) {
113+
if (code[index] === '\\') {
114+
index += 2;
115+
continue;
116+
}
117+
if (code[index] === '`') return index + 1;
118+
if (code[index] === '$' && code[index + 1] === '{') {
119+
index = skipTemplateExpression(code, index + 2);
120+
continue;
121+
}
122+
index += 1;
123+
}
124+
return index;
27125
}
28126

29127
function findBodyRanges(code: string): BodyRange[] {
30-
const sourceFile = ts.createSourceFile('snippet.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
31128
const ranges: BodyRange[] = [];
129+
let index = 0;
32130

33-
function visit(node: ts.Node) {
34-
if (
35-
ts.isPropertyAssignment(node) &&
36-
propertyNameText(node.name) === 'body' &&
37-
(ts.isNoSubstitutionTemplateLiteral(node.initializer) || ts.isTemplateExpression(node.initializer))
38-
) {
39-
const start = node.initializer.getStart(sourceFile) + 1;
40-
const end = node.initializer.getEnd() - 1;
41-
if (end > start) {
42-
ranges.push({ start, end });
43-
}
131+
while (index < code.length) {
132+
index = skipTrivia(code, index);
133+
if (index >= code.length) break;
134+
135+
const char = code[index];
136+
if (char === "'" || char === '"') {
137+
index = skipString(code, index, char);
138+
continue;
139+
}
140+
if (char === '`') {
141+
index = skipTemplateLiteral(code, index);
142+
continue;
143+
}
144+
if (!isIdentifierStart(char)) {
145+
index += 1;
146+
continue;
147+
}
148+
149+
const propertyStart = index;
150+
index += 1;
151+
while (index < code.length && isIdentifierChar(code[index])) {
152+
index += 1;
153+
}
154+
155+
if (code.slice(propertyStart, index) !== 'body') continue;
156+
157+
let valueStart = skipTrivia(code, index);
158+
if (code[valueStart] !== ':') continue;
159+
160+
valueStart = skipTrivia(code, valueStart + 1);
161+
if (code[valueStart] !== '`') {
162+
index = valueStart;
163+
continue;
164+
}
165+
166+
const templateEnd = skipTemplateLiteral(code, valueStart);
167+
if (templateEnd > valueStart + 1) {
168+
ranges.push({ start: valueStart + 1, end: templateEnd - 1 });
44169
}
45-
ts.forEachChild(node, visit);
170+
index = templateEnd;
46171
}
47172

48-
visit(sourceFile);
49-
return ranges.sort((a, b) => a.start - b.start);
173+
return ranges;
50174
}
51175

52176
function flattenTokens(lines: HighlightedToken[][], offsetShift = 0): TokenSpan[] {

0 commit comments

Comments
 (0)