Compare commits

...

15 Commits

Author SHA1 Message Date
Max Richter
7b3261caf9 feat: update playground paths 2025-10-06 00:19:33 +02:00
Max Richter
0ee8f8b655 feat: update playground paths 2025-10-06 00:09:51 +02:00
Max Richter
881afa4b6a feat: update playground paths 2025-10-06 00:08:10 +02:00
Max Richter
fa8e0a82f7 feat: update playground paths 2025-10-06 00:07:07 +02:00
Max Richter
b94dbca69b feat: update playground paths 2025-10-06 00:04:01 +02:00
Max Richter
951f07b25e feat: update playground paths 2025-10-06 00:01:57 +02:00
Max Richter
b763ede6cb fix: update playground to use correct path 2025-10-05 23:57:38 +02:00
Max Richter
76848b396c feat: default playground path to "/app/playground" 2025-10-05 23:55:44 +02:00
Max Richter
0970a662b8 fix: correct playground dir path in dockerfile 2025-10-05 23:53:54 +02:00
Max Richter
5adc3a3cc1 feat: server playground 2025-10-05 23:52:31 +02:00
Max Richter
6d92c92797 feat: simplify data and add cache to LocalFsAdapter 2025-10-05 22:20:43 +02:00
Max Richter
fa283d5dd7 feat: also parse folder content 2025-10-04 13:16:16 +02:00
Max Richter
a5acef77a2 feat: add about and url to article template 2025-10-04 12:33:36 +02:00
Max Richter
74528625e3 feat: handle binary content 2025-10-04 12:31:40 +02:00
Max Richter
ab81c980b5 feat: allow multiple roots 2025-10-03 21:41:39 +02:00
39 changed files with 1126 additions and 160 deletions

View File

@@ -3,7 +3,6 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/wasm_exec.js"></script>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View File

@@ -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

View File

@@ -1,4 +1,8 @@
<img src="/logo-2.svg" alt="logo" width="100%" /> <script>
import favicon from '$lib/assets/favicon.svg';
</script>
<img src={favicon} alt="logo" width="100%" />
<style> <style>
img { img {

View File

@@ -1,7 +1,5 @@
import { readable } from "svelte/store"; import { readable } from "svelte/store";
export const wasmReady = readable(false, (set) => { export const wasmReady = readable(false, (set) => {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
@@ -11,7 +9,7 @@ export const wasmReady = readable(false, (set) => {
const go = new globalThis.Go(); const go = new globalThis.Go();
try { try {
const result = await WebAssembly.instantiateStreaming( const result = await WebAssembly.instantiateStreaming(
fetch("/main.wasm"), fetch("/_playground/main.wasm"),
go.importObject, go.importObject,
); );
go.run(result.instance); go.run(result.instance);
@@ -111,4 +109,3 @@ export function detectType(markdown: string): string | ParseResultError {
} }
return result; return result;
} }

View File

@@ -8,7 +8,7 @@
<svelte:head> <svelte:head>
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
<title>Marka Playground</title> <title>Marka Playground</title>
<script src="/wasm_exec.js"></script> <script src="/_playground/wasm_exec.js"></script>
</svelte:head> </svelte:head>
{@render children?.()} {@render children?.()}

View File

@@ -1,3 +0,0 @@
<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.4 KiB

View File

@@ -12,6 +12,10 @@ const config = {
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // 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. // See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(), adapter: adapter(),
paths: {
base: "/_playground",
},
}, },
}; };

View File

@@ -11,6 +11,8 @@
codec: const codec: const
value: Article value: Article
- path: image - path: image
- path: about
- path: url
- path: author.name - path: author.name
- path: author._type - path: author._type
codec: const codec: const

View File

@@ -10,6 +10,7 @@
- path: "_type" - path: "_type"
codec: const codec: const
value: Recipe value: Recipe
- path: link
- path: image - path: image
- path: author._type - path: author._type
codec: const codec: const

View File

@@ -10,7 +10,8 @@ RUN --mount=type=cache,target=/go/pkg/mod \
FROM scratch FROM scratch
WORKDIR /app WORKDIR /app
COPY --from=build /out/server /app/server COPY --from=build /out/server /app/server
COPY --from=build /src/server/playground /app/playground
USER 65532:65532 USER 65532:65532
EXPOSE 8080 EXPOSE 8080
CMD ["/server","-root=/app/data","-addr=:8080"] CMD ["/app/server","-root=/app/data","-addr=:8080","-playground-root=/app/playground"]

15
server/README.md Normal file
View File

@@ -0,0 +1,15 @@
## Marka Server
```json
{
"name": "Recipe",
"modTime": 123123123123,
"content": [
{
"name": "Baguette",
"modTime": 123123123123,
}
]
}
```

View File

@@ -6,31 +6,61 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"git.max-richter.dev/max/marka/server-new/internal/adapters" "git.max-richter.dev/max/marka/server/internal/adapters"
"git.max-richter.dev/max/marka/server-new/internal/handler" "git.max-richter.dev/max/marka/server/internal/handler"
) )
func main() { type multi []string
root := flag.String("root", ".", "filesystem root to serve")
addr := flag.String("addr", ":8080", "listen address")
flag.Parse()
absRoot, err := filepath.Abs(*root) func (m *multi) String() string { return strings.Join(*m, ",") }
must(err) func (m *multi) Set(v string) error {
*m = append(*m, v)
info, err := os.Stat(absRoot) return nil
must(err)
if !info.IsDir() {
log.Fatal("root is not a directory")
} }
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) must(err)
http.Handle("/", handler.NewHandler(fsAdapter)) 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)) log.Fatal(http.ListenAndServe(*addr, nil))
} }

View File

@@ -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 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
)

View File

@@ -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-20251003194139-ab81c980b590 h1:4C3b/KHD7+ru7nunpYIY6snVw+RI1QeVk9XcbzmdVY0=
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/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 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/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= git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e h1:9Eg81l8YMTXWZC3xlZ5L/NJRuK26bksrVtEHyCTV4sM=

View File

@@ -1,97 +1,266 @@
package adapters package adapters
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync"
"time"
"git.max-richter.dev/max/marka/parser"
) )
type LocalFsAdapter struct { type LocalFsAdapter struct {
root string roots []string
cache map[string]*Entry
mu sync.RWMutex
} }
func (l LocalFsAdapter) readDir(path string) (FsResponse, error) { func (l *LocalFsAdapter) readDir(path string, root string) (*Entry, error) {
dirInfo, _ := os.Stat(path) dirInfo, err := os.Stat(path)
if err != nil {
return nil, err
}
entries, err := os.ReadDir(path) entries, err := os.ReadDir(path)
if err != nil { 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 { for _, e := range entries {
info, _ := e.Info() info, _ := e.Info()
entryType := "dir" entryType := "dir"
if !e.IsDir() { 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(), Name: e.Name(),
Path: responseChildPath,
Type: entryType, Type: entryType,
ModTime: info.ModTime(), 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{ entry := &Entry{
Dir: &FsDir{ Type: "dir",
Files: out, Name: "/",
Name: ResponsePath(l.root, path), Path: "/",
ModTime: dirInfo.ModTime(), ModTime: latestModTime,
}, Content: entries,
}, nil
} }
func (l LocalFsAdapter) readFile(path string) (FsResponse, error) { l.mu.Lock()
fi, err := os.Stat(path) 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 { if err != nil {
return FsResponse{}, err return nil, err
} }
absRoot, err := filepath.Abs(targetRoot)
data, err := os.ReadFile(path)
if err != nil { if err != nil {
return FsResponse{}, err return nil, err
} }
if !strings.HasPrefix(absTarget, absRoot) {
return FsResponse{ return nil, errors.New("path escapes root")
File: &FsFile{
Name: ResponsePath(l.root, path),
Type: contentTypeFor(path),
ModTime: fi.ModTime(),
Content: data,
},
}, nil
} }
func (l LocalFsAdapter) Read(path string) (FsResponse, error) {
cleanRel, err := SafeRel(l.root, path)
if err != nil {
return FsResponse{}, err
}
target := filepath.Join(l.root, filepath.FromSlash(cleanRel))
fi, err := os.Stat(target) fi, err := os.Stat(target)
if err != nil { if err != nil {
if os.IsNotExist(err) { 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() { 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 return nil
} }
func NewLocalFsAdapter(root string) (FileAdapter, error) { func NewLocalFsAdapter(roots []string) (FileAdapter, error) {
return LocalFsAdapter{ return &LocalFsAdapter{
root: root, roots: roots,
cache: make(map[string]*Entry),
}, nil }, nil
} }

View File

@@ -3,10 +3,8 @@ package adapters
import ( import (
"errors" "errors"
"mime" "mime"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
) )
func SafeRel(root, requested string) (string, error) { func SafeRel(root, requested string) (string, error) {
@@ -37,20 +35,6 @@ func ResponsePath(root, full string) string {
return "/" + filepath.ToSlash(rel) 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{ var textPlainExtensions = map[string]bool{
".txt": true, ".txt": true,
".log": true, ".log": true,

View File

@@ -3,32 +3,26 @@ package adapters
import "time" 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 { type FileAdapter interface {
Read(path string) (FsResponse, error) Read(path string) (*Entry, error)
Write(path string, content []byte) 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"`
}

View File

@@ -4,76 +4,44 @@ package handler
import ( import (
"errors" "errors"
"net/http" "net/http"
"time"
"git.max-richter.dev/max/marka/parser" "git.max-richter.dev/max/marka/server/internal/adapters"
"git.max-richter.dev/max/marka/server-new/internal/adapters"
) )
type ResponseItem struct {
Name string `json:"name"`
Content any `json:"content,omitempty"`
Type string `json:"type,omitempty"`
IsDir bool `json:"isDir"`
Size int64 `json:"size,omitempty"`
ModTime time.Time `json:"modTime"`
}
type Handler struct { type Handler struct {
adapter adapters.FileAdapter adapter adapters.FileAdapter
} }
func (h *Handler) get(w http.ResponseWriter, target string) { func (h *Handler) get(w http.ResponseWriter, target string) {
fsEntry, err := h.adapter.Read(target) entry, err := h.adapter.Read(target)
if err != nil { 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 return
} }
if fsEntry.File != nil { if entry.Type == "file" {
// For non-markdown files, content is []byte, write it directly
if fsEntry.File.Content != nil && fsEntry.File.Type == "application/markdown" { if contentBytes, ok := entry.Content.([]byte); ok {
data, err := parser.ParseFile(string(fsEntry.File.Content)) w.Header().Set("Content-Type", entry.MIME)
if err != nil { w.Write(contentBytes)
writeError(w, 500, err) return
}
// For markdown files, content is parsed, return the whole Entry as JSON
writeJSON(w, http.StatusOK, entry)
return return
} }
res := ResponseItem{ // For directories, return the whole Entry as JSON
Name: fsEntry.File.Name, if entry.Type == "dir" {
Type: fsEntry.File.Type, writeJSON(w, http.StatusOK, entry)
Content: data,
IsDir: false,
Size: int64(len(fsEntry.File.Content)),
ModTime: fsEntry.File.ModTime,
}
writeJSON(w, 200, res)
return return
} }
res := ResponseItem{ writeError(w, http.StatusInternalServerError, errors.New("unknown entry type"))
Name: fsEntry.File.Name,
Content: fsEntry.File.Content,
Type: fsEntry.File.Type,
IsDir: false,
Size: int64(len(fsEntry.File.Content)),
ModTime: fsEntry.File.ModTime,
}
writeJSON(w, 200, res)
return
}
if fsEntry.Dir != nil {
res := ResponseItem{
Name: fsEntry.Dir.Name,
Content: fsEntry.Dir.Files,
IsDir: true,
ModTime: fsEntry.Dir.ModTime,
}
writeJSON(w, 200, res)
return
}
} }
func (h *Handler) post(w http.ResponseWriter, target string) { func (h *Handler) post(w http.ResponseWriter, target string) {

View File

@@ -0,0 +1 @@
export const env={}

File diff suppressed because one or more lines are too long

View 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}

View 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};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
typeof window<"u"&&((window.__svelte??={}).v??=new Set).add("5");

File diff suppressed because one or more lines are too long

View 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};

File diff suppressed because one or more lines are too long

View 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};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{l as o,a as r}from"../chunks/BsPC8ki2.js";export{o as load_css,r as start};

View 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};

View 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};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":"1759702763367"}

File diff suppressed because one or more lines are too long

View 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

Binary file not shown.

View File

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

View File

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