wip
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
---
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
---
|
||||
|
@@ -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
|
||||
|
@@ -1,9 +1,9 @@
|
||||
---
|
||||
@type: Review
|
||||
_type: Review
|
||||
author:
|
||||
name: Max Richter
|
||||
itemReviewed:
|
||||
@type: Book
|
||||
_type: Book
|
||||
author:
|
||||
name: F. Scott Fitzgerald
|
||||
reviewRating: 5
|
||||
|
@@ -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
|
||||
---
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -1,4 +1,5 @@
|
||||
---
|
||||
_type: Recipe
|
||||
author.name: Max Richter
|
||||
---
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
---
|
||||
author.name: Jane Doe
|
||||
author.@type: Organization
|
||||
author._type: Organization
|
||||
recipeCategory: ["Dessert", "Vegan", "Quick"]
|
||||
---
|
||||
|
||||
|
5
go.work
5
go.work
@@ -1,10 +1,11 @@
|
||||
go 1.25.1
|
||||
go 1.24.7
|
||||
|
||||
use (
|
||||
./parser
|
||||
./playground/wasm
|
||||
./registry
|
||||
./renderer
|
||||
./server-new
|
||||
./server
|
||||
./template
|
||||
./testdata
|
||||
./validator
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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("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
23
playground/.gitignore
vendored
Normal 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
1
playground/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
engine-strict=true
|
9
playground/.prettierignore
Normal file
9
playground/.prettierignore
Normal 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
16
playground/.prettierrc
Normal 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
38
playground/README.md
Normal 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.
|
41
playground/eslint.config.js
Normal file
41
playground/eslint.config.js
Normal 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
43
playground/package.json
Normal 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
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
1
playground/src/app.css
Normal file
@@ -0,0 +1 @@
|
||||
@import 'tailwindcss';
|
22
playground/src/app.d.ts
vendored
Normal file
22
playground/src/app.d.ts
vendored
Normal 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
12
playground/src/app.html
Normal 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>
|
1
playground/src/lib/assets/favicon.svg
Normal file
1
playground/src/lib/assets/favicon.svg
Normal 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 |
35
playground/src/lib/components/EditorPanel.svelte
Normal file
35
playground/src/lib/components/EditorPanel.svelte
Normal 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>
|
26
playground/src/lib/components/Header.svelte
Normal file
26
playground/src/lib/components/Header.svelte
Normal 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>
|
84
playground/src/lib/components/Playground.svelte
Normal file
84
playground/src/lib/components/Playground.svelte
Normal 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>
|
25
playground/src/lib/components/StatusBar.svelte
Normal file
25
playground/src/lib/components/StatusBar.svelte
Normal 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>
|
1
playground/src/lib/index.ts
Normal file
1
playground/src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
12
playground/src/routes/+layout.svelte
Normal file
12
playground/src/routes/+layout.svelte
Normal 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?.()}
|
33
playground/src/routes/+page.svelte
Normal file
33
playground/src/routes/+page.svelte
Normal 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>
|
47
playground/src/routes/+page.svelte2
Normal file
47
playground/src/routes/+page.svelte2
Normal 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
BIN
playground/static/main.wasm
Normal file
Binary file not shown.
3
playground/static/robots.txt
Normal file
3
playground/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
553
playground/static/wasm_exec.js
Normal file
553
playground/static/wasm_exec.js
Normal 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);
|
||||
});
|
||||
}
|
||||
})();
|
18
playground/svelte.config.js
Normal file
18
playground/svelte.config.js
Normal 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
19
playground/tsconfig.json
Normal 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
|
||||
}
|
7
playground/vite.config.ts
Normal file
7
playground/vite.config.ts
Normal 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
21
playground/wasm/build.sh
Executable 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
3
playground/wasm/go.mod
Normal 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
59
playground/wasm/main.go
Normal 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 {}
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
module git.max-richter.dev/max/marka/registry
|
||||
|
||||
go 1.25.1
|
||||
go 1.24.7
|
||||
|
@@ -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
|
||||
|
@@ -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 }
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -3,6 +3,6 @@
|
||||
path: .
|
||||
codec: yaml
|
||||
fields:
|
||||
- path: "@type"
|
||||
- path: "_type"
|
||||
}
|
||||
---
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,3 @@
|
||||
module git.max-richter.dev/max/marka/server-new
|
||||
|
||||
go 1.25.1
|
||||
go 1.24.7
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -20,6 +20,7 @@ type Block struct {
|
||||
Codec CodecType
|
||||
ListTemplate string
|
||||
Fields []BlockField
|
||||
Optional bool
|
||||
Value any
|
||||
content string
|
||||
}
|
||||
|
2
testdata/data/article_simple/input.md
vendored
2
testdata/data/article_simple/input.md
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
@type: Article
|
||||
_type: Article
|
||||
author.name: John Doe
|
||||
---
|
||||
|
||||
|
5
testdata/data/article_simple/output.json
vendored
5
testdata/data/article_simple/output.json
vendored
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"@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."
|
||||
|
2
testdata/data/baguette/input.md
vendored
2
testdata/data/baguette/input.md
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
@type: Recipe
|
||||
_type: Recipe
|
||||
author.name: Max Richter
|
||||
---
|
||||
|
||||
|
5
testdata/data/baguette/output.json
vendored
5
testdata/data/baguette/output.json
vendored
@@ -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",
|
||||
|
4
testdata/data/complex_front_matter/input.md
vendored
4
testdata/data/complex_front_matter/input.md
vendored
@@ -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:
|
||||
|
@@ -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"
|
||||
},
|
||||
|
2
testdata/data/recipe_no_description/input.md
vendored
2
testdata/data/recipe_no_description/input.md
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
@type: Recipe
|
||||
_type: Recipe
|
||||
author.name: Alex Chef
|
||||
cookTime: PT0M
|
||||
image: https://example.com/salad.jpg
|
||||
|
@@ -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",
|
||||
|
2
testdata/data/recipe_salad/input.md
vendored
2
testdata/data/recipe_salad/input.md
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
@type: Recipe
|
||||
_type: Recipe
|
||||
author.name: Alex Chef
|
||||
cookTime: PT0M
|
||||
image: https://example.com/salad.jpg
|
||||
|
7
testdata/data/recipe_salad/output.json
vendored
7
testdata/data/recipe_salad/output.json
vendored
@@ -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": [
|
||||
|
2
testdata/data/typo_section_header/input.md
vendored
2
testdata/data/typo_section_header/input.md
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
@type: Recipe
|
||||
_type: Recipe
|
||||
name: Typo Recipe
|
||||
---
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Recipe",
|
||||
"_type": "Recipe",
|
||||
"name": "Typo Recipe",
|
||||
"recipeIngredient": [
|
||||
"Item 1",
|
||||
|
2
testdata/go.mod
vendored
2
testdata/go.mod
vendored
@@ -1,3 +1,3 @@
|
||||
module git.max-richter.dev/max/marka/testdata
|
||||
|
||||
go 1.25.1
|
||||
go 1.24.7
|
||||
|
@@ -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
|
||||
|
@@ -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"},
|
||||
|
Reference in New Issue
Block a user