{{ note.Title }}
7 | {{ note.HTMLContent | safe }} 8 |Last updated: {{ note.LastUpdatedAt }}.
9 |
3 |
4 |
8 |
11 |
Make your digital garden with MarkdownBrain.
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 | Releases · 26 | Documentation 27 |
28 | 29 | ## Quick Start 30 | 31 | ### Client 32 | 33 | #### Create client config.yml 34 | 35 | ```bash 36 | echo 'source: "~/Library/Mobile Documents/com~apple~CloudDocs/obsidian/example" 37 | server: "https://your-server-url" 38 | api_key: "1234567890" 39 | ignores: 40 | - "Templates"' > config.yml 41 | ``` 42 | 43 | > Note: The `source` is the path to your Obsidian vault. 44 | 45 | #### Run cli 46 | 47 | ```bash 48 | curl -L https://github.com/blackstorm/markdownbrain/releases/download/v0.1.1/markdownbrain-cli-darwin-amd64 -o markdownbrain-client 49 | chmod +x markdownbrain-client 50 | ./markdownbrain-client -c config.yml 51 | ``` 52 | > Note: Before running the client, ensure the `server` is running. 53 | 54 | ### Server 55 | 56 | #### Create config.yml 57 | ```bash 58 | echo 'lang: "en" 59 | root_note_name: "Welcome" 60 | name: "MarkdownBrain" 61 | description: "MarkdownBrain" 62 | api_key: "1234567890"' > config.yml 63 | ``` 64 | 65 | #### Run Server 66 | 67 | ```bash 68 | docker run -dit --name markdownbrain -v $(pwd)/config.yml:/markdownbrain/config.yml -p 3000:3000 ghcr.io/blackstorm/markdownbrain-server:latest 69 | ``` 70 | 71 | ## Documentation 72 | 73 | [MarkdownBrain.com](https://markdownbrain.com) 74 | 75 | ## License 76 | 77 | [AGPLv3](LICENSE.md) 78 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | config.yaml 2 | config.yml -------------------------------------------------------------------------------- /cli/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-bookworm AS builder 2 | 3 | WORKDIR /markdownbrain 4 | 5 | COPY . . 6 | 7 | RUN go build -o markdownbrain-cli cli/main.go 8 | 9 | FROM debian:bookworm 10 | 11 | WORKDIR /markdownbrain 12 | 13 | COPY --from=builder /markdownbrain/cli/config.yml.example /markdownbrain/config.yml 14 | 15 | CMD ["./markdownbrain-cli", "-config", "/markdownbrain/config.yml"] 16 | -------------------------------------------------------------------------------- /cli/builder/builder.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/blackstorm/markdownbrain/common" 5 | ) 6 | 7 | type DatabaseBuilder interface { 8 | Build(source string, db *common.DB) error 9 | } 10 | -------------------------------------------------------------------------------- /cli/builder/obsidian.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/blackstorm/markdownbrain/common" 14 | strip "github.com/grokify/html-strip-tags-go" 15 | "github.com/yuin/goldmark" 16 | "github.com/yuin/goldmark/extension" 17 | "github.com/yuin/goldmark/renderer/html" 18 | "gopkg.in/yaml.v3" 19 | ) 20 | 21 | type Error struct { 22 | Message string 23 | Err error 24 | } 25 | 26 | func (e *Error) Error() string { 27 | return fmt.Sprintf("%s: %v", e.Message, e.Err) 28 | } 29 | 30 | type NoteData struct { 31 | ID string 32 | Title string 33 | Description string 34 | FilePath string 35 | CreatedAt string 36 | LastUpdatedAt string 37 | } 38 | 39 | type ObsidianBuilder struct { 40 | idGenerator *common.IdGenerator 41 | ignores map[string]bool 42 | db *common.DB 43 | md goldmark.Markdown 44 | } 45 | 46 | func NewObsidianBuilder(ignores []string, db *common.DB) *ObsidianBuilder { 47 | ignoremap := make(map[string]bool) 48 | for _, i := range ignores { 49 | ignoremap[i] = true 50 | } 51 | 52 | md := goldmark.New( 53 | goldmark.WithExtensions(extension.GFM), 54 | goldmark.WithRendererOptions( 55 | html.WithUnsafe(), 56 | html.WithHardWraps(), 57 | ), 58 | ) 59 | 60 | return &ObsidianBuilder{ 61 | idGenerator: common.NewSqidsIdGenerator(), 62 | ignores: ignoremap, 63 | db: db, 64 | md: md, 65 | } 66 | } 67 | 68 | // Build build from a note source dir 69 | func (b *ObsidianBuilder) Build(src string) error { 70 | srcPath := b.resolveHomePath(src) 71 | 72 | info, err := os.Stat(srcPath) 73 | if err != nil { 74 | return &Error{"Failed to access source path", err} 75 | } 76 | 77 | if !info.IsDir() { 78 | return &Error{"Source path is not a directory", nil} 79 | } 80 | 81 | notes, err := b.collectNotes(srcPath) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | return b.processNotes(notes) 87 | } 88 | 89 | // collectNotes collect src path all 90 | func (b *ObsidianBuilder) collectNotes(srcPath string) (map[string]*NoteData, error) { 91 | notes := make(map[string]*NoteData) 92 | 93 | err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { 94 | if err != nil { 95 | return err 96 | } 97 | 98 | for component := range b.ignores { 99 | if strings.Contains(path, component) { 100 | return nil 101 | } 102 | } 103 | 104 | if !info.IsDir() && strings.HasSuffix(strings.ToLower(path), ".md") { 105 | note, err := b.getNoteMetadata(path) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | filename := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 111 | notes[filename] = note 112 | } 113 | 114 | return nil 115 | }) 116 | 117 | if err != nil { 118 | return nil, &Error{"Failed to collect notes", err} 119 | } 120 | 121 | return notes, nil 122 | } 123 | 124 | func (b *ObsidianBuilder) processNotes(notes map[string]*NoteData) error { 125 | for _, data := range notes { 126 | content, err := os.ReadFile(data.FilePath) 127 | if err != nil { 128 | return &Error{"Failed to read note content", err} 129 | } 130 | 131 | // remove YAML frontmatter 132 | contentStr := string(content) 133 | if strings.HasPrefix(contentStr, "---") { 134 | if idx := strings.Index(contentStr[3:], "---"); idx != -1 { 135 | contentStr = contentStr[idx+6:] 136 | } 137 | } 138 | 139 | htmlContent, linkIDs, err := b.processContent(data.ID, contentStr, notes) 140 | if err != nil { 141 | return &Error{"Failed to process note content", err} 142 | } 143 | 144 | linkIDsJSON, err := json.Marshal(linkIDs) 145 | if err != nil { 146 | return errors.New("failed to marshal link IDs") 147 | } 148 | 149 | if data.Description == "" { 150 | htmlContentStripped := strings.ReplaceAll(strip.StripTags(htmlContent), "\n", "") 151 | data.Description = string([]rune(htmlContentStripped)[:common.MinInt(len([]rune(htmlContentStripped)), 100)]) 152 | } 153 | 154 | note := &common.Note{ 155 | ID: data.ID, 156 | Title: data.Title, 157 | Description: data.Description, 158 | HTMLContent: htmlContent, 159 | CreatedAt: data.CreatedAt, 160 | LastUpdatedAt: data.LastUpdatedAt, 161 | LinkNoteIDs: string(linkIDsJSON), 162 | } 163 | 164 | if err := b.db.InsertNote(note); err != nil { 165 | return &Error{"Failed to insert note into database", err} 166 | } 167 | } 168 | 169 | return nil 170 | } 171 | 172 | // processContent process note markdown to html and convert link 173 | func (b *ObsidianBuilder) processContent(noteID string, content string, notes map[string]*NoteData) (string, []string, error) { 174 | linkIDs := make([]string, 0) 175 | processed := b.processLinks(noteID, content, notes, &linkIDs) 176 | 177 | htmlContent, err := b.markdownToHTML(processed) 178 | if err != nil { 179 | return "", nil, err 180 | } 181 | 182 | return htmlContent, linkIDs, nil 183 | } 184 | 185 | // processLinks process [[link]] syntax 186 | func (b *ObsidianBuilder) processLinks(noteID string, content string, notes map[string]*NoteData, linkIDs *[]string) string { 187 | re := regexp.MustCompile(`\[\[([^\[\]]+)\]\]`) 188 | 189 | return re.ReplaceAllStringFunc(content, func(match string) string { 190 | inner := match[2 : len(match)-2] 191 | name, display := b.parseLinkParts(inner) 192 | 193 | if note, ok := notes[name]; ok { 194 | *linkIDs = append(*linkIDs, note.ID) 195 | return b.createLinkHTML(noteID, note.ID, display) 196 | } 197 | 198 | return display 199 | }) 200 | } 201 | 202 | // parseLinkParts parse double [] link and alias 203 | func (b *ObsidianBuilder) parseLinkParts(content string) (name string, display string) { 204 | parts := strings.Split(content, "|") 205 | 206 | if len(parts) == 1 { 207 | name = filepath.Base(parts[0]) 208 | return strings.TrimSpace(name), strings.TrimSpace(name) 209 | } 210 | 211 | name = filepath.Base(parts[0]) 212 | return strings.TrimSpace(name), strings.TrimSpace(parts[1]) 213 | } 214 | 215 | // createLinkHTML create htmx link 216 | func (b *ObsidianBuilder) createLinkHTML(fromID string, toID string, display string) string { 217 | return fmt.Sprintf(`%s`, 220 | toID, toID, fromID, fromID, display) 221 | } 222 | 223 | // getNoteMetadata read note content meta data 224 | func (b *ObsidianBuilder) getNoteMetadata(path string) (*NoteData, error) { 225 | content, err := os.ReadFile(path) 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | info, err := os.Stat(path) 231 | if err != nil { 232 | return nil, err 233 | } 234 | 235 | filename := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 236 | 237 | note := &NoteData{ 238 | Title: filename, 239 | FilePath: path, 240 | CreatedAt: info.ModTime().Format("2006-01-02"), 241 | LastUpdatedAt: info.ModTime().Format("2006-01-02"), 242 | } 243 | 244 | if bytes.HasPrefix(content, []byte("---")) { 245 | if idx := bytes.Index(content[3:], []byte("---")); idx != -1 { 246 | var meta struct { 247 | Title string `yaml:"title"` 248 | Description string `yaml:"description"` 249 | CreatedAt string `yaml:"created_at"` 250 | LastUpdatedAt string `yaml:"last_updated_at"` 251 | } 252 | // If note contains metadata, then use the custom metadata. 253 | if err := yaml.Unmarshal(content[3:idx+3], &meta); err == nil { 254 | if meta.Title != "" { 255 | note.Title = meta.Title 256 | } 257 | if meta.Description != "" { 258 | note.Description = meta.Description 259 | } 260 | if meta.CreatedAt != "" { 261 | note.CreatedAt = meta.CreatedAt 262 | } 263 | if meta.LastUpdatedAt != "" { 264 | note.LastUpdatedAt = meta.LastUpdatedAt 265 | } 266 | } 267 | } 268 | } 269 | 270 | // Generate note id by filename without file ext. 271 | note.ID = b.idGenerator.Generate(filename) 272 | return note, nil 273 | } 274 | 275 | func (b *ObsidianBuilder) resolveHomePath(path string) string { 276 | if strings.HasPrefix(path, "~") { 277 | home, err := os.UserHomeDir() 278 | if err != nil { 279 | return path 280 | } 281 | return filepath.Join(home, path[1:]) 282 | } 283 | return path 284 | } 285 | 286 | func (b *ObsidianBuilder) markdownToHTML(content string) (string, error) { 287 | var buf bytes.Buffer 288 | if err := b.md.Convert([]byte(content), &buf); err != nil { 289 | return "", err 290 | } 291 | return buf.String(), nil 292 | } 293 | -------------------------------------------------------------------------------- /cli/builder/obsidian_test.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/blackstorm/markdownbrain/common" 9 | ) 10 | 11 | func TestObsidianBuilder(t *testing.T) { 12 | tmpDir, err := os.MkdirTemp("", "notes-test") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer os.RemoveAll(tmpDir) 17 | 18 | dbPath := filepath.Join(tmpDir, "test.db") 19 | db, err := common.NewDB(dbPath, true) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | defer db.Close() 24 | 25 | noteContent := `--- 26 | title: Test Note 27 | created_at: 2024-01-01 28 | --- 29 | # Test Note 30 | 31 | This is a test note with a [[link]] to another note.` 32 | 33 | notePath := filepath.Join(tmpDir, "test.md") 34 | if err := os.WriteFile(notePath, []byte(noteContent), 0644); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | builder := NewObsidianBuilder([]string{".git"}, db) 39 | 40 | if err := builder.Build(tmpDir); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /cli/config.yml.example: -------------------------------------------------------------------------------- 1 | source: "~/Library/Mobile Documents/com~apple~CloudDocs/obsidian/example" 2 | server: "http://localhost:3000" 3 | api_key: "1234567890" 4 | ignores: 5 | - "Templates" 6 | -------------------------------------------------------------------------------- /cli/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Source string `yaml:"source"` 5 | Ignores []string `yaml:"ignores"` 6 | Server string `yaml:"server"` 7 | APIKey string `yaml:"api_key"` 8 | } 9 | -------------------------------------------------------------------------------- /cli/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blackstorm/markdownbrain/cli 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/go-resty/resty/v2 v2.16.2 7 | github.com/grokify/html-strip-tags-go v0.1.0 8 | github.com/yuin/goldmark v1.7.8 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | 12 | require ( 13 | golang.org/x/net v0.27.0 // indirect 14 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 15 | ) 16 | 17 | replace github.com/blockstorm/markdownbrain/common => ../common 18 | -------------------------------------------------------------------------------- /cli/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= 2 | github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= 3 | github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= 4 | github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= 5 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 6 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= 11 | github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 12 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 13 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 14 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 17 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/blackstorm/markdownbrain/cli/builder" 13 | "github.com/blackstorm/markdownbrain/cli/config" 14 | "github.com/blackstorm/markdownbrain/common" 15 | "github.com/go-resty/resty/v2" 16 | ) 17 | 18 | type Args struct { 19 | ConfigPath string 20 | } 21 | 22 | func main() { 23 | log.SetFlags(log.LstdFlags | log.Lshortfile) 24 | 25 | args, err := parseArgs() 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | config, err := loadConfig(args.ConfigPath) 31 | if err != nil { 32 | log.Fatalf("Failed to load config: %v", err) 33 | } 34 | 35 | db, err := common.NewDBWithTempDir() 36 | if err != nil { 37 | log.Fatalf("Failed to create database: %v", err) 38 | } 39 | log.Printf("Temporary database at: %s", db.Path) 40 | 41 | // 构建数据库 42 | log.Printf("Building database...") 43 | buildStart := time.Now() 44 | builder := builder.NewObsidianBuilder(mixIgnores(config), db) 45 | if err := builder.Build(config.Source); err != nil { 46 | log.Fatalf("Failed to build database: %v", err) 47 | } 48 | log.Printf("Database built in %v", time.Since(buildStart)) 49 | 50 | // 同步到服务器 51 | log.Printf("Syncing to server...") 52 | syncStart := time.Now() 53 | if err := syncToServer(config, db); err != nil { 54 | log.Fatalf("Failed to sync to server: %v", err) 55 | } 56 | log.Printf("Synced to server in %v", time.Since(syncStart)) 57 | } 58 | 59 | func mixIgnores(config *config.Config) []string { 60 | ignores := make(map[string]bool) 61 | defaultIgnores := []string{".git", ".obsidian", ".DS_Store"} 62 | for _, ignore := range defaultIgnores { 63 | ignores[ignore] = true 64 | } 65 | for _, ignore := range config.Ignores { 66 | ignores[ignore] = true 67 | } 68 | 69 | res := make([]string, 0) 70 | for item, ok := range ignores { 71 | if ok { 72 | res = append(res, item) 73 | } 74 | } 75 | 76 | return res 77 | } 78 | 79 | func syncToServer(config *config.Config, db *common.DB) error { 80 | client := resty.New() 81 | 82 | content, err := os.ReadFile(db.Path) 83 | if err != nil { 84 | return fmt.Errorf("failed to read database file: %w", err) 85 | } 86 | 87 | resp, err := client.R(). 88 | SetHeader("Authorization", fmt.Sprintf("Bearer %s", config.APIKey)). 89 | SetFileReader("db", "notes.db", bytes.NewReader(content)). 90 | Post(fmt.Sprintf("%s/api/sync", config.Server)) 91 | 92 | if err != nil { 93 | return fmt.Errorf("failed to send request: %w", err) 94 | } 95 | 96 | // 检查响应状态 97 | if !resp.IsSuccess() { 98 | return fmt.Errorf("server returned error status %d: %s", resp.StatusCode(), resp.String()) 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func parseArgs() (*Args, error) { 105 | homeDir, err := os.UserHomeDir() 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | path := filepath.Join(homeDir, "/markdownbrain/config.yml") 111 | 112 | var args Args 113 | flag.StringVar(&args.ConfigPath, "config", path, "CLI config path.") 114 | 115 | flag.Parse() 116 | 117 | return &args, nil 118 | } 119 | 120 | func loadConfig(path string) (*config.Config, error) { 121 | if path[:2] == "~/" { 122 | homeDir, err := os.UserHomeDir() 123 | if err != nil { 124 | return nil, fmt.Errorf("failed to get home directory: %w", err) 125 | } 126 | path = filepath.Join(homeDir, path[2:]) 127 | } 128 | 129 | var conf config.Config 130 | 131 | if err := common.ParseYAMLConfig(path, &conf); err != nil { 132 | return nil, err 133 | } 134 | 135 | return &conf, nil 136 | } 137 | -------------------------------------------------------------------------------- /common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "os" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | func ParseYAMLConfig(filePath string, config any) error { 10 | content, err := os.ReadFile(filePath) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | if err := yaml.Unmarshal(content, config); err != nil { 16 | return err 17 | } 18 | 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /common/db.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/jmoiron/sqlx" 13 | _ "modernc.org/sqlite" 14 | ) 15 | 16 | var _DRIVER_NAME = "sqlite" 17 | 18 | var _SCHEMA = ` 19 | CREATE TABLE IF NOT EXISTS notes ( 20 | id TEXT PRIMARY KEY, 21 | title TEXT, 22 | description TEXT, 23 | html_content TEXT, 24 | created_at TEXT, 25 | last_updated_at TEXT, 26 | link_note_ids TEXT 27 | ) 28 | ` 29 | 30 | type DB struct { 31 | Path string 32 | connStr string 33 | pool atomic.Pointer[sqlx.DB] 34 | } 35 | 36 | func NewDBWithTempDir() (*DB, error) { 37 | tempDir := os.TempDir() 38 | tempAt := time.Now().Unix() 39 | path := filepath.Join(tempDir, fmt.Sprintf("temp_%d.db", tempAt)) 40 | 41 | return NewDB(path, false) 42 | } 43 | 44 | func NewDB(path string, readonly bool) (*DB, error) { 45 | if path == "" { 46 | return nil, errors.New("path is required") 47 | } 48 | 49 | mode := "rwc" 50 | 51 | // Create db file if not exists 52 | if _, err := os.Stat(path); err != nil { 53 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 54 | return nil, fmt.Errorf("failed to create database directory: %v", err) 55 | } 56 | 57 | connStr := fmt.Sprintf("file:%s?cache=shared&mode=%s", path, mode) 58 | sqlxDB, err := sqlx.Connect(_DRIVER_NAME, connStr) 59 | if err != nil { 60 | return nil, errors.New("failed to create readonly db") 61 | } 62 | 63 | _, err = sqlxDB.Exec(_SCHEMA) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | if err := sqlxDB.Close(); err != nil { 69 | return nil, err 70 | } 71 | } 72 | 73 | args := "" 74 | if readonly { 75 | mode = "ro" 76 | args = "immutable=1&_query_only=1&_journal_mode=OFF" 77 | } 78 | 79 | connStr := fmt.Sprintf("file:%s?cache=shared&mode=%s&%s", path, mode, args) 80 | 81 | sqlxDB, err := sqlx.Connect(_DRIVER_NAME, connStr) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | db := &DB{ 87 | Path: path, 88 | connStr: connStr, 89 | } 90 | 91 | db.pool.Store(sqlxDB) 92 | 93 | return db, nil 94 | } 95 | 96 | func (db *DB) Close() error { 97 | return db.pool.Load().Close() 98 | } 99 | 100 | func (db *DB) FromBytes(bytes []byte) error { 101 | // Get current connection pool 102 | oldPool := db.pool.Load() 103 | 104 | // Close old connection pool 105 | if oldPool != nil { 106 | oldPool.Close() 107 | } 108 | 109 | // Remove old file 110 | if _, err := os.Stat(db.Path); err == nil { 111 | if err := os.Remove(db.Path); err != nil { 112 | return err 113 | } 114 | } 115 | 116 | // Write new file 117 | if err := os.WriteFile(db.Path, bytes, 0644); err != nil { 118 | return err 119 | } 120 | 121 | // Create new connection pool 122 | newPool, err := sqlx.Connect(_DRIVER_NAME, db.connStr) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | // Atomically replace the connection pool 128 | db.pool.Store(newPool) 129 | return nil 130 | } 131 | 132 | // Helper method: get current connection pool 133 | func (db *DB) getPool() *sqlx.DB { 134 | return db.pool.Load() 135 | } 136 | 137 | func (db *DB) InsertNote(note *Note) error { 138 | _, err := db.getPool().NamedExec(` 139 | INSERT INTO notes (id, title, description, html_content, created_at, last_updated_at, link_note_ids) 140 | VALUES (:id, :title, :description, :html_content, :created_at, :last_updated_at, :link_note_ids)`, 141 | note, 142 | ) 143 | return err 144 | } 145 | 146 | func (db *DB) GetNote(id string) (*Note, error) { 147 | note := &Note{} 148 | err := db.getPool().Get(note, "SELECT * FROM notes WHERE id = ?", id) 149 | if err != nil { 150 | if errors.Is(err, sql.ErrNoRows) { 151 | return nil, nil 152 | } 153 | return nil, err 154 | } 155 | return note, nil 156 | } 157 | 158 | func (db *DB) CountNote() (int64, error) { 159 | var count int64 160 | err := db.getPool().Get(&count, "SELECT COUNT(*) FROM notes") 161 | if err != nil { 162 | return 0, err 163 | } 164 | return count, nil 165 | } 166 | 167 | func (db *DB) GetNotesByIDs(ids []string) ([]Note, error) { 168 | query, args, err := sqlx.In("SELECT * FROM notes WHERE id IN (?)", ids) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | // http://jmoiron.github.io/sqlx/#inQueries 174 | // sqlx.In returns queries with the MySQL placeholder (?), we need to rebind it 175 | // for SQLite 176 | query = db.getPool().Rebind(query) 177 | 178 | notes := []Note{} 179 | err = db.getPool().Select(¬es, query, args...) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | return notes, nil 185 | } 186 | 187 | func (db *DB) GetNotesByLinkTo(id string) ([]Note, error) { 188 | notes := []Note{} 189 | err := db.getPool().Select(¬es, "SELECT * FROM notes WHERE EXISTS (SELECT 1 FROM json_each(link_note_ids) WHERE value = ?)", id) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | return notes, nil 195 | } 196 | -------------------------------------------------------------------------------- /common/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blackstorm/markdownbrain/common 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/jmoiron/sqlx v1.4.0 7 | github.com/sqids/sqids-go v0.4.1 8 | gopkg.in/yaml.v3 v3.0.1 9 | modernc.org/sqlite v1.34.4 10 | ) 11 | 12 | require ( 13 | github.com/dustin/go-humanize v1.0.1 // indirect 14 | github.com/google/uuid v1.6.0 // indirect 15 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 16 | github.com/mattn/go-isatty v0.0.20 // indirect 17 | github.com/ncruces/go-strftime v0.1.9 // indirect 18 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 19 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect 20 | golang.org/x/sys v0.28.0 // indirect 21 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 22 | modernc.org/gc/v3 v3.0.0-20241223112719-96e2e1e4408d // indirect 23 | modernc.org/libc v1.61.5 // indirect 24 | modernc.org/mathutil v1.7.1 // indirect 25 | modernc.org/memory v1.8.0 // indirect 26 | modernc.org/strutil v1.2.1 // indirect 27 | modernc.org/token v1.1.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /common/go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 4 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 6 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 7 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= 8 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 12 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 13 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 14 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 15 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 16 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 17 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 18 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 21 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 22 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 23 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 24 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 25 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 26 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 27 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 31 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 32 | github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw= 33 | github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8= 34 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= 35 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 36 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 37 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 38 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 39 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 40 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 42 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 43 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 44 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 47 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 48 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 49 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | modernc.org/cc/v4 v4.24.1 h1:mLykA8iIlZ/SZbwI2JgYIURXQMSgmOb/+5jaielxPi4= 51 | modernc.org/cc/v4 v4.24.1/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI= 52 | modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw= 53 | modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ= 54 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 55 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 56 | modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4= 57 | modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 58 | modernc.org/gc/v3 v3.0.0-20241223112719-96e2e1e4408d h1:d0JExN5U5FjUVHCP6L9DIlLJBZveR6KUM4AvfDUL3+k= 59 | modernc.org/gc/v3 v3.0.0-20241223112719-96e2e1e4408d/go.mod h1:qBSLm/exCqouT2hrfyTKikWKG9IPq8EoX5fS00l3jqk= 60 | modernc.org/libc v1.61.5 h1:WzsPUvWl2CvsRmk2foyWWHUEUmQ2iW4oFyWOVR0O5ho= 61 | modernc.org/libc v1.61.5/go.mod h1:llBdEGIywhnRgAFuTF+CWaKV8/2bFgACcQZTXhkAuAM= 62 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 63 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 64 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 65 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 66 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 67 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 68 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= 69 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= 70 | modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8= 71 | modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk= 72 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 73 | modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 74 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 75 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 76 | -------------------------------------------------------------------------------- /common/id.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "hash/crc32" 5 | 6 | "github.com/sqids/sqids-go" 7 | ) 8 | 9 | const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 10 | 11 | type IdGenerator struct { 12 | sqids *sqids.Sqids 13 | } 14 | 15 | func NewSqidsIdGenerator() *IdGenerator { 16 | s, _ := sqids.New(sqids.Options{ 17 | Alphabet: alphabet, 18 | }) 19 | return &IdGenerator{sqids: s} 20 | } 21 | 22 | func (g *IdGenerator) Generate(data string) string { 23 | crc := crc32.ChecksumIEEE([]byte(data)) 24 | id, _ := g.sqids.Encode([]uint64{uint64(crc)}) 25 | return id 26 | } 27 | -------------------------------------------------------------------------------- /common/id_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "testing" 4 | 5 | func TestGenerator(t *testing.T) { 6 | generator := NewSqidsIdGenerator() 7 | 8 | tests := []struct { 9 | input string 10 | expected string 11 | }{ 12 | {"welcome", "nWd9WFI"}, 13 | {"欢迎", "Q6YVebZ"}, 14 | {"bienvenue", "psxRKiT"}, 15 | {"むかえる", "4thJ4Q7"}, 16 | } 17 | 18 | for _, tt := range tests { 19 | result := generator.Generate(tt.input) 20 | if result != tt.expected { 21 | t.Errorf("Generate(%s) = %s; want %s", tt.input, result, tt.expected) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /common/note.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Note struct { 8 | ID string `json:"id" db:"id"` 9 | Title string `json:"title" db:"title"` 10 | Description string `json:"description" db:"description"` 11 | HTMLContent string `json:"html_content" db:"html_content"` 12 | CreatedAt string `json:"created_at" db:"created_at"` 13 | LastUpdatedAt string `json:"last_updated_at" db:"last_updated_at"` 14 | LinkNoteIDs string `json:"link_note_ids" db:"link_note_ids"` 15 | LinkToThis []Note 16 | } 17 | 18 | func (n *Note) LoadLinkToThisNotes(db *DB) error { 19 | linkToThis, err := db.GetNotesByLinkTo(n.ID) 20 | if err != nil { 21 | return err 22 | } 23 | n.LinkToThis = linkToThis 24 | return nil 25 | } 26 | 27 | type Notes []Note 28 | 29 | func (n Notes) Titles() string { 30 | titles := make([]string, len(n)) 31 | for i, note := range n { 32 | titles[i] = note.Title 33 | } 34 | return strings.Join(titles, "|") 35 | } 36 | -------------------------------------------------------------------------------- /common/urils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | func MinInt(x, y int) int { 4 | if x < y { 5 | return x 6 | } 7 | return y 8 | } 9 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.23.4 2 | 3 | use ( 4 | ./cli 5 | ./common 6 | ./server 7 | ) 8 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 4 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 5 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 6 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 7 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 8 | golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 9 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 10 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 11 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 12 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 13 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 14 | lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 15 | modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= 16 | modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 17 | modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= 18 | modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= 19 | modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdownbrain", 3 | "version": "0.0.1", 4 | "author": "blackstorm", 5 | "license": "MIT", 6 | "description": "markdownbrain", 7 | "scripts": { 8 | "dev": "pnpx tailwindcss -i ./tailwind.css -o ./server/www/static/app.css --watch", 9 | "build": "pnpx tailwindcss -i ./tailwind.css -o ./server/www/static/app.css --minify" 10 | }, 11 | "devDependencies": { 12 | "@tailwindcss/typography": "^0.5.15", 13 | "tailwindcss": "^3.4.15" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@tailwindcss/typography': 12 | specifier: ^0.5.15 13 | version: 0.5.15(tailwindcss@3.4.17) 14 | tailwindcss: 15 | specifier: ^3.4.15 16 | version: 3.4.17 17 | 18 | packages: 19 | 20 | '@alloc/quick-lru@5.2.0': 21 | resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 22 | engines: {node: '>=10'} 23 | 24 | '@isaacs/cliui@8.0.2': 25 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 26 | engines: {node: '>=12'} 27 | 28 | '@jridgewell/gen-mapping@0.3.8': 29 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 30 | engines: {node: '>=6.0.0'} 31 | 32 | '@jridgewell/resolve-uri@3.1.2': 33 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 34 | engines: {node: '>=6.0.0'} 35 | 36 | '@jridgewell/set-array@1.2.1': 37 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 38 | engines: {node: '>=6.0.0'} 39 | 40 | '@jridgewell/sourcemap-codec@1.5.0': 41 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 42 | 43 | '@jridgewell/trace-mapping@0.3.25': 44 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 45 | 46 | '@nodelib/fs.scandir@2.1.5': 47 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 48 | engines: {node: '>= 8'} 49 | 50 | '@nodelib/fs.stat@2.0.5': 51 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 52 | engines: {node: '>= 8'} 53 | 54 | '@nodelib/fs.walk@1.2.8': 55 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 56 | engines: {node: '>= 8'} 57 | 58 | '@pkgjs/parseargs@0.11.0': 59 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 60 | engines: {node: '>=14'} 61 | 62 | '@tailwindcss/typography@0.5.15': 63 | resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==} 64 | peerDependencies: 65 | tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' 66 | 67 | ansi-regex@5.0.1: 68 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 69 | engines: {node: '>=8'} 70 | 71 | ansi-regex@6.1.0: 72 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 73 | engines: {node: '>=12'} 74 | 75 | ansi-styles@4.3.0: 76 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 77 | engines: {node: '>=8'} 78 | 79 | ansi-styles@6.2.1: 80 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 81 | engines: {node: '>=12'} 82 | 83 | any-promise@1.3.0: 84 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 85 | 86 | anymatch@3.1.3: 87 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 88 | engines: {node: '>= 8'} 89 | 90 | arg@5.0.2: 91 | resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 92 | 93 | balanced-match@1.0.2: 94 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 95 | 96 | binary-extensions@2.3.0: 97 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 98 | engines: {node: '>=8'} 99 | 100 | brace-expansion@2.0.1: 101 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 102 | 103 | braces@3.0.3: 104 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 105 | engines: {node: '>=8'} 106 | 107 | camelcase-css@2.0.1: 108 | resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 109 | engines: {node: '>= 6'} 110 | 111 | chokidar@3.6.0: 112 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 113 | engines: {node: '>= 8.10.0'} 114 | 115 | color-convert@2.0.1: 116 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 117 | engines: {node: '>=7.0.0'} 118 | 119 | color-name@1.1.4: 120 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 121 | 122 | commander@4.1.1: 123 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 124 | engines: {node: '>= 6'} 125 | 126 | cross-spawn@7.0.6: 127 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 128 | engines: {node: '>= 8'} 129 | 130 | cssesc@3.0.0: 131 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 132 | engines: {node: '>=4'} 133 | hasBin: true 134 | 135 | didyoumean@1.2.2: 136 | resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 137 | 138 | dlv@1.1.3: 139 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 140 | 141 | eastasianwidth@0.2.0: 142 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 143 | 144 | emoji-regex@8.0.0: 145 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 146 | 147 | emoji-regex@9.2.2: 148 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 149 | 150 | fast-glob@3.3.2: 151 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 152 | engines: {node: '>=8.6.0'} 153 | 154 | fastq@1.18.0: 155 | resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} 156 | 157 | fill-range@7.1.1: 158 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 159 | engines: {node: '>=8'} 160 | 161 | foreground-child@3.3.0: 162 | resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 163 | engines: {node: '>=14'} 164 | 165 | fsevents@2.3.3: 166 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 167 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 168 | os: [darwin] 169 | 170 | function-bind@1.1.2: 171 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 172 | 173 | glob-parent@5.1.2: 174 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 175 | engines: {node: '>= 6'} 176 | 177 | glob-parent@6.0.2: 178 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 179 | engines: {node: '>=10.13.0'} 180 | 181 | glob@10.4.5: 182 | resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 183 | hasBin: true 184 | 185 | hasown@2.0.2: 186 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 187 | engines: {node: '>= 0.4'} 188 | 189 | is-binary-path@2.1.0: 190 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 191 | engines: {node: '>=8'} 192 | 193 | is-core-module@2.16.1: 194 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} 195 | engines: {node: '>= 0.4'} 196 | 197 | is-extglob@2.1.1: 198 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 199 | engines: {node: '>=0.10.0'} 200 | 201 | is-fullwidth-code-point@3.0.0: 202 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 203 | engines: {node: '>=8'} 204 | 205 | is-glob@4.0.3: 206 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 207 | engines: {node: '>=0.10.0'} 208 | 209 | is-number@7.0.0: 210 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 211 | engines: {node: '>=0.12.0'} 212 | 213 | isexe@2.0.0: 214 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 215 | 216 | jackspeak@3.4.3: 217 | resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 218 | 219 | jiti@1.21.7: 220 | resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} 221 | hasBin: true 222 | 223 | lilconfig@3.1.3: 224 | resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 225 | engines: {node: '>=14'} 226 | 227 | lines-and-columns@1.2.4: 228 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 229 | 230 | lodash.castarray@4.4.0: 231 | resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} 232 | 233 | lodash.isplainobject@4.0.6: 234 | resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} 235 | 236 | lodash.merge@4.6.2: 237 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 238 | 239 | lru-cache@10.4.3: 240 | resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 241 | 242 | merge2@1.4.1: 243 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 244 | engines: {node: '>= 8'} 245 | 246 | micromatch@4.0.8: 247 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 248 | engines: {node: '>=8.6'} 249 | 250 | minimatch@9.0.5: 251 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 252 | engines: {node: '>=16 || 14 >=14.17'} 253 | 254 | minipass@7.1.2: 255 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 256 | engines: {node: '>=16 || 14 >=14.17'} 257 | 258 | mz@2.7.0: 259 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 260 | 261 | nanoid@3.3.8: 262 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 263 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 264 | hasBin: true 265 | 266 | normalize-path@3.0.0: 267 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 268 | engines: {node: '>=0.10.0'} 269 | 270 | object-assign@4.1.1: 271 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 272 | engines: {node: '>=0.10.0'} 273 | 274 | object-hash@3.0.0: 275 | resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 276 | engines: {node: '>= 6'} 277 | 278 | package-json-from-dist@1.0.1: 279 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 280 | 281 | path-key@3.1.1: 282 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 283 | engines: {node: '>=8'} 284 | 285 | path-parse@1.0.7: 286 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 287 | 288 | path-scurry@1.11.1: 289 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 290 | engines: {node: '>=16 || 14 >=14.18'} 291 | 292 | picocolors@1.1.1: 293 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 294 | 295 | picomatch@2.3.1: 296 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 297 | engines: {node: '>=8.6'} 298 | 299 | pify@2.3.0: 300 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 301 | engines: {node: '>=0.10.0'} 302 | 303 | pirates@4.0.6: 304 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 305 | engines: {node: '>= 6'} 306 | 307 | postcss-import@15.1.0: 308 | resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 309 | engines: {node: '>=14.0.0'} 310 | peerDependencies: 311 | postcss: ^8.0.0 312 | 313 | postcss-js@4.0.1: 314 | resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 315 | engines: {node: ^12 || ^14 || >= 16} 316 | peerDependencies: 317 | postcss: ^8.4.21 318 | 319 | postcss-load-config@4.0.2: 320 | resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 321 | engines: {node: '>= 14'} 322 | peerDependencies: 323 | postcss: '>=8.0.9' 324 | ts-node: '>=9.0.0' 325 | peerDependenciesMeta: 326 | postcss: 327 | optional: true 328 | ts-node: 329 | optional: true 330 | 331 | postcss-nested@6.2.0: 332 | resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} 333 | engines: {node: '>=12.0'} 334 | peerDependencies: 335 | postcss: ^8.2.14 336 | 337 | postcss-selector-parser@6.0.10: 338 | resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} 339 | engines: {node: '>=4'} 340 | 341 | postcss-selector-parser@6.1.2: 342 | resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} 343 | engines: {node: '>=4'} 344 | 345 | postcss-value-parser@4.2.0: 346 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 347 | 348 | postcss@8.4.49: 349 | resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} 350 | engines: {node: ^10 || ^12 || >=14} 351 | 352 | queue-microtask@1.2.3: 353 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 354 | 355 | read-cache@1.0.0: 356 | resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 357 | 358 | readdirp@3.6.0: 359 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 360 | engines: {node: '>=8.10.0'} 361 | 362 | resolve@1.22.10: 363 | resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} 364 | engines: {node: '>= 0.4'} 365 | hasBin: true 366 | 367 | reusify@1.0.4: 368 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 369 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 370 | 371 | run-parallel@1.2.0: 372 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 373 | 374 | shebang-command@2.0.0: 375 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 376 | engines: {node: '>=8'} 377 | 378 | shebang-regex@3.0.0: 379 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 380 | engines: {node: '>=8'} 381 | 382 | signal-exit@4.1.0: 383 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 384 | engines: {node: '>=14'} 385 | 386 | source-map-js@1.2.1: 387 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 388 | engines: {node: '>=0.10.0'} 389 | 390 | string-width@4.2.3: 391 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 392 | engines: {node: '>=8'} 393 | 394 | string-width@5.1.2: 395 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 396 | engines: {node: '>=12'} 397 | 398 | strip-ansi@6.0.1: 399 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 400 | engines: {node: '>=8'} 401 | 402 | strip-ansi@7.1.0: 403 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 404 | engines: {node: '>=12'} 405 | 406 | sucrase@3.35.0: 407 | resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 408 | engines: {node: '>=16 || 14 >=14.17'} 409 | hasBin: true 410 | 411 | supports-preserve-symlinks-flag@1.0.0: 412 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 413 | engines: {node: '>= 0.4'} 414 | 415 | tailwindcss@3.4.17: 416 | resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} 417 | engines: {node: '>=14.0.0'} 418 | hasBin: true 419 | 420 | thenify-all@1.6.0: 421 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 422 | engines: {node: '>=0.8'} 423 | 424 | thenify@3.3.1: 425 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 426 | 427 | to-regex-range@5.0.1: 428 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 429 | engines: {node: '>=8.0'} 430 | 431 | ts-interface-checker@0.1.13: 432 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 433 | 434 | util-deprecate@1.0.2: 435 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 436 | 437 | which@2.0.2: 438 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 439 | engines: {node: '>= 8'} 440 | hasBin: true 441 | 442 | wrap-ansi@7.0.0: 443 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 444 | engines: {node: '>=10'} 445 | 446 | wrap-ansi@8.1.0: 447 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 448 | engines: {node: '>=12'} 449 | 450 | yaml@2.6.1: 451 | resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} 452 | engines: {node: '>= 14'} 453 | hasBin: true 454 | 455 | snapshots: 456 | 457 | '@alloc/quick-lru@5.2.0': {} 458 | 459 | '@isaacs/cliui@8.0.2': 460 | dependencies: 461 | string-width: 5.1.2 462 | string-width-cjs: string-width@4.2.3 463 | strip-ansi: 7.1.0 464 | strip-ansi-cjs: strip-ansi@6.0.1 465 | wrap-ansi: 8.1.0 466 | wrap-ansi-cjs: wrap-ansi@7.0.0 467 | 468 | '@jridgewell/gen-mapping@0.3.8': 469 | dependencies: 470 | '@jridgewell/set-array': 1.2.1 471 | '@jridgewell/sourcemap-codec': 1.5.0 472 | '@jridgewell/trace-mapping': 0.3.25 473 | 474 | '@jridgewell/resolve-uri@3.1.2': {} 475 | 476 | '@jridgewell/set-array@1.2.1': {} 477 | 478 | '@jridgewell/sourcemap-codec@1.5.0': {} 479 | 480 | '@jridgewell/trace-mapping@0.3.25': 481 | dependencies: 482 | '@jridgewell/resolve-uri': 3.1.2 483 | '@jridgewell/sourcemap-codec': 1.5.0 484 | 485 | '@nodelib/fs.scandir@2.1.5': 486 | dependencies: 487 | '@nodelib/fs.stat': 2.0.5 488 | run-parallel: 1.2.0 489 | 490 | '@nodelib/fs.stat@2.0.5': {} 491 | 492 | '@nodelib/fs.walk@1.2.8': 493 | dependencies: 494 | '@nodelib/fs.scandir': 2.1.5 495 | fastq: 1.18.0 496 | 497 | '@pkgjs/parseargs@0.11.0': 498 | optional: true 499 | 500 | '@tailwindcss/typography@0.5.15(tailwindcss@3.4.17)': 501 | dependencies: 502 | lodash.castarray: 4.4.0 503 | lodash.isplainobject: 4.0.6 504 | lodash.merge: 4.6.2 505 | postcss-selector-parser: 6.0.10 506 | tailwindcss: 3.4.17 507 | 508 | ansi-regex@5.0.1: {} 509 | 510 | ansi-regex@6.1.0: {} 511 | 512 | ansi-styles@4.3.0: 513 | dependencies: 514 | color-convert: 2.0.1 515 | 516 | ansi-styles@6.2.1: {} 517 | 518 | any-promise@1.3.0: {} 519 | 520 | anymatch@3.1.3: 521 | dependencies: 522 | normalize-path: 3.0.0 523 | picomatch: 2.3.1 524 | 525 | arg@5.0.2: {} 526 | 527 | balanced-match@1.0.2: {} 528 | 529 | binary-extensions@2.3.0: {} 530 | 531 | brace-expansion@2.0.1: 532 | dependencies: 533 | balanced-match: 1.0.2 534 | 535 | braces@3.0.3: 536 | dependencies: 537 | fill-range: 7.1.1 538 | 539 | camelcase-css@2.0.1: {} 540 | 541 | chokidar@3.6.0: 542 | dependencies: 543 | anymatch: 3.1.3 544 | braces: 3.0.3 545 | glob-parent: 5.1.2 546 | is-binary-path: 2.1.0 547 | is-glob: 4.0.3 548 | normalize-path: 3.0.0 549 | readdirp: 3.6.0 550 | optionalDependencies: 551 | fsevents: 2.3.3 552 | 553 | color-convert@2.0.1: 554 | dependencies: 555 | color-name: 1.1.4 556 | 557 | color-name@1.1.4: {} 558 | 559 | commander@4.1.1: {} 560 | 561 | cross-spawn@7.0.6: 562 | dependencies: 563 | path-key: 3.1.1 564 | shebang-command: 2.0.0 565 | which: 2.0.2 566 | 567 | cssesc@3.0.0: {} 568 | 569 | didyoumean@1.2.2: {} 570 | 571 | dlv@1.1.3: {} 572 | 573 | eastasianwidth@0.2.0: {} 574 | 575 | emoji-regex@8.0.0: {} 576 | 577 | emoji-regex@9.2.2: {} 578 | 579 | fast-glob@3.3.2: 580 | dependencies: 581 | '@nodelib/fs.stat': 2.0.5 582 | '@nodelib/fs.walk': 1.2.8 583 | glob-parent: 5.1.2 584 | merge2: 1.4.1 585 | micromatch: 4.0.8 586 | 587 | fastq@1.18.0: 588 | dependencies: 589 | reusify: 1.0.4 590 | 591 | fill-range@7.1.1: 592 | dependencies: 593 | to-regex-range: 5.0.1 594 | 595 | foreground-child@3.3.0: 596 | dependencies: 597 | cross-spawn: 7.0.6 598 | signal-exit: 4.1.0 599 | 600 | fsevents@2.3.3: 601 | optional: true 602 | 603 | function-bind@1.1.2: {} 604 | 605 | glob-parent@5.1.2: 606 | dependencies: 607 | is-glob: 4.0.3 608 | 609 | glob-parent@6.0.2: 610 | dependencies: 611 | is-glob: 4.0.3 612 | 613 | glob@10.4.5: 614 | dependencies: 615 | foreground-child: 3.3.0 616 | jackspeak: 3.4.3 617 | minimatch: 9.0.5 618 | minipass: 7.1.2 619 | package-json-from-dist: 1.0.1 620 | path-scurry: 1.11.1 621 | 622 | hasown@2.0.2: 623 | dependencies: 624 | function-bind: 1.1.2 625 | 626 | is-binary-path@2.1.0: 627 | dependencies: 628 | binary-extensions: 2.3.0 629 | 630 | is-core-module@2.16.1: 631 | dependencies: 632 | hasown: 2.0.2 633 | 634 | is-extglob@2.1.1: {} 635 | 636 | is-fullwidth-code-point@3.0.0: {} 637 | 638 | is-glob@4.0.3: 639 | dependencies: 640 | is-extglob: 2.1.1 641 | 642 | is-number@7.0.0: {} 643 | 644 | isexe@2.0.0: {} 645 | 646 | jackspeak@3.4.3: 647 | dependencies: 648 | '@isaacs/cliui': 8.0.2 649 | optionalDependencies: 650 | '@pkgjs/parseargs': 0.11.0 651 | 652 | jiti@1.21.7: {} 653 | 654 | lilconfig@3.1.3: {} 655 | 656 | lines-and-columns@1.2.4: {} 657 | 658 | lodash.castarray@4.4.0: {} 659 | 660 | lodash.isplainobject@4.0.6: {} 661 | 662 | lodash.merge@4.6.2: {} 663 | 664 | lru-cache@10.4.3: {} 665 | 666 | merge2@1.4.1: {} 667 | 668 | micromatch@4.0.8: 669 | dependencies: 670 | braces: 3.0.3 671 | picomatch: 2.3.1 672 | 673 | minimatch@9.0.5: 674 | dependencies: 675 | brace-expansion: 2.0.1 676 | 677 | minipass@7.1.2: {} 678 | 679 | mz@2.7.0: 680 | dependencies: 681 | any-promise: 1.3.0 682 | object-assign: 4.1.1 683 | thenify-all: 1.6.0 684 | 685 | nanoid@3.3.8: {} 686 | 687 | normalize-path@3.0.0: {} 688 | 689 | object-assign@4.1.1: {} 690 | 691 | object-hash@3.0.0: {} 692 | 693 | package-json-from-dist@1.0.1: {} 694 | 695 | path-key@3.1.1: {} 696 | 697 | path-parse@1.0.7: {} 698 | 699 | path-scurry@1.11.1: 700 | dependencies: 701 | lru-cache: 10.4.3 702 | minipass: 7.1.2 703 | 704 | picocolors@1.1.1: {} 705 | 706 | picomatch@2.3.1: {} 707 | 708 | pify@2.3.0: {} 709 | 710 | pirates@4.0.6: {} 711 | 712 | postcss-import@15.1.0(postcss@8.4.49): 713 | dependencies: 714 | postcss: 8.4.49 715 | postcss-value-parser: 4.2.0 716 | read-cache: 1.0.0 717 | resolve: 1.22.10 718 | 719 | postcss-js@4.0.1(postcss@8.4.49): 720 | dependencies: 721 | camelcase-css: 2.0.1 722 | postcss: 8.4.49 723 | 724 | postcss-load-config@4.0.2(postcss@8.4.49): 725 | dependencies: 726 | lilconfig: 3.1.3 727 | yaml: 2.6.1 728 | optionalDependencies: 729 | postcss: 8.4.49 730 | 731 | postcss-nested@6.2.0(postcss@8.4.49): 732 | dependencies: 733 | postcss: 8.4.49 734 | postcss-selector-parser: 6.1.2 735 | 736 | postcss-selector-parser@6.0.10: 737 | dependencies: 738 | cssesc: 3.0.0 739 | util-deprecate: 1.0.2 740 | 741 | postcss-selector-parser@6.1.2: 742 | dependencies: 743 | cssesc: 3.0.0 744 | util-deprecate: 1.0.2 745 | 746 | postcss-value-parser@4.2.0: {} 747 | 748 | postcss@8.4.49: 749 | dependencies: 750 | nanoid: 3.3.8 751 | picocolors: 1.1.1 752 | source-map-js: 1.2.1 753 | 754 | queue-microtask@1.2.3: {} 755 | 756 | read-cache@1.0.0: 757 | dependencies: 758 | pify: 2.3.0 759 | 760 | readdirp@3.6.0: 761 | dependencies: 762 | picomatch: 2.3.1 763 | 764 | resolve@1.22.10: 765 | dependencies: 766 | is-core-module: 2.16.1 767 | path-parse: 1.0.7 768 | supports-preserve-symlinks-flag: 1.0.0 769 | 770 | reusify@1.0.4: {} 771 | 772 | run-parallel@1.2.0: 773 | dependencies: 774 | queue-microtask: 1.2.3 775 | 776 | shebang-command@2.0.0: 777 | dependencies: 778 | shebang-regex: 3.0.0 779 | 780 | shebang-regex@3.0.0: {} 781 | 782 | signal-exit@4.1.0: {} 783 | 784 | source-map-js@1.2.1: {} 785 | 786 | string-width@4.2.3: 787 | dependencies: 788 | emoji-regex: 8.0.0 789 | is-fullwidth-code-point: 3.0.0 790 | strip-ansi: 6.0.1 791 | 792 | string-width@5.1.2: 793 | dependencies: 794 | eastasianwidth: 0.2.0 795 | emoji-regex: 9.2.2 796 | strip-ansi: 7.1.0 797 | 798 | strip-ansi@6.0.1: 799 | dependencies: 800 | ansi-regex: 5.0.1 801 | 802 | strip-ansi@7.1.0: 803 | dependencies: 804 | ansi-regex: 6.1.0 805 | 806 | sucrase@3.35.0: 807 | dependencies: 808 | '@jridgewell/gen-mapping': 0.3.8 809 | commander: 4.1.1 810 | glob: 10.4.5 811 | lines-and-columns: 1.2.4 812 | mz: 2.7.0 813 | pirates: 4.0.6 814 | ts-interface-checker: 0.1.13 815 | 816 | supports-preserve-symlinks-flag@1.0.0: {} 817 | 818 | tailwindcss@3.4.17: 819 | dependencies: 820 | '@alloc/quick-lru': 5.2.0 821 | arg: 5.0.2 822 | chokidar: 3.6.0 823 | didyoumean: 1.2.2 824 | dlv: 1.1.3 825 | fast-glob: 3.3.2 826 | glob-parent: 6.0.2 827 | is-glob: 4.0.3 828 | jiti: 1.21.7 829 | lilconfig: 3.1.3 830 | micromatch: 4.0.8 831 | normalize-path: 3.0.0 832 | object-hash: 3.0.0 833 | picocolors: 1.1.1 834 | postcss: 8.4.49 835 | postcss-import: 15.1.0(postcss@8.4.49) 836 | postcss-js: 4.0.1(postcss@8.4.49) 837 | postcss-load-config: 4.0.2(postcss@8.4.49) 838 | postcss-nested: 6.2.0(postcss@8.4.49) 839 | postcss-selector-parser: 6.1.2 840 | resolve: 1.22.10 841 | sucrase: 3.35.0 842 | transitivePeerDependencies: 843 | - ts-node 844 | 845 | thenify-all@1.6.0: 846 | dependencies: 847 | thenify: 3.3.1 848 | 849 | thenify@3.3.1: 850 | dependencies: 851 | any-promise: 1.3.0 852 | 853 | to-regex-range@5.0.1: 854 | dependencies: 855 | is-number: 7.0.0 856 | 857 | ts-interface-checker@0.1.13: {} 858 | 859 | util-deprecate@1.0.2: {} 860 | 861 | which@2.0.2: 862 | dependencies: 863 | isexe: 2.0.0 864 | 865 | wrap-ansi@7.0.0: 866 | dependencies: 867 | ansi-styles: 4.3.0 868 | string-width: 4.2.3 869 | strip-ansi: 6.0.1 870 | 871 | wrap-ansi@8.1.0: 872 | dependencies: 873 | ansi-styles: 6.2.1 874 | string-width: 5.1.2 875 | strip-ansi: 7.1.0 876 | 877 | yaml@2.6.1: {} 878 | -------------------------------------------------------------------------------- /screenshots/markdownbrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackstorm/markdownbrain/c9ce7560c1f81c83c6698912a23f50923d492707/screenshots/markdownbrain.png -------------------------------------------------------------------------------- /server.config.example: -------------------------------------------------------------------------------- 1 | root_note_name: "Welcome" 2 | lang: "en" 3 | name: "MarkdownBrain" 4 | description: "MarkdownBrain" 5 | api_key: "1234567890" 6 | # Use CDN 7 | # htmx_js_url: "https://cdn.jsdelivr.net/npm/htmx.org@2.0.4/dist/htmx.min.js" 8 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | DEV_MODE=true 2 | PORT=3000 3 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | www/data/* 3 | !www/data/.keep -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-bookworm AS builder 2 | 3 | WORKDIR /markdownbrain 4 | 5 | COPY . . 6 | 7 | RUN go build -o markdownbrain server/main.go 8 | 9 | FROM debian:bookworm 10 | 11 | WORKDIR /markdownbrain 12 | 13 | COPY --from=builder /markdownbrain/server/www/static /markdownbrain/static 14 | COPY --from=builder /markdownbrain/server/www/config.yml /markdownbrain/config.yml 15 | COPY --from=builder /markdownbrain/markdownbrain /markdownbrain/markdownbrain 16 | 17 | EXPOSE ${PORT:-3000} 18 | 19 | CMD ["./markdownbrain"] 20 | -------------------------------------------------------------------------------- /server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Name string `yaml:"name"` 5 | Description string `yaml:"description"` 6 | Lang string `yaml:"lang"` 7 | RootNoteName string `yaml:"root_note_name"` 8 | APIKey string `yaml:"api_key"` 9 | HtmxJsUrl string `yaml:"htmx_js_url"` 10 | Templates []string `yaml:"templates"` 11 | } 12 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blackstorm/markdownbrain/server 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.52.5 7 | github.com/gofiber/template/django/v3 v3.1.12 8 | github.com/joho/godotenv v1.5.1 9 | ) 10 | 11 | require ( 12 | github.com/andybalholm/brotli v1.0.5 // indirect 13 | github.com/flosch/pongo2/v6 v6.0.0 // indirect 14 | github.com/gofiber/template v1.8.3 // indirect 15 | github.com/gofiber/utils v1.1.0 // indirect 16 | github.com/google/uuid v1.6.0 // indirect 17 | github.com/klauspost/compress v1.17.0 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.20 // indirect 20 | github.com/mattn/go-runewidth v0.0.15 // indirect 21 | github.com/rivo/uniseg v0.2.0 // indirect 22 | github.com/valyala/bytebufferpool v1.0.0 // indirect 23 | github.com/valyala/fasthttp v1.51.0 // indirect 24 | github.com/valyala/tcplisten v1.0.0 // indirect 25 | golang.org/x/sys v0.22.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= 5 | github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= 6 | github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= 7 | github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 8 | github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= 9 | github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= 10 | github.com/gofiber/template/django/v3 v3.1.12 h1:w2jxm9bJajhvrroXqEmUmakbvDSlzjpHgOI8yyh2iJs= 11 | github.com/gofiber/template/django/v3 v3.1.12/go.mod h1:4YNpM+LJ/el+cjUpdulp8lOH6dxZ2jaQmrF4E2/KGAk= 12 | github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= 13 | github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= 14 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 15 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 16 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 17 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 18 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 19 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 20 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 21 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 22 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 23 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 24 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 25 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 26 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 27 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 30 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 31 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 32 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 33 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 34 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 35 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 36 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 37 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 38 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 41 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 42 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/blackstorm/markdownbrain/common" 15 | "github.com/blackstorm/markdownbrain/server/config" 16 | "github.com/gofiber/fiber/v2" 17 | "github.com/gofiber/fiber/v2/middleware/favicon" 18 | "github.com/gofiber/template/django/v3" 19 | _ "github.com/joho/godotenv/autoload" 20 | ) 21 | 22 | type AppState struct { 23 | db *common.DB 24 | config *config.Config 25 | rootNoteId string 26 | } 27 | 28 | //go:embed templates 29 | var templatesAssets embed.FS 30 | 31 | func main() { 32 | devMode := os.Getenv("DEV_MODE") == "true" 33 | 34 | workspace := "/markdownbrain" 35 | if devMode { 36 | currentDir, err := os.Getwd() 37 | if err != nil { 38 | panic(err) 39 | } 40 | workspace = filepath.Join(currentDir, "www") 41 | } 42 | 43 | configPath := filepath.Join(workspace, "config.yml") 44 | dbPath := filepath.Join(workspace, "/data/notes.db") 45 | 46 | log.Printf("Workspace path: %s", workspace) 47 | log.Printf("Config path: %s", configPath) 48 | log.Printf("Database path: %s", dbPath) 49 | 50 | config, err := loadConfig(configPath) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | db, err := common.NewDB(dbPath, true) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | rootNoteHashId := common.NewSqidsIdGenerator().Generate(config.RootNoteName) 61 | state := &AppState{ 62 | db: db, 63 | config: config, 64 | rootNoteId: rootNoteHashId, 65 | } 66 | 67 | // Use views example: django.New("./templates", ".html"), 68 | app := fiber.New(fiber.Config{ 69 | Views: django.NewPathForwardingFileSystem(http.FS(templatesAssets), "/templates", ".html"), 70 | }) 71 | 72 | app.Use(favicon.New(favicon.Config{ 73 | File: filepath.Join(workspace, "/static/favicon.ico"), 74 | URL: "/favicon.ico", 75 | })) 76 | 77 | app.Static("/static", filepath.Join(workspace, "static")) 78 | app.Get("/", withAppState(state, home)) 79 | app.Post("/api/sync", withAuthorization(state, sync)) 80 | app.Get("/:id", withAppState(state, note)) 81 | app.Get("/*", withAppState(state, notes)) 82 | 83 | port := os.Getenv("PORT") 84 | if port == "" { 85 | port = "3000" 86 | } 87 | 88 | app.Listen(fmt.Sprintf(":%s", port)) 89 | } 90 | 91 | // loadConfig loads the application configuration from the specified www path 92 | func loadConfig(configPath string) (*config.Config, error) { 93 | var conf config.Config 94 | if err := common.ParseYAMLConfig(configPath, &conf); err != nil { 95 | return nil, err 96 | } 97 | return &conf, nil 98 | } 99 | 100 | // home handles the root path ("/") request 101 | // It either shows the welcome page for empty notes or renders the root note 102 | func home(state *AppState, c *fiber.Ctx) error { 103 | note, err := state.db.GetNote(state.rootNoteId) 104 | if err != nil { 105 | return fiber.ErrInternalServerError 106 | } 107 | 108 | if note == nil { 109 | count, err := state.db.CountNote() 110 | if err != nil { 111 | return c.Render("500", fiber.Map{}) 112 | } 113 | 114 | is_notes_empty := "true" 115 | if count > 0 { 116 | is_notes_empty = "false" 117 | } 118 | 119 | data := templateValues(state, fiber.Map{ 120 | "root_note_name": state.config.RootNoteName, 121 | "is_notes_empty": is_notes_empty, 122 | }) 123 | 124 | return c.Render("welcome", data) 125 | } 126 | 127 | data := templateValues(state, fiber.Map{ 128 | "title": state.config.Name, 129 | "description": state.config.Description, 130 | "notes": []common.Note{*note}, 131 | }) 132 | 133 | return c.Render("home", data) 134 | } 135 | 136 | // note handles single note display requests 137 | // It supports both regular HTTP requests and HTMX requests with proper URL handling 138 | func note(state *AppState, c *fiber.Ctx) error { 139 | noteId := c.Params("id") 140 | fromNoteId := c.Get("X-From-Note-Id") 141 | currentURL := c.Get("HX-Current-URL") 142 | 143 | isHtmxReq := c.Get("HX-Request") == "true" 144 | 145 | if isHtmxReq && (fromNoteId == "" || currentURL == "") { 146 | return fiber.ErrBadRequest 147 | } 148 | 149 | note, err := state.db.GetNote(noteId) 150 | if err != nil { 151 | return fiber.ErrInternalServerError 152 | } 153 | 154 | if note == nil { 155 | return fiber.ErrNotFound 156 | } 157 | 158 | if isHtmxReq { 159 | var pushURL strings.Builder 160 | 161 | parsedURL, err := url.Parse(currentURL) 162 | if err != nil { 163 | return fiber.ErrBadRequest 164 | } 165 | currentPath := parsedURL.Path 166 | 167 | if currentPath == "/" { 168 | pushURL.WriteString(fmt.Sprintf("/%s/%s", state.rootNoteId, noteId)) 169 | } else { 170 | pathParts := strings.Split(currentPath, "/") 171 | for _, part := range pathParts { 172 | if part == "" { 173 | continue 174 | } 175 | pushURL.WriteString("/") 176 | pushURL.WriteString(part) 177 | if part == fromNoteId { 178 | pushURL.WriteString("/") 179 | pushURL.WriteString(noteId) 180 | break 181 | } 182 | } 183 | } 184 | 185 | linkToThis, err := state.db.GetNotesByLinkTo(noteId) 186 | if err != nil { 187 | return fiber.ErrInternalServerError 188 | } 189 | note.LinkToThis = linkToThis 190 | 191 | c.Set("HX-Push-Url", pushURL.String()) 192 | return c.Render("note", fiber.Map{ 193 | "note": note, 194 | }) 195 | } 196 | 197 | note.LoadLinkToThisNotes(state.db) 198 | 199 | data := templateValues(state, fiber.Map{ 200 | "title": note.Title, 201 | "description": note.Description, 202 | "notes": []common.Note{*note}, 203 | }) 204 | 205 | return c.Render("home", data) 206 | } 207 | 208 | // notes handles requests for displaying multiple notes 209 | // It processes the path to extract note IDs and renders them in a combined view 210 | func notes(state *AppState, c *fiber.Ctx) error { 211 | // Deduplication note id 212 | seen := make(map[string]bool) 213 | ids := make([]string, 0) 214 | for _, id := range strings.Split(c.Path(), "/") { 215 | if id != "" && !seen[id] { 216 | seen[id] = true 217 | ids = append(ids, id) 218 | } 219 | } 220 | 221 | notes, err := state.db.GetNotesByIDs(ids) 222 | if err != nil { 223 | return fiber.ErrInternalServerError 224 | } 225 | 226 | // Reorder notes according to ids order 227 | orderedNotes := make([]common.Note, len(ids)) 228 | noteMap := make(map[string]common.Note) 229 | for _, note := range notes { 230 | noteMap[note.ID] = note 231 | } 232 | for i, id := range ids { 233 | if note, ok := noteMap[id]; ok { 234 | orderedNotes[i] = note 235 | } 236 | } 237 | notes = orderedNotes 238 | 239 | for i := range notes { 240 | notes[i].LoadLinkToThisNotes(state.db) 241 | } 242 | 243 | title := fmt.Sprintf("%s - %s", common.Notes(notes).Titles(), state.config.Name) 244 | 245 | data := templateValues(state, fiber.Map{ 246 | "title": title, 247 | "description": title, 248 | "notes": notes, 249 | }) 250 | 251 | return c.Render("home", data) 252 | } 253 | 254 | func templateValues(state *AppState, values fiber.Map) fiber.Map { 255 | res := fiber.Map{ 256 | "config": fiber.Map{ 257 | "lang": state.config.Lang, 258 | "name": state.config.Name, 259 | "description": state.config.Description, 260 | "templates": state.config.Templates, 261 | }, 262 | } 263 | 264 | for key, value := range values { 265 | res[key] = value 266 | } 267 | 268 | return res 269 | } 270 | 271 | // sync handles database synchronization requests 272 | // It accepts a database file upload and updates the server's database 273 | func sync(state *AppState, c *fiber.Ctx) error { 274 | file, err := c.FormFile("db") 275 | if err != nil { 276 | return fiber.ErrBadRequest 277 | } 278 | 279 | uploadedFile, err := file.Open() 280 | if err != nil { 281 | return fiber.ErrInternalServerError 282 | } 283 | defer uploadedFile.Close() 284 | 285 | bytes, err := io.ReadAll(uploadedFile) 286 | if err != nil { 287 | return fiber.ErrInternalServerError 288 | } 289 | 290 | if err := state.db.FromBytes(bytes); err != nil { 291 | return fiber.ErrInternalServerError 292 | } 293 | 294 | return nil 295 | } 296 | 297 | type withStateHandler func(state *AppState, c *fiber.Ctx) error 298 | 299 | // withAppState is a middleware that injects the application state into request handlers 300 | func withAppState(state *AppState, handler withStateHandler) fiber.Handler { 301 | return func(c *fiber.Ctx) error { 302 | return handler(state, c) 303 | } 304 | } 305 | 306 | // withAuthorization is a middleware that validates API key authentication 307 | // It checks for a valid Bearer token in the Authorization header 308 | func withAuthorization(state *AppState, handler withStateHandler) fiber.Handler { 309 | return func(c *fiber.Ctx) error { 310 | apiKey := c.Get("Authorization") 311 | if apiKey == "" { 312 | return fiber.ErrUnauthorized 313 | } 314 | 315 | apiKey = strings.TrimPrefix(apiKey, "Bearer ") 316 | 317 | if apiKey != state.config.APIKey { 318 | return fiber.ErrUnauthorized 319 | } 320 | 321 | return handler(state, c) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /server/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |{{ message }}
14 | {% else %} 15 |Page not found
16 | {% endif %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /server/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |Last updated: {{ note.LastUpdatedAt }}.
9 |Please config your server, and sync your notes.
67 |Server:
77 | If your already configured, please ensure your root note name correct. 78 |
79 |{{ root_note_name }}