Compare commits
6 Commits
tmp
...
ae5cd8481a
Author | SHA1 | Date | |
---|---|---|---|
|
ae5cd8481a
|
||
|
deae5acac8
|
||
|
96e7f72d1f
|
||
|
b13d5015f4
|
||
|
3f0d25f935
|
||
|
733ae876b9
|
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
@type: Article
|
_type: Article
|
||||||
author.name: Erin Mckean
|
author.name: Erin Mckean
|
||||||
url: https://dressaday.com/2006/10/20/you-dont-have-to-be-pretty/
|
url: https://dressaday.com/2006/10/20/you-dont-have-to-be-pretty/
|
||||||
rating: 5
|
rating: 5
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
---
|
---
|
||||||
@context: https://schema.org
|
_type: Review
|
||||||
@type: Review
|
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Alice
|
name: Alice
|
||||||
datePublished: 2025-08-01
|
datePublished: 2025-08-01
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Book
|
_type: Book
|
||||||
name: Untitled
|
name: Untitled
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Unknown
|
name: Unknown
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
---
|
---
|
||||||
@context: https://schema.org
|
_type: Review
|
||||||
@type: Review
|
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Eve
|
name: Eve
|
||||||
datePublished: 2025-08-15
|
datePublished: 2025-08-15
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Book
|
_type: Book
|
||||||
name: Anonymous Poems
|
name: Anonymous Poems
|
||||||
reviewBody: "Short, haunting, and powerful verses."
|
reviewBody: "Short, haunting, and powerful verses."
|
||||||
reviewRating.ratingValue: 4
|
reviewRating.ratingValue: 4
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
---
|
---
|
||||||
@context: https://schema.org
|
_type: Review
|
||||||
@type: Review
|
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Clara
|
name: Clara
|
||||||
datePublished: 2025-08-10
|
datePublished: 2025-08-10
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Book
|
_type: Book
|
||||||
name: 1984
|
name: 1984
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: George Orwell
|
name: George Orwell
|
||||||
reviewRating: 5
|
reviewRating: 5
|
||||||
---
|
---
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
---
|
---
|
||||||
@context: https://schema.org
|
_type: Review
|
||||||
@type: Review
|
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Bob
|
name: Bob
|
||||||
datePublished: 2025-08-05
|
datePublished: 2025-08-05
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Book
|
_type: Book
|
||||||
name: War and Peace
|
name: War and Peace
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Leo Tolstoy
|
name: Leo Tolstoy
|
||||||
reviewAspect:
|
reviewAspect:
|
||||||
- Length
|
- Length
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
@type: Review
|
_type: Review
|
||||||
author:
|
author:
|
||||||
name: Max Richter
|
name: Max Richter
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Book
|
_type: Book
|
||||||
author:
|
author:
|
||||||
name: F. Scott Fitzgerald
|
name: F. Scott Fitzgerald
|
||||||
reviewRating: 5
|
reviewRating: 5
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
---
|
---
|
||||||
@context: https://schema.org
|
_type: Review
|
||||||
@type: Review
|
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Frank
|
name: Frank
|
||||||
datePublished: 2025-08-01
|
datePublished: 2025-08-01
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Movie
|
_type: Movie
|
||||||
name: Untitled Film
|
name: Untitled Film
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
---
|
---
|
||||||
@context: https://schema.org
|
_type: Review
|
||||||
@type: Review
|
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Grace
|
name: Grace
|
||||||
datePublished: 2025-08-02
|
datePublished: 2025-08-02
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Movie
|
_type: Movie
|
||||||
name: The Room
|
name: The Room
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Tommy Wiseau
|
name: Tommy Wiseau
|
||||||
negativeNotes:
|
negativeNotes:
|
||||||
- Awkward dialogue
|
- Awkward dialogue
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
---
|
---
|
||||||
@context: https://schema.org
|
_type: Review
|
||||||
@type: Review
|
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Jack
|
name: Jack
|
||||||
datePublished: 2025-08-06
|
datePublished: 2025-08-06
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Movie
|
_type: Movie
|
||||||
name: Blade Runner
|
name: Blade Runner
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Ridley Scott
|
name: Ridley Scott
|
||||||
reviewRating: 4.5/5
|
reviewRating: 4.5/5
|
||||||
reviewAspect:
|
reviewAspect:
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
---
|
---
|
||||||
@context: https://schema.org
|
_type: Review
|
||||||
@type: Review
|
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Henry
|
name: Henry
|
||||||
datePublished: 2025-08-03
|
datePublished: 2025-08-03
|
||||||
itemReviewed:
|
itemReviewed:
|
||||||
@type: Movie
|
_type: Movie
|
||||||
name: Inception
|
name: Inception
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Christopher Nolan
|
name: Christopher Nolan
|
||||||
reviewAspect:
|
reviewAspect:
|
||||||
- Story
|
- Story
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
_type: Recipe
|
||||||
author.name: Max Richter
|
author.name: Max Richter
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
author.name: Jane Doe
|
author.name: Jane Doe
|
||||||
author.@type: Organization
|
author._type: Organization
|
||||||
recipeCategory: ["Dessert", "Vegan", "Quick"]
|
recipeCategory: ["Dessert", "Vegan", "Quick"]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
3
go.work
3
go.work
@@ -1,7 +1,8 @@
|
|||||||
go 1.24.5
|
go 1.24.7
|
||||||
|
|
||||||
use (
|
use (
|
||||||
./parser
|
./parser
|
||||||
|
./playground/wasm
|
||||||
./registry
|
./registry
|
||||||
./renderer
|
./renderer
|
||||||
./server
|
./server
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e/go.mod h1:xQK6tsgr9BOoeFw8JxjBwDkVENlOqapmcRkYyf/L+SQ=
|
||||||
|
git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e/go.mod h1:n793S7TENIfgHpZLz0lm0qorM7eCx3zBLby3Fb++hZA=
|
||||||
|
git.max-richter.dev/max/marka/template v0.0.0-20250819170608-69c2550f448e/go.mod h1:Uxi5xcxtnjopsIZjjMlFaWJGuglB9JNL++FuaSbOf6U=
|
||||||
|
git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a/go.mod h1:88SkY5pTONkgfBy1FT10LoqRC8rt36iF1fk/rupjuJY=
|
||||||
|
git.max-richter.dev/max/marka/validator v0.0.0-20250819170608-69c2550f448e/go.mod h1:qdGfCFRzsGedmnd77vb7pu/EMx0W0DcQBMEfvNxMYsw=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
module git.max-richter.dev/max/marka/parser
|
module git.max-richter.dev/max/marka/parser
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567
|
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567
|
||||||
|
@@ -55,7 +55,7 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
|||||||
value string
|
value string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
value: "@type: Recipe\nauthor.name: Max Richter",
|
value: "_type: Recipe\nauthor.name: Max Richter",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "Baguette",
|
value: "Baguette",
|
||||||
@@ -95,7 +95,7 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
blocks, err := template.CompileTemplate(schemaMd)
|
blocks, err := template.CompileTemplate(schemaMd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed to compile template: %s", err.Error())
|
t.Errorf("failed to compile template: %s", err.Error())
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
|
|||||||
value string
|
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",
|
value: "Simple Salad",
|
||||||
|
@@ -5,6 +5,7 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.max-richter.dev/max/marka/parser/decoders"
|
"git.max-richter.dev/max/marka/parser/decoders"
|
||||||
"git.max-richter.dev/max/marka/parser/matcher"
|
"git.max-richter.dev/max/marka/parser/matcher"
|
||||||
@@ -31,47 +32,93 @@ func DetectType(markdownContent string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if result, ok := result.(map[string]any); ok {
|
if result, ok := result.(map[string]any); ok {
|
||||||
if contentType, ok := result["@type"]; ok {
|
if contentType, ok := result["_type"]; ok {
|
||||||
return contentType.(string), nil
|
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")
|
|
||||||
}
|
}
|
||||||
|
return "", fmt.Errorf("could not parse frontmatter")
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareMarkdown(input string) string {
|
func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, error) {
|
||||||
input = strings.TrimSuffix(input, "\n")
|
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||||
input = strings.ReplaceAll(input, "@type:", `"@type":`)
|
|
||||||
input = strings.ReplaceAll(input, "@context:", `"@context":`)
|
tpl, err := template.CompileTemplate(templateContent)
|
||||||
return input
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to compile template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseFile(markdownContent string) (any, error) {
|
func ParseFile(markdownContent string) (any, error) {
|
||||||
markdownContent = prepareMarkdown(markdownContent)
|
timings := make(map[string]int64)
|
||||||
|
|
||||||
|
startDetectType := time.Now()
|
||||||
|
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||||
|
|
||||||
contentType, err := DetectType(markdownContent)
|
contentType, err := DetectType(markdownContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not detect type: %w", err)
|
return nil, fmt.Errorf("could not detect type: %w", err)
|
||||||
}
|
}
|
||||||
|
timings["detect_type"] = time.Since(startDetectType).Milliseconds()
|
||||||
|
|
||||||
|
startGetTemplate := time.Now()
|
||||||
templateContent, err := registry.GetTemplate(contentType)
|
templateContent, err := registry.GetTemplate(contentType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get schema: %w", err)
|
return nil, fmt.Errorf("could not get schema: %w", err)
|
||||||
}
|
}
|
||||||
|
timings["get_template"] = time.Since(startGetTemplate).Milliseconds()
|
||||||
|
|
||||||
template, err := template.CompileTemplate(templateContent)
|
startTemplate := time.Now()
|
||||||
|
tpl, err := template.CompileTemplate(templateContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to compile template: %w", err)
|
return nil, fmt.Errorf("failed to compile template: %w", err)
|
||||||
}
|
}
|
||||||
|
timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
|
||||||
|
|
||||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, template, 0.3)
|
startMarkdown := time.Now()
|
||||||
|
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
||||||
|
|
||||||
result, err := decoders.Parse(blocks)
|
result, err := decoders.Parse(blocks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse blocks: %w", err)
|
return nil, fmt.Errorf("failed to parse blocks: %w", err)
|
||||||
}
|
}
|
||||||
|
timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
|
||||||
|
|
||||||
return result, nil
|
response := map[string]any{
|
||||||
|
"data": result,
|
||||||
|
"timings": timings,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFileWithTemplate(markdownContent string, templateContent string) (any, error) {
|
||||||
|
timings := make(map[string]int64)
|
||||||
|
|
||||||
|
startTemplate := time.Now()
|
||||||
|
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||||
|
|
||||||
|
tpl, err := template.CompileTemplate(templateContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to compile template: %w", err)
|
||||||
|
}
|
||||||
|
timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
|
||||||
|
|
||||||
|
startMarkdown := time.Now()
|
||||||
|
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
||||||
|
|
||||||
|
result, err := decoders.Parse(blocks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse blocks: %w", err)
|
||||||
|
}
|
||||||
|
timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
|
||||||
|
|
||||||
|
response := map[string]any{
|
||||||
|
"data": result,
|
||||||
|
"timings": timings,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, 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.
|
42
playground/eslint.config.js
Normal file
42
playground/eslint.config.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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),
|
||||||
|
{ ignores: ['static/wasm_exec.js'] },
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
53
playground/package.json
Normal file
53
playground/package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "^6.3.4",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
|
"@codemirror/view": "^6.38.3",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"lucide-svelte": "^0.544.0",
|
||||||
|
"svelte-codemirror-editor": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
2867
playground/pnpm-lock.yaml
generated
Normal file
2867
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';
|
13
playground/src/app.d.ts
vendored
Normal file
13
playground/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
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 |
91
playground/src/lib/components/EditorPanel.svelte
Normal file
91
playground/src/lib/components/EditorPanel.svelte
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CheckCircleIcon from '$lib/icons/CheckCircleIcon.svelte';
|
||||||
|
import MinusCircleIcon from '$lib/icons/MinusCircleIcon.svelte';
|
||||||
|
import XCircleIcon from '$lib/icons/XCircleIcon.svelte';
|
||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
|
import { basicSetup } from 'codemirror';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import CodeMirror from 'svelte-codemirror-editor';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
placeholder?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
children?: Snippet;
|
||||||
|
headerActions?: Snippet;
|
||||||
|
subtitle?: string;
|
||||||
|
status?: 'success' | 'error' | 'indeterminate';
|
||||||
|
timing?: number;
|
||||||
|
pillText?: string;
|
||||||
|
langExtension?: Extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
value = $bindable(),
|
||||||
|
placeholder = '',
|
||||||
|
readonly = false,
|
||||||
|
children,
|
||||||
|
headerActions,
|
||||||
|
subtitle,
|
||||||
|
status,
|
||||||
|
timing,
|
||||||
|
pillText,
|
||||||
|
langExtension
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-1 flex-col overflow-hidden border-r border-gray-200 last:border-r-0">
|
||||||
|
<div class="flex items-center border-b border-gray-200 bg-gray-50/50 px-4 py-3">
|
||||||
|
{#if status === 'success'}
|
||||||
|
<CheckCircleIcon class="mr-2 h-5 w-5 text-green-500" />
|
||||||
|
{:else if status === 'error'}
|
||||||
|
<XCircleIcon class="mr-2 h-5 w-5 text-red-500" />
|
||||||
|
{:else if status === 'indeterminate'}
|
||||||
|
<MinusCircleIcon class="mr-2 h-5 w-5 text-gray-400" />
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="flex items-center text-sm font-semibold uppercase tracking-wide text-gray-900">
|
||||||
|
{title}
|
||||||
|
{#if pillText}
|
||||||
|
<span
|
||||||
|
class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800"
|
||||||
|
>
|
||||||
|
{pillText}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
|
{#if subtitle}
|
||||||
|
<p class="text-xs text-gray-500">{subtitle}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if headerActions}
|
||||||
|
<div class="ml-4">
|
||||||
|
{@render headerActions()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if timing !== undefined}
|
||||||
|
<div class="ml-4 text-xs text-gray-500">{timing}ms</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="group relative flex flex-1 flex-col overflow-hidden">
|
||||||
|
<div class="flex-1 overflow-auto">
|
||||||
|
<CodeMirror
|
||||||
|
bind:value
|
||||||
|
extensions={[basicSetup, langExtension].filter(Boolean) as Extension[]}
|
||||||
|
{placeholder}
|
||||||
|
{readonly}
|
||||||
|
class="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 bg-black opacity-0 transition-opacity duration-200 group-hover:opacity-5"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if children}
|
||||||
|
{@render children()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
21
playground/src/lib/components/Header.svelte
Normal file
21
playground/src/lib/components/Header.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Logo from '$lib/icons/Logo.svelte';
|
||||||
|
import { GithubIcon } 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 px-6 py-4">
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Logo />
|
||||||
|
<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>
|
||||||
|
<a href="https://github.com/jim-fx/marka" target="_blank" rel="noopener noreferrer">
|
||||||
|
<GithubIcon class="h-6 w-6 text-gray-600 transition-colors duration-200 hover:text-black" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
189
playground/src/lib/components/Playground.svelte
Normal file
189
playground/src/lib/components/Playground.svelte
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { json } from '@codemirror/lang-json';
|
||||||
|
import { markdown } from '@codemirror/lang-markdown';
|
||||||
|
import {
|
||||||
|
getTemplate,
|
||||||
|
listTemplates,
|
||||||
|
matchBlocks,
|
||||||
|
parseMarkdown,
|
||||||
|
parseMarkdownWithTemplate,
|
||||||
|
wasmReady,
|
||||||
|
type ParseResultSuccess
|
||||||
|
} from '../wasm';
|
||||||
|
import EditorPanel from './EditorPanel.svelte';
|
||||||
|
|
||||||
|
let templates = $state([] as string[]);
|
||||||
|
|
||||||
|
const DEFAULT_MARKDOWN_VALUE = `---
|
||||||
|
_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`;
|
||||||
|
|
||||||
|
const DEFAULT_TEMPLATE_VALUE = '';
|
||||||
|
|
||||||
|
let templateValue = $state(
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
? localStorage.getItem('templateValue') || DEFAULT_TEMPLATE_VALUE
|
||||||
|
: DEFAULT_TEMPLATE_VALUE
|
||||||
|
);
|
||||||
|
let markdownValue = $state(
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
? localStorage.getItem('markdownValue') || DEFAULT_MARKDOWN_VALUE
|
||||||
|
: DEFAULT_MARKDOWN_VALUE
|
||||||
|
);
|
||||||
|
|
||||||
|
let jsonOutput = $state('');
|
||||||
|
let detectedSchemaName = $derived.by(() => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(jsonOutput)['_schema'];
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let timings = $state<ParseResultSuccess['timings'] | null>(null);
|
||||||
|
let templateStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
|
||||||
|
let dataStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('templateValue', templateValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('markdownValue', markdownValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($wasmReady) {
|
||||||
|
try {
|
||||||
|
templates = listTemplates();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$wasmReady) {
|
||||||
|
jsonOutput = 'Loading wasm...';
|
||||||
|
timings = null;
|
||||||
|
templateStatus = undefined;
|
||||||
|
dataStatus = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = templateValue
|
||||||
|
? parseMarkdownWithTemplate(markdownValue, templateValue)
|
||||||
|
: parseMarkdown(markdownValue);
|
||||||
|
|
||||||
|
if ('error' in result) {
|
||||||
|
jsonOutput = result.error;
|
||||||
|
if (result.error.startsWith('failed to compile template')) {
|
||||||
|
templateStatus = 'error';
|
||||||
|
dataStatus = 'indeterminate';
|
||||||
|
} else {
|
||||||
|
templateStatus = undefined;
|
||||||
|
dataStatus = 'error';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jsonOutput = JSON.stringify(result.data, null, 2);
|
||||||
|
timings = result.timings;
|
||||||
|
templateStatus = 'success';
|
||||||
|
dataStatus = 'success';
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
jsonOutput = (e as Error).message;
|
||||||
|
timings = null;
|
||||||
|
if (jsonOutput.startsWith('failed to compile template')) {
|
||||||
|
templateStatus = 'error';
|
||||||
|
dataStatus = 'indeterminate';
|
||||||
|
} else {
|
||||||
|
templateStatus = undefined;
|
||||||
|
dataStatus = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadTemplate(name: string) {
|
||||||
|
if (!name) return;
|
||||||
|
try {
|
||||||
|
templateValue = getTemplate(name);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetMarkdown() {
|
||||||
|
markdownValue = DEFAULT_MARKDOWN_VALUE;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-1 overflow-hidden">
|
||||||
|
<div class="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-3">
|
||||||
|
<EditorPanel
|
||||||
|
title="Template"
|
||||||
|
bind:value={templateValue}
|
||||||
|
placeholder="Enter your Marka template here..."
|
||||||
|
status={templateStatus}
|
||||||
|
timing={timings?.template_compilation}
|
||||||
|
subtitle="Define your mapping schema"
|
||||||
|
langExtension={markdown()}
|
||||||
|
>
|
||||||
|
{#snippet headerActions()}
|
||||||
|
<select
|
||||||
|
onchange={(e) => loadTemplate(e.currentTarget.value)}
|
||||||
|
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
<option value="">Load a template</option>
|
||||||
|
{#each templates as template (template)}
|
||||||
|
<option value={template}>{template}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</EditorPanel>
|
||||||
|
|
||||||
|
<EditorPanel
|
||||||
|
title="Markdown"
|
||||||
|
bind:value={markdownValue}
|
||||||
|
placeholder="Enter your markdown content here..."
|
||||||
|
timing={timings?.markdown_parsing}
|
||||||
|
subtitle="Your source content"
|
||||||
|
langExtension={markdown()}
|
||||||
|
>
|
||||||
|
{#snippet headerActions()}
|
||||||
|
<button
|
||||||
|
onclick={resetMarkdown}
|
||||||
|
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</EditorPanel>
|
||||||
|
|
||||||
|
<EditorPanel
|
||||||
|
title="Data"
|
||||||
|
value={jsonOutput}
|
||||||
|
readonly={true}
|
||||||
|
status={dataStatus}
|
||||||
|
subtitle="Parsed JSON output"
|
||||||
|
pillText={!templateValue && detectedSchemaName
|
||||||
|
? `Detected Template: ${detectedSchemaName}`
|
||||||
|
: undefined}
|
||||||
|
langExtension={json()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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>
|
18
playground/src/lib/icons/CheckCircleIcon.svelte
Normal file
18
playground/src/lib/icons/CheckCircleIcon.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { class: className = '' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
10
playground/src/lib/icons/Logo.svelte
Normal file
10
playground/src/lib/icons/Logo.svelte
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<img src="/logo.svg" alt="logo" width="100%" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
img {
|
||||||
|
height: 64px;
|
||||||
|
width: 64px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
filter: drop-shadow(0px 0px 8px #0002);
|
||||||
|
}
|
||||||
|
</style>
|
14
playground/src/lib/icons/MinusCircleIcon.svelte
Normal file
14
playground/src/lib/icons/MinusCircleIcon.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { class: className = '' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M18 12H6" />
|
||||||
|
</svg>
|
18
playground/src/lib/icons/XCircleIcon.svelte
Normal file
18
playground/src/lib/icons/XCircleIcon.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { class: className = '' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
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.
|
99
playground/src/lib/wasm.ts
Normal file
99
playground/src/lib/wasm.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { readable } from "svelte/store";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
Go: {
|
||||||
|
new(): {
|
||||||
|
run: (inst: WebAssembly.Instance) => Promise<void>;
|
||||||
|
importObject: WebAssembly.Imports;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
markaMatchBlocks: (input: string) => unknown;
|
||||||
|
markaParseFile: (input: string) => string;
|
||||||
|
markaParseFileWithTemplate: (markdown: string, template: string) => string;
|
||||||
|
markaListTemplates: () => string;
|
||||||
|
markaGetTemplate: (name: string) => string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wasmReady = readable(false, (set) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadWasm = async () => {
|
||||||
|
const go = new window.Go();
|
||||||
|
try {
|
||||||
|
const result = await WebAssembly.instantiateStreaming(
|
||||||
|
fetch("/main.wasm"),
|
||||||
|
go.importObject,
|
||||||
|
);
|
||||||
|
go.run(result.instance);
|
||||||
|
set(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading wasm module:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
loadWasm();
|
||||||
|
} else {
|
||||||
|
window.addEventListener("load", loadWasm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ParseResultSuccess = {
|
||||||
|
data: unknown;
|
||||||
|
timings: { [key: string]: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParseResultError = {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParseResult = ParseResultSuccess | ParseResultError;
|
||||||
|
|
||||||
|
export function parseMarkdown(markdown: string): ParseResult {
|
||||||
|
if (typeof window.markaParseFile !== "function") {
|
||||||
|
throw new Error("Wasm module not ready");
|
||||||
|
}
|
||||||
|
const result = window.markaParseFile(markdown);
|
||||||
|
if (result.error) return result;
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchBlocks(markdown: string): ParseResult {
|
||||||
|
if (typeof window.markaMatchBlocks !== "function") {
|
||||||
|
throw new Error("Wasm module not ready");
|
||||||
|
}
|
||||||
|
const result = window.markaMatchBlocks(markdown) as ParseResult;
|
||||||
|
if (result.error) return result;
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMarkdownWithTemplate(
|
||||||
|
markdown: string,
|
||||||
|
template: string,
|
||||||
|
): ParseResult {
|
||||||
|
if (typeof window.markaParseFileWithTemplate !== "function") {
|
||||||
|
throw new Error("Wasm module not ready");
|
||||||
|
}
|
||||||
|
const result = window.markaParseFileWithTemplate(markdown, template);
|
||||||
|
if (result.error) return result;
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listTemplates(): string[] {
|
||||||
|
if (typeof window.markaListTemplates !== "function") {
|
||||||
|
throw new Error("Wasm module not ready");
|
||||||
|
}
|
||||||
|
const result = window.markaListTemplates();
|
||||||
|
return JSON.parse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTemplate(name: string): string {
|
||||||
|
if (typeof window.markaGetTemplate !== "function") {
|
||||||
|
throw new Error("Wasm module not ready");
|
||||||
|
}
|
||||||
|
return window.markaGetTemplate(name);
|
||||||
|
}
|
14
playground/src/routes/+layout.svelte
Normal file
14
playground/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="icon" href={favicon} />
|
||||||
|
<title>Marka Playground</title>
|
||||||
|
<script src="/wasm_exec.js"></script>
|
||||||
|
</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';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-background text-foreground flex min-h-screen flex-col">
|
||||||
|
<Header />
|
||||||
|
<div class="flex flex-1 overflow-hidden">
|
||||||
|
<Playground />
|
||||||
|
</div>
|
||||||
|
</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>
|
15
playground/static/logo.svg
Normal file
15
playground/static/logo.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 50.0001C0 28.9476 19.9406 13.3665 43.3168 13.3665C52.852 13.3665 71.2871 17.0793 79.703 30.1981C79.703 30.1981 75.9901 41.3368 62.8713 43.5645C49.7525 45.7922 40.5941 30.4457 30.9406 30.4457C21.2871 30.4457 13.3663 38.7833 13.3663 51.9283C13.3663 59.1669 15.6639 65.9462 19.6134 71.7442C27.299 83.0278 41.2416 90.5942 56.6832 90.5942C80.0594 90.5942 100 71.3259 100 50.0001C100 77.6145 77.6144 100 50 100C22.3856 100 0 77.6145 0 50.0001Z" fill="url(#paint0_linear_12_27)"/>
|
||||||
|
<path d="M86.6337 51.9282C86.6337 60.2351 82.1782 70.1022 72.7723 74.505C63.3663 78.9077 57.4257 68.0693 43.3168 64.1089C29.2079 60.1485 19.6134 71.7441 19.6134 71.7441C27.299 83.0277 41.2416 90.5941 56.6832 90.5941C80.0594 90.5941 100 71.3257 100 50C100 22.3856 77.6144 0 50 0C22.3856 0 0 22.3856 0 50C0 28.9475 19.9406 13.3663 43.3168 13.3663C52.852 13.3663 61.9391 16.0842 69.3069 20.8153C80.0015 27.6822 87.0733 38.7896 86.6337 51.9802" fill="url(#paint1_linear_12_27)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_12_27" x1="75.1033" y1="92.9796" x2="22.9842" y2="7.71589" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="#545454"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_12_27" x1="23.9405" y1="7.17252" x2="75.3423" y2="92.0288" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="#545454"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
After Width: | Height: | Size: 1.5 KiB |
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
|
||||||
|
}
|
14
playground/vite.config.ts
Normal file
14
playground/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: [
|
||||||
|
"svelte-codemirror-editor",
|
||||||
|
"codemirror",
|
||||||
|
"@codemirror/language-javascript", /* ... */
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
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
|
99
playground/wasm/main.go
Normal file
99
playground/wasm/main.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
p "git.max-richter.dev/max/marka/parser"
|
||||||
|
"git.max-richter.dev/max/marka/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func matchBlocks(_ js.Value, args []js.Value) any {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return js.ValueOf(map[string]any{"error": "missing markdown"})
|
||||||
|
}
|
||||||
|
t, err := p.MatchBlocks(args[0].String(), args[1].String())
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonString, _ := json.Marshal(t)
|
||||||
|
|
||||||
|
return js.ValueOf(string(jsonString)) // plain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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)
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return js.ValueOf(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFileWithTemplate(_ js.Value, args []js.Value) any {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return js.ValueOf(map[string]any{"error": "missing markdown or template"})
|
||||||
|
}
|
||||||
|
res, err := p.ParseFileWithTemplate(args[0].String(), args[1].String())
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return js.ValueOf(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func listTemplates(_ js.Value, args []js.Value) any {
|
||||||
|
templates, err := registry.ListTemplates()
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(templates)
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return js.ValueOf(string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTemplate(_ js.Value, args []js.Value) any {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return js.ValueOf(map[string]any{"error": "missing template name"})
|
||||||
|
}
|
||||||
|
template, err := registry.GetTemplate(args[0].String())
|
||||||
|
if err != nil {
|
||||||
|
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return js.ValueOf(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
js.Global().Set("markaDetectType", js.FuncOf(detectType))
|
||||||
|
js.Global().Set("markaParseFile", js.FuncOf(parseFile))
|
||||||
|
js.Global().Set("markaParseFileWithTemplate", js.FuncOf(parseFileWithTemplate))
|
||||||
|
js.Global().Set("markaMatchBlocks", js.FuncOf(matchBlocks))
|
||||||
|
js.Global().Set("markaListTemplates", js.FuncOf(listTemplates))
|
||||||
|
js.Global().Set("markaGetTemplate", js.FuncOf(getTemplate))
|
||||||
|
select {}
|
||||||
|
}
|
@@ -1,3 +1,3 @@
|
|||||||
module git.max-richter.dev/max/marka/registry
|
module git.max-richter.dev/max/marka/registry
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.7
|
||||||
|
@@ -4,6 +4,9 @@ package registry
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates/*.marka
|
//go:embed templates/*.marka
|
||||||
@@ -23,3 +26,20 @@ func GetTemplate(name string) (string, error) {
|
|||||||
|
|
||||||
return string(templateBytes), nil
|
return string(templateBytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListTemplates() ([]string, error) {
|
||||||
|
var templateNames []string
|
||||||
|
err := fs.WalkDir(templates, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !d.IsDir() && strings.HasSuffix(path, ".marka") {
|
||||||
|
templateNames = append(templateNames, strings.TrimSuffix(filepath.Base(path), ".marka"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return templateNames, nil
|
||||||
|
}
|
||||||
|
@@ -3,20 +3,16 @@
|
|||||||
path: .
|
path: .
|
||||||
codec: yaml
|
codec: yaml
|
||||||
fields:
|
fields:
|
||||||
- path: "@context"
|
- path: "_schema"
|
||||||
codec: const
|
|
||||||
value: https://schema.org
|
|
||||||
hidden: true
|
|
||||||
- path: "@schema"
|
|
||||||
codec: const
|
codec: const
|
||||||
value: Article
|
value: Article
|
||||||
hidden: true
|
hidden: true
|
||||||
- path: "@type"
|
- path: "_type"
|
||||||
codec: const
|
codec: const
|
||||||
value: Article
|
value: Article
|
||||||
- path: image
|
- path: image
|
||||||
- path: author.name
|
- path: author.name
|
||||||
- path: author.@type
|
- path: author._type
|
||||||
codec: const
|
codec: const
|
||||||
value: Person
|
value: Person
|
||||||
hidden: true
|
hidden: true
|
||||||
|
@@ -3,19 +3,16 @@
|
|||||||
path: .
|
path: .
|
||||||
codec: yaml
|
codec: yaml
|
||||||
fields:
|
fields:
|
||||||
- path: "@context"
|
- path: "_schema"
|
||||||
codec: const
|
|
||||||
value: https://schema.org
|
|
||||||
hidden: true
|
|
||||||
- path: "@schema"
|
|
||||||
codec: const
|
codec: const
|
||||||
value: Recipe
|
value: Recipe
|
||||||
hidden: true
|
hidden: true
|
||||||
- path: "@type"
|
- path: "_type"
|
||||||
codec: const
|
codec: const
|
||||||
value: Recipe
|
value: Recipe
|
||||||
|
hidden: true
|
||||||
- path: image
|
- path: image
|
||||||
- path: author.@type
|
- path: author._type
|
||||||
codec: const
|
codec: const
|
||||||
hidden: true
|
hidden: true
|
||||||
value: Person
|
value: Person
|
||||||
@@ -33,7 +30,7 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
# { name | text }
|
# { name | text }
|
||||||
{ keywords | hashtags }
|
{ keywords | hashtags,optional }
|
||||||
|
|
||||||
{ description | text }
|
{ description | text }
|
||||||
|
|
||||||
|
@@ -3,21 +3,17 @@
|
|||||||
path: .
|
path: .
|
||||||
codec: yaml
|
codec: yaml
|
||||||
fields:
|
fields:
|
||||||
- path: "@context"
|
- path: "_schema"
|
||||||
codec: const
|
|
||||||
value: https://schema.org
|
|
||||||
hidden: true
|
|
||||||
- path: "@schema"
|
|
||||||
codec: const
|
codec: const
|
||||||
value: Review
|
value: Review
|
||||||
hidden: true
|
hidden: true
|
||||||
- path: "@type"
|
- path: "_type"
|
||||||
codec: const
|
codec: const
|
||||||
value: Review
|
value: Review
|
||||||
- path: tmdbId
|
- path: tmdbId
|
||||||
- path: image
|
- path: image
|
||||||
- path: author.name
|
- path: author.name
|
||||||
- path: author.@type
|
- path: author._type
|
||||||
codec: const
|
codec: const
|
||||||
value: Person
|
value: Person
|
||||||
hidden: true
|
hidden: true
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
path: .
|
path: .
|
||||||
codec: yaml
|
codec: yaml
|
||||||
fields:
|
fields:
|
||||||
- path: "@type"
|
- path: "_type"
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
@@ -13,8 +13,7 @@ import (
|
|||||||
const emptyBlock = "\uE000"
|
const emptyBlock = "\uE000"
|
||||||
|
|
||||||
func fixRenderedBlock(input string) string {
|
func fixRenderedBlock(input string) string {
|
||||||
input = strings.ReplaceAll(input, "'@type':", "@type:")
|
input = strings.ReplaceAll(input, "'_type':", "_type:")
|
||||||
input = strings.ReplaceAll(input, "'@context':", "@context:")
|
|
||||||
if len(input) == 0 {
|
if len(input) == 0 {
|
||||||
return emptyBlock
|
return emptyBlock
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
module git.max-richter.dev/max/marka/renderer
|
module git.max-richter.dev/max/marka/renderer
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e
|
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)
|
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) extract type from "@type" Property
|
// 2) extract type from "_type" Property
|
||||||
contentType, ok := data["@type"].(string)
|
contentType, ok := data["_type"].(string)
|
||||||
if !ok || contentType == "" {
|
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
|
// 3) get the template from the registry
|
||||||
|
@@ -41,10 +41,10 @@ func TestRenderFile_MissingType(t *testing.T) {
|
|||||||
rawJSON := []byte(`{"name": "Test"}`)
|
rawJSON := []byte(`{"name": "Test"}`)
|
||||||
_, err := renderer.RenderFile(rawJSON)
|
_, err := renderer.RenderFile(rawJSON)
|
||||||
if err == nil {
|
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") {
|
if !strings.Contains(err.Error(), "JSON does not contain a valid '_type' property") {
|
||||||
t.Errorf("expected missing @type error, got: %v", err)
|
t.Errorf("expected missing _type error, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,3 @@
|
|||||||
# Config file for Air - https://github.com/air-verse/air
|
|
||||||
# This file is for the marka-server application.
|
|
||||||
|
|
||||||
root = "."
|
root = "."
|
||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
@@ -7,7 +7,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.max-richter.dev/max/marka/server/internal/handlers"
|
"git.max-richter.dev/max/marka/server-new/internal/adapters"
|
||||||
|
"git.max-richter.dev/max/marka/server-new/internal/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -24,7 +25,10 @@ func main() {
|
|||||||
log.Fatal("root is not a directory")
|
log.Fatal("root is not a directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Handle("/", handlers.NewFile(absRoot))
|
fsAdapter, err := adapters.NewLocalFsAdapter(absRoot)
|
||||||
|
must(err)
|
||||||
|
|
||||||
|
http.Handle("/", handler.NewHandler(fsAdapter))
|
||||||
|
|
||||||
log.Printf("listening on %s, root=%s", *addr, absRoot)
|
log.Printf("listening on %s, root=%s", *addr, absRoot)
|
||||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||||
|
@@ -1,16 +1,3 @@
|
|||||||
module git.max-richter.dev/max/marka/server
|
module git.max-richter.dev/max/marka/server-new
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.7
|
||||||
|
|
||||||
require git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e
|
|
||||||
|
|
||||||
require (
|
|
||||||
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 // indirect
|
|
||||||
git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e // indirect
|
|
||||||
git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567 // indirect
|
|
||||||
git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a // indirect
|
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.1 // indirect
|
|
||||||
golang.org/x/text v0.14.0 // indirect
|
|
||||||
)
|
|
||||||
|
5
server/internal/adapters/errors.go
Normal file
5
server/internal/adapters/errors.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package adapters
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("not found")
|
97
server/internal/adapters/fs.go
Normal file
97
server/internal/adapters/fs.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package adapters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalFsAdapter struct {
|
||||||
|
root string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LocalFsAdapter) readDir(path string) (FsResponse, error) {
|
||||||
|
dirInfo, _ := os.Stat(path)
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return FsResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]FsDirEntry, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
info, _ := e.Info()
|
||||||
|
|
||||||
|
entryType := "dir"
|
||||||
|
if !e.IsDir() {
|
||||||
|
entryType = contentTypeFor(e.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, FsDirEntry{
|
||||||
|
Name: e.Name(),
|
||||||
|
Type: entryType,
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return FsResponse{
|
||||||
|
Dir: &FsDir{
|
||||||
|
Files: out,
|
||||||
|
Name: ResponsePath(l.root, path),
|
||||||
|
ModTime: dirInfo.ModTime(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LocalFsAdapter) readFile(path string) (FsResponse, error) {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return FsResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return FsResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return FsResponse{
|
||||||
|
File: &FsFile{
|
||||||
|
Name: ResponsePath(l.root, path),
|
||||||
|
Type: contentTypeFor(path),
|
||||||
|
ModTime: fi.ModTime(),
|
||||||
|
Content: data,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LocalFsAdapter) Read(path string) (FsResponse, error) {
|
||||||
|
cleanRel, err := SafeRel(l.root, path)
|
||||||
|
if err != nil {
|
||||||
|
return FsResponse{}, err
|
||||||
|
}
|
||||||
|
target := filepath.Join(l.root, filepath.FromSlash(cleanRel))
|
||||||
|
|
||||||
|
fi, err := os.Stat(target)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return FsResponse{}, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return FsResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.IsDir() {
|
||||||
|
return l.readDir(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.readFile(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LocalFsAdapter) Write(path string, content []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocalFsAdapter(root string) (FileAdapter, error) {
|
||||||
|
return LocalFsAdapter{
|
||||||
|
root: root,
|
||||||
|
}, nil
|
||||||
|
}
|
78
server/internal/adapters/fs_utils.go
Normal file
78
server/internal/adapters/fs_utils.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package adapters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"mime"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SafeRel(root, requested string) (string, error) {
|
||||||
|
s := requested
|
||||||
|
if after, ok := strings.CutPrefix(s, "/"); ok {
|
||||||
|
s = after
|
||||||
|
}
|
||||||
|
full := filepath.Join(root, filepath.FromSlash(s))
|
||||||
|
rel, err := filepath.Rel(root, full)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if rel == "." {
|
||||||
|
return "/", nil
|
||||||
|
}
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
if strings.HasPrefix(rel, "..") || strings.Contains(rel, ".."+sep) {
|
||||||
|
return "", errors.New("path escapes root")
|
||||||
|
}
|
||||||
|
return "/" + filepath.ToSlash(rel), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResponsePath(root, full string) string {
|
||||||
|
rel, err := filepath.Rel(root, full)
|
||||||
|
if err != nil || rel == "." {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
return "/" + filepath.ToSlash(rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeOrZero(fi os.FileInfo) int64 {
|
||||||
|
if fi == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return fi.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
func modTimeOrZero(fi os.FileInfo) time.Time {
|
||||||
|
if fi == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return fi.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
var textPlainExtensions = map[string]bool{
|
||||||
|
".txt": true,
|
||||||
|
".log": true,
|
||||||
|
".json": true,
|
||||||
|
".yaml": true,
|
||||||
|
".yml": true,
|
||||||
|
".toml": true,
|
||||||
|
".xml": true,
|
||||||
|
".csv": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func contentTypeFor(name string) string {
|
||||||
|
ext := strings.ToLower(filepath.Ext(name))
|
||||||
|
switch ext {
|
||||||
|
case ".md", ".markdown", ".mdown":
|
||||||
|
return "application/markdown"
|
||||||
|
}
|
||||||
|
if ct := mime.TypeByExtension(ext); ct != "" {
|
||||||
|
return ct
|
||||||
|
}
|
||||||
|
if textPlainExtensions[ext] {
|
||||||
|
return "text/plain; charset=utf-8"
|
||||||
|
}
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
34
server/internal/adapters/interface.go
Normal file
34
server/internal/adapters/interface.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Package adapters are the backend to that connects the marka server to a storage
|
||||||
|
package adapters
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type FileAdapter interface {
|
||||||
|
Read(path string) (FsResponse, error)
|
||||||
|
Write(path string, content []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FsFile struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content []byte `json:"content"`
|
||||||
|
ModTime time.Time `json:"modTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FsDirEntry struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
IsDir bool `json:"isDir,omitempty"`
|
||||||
|
ModTime time.Time `json:"modTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FsDir struct {
|
||||||
|
Files []FsDirEntry `json:"files"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ModTime time.Time `json:"modTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FsResponse struct {
|
||||||
|
Dir *FsDir `json:"dir,omitempty"`
|
||||||
|
File *FsFile `json:"file,omitempty"`
|
||||||
|
}
|
@@ -1,33 +0,0 @@
|
|||||||
package fsx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mime"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var textPlainExtensions = map[string]bool{
|
|
||||||
".txt": true,
|
|
||||||
".log": true,
|
|
||||||
".json": true,
|
|
||||||
".yaml": true,
|
|
||||||
".yml": true,
|
|
||||||
".toml": true,
|
|
||||||
".xml": true,
|
|
||||||
".csv": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContentTypeFor(name string) string {
|
|
||||||
ext := strings.ToLower(filepath.Ext(name))
|
|
||||||
switch ext {
|
|
||||||
case ".md", ".markdown", ".mdown":
|
|
||||||
return "application/markdown"
|
|
||||||
}
|
|
||||||
if ct := mime.TypeByExtension(ext); ct != "" {
|
|
||||||
return ct
|
|
||||||
}
|
|
||||||
if textPlainExtensions[ext] {
|
|
||||||
return "text/plain; charset=utf-8"
|
|
||||||
}
|
|
||||||
return "application/octet-stream"
|
|
||||||
}
|
|
@@ -1,56 +0,0 @@
|
|||||||
package fsx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CleanURLLike(p string) string {
|
|
||||||
p = strings.TrimSpace(p)
|
|
||||||
if p == "" || p == "/" {
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
parts := []string{}
|
|
||||||
for seg := range strings.SplitSeq(strings.ReplaceAll(p, "/", "/"), "/") {
|
|
||||||
switch seg {
|
|
||||||
case "", ".":
|
|
||||||
continue
|
|
||||||
case "..":
|
|
||||||
if len(parts) > 0 {
|
|
||||||
parts = parts[:len(parts)-1]
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
parts = append(parts, seg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "/" + strings.Join(parts, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func SafeRel(root, requested string) (string, error) {
|
|
||||||
s := CleanURLLike(requested)
|
|
||||||
if after, ok := strings.CutPrefix(s, "/"); ok {
|
|
||||||
s = after
|
|
||||||
}
|
|
||||||
full := filepath.Join(root, filepath.FromSlash(s))
|
|
||||||
rel, err := filepath.Rel(root, full)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if rel == "." {
|
|
||||||
return "/", nil
|
|
||||||
}
|
|
||||||
sep := string(filepath.Separator)
|
|
||||||
if strings.HasPrefix(rel, "..") || strings.Contains(rel, ".."+sep) {
|
|
||||||
return "", errors.New("path escapes root")
|
|
||||||
}
|
|
||||||
return "/" + filepath.ToSlash(rel), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResponsePath(root, full string) string {
|
|
||||||
rel, err := filepath.Rel(root, full)
|
|
||||||
if err != nil || rel == "." {
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
return "/" + filepath.ToSlash(rel)
|
|
||||||
}
|
|
@@ -1,4 +1,4 @@
|
|||||||
package httpx
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -9,12 +9,12 @@ type ErrorResponse struct {
|
|||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteJSON(w http.ResponseWriter, code int, v any) {
|
func writeError(w http.ResponseWriter, code int, err error) {
|
||||||
|
writeJSON(w, code, ErrorResponse{Error: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, code int, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteError(w http.ResponseWriter, code int, err error) {
|
|
||||||
WriteJSON(w, code, ErrorResponse{Error: err.Error()})
|
|
||||||
}
|
|
104
server/internal/handler/handler.go
Normal file
104
server/internal/handler/handler.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// Package handler provides the HTTP handler for the marka server
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.max-richter.dev/max/marka/parser"
|
||||||
|
"git.max-richter.dev/max/marka/server-new/internal/adapters"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResponseItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Content any `json:"content,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
IsDir bool `json:"isDir"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
ModTime time.Time `json:"modTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
adapter adapters.FileAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) get(w http.ResponseWriter, target string) {
|
||||||
|
fsEntry, err := h.adapter.Read(target)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fsEntry.File != nil {
|
||||||
|
|
||||||
|
if fsEntry.File.Content != nil && fsEntry.File.Type == "application/markdown" {
|
||||||
|
data, err := parser.ParseFile(string(fsEntry.File.Content))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := ResponseItem{
|
||||||
|
Name: fsEntry.File.Name,
|
||||||
|
Type: fsEntry.File.Type,
|
||||||
|
Content: data,
|
||||||
|
IsDir: false,
|
||||||
|
Size: int64(len(fsEntry.File.Content)),
|
||||||
|
ModTime: fsEntry.File.ModTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, 200, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := ResponseItem{
|
||||||
|
Name: fsEntry.File.Name,
|
||||||
|
Content: fsEntry.File.Content,
|
||||||
|
Type: fsEntry.File.Type,
|
||||||
|
IsDir: false,
|
||||||
|
Size: int64(len(fsEntry.File.Content)),
|
||||||
|
ModTime: fsEntry.File.ModTime,
|
||||||
|
}
|
||||||
|
writeJSON(w, 200, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fsEntry.Dir != nil {
|
||||||
|
res := ResponseItem{
|
||||||
|
Name: fsEntry.Dir.Name,
|
||||||
|
Content: fsEntry.Dir.Files,
|
||||||
|
IsDir: true,
|
||||||
|
ModTime: fsEntry.Dir.ModTime,
|
||||||
|
}
|
||||||
|
writeJSON(w, 200, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) post(w http.ResponseWriter, target string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqPath := r.URL.Path
|
||||||
|
if reqPath == "" {
|
||||||
|
reqPath = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
target := cleanURLLike(reqPath)
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
h.get(w, target)
|
||||||
|
case http.MethodPost:
|
||||||
|
h.post(w, target)
|
||||||
|
default:
|
||||||
|
writeError(w, http.StatusMethodNotAllowed, errors.New("method not allowed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(adapter adapters.FileAdapter) http.Handler {
|
||||||
|
return &Handler{
|
||||||
|
adapter: adapter,
|
||||||
|
}
|
||||||
|
}
|
24
server/internal/handler/utils.go
Normal file
24
server/internal/handler/utils.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func cleanURLLike(p string) string {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" || p == "/" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
parts := []string{}
|
||||||
|
for seg := range strings.SplitSeq(p, "/") {
|
||||||
|
switch seg {
|
||||||
|
case "", ".":
|
||||||
|
continue
|
||||||
|
case "..":
|
||||||
|
if len(parts) > 0 {
|
||||||
|
parts = parts[:len(parts)-1]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
parts = append(parts, seg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "/" + strings.Join(parts, "/")
|
||||||
|
}
|
@@ -1,38 +0,0 @@
|
|||||||
// Package handlers provides HTTP handlers for the file system.
|
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"git.max-richter.dev/max/marka/server/internal/fsx"
|
|
||||||
"git.max-richter.dev/max/marka/server/internal/httpx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type File struct{ root string }
|
|
||||||
|
|
||||||
func NewFile(root string) http.Handler { return &File{root: root} }
|
|
||||||
|
|
||||||
func (h *File) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
reqPath := r.URL.Path
|
|
||||||
if reqPath == "" {
|
|
||||||
reqPath = "/"
|
|
||||||
}
|
|
||||||
cleanRel, err := fsx.SafeRel(h.root, reqPath)
|
|
||||||
if err != nil {
|
|
||||||
httpx.WriteError(w, http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
target := filepath.Join(h.root, filepath.FromSlash(cleanRel))
|
|
||||||
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
h.get(w, r, target)
|
|
||||||
case http.MethodPost:
|
|
||||||
h.post(w, r, target)
|
|
||||||
default:
|
|
||||||
httpx.WriteError(w, http.StatusMethodNotAllowed, errors.New("method not allowed"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,93 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.max-richter.dev/max/marka/server/internal/fsx"
|
|
||||||
"git.max-richter.dev/max/marka/server/internal/httpx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *File) get(w http.ResponseWriter, r *http.Request, target string) {
|
|
||||||
fi, err := os.Stat(target)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
httpx.WriteError(w, http.StatusNotFound, errors.New("not found"))
|
|
||||||
} else {
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if fi.IsDir() {
|
|
||||||
h.handleDir(w, r, target)
|
|
||||||
} else {
|
|
||||||
h.handleFile(w, r, target, fi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *File) handleDir(w http.ResponseWriter, r *http.Request, dirPath string) {
|
|
||||||
entries, err := os.ReadDir(dirPath)
|
|
||||||
if err != nil {
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type item struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Content any `json:"content,omitempty"`
|
|
||||||
IsDir bool `json:"isDir"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
ModTime time.Time `json:"modTime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
out := make([]item, 0, len(entries))
|
|
||||||
for _, e := range entries {
|
|
||||||
info, _ := e.Info() // As in original, ignore error to get partial info
|
|
||||||
entryPath := filepath.Join(dirPath, e.Name())
|
|
||||||
|
|
||||||
var content any
|
|
||||||
if !e.IsDir() && fsx.ContentTypeFor(e.Name()) == "application/markdown" {
|
|
||||||
content, _ = parseMarkdownFile(entryPath) // As in original, ignore error
|
|
||||||
}
|
|
||||||
|
|
||||||
out = append(out, item{
|
|
||||||
Name: e.Name(),
|
|
||||||
Path: fsx.ResponsePath(h.root, entryPath),
|
|
||||||
IsDir: e.IsDir(),
|
|
||||||
Content: content,
|
|
||||||
Size: sizeOrZero(info),
|
|
||||||
ModTime: modTimeOrZero(info),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
httpx.WriteJSON(w, http.StatusOK, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *File) handleFile(w http.ResponseWriter, r *http.Request, target string, fi os.FileInfo) {
|
|
||||||
contentType := fsx.ContentTypeFor(target)
|
|
||||||
|
|
||||||
if contentType == "application/markdown" {
|
|
||||||
res, err := parseMarkdownFile(target)
|
|
||||||
if err != nil {
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, fmt.Errorf("failed to parse markdown: %w", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
httpx.WriteJSON(w, http.StatusOK, res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(target)
|
|
||||||
if err != nil {
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
http.ServeContent(w, r, filepath.Base(target), fi.ModTime(), f)
|
|
||||||
}
|
|
@@ -1,30 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.max-richter.dev/max/marka/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
func parseMarkdownFile(path string) (any, error) {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return parser.ParseFile(string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sizeOrZero(fi os.FileInfo) int64 {
|
|
||||||
if fi == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return fi.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
func modTimeOrZero(fi os.FileInfo) time.Time {
|
|
||||||
if fi == nil {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return fi.ModTime()
|
|
||||||
}
|
|
@@ -1,47 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"git.max-richter.dev/max/marka/server/internal/httpx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *File) post(w http.ResponseWriter, r *http.Request, target string) {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpPath := target + ".tmp~"
|
|
||||||
f, err := os.Create(tmpPath)
|
|
||||||
if err != nil {
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, copyErr := io.Copy(f, r.Body)
|
|
||||||
closeErr := f.Close()
|
|
||||||
|
|
||||||
if copyErr != nil {
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, copyErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if closeErr != nil {
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, closeErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Rename(tmpPath, target); err != nil {
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
httpx.WriteError(w, http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Location", r.URL.Path)
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
package httpx
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
type Router struct{ mux *http.ServeMux }
|
|
||||||
|
|
||||||
func NewRouter() *Router { return &Router{mux: http.NewServeMux()} }
|
|
||||||
func (r *Router) Handle(pattern string, h http.Handler) { r.mux.Handle(pattern, h) }
|
|
||||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
r.mux.ServeHTTP(w, req)
|
|
||||||
}
|
|
@@ -19,8 +19,7 @@ func parseShortTemplate(input string) (Block, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(split) > 1 {
|
if len(split) > 1 {
|
||||||
optionSplit := strings.SplitSeq(split[1], ",")
|
for option := range strings.SplitSeq(split[1], ",") {
|
||||||
for option := range optionSplit {
|
|
||||||
switch strings.TrimSpace(option) {
|
switch strings.TrimSpace(option) {
|
||||||
case "number":
|
case "number":
|
||||||
block.Codec = CodecNumber
|
block.Codec = CodecNumber
|
||||||
@@ -28,6 +27,8 @@ func parseShortTemplate(input string) (Block, error) {
|
|||||||
block.Codec = CodecText
|
block.Codec = CodecText
|
||||||
case "hashtags":
|
case "hashtags":
|
||||||
block.Codec = CodecHashtags
|
block.Codec = CodecHashtags
|
||||||
|
case "optional":
|
||||||
|
block.Optional = true
|
||||||
default:
|
default:
|
||||||
return block, fmt.Errorf("unknown codec option: %s", option)
|
return block, fmt.Errorf("unknown codec option: %s", option)
|
||||||
}
|
}
|
||||||
|
@@ -30,13 +30,13 @@ func TestExtractBlocks(t *testing.T) {
|
|||||||
Path: ".",
|
Path: ".",
|
||||||
Fields: []template.BlockField{
|
Fields: []template.BlockField{
|
||||||
{
|
{
|
||||||
Path: "@type",
|
Path: "_type",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Path: "image",
|
Path: "image",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Path: "author.@type",
|
Path: "author._type",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Path: "author.name",
|
Path: "author.name",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
module git.max-richter.dev/max/marka/template
|
module git.max-richter.dev/max/marka/template
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e
|
git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e
|
||||||
|
@@ -20,6 +20,7 @@ type Block struct {
|
|||||||
Codec CodecType
|
Codec CodecType
|
||||||
ListTemplate string
|
ListTemplate string
|
||||||
Fields []BlockField
|
Fields []BlockField
|
||||||
|
Optional bool
|
||||||
Value any
|
Value any
|
||||||
content string
|
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
|
author.name: John Doe
|
||||||
---
|
---
|
||||||
|
|
||||||
|
7
testdata/data/article_simple/output.json
vendored
7
testdata/data/article_simple/output.json
vendored
@@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"_type": "Article",
|
||||||
"@type": "Article",
|
|
||||||
"headline": "My First Article",
|
"headline": "My First Article",
|
||||||
"author": {
|
"author": {
|
||||||
"@type": "Person",
|
"_type": "Person",
|
||||||
"name": "John Doe"
|
"name": "John Doe"
|
||||||
},
|
},
|
||||||
"articleBody": "This is the content of my first article. It's a simple one."
|
"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
|
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",
|
"name": "Baguette",
|
||||||
"author": {
|
"author": {
|
||||||
"@type": "Person",
|
"_type": "Person",
|
||||||
"name": "Max Richter"
|
"name": "Max Richter"
|
||||||
},
|
},
|
||||||
"description": "My favourite baguette recipe",
|
"description": "My favourite baguette recipe",
|
||||||
|
6
testdata/data/complex_front_matter/input.md
vendored
6
testdata/data/complex_front_matter/input.md
vendored
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
@type: Book
|
_type: Book
|
||||||
name: The Great Book
|
name: The Great Book
|
||||||
author:
|
author:
|
||||||
@type: Person
|
_type: Person
|
||||||
name: Jane Doe
|
name: Jane Doe
|
||||||
email: jane.doe@example.com
|
email: jane.doe@example.com
|
||||||
tags:
|
tags:
|
||||||
@@ -17,4 +17,4 @@ chapters:
|
|||||||
|
|
||||||
# The Great Book
|
# The Great Book
|
||||||
|
|
||||||
This is the content of the great book.
|
This is the content of the great book.
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"_type": "Book",
|
||||||
"@type": "Book",
|
|
||||||
"name": "The Great Book",
|
"name": "The Great Book",
|
||||||
"author": {
|
"author": {
|
||||||
"@type": "Person",
|
"_type": "Person",
|
||||||
"name": "Jane Doe",
|
"name": "Jane Doe",
|
||||||
"email": "jane.doe@example.com"
|
"email": "jane.doe@example.com"
|
||||||
},
|
},
|
||||||
@@ -23,4 +22,4 @@
|
|||||||
],
|
],
|
||||||
"headline": "The Great Book",
|
"headline": "The Great Book",
|
||||||
"articleBody": "This is the content of the great book."
|
"articleBody": "This is the content of the great book."
|
||||||
}
|
}
|
||||||
|
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
|
author.name: Alex Chef
|
||||||
cookTime: PT0M
|
cookTime: PT0M
|
||||||
image: https://example.com/salad.jpg
|
image: https://example.com/salad.jpg
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"_schema": "Recipe",
|
||||||
"@schema": "Recipe",
|
"_type": "Recipe",
|
||||||
"@type": "Recipe",
|
|
||||||
"name": "Simple Salad",
|
"name": "Simple Salad",
|
||||||
"image": "https://example.com/salad.jpg",
|
"image": "https://example.com/salad.jpg",
|
||||||
"author": {
|
"author": {
|
||||||
"@type": "Person",
|
"_type": "Person",
|
||||||
"name": "Alex Chef"
|
"name": "Alex Chef"
|
||||||
},
|
},
|
||||||
"prepTime": "PT10M",
|
"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
|
author.name: Alex Chef
|
||||||
cookTime: PT0M
|
cookTime: PT0M
|
||||||
image: https://example.com/salad.jpg
|
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",
|
||||||
"@schema": "Recipe",
|
"_type": "Recipe",
|
||||||
"@type": "Recipe",
|
|
||||||
"name": "Simple Salad",
|
"name": "Simple Salad",
|
||||||
"image": "https://example.com/salad.jpg",
|
"image": "https://example.com/salad.jpg",
|
||||||
"author": {
|
"author": {
|
||||||
"@type": "Person",
|
"_type": "Person",
|
||||||
"name": "Alex Chef"
|
"name": "Alex Chef"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
4
testdata/data/typo_section_header/input.md
vendored
4
testdata/data/typo_section_header/input.md
vendored
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
@type: Recipe
|
_type: Recipe
|
||||||
name: Typo Recipe
|
name: Typo Recipe
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,4 +11,4 @@ name: Typo Recipe
|
|||||||
|
|
||||||
## Stps
|
## Stps
|
||||||
1. Step 1
|
1. Step 1
|
||||||
2. Step 2
|
2. Step 2
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"_type": "Recipe",
|
||||||
"@type": "Recipe",
|
|
||||||
"name": "Typo Recipe",
|
"name": "Typo Recipe",
|
||||||
"recipeIngredient": [
|
"recipeIngredient": [
|
||||||
"Item 1",
|
"Item 1",
|
||||||
@@ -10,4 +9,4 @@
|
|||||||
"Step 1",
|
"Step 1",
|
||||||
"Step 2"
|
"Step 2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
2
testdata/go.mod
vendored
2
testdata/go.mod
vendored
@@ -1,3 +1,3 @@
|
|||||||
module git.max-richter.dev/max/marka/testdata
|
module git.max-richter.dev/max/marka/testdata
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.7
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
module git.max-richter.dev/max/marka/validator
|
module git.max-richter.dev/max/marka/validator
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e
|
git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func TestValidateRecipe_InvalidType(t *testing.T) {
|
func TestValidateRecipe_InvalidType(t *testing.T) {
|
||||||
recipe := map[string]any{
|
recipe := map[string]any{
|
||||||
"@type": "Recipe",
|
"_type": "Recipe",
|
||||||
"recipeYield": 4,
|
"recipeYield": 4,
|
||||||
"recipeIngredient": []string{
|
"recipeIngredient": []string{
|
||||||
"500 g flour",
|
"500 g flour",
|
||||||
@@ -24,7 +24,7 @@ func TestValidateRecipe_InvalidType(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateRecipe_Valid(t *testing.T) {
|
func TestValidateRecipe_Valid(t *testing.T) {
|
||||||
recipe := map[string]any{
|
recipe := map[string]any{
|
||||||
"@type": "Recipe",
|
"_type": "Recipe",
|
||||||
"name": "Simple Bread",
|
"name": "Simple Bread",
|
||||||
"cookTime": "PT30M",
|
"cookTime": "PT30M",
|
||||||
"recipeIngredient": []any{"500 g flour", "300 ml water", "10 g salt", "3 g yeast"},
|
"recipeIngredient": []any{"500 g flour", "300 ml water", "10 g salt", "3 g yeast"},
|
||||||
|
Reference in New Issue
Block a user