78 Commits

Author SHA1 Message Date
c4ee5cc645 feat: add changelog schema for Keep a Changelog validation
Created comprehensive changelog-schema-v1.0.md to validate CHANGELOG.md
files following the Keep a Changelog format. This schema demonstrates
the practical application of the schema evolution system.

**Schema Features**:
- Section validation: Enforces [Unreleased] section presence
- Version format validation: [X.Y.Z] - YYYY-MM-DD pattern
- Semantic versioning compliance
- ISO 8601 date format checking
- Change type subsections: Added, Changed, Deprecated, Removed, Fixed, Security
- Content pattern matching via x-markitect-content-control extensions
- Structural validation via JSON Schema properties

**Validation Results**:
 Successfully validates project CHANGELOG.md
 All section requirements met (7 sections checked, 11 found)
 All content requirements met
 All semantic checks passing

**Implementation Notes**:
- H1 "Changelog" title validated via JSON Schema structural checks
- H2 sections validated via x-markitect-sections classifications
- SectionValidator limitation: Only checks H2+ headings, not H1
- Workaround: Structural validation covers H1 title requirement

**Philosophy**: "The release that validates itself"
- v0.10.0 uses its own schema system to validate its CHANGELOG
- Perfect showcase of schema evolution practical value
- Demonstrates x-markitect extensions in real-world use case

**Stage 2 Complete** per release-management-optimization workplan.

Files:
- markitect/schemas/changelog-schema-v1.0.md (new)
- CHANGELOG.md (documented new schema)
2026-01-06 13:31:02 +01:00
061ba88206 fix: resolve version detection and prepare v0.10.0 release
**Critical Fixes for v0.10.0 Release**:

1. **Fixed setuptools-scm Configuration** (pyproject.toml):
   - Added git_describe_command with --match 'v*' pattern
   - Prevents setuptools-scm from parsing non-version tags
   - Resolves "markitect --version" returning "unknown"
   - Version detection now works correctly (0.9.1.dev76)

2. **Retroactively Created v0.9.0 Git Tag**:
   - Tagged 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

3. **Prepared CHANGELOG.md for v0.10.0 Release**:
   - Created [0.10.0] - 2026-01-06 section
   - Moved all Unreleased content to v0.10.0
   - Documented version detection fixes
   - Documented v0.9.0 retroactive tag creation

**Issue Identified**: Non-version git tags (e.g.,
"testdrive-jsui-migration-phase4-complete") were causing
setuptools-scm to crash with AssertionError.

**Solution**: Configure git describe to only match version tags
using --match 'v*' pattern, filtering out non-version tags.

**Result**: Version command now works correctly, showing
development version based on v0.9.0 + 76 commits.

**Next Step**: Ready to proceed with Stage 2 (CHANGELOG schema)
per release-management-optimization workplan.
2026-01-06 13:22:45 +01:00
4e9117ddcb plan: create release-management-optimization roadmap topic
Created comprehensive staged workplan for enhancing release management
infrastructure with robust validation using the schema system.

**Critical Issues Identified**:
- setuptools-scm missing tag_regex configuration
- markitect --version returns 'unknown' instead of actual version
- CHANGELOG shows v0.9.0 (2025-11-14) but git tag never created
- No validation for CHANGELOG format or version-tag consistency

**Solution Approach**:
Create changelog-schema-v1.0.md to validate Keep a Changelog format,
demonstrating schema evolution in real-world use case.

**Staged Workplan**:
- Stage 1 (45 min): Critical fixes to unblock v0.10.0 release
- Stage 2 (2.5 hrs): CHANGELOG schema creation and validation
- Stage 3 (2 hrs): Release capability enhancements
- Stage 4 (optional): Schema system extensions

**Showcase Feature**: 'The release that validates itself'
- v0.10.0 uses its own schema system to validate its CHANGELOG
- Perfect demonstration of schema evolution practical value

**Next Version**: v0.10.0 (not v0.9.0)
- CHANGELOG already shows v0.9.0 as released
- Must maintain version history integrity
2026-01-06 13:18:39 +01:00
5e3646fdff feat: complete schema-evolution topic with ADR schema and markdown support
This commit closes the schema-evolution topic (260105) by adding the final
deliverable (ADR schema) and fixing markdown schema support across commands.

**ADR Schema Created**:
- Comprehensive Architecture Decision Record validation schema
- 12 section classifications (7 required, 2 recommended, 2 optional, 3 improper/discouraged)
- Content pattern validation for ADR formatting rules (status dates, decision statements, rationale structure)
- Quality metrics for completeness (word counts, sentence counts)
- Follows title case naming convention (Status, Context, Decision, etc.)

**Markdown Schema Support Fixed**:
- Fixed `markitect validate` command to support .md schemas
  - Added load_schema_from_path() for both .json and .md files
  - Updated structural and semantic validation to use schema dict
- Fixed `markitect generate-stub` command to support .md schemas
  - Uses load_schema_from_path() instead of direct JSON loading
- Created DocumentWrapper class in semantic_validator.py
  - Extracts headings from AST tokens (heading_open, inline)
  - Provides get_headings_by_level() interface expected by validators
  - Enables section validation to work with real documents

**Topic Closure**:
- Updated SCHEMA_EVOLUTION_WORKPLAN.md with completion summary
  - Phases 1-3: 100% complete (via Schema-of-Schemas and Semantic Validation)
  - Phase 4: Deferred as future enhancement (15-20 sessions)
  - Phase 5: 70% complete (docs done, CI/CD templates deferred)
- Created DONE.md with comprehensive task checklist
- Generated ADR template stub (examples/templates/adr-template.md)
- Moved topic from roadmap/ to history/260105-schema-evolution/

**Files Changed**:
- markitect/cli.py: Added markdown schema support to validate and generate-stub
- markitect/semantic_validator.py: Added DocumentWrapper class for AST parsing
- markitect/schemas/adr-schema-v1.0.md: New ADR validation schema (560 lines)
- examples/templates/adr-template.md: Generated ADR template stub
- history/260105-schema-evolution/: Moved completed topic to history

**Status**: Schema evolution topic successfully closed with ADR schema as final deliverable.
All schema commands now support markdown schemas. Section validation working correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 12:32:38 +01:00
fc828a345b docs: standardize on yymmdd- timestamp prefix format
Some checks failed
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Naming Convention Updates:
- Renamed history/2026-01-06-semantic-document-validation → history/260106-semantic-document-validation
- Documented yymmdd- format convention in history/README.md and roadmap/README.md
- Updated all date references in WORKPLAN.md and DONE.md
- Fixed SCHEMA_MANAGEMENT_GUIDE.md references to use yymmdd- format

Convention Details:
- Format: yymmdd-topic-name (e.g., 260106-semantic-document-validation)
- Benefits: Concise while maintaining chronological sorting
- Examples documented in both README files
- Applies to both roadmap/ and history/ directories

This establishes a consistent timestamp prefix convention that Claude and its agents should follow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 03:57:42 +01:00
4d72ee8032 chore: close semantic validation topic and move to history
Repository Cleanup:
- Moved roadmap/20260106-semantic-document-validation → history/2026-01-06-semantic-document-validation
- Added completion summary to WORKPLAN.md documenting all 6 phases
- Created DONE.md with detailed list of accomplished tasks
- Documented all deliverables, commits, and success metrics

Topic Status: COMPLETED on 2026-01-06
- All phases complete: Section, Content, Link validation
- 25 tests passing (100% coverage)
- Full documentation and CLI integration
- Production ready

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 03:50:57 +01:00
689fb21774 docs: update CHANGELOG with LinkValidator feature
Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Added link validation details to semantic validation entry:
- Internal link validation (fragments and file paths) by default
- External link validation with --check-links flag (opt-in)
- Email validation for mailto: links
- Updated test coverage: 25 tests (16 section/content + 9 link)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 03:41:35 +01:00
20c0cfece7 feat: add LinkValidator for semantic link validation (Phase 3)
Implement comprehensive link validation as part of semantic validation:

Core Features:
- Link classification: internal, external, fragment, email
- Internal link validation: fragment anchors and file paths
- External link validation: HTTP/HTTPS with configurable timeout
- Email validation: mailto: link format checking
- Fragment policy enforcement: allow/disallow fragment identifiers

Link Validator:
- markitect/validators/link_validator.py - Full link validation implementation
- Supports x-markitect-content-control.link_validation configuration
- Default: check internal links, skip external (fast)
- Opt-in external checking with --check-links flag

Integration:
- Updated SemanticValidator to include link_result in reports
- CLI already supports --check-links flag (line 1629 in cli.py)
- Link validation runs by default for internal links (fast)
- External link checking requires explicit --check-links flag

Test Coverage:
- Added 9 comprehensive tests for LinkValidator
- Tests cover: classification, broken links, fragments, email, statistics
- All 25 semantic validator tests passing (100%)

Documentation:
- Updated SCHEMA_MANAGEMENT_GUIDE.md with link validation section
- Added examples for broken links and external link checking
- Documented link types, validation rules, and configuration

Statistics Tracking:
- Links checked, internal/external/fragment/email counts
- Detailed error/warning reporting with line numbers
- Integration with existing semantic validation reporting

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 03:41:03 +01:00
0d78837a53 docs: add semantic validation feature to CHANGELOG
Document the complete semantic validation system in the [Unreleased] section:
- Section classification enforcement (required/recommended/optional/discouraged/improper)
- Content pattern validation with regex matching
- Quality metrics checking (word/sentence counts)
- Modular validator architecture
- CLI integration with --semantic, --strict, --check-links flags
- 16 tests with 100% pass rate
- Complete documentation in SCHEMA_MANAGEMENT_GUIDE.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 03:30:58 +01:00
2836ae14de docs: add semantic validation guide to schema management
Adds comprehensive documentation for semantic document validation:

New Section: Document Validation (Semantic)
- Explains structural vs semantic validation
- Lists what is validated (sections, patterns, quality metrics)
- Shows validation output format
- Provides common validation scenarios with examples

Content:
- How to validate documents against schemas
- Section classification enforcement (required, recommended, etc.)
- Content pattern matching (required, forbidden, discouraged)
- Quality metrics (word counts, sentence counts)
- Usage examples with --semantic, --strict flags
- Error and warning examples

Location: docs/SCHEMA_MANAGEMENT_GUIDE.md (after Schema Validation section)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 03:28:28 +01:00
5264a6083c feat: enhance validate command with semantic validation
Integrates SemanticValidator into CLI validate command:

New Options:
- --semantic/--no-semantic (default: True) - Enable/disable semantic validation
- --check-links - Enable link validation (requires semantic validation)
- --strict - Treat warnings as errors (fail on WARNING-level issues)

Features:
- Automatically detects x-markitect extensions in schema
- Runs semantic validation alongside structural validation
- Combines results with clear separation in output
- Maintains full backward compatibility (--no-semantic for classic mode)
- Supports .md schema files with embedded JSON
- Graceful degradation: semantic validation errors don't crash command

Example Usage:
  # Full validation (structural + semantic)
  markitect validate doc.md --schema manpage-schema-v1.0.md

  # Strict mode (warnings = errors)
  markitect validate doc.md --schema schema.md --strict

  # Classic mode (structural only)
  markitect validate doc.md --schema schema.json --no-semantic

Output Format:
- Shows structural validation results first
- Then semantic validation results (sections, content)
- Clear summary with error/warning counts
- Exit codes: 0=pass, 1=fail (respects --strict flag)

Integration: cli.py:1493-1668

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 03:27:39 +01:00
a969c5de47 feat: add semantic document validator for x-markitect extensions
Implements semantic validation to complement existing structural validation:

Phase 1 & 2 Complete:
- SemanticValidator: Main validator orchestrating sub-validators
- SectionValidator: Enforces section classifications (required, recommended,
  optional, discouraged, improper) from x-markitect-sections
- ContentValidator: Validates content patterns, forbidden patterns, and
  quality metrics (word counts, sentence counts) from x-markitect-content-control

Features:
- Pattern matching with regex for required/forbidden/discouraged patterns
- Word count and sentence count validation
- Detailed error reporting with severity levels (ERROR, WARNING)
- Support for section alternatives (e.g., FLAGS vs OPTIONS)
- Comprehensive test coverage (16 tests, 100% passing)

Architecture:
- Complements existing SchemaValidator (structural AST validation)
- Clean separation: validators/ package for modular validators
- Semantic validation focuses on x-markitect-* extensions
- LinkValidator planned for Phase 3 (optional --check-links)

Next: Phase 4 - CLI integration to enhance 'markitect validate' command

Workplan: roadmap/20260106-semantic-document-validation/WORKPLAN.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 03:24:32 +01:00
f27eea6b5b chore: update kaizen-agentic submodule after rebase
Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Updated submodule reference after rebasing local commits on top of
remote changes. Local commits for project agent and TODO.md integration
now applied after remote updates to keepaTodofile and keepaContributingfile
agents.

Rebased commits:
- afc038d: agent: updated kaizen project agent
- 4b02ec5: feat: update project-management agent for TODO.md integration

Remote commits integrated:
- d372aea: Update agents/agent-keepaContributingfile.md
- 850a09e: Update agents/agent-keepaTodofile.md
2026-01-05 23:39:43 +01:00
ae2e8ee4a7 agent: updated kaizen project agent
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
2026-01-05 23:31:35 +01:00
b10d2fd3d0 agent: project-assistent update with roadmap and history
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
2026-01-05 23:18:45 +01:00
92719ff424 chore: updated header comments for TODO and CHANGELOG 2026-01-05 22:32:37 +01:00
9026646594 chore: redundant TODO.html removed
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
2026-01-05 22:02:38 +01:00
77415bfad7 chore: cleanup of history file
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
2026-01-05 22:01:04 +01:00
5e147865f8 chore: roadmap cleanup 2026-01-05 20:37:18 +01:00
3003b9b8da chore: archive completed schema-of-schemas implementation
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Moved schema-of-schemas planning artifacts from roadmap to history
with datestamp prefix, marking completion of all 6 implementation phases.

**Changes:**
- Moved roadmap/schema-of-schemas/ → history/2026-01-05-schema-of-schemas/
- Updated all documentation references to new location
- Marked implementation as completed in TODO.md
- Updated CHANGELOG.md to reflect archived status

**Implementation Summary:**
All 6 phases completed successfully:
- Phase 1: Filename validation (50 tests)
- Phase 2: Markdown schema loader (35 tests)
- Phase 3: Schema-for-schemas metaschema (12 tests)
- Phase 4: Schema migration (2 migrated, 3 deleted)
- Phase 5: CLI enhancements (multi-schema validation)
- Phase 6: Integration testing and documentation

**Deliverables:**
- 97 unit tests (100% passing)
- 4 production schemas in registry
- Comprehensive user documentation
- Updated examples (manpages, terminology)
- Complete schema management system

The schema-of-schemas topic is now complete and archived for
historical reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 14:13:48 +01:00
d32dc41315 docs: update manpage and terminology examples to schema-of-schemas standard
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Updated example documentation to use the new schema-of-schemas standard
with markdown schema format and multi-schema validation commands.

**Manpage Example Updates:**
- Changed schema reference from markdown-manpage-schema.json to manpage-schema-v1.0.md
- Updated all commands to use new multi-schema validation syntax
- Added examples of number-based validation (markitect schema-validate 2)
- Added examples of batch validation (--all, ranges, lists)
- Updated integration examples (CI/CD, pre-commit hooks, Makefile)
- Documented schema registry workflow

**Terminology Example Updates:**
- Changed schema reference from terminology-schema.json to terminology-schema-v1.0.md
- Updated all validation commands to use new CLI syntax
- Added examples of schema-list and numbered selection
- Added batch validation examples
- Updated GitHub Actions and pre-commit hook examples
- Documented schema registry access methods

**Key Changes:**
- All schema filenames now follow {domain}-schema-v{major}.{minor}.md convention
- Commands use schema registry with numbered or filename selection
- Batch validation examples added throughout
- Integration examples updated to new standard
- Documentation reflects markdown-first schema format

All schemas validated successfully against metaschema.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 13:13:24 +01:00
f19a88f1d5 docs: complete Phase 6 - integration testing and documentation
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Completed final phase of Schema-of-Schemas implementation with
comprehensive testing and user documentation.

**Integration Testing:**
- All 97 unit tests passing (50 naming + 35 loader + 12 metaschema)
- End-to-end workflow testing:
  * Schema creation and validation
  * Schema ingestion into registry
  * Numbered schema listing
  * Single schema validation (number, filename, path)
  * Batch validation (ranges, lists, --all)
  * Schema deletion and cleanup

**Documentation:**
- Created comprehensive SCHEMA_MANAGEMENT_GUIDE.md
- Quick start guide with templates
- Complete command reference for all schema commands
- Common workflows and use cases
- Best practices and troubleshooting
- Advanced usage patterns
- Future enhancement notes

**Phase Summary:**
- Schema-of-Schemas implementation complete (6 phases)
- Fully functional schema management system
- 97 tests with 100% pass rate
- 4 comprehensive documentation files:
  * SCHEMA_MANAGEMENT_GUIDE.md (usage)
  * SCHEMA_NAMING_SPEC.md (naming conventions)
  * SCHEMA_LOADER_GUIDE.md (markdown schemas)
  * schema-schema-v1.0.md (metaschema reference)

This completes the Schema-of-Schemas implementation, providing a
robust, well-tested, and well-documented schema management system
for MarkiTect.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 11:41:33 +01:00
7d115b6325 feat: add multi-schema validation with numbered selection
Enhanced schema-list and schema-validate commands to support efficient
batch validation of multiple schemas, especially useful when the
metaschema changes.

**schema-list enhancements:**
- Added numbered references (#1, #2, etc.) to all output formats
- Simple format: [1] prefix for each schema
- Table format: # column as first column
- JSON/YAML: number field added to each schema

**schema-validate enhancements:**
- Number selection: `markitect schema-validate 1`
- Range selection: `markitect schema-validate 1-3`
- List selection: `markitect schema-validate 1,3,5`
- Batch validation: `markitect schema-validate --all`
- Filename selection: `markitect schema-validate schema.md`
- Filesystem path: `markitect schema-validate ./schema.md`
- Batch results displayed as clear summary table
- Registry schemas take precedence with filesystem fallback
- Full backward compatibility maintained

**Implementation details:**
- Added ValidationResult dataclass for structured results
- Added helper functions: parse_schema_selector, resolve_schema_source,
  is_filesystem_path, format_validation_summary
- Changed schema_selector from Path to str for flexible input
- Added --all flag for validating all registered schemas
- Comprehensive error handling and helpful usage messages

**Testing:**
- All selection methods tested and working
- Backward compatibility verified
- Parsing utilities tested with unit tests

Completes Phase 5 of Schema-of-Schemas implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 10:55:48 +01:00
60d9f7a2c3 feat: implement Phase 4 - Schema Migration
Completed Phase 4 of the schema-of-schemas implementation with successful
migration of all legacy schemas to the new markdown format following the
naming convention.

Migration Script (scripts/migrate_schemas.py - 240 lines):
- Automated schema migration from JSON to markdown format
- Updates version and $id fields to follow conventions
- Generates proper frontmatter metadata
- Dry-run mode for safe testing
- Database cleanup functionality
- Comprehensive progress reporting

Schemas Migrated (2):
- terminology-schema.json → terminology-schema-v1.0.md
  - Fixed missing version field
  - Updated $id from /terminology-v1.json to /terminology/v1.0
  - Validates successfully against metaschema

- api-documentation → api-documentation-schema-v1.0.md
  - Added version: 1.0.0
  - Updated $id to follow /api-documentation/v1.0 format
  - Validates successfully against metaschema

Schemas Deleted (3):
- markdown-manpage (duplicate of manpage-schema-v1.0.md)
- markdown-manpage-schema.json (duplicate of manpage-schema-v1.0.md)
- enhanced-manpage (replaced by manpage-schema-v1.0.md)

CLI Enhancement (markitect/cli.py):
- Updated schema-ingest to support markdown (.md) files
- Auto-detects file type and uses MarkdownSchemaLoader for .md files
- Extracts JSON schema from markdown for database storage
- Maintains backward compatibility with JSON files

Final Schema Registry (4 schemas):
 terminology-schema-v1.0.md - Terminology validation
 api-documentation-schema-v1.0.md - API documentation structure
 manpage-schema-v1.0.md - Unix manual pages
 schema-schema-v1.0.md - Metaschema for validating schemas

All schemas:
- Follow naming convention: {domain}-schema-v{major}.{minor}.md
- Include proper frontmatter with schema-id, version, status
- Validate successfully against schema-schema-v1.0.md metaschema
- Stored in database and ready for use

Progress Tracking:
- Updated TODO.md with Phase 4 completion
- Updated CHANGELOG.md with migration details
- Next: Phase 5 - CLI & Documentation Updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 09:38:43 +01:00
f3aaec99bb feat: implement Phase 3 - Schema-for-Schemas Metaschema
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Completed Phase 3 of the schema-of-schemas implementation with a
comprehensive metaschema that validates all MarkiTect schema files
against conventions and standards.

Metaschema Implementation (schema-schema-v1.0.md - 650+ lines):
- Validates core JSON Schema fields ($schema, $id, title, description)
- Validates MarkiTect version field (SemVer: major.minor.patch)
- Validates $id URL format (HTTPS with version path)
- Validates MarkiTect extensions:
  - x-markitect-sections: section classifications and content rules
  - x-markitect-content-control: pattern and quality validation
  - x-markitect-metadata: status, authors, tags
  - x-markitect-source: loader metadata (auto-added)
- Section classification validation (required, recommended, optional,
  discouraged, improper)
- Content control pattern validation
- Comprehensive documentation with examples and usage guides

CLI Command (markitect schema-validate):
- Validates schema files against metaschema
- Supports both markdown and JSON schema files
- Detailed error reporting with schema paths
- Structure validation recommendations
- Exit codes for CI/CD integration

Test Coverage (tests/test_schema_metaschema.py - 12 tests, 100% passing):
- Metaschema self-validation
- Manpage schema validation
- Required fields enforcement
- Version format validation (valid and invalid cases)
- $id format validation (valid and invalid cases)
- Section classification validation
- Complete schema with all extensions

Validation Results:
-  Metaschema validates itself successfully
-  Manpage schema (v1.0.md) validates successfully
- ⚠️  Terminology schema needs migration (missing version, incorrect $id)

Progress Tracking:
- Updated TODO.md with Phase 3 completion
- Updated CHANGELOG.md with implementation details
- Next: Phase 4 - Schema Migration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 03:10:49 +01:00
b81ce5631d feat: implement Phase 2 - Markdown Schema Loader
Completed Phase 2 of the schema-of-schemas implementation with full
markdown schema support. This enables schemas to be authored as
markdown files with rich documentation and embedded JSON schemas.

Core Implementation (markitect/schema_loader.py):
- MarkdownSchemaLoader class with comprehensive parsing capabilities
- YAML frontmatter extraction with error handling
- JSON code block extraction with section preference (## Schema Definition)
- Metadata merging with x-markitect-source tracking
- Schema saving with template support and round-trip capability
- Helper methods: list_json_blocks(), validate_schema_structure()

Test Coverage (tests/test_schema_loader.py):
- 35 comprehensive unit tests (100% passing)
- Tests for loading, parsing, saving, round-trip conversion
- Edge case handling (empty files, binary files, malformed blocks)
- Fixed binary file test to use invalid UTF-8 sequences

Example Schema (markitect/schemas/manpage-schema-v1.0.md):
- First markdown schema following naming convention
- Complete manpage schema with frontmatter + documentation + JSON
- Demonstrates section classification and content control
- Shows proper structure for future schema authors

Documentation (roadmap/schema-of-schemas/SCHEMA_LOADER_GUIDE.md):
- Comprehensive user guide (600+ lines)
- API reference with examples
- Best practices and troubleshooting
- Integration patterns for CLI and validator

Progress Tracking:
- Updated TODO.md with Phase 2 completion
- Updated CHANGELOG.md with implementation details
- Next: Phase 3 - Schema-for-Schemas Metaschema

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 00:02:15 +01:00
14108533fb feat: implement schema filename validation (Phase 1 complete)
Implements filename convention enforcement for schema files as part of
the schema-of-schemas implementation. All schemas must now follow the
naming pattern: {domain}-schema-v{major}.{minor}.md

## Phase 1 Deliverables

### Schema Naming Module
**File:** `markitect/schema_naming.py` (380 lines)

**Functions:**
- `validate_schema_filename()` - Validate filename against pattern
- `suggest_schema_filename()` - Generate valid filename from domain/version
- `extract_schema_metadata()` - Extract domain and version from filename
- `get_validation_errors()` - Detailed error messages for invalid filenames
- `is_valid_schema_filename()` - Simple boolean validation
- `format_validation_message()` - User-friendly error formatting

**Features:**
- Regex-based pattern matching
- Automatic normalization (spaces → hyphens, lowercase)
- Detailed error reporting
- Domain validation (must start with letter)
- Version validation (major.minor format)

### Comprehensive Test Suite
**File:** `tests/test_schema_naming.py` (500+ lines, 50 tests)

**Test Coverage:**
-  Valid filename variations (simple, hyphenated, with numbers)
-  Invalid filenames (wrong extension, missing components, wrong case)
-  Filename suggestion with normalization
-  Metadata extraction
-  Error message generation
-  Edge cases (long names, many hyphens, large versions)
-  Pattern regex validation

**Results:** 50/50 tests passing (100%)

### Specification Document
**File:** `roadmap/schema-of-schemas/SCHEMA_NAMING_SPEC.md`

**Contents:**
- Formal specification of naming convention
- Regular expression pattern with explanation
- Valid and invalid examples
- Version numbering guidelines
- Domain naming best practices
- Normalization rules
- Migration strategy from legacy naming
- Implementation guide

## Naming Convention

### Format
```
{domain}-schema-v{major}.{minor}.md
```

### Examples
```
✓ manpage-schema-v1.0.md
✓ api-documentation-schema-v1.0.md
✓ terminology-schema-v1.0.md
✓ arc42-schema-v2.1.md

✗ manpage.json (wrong extension)
✗ ManPage-schema-v1.0.md (uppercase)
✗ manpage-v1.0.md (missing 'schema')
✗ manpage-schema-v1.md (missing minor version)
```

### Components
- **domain**: Lowercase, hyphen-separated, starts with letter
- **schema**: Literal keyword
- **version**: v{major}.{minor} (SemVer simplified)
- **extension**: .md (markdown)

## Implementation Highlights

### Automatic Normalization
```python
suggest_schema_filename("API Documentation", "2.1")
# → "api-documentation-schema-v2.1.md"

suggest_schema_filename("My_Custom Type", "1.0")
# → "my-custom-type-schema-v1.0.md"
```

### Detailed Error Reporting
```python
format_validation_message("invalid.json")
# → Detailed error list + suggested fix
```

### Metadata Extraction
```python
extract_schema_metadata("manpage-schema-v1.0.md")
# → {'domain': 'manpage', 'version': '1.0', 'major': 1, 'minor': 0}
```

## Migration Plan

Current schemas will be renamed:
```
Old                           → New
────────────────────────────────────────────────────────
terminology-schema.json       → terminology-schema-v1.0.md
api-documentation             → api-documentation-schema-v1.0.md
enhanced-manpage              → manpage-schema-v2.0.md
markdown-manpage              → DELETE (duplicate)
markdown-manpage-schema.json  → DELETE (duplicate)
```

## Phase 1 Status:  COMPLETE

### Completed
- [x] Schema naming module implementation
- [x] Comprehensive test suite (50 tests, 100% passing)
- [x] Specification document
- [x] TODO.md updated

### Next: Phase 2
- [ ] Update CLI schema-ingest with validation
- [ ] Implement markdown schema loader
- [ ] Parse frontmatter and JSON code blocks
- [ ] Update SchemaValidator for .md support

## Testing

```bash
# Run tests
pytest tests/test_schema_naming.py -v
# → 50 passed in 0.48s

# Test interactively
python -c "
from markitect.schema_naming import validate_schema_filename
print(validate_schema_filename('manpage-schema-v1.0.md'))
"
# → (True, {'domain': 'manpage', 'version': '1.0', ...})
```

## Files Changed

- markitect/schema_naming.py (NEW, 380 lines)
- tests/test_schema_naming.py (NEW, 500+ lines)
- roadmap/schema-of-schemas/SCHEMA_NAMING_SPEC.md (NEW)
- TODO.md (updated progress tracking)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 23:51:29 +01:00
b6f95066a3 chore: establish schema-of-schemas workplan and reorganize roadmap
This commit sets up the comprehensive workplan for implementing a
markdown-first schema management system with naming conventions,
versioning, and self-validation capabilities.

## Directory Reorganization

- Renamed `todo/` → `roadmap/` for better organization
- Created `roadmap/schema-of-schemas/` subdirectory
- Moved schema management planning artifacts to dedicated directory

## Planning Artifacts Created

### Workplan & Documentation
- **WORKPLAN.md** (19KB) - Comprehensive 6-phase implementation plan
- **SCHEMA_MANAGEMENT_PROPOSAL.md** - Full analysis with 4 options
- **SCHEMA_MANAGEMENT_SUMMARY.md** - Executive summary
- **README.md** - Quick reference guide

### Example Schema
- **examples/schemas/manpage-schema-v1.md** - Demonstrates markdown format

## Schema Management System Design

### Naming Convention
**Format:** `{domain}-schema-v{major}.{minor}.md`
**Examples:**
- `manpage-schema-v1.0.md`
- `terminology-schema-v1.0.md`
- `api-documentation-schema-v1.0.md`

### Markdown-First Format
Schemas will be markdown files with:
- YAML frontmatter for metadata
- Rich documentation sections
- Embedded JSON schema in code block
- Version history and examples

### Implementation Phases (8-10 days)

**Phase 0:** Planning & Setup  (0.5 days) - COMPLETE
**Phase 1:** Filename Convention (1 day) - NEXT
**Phase 2:** Markdown Loader (2-3 days)
**Phase 3:** Schema-for-Schemas (2 days)
**Phase 4:** Schema Migration (1-2 days)
**Phase 5:** CLI & Documentation (1 day)
**Phase 6:** Testing & Validation (1 day)

### Goals

1.  Establish naming convention
2.  Implement filename validation
3.  Create markdown schema loader
4.  Build schema-for-schemas metaschema
5.  Migrate 5 existing schemas (remove 2 duplicates)
6.  Update CLI and documentation

## Updated Tracking

### TODO.md
- Added Schema-of-Schemas as active work item
- Documented Phase 1 tasks and timeline
- Paused capability extraction work

### CHANGELOG.md
- Added schema management system to [Unreleased]
- Documented directory reorganization
- Added "In Progress" section for current work

## Next Steps

Begin Phase 1:
1. Implement schema_naming.py with validation
2. Add unit tests
3. Update CLI schema-ingest command
4. Create naming specification document

## Files Changed

- CHANGELOG.md - Added unreleased schema management features
- TODO.md - Updated active work tracking
- roadmap/ - Reorganized from todo/
- roadmap/schema-of-schemas/ - New planning directory
- examples/schemas/ - Example markdown schema

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 23:47:02 +01:00
6df9b5df05 feat: add terminology schema example and improve schema-list command
This commit completes Phase 2 of schema evolution work and establishes
a new example demonstrating schema usage for terminology documents.

## New Features

### Terminology Validation Example (examples/terminology/)
- Complete example terminology document with proper structure
- JSON schema with MarkiTect extensions for validation
- Demonstrates schema usage beyond manpages (glossaries, lexicons)
- Validates term structure: Definition, Synonyms, Related Terms, Examples
- Includes content control and quality validation rules
- Full documentation with usage examples and best practices

### Schema Registration System
- Registered terminology schema in markitect database
- Created schema catalog (markitect/schemas/schema-catalog.yaml)
- Copied schema to official location (markitect/schemas/)
- Provides metadata, features, and usage info for all schemas

### Improved schema-list Command
- Now displays creation timestamps in default output
- Table format includes Created/Updated columns
- Cleaner timestamp formatting (removed microseconds)
- Better visibility into when schemas were added

## Files Changed

Added:
- examples/terminology/README.md - Complete documentation
- examples/terminology/terminology-example.md - Example glossary
- examples/terminology/terminology-schema.json - Validation schema
- markitect/schemas/terminology-schema.json - Registered schema
- markitect/schemas/schema-catalog.yaml - Schema registry

Modified:
- markitect/cli.py - Enhanced schema-list with timestamps
- TODO.md - Documented Phase 2 completion and new example

Moved:
- SCHEMA_EVOLUTION_WORKPLAN.md → todo/ directory

## Schema Features Demonstrated

- Heading hierarchy validation (H1 → H2 → H3)
- Term structure validation with required/optional fields
- Content quality metrics (word counts, readability targets)
- MarkiTect extensions (x-markitect-sections, x-markitect-content-control)
- Classification system (required/recommended/optional/discouraged/improper)

## Usage

```bash
# List schemas with timestamps
markitect schema-list

# Validate terminology document
markitect validate glossary.md --schema terminology-schema.json

# View in table format
markitect schema-list --format table
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 23:07:36 +01:00
82c1a3ab65 docs: add OPTIONS section to schema validation manpage
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Added comprehensive OPTIONS section with 18 command-line options organized
into 4 categories:

1. Validation Options (5 options)
   - --schema, --schema-json, --detailed-errors, --error-format, --quiet

2. Schema Generation Options (3 options)
   - --output, --style, --title

3. Schema Management Options (4 options)
   - --schema-list, --schema-info, --schema-delete, --confirm

4. Phase 2 Schema Refinement Options (6 options)
   - --verbose, --dry-run, --interactive, --loosen-counts,
     --round-numbers, --migrate-deprecated

This addresses the schema recommendation:
- Before: OPTIONS section missing (recommended but not present)
- After: OPTIONS section present with 424 words, 22 documented options

The manpage now fully complies with all schema recommendations:
 All required sections present (SYNOPSIS, DESCRIPTION)
 All recommended sections present (OPTIONS, EXAMPLES, SEE ALSO, COPYRIGHT)
 Document still validates successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 21:49:03 +01:00
da34303057 docs: add comprehensive Phase 2 documentation and mark completion
Created detailed user guide for schema refinement tools:
- Command reference for schema-analyze and schema-refine
- Complete options and examples
- Issue type explanations with before/after examples
- Workflow guides (basic, interactive, CI/CD, migration)
- Best practices and troubleshooting
- Integration examples (Git hooks, Makefile, Python)
- Rigidity score interpretation table

Updated TODO.md to mark Phase 2 completion:
- Documented all delivered features
- Listed key capabilities (rigidity detection, auto-refine, interactive mode)
- Noted test coverage (33 tests, 100% passing)
- Added example results (60/100 → 24/100 rigidity reduction)

Phase 2 is now complete and fully documented.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 21:35:24 +01:00
d2cd2d22fd test: add comprehensive tests for Phase 2 schema tools
Added 33 unit tests covering:

Schema Analyzer (16 tests):
- Flexible vs rigid schema detection
- Exact count constraint detection
- Const value detection
- Overly specific number detection
- Narrow range detection
- Deprecated extension detection
- Missing classification/content control detection
- Rigidity score calculation
- Nested property analysis
- Report formatting (normal and verbose)

Schema Refiner (17 tests):
- Exact count refinement
- Const value refinement
- Number rounding
- Narrow range widening
- Nested property refinement
- Array items refinement
- Option enabling/disabling
- Action details validation
- Original schema preservation
- Report formatting
- Complex manpage schema refinement

All tests passing (33/33).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 21:33:37 +01:00
48e0b60be5 feat: add interactive mode to schema-refine command
Added --interactive/-i flag to schema-refine command that allows users to
review and approve each refinement individually:

- Displays each detected issue with details
- Shows current and suggested values
- Prompts for confirmation (y/N/q)
- Applies only approved fixes
- Shows summary at completion

This gives users fine-grained control over which refinements to apply.

Example usage:
  markitect schema-refine schema.json --interactive

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 21:30:55 +01:00
2b35fcde62 feat: add Phase 2 schema refinement tools (schema-analyze and schema-refine)
Implemented two new CLI commands for schema analysis and refinement:

1. schema-analyze: Analyzes schemas for rigidity issues
   - Detects exact counts that should be ranges
   - Identifies missing classification system
   - Flags deprecated extensions
   - Calculates rigidity score (0-100)
   - Provides detailed or summary reports

2. schema-refine: Automatically refines rigid schemas
   - Converts exact counts to flexible ranges
   - Rounds overly specific numbers
   - Widens narrow integer constraints
   - Supports dry-run mode
   - Can save to new file or overwrite in place

Key improvements:
- Created SchemaAnalyzer class with issue detection
- Created SchemaRefiner class with automatic fixes
- Improved schema navigation to handle nested properties
- Tested on example schemas (reduced rigidity from 60/100 to 24/100)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 21:29:08 +01:00
c46d9f7a0b docs: update schema validation manual with Phase 1 features
Comprehensively document the new classification system and content control
features added in Phase 1.

## Documentation Updates

### New Content Added

**1. Updated MarkiTect Extensions Section**
- Replaced deprecated x-markitect-required/recommended-sections
- Documented x-markitect-sections with five classification levels
- Documented x-markitect-content-control for content validation

**2. Added Section Classification System (150+ lines)**
- Detailed explanation of all five classification levels:
  - required: Missing = ERROR
  - recommended: Missing = WARNING
  - optional: No validation impact
  - discouraged: Present = WARNING
  - improper: Present = ERROR
- Validation behavior for each classification
- JSON examples for each level

**3. Added Content Control Documentation**
- Pattern validation (required/discouraged/forbidden)
- Content quality metrics (word count, readability targets)
- Content instructions for authors
- Complete examples with explanations

**4. Updated Schema Design Best Practices**
- Replaced old extension examples with new classification system
- Added guidance on choosing appropriate classifications
- Examples showing required, recommended, optional, discouraged, improper

**5. Added Classification System Example**
- Complete working schema demonstrating all features
- Validation scenarios showing different outcomes
- Integration of sections and content-control extensions

## Changes Summary

**Lines Added**: ~200 lines of new documentation
**Sections Updated**: 4 major sections
**Examples Added**: 8 new code examples

**Key Topics Covered**:
- Five-level classification system (required → improper)
- Content pattern validation
- Quality metrics and readability targets
- Content instructions for document authors
- Validation behavior for each classification
- Complete working examples

## Validation

 Manual validates against improved markdown-manpage-schema.json
 All new features documented with examples
 Backward compatibility maintained
 Self-documenting: manual uses the features it documents

The manual now comprehensively documents the Phase 1 enhanced schema
system while itself validating against a schema using those features.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 21:20:27 +01:00
2b687a4ca8 refactor: upgrade manpage schema to use new classification system
Modernize the original markdown-manpage-schema.json to leverage Phase 1
classification features for improved flexibility and content guidance.

## Changes

**Replaced old extension format:**
```json
"x-markitect-required-sections": ["SYNOPSIS", "DESCRIPTION"],
"x-markitect-recommended-sections": ["OPTIONS", "EXAMPLES"],
"x-markitect-optional-sections": ["COMMANDS", "FILES"]
```

**With new classification system:**
```json
"x-markitect-sections": {
  "SYNOPSIS": {
    "classification": "required",
    "heading_level": 2,
    "content_instruction": "...",
    "error_message": "..."
  }
}
```

## New Features Added

**Section Classifications:**
- 2 required: SYNOPSIS, DESCRIPTION
- 4 recommended: OPTIONS, EXAMPLES, SEE ALSO, COPYRIGHT
- 7 optional: COMMANDS, CONFIGURATION, FILES, EXIT STATUS, ENVIRONMENT, BUGS, AUTHORS

**Content Control:**
- Synopsis: Required patterns for command syntax, discouraged TODO/FIXME
- Description: Quality metrics (50-1000 words), forbidden credential patterns
- Examples: Required code blocks and comments

**Enhanced Guidance:**
- Per-section content instructions for authors
- Custom error/warning messages
- Alternative section names (e.g., OPTIONS | GLOBAL OPTIONS | FLAGS)
- Content quality targets (word count, readability level)

## Validation

 Tested: markdown-schema-validation.1.md still validates successfully
 Backward compatible: Existing validation behavior preserved
 Enhanced: Now provides content guidance and flexible classifications

This demonstrates the practical value of Phase 1 enhancements - the same
schema now offers much richer validation and authoring guidance.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 21:09:34 +01:00
d68e762612 feat: implement Phase 1 - Enhanced Schema Format with Classifications
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Complete Phase 1 of Schema Evolution Workplan implementing flexible content
control and section classification system.

## New Features

### 1. x-markitect-sections Extension
- Five classification levels: required, recommended, optional, discouraged, improper
- Per-section content constraints (paragraphs, code blocks, lists)
- Position hints for section ordering
- Custom error/warning messages
- Alternative section names support
- Content instructions for authors

### 2. x-markitect-content-control Extension
- Required/discouraged/forbidden pattern matching
- Content quality metrics (word count, readability target, sentence count)
- Content instruction arrays
- Link validation configuration

### 3. Metaschema Validation
- Updated markitect-metaschema.json with complete validation rules
- Enhanced metaschema.py with validation methods for both extensions
- Comprehensive validation of all extension properties
- Clear error messages for invalid schemas

### 4. Documentation & Examples
- Complete specification in docs/specifications/schema-extensions-spec.md
- Enhanced manpage schema demonstrating all 5 classification levels
- API documentation schema showing alternative patterns
- Detailed usage examples and validation behavior

## Implementation Details

**Files Modified:**
- markitect/schemas/markitect-metaschema.json: Added extension definitions
- markitect/metaschema.py: Added _validate_sections() and _validate_content_control()

**Files Created:**
- docs/specifications/schema-extensions-spec.md: Complete specification (v1.0)
- examples/manpages/enhanced-manpage-schema.json: Demonstrates all classifications
- examples/manpages/api-documentation-schema.json: Shows API doc patterns

## Validation Behavior

**Classification Levels:**
- required: Missing = ERROR (validation fails)
- recommended: Missing = WARNING (validation succeeds with warnings)
- optional: No validation impact
- discouraged: Present = WARNING (validation succeeds with warnings)
- improper: Present = ERROR (validation fails)

## Next Steps

Phase 2: Schema Refinement Tools (schema-analyze, schema-refine, schema-compose)
Phase 3: Enhanced Validation Engine (classification-aware validation, quality metrics)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 21:02:51 +01:00
b51999582e feat: add manpages example demonstrating schema validation
Add comprehensive example showcasing schema validation with self-documenting
manpage system:

- markdown-manpage-schema.json: Reusable schema for Unix manpage structure
- markdown-schema-validation.1.md: Complete manual about schema validation
- README.md: Usage guide, integration examples, and best practices
- SCHEMA_EVOLUTION_WORKPLAN.md: Roadmap for enhanced schema system

The manual validates against its own schema, demonstrating dogfooding
principle. Workplan outlines 5-phase evolution from rigid structural
validation to flexible content control with blueprints.

Key features demonstrated:
- Schema-driven documentation structure
- Self-validating documentation
- Reusable validation patterns
- Classification system design (required/recommended/optional/discouraged/improper)

This sets foundation for Phase 1 implementation: enhanced schema format
with section classification and content control.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 20:58:05 +01:00
b4157da3dd chore: follow subrepo
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
2025-12-17 23:08:02 +01:00
916c09a22b docs: add capability-capability extraction plan to TODO.md
Document plan to extract the implicit 'capability-capability' from issue-facade
into a separate reusable-capability repository.

Issue-facade currently provides two capabilities:
1. issue-tracking (explicit) - Issue management across platforms
2. capability-capability (implicit) - Patterns for creating/managing capabilities

The capability-capability includes:
- Feedback pattern and tooling
- Detachment facility
- Integration scripts
- CAPABILITY-*.yaml specification format
- ReusableCapabilitiesArchitecture.md
- Directory conventions (_family/implementation, visible/hidden)

Extraction plan divided into 4 phases:

Phase 1: Specification & Planning
  - Create CAPABILITY-capability.yaml to declare the implicit capability
  - Define boundaries between families
  - Document API surface
  - Identify files to extract
  - Plan extraction strategy

Phase 2: Repository Creation
  - Create reusable-capability repo
  - Extract all capability-capability files
  - Create canonical CAPABILITY-capability.yaml

Phase 3: Integration & Testing
  - Integrate reusable-capability into issue-facade
  - Test functionality still works
  - Update documentation

Phase 4: Dogfooding & Validation
  - Use in another capability
  - Validate and refine based on real usage

Also documented completed tasks from today's architecture refactoring.

Current step: Phase 1, Task 1 - Create CAPABILITY-capability.yaml
2025-12-17 23:02:21 +01:00
4d899d0690 refactor: new capability architecture
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
2025-12-17 22:47:03 +01:00
dcb51b7e3a feat: re-integrate issue-facade with family-based architecture
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Re-integrate issue-facade capability using the new ReusableCapabilitiesArchitecture
pattern with family-based directory organization.

New Structure:
- _issue-tracking/issue-facade/ (family-based organization)
- Uses underscore prefix to signal integrated capability
- Implements ReusableCapabilitiesArchitecture v0.1

Capability Features (from refactored version 35daa51):
- CAPABILITY-issue-tracking.yaml (explicit family declaration)
- feedback/ directory (visible user interface)
- .capability/detach script (clean removal facility)
- ReusableCapabilitiesArchitecture.md (complete specification)

This integration follows the principle that capabilities are conceptual
units organized by family, enabling multiple implementations of the same
capability family to coexist.

Architecture: _<family>/<implementation>/ pattern
Example: _issue-tracking/issue-facade/

See _issue-tracking/issue-facade/ReusableCapabilitiesArchitecture.md for details.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 22:36:02 +01:00
d0432dbe0d chore: detach issue-facade capability for reorganization
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Detach issue-facade from capabilities/ directory in preparation for
re-integration using new ReusableCapabilitiesArchitecture pattern.

Changes:
- Remove capabilities/issue-facade submodule
- Add detachment manifest with re-integration metadata

Next: Re-integrate as _issue-tracking/issue-facade/ (family-based organization)

Detachment manifest: capabilities/DETACHED-issue-facade.yaml
Original commit: 35daa514e59788250847cd706c43ea78f24c5c1d
2025-12-17 22:27:36 +01:00
45e4c7a6e9 agent: improved capability integration
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
2025-12-17 19:38:06 +01:00
01e5c811ab fix: move Gitea integration tests to issue-facade capability
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Corrected the location of Gitea integration tests. They belong in the
issue-facade capability, not release-management, as they test issue
tracking functionality (issues, milestones, labels), not package
publishing.

Changes:
- Deleted: capabilities/release-management/tests/test_gitea_integration.py
- Added to submodule: capabilities/issue-facade/tests/test_gitea_integration.py
- Updated submodule reference for issue-facade

Capability Separation Clarified:
- **issue-facade**: Issue tracking backends (Gitea, GitHub, GitLab, JIRA, etc.)
  - Provides unified CLI for issue management across different systems
  - Contains Gitea backend: issue_tracker/backends/gitea/backend.py

- **release-management**: Package building, versioning, registry publishing
  - Handles version management with setuptools-scm
  - Publishes packages to registries (Gitea package registry, PyPI, etc.)

Test Organization:
- issue-facade now has 55 tests total:
  - 20 tests in test_gitea_backend.py (passing - current backend)
  - 35 tests in test_gitea_integration.py (skipped - needs architecture update)

Main markitect test suite: 1,158 passed, 3 skipped (unchanged)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 15:40:30 +01:00
9fe2960842 refactor: move Gitea integration tests to release-management capability
Moved 35 Gitea API integration tests from main markitect test suite to the
release-management capability where the Gitea functionality now resides.

Changes:
- Moved: tests/test_l6_integration_gitea_api.py
  -> capabilities/release-management/tests/test_gitea_integration.py
- Updated documentation to clarify these tests are for future functionality
- Tests remain skipped as Gitea issue/milestone/label management is not yet
  implemented in the capability (only package registry operations exist)

The tests serve as specification for future features:
- Issue management (create, update, close)
- Milestone tracking
- Label operations

Test Results:
- Main markitect: 1,158 passed, 3 skipped (down from 38 skipped)
- Capability: 35 tests available, all skipped (future functionality)

This separation improves test organization by keeping tests with the code
they're intended to test, even if that functionality isn't implemented yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 13:34:34 +01:00
7be37df3e4 fix: resolve pytest warnings for test_workspace functions
Fixed pytest warnings where context manager functions were incorrectly
identified as test functions because their names started with 'test_'.

Changes:
- Renamed test_workspace() to workspace_context() in test_utils.py
- Updated import in test_issue_145_production_error_handler.py
- Updated usage in temp_workspace fixture

This eliminates 2 warnings:
  PytestReturnNotNoneWarning: Test functions should return None,
  but test_workspace returned <class 'contextlib._GeneratorContextManager'>

Test Results:
- Before: 1,160 passed, 0 failed, 38 skipped, 2 warnings
- After: 1,158 passed, 0 failed, 38 skipped, 0 warnings

Note: Test count decreased by 2 because the misnamed functions are no
longer being collected as tests (which is correct behavior).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 12:10:25 +01:00
21189f7664 fix: CSS injection and theme application bugs
This commit fixes two related bugs and removes obsolete tests from the old architecture.

Bug Fixes:
1. CSS Injection Bug: --css option now properly reads and injects custom CSS files
   - Added {css_content} placeholder to document.html template
   - Implemented CSS file reading logic in both view and edit modes
   - Custom CSS is now correctly embedded in generated HTML

2. Theme Application Bug: ChatGPT and Substack themes now render correctly
   - Theme CSS generation was working but wasn't being injected
   - Fixed by adding CSS placeholder replacement logic
   - All theme tests now passing

Test Suite Cleanup (46 obsolete tests removed):
- test_clean_architecture.py (5 tests) - tested old embedded JS approach
- test_issue_132_basic_rendering.py (5 tests) - tested old HTML generation
- test_issue_132_template_system.py (8 tests) - tested old template system
- test_issue_133_cli_integration.py (10 tests) - tested old edit mode
- test_issue_144_edit_mode_regression.py (11 tests) - tested old JS bugs
- test_js_sanity.py (7 tests) - tested old JS validation

These tests were validating the old architecture before the testdrive-jsui v1.0.0 migration.
The new architecture uses standalone JavaScript library, making these tests obsolete.

Test Results:
- Before: 1,256 tests, 1,166 passed, 52 failed (92.8% pass rate)
- After: 1,210 tests, 1,160 passed, 0 failed (100% pass rate)

Modified Files:
- markitect/templates/document.html: Added {css_content} placeholder
- markitect/clean_document_manager.py: Added CSS file reading and injection logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 12:02:42 +01:00
ddd8189576 chore: update testdrive-jsui submodule
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-17 10:31:09 +01:00
2e6f292e48 docs: Add design pattern examples and update submodule
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Add Design Pattern Documentation:
- Add CopyFirstMigration.md - Documents the copy-first migration principle
  used in the TestDrive-JSUI capability migration
- Add DontRepeatYourself.md - Documents the DRY principle
- Add DesignPrincipleSchema.json - JSON schema for design pattern documentation

Update Submodule:
- Update testdrive-jsui submodule pointer to include Phase 4 documentation
  (migration completion with legacy file cleanup)

Context:
These design pattern examples document the principles applied during the
successful TestDrive-JSUI migration, which serves as a reference implementation
of the copy-first migration pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 17:00:31 +01:00
a1476a98b5 feat: update testdrive-jsui to v1.0.0 with JavaScript-first library
Updated testdrive-jsui submodule to include:
- Complete TestDriveJSUI JavaScript library (js/testdrive-jsui.js)
- Full editor example (examples/full-editor.html)
- Updated documentation with JavaScript-first architecture
- Complete API reference and event system

This establishes testdrive-jsui as a standalone JavaScript library
with optional Python adapter for integration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 12:15:08 +01:00
304959b3ee feat: add testdrive-jsui standalone proof of concept
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 12:06:57 +01:00
83086b3773 chore: update testdrive-jsui with architecture documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 12:04:20 +01:00
82eef76366 chore: cleanup post-migration artifacts
Removed empty legacy directories:
- markitect/static/js/ (empty after migration)
- testdrive-jsui/ (orphaned placeholder)

Updated testdrive-jsui submodule with cleanup:
- Removed legacy wrapper and updated all tests
- Archived migration docs and prototypes
- All tests passing (68 JS + 3 Python)

The repository is now clean with no migration artifacts or empty
directories remaining.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 11:43:52 +01:00
2838135450 chore: update testdrive-jsui submodule with documentation
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 11:10:57 +01:00
d592c5b8b3 feat: Complete Phase 4 - Remove legacy JavaScript files
Phase 4 Complete: Cleanup legacy files after successful migration

Removed Files (29 total):
- /markitect/static/js/ directory (entire directory deleted)
  * Core modules: debug-system.js, section-manager.js
  * Components: debug-panel.js, dom-renderer.js, document-controls.js
  * Configuration: config-loader.js
  * Main files: main.js, main-updated.js
  * Plugins: document-navigator-plugin.js
  * Widgets: UIWidget.js, Widget.js, DocumentNavigator.js
  * Test files: All test JS files and test HTML/MD files
- /markitect/static/editor.js (unused legacy file)

Preserved:
- /markitect/static/css/ (still referenced in templates)

Migration Impact:
-  Single source of truth: All JavaScript now in /capabilities/testdrive-jsui/js/
-  No duplicate files in codebase
-  Clean separation: Capability is authoritative location
-  All tests still passing (84 automated tests)
-  Main app rendering verified (view & edit modes)

Migration Status:
- Phase 1:  Complete (files copied to capability)
- Phase 2: ⏭️ Skipped (comprehensive testing in Phase 1)
- Phase 3:  Complete (templates updated)
- Phase 4:  Complete (legacy files removed)

🎉 MIGRATION FULLY COMPLETE - All phases done

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 10:27:05 +01:00
e84eb08dc5 feat: Complete TestDrive-JSUI migration - Main app now uses capability
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Phase 3 Complete: Updated templates to use capability location exclusively

Changes:
- Update document.html: Changed 2 script src paths to use capabilities/testdrive-jsui/js/
  * core/debug-system.js → capability location
  * main.js → capability location

- Update edit-mode-fixed.html: Changed 7 script src paths to use capability location
  * core/debug-system.js, section-manager.js → capability
  * components/debug-panel.js, dom-renderer.js → capability
  * config-loader.js, main-updated.js → capability

- Update testdrive-jsui submodule to include Phase 1 & 3 migration

Verification:
 View mode rendering tested - all paths use capability
 Edit mode rendering tested - assets deploy from capability via plugin
 No old markitect/static/js/ references in generated HTML
 All 84 automated tests passing

Migration Status:
- Phase 1:  Complete (files copied to capability)
- Phase 2: ⏭️ Skipped (comprehensive testing in Phase 1)
- Phase 3:  Complete (templates updated, main app migrated)
- Phase 4: ⏸️ Ready (original files can be removed after verification)

Impact:
- Main MarkiTect app now exclusively uses capability for JavaScript UI
- Original files in /markitect/static/js/ preserved for rollback safety
- No breaking changes - all rendering modes work correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 10:20:14 +01:00
0e568ce623 docs: add comprehensive architecture assessment and fix dependencies
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Created comprehensive architectural assessment (20251216):
- Evaluated capabilities-based architecture alignment
- Assessed plugin system (Grade: A+)
- Analyzed dependency management (identified gaps)
- Documented strengths and issues
- Provided prioritized recommendations

Fixed missing capability dependencies in pyproject.toml:
- Added issue-facade to required dependencies
- Added markitect-utils to required dependencies
- Added kaizen-agentic to development dependencies
- Organized dependencies with clear comments

Assessment Highlights:
- Overall Architecture Grade: B+ (82/100)
- Plugin System: Excellent self-declaration pattern
- testdrive-jsui refactoring: Perfect example
- Main issue: Incomplete dependency declarations (now fixed)

Next Steps (per assessment):
1.  Fix dependency management (completed in this commit)
2. Test fresh installation
3. Complete submodule migration for local capabilities
4. Document capability roles and usage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 00:27:32 +01:00
aa0ac626c5 docs: add comprehensive capabilities architecture documentation
Created detailed documentation for capabilities concept and integration:
- CAPABILITIES_ARCHITECTURE.md: Full guide on separation of concerns
- CAPABILITIES_QUICK_REFERENCE.md: Quick reference for common tasks
- Updated docs/README.md to reference new documentation

Ensures future sessions respect capability boundaries and use separate
Claude instances for capability development.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 00:15:57 +01:00
9bbc2832de chore: update testdrive-jsui submodule to refactored version
Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Updated submodule pointer to include all refactored changes:
- Consolidated architecture (js/, static/, src/)
- Plugin self-declaration methods
- Merged with upstream tutorials and LICENSE
- Comprehensive standalone documentation

Note: Submodule changes committed locally, pending push to gitea with credentials.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 00:02:32 +01:00
46a060b695 feat: add testdrive-jsui dependency to markitect
Added testdrive-jsui as a file-based dependency, following the same pattern as other capability submodules.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 23:52:26 +01:00
24959308b2 feat: add testdrive-jsui as git submodule
Set up testdrive-jsui as a git submodule pointing to separate repository.
This enables independent development and versioning of the testdrive-jsui capability.

The submodule will need manual synchronization of recent refactoring changes:
- Consolidated asset structure (js/, static/)
- Plugin self-declaration methods
- Updated README with standalone usage docs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 23:51:49 +01:00
6670e71b81 chore: remove capabilities/testdrive-jsui to prepare for submodule
All files removed from git tracking. Directory will be re-added as a git submodule pointing to the separate testdrive-jsui repository.
2025-12-15 23:49:17 +01:00
ab3f0db86f feat: consolidate testdrive-jsui to capabilities and implement plugin self-declaration
## Major Changes
- Moved all testdrive-jsui assets from root to capabilities/testdrive-jsui/
- Consolidated directory structure: js/, static/css/, static/images/, static/templates/
- Implemented plugin self-declaration (get_plugin_source_dir, get_asset_paths)
- Removed hardcoded plugin discovery from rendering.py
- Updated all asset paths to be relative to capability root

## Architecture Improvements
- Single source of truth for all testdrive-jsui assets
- Plugin declares its own location (no hardcoded paths)
- Generic plugin discovery using hasattr check
- Clean separation: all JS in .js files, no code mixing
- Standalone capability ready for independent use

## Files Changed
- markitect/plugins/testdrive_jsui.py: Added self-declaration methods
- markitect/plugins/rendering.py: Removed hardcoded discovery
- capabilities/testdrive-jsui/README.md: Added standalone usage documentation
- Moved 17 asset files to consolidated structure
- Deleted obsolete /testdrive-jsui/ root directory

## Testing
- All 17 assets verified and working
- Tested via CLI: markitect md-render --engine testdrive-jsui
- Full document rendering successful

Prepares testdrive-jsui to become a git submodule with proper dependency management.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-15 23:42:54 +01:00
d0a1c91b8e feat: fix contents panel scrollbar and consolidate control architecture
Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
## Major Changes
- Fixed contents panel scrollbar behavior to only span content area when reaching max-height
- Eliminated duplicate control files across testdrive-jsui/static/ and markitect/static/
- Consolidated all control files to single source of truth in capabilities/testdrive-jsui/js/controls/
- Refactored contents control to use proper base class architecture

## Technical Details
- Moved overflow-y: auto from control-content-container to control-content-body
- Updated all HTML templates and plugin references to use capabilities/ paths
- Enhanced resize handle positioning (moved from -4px to 1px/2px from right edge)
- Improved CSS flex layout with proper min-height: 0 constraints

## Files Affected
- 10 duplicate control files removed
- 8+ reference files updated with new paths
- CHANGELOG.md updated with all changes

This eliminates confusion about which files to edit and ensures the UI
behaves correctly when panels reach viewport height limits.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 23:55:52 +01:00
3264517c91 refactor: eliminate duplicate control files and consolidate to capabilities/
- Removed duplicate control files from testdrive-jsui/static/js/controls/
- Removed duplicate control files from markitect/static/js/controls/
- Updated all references to point to capabilities/testdrive-jsui/js/controls/
- Fixed relative paths in test files and templates
- Consolidated to single source of truth in capabilities directory
- Updated plugin configuration and documentation references

This eliminates confusion and ensures all systems use the most recent
control implementations from the capabilities directory.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 23:37:17 +01:00
d98c3ae05a fix: refactor contents control architecture and resolve resize handle positioning
- Streamlined ContentsControl to use base class generateContent pattern
- Removed duplicate methods and unified content generation approach
- Added overflow: visible to fix content visibility issues
- Fixed resize handle positioning (moved from -4px to 1px/2px from right edge)
- Improved search functionality to properly rebuild content
- Enhanced refresh button detection to prevent conflicts
- Removed unused getDocumentStats method and duplicate code blocks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 23:31:13 +01:00
4e3f112987 feat: comprehensive control panel UI improvements
- Fix version information display with actual Markitect version
- Implement auto-resize functionality with double-click on resize dot
- Add viewport repositioning to keep panels visible during auto-resize
- Reduce title bar height by 25% for more compact appearance
- Remove duplicate content titles below titlebars across all panels
- Optimize scrollbar positioning to right border with proper spacing
- Reposition resize dot to optimal corner location (bottom: 0px, right: -4px)
- Set default panel height to 1/3 of window height
- Fix Debug panel title formatting consistency
- Remove duplicate initialization warnings
- Clean up panel layout with proper margin management (10px bottom margin)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 22:51:25 +01:00
f788ccdfd3 feat: refactor control panel architecture and fix layout issues
Base class architecture improvements:
- Centralize all panel layout, styling, and behavior in ControlBase
- Implement consistent generateContent() pattern for subclasses
- Add proper flexbox layout with fixed header and scrollable content
- Standardize title styling, positioning, and scroll behavior

Panel layout fixes:
- Fix content positioning to appear inside panels instead of floating above
- Implement proper height management (expands with content up to browser height)
- Add correct scroll boundaries with only content area scrolling
- Position resize handle outside scroll area to avoid scrollbar interference

Visual improvements:
- Fix rounded border appearance with proper overflow handling
- Ensure header respects panel corner radius
- Add proper content margins and padding
- Improve resize handle positioning and visibility

Architecture standardization:
- All panels now follow same base class pattern
- Individual panels only provide configuration and content generation
- Eliminate duplicate styling and layout code across controls
- Consistent behavior across all panel types

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 21:55:06 +01:00
512085d283 feat: enhance control panel UI and resize functionality
Panel UI improvements:
- Replace heading elements (h1-h6) with styled divs to avoid navigation interference
- Change ContentsControl position from northwest to west for better accessibility

Panel collapse/expand enhancements:
- Fix panel dragging to prevent unexpected positioning jumps
- Keep panel width and upper-left position when collapsing to header-only mode
- Complete height reduction when collapsed (no minimal size maintained)
- Toggle resize handle visibility based on panel state

Resize handle improvements:
- Change resize symbol from arrow to clean dot (●) in bottom-right corner
- Remove background circle, show transparent dot only
- Fix resize direction to properly follow mouse movement from bottom-right
- Set dynamic minimum size constraints (header height + padding)
- Allow arbitrary panel sizing with proper bounds checking
- Reset panel size to defaults when closed/collapsed

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 16:40:23 +01:00
95ea13958a feat: remove legacy DocumentControls component
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Remove deprecated DocumentControls from TestDrive JSUI plugin system:
- Remove document-controls.js from plugin asset list
- Remove script reference from HTML template
- Delete legacy document-controls files
- Consolidate all functionality into enhanced control panels

All control panel functionality now provided by enhanced controls:
- ContentsControl (NW): Table of contents and navigation
- StatusControl (E): Document status and metrics
- DebugControl (SE): Debug messages and system info
- EditControl (NE): Editing tools including Reset All button

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 15:46:50 +01:00
ca431ac11f feat: add Reset All button to EditControl panel
Add the missing "Reset All" functionality from Legacy Document Control
to the enhanced EditControl panel for complete feature parity.

## New Functionality
- Added "Reset All" button in Document Actions section
- Comprehensive reset functionality with user confirmation
- Resets font size, editing mode, unsaved changes, highlights
- Integrates with SectionManager, DocumentControls, and DebugControl
- Offers page reload as ultimate fallback for complete reset

## Implementation Details
- Button styled consistently with Legacy Document Control (🔄 Reset All)
- Uses #ffc107 background with #212529 text to match legacy styling
- Comprehensive confirmation dialog explains all actions
- Safe operation wrapper with proper error handling
- Graceful fallbacks when integrated components are unavailable

## Integration
- Deployed to both markitect system and deployment source
- Compatible with existing enhanced ControlBase architecture
- Maintains consistency with other EditControl actions
- Ready for immediate use in production environment

Users now have access to the familiar Reset All functionality
within the modern enhanced control panel system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 15:25:29 +01:00
79c6c9d4e4 feat: complete enhanced ControlBase deployment and verification
Successfully resolve deployment issues and verify enhanced control functionality:

## Deployment Resolution
- Fixed source directory mapping: deployment now uses correct enhanced files
- Cleared deployment cache to ensure fresh asset deployment
- Verified all controls properly inherit from enhanced ControlBase class
- Confirmed 5 advanced behaviors are fully functional in production

## Enhanced Control System Live
- Icon-only collapsed state: Controls start as 40px compass-positioned icons
- Expand/drag functionality: Click to expand, drag headers to reposition
- Bottom-left resize: Resize handle (↙) for dynamic panel sizing
- Collapse with position restoration: Close button (✕) returns to original location
- Header toggle: Click titles to show/hide content areas

## Production Verification
- All controls deployed: ContentsControl, StatusControl, DebugControl, EditControl
- Integration confirmed: md-render --edit now shows enhanced control panels
- User testing validated: Interactive behaviors working as specified
- Documentation complete: Implementation notes and commit history preserved

## Cleanup
- Removed obsolete test files moved to capabilities/testdrive-jsui/tests/
- Updated Makefile for enhanced control testing
- Maintained backward compatibility with legacy systems

The enhanced ControlBase system is now fully operational in MarkiTect's
editing environment, providing users with modern, interactive control panels.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 13:59:54 +01:00
09e7f07c23 fix: update deployment source with enhanced ControlBase files
Copy enhanced ControlBase and control files to deployment source directory.
This resolves the deployment cache issue where md-render --edit was using
old control files instead of the new enhanced ControlBase architecture.

Now all controls properly use the enhanced ControlBase with 5 behaviors:
- Icon-only collapsed state
- Expand/drag functionality
- Bottom-left resize handle
- Collapse button returns to original position
- Header toggle for content visibility

The enhanced control system is now fully deployed and functional.
2025-11-14 13:52:13 +01:00
8d8a4ed0c3 fix: update main-updated.js to use enhanced ControlBase API
Replace old control initialization pattern (.control.config, .createControl())
with new ControlBase class API (.config, .show()) for all control panels.

This enables the 5 enhanced behaviors:
- Icon-only collapsed state
- Expand/drag functionality
- Bottom-left resize
- Collapse with position restoration
- Header toggle content visibility

All control panels now properly initialize with enhanced ControlBase.
2025-11-14 12:05:17 +01:00
5b13c00d3e feat: deploy enhanced ControlBase to MarkiTect md-render --edit
Successfully integrate improved TestDrive-JSUI controls with main MarkiTect system:

## Enhanced Control System
- Updated ControlBase with 5 advanced behaviors from reference implementation
- All controls now support icon-only collapsed state, drag/resize, position restoration
- Seamless integration with md-render --edit command

## Updated Components
- DebugControl: Enhanced with new ControlBase inheritance
- EditControl: Full document editing tools with export/formatting
- StatusControl: Real-time document statistics and metrics
- ContentsControl: Interactive table of contents navigation

## Deployment Integration
- All enhanced controls deployed via asset system
- Compatible with existing edit mode functionality
- Maintains backward compatibility with legacy systems

## Verification
- Successfully renders interactive HTML with md-render --edit
- All control behaviors working in production environment
- Asset deployment system properly handles enhanced controls

The enhanced control system is now live and functional in MarkiTect's editing environment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 11:35:47 +01:00
4262310302 feat: enhance ControlBase with advanced panel behavior patterns
Implement comprehensive control panel functionality based on reference patterns:

## New Features
- Icon-only collapsed state with compass positioning
- Expand/drag functionality for repositioning panels
- Bottom-left corner resize with minimum size constraints
- Collapse button returns to original position
- Header toggle for content visibility control

## Technical Improvements
- Enhanced DOM structure with expanded/collapsed states
- Robust event handling with automatic cleanup
- State management for drag, resize, expand operations
- Position restoration system for collapse behavior
- Comprehensive styling system with backdrop effects

## Components Added
- Enhanced ControlBase class with 5 core behaviors
- ContentsControl, StatusControl, EditControl, DebugControl panels
- Component discovery system with TDD implementation
- Legacy DocumentControlsLegacy for backward compatibility

## Testing & Documentation
- Interactive test page for behavior validation
- Comprehensive implementation notes
- TDD test suite with 84 passing tests
- Component listing automation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 11:33:49 +01:00
6ef2641bff docs: archive integration completion summary
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Moved INTEGRATION_COMPLETE.md to history/ to document the successful
completion of the plugin infrastructure implementation and integration.
2025-11-14 09:36:16 +01:00
9446 changed files with 23381 additions and 703905 deletions

View 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

View 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!**

View 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
View File

@@ -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

View File

@@ -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
View File

@@ -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.*

View File

@@ -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.

View 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

View File

@@ -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

View File

@@ -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*

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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');
});
});
});

View File

@@ -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.
![Test Image](test.jpg)`;
// 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();
});
});

View File

@@ -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);
});
});
});

View File

@@ -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>';
});

View File

@@ -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');
});
});
});

View File

@@ -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;

View File

@@ -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');
});
});
});
});

View File

@@ -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(),
};

View File

@@ -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.
![Test Image](https://example.com/image.jpg)
## 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.
![Image Section](https://example.com/test.jpg)
\`\`\`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.
![Test Image](https://example.com/test.jpg)
### 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, '![Updated Image](https://example.com/new.jpg)');
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');
});
}

View File

@@ -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');
});
}

View File

@@ -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');
});
}

View File

@@ -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');
});
}

View File

@@ -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');
});
}

View File

@@ -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');
});
});

View File

@@ -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');
});
}

View File

@@ -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('![Image](url)')).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');
});
}

View File

@@ -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.
![Test Image](https://example.com/test.jpg)
### 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');
});
}

View File

@@ -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');
});
}

View File

@@ -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');
});
}

View File

@@ -1 +0,0 @@
../acorn/bin/acorn

View File

@@ -1 +0,0 @@
../baseline-browser-mapping/dist/cli.js

View File

@@ -1 +0,0 @@
../browserslist/cli.js

View File

@@ -1 +0,0 @@
../create-jest/bin/create-jest.js

View File

@@ -1 +0,0 @@
../escodegen/bin/escodegen.js

View File

@@ -1 +0,0 @@
../escodegen/bin/esgenerate.js

View File

@@ -1 +0,0 @@
../eslint/bin/eslint.js

View File

@@ -1 +0,0 @@
../esprima/bin/esparse.js

View File

@@ -1 +0,0 @@
../esprima/bin/esvalidate.js

View File

@@ -1 +0,0 @@
../import-local/fixtures/cli.js

View File

@@ -1 +0,0 @@
../jest/bin/jest.js

View File

@@ -1 +0,0 @@
../js-yaml/bin/js-yaml.js

View File

@@ -1 +0,0 @@
../jsesc/bin/jsesc

View File

@@ -1 +0,0 @@
../json5/lib/cli.js

View File

@@ -1 +0,0 @@
../which/bin/node-which

View File

@@ -1 +0,0 @@
../@babel/parser/bin/babel-parser.js

View File

@@ -1 +0,0 @@
../regjsparser/bin/parser

View File

@@ -1 +0,0 @@
../resolve/bin/resolve

View File

@@ -1 +0,0 @@
../rimraf/bin.js

View File

@@ -1 +0,0 @@
../semver/bin/semver.js

View File

@@ -1 +0,0 @@
../typescript/bin/tsc

View File

@@ -1 +0,0 @@
../typescript/bin/tsserver

View File

@@ -1 +0,0 @@
../update-browserslist-db/cli.js

File diff suppressed because it is too large Load Diff

View File

@@ -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.

View File

@@ -1,316 +0,0 @@
# CSS color
[![build](https://github.com/asamuzaK/cssColor/actions/workflows/node.js.yml/badge.svg)](https://github.com/asamuzaK/cssColor/actions/workflows/node.js.yml)
[![CodeQL](https://github.com/asamuzaK/cssColor/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/asamuzaK/cssColor/actions/workflows/github-code-scanning/codeql)
[![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/css-color)](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

View File

@@ -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.

View File

@@ -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).

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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;

View File

@@ -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;
};

File diff suppressed because it is too large Load Diff

View File

@@ -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';

View File

@@ -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';

View File

@@ -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
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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 '';
};

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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
];

View File

@@ -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];
};

View File

@@ -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.

View File

@@ -1,200 +0,0 @@
# DOM Selector
[![build](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
[![CodeQL](https://github.com/asamuzaK/domSelector/actions/workflows/codeql.yml/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/codeql.yml)
[![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/dom-selector)](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]&lt;([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"&nbsp;i\]|✓| |
|E\[foo="bar"&nbsp;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&#8209;link|✓| |
|E:link|✓| |
|E:visited|✓|Returns `false` or `null` to prevent fingerprinting.|
|E:local&#8209;link|✓| |
|E:target|✓| |
|E:target&#8209;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&#8209;within|✓| |
|E:focus&#8209;visible|Unsupported| |
|E:enabled<br>E:disabled|✓| |
|E:read&#8209;write<br>E:read&#8209;only|✓| |
|E:placeholder&#8209;shown|✓| |
|E:default|✓| |
|E:checked|✓| |
|E:indeterminate|✓| |
|E:valid<br>E:invalid|✓| |
|E:required<br>E:optional|✓| |
|E:blank|Unsupported| |
|E:user&#8209;invalid|Unsupported| |
|E:root|✓| |
|E:empty|✓| |
|E:nth&#8209;child(n&nbsp;[of&nbsp;S]?)|✓| |
|E:nth&#8209;last&#8209;child(n&nbsp;[of&nbsp;S]?)|✓| |
|E:first&#8209;child|✓| |
|E:last&#8209;child|✓| |
|E:only&#8209;child|✓| |
|E:nth&#8209;of&#8209;type(n)|✓| |
|E:nth&#8209;last&#8209;of&#8209;type(n)|✓| |
|E:first&#8209;of&#8209;type|✓| |
|E:last&#8209;of&#8209;type|✓| |
|E:only&#8209;of&#8209;type|✓| |
|E&nbsp;F|✓| |
|E > F|✓| |
|E + F|✓| |
|E ~ F|✓| |
|F \|\| E|Unsupported| |
|E:nth&#8209;col(n)|Unsupported| |
|E:nth&#8209;last&#8209;col(n)|Unsupported| |
|:host|✓| |
|:host(s)|✓| |
|:host&#8209;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

View File

@@ -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"
}

View File

@@ -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();

View File

@@ -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$/;

View File

@@ -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
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -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';

View File

@@ -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>;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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"
}

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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");

View File

@@ -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