From b0c4a33a7c1fa14163a4ec867d9c539ace914605 Mon Sep 17 00:00:00 2001 From: Joakim Date: Wed, 10 Sep 2025 20:19:54 +0200 Subject: [PATCH] feat: implement unified editor with content persistence and server-side upsert - Replace dual update systems with single markdown-first editor architecture - Add server-side upsert to eliminate 404 errors on PUT operations - Fix content persistence race condition between preview and save operations - Remove legacy updateElementContent system entirely - Add comprehensive authentication with JWT scaffolding and dev mode - Implement EditContext.updateOriginalContent() for proper baseline management - Enable markdown formatting in all text elements (h1-h6, p, div, etc) - Clean terminology: remove 'unified' references from codebase Technical changes: * core/editor.js: Remove legacy update system, unify content types as markdown * ui/Editor.js: Add updateOriginalContent() method to fix save persistence * ui/Previewer.js: Clean live preview system for all content types * api/handlers.go: Implement UpsertContent for idempotent PUT operations * auth/*: Complete authentication service with OAuth scaffolding * db/queries/content.sql: Add upsert query with ON CONFLICT handling * Schema: Remove type constraints, rely on server-side validation Result: Clean content editing with persistent saves, no 404 errors, markdown support in all text elements --- AGENTS.md | 31 + TODO.md | 120 ++ cmd/serve.go | 19 +- db/postgresql/schema.sql | 2 +- db/queries/content.sql | 9 + db/sqlite/schema.sql | 2 +- go.mod | 1 + go.sum | 2 + internal/api/handlers.go | 157 +- internal/auth/auth.go | 265 +++ internal/auth/context.go | 29 + internal/db/postgresql/content.sql.go | 39 + internal/db/postgresql/querier.go | 1 + internal/db/sqlite/content.sql.go | 39 + internal/db/sqlite/querier.go | 1 + lib/package-lock.json | 2375 ------------------------- lib/src/core/api-client.js | 174 +- lib/src/core/editor.js | 39 +- lib/src/ui/Editor.js | 492 +++++ lib/src/ui/Previewer.js | 157 ++ lib/src/ui/form-renderer.js | 810 +++------ lib/src/ui/markdown-editor.js | 446 ----- test-ids.html | 33 - 23 files changed, 1658 insertions(+), 3585 deletions(-) create mode 100644 AGENTS.md create mode 100644 internal/auth/auth.go create mode 100644 internal/auth/context.go create mode 100644 lib/src/ui/Editor.js create mode 100644 lib/src/ui/Previewer.js delete mode 100644 lib/src/ui/markdown-editor.js delete mode 100644 test-ids.html diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fb3bcaa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md - Developer Guide for Insertr + +## Build/Test Commands +- `just dev` - Full-stack development (recommended) +- `just build` - Build entire project (Go binary + JS library) +- `just build-lib` - Build JS library only +- `just test` - Run tests (placeholder, no actual tests yet) +- `just lint` - Run linting (placeholder, no actual linting yet) +- `just air` - Hot reload Go backend only +- `go test ./...` - Run Go tests (when available) + +## Code Style Guidelines + +### Go (Backend) +- Use standard Go formatting (`gofmt`) +- Package imports: stdlib → third-party → internal +- Error handling: always check and handle errors +- Naming: camelCase for local vars, PascalCase for exported +- Comments: package-level comments, exported functions documented + +### JavaScript (Frontend) +- ES6+ modules, no CommonJS +- camelCase for variables/functions, PascalCase for classes +- Use `const`/`let`, avoid `var` +- Prefer template literals over string concatenation +- Export classes/functions explicitly, avoid default exports when multiple exports exist + +### Database +- Use `sqlc` for Go database code generation +- SQL files in `db/queries/`, schemas in `db/{sqlite,postgresql}/schema.sql` +- Run `just sqlc` after schema changes \ No newline at end of file diff --git a/TODO.md b/TODO.md index b7b70da..f558a17 100644 --- a/TODO.md +++ b/TODO.md @@ -242,3 +242,123 @@ CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id); - `db/*/setup.sql` - Contains table creation queries (sqlc generates type-safe functions) - `internal/db/database.go` - Manual index/trigger creation using raw SQL - **Best Practice**: Use sqlc for what it supports, manual SQL for what it doesn't + +## 🔄 **Editor Cache Architecture & Content State Management** (Dec 2024) + +### **Current Architecture Issues Identified** + +**Problem**: Conflict between preview system and content persistence after save operations. + +**Root Cause**: The unified Editor system was designed with preview-first architecture: +- `originalContent` stores DOM state when editing begins +- `clearPreview()` always restores to `originalContent` +- This creates race condition: `applyContent()` → `clearPreview()` → content reverted + +### **✅ Implemented Solution: Post-Save Baseline Update** + +**Strategy**: After successful save, update the stored `originalContent` to match the new saved state. + +**Implementation** (`lib/src/ui/Editor.js`): +```js +// In save handler: +context.applyContent(content); // Apply new content to DOM +context.updateOriginalContent(); // Update baseline to match current DOM +this.previewer.clearPreview(); // Clear preview (won't revert since baseline matches) +``` + +**Benefits**: +- ✅ Content persists after save operations +- ✅ Future cancellations restore to saved state, not pre-edit state +- ✅ Maintains clean preview functionality +- ✅ No breaking changes to existing architecture + +### **Future Considerations: Draft System Architecture** + +**Current State Management**: +- **Browser Cache**: DOM elements store current content state +- **Server Cache**: Database stores persisted content +- **No Intermediate Cache**: Edits are either preview (temporary) or saved (permanent) + +**Potential Draft System Design**: + +#### **Option 1: LocalStorage Drafts** +```js +// Auto-save drafts locally during editing +const draftKey = `insertr_draft_${contentId}_${siteId}`; +localStorage.setItem(draftKey, JSON.stringify({ + content: currentContent, + timestamp: Date.now(), + originalContent: baseline +})); +``` + +**Benefits**: Offline support, immediate feedback, no server load +**Drawbacks**: Per-browser, no cross-device sync, storage limits + +#### **Option 2: Server-Side Drafts** +```js +// Auto-save drafts to server with special draft flag +PUT /api/content/{id}/draft +{ + "value": "Draft content...", + "type": "markdown", + "is_draft": true +} +``` + +**Benefits**: Cross-device sync, unlimited storage, collaborative editing potential +**Drawbacks**: Server complexity, authentication requirements, network dependency + +#### **Option 3: Hybrid Draft System** +- **LocalStorage**: Immediate draft saving during typing +- **Server Sync**: Periodic sync of drafts (every 30s or on significant changes) +- **Conflict Resolution**: Handle cases where server content changed while editing draft + +### **Cache Invalidation Strategy** + +**Current Behavior**: +- Content is cached in DOM until page reload +- No automatic refresh when content changes on server +- No awareness of multi-user editing scenarios + +**Recommended Enhancements**: + +#### **Option A: Manual Refresh Strategy** +- Add "Refresh Content" button to editor interface +- Check server content before editing (warn if changed) +- Simple conflict resolution (server wins vs local wins vs merge) + +#### **Option B: Polling Strategy** +- Poll server every N minutes for content changes +- Show notification if content was updated by others +- Allow user to choose: keep editing, reload, or merge + +#### **Option C: WebSocket Strategy** +- Real-time content change notifications +- Live collaborative editing indicators +- Automatic conflict resolution + +### **Implementation Priority** + +**Phase 1** (Immediate): ✅ **COMPLETED** +- Fix content persistence after save (baseline update approach) + +**Phase 2** (Short-term): +- Add LocalStorage draft auto-save during editing +- Implement draft recovery on page reload +- Basic conflict detection (server timestamp vs local timestamp) + +**Phase 3** (Long-term): +- Server-side draft support +- Real-time collaboration features +- Advanced conflict resolution + +### **Design Principles for Draft System** + +1. **Progressive Enhancement**: Site works without drafts, drafts enhance UX +2. **Data Safety**: Never lose user content, even in edge cases +3. **Performance First**: Drafts shouldn't impact site loading for regular visitors +4. **Conflict Transparency**: Always show user what's happening with their content +5. **Graceful Degradation**: Fallback to basic editing if draft system fails + +**Note**: Current architecture already supports foundation for all these enhancements through the unified Editor system and API client pattern. diff --git a/cmd/serve.go b/cmd/serve.go index 666cd99..b7ac88e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -13,6 +13,7 @@ import ( "github.com/spf13/viper" "github.com/insertr/insertr/internal/api" + "github.com/insertr/insertr/internal/auth" "github.com/insertr/insertr/internal/db" ) @@ -51,8 +52,24 @@ func runServe(cmd *cobra.Command, args []string) { } defer database.Close() + // Initialize authentication service + authConfig := &auth.AuthConfig{ + DevMode: viper.GetBool("dev_mode"), + JWTSecret: viper.GetString("jwt_secret"), + } + + // Set default JWT secret if not configured + if authConfig.JWTSecret == "" { + authConfig.JWTSecret = "dev-secret-change-in-production" + if authConfig.DevMode { + log.Printf("🔑 Using default JWT secret for development") + } + } + + authService := auth.NewAuthService(authConfig) + // Initialize handlers - contentHandler := api.NewContentHandler(database) + contentHandler := api.NewContentHandler(database, authService) // Setup router router := mux.NewRouter() diff --git a/db/postgresql/schema.sql b/db/postgresql/schema.sql index b64133f..503c392 100644 --- a/db/postgresql/schema.sql +++ b/db/postgresql/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE content ( id TEXT NOT NULL, site_id TEXT NOT NULL, value TEXT NOT NULL, - type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')), + type TEXT NOT NULL, created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, last_edited_by TEXT DEFAULT 'system' NOT NULL, diff --git a/db/queries/content.sql b/db/queries/content.sql index 0bc22ff..98a7dbf 100644 --- a/db/queries/content.sql +++ b/db/queries/content.sql @@ -25,6 +25,15 @@ SET value = sqlc.arg(value), type = sqlc.arg(type), last_edited_by = sqlc.arg(la WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id) RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by; +-- name: UpsertContent :one +INSERT INTO content (id, site_id, value, type, last_edited_by) +VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(last_edited_by)) +ON CONFLICT(id, site_id) DO UPDATE SET + value = EXCLUDED.value, + type = EXCLUDED.type, + last_edited_by = EXCLUDED.last_edited_by +RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by; + -- name: DeleteContent :exec DELETE FROM content WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id); \ No newline at end of file diff --git a/db/sqlite/schema.sql b/db/sqlite/schema.sql index 88c4fe9..722ca2f 100644 --- a/db/sqlite/schema.sql +++ b/db/sqlite/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE content ( id TEXT NOT NULL, site_id TEXT NOT NULL, value TEXT NOT NULL, - type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')), + type TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, last_edited_by TEXT DEFAULT 'system' NOT NULL, diff --git a/go.mod b/go.mod index d4a61d4..cfe06b4 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect diff --git a/go.sum b/go.sum index 85b12f5..ea71084 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= diff --git a/internal/api/handlers.go b/internal/api/handlers.go index a4e289b..35d7d4c 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gorilla/mux" + "github.com/insertr/insertr/internal/auth" "github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/db/postgresql" "github.com/insertr/insertr/internal/db/sqlite" @@ -18,13 +19,15 @@ import ( // ContentHandler handles all content-related HTTP requests type ContentHandler struct { - database *db.Database + database *db.Database + authService *auth.AuthService } // NewContentHandler creates a new content handler -func NewContentHandler(database *db.Database) *ContentHandler { +func NewContentHandler(database *db.Database, authService *auth.AuthService) *ContentHandler { return &ContentHandler{ - database: database, + database: database, + authService: authService, } } @@ -173,14 +176,13 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) { siteID = "default" // final fallback } - // Extract user from request (for now, use X-User-ID header or fallback) - userID := r.Header.Get("X-User-ID") - if userID == "" && req.CreatedBy != "" { - userID = req.CreatedBy - } - if userID == "" { - userID = "anonymous" + // Extract user from request using authentication service + userInfo, authErr := h.authService.ExtractUserFromRequest(r) + if authErr != nil { + http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized) + return } + userID := userInfo.ID var content interface{} var err error @@ -219,7 +221,7 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(item) } -// UpdateContent handles PUT /api/content/{id} +// UpdateContent handles PUT /api/content/{id} with upsert functionality func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) contentID := vars["id"] @@ -236,29 +238,70 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) { return } - // Extract user from request - userID := r.Header.Get("X-User-ID") - if userID == "" && req.UpdatedBy != "" { - userID = req.UpdatedBy + // Extract user from request using authentication service + userInfo, authErr := h.authService.ExtractUserFromRequest(r) + if authErr != nil { + http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized) + return } - if userID == "" { - userID = "anonymous" + userID := userInfo.ID + + // Check if content exists for version history (non-blocking) + var existingContent interface{} + var contentExists bool + + switch h.database.GetDBType() { + case "sqlite3": + existingContent, _ = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{ + ID: contentID, + SiteID: siteID, + }) + contentExists = existingContent != nil + case "postgresql": + existingContent, _ = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{ + ID: contentID, + SiteID: siteID, + }) + contentExists = existingContent != nil } - // Get current content for version history and type preservation - var currentContent interface{} + // Archive existing version before upsert (only if content already exists) + if contentExists { + if err := h.createContentVersion(existingContent); err != nil { + // Log error but don't fail the request - version history is non-critical + fmt.Printf("Warning: Failed to create content version: %v\n", err) + } + } + + // Determine content type: use provided type, fallback to existing type, default to "text" + contentType := req.Type + if contentType == "" && contentExists { + contentType = h.getContentType(existingContent) + } + if contentType == "" { + contentType = "text" // default type for new content + } + + // Perform upsert operation + var upsertedContent interface{} var err error switch h.database.GetDBType() { case "sqlite3": - currentContent, err = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{ - ID: contentID, - SiteID: siteID, + upsertedContent, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{ + ID: contentID, + SiteID: siteID, + Value: req.Value, + Type: contentType, + LastEditedBy: userID, }) case "postgresql": - currentContent, err = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{ - ID: contentID, - SiteID: siteID, + upsertedContent, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{ + ID: contentID, + SiteID: siteID, + Value: req.Value, + Type: contentType, + LastEditedBy: userID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) @@ -266,58 +309,11 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) { } if err != nil { - if err == sql.ErrNoRows { - http.Error(w, "Content not found", http.StatusNotFound) - return - } - http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("Failed to upsert content: %v", err), http.StatusInternalServerError) return } - // Archive current version before updating - err = h.createContentVersion(currentContent) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to create version: %v", err), http.StatusInternalServerError) - return - } - - // Determine content type - contentType := req.Type - if contentType == "" { - contentType = h.getContentType(currentContent) // preserve existing type if not specified - } - - // Update the content - var updatedContent interface{} - - switch h.database.GetDBType() { - case "sqlite3": - updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{ - Value: req.Value, - Type: contentType, - LastEditedBy: userID, - ID: contentID, - SiteID: siteID, - }) - case "postgresql": - updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{ - Value: req.Value, - Type: contentType, - LastEditedBy: userID, - ID: contentID, - SiteID: siteID, - }) - default: - http.Error(w, "Unsupported database type", http.StatusInternalServerError) - return - } - - if err != nil { - http.Error(w, fmt.Sprintf("Failed to update content: %v", err), http.StatusInternalServerError) - return - } - - item := h.convertToAPIContent(updatedContent) + item := h.convertToAPIContent(upsertedContent) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(item) @@ -459,14 +455,13 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request) return } - // Extract user from request - userID := r.Header.Get("X-User-ID") - if userID == "" && req.RolledBackBy != "" { - userID = req.RolledBackBy - } - if userID == "" { - userID = "anonymous" + // Extract user from request using authentication service + userInfo, authErr := h.authService.ExtractUserFromRequest(r) + if authErr != nil { + http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized) + return } + userID := userInfo.ID // Archive current version before rollback var currentContent interface{} diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..8723633 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,265 @@ +package auth + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// UserInfo represents authenticated user information +type UserInfo struct { + ID string `json:"sub"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Provider string `json:"iss,omitempty"` +} + +// AuthConfig holds authentication configuration +type AuthConfig struct { + DevMode bool + JWTSecret string + OAuthConfigs map[string]OAuthConfig +} + +// OAuthConfig holds OAuth provider configuration +type OAuthConfig struct { + ClientID string + ClientSecret string + RedirectURL string + Scopes []string +} + +// AuthService handles authentication operations +type AuthService struct { + config *AuthConfig +} + +// NewAuthService creates a new authentication service +func NewAuthService(config *AuthConfig) *AuthService { + return &AuthService{config: config} +} + +// ExtractUserFromRequest extracts user information from HTTP request +func (a *AuthService) ExtractUserFromRequest(r *http.Request) (*UserInfo, error) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + return &UserInfo{ID: "anonymous"}, nil + } + + // Parse Bearer token + if !strings.HasPrefix(authHeader, "Bearer ") { + return nil, fmt.Errorf("invalid authorization header format") + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + + // Handle mock tokens in development mode + if a.config.DevMode && strings.HasPrefix(token, "mock-") { + return a.parseMockToken(token), nil + } + + // Parse real JWT token + return a.parseJWT(token) +} + +// parseMockToken handles mock development tokens +func (a *AuthService) parseMockToken(token string) *UserInfo { + // Mock token format: mock-{user}-{timestamp}-{random} + parts := strings.Split(token, "-") + if len(parts) >= 2 { + return &UserInfo{ + ID: parts[1], // user part + Email: fmt.Sprintf("%s@localhost", parts[1]), + Name: strings.Title(parts[1]), + Provider: "insertr-dev", + } + } + + return &UserInfo{ID: "anonymous"} +} + +// parseJWT parses and validates a real JWT token +func (a *AuthService) parseJWT(tokenString string) (*UserInfo, error) { + // Parse the token + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Validate signing method + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(a.config.JWTSecret), nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to parse JWT: %w", err) + } + + // Extract claims + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + userInfo := &UserInfo{} + + // Extract standard JWT claims + if sub, ok := claims["sub"].(string); ok { + userInfo.ID = sub + } + if email, ok := claims["email"].(string); ok { + userInfo.Email = email + } + if name, ok := claims["name"].(string); ok { + userInfo.Name = name + } + if iss, ok := claims["iss"].(string); ok { + userInfo.Provider = iss + } + + // Fallback to alternative claim names + if userInfo.ID == "" { + if userID, ok := claims["user_id"].(string); ok { + userInfo.ID = userID + } + } + + // Default to anonymous if no user ID found + if userInfo.ID == "" { + userInfo.ID = "anonymous" + } + + return userInfo, nil + } + + return nil, fmt.Errorf("invalid JWT claims") +} + +// CreateMockJWT creates a mock JWT token for development/testing +func (a *AuthService) CreateMockJWT(userID, email, name string) (string, error) { + if !a.config.DevMode { + return "", fmt.Errorf("mock JWT creation only allowed in development mode") + } + + // Create the claims + claims := jwt.MapClaims{ + "sub": userID, + "email": email, + "name": name, + "iss": "insertr-dev", + "iat": time.Now().Unix(), + "exp": time.Now().Add(24 * time.Hour).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Sign and get the complete encoded token as a string + tokenString, err := token.SignedString([]byte(a.config.JWTSecret)) + if err != nil { + return "", fmt.Errorf("failed to sign JWT: %w", err) + } + + return tokenString, nil +} + +// ValidateToken validates a JWT token without extracting user info +func (a *AuthService) ValidateToken(tokenString string) error { + _, err := a.parseJWT(tokenString) + return err +} + +// RefreshToken creates a new token with extended expiration +func (a *AuthService) RefreshToken(tokenString string) (string, error) { + userInfo, err := a.parseJWT(tokenString) + if err != nil { + return "", fmt.Errorf("cannot refresh invalid token: %w", err) + } + + // Create new token with same user info but extended expiration + claims := jwt.MapClaims{ + "sub": userInfo.ID, + "email": userInfo.Email, + "name": userInfo.Name, + "iss": userInfo.Provider, + "iat": time.Now().Unix(), + "exp": time.Now().Add(24 * time.Hour).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(a.config.JWTSecret)) +} + +// IsAuthenticated checks if the request has valid authentication +func (a *AuthService) IsAuthenticated(r *http.Request) bool { + userInfo, err := a.ExtractUserFromRequest(r) + return err == nil && userInfo.ID != "anonymous" +} + +// RequireAuth middleware that requires authentication +func (a *AuthService) RequireAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userInfo, err := a.ExtractUserFromRequest(r) + if err != nil || userInfo.ID == "anonymous" { + http.Error(w, "Authentication required", http.StatusUnauthorized) + return + } + + // Add user info to request context for use by handlers + ctx := r.Context() + ctx = ContextWithUser(ctx, userInfo) + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + }) +} + +// OAuth flow handlers (to be implemented when adding real OAuth) + +// HandleOAuthLogin initiates OAuth flow +func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) { + provider := r.URL.Query().Get("provider") + if provider == "" { + provider = "google" + } + + // TODO: Implement OAuth initiation + // For now, return mock success in dev mode + if a.config.DevMode { + response := map[string]interface{}{ + "message": "OAuth login not yet implemented", + "redirect_url": "/auth/callback?code=mock_code&state=mock_state", + "dev_mode": true, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + http.Error(w, "OAuth not implemented", http.StatusNotImplemented) +} + +// HandleOAuthCallback handles OAuth provider callback +func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + _ = r.URL.Query().Get("state") // TODO: validate state parameter + + // TODO: Implement OAuth token exchange + // For now, return mock token in dev mode + if a.config.DevMode && code != "" { + mockToken, err := a.CreateMockJWT("dev-user", "dev@localhost", "Development User") + if err != nil { + http.Error(w, "Failed to create mock token", http.StatusInternalServerError) + return + } + + response := map[string]interface{}{ + "token": mockToken, + "dev_mode": true, + "message": "Mock OAuth callback successful", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + return + } + + http.Error(w, "OAuth callback not implemented", http.StatusNotImplemented) +} diff --git a/internal/auth/context.go b/internal/auth/context.go new file mode 100644 index 0000000..636b111 --- /dev/null +++ b/internal/auth/context.go @@ -0,0 +1,29 @@ +package auth + +import "context" + +// Context keys for user information +type contextKey string + +const ( + userInfoContextKey contextKey = "user_info" +) + +// ContextWithUser adds user information to request context +func ContextWithUser(ctx context.Context, user *UserInfo) context.Context { + return context.WithValue(ctx, userInfoContextKey, user) +} + +// UserFromContext extracts user information from request context +func UserFromContext(ctx context.Context) (*UserInfo, bool) { + user, ok := ctx.Value(userInfoContextKey).(*UserInfo) + return user, ok +} + +// UserIDFromContext extracts user ID from request context +func UserIDFromContext(ctx context.Context) string { + if user, ok := UserFromContext(ctx); ok && user != nil { + return user.ID + } + return "anonymous" +} diff --git a/internal/db/postgresql/content.sql.go b/internal/db/postgresql/content.sql.go index b3230c3..e307674 100644 --- a/internal/db/postgresql/content.sql.go +++ b/internal/db/postgresql/content.sql.go @@ -212,3 +212,42 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C ) return i, err } + +const upsertContent = `-- name: UpsertContent :one +INSERT INTO content (id, site_id, value, type, last_edited_by) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT(id, site_id) DO UPDATE SET + value = EXCLUDED.value, + type = EXCLUDED.type, + last_edited_by = EXCLUDED.last_edited_by +RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by +` + +type UpsertContentParams struct { + ID string `json:"id"` + SiteID string `json:"site_id"` + Value string `json:"value"` + Type string `json:"type"` + LastEditedBy string `json:"last_edited_by"` +} + +func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) { + row := q.db.QueryRowContext(ctx, upsertContent, + arg.ID, + arg.SiteID, + arg.Value, + arg.Type, + arg.LastEditedBy, + ) + var i Content + err := row.Scan( + &i.ID, + &i.SiteID, + &i.Value, + &i.Type, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastEditedBy, + ) + return i, err +} diff --git a/internal/db/postgresql/querier.go b/internal/db/postgresql/querier.go index 37cf939..ff09adc 100644 --- a/internal/db/postgresql/querier.go +++ b/internal/db/postgresql/querier.go @@ -26,6 +26,7 @@ type Querier interface { InitializeSchema(ctx context.Context) error InitializeVersionsTable(ctx context.Context) error UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) + UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) } var _ Querier = (*Queries)(nil) diff --git a/internal/db/sqlite/content.sql.go b/internal/db/sqlite/content.sql.go index ce90e1a..20f5261 100644 --- a/internal/db/sqlite/content.sql.go +++ b/internal/db/sqlite/content.sql.go @@ -212,3 +212,42 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C ) return i, err } + +const upsertContent = `-- name: UpsertContent :one +INSERT INTO content (id, site_id, value, type, last_edited_by) +VALUES (?1, ?2, ?3, ?4, ?5) +ON CONFLICT(id, site_id) DO UPDATE SET + value = EXCLUDED.value, + type = EXCLUDED.type, + last_edited_by = EXCLUDED.last_edited_by +RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by +` + +type UpsertContentParams struct { + ID string `json:"id"` + SiteID string `json:"site_id"` + Value string `json:"value"` + Type string `json:"type"` + LastEditedBy string `json:"last_edited_by"` +} + +func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) { + row := q.db.QueryRowContext(ctx, upsertContent, + arg.ID, + arg.SiteID, + arg.Value, + arg.Type, + arg.LastEditedBy, + ) + var i Content + err := row.Scan( + &i.ID, + &i.SiteID, + &i.Value, + &i.Type, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastEditedBy, + ) + return i, err +} diff --git a/internal/db/sqlite/querier.go b/internal/db/sqlite/querier.go index f2c5dac..c246168 100644 --- a/internal/db/sqlite/querier.go +++ b/internal/db/sqlite/querier.go @@ -22,6 +22,7 @@ type Querier interface { InitializeSchema(ctx context.Context) error InitializeVersionsTable(ctx context.Context) error UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) + UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) } var _ Querier = (*Queries)(nil) diff --git a/lib/package-lock.json b/lib/package-lock.json index 7d31e9e..01eb4fe 100644 --- a/lib/package-lock.json +++ b/lib/package-lock.json @@ -15,7 +15,6 @@ "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.0", "@rollup/plugin-terser": "^0.4.0", - "live-server": "^1.2.2", "rollup": "^3.0.0" } }, @@ -160,43 +159,6 @@ "dev": true, "license": "MIT" }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -210,238 +172,6 @@ "node": ">=0.4.0" } }, - "node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "license": "ISC", - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/apache-crypt": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.6.tgz", - "integrity": "sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "unix-crypt-td-js": "^1.1.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/apache-md5": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.8.tgz", - "integrity": "sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async-each": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", - "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -449,137 +179,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/chokidar/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -587,83 +186,6 @@ "dev": true, "license": "MIT" }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -674,61 +196,6 @@ "node": ">=0.10.0" } }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -736,220 +203,6 @@ "dev": true, "license": "MIT" }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true, - "license": "MIT" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -975,89 +228,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1071,96 +241,6 @@ "node": ">= 0.4" } }, - "node_modules/http-auth": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz", - "integrity": "sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "apache-crypt": "^1.1.2", - "apache-md5": "^1.0.6", - "bcryptjs": "^2.3.0", - "uuid": "^3.0.0" - }, - "engines": { - "node": ">=4.6.1" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "license": "MIT" - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -1177,66 +257,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -1244,139 +264,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/live-server": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.2.tgz", - "integrity": "sha512-t28HXLjITRGoMSrCOv4eZ88viHaBVIjKjdI5PO92Vxlu+twbk6aE0t7dVIaz6ZWkjPilYFV6OSdMYl9ybN2B4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^2.0.4", - "colors": "1.4.0", - "connect": "^3.6.6", - "cors": "latest", - "event-stream": "3.3.4", - "faye-websocket": "0.11.x", - "http-auth": "3.1.x", - "morgan": "^1.9.1", - "object-assign": "latest", - "opn": "latest", - "proxy-middleware": "latest", - "send": "latest", - "serve-index": "^1.9.1" - }, - "bin": { - "live-server": "live-server.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/marked": { "version": "16.2.1", "resolved": "https://registry.npmjs.org/marked/-/marked-16.2.1.tgz", @@ -1389,382 +276,6 @@ "node": ">= 20" } }, - "node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/micromatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.1.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/nan": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", - "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/opn": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", - "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", - "deprecated": "The package has been renamed to `open`", - "dev": true, - "license": "MIT", - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -1772,19 +283,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "license": [ - "MIT", - "Apache2" - ], - "dependencies": { - "through": "~2.3" - } - }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -1798,33 +296,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-middleware": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", - "integrity": "sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1835,122 +306,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true, - "license": "ISC" - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -1972,24 +327,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true, - "license": "MIT" - }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12" - } - }, "node_modules/rollup": { "version": "3.29.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", @@ -2028,97 +365,6 @@ ], "license": "MIT" }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ret": "~0.1.10" - } - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/send/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -2129,111 +375,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, "node_modules/smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", @@ -2241,104 +382,6 @@ "dev": true, "license": "MIT" }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/snapdragon/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2349,21 +392,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "dev": true, - "license": "MIT", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -2375,145 +403,6 @@ "source-map": "^0.6.0" } }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "dev": true, - "license": "MIT" - }, - "node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "~0.1.1" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2546,93 +435,6 @@ "node": ">=10" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/turndown": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.1.tgz", @@ -2641,183 +443,6 @@ "dependencies": { "@mixmark-io/domino": "^2.2.0" } - }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unix-crypt-td-js": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", - "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true, - "license": "MIT" - }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } } } } diff --git a/lib/src/core/api-client.js b/lib/src/core/api-client.js index 4bdc2a4..43f16e6 100644 --- a/lib/src/core/api-client.js +++ b/lib/src/core/api-client.js @@ -34,7 +34,7 @@ export class ApiClient { method: 'PUT', headers: { 'Content-Type': 'application/json', - 'X-User-ID': this.getCurrentUser() + 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify({ value: content }) }); @@ -64,7 +64,7 @@ export class ApiClient { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-User-ID': this.getCurrentUser() + 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify({ id: contentId, @@ -113,7 +113,7 @@ export class ApiClient { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-User-ID': this.getCurrentUser() + 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify({ version_id: versionId @@ -133,9 +133,171 @@ export class ApiClient { } } - // Helper to get current user (for user attribution) + /** + * Get authentication token for API requests + * @returns {string} JWT token or mock token for development + */ + getAuthToken() { + // Check if we have a real JWT token from OAuth + const realToken = this.getStoredToken(); + if (realToken && !this.isTokenExpired(realToken)) { + return realToken; + } + + // Development/mock token for when no real auth is present + return this.getMockToken(); + } + + /** + * Get current user information from token + * @returns {string} User identifier + */ getCurrentUser() { - // This could be enhanced to get from authentication system - return 'anonymous'; + const token = this.getAuthToken(); + + // If it's a mock token, return mock user + if (token.startsWith('mock-')) { + return 'anonymous'; + } + + // Parse real JWT token for user info + try { + const payload = this.parseJWT(token); + return payload.sub || payload.user_id || payload.email || 'anonymous'; + } catch (error) { + console.warn('Failed to parse JWT token:', error); + return 'anonymous'; + } + } + + /** + * Get stored JWT token from localStorage/sessionStorage + * @returns {string|null} Stored JWT token + */ + getStoredToken() { + // Try localStorage first (persistent), then sessionStorage (session-only) + return localStorage.getItem('insertr_auth_token') || + sessionStorage.getItem('insertr_auth_token') || + null; + } + + /** + * Store JWT token for future requests + * @param {string} token - JWT token from OAuth provider + * @param {boolean} persistent - Whether to use localStorage (true) or sessionStorage (false) + */ + setStoredToken(token, persistent = true) { + const storage = persistent ? localStorage : sessionStorage; + storage.setItem('insertr_auth_token', token); + + // Clear the other storage to avoid conflicts + const otherStorage = persistent ? sessionStorage : localStorage; + otherStorage.removeItem('insertr_auth_token'); + } + + /** + * Clear stored authentication token + */ + clearStoredToken() { + localStorage.removeItem('insertr_auth_token'); + sessionStorage.removeItem('insertr_auth_token'); + } + + /** + * Generate mock JWT token for development/testing + * @returns {string} Mock JWT token + */ + getMockToken() { + // Create a mock JWT-like token for development + // Format: mock-{user}-{timestamp}-{random} + const user = 'anonymous'; + const timestamp = Date.now(); + const random = Math.random().toString(36).substr(2, 9); + return `mock-${user}-${timestamp}-${random}`; + } + + /** + * Parse JWT token payload + * @param {string} token - JWT token + * @returns {object} Parsed payload + */ + parseJWT(token) { + if (token.startsWith('mock-')) { + // Return mock payload for development tokens + return { + sub: 'anonymous', + user_id: 'anonymous', + email: 'anonymous@localhost', + iss: 'insertr-dev', + exp: Date.now() + 24 * 60 * 60 * 1000 // 24 hours from now + }; + } + + try { + // Parse real JWT token + const parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('Invalid JWT format'); + } + + const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); + return payload; + } catch (error) { + throw new Error(`Failed to parse JWT token: ${error.message}`); + } + } + + /** + * Check if JWT token is expired + * @param {string} token - JWT token + * @returns {boolean} True if token is expired + */ + isTokenExpired(token) { + try { + const payload = this.parseJWT(token); + const now = Math.floor(Date.now() / 1000); + return payload.exp && payload.exp < now; + } catch (error) { + // If we can't parse the token, consider it expired + return true; + } + } + + /** + * Initialize OAuth flow with provider (Google, GitHub, etc.) + * @param {string} provider - OAuth provider ('google', 'github', etc.) + * @returns {Promise} Success status + */ + async initiateOAuth(provider = 'google') { + // This will be implemented when we add real OAuth integration + console.log(`🔐 OAuth flow with ${provider} not yet implemented`); + console.log('💡 For now, using mock authentication in development'); + + // Store a mock token for development + const mockToken = this.getMockToken(); + this.setStoredToken(mockToken, true); + + return true; + } + + /** + * Handle OAuth callback after user returns from provider + * @param {URLSearchParams} urlParams - URL parameters from OAuth callback + * @returns {Promise} Success status + */ + async handleOAuthCallback(urlParams) { + // This will be implemented when we add real OAuth integration + const code = urlParams.get('code'); + const state = urlParams.get('state'); + + if (code) { + console.log('🔐 OAuth callback received, exchanging code for token...'); + // TODO: Exchange authorization code for JWT token + // const token = await this.exchangeCodeForToken(code, state); + // this.setStoredToken(token, true); + return true; + } + + return false; } } \ No newline at end of file diff --git a/lib/src/core/editor.js b/lib/src/core/editor.js index 40fafaf..a24daf9 100644 --- a/lib/src/core/editor.js +++ b/lib/src/core/editor.js @@ -116,8 +116,7 @@ export class InsertrEditor { } } - // Update element content regardless of API success (optimistic update) - this.updateElementContent(meta.element, formData); + // Close form this.formRenderer.closeForm(); @@ -127,8 +126,7 @@ export class InsertrEditor { } catch (error) { console.error('❌ Error saving content:', error); - // Still update the UI even if API fails - this.updateElementContent(meta.element, formData); + this.formRenderer.closeForm(); } } @@ -140,44 +138,15 @@ export class InsertrEditor { return 'link'; } - if (tagName === 'p' || tagName === 'div') { - return 'markdown'; - } - - // Default to text for headings and other elements - return 'text'; + // ALL text elements use markdown for consistent editing experience + return 'markdown'; } handleCancel(meta) { console.log('❌ Edit cancelled:', meta.contentId); } - updateElementContent(element, formData) { - // Skip updating markdown elements and groups - they're handled by the unified markdown editor - if (element.classList.contains('insertr-group') || this.isMarkdownElement(element)) { - console.log('🔄 Skipping element update - handled by unified markdown editor'); - return; - } - - if (element.tagName.toLowerCase() === 'a') { - // Update link element - if (formData.text !== undefined) { - element.textContent = formData.text; - } - if (formData.url !== undefined) { - element.setAttribute('href', formData.url); - } - } else { - // Update text content for non-markdown elements - element.textContent = formData.text || ''; - } - } - isMarkdownElement(element) { - // Check if element uses markdown based on form config - const markdownTags = new Set(['p', 'h3', 'h4', 'h5', 'h6', 'span']); - return markdownTags.has(element.tagName.toLowerCase()); - } addEditorStyles() { const styles = ` .insertr-editing-hover { diff --git a/lib/src/ui/Editor.js b/lib/src/ui/Editor.js new file mode 100644 index 0000000..153faa4 --- /dev/null +++ b/lib/src/ui/Editor.js @@ -0,0 +1,492 @@ +/** + * Editor - Handles all content types with markdown-first approach + */ +import { markdownConverter } from '../utils/markdown.js'; +import { Previewer } from './Previewer.js'; + +export class Editor { + constructor() { + this.currentOverlay = null; + this.previewer = new Previewer(); + } + + /** + * Edit any content element with markdown interface + * @param {Object} meta - Element metadata {element, contentId, contentType} + * @param {string|Object} currentContent - Current content value + * @param {Function} onSave - Save callback + * @param {Function} onCancel - Cancel callback + */ + edit(meta, currentContent, onSave, onCancel) { + const { element } = meta; + + // Handle both single elements and groups uniformly + const elements = Array.isArray(element) ? element : [element]; + const context = new EditContext(elements, currentContent); + + // Close any existing editor + this.close(); + + // Create editor form + const form = this.createForm(context, meta); + const overlay = this.createOverlay(form); + + // Position relative to primary element + this.positionForm(context.primaryElement, overlay); + + // Setup event handlers + this.setupEventHandlers(form, overlay, context, { onSave, onCancel }); + + // Show editor + document.body.appendChild(overlay); + this.currentOverlay = overlay; + + // Focus textarea + const textarea = form.querySelector('textarea'); + if (textarea) { + setTimeout(() => textarea.focus(), 100); + } + + return overlay; + } + + /** + * Create editing form for any content type + */ + createForm(context, meta) { + const config = this.getFieldConfig(context); + const currentContent = context.extractContent(); + + const form = document.createElement('div'); + form.className = 'insertr-edit-form'; + + // Build form HTML + let formHTML = `
${config.label}
`; + + // Markdown textarea (always present) + formHTML += this.createMarkdownField(config, currentContent); + + // URL field (for links only) + if (config.includeUrl) { + formHTML += this.createUrlField(currentContent); + } + + // Form actions + formHTML += ` +
+ + + +
+ `; + + form.innerHTML = formHTML; + return form; + } + + /** + * Get field configuration for any element type (markdown-first) + */ + getFieldConfig(context) { + const elementCount = context.elements.length; + const primaryElement = context.primaryElement; + const isLink = primaryElement.tagName.toLowerCase() === 'a'; + + // Multi-element groups + if (elementCount > 1) { + return { + type: 'markdown', + includeUrl: false, + label: `Group Content (${elementCount} elements)`, + rows: Math.max(8, elementCount * 2), + placeholder: 'Edit all content together using markdown...' + }; + } + + // Single elements - all get markdown by default + const tag = primaryElement.tagName.toLowerCase(); + const baseConfig = { + type: 'markdown', + includeUrl: isLink, + placeholder: 'Enter content using markdown...' + }; + + // Customize by element type + switch (tag) { + case 'h1': + return { ...baseConfig, label: 'Main Headline', rows: 1, placeholder: 'Enter main headline...' }; + case 'h2': + return { ...baseConfig, label: 'Subheading', rows: 1, placeholder: 'Enter subheading...' }; + case 'h3': case 'h4': case 'h5': case 'h6': + return { ...baseConfig, label: 'Heading', rows: 2, placeholder: 'Enter heading (markdown supported)...' }; + case 'p': + return { ...baseConfig, label: 'Content', rows: 4, placeholder: 'Enter content using markdown...' }; + case 'span': + return { ...baseConfig, label: 'Text', rows: 2, placeholder: 'Enter text (markdown supported)...' }; + case 'button': + return { ...baseConfig, label: 'Button Text', rows: 1, placeholder: 'Enter button text...' }; + case 'a': + return { ...baseConfig, label: 'Link', rows: 2, placeholder: 'Enter link text (markdown supported)...' }; + default: + return { ...baseConfig, label: 'Content', rows: 3, placeholder: 'Enter content using markdown...' }; + } + } + + /** + * Create markdown textarea field + */ + createMarkdownField(config, content) { + const textContent = typeof content === 'object' ? content.text || '' : content; + + return ` +
+ +
+ Supports Markdown formatting (bold, italic, links, etc.) +
+
+ `; + } + + /** + * Create URL field for links + */ + createUrlField(content) { + const url = typeof content === 'object' ? content.url || '' : ''; + + return ` +
+ + +
+ `; + } + + /** + * Setup event handlers + */ + setupEventHandlers(form, overlay, context, { onSave, onCancel }) { + const textarea = form.querySelector('textarea'); + const urlInput = form.querySelector('input[name="url"]'); + const saveBtn = form.querySelector('.insertr-btn-save'); + const cancelBtn = form.querySelector('.insertr-btn-cancel'); + const historyBtn = form.querySelector('.insertr-btn-history'); + + // Initialize previewer + this.previewer.setActiveContext(context); + + // Setup live preview for content changes + if (textarea) { + textarea.addEventListener('input', () => { + const content = this.extractFormData(form); + this.previewer.schedulePreview(context, content); + }); + } + + // Setup live preview for URL changes (links only) + if (urlInput) { + urlInput.addEventListener('input', () => { + const content = this.extractFormData(form); + this.previewer.schedulePreview(context, content); + }); + } + + // Save handler + if (saveBtn) { + saveBtn.addEventListener('click', () => { + const content = this.extractFormData(form); + + // Apply final content to elements + context.applyContent(content); + + // Update stored original content to match current state + // This makes the saved content the new baseline for future edits + context.updateOriginalContent(); + + // Clear preview styling (won't restore content since original matches current) + this.previewer.clearPreview(); + + // Callback with the content + onSave(content); + this.close(); + }); + } + + // Cancel handler + if (cancelBtn) { + cancelBtn.addEventListener('click', () => { + this.previewer.clearPreview(); + onCancel(); + this.close(); + }); + } + + // History handler + if (historyBtn) { + historyBtn.addEventListener('click', () => { + const contentId = historyBtn.getAttribute('data-content-id'); + console.log('Version history not implemented yet for:', contentId); + // TODO: Implement version history integration + }); + } + + // ESC key handler + const keyHandler = (e) => { + if (e.key === 'Escape') { + this.previewer.clearPreview(); + onCancel(); + this.close(); + document.removeEventListener('keydown', keyHandler); + } + }; + document.addEventListener('keydown', keyHandler); + + // Click outside handler + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + this.previewer.clearPreview(); + onCancel(); + this.close(); + } + }); + } + + /** + * Extract form data consistently + */ + extractFormData(form) { + const textarea = form.querySelector('textarea[name="content"]'); + const urlInput = form.querySelector('input[name="url"]'); + + const content = textarea ? textarea.value : ''; + + if (urlInput) { + // Link content + return { + text: content, + url: urlInput.value + }; + } + + // Regular content + return content; + } + + /** + * Create overlay with backdrop + */ + createOverlay(form) { + const overlay = document.createElement('div'); + overlay.className = 'insertr-form-overlay'; + overlay.appendChild(form); + return overlay; + } + + /** + * Position form relative to primary element + */ + positionForm(element, overlay) { + const rect = element.getBoundingClientRect(); + const form = overlay.querySelector('.insertr-edit-form'); + const viewportWidth = window.innerWidth; + + // Calculate optimal width + let formWidth; + if (viewportWidth < 768) { + formWidth = Math.min(viewportWidth - 40, 500); + } else { + const minComfortableWidth = 600; + const maxWidth = Math.min(viewportWidth * 0.9, 800); + formWidth = Math.max(minComfortableWidth, Math.min(rect.width * 1.5, maxWidth)); + } + + form.style.width = `${formWidth}px`; + + // Position below element + const top = rect.bottom + window.scrollY + 10; + const centerLeft = rect.left + window.scrollX + (rect.width / 2) - (formWidth / 2); + const minLeft = 20; + const maxLeft = window.innerWidth - formWidth - 20; + const left = Math.max(minLeft, Math.min(centerLeft, maxLeft)); + + overlay.style.position = 'absolute'; + overlay.style.top = `${top}px`; + overlay.style.left = `${left}px`; + overlay.style.zIndex = '10000'; + + // Ensure visibility + this.ensureModalVisible(overlay); + } + + /** + * Ensure modal is visible by scrolling if needed + */ + ensureModalVisible(overlay) { + requestAnimationFrame(() => { + const modal = overlay.querySelector('.insertr-edit-form'); + const modalRect = modal.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + + if (modalRect.bottom > viewportHeight) { + const scrollAmount = modalRect.bottom - viewportHeight + 20; + window.scrollBy({ + top: scrollAmount, + behavior: 'smooth' + }); + } + }); + } + + /** + * Close current editor + */ + close() { + if (this.previewer) { + this.previewer.clearPreview(); + } + + if (this.currentOverlay) { + this.currentOverlay.remove(); + this.currentOverlay = null; + } + } + + /** + * Escape HTML to prevent XSS + */ + escapeHtml(text) { + if (typeof text !== 'string') return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +/** + * EditContext - Represents content elements for editing + */ +class EditContext { + constructor(elements, currentContent) { + this.elements = elements; + this.primaryElement = elements[0]; + this.originalContent = null; + this.currentContent = currentContent; + } + + /** + * Extract content from elements in markdown format + */ + extractContent() { + if (this.elements.length === 1) { + const element = this.elements[0]; + + // Handle links specially + if (element.tagName.toLowerCase() === 'a') { + return { + text: markdownConverter.htmlToMarkdown(element.innerHTML), + url: element.href + }; + } + + // Single element - convert to markdown + return markdownConverter.htmlToMarkdown(element.innerHTML); + } else { + // Multiple elements - use group extraction + return markdownConverter.extractGroupMarkdown(this.elements); + } + } + + /** + * Apply content to elements from markdown/object + */ + applyContent(content) { + if (this.elements.length === 1) { + const element = this.elements[0]; + + // Handle links specially + if (element.tagName.toLowerCase() === 'a' && typeof content === 'object') { + element.innerHTML = markdownConverter.markdownToHtml(content.text || ''); + if (content.url) { + element.href = content.url; + } + return; + } + + // Single element - convert markdown to HTML + const html = markdownConverter.markdownToHtml(content); + element.innerHTML = html; + } else { + // Multiple elements - use group update + markdownConverter.updateGroupElements(this.elements, content); + } + } + + /** + * Store original content for preview restoration + */ + storeOriginalContent() { + this.originalContent = this.elements.map(el => ({ + innerHTML: el.innerHTML, + href: el.href // Store href for links + })); + } + + /** + * Restore original content (for preview cancellation) + */ + restoreOriginalContent() { + if (this.originalContent) { + this.elements.forEach((el, index) => { + if (this.originalContent[index] !== undefined) { + el.innerHTML = this.originalContent[index].innerHTML; + if (this.originalContent[index].href) { + el.href = this.originalContent[index].href; + } + } + }); + } + } + + /** + * Update original content to match current element state (after save) + * This makes the current content the new baseline for future cancellations + */ + updateOriginalContent() { + this.originalContent = this.elements.map(el => ({ + innerHTML: el.innerHTML, + href: el.href // Store href for links + })); + } + + /** + * Apply preview styling to all elements + */ + applyPreviewStyling() { + this.elements.forEach(el => { + el.classList.add('insertr-preview-active'); + }); + + // Also apply to containers if they're groups + if (this.primaryElement.classList.contains('insertr-group')) { + this.primaryElement.classList.add('insertr-preview-active'); + } + } + + /** + * Remove preview styling from all elements + */ + removePreviewStyling() { + this.elements.forEach(el => { + el.classList.remove('insertr-preview-active'); + }); + + // Also remove from containers + if (this.primaryElement.classList.contains('insertr-group')) { + this.primaryElement.classList.remove('insertr-preview-active'); + } + } +} \ No newline at end of file diff --git a/lib/src/ui/Previewer.js b/lib/src/ui/Previewer.js new file mode 100644 index 0000000..7698b02 --- /dev/null +++ b/lib/src/ui/Previewer.js @@ -0,0 +1,157 @@ +/** + * Previewer - Handles live preview for all content types + */ +import { markdownConverter } from '../utils/markdown.js'; + +export class Previewer { + constructor() { + this.previewTimeout = null; + this.activeContext = null; + this.resizeObserver = null; + this.onHeightChange = null; + } + + /** + * Set the active editing context for preview + */ + setActiveContext(context) { + this.clearPreview(); + this.activeContext = context; + this.startResizeObserver(); + } + + /** + * Schedule a preview update with debouncing + */ + schedulePreview(context, content) { + // Clear existing timeout + if (this.previewTimeout) { + clearTimeout(this.previewTimeout); + } + + // Schedule new preview with 500ms debounce + this.previewTimeout = setTimeout(() => { + this.updatePreview(context, content); + }, 500); + } + + /** + * Update preview with new content + */ + updatePreview(context, content) { + // Store original content if first preview + if (!context.originalContent) { + context.storeOriginalContent(); + } + + // Apply preview content to elements + this.applyPreviewContent(context, content); + context.applyPreviewStyling(); + } + + /** + * Apply preview content to context elements + */ + applyPreviewContent(context, content) { + if (context.elements.length === 1) { + const element = context.elements[0]; + + // Handle links specially + if (element.tagName.toLowerCase() === 'a') { + if (typeof content === 'object') { + // Update link text (markdown to HTML) + if (content.text !== undefined) { + const html = markdownConverter.markdownToHtml(content.text); + element.innerHTML = html; + } + // Update link URL + if (content.url !== undefined && content.url.trim()) { + element.href = content.url; + } + } else if (content && content.trim()) { + // Just markdown content for link text + const html = markdownConverter.markdownToHtml(content); + element.innerHTML = html; + } + return; + } + + // Regular single element + if (content && content.trim()) { + const html = markdownConverter.markdownToHtml(content); + element.innerHTML = html; + } + } else { + // Multiple elements - use group update + if (content && content.trim()) { + markdownConverter.updateGroupElements(context.elements, content); + } + } + } + + /** + * Clear all preview state and restore original content + */ + clearPreview() { + if (this.activeContext) { + this.activeContext.restoreOriginalContent(); + this.activeContext.removePreviewStyling(); + this.activeContext = null; + } + + if (this.previewTimeout) { + clearTimeout(this.previewTimeout); + this.previewTimeout = null; + } + + this.stopResizeObserver(); + } + + /** + * Start observing element size changes for modal repositioning + */ + startResizeObserver() { + this.stopResizeObserver(); + + if (this.activeContext) { + this.resizeObserver = new ResizeObserver(() => { + // Handle height changes for modal repositioning + if (this.onHeightChange) { + this.onHeightChange(this.activeContext.primaryElement); + } + }); + + // Observe all elements in the context + this.activeContext.elements.forEach(el => { + this.resizeObserver.observe(el); + }); + } + } + + /** + * Stop observing element size changes + */ + stopResizeObserver() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } + } + + /** + * Set callback for height changes (for modal repositioning) + */ + setHeightChangeCallback(callback) { + this.onHeightChange = callback; + } + + /** + * Get unique element ID for tracking + */ + getElementId(element) { + if (!element._insertrId) { + element._insertrId = 'insertr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } + return element._insertrId; + } +} \ No newline at end of file diff --git a/lib/src/ui/form-renderer.js b/lib/src/ui/form-renderer.js index 4eca4e5..923fc09 100644 --- a/lib/src/ui/form-renderer.js +++ b/lib/src/ui/form-renderer.js @@ -1,270 +1,35 @@ -import { markdownConverter } from '../utils/markdown.js'; -import { MarkdownEditor } from './markdown-editor.js'; - /** - * LivePreviewManager - Handles debounced live preview updates for non-markdown elements + * InsertrFormRenderer - Form renderer using markdown-first approach + * Thin wrapper around the Editor system */ -class LivePreviewManager { - constructor() { - this.previewTimeouts = new Map(); - this.activeElement = null; - this.originalContent = null; - this.originalStyles = null; - this.resizeObserver = null; - this.onHeightChangeCallback = null; - } +import { Editor } from './Editor.js'; - schedulePreview(element, newValue, elementType) { - const elementId = this.getElementId(element); - - // Clear existing timeout - if (this.previewTimeouts.has(elementId)) { - clearTimeout(this.previewTimeouts.get(elementId)); - } - - // Schedule new preview update with 500ms debounce - const timeoutId = setTimeout(() => { - this.updatePreview(element, newValue, elementType); - }, 500); - - this.previewTimeouts.set(elementId, timeoutId); - } - - - - updatePreview(element, newValue, elementType) { - // Store original content if first preview - if (!this.originalContent && this.activeElement === element) { - this.originalContent = this.extractOriginalContent(element, elementType); - } - - // Apply preview styling and content - this.applyPreviewContent(element, newValue, elementType); - - // ResizeObserver will automatically detect height changes - } - - - - extractOriginalContent(element, elementType) { - switch (elementType) { - case 'link': - return { - text: element.textContent, - url: element.href - }; - default: - return element.textContent; - } - } - - applyPreviewContent(element, newValue, elementType) { - // Add preview indicator - element.classList.add('insertr-preview-active'); - - // Update content based on element type - switch (elementType) { - case 'text': - case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - case 'span': case 'button': - if (newValue && newValue.trim()) { - element.textContent = newValue; - } - break; - - case 'textarea': - case 'p': - if (newValue && newValue.trim()) { - element.textContent = newValue; - } - break; - - case 'link': - if (typeof newValue === 'object') { - if (newValue.text !== undefined && newValue.text.trim()) { - element.textContent = newValue.text; - } - if (newValue.url !== undefined && newValue.url.trim()) { - element.href = newValue.url; - } - } else if (newValue && newValue.trim()) { - element.textContent = newValue; - } - break; - - - } - } - - clearPreview(element) { - if (!element) return; - - const elementId = this.getElementId(element); - - // Clear any pending preview - if (this.previewTimeouts.has(elementId)) { - clearTimeout(this.previewTimeouts.get(elementId)); - this.previewTimeouts.delete(elementId); - } - - // Stop ResizeObserver - this.stopResizeObserver(); - - // Restore original content - if (this.originalContent && element === this.activeElement) { - this.restoreOriginalContent(element); - } - - // Remove preview styling - element.classList.remove('insertr-preview-active'); - - this.activeElement = null; - this.originalContent = null; - } - - restoreOriginalContent(element) { - if (!this.originalContent) return; - - if (typeof this.originalContent === 'object') { - // Link element - element.textContent = this.originalContent.text; - if (this.originalContent.url) { - element.href = this.originalContent.url; - } - } else { - // Text element - element.textContent = this.originalContent; - } - } - - getElementId(element) { - // Create unique ID for element tracking - if (!element._insertrId) { - element._insertrId = 'insertr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); - } - return element._insertrId; - } - - setActiveElement(element) { - this.activeElement = element; - this.originalContent = null; - this.startResizeObserver(element); - } - - setHeightChangeCallback(callback) { - this.onHeightChangeCallback = callback; - } - - startResizeObserver(element) { - // Clean up existing observer - this.stopResizeObserver(); - - // Create new ResizeObserver for this element - this.resizeObserver = new ResizeObserver(entries => { - // Use requestAnimationFrame to ensure smooth updates - requestAnimationFrame(() => { - if (this.onHeightChangeCallback && element === this.activeElement) { - this.onHeightChangeCallback(element); - } - }); - }); - - // Start observing the element - this.resizeObserver.observe(element); - } - - stopResizeObserver() { - if (this.resizeObserver) { - this.resizeObserver.disconnect(); - this.resizeObserver = null; - } - } -} - -/** - * InsertrFormRenderer - Professional modal editing forms with live preview - * Enhanced with debounced live preview and comfortable input sizing - */ export class InsertrFormRenderer { constructor(apiClient = null) { this.apiClient = apiClient; - this.currentOverlay = null; - this.previewManager = new LivePreviewManager(); - this.markdownEditor = new MarkdownEditor(); + this.editor = new Editor(); this.setupStyles(); } /** - * Create and show edit form for content element + * Show edit form for any content element * @param {Object} meta - Element metadata {element, contentId, contentType} - * @param {string} currentContent - Current content value + * @param {string|Object} currentContent - Current content value * @param {Function} onSave - Save callback * @param {Function} onCancel - Cancel callback */ showEditForm(meta, currentContent, onSave, onCancel) { - // Close any existing form - this.closeForm(); + const { element } = meta; - const { element, contentId, contentType } = meta; - const config = this.getFieldConfig(element, contentType); - - // Route to unified markdown editor for markdown content - if (config.type === 'markdown') { - return this.markdownEditor.edit(element, onSave, onCancel); - } - - // Route to unified markdown editor for group elements + // Handle insertr-group elements by getting their viable children if (element.classList.contains('insertr-group')) { const children = this.getGroupChildren(element); - return this.markdownEditor.edit(children, onSave, onCancel); + const groupMeta = { ...meta, element: children }; + return this.editor.edit(groupMeta, currentContent, onSave, onCancel); } - // Handle non-markdown elements (text, links, etc.) with legacy system - return this.showLegacyEditForm(meta, currentContent, onSave, onCancel); - } - - - - - - /** - * Show legacy edit form for non-markdown elements (text, links, etc.) - */ - showLegacyEditForm(meta, currentContent, onSave, onCancel) { - const { element, contentId, contentType } = meta; - const config = this.getFieldConfig(element, contentType); - - // Initialize preview manager for this element - this.previewManager.setActiveElement(element); - - // Set up height change callback - this.previewManager.setHeightChangeCallback((changedElement) => { - this.repositionModal(changedElement, overlay); - }); - - // Create form - const form = this.createEditForm(contentId, config, currentContent); - - // Create overlay with backdrop - const overlay = this.createOverlay(form); - - // Position form with enhanced sizing - this.positionForm(element, overlay); - - // Setup event handlers with live preview - this.setupFormHandlers(form, overlay, element, config, { onSave, onCancel }); - - // Show form - document.body.appendChild(overlay); - this.currentOverlay = overlay; - - // Focus first input - const firstInput = form.querySelector('input, textarea'); - if (firstInput) { - setTimeout(() => firstInput.focus(), 100); - } - - return overlay; + // All other elements use the editor directly + return this.editor.edit(meta, currentContent, onSave, onCancel); } /** @@ -273,7 +38,7 @@ export class InsertrFormRenderer { getGroupChildren(groupElement) { const children = []; for (const child of groupElement.children) { - // Skip elements that don't have text content + // Skip elements that don't have meaningful text content if (child.textContent.trim().length > 0) { children.push(child); } @@ -285,190 +50,28 @@ export class InsertrFormRenderer { * Close current form */ closeForm() { - // Close markdown editor if active - this.markdownEditor.close(); - - // Clear any active legacy previews - if (this.previewManager.activeElement) { - this.previewManager.clearPreview(this.previewManager.activeElement); - } - - if (this.currentOverlay) { - this.currentOverlay.remove(); - this.currentOverlay = null; - } + this.editor.close(); } /** - * Generate field configuration based on element - */ - getFieldConfig(element, contentType) { - const tagName = element.tagName.toLowerCase(); - const classList = Array.from(element.classList); - - // Default configurations based on element type - using markdown for rich content - const configs = { - h1: { type: 'text', label: 'Headline', maxLength: 60, placeholder: 'Enter headline...' }, - h2: { type: 'text', label: 'Subheading', maxLength: 80, placeholder: 'Enter subheading...' }, - h3: { type: 'markdown', label: 'Section Title', rows: 2, placeholder: 'Enter title (markdown supported)...' }, - h4: { type: 'markdown', label: 'Title', rows: 2, placeholder: 'Enter title (markdown supported)...' }, - h5: { type: 'markdown', label: 'Title', rows: 2, placeholder: 'Enter title (markdown supported)...' }, - h6: { type: 'markdown', label: 'Title', rows: 2, placeholder: 'Enter title (markdown supported)...' }, - p: { type: 'markdown', label: 'Content', rows: 4, placeholder: 'Enter content using markdown...' }, - a: { type: 'link', label: 'Link', placeholder: 'Enter link text...', includeUrl: true }, - span: { type: 'markdown', label: 'Text', rows: 2, placeholder: 'Enter text (markdown supported)...' }, - button: { type: 'text', label: 'Button Text', placeholder: 'Enter button text...' }, - }; - - let config = configs[tagName] || { type: 'text', label: 'Text', placeholder: 'Enter text...' }; - - // CSS class enhancements - if (classList.includes('lead')) { - config = { ...config, label: 'Lead Paragraph', rows: 4, placeholder: 'Enter lead paragraph...' }; - } - - // Override with contentType from CLI if specified - if (contentType === 'markdown') { - config = { ...config, type: 'markdown', label: 'Markdown Content', rows: 8 }; - } - - return config; - } - - /** - * Create form HTML structure - */ - createEditForm(contentId, config, currentContent) { - const form = document.createElement('div'); - form.className = 'insertr-edit-form'; - - let formHTML = `
${config.label}
`; - - if (config.type === 'markdown') { - formHTML += this.createMarkdownField(config, currentContent); - } else if (config.type === 'link' && config.includeUrl) { - formHTML += this.createLinkField(config, currentContent); - } else if (config.type === 'textarea') { - formHTML += this.createTextareaField(config, currentContent); - } else { - formHTML += this.createTextField(config, currentContent); - } - - // Form buttons - formHTML += ` -
- - - -
- `; - - form.innerHTML = formHTML; - return form; - } - - /** - * Create markdown field with preview - */ - createMarkdownField(config, currentContent) { - return ` -
- -
- Supports Markdown formatting (bold, italic, links, etc.) -
-
- `; - } - - /** - * Create link field (text + URL) - */ - createLinkField(config, currentContent) { - const linkText = typeof currentContent === 'object' ? currentContent.text || '' : currentContent; - const linkUrl = typeof currentContent === 'object' ? currentContent.url || '' : ''; - - return ` -
- - -
-
- - -
- `; - } - - /** - * Create textarea field - */ - createTextareaField(config, currentContent) { - const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent; - return ` -
- -
- `; - } - - /** - * Create text input field - */ - createTextField(config, currentContent) { - const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent; - return ` -
- -
- `; - } - - /** - * Create overlay with backdrop - */ - createOverlay(form) { - const overlay = document.createElement('div'); - overlay.className = 'insertr-form-overlay'; - overlay.appendChild(form); - return overlay; - } - - /** - * Get element ID for preview tracking - */ - getElementId(element) { - return element.id || element.getAttribute('data-content-id') || - `element-${element.tagName}-${Date.now()}`; - } - - /** - * Show version history modal + * Show version history modal (placeholder for future implementation) */ async showVersionHistory(contentId, element, onRestore) { try { - // Get version history from API (we'll need to pass this in) + // Get version history from API const apiClient = this.getApiClient(); - const versions = await apiClient.getContentVersions(contentId); + if (!apiClient) { + console.warn('No API client configured for version history'); + return; + } + const versions = await apiClient.getContentVersions(contentId); + // Create version history modal const historyModal = this.createVersionHistoryModal(contentId, versions, onRestore); document.body.appendChild(historyModal); - - // Focus and setup handlers + + // Setup handlers this.setupVersionHistoryHandlers(historyModal, contentId); } catch (error) { @@ -478,7 +81,7 @@ export class InsertrFormRenderer { } /** - * Create version history modal + * Create version history modal (simplified placeholder) */ createVersionHistoryModal(contentId, versions, onRestore) { const modal = document.createElement('div'); @@ -547,7 +150,6 @@ export class InsertrFormRenderer { if (await this.confirmRestore()) { await this.restoreVersion(contentId, versionId); modal.remove(); - // Refresh the current form or close it this.closeForm(); } }); @@ -626,177 +228,6 @@ export class InsertrFormRenderer { return this.apiClient || window.insertrAPIClient || null; } - /** - * Reposition modal based on current element size and ensure visibility - */ - repositionModal(element, overlay) { - // Wait for next frame to ensure DOM is updated - requestAnimationFrame(() => { - const rect = element.getBoundingClientRect(); - const form = overlay.querySelector('.insertr-edit-form'); - - // Calculate new position below the current element boundaries - const newTop = rect.bottom + window.scrollY + 10; - - // Update modal position - overlay.style.top = `${newTop}px`; - - // After repositioning, ensure modal is still visible - this.ensureModalVisible(element, overlay); - }); - } - - /** - * Ensure modal is fully visible by scrolling viewport if necessary - */ - ensureModalVisible(element, overlay) { - // Wait for next frame to ensure DOM is updated - requestAnimationFrame(() => { - const modal = overlay.querySelector('.insertr-edit-form'); - const modalRect = modal.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - - // Calculate if modal extends below viewport - const modalBottom = modalRect.bottom; - const viewportBottom = viewportHeight; - - if (modalBottom > viewportBottom) { - // Calculate scroll amount needed with some padding - const scrollAmount = modalBottom - viewportBottom + 20; - - window.scrollBy({ - top: scrollAmount, - behavior: 'smooth' - }); - } - }); - } - - /** - * Setup form event handlers - */ - setupFormHandlers(form, overlay, element, config, { onSave, onCancel }) { - const saveBtn = form.querySelector('.insertr-btn-save'); - const cancelBtn = form.querySelector('.insertr-btn-cancel'); - const elementType = this.getElementType(element, config); - - // Setup live preview for input changes - this.setupLivePreview(form, element, elementType); - - if (saveBtn) { - saveBtn.addEventListener('click', () => { - // Clear preview before saving (makes changes permanent) - this.previewManager.clearPreview(element); - const formData = this.extractFormData(form); - onSave(formData); - this.closeForm(); - }); - } - - if (cancelBtn) { - cancelBtn.addEventListener('click', () => { - // Clear preview to restore original content - this.previewManager.clearPreview(element); - onCancel(); - this.closeForm(); - }); - } - - // Version History button - const historyBtn = form.querySelector('.insertr-btn-history'); - if (historyBtn) { - historyBtn.addEventListener('click', () => { - const contentId = historyBtn.getAttribute('data-content-id'); - this.showVersionHistory(contentId, element, onSave); - }); - } - - // ESC key to cancel - const keyHandler = (e) => { - if (e.key === 'Escape') { - this.previewManager.clearPreview(element); - onCancel(); - this.closeForm(); - document.removeEventListener('keydown', keyHandler); - } - }; - document.addEventListener('keydown', keyHandler); - - // Click outside to cancel - overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - this.previewManager.clearPreview(element); - onCancel(); - this.closeForm(); - } - }); - } - - setupLivePreview(form, element, elementType) { - // Get all input elements that should trigger preview updates - const inputs = form.querySelectorAll('input, textarea'); - - inputs.forEach(input => { - input.addEventListener('input', () => { - const newValue = this.extractInputValue(form, elementType); - this.previewManager.schedulePreview(element, newValue, elementType); - }); - }); - } - - extractInputValue(form, elementType) { - // Extract current form values for preview - const textInput = form.querySelector('input[name="text"]'); - const urlInput = form.querySelector('input[name="url"]'); - const contentInput = form.querySelector('input[name="content"], textarea[name="content"]'); - - if (textInput && urlInput) { - // Link field - return { - text: textInput.value, - url: urlInput.value - }; - } else if (contentInput) { - // Text or textarea field - return contentInput.value; - } - - return ''; - } - - getElementType(element, config) { - // Determine element type for preview handling - if (config.type === 'link') return 'link'; - if (config.type === 'markdown') return 'markdown'; - if (config.type === 'textarea') return 'textarea'; - - const tagName = element.tagName.toLowerCase(); - return tagName === 'p' ? 'p' : 'text'; - } - - /** - * Extract form data - */ - extractFormData(form) { - const data = {}; - - // Handle different field types - const textInput = form.querySelector('input[name="text"]'); - const urlInput = form.querySelector('input[name="url"]'); - const contentInput = form.querySelector('input[name="content"], textarea[name="content"]'); - - if (textInput && urlInput) { - // Link field - data.text = textInput.value; - data.url = urlInput.value; - } else if (contentInput) { - // Text or textarea field - data.text = contentInput.value; - } - - return data; - } - /** * Escape HTML to prevent XSS */ @@ -808,10 +239,11 @@ export class InsertrFormRenderer { } /** - * Setup form styles + * Setup form styles (consolidated and simplified) */ setupStyles() { const styles = ` + /* Overlay and Form Container */ .insertr-form-overlay { position: absolute; z-index: 10000; @@ -826,8 +258,11 @@ export class InsertrFormRenderer { width: 100%; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + min-width: 600px; + max-width: 800px; } + /* Form Header */ .insertr-form-header { font-weight: 600; color: #1f2937; @@ -839,6 +274,7 @@ export class InsertrFormRenderer { letter-spacing: 0.5px; } + /* Form Groups and Fields */ .insertr-form-group { margin-bottom: 1rem; } @@ -874,6 +310,7 @@ export class InsertrFormRenderer { box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1); } + /* Markdown Editor Styling */ .insertr-form-textarea { min-height: 120px; resize: vertical; @@ -888,6 +325,7 @@ export class InsertrFormRenderer { background-color: #f8fafc; } + /* Form Actions */ .insertr-form-actions { display: flex; gap: 0.5rem; @@ -929,6 +367,22 @@ export class InsertrFormRenderer { background: #4b5563; } + .insertr-btn-history { + background: #6f42c1; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + font-size: 0.875rem; + } + + .insertr-btn-history:hover { + background: #5a359a; + } + .insertr-form-help { font-size: 0.75rem; color: #6b7280; @@ -960,12 +414,7 @@ export class InsertrFormRenderer { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } - /* Enhanced modal sizing for comfortable editing */ - .insertr-edit-form { - min-width: 600px; /* Ensures ~70 character width */ - max-width: 800px; - } - + /* Responsive Design */ @media (max-width: 768px) { .insertr-edit-form { min-width: 90vw; @@ -979,10 +428,159 @@ export class InsertrFormRenderer { } } - /* Enhanced input styling for comfortable editing */ - .insertr-form-input { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; - letter-spacing: 0.02em; + /* Version History Modal Styles */ + .insertr-version-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10001; + } + + .insertr-version-backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + } + + .insertr-version-content-modal { + background: white; + border-radius: 8px; + max-width: 600px; + width: 100%; + max-height: 80vh; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + } + + .insertr-version-header { + padding: 20px 20px 0; + border-bottom: 1px solid #eee; + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; + } + + .insertr-version-header h3 { + margin: 0 0 20px; + color: #333; + font-size: 18px; + } + + .insertr-btn-close { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #666; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + } + + .insertr-btn-close:hover { + color: #333; + } + + .insertr-version-list { + overflow-y: auto; + padding: 20px; + flex: 1; + } + + .insertr-version-item { + border: 1px solid #e1e5e9; + border-radius: 6px; + padding: 16px; + margin-bottom: 12px; + background: #f8f9fa; + } + + .insertr-version-meta { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; + font-size: 13px; + } + + .insertr-version-label { + font-weight: 600; + color: #0969da; + } + + .insertr-version-date { + color: #656d76; + } + + .insertr-version-user { + color: #656d76; + } + + .insertr-version-content { + margin-bottom: 12px; + padding: 8px; + background: white; + border-radius: 4px; + font-family: monospace; + font-size: 14px; + color: #24292f; + white-space: pre-wrap; + } + + .insertr-version-actions { + display: flex; + gap: 8px; + } + + .insertr-btn-restore { + background: #0969da; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + } + + .insertr-btn-restore:hover { + background: #0860ca; + } + + .insertr-btn-view-diff { + background: #f6f8fa; + color: #24292f; + border: 1px solid #d1d9e0; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + } + + .insertr-btn-view-diff:hover { + background: #f3f4f6; + } + + .insertr-version-empty { + text-align: center; + color: #656d76; + font-style: italic; + padding: 40px 20px; } `; @@ -991,4 +589,4 @@ export class InsertrFormRenderer { styleSheet.innerHTML = styles; document.head.appendChild(styleSheet); } -} +} \ No newline at end of file diff --git a/lib/src/ui/markdown-editor.js b/lib/src/ui/markdown-editor.js deleted file mode 100644 index 425345b..0000000 --- a/lib/src/ui/markdown-editor.js +++ /dev/null @@ -1,446 +0,0 @@ -/** - * Unified Markdown Editor - Handles both single and multiple element editing - */ -import { markdownConverter } from '../utils/markdown.js'; - -export class MarkdownEditor { - constructor() { - this.currentOverlay = null; - this.previewManager = new MarkdownPreviewManager(); - } - - /** - * Edit elements with markdown - unified interface for single or multiple elements - * @param {HTMLElement|HTMLElement[]} elements - Element(s) to edit - * @param {Function} onSave - Save callback - * @param {Function} onCancel - Cancel callback - */ - edit(elements, onSave, onCancel) { - // Normalize to array - const elementArray = Array.isArray(elements) ? elements : [elements]; - const context = new MarkdownContext(elementArray); - - // Close any existing editor - this.close(); - - // Create unified editor form - const form = this.createMarkdownForm(context); - const overlay = this.createOverlay(form); - - // Position relative to primary element - this.positionForm(context.primaryElement, overlay); - - // Setup unified event handlers - this.setupEventHandlers(form, overlay, context, { onSave, onCancel }); - - // Show editor - document.body.appendChild(overlay); - this.currentOverlay = overlay; - - // Focus textarea - const textarea = form.querySelector('textarea'); - if (textarea) { - setTimeout(() => textarea.focus(), 100); - } - - return overlay; - } - - /** - * Create markdown editing form - */ - createMarkdownForm(context) { - const config = this.getMarkdownConfig(context); - const currentContent = context.extractMarkdown(); - - const form = document.createElement('div'); - form.className = 'insertr-edit-form'; - - form.innerHTML = ` -
${config.label}
-
- -
- Supports Markdown formatting (bold, italic, links, etc.) -
-
-
- - -
- `; - - return form; - } - - /** - * Get markdown configuration based on context - */ - getMarkdownConfig(context) { - const elementCount = context.elements.length; - - if (elementCount === 1) { - const element = context.elements[0]; - const tag = element.tagName.toLowerCase(); - - switch (tag) { - case 'h3': case 'h4': case 'h5': case 'h6': - return { - label: 'Title (Markdown)', - rows: 2, - placeholder: 'Enter title using markdown...' - }; - case 'p': - return { - label: 'Content (Markdown)', - rows: 4, - placeholder: 'Enter content using markdown...' - }; - case 'span': - return { - label: 'Text (Markdown)', - rows: 2, - placeholder: 'Enter text using markdown...' - }; - default: - return { - label: 'Content (Markdown)', - rows: 3, - placeholder: 'Enter content using markdown...' - }; - } - } else { - return { - label: `Group Content (${elementCount} elements)`, - rows: Math.max(8, elementCount * 2), - placeholder: 'Edit all content together using markdown...' - }; - } - } - - /** - * Setup unified event handlers - */ - setupEventHandlers(form, overlay, context, { onSave, onCancel }) { - const textarea = form.querySelector('textarea'); - const saveBtn = form.querySelector('.insertr-btn-save'); - const cancelBtn = form.querySelector('.insertr-btn-cancel'); - - // Initialize preview manager - this.previewManager.setActiveContext(context); - - // Setup debounced live preview - if (textarea) { - textarea.addEventListener('input', () => { - const markdown = textarea.value; - this.previewManager.schedulePreview(context, markdown); - }); - } - - // Save handler - if (saveBtn) { - saveBtn.addEventListener('click', () => { - const markdown = textarea.value; - - // Update elements with final content - context.applyMarkdown(markdown); - - // Clear preview styling - this.previewManager.clearPreview(); - - // Callback and close - onSave({ text: markdown }); - this.close(); - }); - } - - // Cancel handler - if (cancelBtn) { - cancelBtn.addEventListener('click', () => { - this.previewManager.clearPreview(); - onCancel(); - this.close(); - }); - } - - // ESC key handler - const keyHandler = (e) => { - if (e.key === 'Escape') { - this.previewManager.clearPreview(); - onCancel(); - this.close(); - document.removeEventListener('keydown', keyHandler); - } - }; - document.addEventListener('keydown', keyHandler); - - // Click outside handler - overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - this.previewManager.clearPreview(); - onCancel(); - this.close(); - } - }); - } - - /** - * Create overlay with backdrop - */ - createOverlay(form) { - const overlay = document.createElement('div'); - overlay.className = 'insertr-form-overlay'; - overlay.appendChild(form); - return overlay; - } - - /** - * Position form relative to primary element - */ - positionForm(element, overlay) { - const rect = element.getBoundingClientRect(); - const form = overlay.querySelector('.insertr-edit-form'); - const viewportWidth = window.innerWidth; - - // Calculate optimal width - let formWidth; - if (viewportWidth < 768) { - formWidth = Math.min(viewportWidth - 40, 500); - } else { - const minComfortableWidth = 600; - const maxWidth = Math.min(viewportWidth * 0.9, 800); - formWidth = Math.max(minComfortableWidth, Math.min(rect.width * 1.5, maxWidth)); - } - - form.style.width = `${formWidth}px`; - - // Position below element - const top = rect.bottom + window.scrollY + 10; - const centerLeft = rect.left + window.scrollX + (rect.width / 2) - (formWidth / 2); - const minLeft = 20; - const maxLeft = window.innerWidth - formWidth - 20; - const left = Math.max(minLeft, Math.min(centerLeft, maxLeft)); - - overlay.style.position = 'absolute'; - overlay.style.top = `${top}px`; - overlay.style.left = `${left}px`; - overlay.style.zIndex = '10000'; - - // Ensure visibility - this.ensureModalVisible(element, overlay); - } - - /** - * Ensure modal is visible by scrolling if needed - */ - ensureModalVisible(element, overlay) { - requestAnimationFrame(() => { - const modal = overlay.querySelector('.insertr-edit-form'); - const modalRect = modal.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - - if (modalRect.bottom > viewportHeight) { - const scrollAmount = modalRect.bottom - viewportHeight + 20; - window.scrollBy({ - top: scrollAmount, - behavior: 'smooth' - }); - } - }); - } - - /** - * Close current editor - */ - close() { - if (this.previewManager) { - this.previewManager.clearPreview(); - } - - if (this.currentOverlay) { - this.currentOverlay.remove(); - this.currentOverlay = null; - } - } - - /** - * Escape HTML to prevent XSS - */ - escapeHtml(text) { - if (typeof text !== 'string') return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } -} - -/** - * Markdown Context - Represents single or multiple elements for editing - */ -class MarkdownContext { - constructor(elements) { - this.elements = elements; - this.primaryElement = elements[0]; // Used for positioning - this.originalContent = null; - } - - /** - * Extract markdown content from elements - */ - extractMarkdown() { - if (this.elements.length === 1) { - // Single element - convert its HTML to markdown - return markdownConverter.htmlToMarkdown(this.elements[0].innerHTML); - } else { - // Multiple elements - combine and convert to markdown - return markdownConverter.extractGroupMarkdown(this.elements); - } - } - - /** - * Apply markdown content to elements - */ - applyMarkdown(markdown) { - if (this.elements.length === 1) { - // Single element - convert markdown to HTML and apply - const html = markdownConverter.markdownToHtml(markdown); - this.elements[0].innerHTML = html; - } else { - // Multiple elements - use group update logic - markdownConverter.updateGroupElements(this.elements, markdown); - } - } - - /** - * Store original content for preview restoration - */ - storeOriginalContent() { - this.originalContent = this.elements.map(el => el.innerHTML); - } - - /** - * Restore original content (for preview cancellation) - */ - restoreOriginalContent() { - if (this.originalContent) { - this.elements.forEach((el, index) => { - if (this.originalContent[index] !== undefined) { - el.innerHTML = this.originalContent[index]; - } - }); - } - } - - /** - * Apply preview styling - */ - applyPreviewStyling() { - this.elements.forEach(el => { - el.classList.add('insertr-preview-active'); - }); - - // Also apply to primary element if it's a container - if (this.primaryElement.classList.contains('insertr-group')) { - this.primaryElement.classList.add('insertr-preview-active'); - } - } - - /** - * Remove preview styling - */ - removePreviewStyling() { - this.elements.forEach(el => { - el.classList.remove('insertr-preview-active'); - }); - - // Also remove from containers - if (this.primaryElement.classList.contains('insertr-group')) { - this.primaryElement.classList.remove('insertr-preview-active'); - } - } -} - -/** - * Unified Preview Manager for Markdown Content - */ -class MarkdownPreviewManager { - constructor() { - this.previewTimeout = null; - this.activeContext = null; - this.resizeObserver = null; - } - - setActiveContext(context) { - this.clearPreview(); - this.activeContext = context; - this.startResizeObserver(); - } - - schedulePreview(context, markdown) { - // Clear existing timeout - if (this.previewTimeout) { - clearTimeout(this.previewTimeout); - } - - // Schedule new preview with 500ms debounce - this.previewTimeout = setTimeout(() => { - this.updatePreview(context, markdown); - }, 500); - } - - updatePreview(context, markdown) { - // Store original content if first preview - if (!context.originalContent) { - context.storeOriginalContent(); - } - - // Apply preview content - context.applyMarkdown(markdown); - context.applyPreviewStyling(); - } - - clearPreview() { - if (this.activeContext) { - this.activeContext.restoreOriginalContent(); - this.activeContext.removePreviewStyling(); - this.activeContext = null; - } - - if (this.previewTimeout) { - clearTimeout(this.previewTimeout); - this.previewTimeout = null; - } - - this.stopResizeObserver(); - } - - startResizeObserver() { - this.stopResizeObserver(); - - if (this.activeContext) { - this.resizeObserver = new ResizeObserver(() => { - // Handle height changes for modal repositioning - if (this.onHeightChange) { - this.onHeightChange(this.activeContext.primaryElement); - } - }); - - this.activeContext.elements.forEach(el => { - this.resizeObserver.observe(el); - }); - } - } - - stopResizeObserver() { - if (this.resizeObserver) { - this.resizeObserver.disconnect(); - this.resizeObserver = null; - } - } - - setHeightChangeCallback(callback) { - this.onHeightChange = callback; - } -} \ No newline at end of file diff --git a/test-ids.html b/test-ids.html deleted file mode 100644 index 92df999..0000000 --- a/test-ids.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - Test ID Generation - - - -
-

Transform Your Business with Expert Consulting

-

We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.

- Get Started Today -
- - - - \ No newline at end of file