This commit is contained in:
Max Richter
2025-09-25 16:41:26 +02:00
parent 3f0d25f935
commit b13d5015f4
75 changed files with 3881 additions and 141 deletions

View File

@@ -1,5 +1,5 @@
---
@type: Article
_type: Article
author.name: Erin Mckean
url: https://dressaday.com/2006/10/20/you-dont-have-to-be-pretty/
rating: 5

View File

@@ -1,15 +1,14 @@
---
@context: https://schema.org
@type: Review
_type: Review
author:
@type: Person
_type: Person
name: Alice
datePublished: 2025-08-01
itemReviewed:
@type: Book
_type: Book
name: Untitled
author:
@type: Person
_type: Person
name: Unknown
---

View File

@@ -1,12 +1,11 @@
---
@context: https://schema.org
@type: Review
_type: Review
author:
@type: Person
_type: Person
name: Eve
datePublished: 2025-08-15
itemReviewed:
@type: Book
_type: Book
name: Anonymous Poems
reviewBody: "Short, haunting, and powerful verses."
reviewRating.ratingValue: 4

View File

@@ -1,15 +1,14 @@
---
@context: https://schema.org
@type: Review
_type: Review
author:
@type: Person
_type: Person
name: Clara
datePublished: 2025-08-10
itemReviewed:
@type: Book
_type: Book
name: 1984
author:
@type: Person
_type: Person
name: George Orwell
reviewRating: 5
---

View File

@@ -1,15 +1,14 @@
---
@context: https://schema.org
@type: Review
_type: Review
author:
@type: Person
_type: Person
name: Bob
datePublished: 2025-08-05
itemReviewed:
@type: Book
_type: Book
name: War and Peace
author:
@type: Person
_type: Person
name: Leo Tolstoy
reviewAspect:
- Length

View File

@@ -1,9 +1,9 @@
---
@type: Review
_type: Review
author:
name: Max Richter
itemReviewed:
@type: Book
_type: Book
author:
name: F. Scott Fitzgerald
reviewRating: 5

View File

@@ -1,12 +1,11 @@
---
@context: https://schema.org
@type: Review
_type: Review
author:
@type: Person
_type: Person
name: Frank
datePublished: 2025-08-01
itemReviewed:
@type: Movie
_type: Movie
name: Untitled Film
---

View File

@@ -1,15 +1,14 @@
---
@context: https://schema.org
@type: Review
_type: Review
author:
@type: Person
_type: Person
name: Grace
datePublished: 2025-08-02
itemReviewed:
@type: Movie
_type: Movie
name: The Room
author:
@type: Person
_type: Person
name: Tommy Wiseau
negativeNotes:
- Awkward dialogue

View File

@@ -1,15 +1,14 @@
---
@context: https://schema.org
@type: Review
_type: Review
author:
@type: Person
_type: Person
name: Jack
datePublished: 2025-08-06
itemReviewed:
@type: Movie
_type: Movie
name: Blade Runner
author:
@type: Person
_type: Person
name: Ridley Scott
reviewRating: 4.5/5
reviewAspect:

View File

@@ -1,15 +1,14 @@
---
@context: https://schema.org
@type: Review
_type: Review
author:
@type: Person
_type: Person
name: Henry
datePublished: 2025-08-03
itemReviewed:
@type: Movie
_type: Movie
name: Inception
author:
@type: Person
_type: Person
name: Christopher Nolan
reviewAspect:
- Story

View File

@@ -1,4 +1,5 @@
---
_type: Recipe
author.name: Max Richter
---

View File

@@ -1,6 +1,6 @@
---
author.name: Jane Doe
author.@type: Organization
author._type: Organization
recipeCategory: ["Dessert", "Vegan", "Quick"]
---

View File

@@ -1,10 +1,11 @@
go 1.25.1
go 1.24.7
use (
./parser
./playground/wasm
./registry
./renderer
./server-new
./server
./template
./testdata
./validator

2
mise.toml Normal file
View File

@@ -0,0 +1,2 @@
[tools]
go = "1.24.7"

View File

@@ -1,6 +1,6 @@
module git.max-richter.dev/max/marka/parser
go 1.25.1
go 1.24.7
require (
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567

View File

@@ -55,7 +55,7 @@ func TestFuzzyBlockMatch(t *testing.T) {
value string
}{
{
value: "@type: Recipe\nauthor.name: Max Richter",
value: "_type: Recipe\nauthor.name: Max Richter",
},
{
value: "Baguette",
@@ -105,7 +105,7 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
value string
}{
{
value: "@type: Recipe\nauthor.name: Alex Chef\ncookTime: PT0M\nimage: https://example.com/salad.jpg\nprepTime: PT10M\nrecipeYield: 2 servings",
value: "_type: Recipe\nauthor.name: Alex Chef\ncookTime: PT0M\nimage: https://example.com/salad.jpg\nprepTime: PT10M\nrecipeYield: 2 servings",
},
{
value: "Simple Salad",

View File

@@ -23,8 +23,6 @@ func DetectType(markdownContent string) (string, error) {
return "", fmt.Errorf("failed to compile template: %w", err)
}
fmt.Println("Content:", markdownContent)
blocks := matcher.MatchBlocksFuzzy(markdownContent, defaultSchema, 0.3)
result, err := decoders.Parse(blocks)
@@ -32,28 +30,17 @@ func DetectType(markdownContent string) (string, error) {
return "", fmt.Errorf("failed to parse blocks: %w", err)
}
fmt.Println("Result: ", result)
if result, ok := result.(map[string]any); ok {
if contentType, ok := result["@type"]; ok {
if contentType, ok := result["_type"]; ok {
return contentType.(string), nil
} else {
return "", fmt.Errorf("frontmatter did not contain '@type'")
}
} else {
return "", fmt.Errorf("could not parse frontmatter")
return "", fmt.Errorf("frontmatter did not contain '_type'")
}
return "", fmt.Errorf("could not parse frontmatter")
}
func prepareMarkdown(input string) string {
input = strings.TrimSuffix(input, "\n")
input = strings.ReplaceAll(input, "@type:", `"@type":`)
input = strings.ReplaceAll(input, "@context:", `"@context":`)
return input
}
func ParseFile(markdownContent string) (any, error) {
markdownContent = prepareMarkdown(markdownContent)
func MatchBlocks(markdownContent string) ([]matcher.Block, error) {
markdownContent = strings.TrimSuffix(markdownContent, "\n")
contentType, err := DetectType(markdownContent)
if err != nil {
@@ -65,12 +52,33 @@ func ParseFile(markdownContent string) (any, error) {
return nil, fmt.Errorf("could not get schema: %w", err)
}
template, err := template.CompileTemplate(templateContent)
tpl, err := template.CompileTemplate(templateContent)
if err != nil {
return nil, fmt.Errorf("failed to compile template: %w", err)
}
blocks := matcher.MatchBlocksFuzzy(markdownContent, template, 0.3)
return matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3), nil
}
func ParseFile(markdownContent string) (any, error) {
markdownContent = strings.TrimSuffix(markdownContent, "\n")
contentType, err := DetectType(markdownContent)
if err != nil {
return nil, fmt.Errorf("could not detect type: %w", err)
}
templateContent, err := registry.GetTemplate(contentType)
if err != nil {
return nil, fmt.Errorf("could not get schema: %w", err)
}
tpl, err := template.CompileTemplate(templateContent)
if err != nil {
return nil, fmt.Errorf("failed to compile template: %w", err)
}
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
result, err := decoders.Parse(blocks)
if err != nil {

23
playground/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
playground/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/

16
playground/.prettierrc Normal file
View File

@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/app.css"
}

38
playground/README.md Normal file
View File

@@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

View File

@@ -0,0 +1,41 @@
import prettier from 'eslint-config-prettier';
import { fileURLToPath } from 'node:url';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

43
playground/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "playground",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.22.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^22",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^7.0.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
}

2599
playground/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

1
playground/src/app.css Normal file
View File

@@ -0,0 +1 @@
@import 'tailwindcss';

22
playground/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
// 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;
};
};
}
}
export { };

12
playground/src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/wasm_exec.js"></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
title: string;
value: string;
placeholder?: string;
readonly?: boolean;
children?: Snippet;
}
let { title, value = $bindable(), placeholder = "", readonly = false, children }: 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>
<div class="flex-1 relative group">
{#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>
{: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>
{/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>
{#if children}
{@render children()}
{/if}
</div>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
// import { CodeIcon, ArrowRightIcon } from "lucide-svelte";
</script>
<header class="sticky top-0 z-10 border-b border-gray-200 bg-white/80 backdrop-blur-sm">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-black p-2">
<!-- <CodeIcon class="h-5 w-5 text-white" /> -->
</div>
<div>
<h1 class="text-2xl font-bold text-black">Marka</h1>
<p class="text-sm text-gray-600">Bidirectional Markdown ↔ JSON Parser</p>
</div>
</div>
<div class="hidden items-center gap-2 text-sm text-gray-500 md:flex">
<span>Template</span>
<!-- <ArrowRightIcon class="w-4 h-4" /> -->
<span>Markdown</span>
<!-- <ArrowRightIcon class="w-4 h-4" /> -->
<span>JSON</span>
</div>
</div>
</div>
</header>

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import EditorPanel from './EditorPanel.svelte';
// 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`);
let markdownValue = $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`);
// Simulated JSON output - in real implementation this would be generated from the parser
let jsonOutput = $derived(() => {
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';
}
});
</script>
<div class="grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-3">
<EditorPanel
title="Template"
bind:value={templateValue}
placeholder="Enter your Marka template here..."
/>
<EditorPanel
title="Markdown"
bind:value={markdownValue}
placeholder="Enter your markdown content here..."
/>
<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>
</div>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
// import { CheckCircleIcon, AlertCircleIcon } from "lucide-svelte";
let status = $state<'success' | 'error' | 'idle'>('success');
let message = $state('Template parsed successfully');
</script>
<div class="border-t border-gray-200 bg-gray-50/50 px-6 py-2">
<div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2">
{#if status === 'success'}
<!-- <CheckCircleIcon class="h-4 w-4 text-green-600" /> -->
<span class="text-green-700">{message}</span>
{:else if status === 'error'}
<!-- <AlertCircleIcon class="h-4 w-4 text-red-600" /> -->
<span class="text-red-700">{message}</span>
{:else}
<div class="h-4 w-4 rounded-full bg-gray-300"></div>
<span class="text-gray-600">Ready</span>
{/if}
</div>
<div class="text-gray-500">Schema.org validation enabled</div>
</div>
</div>

View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children?.()}

View File

@@ -0,0 +1,33 @@
<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>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: white;
}
:global(*) {
box-sizing: border-box;
}
:global(textarea:focus) {
outline: none;
}
:global(pre) {
margin: 0;
font-family:
'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
}
</style>

View File

@@ -0,0 +1,47 @@
<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>

BIN
playground/static/main.wasm Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

View File

@@ -0,0 +1,553 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// This file has been modified for use by the TinyGo compiler.
(() => {
// Map multiple JavaScript environments to a single common API,
// preferring web standards over Node.js API.
//
// Environments considered:
// - Browsers
// - Node.js
// - Electron
// - Parcel
if (typeof global !== "undefined") {
// global already exists
} else if (typeof window !== "undefined") {
window.global = window;
} else if (typeof self !== "undefined") {
self.global = self;
} else {
throw new Error("cannot export Go (neither global, window nor self is defined)");
}
if (!global.require && typeof require !== "undefined") {
global.require = require;
}
if (!global.fs && global.require) {
global.fs = require("node:fs");
}
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!global.fs) {
let outputBuf = "";
global.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substr(0, nl));
outputBuf = outputBuf.substr(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!global.process) {
global.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!global.crypto) {
const nodeCrypto = require("node:crypto");
global.crypto = {
getRandomValues(b) {
nodeCrypto.randomFillSync(b);
},
};
}
if (!global.performance) {
global.performance = {
now() {
const [sec, nsec] = process.hrtime();
return sec * 1000 + nsec / 1000000;
},
};
}
if (!global.TextEncoder) {
global.TextEncoder = require("node:util").TextEncoder;
}
if (!global.TextDecoder) {
global.TextDecoder = require("node:util").TextDecoder;
}
// End of polyfills for common API.
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
let reinterpretBuf = new DataView(new ArrayBuffer(8));
var logLine = [];
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
global.Go = class {
constructor() {
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const mem = () => {
// The buffer may change when requesting more memory.
return new DataView(this._inst.exports.memory.buffer);
}
const unboxValue = (v_ref) => {
reinterpretBuf.setBigInt64(0, v_ref, true);
const f = reinterpretBuf.getFloat64(0, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = v_ref & 0xffffffffn;
return this._values[id];
}
const loadValue = (addr) => {
let v_ref = mem().getBigUint64(addr, true);
return unboxValue(v_ref);
}
const boxValue = (v) => {
const nanHead = 0x7FF80000n;
if (typeof v === "number") {
if (isNaN(v)) {
return nanHead << 32n;
}
if (v === 0) {
return (nanHead << 32n) | 1n;
}
reinterpretBuf.setFloat64(0, v, true);
return reinterpretBuf.getBigInt64(0, true);
}
switch (v) {
case undefined:
return 0n;
case null:
return (nanHead << 32n) | 2n;
case true:
return (nanHead << 32n) | 3n;
case false:
return (nanHead << 32n) | 4n;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = BigInt(this._values.length);
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 1n;
switch (typeof v) {
case "string":
typeFlag = 2n;
break;
case "symbol":
typeFlag = 3n;
break;
case "function":
typeFlag = 4n;
break;
}
return id | ((nanHead | typeFlag) << 32n);
}
const storeValue = (addr, v) => {
let v_ref = boxValue(v);
mem().setBigUint64(addr, v_ref, true);
}
const loadSlice = (array, len, cap) => {
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
}
const loadSliceOfValues = (array, len, cap) => {
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (ptr, len) => {
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
wasi_snapshot_preview1: {
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
let nwritten = 0;
if (fd == 1) {
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
let ptr = mem().getUint32(iov_ptr + 0, true);
let len = mem().getUint32(iov_ptr + 4, true);
nwritten += len;
for (let i=0; i<len; i++) {
let c = mem().getUint8(ptr+i);
if (c == 13) { // CR
// ignore
} else if (c == 10) { // LF
// write line
let line = decoder.decode(new Uint8Array(logLine));
logLine = [];
console.log(line);
} else {
logLine.push(c);
}
}
}
} else {
console.error('invalid file descriptor:', fd);
}
mem().setUint32(nwritten_ptr, nwritten, true);
return 0;
},
fd_close: () => 0, // dummy
fd_fdstat_get: () => 0, // dummy
fd_seek: () => 0, // dummy
proc_exit: (code) => {
this.exited = true;
this.exitCode = code;
this._resolveExitPromise();
throw wasmExit;
},
random_get: (bufPtr, bufLen) => {
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
return 0;
},
},
gojs: {
// func ticks() float64
"runtime.ticks": () => {
return timeOrigin + performance.now();
},
// func sleepTicks(timeout float64)
"runtime.sleepTicks": (timeout) => {
// Do not sleep, only reactivate scheduler after the given timeout.
setTimeout(() => {
if (this.exited) return;
try {
this._inst.exports.go_scheduler();
} catch (e) {
if (e !== wasmExit) throw e;
}
}, timeout);
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (v_ref) => {
// Note: TinyGo does not support finalizers so this is only called
// for one specific case, by js.go:jsString. and can/might leak memory.
const id = v_ref & 0xffffffffn;
if (this._goRefCounts?.[id] !== undefined) {
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
} else {
console.error("syscall/js.finalizeRef: unknown id", id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (value_ptr, value_len) => {
value_ptr >>>= 0;
const s = loadString(value_ptr, value_len);
return boxValue(s);
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
let prop = loadString(p_ptr, p_len);
let v = unboxValue(v_ref);
let result = Reflect.get(v, prop);
return boxValue(result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
const v = unboxValue(v_ref);
const p = loadString(p_ptr, p_len);
const x = unboxValue(x_ref);
Reflect.set(v, p, x);
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
const v = unboxValue(v_ref);
const p = loadString(p_ptr, p_len);
Reflect.deleteProperty(v, p);
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (v_ref, i) => {
return boxValue(Reflect.get(unboxValue(v_ref), i));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
const v = unboxValue(v_ref);
const name = loadString(m_ptr, m_len);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
const m = Reflect.get(v, name);
storeValue(ret_addr, Reflect.apply(m, v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
try {
const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
storeValue(ret_addr, Reflect.apply(v, undefined, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr + 8, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
const v = unboxValue(v_ref);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try {
storeValue(ret_addr, Reflect.construct(v, args));
mem().setUint8(ret_addr + 8, 1);
} catch (err) {
storeValue(ret_addr, err);
mem().setUint8(ret_addr+ 8, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (v_ref) => {
return unboxValue(v_ref).length;
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
const s = String(unboxValue(v_ref));
const str = encoder.encode(s);
storeValue(ret_addr, str);
mem().setInt32(ret_addr + 8, str.length, true);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
const str = unboxValue(v_ref);
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
return unboxValue(v_ref) instanceof unboxValue(t_ref);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = loadSlice(dest_addr, dest_len);
const src = unboxValue(src_ref);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
// copyBytesToJS(dst ref, src []byte) (int, bool)
// Originally copied from upstream Go project, then modified:
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
let num_bytes_copied_addr = ret_addr;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
const dst = unboxValue(dst_ref);
const src = loadSlice(src_addr, src_len);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
mem().setUint8(returned_status_addr, 1); // Return "ok" status
},
}
};
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
// For compatibility, we use both as long as Go 1.20 is supported.
this.importObject.env = this.importObject.gojs;
}
async run(instance) {
this._inst = instance;
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
global,
this,
];
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
this.exitCode = 0;
if (this._inst.exports._start) {
let exitPromise = new Promise((resolve, reject) => {
this._resolveExitPromise = resolve;
});
// Run program, but catch the wasmExit exception that's thrown
// to return back here.
try {
this._inst.exports._start();
} catch (e) {
if (e !== wasmExit) throw e;
}
await exitPromise;
return this.exitCode;
} else {
this._inst.exports._initialize();
}
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
try {
this._inst.exports.resume();
} catch (e) {
if (e !== wasmExit) throw e;
}
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
if (
global.require &&
global.require.main === module &&
global.process &&
global.process.versions &&
!global.process.versions.electron
) {
if (process.argv.length != 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1);
}
const go = new Go();
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
let exitCode = await go.run(result.instance);
process.exit(exitCode);
}).catch((err) => {
console.error(err);
process.exit(1);
});
}
})();

View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// 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()
}
};
export default config;

19
playground/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

View File

@@ -0,0 +1,7 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
});

21
playground/wasm/build.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_DIR="$SCRIPT_DIR/../static"
OUT_WASM="$OUT_DIR/main.wasm"
mkdir -p "$OUT_DIR"
tinygo build -target=wasm -opt=z -no-debug -panic=trap -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-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"
# Copy TinyGo runtime
cp -f "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" "$OUT_DIR/wasm_exec.js"

3
playground/wasm/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module git.max-richter.dev/max/marka/playground-wasm
go 1.24.7

59
playground/wasm/main.go Normal file
View File

@@ -0,0 +1,59 @@
//go:build js && wasm
package main
import (
"encoding/json"
"syscall/js"
p "git.max-richter.dev/max/marka/parser"
)
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())
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
jsonString,_ := json.Marshal(t)
return js.ValueOf(string(jsonString)) // plain string
}
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())
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
return js.ValueOf(t) // plain string
}
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())
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
b, err := json.Marshal(res) // return JSON string to avoid reflect-heavy bridging
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
return js.ValueOf(string(b))
}
func main() {
js.Global().Set("markaDetectType", js.FuncOf(detectType))
js.Global().Set("markaParseFile", js.FuncOf(parseFile))
js.Global().Set("markaMatchBlocks", js.FuncOf(matchBlocks))
select {}
}

View File

@@ -1,3 +1,3 @@
module git.max-richter.dev/max/marka/registry
go 1.25.1
go 1.24.7

View File

@@ -3,20 +3,16 @@
path: .
codec: yaml
fields:
- path: "@context"
codec: const
value: https://schema.org
hidden: true
- path: "@schema"
- path: "_schema"
codec: const
value: Article
hidden: true
- path: "@type"
- path: "_type"
codec: const
value: Article
- path: image
- path: author.name
- path: author.@type
- path: author._type
codec: const
value: Person
hidden: true

View File

@@ -3,19 +3,16 @@
path: .
codec: yaml
fields:
- path: "@context"
codec: const
value: https://schema.org
hidden: true
- path: "@schema"
- path: "_schema"
codec: const
value: Recipe
hidden: true
- path: "@type"
- path: "_type"
codec: const
value: Recipe
hidden: true
- path: image
- path: author.@type
- path: author._type
codec: const
hidden: true
value: Person
@@ -33,7 +30,7 @@
---
# { name | text }
{ keywords | hashtags }
{ keywords | hashtags,optional }
{ description | text }

View File

@@ -3,21 +3,17 @@
path: .
codec: yaml
fields:
- path: "@context"
codec: const
value: https://schema.org
hidden: true
- path: "@schema"
- path: "_schema"
codec: const
value: Review
hidden: true
- path: "@type"
- path: "_type"
codec: const
value: Review
- path: tmdbId
- path: image
- path: author.name
- path: author.@type
- path: author._type
codec: const
value: Person
hidden: true

View File

@@ -3,6 +3,6 @@
path: .
codec: yaml
fields:
- path: "@type"
- path: "_type"
}
---

View File

@@ -13,8 +13,7 @@ import (
const emptyBlock = "\uE000"
func fixRenderedBlock(input string) string {
input = strings.ReplaceAll(input, "'@type':", "@type:")
input = strings.ReplaceAll(input, "'@context':", "@context:")
input = strings.ReplaceAll(input, "'_type':", "_type:")
if len(input) == 0 {
return emptyBlock
}

View File

@@ -1,6 +1,6 @@
module git.max-richter.dev/max/marka/renderer
go 1.25.1
go 1.24.7
require (
git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e

View File

@@ -18,10 +18,10 @@ func RenderFile(rawJSON []byte) ([]byte, error) {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
// 2) extract type from "@type" Property
contentType, ok := data["@type"].(string)
// 2) extract type from "_type" Property
contentType, ok := data["_type"].(string)
if !ok || contentType == "" {
return nil, fmt.Errorf("JSON does not contain a valid '@type' property")
return nil, fmt.Errorf("JSON does not contain a valid '_type' property")
}
// 3) get the template from the registry

View File

@@ -41,10 +41,10 @@ func TestRenderFile_MissingType(t *testing.T) {
rawJSON := []byte(`{"name": "Test"}`)
_, err := renderer.RenderFile(rawJSON)
if err == nil {
t.Fatal("expected error for missing @type, got nil")
t.Fatal("expected error for missing _type, got nil")
}
if !strings.Contains(err.Error(), "JSON does not contain a valid '@type' property") {
t.Errorf("expected missing @type error, got: %v", err)
if !strings.Contains(err.Error(), "JSON does not contain a valid '_type' property") {
t.Errorf("expected missing _type error, got: %v", err)
}
}

View File

@@ -1,3 +1,3 @@
module git.max-richter.dev/max/marka/server-new
go 1.25.1
go 1.24.7

View File

@@ -19,8 +19,7 @@ func parseShortTemplate(input string) (Block, error) {
}
if len(split) > 1 {
optionSplit := strings.SplitSeq(split[1], ",")
for option := range optionSplit {
for option := range strings.SplitSeq(split[1], ",") {
switch strings.TrimSpace(option) {
case "number":
block.Codec = CodecNumber
@@ -28,6 +27,8 @@ func parseShortTemplate(input string) (Block, error) {
block.Codec = CodecText
case "hashtags":
block.Codec = CodecHashtags
case "optional":
block.Optional = true
default:
return block, fmt.Errorf("unknown codec option: %s", option)
}

View File

@@ -30,13 +30,13 @@ func TestExtractBlocks(t *testing.T) {
Path: ".",
Fields: []template.BlockField{
{
Path: "@type",
Path: "_type",
},
{
Path: "image",
},
{
Path: "author.@type",
Path: "author._type",
},
{
Path: "author.name",

View File

@@ -1,6 +1,6 @@
module git.max-richter.dev/max/marka/template
go 1.25.1
go 1.24.7
require (
git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e

View File

@@ -20,6 +20,7 @@ type Block struct {
Codec CodecType
ListTemplate string
Fields []BlockField
Optional bool
Value any
content string
}

View File

@@ -1,5 +1,5 @@
---
@type: Article
_type: Article
author.name: John Doe
---

View File

@@ -1,10 +1,9 @@
{
"@context": "https://schema.org",
"@type": "Article",
"_type": "Article",
"headline": "My First Article",
"author": {
"@type": "Person",
"_type": "Person",
"name": "John Doe"
},
"articleBody": "This is the content of my first article. It's a simple one."
}
}

View File

@@ -1,5 +1,5 @@
---
@type: Recipe
_type: Recipe
author.name: Max Richter
---

View File

@@ -1,9 +1,8 @@
{
"@context": "https://schema.org",
"@type": "Recipe",
"_type": "Recipe",
"name": "Baguette",
"author": {
"@type": "Person",
"_type": "Person",
"name": "Max Richter"
},
"description": "My favourite baguette recipe",

View File

@@ -1,8 +1,8 @@
---
@type: Book
_type: Book
name: The Great Book
author:
@type: Person
_type: Person
name: Jane Doe
email: jane.doe@example.com
tags:
@@ -17,4 +17,4 @@ chapters:
# The Great Book
This is the content of the great book.
This is the content of the great book.

View File

@@ -1,9 +1,8 @@
{
"@context": "https://schema.org",
"@type": "Book",
"_type": "Book",
"name": "The Great Book",
"author": {
"@type": "Person",
"_type": "Person",
"name": "Jane Doe",
"email": "jane.doe@example.com"
},
@@ -23,4 +22,4 @@
],
"headline": "The Great Book",
"articleBody": "This is the content of the great book."
}
}

View File

@@ -1,5 +1,5 @@
---
@type: Recipe
_type: Recipe
author.name: Alex Chef
cookTime: PT0M
image: https://example.com/salad.jpg

View File

@@ -1,11 +1,10 @@
{
"@context": "https://schema.org",
"@schema": "Recipe",
"@type": "Recipe",
"_schema": "Recipe",
"_type": "Recipe",
"name": "Simple Salad",
"image": "https://example.com/salad.jpg",
"author": {
"@type": "Person",
"_type": "Person",
"name": "Alex Chef"
},
"prepTime": "PT10M",

View File

@@ -1,5 +1,5 @@
---
@type: Recipe
_type: Recipe
author.name: Alex Chef
cookTime: PT0M
image: https://example.com/salad.jpg

View File

@@ -1,11 +1,10 @@
{
"@context": "https://schema.org",
"@schema": "Recipe",
"@type": "Recipe",
"_schema": "Recipe",
"_type": "Recipe",
"name": "Simple Salad",
"image": "https://example.com/salad.jpg",
"author": {
"@type": "Person",
"_type": "Person",
"name": "Alex Chef"
},
"keywords": [

View File

@@ -1,5 +1,5 @@
---
@type: Recipe
_type: Recipe
name: Typo Recipe
---
@@ -11,4 +11,4 @@ name: Typo Recipe
## Stps
1. Step 1
2. Step 2
2. Step 2

View File

@@ -1,6 +1,5 @@
{
"@context": "https://schema.org",
"@type": "Recipe",
"_type": "Recipe",
"name": "Typo Recipe",
"recipeIngredient": [
"Item 1",
@@ -10,4 +9,4 @@
"Step 1",
"Step 2"
]
}
}

2
testdata/go.mod vendored
View File

@@ -1,3 +1,3 @@
module git.max-richter.dev/max/marka/testdata
go 1.25.1
go 1.24.7

View File

@@ -1,6 +1,6 @@
module git.max-richter.dev/max/marka/validator
go 1.25.1
go 1.24.7
require (
git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e

View File

@@ -8,7 +8,7 @@ import (
func TestValidateRecipe_InvalidType(t *testing.T) {
recipe := map[string]any{
"@type": "Recipe",
"_type": "Recipe",
"recipeYield": 4,
"recipeIngredient": []string{
"500 g flour",
@@ -24,7 +24,7 @@ func TestValidateRecipe_InvalidType(t *testing.T) {
func TestValidateRecipe_Valid(t *testing.T) {
recipe := map[string]any{
"@type": "Recipe",
"_type": "Recipe",
"name": "Simple Bread",
"cookTime": "PT30M",
"recipeIngredient": []any{"500 g flour", "300 ml water", "10 g salt", "3 g yeast"},