This commit is contained in:
Max Richter
2025-09-25 17:28:59 +02:00
parent b13d5015f4
commit 96e7f72d1f
13 changed files with 336 additions and 123 deletions

View File

@@ -1,22 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
export interface Window {
Go: {
new(): {
run: (inst: WebAssembly.Instance) => Promise<void>;
importObject: WebAssembly.Imports;
};
};
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export { };
export {};

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/wasm_exec.js"></script>
<script src="/wasm_exec.js"></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { Snippet } from 'svelte';
interface Props {
title: string;
@@ -7,26 +7,92 @@
placeholder?: string;
readonly?: boolean;
children?: Snippet;
headerActions?: Snippet;
subtitle?: string;
status?: 'success' | 'error';
timing?: number;
}
let { title, value = $bindable(), placeholder = "", readonly = false, children }: Props = $props();
let {
title,
value = $bindable(),
placeholder = '',
readonly = false,
children,
headerActions,
subtitle,
status,
timing
}: Props = $props();
</script>
<div class="flex flex-col h-full border-r border-gray-200 last:border-r-0">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50/50">
<h2 class="font-semibold text-gray-900 text-sm uppercase tracking-wide">{title}</h2>
<div class="flex h-full flex-col border-r border-gray-200 last:border-r-0">
<div class="flex items-center border-b border-gray-200 bg-gray-50/50 px-4 py-3">
{#if status === 'success'}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-5 w-5 text-green-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{:else if status === 'error'}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-5 w-5 text-red-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/if}
<div class="flex-1">
<h2 class="text-sm font-semibold tracking-wide text-gray-900 uppercase">{title}</h2>
{#if subtitle}
<p class="text-xs text-gray-500">{subtitle}</p>
{/if}
</div>
{#if headerActions}
<div class="ml-4">
{@render headerActions()}
</div>
{/if}
{#if timing !== undefined}
<div class="ml-4 text-xs text-gray-500">{timing}ms</div>
{/if}
</div>
<div class="flex-1 relative group">
<div class="group relative flex-1">
{#if readonly}
<div class="absolute inset-0 p-4 overflow-auto">
<pre class="text-sm font-mono text-gray-800 whitespace-pre-wrap leading-relaxed">{value}</pre>
<div class="absolute inset-0 overflow-auto p-4">
<pre
class="font-mono text-sm leading-relaxed whitespace-pre-wrap text-gray-800">{value}</pre>
</div>
{:else}
<textarea bind:value {placeholder} class="absolute inset-0 w-full h-full p-4 text-sm font-mono resize-none border-0 outline-none bg-transparent text-gray-800 leading-relaxed placeholder:text-gray-400 focus:bg-gray-50/30 transition-colors" spellcheck="false"></textarea>
<textarea
bind:value
{placeholder}
class="absolute inset-0 h-full w-full resize-none border-0 bg-transparent p-4 font-mono text-sm leading-relaxed text-gray-800 transition-colors outline-none placeholder:text-gray-400 focus:bg-gray-50/30"
spellcheck="false"
></textarea>
{/if}
<!-- Subtle hover effect -->
<div class="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-5 bg-black transition-opacity duration-200"></div>
<div
class="pointer-events-none absolute inset-0 bg-black opacity-0 transition-opacity duration-200 group-hover:opacity-5"
></div>
</div>
{#if children}

View File

@@ -1,24 +1,9 @@
<script lang="ts">
import EditorPanel from './EditorPanel.svelte';
import type { ParseResult } from '../wasm';
// Sample data based on the provided example
let templateValue = $state(`---
_type: Recipe
author.name: Max Richter
---
# Baguette
My favourite baguette recipe
## Ingredients
- Flour
- Water
- Salt
## Steps
1. Mix Flour Water and Salt
2. Bake the bread`);
type StringArray = string[];
let templateValue = $state(``);
let templates = $state<StringArray>([]);
let markdownValue = $state(`---
_type: Recipe
@@ -38,25 +23,46 @@ My favourite baguette recipe
1. Mix Flour Water and Salt
2. Bake the bread`);
// Simulated JSON output - in real implementation this would be generated from the parser
let jsonOutput = $derived(() => {
let jsonOutput = $state('');
let timings = $state<ParseResult['timings'] | null>(null);
let status = $state<'success' | 'error' | undefined>(undefined);
$effect(() => {
if ($wasmReady) {
try {
templates = listTemplates();
} catch (e) {
console.error(e);
}
}
if (!$wasmReady) {
jsonOutput = 'Loading wasm...';
timings = null;
status = undefined;
return;
}
try {
const output = {
_type: 'Recipe',
name: 'Baguette',
author: {
_type: 'Person',
name: 'Max Richter'
},
description: 'My favourite baguette recipe',
recipeIngredient: ['Flour', 'Water', 'Salt'],
recipeInstructions: ['Mix Flour Water and Salt', 'Bake the bread']
};
return JSON.stringify(output, null, 2);
} catch (e) {
return 'Invalid input';
const result = templateValue
? parseMarkdownWithTemplate(markdownValue, templateValue)
: parseMarkdown(markdownValue);
jsonOutput = JSON.stringify(result.data, null, 2);
timings = result.timings;
status = 'success';
} catch (e: unknown) {
jsonOutput = (e as Error).message;
timings = null;
status = 'error';
}
});
function loadTemplate(name: string) {
if (!name) return;
try {
templateValue = getTemplate(name);
} catch (e) {
console.error(e);
}
}
</script>
<div class="grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-3">
@@ -64,21 +70,37 @@ My favourite baguette recipe
title="Template"
bind:value={templateValue}
placeholder="Enter your Marka template here..."
/>
{status}
timing={timings?.template_compilation}
subtitle="Define your mapping schema"
>
{#snippet headerActions()}
<select
onchange={(e) => loadTemplate(e.currentTarget.value)}
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
>
<option value="">Load a template</option>
{#each templates as template (template)}
<option value={template}>{template}</option>
{/each}
</select>
{/snippet}
</EditorPanel>
<EditorPanel
title="Markdown"
bind:value={markdownValue}
placeholder="Enter your markdown content here..."
{status}
timing={timings?.markdown_parsing}
subtitle="Your source content"
/>
<EditorPanel title="Data" value={jsonOutput()} readonly={true}>
{#snippet children()}
<div class="absolute bottom-4 right-4 opacity-60">
<div class="rounded border bg-white px-2 py-1 text-xs text-gray-500">
Auto-generated JSON
</div>
</div>
{/snippet}
</EditorPanel>
<EditorPanel
title="Data"
value={jsonOutput}
readonly={true}
{status}
subtitle="Parsed JSON output"
/>
</div>

View File

@@ -0,0 +1,74 @@
import { readable } from 'svelte/store';
declare global {
interface Window {
Go: {
new (): {
run: (inst: WebAssembly.Instance) => Promise<void>;
importObject: WebAssembly.Imports;
};
};
markaParseFile: (input: string) => string;
markaParseFileWithTemplate: (markdown: string, template: string) => string;
markaListTemplates: () => string;
markaGetTemplate: (name: string) => string;
}
}
export const wasmReady = readable(false, (set) => {
if (typeof window === 'undefined') {
return;
}
const loadWasm = async () => {
const go = new window.Go();
try {
const result = await WebAssembly.instantiateStreaming(fetch('/main.wasm'), go.importObject);
go.run(result.instance);
set(true);
} catch (error) {
console.error('Error loading wasm module:', error);
}
};
if (document.readyState === 'complete') {
loadWasm();
} else {
window.addEventListener('load', loadWasm);
}
});
export interface ParseResult {
data: unknown;
timings: { [key: string]: number };
}
export function parseMarkdown(markdown: string): ParseResult {
if (typeof window.markaParseFile !== 'function') {
throw new Error('Wasm module not ready');
}
const result = window.markaParseFile(markdown);
return JSON.parse(result);
}
export function parseMarkdownWithTemplate(markdown: string, template: string): ParseResult {
if (typeof window.markaParseFileWithTemplate !== 'function') {
throw new Error('Wasm module not ready');
}
const result = window.markaParseFileWithTemplate(markdown, template);
return JSON.parse(result);
}
export function listTemplates(): string[] {
if (typeof window.markaListTemplates !== 'function') {
throw new Error('Wasm module not ready');
}
const result = window.markaListTemplates();
return JSON.parse(result);
}
export function getTemplate(name: string): string {
if (typeof window.markaGetTemplate !== 'function') {
throw new Error('Wasm module not ready');
}
return window.markaGetTemplate(name);
}

View File

@@ -7,6 +7,7 @@
<svelte:head>
<link rel="icon" href={favicon} />
<script src="/wasm_exec.js"></script>
</svelte:head>
{@render children?.()}

View File

@@ -1,13 +1,11 @@
<script lang="ts">
import Header from '$lib/components/Header.svelte';
import Playground from '$lib/components/Playground.svelte';
import StatusBar from '$lib/components/StatusBar.svelte';
</script>
<div class="bg-background text-foreground flex min-h-screen flex-col">
<Header />
<Playground />
<StatusBar />
</div>
<style>

View File

@@ -1,47 +0,0 @@
<script lang="ts">
import {onMount} from 'svelte';
const test = `---
_type: Recipe
author.name: Max Richter
---
# Baguette
My favourite baguette recipe
## Ingredients
- Flour
- Water
- Salt
## Steps
1. Mix Flour Water and Salt
2. Bake the bread
`
function exec(){
let start = performance.now();
const res = markaParseFile(test);
let end = performance.now();
console.log(JSON.parse(res))
start = performance.now();
const res2 = markaMatchBlocks(test);
end = performance.now();
console.log(JSON.parse(res2))
}
onMount(async () => {
const go = new Go();
WebAssembly.instantiateStreaming(fetch("/main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
});
</script>
<button onclick={exec}>test</button>