Compare commits

..

19 commits

Author SHA1 Message Date
Alamion
294a18b37f
Update README.md 2026-07-22 14:17:29 +03:00
Alamion
3365e6ba5d
Update README.md 2026-07-22 14:15:44 +03:00
Alamion
a1deb0e95a
fixes:
- changed video format
2026-07-22 14:05:52 +03:00
Alamion
59f863eb97
fixes:
- markdownToAdf refactor
  - readme refactor
  - minor obsidian plugin dev best practices issues fixed
2026-07-22 13:38:15 +03:00
Alamion
f77c6ea3f6
fixes:
- another node dependencies fix
2026-06-25 18:14:48 +03:00
Alamion
57fa0d1873
fixes:
- node dependencies fix
2026-06-25 18:11:01 +03:00
Alamion
2aeb8c1bee
feats:
- presets for JQL
  - rename all cached issues
  - rebuild cache command
  - bunch of vitests
fixes:
  - incorrect migration save/load settings
  - incorrect cache validation before obsidian init
2026-06-25 17:47:22 +03:00
Alamion
3278de8748
Merge pull request #28 from Alamion/testing
A bunch of minor fixes
2026-06-03 18:38:32 +03:00
Alamion
ad06c8f4d8
Fix:
- `jira-sync-block-start-` docs fix
- custom fields functions correct load on startup
- jira values highlight rollback due to contributor dropped feature full realization. (In current state it's too buggy to release)
2026-06-03 18:35:22 +03:00
Alamion
e3a2a01270
Merge remote-tracking branch 'slipstyle/feature/highlight-sync-sections' into testing
# Conflicts:
#	src/postprocessing/livePreview.ts
#	src/postprocessing/reading.ts
#	src/settings/default.ts
2026-05-09 14:59:06 +03:00
slips
d4d9746647 fix: resolve TypeScript errors in createUpdateIssue and markdownToAdf
- Fixed processFrontmatter typo (processFrontMatter)
- Fixed minimalIssue type assertion (as any as JiraIssue)
- Fixed AdfTaskListNode interface duplicates
- Fixed AdfBlockNode type union to use AdfTaskListNode
- Fixed statusMapping.fromJira second parameter (api_version)
2026-05-05 18:01:08 -04:00
slips
f259446b74 fix: return null for undefined inputs in markdown converters
- adfToMarkdown(undefined) now returns null instead of ''
- jiraToMarkdown(undefined) now returns null instead of ''
- Prevents empty strings from passing guard clause and clearing note fields
- Proper semantics: undefined = field not in issue, null = field cleared in Jira
2026-05-05 17:03:36 -04:00
slips
5229b2d1a5 fix: update status only — don't call updateJiraToLocal with partial issue
Passing {fields: {status: {name: ...}}} to updateJiraToLocal caused all
fromJira mappings to run with undefined inputs, producing empty strings
that overwrote description and comments. Now updates only the status
field in frontmatter and sync content markers directly.
2026-05-05 16:52:45 -04:00
slips
5df851934d revert: restore broad marker regex for custom field support 2026-04-27 11:05:29 -04:00
slips
1b400e7e55 fix: tighten marker regex to avoid matching CSS class names in docs 2026-04-27 11:02:18 -04:00
slips
aa6e36ba5d Fix block highlight viewport bug: scan full doc for block ranges 2026-04-27 09:05:58 -04:00
slips
3169273f5e fix: use inset box-shadow for block border to preserve list indentation
border-left + padding-left was overriding Obsidian's list item padding,
causing bullet and ordered list markers to overlap the accent bar. An
inset box-shadow achieves the same visual effect without touching layout.
2026-04-22 14:29:35 -04:00
slips
1562f4ff52 refactor: highlight synced content instead of markers
When highlight mode is on, hide markers as before but wrap the synced
content itself in a highlight decoration:
- Inline sections: background + outline on text between start/end markers
- Line sections: same treatment on text after the line marker
- Block sections: left accent border + tinted background on each content line

Reading mode wraps the same content in a jira-sync-content span.
Uses color-mix() for theme-aware accent tinting.
2026-04-22 11:20:32 -04:00
slips
f4732263f8 feat: add highlight sync sections setting
Add a toggle in General settings to make jira-sync markers visible as
styled labels instead of hiding them. When enabled:
- Markers render with tag-style pill appearance (theme-aware via --tag-* vars)
- Block content between jira-sync-block-start and jira-sync-end gets a
  left border accent line in live preview
- Works in both live preview (CM6) and reading mode
- Cursor inside a marker adds an accent outline (jira-sync-active)

Default is off, preserving existing hide-on-blur behaviour.
2026-04-22 11:09:08 -04:00
54 changed files with 2602 additions and 783 deletions

View file

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

6
.gitignore vendored
View file

@ -31,3 +31,9 @@ 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,5 +1,6 @@
node check-version.js
yarn run format
yarn run lint:fix
yarn run test
yarn run build
yarn run validate_locale_once

View file

@ -0,0 +1,118 @@
---
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

@ -0,0 +1,54 @@
---
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,265 +1,52 @@
# Obsidian Jira Sync - Community plugin for Obsidian
# Jira Issue Manager — Agent guide
## Project overview
## Project facts
- **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`)
- **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
## Environment & tooling
## Known bugs
- 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
- `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.
### Install
## Quick reference
```bash
yarn 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
```
### 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
For general Obsidian plugin development conventions and project-specific details, see `.opencode/skills/`.

64
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,64 @@
# 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,160 +1,52 @@
# Jira Sync Plugin for Obsidian
# Jira Issue Manager
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.
A two-way Jira synchronization plugin for Obsidian. Templates, custom field mapping, time tracking, and batch operations — all without leaving your notes.
Originally forked from [obsidian-to-jira](https://github.com/angelperezasenjo/obsidian-to-jira), but now packed with features you won't find elsewhere.
https://github.com/user-attachments/assets/55cb2c99-34a9-47fc-85f1-f79dba5a27b1
## Why this plugin exists
## Features
Other plugins treat Jira issues as read-only reference material. This one lets you:
- **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
- **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
![demo_image.png](docs/images/demo_image.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
![field_mapping_demo.png](docs/images/field_mapping_demo.png)
## Quick start
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
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`
## Documentation
For detailed setup, configuration, and advanced usage:
- [English guide](docs/how_to_en.md)
- [Russian guide](docs/how_to_ru.md)
- [Template examples](docs/template_example.md)
- [English Guide](docs/how_to_en.md)
- [Russian Guide](docs/how_to_ru.md)
- [Template Examples](docs/template_example.md)
## Why this one?
---
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:
**Ready to transform your Jira workflow?** Install the plugin and start building your perfect issue management system in Obsidian today!
- 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).

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-assignee`
The responsible person for this task is `jira-sync-block-start-assignee`
Bob
`jira-sync-end`, and the description is `jira-sync-block-description`
`jira-sync-end`, and the description is `jira-sync-block-start-description`
Some description
`jira-sync-end`.
```

View file

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

BIN
docs/images/demo_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB

BIN
docs/images/demo_video.mp4 Normal file

Binary file not shown.

Binary file not shown.

After

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'],
ignores: ['main.js', 'node_modules/**', 'src/**/*.js', 'tests/**'],
},
{
files: ['**/*.ts'],

View file

@ -1,7 +1,7 @@
{
"id": "jira-sync",
"name": "Jira Issue Manager",
"version": "1.6.0",
"version": "1.7.4",
"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.6.0",
"version": "1.7.4",
"packageManager": "yarn@1.22.22",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
@ -15,6 +15,9 @@
"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": [],
@ -27,7 +30,7 @@
"@codemirror/view": "^6.38.1",
"@eslint/js": "^10.0.1",
"@types/lodash": "^4.17.16",
"@types/node": "^16.11.6",
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"builtin-modules": "3.3.0",
@ -36,11 +39,13 @@
"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"
"typescript": "^6.0.3",
"vitest": "^4.1.9"
},
"dependencies": {
"acorn": "^8.14.1",

View file

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

View file

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

View file

@ -0,0 +1,13 @@
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,6 +5,7 @@ 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);
@ -72,8 +73,24 @@ export async function updateStatusFromFile(
}
await updateJiraStatus(plugin, fields.key, transition.id);
await updateJiraToLocal(plugin, file, {
fields: { status: { name: transition.status } },
} as JiraIssue);
// 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) });
});
}
return fields.key;
}

View file

@ -7,24 +7,18 @@ import { Notice, TFile, TFolder } from 'obsidian';
import { defaultTemplate } from '../default/defaultTemplate';
import { debugLog } from '../tools/debugLogging';
function generateFilenameFromTemplate(template: string, issue: JiraIssue): string {
export function generateFilenameFromTemplate(
template: string,
issue: { key: string; fields?: { summary?: string } },
): 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) {
@ -33,10 +27,53 @@ function generateFilenameFromTemplate(template: string, issue: JiraIssue): strin
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);
@ -48,7 +85,6 @@ 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);
@ -60,19 +96,16 @@ 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;
// Add to cache for future use
plugin.setFilePathForIssueKey(issue.key, targetPath);
await 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);
@ -84,12 +117,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, '');
// Add new file to cache
plugin.setFilePathForIssueKey(issue.key, newFile.path);
await plugin.setFilePathForIssueKey(issue.key, newFile.path);
}
new Notice(`Issue ${issue.key} imported successfully`);
} catch (error: unknown) {
@ -108,7 +141,6 @@ 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`);
@ -116,10 +148,8 @@ 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');
@ -129,11 +159,9 @@ 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,6 +12,16 @@ 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,3 +26,10 @@ 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,8 +12,18 @@ 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,3 +21,10 @@ 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,11 +11,12 @@ 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, validateCache } from './tools/cacheUtils';
import { buildCacheFromFilesystem } from './tools/cacheUtils';
import { checkMigrateSettings } from './tools/migrateSettings';
export default class JiraPlugin extends Plugin {
@ -27,9 +28,8 @@ export default class JiraPlugin extends Plugin {
async onload() {
await this.loadSettings();
// validate cache from settings
// initialize cache from settings
this.initializeCache();
await validateCache(this);
// Register all commands
registerUpdateIssueCommand(this);
@ -42,12 +42,13 @@ 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.bind(this));
this.registerMarkdownPostProcessor(hideJiraPointersReading());
// Handle Live Preview/Edit mode (CodeMirror extension)
this.registerEditorExtension(createJiraSyncExtension(this));
@ -58,13 +59,26 @@ 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 new_data = checkMigrateSettings(old_data, this.saveSettings);
this.settings = Object.assign({}, DEFAULT_SETTINGS, new_data);
// 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 || {}),
},
};
this.settings.fieldMapping.fieldMappings = await transform_string_to_functions_mappings(
this.settings.fieldMapping.fieldMappingsStrings,
);
if (migrationChanged) {
await this.saveSettings();
}
}
async saveSettings() {
@ -93,36 +107,36 @@ export default class JiraPlugin extends Plugin {
return this.settings.connections[this.settings.currentConnectionIndex];
}
setFilePathForIssueKey(issueKey: string, filePath: string) {
async setFilePathForIssueKey(issueKey: string, filePath: string) {
this.issueKeyToFilePathCache.set(issueKey, filePath);
this.settings.issueKeyToFilePathCache[issueKey] = filePath;
this.saveSettings();
await this.saveSettings();
}
removeIssueKeyFromCache(issueKey: string) {
async removeIssueKeyFromCache(issueKey: string) {
this.issueKeyToFilePathCache.delete(issueKey);
delete this.settings.issueKeyToFilePathCache[issueKey];
this.saveSettings();
await this.saveSettings();
}
clearCache() {
async clearCache() {
this.issueKeyToFilePathCache.clear();
this.settings.issueKeyToFilePathCache = {};
this.saveSettings();
await this.saveSettings();
}
async rebuildCache() {
this.clearCache();
await this.clearCache();
await buildCacheFromFilesystem(this);
}
private registerVaultEventListeners() {
// Handle file renames
this.registerEvent(
this.app.vault.on('rename', (file, oldPath) => {
this.app.vault.on('rename', async (file, oldPath) => {
for (const [issueKey, cachedPath] of this.issueKeyToFilePathCache.entries()) {
if (cachedPath === oldPath) {
this.setFilePathForIssueKey(issueKey, file.path);
await this.setFilePathForIssueKey(issueKey, file.path);
break;
}
}
@ -131,10 +145,10 @@ export default class JiraPlugin extends Plugin {
// Handle file deletions
this.registerEvent(
this.app.vault.on('delete', (file) => {
this.app.vault.on('delete', async (file) => {
for (const [issueKey, cachedPath] of this.issueKeyToFilePathCache.entries()) {
if (cachedPath === file.path) {
this.removeIssueKeyFromCache(issueKey);
await this.removeIssueKeyFromCache(issueKey);
break;
}
}

View file

@ -0,0 +1,39 @@
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,13 +1,11 @@
import { App, Modal, Setting } from 'obsidian';
import { App, Modal, Setting, DropdownComponent } 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;
@ -15,6 +13,8 @@ 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,8 +23,68 @@ 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'))
@ -38,7 +98,6 @@ 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('');
@ -49,17 +108,18 @@ 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());
}
@ -67,21 +127,56 @@ 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(() => {
// Error handling is done within the preview component
});
this.preview.loadPreview(this.jql).catch(() => {});
}
}, 600);
}

View file

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

View file

@ -31,32 +31,36 @@ 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;
const contentStart = start; // without first `
const contentEnd = end; // without last `
// Find code blocks to ignore
if (this.isInsideCodeBlock(contentStart, contentEnd, codeBlocks)) {
continue;
}
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 }));
if (this.isInsideCodeBlock(start, end, codeBlocks)) continue;
markers.push({ start, end, name: match[1] });
}
// 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 }) });
}
}
// 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);
}
return builder.finish() as DecorationSet;

View file

@ -1,13 +1,12 @@
// 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');
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[];
codeElements.forEach((codeEl) => {
const text = codeEl.textContent || '';
if (text.startsWith('jira-sync-')) {
for (const codeEl of codeElements) {
codeEl.addClass('jira-sync-hidden');
}
});
};
}

View file

@ -66,6 +66,9 @@ 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;
@ -83,7 +86,7 @@ export class ConnectionSettingsComponent implements SettingsComponent {
private populateConnectionDropdown(cb: any): void {
const { plugin } = this.props;
cb.selectEl.innerHTML = '';
cb.selectEl.empty();
plugin.settings.connections.forEach((conn, index) => {
cb.addOption(index.toString(), conn.name || `Connection ${index + 1}`);
});

View file

@ -1,6 +1,8 @@
import { Notice, Setting } from 'obsidian';
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
import { fetchIssue } from '../../api';
import { fetchIssue, validateSettings } from '../../api';
import { renameExistingIssueFiles } from '../../file_operations/getIssue';
import { ConfirmModal } from '../../modals/ConfirmModal';
import debounce from 'lodash/debounce';
import hljs from 'highlight.js';
import { useTranslations } from '../../localization/translator';
@ -23,7 +25,6 @@ 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'));
@ -37,6 +38,40 @@ 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'
@ -87,7 +122,6 @@ export class FetchIssueComponent implements SettingsComponent {
});
});
// Add input field for issue key
new Setting(containerEl)
.setName(t('key.name'))
.setDesc(t('key.desc'))
@ -98,7 +132,6 @@ 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
setTimeout(resize, 0);
window.setTimeout(resize, 0);
}
/**

View file

@ -156,28 +156,42 @@ export class FieldMappingsComponent implements SettingsComponent {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'modal-container';
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>
`;
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);
document.body.appendChild(modal);
const confirmBtn = modal.querySelector('#confirm-btn') as HTMLButtonElement;
const cancelBtn = modal.querySelector('#cancel-btn') as HTMLButtonElement;
confirmBtn?.addEventListener('click', () => {
confirmBtn.addEventListener('click', () => {
document.body.removeChild(modal);
resolve(true);
});
cancelBtn?.addEventListener('click', () => {
cancelBtn.addEventListener('click', () => {
document.body.removeChild(modal);
resolve(false);
});

View file

@ -1,5 +1,10 @@
import { FieldMapping } from '../default/obsidianJiraFieldsMapping';
export interface JQLPreset {
name: string;
query: string;
}
export interface ConnectionSettingsInterface {
name: string;
jiraUrl: string;
@ -9,6 +14,9 @@ export interface ConnectionSettingsInterface {
username: string;
email: string;
password: string;
jqlPresets: JQLPreset[];
lastJqlQuery: string;
sessionCookie: string;
}
export interface GlobalSettingsInterface {
@ -78,6 +86,9 @@ 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') {
plugin.setFilePathForIssueKey(issueKey, child.path);
await 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
entriesToRemove.forEach((issueKey) => {
plugin.removeIssueKeyFromCache(issueKey);
});
for (const issueKey of entriesToRemove) {
await 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 = safeStringToFunction(toJira, 'toJira', extraValidate);
const fromJiraFn = safeStringToFunction(fromJira, 'fromJira', extraValidate);
const toJiraFn = await safeStringToFunction(toJira, 'toJira', extraValidate);
const fromJiraFn = await safeStringToFunction(fromJira, 'fromJira', extraValidate);
if (toJiraFn && fromJiraFn) {
transformedMappings[fieldName] = {
toJira: (await toJiraFn) as (value: any, api_version?: '2' | '3') => any,
fromJira: (await fromJiraFn) as (
toJira: toJiraFn as (value: any, api_version?: '2' | '3') => any,
fromJira: fromJiraFn as (
issue: JiraIssue,
api_version?: '2' | '3',
data_source?: Record<string, any>,

View file

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

View file

@ -1,5 +1,5 @@
interface AdfTextMark {
type: 'strong' | 'em' | 'code' | 'link';
type: 'strong' | 'em' | 'code' | 'link' | 'underline' | 'strike';
attrs?: { href: string };
}
@ -47,9 +47,15 @@ interface AdfOrderedListNode {
content: AdfListItemNode[];
}
interface AdfTaskItemNode {
type: 'taskItem';
attrs: { localId: string; state: 'TODO' | 'DONE' };
content: AdfInlineContent[];
}
interface AdfTaskListNode {
type: 'taskList';
content: AdfListItemNode[];
content: AdfTaskItemNode[];
attrs?: { localId: string };
}
@ -72,45 +78,219 @@ interface AdfDoc {
content: AdfBlockNode[];
}
function parseInline(text: string): AdfInlineContent[] {
const nodes: AdfInlineContent[] = [];
const regex = /(\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)|\[\[[^\]]+\]\])/g;
let lastIndex = 0;
let match: RegExpExecArray | null;
// --- Inline formatting rules -------------------------------------------------
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 {
// [[wikilink]] — preserve as plain text so round-trip survives
nodes.push({ type: 'text', text: match[0] });
}
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
nodes.push({ type: 'text', text: text.slice(lastIndex) });
}
return nodes.length > 0 ? nodes : [{ type: 'text', text }];
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);
}
// --- 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,
});
} else {
i++;
}
}
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] });
}
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,})$/)
);
}
// --- Markdown → ADF ----------------------------------------------------------
export function markdownToAdf(markdown: string): AdfDoc | null {
if (!markdown || !markdown.trim()) return null;
@ -121,159 +301,72 @@ export function markdownToAdf(markdown: string): AdfDoc | null {
while (i < lines.length) {
const line = lines[i];
// 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') }],
});
const code = parseFencedCode(lines, i);
if (code) {
content.push(code.node);
i = code.next;
continue;
}
const heading = parseHeading(line);
if (heading) {
content.push(heading);
i++;
continue;
}
// Heading
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
if (headingMatch) {
content.push({
type: 'heading',
attrs: { level: headingMatch[1].length },
content: parseInline(headingMatch[2]),
});
const hr = parseHr(line);
if (hr) {
content.push(hr);
i++;
continue;
}
// Horizontal rule
if (line.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
content.push({ type: 'rule' });
i++;
const task = parseTaskList(lines, i);
if (task) {
content.push(task.node);
i = task.next;
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 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 });
const r = parseListItems(lines, i, /^[-*+]\s+/, /^[-*+]\s+/);
content.push({ type: 'bulletList', content: r.items });
i = r.next;
continue;
}
// Ordered list
if (line.match(/^\d+\.\s+/)) {
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 });
const r = parseListItems(lines, i, /^\d+\.\s+/, /^\d+\.\s+/);
content.push({ type: 'orderedList', content: r.items });
i = r.next;
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() !== '' &&
!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,})$/)
) {
while (i < lines.length && lines[i].trim() !== '' && !isBlockStart(lines[i])) {
paraLines.push(lines[i]);
i++;
}
if (paraLines.length > 0) {
content.push({
type: 'paragraph',
content: parseInline(paraLines.join('\n')),
});
if (paraLines.length) {
content.push({ type: 'paragraph', content: parseInline(paraLines.join('\n')) });
}
}
return content.length > 0 ? { version: 1, type: 'doc', content } : null;
return content.length ? { version: 1, type: 'doc', content } : null;
}
// --- ADF → Markdown helpers --------------------------------------------------
function adfInlineToMarkdown(nodes: any[]): string {
if (!nodes) return '';
return nodes
.map((node) => {
.map((node: any) => {
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 || '';
@ -287,100 +380,80 @@ 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 = `~~${result}~~`;
if (marks.includes('strong')) result = `**${result}**`;
if (marks.includes('em')) result = `*${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 (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': {
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 'heading':
return `${'#'.repeat(node.attrs?.level || 1)} ${adfInlineToMarkdown(node.content || [])}`;
case 'paragraph':
return adfInlineToMarkdown(node.content || []);
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 (node.content || [])
.map((item: any) => {
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 `- ${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': {
case 'bulletList':
return renderList(node.content || [], false);
case 'orderedList':
return renderList(node.content || [], true);
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 =
const raw =
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
const lines = raw.split('\n');
const full =
lines[0] +
(lines.slice(1).length
? '\n' +
textLines
lines
.slice(1)
.map((l: string) => ' ' + l)
.join('\n')
: '');
const nested = rest
.map((b: any) => adfBlockToMarkdown(b))
.filter((s: string) => s.trim() !== '')
.filter((s: string) => s.trim())
.map((s: string) =>
s
.split('\n')
@ -388,31 +461,32 @@ function adfBlockToMarkdown(node: any): string {
.join('\n'),
)
.join('\n');
return `- [${checked ? 'x' : ' '}] ${fullText}${nested ? '\n' + nested : ''}`;
return `- [${checked ? 'x' : ' '}] ${full}${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((line: string) => `> ${line}`)
.map((l: string) => `> ${l}`)
.join('\n');
}
case 'table': {
const rows: any[] = node.content || [];
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 '';
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 sep = `| ${rows[0].content.map(() => '---').join(' | ')} |`;
return [mdRows[0], sep, ...mdRows.slice(1)].join('\n');
}
@ -426,8 +500,11 @@ function adfBlockToMarkdown(node: any): string {
}
}
export function adfToMarkdown(adf: any): string {
// --- ADF → Markdown ----------------------------------------------------------
export function adfToMarkdown(adf: any): string | null {
if (adf === undefined) return null;
if (!adf || typeof adf !== 'object') return '';
const blocks: string[] = (adf.content || []).map((node: any) => adfBlockToMarkdown(node));
return blocks.filter((b) => b !== '').join('\n\n');
const blocks = (adf.content || []).map((node: any) => adfBlockToMarkdown(node));
return blocks.filter((b: string) => b !== '').join('\n\n');
}

View file

@ -5,8 +5,8 @@ import {
TimekeepSettingsInterface,
} from '../settings/default';
export function checkMigrateSettings(data: any, saveSettings: () => void): any {
if (!data || typeof data !== 'object') return data;
export function checkMigrateSettings(data: any): { result: any; changed: boolean } {
if (!data || typeof data !== 'object') return { result: data, changed: false };
const result = { ...data };
let data_changed = false;
@ -84,6 +84,8 @@ export function checkMigrateSettings(data: any, saveSettings: () => void): any {
username: result.connection.username || '',
email: result.connection.email || '',
password: result.connection.password || '',
jqlPresets: [],
lastJqlQuery: '',
},
];
result.currentConnectionIndex = 0;
@ -91,9 +93,23 @@ export function checkMigrateSettings(data: any, saveSettings: () => void): any {
data_changed = true;
}
if (data_changed) {
saveSettings();
// 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;
}
}
}
return result;
if (data_changed) {
return { result, changed: true };
}
return { result, changed: false };
}

183
tests/__mocks__/obsidian.ts Normal file
View file

@ -0,0 +1,183 @@
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

@ -0,0 +1,175 @@
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

@ -0,0 +1,75 @@
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

@ -0,0 +1,55 @@
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');
});
});
});

4
tests/__tests__/setup.ts Normal file
View file

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

View file

@ -0,0 +1,62 @@
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

@ -0,0 +1,56 @@
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

@ -0,0 +1,112 @@
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

@ -0,0 +1,19 @@
{
"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": ""
}
]
}

15
tests/vitest.config.ts Normal file
View file

@ -0,0 +1,15 @@
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,5 +14,6 @@
"lib": ["DOM", "ES2017", "ES2018", "ES2019", "ES2020"],
"types": ["node"]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
"exclude": ["tests/", "node_modules/"]
}

View file

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

750
yarn.lock
View file

@ -2,6 +2,45 @@
# 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"
@ -70,6 +109,61 @@
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"
@ -248,6 +342,11 @@
"@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"
@ -279,6 +378,11 @@
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"
@ -312,6 +416,122 @@
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"
@ -319,6 +539,11 @@
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"
@ -329,6 +554,11 @@
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"
@ -339,10 +569,12 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.24.tgz#4ae334fc62c0e915ca8ed8e35dcc6d4eeb29215f"
integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==
"@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/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/tern@*":
version "0.23.9"
@ -447,6 +679,66 @@
"@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"
@ -479,11 +771,23 @@ 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"
@ -496,6 +800,11 @@ 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"
@ -544,6 +853,11 @@ 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"
@ -558,6 +872,22 @@ 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"
@ -565,16 +895,36 @@ 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"
@ -700,11 +1050,23 @@ 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"
@ -762,6 +1124,11 @@ 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"
@ -794,6 +1161,13 @@ 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"
@ -831,11 +1205,43 @@ 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"
@ -875,6 +1281,80 @@ 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"
@ -887,6 +1367,23 @@ 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"
@ -904,6 +1401,11 @@ 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"
@ -917,6 +1419,11 @@ 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"
@ -943,6 +1450,13 @@ 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"
@ -953,11 +1467,30 @@ path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
picomatch@^4.0.4:
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:
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"
@ -968,7 +1501,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.1.0, punycode@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
@ -983,6 +1516,35 @@ 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"
@ -990,6 +1552,13 @@ 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"
@ -1012,6 +1581,26 @@ 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"
@ -1047,6 +1636,21 @@ 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"
@ -1055,6 +1659,45 @@ 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"
@ -1070,7 +1713,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.1.0, tslib@^2.4.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@ -1087,6 +1730,16 @@ 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"
@ -1099,11 +1752,76 @@ 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"
@ -1111,6 +1829,14 @@ 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"
@ -1125,6 +1851,16 @@ 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"