feat(markdown-editor): implement wysiswyg markdown editor with live preview
- Build Go backend with Cobra CLI and REST API - CRUD operations for markdown files (GET, POST, PUT, DELETE) - File storage with flat .md file structure - Comprehensive logrus logging with JSON format - Static asset serving for frontend - Build React/TypeScript frontend with Tailwind CSS - Markdown editor with live GFM preview - File management UI (list, create, open, delete) - Theme system (Dark/Light/System) with persistence - Responsive design (320px mobile, 1920px desktop) - Add comprehensive test coverage - Backend: API, storage, and logger tests (13 tests passing) - Frontend: Editor and App component tests - Setup Nix development environment with Go, Node.js, and TypeScript
This commit is contained in:
74
backend/internal/storage/storage.go
Normal file
74
backend/internal/storage/storage.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/markdown-editor/pkg/logger"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
dataDir string
|
||||
}
|
||||
|
||||
func NewStorage(dataDir string) *Storage {
|
||||
// Ensure data directory exists
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
logger.Fatalf("Failed to create data directory: %v", err)
|
||||
}
|
||||
return &Storage{dataDir: dataDir}
|
||||
}
|
||||
|
||||
func (s *Storage) ListFiles() ([]string, error) {
|
||||
files, err := os.ReadDir(s.dataDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read directory: %w", err)
|
||||
}
|
||||
|
||||
var mdFiles []string
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && strings.HasSuffix(file.Name(), ".md") {
|
||||
mdFiles = append(mdFiles, file.Name())
|
||||
}
|
||||
}
|
||||
return mdFiles, nil
|
||||
}
|
||||
|
||||
func (s *Storage) GetFile(filename string) (string, error) {
|
||||
path := filepath.Join(s.dataDir, filename)
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("file not found: %s", filename)
|
||||
}
|
||||
return "", fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func (s *Storage) SaveFile(filename, content string) error {
|
||||
path := filepath.Join(s.dataDir, filename)
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteFile(filename string) error {
|
||||
path := filepath.Join(s.dataDir, filename)
|
||||
if err := os.Remove(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("file not found: %s", filename)
|
||||
}
|
||||
return fmt.Errorf("failed to delete file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) Exists(filename string) bool {
|
||||
path := filepath.Join(s.dataDir, filename)
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
141
backend/internal/storage/storage_test.go
Normal file
141
backend/internal/storage/storage_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupTestStorage(t *testing.T) *Storage {
|
||||
tempDir := t.TempDir()
|
||||
return NewStorage(tempDir)
|
||||
}
|
||||
|
||||
func TestListFiles(t *testing.T) {
|
||||
storage := setupTestStorage(t)
|
||||
|
||||
// Create test files
|
||||
content := "Test content"
|
||||
testFiles := []string{"test1.md", "test2.md", "notes.md"}
|
||||
|
||||
for _, filename := range testFiles {
|
||||
path := filepath.Join(storage.dataDir, filename)
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
files, err := storage.ListFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list files: %v", err)
|
||||
}
|
||||
|
||||
if len(files) != 3 {
|
||||
t.Errorf("Expected 3 files, got %d", len(files))
|
||||
}
|
||||
|
||||
expected := map[string]bool{"test1.md": true, "test2.md": true, "notes.md": true}
|
||||
for _, file := range files {
|
||||
if !expected[file] {
|
||||
t.Errorf("Unexpected file: %s", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFile(t *testing.T) {
|
||||
storage := setupTestStorage(t)
|
||||
|
||||
filename := "testfile.md"
|
||||
content := "# Test Heading\n\nTest content."
|
||||
path := filepath.Join(storage.dataDir, filename)
|
||||
os.WriteFile(path, []byte(content), 0644)
|
||||
|
||||
fileContent, err := storage.GetFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get file: %v", err)
|
||||
}
|
||||
|
||||
if fileContent != content {
|
||||
t.Errorf("Expected content %q, got %q", content, fileContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFileNotFound(t *testing.T) {
|
||||
storage := setupTestStorage(t)
|
||||
|
||||
_, err := storage.GetFile("nonexistent.md")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveFile(t *testing.T) {
|
||||
storage := setupTestStorage(t)
|
||||
|
||||
filename := "newfile.md"
|
||||
content := "# New File\n\nContent here."
|
||||
|
||||
err := storage.SaveFile(filename, content)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to save file: %v", err)
|
||||
}
|
||||
|
||||
// Verify file was saved
|
||||
path := filepath.Join(storage.dataDir, filename)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
t.Error("File was not saved")
|
||||
}
|
||||
|
||||
// Verify content
|
||||
storedContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read file: %v", err)
|
||||
}
|
||||
if string(storedContent) != content {
|
||||
t.Error("File content does not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFile(t *testing.T) {
|
||||
storage := setupTestStorage(t)
|
||||
|
||||
filename := "todelete.md"
|
||||
content := "To be deleted."
|
||||
path := filepath.Join(storage.dataDir, filename)
|
||||
os.WriteFile(path, []byte(content), 0644)
|
||||
|
||||
err := storage.DeleteFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete file: %v", err)
|
||||
}
|
||||
|
||||
// Verify file was deleted
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Error("File was not deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFileNotFound(t *testing.T) {
|
||||
storage := setupTestStorage(t)
|
||||
|
||||
err := storage.DeleteFile("nonexistent.md")
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExists(t *testing.T) {
|
||||
storage := setupTestStorage(t)
|
||||
|
||||
filename := "exists.md"
|
||||
path := filepath.Join(storage.dataDir, filename)
|
||||
os.WriteFile(path, []byte("content"), 0644)
|
||||
|
||||
if !storage.Exists(filename) {
|
||||
t.Error("File should exist")
|
||||
}
|
||||
|
||||
if storage.Exists("nonexistent.md") {
|
||||
t.Error("Non-existent file should not exist")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user