diff --git a/.docs/summary-benches-markdown-bench-rs.md b/.docs/summary-benches-markdown-bench-rs.md new file mode 100644 index 0000000..260f71d --- /dev/null +++ b/.docs/summary-benches-markdown-bench-rs.md @@ -0,0 +1,153 @@ +# File Summary: benches/markdown_bench.rs + +## File Path +`benches/markdown_bench.rs` + +## Purpose +Performance benchmarking suite using Criterion to measure markdown-to-HTML conversion speed in native Rust environment (non-WASM). Provides baseline performance metrics for optimization efforts and regression testing. + +## Key Components + +### Benchmark Configuration +- **`criterion_group!(benches, benchmark_markdown)`** - Defines benchmark group +- **`criterion_main!(benches)`** - Entry point for Criterion benchmark runner +- Uses `#[bench]` harness = false in Cargo.toml to use Criterion instead of built-in benchmarking + +### Test Data +- **`BENCHMARK_TEXT`** - Standardized markdown sample for consistent measurements + - Same content as `tests/web.rs` for cross-environment comparison + - Includes diverse markdown syntax: + - 3 levels of headings + - Bold and italic formatting + - Nested lists (unordered and ordered) + - Blockquotes + - Fenced code blocks with language specification + - Links + - Tables + - Represents realistic document complexity + +## Benchmark Functions + +### `benchmark_markdown(c: &mut Criterion)` +**Purpose**: Measures raw markdown conversion performance + +**Implementation**: +- Creates `Options::default()` outside timing loop (warm-up) +- Uses `black_box()` to prevent compiler optimizations +- Calls `markdown::to_html_with_options()` repeatedly +- Criterion automatically: + - Determines optimal iteration count + - Calculates statistical metrics (mean, median, std dev) + - Detects performance regressions + - Generates detailed reports + +**Metrics Captured**: +- Throughput (iterations per second) +- Mean execution time +- Standard deviation +- Confidence intervals +- Outlier detection + +## Dependencies +- `criterion` (0.5) - Statistical benchmarking framework + - Provides `black_box()` to prevent optimization + - Handles warm-up, iteration counting, and statistical analysis + - Generates HTML reports with graphs +- `markdown` - Same conversion library used in production code +- `terraphim_editor` - Project crate (for potential future benchmarks) + +## Integration Points + +### Execution +- **Command**: `cargo bench` +- **Output**: Console summary + HTML report in `target/criterion/` +- **Baseline Comparison**: Can save baselines for regression detection + +### CI/CD Integration +- Can be integrated into automated performance testing +- Baseline comparisons catch performance regressions +- Results can be tracked over time + +## Notable Patterns + +### Black Box Pattern +```rust +markdown::to_html_with_options(black_box(BENCHMARK_TEXT), &options) +``` +- Prevents compiler from optimizing away the actual work +- Ensures realistic measurements +- Forces full execution path + +### Options Reuse +- Creates `Options::default()` once before benchmark +- Avoids measuring allocation/initialization overhead +- Focuses purely on conversion logic + +## Performance Characteristics + +### What This Measures +- **Pure Rust Performance**: Native code without WASM overhead +- **Conversion Algorithm Speed**: Core markdown parsing and HTML generation +- **Baseline for WASM Comparison**: Compare against `tests/web.rs` benchmark + +### What This Doesn't Measure +- WASM runtime overhead +- DOM manipulation speed +- Event handler latency +- Browser-specific performance +- Memory allocation patterns + +## Benchmark Results Interpretation + +### Typical Metrics +- **Mean Time**: Average conversion time per iteration +- **Throughput**: Conversions per second +- **Variance**: Consistency of performance +- **Outliers**: Anomalous measurements (GC, system interruption) + +### Comparison with Browser Tests +- Native benchmarks typically faster than WASM + DOM updates +- Browser test (`tests/web.rs`) measures end-to-end performance +- This benchmark isolates conversion logic + +## Use Cases + +### Development Workflow +1. **Optimization**: Measure impact of code changes +2. **Regression Detection**: Catch performance degradation +3. **Profiling Guide**: Identify hot paths for optimization +4. **Algorithm Comparison**: Evaluate alternative implementations + +### Performance Goals +- Establish baseline performance metrics +- Set acceptable performance thresholds +- Track performance trends over project lifetime +- Validate optimization efforts + +## Technical Considerations + +### Benchmark Accuracy +- Criterion runs multiple iterations for statistical significance +- Outlier filtering removes anomalous measurements +- Warm-up iterations prevent cold-start bias +- Uses high-precision timers + +### Platform Variations +- Performance varies by CPU, OS, and system load +- Important to benchmark on target deployment platforms +- Relative measurements more reliable than absolute + +### Optimization Considerations +- Results guide optimization priorities +- Helps identify whether WASM overhead or algorithm is bottleneck +- Can benchmark individual functions by extracting them + +## Improvement Opportunities +- Add benchmarks for different document sizes (small, medium, large, huge) +- Benchmark individual markdown features separately +- Add memory usage profiling +- Compare different markdown crate options +- Benchmark with different `Options` configurations +- Add worst-case scenarios (deeply nested structures, very long lines) +- Benchmark incremental updates vs full re-rendering +- Profile allocation patterns diff --git a/.docs/summary-build-sh.md b/.docs/summary-build-sh.md new file mode 100644 index 0000000..c94a35f --- /dev/null +++ b/.docs/summary-build-sh.md @@ -0,0 +1,133 @@ +# File Summary: build.sh + +## File Path +`build.sh` + +## Purpose +Production build orchestration script that coordinates the full build pipeline: compiling Rust to WASM, bundling JavaScript, and assembling an NPM-ready package with examples for all module formats. + +## Build Pipeline + +### Step 1: WASM Compilation +```bash +wasm-pack build --target web +``` +- Compiles Rust to WASM for web target +- Generates `pkg/` directory with: + - `terraphim_editor_bg.wasm` (compiled module) + - `terraphim_editor.js` (JavaScript bindings) + - `package.json` (auto-generated) + +### Step 2: NPM Dependencies +```bash +npm install +``` +- Installs Vite and plugins +- Required for JavaScript bundling step + +### Step 3: Vite Build +```bash +npm run build +``` +- Runs `vite build` (configured in `vite.config.js`) +- Produces multi-format library bundles in `dist/` +- Creates ESM, UMD, and IIFE variants + +### Step 4: Package Assembly +```bash +rm -rf package +mkdir -p package/dist/{js,css,wasm} +``` +- Cleans previous package output +- Creates directory structure for npm package + +### Step 5: Artifact Copying +```bash +cp -r dist/* package/dist/ +cp -r public/wasm/* package/dist/wasm/ +cp -r public/js/config.js package/dist/js/ +cp -r public/css/terraphim-editor.css package/dist/css/ +cp README.md LICENSE package/ +``` +- Consolidates all build artifacts into `package/` +- Includes WASM binaries, JS bundles, CSS, and metadata files + +### Step 6: Package.json Generation +```bash +VERSION=$(grep '^version' Cargo.toml | head -1 | cut -d '"' -f 2) +cat > package/package.json << EOL +... +EOL +``` +- Extracts version from `Cargo.toml` (single source of truth) +- Generates npm-compatible `package.json` +- Defines entry points for all module formats +- Specifies exports map and peer dependencies + +### Step 7: Example File Generation +Creates three example HTML files demonstrating different module formats: +- `example-esm.html` — ES module with import map +- `example-umd.html` — Universal module with global variable +- `example-iife.html` — Immediately-invoked function expression + +Each example shows: +- How to include the library +- How to initialize the editor +- Container setup +- WASM URL configuration + +## Output Structure +``` +package/ +├── dist/ +│ ├── js/ +│ │ ├── terraphim-editor.mjs (ESM) +│ │ ├── terraphim-editor.umd.js (UMD) +│ │ ├── terraphim-editor.iife.js (IIFE) +│ │ ├── terraphim-editor.d.ts (TypeScript) +│ │ └── config.js +│ ├── css/ +│ │ └── terraphim-editor.css +│ └── wasm/ +│ └── terraphim_editor_bg.wasm +├── package.json +├── README.md +├── LICENSE +└── example-*.html (3 files) +``` + +## Key Features + +### Version Synchronization +- Single version source in `Cargo.toml` +- Automatically propagated to npm package +- Prevents version mismatch between Rust and JS + +### Module Format Support +- **ESM**: Modern bundlers (Webpack, Rollup, Vite) +- **UMD**: Node.js, RequireJS, browser globals +- **IIFE**: Direct CDN/script tag usage + +### Complete Examples +- Each module format has working example +- Shows integration best practices +- Demonstrates async initialization pattern + +## Integration Notes +- Run after code changes to prepare for publishing +- Output in `package/` ready for `npm publish` +- Examples can be tested directly in browser + +## Error Handling +- No explicit error checking (relies on command failures) +- Commands fail fast (bash default behavior) +- Should add `set -e` for explicit fail-fast + +## Improvement Opportunities +- Add `set -e` for strict error handling +- Add `set -x` option for debugging +- Validate `pkg/` existence before copying +- Add checksum/integrity verification +- Generate TypeScript declaration files +- Add build artifact size reporting +- Implement incremental builds (skip unchanged steps) diff --git a/.docs/summary-cargo-toml.md b/.docs/summary-cargo-toml.md new file mode 100644 index 0000000..ce04847 --- /dev/null +++ b/.docs/summary-cargo-toml.md @@ -0,0 +1,173 @@ +# File Summary: Cargo.toml + +## File Path +`Cargo.toml` + +## Purpose +Rust package manifest defining project metadata, dependencies, build configuration, and WebAssembly compilation settings for the terraphim-editor WASM library. + +## Key Configuration + +### Package Metadata +- **Name**: `terraphim-editor` +- **Version**: `0.1.0` +- **Edition**: `2021` (Rust 2021 edition features) + +### Library Configuration +```toml +[lib] +crate-type = ["cdylib", "rlib"] +``` +- **`cdylib`**: Creates dynamic library for WebAssembly target + - Essential for WASM compilation + - Produces `.wasm` file for browser consumption +- **`rlib`**: Creates Rust library for native compilation + - Enables `cargo test` and `cargo bench` + - Allows linking in Rust-native contexts + +## Dependencies + +### Core WASM Dependencies +**`wasm-bindgen = "0.2.89"`** +- JavaScript/Rust FFI bindings +- Enables exporting Rust functions to JavaScript +- Provides `#[wasm_bindgen]` attribute macro +- Handles type conversions between Rust and JS + +**`web-sys = { version = "0.3.66", features = [...] }`** +- Browser API bindings +- Feature flags enable specific web APIs: + - **DOM APIs**: `Document`, `Element`, `HtmlElement`, `Node`, `Text` + - **Form Elements**: `HtmlTextAreaElement`, `HtmlDivElement` + - **Events**: `InputEvent` + - **Window APIs**: `Window`, `Performance` + - **Console**: `console` for logging + +**`console_error_panic_hook = "0.1.7"`** +- Better panic messages in browser console +- Essential for debugging WASM code +- Converts Rust panics to JavaScript errors + +### Functional Dependencies +**`markdown = "1.0.0-alpha.21"`** +- Core markdown-to-HTML conversion +- Alpha version indicates active development +- Provides `to_html_with_options()` API +- Configurable via `Options` struct + +**`rinja = { version = "0.3.5" }`** +- Template rendering engine (Jinja2-like syntax) +- Compile-time template parsing +- Used for rendering `templates/editor.html` +- Type-safe template rendering + +## Development Dependencies + +**`wasm-bindgen-test = "0.3.39"`** +- WASM-specific test framework +- Enables browser-based integration tests +- Required for `tests/web.rs` + +**`criterion = "0.5"`** +- Statistical benchmarking framework +- Used in `benches/markdown_bench.rs` +- Provides performance regression detection + +## Benchmark Configuration + +```toml +[[bench]] +name = "markdown_bench" +harness = false +``` +- **`harness = false`**: Disables default benchmark harness +- Allows using Criterion framework +- Creates separate benchmark binary + +## Build Implications + +### WASM Target Build +```bash +cargo build --target wasm32-unknown-unknown --release +``` +- Uses `cdylib` crate type +- Produces `terraphim_editor.wasm` +- Requires `wasm-bindgen` CLI for final processing + +### Native Build +```bash +cargo build +``` +- Uses `rlib` crate type +- Enables running tests and benchmarks +- Standard Rust compilation + +### Feature Considerations +- No feature flags defined +- All web-sys features explicitly listed +- Prevents unnecessary code bloat +- Optimizes WASM binary size + +## Dependency Version Strategy + +### Stable vs Alpha +- **Stable**: wasm-bindgen, web-sys, console_error_panic_hook (production-ready) +- **Alpha**: markdown crate (1.0.0-alpha.21) + - Potential breaking changes in future updates + - May need migration to stable 1.0 when available + +### Version Pinning +- Exact versions not pinned (allows patch updates) +- Uses caret requirements (`^`) +- Balances stability with security updates + +## Integration with Build Tools + +### wasm-pack Integration +- Respects `cdylib` crate type +- Generates JavaScript bindings automatically +- Creates `pkg/` directory with bundled artifacts + +### Trunk Integration +- Trunk compiles WASM automatically +- Uses `Cargo.toml` for dependency resolution +- Serves compiled WASM during development + +### Vite Integration +- Vite uses pre-built WASM from `pkg/` directory +- Cargo.toml not directly used by Vite +- Build pipeline: Cargo → wasm-pack → Vite + +## Notable Patterns + +### Dual Compilation Targets +- Single `Cargo.toml` supports both WASM and native +- `cdylib` + `rlib` enables testing native performance +- Allows benchmarking without WASM overhead + +### Minimal Dependencies +- Only 6 dependencies (4 production, 2 dev) +- Reduces WASM bundle size +- Faster compilation times +- Smaller attack surface + +## Security Considerations + +### Dependency Audit +- All dependencies from crates.io +- Should run `cargo audit` regularly +- Alpha dependencies require extra scrutiny + +### WASM Sandboxing +- cdylib restricts capabilities to browser sandbox +- No file system access +- No network access (unless via browser APIs) +- No process spawning + +## Improvement Opportunities +- Consider pinning markdown crate to specific alpha version +- Add `[profile.release]` optimizations for WASM size reduction +- Could add `wasm-opt` configuration +- Consider adding `web-sys` features incrementally as needed +- Add `[package.metadata.wasm-pack]` configuration section +- Consider LTO (Link Time Optimization) for smaller WASM binaries diff --git a/.docs/summary-package-json.md b/.docs/summary-package-json.md new file mode 100644 index 0000000..337e102 --- /dev/null +++ b/.docs/summary-package-json.md @@ -0,0 +1,52 @@ +# File Summary: package.json + +## File Path +`package.json` + +## Purpose +JavaScript package manifest for building and distributing the editor as a consumable library via Vite. Defines build scripts, output formats (ESM/UMD/IIFE), and peer dependencies for UI components. + +## Key Fields +- `name`: `terraphim-editor` +- `version`: `1.0.0` +- `type`: `module` (enables ESM semantics) +- `files`: Publish whitelist: `dist`, `README.md`, `LICENSE` +- Entry Points: + - `main`: `./dist/js/terraphim-editor.umd.js` (Common entry for Node/UMD) + - `module`: `./dist/js/terraphim-editor.mjs` (ESM import) + - `unpkg`: `./dist/js/terraphim-editor.iife.js` (CDN/global) + - `types`: `./dist/js/terraphim-editor.d.ts` (TypeScript types) +- `exports` map: + - `.`: Conditional exports for `types`/`import`/`require`/`default` + - `./style.css`: Exposes CSS entry `./dist/css/terraphim-editor.css` + +## Scripts +- `dev`: `vite` (starts Vite dev server; used for iterating on JS wrapper and examples) +- `build`: `vite build` (produces library bundles in `dist/`) +- `preview`: `vite preview` +- `prepublishOnly`: `npm run build` (ensures bundles are built before publishing) + +## Dev Dependencies +- `vite` `^5.0.10` — bundler/build tool +- `vite-plugin-wasm` `^3.3.0` — supports importing WASM modules +- `vite-plugin-top-level-await` `^1.4.1` — enables top-level await usage for WASM init + +## Peer Dependencies +- `@shoelace-style/shoelace` `^2.12.0` — UI component library used by examples and editor toolbar + - Declared as peer to avoid bundling and to let host app control version + +## Role in Build System +- Complements Rust/WASM build by bundling JS wrapper (`public/js/terraphim-editor.js`) into multiple formats +- Works alongside `build.sh` to assemble npm-ready package structure in `package/` +- Not responsible for compiling Rust to WASM (handled by wasm-pack/Trunk) + +## Output Artifacts +- `dist/js/terraphim-editor.{mjs,umd.js,iife.js}` — library builds +- `dist/js/terraphim-editor.d.ts` — types +- `dist/css/terraphim-editor.css` — styles via asset pipeline +- `dist/wasm/*` — WASM copied via rollup asset config + +## Integration Notes +- Consumers can import as ESM, require as UMD, or load via IIFE on CDN +- CSS exposed as `terraphim-editor/style.css` for straightforward theming +- Requires Shoelace to be provided by consumer (peer dependency) diff --git a/.docs/summary-public-js-config-js.md b/.docs/summary-public-js-config-js.md new file mode 100644 index 0000000..eca7f74 --- /dev/null +++ b/.docs/summary-public-js-config-js.md @@ -0,0 +1,41 @@ +# File Summary: public/js/config.js + +## File Path +`public/js/config.js` + +## Purpose +Configuration file defining editor behavior, keyboard shortcuts, toolbar commands, initial content, and styling options. Exposed as global `window.EditorConfig` for consumption by editor initialization code. + +## Key Configuration Objects + +###Shortcuts Array +Defines keyboard shortcuts for markdown formatting: +- Bold (`ctrl+b`), Italic (`ctrl+i`), Code (`ctrl+k`), Link (`ctrl+l`), Heading (`ctrl+h`) +- Each shortcut specifies: name, key combo, prefix/suffix strings, description + +### Commands Array +Toolbar button configurations for visual formatting: +- Headings (H1-H3), Bold, Italic, Underline +- Each command has: name, icon, prefix/suffix for text wrapping + +### Initial Content +Default markdown text displayed on editor load - demonstrates features and provides user onboarding + +### Styles Object +Layout configuration: editor/preview dimensions, split position (50%) + +## Export Pattern +```javascript +if (typeof exports !== 'undefined') { + exports.EditorConfig = EditorConfig; +} else { + window.EditorConfig = EditorConfig; +} +``` +Supports both CommonJS and browser globals for maximum compatibility + +## Integration +- Loaded before editor.js in index.html +- Used by `MarkdownEditor` class for initialization +- Used by `TeraphimEditor` wrapper for configuration +- Single source of truth for UI behavior diff --git a/.docs/summary-src-lib-rs.md b/.docs/summary-src-lib-rs.md new file mode 100644 index 0000000..3a2d3fc --- /dev/null +++ b/.docs/summary-src-lib-rs.md @@ -0,0 +1,96 @@ +# File Summary: src/lib.rs + +## File Path +`src/lib.rs` + +## Purpose +Core Rust WebAssembly library that implements the terraphim-editor's markdown rendering engine and editor initialization logic. This is the main entry point for the WASM module that bridges Rust functionality with JavaScript/web environment. + +## Key Components + +### Entry Point Function +- **`run()`** - WASM-bindgen start function that auto-executes on WASM initialization + - Configures panic hook for better error reporting in browser console + - Locates the `#editor-container` DOM element + - Renders the initial editor template using Rinja + - Sets up markdown conversion event handlers + - Returns `Result<(), JsValue>` for error handling + +### Template System +- **`EditorTemplate`** struct - Rinja template data structure + - `initial_content: String` - Pre-populated markdown content + - `initial_preview: String` - Pre-rendered HTML preview + - Uses `templates/editor.html` as the template source + +### Markdown Conversion +- **`setup_markdown_conversion()`** - Configures live preview functionality + - Attaches input event listener to `.markdown-input` textarea + - Converts markdown to HTML on every keystroke + - Updates `.markdown-preview` div with rendered content + - Uses closure with `forget()` for persistent event handling + +- **`render_markdown()`** - Public WASM-exposed function + - Takes markdown string as input + - Returns rendered HTML or error + - Can be called directly from JavaScript + +### Constants +- **`INITIAL_MARKDOWN`** - Welcome text demonstrating editor features + - Showcases various markdown elements (headings, lists, quotes, code blocks) + - Serves as user onboarding content + +## Dependencies +- `wasm-bindgen` - Core FFI bindings between Rust and JavaScript +- `web-sys` - Web API bindings (DOM manipulation, events) + - Document, Element, HtmlElement, HtmlTextAreaElement, HtmlDivElement + - InputEvent, Node, Window, Text, Performance +- `console_error_panic_hook` - Better panic messages in browser console +- `markdown` crate (1.0.0-alpha.21) - Markdown to HTML conversion with Options API +- `rinja` (0.3.5) - Template rendering engine + +## Integration Points + +### JavaScript Integration +- Exports `render_markdown()` function for direct JS calls +- Auto-runs `run()` function via `#[wasm_bindgen(start)]` attribute +- Expects `#editor-container` element to exist in DOM before initialization + +### Template Integration +- Requires `templates/editor.html` file for UI structure +- Template receives initial content and preview data +- Shoelace web components rendered via template + +### Event Flow +1. WASM module loads → `run()` executes automatically +2. Editor template rendered into `#editor-container` +3. Event listener attached to textarea +4. User types → Input event fires → Markdown converted → Preview updated + +## Notable Patterns + +### Error Handling +- Consistent use of `Result` for WASM-compatible errors +- `.ok_or_else()` with descriptive error messages for Option unwrapping +- `.map_err()` to convert Rust errors to JsValue strings + +### Memory Management +- Event handler closure uses `.forget()` to prevent deallocation +- Necessary for persistent event listeners in WASM context +- Clone of preview element to move into closure + +### Performance Considerations +- Live markdown conversion on every input event (no debouncing) +- Synchronous rendering (blocking) +- Direct DOM manipulation via web-sys + +## Testing +- Unit tests included for markdown conversion logic +- Template rendering validation +- Tests verify HTML output contains expected tags + +## Technical Debt & Improvement Opportunities +- No input sanitization before setting innerHTML (XSS risk) +- No debouncing for markdown conversion (performance concern for large documents) +- Handler closure leaked with `forget()` - no cleanup mechanism +- No error recovery UI when conversion fails +- Hard-coded selector strings could be constants diff --git a/.docs/summary-templates-editor-html.md b/.docs/summary-templates-editor-html.md new file mode 100644 index 0000000..275fcfe --- /dev/null +++ b/.docs/summary-templates-editor-html.md @@ -0,0 +1,31 @@ +# File Summary: templates/editor.html + +## File Path +`templates/editor.html` + +## Purpose +Rinja (Jinja2-like) template rendered by Rust code to generate the editor's HTML structure. Creates split-pane interface with markdown input, live preview, toolbar, and keyboard shortcut dialog. + +## Key Components +- **Split Panel**: Uses Shoelace `` for resizable panes +- **Toolbar**: Button group with formatting controls and help button +- **Markdown Input**: Textarea with monospace font for editing +- **Preview Pane**: Div for rendered HTML output +- **Shortcuts Dialog**: Modal showing keyboard shortcuts + +## Template Variables +- `{{initial_content}}` - Pre-populated markdown text +- `{{initial_preview|safe}}` - Pre-rendered HTML (unescaped) + +## Shoelace Components Used +- `sl-split-panel`, `sl-button-group`, `sl-button`, `sl-icon`, `sl-dialog`, `sl-divider` + +## Styling +- Inline ` - - -
- - - \ No newline at end of file diff --git a/docs/CI_TROUBLESHOOTING.md b/docs/CI_TROUBLESHOOTING.md new file mode 100644 index 0000000..bb0a2e1 --- /dev/null +++ b/docs/CI_TROUBLESHOOTING.md @@ -0,0 +1,120 @@ +# CI Troubleshooting Guide + +## E2E Test Timeout Issues + +### Problem: Playwright Timeout Waiting for Dev Server + +**Symptom:** +``` +Error: Timed out waiting 120000ms from config.webServer. +``` + +**Root Cause:** +The Vite dev server was binding to `localhost` by default, while Playwright was configured to connect to `127.0.0.1`. In some CI environments (particularly containerized ones), these addresses may not resolve to the same location, causing connection timeouts. + +**Solution:** +Explicitly set `host: '127.0.0.1'` in `vite.config.js`: + +```javascript +server: { + host: '127.0.0.1', // Match Playwright config + port: 8080, + strictPort: true, + fs: { + allow: ['..'] + } +} +``` + +This ensures the server binds to the exact address Playwright is checking in `playwright.config.js`: + +```javascript +webServer: { + command: 'npm run dev', + url: 'http://127.0.0.1:8080', + // ... +} +``` + +### Verification + +After the fix, the Vite startup log should show: +``` +➜ Local: http://127.0.0.1:8080/ +``` + +Instead of: +``` +➜ Local: http://localhost:8080/ +``` + +## Other Common CI Issues + +### WASM Files Not Found + +**Symptom:** +Tests fail because WASM module can't be loaded. + +**Solution:** +Verify WASM files are being copied correctly: +```bash +test -f public/wasm/terraphim_editor_bg.wasm +test -f public/js/terraphim_editor.js +``` + +The build script in `vite.config.js` handles this automatically when `pkg/` directory exists. + +### Browser-Specific Failures + +Use `fail-fast: false` in GitHub Actions matrix to see all browser failures: + +```yaml +strategy: + fail-fast: false + matrix: + browser: [chromium, firefox, webkit] +``` + +### Debugging Tips + +1. **Enable verbose logging** in `playwright.config.js`: + ```javascript + webServer: { + stdout: 'pipe', + stderr: 'pipe', + } + ``` + +2. **Increase timeout** for slow CI environments: + ```javascript + webServer: { + timeout: 120 * 1000, // 2 minutes + } + ``` + +3. **Run locally in CI mode**: + ```bash + CI=true npm run test:e2e + ``` + +4. **Check specific browser**: + ```bash + npm run test:e2e:chromium + npm run test:e2e:firefox + npm run test:e2e:webkit + ``` + +## Test Coverage + +Current test suite: +- **49 total tests** (47 E2E + 2 Rust) +- **3 browser engines** (Chromium, Firefox, WebKit) +- **3 UI variants** (Shoelace, Vanilla, Web Awesome) +- **Dynamic loading** scenarios tested + +## Related Files + +- `.github/workflows/ci.yml` - CI pipeline configuration +- `playwright.config.js` - E2E test configuration +- `vite.config.js` - Dev server and build configuration +- `tests/e2e/` - E2E test suites diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d512ed5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,184 @@ +# Terraphim Editor Documentation + +> Your one-stop reference for using, developing, and publishing **terraphim-editor**. + +--- + +## 1 Project Structure Overview + +``` +terraphim-editor/ +│ Cargo.toml — Rust crate manifest (WASM library) +│ Trunk.toml — Trunk build configuration +│ build.sh — End-to-end packaging script (wasm-pack ➜ vite ➜ npm) +│ vite.config.js — Front-end bundler configuration (ESM/UMD/IIFE builds) +│ README.md — High-level project description (quick start) +│ +├─ src/ — Rust source (WASM entry + helpers) +│ └─ lib.rs — `run()` initialises UI, `render_markdown()` helper +│ +├─ templates/ — HTML fragments rendered by Rinja (Rust side) +│ └─ editor.html — Editor UI template injected at runtime +│ +├─ public/ — Static assets copied verbatim by Trunk/Vite +│ ├─ css/… — Editor stylesheet +│ ├─ js/… — Thin JS wrapper + config +│ └─ wasm/… — WASM binary copied for dev-server +│ +├─ pkg/ — wasm-pack output (auto-generated, source of truth for WASM bindings) +├─ dist/ — Vite production bundles (auto-generated) +├─ package/ — Final npm-ready directory with dist/, examples, licence, etc. +│ +└─ tests/ & benches/ — Browser + criterion benchmarks +``` + +--- + +## 2 Using the Editor in Your Project + +### 2.1 Install from npm (recommended) +```bash +npm install terraphim-editor +# or +pnpm add terraphim-editor +``` + +### 2.2 Choose a Build Flavour +* **ESM** (modern bundlers) +* **UMD** (Node, classic script loaders) +* **IIFE** (drop-in ` + +
+ +``` + +> **Note**: Point `wasmUrl` to the bundled WASM file *relative to* your site. + +--- + +## 3 Local Development Workflow + +### 3.1 Prerequisites +* Rust + `wasm32-unknown-unknown` target +* [Trunk](https://trunkrs.dev) (`cargo install trunk`) +* Node ≥18 & npm/pnpm/yarn + +### 3.2 Start Dev Server (live-reload) +```bash +# One-shot helper that ensures WASM + JS outputs exist +trunk serve +# visit http://127.0.0.1:8080 +``` +Trunk watches Rust & template changes and rebuilds on the fly. Vite handles JS/TS bundling. + +### 3.3 Running Tests +```bash +# Rust unit tests +cargo test + +# Browser WASM tests +wasm-pack test --chrome +``` + +### 3.4 Benchmarks +```bash +cargo bench # criterion +``` + +--- + +## 4 Building & Packaging + +### 4.1 Generate Production Assets +```bash +./build.sh # orchestrates wasm-pack ➜ vite ➜ npm package preparation +``` +Outputs: +* `dist/` – minified JS bundles + source maps +* `package/` – npm-publishable directory with CSS, JS bundles, WASM, examples + +### 4.2 Create .tgz for Inspection +```bash +cd package && npm pack +``` +The tarball should contain: +* `dist/css/terraphim-editor.css` +* `dist/js/terraphim-editor.{mjs,umd.js,iife.js}` (+ maps) +* `dist/wasm/terraphim_editor_bg.wasm` +* Example HTML files +* Licence & README + +--- + +## 5 Publishing to npm +1. Make sure you're in a **clean git state** and on the correct version tag. +2. Bump version in `Cargo.toml` (and optionally `package.json` template inside `build.sh`). +3. Run `./build.sh`. +4. `cd package` +5. `npm publish --access public` + +> The script auto-generates a fresh `package.json` with the right version and export map. + +--- + +## 6 Contributing Guidelines + +### 6.1 Branch & Commit +* Use feature branches: `feature/…`, `fix/…` +* Write clear, concise commit messages (present tense). + Example: `fix: handle empty markdown input gracefully`. + +### 6.2 Code Style & Principles +* Follow Rust & JavaScript idioms. + (See `.editorconfig` if present.) +* Async Rust via **tokio** (see `user_rules`). +* Keep public APIs minimal; document all exports with `rustdoc` & JSDoc. + +### 6.3 Pull Request Checklist +- [ ] `cargo test` & `wasm-pack test` pass +- [ ] `npm run build` succeeds +- [ ] Benchmarks unaffected (or updated) +- [ ] Docs & examples updated + +--- + +## 7 FAQ + +**Q: The preview doesn't update!** +A: Ensure the WASM file is served from `wasmUrl`, and that `render_markdown()` is called on `input`. + +**Q: Can I style the editor?** +A: Yes – override variables in `public/css/terraphim-editor.css` or extend with your own stylesheet. + +--- + +## 8 Useful Commands Reference +| Purpose | Command | +|---|---| +| build & package | `./build.sh` | +| dev server | `trunk serve` | +| unit tests | `cargo test` | +| browser tests | `wasm-pack test --chrome` | +| benchmark | `cargo bench` | +| generate .tgz | `cd package && npm pack` | +| publish | `cd package && npm publish --access public` | + +--- + +Happy hacking! 🎉 \ No newline at end of file diff --git a/editor_config.toml b/editor_config.toml deleted file mode 100644 index 972f5f4..0000000 --- a/editor_config.toml +++ /dev/null @@ -1,24 +0,0 @@ -[shortcuts.bold] -key = "ctrl+b" -prefix = "**" -suffix = "**" - -[shortcuts.italic] -key = "ctrl+i" -prefix = "_" -suffix = "_" - -[shortcuts.code] -key = "ctrl+k" -prefix = "`" -suffix = "`" - -[shortcuts.link] -key = "ctrl+l" -prefix = "[" -suffix = "](url)" - -[shortcuts.heading] -key = "ctrl+h" -prefix = "# " -suffix = "" \ No newline at end of file diff --git a/example.html b/example.html deleted file mode 100644 index 25195f4..0000000 --- a/example.html +++ /dev/null @@ -1,18 +0,0 @@ - - - -
- - \ No newline at end of file diff --git a/index.html b/index.html index 4bbf61c..96bd24e 100644 --- a/index.html +++ b/index.html @@ -17,20 +17,18 @@ - - + - -
+
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1c5cc65 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1440 @@ +{ + "name": "terraphim-editor", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "terraphim-editor", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.56.1", + "playwright": "^1.56.1", + "vite": "^5.0.10", + "vite-plugin-top-level-await": "^1.4.1", + "vite-plugin-wasm": "^3.3.0" + }, + "peerDependencies": { + "@shoelace-style/shoelace": "^2.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", + "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT", + "peer": true + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", + "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@lit/react": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.6.tgz", + "integrity": "sha512-QIss8MPh6qUoFJmuaF4dSHts3qCsA36S3HcOLiNPShxhgYPr4XJRnCBKPipk85sR9xr6TQrOcDMfexwbNdJHYA==", + "license": "BSD-3-Clause", + "peer": true, + "peerDependencies": { + "@types/react": "17 || 18" + } + }, + "node_modules/@lit/reactive-element": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", + "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", + "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", + "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", + "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", + "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", + "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", + "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", + "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", + "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", + "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", + "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", + "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", + "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", + "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", + "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", + "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", + "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", + "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", + "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shoelace-style/animations": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@shoelace-style/animations/-/animations-1.2.0.tgz", + "integrity": "sha512-avvo1xxkLbv2dgtabdewBbqcJfV0e0zCwFqkPMnHFGbJbBHorRFfMAHh1NG9ymmXn0jW95ibUVH03E1NYXD6Gw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/claviska" + } + }, + "node_modules/@shoelace-style/localize": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.2.1.tgz", + "integrity": "sha512-r4C9C/5kSfMBIr0D9imvpRdCNXtUNgyYThc4YlS6K5Hchv1UyxNQ9mxwj+BTRH2i1Neits260sR3OjKMnplsFA==", + "license": "MIT", + "peer": true + }, + "node_modules/@shoelace-style/shoelace": { + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/@shoelace-style/shoelace/-/shoelace-2.19.1.tgz", + "integrity": "sha512-aRwf9Bh9g+x206/pNN9Ty6DUZA4NQJuK9ET0zUA10223aoxhB+p6/GG01vmhB9DU53Nukj5nMBy5AxJQ/6vmLQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ctrl/tinycolor": "^4.1.0", + "@floating-ui/dom": "^1.6.12", + "@lit/react": "^1.0.6", + "@shoelace-style/animations": "^1.2.0", + "@shoelace-style/localize": "^3.2.1", + "composed-offset-position": "^0.0.6", + "lit": "^3.2.1", + "qr-creator": "^1.0.0" + }, + "engines": { + "node": ">=14.17.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/claviska" + } + }, + "node_modules/@swc/core": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.4.tgz", + "integrity": "sha512-ut3zfiTLORMxhr6y/GBxkHmzcGuVpwJYX4qyXWuBKkpw/0g0S5iO1/wW7RnLnZbAi8wS/n0atRZoaZlXWBkeJg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.17" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.10.4", + "@swc/core-darwin-x64": "1.10.4", + "@swc/core-linux-arm-gnueabihf": "1.10.4", + "@swc/core-linux-arm64-gnu": "1.10.4", + "@swc/core-linux-arm64-musl": "1.10.4", + "@swc/core-linux-x64-gnu": "1.10.4", + "@swc/core-linux-x64-musl": "1.10.4", + "@swc/core-win32-arm64-msvc": "1.10.4", + "@swc/core-win32-ia32-msvc": "1.10.4", + "@swc/core-win32-x64-msvc": "1.10.4" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.4.tgz", + "integrity": "sha512-sV/eurLhkjn/197y48bxKP19oqcLydSel42Qsy2zepBltqUx+/zZ8+/IS0Bi7kaWVFxerbW1IPB09uq8Zuvm3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.4.tgz", + "integrity": "sha512-gjYNU6vrAUO4+FuovEo9ofnVosTFXkF0VDuo1MKPItz6e2pxc2ale4FGzLw0Nf7JB1sX4a8h06CN16/pLJ8Q2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.4.tgz", + "integrity": "sha512-zd7fXH5w8s+Sfvn2oO464KDWl+ZX1MJiVmE4Pdk46N3PEaNwE0koTfgx2vQRqRG4vBBobzVvzICC3618WcefOA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.4.tgz", + "integrity": "sha512-+UGfoHDxsMZgFD3tABKLeEZHqLNOkxStu+qCG7atGBhS4Slri6h6zijVvf4yI5X3kbXdvc44XV/hrP/Klnui2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.4.tgz", + "integrity": "sha512-cDDj2/uYsOH0pgAnDkovLZvKJpFmBMyXkxEG6Q4yw99HbzO6QzZ5HDGWGWVq/6dLgYKlnnmpjZCPPQIu01mXEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.4.tgz", + "integrity": "sha512-qJXh9D6Kf5xSdGWPINpLGixAbB5JX8JcbEJpRamhlDBoOcQC79dYfOMEIxWPhTS1DGLyFakAx2FX/b2VmQmj0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.4.tgz", + "integrity": "sha512-A76lIAeyQnHCVt0RL/pG+0er8Qk9+acGJqSZOZm67Ve3B0oqMd871kPtaHBM0BW3OZAhoILgfHW3Op9Q3mx3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.4.tgz", + "integrity": "sha512-e6j5kBu4fIY7fFxFxnZI0MlEovRvp50Lg59Fw+DVbtqHk3C85dckcy5xKP+UoXeuEmFceauQDczUcGs19SRGSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.4.tgz", + "integrity": "sha512-RSYHfdKgNXV/amY5Tqk1EWVsyQnhlsM//jeqMLw5Fy9rfxP592W9UTumNikNRPdjI8wKKzNMXDb1U29tQjN0dg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.4.tgz", + "integrity": "sha512-1ujYpaqfqNPYdwKBlvJnOqcl+Syn3UrQ4XE0Txz6zMYgyh6cdU6a3pxqLqIUSJ12MtXRA9ZUhEz1ekU3LfLWXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", + "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/react": { + "version": "18.3.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", + "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "peer": true + }, + "node_modules/composed-offset-position": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.6.tgz", + "integrity": "sha512-Q7dLompI6lUwd7LWyIcP66r4WcS9u7AL2h8HaeipiRfCRPLMWqRx8fYsjb4OHi6UQFifO7XtNC2IlEJ1ozIFxw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@floating-ui/utils": "^0.2.5" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lit": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz", + "integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.1.0", + "lit-html": "^3.2.0" + } + }, + "node_modules/lit-element": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz", + "integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.2.0" + } + }, + "node_modules/lit-html": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz", + "integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/qr-creator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/qr-creator/-/qr-creator-1.0.0.tgz", + "integrity": "sha512-C0cqfbS1P5hfqN4NhsYsUXePlk9BO+a45bAQ3xLYjBL3bOIFzoVEjs79Fado9u9BPBD3buHi3+vY+C8tHh4qMQ==", + "license": "MIT", + "peer": true + }, + "node_modules/rollup": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", + "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.29.1", + "@rollup/rollup-android-arm64": "4.29.1", + "@rollup/rollup-darwin-arm64": "4.29.1", + "@rollup/rollup-darwin-x64": "4.29.1", + "@rollup/rollup-freebsd-arm64": "4.29.1", + "@rollup/rollup-freebsd-x64": "4.29.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", + "@rollup/rollup-linux-arm-musleabihf": "4.29.1", + "@rollup/rollup-linux-arm64-gnu": "4.29.1", + "@rollup/rollup-linux-arm64-musl": "4.29.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", + "@rollup/rollup-linux-riscv64-gnu": "4.29.1", + "@rollup/rollup-linux-s390x-gnu": "4.29.1", + "@rollup/rollup-linux-x64-gnu": "4.29.1", + "@rollup/rollup-linux-x64-musl": "4.29.1", + "@rollup/rollup-win32-arm64-msvc": "4.29.1", + "@rollup/rollup-win32-ia32-msvc": "4.29.1", + "@rollup/rollup-win32-x64-msvc": "4.29.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.4.tgz", + "integrity": "sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.7.0", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.4.1.tgz", + "integrity": "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa5452f --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "terraphim-editor", + "version": "1.0.0", + "type": "module", + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "main": "./dist/js/terraphim-editor.umd.js", + "module": "./dist/js/terraphim-editor.mjs", + "unpkg": "./dist/js/terraphim-editor.iife.js", + "types": "./dist/js/terraphim-editor.d.ts", + "exports": { + ".": { + "types": "./dist/js/terraphim-editor.d.ts", + "import": "./dist/js/terraphim-editor.mjs", + "require": "./dist/js/terraphim-editor.umd.js", + "default": "./dist/js/terraphim-editor.iife.js" + }, + "./style.css": "./dist/css/terraphim-editor.css" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "prepublishOnly": "npm run build", + "test": "npm run test:rust && npm run test:e2e", + "test:rust": "cargo test --all-targets", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed", + "test:e2e:chromium": "playwright test --project=chromium", + "test:e2e:firefox": "playwright test --project=firefox", + "test:e2e:webkit": "playwright test --project=webkit" + }, + "devDependencies": { + "@playwright/test": "^1.56.1", + "playwright": "^1.56.1", + "vite": "^5.0.10", + "vite-plugin-top-level-await": "^1.4.1", + "vite-plugin-wasm": "^3.3.0" + }, + "peerDependencies": { + "@shoelace-style/shoelace": "^2.12.0" + } +} diff --git a/package/LICENSE b/package/LICENSE new file mode 100644 index 0000000..256c953 --- /dev/null +++ b/package/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Terraphim Editor Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/package/README.md b/package/README.md new file mode 100644 index 0000000..ee2410d --- /dev/null +++ b/package/README.md @@ -0,0 +1,74 @@ +# Terraphim Editor + +A WebAssembly-based Markdown editor built with Rust, [Shoelace](https://shoelace.style/) styles and no other dependencies. +Trunk is used for the build system + +## Features + +- Live Markdown preview +- Pure Javascript for front end and WebAssembly implementation for rendering Markdown +- Minimal external dependencies +- Modern web components UI + +## Prerequisites + +1. Install Rust: +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +2. Add the WebAssembly target: +```bash +rustup target add wasm32-unknown-unknown +``` + +3. Install Trunk: +```bash +cargo install trunk +``` + +## Development + +Run the development server: + +```bash +trunk serve +``` + +Visit `http://127.0.0.1:8080` in your browser. + +## Testing + +### Rust Tests +Run the Rust unit tests: +```bash +cargo test +``` + +### Frontend Tests (Wasm) +Run the frontend tests: +```bash +wasm-pack test --chrome +``` + +## Building for Production + +Create a production build: + +```bash +trunk build --release +``` + +The output will be in the `dist` directory. + +## Contributing + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/package/example-esm.html b/package/example-esm.html new file mode 100644 index 0000000..79b7184 --- /dev/null +++ b/package/example-esm.html @@ -0,0 +1,32 @@ + + + + + Terraphim Editor - ESM Example + + + + +
+ + + diff --git a/package/example-iife.html b/package/example-iife.html new file mode 100644 index 0000000..6ad842e --- /dev/null +++ b/package/example-iife.html @@ -0,0 +1,24 @@ + + + + + Terraphim Editor - IIFE Example + + + + +
+ + + diff --git a/package/example-umd.html b/package/example-umd.html new file mode 100644 index 0000000..465a52a --- /dev/null +++ b/package/example-umd.html @@ -0,0 +1,24 @@ + + + + + Terraphim Editor - UMD Example + + + + +
+ + + diff --git a/package/package.json b/package/package.json new file mode 100644 index 0000000..a5131c1 --- /dev/null +++ b/package/package.json @@ -0,0 +1,22 @@ +{ + "name": "terraphim-editor", + "version": "0.1.0", + "description": "WebAssembly-based Markdown editor component built with Rust", + "main": "./dist/js/terraphim-editor.umd.js", + "module": "./dist/js/terraphim-editor.mjs", + "unpkg": "./dist/js/terraphim-editor.iife.js", + "types": "./dist/js/terraphim-editor.d.ts", + "files": ["dist", "README.md", "LICENSE"], + "exports": { + ".": { + "types": "./dist/js/terraphim-editor.d.ts", + "import": "./dist/js/terraphim-editor.mjs", + "require": "./dist/js/terraphim-editor.umd.js", + "default": "./dist/js/terraphim-editor.iife.js" + }, + "./style.css": "./dist/css/terraphim-editor.css" + }, + "peerDependencies": { + "@shoelace-style/shoelace": "^2.12.0" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..b7667e7 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,70 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://127.0.0.1:8080', + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run dev', + url: 'http://127.0.0.1:8080', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + stdout: 'pipe', + stderr: 'pipe', + }, +}); diff --git a/public/css/terraphim-editor.css b/public/css/terraphim-editor.css new file mode 100644 index 0000000..73d2e82 --- /dev/null +++ b/public/css/terraphim-editor.css @@ -0,0 +1,49 @@ +.terraphim-editor { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; +} + +.editor-toolbar { + padding: 8px; + border-bottom: 1px solid #ccc; +} + +.editor-content { + display: flex; + flex: 1; + gap: 16px; + padding: 16px; +} + +.editor-input, +.editor-preview { + flex: 1; + min-height: 0; +} + +.markdown-input { + width: 100%; + height: 100%; + resize: none; + border: 1px solid #ccc; + border-radius: 4px; + padding: 8px; +} + +.markdown-preview { + border: 1px solid #ccc; + border-radius: 4px; + padding: 8px; + overflow-y: auto; +} + +#formatting-toolbar { + display: flex; + gap: 8px; +} + +#formatting-toolbar sl-button::part(base) { + padding: 4px 8px; +} \ No newline at end of file diff --git a/public/example-esm.html b/public/example-esm.html new file mode 100644 index 0000000..e85da30 --- /dev/null +++ b/public/example-esm.html @@ -0,0 +1,39 @@ + + + + + Teraphim Editor - ESM Example + + + + + + +

Teraphim Editor - ESM Example

+
+ + + + + + + \ No newline at end of file diff --git a/public/example-iife.html b/public/example-iife.html new file mode 100644 index 0000000..7c55008 --- /dev/null +++ b/public/example-iife.html @@ -0,0 +1,90 @@ + + + + + Teraphim Editor - IIFE Example + + + + + + + + + + +

Teraphim Editor - IIFE Example

+
+ + + + + + + + \ No newline at end of file diff --git a/public/example-shoelace.html b/public/example-shoelace.html new file mode 100644 index 0000000..6fa07dc --- /dev/null +++ b/public/example-shoelace.html @@ -0,0 +1,95 @@ + + + + + Terraphim Editor - Shoelace + + + + + + + + + + + + + + + + + + + + + +
+

Terraphim Editor - Shoelace

+

+ This version uses Shoelace web components for a polished, modern UI. + Beautiful and accessible by default! +

+ +
+
+ + + + + diff --git a/public/example-umd.html b/public/example-umd.html new file mode 100644 index 0000000..efe4019 --- /dev/null +++ b/public/example-umd.html @@ -0,0 +1,42 @@ + + + + + Teraphim Editor - UMD Example + + + + + + +

Teraphim Editor - UMD Example

+
+ + + + + + + \ No newline at end of file diff --git a/public/example-vanilla.html b/public/example-vanilla.html new file mode 100644 index 0000000..3e6ae5b --- /dev/null +++ b/public/example-vanilla.html @@ -0,0 +1,79 @@ + + + + + Terraphim Editor - Vanilla HTML/CSS + + + + + + + + +
+

Terraphim Editor - Vanilla HTML/CSS

+

+ This version uses pure HTML, CSS, and JavaScript with no external UI libraries. + Zero dependencies, maximum performance! +

+ +
+
+ + + + + diff --git a/public/example-webawesome.html b/public/example-webawesome.html new file mode 100644 index 0000000..c03cabf --- /dev/null +++ b/public/example-webawesome.html @@ -0,0 +1,113 @@ + + + + + Terraphim Editor - Web Awesome + + + + + + + + + + + + + + + + + + + + + + + +
+

Terraphim Editor - Web Awesome

+

+ This version uses Web Awesome, the next-generation successor to Shoelace. + Enhanced features, more themes, and a comprehensive design system! +

+ +
+ Note: To use Web Awesome in production, create a project at + webawesome.com and replace the CDN links above + with your project-specific Web Awesome CDN link. This example currently uses Shoelace as a fallback + since Web Awesome uses compatible syntax. +
+ +
+
+ + + + + diff --git a/public/example.html b/public/example.html new file mode 100644 index 0000000..d913f80 --- /dev/null +++ b/public/example.html @@ -0,0 +1,94 @@ + + + + + Terraphim Editor Example + + + + + + + + + + + + + + + + + + + + +

Terraphim Editor Examples

+ +

ES Module Example

+
+ +

UMD Example

+
+ + + + + + + + + \ No newline at end of file diff --git a/public/index-multistyle.html b/public/index-multistyle.html new file mode 100644 index 0000000..7850eb3 --- /dev/null +++ b/public/index-multistyle.html @@ -0,0 +1,323 @@ + + + + + Terraphim Editor - Multi-Style Demo + + + + + + + + + + + + + + + + + + + +
+
+

🎨 Terraphim Editor

+

Choose Your Style: Three Ways to Build

+ +
+ + + Shoelace + Vanilla + Web Awesome + +
+
+
+ +
+
+

Shoelace Components

+

Beautiful, accessible web components with a comprehensive design system.

+
+
Professional UI components
+
Accessibility built-in
+
Theme customization
+
+
+ +
+
+ + + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..035183c --- /dev/null +++ b/public/index.html @@ -0,0 +1,20 @@ + + + + + Terraphim Editor Examples + + + +

Terraphim Editor Examples

+ + + +

Click any of the above links to view the different examples of the Terraphim Editor implementation.

+
+ + diff --git a/public/js/alert.js b/public/js/alert.js deleted file mode 100644 index 88721cb..0000000 --- a/public/js/alert.js +++ /dev/null @@ -1 +0,0 @@ -alert("Hello Trunk"); \ No newline at end of file diff --git a/public/js/editor-vanilla.js b/public/js/editor-vanilla.js new file mode 100644 index 0000000..0895785 --- /dev/null +++ b/public/js/editor-vanilla.js @@ -0,0 +1,422 @@ +class MarkdownEditorVanilla { + constructor(config) { + this.config = config; + this.shortcuts = config.shortcuts; + this.commands = config.commands.map(cmd => ({ + ...cmd, + action: () => this.wrapSelectedText(cmd.prefix, cmd.suffix) + })); + this.isDragging = false; + } + + initialize() { + // Get DOM elements after template is rendered + this.textarea = document.querySelector('.markdown-input'); + this.toolbar = document.querySelector('#formatting-toolbar'); + this.shortcutsList = document.querySelector('#shortcuts-list'); + this.dialog = document.querySelector('.shortcuts-dialog'); + this.helpButton = document.querySelector('#show-help'); + this.closeButton = document.querySelector('#close-help'); + this.divider = document.querySelector('#panel-divider'); + + // Check if elements exist + if (!this.textarea || !this.toolbar || !this.shortcutsList || !this.dialog || !this.helpButton) { + console.error('Required DOM elements not found'); + return; + } + + if (this.shortcuts.length === 0) { + console.error('No shortcuts available'); + return; + } + + this.setupShortcuts(); + this.setupHelpDialog(); + this.setupCommandPalette(); + this.setupResizablePanels(); + } + + wrapSelectedText(prefix, suffix) { + const start = this.textarea.selectionStart; + const end = this.textarea.selectionEnd; + const text = this.textarea.value; + const before = text.substring(0, start); + const selection = text.substring(start, end); + const after = text.substring(end); + + const wrappedText = selection ? selection : 'text'; + this.textarea.value = before + prefix + wrappedText + suffix + after; + + this.textarea.focus(); + this.textarea.selectionStart = selection ? start + prefix.length : start + prefix.length; + this.textarea.selectionEnd = selection ? end + prefix.length : start + prefix.length + 4; + + this.textarea.dispatchEvent(new Event('input')); + } + + getIconSymbol(iconName) { + // Map shoelace icon names to simple text symbols + const iconMap = { + 'type-bold': 'B', + 'type-italic': 'I', + 'type-strikethrough': 'S', + 'code-slash': '', + 'type-h1': 'H1', + 'type-h2': 'H2', + 'type-h3': 'H3', + 'list-ul': '•', + 'list-ol': '1.', + 'link-45deg': '🔗', + 'image': '🖼', + 'quote': '"', + 'question-circle': '?' + }; + return iconMap[iconName] || iconName.charAt(0).toUpperCase(); + } + + setupShortcuts() { + // Create toolbar buttons + this.shortcuts.forEach(shortcut => { + const tooltip = document.createElement('div'); + tooltip.className = 'tooltip'; + + const button = document.createElement('button'); + button.className = 'btn btn-sm'; + button.innerHTML = `${this.getIconSymbol(shortcut.name)}`; + + const tooltipText = document.createElement('span'); + tooltipText.className = 'tooltip-text'; + tooltipText.textContent = shortcut.key; + + tooltip.appendChild(button); + tooltip.appendChild(tooltipText); + + button.addEventListener('click', () => { + this.wrapSelectedText(shortcut.prefix, shortcut.suffix); + }); + + this.toolbar.appendChild(tooltip); + }); + + // Setup keyboard shortcuts + this.textarea.addEventListener('keydown', (e) => { + const key = `${e.ctrlKey ? 'ctrl+' : ''}${e.key.toLowerCase()}`; + const shortcut = this.shortcuts.find(s => s.key === key); + + if (shortcut) { + e.preventDefault(); + this.wrapSelectedText(shortcut.prefix, shortcut.suffix); + } + }); + } + + setupHelpDialog() { + // Create shortcut list items + this.shortcuts.forEach(shortcut => { + const item = document.createElement('div'); + item.className = 'shortcut-item'; + item.innerHTML = ` + ${this.getIconSymbol(shortcut.name)} + ${shortcut.desc} + ${shortcut.key} + `; + this.shortcutsList.appendChild(item); + }); + + this.helpButton.addEventListener('click', () => this.openDialog()); + this.closeButton.addEventListener('click', () => this.closeDialog()); + + // Close dialog when clicking overlay + this.dialog.querySelector('.dialog-overlay').addEventListener('click', () => { + this.closeDialog(); + }); + + // Close on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.dialog.classList.contains('open')) { + this.closeDialog(); + } + }); + } + + openDialog() { + this.dialog.classList.add('open'); + } + + closeDialog() { + this.dialog.classList.remove('open'); + } + + setupResizablePanels() { + let startX, startWidth; + const panel = this.divider.previousElementSibling; + + this.divider.addEventListener('mousedown', (e) => { + this.isDragging = true; + startX = e.clientX; + startWidth = panel.offsetWidth; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + + e.preventDefault(); + }); + + const onMouseMove = (e) => { + if (!this.isDragging) return; + + const deltaX = e.clientX - startX; + const container = this.divider.parentElement; + const newWidth = ((startWidth + deltaX) / container.offsetWidth) * 100; + + // Limit between 20% and 80% + if (newWidth >= 20 && newWidth <= 80) { + panel.style.flex = `0 0 ${newWidth}%`; + this.divider.nextElementSibling.style.flex = `0 0 ${100 - newWidth}%`; + } + }; + + const onMouseUp = () => { + this.isDragging = false; + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + } + + setupCommandPalette() { + // Create inline command menu + const commandMenu = document.createElement('div'); + commandMenu.classList.add('command-menu'); + commandMenu.style.display = 'none'; + commandMenu.setAttribute('tabindex', '0'); + + const commandList = document.createElement('div'); + commandList.classList.add('command-list'); + + commandMenu.appendChild(commandList); + document.body.appendChild(commandMenu); + + let selectedIndex = -1; + let visibleItems = []; + let slashPosition = null; + + // Add commands to the list + this.commands.forEach(cmd => { + const item = document.createElement('div'); + item.classList.add('command-item'); + item.innerHTML = ` + ${this.getIconSymbol(cmd.icon)} + ${cmd.name} + `; + + item.addEventListener('click', () => { + if (slashPosition !== null) { + const text = this.textarea.value; + this.textarea.value = text.substring(0, slashPosition) + text.substring(slashPosition + 1); + this.textarea.selectionStart = slashPosition; + this.textarea.selectionEnd = slashPosition; + } + cmd.action(); + hideCommandMenu(); + }); + + commandList.appendChild(item); + }); + + const updateSelection = () => { + visibleItems = Array.from(commandList.querySelectorAll('.command-item')); + visibleItems.forEach((item, index) => { + if (index === selectedIndex) { + item.classList.add('selected'); + item.scrollIntoView({ block: 'nearest' }); + } else { + item.classList.remove('selected'); + } + }); + }; + + const positionCommandMenu = () => { + const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); + const textareaRect = this.textarea.getBoundingClientRect(); + const menuRect = commandMenu.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Calculate initial position + let left = textareaRect.left + caretPosition.left; + let top = textareaRect.top + caretPosition.top + 20; + + // Adjust horizontal position if menu would go outside viewport + if (left + menuRect.width > viewportWidth) { + left = viewportWidth - menuRect.width - 10; + } + if (left < 0) { + left = 10; + } + + // Adjust vertical position if menu would go outside viewport + if (top + menuRect.height > viewportHeight) { + top = textareaRect.top + caretPosition.top - menuRect.height - 10; + } + if (top < 0) { + top = 10; + } + + commandMenu.style.position = 'fixed'; + commandMenu.style.left = `${left}px`; + commandMenu.style.top = `${top}px`; + }; + + const showCommandMenu = () => { + commandMenu.style.display = 'block'; + selectedIndex = 0; + updateSelection(); + positionCommandMenu(); + commandMenu.focus(); + }; + + const hideCommandMenu = () => { + commandMenu.style.display = 'none'; + selectedIndex = -1; + slashPosition = null; + this.textarea.focus(); + }; + + // Keyboard navigation + commandMenu.addEventListener('keydown', (e) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + selectedIndex = Math.min(selectedIndex + 1, visibleItems.length - 1); + if (selectedIndex === -1 && visibleItems.length > 0) selectedIndex = 0; + updateSelection(); + break; + + case 'ArrowUp': + e.preventDefault(); + selectedIndex = Math.max(selectedIndex - 1, 0); + updateSelection(); + break; + + case 'Enter': + e.preventDefault(); + if (selectedIndex >= 0 && selectedIndex < visibleItems.length) { + visibleItems[selectedIndex].click(); + } + break; + + case 'Escape': + e.preventDefault(); + hideCommandMenu(); + break; + } + }); + + // Show command menu on forward slash + this.textarea.addEventListener('keydown', (e) => { + if (e.key === '/' && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + const start = this.textarea.selectionStart; + const text = this.textarea.value; + this.textarea.value = text.substring(0, start) + '/' + text.substring(this.textarea.selectionEnd); + this.textarea.selectionStart = start + 1; + this.textarea.selectionEnd = start + 1; + + slashPosition = start; + showCommandMenu(); + } + }); + + // Hide menu when clicking outside + document.addEventListener('click', (e) => { + if (!commandMenu.contains(e.target) && e.target !== this.textarea) { + hideCommandMenu(); + } + }); + + // Update menu position on scroll or resize + window.addEventListener('scroll', positionCommandMenu); + window.addEventListener('resize', positionCommandMenu); + this.textarea.addEventListener('scroll', positionCommandMenu); + } +} + +function getCaretCoordinates(element, position) { + const div = document.createElement('div'); + const styles = getComputedStyle(element); + const properties = [ + 'direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY', + 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', + 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', + 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', + 'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', + 'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing' + ]; + + div.style.position = 'absolute'; + div.style.visibility = 'hidden'; + div.style.whiteSpace = 'pre-wrap'; + + properties.forEach(prop => { + div.style[prop] = styles[prop]; + }); + + div.textContent = element.value.substring(0, position); + const span = document.createElement('span'); + span.textContent = element.value.substring(position) || '.'; + div.appendChild(span); + + document.body.appendChild(div); + const coordinates = { + top: span.offsetTop, + left: span.offsetLeft + }; + document.body.removeChild(div); + + return coordinates; +} + +// Update the initEditor function +const initEditor = () => { + const checkElements = () => { + const required = [ + '.markdown-input', + '#formatting-toolbar', + '#shortcuts-list', + '.shortcuts-dialog', + '#show-help' + ]; + + if (required.every(selector => document.querySelector(selector))) { + // Pass the EditorConfig when initializing + const editor = new MarkdownEditorVanilla(window.EditorConfig || { + shortcuts: [], + commands: [], + styles: {} + }); + editor.initialize(); + } else { + // Check again in 100ms + setTimeout(checkElements, 100); + } + }; + + checkElements(); +}; + +// Initialize editor - works with both static and dynamic script loading +function initializeEditor() { + if (window.EditorConfig) { + initEditor(); + } else { + console.error('Editor configuration not found. Make sure config.js is loaded before editor-vanilla.js'); + } +} + +// Run immediately if DOM is ready, otherwise wait for DOMContentLoaded +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeEditor); +} else { + // DOM is already ready, initialize immediately (handles dynamic script loading) + initializeEditor(); +} diff --git a/public/js/editor-webawesome.js b/public/js/editor-webawesome.js new file mode 100644 index 0000000..7c686b8 --- /dev/null +++ b/public/js/editor-webawesome.js @@ -0,0 +1,385 @@ +class MarkdownEditorWebAwesome { + constructor(config) { + this.config = config; + this.shortcuts = config.shortcuts; + this.commands = config.commands.map(cmd => ({ + ...cmd, + action: () => this.wrapSelectedText(cmd.prefix, cmd.suffix) + })); + } + + initialize() { + // Get DOM elements after template is rendered + this.textarea = document.querySelector('.markdown-input'); + this.toolbar = document.querySelector('#formatting-toolbar'); + this.shortcutsList = document.querySelector('#shortcuts-list'); + this.dialog = document.querySelector('.shortcuts-dialog'); + this.helpButton = document.querySelector('#show-help'); + + // Check if elements exist + if (!this.textarea || !this.toolbar || !this.shortcutsList || !this.dialog || !this.helpButton) { + console.error('Required DOM elements not found'); + return; + } + + if (this.shortcuts.length === 0) { + console.error('No shortcuts available'); + return; + } + + this.setupShortcuts(); + this.setupHelpDialog(); + this.setupCommandPalette(); + } + + wrapSelectedText(prefix, suffix) { + const start = this.textarea.selectionStart; + const end = this.textarea.selectionEnd; + const text = this.textarea.value; + const before = text.substring(0, start); + const selection = text.substring(start, end); + const after = text.substring(end); + + const wrappedText = selection ? selection : 'text'; + this.textarea.value = before + prefix + wrappedText + suffix + after; + + this.textarea.focus(); + this.textarea.selectionStart = selection ? start + prefix.length : start + prefix.length; + this.textarea.selectionEnd = selection ? end + prefix.length : start + prefix.length + 4; + + this.textarea.dispatchEvent(new Event('input')); + } + + // Map Shoelace icon names to Web Awesome icon names + mapIconName(shoelaceIcon) { + const iconMap = { + 'type-bold': 'bold', + 'type-italic': 'italic', + 'type-strikethrough': 'strikethrough', + 'code-slash': 'code', + 'type-h1': 'heading', + 'type-h2': 'heading', + 'type-h3': 'heading', + 'list-ul': 'list-ul', + 'list-ol': 'list-ol', + 'link-45deg': 'link', + 'image': 'image', + 'quote': 'quote-right', + 'question-circle': 'circle-question' + }; + return iconMap[shoelaceIcon] || shoelaceIcon; + } + + setupShortcuts() { + // Create toolbar buttons + this.shortcuts.forEach(shortcut => { + const button = document.createElement('wa-tooltip'); + button.setAttribute('content', shortcut.key); + + button.innerHTML = ` + + + + `; + + button.querySelector('wa-button').addEventListener('click', () => { + this.wrapSelectedText(shortcut.prefix, shortcut.suffix); + }); + + this.toolbar.appendChild(button); + }); + + // Setup keyboard shortcuts + this.textarea.addEventListener('keydown', (e) => { + const key = `${e.ctrlKey ? 'ctrl+' : ''}${e.key.toLowerCase()}`; + const shortcut = this.shortcuts.find(s => s.key === key); + + if (shortcut) { + e.preventDefault(); + this.wrapSelectedText(shortcut.prefix, shortcut.suffix); + } + }); + } + + setupHelpDialog() { + // Create shortcut list items + this.shortcuts.forEach(shortcut => { + const item = document.createElement('div'); + item.className = 'shortcut-item'; + item.innerHTML = ` + + ${shortcut.desc} + ${shortcut.key} + `; + this.shortcutsList.appendChild(item); + }); + + this.helpButton.addEventListener('click', () => this.dialog.show()); + } + + setupCommandPalette() { + // Create inline command menu + const commandMenu = document.createElement('div'); + commandMenu.classList.add('command-menu'); + commandMenu.style.display = 'none'; + commandMenu.setAttribute('tabindex', '0'); + + const commandList = document.createElement('div'); + commandList.classList.add('command-list'); + + commandMenu.appendChild(commandList); + document.body.appendChild(commandMenu); + + let selectedIndex = -1; + let visibleItems = []; + let slashPosition = null; + + // Add commands to the list + this.commands.forEach(cmd => { + const item = document.createElement('div'); + item.classList.add('command-item'); + item.innerHTML = ` + + ${cmd.name} + `; + + item.addEventListener('click', () => { + if (slashPosition !== null) { + const text = this.textarea.value; + this.textarea.value = text.substring(0, slashPosition) + text.substring(slashPosition + 1); + this.textarea.selectionStart = slashPosition; + this.textarea.selectionEnd = slashPosition; + } + cmd.action(); + hideCommandMenu(); + }); + + commandList.appendChild(item); + }); + + const updateSelection = () => { + visibleItems = Array.from(commandList.querySelectorAll('.command-item')); + visibleItems.forEach((item, index) => { + if (index === selectedIndex) { + item.classList.add('selected'); + item.scrollIntoView({ block: 'nearest' }); + } else { + item.classList.remove('selected'); + } + }); + }; + + const positionCommandMenu = () => { + const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); + const textareaRect = this.textarea.getBoundingClientRect(); + const menuRect = commandMenu.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Calculate initial position + let left = textareaRect.left + caretPosition.left; + let top = textareaRect.top + caretPosition.top + 20; + + // Adjust horizontal position if menu would go outside viewport + if (left + menuRect.width > viewportWidth) { + left = viewportWidth - menuRect.width - 10; + } + if (left < 0) { + left = 10; + } + + // Adjust vertical position if menu would go outside viewport + if (top + menuRect.height > viewportHeight) { + top = textareaRect.top + caretPosition.top - menuRect.height - 10; + } + if (top < 0) { + top = 10; + } + + commandMenu.style.position = 'fixed'; + commandMenu.style.left = `${left}px`; + commandMenu.style.top = `${top}px`; + }; + + const showCommandMenu = () => { + commandMenu.style.display = 'block'; + selectedIndex = 0; + updateSelection(); + positionCommandMenu(); + commandMenu.focus(); + }; + + const hideCommandMenu = () => { + commandMenu.style.display = 'none'; + selectedIndex = -1; + slashPosition = null; + this.textarea.focus(); + }; + + // Keyboard navigation + commandMenu.addEventListener('keydown', (e) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + selectedIndex = Math.min(selectedIndex + 1, visibleItems.length - 1); + if (selectedIndex === -1 && visibleItems.length > 0) selectedIndex = 0; + updateSelection(); + break; + + case 'ArrowUp': + e.preventDefault(); + selectedIndex = Math.max(selectedIndex - 1, 0); + updateSelection(); + break; + + case 'Enter': + e.preventDefault(); + if (selectedIndex >= 0 && selectedIndex < visibleItems.length) { + visibleItems[selectedIndex].click(); + } + break; + + case 'Escape': + e.preventDefault(); + hideCommandMenu(); + break; + } + }); + + // Show command menu on forward slash + this.textarea.addEventListener('keydown', (e) => { + if (e.key === '/' && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + const start = this.textarea.selectionStart; + const text = this.textarea.value; + this.textarea.value = text.substring(0, start) + '/' + text.substring(this.textarea.selectionEnd); + this.textarea.selectionStart = start + 1; + this.textarea.selectionEnd = start + 1; + + slashPosition = start; + showCommandMenu(); + } + }); + + // Hide menu when clicking outside + document.addEventListener('click', (e) => { + if (!commandMenu.contains(e.target) && e.target !== this.textarea) { + hideCommandMenu(); + } + }); + + // Update menu position on scroll or resize + window.addEventListener('scroll', positionCommandMenu); + window.addEventListener('resize', positionCommandMenu); + this.textarea.addEventListener('scroll', positionCommandMenu); + } + + showCustomDialog() { + const dialog = document.createElement('wa-dialog'); + dialog.label = 'Custom Formatting'; + + dialog.innerHTML = ` + + + Apply + Cancel + `; + + document.body.appendChild(dialog); + + const [applyBtn, cancelBtn] = dialog.querySelectorAll('wa-button'); + const prefixInput = dialog.querySelector('#prefix-input'); + const suffixInput = dialog.querySelector('#suffix-input'); + + applyBtn.addEventListener('click', () => { + this.wrapSelectedText(prefixInput.value, suffixInput.value); + dialog.hide(); + }); + + cancelBtn.addEventListener('click', () => dialog.hide()); + + dialog.addEventListener('wa-after-hide', () => dialog.remove()); + + dialog.show(); + } +} + +function getCaretCoordinates(element, position) { + const div = document.createElement('div'); + const styles = getComputedStyle(element); + const properties = [ + 'direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY', + 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', + 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', + 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', + 'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', + 'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing' + ]; + + div.style.position = 'absolute'; + div.style.visibility = 'hidden'; + div.style.whiteSpace = 'pre-wrap'; + + properties.forEach(prop => { + div.style[prop] = styles[prop]; + }); + + div.textContent = element.value.substring(0, position); + const span = document.createElement('span'); + span.textContent = element.value.substring(position) || '.'; + div.appendChild(span); + + document.body.appendChild(div); + const coordinates = { + top: span.offsetTop, + left: span.offsetLeft + }; + document.body.removeChild(div); + + return coordinates; +} + +// Update the initEditor function +const initEditor = () => { + const checkElements = () => { + const required = [ + '.markdown-input', + '#formatting-toolbar', + '#shortcuts-list', + '.shortcuts-dialog', + '#show-help' + ]; + + if (required.every(selector => document.querySelector(selector))) { + // Pass the EditorConfig when initializing + const editor = new MarkdownEditorWebAwesome(window.EditorConfig || { + shortcuts: [], + commands: [], + styles: {} + }); + editor.initialize(); + } else { + // Check again in 100ms + setTimeout(checkElements, 100); + } + }; + + checkElements(); +}; + +// Initialize editor - works with both static and dynamic script loading +function initializeEditor() { + if (window.EditorConfig) { + initEditor(); + } else { + console.error('Editor configuration not found. Make sure config.js is loaded before editor-webawesome.js'); + } +} + +// Run immediately if DOM is ready, otherwise wait for DOMContentLoaded +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeEditor); +} else { + // DOM is already ready, initialize immediately (handles dynamic script loading) + initializeEditor(); +} diff --git a/public/js/editor.js b/public/js/editor.js index e686fd1..70e4932 100644 --- a/public/js/editor.js +++ b/public/js/editor.js @@ -348,11 +348,19 @@ const initEditor = () => { checkElements(); }; -// Make sure config is loaded before initializing -document.addEventListener('DOMContentLoaded', () => { +// Initialize editor - works with both static and dynamic script loading +function initializeEditor() { if (window.EditorConfig) { initEditor(); } else { console.error('Editor configuration not found. Make sure config.js is loaded before editor.js'); } -}); \ No newline at end of file +} + +// Run immediately if DOM is ready, otherwise wait for DOMContentLoaded +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeEditor); +} else { + // DOM is already ready, initialize immediately (handles dynamic script loading) + initializeEditor(); +} \ No newline at end of file diff --git a/public/js/shortcuts.js b/public/js/shortcuts.js deleted file mode 100644 index 4f0cf2b..0000000 --- a/public/js/shortcuts.js +++ /dev/null @@ -1,37 +0,0 @@ -window.shortcuts = [ - { - "name": "type-bold", - "key": "ctrl+b", - "prefix": "**", - "suffix": "**", - "desc": "Bold text" - }, - { - "name": "type-italic", - "key": "ctrl+i", - "prefix": "_", - "suffix": "_", - "desc": "Italic text" - }, - { - "name": "code", - "key": "ctrl+k", - "prefix": "`", - "suffix": "`", - "desc": "Inline code" - }, - { - "name": "link", - "key": "ctrl+l", - "prefix": "[", - "suffix": "](url)", - "desc": "Create link" - }, - { - "name": "type-h1", - "key": "ctrl+h", - "prefix": "# ", - "suffix": "", - "desc": "Heading" - } -]; \ No newline at end of file diff --git a/public/js/terraphim-editor.js b/public/js/terraphim-editor.js index 900e24b..17c4118 100644 --- a/public/js/terraphim-editor.js +++ b/public/js/terraphim-editor.js @@ -1,66 +1,112 @@ +import init, { render_markdown } from './terraphim_editor.js'; + class TeraphimEditor { - constructor(targetElement, config = {}) { - this.config = Object.assign({}, EditorConfig, config); - this.targetElement = targetElement; - this.editor = null; - } + constructor(options) { + console.log('TeraphimEditor constructor called with options:', options); + + if (!options || !options.container) { + throw new Error('Container element is required'); + } + + this.container = options.container; + this.config = options.config || {}; + this.wasmUrl = options.wasmUrl || '/wasm/terraphim_editor_bg.wasm'; + + console.log('Container element:', this.container); + console.log('Configuration:', this.config); + } - async initialize() { - // Load required Shoelace components - await this.loadShoelaceComponents(); - - // Create editor container - const container = document.createElement('div'); - container.className = 'terraphim-editor-container'; - this.targetElement.appendChild(container); + async initialize() { + try { + console.log('Initializing editor...'); - // Initialize editor - this.editor = new MarkdownEditor(this.config); - await this.editor.initialize(container); + // Insert the placeholder element *before* loading WASM so the + // Rust `#[wasm_bindgen(start)]` function can find it. + this.container.innerHTML = `
`; - return this; - } + // Initialize WASM; the start function will render the template. + console.log('Initializing WASM from:', this.wasmUrl); + await init(this.wasmUrl); - async loadShoelaceComponents() { - const shoelaceBase = 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.12.0/cdn/'; - - // Load Shoelace theme - if (!document.querySelector('link[href*="shoelace"]')) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = `${shoelaceBase}themes/light.css`; - document.head.appendChild(link); + console.log('WASM rendered editor template'); + + // Apply user-provided initial content & commands + await this.postSetup(); + console.log('Editor initialized successfully'); + } catch (error) { + console.error('Editor initialization failed:', error); + throw error; + } } - // Load required components - const components = [ - 'input/input.js', - 'icon/icon.js', - 'button/button.js', - 'button-group/button-group.js', - 'tooltip/tooltip.js', - 'split-panel/split-panel.js', - 'dialog/dialog.js', - 'divider/divider.js', - 'badge/badge.js' - ]; + async postSetup() { + const input = document.querySelector('.markdown-input'); + const preview = document.querySelector('.markdown-preview'); + const toolbar = document.querySelector('#formatting-toolbar'); + + if (!input || !preview || !toolbar) { + throw new Error('Required editor elements not found after WASM render'); + } - await Promise.all(components.map(async (component) => { - if (!customElements.get(`sl-${component.split('/')[0]}`)) { - await import(`${shoelaceBase}components/${component}`); - } - })); - } + // Populate toolbar with extra commands if provided + if (Array.isArray(this.config.commands)) { + this.config.commands.forEach(command => { + const button = document.createElement('sl-button'); + button.innerHTML = ``; + button.setAttribute('size', 'small'); + button.setAttribute('title', command.name); + button.addEventListener('click', () => { + // Insert formatting + const start = input.selectionStart; + const end = input.selectionEnd; + const before = input.value.substring(0, start); + const selection = input.value.substring(start, end); + const after = input.value.substring(end); + input.value = before + command.prefix + (selection || 'text') + command.suffix + after; + input.focus(); + input.selectionStart = input.selectionEnd = start + command.prefix.length; + // Update preview using WASM helper + try { + preview.innerHTML = render_markdown(input.value); + } catch (err) { + console.error('Markdown render error:', err); + } + }); + toolbar.appendChild(button); + }); + } - getValue() { - return this.editor.textarea.value; - } + // Inject user-provided initial markdown, if any + if (this.config.initialContent) { + input.value = this.config.initialContent; + try { + preview.innerHTML = render_markdown(input.value); + } catch (err) { + console.error('Markdown render error:', err); + } + } - setValue(markdown) { - this.editor.textarea.value = markdown; - this.editor.textarea.dispatchEvent(new Event('input')); - } + // Setup live preview using render_markdown directly + input.addEventListener('input', () => { + try { + preview.innerHTML = render_markdown(input.value); + } catch (err) { + console.error('Markdown render error:', err); + } + }); + } + + destroy() { + if (this.container) { + this.container.innerHTML = ''; + } + } } -// Make it available globally -window.TeraphimEditor = TeraphimEditor; \ No newline at end of file +// Export for ESM +export { TeraphimEditor }; + +// For IIFE and UMD builds +if (typeof window !== 'undefined') { + window.TeraphimEditor = TeraphimEditor; +} \ No newline at end of file diff --git a/public/js/terraphim_editor.js b/public/js/terraphim_editor.js new file mode 100644 index 0000000..4137a42 --- /dev/null +++ b/public/js/terraphim_editor.js @@ -0,0 +1,468 @@ +let wasm; + +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_export_2.set(idx, obj); + return idx; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedDataViewMemory0 = null; + +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => { + wasm.__wbindgen_export_6.get(state.dtor)(state.a, state.b) +}); + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_6.get(state.dtor)(a, state.b); + CLOSURE_DTORS.unregister(state); + } else { + state.a = a; + } + } + }; + real.original = state; + CLOSURE_DTORS.register(real, state, state); + return real; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_export_2.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} +/** + * @param {string} input + * @returns {string} + */ +export function render_markdown(input) { + let deferred3_0; + let deferred3_1; + try { + const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.render_markdown(ptr0, len0); + var ptr2 = ret[0]; + var len2 = ret[1]; + if (ret[3]) { + ptr2 = 0; len2 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred3_0 = ptr2; + deferred3_1 = len2; + return getStringFromWasm0(ptr2, len2); + } finally { + wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); + } +} + +/** + * @param {EditorStyle} style + */ +export function run_with_style(style) { + const ret = wasm.run_with_style(style); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } +} + +export function run() { + wasm.run(); +} + +/** + * @param {EditorStyle} style + * @param {string} content + * @returns {string} + */ +export function render_editor_html(style, content) { + let deferred3_0; + let deferred3_1; + try { + const ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.render_editor_html(style, ptr0, len0); + var ptr2 = ret[0]; + var len2 = ret[1]; + if (ret[3]) { + ptr2 = 0; len2 = 0; + throw takeFromExternrefTable0(ret[2]); + } + deferred3_0 = ptr2; + deferred3_1 = len2; + return getStringFromWasm0(ptr2, len2); + } finally { + wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); + } +} + +function __wbg_adapter_18(arg0, arg1, arg2) { + wasm.closure17_externref_shim(arg0, arg1, arg2); +} + +/** + * @enum {0 | 1 | 2} + */ +export const EditorStyle = Object.freeze({ + Shoelace: 0, "0": "Shoelace", + Vanilla: 1, "1": "Vanilla", + WebAwesome: 2, "2": "WebAwesome", +}); + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbg_addEventListener_b9481c2c2cab6047 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + arg0.addEventListener(getStringFromWasm0(arg1, arg2), arg3); + }, arguments) }; + imports.wbg.__wbg_call_b0d8e36992d9900d = function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments) }; + imports.wbg.__wbg_document_f11bc4f7c03e1745 = function(arg0) { + const ret = arg0.document; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }; + imports.wbg.__wbg_getElementById_dcc9f1f3cfdca0bc = function(arg0, arg1, arg2) { + const ret = arg0.getElementById(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_instanceof_HtmlDivElement_5853ec72f4da3564 = function(arg0) { + let result; + try { + result = arg0 instanceof HTMLDivElement; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_HtmlTextAreaElement_88347fc269bfb466 = function(arg0) { + let result; + try { + result = arg0 instanceof HTMLTextAreaElement; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_Window_d2514c6a7ee7ba60 = function(arg0) { + let result; + try { + result = arg0 instanceof Window; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_new_8a6f238a6ece86ea = function() { + const ret = new Error(); + return ret; + }; + imports.wbg.__wbg_newnoargs_fd9e4bf8be2bc16d = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return ret; + }; + imports.wbg.__wbg_querySelector_7b4362006fdeda68 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.querySelector(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments) }; + imports.wbg.__wbg_setinnerHTML_2d75307ba8832258 = function(arg0, arg1, arg2) { + arg0.innerHTML = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_0be7472e492ad3e3 = function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_GLOBAL_THIS_1a6eb482d12c9bfb = function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_SELF_1dc398a895c82351 = function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_static_accessor_WINDOW_ae1c80c7eea8d64a = function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_target_a8fe593e7ee79c21 = function(arg0) { + const ret = arg0.target; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + imports.wbg.__wbg_value_a8b8b65bc31190d6 = function(arg0, arg1) { + const ret = arg1.value; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = arg0.original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }; + imports.wbg.__wbindgen_closure_wrapper60 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 18, __wbg_adapter_18); + return ret; + }; + imports.wbg.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_export_2; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + ; + }; + imports.wbg.__wbindgen_is_undefined = function(arg0) { + const ret = arg0 === undefined; + return ret; + }; + imports.wbg.__wbindgen_rethrow = function(arg0) { + throw arg0; + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + + return imports; +} + +function __wbg_init_memory(imports, memory) { + +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('terraphim_editor_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/public/js/umd-example.js b/public/js/umd-example.js new file mode 100644 index 0000000..9831bd6 --- /dev/null +++ b/public/js/umd-example.js @@ -0,0 +1,18 @@ +async function initializeUMDEditor() { + if (!window.EditorConfig) { + console.error('Configuration not loaded! Please check if config.js is loaded properly.'); + return; + } + + try { + const editor = new window.TeraphimEditor({ + container: document.getElementById('editor-container-umd'), + config: window.EditorConfig + }); + await editor.initialize(); + } catch (error) { + console.error('Failed to initialize UMD editor:', error); + } +} + +window.addEventListener('load', initializeUMDEditor); \ No newline at end of file diff --git a/public/shortcuts.json b/public/shortcuts.json deleted file mode 100644 index bf994dc..0000000 --- a/public/shortcuts.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "shortcuts": [ - { - "name": "type-bold", - "key": "ctrl+b", - "prefix": "**", - "suffix": "**", - "desc": "Bold text" - }, - { - "name": "type-italic", - "key": "ctrl+i", - "prefix": "_", - "suffix": "_", - "desc": "Italic text" - }, - { - "name": "code", - "key": "ctrl+k", - "prefix": "`", - "suffix": "`", - "desc": "Inline code" - }, - { - "name": "link", - "key": "ctrl+l", - "prefix": "[", - "suffix": "](url)", - "desc": "Create link" - }, - { - "name": "type-h1", - "key": "ctrl+h", - "prefix": "# ", - "suffix": "", - "desc": "Heading" - } - ] -} \ No newline at end of file diff --git a/public/wasm/terraphim_editor_bg.wasm b/public/wasm/terraphim_editor_bg.wasm new file mode 100644 index 0000000..6360612 Binary files /dev/null and b/public/wasm/terraphim_editor_bg.wasm differ diff --git a/scratchpad.md b/scratchpad.md new file mode 100644 index 0000000..945ff90 --- /dev/null +++ b/scratchpad.md @@ -0,0 +1,37 @@ +# Terraphim Editor – Deep-Dive Review Scratchpad + +## 2025-06-21 – Initial project audit + +### High-priority issues discovered + +1. **Container ID mismatch** + * `src/lib.rs::run()` expects an element with id `editor-container` (error string still mentions `app`). + * `tests/web.rs` creates a container with id `app` – all browser tests will fail. + * Packaging wrapper `public/js/terraphim-editor.js` dynamically assigns a random id to the passed container, then invokes `run()`, which will **not** find `#editor-container` ⇒ runtime failure in bundled build. + +2. **Duplicate / competing rendering layers** + * Rust/WASM already renders full editor UI via `Rinja` template (`templates/editor.html`). + * `public/js/terraphim-editor.js` **also** builds its own DOM subtree & later calls the same `run()` again. + * Leads to double DOM, duplicate listeners, BROKEN separation of concerns. + +3. **Markdown preview update strategy** + * `src/lib.rs::setup_markdown_conversion()` converts on every `input` event; that's fine, but JS wrapper calls `run()` on every keystroke which re-initialises everything – very expensive and probably broken. + +4. **Packaging scripts** + * `vite.config.js` copies `pkg/terraphim_editor.js` & `.wasm` into `public` before every build – OK, but we must ensure `wasm-pack build` ran first. + * `build.sh` assumes `dist/` exists *and* contains CSS named `style.css`; Vite renames it to `terraphim-editor.css` via `assetFileNames`, but only when css is part of bundle. Need to confirm CSS pipeline or copy from `public/css/terraphim-editor.css`. + +5. **Examples** + * All examples expect global constructor `TeraphimEditor` that works; because of (1) & (2) they currently fail to render. + +### Proposed next steps + +- [ ] Decide single source of truth for editor UI (Rust vs JS). +- [x] Align container id across Rust code, tests, and JS wrapper. +- [x] Expose a lightweight `render_markdown(input: &str) -> String` func from WASM for JS to use instead of calling `run()`. +- [x] Simplify JS wrapper & use `render_markdown()` for live preview & toolbar actions. +- [x] Fix build.sh CSS handling & verify `package/` examples after `npm pack` (tarball inspected, CSS + wasm present). + +### Minor polish + +- Update panic message in `run()` \ No newline at end of file diff --git a/scripts/build-dist.sh b/scripts/build-dist.sh deleted file mode 100755 index 2f73133..0000000 --- a/scripts/build-dist.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Create dist directory -mkdir -p dist - -# Copy and minify JS files -terser public/js/config.js public/js/editor.js public/js/terraphim-editor.js -o dist/terraphim-editor.min.js - -# Copy and minify CSS -cleancss public/styles.css -o dist/terraphim-editor.min.css - -# Create example usage file -cat > dist/example.html << EOL - - - - Terraphim Editor Example - - - -
- - - - -EOL \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index fbc4654..d0fcc0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,14 @@ -use wasm_bindgen::prelude::*; -use web_sys::{Document, Element, Window, HtmlTextAreaElement, HtmlDivElement, InputEvent}; use markdown::{to_html_with_options, Options}; use rinja::Template; +use wasm_bindgen::prelude::*; +use web_sys::{Document, Element, HtmlDivElement, HtmlTextAreaElement, InputEvent, Window}; const INITIAL_MARKDOWN: &str = r#"# Welcome to Markdown Editor! This is a simple markdown editor built with: - Rust - WebAssembly -- Shoelace components +- Multiple UI frameworks ## Try it out 1. Edit this text on the left @@ -20,6 +20,7 @@ This is a simple markdown editor built with: > Made with ❤️ using Rust and WASM "#; +// Shoelace template (default) #[derive(Template)] #[template(path = "editor.html")] struct EditorTemplate { @@ -27,49 +28,142 @@ struct EditorTemplate { initial_preview: String, } +// Vanilla HTML/CSS template +#[derive(Template)] +#[template(path = "editor-vanilla.html")] +struct EditorTemplateVanilla { + initial_content: String, + initial_preview: String, +} + +// Web Awesome template +#[derive(Template)] +#[template(path = "editor-webawesome.html")] +struct EditorTemplateWebAwesome { + initial_content: String, + initial_preview: String, +} + +#[wasm_bindgen] +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum EditorStyle { + #[default] + Shoelace, + Vanilla, + WebAwesome, +} + #[wasm_bindgen(start)] pub fn run() -> Result<(), JsValue> { + run_with_style(EditorStyle::Shoelace) +} + +#[wasm_bindgen] +pub fn run_with_style(style: EditorStyle) -> Result<(), JsValue> { console_error_panic_hook::set_once(); - - let window: Window = web_sys::window() - .ok_or_else(|| JsValue::from_str("No window found"))?; - let document: Document = window.document() + + let window: Window = web_sys::window().ok_or_else(|| JsValue::from_str("No window found"))?; + let document: Document = window + .document() .ok_or_else(|| JsValue::from_str("No document found"))?; - let app: Element = document.get_element_by_id("app") - .ok_or_else(|| JsValue::from_str("No element with id 'app' found"))?; + let app: Element = document + .get_element_by_id("editor-container") + .ok_or_else(|| JsValue::from_str("No element with id 'editor-container' found"))?; let initial_preview = to_html_with_options(INITIAL_MARKDOWN, &Options::default()) .map_err(|e| JsValue::from_str(&format!("Failed to convert markdown: {}", e)))?; - let template = EditorTemplate { - initial_content: INITIAL_MARKDOWN.to_string(), - initial_preview, + let html = match style { + EditorStyle::Shoelace => { + let template = EditorTemplate { + initial_content: INITIAL_MARKDOWN.to_string(), + initial_preview, + }; + template + .render() + .map_err(|e| JsValue::from_str(&format!("Failed to render template: {}", e)))? + } + EditorStyle::Vanilla => { + let template = EditorTemplateVanilla { + initial_content: INITIAL_MARKDOWN.to_string(), + initial_preview, + }; + template + .render() + .map_err(|e| JsValue::from_str(&format!("Failed to render template: {}", e)))? + } + EditorStyle::WebAwesome => { + let template = EditorTemplateWebAwesome { + initial_content: INITIAL_MARKDOWN.to_string(), + initial_preview, + }; + template + .render() + .map_err(|e| JsValue::from_str(&format!("Failed to render template: {}", e)))? + } }; - app.set_inner_html(&template.render() - .map_err(|e| JsValue::from_str(&format!("Failed to render template: {}", e)))?); - + app.set_inner_html(&html); setup_markdown_conversion(&document)?; Ok(()) } +#[wasm_bindgen] +pub fn render_editor_html(style: EditorStyle, content: &str) -> Result { + let initial_preview = to_html_with_options(content, &Options::default()) + .map_err(|e| JsValue::from_str(&format!("Failed to convert markdown: {}", e)))?; + + match style { + EditorStyle::Shoelace => { + let template = EditorTemplate { + initial_content: content.to_string(), + initial_preview, + }; + template + .render() + .map_err(|e| JsValue::from_str(&format!("Failed to render template: {}", e))) + } + EditorStyle::Vanilla => { + let template = EditorTemplateVanilla { + initial_content: content.to_string(), + initial_preview, + }; + template + .render() + .map_err(|e| JsValue::from_str(&format!("Failed to render template: {}", e))) + } + EditorStyle::WebAwesome => { + let template = EditorTemplateWebAwesome { + initial_content: content.to_string(), + initial_preview, + }; + template + .render() + .map_err(|e| JsValue::from_str(&format!("Failed to render template: {}", e))) + } + } +} + fn setup_markdown_conversion(document: &Document) -> Result<(), JsValue> { - let textarea = document.query_selector(".markdown-input")? + let textarea = document + .query_selector(".markdown-input")? .ok_or_else(|| JsValue::from_str("No textarea found"))?; - let preview = document.query_selector(".markdown-preview")? + let preview = document + .query_selector(".markdown-preview")? .ok_or_else(|| JsValue::from_str("No preview div found"))?; let preview_clone = preview.clone(); let handler = Closure::wrap(Box::new(move |event: InputEvent| { - let input = event.target() + let input = event + .target() .and_then(|t| t.dyn_into::().ok()) .map(|t| t.value()) .expect("Could not get textarea value"); - + let html = to_html_with_options(&input, &Options::default()) .expect("Failed to convert markdown to HTML"); - + preview_clone .dyn_ref::() .expect("Preview div not found") @@ -78,10 +172,16 @@ fn setup_markdown_conversion(document: &Document) -> Result<(), JsValue> { textarea.add_event_listener_with_callback("input", handler.as_ref().unchecked_ref())?; handler.forget(); - + Ok(()) } +#[wasm_bindgen] +pub fn render_markdown(input: &str) -> Result { + to_html_with_options(input, &Options::default()) + .map_err(|e| JsValue::from_str(&format!("Failed to convert markdown: {}", e))) +} + #[cfg(test)] mod tests { use super::*; @@ -108,4 +208,4 @@ mod tests { assert!(html.contains("# Test")); assert!(html.contains("

Test

")); } -} \ No newline at end of file +} diff --git a/templates/editor-vanilla.html b/templates/editor-vanilla.html new file mode 100644 index 0000000..0ec016d --- /dev/null +++ b/templates/editor-vanilla.html @@ -0,0 +1,364 @@ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+
{{ initial_preview|safe }}
+
+
+
+ +
+
+
+
+

Markdown Shortcuts

+ +
+
+
+ +
+
+
+ + diff --git a/templates/editor-webawesome.html b/templates/editor-webawesome.html new file mode 100644 index 0000000..71c55ba --- /dev/null +++ b/templates/editor-webawesome.html @@ -0,0 +1,135 @@ +
+ +
+
+ + + + + + + + + +
+ +
+
+
{{ initial_preview|safe }}
+
+
+
+ + + +
+ +
+
+ + diff --git a/templates/editor.html b/templates/editor.html index 50f4105..64fbbe2 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -28,6 +28,12 @@ \ No newline at end of file diff --git a/tests/e2e/dynamic-loading.spec.js b/tests/e2e/dynamic-loading.spec.js new file mode 100644 index 0000000..3da8be5 --- /dev/null +++ b/tests/e2e/dynamic-loading.spec.js @@ -0,0 +1,153 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Dynamic Script Loading', () => { + test('should initialize editor when script is loaded after DOMContentLoaded', async ({ page }) => { + // Navigate to a minimal page + await page.setContent(` + + + + + Dynamic Loading Test + + + +
+ + + `); + + // Wait for page to fully load + await page.waitForLoadState('domcontentloaded'); + + // Inject WASM initialization + await page.evaluate(async () => { + const init = await import('/js/terraphim_editor.js'); + const wasm = await init.default('/wasm/terraphim_editor_bg.wasm'); + wasm.run_with_style(wasm.EditorStyle.Vanilla); + }); + + // Dynamically load editor script AFTER DOMContentLoaded has fired + await page.evaluate(() => { + const script = document.createElement('script'); + script.src = '/js/editor-vanilla.js'; + document.body.appendChild(script); + }); + + // Wait for editor to initialize + await page.waitForSelector('.markdown-input', { timeout: 5000 }); + + // Verify editor is interactive + const textarea = page.locator('.markdown-input'); + await expect(textarea).toBeVisible(); + + // Test that toolbar exists + const toolbar = page.locator('.toolbar'); + await expect(toolbar).toBeVisible(); + + // Test that editor is functional + await textarea.clear(); + await textarea.fill('# Dynamic Test'); + await page.waitForTimeout(100); + + const preview = page.locator('.markdown-preview'); + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

Dynamic Test

'); + }); + + test('should work with Shoelace variant when loaded dynamically', async ({ page }) => { + await page.setContent(` + + + + + Dynamic Shoelace Test + + + + + + + + + + + + + +
+ + + `); + + await page.waitForLoadState('domcontentloaded'); + + // Inject WASM initialization + await page.evaluate(async () => { + const init = await import('/js/terraphim_editor.js'); + const wasm = await init.default('/wasm/terraphim_editor_bg.wasm'); + wasm.run_with_style(wasm.EditorStyle.Shoelace); + }); + + // Dynamically load editor script + await page.evaluate(() => { + const script = document.createElement('script'); + script.src = '/js/editor.js'; + document.body.appendChild(script); + }); + + // Wait for editor to initialize + await page.waitForSelector('.markdown-input', { timeout: 5000 }); + + const textarea = page.locator('.markdown-input'); + await expect(textarea).toBeVisible(); + + // Test functionality + await textarea.clear(); + await textarea.fill('**Dynamically loaded**'); + await page.waitForTimeout(100); + + const preview = page.locator('.markdown-preview'); + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('Dynamically loaded'); + }); + + test('multi-style switcher should work after switching styles', async ({ page }) => { + await page.goto('/index-multistyle.html'); + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + // Test initial Shoelace style + let textarea = page.locator('.markdown-input'); + await textarea.clear(); + await textarea.fill('# Initial'); + await page.waitForTimeout(100); + + let preview = page.locator('.markdown-preview'); + let html = await preview.innerHTML(); + expect(html).toContain('

Initial

'); + + // Switch to Vanilla + await page.locator('#btn-vanilla').click(); + await page.waitForTimeout(1000); + + // Verify vanilla editor is interactive + textarea = page.locator('.markdown-input'); + await textarea.clear(); + await textarea.fill('# After Switch'); + await page.waitForTimeout(100); + + preview = page.locator('.markdown-preview'); + html = await preview.innerHTML(); + expect(html).toContain('

After Switch

'); + + // Verify toolbar works + const toolbar = page.locator('.toolbar'); + await expect(toolbar).toBeVisible(); + + const firstButton = toolbar.locator('.btn').first(); + await expect(firstButton).toBeVisible(); + }); +}); diff --git a/tests/e2e/multi-style-switcher.spec.js b/tests/e2e/multi-style-switcher.spec.js new file mode 100644 index 0000000..b304a03 --- /dev/null +++ b/tests/e2e/multi-style-switcher.spec.js @@ -0,0 +1,211 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Multi-Style Switcher', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/index-multistyle.html'); + // Wait for WASM to initialize + await page.waitForSelector('#editor-container', { timeout: 10000 }); + }); + + test('should load with Shoelace style by default', async ({ page }) => { + // Wait for editor to load + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + // Check that Shoelace button is active + const shoelaceBtn = page.locator('#btn-shoelace'); + const variant = await shoelaceBtn.getAttribute('variant'); + expect(variant).toBe('primary'); + + // Check Shoelace components are loaded + const splitPanel = page.locator('sl-split-panel'); + await expect(splitPanel).toBeVisible(); + }); + + test('should switch to Vanilla style', async ({ page }) => { + // Wait for initial load + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + // Click Vanilla button + const vanillaBtn = page.locator('#btn-vanilla'); + await vanillaBtn.click(); + + // Wait for switch + await page.waitForTimeout(500); + + // Check Vanilla button is active + const variant = await vanillaBtn.getAttribute('variant'); + expect(variant).toBe('primary'); + + // Check vanilla split panel is loaded + const splitPanel = page.locator('.split-panel'); + await expect(splitPanel).toBeVisible(); + + // Check editor still works + const textarea = page.locator('.markdown-input'); + await expect(textarea).toBeVisible(); + }); + + test('should switch to Web Awesome style', async ({ page }) => { + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + // Click Web Awesome button + const webAwesomeBtn = page.locator('#btn-webawesome'); + await webAwesomeBtn.click(); + + await page.waitForTimeout(500); + + // Check Web Awesome button is active + const variant = await webAwesomeBtn.getAttribute('variant'); + expect(variant).toBe('primary'); + + // Check editor loaded + const textarea = page.locator('.markdown-input'); + await expect(textarea).toBeVisible(); + }); + + test('should update info panel when switching styles', async ({ page }) => { + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + const infoPanel = page.locator('#style-info'); + + // Check initial info + let infoText = await infoPanel.textContent(); + expect(infoText).toContain('Shoelace'); + + // Switch to Vanilla + await page.locator('#btn-vanilla').click(); + await page.waitForTimeout(300); + + infoText = await infoPanel.textContent(); + expect(infoText).toContain('Pure HTML/CSS'); + expect(infoText).toContain('Zero dependencies'); + + // Switch to Web Awesome + await page.locator('#btn-webawesome').click(); + await page.waitForTimeout(300); + + infoText = await infoPanel.textContent(); + expect(infoText).toContain('Web Awesome'); + }); + + test('should maintain editor functionality after switching', async ({ page }) => { + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + const textarea = page.locator('.markdown-input'); + const preview = page.locator('.markdown-preview'); + + // Test with Shoelace + await textarea.clear(); + await textarea.fill('# Shoelace Test'); + await page.waitForTimeout(100); + let previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

Shoelace Test

'); + + // Switch to Vanilla + await page.locator('#btn-vanilla').click(); + await page.waitForTimeout(500); + + // Test with Vanilla + await textarea.clear(); + await textarea.fill('# Vanilla Test'); + await page.waitForTimeout(100); + previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

Vanilla Test

'); + + // Switch to Web Awesome + await page.locator('#btn-webawesome').click(); + await page.waitForTimeout(500); + + // Test with Web Awesome + await textarea.clear(); + await textarea.fill('# Web Awesome Test'); + await page.waitForTimeout(100); + previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

Web Awesome Test

'); + }); + + test('should have proper styling for header', async ({ page }) => { + const header = page.locator('.header'); + await expect(header).toBeVisible(); + + const bgColor = await header.evaluate((el) => { + return window.getComputedStyle(el).backgroundColor; + }); + + // Should not be transparent + expect(bgColor).not.toBe('rgba(0, 0, 0, 0)'); + }); + + test('should have gradient background on body', async ({ page }) => { + const bodyBg = await page.evaluate(() => { + return window.getComputedStyle(document.body).background; + }); + + // Should have gradient + expect(bodyBg).toContain('gradient'); + }); + + test('should display all three style options', async ({ page }) => { + const shoelaceBtn = page.locator('#btn-shoelace'); + const vanillaBtn = page.locator('#btn-vanilla'); + const webAwesomeBtn = page.locator('#btn-webawesome'); + + await expect(shoelaceBtn).toBeVisible(); + await expect(vanillaBtn).toBeVisible(); + await expect(webAwesomeBtn).toBeVisible(); + + // Check button text + expect(await shoelaceBtn.textContent()).toContain('Shoelace'); + expect(await vanillaBtn.textContent()).toContain('Vanilla'); + expect(await webAwesomeBtn.textContent()).toContain('Web Awesome'); + }); + + test('should show features for each style', async ({ page }) => { + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + // Each style should show features + const features = page.locator('.feature'); + const count = await features.count(); + expect(count).toBe(3); // Default Shoelace shows 3 features + }); + + test('should handle rapid style switching', async ({ page }) => { + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + // Rapidly switch between styles + await page.locator('#btn-vanilla').click(); + await page.waitForTimeout(100); + await page.locator('#btn-webawesome').click(); + await page.waitForTimeout(100); + await page.locator('#btn-shoelace').click(); + await page.waitForTimeout(500); + + // Editor should still work + const textarea = page.locator('.markdown-input'); + await expect(textarea).toBeVisible(); + + await textarea.clear(); + await textarea.fill('# Rapid Switch Test'); + await page.waitForTimeout(100); + + const preview = page.locator('.markdown-preview'); + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

Rapid Switch Test

'); + }); + + test('should be responsive on mobile', async ({ page }) => { + // Set mobile viewport + await page.setViewportSize({ width: 375, height: 667 }); + + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + + // Everything should still be visible + const header = page.locator('.header'); + const styleSelector = page.locator('.style-selector'); + const editorContainer = page.locator('#editor-container'); + + await expect(header).toBeVisible(); + await expect(styleSelector).toBeVisible(); + await expect(editorContainer).toBeVisible(); + }); +}); diff --git a/tests/e2e/shoelace-variant.spec.js b/tests/e2e/shoelace-variant.spec.js new file mode 100644 index 0000000..46fec4c --- /dev/null +++ b/tests/e2e/shoelace-variant.spec.js @@ -0,0 +1,162 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Shoelace Variant', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/example-shoelace.html'); + // Wait for WASM to initialize + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + }); + + test('should load editor with initial content', async ({ page }) => { + // Check that the editor container is present + const container = page.locator('#editor-container'); + await expect(container).toBeVisible(); + + // Check initial markdown content + const textarea = page.locator('.markdown-input'); + const content = await textarea.inputValue(); + expect(content).toContain('Welcome to Markdown Editor'); + expect(content).toContain('Rust'); + expect(content).toContain('WebAssembly'); + }); + + test('should render markdown preview', async ({ page }) => { + // Check that preview is rendered + const preview = page.locator('.markdown-preview'); + await expect(preview).toBeVisible(); + + // Verify initial preview contains rendered HTML + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

Welcome to Markdown Editor!

'); + expect(previewHTML).toContain('
  • Rust
  • '); + expect(previewHTML).toContain('
  • WebAssembly
  • '); + }); + + test('should update preview on input', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + const preview = page.locator('.markdown-preview'); + + // Clear and type new content + await textarea.clear(); + await textarea.fill('# Test Heading\n\nThis is **bold** text.'); + + // Wait a bit for update + await page.waitForTimeout(100); + + // Check preview updated + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

    Test Heading

    '); + expect(previewHTML).toContain('bold'); + }); + + test('should have toolbar buttons', async ({ page }) => { + // Check toolbar is present + const toolbar = page.locator('.toolbar'); + await expect(toolbar).toBeVisible(); + + // Check formatting buttons exist + const buttons = page.locator('#formatting-toolbar sl-button'); + const count = await buttons.count(); + expect(count).toBeGreaterThan(0); + }); + + test('should open help dialog', async ({ page }) => { + const helpButton = page.locator('#show-help'); + await helpButton.click(); + + // Wait for dialog to open + await page.waitForTimeout(300); + + // Check dialog is visible + const dialog = page.locator('.shortcuts-dialog'); + const isOpen = await dialog.evaluate((el) => el.hasAttribute('open')); + expect(isOpen).toBe(true); + + // Check shortcuts are listed + const shortcuts = page.locator('#shortcuts-list .shortcut-item'); + const shortcutCount = await shortcuts.count(); + expect(shortcutCount).toBeGreaterThan(0); + }); + + test('should apply bold formatting with toolbar button', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + + // Clear and set content + await textarea.clear(); + await textarea.fill('selected text'); + + // Select the text + await textarea.evaluate((el) => { + el.setSelectionRange(0, 13); + }); + + // Click bold button (first button) + const boldButton = page.locator('#formatting-toolbar sl-button').first(); + await boldButton.click(); + + // Check text is wrapped + const value = await textarea.inputValue(); + expect(value).toContain('**selected text**'); + }); + + test('should have split panel for layout', async ({ page }) => { + // Check split panel component exists + const splitPanel = page.locator('sl-split-panel'); + await expect(splitPanel).toBeVisible(); + + // Check both panels are present + const startPanel = page.locator('sl-split-panel [slot="start"]'); + const endPanel = page.locator('sl-split-panel [slot="end"]'); + + await expect(startPanel).toBeVisible(); + await expect(endPanel).toBeVisible(); + }); + + test('should render code blocks correctly', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + const preview = page.locator('.markdown-preview'); + + // Type code block + await textarea.clear(); + await textarea.fill('```javascript\nconst x = 42;\n```'); + + await page.waitForTimeout(100); + + // Check code block is rendered + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('
    ');
    +    expect(previewHTML).toContain(' {
    +    const textarea = page.locator('.markdown-input');
    +    const preview = page.locator('.markdown-preview');
    +
    +    await textarea.clear();
    +    await textarea.fill('- Item 1\n- Item 2\n- Item 3');
    +
    +    await page.waitForTimeout(100);
    +
    +    const previewHTML = await preview.innerHTML();
    +    expect(previewHTML).toContain('
      '); + expect(previewHTML).toContain('
    • Item 1
    • '); + expect(previewHTML).toContain('
    • Item 2
    • '); + }); + + test('should handle special characters', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + const preview = page.locator('.markdown-preview'); + + await textarea.clear(); + await textarea.fill('Test & < > " \' characters'); + + await page.waitForTimeout(100); + + // HTML should be properly escaped + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('&'); + expect(previewHTML).toContain('<'); + expect(previewHTML).toContain('>'); + }); +}); diff --git a/tests/e2e/vanilla-variant.spec.js b/tests/e2e/vanilla-variant.spec.js new file mode 100644 index 0000000..87f1ce9 --- /dev/null +++ b/tests/e2e/vanilla-variant.spec.js @@ -0,0 +1,155 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Vanilla Variant', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/example-vanilla.html'); + // Wait for WASM to initialize + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + }); + + test('should load editor with no external dependencies', async ({ page }) => { + // Check that no Shoelace/Web Awesome scripts are loaded + const scripts = await page.evaluate(() => { + return Array.from(document.querySelectorAll('script')) + .map(s => s.src) + .filter(src => src.includes('shoelace') || src.includes('webawesome')); + }); + expect(scripts.length).toBe(0); + }); + + test('should have fully functional vanilla UI', async ({ page }) => { + const container = page.locator('#editor-container'); + await expect(container).toBeVisible(); + + // Check vanilla-specific classes + const splitPanel = page.locator('.split-panel'); + await expect(splitPanel).toBeVisible(); + }); + + test('should render markdown correctly', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + const preview = page.locator('.markdown-preview'); + + await textarea.clear(); + await textarea.fill('# Vanilla Test\n\nThis is **bold** and *italic*.'); + + await page.waitForTimeout(100); + + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

      Vanilla Test

      '); + expect(previewHTML).toContain('bold'); + expect(previewHTML).toContain('italic'); + }); + + test('should have resizable split panel', async ({ page }) => { + const divider = page.locator('#panel-divider'); + await expect(divider).toBeVisible(); + + // Check cursor style + const cursor = await divider.evaluate((el) => { + return window.getComputedStyle(el).cursor; + }); + expect(cursor).toBe('col-resize'); + }); + + test('should open vanilla dialog on help button', async ({ page }) => { + const helpButton = page.locator('#show-help'); + await helpButton.click(); + + await page.waitForTimeout(100); + + // Check vanilla dialog is visible + const dialog = page.locator('.shortcuts-dialog'); + const isVisible = await dialog.evaluate((el) => { + return el.classList.contains('open'); + }); + expect(isVisible).toBe(true); + }); + + test('should close dialog on close button', async ({ page }) => { + const helpButton = page.locator('#show-help'); + await helpButton.click(); + + await page.waitForTimeout(100); + + const closeButton = page.locator('#close-help'); + await closeButton.click(); + + await page.waitForTimeout(100); + + const dialog = page.locator('.shortcuts-dialog'); + const isVisible = await dialog.evaluate((el) => { + return el.classList.contains('open'); + }); + expect(isVisible).toBe(false); + }); + + test('should have toolbar buttons with text icons', async ({ page }) => { + const toolbar = page.locator('#formatting-toolbar'); + await expect(toolbar).toBeVisible(); + + // Vanilla buttons should have text-based icons + const firstButton = toolbar.locator('.btn').first(); + const iconText = await firstButton.locator('.icon').textContent(); + expect(iconText).toBeTruthy(); + expect(iconText.length).toBeGreaterThan(0); + }); + + test('should show tooltips on hover', async ({ page }) => { + const firstButton = page.locator('#formatting-toolbar .tooltip').first(); + + // Hover over button + await firstButton.hover(); + + await page.waitForTimeout(100); + + // Check tooltip is visible + const tooltip = firstButton.locator('.tooltip-text'); + const isVisible = await tooltip.isVisible(); + expect(isVisible).toBe(true); + }); + + test('should apply formatting with vanilla buttons', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + + await textarea.clear(); + await textarea.fill('text to format'); + + await textarea.evaluate((el) => { + el.setSelectionRange(0, 14); + }); + + // Click first formatting button + const firstButton = page.locator('#formatting-toolbar .btn').first(); + await firstButton.click(); + + const value = await textarea.inputValue(); + expect(value).toContain('**text to format**'); + }); + + test('should have proper styling without external libraries', async ({ page }) => { + // Check that styles are applied + const toolbar = page.locator('.toolbar'); + const bgColor = await toolbar.evaluate((el) => { + return window.getComputedStyle(el).backgroundColor; + }); + + // Should have a background color set + expect(bgColor).not.toBe('rgba(0, 0, 0, 0)'); + expect(bgColor).not.toBe('transparent'); + }); + + test('should handle responsive layout', async ({ page }) => { + // Set mobile viewport + await page.setViewportSize({ width: 375, height: 667 }); + + await page.waitForTimeout(100); + + // Editor should still be visible and functional + const container = page.locator('#editor-container'); + await expect(container).toBeVisible(); + + const textarea = page.locator('.markdown-input'); + await expect(textarea).toBeVisible(); + }); +}); diff --git a/tests/e2e/webawesome-variant.spec.js b/tests/e2e/webawesome-variant.spec.js new file mode 100644 index 0000000..66acf36 --- /dev/null +++ b/tests/e2e/webawesome-variant.spec.js @@ -0,0 +1,132 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Web Awesome Variant', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/example-webawesome.html'); + // Wait for WASM to initialize + await page.waitForSelector('.markdown-input', { timeout: 10000 }); + }); + + test('should load editor with Web Awesome components', async ({ page }) => { + const container = page.locator('#editor-container'); + await expect(container).toBeVisible(); + + // Note: Currently using Shoelace as fallback for Web Awesome + // This will work with actual Web Awesome CDN when configured + }); + + test('should render markdown preview', async ({ page }) => { + const preview = page.locator('.markdown-preview'); + await expect(preview).toBeVisible(); + + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

      Welcome to Markdown Editor!

      '); + }); + + test('should update preview on input', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + const preview = page.locator('.markdown-preview'); + + await textarea.clear(); + await textarea.fill('# Web Awesome Test\n\n**Bold** and *italic* text.'); + + await page.waitForTimeout(100); + + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

      Web Awesome Test

      '); + expect(previewHTML).toContain('Bold'); + expect(previewHTML).toContain('italic'); + }); + + test('should have toolbar with components', async ({ page }) => { + const toolbar = page.locator('.toolbar'); + await expect(toolbar).toBeVisible(); + + // Web Awesome uses wa- prefix (or sl- as fallback) + const buttons = page.locator('#formatting-toolbar button, #formatting-toolbar sl-button, #formatting-toolbar wa-button'); + const count = await buttons.count(); + expect(count).toBeGreaterThan(0); + }); + + test('should open help dialog', async ({ page }) => { + const helpButton = page.locator('#show-help'); + await helpButton.click(); + + await page.waitForTimeout(300); + + const dialog = page.locator('.shortcuts-dialog'); + + // Check if using Web Awesome or Shoelace fallback + const isOpen = await dialog.evaluate((el) => { + return el.hasAttribute && el.hasAttribute('open'); + }); + + expect(isOpen).toBe(true); + }); + + test('should have split panel layout', async ({ page }) => { + // Check for split panel (wa-split-panel or sl-split-panel) + const splitPanel = page.locator('wa-split-panel, sl-split-panel'); + await expect(splitPanel).toBeVisible(); + + const startPanel = page.locator('[slot="start"]'); + const endPanel = page.locator('[slot="end"]'); + + await expect(startPanel).toBeVisible(); + await expect(endPanel).toBeVisible(); + }); + + test('should apply formatting', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + + await textarea.clear(); + await textarea.fill('format this'); + + await textarea.evaluate((el) => { + el.setSelectionRange(0, 11); + }); + + const firstButton = page.locator('#formatting-toolbar button, #formatting-toolbar sl-button').first(); + await firstButton.click(); + + const value = await textarea.inputValue(); + expect(value).toContain('**format this**'); + }); + + test('should render complex markdown', async ({ page }) => { + const textarea = page.locator('.markdown-input'); + const preview = page.locator('.markdown-preview'); + + const markdown = `# Heading 1 +## Heading 2 + +- List item 1 +- List item 2 + +\`\`\`javascript +const x = 42; +\`\`\` + +> Blockquote +`; + + await textarea.clear(); + await textarea.fill(markdown); + + await page.waitForTimeout(100); + + const previewHTML = await preview.innerHTML(); + expect(previewHTML).toContain('

      Heading 1

      '); + expect(previewHTML).toContain('

      Heading 2

      '); + expect(previewHTML).toContain('
        '); + expect(previewHTML).toContain('
        ');
        +    expect(previewHTML).toContain('
        '); + }); + + test('should show note about Web Awesome setup', async ({ page }) => { + // Check if the note about Web Awesome project is present + const note = page.locator('.note'); + const noteText = await note.textContent(); + expect(noteText).toContain('webawesome.com'); + }); +}); diff --git a/tests/web.rs b/tests/web.rs index 6df116d..d2a60b0 100644 --- a/tests/web.rs +++ b/tests/web.rs @@ -1,5 +1,5 @@ -use wasm_bindgen_test::*; use wasm_bindgen::JsCast; +use wasm_bindgen_test::*; use web_sys::Performance; wasm_bindgen_test_configure!(run_in_browser); @@ -37,19 +37,19 @@ fn hello_world() { fn test_editor_initialization() { let window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); - + // Create test container let app = document.create_element("div").unwrap(); - app.set_id("app"); + app.set_id("editor-container"); document.body().unwrap().append_child(&app).unwrap(); - + // Initialize the editor terraphim_editor::run().expect("Editor should initialize"); - + // Check if editor elements are present let textarea = document.query_selector(".markdown-input").unwrap(); assert!(textarea.is_some(), "Textarea should be present"); - + let preview = document.query_selector(".markdown-preview").unwrap(); assert!(preview.is_some(), "Preview div should be present"); } @@ -58,73 +58,85 @@ fn test_editor_initialization() { fn test_markdown_conversion() { let window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); - + // Initialize editor let app = document.create_element("div").unwrap(); - app.set_id("app"); + app.set_id("editor-container"); document.body().unwrap().append_child(&app).unwrap(); terraphim_editor::run().expect("Editor should initialize"); - + // Get editor elements - let textarea = document.query_selector(".markdown-input").unwrap() + let textarea = document + .query_selector(".markdown-input") + .unwrap() .expect("Textarea should be present") .dyn_into::() .unwrap(); - - let preview = document.query_selector(".markdown-preview").unwrap() + + let preview = document + .query_selector(".markdown-preview") + .unwrap() .expect("Preview div should be present"); - + // Test markdown conversion let test_input = "# Test Heading"; textarea.set_value(test_input); - + // Trigger input event let event = web_sys::InputEvent::new("input").unwrap(); textarea.dispatch_event(&event).unwrap(); - + // Check preview content let preview_html = preview.inner_html(); - assert!(preview_html.contains("

        Test Heading

        "), - "Preview should contain converted markdown"); + assert!( + preview_html.contains("

        Test Heading

        "), + "Preview should contain converted markdown" + ); } #[wasm_bindgen_test] fn bench_markdown_conversion_in_browser() { let window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); - let performance = window.performance().expect("performance should be available"); - + let performance = window + .performance() + .expect("performance should be available"); + // Initialize editor let app = document.create_element("div").unwrap(); - app.set_id("app"); + app.set_id("editor-container"); document.body().unwrap().append_child(&app).unwrap(); terraphim_editor::run().expect("Editor should initialize"); - + // Get editor elements - let textarea = document.query_selector(".markdown-input").unwrap() + let textarea = document + .query_selector(".markdown-input") + .unwrap() .expect("Textarea should be present") .dyn_into::() .unwrap(); - - let preview = document.query_selector(".markdown-preview").unwrap() + + let preview = document + .query_selector(".markdown-preview") + .unwrap() .expect("Preview div should be present"); - + // Measure performance let start = performance.now(); - + // Run conversion multiple times for _ in 0..100 { textarea.set_value(BENCHMARK_TEXT); let event = web_sys::InputEvent::new("input").unwrap(); textarea.dispatch_event(&event).unwrap(); } - + let end = performance.now(); let avg_time = (end - start) / 100.0; - + // Log performance results web_sys::console::log_1(&format!("Average conversion time: {}ms", avg_time).into()); - + // Basic assertion to ensure reasonable performance assert!(avg_time < 50.0, "Conversion took too long: {}ms", avg_time); -} \ No newline at end of file +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..768d738 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,76 @@ +import { defineConfig } from 'vite' +import wasm from 'vite-plugin-wasm' +import topLevelAwait from 'vite-plugin-top-level-await' +import { resolve } from 'path' +import { copyFileSync, mkdirSync, existsSync } from 'fs' + +// Copy WASM files from pkg to public (only if pkg exists) +try { + if (existsSync('pkg/terraphim_editor_bg.wasm')) { + mkdirSync('public/wasm', { recursive: true }); + mkdirSync('public/js', { recursive: true }); + copyFileSync('pkg/terraphim_editor_bg.wasm', 'public/wasm/terraphim_editor_bg.wasm'); + copyFileSync('pkg/terraphim_editor.js', 'public/js/terraphim_editor.js'); + console.log('✓ WASM files copied to public/'); + } +} catch (error) { + console.error('Warning: Could not copy WASM files:', error.message); +} + +export default defineConfig({ + plugins: [ + wasm(), + topLevelAwait() + ], + // Use public as the root for dev server (for E2E tests) + root: 'public', + server: { + host: '127.0.0.1', + port: 8080, + strictPort: true, + fs: { + // Allow serving files from project root + allow: ['..'] + } + }, + build: { + outDir: '../dist', + emptyOutDir: true, + target: 'esnext', + lib: { + entry: resolve(__dirname, 'public/js/terraphim-editor.js'), + name: 'TeraphimEditor', + formats: ['es', 'umd', 'iife'], + fileName: (format) => { + switch (format) { + case 'es': + return 'js/terraphim-editor.mjs' + case 'umd': + return 'js/terraphim-editor.umd.js' + case 'iife': + return 'js/terraphim-editor.iife.js' + default: + return 'js/terraphim-editor.js' + } + } + }, + rollupOptions: { + external: ['./config.js'], + output: { + globals: { + './config.js': 'TeraphimConfig' + }, + assetFileNames: (assetInfo) => { + if (assetInfo.name === 'style.css') return 'css/terraphim-editor.css' + if (assetInfo.name.endsWith('.wasm')) return 'wasm/[name][extname]' + return '[ext]/[name][extname]' + } + } + }, + sourcemap: true, + minify: 'esbuild' + }, + optimizeDeps: { + exclude: ['./config.js'] + } +})