diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | .gitmodules | 0 | ||||
| -rw-r--r-- | CLAUDE.md | 172 | ||||
| -rw-r--r-- | cmd/nebbet/main.go | 20 | ||||
| -rw-r--r-- | content/admin/index.md | 16 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-04-03-gin-migration-plan.md | 739 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-04-03-gin-migration-design.md | 198 | ||||
| -rw-r--r-- | internal/admin/auth/auth.go (renamed from internal/auth/auth.go) | 0 | ||||
| -rw-r--r-- | internal/admin/server.go | 137 | ||||
| -rw-r--r-- | internal/db/meta.go | 2 | ||||
| -rw-r--r-- | internal/db/search.go | 2 | ||||
| -rw-r--r-- | lib/bun.lock | 422 | ||||
| -rw-r--r-- | lib/package.json | 5 | ||||
| -rw-r--r-- | scripts/download-deps.sh | 25 | ||||
| -rw-r--r-- | styles/admin.css | 28 | ||||
| -rw-r--r-- | templates/admin/base.html | 34 | ||||
| -rw-r--r-- | templates/admin/error.html | 7 | ||||
| -rw-r--r-- | templates/admin/form.html | 62 | ||||
| -rw-r--r-- | templates/admin/list.html | 40 | ||||
| -rw-r--r-- | tmp/build-errors.log | 1 |
20 files changed, 1801 insertions, 110 deletions
@@ -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 |
