Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4ee5cc645 | |||
| 061ba88206 | |||
| 4e9117ddcb | |||
| 5e3646fdff | |||
| fc828a345b | |||
| 4d72ee8032 | |||
| 689fb21774 | |||
| 20c0cfece7 | |||
| 0d78837a53 | |||
| 2836ae14de | |||
| 5264a6083c | |||
| a969c5de47 | |||
| f27eea6b5b | |||
| ae2e8ee4a7 | |||
| b10d2fd3d0 | |||
| 92719ff424 | |||
| 9026646594 | |||
| 77415bfad7 | |||
| 5e147865f8 | |||
| 3003b9b8da | |||
| d32dc41315 | |||
| f19a88f1d5 | |||
| 7d115b6325 | |||
| 60d9f7a2c3 | |||
| f3aaec99bb | |||
| b81ce5631d | |||
| 14108533fb | |||
| b6f95066a3 | |||
| 6df9b5df05 | |||
| 82c1a3ab65 | |||
| da34303057 | |||
| d2cd2d22fd | |||
| 48e0b60be5 | |||
| 2b35fcde62 | |||
| c46d9f7a0b | |||
| 2b687a4ca8 | |||
| d68e762612 | |||
| b51999582e | |||
| b4157da3dd | |||
| 916c09a22b | |||
| 4d899d0690 | |||
| dcb51b7e3a | |||
| d0432dbe0d | |||
| 45e4c7a6e9 | |||
| 01e5c811ab | |||
| 9fe2960842 | |||
| 7be37df3e4 | |||
| 21189f7664 | |||
| ddd8189576 | |||
| 2e6f292e48 | |||
| a1476a98b5 | |||
| 304959b3ee | |||
| 83086b3773 | |||
| 82eef76366 | |||
| 2838135450 | |||
| d592c5b8b3 | |||
| e84eb08dc5 | |||
| 0e568ce623 | |||
| aa0ac626c5 | |||
| 9bbc2832de | |||
| 46a060b695 | |||
| 24959308b2 | |||
| 6670e71b81 | |||
| ab3f0db86f | |||
| d0a1c91b8e | |||
| 3264517c91 | |||
| d98c3ae05a | |||
| 4e3f112987 | |||
| f788ccdfd3 | |||
| 512085d283 | |||
| 95ea13958a | |||
| ca431ac11f | |||
| 79c6c9d4e4 | |||
| 09e7f07c23 | |||
| 8d8a4ed0c3 | |||
| 5b13c00d3e | |||
| 4262310302 | |||
| 6ef2641bff |
323
.claude/capabilities/issue-facade.md
Normal file
323
.claude/capabilities/issue-facade.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# Issue Facade - Agent Integration Context
|
||||
|
||||
**🤖 For Coding Agents: Read this to understand how to use issue tracking in this project.**
|
||||
|
||||
## Critical: DO NOT Bypass This Capability
|
||||
|
||||
⚠️ **IMPORTANT:** If you need to work with issues, **you MUST use this capability**. Do NOT:
|
||||
- ❌ Make direct API calls to Gitea/GitHub/GitLab
|
||||
- ❌ Use platform CLIs (gh, glab, etc.)
|
||||
- ❌ Import platform libraries (PyGithub, python-gitlab, etc.)
|
||||
- ❌ Parse HTML/scrape issue tracker web UIs
|
||||
|
||||
**Why?** Bypassing this capability causes:
|
||||
- Credential management chaos (tokens scattered everywhere)
|
||||
- Inconsistent issue state across agents
|
||||
- Massive token waste (redundant API calls)
|
||||
- Platform lock-in (can't switch Gitea → GitHub easily)
|
||||
- Race conditions in multi-agent scenarios
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Check if Capability is Available
|
||||
|
||||
```bash
|
||||
# Verify installation
|
||||
issue --version
|
||||
# or
|
||||
python -c "from issue_tracker.backends.gitea import GiteaBackend; print('OK')"
|
||||
```
|
||||
|
||||
### Basic Usage (Python)
|
||||
|
||||
```python
|
||||
from issue_tracker.backends.gitea import GiteaBackend
|
||||
from issue_tracker.core.models import Issue, Label, IssueState, User, Comment
|
||||
from issue_tracker.core.interfaces import IssueFilter
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
|
||||
# Connect (assumes backend is configured)
|
||||
backend = GiteaBackend()
|
||||
backend.connect({
|
||||
'base_url': os.environ['GITEA_URL'],
|
||||
'token': os.environ['GITEA_API_TOKEN'],
|
||||
'owner': os.environ['GITEA_OWNER'],
|
||||
'repo': os.environ['GITEA_REPO']
|
||||
})
|
||||
|
||||
# List issues for me
|
||||
my_issues = backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
assignee='my-agent-id',
|
||||
labels=['needs-implementation']
|
||||
))
|
||||
|
||||
# Create issue
|
||||
new_issue = Issue(
|
||||
id=None, number=0,
|
||||
title="Implement feature X",
|
||||
description="Details...",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
labels=[Label(name="feature"), Label(name="priority:high")]
|
||||
)
|
||||
created = backend.create_issue(new_issue)
|
||||
|
||||
# Update issue
|
||||
created.state = IssueState.IN_PROGRESS
|
||||
created.assignees = [User(id="agent-id", username="agent-id")]
|
||||
backend.update_issue(created)
|
||||
|
||||
# Add comment
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body="Implementation started. Working on database schema.",
|
||||
author=User(id="agent-id", username="agent-id"),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
backend.add_comment(created.id, comment)
|
||||
|
||||
# Close when done
|
||||
created.state = IssueState.CLOSED
|
||||
created.closed_at = datetime.now(timezone.utc)
|
||||
backend.update_issue(created)
|
||||
```
|
||||
|
||||
### Basic Usage (CLI)
|
||||
|
||||
```bash
|
||||
# List my open issues
|
||||
issue list --state=open --assignee=agent-id --format=json
|
||||
|
||||
# Create issue
|
||||
issue create "Implement feature X" \
|
||||
--label=feature \
|
||||
--label=priority:high \
|
||||
--description="Details here"
|
||||
|
||||
# Update state
|
||||
issue edit 42 --state=in_progress --assignee=agent-id
|
||||
|
||||
# Add comment
|
||||
issue comment 42 "Implementation started"
|
||||
|
||||
# Close
|
||||
issue close 42 --comment="Completed successfully"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Find Work
|
||||
|
||||
```python
|
||||
# Get next available task
|
||||
available_tasks = backend.list_issues(IssueFilter(
|
||||
state='open',
|
||||
labels=['ready', 'needs-implementation']
|
||||
))
|
||||
|
||||
# Filter to unassigned
|
||||
unassigned = [t for t in available_tasks if not t.assignees]
|
||||
|
||||
if unassigned:
|
||||
task = unassigned[0]
|
||||
# Claim it...
|
||||
```
|
||||
|
||||
### Pattern 2: Claim Issue (Prevent Race Conditions)
|
||||
|
||||
```python
|
||||
def claim_issue(issue: Issue, agent_id: str) -> bool:
|
||||
"""Claim an issue safely."""
|
||||
# Check if already claimed
|
||||
if issue.assignees:
|
||||
return False # Already taken
|
||||
|
||||
# Claim it
|
||||
issue.state = IssueState.IN_PROGRESS
|
||||
issue.assignees = [User(id=agent_id, username=agent_id)]
|
||||
backend.update_issue(issue)
|
||||
|
||||
# Announce claim
|
||||
backend.add_comment(issue.id, Comment(
|
||||
id=None,
|
||||
body=f"🤖 Claimed by {agent_id}",
|
||||
author=User(id=agent_id, username=agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
))
|
||||
return True
|
||||
```
|
||||
|
||||
### Pattern 3: Progress Updates
|
||||
|
||||
```python
|
||||
def report_progress(issue: Issue, message: str, agent_id: str):
|
||||
"""Report progress on an issue."""
|
||||
backend.add_comment(issue.id, Comment(
|
||||
id=None,
|
||||
body=f"**Progress Update:**\n\n{message}",
|
||||
author=User(id=agent_id, username=agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
))
|
||||
```
|
||||
|
||||
### Pattern 4: Agent-to-Agent Communication
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
def post_agent_message(issue_id: str, msg_type: str, data: dict, agent_id: str):
|
||||
"""Post structured message for other agents."""
|
||||
message = {
|
||||
'type': msg_type,
|
||||
'agent': agent_id,
|
||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||
'data': data
|
||||
}
|
||||
backend.add_comment(issue_id, Comment(
|
||||
id=None,
|
||||
body=f"```agent-message\n{json.dumps(message, indent=2)}\n```",
|
||||
author=User(id=agent_id, username=agent_id),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
))
|
||||
|
||||
def read_agent_messages(issue_id: str, msg_type: str = None):
|
||||
"""Read messages from other agents."""
|
||||
comments = backend.get_comments(issue_id)
|
||||
messages = []
|
||||
for comment in comments:
|
||||
if '```agent-message' in comment.body:
|
||||
try:
|
||||
json_str = comment.body.split('```agent-message\n')[1].split('\n```')[0]
|
||||
msg = json.loads(json_str)
|
||||
if msg_type is None or msg['type'] == msg_type:
|
||||
messages.append(msg)
|
||||
except:
|
||||
continue
|
||||
return messages
|
||||
```
|
||||
|
||||
## Configuration Check
|
||||
|
||||
Before using issue tracking, verify configuration:
|
||||
|
||||
```python
|
||||
def verify_issue_backend() -> bool:
|
||||
"""Verify issue backend is configured."""
|
||||
try:
|
||||
backend = GiteaBackend()
|
||||
backend.connect({
|
||||
'base_url': os.environ['GITEA_URL'],
|
||||
'token': os.environ['GITEA_API_TOKEN'],
|
||||
'owner': os.environ['GITEA_OWNER'],
|
||||
'repo': os.environ['GITEA_REPO']
|
||||
})
|
||||
return backend.test_connection()
|
||||
except Exception as e:
|
||||
print(f"Issue backend not configured: {e}")
|
||||
return False
|
||||
|
||||
# Use it
|
||||
if not verify_issue_backend():
|
||||
print("ERROR: Issue tracking not available. Check configuration.")
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```python
|
||||
from issue_tracker.backends.gitea.backend import GiteaAPIError
|
||||
|
||||
try:
|
||||
issue = backend.get_issue_by_number(42)
|
||||
except GiteaAPIError as e:
|
||||
if e.status_code == 404:
|
||||
print("Issue not found")
|
||||
elif e.status_code == 401:
|
||||
print("Authentication failed - check GITEA_API_TOKEN")
|
||||
elif e.status_code == 429:
|
||||
print("Rate limited - wait and retry")
|
||||
else:
|
||||
print(f"API error: {e}")
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use filters** instead of fetching all issues:
|
||||
```python
|
||||
# BAD: Get all, filter in Python
|
||||
all_issues = backend.list_issues()
|
||||
my_issues = [i for i in all_issues if i.assignees and i.assignees[0].username == 'me']
|
||||
|
||||
# GOOD: Filter at backend
|
||||
my_issues = backend.list_issues(IssueFilter(assignee='me'))
|
||||
```
|
||||
|
||||
2. **Use JSON output** for CLI parsing:
|
||||
```bash
|
||||
issue list --format=json | jq '.[] | select(.state == "open")'
|
||||
```
|
||||
|
||||
3. **Batch comments** instead of rapid-fire updates
|
||||
|
||||
4. **Check local cache** before querying (if available)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Backend not configured"
|
||||
```bash
|
||||
# Check config
|
||||
issue backend list
|
||||
|
||||
# If empty, configure
|
||||
export GITEA_API_TOKEN="your-token"
|
||||
issue backend add myproject gitea
|
||||
issue backend set-default myproject
|
||||
```
|
||||
|
||||
### "Authentication failed"
|
||||
```bash
|
||||
# Verify token
|
||||
curl -H "Authorization: token $GITEA_API_TOKEN" $GITEA_URL/api/v1/user
|
||||
```
|
||||
|
||||
### "Issue not found"
|
||||
```python
|
||||
# Use get_issue_by_number, not get_issue
|
||||
issue = backend.get_issue_by_number(42) # Correct
|
||||
# issue = backend.get_issue("42") # Wrong - needs backend_id
|
||||
```
|
||||
|
||||
## Full Documentation
|
||||
|
||||
- **Integration Guide:** `AGENT_INTEGRATION.md` (comprehensive patterns and strategies)
|
||||
- **API Reference:** `CLAUDE.md` (for developers extending the capability)
|
||||
- **Examples:** `examples/agents/` (working agent implementations)
|
||||
- **Roadmap:** `ROADMAP.md` (upcoming features)
|
||||
|
||||
## Current Limitations (v1.0)
|
||||
|
||||
Be aware of these limitations:
|
||||
|
||||
1. **Manual Configuration:** Backend must be configured before use (auto-detect in v1.1)
|
||||
2. **User Context:** Uses hardcoded user for CLI operations (agent identity in v1.2)
|
||||
3. **No Built-in Locking:** Use assignee + comment workaround for claiming (native in v1.2)
|
||||
4. **Basic Conflicts:** Manual resolution required for complex sync conflicts (advanced in v2.0)
|
||||
|
||||
Workarounds are documented in `AGENT_INTEGRATION.md`.
|
||||
|
||||
## Questions?
|
||||
|
||||
If you're unsure whether to use this capability for something:
|
||||
|
||||
**ASK:** "Does this involve creating, reading, updating, or searching issues?"
|
||||
- **YES** → Use this capability
|
||||
- **NO** → You can use other methods
|
||||
|
||||
**Example:**
|
||||
- "Create an issue for the bug I found" → **Use issue-facade**
|
||||
- "Read the project README" → Don't need issue-facade
|
||||
- "Check if issue #42 exists" → **Use issue-facade**
|
||||
- "Clone the repository" → Don't need issue-facade
|
||||
6
.claude/commands/use-issues.md
Normal file
6
.claude/commands/use-issues.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Use the issue-facade capability for all issue tracking operations.
|
||||
|
||||
**Quick reference:** See `.claude/capabilities/issue-facade.md`
|
||||
**Examples:** See `capabilities/issue-facade/examples/agents/`
|
||||
|
||||
**DO NOT use direct API calls or platform CLIs!**
|
||||
8
.claude/context/capabilities.md
Normal file
8
.claude/context/capabilities.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Available Capabilities
|
||||
|
||||
## Issue Tracking: issue-facade
|
||||
|
||||
**CRITICAL:** Always use this for issue operations. Never bypass with direct API calls.
|
||||
|
||||
**Docs:** `.claude/capabilities/issue-facade.md`
|
||||
**Usage:** `/use-issues`
|
||||
10
.gitmodules
vendored
10
.gitmodules
vendored
@@ -2,9 +2,13 @@
|
||||
path = wiki
|
||||
url = http://92.205.130.254:32166/coulomb/markitect_project.wiki.git
|
||||
branch = main
|
||||
[submodule "capabilities/issue-facade"]
|
||||
path = capabilities/issue-facade
|
||||
url = http://92.205.130.254:32166/coulomb/issue-facade.git
|
||||
[submodule "capabilities/kaizen-agentic"]
|
||||
path = capabilities/kaizen-agentic
|
||||
url = http://92.205.130.254:32166/coulomb/kaizen-agentic.git
|
||||
[submodule "capabilities/testdrive-jsui"]
|
||||
path = capabilities/testdrive-jsui
|
||||
url = http://92.205.130.254:32166/coulomb/testdrive-jsui.git
|
||||
[submodule "_issue-tracking/issue-facade"]
|
||||
path = _issue-tracking/issue-facade
|
||||
url = http://92.205.130.254:32166/coulomb/issue-facade.git
|
||||
branch = main
|
||||
|
||||
87
CHANGELOG.md
87
CHANGELOG.md
@@ -5,8 +5,95 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
See roadmap/YYMMDD-ROADMAPTOPIC/ directories for planning information like concepts, workplans, etc...
|
||||
See history/YYMMDD-ROADMAOTOPIC/ directories for planning information of closed topics
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.10.0] - 2026-01-06
|
||||
|
||||
### Added
|
||||
- **Schema Management System**: Comprehensive schema management infrastructure with naming conventions and versioning
|
||||
- Naming convention: `{domain}-schema-v{major}.{minor}.md` for all schemas
|
||||
- Markdown-first schema format with embedded JSON (documentation + schema in one file)
|
||||
- Schema catalog (`markitect/schemas/schema-catalog.yaml`) for metadata and discovery
|
||||
- Terminology validation example (`examples/terminology/`) demonstrating schema usage beyond manpages
|
||||
- Schema-of-schemas implementation archived in `history/2026-01-05-schema-of-schemas/`
|
||||
- **Enhanced schema-list Command**: Now displays numbered references in all output formats for easy selection
|
||||
- Simple format: `[1] schema-name.md` prefix for each schema
|
||||
- Table format: `#` column as first column
|
||||
- JSON/YAML: `number` field added to each schema
|
||||
- Default format shows timestamps inline: `schema-name.json (added: 2026-01-04T23:01:19)`
|
||||
- Table format includes Created/Updated columns
|
||||
- Cleaner timestamp formatting (removed microseconds)
|
||||
- **Multi-Schema Validation**: Enhanced schema-validate command with multiple selection methods
|
||||
- Number selection: `markitect schema-validate 1` validates schema #1
|
||||
- Range selection: `markitect schema-validate 1-3` validates schemas #1-3
|
||||
- List selection: `markitect schema-validate 1,3,5` validates schemas #1,3,5
|
||||
- Batch validation: `markitect schema-validate --all` validates all registered schemas
|
||||
- Filename selection: `markitect schema-validate schema.md` from registry
|
||||
- Filesystem path: `markitect schema-validate ./schema.md` from disk
|
||||
- Batch results displayed as clear summary table with validation status
|
||||
- Registry schemas take precedence over filesystem (with fallback)
|
||||
- Full backward compatibility with existing single-file validation
|
||||
- Enhanced control panel UI with better resize handle positioning for improved user interaction
|
||||
- **Semantic Document Validation**: Complete semantic validation system for markdown documents against x-markitect schema extensions
|
||||
- Section classification enforcement: required/recommended/optional/discouraged/improper sections validated
|
||||
- Content pattern validation: required_patterns, forbidden_patterns, discouraged_patterns with regex matching
|
||||
- Quality metrics checking: min_words, max_words, min_sentences validation with configurable thresholds
|
||||
- Link validation: Internal/external link checking with configurable policies
|
||||
- Internal links: Fragment anchors (#section) and file paths validated by default
|
||||
- External links: HTTP/HTTPS validation with --check-links flag (opt-in, may be slow)
|
||||
- Email validation: mailto: link format checking
|
||||
- Broken link detection with line numbers and detailed error messages
|
||||
- Modular validator architecture: SectionValidator, ContentValidator, LinkValidator with clean separation of concerns
|
||||
- CLI integration: `--semantic/--no-semantic`, `--strict`, `--check-links` flags for validate command
|
||||
- Comprehensive reporting: Detailed validation reports with errors/warnings, line numbers, matched text
|
||||
- Test coverage: 25 tests for semantic validators (16 section/content + 9 link), 100% passing
|
||||
- Full documentation: Semantic validation guide in SCHEMA_MANAGEMENT_GUIDE.md with examples
|
||||
- Complements existing structural AST validation for complete document compliance checking
|
||||
- **Changelog Schema**: Production schema for validating CHANGELOG.md files following Keep a Changelog format
|
||||
- Schema file: `changelog-schema-v1.0.md` validates version history structure and formatting
|
||||
- Enforces Unreleased section presence (required)
|
||||
- Validates version format: `[X.Y.Z] - YYYY-MM-DD` with semantic versioning
|
||||
- Validates change type subsections: Added, Changed, Deprecated, Removed, Fixed, Security
|
||||
- Content pattern validation for version sections, date formats (ISO 8601), and change types
|
||||
- Demonstrates real-world schema system usage: "The release that validates itself"
|
||||
- Successfully validates project CHANGELOG.md with all semantic checks passing
|
||||
|
||||
### Changed
|
||||
- **Directory Reorganization**:
|
||||
- Renamed `todo/` → `roadmap/` for better organization of planning documents
|
||||
- Completed schema-of-schemas implementation archived to `history/2026-01-05-schema-of-schemas/`
|
||||
- Moved completed planning artifacts to history for reference
|
||||
- Refactored contents control architecture to use base class pattern properly for better code organization
|
||||
- Updated all file references and paths to point to single source of truth in capabilities/testdrive-jsui/js/controls/ directory
|
||||
|
||||
### Fixed
|
||||
- **Version Detection Issue**: Fixed `markitect --version` returning "unknown" instead of actual version
|
||||
- Added `git_describe_command` to setuptools-scm configuration to filter version tags correctly
|
||||
- Configured git describe to use `--match 'v*'` pattern to ignore non-version tags
|
||||
- Version detection now works correctly with development versions (e.g., 0.9.1.dev76)
|
||||
- **Missing v0.9.0 Git Tag**: Retroactively created v0.9.0 annotated tag on commit b9c1b90 from 2025-11-14
|
||||
- Maintains version history integrity (CHANGELOG documented v0.9.0 but tag was missing)
|
||||
- Enables proper version progression to v0.10.0
|
||||
- Duplicate file structure issue by eliminating duplicate control files and consolidating to capabilities/ directory
|
||||
- Contents panel scrollbar behavior - moved overflow-y: auto to correct container level so scrollbar only spans content area when panel reaches max-height
|
||||
|
||||
### Removed
|
||||
- **BREAKING**: Legacy DocumentControls component from TestDrive JSUI plugin system - all control panel functionality now provided by enhanced control panels (ContentsControl, StatusControl, DebugControl, EditControl) with Reset All button functionality moved to EditControl for better maintainability and elimination of code duplication
|
||||
|
||||
### Completed Features
|
||||
- **Schema-of-Schemas Implementation** (All 6 Phases Complete ✅)
|
||||
- ✅ Phase 1: Filename validation for schema naming convention (`markitect/schema_naming.py`, 50 tests)
|
||||
- ✅ Phase 2: Markdown schema loader to parse `.md` schema files (`markitect/schema_loader.py`, 35 tests)
|
||||
- ✅ Phase 3: Schema-for-schemas metaschema for schema validation (`schema-schema-v1.0.md`, 12 tests)
|
||||
- ✅ Phase 4: Migration of 5 existing schemas to new format (migrated 2, deleted 3 duplicates)
|
||||
- ✅ Phase 5: CLI enhancements - numbered schema-list, multi-schema validation with selection methods
|
||||
- ✅ Phase 6: Integration testing and comprehensive documentation (SCHEMA_MANAGEMENT_GUIDE.md)
|
||||
- **Total Test Coverage**: 97 tests, 100% passing
|
||||
- **Complete Documentation**: Usage guide, naming spec, loader guide, metaschema reference
|
||||
|
||||
## [0.9.0] - 2025-11-14
|
||||
|
||||
### Added
|
||||
|
||||
59
TODO.md
59
TODO.md
@@ -6,16 +6,67 @@ The format is based on [Keep a Todofile V0.0.1](https://coulomb.social/open/Keep
|
||||
|
||||
The structure organizes **future tasks** by their impact, just as a changelog organizes past changes by their impact.
|
||||
|
||||
See roadmap/YYMMDD-ROADMAPTOPIC/ directories for planning information like concepts, workplans, etc...
|
||||
|
||||
***
|
||||
|
||||
## [Unreleased] - *Active Vibe-Coding State* 💡
|
||||
|
||||
This section is for tasks currently being discussed with or worked on by the coding assistant. These are the ephemeral, flow-of-thought tasks.
|
||||
|
||||
*No active tasks at this time.*
|
||||
### Extract Capability-Capability from Issue-Facade (Paused)
|
||||
|
||||
***
|
||||
**Context:** Issue-facade currently provides two capabilities:
|
||||
1. **issue-tracking** (explicit in CAPABILITY-issue-tracking.yaml) - Issue management across platforms
|
||||
2. **capability-capability** (implicit) - Patterns and tools for creating/managing capabilities
|
||||
|
||||
## Completed Tasks
|
||||
The **capability-capability** includes:
|
||||
- Feedback pattern (feedback/ directory, .capability/feedback CLI tool, documentation)
|
||||
- Detachment facility (.capability/detach script for clean capability removal)
|
||||
- Integration pattern (.capability/integrate.sh for project integration)
|
||||
- CAPABILITY-*.yaml specification format
|
||||
- ReusableCapabilitiesArchitecture.md (complete specification)
|
||||
- Directory conventions (_family/implementation, visible/hidden patterns)
|
||||
|
||||
**Goal:** Extract capability-capability to separate `reusable-capability` repository so it can be used by any capability in the markitect ecosystem.
|
||||
|
||||
**Approach:** Step-by-step extraction, starting with specification.
|
||||
|
||||
#### Phase 1: Specification & Planning (Current)
|
||||
|
||||
- [ ] Create CAPABILITY-capability.yaml in issue-facade to explicitly declare the implicit capability
|
||||
- [ ] Define what belongs to capability-capability family vs issue-tracking family
|
||||
- [ ] Document the capability-capability API surface (what tools/patterns it provides)
|
||||
- [ ] Identify all files/directories to extract
|
||||
- [ ] Plan extraction strategy (copy vs move, how to maintain during transition)
|
||||
|
||||
#### Phase 2: Repository Creation
|
||||
|
||||
- [ ] Create reusable-capability repository structure
|
||||
- [ ] Extract ReusableCapabilitiesArchitecture.md to new repo
|
||||
- [ ] Extract feedback pattern (directory structure, CLI tool, README)
|
||||
- [ ] Extract detachment facility (.capability/detach)
|
||||
- [ ] Extract integration scripts (.capability/integrate.sh, integration-checklist.md)
|
||||
- [ ] Create CAPABILITY-capability.yaml in new repo (canonical version)
|
||||
- [ ] Add README.md for reusable-capability repo
|
||||
|
||||
#### Phase 3: Integration & Testing
|
||||
|
||||
- [ ] Update issue-facade to depend on reusable-capability (as integrated capability)
|
||||
- [ ] Integrate reusable-capability into issue-facade using _capability/reusable-capability pattern
|
||||
- [ ] Test that issue-facade still works with extracted capability
|
||||
- [ ] Update issue-facade documentation to reference both capabilities it provides/uses
|
||||
- [ ] Verify feedback system still works
|
||||
- [ ] Verify detachment still works
|
||||
|
||||
#### Phase 4: Dogfooding & Validation
|
||||
|
||||
- [ ] Choose another markitect capability for dogfooding
|
||||
- [ ] Integrate reusable-capability into that capability
|
||||
- [ ] Add feedback system to new capability
|
||||
- [ ] Add detachment facility to new capability
|
||||
- [ ] Document learnings and refine reusable-capability based on real-world usage
|
||||
- [ ] Update ReusableCapabilitiesArchitecture.md with insights
|
||||
|
||||
**Current Step:** Phase 1, Task 1 - Create CAPABILITY-capability.yaml
|
||||
|
||||
*Recent completed tasks have been documented in CHANGELOG.md following Keep a Changelog format.*
|
||||
1
_issue-tracking/issue-facade
Submodule
1
_issue-tracking/issue-facade
Submodule
Submodule _issue-tracking/issue-facade added at 70d7ec0cdc
@@ -15,19 +15,25 @@ You are the MarkiTect project assistant, specialized in providing project status
|
||||
|
||||
### Key Project Files & Their Purpose
|
||||
|
||||
- **ProjectStatusDigest.md**: The canonical source of truth for project architecture, features, and current state
|
||||
- **ProjectDiary.md**: Chronological record of major work packages, milestones, and development sessions
|
||||
- **TODO.md**: Task management and priorities following Keep a Todofile format for maintaining coding flow
|
||||
- **TODO.md**: Current state of implemenation based on the Keep-A-Todofile format for maintaining coding flow
|
||||
- **CHANGELOG.md**: History of releases based on the Keep-A-Changelog format for easy access to what happend before
|
||||
- **roadmap/**: Directory with current and close range roadmap-topic-directories for concepts, workplans, examples...
|
||||
- **history/**: Directory with closed roadmap-topic-directories including finishd TODO.md files as YYMMDD-DONE.md
|
||||
- **Makefile**: Provides helpers to use and improve the capabilities provided by the project
|
||||
**Gitea Issues**: Backlog of issues and backlog of tasks stored as issues in gitea
|
||||
**Gitea Issues**: Backlog of issues and backlog of tasks stored as issues in gitea before selection as roadmap topics
|
||||
|
||||
### Project Infrastructure Knowledge
|
||||
|
||||
**Repository Structure:**
|
||||
- Main project hosted on Gitea with issue tracking for use cases and tasks
|
||||
- Documentation maintained in `wiki/` submodule
|
||||
- Planning documentation goes to roadmap/ROADMAPTOPIC subdirectories
|
||||
- Closed roadmap-topic-directories git-mv to history/
|
||||
- Auto generated documentation maintained in docs/
|
||||
- Human generated documentation maintained in wiki/ submodule
|
||||
- Test-driven development workflow with comprehensive test coverage
|
||||
|
||||
Important: Respect the directory structure! If in doubt ask or use directories under tmp/ to keep the structure clean!
|
||||
|
||||
**Development Workflow:**
|
||||
- Issue-driven development using Gitea API integration
|
||||
- Issue management via universal issue-facade CLI that works with multiple backends
|
||||
@@ -56,17 +62,19 @@ You are the MarkiTect project assistant, specialized in providing project status
|
||||
|
||||
When asked about project status or next steps:
|
||||
|
||||
1. **Start with Current State**: Always check ProjectStatusDigest.md for the latest architecture and status
|
||||
2. **Review Recent Progress**: Check ProjectDiary.md for recent accomplishments and context
|
||||
3. **Check Planned Work**: Read Next.md for documented next steps and priorities
|
||||
4. **Consider Git Status**: Be aware of current working directory state and recent commits
|
||||
1. **Start with Current State**: Always check TODO.md for the latest activity
|
||||
2. **Review Recent Progress**: Check CHANGELOG.md for previous work and progress
|
||||
3. **Check Planned Work**: TODO.md documents next steps and priorities, if empty see topics in roadmap/
|
||||
4. **Project Scope and Goals**: Vision, Mission, Guidelines and Usecases live in wiki/ if available
|
||||
5. **Planning New Stuff**: Requirements (Epics and Stories) are gitea issues to be planned as roadmap topics
|
||||
6. **Consider Git Status**: Allways be aware of current working directory state and recent commits
|
||||
|
||||
### Issue Management Guidelines
|
||||
|
||||
**When to Create Gitea Issues:**
|
||||
- New feature requests or enhancement ideas emerge during development
|
||||
- Bugs or technical debt are discovered but not immediately fixable
|
||||
- Future improvements are identified but outside current session scope
|
||||
- Future improvements are identified but outside current session and topic scope
|
||||
- Architecture decisions require documentation and future review
|
||||
- Sidequests that we want to remember for later implementation
|
||||
|
||||
@@ -78,10 +86,12 @@ When asked about project status or next steps:
|
||||
- Do NOT implement immediately - issues are for tracking and planning
|
||||
|
||||
**Issue vs. Immediate Work:**
|
||||
- Current session planned work: implement directly (from Next.md)
|
||||
- Discovered improvements: create issue, continue with planned work
|
||||
- Current session planned work: document in TODO.md and roadmap/ROADMAPTOPIC
|
||||
- Discovered improvements: add to workplan in roadmap topic, continue with planned work
|
||||
- Critical bugs affecting current work: fix immediately, then create issue for root cause analysis
|
||||
- Future enhancements: always create issue first for proper planning
|
||||
- Future enhancements: note in roadmap-topic to create issues first for proper planning
|
||||
- If possible create issues interactively when closing a topic, they are for human oversight and longterm
|
||||
- Do not create issues for stuff that is detailed and can be adressed before closing the current topic
|
||||
|
||||
**Response Format:**
|
||||
- Provide a brief status summary (2-3 sentences)
|
||||
@@ -102,8 +112,6 @@ When asked about project status or next steps:
|
||||
1. [Action from Next.md or logical progression]
|
||||
2. [Secondary priority or alternative approach]
|
||||
3. [Maintenance or validation task if applicable]
|
||||
|
||||
Based on: ProjectStatusDigest.md:74-79, Next.md:7-13
|
||||
```
|
||||
|
||||
## Session Start-Up Protocol
|
||||
@@ -113,10 +121,10 @@ When asked what's up for a new coding session, follow this standardized routine:
|
||||
### Start-of-Session Checklist
|
||||
1. **Mission Status**: Provide reminder to project vision and how we are doing
|
||||
2. **Recently**: Provide reminder what we did last from the last entry to the diary
|
||||
3. **NEXT.txt**: Check if we provided guidance for what to do next at the end of the last coding session
|
||||
3. **TODO.md**: Check if we provided guidance for what to do next at the end of the last coding session
|
||||
4. **git status**: Check if git is clean or work has been left unfinished
|
||||
5. **Workspace clean**: Check if workspace is clean or we left of in the middle of a TDD cycle
|
||||
6. **Issue finished**: Check if we are currently working on a specific issue or need to select the next one
|
||||
6. **Topic or issue finished**: Check if we are currently working on a specific roadmap-topic or issue
|
||||
7. **Suggestion**: Provide a sensible suggestion of what to do next
|
||||
|
||||
## Session Wrap-Up Protocol
|
||||
@@ -124,11 +132,10 @@ When asked what's up for a new coding session, follow this standardized routine:
|
||||
When asked to help wrap up a development session, follow this standardized routine:
|
||||
|
||||
### End-of-Session Checklist:
|
||||
1. **Update ProjectDiary.md**: Add entry documenting progress, challenges, and achievements
|
||||
2. **Update TODO.md**: Set clear priorities and strategy for next session using todofile format
|
||||
3. **Update ProjectStatusDigest.md**: Refresh current status, metrics, and completed features
|
||||
3. **Update roadmap-topic directory information**: Refresh current status, metrics, and completed features
|
||||
4. **Issue Management**: Review and create any issues for sidequests and discoveries made during session
|
||||
5. **Anchor patterns**: Update this project-assistant definition with any new workflow patterns
|
||||
5. **Anchor patterns**: Add Update suggestions for this project-assistant definition with any new workflow patterns
|
||||
6. **Prepare for commit**: Ensure all documentation reflects current state
|
||||
|
||||
### Session Success Indicators:
|
||||
@@ -143,9 +150,9 @@ When asked to help wrap up a development session, follow this standardized routi
|
||||
[Brief overview of accomplishments and current state]
|
||||
|
||||
## Documentation Updates
|
||||
- ✅ ProjectDiary.md: [what was added]
|
||||
- ✅ Next.md: [priorities set]
|
||||
- ✅ ProjectStatusDigest.md: [status updated]
|
||||
- ✅ TODO.md: [priorities set]
|
||||
- ✅ roadmap/TOPIC files: [what was added or changed]
|
||||
- ✅ CHANGELOG.ms: [status updated especially on release]
|
||||
|
||||
## Issues Created/Updated
|
||||
- 🎯 Issue #X: [brief description] - [reason for creation]
|
||||
@@ -157,9 +164,19 @@ When asked to help wrap up a development session, follow this standardized routi
|
||||
Ready for commit: [list of files to commit]
|
||||
```
|
||||
|
||||
### Example Capture Small Off-Topic Improvements in roadmap/eat-the-frog:
|
||||
**Smell**: Different filename conventions od conflicting concepts, unclear guideance
|
||||
**Hunch**: Ideas to explore that need consideration if useful and in scope
|
||||
**Hickups**: Notes on inefficient or roundtripping implementation to analyse later
|
||||
|
||||
Collect these in the roadmap-topic-directory and move stuff to eat-the-frog on close if unfinished
|
||||
|
||||
### Example Issue Creation During Development:
|
||||
**Scenario**: While implementing CLI commands, discover that error messages could be improved
|
||||
**Action**: Create issue "Enhance CLI error messages with user-friendly formatting and suggestions"
|
||||
**Result**: Continue with current CLI implementation, address error enhancement in future session
|
||||
|
||||
Generate issues for relevantly expensive or risky stuff and in direct feedback with developers.
|
||||
Controled in-scope-work does not need the costly issue capture, refinement, selection roundtrip.
|
||||
|
||||
Remember: Your role is to help developers quickly understand "where we are" and "what should we do next" when picking up work on the MarkiTect project, and to ensure proper session wrap-up for continuity.
|
||||
51
capabilities/DETACHED-issue-facade.yaml
Normal file
51
capabilities/DETACHED-issue-facade.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
# Detachment Manifest
|
||||
# This file records the removal of the issue-facade capability
|
||||
# Use this information to re-integrate with updated architecture
|
||||
|
||||
detachment:
|
||||
timestamp: 2025-12-17T21:23:14Z
|
||||
capability_name: issue-facade
|
||||
capability_family: issue-tracking
|
||||
integration_pattern: capabilities-directory
|
||||
original_location: /home/worsch/markitect_project/capabilities/issue-facade
|
||||
|
||||
capability_metadata:
|
||||
spec_file: CAPABILITY-issue-tracking.yaml
|
||||
version: unknown
|
||||
implementation: unknown
|
||||
maturity: unknown
|
||||
|
||||
integration_details:
|
||||
parent_project: capabilities
|
||||
parent_path: /home/worsch/markitect_project/capabilities
|
||||
|
||||
re_integration_guide: |
|
||||
To re-integrate this capability using the new architecture:
|
||||
|
||||
# Option 1: Git submodule (recommended)
|
||||
cd /home/worsch/markitect_project/capabilities
|
||||
git submodule add <repo-url> _issue-facade
|
||||
pip install -e _issue-facade/
|
||||
|
||||
# Option 2: Clone directly
|
||||
cd /home/worsch/markitect_project/capabilities
|
||||
git clone <repo-url> _issue-facade
|
||||
pip install -e _issue-facade/
|
||||
|
||||
# Option 3: Copy into project
|
||||
cd /home/worsch/markitect_project/capabilities
|
||||
cp -r /path/to/issue-facade _issue-facade
|
||||
pip install -e _issue-facade/
|
||||
|
||||
Note: Use underscore prefix (_issue-facade) per ReusableCapabilitiesArchitecture
|
||||
|
||||
notes:
|
||||
- The original integration used pattern: capabilities-directory
|
||||
- New architecture recommends: underscore-prefix at repo root
|
||||
- See ReusableCapabilitiesArchitecture.md for details
|
||||
|
||||
repository_info:
|
||||
# Fill in if re-integrating from git
|
||||
git_url: "http://92.205.130.254:32166/coulomb/issue-facade.git" # e.g., https://github.com/markitect/issue-facade
|
||||
git_branch: "main" # e.g., main
|
||||
git_commit: "35daa514e59788250847cd706c43ea78f24c5c1d" # Optional: specific commit to use
|
||||
Submodule capabilities/issue-facade deleted from 34a8bc7d4c
Submodule capabilities/kaizen-agentic updated: 1e0ff82d74...afc038d98b
1
capabilities/testdrive-jsui
Submodule
1
capabilities/testdrive-jsui
Submodule
Submodule capabilities/testdrive-jsui added at b8f13b4ae5
@@ -1,261 +0,0 @@
|
||||
# TestDrive-JSUI Capability Makefile
|
||||
# JavaScript UI testing framework for MarkiTect
|
||||
|
||||
# Capability metadata
|
||||
CAPABILITY_NAME := testdrive-jsui
|
||||
CAPABILITY_DESCRIPTION := JavaScript UI testing framework with Python integration
|
||||
|
||||
# Python virtual environment detection
|
||||
VENV_PYTHON := $(shell which python3 2>/dev/null || which python 2>/dev/null)
|
||||
ifeq ($(VENV_PYTHON),)
|
||||
VENV_PYTHON := python
|
||||
endif
|
||||
|
||||
# Node.js detection
|
||||
NODE := $(shell command -v node 2> /dev/null)
|
||||
NPM := $(shell command -v npm 2> /dev/null)
|
||||
|
||||
# Default target
|
||||
.PHONY: help
|
||||
help: ## Show testdrive-jsui capability help
|
||||
@echo "🧪 TestDrive-JSUI Capability"
|
||||
@echo "============================"
|
||||
@echo ""
|
||||
@echo "JavaScript UI Testing Framework for MarkiTect"
|
||||
@echo ""
|
||||
@echo "Environment Setup:"
|
||||
@echo " testdrive-jsui-install Install Python package"
|
||||
@echo " testdrive-jsui-install-dev Install with development dependencies"
|
||||
@echo " testdrive-jsui-install-js Install JavaScript dependencies"
|
||||
@echo " testdrive-jsui-setup Complete setup (Python + JavaScript)"
|
||||
@echo ""
|
||||
@echo "Testing:"
|
||||
@echo " testdrive-jsui-test-js Run JavaScript tests only"
|
||||
@echo " testdrive-jsui-test-python Run Python tests only"
|
||||
@echo " testdrive-jsui-test-integration Run Python-JS integration tests"
|
||||
@echo " testdrive-jsui-test-all Run all tests (JS + Python + Integration)"
|
||||
@echo ""
|
||||
@echo "Development:"
|
||||
@echo " testdrive-jsui-lint-js Lint JavaScript code"
|
||||
@echo " testdrive-jsui-lint-python Lint Python code"
|
||||
@echo " testdrive-jsui-format-python Format Python code with black"
|
||||
@echo " testdrive-jsui-watch Watch mode for JavaScript tests"
|
||||
@echo ""
|
||||
@echo "Utilities:"
|
||||
@echo " testdrive-jsui-status Show capability status"
|
||||
@echo " testdrive-jsui-clean Clean build artifacts"
|
||||
@echo " testdrive-jsui-info Show environment information"
|
||||
|
||||
# Environment status check
|
||||
.PHONY: testdrive-jsui-status
|
||||
testdrive-jsui-status: ## Show capability status
|
||||
@echo "🧪 TestDrive-JSUI Status"
|
||||
@echo "========================"
|
||||
@echo ""
|
||||
ifdef NODE
|
||||
@echo "✅ Node.js: $(shell node --version)"
|
||||
else
|
||||
@echo "❌ Node.js: Not found"
|
||||
endif
|
||||
ifdef NPM
|
||||
@echo "✅ npm: $(shell npm --version)"
|
||||
else
|
||||
@echo "❌ npm: Not found"
|
||||
endif
|
||||
@echo "✅ Python: $(shell $(VENV_PYTHON) --version)"
|
||||
@if [ -f "package.json" ]; then \
|
||||
echo "✅ package.json: Available"; \
|
||||
else \
|
||||
echo "❌ package.json: Missing"; \
|
||||
fi
|
||||
@if [ -f "pyproject.toml" ]; then \
|
||||
echo "✅ pyproject.toml: Available"; \
|
||||
else \
|
||||
echo "❌ pyproject.toml: Missing"; \
|
||||
fi
|
||||
@if [ -d "node_modules" ]; then \
|
||||
echo "✅ JavaScript dependencies: Installed"; \
|
||||
else \
|
||||
echo "❌ JavaScript dependencies: Not installed"; \
|
||||
fi
|
||||
@echo ""
|
||||
|
||||
# Installation targets
|
||||
.PHONY: testdrive-jsui-install
|
||||
testdrive-jsui-install: ## Install Python package
|
||||
$(VENV_PYTHON) -m pip install -e .
|
||||
|
||||
.PHONY: testdrive-jsui-install-dev
|
||||
testdrive-jsui-install-dev: ## Install with development dependencies
|
||||
$(VENV_PYTHON) -m pip install -e ".[dev,testing]"
|
||||
|
||||
.PHONY: testdrive-jsui-install-js
|
||||
testdrive-jsui-install-js: ## Install JavaScript dependencies
|
||||
ifndef NPM
|
||||
@echo "❌ npm not found. Please install Node.js and npm first."
|
||||
@exit 1
|
||||
endif
|
||||
npm install
|
||||
|
||||
.PHONY: testdrive-jsui-setup
|
||||
testdrive-jsui-setup: testdrive-jsui-install-dev testdrive-jsui-install-js ## Complete setup (Python + JavaScript)
|
||||
@echo "✅ TestDrive-JSUI setup complete!"
|
||||
|
||||
# Testing targets
|
||||
.PHONY: testdrive-jsui-test-js
|
||||
testdrive-jsui-test-js: ## Run JavaScript tests only
|
||||
ifndef NPM
|
||||
@echo "❌ npm not found. Run 'make testdrive-jsui-install-js' first."
|
||||
@exit 1
|
||||
endif
|
||||
npm test
|
||||
|
||||
.PHONY: testdrive-jsui-test-python
|
||||
testdrive-jsui-test-python: ## Run Python tests only
|
||||
$(VENV_PYTHON) -m pytest tests/ -v
|
||||
|
||||
.PHONY: testdrive-jsui-test-integration
|
||||
testdrive-jsui-test-integration: ## Run Python-JS integration tests
|
||||
$(VENV_PYTHON) -m pytest tests/ -v -m javascript
|
||||
|
||||
.PHONY: testdrive-jsui-test-all
|
||||
testdrive-jsui-test-all: ## Run all tests (JS + Python + Integration)
|
||||
@echo "🧪 Running all TestDrive-JSUI tests..."
|
||||
@echo ""
|
||||
ifndef NPM
|
||||
@echo "❌ npm not found. Run 'make testdrive-jsui-install-js' first."
|
||||
@exit 1
|
||||
endif
|
||||
@echo "📋 JavaScript Tests (Jest):"
|
||||
@echo "=============================="
|
||||
@if npm test > /tmp/jest_results.log 2>&1; then \
|
||||
echo "✅ JavaScript tests completed successfully"; \
|
||||
grep -E "(Test Suites:|Tests:|Time:)" /tmp/jest_results.log || true; \
|
||||
else \
|
||||
echo "❌ JavaScript tests failed"; \
|
||||
cat /tmp/jest_results.log; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "📋 Python Integration Tests (pytest):"
|
||||
@echo "======================================"
|
||||
@if $(VENV_PYTHON) -m pytest tests/ -v > /tmp/pytest_results.log 2>&1; then \
|
||||
echo "✅ Python integration tests completed successfully"; \
|
||||
grep -E "===.*passed.*===" /tmp/pytest_results.log | tail -1 || true; \
|
||||
else \
|
||||
echo "❌ Python integration tests failed"; \
|
||||
cat /tmp/pytest_results.log; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "🎯 Combined Test Results Summary:"
|
||||
@echo "=================================="
|
||||
@js_tests=$$(grep "Tests:" /tmp/jest_results.log | grep -o "[0-9]\+ passed" | grep -o "[0-9]\+" || echo "0"); \
|
||||
py_tests=$$(grep "passed" /tmp/pytest_results.log | tail -1 | grep -o "[0-9]\+ passed" | grep -o "[0-9]\+" || echo "0"); \
|
||||
js_suites=$$(grep "Test Suites:" /tmp/jest_results.log | grep -o "[0-9]\+ passed" | grep -o "[0-9]\+" || echo "0"); \
|
||||
total_tests=$$((js_tests + py_tests)); \
|
||||
echo " 📊 JavaScript: $$js_tests tests in $$js_suites test suites - ALL PASSED ✅"; \
|
||||
echo " 📊 Python: $$py_tests integration tests - ALL PASSED ✅"; \
|
||||
echo " 📊 Total: $$total_tests tests - ALL PASSED ✅"; \
|
||||
echo ""
|
||||
@echo "✅ All TestDrive-JSUI tests completed successfully!"
|
||||
@rm -f /tmp/jest_results.log /tmp/pytest_results.log
|
||||
|
||||
# Development targets
|
||||
.PHONY: testdrive-jsui-lint-js
|
||||
testdrive-jsui-lint-js: ## Lint JavaScript code
|
||||
ifndef NPM
|
||||
@echo "❌ npm not found. Run 'make testdrive-jsui-install-js' first."
|
||||
@exit 1
|
||||
endif
|
||||
npm run lint
|
||||
|
||||
.PHONY: testdrive-jsui-lint-python
|
||||
testdrive-jsui-lint-python: ## Lint Python code
|
||||
$(VENV_PYTHON) -m flake8 src/ tests/
|
||||
|
||||
.PHONY: testdrive-jsui-format-python
|
||||
testdrive-jsui-format-python: ## Format Python code with black
|
||||
$(VENV_PYTHON) -m black src/ tests/
|
||||
|
||||
.PHONY: testdrive-jsui-watch
|
||||
testdrive-jsui-watch: ## Watch mode for JavaScript tests
|
||||
ifndef NPM
|
||||
@echo "❌ npm not found. Run 'make testdrive-jsui-install-js' first."
|
||||
@exit 1
|
||||
endif
|
||||
npm run test:watch
|
||||
|
||||
# Utility targets
|
||||
.PHONY: testdrive-jsui-clean
|
||||
testdrive-jsui-clean: ## Clean build artifacts
|
||||
rm -rf build/
|
||||
rm -rf dist/
|
||||
rm -rf *.egg-info/
|
||||
rm -rf .pytest_cache/
|
||||
rm -rf coverage/
|
||||
rm -rf .coverage
|
||||
rm -rf node_modules/.cache/
|
||||
find . -type d -name __pycache__ -exec rm -rf {} +
|
||||
find . -type f -name "*.pyc" -delete
|
||||
|
||||
.PHONY: testdrive-jsui-info
|
||||
testdrive-jsui-info: ## Show environment information
|
||||
@echo "🧪 TestDrive-JSUI Environment Information"
|
||||
@echo "========================================="
|
||||
@echo ""
|
||||
@echo "📁 Capability Root: $(shell pwd)"
|
||||
@echo "🐍 Python: $(VENV_PYTHON)"
|
||||
@echo "📦 Python Version: $(shell $(VENV_PYTHON) --version)"
|
||||
ifdef NODE
|
||||
@echo "🟢 Node.js: $(shell node --version)"
|
||||
@echo "📦 npm: $(shell npm --version)"
|
||||
else
|
||||
@echo "❌ Node.js: Not available"
|
||||
endif
|
||||
@echo ""
|
||||
@echo "📋 Available JavaScript Tests:"
|
||||
@if [ -d "js/tests" ]; then \
|
||||
find js/tests -name "*.js" -type f | sed 's/^/ - /'; \
|
||||
else \
|
||||
echo " No JavaScript tests found"; \
|
||||
fi
|
||||
@echo ""
|
||||
@echo "📋 Available Python Tests:"
|
||||
@if [ -d "tests" ]; then \
|
||||
find tests -name "test_*.py" -type f | sed 's/^/ - /'; \
|
||||
else \
|
||||
echo " No Python tests found"; \
|
||||
fi
|
||||
|
||||
# Integration with main capability system
|
||||
.PHONY: capability-info
|
||||
capability-info: ## Show capability information
|
||||
@echo "Name: $(CAPABILITY_NAME)"
|
||||
@echo "Description: $(CAPABILITY_DESCRIPTION)"
|
||||
@echo "Type: JavaScript Testing Framework"
|
||||
@echo "Status: Development"
|
||||
@echo "Targets:"
|
||||
@$(MAKE) --no-print-directory help | grep "^ " | sed 's/^ / /'
|
||||
|
||||
# Quick start target
|
||||
.PHONY: testdrive-jsui-quickstart
|
||||
testdrive-jsui-quickstart: ## Quick start: setup and run basic tests
|
||||
@echo "🚀 TestDrive-JSUI Quick Start"
|
||||
@echo "============================="
|
||||
@echo ""
|
||||
@echo "📋 Step 1: Installing dependencies..."
|
||||
@$(MAKE) --no-print-directory testdrive-jsui-setup
|
||||
@echo ""
|
||||
@echo "📋 Step 2: Running status check..."
|
||||
@$(MAKE) --no-print-directory testdrive-jsui-status
|
||||
@echo ""
|
||||
@echo "📋 Step 3: Running basic tests..."
|
||||
@$(MAKE) --no-print-directory testdrive-jsui-test-python
|
||||
@echo ""
|
||||
@echo "✅ Quick start complete! Use 'make testdrive-jsui-help' for more options."
|
||||
|
||||
# Standard test target for capability discovery system compatibility
|
||||
.PHONY: test
|
||||
test: ## Run all tests (required for capability integration)
|
||||
@$(MAKE) --no-print-directory testdrive-jsui-test-all
|
||||
@@ -1,313 +0,0 @@
|
||||
# TestDrive-JSUI Capability
|
||||
|
||||
A comprehensive JavaScript UI testing framework capability for MarkiTect. Provides seamless integration between Python and JavaScript testing environments, enabling safe development and testing of JavaScript UI components.
|
||||
|
||||
## 🎯 **Purpose**
|
||||
|
||||
TestDrive-JSUI is designed to:
|
||||
|
||||
- **🔒 Protect existing JavaScript UI functionality** during refactoring and development
|
||||
- **🧪 Integrate JavaScript tests** into the main Python test suite
|
||||
- **🏗️ Provide a clean architecture** for JavaScript framework development
|
||||
- **📊 Enable comprehensive testing** of JavaScript UI components
|
||||
- **🚀 Support future extensibility** for JavaScript framework evolution
|
||||
|
||||
## 🏗️ **Architecture**
|
||||
|
||||
```
|
||||
testdrive-jsui/
|
||||
├── src/testdrive_jsui/ # Python package
|
||||
│ ├── core/ # Core framework components
|
||||
│ ├── components/ # UI component helpers
|
||||
│ ├── utils/ # Utility functions
|
||||
│ └── testing/ # Python-JS bridge
|
||||
│ ├── js_test_runner.py # JavaScript test execution
|
||||
│ └── integration.py # Pytest integration
|
||||
├── js/ # JavaScript source
|
||||
│ ├── core/ # Core JS components
|
||||
│ ├── components/ # UI components
|
||||
│ ├── utils/ # JS utilities
|
||||
│ └── tests/ # JavaScript tests
|
||||
├── tests/ # Python tests
|
||||
├── Makefile # Capability commands
|
||||
├── pyproject.toml # Python package config
|
||||
├── package.json # JavaScript dependencies
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🚀 **Quick Start**
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Python 3.8+** with pip
|
||||
- **Node.js 16+** with npm
|
||||
- **MarkiTect main project** installed
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Navigate to the capability directory
|
||||
cd capabilities/testdrive-jsui
|
||||
|
||||
# Quick setup (installs everything)
|
||||
make testdrive-jsui-quickstart
|
||||
|
||||
# Or step by step:
|
||||
make testdrive-jsui-setup # Install all dependencies
|
||||
make testdrive-jsui-status # Check environment
|
||||
make testdrive-jsui-test-all # Run all tests
|
||||
```
|
||||
|
||||
## 🧪 **Testing**
|
||||
|
||||
### JavaScript Tests
|
||||
|
||||
```bash
|
||||
# Run JavaScript tests only
|
||||
make testdrive-jsui-test-js
|
||||
|
||||
# Run with coverage
|
||||
npm run test:coverage
|
||||
|
||||
# Watch mode for development
|
||||
make testdrive-jsui-watch
|
||||
```
|
||||
|
||||
### Python Integration Tests
|
||||
|
||||
```bash
|
||||
# Run Python tests only
|
||||
make testdrive-jsui-test-python
|
||||
|
||||
# Run integration tests
|
||||
make testdrive-jsui-test-integration
|
||||
|
||||
# Run all tests
|
||||
make testdrive-jsui-test-all
|
||||
```
|
||||
|
||||
### Main Project Integration
|
||||
|
||||
From the main MarkiTect project:
|
||||
|
||||
```bash
|
||||
# Run capability tests
|
||||
make testdrive-jsui-test-all
|
||||
|
||||
# Include in main test suite
|
||||
make test-all # (when integrated)
|
||||
```
|
||||
|
||||
## 📋 **Available Commands**
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `make testdrive-jsui-help` | Show all available commands |
|
||||
| `make testdrive-jsui-status` | Check environment status |
|
||||
| `make testdrive-jsui-setup` | Install all dependencies |
|
||||
| `make testdrive-jsui-test-all` | Run all tests |
|
||||
| `make testdrive-jsui-watch` | Development watch mode |
|
||||
| `make testdrive-jsui-clean` | Clean build artifacts |
|
||||
|
||||
## 🔧 **Development**
|
||||
|
||||
### Adding JavaScript Tests
|
||||
|
||||
1. Create test files in `js/tests/`:
|
||||
```javascript
|
||||
// js/tests/test-my-component.js
|
||||
describe('MyComponent', () => {
|
||||
test('should do something', () => {
|
||||
// Your test here
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
2. Run tests:
|
||||
```bash
|
||||
make testdrive-jsui-test-js
|
||||
```
|
||||
|
||||
### Adding Python Integration Tests
|
||||
|
||||
1. Create test files in `tests/`:
|
||||
```python
|
||||
# tests/test_my_integration.py
|
||||
import pytest
|
||||
from testdrive_jsui.testing import JavaScriptTestRunner
|
||||
|
||||
@pytest.mark.javascript
|
||||
def test_my_js_component():
|
||||
runner = JavaScriptTestRunner()
|
||||
result = runner.run_specific_test('test-my-component.js')
|
||||
assert result.success
|
||||
```
|
||||
|
||||
2. Run tests:
|
||||
```bash
|
||||
make testdrive-jsui-test-integration
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Lint JavaScript
|
||||
make testdrive-jsui-lint-js
|
||||
|
||||
# Lint Python
|
||||
make testdrive-jsui-lint-python
|
||||
|
||||
# Format Python code
|
||||
make testdrive-jsui-format-python
|
||||
```
|
||||
|
||||
## 🔗 **Integration with Main Project**
|
||||
|
||||
### Python Test Suite Integration
|
||||
|
||||
The capability provides pytest integration to run JavaScript tests from Python:
|
||||
|
||||
```python
|
||||
# In main project tests
|
||||
from testdrive_jsui.testing import JavaScriptTestRunner
|
||||
|
||||
def test_javascript_ui_components():
|
||||
runner = JavaScriptTestRunner()
|
||||
result = runner.run_js_tests()
|
||||
assert result.success
|
||||
assert result.tests_passed > 0
|
||||
```
|
||||
|
||||
### Capability System Integration
|
||||
|
||||
The capability integrates with MarkiTect's capability discovery system:
|
||||
|
||||
```bash
|
||||
# From main project
|
||||
make capabilities-list # Shows testdrive-jsui
|
||||
make testdrive-jsui-test-all # Direct delegation
|
||||
make capabilities-test # Includes JS tests
|
||||
```
|
||||
|
||||
## 📊 **Testing Framework Features**
|
||||
|
||||
### JavaScript Test Runner
|
||||
|
||||
- **Jest integration** with JSDOM environment
|
||||
- **Coverage reporting** with detailed metrics
|
||||
- **Test isolation** with proper setup/teardown
|
||||
- **Mock support** for DOM APIs and browser features
|
||||
- **Async testing** support for modern JavaScript
|
||||
|
||||
### Python-JavaScript Bridge
|
||||
|
||||
- **Subprocess execution** of JavaScript tests
|
||||
- **Result parsing** with structured output
|
||||
- **Error handling** with detailed failure information
|
||||
- **Test discovery** for pytest integration
|
||||
- **Coverage integration** between Python and JavaScript
|
||||
|
||||
### Safety Mechanisms
|
||||
|
||||
- **Copy-first migration** (never move, always copy)
|
||||
- **Dual-track testing** during migration
|
||||
- **Gradual integration** with rollback options
|
||||
- **Test verification** at each step
|
||||
- **Environment validation** before execution
|
||||
|
||||
## 🔄 **Migration Strategy**
|
||||
|
||||
When migrating JavaScript UI code to this capability:
|
||||
|
||||
1. **Copy** (don't move) JavaScript files to `js/` directory
|
||||
2. **Verify** tests work in new location
|
||||
3. **Create** Python integration tests
|
||||
4. **Run** dual-track testing to compare results
|
||||
5. **Gradually** switch to capability-based testing
|
||||
6. **Remove** original files only after full verification
|
||||
|
||||
## 📚 **Examples**
|
||||
|
||||
### Running Specific Tests
|
||||
|
||||
```bash
|
||||
# Run a specific JavaScript test
|
||||
npm test -- --testNamePattern="SectionManager"
|
||||
|
||||
# Run specific Python integration test
|
||||
pytest tests/test_section_manager_integration.py -v
|
||||
|
||||
# Run tests with coverage
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Environment Information
|
||||
|
||||
```bash
|
||||
# Get detailed environment info
|
||||
make testdrive-jsui-info
|
||||
|
||||
# Check what tests are available
|
||||
make testdrive-jsui-status
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```bash
|
||||
# Start development session
|
||||
make testdrive-jsui-watch # Terminal 1: Watch JS tests
|
||||
make testdrive-jsui-test-python # Terminal 2: Run Python tests
|
||||
|
||||
# Before committing
|
||||
make testdrive-jsui-lint-js # Lint JavaScript
|
||||
make testdrive-jsui-format-python # Format Python
|
||||
make testdrive-jsui-test-all # Run all tests
|
||||
```
|
||||
|
||||
## 🎯 **Future Enhancements**
|
||||
|
||||
- **Visual regression testing** with screenshot comparison
|
||||
- **Performance benchmarking** for JavaScript components
|
||||
- **Browser automation** with Selenium/Playwright
|
||||
- **Component documentation** auto-generation
|
||||
- **Real browser testing** in CI/CD pipelines
|
||||
|
||||
## 📋 **Troubleshooting**
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Node.js not found:**
|
||||
```bash
|
||||
# Install Node.js first
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
**Tests failing:**
|
||||
```bash
|
||||
# Check environment
|
||||
make testdrive-jsui-status
|
||||
|
||||
# Reinstall dependencies
|
||||
make testdrive-jsui-clean
|
||||
make testdrive-jsui-setup
|
||||
```
|
||||
|
||||
**Integration issues:**
|
||||
```bash
|
||||
# Verify Python package is installed
|
||||
pip list | grep testdrive-jsui
|
||||
|
||||
# Check JavaScript dependencies
|
||||
npm list
|
||||
```
|
||||
|
||||
## 📄 **License**
|
||||
|
||||
MIT License - See main MarkiTect project for details.
|
||||
|
||||
---
|
||||
|
||||
*Generated: 2025-11-09*
|
||||
*Status: Phase 1 Implementation*
|
||||
*Next: Copy JavaScript files and create integration tests*
|
||||
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
* DebugPanel Component
|
||||
*
|
||||
* Extracted from monolithic editor.js as part of architecture refactoring.
|
||||
* Handles debug message display and management for client-side debugging.
|
||||
*
|
||||
* Dependencies:
|
||||
* - None (standalone component)
|
||||
*/
|
||||
|
||||
/**
|
||||
* DebugPanel - Manages debug message display and interaction
|
||||
*/
|
||||
class DebugPanel {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
this.isActive = false;
|
||||
this.maxMessages = 1000; // Keep last 1000 messages
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a debug message
|
||||
*/
|
||||
addMessage(message, category = 'INFO') {
|
||||
const messageObj = {
|
||||
message,
|
||||
category,
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
};
|
||||
|
||||
this.messages.push(messageObj);
|
||||
|
||||
// Keep only last maxMessages
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages = this.messages.slice(-this.maxMessages);
|
||||
}
|
||||
|
||||
// Auto-update if panel is visible
|
||||
if (this.isActive) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the debug panel on/off
|
||||
*/
|
||||
toggle() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isActive) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the debug panel
|
||||
*/
|
||||
show() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
debugContainer.style.display = 'block';
|
||||
debugButton.textContent = '🔍 Debug (ON)';
|
||||
debugButton.style.background = '#28a745';
|
||||
this.isActive = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the debug panel
|
||||
*/
|
||||
hide() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
const debugButton = document.getElementById('toggle-debug');
|
||||
|
||||
if (!debugContainer || !debugButton) {
|
||||
console.warn('DebugPanel: Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
debugContainer.style.display = 'none';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
debugButton.style.background = '#6c757d';
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the debug panel with current messages
|
||||
*/
|
||||
update() {
|
||||
const debugContainer = document.getElementById('debug-messages-container');
|
||||
if (!debugContainer || !this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.messages.length === 0) {
|
||||
debugContainer.innerHTML = '<div style="color: #6c757d; font-style: italic; padding: 12px;">No debug messages yet. Click sections to generate debug output.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the last 50 messages in reverse order (newest first)
|
||||
const recentMessages = this.messages.slice(-50).reverse();
|
||||
|
||||
const messagesHtml = recentMessages.map(msg => {
|
||||
const categoryColor = {
|
||||
'INFO': '#17a2b8',
|
||||
'WARNING': '#ffc107',
|
||||
'ERROR': '#dc3545',
|
||||
'SUCCESS': '#28a745',
|
||||
'DEBUG': '#6f42c1'
|
||||
}[msg.category] || '#6c757d';
|
||||
|
||||
return `
|
||||
<div style="margin-bottom: 6px; padding: 4px; border-left: 3px solid ${categoryColor}; background: white; border-radius: 2px;">
|
||||
<span style="color: #6c757d; font-size: 11px;">[${msg.timestamp}]</span>
|
||||
<span style="color: ${categoryColor}; font-weight: bold;">${msg.category}:</span>
|
||||
<span style="color: #333;">${msg.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
debugContainer.innerHTML = `
|
||||
<div style="margin-bottom: 8px; padding: 6px; background: #e9ecef; border-radius: 4px; font-weight: bold; color: #495057;">
|
||||
Debug Messages (${this.messages.length} total, showing last ${recentMessages.length})
|
||||
<button id="debug-clear-btn" style="float: right; background: #dc3545; color: white; border: none; padding: 2px 6px; border-radius: 2px; font-size: 11px; cursor: pointer;">Clear</button>
|
||||
</div>
|
||||
<div style="max-height: 250px; overflow-y: auto;">
|
||||
${messagesHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listener for clear button
|
||||
const clearBtn = debugContainer.querySelector('#debug-clear-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
this.clear();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom to show newest messages
|
||||
const scrollContainer = debugContainer.querySelector('div[style*="overflow-y"]');
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all debug messages
|
||||
*/
|
||||
clear() {
|
||||
this.messages = [];
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of messages
|
||||
*/
|
||||
getMessageCount() {
|
||||
return this.messages.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent messages
|
||||
*/
|
||||
getRecentMessages(count = 10) {
|
||||
return this.messages.slice(-count);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { DebugPanel };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DebugPanel = DebugPanel;
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
/**
|
||||
* DocumentControls Component
|
||||
*
|
||||
* Extracted from monolithic editor.js as part of architecture refactoring.
|
||||
* Handles the floating control panel and document-level actions.
|
||||
*
|
||||
* Dependencies:
|
||||
* - None (standalone component)
|
||||
*/
|
||||
|
||||
/**
|
||||
* DocumentControls - Manages the floating control panel and its buttons
|
||||
*/
|
||||
class DocumentControls {
|
||||
constructor() {
|
||||
this.controlPanel = null;
|
||||
this.buttons = new Map();
|
||||
this.eventHandlers = new Map();
|
||||
this.isVisible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the control panel and add it to the DOM
|
||||
*/
|
||||
create() {
|
||||
if (this.controlPanel) {
|
||||
this.destroy(); // Remove existing panel
|
||||
}
|
||||
|
||||
// Also remove any existing panel with the same ID in the DOM
|
||||
const existingPanel = document.getElementById('markitect-global-controls');
|
||||
if (existingPanel && existingPanel.parentNode) {
|
||||
existingPanel.parentNode.removeChild(existingPanel);
|
||||
}
|
||||
|
||||
// Create the floating control panel
|
||||
this.controlPanel = document.createElement('div');
|
||||
this.controlPanel.id = 'markitect-global-controls';
|
||||
this.controlPanel.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(248, 249, 250, 0.95);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(8px);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 14px;
|
||||
min-width: 200px;
|
||||
`;
|
||||
|
||||
// Add title
|
||||
const title = document.createElement('div');
|
||||
title.style.cssText = `
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #495057;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 4px;
|
||||
`;
|
||||
title.textContent = 'Document Controls';
|
||||
|
||||
// Create button container
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.id = 'button-container';
|
||||
buttonContainer.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
this.controlPanel.appendChild(title);
|
||||
this.controlPanel.appendChild(buttonContainer);
|
||||
|
||||
// Add default buttons
|
||||
this.addDefaultButtons();
|
||||
|
||||
// Add debug messages container
|
||||
this.addDebugContainer();
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(this.controlPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default buttons to the control panel
|
||||
*/
|
||||
addDefaultButtons() {
|
||||
// Save Document button
|
||||
this.addButton('save-document', '💾 Save Document', '#28a745');
|
||||
|
||||
// Reset All button
|
||||
this.addButton('reset-all', '🔄 Reset All', '#ffc107', '#212529');
|
||||
|
||||
// Show Status button
|
||||
this.addButton('show-status', '📊 Show Status', '#17a2b8');
|
||||
|
||||
// Debug button
|
||||
this.addButton('toggle-debug', '🔍 Debug', '#6c757d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add debug container to the control panel
|
||||
*/
|
||||
addDebugContainer() {
|
||||
const debugContainer = document.createElement('div');
|
||||
debugContainer.id = 'debug-messages-container';
|
||||
debugContainer.style.cssText = `
|
||||
margin-top: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background: #f8f9fa;
|
||||
padding: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
display: none;
|
||||
`;
|
||||
|
||||
this.controlPanel.appendChild(debugContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a button to the control panel
|
||||
*/
|
||||
addButton(id, text, backgroundColor, textColor = 'white') {
|
||||
const buttonContainer = this.controlPanel.querySelector('#button-container');
|
||||
if (!buttonContainer) {
|
||||
throw new Error('Button container not found. Call create() first.');
|
||||
}
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.id = id;
|
||||
button.textContent = text;
|
||||
button.style.cssText = `
|
||||
background: ${backgroundColor};
|
||||
color: ${textColor};
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
`;
|
||||
|
||||
buttonContainer.appendChild(button);
|
||||
this.buttons.set(id, button);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a button from the control panel
|
||||
*/
|
||||
removeButton(id) {
|
||||
const button = this.buttons.get(id);
|
||||
if (button && button.parentNode) {
|
||||
button.parentNode.removeChild(button);
|
||||
this.buttons.delete(id);
|
||||
this.eventHandlers.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set event handlers for buttons
|
||||
*/
|
||||
setEventHandlers(handlers) {
|
||||
for (const [buttonId, handler] of Object.entries(handlers)) {
|
||||
const button = this.buttons.get(buttonId);
|
||||
if (button) {
|
||||
// Remove existing handler if any
|
||||
if (this.eventHandlers.has(buttonId)) {
|
||||
button.removeEventListener('click', this.eventHandlers.get(buttonId));
|
||||
}
|
||||
|
||||
// Add new handler
|
||||
button.addEventListener('click', handler);
|
||||
this.eventHandlers.set(buttonId, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the control panel
|
||||
*/
|
||||
show() {
|
||||
if (this.controlPanel) {
|
||||
this.controlPanel.style.display = 'block';
|
||||
this.isVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the control panel
|
||||
*/
|
||||
hide() {
|
||||
if (this.controlPanel) {
|
||||
this.controlPanel.style.display = 'none';
|
||||
this.isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status display (can be extended as needed)
|
||||
*/
|
||||
updateStatus(status) {
|
||||
// This method can be extended to show status information
|
||||
// For now, it just stores the status for potential display
|
||||
this.lastStatus = status;
|
||||
|
||||
// Could update a status indicator in the panel if needed
|
||||
if (status && this.controlPanel) {
|
||||
const title = this.controlPanel.querySelector('div');
|
||||
if (title) {
|
||||
const statusText = `Document Controls (${status.totalSections} sections, ${status.editingSections} editing)`;
|
||||
// Could update title or add status indicator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the control panel element
|
||||
*/
|
||||
getControlPanel() {
|
||||
return this.controlPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the control panel and clean up
|
||||
*/
|
||||
destroy() {
|
||||
if (this.controlPanel && this.controlPanel.parentNode) {
|
||||
this.controlPanel.parentNode.removeChild(this.controlPanel);
|
||||
}
|
||||
|
||||
// Clean up references
|
||||
this.controlPanel = null;
|
||||
this.buttons.clear();
|
||||
this.eventHandlers.clear();
|
||||
this.isVisible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the control panel is visible
|
||||
*/
|
||||
isVisible() {
|
||||
return this.isVisible && this.controlPanel && this.controlPanel.style.display !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all button IDs
|
||||
*/
|
||||
getButtonIds() {
|
||||
return Array.from(this.buttons.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific button by ID
|
||||
*/
|
||||
getButton(id) {
|
||||
return this.buttons.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { DocumentControls };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.DocumentControls = DocumentControls;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,544 +0,0 @@
|
||||
/**
|
||||
* SectionManager Component
|
||||
*
|
||||
* Extracted from monolithic editor.js as part of architecture refactoring.
|
||||
* Manages the collection of sections and their state transitions.
|
||||
*
|
||||
* Dependencies:
|
||||
* - EditState enum (imported)
|
||||
* - SectionType enum (imported)
|
||||
* - Section class (imported)
|
||||
* - debug function (imported)
|
||||
*/
|
||||
|
||||
// Import dependencies - these will be separate modules
|
||||
const EditState = Object.freeze({
|
||||
ORIGINAL: 'original',
|
||||
EDITING: 'editing',
|
||||
MODIFIED: 'modified',
|
||||
SAVED: 'saved'
|
||||
});
|
||||
|
||||
const SectionType = Object.freeze({
|
||||
HEADING: 'heading',
|
||||
PARAGRAPH: 'paragraph',
|
||||
LIST: 'list',
|
||||
CODE: 'code',
|
||||
QUOTE: 'quote',
|
||||
TABLE: 'table',
|
||||
HR: 'hr',
|
||||
IMAGE: 'image'
|
||||
});
|
||||
|
||||
// Debug function (will be extracted to utils)
|
||||
function debug(message, category = 'INFO') {
|
||||
// Simple console debug for now - will be enhanced later
|
||||
console.log(`DEBUG ${category}: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section Class - manages individual section state and content
|
||||
*/
|
||||
class Section {
|
||||
constructor(id, markdown, type) {
|
||||
this.id = id;
|
||||
this.originalMarkdown = markdown;
|
||||
this.currentMarkdown = markdown;
|
||||
this.editingMarkdown = markdown;
|
||||
this.pendingMarkdown = null;
|
||||
this.type = type;
|
||||
this.state = EditState.ORIGINAL;
|
||||
this.domElement = null;
|
||||
this.lastSaved = null;
|
||||
this.created = new Date();
|
||||
}
|
||||
|
||||
static generateId(markdown, position, strategy = 'hash', parentId = null) {
|
||||
return this.generateIdWithStrategy(markdown, position, strategy, parentId);
|
||||
}
|
||||
|
||||
static generateIdWithStrategy(markdown, position, strategy = 'hash', parentId = null) {
|
||||
const sanitizedContent = this.sanitizeContentForId(markdown);
|
||||
const normalizedContent = this.normalizeContentForHashing(sanitizedContent);
|
||||
const sectionType = this.detectType(markdown);
|
||||
|
||||
switch (strategy) {
|
||||
case 'timestamp':
|
||||
return this.generateTimestampId(normalizedContent, position, sectionType);
|
||||
case 'sequential':
|
||||
return this.generateSequentialId(normalizedContent, position, sectionType);
|
||||
case 'hierarchical':
|
||||
return this.generateHierarchicalId(normalizedContent, position, parentId);
|
||||
case 'hash':
|
||||
default:
|
||||
return this.generateAdvancedId(normalizedContent, position, sectionType);
|
||||
}
|
||||
}
|
||||
|
||||
static generateAdvancedId(content, position, sectionType) {
|
||||
const contentHash = this.generateCryptoHash(content);
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
const positionHex = position.toString(16).padStart(2, '0');
|
||||
|
||||
return `section-${typePrefix}-${contentHash}-${positionHex}`;
|
||||
}
|
||||
|
||||
static generateCryptoHash(content) {
|
||||
let hash = 0;
|
||||
if (content.length === 0) return '00000000';
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
const hexHash = Math.abs(hash).toString(16).padStart(8, '0');
|
||||
return hexHash.substring(0, 8);
|
||||
}
|
||||
|
||||
static normalizeContentForHashing(content) {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
static sanitizeContentForId(content) {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return content
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/[^\w\s\-_.#]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
static generateTimestampId(content, position = 0, sectionType = 'paragraph') {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const contentSnippet = this.generateCryptoHash(content || '').substring(0, 4);
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
|
||||
return `section-${typePrefix}-${contentSnippet}-${timestamp}`;
|
||||
}
|
||||
|
||||
static generateSequentialId(content, position, sectionType = 'paragraph') {
|
||||
const safeType = sectionType || 'paragraph';
|
||||
const typePrefix = safeType.substring(0, 3);
|
||||
const seqNumber = (position || 0).toString().padStart(3, '0');
|
||||
const contentHash = this.generateCryptoHash(content || '').substring(0, 4);
|
||||
|
||||
return `section-${typePrefix}-seq${seqNumber}-${contentHash}`;
|
||||
}
|
||||
|
||||
static generateHierarchicalId(content, position, parentId = null) {
|
||||
const contentHash = this.generateCryptoHash(content || '').substring(0, 6);
|
||||
|
||||
if (parentId) {
|
||||
const childIndex = (position || 0).toString().padStart(2, '0');
|
||||
return `${parentId}-child-${childIndex}-${contentHash}`;
|
||||
} else {
|
||||
return `section-root-${position || 0}-${contentHash}`;
|
||||
}
|
||||
}
|
||||
|
||||
static detectType(markdown) {
|
||||
if (!markdown || typeof markdown !== 'string') {
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
const content = markdown.replace(/^\n+|\n+$/g, '');
|
||||
if (!content) {
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
const trimmed = content.trim();
|
||||
|
||||
// Detection order matters - most specific first
|
||||
if (this.isHeading(trimmed)) {
|
||||
return SectionType.HEADING;
|
||||
}
|
||||
|
||||
if (this.isImage(trimmed)) {
|
||||
return SectionType.IMAGE;
|
||||
}
|
||||
|
||||
if (this.isCodeBlock(trimmed)) {
|
||||
return SectionType.CODE;
|
||||
}
|
||||
|
||||
return SectionType.PARAGRAPH;
|
||||
}
|
||||
|
||||
static isHeading(trimmed) {
|
||||
const headingPattern = /^#{1,6}\s+.+/;
|
||||
return headingPattern.test(trimmed);
|
||||
}
|
||||
|
||||
static isImage(trimmed) {
|
||||
const imagePattern = /!\[.*?\]\([^)]+\)/;
|
||||
return imagePattern.test(trimmed);
|
||||
}
|
||||
|
||||
static isCodeBlock(trimmed) {
|
||||
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes('```') || trimmed.includes('~~~')) {
|
||||
const codeBlockPattern = /```[\s\S]*?```|~~~[\s\S]*?~~~/;
|
||||
if (codeBlockPattern.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
if (this.state === EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is already being edited`);
|
||||
}
|
||||
this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
|
||||
this.state = EditState.EDITING;
|
||||
return this.editingMarkdown;
|
||||
}
|
||||
|
||||
updateContent(markdown) {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.editingMarkdown = markdown;
|
||||
}
|
||||
|
||||
acceptChanges() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.currentMarkdown = this.editingMarkdown;
|
||||
this.editingMarkdown = null;
|
||||
this.pendingMarkdown = null;
|
||||
this.state = EditState.SAVED;
|
||||
this.lastSaved = new Date();
|
||||
return this.currentMarkdown;
|
||||
}
|
||||
|
||||
cancelChanges() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
throw new Error(`Section ${this.id} is not in editing state`);
|
||||
}
|
||||
this.editingMarkdown = null;
|
||||
if (this.pendingMarkdown !== null) {
|
||||
this.state = EditState.MODIFIED;
|
||||
return this.pendingMarkdown;
|
||||
} else if (this.lastSaved !== null) {
|
||||
this.state = EditState.SAVED;
|
||||
return this.currentMarkdown;
|
||||
} else {
|
||||
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
||||
return this.currentMarkdown;
|
||||
}
|
||||
}
|
||||
|
||||
stopEditing() {
|
||||
if (this.state !== EditState.EDITING) {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
|
||||
this.pendingMarkdown = this.editingMarkdown;
|
||||
this.state = EditState.MODIFIED;
|
||||
} else {
|
||||
this.pendingMarkdown = null;
|
||||
if (this.lastSaved !== null) {
|
||||
this.state = EditState.SAVED;
|
||||
} else {
|
||||
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
||||
}
|
||||
}
|
||||
|
||||
this.editingMarkdown = null;
|
||||
return this.state;
|
||||
}
|
||||
|
||||
resetToOriginal() {
|
||||
this.currentMarkdown = this.originalMarkdown;
|
||||
this.editingMarkdown = this.originalMarkdown;
|
||||
this.pendingMarkdown = null;
|
||||
this.state = EditState.ORIGINAL;
|
||||
return this.originalMarkdown;
|
||||
}
|
||||
|
||||
isEditing() {
|
||||
return this.state === EditState.EDITING;
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return this.currentMarkdown !== this.originalMarkdown;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
id: this.id,
|
||||
state: this.state,
|
||||
hasChanges: this.hasChanges(),
|
||||
isEditing: this.isEditing(),
|
||||
contentLength: this.currentMarkdown.length,
|
||||
lastSaved: this.lastSaved,
|
||||
type: this.type,
|
||||
originalLength: this.originalMarkdown.length,
|
||||
currentLength: this.currentMarkdown.length
|
||||
};
|
||||
}
|
||||
|
||||
isImage() {
|
||||
return this.type === SectionType.IMAGE;
|
||||
}
|
||||
|
||||
redetectType(content = null) {
|
||||
const markdown = content || this.currentMarkdown;
|
||||
const oldType = this.type;
|
||||
this.type = Section.detectType(markdown);
|
||||
|
||||
if (oldType !== this.type) {
|
||||
debug(`Section ${this.id} type changed from ${oldType} to ${this.type}`, 'TYPE_DETECTION');
|
||||
}
|
||||
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SectionManager - Manages the collection of sections
|
||||
*/
|
||||
class SectionManager {
|
||||
constructor() {
|
||||
this.sections = new Map();
|
||||
this.listeners = new Map();
|
||||
this.statusInterval = null;
|
||||
this.lastStatusUpdate = new Date().toISOString();
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(callback => callback(data));
|
||||
}
|
||||
}
|
||||
|
||||
createSectionsFromMarkdown(markdownContent) {
|
||||
// Split content into blocks separated by double newlines
|
||||
const blocks = markdownContent.split(/\n\s*\n/);
|
||||
const sections = [];
|
||||
let position = 0;
|
||||
|
||||
for (const block of blocks) {
|
||||
const trimmedBlock = block.trim();
|
||||
if (!trimmedBlock) continue;
|
||||
|
||||
// Check if this block should be split further
|
||||
const lines = trimmedBlock.split('\n');
|
||||
let currentSection = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const isHeading = /^#{1,6}\s/.test(line.trim());
|
||||
const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line);
|
||||
|
||||
// Each heading or image starts a new section
|
||||
if ((isHeading || isImage) && currentSection.trim()) {
|
||||
// Save the previous section
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
position++;
|
||||
currentSection = line;
|
||||
} else {
|
||||
if (currentSection) currentSection += '\n';
|
||||
currentSection += line;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the final section from this block
|
||||
if (currentSection.trim()) {
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('sections-created', { sections, count: sections.length });
|
||||
return sections;
|
||||
}
|
||||
|
||||
startEditing(sectionId) {
|
||||
debug('MANAGER: startEditing called for: ' + sectionId, 'MANAGER');
|
||||
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
if (section.isEditing()) {
|
||||
debug('MANAGER: Section already in editing state: ' + sectionId, 'MANAGER');
|
||||
return section.editingMarkdown;
|
||||
}
|
||||
|
||||
debug('MANAGER: Starting edit for section: ' + sectionId, 'MANAGER');
|
||||
const content = section.startEdit();
|
||||
|
||||
debug('MANAGER: About to emit edit-started event for: ' + sectionId, 'MANAGER');
|
||||
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
|
||||
debug('MANAGER: Emitted edit-started event for: ' + sectionId, 'MANAGER');
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
updateContent(sectionId, markdown) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const oldType = section.type;
|
||||
section.updateContent(markdown);
|
||||
const newType = section.redetectType(markdown);
|
||||
|
||||
const eventData = {
|
||||
sectionId,
|
||||
markdown,
|
||||
section: section.getStatus(),
|
||||
typeChanged: oldType !== newType,
|
||||
oldType,
|
||||
newType
|
||||
};
|
||||
|
||||
this.emit('content-updated', eventData);
|
||||
|
||||
if (oldType !== newType) {
|
||||
this.emit('section-type-changed', {
|
||||
sectionId,
|
||||
oldType,
|
||||
newType,
|
||||
section: section.getStatus()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
acceptChanges(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.acceptChanges();
|
||||
this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
cancelChanges(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.cancelChanges();
|
||||
this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
resetSection(sectionId) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
const content = section.resetToOriginal();
|
||||
this.emit('section-reset', { sectionId, content, section: section.getStatus() });
|
||||
return content;
|
||||
}
|
||||
|
||||
getDocumentMarkdown() {
|
||||
const sortedSections = Array.from(this.sections.values())
|
||||
.sort((a, b) => a.created - b.created);
|
||||
|
||||
return sortedSections.map(section => section.currentMarkdown).join('\n\n');
|
||||
}
|
||||
|
||||
getAllSections() {
|
||||
return Array.from(this.sections.values());
|
||||
}
|
||||
|
||||
getDocumentStatus() {
|
||||
const sections = Array.from(this.sections.values());
|
||||
const editingSections = sections.filter(section => section.isEditing).length;
|
||||
|
||||
return {
|
||||
totalSections: sections.length,
|
||||
editingSections: editingSections
|
||||
};
|
||||
}
|
||||
|
||||
extractHeadings(content) {
|
||||
if (!content) return [];
|
||||
const lines = content.split('\n');
|
||||
return lines.filter(line => /^#{1,6}\s/.test(line.trim()));
|
||||
}
|
||||
|
||||
handleSectionSplit(sectionId, newContent) {
|
||||
const section = this.sections.get(sectionId);
|
||||
if (!section) {
|
||||
throw new Error(`Section ${sectionId} not found`);
|
||||
}
|
||||
|
||||
// Remove the original section
|
||||
this.sections.delete(sectionId);
|
||||
|
||||
// Create new sections from the content
|
||||
const newSections = this.createSectionsFromMarkdown(newContent);
|
||||
|
||||
// Emit section-split event
|
||||
this.emit('section-split', {
|
||||
originalSectionId: sectionId,
|
||||
newSections: newSections,
|
||||
count: newSections.length
|
||||
});
|
||||
|
||||
return newSections;
|
||||
}
|
||||
|
||||
createSectionsFromContent(content) {
|
||||
return this.createSectionsFromMarkdown(content);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests and other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { SectionManager, Section, EditState, SectionType };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SectionManager = SectionManager;
|
||||
window.Section = Section;
|
||||
window.EditState = EditState;
|
||||
window.SectionType = SectionType;
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
/**
|
||||
* Button Functionality and DOM Events Tests
|
||||
*
|
||||
* Tests button interactions, event handling, and DOM manipulation
|
||||
* Based on functionality from history/javascript-dev-tests/test_*button*.js and test_*events*.js files
|
||||
*/
|
||||
|
||||
describe('Button Functionality and DOM Events', () => {
|
||||
let mockSection;
|
||||
let documentControls;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup DOM with various buttons and controls
|
||||
document.body.innerHTML = `
|
||||
<div id="content">
|
||||
<div class="section" data-section-id="test-section">
|
||||
<div class="section-content">
|
||||
<p>Section content</p>
|
||||
</div>
|
||||
<div class="section-controls">
|
||||
<button class="edit-btn" data-action="edit">Edit</button>
|
||||
<button class="accept-btn" data-action="accept" style="display: none;">Accept</button>
|
||||
<button class="cancel-btn" data-action="cancel" style="display: none;">Cancel</button>
|
||||
<button class="delete-btn" data-action="delete">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floating-controls">
|
||||
<button class="add-section-btn">Add Section</button>
|
||||
<button class="save-all-btn">Save All</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
mockSection = document.querySelector('.section');
|
||||
|
||||
// Load components
|
||||
require('../components/document-controls.js');
|
||||
if (global.DocumentControls) {
|
||||
documentControls = new global.DocumentControls(document.getElementById('content'));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Section edit buttons', () => {
|
||||
test('should show accept/cancel buttons when edit is clicked', () => {
|
||||
const editBtn = document.querySelector('.edit-btn');
|
||||
const acceptBtn = document.querySelector('.accept-btn');
|
||||
const cancelBtn = document.querySelector('.cancel-btn');
|
||||
|
||||
expect(editBtn).toBeTruthy();
|
||||
|
||||
// Simulate edit button click
|
||||
editBtn.click();
|
||||
|
||||
// In real implementation, accept/cancel should become visible
|
||||
expect(acceptBtn.style.display).toBe('none'); // Initially hidden
|
||||
expect(cancelBtn.style.display).toBe('none'); // Initially hidden
|
||||
|
||||
// Test that buttons exist for functionality
|
||||
expect(acceptBtn).toBeTruthy();
|
||||
expect(cancelBtn).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should hide edit button when in edit mode', () => {
|
||||
const editBtn = document.querySelector('.edit-btn');
|
||||
|
||||
editBtn.click();
|
||||
|
||||
// In real implementation, edit button should be hidden
|
||||
expect(editBtn.style.display).not.toBe('block');
|
||||
});
|
||||
|
||||
test('should restore edit button when edit is cancelled', () => {
|
||||
const editBtn = document.querySelector('.edit-btn');
|
||||
const cancelBtn = document.querySelector('.cancel-btn');
|
||||
|
||||
// Simulate edit mode
|
||||
editBtn.style.display = 'none';
|
||||
cancelBtn.style.display = 'inline-block';
|
||||
|
||||
cancelBtn.click();
|
||||
|
||||
// In real implementation, should restore edit button
|
||||
expect(cancelBtn).toBeTruthy();
|
||||
expect(editBtn).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Button event propagation', () => {
|
||||
test('should prevent event bubbling for section buttons', () => {
|
||||
const editBtn = document.querySelector('.edit-btn');
|
||||
let sectionClicked = false;
|
||||
|
||||
mockSection.addEventListener('click', () => {
|
||||
sectionClicked = true;
|
||||
});
|
||||
|
||||
// Create event with stopPropagation mock
|
||||
const clickEvent = new Event('click', { bubbles: true });
|
||||
clickEvent.stopPropagation = jest.fn();
|
||||
|
||||
editBtn.dispatchEvent(clickEvent);
|
||||
|
||||
// In real implementation, should call stopPropagation
|
||||
expect(clickEvent.stopPropagation).toHaveBeenCalledWith ||
|
||||
expect(sectionClicked).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle rapid button clicks gracefully', () => {
|
||||
const editBtn = document.querySelector('.edit-btn');
|
||||
|
||||
// Simulate rapid clicks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
editBtn.click();
|
||||
}
|
||||
|
||||
// Should not cause errors
|
||||
expect(editBtn).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should debounce button actions', () => {
|
||||
const saveBtn = document.querySelector('.save-all-btn');
|
||||
let clickCount = 0;
|
||||
|
||||
const debouncedHandler = jest.fn(() => {
|
||||
clickCount++;
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', debouncedHandler);
|
||||
|
||||
// Simulate multiple quick clicks
|
||||
saveBtn.click();
|
||||
saveBtn.click();
|
||||
saveBtn.click();
|
||||
|
||||
expect(debouncedHandler).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Button state management', () => {
|
||||
test('should disable buttons during processing', () => {
|
||||
const acceptBtn = document.querySelector('.accept-btn');
|
||||
|
||||
// Simulate processing state
|
||||
acceptBtn.disabled = true;
|
||||
|
||||
expect(acceptBtn.disabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should show loading state for async operations', () => {
|
||||
const saveBtn = document.querySelector('.save-all-btn');
|
||||
|
||||
// Simulate loading state
|
||||
const originalText = saveBtn.textContent;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
expect(saveBtn.textContent).toBe('Saving...');
|
||||
expect(saveBtn.disabled).toBe(true);
|
||||
|
||||
// Restore state
|
||||
saveBtn.textContent = originalText;
|
||||
saveBtn.disabled = false;
|
||||
|
||||
expect(saveBtn.textContent).toBe('Save All');
|
||||
expect(saveBtn.disabled).toBe(false);
|
||||
});
|
||||
|
||||
test('should maintain button visibility states', () => {
|
||||
const buttons = {
|
||||
edit: document.querySelector('.edit-btn'),
|
||||
accept: document.querySelector('.accept-btn'),
|
||||
cancel: document.querySelector('.cancel-btn')
|
||||
};
|
||||
|
||||
// Default state: edit visible, accept/cancel hidden
|
||||
expect(buttons.edit.style.display).not.toBe('none');
|
||||
expect(buttons.accept.style.display).toBe('none');
|
||||
expect(buttons.cancel.style.display).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DOM event handling', () => {
|
||||
test('should handle click events correctly', () => {
|
||||
const addSectionBtn = document.querySelector('.add-section-btn');
|
||||
let clicked = false;
|
||||
|
||||
addSectionBtn.addEventListener('click', () => {
|
||||
clicked = true;
|
||||
});
|
||||
|
||||
addSectionBtn.click();
|
||||
|
||||
expect(clicked).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle keyboard events for accessibility', () => {
|
||||
const editBtn = document.querySelector('.edit-btn');
|
||||
let keyPressed = false;
|
||||
|
||||
editBtn.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
keyPressed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Simulate Enter key press
|
||||
const enterEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
editBtn.dispatchEvent(enterEvent);
|
||||
|
||||
expect(keyPressed).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle focus and blur events', () => {
|
||||
const editBtn = document.querySelector('.edit-btn');
|
||||
let focused = false;
|
||||
let blurred = false;
|
||||
|
||||
editBtn.addEventListener('focus', () => {
|
||||
focused = true;
|
||||
});
|
||||
|
||||
editBtn.addEventListener('blur', () => {
|
||||
blurred = true;
|
||||
});
|
||||
|
||||
editBtn.focus();
|
||||
expect(focused).toBe(true);
|
||||
|
||||
editBtn.blur();
|
||||
expect(blurred).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Button positioning and layout', () => {
|
||||
test('should position floating controls correctly', () => {
|
||||
const floatingControls = document.querySelector('.floating-controls');
|
||||
|
||||
// Test positioning properties
|
||||
floatingControls.style.position = 'fixed';
|
||||
floatingControls.style.top = '20px';
|
||||
floatingControls.style.right = '20px';
|
||||
|
||||
expect(floatingControls.style.position).toBe('fixed');
|
||||
expect(floatingControls.style.top).toBe('20px');
|
||||
expect(floatingControls.style.right).toBe('20px');
|
||||
});
|
||||
|
||||
test('should handle responsive button layouts', () => {
|
||||
const sectionControls = document.querySelector('.section-controls');
|
||||
|
||||
// Test responsive classes
|
||||
sectionControls.classList.add('responsive-controls');
|
||||
|
||||
expect(sectionControls.classList.contains('responsive-controls')).toBe(true);
|
||||
});
|
||||
|
||||
test('should maintain button alignment in sections', () => {
|
||||
const controls = document.querySelector('.section-controls');
|
||||
const buttons = controls.querySelectorAll('button');
|
||||
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
|
||||
// All buttons should be in the same container
|
||||
buttons.forEach(button => {
|
||||
expect(button.parentElement).toBe(controls);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Button confirmation dialogs', () => {
|
||||
test('should show confirmation for destructive actions', () => {
|
||||
const deleteBtn = document.querySelector('.delete-btn');
|
||||
|
||||
// Mock confirm dialog
|
||||
window.confirm = jest.fn(() => false);
|
||||
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
if (window.confirm('Are you sure you want to delete this section?')) {
|
||||
// Perform deletion
|
||||
}
|
||||
});
|
||||
|
||||
deleteBtn.click();
|
||||
|
||||
// Should show confirmation
|
||||
expect(window.confirm).toHaveBeenCalledWith('Are you sure you want to delete this section?');
|
||||
});
|
||||
|
||||
test('should cancel action when confirmation is denied', () => {
|
||||
const deleteBtn = document.querySelector('.delete-btn');
|
||||
let deleted = false;
|
||||
|
||||
window.confirm = jest.fn(() => false);
|
||||
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
if (window.confirm('Are you sure?')) {
|
||||
deleted = true;
|
||||
}
|
||||
});
|
||||
|
||||
deleteBtn.click();
|
||||
|
||||
expect(deleted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DocumentControls integration', () => {
|
||||
test('should integrate with DocumentControls class', () => {
|
||||
if (documentControls) {
|
||||
expect(typeof documentControls.create).toBe('function');
|
||||
expect(typeof documentControls.addButton).toBe('function');
|
||||
expect(typeof documentControls.setEventHandlers).toBe('function');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle button events through DocumentControls', () => {
|
||||
if (!documentControls) return;
|
||||
|
||||
// Test that DocumentControls can manage event handlers
|
||||
expect(documentControls.eventHandlers).toBeDefined();
|
||||
expect(documentControls.eventHandlers instanceof Map).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle button actions through event delegation', () => {
|
||||
const content = document.getElementById('content');
|
||||
let actionTriggered = '';
|
||||
|
||||
content.addEventListener('click', (event) => {
|
||||
if (event.target.matches('button[data-action]')) {
|
||||
actionTriggered = event.target.getAttribute('data-action');
|
||||
}
|
||||
});
|
||||
|
||||
const editBtn = document.querySelector('.edit-btn');
|
||||
editBtn.click();
|
||||
|
||||
expect(actionTriggered).toBe('edit');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Component Integration Tests (Jest Version)
|
||||
*
|
||||
* Tests that extracted components work together properly.
|
||||
* Verifies the complete workflow: Section Creation → Rendering → Editing → Saving
|
||||
*/
|
||||
|
||||
describe('Component Integration Tests', () => {
|
||||
let SectionManager, Section, DOMRenderer, FloatingMenu, EditState;
|
||||
let sectionManager, domRenderer, container;
|
||||
|
||||
beforeAll(() => {
|
||||
// Load extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
|
||||
SectionManager = sectionModule.SectionManager;
|
||||
Section = sectionModule.Section;
|
||||
DOMRenderer = domModule.DOMRenderer;
|
||||
FloatingMenu = domModule.FloatingMenu;
|
||||
EditState = sectionModule.EditState;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup fresh container and components for each test
|
||||
container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
sectionManager = new SectionManager();
|
||||
domRenderer = new DOMRenderer(sectionManager, container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Cleanup
|
||||
if (container && container.parentNode) {
|
||||
container.parentNode.removeChild(container);
|
||||
}
|
||||
});
|
||||
|
||||
test('should load all extracted components', () => {
|
||||
expect(SectionManager).toBeTruthy();
|
||||
expect(Section).toBeTruthy();
|
||||
expect(DOMRenderer).toBeTruthy();
|
||||
expect(FloatingMenu).toBeTruthy();
|
||||
expect(EditState).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should support complete section creation workflow', () => {
|
||||
// Test basic functionality without complex DOM manipulation
|
||||
expect(sectionManager).toBeInstanceOf(SectionManager);
|
||||
expect(domRenderer).toBeInstanceOf(DOMRenderer);
|
||||
|
||||
// Test section creation from markdown
|
||||
const testMarkdown = `# Test Header
|
||||
|
||||
This is test content.
|
||||
|
||||
`;
|
||||
|
||||
// Create sections from markdown (the right method)
|
||||
expect(() => {
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
expect(sections.length).toBeGreaterThan(0);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should have core DOM rendering methods', () => {
|
||||
expect(typeof domRenderer.renderAllSections).toBe('function');
|
||||
expect(typeof domRenderer.showEditor).toBe('function');
|
||||
expect(typeof domRenderer.findSectionElement).toBe('function');
|
||||
});
|
||||
|
||||
test('should preserve editor showing functionality', () => {
|
||||
const mockSection = {
|
||||
id: 'test-section-001',
|
||||
type: 'header',
|
||||
content: 'Test content'
|
||||
};
|
||||
|
||||
// Test basic editor functionality
|
||||
expect(() => {
|
||||
domRenderer.showEditor(mockSection.id);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,280 +0,0 @@
|
||||
/**
|
||||
* Image Editing Functionality Tests
|
||||
*
|
||||
* Tests image editing, positioning, and reset functionality
|
||||
* Based on functionality from history/javascript-dev-tests/test_*image*.js files
|
||||
*/
|
||||
|
||||
describe('Image Editing', () => {
|
||||
let mockImageSection;
|
||||
let mockImageElement;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup DOM with image section
|
||||
document.body.innerHTML = `
|
||||
<div id="content">
|
||||
<div class="section image-section" data-section-id="image-section-1">
|
||||
<div class="section-content">
|
||||
<img src="test-image.jpg" alt="Test image" class="section-image">
|
||||
<div class="image-controls">
|
||||
<button class="edit-image-btn">Edit Image</button>
|
||||
<button class="reset-image-btn">Reset</button>
|
||||
</div>
|
||||
<div class="image-editor-dialog" style="display: none;">
|
||||
<textarea class="alt-text-input" placeholder="Alt text"></textarea>
|
||||
<input type="text" class="image-caption" placeholder="Caption">
|
||||
<button class="apply-image-changes">Apply</button>
|
||||
<button class="cancel-image-changes">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
mockImageSection = document.querySelector('.image-section');
|
||||
mockImageElement = document.querySelector('.section-image');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Image editor dialog', () => {
|
||||
test('should show image editor when edit button is clicked', () => {
|
||||
const editButton = document.querySelector('.edit-image-btn');
|
||||
const dialog = document.querySelector('.image-editor-dialog');
|
||||
|
||||
expect(editButton).toBeTruthy();
|
||||
expect(dialog).toBeTruthy();
|
||||
|
||||
// Simulate edit button click
|
||||
editButton.click();
|
||||
|
||||
// In real implementation, dialog should become visible
|
||||
expect(dialog.style.display).toBe('none'); // Initially hidden
|
||||
});
|
||||
|
||||
test('should populate current alt text and caption', () => {
|
||||
const altTextInput = document.querySelector('.alt-text-input');
|
||||
const captionInput = document.querySelector('.image-caption');
|
||||
|
||||
expect(altTextInput).toBeTruthy();
|
||||
expect(captionInput).toBeTruthy();
|
||||
|
||||
// Simulate populating current values
|
||||
const currentAlt = mockImageElement.alt;
|
||||
altTextInput.value = currentAlt;
|
||||
|
||||
expect(altTextInput.value).toBe(currentAlt);
|
||||
});
|
||||
|
||||
test('should handle dialog positioning correctly', () => {
|
||||
const dialog = document.querySelector('.image-editor-dialog');
|
||||
|
||||
// Test that dialog positioning can be set
|
||||
dialog.style.position = 'absolute';
|
||||
dialog.style.top = '100px';
|
||||
dialog.style.left = '100px';
|
||||
|
||||
expect(dialog.style.position).toBe('absolute');
|
||||
expect(dialog.style.top).toBe('100px');
|
||||
expect(dialog.style.left).toBe('100px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Image modifications', () => {
|
||||
test('should update alt text when applied', () => {
|
||||
const altTextInput = document.querySelector('.alt-text-input');
|
||||
const applyButton = document.querySelector('.apply-image-changes');
|
||||
|
||||
const newAltText = 'Updated alt text for image';
|
||||
altTextInput.value = newAltText;
|
||||
|
||||
// Simulate apply action
|
||||
applyButton.click();
|
||||
|
||||
// In real implementation, image alt text should be updated
|
||||
expect(altTextInput.value).toBe(newAltText);
|
||||
});
|
||||
|
||||
test('should update image caption when applied', () => {
|
||||
const captionInput = document.querySelector('.image-caption');
|
||||
const newCaption = 'Updated image caption';
|
||||
|
||||
captionInput.value = newCaption;
|
||||
|
||||
expect(captionInput.value).toBe(newCaption);
|
||||
});
|
||||
|
||||
test('should validate required fields', () => {
|
||||
const altTextInput = document.querySelector('.alt-text-input');
|
||||
|
||||
// Test empty alt text validation
|
||||
altTextInput.value = '';
|
||||
|
||||
const isEmpty = altTextInput.value.trim() === '';
|
||||
expect(isEmpty).toBe(true);
|
||||
|
||||
// Test filled alt text
|
||||
altTextInput.value = 'Valid alt text';
|
||||
const isFilled = altTextInput.value.trim() !== '';
|
||||
expect(isFilled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Image reset functionality', () => {
|
||||
test('should reset image to original state', () => {
|
||||
const resetButton = document.querySelector('.reset-image-btn');
|
||||
const altTextInput = document.querySelector('.alt-text-input');
|
||||
|
||||
// Store original values
|
||||
const originalAlt = mockImageElement.alt;
|
||||
|
||||
// Modify values
|
||||
altTextInput.value = 'Modified alt text';
|
||||
mockImageElement.alt = 'Modified alt';
|
||||
|
||||
// Simulate reset
|
||||
resetButton.click();
|
||||
|
||||
// In real implementation, should restore original values
|
||||
expect(resetButton).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should confirm before resetting changes', () => {
|
||||
const resetButton = document.querySelector('.reset-image-btn');
|
||||
|
||||
// Mock confirm dialog
|
||||
window.confirm = jest.fn(() => true);
|
||||
|
||||
resetButton.click();
|
||||
|
||||
// In real implementation, should show confirmation
|
||||
expect(resetButton).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should preserve original image data', () => {
|
||||
// Test that original image data is stored
|
||||
const originalData = {
|
||||
src: mockImageElement.src,
|
||||
alt: mockImageElement.alt,
|
||||
caption: ''
|
||||
};
|
||||
|
||||
expect(originalData.src).toBeTruthy();
|
||||
expect(typeof originalData.alt).toBe('string');
|
||||
expect(typeof originalData.caption).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Image editor UI controls', () => {
|
||||
test('should handle cancel button correctly', () => {
|
||||
const cancelButton = document.querySelector('.cancel-image-changes');
|
||||
const dialog = document.querySelector('.image-editor-dialog');
|
||||
|
||||
cancelButton.click();
|
||||
|
||||
// In real implementation, should close dialog without saving
|
||||
expect(cancelButton).toBeTruthy();
|
||||
expect(dialog).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should close dialog after applying changes', () => {
|
||||
const applyButton = document.querySelector('.apply-image-changes');
|
||||
const dialog = document.querySelector('.image-editor-dialog');
|
||||
|
||||
applyButton.click();
|
||||
|
||||
// In real implementation, should close dialog after applying
|
||||
expect(applyButton).toBeTruthy();
|
||||
expect(dialog.style.display).toBe('none');
|
||||
});
|
||||
|
||||
test('should handle escape key to cancel', () => {
|
||||
const dialog = document.querySelector('.image-editor-dialog');
|
||||
const altTextInput = document.querySelector('.alt-text-input');
|
||||
|
||||
// Simulate escape key press
|
||||
const escapeEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
altTextInput.dispatchEvent(escapeEvent);
|
||||
|
||||
// In real implementation, should close dialog
|
||||
expect(dialog).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Advanced image editor features', () => {
|
||||
test('should support image URL editing', () => {
|
||||
const imageUrl = mockImageElement.src;
|
||||
|
||||
// Test URL validation
|
||||
const isValidUrl = /^https?:\/\//.test(imageUrl) || imageUrl.startsWith('/') || imageUrl.startsWith('./');
|
||||
|
||||
// Local files and URLs should be valid
|
||||
expect(typeof imageUrl).toBe('string');
|
||||
});
|
||||
|
||||
test('should handle image loading errors', () => {
|
||||
const errorHandler = jest.fn();
|
||||
|
||||
mockImageElement.onerror = errorHandler;
|
||||
mockImageElement.src = 'invalid-image-url.jpg';
|
||||
|
||||
// In real implementation, should handle image load errors
|
||||
expect(mockImageElement.onerror).toBe(errorHandler);
|
||||
});
|
||||
|
||||
test('should support image alignment options', () => {
|
||||
const alignmentOptions = ['left', 'center', 'right', 'full-width'];
|
||||
|
||||
alignmentOptions.forEach(alignment => {
|
||||
mockImageElement.className = `section-image align-${alignment}`;
|
||||
expect(mockImageElement.className).toContain(`align-${alignment}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle responsive image sizing', () => {
|
||||
// Test responsive image attributes
|
||||
mockImageElement.style.maxWidth = '100%';
|
||||
mockImageElement.style.height = 'auto';
|
||||
|
||||
expect(mockImageElement.style.maxWidth).toBe('100%');
|
||||
expect(mockImageElement.style.height).toBe('auto');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Image section integration', () => {
|
||||
test('should maintain section integrity during image editing', () => {
|
||||
const sectionId = mockImageSection.getAttribute('data-section-id');
|
||||
|
||||
expect(sectionId).toBeTruthy();
|
||||
expect(mockImageSection.classList.contains('image-section')).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle multiple images in one section', () => {
|
||||
// Add another image to the section
|
||||
const secondImage = document.createElement('img');
|
||||
secondImage.src = 'second-image.jpg';
|
||||
secondImage.alt = 'Second image';
|
||||
secondImage.className = 'section-image';
|
||||
|
||||
mockImageSection.querySelector('.section-content').appendChild(secondImage);
|
||||
|
||||
const images = mockImageSection.querySelectorAll('.section-image');
|
||||
expect(images.length).toBe(2);
|
||||
});
|
||||
|
||||
test('should preserve section order when editing images', () => {
|
||||
const sectionContent = mockImageSection.querySelector('.section-content');
|
||||
const children = Array.from(sectionContent.children);
|
||||
|
||||
const imageIndex = children.findIndex(child => child.tagName === 'IMG');
|
||||
expect(imageIndex).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Jest Setup File for JavaScript UI Tests
|
||||
*
|
||||
* Sets up environment and global utilities for testing.
|
||||
* Jest with jsdom environment already provides DOM globals.
|
||||
*/
|
||||
|
||||
// Add TextEncoder/TextDecoder polyfills for Node.js compatibility
|
||||
const { TextEncoder, TextDecoder } = require('util');
|
||||
global.TextEncoder = TextEncoder;
|
||||
global.TextDecoder = TextDecoder;
|
||||
|
||||
// Mock console methods to reduce noise in tests
|
||||
const originalLog = console.log;
|
||||
console.log = (...args) => {
|
||||
// Only log if DEBUG_TESTS environment variable is set
|
||||
if (process.env.DEBUG_TESTS) {
|
||||
originalLog(...args);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup DOM fixtures after page load
|
||||
beforeEach(() => {
|
||||
// Reset document body for each test
|
||||
document.body.innerHTML = '<div id="content"></div>';
|
||||
});
|
||||
@@ -1,219 +0,0 @@
|
||||
/**
|
||||
* Keyboard Shortcuts Functionality Tests
|
||||
*
|
||||
* Tests keyboard shortcuts for section editing (Ctrl+Enter, Escape, etc.)
|
||||
* Based on functionality from history/javascript-dev-tests/test_keyboard_shortcuts.js
|
||||
*/
|
||||
|
||||
describe('Keyboard Shortcuts', () => {
|
||||
let domRenderer;
|
||||
let mockTextarea;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup DOM
|
||||
document.body.innerHTML = `
|
||||
<div id="content">
|
||||
<div class="section" data-section-id="test-section">
|
||||
<textarea class="edit-textarea">Test content</textarea>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Load components
|
||||
require('../components/dom-renderer.js');
|
||||
require('../core/section-manager.js');
|
||||
|
||||
// Mock SectionManager with event system
|
||||
const mockSectionManager = {
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
handleSectionSplit: jest.fn(),
|
||||
sections: []
|
||||
};
|
||||
|
||||
if (global.DOMRenderer) {
|
||||
// Create DOMRenderer with mocked dependencies
|
||||
try {
|
||||
domRenderer = new global.DOMRenderer(mockSectionManager, document.getElementById('content'));
|
||||
} catch (error) {
|
||||
// If constructor fails, create a mock with the methods we need
|
||||
domRenderer = {
|
||||
applyChanges: jest.fn(),
|
||||
cancelEdit: jest.fn()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
mockTextarea = document.querySelector('.edit-textarea');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Ctrl+Enter shortcut (Accept Changes)', () => {
|
||||
test('should apply changes when Ctrl+Enter is pressed', () => {
|
||||
if (!mockTextarea) {
|
||||
console.warn('Textarea not available, skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
// Test that Ctrl+Enter event can be dispatched
|
||||
const ctrlEnterEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
ctrlKey: true,
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
let eventFired = false;
|
||||
mockTextarea.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
eventFired = true;
|
||||
}
|
||||
});
|
||||
|
||||
mockTextarea.dispatchEvent(ctrlEnterEvent);
|
||||
|
||||
// Verify event was handled
|
||||
expect(eventFired).toBe(true);
|
||||
});
|
||||
|
||||
test('should prevent default behavior on Ctrl+Enter', () => {
|
||||
if (!mockTextarea) return;
|
||||
|
||||
const preventDefault = jest.fn();
|
||||
const ctrlEnterEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
ctrlKey: true,
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
// Mock preventDefault
|
||||
ctrlEnterEvent.preventDefault = preventDefault;
|
||||
|
||||
mockTextarea.dispatchEvent(ctrlEnterEvent);
|
||||
|
||||
// Note: In real implementation, preventDefault should be called
|
||||
// This test documents the expected behavior
|
||||
expect(true).toBe(true); // Placeholder for actual implementation check
|
||||
});
|
||||
});
|
||||
|
||||
describe('Escape shortcut (Cancel Changes)', () => {
|
||||
test('should cancel changes when Escape is pressed', () => {
|
||||
if (!mockTextarea) {
|
||||
console.warn('Textarea not available, skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
// Test that Escape event can be dispatched
|
||||
const escapeEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
let escapePressed = false;
|
||||
mockTextarea.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
escapePressed = true;
|
||||
}
|
||||
});
|
||||
|
||||
mockTextarea.dispatchEvent(escapeEvent);
|
||||
|
||||
// Verify escape was detected
|
||||
expect(escapePressed).toBe(true);
|
||||
});
|
||||
|
||||
test('should restore original content on Escape', () => {
|
||||
if (!mockTextarea) return;
|
||||
|
||||
const originalContent = 'Original content';
|
||||
mockTextarea.setAttribute('data-original-content', originalContent);
|
||||
mockTextarea.value = 'Modified content';
|
||||
|
||||
const escapeEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
mockTextarea.dispatchEvent(escapeEvent);
|
||||
|
||||
// In real implementation, content should be restored
|
||||
// This test documents the expected behavior
|
||||
expect(mockTextarea.getAttribute('data-original-content')).toBe(originalContent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keyboard shortcuts integration', () => {
|
||||
test('should bind keyboard handlers to textareas', () => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'edit-textarea';
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
// Check if event listeners can be added (integration test)
|
||||
let listenerAdded = false;
|
||||
const originalAddEventListener = textarea.addEventListener;
|
||||
textarea.addEventListener = jest.fn((event, handler) => {
|
||||
if (event === 'keydown') {
|
||||
listenerAdded = true;
|
||||
}
|
||||
return originalAddEventListener.call(textarea, event, handler);
|
||||
});
|
||||
|
||||
// In real implementation, DOMRenderer should bind keydown listeners
|
||||
// This test ensures the capability exists
|
||||
expect(textarea.addEventListener).toBeDefined();
|
||||
expect(typeof textarea.addEventListener).toBe('function');
|
||||
});
|
||||
|
||||
test('should handle multiple keyboard events correctly', () => {
|
||||
if (!mockTextarea) return;
|
||||
|
||||
const events = [
|
||||
{ key: 'Enter', ctrlKey: true },
|
||||
{ key: 'Escape', ctrlKey: false },
|
||||
{ key: 'Tab', ctrlKey: false }
|
||||
];
|
||||
|
||||
events.forEach(eventData => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
...eventData,
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
// Should not throw errors when handling various key events
|
||||
expect(() => {
|
||||
mockTextarea.dispatchEvent(event);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keyboard shortcuts accessibility', () => {
|
||||
test('should provide keyboard alternatives to mouse actions', () => {
|
||||
// This test ensures keyboard accessibility is maintained
|
||||
const shortcuts = [
|
||||
{ key: 'Enter', ctrlKey: true, action: 'apply' },
|
||||
{ key: 'Escape', ctrlKey: false, action: 'cancel' }
|
||||
];
|
||||
|
||||
shortcuts.forEach(shortcut => {
|
||||
expect(shortcut.key).toBeDefined();
|
||||
expect(shortcut.action).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('should work with screen readers and assistive technology', () => {
|
||||
if (!mockTextarea) return;
|
||||
|
||||
// Test ARIA attributes and accessibility features
|
||||
mockTextarea.setAttribute('aria-label', 'Edit section content');
|
||||
mockTextarea.setAttribute('role', 'textbox');
|
||||
|
||||
expect(mockTextarea.getAttribute('aria-label')).toBeTruthy();
|
||||
expect(mockTextarea.getAttribute('role')).toBe('textbox');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,216 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test Runner for JavaScript Refactoring
|
||||
*
|
||||
* Drives component extraction and testing during architecture refactoring.
|
||||
* Ensures all functionality remains stable while achieving separation of concerns.
|
||||
*/
|
||||
|
||||
class RefactorTestRunner {
|
||||
constructor() {
|
||||
this.tests = [];
|
||||
this.passed = 0;
|
||||
this.failed = 0;
|
||||
this.currentSuite = null;
|
||||
this.setupDOM();
|
||||
}
|
||||
|
||||
setupDOM() {
|
||||
// Set up minimal DOM environment for testing
|
||||
if (typeof document === 'undefined') {
|
||||
const { JSDOM } = require('jsdom');
|
||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
||||
url: 'http://localhost',
|
||||
pretendToBeVisual: true,
|
||||
resources: 'usable'
|
||||
});
|
||||
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
global.HTMLElement = dom.window.HTMLElement;
|
||||
global.Event = dom.window.Event;
|
||||
global.CustomEvent = dom.window.CustomEvent;
|
||||
|
||||
// Only set navigator if it doesn't exist
|
||||
if (typeof global.navigator === 'undefined') {
|
||||
global.navigator = dom.window.navigator;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe(suiteName, fn) {
|
||||
console.log(`\n📁 ${suiteName}`);
|
||||
this.currentSuite = suiteName;
|
||||
fn();
|
||||
this.currentSuite = null;
|
||||
}
|
||||
|
||||
it(testName, fn) {
|
||||
const fullName = this.currentSuite ? `${this.currentSuite}: ${testName}` : testName;
|
||||
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✅ ${testName}`);
|
||||
this.passed++;
|
||||
} catch (error) {
|
||||
console.log(` ❌ ${testName}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
if (error.stack) {
|
||||
console.log(` Stack: ${error.stack.split('\n')[1]?.trim()}`);
|
||||
}
|
||||
this.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
expect(actual) {
|
||||
return {
|
||||
toBe: (expected) => {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
},
|
||||
toBeTruthy: () => {
|
||||
if (!actual) {
|
||||
throw new Error(`Expected truthy value, got ${actual}`);
|
||||
}
|
||||
},
|
||||
toBeFalsy: () => {
|
||||
if (actual) {
|
||||
throw new Error(`Expected falsy value, got ${actual}`);
|
||||
}
|
||||
},
|
||||
toEqual: (expected) => {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
||||
}
|
||||
},
|
||||
toContain: (expected) => {
|
||||
if (!actual.includes(expected)) {
|
||||
throw new Error(`Expected ${actual} to contain ${expected}`);
|
||||
}
|
||||
},
|
||||
toHaveProperty: (property) => {
|
||||
if (!(property in actual)) {
|
||||
throw new Error(`Expected object to have property ${property}`);
|
||||
}
|
||||
},
|
||||
toBeInstanceOf: (expectedClass) => {
|
||||
if (!(actual instanceof expectedClass)) {
|
||||
throw new Error(`Expected instance of ${expectedClass.name}, got ${actual.constructor.name}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a component can be extracted from the monolith without breaking functionality
|
||||
*/
|
||||
testComponentExtraction(componentName, extractFn, originalTests) {
|
||||
this.describe(`Component Extraction: ${componentName}`, () => {
|
||||
this.it('should extract without syntax errors', () => {
|
||||
try {
|
||||
const component = extractFn();
|
||||
this.expect(component).toBeTruthy();
|
||||
} catch (error) {
|
||||
throw new Error(`Component extraction failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.it('should maintain original API', () => {
|
||||
const component = extractFn();
|
||||
originalTests.forEach(test => {
|
||||
try {
|
||||
test(component);
|
||||
} catch (error) {
|
||||
throw new Error(`API compatibility test failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test component integration after extraction
|
||||
*/
|
||||
testComponentIntegration(components, integrationTests) {
|
||||
this.describe('Component Integration', () => {
|
||||
integrationTests.forEach((test, index) => {
|
||||
this.it(`integration test ${index + 1}`, () => {
|
||||
test(components);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup test environment with mock dependencies
|
||||
*/
|
||||
setupTestEnvironment() {
|
||||
// Create test container
|
||||
const container = document.createElement('div');
|
||||
container.id = 'test-container';
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Mock any global dependencies
|
||||
global.mockSectionManager = {
|
||||
sections: new Map(),
|
||||
createSectionsFromMarkdown: () => [],
|
||||
startEditing: () => true,
|
||||
stopEditing: () => true,
|
||||
getAllSections: () => []
|
||||
};
|
||||
|
||||
return { container };
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup test environment
|
||||
*/
|
||||
cleanupTestEnvironment() {
|
||||
const container = document.getElementById('test-container');
|
||||
if (container) {
|
||||
container.remove();
|
||||
}
|
||||
|
||||
// Clear any global mocks
|
||||
delete global.mockSectionManager;
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log('🧪 TDD Refactoring Test Runner Starting...\n');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Run all collected tests
|
||||
// Tests will be added by importing component test files
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
console.log(`\n📊 Test Results:`);
|
||||
console.log(` ✅ Passed: ${this.passed}`);
|
||||
console.log(` ❌ Failed: ${this.failed}`);
|
||||
console.log(` ⏱️ Duration: ${duration}ms`);
|
||||
|
||||
if (this.failed > 0) {
|
||||
console.log(`\n❌ ${this.failed} test(s) failed. Refactoring should not proceed.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`\n✅ All tests passed! Refactoring is safe to continue.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in component tests
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { RefactorTestRunner };
|
||||
}
|
||||
|
||||
// Export for browser use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.RefactorTestRunner = RefactorTestRunner;
|
||||
}
|
||||
|
||||
module.exports = RefactorTestRunner;
|
||||
@@ -1,267 +0,0 @@
|
||||
/**
|
||||
* Section Splitting Functionality Tests
|
||||
*
|
||||
* Tests dynamic section splitting when headings are detected
|
||||
* Based on functionality from history/javascript-dev-tests/test_section_splitting.js
|
||||
*/
|
||||
|
||||
describe('Section Splitting', () => {
|
||||
let sectionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup DOM
|
||||
document.body.innerHTML = `
|
||||
<div id="content">
|
||||
<div class="section" data-section-id="main-section">
|
||||
<div class="section-content">
|
||||
<p>Original content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Load components
|
||||
require('../core/section-manager.js');
|
||||
|
||||
if (global.SectionManager) {
|
||||
sectionManager = new global.SectionManager();
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Heading detection', () => {
|
||||
test('should detect new headings in content', () => {
|
||||
const textWithHeading = `
|
||||
This is some content.
|
||||
|
||||
# New Heading
|
||||
|
||||
This should be a new section.
|
||||
`;
|
||||
|
||||
// Test heading detection with regex
|
||||
const lines = textWithHeading.trim().split('\n');
|
||||
const headingLine = lines.find(line => /^#+ /.test(line.trim()));
|
||||
expect(headingLine).toBeTruthy();
|
||||
expect(headingLine.trim()).toBe('# New Heading');
|
||||
});
|
||||
|
||||
test('should identify different heading levels', () => {
|
||||
const headingTests = [
|
||||
{ text: '# Heading 1', level: 1 },
|
||||
{ text: '## Heading 2', level: 2 },
|
||||
{ text: '### Heading 3', level: 3 },
|
||||
{ text: '#### Heading 4', level: 4 }
|
||||
];
|
||||
|
||||
headingTests.forEach(({ text, level }) => {
|
||||
const match = text.match(/^(#+) /);
|
||||
expect(match).toBeTruthy();
|
||||
if (match) {
|
||||
expect(match[1].length).toBe(level);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should distinguish headings from regular text', () => {
|
||||
const testCases = [
|
||||
{ text: '# This is a heading', isHeading: true },
|
||||
{ text: 'This is not a heading', isHeading: false },
|
||||
{ text: 'Neither is this # hash in middle', isHeading: false },
|
||||
{ text: '## Another heading', isHeading: true }
|
||||
];
|
||||
|
||||
testCases.forEach(({ text, isHeading }) => {
|
||||
const match = /^#+\s/.test(text.trim());
|
||||
expect(match).toBe(isHeading);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Section splitting logic', () => {
|
||||
test('should split content when heading is detected', () => {
|
||||
const originalContent = 'Original content without headings';
|
||||
const newContent = `
|
||||
${originalContent}
|
||||
|
||||
# New Section
|
||||
|
||||
New section content
|
||||
`;
|
||||
|
||||
// Simulate section splitting logic
|
||||
const parts = newContent.split(/\n(?=#)/);
|
||||
|
||||
if (parts.length > 1) {
|
||||
expect(parts.length).toBeGreaterThan(1);
|
||||
expect(parts[0]).toContain('Original content');
|
||||
expect(parts[1]).toContain('# New Section');
|
||||
}
|
||||
});
|
||||
|
||||
test('should preserve content when no headings are present', () => {
|
||||
const content = 'Just regular content without any headings';
|
||||
const parts = content.split(/\n(?=#)/);
|
||||
|
||||
expect(parts.length).toBe(1);
|
||||
expect(parts[0]).toBe(content);
|
||||
});
|
||||
|
||||
test('should handle multiple headings correctly', () => {
|
||||
const contentWithMultipleHeadings = `Initial content
|
||||
|
||||
# First Heading
|
||||
First section content
|
||||
|
||||
## Second Heading
|
||||
Second section content
|
||||
|
||||
# Third Heading
|
||||
Third section content`;
|
||||
|
||||
// Split on lines that start with headings
|
||||
const parts = contentWithMultipleHeadings.split(/\n(?=#)/);
|
||||
|
||||
// Should split into multiple sections
|
||||
expect(parts.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Find heading lines
|
||||
const headings = contentWithMultipleHeadings.match(/^#+.*$/gm);
|
||||
expect(headings).toBeTruthy();
|
||||
expect(headings.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SectionManager integration', () => {
|
||||
test('should have handleSectionSplit method', () => {
|
||||
if (!sectionManager) {
|
||||
console.warn('SectionManager not available, skipping test');
|
||||
return;
|
||||
}
|
||||
|
||||
expect(typeof sectionManager.handleSectionSplit).toBe('function');
|
||||
});
|
||||
|
||||
test('should maintain section state during splits', () => {
|
||||
if (!sectionManager) return;
|
||||
|
||||
const originalSectionCount = document.querySelectorAll('.section').length;
|
||||
|
||||
// Mock section splitting
|
||||
const mockNewSection = document.createElement('div');
|
||||
mockNewSection.className = 'section';
|
||||
mockNewSection.setAttribute('data-section-id', 'split-section');
|
||||
|
||||
if (originalSectionCount > 0) {
|
||||
expect(originalSectionCount).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dynamic section creation', () => {
|
||||
test('should create new section elements when splitting', () => {
|
||||
const sectionContent = `
|
||||
Original content
|
||||
|
||||
# New Section Title
|
||||
|
||||
New section content
|
||||
`;
|
||||
|
||||
// Simulate section creation
|
||||
const newSection = document.createElement('div');
|
||||
newSection.className = 'section';
|
||||
newSection.setAttribute('data-section-id', 'generated-section-id');
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'section-content';
|
||||
contentDiv.textContent = 'New section content';
|
||||
|
||||
newSection.appendChild(contentDiv);
|
||||
|
||||
expect(newSection.className).toBe('section');
|
||||
expect(newSection.getAttribute('data-section-id')).toBeTruthy();
|
||||
expect(newSection.querySelector('.section-content')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should generate unique section IDs', () => {
|
||||
const headingText = 'My New Section';
|
||||
|
||||
// Simulate ID generation from heading
|
||||
const sectionId = headingText
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
expect(sectionId).toBe('my-new-section');
|
||||
});
|
||||
|
||||
test('should preserve section hierarchy', () => {
|
||||
const hierarchicalContent = `
|
||||
# Main Section
|
||||
Main content
|
||||
|
||||
## Subsection
|
||||
Sub content
|
||||
|
||||
### Sub-subsection
|
||||
Sub-sub content
|
||||
`;
|
||||
|
||||
const headings = hierarchicalContent.match(/^#+.*$/gm);
|
||||
|
||||
if (headings) {
|
||||
expect(headings.length).toBe(3);
|
||||
expect(headings[0]).toMatch(/^# /);
|
||||
expect(headings[1]).toMatch(/^## /);
|
||||
expect(headings[2]).toMatch(/^### /);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Section splitting edge cases', () => {
|
||||
test('should handle empty headings gracefully', () => {
|
||||
const contentWithEmptyHeading = `
|
||||
Content before
|
||||
|
||||
#
|
||||
|
||||
Content after
|
||||
`;
|
||||
|
||||
const parts = contentWithEmptyHeading.split(/\n(?=#)/);
|
||||
expect(parts.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('should handle headings at the start of content', () => {
|
||||
const contentStartingWithHeading = `# First Heading
|
||||
Content for first section
|
||||
|
||||
# Second Heading
|
||||
Content for second section
|
||||
`;
|
||||
|
||||
const parts = contentStartingWithHeading.split(/\n(?=#)/);
|
||||
expect(parts[0]).toContain('# First Heading');
|
||||
});
|
||||
|
||||
test('should handle malformed headings', () => {
|
||||
const malformedHeadings = [
|
||||
'#NoSpace',
|
||||
'# ',
|
||||
'########## Too many hashes',
|
||||
'Not a heading # at all'
|
||||
];
|
||||
|
||||
malformedHeadings.forEach(text => {
|
||||
const isValidHeading = /^#{1,6}\s+\S/.test(text);
|
||||
// Most should be invalid except properly formatted ones
|
||||
expect(typeof isValidHeading).toBe('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,139 +0,0 @@
|
||||
/**
|
||||
* Jest Test Setup for TestDrive-JSUI
|
||||
*
|
||||
* Sets up the testing environment for JavaScript UI components.
|
||||
* Provides DOM mocking, global utilities, and test helpers.
|
||||
*/
|
||||
|
||||
// Mock DOM globals that might be missing in JSDOM
|
||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock window.matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock local storage
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
};
|
||||
global.localStorage = localStorageMock;
|
||||
|
||||
// Mock session storage
|
||||
const sessionStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
};
|
||||
global.sessionStorage = sessionStorageMock;
|
||||
|
||||
// Global test utilities
|
||||
global.testUtils = {
|
||||
/**
|
||||
* Create a mock DOM element with specified tag and attributes
|
||||
*/
|
||||
createElement: (tag, attributes = {}) => {
|
||||
const element = document.createElement(tag);
|
||||
Object.entries(attributes).forEach(([key, value]) => {
|
||||
element.setAttribute(key, value);
|
||||
});
|
||||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a test markdown content div
|
||||
*/
|
||||
createMarkdownContent: (content = '# Test Content') => {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'markdown-content';
|
||||
div.innerHTML = content;
|
||||
return div;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wait for next tick (useful for async operations)
|
||||
*/
|
||||
nextTick: () => new Promise(resolve => setTimeout(resolve, 0)),
|
||||
|
||||
/**
|
||||
* Simulate user interaction events
|
||||
*/
|
||||
simulateEvent: (element, eventType, eventProperties = {}) => {
|
||||
const event = new Event(eventType, { bubbles: true, ...eventProperties });
|
||||
Object.entries(eventProperties).forEach(([key, value]) => {
|
||||
event[key] = value;
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
return event;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clean up DOM after each test
|
||||
*/
|
||||
cleanupDOM: () => {
|
||||
document.body.innerHTML = '';
|
||||
document.head.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
// Setup and teardown
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset localStorage/sessionStorage
|
||||
localStorageMock.getItem.mockClear();
|
||||
localStorageMock.setItem.mockClear();
|
||||
localStorageMock.removeItem.mockClear();
|
||||
localStorageMock.clear.mockClear();
|
||||
|
||||
sessionStorageMock.getItem.mockClear();
|
||||
sessionStorageMock.setItem.mockClear();
|
||||
sessionStorageMock.removeItem.mockClear();
|
||||
sessionStorageMock.clear.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up DOM
|
||||
global.testUtils.cleanupDOM();
|
||||
|
||||
// Clean up any timers
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// Console helpers for test debugging
|
||||
global.console = {
|
||||
...console,
|
||||
// Keep these methods for test debugging
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
// Mock these to avoid noise in tests
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
};
|
||||
@@ -1,521 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Comprehensive Component Integration Test
|
||||
*
|
||||
* Tests that extracted components work together properly.
|
||||
* Verifies the complete workflow: Section Creation → Rendering → Editing → Saving
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Component Integration Tests', () => {
|
||||
|
||||
runner.it('should load all extracted components', () => {
|
||||
try {
|
||||
// Load extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
|
||||
runner.expect(sectionModule.SectionManager).toBeTruthy();
|
||||
runner.expect(sectionModule.Section).toBeTruthy();
|
||||
runner.expect(domModule.DOMRenderer).toBeTruthy();
|
||||
runner.expect(domModule.FloatingMenu).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = sectionModule.SectionManager;
|
||||
global.ExtractedSection = sectionModule.Section;
|
||||
global.ExtractedDOMRenderer = domModule.DOMRenderer;
|
||||
global.ExtractedFloatingMenu = domModule.FloatingMenu;
|
||||
global.ExtractedEditState = sectionModule.EditState;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted components: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support complete section creation workflow', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Test workflow: Create sections from markdown
|
||||
const testMarkdown = `# Main Heading
|
||||
This is the introduction content.
|
||||
|
||||
## Subheading One
|
||||
Content for first subsection.
|
||||
|
||||

|
||||
|
||||
## Subheading Two
|
||||
Content for second subsection.`;
|
||||
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
|
||||
// Verify sections were created
|
||||
// Expected: heading+paragraph, heading+paragraph, image, heading+paragraph = 4 sections
|
||||
runner.expect(sections.length).toBe(4);
|
||||
runner.expect(sections[0].type).toBe('heading');
|
||||
runner.expect(sections[2].type).toBe('image');
|
||||
|
||||
// Verify DOM rendering
|
||||
domRenderer.renderAllSections(sections);
|
||||
const renderedElements = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(renderedElements.length).toBe(sections.length);
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support complete editing workflow', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const EditState = global.ExtractedEditState;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Test Heading\nOriginal content here.';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const section = sectionManager.sections.get(sectionId);
|
||||
|
||||
// Test workflow: Start editing
|
||||
runner.expect(section.state).toBe(EditState.ORIGINAL);
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
|
||||
const content = sectionManager.startEditing(sectionId);
|
||||
runner.expect(content).toContain('Test Heading');
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
runner.expect(section.state).toBe(EditState.EDITING);
|
||||
|
||||
// Test workflow: Update content
|
||||
const newContent = '# Updated Heading\nModified content here.';
|
||||
sectionManager.updateContent(sectionId, newContent);
|
||||
runner.expect(section.editingMarkdown).toBe(newContent);
|
||||
|
||||
// Test workflow: Accept changes
|
||||
sectionManager.acceptChanges(sectionId);
|
||||
runner.expect(section.currentMarkdown).toBe(newContent);
|
||||
runner.expect(section.state).toBe(EditState.SAVED);
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support accept/cancel button functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Test Heading\nOriginal content here.';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const section = sectionManager.sections.get(sectionId);
|
||||
|
||||
// Start editing to trigger floating menu with buttons
|
||||
sectionManager.startEditing(sectionId);
|
||||
|
||||
// Check if floating menu exists
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
|
||||
runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy();
|
||||
|
||||
// Find buttons in the floating menu
|
||||
const menuElement = domRenderer.currentFloatingMenu.element;
|
||||
runner.expect(menuElement).toBeTruthy();
|
||||
|
||||
const buttons = menuElement.querySelectorAll('button');
|
||||
runner.expect(buttons.length >= 2).toBeTruthy(); // At least Accept and Cancel buttons
|
||||
|
||||
const acceptBtn = Array.from(buttons).find(btn => btn.textContent === 'Accept');
|
||||
const cancelBtn = Array.from(buttons).find(btn => btn.textContent === 'Cancel');
|
||||
|
||||
runner.expect(acceptBtn).toBeTruthy();
|
||||
runner.expect(cancelBtn).toBeTruthy();
|
||||
|
||||
// Test Accept button functionality
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
|
||||
// Simulate updating content and clicking Accept
|
||||
const textarea = menuElement.querySelector('textarea');
|
||||
runner.expect(textarea).toBeTruthy();
|
||||
textarea.value = '# Updated Heading\nUpdated content via button.';
|
||||
|
||||
acceptBtn.click();
|
||||
|
||||
// After clicking Accept, section should be saved and menu hidden
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
runner.expect(section.currentMarkdown).toContain('Updated Heading');
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support cancel button functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Original Heading\nOriginal content here.';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const section = sectionManager.sections.get(sectionId);
|
||||
|
||||
// Start editing
|
||||
sectionManager.startEditing(sectionId);
|
||||
|
||||
// Find buttons in the floating menu
|
||||
const menuElement = domRenderer.currentFloatingMenu.element;
|
||||
const cancelBtn = Array.from(menuElement.querySelectorAll('button')).find(btn => btn.textContent === 'Cancel');
|
||||
|
||||
runner.expect(cancelBtn).toBeTruthy();
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
|
||||
// Simulate changing content but then canceling
|
||||
const textarea = menuElement.querySelector('textarea');
|
||||
textarea.value = '# Changed Heading\nThis should be discarded.';
|
||||
|
||||
cancelBtn.click();
|
||||
|
||||
// After clicking Cancel, section should not be saved and menu hidden
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
runner.expect(section.currentMarkdown).toContain('Original Heading'); // Original content preserved
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support event-driven communication', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Track events
|
||||
let sectionsCreatedEvent = null;
|
||||
let editStartedEvent = null;
|
||||
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
sectionsCreatedEvent = data;
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
editStartedEvent = data;
|
||||
});
|
||||
|
||||
// Test event: sections-created
|
||||
const testMarkdown = '# Test\nContent';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
runner.expect(sectionsCreatedEvent).toBeTruthy();
|
||||
runner.expect(sectionsCreatedEvent.sections).toEqual(sections);
|
||||
runner.expect(sectionsCreatedEvent.count).toBe(1);
|
||||
|
||||
// Test event: edit-started
|
||||
const sectionId = sections[0].id;
|
||||
sectionManager.startEditing(sectionId);
|
||||
|
||||
runner.expect(editStartedEvent).toBeTruthy();
|
||||
runner.expect(editStartedEvent.sectionId).toBe(sectionId);
|
||||
runner.expect(editStartedEvent.content).toContain('Test');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support section type detection and rendering', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const Section = global.ExtractedSection;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Test different section types
|
||||
const testMarkdown = `# Heading Section
|
||||
Regular paragraph content.
|
||||
|
||||

|
||||
|
||||
\`\`\`javascript
|
||||
// Code section
|
||||
console.log('test');
|
||||
\`\`\``;
|
||||
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
|
||||
// Verify type detection - adjusted for actual parsing behavior
|
||||
// Expected: heading+paragraph, image, code = 3 sections
|
||||
runner.expect(sections[0].type).toBe('heading'); // Combined heading+paragraph
|
||||
runner.expect(sections[1].type).toBe('image'); // Image section
|
||||
runner.expect(sections[2].type).toBe('code'); // Code section
|
||||
|
||||
// Verify image detection
|
||||
runner.expect(sections[1].isImage()).toBeTruthy(); // Image is now at index 1
|
||||
runner.expect(sections[0].isImage()).toBeFalsy();
|
||||
|
||||
// Verify rendering handles different types
|
||||
domRenderer.renderAllSections(sections);
|
||||
const renderedElements = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(renderedElements.length).toBe(sections.length);
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support FloatingMenu integration', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const FloatingMenu = global.ExtractedFloatingMenu;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// Test showing editor (which uses FloatingMenu)
|
||||
domRenderer.showEditor(sectionId, 'test content');
|
||||
|
||||
// Verify floating menu state
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
|
||||
runner.expect(domRenderer.currentFloatingMenu.sectionId).toBe(sectionId);
|
||||
runner.expect(domRenderer.currentFloatingMenu.isVisible).toBeTruthy();
|
||||
runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy();
|
||||
|
||||
// Test hiding editor
|
||||
domRenderer.hideCurrentEditor();
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeFalsy();
|
||||
runner.expect(domRenderer.editingSections.has(sectionId)).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support complete click-to-edit workflow', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Create and render sections
|
||||
const testMarkdown = '# Test Heading\nTest content for editing';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const element = domRenderer.findSectionElement(sectionId);
|
||||
|
||||
// Simulate click event
|
||||
const clickEvent = new Event('click', { bubbles: true });
|
||||
Object.defineProperty(clickEvent, 'target', { value: element });
|
||||
|
||||
// Test complete workflow
|
||||
domRenderer.handleSectionClick(clickEvent);
|
||||
|
||||
// Verify editing state was triggered
|
||||
const section = sectionManager.sections.get(sectionId);
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
runner.expect(domRenderer.editingSections.has(sectionId)).toBeTruthy();
|
||||
runner.expect(domRenderer.currentFloatingMenu).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should support document status tracking', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const container = document.createElement('div');
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Test initial status
|
||||
let status = sectionManager.getDocumentStatus();
|
||||
runner.expect(status.totalSections).toBe(0);
|
||||
runner.expect(status.editingSections).toBe(0);
|
||||
|
||||
// Create sections
|
||||
const testMarkdown = '# Section 1\nContent 1\n\n# Section 2\nContent 2';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
status = sectionManager.getDocumentStatus();
|
||||
runner.expect(status.totalSections).toBe(2);
|
||||
runner.expect(status.editingSections).toBe(2); // Bug compatibility (isEditing property exists)
|
||||
|
||||
// Test getAllSections
|
||||
const allSections = sectionManager.getAllSections();
|
||||
runner.expect(allSections.length).toBe(2);
|
||||
runner.expect(allSections[0].currentMarkdown).toContain('Section 1');
|
||||
runner.expect(allSections[1].currentMarkdown).toContain('Section 2');
|
||||
});
|
||||
|
||||
runner.it('should support event tracking and analytics', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Test event tracking
|
||||
domRenderer.trackEvent('test-event', { data: 'test' });
|
||||
domRenderer.trackEvent('section-click', { sectionId: 'test-123' });
|
||||
|
||||
const stats = domRenderer.getEventStats();
|
||||
runner.expect(stats.totalEvents).toBe(1); // Only section-click is tracked in stats
|
||||
runner.expect(stats.stats['section-click']).toBe(1);
|
||||
runner.expect(stats.recentEvents.length).toBe(2);
|
||||
runner.expect(stats.recentEvents[0].type).toBe('test-event');
|
||||
runner.expect(stats.recentEvents[1].type).toBe('section-click');
|
||||
});
|
||||
|
||||
// Integration stress test
|
||||
runner.it('should handle complex document with multiple operations', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Setup
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Complex document
|
||||
const complexMarkdown = `# Document Title
|
||||
Introduction paragraph with some content.
|
||||
|
||||
## Section A
|
||||
Content for section A with details.
|
||||
|
||||

|
||||
|
||||
### Subsection A.1
|
||||
More detailed content here.
|
||||
|
||||
\`\`\`javascript
|
||||
function test() {
|
||||
console.log('code block');
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Section B
|
||||
Final section content.`;
|
||||
|
||||
// Create and render
|
||||
const sections = sectionManager.createSectionsFromMarkdown(complexMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
runner.expect(sections.length).toBe(6); // Adjusted based on actual parsing
|
||||
|
||||
// Test editing multiple sections
|
||||
const firstSection = sections[0];
|
||||
const imageSection = sections.find(s => s.isImage());
|
||||
const codeSection = sections.find(s => s.type === 'code');
|
||||
|
||||
// Edit first section
|
||||
sectionManager.startEditing(firstSection.id);
|
||||
sectionManager.updateContent(firstSection.id, '# Updated Title\nUpdated intro.');
|
||||
sectionManager.acceptChanges(firstSection.id);
|
||||
|
||||
// Edit image section
|
||||
sectionManager.startEditing(imageSection.id);
|
||||
sectionManager.updateContent(imageSection.id, '');
|
||||
sectionManager.acceptChanges(imageSection.id);
|
||||
|
||||
// Verify changes
|
||||
runner.expect(firstSection.currentMarkdown).toContain('Updated Title');
|
||||
runner.expect(imageSection.currentMarkdown).toContain('Updated Image');
|
||||
|
||||
// Verify document reconstruction
|
||||
const finalMarkdown = sectionManager.getDocumentMarkdown();
|
||||
runner.expect(finalMarkdown).toContain('Updated Title');
|
||||
runner.expect(finalMarkdown).toContain('Updated Image');
|
||||
runner.expect(finalMarkdown).toContain('Section B');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Running Component Integration Tests');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Component integration tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for Debug Panel Component Extraction
|
||||
*
|
||||
* Tests the extraction of DebugPanel from the monolithic editor.js
|
||||
* DebugPanel handles debug message display and management.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
// Define expected DebugPanel API
|
||||
const EXPECTED_DEBUGPANEL_API = [
|
||||
'constructor',
|
||||
'toggle',
|
||||
'update',
|
||||
'clear',
|
||||
'addMessage',
|
||||
'show',
|
||||
'hide',
|
||||
'getMessageCount',
|
||||
'getRecentMessages'
|
||||
];
|
||||
|
||||
runner.describe('DebugPanel Component Extraction', () => {
|
||||
|
||||
runner.it('should define expected API methods', () => {
|
||||
const expectedMethods = EXPECTED_DEBUGPANEL_API;
|
||||
runner.expect(expectedMethods.length).toBe(9);
|
||||
runner.expect(expectedMethods).toContain('toggle');
|
||||
runner.expect(expectedMethods).toContain('update');
|
||||
runner.expect(expectedMethods).toContain('addMessage');
|
||||
});
|
||||
|
||||
runner.it('should load extracted DebugPanel component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../components/debug-panel.js')];
|
||||
|
||||
try {
|
||||
const module = require('../components/debug-panel.js');
|
||||
runner.expect(module.DebugPanel).toBeTruthy();
|
||||
|
||||
// Set global for other tests
|
||||
global.ExtractedDebugPanel = module.DebugPanel;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted DebugPanel: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve constructor functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
runner.expect(debugPanel).toBeInstanceOf(DebugPanel);
|
||||
runner.expect(debugPanel.messages).toBeInstanceOf(Array);
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should preserve message handling functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test adding messages
|
||||
debugPanel.addMessage('Test message', 'INFO');
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(1);
|
||||
|
||||
const recentMessages = debugPanel.getRecentMessages(1);
|
||||
runner.expect(recentMessages.length).toBe(1);
|
||||
runner.expect(recentMessages[0].message).toBe('Test message');
|
||||
runner.expect(recentMessages[0].category).toBe('INFO');
|
||||
});
|
||||
|
||||
runner.it('should preserve toggle functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
// Create container element
|
||||
const container = document.createElement('div');
|
||||
container.id = 'debug-messages-container';
|
||||
container.style.display = 'none';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const debugButton = document.createElement('button');
|
||||
debugButton.id = 'toggle-debug';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
document.body.appendChild(debugButton);
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test toggle on
|
||||
debugPanel.toggle();
|
||||
runner.expect(debugPanel.isActive).toBeTruthy();
|
||||
|
||||
// Test toggle off
|
||||
debugPanel.toggle();
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(debugButton);
|
||||
});
|
||||
|
||||
runner.it('should preserve update functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.id = 'debug-messages-container';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const debugButton = document.createElement('button');
|
||||
debugButton.id = 'toggle-debug';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
document.body.appendChild(debugButton);
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
debugPanel.show();
|
||||
|
||||
debugPanel.addMessage('Test message 1', 'INFO');
|
||||
debugPanel.addMessage('Test message 2', 'ERROR');
|
||||
debugPanel.update();
|
||||
|
||||
runner.expect(container.innerHTML.length > 100).toBeTruthy();
|
||||
runner.expect(container.innerHTML).toContain('Test message 1');
|
||||
runner.expect(container.innerHTML).toContain('Test message 2');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(debugButton);
|
||||
});
|
||||
|
||||
runner.it('should preserve clear functionality', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
debugPanel.addMessage('Test message 1', 'INFO');
|
||||
debugPanel.addMessage('Test message 2', 'ERROR');
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(2);
|
||||
|
||||
debugPanel.clear();
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(0);
|
||||
});
|
||||
|
||||
runner.it('should have core debug panel methods', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Should have core methods
|
||||
runner.expect(typeof debugPanel.toggle === 'function').toBeTruthy();
|
||||
runner.expect(typeof debugPanel.update === 'function').toBeTruthy();
|
||||
runner.expect(typeof debugPanel.addMessage === 'function').toBeTruthy();
|
||||
runner.expect(typeof debugPanel.clear === 'function').toBeTruthy();
|
||||
});
|
||||
|
||||
runner.it('should handle message categories properly', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test different message categories
|
||||
debugPanel.addMessage('Info message', 'INFO');
|
||||
debugPanel.addMessage('Warning message', 'WARNING');
|
||||
debugPanel.addMessage('Error message', 'ERROR');
|
||||
debugPanel.addMessage('Success message', 'SUCCESS');
|
||||
|
||||
const messages = debugPanel.getRecentMessages(4);
|
||||
runner.expect(messages.length).toBe(4);
|
||||
|
||||
const categories = messages.map(m => m.category);
|
||||
runner.expect(categories).toContain('INFO');
|
||||
runner.expect(categories).toContain('WARNING');
|
||||
runner.expect(categories).toContain('ERROR');
|
||||
runner.expect(categories).toContain('SUCCESS');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
runner,
|
||||
EXPECTED_DEBUGPANEL_API
|
||||
};
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing DebugPanel Component Extraction');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ DebugPanel extraction tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* DebugPanel Integration Test
|
||||
*
|
||||
* Tests that the extracted DebugPanel component integrates properly
|
||||
* with the existing SectionManager and DOMRenderer components.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('DebugPanel Integration Tests', () => {
|
||||
|
||||
runner.it('should load all extracted components including DebugPanel', () => {
|
||||
try {
|
||||
// Load extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
|
||||
runner.expect(sectionModule.SectionManager).toBeTruthy();
|
||||
runner.expect(domModule.DOMRenderer).toBeTruthy();
|
||||
runner.expect(debugModule.DebugPanel).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = sectionModule.SectionManager;
|
||||
global.ExtractedDOMRenderer = domModule.DOMRenderer;
|
||||
global.ExtractedDebugPanel = debugModule.DebugPanel;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted components: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support debug panel with section editing workflow', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
// Setup DOM elements
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const debugContainer = document.createElement('div');
|
||||
debugContainer.id = 'debug-messages-container';
|
||||
debugContainer.style.display = 'none';
|
||||
document.body.appendChild(debugContainer);
|
||||
|
||||
const debugButton = document.createElement('button');
|
||||
debugButton.id = 'toggle-debug';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
document.body.appendChild(debugButton);
|
||||
|
||||
// Create components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test workflow: Create sections and debug them
|
||||
const testMarkdown = '# Test Heading\nTest content for debugging';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
// Add debug messages
|
||||
debugPanel.addMessage('Section created: ' + sections[0].id, 'INFO');
|
||||
debugPanel.addMessage('DOM rendered successfully', 'SUCCESS');
|
||||
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(2);
|
||||
|
||||
// Test showing debug panel
|
||||
debugPanel.show();
|
||||
runner.expect(debugPanel.isActive).toBeTruthy();
|
||||
|
||||
// Test debug panel content
|
||||
const messages = debugPanel.getRecentMessages(2);
|
||||
runner.expect(messages[0].message).toContain('Section created');
|
||||
runner.expect(messages[1].message).toContain('DOM rendered');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(debugContainer);
|
||||
document.body.removeChild(debugButton);
|
||||
});
|
||||
|
||||
runner.it('should support debug panel clearing and message management', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Add multiple messages
|
||||
for (let i = 0; i < 10; i++) {
|
||||
debugPanel.addMessage(`Test message ${i}`, i % 2 === 0 ? 'INFO' : 'WARNING');
|
||||
}
|
||||
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(10);
|
||||
|
||||
// Test getting recent messages
|
||||
const recentFive = debugPanel.getRecentMessages(5);
|
||||
runner.expect(recentFive.length).toBe(5);
|
||||
runner.expect(recentFive[4].message).toContain('Test message 9');
|
||||
|
||||
// Test clearing
|
||||
debugPanel.clear();
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(0);
|
||||
});
|
||||
|
||||
runner.it('should handle debug panel DOM integration properly', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
// Setup DOM
|
||||
const debugContainer = document.createElement('div');
|
||||
debugContainer.id = 'debug-messages-container';
|
||||
debugContainer.style.display = 'none';
|
||||
document.body.appendChild(debugContainer);
|
||||
|
||||
const debugButton = document.createElement('button');
|
||||
debugButton.id = 'toggle-debug';
|
||||
debugButton.textContent = '🔍 Debug';
|
||||
debugButton.style.background = '#6c757d';
|
||||
document.body.appendChild(debugButton);
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Test initial state
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
runner.expect(debugContainer.style.display).toBe('none');
|
||||
|
||||
// Test toggle on
|
||||
debugPanel.toggle();
|
||||
runner.expect(debugPanel.isActive).toBeTruthy();
|
||||
runner.expect(debugContainer.style.display).toBe('block');
|
||||
runner.expect(debugButton.textContent).toContain('Debug (ON)');
|
||||
|
||||
// Test toggle off
|
||||
debugPanel.toggle();
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
runner.expect(debugContainer.style.display).toBe('none');
|
||||
runner.expect(debugButton.textContent).toBe('🔍 Debug');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(debugContainer);
|
||||
document.body.removeChild(debugButton);
|
||||
});
|
||||
|
||||
runner.it('should handle missing DOM elements gracefully', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Try to toggle without DOM elements (should not throw)
|
||||
try {
|
||||
debugPanel.toggle();
|
||||
debugPanel.show();
|
||||
debugPanel.hide();
|
||||
debugPanel.update();
|
||||
runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
|
||||
} catch (error) {
|
||||
throw new Error(`DebugPanel should handle missing DOM gracefully: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support event-driven debug message addition', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const debugPanel = new DebugPanel();
|
||||
|
||||
// Listen to section manager events and add debug messages
|
||||
let eventCount = 0;
|
||||
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
debugPanel.addMessage(`Sections created: ${data.count} sections`, 'INFO');
|
||||
eventCount++;
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
|
||||
eventCount++;
|
||||
});
|
||||
|
||||
// Create sections
|
||||
const testMarkdown = '# Test\nContent';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
// Start editing
|
||||
sectionManager.startEditing(sections[0].id);
|
||||
|
||||
// Verify debug messages were added
|
||||
runner.expect(eventCount).toBe(2);
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(2);
|
||||
|
||||
const messages = debugPanel.getRecentMessages(2);
|
||||
runner.expect(messages[0].message).toContain('Sections created');
|
||||
runner.expect(messages[1].message).toContain('Edit started');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Running DebugPanel Integration Tests');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ DebugPanel integration tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for Document Controls Component Extraction
|
||||
*
|
||||
* Tests the extraction of DocumentControls from the monolithic editor.js
|
||||
* DocumentControls handles the floating control panel and its actions.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
// Define expected DocumentControls API
|
||||
const EXPECTED_DOCUMENTCONTROLS_API = [
|
||||
'constructor',
|
||||
'create',
|
||||
'destroy',
|
||||
'show',
|
||||
'hide',
|
||||
'addButton',
|
||||
'removeButton',
|
||||
'setEventHandlers',
|
||||
'updateStatus',
|
||||
'getControlPanel'
|
||||
];
|
||||
|
||||
runner.describe('DocumentControls Component Extraction', () => {
|
||||
|
||||
runner.it('should define expected API methods', () => {
|
||||
const expectedMethods = EXPECTED_DOCUMENTCONTROLS_API;
|
||||
runner.expect(expectedMethods.length).toBe(10);
|
||||
runner.expect(expectedMethods).toContain('create');
|
||||
runner.expect(expectedMethods).toContain('addButton');
|
||||
runner.expect(expectedMethods).toContain('setEventHandlers');
|
||||
});
|
||||
|
||||
runner.it('should load extracted DocumentControls component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../components/document-controls.js')];
|
||||
|
||||
try {
|
||||
const module = require('../components/document-controls.js');
|
||||
runner.expect(module.DocumentControls).toBeTruthy();
|
||||
|
||||
// Set global for other tests
|
||||
global.ExtractedDocumentControls = module.DocumentControls;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted DocumentControls: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve constructor functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
runner.expect(controls).toBeInstanceOf(DocumentControls);
|
||||
runner.expect(controls.controlPanel).toBeFalsy(); // Initially null
|
||||
runner.expect(controls.buttons).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
runner.it('should preserve control panel creation functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
const panel = controls.getControlPanel();
|
||||
runner.expect(panel).toBeTruthy();
|
||||
runner.expect(panel.id).toBe('markitect-global-controls');
|
||||
|
||||
// Check that panel is added to DOM
|
||||
const domPanel = document.getElementById('markitect-global-controls');
|
||||
runner.expect(domPanel).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should preserve button creation functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
// Default buttons should be created
|
||||
runner.expect(controls.buttons.has('save-document')).toBeTruthy();
|
||||
runner.expect(controls.buttons.has('reset-all')).toBeTruthy();
|
||||
runner.expect(controls.buttons.has('show-status')).toBeTruthy();
|
||||
runner.expect(controls.buttons.has('toggle-debug')).toBeTruthy();
|
||||
|
||||
// Check DOM elements exist
|
||||
runner.expect(document.getElementById('save-document')).toBeTruthy();
|
||||
runner.expect(document.getElementById('reset-all')).toBeTruthy();
|
||||
runner.expect(document.getElementById('show-status')).toBeTruthy();
|
||||
runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support custom button addition', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
// Add custom button
|
||||
const customButton = controls.addButton('custom-test', '🎯 Test', '#ff6600');
|
||||
runner.expect(customButton).toBeTruthy();
|
||||
runner.expect(customButton.id).toBe('custom-test');
|
||||
runner.expect(customButton.textContent).toBe('🎯 Test');
|
||||
|
||||
// Check button is in map and DOM
|
||||
runner.expect(controls.buttons.has('custom-test')).toBeTruthy();
|
||||
runner.expect(document.getElementById('custom-test')).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support event handler configuration', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
let saveClicked = false;
|
||||
let resetClicked = false;
|
||||
|
||||
const handlers = {
|
||||
'save-document': () => { saveClicked = true; },
|
||||
'reset-all': () => { resetClicked = true; }
|
||||
};
|
||||
|
||||
controls.setEventHandlers(handlers);
|
||||
|
||||
// Simulate button clicks
|
||||
const saveBtn = document.getElementById('save-document');
|
||||
const resetBtn = document.getElementById('reset-all');
|
||||
|
||||
saveBtn.click();
|
||||
resetBtn.click();
|
||||
|
||||
runner.expect(saveClicked).toBeTruthy();
|
||||
runner.expect(resetClicked).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support show/hide functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
const panel = controls.getControlPanel();
|
||||
|
||||
// Test hiding
|
||||
controls.hide();
|
||||
runner.expect(panel.style.display).toBe('none');
|
||||
|
||||
// Test showing
|
||||
controls.show();
|
||||
runner.expect(panel.style.display).toBe('block');
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should preserve destroy functionality', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
// Verify panel exists
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
|
||||
|
||||
// Destroy
|
||||
controls.destroy();
|
||||
|
||||
// Verify panel is removed
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
|
||||
runner.expect(controls.controlPanel).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should support status updates', () => {
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
const controls = new DocumentControls();
|
||||
controls.create();
|
||||
|
||||
// Test status update
|
||||
controls.updateStatus({ totalSections: 5, editingSections: 2 });
|
||||
|
||||
// The status should be reflected in the panel (implementation specific)
|
||||
const panel = controls.getControlPanel();
|
||||
runner.expect(panel).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
controls.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
runner,
|
||||
EXPECTED_DOCUMENTCONTROLS_API
|
||||
};
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing DocumentControls Component Extraction');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ DocumentControls extraction tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for DOMRenderer Component Extraction
|
||||
*
|
||||
* Tests the extraction of DOMRenderer from the monolithic editor.js
|
||||
* DOMRenderer handles all DOM interactions and UI rendering.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
// Define expected DOMRenderer API
|
||||
const EXPECTED_DOMRENDERER_API = [
|
||||
'constructor',
|
||||
'renderAllSections',
|
||||
'renderSection',
|
||||
'showEditor',
|
||||
'hideCurrentEditor',
|
||||
'showImageEditor',
|
||||
'findSectionElement',
|
||||
'handleSectionClick',
|
||||
'setupSectionElement',
|
||||
'trackEvent',
|
||||
'getEventStats'
|
||||
// Note: addGlobalControls and debug methods are on MarkitectCleanEditor, not DOMRenderer
|
||||
];
|
||||
|
||||
runner.describe('DOMRenderer Component Extraction', () => {
|
||||
|
||||
runner.it('should define expected API methods', () => {
|
||||
const expectedMethods = EXPECTED_DOMRENDERER_API;
|
||||
runner.expect(expectedMethods.length).toBe(11);
|
||||
runner.expect(expectedMethods).toContain('renderAllSections');
|
||||
runner.expect(expectedMethods).toContain('showEditor');
|
||||
runner.expect(expectedMethods).toContain('handleSectionClick');
|
||||
});
|
||||
|
||||
runner.it('should extract from monolithic editor.js', () => {
|
||||
// Load the monolithic editor.js to extract DOMRenderer
|
||||
delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
|
||||
|
||||
try {
|
||||
const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
|
||||
runner.expect(editorModule.DOMRenderer).toBeTruthy();
|
||||
// Set global for other tests
|
||||
global.DOMRenderer = editorModule.DOMRenderer;
|
||||
global.SectionManager = editorModule.SectionManager;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve DOMRenderer constructor functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
runner.expect(renderer).toBeInstanceOf(DOMRenderer);
|
||||
runner.expect(renderer.sectionManager).toBe(sectionManager);
|
||||
runner.expect(renderer.container).toBe(container);
|
||||
});
|
||||
|
||||
runner.it('should preserve section rendering functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
// This should not throw an error
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
// Check that some content was rendered
|
||||
runner.expect(container.innerHTML.length).toBe(container.innerHTML.length); // Basic sanity check
|
||||
});
|
||||
|
||||
runner.it('should preserve findSectionElement functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const element = renderer.findSectionElement(sectionId);
|
||||
|
||||
// Should find an element or return null (not throw error)
|
||||
runner.expect(typeof element === 'object').toBeTruthy();
|
||||
});
|
||||
|
||||
runner.it('should preserve event tracking functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Should have trackEvent method
|
||||
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
|
||||
|
||||
// Should be able to track an event
|
||||
renderer.trackEvent('test-event', { data: 'test' });
|
||||
|
||||
// Should have getEventStats method
|
||||
runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
|
||||
|
||||
const stats = renderer.getEventStats();
|
||||
runner.expect(typeof stats === 'object').toBeTruthy();
|
||||
});
|
||||
|
||||
runner.it('should preserve editor showing functionality', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// showEditor should not throw error
|
||||
try {
|
||||
renderer.showEditor(sectionId, 'test content');
|
||||
runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
|
||||
} catch (error) {
|
||||
// Some errors are expected if DOM structure isn't complete
|
||||
runner.expect(typeof error.message === 'string').toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should have core DOM rendering methods', () => {
|
||||
const DOMRenderer = global.DOMRenderer;
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Should have core methods
|
||||
runner.expect(typeof renderer.renderAllSections === 'function').toBeTruthy();
|
||||
runner.expect(typeof renderer.showEditor === 'function').toBeTruthy();
|
||||
runner.expect(typeof renderer.findSectionElement === 'function').toBeTruthy();
|
||||
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// Export API tests for use during extraction
|
||||
const DOMRENDERER_API_TESTS = [
|
||||
(DOMRenderer, SectionManager) => {
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
if (!renderer.sectionManager) {
|
||||
throw new Error('sectionManager property missing');
|
||||
}
|
||||
},
|
||||
(DOMRenderer, SectionManager) => {
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
if (typeof renderer.renderAllSections !== 'function') {
|
||||
throw new Error('renderAllSections method missing');
|
||||
}
|
||||
},
|
||||
(DOMRenderer, SectionManager) => {
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
if (typeof renderer.showEditor !== 'function') {
|
||||
throw new Error('showEditor method missing');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
runner,
|
||||
EXPECTED_DOMRENDERER_API,
|
||||
DOMRENDERER_API_TESTS
|
||||
};
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing DOMRenderer Component Extraction');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ DOMRenderer extraction tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Environment Test - Verifies Jest setup is working correctly
|
||||
*/
|
||||
|
||||
describe('Test Environment', () => {
|
||||
test('should have JSDOM environment available', () => {
|
||||
expect(global.document).toBeDefined();
|
||||
expect(global.window).toBeDefined();
|
||||
expect(document.createElement).toBeDefined();
|
||||
});
|
||||
|
||||
test('should be able to create DOM elements', () => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = 'Test content';
|
||||
expect(div.tagName).toBe('DIV');
|
||||
expect(div.textContent).toBe('Test content');
|
||||
});
|
||||
|
||||
test('should have content container available', () => {
|
||||
const contentEl = document.getElementById('content');
|
||||
expect(contentEl).toBeDefined();
|
||||
expect(contentEl.tagName).toBe('DIV');
|
||||
});
|
||||
});
|
||||
@@ -1,271 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for Extracted DOMRenderer Component
|
||||
*
|
||||
* Tests the extracted DOMRenderer component independently from the monolith.
|
||||
* Verifies that core functionality is preserved after extraction.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Extracted DOMRenderer Component', () => {
|
||||
|
||||
runner.it('should load extracted DOMRenderer component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../components/dom-renderer.js')];
|
||||
|
||||
try {
|
||||
const module = require('../components/dom-renderer.js');
|
||||
runner.expect(module.DOMRenderer).toBeTruthy();
|
||||
runner.expect(module.FloatingMenu).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedDOMRenderer = module.DOMRenderer;
|
||||
global.ExtractedFloatingMenu = module.FloatingMenu;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted DOMRenderer: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve constructor functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
|
||||
// Load SectionManager from our extracted core
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
runner.expect(renderer).toBeInstanceOf(DOMRenderer);
|
||||
runner.expect(renderer.sectionManager).toBe(sectionManager);
|
||||
runner.expect(renderer.container).toBe(container);
|
||||
runner.expect(renderer.editingSections).toBeInstanceOf(Set);
|
||||
});
|
||||
|
||||
runner.it('should preserve section rendering functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
// This should not throw an error
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
// Check that content was rendered
|
||||
runner.expect(container.innerHTML.length > 100).toBeTruthy();
|
||||
runner.expect(container.innerHTML).toContain('Test Heading');
|
||||
});
|
||||
|
||||
runner.it('should preserve findSectionElement functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const element = renderer.findSectionElement(sectionId);
|
||||
|
||||
runner.expect(element).toBeTruthy();
|
||||
runner.expect(element.getAttribute('data-section-id')).toBe(sectionId);
|
||||
});
|
||||
|
||||
runner.it('should preserve event tracking functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
// Should have trackEvent method
|
||||
runner.expect(typeof renderer.trackEvent === 'function').toBeTruthy();
|
||||
|
||||
// Should be able to track an event
|
||||
renderer.trackEvent('test-event', { data: 'test' });
|
||||
|
||||
// Should have getEventStats method
|
||||
runner.expect(typeof renderer.getEventStats === 'function').toBeTruthy();
|
||||
|
||||
const stats = renderer.getEventStats();
|
||||
runner.expect(typeof stats === 'object').toBeTruthy();
|
||||
runner.expect(stats).toHaveProperty('stats');
|
||||
runner.expect(stats).toHaveProperty('totalEvents');
|
||||
runner.expect(stats).toHaveProperty('recentEvents');
|
||||
});
|
||||
|
||||
runner.it('should preserve editor showing functionality', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// showEditor should not throw error
|
||||
try {
|
||||
renderer.showEditor(sectionId, 'test content');
|
||||
runner.expect(true).toBeTruthy(); // If we get here, no error was thrown
|
||||
|
||||
// Check that editing state was set
|
||||
runner.expect(renderer.editingSections.has(sectionId)).toBeTruthy();
|
||||
} catch (error) {
|
||||
throw new Error(`showEditor failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve FloatingMenu functionality', () => {
|
||||
const FloatingMenu = global.ExtractedFloatingMenu;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const floatingMenu = new FloatingMenu(sectionId, 'text', renderer);
|
||||
|
||||
runner.expect(floatingMenu.sectionId).toBe(sectionId);
|
||||
runner.expect(floatingMenu.type).toBe('text');
|
||||
runner.expect(floatingMenu.renderer).toBe(renderer);
|
||||
runner.expect(floatingMenu.isVisible).toBeFalsy();
|
||||
|
||||
// Test show/hide functionality
|
||||
const content = document.createElement('div');
|
||||
content.textContent = 'Test content';
|
||||
|
||||
floatingMenu.show(content);
|
||||
runner.expect(floatingMenu.isVisible).toBeTruthy();
|
||||
|
||||
floatingMenu.hide();
|
||||
runner.expect(floatingMenu.isVisible).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should handle section click events', () => {
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const SectionManager = sectionModule.SectionManager;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const renderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = '# Test Heading\nTest content';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
renderer.renderAllSections(sections);
|
||||
|
||||
const sectionId = sections[0].id;
|
||||
const element = renderer.findSectionElement(sectionId);
|
||||
|
||||
// Simulate a click event
|
||||
const clickEvent = new Event('click', { bubbles: true });
|
||||
Object.defineProperty(clickEvent, 'target', { value: element });
|
||||
|
||||
// Should not throw error
|
||||
try {
|
||||
renderer.handleSectionClick(clickEvent);
|
||||
runner.expect(true).toBeTruthy();
|
||||
} catch (error) {
|
||||
throw new Error(`handleSectionClick failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Comparative test - verify extracted component behaves similarly to original
|
||||
runner.it('should behave similarly to original monolithic component', () => {
|
||||
// Load both components
|
||||
const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
|
||||
const extractedModule = require('../components/dom-renderer.js');
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
|
||||
const originalSectionManager = new originalModule.SectionManager();
|
||||
const extractedSectionManager = new sectionModule.SectionManager();
|
||||
|
||||
const originalContainer = document.createElement('div');
|
||||
originalContainer.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const extractedContainer = document.createElement('div');
|
||||
extractedContainer.innerHTML = '<div id="markdown-content"></div>';
|
||||
|
||||
const originalRenderer = new originalModule.DOMRenderer(originalSectionManager, originalContainer);
|
||||
const extractedRenderer = new extractedModule.DOMRenderer(extractedSectionManager, extractedContainer);
|
||||
|
||||
const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
|
||||
|
||||
// Create sections with both
|
||||
const originalSections = originalSectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
const extractedSections = extractedSectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
// Render with both
|
||||
originalRenderer.renderAllSections(originalSections);
|
||||
extractedRenderer.renderAllSections(extractedSections);
|
||||
|
||||
// Should have rendered content
|
||||
runner.expect(originalContainer.innerHTML.length > 100).toBeTruthy();
|
||||
runner.expect(extractedContainer.innerHTML.length > 100).toBeTruthy();
|
||||
|
||||
// Should have same number of section elements
|
||||
const originalSectionElements = originalContainer.querySelectorAll('.ui-edit-section');
|
||||
const extractedSectionElements = extractedContainer.querySelectorAll('.ui-edit-section');
|
||||
|
||||
runner.expect(extractedSectionElements.length).toBe(originalSectionElements.length);
|
||||
|
||||
// Should have similar event stats structure
|
||||
const originalStats = originalRenderer.getEventStats();
|
||||
const extractedStats = extractedRenderer.getEventStats();
|
||||
|
||||
runner.expect(extractedStats).toHaveProperty('stats');
|
||||
runner.expect(extractedStats).toHaveProperty('totalEvents');
|
||||
runner.expect(extractedStats).toHaveProperty('recentEvents');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing Extracted DOMRenderer Component');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Extracted DOMRenderer tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for Extracted SectionManager Component
|
||||
*
|
||||
* Tests the extracted SectionManager component independently from the monolith.
|
||||
* Verifies that all functionality is preserved after extraction.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Extracted SectionManager Component', () => {
|
||||
|
||||
runner.it('should load extracted SectionManager component', () => {
|
||||
// Load the extracted component
|
||||
delete require.cache[require.resolve('../core/section-manager.js')];
|
||||
|
||||
try {
|
||||
const module = require('../core/section-manager.js');
|
||||
runner.expect(module.SectionManager).toBeTruthy();
|
||||
runner.expect(module.Section).toBeTruthy();
|
||||
runner.expect(module.EditState).toBeTruthy();
|
||||
runner.expect(module.SectionType).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = module.SectionManager;
|
||||
global.ExtractedSection = module.Section;
|
||||
global.ExtractedEditState = module.EditState;
|
||||
global.ExtractedSectionType = module.SectionType;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted SectionManager: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve constructor functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
|
||||
const manager = new SectionManager();
|
||||
runner.expect(manager).toBeInstanceOf(SectionManager);
|
||||
runner.expect(manager.sections).toBeInstanceOf(Map);
|
||||
runner.expect(manager.listeners).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
runner.it('should preserve section creation functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
|
||||
const sections = manager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
runner.expect(Array.isArray(sections)).toBeTruthy();
|
||||
runner.expect(sections.length).toBe(2);
|
||||
runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
|
||||
runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
|
||||
});
|
||||
|
||||
runner.it('should preserve section editing functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const sections = manager.createSectionsFromMarkdown('# Test\nContent');
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// Test start editing
|
||||
const content = manager.startEditing(sectionId);
|
||||
runner.expect(content).toContain('Test');
|
||||
|
||||
const section = manager.sections.get(sectionId);
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
|
||||
// Test stop editing
|
||||
section.stopEditing();
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should preserve event system functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
let eventFired = false;
|
||||
let eventData = null;
|
||||
|
||||
manager.on('test-event', (data) => {
|
||||
eventFired = true;
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
manager.emit('test-event', { test: 'data' });
|
||||
|
||||
runner.expect(eventFired).toBeTruthy();
|
||||
runner.expect(eventData).toEqual({ test: 'data' });
|
||||
});
|
||||
|
||||
runner.it('should preserve document status functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
manager.createSectionsFromMarkdown('# Test\nContent');
|
||||
const status = manager.getDocumentStatus();
|
||||
|
||||
runner.expect(status).toHaveProperty('totalSections');
|
||||
runner.expect(status).toHaveProperty('editingSections');
|
||||
runner.expect(status.totalSections).toBe(1);
|
||||
});
|
||||
|
||||
runner.it('should preserve getAllSections functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const testMarkdown = '# One\nContent\n\n# Two\nMore content';
|
||||
manager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
const allSections = manager.getAllSections();
|
||||
runner.expect(Array.isArray(allSections)).toBeTruthy();
|
||||
runner.expect(allSections.length).toBe(2);
|
||||
});
|
||||
|
||||
runner.it('should preserve section splitting functionality', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const sections = manager.createSectionsFromMarkdown('# Original\nContent');
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
|
||||
const newSections = manager.handleSectionSplit(sectionId, newContent);
|
||||
|
||||
runner.expect(Array.isArray(newSections)).toBeTruthy();
|
||||
runner.expect(newSections.length).toBe(2);
|
||||
runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
|
||||
});
|
||||
|
||||
runner.it('should preserve Section class functionality', () => {
|
||||
const Section = global.ExtractedSection;
|
||||
const EditState = global.ExtractedEditState;
|
||||
|
||||
const section = new Section('test-id', '# Test Content', 'heading');
|
||||
|
||||
runner.expect(section.id).toBe('test-id');
|
||||
runner.expect(section.currentMarkdown).toBe('# Test Content');
|
||||
runner.expect(section.type).toBe('heading');
|
||||
runner.expect(section.state).toBe(EditState.ORIGINAL);
|
||||
});
|
||||
|
||||
runner.it('should preserve Section ID generation', () => {
|
||||
const Section = global.ExtractedSection;
|
||||
|
||||
const id1 = Section.generateId('# Test Heading', 0);
|
||||
const id2 = Section.generateId('# Different Heading', 1);
|
||||
|
||||
runner.expect(typeof id1 === 'string').toBeTruthy();
|
||||
runner.expect(typeof id2 === 'string').toBeTruthy();
|
||||
runner.expect(id1).toContain('section-');
|
||||
runner.expect(id2).toContain('section-');
|
||||
runner.expect(id1 !== id2).toBeTruthy(); // Should be unique
|
||||
});
|
||||
|
||||
runner.it('should preserve Section type detection', () => {
|
||||
const Section = global.ExtractedSection;
|
||||
const SectionType = global.ExtractedSectionType;
|
||||
|
||||
runner.expect(Section.detectType('# Heading')).toBe(SectionType.HEADING);
|
||||
runner.expect(Section.detectType('')).toBe(SectionType.IMAGE);
|
||||
runner.expect(Section.detectType('```code```')).toBe(SectionType.CODE);
|
||||
runner.expect(Section.detectType('Regular paragraph')).toBe(SectionType.PARAGRAPH);
|
||||
});
|
||||
|
||||
// Comparative test - verify extracted component behaves identically to original
|
||||
runner.it('should behave identically to original monolithic component', () => {
|
||||
// Load both components
|
||||
const originalModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
|
||||
const extractedModule = require('../core/section-manager.js');
|
||||
|
||||
const originalManager = new originalModule.SectionManager();
|
||||
const extractedManager = new extractedModule.SectionManager();
|
||||
|
||||
const testMarkdown = '# Test\nContent\n\n## Subheading\nMore content';
|
||||
|
||||
// Debug: Check what each component produces
|
||||
console.log('Creating sections with original component...');
|
||||
const originalSections = originalManager.createSectionsFromMarkdown(testMarkdown);
|
||||
console.log(`Original produced ${originalSections.length} sections`);
|
||||
|
||||
console.log('Creating sections with extracted component...');
|
||||
const extractedSections = extractedManager.createSectionsFromMarkdown(testMarkdown);
|
||||
console.log(`Extracted produced ${extractedSections.length} sections`);
|
||||
|
||||
if (originalSections.length > 0) {
|
||||
console.log('Original first section:', originalSections[0].currentMarkdown);
|
||||
}
|
||||
if (extractedSections.length > 0) {
|
||||
console.log('Extracted first section:', extractedSections[0].currentMarkdown);
|
||||
}
|
||||
|
||||
// Should have same number of sections
|
||||
runner.expect(extractedSections.length).toBe(originalSections.length);
|
||||
|
||||
// Should have same content
|
||||
for (let i = 0; i < originalSections.length; i++) {
|
||||
runner.expect(extractedSections[i].currentMarkdown).toBe(originalSections[i].currentMarkdown);
|
||||
runner.expect(extractedSections[i].type).toBe(originalSections[i].type);
|
||||
}
|
||||
|
||||
// Should have same document status structure
|
||||
const originalStatus = originalManager.getDocumentStatus();
|
||||
const extractedStatus = extractedManager.getDocumentStatus();
|
||||
|
||||
console.log('Original status:', originalStatus);
|
||||
console.log('Extracted status:', extractedStatus);
|
||||
|
||||
runner.expect(extractedStatus.totalSections).toBe(originalStatus.totalSections);
|
||||
runner.expect(extractedStatus.editingSections).toBe(originalStatus.editingSections);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing Extracted SectionManager Component');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Extracted SectionManager tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Full Integration Test
|
||||
*
|
||||
* Tests that all extracted components (SectionManager, DOMRenderer,
|
||||
* DebugPanel, DocumentControls) work together as a complete system.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Full Component Integration Tests', () => {
|
||||
|
||||
runner.it('should load all extracted components', () => {
|
||||
try {
|
||||
// Load all extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
runner.expect(sectionModule.SectionManager).toBeTruthy();
|
||||
runner.expect(domModule.DOMRenderer).toBeTruthy();
|
||||
runner.expect(debugModule.DebugPanel).toBeTruthy();
|
||||
runner.expect(controlsModule.DocumentControls).toBeTruthy();
|
||||
|
||||
// Set globals for other tests
|
||||
global.ExtractedSectionManager = sectionModule.SectionManager;
|
||||
global.ExtractedDOMRenderer = domModule.DOMRenderer;
|
||||
global.ExtractedDebugPanel = debugModule.DebugPanel;
|
||||
global.ExtractedDocumentControls = controlsModule.DocumentControls;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load extracted components: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support complete document editing workflow with all components', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Setup DOM container
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Create all components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// Setup document controls
|
||||
documentControls.create();
|
||||
|
||||
// Wire up event handlers for debugging
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
debugPanel.addMessage(`Created ${data.count} sections`, 'INFO');
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
debugPanel.addMessage(`Edit started for section: ${data.sectionId}`, 'DEBUG');
|
||||
});
|
||||
|
||||
// Test workflow: Create document
|
||||
const testMarkdown = `# Document Title
|
||||
Introduction paragraph with some content.
|
||||
|
||||
## Section A
|
||||
Content for section A with details.
|
||||
|
||||

|
||||
|
||||
### Subsection A.1
|
||||
More detailed content here.`;
|
||||
|
||||
// Create sections
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
runner.expect(sections.length).toBe(4);
|
||||
|
||||
// Render sections
|
||||
domRenderer.renderAllSections(sections);
|
||||
const renderedElements = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(renderedElements.length).toBe(sections.length);
|
||||
|
||||
// Test editing workflow
|
||||
const firstSection = sections[0];
|
||||
sectionManager.startEditing(firstSection.id);
|
||||
runner.expect(firstSection.isEditing()).toBeTruthy();
|
||||
|
||||
// Check debug messages were created
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(2); // sections-created + edit-started
|
||||
|
||||
// Test document controls functionality
|
||||
const controlPanel = documentControls.getControlPanel();
|
||||
runner.expect(controlPanel).toBeTruthy();
|
||||
runner.expect(document.getElementById('save-document')).toBeTruthy();
|
||||
runner.expect(document.getElementById('toggle-debug')).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support debug panel integration with document controls', () => {
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Create components
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// Setup document controls
|
||||
documentControls.create();
|
||||
|
||||
// Setup debug panel toggle handler
|
||||
const handlers = {
|
||||
'toggle-debug': () => debugPanel.toggle()
|
||||
};
|
||||
documentControls.setEventHandlers(handlers);
|
||||
|
||||
// Test debug toggle functionality
|
||||
const debugButton = documentControls.getButton('toggle-debug');
|
||||
runner.expect(debugButton).toBeTruthy();
|
||||
|
||||
// Add some debug messages
|
||||
debugPanel.addMessage('Test message 1', 'INFO');
|
||||
debugPanel.addMessage('Test message 2', 'ERROR');
|
||||
|
||||
// Simulate button click to show debug panel
|
||||
debugButton.click();
|
||||
runner.expect(debugPanel.isActive).toBeTruthy();
|
||||
|
||||
// Simulate button click to hide debug panel
|
||||
debugButton.click();
|
||||
runner.expect(debugPanel.isActive).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should support event-driven communication between all components', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Setup container
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Create components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
documentControls.create();
|
||||
|
||||
// Setup comprehensive event handling
|
||||
let eventLog = [];
|
||||
|
||||
sectionManager.on('sections-created', (data) => {
|
||||
eventLog.push(`sections-created: ${data.count} sections`);
|
||||
debugPanel.addMessage(`Sections created: ${data.count}`, 'INFO');
|
||||
});
|
||||
|
||||
sectionManager.on('edit-started', (data) => {
|
||||
eventLog.push(`edit-started: ${data.sectionId}`);
|
||||
debugPanel.addMessage(`Edit started: ${data.sectionId}`, 'DEBUG');
|
||||
});
|
||||
|
||||
sectionManager.on('changes-accepted', (data) => {
|
||||
eventLog.push(`changes-accepted: ${data.sectionId}`);
|
||||
debugPanel.addMessage(`Changes accepted: ${data.sectionId}`, 'SUCCESS');
|
||||
});
|
||||
|
||||
// Test complete workflow
|
||||
const testMarkdown = '# Test\nContent for testing';
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
// Start editing
|
||||
sectionManager.startEditing(sections[0].id);
|
||||
sectionManager.updateContent(sections[0].id, '# Updated Test\nUpdated content');
|
||||
sectionManager.acceptChanges(sections[0].id);
|
||||
|
||||
// Verify events were logged
|
||||
runner.expect(eventLog.length).toBe(3);
|
||||
runner.expect(eventLog[0]).toContain('sections-created');
|
||||
runner.expect(eventLog[1]).toContain('edit-started');
|
||||
runner.expect(eventLog[2]).toContain('changes-accepted');
|
||||
|
||||
// Verify debug messages were created
|
||||
runner.expect(debugPanel.getMessageCount()).toBe(3);
|
||||
|
||||
// Test document controls status update
|
||||
const status = sectionManager.getDocumentStatus();
|
||||
documentControls.updateStatus(status);
|
||||
runner.expect(documentControls.lastStatus).toBeTruthy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should handle error scenarios gracefully across components', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Test component creation without proper DOM setup
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// These should not throw errors
|
||||
try {
|
||||
debugPanel.toggle(); // No DOM elements
|
||||
debugPanel.update(); // No DOM elements
|
||||
documentControls.show(); // No control panel created yet
|
||||
documentControls.hide(); // No control panel created yet
|
||||
|
||||
runner.expect(true).toBeTruthy(); // If we get here, no errors were thrown
|
||||
} catch (error) {
|
||||
throw new Error(`Components should handle missing DOM gracefully: ${error.message}`);
|
||||
}
|
||||
|
||||
// Test section manager with invalid input
|
||||
const sectionManager = new SectionManager();
|
||||
const sections = sectionManager.createSectionsFromMarkdown('');
|
||||
runner.expect(sections.length).toBe(0);
|
||||
|
||||
// Test DOM renderer with invalid container
|
||||
try {
|
||||
const invalidRenderer = new DOMRenderer(sectionManager, null);
|
||||
runner.expect(invalidRenderer.container).toBeFalsy();
|
||||
} catch (error) {
|
||||
// This is acceptable - constructor might validate input
|
||||
runner.expect(typeof error.message === 'string').toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should support scalable architecture with component lifecycle', () => {
|
||||
const SectionManager = global.ExtractedSectionManager;
|
||||
const DOMRenderer = global.ExtractedDOMRenderer;
|
||||
const DebugPanel = global.ExtractedDebugPanel;
|
||||
const DocumentControls = global.ExtractedDocumentControls;
|
||||
|
||||
// Test multiple instances
|
||||
const sectionManager1 = new SectionManager();
|
||||
const sectionManager2 = new SectionManager();
|
||||
const debugPanel1 = new DebugPanel();
|
||||
const debugPanel2 = new DebugPanel();
|
||||
|
||||
// Each should be independent
|
||||
debugPanel1.addMessage('Message from panel 1', 'INFO');
|
||||
debugPanel2.addMessage('Message from panel 2', 'ERROR');
|
||||
|
||||
runner.expect(debugPanel1.getMessageCount()).toBe(1);
|
||||
runner.expect(debugPanel2.getMessageCount()).toBe(1);
|
||||
|
||||
// Test section managers are independent
|
||||
const sections1 = sectionManager1.createSectionsFromMarkdown('# Document 1');
|
||||
const sections2 = sectionManager2.createSectionsFromMarkdown('# Document 2');
|
||||
|
||||
runner.expect(sections1.length).toBe(1);
|
||||
runner.expect(sections2.length).toBe(1);
|
||||
runner.expect(sections1[0]).toBeTruthy();
|
||||
runner.expect(sections2[0]).toBeTruthy();
|
||||
|
||||
// IDs should be different (each section gets unique ID)
|
||||
const id1 = sections1[0].id;
|
||||
const id2 = sections2[0].id;
|
||||
runner.expect(id1 !== id2).toBeTruthy();
|
||||
|
||||
// Test document controls lifecycle
|
||||
const controls1 = new DocumentControls();
|
||||
const controls2 = new DocumentControls();
|
||||
|
||||
controls1.create();
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
|
||||
|
||||
controls2.create(); // Should replace the first one
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeTruthy();
|
||||
|
||||
controls2.destroy();
|
||||
runner.expect(document.getElementById('markitect-global-controls')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Running Full Component Integration Tests');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Full integration tests completed');
|
||||
});
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Real User Functionality Tests
|
||||
*
|
||||
* This test file validates the actual functionality that users experience,
|
||||
* not just internal API calls. It tests the complete user workflow.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
runner.describe('Real User Functionality Tests', () => {
|
||||
|
||||
runner.it('should allow users to edit content and see changes in DOM', () => {
|
||||
// Load all extracted components
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DebugPanel } = debugModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
// Setup DOM container
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Create components
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
// Setup document controls
|
||||
documentControls.create();
|
||||
|
||||
// Create sections from test markdown
|
||||
const testMarkdown = `# Original Title\nOriginal content that should be editable.`;
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const firstSection = sections[0];
|
||||
const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
|
||||
// Verify original content is rendered
|
||||
runner.expect(sectionElement.innerHTML).toContain('Original Title');
|
||||
|
||||
// Simulate user clicking on section
|
||||
const clickEvent = new Event('click', { bubbles: true });
|
||||
sectionElement.dispatchEvent(clickEvent);
|
||||
|
||||
// Verify editing state is active
|
||||
runner.expect(firstSection.isEditing()).toBeTruthy();
|
||||
|
||||
// Find the floating menu and edit controls
|
||||
const floatingMenu = document.querySelector('.ui-edit-floating-menu');
|
||||
runner.expect(floatingMenu).toBeTruthy();
|
||||
|
||||
const textarea = floatingMenu.querySelector('textarea');
|
||||
const acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
|
||||
|
||||
runner.expect(textarea).toBeTruthy();
|
||||
runner.expect(acceptButton).toBeTruthy();
|
||||
|
||||
// Simulate user editing content
|
||||
const newContent = '# Updated Title\nCompletely new content added by user.';
|
||||
textarea.value = newContent;
|
||||
|
||||
// Simulate user clicking accept
|
||||
acceptButton.click();
|
||||
|
||||
// Verify section is no longer editing
|
||||
runner.expect(firstSection.isEditing()).toBeFalsy();
|
||||
|
||||
// Verify floating menu is gone
|
||||
const menuAfterAccept = document.querySelector('.ui-edit-floating-menu');
|
||||
runner.expect(menuAfterAccept).toBeFalsy();
|
||||
|
||||
// CRITICAL TEST: Verify DOM was actually updated with new content
|
||||
const updatedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
runner.expect(updatedElement.innerHTML).toContain('Updated Title');
|
||||
runner.expect(updatedElement.innerHTML).toContain('Completely new content');
|
||||
runner.expect(updatedElement.innerHTML).not.toContain('Original Title');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should allow users to reset all changes', () => {
|
||||
// Setup similar to above
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
documentControls.create();
|
||||
|
||||
// Create and modify content
|
||||
const testMarkdown = `# Test Section\nOriginal content for reset test.`;
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const firstSection = sections[0];
|
||||
|
||||
// Make changes to the section
|
||||
sectionManager.startEditing(firstSection.id);
|
||||
sectionManager.updateContent(firstSection.id, '# Modified Title\nModified content.');
|
||||
sectionManager.acceptChanges(firstSection.id);
|
||||
|
||||
// Verify changes are applied
|
||||
let sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
runner.expect(sectionElement.innerHTML).toContain('Modified Title');
|
||||
runner.expect(firstSection.hasChanges()).toBeTruthy();
|
||||
|
||||
// Test reset functionality
|
||||
const resetButton = documentControls.getButton('reset-all');
|
||||
runner.expect(resetButton).toBeTruthy();
|
||||
|
||||
// Click reset button
|
||||
resetButton.click();
|
||||
|
||||
// Verify content is reset
|
||||
sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
runner.expect(sectionElement.innerHTML).toContain('Test Section');
|
||||
runner.expect(sectionElement.innerHTML).not.toContain('Modified Title');
|
||||
runner.expect(firstSection.hasChanges()).toBeFalsy();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
|
||||
runner.it('should handle cancel operations correctly', () => {
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
|
||||
const testMarkdown = `# Cancel Test\nContent that should remain unchanged.`;
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
const firstSection = sections[0];
|
||||
const originalContent = firstSection.currentMarkdown;
|
||||
|
||||
// Start editing
|
||||
const sectionElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
sectionElement.click();
|
||||
|
||||
// Make changes but cancel them
|
||||
const floatingMenu = document.querySelector('.ui-edit-floating-menu');
|
||||
const textarea = floatingMenu.querySelector('textarea');
|
||||
const cancelButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Cancel'));
|
||||
|
||||
textarea.value = '# This should be cancelled\nThis content should not appear.';
|
||||
cancelButton.click();
|
||||
|
||||
// Verify content is unchanged
|
||||
const unchangedElement = container.querySelector(`[data-section-id="${firstSection.id}"]`);
|
||||
runner.expect(unchangedElement.innerHTML).toContain('Cancel Test');
|
||||
runner.expect(unchangedElement.innerHTML).not.toContain('This should be cancelled');
|
||||
runner.expect(firstSection.currentMarkdown).toBe(originalContent);
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
runner.it('should validate the complete editing workflow', () => {
|
||||
// This test validates the entire user experience end-to-end
|
||||
const sectionModule = require('../core/section-manager.js');
|
||||
const domModule = require('../components/dom-renderer.js');
|
||||
const debugModule = require('../components/debug-panel.js');
|
||||
const controlsModule = require('../components/document-controls.js');
|
||||
|
||||
const { SectionManager } = sectionModule;
|
||||
const { DOMRenderer } = domModule;
|
||||
const { DebugPanel } = debugModule;
|
||||
const { DocumentControls } = controlsModule;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<div id="markdown-content"></div>';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const sectionManager = new SectionManager();
|
||||
const domRenderer = new DOMRenderer(sectionManager, container);
|
||||
const debugPanel = new DebugPanel();
|
||||
const documentControls = new DocumentControls();
|
||||
|
||||
documentControls.create();
|
||||
|
||||
// Multi-section document
|
||||
const testMarkdown = `# Document Title
|
||||
Introduction paragraph.
|
||||
|
||||
## Section A
|
||||
Content for section A.
|
||||
|
||||
## Section B
|
||||
Content for section B.`;
|
||||
|
||||
const sections = sectionManager.createSectionsFromMarkdown(testMarkdown);
|
||||
domRenderer.renderAllSections(sections);
|
||||
|
||||
// Verify all sections are rendered
|
||||
const renderedSections = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(renderedSections.length).toBe(sections.length);
|
||||
|
||||
// Test editing multiple sections
|
||||
const firstSection = sections[0];
|
||||
const secondSection = sections[2]; // Section A
|
||||
|
||||
// Edit first section
|
||||
renderedSections[0].click();
|
||||
let floatingMenu = document.querySelector('.ui-edit-floating-menu');
|
||||
let textarea = floatingMenu.querySelector('textarea');
|
||||
let acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
|
||||
|
||||
textarea.value = '# Updated Document Title\nUpdated introduction.';
|
||||
acceptButton.click();
|
||||
|
||||
// Edit second section
|
||||
renderedSections[2].click();
|
||||
floatingMenu = document.querySelector('.ui-edit-floating-menu');
|
||||
textarea = floatingMenu.querySelector('textarea');
|
||||
acceptButton = Array.from(floatingMenu.querySelectorAll('button')).find(btn => btn.textContent.includes('Accept'));
|
||||
|
||||
textarea.value = '## Updated Section A\nCompletely new content for section A.';
|
||||
acceptButton.click();
|
||||
|
||||
// Verify both sections were updated
|
||||
const updatedSections = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(updatedSections[0].innerHTML).toContain('Updated Document Title');
|
||||
runner.expect(updatedSections[2].innerHTML).toContain('Updated Section A');
|
||||
|
||||
// Test reset restores all sections
|
||||
const resetButton = documentControls.getButton('reset-all');
|
||||
resetButton.click();
|
||||
|
||||
const resetSections = container.querySelectorAll('.ui-edit-section');
|
||||
runner.expect(resetSections[0].innerHTML).toContain('Document Title');
|
||||
runner.expect(resetSections[0].innerHTML).not.toContain('Updated Document Title');
|
||||
runner.expect(resetSections[2].innerHTML).toContain('Section A');
|
||||
runner.expect(resetSections[2].innerHTML).not.toContain('Updated Section A');
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(container);
|
||||
documentControls.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = runner;
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Running Real User Functionality Tests');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ Real user functionality tests completed');
|
||||
console.log('These tests validate what users actually experience, not just internal APIs');
|
||||
});
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Test for SectionManager Component Extraction
|
||||
*
|
||||
* Tests the extraction of SectionManager from the monolithic editor.js
|
||||
* Ensures all functionality is preserved during refactoring.
|
||||
*/
|
||||
|
||||
const RefactorTestRunner = require('./refactor-test-runner.js');
|
||||
|
||||
const runner = new RefactorTestRunner();
|
||||
|
||||
// First, let's define what the SectionManager API should look like
|
||||
const EXPECTED_SECTION_MANAGER_API = [
|
||||
'constructor',
|
||||
'createSectionsFromMarkdown',
|
||||
'startEditing',
|
||||
'stopEditing',
|
||||
'getAllSections',
|
||||
'sections', // Map property, not method
|
||||
'getDocumentStatus',
|
||||
'getDocumentMarkdown',
|
||||
'on', // event system
|
||||
'emit', // event system
|
||||
'handleSectionSplit',
|
||||
'updateContent',
|
||||
'acceptChanges',
|
||||
'cancelChanges',
|
||||
'resetSection'
|
||||
];
|
||||
|
||||
runner.describe('SectionManager Component Extraction', () => {
|
||||
|
||||
runner.it('should define expected API methods', () => {
|
||||
// This test defines what we expect from the extracted SectionManager
|
||||
const expectedMethods = EXPECTED_SECTION_MANAGER_API;
|
||||
runner.expect(expectedMethods.length).toBe(15);
|
||||
runner.expect(expectedMethods).toContain('createSectionsFromMarkdown');
|
||||
runner.expect(expectedMethods).toContain('startEditing');
|
||||
runner.expect(expectedMethods).toContain('stopEditing');
|
||||
});
|
||||
|
||||
runner.it('should extract from monolithic editor.js', () => {
|
||||
// Load the monolithic editor.js to extract SectionManager
|
||||
delete require.cache[require.resolve('/home/worsch/markitect_project/markitect/static/editor.js')];
|
||||
|
||||
try {
|
||||
const editorModule = require('/home/worsch/markitect_project/markitect/static/editor.js');
|
||||
runner.expect(editorModule.SectionManager).toBeTruthy();
|
||||
// Set global for other tests
|
||||
global.SectionManager = editorModule.SectionManager;
|
||||
global.Section = editorModule.Section;
|
||||
global.EditState = editorModule.EditState;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load monolithic editor.js: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
runner.it('should preserve SectionManager constructor functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
|
||||
const manager = new SectionManager();
|
||||
runner.expect(manager).toBeInstanceOf(SectionManager);
|
||||
runner.expect(manager.sections).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
runner.it('should preserve createSectionsFromMarkdown functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const testMarkdown = `# Heading 1\nContent 1\n\n## Heading 2\nContent 2`;
|
||||
const sections = manager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
runner.expect(Array.isArray(sections)).toBeTruthy();
|
||||
runner.expect(sections.length).toBe(2);
|
||||
runner.expect(sections[0].currentMarkdown).toContain('Heading 1');
|
||||
runner.expect(sections[1].currentMarkdown).toContain('Heading 2');
|
||||
});
|
||||
|
||||
runner.it('should preserve section editing state management', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const sections = manager.createSectionsFromMarkdown('# Test\nContent');
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
// Test start editing
|
||||
runner.expect(manager.startEditing(sectionId)).toBeTruthy();
|
||||
const section = manager.sections.get(sectionId);
|
||||
runner.expect(section.isEditing()).toBeTruthy();
|
||||
|
||||
// Test stop editing
|
||||
section.stopEditing();
|
||||
runner.expect(section.isEditing()).toBeFalsy();
|
||||
});
|
||||
|
||||
runner.it('should preserve event system functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
let eventFired = false;
|
||||
let eventData = null;
|
||||
|
||||
manager.on('test-event', (data) => {
|
||||
eventFired = true;
|
||||
eventData = data;
|
||||
});
|
||||
|
||||
manager.emit('test-event', { test: 'data' });
|
||||
|
||||
runner.expect(eventFired).toBeTruthy();
|
||||
runner.expect(eventData).toEqual({ test: 'data' });
|
||||
});
|
||||
|
||||
runner.it('should preserve document status functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
manager.createSectionsFromMarkdown('# Test\nContent');
|
||||
const status = manager.getDocumentStatus();
|
||||
|
||||
runner.expect(status).toHaveProperty('totalSections');
|
||||
runner.expect(status).toHaveProperty('editingSections');
|
||||
runner.expect(status.totalSections).toBe(1);
|
||||
});
|
||||
|
||||
runner.it('should preserve getAllSections functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const testMarkdown = '# One\nContent\n\n# Two\nMore content';
|
||||
manager.createSectionsFromMarkdown(testMarkdown);
|
||||
|
||||
const allSections = manager.getAllSections();
|
||||
runner.expect(Array.isArray(allSections)).toBeTruthy();
|
||||
runner.expect(allSections.length).toBe(2);
|
||||
});
|
||||
|
||||
runner.it('should preserve section splitting functionality', () => {
|
||||
const SectionManager = global.SectionManager;
|
||||
const manager = new SectionManager();
|
||||
|
||||
const sections = manager.createSectionsFromMarkdown('# Original\nContent');
|
||||
const sectionId = sections[0].id;
|
||||
|
||||
const newContent = '# Split 1\nContent 1\n\n# Split 2\nContent 2';
|
||||
const newSections = manager.handleSectionSplit(sectionId, newContent);
|
||||
|
||||
runner.expect(Array.isArray(newSections)).toBeTruthy();
|
||||
runner.expect(newSections.length).toBe(2);
|
||||
runner.expect(manager.sections.has(sectionId)).toBeFalsy(); // Original removed
|
||||
});
|
||||
});
|
||||
|
||||
// Export API tests for use during extraction
|
||||
const SECTION_MANAGER_API_TESTS = [
|
||||
(SectionManager) => {
|
||||
const manager = new SectionManager();
|
||||
if (!manager.sections || !(manager.sections instanceof Map)) {
|
||||
throw new Error('sections property missing or not a Map');
|
||||
}
|
||||
},
|
||||
(SectionManager) => {
|
||||
const manager = new SectionManager();
|
||||
if (typeof manager.createSectionsFromMarkdown !== 'function') {
|
||||
throw new Error('createSectionsFromMarkdown method missing');
|
||||
}
|
||||
},
|
||||
(SectionManager) => {
|
||||
const manager = new SectionManager();
|
||||
if (typeof manager.startEditing !== 'function') {
|
||||
throw new Error('startEditing method missing');
|
||||
}
|
||||
},
|
||||
(SectionManager) => {
|
||||
const manager = new SectionManager();
|
||||
if (typeof manager.stopEditing !== 'function') {
|
||||
throw new Error('stopEditing method missing');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
runner,
|
||||
EXPECTED_SECTION_MANAGER_API,
|
||||
SECTION_MANAGER_API_TESTS
|
||||
};
|
||||
|
||||
// Run tests if called directly
|
||||
if (require.main === module) {
|
||||
console.log('🧪 Testing SectionManager Component Extraction');
|
||||
runner.run().then(() => {
|
||||
console.log('✅ SectionManager extraction tests completed');
|
||||
});
|
||||
}
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/acorn
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/acorn
generated
vendored
@@ -1 +0,0 @@
|
||||
../acorn/bin/acorn
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/baseline-browser-mapping
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/baseline-browser-mapping
generated
vendored
@@ -1 +0,0 @@
|
||||
../baseline-browser-mapping/dist/cli.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/browserslist
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/browserslist
generated
vendored
@@ -1 +0,0 @@
|
||||
../browserslist/cli.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/create-jest
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/create-jest
generated
vendored
@@ -1 +0,0 @@
|
||||
../create-jest/bin/create-jest.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/escodegen
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/escodegen
generated
vendored
@@ -1 +0,0 @@
|
||||
../escodegen/bin/escodegen.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/esgenerate
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/esgenerate
generated
vendored
@@ -1 +0,0 @@
|
||||
../escodegen/bin/esgenerate.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/eslint
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/eslint
generated
vendored
@@ -1 +0,0 @@
|
||||
../eslint/bin/eslint.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/esparse
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/esparse
generated
vendored
@@ -1 +0,0 @@
|
||||
../esprima/bin/esparse.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/esvalidate
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/esvalidate
generated
vendored
@@ -1 +0,0 @@
|
||||
../esprima/bin/esvalidate.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/import-local-fixture
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/import-local-fixture
generated
vendored
@@ -1 +0,0 @@
|
||||
../import-local/fixtures/cli.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/jest
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/jest
generated
vendored
@@ -1 +0,0 @@
|
||||
../jest/bin/jest.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/js-yaml
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/js-yaml
generated
vendored
@@ -1 +0,0 @@
|
||||
../js-yaml/bin/js-yaml.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/jsesc
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/jsesc
generated
vendored
@@ -1 +0,0 @@
|
||||
../jsesc/bin/jsesc
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/json5
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/json5
generated
vendored
@@ -1 +0,0 @@
|
||||
../json5/lib/cli.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/node-which
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/node-which
generated
vendored
@@ -1 +0,0 @@
|
||||
../which/bin/node-which
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/parser
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/parser
generated
vendored
@@ -1 +0,0 @@
|
||||
../@babel/parser/bin/babel-parser.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/regjsparser
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/regjsparser
generated
vendored
@@ -1 +0,0 @@
|
||||
../regjsparser/bin/parser
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/resolve
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/resolve
generated
vendored
@@ -1 +0,0 @@
|
||||
../resolve/bin/resolve
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/rimraf
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/rimraf
generated
vendored
@@ -1 +0,0 @@
|
||||
../rimraf/bin.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/semver
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/semver
generated
vendored
@@ -1 +0,0 @@
|
||||
../semver/bin/semver.js
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/tsc
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/tsc
generated
vendored
@@ -1 +0,0 @@
|
||||
../typescript/bin/tsc
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/tsserver
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/tsserver
generated
vendored
@@ -1 +0,0 @@
|
||||
../typescript/bin/tsserver
|
||||
1
capabilities/testdrive-jsui/node_modules/.bin/update-browserslist-db
generated
vendored
1
capabilities/testdrive-jsui/node_modules/.bin/update-browserslist-db
generated
vendored
@@ -1 +0,0 @@
|
||||
../update-browserslist-db/cli.js
|
||||
9213
capabilities/testdrive-jsui/node_modules/.package-lock.json
generated
vendored
9213
capabilities/testdrive-jsui/node_modules/.package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
21
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/LICENSE
generated
vendored
21
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 asamuzaK (Kazz)
|
||||
|
||||
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.
|
||||
316
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/README.md
generated
vendored
316
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/README.md
generated
vendored
@@ -1,316 +0,0 @@
|
||||
# CSS color
|
||||
|
||||
[](https://github.com/asamuzaK/cssColor/actions/workflows/node.js.yml)
|
||||
[](https://github.com/asamuzaK/cssColor/actions/workflows/github-code-scanning/codeql)
|
||||
[](https://www.npmjs.com/package/@asamuzakjp/css-color)
|
||||
|
||||
Resolve and convert CSS colors.
|
||||
|
||||
## Install
|
||||
|
||||
```console
|
||||
npm i @asamuzakjp/css-color
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { convert, resolve, utils } from '@asamuzakjp/css-color';
|
||||
|
||||
const resolvedValue = resolve(
|
||||
'color-mix(in oklab, lch(67.5345 42.5 258.2), color(srgb 0 0.5 0))'
|
||||
);
|
||||
// 'oklab(0.620754 -0.0931934 -0.00374881)'
|
||||
|
||||
const convertedValue = covert.colorToHex('lab(46.2775% -47.5621 48.5837)');
|
||||
// '#008000'
|
||||
|
||||
const result = utils.isColor('green');
|
||||
// true
|
||||
```
|
||||
|
||||
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
|
||||
|
||||
### resolve(color, opt)
|
||||
|
||||
resolves CSS color
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `color` **[string][133]** color value
|
||||
- system colors are not supported
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.currentColor` **[string][133]?**
|
||||
- color to use for `currentcolor` keyword
|
||||
- if omitted, it will be treated as a missing color,
|
||||
i.e. `rgb(none none none / none)`
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties
|
||||
- pair of `--` prefixed property name as a key and it's value,
|
||||
e.g.
|
||||
```javascript
|
||||
const opt = {
|
||||
customProperty: {
|
||||
'--some-color': '#008000',
|
||||
'--some-length': '16px'
|
||||
}
|
||||
};
|
||||
```
|
||||
- and/or `callback` function to get the value of the custom property,
|
||||
e.g.
|
||||
```javascript
|
||||
const node = document.getElementById('foo');
|
||||
const opt = {
|
||||
customProperty: {
|
||||
callback: node.style.getPropertyValue
|
||||
}
|
||||
};
|
||||
```
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, e.g. for converting relative length to pixels
|
||||
- pair of unit as a key and number in pixels as it's value,
|
||||
e.g. suppose `1em === 12px`, `1rem === 16px` and `100vw === 1024px`, then
|
||||
```javascript
|
||||
const opt = {
|
||||
dimension: {
|
||||
em: 12,
|
||||
rem: 16,
|
||||
vw: 10.24
|
||||
}
|
||||
};
|
||||
```
|
||||
- and/or `callback` function to get the value as a number in pixels,
|
||||
e.g.
|
||||
```javascript
|
||||
const opt = {
|
||||
dimension: {
|
||||
callback: unit => {
|
||||
switch (unit) {
|
||||
case 'em':
|
||||
return 12;
|
||||
case 'rem':
|
||||
return 16;
|
||||
case 'vw':
|
||||
return 10.24;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
- `opt.format` **[string][133]?**
|
||||
- output format, one of below
|
||||
- `computedValue` (default), [computed value][139] of the color
|
||||
- `specifiedValue`, [specified value][140] of the color
|
||||
- `hex`, hex color notation, i.e. `#rrggbb`
|
||||
- `hexAlpha`, hex color notation with alpha channel, i.e. `#rrggbbaa`
|
||||
|
||||
Returns **[string][133]?** one of `rgba?()`, `#rrggbb(aa)?`, `color-name`, `color(color-space r g b / alpha)`, `color(color-space x y z / alpha)`, `(ok)?lab(l a b / alpha)`, `(ok)?lch(l c h / alpha)`, `'(empty-string)'`, `null`
|
||||
|
||||
- in `computedValue`, values are numbers, however `rgb()` values are integers
|
||||
- in `specifiedValue`, returns `empty string` for unknown and/or invalid color
|
||||
- in `hex`, returns `null` for `transparent`, and also returns `null` if any of `r`, `g`, `b`, `alpha` is not a number
|
||||
- in `hexAlpha`, returns `#00000000` for `transparent`, however returns `null` if any of `r`, `g`, `b`, `alpha` is not a number
|
||||
|
||||
### convert
|
||||
|
||||
Contains various color conversion functions.
|
||||
|
||||
### convert.numberToHex(value)
|
||||
|
||||
convert number to hex string
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[number][134]** color value
|
||||
|
||||
Returns **[string][133]** hex string: 00..ff
|
||||
|
||||
### convert.colorToHex(value, opt)
|
||||
|
||||
convert color to hex
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.alpha` **[boolean][136]?** return in #rrggbbaa notation
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[string][133]** #rrggbb(aa)?
|
||||
|
||||
### convert.colorToHsl(value, opt)
|
||||
|
||||
convert color to hsl
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[h, s, l, alpha]
|
||||
|
||||
### convert.colorToHwb(value, opt)
|
||||
|
||||
convert color to hwb
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[h, w, b, alpha]
|
||||
|
||||
### convert.colorToLab(value, opt)
|
||||
|
||||
convert color to lab
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[l, a, b, alpha]
|
||||
|
||||
### convert.colorToLch(value, opt)
|
||||
|
||||
convert color to lch
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[l, c, h, alpha]
|
||||
|
||||
### convert.colorToOklab(value, opt)
|
||||
|
||||
convert color to oklab
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[l, a, b, alpha]
|
||||
|
||||
### convert.colorToOklch(value, opt)
|
||||
|
||||
convert color to oklch
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[l, c, h, alpha]
|
||||
|
||||
### convert.colorToRgb(value, opt)
|
||||
|
||||
convert color to rgb
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[r, g, b, alpha]
|
||||
|
||||
### convert.colorToXyz(value, opt)
|
||||
|
||||
convert color to xyz
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
- `opt.d50` **[boolean][136]?** xyz in d50 white point
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[x, y, z, alpha]
|
||||
|
||||
### convert.colorToXyzD50(value, opt)
|
||||
|
||||
convert color to xyz-d50
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `value` **[string][133]** color value
|
||||
- `opt` **[object][135]?** options (optional, default `{}`)
|
||||
- `opt.customProperty` **[object][135]?**
|
||||
- custom properties, see `resolve()` function above
|
||||
- `opt.dimension` **[object][135]?**
|
||||
- dimension, see `resolve()` function above
|
||||
|
||||
Returns **[Array][137]<[number][134]>** \[x, y, z, alpha]
|
||||
|
||||
### utils
|
||||
|
||||
Contains utility functions.
|
||||
|
||||
### utils.isColor(color)
|
||||
|
||||
is valid color type
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `color` **[string][133]** color value
|
||||
- system colors are not supported
|
||||
|
||||
Returns **[boolean][136]**
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
The following resources have been of great help in the development of the CSS color.
|
||||
|
||||
- [csstools/postcss-plugins](https://github.com/csstools/postcss-plugins)
|
||||
- [lru-cache](https://github.com/isaacs/node-lru-cache)
|
||||
|
||||
---
|
||||
|
||||
Copyright (c) 2024 [asamuzaK (Kazz)](https://github.com/asamuzaK/)
|
||||
|
||||
[133]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
|
||||
[134]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
|
||||
[135]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
|
||||
[136]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
|
||||
[137]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
|
||||
[138]: https://w3c.github.io/csswg-drafts/css-color-4/#color-conversion-code
|
||||
[139]: https://developer.mozilla.org/en-US/docs/Web/CSS/computed_value
|
||||
[140]: https://developer.mozilla.org/en-US/docs/Web/CSS/specified_value
|
||||
[141]: https://www.npmjs.com/package/@csstools/css-calc
|
||||
@@ -1,15 +0,0 @@
|
||||
The ISC License
|
||||
|
||||
Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -1,331 +0,0 @@
|
||||
# lru-cache
|
||||
|
||||
A cache object that deletes the least-recently-used items.
|
||||
|
||||
Specify a max number of the most recently used items that you
|
||||
want to keep, and this cache will keep that many of the most
|
||||
recently accessed items.
|
||||
|
||||
This is not primarily a TTL cache, and does not make strong TTL
|
||||
guarantees. There is no preemptive pruning of expired items by
|
||||
default, but you _may_ set a TTL on the cache or on a single
|
||||
`set`. If you do so, it will treat expired items as missing, and
|
||||
delete them when fetched. If you are more interested in TTL
|
||||
caching than LRU caching, check out
|
||||
[@isaacs/ttlcache](http://npm.im/@isaacs/ttlcache).
|
||||
|
||||
As of version 7, this is one of the most performant LRU
|
||||
implementations available in JavaScript, and supports a wide
|
||||
diversity of use cases. However, note that using some of the
|
||||
features will necessarily impact performance, by causing the
|
||||
cache to have to do more work. See the "Performance" section
|
||||
below.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install lru-cache --save
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
// hybrid module, either works
|
||||
import { LRUCache } from 'lru-cache'
|
||||
// or:
|
||||
const { LRUCache } = require('lru-cache')
|
||||
// or in minified form for web browsers:
|
||||
import { LRUCache } from 'http://unpkg.com/lru-cache@9/dist/mjs/index.min.mjs'
|
||||
|
||||
// At least one of 'max', 'ttl', or 'maxSize' is required, to prevent
|
||||
// unsafe unbounded storage.
|
||||
//
|
||||
// In most cases, it's best to specify a max for performance, so all
|
||||
// the required memory allocation is done up-front.
|
||||
//
|
||||
// All the other options are optional, see the sections below for
|
||||
// documentation on what each one does. Most of them can be
|
||||
// overridden for specific items in get()/set()
|
||||
const options = {
|
||||
max: 500,
|
||||
|
||||
// for use with tracking overall storage size
|
||||
maxSize: 5000,
|
||||
sizeCalculation: (value, key) => {
|
||||
return 1
|
||||
},
|
||||
|
||||
// for use when you need to clean up something when objects
|
||||
// are evicted from the cache
|
||||
dispose: (value, key) => {
|
||||
freeFromMemoryOrWhatever(value)
|
||||
},
|
||||
|
||||
// how long to live in ms
|
||||
ttl: 1000 * 60 * 5,
|
||||
|
||||
// return stale items before removing from cache?
|
||||
allowStale: false,
|
||||
|
||||
updateAgeOnGet: false,
|
||||
updateAgeOnHas: false,
|
||||
|
||||
// async method to use for cache.fetch(), for
|
||||
// stale-while-revalidate type of behavior
|
||||
fetchMethod: async (
|
||||
key,
|
||||
staleValue,
|
||||
{ options, signal, context }
|
||||
) => {},
|
||||
}
|
||||
|
||||
const cache = new LRUCache(options)
|
||||
|
||||
cache.set('key', 'value')
|
||||
cache.get('key') // "value"
|
||||
|
||||
// non-string keys ARE fully supported
|
||||
// but note that it must be THE SAME object, not
|
||||
// just a JSON-equivalent object.
|
||||
var someObject = { a: 1 }
|
||||
cache.set(someObject, 'a value')
|
||||
// Object keys are not toString()-ed
|
||||
cache.set('[object Object]', 'a different value')
|
||||
assert.equal(cache.get(someObject), 'a value')
|
||||
// A similar object with same keys/values won't work,
|
||||
// because it's a different object identity
|
||||
assert.equal(cache.get({ a: 1 }), undefined)
|
||||
|
||||
cache.clear() // empty the cache
|
||||
```
|
||||
|
||||
If you put more stuff in the cache, then less recently used items
|
||||
will fall out. That's what an LRU cache is.
|
||||
|
||||
For full description of the API and all options, please see [the
|
||||
LRUCache typedocs](https://isaacs.github.io/node-lru-cache/)
|
||||
|
||||
## Storage Bounds Safety
|
||||
|
||||
This implementation aims to be as flexible as possible, within
|
||||
the limits of safe memory consumption and optimal performance.
|
||||
|
||||
At initial object creation, storage is allocated for `max` items.
|
||||
If `max` is set to zero, then some performance is lost, and item
|
||||
count is unbounded. Either `maxSize` or `ttl` _must_ be set if
|
||||
`max` is not specified.
|
||||
|
||||
If `maxSize` is set, then this creates a safe limit on the
|
||||
maximum storage consumed, but without the performance benefits of
|
||||
pre-allocation. When `maxSize` is set, every item _must_ provide
|
||||
a size, either via the `sizeCalculation` method provided to the
|
||||
constructor, or via a `size` or `sizeCalculation` option provided
|
||||
to `cache.set()`. The size of every item _must_ be a positive
|
||||
integer.
|
||||
|
||||
If neither `max` nor `maxSize` are set, then `ttl` tracking must
|
||||
be enabled. Note that, even when tracking item `ttl`, items are
|
||||
_not_ preemptively deleted when they become stale, unless
|
||||
`ttlAutopurge` is enabled. Instead, they are only purged the
|
||||
next time the key is requested. Thus, if `ttlAutopurge`, `max`,
|
||||
and `maxSize` are all not set, then the cache will potentially
|
||||
grow unbounded.
|
||||
|
||||
In this case, a warning is printed to standard error. Future
|
||||
versions may require the use of `ttlAutopurge` if `max` and
|
||||
`maxSize` are not specified.
|
||||
|
||||
If you truly wish to use a cache that is bound _only_ by TTL
|
||||
expiration, consider using a `Map` object, and calling
|
||||
`setTimeout` to delete entries when they expire. It will perform
|
||||
much better than an LRU cache.
|
||||
|
||||
Here is an implementation you may use, under the same
|
||||
[license](./LICENSE) as this package:
|
||||
|
||||
```js
|
||||
// a storage-unbounded ttl cache that is not an lru-cache
|
||||
const cache = {
|
||||
data: new Map(),
|
||||
timers: new Map(),
|
||||
set: (k, v, ttl) => {
|
||||
if (cache.timers.has(k)) {
|
||||
clearTimeout(cache.timers.get(k))
|
||||
}
|
||||
cache.timers.set(
|
||||
k,
|
||||
setTimeout(() => cache.delete(k), ttl)
|
||||
)
|
||||
cache.data.set(k, v)
|
||||
},
|
||||
get: k => cache.data.get(k),
|
||||
has: k => cache.data.has(k),
|
||||
delete: k => {
|
||||
if (cache.timers.has(k)) {
|
||||
clearTimeout(cache.timers.get(k))
|
||||
}
|
||||
cache.timers.delete(k)
|
||||
return cache.data.delete(k)
|
||||
},
|
||||
clear: () => {
|
||||
cache.data.clear()
|
||||
for (const v of cache.timers.values()) {
|
||||
clearTimeout(v)
|
||||
}
|
||||
cache.timers.clear()
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If that isn't to your liking, check out
|
||||
[@isaacs/ttlcache](http://npm.im/@isaacs/ttlcache).
|
||||
|
||||
## Storing Undefined Values
|
||||
|
||||
This cache never stores undefined values, as `undefined` is used
|
||||
internally in a few places to indicate that a key is not in the
|
||||
cache.
|
||||
|
||||
You may call `cache.set(key, undefined)`, but this is just
|
||||
an alias for `cache.delete(key)`. Note that this has the effect
|
||||
that `cache.has(key)` will return _false_ after setting it to
|
||||
undefined.
|
||||
|
||||
```js
|
||||
cache.set(myKey, undefined)
|
||||
cache.has(myKey) // false!
|
||||
```
|
||||
|
||||
If you need to track `undefined` values, and still note that the
|
||||
key is in the cache, an easy workaround is to use a sigil object
|
||||
of your own.
|
||||
|
||||
```js
|
||||
import { LRUCache } from 'lru-cache'
|
||||
const undefinedValue = Symbol('undefined')
|
||||
const cache = new LRUCache(...)
|
||||
const mySet = (key, value) =>
|
||||
cache.set(key, value === undefined ? undefinedValue : value)
|
||||
const myGet = (key, value) => {
|
||||
const v = cache.get(key)
|
||||
return v === undefinedValue ? undefined : v
|
||||
}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
As of January 2022, version 7 of this library is one of the most
|
||||
performant LRU cache implementations in JavaScript.
|
||||
|
||||
Benchmarks can be extremely difficult to get right. In
|
||||
particular, the performance of set/get/delete operations on
|
||||
objects will vary _wildly_ depending on the type of key used. V8
|
||||
is highly optimized for objects with keys that are short strings,
|
||||
especially integer numeric strings. Thus any benchmark which
|
||||
tests _solely_ using numbers as keys will tend to find that an
|
||||
object-based approach performs the best.
|
||||
|
||||
Note that coercing _anything_ to strings to use as object keys is
|
||||
unsafe, unless you can be 100% certain that no other type of
|
||||
value will be used. For example:
|
||||
|
||||
```js
|
||||
const myCache = {}
|
||||
const set = (k, v) => (myCache[k] = v)
|
||||
const get = k => myCache[k]
|
||||
|
||||
set({}, 'please hang onto this for me')
|
||||
set('[object Object]', 'oopsie')
|
||||
```
|
||||
|
||||
Also beware of "Just So" stories regarding performance. Garbage
|
||||
collection of large (especially: deep) object graphs can be
|
||||
incredibly costly, with several "tipping points" where it
|
||||
increases exponentially. As a result, putting that off until
|
||||
later can make it much worse, and less predictable. If a library
|
||||
performs well, but only in a scenario where the object graph is
|
||||
kept shallow, then that won't help you if you are using large
|
||||
objects as keys.
|
||||
|
||||
In general, when attempting to use a library to improve
|
||||
performance (such as a cache like this one), it's best to choose
|
||||
an option that will perform well in the sorts of scenarios where
|
||||
you'll actually use it.
|
||||
|
||||
This library is optimized for repeated gets and minimizing
|
||||
eviction time, since that is the expected need of a LRU. Set
|
||||
operations are somewhat slower on average than a few other
|
||||
options, in part because of that optimization. It is assumed
|
||||
that you'll be caching some costly operation, ideally as rarely
|
||||
as possible, so optimizing set over get would be unwise.
|
||||
|
||||
If performance matters to you:
|
||||
|
||||
1. If it's at all possible to use small integer values as keys,
|
||||
and you can guarantee that no other types of values will be
|
||||
used as keys, then do that, and use a cache such as
|
||||
[lru-fast](https://npmjs.com/package/lru-fast), or
|
||||
[mnemonist's
|
||||
LRUCache](https://yomguithereal.github.io/mnemonist/lru-cache)
|
||||
which uses an Object as its data store.
|
||||
|
||||
2. Failing that, if at all possible, use short non-numeric
|
||||
strings (ie, less than 256 characters) as your keys, and use
|
||||
[mnemonist's
|
||||
LRUCache](https://yomguithereal.github.io/mnemonist/lru-cache).
|
||||
|
||||
3. If the types of your keys will be anything else, especially
|
||||
long strings, strings that look like floats, objects, or some
|
||||
mix of types, or if you aren't sure, then this library will
|
||||
work well for you.
|
||||
|
||||
If you do not need the features that this library provides
|
||||
(like asynchronous fetching, a variety of TTL staleness
|
||||
options, and so on), then [mnemonist's
|
||||
LRUMap](https://yomguithereal.github.io/mnemonist/lru-map) is
|
||||
a very good option, and just slightly faster than this module
|
||||
(since it does considerably less).
|
||||
|
||||
4. Do not use a `dispose` function, size tracking, or especially
|
||||
ttl behavior, unless absolutely needed. These features are
|
||||
convenient, and necessary in some use cases, and every attempt
|
||||
has been made to make the performance impact minimal, but it
|
||||
isn't nothing.
|
||||
|
||||
## Breaking Changes in Version 7
|
||||
|
||||
This library changed to a different algorithm and internal data
|
||||
structure in version 7, yielding significantly better
|
||||
performance, albeit with some subtle changes as a result.
|
||||
|
||||
If you were relying on the internals of LRUCache in version 6 or
|
||||
before, it probably will not work in version 7 and above.
|
||||
|
||||
## Breaking Changes in Version 8
|
||||
|
||||
- The `fetchContext` option was renamed to `context`, and may no
|
||||
longer be set on the cache instance itself.
|
||||
- Rewritten in TypeScript, so pretty much all the types moved
|
||||
around a lot.
|
||||
- The AbortController/AbortSignal polyfill was removed. For this
|
||||
reason, **Node version 16.14.0 or higher is now required**.
|
||||
- Internal properties were moved to actual private class
|
||||
properties.
|
||||
- Keys and values must not be `null` or `undefined`.
|
||||
- Minified export available at `'lru-cache/min'`, for both CJS
|
||||
and MJS builds.
|
||||
|
||||
## Breaking Changes in Version 9
|
||||
|
||||
- Named export only, no default export.
|
||||
- AbortController polyfill returned, albeit with a warning when
|
||||
used.
|
||||
|
||||
## Breaking Changes in Version 10
|
||||
|
||||
- `cache.fetch()` return type is now `Promise<V | undefined>`
|
||||
instead of `Promise<V | void>`. This is an irrelevant change
|
||||
practically speaking, but can require changes for TypeScript
|
||||
users.
|
||||
|
||||
For more info, see the [change log](CHANGELOG.md).
|
||||
@@ -1,116 +0,0 @@
|
||||
{
|
||||
"name": "lru-cache",
|
||||
"publishConfig": {
|
||||
"tag": "legacy-v10"
|
||||
},
|
||||
"description": "A cache object that deletes the least-recently-used items.",
|
||||
"version": "10.4.3",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me>",
|
||||
"keywords": [
|
||||
"mru",
|
||||
"lru",
|
||||
"cache"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "npm run prepare",
|
||||
"prepare": "tshy && bash fixup.sh",
|
||||
"pretest": "npm run prepare",
|
||||
"presnap": "npm run prepare",
|
||||
"test": "tap",
|
||||
"snap": "tap",
|
||||
"preversion": "npm test",
|
||||
"postversion": "npm publish",
|
||||
"prepublishOnly": "git push origin --follow-tags",
|
||||
"format": "prettier --write .",
|
||||
"typedoc": "typedoc --tsconfig ./.tshy/esm.json ./src/*.ts",
|
||||
"benchmark-results-typedoc": "bash scripts/benchmark-results-typedoc.sh",
|
||||
"prebenchmark": "npm run prepare",
|
||||
"benchmark": "make -C benchmark",
|
||||
"preprofile": "npm run prepare",
|
||||
"profile": "make -C benchmark profile"
|
||||
},
|
||||
"main": "./dist/commonjs/index.js",
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"tshy": {
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./min": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.min.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"default": "./dist/commonjs/index.min.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/isaacs/node-lru-cache.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/tap": "^15.0.6",
|
||||
"benchmark": "^2.1.4",
|
||||
"esbuild": "^0.17.11",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"marked": "^4.2.12",
|
||||
"mkdirp": "^2.1.5",
|
||||
"prettier": "^2.6.2",
|
||||
"tap": "^20.0.3",
|
||||
"tshy": "^2.0.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typedoc": "^0.25.3",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"license": "ISC",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 70,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"bracketSameLine": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf"
|
||||
},
|
||||
"tap": {
|
||||
"node-arg": [
|
||||
"--expose-gc"
|
||||
],
|
||||
"plugin": [
|
||||
"@tapjs/clock"
|
||||
]
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"default": "./dist/commonjs/index.js"
|
||||
}
|
||||
},
|
||||
"./min": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.min.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"default": "./dist/commonjs/index.min.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "module",
|
||||
"module": "./dist/esm/index.js"
|
||||
}
|
||||
81
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/package.json
generated
vendored
81
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/package.json
generated
vendored
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"name": "@asamuzakjp/css-color",
|
||||
"description": "CSS color - Resolve and convert CSS colors.",
|
||||
"author": "asamuzaK",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/asamuzaK/cssColor.git"
|
||||
},
|
||||
"homepage": "https://github.com/asamuzaK/cssColor#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/asamuzaK/cssColor/issues"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/index.d.cts",
|
||||
"default": "./dist/cjs/index.cjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/vite-config": "^0.2.0",
|
||||
"@vitest/coverage-istanbul": "^3.1.4",
|
||||
"esbuild": "^0.25.4",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-regexp": "^2.7.0",
|
||||
"globals": "^16.1.0",
|
||||
"knip": "^5.56.0",
|
||||
"neostandard": "^0.12.1",
|
||||
"prettier": "^3.5.3",
|
||||
"publint": "^0.3.12",
|
||||
"rimraf": "^6.0.1",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"unrs-resolver"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm run clean && pnpm run test && pnpm run knip && pnpm run build:prod && pnpm run build:cjs && pnpm run build:browser && pnpm run publint",
|
||||
"build:browser": "vite build -c ./vite.browser.config.ts",
|
||||
"build:prod": "vite build",
|
||||
"build:cjs": "tsup ./src/index.ts --format=cjs --platform=node --outDir=./dist/cjs/ --sourcemap --dts",
|
||||
"clean": "rimraf ./coverage ./dist",
|
||||
"knip": "knip",
|
||||
"prettier": "prettier . --ignore-unknown --write",
|
||||
"publint": "publint --strict",
|
||||
"test": "pnpm run prettier && pnpm run --stream \"/^test:.*/\"",
|
||||
"test:eslint": "eslint ./src ./test --fix",
|
||||
"test:types": "tsc",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"version": "3.2.0"
|
||||
}
|
||||
27
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/index.ts
generated
vendored
27
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/index.ts
generated
vendored
@@ -1,27 +0,0 @@
|
||||
/*!
|
||||
* CSS color - Resolve, parse, convert CSS color.
|
||||
* @license MIT
|
||||
* @copyright asamuzaK (Kazz)
|
||||
* @see {@link https://github.com/asamuzaK/cssColor/blob/main/LICENSE}
|
||||
*/
|
||||
|
||||
import { cssCalc as csscalc } from './js/css-calc';
|
||||
import { isGradient } from './js/css-gradient';
|
||||
import { cssVar } from './js/css-var';
|
||||
import { extractDashedIdent, isColor as iscolor, splitValue } from './js/util';
|
||||
|
||||
export { convert } from './js/convert';
|
||||
export { resolve } from './js/resolve';
|
||||
/* utils */
|
||||
export const utils = {
|
||||
cssCalc: csscalc,
|
||||
cssVar,
|
||||
extractDashedIdent,
|
||||
isColor: iscolor,
|
||||
isGradient,
|
||||
splitValue
|
||||
};
|
||||
/* TODO: remove later */
|
||||
/* alias */
|
||||
export const isColor = utils.isColor;
|
||||
export const cssCalc = utils.cssCalc;
|
||||
114
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/cache.ts
generated
vendored
114
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/cache.ts
generated
vendored
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* cache
|
||||
*/
|
||||
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { Options } from './typedef';
|
||||
import { valueToJsonString } from './util';
|
||||
|
||||
/* numeric constants */
|
||||
const MAX_CACHE = 4096;
|
||||
|
||||
/**
|
||||
* CacheItem
|
||||
*/
|
||||
export class CacheItem {
|
||||
/* private */
|
||||
#isNull: boolean;
|
||||
#item: unknown;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*/
|
||||
constructor(item: unknown, isNull: boolean = false) {
|
||||
this.#item = item;
|
||||
this.#isNull = !!isNull;
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this.#item;
|
||||
}
|
||||
|
||||
get isNull() {
|
||||
return this.#isNull;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NullObject
|
||||
*/
|
||||
export class NullObject extends CacheItem {
|
||||
/**
|
||||
* constructor
|
||||
*/
|
||||
constructor() {
|
||||
super(Symbol('null'), true);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* lru cache
|
||||
*/
|
||||
export const lruCache = new LRUCache({
|
||||
max: MAX_CACHE
|
||||
});
|
||||
|
||||
/**
|
||||
* set cache
|
||||
* @param key - cache key
|
||||
* @param value - value to cache
|
||||
* @returns void
|
||||
*/
|
||||
export const setCache = (key: string, value: unknown): void => {
|
||||
if (key) {
|
||||
if (value === null) {
|
||||
lruCache.set(key, new NullObject());
|
||||
} else if (value instanceof CacheItem) {
|
||||
lruCache.set(key, value);
|
||||
} else {
|
||||
lruCache.set(key, new CacheItem(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* get cache
|
||||
* @param key - cache key
|
||||
* @returns cached item or false otherwise
|
||||
*/
|
||||
export const getCache = (key: string): CacheItem | boolean => {
|
||||
if (key && lruCache.has(key)) {
|
||||
const item = lruCache.get(key);
|
||||
if (item instanceof CacheItem) {
|
||||
return item;
|
||||
}
|
||||
// delete unexpected cached item
|
||||
lruCache.delete(key);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* create cache key
|
||||
* @param keyData - key data
|
||||
* @param [opt] - options
|
||||
* @returns cache key
|
||||
*/
|
||||
export const createCacheKey = (
|
||||
keyData: Record<string, string>,
|
||||
opt: Options = {}
|
||||
): string => {
|
||||
const { customProperty = {}, dimension = {} } = opt;
|
||||
let cacheKey = '';
|
||||
if (
|
||||
keyData &&
|
||||
Object.keys(keyData).length &&
|
||||
typeof customProperty.callback !== 'function' &&
|
||||
typeof dimension.callback !== 'function'
|
||||
) {
|
||||
keyData.opt = valueToJsonString(opt);
|
||||
cacheKey = valueToJsonString(keyData);
|
||||
}
|
||||
return cacheKey;
|
||||
};
|
||||
3459
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/color.ts
generated
vendored
3459
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/color.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
31
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/common.ts
generated
vendored
31
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/common.ts
generated
vendored
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* common
|
||||
*/
|
||||
|
||||
/* numeric constants */
|
||||
const TYPE_FROM = 8;
|
||||
const TYPE_TO = -1;
|
||||
|
||||
/**
|
||||
* get type
|
||||
* @param o - object to check
|
||||
* @returns type of object
|
||||
*/
|
||||
export const getType = (o: unknown): string =>
|
||||
Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO);
|
||||
|
||||
/**
|
||||
* is string
|
||||
* @param o - object to check
|
||||
* @returns result
|
||||
*/
|
||||
export const isString = (o: unknown): o is string =>
|
||||
typeof o === 'string' || o instanceof String;
|
||||
|
||||
/**
|
||||
* is string or number
|
||||
* @param o - object to check
|
||||
* @returns result
|
||||
*/
|
||||
export const isStringOrNumber = (o: unknown): boolean =>
|
||||
isString(o) || typeof o === 'number';
|
||||
66
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/constant.ts
generated
vendored
66
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/constant.ts
generated
vendored
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* constant
|
||||
*/
|
||||
|
||||
/* values and units */
|
||||
const _DIGIT = '(?:0|[1-9]\\d*)';
|
||||
const _COMPARE = 'clamp|max|min';
|
||||
const _EXPO = 'exp|hypot|log|pow|sqrt';
|
||||
const _SIGN = 'abs|sign';
|
||||
const _STEP = 'mod|rem|round';
|
||||
const _TRIG = 'a?(?:cos|sin|tan)|atan2';
|
||||
const _MATH = `${_COMPARE}|${_EXPO}|${_SIGN}|${_STEP}|${_TRIG}`;
|
||||
const _CALC = `calc|${_MATH}`;
|
||||
const _VAR = `var|${_CALC}`;
|
||||
export const ANGLE = 'deg|g?rad|turn';
|
||||
export const LENGTH =
|
||||
'[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic)';
|
||||
export const NUM = `[+-]?(?:${_DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${_DIGIT})?`;
|
||||
export const NUM_POSITIVE = `\\+?(?:${_DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${_DIGIT})?`;
|
||||
export const NONE = 'none';
|
||||
export const PCT = `${NUM}%`;
|
||||
export const SYN_FN_CALC = `^(?:${_CALC})\\(|(?<=[*\\/\\s\\(])(?:${_CALC})\\(`;
|
||||
export const SYN_FN_MATH_START = `^(?:${_MATH})\\($`;
|
||||
export const SYN_FN_VAR = '^var\\(|(?<=[*\\/\\s\\(])var\\(';
|
||||
export const SYN_FN_VAR_START = `^(?:${_VAR})\\(`;
|
||||
|
||||
/* colors */
|
||||
const _ALPHA = `(?:\\s*\\/\\s*(?:${NUM}|${PCT}|${NONE}))?`;
|
||||
const _ALPHA_LV3 = `(?:\\s*,\\s*(?:${NUM}|${PCT}))?`;
|
||||
const _COLOR_FUNC = '(?:ok)?l(?:ab|ch)|color|hsla?|hwb|rgba?';
|
||||
const _COLOR_KEY = '[a-z]+|#[\\da-f]{3}|#[\\da-f]{4}|#[\\da-f]{6}|#[\\da-f]{8}';
|
||||
const _CS_HUE = '(?:ok)?lch|hsl|hwb';
|
||||
const _CS_HUE_ARC = '(?:de|in)creasing|longer|shorter';
|
||||
const _NUM_ANGLE = `${NUM}(?:${ANGLE})?`;
|
||||
const _NUM_ANGLE_NONE = `(?:${NUM}(?:${ANGLE})?|${NONE})`;
|
||||
const _NUM_PCT_NONE = `(?:${NUM}|${PCT}|${NONE})`;
|
||||
export const CS_HUE = `(?:${_CS_HUE})(?:\\s(?:${_CS_HUE_ARC})\\shue)?`;
|
||||
export const CS_HUE_CAPT = `(${_CS_HUE})(?:\\s(${_CS_HUE_ARC})\\shue)?`;
|
||||
export const CS_LAB = '(?:ok)?lab';
|
||||
export const CS_LCH = '(?:ok)?lch';
|
||||
export const CS_SRGB = 'srgb(?:-linear)?';
|
||||
export const CS_RGB = `(?:a98|prophoto)-rgb|display-p3|rec2020|${CS_SRGB}`;
|
||||
export const CS_XYZ = 'xyz(?:-d(?:50|65))?';
|
||||
export const CS_RECT = `${CS_LAB}|${CS_RGB}|${CS_XYZ}`;
|
||||
export const CS_MIX = `${CS_HUE}|${CS_RECT}`;
|
||||
export const FN_COLOR = 'color(';
|
||||
export const FN_MIX = 'color-mix(';
|
||||
export const FN_REL = `(?:${_COLOR_FUNC})\\(\\s*from\\s+`;
|
||||
export const FN_REL_CAPT = `(${_COLOR_FUNC})\\(\\s*from\\s+`;
|
||||
export const FN_VAR = 'var(';
|
||||
export const SYN_FN_COLOR = `(?:${CS_RGB}|${CS_XYZ})(?:\\s+${_NUM_PCT_NONE}){3}${_ALPHA}`;
|
||||
export const SYN_FN_REL = `^${FN_REL}|(?<=[\\s])${FN_REL}`;
|
||||
export const SYN_HSL = `${_NUM_ANGLE_NONE}(?:\\s+${_NUM_PCT_NONE}){2}${_ALPHA}`;
|
||||
export const SYN_HSL_LV3 = `${_NUM_ANGLE}(?:\\s*,\\s*${PCT}){2}${_ALPHA_LV3}`;
|
||||
export const SYN_LCH = `(?:${_NUM_PCT_NONE}\\s+){2}${_NUM_ANGLE_NONE}${_ALPHA}`;
|
||||
export const SYN_MOD = `${_NUM_PCT_NONE}(?:\\s+${_NUM_PCT_NONE}){2}${_ALPHA}`;
|
||||
export const SYN_RGB_LV3 = `(?:${NUM}(?:\\s*,\\s*${NUM}){2}|${PCT}(?:\\s*,\\s*${PCT}){2})${_ALPHA_LV3}`;
|
||||
export const SYN_COLOR_TYPE = `${_COLOR_KEY}|hsla?\\(\\s*${SYN_HSL_LV3}\\s*\\)|rgba?\\(\\s*${SYN_RGB_LV3}\\s*\\)|(?:hsla?|hwb)\\(\\s*${SYN_HSL}\\s*\\)|(?:(?:ok)?lab|rgba?)\\(\\s*${SYN_MOD}\\s*\\)|(?:ok)?lch\\(\\s*${SYN_LCH}\\s*\\)|color\\(\\s*${SYN_FN_COLOR}\\s*\\)`;
|
||||
export const SYN_MIX_PART = `(?:${SYN_COLOR_TYPE})(?:\\s+${PCT})?`;
|
||||
export const SYN_MIX = `color-mix\\(\\s*in\\s+(?:${CS_MIX})\\s*,\\s*${SYN_MIX_PART}\\s*,\\s*${SYN_MIX_PART}\\s*\\)`;
|
||||
export const SYN_MIX_CAPT = `color-mix\\(\\s*in\\s+(${CS_MIX})\\s*,\\s*(${SYN_MIX_PART})\\s*,\\s*(${SYN_MIX_PART})\\s*\\)`;
|
||||
|
||||
/* formats */
|
||||
export const VAL_COMP = 'computedValue';
|
||||
export const VAL_MIX = 'mixValue';
|
||||
export const VAL_SPEC = 'specifiedValue';
|
||||
469
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/convert.ts
generated
vendored
469
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/convert.ts
generated
vendored
@@ -1,469 +0,0 @@
|
||||
/**
|
||||
* convert
|
||||
*/
|
||||
|
||||
import {
|
||||
CacheItem,
|
||||
NullObject,
|
||||
createCacheKey,
|
||||
getCache,
|
||||
setCache
|
||||
} from './cache';
|
||||
import {
|
||||
convertColorToHsl,
|
||||
convertColorToHwb,
|
||||
convertColorToLab,
|
||||
convertColorToLch,
|
||||
convertColorToOklab,
|
||||
convertColorToOklch,
|
||||
convertColorToRgb,
|
||||
numberToHexString,
|
||||
parseColorFunc,
|
||||
parseColorValue
|
||||
} from './color';
|
||||
import { isString } from './common';
|
||||
import { cssCalc } from './css-calc';
|
||||
import { resolveVar } from './css-var';
|
||||
import { resolveRelativeColor } from './relative-color';
|
||||
import { resolveColor } from './resolve';
|
||||
import { ColorChannels, ComputedColorChannels, Options } from './typedef';
|
||||
|
||||
/* constants */
|
||||
import { SYN_FN_CALC, SYN_FN_REL, SYN_FN_VAR, VAL_COMP } from './constant';
|
||||
const NAMESPACE = 'convert';
|
||||
|
||||
/* regexp */
|
||||
const REG_FN_CALC = new RegExp(SYN_FN_CALC);
|
||||
const REG_FN_REL = new RegExp(SYN_FN_REL);
|
||||
const REG_FN_VAR = new RegExp(SYN_FN_VAR);
|
||||
|
||||
/**
|
||||
* pre process
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns value
|
||||
*/
|
||||
export const preProcess = (
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): string | NullObject => {
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
if (!value) {
|
||||
return new NullObject();
|
||||
}
|
||||
} else {
|
||||
return new NullObject();
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'preProcess',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
if (cachedResult.isNull) {
|
||||
return cachedResult as NullObject;
|
||||
}
|
||||
return cachedResult.item as string;
|
||||
}
|
||||
if (REG_FN_VAR.test(value)) {
|
||||
const resolvedValue = resolveVar(value, opt);
|
||||
if (isString(resolvedValue)) {
|
||||
value = resolvedValue;
|
||||
} else {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
}
|
||||
if (REG_FN_REL.test(value)) {
|
||||
const resolvedValue = resolveRelativeColor(value, opt);
|
||||
if (isString(resolvedValue)) {
|
||||
value = resolvedValue;
|
||||
} else {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
} else if (REG_FN_CALC.test(value)) {
|
||||
value = cssCalc(value, opt);
|
||||
}
|
||||
if (value.startsWith('color-mix')) {
|
||||
const clonedOpt = structuredClone(opt);
|
||||
clonedOpt.format = VAL_COMP;
|
||||
clonedOpt.nullable = true;
|
||||
const resolvedValue = resolveColor(value, clonedOpt);
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
}
|
||||
setCache(cacheKey, value);
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert number to hex string
|
||||
* @param value - numeric value
|
||||
* @returns hex string: 00..ff
|
||||
*/
|
||||
export const numberToHex = (value: number): string => {
|
||||
const hex = numberToHexString(value);
|
||||
return hex;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to hex
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @param [opt.alpha] - enable alpha channel
|
||||
* @returns #rrggbb | #rrggbbaa | null
|
||||
*/
|
||||
export const colorToHex = (value: string, opt: Options = {}): string | null => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return null;
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const { alpha = false } = opt;
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToHex',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
if (cachedResult.isNull) {
|
||||
return null;
|
||||
}
|
||||
return cachedResult.item as string;
|
||||
}
|
||||
let hex;
|
||||
opt.nullable = true;
|
||||
if (alpha) {
|
||||
opt.format = 'hexAlpha';
|
||||
hex = resolveColor(value, opt);
|
||||
} else {
|
||||
opt.format = 'hex';
|
||||
hex = resolveColor(value, opt);
|
||||
}
|
||||
if (isString(hex)) {
|
||||
setCache(cacheKey, hex);
|
||||
return hex;
|
||||
}
|
||||
setCache(cacheKey, null);
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to hsl
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [h, s, l, alpha]
|
||||
*/
|
||||
export const colorToHsl = (value: string, opt: Options = {}): ColorChannels => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToHsl',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as ColorChannels;
|
||||
}
|
||||
opt.format = 'hsl';
|
||||
const hsl = convertColorToHsl(value, opt) as ColorChannels;
|
||||
setCache(cacheKey, hsl);
|
||||
return hsl;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to hwb
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [h, w, b, alpha]
|
||||
*/
|
||||
export const colorToHwb = (value: string, opt: Options = {}): ColorChannels => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToHwb',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as ColorChannels;
|
||||
}
|
||||
opt.format = 'hwb';
|
||||
const hwb = convertColorToHwb(value, opt) as ColorChannels;
|
||||
setCache(cacheKey, hwb);
|
||||
return hwb;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to lab
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [l, a, b, alpha]
|
||||
*/
|
||||
export const colorToLab = (value: string, opt: Options = {}): ColorChannels => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToLab',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as ColorChannels;
|
||||
}
|
||||
const lab = convertColorToLab(value, opt) as ColorChannels;
|
||||
setCache(cacheKey, lab);
|
||||
return lab;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to lch
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [l, c, h, alpha]
|
||||
*/
|
||||
export const colorToLch = (value: string, opt: Options = {}): ColorChannels => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToLch',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as ColorChannels;
|
||||
}
|
||||
const lch = convertColorToLch(value, opt) as ColorChannels;
|
||||
setCache(cacheKey, lch);
|
||||
return lch;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to oklab
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [l, a, b, alpha]
|
||||
*/
|
||||
export const colorToOklab = (
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): ColorChannels => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToOklab',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as ColorChannels;
|
||||
}
|
||||
const lab = convertColorToOklab(value, opt) as ColorChannels;
|
||||
setCache(cacheKey, lab);
|
||||
return lab;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to oklch
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [l, c, h, alpha]
|
||||
*/
|
||||
export const colorToOklch = (
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): ColorChannels => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToOklch',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as ColorChannels;
|
||||
}
|
||||
const lch = convertColorToOklch(value, opt) as ColorChannels;
|
||||
setCache(cacheKey, lch);
|
||||
return lch;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to rgb
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [r, g, b, alpha]
|
||||
*/
|
||||
export const colorToRgb = (value: string, opt: Options = {}): ColorChannels => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToRgb',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as ColorChannels;
|
||||
}
|
||||
const rgb = convertColorToRgb(value, opt) as ColorChannels;
|
||||
setCache(cacheKey, rgb);
|
||||
return rgb;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to xyz
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [x, y, z, alpha]
|
||||
*/
|
||||
export const colorToXyz = (value: string, opt: Options = {}): ColorChannels => {
|
||||
if (isString(value)) {
|
||||
const resolvedValue = preProcess(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
value = resolvedValue.toLowerCase();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'colorToXyz',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as ColorChannels;
|
||||
}
|
||||
let xyz;
|
||||
if (value.startsWith('color(')) {
|
||||
[, ...xyz] = parseColorFunc(value, opt) as ComputedColorChannels;
|
||||
} else {
|
||||
[, ...xyz] = parseColorValue(value, opt) as ComputedColorChannels;
|
||||
}
|
||||
setCache(cacheKey, xyz);
|
||||
return xyz as ColorChannels;
|
||||
};
|
||||
|
||||
/**
|
||||
* convert color to xyz-d50
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns ColorChannels - [x, y, z, alpha]
|
||||
*/
|
||||
export const colorToXyzD50 = (
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): ColorChannels => {
|
||||
opt.d50 = true;
|
||||
return colorToXyz(value, opt);
|
||||
};
|
||||
|
||||
/* convert */
|
||||
export const convert = {
|
||||
colorToHex,
|
||||
colorToHsl,
|
||||
colorToHwb,
|
||||
colorToLab,
|
||||
colorToLch,
|
||||
colorToOklab,
|
||||
colorToOklch,
|
||||
colorToRgb,
|
||||
colorToXyz,
|
||||
colorToXyzD50,
|
||||
numberToHex
|
||||
};
|
||||
965
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/css-calc.ts
generated
vendored
965
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/css-calc.ts
generated
vendored
@@ -1,965 +0,0 @@
|
||||
/**
|
||||
* css-calc
|
||||
*/
|
||||
|
||||
import { calc } from '@csstools/css-calc';
|
||||
import { CSSToken, TokenType, tokenize } from '@csstools/css-tokenizer';
|
||||
import {
|
||||
CacheItem,
|
||||
NullObject,
|
||||
createCacheKey,
|
||||
getCache,
|
||||
setCache
|
||||
} from './cache';
|
||||
import { isString, isStringOrNumber } from './common';
|
||||
import { resolveVar } from './css-var';
|
||||
import { roundToPrecision } from './util';
|
||||
import { MatchedRegExp, Options } from './typedef';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
ANGLE,
|
||||
LENGTH,
|
||||
NUM,
|
||||
SYN_FN_CALC,
|
||||
SYN_FN_MATH_START,
|
||||
SYN_FN_VAR,
|
||||
SYN_FN_VAR_START,
|
||||
VAL_SPEC
|
||||
} from './constant';
|
||||
const {
|
||||
CloseParen: PAREN_CLOSE,
|
||||
Comment: COMMENT,
|
||||
Dimension: DIM,
|
||||
EOF,
|
||||
Function: FUNC,
|
||||
OpenParen: PAREN_OPEN,
|
||||
Whitespace: W_SPACE
|
||||
} = TokenType;
|
||||
const NAMESPACE = 'css-calc';
|
||||
|
||||
/* numeric constants */
|
||||
const TRIA = 3;
|
||||
const HEX = 16;
|
||||
const MAX_PCT = 100;
|
||||
|
||||
/* regexp */
|
||||
const REG_FN_CALC = new RegExp(SYN_FN_CALC);
|
||||
const REG_FN_CALC_NUM = new RegExp(`^calc\\((${NUM})\\)$`);
|
||||
const REG_FN_MATH_START = new RegExp(SYN_FN_MATH_START);
|
||||
const REG_FN_VAR = new RegExp(SYN_FN_VAR);
|
||||
const REG_FN_VAR_START = new RegExp(SYN_FN_VAR_START);
|
||||
const REG_OPERATOR = /\s[*+/-]\s/;
|
||||
const REG_TYPE_DIM = new RegExp(`^(${NUM})(${ANGLE}|${LENGTH})$`);
|
||||
const REG_TYPE_DIM_PCT = new RegExp(`^(${NUM})(${ANGLE}|${LENGTH}|%)$`);
|
||||
const REG_TYPE_PCT = new RegExp(`^(${NUM})%$`);
|
||||
|
||||
/**
|
||||
* Calclator
|
||||
*/
|
||||
export class Calculator {
|
||||
/* private */
|
||||
// number
|
||||
#hasNum: boolean;
|
||||
#numSum: number[];
|
||||
#numMul: number[];
|
||||
// percentage
|
||||
#hasPct: boolean;
|
||||
#pctSum: number[];
|
||||
#pctMul: number[];
|
||||
// dimension
|
||||
#hasDim: boolean;
|
||||
#dimSum: string[];
|
||||
#dimSub: string[];
|
||||
#dimMul: string[];
|
||||
#dimDiv: string[];
|
||||
// et cetra
|
||||
#hasEtc: boolean;
|
||||
#etcSum: string[];
|
||||
#etcSub: string[];
|
||||
#etcMul: string[];
|
||||
#etcDiv: string[];
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*/
|
||||
constructor() {
|
||||
// number
|
||||
this.#hasNum = false;
|
||||
this.#numSum = [];
|
||||
this.#numMul = [];
|
||||
// percentage
|
||||
this.#hasPct = false;
|
||||
this.#pctSum = [];
|
||||
this.#pctMul = [];
|
||||
// dimension
|
||||
this.#hasDim = false;
|
||||
this.#dimSum = [];
|
||||
this.#dimSub = [];
|
||||
this.#dimMul = [];
|
||||
this.#dimDiv = [];
|
||||
// et cetra
|
||||
this.#hasEtc = false;
|
||||
this.#etcSum = [];
|
||||
this.#etcSub = [];
|
||||
this.#etcMul = [];
|
||||
this.#etcDiv = [];
|
||||
}
|
||||
|
||||
get hasNum() {
|
||||
return this.#hasNum;
|
||||
}
|
||||
|
||||
set hasNum(value: boolean) {
|
||||
this.#hasNum = !!value;
|
||||
}
|
||||
|
||||
get numSum() {
|
||||
return this.#numSum;
|
||||
}
|
||||
|
||||
get numMul() {
|
||||
return this.#numMul;
|
||||
}
|
||||
|
||||
get hasPct() {
|
||||
return this.#hasPct;
|
||||
}
|
||||
|
||||
set hasPct(value: boolean) {
|
||||
this.#hasPct = !!value;
|
||||
}
|
||||
|
||||
get pctSum() {
|
||||
return this.#pctSum;
|
||||
}
|
||||
|
||||
get pctMul() {
|
||||
return this.#pctMul;
|
||||
}
|
||||
|
||||
get hasDim() {
|
||||
return this.#hasDim;
|
||||
}
|
||||
|
||||
set hasDim(value: boolean) {
|
||||
this.#hasDim = !!value;
|
||||
}
|
||||
|
||||
get dimSum() {
|
||||
return this.#dimSum;
|
||||
}
|
||||
|
||||
get dimSub() {
|
||||
return this.#dimSub;
|
||||
}
|
||||
|
||||
get dimMul() {
|
||||
return this.#dimMul;
|
||||
}
|
||||
|
||||
get dimDiv() {
|
||||
return this.#dimDiv;
|
||||
}
|
||||
|
||||
get hasEtc() {
|
||||
return this.#hasEtc;
|
||||
}
|
||||
|
||||
set hasEtc(value: boolean) {
|
||||
this.#hasEtc = !!value;
|
||||
}
|
||||
|
||||
get etcSum() {
|
||||
return this.#etcSum;
|
||||
}
|
||||
|
||||
get etcSub() {
|
||||
return this.#etcSub;
|
||||
}
|
||||
|
||||
get etcMul() {
|
||||
return this.#etcMul;
|
||||
}
|
||||
|
||||
get etcDiv() {
|
||||
return this.#etcDiv;
|
||||
}
|
||||
|
||||
/**
|
||||
* clear values
|
||||
* @returns void
|
||||
*/
|
||||
clear() {
|
||||
// number
|
||||
this.#hasNum = false;
|
||||
this.#numSum = [];
|
||||
this.#numMul = [];
|
||||
// percentage
|
||||
this.#hasPct = false;
|
||||
this.#pctSum = [];
|
||||
this.#pctMul = [];
|
||||
// dimension
|
||||
this.#hasDim = false;
|
||||
this.#dimSum = [];
|
||||
this.#dimSub = [];
|
||||
this.#dimMul = [];
|
||||
this.#dimDiv = [];
|
||||
// et cetra
|
||||
this.#hasEtc = false;
|
||||
this.#etcSum = [];
|
||||
this.#etcSub = [];
|
||||
this.#etcMul = [];
|
||||
this.#etcDiv = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* sort values
|
||||
* @param values - values
|
||||
* @returns sorted values
|
||||
*/
|
||||
sort(values: string[] = []): string[] {
|
||||
const arr = [...values];
|
||||
if (arr.length > 1) {
|
||||
arr.sort((a, b) => {
|
||||
let res;
|
||||
if (REG_TYPE_DIM_PCT.test(a) && REG_TYPE_DIM_PCT.test(b)) {
|
||||
const [, valA, unitA] = a.match(REG_TYPE_DIM_PCT) as MatchedRegExp;
|
||||
const [, valB, unitB] = b.match(REG_TYPE_DIM_PCT) as MatchedRegExp;
|
||||
if (unitA === unitB) {
|
||||
if (Number(valA) === Number(valB)) {
|
||||
res = 0;
|
||||
} else if (Number(valA) > Number(valB)) {
|
||||
res = 1;
|
||||
} else {
|
||||
res = -1;
|
||||
}
|
||||
} else if (unitA > unitB) {
|
||||
res = 1;
|
||||
} else {
|
||||
res = -1;
|
||||
}
|
||||
} else {
|
||||
if (a === b) {
|
||||
res = 0;
|
||||
} else if (a > b) {
|
||||
res = 1;
|
||||
} else {
|
||||
res = -1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* multiply values
|
||||
* @returns resolved value
|
||||
*/
|
||||
multiply(): string {
|
||||
const value = [];
|
||||
let num;
|
||||
if (this.#hasNum) {
|
||||
num = 1;
|
||||
for (const i of this.#numMul) {
|
||||
num *= i;
|
||||
if (num === 0 || !Number.isFinite(num) || Number.isNaN(num)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.#hasPct && !this.#hasDim && !this.hasEtc) {
|
||||
if (Number.isFinite(num)) {
|
||||
num = roundToPrecision(num, HEX);
|
||||
}
|
||||
value.push(num);
|
||||
}
|
||||
}
|
||||
if (this.#hasPct) {
|
||||
if (typeof num !== 'number') {
|
||||
num = 1;
|
||||
}
|
||||
for (const i of this.#pctMul) {
|
||||
num *= i;
|
||||
if (num === 0 || !Number.isFinite(num) || Number.isNaN(num)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Number.isFinite(num)) {
|
||||
num = `${roundToPrecision(num, HEX)}%`;
|
||||
}
|
||||
if (!this.#hasDim && !this.hasEtc) {
|
||||
value.push(num);
|
||||
}
|
||||
}
|
||||
if (this.#hasDim) {
|
||||
let dim = '';
|
||||
let mul = '';
|
||||
let div = '';
|
||||
if (this.#dimMul.length) {
|
||||
if (this.#dimMul.length === 1) {
|
||||
[mul] = this.#dimMul as [string];
|
||||
} else {
|
||||
mul = `${this.sort(this.#dimMul).join(' * ')}`;
|
||||
}
|
||||
}
|
||||
if (this.#dimDiv.length) {
|
||||
if (this.#dimDiv.length === 1) {
|
||||
[div] = this.#dimDiv as [string];
|
||||
} else {
|
||||
div = `${this.sort(this.#dimDiv).join(' * ')}`;
|
||||
}
|
||||
}
|
||||
if (Number.isFinite(num)) {
|
||||
if (mul) {
|
||||
if (div) {
|
||||
if (div.includes('*')) {
|
||||
dim = calc(`calc(${num} * ${mul} / (${div}))`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
} else {
|
||||
dim = calc(`calc(${num} * ${mul} / ${div})`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dim = calc(`calc(${num} * ${mul})`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
} else if (div.includes('*')) {
|
||||
dim = calc(`calc(${num} / (${div}))`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
} else {
|
||||
dim = calc(`calc(${num} / ${div})`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
value.push(dim.replace(/^calc/, ''));
|
||||
} else {
|
||||
if (!value.length && num !== undefined) {
|
||||
value.push(num);
|
||||
}
|
||||
if (mul) {
|
||||
if (div) {
|
||||
if (div.includes('*')) {
|
||||
dim = calc(`calc(${mul} / (${div}))`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
} else {
|
||||
dim = calc(`calc(${mul} / ${div})`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dim = calc(`calc(${mul})`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
if (value.length) {
|
||||
value.push('*', dim.replace(/^calc/, ''));
|
||||
} else {
|
||||
value.push(dim.replace(/^calc/, ''));
|
||||
}
|
||||
} else {
|
||||
dim = calc(`calc(${div})`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
if (value.length) {
|
||||
value.push('/', dim.replace(/^calc/, ''));
|
||||
} else {
|
||||
value.push('1', '/', dim.replace(/^calc/, ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.#hasEtc) {
|
||||
if (this.#etcMul.length) {
|
||||
if (!value.length && num !== undefined) {
|
||||
value.push(num);
|
||||
}
|
||||
const mul = this.sort(this.#etcMul).join(' * ');
|
||||
if (value.length) {
|
||||
value.push(`* ${mul}`);
|
||||
} else {
|
||||
value.push(`${mul}`);
|
||||
}
|
||||
}
|
||||
if (this.#etcDiv.length) {
|
||||
const div = this.sort(this.#etcDiv).join(' * ');
|
||||
if (div.includes('*')) {
|
||||
if (value.length) {
|
||||
value.push(`/ (${div})`);
|
||||
} else {
|
||||
value.push(`1 / (${div})`);
|
||||
}
|
||||
} else if (value.length) {
|
||||
value.push(`/ ${div}`);
|
||||
} else {
|
||||
value.push(`1 / ${div}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.length) {
|
||||
return value.join(' ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* sum values
|
||||
* @returns resolved value
|
||||
*/
|
||||
sum(): string {
|
||||
const value = [];
|
||||
if (this.#hasNum) {
|
||||
let num = 0;
|
||||
for (const i of this.#numSum) {
|
||||
num += i;
|
||||
if (!Number.isFinite(num) || Number.isNaN(num)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
value.push(num);
|
||||
}
|
||||
if (this.#hasPct) {
|
||||
let num: number | string = 0;
|
||||
for (const i of this.#pctSum) {
|
||||
num += i;
|
||||
if (!Number.isFinite(num)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Number.isFinite(num)) {
|
||||
num = `${num}%`;
|
||||
}
|
||||
if (value.length) {
|
||||
value.push(`+ ${num}`);
|
||||
} else {
|
||||
value.push(num);
|
||||
}
|
||||
}
|
||||
if (this.#hasDim) {
|
||||
let dim, sum, sub;
|
||||
if (this.#dimSum.length) {
|
||||
sum = this.sort(this.#dimSum).join(' + ');
|
||||
}
|
||||
if (this.#dimSub.length) {
|
||||
sub = this.sort(this.#dimSub).join(' + ');
|
||||
}
|
||||
if (sum) {
|
||||
if (sub) {
|
||||
if (sub.includes('-')) {
|
||||
dim = calc(`calc(${sum} - (${sub}))`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
} else {
|
||||
dim = calc(`calc(${sum} - ${sub})`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dim = calc(`calc(${sum})`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dim = calc(`calc(-1 * (${sub}))`, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
if (value.length) {
|
||||
value.push('+', dim.replace(/^calc/, ''));
|
||||
} else {
|
||||
value.push(dim.replace(/^calc/, ''));
|
||||
}
|
||||
}
|
||||
if (this.#hasEtc) {
|
||||
if (this.#etcSum.length) {
|
||||
const sum = this.sort(this.#etcSum)
|
||||
.map(item => {
|
||||
let res;
|
||||
if (
|
||||
REG_OPERATOR.test(item) &&
|
||||
!item.startsWith('(') &&
|
||||
!item.endsWith(')')
|
||||
) {
|
||||
res = `(${item})`;
|
||||
} else {
|
||||
res = item;
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.join(' + ');
|
||||
if (value.length) {
|
||||
if (this.#etcSum.length > 1) {
|
||||
value.push(`+ (${sum})`);
|
||||
} else {
|
||||
value.push(`+ ${sum}`);
|
||||
}
|
||||
} else {
|
||||
value.push(`${sum}`);
|
||||
}
|
||||
}
|
||||
if (this.#etcSub.length) {
|
||||
const sub = this.sort(this.#etcSub)
|
||||
.map(item => {
|
||||
let res;
|
||||
if (
|
||||
REG_OPERATOR.test(item) &&
|
||||
!item.startsWith('(') &&
|
||||
!item.endsWith(')')
|
||||
) {
|
||||
res = `(${item})`;
|
||||
} else {
|
||||
res = item;
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.join(' + ');
|
||||
if (value.length) {
|
||||
if (this.#etcSub.length > 1) {
|
||||
value.push(`- (${sub})`);
|
||||
} else {
|
||||
value.push(`- ${sub}`);
|
||||
}
|
||||
} else if (this.#etcSub.length > 1) {
|
||||
value.push(`-1 * (${sub})`);
|
||||
} else {
|
||||
value.push(`-1 * ${sub}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.length) {
|
||||
return value.join(' ');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sort calc values
|
||||
* @param values - values to sort
|
||||
* @param [finalize] - finalize values
|
||||
* @returns sorted values
|
||||
*/
|
||||
export const sortCalcValues = (
|
||||
values: (number | string)[] = [],
|
||||
finalize: boolean = false
|
||||
): string => {
|
||||
if (values.length < TRIA) {
|
||||
throw new Error(`Unexpected array length ${values.length}.`);
|
||||
}
|
||||
const start = values.shift();
|
||||
if (!isString(start) || !start.endsWith('(')) {
|
||||
throw new Error(`Unexpected token ${start}.`);
|
||||
}
|
||||
const end = values.pop();
|
||||
if (end !== ')') {
|
||||
throw new Error(`Unexpected token ${end}.`);
|
||||
}
|
||||
if (values.length === 1) {
|
||||
const [value] = values;
|
||||
if (!isStringOrNumber(value)) {
|
||||
throw new Error(`Unexpected token ${value}.`);
|
||||
}
|
||||
return `${start}${value}${end}`;
|
||||
}
|
||||
const sortedValues = [];
|
||||
const cal = new Calculator();
|
||||
let operator: string = '';
|
||||
const l = values.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
const value = values[i];
|
||||
if (!isStringOrNumber(value)) {
|
||||
throw new Error(`Unexpected token ${value}.`);
|
||||
}
|
||||
if (value === '*' || value === '/') {
|
||||
operator = value;
|
||||
} else if (value === '+' || value === '-') {
|
||||
const sortedValue = cal.multiply();
|
||||
if (sortedValue) {
|
||||
sortedValues.push(sortedValue, value);
|
||||
}
|
||||
cal.clear();
|
||||
operator = '';
|
||||
} else {
|
||||
const numValue = Number(value);
|
||||
const strValue = `${value}`;
|
||||
switch (operator) {
|
||||
case '/': {
|
||||
if (Number.isFinite(numValue)) {
|
||||
cal.hasNum = true;
|
||||
cal.numMul.push(1 / numValue);
|
||||
} else if (REG_TYPE_PCT.test(strValue)) {
|
||||
const [, val] = strValue.match(REG_TYPE_PCT) as MatchedRegExp;
|
||||
cal.hasPct = true;
|
||||
cal.pctMul.push((MAX_PCT * MAX_PCT) / Number(val));
|
||||
} else if (REG_TYPE_DIM.test(strValue)) {
|
||||
cal.hasDim = true;
|
||||
cal.dimDiv.push(strValue);
|
||||
} else {
|
||||
cal.hasEtc = true;
|
||||
cal.etcDiv.push(strValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '*':
|
||||
default: {
|
||||
if (Number.isFinite(numValue)) {
|
||||
cal.hasNum = true;
|
||||
cal.numMul.push(numValue);
|
||||
} else if (REG_TYPE_PCT.test(strValue)) {
|
||||
const [, val] = strValue.match(REG_TYPE_PCT) as MatchedRegExp;
|
||||
cal.hasPct = true;
|
||||
cal.pctMul.push(Number(val));
|
||||
} else if (REG_TYPE_DIM.test(strValue)) {
|
||||
cal.hasDim = true;
|
||||
cal.dimMul.push(strValue);
|
||||
} else {
|
||||
cal.hasEtc = true;
|
||||
cal.etcMul.push(strValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i === l - 1) {
|
||||
const sortedValue = cal.multiply();
|
||||
if (sortedValue) {
|
||||
sortedValues.push(sortedValue);
|
||||
}
|
||||
cal.clear();
|
||||
operator = '';
|
||||
}
|
||||
}
|
||||
let resolvedValue = '';
|
||||
if (finalize && (sortedValues.includes('+') || sortedValues.includes('-'))) {
|
||||
const finalizedValues = [];
|
||||
cal.clear();
|
||||
operator = '';
|
||||
const l = sortedValues.length;
|
||||
for (let i = 0; i < l; i++) {
|
||||
const value = sortedValues[i];
|
||||
if (isStringOrNumber(value)) {
|
||||
if (value === '+' || value === '-') {
|
||||
operator = value;
|
||||
} else {
|
||||
const numValue = Number(value);
|
||||
const strValue = `${value}`;
|
||||
switch (operator) {
|
||||
case '-': {
|
||||
if (Number.isFinite(numValue)) {
|
||||
cal.hasNum = true;
|
||||
cal.numSum.push(-1 * numValue);
|
||||
} else if (REG_TYPE_PCT.test(strValue)) {
|
||||
const [, val] = strValue.match(REG_TYPE_PCT) as MatchedRegExp;
|
||||
cal.hasPct = true;
|
||||
cal.pctSum.push(-1 * Number(val));
|
||||
} else if (REG_TYPE_DIM.test(strValue)) {
|
||||
cal.hasDim = true;
|
||||
cal.dimSub.push(strValue);
|
||||
} else {
|
||||
cal.hasEtc = true;
|
||||
cal.etcSub.push(strValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '+':
|
||||
default: {
|
||||
if (Number.isFinite(numValue)) {
|
||||
cal.hasNum = true;
|
||||
cal.numSum.push(numValue);
|
||||
} else if (REG_TYPE_PCT.test(strValue)) {
|
||||
const [, val] = strValue.match(REG_TYPE_PCT) as MatchedRegExp;
|
||||
cal.hasPct = true;
|
||||
cal.pctSum.push(Number(val));
|
||||
} else if (REG_TYPE_DIM.test(strValue)) {
|
||||
cal.hasDim = true;
|
||||
cal.dimSum.push(strValue);
|
||||
} else {
|
||||
cal.hasEtc = true;
|
||||
cal.etcSum.push(strValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i === l - 1) {
|
||||
const sortedValue = cal.sum();
|
||||
if (sortedValue) {
|
||||
finalizedValues.push(sortedValue);
|
||||
}
|
||||
cal.clear();
|
||||
operator = '';
|
||||
}
|
||||
}
|
||||
resolvedValue = finalizedValues.join(' ').replace(/\+\s-/g, '- ');
|
||||
} else {
|
||||
resolvedValue = sortedValues.join(' ').replace(/\+\s-/g, '- ');
|
||||
}
|
||||
if (
|
||||
resolvedValue.startsWith('(') &&
|
||||
resolvedValue.endsWith(')') &&
|
||||
resolvedValue.lastIndexOf('(') === 0 &&
|
||||
resolvedValue.indexOf(')') === resolvedValue.length - 1
|
||||
) {
|
||||
resolvedValue = resolvedValue.replace(/^\(/, '').replace(/\)$/, '');
|
||||
}
|
||||
return `${start}${resolvedValue}${end}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* serialize calc
|
||||
* @param value - CSS value
|
||||
* @param [opt] - options
|
||||
* @returns serialized value
|
||||
*/
|
||||
export const serializeCalc = (value: string, opt: Options = {}): string => {
|
||||
const { format = '' } = opt;
|
||||
if (isString(value)) {
|
||||
if (!REG_FN_VAR_START.test(value) || format !== VAL_SPEC) {
|
||||
return value;
|
||||
}
|
||||
value = value.toLowerCase().trim();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'serializeCalc',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as string;
|
||||
}
|
||||
const items: string[] = tokenize({ css: value })
|
||||
.map((token: CSSToken): string => {
|
||||
const [type, value] = token as [TokenType, string];
|
||||
let res = '';
|
||||
if (type !== W_SPACE && type !== COMMENT) {
|
||||
res = value;
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.filter(v => v);
|
||||
let startIndex = items.findLastIndex((item: string) => /\($/.test(item));
|
||||
while (startIndex) {
|
||||
const endIndex = items.findIndex((item: unknown, index: number) => {
|
||||
return item === ')' && index > startIndex;
|
||||
});
|
||||
const slicedValues: string[] = items.slice(startIndex, endIndex + 1);
|
||||
let serializedValue: string = sortCalcValues(slicedValues);
|
||||
if (REG_FN_VAR_START.test(serializedValue)) {
|
||||
serializedValue = calc(serializedValue, {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
}
|
||||
items.splice(startIndex, endIndex - startIndex + 1, serializedValue);
|
||||
startIndex = items.findLastIndex((item: string) => /\($/.test(item));
|
||||
}
|
||||
const serializedCalc = sortCalcValues(items, true);
|
||||
setCache(cacheKey, serializedCalc);
|
||||
return serializedCalc;
|
||||
};
|
||||
|
||||
/**
|
||||
* resolve dimension
|
||||
* @param token - CSS token
|
||||
* @param [opt] - options
|
||||
* @returns resolved value
|
||||
*/
|
||||
export const resolveDimension = (
|
||||
token: CSSToken,
|
||||
opt: Options = {}
|
||||
): string | NullObject => {
|
||||
if (!Array.isArray(token)) {
|
||||
throw new TypeError(`${token} is not an array.`);
|
||||
}
|
||||
const [, , , , detail = {}] = token;
|
||||
const { unit, value } = detail as {
|
||||
unit: string;
|
||||
value: number;
|
||||
};
|
||||
const { dimension = {} } = opt;
|
||||
if (unit === 'px') {
|
||||
return `${value}${unit}`;
|
||||
}
|
||||
const relativeValue = Number(value);
|
||||
if (unit && Number.isFinite(relativeValue)) {
|
||||
let pixelValue;
|
||||
if (Object.hasOwnProperty.call(dimension, unit)) {
|
||||
pixelValue = dimension[unit];
|
||||
} else if (typeof dimension.callback === 'function') {
|
||||
pixelValue = dimension.callback(unit);
|
||||
}
|
||||
pixelValue = Number(pixelValue);
|
||||
if (Number.isFinite(pixelValue)) {
|
||||
return `${relativeValue * pixelValue}px`;
|
||||
}
|
||||
}
|
||||
return new NullObject();
|
||||
};
|
||||
|
||||
/**
|
||||
* parse tokens
|
||||
* @param tokens - CSS tokens
|
||||
* @param [opt] - options
|
||||
* @returns parsed tokens
|
||||
*/
|
||||
export const parseTokens = (
|
||||
tokens: CSSToken[],
|
||||
opt: Options = {}
|
||||
): string[] => {
|
||||
if (!Array.isArray(tokens)) {
|
||||
throw new TypeError(`${tokens} is not an array.`);
|
||||
}
|
||||
const { format = '' } = opt;
|
||||
const mathFunc = new Set();
|
||||
let nest = 0;
|
||||
const res: string[] = [];
|
||||
while (tokens.length) {
|
||||
const token = tokens.shift();
|
||||
if (!Array.isArray(token)) {
|
||||
throw new TypeError(`${token} is not an array.`);
|
||||
}
|
||||
const [type = '', value = ''] = token as [TokenType, string];
|
||||
switch (type) {
|
||||
case DIM: {
|
||||
if (format === VAL_SPEC && !mathFunc.has(nest)) {
|
||||
res.push(value);
|
||||
} else {
|
||||
const resolvedValue = resolveDimension(token, opt);
|
||||
if (isString(resolvedValue)) {
|
||||
res.push(resolvedValue);
|
||||
} else {
|
||||
res.push(value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FUNC:
|
||||
case PAREN_OPEN: {
|
||||
res.push(value);
|
||||
nest++;
|
||||
if (REG_FN_MATH_START.test(value)) {
|
||||
mathFunc.add(nest);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PAREN_CLOSE: {
|
||||
if (res.length) {
|
||||
const lastValue = res[res.length - 1];
|
||||
if (lastValue === ' ') {
|
||||
res.splice(-1, 1, value);
|
||||
} else {
|
||||
res.push(value);
|
||||
}
|
||||
} else {
|
||||
res.push(value);
|
||||
}
|
||||
if (mathFunc.has(nest)) {
|
||||
mathFunc.delete(nest);
|
||||
}
|
||||
nest--;
|
||||
break;
|
||||
}
|
||||
case W_SPACE: {
|
||||
if (res.length) {
|
||||
const lastValue = res[res.length - 1];
|
||||
if (
|
||||
isString(lastValue) &&
|
||||
!lastValue.endsWith('(') &&
|
||||
lastValue !== ' '
|
||||
) {
|
||||
res.push(value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (type !== COMMENT && type !== EOF) {
|
||||
res.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* CSS calc()
|
||||
* @param value - CSS value including calc()
|
||||
* @param [opt] - options
|
||||
* @returns resolved value
|
||||
*/
|
||||
export const cssCalc = (value: string, opt: Options = {}): string => {
|
||||
const { format = '' } = opt;
|
||||
if (isString(value)) {
|
||||
if (REG_FN_VAR.test(value)) {
|
||||
if (format === VAL_SPEC) {
|
||||
return value;
|
||||
} else {
|
||||
const resolvedValue = resolveVar(value, opt);
|
||||
if (isString(resolvedValue)) {
|
||||
return resolvedValue;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
} else if (!REG_FN_CALC.test(value)) {
|
||||
return value;
|
||||
}
|
||||
value = value.toLowerCase().trim();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'cssCalc',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as string;
|
||||
}
|
||||
const tokens = tokenize({ css: value });
|
||||
const values = parseTokens(tokens, opt);
|
||||
let resolvedValue: string = calc(values.join(''), {
|
||||
toCanonicalUnits: true
|
||||
});
|
||||
if (REG_FN_VAR_START.test(value)) {
|
||||
if (REG_TYPE_DIM_PCT.test(resolvedValue)) {
|
||||
const [, val, unit] = resolvedValue.match(
|
||||
REG_TYPE_DIM_PCT
|
||||
) as MatchedRegExp;
|
||||
resolvedValue = `${roundToPrecision(Number(val), HEX)}${unit}`;
|
||||
}
|
||||
// wrap with `calc()`
|
||||
if (
|
||||
resolvedValue &&
|
||||
!REG_FN_VAR_START.test(resolvedValue) &&
|
||||
format === VAL_SPEC
|
||||
) {
|
||||
resolvedValue = `calc(${resolvedValue})`;
|
||||
}
|
||||
}
|
||||
if (format === VAL_SPEC) {
|
||||
if (/\s[-+*/]\s/.test(resolvedValue) && !resolvedValue.includes('NaN')) {
|
||||
resolvedValue = serializeCalc(resolvedValue, opt);
|
||||
} else if (REG_FN_CALC_NUM.test(resolvedValue)) {
|
||||
const [, val] = resolvedValue.match(REG_FN_CALC_NUM) as MatchedRegExp;
|
||||
resolvedValue = `calc(${roundToPrecision(Number(val), HEX)})`;
|
||||
}
|
||||
}
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
};
|
||||
289
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/css-gradient.ts
generated
vendored
289
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/css-gradient.ts
generated
vendored
@@ -1,289 +0,0 @@
|
||||
/**
|
||||
* css-gradient
|
||||
*/
|
||||
|
||||
import { CacheItem, createCacheKey, getCache, setCache } from './cache';
|
||||
import { isString } from './common';
|
||||
import { MatchedRegExp, Options } from './typedef';
|
||||
import { isColor, splitValue } from './util';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
ANGLE,
|
||||
CS_HUE,
|
||||
CS_RECT,
|
||||
LENGTH,
|
||||
NUM,
|
||||
NUM_POSITIVE,
|
||||
PCT
|
||||
} from './constant';
|
||||
const NAMESPACE = 'css-gradient';
|
||||
const DIM_ANGLE = `${NUM}(?:${ANGLE})`;
|
||||
const DIM_ANGLE_PCT = `${DIM_ANGLE}|${PCT}`;
|
||||
const DIM_LEN = `${NUM}(?:${LENGTH})|0`;
|
||||
const DIM_LEN_PCT = `${DIM_LEN}|${PCT}`;
|
||||
const DIM_LEN_PCT_POSI = `${NUM_POSITIVE}(?:${LENGTH}|%)|0`;
|
||||
const DIM_LEN_POSI = `${NUM_POSITIVE}(?:${LENGTH})|0`;
|
||||
const CTR = 'center';
|
||||
const L_R = 'left|right';
|
||||
const T_B = 'top|bottom';
|
||||
const S_E = 'start|end';
|
||||
const AXIS_X = `${L_R}|x-(?:${S_E})`;
|
||||
const AXIS_Y = `${T_B}|y-(?:${S_E})`;
|
||||
const BLOCK = `block-(?:${S_E})`;
|
||||
const INLINE = `inline-(?:${S_E})`;
|
||||
const POS_1 = `${CTR}|${AXIS_X}|${AXIS_Y}|${BLOCK}|${INLINE}|${DIM_LEN_PCT}`;
|
||||
const POS_2 = [
|
||||
`(?:${CTR}|${AXIS_X})\\s+(?:${CTR}|${AXIS_Y})`,
|
||||
`(?:${CTR}|${AXIS_Y})\\s+(?:${CTR}|${AXIS_X})`,
|
||||
`(?:${CTR}|${AXIS_X}|${DIM_LEN_PCT})\\s+(?:${CTR}|${AXIS_Y}|${DIM_LEN_PCT})`,
|
||||
`(?:${CTR}|${BLOCK})\\s+(?:${CTR}|${INLINE})`,
|
||||
`(?:${CTR}|${INLINE})\\s+(?:${CTR}|${BLOCK})`,
|
||||
`(?:${CTR}|${S_E})\\s+(?:${CTR}|${S_E})`
|
||||
].join('|');
|
||||
const POS_4 = [
|
||||
`(?:${AXIS_X})\\s+(?:${DIM_LEN_PCT})\\s+(?:${AXIS_Y})\\s+(?:${DIM_LEN_PCT})`,
|
||||
`(?:${AXIS_Y})\\s+(?:${DIM_LEN_PCT})\\s+(?:${AXIS_X})\\s+(?:${DIM_LEN_PCT})`,
|
||||
`(?:${BLOCK})\\s+(?:${DIM_LEN_PCT})\\s+(?:${INLINE})\\s+(?:${DIM_LEN_PCT})`,
|
||||
`(?:${INLINE})\\s+(?:${DIM_LEN_PCT})\\s+(?:${BLOCK})\\s+(?:${DIM_LEN_PCT})`,
|
||||
`(?:${S_E})\\s+(?:${DIM_LEN_PCT})\\s+(?:${S_E})\\s+(?:${DIM_LEN_PCT})`
|
||||
].join('|');
|
||||
const RAD_EXTENT = '(?:clos|farth)est-(?:corner|side)';
|
||||
const RAD_SIZE = [
|
||||
`${RAD_EXTENT}(?:\\s+${RAD_EXTENT})?`,
|
||||
`${DIM_LEN_POSI}`,
|
||||
`(?:${DIM_LEN_PCT_POSI})\\s+(?:${DIM_LEN_PCT_POSI})`
|
||||
].join('|');
|
||||
const RAD_SHAPE = 'circle|ellipse';
|
||||
const FROM_ANGLE = `from\\s+${DIM_ANGLE}`;
|
||||
const AT_POSITION = `at\\s+(?:${POS_1}|${POS_2}|${POS_4})`;
|
||||
const TO_SIDE_CORNER = `to\\s+(?:(?:${L_R})(?:\\s(?:${T_B}))?|(?:${T_B})(?:\\s(?:${L_R}))?)`;
|
||||
const IN_COLOR_SPACE = `in\\s+(?:${CS_RECT}|${CS_HUE})`;
|
||||
|
||||
/* type definitions */
|
||||
/**
|
||||
* @type ColorStopList - list of color stops
|
||||
*/
|
||||
type ColorStopList = [string, string, ...string[]];
|
||||
|
||||
/**
|
||||
* @typedef Gradient - parsed CSS gradient
|
||||
* @property value - input value
|
||||
* @property type - gradient type
|
||||
* @property [gradientLine] - gradient line
|
||||
* @property colorStopList - list of color stops
|
||||
*/
|
||||
interface Gradient {
|
||||
value: string;
|
||||
type: string;
|
||||
gradientLine?: string;
|
||||
colorStopList: ColorStopList;
|
||||
}
|
||||
|
||||
/* regexp */
|
||||
const REG_GRAD = /^(?:repeating-)?(?:conic|linear|radial)-gradient\(/;
|
||||
const REG_GRAD_CAPT = /^((?:repeating-)?(?:conic|linear|radial)-gradient)\(/;
|
||||
|
||||
/**
|
||||
* get gradient type
|
||||
* @param value - gradient value
|
||||
* @returns gradient type
|
||||
*/
|
||||
export const getGradientType = (value: string): string => {
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
if (REG_GRAD.test(value)) {
|
||||
const [, type] = value.match(REG_GRAD_CAPT) as MatchedRegExp;
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* validate gradient line
|
||||
* @param value - gradient line value
|
||||
* @param type - gradient type
|
||||
* @returns result
|
||||
*/
|
||||
export const validateGradientLine = (value: string, type: string): boolean => {
|
||||
if (isString(value) && isString(type)) {
|
||||
value = value.trim();
|
||||
type = type.trim();
|
||||
let lineSyntax = '';
|
||||
if (/^(?:repeating-)?linear-gradient$/.test(type)) {
|
||||
/*
|
||||
* <linear-gradient-line> = [
|
||||
* [ <angle> | to <side-or-corner> ] ||
|
||||
* <color-interpolation-method>
|
||||
* ]
|
||||
*/
|
||||
lineSyntax = [
|
||||
`(?:${DIM_ANGLE}|${TO_SIDE_CORNER})(?:\\s+${IN_COLOR_SPACE})?`,
|
||||
`${IN_COLOR_SPACE}(?:\\s+(?:${DIM_ANGLE}|${TO_SIDE_CORNER}))?`
|
||||
].join('|');
|
||||
} else if (/^(?:repeating-)?radial-gradient$/.test(type)) {
|
||||
/*
|
||||
* <radial-gradient-line> = [
|
||||
* [ [ <radial-shape> || <radial-size> ]? [ at <position> ]? ] ||
|
||||
* <color-interpolation-method>]?
|
||||
*/
|
||||
lineSyntax = [
|
||||
`(?:${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
|
||||
`(?:${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
|
||||
`${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`,
|
||||
`${IN_COLOR_SPACE}(?:\\s+${RAD_SHAPE})(?:\\s+(?:${RAD_SIZE}))?(?:\\s+${AT_POSITION})?`,
|
||||
`${IN_COLOR_SPACE}(?:\\s+${RAD_SIZE})(?:\\s+(?:${RAD_SHAPE}))?(?:\\s+${AT_POSITION})?`,
|
||||
`${IN_COLOR_SPACE}(?:\\s+${AT_POSITION})?`
|
||||
].join('|');
|
||||
} else if (/^(?:repeating-)?conic-gradient$/.test(type)) {
|
||||
/*
|
||||
* <conic-gradient-line> = [
|
||||
* [ [ from <angle> ]? [ at <position> ]? ] ||
|
||||
* <color-interpolation-method>
|
||||
* ]
|
||||
*/
|
||||
lineSyntax = [
|
||||
`${FROM_ANGLE}(?:\\s+${AT_POSITION})?(?:\\s+${IN_COLOR_SPACE})?`,
|
||||
`${AT_POSITION}(?:\\s+${IN_COLOR_SPACE})?`,
|
||||
`${IN_COLOR_SPACE}(?:\\s+${FROM_ANGLE})?(?:\\s+${AT_POSITION})?`
|
||||
].join('|');
|
||||
}
|
||||
if (lineSyntax) {
|
||||
const reg = new RegExp(`^(?:${lineSyntax})$`);
|
||||
return reg.test(value);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* validate color stop list
|
||||
* @param list
|
||||
* @param type
|
||||
* @param [opt]
|
||||
* @returns result
|
||||
*/
|
||||
export const validateColorStopList = (
|
||||
list: string[],
|
||||
type: string,
|
||||
opt: Options = {}
|
||||
): boolean => {
|
||||
if (Array.isArray(list) && list.length > 1) {
|
||||
const dimension = /^(?:repeating-)?conic-gradient$/.test(type)
|
||||
? DIM_ANGLE_PCT
|
||||
: DIM_LEN_PCT;
|
||||
const regColorHint = new RegExp(`^(?:${dimension})$`);
|
||||
const regDimension = new RegExp(`(?:\\s+(?:${dimension})){1,2}$`);
|
||||
const arr = [];
|
||||
for (const item of list) {
|
||||
if (isString(item)) {
|
||||
if (regColorHint.test(item)) {
|
||||
arr.push('hint');
|
||||
} else {
|
||||
const color = item.replace(regDimension, '');
|
||||
if (isColor(color, opt)) {
|
||||
arr.push('color');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const value = arr.join(',');
|
||||
return /^color(?:,(?:hint,)?color)+$/.test(value);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* parse CSS gradient
|
||||
* @param value - gradient value
|
||||
* @param [opt] - options
|
||||
* @returns parsed result
|
||||
*/
|
||||
export const parseGradient = (
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): Gradient | null => {
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'parseGradient',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
if (cachedResult.isNull) {
|
||||
return null;
|
||||
}
|
||||
return cachedResult.item as Gradient;
|
||||
}
|
||||
const type = getGradientType(value);
|
||||
const gradValue = value.replace(REG_GRAD, '').replace(/\)$/, '');
|
||||
if (type && gradValue) {
|
||||
const [lineOrColorStop = '', ...colorStops] = splitValue(gradValue, {
|
||||
delimiter: ','
|
||||
});
|
||||
const dimension = /^(?:repeating-)?conic-gradient$/.test(type)
|
||||
? DIM_ANGLE_PCT
|
||||
: DIM_LEN_PCT;
|
||||
const regDimension = new RegExp(`(?:\\s+(?:${dimension})){1,2}$`);
|
||||
let isColorStop = false;
|
||||
if (regDimension.test(lineOrColorStop)) {
|
||||
const colorStop = lineOrColorStop.replace(regDimension, '');
|
||||
if (isColor(colorStop, opt)) {
|
||||
isColorStop = true;
|
||||
}
|
||||
} else if (isColor(lineOrColorStop, opt)) {
|
||||
isColorStop = true;
|
||||
}
|
||||
if (isColorStop) {
|
||||
colorStops.unshift(lineOrColorStop);
|
||||
const valid = validateColorStopList(colorStops, type, opt);
|
||||
if (valid) {
|
||||
const res: Gradient = {
|
||||
value,
|
||||
type,
|
||||
colorStopList: colorStops as ColorStopList
|
||||
};
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
} else if (colorStops.length > 1) {
|
||||
const gradientLine = lineOrColorStop;
|
||||
const valid =
|
||||
validateGradientLine(gradientLine, type) &&
|
||||
validateColorStopList(colorStops, type, opt);
|
||||
if (valid) {
|
||||
const res: Gradient = {
|
||||
value,
|
||||
type,
|
||||
gradientLine,
|
||||
colorStopList: colorStops as ColorStopList
|
||||
};
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
setCache(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* is CSS gradient
|
||||
* @param value - CSS value
|
||||
* @param [opt] - options
|
||||
* @returns result
|
||||
*/
|
||||
export const isGradient = (value: string, opt: Options = {}): boolean => {
|
||||
const gradient = parseGradient(value, opt);
|
||||
return gradient !== null;
|
||||
};
|
||||
250
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/css-var.ts
generated
vendored
250
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/css-var.ts
generated
vendored
@@ -1,250 +0,0 @@
|
||||
/**
|
||||
* css-var
|
||||
*/
|
||||
|
||||
import { CSSToken, TokenType, tokenize } from '@csstools/css-tokenizer';
|
||||
import {
|
||||
CacheItem,
|
||||
NullObject,
|
||||
createCacheKey,
|
||||
getCache,
|
||||
setCache
|
||||
} from './cache';
|
||||
import { isString } from './common';
|
||||
import { cssCalc } from './css-calc';
|
||||
import { isColor } from './util';
|
||||
import { Options } from './typedef';
|
||||
|
||||
/* constants */
|
||||
import { FN_VAR, SYN_FN_CALC, SYN_FN_VAR, VAL_SPEC } from './constant';
|
||||
const {
|
||||
CloseParen: PAREN_CLOSE,
|
||||
Comment: COMMENT,
|
||||
EOF,
|
||||
Ident: IDENT,
|
||||
Whitespace: W_SPACE
|
||||
} = TokenType;
|
||||
const NAMESPACE = 'css-var';
|
||||
|
||||
/* regexp */
|
||||
const REG_FN_CALC = new RegExp(SYN_FN_CALC);
|
||||
const REG_FN_VAR = new RegExp(SYN_FN_VAR);
|
||||
|
||||
/**
|
||||
* resolve custom property
|
||||
* @param tokens - CSS tokens
|
||||
* @param [opt] - options
|
||||
* @returns result - [tokens, resolvedValue]
|
||||
*/
|
||||
export function resolveCustomProperty(
|
||||
tokens: CSSToken[],
|
||||
opt: Options = {}
|
||||
): [CSSToken[], string] {
|
||||
if (!Array.isArray(tokens)) {
|
||||
throw new TypeError(`${tokens} is not an array.`);
|
||||
}
|
||||
const { customProperty = {} } = opt;
|
||||
const items: string[] = [];
|
||||
while (tokens.length) {
|
||||
const token = tokens.shift();
|
||||
if (!Array.isArray(token)) {
|
||||
throw new TypeError(`${token} is not an array.`);
|
||||
}
|
||||
const [type, value] = token as [TokenType, string];
|
||||
// end of var()
|
||||
if (type === PAREN_CLOSE) {
|
||||
break;
|
||||
}
|
||||
// nested var()
|
||||
if (value === FN_VAR) {
|
||||
const [restTokens, item] = resolveCustomProperty(tokens, opt);
|
||||
tokens = restTokens;
|
||||
if (item) {
|
||||
items.push(item);
|
||||
}
|
||||
} else if (type === IDENT) {
|
||||
if (value.startsWith('--')) {
|
||||
let item;
|
||||
if (Object.hasOwnProperty.call(customProperty, value)) {
|
||||
item = customProperty[value] as string;
|
||||
} else if (typeof customProperty.callback === 'function') {
|
||||
item = customProperty.callback(value);
|
||||
}
|
||||
if (item) {
|
||||
items.push(item);
|
||||
}
|
||||
} else if (value) {
|
||||
items.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
let resolveAsColor = false;
|
||||
if (items.length > 1) {
|
||||
const lastValue = items[items.length - 1];
|
||||
resolveAsColor = isColor(lastValue);
|
||||
}
|
||||
let resolvedValue = '';
|
||||
for (let item of items) {
|
||||
item = item.trim();
|
||||
if (REG_FN_VAR.test(item)) {
|
||||
// recurse resolveVar()
|
||||
const resolvedItem = resolveVar(item, opt);
|
||||
if (isString(resolvedItem)) {
|
||||
if (resolveAsColor) {
|
||||
if (isColor(resolvedItem)) {
|
||||
resolvedValue = resolvedItem;
|
||||
}
|
||||
} else {
|
||||
resolvedValue = resolvedItem;
|
||||
}
|
||||
}
|
||||
} else if (REG_FN_CALC.test(item)) {
|
||||
item = cssCalc(item, opt);
|
||||
if (resolveAsColor) {
|
||||
if (isColor(item)) {
|
||||
resolvedValue = item;
|
||||
}
|
||||
} else {
|
||||
resolvedValue = item;
|
||||
}
|
||||
} else if (
|
||||
item &&
|
||||
!/^(?:inherit|initial|revert(?:-layer)?|unset)$/.test(item)
|
||||
) {
|
||||
if (resolveAsColor) {
|
||||
if (isColor(item)) {
|
||||
resolvedValue = item;
|
||||
}
|
||||
} else {
|
||||
resolvedValue = item;
|
||||
}
|
||||
}
|
||||
if (resolvedValue) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [tokens, resolvedValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* parse tokens
|
||||
* @param tokens - CSS tokens
|
||||
* @param [opt] - options
|
||||
* @returns parsed tokens
|
||||
*/
|
||||
export function parseTokens(
|
||||
tokens: CSSToken[],
|
||||
opt: Options = {}
|
||||
): string[] | NullObject {
|
||||
const res: string[] = [];
|
||||
while (tokens.length) {
|
||||
const token = tokens.shift();
|
||||
const [type = '', value = ''] = token as [TokenType, string];
|
||||
if (value === FN_VAR) {
|
||||
const [restTokens, resolvedValue] = resolveCustomProperty(tokens, opt);
|
||||
if (!resolvedValue) {
|
||||
return new NullObject();
|
||||
}
|
||||
tokens = restTokens;
|
||||
res.push(resolvedValue);
|
||||
} else {
|
||||
switch (type) {
|
||||
case PAREN_CLOSE: {
|
||||
if (res.length) {
|
||||
const lastValue = res[res.length - 1];
|
||||
if (lastValue === ' ') {
|
||||
res.splice(-1, 1, value);
|
||||
} else {
|
||||
res.push(value);
|
||||
}
|
||||
} else {
|
||||
res.push(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case W_SPACE: {
|
||||
if (res.length) {
|
||||
const lastValue = res[res.length - 1];
|
||||
if (
|
||||
isString(lastValue) &&
|
||||
!lastValue.endsWith('(') &&
|
||||
lastValue !== ' '
|
||||
) {
|
||||
res.push(value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (type !== COMMENT && type !== EOF) {
|
||||
res.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* resolve CSS var()
|
||||
* @param value - CSS value including var()
|
||||
* @param [opt] - options
|
||||
* @returns resolved value
|
||||
*/
|
||||
export function resolveVar(
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): string | NullObject {
|
||||
const { format = '' } = opt;
|
||||
if (isString(value)) {
|
||||
if (!REG_FN_VAR.test(value) || format === VAL_SPEC) {
|
||||
return value;
|
||||
}
|
||||
value = value.trim();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'resolveVar',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
if (cachedResult.isNull) {
|
||||
return cachedResult as NullObject;
|
||||
}
|
||||
return cachedResult.item as string;
|
||||
}
|
||||
const tokens = tokenize({ css: value });
|
||||
const values = parseTokens(tokens, opt);
|
||||
if (Array.isArray(values)) {
|
||||
let color = values.join('');
|
||||
if (REG_FN_CALC.test(color)) {
|
||||
color = cssCalc(color, opt);
|
||||
}
|
||||
setCache(cacheKey, color);
|
||||
return color;
|
||||
} else {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS var()
|
||||
* @param value - CSS value including var()
|
||||
* @param [opt] - options
|
||||
* @returns resolved value
|
||||
*/
|
||||
export const cssVar = (value: string, opt: Options = {}): string => {
|
||||
const resolvedValue = resolveVar(value, opt);
|
||||
if (isString(resolvedValue)) {
|
||||
return resolvedValue;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
580
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/relative-color.ts
generated
vendored
580
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/relative-color.ts
generated
vendored
@@ -1,580 +0,0 @@
|
||||
/**
|
||||
* relative-color
|
||||
*/
|
||||
|
||||
import { SyntaxFlag, color as colorParser } from '@csstools/css-color-parser';
|
||||
import {
|
||||
ComponentValue,
|
||||
parseComponentValue
|
||||
} from '@csstools/css-parser-algorithms';
|
||||
import { CSSToken, TokenType, tokenize } from '@csstools/css-tokenizer';
|
||||
import {
|
||||
CacheItem,
|
||||
NullObject,
|
||||
createCacheKey,
|
||||
getCache,
|
||||
setCache
|
||||
} from './cache';
|
||||
import { NAMED_COLORS, convertColorToRgb } from './color';
|
||||
import { isString, isStringOrNumber } from './common';
|
||||
import { resolveDimension, serializeCalc } from './css-calc';
|
||||
import { resolveColor } from './resolve';
|
||||
import { roundToPrecision } from './util';
|
||||
import {
|
||||
ColorChannels,
|
||||
MatchedRegExp,
|
||||
Options,
|
||||
StringColorChannels
|
||||
} from './typedef';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
CS_LAB,
|
||||
CS_LCH,
|
||||
FN_REL,
|
||||
FN_REL_CAPT,
|
||||
FN_VAR,
|
||||
NONE,
|
||||
SYN_COLOR_TYPE,
|
||||
SYN_FN_MATH_START,
|
||||
SYN_FN_VAR,
|
||||
SYN_MIX,
|
||||
VAL_SPEC
|
||||
} from './constant';
|
||||
const {
|
||||
CloseParen: PAREN_CLOSE,
|
||||
Comment: COMMENT,
|
||||
Dimension: DIM,
|
||||
EOF,
|
||||
Function: FUNC,
|
||||
Ident: IDENT,
|
||||
Number: NUM,
|
||||
OpenParen: PAREN_OPEN,
|
||||
Percentage: PCT,
|
||||
Whitespace: W_SPACE
|
||||
} = TokenType;
|
||||
const { HasNoneKeywords: KEY_NONE } = SyntaxFlag;
|
||||
const NAMESPACE = 'relative-color';
|
||||
|
||||
/* numeric constants */
|
||||
const OCT = 8;
|
||||
const DEC = 10;
|
||||
const HEX = 16;
|
||||
const MAX_PCT = 100;
|
||||
const MAX_RGB = 255;
|
||||
|
||||
/* type definitions */
|
||||
/**
|
||||
* @type NumberOrStringColorChannels - color channel
|
||||
*/
|
||||
type NumberOrStringColorChannels = ColorChannels & StringColorChannels;
|
||||
|
||||
/* regexp */
|
||||
const REG_COLOR_CAPT = new RegExp(
|
||||
`^${FN_REL}(${SYN_COLOR_TYPE}|${SYN_MIX})\\s+`
|
||||
);
|
||||
const REG_CS_HSL = /(?:hsla?|hwb)$/;
|
||||
const REG_CS_CIE = new RegExp(`^(?:${CS_LAB}|${CS_LCH})$`);
|
||||
const REG_FN_MATH_START = new RegExp(SYN_FN_MATH_START);
|
||||
const REG_FN_REL = new RegExp(FN_REL);
|
||||
const REG_FN_REL_CAPT = new RegExp(`^${FN_REL_CAPT}`);
|
||||
const REG_FN_REL_START = new RegExp(`^${FN_REL}`);
|
||||
const REG_FN_VAR = new RegExp(SYN_FN_VAR);
|
||||
|
||||
/**
|
||||
* resolve relative color channels
|
||||
* @param tokens - CSS tokens
|
||||
* @param [opt] - options
|
||||
* @returns resolved color channels
|
||||
*/
|
||||
export function resolveColorChannels(
|
||||
tokens: CSSToken[],
|
||||
opt: Options = {}
|
||||
): NumberOrStringColorChannels | NullObject {
|
||||
if (!Array.isArray(tokens)) {
|
||||
throw new TypeError(`${tokens} is not an array.`);
|
||||
}
|
||||
const { colorSpace = '', format = '' } = opt;
|
||||
const colorChannels = new Map([
|
||||
['color', ['r', 'g', 'b', 'alpha']],
|
||||
['hsl', ['h', 's', 'l', 'alpha']],
|
||||
['hsla', ['h', 's', 'l', 'alpha']],
|
||||
['hwb', ['h', 'w', 'b', 'alpha']],
|
||||
['lab', ['l', 'a', 'b', 'alpha']],
|
||||
['lch', ['l', 'c', 'h', 'alpha']],
|
||||
['oklab', ['l', 'a', 'b', 'alpha']],
|
||||
['oklch', ['l', 'c', 'h', 'alpha']],
|
||||
['rgb', ['r', 'g', 'b', 'alpha']],
|
||||
['rgba', ['r', 'g', 'b', 'alpha']]
|
||||
]);
|
||||
const colorChannel = colorChannels.get(colorSpace);
|
||||
// invalid color channel
|
||||
if (!colorChannel) {
|
||||
return new NullObject();
|
||||
}
|
||||
const mathFunc = new Set();
|
||||
const channels: [
|
||||
(number | string)[],
|
||||
(number | string)[],
|
||||
(number | string)[],
|
||||
(number | string)[]
|
||||
] = [[], [], [], []];
|
||||
let i = 0;
|
||||
let nest = 0;
|
||||
let func = false;
|
||||
while (tokens.length) {
|
||||
const token = tokens.shift();
|
||||
if (!Array.isArray(token)) {
|
||||
throw new TypeError(`${token} is not an array.`);
|
||||
}
|
||||
const [type, value, , , detail] = token as [
|
||||
TokenType,
|
||||
string,
|
||||
number,
|
||||
number,
|
||||
{ value: string | number } | undefined
|
||||
];
|
||||
const channel = channels[i];
|
||||
if (Array.isArray(channel)) {
|
||||
switch (type) {
|
||||
case DIM: {
|
||||
const resolvedValue = resolveDimension(token, opt);
|
||||
if (isString(resolvedValue)) {
|
||||
channel.push(resolvedValue);
|
||||
} else {
|
||||
channel.push(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FUNC: {
|
||||
channel.push(value);
|
||||
func = true;
|
||||
nest++;
|
||||
if (REG_FN_MATH_START.test(value)) {
|
||||
mathFunc.add(nest);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IDENT: {
|
||||
// invalid channel key
|
||||
if (!colorChannel.includes(value)) {
|
||||
return new NullObject();
|
||||
}
|
||||
channel.push(value);
|
||||
if (!func) {
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NUM: {
|
||||
channel.push(Number(detail?.value));
|
||||
if (!func) {
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PAREN_OPEN: {
|
||||
channel.push(value);
|
||||
nest++;
|
||||
break;
|
||||
}
|
||||
case PAREN_CLOSE: {
|
||||
if (func) {
|
||||
const lastValue = channel[channel.length - 1];
|
||||
if (lastValue === ' ') {
|
||||
channel.splice(-1, 1, value);
|
||||
} else {
|
||||
channel.push(value);
|
||||
}
|
||||
if (mathFunc.has(nest)) {
|
||||
mathFunc.delete(nest);
|
||||
}
|
||||
nest--;
|
||||
if (nest === 0) {
|
||||
func = false;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PCT: {
|
||||
channel.push(Number(detail?.value) / MAX_PCT);
|
||||
if (!func) {
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case W_SPACE: {
|
||||
if (channel.length && func) {
|
||||
const lastValue = channel[channel.length - 1];
|
||||
if (typeof lastValue === 'number') {
|
||||
channel.push(value);
|
||||
} else if (
|
||||
isString(lastValue) &&
|
||||
!lastValue.endsWith('(') &&
|
||||
lastValue !== ' '
|
||||
) {
|
||||
channel.push(value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (type !== COMMENT && type !== EOF && func) {
|
||||
channel.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const channelValues = [];
|
||||
for (const channel of channels) {
|
||||
if (channel.length === 1) {
|
||||
const [resolvedValue] = channel;
|
||||
if (isStringOrNumber(resolvedValue)) {
|
||||
channelValues.push(resolvedValue);
|
||||
}
|
||||
} else if (channel.length) {
|
||||
const resolvedValue = serializeCalc(channel.join(''), {
|
||||
format
|
||||
});
|
||||
channelValues.push(resolvedValue);
|
||||
}
|
||||
}
|
||||
return channelValues as NumberOrStringColorChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
* extract origin color
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns origin color value
|
||||
*/
|
||||
export function extractOriginColor(
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): string | NullObject {
|
||||
const { currentColor = '', format = '' } = opt;
|
||||
if (isString(value)) {
|
||||
value = value.toLowerCase().trim();
|
||||
if (!value) {
|
||||
return new NullObject();
|
||||
}
|
||||
if (!REG_FN_REL_START.test(value)) {
|
||||
return value;
|
||||
}
|
||||
} else {
|
||||
return new NullObject();
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'extractOriginColor',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
if (cachedResult.isNull) {
|
||||
return cachedResult as NullObject;
|
||||
}
|
||||
return cachedResult.item as string;
|
||||
}
|
||||
if (/currentcolor/.test(value)) {
|
||||
if (currentColor) {
|
||||
value = value.replace(/currentcolor/g, currentColor);
|
||||
} else {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
}
|
||||
let colorSpace = '';
|
||||
if (REG_FN_REL_CAPT.test(value)) {
|
||||
[, colorSpace] = value.match(REG_FN_REL_CAPT) as MatchedRegExp;
|
||||
}
|
||||
opt.colorSpace = colorSpace;
|
||||
if (REG_COLOR_CAPT.test(value)) {
|
||||
const [, originColor] = value.match(REG_COLOR_CAPT) as MatchedRegExp;
|
||||
const [, restValue] = value.split(originColor) as MatchedRegExp;
|
||||
if (/^[a-z]+$/.test(originColor)) {
|
||||
if (
|
||||
!/^transparent$/.test(originColor) &&
|
||||
!Object.prototype.hasOwnProperty.call(NAMED_COLORS, originColor)
|
||||
) {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
} else if (format === VAL_SPEC) {
|
||||
const resolvedOriginColor = resolveColor(originColor, opt);
|
||||
if (isString(resolvedOriginColor)) {
|
||||
value = value.replace(originColor, resolvedOriginColor);
|
||||
}
|
||||
}
|
||||
if (format === VAL_SPEC) {
|
||||
const tokens = tokenize({ css: restValue });
|
||||
const channelValues = resolveColorChannels(tokens, opt);
|
||||
if (channelValues instanceof NullObject) {
|
||||
setCache(cacheKey, null);
|
||||
return channelValues;
|
||||
}
|
||||
const [v1, v2, v3, v4] = channelValues;
|
||||
let channelValue = '';
|
||||
if (isStringOrNumber(v4)) {
|
||||
channelValue = ` ${v1} ${v2} ${v3} / ${v4})`;
|
||||
} else {
|
||||
channelValue = ` ${channelValues.join(' ')})`;
|
||||
}
|
||||
if (restValue !== channelValue) {
|
||||
value = value.replace(restValue, channelValue);
|
||||
}
|
||||
}
|
||||
// nested relative color
|
||||
} else {
|
||||
const [, restValue] = value.split(REG_FN_REL_START) as MatchedRegExp;
|
||||
const tokens = tokenize({ css: restValue });
|
||||
const originColor: string[] = [];
|
||||
let nest = 0;
|
||||
while (tokens.length) {
|
||||
const [type, tokenValue] = tokens.shift() as [TokenType, string];
|
||||
switch (type) {
|
||||
case FUNC:
|
||||
case PAREN_OPEN: {
|
||||
originColor.push(tokenValue);
|
||||
nest++;
|
||||
break;
|
||||
}
|
||||
case PAREN_CLOSE: {
|
||||
const lastValue = originColor[originColor.length - 1];
|
||||
if (lastValue === ' ') {
|
||||
originColor.splice(-1, 1, tokenValue);
|
||||
} else if (isString(lastValue)) {
|
||||
originColor.push(tokenValue);
|
||||
}
|
||||
nest--;
|
||||
break;
|
||||
}
|
||||
case W_SPACE: {
|
||||
const lastValue = originColor[originColor.length - 1];
|
||||
if (
|
||||
isString(lastValue) &&
|
||||
!lastValue.endsWith('(') &&
|
||||
lastValue !== ' '
|
||||
) {
|
||||
originColor.push(tokenValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (type !== COMMENT && type !== EOF) {
|
||||
originColor.push(tokenValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nest === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const resolvedOriginColor = resolveRelativeColor(
|
||||
originColor.join('').trim(),
|
||||
opt
|
||||
);
|
||||
if (resolvedOriginColor instanceof NullObject) {
|
||||
setCache(cacheKey, null);
|
||||
return resolvedOriginColor;
|
||||
}
|
||||
const channelValues = resolveColorChannels(tokens, opt);
|
||||
if (channelValues instanceof NullObject) {
|
||||
setCache(cacheKey, null);
|
||||
return channelValues;
|
||||
}
|
||||
const [v1, v2, v3, v4] = channelValues;
|
||||
let channelValue = '';
|
||||
if (isStringOrNumber(v4)) {
|
||||
channelValue = ` ${v1} ${v2} ${v3} / ${v4})`;
|
||||
} else {
|
||||
channelValue = ` ${channelValues.join(' ')})`;
|
||||
}
|
||||
value = value.replace(restValue, `${resolvedOriginColor}${channelValue}`);
|
||||
}
|
||||
setCache(cacheKey, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* resolve relative color
|
||||
* @param value - CSS relative color value
|
||||
* @param [opt] - options
|
||||
* @returns resolved value
|
||||
*/
|
||||
export function resolveRelativeColor(
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): string | NullObject {
|
||||
const { format = '' } = opt;
|
||||
if (isString(value)) {
|
||||
if (REG_FN_VAR.test(value)) {
|
||||
if (format === VAL_SPEC) {
|
||||
return value;
|
||||
// var() must be resolved before resolveRelativeColor()
|
||||
} else {
|
||||
throw new SyntaxError(`Unexpected token ${FN_VAR} found.`);
|
||||
}
|
||||
} else if (!REG_FN_REL.test(value)) {
|
||||
return value;
|
||||
}
|
||||
value = value.toLowerCase().trim();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'resolveRelativeColor',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
if (cachedResult.isNull) {
|
||||
return cachedResult as NullObject;
|
||||
}
|
||||
return cachedResult.item as string;
|
||||
}
|
||||
const originColor = extractOriginColor(value, opt);
|
||||
if (originColor instanceof NullObject) {
|
||||
setCache(cacheKey, null);
|
||||
return originColor;
|
||||
}
|
||||
value = originColor;
|
||||
if (format === VAL_SPEC) {
|
||||
if (value.startsWith('rgba(')) {
|
||||
value = value.replace(/^rgba\(/, 'rgb(');
|
||||
} else if (value.startsWith('hsla(')) {
|
||||
value = value.replace(/^hsla\(/, 'hsl(');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
const tokens = tokenize({ css: value });
|
||||
const components = parseComponentValue(tokens) as ComponentValue;
|
||||
const parsedComponents = colorParser(components);
|
||||
if (!parsedComponents) {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
const {
|
||||
alpha: alphaComponent,
|
||||
channels: channelsComponent,
|
||||
colorNotation,
|
||||
syntaxFlags
|
||||
} = parsedComponents;
|
||||
let alpha: number | string;
|
||||
if (Number.isNaN(Number(alphaComponent))) {
|
||||
if (syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE)) {
|
||||
alpha = NONE;
|
||||
} else {
|
||||
alpha = 0;
|
||||
}
|
||||
} else {
|
||||
alpha = roundToPrecision(Number(alphaComponent), OCT);
|
||||
}
|
||||
let v1: number | string;
|
||||
let v2: number | string;
|
||||
let v3: number | string;
|
||||
[v1, v2, v3] = channelsComponent;
|
||||
let resolvedValue;
|
||||
if (REG_CS_CIE.test(colorNotation)) {
|
||||
const hasNone = syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE);
|
||||
if (Number.isNaN(v1)) {
|
||||
if (hasNone) {
|
||||
v1 = NONE;
|
||||
} else {
|
||||
v1 = 0;
|
||||
}
|
||||
} else {
|
||||
v1 = roundToPrecision(v1, HEX);
|
||||
}
|
||||
if (Number.isNaN(v2)) {
|
||||
if (hasNone) {
|
||||
v2 = NONE;
|
||||
} else {
|
||||
v2 = 0;
|
||||
}
|
||||
} else {
|
||||
v2 = roundToPrecision(v2, HEX);
|
||||
}
|
||||
if (Number.isNaN(v3)) {
|
||||
if (hasNone) {
|
||||
v3 = NONE;
|
||||
} else {
|
||||
v3 = 0;
|
||||
}
|
||||
} else {
|
||||
v3 = roundToPrecision(v3, HEX);
|
||||
}
|
||||
if (alpha === 1) {
|
||||
resolvedValue = `${colorNotation}(${v1} ${v2} ${v3})`;
|
||||
} else {
|
||||
resolvedValue = `${colorNotation}(${v1} ${v2} ${v3} / ${alpha})`;
|
||||
}
|
||||
} else if (REG_CS_HSL.test(colorNotation)) {
|
||||
if (Number.isNaN(v1)) {
|
||||
v1 = 0;
|
||||
}
|
||||
if (Number.isNaN(v2)) {
|
||||
v2 = 0;
|
||||
}
|
||||
if (Number.isNaN(v3)) {
|
||||
v3 = 0;
|
||||
}
|
||||
let [r, g, b] = convertColorToRgb(
|
||||
`${colorNotation}(${v1} ${v2} ${v3} / ${alpha})`
|
||||
) as ColorChannels;
|
||||
r = roundToPrecision(r / MAX_RGB, DEC);
|
||||
g = roundToPrecision(g / MAX_RGB, DEC);
|
||||
b = roundToPrecision(b / MAX_RGB, DEC);
|
||||
if (alpha === 1) {
|
||||
resolvedValue = `color(srgb ${r} ${g} ${b})`;
|
||||
} else {
|
||||
resolvedValue = `color(srgb ${r} ${g} ${b} / ${alpha})`;
|
||||
}
|
||||
} else {
|
||||
const cs = colorNotation === 'rgb' ? 'srgb' : colorNotation;
|
||||
const hasNone = syntaxFlags instanceof Set && syntaxFlags.has(KEY_NONE);
|
||||
if (Number.isNaN(v1)) {
|
||||
if (hasNone) {
|
||||
v1 = NONE;
|
||||
} else {
|
||||
v1 = 0;
|
||||
}
|
||||
} else {
|
||||
v1 = roundToPrecision(v1, DEC);
|
||||
}
|
||||
if (Number.isNaN(v2)) {
|
||||
if (hasNone) {
|
||||
v2 = NONE;
|
||||
} else {
|
||||
v2 = 0;
|
||||
}
|
||||
} else {
|
||||
v2 = roundToPrecision(v2, DEC);
|
||||
}
|
||||
if (Number.isNaN(v3)) {
|
||||
if (hasNone) {
|
||||
v3 = NONE;
|
||||
} else {
|
||||
v3 = 0;
|
||||
}
|
||||
} else {
|
||||
v3 = roundToPrecision(v3, DEC);
|
||||
}
|
||||
if (alpha === 1) {
|
||||
resolvedValue = `color(${cs} ${v1} ${v2} ${v3})`;
|
||||
} else {
|
||||
resolvedValue = `color(${cs} ${v1} ${v2} ${v3} / ${alpha})`;
|
||||
}
|
||||
}
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
}
|
||||
379
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/resolve.ts
generated
vendored
379
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/resolve.ts
generated
vendored
@@ -1,379 +0,0 @@
|
||||
/**
|
||||
* resolve
|
||||
*/
|
||||
|
||||
import {
|
||||
CacheItem,
|
||||
NullObject,
|
||||
createCacheKey,
|
||||
getCache,
|
||||
setCache
|
||||
} from './cache';
|
||||
import {
|
||||
convertRgbToHex,
|
||||
resolveColorFunc,
|
||||
resolveColorMix,
|
||||
resolveColorValue
|
||||
} from './color';
|
||||
import { isString } from './common';
|
||||
import { cssCalc } from './css-calc';
|
||||
import { resolveVar } from './css-var';
|
||||
import { resolveRelativeColor } from './relative-color';
|
||||
import {
|
||||
ComputedColorChannels,
|
||||
Options,
|
||||
SpecifiedColorChannels
|
||||
} from './typedef';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
FN_COLOR,
|
||||
FN_MIX,
|
||||
SYN_FN_CALC,
|
||||
SYN_FN_REL,
|
||||
SYN_FN_VAR,
|
||||
VAL_COMP,
|
||||
VAL_SPEC
|
||||
} from './constant';
|
||||
const NAMESPACE = 'resolve';
|
||||
const RGB_TRANSPARENT = 'rgba(0, 0, 0, 0)';
|
||||
|
||||
/* regexp */
|
||||
const REG_FN_CALC = new RegExp(SYN_FN_CALC);
|
||||
const REG_FN_REL = new RegExp(SYN_FN_REL);
|
||||
const REG_FN_VAR = new RegExp(SYN_FN_VAR);
|
||||
|
||||
/**
|
||||
* resolve color
|
||||
* @param value - CSS color value
|
||||
* @param [opt] - options
|
||||
* @returns resolved color
|
||||
*/
|
||||
export const resolveColor = (
|
||||
value: string,
|
||||
opt: Options = {}
|
||||
): string | NullObject => {
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const { currentColor = '', format = VAL_COMP, nullable = false } = opt;
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'resolve',
|
||||
value
|
||||
},
|
||||
opt
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
if (cachedResult.isNull) {
|
||||
return cachedResult as NullObject;
|
||||
}
|
||||
return cachedResult.item as string;
|
||||
}
|
||||
if (REG_FN_VAR.test(value)) {
|
||||
if (format === VAL_SPEC) {
|
||||
setCache(cacheKey, value);
|
||||
return value;
|
||||
}
|
||||
const resolvedValue = resolveVar(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
switch (format) {
|
||||
case 'hex':
|
||||
case 'hexAlpha': {
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
}
|
||||
default: {
|
||||
if (nullable) {
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
}
|
||||
const res = RGB_TRANSPARENT;
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value = resolvedValue;
|
||||
}
|
||||
}
|
||||
if (opt.format !== format) {
|
||||
opt.format = format;
|
||||
}
|
||||
value = value.toLowerCase();
|
||||
if (REG_FN_REL.test(value)) {
|
||||
const resolvedValue = resolveRelativeColor(value, opt);
|
||||
if (format === VAL_COMP) {
|
||||
let res;
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
if (nullable) {
|
||||
res = resolvedValue;
|
||||
} else {
|
||||
res = RGB_TRANSPARENT;
|
||||
}
|
||||
} else {
|
||||
res = resolvedValue;
|
||||
}
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
if (format === VAL_SPEC) {
|
||||
let res = '';
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
res = '';
|
||||
} else {
|
||||
res = resolvedValue;
|
||||
}
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
value = '';
|
||||
} else {
|
||||
value = resolvedValue;
|
||||
}
|
||||
}
|
||||
if (REG_FN_CALC.test(value)) {
|
||||
value = cssCalc(value, opt);
|
||||
}
|
||||
let cs = '';
|
||||
let r = NaN;
|
||||
let g = NaN;
|
||||
let b = NaN;
|
||||
let alpha = NaN;
|
||||
if (value === 'transparent') {
|
||||
switch (format) {
|
||||
case VAL_SPEC: {
|
||||
setCache(cacheKey, value);
|
||||
return value;
|
||||
}
|
||||
case 'hex': {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
case 'hexAlpha': {
|
||||
const res = '#00000000';
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
case VAL_COMP:
|
||||
default: {
|
||||
const res = RGB_TRANSPARENT;
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
} else if (value === 'currentcolor') {
|
||||
if (format === VAL_SPEC) {
|
||||
setCache(cacheKey, value);
|
||||
return value;
|
||||
}
|
||||
if (currentColor) {
|
||||
let resolvedValue;
|
||||
if (currentColor.startsWith(FN_MIX)) {
|
||||
resolvedValue = resolveColorMix(currentColor, opt);
|
||||
} else if (currentColor.startsWith(FN_COLOR)) {
|
||||
resolvedValue = resolveColorFunc(currentColor, opt);
|
||||
} else {
|
||||
resolvedValue = resolveColorValue(currentColor, opt);
|
||||
}
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
}
|
||||
[cs, r, g, b, alpha] = resolvedValue as ComputedColorChannels;
|
||||
} else if (format === VAL_COMP) {
|
||||
const res = RGB_TRANSPARENT;
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
} else if (format === VAL_SPEC) {
|
||||
if (value.startsWith(FN_MIX)) {
|
||||
const res = resolveColorMix(value, opt) as string;
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
} else if (value.startsWith(FN_COLOR)) {
|
||||
const [scs, rr, gg, bb, aa] = resolveColorFunc(
|
||||
value,
|
||||
opt
|
||||
) as SpecifiedColorChannels;
|
||||
let res = '';
|
||||
if (aa === 1) {
|
||||
res = `color(${scs} ${rr} ${gg} ${bb})`;
|
||||
} else {
|
||||
res = `color(${scs} ${rr} ${gg} ${bb} / ${aa})`;
|
||||
}
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
} else {
|
||||
const rgb = resolveColorValue(value, opt);
|
||||
if (isString(rgb)) {
|
||||
setCache(cacheKey, rgb);
|
||||
return rgb;
|
||||
}
|
||||
const [scs, rr, gg, bb, aa] = rgb as SpecifiedColorChannels;
|
||||
let res = '';
|
||||
if (scs === 'rgb') {
|
||||
if (aa === 1) {
|
||||
res = `${scs}(${rr}, ${gg}, ${bb})`;
|
||||
} else {
|
||||
res = `${scs}a(${rr}, ${gg}, ${bb}, ${aa})`;
|
||||
}
|
||||
} else if (aa === 1) {
|
||||
res = `${scs}(${rr} ${gg} ${bb})`;
|
||||
} else {
|
||||
res = `${scs}(${rr} ${gg} ${bb} / ${aa})`;
|
||||
}
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
}
|
||||
} else if (value.startsWith(FN_MIX)) {
|
||||
if (/currentcolor/.test(value)) {
|
||||
if (currentColor) {
|
||||
value = value.replace(/currentcolor/g, currentColor);
|
||||
}
|
||||
}
|
||||
if (/transparent/.test(value)) {
|
||||
value = value.replace(/transparent/g, RGB_TRANSPARENT);
|
||||
}
|
||||
const resolvedValue = resolveColorMix(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
}
|
||||
[cs, r, g, b, alpha] = resolvedValue as ComputedColorChannels;
|
||||
} else if (value.startsWith(FN_COLOR)) {
|
||||
const resolvedValue = resolveColorFunc(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
}
|
||||
[cs, r, g, b, alpha] = resolvedValue as ComputedColorChannels;
|
||||
} else if (value) {
|
||||
const resolvedValue = resolveColorValue(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
setCache(cacheKey, resolvedValue);
|
||||
return resolvedValue;
|
||||
}
|
||||
[cs, r, g, b, alpha] = resolvedValue as ComputedColorChannels;
|
||||
}
|
||||
let res = '';
|
||||
switch (format) {
|
||||
case 'hex': {
|
||||
if (
|
||||
Number.isNaN(r) ||
|
||||
Number.isNaN(g) ||
|
||||
Number.isNaN(b) ||
|
||||
Number.isNaN(alpha) ||
|
||||
alpha === 0
|
||||
) {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
res = convertRgbToHex([r, g, b, 1]);
|
||||
break;
|
||||
}
|
||||
case 'hexAlpha': {
|
||||
if (
|
||||
Number.isNaN(r) ||
|
||||
Number.isNaN(g) ||
|
||||
Number.isNaN(b) ||
|
||||
Number.isNaN(alpha)
|
||||
) {
|
||||
setCache(cacheKey, null);
|
||||
return new NullObject();
|
||||
}
|
||||
res = convertRgbToHex([r, g, b, alpha]);
|
||||
break;
|
||||
}
|
||||
case VAL_COMP:
|
||||
default: {
|
||||
switch (cs) {
|
||||
case 'rgb': {
|
||||
if (alpha === 1) {
|
||||
res = `${cs}(${r}, ${g}, ${b})`;
|
||||
} else {
|
||||
res = `${cs}a(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'lab':
|
||||
case 'lch':
|
||||
case 'oklab':
|
||||
case 'oklch': {
|
||||
if (alpha === 1) {
|
||||
res = `${cs}(${r} ${g} ${b})`;
|
||||
} else {
|
||||
res = `${cs}(${r} ${g} ${b} / ${alpha})`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// color()
|
||||
default: {
|
||||
if (alpha === 1) {
|
||||
res = `color(${cs} ${r} ${g} ${b})`;
|
||||
} else {
|
||||
res = `color(${cs} ${r} ${g} ${b} / ${alpha})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* resolve CSS color
|
||||
* @param value
|
||||
* - CSS color value
|
||||
* - system colors are not supported
|
||||
* @param [opt] - options
|
||||
* @param [opt.currentColor]
|
||||
* - color to use for `currentcolor` keyword
|
||||
* - if omitted, it will be treated as a missing color
|
||||
* i.e. `rgb(none none none / none)`
|
||||
* @param [opt.customProperty]
|
||||
* - custom properties
|
||||
* - pair of `--` prefixed property name and value,
|
||||
* e.g. `customProperty: { '--some-color': '#0000ff' }`
|
||||
* - and/or `callback` function to get the value of the custom property,
|
||||
* e.g. `customProperty: { callback: someDeclaration.getPropertyValue }`
|
||||
* @param [opt.dimension]
|
||||
* - dimension, convert relative length to pixels
|
||||
* - pair of unit and it's value as a number in pixels,
|
||||
* e.g. `dimension: { em: 12, rem: 16, vw: 10.26 }`
|
||||
* - and/or `callback` function to get the value as a number in pixels,
|
||||
* e.g. `dimension: { callback: convertUnitToPixel }`
|
||||
* @param [opt.format]
|
||||
* - output format, one of below
|
||||
* - `computedValue` (default), [computed value][139] of the color
|
||||
* - `specifiedValue`, [specified value][140] of the color
|
||||
* - `hex`, hex color notation, i.e. `rrggbb`
|
||||
* - `hexAlpha`, hex color notation with alpha channel, i.e. `#rrggbbaa`
|
||||
* @returns
|
||||
* - one of rgba?(), #rrggbb(aa)?, color-name, '(empty-string)',
|
||||
* color(color-space r g b / alpha), color(color-space x y z / alpha),
|
||||
* lab(l a b / alpha), lch(l c h / alpha), oklab(l a b / alpha),
|
||||
* oklch(l c h / alpha), null
|
||||
* - in `computedValue`, values are numbers, however `rgb()` values are
|
||||
* integers
|
||||
* - in `specifiedValue`, returns `empty string` for unknown and/or invalid
|
||||
* color
|
||||
* - in `hex`, returns `null` for `transparent`, and also returns `null` if
|
||||
* any of `r`, `g`, `b`, `alpha` is not a number
|
||||
* - in `hexAlpha`, returns `#00000000` for `transparent`,
|
||||
* however returns `null` if any of `r`, `g`, `b`, `alpha` is not a number
|
||||
*/
|
||||
export const resolve = (value: string, opt: Options = {}): string | null => {
|
||||
opt.nullable = false;
|
||||
const resolvedValue = resolveColor(value, opt);
|
||||
if (resolvedValue instanceof NullObject) {
|
||||
return null;
|
||||
}
|
||||
return resolvedValue as string;
|
||||
};
|
||||
87
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/typedef.ts
generated
vendored
87
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/typedef.ts
generated
vendored
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* typedef
|
||||
*/
|
||||
|
||||
/* type definitions */
|
||||
/**
|
||||
* @typedef Options - options
|
||||
* @property [alpha] - enable alpha
|
||||
* @property [colorSpace] - color space
|
||||
* @property [currentColor] - color for currentcolor
|
||||
* @property [customPropeerty] - custom properties
|
||||
* @property [d50] - white point in d50
|
||||
* @property [dimension] - dimension
|
||||
* @property [format] - output format
|
||||
* @property [key] - key
|
||||
*/
|
||||
export interface Options {
|
||||
alpha?: boolean;
|
||||
colorSpace?: string;
|
||||
currentColor?: string;
|
||||
customProperty?: Record<string, string | ((K: string) => string)>;
|
||||
d50?: boolean;
|
||||
delimiter?: string | string[];
|
||||
dimension?: Record<string, number | ((K: string) => number)>;
|
||||
format?: string;
|
||||
nullable?: boolean;
|
||||
preserveComment?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type ColorChannels - color channels
|
||||
*/
|
||||
export type ColorChannels = [x: number, y: number, z: number, alpha: number];
|
||||
|
||||
/**
|
||||
* @type StringColorChannels - color channels
|
||||
*/
|
||||
export type StringColorChannels = [
|
||||
x: string,
|
||||
y: string,
|
||||
z: string,
|
||||
alpha: string | undefined
|
||||
];
|
||||
|
||||
/**
|
||||
* @type StringColorSpacedChannels - specified value
|
||||
*/
|
||||
export type StringColorSpacedChannels = [
|
||||
cs: string,
|
||||
x: string,
|
||||
y: string,
|
||||
z: string,
|
||||
alpha: string | undefined
|
||||
];
|
||||
|
||||
/**
|
||||
* @type ComputedColorChannels - computed value
|
||||
*/
|
||||
export type ComputedColorChannels = [
|
||||
cs: string,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
alpha: number
|
||||
];
|
||||
|
||||
/**
|
||||
* @type SpecifiedColorChannels - specified value
|
||||
*/
|
||||
export type SpecifiedColorChannels = [
|
||||
cs: string,
|
||||
x: number | string,
|
||||
y: number | string,
|
||||
z: number | string,
|
||||
alpha: number | string
|
||||
];
|
||||
|
||||
/**
|
||||
* @type MatchedRegExp - matched regexp array
|
||||
*/
|
||||
export type MatchedRegExp = [
|
||||
match: string,
|
||||
gr1: string,
|
||||
gr2: string,
|
||||
gr3: string,
|
||||
gr4: string
|
||||
];
|
||||
336
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/util.ts
generated
vendored
336
capabilities/testdrive-jsui/node_modules/@asamuzakjp/css-color/src/js/util.ts
generated
vendored
@@ -1,336 +0,0 @@
|
||||
/**
|
||||
* util
|
||||
*/
|
||||
|
||||
import { TokenType, tokenize } from '@csstools/css-tokenizer';
|
||||
import { CacheItem, createCacheKey, getCache, setCache } from './cache';
|
||||
import { isString } from './common';
|
||||
import { resolveColor } from './resolve';
|
||||
import { Options } from './typedef';
|
||||
|
||||
/* constants */
|
||||
import { NAMED_COLORS } from './color';
|
||||
import { SYN_COLOR_TYPE, SYN_MIX, VAL_SPEC } from './constant';
|
||||
const {
|
||||
CloseParen: PAREN_CLOSE,
|
||||
Comma: COMMA,
|
||||
Comment: COMMENT,
|
||||
Delim: DELIM,
|
||||
EOF,
|
||||
Function: FUNC,
|
||||
Ident: IDENT,
|
||||
OpenParen: PAREN_OPEN,
|
||||
Whitespace: W_SPACE
|
||||
} = TokenType;
|
||||
const NAMESPACE = 'util';
|
||||
|
||||
/* numeric constants */
|
||||
const DEC = 10;
|
||||
const HEX = 16;
|
||||
const DEG = 360;
|
||||
const DEG_HALF = 180;
|
||||
|
||||
/* regexp */
|
||||
const REG_COLOR = new RegExp(`^(?:${SYN_COLOR_TYPE})$`);
|
||||
const REG_FN_COLOR =
|
||||
/^(?:(?:ok)?l(?:ab|ch)|color(?:-mix)?|hsla?|hwb|rgba?|var)\(/;
|
||||
const REG_MIX = new RegExp(SYN_MIX);
|
||||
|
||||
/**
|
||||
* split value
|
||||
* NOTE: comments are stripped, it can be preserved if, in the options param,
|
||||
* `delimiter` is either ',' or '/' and with `preserveComment` set to `true`
|
||||
* @param value - CSS value
|
||||
* @param [opt] - options
|
||||
* @returns array of values
|
||||
*/
|
||||
export const splitValue = (value: string, opt: Options = {}): string[] => {
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const { delimiter = ' ', preserveComment = false } = opt;
|
||||
const cacheKey: string = createCacheKey(
|
||||
{
|
||||
namespace: NAMESPACE,
|
||||
name: 'splitValue',
|
||||
value
|
||||
},
|
||||
{
|
||||
delimiter,
|
||||
preserveComment
|
||||
}
|
||||
);
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as string[];
|
||||
}
|
||||
let regDelimiter;
|
||||
if (delimiter === ',') {
|
||||
regDelimiter = /^,$/;
|
||||
} else if (delimiter === '/') {
|
||||
regDelimiter = /^\/$/;
|
||||
} else {
|
||||
regDelimiter = /^\s+$/;
|
||||
}
|
||||
const tokens = tokenize({ css: value });
|
||||
let nest = 0;
|
||||
let str = '';
|
||||
const res: string[] = [];
|
||||
while (tokens.length) {
|
||||
const [type, value] = tokens.shift() as [TokenType, string];
|
||||
switch (type) {
|
||||
case COMMA: {
|
||||
if (regDelimiter.test(value)) {
|
||||
if (nest === 0) {
|
||||
res.push(str.trim());
|
||||
str = '';
|
||||
} else {
|
||||
str += value;
|
||||
}
|
||||
} else {
|
||||
str += value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DELIM: {
|
||||
if (regDelimiter.test(value)) {
|
||||
if (nest === 0) {
|
||||
res.push(str.trim());
|
||||
str = '';
|
||||
} else {
|
||||
str += value;
|
||||
}
|
||||
} else {
|
||||
str += value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case COMMENT: {
|
||||
if (preserveComment && (delimiter === ',' || delimiter === '/')) {
|
||||
str += value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FUNC:
|
||||
case PAREN_OPEN: {
|
||||
str += value;
|
||||
nest++;
|
||||
break;
|
||||
}
|
||||
case PAREN_CLOSE: {
|
||||
str += value;
|
||||
nest--;
|
||||
break;
|
||||
}
|
||||
case W_SPACE: {
|
||||
if (regDelimiter.test(value)) {
|
||||
if (nest === 0) {
|
||||
if (str) {
|
||||
res.push(str.trim());
|
||||
str = '';
|
||||
}
|
||||
} else {
|
||||
str += ' ';
|
||||
}
|
||||
} else if (!str.endsWith(' ')) {
|
||||
str += ' ';
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (type === EOF) {
|
||||
res.push(str.trim());
|
||||
str = '';
|
||||
} else {
|
||||
str += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* extract dashed-ident tokens
|
||||
* @param value - CSS value
|
||||
* @returns array of dashed-ident tokens
|
||||
*/
|
||||
export const extractDashedIdent = (value: string): string[] => {
|
||||
if (isString(value)) {
|
||||
value = value.trim();
|
||||
} else {
|
||||
throw new TypeError(`${value} is not a string.`);
|
||||
}
|
||||
const cacheKey: string = createCacheKey({
|
||||
namespace: NAMESPACE,
|
||||
name: 'extractDashedIdent',
|
||||
value
|
||||
});
|
||||
const cachedResult = getCache(cacheKey);
|
||||
if (cachedResult instanceof CacheItem) {
|
||||
return cachedResult.item as string[];
|
||||
}
|
||||
const tokens = tokenize({ css: value });
|
||||
const items = new Set();
|
||||
while (tokens.length) {
|
||||
const [type, value] = tokens.shift() as [TokenType, string];
|
||||
if (type === IDENT && value.startsWith('--')) {
|
||||
items.add(value);
|
||||
}
|
||||
}
|
||||
const res = [...items] as string[];
|
||||
setCache(cacheKey, res);
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* is color
|
||||
* @param value - CSS value
|
||||
* @param [opt] - options
|
||||
* @returns result
|
||||
*/
|
||||
export const isColor = (value: unknown, opt: Options = {}): boolean => {
|
||||
if (isString(value)) {
|
||||
value = value.toLowerCase().trim();
|
||||
if (value && isString(value)) {
|
||||
if (/^[a-z]+$/.test(value)) {
|
||||
if (
|
||||
/^(?:currentcolor|transparent)$/.test(value) ||
|
||||
Object.prototype.hasOwnProperty.call(NAMED_COLORS, value)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else if (REG_COLOR.test(value) || REG_MIX.test(value)) {
|
||||
return true;
|
||||
} else if (REG_FN_COLOR.test(value)) {
|
||||
opt.nullable = true;
|
||||
if (!opt.format) {
|
||||
opt.format = VAL_SPEC;
|
||||
}
|
||||
const resolvedValue = resolveColor(value, opt);
|
||||
if (resolvedValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* value to JSON string
|
||||
* @param value - CSS value
|
||||
* @param [func] - stringify function
|
||||
* @returns stringified value in JSON notation
|
||||
*/
|
||||
export const valueToJsonString = (
|
||||
value: unknown,
|
||||
func: boolean = false
|
||||
): string => {
|
||||
if (typeof value === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
const res = JSON.stringify(value, (_key, val) => {
|
||||
let replacedValue;
|
||||
if (typeof val === 'undefined') {
|
||||
replacedValue = null;
|
||||
} else if (typeof val === 'function') {
|
||||
if (func) {
|
||||
replacedValue = val.toString().replace(/\s/g, '').substring(0, HEX);
|
||||
} else {
|
||||
replacedValue = val.name;
|
||||
}
|
||||
} else if (val instanceof Map || val instanceof Set) {
|
||||
replacedValue = [...val];
|
||||
} else if (typeof val === 'bigint') {
|
||||
replacedValue = val.toString();
|
||||
} else {
|
||||
replacedValue = val;
|
||||
}
|
||||
return replacedValue;
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* round to specified precision
|
||||
* @param value - numeric value
|
||||
* @param bit - minimum bits
|
||||
* @returns rounded value
|
||||
*/
|
||||
export const roundToPrecision = (value: number, bit: number = 0): number => {
|
||||
if (!Number.isFinite(value)) {
|
||||
throw new TypeError(`${value} is not a finite number.`);
|
||||
}
|
||||
if (!Number.isFinite(bit)) {
|
||||
throw new TypeError(`${bit} is not a finite number.`);
|
||||
} else if (bit < 0 || bit > HEX) {
|
||||
throw new RangeError(`${bit} is not between 0 and ${HEX}.`);
|
||||
}
|
||||
if (bit === 0) {
|
||||
return Math.round(value);
|
||||
}
|
||||
let val;
|
||||
if (bit === HEX) {
|
||||
val = value.toPrecision(6);
|
||||
} else if (bit < DEC) {
|
||||
val = value.toPrecision(4);
|
||||
} else {
|
||||
val = value.toPrecision(5);
|
||||
}
|
||||
return parseFloat(val);
|
||||
};
|
||||
|
||||
/**
|
||||
* interpolate hue
|
||||
* @param hueA - hue value
|
||||
* @param hueB - hue value
|
||||
* @param arc - shorter | longer | increasing | decreasing
|
||||
* @returns result - [hueA, hueB]
|
||||
*/
|
||||
export const interpolateHue = (
|
||||
hueA: number,
|
||||
hueB: number,
|
||||
arc: string = 'shorter'
|
||||
): [number, number] => {
|
||||
if (!Number.isFinite(hueA)) {
|
||||
throw new TypeError(`${hueA} is not a finite number.`);
|
||||
}
|
||||
if (!Number.isFinite(hueB)) {
|
||||
throw new TypeError(`${hueB} is not a finite number.`);
|
||||
}
|
||||
switch (arc) {
|
||||
case 'decreasing': {
|
||||
if (hueB > hueA) {
|
||||
hueA += DEG;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'increasing': {
|
||||
if (hueB < hueA) {
|
||||
hueB += DEG;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'longer': {
|
||||
if (hueB > hueA && hueB < hueA + DEG_HALF) {
|
||||
hueA += DEG;
|
||||
} else if (hueB > hueA + DEG_HALF * -1 && hueB <= hueA) {
|
||||
hueB += DEG;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'shorter':
|
||||
default: {
|
||||
if (hueB > hueA + DEG_HALF) {
|
||||
hueA += DEG;
|
||||
} else if (hueB < hueA + DEG_HALF * -1) {
|
||||
hueB += DEG;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [hueA, hueB];
|
||||
};
|
||||
21
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/LICENSE
generated
vendored
21
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 asamuzaK (Kazz)
|
||||
|
||||
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.
|
||||
200
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/README.md
generated
vendored
200
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/README.md
generated
vendored
@@ -1,200 +0,0 @@
|
||||
# DOM Selector
|
||||
|
||||
[](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
|
||||
[](https://github.com/asamuzaK/domSelector/actions/workflows/codeql.yml)
|
||||
[](https://www.npmjs.com/package/@asamuzakjp/dom-selector)
|
||||
|
||||
A CSS selector engine.
|
||||
Used in jsdom since [jsdom v23.2.0](https://github.com/jsdom/jsdom/releases/tag/23.2.0).
|
||||
|
||||
## Install
|
||||
|
||||
```console
|
||||
npm i @asamuzakjp/dom-selector
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import {
|
||||
matches, closest, querySelector, querySelectorAll
|
||||
} from '@asamuzakjp/dom-selector';
|
||||
```
|
||||
|
||||
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
|
||||
|
||||
### matches(selector, node, opt)
|
||||
|
||||
matches - same functionality as [Element.matches()][64]
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `selector` **[string][59]** CSS selector
|
||||
- `node` **[object][60]** Element node
|
||||
- `opt` **[object][60]?** options
|
||||
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
|
||||
|
||||
Returns **[boolean][61]** `true` if matched, `false` otherwise
|
||||
|
||||
|
||||
### closest(selector, node, opt)
|
||||
|
||||
closest - same functionality as [Element.closest()][65]
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `selector` **[string][59]** CSS selector
|
||||
- `node` **[object][60]** Element node
|
||||
- `opt` **[object][60]?** options
|
||||
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
|
||||
|
||||
Returns **[object][60]?** matched node
|
||||
|
||||
|
||||
### querySelector(selector, node, opt)
|
||||
|
||||
querySelector - same functionality as [Document.querySelector()][66], [DocumentFragment.querySelector()][67], [Element.querySelector()][68]
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `selector` **[string][59]** CSS selector
|
||||
- `node` **[object][60]** Document, DocumentFragment or Element node
|
||||
- `opt` **[object][60]?** options
|
||||
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
|
||||
|
||||
Returns **[object][60]?** matched node
|
||||
|
||||
|
||||
### querySelectorAll(selector, node, opt)
|
||||
|
||||
querySelectorAll - same functionality as [Document.querySelectorAll()][69], [DocumentFragment.querySelectorAll()][70], [Element.querySelectorAll()][71]
|
||||
**NOTE**: returns Array, not NodeList
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `selector` **[string][59]** CSS selector
|
||||
- `node` **[object][60]** Document, DocumentFragment or Element node
|
||||
- `opt` **[object][60]?** options
|
||||
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
|
||||
|
||||
Returns **[Array][62]<([object][60] \| [undefined][63])>** array of matched nodes
|
||||
|
||||
|
||||
## Supported CSS selectors
|
||||
|
||||
|Pattern|Supported|Note|
|
||||
|:--------|:-------:|:--------|
|
||||
|\*|✓| |
|
||||
|ns\|E|✓| |
|
||||
|\*\|E|✓| |
|
||||
|\|E|✓| |
|
||||
|E|✓| |
|
||||
|E:not(s1, s2, …)|✓| |
|
||||
|E:is(s1, s2, …)|✓| |
|
||||
|E:where(s1, s2, …)|✓| |
|
||||
|E:has(rs1, rs2, …)|✓| |
|
||||
|E.warning|✓| |
|
||||
|E#myid|✓| |
|
||||
|E\[foo\]|✓| |
|
||||
|E\[foo="bar"\]|✓| |
|
||||
|E\[foo="bar" i\]|✓| |
|
||||
|E\[foo="bar" s\]|✓| |
|
||||
|E\[foo~="bar"\]|✓| |
|
||||
|E\[foo^="bar"\]|✓| |
|
||||
|E\[foo$="bar"\]|✓| |
|
||||
|E\[foo*="bar"\]|✓| |
|
||||
|E\[foo\|="en"\]|✓| |
|
||||
|E:defined|Unsupported| |
|
||||
|E:dir(ltr)|✓| |
|
||||
|E:lang(en)|Partially supported|Comma-separated list of language codes, e.g. `:lang(en, fr)`, is not yet supported.|
|
||||
|E:any‑link|✓| |
|
||||
|E:link|✓| |
|
||||
|E:visited|✓|Returns `false` or `null` to prevent fingerprinting.|
|
||||
|E:local‑link|✓| |
|
||||
|E:target|✓| |
|
||||
|E:target‑within|✓| |
|
||||
|E:scope|✓| |
|
||||
|E:current|Unsupported| |
|
||||
|E:current(s)|Unsupported| |
|
||||
|E:past|Unsupported| |
|
||||
|E:future|Unsupported| |
|
||||
|E:active|Unsupported| |
|
||||
|E:hover|Unsupported| |
|
||||
|E:focus|✓| |
|
||||
|E:focus‑within|✓| |
|
||||
|E:focus‑visible|Unsupported| |
|
||||
|E:enabled<br>E:disabled|✓| |
|
||||
|E:read‑write<br>E:read‑only|✓| |
|
||||
|E:placeholder‑shown|✓| |
|
||||
|E:default|✓| |
|
||||
|E:checked|✓| |
|
||||
|E:indeterminate|✓| |
|
||||
|E:valid<br>E:invalid|✓| |
|
||||
|E:required<br>E:optional|✓| |
|
||||
|E:blank|Unsupported| |
|
||||
|E:user‑invalid|Unsupported| |
|
||||
|E:root|✓| |
|
||||
|E:empty|✓| |
|
||||
|E:nth‑child(n [of S]?)|✓| |
|
||||
|E:nth‑last‑child(n [of S]?)|✓| |
|
||||
|E:first‑child|✓| |
|
||||
|E:last‑child|✓| |
|
||||
|E:only‑child|✓| |
|
||||
|E:nth‑of‑type(n)|✓| |
|
||||
|E:nth‑last‑of‑type(n)|✓| |
|
||||
|E:first‑of‑type|✓| |
|
||||
|E:last‑of‑type|✓| |
|
||||
|E:only‑of‑type|✓| |
|
||||
|E F|✓| |
|
||||
|E > F|✓| |
|
||||
|E + F|✓| |
|
||||
|E ~ F|✓| |
|
||||
|F \|\| E|Unsupported| |
|
||||
|E:nth‑col(n)|Unsupported| |
|
||||
|E:nth‑last‑col(n)|Unsupported| |
|
||||
|:host|✓| |
|
||||
|:host(s)|✓| |
|
||||
|:host‑context(s)|✓| |
|
||||
|
||||
|
||||
<!--
|
||||
### Performance
|
||||
|
||||
TODO: rewrite benchmark table
|
||||
-->
|
||||
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
The following resources have been of great help in the development of the DOM Selector.
|
||||
|
||||
- [CSSTree](https://github.com/csstree/csstree)
|
||||
- [selery](https://github.com/danburzo/selery)
|
||||
- [jsdom](https://github.com/jsdom/jsdom)
|
||||
|
||||
|
||||
---
|
||||
Copyright (c) 2023 [asamuzaK (Kazz)](https://github.com/asamuzaK/)
|
||||
|
||||
|
||||
[1]: #matches
|
||||
[2]: #parameters
|
||||
[3]: #closest
|
||||
[4]: #parameters-1
|
||||
[5]: #queryselector
|
||||
[6]: #parameters-2
|
||||
[7]: #queryselectorall
|
||||
[8]: #parameters-3
|
||||
[59]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
|
||||
[60]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
|
||||
[61]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
|
||||
[62]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
|
||||
[63]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
|
||||
[64]: https://developer.mozilla.org/docs/Web/API/Element/matches
|
||||
[65]: https://developer.mozilla.org/docs/Web/API/Element/closest
|
||||
[66]: https://developer.mozilla.org/docs/Web/API/Document/querySelector
|
||||
[67]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector
|
||||
[68]: https://developer.mozilla.org/docs/Web/API/Element/querySelector
|
||||
[69]: https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll
|
||||
[70]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll
|
||||
[71]: https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll
|
||||
62
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/package.json
generated
vendored
62
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/package.json
generated
vendored
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "@asamuzakjp/dom-selector",
|
||||
"description": "A CSS selector engine.",
|
||||
"author": "asamuzaK",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/asamuzaK/domSelector#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/asamuzaK/domSelector/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/asamuzaK/domSelector.git"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"types"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"import": "./src/index.js",
|
||||
"require": "./dist/cjs/index.js"
|
||||
},
|
||||
"types": "types/index.d.ts",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^2.3.1",
|
||||
"is-potential-custom-element-name": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/css-tree": "^2.3.4",
|
||||
"benchmark": "^2.1.4",
|
||||
"c8": "^9.0.0",
|
||||
"chai": "^5.0.0",
|
||||
"commander": "^11.1.0",
|
||||
"esbuild": "^0.19.11",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.0.2",
|
||||
"eslint-plugin-regexp": "^2.1.2",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"happy-dom": "^12.10.3",
|
||||
"jsdom": "^23.1.0",
|
||||
"linkedom": "^0.16.6",
|
||||
"mocha": "^10.2.0",
|
||||
"sinon": "^17.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"wpt-runner": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"bench": "node benchmark/bench.js",
|
||||
"build": "npm run tsc && npm run lint && npm test && npm run compat",
|
||||
"compat": "esbuild --format=cjs --platform=node --outdir=dist/cjs/ --minify --sourcemap src/**/*.js",
|
||||
"lint": "eslint --fix .",
|
||||
"test": "c8 --reporter=text mocha --exit test/**/*.test.js",
|
||||
"test-wpt": "npm run update-wpt && node test/wpt/wpt-runner.js",
|
||||
"tsc": "npx tsc",
|
||||
"update-wpt": "git submodule update --init --recursive --remote"
|
||||
},
|
||||
"version": "2.0.2"
|
||||
}
|
||||
54
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/index.js
generated
vendored
54
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/index.js
generated
vendored
@@ -1,54 +0,0 @@
|
||||
/*!
|
||||
* DOM Selector - A CSS selector engine.
|
||||
* @license MIT
|
||||
* @copyright asamuzaK (Kazz)
|
||||
* @see {@link https://github.com/asamuzaK/domSelector/blob/main/LICENSE}
|
||||
*/
|
||||
|
||||
/* import */
|
||||
import { Matcher } from './js/matcher.js';
|
||||
|
||||
/**
|
||||
* matches
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {object} node - Element node
|
||||
* @param {object} [opt] - options
|
||||
* @param {boolean} [opt.warn] - console warn e.g. unsupported pseudo-class
|
||||
* @returns {boolean} - `true` if matched, `false` otherwise
|
||||
*/
|
||||
export const matches = (selector, node, opt) =>
|
||||
new Matcher(selector, node, opt).matches();
|
||||
|
||||
/**
|
||||
* closest
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {object} node - Element node
|
||||
* @param {object} [opt] - options
|
||||
* @param {boolean} [opt.warn] - console warn e.g. unsupported pseudo-class
|
||||
* @returns {?object} - matched node
|
||||
*/
|
||||
export const closest = (selector, node, opt) =>
|
||||
new Matcher(selector, node, opt).closest();
|
||||
|
||||
/**
|
||||
* querySelector
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {object} node - Document, DocumentFragment or Element node
|
||||
* @param {object} [opt] - options
|
||||
* @param {boolean} [opt.warn] - console warn e.g. unsupported pseudo-class
|
||||
* @returns {?object} - matched node
|
||||
*/
|
||||
export const querySelector = (selector, node, opt) =>
|
||||
new Matcher(selector, node, opt).querySelector();
|
||||
|
||||
/**
|
||||
* querySelectorAll
|
||||
* NOTE: returns Array, not NodeList
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {object} node - Document, DocumentFragment or Element node
|
||||
* @param {object} [opt] - options
|
||||
* @param {boolean} [opt.warn] - console warn e.g. unsupported pseudo-class
|
||||
* @returns {Array.<object|undefined>} - array of matched nodes
|
||||
*/
|
||||
export const querySelectorAll = (selector, node, opt) =>
|
||||
new Matcher(selector, node, opt).querySelectorAll();
|
||||
58
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/constant.js
generated
vendored
58
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/constant.js
generated
vendored
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* constant.js
|
||||
*/
|
||||
|
||||
/* string */
|
||||
export const ALPHA_NUM = '[A-Z\\d]+';
|
||||
export const AN_PLUS_B = 'AnPlusB';
|
||||
export const COMBINATOR = 'Combinator';
|
||||
export const IDENTIFIER = 'Identifier';
|
||||
export const NOT_SUPPORTED_ERR = 'NotSupportedError';
|
||||
export const NTH = 'Nth';
|
||||
export const RAW = 'Raw';
|
||||
export const SELECTOR = 'Selector';
|
||||
export const SELECTOR_ATTR = 'AttributeSelector';
|
||||
export const SELECTOR_CLASS = 'ClassSelector';
|
||||
export const SELECTOR_ID = 'IdSelector';
|
||||
export const SELECTOR_LIST = 'SelectorList';
|
||||
export const SELECTOR_PSEUDO_CLASS = 'PseudoClassSelector';
|
||||
export const SELECTOR_PSEUDO_ELEMENT = 'PseudoElementSelector';
|
||||
export const SELECTOR_TYPE = 'TypeSelector';
|
||||
export const STRING = 'String';
|
||||
export const SYNTAX_ERR = 'SyntaxError';
|
||||
export const U_FFFD = '\uFFFD';
|
||||
|
||||
/* numeric */
|
||||
export const BIT_01 = 1;
|
||||
export const BIT_02 = 2;
|
||||
export const BIT_04 = 4;
|
||||
export const BIT_08 = 8;
|
||||
export const BIT_16 = 0x10;
|
||||
export const BIT_32 = 0x20;
|
||||
export const BIT_HYPHEN = 0x2D;
|
||||
export const DUO = 2;
|
||||
export const HEX = 16;
|
||||
export const MAX_BIT_16 = 0xFFFF;
|
||||
export const TYPE_FROM = 8;
|
||||
export const TYPE_TO = -1;
|
||||
|
||||
/* Node */
|
||||
export const ELEMENT_NODE = 1;
|
||||
export const TEXT_NODE = 3;
|
||||
export const DOCUMENT_NODE = 9;
|
||||
export const DOCUMENT_FRAGMENT_NODE = 11;
|
||||
export const DOCUMENT_POSITION_PRECEDING = 2;
|
||||
export const DOCUMENT_POSITION_CONTAINS = 8;
|
||||
export const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
|
||||
|
||||
/* NodeFilter */
|
||||
export const SHOW_ALL = 0xffffffff;
|
||||
export const SHOW_DOCUMENT = 0x100;
|
||||
export const SHOW_DOCUMENT_FRAGMENT = 0x400;
|
||||
export const SHOW_ELEMENT = 1;
|
||||
|
||||
/* regexp */
|
||||
export const REG_LOGICAL_PSEUDO = /^(?:(?:ha|i)s|not|where)$/;
|
||||
export const REG_SHADOW_HOST = /^host(?:-context)?$/;
|
||||
export const REG_SHADOW_MODE = /^(?:close|open)$/;
|
||||
export const REG_SHADOW_PSEUDO = /^part|slotted$/;
|
||||
294
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/dom-util.js
generated
vendored
294
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/dom-util.js
generated
vendored
@@ -1,294 +0,0 @@
|
||||
/**
|
||||
* dom-util.js
|
||||
*/
|
||||
|
||||
/* import */
|
||||
import bidiFactory from 'bidi-js';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, DOCUMENT_POSITION_CONTAINS,
|
||||
DOCUMENT_POSITION_CONTAINED_BY, DOCUMENT_POSITION_PRECEDING, ELEMENT_NODE,
|
||||
REG_SHADOW_MODE, SYNTAX_ERR, TEXT_NODE
|
||||
} from './constant.js';
|
||||
|
||||
/**
|
||||
* is in shadow tree
|
||||
* @param {object} node - node
|
||||
* @returns {boolean} - result;
|
||||
*/
|
||||
export const isInShadowTree = (node = {}) => {
|
||||
let bool;
|
||||
if (node.nodeType === ELEMENT_NODE ||
|
||||
node.nodeType === DOCUMENT_FRAGMENT_NODE) {
|
||||
let refNode = node;
|
||||
while (refNode) {
|
||||
const { host, mode, nodeType, parentNode } = refNode;
|
||||
if (host && mode && nodeType === DOCUMENT_FRAGMENT_NODE &&
|
||||
REG_SHADOW_MODE.test(mode)) {
|
||||
bool = true;
|
||||
break;
|
||||
}
|
||||
refNode = parentNode;
|
||||
}
|
||||
}
|
||||
return !!bool;
|
||||
};
|
||||
|
||||
/**
|
||||
* get slotted text content
|
||||
* @param {object} node - Element node
|
||||
* @returns {?string} - text content
|
||||
*/
|
||||
export const getSlottedTextContent = (node = {}) => {
|
||||
let res;
|
||||
if (node.localName === 'slot' && isInShadowTree(node)) {
|
||||
const nodes = node.assignedNodes();
|
||||
if (nodes.length) {
|
||||
for (const item of nodes) {
|
||||
res = item.textContent.trim();
|
||||
if (res) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res = node.textContent.trim();
|
||||
}
|
||||
}
|
||||
return res ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* get directionality of node
|
||||
* @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
|
||||
* @param {object} node - Element node
|
||||
* @returns {?string} - 'ltr' / 'rtl'
|
||||
*/
|
||||
export const getDirectionality = (node = {}) => {
|
||||
let res;
|
||||
if (node.nodeType === ELEMENT_NODE) {
|
||||
const { dir: nodeDir, localName, parentNode } = node;
|
||||
const { getEmbeddingLevels } = bidiFactory();
|
||||
const regDir = /^(?:ltr|rtl)$/;
|
||||
if (regDir.test(nodeDir)) {
|
||||
res = nodeDir;
|
||||
} else if (nodeDir === 'auto') {
|
||||
let text;
|
||||
switch (localName) {
|
||||
case 'input': {
|
||||
if (!node.type || /^(?:(?:butto|hidde)n|(?:emai|te|ur)l|(?:rese|submi|tex)t|password|search)$/.test(node.type)) {
|
||||
text = node.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'slot': {
|
||||
text = getSlottedTextContent(node);
|
||||
break;
|
||||
}
|
||||
case 'textarea': {
|
||||
text = node.value;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const items = [].slice.call(node.childNodes);
|
||||
for (const item of items) {
|
||||
const {
|
||||
dir: itemDir, localName: itemLocalName, nodeType: itemNodeType,
|
||||
textContent: itemTextContent
|
||||
} = item;
|
||||
if (itemNodeType === TEXT_NODE) {
|
||||
text = itemTextContent.trim();
|
||||
} else if (itemNodeType === ELEMENT_NODE) {
|
||||
if (!/^(?:bdi|s(?:cript|tyle)|textarea)$/.test(itemLocalName) &&
|
||||
(!itemDir || !regDir.test(itemDir))) {
|
||||
if (itemLocalName === 'slot') {
|
||||
text = getSlottedTextContent(item);
|
||||
} else {
|
||||
text = itemTextContent.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
const { paragraphs: [{ level }] } = getEmbeddingLevels(text);
|
||||
if (level % 2 === 1) {
|
||||
res = 'rtl';
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
if (!res) {
|
||||
if (parentNode) {
|
||||
const { nodeType: parentNodeType } = parentNode;
|
||||
if (parentNodeType === ELEMENT_NODE) {
|
||||
res = getDirectionality(parentNode);
|
||||
} else if (parentNodeType === DOCUMENT_NODE ||
|
||||
parentNodeType === DOCUMENT_FRAGMENT_NODE) {
|
||||
res = 'ltr';
|
||||
}
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
} else if (localName === 'bdi') {
|
||||
const text = node.textContent.trim();
|
||||
if (text) {
|
||||
const { paragraphs: [{ level }] } = getEmbeddingLevels(text);
|
||||
if (level % 2 === 1) {
|
||||
res = 'rtl';
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
if (!(res || parentNode)) {
|
||||
res = 'ltr';
|
||||
}
|
||||
} else if (localName === 'input' && node.type === 'tel') {
|
||||
res = 'ltr';
|
||||
} else if (parentNode) {
|
||||
if (localName === 'slot') {
|
||||
const text = getSlottedTextContent(node);
|
||||
if (text) {
|
||||
const { paragraphs: [{ level }] } = getEmbeddingLevels(text);
|
||||
if (level % 2 === 1) {
|
||||
res = 'rtl';
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!res) {
|
||||
const { nodeType: parentNodeType } = parentNode;
|
||||
if (parentNodeType === ELEMENT_NODE) {
|
||||
res = getDirectionality(parentNode);
|
||||
} else if (parentNodeType === DOCUMENT_NODE ||
|
||||
parentNodeType === DOCUMENT_FRAGMENT_NODE) {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res = 'ltr';
|
||||
}
|
||||
}
|
||||
return res ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* is content editable
|
||||
* NOTE: not implemented in jsdom https://github.com/jsdom/jsdom/issues/1670
|
||||
* @param {object} node - Element node
|
||||
* @returns {boolean} - result
|
||||
*/
|
||||
export const isContentEditable = (node = {}) => {
|
||||
let res;
|
||||
if (node.nodeType === ELEMENT_NODE) {
|
||||
if (typeof node.isContentEditable === 'boolean') {
|
||||
res = node.isContentEditable;
|
||||
} else if (node.ownerDocument.designMode === 'on') {
|
||||
res = true;
|
||||
} else if (node.hasAttribute('contenteditable')) {
|
||||
const attr = node.getAttribute('contenteditable');
|
||||
if (attr === '' || /^(?:plaintext-only|true)$/.test(attr)) {
|
||||
res = true;
|
||||
} else if (attr === 'inherit') {
|
||||
let parent = node.parentNode;
|
||||
while (parent) {
|
||||
if (isContentEditable(parent)) {
|
||||
res = true;
|
||||
break;
|
||||
}
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* is namespace declared
|
||||
* @param {string} ns - namespace
|
||||
* @param {object} node - Element node
|
||||
* @returns {boolean} - result
|
||||
*/
|
||||
export const isNamespaceDeclared = (ns = '', node = {}) => {
|
||||
let res;
|
||||
if (ns && typeof ns === 'string' && node.nodeType === ELEMENT_NODE) {
|
||||
const attr = `xmlns:${ns}`;
|
||||
const root = node.ownerDocument.documentElement;
|
||||
let parent = node;
|
||||
while (parent) {
|
||||
if (typeof parent.hasAttribute === 'function' &&
|
||||
parent.hasAttribute(attr)) {
|
||||
res = true;
|
||||
break;
|
||||
} else if (parent === root) {
|
||||
break;
|
||||
}
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* is inclusive - nodeA and nodeB are in inclusive relation
|
||||
* @param {object} nodeA - Element node
|
||||
* @param {object} nodeB - Element node
|
||||
* @returns {boolean} - result
|
||||
*/
|
||||
export const isInclusive = (nodeA = {}, nodeB = {}) => {
|
||||
let res;
|
||||
if (nodeA.nodeType === ELEMENT_NODE && nodeB.nodeType === ELEMENT_NODE) {
|
||||
const posBit = nodeB.compareDocumentPosition(nodeA);
|
||||
res = posBit & DOCUMENT_POSITION_CONTAINS ||
|
||||
posBit & DOCUMENT_POSITION_CONTAINED_BY;
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* is preceding - nodeA precedes and/or contains nodeB
|
||||
* @param {object} nodeA - Element node
|
||||
* @param {object} nodeB - Element node
|
||||
* @returns {boolean} - result
|
||||
*/
|
||||
export const isPreceding = (nodeA = {}, nodeB = {}) => {
|
||||
let res;
|
||||
if (nodeA.nodeType === ELEMENT_NODE && nodeB.nodeType === ELEMENT_NODE) {
|
||||
const posBit = nodeB.compareDocumentPosition(nodeA);
|
||||
res = posBit & DOCUMENT_POSITION_PRECEDING ||
|
||||
posBit & DOCUMENT_POSITION_CONTAINS;
|
||||
}
|
||||
return !!res;
|
||||
};
|
||||
|
||||
/**
|
||||
* selector to node properties - e.g. ns|E -> { prefix: ns, tagName: E }
|
||||
* @param {string} selector - type selector
|
||||
* @param {object} [node] - Element node
|
||||
* @returns {object} - node properties
|
||||
*/
|
||||
export const selectorToNodeProps = (selector, node) => {
|
||||
let prefix;
|
||||
let tagName;
|
||||
if (selector && typeof selector === 'string') {
|
||||
if (selector.indexOf('|') > -1) {
|
||||
[prefix, tagName] = selector.split('|');
|
||||
} else {
|
||||
prefix = '*';
|
||||
tagName = selector;
|
||||
}
|
||||
} else {
|
||||
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
|
||||
}
|
||||
return {
|
||||
prefix,
|
||||
tagName
|
||||
};
|
||||
};
|
||||
3045
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js
generated
vendored
3045
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/matcher.js
generated
vendored
File diff suppressed because it is too large
Load Diff
222
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/parser.js
generated
vendored
222
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/src/js/parser.js
generated
vendored
@@ -1,222 +0,0 @@
|
||||
/**
|
||||
* parser.js
|
||||
*/
|
||||
|
||||
/* import */
|
||||
import { findAll, parse, toPlainObject, walk } from 'css-tree';
|
||||
|
||||
/* constants */
|
||||
import {
|
||||
DUO, HEX, MAX_BIT_16, BIT_HYPHEN, REG_LOGICAL_PSEUDO, REG_SHADOW_PSEUDO,
|
||||
SELECTOR, SELECTOR_PSEUDO_CLASS, SELECTOR_PSEUDO_ELEMENT, SYNTAX_ERR,
|
||||
TYPE_FROM, TYPE_TO, U_FFFD
|
||||
} from './constant.js';
|
||||
|
||||
/**
|
||||
* unescape selector
|
||||
* @param {string} selector - CSS selector
|
||||
* @returns {?string} - unescaped selector
|
||||
*/
|
||||
export const unescapeSelector = (selector = '') => {
|
||||
if (typeof selector === 'string' && selector.indexOf('\\', 0) >= 0) {
|
||||
const arr = selector.split('\\');
|
||||
const l = arr.length;
|
||||
for (let i = 1; i < l; i++) {
|
||||
let item = arr[i];
|
||||
if (item === '' && i === l - 1) {
|
||||
item = U_FFFD;
|
||||
} else {
|
||||
const hexExists = /^([\da-f]{1,6}\s?)/i.exec(item);
|
||||
if (hexExists) {
|
||||
const [, hex] = hexExists;
|
||||
let str;
|
||||
try {
|
||||
const low = parseInt('D800', HEX);
|
||||
const high = parseInt('DFFF', HEX);
|
||||
const deci = parseInt(hex, HEX);
|
||||
if (deci === 0 || (deci >= low && deci <= high)) {
|
||||
str = U_FFFD;
|
||||
} else {
|
||||
str = String.fromCodePoint(deci);
|
||||
}
|
||||
} catch (e) {
|
||||
str = U_FFFD;
|
||||
}
|
||||
let postStr = '';
|
||||
if (item.length > hex.length) {
|
||||
postStr = item.substring(hex.length);
|
||||
}
|
||||
item = `${str}${postStr}`;
|
||||
// whitespace
|
||||
} else if (/^[\n\r\f]/.test(item)) {
|
||||
item = '\\' + item;
|
||||
}
|
||||
}
|
||||
arr[i] = item;
|
||||
}
|
||||
selector = arr.join('');
|
||||
}
|
||||
return selector;
|
||||
};
|
||||
|
||||
/**
|
||||
* preprocess
|
||||
* @see https://drafts.csswg.org/css-syntax-3/#input-preprocessing
|
||||
* @param {...*} args - arguments
|
||||
* @returns {string} - filtered selector string
|
||||
*/
|
||||
export const preprocess = (...args) => {
|
||||
if (!args.length) {
|
||||
throw new TypeError('1 argument required, but only 0 present.');
|
||||
}
|
||||
let [selector] = args;
|
||||
if (typeof selector === 'string') {
|
||||
let index = 0;
|
||||
while (index >= 0) {
|
||||
index = selector.indexOf('#', index);
|
||||
if (index < 0) {
|
||||
break;
|
||||
}
|
||||
const preHash = selector.substring(0, index + 1);
|
||||
let postHash = selector.substring(index + 1);
|
||||
const codePoint = postHash.codePointAt(0);
|
||||
// @see https://drafts.csswg.org/selectors/#id-selectors
|
||||
// @see https://drafts.csswg.org/css-syntax-3/#ident-token-diagram
|
||||
if (codePoint === BIT_HYPHEN) {
|
||||
if (/^\d$/.test(postHash.substring(1, 2))) {
|
||||
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
|
||||
}
|
||||
// escape char above 0xFFFF
|
||||
} else if (codePoint > MAX_BIT_16) {
|
||||
const str = `\\${codePoint.toString(HEX)} `;
|
||||
if (postHash.length === DUO) {
|
||||
postHash = str;
|
||||
} else {
|
||||
postHash = `${str}${postHash.substring(DUO)}`;
|
||||
}
|
||||
}
|
||||
selector = `${preHash}${postHash}`;
|
||||
index++;
|
||||
}
|
||||
selector = selector.replace(/\f|\r\n?/g, '\n')
|
||||
.replace(/[\0\uD800-\uDFFF]|\\$/g, U_FFFD);
|
||||
} else if (selector === undefined || selector === null) {
|
||||
selector = Object.prototype.toString.call(selector)
|
||||
.slice(TYPE_FROM, TYPE_TO).toLowerCase();
|
||||
} else if (Array.isArray(selector)) {
|
||||
selector = selector.join(',');
|
||||
} else if (Object.prototype.hasOwnProperty.call(selector, 'toString')) {
|
||||
selector = selector.toString();
|
||||
} else {
|
||||
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
|
||||
}
|
||||
return selector;
|
||||
};
|
||||
|
||||
/**
|
||||
* create AST from CSS selector
|
||||
* @param {string} selector - CSS selector
|
||||
* @returns {object} - AST
|
||||
*/
|
||||
export const parseSelector = selector => {
|
||||
selector = preprocess(selector);
|
||||
// invalid selectors
|
||||
if (/^$|^\s*>|,\s*$/.test(selector)) {
|
||||
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
const ast = parse(selector, {
|
||||
context: 'selectorList',
|
||||
parseCustomProperty: true
|
||||
});
|
||||
res = toPlainObject(ast);
|
||||
} catch (e) {
|
||||
// workaround for https://github.com/csstree/csstree/issues/265
|
||||
// NOTE: still throws on `:lang("")`;
|
||||
const regLang = /(:lang\(\s*("[A-Za-z\d\-*]+")\s*\))/;
|
||||
if (e.message === 'Identifier is expected' && regLang.test(selector)) {
|
||||
const [, lang, range] = regLang.exec(selector);
|
||||
const escapedRange =
|
||||
range.replaceAll('*', '\\*').replace(/^"/, '').replace(/"$/, '');
|
||||
const escapedLang = lang.replace(range, escapedRange);
|
||||
res = parseSelector(selector.replace(lang, escapedLang));
|
||||
} else if (e.message === '"]" is expected' && !selector.endsWith(']')) {
|
||||
res = parseSelector(`${selector}]`);
|
||||
} else if (e.message === '")" is expected' && !selector.endsWith(')')) {
|
||||
res = parseSelector(`${selector})`);
|
||||
} else {
|
||||
throw new DOMException(e.message, SYNTAX_ERR);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* walk AST
|
||||
* @param {object} ast - AST
|
||||
* @returns {Array.<object|undefined>} - collection of AST branches
|
||||
*/
|
||||
export const walkAST = (ast = {}) => {
|
||||
const branches = new Set();
|
||||
let hasPseudoFunc;
|
||||
const opt = {
|
||||
enter: node => {
|
||||
if (node.type === SELECTOR) {
|
||||
branches.add(node.children);
|
||||
} else if ((node.type === SELECTOR_PSEUDO_CLASS &&
|
||||
REG_LOGICAL_PSEUDO.test(node.name)) ||
|
||||
(node.type === SELECTOR_PSEUDO_ELEMENT &&
|
||||
REG_SHADOW_PSEUDO.test(node.name))) {
|
||||
hasPseudoFunc = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(ast, opt);
|
||||
if (hasPseudoFunc) {
|
||||
findAll(ast, (node, item, list) => {
|
||||
if (list) {
|
||||
if (node.type === SELECTOR_PSEUDO_CLASS &&
|
||||
REG_LOGICAL_PSEUDO.test(node.name)) {
|
||||
const itemList = list.filter(i => {
|
||||
const { name, type } = i;
|
||||
const res =
|
||||
type === SELECTOR_PSEUDO_CLASS && REG_LOGICAL_PSEUDO.test(name);
|
||||
return res;
|
||||
});
|
||||
for (const { children } of itemList) {
|
||||
// SelectorList
|
||||
for (const { children: grandChildren } of children) {
|
||||
// Selector
|
||||
for (const { children: greatGrandChildren } of grandChildren) {
|
||||
if (branches.has(greatGrandChildren)) {
|
||||
branches.delete(greatGrandChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (node.type === SELECTOR_PSEUDO_ELEMENT &&
|
||||
REG_SHADOW_PSEUDO.test(node.name)) {
|
||||
const itemList = list.filter(i => {
|
||||
const { name, type } = i;
|
||||
const res =
|
||||
type === SELECTOR_PSEUDO_ELEMENT && REG_SHADOW_PSEUDO.test(name);
|
||||
return res;
|
||||
});
|
||||
for (const { children } of itemList) {
|
||||
// Selector
|
||||
for (const { children: grandChildren } of children) {
|
||||
if (branches.has(grandChildren)) {
|
||||
branches.delete(grandChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return [...branches];
|
||||
};
|
||||
|
||||
/* export */
|
||||
export { generate as generateCSS } from 'css-tree';
|
||||
12
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/types/index.d.ts
generated
vendored
12
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/types/index.d.ts
generated
vendored
@@ -1,12 +0,0 @@
|
||||
export function matches(selector: string, node: object, opt?: {
|
||||
warn?: boolean;
|
||||
}): boolean;
|
||||
export function closest(selector: string, node: object, opt?: {
|
||||
warn?: boolean;
|
||||
}): object | null;
|
||||
export function querySelector(selector: string, node: object, opt?: {
|
||||
warn?: boolean;
|
||||
}): object | null;
|
||||
export function querySelectorAll(selector: string, node: object, opt?: {
|
||||
warn?: boolean;
|
||||
}): Array<object | undefined>;
|
||||
45
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/types/js/constant.d.ts
generated
vendored
45
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/types/js/constant.d.ts
generated
vendored
@@ -1,45 +0,0 @@
|
||||
export const ALPHA_NUM: "[A-Z\\d]+";
|
||||
export const AN_PLUS_B: "AnPlusB";
|
||||
export const COMBINATOR: "Combinator";
|
||||
export const IDENTIFIER: "Identifier";
|
||||
export const NOT_SUPPORTED_ERR: "NotSupportedError";
|
||||
export const NTH: "Nth";
|
||||
export const RAW: "Raw";
|
||||
export const SELECTOR: "Selector";
|
||||
export const SELECTOR_ATTR: "AttributeSelector";
|
||||
export const SELECTOR_CLASS: "ClassSelector";
|
||||
export const SELECTOR_ID: "IdSelector";
|
||||
export const SELECTOR_LIST: "SelectorList";
|
||||
export const SELECTOR_PSEUDO_CLASS: "PseudoClassSelector";
|
||||
export const SELECTOR_PSEUDO_ELEMENT: "PseudoElementSelector";
|
||||
export const SELECTOR_TYPE: "TypeSelector";
|
||||
export const STRING: "String";
|
||||
export const SYNTAX_ERR: "SyntaxError";
|
||||
export const U_FFFD: "<22>";
|
||||
export const BIT_01: 1;
|
||||
export const BIT_02: 2;
|
||||
export const BIT_04: 4;
|
||||
export const BIT_08: 8;
|
||||
export const BIT_16: 16;
|
||||
export const BIT_32: 32;
|
||||
export const BIT_HYPHEN: 45;
|
||||
export const DUO: 2;
|
||||
export const HEX: 16;
|
||||
export const MAX_BIT_16: 65535;
|
||||
export const TYPE_FROM: 8;
|
||||
export const TYPE_TO: -1;
|
||||
export const ELEMENT_NODE: 1;
|
||||
export const TEXT_NODE: 3;
|
||||
export const DOCUMENT_NODE: 9;
|
||||
export const DOCUMENT_FRAGMENT_NODE: 11;
|
||||
export const DOCUMENT_POSITION_PRECEDING: 2;
|
||||
export const DOCUMENT_POSITION_CONTAINS: 8;
|
||||
export const DOCUMENT_POSITION_CONTAINED_BY: 16;
|
||||
export const SHOW_ALL: 4294967295;
|
||||
export const SHOW_DOCUMENT: 256;
|
||||
export const SHOW_DOCUMENT_FRAGMENT: 1024;
|
||||
export const SHOW_ELEMENT: 1;
|
||||
export const REG_LOGICAL_PSEUDO: RegExp;
|
||||
export const REG_SHADOW_HOST: RegExp;
|
||||
export const REG_SHADOW_MODE: RegExp;
|
||||
export const REG_SHADOW_PSEUDO: RegExp;
|
||||
@@ -1,8 +0,0 @@
|
||||
export function isInShadowTree(node?: object): boolean;
|
||||
export function getSlottedTextContent(node?: object): string | null;
|
||||
export function getDirectionality(node?: object): string | null;
|
||||
export function isContentEditable(node?: object): boolean;
|
||||
export function isNamespaceDeclared(ns?: string, node?: object): boolean;
|
||||
export function isInclusive(nodeA?: object, nodeB?: object): boolean;
|
||||
export function isPreceding(nodeA?: object, nodeB?: object): boolean;
|
||||
export function selectorToNodeProps(selector: string, node?: object): object;
|
||||
61
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/types/js/matcher.d.ts
generated
vendored
61
capabilities/testdrive-jsui/node_modules/@asamuzakjp/dom-selector/types/js/matcher.d.ts
generated
vendored
@@ -1,61 +0,0 @@
|
||||
export class Matcher {
|
||||
constructor(selector: string, node: object, opt?: {
|
||||
warn?: boolean;
|
||||
});
|
||||
_onError(e: Error): void;
|
||||
_setup(node: object): Array<object>;
|
||||
_sortLeaves(leaves: Array<object>): Array<object>;
|
||||
_correspond(selector: string): Array<Array<object | undefined>>;
|
||||
_traverse(node?: object, walker?: object): object | null;
|
||||
_collectNthChild(anb: {
|
||||
a: number;
|
||||
b: number;
|
||||
reverse?: boolean;
|
||||
selector?: object;
|
||||
}, node: object): Set<object>;
|
||||
_collectNthOfType(anb: {
|
||||
a: number;
|
||||
b: number;
|
||||
reverse?: boolean;
|
||||
}, node: object): Set<object>;
|
||||
_matchAnPlusB(ast: object, node: object, nthName: string): Set<object>;
|
||||
_matchPseudoElementSelector(astName: string, opt?: {
|
||||
forgive?: boolean;
|
||||
}): void;
|
||||
_matchDirectionPseudoClass(ast: object, node: object): object | null;
|
||||
_matchLanguagePseudoClass(ast: object, node: object): object | null;
|
||||
_matchHasPseudoFunc(leaves: Array<object>, node: object): boolean;
|
||||
_matchLogicalPseudoFunc(astData: object, node: object): object | null;
|
||||
_matchPseudoClassSelector(ast: object, node: object, opt?: {
|
||||
forgive?: boolean;
|
||||
}): Set<object>;
|
||||
_matchAttributeSelector(ast: object, node: object): object | null;
|
||||
_matchClassSelector(ast: object, node: object): object | null;
|
||||
_matchIDSelector(ast: object, node: object): object | null;
|
||||
_matchTypeSelector(ast: object, node: object, opt?: {
|
||||
forgive?: boolean;
|
||||
}): object | null;
|
||||
_matchShadowHostPseudoClass(ast: object, node: object): object | null;
|
||||
_matchSelector(ast: object, node: object, opt?: object): Set<object>;
|
||||
_matchLeaves(leaves: Array<object>, node: object, opt?: object): boolean;
|
||||
_findDescendantNodes(leaves: Array<object>, baseNode: object): object;
|
||||
_matchCombinator(twig: object, node: object, opt?: {
|
||||
dir?: string;
|
||||
forgive?: boolean;
|
||||
}): Set<object>;
|
||||
_findNode(leaves: Array<object>, opt?: {
|
||||
node?: object;
|
||||
tree?: object;
|
||||
}): object | null;
|
||||
_findEntryNodes(twig: object, targetType: string): object;
|
||||
_getEntryTwig(branch: Array<object>, targetType: string): object;
|
||||
_collectNodes(targetType: string): Array<Array<object | undefined>>;
|
||||
_sortNodes(nodes: Array<object> | Set<object>): Array<object | undefined>;
|
||||
_matchNodes(targetType: string): Set<object>;
|
||||
_find(targetType: string): Set<object>;
|
||||
matches(): boolean;
|
||||
closest(): object | null;
|
||||
querySelector(): object | null;
|
||||
querySelectorAll(): Array<object | undefined>;
|
||||
#private;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export function unescapeSelector(selector?: string): string | null;
|
||||
export function preprocess(...args: any[]): string;
|
||||
export function parseSelector(selector: string): object;
|
||||
export function walkAST(ast?: object): Array<object | undefined>;
|
||||
export { generate as generateCSS } from "css-tree";
|
||||
22
capabilities/testdrive-jsui/node_modules/@babel/code-frame/LICENSE
generated
vendored
22
capabilities/testdrive-jsui/node_modules/@babel/code-frame/LICENSE
generated
vendored
@@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other 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.
|
||||
19
capabilities/testdrive-jsui/node_modules/@babel/code-frame/README.md
generated
vendored
19
capabilities/testdrive-jsui/node_modules/@babel/code-frame/README.md
generated
vendored
@@ -1,19 +0,0 @@
|
||||
# @babel/code-frame
|
||||
|
||||
> Generate errors that contain a code frame that point to source locations.
|
||||
|
||||
See our website [@babel/code-frame](https://babeljs.io/docs/babel-code-frame) for more information.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save-dev @babel/code-frame
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add @babel/code-frame --dev
|
||||
```
|
||||
31
capabilities/testdrive-jsui/node_modules/@babel/code-frame/package.json
generated
vendored
31
capabilities/testdrive-jsui/node_modules/@babel/code-frame/package.json
generated
vendored
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@babel/code-frame",
|
||||
"version": "7.27.1",
|
||||
"description": "Generate errors that contain a code frame that point to source locations.",
|
||||
"author": "The Babel Team (https://babel.dev/team)",
|
||||
"homepage": "https://babel.dev/docs/en/next/babel-code-frame",
|
||||
"bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/babel/babel.git",
|
||||
"directory": "packages/babel-code-frame"
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"import-meta-resolve": "^4.1.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"type": "commonjs"
|
||||
}
|
||||
22
capabilities/testdrive-jsui/node_modules/@babel/compat-data/LICENSE
generated
vendored
22
capabilities/testdrive-jsui/node_modules/@babel/compat-data/LICENSE
generated
vendored
@@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other 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.
|
||||
19
capabilities/testdrive-jsui/node_modules/@babel/compat-data/README.md
generated
vendored
19
capabilities/testdrive-jsui/node_modules/@babel/compat-data/README.md
generated
vendored
@@ -1,19 +0,0 @@
|
||||
# @babel/compat-data
|
||||
|
||||
> The compat-data to determine required Babel plugins
|
||||
|
||||
See our website [@babel/compat-data](https://babeljs.io/docs/babel-compat-data) for more information.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save @babel/compat-data
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add @babel/compat-data
|
||||
```
|
||||
2
capabilities/testdrive-jsui/node_modules/@babel/compat-data/corejs2-built-ins.js
generated
vendored
2
capabilities/testdrive-jsui/node_modules/@babel/compat-data/corejs2-built-ins.js
generated
vendored
@@ -1,2 +0,0 @@
|
||||
// Todo (Babel 8): remove this file as Babel 8 drop support of core-js 2
|
||||
module.exports = require("./data/corejs2-built-ins.json");
|
||||
@@ -1,2 +0,0 @@
|
||||
// Todo (Babel 8): remove this file now that it is included in babel-plugin-polyfill-corejs3
|
||||
module.exports = require("./data/corejs3-shipped-proposals.json");
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user