summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-03 14:22:23 +0200
committerivar <i@oiee.no>2026-04-03 14:22:23 +0200
commit33310be68544d3381ac6e9899790f4a106e17e8f (patch)
tree385102a7216ffba17d054f11032dae4447371d52
parenta8914a8f18c345e934bce93b37845a9dfe0ad73e (diff)
downloadnebbet.no-33310be68544d3381ac6e9899790f4a106e17e8f.tar.xz
nebbet.no-33310be68544d3381ac6e9899790f4a106e17e8f.zip
refactor: convert admin handlers to Gin context-based signatures
- Remove old ServeHTTP method (no longer needed with Gin routing) - Update all 6 handler methods to use *gin.Context instead of http.ResponseWriter, *http.Request - Convert handler signatures: handleList, handleNew, handleNewPost, handleEdit, handleDelete - Remove render() helper (use c.HTML() directly) - Update renderError() to accept gin.Context instead of http.ResponseWriter - Update postFromForm() to extract form data from gin.Context using c.PostForm() - Update main.go to use adminSrv.NewServer() and adminSrv.Engine() - All handlers now use Gin methods: c.HTML(), c.PostForm(), c.Param(), c.Redirect() - Path parameters now extracted via c.Param("slug") instead of function arguments - HTTP status codes and error handling fully migrated to Gin patterns Build verified: go build ./cmd/nebbet succeeds Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules0
-rw-r--r--CLAUDE.md172
-rw-r--r--cmd/nebbet/main.go20
-rw-r--r--content/admin/index.md16
-rw-r--r--docs/superpowers/plans/2026-04-03-gin-migration-plan.md739
-rw-r--r--docs/superpowers/specs/2026-04-03-gin-migration-design.md198
-rw-r--r--internal/admin/auth/auth.go (renamed from internal/auth/auth.go)0
-rw-r--r--internal/admin/server.go137
-rw-r--r--internal/db/meta.go2
-rw-r--r--internal/db/search.go2
-rw-r--r--lib/bun.lock422
-rw-r--r--lib/package.json5
-rw-r--r--scripts/download-deps.sh25
-rw-r--r--styles/admin.css28
-rw-r--r--templates/admin/base.html34
-rw-r--r--templates/admin/error.html7
-rw-r--r--templates/admin/form.html62
-rw-r--r--templates/admin/list.html40
-rw-r--r--tmp/build-errors.log1
20 files changed, 1801 insertions, 110 deletions
diff --git a/.gitignore b/.gitignore
index 1c8c6be..31c9711 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ public/
data/
.passwords
/nebbet
+node_modules/
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index e69de29..0000000
--- a/.gitmodules
+++ /dev/null
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..aaede3c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,172 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+**nebbet** is a static site generator (SSG) with an integrated admin UI for managing blog posts. It compiles markdown content to HTML, maintains metadata and search indices in SQLite, and provides a web interface for post creation/editing without redeploying.
+
+## Build & Run Commands
+
+### Build a production site
+```bash
+go run ./cmd/nebbet build
+```
+
+### Watch mode (continuous rebuild on file changes)
+```bash
+go run ./cmd/nebbet watch
+```
+
+### Development server (watch + live admin UI)
+```bash
+go run ./cmd/nebbet serve --port 8080
+```
+Visit `http://localhost:8080/admin/` to manage posts. Admin requires a user; create one first:
+
+### User management
+```bash
+# Add a user (prompts for password)
+go run ./cmd/nebbet user add alice
+
+# List users
+go run ./cmd/nebbet user list
+
+# Change password
+go run ./cmd/nebbet user passwd alice
+
+# Delete user
+go run ./cmd/nebbet user delete alice
+```
+
+## High-Level Architecture
+
+### The Build Pipeline
+
+1. **Source**: Markdown files in `content/` with YAML frontmatter
+2. **Parse**: Extract frontmatter (title, date, tags, layout, draft flag) and body
+3. **Render**: Convert markdown → HTML using Goldmark with GFM extensions
+4. **Process**: Replace component directives with custom element tags
+5. **Output**: Write HTML to `public/` using layout templates
+6. **Index**: Update two SQLite databases:
+ - **meta.db**: Page metadata for listing/filtering by tag
+ - **search.db**: Full-text search index (FTS5)
+7. **Watch**: Monitor content, templates, components for changes; rebuild with 150ms debounce
+
+### Directory Structure
+
+```
+content/ # Source markdown files (mirrored in public/ as .html)
+ index.md # Becomes /index.html → /
+ admin/ # Skipped from build (admin UI only)
+ posts/ # Blog posts (managed via admin UI)
+
+public/ # Generated static output
+
+templates/ # HTML layout templates
+ base.html # Default page layout
+ admin.html # Admin template (for future)
+
+components/ # JavaScript web components
+ site-greeting.js # Example component
+
+lib/ # JavaScript import map + libraries
+
+styles/ # CSS files (served directly, not copied)
+
+data/ # SQLite databases (created on first build)
+ meta.db # Page metadata
+ search.db # Full-text search index
+```
+
+### Component System
+
+Components are declared as HTML comments in markdown and converted to custom elements at build time:
+
+```markdown
+<!-- component:site-greeting {"name": "visitor"} -->
+```
+
+becomes:
+
+```html
+<site-greeting name="visitor"></site-greeting>
+```
+
+The `components/` directory contains JavaScript modules that define these custom elements. An importmap is auto-generated from `lib/` and injected into each page.
+
+### Frontmatter Spec
+
+Supported frontmatter fields (YAML-style):
+```
+title: Page Title # Required
+date: 2024-03-31 # Optional; ISO date format
+tags: tag1, tag2 # Optional; comma-separated or JSON array
+layout: base # Optional; template name (default: base)
+draft: false # Optional; if true, page is skipped at build time
+```
+
+## Key Modules
+
+### `internal/builder/`
+- **builder.go**: Core orchestrator (BuildAll, BuildFile, Watch, RemovePage)
+- **frontmatter.go**: Parse YAML frontmatter from markdown
+- **markdown.go**: Markdown → HTML conversion (Goldmark + GFM)
+- **components.go**: Component directive processing (comments → custom elements)
+- **importmap.go**: Auto-generate ES module importmap from lib/
+
+### `internal/admin/`
+- **server.go**: HTTP handlers for listing, creating, editing, and deleting posts
+- Uses Basic Auth (htpasswd-compatible bcrypt passwords)
+- Serves form UI for post management
+- Triggers rebuilds via the builder after create/edit/delete
+
+### `internal/auth/`
+Manages password file (compatible with nginx auth_basic):
+- AddUser, DeleteUser, ChangePassword, ListUsers
+- Verify passwords with bcrypt.CompareHashAndPassword
+- File format: `username:$2a$...` (one per line)
+
+### `internal/db/`
+Two SQLite databases:
+
+**meta.db** (PageMeta):
+- Stores path, title, date, tags, html_path for each page
+- Indexed by path and date
+- Used for tag filtering and chronological listing
+
+**search.db** (SearchDB):
+- FTS5 virtual table for full-text search
+- Tokenizer: porter unicode61
+- Stores path, title, searchable plain-text content
+
+## Deployment
+
+### systemd service
+See `nebbet.service` for running the watch daemon as a service. Update SITE_ROOT placeholder.
+
+### nginx configuration
+See `nginx.conf` for:
+- Public site serving from `public/`
+- Admin UI with htpasswd auth (via `.passwords` file)
+- Static assets (styles/, components/, lib/) served directly from source
+- Clean URLs (/about → /about.html)
+
+Replace SITE_ROOT with absolute path to project directory.
+
+## Testing & Debugging
+
+The watch debounce is 150ms. When developing:
+- Markdown changes only rebuild that file (fast)
+- Template/component/lib changes trigger full rebuild
+- Multiple simultaneous markdown changes trigger full rebuild (optimization)
+
+To test the admin UI locally:
+```bash
+go run ./cmd/nebbet serve
+# Create a user first if .passwords doesn't exist
+go run ./cmd/nebbet user add admin
+# Visit http://localhost:8080/admin/
+```
+
+Draft pages (draft: true) are silently skipped during builds but can be tested by removing the draft flag temporarily.
diff --git a/cmd/nebbet/main.go b/cmd/nebbet/main.go
index 1d8b3c0..e7029c6 100644
--- a/cmd/nebbet/main.go
+++ b/cmd/nebbet/main.go
@@ -18,7 +18,7 @@ import (
"os"
"nebbet.no/internal/admin"
- "nebbet.no/internal/auth"
+ "nebbet.no/internal/admin/auth"
"nebbet.no/internal/builder"
"nebbet.no/internal/db"
)
@@ -54,8 +54,6 @@ func main() {
}
}
-// ── build ─────────────────────────────────────────────────────────────────────
-
func cmdBuild(args []string) {
fs := flag.NewFlagSet("build", flag.ExitOnError)
watch := fs.Bool("watch", false, "watch for changes and rebuild")
@@ -77,8 +75,6 @@ func cmdBuild(args []string) {
}
}
-// ── watch ─────────────────────────────────────────────────────────────────────
-
func cmdWatch() {
b := mustBuilder()
defer b.MetaDB.Close()
@@ -93,8 +89,6 @@ func cmdWatch() {
}
}
-// ── serve ─────────────────────────────────────────────────────────────────────
-
func cmdServe(args []string) {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
port := fs.String("port", "8080", "port to listen on")
@@ -117,15 +111,13 @@ func cmdServe(args []string) {
}
}()
- adminSrv := &admin.Server{
- PostsDir: postsDir,
- AuthFile: passwordFile,
- Builder: b,
- }
+ adminSrv := admin.NewServer(postsDir, passwordFile, b)
mux := http.NewServeMux()
// Admin routes — handled dynamically, never from the static output dir.
- mux.Handle("/admin/", http.StripPrefix("/admin", adminSrv))
+ mux.Handle("/admin/", http.StripPrefix("/admin", adminSrv.Engine()))
+ // Serve dependencies (Crepe, etc.)
+ mux.Handle("/lib/", http.StripPrefix("/lib/", http.FileServer(http.Dir("lib"))))
// Everything else — serve the pre-built static files.
mux.Handle("/", http.FileServer(http.Dir(outputDir)))
@@ -188,8 +180,6 @@ func cmdUser(args []string) {
}
}
-// ── helpers ───────────────────────────────────────────────────────────────────
-
func mustBuilder() *builder.Builder {
if err := os.MkdirAll(dataDir, 0755); err != nil {
fmt.Fprintln(os.Stderr, err)
diff --git a/content/admin/index.md b/content/admin/index.md
deleted file mode 100644
index c3badbb..0000000
--- a/content/admin/index.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-title: Dashboard
-layout: admin
-draft: true
----
-
-# Admin Dashboard
-
-Site is served statically. Use the CLI to manage content and users.
-
-```
-nebbet build # one-shot build
-nebbet build --watch # watch mode
-nebbet user add admin
-nebbet user list
-```
diff --git a/docs/superpowers/plans/2026-04-03-gin-migration-plan.md b/docs/superpowers/plans/2026-04-03-gin-migration-plan.md
new file mode 100644
index 0000000..6b95cac
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-03-gin-migration-plan.md
@@ -0,0 +1,739 @@
+# Gin Migration Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Migrate the nebbet admin server from `net/http` to Gin with RESTful routes and middleware-based auth.
+
+**Architecture:** Replace the manual `ServeHTTP` handler with Gin's declarative routing. The `Server` struct gains a `*gin.Engine` field. Auth becomes reusable middleware. Route handlers update their signatures to use `*gin.Context`. Routes move under `/admin` namespace with RESTful patterns (GET/POST for forms, DELETE for removal).
+
+**Tech Stack:** Go, Gin, existing auth/template system (unchanged)
+
+---
+
+## File Structure
+
+**Files to modify:**
+- `go.mod` — Add Gin dependency
+- `internal/admin/server.go` — Core changes: NewServer(), middleware, handler rewrites
+- `cmd/nebbet/main.go` — Update cmdServe() to use NewServer()
+
+---
+
+### Task 1: Add Gin Dependency
+
+**Files:**
+- Modify: `go.mod`
+
+- [ ] **Step 1: Add Gin to go.mod**
+
+Run:
+```bash
+cd /Users/ivarlovlie/p/nebbet.no && go get github.com/gin-gonic/gin
+```
+
+Expected output: Gin module downloaded and added to go.mod
+
+- [ ] **Step 2: Verify go.mod was updated**
+
+Run:
+```bash
+grep "github.com/gin-gonic/gin" go.mod
+```
+
+Expected output: A line like `github.com/gin-gonic/gin v1.X.X` (version may vary)
+
+- [ ] **Step 3: Run go mod tidy**
+
+Run:
+```bash
+go mod tidy
+```
+
+Expected: No errors, go.sum updated
+
+---
+
+### Task 2: Refactor server.go — Add NewServer() Constructor
+
+**Files:**
+- Modify: `internal/admin/server.go` (top section with Server struct)
+
+- [ ] **Step 1: Update Server struct to include Gin engine**
+
+Replace the struct definition:
+
+```go
+// Server is an HTTP handler for the admin post-management UI.
+type Server struct {
+ // PostsDir is the directory where post markdown files are stored,
+ // e.g. "content/posts". It is created on first use if it doesn't exist.
+ PostsDir string
+ // AuthFile is the path to the htpasswd-compatible passwords file.
+ // Authentication is skipped when AuthFile is empty or the file doesn't exist.
+ AuthFile string
+ // Builder is used to rebuild pages after create/edit/delete operations.
+ Builder *builder.Builder
+
+ engine *gin.Engine
+ tmpl *template.Template
+}
+```
+
+- [ ] **Step 2: Add imports for Gin**
+
+Add to the imports section at the top of server.go:
+
+```go
+import (
+ "fmt"
+ "html/template"
+ "net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+
+ "nebbet.no/internal/admin/auth"
+ "nebbet.no/internal/builder"
+)
+```
+
+- [ ] **Step 3: Add NewServer() constructor function**
+
+Add this after the Server struct definition (before any methods):
+
+```go
+// NewServer creates a new admin server with Gin routing and auth middleware.
+func NewServer(postsDir, authFile string, builder *builder.Builder) *Server {
+ s := &Server{
+ PostsDir: postsDir,
+ AuthFile: authFile,
+ Builder: builder,
+ }
+
+ // Initialize Gin engine
+ s.engine = gin.Default()
+
+ // Load templates
+ s.tmpl = mustParseTemplates()
+
+ // Apply auth middleware to all routes
+ s.engine.Use(s.authMiddleware())
+
+ // Register routes under /admin prefix
+ admin := s.engine.Group("/admin")
+ {
+ // List posts
+ admin.GET("/", s.handleList)
+ // Create form and create post
+ admin.GET("/new", s.handleNew)
+ admin.POST("/", s.handleNewPost)
+ // Edit form and update post
+ admin.GET("/:slug", s.handleEdit)
+ admin.POST("/:slug", s.handleEdit)
+ // Delete post
+ admin.DELETE("/:slug", s.handleDelete)
+ }
+
+ return s
+}
+
+// Engine returns the Gin engine for this server.
+func (s *Server) Engine() *gin.Engine {
+ return s.engine
+}
+```
+
+---
+
+### Task 3: Refactor server.go — Add Auth Middleware
+
+**Files:**
+- Modify: `internal/admin/server.go` (replace checkAuth method)
+
+- [ ] **Step 1: Replace checkAuth with middleware function**
+
+Remove the old `checkAuth()` method and add this in its place:
+
+```go
+// authMiddleware returns a Gin middleware that validates Basic Auth credentials.
+func (s *Server) authMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // Skip auth if no auth file is configured
+ if s.AuthFile == "" {
+ c.Next()
+ return
+ }
+
+ // Skip auth if auth file doesn't exist
+ if _, err := os.Stat(s.AuthFile); os.IsNotExist(err) {
+ c.Next()
+ return
+ }
+
+ // Extract Basic Auth credentials
+ username, password, ok := c.Request.BasicAuth()
+ if !ok {
+ c.Header("WWW-Authenticate", `Basic realm="Admin"`)
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
+ return
+ }
+
+ // Verify credentials
+ a := auth.New(s.AuthFile)
+ valid, err := a.Verify(username, password)
+ if err != nil || !valid {
+ c.Header("WWW-Authenticate", `Basic realm="Admin"`)
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
+ return
+ }
+
+ // Auth succeeded, continue
+ c.Next()
+ }
+}
+```
+
+---
+
+### Task 4: Refactor server.go — Remove ServeHTTP and Update Handlers
+
+**Files:**
+- Modify: `internal/admin/server.go` (remove ServeHTTP, update handler signatures)
+
+- [ ] **Step 1: Remove the ServeHTTP method**
+
+Delete the entire `func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)` method (lines 45-70 in original).
+
+- [ ] **Step 2: Update handleList signature and implementation**
+
+Replace:
+```go
+func (s *Server) handleList(w http.ResponseWriter, r *http.Request) {
+ posts, err := s.listPosts()
+ if err != nil {
+ http.Error(w, "Failed to list posts: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ s.render(w, "base", map[string]any{
+ "Title": "Posts",
+ "ContentTemplate": "list-content",
+ "Posts": posts,
+ })
+}
+```
+
+With:
+```go
+func (s *Server) handleList(c *gin.Context) {
+ posts, err := s.listPosts()
+ if err != nil {
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": "Failed to list posts: " + err.Error(),
+ })
+ return
+ }
+ c.HTML(http.StatusOK, "base", gin.H{
+ "Title": "Posts",
+ "ContentTemplate": "list-content",
+ "Posts": posts,
+ })
+}
+```
+
+- [ ] **Step 3: Update handleNew signature and implementation**
+
+Replace:
+```go
+func (s *Server) handleNew(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodPost {
+ s.handleNewPost(w, r)
+ return
+ }
+ s.render(w, "base", map[string]any{
+ "Title": "New Post",
+ "ContentTemplate": "form-content",
+ "Action": "/admin/new",
+ "Post": Post{Date: time.Now().Format("2006-01-02")},
+ "IsNew": true,
+ })
+}
+```
+
+With:
+```go
+func (s *Server) handleNew(c *gin.Context) {
+ c.HTML(http.StatusOK, "base", gin.H{
+ "Title": "New Post",
+ "ContentTemplate": "form-content",
+ "Action": "/admin/new",
+ "Post": Post{Date: time.Now().Format("2006-01-02")},
+ "IsNew": true,
+ })
+}
+```
+
+- [ ] **Step 4: Update handleNewPost signature and implementation**
+
+Replace:
+```go
+func (s *Server) handleNewPost(w http.ResponseWriter, r *http.Request) {
+ p := postFromForm(r)
+ if p.Title == "" {
+ s.renderError(w, "Title is required")
+ return
+ }
+ if p.Slug == "" {
+ p.Slug = slugify(p.Title)
+ }
+
+ mdPath := filepath.Join(s.PostsDir, p.Slug+".md")
+ if err := os.MkdirAll(s.PostsDir, 0755); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if _, err := os.Stat(mdPath); err == nil {
+ s.renderError(w, fmt.Sprintf("Post %q already exists", p.Slug))
+ return
+ }
+ if err := writePostFile(mdPath, p); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ s.rebuild(mdPath)
+ http.Redirect(w, r, "/admin/", http.StatusSeeOther)
+}
+```
+
+With:
+```go
+func (s *Server) handleNewPost(c *gin.Context) {
+ p := postFromForm(c)
+ if p.Title == "" {
+ s.renderError(c, "Title is required")
+ return
+ }
+ if p.Slug == "" {
+ p.Slug = slugify(p.Title)
+ }
+
+ mdPath := filepath.Join(s.PostsDir, p.Slug+".md")
+ if err := os.MkdirAll(s.PostsDir, 0755); err != nil {
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": err.Error(),
+ })
+ return
+ }
+ if _, err := os.Stat(mdPath); err == nil {
+ s.renderError(c, fmt.Sprintf("Post %q already exists", p.Slug))
+ return
+ }
+ if err := writePostFile(mdPath, p); err != nil {
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": err.Error(),
+ })
+ return
+ }
+ s.rebuild(mdPath)
+ c.Redirect(http.StatusSeeOther, "/admin/")
+}
+```
+
+- [ ] **Step 5: Update handleEdit signature and implementation**
+
+Replace:
+```go
+func (s *Server) handleEdit(w http.ResponseWriter, r *http.Request, slug string) {
+ mdPath := filepath.Join(s.PostsDir, slug+".md")
+ p, err := readPostFile(mdPath, slug)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ if r.Method == http.MethodPost {
+ updated := postFromForm(r)
+ updated.Slug = slug // slug is immutable after creation
+ if updated.Title == "" {
+ s.renderError(w, "Title is required")
+ return
+ }
+ if err := writePostFile(mdPath, updated); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ s.rebuild(mdPath)
+ http.Redirect(w, r, "/admin/", http.StatusSeeOther)
+ return
+ }
+
+ s.render(w, "base", map[string]any{
+ "Title": "Edit Post",
+ "ContentTemplate": "form-content",
+ "Action": "/admin/" + slug + "/edit",
+ "Post": p,
+ "IsNew": false,
+ })
+}
+```
+
+With:
+```go
+func (s *Server) handleEdit(c *gin.Context) {
+ slug := c.Param("slug")
+ mdPath := filepath.Join(s.PostsDir, slug+".md")
+ p, err := readPostFile(mdPath, slug)
+ if err != nil {
+ c.HTML(http.StatusNotFound, "base", gin.H{
+ "Title": "Not Found",
+ "ContentTemplate": "error-content",
+ "Message": "Post not found",
+ })
+ return
+ }
+
+ if c.Request.Method == http.MethodPost {
+ updated := postFromForm(c)
+ updated.Slug = slug // slug is immutable after creation
+ if updated.Title == "" {
+ s.renderError(c, "Title is required")
+ return
+ }
+ if err := writePostFile(mdPath, updated); err != nil {
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": err.Error(),
+ })
+ return
+ }
+ s.rebuild(mdPath)
+ c.Redirect(http.StatusSeeOther, "/admin/")
+ return
+ }
+
+ c.HTML(http.StatusOK, "base", gin.H{
+ "Title": "Edit Post",
+ "ContentTemplate": "form-content",
+ "Action": "/admin/" + slug,
+ "Post": p,
+ "IsNew": false,
+ })
+}
+```
+
+- [ ] **Step 6: Update handleDelete signature and implementation**
+
+Replace:
+```go
+func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, slug string) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ mdPath := filepath.Join(s.PostsDir, slug+".md")
+ if err := os.Remove(mdPath); err != nil && !os.IsNotExist(err) {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if s.Builder != nil {
+ _ = s.Builder.RemovePage(mdPath)
+ }
+ http.Redirect(w, r, "/admin/", http.StatusSeeOther)
+}
+```
+
+With:
+```go
+func (s *Server) handleDelete(c *gin.Context) {
+ slug := c.Param("slug")
+ mdPath := filepath.Join(s.PostsDir, slug+".md")
+ if err := os.Remove(mdPath); err != nil && !os.IsNotExist(err) {
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": err.Error(),
+ })
+ return
+ }
+ if s.Builder != nil {
+ _ = s.Builder.RemovePage(mdPath)
+ }
+ c.Redirect(http.StatusSeeOther, "/admin/")
+}
+```
+
+---
+
+### Task 5: Refactor server.go — Update Helper Functions
+
+**Files:**
+- Modify: `internal/admin/server.go` (helper functions section)
+
+- [ ] **Step 1: Update render() helper**
+
+Replace:
+```go
+func (s *Server) render(w http.ResponseWriter, name string, data map[string]any) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err := s.tmpl.ExecuteTemplate(w, name, data); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+```
+
+With:
+```go
+func (s *Server) render(c *gin.Context, name string, data gin.H) {
+ if err := s.tmpl.ExecuteTemplate(c.Writer, name, data); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ }
+}
+```
+
+- [ ] **Step 2: Update renderError() helper**
+
+Replace:
+```go
+func (s *Server) renderError(w http.ResponseWriter, msg string) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusBadRequest)
+ _ = s.tmpl.ExecuteTemplate(w, "base", map[string]any{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": msg,
+ })
+}
+```
+
+With:
+```go
+func (s *Server) renderError(c *gin.Context, msg string) {
+ c.HTML(http.StatusBadRequest, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": msg,
+ })
+}
+```
+
+- [ ] **Step 3: Update postFromForm() to accept *gin.Context**
+
+Replace:
+```go
+// postFromForm reads a post from an HTTP form submission.
+func postFromForm(r *http.Request) Post {
+ _ = r.ParseForm()
+ return Post{
+ Title: strings.TrimSpace(r.FormValue("title")),
+ Date: strings.TrimSpace(r.FormValue("date")),
+ Tags: strings.TrimSpace(r.FormValue("tags")),
+ Content: r.FormValue("content"),
+ }
+}
+```
+
+With:
+```go
+// postFromForm reads a post from an HTTP form submission.
+func postFromForm(c *gin.Context) Post {
+ return Post{
+ Title: strings.TrimSpace(c.PostForm("title")),
+ Date: strings.TrimSpace(c.PostForm("date")),
+ Tags: strings.TrimSpace(c.PostForm("tags")),
+ Content: c.PostForm("content"),
+ }
+}
+```
+
+---
+
+### Task 6: Update cmd/nebbet/main.go
+
+**Files:**
+- Modify: `cmd/nebbet/main.go` (cmdServe function)
+
+- [ ] **Step 1: Update import to include Gin**
+
+Add `"github.com/gin-gonic/gin"` to the imports section:
+
+```go
+import (
+ "flag"
+ "fmt"
+ "net/http"
+ "os"
+
+ "github.com/gin-gonic/gin"
+
+ "nebbet.no/internal/admin"
+ "nebbet.no/internal/admin/auth"
+ "nebbet.no/internal/builder"
+ "nebbet.no/internal/db"
+)
+```
+
+- [ ] **Step 2: Rewrite cmdServe() function**
+
+Replace the entire `cmdServe()` function with:
+
+```go
+func cmdServe(args []string) {
+ fs := flag.NewFlagSet("serve", flag.ExitOnError)
+ port := fs.String("port", "8080", "port to listen on")
+ _ = fs.Parse(args)
+
+ b := mustBuilder()
+ defer b.MetaDB.Close()
+ defer b.SearchDB.Close()
+
+ // Initial build.
+ if err := b.BuildAll(); err != nil {
+ fmt.Fprintln(os.Stderr, "build error:", err)
+ os.Exit(1)
+ }
+
+ // Watch in background.
+ go func() {
+ if err := b.Watch(); err != nil {
+ fmt.Fprintln(os.Stderr, "watch error:", err)
+ }
+ }()
+
+ // Create admin server with Gin
+ adminSrv := admin.NewServer(postsDir, passwordFile, b)
+ engine := adminSrv.Engine()
+
+ // Serve dependencies (lib/)
+ engine.Static("/lib", "lib")
+
+ // Serve static site from public/
+ engine.Static("/", outputDir)
+
+ addr := ":" + *port
+ fmt.Printf("listening on http://localhost%s\n", addr)
+ fmt.Printf(" public site: http://localhost%s/\n", addr)
+ fmt.Printf(" admin UI: http://localhost%s/admin/\n", addr)
+ if err := engine.Run(addr); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+```
+
+---
+
+### Task 7: Test the Migration
+
+**Files:**
+- Test: Manual testing (no automated tests needed for this migration)
+
+- [ ] **Step 1: Build the project**
+
+Run:
+```bash
+cd /Users/ivarlovlie/p/nebbet.no && go build ./cmd/nebbet
+```
+
+Expected: Binary builds without errors
+
+- [ ] **Step 2: Start the server**
+
+Run:
+```bash
+go run ./cmd/nebbet serve --port 8080
+```
+
+Expected output:
+```
+listening on http://localhost:8080
+ public site: http://localhost:8080/
+ admin UI: http://localhost:8080/admin/
+```
+
+- [ ] **Step 3: Test list posts endpoint**
+
+Open browser: `http://localhost:8080/admin/`
+
+Expected: Posts list page loads (may show empty if no posts)
+
+- [ ] **Step 4: Create a new post**
+
+- Navigate to `http://localhost:8080/admin/new`
+- Fill in: Title, Date, Tags, Content
+- Click "Create" (or submit button)
+
+Expected: Redirects to `/admin/`, new post appears in list
+
+- [ ] **Step 5: Edit a post**
+
+- Click edit button on a post in the list
+- Modify a field (e.g., title)
+- Submit
+
+Expected: Redirects to `/admin/`, post shows updated content
+
+- [ ] **Step 6: Delete a post**
+
+- On the edit page, click delete button
+- Confirm deletion
+
+Expected: Redirects to `/admin/`, post no longer in list
+
+- [ ] **Step 7: Test static file serving**
+
+- Navigate to `http://localhost:8080/`
+- Check that public site loads
+- Navigate to `http://localhost:8080/lib/` to verify lib/ is served
+
+Expected: Both static file paths work
+
+- [ ] **Step 8: Test auth**
+
+- Stop the server (Ctrl+C)
+- In another terminal, create a user if needed:
+
+```bash
+go run ./cmd/nebbet user add testuser
+# Enter a password when prompted
+```
+
+- Start the server again
+- Visit `http://localhost:8080/admin/`
+- When prompted for auth, enter invalid credentials
+
+Expected: 401 Unauthorized error
+
+- Enter valid credentials (testuser + password)
+
+Expected: Admin page loads
+
+---
+
+## Self-Review Checklist
+
+✓ **Spec coverage:**
+- Route migration (Task 2 NewServer) ✓
+- Auth middleware (Task 3) ✓
+- Handler refactoring (Task 4) ✓
+- Helper functions (Task 5) ✓
+- CLI integration (Task 6) ✓
+- Testing (Task 7) ✓
+
+✓ **Placeholder scan:** No TBDs, all code is complete and specific
+
+✓ **Type consistency:**
+- Handler signatures consistently `(c *gin.Context)`
+- Form helpers use `postFromForm(c)`
+- Template rendering uses `c.HTML()`
+- No mismatched method names
+
+✓ **Function signatures match:** All handlers in NewServer() route definitions match their actual signatures
diff --git a/docs/superpowers/specs/2026-04-03-gin-migration-design.md b/docs/superpowers/specs/2026-04-03-gin-migration-design.md
new file mode 100644
index 0000000..41163a3
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-03-gin-migration-design.md
@@ -0,0 +1,198 @@
+---
+name: Gin Migration Design
+description: Migrate admin server from net/http to Gin framework with RESTful routes and middleware-based auth
+type: implementation
+date: 2026-04-03
+---
+
+# Gin Migration Design
+
+## Overview
+
+Migrate the nebbet admin server from Go's `net/http` package to the Gin web framework. The migration will:
+- Replace manual route handling with Gin's declarative routing
+- Reorganize routes to follow RESTful conventions
+- Extract auth into reusable middleware
+- Preserve all existing functionality (auth, template rendering, post management)
+- Keep UI behavior identical from the user's perspective
+
+## Goals
+
+1. **Cleaner routing:** Replace manual `ServeHTTP` switch with Gin's route declarations
+2. **Better framework:** Leverage Gin for middleware, error handling, and future extensibility
+3. **RESTful design:** Modernize route structure while keeping templates server-rendered
+
+## Non-Goals
+
+- Rewriting the frontend to a JS framework
+- Adding new features beyond the migration
+- Changing auth format or password file structure
+- Changing how posts are stored or built
+
+## Architecture
+
+### Current State
+
+The `admin.Server` implements `http.Handler` with manual routing in `ServeHTTP()`:
+```
+GET/POST /admin → list posts
+GET/POST /admin/new → create form / create post
+GET/POST /admin/{slug}/edit → edit form / update post
+POST /admin/{slug}/delete → delete post
+```
+
+Auth is checked manually via `checkAuth()` for every request.
+
+### Target State
+
+Replace with Gin routing and middleware:
+- `Server` struct holds a `*gin.Engine`
+- `NewServer()` initializes Gin with routes and middleware
+- Auth middleware wraps all admin routes
+- Handlers call `gin.Context` instead of `http.ResponseWriter`
+
+**New route structure (RESTful, under `/admin` namespace):**
+```
+GET /admin/ → list posts
+GET /admin/new → create form
+POST /admin/ → create post
+GET /admin/:slug → edit form
+POST /admin/:slug → update post
+DELETE /admin/:slug → delete post
+```
+
+## Implementation Details
+
+### File: `internal/admin/server.go`
+
+#### Server Struct
+- Add field: `engine *gin.Engine`
+- Keep existing fields: `PostsDir`, `AuthFile`, `Builder`, `tmpl`
+
+#### Constructor: `NewServer(postsDir, authFile string, builder *builder.Builder) *Server`
+- Create and configure Gin engine
+- Register middleware (auth)
+- Register all routes
+- Return `*Server`
+
+#### Auth Middleware
+- Extracted from `checkAuth()` into a middleware function
+- Signature: `func (s *Server) authMiddleware() gin.HandlerFunc`
+- Logic:
+ - Skip auth if `AuthFile` is empty or doesn't exist
+ - Extract Basic Auth credentials
+ - Call `auth.Verify(username, password)`
+ - Send 401 + `WWW-Authenticate` header on failure
+ - Call `c.Next()` to proceed on success
+
+#### Route Handlers
+Keep existing handler functions, update signatures:
+- `handleList(c *gin.Context)` — render post list
+- `handleNew(c *gin.Context)` — GET shows form, POST creates post
+- `handleNewPost(c *gin.Context)` — handle POST /new (merge with `handleNew` or keep separate)
+- `handleEdit(c *gin.Context)` — GET shows form, POST updates post
+- `handleDelete(c *gin.Context)` — DELETE removes post
+
+For GET requests that show forms, continue using `c.HTML()` with the template and data map.
+For POST requests, validate form data, write to disk, rebuild, and redirect to `/`.
+
+#### Helper Functions
+Keep existing:
+- `listPosts()` — read `.md` files from PostsDir
+- `render(c *gin.Context, name string, data map[string]any)` — render template (adapt to use `c.HTML()`)
+- `renderError(c *gin.Context, msg string)` — render error template
+- `postFromForm(c *gin.Context) Post` — extract form values from `c.PostForm()`
+- `readPostFile()`, `writePostFile()`, `slugify()` — unchanged
+- `mustParseTemplates()` — unchanged
+
+### File: `cmd/nebbet/main.go`
+
+#### Changes in `cmdServe()`
+Instead of:
+```go
+adminSrv := &admin.Server{...}
+mux := http.NewServeMux()
+mux.Handle("/admin/", http.StripPrefix("/admin", adminSrv))
+mux.Handle("/lib/", ...)
+mux.Handle("/", ...)
+http.ListenAndServe(addr, mux)
+```
+
+Change to:
+```go
+adminSrv := admin.NewServer(postsDir, passwordFile, b)
+engine := adminSrv.Engine() // or keep internal, add to main router
+
+// Either:
+// 1. Use adminSrv.Engine directly (if it handles all routes)
+// 2. Create a main Gin router and nest adminSrv's routes
+
+// Handle /lib/ and static files as Gin routes or separate handlers
+```
+
+Exact approach depends on whether we keep `/lib` and public site serving separate or consolidate into one Gin router. **Recommended:** single Gin router for consistency.
+
+## Route Mapping
+
+| Old Route | New Route | Method | Action |
+|-----------|-----------|--------|--------|
+| `/admin/` | `/admin/` | GET | List posts |
+| `/admin/new` | `/admin/new` | GET | Show create form |
+| `/admin/new` | `/admin/` | POST | Create post |
+| `/admin/{slug}/edit` | `/admin/{slug}` | GET | Show edit form |
+| `/admin/{slug}/edit` | `/admin/{slug}` | POST | Update post |
+| `/admin/{slug}/delete` | `/admin/{slug}` | DELETE | Delete post |
+
+**HTML Form Handling:** Standard HTML forms only support GET/POST. For DELETE:
+- **Option A:** Keep POST with custom handling (check method override or hidden field)
+- **Option B:** Use POST `/admin/{slug}?method=delete` and parse in handler
+
+Recommended: keep as POST for form compatibility, or use POST with Gin's `c.PostForm("_method")` check.
+
+## Dependencies
+
+- Add `github.com/gin-gonic/gin` to `go.mod`
+- No changes to other dependencies
+
+## Backwards Compatibility
+
+- **URLs:** Admin routes change (listed above). Bookmarks will break, but it's an internal admin interface.
+- **Auth:** No changes. Password file format and Basic Auth remain the same.
+- **Posts:** No changes. Markdown files, frontmatter, build process unchanged.
+- **Public site:** No changes. Static files served the same way.
+
+## Testing Strategy
+
+- **Manual:** Create/edit/delete posts via the admin UI, verify rebuilds work
+- **Auth:** Test Basic Auth with valid/invalid credentials
+- **Forms:** Verify all form fields (title, date, tags, content) are captured and saved correctly
+- **Errors:** Verify error handling for missing posts, invalid input, file write failures
+
+## Implementation Order
+
+1. Add Gin dependency to `go.mod`
+2. Create `NewServer()` constructor and auth middleware in `server.go`
+3. Register routes in `NewServer()`
+4. Convert handler signatures to `*gin.Context`
+5. Update `render()` and helper functions to work with Gin
+6. Update `cmd/nebbet/main.go` to use `NewServer()`
+7. Verify all routes work locally
+8. Test admin UI end-to-end
+
+## Risks & Mitigation
+
+| Risk | Mitigation |
+|------|-----------|
+| Breaking the admin UI during migration | Test each route in a browser after updating |
+| Form submission issues (e.g., multipart/form-data) | Use `c.PostForm()` and test file uploads if used |
+| Auth middleware interfering with static files | Apply middleware only to admin routes, not `/lib` or `/` |
+| Template rendering errors | Keep template loading and execution the same |
+
+## Success Criteria
+
+- ✓ All admin routes work with Gin
+- ✓ Auth middleware blocks unauthorized access
+- ✓ Create/edit/delete posts works end-to-end
+- ✓ Posts rebuild after create/edit/delete
+- ✓ No changes to password file format or post storage
+- ✓ Static file serving (`/lib`, public site) unchanged
diff --git a/internal/auth/auth.go b/internal/admin/auth/auth.go
index b0de7d9..b0de7d9 100644
--- a/internal/auth/auth.go
+++ b/internal/admin/auth/auth.go
diff --git a/internal/admin/server.go b/internal/admin/server.go
index 410560f..33413ee 100644
--- a/internal/admin/server.go
+++ b/internal/admin/server.go
@@ -122,37 +122,37 @@ func (s *Server) authMiddleware() gin.HandlerFunc {
// ── Handlers ─────────────────────────────────────────────────────────────────
-func (s *Server) handleList(w http.ResponseWriter, r *http.Request) {
+func (s *Server) handleList(c *gin.Context) {
posts, err := s.listPosts()
if err != nil {
- http.Error(w, "Failed to list posts: "+err.Error(), http.StatusInternalServerError)
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": "Failed to list posts: " + err.Error(),
+ })
return
}
- s.render(w, "base", map[string]any{
- "Title": "Posts",
- "ContentTemplate": "list-content",
- "Posts": posts,
+ c.HTML(http.StatusOK, "base", gin.H{
+ "Title": "Posts",
+ "ContentTemplate": "list-content",
+ "Posts": posts,
})
}
-func (s *Server) handleNew(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- s.handleNewPost(w, r)
- return
- }
- s.render(w, "base", map[string]any{
- "Title": "New Post",
- "ContentTemplate": "form-content",
- "Action": "/admin/new",
- "Post": Post{Date: time.Now().Format("2006-01-02")},
- "IsNew": true,
+func (s *Server) handleNew(c *gin.Context) {
+ c.HTML(http.StatusOK, "base", gin.H{
+ "Title": "New Post",
+ "ContentTemplate": "form-content",
+ "Action": "/admin/new",
+ "Post": Post{Date: time.Now().Format("2006-01-02")},
+ "IsNew": true,
})
}
-func (s *Server) handleNewPost(w http.ResponseWriter, r *http.Request) {
- p := postFromForm(r)
+func (s *Server) handleNewPost(c *gin.Context) {
+ p := postFromForm(c)
if p.Title == "" {
- s.renderError(w, "Title is required")
+ s.renderError(c, "Title is required")
return
}
if p.Slug == "" {
@@ -161,67 +161,86 @@ func (s *Server) handleNewPost(w http.ResponseWriter, r *http.Request) {
mdPath := filepath.Join(s.PostsDir, p.Slug+".md")
if err := os.MkdirAll(s.PostsDir, 0755); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": err.Error(),
+ })
return
}
if _, err := os.Stat(mdPath); err == nil {
- s.renderError(w, fmt.Sprintf("Post %q already exists", p.Slug))
+ s.renderError(c, fmt.Sprintf("Post %q already exists", p.Slug))
return
}
if err := writePostFile(mdPath, p); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": err.Error(),
+ })
return
}
s.rebuild(mdPath)
- http.Redirect(w, r, "/admin/", http.StatusSeeOther)
+ c.Redirect(http.StatusSeeOther, "/admin/")
}
-func (s *Server) handleEdit(w http.ResponseWriter, r *http.Request, slug string) {
+func (s *Server) handleEdit(c *gin.Context) {
+ slug := c.Param("slug")
mdPath := filepath.Join(s.PostsDir, slug+".md")
p, err := readPostFile(mdPath, slug)
if err != nil {
- http.NotFound(w, r)
+ c.HTML(http.StatusNotFound, "base", gin.H{
+ "Title": "Not Found",
+ "ContentTemplate": "error-content",
+ "Message": "Post not found",
+ })
return
}
- if r.Method == http.MethodPost {
- updated := postFromForm(r)
+
+ if c.Request.Method == http.MethodPost {
+ updated := postFromForm(c)
updated.Slug = slug // slug is immutable after creation
if updated.Title == "" {
- s.renderError(w, "Title is required")
+ s.renderError(c, "Title is required")
return
}
if err := writePostFile(mdPath, updated); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": err.Error(),
+ })
return
}
s.rebuild(mdPath)
- http.Redirect(w, r, "/admin/", http.StatusSeeOther)
+ c.Redirect(http.StatusSeeOther, "/admin/")
return
}
- s.render(w, "base", map[string]any{
- "Title": "Edit Post",
- "ContentTemplate": "form-content",
- "Action": "/admin/" + slug + "/edit",
- "Post": p,
- "IsNew": false,
+ c.HTML(http.StatusOK, "base", gin.H{
+ "Title": "Edit Post",
+ "ContentTemplate": "form-content",
+ "Action": "/admin/" + slug,
+ "Post": p,
+ "IsNew": false,
})
}
-func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request, slug string) {
- if r.Method != http.MethodPost {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
+func (s *Server) handleDelete(c *gin.Context) {
+ slug := c.Param("slug")
mdPath := filepath.Join(s.PostsDir, slug+".md")
if err := os.Remove(mdPath); err != nil && !os.IsNotExist(err) {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ c.HTML(http.StatusInternalServerError, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": err.Error(),
+ })
return
}
if s.Builder != nil {
_ = s.Builder.RemovePage(mdPath)
}
- http.Redirect(w, r, "/admin/", http.StatusSeeOther)
+ c.Redirect(http.StatusSeeOther, "/admin/")
}
func (s *Server) rebuild(mdPath string) {
@@ -254,31 +273,21 @@ func (s *Server) listPosts() ([]Post, error) {
return posts, nil
}
-func (s *Server) render(w http.ResponseWriter, name string, data map[string]any) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- if err := s.tmpl.ExecuteTemplate(w, name, data); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
-}
-
-func (s *Server) renderError(w http.ResponseWriter, msg string) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.WriteHeader(http.StatusBadRequest)
- _ = s.tmpl.ExecuteTemplate(w, "base", map[string]any{
- "Title": "Error",
- "ContentTemplate": "error-content",
- "Message": msg,
+func (s *Server) renderError(c *gin.Context, msg string) {
+ c.HTML(http.StatusBadRequest, "base", gin.H{
+ "Title": "Error",
+ "ContentTemplate": "error-content",
+ "Message": msg,
})
}
// postFromForm reads a post from an HTTP form submission.
-func postFromForm(r *http.Request) Post {
- _ = r.ParseForm()
+func postFromForm(c *gin.Context) Post {
return Post{
- Title: strings.TrimSpace(r.FormValue("title")),
- Date: strings.TrimSpace(r.FormValue("date")),
- Tags: strings.TrimSpace(r.FormValue("tags")),
- Content: r.FormValue("content"),
+ Title: strings.TrimSpace(c.PostForm("title")),
+ Date: strings.TrimSpace(c.PostForm("date")),
+ Tags: strings.TrimSpace(c.PostForm("tags")),
+ Content: c.PostForm("content"),
}
}
diff --git a/internal/db/meta.go b/internal/db/meta.go
index 33e0da3..dd05764 100644
--- a/internal/db/meta.go
+++ b/internal/db/meta.go
@@ -23,7 +23,7 @@ type PageMeta struct {
}
func OpenMeta(path string) (*MetaDB, error) {
- db, err := sql.Open("sqlite3", path)
+ db, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
diff --git a/internal/db/search.go b/internal/db/search.go
index 545645e..b9990a5 100644
--- a/internal/db/search.go
+++ b/internal/db/search.go
@@ -23,7 +23,7 @@ type SearchResult struct {
}
func OpenSearch(path string) (*SearchDB, error) {
- db, err := sql.Open("sqlite3", path)
+ db, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
diff --git a/lib/bun.lock b/lib/bun.lock
new file mode 100644
index 0000000..f6de9d1
--- /dev/null
+++ b/lib/bun.lock
@@ -0,0 +1,422 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "dependencies": {
+ "@milkdown/crepe": "^7.20.0",
+ },
+ },
+ },
+ "packages": {
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="],
+
+ "@codemirror/commands": ["@codemirror/commands@6.10.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q=="],
+
+ "@codemirror/lang-angular": ["@codemirror/lang-angular@0.1.4", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.3" } }, "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g=="],
+
+ "@codemirror/lang-cpp": ["@codemirror/lang-cpp@6.0.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/cpp": "^1.0.0" } }, "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA=="],
+
+ "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="],
+
+ "@codemirror/lang-go": ["@codemirror/lang-go@6.0.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/go": "^1.0.0" } }, "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg=="],
+
+ "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="],
+
+ "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="],
+
+ "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="],
+
+ "@codemirror/lang-jinja": ["@codemirror/lang-jinja@6.0.0", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" } }, "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw=="],
+
+ "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="],
+
+ "@codemirror/lang-less": ["@codemirror/lang-less@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ=="],
+
+ "@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw=="],
+
+ "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="],
+
+ "@codemirror/lang-php": ["@codemirror/lang-php@6.0.2", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/php": "^1.0.0" } }, "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA=="],
+
+ "@codemirror/lang-python": ["@codemirror/lang-python@6.2.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.3.2", "@codemirror/language": "^6.8.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/python": "^1.1.4" } }, "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw=="],
+
+ "@codemirror/lang-rust": ["@codemirror/lang-rust@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/rust": "^1.0.0" } }, "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA=="],
+
+ "@codemirror/lang-sass": ["@codemirror/lang-sass@6.0.2", "", { "dependencies": { "@codemirror/lang-css": "^6.2.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/sass": "^1.0.0" } }, "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q=="],
+
+ "@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="],
+
+ "@codemirror/lang-vue": ["@codemirror/lang-vue@0.1.3", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug=="],
+
+ "@codemirror/lang-wast": ["@codemirror/lang-wast@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q=="],
+
+ "@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="],
+
+ "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.3", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ=="],
+
+ "@codemirror/language": ["@codemirror/language@6.12.3", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA=="],
+
+ "@codemirror/language-data": ["@codemirror/language-data@6.5.2", "", { "dependencies": { "@codemirror/lang-angular": "^0.1.0", "@codemirror/lang-cpp": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-go": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-less": "^6.0.0", "@codemirror/lang-liquid": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", "@codemirror/lang-php": "^6.0.0", "@codemirror/lang-python": "^6.0.0", "@codemirror/lang-rust": "^6.0.0", "@codemirror/lang-sass": "^6.0.0", "@codemirror/lang-sql": "^6.0.0", "@codemirror/lang-vue": "^0.1.1", "@codemirror/lang-wast": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.4.0" } }, "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg=="],
+
+ "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="],
+
+ "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="],
+
+ "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
+
+ "@codemirror/state": ["@codemirror/state@6.6.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ=="],
+
+ "@codemirror/theme-one-dark": ["@codemirror/theme-one-dark@6.1.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA=="],
+
+ "@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
+
+ "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
+
+ "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
+
+ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="],
+
+ "@lezer/cpp": ["@lezer/cpp@1.1.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw=="],
+
+ "@lezer/css": ["@lezer/css@1.3.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg=="],
+
+ "@lezer/go": ["@lezer/go@1.0.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ=="],
+
+ "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
+
+ "@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="],
+
+ "@lezer/java": ["@lezer/java@1.1.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw=="],
+
+ "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="],
+
+ "@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="],
+
+ "@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="],
+
+ "@lezer/markdown": ["@lezer/markdown@1.6.3", "", { "dependencies": { "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw=="],
+
+ "@lezer/php": ["@lezer/php@1.0.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.1.0" } }, "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA=="],
+
+ "@lezer/python": ["@lezer/python@1.1.18", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg=="],
+
+ "@lezer/rust": ["@lezer/rust@1.0.2", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg=="],
+
+ "@lezer/sass": ["@lezer/sass@1.1.0", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ=="],
+
+ "@lezer/xml": ["@lezer/xml@1.0.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="],
+
+ "@lezer/yaml": ["@lezer/yaml@1.0.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw=="],
+
+ "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
+
+ "@milkdown/components": ["@milkdown/components@7.20.0", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/exception": "7.20.0", "@milkdown/plugin-tooltip": "7.20.0", "@milkdown/preset-commonmark": "7.20.0", "@milkdown/preset-gfm": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/transformer": "7.20.0", "@milkdown/utils": "7.20.0", "@types/lodash-es": "^4.17.12", "clsx": "^2.0.0", "dompurify": "^3.2.5", "lodash-es": "^4.17.21", "nanoid": "^5.0.9", "unist-util-visit": "^5.0.0", "vue": "^3.5.20" }, "peerDependencies": { "@codemirror/language": "^6", "@codemirror/state": "^6", "@codemirror/view": "^6" } }, "sha512-Qn91/oZugGjf17ARE51nbEsH4YklZQaomRSsfxOAtIcwGEJe5osq+zhhKGtgAYFfUb6rU3W86Pe4XDlXN6vFjg=="],
+
+ "@milkdown/core": ["@milkdown/core@7.20.0", "", { "dependencies": { "@milkdown/ctx": "7.20.0", "@milkdown/exception": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/transformer": "7.20.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.3" } }, "sha512-X9LaUcIR4Y2oiY2J5tslavlPVOwIB3X8/9z1bOeBjlIPtr+urbkY7YEX86EeLV9LyRQ3+t+jXaLMCIjW1wsV6w=="],
+
+ "@milkdown/crepe": ["@milkdown/crepe@7.20.0", "", { "dependencies": { "@codemirror/commands": "^6.2.4", "@codemirror/language": "^6.10.1", "@codemirror/language-data": "^6.3.1", "@codemirror/state": "^6.4.1", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.16.0", "@milkdown/kit": "7.20.0", "@types/lodash-es": "^4.17.12", "clsx": "^2.0.0", "codemirror": "^6.0.1", "katex": "^0.16.0", "lodash-es": "^4.17.21", "prosemirror-virtual-cursor": "^0.4.2", "remark-math": "^6.0.0", "unist-util-visit": "^5.0.0", "vue": "^3.5.20" } }, "sha512-KT+oFF6Q7mI41z01c9v/wUUCyQ2f908TgOsa6mwi25yuxnxQxISZFCjRvlh0sc9p9D3CrMeuJWGCN6DialQdig=="],
+
+ "@milkdown/ctx": ["@milkdown/ctx@7.20.0", "", { "dependencies": { "@milkdown/exception": "7.20.0" } }, "sha512-LUK4xdsUngY2xCCvPTyHPifjAknJ5rE6VBjgsP+LySIUKeFUrhqzo/zz2vaOODKzm3DBMIhpZAoW3MAqxoMGIQ=="],
+
+ "@milkdown/exception": ["@milkdown/exception@7.20.0", "", {}, "sha512-u8EL7rbqgrWrPpkDhrxUYXauw2DO52JUQmuokrUZvqezmflo7pgIDCr+Rk6AQslzB4Xw+n9eYik5rXX3RXC7Qg=="],
+
+ "@milkdown/kit": ["@milkdown/kit@7.20.0", "", { "dependencies": { "@milkdown/components": "7.20.0", "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/plugin-block": "7.20.0", "@milkdown/plugin-clipboard": "7.20.0", "@milkdown/plugin-cursor": "7.20.0", "@milkdown/plugin-history": "7.20.0", "@milkdown/plugin-indent": "7.20.0", "@milkdown/plugin-listener": "7.20.0", "@milkdown/plugin-slash": "7.20.0", "@milkdown/plugin-tooltip": "7.20.0", "@milkdown/plugin-trailing": "7.20.0", "@milkdown/plugin-upload": "7.20.0", "@milkdown/preset-commonmark": "7.20.0", "@milkdown/preset-gfm": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/transformer": "7.20.0", "@milkdown/utils": "7.20.0" } }, "sha512-X74KMa0tcDAAMOE9aFtBRN+RCdD/HMXor5YN18e7d0pe4a65MGFklUGlcg1U6zEfeMMYeC3msNvMKLMwk3O5RA=="],
+
+ "@milkdown/plugin-block": ["@milkdown/plugin-block@7.20.0", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0", "@types/lodash-es": "^4.17.12", "lodash-es": "^4.17.21" } }, "sha512-jIXfzJ8Zje+6+9ZwQuVmNeYE8KfzqL9YJ/YdMvWQIEiKhy2x9pZMAkkufgmUlq1aouxOV+gk5fX+ovxzEzfSrA=="],
+
+ "@milkdown/plugin-clipboard": ["@milkdown/plugin-clipboard@7.20.0", "", { "dependencies": { "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0" } }, "sha512-PyokNvwgWcO6/I/0LxDRnATpnxvs5upFRlp6eO8PhjwBFZftCIU6D15Wg4JAxwW7Y0NyTWfViWjc9TwiBd6KOQ=="],
+
+ "@milkdown/plugin-cursor": ["@milkdown/plugin-cursor@7.20.0", "", { "dependencies": { "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0", "prosemirror-drop-indicator": "^0.1.0" } }, "sha512-goCPwUARBzGV6Hvnr3P57Bj5TnyFjYIfDFLvgWTIlsm/dR2Wr4Syy4HDOtaKO9YL/VtZ8gtiZVgeo0vhc4CzMA=="],
+
+ "@milkdown/plugin-history": ["@milkdown/plugin-history@7.20.0", "", { "dependencies": { "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0" } }, "sha512-lqOYQBrxKj4px/i0Cav3zRkCArwnkv8o7fGMh3NtnUXMLSE7/xojK5GFPS4EaS/UKK7/+i1oV2+HRA6+6Ezy7w=="],
+
+ "@milkdown/plugin-indent": ["@milkdown/plugin-indent@7.20.0", "", { "dependencies": { "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0" } }, "sha512-KfdIztQMuHv4Rx1JmSQe2vooN4+Zm7MhjQkNolGyiI7BPZbu855hVIC/s96x3Dk04tkbb+M/i9MJhxCazxfd6Q=="],
+
+ "@milkdown/plugin-listener": ["@milkdown/plugin-listener@7.20.0", "", { "dependencies": { "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@types/lodash-es": "^4.17.12", "lodash-es": "^4.17.21" } }, "sha512-Sj+B63JfM3NVVS3uGXTPkoz8xx8MQYrR28pI9AaqX5q60tvCvOJw9E1ODvSsBEjeqnN4kablDthIugLlBhOlwQ=="],
+
+ "@milkdown/plugin-slash": ["@milkdown/plugin-slash@7.20.0", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0", "@types/lodash-es": "^4.17.12", "lodash-es": "^4.17.21" } }, "sha512-Qm3/ZxkGYd5XN+J/X91lGGu7SBzuQBOTOLjuJdg4qDBZmdEHlGojB+5BhCSAMB3WGyCpQQGbSqKOelUrXtj68w=="],
+
+ "@milkdown/plugin-tooltip": ["@milkdown/plugin-tooltip@7.20.0", "", { "dependencies": { "@floating-ui/dom": "^1.5.1", "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0", "@types/lodash-es": "^4.17.12", "lodash-es": "^4.17.21" } }, "sha512-BVaXorpmA6ZAS3+xv0rgrtjV1h2K39G5Z9Wun4RxT1YXJTTbzIuFQ3hwBAGLjLMwTsosp7YhRLaMJJAC0jEY5Q=="],
+
+ "@milkdown/plugin-trailing": ["@milkdown/plugin-trailing@7.20.0", "", { "dependencies": { "@milkdown/ctx": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0" } }, "sha512-AxDeMSAZfj0Er7RYLvLRf6FKdQtLVmotxML6Se6zgqIa++bFeIXCU22/FC+9r/6d1eUtraTva9ez5K2qPy8qig=="],
+
+ "@milkdown/plugin-upload": ["@milkdown/plugin-upload@7.20.0", "", { "dependencies": { "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/exception": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/utils": "7.20.0" } }, "sha512-g3UQrD2zfpm86r3BcBDfOdEAyQHhay1nf5wUQgNf4zn6IgRttfEF8tosQsL1B/WBnZB05hH5scLWo4DR2bFhUw=="],
+
+ "@milkdown/preset-commonmark": ["@milkdown/preset-commonmark@7.20.0", "", { "dependencies": { "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/exception": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/transformer": "7.20.0", "@milkdown/utils": "7.20.0", "remark-inline-links": "^7.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-+mPcONXfdjaXdx8JMxDIOT3PWHfy5vewK8iY8j8bUiYD8Iw7YfyWTUh9JHOf4vmOpiKGCJd7+iz7e93u95bQRw=="],
+
+ "@milkdown/preset-gfm": ["@milkdown/preset-gfm@7.20.0", "", { "dependencies": { "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/exception": "7.20.0", "@milkdown/preset-commonmark": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/transformer": "7.20.0", "@milkdown/utils": "7.20.0", "prosemirror-safari-ime-span": "^1.0.1", "remark-gfm": "^4.0.1" } }, "sha512-ulErTWDqrGYYqto4kQO9dPTMRp+q44pdRTPW4MTeiSO7eJ6JIBUKSqtCm1zf7MX6nZPaPhuscmg5CU2moXOwxQ=="],
+
+ "@milkdown/prose": ["@milkdown/prose@7.20.0", "", { "dependencies": { "@milkdown/exception": "7.20.0", "prosemirror-changeset": "^2.3.1", "prosemirror-commands": "^1.7.1", "prosemirror-dropcursor": "^1.8.2", "prosemirror-gapcursor": "^1.4.0", "prosemirror-history": "^1.5.0", "prosemirror-inputrules": "^1.5.1", "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-schema-list": "^1.5.1", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.1", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.3" } }, "sha512-Qe6jmKcXsjOfpk8duDFdkLCEo5044L8HSyKVn7ewAe7XJJPUM6bPQaP130UAznq75/+TiKxFCzurcrBO3LzNRg=="],
+
+ "@milkdown/transformer": ["@milkdown/transformer@7.20.0", "", { "dependencies": { "@milkdown/exception": "7.20.0", "@milkdown/prose": "7.20.0", "remark": "^15.0.1", "unified": "^11.0.3" } }, "sha512-h7KGFr1o5AYwc+hEfnA3Dldo4jRrYOB/7KExaqelcjUz++KYI/9LXMOsV7CpgjtLI3Xtf2IIRTZND1+p2nsOaw=="],
+
+ "@milkdown/utils": ["@milkdown/utils@7.20.0", "", { "dependencies": { "@milkdown/core": "7.20.0", "@milkdown/ctx": "7.20.0", "@milkdown/exception": "7.20.0", "@milkdown/prose": "7.20.0", "@milkdown/transformer": "7.20.0", "nanoid": "^5.0.9" } }, "sha512-ciEhtLKhIW/Kaz/NRE5DUXVoMCdenn7S4mClrO7sZ/nXtmObnk3okJzSDnamQoDOcLOIbpOu1V3E1Btkvc5x9w=="],
+
+ "@ocavue/utils": ["@ocavue/utils@1.6.0", "", {}, "sha512-8W3q1hxx9qFdrYgPtbElllG/tqYkO/dMhlRUiqasO0SuDFTj78azSQjhIrBTFWxlBPPsSZN6zXYHmb3RwN2Jtg=="],
+
+ "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
+ "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
+
+ "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
+
+ "@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
+
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
+ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
+
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "@vue/compiler-core": ["@vue/compiler-core@3.5.32", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.32", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ=="],
+
+ "@vue/compiler-dom": ["@vue/compiler-dom@3.5.32", "", { "dependencies": { "@vue/compiler-core": "3.5.32", "@vue/shared": "3.5.32" } }, "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q=="],
+
+ "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.32", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.32", "@vue/compiler-dom": "3.5.32", "@vue/compiler-ssr": "3.5.32", "@vue/shared": "3.5.32", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg=="],
+
+ "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.32", "", { "dependencies": { "@vue/compiler-dom": "3.5.32", "@vue/shared": "3.5.32" } }, "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw=="],
+
+ "@vue/reactivity": ["@vue/reactivity@3.5.32", "", { "dependencies": { "@vue/shared": "3.5.32" } }, "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ=="],
+
+ "@vue/runtime-core": ["@vue/runtime-core@3.5.32", "", { "dependencies": { "@vue/reactivity": "3.5.32", "@vue/shared": "3.5.32" } }, "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ=="],
+
+ "@vue/runtime-dom": ["@vue/runtime-dom@3.5.32", "", { "dependencies": { "@vue/reactivity": "3.5.32", "@vue/runtime-core": "3.5.32", "@vue/shared": "3.5.32", "csstype": "^3.2.3" } }, "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ=="],
+
+ "@vue/server-renderer": ["@vue/server-renderer@3.5.32", "", { "dependencies": { "@vue/compiler-ssr": "3.5.32", "@vue/shared": "3.5.32" }, "peerDependencies": { "vue": "3.5.32" } }, "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ=="],
+
+ "@vue/shared": ["@vue/shared@3.5.32", "", {}, "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg=="],
+
+ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
+
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
+
+ "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+
+ "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+ "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="],
+
+ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
+
+ "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
+
+ "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "katex": ["katex@0.16.44", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ=="],
+
+ "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
+
+ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
+
+ "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
+
+ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
+
+ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
+
+ "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
+
+ "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
+
+ "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
+
+ "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
+
+ "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
+
+ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
+
+ "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
+
+ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
+
+ "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
+
+ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+
+ "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
+
+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
+
+ "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
+
+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
+
+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
+
+ "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
+
+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
+
+ "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
+
+ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
+
+ "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
+
+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
+
+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+ "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="],
+
+ "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "prosemirror-changeset": ["prosemirror-changeset@2.4.0", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng=="],
+
+ "prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="],
+
+ "prosemirror-drop-indicator": ["prosemirror-drop-indicator@0.1.3", "", { "dependencies": { "@ocavue/utils": "^1.0.0", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-view": "^1.41.3" } }, "sha512-fJV6G2tHIVXZLUuc60fS9ly1/GuGOlAZUm67S1El+kGFUYh27Hyv6hcGx3rrJ+Q/JZL5jnyAibIZYYWpPqE45g=="],
+
+ "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="],
+
+ "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.1", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="],
+
+ "prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="],
+
+ "prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="],
+
+ "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="],
+
+ "prosemirror-model": ["prosemirror-model@1.25.4", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA=="],
+
+ "prosemirror-safari-ime-span": ["prosemirror-safari-ime-span@1.0.2", "", { "dependencies": { "prosemirror-state": "^1.4.3", "prosemirror-view": "^1.33.8" } }, "sha512-QJqD8s1zE/CuK56kDsUhndh5hiHh/gFnAuPOA9ytva2s85/ZEt2tNWeALTJN48DtWghSKOmiBsvVn2OlnJ5H2w=="],
+
+ "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="],
+
+ "prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="],
+
+ "prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="],
+
+ "prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="],
+
+ "prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="],
+
+ "prosemirror-virtual-cursor": ["prosemirror-virtual-cursor@0.4.2", "", { "peerDependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" }, "optionalPeers": ["prosemirror-model", "prosemirror-state", "prosemirror-view"] }, "sha512-pUMKnIuOhhnMcgIJUjhIQTVJruBEGxfMBVQSrK0g2qhGPDm1i12KdsVaFw15dYk+29tZcxjMeR7P5VDKwmbwJg=="],
+
+ "remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="],
+
+ "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
+
+ "remark-inline-links": ["remark-inline-links@7.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-definitions": "^6.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ=="],
+
+ "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
+
+ "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
+
+ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
+
+ "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
+
+ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+
+ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
+
+ "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
+
+ "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
+
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
+
+ "vue": ["vue@3.5.32", "", { "dependencies": { "@vue/compiler-dom": "3.5.32", "@vue/compiler-sfc": "3.5.32", "@vue/runtime-dom": "3.5.32", "@vue/server-renderer": "3.5.32", "@vue/shared": "3.5.32" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw=="],
+
+ "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
+
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
+ "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+ }
+}
diff --git a/lib/package.json b/lib/package.json
new file mode 100644
index 0000000..339a337
--- /dev/null
+++ b/lib/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "@milkdown/crepe": "^7.20.0"
+ }
+} \ No newline at end of file
diff --git a/scripts/download-deps.sh b/scripts/download-deps.sh
new file mode 100644
index 0000000..6d1b2e4
--- /dev/null
+++ b/scripts/download-deps.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Download Milkdown Crepe from jsDelivr CDN and extract to lib/
+
+set -e
+
+LIB_DIR="lib/milkdown-crepe"
+CREPE_VERSION="7.7.0"
+CREPE_URL="https://cdn.jsdelivr.net/npm/@milkdown/crepe@${CREPE_VERSION}/dist"
+
+echo "📦 Downloading Milkdown Crepe v${CREPE_VERSION}..."
+
+mkdir -p "$LIB_DIR"
+
+# Download key files
+echo " → index.js"
+curl -fsSL "${CREPE_URL}/index.js" -o "${LIB_DIR}/index.js"
+
+echo " → index.d.ts"
+curl -fsSL "${CREPE_URL}/index.d.ts" -o "${LIB_DIR}/index.d.ts"
+
+echo " → style.css"
+curl -fsSL "${CREPE_URL}/style.css" -o "${LIB_DIR}/style.css"
+
+echo "✓ Milkdown Crepe installed to ${LIB_DIR}"
diff --git a/styles/admin.css b/styles/admin.css
index da2ebe9..ce19507 100644
--- a/styles/admin.css
+++ b/styles/admin.css
@@ -1,17 +1,19 @@
-/* Admin-specific styles */
-
-body.admin {
- --color-bg: #f8f8f8;
+form {
+ display: block;
+ width: 300px;
}
-.admin-nav {
- max-width: var(--max-width);
- margin: 0 auto 2rem;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid #ddd;
- font-size: 0.9em;
+#editor {
+ background: #fff;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 0.5rem;
+ min-height: 340px;
+ font-family: 'Menlo', 'Consolas', monospace;
+ font-size: 0.85rem;
}
-.admin-nav a {
- margin-right: 1rem;
-}
+#editor.crepe-editor {
+ border-color: #2d5fc4;
+ box-shadow: 0 0 0 2px rgba(45, 95, 196, .15);
+} \ No newline at end of file
diff --git a/templates/admin/base.html b/templates/admin/base.html
new file mode 100644
index 0000000..f88bba6
--- /dev/null
+++ b/templates/admin/base.html
@@ -0,0 +1,34 @@
+{{define "base"}}
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Admin — {{.Title}}</title>
+ <link rel="stylesheet" href="/styles/admin.css">
+ <link rel="stylesheet" href="/lib/milkdown-crepe/style.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "@milkdown/crepe": "/lib/node_modules/@milkdown/crepe"
+ }
+ }
+ </script>
+</head>
+
+<body>
+ <nav>
+ <span class="brand">Admin</span>
+ <a href="/admin/">Posts</a>
+ <a href="/admin/new">New Post</a>
+ </nav>
+ <main>
+ {{if eq .ContentTemplate "list-content"}}{{template "list-content" .}}{{end}}
+ {{if eq .ContentTemplate "form-content"}}{{template "form-content" .}}{{end}}
+ {{if eq .ContentTemplate "error-content"}}{{template "error-content" .}}{{end}}
+ </main>
+</body>
+
+</html>
+{{end}} \ No newline at end of file
diff --git a/templates/admin/error.html b/templates/admin/error.html
new file mode 100644
index 0000000..36a4a8a
--- /dev/null
+++ b/templates/admin/error.html
@@ -0,0 +1,7 @@
+{{define "error-content"}}
+ <div class="alert">
+ <h2>Error</h2>
+ <p>{{.Message}}</p>
+ <p><a href="javascript:history.back()">Go back</a></p>
+ </div>
+{{end}}
diff --git a/templates/admin/form.html b/templates/admin/form.html
new file mode 100644
index 0000000..1b577c9
--- /dev/null
+++ b/templates/admin/form.html
@@ -0,0 +1,62 @@
+{{define "form-content"}}
+ <h1>{{.Title}}</h1>
+ <form method="POST" action="{{.Action}}">
+ <label for="title">Title</label>
+ <input type="text" id="title" name="title" value="{{.Post.Title}}" required autofocus>
+
+ <label for="date">Date</label>
+ <input type="date" id="date" name="date" value="{{.Post.Date}}">
+
+ <label for="tags">Tags</label>
+ <input type="text" id="tags" name="tags" value="{{.Post.Tags}}" placeholder="tag1, tag2, tag3">
+ <p class="hint">Comma-separated list of tags.</p>
+
+ <label for="content">Content (Markdown)</label>
+ <textarea id="content" name="content" style="display: none;">{{.Post.Content}}</textarea>
+ <div id="editor" style="border: 1px solid #ccc; border-radius: 4px; min-height: 340px;"></div>
+
+ <div class="form-actions">
+ <button type="submit" class="btn btn-primary">
+ {{if .IsNew}}Create Post{{else}}Save Changes{{end}}
+ </button>
+ <a href="/admin/" class="btn btn-secondary">Cancel</a>
+ </div>
+ </form>
+
+ <script type="module">
+ import { Crepe } from '@milkdown/crepe';
+
+ const contentField = document.getElementById('content');
+ const editorContainer = document.getElementById('editor');
+ const form = contentField.closest('form');
+
+ // Initialize Crepe with the textarea content
+ const crepe = new Crepe({
+ root: editorContainer,
+ defaultValue: contentField.value,
+ });
+
+ // Sync editor content back to textarea before form submission
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ try {
+ // Get the markdown content from Crepe
+ const markdown = await crepe.getMarkdown();
+ contentField.value = markdown;
+ } catch (err) {
+ console.error('Failed to get markdown from editor:', err);
+ }
+
+ // Submit the form
+ form.submit();
+ });
+ </script>
+
+ <noscript>
+ <style>
+ #editor { display: none; }
+ #content { display: block !important; }
+ </style>
+ </noscript>
+{{end}}
diff --git a/templates/admin/list.html b/templates/admin/list.html
new file mode 100644
index 0000000..561e317
--- /dev/null
+++ b/templates/admin/list.html
@@ -0,0 +1,40 @@
+{{define "list-content"}}
+ <h1>Posts</h1>
+ {{if .Posts}}
+ <table>
+ <thead>
+ <tr>
+ <th>Title</th>
+ <th>Date</th>
+ <th>Tags</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range .Posts}}
+ <tr>
+ <td>{{.Title}}</td>
+ <td>{{.Date}}</td>
+ <td>
+ {{if .Tags}}
+ <div class="tags">
+ {{range (splitTags .Tags)}}<span class="tag">{{.}}</span>{{end}}
+ </div>
+ {{end}}
+ </td>
+ <td>
+ <div class="actions">
+ <a href="/admin/{{.Slug}}/edit" class="btn btn-secondary">Edit</a>
+ <form method="POST" action="/admin/{{.Slug}}/delete" onsubmit="return confirm('Delete {{.Title}}?')">
+ <button type="submit" class="btn btn-danger">Delete</button>
+ </form>
+ </div>
+ </td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ {{else}}
+ <div class="empty">No posts yet. <a href="/admin/new">Create one.</a></div>
+ {{end}}
+{{end}}
diff --git a/tmp/build-errors.log b/tmp/build-errors.log
new file mode 100644
index 0000000..40b5ed1
--- /dev/null
+++ b/tmp/build-errors.log
@@ -0,0 +1 @@
+exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file