mirror of
https://github.com/peritus/obsidian-interactive-ratings.git
synced 2026-07-22 12:20:31 +00:00
Compare commits
No commits in common. "main" and "1.0.4" have entirely different histories.
49 changed files with 728 additions and 1953 deletions
|
|
@ -1,44 +0,0 @@
|
|||
[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,51 +1,19 @@
|
|||
[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+))?"
|
||||
|
||||
# Release serialization - always removes preview suffix
|
||||
serialize = [
|
||||
"{major}.{minor}.{patch}" # Final releases: 0.9.1
|
||||
]
|
||||
|
||||
current_version = "1.0.4"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = "{new_version}"
|
||||
tag_message = "Release {new_version}"
|
||||
tag_message = "Version {new_version}"
|
||||
message = "Bump version: {current_version} → {new_version}"
|
||||
regex = true
|
||||
|
||||
# RELEASE CONFIG: Update package.json, manifest.json, AND sync preview config
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "manifest.json"
|
||||
search = '"version": "{current_version}"'
|
||||
replace = '"version": "{new_version}"'
|
||||
|
||||
[[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 @@
|
|||
main.js
|
||||
*.js
|
||||
node_modules/
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
# 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,42 +2,19 @@
|
|||
|
||||
Add interactive rating symbols to your notes that update with a click.
|
||||
|
||||
<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
|
||||
|
||||
<img alt="demo of the Interactive Ratings Plugin for Obsidian" src="screencast.gif" width="400" />
|
||||
|
||||
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:
|
||||
|
|
@ -50,33 +27,24 @@ Add numerical ratings with these formats:
|
|||
|
||||
The numerical rating updates automatically when you change the symbols.
|
||||
|
||||
### 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
|
||||
## Supported Symbol Sets
|
||||
|
||||
| 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
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ paths_blocked = [
|
|||
build = ["npm", "run", "build"]
|
||||
|
||||
# Debug build
|
||||
build_debug = ["npm", "run", "build:debug"]
|
||||
build_debug = ["npm", "run", "build-debug"]
|
||||
|
||||
# Type checking
|
||||
type_check = ["npm", "run", "type-check"]
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const buildOptions = {
|
|||
format: 'cjs',
|
||||
banner: { js: banner },
|
||||
define: {
|
||||
'process.env.LOGGING_ENABLED': loggingEnabled ? '"true"' : '"false"'
|
||||
'process.env.LOGGING_ENABLED': JSON.stringify(loggingEnabled)
|
||||
},
|
||||
external: [
|
||||
'obsidian',
|
||||
|
|
|
|||
146
install-dev.js
146
install-dev.js
|
|
@ -1,146 +0,0 @@
|
|||
#!/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.8",
|
||||
"version": "1.0.4",
|
||||
"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.7",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-interactive-ratings",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.1",
|
||||
|
|
|
|||
17
package.json
17
package.json
|
|
@ -1,24 +1,13 @@
|
|||
{
|
||||
"name": "obsidian-interactive-ratings",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.4",
|
||||
"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-debug": "LOGGING_ENABLED=true node esbuild.config.js",
|
||||
"build:watch": "node esbuild.config.js --watch",
|
||||
"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"
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidianmd"
|
||||
|
|
|
|||
|
|
@ -1,30 +1,16 @@
|
|||
import { Plugin } from 'obsidian';
|
||||
import { LOGGING_ENABLED } from './constants';
|
||||
import { ratingEditorExtension } from './editor-extension/ratingViewPlugin/ratingViewPlugin';
|
||||
import { InteractiveRatingsSettings } from './types';
|
||||
import { DEFAULT_SETTINGS, InteractiveRatingsSettingTab } from './settings';
|
||||
import { updateSymbolPatternsFromSettings } from './utils/updateSymbolPatternsFromSettings';
|
||||
import { ratingEditorExtension } from './editor-extension';
|
||||
|
||||
export class InteractiveRatingsPlugin extends Plugin {
|
||||
settings: InteractiveRatingsSettings;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info('[InteractiveRatings] Plugin loading - edit mode only');
|
||||
}
|
||||
|
||||
// Load settings
|
||||
await this.loadSettings();
|
||||
|
||||
// Update symbol patterns based on settings
|
||||
this.updateSymbolPatterns();
|
||||
|
||||
// Register editor extension for interactive ratings in editing mode only
|
||||
this.registerEditorExtension(ratingEditorExtension);
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new InteractiveRatingsSettingTab(this.app, this));
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info('[InteractiveRatings] Plugin loaded successfully');
|
||||
}
|
||||
|
|
@ -35,16 +21,4 @@ export class InteractiveRatingsPlugin extends Plugin {
|
|||
console.info('[InteractiveRatings] Plugin unloaded');
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
updateSymbolPatterns(): void {
|
||||
updateSymbolPatternsFromSettings(this.settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,16 @@
|
|||
import { SymbolSet } from './types';
|
||||
|
||||
// ============================================================================
|
||||
// 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.
|
||||
// ============================================================================
|
||||
// Global logging control - set at build time via environment variable
|
||||
export const LOGGING_ENABLED = process.env.LOGGING_ENABLED === 'true';
|
||||
|
||||
// Base symbol patterns (excluding user-configurable emojis)
|
||||
export const BASE_SYMBOL_PATTERNS: SymbolSet[] = [
|
||||
// Define symbol patterns as a global constant
|
||||
export const 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
|
||||
|
|
@ -54,20 +24,6 @@ export const BASE_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
|
||||
424
src/editor-extension.ts
Normal file
424
src/editor-extension.ts
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
import { EditorView, Decoration, DecorationSet, ViewPlugin, ViewUpdate, WidgetType } from "@codemirror/view";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { generateSymbolRegexPatterns, getSymbolSetForPattern, calculateRating, parseRatingText } from './ratings-parser';
|
||||
import { generateSymbolsString, formatRatingText } from './utils';
|
||||
import { LOGGING_ENABLED } from './constants';
|
||||
import { RatingText } from './types';
|
||||
|
||||
/**
|
||||
* Interface for rating match data with optional rating text
|
||||
*/
|
||||
interface RatingMatch {
|
||||
pattern: string;
|
||||
start: number;
|
||||
end: number;
|
||||
symbolSet: any;
|
||||
rating: number;
|
||||
ratingText?: RatingText | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rating widget for inline editing in CodeMirror
|
||||
* Handles both symbols and rating text with full half-symbol support
|
||||
*/
|
||||
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());
|
||||
container.setAttribute('data-pattern-length', this.pattern.length.toString());
|
||||
container.setAttribute('data-supports-half', (!!this.symbolSet.half).toString());
|
||||
|
||||
// Create symbols container
|
||||
const symbolsContainer = document.createElement('span');
|
||||
symbolsContainer.className = 'interactive-rating-symbols';
|
||||
|
||||
// Create clickable symbols
|
||||
[...this.pattern].forEach((symbol, index) => {
|
||||
const span = document.createElement('span');
|
||||
span.textContent = symbol;
|
||||
span.style.cursor = 'pointer';
|
||||
span.style.position = 'relative';
|
||||
span.setAttribute('data-symbol-index', index.toString());
|
||||
|
||||
// Add click handler with half-symbol support
|
||||
span.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const newRating = this.calculateRatingFromClick(e, span, index);
|
||||
this.updateRating(view, newRating);
|
||||
});
|
||||
|
||||
// Add hover preview with half-symbol support
|
||||
span.addEventListener('mouseenter', (e) => {
|
||||
const previewRating = this.calculateRatingFromHover(e, span, index);
|
||||
this.previewRating(previewRating, container);
|
||||
});
|
||||
|
||||
// Add mousemove for fine-grained half-symbol preview
|
||||
span.addEventListener('mousemove', (e) => {
|
||||
const previewRating = this.calculateRatingFromHover(e, span, index);
|
||||
this.previewRating(previewRating, container);
|
||||
});
|
||||
|
||||
symbolsContainer.appendChild(span);
|
||||
});
|
||||
|
||||
container.appendChild(symbolsContainer);
|
||||
|
||||
// Create rating text container if rating text exists
|
||||
if (this.ratingText) {
|
||||
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', () => {
|
||||
this.renderRating(this.rating, container);
|
||||
});
|
||||
|
||||
// Initial render
|
||||
this.renderRating(this.rating, container);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate rating from click position with half-symbol support
|
||||
*/
|
||||
private calculateRatingFromClick(event: MouseEvent, span: HTMLElement, symbolIndex: number): number {
|
||||
if (!this.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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate rating from hover position with half-symbol support
|
||||
*/
|
||||
private calculateRatingFromHover(event: MouseEvent, span: HTMLElement, symbolIndex: number): number {
|
||||
if (!this.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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview rating with proper half-symbol rendering
|
||||
*/
|
||||
private previewRating(newRating: number, container: HTMLElement): void {
|
||||
const symbolsContainer = container.querySelector('.interactive-rating-symbols');
|
||||
if (!symbolsContainer) return;
|
||||
|
||||
const spans = symbolsContainer.querySelectorAll('span');
|
||||
spans.forEach((span, index) => {
|
||||
const symbolRating = index + 1;
|
||||
const halfRating = index + 0.5;
|
||||
|
||||
if (symbolRating <= newRating) {
|
||||
// Full symbol
|
||||
span.textContent = this.symbolSet.full;
|
||||
} else if (this.symbolSet.half && halfRating <= newRating && halfRating > newRating - 0.5) {
|
||||
// Half symbol
|
||||
span.textContent = this.symbolSet.half;
|
||||
} else {
|
||||
// Empty symbol
|
||||
span.textContent = this.symbolSet.empty;
|
||||
}
|
||||
});
|
||||
|
||||
// Preview rating text if it exists
|
||||
if (this.ratingText) {
|
||||
const textContainer = container.querySelector('.interactive-rating-text');
|
||||
if (textContainer) {
|
||||
// Use the current pattern length as the denominator for preview
|
||||
const previewText = formatRatingText(
|
||||
this.ratingText.format,
|
||||
newRating,
|
||||
this.pattern.length,
|
||||
this.pattern.length, // Use symbol count as denominator
|
||||
!!this.symbolSet.half
|
||||
);
|
||||
textContainer.textContent = previewText;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug('[InteractiveRatings] Preview rating with half-symbol support', {
|
||||
newRating,
|
||||
hasHalf: !!this.symbolSet.half,
|
||||
symbolSet: this.symbolSet,
|
||||
denominator: this.pattern.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render rating with proper half-symbol display
|
||||
*/
|
||||
private renderRating(rating: number, container: HTMLElement): void {
|
||||
const symbolsContainer = container.querySelector('.interactive-rating-symbols');
|
||||
if (!symbolsContainer) return;
|
||||
|
||||
const spans = symbolsContainer.querySelectorAll('span');
|
||||
spans.forEach((span, index) => {
|
||||
const symbolRating = index + 1;
|
||||
const halfRating = index + 0.5;
|
||||
|
||||
if (symbolRating <= rating) {
|
||||
// Full symbol
|
||||
span.textContent = this.symbolSet.full;
|
||||
} else if (this.symbolSet.half && halfRating <= rating && halfRating > rating - 0.5) {
|
||||
// Half symbol
|
||||
span.textContent = this.symbolSet.half;
|
||||
} else {
|
||||
// Empty symbol
|
||||
span.textContent = this.symbolSet.empty;
|
||||
}
|
||||
});
|
||||
|
||||
// Restore original rating text
|
||||
if (this.ratingText) {
|
||||
const textContainer = container.querySelector('.interactive-rating-text');
|
||||
if (textContainer) {
|
||||
textContainer.textContent = this.ratingText.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.debug('[InteractiveRatings] Render rating with half-symbol support', {
|
||||
rating,
|
||||
hasHalf: !!this.symbolSet.half,
|
||||
symbolSet: this.symbolSet
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update rating in the document with half-symbol support
|
||||
*/
|
||||
private updateRating(view: EditorView, newRating: number): void {
|
||||
try {
|
||||
// Generate new symbol string with half-symbol support
|
||||
const newSymbols = generateSymbolsString(
|
||||
newRating,
|
||||
this.pattern.length,
|
||||
this.symbolSet.full,
|
||||
this.symbolSet.empty,
|
||||
this.symbolSet.half || '',
|
||||
!!this.symbolSet.half
|
||||
);
|
||||
|
||||
// Generate new rating text if it exists
|
||||
let newText = newSymbols;
|
||||
if (this.ratingText) {
|
||||
// Use the current pattern length as the denominator for final update
|
||||
const newRatingText = formatRatingText(
|
||||
this.ratingText.format,
|
||||
newRating,
|
||||
this.pattern.length,
|
||||
this.pattern.length, // Use symbol count as denominator
|
||||
!!this.symbolSet.half
|
||||
);
|
||||
newText = newSymbols + newRatingText;
|
||||
}
|
||||
|
||||
// Update the document (replace both symbols and rating text)
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: this.startPos,
|
||||
to: this.endPos,
|
||||
insert: newText
|
||||
}
|
||||
});
|
||||
|
||||
if (LOGGING_ENABLED) {
|
||||
console.info('[InteractiveRatings] Rating updated in editor with half-symbol support', {
|
||||
oldRating: this.rating,
|
||||
newRating,
|
||||
newSymbols,
|
||||
newText,
|
||||
hasRatingText: !!this.ratingText,
|
||||
hasHalf: !!this.symbolSet.half,
|
||||
oldDenominator: this.ratingText?.denominator,
|
||||
newDenominator: this.pattern.length,
|
||||
position: { from: this.startPos, to: this.endPos }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (LOGGING_ENABLED) {
|
||||
console.error('[InteractiveRatings] Error updating rating', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewPlugin to detect and replace rating patterns with interactive widgets
|
||||
*/
|
||||
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: 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;
|
||||
|
||||
// Skip if too short (minimum 3 symbols)
|
||||
if (pattern.length < 3) continue;
|
||||
|
||||
const symbolSet = getSymbolSetForPattern(pattern);
|
||||
if (!symbolSet) continue;
|
||||
|
||||
const rating = calculateRating(pattern, symbolSet);
|
||||
|
||||
// Check for rating text after the symbols
|
||||
const ratingText = parseRatingText(text, start, end);
|
||||
const actualEnd = ratingText ? ratingText.endPosition : end;
|
||||
|
||||
matches.push({
|
||||
pattern,
|
||||
start,
|
||||
end: actualEnd,
|
||||
symbolSet,
|
||||
rating,
|
||||
ratingText
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort matches by start position (required by RangeSetBuilder)
|
||||
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 matches) {
|
||||
if (match.start >= lastEnd) {
|
||||
filteredMatches.push(match);
|
||||
lastEnd = match.end;
|
||||
}
|
||||
}
|
||||
|
||||
// Add decorations in sorted order, but skip if cursor is nearby
|
||||
for (const match of filteredMatches) {
|
||||
// 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 && filteredMatches.length > 0) {
|
||||
const skippedCount = filteredMatches.filter(m =>
|
||||
cursorPos >= m.start - 1 && cursorPos <= m.end + 1
|
||||
).length;
|
||||
console.debug(`[InteractiveRatings] Built ${filteredMatches.length - skippedCount}/${filteredMatches.length} rating decorations (${skippedCount} skipped due to cursor proximity)`, {
|
||||
cursorPos,
|
||||
withRatingText: filteredMatches.filter(m => m.ratingText).length,
|
||||
symbolsOnly: filteredMatches.filter(m => !m.ratingText).length,
|
||||
withHalfSymbols: filteredMatches.filter(m => m.symbolSet.half).length
|
||||
});
|
||||
}
|
||||
|
||||
} 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,11 +0,0 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* 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}`);
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
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];
|
||||
28
src/main.ts
28
src/main.ts
|
|
@ -2,30 +2,10 @@ import { InteractiveRatingsPlugin } from './InteractiveRatingsPlugin';
|
|||
|
||||
// Re-export from core modules
|
||||
export * from './constants';
|
||||
export * from './utils';
|
||||
export * from './types';
|
||||
|
||||
// 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 * from './ratings-parser';
|
||||
export * from './editor-extension';
|
||||
|
||||
// Export the main plugin class
|
||||
export default InteractiveRatingsPlugin;
|
||||
export default InteractiveRatingsPlugin;
|
||||
|
|
|
|||
100
src/ratings-parser.ts
Normal file
100
src/ratings-parser.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
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');
|
||||
});
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
/**
|
||||
* Escape special regex characters, handling Unicode properly
|
||||
*/
|
||||
export function escapeRegexChar(char: string): string {
|
||||
return char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
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');
|
||||
});
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
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 };
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,3 @@ export interface RatingText {
|
|||
text: string;
|
||||
endPosition: number;
|
||||
}
|
||||
|
||||
export interface InteractiveRatingsSettings {
|
||||
supportedEmojis: string;
|
||||
}
|
||||
149
src/utils.ts
Normal file
149
src/utils.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* 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('');
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
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);
|
||||
}
|
||||
133
styles.css
133
styles.css
|
|
@ -38,78 +38,11 @@
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
/* Base symbol styling */
|
||||
.interactive-rating-symbol {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
transition: opacity 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 */
|
||||
|
|
@ -141,47 +74,6 @@
|
|||
/* 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-rating-editor-widget {
|
||||
|
|
@ -204,24 +96,6 @@
|
|||
.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 */
|
||||
|
|
@ -229,9 +103,8 @@
|
|||
.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;
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols span::after {
|
||||
/* No transitions to remove */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,5 +119,5 @@
|
|||
|
||||
/* Smooth symbol transitions during preview */
|
||||
.interactive-rating-editor-widget .interactive-rating-symbols span {
|
||||
/* Transitions defined above */
|
||||
/* No transitions */
|
||||
}
|
||||
Loading…
Reference in a new issue