Skip to content

Commit 16139c5

Browse files
authored
chore: migrate out smart cell logic to its own package (#6808)
Internal refactor to help share code with vscode-extension for our smart cells
1 parent 970dbdd commit 16139c5

31 files changed

+2045
-683
lines changed

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@marimo-team/codemirror-sql": "^0.2.3",
5656
"@marimo-team/llm-info": "workspace:*",
5757
"@marimo-team/marimo-api": "workspace:*",
58+
"@marimo-team/smart-cells": "workspace:*",
5859
"@marimo-team/react-slotz": "^0.2.0",
5960
"@mui/material": "^6.5.0",
6061
"@open-rpc/client-js": "^1.8.1",

frontend/src/core/codemirror/language/__tests__/ast.test.ts

Lines changed: 0 additions & 124 deletions
This file was deleted.

frontend/src/core/codemirror/language/__tests__/extension.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,17 +174,25 @@ describe("switchLanguage", () => {
174174
// Switch to sql
175175
switchLanguage(mockEditor, { language: "sql", keepCodeAsIs: false });
176176
expect(mockEditor.state.doc.toString()).toMatchInlineSnapshot(`
177-
"print('Hello')
178-
print('Goodbye')"
177+
"mo.md(
178+
r"""
179+
print('Hello')
180+
print('Goodbye')
181+
"""
182+
)"
179183
`);
180184

181185
// Switch back to python
182186
switchLanguage(mockEditor, { language: "python", keepCodeAsIs: false });
183187
expect(mockEditor.state.doc.toString()).toMatchInlineSnapshot(`
184188
"_df = mo.sql(
185189
f"""
190+
mo.md(
191+
r\\"""
186192
print('Hello')
187193
print('Goodbye')
194+
\\"""
195+
)
188196
"""
189197
)"
190198
`);

frontend/src/core/codemirror/language/languages/markdown.ts

Lines changed: 16 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { languages } from "@codemirror/language-data";
88
import { stexMath } from "@codemirror/legacy-modes/mode/stex";
99
import type { Extension } from "@codemirror/state";
1010
import { type EditorView, ViewPlugin } from "@codemirror/view";
11-
import dedent from "string-dedent";
11+
import {
12+
type MarkdownMetadata,
13+
MarkdownParser,
14+
} from "@marimo-team/smart-cells";
1215
import type { CellId } from "@/core/cells/ids";
1316
import type { CompletionConfig } from "@/core/config/config-schema";
1417
import type { HotkeyProvider } from "@/core/hotkeys/hotkeys";
@@ -22,183 +25,43 @@ import { parsePython } from "../embedded/embedded-python";
2225
import { parseLatex } from "../embedded/latex";
2326
import { languageMetadataField } from "../metadata";
2427
import type { LanguageAdapter } from "../types";
25-
import {
26-
QUOTE_PREFIX_KINDS,
27-
type QuotePrefixKind,
28-
splitQuotePrefix,
29-
} from "../utils/quotes";
30-
31-
export interface MarkdownLanguageAdapterMetadata {
32-
quotePrefix: QuotePrefixKind;
33-
}
3428

35-
const quoteKinds = [
36-
['"""', '"""'],
37-
["'''", "'''"],
38-
['"', '"'],
39-
["'", "'"],
40-
];
41-
42-
// explode into all combinations
43-
const pairs = QUOTE_PREFIX_KINDS.flatMap((prefix) =>
44-
quoteKinds.map(([start, end]) => [prefix + start, end]),
45-
);
46-
47-
const regexes = pairs.map(
48-
([start, end]) =>
49-
// mo.md( + any number of spaces + start + capture + any number of spaces + end)
50-
[
51-
start,
52-
new RegExp(`^mo\\.md\\(\\s*${start}(.*)${end}\\s*\\)$`, "s"),
53-
] as const,
54-
);
29+
export interface MarkdownLanguageAdapterMetadata extends MarkdownMetadata {}
5530

5631
/**
5732
* Language adapter for Markdown.
5833
*/
5934
export class MarkdownLanguageAdapter
6035
implements LanguageAdapter<MarkdownLanguageAdapterMetadata>
6136
{
37+
private parser = new MarkdownParser();
38+
6239
readonly type = "markdown";
63-
readonly defaultCode = 'mo.md(r"""\n""")';
64-
readonly defaultMetadata: MarkdownLanguageAdapterMetadata = {
65-
quotePrefix: "r",
66-
};
40+
readonly defaultCode = this.parser.defaultCode;
41+
readonly defaultMetadata: MarkdownLanguageAdapterMetadata =
42+
this.parser.defaultMetadata;
6743

6844
static fromMarkdown(markdown: string) {
69-
return `mo.md(r"""\n${markdown}\n""")`;
45+
return MarkdownParser.fromMarkdown(markdown);
7046
}
7147

7248
transformIn(
7349
pythonCode: string,
7450
): [string, number, MarkdownLanguageAdapterMetadata] {
75-
pythonCode = pythonCode.trim();
76-
77-
const metadata = { ...this.defaultMetadata };
78-
79-
// empty string
80-
if (pythonCode === "") {
81-
return ["", 0, metadata];
82-
}
83-
84-
for (const [start, regex] of regexes) {
85-
const match = pythonCode.match(regex);
86-
if (match) {
87-
const innerCode = match[1];
88-
const [quotePrefix, quoteType] = splitQuotePrefix(start);
89-
metadata.quotePrefix = quotePrefix;
90-
const unescapedCode = innerCode.replaceAll(`\\${quoteType}`, quoteType);
91-
92-
const offset = pythonCode.indexOf(innerCode);
93-
// string-dedent expects the first and last line to be empty / contain only whitespace, so we pad with \n
94-
return [dedent(`\n${unescapedCode}\n`).trim(), offset, metadata];
95-
}
96-
}
97-
98-
// no match
99-
return [pythonCode, 0, metadata];
51+
const result = this.parser.transformIn(pythonCode);
52+
return [result.code, result.offset, result.metadata];
10053
}
10154

10255
transformOut(
10356
code: string,
10457
metadata: MarkdownLanguageAdapterMetadata,
10558
): [string, number] {
106-
// NB. Must be kept consistent with marimo/_convert/utils.py
107-
// ::markdown_to_marimo
108-
109-
// Empty string
110-
if (code === "") {
111-
// Need at least a space, otherwise the output will be 6 quotes
112-
code = " ";
113-
}
114-
115-
const { quotePrefix } = metadata;
116-
117-
// We always transform back with triple quotes, as to avoid needing to
118-
// escape single quotes.
119-
const escapedCode = code.replaceAll('"""', String.raw`\"""`);
120-
121-
// If its one line and not bounded by quotes, write it as single line
122-
const isOneLine = !code.includes("\n");
123-
const boundedByQuote = code.startsWith('"') || code.endsWith('"');
124-
if (isOneLine && !boundedByQuote) {
125-
const start = `mo.md(${quotePrefix}"""`;
126-
const end = `""")`;
127-
return [start + escapedCode + end, start.length];
128-
}
129-
130-
// Multiline code
131-
const start = `mo.md(\n ${quotePrefix}"""\n`;
132-
const end = `\n"""\n)`;
133-
return [start + escapedCode + end, start.length + 1];
59+
const result = this.parser.transformOut(code, metadata);
60+
return [result.code, result.offset];
13461
}
13562

13663
isSupported(pythonCode: string): boolean {
137-
pythonCode = pythonCode.trim();
138-
139-
// Empty strings are supported
140-
if (pythonCode === "") {
141-
return true;
142-
}
143-
144-
// Must start with mo.md(
145-
if (!pythonCode.startsWith("mo.md(")) {
146-
return false;
147-
}
148-
149-
// Empty function calls are supported
150-
if (pythonCode === "mo.md()") {
151-
return true;
152-
}
153-
154-
// Parse the code using Lezer and check for the exact match of mo.md() signature
155-
const tree = pythonLanguage.parser.parse(pythonCode);
156-
157-
// This is the exact match of mo.md() signature
158-
const enterOrder: { match: string | RegExp; stop?: boolean }[] = [
159-
{ match: "Script" },
160-
{ match: "ExpressionStatement" },
161-
{ match: "CallExpression" },
162-
{ match: "MemberExpression" },
163-
{ match: "VariableName" },
164-
{ match: "." },
165-
{ match: "PropertyName" },
166-
{ match: "ArgList" },
167-
{ match: "(" },
168-
{ match: /String|FormatString/, stop: true },
169-
{ match: ")" },
170-
];
171-
172-
let isValid = true;
173-
174-
// Parse the code using Lezer to check for multiple function calls and string content
175-
tree.iterate({
176-
enter: (node) => {
177-
const current = enterOrder.shift();
178-
if (current === undefined) {
179-
// If our list is empty, but we are still going
180-
// then this is not a valid call
181-
isValid = false;
182-
return false;
183-
}
184-
185-
const match = current.match;
186-
187-
if (typeof match === "string") {
188-
isValid = isValid && match === node.name;
189-
return isValid && !current.stop;
190-
}
191-
192-
if (!match.test(node.name)) {
193-
isValid = false;
194-
return isValid && !current.stop;
195-
}
196-
197-
return isValid && !current.stop;
198-
},
199-
});
200-
201-
return isValid;
64+
return this.parser.isSupported(pythonCode);
20265
}
20366

20467
getExtension(

0 commit comments

Comments
 (0)