Skip to content

Commit

Permalink
Add Python mathdsl to web example
Browse files Browse the repository at this point in the history
  • Loading branch information
niklaskorz committed Sep 11, 2023
1 parent d29997c commit 14905d1
Show file tree
Hide file tree
Showing 38 changed files with 1,357 additions and 25 deletions.
5 changes: 4 additions & 1 deletion apps/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
"prebuild": "npm run generate-dsl-declarations",
"build": "vite build",
"dev": "npm run prebuild && vite",
"generate-dsl-declarations": "dts-bundle-generator -o public/dsl.d.ts src/dsl/index.ts",
"generate-dsl-declarations": "dts-bundle-generator -o public/dsl.d.ts src/ts/dsl/index.ts",
"lint": "svelte-check --tsconfig ./tsconfig.json",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/autocomplete": "6",
"@codemirror/commands": "6",
"@codemirror/lang-python": "6",
"@codemirror/language": "6",
"@codemirror/lint": "6",
"@codemirror/state": "6",
"@codemirror/theme-one-dark": "6",
Expand All @@ -25,6 +27,7 @@
"@puredit/utils": "*",
"@replit/codemirror-indentation-markers": "6",
"codemirror": "6",
"mathlive": "^0.95.5",
"normalize.css": "^8.0.1",
"svelte-dark-mode": "^2.1.0"
},
Expand Down
27 changes: 11 additions & 16 deletions apps/example/public/examples/example.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
from dsl import db
import mathdsl

def x(input, name):
print("Hello world!")
if True:
while 2 < 1:
with db.change("rooms") as table:
table.column("lastName").replace("<target>", "<value>")
table.column("firstName").trim("both")
# Math term transformed into function
f, args = mathdsl.compile("20\\pi^2+\\sin x")
print("f(x):", f(x=90))

with db.change("students") as table:
table.column("name").replace("Mister", "Mr.");
table.column("lastName").trim("right");
# Math term with a matrix
rotation, args = mathdsl.compile("\\begin{pmatrix}\\cos\\theta & -\\sin\\theta \\\\ \\sin\\theta & \\cos\\theta\\end{pmatrix}")
print("rotation(theta):", rotation(theta=0.5))

y = 42
y + 10

if y > 42:
x = 3
# Math term evaluated immediately, using variables from local scope
r = 5
x = mathdsl.evaluate("r^r", locals())
print("x:", x)
14 changes: 14 additions & 0 deletions apps/example/public/examples/mathdsl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import sympy
import latex2sympy2


def compile(latex):
symbolic = latex2sympy2.latex2sympy(latex)
simplified = sympy.simplify(symbolic)
args = tuple(simplified.free_symbols)
return sympy.lambdify(args, simplified, "numpy"), args


def evaluate(latex, scope):
f, args = compile(latex)
return f(**{symbol.name: scope[symbol.name] for symbol in args})
51 changes: 49 additions & 2 deletions apps/example/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
<script lang="ts">
import Editor from "./Editor.svelte";
import TypescriptEditor from "./ts/Editor.svelte";
import PythonEditor from "./py/Editor.svelte";
let activeTab: "ts" | "py" = "ts";
function selectTypescript() {
activeTab = "ts";
}
function selectPython() {
activeTab = "py";
}
</script>

<main>
<Editor />
<ul class="tab-list">
<li class={activeTab === "ts" ? "active" : ""} on:click={selectTypescript}>

Check warning on line 18 in apps/example/src/App.svelte

View workflow job for this annotation

GitHub Actions / Deploy

A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event
TypeScript
</li>
<li class={activeTab === "py" ? "active" : ""} on:click={selectPython}>

Check warning on line 21 in apps/example/src/App.svelte

View workflow job for this annotation

GitHub Actions / Deploy

A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event
Python
</li>
</ul>
{#if activeTab === "ts"}
<TypescriptEditor />
{:else if activeTab === "py"}
<PythonEditor />
{/if}
</main>

<style>
Expand All @@ -25,4 +48,28 @@
--mono-font: "JetBrains Mono", "SF Mono", "Menlo", "Consolas", "Monaco",
"Courier New", monospace;
}
.tab-list {
font-family: var(--system-font);
display: flex;
list-style: none;
background-color: #111;
color: #fff;
margin: 0;
padding: 0 10px;
font-size: 0.8em;
& > li {
padding: 10px 10px;
border: 1px solid transparent;
cursor: pointer;
&:hover {
border-color: #fff;
}
&.active {
text-decoration: underline;
}
}
}
</style>
120 changes: 120 additions & 0 deletions apps/example/src/py/Editor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script lang="ts">
import DarkMode from "svelte-dark-mode";
import type { Theme } from "svelte-dark-mode/types/DarkMode.svelte";
import { basicSetup } from "codemirror";
import { EditorState, Annotation, Compartment } from "@codemirror/state";
import type { Extension } from "@codemirror/state";
import { EditorView, keymap } from "@codemirror/view";
import { indentWithTab } from "@codemirror/commands";
import { autocompletion } from "@codemirror/autocomplete";
import { indentUnit } from "@codemirror/language";
import { python } from "@codemirror/lang-python";
import { onDestroy, onMount } from "svelte";
import { example } from "./code";
import { projectionPlugin, completions } from "@puredit/projections";
import { oneDark } from "@codemirror/theme-one-dark";
import { indentationMarkers } from "@replit/codemirror-indentation-markers";
import { projectionPluginConfig } from "./projections";
let theme: Theme | undefined;
let container: HTMLDivElement;
let projectionalEditor: EditorView | undefined;
let codeEditor: EditorView | undefined;
const syncChangeAnnotation = Annotation.define<boolean>();
const darkThemeCompartment = new Compartment();
onMount(() => {
const extensions: Extension[] = [
basicSetup,
indentUnit.of(" "), // 4 spaces for Python
keymap.of([indentWithTab]),
darkThemeCompartment.of(theme === "dark" ? oneDark : []),
indentationMarkers(),
EditorView.theme({
".cm-scroller": {
fontFamily: "var(--mono-font, monospace)",
fontSize: "14px",
},
".cm-tooltip": {
fontFamily: "var(--system-font, sans-serif)",
},
}),
python(),
];
const projectionalEditorExtensions = extensions.concat([
projectionPlugin(projectionPluginConfig),
autocompletion({
activateOnTyping: true,
override: [completions],
}),
]);
const codeEditorExtensions = extensions;
projectionalEditor = new EditorView({
state: EditorState.create({
doc: example,
extensions: projectionalEditorExtensions,
}),
parent: container,
dispatch(tr) {
projectionalEditor!.update([tr]);
if (!tr.changes.empty && !tr.annotation(syncChangeAnnotation)) {
codeEditor!.dispatch({
changes: tr.changes,
annotations: syncChangeAnnotation.of(true),
filter: false,
});
}
},
});
codeEditor = new EditorView({
state: EditorState.create({
doc: example,
extensions: codeEditorExtensions,
}),
parent: container,
dispatch(tr) {
codeEditor!.update([tr]);
if (!tr.changes.empty && !tr.annotation(syncChangeAnnotation)) {
projectionalEditor!.dispatch({
changes: tr.changes,
annotations: syncChangeAnnotation.of(true),
filter: false,
});
}
},
});
});
onDestroy(() => {
projectionalEditor?.destroy();
codeEditor?.destroy();
});
function onThemeChange(theme?: Theme) {
const transaction = {
effects: [
darkThemeCompartment.reconfigure(theme === "dark" ? oneDark : []),
],
};
projectionalEditor?.dispatch(transaction);
codeEditor?.dispatch(transaction);
}
// Dynamically update color scheme on theme change
$: onThemeChange(theme);
</script>

<DarkMode bind:theme />

<div class="container" bind:this={container} />

<style>
.container {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 50% 50%;
}
</style>
6 changes: 6 additions & 0 deletions apps/example/src/py/code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
async function fetchText(url: string): Promise<string> {
const resp = await fetch(url);
return resp.text();
}

export const example = await fetchText("/examples/example.py");
82 changes: 82 additions & 0 deletions apps/example/src/py/projections/EquationEditor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script type="ts">
import type { EditorState } from "@codemirror/state";
import type { EditorView } from "@codemirror/view";
import type { SyntaxNode } from "@puredit/parser";
import type { FocusGroup } from "@puredit/projections/focus";
import {
stringLiteralValue,
stringLiteralValueChange,
} from "@puredit/projections/shared";
import { MathfieldElement } from "mathlive";
import { onDestroy, onMount } from "svelte";
export let view: EditorView | null;
export let node: SyntaxNode;
export let state: EditorState;
export let focusGroup: FocusGroup | null = null;
let target: HTMLElement;
MathfieldElement.fontsDirectory =
"https://unpkg.com/[email protected]/dist/fonts";
MathfieldElement.soundsDirectory =
"https://unpkg.com/[email protected]/dist/sounds";
const mfe = new MathfieldElement();
function updateMathfield(value: string) {
if (!mfe.hasFocus()) {
mfe.setValue(value, { silenceNotifications: true });
}
}
$: updateMathfield(stringLiteralValue(node, state.doc));
mfe.addEventListener("input", () => {
view?.dispatch({
filter: false,
changes: stringLiteralValueChange(node, mfe.value),
});
});
mfe.addEventListener("move-out", (e) => {
switch (e.detail.direction) {
case "forward":
case "downward":
focusGroup.next(mfe);
break;
case "backward":
case "upward":
focusGroup.previous(mfe);
break;
}
});
onMount(() => {
target.appendChild(mfe);
mfe.inlineShortcuts = {
...mfe.inlineShortcuts,
matrix: "\\begin{pmatrix} \\end{pmatrix}",
col: "&",
row: "\\\\",
};
});
$: if (mfe && focusGroup) {
focusGroup.registerElement(mfe);
}
onDestroy(() => {
if (mfe && focusGroup) {
focusGroup.unregisterElement(mfe);
}
});
</script>
<span bind:this={target} />
<!-- svelte-ignore css-unused-selector -->
<style global>
math-field {
display: inline-flex;
font-size: 1.2em;
}
</style>
27 changes: 27 additions & 0 deletions apps/example/src/py/projections/MathProjection.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import type { EditorState } from "@codemirror/state";
import type { EditorView } from "@codemirror/view";
import type { Match } from "@puredit/parser";
import type { FocusGroup } from "@puredit/projections/focus";
import { onMount } from "svelte";
import EquationEditor from "./EquationEditor.svelte";
export let isNew: boolean;
export let view: EditorView | null;
export let match: Match;
// svelte-ignore unused-export-let
export let context: object;
export let state: EditorState;
export let focusGroup: FocusGroup;
onMount(() => {
if (isNew) {
requestAnimationFrame(() => {
focusGroup.first();
});
}
});
</script>

<EquationEditor node={match.args.latex} {state} {view} {focusGroup} />
24 changes: 24 additions & 0 deletions apps/example/src/py/projections/compileMathProjection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { arg, contextVariable } from "@puredit/parser";
import { svelteProjection } from "@puredit/projections/svelte";
import type { Projection } from "@puredit/projections/types";
import MathProjection from "./MathProjection.svelte";
import { pythonParser } from "./parser";

const dsl = contextVariable("mathdsl");
const latex = arg("latex", "string");

export const [pattern, draft] = pythonParser.expressionPattern`
${dsl}.compile(${latex})
`;

export const widget = svelteProjection(MathProjection);

export const compileMathProjection: Projection = {
name: "compile math",
description:
"Transforms an expression in mathematical notation into a reusable functions, using free symbols as named parameters.",
pattern,
draft,
requiredContextVariables: [],
widgets: [widget],
};
Loading

0 comments on commit 14905d1

Please sign in to comment.