Compare commits
29 Commits
ae5cd8481a
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
7b3261caf9
|
||
|
0ee8f8b655
|
||
|
881afa4b6a
|
||
|
fa8e0a82f7
|
||
|
b94dbca69b
|
||
|
951f07b25e
|
||
|
b763ede6cb
|
||
|
76848b396c
|
||
|
0970a662b8
|
||
|
5adc3a3cc1
|
||
|
6d92c92797
|
||
|
fa283d5dd7
|
||
|
a5acef77a2
|
||
|
74528625e3
|
||
|
ab81c980b5
|
||
3986e22dd3
|
|||
8140ea856c
|
|||
|
c2179c6d22
|
||
|
2a1572f99d
|
||
|
d35f3e5e2e
|
||
|
57ea1f6e3e
|
||
|
fb0b80be78
|
||
|
a7ee527ff5
|
||
|
198099f392
|
||
|
8d882ee571
|
||
|
eab2a55f5e
|
||
|
8ca5152c3c
|
||
|
ea978b48f6
|
||
|
d302c9b098
|
32
README.md
32
README.md
@@ -8,13 +8,43 @@ Bidirectional mapping between Markdown and JSON (Schema.org-style) via small, de
|
||||
* **Declarative Templates:** Define your mappings with concise, easy-to-understand templates.
|
||||
* **JSON Schema Validation:** Validate parsed JSON against Schema.org entities using JSON Schema.
|
||||
|
||||
## How it works
|
||||
|
||||
Marka uses a custom template file to enable bidirectional between Markdown and Data. A minimal `marka` schema could look like:
|
||||
|
||||
```markdown
|
||||
# { articleTitle }
|
||||
|
||||
{ articleBody }
|
||||
```
|
||||
|
||||
Then it could parse the following markdown:
|
||||
|
||||
```markdown
|
||||
# MyArticle
|
||||
|
||||
The article body which could
|
||||
contain newlines.
|
||||
```
|
||||
|
||||
Into the following data:
|
||||
|
||||
```json
|
||||
{
|
||||
"articleTitle": "MyArticle",
|
||||
"articleBody": "The article body which could\ncontain newlines.",
|
||||
}
|
||||
```
|
||||
|
||||
And reversely if you give it the json it could reproduce the input markdown. If you want to dive into the deeper workings of the template look at [template/README.md](./template/README.md)
|
||||
|
||||
## Docker Image (CRUD API)
|
||||
|
||||
A Docker image is available to transform a specified directory into a CRUD API. This allows you to expose your Markdown-to-JSON mappings as a web service.
|
||||
|
||||
To run the Docker image:
|
||||
```bash
|
||||
docker run -p 8080:8080 -v /path/to/your/data:/app/data marka-crud-api
|
||||
docker run -p 8080:8080 -v /path/to/your/data:/app/data max/marka-server
|
||||
```
|
||||
*(Replace `/path/to/your/data` with the absolute path to the directory you want to expose.)*
|
||||
|
||||
|
@@ -20,21 +20,27 @@ func ParseBlock(input string, block template.Block) (any, error) {
|
||||
case template.CodecHashtags:
|
||||
return Keywords(input, block)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown codec: %s", block.Codec)
|
||||
fmt.Printf("%#v\n", block)
|
||||
return nil, fmt.Errorf("unknown codec '%s'", block.Codec)
|
||||
}
|
||||
|
||||
func Parse(matches []matcher.Block) (any, error) {
|
||||
var result any
|
||||
|
||||
for _, m := range matches {
|
||||
for i, m := range matches {
|
||||
if m.Block.Path == "@index" {
|
||||
continue
|
||||
}
|
||||
|
||||
input := m.GetContent()
|
||||
value, err := ParseBlock(input, m.Block)
|
||||
var blockIdentifier any
|
||||
blockIdentifier = m.Block.Path
|
||||
if blockIdentifier == "" {
|
||||
blockIdentifier = fmt.Sprintf("#%d", i)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse block(%s): %w", m.Block.Path, err)
|
||||
return nil, fmt.Errorf("failed to parse block(%s) -> %w", blockIdentifier, err)
|
||||
}
|
||||
result = utils.SetPathValue(m.Block.Path, value, result)
|
||||
}
|
||||
|
@@ -50,7 +50,6 @@ func MatchBlocksFuzzy(markdown string, templateBlocks []template.Block, maxDist
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the last block
|
||||
if len(templateBlocks) > 0 {
|
||||
lastBlock := templateBlocks[len(templateBlocks)-1]
|
||||
if lastBlock.Type == template.DataBlock {
|
||||
|
@@ -10,21 +10,21 @@ import (
|
||||
"git.max-richter.dev/max/marka/testdata"
|
||||
)
|
||||
|
||||
func TestFuzzyFindAll(t *testing.T) {
|
||||
func TestMatch_FuzzyFindAll(t *testing.T) {
|
||||
recipeMd := testdata.Read(t, "baguette/input.md")
|
||||
|
||||
tests := []struct {
|
||||
Needle string
|
||||
Start, End, StartIndex int
|
||||
}{
|
||||
{StartIndex: 0, Needle: "# Ingredients\n", Start: 77, End: 91},
|
||||
{StartIndex: 0, Needle: "# Ingrdients\n", Start: 77, End: 91},
|
||||
{StartIndex: 0, Needle: "# Inrdients\n", Start: 77, End: 91},
|
||||
{StartIndex: 0, Needle: "## Ingredients\n", Start: 90, End: 105},
|
||||
{StartIndex: 0, Needle: "## Ingrdients\n", Start: 90, End: 105},
|
||||
{StartIndex: 0, Needle: "## Inrdients\n", Start: 90, End: 105},
|
||||
{StartIndex: 0, Needle: "---\n", Start: 0, End: 4},
|
||||
{StartIndex: 4, Needle: "---\n", Start: 29, End: 33},
|
||||
{StartIndex: 0, Needle: "# Steps\n", Start: 116, End: 124},
|
||||
{StartIndex: 0, Needle: "# Stps\n", Start: 116, End: 124},
|
||||
{StartIndex: 0, Needle: "# Step\n", Start: 116, End: 124},
|
||||
{StartIndex: 4, Needle: "---\n", Start: 43, End: 47},
|
||||
{StartIndex: 0, Needle: "## Steps\n", Start: 129, End: 138},
|
||||
{StartIndex: 0, Needle: "## Stps\n", Start: 129, End: 138},
|
||||
{StartIndex: 0, Needle: "## Step\n", Start: 129, End: 138},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -36,13 +36,14 @@ func TestFuzzyFindAll(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyBlockMatch(t *testing.T) {
|
||||
func TestMatch_FuzzyBlockBaguette(t *testing.T) {
|
||||
recipeMd := testdata.Read(t, "baguette/input.md")
|
||||
schemaMd, err := registry.GetTemplate("Recipe")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load template: %s", err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
blocks, err := template.CompileTemplate(schemaMd)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to compile template: %s", err.Error())
|
||||
@@ -51,6 +52,10 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
||||
|
||||
matches := matcher.MatchBlocksFuzzy(string(recipeMd), blocks, 0.3)
|
||||
|
||||
for _, m := range matches {
|
||||
fmt.Printf("Content: '%s'->'%q'\n\n", m.Block.Path, m.GetContent())
|
||||
}
|
||||
|
||||
expected := []struct {
|
||||
value string
|
||||
}{
|
||||
@@ -61,10 +66,7 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
||||
value: "Baguette",
|
||||
},
|
||||
{
|
||||
value: "\nMy favourite baguette recipe",
|
||||
},
|
||||
{
|
||||
value: "",
|
||||
value: "My favourite baguette recipe",
|
||||
},
|
||||
{
|
||||
value: "- Flour\n- Water\n- Salt",
|
||||
@@ -80,13 +82,12 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
||||
t.FailNow()
|
||||
}
|
||||
if expected[i].value != m.GetContent() {
|
||||
t.Errorf("Match %d did not match expected: %q", i, m.GetContent())
|
||||
t.Errorf("Match %d did not match expected: %q", i, expected[i].value)
|
||||
}
|
||||
fmt.Printf("match: %s->%q\n", m.Block.Path, m.GetContent())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyBlockMatchSalad(t *testing.T) {
|
||||
func TestMatch_FuzzyBlockSalad(t *testing.T) {
|
||||
recipeMd := testdata.Read(t, "recipe_salad/input.md")
|
||||
schemaMd, err := registry.GetTemplate("Recipe")
|
||||
if err != nil {
|
||||
@@ -110,9 +111,6 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
|
||||
{
|
||||
value: "Simple Salad",
|
||||
},
|
||||
{
|
||||
value: "#healthy #salad",
|
||||
},
|
||||
{
|
||||
value: "A quick green salad.",
|
||||
},
|
||||
|
@@ -5,7 +5,6 @@ package parser
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.max-richter.dev/max/marka/parser/decoders"
|
||||
"git.max-richter.dev/max/marka/parser/matcher"
|
||||
@@ -16,19 +15,19 @@ import (
|
||||
func DetectType(markdownContent string) (string, error) {
|
||||
defaultSchemaContent, err := registry.GetTemplate("_default")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not get schema: %w", err)
|
||||
return "", fmt.Errorf("could not get schema -> %w", err)
|
||||
}
|
||||
|
||||
defaultSchema, err := template.CompileTemplate(defaultSchemaContent)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to compile template: %w", err)
|
||||
return "", fmt.Errorf("failed to compile template -> %w", err)
|
||||
}
|
||||
|
||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, defaultSchema, 0.3)
|
||||
|
||||
result, err := decoders.Parse(blocks)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse blocks: %w", err)
|
||||
return "", fmt.Errorf("failed to parse blocks -> %w", err)
|
||||
}
|
||||
|
||||
if result, ok := result.(map[string]any); ok {
|
||||
@@ -37,6 +36,7 @@ func DetectType(markdownContent string) (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("frontmatter did not contain '_type'")
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not parse frontmatter")
|
||||
}
|
||||
|
||||
@@ -45,80 +45,40 @@ func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, erro
|
||||
|
||||
tpl, err := template.CompileTemplate(templateContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile template: %w", err)
|
||||
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
||||
}
|
||||
|
||||
return matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3), nil
|
||||
}
|
||||
|
||||
func ParseFile(markdownContent string) (any, error) {
|
||||
timings := make(map[string]int64)
|
||||
|
||||
startDetectType := time.Now()
|
||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||
|
||||
contentType, err := DetectType(markdownContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not detect type: %w", err)
|
||||
return nil, fmt.Errorf("could not detect type -> %w", err)
|
||||
}
|
||||
timings["detect_type"] = time.Since(startDetectType).Milliseconds()
|
||||
|
||||
startGetTemplate := time.Now()
|
||||
templateContent, err := registry.GetTemplate(contentType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get schema: %w", err)
|
||||
}
|
||||
timings["get_template"] = time.Since(startGetTemplate).Milliseconds()
|
||||
|
||||
startTemplate := time.Now()
|
||||
tpl, err := template.CompileTemplate(templateContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile template: %w", err)
|
||||
}
|
||||
timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
|
||||
|
||||
startMarkdown := time.Now()
|
||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
||||
|
||||
result, err := decoders.Parse(blocks)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse blocks: %w", err)
|
||||
}
|
||||
timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
|
||||
|
||||
response := map[string]any{
|
||||
"data": result,
|
||||
"timings": timings,
|
||||
return nil, fmt.Errorf("could not get template -> %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return ParseFileWithTemplate(markdownContent, templateContent)
|
||||
}
|
||||
|
||||
func ParseFileWithTemplate(markdownContent string, templateContent string) (any, error) {
|
||||
timings := make(map[string]int64)
|
||||
|
||||
startTemplate := time.Now()
|
||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||
|
||||
tpl, err := template.CompileTemplate(templateContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile template: %w", err)
|
||||
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
||||
}
|
||||
timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
|
||||
|
||||
startMarkdown := time.Now()
|
||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
||||
|
||||
result, err := decoders.Parse(blocks)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse blocks: %w", err)
|
||||
}
|
||||
timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
|
||||
|
||||
response := map[string]any{
|
||||
"data": result,
|
||||
"timings": timings,
|
||||
return nil, fmt.Errorf("failed to compile blocks -> %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return result, nil
|
||||
}
|
||||
|
@@ -9,7 +9,30 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestParseRecipe_Golden(t *testing.T) {
|
||||
func TestParse_DetectType(t *testing.T) {
|
||||
recipe := testdata.Read(t, "recipe_salad/input.md")
|
||||
article := testdata.Read(t, "article_simple/input.md")
|
||||
|
||||
recipeType, err := parser.DetectType(string(recipe))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to detect recipeType: %v", err)
|
||||
}
|
||||
|
||||
articleType, err := parser.DetectType(string(article))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to detect articleType: %v", err)
|
||||
}
|
||||
|
||||
if recipeType != "Recipe" {
|
||||
t.Errorf("recipeType did not match expected type 'Recipe' -> %s", recipeType)
|
||||
}
|
||||
|
||||
if articleType != "Article" {
|
||||
t.Errorf("articleType did not match expected type 'Article' -> %s", articleType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_RecipeSalad(t *testing.T) {
|
||||
inputContent := testdata.Read(t, "recipe_salad/input.md")
|
||||
output := testdata.Read(t, "recipe_salad/output.json")
|
||||
|
||||
@@ -28,7 +51,7 @@ func TestParseRecipe_Golden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRecipe_NoDescription(t *testing.T) {
|
||||
func TestParse_RecipeNoDescription(t *testing.T) {
|
||||
inputContent := testdata.Read(t, "recipe_no_description/input.md")
|
||||
|
||||
got, err := parser.ParseFile(string(inputContent))
|
||||
@@ -47,7 +70,7 @@ func TestParseRecipe_NoDescription(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRecipe_Baguette(t *testing.T) {
|
||||
func TestParse_Baguette(t *testing.T) {
|
||||
inputContent := testdata.Read(t, "baguette/input.md")
|
||||
|
||||
got, err := parser.ParseFile(string(inputContent))
|
||||
@@ -66,7 +89,7 @@ func TestParseRecipe_Baguette(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArticle_Simple(t *testing.T) {
|
||||
func TestParse_Article(t *testing.T) {
|
||||
inputContent := testdata.Read(t, "article_simple/input.md")
|
||||
|
||||
got, err := parser.ParseFile(string(inputContent))
|
||||
|
@@ -17,6 +17,7 @@
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.9",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
@@ -44,9 +45,13 @@
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.3.4",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.3",
|
||||
"@fsegurai/codemirror-theme-github-light": "^6.2.2",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-theme-github": "^1.1.0",
|
||||
"lucide-svelte": "^0.544.0",
|
||||
"svelte-codemirror-editor": "^2.0.0"
|
||||
}
|
||||
|
66
playground/pnpm-lock.yaml
generated
66
playground/pnpm-lock.yaml
generated
@@ -17,15 +17,27 @@ importers:
|
||||
'@codemirror/lang-markdown':
|
||||
specifier: ^6.3.4
|
||||
version: 6.3.4
|
||||
'@codemirror/lang-yaml':
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
'@codemirror/language':
|
||||
specifier: ^6.11.3
|
||||
version: 6.11.3
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.2
|
||||
version: 6.5.2
|
||||
'@codemirror/view':
|
||||
specifier: ^6.38.3
|
||||
version: 6.38.3
|
||||
'@fsegurai/codemirror-theme-github-light':
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.3)(@lezer/highlight@1.2.1)
|
||||
codemirror:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
codemirror-theme-github:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
lucide-svelte:
|
||||
specifier: ^0.544.0
|
||||
version: 0.544.0(svelte@5.39.6)
|
||||
@@ -42,6 +54,9 @@ importers:
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^6.0.0
|
||||
version: 6.1.0(@sveltejs/kit@2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))
|
||||
'@sveltejs/adapter-static':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(@sveltejs/kit@2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.22.0
|
||||
version: 2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1))
|
||||
@@ -117,6 +132,9 @@ packages:
|
||||
'@codemirror/lang-markdown@6.3.4':
|
||||
resolution: {integrity: sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==}
|
||||
|
||||
'@codemirror/lang-yaml@6.1.2':
|
||||
resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==}
|
||||
|
||||
'@codemirror/language@6.11.3':
|
||||
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
|
||||
|
||||
@@ -339,6 +357,14 @@ packages:
|
||||
resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@fsegurai/codemirror-theme-github-light@6.2.2':
|
||||
resolution: {integrity: sha512-YQr5MbhMlhRlAQcSCSbet4NDDkMvd5sbUyk9JmM0vfZhQbatvw4c56gNG/54JKGM0kWY5zRWzgLtFuz6D7yEsw==}
|
||||
peerDependencies:
|
||||
'@codemirror/language': ^6.0.0
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
'@lezer/highlight': ^1.0.0
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -399,6 +425,9 @@ packages:
|
||||
'@lezer/markdown@1.4.3':
|
||||
resolution: {integrity: sha512-kfw+2uMrQ/wy/+ONfrH83OkdFNM0ye5Xq96cLlaCy7h5UT9FO54DU4oRoIc0CSBh5NWmWuiIJA7NGLMJbQ+Oxg==}
|
||||
|
||||
'@lezer/yaml@1.0.3':
|
||||
resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
@@ -540,6 +569,11 @@ packages:
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.0.0
|
||||
|
||||
'@sveltejs/adapter-static@3.0.9':
|
||||
resolution: {integrity: sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw==}
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.0.0
|
||||
|
||||
'@sveltejs/kit@2.43.4':
|
||||
resolution: {integrity: sha512-GfvOq3A/qMRhj2L9eKjxaI8FLqZDh5SY74YzhRKT//u2AvQw96ksEfjuHviC4jg9U08mBVB0Y47EwEJHO4BB4Q==}
|
||||
engines: {node: '>=18.13'}
|
||||
@@ -790,6 +824,9 @@ packages:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
codemirror-theme-github@1.1.0:
|
||||
resolution: {integrity: sha512-05qv2eBNdrLvkXx6gdW4IFnRQN0NbUOew8/x4/B62I5UfnIDaPUnoGzqQUluwsBhWTMM7EpFSXSPNCqvcwB89g==}
|
||||
|
||||
codemirror@6.0.2:
|
||||
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
|
||||
|
||||
@@ -1613,6 +1650,16 @@ snapshots:
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/markdown': 1.4.3
|
||||
|
||||
'@codemirror/lang-yaml@6.1.2':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.18.7
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
'@lezer/yaml': 1.0.3
|
||||
|
||||
'@codemirror/language@6.11.3':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
@@ -1777,6 +1824,13 @@ snapshots:
|
||||
'@eslint/core': 0.15.2
|
||||
levn: 0.4.1
|
||||
|
||||
'@fsegurai/codemirror-theme-github-light@6.2.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.3)(@lezer/highlight@1.2.1)':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
@@ -1850,6 +1904,12 @@ snapshots:
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
|
||||
'@lezer/yaml@1.0.3':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@@ -1942,6 +2002,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1))
|
||||
|
||||
'@sveltejs/adapter-static@3.0.9(@sveltejs/kit@2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1))
|
||||
|
||||
'@sveltejs/kit@2.43.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.6)(vite@7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1))':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
@@ -2209,6 +2273,8 @@ snapshots:
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
codemirror-theme-github@1.1.0: {}
|
||||
|
||||
codemirror@6.0.2:
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.18.7
|
||||
|
@@ -1 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
|
||||
.cm-foldGutter + .cm-foldGutter {
|
||||
display: none !important;
|
||||
}
|
||||
|
17
playground/src/app.d.ts
vendored
17
playground/src/app.d.ts
vendored
@@ -8,6 +8,23 @@ declare global {
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
class Go {
|
||||
new(): {
|
||||
run: (inst: WebAssembly.Instance) => Promise<void>;
|
||||
importObject: WebAssembly.Imports;
|
||||
};
|
||||
}
|
||||
|
||||
const marka: {
|
||||
matchBlocks(s: string, t: string): string;
|
||||
detectType(markdown: string): string;
|
||||
parseFile(input: string): string;
|
||||
parseFileWithTemplate(markdown: string, template: string): string;
|
||||
listTemplates(): string;
|
||||
getTemplate(name: string): string;
|
||||
compileTemplate(source: string): string;
|
||||
};
|
||||
}
|
||||
|
||||
export {};
|
@@ -3,7 +3,6 @@
|
||||
<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">
|
||||
|
@@ -1 +1,3 @@
|
||||
<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>
|
||||
<svg width="202" height="202" viewBox="0 0 202 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M198 101C198 47.4284 154.572 4 101 4C63.6769 4 31.2771 25.0794 15.0566 55.9824C31.5802 36.8304 58.3585 25 87.5 25C107.135 25 125.868 30.5962 141.081 40.3643C163.17 54.5475 177.915 77.5995 176.999 105.066C176.961 122.231 171.229 138.064 161.595 150.771L159.861 153.057L133.999 112.606L100.778 164.568L65.0068 108.618L42.0176 144.873C57.3037 166.504 84.4388 181 114.5 181C149.395 181 180.331 159.267 193.398 130.6C196.385 121.268 198 111.323 198 101ZM29.0127 106.222C29.2632 118.862 33.0866 130.771 39.5967 141.223L64.9932 101.171L100.778 157.143L134 105.182L160.106 146.016C168.233 134.357 173 120.186 173 104.895H173.002C173.825 79.7876 160.762 58.4662 140.614 44.8486L101 108.688L61.3604 44.7793C41.8587 57.6651 29 79.7788 29 104.895L29.0127 106.222ZM202 101C202 156.781 156.781 202 101 202V198C134.1 198 163.327 181.42 180.831 156.113C164.258 173.559 140.379 185 114.5 185C82.6725 185 53.8784 169.41 37.9658 146.05C30.0365 134.409 25.3017 120.829 25.0137 106.303L25 104.895C25 77.6227 39.3659 53.7077 60.9336 40.3018L62.6338 39.2441L101 101.101L137.249 42.6855C123.002 33.9863 105.671 29 87.5 29C51.3264 29 19.6837 47.7188 7.41992 75.377C5.19019 83.5392 4 92.1306 4 101C4 154.572 47.4284 198 101 198V202C45.2192 202 0 156.781 0 101C0 45.2192 45.2192 0 101 0C156.781 0 202 45.2192 202 101Z" fill="#555"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -3,6 +3,7 @@
|
||||
import MinusCircleIcon from '$lib/icons/MinusCircleIcon.svelte';
|
||||
import XCircleIcon from '$lib/icons/XCircleIcon.svelte';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { githubLight } from '@fsegurai/codemirror-theme-github-light';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import type { Snippet } from 'svelte';
|
||||
import CodeMirror from 'svelte-codemirror-editor';
|
||||
@@ -15,6 +16,7 @@
|
||||
children?: Snippet;
|
||||
headerActions?: Snippet;
|
||||
subtitle?: string;
|
||||
error?: string;
|
||||
status?: 'success' | 'error' | 'indeterminate';
|
||||
timing?: number;
|
||||
pillText?: string;
|
||||
@@ -23,6 +25,7 @@
|
||||
|
||||
let {
|
||||
title,
|
||||
error,
|
||||
value = $bindable(),
|
||||
placeholder = '',
|
||||
readonly = false,
|
||||
@@ -36,7 +39,7 @@
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-1 flex-col overflow-hidden border-r border-gray-200 last:border-r-0">
|
||||
<div class="relative flex flex-1 flex-col overflow-hidden border-r border-gray-200 last:border-r-0">
|
||||
<div class="flex items-center border-b border-gray-200 bg-gray-50/50 px-4 py-3">
|
||||
{#if status === 'success'}
|
||||
<CheckCircleIcon class="mr-2 h-5 w-5 text-green-500" />
|
||||
@@ -46,7 +49,7 @@
|
||||
<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">
|
||||
<h2 class="flex items-center text-sm font-semibold tracking-wide text-gray-900 uppercase">
|
||||
{title}
|
||||
{#if pillText}
|
||||
<span
|
||||
@@ -69,21 +72,21 @@
|
||||
<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">
|
||||
|
||||
{#if error}
|
||||
<div class="border-b border-gray-200 bg-red-300 p-4 text-xs">
|
||||
<pre>{error}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CodeMirror
|
||||
bind:value
|
||||
extensions={[basicSetup, langExtension].filter(Boolean) as Extension[]}
|
||||
theme={githubLight}
|
||||
{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()}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
</script>
|
||||
|
||||
<header class="sticky top-0 z-10 border-b border-gray-200 bg-white/80 backdrop-blur-sm">
|
||||
<div class="container px-6 py-4">
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Logo />
|
||||
|
@@ -2,9 +2,9 @@
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import {
|
||||
compileTemplate,
|
||||
getTemplate,
|
||||
listTemplates,
|
||||
matchBlocks,
|
||||
parseMarkdown,
|
||||
parseMarkdownWithTemplate,
|
||||
wasmReady,
|
||||
@@ -57,6 +57,7 @@ My favourite baguette recipe
|
||||
let timings = $state<ParseResultSuccess['timings'] | null>(null);
|
||||
let templateStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
|
||||
let dataStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
|
||||
let templateError = $state<string | undefined>();
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -86,26 +87,32 @@ My favourite baguette recipe
|
||||
return;
|
||||
}
|
||||
try {
|
||||
compileTemplate(templateValue);
|
||||
|
||||
const result = templateValue
|
||||
? parseMarkdownWithTemplate(markdownValue, templateValue)
|
||||
: parseMarkdown(markdownValue);
|
||||
|
||||
if ('error' in result) {
|
||||
jsonOutput = result.error;
|
||||
jsonOutput = '';
|
||||
if (result.error.startsWith('failed to compile template')) {
|
||||
templateError = result.error.replaceAll(' -> ', '\n ⟶ ');
|
||||
templateStatus = 'error';
|
||||
dataStatus = 'indeterminate';
|
||||
} else {
|
||||
templateError = undefined;
|
||||
templateStatus = undefined;
|
||||
dataStatus = 'error';
|
||||
}
|
||||
} else {
|
||||
jsonOutput = JSON.stringify(result.data, null, 2);
|
||||
templateError = undefined;
|
||||
jsonOutput = JSON.stringify(result, null, 2);
|
||||
timings = result.timings;
|
||||
templateStatus = 'success';
|
||||
dataStatus = 'success';
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.log({ e });
|
||||
jsonOutput = (e as Error).message;
|
||||
timings = null;
|
||||
if (jsonOutput.startsWith('failed to compile template')) {
|
||||
@@ -138,6 +145,7 @@ My favourite baguette recipe
|
||||
title="Template"
|
||||
bind:value={templateValue}
|
||||
placeholder="Enter your Marka template here..."
|
||||
error={templateError}
|
||||
status={templateStatus}
|
||||
timing={timings?.template_compilation}
|
||||
subtitle="Define your mapping schema"
|
||||
@@ -146,7 +154,7 @@ My favourite baguette recipe
|
||||
{#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"
|
||||
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
|
||||
>
|
||||
<option value="">Load a template</option>
|
||||
{#each templates as template (template)}
|
||||
@@ -167,7 +175,7 @@ My favourite baguette recipe
|
||||
{#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"
|
||||
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
|
@@ -1,4 +1,8 @@
|
||||
<img src="/logo.svg" alt="logo" width="100%" />
|
||||
<script>
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
</script>
|
||||
|
||||
<img src={favicon} alt="logo" width="100%" />
|
||||
|
||||
<style>
|
||||
img {
|
||||
|
@@ -1,31 +1,15 @@
|
||||
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();
|
||||
const go = new globalThis.Go();
|
||||
try {
|
||||
const result = await WebAssembly.instantiateStreaming(
|
||||
fetch("/main.wasm"),
|
||||
fetch("/_playground/main.wasm"),
|
||||
go.importObject,
|
||||
);
|
||||
go.run(result.instance);
|
||||
@@ -38,7 +22,7 @@ export const wasmReady = readable(false, (set) => {
|
||||
if (document.readyState === "complete") {
|
||||
loadWasm();
|
||||
} else {
|
||||
window.addEventListener("load", loadWasm);
|
||||
globalThis.addEventListener("load", loadWasm);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -54,46 +38,74 @@ export type ParseResultError = {
|
||||
export type ParseResult = ParseResultSuccess | ParseResultError;
|
||||
|
||||
export function parseMarkdown(markdown: string): ParseResult {
|
||||
if (typeof window.markaParseFile !== "function") {
|
||||
if (typeof globalThis.marka?.parseFile !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const result = window.markaParseFile(markdown);
|
||||
if (result.error) return result;
|
||||
return JSON.parse(result);
|
||||
const resultString = globalThis.marka.parseFile(markdown);
|
||||
return JSON.parse(resultString);
|
||||
}
|
||||
|
||||
export function matchBlocks(markdown: string): ParseResult {
|
||||
if (typeof window.markaMatchBlocks !== "function") {
|
||||
export function compileTemplate(templateSource: string) {
|
||||
if (typeof globalThis.marka?.compileTemplate !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const result = window.markaMatchBlocks(markdown) as ParseResult;
|
||||
if (result.error) return result;
|
||||
return JSON.parse(result);
|
||||
const resultString = globalThis.marka.compileTemplate(templateSource);
|
||||
const result = JSON.parse(resultString);
|
||||
console.log({ result });
|
||||
return result;
|
||||
}
|
||||
|
||||
export function matchBlocks(markdown: string, template: string): ParseResult {
|
||||
if (typeof globalThis.marka?.matchBlocks !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const resultString = globalThis.marka.matchBlocks(markdown, template);
|
||||
return JSON.parse(resultString);
|
||||
}
|
||||
|
||||
export function parseMarkdownWithTemplate(
|
||||
markdown: string,
|
||||
template: string,
|
||||
): ParseResult {
|
||||
if (typeof window.markaParseFileWithTemplate !== "function") {
|
||||
if (typeof globalThis.marka?.parseFileWithTemplate !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const result = window.markaParseFileWithTemplate(markdown, template);
|
||||
if (result.error) return result;
|
||||
return JSON.parse(result);
|
||||
const resultString = globalThis.marka.parseFileWithTemplate(
|
||||
markdown,
|
||||
template,
|
||||
);
|
||||
return JSON.parse(resultString);
|
||||
}
|
||||
|
||||
export function listTemplates(): string[] {
|
||||
if (typeof window.markaListTemplates !== "function") {
|
||||
if (typeof globalThis.marka?.listTemplates !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const result = window.markaListTemplates();
|
||||
return JSON.parse(result);
|
||||
const resultString = globalThis.marka.listTemplates();
|
||||
return JSON.parse(resultString);
|
||||
}
|
||||
|
||||
export function getTemplate(name: string): string {
|
||||
if (typeof window.markaGetTemplate !== "function") {
|
||||
if (typeof globalThis.marka?.getTemplate !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
return window.markaGetTemplate(name);
|
||||
return globalThis.marka.getTemplate(name);
|
||||
}
|
||||
|
||||
export function detectType(markdown: string): string | ParseResultError {
|
||||
if (typeof globalThis.marka?.detectType !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const result = globalThis.marka.detectType(markdown);
|
||||
try {
|
||||
// If the result is a JSON string with an error, parse and return it
|
||||
const parsed = JSON.parse(result);
|
||||
if (parsed.error) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
// Otherwise, it's a plain string for success
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<title>Marka Playground</title>
|
||||
<script src="/wasm_exec.js"></script>
|
||||
<script src="/_playground/wasm_exec.js"></script>
|
||||
</svelte:head>
|
||||
|
||||
{@render children?.()}
|
||||
|
1
playground/src/routes/+layout.ts
Normal file
1
playground/src/routes/+layout.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
@@ -1,15 +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)"/>
|
||||
<svg width="100" height="100" 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 20.1238 13 43.5 13C53.0351 13 71.5842 16.8812 80 30C80 30 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 16.0505 65.702 20 71.5C27.6856 82.7837 41.5584 90 57 90C80.3762 90 100 71.3259 100 50.0001C100 77.6145 77.6144 100 50 100C22.3856 100 0 77.6145 0 50.0001Z" fill="url(#paint0_radial_13_17)"/>
|
||||
<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_radial_13_17)"/>
|
||||
<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>
|
||||
<radialGradient id="paint0_radial_13_17" cx="0" cy="0" r="1" gradientTransform="matrix(-59 -89 76.5985 -50.3819 82 91)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8E8E8E"/>
|
||||
<stop offset="1" stop-color="#2C2C2C"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_13_17" cx="0" cy="0" r="1" gradientTransform="matrix(58 89.5 -74.0064 52.2522 26 5.5)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8E8E8E"/>
|
||||
<stop offset="1" stop-color="#2C2C2C"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
@@ -11,8 +11,12 @@ const config = {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
adapter: adapter(),
|
||||
|
||||
paths: {
|
||||
base: "/_playground",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@@ -8,7 +8,7 @@ export default defineConfig({
|
||||
exclude: [
|
||||
"svelte-codemirror-editor",
|
||||
"codemirror",
|
||||
"@codemirror/language-javascript", /* ... */
|
||||
"@codemirror/language",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
@@ -7,15 +7,12 @@ OUT_WASM="$OUT_DIR/main.wasm"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
tinygo build -target=wasm -opt=z -no-debug -panic=trap -gc=leaking \
|
||||
tinygo build -target=wasm \
|
||||
-no-debug -panic=print -gc=leaking \
|
||||
-o "$OUT_WASM" "$SCRIPT_DIR"
|
||||
|
||||
# Optional post-process (run only if tools exist)
|
||||
# command -v wasm-opt >/dev/null && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers \
|
||||
# -o "$OUT_WASM.tmp" "$OUT_WASM" && mv "$OUT_WASM.tmp" "$OUT_WASM"
|
||||
command -v wasm-opt >/dev/null && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers \
|
||||
-o "$OUT_WASM.tmp" "$OUT_WASM" && mv "$OUT_WASM.tmp" "$OUT_WASM"
|
||||
# command -v wasm-strip >/dev/null && wasm-strip "$OUT_WASM"
|
||||
# command -v brotli >/dev/null && brotli -f -q 11 "$OUT_WASM" -o "$OUT_WASM.br"
|
||||
# command -v gzip >/dev/null && gzip -c -9 "$OUT_WASM" > "$OUT_WASM.gz"
|
||||
|
||||
# Copy TinyGo runtime
|
||||
cp -f "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" "$OUT_DIR/wasm_exec.js"
|
||||
|
@@ -8,92 +8,108 @@ import (
|
||||
|
||||
p "git.max-richter.dev/max/marka/parser"
|
||||
"git.max-richter.dev/max/marka/registry"
|
||||
"git.max-richter.dev/max/marka/template"
|
||||
)
|
||||
|
||||
func matchBlocks(_ js.Value, args []js.Value) any {
|
||||
if len(args) == 0 {
|
||||
return js.ValueOf(map[string]any{"error": "missing markdown"})
|
||||
func wrapError(err error) string {
|
||||
errMap := map[string]any{"error": err.Error()}
|
||||
errJSON, _ := json.Marshal(errMap)
|
||||
return string(errJSON)
|
||||
}
|
||||
t, err := p.MatchBlocks(args[0].String(), args[1].String())
|
||||
|
||||
func MatchBlocks(this js.Value, args []js.Value) any {
|
||||
s := args[0].String()
|
||||
t := args[1].String()
|
||||
matched, err := p.MatchBlocks(s, t)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
jsonString, _ := json.Marshal(matched)
|
||||
return string(jsonString)
|
||||
}
|
||||
|
||||
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())
|
||||
func DetectType(this js.Value, args []js.Value) any {
|
||||
markdown := args[0].String()
|
||||
t, err := p.DetectType(markdown)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
return js.ValueOf(t) // plain string
|
||||
return t
|
||||
}
|
||||
|
||||
func parseFile(_ js.Value, args []js.Value) any {
|
||||
if len(args) == 0 {
|
||||
return js.ValueOf(map[string]any{"error": "missing markdown"})
|
||||
}
|
||||
res, err := p.ParseFile(args[0].String())
|
||||
func ParseFile(this js.Value, args []js.Value) any {
|
||||
markdown := args[0].String()
|
||||
res, err := p.ParseFile(markdown)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
return js.ValueOf(string(b))
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func parseFileWithTemplate(_ js.Value, args []js.Value) any {
|
||||
if len(args) < 2 {
|
||||
return js.ValueOf(map[string]any{"error": "missing markdown or template"})
|
||||
}
|
||||
res, err := p.ParseFileWithTemplate(args[0].String(), args[1].String())
|
||||
func ParseFileWithTemplate(this js.Value, args []js.Value) any {
|
||||
markdown := args[0].String()
|
||||
template := args[1].String()
|
||||
res, err := p.ParseFileWithTemplate(markdown, template)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
return js.ValueOf(string(b))
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func listTemplates(_ js.Value, args []js.Value) any {
|
||||
func ListTemplates(this js.Value, args []js.Value) any {
|
||||
templates, err := registry.ListTemplates()
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
b, err := json.Marshal(templates)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
return js.ValueOf(string(b))
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func getTemplate(_ js.Value, args []js.Value) any {
|
||||
if len(args) == 0 {
|
||||
return js.ValueOf(map[string]any{"error": "missing template name"})
|
||||
}
|
||||
template, err := registry.GetTemplate(args[0].String())
|
||||
func GetTemplate(this js.Value, args []js.Value) any {
|
||||
name := args[0].String()
|
||||
template, err := registry.GetTemplate(name)
|
||||
if err != nil {
|
||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
||||
return wrapError(err)
|
||||
}
|
||||
return js.ValueOf(template)
|
||||
return template
|
||||
}
|
||||
|
||||
func CompileTemplate(this js.Value, args []js.Value) any {
|
||||
source := args[0].String()
|
||||
template, err := template.CompileTemplate(source)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
b, err := json.Marshal(template)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func main() {
|
||||
js.Global().Set("markaDetectType", js.FuncOf(detectType))
|
||||
js.Global().Set("markaParseFile", js.FuncOf(parseFile))
|
||||
js.Global().Set("markaParseFileWithTemplate", js.FuncOf(parseFileWithTemplate))
|
||||
js.Global().Set("markaMatchBlocks", js.FuncOf(matchBlocks))
|
||||
js.Global().Set("markaListTemplates", js.FuncOf(listTemplates))
|
||||
js.Global().Set("markaGetTemplate", js.FuncOf(getTemplate))
|
||||
marka := js.Global().Get("Object").New()
|
||||
|
||||
marka.Set("matchBlocks", js.FuncOf(MatchBlocks))
|
||||
marka.Set("detectType", js.FuncOf(DetectType))
|
||||
marka.Set("parseFile", js.FuncOf(ParseFile))
|
||||
marka.Set("parseFileWithTemplate", js.FuncOf(ParseFileWithTemplate))
|
||||
marka.Set("listTemplates", js.FuncOf(ListTemplates))
|
||||
marka.Set("getTemplate", js.FuncOf(GetTemplate))
|
||||
marka.Set("compileTemplate", js.FuncOf(CompileTemplate))
|
||||
|
||||
js.Global().Set("marka", marka)
|
||||
|
||||
select {}
|
||||
}
|
@@ -11,6 +11,8 @@
|
||||
codec: const
|
||||
value: Article
|
||||
- path: image
|
||||
- path: about
|
||||
- path: url
|
||||
- path: author.name
|
||||
- path: author._type
|
||||
codec: const
|
||||
@@ -22,16 +24,13 @@
|
||||
pathAlias: rating
|
||||
- path: reviewRating.bestRating
|
||||
codec: const
|
||||
value: 5
|
||||
hidden: true
|
||||
- path: reviewRating.worstRating
|
||||
codec: const
|
||||
value: 1
|
||||
hidden: true
|
||||
}
|
||||
---
|
||||
|
||||
# { headline }
|
||||
{ keywords | hashtags }
|
||||
|
||||
{ articleBody }
|
||||
|
@@ -10,7 +10,7 @@
|
||||
- path: "_type"
|
||||
codec: const
|
||||
value: Recipe
|
||||
hidden: true
|
||||
- path: link
|
||||
- path: image
|
||||
- path: author._type
|
||||
codec: const
|
||||
@@ -30,7 +30,6 @@
|
||||
---
|
||||
|
||||
# { name | text }
|
||||
{ keywords | hashtags,optional }
|
||||
|
||||
{ description | text }
|
||||
|
||||
|
17
server/Dockerfile
Normal file
17
server/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM golang:1.24.7 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -trimpath -ldflags="-s -w" \
|
||||
-o /out/server ./server/cmd/marka-server
|
||||
|
||||
FROM scratch
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/server /app/server
|
||||
COPY --from=build /src/server/playground /app/playground
|
||||
USER 65532:65532
|
||||
EXPOSE 8080
|
||||
CMD ["/app/server","-root=/app/data","-addr=:8080","-playground-root=/app/playground"]
|
||||
|
15
server/README.md
Normal file
15
server/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## Marka Server
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Recipe",
|
||||
"modTime": 123123123123,
|
||||
"content": [
|
||||
{
|
||||
"name": "Baguette",
|
||||
"modTime": 123123123123,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
@@ -6,31 +6,61 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.max-richter.dev/max/marka/server-new/internal/adapters"
|
||||
"git.max-richter.dev/max/marka/server-new/internal/handler"
|
||||
"git.max-richter.dev/max/marka/server/internal/adapters"
|
||||
"git.max-richter.dev/max/marka/server/internal/handler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
root := flag.String("root", ".", "filesystem root to serve")
|
||||
addr := flag.String("addr", ":8080", "listen address")
|
||||
flag.Parse()
|
||||
type multi []string
|
||||
|
||||
absRoot, err := filepath.Abs(*root)
|
||||
must(err)
|
||||
|
||||
info, err := os.Stat(absRoot)
|
||||
must(err)
|
||||
if !info.IsDir() {
|
||||
log.Fatal("root is not a directory")
|
||||
func (m *multi) String() string { return strings.Join(*m, ",") }
|
||||
func (m *multi) Set(v string) error {
|
||||
*m = append(*m, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
fsAdapter, err := adapters.NewLocalFsAdapter(absRoot)
|
||||
func main() {
|
||||
var roots multi
|
||||
flag.Var(&roots, "root", "repeatable; specify multiple -root flags")
|
||||
addr := flag.String("addr", ":8080", "listen address")
|
||||
playgroundRoot := flag.String("playground-root", "/app/playground", "path to playground build directory")
|
||||
flag.Parse()
|
||||
|
||||
if *playgroundRoot != "" {
|
||||
absPlaygroundRoot, err := filepath.Abs(*playgroundRoot)
|
||||
must(err)
|
||||
info, err := os.Stat(absPlaygroundRoot)
|
||||
must(err)
|
||||
if !info.IsDir() {
|
||||
log.Fatalf("playground-root %s is not a directory", *playgroundRoot)
|
||||
}
|
||||
log.Printf("serving playground from %s", absPlaygroundRoot)
|
||||
http.Handle("/_playground/", http.StripPrefix("/_playground/", http.FileServer(http.Dir(absPlaygroundRoot))))
|
||||
}
|
||||
|
||||
if len(roots) == 0 {
|
||||
log.Fatal("at least one -root flag must be specified")
|
||||
}
|
||||
|
||||
absRoots := make([]string, len(roots))
|
||||
for i, r := range roots {
|
||||
abs, err := filepath.Abs(r)
|
||||
must(err)
|
||||
info, err := os.Stat(abs)
|
||||
must(err)
|
||||
if !info.IsDir() {
|
||||
log.Fatalf("root %s is not a directory", r)
|
||||
}
|
||||
absRoots[i] = abs
|
||||
}
|
||||
|
||||
fsAdapter, err := adapters.NewLocalFsAdapter(absRoots)
|
||||
must(err)
|
||||
|
||||
http.Handle("/", handler.NewHandler(fsAdapter))
|
||||
|
||||
log.Printf("listening on %s, root=%s", *addr, absRoot)
|
||||
log.Printf("listening on %s, roots=%s", *addr, strings.Join(absRoots, ", "))
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,15 @@
|
||||
module git.max-richter.dev/max/marka/server-new
|
||||
module git.max-richter.dev/max/marka/server
|
||||
|
||||
go 1.24.7
|
||||
|
||||
require git.max-richter.dev/max/marka/parser v0.0.0-20251003194139-ab81c980b590
|
||||
|
||||
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
|
||||
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
|
||||
)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e h1:enZufetD3UoIVTnTNTQSFlr1Ir0jG7wObUAxb6+xwWg=
|
||||
git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e/go.mod h1:xQK6tsgr9BOoeFw8JxjBwDkVENlOqapmcRkYyf/L+SQ=
|
||||
git.max-richter.dev/max/marka/parser v0.0.0-20251003194139-ab81c980b590 h1:4C3b/KHD7+ru7nunpYIY6snVw+RI1QeVk9XcbzmdVY0=
|
||||
git.max-richter.dev/max/marka/parser v0.0.0-20251003194139-ab81c980b590/go.mod h1:ykBvG4AW+fNFila0ZO2TlBqsCACYKb0AJaHsAopyRVI=
|
||||
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 h1:oe7Xb8dE43S8mRla5hfEqagMnvhvEVHsvRlzl2v540w=
|
||||
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567/go.mod h1:qGWl42P8mgEktfor/IjQp0aS9SqmpeIlhSuVTlUOXLQ=
|
||||
git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e h1:9Eg81l8YMTXWZC3xlZ5L/NJRuK26bksrVtEHyCTV4sM=
|
||||
|
@@ -1,97 +1,266 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.max-richter.dev/max/marka/parser"
|
||||
)
|
||||
|
||||
type LocalFsAdapter struct {
|
||||
root string
|
||||
roots []string
|
||||
cache map[string]*Entry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (l LocalFsAdapter) readDir(path string) (FsResponse, error) {
|
||||
dirInfo, _ := os.Stat(path)
|
||||
func (l *LocalFsAdapter) readDir(path string, root string) (*Entry, error) {
|
||||
dirInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]FsDirEntry, 0, len(entries))
|
||||
out := make([]*Entry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
info, _ := e.Info()
|
||||
|
||||
entryType := "dir"
|
||||
if !e.IsDir() {
|
||||
entryType = contentTypeFor(e.Name())
|
||||
entryType = "file"
|
||||
}
|
||||
|
||||
out = append(out, FsDirEntry{
|
||||
var content any
|
||||
var mime string
|
||||
var size int64
|
||||
if !e.IsDir() {
|
||||
mime = contentTypeFor(e.Name())
|
||||
if info != nil {
|
||||
size = info.Size()
|
||||
}
|
||||
if mime == "application/markdown" {
|
||||
entryPath := filepath.Join(path, e.Name())
|
||||
fileContent, err := os.ReadFile(entryPath)
|
||||
if err == nil {
|
||||
parsedContent, err := parser.ParseFile(string(fileContent))
|
||||
if err == nil {
|
||||
content = parsedContent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
childPath, err := filepath.Rel(root, filepath.Join(path, e.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responseChildPath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), childPath))
|
||||
|
||||
out = append(out, &Entry{
|
||||
Name: e.Name(),
|
||||
Path: responseChildPath,
|
||||
Type: entryType,
|
||||
ModTime: info.ModTime(),
|
||||
MIME: mime,
|
||||
Size: size,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if relPath == "." {
|
||||
relPath = ""
|
||||
}
|
||||
|
||||
responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath))
|
||||
|
||||
return &Entry{
|
||||
Type: "dir",
|
||||
Name: filepath.Base(responsePath),
|
||||
Path: responsePath,
|
||||
ModTime: dirInfo.ModTime(),
|
||||
Content: out,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LocalFsAdapter) readFile(path string, root string) (*Entry, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responsePath := "/" + filepath.ToSlash(filepath.Join(filepath.Base(root), relPath))
|
||||
mime := contentTypeFor(path)
|
||||
var content any
|
||||
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mime == "application/markdown" {
|
||||
parsedContent, err := parser.ParseFile(string(fileContent))
|
||||
if err == nil {
|
||||
content = parsedContent
|
||||
} else {
|
||||
// Fallback to raw content on parsing error
|
||||
content = fileContent
|
||||
}
|
||||
} else {
|
||||
content = fileContent
|
||||
}
|
||||
|
||||
return &Entry{
|
||||
Type: "file",
|
||||
Name: fi.Name(),
|
||||
Path: responsePath,
|
||||
ModTime: fi.ModTime(),
|
||||
MIME: mime,
|
||||
Size: fi.Size(),
|
||||
Content: content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LocalFsAdapter) Read(path string) (*Entry, error) {
|
||||
if path == "/" {
|
||||
var latestModTime time.Time
|
||||
for _, r := range l.roots {
|
||||
info, err := os.Stat(r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().After(latestModTime) {
|
||||
latestModTime = info.ModTime()
|
||||
}
|
||||
}
|
||||
|
||||
l.mu.RLock()
|
||||
cached, found := l.cache[path]
|
||||
l.mu.RUnlock()
|
||||
|
||||
if found && !latestModTime.After(cached.ModTime) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
entries := make([]*Entry, 0, len(l.roots))
|
||||
for _, r := range l.roots {
|
||||
info, err := os.Stat(r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
name := filepath.Base(r)
|
||||
entries = append(entries, &Entry{
|
||||
Name: name,
|
||||
Path: "/" + name,
|
||||
Type: "dir",
|
||||
ModTime: info.ModTime(),
|
||||
})
|
||||
}
|
||||
|
||||
return FsResponse{
|
||||
Dir: &FsDir{
|
||||
Files: out,
|
||||
Name: ResponsePath(l.root, path),
|
||||
ModTime: dirInfo.ModTime(),
|
||||
},
|
||||
}, nil
|
||||
entry := &Entry{
|
||||
Type: "dir",
|
||||
Name: "/",
|
||||
Path: "/",
|
||||
ModTime: latestModTime,
|
||||
Content: entries,
|
||||
}
|
||||
|
||||
func (l LocalFsAdapter) readFile(path string) (FsResponse, error) {
|
||||
fi, err := os.Stat(path)
|
||||
l.mu.Lock()
|
||||
l.cache[path] = entry
|
||||
l.mu.Unlock()
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
pathParts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
if len(pathParts) == 0 || pathParts[0] == "" {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
rootIdentifier := pathParts[0]
|
||||
var targetRoot string
|
||||
for _, r := range l.roots {
|
||||
if filepath.Base(r) == rootIdentifier {
|
||||
targetRoot = r
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetRoot == "" {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
subPath := filepath.Join(pathParts[1:]...)
|
||||
target := filepath.Join(targetRoot, subPath)
|
||||
|
||||
absTarget, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
absRoot, err := filepath.Abs(targetRoot)
|
||||
if err != nil {
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return FsResponse{
|
||||
File: &FsFile{
|
||||
Name: ResponsePath(l.root, path),
|
||||
Type: contentTypeFor(path),
|
||||
ModTime: fi.ModTime(),
|
||||
Content: data,
|
||||
},
|
||||
}, nil
|
||||
if !strings.HasPrefix(absTarget, absRoot) {
|
||||
return nil, errors.New("path escapes root")
|
||||
}
|
||||
|
||||
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 nil, ErrNotFound
|
||||
}
|
||||
|
||||
return FsResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.mu.RLock()
|
||||
cached, found := l.cache[path]
|
||||
l.mu.RUnlock()
|
||||
|
||||
if found && !fi.ModTime().After(cached.ModTime) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
var newEntry *Entry
|
||||
if fi.IsDir() {
|
||||
return l.readDir(target)
|
||||
newEntry, err = l.readDir(target, targetRoot)
|
||||
} else {
|
||||
newEntry, err = l.readFile(target, targetRoot)
|
||||
}
|
||||
|
||||
return l.readFile(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (LocalFsAdapter) Write(path string, content []byte) error {
|
||||
l.mu.Lock()
|
||||
l.cache[path] = newEntry
|
||||
l.mu.Unlock()
|
||||
|
||||
return newEntry, nil
|
||||
}
|
||||
|
||||
func (l *LocalFsAdapter) Write(path string, content []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLocalFsAdapter(root string) (FileAdapter, error) {
|
||||
return LocalFsAdapter{
|
||||
root: root,
|
||||
func NewLocalFsAdapter(roots []string) (FileAdapter, error) {
|
||||
return &LocalFsAdapter{
|
||||
roots: roots,
|
||||
cache: make(map[string]*Entry),
|
||||
}, nil
|
||||
}
|
||||
|
@@ -3,10 +3,8 @@ package adapters
|
||||
import (
|
||||
"errors"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SafeRel(root, requested string) (string, error) {
|
||||
@@ -37,20 +35,6 @@ func ResponsePath(root, full string) string {
|
||||
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,
|
||||
|
@@ -3,32 +3,26 @@ package adapters
|
||||
|
||||
import "time"
|
||||
|
||||
// Entry represents a file or directory in the filesystem.
|
||||
type Entry struct {
|
||||
Type string `json:"type"` // "file" | "dir"
|
||||
Name string `json:"name"` // base name
|
||||
Path string `json:"path"` // full path or virtual path
|
||||
ModTime time.Time `json:"modTime"` // last modified
|
||||
|
||||
// File-only (optional)
|
||||
MIME string `json:"mime,omitempty"` // e.g. "text/markdown"
|
||||
Size int64 `json:"size,omitempty"` // bytes
|
||||
|
||||
// Content:
|
||||
// - markdown file: any (parsed AST / arbitrary JSON)
|
||||
// - directory: []*Entry (children)
|
||||
// - other files: omitted
|
||||
Content any `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// FileAdapter is the interface for accessing the file system.
|
||||
type FileAdapter interface {
|
||||
Read(path string) (FsResponse, error)
|
||||
Read(path string) (*Entry, 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"`
|
||||
}
|
||||
|
@@ -4,76 +4,44 @@ package handler
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.max-richter.dev/max/marka/parser"
|
||||
"git.max-richter.dev/max/marka/server-new/internal/adapters"
|
||||
"git.max-richter.dev/max/marka/server/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)
|
||||
entry, err := h.adapter.Read(target)
|
||||
if err != nil {
|
||||
writeError(w, 500, err)
|
||||
if errors.Is(err, adapters.ErrNotFound) {
|
||||
writeError(w, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, 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)
|
||||
if entry.Type == "file" {
|
||||
// For non-markdown files, content is []byte, write it directly
|
||||
if contentBytes, ok := entry.Content.([]byte); ok {
|
||||
w.Header().Set("Content-Type", entry.MIME)
|
||||
w.Write(contentBytes)
|
||||
return
|
||||
}
|
||||
// For markdown files, content is parsed, return the whole Entry as JSON
|
||||
writeJSON(w, http.StatusOK, entry)
|
||||
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)
|
||||
// For directories, return the whole Entry as JSON
|
||||
if entry.Type == "dir" {
|
||||
writeJSON(w, http.StatusOK, entry)
|
||||
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
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, errors.New("unknown entry type"))
|
||||
}
|
||||
|
||||
func (h *Handler) post(w http.ResponseWriter, target string) {
|
||||
|
1
server/playground/_app/env.js
Normal file
1
server/playground/_app/env.js
Normal file
@@ -0,0 +1 @@
|
||||
export const env={}
|
1
server/playground/_app/immutable/assets/0.z6k2NkFs.css
Normal file
1
server/playground/_app/immutable/assets/0.z6k2NkFs.css
Normal file
File diff suppressed because one or more lines are too long
1
server/playground/_app/immutable/assets/2.DQ5mJGGf.css
Normal file
1
server/playground/_app/immutable/assets/2.DQ5mJGGf.css
Normal file
@@ -0,0 +1 @@
|
||||
img.svelte-6uiykz{height:64px;width:64px;aspect-ratio:1;filter:drop-shadow(0px 0px 8px #0002)}.codemirror-wrapper.svelte-3fyul5 .cm-focused{outline:none}.scm-waiting.svelte-3fyul5{position:relative}.scm-waiting__loading.svelte-3fyul5{position:absolute;inset:0;background-color:#ffffff80}.scm-loading.svelte-3fyul5{display:flex;align-items:center;justify-content:center}.scm-loading__spinner.svelte-3fyul5{width:1rem;height:1rem;border-radius:100%;border:solid 2px #000;border-top-color:transparent;margin-right:.75rem;animation:svelte-3fyul5-spin 1s linear infinite}.scm-loading__text.svelte-3fyul5{font-family:sans-serif}.scm-pre.svelte-3fyul5{font-size:.85rem;font-family:monospace;tab-size:2;-moz-tab-size:2;resize:none;pointer-events:none;-webkit-user-select:none;user-select:none;overflow:auto}@keyframes svelte-3fyul5-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:#fff}*{box-sizing:border-box}textarea:focus{outline:none}pre{margin:0;font-family:SF Mono,Monaco,Cascadia Code,Roboto Mono,Consolas,Courier New,monospace}
|
2
server/playground/_app/immutable/chunks/B1lAeocp.js
Normal file
2
server/playground/_app/immutable/chunks/B1lAeocp.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import{af as X,g as $,Z as j,Q as Z,b as y,ag as I,R as U,O as p,A as g,ah as ee,Y as R,z as te,B as se,ai as q,H as ne,J as d,aj as C,N as D,a6 as L,ak as E,al as P,am as ie,an as S,c as f,ao as ae,ap as N,F as x,n as re,D as oe,aq as z,ar as le,E as ue,as as fe,at as ce,au as V,av as he,aw as k,ax as _e,ay as de,az as O,G as w,aA as pe,aB as ge,aC as be,aD as ve,aE as B,aF as ye,aG as T,aH as me,I as Ee,aI as we,p as Te,aJ as Re,aK as Ae,aL as Se,q as xe,m as A,aM as Ce,aN as m,a as De,aO as Ne,aP as ke,aQ as W,a8 as Oe,aR as Fe,aS as qe,aT as Me,aU as Ye,aV as He,aW as Ie,aX as Le,aY as Pe}from"./bc36GTfJ.js";function ze(t){let e=0,s=j(0),n;return()=>{X()&&($(s),Z(()=>(e===0&&(n=y(()=>t(()=>I(s)))),e+=1,()=>{U(()=>{e-=1,e===0&&(n?.(),n=void 0,I(s))})})))}}var Be=ue|fe|ce;function $e(t,e,s){new je(t,e,s)}class je{parent;#n=!1;#t;#p=g?p:null;#i;#f;#a;#s=null;#e=null;#r=null;#o=null;#c=0;#l=0;#h=!1;#u=null;#b=()=>{this.#u&&ee(this.#u,this.#c)};#v=ze(()=>(this.#u=j(this.#c),()=>{this.#u=null}));constructor(e,s,n){this.#t=e,this.#i=s,this.#f=n,this.parent=R.b,this.#n=!!this.#i.pending,this.#a=te(()=>{if(R.b=this,g){const i=this.#p;se(),i.nodeType===q&&i.data===ne?this.#m():this.#y()}else{try{this.#s=d(()=>n(this.#t))}catch(i){this.error(i)}this.#l>0?this.#d():this.#n=!1}},Be),g&&(this.#t=p)}#y(){try{this.#s=d(()=>this.#f(this.#t))}catch(e){this.error(e)}this.#n=!1}#m(){const e=this.#i.pending;e&&(this.#e=d(()=>e(this.#t)),C.enqueue(()=>{this.#s=this.#_(()=>(C.ensure(),d(()=>this.#f(this.#t)))),this.#l>0?this.#d():(D(this.#e,()=>{this.#e=null}),this.#n=!1)}))}is_pending(){return this.#n||!!this.parent&&this.parent.is_pending()}has_pending_snippet(){return!!this.#i.pending}#_(e){var s=R,n=S,i=f;L(this.#a),E(this.#a),P(this.#a.ctx);try{return e()}catch(a){return ie(a),null}finally{L(s),E(n),P(i)}}#d(){const e=this.#i.pending;this.#s!==null&&(this.#o=document.createDocumentFragment(),Ue(this.#s,this.#o)),this.#e===null&&(this.#e=d(()=>e(this.#t)))}#g(e){if(!this.has_pending_snippet()){this.parent&&this.parent.#g(e);return}this.#l+=e,this.#l===0&&(this.#n=!1,this.#e&&D(this.#e,()=>{this.#e=null}),this.#o&&(this.#t.before(this.#o),this.#o=null))}update_pending_count(e){this.#g(e),this.#c+=e,ae.add(this.#b)}get_effect_pending(){return this.#v(),$(this.#u)}error(e){var s=this.#i.onerror;let n=this.#i.failed;if(this.#h||!s&&!n)throw e;this.#s&&(N(this.#s),this.#s=null),this.#e&&(N(this.#e),this.#e=null),this.#r&&(N(this.#r),this.#r=null),g&&(x(this.#p),re(),x(oe()));var i=!1,a=!1;const o=()=>{if(i){he();return}i=!0,a&&le(),C.ensure(),this.#c=0,this.#r!==null&&D(this.#r,()=>{this.#r=null}),this.#n=this.has_pending_snippet(),this.#s=this.#_(()=>(this.#h=!1,d(()=>this.#f(this.#t)))),this.#l>0?this.#d():this.#n=!1};var r=S;try{E(null),a=!0,s?.(e,o),a=!1}catch(h){z(h,this.#a&&this.#a.parent)}finally{E(r)}n&&U(()=>{this.#r=this.#_(()=>{this.#h=!0;try{return d(()=>{n(this.#t,()=>e,()=>o)})}catch(h){return z(h,this.#a.parent),null}finally{this.#h=!1}})})}}function Ue(t,e){for(var s=t.nodes_start,n=t.nodes_end;s!==null;){var i=s===n?null:V(s);e.append(s),s=i}}function nt(t,e){var s=e==null?"":typeof e=="object"?e+"":e;s!==(t.__t??=t.nodeValue)&&(t.__t=s,t.nodeValue=s+"")}function G(t,e){return J(t,e)}function Ve(t,e){k(),e.intro=e.intro??!1;const s=e.target,n=g,i=p;try{for(var a=_e(s);a&&(a.nodeType!==q||a.data!==de);)a=V(a);if(!a)throw O;w(!0),x(a);const o=J(t,{...e,anchor:a});return w(!1),o}catch(o){if(o instanceof Error&&o.message.split(`
|
||||
`).some(r=>r.startsWith("https://svelte.dev/e/")))throw o;return o!==O&&console.warn("Failed to hydrate: ",o),e.recover===!1&&pe(),k(),ge(s),w(!1),G(t,e)}finally{w(n),x(i),we()}}const b=new Map;function J(t,{target:e,anchor:s,props:n={},events:i,context:a,intro:o=!0}){k();var r=new Set,h=_=>{for(var c=0;c<_.length;c++){var u=_[c];if(!r.has(u)){r.add(u);var Y=me(u);e.addEventListener(u,T,{passive:Y});var H=b.get(u);H===void 0?(document.addEventListener(u,T,{passive:Y}),b.set(u,1)):b.set(u,H+1)}}};h(be(ve)),B.add(h);var l=void 0,K=ye(()=>{var _=s??e.appendChild(Ee());return $e(_,{pending:()=>{}},c=>{if(a){Te({});var u=f;u.c=a}if(i&&(n.$$events=i),g&&Re(c,null),l=t(c,n)||{},g&&(R.nodes_end=p,p===null||p.nodeType!==q||p.data!==Ae))throw Se(),O;a&&xe()}),()=>{for(var c of r){e.removeEventListener(c,T);var u=b.get(c);--u===0?(document.removeEventListener(c,T),b.delete(c)):b.set(c,u)}B.delete(h),_!==s&&_.parentNode?.removeChild(_)}});return F.set(l,K),l}let F=new WeakMap;function We(t,e){const s=F.get(t);return s?(F.delete(t),s(e)):Promise.resolve()}function Ge(t,e,s){if(t==null)return e(void 0),A;const n=y(()=>t.subscribe(e,s));return n.unsubscribe?()=>n.unsubscribe():n}const v=[];function it(t,e){return{subscribe:Je(t,e).subscribe}}function Je(t,e=A){let s=null;const n=new Set;function i(r){if(Ce(t,r)&&(t=r,s)){const h=!v.length;for(const l of n)l[1](),v.push(l,t);if(h){for(let l=0;l<v.length;l+=2)v[l][0](v[l+1]);v.length=0}}}function a(r){i(r(t))}function o(r,h=A){const l=[r,h];return n.add(l),n.size===1&&(s=e(i,a)||A),r(t),()=>{n.delete(l),n.size===0&&s&&(s(),s=null)}}return{set:i,update:a,subscribe:o}}function at(t){let e;return Ge(t,s=>e=s)(),e}function Qe(){return S===null&&Ne(),(S.ac??=new AbortController).signal}function Q(t){f===null&&m(),Oe&&f.l!==null?M(f).m.push(t):De(()=>{const e=y(t);if(typeof e=="function")return e})}function Ke(t){f===null&&m(),Q(()=>()=>y(t))}function Xe(t,e,{bubbles:s=!1,cancelable:n=!1}={}){return new CustomEvent(t,{detail:e,bubbles:s,cancelable:n})}function Ze(){const t=f;return t===null&&m(),(e,s,n)=>{const i=t.s.$$events?.[e];if(i){const a=ke(i)?i.slice():[i],o=Xe(e,s,n);for(const r of a)r.call(t.x,o);return!o.defaultPrevented}return!0}}function et(t){f===null&&m(),f.l===null&&W(),M(f).b.push(t)}function tt(t){f===null&&m(),f.l===null&&W(),M(f).a.push(t)}function M(t){var e=t.l;return e.u??={a:[],b:[],m:[]}}const rt=Object.freeze(Object.defineProperty({__proto__:null,afterUpdate:tt,beforeUpdate:et,createEventDispatcher:Ze,createRawSnippet:Fe,flushSync:qe,getAbortSignal:Qe,getAllContexts:Me,getContext:Ye,hasContext:He,hydrate:Ve,mount:G,onDestroy:Ke,onMount:Q,setContext:Ie,settled:Le,tick:Pe,unmount:We,untrack:y},Symbol.toStringTag,{value:"Module"}));export{Ge as a,Ke as b,rt as c,at as g,Ve as h,G as m,Q as o,it as r,nt as s,We as u,Je as w};
|
3
server/playground/_app/immutable/chunks/BsPC8ki2.js
Normal file
3
server/playground/_app/immutable/chunks/BsPC8ki2.js
Normal file
File diff suppressed because one or more lines are too long
1
server/playground/_app/immutable/chunks/DsnmJJEf.js
Normal file
1
server/playground/_app/immutable/chunks/DsnmJJEf.js
Normal file
@@ -0,0 +1 @@
|
||||
typeof window<"u"&&((window.__svelte??={}).v??=new Set).add("5");
|
2
server/playground/_app/immutable/chunks/DyOAiIET.js
Normal file
2
server/playground/_app/immutable/chunks/DyOAiIET.js
Normal file
File diff suppressed because one or more lines are too long
1
server/playground/_app/immutable/chunks/DyoUZ9Ht.js
Normal file
1
server/playground/_app/immutable/chunks/DyoUZ9Ht.js
Normal file
@@ -0,0 +1 @@
|
||||
import{z as K,A as R,B as j,E as q,C as Z,H as $,D as z,F as G,G as x,I as H,J as D,K as J,U as Q,L as V,M as W,N as X,O as k,P as ee,Q as re,b as B,R as ne,S as y,m as N,T as se,g as P,V as ie,W as M,X as ae,Y as A,Z as te,_ as m,a0 as ue,a1 as C,a2 as fe,a3 as le,f as ce,a4 as oe,a5 as L,a6 as U,a7 as de,a8 as _e,a9 as pe,aa as ve,ab as S,ac as F,ad as be,ae as he}from"./bc36GTfJ.js";import{a as Se,g as Pe}from"./B1lAeocp.js";function me(e,r,s=!1){R&&j();var n=e,i=null,a=null,l=Q,d=s?q:0,p=!1;const I=(c,u=!0)=>{p=!0,_(u,c)};var f=null;function g(){f!==null&&(f.lastChild.remove(),n.before(f),f=null);var c=l?i:a,u=l?a:i;c&&W(c),u&&X(u,()=>{l?a=null:i=null})}const _=(c,u)=>{if(l===(l=c))return;let E=!1;if(R){const O=Z(n)===$;!!l===O&&(n=z(),G(n),x(!1),E=!0)}var b=V(),o=n;if(b&&(f=document.createDocumentFragment(),f.append(o=H())),l?i??=u&&D(()=>u(o)):a??=u&&D(()=>u(o)),b){var h=J,t=l?i:a,v=l?a:i;t&&h.skipped_effects.delete(t),v&&h.skipped_effects.add(v),h.add_callback(g)}else g();E&&x(!0)};K(()=>{p=!1,r(I),p||_(null,null)},d),R&&(n=k)}function Y(e,r){return e===r||e?.[y]===r}function we(e={},r,s,n){return ee(()=>{var i,a;return re(()=>{i=a,a=[],B(()=>{e!==s(...a)&&(r(e,...a),i&&Y(s(...i),e)&&r(null,...i))})}),()=>{ne(()=>{a&&Y(s(...a),e)&&r(null,...a)})}}),e}let T=!1,w=Symbol();function ye(e,r,s){const n=s[r]??={store:null,source:se(void 0),unsubscribe:N};if(n.store!==e&&!(w in s))if(n.unsubscribe(),n.store=e??null,e==null)n.source.v=void 0,n.unsubscribe=N;else{var i=!0;n.unsubscribe=Se(e,a=>{i?n.source.v=a:M(n.source,a)}),i=!1}return e&&w in s?Pe(e):P(n.source)}function Ae(){const e={};function r(){ie(()=>{for(var s in e)e[s].unsubscribe();ae(e,w,{enumerable:!1,value:!0})})}return[e,r]}function Ie(e){var r=T;try{return T=!1,[e(),T]}finally{T=r}}const ge={get(e,r){if(!e.exclude.includes(r))return P(e.version),r in e.special?e.special[r]():e.props[r]},set(e,r,s){if(!(r in e.special)){var n=A;try{U(e.parent_effect),e.special[r]=Oe({get[r](){return e.props[r]}},r,C)}finally{U(n)}}return e.special[r](s),L(e.version),!0},getOwnPropertyDescriptor(e,r){if(!e.exclude.includes(r)&&r in e.props)return{enumerable:!0,configurable:!0,value:e.props[r]}},deleteProperty(e,r){return e.exclude.includes(r)||(e.exclude.push(r),L(e.version)),!0},has(e,r){return e.exclude.includes(r)?!1:r in e.props},ownKeys(e){return Reflect.ownKeys(e.props).filter(r=>!e.exclude.includes(r))}};function xe(e,r){return new Proxy({props:e,exclude:r,special:{},version:te(0),parent_effect:A},ge)}const Ee={get(e,r){let s=e.props.length;for(;s--;){let n=e.props[s];if(S(n)&&(n=n()),typeof n=="object"&&n!==null&&r in n)return n[r]}},set(e,r,s){let n=e.props.length;for(;n--;){let i=e.props[n];S(i)&&(i=i());const a=m(i,r);if(a&&a.set)return a.set(s),!0}return!1},getOwnPropertyDescriptor(e,r){let s=e.props.length;for(;s--;){let n=e.props[s];if(S(n)&&(n=n()),typeof n=="object"&&n!==null&&r in n){const i=m(n,r);return i&&!i.configurable&&(i.configurable=!0),i}}},has(e,r){if(r===y||r===F)return!1;for(let s of e.props)if(S(s)&&(s=s()),s!=null&&r in s)return!0;return!1},ownKeys(e){const r=[];for(let s of e.props)if(S(s)&&(s=s()),!!s){for(const n in s)r.includes(n)||r.push(n);for(const n of Object.getOwnPropertySymbols(s))r.includes(n)||r.push(n)}return r}};function De(...e){return new Proxy({props:e},Ee)}function Oe(e,r,s,n){var i=!_e||(s&pe)!==0,a=(s&de)!==0,l=(s&be)!==0,d=n,p=!0,I=()=>(p&&(p=!1,d=l?B(n):n),d),f;if(a){var g=y in e||F in e;f=m(e,r)?.set??(g&&r in e?t=>e[r]=t:void 0)}var _,c=!1;a?[_,c]=Ie(()=>e[r]):_=e[r],_===void 0&&n!==void 0&&(_=I(),f&&(i&&ue(),f(_)));var u;if(i?u=()=>{var t=e[r];return t===void 0?I():(p=!0,t)}:u=()=>{var t=e[r];return t!==void 0&&(d=void 0),t===void 0?d:t},i&&(s&C)===0)return u;if(f){var E=e.$$legacy;return(function(t,v){return arguments.length>0?((!i||!v||E||c)&&f(v?u():t),t):u()})}var b=!1,o=((s&ve)!==0?ce:oe)(()=>(b=!1,u()));a&&P(o);var h=A;return(function(t,v){if(arguments.length>0){const O=v?P(o):i&&a?fe(t):t;return M(o,O),b=!0,d!==void 0&&(d=O),t}return he&&b||(h.f&le)!==0?o.v:P(o)})}export{ye as a,we as b,Ae as c,me as i,xe as l,Oe as p,De as s};
|
1
server/playground/_app/immutable/chunks/bc36GTfJ.js
Normal file
1
server/playground/_app/immutable/chunks/bc36GTfJ.js
Normal file
File diff suppressed because one or more lines are too long
1
server/playground/_app/immutable/chunks/kadZwC1X.js
Normal file
1
server/playground/_app/immutable/chunks/kadZwC1X.js
Normal file
@@ -0,0 +1 @@
|
||||
import{c as d,u as g,a as l,b,r as i,d as m,g as p,e as h,f as v,h as k}from"./bc36GTfJ.js";function x(n=!1){const s=d,e=s.l.u;if(!e)return;let o=()=>h(s.s);if(n){let a=0,t={};const _=v(()=>{let c=!1;const r=s.s;for(const f in r)r[f]!==t[f]&&(t[f]=r[f],c=!0);return c&&a++,a});o=()=>p(_)}e.b.length&&g(()=>{u(s,o),i(e.b)}),l(()=>{const a=b(()=>e.m.map(m));return()=>{for(const t of a)typeof t=="function"&&t()}}),e.a.length&&l(()=>{u(s,o),i(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}k();export{x as i};
|
2
server/playground/_app/immutable/entry/app.TBE8HBpz.js
Normal file
2
server/playground/_app/immutable/entry/app.TBE8HBpz.js
Normal file
File diff suppressed because one or more lines are too long
1
server/playground/_app/immutable/entry/start.DAIBqtaT.js
Normal file
1
server/playground/_app/immutable/entry/start.DAIBqtaT.js
Normal file
@@ -0,0 +1 @@
|
||||
import{l as o,a as r}from"../chunks/BsPC8ki2.js";export{o as load_css,r as start};
|
1
server/playground/_app/immutable/nodes/0.BxuPCyXV.js
Normal file
1
server/playground/_app/immutable/nodes/0.BxuPCyXV.js
Normal file
@@ -0,0 +1 @@
|
||||
import"../chunks/DsnmJJEf.js";import{i as c,j as p,k as a,s as m,l as r,w as _,$ as f,n as u,t as d,m as v,o as g}from"../chunks/bc36GTfJ.js";import{s as h,f as y}from"../chunks/DyOAiIET.js";const b=!0,$=Object.freeze(Object.defineProperty({__proto__:null,prerender:b},Symbol.toStringTag,{value:"Module"}));var j=_(g('<link rel="icon"/> <script src="/_playground/wasm_exec.js"><\/script>',1));function M(o,s){var t=c();p(i=>{var e=j();f.title="Marka Playground";var l=a(e);u(2),d(()=>h(l,"href",y)),r(i,e)});var n=a(t);m(n,()=>s.children??v),r(o,t)}export{M as component,$ as universal};
|
1
server/playground/_app/immutable/nodes/1.D9DCRrr8.js
Normal file
1
server/playground/_app/immutable/nodes/1.D9DCRrr8.js
Normal file
@@ -0,0 +1 @@
|
||||
import"../chunks/DsnmJJEf.js";import{i as u}from"../chunks/kadZwC1X.js";import{p as h,o as g,k as l,t as v,l as d,q as x,v as e,x as a,y as _}from"../chunks/bc36GTfJ.js";import{s as o}from"../chunks/B1lAeocp.js";import{s as k,p}from"../chunks/BsPC8ki2.js";const $={get error(){return p.error},get status(){return p.status}};k.updated.check;const m=$;var b=g("<h1> </h1> <p> </p>",1);function z(i,n){h(n,!1),u();var r=b(),t=l(r),c=e(t,!0);a(t);var s=_(t,2),f=e(s,!0);a(s),v(()=>{o(c,m.status),o(f,m.error?.message)}),d(i,r),x()}export{z as component};
|
132
server/playground/_app/immutable/nodes/2.BEtOseUT.js
Normal file
132
server/playground/_app/immutable/nodes/2.BEtOseUT.js
Normal file
File diff suppressed because one or more lines are too long
1
server/playground/_app/version.json
Normal file
1
server/playground/_app/version.json
Normal file
@@ -0,0 +1 @@
|
||||
{"version":"1759702763367"}
|
64
server/playground/index.html
Normal file
64
server/playground/index.html
Normal file
File diff suppressed because one or more lines are too long
15
server/playground/logo.svg
Normal file
15
server/playground/logo.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="100" height="100" 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 20.1238 13 43.5 13C53.0351 13 71.5842 16.8812 80 30C80 30 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 16.0505 65.702 20 71.5C27.6856 82.7837 41.5584 90 57 90C80.3762 90 100 71.3259 100 50.0001C100 77.6145 77.6144 100 50 100C22.3856 100 0 77.6145 0 50.0001Z" fill="url(#paint0_radial_13_17)"/>
|
||||
<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_radial_13_17)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_13_17" cx="0" cy="0" r="1" gradientTransform="matrix(-59 -89 76.5985 -50.3819 82 91)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8E8E8E"/>
|
||||
<stop offset="1" stop-color="#2C2C2C"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_13_17" cx="0" cy="0" r="1" gradientTransform="matrix(58 89.5 -74.0064 52.2522 26 5.5)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8E8E8E"/>
|
||||
<stop offset="1" stop-color="#2C2C2C"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
server/playground/main.wasm
Normal file
BIN
server/playground/main.wasm
Normal file
Binary file not shown.
3
server/playground/robots.txt
Normal file
3
server/playground/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
553
server/playground/wasm_exec.js
Normal file
553
server/playground/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);
|
||||
});
|
||||
}
|
||||
})();
|
118
template/README.md
Normal file
118
template/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Marka Template Language
|
||||
|
||||
The marka template languages uses curly braces (`{`/`}`) to seperate `data` content from `matching` content.
|
||||
|
||||
The data content is extracted from the markdown file during parsing and replaced by actual data during rendering.
|
||||
|
||||
The `data` parts aka `data-blocks` support two different syntaxes which with they are defined.
|
||||
|
||||
## Short Syntax
|
||||
|
||||
The first is the "short" syntax which may look like one of the following:
|
||||
|
||||
```
|
||||
{ articleTitle }
|
||||
{ articleId | number }
|
||||
{ articleTags | hashtags,optional }
|
||||
```
|
||||
|
||||
Before the `|` character is the path of the data block inside the data. After the (optional) `|` come comma seperated flags on how to parse/render this specific block.
|
||||
|
||||
## Long Syntax
|
||||
|
||||
The long syntax uses `yaml` to define the `data-blocks`:
|
||||
|
||||
```
|
||||
{
|
||||
path: recipeIngredient
|
||||
codec: list
|
||||
listTemplate: "- { . }"
|
||||
}
|
||||
```
|
||||
|
||||
## Codecs
|
||||
|
||||
Codecs are the predefined ways on how to parse/render `data-blocks`. The current list of codecs looks like this:
|
||||
|
||||
- text
|
||||
- number
|
||||
- yaml
|
||||
- list
|
||||
- const
|
||||
- hashtags
|
||||
|
||||
While `text` or `number` may be self explanatory the other codecs may require some more explanation.
|
||||
|
||||
### Const Codec
|
||||
|
||||
The const codec is used to specify fields which should always be the same during rendering. For example if for a specified template the title should always be the same when rendering/parsing:
|
||||
|
||||
```
|
||||
# {
|
||||
path: articleTitle
|
||||
codec: const
|
||||
value: TestArticle
|
||||
}
|
||||
```
|
||||
|
||||
> Note: You need to supply a "value" with the "const" codec otherwise the application will throw an error
|
||||
|
||||
|
||||
### List Codec
|
||||
|
||||
The list codec is used to parse/render list blocks like the following:
|
||||
|
||||
```markdown
|
||||
# Ingredients
|
||||
- Flour
|
||||
- Water
|
||||
- Yeast
|
||||
- Salt
|
||||
```
|
||||
|
||||
Or even numbered lists like:
|
||||
|
||||
```markdown
|
||||
# Steps
|
||||
1. Mix all the ingredients except salt
|
||||
2. Let the dough sit for 1/2 hour (autolyse)
|
||||
3. Add the salt
|
||||
4. Knead again
|
||||
```
|
||||
|
||||
To parse the steps list you could use a template like this:
|
||||
|
||||
```markdown
|
||||
# Steps
|
||||
{
|
||||
path: recipeInstructions
|
||||
codec: list
|
||||
listTemplate: "{ @index }. { . }"
|
||||
}
|
||||
```
|
||||
|
||||
The `listTemplate` is a required field on the `list` block. It uses the same syntax as the template itself, with the special edition of a `@index` variable to handle the index in numbered lists.
|
||||
|
||||
### Yaml Codec
|
||||
|
||||
The `yaml` codec parses parses/renders its content as yaml. Every field inside the yaml needs to be specified:
|
||||
|
||||
```
|
||||
{
|
||||
path: .
|
||||
codec: yaml
|
||||
fields:
|
||||
- path: "@context"
|
||||
codec: const
|
||||
value: https://schema.org
|
||||
hidden: true
|
||||
- path: "@schema"
|
||||
codec: const
|
||||
value: Article
|
||||
hidden: true
|
||||
- path: "@type"
|
||||
codec: const
|
||||
value: Article
|
||||
}
|
||||
```
|
||||
|
@@ -1,8 +1,7 @@
|
||||
// Package blocks contains the logic for parsing template blocks.
|
||||
// Package template contains the logic for parsing templates.
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -16,8 +15,8 @@ const (
|
||||
)
|
||||
|
||||
// DetectTemplateType checks if the template is short or long.
|
||||
func DetectTemplateType(tmpl string) TemplateType {
|
||||
trimmed := strings.TrimSpace(tmpl)
|
||||
func DetectTemplateType(tmpl Slice) TemplateType {
|
||||
trimmed := strings.TrimSpace(tmpl.String())
|
||||
|
||||
// Short type: starts with "{" and ends with "}" on a single line,
|
||||
// and contains "|" or "," inside for inline definition
|
||||
@@ -43,15 +42,15 @@ func DetectTemplateType(tmpl string) TemplateType {
|
||||
return InvalidTemplate
|
||||
}
|
||||
|
||||
func cleanTemplate(input string) string {
|
||||
s := strings.TrimSpace(input)
|
||||
func cleanTemplate(input Slice) string {
|
||||
s := strings.TrimSpace(input.String())
|
||||
s = strings.TrimPrefix(s, "{")
|
||||
s = strings.TrimSuffix(s, "}")
|
||||
s = strings.Trim(s, "\n")
|
||||
return s
|
||||
}
|
||||
|
||||
func ParseTemplateBlock(template string, blockType BlockType) (block Block, err error) {
|
||||
func ParseTemplateBlock(template Slice, blockType BlockType) (block Block, err error) {
|
||||
if blockType == MatchingBlock {
|
||||
return Block{
|
||||
Type: MatchingBlock,
|
||||
@@ -66,5 +65,5 @@ func ParseTemplateBlock(template string, blockType BlockType) (block Block, err
|
||||
return parseYamlTemplate(template)
|
||||
}
|
||||
|
||||
return block, fmt.Errorf("invalid template")
|
||||
return block, NewErrorf("invalid template: '%s'", template.String()).WithPosition(template.start, template.end)
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseShortTemplate(input string) (Block, error) {
|
||||
func parseShortTemplate(input Slice) (Block, error) {
|
||||
split := strings.Split(cleanTemplate(input), "|")
|
||||
if len(split) < 1 {
|
||||
return Block{}, fmt.Errorf("invalid short template")
|
||||
|
@@ -22,10 +22,10 @@ type yamlField struct {
|
||||
Value any `yaml:"value,omitempty"`
|
||||
Codec string `yaml:"codec"`
|
||||
Hidden bool `yaml:"hidden,omitempty"`
|
||||
PathAlias []string `yaml:"pathAlias,omitempty"`
|
||||
PathAlias string `yaml:"pathAlias,omitempty"`
|
||||
}
|
||||
|
||||
func parseYamlTemplate(input string) (block Block, err error) {
|
||||
func parseYamlTemplate(input Slice) (block Block, err error) {
|
||||
var blk yamlBlock
|
||||
|
||||
cleaned := cleanTemplate(input)
|
||||
@@ -34,7 +34,7 @@ func parseYamlTemplate(input string) (block Block, err error) {
|
||||
dec.KnownFields(true)
|
||||
|
||||
if err := dec.Decode(&blk); err != nil {
|
||||
return block, fmt.Errorf("content '%q': %w", cleaned, err)
|
||||
return block, NewErrorf("failed to parse yaml -> %w", err).WithPosition(input.start, input.end)
|
||||
}
|
||||
|
||||
if blk.Path == "" {
|
||||
@@ -47,7 +47,7 @@ func parseYamlTemplate(input string) (block Block, err error) {
|
||||
|
||||
codec, err := parseCodecType(blk.Codec)
|
||||
if err != nil {
|
||||
return block, fmt.Errorf("failed to parse codec: %w", err)
|
||||
return block, fmt.Errorf("failed to parse codec -> %w", err)
|
||||
}
|
||||
|
||||
var fields []BlockField
|
||||
@@ -63,7 +63,7 @@ func parseYamlTemplate(input string) (block Block, err error) {
|
||||
|
||||
fieldCodec, err := parseCodecType(field.Codec)
|
||||
if err != nil {
|
||||
return block, fmt.Errorf("failed to parse codec: %w", err)
|
||||
return block, fmt.Errorf("failed to parse codec -> %w", err)
|
||||
}
|
||||
|
||||
fields = append(fields, BlockField{
|
||||
|
@@ -1,13 +1,13 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
import "strings"
|
||||
|
||||
// CompileTemplate scans once, emitting:
|
||||
// - data blocks: inner content between a line that's exactly "{" and a line that's exactly "}"
|
||||
// - matching blocks: gaps between data blocks (excluding the brace lines themselves)
|
||||
func CompileTemplate(template string) ([]Block, error) {
|
||||
func CompileTemplate(templateSource string) ([]Block, error) {
|
||||
templateSource = strings.TrimSuffix(templateSource, "\n")
|
||||
|
||||
var out []Block
|
||||
var curlyIndex int
|
||||
|
||||
@@ -16,14 +16,15 @@ func CompileTemplate(template string) ([]Block, error) {
|
||||
|
||||
var start int
|
||||
var blockType BlockType
|
||||
template := NewSlice(templateSource)
|
||||
|
||||
if len(template) > 0 && template[0] == OPENING {
|
||||
if template.Len() > 0 && template.At(0) == OPENING {
|
||||
blockType = DataBlock
|
||||
} else {
|
||||
blockType = MatchingBlock
|
||||
}
|
||||
|
||||
for i, r := range template {
|
||||
for i, r := range template.Chars() {
|
||||
|
||||
nextCurlyIndex := curlyIndex
|
||||
|
||||
@@ -36,27 +37,26 @@ func CompileTemplate(template string) ([]Block, error) {
|
||||
|
||||
if curlyIndex == 0 && nextCurlyIndex == 1 {
|
||||
if i > start {
|
||||
block, err := ParseTemplateBlock(template[start:i], blockType)
|
||||
block, err := ParseTemplateBlock(template.Slice(start, i), blockType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse block: %w", err)
|
||||
return nil, NewErrorf("cannot parse block @pos -> %w", err).WithPosition(start, i)
|
||||
}
|
||||
out = append(out, block)
|
||||
}
|
||||
start = i
|
||||
blockType = DataBlock
|
||||
} else if curlyIndex == 1 && nextCurlyIndex == 0 {
|
||||
|
||||
if i > start {
|
||||
block, err := ParseTemplateBlock(template[start:i+1], blockType)
|
||||
block, err := ParseTemplateBlock(template.Slice(start, i+1), blockType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse block: %w", err)
|
||||
return nil, NewErrorf("cannot parse block @pos -> %w", err).WithPosition(start, i+1)
|
||||
}
|
||||
out = append(out, block)
|
||||
}
|
||||
|
||||
nextChar := ' '
|
||||
if i+1 < len(template) {
|
||||
nextChar = rune(template[i+1])
|
||||
if i+1 < template.Len() {
|
||||
nextChar = rune(template.At(i + 1))
|
||||
}
|
||||
|
||||
if nextChar == OPENING {
|
||||
@@ -71,5 +71,17 @@ func CompileTemplate(template string) ([]Block, error) {
|
||||
curlyIndex = nextCurlyIndex
|
||||
}
|
||||
|
||||
if curlyIndex != 0 {
|
||||
return nil, NewErrorf("unclosed block").WithPosition(start, template.Len())
|
||||
}
|
||||
|
||||
if start < template.Len() {
|
||||
block, err := ParseTemplateBlock(template.Slice(start, template.Len()), blockType)
|
||||
if err != nil {
|
||||
return nil, NewErrorf("cannot parse final block @pos -> %w", err).WithPosition(start, template.Len())
|
||||
}
|
||||
out = append(out, block)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.max-richter.dev/max/marka/registry"
|
||||
@@ -10,16 +11,20 @@ import (
|
||||
func TestExtractBlocks(t *testing.T) {
|
||||
src, err := registry.GetTemplate("Recipe")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to extract blocks: %s", err.Error())
|
||||
t.Errorf("failed to load template 'Recipe' -> %s", err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
templateBlocks, err := template.CompileTemplate(src)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to extract blocks: %s", err.Error())
|
||||
t.Errorf("failed to compile template -> %s", err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
for i, b := range templateBlocks {
|
||||
fmt.Printf("Block#%d: %q\n", i, b.GetContent())
|
||||
}
|
||||
|
||||
expected := []template.Block{
|
||||
{
|
||||
Type: template.MatchingBlock,
|
||||
|
36
template/error.go
Normal file
36
template/error.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
err error
|
||||
start, end int
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
content := e.err.Error()
|
||||
|
||||
if strings.Contains(content, " @pos ") {
|
||||
if e.start == e.end {
|
||||
return strings.ReplaceAll(content, " @pos ", " ")
|
||||
}
|
||||
return strings.ReplaceAll(content, " @pos ", fmt.Sprintf(" position=%d:%d ", e.start, e.end))
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func NewErrorf(msg string, args ...any) Error {
|
||||
return Error{
|
||||
err: fmt.Errorf(msg, args...),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Error) WithPosition(start, end int) Error {
|
||||
e.start = start
|
||||
e.end = end
|
||||
return e
|
||||
}
|
46
template/slice.go
Normal file
46
template/slice.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package template
|
||||
|
||||
func NewSlice(s string) Slice {
|
||||
return Slice{
|
||||
source: &s,
|
||||
start: 0,
|
||||
end: len(s),
|
||||
}
|
||||
}
|
||||
|
||||
type Slice struct {
|
||||
source *string
|
||||
start, end int
|
||||
}
|
||||
|
||||
func (s Slice) Chars() []byte {
|
||||
return []byte((*s.source)[s.start:s.end])
|
||||
}
|
||||
|
||||
func (s Slice) Len() int {
|
||||
return s.end - s.start
|
||||
}
|
||||
|
||||
func (s Slice) At(i int) byte {
|
||||
return (*s.source)[s.start+i]
|
||||
}
|
||||
|
||||
func SliceFromString(s string, start, end int) Slice {
|
||||
return Slice{
|
||||
source: &s,
|
||||
start: start,
|
||||
end: end,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Slice) String() string {
|
||||
return (*s.source)[s.start:s.end]
|
||||
}
|
||||
|
||||
func (s Slice) Slice(start, end int) Slice {
|
||||
return Slice{
|
||||
source: s.source,
|
||||
start: s.start + start,
|
||||
end: s.start + end,
|
||||
}
|
||||
}
|
@@ -22,9 +22,9 @@ type Block struct {
|
||||
Fields []BlockField
|
||||
Optional bool
|
||||
Value any
|
||||
content string
|
||||
content Slice
|
||||
}
|
||||
|
||||
func (b Block) GetContent() string {
|
||||
return b.content
|
||||
return b.content.String()
|
||||
}
|
||||
|
1
testdata/data/article_simple/output.json
vendored
1
testdata/data/article_simple/output.json
vendored
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"_schema": "Article",
|
||||
"_type": "Article",
|
||||
"headline": "My First Article",
|
||||
"author": {
|
||||
|
1
testdata/data/baguette/output.json
vendored
1
testdata/data/baguette/output.json
vendored
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"_schema": "Recipe",
|
||||
"_type": "Recipe",
|
||||
"name": "Baguette",
|
||||
"author": {
|
||||
|
1
testdata/data/recipe_salad/input.md
vendored
1
testdata/data/recipe_salad/input.md
vendored
@@ -8,7 +8,6 @@ recipeYield: 2 servings
|
||||
---
|
||||
|
||||
# Simple Salad
|
||||
#healthy #salad
|
||||
|
||||
A quick green salad.
|
||||
|
||||
|
4
testdata/data/recipe_salad/output.json
vendored
4
testdata/data/recipe_salad/output.json
vendored
@@ -7,10 +7,6 @@
|
||||
"_type": "Person",
|
||||
"name": "Alex Chef"
|
||||
},
|
||||
"keywords": [
|
||||
"healthy",
|
||||
"salad"
|
||||
],
|
||||
"description": "A quick green salad.",
|
||||
"prepTime": "PT10M",
|
||||
"cookTime": "PT0M",
|
||||
|
Reference in New Issue
Block a user