Compare commits

...

4 Commits

Author SHA1 Message Date
Max Richter
2a1572f99d fix: most of the template blocks 2025-09-30 19:28:56 +02:00
Max Richter
d35f3e5e2e fix: some bug 2025-09-28 20:08:21 +02:00
Max Richter
57ea1f6e3e fix: trying to fix error in matching code 2025-09-28 14:40:29 +02:00
Max Richter
fb0b80be78 feat(playground): configure adapter static 2025-09-26 18:07:22 +02:00
29 changed files with 269 additions and 199 deletions

View File

@@ -20,21 +20,27 @@ func ParseBlock(input string, block template.Block) (any, error) {
case template.CodecHashtags:
return Keywords(input, block)
}
return nil, fmt.Errorf("unknown codec: %s", block.Codec)
fmt.Printf("%#v\n", block)
return nil, fmt.Errorf("unknown codec '%s'", block.Codec)
}
func Parse(matches []matcher.Block) (any, error) {
var result any
for _, m := range matches {
for i, m := range matches {
if m.Block.Path == "@index" {
continue
}
input := m.GetContent()
value, err := ParseBlock(input, m.Block)
var blockIdentifier any
blockIdentifier = m.Block.Path
if blockIdentifier == "" {
blockIdentifier = fmt.Sprintf("#%d", i)
}
if err != nil {
return nil, fmt.Errorf("failed to parse block(%s): %w", m.Block.Path, err)
return nil, fmt.Errorf("failed to parse block(%s) -> %w", blockIdentifier, err)
}
result = utils.SetPathValue(m.Block.Path, value, result)
}

View File

@@ -50,7 +50,6 @@ func MatchBlocksFuzzy(markdown string, templateBlocks []template.Block, maxDist
}
}
// Handle the last block
if len(templateBlocks) > 0 {
lastBlock := templateBlocks[len(templateBlocks)-1]
if lastBlock.Type == template.DataBlock {

View File

@@ -10,21 +10,21 @@ import (
"git.max-richter.dev/max/marka/testdata"
)
func TestFuzzyFindAll(t *testing.T) {
func TestMatch_FuzzyFindAll(t *testing.T) {
recipeMd := testdata.Read(t, "baguette/input.md")
tests := []struct {
Needle string
Start, End, StartIndex int
}{
{StartIndex: 0, Needle: "# Ingredients\n", Start: 77, End: 91},
{StartIndex: 0, Needle: "# Ingrdients\n", Start: 77, End: 91},
{StartIndex: 0, Needle: "# Inrdients\n", Start: 77, End: 91},
{StartIndex: 0, Needle: "## Ingredients\n", Start: 90, End: 105},
{StartIndex: 0, Needle: "## Ingrdients\n", Start: 90, End: 105},
{StartIndex: 0, Needle: "## Inrdients\n", Start: 90, End: 105},
{StartIndex: 0, Needle: "---\n", Start: 0, End: 4},
{StartIndex: 4, Needle: "---\n", Start: 29, End: 33},
{StartIndex: 0, Needle: "# Steps\n", Start: 116, End: 124},
{StartIndex: 0, Needle: "# Stps\n", Start: 116, End: 124},
{StartIndex: 0, Needle: "# Step\n", Start: 116, End: 124},
{StartIndex: 4, Needle: "---\n", Start: 43, End: 47},
{StartIndex: 0, Needle: "## Steps\n", Start: 129, End: 138},
{StartIndex: 0, Needle: "## Stps\n", Start: 129, End: 138},
{StartIndex: 0, Needle: "## Step\n", Start: 129, End: 138},
}
for _, test := range tests {
@@ -36,13 +36,14 @@ func TestFuzzyFindAll(t *testing.T) {
}
}
func TestFuzzyBlockMatch(t *testing.T) {
func TestMatch_FuzzyBlockBaguette(t *testing.T) {
recipeMd := testdata.Read(t, "baguette/input.md")
schemaMd, err := registry.GetTemplate("Recipe")
if err != nil {
t.Errorf("Failed to load template: %s", err.Error())
t.FailNow()
}
blocks, err := template.CompileTemplate(schemaMd)
if err != nil {
t.Errorf("Failed to compile template: %s", err.Error())
@@ -51,6 +52,10 @@ func TestFuzzyBlockMatch(t *testing.T) {
matches := matcher.MatchBlocksFuzzy(string(recipeMd), blocks, 0.3)
for _, m := range matches {
fmt.Printf("Content: '%s'->'%q'\n\n", m.Block.Path, m.GetContent())
}
expected := []struct {
value string
}{
@@ -61,10 +66,7 @@ func TestFuzzyBlockMatch(t *testing.T) {
value: "Baguette",
},
{
value: "\nMy favourite baguette recipe",
},
{
value: "",
value: "My favourite baguette recipe",
},
{
value: "- Flour\n- Water\n- Salt",
@@ -80,13 +82,12 @@ func TestFuzzyBlockMatch(t *testing.T) {
t.FailNow()
}
if expected[i].value != m.GetContent() {
t.Errorf("Match %d did not match expected: %q", i, m.GetContent())
t.Errorf("Match %d did not match expected: %q", i, expected[i].value)
}
fmt.Printf("match: %s->%q\n", m.Block.Path, m.GetContent())
}
}
func TestFuzzyBlockMatchSalad(t *testing.T) {
func TestMatch_FuzzyBlockSalad(t *testing.T) {
recipeMd := testdata.Read(t, "recipe_salad/input.md")
schemaMd, err := registry.GetTemplate("Recipe")
if err != nil {
@@ -110,9 +111,6 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
{
value: "Simple Salad",
},
{
value: "#healthy #salad",
},
{
value: "A quick green salad.",
},

View File

@@ -5,7 +5,6 @@ package parser
import (
"fmt"
"strings"
"time"
"git.max-richter.dev/max/marka/parser/decoders"
"git.max-richter.dev/max/marka/parser/matcher"
@@ -37,6 +36,7 @@ func DetectType(markdownContent string) (string, error) {
}
return "", fmt.Errorf("frontmatter did not contain '_type'")
}
return "", fmt.Errorf("could not parse frontmatter")
}
@@ -52,73 +52,33 @@ func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, erro
}
func ParseFile(markdownContent string) (any, error) {
timings := make(map[string]int64)
startDetectType := time.Now()
markdownContent = strings.TrimSuffix(markdownContent, "\n")
contentType, err := DetectType(markdownContent)
if err != nil {
return nil, fmt.Errorf("could not detect type -> %w", err)
}
timings["detect_type"] = time.Since(startDetectType).Milliseconds()
startGetTemplate := time.Now()
templateContent, err := registry.GetTemplate(contentType)
if err != nil {
return nil, fmt.Errorf("could not get schema -> %w", err)
}
timings["get_template"] = time.Since(startGetTemplate).Milliseconds()
startTemplate := time.Now()
tpl, err := template.CompileTemplate(templateContent)
if err != nil {
return nil, fmt.Errorf("failed to compile template -> %w", err)
}
timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
startMarkdown := time.Now()
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
result, err := decoders.Parse(blocks)
if err != nil {
return nil, fmt.Errorf("failed to parse blocks -> %w", err)
}
timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
response := map[string]any{
"data": result,
"timings": timings,
return nil, fmt.Errorf("could not get template -> %w", err)
}
return response, nil
return ParseFileWithTemplate(markdownContent, templateContent)
}
func ParseFileWithTemplate(markdownContent string, templateContent string) (any, error) {
timings := make(map[string]int64)
startTemplate := time.Now()
markdownContent = strings.TrimSuffix(markdownContent, "\n")
tpl, err := template.CompileTemplate(templateContent)
if err != nil {
return nil, fmt.Errorf("failed to compile template -> %w", err)
}
timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
startMarkdown := time.Now()
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
result, err := decoders.Parse(blocks)
if err != nil {
return nil, fmt.Errorf("failed to parse blocks -> %w", err)
}
timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
response := map[string]any{
"data": result,
"timings": timings,
return nil, fmt.Errorf("failed to compile blocks -> %w", err)
}
return response, nil
return result, nil
}

View File

@@ -9,7 +9,30 @@ import (
"github.com/google/go-cmp/cmp"
)
func TestParseRecipe_Golden(t *testing.T) {
func TestParse_DetectType(t *testing.T) {
recipe := testdata.Read(t, "recipe_salad/input.md")
article := testdata.Read(t, "article_simple/input.md")
recipeType, err := parser.DetectType(string(recipe))
if err != nil {
t.Fatalf("failed to detect recipeType: %v", err)
}
articleType, err := parser.DetectType(string(article))
if err != nil {
t.Fatalf("failed to detect articleType: %v", err)
}
if recipeType != "Recipe" {
t.Errorf("recipeType did not match expected type 'Recipe' -> %s", recipeType)
}
if articleType != "Article" {
t.Errorf("articleType did not match expected type 'Article' -> %s", articleType)
}
}
func TestParse_RecipeSalad(t *testing.T) {
inputContent := testdata.Read(t, "recipe_salad/input.md")
output := testdata.Read(t, "recipe_salad/output.json")
@@ -28,7 +51,7 @@ func TestParseRecipe_Golden(t *testing.T) {
}
}
func TestParseRecipe_NoDescription(t *testing.T) {
func TestParse_RecipeNoDescription(t *testing.T) {
inputContent := testdata.Read(t, "recipe_no_description/input.md")
got, err := parser.ParseFile(string(inputContent))
@@ -47,7 +70,7 @@ func TestParseRecipe_NoDescription(t *testing.T) {
}
}
func TestParseRecipe_Baguette(t *testing.T) {
func TestParse_Baguette(t *testing.T) {
inputContent := testdata.Read(t, "baguette/input.md")
got, err := parser.ParseFile(string(inputContent))
@@ -66,7 +89,7 @@ func TestParseRecipe_Baguette(t *testing.T) {
}
}
func TestParseArticle_Simple(t *testing.T) {
func TestParse_Article(t *testing.T) {
inputContent := testdata.Read(t, "article_simple/input.md")
got, err := parser.ParseFile(string(inputContent))

View File

@@ -17,6 +17,7 @@
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.22.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",

View File

@@ -42,6 +42,9 @@ importers:
'@sveltejs/adapter-auto':
specifier: ^6.0.0
version: 6.1.0(@sveltejs/kit@2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))
'@sveltejs/adapter-static':
specifier: ^3.0.9
version: 3.0.9(@sveltejs/kit@2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))
'@sveltejs/kit':
specifier: ^2.22.0
version: 2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1))
@@ -540,6 +543,11 @@ packages:
peerDependencies:
'@sveltejs/kit': ^2.0.0
'@sveltejs/adapter-static@3.0.9':
resolution: {integrity: sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw==}
peerDependencies:
'@sveltejs/kit': ^2.0.0
'@sveltejs/kit@2.43.4':
resolution: {integrity: sha512-GfvOq3A/qMRhj2L9eKjxaI8FLqZDh5SY74YzhRKT//u2AvQw96ksEfjuHviC4jg9U08mBVB0Y47EwEJHO4BB4Q==}
engines: {node: '>=18.13'}
@@ -1942,6 +1950,10 @@ snapshots:
dependencies:
'@sveltejs/kit': 2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1))
'@sveltejs/adapter-static@3.0.9(@sveltejs/kit@2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))':
dependencies:
'@sveltejs/kit': 2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1))
'@sveltejs/kit@2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1))':
dependencies:
'@standard-schema/spec': 1.0.0

View File

@@ -8,6 +8,23 @@ declare global {
// interface PageState {}
// interface Platform {}
}
class Go {
new(): {
run: (inst: WebAssembly.Instance) => Promise<void>;
importObject: WebAssembly.Imports;
};
}
const marka: {
matchBlocks(s: string, t: string): string;
detectType(markdown: string): string;
parseFile(input: string): string;
parseFileWithTemplate(markdown: string, template: string): string;
listTemplates(): string;
getTemplate(name: string): string;
compileTemplate(source: string): string;
};
}
export {};

View File

@@ -38,7 +38,7 @@
}: Props = $props();
</script>
<div class="flex flex-1 flex-col overflow-hidden border-r border-gray-200 last:border-r-0">
<div class="relative flex flex-1 flex-col overflow-hidden 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'}
<CheckCircleIcon class="mr-2 h-5 w-5 text-green-500" />

View File

@@ -4,7 +4,7 @@
</script>
<header class="sticky top-0 z-10 border-b border-gray-200 bg-white/80 backdrop-blur-sm">
<div class="container px-6 py-4">
<div class="px-6 py-4">
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-3">
<Logo />

View File

@@ -2,6 +2,7 @@
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import {
compileTemplate,
getTemplate,
listTemplates,
parseMarkdown,
@@ -86,9 +87,12 @@ My favourite baguette recipe
return;
}
try {
compileTemplate(templateValue);
const result = templateValue
? parseMarkdownWithTemplate(markdownValue, templateValue)
: parseMarkdown(markdownValue);
console.log({ result });
if ('error' in result) {
jsonOutput = '';
@@ -102,6 +106,7 @@ My favourite baguette recipe
dataStatus = 'error';
}
} else {
templateError = undefined;
jsonOutput = JSON.stringify(result.data, null, 2);
timings = result.timings;
templateStatus = 'success';

View File

@@ -1,4 +1,4 @@
<img src="/logo.svg" alt="logo" width="100%" />
<img src="/logo-2.svg" alt="logo" width="100%" />
<style>
img {

View File

@@ -1,20 +1,6 @@
import { readable } from "svelte/store";
declare global {
interface Window {
Go: {
new(): {
run: (inst: WebAssembly.Instance) => Promise<void>;
importObject: WebAssembly.Imports;
};
};
markaMatchBlocks: (input: string) => unknown;
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") {
@@ -22,7 +8,7 @@ export const wasmReady = readable(false, (set) => {
}
const loadWasm = async () => {
const go = new window.Go();
const go = new globalThis.Go();
try {
const result = await WebAssembly.instantiateStreaming(
fetch("/main.wasm"),
@@ -38,7 +24,7 @@ export const wasmReady = readable(false, (set) => {
if (document.readyState === "complete") {
loadWasm();
} else {
window.addEventListener("load", loadWasm);
globalThis.addEventListener("load", loadWasm);
}
});
@@ -54,46 +40,75 @@ export type ParseResultError = {
export type ParseResult = ParseResultSuccess | ParseResultError;
export function parseMarkdown(markdown: string): ParseResult {
if (typeof window.markaParseFile !== "function") {
if (typeof globalThis.marka?.parseFile !== "function") {
throw new Error("Wasm module not ready");
}
const result = window.markaParseFile(markdown);
if (result.error) return result;
return JSON.parse(result);
const resultString = globalThis.marka.parseFile(markdown);
return JSON.parse(resultString);
}
export function matchBlocks(markdown: string): ParseResult {
if (typeof window.markaMatchBlocks !== "function") {
export function compileTemplate(templateSource: string) {
if (typeof globalThis.marka?.compileTemplate !== "function") {
throw new Error("Wasm module not ready");
}
const result = window.markaMatchBlocks(markdown) as ParseResult;
if (result.error) return result;
return JSON.parse(result);
const resultString = globalThis.marka.compileTemplate(templateSource);
const result = JSON.parse(resultString);
console.log({ result });
return result;
}
export function matchBlocks(markdown: string, template: string): ParseResult {
if (typeof globalThis.marka?.matchBlocks !== "function") {
throw new Error("Wasm module not ready");
}
const resultString = globalThis.marka.matchBlocks(markdown, template);
return JSON.parse(resultString);
}
export function parseMarkdownWithTemplate(
markdown: string,
template: string,
): ParseResult {
if (typeof window.markaParseFileWithTemplate !== "function") {
if (typeof globalThis.marka?.parseFileWithTemplate !== "function") {
throw new Error("Wasm module not ready");
}
const result = window.markaParseFileWithTemplate(markdown, template);
if (result.error) return result;
return JSON.parse(result);
const resultString = globalThis.marka.parseFileWithTemplate(
markdown,
template,
);
return JSON.parse(resultString);
}
export function listTemplates(): string[] {
if (typeof window.markaListTemplates !== "function") {
if (typeof globalThis.marka?.listTemplates !== "function") {
throw new Error("Wasm module not ready");
}
const result = window.markaListTemplates();
return JSON.parse(result);
const resultString = globalThis.marka.listTemplates();
return JSON.parse(resultString);
}
export function getTemplate(name: string): string {
if (typeof window.markaGetTemplate !== "function") {
if (typeof globalThis.marka?.getTemplate !== "function") {
throw new Error("Wasm module not ready");
}
return window.markaGetTemplate(name);
return globalThis.marka.getTemplate(name);
}
export function detectType(markdown: string): string | ParseResultError {
if (typeof globalThis.marka?.detectType !== "function") {
throw new Error("Wasm module not ready");
}
const result = globalThis.marka.detectType(markdown);
try {
// If the result is a JSON string with an error, parse and return it
const parsed = JSON.parse(result);
if (parsed.error) {
return parsed;
}
} catch (e) {
// Otherwise, it's a plain string for success
return result;
}
return result;
}

View File

@@ -0,0 +1 @@
export const prerender = true;

View File

@@ -0,0 +1,3 @@
<svg width="202" height="202" viewBox="0 0 202 202" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M198 101C198 47.4284 154.572 4 101 4C63.6769 4 31.2771 25.0794 15.0566 55.9824C31.5802 36.8304 58.3585 25 87.5 25C107.135 25 125.868 30.5962 141.081 40.3643C163.17 54.5475 177.915 77.5995 176.999 105.066C176.961 122.231 171.229 138.064 161.595 150.771L159.861 153.057L133.999 112.606L100.778 164.568L65.0068 108.618L42.0176 144.873C57.3037 166.504 84.4388 181 114.5 181C149.395 181 180.331 159.267 193.398 130.6C196.385 121.268 198 111.323 198 101ZM29.0127 106.222C29.2632 118.862 33.0866 130.771 39.5967 141.223L64.9932 101.171L100.778 157.143L134 105.182L160.106 146.016C168.233 134.357 173 120.186 173 104.895H173.002C173.825 79.7876 160.762 58.4662 140.614 44.8486L101 108.688L61.3604 44.7793C41.8587 57.6651 29 79.7788 29 104.895L29.0127 106.222ZM202 101C202 156.781 156.781 202 101 202V198C134.1 198 163.327 181.42 180.831 156.113C164.258 173.559 140.379 185 114.5 185C82.6725 185 53.8784 169.41 37.9658 146.05C30.0365 134.409 25.3017 120.829 25.0137 106.303L25 104.895C25 77.6227 39.3659 53.7077 60.9336 40.3018L62.6338 39.2441L101 101.101L137.249 42.6855C123.002 33.9863 105.671 29 87.5 29C51.3264 29 19.6837 47.7188 7.41992 75.377C5.19019 83.5392 4 92.1306 4 101C4 154.572 47.4284 198 101 198V202C45.2192 202 0 156.781 0 101C0 45.2192 45.2192 0 101 0C156.781 0 202 45.2192 202 101Z" fill="#555"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

View File

@@ -1,5 +1,5 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import adapter from "@sveltejs/adapter-static";
/** @type {import('@sveltejs/kit').Config} */
const config = {
@@ -11,8 +11,8 @@ const config = {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
adapter: adapter(),
},
};
export default config;

View File

@@ -7,12 +7,13 @@ OUT_WASM="$OUT_DIR/main.wasm"
mkdir -p "$OUT_DIR"
tinygo build -target=wasm -opt=z -no-debug -panic=trap -gc=leaking \
tinygo build -target=wasm \
-opt=z -no-debug -panic=print -gc=leaking \
-o "$OUT_WASM" "$SCRIPT_DIR"
# Optional post-process (run only if tools exist)
# command -v wasm-opt >/dev/null && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers \
# -o "$OUT_WASM.tmp" "$OUT_WASM" && mv "$OUT_WASM.tmp" "$OUT_WASM"
command -v wasm-opt >/dev/null && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers \
-o "$OUT_WASM.tmp" "$OUT_WASM" && mv "$OUT_WASM.tmp" "$OUT_WASM"
# command -v wasm-strip >/dev/null && wasm-strip "$OUT_WASM"
# command -v brotli >/dev/null && brotli -f -q 11 "$OUT_WASM" -o "$OUT_WASM.br"
# command -v gzip >/dev/null && gzip -c -9 "$OUT_WASM" > "$OUT_WASM.gz"

View File

@@ -8,92 +8,108 @@ import (
p "git.max-richter.dev/max/marka/parser"
"git.max-richter.dev/max/marka/registry"
"git.max-richter.dev/max/marka/template"
)
func matchBlocks(_ js.Value, args []js.Value) any {
if len(args) == 0 {
return js.ValueOf(map[string]any{"error": "missing markdown"})
}
t, err := p.MatchBlocks(args[0].String(), args[1].String())
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
jsonString, _ := json.Marshal(t)
return js.ValueOf(string(jsonString)) // plain string
func wrapError(err error) string {
errMap := map[string]any{"error": err.Error()}
errJSON, _ := json.Marshal(errMap)
return string(errJSON)
}
func detectType(_ js.Value, args []js.Value) any {
if len(args) == 0 {
return js.ValueOf(map[string]any{"error": "missing markdown"})
}
t, err := p.DetectType(args[0].String())
func MatchBlocks(this js.Value, args []js.Value) any {
s := args[0].String()
t := args[1].String()
matched, err := p.MatchBlocks(s, t)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
return wrapError(err)
}
return js.ValueOf(t) // plain string
jsonString, _ := json.Marshal(matched)
return string(jsonString)
}
func parseFile(_ js.Value, args []js.Value) any {
if len(args) == 0 {
return js.ValueOf(map[string]any{"error": "missing markdown"})
}
res, err := p.ParseFile(args[0].String())
func DetectType(this js.Value, args []js.Value) any {
markdown := args[0].String()
t, err := p.DetectType(markdown)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
return wrapError(err)
}
return t
}
func ParseFile(this js.Value, args []js.Value) any {
markdown := args[0].String()
res, err := p.ParseFile(markdown)
if err != nil {
return wrapError(err)
}
b, err := json.Marshal(res)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
return wrapError(err)
}
return js.ValueOf(string(b))
return string(b)
}
func parseFileWithTemplate(_ js.Value, args []js.Value) any {
if len(args) < 2 {
return js.ValueOf(map[string]any{"error": "missing markdown or template"})
}
res, err := p.ParseFileWithTemplate(args[0].String(), args[1].String())
func ParseFileWithTemplate(this js.Value, args []js.Value) any {
markdown := args[0].String()
template := args[1].String()
res, err := p.ParseFileWithTemplate(markdown, template)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
return wrapError(err)
}
b, err := json.Marshal(res)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
return wrapError(err)
}
return js.ValueOf(string(b))
return string(b)
}
func listTemplates(_ js.Value, args []js.Value) any {
func ListTemplates(this js.Value, args []js.Value) any {
templates, err := registry.ListTemplates()
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
return wrapError(err)
}
b, err := json.Marshal(templates)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
return wrapError(err)
}
return js.ValueOf(string(b))
return string(b)
}
func getTemplate(_ js.Value, args []js.Value) any {
if len(args) == 0 {
return js.ValueOf(map[string]any{"error": "missing template name"})
}
template, err := registry.GetTemplate(args[0].String())
func GetTemplate(this js.Value, args []js.Value) any {
name := args[0].String()
template, err := registry.GetTemplate(name)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
return wrapError(err)
}
return js.ValueOf(template)
return template
}
func CompileTemplate(this js.Value, args []js.Value) any {
source := args[0].String()
template, err := template.CompileTemplate(source)
if err != nil {
return wrapError(err)
}
b, err := json.Marshal(template)
if err != nil {
return wrapError(err)
}
return string(b)
}
func main() {
js.Global().Set("markaDetectType", js.FuncOf(detectType))
js.Global().Set("markaParseFile", js.FuncOf(parseFile))
js.Global().Set("markaParseFileWithTemplate", js.FuncOf(parseFileWithTemplate))
js.Global().Set("markaMatchBlocks", js.FuncOf(matchBlocks))
js.Global().Set("markaListTemplates", js.FuncOf(listTemplates))
js.Global().Set("markaGetTemplate", js.FuncOf(getTemplate))
marka := js.Global().Get("Object").New()
marka.Set("matchBlocks", js.FuncOf(MatchBlocks))
marka.Set("detectType", js.FuncOf(DetectType))
marka.Set("parseFile", js.FuncOf(ParseFile))
marka.Set("parseFileWithTemplate", js.FuncOf(ParseFileWithTemplate))
marka.Set("listTemplates", js.FuncOf(ListTemplates))
marka.Set("getTemplate", js.FuncOf(GetTemplate))
marka.Set("compileTemplate", js.FuncOf(CompileTemplate))
js.Global().Set("marka", marka)
select {}
}

View File

@@ -22,16 +22,13 @@
pathAlias: rating
- path: reviewRating.bestRating
codec: const
value: 5
hidden: true
- path: reviewRating.worstRating
codec: const
value: 1
hidden: true
}
---
# { headline }
{ keywords | hashtags }
{ articleBody }

View File

@@ -10,7 +10,6 @@
- path: "_type"
codec: const
value: Recipe
hidden: true
- path: image
- path: author._type
codec: const
@@ -30,7 +29,6 @@
---
# { name | text }
{ keywords | hashtags,optional }
{ description | text }

View File

@@ -65,5 +65,5 @@ func ParseTemplateBlock(template Slice, blockType BlockType) (block Block, err e
return parseYamlTemplate(template)
}
return block, NewErrorf("invalid template").WithPosition(template.start, template.end)
return block, NewErrorf("invalid template: '%s'", template.String()).WithPosition(template.start, template.end)
}

View File

@@ -22,7 +22,7 @@ type yamlField struct {
Value any `yaml:"value,omitempty"`
Codec string `yaml:"codec"`
Hidden bool `yaml:"hidden,omitempty"`
PathAlias []string `yaml:"pathAlias,omitempty"`
PathAlias string `yaml:"pathAlias,omitempty"`
}
func parseYamlTemplate(input Slice) (block Block, err error) {
@@ -34,7 +34,7 @@ func parseYamlTemplate(input Slice) (block Block, err error) {
dec.KnownFields(true)
if err := dec.Decode(&blk); err != nil {
return block, NewErrorf("content '%q' -> %w", cleaned, err).WithPosition(input.start, input.end)
return block, NewErrorf("failed to parse yaml -> %w", err).WithPosition(input.start, input.end)
}
if blk.Path == "" {

View File

@@ -1,9 +1,13 @@
package template
import "strings"
// CompileTemplate scans once, emitting:
// - data blocks: inner content between a line that's exactly "{" and a line that's exactly "}"
// - matching blocks: gaps between data blocks (excluding the brace lines themselves)
func CompileTemplate(templateSource string) ([]Block, error) {
templateSource = strings.TrimSuffix(templateSource, "\n")
var out []Block
var curlyIndex int
@@ -43,9 +47,9 @@ func CompileTemplate(templateSource string) ([]Block, error) {
blockType = DataBlock
} else if curlyIndex == 1 && nextCurlyIndex == 0 {
if i > start {
block, err := ParseTemplateBlock(template.Slice(start, i), blockType)
block, err := ParseTemplateBlock(template.Slice(start, i+1), blockType)
if err != nil {
return nil, NewErrorf("cannot parse block @pos -> %w", err).WithPosition(start, i)
return nil, NewErrorf("cannot parse block @pos -> %w", err).WithPosition(start, i+1)
}
out = append(out, block)
}
@@ -67,5 +71,17 @@ func CompileTemplate(templateSource string) ([]Block, error) {
curlyIndex = nextCurlyIndex
}
if curlyIndex != 0 {
return nil, NewErrorf("unclosed block").WithPosition(start, template.Len())
}
if start < template.Len() {
block, err := ParseTemplateBlock(template.Slice(start, template.Len()), blockType)
if err != nil {
return nil, NewErrorf("cannot parse final block @pos -> %w", err).WithPosition(start, template.Len())
}
out = append(out, block)
}
return out, nil
}

View File

@@ -1,6 +1,7 @@
package template_test
import (
"fmt"
"testing"
"git.max-richter.dev/max/marka/registry"
@@ -10,16 +11,20 @@ import (
func TestExtractBlocks(t *testing.T) {
src, err := registry.GetTemplate("Recipe")
if err != nil {
t.Errorf("Failed to extract blocks: %s", err.Error())
t.Errorf("failed to load template 'Recipe' -> %s", err.Error())
t.FailNow()
}
templateBlocks, err := template.CompileTemplate(src)
if err != nil {
t.Errorf("Failed to extract blocks: %s", err.Error())
t.Errorf("failed to compile template -> %s", err.Error())
t.FailNow()
}
for i, b := range templateBlocks {
fmt.Printf("Block#%d: %q\n", i, b.GetContent())
}
expected := []template.Block{
{
Type: template.MatchingBlock,

View File

@@ -1,4 +1,5 @@
{
"_schema": "Article",
"_type": "Article",
"headline": "My First Article",
"author": {

View File

@@ -1,4 +1,5 @@
{
"_schema": "Recipe",
"_type": "Recipe",
"name": "Baguette",
"author": {

View File

@@ -8,7 +8,6 @@ recipeYield: 2 servings
---
# Simple Salad
#healthy #salad
A quick green salad.

View File

@@ -7,10 +7,6 @@
"_type": "Person",
"name": "Alex Chef"
},
"keywords": [
"healthy",
"salad"
],
"description": "A quick green salad.",
"prepTime": "PT10M",
"cookTime": "PT0M",