Compare commits

..

No commits in common. "master" and "1.6.0" have entirely different histories.

54 changed files with 782 additions and 2601 deletions

View file

@ -15,12 +15,12 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '20.x'
node-version: '18.x'
- name: Build plugin
run: |
yarn install --frozen-lockfile
yarn run build
npm install
npm run build
- name: Create release
env:

6
.gitignore vendored
View file

@ -31,9 +31,3 @@ tmp/
# localization compiled
src/localization/compiled
# test configuration with credentials
tests/test-config.local.json
# test configuration with credentials
tests/test-config.local.json

View file

@ -1,6 +1,5 @@
node check-version.js
yarn run format
yarn run lint:fix
yarn run test
yarn run build
yarn run validate_locale_once

View file

@ -1,118 +0,0 @@
---
name: jira-sync
description: >-
Project-specific knowledge for the Jira Issue Manager plugin.
Covers architecture, known bugs, commands, cache, localization,
field mapping, indicator system, build/test commands, and release process.
Use this skill when implementing features, fixing bugs, or
navigating the codebase.
license: MIT
compatibility: opencode
metadata:
plugin-id: jira-sync
display-name: Jira Issue Manager
entry: src/main.ts
---
# Jira Sync — Project-specific knowledge
## Architecture
- **Plugin ID**: `jira-sync`, display name: "Jira Issue Manager"
- **Entry**: `src/main.ts``JiraPlugin extends Plugin`
- **10 commands** registered via `register*Command` wrappers in `src/commands/`
- **Settings**: `src/settings/default.ts``JiraSettingsInterface` with connections, field mappings, cache
- **API layer**: `src/api/base.ts``baseRequest(plugin, method, path)` constructs `{jiraUrl}/rest/api/{version}{path}`
- **Multi-connection**: `settings.connections[]`, `currentConnectionIndex`
## Known bugs
- `src/tools/debugLogging.ts:1``process.env.NODE_ENV` is always `undefined` in Obsidian (no Node.js runtime). Logging is **always on** in both dev and production. Prefix: `[DEBUG]`, `[DEBUG WARNING]`, `[DEBUG ERROR]`.
## Directory structure
```
src/
├── main.ts # Plugin lifecycle, cache init, command/settings registration
├── api/ # Jira REST API (base, auth, issues, projects, self)
├── commands/ # 10 user-facing commands
├── modals/ # UI dialogs (search, JQL, worklog, project/type/status selectors)
├── settings/ # Settings tab + components (connection, field mapping, timekeep)
├── file_operations/ # File read/write for issue notes
├── tools/ # Utilities (logging, cache, files, field mapping, sanitizers, etc.)
├── postprocessing/ # Live Preview (CM6) + Reading mode hide-jira-markers
├── default/ # Default template, mock issue, built-in field mappings
├── interfaces/ # TypeScript types
└── localization/ # i18n (en/ru YAML sources → compiled JSON)
```
## Commands
| ID | Name |
| ------------------------------- | ---------------------------------------- |
| `get-issue-jira` | Get current issue from Jira |
| `get-issue-jira-key` | Get issue from Jira with custom key |
| `create-issue-jira` | Create issue in Jira |
| `update-issue-jira` | Update issue in Jira |
| `update-issue-status-jira` | Update issue status in Jira |
| `update-work-log-jira-manually` | Update work log manually |
| `update-work-log-jira-batch` | Update work log by batch |
| `batch-fetch-issues-jira` | Batch Fetch Issues by JQL |
| `add-comment-jira` | Add comment to Jira |
| `rebuild-issue-cache` | Rebuild issue file cache from filesystem |
## Cache system
- In-memory `Map<string, string>` in `JiraPlugin` instance
- Persisted in `settings.issueKeyToFilePathCache`
- Vault event listeners for maintenance: `rename` updates path, `delete` removes entry
- `rebuildCache()` scans issues folder, reads frontmatter `key` fields
## Localization
- Sources: `src/localization/source/{lang}/*.yaml` → compiled to `src/localization/compiled/{lang}.json`
- Locale auto-detected from `window.localStorage.getItem('language')`, falls back to `en`
- Build commands: `yarn run compile_locale_once`, `yarn run validate_locale_once`
## Field mapping system
- User-defined JS arrow-functions stored as strings in `fieldMappingsStrings`
- Validated via `acorn` parser, evaluated via `new Function()` with sandboxed context
- Helpers available: `jiraToMarkdown`, `markdownToJira`, `markdownToAdf`, `adfToMarkdown`, `JSON`, `Math`, `Date`
- Built-in mappings in `src/default/obsidianJiraFieldsMapping.ts`
## Indicator system
Three marker types in notes (hidden in both Live Preview and Reading mode):
- **Section**: `` `jira-sync-section-fieldname` `` — content until next heading
- **Line**: `` `jira-sync-line-fieldname` `` — content on same line
- **Inline**: `` `jira-sync-inline-start-fieldname` content `jira-sync-end` ``
## Build commands
```bash
yarn install # Install
yarn run dev # Watch mode
yarn run build # Production (compile_locale → tsc check → esbuild)
yarn run format # Prettier
yarn run lint:fix # ESLint autofix
yarn run test # Vitest unit tests
yarn run test:int # API integration tests
```
## Pre-commit hooks (husky)
Currently runs `node check-version.js` — validates version consistency across files.
## Versioning
- `yarn run version` bumps via `version-bump.mjs` (syncs `package.json``manifest.json` + `versions.json`)
- GitHub release tag = version string (no leading `v`)
- Attach: `main.js`, `manifest.json`, `styles.css` (if present)
## Testing
- `yarn run test` — Vitest, config at `tests/vitest.config.ts`
- `yarn run test:int` — API integration tests at `tests/__tests__/api/`
- Manual: copy `main.js`, `manifest.json`, `styles.css` to `<vault>/.obsidian/plugins/jira-sync/`

View file

@ -1,54 +0,0 @@
---
name: obsidian-plugin
description: >-
General Obsidian plugin development conventions — bundling with esbuild,
manifest structure, plugin lifecycle (onload/onunload), settings persistence,
and coding patterns for community plugins. Use when writing or modifying
any Obsidian plugin.
license: MIT
compatibility: opencode
---
# Obsidian Plugin Development
## Plugin anatomy
- `Plugin` class from `obsidian` package with `onload()` / `onunload()` lifecycle
- Entry: `src/main.ts`, output: `main.js` (bundled via esbuild)
- Required artifacts: `main.js`, `manifest.json`, optional `styles.css`
- Settings: `this.loadData()` / `this.saveData()` (JSON persisted by Obsidian)
## Bundling
- **esbuild**: bundles all source + runtime deps into single `main.js`
- Externals: `obsidian`, `electron`, `@codemirror/*`, `@lezer/*`, built-in Node modules
- Format: CJS, target: ES2018
- Sourcemaps in dev only
## Manifest (`manifest.json`)
```json
{
"id": "stable-id", // Never change after release
"name": "Display Name",
"version": "x.y.z", // SemVer
"minAppVersion": "1.x.x",
"description": "...",
"author": "...",
"isDesktopOnly": false
}
```
## Cleanup
- Use `this.register*` helpers (`registerEvent`, `registerDomEvent`, `registerInterval`, `registerEditorExtension`) for automatic cleanup on unload
- Never leak event listeners or intervals
## Code conventions
- TypeScript with `strict: true`
- `src/main.ts` minimal — delegate to modules
- `async/await` over promises
- No comments unless explicitly requested
- Bundle all deps — no unbundled runtime dependencies
- Prefer `checkCallback` for commands (cleaner UX)

301
AGENTS.md
View file

@ -1,52 +1,265 @@
# Jira Issue Manager — Agent guide
# Obsidian Jira Sync - Community plugin for Obsidian
## Project facts
## Project overview
- **Plugin ID**: `jira-sync`, display name: "Jira Issue Manager"
- **Entry**: `src/main.ts``main.js` (bundled via esbuild)
- **Package manager**: yarn (required)
- **Author**: Alamion, min App Version: 1.10.1, mobile-compatible
- **Plugin ID**: `jira-sync`
- **Name**: Jira Issue Manager
- **Target**: Obsidian Community Plugin (TypeScript → bundled JavaScript)
- **Entry point**: `main.ts` compiled to `main.js` and loaded by Obsidian
- **Required release artifacts**: `main.js`, `manifest.json`, and optional `styles.css`
- **Min App Version**: 1.10.1
- **Author**: Alamion
- **Mobile compatible**: Yes (`isDesktopOnly: false`)
## Known bugs
## Environment & tooling
- `src/tools/debugLogging.ts:1``process.env.NODE_ENV` is always `undefined` in Obsidian (no Node.js). Logging is **always on**. Uses `console.log('[DEBUG]', ...)` etc.
- Node.js: use current LTS (Node 18+ recommended)
- **Package manager: yarn** (required - `package.json` defines yarn scripts)
- **Bundler: esbuild** (required - `esbuild.config.mjs` and build scripts depend on it)
- Types: `obsidian` type definitions
## Quick reference
### Install
| Concern | Location |
| -------------- | -------------------------------------------------------------------------------------- |
| Commands | `src/commands/*.ts` — 10 commands, each in `register*Command(plugin)` |
| API requests | `src/api/base.ts``baseRequest(plugin, method, path)` |
| Settings | `src/settings/default.ts``JiraSettingsInterface`, persisted via `loadData/saveData` |
| Modals | `src/modals/*.ts` — search, JQL, worklog, project/type/status selectors |
| Cache | `Map<string, string>` in `JiraPlugin`, persisted in `settings.issueKeyToFilePathCache` |
| i18n | `src/localization/` — YAML sources → compiled JSON, auto-detect locale |
| Field mappings | JS arrow-function strings in `fieldMappingsStrings`, validated with `acorn` |
| View markers | `` `jira-sync-section-*` ``, `` `jira-sync-line-*` ``, `` `jira-sync-inline-*` `` |
## Important patterns
- Commands use `checkCallback` (except `add-comment-jira` which uses `editorCheckCallback`, and `rebuild-issue-cache` which uses `callback`)
- Always use `this.register*` helpers for listeners/events — never leak
- Use `this.getCurrentConnection()` to access active Jira connection
- Cache is maintained via vault `rename`/`delete` event listeners
- Migration check runs in `loadSettings()``checkMigrateSettings()` handles old single-connection → multi-connection migration
## Directory structure
```
src/
├── main.ts # lifecycle, cache, registration
├── api/ # Jira REST API layer
├── commands/ # user commands
├── modals/ # UI dialogs
├── settings/ # settings tab + components
├── file_operations/ # note read/write
├── tools/ # utilities
├── postprocessing/ # view rendering (Live Preview + Reading)
├── default/ # defaults (template, mock issue, field mappings)
├── interfaces/ # TypeScript types
└── localization/ # i18n
```bash
yarn install
```
For general Obsidian plugin development conventions and project-specific details, see `.opencode/skills/`.
### Dev (watch)
```bash
yarn run dev
```
### Production build
```bash
yarn run build
```
### Localization
```bash
yarn run validate_locale_once # Validate YAML source files
yarn run compile_locale_once # Compile YAML to JSON
```
## Linting
All three commands must pass without errors:
1. `yarn run format` - No Prettier errors
2. `yarn run lint:fix` - No ESLint errors
3. `yarn run build` - No TypeScript/build errors
4. `yarn run validate_locale_once` - No localization errors
## File & folder conventions
- **Source lives in `src/`**. Keep `main.ts` small and focused on plugin lifecycle.
- **Do not commit build artifacts**: Never commit `node_modules/`, `main.js`, or other generated files.
- Generated output should be placed at the plugin root. Release artifacts must end up at the top level of the plugin folder in the vault.
### Directory structure (`src/`)
| Directory | Purpose | Key Files |
| ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `commands/` | User commands | `getIssue.ts`, `createIssue.ts`, `updateIssue.ts`, `updateStatus.ts`, `addWorkLogBatch.ts`, `addWorkLogManually.ts`, `batchFetchIssues.ts` |
| `api/` | Jira REST API | `base.ts` (core HTTP), `auth.ts`, `issues.ts`, `projects.ts`, `self.ts` |
| `settings/` | Settings tab & components | `default.ts` (defaults), `JiraSettingTab.ts`, `components/*` |
| `modals/` | UI dialogs | `IssueSearchModal.ts`, `JQLSearchModal.ts`, `IssueWorkLogModal.ts`, `ProjectModal.ts`, `IssueTypeModal.ts`, `IssueStatusModal.ts` |
| `file_operations/` | File read/write | `getIssue.ts`, `createUpdateIssue.ts`, `commonPrepareData.ts` |
| `tools/` | Utilities | `debugLogging.ts`, `cacheUtils.ts`, `filesUtils.ts`, `convertFunctionString.ts`, `sanitizers.ts`, `asyncLimiter.ts` |
| `postprocessing/` | Live Preview/Reading | `livePreview.ts`, `reading.ts` |
| `default/` | Defaults | `defaultTemplate.ts`, `defaultIssue.ts`, `obsidianJiraFieldsMapping.ts` |
| `interfaces/` | TypeScript types | `settingsTypes.ts`, `index.ts` |
| `localization/` | i18n | `translator.ts`, `compiled/*.json`, `source/*.yaml` |
## Packages used
| Package | Purpose | Usage |
| -------------- | ----------------- | ------------------------------------------------------ |
| `obsidian` | Obsidian API | Core plugin, `requestUrl`, `TFile`, `Plugin` etc. |
| `esbuild` | Bundler | `esbuild.config.mjs` - bundles all deps into `main.js` |
| `typescript` | Type safety | TypeScript 4.7.4 |
| `fs-extra` | File operations | `src/tools/filesUtils.ts` |
| `yaml` | YAML parsing | Settings validation, field mapping config |
| `highlight.js` | Code highlighting | `src/tools/markdownHtml.ts` |
| `chokidar` | File watching | Localization compiler watch mode |
## Logging
- Location: `src/tools/debugLogging.ts:1-19`
- **IMPORTANT BUG**: Uses `process.env.NODE_ENV` which is always `undefined` in Obsidian (no Node.js runtime) - logging is **always on** in both dev and production
- Exports: `debugLog`, `debugWarn`, `debugError`
- Used in: `src/api/base.ts` for API request/response logging
- Prefix format: `[DEBUG]`, `[DEBUG WARNING]`, `[DEBUG ERROR]`
```typescript
import { debugLog, debugWarn, debugError } from './tools/debugLogging';
debugLog('Request:', { url, method });
debugWarn('Missing field:', fieldName);
debugError('Failed to fetch:', error);
```
## Cache system
- Maps Jira issue keys to local file paths
- In-memory cache: `issueKeyToFilePathCache` (Map in plugin instance)
- Persisted in settings: `settings.issueKeyToFilePathCache`
- Vault event listeners for maintenance (`main.ts:105-130`):
- `rename`: updates cache when file is moved
- `delete`: removes from cache when file is deleted
## Localization (i18n)
- Source files: `src/localization/source/{lang}/*.yaml`
- Compiled to: `src/localization/compiled/{lang}.json`
- Auto-detects locale from `window.localStorage.getItem('language')`
- Falls back to `en` if not found
- Build-time compilation: `src/localization/compiler.js`
## Pre-commit hooks
- Uses husky (`.husky/pre-commit`)
- Current check: `node check-version.js`
- `version-bump.mjs` - syncs version from `package.json` to `manifest.json` and `versions.json`
```bash
# Runs on every commit:
node check-version.js
```
- **Note**: Future pre-commit upgrades may include linting and type checking
## Manifest rules
- Must include:
- `id` (stable - never change after release)
- `name`
- `version` (Semantic Versioning x.y.z)
- `minAppVersion`
- `description`
- `isDesktopOnly` (boolean)
- Keep `minAppVersion` accurate when using newer APIs
## Testing
- Manual install for testing: copy `main.js`, `manifest.json`, `styles.css` to:
```
<Vault>/.obsidian/plugins/<plugin-id>/
```
- Reload Obsidian and enable in **Settings → Community plugins**
## Commands & settings
- Commands added via `this.addCommand(...)` in `src/commands/`
- Settings persisted with `this.loadData()` / `this.saveData()`
- Use stable command IDs - avoid renaming after release
- All commands wrapped in `register*Command` functions
## Versioning & releases
- Bump version in `manifest.json` (SemVer) and update `versions.json`
- Run `yarn version` to auto-bump via `version-bump.mjs`
- Create GitHub release with tag matching `manifest.json` version (no leading `v`)
- Attach: `manifest.json`, `main.js`, `styles.css` (if present)
## Security, privacy, and compliance
- Default to local/offline operation. Only make network requests when essential.
- No hidden telemetry.
- Never execute remote code, fetch and eval scripts.
- Minimize scope: read/write only what's necessary inside the vault.
- Use `register*` helpers for all listeners to ensure proper cleanup on unload.
## Performance
- Keep startup light - defer heavy work until needed
- Batch disk access, avoid excessive vault scans
- Debounce/throttle operations in response to file system events
## Coding conventions
- TypeScript with `"strict": true` preferred
- Keep `main.ts` minimal - delegate feature logic to separate modules
- Split large files (~200-300+ lines) into smaller focused modules
- Bundle everything into `main.js` (no unbundled runtime deps)
- Prefer `async/await` over promise chains
- Handle errors gracefully
- **Do not add comments unless explicitly requested**
## Agent do/don't
**Do**
- Add commands with stable IDs (don't rename after release)
- Provide defaults and validation in settings
- Write idempotent code paths
- Use `this.register*` helpers for cleanup
**Don't**
- Introduce network calls without user-facing reason and documentation
- Ship features requiring cloud services without opt-in
- Store or transmit vault contents unless essential
## Common tasks
### Add a command
```typescript
export function registerMyCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: 'my-command',
name: t('name'),
checkCallback: (checking: boolean) => {
const valid = validateSettings(plugin);
if (!checking && valid) doSomething(plugin);
return valid;
},
});
}
```
### Make API request
```typescript
import { baseRequest } from './api/base';
const issue = await baseRequest(plugin, 'GET', `/issue/${issueKey}`);
```
### Persist settings
```typescript
await this.saveData(this.settings);
```
### Register listeners safely
```typescript
this.registerEvent(
this.app.workspace.on('file-open', (f) => {
/* ... */
}),
);
this.registerDomEvent(window, 'resize', () => {
/* ... */
});
this.registerInterval(
window.setInterval(() => {
/* ... */
}, 1000),
);
```
## Troubleshooting
- Plugin doesn't load: ensure `main.js` and `manifest.json` at top level of plugin folder
- Build issues: run `yarn run dev` or `yarn run build` to compile
- Commands not appearing: verify `addCommand` runs after `onload` and IDs are unique
- Settings not persisting: ensure `loadData`/`saveData` are awaited
## References
- Obsidian plugin docs: https://docs.obsidian.md
- Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
- Style guide: https://help.obsidian.md/style-guide

View file

@ -1,64 +0,0 @@
# Contributing
## Development setup
```bash
yarn install
```
## Commands
```bash
yarn run dev # Watch mode (esbuild + localization)
yarn run build # Production build
yarn run format # Prettier formatting
yarn run lint:fix # ESLint autofix
yarn run test # Unit tests (Vitest)
yarn run test:int # API integration tests
yarn run validate_locale_once # Validate translation keys
yarn run compile_locale_once # Compile YAML -> JSON
```
## Guidelines
- **Source in `src/`**, keep `main.ts` focused on lifecycle
- **No comments** in code unless the logic is non-obvious
- **Bundle everything** — no unbundled runtime dependencies
- **Use `this.register*`** helpers for all listeners and intervals
- **Stable command IDs** — never rename after release
- **Follow existing patterns** — look at similar files first
- **Prefer `checkCallback`** for commands (cleaner UX)
- **Async/await** over promise chains
- **Handle errors gracefully** — no silent failures
## Pre-commit checks
The repo uses husky. Currently runs `node check-version.js` to validate version consistency across `package.json`, `manifest.json`, and `versions.json`.
## Versioning
Run `yarn run version` to bump — this syncs `package.json``manifest.json` + `versions.json` via `version-bump.mjs`. Semantic versioning (x.y.z).
## Release
1. Bump version
2. Build: `yarn run build`
3. Create GitHub release with tag matching `manifest.json` version (no leading `v`)
4. Attach: `main.js`, `manifest.json`, `styles.css` (if changed)
## Testing
- Unit tests: `yarn run test`
- Integration tests: `yarn run test:int` (requires a running Jira instance)
- Manual test: copy `main.js`, `manifest.json`, `styles.css` to `<vault>/.obsidian/plugins/jira-sync/` and reload Obsidian
## Translation / i18n
- Source files: `src/localization/source/{lang}/*.yaml` (one YAML per component)
- Compile: `yarn run compile_locale_once`
- Validate: `yarn run validate_locale_once` (checks all keys exist and are referenced in code)
- Locale is auto-detected from `window.localStorage.getItem('language')`, falls back to `en`
## Getting help
Open an issue on GitHub for bugs, feature requests, or questions.

182
README.md
View file

@ -1,52 +1,160 @@
# Jira Issue Manager
# Jira Sync Plugin for Obsidian
A two-way Jira synchronization plugin for Obsidian. Templates, custom field mapping, time tracking, and batch operations — all without leaving your notes.
Tired of Jira plugins that only give you basic issue links and descriptions? This one's different. It brings **your entire Jira workflow into Obsidian** with customizable templates, deep field mapping, and seamless two-way synchronization—including all those custom fields your team added over the years.
https://github.com/user-attachments/assets/55cb2c99-34a9-47fc-85f1-f79dba5a27b1
Originally forked from [obsidian-to-jira](https://github.com/angelperezasenjo/obsidian-to-jira), but now packed with features you won't find elsewhere.
## Features
## Why this plugin exists
- **Full two-way sync** — fetch issues from Jira, edit in Obsidian, push changes back
- **Custom templates** — structure your issue notes however you want using `jira-sync-section`, `jira-sync-line`, and `jira-sync-inline` markers that are hidden from view
- **Deep field mapping** — sync any Jira field, including custom fields from ScriptRunner, Insight, and other add-ons. Built-in mappings for 15+ standard fields, with support for custom JavaScript transform functions
- **Time tracking** — built-in work log statistics and batch submission. Supports Timekeep and Super Simple Time Tracker formats
- **Status management** — transition issues through workflows directly from Obsidian
- **Multiple authentication methods** — Bearer Token (PAT), Basic Auth, or Session Cookie
- **Multi-connection support** — work with multiple Jira instances
- **JQL search and batch fetch** — find and pull in issues by query
- **Inline comments** — add comments to Jira issues with text selection support
Other plugins treat Jira issues as read-only reference material. This one lets you:
![demo_image.png](docs/images/demo_image.png)
- **Build full-featured issue templates** that mirror your Jira workflow
- **Sync ANY field**—even custom ones from plugins like ScriptRunner or Insight
- **Create new fields** with dynamic field mapping (e.g. auto-generate browse URLs from `issue.self + issue.key` from API response)
- **Track time seamlessly** with integrated work log statistics and batch operations
- **Work offline** in Obsidian, then sync everything back to Jira when ready
![field_mapping_demo.png](docs/images/field_mapping_demo.png)
> **Pro Tip**: While TypeScript skills help for advanced mappings, we include ready-made mappings for 90% of basic use cases.
## Killer features
### 🧩 Your Jira, your template
Create Obsidian notes that look exactly like your team's Jira workflow. Pull in:
- Standard fields (status, assignee, priority)
- Custom fields (progress bars, sprint IDs, epic links)
- Plugin fields (ScriptRunner outputs, Insight assets)
- Inline indicators that work anywhere in your text
### Brief example:
#### Here is how template can look like:
```markdown
---
key: ""
status: "" <!-- Built-in field -->
priority: "" <!-- Another built-in field -->
sprint: "" <!-- Custom field -->
epic: "" <!-- Another Custom field -->
link: "" <!-- Built-in auto-generated link -->
---
### Customer Impact `jira-sync-section-customfield_10842` <!-- From your CRM plugin -->
### Other
User `jira-sync-inline-start-assignee``jira-sync-inline-end` should be working on this. <!-- Inline indicator, built-in -->
Expected time spent on the task: `jira-sync-line-originalEstimate` <!-- Line indicator, custom -->
```
#### Here is how it will look after syncing:
```markdown
---
key: JIR-1234
status: In Progress
priority: Medium
sprint: Mobile-Q2-24
epic: API-Overhaul
link: http://jira.local:8000/browse/JIR-1234
---
### Customer Impact `jira-sync-section-customfield_10842`
The current API structure is too fragmented and requires standardization.
### Other
User `jira-sync-inline-start-assignee`Jack A.M.`jira-sync-inline-end` should be working on this.
Expected time spent on the task: `jira-sync-line-originalEstimate`1w 3d
```
#### Here is how the user will see it most cases (all indicators are hidden from view):
```markdown
---
key: JIR-1234
status: In Progress
priority: Medium
sprint: Mobile-Q2-24
epic: API-Overhaul
link: http://jira.local:8000/browse/JIR-1234
---
### Customer Impact
The current API structure is too fragmented and requires standardization.
### Other
User Jack A.M. should be working on this.
Expected time spent on the task: 1w 3d
```
### 🔐 Multiple authentication methods
Choose the authentication that fits your security requirements:
- **Bearer Token (PAT)** - Personal Access Token for secure API access
- **Basic Auth (Username + PAT)** - Username with Personal Access Token
- **Session Cookie (Username + Password)** - Traditional authentication
> **Security Note**: When using PAT authentication, ensure `write:jira-work` and `read:jira-work` scopes are enabled.
### 🔄 Two-way sync that doesn't fight you
- **Smart conflict resolution** when notes change locally while syncing
- **Partial updates**—edit just the fields you care about
- **Worklog batching** push a week's worth of time entries at once
- **Status management** update issue status directly from Obsidian
### ⚙️ Field mapping kitchen
We include mappings for:
- Basic fields (like status, priority, assignee)
- Temporal fields (created, updated, due dates)
- Calculated fields (progress %, time estimates, custom formulas)
- **Bring your own** for custom integrations and complex transformations
### 📊 Integrated work log statistics
- **No external plugins required** - everything is built-in
- **Support for multiple formats** - works with Timekeep and Super Simple Time Tracker time tracking formats
- **Dynamic time period selection** - view stats for days, weeks, or months
- **Batch work log submission** - send multiple time entries at once
- **Visual progress tracking** - see your work patterns at a glance
## Quick start
1. Install from Community Plugins → search "Jira Issue Manager"
2. Open Settings → configure Jira URL and authentication
3. Use `Get issue from Jira with custom key` from the command palette
4. Edit your note — changes sync back with `Update issue in Jira`
1. **Install**: Community plugins → Search "Jira Issue Manager"
2. **Connect**: Settings → Choose authentication method + add your Jira URL + credentials
3. **Go**:
- `Get issue from Jira with custom key` from command palette
- Edit like any note using our flexible indicator system
- When you are ready → `Update issue in Jira` - changes sync back
## What makes us different
- **🎯 No external dependencies** - Statistics and work tracking built right in
- **🔧 Flexible indicators** - Use `section`, `line`, or `inline` patterns wherever you need them
- **📈 Real-time insights** - See your work patterns and productivity trends
- **🔄 True two-way sync** - Create, update, and manage issues entirely from Obsidian
- **⚡ Batch operations** - Handle multiple work logs and updates efficiently
## Documentation
- [English guide](docs/how_to_en.md)
- [Russian guide](docs/how_to_ru.md)
- [Template examples](docs/template_example.md)
For detailed setup, configuration, and advanced usage:
## Why this one?
- [English Guide](docs/how_to_en.md)
- [Russian Guide](docs/how_to_ru.md)
- [Template Examples](docs/template_example.md)
Most Jira plugins for Obsidian treat issues as read-only. This one is built for people who want to work _in_ Obsidian and push changes back to Jira. Key differentiators:
---
- True two-way synchronization (not just display)
- Support for arbitrary custom fields with programmable mapping
- Integrated time tracking and batch work log submission
- No external dependencies for statistics or time tracking
- Markers are hidden in both Live Preview and Reading mode
![Obsidian Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&label=downloads&query=$[%27jira-sync%27].downloads&url=https://raw.githubusercontent.com/obsidianmd/obsidian-releases/refs/heads/master/community-plugin-stats.json)
![GitHub release](https://img.shields.io/github/v/release/Alamion/obsidian-jira-sync)
![Platform](https://img.shields.io/badge/platform-desktop%20%7C%20mobile-blue)
## License
MIT — originally forked from [obsidian-to-jira](https://github.com/angelperezasenjo/obsidian-to-jira).
**Ready to transform your Jira workflow?** Install the plugin and start building your perfect issue management system in Obsidian today!

View file

@ -69,9 +69,9 @@ The responsible person for this task is `jira-sync-inline-start-assignee`Bob`jir
Example:
```md
The responsible person for this task is `jira-sync-block-start-assignee`
The responsible person for this task is `jira-sync-block-assignee`
Bob
`jira-sync-end`, and the description is `jira-sync-block-start-description`
`jira-sync-end`, and the description is `jira-sync-block-description`
Some description
`jira-sync-end`.
```

View file

@ -68,9 +68,9 @@
Пример:
```md
Ответственным за эту задачу является `jira-sync-block-start-assignee`
Ответственным за эту задачу является `jira-sync-block-assignee`
Bob
`jira-sync-end`, а описание — `jira-sync-block-start-description`
`jira-sync-end`, а описание — `jira-sync-block-description`
Некоторое описание
`jira-sync-end`.
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 887 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View file

@ -6,7 +6,7 @@ import tsPlugin from '@typescript-eslint/eslint-plugin';
export default defineConfig([
{
ignores: ['main.js', 'node_modules/**', 'src/**/*.js', 'tests/**'],
ignores: ['main.js', 'node_modules/**', 'src/**/*.js'],
},
{
files: ['**/*.ts'],

View file

@ -1,7 +1,7 @@
{
"id": "jira-sync",
"name": "Jira Issue Manager",
"version": "1.7.4",
"version": "1.6.0",
"minAppVersion": "1.10.1",
"description": "Get Jira issues, create and update them. Issue status and worklog management.",
"author": "Alamion",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-jira-sync",
"version": "1.7.4",
"version": "1.6.0",
"packageManager": "yarn@1.22.22",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
@ -15,9 +15,6 @@
"prepare": "husky",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run --config tests/vitest.config.ts",
"test:watch": "vitest --config tests/vitest.config.ts",
"test:int": "vitest run --config tests/vitest.config.ts tests/__tests__/api/",
"format": "prettier --write ."
},
"keywords": [],
@ -30,7 +27,7 @@
"@codemirror/view": "^6.38.1",
"@eslint/js": "^10.0.1",
"@types/lodash": "^4.17.16",
"@types/node": "^20.0.0",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"builtin-modules": "3.3.0",
@ -39,13 +36,11 @@
"eslint": "^10.2.1",
"globals": "^17.5.0",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lodash": "^4.17.23",
"obsidian": "latest",
"prettier": "^3.8.3",
"tslib": "2.4.0",
"typescript": "^6.0.3",
"vitest": "^4.1.9"
"typescript": "^6.0.3"
},
"dependencies": {
"acorn": "^8.14.1",

View file

@ -1,6 +1,10 @@
import { Notice, requestUrl } from 'obsidian';
import JiraPlugin from '../main';
export function getSessionCookieKey(plugin: JiraPlugin): string {
return 'jira-issue-managing-session-cookie-' + (plugin.app as any).appId;
}
// Session cookie only
export async function authenticate(plugin: JiraPlugin): Promise<boolean> {
try {
@ -25,8 +29,7 @@ export async function authenticate(plugin: JiraPlugin): Promise<boolean> {
},
});
conn.sessionCookie = response.json.session.value;
await plugin.saveSettings();
localStorage.setItem(getSessionCookieKey(plugin), response.json.session.value);
return true;
} catch (error: unknown) {
new Notice('Authentication failed: ' + ((error as Error).message || 'Unknown error'));
@ -84,10 +87,10 @@ export async function getAuthHeaders(plugin: JiraPlugin): Promise<Record<string,
};
} else if (conn.authMethod === 'session') {
// Option 3: Session cookie
let cookie = conn.sessionCookie;
let cookie = localStorage.getItem(getSessionCookieKey(plugin));
if (!cookie) {
await authenticate(plugin);
cookie = conn.sessionCookie;
cookie = localStorage.getItem(getSessionCookieKey(plugin));
}
if (cookie) {

View file

@ -6,5 +6,3 @@ export * from './createIssue';
export * from './getIssue';
export * from './updateIssue';
export * from './updateStatus';
export * from './rebuildCache';
export * from './rebuildCache';

View file

@ -1,13 +0,0 @@
import { Notice } from 'obsidian';
import JiraPlugin from '../main';
export function registerRebuildCacheCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: 'rebuild-issue-cache',
name: 'Rebuild issue file cache from filesystem',
callback: async () => {
await plugin.rebuildCache();
new Notice('Issue cache rebuilt successfully');
},
});
}

View file

@ -5,7 +5,6 @@ import { prepareJiraFieldsFromFile } from './commonPrepareData';
import { localToJiraFields, updateJiraToLocal } from '../tools/mapObsidianJiraFields';
import { JiraIssue, JiraTransitionType } from '../interfaces';
import { obsidianJiraFieldMappings } from '../default/obsidianJiraFieldsMapping';
import { updateJiraSyncContent } from '../tools/sectionTools';
export async function updateIssueFromFile(plugin: JiraPlugin, file: TFile): Promise<string> {
let fields = await prepareJiraFieldsFromFile(plugin, file);
@ -73,24 +72,8 @@ export async function updateStatusFromFile(
}
await updateJiraStatus(plugin, fields.key, transition.id);
// Only update the status field. Calling updateJiraToLocal with a partial issue
// causes all fromJira mappings to run with undefined inputs, producing empty strings
// that overwrite description and comments.
const allMappings = { ...obsidianJiraFieldMappings, ...plugin.settings.fieldMapping.fieldMappings };
const statusMapping = allMappings['status'];
const minimalIssue = { fields: { status: { name: transition.status } } } as JiraIssue;
const localStatusValue = statusMapping
? statusMapping.fromJira(minimalIssue, plugin.getCurrentConnection()?.apiVersion)
: transition.status;
if (localStatusValue !== null && localStatusValue !== undefined) {
await plugin.app.fileManager.processFrontMatter(file, (frontmatter: any) => {
frontmatter['status'] = localStatusValue;
});
await plugin.app.vault.process(file, (fileContent) => {
return updateJiraSyncContent(fileContent, { status: String(localStatusValue) });
});
}
await updateJiraToLocal(plugin, file, {
fields: { status: { name: transition.status } },
} as JiraIssue);
return fields.key;
}

View file

@ -7,18 +7,24 @@ import { Notice, TFile, TFolder } from 'obsidian';
import { defaultTemplate } from '../default/defaultTemplate';
import { debugLog } from '../tools/debugLogging';
export function generateFilenameFromTemplate(
template: string,
issue: { key: string; fields?: { summary?: string } },
): string {
function generateFilenameFromTemplate(template: string, issue: JiraIssue): string {
let filename = template;
// Replace {summary} with sanitized summary
const summary = issue.fields?.summary || '';
const sanitizedSummary = sanitizeFileName(summary);
filename = filename.replace(/\{summary\}/g, sanitizedSummary);
// Replace {key} with issue key
const key = issue.key || '';
filename = filename.replace(/\{key\}/g, key);
// Sanitize the entire filename to remove any prohibited characters
filename = sanitizeFileName(filename);
// Fallback if the result is empty or only whitespace
if (!filename || filename.trim() === '') {
// Use key if available, otherwise use a default
if (key) {
filename = key;
} else if (sanitizedSummary) {
@ -27,53 +33,10 @@ export function generateFilenameFromTemplate(
filename = 'jira-issue';
}
}
return filename.trim();
}
export async function renameExistingIssueFiles(plugin: JiraPlugin): Promise<{ renamed: number; errors: string[] }> {
const cacheMap = plugin.getAllIssueKeysMap();
const template = plugin.settings.fetchIssue.filenameTemplate || '{summary} ({key})';
const issuesFolder = plugin.settings.global.issuesFolder;
let renamed = 0;
const errors: string[] = [];
for (const [issueKey, currentPath] of cacheMap.entries()) {
try {
const file = plugin.app.vault.getFileByPath(currentPath);
if (!file) {
errors.push(`${issueKey}: file not found at ${currentPath}`);
continue;
}
let summary = '';
const metadata = plugin.app.metadataCache.getFileCache(file);
const cachedSummary = metadata?.frontmatter?.summary;
if (cachedSummary && typeof cachedSummary === 'string') {
summary = cachedSummary;
}
const newFilename = generateFilenameFromTemplate(template, { key: issueKey, fields: { summary } });
const newPath = `${issuesFolder}/${newFilename}.md`;
if (newPath === currentPath) continue;
const existingFile = plugin.app.vault.getFileByPath(newPath);
if (existingFile) {
errors.push(`${issueKey}: target path already exists: ${newPath}`);
continue;
}
await plugin.app.vault.rename(file, newPath);
await plugin.setFilePathForIssueKey(issueKey, newPath);
renamed++;
} catch (error) {
errors.push(`${issueKey}: ${(error as Error).message}`);
}
}
return { renamed, errors };
}
export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIssue, filePath?: string): Promise<void> {
try {
await ensureIssuesFolder(plugin);
@ -85,6 +48,7 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss
targetPath = filePath;
targetFile = plugin.app.vault.getFileByPath(filePath);
} else {
// First, try to find the file using cache
const cachedPath = plugin.getFilePathForIssueKey(issue.key);
if (cachedPath) {
targetFile = plugin.app.vault.getFileByPath(cachedPath);
@ -96,16 +60,19 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss
}
}
// If not found in cache or file doesn't exist, fallback to search
if (!targetFile) {
debugLog(`Issue ${issue.key} not in cache, searching filesystem...`);
targetFile = await findFileByIssueKey(plugin, issue.key);
if (targetFile) {
targetPath = targetFile.path;
await plugin.setFilePathForIssueKey(issue.key, targetPath);
// Add to cache for future use
plugin.setFilePathForIssueKey(issue.key, targetPath);
debugLog(`Found issue ${issue.key} via search, added to cache: ${targetPath}`);
}
}
// If still not found, create new file
if (!targetFile) {
const template = plugin.settings.fetchIssue.filenameTemplate || '{summary} ({key})';
const filename = generateFilenameFromTemplate(template, issue);
@ -117,12 +84,12 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss
if (targetFile) {
await updateJiraToLocal(plugin, targetFile, issue);
await plugin.app.workspace.openLinkText(targetFile.path, '');
await plugin.setFilePathForIssueKey(issue.key, targetFile.path);
} else {
const newFile = await createNewIssueFile(plugin, targetPath);
await updateJiraToLocal(plugin, newFile, issue);
await plugin.app.workspace.openLinkText(newFile.path, '');
await plugin.setFilePathForIssueKey(issue.key, newFile.path);
// Add new file to cache
plugin.setFilePathForIssueKey(issue.key, newFile.path);
}
new Notice(`Issue ${issue.key} imported successfully`);
} catch (error: unknown) {
@ -141,6 +108,7 @@ async function createNewIssueFile(plugin: JiraPlugin, filePath: string): Promise
if (templatePath && templatePath.trim() !== '') {
const templateFile = plugin.app.vault.getFileByPath(templatePath);
if (templateFile) {
// Use the template as initial content
initialContent = await plugin.app.vault.read(templateFile);
} else {
new Notice(`Template file not found: ${templatePath}, using default template`);
@ -148,8 +116,10 @@ async function createNewIssueFile(plugin: JiraPlugin, filePath: string): Promise
}
if (initialContent === '') initialContent = defaultTemplate;
// Create the file with initial content
await plugin.app.vault.create(filePath, initialContent);
// Get file reference and update frontmatter
const newFile = plugin.app.vault.getFileByPath(filePath);
if (!newFile) {
throw new Error('Could not create file');
@ -159,9 +129,11 @@ async function createNewIssueFile(plugin: JiraPlugin, filePath: string): Promise
async function findFileByIssueKey(plugin: JiraPlugin, issueKey: string): Promise<TFile | null> {
const issuesFolder = plugin.app.vault.getAbstractFileByPath(plugin.settings.global.issuesFolder);
if (!issuesFolder || !(issuesFolder instanceof TFolder)) {
return null;
}
return await searchFolderForIssueKey(plugin, issuesFolder, issueKey);
}

View file

@ -12,16 +12,6 @@ maxResults:
submit: Search Issues
presets:
load: JQL Preset
load_desc: Load a saved JQL query preset
select: Select preset
save: Save as preset
save_desc: Save current JQL query as a named preset
save_hint: Preset name
save_btn: Save
delete: Delete preset
preview:
total: Total {total} issues found
showing: ' (showing first {limit} issues)'

View file

@ -26,10 +26,3 @@ filenameTemplate:
desc: |
Template for created issue filenames. Use {summary} for issue summary and {key} for issue key
placeholder: '{summary} ({key})'
rename_files: Rename existing files
no_files_to_rename: No files in cache to rename
rename_confirm: 'This will rename {count} cached file(s). Continue?'
rename_success: Successfully renamed {count} file(s)
rename_errors: 'Failed to rename {count} file(s)'
rename_no_changes: No files needed renaming

View file

@ -12,18 +12,8 @@ maxResults:
submit: Найти задачи
presets:
load: JQL пресет
load_desc: Загрузить сохраненный JQL запрос
select: Выберите пресет
save: Сохранить как пресет
save_desc: Сохранить текущий JQL запрос как именованный пресет
save_hint: Имя пресета
save_btn: Сохранить
delete: Удалить пресет
preview:
total: '{total} задач найдено'
total: '{total} задач найдено'
showing: ' (показаны первые {limit} задач)'
error: '{error}'
loading: Загрузка...

View file

@ -21,10 +21,3 @@ filenameTemplate:
desc: |
Шаблон для названий файлов созданных задач. Используйте {summary} для названия задачи и {key} для ключа задачи
placeholder: '{summary} ({key})'
rename_files: Переименовать существующие файлы
no_files_to_rename: Нет файлов в кэше для переименования
rename_confirm: 'Будет переименовано {count} файл(ов). Продолжить?'
rename_success: 'Успешно переименовано {count} файл(ов)'
rename_errors: 'Не удалось переименовать {count} файл(ов)'
rename_no_changes: Файлы не нуждаются в переименовании

View file

@ -11,12 +11,11 @@ import {
registerUpdateIssueStatusCommand,
registerBatchFetchIssuesCommand,
registerAddCommentCommand,
registerRebuildCacheCommand,
} from './commands';
import { transform_string_to_functions_mappings } from './tools/convertFunctionString';
import { createJiraSyncExtension } from './postprocessing/livePreview';
import { hideJiraPointersReading } from './postprocessing/reading';
import { buildCacheFromFilesystem } from './tools/cacheUtils';
import { buildCacheFromFilesystem, validateCache } from './tools/cacheUtils';
import { checkMigrateSettings } from './tools/migrateSettings';
export default class JiraPlugin extends Plugin {
@ -28,8 +27,9 @@ export default class JiraPlugin extends Plugin {
async onload() {
await this.loadSettings();
// initialize cache from settings
// validate cache from settings
this.initializeCache();
await validateCache(this);
// Register all commands
registerUpdateIssueCommand(this);
@ -42,13 +42,12 @@ export default class JiraPlugin extends Plugin {
registerUpdateWorkLogManuallyCommand(this);
registerUpdateWorkLogBatchCommand(this);
registerAddCommentCommand(this);
registerRebuildCacheCommand(this);
// Add settings tab
this.addSettingTab(new JiraSettingTab(this.app, this));
// Handle Reading mode (post-processor for rendered markdown)
this.registerMarkdownPostProcessor(hideJiraPointersReading());
this.registerMarkdownPostProcessor(hideJiraPointersReading.bind(this));
// Handle Live Preview/Edit mode (CodeMirror extension)
this.registerEditorExtension(createJiraSyncExtension(this));
@ -59,26 +58,13 @@ export default class JiraPlugin extends Plugin {
async loadSettings() {
const old_data = await this.loadData();
// TODO: Cancel and delete migration check in future (approximately 2026-2027)
const { result: migratedData, changed: migrationChanged } = checkMigrateSettings(old_data);
this.settings = {
...DEFAULT_SETTINGS,
...migratedData,
fieldMapping: {
...DEFAULT_SETTINGS.fieldMapping,
...(migratedData?.fieldMapping || {}),
},
};
const new_data = checkMigrateSettings(old_data, this.saveSettings);
this.settings = Object.assign({}, DEFAULT_SETTINGS, new_data);
this.settings.fieldMapping.fieldMappings = await transform_string_to_functions_mappings(
this.settings.fieldMapping.fieldMappingsStrings,
);
if (migrationChanged) {
await this.saveSettings();
}
}
async saveSettings() {
@ -107,36 +93,36 @@ export default class JiraPlugin extends Plugin {
return this.settings.connections[this.settings.currentConnectionIndex];
}
async setFilePathForIssueKey(issueKey: string, filePath: string) {
setFilePathForIssueKey(issueKey: string, filePath: string) {
this.issueKeyToFilePathCache.set(issueKey, filePath);
this.settings.issueKeyToFilePathCache[issueKey] = filePath;
await this.saveSettings();
this.saveSettings();
}
async removeIssueKeyFromCache(issueKey: string) {
removeIssueKeyFromCache(issueKey: string) {
this.issueKeyToFilePathCache.delete(issueKey);
delete this.settings.issueKeyToFilePathCache[issueKey];
await this.saveSettings();
this.saveSettings();
}
async clearCache() {
clearCache() {
this.issueKeyToFilePathCache.clear();
this.settings.issueKeyToFilePathCache = {};
await this.saveSettings();
this.saveSettings();
}
async rebuildCache() {
await this.clearCache();
this.clearCache();
await buildCacheFromFilesystem(this);
}
private registerVaultEventListeners() {
// Handle file renames
this.registerEvent(
this.app.vault.on('rename', async (file, oldPath) => {
this.app.vault.on('rename', (file, oldPath) => {
for (const [issueKey, cachedPath] of this.issueKeyToFilePathCache.entries()) {
if (cachedPath === oldPath) {
await this.setFilePathForIssueKey(issueKey, file.path);
this.setFilePathForIssueKey(issueKey, file.path);
break;
}
}
@ -145,10 +131,10 @@ export default class JiraPlugin extends Plugin {
// Handle file deletions
this.registerEvent(
this.app.vault.on('delete', async (file) => {
this.app.vault.on('delete', (file) => {
for (const [issueKey, cachedPath] of this.issueKeyToFilePathCache.entries()) {
if (cachedPath === file.path) {
await this.removeIssueKeyFromCache(issueKey);
this.removeIssueKeyFromCache(issueKey);
break;
}
}

View file

@ -1,39 +0,0 @@
import { App, Modal, Setting } from 'obsidian';
export class ConfirmModal extends Modal {
private message: string;
private onConfirm: () => void;
private onCancel: () => void;
constructor(app: App, message: string, onConfirm: () => void, onCancel: () => void) {
super(app);
this.message = message;
this.onConfirm = onConfirm;
this.onCancel = onCancel;
}
onOpen() {
this.contentEl.createEl('h2', { text: this.message });
new Setting(this.contentEl)
.addButton((btn) =>
btn
.setButtonText('Yes')
.setCta()
.onClick(() => {
this.close();
this.onConfirm();
}),
)
.addButton((btn) =>
btn.setButtonText('No').onClick(() => {
this.close();
this.onCancel();
}),
);
}
onClose() {
this.contentEl.empty();
}
}

View file

@ -1,11 +1,13 @@
import { App, Modal, Setting, DropdownComponent } from 'obsidian';
import { App, Modal, Setting } from 'obsidian';
import { useTranslations } from '../localization/translator';
import JiraPlugin from '../main';
import { JQLPreview } from '../settings/components/JQLPreview';
import { JQLPreset } from '../settings/default';
const t = useTranslations('modals.jql_search').t;
/**
* Modal for searching issues by JQL query
*/
export class JQLSearchModal extends Modal {
private onSubmit: (jql: string) => void;
private plugin: JiraPlugin;
@ -13,8 +15,6 @@ export class JQLSearchModal extends Modal {
private previewEl?: HTMLElement;
private preview?: JQLPreview;
private debounceTimer?: number;
private presetName: string = '';
private presetDropdown?: DropdownComponent;
constructor(app: App, plugin: JiraPlugin, onSubmit: (jql: string) => void) {
super(app);
@ -23,68 +23,8 @@ export class JQLSearchModal extends Modal {
}
onOpen() {
const conn = this.plugin.getCurrentConnection();
if (conn?.lastJqlQuery) {
this.jql = conn.lastJqlQuery;
this.schedulePreview();
}
this.contentEl.createEl('h2', { text: t('desc') });
const presetSetting = new Setting(this.contentEl).setName(t('presets.load')).setDesc(t('presets.load_desc'));
presetSetting.addDropdown((dropdown) => {
this.presetDropdown = dropdown;
this.populateDropdown(dropdown);
dropdown.onChange((value) => {
if (value) {
const preset = this.getPresets().find((p) => p.name === value);
if (preset) {
this.jql = preset.query;
const textarea = this.contentEl.querySelector('textarea');
if (textarea) {
textarea.value = preset.query;
}
this.schedulePreview();
}
}
});
});
presetSetting.addButton((btn) =>
btn.setButtonText(t('presets.delete')).onClick(() => {
const selectedName = this.presetDropdown?.getValue();
if (selectedName) {
this.deletePreset(selectedName);
if (this.presetDropdown) {
this.populateDropdown(this.presetDropdown);
}
}
}),
);
new Setting(this.contentEl)
.setName(t('presets.save'))
.setDesc(t('presets.save_desc'))
.addText((text) =>
text.setPlaceholder(t('presets.save_hint')).onChange((value) => {
this.presetName = value;
}),
)
.addButton((btn) =>
btn.setButtonText(t('presets.save_btn')).onClick(() => {
if (this.jql.trim() && this.presetName.trim()) {
this.savePreset(this.presetName.trim(), this.jql.trim());
this.presetName = '';
const nameInput = this.contentEl.querySelectorAll('input')[1];
if (nameInput) nameInput.value = '';
if (this.presetDropdown) {
this.populateDropdown(this.presetDropdown);
}
}
}),
);
new Setting(this.contentEl)
.setName(t('jql.name'))
.setDesc(t('jql.desc'))
@ -98,6 +38,7 @@ export class JQLSearchModal extends Modal {
text.inputEl.style.height = '100px';
});
// Create preview container and initialize preview component
this.previewEl = this.contentEl.createDiv();
this.preview = new JQLPreview(this.plugin, this.previewEl, 5);
this.preview.loadPreview('');
@ -108,18 +49,17 @@ export class JQLSearchModal extends Modal {
.setCta()
.onClick(() => {
if (this.jql.trim()) {
this.saveLastQuery(this.jql.trim());
this.close();
this.onSubmit(this.jql.trim());
}
}),
);
// Handle Enter key (Ctrl+Enter for textarea)
this.contentEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
if (this.jql.trim()) {
this.saveLastQuery(this.jql.trim());
this.close();
this.onSubmit(this.jql.trim());
}
@ -127,56 +67,21 @@ export class JQLSearchModal extends Modal {
});
}
private populateDropdown(dropdown: DropdownComponent) {
dropdown.selectEl.empty();
dropdown.addOption('', '— ' + t('presets.select') + ' —');
for (const preset of this.getPresets()) {
dropdown.addOption(preset.name, preset.name);
}
}
private getPresets(): JQLPreset[] {
const conn = this.plugin.getCurrentConnection();
return conn?.jqlPresets || [];
}
private savePreset(name: string, query: string) {
const conn = this.plugin.getCurrentConnection();
if (!conn) return;
const existingIndex = conn.jqlPresets.findIndex((p) => p.name === name);
if (existingIndex >= 0) {
conn.jqlPresets[existingIndex].query = query;
} else {
conn.jqlPresets.push({ name, query });
}
this.plugin.saveSettings();
}
private deletePreset(name: string) {
const conn = this.plugin.getCurrentConnection();
if (!conn) return;
conn.jqlPresets = conn.jqlPresets.filter((p) => p.name !== name);
this.plugin.saveSettings();
}
private saveLastQuery(jql: string) {
const conn = this.plugin.getCurrentConnection();
if (!conn) return;
conn.lastJqlQuery = jql;
this.plugin.saveSettings();
}
private schedulePreview() {
if (this.debounceTimer) {
window.clearTimeout(this.debounceTimer);
}
// Show loading immediately if there's text
if (this.jql.trim() && this.preview) {
this.preview.showLoading();
}
this.debounceTimer = window.setTimeout(() => {
if (this.preview) {
this.preview.loadPreview(this.jql).catch(() => {});
this.preview.loadPreview(this.jql).catch(() => {
// Error handling is done within the preview component
});
}
}, 600);
}

View file

@ -5,5 +5,3 @@ export * from './IssueTypeModal';
export * from './IssueWorkLogModal';
export * from './ProjectModal';
export * from './JQLSearchModal';
export * from './ConfirmModal';
export * from './ConfirmModal';

View file

@ -31,36 +31,32 @@ export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
buildDecorations(view: EditorView) {
const builder = new RangeSetBuilder<Decoration>();
const sel = view.state.selection;
const allDecs: Array<{ from: number; to: number; dec: Decoration }> = [];
for (let { from, to } of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to);
const codeBlocks = this.findCodeBlocks(text, from);
// Collect all jira-sync marker positions
const markers: Array<{ start: number; end: number; name: string }> = [];
for (const match of text.matchAll(/`(jira-sync-[^`]+)`/g)) {
const start = from + match.index!;
const end = start + match[0].length;
if (this.isInsideCodeBlock(start, end, codeBlocks)) continue;
markers.push({ start, end, name: match[1] });
}
const contentStart = start; // without first `
const contentEnd = end; // without last `
// Always hide the markers themselves, show when cursor is inside
for (const { start, end } of markers) {
const isActive = sel.ranges.some((r) => r.from <= end && r.to >= start);
const cls = isActive ? 'jira-sync-hidden jira-sync-active' : 'jira-sync-hidden';
allDecs.push({ from: start, to: end, dec: Decoration.mark({ class: cls }) });
}
}
// Find code blocks to ignore
if (this.isInsideCodeBlock(contentStart, contentEnd, codeBlocks)) {
continue;
}
// RangeSetBuilder requires ranges in ascending order of `from`, then `to`
allDecs.sort((a, b) => a.from - b.from || a.to - b.to);
for (const { from, to, dec } of allDecs) {
builder.add(from, to, dec);
let className = 'jira-sync-hidden';
// Check if cursor is inside
const sel = view.state.selection;
if (sel.ranges.some((r) => r.from <= contentEnd && r.to >= contentStart)) {
className += ' jira-sync-active';
}
builder.add(start, end, Decoration.mark({ class: className }));
}
}
return builder.finish() as DecorationSet;

View file

@ -1,12 +1,13 @@
export function hideJiraPointersReading() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return function (element: HTMLElement, _: any) {
const codeElements = Array.from(element.querySelectorAll(':not(pre) > code')).filter((el) =>
el.textContent?.startsWith('jira-sync-'),
) as HTMLElement[];
// For Reading mode - targets <code> elements in <p> tags
export function hideJiraPointersReading(element: HTMLElement) {
// In Reading mode, inline code becomes <code> elements
const codeElements = element.querySelectorAll(':not(pre) > code');
for (const codeEl of codeElements) {
codeElements.forEach((codeEl) => {
const text = codeEl.textContent || '';
if (text.startsWith('jira-sync-')) {
codeEl.addClass('jira-sync-hidden');
}
};
});
}

View file

@ -66,9 +66,6 @@ export class ConnectionSettingsComponent implements SettingsComponent {
password: '',
jiraUrl: '',
apiVersion: '2' as const,
jqlPresets: [],
lastJqlQuery: '',
sessionCookie: '',
};
plugin.settings.connections.push(newConnection);
plugin.settings.currentConnectionIndex = plugin.settings.connections.length - 1;
@ -86,7 +83,7 @@ export class ConnectionSettingsComponent implements SettingsComponent {
private populateConnectionDropdown(cb: any): void {
const { plugin } = this.props;
cb.selectEl.empty();
cb.selectEl.innerHTML = '';
plugin.settings.connections.forEach((conn, index) => {
cb.addOption(index.toString(), conn.name || `Connection ${index + 1}`);
});

View file

@ -1,8 +1,6 @@
import { Notice, Setting } from 'obsidian';
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
import { fetchIssue, validateSettings } from '../../api';
import { renameExistingIssueFiles } from '../../file_operations/getIssue';
import { ConfirmModal } from '../../modals/ConfirmModal';
import { fetchIssue } from '../../api';
import debounce from 'lodash/debounce';
import hljs from 'highlight.js';
import { useTranslations } from '../../localization/translator';
@ -25,6 +23,7 @@ export class FetchIssueComponent implements SettingsComponent {
render(containerEl: HTMLElement): void {
const { plugin } = this.props;
// Filename template setting
const filenameTemplateSetting = new Setting(containerEl)
.setName(t('filenameTemplate.name'))
.setDesc(t('filenameTemplate.desc'));
@ -38,40 +37,6 @@ export class FetchIssueComponent implements SettingsComponent {
});
});
filenameTemplateSetting.addButton((btn) =>
btn.setButtonText(t('rename_files')).onClick(async () => {
if (!validateSettings(plugin)) return;
const cacheSize = plugin.getAllIssueKeysMap().size;
if (cacheSize === 0) {
new Notice(t('no_files_to_rename'));
return;
}
new ConfirmModal(
plugin.app,
t('rename_confirm', { count: cacheSize.toString() }),
async () => {
const result = await renameExistingIssueFiles(plugin);
if (result.renamed > 0) {
new Notice(t('rename_success', { count: result.renamed.toString() }));
}
if (result.errors.length > 0) {
new Notice(
t('rename_errors', { count: result.errors.length.toString() }) +
': ' +
result.errors.slice(0, 3).join(', '),
);
}
if (result.renamed === 0 && result.errors.length === 0) {
new Notice(t('rename_no_changes'));
}
},
() => {},
).open();
}),
);
const link =
plugin.getCurrentConnection()?.apiVersion === '3'
? 'https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-post'
@ -122,6 +87,7 @@ export class FetchIssueComponent implements SettingsComponent {
});
});
// Add input field for issue key
new Setting(containerEl)
.setName(t('key.name'))
.setDesc(t('key.desc'))
@ -132,6 +98,7 @@ export class FetchIssueComponent implements SettingsComponent {
}),
);
// Create container for issue data
this.issueDataContainer = containerEl.createDiv({
cls: 'jira-raw-issue-container',
});

View file

@ -133,7 +133,7 @@ export class FieldMappingItem {
textarea.addEventListener('focus', resize);
// Initial resize
window.setTimeout(resize, 0);
setTimeout(resize, 0);
}
/**

View file

@ -156,42 +156,28 @@ export class FieldMappingsComponent implements SettingsComponent {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'modal-container';
const modalContent = document.createElement('div');
modalContent.className = 'modal';
modal.appendChild(modalContent);
const titleEl = document.createElement('div');
titleEl.className = 'modal-title';
titleEl.textContent = title;
modalContent.appendChild(titleEl);
const contentEl = document.createElement('div');
contentEl.className = 'modal-content';
contentEl.textContent = message;
modalContent.appendChild(contentEl);
const buttonContainer = document.createElement('div');
buttonContainer.className = 'modal-button-container';
modalContent.appendChild(buttonContainer);
const confirmBtn = document.createElement('button');
confirmBtn.className = 'mod-cta';
confirmBtn.textContent = 'Confirm';
buttonContainer.appendChild(confirmBtn);
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
buttonContainer.appendChild(cancelBtn);
modal.innerHTML = `
<div class="modal">
<div class="modal-title">${title}</div>
<div class="modal-content">${message}</div>
<div class="modal-button-container">
<button class="mod-cta" id="confirm-btn">Confirm</button>
<button id="cancel-btn">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
confirmBtn.addEventListener('click', () => {
const confirmBtn = modal.querySelector('#confirm-btn') as HTMLButtonElement;
const cancelBtn = modal.querySelector('#cancel-btn') as HTMLButtonElement;
confirmBtn?.addEventListener('click', () => {
document.body.removeChild(modal);
resolve(true);
});
cancelBtn.addEventListener('click', () => {
cancelBtn?.addEventListener('click', () => {
document.body.removeChild(modal);
resolve(false);
});

View file

@ -1,10 +1,5 @@
import { FieldMapping } from '../default/obsidianJiraFieldsMapping';
export interface JQLPreset {
name: string;
query: string;
}
export interface ConnectionSettingsInterface {
name: string;
jiraUrl: string;
@ -14,9 +9,6 @@ export interface ConnectionSettingsInterface {
username: string;
email: string;
password: string;
jqlPresets: JQLPreset[];
lastJqlQuery: string;
sessionCookie: string;
}
export interface GlobalSettingsInterface {
@ -86,9 +78,6 @@ export const DEFAULT_SETTINGS: JiraSettingsInterface = {
password: '',
jiraUrl: '',
apiVersion: '2',
jqlPresets: [],
lastJqlQuery: '',
sessionCookie: '',
},
],
currentConnectionIndex: 0,

View file

@ -28,7 +28,7 @@ async function scanFolderForIssueKeys(plugin: JiraPlugin, folder: TFolder): Prom
const metadata = plugin.app.metadataCache.getFileCache(child);
const issueKey = metadata?.frontmatter?.key;
if (issueKey && typeof issueKey === 'string') {
await plugin.setFilePathForIssueKey(issueKey, child.path);
plugin.setFilePathForIssueKey(issueKey, child.path);
}
} else if (child instanceof TFolder) {
await scanFolderForIssueKeys(plugin, child);
@ -51,7 +51,7 @@ export async function validateCache(plugin: JiraPlugin): Promise<void> {
}
// Remove invalid entries
for (const issueKey of entriesToRemove) {
await plugin.removeIssueKeyFromCache(issueKey);
}
entriesToRemove.forEach((issueKey) => {
plugin.removeIssueKeyFromCache(issueKey);
});
}

View file

@ -340,13 +340,13 @@ export async function transform_string_to_functions_mappings(
// Also convert to functions for runtime use
const transformedMappings: Record<string, FieldMapping> = {};
for (const [fieldName, { toJira, fromJira }] of Object.entries(mappings)) {
const toJiraFn = await safeStringToFunction(toJira, 'toJira', extraValidate);
const fromJiraFn = await safeStringToFunction(fromJira, 'fromJira', extraValidate);
const toJiraFn = safeStringToFunction(toJira, 'toJira', extraValidate);
const fromJiraFn = safeStringToFunction(fromJira, 'fromJira', extraValidate);
if (toJiraFn && fromJiraFn) {
transformedMappings[fieldName] = {
toJira: toJiraFn as (value: any, api_version?: '2' | '3') => any,
fromJira: fromJiraFn as (
toJira: (await toJiraFn) as (value: any, api_version?: '2' | '3') => any,
fromJira: (await fromJiraFn) as (
issue: JiraIssue,
api_version?: '2' | '3',
data_source?: Record<string, any>,

View file

@ -3,10 +3,9 @@
* @param str The Jira markup string
* @returns The Markdown string
*/
export function jiraToMarkdown(str: any): string | null {
export function jiraToMarkdown(str: any): string {
try {
if (str === undefined) return null;
if (str === null) return '';
if (str === null || str === undefined) return '';
// Initial normalization to string
let content: string = '';

View file

@ -1,5 +1,5 @@
interface AdfTextMark {
type: 'strong' | 'em' | 'code' | 'link' | 'underline' | 'strike';
type: 'strong' | 'em' | 'code' | 'link';
attrs?: { href: string };
}
@ -47,15 +47,9 @@ interface AdfOrderedListNode {
content: AdfListItemNode[];
}
interface AdfTaskItemNode {
type: 'taskItem';
attrs: { localId: string; state: 'TODO' | 'DONE' };
content: AdfInlineContent[];
}
interface AdfTaskListNode {
type: 'taskList';
content: AdfTaskItemNode[];
content: AdfListItemNode[];
attrs?: { localId: string };
}
@ -78,219 +72,45 @@ interface AdfDoc {
content: AdfBlockNode[];
}
// --- Inline formatting rules -------------------------------------------------
interface FormatRule {
open: string;
close: string;
marks: AdfTextMark[];
}
const formatRules: FormatRule[] = [
{ open: '<b>', close: '</b>', marks: [{ type: 'strong' }] },
{ open: '<strong>', close: '</strong>', marks: [{ type: 'strong' }] },
{ open: '<i>', close: '</i>', marks: [{ type: 'em' }] },
{ open: '<em>', close: '</em>', marks: [{ type: 'em' }] },
{ open: '<u>', close: '</u>', marks: [{ type: 'underline' }] },
{ open: '<ins>', close: '</ins>', marks: [{ type: 'underline' }] },
{ open: '<s>', close: '</s>', marks: [{ type: 'strike' }] },
{ open: '<strike>', close: '</strike>', marks: [{ type: 'strike' }] },
{ open: '<del>', close: '</del>', marks: [{ type: 'strike' }] },
{ open: '***', close: '***', marks: [{ type: 'strong' }, { type: 'em' }] },
{ open: '**', close: '**', marks: [{ type: 'strong' }] },
{ open: '*', close: '*', marks: [{ type: 'em' }] },
{ open: '`', close: '`', marks: [{ type: 'code' }] },
];
const linkRe = /^\[([^\]]+)\]\(([^)]+)\)/;
const wikiRe = /^\[\[[^\]]+\]\]/;
// --- Inline parser -----------------------------------------------------------
function mergeTextNodes(nodes: AdfInlineContent[]): AdfInlineContent[] {
const out: AdfInlineContent[] = [];
for (const n of nodes) {
if (n.type !== 'text' || !out.length || out[out.length - 1].type !== 'text') {
out.push(n);
continue;
}
const last = out[out.length - 1] as AdfTextNode;
if (JSON.stringify(last.marks) !== JSON.stringify(n.marks)) {
out.push(n);
continue;
}
last.text += n.text;
}
return out;
}
function parseInlineContent(text: string): AdfInlineContent[] {
const nodes: AdfInlineContent[] = [];
let pos = 0;
let textStart = 0;
const flush = () => {
if (textStart < pos) nodes.push({ type: 'text', text: text.slice(textStart, pos) });
};
while (pos < text.length) {
const remaining = text.slice(pos);
const linkMatch = remaining.match(linkRe);
if (linkMatch) {
flush();
const linkMark: AdfTextMark = { type: 'link', attrs: { href: linkMatch[2] } };
for (const n of parseInlineContent(linkMatch[1])) {
if (n.type === 'text') {
nodes.push({ type: 'text', text: n.text, marks: n.marks ? [...n.marks, linkMark] : [linkMark] });
}
}
pos += linkMatch[0].length;
textStart = pos;
continue;
}
const wikiMatch = remaining.match(wikiRe);
if (wikiMatch) {
flush();
nodes.push({ type: 'text', text: wikiMatch[0] });
pos += wikiMatch[0].length;
textStart = pos;
continue;
}
let matched = false;
for (const rule of formatRules) {
if (!remaining.startsWith(rule.open)) continue;
const afterOpen = remaining.slice(rule.open.length);
const closeIdx = afterOpen.indexOf(rule.close);
if (closeIdx === -1) continue;
if (rule.marks[0].type !== 'code' && closeIdx === 0) continue;
const content = afterOpen.slice(0, closeIdx);
flush();
if (rule.marks[0].type === 'code') {
nodes.push({ type: 'text', text: content, marks: [...rule.marks] });
} else {
const inner = parseInlineContent(content);
for (const n of inner) {
if (n.type === 'text') {
nodes.push({ type: 'text', text: n.text, marks: [...rule.marks, ...(n.marks || [])] });
}
}
}
pos += rule.open.length + content.length + rule.close.length;
textStart = pos;
matched = true;
break;
}
if (!matched) pos++;
}
flush();
return mergeTextNodes(nodes);
}
function parseInline(text: string): AdfInlineContent[] {
return parseInlineContent(text);
}
const nodes: AdfInlineContent[] = [];
const regex = /(\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)|\[\[[^\]]+\]\])/g;
let lastIndex = 0;
let match: RegExpExecArray | null;
// --- Block-level parsing helpers ---------------------------------------------
function parseFencedCode(lines: string[], i: number): { node: AdfCodeBlockNode; next: number } | null {
if (!lines[i].startsWith('```')) return null;
const lang = lines[i].slice(3).trim();
const codeLines: string[] = [];
i++;
while (i < lines.length && !lines[i].startsWith('```')) {
codeLines.push(lines[i]);
i++;
}
return {
node: { type: 'codeBlock', attrs: { language: lang }, content: [{ type: 'text', text: codeLines.join('\n') }] },
next: i + 1,
};
}
function parseHeading(line: string): AdfHeadingNode | null {
const m = line.match(/^(#{1,6})\s+(.*)/);
if (!m) return null;
return { type: 'heading', attrs: { level: m[1].length }, content: parseInline(m[2]) };
}
function parseHr(line: string): AdfRuleNode | null {
return line.match(/^(-{3,}|\*{3,}|_{3,})$/) ? { type: 'rule' } : null;
}
function parseTaskList(lines: string[], i: number): { node: AdfTaskListNode; next: number } | null {
if (!lines[i].match(/^[-*+]\s+\[[ xX]\]\s*/)) return null;
const items: AdfTaskItemNode[] = [];
let counter = 0;
while (i < lines.length && lines[i].match(/^[-*+]\s+\[[ xX]\]\s*/)) {
const m = lines[i].match(/^[-*+]\s+\[([ xX])\]\s*(.*)/);
if (m) {
const state = m[1].toLowerCase() === 'x' ? ('DONE' as const) : ('TODO' as const);
const inline: AdfInlineContent[] = parseInline(m[2]);
i++;
while (i < lines.length && lines[i].match(/^\s+[-*+]\s+/)) {
inline.push({ type: 'hardBreak' });
inline.push(...parseInline('- ' + lines[i].replace(/^\s+[-*+]\s+/, '')));
i++;
}
items.push({
type: 'taskItem',
attrs: { localId: `task-${Date.now()}-${counter++}`, state },
content: inline,
});
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
nodes.push({ type: 'text', text: text.slice(lastIndex, match.index) });
}
if (match[2] !== undefined) {
// ***bold+italic***
nodes.push({ type: 'text', text: match[2], marks: [{ type: 'strong' }, { type: 'em' }] });
} else if (match[3] !== undefined) {
// **bold**
nodes.push({ type: 'text', text: match[2], marks: [{ type: 'strong' }] });
} else if (match[4] !== undefined) {
// *italic*
nodes.push({ type: 'text', text: match[3], marks: [{ type: 'em' }] });
} else if (match[5] !== undefined) {
// `code`
nodes.push({ type: 'text', text: match[4], marks: [{ type: 'code' }] });
} else if (match[6] !== undefined) {
// [text](url) — convert to ADF link mark
nodes.push({ type: 'text', text: match[5], marks: [{ type: 'link', attrs: { href: match[6] } }] });
} else {
i++;
// [[wikilink]] — preserve as plain text so round-trip survives
nodes.push({ type: 'text', text: match[0] });
}
lastIndex = match.index + match[0].length;
}
return { node: { type: 'taskList', attrs: { localId: `tasklist-${Date.now()}` }, content: items }, next: i };
}
function parseListItems(
lines: string[],
i: number,
itemRe: RegExp,
stripRe: RegExp,
): { items: AdfListItemNode[]; next: number } {
const items: AdfListItemNode[] = [];
while (i < lines.length && lines[i].match(itemRe)) {
const content: any[] = [{ type: 'paragraph', content: parseInline(lines[i].replace(stripRe, '')) }];
i++;
const subs: AdfListItemNode[] = [];
while (i < lines.length && lines[i].match(/^\s+[-*+]\s+/)) {
subs.push({
type: 'listItem',
content: [{ type: 'paragraph', content: parseInline(lines[i].replace(/^\s+[-*+]\s+/, '')) }],
});
i++;
}
if (subs.length) content.push({ type: 'bulletList', content: subs });
items.push({ type: 'listItem', content: content as [AdfParagraphNode] });
if (lastIndex < text.length) {
nodes.push({ type: 'text', text: text.slice(lastIndex) });
}
return { items, next: i };
}
function isBlockStart(line: string): boolean {
return (
line.startsWith('```') ||
!!line.match(/^#{1,6}\s/) ||
!!line.match(/^[-*+]\s+\[[ xX]\]\s*/) ||
!!line.match(/^[-*+]\s/) ||
!!line.match(/^\d+\.\s/) ||
!!line.match(/^(-{3,}|\*{3,}|_{3,})$/)
);
return nodes.length > 0 ? nodes : [{ type: 'text', text }];
}
// --- Markdown → ADF ----------------------------------------------------------
export function markdownToAdf(markdown: string): AdfDoc | null {
if (!markdown || !markdown.trim()) return null;
@ -301,72 +121,159 @@ export function markdownToAdf(markdown: string): AdfDoc | null {
while (i < lines.length) {
const line = lines[i];
const code = parseFencedCode(lines, i);
if (code) {
content.push(code.node);
i = code.next;
continue;
}
const heading = parseHeading(line);
if (heading) {
content.push(heading);
// Fenced code block
if (line.startsWith('```')) {
const lang = line.slice(3).trim();
const codeLines: string[] = [];
i++;
while (i < lines.length && !lines[i].startsWith('```')) {
codeLines.push(lines[i]);
i++;
}
content.push({
type: 'codeBlock',
attrs: { language: lang },
content: [{ type: 'text', text: codeLines.join('\n') }],
});
i++;
continue;
}
const hr = parseHr(line);
if (hr) {
content.push(hr);
// Heading
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
if (headingMatch) {
content.push({
type: 'heading',
attrs: { level: headingMatch[1].length },
content: parseInline(headingMatch[2]),
});
i++;
continue;
}
const task = parseTaskList(lines, i);
if (task) {
content.push(task.node);
i = task.next;
// Horizontal rule
if (line.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
content.push({ type: 'rule' });
i++;
continue;
}
// Task list (checkboxes) — must be checked before bullet list
if (line.match(/^[-*+]\s+\[[ xX]\]\s*/)) {
const items: any[] = [];
let taskCounter = 0;
while (i < lines.length && lines[i].match(/^[-*+]\s+\[[ xX]\]\s*/)) {
const checkMatch = lines[i].match(/^[-*+]\s+\[([ xX])\]\s*(.*)/);
if (checkMatch) {
const state = checkMatch[1].toLowerCase() === 'x' ? 'DONE' : 'TODO';
// taskItem only allows inline nodes — flatten sub-items as hardBreak + inline
const inlineContent: AdfInlineContent[] = parseInline(checkMatch[2]);
i++;
while (i < lines.length && lines[i].match(/^\s+[-*+]\s+/)) {
inlineContent.push({ type: 'hardBreak' });
inlineContent.push(...parseInline('- ' + lines[i].replace(/^\s+[-*+]\s+/, '')));
i++;
}
items.push({
type: 'taskItem',
attrs: { localId: `task-${Date.now()}-${taskCounter++}`, state },
content: inlineContent,
});
} else {
i++;
}
}
content.push({ type: 'taskList', attrs: { localId: `tasklist-${Date.now()}` }, content: items });
continue;
}
// Bullet list
if (line.match(/^[-*+]\s+/)) {
const r = parseListItems(lines, i, /^[-*+]\s+/, /^[-*+]\s+/);
content.push({ type: 'bulletList', content: r.items });
i = r.next;
const items: AdfListItemNode[] = [];
while (i < lines.length && lines[i].match(/^[-*+]\s+/)) {
const itemContent: any[] = [
{ type: 'paragraph', content: parseInline(lines[i].replace(/^[-*+]\s+/, '')) },
];
i++;
const subItems: AdfListItemNode[] = [];
while (i < lines.length && lines[i].match(/^\s+[-*+]\s+/)) {
subItems.push({
type: 'listItem',
content: [{ type: 'paragraph', content: parseInline(lines[i].replace(/^\s+[-*+]\s+/, '')) }],
});
i++;
}
if (subItems.length > 0) {
itemContent.push({ type: 'bulletList', content: subItems });
}
items.push({ type: 'listItem', content: itemContent as [AdfParagraphNode] });
}
content.push({ type: 'bulletList', content: items });
continue;
}
// Ordered list
if (line.match(/^\d+\.\s+/)) {
const r = parseListItems(lines, i, /^\d+\.\s+/, /^\d+\.\s+/);
content.push({ type: 'orderedList', content: r.items });
i = r.next;
const items: AdfListItemNode[] = [];
while (i < lines.length && lines[i].match(/^\d+\.\s+/)) {
const itemContent: any[] = [
{ type: 'paragraph', content: parseInline(lines[i].replace(/^\d+\.\s+/, '')) },
];
i++;
const subItems: AdfListItemNode[] = [];
while (i < lines.length && lines[i].match(/^\s+[-*+]\s+/)) {
subItems.push({
type: 'listItem',
content: [{ type: 'paragraph', content: parseInline(lines[i].replace(/^\s+[-*+]\s+/, '')) }],
});
i++;
}
if (subItems.length > 0) {
itemContent.push({ type: 'bulletList', content: subItems });
}
items.push({ type: 'listItem', content: itemContent as [AdfParagraphNode] });
}
content.push({ type: 'orderedList', content: items });
continue;
}
// Empty line
if (line.trim() === '') {
i++;
continue;
}
// Paragraph — collect until empty line or block-level element
const paraLines: string[] = [];
while (i < lines.length && lines[i].trim() !== '' && !isBlockStart(lines[i])) {
while (
i < lines.length &&
lines[i].trim() !== '' &&
!lines[i].match(/^#{1,6}\s/) &&
!lines[i].match(/^[-*+]\s+\[[ xX]\]\s*/) &&
!lines[i].match(/^[-*+]\s/) &&
!lines[i].match(/^\d+\.\s/) &&
!lines[i].startsWith('```') &&
!lines[i].match(/^(-{3,}|\*{3,}|_{3,})$/)
) {
paraLines.push(lines[i]);
i++;
}
if (paraLines.length) {
content.push({ type: 'paragraph', content: parseInline(paraLines.join('\n')) });
if (paraLines.length > 0) {
content.push({
type: 'paragraph',
content: parseInline(paraLines.join('\n')),
});
}
}
return content.length ? { version: 1, type: 'doc', content } : null;
return content.length > 0 ? { version: 1, type: 'doc', content } : null;
}
// --- ADF → Markdown helpers --------------------------------------------------
function adfInlineToMarkdown(nodes: any[]): string {
if (!nodes) return '';
return nodes
.map((node: any) => {
.map((node) => {
if (node.type === 'hardBreak') return '\n';
if (node.type === 'mention') return node.attrs?.text || node.attrs?.displayName || '';
if (node.type === 'emoji') return node.attrs?.text || node.attrs?.shortName || '';
@ -380,80 +287,43 @@ function adfInlineToMarkdown(nodes: any[]): string {
const linkMark = (node.marks || []).find((m: any) => m.type === 'link');
let result = text;
if (marks.includes('code')) return `\`${result}\``;
if (marks.includes('strike')) result = `<s>${result}</s>`;
if (marks.includes('underline')) result = `<u>${result}</u>`;
if (marks.includes('strong')) result = `<b>${result}</b>`;
if (marks.includes('em')) result = `<i>${result}</i>`;
if (marks.includes('strike')) result = `~~${result}~~`;
if (marks.includes('strong')) result = `**${result}**`;
if (marks.includes('em')) result = `*${result}*`;
if (linkMark) result = `[${result}](${linkMark.attrs?.href || ''})`;
return result;
})
.join('');
}
function renderList(items: any[], ordered: boolean): string {
return items
.map((item: any, idx: number) => {
const blocks: any[] = item.content || [];
const first = blocks[0];
const rest = blocks.slice(1);
const main = first ? adfBlockToMarkdown(first) : '';
const nested = rest
.map((b: any) => adfBlockToMarkdown(b))
.filter((s: string) => s.trim())
.map((s: string) =>
s
.split('\n')
.map((l: string) => ' ' + l)
.join('\n'),
)
.join('\n');
const prefix = ordered ? `${idx + 1}.` : '-';
return `${prefix} ${main}${nested ? '\n' + nested : ''}`;
})
.join('\n');
}
function adfBlockToMarkdown(node: any): string {
if (!node) return '';
switch (node.type) {
case 'heading':
return `${'#'.repeat(node.attrs?.level || 1)} ${adfInlineToMarkdown(node.content || [])}`;
case 'paragraph':
return adfInlineToMarkdown(node.content || []);
case 'heading': {
const level = node.attrs?.level || 1;
const text = adfInlineToMarkdown(node.content || []);
return `${'#'.repeat(level)} ${text}`;
}
case 'paragraph': {
const text = adfInlineToMarkdown(node.content || []);
return text;
}
case 'codeBlock': {
const lang = node.attrs?.language || '';
const code = (node.content || []).map((n: any) => n.text || '').join('');
return `\`\`\`${lang}\n${code}\n\`\`\``;
}
case 'bulletList':
return renderList(node.content || [], false);
case 'orderedList':
return renderList(node.content || [], true);
case 'taskList':
case 'bulletList': {
return (node.content || [])
.map((item: any) => {
const checked = item.attrs?.state === 'DONE';
const blocks: any[] = item.content || [];
const first = blocks[0];
const rest = blocks.slice(1);
const raw =
first?.type === 'paragraph'
? adfInlineToMarkdown(first.content || [])
: adfInlineToMarkdown(blocks);
const lines = raw.split('\n');
const full =
lines[0] +
(lines.slice(1).length
? '\n' +
lines
.slice(1)
.map((l: string) => ' ' + l)
.join('\n')
: '');
const mainText = first ? adfBlockToMarkdown(first) : '';
const nested = rest
.map((b: any) => adfBlockToMarkdown(b))
.filter((s: string) => s.trim())
.filter((s: string) => s.trim() !== '')
.map((s: string) =>
s
.split('\n')
@ -461,32 +331,88 @@ function adfBlockToMarkdown(node: any): string {
.join('\n'),
)
.join('\n');
return `- [${checked ? 'x' : ' '}] ${full}${nested ? '\n' + nested : ''}`;
return `- ${mainText}${nested ? '\n' + nested : ''}`;
})
.join('\n');
}
case 'orderedList': {
return (node.content || [])
.map((item: any, idx: number) => {
const blocks: any[] = item.content || [];
const first = blocks[0];
const rest = blocks.slice(1);
const mainText = first ? adfBlockToMarkdown(first) : '';
const nested = rest
.map((b: any) => adfBlockToMarkdown(b))
.filter((s: string) => s.trim() !== '')
.map((s: string) =>
s
.split('\n')
.map((l: string) => ' ' + l)
.join('\n'),
)
.join('\n');
return `${idx + 1}. ${mainText}${nested ? '\n' + nested : ''}`;
})
.join('\n');
}
case 'taskList': {
return (node.content || [])
.map((item: any) => {
const checked = item.attrs?.state === 'DONE';
const blocks: any[] = item.content || [];
const first = blocks[0];
const rest = blocks.slice(1);
const rawText =
first?.type === 'paragraph'
? adfInlineToMarkdown(first.content || [])
: adfInlineToMarkdown(blocks);
// Indent continuation lines (e.g. hardBreak + sub-item text)
const textLines = rawText.split('\n');
const fullText =
textLines[0] +
(textLines.slice(1).length
? '\n' +
textLines
.slice(1)
.map((l: string) => ' ' + l)
.join('\n')
: '');
const nested = rest
.map((b: any) => adfBlockToMarkdown(b))
.filter((s: string) => s.trim() !== '')
.map((s: string) =>
s
.split('\n')
.map((l: string) => ' ' + l)
.join('\n'),
)
.join('\n');
return `- [${checked ? 'x' : ' '}] ${fullText}${nested ? '\n' + nested : ''}`;
})
.join('\n');
}
case 'rule':
return '---';
case 'blockquote': {
const inner = (node.content || []).map((n: any) => adfBlockToMarkdown(n)).join('\n');
return inner
.split('\n')
.map((l: string) => `> ${l}`)
.map((line: string) => `> ${line}`)
.join('\n');
}
case 'table': {
const rows: any[] = node.content || [];
if (!rows.length) return '';
const mdRows = rows.map(
(row: any) =>
`| ${(row.content || [])
.map((cell: any) =>
(cell.content || [])
.map((n: any) => adfBlockToMarkdown(n))
.join(' ')
.replace(/\|/g, '\\|'),
)
.join(' | ')} |`,
);
const mdRows = rows.map((row: any) => {
const cells = (row.content || []).map((cell: any) =>
(cell.content || [])
.map((n: any) => adfBlockToMarkdown(n))
.join(' ')
.replace(/\|/g, '\\|'),
);
return `| ${cells.join(' | ')} |`;
});
if (mdRows.length === 0) return '';
const sep = `| ${rows[0].content.map(() => '---').join(' | ')} |`;
return [mdRows[0], sep, ...mdRows.slice(1)].join('\n');
}
@ -500,11 +426,8 @@ function adfBlockToMarkdown(node: any): string {
}
}
// --- ADF → Markdown ----------------------------------------------------------
export function adfToMarkdown(adf: any): string | null {
if (adf === undefined) return null;
export function adfToMarkdown(adf: any): string {
if (!adf || typeof adf !== 'object') return '';
const blocks = (adf.content || []).map((node: any) => adfBlockToMarkdown(node));
return blocks.filter((b: string) => b !== '').join('\n\n');
const blocks: string[] = (adf.content || []).map((node: any) => adfBlockToMarkdown(node));
return blocks.filter((b) => b !== '').join('\n\n');
}

View file

@ -5,8 +5,8 @@ import {
TimekeepSettingsInterface,
} from '../settings/default';
export function checkMigrateSettings(data: any): { result: any; changed: boolean } {
if (!data || typeof data !== 'object') return { result: data, changed: false };
export function checkMigrateSettings(data: any, saveSettings: () => void): any {
if (!data || typeof data !== 'object') return data;
const result = { ...data };
let data_changed = false;
@ -84,8 +84,6 @@ export function checkMigrateSettings(data: any): { result: any; changed: boolean
username: result.connection.username || '',
email: result.connection.email || '',
password: result.connection.password || '',
jqlPresets: [],
lastJqlQuery: '',
},
];
result.currentConnectionIndex = 0;
@ -93,23 +91,9 @@ export function checkMigrateSettings(data: any): { result: any; changed: boolean
data_changed = true;
}
// Ensure all connections have jqlPresets and lastJqlQuery
if (result.connections && Array.isArray(result.connections)) {
for (const conn of result.connections) {
if (!('jqlPresets' in conn)) {
conn.jqlPresets = [];
data_changed = true;
}
if (!('lastJqlQuery' in conn)) {
conn.lastJqlQuery = '';
data_changed = true;
}
}
}
if (data_changed) {
return { result, changed: true };
saveSettings();
}
return { result, changed: false };
return result;
}

View file

@ -1,183 +0,0 @@
export class Plugin {
app: App;
settings: Record<string, any> = {};
constructor() {
this.app = new App();
}
async loadData(): Promise<any> {
return {};
}
async saveData(data: any): Promise<void> {}
addCommand(_cmd: any): void {}
addSettingTab(_tab: any): void {}
registerEvent(_event: any): void {}
registerEditorExtension(_ext: any): void {}
registerMarkdownPostProcessor(_proc: any): void {}
registerInterval(_id: number): number {
return 0;
}
}
export class App {
vault: Vault;
workspace: Workspace;
metadataCache: MetadataCache;
fileManager: FileManager;
}
export class Vault {
async read(_file: TFile): Promise<string> {
return '';
}
async process(_file: TFile, _fn: (data: string) => string): Promise<string> {
return '';
}
async create(_path: string, _content: string): Promise<TFile> {
return new TFile();
}
async rename(_file: TFile, _newPath: string): Promise<void> {}
getFileByPath(_path: string): TFile | null {
return null;
}
getFolderByPath(_path: string): TFolder | null {
return null;
}
async createFolder(_path: string): Promise<void> {}
}
export class TFile {
path: string = '';
extension: string = 'md';
name: string = '';
parent: TFolder | null = null;
vault: Vault = new Vault();
}
export class TFolder {
path: string = '';
name: string = '';
children: (TFile | TFolder)[] = [];
isRoot(): boolean {
return false;
}
}
export class Workspace {
on(_name: string, _cb: (...args: any[]) => any): void {}
openLinkText(_path: string, _sourcePath?: string): Promise<void> {
return Promise.resolve();
}
}
export class MetadataCache {
getFileCache(_file: TFile): any {
return null;
}
}
export class FileManager {
async processFrontMatter(_file: TFile, _fn: (frontmatter: any) => void): Promise<void> {}
}
export class Modal {
app: App;
contentEl: HTMLElement;
constructor(app: App) {
this.app = app;
this.contentEl = document.createElement('div');
}
open(): void {}
close(): void {}
}
export class Setting {
constructor(_containerEl: HTMLElement) {
this.setName('');
this.setDesc('');
}
setName(_name: string): this {
return this;
}
setDesc(_desc: string): this {
return this;
}
addText(_cb: (text: TextComponent) => any): this {
return this;
}
addTextArea(_cb: (text: TextComponent) => any): this {
return this;
}
addButton(_cb: (btn: ButtonComponent) => any): this {
return this;
}
addDropdown(_cb: (dropdown: DropdownComponent) => any): this {
return this;
}
}
export class TextComponent {
inputEl: HTMLInputElement;
constructor() {
this.inputEl = document.createElement('input');
}
setPlaceholder(_placeholder: string): this {
return this;
}
setValue(_value: string): this {
return this;
}
onChange(_cb: (value: string) => void): this {
return this;
}
}
export class ButtonComponent {
buttonEl: HTMLButtonElement;
constructor() {
this.buttonEl = document.createElement('button');
}
setButtonText(_text: string): this {
return this;
}
setCta(): this {
return this;
}
onClick(_cb: () => void): this {
return this;
}
}
export class DropdownComponent {
selectEl: HTMLSelectElement;
constructor() {
this.selectEl = document.createElement('select');
}
addOption(_value: string, _display: string): this {
return this;
}
setValue(_value: string): this {
return this;
}
onChange(_cb: (value: string) => void): this {
return this;
}
}
export class Notice {
constructor(_message: string) {}
}
export function requestUrl(params: any): Promise<any> {
return Promise.resolve({ status: 200, json: {}, text: '' });
}

View file

@ -1,175 +0,0 @@
import { describe, it, expect, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
interface TestConnection {
name: string;
jiraUrl: string;
apiVersion: '2' | '3';
authMethod: 'bearer' | 'basic';
apiToken: string;
email?: string;
}
interface TestConfig {
connections: TestConnection[];
}
function loadConfig(): TestConfig | null {
try {
const configPath = path.resolve(__dirname, '../../test-config.local.json');
if (!fs.existsSync(configPath)) {
return null;
}
const raw = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(raw);
} catch {
return null;
}
}
const config = loadConfig();
const runIntegration = !!config && process.env.RUN_INTEGRATION_TESTS === 'true';
async function mockBaseRequest(conn: TestConnection, method: string, urlPath: string, body?: string): Promise<any> {
const url = `${conn.jiraUrl}/rest/api/${conn.apiVersion}${urlPath}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (conn.authMethod === 'bearer') {
headers['Authorization'] = `Bearer ${conn.apiToken}`;
} else if (conn.authMethod === 'basic' && conn.email) {
const encoded = Buffer.from(`${conn.email}:${conn.apiToken}`).toString('base64');
headers['Authorization'] = `Basic ${encoded}`;
}
const fetchArgs: RequestInit = {
method,
headers,
body: body || undefined,
};
const response = await fetch(url, fetchArgs);
const json = await response.json();
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(json)}`);
}
return json;
}
async function findExistingIssue(conn: TestConnection): Promise<string | null> {
try {
const result = await mockBaseRequest(
conn,
'POST',
'/search',
JSON.stringify({ jql: '', maxResults: 1, fields: ['key'] }),
);
if (result.issues && result.issues.length > 0) {
return result.issues[0].key;
}
return null;
} catch {
return null;
}
}
if (runIntegration) {
describe.each(config!.connections)('Integration: $name ($apiVersion)', (conn) => {
let existingIssueKey: string | null;
beforeAll(async () => {
existingIssueKey = await findExistingIssue(conn);
});
it('fetches server info (self)', async () => {
const result = await mockBaseRequest(conn, 'GET', '/serverInfo');
expect(result).toHaveProperty('baseUrl');
expect(result).toHaveProperty('version');
});
it('searches issues with empty JQL', async () => {
const result = await mockBaseRequest(
conn,
'POST',
'/search',
JSON.stringify({ jql: '', maxResults: 1, fields: ['key', 'summary'] }),
);
expect(result).toHaveProperty('issues');
expect(Array.isArray(result.issues)).toBe(true);
if (result.issues.length > 0) {
expect(result.issues[0]).toHaveProperty('key');
expect(result.issues[0]).toHaveProperty('fields');
}
});
it('fetches a specific issue by key', async () => {
if (!existingIssueKey) {
console.warn('No existing issues found, skipping test');
return;
}
const result = await mockBaseRequest(conn, 'GET', `/issue/${existingIssueKey}`);
expect(result).toHaveProperty('key', existingIssueKey);
expect(result).toHaveProperty('fields');
expect(result.fields).toHaveProperty('summary');
});
it('searches by project JQL', async () => {
if (!existingIssueKey) {
console.warn('No existing issues found, skipping test');
return;
}
const projectKey = existingIssueKey.split('-')[0];
const result = await mockBaseRequest(
conn,
'POST',
'/search',
JSON.stringify({ jql: `project = ${projectKey}`, maxResults: 3, fields: ['key', 'summary'] }),
);
expect(result).toHaveProperty('issues');
expect(result.issues.length).toBeGreaterThan(0);
for (const issue of result.issues) {
expect(issue.key).toMatch(new RegExp(`^${projectKey}-`));
}
});
it('fetches issue transitions', async () => {
if (!existingIssueKey) {
console.warn('No existing issues found, skipping test');
return;
}
const result = await mockBaseRequest(conn, 'GET', `/issue/${existingIssueKey}/transitions`);
expect(result).toHaveProperty('transitions');
expect(Array.isArray(result.transitions)).toBe(true);
});
if (conn.apiVersion === '3') {
it('uses approximate count endpoint (v3 specific)', async () => {
const result = await mockBaseRequest(
conn,
'POST',
'/search/approximate-count',
JSON.stringify({ jql: '' }),
);
expect(result).toHaveProperty('count');
expect(typeof result.count).toBe('number');
});
}
});
} else {
describe('Integration tests', () => {
it('are skipped unless RUN_INTEGRATION_TESTS=true and tests/test-config.local.json exists', () => {
if (config) {
console.info('Test config found. Set RUN_INTEGRATION_TESTS=true to run integration tests.');
} else {
console.info(
'No test config found. Create tests/test-config.local.json (see test-config.example.json).',
);
}
expect(true).toBe(true);
});
});
}

View file

@ -1,75 +0,0 @@
import { describe, it, expect } from 'vitest';
import { sanitizeFileName } from '../../../src/tools/sanitizers';
function generateFilenameFromTemplate(template: string, issue: { key: string; fields: { summary?: string } }): string {
let filename = template;
const summary = issue.fields?.summary || '';
const sanitizedSummary = sanitizeFileName(summary);
filename = filename.replace(/\{summary\}/g, sanitizedSummary);
const key = issue.key || '';
filename = filename.replace(/\{key\}/g, key);
filename = sanitizeFileName(filename);
if (!filename || filename.trim() === '') {
if (key) {
filename = key;
} else if (sanitizedSummary) {
filename = sanitizedSummary;
} else {
filename = 'jira-issue';
}
}
return filename.trim();
}
describe('generateFilenameFromTemplate', () => {
it('generates filename with summary and key default template', () => {
const issue = { key: 'PROJ-123', fields: { summary: 'Fix login bug' } };
const result = generateFilenameFromTemplate('{summary} ({key})', issue);
expect(result).toBe('Fix login bug (PROJ-123)');
});
it('handles template with only key', () => {
const issue = { key: 'PROJ-123', fields: { summary: 'Fix login bug' } };
const result = generateFilenameFromTemplate('{key}', issue);
expect(result).toBe('PROJ-123');
});
it('handles template with only summary', () => {
const issue = { key: 'PROJ-123', fields: { summary: 'Fix login bug' } };
const result = generateFilenameFromTemplate('{summary}', issue);
expect(result).toBe('Fix login bug');
});
it('sanitizes summary in filename', () => {
const issue = { key: 'PROJ-1', fields: { summary: 'bug: fix "critical" issue?' } };
const result = generateFilenameFromTemplate('{summary}', issue);
expect(result).not.toContain(':');
expect(result).not.toContain('"');
expect(result).not.toContain('?');
expect(result).toContain('-');
});
it('falls back to key when summary is empty but template has non-template text', () => {
const issue = { key: 'PROJ-1', fields: { summary: '' } };
const result = generateFilenameFromTemplate('{summary}', issue);
expect(result).toBe('PROJ-1');
});
it('falls back to jira-issue when both key and summary are empty', () => {
const issue = { key: '', fields: { summary: '' } };
const result = generateFilenameFromTemplate('{summary}', issue);
expect(result).toBe('jira-issue');
});
it('preserves custom template structure', () => {
const issue = { key: 'PROJ-42', fields: { summary: 'My Task' } };
const result = generateFilenameFromTemplate('{key}-{summary}', issue);
expect(result).toBe('PROJ-42-My Task');
});
it('handles missing fields gracefully by showing non-template parts', () => {
const issue = { key: 'PROJ-1', fields: {} };
const result = generateFilenameFromTemplate('{summary} ({key})', issue);
expect(result).toBe('(PROJ-1)');
});
});

View file

@ -1,55 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { useTranslations, t } from '../../../src/localization/translator';
describe('translator', () => {
const originalLanguage = window.localStorage.getItem('language');
afterEach(() => {
if (originalLanguage) {
window.localStorage.setItem('language', originalLanguage);
} else {
window.localStorage.removeItem('language');
}
});
describe('useTranslations', () => {
it('returns translate function with correct prefix', () => {
const { t: translate } = useTranslations('modals.jql_search');
const result = translate('desc');
expect(result).toBe('Search issues by JQL query');
});
it('returns locale string', () => {
const { locale } = useTranslations();
expect(locale).toBe('en');
});
it('returns correct locale when language is set', () => {
// ru locale may not be compiled; test with en
const { locale } = useTranslations();
expect(locale).toBe('en');
});
});
describe('t', () => {
it('returns translation for valid key with prefix', () => {
const result = t('jql.name', 'modals.jql_search');
expect(result).toBe('JQL Query');
});
it('returns translation key when not found', () => {
const result = t('nonexistent.key', 'some_prefix');
expect(result).toBe('some_prefix.nonexistent.key');
});
it('replaces placeholders in translation', () => {
const result = t('total', 'modals.jql_search.preview', { total: '42' });
expect(result).toBe('Total 42 issues found');
});
it('returns command translation', () => {
const result = t('name', 'commands.batch_fetch_issues');
expect(result).toBe('Batch Fetch Issues by JQL');
});
});
});

View file

@ -1,4 +0,0 @@
import { vi } from 'vitest';
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => setTimeout(cb, 0));
vi.stubGlobal('cancelAnimationFrame', (id: number) => clearTimeout(id));

View file

@ -1,62 +0,0 @@
import { describe, it, expect } from 'vitest';
import { validateFunctionString } from '../../../src/tools/convertFunctionString';
describe('validateFunctionString', () => {
it('accepts empty string', async () => {
const result = await validateFunctionString('');
expect(result.isValid).toBe(true);
});
it('accepts whitespace-only string', async () => {
const result = await validateFunctionString(' ');
expect(result.isValid).toBe(true);
});
it('rejects document reference', async () => {
const result = await validateFunctionString('document.title');
expect(result.isValid).toBe(false);
expect(result.errorMessage).toContain('document');
});
it('rejects window reference', async () => {
const result = await validateFunctionString('window.location');
expect(result.isValid).toBe(false);
expect(result.errorMessage).toContain('window');
});
it('rejects eval reference', async () => {
const result = await validateFunctionString('eval("1+1")');
expect(result.isValid).toBe(false);
expect(result.errorMessage).toContain('eval');
});
it('rejects Function reference', async () => {
const result = await validateFunctionString('Function("return 1")');
expect(result.isValid).toBe(false);
expect(result.errorMessage).toContain('Function');
});
it('rejects fetch reference', async () => {
const result = await validateFunctionString('fetch("/api")');
expect(result.isValid).toBe(false);
expect(result.errorMessage).toContain('fetch');
});
it('accepts simple property access', async () => {
const result = await validateFunctionString('issue.fields.summary', ['issue']);
expect(result.isValid).toBe(true);
});
it('accepts simple expressions', async () => {
const result = await validateFunctionString('value + "!"', ['value']);
expect(result.isValid).toBe(true);
});
it('accepts arrow function syntax', async () => {
const result = await validateFunctionString('(issue, api_version) => issue.fields.summary', [
'issue',
'api_version',
]);
expect(result.isValid).toBe(true);
});
});

View file

@ -1,56 +0,0 @@
import { describe, it, expect } from 'vitest';
import { sanitizeFileName } from '../../../src/tools/sanitizers';
describe('sanitizeFileName', () => {
it('replaces backslash with dash', () => {
expect(sanitizeFileName('foo\\bar')).toBe('foo-bar');
});
it('replaces forward slash with dash', () => {
expect(sanitizeFileName('foo/bar')).toBe('foo-bar');
});
it('replaces colon with dash', () => {
expect(sanitizeFileName('foo: bar')).toBe('foo- bar');
});
it('replaces asterisk with dash', () => {
expect(sanitizeFileName('foo*bar')).toBe('foo-bar');
});
it('replaces question mark with dash', () => {
expect(sanitizeFileName('foo?bar')).toBe('foo-bar');
});
it('replaces double quote with dash', () => {
expect(sanitizeFileName('foo"bar')).toBe('foo-bar');
});
it('replaces left angle bracket with dash', () => {
expect(sanitizeFileName('foo<bar')).toBe('foo-bar');
});
it('replaces right angle bracket with dash', () => {
expect(sanitizeFileName('foo>bar')).toBe('foo-bar');
});
it('replaces pipe with dash', () => {
expect(sanitizeFileName('foo|bar')).toBe('foo-bar');
});
it('replaces all invalid characters', () => {
expect(sanitizeFileName('a<b>c"d:e/f\\g*h?i|j')).toBe('a-b-c-d-e-f-g-h-i-j');
});
it('returns empty string for empty input', () => {
expect(sanitizeFileName('')).toBe('');
});
it('preserves valid characters', () => {
expect(sanitizeFileName('hello-world 123')).toBe('hello-world 123');
});
it('handles unicode characters', () => {
expect(sanitizeFileName('привет мир')).toBe('привет мир');
});
});

View file

@ -1,112 +0,0 @@
import { describe, it, expect } from 'vitest';
import {
parseFileContent,
extractAllJiraSyncValuesFromContent,
updateJiraSyncContent,
} from '../../../src/tools/sectionTools';
describe('parseFileContent', () => {
it('parses a sync section (stops at next heading)', () => {
const content = '`jira-sync-section-status`\nIn Progress\n\n## Next Section\nmore text';
const blocks = parseFileContent(content);
expect(blocks).toHaveLength(1);
expect(blocks[0].type).toBe('section');
expect(blocks[0].name).toBe('status');
expect(blocks[0].content).toBe('In Progress');
});
it('parses a sync section without heading captures everything', () => {
const content = '`jira-sync-section-status`\nIn Progress\n\nmore text';
const blocks = parseFileContent(content);
expect(blocks[0].type).toBe('section');
expect(blocks[0].content).toContain('In Progress');
expect(blocks[0].content).toContain('more text');
});
it('parses a sync line (captures rest of line)', () => {
const content = 'text `jira-sync-line-assignee`johndoe more text';
const blocks = parseFileContent(content);
expect(blocks[0].type).toBe('line');
expect(blocks[0].name).toBe('assignee');
expect(blocks[0].content).toBe('johndoe more text');
});
it('parses an inline block', () => {
const content = 'hello `jira-sync-inline-start-summary`my issue`jira-sync-end` world';
const blocks = parseFileContent(content);
expect(blocks).toHaveLength(1);
expect(blocks[0].type).toBe('inline');
expect(blocks[0].name).toBe('summary');
expect(blocks[0].content).toBe('my issue');
});
it('parses a block', () => {
const content = 'before\n`jira-sync-block-start-description`\nline1\nline2\n`jira-sync-end`\nafter';
const blocks = parseFileContent(content);
expect(blocks).toHaveLength(1);
expect(blocks[0].type).toBe('block');
expect(blocks[0].name).toBe('description');
expect(blocks[0].content).toBe('line1\nline2');
});
it('returns empty array for content without sync markers', () => {
const content = 'just plain text\nno markers here';
const blocks = parseFileContent(content);
expect(blocks).toHaveLength(0);
});
it('parses multiple blocks of different types', () => {
const content = [
'`jira-sync-section-status`',
'Done',
'',
'`jira-sync-line-assignee`john',
'`jira-sync-inline-start-priority`High`jira-sync-end`',
].join('\n');
const blocks = parseFileContent(content);
expect(blocks).toHaveLength(3);
});
});
describe('extractAllJiraSyncValuesFromContent', () => {
it('extracts values into a dictionary', () => {
const content = '`jira-sync-section-status`\nDone\n`jira-sync-line-assignee`john';
const result = extractAllJiraSyncValuesFromContent(content);
expect(result).toEqual({ status: 'Done', assignee: 'john' });
});
it('returns empty object for content without sync markers', () => {
const result = extractAllJiraSyncValuesFromContent('plain text');
expect(result).toEqual({});
});
});
describe('updateJiraSyncContent', () => {
it('updates a section with new content', () => {
const content = '`jira-sync-section-status`\nIn Progress';
const result = updateJiraSyncContent(content, { status: 'Done' });
expect(result).toContain('Done');
});
it('does not modify content for unknown field names', () => {
const content = '`jira-sync-section-status`\nIn Progress';
const result = updateJiraSyncContent(content, { assignee: 'john' });
expect(result).toBe(content);
});
it('updates only the specified fields', () => {
const content = ['`jira-sync-section-status`', 'In Progress', '`jira-sync-line-assignee`jane'].join('\n');
const result = updateJiraSyncContent(content, { status: 'Done' });
expect(result).toContain('Done');
expect(result).toContain('jane');
});
it('preserves content after section when a heading boundary is present', () => {
const content = 'some header\n\n`jira-sync-section-status`\nold value\n\n## Next\nfooter';
const result = updateJiraSyncContent(content, { status: 'new value' });
expect(result).toContain('`jira-sync-section-status`');
expect(result).toContain('new value');
expect(result).toContain('some header');
expect(result).toContain('footer');
});
});

View file

@ -1,19 +0,0 @@
{
"connections": [
{
"name": "Test Jira v2",
"jiraUrl": "http://jira-test.local:8080",
"apiVersion": "2",
"authMethod": "bearer",
"apiToken": ""
},
{
"name": "Test Jira v3",
"jiraUrl": "https://your-domain.atlassian.net",
"apiVersion": "3",
"authMethod": "basic",
"apiToken": "",
"email": ""
}
]
}

View file

@ -1,15 +0,0 @@
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
include: ['tests/__tests__/**/*.test.ts'],
environment: 'jsdom',
setupFiles: ['tests/__tests__/setup.ts'],
},
resolve: {
alias: {
obsidian: path.resolve(__dirname, '__mocks__/obsidian.ts'),
},
},
});

View file

@ -14,6 +14,5 @@
"lib": ["DOM", "ES2017", "ES2018", "ES2019", "ES2020"],
"types": ["node"]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
"exclude": ["tests/", "node_modules/"]
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
}

View file

@ -1,3 +1,3 @@
{
"1.0.0": "1.7.4"
"1.0.0": "1.7.7"
}

750
yarn.lock
View file

@ -2,45 +2,6 @@
# yarn lockfile v1
"@asamuzakjp/css-color@^5.1.11":
version "5.1.11"
resolved "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz#28a0aac8220a4cc19045ac3bd9a813d4060bd375"
integrity sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==
dependencies:
"@asamuzakjp/generational-cache" "^1.0.1"
"@csstools/css-calc" "^3.2.0"
"@csstools/css-color-parser" "^4.1.0"
"@csstools/css-parser-algorithms" "^4.0.0"
"@csstools/css-tokenizer" "^4.0.0"
"@asamuzakjp/dom-selector@^7.1.1":
version "7.1.1"
resolved "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz#01880086bb2490098f167beb58555da1a6c9adbd"
integrity sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==
dependencies:
"@asamuzakjp/generational-cache" "^1.0.1"
"@asamuzakjp/nwsapi" "^2.3.9"
bidi-js "^1.0.3"
css-tree "^3.2.1"
is-potential-custom-element-name "^1.0.1"
"@asamuzakjp/generational-cache@^1.0.1":
version "1.0.1"
resolved "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz#3d0bf6be4fc059851390a7070720c6007af793ec"
integrity sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==
"@asamuzakjp/nwsapi@^2.3.9":
version "2.3.9"
resolved "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz#ad5549322dfe9d153d4b4dd6f7ff2ae234b06e24"
integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==
"@bramus/specificity@^2.4.2":
version "2.4.2"
resolved "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz#aa8db8eb173fdee7324f82284833106adeecc648"
integrity sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==
dependencies:
css-tree "^3.0.0"
"@codemirror/autocomplete@^6.0.0":
version "6.20.1"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz#4cfbc8b2e1e25f890ec34a081037e58b4e44143e"
@ -109,61 +70,6 @@
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@csstools/color-helpers@^6.0.2":
version "6.0.2"
resolved "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz#82c59fd30649cf0b4d3c82160489748666e6550b"
integrity sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==
"@csstools/css-calc@^3.2.0", "@csstools/css-calc@^3.2.1":
version "3.2.1"
resolved "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.2.1.tgz#b30e061ca9f297ccb2b3b032bfee32fda02b1b27"
integrity sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==
"@csstools/css-color-parser@^4.1.0":
version "4.1.8"
resolved "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz#54c156703a4072a23b0f5e53915ce3479f4d29ec"
integrity sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==
dependencies:
"@csstools/color-helpers" "^6.0.2"
"@csstools/css-calc" "^3.2.1"
"@csstools/css-parser-algorithms@^4.0.0":
version "4.0.0"
resolved "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz#e1c65dc09378b42f26a111fca7f7075fc2c26164"
integrity sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==
"@csstools/css-syntax-patches-for-csstree@^1.1.3":
version "1.1.5"
resolved "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz#b8e26e0fe25e9a6ec607d045470cc46d2f62731e"
integrity sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==
"@csstools/css-tokenizer@^4.0.0":
version "4.0.0"
resolved "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz#798a33950d11226a0ebb6acafa60f5594424967f"
integrity sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==
"@emnapi/core@1.11.1":
version "1.11.1"
resolved "https://registry.npmmirror.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6"
integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==
dependencies:
"@emnapi/wasi-threads" "1.2.2"
tslib "^2.4.0"
"@emnapi/runtime@1.11.1":
version "1.11.1"
resolved "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24"
integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==
dependencies:
tslib "^2.4.0"
"@emnapi/wasi-threads@1.2.2":
version "1.2.2"
resolved "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz#4c93becf5bfa3b13d1bbdcc06aee38321ad8139a"
integrity sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==
dependencies:
tslib "^2.4.0"
"@esbuild/aix-ppc64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64"
@ -342,11 +248,6 @@
"@eslint/core" "^1.2.1"
levn "^0.4.1"
"@exodus/bytes@^1.11.0", "@exodus/bytes@^1.15.0", "@exodus/bytes@^1.6.0":
version "1.15.1"
resolved "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz#b13bc464ca162c17abf0837fb3a11aeab79e45d1"
integrity sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==
"@humanfs/core@^0.19.2":
version "0.19.2"
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.2.tgz#a8272ca03b2acf492670222b2320b6c421bfde60"
@ -378,11 +279,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
"@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@lezer/common@^1.0.0", "@lezer/common@^1.2.0", "@lezer/common@^1.3.0", "@lezer/common@^1.5.0":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.5.2.tgz#d6840db13779e3f1b42e70c9a97c4086d12fae22"
@ -416,122 +312,6 @@
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@napi-rs/wasm-runtime@^1.1.6":
version "1.1.6"
resolved "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz#ed33806d0f9be98dc76d0c3d4fd872fda701b5d5"
integrity sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==
dependencies:
"@tybys/wasm-util" "^0.10.3"
"@oxc-project/types@=0.137.0":
version "0.137.0"
resolved "https://registry.npmmirror.com/@oxc-project/types/-/types-0.137.0.tgz#56e77f8bb221fa05f18b1cd34d73f94f0954a773"
integrity sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==
"@rolldown/binding-android-arm64@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz#cc6153029c3d9afc9caaae2dc362d899ae94ac4f"
integrity sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==
"@rolldown/binding-darwin-arm64@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz#3af681a5d7610340257b3ac7753353b23e884765"
integrity sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==
"@rolldown/binding-darwin-x64@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz#80ada35e9f35efb7e48a887444ce2052f615d645"
integrity sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==
"@rolldown/binding-freebsd-x64@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz#65b2bbb82f005f08aeeff0b6d81e19be68360201"
integrity sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==
"@rolldown/binding-linux-arm-gnueabihf@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz#7e8d34ad0c7bcfd3baed268e9798571e3888ca71"
integrity sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==
"@rolldown/binding-linux-arm64-gnu@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz#52bbf400ff219bda1e56c042160d96deb08bfecc"
integrity sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==
"@rolldown/binding-linux-arm64-musl@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz#9e82899186f73329f3d8155fa1618ae2e86ffa2a"
integrity sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==
"@rolldown/binding-linux-ppc64-gnu@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz#050520177316586ccad816eb466ea11015e17ba7"
integrity sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==
"@rolldown/binding-linux-s390x-gnu@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz#50efa7b20219c6e31235fded0fd3427f36123e5a"
integrity sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==
"@rolldown/binding-linux-x64-gnu@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz#c5973445113dff50d4077d0edaa4b8a69533dc6f"
integrity sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==
"@rolldown/binding-linux-x64-musl@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz#ddb023f7fc98ccbb8c1b683545216ca7b4e7ebdd"
integrity sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==
"@rolldown/binding-openharmony-arm64@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz#832a6da3472722427c73d178c75681858b76aeed"
integrity sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==
"@rolldown/binding-wasm32-wasi@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz#256cdcc06ad9ada611606526f319642fc0830b0f"
integrity sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==
dependencies:
"@emnapi/core" "1.11.1"
"@emnapi/runtime" "1.11.1"
"@napi-rs/wasm-runtime" "^1.1.6"
"@rolldown/binding-win32-arm64-msvc@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz#e735c7024a5e17ebaf13689112fa0bdfc6886c38"
integrity sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==
"@rolldown/binding-win32-x64-msvc@1.1.3":
version "1.1.3"
resolved "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz#f06c09db5c8ad4b6904b4d406c9b6f17f392b5c6"
integrity sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==
"@rolldown/pluginutils@^1.0.0":
version "1.0.1"
resolved "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be"
integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==
"@standard-schema/spec@^1.1.0":
version "1.1.0"
resolved "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
"@tybys/wasm-util@^0.10.3":
version "0.10.3"
resolved "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d"
integrity sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==
dependencies:
tslib "^2.4.0"
"@types/chai@^5.2.2":
version "5.2.3"
resolved "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a"
integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==
dependencies:
"@types/deep-eql" "*"
assertion-error "^2.0.1"
"@types/codemirror@5.60.8":
version "5.60.8"
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.8.tgz#b647d04b470e8e1836dd84b2879988fc55c9de68"
@ -539,11 +319,6 @@
dependencies:
"@types/tern" "*"
"@types/deep-eql@*":
version "4.0.2"
resolved "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd"
integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==
"@types/esrecurse@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec"
@ -554,11 +329,6 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/estree@^1.0.0":
version "1.0.9"
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24"
integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@ -569,12 +339,10 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.24.tgz#4ae334fc62c0e915ca8ed8e35dcc6d4eeb29215f"
integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==
"@types/node@^20.0.0":
version "20.19.43"
resolved "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz#fcecf580ba42a0db55cf404c372c97973c376c97"
integrity sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==
dependencies:
undici-types "~6.21.0"
"@types/node@^16.11.6":
version "16.18.126"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.126.tgz#27875faa2926c0f475b39a8bb1e546c0176f8d4b"
integrity sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==
"@types/tern@*":
version "0.23.9"
@ -679,66 +447,6 @@
"@typescript-eslint/types" "8.59.1"
eslint-visitor-keys "^5.0.0"
"@vitest/expect@4.1.9":
version "4.1.9"
resolved "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.9.tgz#ba1af73ae53262e3dc9b518cb7b76fb614e0ef53"
integrity sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==
dependencies:
"@standard-schema/spec" "^1.1.0"
"@types/chai" "^5.2.2"
"@vitest/spy" "4.1.9"
"@vitest/utils" "4.1.9"
chai "^6.2.2"
tinyrainbow "^3.1.0"
"@vitest/mocker@4.1.9":
version "4.1.9"
resolved "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.9.tgz#a483de79b358aba3dd8f319a0d8ab17c89f5c75d"
integrity sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==
dependencies:
"@vitest/spy" "4.1.9"
estree-walker "^3.0.3"
magic-string "^0.30.21"
"@vitest/pretty-format@4.1.9":
version "4.1.9"
resolved "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.9.tgz#885cfe9fcb6ff3df4409ea66192cc1fb23d62fae"
integrity sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==
dependencies:
tinyrainbow "^3.1.0"
"@vitest/runner@4.1.9":
version "4.1.9"
resolved "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.9.tgz#bb742947ce4841dfb2d8984a2f9014850be10f51"
integrity sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==
dependencies:
"@vitest/utils" "4.1.9"
pathe "^2.0.3"
"@vitest/snapshot@4.1.9":
version "4.1.9"
resolved "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.9.tgz#bdfb670ae5617613ea8776e93d0666a66defeeb7"
integrity sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==
dependencies:
"@vitest/pretty-format" "4.1.9"
"@vitest/utils" "4.1.9"
magic-string "^0.30.21"
pathe "^2.0.3"
"@vitest/spy@4.1.9":
version "4.1.9"
resolved "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.9.tgz#bfc40d48fb9bd1a1228bfbfde7f5555e7f6b3867"
integrity sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==
"@vitest/utils@4.1.9":
version "4.1.9"
resolved "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.9.tgz#0184c7e6eb3234739b2b6b3b985f78d1ed823ee1"
integrity sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==
dependencies:
"@vitest/pretty-format" "4.1.9"
convert-source-map "^2.0.0"
tinyrainbow "^3.1.0"
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -771,23 +479,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
assertion-error@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
balanced-match@^4.0.2:
version "4.0.4"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
bidi-js@^1.0.3:
version "1.0.3"
resolved "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2"
integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==
dependencies:
require-from-string "^2.0.2"
brace-expansion@^5.0.5:
version "5.0.5"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb"
@ -800,11 +496,6 @@ builtin-modules@3.3.0:
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
chai@^6.2.2:
version "6.2.2"
resolved "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
chalk@4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@ -853,11 +544,6 @@ concurrently@^9.2.1:
tree-kill "1.2.2"
yargs "17.7.2"
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
crelt@^1.0.5, crelt@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
@ -872,22 +558,6 @@ cross-spawn@^7.0.6:
shebang-command "^2.0.0"
which "^2.0.1"
css-tree@^3.0.0, css-tree@^3.2.1:
version "3.2.1"
resolved "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz#86cac7011561272b30e6b1e042ba6ce047aa7518"
integrity sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==
dependencies:
mdn-data "2.27.1"
source-map-js "^1.2.1"
data-urls@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz#6dce8b63226a1ecfdd907ce18a8ccfb1eee506d3"
integrity sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==
dependencies:
whatwg-mimetype "^5.0.0"
whatwg-url "^16.0.0"
debug@^4.3.1, debug@^4.3.2, debug@^4.4.3:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
@ -895,36 +565,16 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.4.3:
dependencies:
ms "^2.1.3"
decimal.js@^10.6.0:
version "10.6.0"
resolved "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
deep-is@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
detect-libc@^2.0.3:
version "2.1.2"
resolved "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
entities@^8.0.0:
version "8.0.0"
resolved "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz#c1df5fe3602429747fa233d0dd26f142f0ce4743"
integrity sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==
es-module-lexer@^2.0.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz#1dfcbb5ea3bbfb63f28e1fc3676c3676d1c9624c"
integrity sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==
esbuild@0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.0.tgz#0de1787a77206c5a79eeb634a623d39b5006ce92"
@ -1050,23 +700,11 @@ estraverse@^5.1.0, estraverse@^5.2.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^3.0.3:
version "3.0.3"
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
dependencies:
"@types/estree" "^1.0.0"
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
expect-type@^1.3.0:
version "1.3.0"
resolved "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68"
integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -1124,11 +762,6 @@ fs-extra@^11.3.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
@ -1161,13 +794,6 @@ highlight.js@^11.11.1:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.11.1.tgz#fca06fa0e5aeecf6c4d437239135fabc15213585"
integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==
html-encoding-sniffer@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz#f8d9390b3b348b50d4f61c16dd2ef5c05980a882"
integrity sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==
dependencies:
"@exodus/bytes" "^1.6.0"
husky@^9.1.7:
version "9.1.7"
resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d"
@ -1205,43 +831,11 @@ is-glob@^4.0.0, is-glob@^4.0.3:
dependencies:
is-extglob "^2.1.1"
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jsdom@^29.1.1:
version "29.1.1"
resolved "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz#5b9704906f3cd510c34aa941ae2f8f7f8179df01"
integrity sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==
dependencies:
"@asamuzakjp/css-color" "^5.1.11"
"@asamuzakjp/dom-selector" "^7.1.1"
"@bramus/specificity" "^2.4.2"
"@csstools/css-syntax-patches-for-csstree" "^1.1.3"
"@exodus/bytes" "^1.15.0"
css-tree "^3.2.1"
data-urls "^7.0.0"
decimal.js "^10.6.0"
html-encoding-sniffer "^6.0.0"
is-potential-custom-element-name "^1.0.1"
lru-cache "^11.3.5"
parse5 "^8.0.1"
saxes "^6.0.0"
symbol-tree "^3.2.4"
tough-cookie "^6.0.1"
undici "^7.25.0"
w3c-xmlserializer "^5.0.0"
webidl-conversions "^8.0.1"
whatwg-mimetype "^5.0.0"
whatwg-url "^16.0.1"
xml-name-validator "^5.0.0"
json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
@ -1281,80 +875,6 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lightningcss-android-arm64@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==
lightningcss-darwin-arm64@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5"
integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==
lightningcss-darwin-x64@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e"
integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==
lightningcss-freebsd-x64@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575"
integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==
lightningcss-linux-arm-gnueabihf@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d"
integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==
lightningcss-linux-arm64-gnu@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335"
integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==
lightningcss-linux-arm64-musl@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133"
integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==
lightningcss-linux-x64-gnu@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6"
integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==
lightningcss-linux-x64-musl@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b"
integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==
lightningcss-win32-arm64-msvc@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38"
integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==
lightningcss-win32-x64-msvc@1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a"
integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==
lightningcss@^1.32.0:
version "1.32.0"
resolved "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9"
integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==
dependencies:
detect-libc "^2.0.3"
optionalDependencies:
lightningcss-android-arm64 "1.32.0"
lightningcss-darwin-arm64 "1.32.0"
lightningcss-darwin-x64 "1.32.0"
lightningcss-freebsd-x64 "1.32.0"
lightningcss-linux-arm-gnueabihf "1.32.0"
lightningcss-linux-arm64-gnu "1.32.0"
lightningcss-linux-arm64-musl "1.32.0"
lightningcss-linux-x64-gnu "1.32.0"
lightningcss-linux-x64-musl "1.32.0"
lightningcss-win32-arm64-msvc "1.32.0"
lightningcss-win32-x64-msvc "1.32.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
@ -1367,23 +887,6 @@ lodash@^4.17.23:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
lru-cache@^11.3.5:
version "11.5.1"
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz#f3daa3540847b9737ebc02499ddb36765e54db4a"
integrity sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==
magic-string@^0.30.21:
version "0.30.21"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
mdn-data@2.27.1:
version "2.27.1"
resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz#e37b9c50880b75366c4d40ac63d9bbcacdb61f0e"
integrity sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==
minimatch@^10.2.2, minimatch@^10.2.4:
version "10.2.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1"
@ -1401,11 +904,6 @@ ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.3.12:
version "3.3.15"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz#36c490fad8c6e86c824c940dfdde999b69ed4316"
integrity sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -1419,11 +917,6 @@ obsidian@latest:
"@types/codemirror" "5.60.8"
moment "2.29.4"
obug@^2.1.1:
version "2.1.3"
resolved "https://registry.npmmirror.com/obug/-/obug-2.1.3.tgz#c02c60f95abd603409330e767db7f2823193331e"
integrity sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==
optionator@^0.9.3:
version "0.9.4"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
@ -1450,13 +943,6 @@ p-locate@^5.0.0:
dependencies:
p-limit "^3.0.2"
parse5@^8.0.1:
version "8.0.1"
resolved "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz#f43bcd2cd683efe084075333e9ce0da7d06da31e"
integrity sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==
dependencies:
entities "^8.0.0"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -1467,30 +953,11 @@ path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
pathe@^2.0.3:
version "2.0.3"
resolved "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^4.0.3, picomatch@^4.0.4:
picomatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
postcss@^8.5.15:
version "8.5.15"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c"
integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==
dependencies:
nanoid "^3.3.12"
picocolors "^1.1.1"
source-map-js "^1.2.1"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -1501,7 +968,7 @@ prettier@^3.8.3:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0"
integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==
punycode@^2.1.0, punycode@^2.3.1:
punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@ -1516,35 +983,6 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
rolldown@~1.1.2:
version "1.1.3"
resolved "https://registry.npmmirror.com/rolldown/-/rolldown-1.1.3.tgz#87072bfd0d1bdd02a66076a261a62e8e49b3f0e2"
integrity sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==
dependencies:
"@oxc-project/types" "=0.137.0"
"@rolldown/pluginutils" "^1.0.0"
optionalDependencies:
"@rolldown/binding-android-arm64" "1.1.3"
"@rolldown/binding-darwin-arm64" "1.1.3"
"@rolldown/binding-darwin-x64" "1.1.3"
"@rolldown/binding-freebsd-x64" "1.1.3"
"@rolldown/binding-linux-arm-gnueabihf" "1.1.3"
"@rolldown/binding-linux-arm64-gnu" "1.1.3"
"@rolldown/binding-linux-arm64-musl" "1.1.3"
"@rolldown/binding-linux-ppc64-gnu" "1.1.3"
"@rolldown/binding-linux-s390x-gnu" "1.1.3"
"@rolldown/binding-linux-x64-gnu" "1.1.3"
"@rolldown/binding-linux-x64-musl" "1.1.3"
"@rolldown/binding-openharmony-arm64" "1.1.3"
"@rolldown/binding-wasm32-wasi" "1.1.3"
"@rolldown/binding-win32-arm64-msvc" "1.1.3"
"@rolldown/binding-win32-x64-msvc" "1.1.3"
rxjs@7.8.2:
version "7.8.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b"
@ -1552,13 +990,6 @@ rxjs@7.8.2:
dependencies:
tslib "^2.1.0"
saxes@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
dependencies:
xmlchars "^2.2.0"
semver@^7.7.3:
version "7.7.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
@ -1581,26 +1012,6 @@ shell-quote@1.8.3:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b"
integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
siginfo@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
stackback@0.0.2:
version "0.0.2"
resolved "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
std-env@^4.0.0-rc.1:
version "4.1.0"
resolved "https://registry.npmmirror.com/std-env/-/std-env-4.1.0.tgz#45899abc590d86d682e87f0acd1033a75084cd3f"
integrity sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@ -1636,21 +1047,6 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
tinybench@^2.9.0:
version "2.9.0"
resolved "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
tinyexec@^1.0.2:
version "1.2.4"
resolved "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz#ae45bb2edebda94c70f4ea897e0f1243e470db71"
integrity sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==
tinyglobby@^0.2.15:
version "0.2.16"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"
@ -1659,45 +1055,6 @@ tinyglobby@^0.2.15:
fdir "^6.5.0"
picomatch "^4.0.4"
tinyglobby@^0.2.17:
version "0.2.17"
resolved "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.4"
tinyrainbow@^3.1.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz#1d8a623893f95cf0a2ddb9e5d11150e191409421"
integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==
tldts-core@^7.4.4:
version "7.4.4"
resolved "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.4.tgz#a06de228c70a334e26d1597af7a82376e8588bce"
integrity sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==
tldts@^7.0.5:
version "7.4.4"
resolved "https://registry.npmmirror.com/tldts/-/tldts-7.4.4.tgz#343191348abbff80118732a1a84e90fcf95b29df"
integrity sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==
dependencies:
tldts-core "^7.4.4"
tough-cookie@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz#a495f833836609ed983c19bc65639cfbceb54c76"
integrity sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==
dependencies:
tldts "^7.0.5"
tr46@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz#f5a1ae546a0adb32a277a2278d0d17fa2f9093e6"
integrity sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==
dependencies:
punycode "^2.3.1"
tree-kill@1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
@ -1713,7 +1070,7 @@ tslib@2.4.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tslib@^2.1.0, tslib@^2.4.0:
tslib@^2.1.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@ -1730,16 +1087,6 @@ typescript@^6.0.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21"
integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==
undici-types@~6.21.0:
version "6.21.0"
resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
undici@^7.25.0:
version "7.28.0"
resolved "https://registry.npmmirror.com/undici/-/undici-7.28.0.tgz#97d64564198b285bc281f0e8e29597e3d11fe7ec"
integrity sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
@ -1752,76 +1099,11 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
"vite@^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.1.0"
resolved "https://registry.npmmirror.com/vite/-/vite-8.1.0.tgz#0734dc1a48faeb2bd5f5b16b66dcbfae484fec55"
integrity sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==
dependencies:
lightningcss "^1.32.0"
picomatch "^4.0.4"
postcss "^8.5.15"
rolldown "~1.1.2"
tinyglobby "^0.2.17"
optionalDependencies:
fsevents "~2.3.3"
vitest@^4.1.9:
version "4.1.9"
resolved "https://registry.npmmirror.com/vitest/-/vitest-4.1.9.tgz#98f22fbd70e2a18c4a92bb20624bc92e5dfac5f3"
integrity sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==
dependencies:
"@vitest/expect" "4.1.9"
"@vitest/mocker" "4.1.9"
"@vitest/pretty-format" "4.1.9"
"@vitest/runner" "4.1.9"
"@vitest/snapshot" "4.1.9"
"@vitest/spy" "4.1.9"
"@vitest/utils" "4.1.9"
es-module-lexer "^2.0.0"
expect-type "^1.3.0"
magic-string "^0.30.21"
obug "^2.1.1"
pathe "^2.0.3"
picomatch "^4.0.3"
std-env "^4.0.0-rc.1"
tinybench "^2.9.0"
tinyexec "^1.0.2"
tinyglobby "^0.2.15"
tinyrainbow "^3.1.0"
vite "^6.0.0 || ^7.0.0 || ^8.0.0"
why-is-node-running "^2.3.0"
w3c-keyname@^2.2.4:
version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
w3c-xmlserializer@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==
dependencies:
xml-name-validator "^5.0.0"
webidl-conversions@^8.0.1:
version "8.0.1"
resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz#0657e571fe6f06fcb15ca50ed1fdbcb495cd1686"
integrity sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==
whatwg-mimetype@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz#d8232895dbd527ceaee74efd4162008fb8a8cf48"
integrity sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==
whatwg-url@^16.0.0, whatwg-url@^16.0.1:
version "16.0.1"
resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz#047f7f4bd36ef76b7198c172d1b1cebc66f764dd"
integrity sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==
dependencies:
"@exodus/bytes" "^1.11.0"
tr46 "^6.0.0"
webidl-conversions "^8.0.1"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@ -1829,14 +1111,6 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
why-is-node-running@^2.3.0:
version "2.3.0"
resolved "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04"
integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==
dependencies:
siginfo "^2.0.0"
stackback "0.0.2"
word-wrap@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
@ -1851,16 +1125,6 @@ wrap-ansi@^7.0.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
xml-name-validator@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"