mirror of
https://github.com/peritus/obsidian-interactive-ratings.git
synced 2026-07-22 12:20:31 +00:00
Compare commits
60 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bf43556e9 | ||
|
|
3816304fb8 | ||
|
|
6e28f2a726 | ||
|
|
3c5ef7595b | ||
|
|
6d3323601c | ||
|
|
fb5c9496f1 | ||
|
|
c8b6605e7a | ||
|
|
ae6d438929 | ||
|
|
0dd7b2ec75 | ||
|
|
04c19b818a | ||
|
|
5cd8cd32b9 | ||
|
|
ebcb212610 | ||
|
|
9b21e5e6bd | ||
|
|
eadace6854 | ||
|
|
5fe87e5c50 | ||
|
|
3e32fc1d97 | ||
|
|
97263123b3 | ||
|
|
b48938ae01 | ||
|
|
f5f2ab6537 | ||
|
|
aa45cf217f | ||
|
|
607913f700 | ||
|
|
1349c7ac41 | ||
|
|
cd9806b428 | ||
|
|
fea9a30577 | ||
|
|
6b4275da08 | ||
|
|
785efc88ed | ||
|
|
7c9ac9d5cd | ||
|
|
d42786db60 | ||
|
|
c73557fee9 | ||
|
|
b0590c959a | ||
|
|
c43886f062 | ||
|
|
784e2f905e | ||
|
|
2e53c1e164 | ||
|
|
ca5cd0fd46 | ||
|
|
0a7997187a | ||
|
|
cc7ea8ead9 | ||
|
|
f163bf7e60 | ||
|
|
f0b00e11e3 | ||
|
|
b60874efdc | ||
|
|
40b05f606a | ||
|
|
c17ec8be7a | ||
|
|
6ad882397a | ||
|
|
2ea7af7bbd | ||
|
|
3c0cf4029a | ||
|
|
1218efd3aa | ||
|
|
014446d672 | ||
|
|
a792713e3c | ||
|
|
22b4fbd9d4 | ||
|
|
b86ba20f2d | ||
|
|
c3abe0f9ee | ||
|
|
4b4cef482c | ||
|
|
0a65dbba48 | ||
|
|
1c929aff48 | ||
|
|
4a5e7d65f8 | ||
|
|
f7aa86a5bb | ||
|
|
a8b30bea88 | ||
|
|
cedf7c01da | ||
|
|
68ed60976c | ||
|
|
45883a0d13 | ||
|
|
bc8ea924b8 |
51 changed files with 2134 additions and 1200 deletions
44
.bumpversion-preview.toml
Normal file
44
.bumpversion-preview.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
[tool.bumpversion]
|
||||
current_version = "1.0.8"
|
||||
|
||||
# Parse pattern that understands both regular and preview versions
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(?:-(?P<release>preview)\\.(?P<preview>\\d+))?"
|
||||
|
||||
# Preview serialization - always includes preview suffix
|
||||
serialize = [
|
||||
"{major}.{minor}.{patch}-{release}.{preview}" # Preview versions: 0.9.1-preview.1
|
||||
]
|
||||
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = "{new_version}"
|
||||
tag_message = "Preview {new_version}"
|
||||
message = "Bump version: {current_version} → {new_version}"
|
||||
regex = true
|
||||
|
||||
# PREVIEW CONFIG: Update package.json AND sync release config
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "package.json"
|
||||
search = '"version": "{current_version}"'
|
||||
replace = '"version": "{new_version}"'
|
||||
|
||||
# Keep release config in sync
|
||||
[[tool.bumpversion.files]]
|
||||
filename = ".bumpversion.toml"
|
||||
search = 'current_version = "{current_version}"'
|
||||
replace = 'current_version = "{new_version}"'
|
||||
|
||||
# Keep preview config in sync with itself
|
||||
[[tool.bumpversion.files]]
|
||||
filename = ".bumpversion-preview.toml"
|
||||
search = 'current_version = "{current_version}"'
|
||||
replace = 'current_version = "{new_version}"'
|
||||
|
||||
# Release part: always "preview" for this config
|
||||
[tool.bumpversion.parts.release]
|
||||
values = ["preview"]
|
||||
|
||||
# Preview part: for incrementing preview numbers
|
||||
[tool.bumpversion.parts.preview]
|
||||
first_value = "1"
|
||||
|
|
@ -1,19 +1,51 @@
|
|||
[tool.bumpversion]
|
||||
current_version = "1.0.2"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
current_version = "1.0.8"
|
||||
|
||||
# Parse pattern that understands both regular and preview versions
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(?:-(?P<release>preview)\\.(?P<preview>\\d+))?"
|
||||
|
||||
# Release serialization - always removes preview suffix
|
||||
serialize = [
|
||||
"{major}.{minor}.{patch}" # Final releases: 0.9.1
|
||||
]
|
||||
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = "{new_version}"
|
||||
tag_message = "Version {new_version}"
|
||||
tag_message = "Release {new_version}"
|
||||
message = "Bump version: {current_version} → {new_version}"
|
||||
regex = true
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "manifest.json"
|
||||
search = '"version": "{current_version}"'
|
||||
replace = '"version": "{new_version}"'
|
||||
# RELEASE CONFIG: Update package.json, manifest.json, AND sync preview config
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "package.json"
|
||||
search = '"version": "{current_version}"'
|
||||
replace = '"version": "{new_version}"'
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "manifest.json"
|
||||
search = '"version": "[^"]*"'
|
||||
replace = '"version": "{new_version}"'
|
||||
regex = true
|
||||
|
||||
# Keep release config in sync
|
||||
[[tool.bumpversion.files]]
|
||||
filename = ".bumpversion.toml"
|
||||
search = 'current_version = "{current_version}"'
|
||||
replace = 'current_version = "{new_version}"'
|
||||
|
||||
# Keep preview config in sync with itself
|
||||
[[tool.bumpversion.files]]
|
||||
filename = ".bumpversion-preview.toml"
|
||||
search = 'current_version = "{current_version}"'
|
||||
replace = 'current_version = "{new_version}"'
|
||||
|
||||
# Release part: preview -> stable (stable is hidden in serialization)
|
||||
[tool.bumpversion.parts.release]
|
||||
values = ["preview", "stable"]
|
||||
optional_value = "stable"
|
||||
|
||||
# Preview part: for incrementing preview numbers
|
||||
[tool.bumpversion.parts.preview]
|
||||
first_value = "1"
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,2 @@
|
|||
*.js
|
||||
main.js
|
||||
node_modules/
|
||||
203
CSS-CUSTOMIZATION.md
Normal file
203
CSS-CUSTOMIZATION.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# CSS Customization Guide
|
||||
|
||||
This guide explains how to customize the appearance of interactive rating symbols using CSS snippets in Obsidian.
|
||||
|
||||
## ⚠️ Important: Minimal Default Styling
|
||||
|
||||
**The Interactive Ratings plugin applies minimal default styling to maintain functionality:**
|
||||
|
||||
- **Regular rating systems** (★☆): No default styling - symbols are visually distinct by character
|
||||
- **Full-only rating systems** (★★★★★): Essential default styling preserved - without it, ratings would be unreadable
|
||||
|
||||
**To get started with regular rating systems, try the "Basic Styling" example below** to add visual distinction between filled, half, and empty symbols.
|
||||
|
||||
## New Feature: Distinct CSS Classes for Selected/Unselected Symbols
|
||||
|
||||
The Interactive Ratings plugin provides distinct CSS classes for selected (filled), half-filled, and unselected (empty) symbols, making it easy to customize their appearance.
|
||||
|
||||
### Available CSS Classes
|
||||
|
||||
#### For Regular Rating Systems (with filled/half/empty symbols):
|
||||
- `.interactive-rating-symbol--rated` - Applied to selected/filled symbols (★)
|
||||
- `.interactive-rating-symbol--half` - Applied to half-filled symbols (☆★)
|
||||
- `.interactive-rating-symbol--empty` - Applied to unselected/empty symbols (☆)
|
||||
- `.interactive-rating-symbol--normal` - Legacy class (no longer used for filled/empty distinction)
|
||||
|
||||
#### For Full-Only Rating Systems (symbols that don't have empty variants):
|
||||
- `.interactive-rating-symbol--rated` - Applied to symbols within the rating (has default styling)
|
||||
- `.interactive-rating-symbol--unrated` - Applied to symbols beyond the rating (has default styling)
|
||||
|
||||
**Important:** Full-only systems have essential default styling preserved because without it, all symbols would look identical and ratings would be unreadable. You can still override these defaults with your own CSS.
|
||||
|
||||
### Example CSS Snippets
|
||||
|
||||
Here are some examples of how you can customize the appearance of rating symbols:
|
||||
|
||||
#### 🚀 Quick Start: Basic Visual Distinction
|
||||
```css
|
||||
/* Add this snippet to get started with basic visual distinction for regular rating systems */
|
||||
.interactive-rating-symbol--rated {
|
||||
opacity: 1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--half {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--empty {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Note: .interactive-rating-symbol--unrated (full-only systems) already has default styling */
|
||||
```
|
||||
|
||||
#### 2. Make Empty Symbols More Subtle
|
||||
```css
|
||||
.interactive-rating-symbol--empty {
|
||||
opacity: 0.3;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Add Color to Filled, Half, and Empty Symbols
|
||||
```css
|
||||
.interactive-rating-symbol--rated {
|
||||
color: #FFD700; /* Gold color for filled stars */
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--half {
|
||||
color: #FFA500; /* Orange color for half stars */
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--empty {
|
||||
color: #CCCCCC; /* Light gray for empty stars */
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Create Gradient Effect for Half Symbols
|
||||
```css
|
||||
.interactive-rating-symbol--rated {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--half {
|
||||
background: linear-gradient(90deg, #FFD700 50%, #CCCCCC 50%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--empty {
|
||||
color: #CCCCCC;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Add Hover Effects
|
||||
```css
|
||||
.interactive-rating-symbol--empty:hover {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--half:hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--rated:hover {
|
||||
transform: scale(1.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. Different Styling for Different Symbol Types
|
||||
```css
|
||||
/* For star ratings */
|
||||
.interactive-rating-symbol--rated:contains("★") {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--half:contains("☆★") {
|
||||
color: #FFA500;
|
||||
}
|
||||
|
||||
/* For heart ratings */
|
||||
.interactive-rating-symbol--rated:contains("♥") {
|
||||
color: #FF6B9D;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--half:contains("♡") {
|
||||
color: #FFB3D1;
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. Add Drop Shadow to Filled and Half Symbols
|
||||
```css
|
||||
.interactive-rating-symbol--rated {
|
||||
text-shadow: 0 0 3px rgba(255, 215, 0, 0.6);
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--half {
|
||||
text-shadow: 0 0 2px rgba(255, 165, 0, 0.5);
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--empty {
|
||||
opacity: 0.4;
|
||||
}
|
||||
```
|
||||
|
||||
### How to Apply CSS Snippets in Obsidian
|
||||
|
||||
1. Open Obsidian Settings
|
||||
2. Go to "Appearance" → "CSS snippets"
|
||||
3. Click the folder icon to open the snippets folder
|
||||
4. Create a new `.css` file (e.g., `rating-customization.css`)
|
||||
5. Add your desired CSS rules
|
||||
6. Enable the snippet in Obsidian's CSS snippets section
|
||||
|
||||
### Default Styling
|
||||
|
||||
**The plugin applies minimal default styling to maintain functionality:**
|
||||
|
||||
**Regular Rating Systems** (★☆ - with distinct filled/empty symbols):
|
||||
- **Filled symbols** (`.interactive-rating-symbol--rated`): No default styling - add your own
|
||||
- **Half symbols** (`.interactive-rating-symbol--half`): No default styling - add your own
|
||||
- **Empty symbols** (`.interactive-rating-symbol--empty`): No default styling - add your own
|
||||
|
||||
**Full-Only Rating Systems** (★★★★★ - single symbol type):
|
||||
- **Rated symbols** (`.interactive-rating-symbol--rated`): Default styling preserved (opacity: 1, no filter)
|
||||
- **Unrated symbols** (`.interactive-rating-symbol--unrated`): Default styling preserved (opacity: 0.5, grayscale filter)
|
||||
- **Reason**: Without default styling, full-only systems would be unreadable (all symbols would look identical)
|
||||
|
||||
**Accessibility styling is preserved:** High contrast mode will automatically apply enhanced contrast styling only when users have high contrast preferences enabled.
|
||||
|
||||
### Accessibility Considerations
|
||||
|
||||
**The plugin includes automatic high contrast support as the only built-in styling.** When users have high contrast mode enabled, empty and half symbols will automatically receive enhanced contrast styling for accessibility compliance. This accessibility styling is preserved even though no default visual styling is applied in normal viewing modes.
|
||||
|
||||
You can override the accessibility styling by providing your own high contrast styles:
|
||||
|
||||
```css
|
||||
@media (prefers-contrast: high) {
|
||||
.interactive-rating-symbol--empty {
|
||||
opacity: 0.2 !important;
|
||||
filter: grayscale(100%) contrast(1.2) !important;
|
||||
}
|
||||
|
||||
.interactive-rating-symbol--half {
|
||||
opacity: 0.5 !important;
|
||||
filter: contrast(1.3) !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Migration from Previous Versions
|
||||
|
||||
If you were previously using CSS that targeted `.interactive-rating-symbol` without state classes, your existing styles should still work. However, for more precise control, consider updating to use the new state-specific classes:
|
||||
|
||||
- Replace `.interactive-rating-symbol` with `.interactive-rating-symbol--rated` for filled symbols
|
||||
- Add styles for `.interactive-rating-symbol--half` for half symbols
|
||||
- Add styles for `.interactive-rating-symbol--empty` for empty symbols
|
||||
|
||||
This enhancement provides much more flexibility for customizing the visual appearance of your rating systems, with complete control over filled, half, and empty symbol states!
|
||||
84
README.md
84
README.md
|
|
@ -2,19 +2,42 @@
|
|||
|
||||
Add interactive rating symbols to your notes that update with a click.
|
||||
|
||||
## Usage
|
||||
|
||||
<img alt="demo of the Interactive Ratings Plugin for Obsidian" src="screencast.gif" width="400" />
|
||||
|
||||
|
||||
## Supported Symbol Sets
|
||||
|
||||
| Symbol Type | Full | Empty | Half | Examples |
|
||||
|-------------|------|-------|------|----------|
|
||||
| Stars | ★ | ☆ | | `★★★☆☆ (3/5)` Book rating |
|
||||
| Star Symbols | ✦ | ✧ | | `✦✦✦✧✧ (3/5)` Stargazing |
|
||||
| Moon Phases | 🌕 | 🌑 | 🌗 | `🌕🌕🌗🌑🌑 (2.5/5)` Lunar observation |
|
||||
| Circles | ● | ○ | ◐ | `●●●○○○○○○○ 3/10` Movie review scale |
|
||||
| Squares | ■ | □ | ◧ | `■■■□ (3/4)` Recipe difficulty |
|
||||
| Triangles | ▲ | △ | | `▲▲▲▲▲△△△△△△△ 5/12` Hiking difficulty |
|
||||
| Red Hearts | ❤️ | 🤍 | | `❤️❤️❤️🤍🤍 (3/5)` Celebrity crush |
|
||||
| Orange Hearts | 🧡 | 🤍 | | `🧡🧡🧡🧡🤍 80%` Sunset addiction |
|
||||
| Yellow Hearts | 💛 | 🤍 | | `💛💛🤍🤍 2/4` Wizard vibes |
|
||||
| Green Hearts | 💚 | 🤍 | | `💚💚💚🤍🤍🤍 50%` Plant guilt |
|
||||
| Blue Hearts | 💙 | 🤍 | | `💙💙🤍🤍🤍 (2/5)` Ocean yearning |
|
||||
| Purple Hearts | 💜 | 🤍 | | `💜💜💜💜🤍 4/5` Unicorn belief |
|
||||
| Black Hearts | 🖤 | 🤍 | | `🖤🖤🖤🤍 75%` Villain sympathy |
|
||||
| Brown Hearts | 🤎 | 🤍 | | `🤎🤎🤍🤍🤍 (2/5)` Chocolate dependency |
|
||||
| Block Progress | █ | ▁ | | `███▁▁▁ (3/6)` Project completion |
|
||||
| Braille Dots | ⣿ | ⣀ | ⡇ | `⣿⣿⡇⣀⣀⣀⣀⣀⣀⣀ 2.5/10` |
|
||||
| Solid/Empty Circles | ⬤ | ○ | | `⬤⬤⬤○○○○○○○○○ 25%` Budget spending |
|
||||
| Solid/Empty Squares | ■ | □ | | `■■■□□□□ 3/7` Weekly progress |
|
||||
| Dotted Squares | ▰ | ▱ | | `▰▰▰▱▱▱▱▱▱▱ (3/10)` Task complexity |
|
||||
| Filled/Empty Rectangles | ◼ | ▭ | | `◼◼◼▭ 75%` Reading progress |
|
||||
| Vertical Bars | ▮ | ▯ | | `▮▮▮▮▮▮▯▯▯▯▯▯ 50%` Battery level |
|
||||
| Bold Circles | ⬤ | ◯ | | `⬤◯◯ 1/3` Quick product review |
|
||||
| Black/White Circles | ⚫ | ⚪ | | `⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪ (5/10)` Coffee strength |
|
||||
| Block/Light Shade | █ | ░ | | `███░░░░░░░ 30%` Download progress |
|
||||
|
||||
## Usage
|
||||
|
||||
Type any supported symbol sequence (minimum 3 identical symbols) in your note. When you hover over these symbols in edit mode, clicking on one of the symbols allowing you to update the rating.
|
||||
|
||||
## Installation
|
||||
|
||||
1. In Obsidian settings, go to Community Plugins
|
||||
2. Disable Safe Mode
|
||||
3. Search for "Interactive Ratings"
|
||||
4. Install and enable the plugin
|
||||
|
||||
### Rating Text (optional)
|
||||
|
||||
Add numerical ratings with these formats:
|
||||
|
|
@ -27,24 +50,33 @@ Add numerical ratings with these formats:
|
|||
|
||||
The numerical rating updates automatically when you change the symbols.
|
||||
|
||||
## Supported Symbol Sets
|
||||
### Custom Emojis
|
||||
|
||||
You can configure which emojis are supported for rating interactions by going to Settings → Community Plugins → Interactive Ratings. In the "Emojis to support in ratings" setting, enter any emojis you want to use for ratings.
|
||||
|
||||
**Default emojis**: `🎥🏆⭐💎🔥⚡🎯🚀💰🎖️`
|
||||
|
||||
**ZWJ Sequence Support**: The plugin fully supports complex emojis including ZWJ (Zero Width Joiner) sequences such as:
|
||||
- 🏴☠️ (Pirate Flag)
|
||||
- 👨🚒 (Man Firefighter)
|
||||
- 🏳️🌈 (Rainbow Flag)
|
||||
- And other compound emojis
|
||||
|
||||
**Example usage**:
|
||||
- `🎥🎥🎥🎥🎥 (4/5)` Movie rating
|
||||
- `🏆🏆🏆 3/5` Achievement level
|
||||
- `🔥🔥🔥🔥 80%` Spice level
|
||||
- `🎯🎯🎯🎯🎯🎯 (5/6)` Accuracy rating
|
||||
- `🏴☠️🏴☠️🏴☠️ (3/5)` Pirate movie rating
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
1. In Obsidian settings, go to Community Plugins
|
||||
2. Disable Safe Mode
|
||||
3. Search for "Interactive Ratings"
|
||||
4. Install and enable the plugin
|
||||
|
||||
| Symbol Type | Full | Empty | Half | Examples |
|
||||
|-------------|------|-------|------|----------|
|
||||
| Stars | ★ | ☆ | | `★★★☆☆ (3/5)` Book rating |
|
||||
| Circles | ● | ○ | ◐ | `●●●○○○○○○○ 3/10` Movie review scale |
|
||||
| Squares | ■ | □ | ◧ | `■■■□ (3/4)` Recipe difficulty |
|
||||
| Triangles | ▲ | △ | | `▲▲▲▲▲△△△△△△△ 5/12` Hiking difficulty |
|
||||
| Block Progress | █ | ▁ | | `███▁▁▁ (3/6)` Project completion |
|
||||
| Braille Dots | ⣿ | ⣀ | ⡇ | `⣿⣿⡇⣀⣀⣀⣀⣀⣀⣀ 2.5/10` |
|
||||
| Solid/Empty Circles | ⬤ | ○ | | `⬤⬤⬤○○○○○○○○○ 25%` Budget spending |
|
||||
| Solid/Empty Squares | ■ | □ | | `■■■□□□□ 3/7` Weekly progress |
|
||||
| Dotted Squares | ▰ | ▱ | | `▰▰▰▱▱▱▱▱▱▱ (3/10)` Task complexity |
|
||||
| Filled/Empty Rectangles | ◼ | ▭ | | `◼◼◼▭ 75%` Reading progress |
|
||||
| Vertical Bars | ▮ | ▯ | | `▮▮▮▮▮▮▯▯▯▯▯▯ 50%` Battery level |
|
||||
| Bold Circles | ⬤ | ◯ | | `⬤◯◯ 1/3` Quick product review |
|
||||
| Black/White Circles | ⚫ | ⚪ | | `⚫⚫⚫⚫⚫⚪⚪⚪⚪⚪ (5/10)` Coffee strength |
|
||||
| Block/Light Shade | █ | ░ | | `███░░░░░░░ 30%` Download progress |
|
||||
|
||||
## Use Cases
|
||||
|
||||
|
|
|
|||
36
codemcp.toml
Normal file
36
codemcp.toml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
prompt = """
|
||||
You are an expert software developer.
|
||||
|
||||
Development instructions:
|
||||
- ...
|
||||
|
||||
Commit messages:
|
||||
- Write a commit message in imperative mood (e.g., "Refactor X", not "Refactored X").
|
||||
- Do not describe success or effort (e.g., avoid "Successfully", "Cleaned up", etc.).
|
||||
- Just state what the commit does, not how well or completely it does it.
|
||||
"""
|
||||
|
||||
paths_blocked = [
|
||||
# Version control internals (major space wasters)
|
||||
".jj/**",
|
||||
".git/**",
|
||||
|
||||
# Node modules
|
||||
"node_modules/**",
|
||||
]
|
||||
|
||||
[commands]
|
||||
# Standard build
|
||||
build = ["npm", "run", "build"]
|
||||
|
||||
# Debug build
|
||||
build_debug = ["npm", "run", "build:debug"]
|
||||
|
||||
# Type checking
|
||||
type_check = ["npm", "run", "type-check"]
|
||||
|
||||
# Install packages
|
||||
npm_install = ["npm", "install"]
|
||||
|
||||
# bump version
|
||||
bump_version_patch = ["bump-my-version", "bump", "patch", "-vv"]
|
||||
|
|
@ -11,6 +11,10 @@ Interactive Ratings Plugin for Obsidian (https://obsidian.md)
|
|||
// Check if watch flag is provided
|
||||
const isWatch = process.argv.includes('--watch');
|
||||
|
||||
// Get logging configuration from environment variable
|
||||
const loggingEnabled = process.env.LOGGING_ENABLED === 'true';
|
||||
console.log(`Building with logging ${loggingEnabled ? 'ENABLED' : 'DISABLED'}`);
|
||||
|
||||
// Build configuration
|
||||
const buildOptions = {
|
||||
entryPoints: ['src/main.ts'],
|
||||
|
|
@ -20,7 +24,37 @@ const buildOptions = {
|
|||
target: 'es2016',
|
||||
format: 'cjs',
|
||||
banner: { js: banner },
|
||||
external: ['obsidian'],
|
||||
define: {
|
||||
'process.env.LOGGING_ENABLED': loggingEnabled ? '"true"' : '"false"'
|
||||
},
|
||||
external: [
|
||||
'obsidian',
|
||||
'electron',
|
||||
'codemirror',
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/closebrackets',
|
||||
'@codemirror/collab',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/comment',
|
||||
'@codemirror/fold',
|
||||
'@codemirror/gutter',
|
||||
'@codemirror/highlight',
|
||||
'@codemirror/history',
|
||||
'@codemirror/language',
|
||||
'@codemirror/lint',
|
||||
'@codemirror/matchbrackets',
|
||||
'@codemirror/panel',
|
||||
'@codemirror/rangeset',
|
||||
'@codemirror/rectangular-selection',
|
||||
'@codemirror/search',
|
||||
'@codemirror/state',
|
||||
'@codemirror/stream-parser',
|
||||
'@codemirror/text',
|
||||
'@codemirror/tooltip',
|
||||
'@codemirror/view',
|
||||
'@lezer/common',
|
||||
'@lezer/lr'
|
||||
],
|
||||
logLevel: 'info',
|
||||
minify: false,
|
||||
treeShaking: true,
|
||||
|
|
@ -55,4 +89,4 @@ async function build() {
|
|||
}
|
||||
|
||||
// Run the build
|
||||
build();
|
||||
build();
|
||||
146
install-dev.js
Normal file
146
install-dev.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function getVCSChangeId() {
|
||||
try {
|
||||
// Try Jujutsu first
|
||||
try {
|
||||
// Try current working copy @
|
||||
let changeId = execSync('jj log -r @ --no-graph -T change_id', { encoding: 'utf8' }).trim();
|
||||
|
||||
// If working copy is empty, try parent revision @-
|
||||
if (!changeId || changeId === '' || changeId.includes('(empty)')) {
|
||||
changeId = execSync('jj log -r @- --no-graph -T change_id', { encoding: 'utf8' }).trim();
|
||||
}
|
||||
|
||||
return changeId;
|
||||
} catch (jjError) {
|
||||
// Fall back to Git
|
||||
const gitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
||||
return gitHash;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Warning: Could not get VCS change ID:', error.message);
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function getLatestCommitMessage() {
|
||||
try {
|
||||
// Try Jujutsu first
|
||||
try {
|
||||
// Try current working copy @
|
||||
let message = execSync('jj log -r @ --no-graph -T description', { encoding: 'utf8' }).trim();
|
||||
|
||||
// If working copy is empty, try parent revision @-
|
||||
if (!message || message === '' || message.includes('(empty)')) {
|
||||
message = execSync('jj log -r @- --no-graph -T description', { encoding: 'utf8' }).trim();
|
||||
}
|
||||
|
||||
const firstLine = message.split('\n')[0];
|
||||
return firstLine;
|
||||
} catch (jjError) {
|
||||
// Fall back to Git
|
||||
const message = execSync('git log -1 --pretty=%B', { encoding: 'utf8' }).trim();
|
||||
const firstLine = message.split('\n')[0];
|
||||
return firstLine;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Warning: Could not get commit message:', error.message);
|
||||
return 'Development build';
|
||||
}
|
||||
}
|
||||
|
||||
function generateDevManifest() {
|
||||
try {
|
||||
// Read the original manifest
|
||||
const manifestPath = path.join(__dirname, 'manifest.json');
|
||||
const originalManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
|
||||
// Get VCS info
|
||||
const changeId = getVCSChangeId();
|
||||
const commitMessage = getLatestCommitMessage();
|
||||
|
||||
// Create dev version of manifest
|
||||
const devManifest = {
|
||||
...originalManifest,
|
||||
id: `${originalManifest.id}-dev`,
|
||||
name: `${originalManifest.name} (Dev)`,
|
||||
version: `${originalManifest.version}-dev-${changeId.substring(0, 8)}`,
|
||||
description: `${commitMessage} [dev-${changeId.substring(0, 8)}]`
|
||||
};
|
||||
|
||||
// Return JSON string when used as module, output to stdout when run directly
|
||||
const jsonString = JSON.stringify(devManifest, null, 2);
|
||||
|
||||
if (require.main === module && process.argv.includes('--preview')) {
|
||||
console.log(jsonString);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return jsonString;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating dev manifest:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function buildAndInstall() {
|
||||
try {
|
||||
// Check if VAULT_PATH is provided
|
||||
const vaultPath = process.env.VAULT_PATH;
|
||||
if (!vaultPath) {
|
||||
console.error('Usage: VAULT_PATH="/path/to/vault" npm run build-and-install');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('📄 Generating dev manifest...');
|
||||
const devManifestContent = generateDevManifest();
|
||||
|
||||
// Read original manifest to get plugin ID
|
||||
const originalManifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
|
||||
const pluginId = originalManifest.id;
|
||||
|
||||
// Create plugin directory path
|
||||
const pluginDir = path.join(vaultPath, '.obsidian', 'plugins', pluginId);
|
||||
|
||||
console.log('📁 Creating plugin directory...');
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
|
||||
console.log('📋 Copying plugin files...');
|
||||
// Copy main files
|
||||
const filesToCopy = ['main.js', 'styles.css'];
|
||||
for (const file of filesToCopy) {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.copyFileSync(file, path.join(pluginDir, file));
|
||||
console.log(` ✓ Copied ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Write dev manifest
|
||||
fs.writeFileSync(path.join(pluginDir, 'manifest.json'), devManifestContent);
|
||||
console.log(' ✓ Copied dev manifest.json');
|
||||
|
||||
console.log('✅ Installed dev build successfully!');
|
||||
console.log(` Plugin directory: ${pluginDir}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Build and install failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle command line arguments
|
||||
if (require.main === module) {
|
||||
if (process.argv.includes('--preview')) {
|
||||
generateDevManifest();
|
||||
} else {
|
||||
buildAndInstall();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { buildAndInstall, generateDevManifest, getVCSChangeId, getLatestCommitMessage };
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "interactive-ratings",
|
||||
"name": "Interactive Ratings",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"minAppVersion": "0.15.0",
|
||||
"author": "peritus",
|
||||
"authorUrl": "https://github.com/peritus",
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-interactive-ratings",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-interactive-ratings",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.7",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.1",
|
||||
|
|
|
|||
18
package.json
18
package.json
|
|
@ -1,12 +1,24 @@
|
|||
{
|
||||
"name": "obsidian-interactive-ratings",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.8",
|
||||
"description": "Edit symbol ratings in your Obsidian notes interactively.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"build": "node esbuild.config.js",
|
||||
"build:debug": "LOGGING_ENABLED=true node esbuild.config.js",
|
||||
"build:watch": "node esbuild.config.js --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
"build-and-install": "npm run build:debug && node install-dev.js",
|
||||
"type-check": "tsc --noEmit",
|
||||
"version:preview:patch": "bump-my-version bump patch --config-file .bumpversion-preview.toml",
|
||||
"version:preview:minor": "bump-my-version bump minor --config-file .bumpversion-preview.toml ",
|
||||
"version:preview:major": "bump-my-version bump major --config-file .bumpversion-preview.toml ",
|
||||
"version:preview": "bump-my-version bump preview --config-file .bumpversion-preview.toml",
|
||||
"version:release": "bump-my-version bump release",
|
||||
"version:patch": "bump-my-version bump patch",
|
||||
"version:minor": "bump-my-version bump minor",
|
||||
"version:major": "bump-my-version bump major",
|
||||
"version:show": "bump-my-version show-bump",
|
||||
"version:preview:show": "bump-my-version show-bump --config-file .bumpversion-preview.toml"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidianmd"
|
||||
|
|
@ -15,7 +27,7 @@
|
|||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.1",
|
||||
"obsidian": "^1.8.7",
|
||||
"obsidian": "^1.8.7",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,153 +1,50 @@
|
|||
import { App, MarkdownView, Plugin } from 'obsidian';
|
||||
import { Plugin } from 'obsidian';
|
||||
import { LOGGING_ENABLED } from './constants';
|
||||
import { handleEditorInteraction, handlePointerMove, handlePointerUp, isInSourceMode } from './event-handlers';
|
||||
import { calculateNewRating } from './utils';
|
||||
import { adaptEvent, ExtendedEditor } from './types';
|
||||
import { addInteractionListeners, applyRatingUpdate, createEditorOverlay, removeRatingsOverlay, updateOverlayDisplay } from './ratings-overlay';
|
||||
import { ratingEditorExtension } from './editor-extension/ratingViewPlugin/ratingViewPlugin';
|
||||
import { InteractiveRatingsSettings } from './types';
|
||||
import { DEFAULT_SETTINGS, InteractiveRatingsSettingTab } from './settings';
|
||||
import { updateSymbolPatternsFromSettings } from './utils/updateSymbolPatternsFromSettings';
|
||||
|
||||
export class InteractiveRatingsPlugin extends Plugin {
|
||||
app: App;
|
||||
ratingsOverlay: HTMLElement | null;
|
||||
settings: InteractiveRatingsSettings;
|
||||
|
||||
async onload() {
|
||||
async onload(): Promise<void> {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Plugin loading`);
|
||||
console.info('[InteractiveRatings] Plugin loading - edit mode only');
|
||||
}
|
||||
|
||||
// Use pointer events for all interactions
|
||||
this.registerDomEvent(document, 'pointermove', (evt: PointerEvent) => {
|
||||
const { isSourceMode, editor } = isInSourceMode(this.app);
|
||||
|
||||
// First handle potential rating detection
|
||||
if (isSourceMode && editor) {
|
||||
this.handleInteraction(evt, editor);
|
||||
}
|
||||
|
||||
// Then handle overlay movement if it exists
|
||||
handlePointerMove(evt, this.ratingsOverlay, this.updateOverlayDisplay.bind(this));
|
||||
});
|
||||
// Load settings
|
||||
await this.loadSettings();
|
||||
|
||||
// Add pointer down for initial detection and pointer capture
|
||||
this.registerDomEvent(document, 'pointerdown', (evt: PointerEvent) => {
|
||||
const { isSourceMode, editor } = isInSourceMode(this.app);
|
||||
if (isSourceMode && editor) {
|
||||
this.handleInteraction(evt, editor);
|
||||
}
|
||||
|
||||
// If we have an overlay and the event is within it, capture the pointer
|
||||
if (this.ratingsOverlay && evt.target instanceof HTMLElement) {
|
||||
const rect = this.ratingsOverlay.getBoundingClientRect();
|
||||
if (
|
||||
evt.clientX >= rect.left &&
|
||||
evt.clientX <= rect.right &&
|
||||
evt.clientY >= rect.top &&
|
||||
evt.clientY <= rect.bottom
|
||||
) {
|
||||
try {
|
||||
this.ratingsOverlay.setPointerCapture(evt.pointerId);
|
||||
} catch (e) {
|
||||
// Ignore errors with pointer capture
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Update symbol patterns based on settings
|
||||
this.updateSymbolPatterns();
|
||||
|
||||
// Pointer up to finalize the selection
|
||||
this.registerDomEvent(document, 'pointerup', (evt: PointerEvent) => {
|
||||
handlePointerUp(
|
||||
evt,
|
||||
this.ratingsOverlay,
|
||||
this.applyRatingUpdate.bind(this),
|
||||
this.removeRatingsOverlay.bind(this)
|
||||
);
|
||||
});
|
||||
// Register editor extension for interactive ratings in editing mode only
|
||||
this.registerEditorExtension(ratingEditorExtension);
|
||||
|
||||
// Also handle pointer cancel to clean up
|
||||
this.registerDomEvent(document, 'pointercancel', (evt: PointerEvent) => {
|
||||
this.removeRatingsOverlay();
|
||||
});
|
||||
// Add settings tab
|
||||
this.addSettingTab(new InteractiveRatingsSettingTab(this.app, this));
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Plugin loaded successfully`);
|
||||
console.info('[InteractiveRatings] Plugin loaded successfully');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified handler for all interactions to detect rating patterns
|
||||
*/
|
||||
handleInteraction(event: PointerEvent, editor: ExtendedEditor): void {
|
||||
handleEditorInteraction(
|
||||
adaptEvent(event),
|
||||
editor,
|
||||
this.ratingsOverlay,
|
||||
this.removeRatingsOverlay.bind(this),
|
||||
this.createEditorOverlay.bind(this)
|
||||
);
|
||||
onunload(): void {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info('[InteractiveRatings] Plugin unloaded');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an editor overlay for ratings interaction
|
||||
*/
|
||||
createEditorOverlay(
|
||||
editor: ExtendedEditor,
|
||||
line: number,
|
||||
start: number,
|
||||
pattern: string,
|
||||
originalRating: number,
|
||||
symbolSet: any,
|
||||
ratingText: any
|
||||
): void {
|
||||
this.ratingsOverlay = createEditorOverlay(
|
||||
editor,
|
||||
line,
|
||||
start,
|
||||
pattern,
|
||||
originalRating,
|
||||
symbolSet,
|
||||
ratingText,
|
||||
this.addInteractionListeners.bind(this)
|
||||
);
|
||||
async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the overlay display based on user interaction
|
||||
*/
|
||||
updateOverlayDisplay(overlay: HTMLElement, rating: number): void {
|
||||
updateOverlayDisplay(overlay, rating);
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the ratings overlay
|
||||
*/
|
||||
removeRatingsOverlay(): void {
|
||||
this.ratingsOverlay = removeRatingsOverlay(this.ratingsOverlay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the rating update to the document
|
||||
*/
|
||||
applyRatingUpdate(rating: number): void {
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const editor = markdownView?.editor as ExtendedEditor;
|
||||
if (!editor || !this.ratingsOverlay) return;
|
||||
|
||||
applyRatingUpdate(editor, this.ratingsOverlay, rating);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add interaction listeners to the overlay
|
||||
*/
|
||||
addInteractionListeners(container: HTMLElement): void {
|
||||
addInteractionListeners(
|
||||
container,
|
||||
this.applyRatingUpdate.bind(this),
|
||||
this.removeRatingsOverlay.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
const styleEl = document.getElementById('interactive-ratings-style');
|
||||
if (styleEl) styleEl.remove();
|
||||
this.removeRatingsOverlay();
|
||||
updateSymbolPatterns(): void {
|
||||
updateSymbolPatternsFromSettings(this.settings);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,46 @@
|
|||
import { SymbolSet } from './types';
|
||||
|
||||
// Global logging control
|
||||
export const LOGGING_ENABLED = false;
|
||||
// ============================================================================
|
||||
// CRITICAL: DO NOT MODIFY THE LOGGING CONFIGURATION BELOW
|
||||
// ============================================================================
|
||||
//
|
||||
// Logging is controlled via esbuild's sophisticated build-time mechanism:
|
||||
// - Production builds: `npm run build` (logging disabled)
|
||||
// - Debug builds: `npm run build:debug` (logging enabled via LOGGING_ENABLED=true)
|
||||
//
|
||||
// The value below is replaced at BUILD TIME by esbuild.config.js using the
|
||||
// `define` feature. This ensures zero runtime overhead for production builds.
|
||||
//
|
||||
// ⚠️ NEVER change this to a hardcoded boolean value!
|
||||
// ⚠️ Use the npm scripts to control logging instead:
|
||||
// - `npm run build` for production (no logging)
|
||||
// - `npm run build:debug` for debug builds (with logging)
|
||||
//
|
||||
// The esbuild configuration will replace `process.env.LOGGING_ENABLED` with
|
||||
// the actual boolean value at compile time, so this becomes a constant in
|
||||
// the final bundle with no runtime cost.
|
||||
// ============================================================================
|
||||
export const LOGGING_ENABLED = process.env.LOGGING_ENABLED === 'true';
|
||||
|
||||
// Define symbol patterns as a global constant
|
||||
export const SYMBOL_PATTERNS: SymbolSet[] = [
|
||||
// Base symbol patterns (excluding user-configurable emojis)
|
||||
export const BASE_SYMBOL_PATTERNS: SymbolSet[] = [
|
||||
{ full: '★', empty: '☆', half: null }, // Symbols
|
||||
{ full: '✦', empty: '✧', half: null }, // Star symbols
|
||||
{ full: '🌕', empty: '🌑', half: '🌗' }, // Moon phases
|
||||
{ full: '●', empty: '○', half: '◐' }, // Circles
|
||||
{ full: '■', empty: '□', half: '◧' }, // Squares
|
||||
{ full: '▲', empty: '△', half: null }, // Triangles (no half)
|
||||
|
||||
// Heart symbols
|
||||
{ full: '❤️', empty: '🤍', half: null }, // Red hearts
|
||||
{ full: '🧡', empty: '🤍', half: null }, // Orange hearts
|
||||
{ full: '💛', empty: '🤍', half: null }, // Yellow hearts
|
||||
{ full: '💚', empty: '🤍', half: null }, // Green hearts
|
||||
{ full: '💙', empty: '🤍', half: null }, // Blue hearts
|
||||
{ full: '💜', empty: '🤍', half: null }, // Purple hearts
|
||||
{ full: '🖤', empty: '🤍', half: null }, // Black hearts
|
||||
{ full: '🤎', empty: '🤍', half: null }, // Brown hearts
|
||||
|
||||
// Progress bar patterns
|
||||
{ full: '█', empty: '▁', half: null }, // Block progress
|
||||
{ full: '⣿', empty: '⣀', half: '⡇' }, // Braille dots
|
||||
|
|
@ -23,6 +54,20 @@ export const SYMBOL_PATTERNS: SymbolSet[] = [
|
|||
{ full: '█', empty: '░', half: null }, // Block/light shade
|
||||
];
|
||||
|
||||
// Mutable symbol patterns - starts with base patterns and user-configurable emojis are added dynamically
|
||||
export let SYMBOL_PATTERNS: SymbolSet[] = [
|
||||
...BASE_SYMBOL_PATTERNS,
|
||||
// User-configurable emojis are added dynamically from settings
|
||||
];
|
||||
|
||||
/**
|
||||
* Update the global symbol patterns array
|
||||
*/
|
||||
export function updateSymbolPatterns(newPatterns: SymbolSet[]): void {
|
||||
SYMBOL_PATTERNS.length = 0; // Clear the array
|
||||
SYMBOL_PATTERNS.push(...newPatterns); // Add new patterns
|
||||
}
|
||||
|
||||
// Interaction constants
|
||||
export const INTERACTION_BUFFER = 5; // Buffer for interaction detection
|
||||
export const OVERLAY_VERTICAL_ADJUSTMENT = 2.1; // Vertical alignment for overlay
|
||||
export const OVERLAY_VERTICAL_ADJUSTMENT = 2.1; // Vertical alignment for overlay
|
||||
11
src/editor-extension/RatingMatch.ts
Normal file
11
src/editor-extension/RatingMatch.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Interface for rating match data with optional rating text
|
||||
*/
|
||||
export interface RatingMatch {
|
||||
pattern: string;
|
||||
start: number;
|
||||
end: number;
|
||||
symbolSet: any;
|
||||
rating: number;
|
||||
ratingText?: any | null;
|
||||
}
|
||||
101
src/editor-extension/RatingWidget/RatingWidget.ts
Normal file
101
src/editor-extension/RatingWidget/RatingWidget.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { EditorView, WidgetType } from "@codemirror/view";
|
||||
import { getUnicodeCharLength } from '../../utils/getUnicodeCharLength';
|
||||
import { isFullOnlySymbol } from '../../utils/isFullOnlySymbol';
|
||||
import { RatingText } from '../../types';
|
||||
import { calculateRatingFromClick } from './calculateRatingFromClick';
|
||||
import { calculateRatingFromHover } from './calculateRatingFromHover';
|
||||
import { previewRating } from './previewRating';
|
||||
import { renderRating } from './renderRating';
|
||||
import { updateRating } from './updateRating';
|
||||
|
||||
/**
|
||||
* Rating widget for inline editing in CodeMirror
|
||||
* Handles both symbols and rating text with full half-symbol support, full-only symbols, and simplified HTML comments
|
||||
*/
|
||||
export class RatingWidget extends WidgetType {
|
||||
constructor(
|
||||
private pattern: string,
|
||||
private rating: number,
|
||||
private symbolSet: any,
|
||||
private startPos: number,
|
||||
private endPos: number,
|
||||
private ratingText?: RatingText | null
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
toDOM(view: EditorView): HTMLElement {
|
||||
const container = document.createElement('span');
|
||||
container.className = 'interactive-rating-editor-widget';
|
||||
container.setAttribute('data-rating', this.rating.toString());
|
||||
const unicodeLength = getUnicodeCharLength(this.pattern);
|
||||
container.setAttribute('data-pattern-length', unicodeLength.toString());
|
||||
container.setAttribute('data-supports-half', (!!this.symbolSet.half).toString());
|
||||
|
||||
const isFullOnly = isFullOnlySymbol(this.symbolSet);
|
||||
container.setAttribute('data-full-only', isFullOnly.toString());
|
||||
|
||||
// For full-only symbols, use denominator from rating text if available, otherwise use pattern length
|
||||
const displaySymbolCount = (isFullOnly && this.ratingText) ? this.ratingText.denominator : unicodeLength;
|
||||
container.setAttribute('data-display-symbol-count', displaySymbolCount.toString());
|
||||
|
||||
// Check if rating text is HTML comment format (only supported for full-only symbols)
|
||||
const isCommentFormat = isFullOnly && this.ratingText && this.ratingText.format === 'comment-fraction';
|
||||
container.setAttribute('data-comment-format', isCommentFormat ? 'true' : 'false');
|
||||
|
||||
// Create symbols container
|
||||
const symbolsContainer = document.createElement('span');
|
||||
symbolsContainer.className = 'interactive-rating-symbols';
|
||||
|
||||
// Create clickable symbols based on display count
|
||||
for (let i = 0; i < displaySymbolCount; i++) {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = this.symbolSet.full;
|
||||
span.className = 'interactive-rating-symbol';
|
||||
span.setAttribute('data-symbol-index', i.toString());
|
||||
|
||||
// Add click handler with full-only validation
|
||||
span.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const newRating = calculateRatingFromClick(e, span, i, this.symbolSet);
|
||||
updateRating(view, newRating, this.symbolSet, this.pattern, this.startPos, this.endPos, this.rating, this.ratingText);
|
||||
});
|
||||
|
||||
// Add hover preview
|
||||
span.addEventListener('mouseenter', (e) => {
|
||||
const previewRatingValue = calculateRatingFromHover(e, span, i, this.symbolSet);
|
||||
previewRating(previewRatingValue, container, this.symbolSet, this.pattern, this.ratingText);
|
||||
});
|
||||
|
||||
// Add mousemove for fine-grained preview
|
||||
span.addEventListener('mousemove', (e) => {
|
||||
const previewRatingValue = calculateRatingFromHover(e, span, i, this.symbolSet);
|
||||
previewRating(previewRatingValue, container, this.symbolSet, this.pattern, this.ratingText);
|
||||
});
|
||||
|
||||
symbolsContainer.appendChild(span);
|
||||
}
|
||||
|
||||
container.appendChild(symbolsContainer);
|
||||
|
||||
// Create rating text container if rating text exists and is not HTML comment format
|
||||
if (this.ratingText && !isCommentFormat) {
|
||||
const textContainer = document.createElement('span');
|
||||
textContainer.className = 'interactive-rating-text';
|
||||
textContainer.textContent = this.ratingText.text;
|
||||
container.appendChild(textContainer);
|
||||
}
|
||||
|
||||
// Reset on mouse leave
|
||||
container.addEventListener('mouseleave', () => {
|
||||
renderRating(this.rating, container, this.symbolSet, this.ratingText);
|
||||
});
|
||||
|
||||
// Initial render
|
||||
renderRating(this.rating, container, this.symbolSet, this.ratingText);
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
10
src/editor-extension/RatingWidget/applySymbolState.ts
Normal file
10
src/editor-extension/RatingWidget/applySymbolState.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* Apply the appropriate CSS class to a symbol based on its state
|
||||
*/
|
||||
export function applySymbolState(span: HTMLElement, state: 'rated' | 'unrated' | 'normal' | 'empty' | 'half'): void {
|
||||
// Remove all state classes
|
||||
span.classList.remove('interactive-rating-symbol--rated', 'interactive-rating-symbol--unrated', 'interactive-rating-symbol--normal', 'interactive-rating-symbol--empty', 'interactive-rating-symbol--half');
|
||||
|
||||
// Add the appropriate state class
|
||||
span.classList.add(`interactive-rating-symbol--${state}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { isFullOnlySymbol } from '../../utils/isFullOnlySymbol';
|
||||
|
||||
/**
|
||||
* Calculate rating from click position with full-only validation
|
||||
*/
|
||||
export function calculateRatingFromClick(
|
||||
event: MouseEvent,
|
||||
span: HTMLElement,
|
||||
symbolIndex: number,
|
||||
symbolSet: any
|
||||
): number {
|
||||
const isFullOnly = isFullOnlySymbol(symbolSet);
|
||||
|
||||
// For full-only symbols, don't support half ratings and enforce minimum rating of 1
|
||||
if (isFullOnly) {
|
||||
return Math.max(1, symbolIndex + 1);
|
||||
}
|
||||
|
||||
if (!symbolSet.half) {
|
||||
// No half-symbol support, return full symbol rating
|
||||
return symbolIndex + 1;
|
||||
}
|
||||
|
||||
const rect = span.getBoundingClientRect();
|
||||
const relativeX = event.clientX - rect.left;
|
||||
const symbolWidth = rect.width;
|
||||
const position = relativeX / symbolWidth;
|
||||
|
||||
// If clicked on left half, use half symbol; if right half, use full symbol
|
||||
if (position <= 0.5) {
|
||||
return symbolIndex + 0.5;
|
||||
} else {
|
||||
return symbolIndex + 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { isFullOnlySymbol } from '../../utils/isFullOnlySymbol';
|
||||
|
||||
/**
|
||||
* Calculate rating from hover position with full-only validation
|
||||
*/
|
||||
export function calculateRatingFromHover(
|
||||
event: MouseEvent,
|
||||
span: HTMLElement,
|
||||
symbolIndex: number,
|
||||
symbolSet: any
|
||||
): number {
|
||||
const isFullOnly = isFullOnlySymbol(symbolSet);
|
||||
|
||||
// For full-only symbols, don't support half ratings and enforce minimum rating of 1
|
||||
if (isFullOnly) {
|
||||
return Math.max(1, symbolIndex + 1);
|
||||
}
|
||||
|
||||
if (!symbolSet.half) {
|
||||
// No half-symbol support, return full symbol rating
|
||||
return symbolIndex + 1;
|
||||
}
|
||||
|
||||
const rect = span.getBoundingClientRect();
|
||||
const relativeX = event.clientX - rect.left;
|
||||
const symbolWidth = rect.width;
|
||||
const position = relativeX / symbolWidth;
|
||||
|
||||
// More responsive half-symbol detection for hover
|
||||
if (position <= 0.5) {
|
||||
return symbolIndex + 0.5;
|
||||
} else {
|
||||
return symbolIndex + 1;
|
||||
}
|
||||
}
|
||||
82
src/editor-extension/RatingWidget/previewRating.ts
Normal file
82
src/editor-extension/RatingWidget/previewRating.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { formatRatingText } from '../../utils/formatRatingText';
|
||||
import { getUnicodeCharLength } from '../../utils/getUnicodeCharLength';
|
||||
import { isFullOnlySymbol } from '../../utils/isFullOnlySymbol';
|
||||
import { LOGGING_ENABLED } from '../../constants';
|
||||
import { RatingText } from '../../types';
|
||||
import { applySymbolState } from './applySymbolState';
|
||||
|
||||
/**
|
||||
* Preview rating with proper half-symbol rendering and full-only symbol support
|
||||
*/
|
||||
export function previewRating(
|
||||
newRating: number,
|
||||
container: HTMLElement,
|
||||
symbolSet: any,
|
||||
pattern: string,
|
||||
ratingText?: RatingText | null
|
||||
): void {
|
||||
const symbolsContainer = container.querySelector('.interactive-rating-symbols');
|
||||
if (!symbolsContainer) return;
|
||||
|
||||
const spans = symbolsContainer.querySelectorAll('span');
|
||||
const isFullOnly = isFullOnlySymbol(symbolSet);
|
||||
const isCommentFormat = isFullOnly && ratingText && ratingText.format === 'comment-fraction';
|
||||
|
||||
spans.forEach((span, index) => {
|
||||
const symbolRating = index + 1;
|
||||
const halfRating = index + 0.5;
|
||||
|
||||
if (isFullOnly) {
|
||||
// For full-only symbols: show full symbol for rated, grey for unrated
|
||||
span.textContent = symbolSet.full;
|
||||
if (symbolRating <= newRating) {
|
||||
applySymbolState(span, 'rated');
|
||||
} else {
|
||||
applySymbolState(span, 'unrated');
|
||||
}
|
||||
} else {
|
||||
// Regular symbol behavior with distinct states for filled/half/empty
|
||||
if (symbolRating <= newRating) {
|
||||
// Full symbol - apply 'rated' state
|
||||
span.textContent = symbolSet.full;
|
||||
applySymbolState(span, 'rated');
|
||||
} else if (symbolSet.half && halfRating <= newRating && halfRating > newRating - 0.5) {
|
||||
// Half symbol - apply 'half' state
|
||||
span.textContent = symbolSet.half;
|
||||
applySymbolState(span, 'half');
|
||||
} else {
|
||||
// Empty symbol - apply 'empty' state
|
||||
span.textContent = symbolSet.empty;
|
||||
applySymbolState(span, 'empty');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Preview rating text if it exists and is not HTML comment format
|
||||
if (ratingText && !isCommentFormat) {
|
||||
const textContainer = container.querySelector('.interactive-rating-text');
|
||||
if (textContainer) {
|
||||
const unicodeLength = getUnicodeCharLength(pattern);
|
||||
const previewText = formatRatingText(
|
||||
ratingText.format,
|
||||
newRating,
|
||||
unicodeLength, // Always use actual pattern length for percentage calculations
|
||||
ratingText.denominator, // Use original denominator for fraction displays
|
||||
!!symbolSet.half && !isFullOnly,
|
||||
isFullOnly
|
||||
);
|
||||
textContainer.textContent = previewText;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug('[InteractiveRatings] Preview rating with simplified comment format support', {
|
||||
newRating,
|
||||
hasHalf: !!symbolSet.half,
|
||||
isFullOnly,
|
||||
isCommentFormat,
|
||||
symbolSet: symbolSet,
|
||||
denominator: ratingText?.denominator
|
||||
});
|
||||
}
|
||||
}
|
||||
69
src/editor-extension/RatingWidget/renderRating.ts
Normal file
69
src/editor-extension/RatingWidget/renderRating.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { isFullOnlySymbol } from '../../utils/isFullOnlySymbol';
|
||||
import { LOGGING_ENABLED } from '../../constants';
|
||||
import { RatingText } from '../../types';
|
||||
import { applySymbolState } from './applySymbolState';
|
||||
|
||||
/**
|
||||
* Render rating with proper half-symbol display and full-only symbol support
|
||||
*/
|
||||
export function renderRating(
|
||||
rating: number,
|
||||
container: HTMLElement,
|
||||
symbolSet: any,
|
||||
ratingText?: RatingText | null
|
||||
): void {
|
||||
const symbolsContainer = container.querySelector('.interactive-rating-symbols');
|
||||
if (!symbolsContainer) return;
|
||||
|
||||
const spans = symbolsContainer.querySelectorAll('span');
|
||||
const isFullOnly = isFullOnlySymbol(symbolSet);
|
||||
const isCommentFormat = isFullOnly && ratingText && ratingText.format === 'comment-fraction';
|
||||
|
||||
spans.forEach((span, index) => {
|
||||
const symbolRating = index + 1;
|
||||
const halfRating = index + 0.5;
|
||||
|
||||
if (isFullOnly) {
|
||||
// For full-only symbols: show full symbol for rated, grey for unrated
|
||||
span.textContent = symbolSet.full;
|
||||
if (symbolRating <= rating) {
|
||||
applySymbolState(span, 'rated');
|
||||
} else {
|
||||
applySymbolState(span, 'unrated');
|
||||
}
|
||||
} else {
|
||||
// Regular symbol behavior with distinct states for filled/half/empty
|
||||
if (symbolRating <= rating) {
|
||||
// Full symbol - apply 'rated' state
|
||||
span.textContent = symbolSet.full;
|
||||
applySymbolState(span, 'rated');
|
||||
} else if (symbolSet.half && halfRating <= rating && halfRating > rating - 0.5) {
|
||||
// Half symbol - apply 'half' state
|
||||
span.textContent = symbolSet.half;
|
||||
applySymbolState(span, 'half');
|
||||
} else {
|
||||
// Empty symbol - apply 'empty' state
|
||||
span.textContent = symbolSet.empty;
|
||||
applySymbolState(span, 'empty');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Restore original rating text (only if not HTML comment format)
|
||||
if (ratingText && !isCommentFormat) {
|
||||
const textContainer = container.querySelector('.interactive-rating-text');
|
||||
if (textContainer) {
|
||||
textContainer.textContent = ratingText.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug('[InteractiveRatings] Render rating with simplified comment format support', {
|
||||
rating,
|
||||
hasHalf: !!symbolSet.half,
|
||||
isFullOnly,
|
||||
isCommentFormat,
|
||||
symbolSet: symbolSet
|
||||
});
|
||||
}
|
||||
}
|
||||
94
src/editor-extension/RatingWidget/updateRating.ts
Normal file
94
src/editor-extension/RatingWidget/updateRating.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { EditorView } from "@codemirror/view";
|
||||
import { generateSymbolsStringForDisk } from '../../utils/generateSymbolsStringForDisk';
|
||||
import { formatRatingText } from '../../utils/formatRatingText';
|
||||
import { getUnicodeCharLength } from '../../utils/getUnicodeCharLength';
|
||||
import { isFullOnlySymbol } from '../../utils/isFullOnlySymbol';
|
||||
import { LOGGING_ENABLED } from '../../constants';
|
||||
import { RatingText } from '../../types';
|
||||
|
||||
/**
|
||||
* Update rating in the document with half-symbol support and full-only symbols
|
||||
*/
|
||||
export function updateRating(
|
||||
view: EditorView,
|
||||
newRating: number,
|
||||
symbolSet: any,
|
||||
pattern: string,
|
||||
startPos: number,
|
||||
endPos: number,
|
||||
rating: number,
|
||||
ratingText?: RatingText | null
|
||||
): void {
|
||||
try {
|
||||
const isFullOnly = isFullOnlySymbol(symbolSet);
|
||||
const unicodeLength = getUnicodeCharLength(pattern);
|
||||
|
||||
// For full-only symbols, use the disk-safe function that only includes rated symbols
|
||||
const newSymbols = generateSymbolsStringForDisk(
|
||||
newRating,
|
||||
unicodeLength,
|
||||
symbolSet.full,
|
||||
symbolSet.empty,
|
||||
symbolSet.half || '',
|
||||
!!symbolSet.half && !isFullOnly,
|
||||
symbolSet
|
||||
);
|
||||
|
||||
// Generate new rating text
|
||||
let newText = newSymbols;
|
||||
let newRatingFormat = '';
|
||||
|
||||
if (ratingText) {
|
||||
// Use existing rating text format
|
||||
newRatingFormat = ratingText.format;
|
||||
} else if (isFullOnly) {
|
||||
// Auto-add HTML comment rating text for full-only symbols without rating text
|
||||
newRatingFormat = 'comment-fraction';
|
||||
}
|
||||
|
||||
if (newRatingFormat) {
|
||||
const denominator = ratingText ? ratingText.denominator : unicodeLength;
|
||||
const newRatingText = formatRatingText(
|
||||
newRatingFormat,
|
||||
newRating,
|
||||
unicodeLength, // Always use actual pattern length for percentage calculations
|
||||
denominator, // Use original denominator for fraction displays
|
||||
!!symbolSet.half && !isFullOnly,
|
||||
isFullOnly
|
||||
);
|
||||
newText = newSymbols + newRatingText;
|
||||
}
|
||||
|
||||
// Update the document (replace both symbols and rating text)
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: startPos,
|
||||
to: endPos,
|
||||
insert: newText
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent keyboard from appearing on mobile by blurring the editor
|
||||
view.contentDOM.blur();
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info('[InteractiveRatings] Rating updated with auto-add HTML comment support', {
|
||||
oldRating: rating,
|
||||
newRating,
|
||||
newSymbols,
|
||||
newText,
|
||||
hasRatingText: !!ratingText,
|
||||
hasHalf: !!symbolSet.half,
|
||||
isFullOnly,
|
||||
autoAddedComment: !ratingText && isFullOnly,
|
||||
newRatingFormat,
|
||||
denominator: ratingText ? ratingText.denominator : unicodeLength,
|
||||
position: { from: startPos, to: endPos }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.error('[InteractiveRatings] Error updating rating', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { getUnicodeCharLength } from '../../utils/getUnicodeCharLength';
|
||||
import { isFullOnlySymbol } from '../../utils/isFullOnlySymbol';
|
||||
import { LOGGING_ENABLED } from '../../constants';
|
||||
import { RatingMatch } from '../RatingMatch';
|
||||
import { RatingWidget } from '../RatingWidget/RatingWidget';
|
||||
|
||||
/**
|
||||
* Build decorations from filtered matches, skipping those near the cursor
|
||||
*/
|
||||
export function buildDecorationsFromMatches(
|
||||
matches: RatingMatch[],
|
||||
cursorPos: number,
|
||||
builder: RangeSetBuilder<Decoration>
|
||||
): void {
|
||||
// Add decorations in sorted order, but skip if cursor is nearby
|
||||
for (const match of matches) {
|
||||
// Skip creating widget if cursor is near this rating
|
||||
if (cursorPos >= match.start - 1 && cursorPos <= match.end + 1) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug('[InteractiveRatings] Skipping widget creation due to nearby cursor', {
|
||||
cursorPos,
|
||||
ratingStart: match.start,
|
||||
ratingEnd: match.end
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const decoration = Decoration.replace({
|
||||
widget: new RatingWidget(
|
||||
match.pattern,
|
||||
match.rating,
|
||||
match.symbolSet,
|
||||
match.start,
|
||||
match.end,
|
||||
match.ratingText
|
||||
),
|
||||
inclusive: false
|
||||
});
|
||||
|
||||
builder.add(match.start, match.end, decoration);
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED && matches.length > 0) {
|
||||
const skippedCount = matches.filter(m =>
|
||||
cursorPos >= m.start - 1 && cursorPos <= m.end + 1
|
||||
).length;
|
||||
const fullOnlyCount = matches.filter(m => isFullOnlySymbol(m.symbolSet)).length;
|
||||
const commentCount = matches.filter(m => m.ratingText && m.ratingText.format === 'comment-fraction').length;
|
||||
const autoAddCandidates = matches.filter(m => isFullOnlySymbol(m.symbolSet) && !m.ratingText).length;
|
||||
const shortPatternCount = matches.filter(m => getUnicodeCharLength(m.pattern) < 3).length;
|
||||
console.debug(`[InteractiveRatings] Built ${matches.length - skippedCount}/${matches.length} rating decorations (${skippedCount} skipped due to cursor proximity)`, {
|
||||
cursorPos,
|
||||
withRatingText: matches.filter(m => m.ratingText).length,
|
||||
symbolsOnly: matches.filter(m => !m.ratingText).length,
|
||||
withHalfSymbols: matches.filter(m => m.symbolSet.half).length,
|
||||
fullOnlySymbols: fullOnlyCount,
|
||||
commentFormatSymbols: commentCount,
|
||||
autoAddCandidates,
|
||||
shortPatterns: shortPatternCount
|
||||
});
|
||||
}
|
||||
}
|
||||
75
src/editor-extension/ratingViewPlugin/collectMatches.ts
Normal file
75
src/editor-extension/ratingViewPlugin/collectMatches.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { generateSymbolRegexPatterns } from '../../ratings-parser/generateSymbolRegexPatterns';
|
||||
import { getSymbolSetForPattern } from '../../ratings-parser/getSymbolSetForPattern';
|
||||
import { calculateRating } from '../../ratings-parser/calculateRating';
|
||||
import { parseRatingText } from '../../ratings-parser/parseRatingText/parseRatingText';
|
||||
import { getUnicodeCharLength } from '../../utils/getUnicodeCharLength';
|
||||
import { isFullOnlySymbol } from '../../utils/isFullOnlySymbol';
|
||||
import { LOGGING_ENABLED } from '../../constants';
|
||||
import { RatingMatch } from '../RatingMatch';
|
||||
|
||||
/**
|
||||
* Collect all rating matches from the text
|
||||
*/
|
||||
export function collectMatches(text: string): RatingMatch[] {
|
||||
const matches: RatingMatch[] = [];
|
||||
const symbolRegexes = generateSymbolRegexPatterns();
|
||||
|
||||
for (const regex of symbolRegexes) {
|
||||
let match;
|
||||
regex.lastIndex = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const pattern = match[0];
|
||||
const start = match.index;
|
||||
const end = start + pattern.length;
|
||||
|
||||
const symbolSet = getSymbolSetForPattern(pattern);
|
||||
if (!symbolSet) continue;
|
||||
|
||||
const rating = calculateRating(pattern, symbolSet);
|
||||
const isFullOnly = isFullOnlySymbol(symbolSet);
|
||||
|
||||
// For full-only symbols, skip if rating is 0 (not supported)
|
||||
if (isFullOnly && rating === 0) continue;
|
||||
|
||||
// Check for rating text after the symbols - pass UTF-16 positions
|
||||
const ratingText = parseRatingText(text, start, end);
|
||||
|
||||
// Simple rule: if no rating text, require minimum 3 symbols to avoid false positives
|
||||
const unicodeLength = getUnicodeCharLength(pattern);
|
||||
if (!ratingText && unicodeLength < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const actualEnd = ratingText ? ratingText.endPosition : end;
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug('[InteractiveRatings] Found rating match', {
|
||||
pattern,
|
||||
start,
|
||||
end,
|
||||
actualEnd,
|
||||
unicodeLength,
|
||||
rating,
|
||||
hasRatingText: !!ratingText,
|
||||
ratingTextDetails: ratingText,
|
||||
symbolSet: symbolSet,
|
||||
isFullOnly,
|
||||
isCommentFormat: ratingText && ratingText.format === 'comment-fraction',
|
||||
canAutoAddComment: isFullOnly && !ratingText
|
||||
});
|
||||
}
|
||||
|
||||
matches.push({
|
||||
pattern,
|
||||
start,
|
||||
end: actualEnd,
|
||||
symbolSet,
|
||||
rating,
|
||||
ratingText
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { RatingMatch } from '../RatingMatch';
|
||||
|
||||
/**
|
||||
* Filter overlapping matches, keeping the first one in each overlapping group
|
||||
*/
|
||||
export function filterOverlappingMatches(matches: RatingMatch[]): RatingMatch[] {
|
||||
// Sort matches by start position (required by RangeSetBuilder)
|
||||
const sortedMatches = [...matches].sort((a, b) => a.start - b.start);
|
||||
|
||||
// Remove overlapping matches (keep the first one)
|
||||
const filteredMatches: RatingMatch[] = [];
|
||||
let lastEnd = -1;
|
||||
|
||||
for (const match of sortedMatches) {
|
||||
if (match.start >= lastEnd) {
|
||||
filteredMatches.push(match);
|
||||
lastEnd = match.end;
|
||||
}
|
||||
}
|
||||
|
||||
return filteredMatches;
|
||||
}
|
||||
56
src/editor-extension/ratingViewPlugin/ratingViewPlugin.ts
Normal file
56
src/editor-extension/ratingViewPlugin/ratingViewPlugin.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { EditorView, DecorationSet, ViewPlugin, ViewUpdate, Decoration } from "@codemirror/view";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { LOGGING_ENABLED } from '../../constants';
|
||||
import { collectMatches } from './collectMatches';
|
||||
import { filterOverlappingMatches } from './filterOverlappingMatches';
|
||||
import { buildDecorationsFromMatches } from './buildDecorationsFromMatches';
|
||||
|
||||
/**
|
||||
* ViewPlugin to detect and replace rating patterns with interactive widgets
|
||||
*/
|
||||
export const ratingViewPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView): DecorationSet {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
|
||||
try {
|
||||
const text = view.state.doc.toString();
|
||||
const cursorPos = view.state.selection.main.head;
|
||||
|
||||
// Collect all matches first
|
||||
const matches = collectMatches(text);
|
||||
|
||||
// Filter overlapping matches
|
||||
const filteredMatches = filterOverlappingMatches(matches);
|
||||
|
||||
// Build decorations from filtered matches
|
||||
buildDecorationsFromMatches(filteredMatches, cursorPos, builder);
|
||||
|
||||
} catch (error) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.error('[InteractiveRatings] Error building decorations', error);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: v => v.decorations
|
||||
}
|
||||
);
|
||||
|
||||
// Export the extension array
|
||||
export const ratingEditorExtension = [ratingViewPlugin];
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
import { Editor, MarkdownView } from 'obsidian';
|
||||
import { LOGGING_ENABLED } from './constants';
|
||||
import { isInteractionWithinRegion, isInteractingWithElement } from './ratings-calculator';
|
||||
import { calculateRating, generateSymbolRegexPatterns, getSymbolSetForPattern, parseRatingText } from './ratings-parser';
|
||||
import { EditorInteractionEvent, ExtendedEditor, RatingText } from './types';
|
||||
import { calculateNewRating, getUnicodeCharLength } from './utils';
|
||||
|
||||
/**
|
||||
* Handle editor interactions to detect and process rating patterns
|
||||
*/
|
||||
export function handleEditorInteraction(
|
||||
event: EditorInteractionEvent,
|
||||
editor: ExtendedEditor,
|
||||
ratingsOverlay: HTMLElement | null,
|
||||
removeRatingsOverlayFn: () => void,
|
||||
createEditorOverlayFn: (
|
||||
editor: ExtendedEditor,
|
||||
line: number,
|
||||
start: number,
|
||||
symbols: string,
|
||||
originalRating: number,
|
||||
symbolSet: any,
|
||||
ratingText: RatingText | null
|
||||
) => void
|
||||
): void {
|
||||
// Clear any existing overlay if not interacting with it
|
||||
if (ratingsOverlay && !isInteractingWithElement(event, ratingsOverlay)) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Removing overlay as interaction moved away`);
|
||||
}
|
||||
removeRatingsOverlayFn();
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
|
||||
// Get editor position using the original event
|
||||
const editorPos = editor.posAtMouse(event.originalEvent);
|
||||
if (!editorPos) return;
|
||||
|
||||
// Get the line at the current position
|
||||
const line = editor.getLine(editorPos.line);
|
||||
|
||||
// Generate regex patterns from the symbol sets
|
||||
const symbolRegexes = generateSymbolRegexPatterns();
|
||||
|
||||
// Check for all symbol patterns
|
||||
for (const regex of symbolRegexes) {
|
||||
let match;
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
const start = match.index;
|
||||
const end = start + getUnicodeCharLength(match[0]);
|
||||
|
||||
// Parse rating text if present
|
||||
const ratingText = parseRatingText(line, start, end);
|
||||
const textEndPosition = ratingText ? ratingText.endPosition : end;
|
||||
|
||||
// Calculate character position range
|
||||
const startPos = { line: editorPos.line, ch: start };
|
||||
const endPos = { line: editorPos.line, ch: textEndPosition };
|
||||
|
||||
// Get screen coordinates for start and end of pattern
|
||||
const startCoords = editor.coordsAtPos(startPos);
|
||||
const endCoords = editor.coordsAtPos(endPos);
|
||||
|
||||
if (!startCoords || !endCoords) continue;
|
||||
|
||||
// Check if interaction is inside the region
|
||||
if (isInteractionWithinRegion(event, startCoords, endCoords)) {
|
||||
// Determine pattern type by checking characters
|
||||
const pattern = match[0];
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Detected rating pattern`, {
|
||||
pattern,
|
||||
line: editorPos.line,
|
||||
start,
|
||||
end,
|
||||
ratingText: ratingText ? JSON.stringify(ratingText) : 'none'
|
||||
});
|
||||
};
|
||||
|
||||
const symbolSet = getSymbolSetForPattern(pattern);
|
||||
|
||||
if (!symbolSet) continue;
|
||||
|
||||
// Calculate original rating (count full symbols as 1.0 and half symbols as 0.5)
|
||||
const originalRating = calculateRating(pattern, symbolSet);
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Detected original rating`, {
|
||||
originalRating,
|
||||
pattern,
|
||||
symbolSet: JSON.stringify(symbolSet)
|
||||
});
|
||||
}
|
||||
|
||||
// Create overlay if it doesn't exist or is for a different pattern
|
||||
if (!ratingsOverlay || ratingsOverlay.dataset.linePosition !== `${editorPos.line}-${start}`) {
|
||||
createEditorOverlayFn(editor, editorPos.line, start, pattern, originalRating, symbolSet, ratingText);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pointer move event for rating overlays
|
||||
*/
|
||||
export function handlePointerMove(
|
||||
event: PointerEvent,
|
||||
ratingsOverlay: HTMLElement | null,
|
||||
updateOverlayDisplayFn: (overlay: HTMLElement, rating: number) => void
|
||||
): void {
|
||||
if (ratingsOverlay) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Pointer move event while overlay exists`, {
|
||||
pointerType: event.pointerType,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
pointerId: event.pointerId
|
||||
});
|
||||
}
|
||||
|
||||
const rating = calculateNewRating(ratingsOverlay, event.clientX);
|
||||
updateOverlayDisplayFn(ratingsOverlay, rating);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pointer up event for rating overlays
|
||||
*/
|
||||
export function handlePointerUp(
|
||||
event: PointerEvent,
|
||||
ratingsOverlay: HTMLElement | null,
|
||||
applyRatingUpdateFn: (rating: number) => void,
|
||||
removeRatingsOverlayFn: () => void
|
||||
): void {
|
||||
if (ratingsOverlay) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Pointer up event while overlay exists`, {
|
||||
pointerType: event.pointerType,
|
||||
pointerId: event.pointerId
|
||||
});
|
||||
}
|
||||
|
||||
const rating = parseFloat(ratingsOverlay.dataset.currentRating);
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Finalizing rating`, { rating });
|
||||
}
|
||||
|
||||
applyRatingUpdateFn(rating);
|
||||
removeRatingsOverlayFn();
|
||||
|
||||
// Release pointer capture if it was captured
|
||||
try {
|
||||
const element = event.target as HTMLElement;
|
||||
if (element && element.hasPointerCapture(event.pointerId)) {
|
||||
element.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors with pointer capture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we are in source mode in an editor view
|
||||
*/
|
||||
export function isInSourceMode(app: any): {isSourceMode: boolean, editor: ExtendedEditor | null, markdownView: MarkdownView | null} {
|
||||
// Check if we're in editor mode using getActiveViewOfType
|
||||
const markdownView = app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!markdownView) {
|
||||
return { isSourceMode: false, editor: null, markdownView: null };
|
||||
}
|
||||
|
||||
// Check if the view is in source mode
|
||||
const isSourceMode = markdownView.getMode() !== 'preview';
|
||||
if (!isSourceMode) {
|
||||
return { isSourceMode: false, editor: null, markdownView: null };
|
||||
}
|
||||
|
||||
// Get the editor using the markdownView
|
||||
const editor = markdownView.editor as ExtendedEditor;
|
||||
if (!editor) {
|
||||
return { isSourceMode: false, editor: null, markdownView: null };
|
||||
}
|
||||
|
||||
return { isSourceMode: true, editor, markdownView };
|
||||
}
|
||||
30
src/main.ts
30
src/main.ts
|
|
@ -1,13 +1,31 @@
|
|||
import { InteractiveRatingsPlugin } from './InteractiveRatingsPlugin';
|
||||
|
||||
// Re-export from all modules
|
||||
// Re-export from core modules
|
||||
export * from './constants';
|
||||
export * from './utils';
|
||||
export * from './types';
|
||||
export * from './ratings-parser';
|
||||
export * from './ratings-calculator';
|
||||
export * from './ratings-overlay';
|
||||
export * from './event-handlers';
|
||||
|
||||
// Re-export from utils
|
||||
export { getUnicodeCharLength } from './utils/getUnicodeCharLength';
|
||||
export { getUnicodeSubstring } from './utils/getUnicodeSubstring';
|
||||
export { utf16ToUnicodePosition } from './utils/utf16ToUnicodePosition';
|
||||
export { isFullOnlySymbol } from './utils/isFullOnlySymbol';
|
||||
export { generateSymbolsString } from './utils/generateSymbolsString';
|
||||
export { generateSymbolsStringForDisk } from './utils/generateSymbolsStringForDisk';
|
||||
export { calculateNewRating } from './utils/calculateNewRating';
|
||||
export { formatRatingText } from './utils/formatRatingText';
|
||||
export { updateSymbolPatternsFromSettings } from './utils/updateSymbolPatternsFromSettings';
|
||||
|
||||
// Re-export from ratings-parser
|
||||
export { parseRatingText } from './ratings-parser/parseRatingText/parseRatingText';
|
||||
export { getSymbolSetForPattern } from './ratings-parser/getSymbolSetForPattern';
|
||||
export { calculateRating } from './ratings-parser/calculateRating';
|
||||
export { escapeRegexChar } from './ratings-parser/escapeRegexChar';
|
||||
export { generateSymbolRegexPatterns } from './ratings-parser/generateSymbolRegexPatterns';
|
||||
|
||||
// Re-export from editor-extension
|
||||
export { ratingViewPlugin, ratingEditorExtension } from './editor-extension/ratingViewPlugin/ratingViewPlugin';
|
||||
export { RatingWidget } from './editor-extension/RatingWidget/RatingWidget';
|
||||
export { RatingMatch } from './editor-extension/RatingMatch';
|
||||
|
||||
// Export the main plugin class
|
||||
export default InteractiveRatingsPlugin;
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { LOGGING_ENABLED } from './constants';
|
||||
import { Coordinates, EditorInteractionEvent } from './types';
|
||||
|
||||
/**
|
||||
* Check if an event is interacting with a specific DOM element
|
||||
*/
|
||||
export function isInteractingWithElement(
|
||||
event: EditorInteractionEvent,
|
||||
element: HTMLElement
|
||||
): boolean {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const { clientX, clientY, pointerType } = event;
|
||||
|
||||
const isInteracting = (
|
||||
clientX >= rect.left &&
|
||||
clientX <= rect.right &&
|
||||
clientY >= rect.top &&
|
||||
clientY <= rect.bottom
|
||||
);
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Checking interaction with element (${pointerType || 'pointer'})`, {
|
||||
clientX,
|
||||
clientY,
|
||||
rect: {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
top: rect.top,
|
||||
bottom: rect.bottom
|
||||
},
|
||||
isInteracting
|
||||
});
|
||||
};
|
||||
|
||||
return isInteracting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an interaction event is within a specific region defined by coordinates
|
||||
*/
|
||||
export function isInteractionWithinRegion(
|
||||
event: EditorInteractionEvent,
|
||||
startCoords: Coordinates,
|
||||
endCoords: Coordinates,
|
||||
buffer: number = 5
|
||||
): boolean {
|
||||
const { clientX, clientY } = event;
|
||||
|
||||
const isWithin = (
|
||||
clientX >= startCoords.left - buffer &&
|
||||
clientX <= endCoords.right + buffer &&
|
||||
clientY >= startCoords.top - buffer &&
|
||||
clientY <= endCoords.bottom + buffer
|
||||
);
|
||||
|
||||
return isWithin;
|
||||
}
|
||||
|
|
@ -1,457 +0,0 @@
|
|||
import { Editor } from 'obsidian';
|
||||
import { LOGGING_ENABLED, OVERLAY_VERTICAL_ADJUSTMENT } from './constants';
|
||||
import { ExtendedEditor, RatingText, SymbolSet } from './types';
|
||||
import { calculateNewRating, formatRatingText, generateSymbolsString, getUnicodeCharLength } from './utils';
|
||||
|
||||
/**
|
||||
* Create an overlay for interactive rating editing
|
||||
*/
|
||||
export function createEditorOverlay(
|
||||
editor: ExtendedEditor,
|
||||
line: number,
|
||||
start: number,
|
||||
symbols: string,
|
||||
originalRating: number,
|
||||
symbolSet: SymbolSet,
|
||||
ratingText: RatingText | null,
|
||||
addInteractionListeners: (container: HTMLElement) => void
|
||||
): HTMLElement | null {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.group(`[InteractiveRatings] Creating editor overlay`);
|
||||
};
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Creating editor overlay`, {
|
||||
line,
|
||||
start,
|
||||
symbols,
|
||||
originalRating,
|
||||
symbolSet,
|
||||
ratingText: ratingText ? JSON.stringify(ratingText) : 'none'
|
||||
});
|
||||
};
|
||||
|
||||
// First, remove all existing overlays
|
||||
document.querySelectorAll('.interactive-ratings-editor-overlay').forEach(el => {
|
||||
el.remove();
|
||||
});
|
||||
|
||||
// Get coordinates for the position
|
||||
const posCoords = editor.coordsAtPos({ line: line, ch: start });
|
||||
if (!posCoords) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.warn(`[InteractiveRatings] Could not get coordinates for position`, { line, start });
|
||||
console.groupEnd();
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create container for the overlay
|
||||
const overlay = document.createElement('div');
|
||||
|
||||
overlay.tabIndex = 0;
|
||||
|
||||
overlay.className = 'interactive-ratings-editor-overlay';
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.zIndex = '1000';
|
||||
overlay.style.backgroundColor = 'var(--background-primary)';
|
||||
overlay.style.left = `${posCoords.left}px`;
|
||||
|
||||
// Calculate a more precise vertical position
|
||||
overlay.style.top = `${posCoords.top - OVERLAY_VERTICAL_ADJUSTMENT}px`;
|
||||
|
||||
// Store position information for comparison
|
||||
overlay.dataset.linePosition = `${line}-${start}`;
|
||||
|
||||
// Store original rating and symbol information
|
||||
overlay.dataset.originalRating = originalRating.toString();
|
||||
overlay.dataset.full = symbolSet.full;
|
||||
overlay.dataset.empty = symbolSet.empty;
|
||||
overlay.dataset.half = symbolSet.half || '';
|
||||
overlay.dataset.supportsHalf = symbolSet.half ? 'true' : 'false';
|
||||
overlay.dataset.originalSymbols = symbols;
|
||||
|
||||
if (ratingText) {
|
||||
overlay.dataset.hasRatingText = 'true';
|
||||
overlay.dataset.ratingFormat = ratingText.format;
|
||||
overlay.dataset.ratingNumerator = ratingText.numerator.toString();
|
||||
overlay.dataset.ratingDenominator = ratingText.denominator.toString();
|
||||
overlay.dataset.ratingText = ratingText.text;
|
||||
overlay.dataset.ratingEndPosition = ratingText.endPosition.toString();
|
||||
} else {
|
||||
overlay.dataset.hasRatingText = 'false';
|
||||
}
|
||||
|
||||
// Track current hover position
|
||||
overlay.dataset.currentRating = "0";
|
||||
|
||||
// Get the editor element and compute its styles
|
||||
const editorEl = editor.editorComponent.editorEl;
|
||||
const editorStyles = window.getComputedStyle(editorEl);
|
||||
|
||||
// Also, ensure vertical alignment matches
|
||||
overlay.style.verticalAlign = editorStyles.verticalAlign || 'baseline';
|
||||
|
||||
// Match editor font properties exactly
|
||||
overlay.style.fontFamily = editorStyles.fontFamily;
|
||||
overlay.style.fontSize = editorStyles.fontSize;
|
||||
overlay.style.fontWeight = editorStyles.fontWeight;
|
||||
overlay.style.letterSpacing = editorStyles.letterSpacing;
|
||||
overlay.style.lineHeight = editorStyles.lineHeight;
|
||||
overlay.style.cursor = 'pointer';
|
||||
|
||||
// Apply exact padding and margins to match editor
|
||||
overlay.style.margin = '0';
|
||||
overlay.style.border = 'none';
|
||||
overlay.style.boxSizing = 'border-box';
|
||||
|
||||
// Prevent any text selection that might cause movement
|
||||
overlay.style.userSelect = 'none';
|
||||
|
||||
// Use Unicode-aware character counting
|
||||
const symbolCount = getUnicodeCharLength(symbols);
|
||||
overlay.dataset.symbolCount = symbolCount.toString();
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Constructed overlay element`, {
|
||||
position: `${posCoords.left}px, ${posCoords.top - OVERLAY_VERTICAL_ADJUSTMENT}px`,
|
||||
symbolCount,
|
||||
datasets: { ...overlay.dataset }
|
||||
});
|
||||
}
|
||||
|
||||
// Add symbols to the overlay - properly iterate over Unicode characters
|
||||
const symbolsArray = [...symbols];
|
||||
for (let i = 0; i < symbolCount; i++) {
|
||||
const symbolSpan = document.createElement('span');
|
||||
symbolSpan.className = 'interactive-ratings-symbol';
|
||||
symbolSpan.textContent = symbolsArray[i];
|
||||
symbolSpan.dataset.position = i.toString();
|
||||
symbolSpan.dataset.originalChar = symbolsArray[i];
|
||||
|
||||
symbolSpan.style.padding = '0';
|
||||
symbolSpan.style.margin = '0';
|
||||
symbolSpan.style.height = 'auto';
|
||||
symbolSpan.style.display = 'inline-block';
|
||||
|
||||
overlay.appendChild(symbolSpan);
|
||||
}
|
||||
|
||||
// Add event listeners for mouse interactions
|
||||
addInteractionListeners(overlay);
|
||||
|
||||
// Add to document
|
||||
document.body.appendChild(overlay);
|
||||
overlay.focus();
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Editor overlay created successfully`);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the rating overlay display based on the current rating
|
||||
*/
|
||||
export function updateOverlayDisplay(overlay: HTMLElement, rating: number): void {
|
||||
overlay.dataset.currentRating = rating.toString();
|
||||
|
||||
const symbols = overlay.querySelectorAll('.interactive-ratings-symbol');
|
||||
const full = overlay.dataset.full;
|
||||
const empty = overlay.dataset.empty;
|
||||
const half = overlay.dataset.half;
|
||||
const supportsHalf = overlay.dataset.supportsHalf === 'true';
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Updating overlay display`, {
|
||||
rating,
|
||||
full,
|
||||
empty,
|
||||
half,
|
||||
supportsHalf,
|
||||
symbolCount: symbols.length
|
||||
});
|
||||
}
|
||||
|
||||
symbols.forEach((symbol, index) => {
|
||||
const oldContent = symbol.textContent;
|
||||
let newContent;
|
||||
|
||||
if (index < Math.floor(rating)) {
|
||||
newContent = full;
|
||||
} else if (supportsHalf && index === Math.floor(rating) && rating % 1 !== 0) {
|
||||
newContent = half;
|
||||
} else {
|
||||
newContent = empty;
|
||||
}
|
||||
|
||||
if (oldContent !== newContent) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Symbol ${index} changing from ${oldContent} to ${newContent}`);
|
||||
}
|
||||
symbol.textContent = newContent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the ratings overlay from the DOM
|
||||
*/
|
||||
export function removeRatingsOverlay(ratingsOverlay: HTMLElement | null): HTMLElement | null {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.group(`[InteractiveRatings] Removing ratings overlay`);
|
||||
}
|
||||
|
||||
if (ratingsOverlay) {
|
||||
if (ratingsOverlay.parentNode) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Removing ratings overlay from DOM`, {
|
||||
linePosition: ratingsOverlay.dataset.linePosition,
|
||||
currentRating: ratingsOverlay.dataset.currentRating
|
||||
});
|
||||
}
|
||||
|
||||
ratingsOverlay.blur();
|
||||
ratingsOverlay.parentNode.removeChild(ratingsOverlay);
|
||||
return null;
|
||||
} else {
|
||||
console.warn("Ratings overlay exists but is not in DOM");
|
||||
}
|
||||
} else {
|
||||
console.debug("No ratings overlay to remove");
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
return ratingsOverlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the rating in the editor
|
||||
*/
|
||||
export function updateRatingInEditor(
|
||||
editor: ExtendedEditor,
|
||||
line: number,
|
||||
start: number,
|
||||
newSymbols: string,
|
||||
updatedRatingText: string,
|
||||
originalSymbols: string,
|
||||
hasRatingText: string,
|
||||
ratingEndPosition: string
|
||||
): void {
|
||||
let startPos = { line: line, ch: start };
|
||||
let endPos;
|
||||
|
||||
if (hasRatingText === 'true') {
|
||||
// Use the stored rating end position
|
||||
endPos = { line: line, ch: parseInt(ratingEndPosition) };
|
||||
} else {
|
||||
// If there's no rating text, just replace the symbols
|
||||
endPos = { line: line, ch: start + getUnicodeCharLength(originalSymbols) };
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Updating document in editor`, {
|
||||
line,
|
||||
start,
|
||||
startPos,
|
||||
endPos,
|
||||
newSymbols,
|
||||
updatedRatingText,
|
||||
originalSymbols,
|
||||
hasRatingText,
|
||||
ratingEndPosition,
|
||||
finalContent: newSymbols + updatedRatingText
|
||||
});
|
||||
};
|
||||
|
||||
// Replace the entire content
|
||||
editor.replaceRange(
|
||||
newSymbols + updatedRatingText,
|
||||
startPos,
|
||||
endPos
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the rating update to the editor
|
||||
*/
|
||||
export function applyRatingUpdate(
|
||||
editor: ExtendedEditor,
|
||||
overlay: HTMLElement,
|
||||
rating: number
|
||||
): void {
|
||||
if (!overlay) return;
|
||||
|
||||
const line = parseInt(overlay.dataset.linePosition.split('-')[0]);
|
||||
const start = parseInt(overlay.dataset.linePosition.split('-')[1]);
|
||||
const symbolCount = parseInt(overlay.dataset.symbolCount);
|
||||
const full = overlay.dataset.full;
|
||||
const empty = overlay.dataset.empty;
|
||||
const half = overlay.dataset.half;
|
||||
const supportsHalf = overlay.dataset.supportsHalf === 'true';
|
||||
const hasRatingText = overlay.dataset.hasRatingText;
|
||||
const originalSymbols = overlay.dataset.originalSymbols;
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.group(`[InteractiveRatings] Applying rating update`);
|
||||
};
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Rating update details`, {
|
||||
rating,
|
||||
line,
|
||||
start,
|
||||
symbolCount,
|
||||
full,
|
||||
empty,
|
||||
half,
|
||||
supportsHalf,
|
||||
hasRatingText,
|
||||
originalSymbols
|
||||
});
|
||||
};
|
||||
|
||||
// Generate new symbols string
|
||||
const newSymbols = generateSymbolsString(rating, symbolCount, full, empty, half, supportsHalf);
|
||||
|
||||
// Handle rating text update
|
||||
let updatedRatingText = '';
|
||||
if (hasRatingText === 'true') {
|
||||
const format = overlay.dataset.ratingFormat;
|
||||
const denominator = parseInt(overlay.dataset.ratingDenominator);
|
||||
let adjustedRating = rating;
|
||||
if (rating > denominator) {
|
||||
adjustedRating = denominator;
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Rating capped to denominator`, { rating: adjustedRating, denominator });
|
||||
};
|
||||
}
|
||||
updatedRatingText = formatRatingText(format, adjustedRating, symbolCount, denominator, supportsHalf);
|
||||
}
|
||||
|
||||
// Update the document
|
||||
updateRatingInEditor(
|
||||
editor,
|
||||
line,
|
||||
start,
|
||||
newSymbols,
|
||||
updatedRatingText,
|
||||
originalSymbols,
|
||||
hasRatingText,
|
||||
overlay.dataset.ratingEndPosition
|
||||
);
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add interaction listeners to the overlay
|
||||
*/
|
||||
export function addInteractionListeners(
|
||||
container: HTMLElement,
|
||||
applyRatingFn: (rating: number) => void,
|
||||
removeOverlayFn: () => void
|
||||
): void {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.group(`[InteractiveRatings] Adding interaction listeners`);
|
||||
console.info("Adding interaction listeners to overlay", {
|
||||
linePosition: container.dataset.linePosition,
|
||||
symbolCount: container.dataset.symbolCount,
|
||||
supportsHalf: container.dataset.supportsHalf
|
||||
});
|
||||
}
|
||||
|
||||
const symbols = container.querySelectorAll('.interactive-ratings-symbol');
|
||||
const full = container.dataset.full;
|
||||
const empty = container.dataset.empty;
|
||||
const half = container.dataset.half;
|
||||
const supportsHalf = container.dataset.supportsHalf === 'true';
|
||||
|
||||
// Use pointer events for all interactions
|
||||
container.addEventListener('pointermove', (e) => {
|
||||
e.preventDefault(); // Prevent default behavior
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Pointer move event on overlay`, {
|
||||
pointerType: e.pointerType,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
pointerId: e.pointerId
|
||||
});
|
||||
}
|
||||
|
||||
const rating = calculateNewRating(container, e.clientX);
|
||||
updateOverlayDisplay(container, rating);
|
||||
});
|
||||
|
||||
// Reset on pointer leave
|
||||
container.addEventListener('pointerleave', () => {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Pointer leave event on overlay`);
|
||||
}
|
||||
|
||||
// Reset to original state
|
||||
symbols.forEach((symbol) => {
|
||||
const originalChar = (symbol as HTMLElement).dataset.originalChar || empty;
|
||||
symbol.textContent = originalChar;
|
||||
});
|
||||
});
|
||||
|
||||
// Pointer down to capture the pointer
|
||||
container.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault(); // Prevent default behavior like cursor movement
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Pointer down event on overlay`, {
|
||||
pointerType: e.pointerType,
|
||||
pointerId: e.pointerId
|
||||
});
|
||||
}
|
||||
|
||||
// Capture pointer to ensure we get all events even if finger moves outside the element
|
||||
try {
|
||||
container.setPointerCapture(e.pointerId);
|
||||
} catch (e) {
|
||||
// Ignore errors with pointer capture
|
||||
}
|
||||
});
|
||||
|
||||
// Pointer up to finalize the selection
|
||||
container.addEventListener('pointerup', (e) => {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info(`[InteractiveRatings] Pointer up event on overlay`, {
|
||||
pointerType: e.pointerType,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
pointerId: e.pointerId
|
||||
});
|
||||
}
|
||||
|
||||
const rating = calculateNewRating(container, e.clientX);
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Rating on pointer up`, { rating });
|
||||
}
|
||||
|
||||
applyRatingFn(rating);
|
||||
removeOverlayFn();
|
||||
|
||||
// Release pointer capture
|
||||
try {
|
||||
if (container.hasPointerCapture(e.pointerId)) {
|
||||
container.releasePointerCapture(e.pointerId);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors with pointer capture
|
||||
}
|
||||
});
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info("Interaction listeners added successfully");
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import { LOGGING_ENABLED, SYMBOL_PATTERNS } from './constants';
|
||||
import { RatingText, SymbolSet } from './types';
|
||||
import { getUnicodeCharLength, getUnicodeSubstring } from './utils';
|
||||
|
||||
/**
|
||||
* Parse rating text from a line
|
||||
*/
|
||||
export function parseRatingText(line: string, start: number, end: number): RatingText | null {
|
||||
// Get the substring after the symbols using Unicode-aware functions
|
||||
const afterSymbols = getUnicodeSubstring(line, end, getUnicodeCharLength(line));
|
||||
|
||||
// Check for rating patterns
|
||||
const ratingTextMatch = afterSymbols.match(/^\s*(?:\(([\d\.]+)\/(\d+)\)|([\d\.]+)\/(\d+)|(?:\()?(\d+)%(?:\))?)/);
|
||||
|
||||
if (ratingTextMatch) {
|
||||
let format = '';
|
||||
let numerator = 0;
|
||||
let denominator = 0;
|
||||
|
||||
if (ratingTextMatch[1] && ratingTextMatch[2]) {
|
||||
// (14.5/33) format
|
||||
format = 'fraction-parentheses';
|
||||
numerator = parseFloat(ratingTextMatch[1]);
|
||||
denominator = parseInt(ratingTextMatch[2]);
|
||||
} else if (ratingTextMatch[3] && ratingTextMatch[4]) {
|
||||
// 14.5/33 format
|
||||
format = 'fraction';
|
||||
numerator = parseFloat(ratingTextMatch[3]);
|
||||
denominator = parseInt(ratingTextMatch[4]);
|
||||
} else if (ratingTextMatch[5]) {
|
||||
// 60% or (60%) format
|
||||
format = afterSymbols.includes('(') ? 'percent-parentheses' : 'percent';
|
||||
numerator = parseInt(ratingTextMatch[5]);
|
||||
denominator = 100;
|
||||
}
|
||||
|
||||
// Calculate the end position correctly with Unicode-aware calculations
|
||||
const endPosition = end + ratingTextMatch[0].length;
|
||||
|
||||
const result: RatingText = {
|
||||
format,
|
||||
numerator,
|
||||
denominator,
|
||||
text: ratingTextMatch[0],
|
||||
endPosition: endPosition
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate symbol set for a given rating pattern
|
||||
*/
|
||||
export function getSymbolSetForPattern(pattern: string): SymbolSet | null {
|
||||
// Find the symbol set that matches the pattern
|
||||
for (const symbolSet of SYMBOL_PATTERNS) {
|
||||
if (pattern.includes(symbolSet.full) || pattern.includes(symbolSet.empty) ||
|
||||
(symbolSet.half && pattern.includes(symbolSet.half))) {
|
||||
return symbolSet;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the rating value from a pattern string
|
||||
*/
|
||||
export function calculateRating(pattern: string, symbolSet: SymbolSet): number {
|
||||
let rating = 0;
|
||||
// Use array spread to properly iterate over Unicode characters
|
||||
for (const char of [...pattern]) {
|
||||
if (char === symbolSet.full) rating += 1.0;
|
||||
else if (symbolSet.half && char === symbolSet.half) rating += 0.5;
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Calculated pattern rating`, {
|
||||
pattern,
|
||||
full: symbolSet.full,
|
||||
empty: symbolSet.empty,
|
||||
half: symbolSet.half,
|
||||
rating
|
||||
});
|
||||
};
|
||||
|
||||
return rating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate regex patterns for detecting rating symbols in text
|
||||
*/
|
||||
export function generateSymbolRegexPatterns(): RegExp[] {
|
||||
return SYMBOL_PATTERNS.map(symbols => {
|
||||
const pattern = `[${symbols.full}${symbols.empty}${symbols.half ? symbols.half : ''}]{3,}`;
|
||||
return new RegExp(pattern, 'g');
|
||||
});
|
||||
}
|
||||
27
src/ratings-parser/calculateRating.ts
Normal file
27
src/ratings-parser/calculateRating.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { LOGGING_ENABLED } from '../constants';
|
||||
import { SymbolSet } from '../types';
|
||||
|
||||
/**
|
||||
* Calculate the rating value from a pattern string
|
||||
*/
|
||||
export function calculateRating(pattern: string, symbolSet: SymbolSet): number {
|
||||
let rating = 0;
|
||||
// Use Intl.Segmenter to properly iterate over grapheme clusters (handles ZWJ sequences)
|
||||
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
||||
for (const {segment: char} of segmenter.segment(pattern)) {
|
||||
if (char === symbolSet.full) rating += 1.0;
|
||||
else if (symbolSet.half && char === symbolSet.half) rating += 0.5;
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Calculated pattern rating`, {
|
||||
pattern,
|
||||
full: symbolSet.full,
|
||||
empty: symbolSet.empty,
|
||||
half: symbolSet.half,
|
||||
rating
|
||||
});
|
||||
};
|
||||
|
||||
return rating;
|
||||
}
|
||||
6
src/ratings-parser/escapeRegexChar.ts
Normal file
6
src/ratings-parser/escapeRegexChar.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Escape special regex characters, handling Unicode properly
|
||||
*/
|
||||
export function escapeRegexChar(char: string): string {
|
||||
return char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
33
src/ratings-parser/generateSymbolRegexPatterns.ts
Normal file
33
src/ratings-parser/generateSymbolRegexPatterns.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { LOGGING_ENABLED, SYMBOL_PATTERNS } from '../constants';
|
||||
import { escapeRegexChar } from './escapeRegexChar';
|
||||
|
||||
/**
|
||||
* Generate regex patterns for detecting rating symbols in text
|
||||
* Fixed to handle multibyte Unicode characters (emojis) properly
|
||||
* Now supports 1+ symbols to detect low ratings
|
||||
*/
|
||||
export function generateSymbolRegexPatterns(): RegExp[] {
|
||||
return SYMBOL_PATTERNS.map(symbols => {
|
||||
// Build alternation pattern instead of character class for emoji support
|
||||
const symbolChars = [symbols.full, symbols.empty];
|
||||
if (symbols.half) {
|
||||
symbolChars.push(symbols.half);
|
||||
}
|
||||
|
||||
// Escape each symbol for regex and create alternation
|
||||
const escapedSymbols = symbolChars.map(escapeRegexChar);
|
||||
// Changed from {3,} to {1,} to detect single symbols (fixes low rating bug)
|
||||
const pattern = `(?:${escapedSymbols.join('|')})+`;
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Generated regex pattern for symbol set`, {
|
||||
symbolSet: symbols,
|
||||
pattern,
|
||||
escapedSymbols,
|
||||
allowsSingleSymbol: true
|
||||
});
|
||||
}
|
||||
|
||||
return new RegExp(pattern, 'g');
|
||||
});
|
||||
}
|
||||
16
src/ratings-parser/getSymbolSetForPattern.ts
Normal file
16
src/ratings-parser/getSymbolSetForPattern.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { SYMBOL_PATTERNS } from '../constants';
|
||||
import { SymbolSet } from '../types';
|
||||
|
||||
/**
|
||||
* Get the appropriate symbol set for a given rating pattern
|
||||
*/
|
||||
export function getSymbolSetForPattern(pattern: string): SymbolSet | null {
|
||||
// Find the symbol set that matches the pattern
|
||||
for (const symbolSet of SYMBOL_PATTERNS) {
|
||||
if (pattern.includes(symbolSet.full) || pattern.includes(symbolSet.empty) ||
|
||||
(symbolSet.half && pattern.includes(symbolSet.half))) {
|
||||
return symbolSet;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
75
src/ratings-parser/parseRatingText/buildRatingTextResult.ts
Normal file
75
src/ratings-parser/parseRatingText/buildRatingTextResult.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { LOGGING_ENABLED } from '../../constants';
|
||||
import { RatingText } from '../../types';
|
||||
|
||||
/**
|
||||
* Build the final RatingText result from matched patterns
|
||||
*/
|
||||
export function buildRatingTextResult(
|
||||
visibleMatch: RegExpMatchArray | null,
|
||||
htmlCommentMatch: RegExpMatchArray | null,
|
||||
totalMatchLength: number,
|
||||
utf16End: number
|
||||
): RatingText | null {
|
||||
// Use visible rating if present, otherwise use comment
|
||||
const match = visibleMatch || htmlCommentMatch;
|
||||
const isComment = !visibleMatch && !!htmlCommentMatch;
|
||||
const hasBothFormats = !!(visibleMatch && htmlCommentMatch);
|
||||
|
||||
if (match) {
|
||||
let format = '';
|
||||
let numerator = 0;
|
||||
let denominator = 0;
|
||||
|
||||
if (isComment && htmlCommentMatch) {
|
||||
// Simplified HTML comment format: <!-- 3/5 -->
|
||||
format = 'comment-fraction';
|
||||
numerator = parseFloat(htmlCommentMatch[1]);
|
||||
denominator = parseInt(htmlCommentMatch[2]);
|
||||
} else if (match[1] && match[2]) {
|
||||
// (14.5/33) format
|
||||
format = 'fraction-parentheses';
|
||||
numerator = parseFloat(match[1]);
|
||||
denominator = parseInt(match[2]);
|
||||
} else if (match[3] && match[4]) {
|
||||
// 14.5/33 format
|
||||
format = 'fraction';
|
||||
numerator = parseFloat(match[3]);
|
||||
denominator = parseInt(match[4]);
|
||||
} else if (match[5]) {
|
||||
// 60% or (60%) format
|
||||
const hasParens = match[0].includes('(') && match[0].includes(')');
|
||||
format = hasParens ? 'percent-parentheses' : 'percent';
|
||||
numerator = parseInt(match[5]);
|
||||
denominator = 100;
|
||||
}
|
||||
|
||||
// Calculate the end position correctly - use total length when both formats present
|
||||
const endPosition = utf16End + totalMatchLength;
|
||||
|
||||
const result: RatingText = {
|
||||
format,
|
||||
numerator,
|
||||
denominator,
|
||||
text: match[0],
|
||||
endPosition: endPosition
|
||||
};
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug('[InteractiveRatings] Found rating text', {
|
||||
result,
|
||||
matchedText: match[0],
|
||||
isComment,
|
||||
hasPrecedence: !isComment,
|
||||
hasBothFormats,
|
||||
visibleMatchLength: visibleMatch ? visibleMatch[0].length : 0,
|
||||
commentMatchLength: htmlCommentMatch ? htmlCommentMatch[0].length : 0,
|
||||
totalMatchLength,
|
||||
calculatedEndPosition: endPosition
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
34
src/ratings-parser/parseRatingText/convertPositions.ts
Normal file
34
src/ratings-parser/parseRatingText/convertPositions.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { getUnicodeCharLength } from '../../utils/getUnicodeCharLength';
|
||||
import { getUnicodeSubstring } from '../../utils/getUnicodeSubstring';
|
||||
import { utf16ToUnicodePosition } from '../../utils/utf16ToUnicodePosition';
|
||||
import { LOGGING_ENABLED } from '../../constants';
|
||||
|
||||
/**
|
||||
* Convert UTF-16 positions to Unicode and extract text after symbols
|
||||
*/
|
||||
export function convertPositions(line: string, utf16Start: number, utf16End: number): {
|
||||
unicodeStart: number;
|
||||
unicodeEnd: number;
|
||||
afterSymbols: string;
|
||||
} {
|
||||
// Convert UTF-16 positions to Unicode character positions
|
||||
const unicodeStart = utf16ToUnicodePosition(line, utf16Start);
|
||||
const unicodeEnd = utf16ToUnicodePosition(line, utf16End);
|
||||
|
||||
// Get the substring after the symbols using Unicode-aware functions
|
||||
const afterSymbols = getUnicodeSubstring(line, unicodeEnd, getUnicodeCharLength(line));
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug('[InteractiveRatings] convertPositions debug', {
|
||||
utf16Start,
|
||||
utf16End,
|
||||
unicodeStart,
|
||||
unicodeEnd,
|
||||
afterSymbols: afterSymbols.substring(0, 40) + '...',
|
||||
lineLength: line.length,
|
||||
unicodeLineLength: getUnicodeCharLength(line)
|
||||
});
|
||||
}
|
||||
|
||||
return { unicodeStart, unicodeEnd, afterSymbols };
|
||||
}
|
||||
37
src/ratings-parser/parseRatingText/findRatingPatterns.ts
Normal file
37
src/ratings-parser/parseRatingText/findRatingPatterns.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Find rating patterns in text after symbols
|
||||
*/
|
||||
export function findRatingPatterns(afterSymbols: string): {
|
||||
visibleMatch: RegExpMatchArray | null;
|
||||
htmlCommentMatch: RegExpMatchArray | null;
|
||||
totalMatchLength: number;
|
||||
} {
|
||||
// Check for regular rating patterns first (takes precedence over HTML comments)
|
||||
const ratingTextMatch = afterSymbols.match(/^\s*(?:\(([\d\.]+)\/(\d+)\)|([\d\.]+)\/(\d+)|(?:\()?(\d+)%(?:\))?)/);
|
||||
|
||||
// Check for simplified HTML comment rating patterns (only basic fraction format)
|
||||
const commentMatch = afterSymbols.match(/^\s*<!--\s*([\d\.]+)\/(\d+)\s*-->/);
|
||||
|
||||
// Look for both formats to handle precedence properly
|
||||
let visibleMatch = null;
|
||||
let htmlCommentMatch = null;
|
||||
let totalMatchLength = 0;
|
||||
|
||||
if (ratingTextMatch) {
|
||||
visibleMatch = ratingTextMatch;
|
||||
totalMatchLength = ratingTextMatch[0].length;
|
||||
|
||||
// Check if there's also an HTML comment after the visible rating
|
||||
const afterVisible = afterSymbols.substring(ratingTextMatch[0].length);
|
||||
const followingCommentMatch = afterVisible.match(/^\s*<!--\s*([\d\.]+)\/(\d+)\s*-->/);
|
||||
if (followingCommentMatch) {
|
||||
htmlCommentMatch = followingCommentMatch;
|
||||
totalMatchLength += followingCommentMatch[0].length;
|
||||
}
|
||||
} else if (commentMatch) {
|
||||
htmlCommentMatch = commentMatch;
|
||||
totalMatchLength = commentMatch[0].length;
|
||||
}
|
||||
|
||||
return { visibleMatch, htmlCommentMatch, totalMatchLength };
|
||||
}
|
||||
21
src/ratings-parser/parseRatingText/parseRatingText.ts
Normal file
21
src/ratings-parser/parseRatingText/parseRatingText.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { RatingText } from '../../types';
|
||||
import { convertPositions } from './convertPositions';
|
||||
import { findRatingPatterns } from './findRatingPatterns';
|
||||
import { buildRatingTextResult } from './buildRatingTextResult';
|
||||
|
||||
/**
|
||||
* Parse rating text from a line, including HTML comment formats with precedence
|
||||
* @param line The full line of text
|
||||
* @param utf16Start UTF-16 byte position where symbols start
|
||||
* @param utf16End UTF-16 byte position where symbols end
|
||||
*/
|
||||
export function parseRatingText(line: string, utf16Start: number, utf16End: number): RatingText | null {
|
||||
// Convert UTF-16 positions to Unicode and extract text after symbols
|
||||
const { afterSymbols } = convertPositions(line, utf16Start, utf16End);
|
||||
|
||||
// Find rating patterns in the text
|
||||
const { visibleMatch, htmlCommentMatch, totalMatchLength } = findRatingPatterns(afterSymbols);
|
||||
|
||||
// Build and return the result
|
||||
return buildRatingTextResult(visibleMatch, htmlCommentMatch, totalMatchLength, utf16End);
|
||||
}
|
||||
39
src/settings.ts
Normal file
39
src/settings.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { InteractiveRatingsPlugin } from './InteractiveRatingsPlugin';
|
||||
import { InteractiveRatingsSettings } from './types';
|
||||
|
||||
const DEFAULT_SUPPORTED_EMOJIS = '🎥🏆⭐💎🔥⚡🎯🚀💰🎖️';
|
||||
|
||||
export const DEFAULT_SETTINGS: InteractiveRatingsSettings = {
|
||||
supportedEmojis: DEFAULT_SUPPORTED_EMOJIS
|
||||
};
|
||||
|
||||
export class InteractiveRatingsSettingTab extends PluginSettingTab {
|
||||
plugin: InteractiveRatingsPlugin;
|
||||
|
||||
constructor(app: App, plugin: InteractiveRatingsPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: 'Interactive Ratings Settings' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Emojis to support in ratings')
|
||||
.setDesc('Emojis to use for ratings.')
|
||||
.addTextArea(text => text
|
||||
.setPlaceholder(DEFAULT_SUPPORTED_EMOJIS)
|
||||
.setValue(this.plugin.settings.supportedEmojis)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.supportedEmojis = value;
|
||||
await this.plugin.saveSettings();
|
||||
// Update the symbol patterns with new emojis
|
||||
this.plugin.updateSymbolPatterns();
|
||||
}));
|
||||
}
|
||||
}
|
||||
49
src/types.ts
49
src/types.ts
|
|
@ -1,15 +1,3 @@
|
|||
import { Editor, MarkdownView } from 'obsidian';
|
||||
|
||||
// Extend the Editor interface to include CM6 specific methods
|
||||
export interface ExtendedEditor extends Editor {
|
||||
posAtMouse(event: MouseEvent): Position;
|
||||
posAtCoords(coords: {left: number, top: number}): Position;
|
||||
coordsAtPos(pos: Position): Coordinates;
|
||||
editorComponent: {
|
||||
editorEl: HTMLElement;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SymbolSet {
|
||||
full: string;
|
||||
empty: string;
|
||||
|
|
@ -24,37 +12,6 @@ export interface RatingText {
|
|||
endPosition: number;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
line: number;
|
||||
ch: number;
|
||||
}
|
||||
|
||||
export interface Coordinates {
|
||||
left: number;
|
||||
right?: number;
|
||||
top: number;
|
||||
bottom?: number;
|
||||
}
|
||||
|
||||
export type InteractionType = 'mouse' | 'touch';
|
||||
|
||||
export interface EditorInteractionEvent {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
target: EventTarget | null;
|
||||
originalEvent: PointerEvent;
|
||||
pointerId?: number;
|
||||
pointerType?: string;
|
||||
}
|
||||
|
||||
// Simplified adapter for pointer events
|
||||
export function adaptEvent(event: PointerEvent): EditorInteractionEvent {
|
||||
return {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
target: event.target,
|
||||
originalEvent: event,
|
||||
pointerId: event.pointerId,
|
||||
pointerType: event.pointerType
|
||||
};
|
||||
}
|
||||
export interface InteractiveRatingsSettings {
|
||||
supportedEmojis: string;
|
||||
}
|
||||
149
src/utils.ts
149
src/utils.ts
|
|
@ -1,149 +0,0 @@
|
|||
import { LOGGING_ENABLED } from './constants';
|
||||
|
||||
/**
|
||||
* Get the length of a string in Unicode characters
|
||||
*/
|
||||
export function getUnicodeCharLength(str: string): number {
|
||||
return [...str].length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a substring with proper Unicode character handling
|
||||
*/
|
||||
export function getUnicodeSubstring(str: string, start: number, end: number): string {
|
||||
return [...str].slice(start, end).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the new rating based on cursor position
|
||||
*/
|
||||
export function calculateNewRating(overlay: HTMLElement, clientX: number): number {
|
||||
const containerRect = overlay.getBoundingClientRect();
|
||||
const symbolCount = parseInt(overlay.dataset.symbolCount);
|
||||
const symbolWidth = containerRect.width / symbolCount;
|
||||
const relativeX = clientX - containerRect.left;
|
||||
const hoveredSymbolIndex = Math.floor(relativeX / symbolWidth);
|
||||
const positionWithinSymbol = (relativeX % symbolWidth) / symbolWidth;
|
||||
|
||||
// Determine if we should use half symbol or full symbol
|
||||
const supportsHalf = overlay.dataset.supportsHalf === 'true';
|
||||
const useHalfSymbol = supportsHalf && positionWithinSymbol < 0.5;
|
||||
|
||||
// Calculate rating
|
||||
let newRating = hoveredSymbolIndex + (useHalfSymbol ? 0.5 : 1);
|
||||
if (newRating < 0) {
|
||||
newRating = 0;
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Calculated new rating`, {
|
||||
clientX,
|
||||
containerRect: {
|
||||
left: containerRect.left,
|
||||
width: containerRect.width
|
||||
},
|
||||
symbolCount,
|
||||
symbolWidth,
|
||||
relativeX,
|
||||
hoveredSymbolIndex,
|
||||
positionWithinSymbol,
|
||||
supportsHalf,
|
||||
useHalfSymbol,
|
||||
newRating
|
||||
});
|
||||
}
|
||||
|
||||
return newRating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a string of rating symbols
|
||||
*/
|
||||
export function generateSymbolsString(
|
||||
rating: number,
|
||||
symbolCount: number,
|
||||
full: string,
|
||||
empty: string,
|
||||
half: string,
|
||||
supportsHalf: boolean
|
||||
): string {
|
||||
let newSymbols = '';
|
||||
|
||||
for (let i = 0; i < symbolCount; i++) {
|
||||
if (i < Math.floor(rating)) {
|
||||
newSymbols += full;
|
||||
} else if (supportsHalf && i === Math.floor(rating) && rating % 1 !== 0) {
|
||||
newSymbols += half;
|
||||
} else {
|
||||
newSymbols += empty;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Generated symbols string`, {
|
||||
rating,
|
||||
symbolCount,
|
||||
full,
|
||||
empty,
|
||||
half,
|
||||
supportsHalf,
|
||||
newSymbols
|
||||
});
|
||||
};
|
||||
|
||||
return newSymbols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the rating text based on the specified format
|
||||
*/
|
||||
export function formatRatingText(
|
||||
format: string,
|
||||
newRating: number,
|
||||
symbolCount: number,
|
||||
denominator: number,
|
||||
supportsHalf: boolean
|
||||
): string {
|
||||
let newNumerator;
|
||||
if (format.includes('percent')) {
|
||||
newNumerator = Math.round((newRating / symbolCount) * 100);
|
||||
} else {
|
||||
newNumerator = newRating;
|
||||
if (!supportsHalf) {
|
||||
newNumerator = Math.round(newNumerator);
|
||||
}
|
||||
}
|
||||
|
||||
// Format the text based on the original format
|
||||
let formattedText = '';
|
||||
switch (format) {
|
||||
case 'fraction':
|
||||
formattedText = ` ${newNumerator}/${denominator}`;
|
||||
break;
|
||||
case 'fraction-parentheses':
|
||||
formattedText = ` (${newNumerator}/${denominator})`;
|
||||
break;
|
||||
case 'percent':
|
||||
formattedText = ` ${newNumerator}%`;
|
||||
break;
|
||||
case 'percent-parentheses':
|
||||
formattedText = ` (${newNumerator}%)`;
|
||||
break;
|
||||
default:
|
||||
formattedText = '';
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Formatted rating text`, {
|
||||
format,
|
||||
newRating,
|
||||
symbolCount,
|
||||
denominator,
|
||||
supportsHalf,
|
||||
newNumerator,
|
||||
formattedText
|
||||
});
|
||||
};
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
43
src/utils/calculateNewRating.ts
Normal file
43
src/utils/calculateNewRating.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { LOGGING_ENABLED } from '../constants';
|
||||
|
||||
/**
|
||||
* Calculate the new rating based on cursor position
|
||||
*/
|
||||
export function calculateNewRating(overlay: HTMLElement, clientX: number): number {
|
||||
const containerRect = overlay.getBoundingClientRect();
|
||||
const symbolCount = parseInt(overlay.dataset.symbolCount);
|
||||
const symbolWidth = containerRect.width / symbolCount;
|
||||
const relativeX = clientX - containerRect.left;
|
||||
const hoveredSymbolIndex = Math.floor(relativeX / symbolWidth);
|
||||
const positionWithinSymbol = (relativeX % symbolWidth) / symbolWidth;
|
||||
|
||||
// Determine if we should use half symbol or full symbol
|
||||
const supportsHalf = overlay.dataset.supportsHalf === 'true';
|
||||
const useHalfSymbol = supportsHalf && positionWithinSymbol < 0.5;
|
||||
|
||||
// Calculate rating
|
||||
let newRating = hoveredSymbolIndex + (useHalfSymbol ? 0.5 : 1);
|
||||
if (newRating < 0) {
|
||||
newRating = 0;
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Calculated new rating`, {
|
||||
clientX,
|
||||
containerRect: {
|
||||
left: containerRect.left,
|
||||
width: containerRect.width
|
||||
},
|
||||
symbolCount,
|
||||
symbolWidth,
|
||||
relativeX,
|
||||
hoveredSymbolIndex,
|
||||
positionWithinSymbol,
|
||||
supportsHalf,
|
||||
useHalfSymbol,
|
||||
newRating
|
||||
});
|
||||
}
|
||||
|
||||
return newRating;
|
||||
}
|
||||
69
src/utils/formatRatingText.ts
Normal file
69
src/utils/formatRatingText.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { LOGGING_ENABLED } from '../constants';
|
||||
|
||||
/**
|
||||
* Format the rating text based on the specified format, including simplified HTML comments
|
||||
*/
|
||||
export function formatRatingText(
|
||||
format: string,
|
||||
newRating: number,
|
||||
symbolCount: number,
|
||||
denominator: number,
|
||||
supportsHalf: boolean,
|
||||
isFullOnlySymbol: boolean = false
|
||||
): string {
|
||||
let newNumerator;
|
||||
if (format.includes('percent')) {
|
||||
newNumerator = Math.round((newRating / symbolCount) * 100);
|
||||
} else {
|
||||
newNumerator = newRating;
|
||||
if (!supportsHalf) {
|
||||
newNumerator = Math.round(newNumerator);
|
||||
}
|
||||
}
|
||||
|
||||
// For full-only symbols with HTML comment format, remove comment if perfect rating
|
||||
if (isFullOnlySymbol && format === 'comment-fraction' && newNumerator === denominator) {
|
||||
return ''; // Remove HTML comment for perfect ratings
|
||||
}
|
||||
|
||||
// Format the text based on the original format
|
||||
let formattedText = '';
|
||||
switch (format) {
|
||||
case 'fraction':
|
||||
formattedText = ` ${newNumerator}/${denominator}`;
|
||||
break;
|
||||
case 'fraction-parentheses':
|
||||
formattedText = ` (${newNumerator}/${denominator})`;
|
||||
break;
|
||||
case 'percent':
|
||||
formattedText = ` ${newNumerator}%`;
|
||||
break;
|
||||
case 'percent-parentheses':
|
||||
formattedText = ` (${newNumerator}%)`;
|
||||
break;
|
||||
// Simplified HTML comment format (only basic fraction)
|
||||
case 'comment-fraction':
|
||||
formattedText = `<!-- ${newNumerator}/${denominator} -->`;
|
||||
break;
|
||||
default:
|
||||
formattedText = '';
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Formatted rating text`, {
|
||||
format,
|
||||
newRating,
|
||||
symbolCount,
|
||||
denominator,
|
||||
supportsHalf,
|
||||
isFullOnlySymbol,
|
||||
newNumerator,
|
||||
formattedText,
|
||||
isComment: format.startsWith('comment-'),
|
||||
isPerfectRating: newNumerator === denominator,
|
||||
removedComment: isFullOnlySymbol && format === 'comment-fraction' && newNumerator === denominator
|
||||
});
|
||||
};
|
||||
|
||||
return formattedText;
|
||||
}
|
||||
51
src/utils/generateSymbolsString.ts
Normal file
51
src/utils/generateSymbolsString.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { LOGGING_ENABLED } from '../constants';
|
||||
import { SymbolSet } from '../types';
|
||||
import { isFullOnlySymbol } from './isFullOnlySymbol';
|
||||
|
||||
/**
|
||||
* Generate a string of rating symbols for display purposes
|
||||
* For full-only symbols with rating text, this shows denominator count symbols
|
||||
*/
|
||||
export function generateSymbolsString(
|
||||
rating: number,
|
||||
symbolCount: number,
|
||||
full: string,
|
||||
empty: string,
|
||||
half: string,
|
||||
supportsHalf: boolean,
|
||||
symbolSet?: SymbolSet,
|
||||
denominator?: number
|
||||
): string {
|
||||
// For full-only symbols with rating text, use denominator for total count
|
||||
const isFullOnly = symbolSet && isFullOnlySymbol(symbolSet);
|
||||
const totalSymbols = (isFullOnly && denominator) ? denominator : symbolCount;
|
||||
|
||||
let newSymbols = '';
|
||||
|
||||
for (let i = 0; i < totalSymbols; i++) {
|
||||
if (i < Math.floor(rating)) {
|
||||
newSymbols += full;
|
||||
} else if (supportsHalf && i === Math.floor(rating) && rating % 1 !== 0) {
|
||||
newSymbols += half;
|
||||
} else {
|
||||
newSymbols += empty;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug(`[InteractiveRatings] Generated symbols string`, {
|
||||
rating,
|
||||
symbolCount,
|
||||
totalSymbols,
|
||||
full,
|
||||
empty,
|
||||
half,
|
||||
supportsHalf,
|
||||
isFullOnly,
|
||||
denominator,
|
||||
newSymbols
|
||||
});
|
||||
};
|
||||
|
||||
return newSymbols;
|
||||
}
|
||||
25
src/utils/generateSymbolsStringForDisk.ts
Normal file
25
src/utils/generateSymbolsStringForDisk.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { SymbolSet } from '../types';
|
||||
import { isFullOnlySymbol } from './isFullOnlySymbol';
|
||||
import { generateSymbolsString } from './generateSymbolsString';
|
||||
|
||||
/**
|
||||
* Generate a string of rating symbols for writing to disk (full-only symbols only include rated ones)
|
||||
*/
|
||||
export function generateSymbolsStringForDisk(
|
||||
rating: number,
|
||||
symbolCount: number,
|
||||
full: string,
|
||||
empty: string,
|
||||
half: string,
|
||||
supportsHalf: boolean,
|
||||
symbolSet?: SymbolSet
|
||||
): string {
|
||||
// For full-only symbols, only write the rated symbols
|
||||
if (symbolSet && isFullOnlySymbol(symbolSet)) {
|
||||
const ratedCount = Math.floor(rating);
|
||||
return full.repeat(ratedCount);
|
||||
}
|
||||
|
||||
// For regular symbols, use the standard logic
|
||||
return generateSymbolsString(rating, symbolCount, full, empty, half, supportsHalf, symbolSet);
|
||||
}
|
||||
8
src/utils/getUnicodeCharLength.ts
Normal file
8
src/utils/getUnicodeCharLength.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Get the length of a string in Unicode grapheme clusters
|
||||
* This properly handles ZWJ sequences, variation selectors, and complex emoji
|
||||
*/
|
||||
export function getUnicodeCharLength(str: string): number {
|
||||
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
||||
return Array.from(segmenter.segment(str)).length;
|
||||
}
|
||||
8
src/utils/getUnicodeSubstring.ts
Normal file
8
src/utils/getUnicodeSubstring.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Get a substring with proper Unicode grapheme cluster handling
|
||||
* This properly handles ZWJ sequences, variation selectors, and complex emoji
|
||||
*/
|
||||
export function getUnicodeSubstring(str: string, start: number, end: number): string {
|
||||
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
||||
return Array.from(segmenter.segment(str), segment => segment.segment).slice(start, end).join('');
|
||||
}
|
||||
8
src/utils/isFullOnlySymbol.ts
Normal file
8
src/utils/isFullOnlySymbol.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { SymbolSet } from '../types';
|
||||
|
||||
/**
|
||||
* Check if a symbol set is full-only (same symbol for full and empty, no half)
|
||||
*/
|
||||
export function isFullOnlySymbol(symbolSet: SymbolSet): boolean {
|
||||
return symbolSet.full === symbolSet.empty && !symbolSet.half;
|
||||
}
|
||||
43
src/utils/updateSymbolPatternsFromSettings.ts
Normal file
43
src/utils/updateSymbolPatternsFromSettings.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { LOGGING_ENABLED, BASE_SYMBOL_PATTERNS, updateSymbolPatterns } from '../constants';
|
||||
import { InteractiveRatingsSettings } from '../types';
|
||||
|
||||
/**
|
||||
* Update symbol patterns based on user settings
|
||||
*/
|
||||
export function updateSymbolPatternsFromSettings(settings: InteractiveRatingsSettings): void {
|
||||
// Start with base patterns (all the existing ones except the customizable emojis)
|
||||
const newPatterns = [...BASE_SYMBOL_PATTERNS];
|
||||
|
||||
// Add emoji patterns from settings
|
||||
if (settings.supportedEmojis) {
|
||||
// Split the emoji string into individual grapheme clusters (handling ZWJ sequences properly)
|
||||
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
||||
const emojis = Array.from(segmenter.segment(settings.supportedEmojis), segment => segment.segment);
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.log('[InteractiveRatings] Parsed emojis from settings:', emojis);
|
||||
}
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (emoji.trim()) { // Skip empty characters and whitespace
|
||||
newPatterns.push({
|
||||
full: emoji,
|
||||
empty: emoji,
|
||||
half: null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the global symbol patterns
|
||||
updateSymbolPatterns(newPatterns);
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
||||
console.debug('[InteractiveRatings] Updated symbol patterns from settings', {
|
||||
supportedEmojis: settings.supportedEmojis,
|
||||
parsedEmojis: settings.supportedEmojis ? Array.from(segmenter.segment(settings.supportedEmojis), segment => segment.segment) : [],
|
||||
totalPatterns: newPatterns.length
|
||||
});
|
||||
}
|
||||
}
|
||||
11
src/utils/utf16ToUnicodePosition.ts
Normal file
11
src/utils/utf16ToUnicodePosition.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { getUnicodeCharLength } from './getUnicodeCharLength';
|
||||
|
||||
/**
|
||||
* Convert UTF-16 byte position to Unicode character position
|
||||
*/
|
||||
export function utf16ToUnicodePosition(str: string, utf16Position: number): number {
|
||||
// Get the substring up to the UTF-16 position
|
||||
const utf16Substring = str.substring(0, utf16Position);
|
||||
// Return the Unicode character length of that substring
|
||||
return getUnicodeCharLength(utf16Substring);
|
||||
}
|
||||
254
styles.css
254
styles.css
|
|
@ -1,26 +1,250 @@
|
|||
.interactive-ratings-container {
|
||||
display: inline-block;
|
||||
/* Interactive Ratings Plugin - Editor Mode Only */
|
||||
|
||||
/* Editor Widget Styles - For Editing Mode Only */
|
||||
.interactive-rating-editor-widget {
|
||||
display: inline;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.interactive-rating-editor-widget:hover {
|
||||
/* No visual changes on hover */
|
||||
}
|
||||
|
||||
/* Editor widget symbols container */
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols {
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.interactive-ratings-symbol {
|
||||
display: inline-block;
|
||||
transition: transform 0.1s ease;
|
||||
/* Editor widget rating text */
|
||||
.interactive-rating-editor-widget .interactive-rating-text {
|
||||
display: inline;
|
||||
cursor: default;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.interactive-ratings-editor-overlay {
|
||||
display: inline-block;
|
||||
/* Base symbol styling */
|
||||
.interactive-rating-symbol {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
touch-action: none; /* Prevent browser handling of touch gestures */
|
||||
-webkit-user-select: none; /* Prevent text selection on iOS */
|
||||
user-select: none; /* Prevent text selection */
|
||||
-webkit-touch-callout: none; /* Disable callout on long press */
|
||||
display: inline-block;
|
||||
transition: opacity 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
/* Make touch targets larger on mobile */
|
||||
/*
|
||||
* THEMING HOOKS - Symbol State Classes
|
||||
* ===================================
|
||||
* The following CSS classes are provided as theming hooks for customizing
|
||||
* the appearance of rating symbols. No default visual styling is applied
|
||||
* to maintain theme neutrality. Add your own styles to customize appearance.
|
||||
*
|
||||
* For comprehensive styling examples and documentation, see CSS-CUSTOMIZATION.md
|
||||
*/
|
||||
|
||||
/*
|
||||
* Filled/Active symbol state (★ in regular systems, active ★ in full-only systems)
|
||||
* Applied to: symbols representing the filled portion of a rating
|
||||
* Use for: styling fully selected symbols (e.g., color, opacity, effects)
|
||||
*/
|
||||
.interactive-rating-symbol--rated {
|
||||
/* Add your custom styles here */
|
||||
}
|
||||
|
||||
/*
|
||||
* Unrated symbol state (★ beyond rating in full-only systems)
|
||||
* Applied to: symbols in full-only systems that are beyond the current rating
|
||||
* Use for: styling inactive symbols in systems that only have one symbol type
|
||||
* Note: This is different from 'empty' - these are the same symbol but inactive
|
||||
*/
|
||||
.interactive-rating-symbol--unrated {
|
||||
/* Add your custom styles here */
|
||||
}
|
||||
|
||||
/*
|
||||
* Normal symbol state (legacy compatibility)
|
||||
* Applied to: symbols in legacy mode (deprecated)
|
||||
* Note: This class is maintained for backward compatibility only
|
||||
*/
|
||||
.interactive-rating-symbol--normal {
|
||||
/* Add your custom styles here */
|
||||
}
|
||||
|
||||
/*
|
||||
* Empty symbol state (☆ in regular rating systems)
|
||||
* Applied to: empty/outline symbols representing unselected portions of rating
|
||||
* Use for: styling empty symbols in systems with distinct filled/empty symbols
|
||||
* Note: This is different from 'unrated' - these are different symbol characters
|
||||
*/
|
||||
.interactive-rating-symbol--empty {
|
||||
/* Add your custom styles here */
|
||||
}
|
||||
|
||||
/*
|
||||
* Half symbol state (☆★ or similar in regular rating systems)
|
||||
* Applied to: half-filled symbols representing partial ratings
|
||||
* Use for: styling half-symbols (e.g., gradients, special colors, opacity)
|
||||
* Common styling: gradients to show half-fill effect
|
||||
*/
|
||||
.interactive-rating-symbol--half {
|
||||
/* Add your custom styles here */
|
||||
}
|
||||
|
||||
/* Individual symbol spans in editor widget with half-symbol support */
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols span {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
transition: opacity 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
/* Enhanced hover effect for symbols with half-symbol visual feedback */
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols span:hover {
|
||||
/* No visual changes on hover */
|
||||
}
|
||||
|
||||
/* Half-symbol visual feedback - subtle indicator for symbols that support halves */
|
||||
.interactive-rating-editor-widget[data-supports-half="true"] .interactive-rating-symbols span {
|
||||
/* No visual changes */
|
||||
}
|
||||
|
||||
.interactive-rating-editor-widget[data-supports-half="true"] .interactive-rating-symbols span:hover {
|
||||
/* No visual changes */
|
||||
}
|
||||
|
||||
/* Visual hint for half-symbol positioning */
|
||||
.interactive-rating-editor-widget[data-supports-half="true"] .interactive-rating-symbols span::before {
|
||||
/* No visual elements */
|
||||
}
|
||||
|
||||
/* Show the half-symbol divider on hover when half symbols are supported */
|
||||
.interactive-rating-editor-widget[data-supports-half="true"]:hover .interactive-rating-symbols span::before {
|
||||
/* No visual elements */
|
||||
}
|
||||
|
||||
/* Additional visual enhancement for half-symbol interactions */
|
||||
.interactive-rating-editor-widget[data-supports-half="true"] .interactive-rating-symbols span:hover::after {
|
||||
/* No visual elements */
|
||||
}
|
||||
|
||||
/*
|
||||
* FULL-ONLY SYMBOL SYSTEM THEMING HOOKS
|
||||
* =====================================
|
||||
* The following selectors target symbols in full-only rating systems specifically.
|
||||
* These provide more granular control when you want different styling for symbols
|
||||
* in full-only systems (which only have one symbol type) vs regular systems.
|
||||
* No default styling is applied - add your own styles as needed.
|
||||
*/
|
||||
|
||||
/* Full-only symbol styles - structural transitions only */
|
||||
.interactive-rating-editor-widget[data-full-only="true"] .interactive-rating-symbols span {
|
||||
transition: opacity 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
/*
|
||||
* Full-only unrated symbols (★ beyond rating in full-only systems)
|
||||
* Applied to: symbols beyond the current rating in systems with only one symbol type
|
||||
* Use for: custom styling of inactive symbols in full-only systems
|
||||
* Default styling: Essential for functionality - without this, full-only ratings would be unreadable
|
||||
*/
|
||||
.interactive-rating-editor-widget[data-full-only="true"] .interactive-rating-symbol--unrated {
|
||||
opacity: 0.5 !important;
|
||||
filter: grayscale(100%) !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Full-only rated symbols (★ within rating in full-only systems)
|
||||
* Applied to: symbols within the current rating in systems with only one symbol type
|
||||
* Use for: custom styling of active symbols in full-only systems
|
||||
* Default styling: Ensures rated symbols remain fully visible
|
||||
*/
|
||||
.interactive-rating-editor-widget[data-full-only="true"] .interactive-rating-symbol--rated {
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
/* Support for display symbol count different from pattern length */
|
||||
.interactive-rating-editor-widget[data-display-symbol-count] .interactive-rating-symbols {
|
||||
/* Styles for symbols with extended display count */
|
||||
}
|
||||
|
||||
/* Mobile/touch enhancements for editor */
|
||||
@media (max-width: 768px) {
|
||||
.interactive-ratings-symbol {
|
||||
min-height: 24px;
|
||||
padding: 2px;
|
||||
.interactive-rating-editor-widget {
|
||||
padding: 0;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast accessibility for editor */
|
||||
@media (prefers-contrast: high) {
|
||||
.interactive-rating-editor-widget:hover {
|
||||
/* No visual changes */
|
||||
}
|
||||
|
||||
/* Enhanced visibility for half-symbol indicators in high contrast */
|
||||
.interactive-rating-editor-widget[data-supports-half="true"] .interactive-rating-symbols span::before {
|
||||
/* No visual elements */
|
||||
}
|
||||
|
||||
.interactive-rating-editor-widget[data-supports-half="true"]:hover .interactive-rating-symbols span::before {
|
||||
/* No visual elements */
|
||||
}
|
||||
|
||||
/* Enhanced contrast for full-only symbols using class-based approach */
|
||||
.interactive-rating-editor-widget[data-full-only="true"] .interactive-rating-symbol--unrated {
|
||||
opacity: 0.3 !important;
|
||||
filter: grayscale(100%) contrast(0.7) !important;
|
||||
}
|
||||
|
||||
/* Enhanced contrast for empty symbols in regular rating systems */
|
||||
.interactive-rating-symbol--empty {
|
||||
opacity: 0.3 !important;
|
||||
filter: grayscale(100%) contrast(0.7) !important;
|
||||
}
|
||||
|
||||
/* Enhanced contrast for half symbols in regular rating systems */
|
||||
.interactive-rating-symbol--half {
|
||||
opacity: 0.6 !important;
|
||||
filter: contrast(1.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.interactive-rating-editor-widget,
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols span,
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols span::before,
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols span::after,
|
||||
.interactive-rating-symbol {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for rating transitions */
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols {
|
||||
/* No transitions */
|
||||
}
|
||||
|
||||
.interactive-rating-editor-widget .interactive-rating-text {
|
||||
/* No transitions */
|
||||
}
|
||||
|
||||
/* Smooth symbol transitions during preview */
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols span {
|
||||
/* Transitions defined above */
|
||||
}
|
||||
Loading…
Reference in a new issue