chore: initial project commit
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# Path to Zotero executable
|
||||
# Linux: /usr/bin/zotero
|
||||
# macOS: /Applications/Zotero.app/Contents/MacOS/zotero
|
||||
# Windows: C:\Program Files\Zotero\zotero.exe
|
||||
ZOTERO_PLUGIN_ZOTERO_BIN_PATH=
|
||||
|
||||
# Path to Zotero profile directory for development
|
||||
# Find via: Help -> Troubleshooting Information -> Profile Directory
|
||||
ZOTERO_PLUGIN_PROFILE_PATH=
|
||||
|
||||
# Data directory (optional, defaults to profile's default)
|
||||
# ZOTERO_PLUGIN_DATA_DIR=
|
||||
15
.eslintrc.js
Normal file
15
.eslintrc.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
ignorePatterns: [".scaffold/", "node_modules/", "addon/bootstrap.js", "*.config.js"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
};
|
||||
85
.gitea/workflows/release.yml
Normal file
85
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create release and upload assets
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
REPO="${GITHUB_REPOSITORY}"
|
||||
SERVER_URL="${GITHUB_SERVER_URL}"
|
||||
API_URL="${SERVER_URL}/api/v1"
|
||||
|
||||
# Create the release
|
||||
RELEASE=$(curl -s -X POST "${API_URL}/repos/${REPO}/releases" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"tag_name\": \"${TAG}\",
|
||||
\"name\": \"${TAG}\",
|
||||
\"body\": \"Release ${TAG}\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}")
|
||||
|
||||
RELEASE_ID=$(echo "${RELEASE}" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
|
||||
if [ -z "${RELEASE_ID}" ]; then
|
||||
echo "Failed to create release. Response: ${RELEASE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Created release ID: ${RELEASE_ID}"
|
||||
|
||||
# Upload all XPI files
|
||||
for XPI in .scaffold/build/*.xpi; do
|
||||
[ -f "${XPI}" ] || continue
|
||||
FILENAME=$(basename "${XPI}")
|
||||
echo "Uploading ${FILENAME}..."
|
||||
curl -s -X POST "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${XPI}"
|
||||
done
|
||||
|
||||
# Upload update manifests if present
|
||||
for JSON in .scaffold/build/update.json .scaffold/build/update-beta.json; do
|
||||
[ -f "${JSON}" ] || continue
|
||||
FILENAME=$(basename "${JSON}")
|
||||
echo "Uploading ${FILENAME}..."
|
||||
curl -s -X POST "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}" \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary "@${JSON}"
|
||||
done
|
||||
|
||||
echo "Release ${TAG} published successfully."
|
||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
.scaffold/
|
||||
*.xpi
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Test
|
||||
coverage/
|
||||
|
||||
# Nix
|
||||
result
|
||||
.direnv/
|
||||
|
||||
# AI agent artifacts
|
||||
.agent-shell/
|
||||
opencode.json
|
||||
|
||||
# Internal development artifacts
|
||||
PLAN.md
|
||||
zotero-notes-export.ts
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100
|
||||
}
|
||||
286
AGENTS.md
Normal file
286
AGENTS.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Development Environment Commands
|
||||
|
||||
### Primary Commands (run inside `nix develop`)
|
||||
|
||||
```bash
|
||||
# Development
|
||||
nix develop # Enter dev shell with Node.js and tooling
|
||||
npm run start # Start dev server with hot reload
|
||||
npm run build # Build production XPI to .scaffold/build/
|
||||
|
||||
# Code Quality
|
||||
npm run lint # Run ESLint on TypeScript source
|
||||
npm run lint:fix # Auto-fix ESLint issues
|
||||
npm run format # Format with Prettier
|
||||
npm run format:check # Check formatting without changes
|
||||
|
||||
# Release
|
||||
npm run release # Bump version, commit, tag, and build
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
This project does not currently have automated tests. To test manually:
|
||||
|
||||
1. Run `npm run build` to create the XPI
|
||||
2. Install the XPI in Zotero 7
|
||||
3. Test export functionality manually via Tools menu
|
||||
|
||||
---
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Imports and Exports
|
||||
|
||||
```typescript
|
||||
// Use relative imports for internal modules
|
||||
import { OrgExporter } from "./modules/exporter";
|
||||
import type { Prefs } from "./modules/prefs";
|
||||
|
||||
// Use type-only imports where possible
|
||||
import type { AddonInfo } from "./addon";
|
||||
|
||||
// Use default exports for single main entity
|
||||
export class OrgExportAnnotationsAddon {}
|
||||
export function createHooks() {}
|
||||
|
||||
// Use named exports for utilities
|
||||
export async function convertToOrg() {}
|
||||
export async function testPandoc() {}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Prefer named exports for functions
|
||||
- Use `import type` for type-only imports
|
||||
- Import from `./modules/` with relative paths
|
||||
- No namespace imports (`import * as X`)
|
||||
- No `export default` for modules (use named exports)
|
||||
|
||||
### Formatting (Prettier)
|
||||
|
||||
```json
|
||||
{
|
||||
"semi": true, // Always use semicolons
|
||||
"singleQuote": false, // Use double quotes
|
||||
"tabWidth": 2, // 2 space indentation
|
||||
"trailingComma": "es5", // Trailing commas in valid ES5
|
||||
"printWidth": 100 // Max line length
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
|
||||
- **Strict mode enabled**: All type checking rules active
|
||||
- **Target**: ES2022 with DOM types
|
||||
- **Module**: ESNext with bundler resolution
|
||||
- **Source maps**: Enabled for debugging
|
||||
- **Output**: Bundled to `.scaffold/build/addon/content/scripts/`
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| Entity | Convention | Examples |
|
||||
| --------------- | ---------------- | ---------------------------------------------------------- |
|
||||
| Classes | PascalCase | `OrgExporter`, `Prefs`, `AddonInfo` |
|
||||
| Functions | camelCase | `convertToOrg`, `isAnnotationNote`, `ensureNotesDirectory` |
|
||||
| Methods | camelCase | `init()`, `exportAttachment()`, `cleanupTempFile()` |
|
||||
| Private Methods | camelCase | Prefix with `private` keyword |
|
||||
| Constants | UPPER_SNAKE_CASE | `PREF_PREFIX`, `MENU_IDS` |
|
||||
| Interfaces | PascalCase | `AddonInfo`, `Hooks` |
|
||||
| Types | PascalCase | `export type Preference = ...` |
|
||||
| File Names | kebab-case | `converter.ts`, `exporter.ts`, `preferences.js` |
|
||||
|
||||
```typescript
|
||||
// Constants
|
||||
const PREF_PREFIX = "extensions.zotero.orgexportannotations";
|
||||
const MENU_IDS = { toolsExportAll: "...", itemExportSelected: "..." };
|
||||
|
||||
// Private methods
|
||||
private async getLastAnnotationsNote(): Promise<Zotero.Item | undefined> {}
|
||||
private isAnnotationNote(noteHTML: string): boolean {}
|
||||
```
|
||||
|
||||
### Type Annotations
|
||||
|
||||
```typescript
|
||||
// Explicit return types on functions
|
||||
export async function convertToOrg(...): Promise<void> {}
|
||||
export function testPandoc(pandocPath: string): Promise<boolean> {}
|
||||
|
||||
// Type assertions for Zotero API returns
|
||||
const title = attachment.parentItem.getField("title") as string;
|
||||
const citationKey = item.getField("citationKey") as string;
|
||||
|
||||
// Optional chaining with nullish coalescing
|
||||
const citationKey = note.parentItem?.getField("citationKey") || "temp";
|
||||
|
||||
// Generic type parameters
|
||||
private getPref<T>(key: string, defaultValue: T): T {
|
||||
return (value as T) ?? defaultValue;
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
// Standard pattern with logging
|
||||
try {
|
||||
await operation();
|
||||
Zotero.OrgExportAnnotations.log("Operation succeeded");
|
||||
} catch (error) {
|
||||
Zotero.OrgExportAnnotations.error("Operation failed", error as Error);
|
||||
// Optionally re-throw
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Empty catch blocks must use underscore prefix
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (_error) {
|
||||
// Cleanup errors are non-critical
|
||||
}
|
||||
|
||||
// Return error values instead of throwing for utility functions
|
||||
export async function testPandoc(pandocPath: string): Promise<boolean> {
|
||||
try {
|
||||
await Zotero.Utilities.Internal.exec(pandocPath, ["--version"]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
```typescript
|
||||
// Debug logging (respects debug mode)
|
||||
Zotero.OrgExportAnnotations.log("Message", ...args);
|
||||
|
||||
// Error logging (always logged)
|
||||
Zotero.OrgExportAnnotations.error("Message", error as Error);
|
||||
|
||||
// Warning logging (always logged)
|
||||
Zotero.OrgExportAnnotations.warn("Message");
|
||||
|
||||
// Verbose debugging with objects
|
||||
Zotero.OrgExportAnnotations.log("Processing item", { id, title });
|
||||
```
|
||||
|
||||
### Async/Await Patterns
|
||||
|
||||
```typescript
|
||||
// Use for...of for sequential async operations
|
||||
for (const attachment of attachments) {
|
||||
if (await this.exportAttachment(attachment)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Early returns with await
|
||||
if (!item.isFileAttachment()) return false;
|
||||
if (!path) throw new Error("Notes path not configured");
|
||||
```
|
||||
|
||||
### Zotero API Usage
|
||||
|
||||
```typescript
|
||||
// Non-null assertions for known APIs
|
||||
Zotero.BetterNotes!.api.convert.note2md(note, notesPath);
|
||||
|
||||
// Zotero utilities for file operations
|
||||
await Zotero.File.putContentsAsync(path, content);
|
||||
const content = await Zotero.File.getContentsAsync(path);
|
||||
await Zotero.Utilities.Internal.exec(command, args);
|
||||
|
||||
// Zotero preferences
|
||||
Zotero.Prefs.get(key, global);
|
||||
Zotero.Prefs.set(key, value, global);
|
||||
|
||||
// Zotero items
|
||||
const item = Zotero.Items.get(id);
|
||||
await Zotero.Items.trashTx(id);
|
||||
```
|
||||
|
||||
### Class Structure
|
||||
|
||||
```typescript
|
||||
export class MyClass {
|
||||
// Public properties
|
||||
public property!: string;
|
||||
|
||||
// Private properties
|
||||
private internal = { enabled: false };
|
||||
|
||||
// Constructor with dependency injection
|
||||
constructor(private deps: Dependencies) {}
|
||||
|
||||
// Public methods
|
||||
public async doSomething(): Promise<void> {
|
||||
try {
|
||||
// Implementation
|
||||
} catch (error) {
|
||||
this.handleError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
private handleError(error: Error): void {
|
||||
Zotero.OrgExportAnnotations.error("Error occurred", error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ESLint Rules
|
||||
|
||||
- **Unused variables**: Error, but ignored if prefixed with `_`
|
||||
- **Explicit any**: Warning (prefer specific types)
|
||||
- **No-empty**: Error for empty catch blocks (use `catch (_error)`)
|
||||
|
||||
### File Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Main entry point
|
||||
├── addon.ts # Core addon class
|
||||
├── hooks.ts # Lifecycle hooks
|
||||
└── modules/
|
||||
├── prefs.ts # Preferences management
|
||||
├── exporter.ts # Core export logic
|
||||
├── converter.ts # Format conversion
|
||||
├── notifier.ts # Zotero event handlers
|
||||
└── menu.ts # UI menu registration
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Preference getters/setters**:
|
||||
|
||||
```typescript
|
||||
get notesPath(): string {
|
||||
const path = this.getPref("notesPath", "");
|
||||
if (path && path.startsWith("~")) {
|
||||
return path.replace("~", PathUtils.profileDir);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
set notesPath(value: string) {
|
||||
this.setPref("notesPath", value);
|
||||
}
|
||||
```
|
||||
|
||||
**Filter operations**:
|
||||
|
||||
```typescript
|
||||
return items
|
||||
.filter((item) => this.checkCondition(item))
|
||||
.filter((item) => item.parentItem !== undefined);
|
||||
```
|
||||
|
||||
**Reduce operations**:
|
||||
|
||||
```typescript
|
||||
return notes.reduce((max, current) => (current.dateModified > max.dateModified ? current : max));
|
||||
```
|
||||
211
README.md
Normal file
211
README.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Zotero Org Export Annotations
|
||||
|
||||
Export PDF and EPUB annotations from Zotero to Org-mode files for use with Emacs, org-roam, and citar.
|
||||
|
||||
## Features
|
||||
|
||||
- Export annotations to individual Org files named by citation key
|
||||
- Automatic export after Zotero sync completes
|
||||
- Export when closing a PDF/EPUB reader tab
|
||||
- Configurable output directory
|
||||
- Optional attachment of Org files to Zotero items
|
||||
|
||||
## Requirements
|
||||
|
||||
- **[Zotero](https://www.zotero.org/) 7.0+**
|
||||
- **[Better Notes](https://github.com/windingwind/zotero-better-notes)** plugin — must be installed before this plugin
|
||||
- **[Pandoc](https://pandoc.org/)** — for Markdown to Org conversion
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install the **Better Notes** plugin first (see its own instructions)
|
||||
2. Install **Pandoc** and ensure it is on your `PATH` (or note its full path)
|
||||
3. Download the latest `.xpi` file from [Releases](../../releases)
|
||||
4. In Zotero, go to **Tools → Add-ons**
|
||||
5. Click the gear icon (top-right) and select **Install Add-on From File...**
|
||||
6. Select the downloaded `.xpi` file and restart Zotero if prompted
|
||||
|
||||
## Configuration
|
||||
|
||||
After installation, open **Tools → Org Export Preferences** to configure:
|
||||
|
||||
| Preference | Default | Description |
|
||||
| ----------------------- | --------- | --------------------------------------------------------------- |
|
||||
| Notes Directory | _(empty)_ | Directory where Org files will be saved. Supports `~` for home. |
|
||||
| Pandoc Path | `pandoc` | Full path to the pandoc binary if not on `PATH` |
|
||||
| Attach Org file to item | `false` | Link exported Org files as Zotero attachments |
|
||||
| Auto-export on sync | `true` | Export automatically after Zotero sync completes |
|
||||
| Export on tab close | `true` | Export when closing a PDF/EPUB reader tab |
|
||||
| Show notifications | `true` | Display a progress notification during export |
|
||||
| Debug mode | `false` | Enable verbose logging to the Zotero error console |
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Export
|
||||
|
||||
- **Export all items**: **Tools → Export All Annotations to Org**
|
||||
- **Export selected items**: Right-click one or more items → **Export Annotations to Org**
|
||||
|
||||
### Automatic Export
|
||||
|
||||
When enabled in preferences, exports happen automatically:
|
||||
|
||||
- After a Zotero sync completes (only items with new/changed annotations are exported)
|
||||
- When you close a PDF or EPUB reader tab
|
||||
|
||||
### Output Format
|
||||
|
||||
Files are written to the configured Notes Directory, named by citation key:
|
||||
|
||||
```
|
||||
~/org/notes/smith2020.org
|
||||
~/org/notes/doe2019.org
|
||||
```
|
||||
|
||||
Each file contains:
|
||||
|
||||
```org
|
||||
#+title: The Full Title of the Referenced Work
|
||||
|
||||
* Annotation heading
|
||||
Converted annotation text...
|
||||
```
|
||||
|
||||
Only items that have a citation key (via Better BibTeX or the `citationKey` field) will be exported.
|
||||
|
||||
## For Maintainers
|
||||
|
||||
This section covers building, developing, and releasing the plugin.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Entry point: mounts addon to Zotero global
|
||||
├── addon.ts # Core addon class (logging, init, module wiring)
|
||||
├── hooks.ts # Lifecycle hooks (startup/shutdown, menu actions)
|
||||
└── modules/
|
||||
├── prefs.ts # Typed read/write for all preferences
|
||||
├── exporter.ts # Attachment detection, sync checking, batch export
|
||||
├── converter.ts # Markdown → Org via pandoc, file I/O
|
||||
├── notifier.ts # Zotero event handlers (sync finish, tab close)
|
||||
└── menu.ts # Menu registration (Zotero 7 + 8 compatible)
|
||||
|
||||
addon/ # Static plugin files copied verbatim into the XPI
|
||||
├── manifest.json
|
||||
├── bootstrap.js # Zotero plugin lifecycle entry point
|
||||
├── prefs.js # Default preference values
|
||||
└── content/
|
||||
├── preferences.xhtml # Preferences pane UI (XUL)
|
||||
├── scripts/preferences.js
|
||||
├── icons/
|
||||
└── locale/en-US/ # Fluent (.ftl) string bundles
|
||||
```
|
||||
|
||||
The build system uses [zotero-plugin-scaffold](https://github.com/windingwind/zotero-plugin-scaffold) with esbuild, which bundles `src/` into a single `index.js` and packages everything as an XPI.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**With Nix (recommended):**
|
||||
|
||||
```bash
|
||||
# Requires Nix with flakes enabled
|
||||
nix develop
|
||||
```
|
||||
|
||||
This provides Node.js 20, npm, TypeScript, ESLint, Prettier, and Pandoc.
|
||||
|
||||
**Without Nix:**
|
||||
|
||||
- Node.js 20+
|
||||
- npm
|
||||
- Pandoc (for manual testing)
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Copy the environment template and fill in your local paths:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
```bash
|
||||
# Path to the Zotero binary (required for the dev server)
|
||||
ZOTERO_PLUGIN_ZOTERO_BIN_PATH=/path/to/zotero
|
||||
|
||||
# Path to a Zotero development profile directory
|
||||
ZOTERO_PLUGIN_PROFILE_PATH=/path/to/zotero-dev-profile
|
||||
```
|
||||
|
||||
Create a dedicated Zotero profile for development to avoid affecting your main library.
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```bash
|
||||
# Start the dev server — builds and hot-reloads the plugin into a running Zotero instance
|
||||
npm start
|
||||
|
||||
# One-off build — outputs XPI to .scaffold/build/
|
||||
npm run build
|
||||
|
||||
# Lint TypeScript source
|
||||
npm run lint
|
||||
npm run lint:fix # auto-fix
|
||||
|
||||
# Format source files
|
||||
npm run format
|
||||
npm run format:check # check without writing
|
||||
```
|
||||
|
||||
The dev server (`npm start`) watches for changes in `src/` and `addon/`, rebuilds, and reloads the plugin inside Zotero automatically. Zotero must already be running with the configured profile.
|
||||
|
||||
### Testing
|
||||
|
||||
There are no automated tests. To test manually:
|
||||
|
||||
1. `npm run build` to produce the XPI
|
||||
2. Install the XPI in a Zotero 7 instance via **Tools → Add-ons → Install Add-on From File...**
|
||||
3. Exercise the export functionality manually via the Tools menu and right-click context menu
|
||||
|
||||
### Releasing
|
||||
|
||||
Releases are created by pushing a version tag. The CI/CD pipeline (Gitea Actions) will build the XPI and publish a release automatically.
|
||||
|
||||
To cut a release locally:
|
||||
|
||||
```bash
|
||||
npm run release
|
||||
```
|
||||
|
||||
This runs [bumpp](https://github.com/antfu/bumpp) interactively to select the new version, then:
|
||||
|
||||
1. Updates `package.json` version
|
||||
2. Commits and tags the version
|
||||
3. Pushes the commit and tag to the remote
|
||||
4. Builds the XPI
|
||||
|
||||
Once the tag is pushed, Gitea Actions takes over and creates the release with the XPI attached.
|
||||
|
||||
### Code Style
|
||||
|
||||
- **Formatter**: Prettier (config in `.prettierrc`) — double quotes, 2-space indent, semicolons
|
||||
- **Linter**: ESLint with TypeScript-ESLint (config in `.eslintrc.js`) — strict mode
|
||||
- **Naming**: PascalCase for classes/types, camelCase for functions/methods, UPPER_SNAKE_CASE for constants
|
||||
- **Imports**: relative paths, `import type` for type-only imports, no namespace imports
|
||||
- **Error handling**: always catch with a typed `error as Error`; use `_error` for non-critical ignores
|
||||
|
||||
See `AGENTS.md` for the full style guide used during AI-assisted development.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-or-later
|
||||
|
||||
## Credits
|
||||
|
||||
Based on the [zotero-plugin-template](https://github.com/windingwind/zotero-plugin-template) by windingwind.
|
||||
70
addon/bootstrap.js
vendored
Normal file
70
addon/bootstrap.js
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
var chromeHandle;
|
||||
|
||||
async function startup({ id, version, rootURI }, reason) {
|
||||
await Zotero.initializationPromise;
|
||||
|
||||
try {
|
||||
const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(
|
||||
Ci.amIAddonManagerStartup
|
||||
);
|
||||
const manifestURI = Services.io.newURI(rootURI + "manifest.json");
|
||||
chromeHandle = aomStartup.registerChrome(manifestURI, [
|
||||
["content", "orgexportannotations", rootURI + "content/"],
|
||||
]);
|
||||
|
||||
Services.scriptloader.loadSubScript(rootURI + "content/scripts/index.js");
|
||||
|
||||
if (typeof Zotero.OrgExportAnnotations === "undefined") {
|
||||
throw new Error("Zotero.OrgExportAnnotations not loaded from index.js");
|
||||
}
|
||||
|
||||
await Zotero.OrgExportAnnotations.init({ id, version, rootURI });
|
||||
await Zotero.OrgExportAnnotations.hooks.onStartup();
|
||||
} catch (error) {
|
||||
Zotero.logError("[OrgExportAnnotations] Startup failed: " + error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function onMainWindowLoad({ window }) {
|
||||
try {
|
||||
await Zotero.OrgExportAnnotations?.hooks?.onMainWindowLoad(window);
|
||||
} catch (error) {
|
||||
Zotero.logError("[OrgExportAnnotations] onMainWindowLoad failed: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
async function onMainWindowUnload({ window }) {
|
||||
try {
|
||||
await Zotero.OrgExportAnnotations?.hooks?.onMainWindowUnload(window);
|
||||
} catch (error) {
|
||||
Zotero.logError("[OrgExportAnnotations] onMainWindowUnload failed: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
function shutdown({ id, version, rootURI }, reason) {
|
||||
if (reason === APP_SHUTDOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Zotero.OrgExportAnnotations?.hooks?.onShutdown();
|
||||
} catch (error) {
|
||||
Zotero.logError("[OrgExportAnnotations] onShutdown failed: " + error);
|
||||
}
|
||||
|
||||
try {
|
||||
chromeHandle?.destruct();
|
||||
} catch (error) {
|
||||
Zotero.logError("[OrgExportAnnotations] Failed to destruct chrome handle: " + error);
|
||||
}
|
||||
chromeHandle = null;
|
||||
|
||||
delete Zotero.OrgExportAnnotations;
|
||||
}
|
||||
|
||||
function install() {}
|
||||
|
||||
function uninstall() {}
|
||||
1
addon/content/icons/icon.svg
Normal file
1
addon/content/icons/icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="-7.65 -13.389 144.98 160"><path fill="#a04d32" stroke="#000" stroke-width="3" d="M133.399 46.067c-.205-3.15-2.842-4.366-5.993-2.125-7.22-1.297-14.305-.687-17.8-.981-7.662-1.073-14.041-5.128-14.041-5.128.932-1.239.486-3.917-5.498-4.101-1.646-.542-3.336-1.327-4.933-1.979.544-1.145-.133-2.836-.133-2.836 2.435-.672 2.808-3.842 1.848-5.709 3.106.084 2.612-4.718 2.183-6.381 2.435-.923 2.77-3.831 1.763-6.129 2.938-.671 3.022-4.114 2.77-6.548 3.023-.168 2.604-5.457 2.604-6.549 2.604-1.679 2.016-3.946 2.425-6.573 1.605-3.25-.577-4.173-2.116-.71-1.651 3.001-3.77 4.311-3.75 6.528.755 1.259-5.625 3.106-3.61 7.052-1.428 1.763-4.785 4.03-3.592 6.733-.606 1.326-4.888 4.433-3.041 7.371-4.03 2.687-3.79 3.335-2.938 5.793-1.147.736-2.318 1.862-2.995 3.094-1.32-1.568-2.603-4.429-2.584-8.294 0-3.275-6.1.318-6.1 6.784 0 .556-.056 1.061-.134 1.542-2.11.243-4.751.707-8.08 1.494-.106.073-.157.186-.182.316a8.704 8.704 0 01-.277-1.553c-.582-3.79-4.934-9.56-7.057-2.434-1.096 2.611-1.74 4.392-2.115 5.789v0s-.336.226-.957.61c-2.62 1.622-3.562 6.686-13.075 9.883-3.211 1.079-7.4 1.945-12.96 2.395-9.57.773-27.887 17.314-29.114 33.097-.283 3.964.31 13.737 3.596 22.31l.005.02c.015.042.032.081.048.122.052.134.103.267.156.398.28.718.579 1.405.895 2.062 1.885 4.028 4.46 7.59 7.934 9.882a25.252 25.252 0 004.372 2.762c5.907 9.749 18.442 22.252 42.075 14.859 36.255-10.284 56.263 13.809 58.568 15.5 3.399 3.433-8.786-29.835-34.587-44.788-15.253-8.322-5.678-22.656-4.585-27.718 0 0 12.227 8.557 21.087-4.52 8.004 2.062 13.367-1.462 20.25 1.03 4.184 1.833 21.77.726 15.235-9.104 4.11-2.683 4.544-1.815 6.6-5.9 1.104-4.952-1.403-6.012-2.167-7.366z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
addon/content/icons/icon@2x.svg
Normal file
1
addon/content/icons/icon@2x.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="-7.65 -13.389 144.98 160"><path fill="#a04d32" stroke="#000" stroke-width="3" d="M133.399 46.067c-.205-3.15-2.842-4.366-5.993-2.125-7.22-1.297-14.305-.687-17.8-.981-7.662-1.073-14.041-5.128-14.041-5.128.932-1.239.486-3.917-5.498-4.101-1.646-.542-3.336-1.327-4.933-1.979.544-1.145-.133-2.836-.133-2.836 2.435-.672 2.808-3.842 1.848-5.709 3.106.084 2.612-4.718 2.183-6.381 2.435-.923 2.77-3.831 1.763-6.129 2.938-.671 3.022-4.114 2.77-6.548 3.023-.168 2.604-5.457 2.604-6.549 2.604-1.679 2.016-3.946 2.425-6.573 1.605-3.25-.577-4.173-2.116-.71-1.651 3.001-3.77 4.311-3.75 6.528.755 1.259-5.625 3.106-3.61 7.052-1.428 1.763-4.785 4.03-3.592 6.733-.606 1.326-4.888 4.433-3.041 7.371-4.03 2.687-3.79 3.335-2.938 5.793-1.147.736-2.318 1.862-2.995 3.094-1.32-1.568-2.603-4.429-2.584-8.294 0-3.275-6.1.318-6.1 6.784 0 .556-.056 1.061-.134 1.542-2.11.243-4.751.707-8.08 1.494-.106.073-.157.186-.182.316a8.704 8.704 0 01-.277-1.553c-.582-3.79-4.934-9.56-7.057-2.434-1.096 2.611-1.74 4.392-2.115 5.789v0s-.336.226-.957.61c-2.62 1.622-3.562 6.686-13.075 9.883-3.211 1.079-7.4 1.945-12.96 2.395-9.57.773-27.887 17.314-29.114 33.097-.283 3.964.31 13.737 3.596 22.31l.005.02c.015.042.032.081.048.122.052.134.103.267.156.398.28.718.579 1.405.895 2.062 1.885 4.028 4.46 7.59 7.934 9.882a25.252 25.252 0 004.372 2.762c5.907 9.749 18.442 22.252 42.075 14.859 36.255-10.284 56.263 13.809 58.568 15.5 3.399 3.433-8.786-29.835-34.587-44.788-15.253-8.322-5.678-22.656-4.585-27.718 0 0 12.227 8.557 21.087-4.52 8.004 2.062 13.367-1.462 20.25 1.03 4.184 1.833 21.77.726 15.235-9.104 4.11-2.683 4.544-1.815 6.6-5.9 1.104-4.952-1.403-6.012-2.167-7.366z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
addon/content/icons/menu-icon.svg
Normal file
1
addon/content/icons/menu-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-7.65 -13.389 144.98 160"><path fill="#a04d32" stroke="#000" stroke-width="3" d="M133.399 46.067c-.205-3.15-2.842-4.366-5.993-2.125-7.22-1.297-14.305-.687-17.8-.981-7.662-1.073-14.041-5.128-14.041-5.128.932-1.239.486-3.917-5.498-4.101-1.646-.542-3.336-1.327-4.933-1.979.544-1.145-.133-2.836-.133-2.836 2.435-.672 2.808-3.842 1.848-5.709 3.106.084 2.612-4.718 2.183-6.381 2.435-.923 2.77-3.831 1.763-6.129 2.938-.671 3.022-4.114 2.77-6.548 3.023-.168 2.604-5.457 2.604-6.549 2.604-1.679 2.016-3.946 2.425-6.573 1.605-3.25-.577-4.173-2.116-.71-1.651 3.001-3.77 4.311-3.75 6.528.755 1.259-5.625 3.106-3.61 7.052-1.428 1.763-4.785 4.03-3.592 6.733-.606 1.326-4.888 4.433-3.041 7.371-4.03 2.687-3.79 3.335-2.938 5.793-1.147.736-2.318 1.862-2.995 3.094-1.32-1.568-2.603-4.429-2.584-8.294 0-3.275-6.1.318-6.1 6.784 0 .556-.056 1.061-.134 1.542-2.11.243-4.751.707-8.08 1.494-.106.073-.157.186-.182.316a8.704 8.704 0 01-.277-1.553c-.582-3.79-4.934-9.56-7.057-2.434-1.096 2.611-1.74 4.392-2.115 5.789v0s-.336.226-.957.61c-2.62 1.622-3.562 6.686-13.075 9.883-3.211 1.079-7.4 1.945-12.96 2.395-9.57.773-27.887 17.314-29.114 33.097-.283 3.964.31 13.737 3.596 22.31l.005.02c.015.042.032.081.048.122.052.134.103.267.156.398.28.718.579 1.405.895 2.062 1.885 4.028 4.46 7.59 7.934 9.882a25.252 25.252 0 004.372 2.762c5.907 9.749 18.442 22.252 42.075 14.859 36.255-10.284 56.263 13.809 58.568 15.5 3.399 3.433-8.786-29.835-34.587-44.788-15.253-8.322-5.678-22.656-4.585-27.718 0 0 12.227 8.557 21.087-4.52 8.004 2.062 13.367-1.462 20.25 1.03 4.184 1.833 21.77.726 15.235-9.104 4.11-2.683 4.544-1.815 6.6-5.9 1.104-4.952-1.403-6.012-2.167-7.366z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
83
addon/content/preferences.xhtml
Normal file
83
addon/content/preferences.xhtml
Normal file
@@ -0,0 +1,83 @@
|
||||
<vbox id="orgexportannotations-prefs"
|
||||
onload="window.OrgExportAnnotationsPrefs?.init()">
|
||||
|
||||
<linkset>
|
||||
<html:link rel="localization" href="orgexportannotations-addon.ftl"/>
|
||||
<html:link rel="localization" href="orgexportannotations-preferences.ftl"/>
|
||||
</linkset>
|
||||
|
||||
<!-- Paths Section -->
|
||||
<groupbox>
|
||||
<caption data-l10n-id="prefs-section-paths"/>
|
||||
|
||||
<vbox class="orgexportannotations-pref-row">
|
||||
<hbox align="center">
|
||||
<label data-l10n-id="prefs-notes-path"
|
||||
control="orgexportannotations-notes-path"/>
|
||||
<html:input id="orgexportannotations-notes-path"
|
||||
type="text"
|
||||
preference="extensions.zotero.orgexportannotations.notesPath"
|
||||
style="flex: 1;"/>
|
||||
<button data-l10n-id="prefs-browse"
|
||||
oncommand="window.OrgExportAnnotationsPrefs?.browseNotesPath()"/>
|
||||
</hbox>
|
||||
<description data-l10n-id="prefs-notes-path-description"
|
||||
class="orgexportannotations-pref-description"/>
|
||||
</vbox>
|
||||
|
||||
<vbox class="orgexportannotations-pref-row">
|
||||
<hbox align="center">
|
||||
<label data-l10n-id="prefs-pandoc-path"
|
||||
control="orgexportannotations-pandoc-path"/>
|
||||
<html:input id="orgexportannotations-pandoc-path"
|
||||
type="text"
|
||||
preference="extensions.zotero.orgexportannotations.pandocPath"
|
||||
style="flex: 1;"/>
|
||||
<button data-l10n-id="prefs-browse"
|
||||
oncommand="window.OrgExportAnnotationsPrefs?.browsePandocPath()"/>
|
||||
<button data-l10n-id="prefs-test-pandoc"
|
||||
oncommand="window.OrgExportAnnotationsPrefs?.testPandoc()"/>
|
||||
</hbox>
|
||||
<description data-l10n-id="prefs-pandoc-path-description"
|
||||
class="orgexportannotations-pref-description"/>
|
||||
</vbox>
|
||||
</groupbox>
|
||||
|
||||
<!-- Behavior Section -->
|
||||
<groupbox>
|
||||
<caption data-l10n-id="prefs-section-behavior"/>
|
||||
|
||||
<checkbox id="orgexportannotations-attach-org"
|
||||
data-l10n-id="prefs-attach-org"
|
||||
preference="extensions.zotero.orgexportannotations.attachOrgFile"/>
|
||||
|
||||
<checkbox id="orgexportannotations-auto-export-sync"
|
||||
data-l10n-id="prefs-auto-export-sync"
|
||||
preference="extensions.zotero.orgexportannotations.autoExportOnSync"/>
|
||||
|
||||
<checkbox id="orgexportannotations-export-tab-close"
|
||||
data-l10n-id="prefs-export-tab-close"
|
||||
preference="extensions.zotero.orgexportannotations.exportOnTabClose"/>
|
||||
|
||||
<checkbox id="orgexportannotations-show-notification"
|
||||
data-l10n-id="prefs-show-notification"
|
||||
preference="extensions.zotero.orgexportannotations.showNotification"/>
|
||||
</groupbox>
|
||||
|
||||
<!-- Advanced Section -->
|
||||
<groupbox>
|
||||
<caption data-l10n-id="prefs-section-advanced"/>
|
||||
|
||||
<checkbox id="orgexportannotations-debug"
|
||||
data-l10n-id="prefs-debug"
|
||||
preference="extensions.zotero.orgexportannotations.debug"/>
|
||||
|
||||
<hbox>
|
||||
<button data-l10n-id="prefs-export-now"
|
||||
oncommand="window.OrgExportAnnotationsPrefs?.exportAllNow()"/>
|
||||
<button data-l10n-id="prefs-force-export"
|
||||
oncommand="window.OrgExportAnnotationsPrefs?.forceExportAllNow()"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
||||
</vbox>
|
||||
76
addon/content/scripts/preferences.js
Normal file
76
addon/content/scripts/preferences.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const OrgExportAnnotationsPrefs = {
|
||||
init() {
|
||||
try {
|
||||
console.log("[OrgExportAnnotationsPrefs] Initializing preferences pane");
|
||||
} catch (error) {
|
||||
console.error("[OrgExportAnnotationsPrefs] Init error:", error);
|
||||
}
|
||||
},
|
||||
async browseNotesPath() {
|
||||
const fp = new FilePicker();
|
||||
// Use browsingContext for Zotero 8+, fall back to window for Zotero 7
|
||||
const context = window.browsingContext || window;
|
||||
fp.init(context, "Select Notes Directory", fp.modeGetFolder);
|
||||
|
||||
const result = await new Promise((resolve) => fp.open(resolve));
|
||||
if (result === fp.returnOK) {
|
||||
const input = document.getElementById("orgexportannotations-notes-path");
|
||||
if (input) {
|
||||
input.value = fp.file.path;
|
||||
input.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async browsePandocPath() {
|
||||
const fp = new FilePicker();
|
||||
// Use browsingContext for Zotero 8+, fall back to window for Zotero 7
|
||||
const context = window.browsingContext || window;
|
||||
fp.init(context, "Select Pandoc Executable", fp.modeOpen);
|
||||
|
||||
const result = await new Promise((resolve) => fp.open(resolve));
|
||||
if (result === fp.returnOK) {
|
||||
const input = document.getElementById("orgexportannotations-pandoc-path");
|
||||
if (input) {
|
||||
input.value = fp.file.path;
|
||||
input.dispatchEvent(new Event("change"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async testPandoc() {
|
||||
const input = document.getElementById("orgexportannotations-pandoc-path");
|
||||
const pandocPath = (input && input.value) || "pandoc";
|
||||
|
||||
try {
|
||||
await Zotero.Utilities.Internal.exec(pandocPath, ["--version"]);
|
||||
alert("Pandoc is working correctly!");
|
||||
} catch (_error) {
|
||||
alert(`Pandoc not found at: ${pandocPath}\n\nPlease check the path.`);
|
||||
}
|
||||
},
|
||||
|
||||
async exportAllNow() {
|
||||
try {
|
||||
if (Zotero.OrgExportAnnotations && Zotero.OrgExportAnnotations.hooks) {
|
||||
await Zotero.OrgExportAnnotations.hooks.onMenuExportAll();
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Export failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async forceExportAllNow() {
|
||||
try {
|
||||
if (Zotero.OrgExportAnnotations && Zotero.OrgExportAnnotations.hooks) {
|
||||
await Zotero.OrgExportAnnotations.hooks.onMenuForceExportAll();
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Force export failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.OrgExportAnnotationsPrefs = OrgExportAnnotationsPrefs;
|
||||
27
addon/locale/en-US/addon.ftl
Normal file
27
addon/locale/en-US/addon.ftl
Normal file
@@ -0,0 +1,27 @@
|
||||
name = Zotero Org Export Annotations
|
||||
description = Export PDF/EPUB annotations to Org-mode files
|
||||
|
||||
menu-export-all = Export All Annotations to Org
|
||||
menu-export-selected = Export Selected to Org
|
||||
menu-preferences = Org Export Preferences...
|
||||
|
||||
notification-export-complete =
|
||||
{ $count ->
|
||||
[one] Exported { $count } item to Org
|
||||
*[other] Exported { $count } items to Org
|
||||
}
|
||||
notification-export-error = Export failed: { $error }
|
||||
notification-no-items = No items with annotations to export
|
||||
notification-config-required = Please configure the notes path in preferences
|
||||
|
||||
error-better-notes-required =
|
||||
Better Notes plugin is required for Org export.
|
||||
Please install it from: https://github.com/windingwind/zotero-better-notes
|
||||
error-pandoc-not-found =
|
||||
Pandoc not found at: { $path }
|
||||
Please install pandoc or configure the correct path in preferences.
|
||||
error-notes-path-not-set =
|
||||
Notes output path is not configured.
|
||||
Please set it in Tools > Org Export Preferences.
|
||||
error-notes-path-invalid =
|
||||
Notes path does not exist: { $path }
|
||||
40
addon/locale/en-US/preferences.ftl
Normal file
40
addon/locale/en-US/preferences.ftl
Normal file
@@ -0,0 +1,40 @@
|
||||
prefs-title = Org Export Annotations
|
||||
|
||||
prefs-section-paths =
|
||||
.label = Output Paths
|
||||
prefs-notes-path =
|
||||
.value = Notes Directory:
|
||||
prefs-notes-path-description =
|
||||
Directory where Org files will be saved.
|
||||
Files are named using the citation key (e.g., smith2020.org).
|
||||
prefs-pandoc-path =
|
||||
.value = Pandoc Path:
|
||||
prefs-pandoc-path-description =
|
||||
Path to pandoc executable. Leave as "pandoc" to use system PATH.
|
||||
prefs-browse =
|
||||
.label = Browse...
|
||||
|
||||
prefs-section-behavior =
|
||||
.label = Behavior
|
||||
prefs-attach-org =
|
||||
.label = Attach Org files to Zotero items
|
||||
prefs-attach-org-description =
|
||||
Link generated Org files as attachments to their parent items.
|
||||
prefs-auto-export-sync =
|
||||
.label = Export automatically after Zotero sync
|
||||
prefs-export-tab-close =
|
||||
.label = Export when closing reader tab
|
||||
prefs-show-notification =
|
||||
.label = Show notification after export
|
||||
|
||||
prefs-section-advanced =
|
||||
.label = Advanced
|
||||
prefs-debug =
|
||||
.label = Enable debug logging
|
||||
|
||||
prefs-export-now =
|
||||
.label = Export All Now
|
||||
prefs-force-export =
|
||||
.label = Force Export All
|
||||
prefs-test-pandoc =
|
||||
.label = Test Pandoc
|
||||
20
addon/manifest.json
Normal file
20
addon/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Zotero Org Export Annotations",
|
||||
"version": "1.0.0",
|
||||
"description": "Export Zotero PDF/EPUB annotations to Org-mode files for Emacs, org-roam, and citar",
|
||||
"author": "Ignacio",
|
||||
"homepage_url": "https://github.com/ignaciobll/zotero-org-export-annotations",
|
||||
"icons": {
|
||||
"48": "content/icons/icon.svg",
|
||||
"96": "content/icons/icon@2x.svg"
|
||||
},
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"id": "org-export-annotations@zotero.org",
|
||||
"update_url": "https://github.com/ignaciobll/zotero-org-export-annotations/releases/download/release/update.json",
|
||||
"strict_min_version": "7.0",
|
||||
"strict_max_version": "8.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
addon/prefs.js
Normal file
7
addon/prefs.js
Normal file
@@ -0,0 +1,7 @@
|
||||
pref("extensions.zotero.orgexportannotations.notesPath", "");
|
||||
pref("extensions.zotero.orgexportannotations.pandocPath", "pandoc");
|
||||
pref("extensions.zotero.orgexportannotations.attachOrgFile", false);
|
||||
pref("extensions.zotero.orgexportannotations.autoExportOnSync", true);
|
||||
pref("extensions.zotero.orgexportannotations.exportOnTabClose", true);
|
||||
pref("extensions.zotero.orgexportannotations.showNotification", true);
|
||||
pref("extensions.zotero.orgexportannotations.debug", false);
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771008912,
|
||||
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
72
flake.nix
Normal file
72
flake.nix
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
description = "Zotero Org Export Annotations - Export Zotero annotations to Org-mode files";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# Node.js ecosystem
|
||||
nodejs_20
|
||||
nodePackages.npm
|
||||
|
||||
# TypeScript tooling
|
||||
nodePackages.typescript
|
||||
nodePackages.typescript-language-server
|
||||
|
||||
# Code quality
|
||||
nodePackages.eslint
|
||||
nodePackages.prettier
|
||||
|
||||
# Runtime dependency (for testing exports)
|
||||
pandoc
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Zotero Org Export Annotations - Dev Environment ║"
|
||||
echo "╠══════════════════════════════════════════════════════════╣"
|
||||
echo "║ Commands: ║"
|
||||
echo "║ npm install - Install dependencies ║"
|
||||
echo "║ npm start - Start dev server with hot reload ║"
|
||||
echo "║ npm run build - Build production XPI ║"
|
||||
echo "║ npm run lint - Run ESLint ║"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
export PATH="$PWD/node_modules/.bin:$PATH"
|
||||
'';
|
||||
};
|
||||
|
||||
packages.default = pkgs.stdenv.mkDerivation {
|
||||
pname = "zotero-org-export-annotations";
|
||||
version = "1.0.0";
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
nodejs_20
|
||||
nodePackages.npm
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
export HOME=$(mktemp -d)
|
||||
npm ci --ignore-scripts
|
||||
npm run build
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r .scaffold/build/*.xpi $out/
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
4488
package-lock.json
generated
Normal file
4488
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
package.json
Normal file
53
package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "zotero-org-export-annotations",
|
||||
"version": "1.0.0",
|
||||
"description": "Export Zotero PDF/EPUB annotations to Org-mode files for Emacs integration",
|
||||
"config": {
|
||||
"addonName": "Zotero Org Export Annotations",
|
||||
"addonID": "org-export-annotations@zotero.org",
|
||||
"addonRef": "orgexportannotations",
|
||||
"addonInstance": "OrgExportAnnotations",
|
||||
"prefsPrefix": "extensions.zotero.orgexportannotations"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "zotero-plugin serve",
|
||||
"build": "zotero-plugin build",
|
||||
"release": "bumpp --commit --push --tag && npm run build",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"addon/**/*.js\"",
|
||||
"format:check": "prettier --check \"src/**/*.ts\" \"addon/**/*.js\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ignaciobll/zotero-org-export-annotations.git"
|
||||
},
|
||||
"keywords": [
|
||||
"zotero",
|
||||
"zotero-plugin",
|
||||
"org-mode",
|
||||
"emacs",
|
||||
"annotations",
|
||||
"pdf",
|
||||
"org-roam",
|
||||
"citar"
|
||||
],
|
||||
"author": "Ignacio",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ignaciobll/zotero-org-export-annotations/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ignaciobll/zotero-org-export-annotations#readme",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"bumpp": "^9.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^3.2.0",
|
||||
"typescript": "^5.3.0",
|
||||
"zotero-plugin-scaffold": "^0.8.3",
|
||||
"zotero-types": "^4.0.5"
|
||||
}
|
||||
}
|
||||
53
src/addon.ts
Normal file
53
src/addon.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { OrgExporter } from "./modules/exporter";
|
||||
import { Prefs } from "./modules/prefs";
|
||||
|
||||
export interface AddonInfo {
|
||||
id: string;
|
||||
version: string;
|
||||
rootURI: string;
|
||||
}
|
||||
|
||||
export class OrgExportAnnotationsAddon {
|
||||
public id!: string;
|
||||
public version!: string;
|
||||
public rootURI!: string;
|
||||
|
||||
public data = {
|
||||
env: typeof __env__ !== "undefined" ? __env__ : "production",
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
public prefs!: Prefs;
|
||||
public exporter!: OrgExporter;
|
||||
|
||||
init(info: AddonInfo): void {
|
||||
this.id = info.id;
|
||||
this.version = info.version;
|
||||
this.rootURI = info.rootURI;
|
||||
|
||||
this.prefs = new Prefs();
|
||||
this.exporter = new OrgExporter(this.prefs);
|
||||
|
||||
this.log(`Initialized version ${this.version}`);
|
||||
}
|
||||
|
||||
log(message: string, ...args: unknown[]): void {
|
||||
if (this.data.env === "development" || this.prefs?.debug) {
|
||||
Zotero.debug(`[OrgExportAnnotations] ${message}`, false);
|
||||
if (args.length > 0) {
|
||||
Zotero.debug(JSON.stringify(args, null, 2), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string, error?: Error): void {
|
||||
Zotero.logError(`[OrgExportAnnotations] ${message}`);
|
||||
if (error) {
|
||||
Zotero.logError(error);
|
||||
}
|
||||
}
|
||||
|
||||
warn(message: string): void {
|
||||
Zotero.debug(`[OrgExportAnnotations] [WARN] ${message}`, false);
|
||||
}
|
||||
}
|
||||
157
src/hooks.ts
Normal file
157
src/hooks.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { OrgExportAnnotationsAddon } from "./addon";
|
||||
import { registerNotifiers, unregisterNotifiers } from "./modules/notifier";
|
||||
import { registerMenus, unregisterMenus } from "./modules/menu";
|
||||
|
||||
export interface Hooks {
|
||||
onStartup(): Promise<void>;
|
||||
onShutdown(): void;
|
||||
onMainWindowLoad(window: Window): Promise<void>;
|
||||
onMainWindowUnload(window: Window): Promise<void>;
|
||||
onMenuExportAll(): Promise<void>;
|
||||
onMenuForceExportAll(): Promise<void>;
|
||||
onMenuExportSelected(): Promise<void>;
|
||||
}
|
||||
|
||||
function showNotification(message: string): void {
|
||||
const pw = new Zotero.ProgressWindow({ closeOnClick: true });
|
||||
pw.changeHeadline("Org Export");
|
||||
pw.addDescription(message);
|
||||
pw.show();
|
||||
pw.startCloseTimer(4000);
|
||||
}
|
||||
|
||||
export function createHooks(addon: OrgExportAnnotationsAddon): Hooks {
|
||||
let notifierIDs: string[] = [];
|
||||
|
||||
return {
|
||||
async onStartup(): Promise<void> {
|
||||
addon.log("Starting up...");
|
||||
|
||||
try {
|
||||
// Wait for Zotero UI to be fully ready before registering menus
|
||||
await Promise.all([
|
||||
Zotero.initializationPromise,
|
||||
Zotero.unlockPromise,
|
||||
Zotero.uiReadyPromise,
|
||||
]);
|
||||
|
||||
if (!Zotero.BetterNotes) {
|
||||
addon.warn("Better Notes plugin not found - some features may not work");
|
||||
}
|
||||
|
||||
Zotero.PreferencePanes.register({
|
||||
pluginID: addon.id,
|
||||
src: addon.rootURI + "content/preferences.xhtml",
|
||||
scripts: [addon.rootURI + "content/scripts/preferences.js"],
|
||||
label: "Org Export",
|
||||
image: addon.rootURI + "content/icons/icon.svg",
|
||||
});
|
||||
|
||||
notifierIDs = registerNotifiers(addon);
|
||||
|
||||
// Register menus for any windows that are already open
|
||||
// (onMainWindowLoad may have fired before our startup completed)
|
||||
const mainWindows = Zotero.getMainWindows();
|
||||
for (const win of mainWindows) {
|
||||
await this.onMainWindowLoad(win);
|
||||
}
|
||||
|
||||
addon.data.initialized = true;
|
||||
addon.log("Startup complete");
|
||||
} catch (error) {
|
||||
addon.error("Startup failed", error as Error);
|
||||
addon.data.initialized = false;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
onShutdown(): void {
|
||||
addon.log("Shutting down...");
|
||||
|
||||
try {
|
||||
unregisterNotifiers(notifierIDs);
|
||||
notifierIDs = [];
|
||||
|
||||
addon.data.initialized = false;
|
||||
|
||||
addon.log("Shutdown complete");
|
||||
} catch (error) {
|
||||
addon.error("Shutdown failed", error as Error);
|
||||
}
|
||||
},
|
||||
|
||||
async onMainWindowLoad(window: Window): Promise<void> {
|
||||
addon.log("Main window loaded");
|
||||
registerMenus(addon, window);
|
||||
},
|
||||
|
||||
async onMainWindowUnload(window: Window): Promise<void> {
|
||||
addon.log("Main window unloading");
|
||||
unregisterMenus(window);
|
||||
},
|
||||
|
||||
async onMenuExportAll(): Promise<void> {
|
||||
addon.log("Menu: Export All triggered");
|
||||
try {
|
||||
const count = await addon.exporter.syncAll();
|
||||
if (addon.prefs.showNotification) {
|
||||
const message =
|
||||
count > 0
|
||||
? `Exported ${count} item${count === 1 ? "" : "s"} to Org`
|
||||
: "No items needed export";
|
||||
showNotification(message);
|
||||
}
|
||||
} catch (error) {
|
||||
addon.error("Export All failed", error as Error);
|
||||
showNotification(`Export failed: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async onMenuForceExportAll(): Promise<void> {
|
||||
addon.log("Menu: Force Export All triggered");
|
||||
try {
|
||||
const count = await addon.exporter.forceExportAll();
|
||||
if (addon.prefs.showNotification) {
|
||||
const message =
|
||||
count > 0
|
||||
? `Force exported ${count} item${count === 1 ? "" : "s"} to Org`
|
||||
: "No items with annotations to export";
|
||||
showNotification(message);
|
||||
}
|
||||
} catch (error) {
|
||||
addon.error("Force Export All failed", error as Error);
|
||||
showNotification(`Export failed: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async onMenuExportSelected(): Promise<void> {
|
||||
addon.log("Menu: Export Selected triggered");
|
||||
try {
|
||||
const zoteroPane = Zotero.getActiveZoteroPane();
|
||||
const selectedItems = zoteroPane.getSelectedItems();
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
showNotification("No items selected");
|
||||
return;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const item of selectedItems) {
|
||||
const exported = await addon.exporter.exportItem(item);
|
||||
if (exported) count++;
|
||||
}
|
||||
|
||||
if (addon.prefs.showNotification) {
|
||||
const message =
|
||||
count > 0
|
||||
? `Exported ${count} item${count === 1 ? "" : "s"} to Org`
|
||||
: "No annotations found in selected items";
|
||||
showNotification(message);
|
||||
}
|
||||
} catch (error) {
|
||||
addon.error("Export Selected failed", error as Error);
|
||||
showNotification(`Export failed: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
31
src/index.ts
Normal file
31
src/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { OrgExportAnnotationsAddon } from "./addon";
|
||||
import { createHooks } from "./hooks";
|
||||
|
||||
const addon = new OrgExportAnnotationsAddon();
|
||||
const hooks = createHooks(addon);
|
||||
|
||||
Zotero.OrgExportAnnotations = {
|
||||
init: addon.init.bind(addon),
|
||||
log: addon.log.bind(addon),
|
||||
error: addon.error.bind(addon),
|
||||
warn: addon.warn.bind(addon),
|
||||
get id() {
|
||||
return addon.id;
|
||||
},
|
||||
get version() {
|
||||
return addon.version;
|
||||
},
|
||||
get rootURI() {
|
||||
return addon.rootURI;
|
||||
},
|
||||
get data() {
|
||||
return addon.data;
|
||||
},
|
||||
get prefs() {
|
||||
return addon.prefs;
|
||||
},
|
||||
get exporter() {
|
||||
return addon.exporter;
|
||||
},
|
||||
hooks,
|
||||
};
|
||||
58
src/modules/converter.ts
Normal file
58
src/modules/converter.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export async function convertToOrg(
|
||||
note: Zotero.Item,
|
||||
notesPath: string,
|
||||
orgPath: string,
|
||||
title: string,
|
||||
pandocPath: string
|
||||
): Promise<void> {
|
||||
const citationKey = note.parentItem?.getField("citationKey") || "temp";
|
||||
const mdPath = PathUtils.join(notesPath, `${citationKey}.md`);
|
||||
|
||||
try {
|
||||
const mdContent = await Zotero.BetterNotes!.api.convert.note2md(note, notesPath);
|
||||
await Zotero.File.putContentsAsync(mdPath, mdContent);
|
||||
|
||||
await Zotero.Utilities.Internal.exec(pandocPath, [
|
||||
"--from=markdown",
|
||||
"--to=org",
|
||||
`--output=${orgPath}`,
|
||||
mdPath,
|
||||
]);
|
||||
|
||||
await prependOrgTitle(orgPath, title);
|
||||
|
||||
await cleanupTempFile(mdPath);
|
||||
} catch (error) {
|
||||
try {
|
||||
await cleanupTempFile(mdPath);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function prependOrgTitle(orgPath: string, title: string): Promise<void> {
|
||||
const orgContent = await Zotero.File.getContentsAsync(orgPath);
|
||||
const withTitle = `#+title: ${title}\n${orgContent}`;
|
||||
await Zotero.File.putContentsAsync(orgPath, withTitle);
|
||||
}
|
||||
|
||||
async function cleanupTempFile(path: string): Promise<void> {
|
||||
try {
|
||||
if (await IOUtils.exists(path)) {
|
||||
await IOUtils.remove(path);
|
||||
}
|
||||
} catch (_error) {
|
||||
Zotero.OrgExportAnnotations.warn(`Failed to clean up temp file: ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function testPandoc(pandocPath: string): Promise<boolean> {
|
||||
try {
|
||||
await Zotero.Utilities.Internal.exec(pandocPath, ["--version"]);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
280
src/modules/exporter.ts
Normal file
280
src/modules/exporter.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import type { Prefs } from "./prefs";
|
||||
import { convertToOrg } from "./converter";
|
||||
|
||||
export class OrgExporter {
|
||||
constructor(private prefs: Prefs) {}
|
||||
|
||||
isAttachmentWithExtractableAnnotations(item: Zotero.Item): boolean {
|
||||
if (!item.isFileAttachment()) {
|
||||
Zotero.OrgExportAnnotations.warn(`Item ${item.id}: not a file attachment`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isSupported =
|
||||
item.isPDFAttachment() || item.isEPUBAttachment() || item.isSnapshotAttachment();
|
||||
|
||||
if (!isSupported) {
|
||||
Zotero.OrgExportAnnotations.warn(
|
||||
`Item ${item.id}: unsupported attachment type (not PDF/EPUB/Snapshot)`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const annotations = item.getAnnotations();
|
||||
Zotero.OrgExportAnnotations.warn(
|
||||
`Item ${item.id}: found ${annotations?.length ?? 0} annotations`
|
||||
);
|
||||
return annotations && annotations.length > 0;
|
||||
} catch (error) {
|
||||
Zotero.OrgExportAnnotations.error("Failed to check annotations", error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isAnnotationNote(noteHTML: string): boolean {
|
||||
return (
|
||||
noteHTML.startsWith("<div data-citation-items") ||
|
||||
noteHTML.startsWith("<div datap-citation-items") ||
|
||||
noteHTML.startsWith("<h1>Annotations")
|
||||
);
|
||||
}
|
||||
|
||||
private async getLastAnnotationsNote(item: Zotero.Item): Promise<Zotero.Item | undefined> {
|
||||
const noteIDs = item.getNotes();
|
||||
const notes = Zotero.Items.get(noteIDs).filter((note) => this.isAnnotationNote(note.getNote()));
|
||||
|
||||
if (notes.length === 0) return undefined;
|
||||
|
||||
return notes.reduce((max, current) =>
|
||||
current.dateModified > max.dateModified ? current : max
|
||||
);
|
||||
}
|
||||
|
||||
private async removeExistingNotes(item: Zotero.Item): Promise<void> {
|
||||
try {
|
||||
const noteIDs = item.getNotes();
|
||||
const notes = Zotero.Items.get(noteIDs).filter((note) =>
|
||||
this.isAnnotationNote(note.getNote())
|
||||
);
|
||||
|
||||
for (const note of notes) {
|
||||
await Zotero.Items.trashTx(note.id);
|
||||
Zotero.OrgExportAnnotations.log(`Removed old annotation note: ${note.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
Zotero.OrgExportAnnotations.error("Failed to remove existing notes", error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
private getAttachmentLastModifiedAt(attachment: Zotero.Item): string | undefined {
|
||||
if (!this.isAttachmentWithExtractableAnnotations(attachment)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const annotations = attachment.getAnnotations();
|
||||
if (!annotations || annotations.length === 0) return undefined;
|
||||
|
||||
return annotations.reduce((max, current) =>
|
||||
current.dateModified > max.dateModified ? current : max
|
||||
).dateModified;
|
||||
}
|
||||
|
||||
async needsOrgSync(attachment: Zotero.Item): Promise<boolean> {
|
||||
if (!attachment.parentItem) return false;
|
||||
|
||||
const note = await this.getLastAnnotationsNote(attachment.parentItem);
|
||||
const annotationDateModified = this.getAttachmentLastModifiedAt(attachment);
|
||||
|
||||
if (note && annotationDateModified) {
|
||||
return annotationDateModified > note.dateModified;
|
||||
}
|
||||
|
||||
if (annotationDateModified && !note) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async createNote(attachment: Zotero.Item): Promise<Zotero.Item | undefined> {
|
||||
try {
|
||||
const ZoteroPane = Zotero.getActiveZoteroPane();
|
||||
const note = await ZoteroPane.addNoteFromAnnotationsForAttachment(attachment, {
|
||||
skipSelect: true,
|
||||
});
|
||||
Zotero.OrgExportAnnotations.log(`Created note from annotations: ${note?.id}`);
|
||||
return note;
|
||||
} catch (error) {
|
||||
Zotero.OrgExportAnnotations.error("Failed to create note", error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private existsOrgAttachment(parentItem: Zotero.Item, path: string): boolean {
|
||||
try {
|
||||
const attachmentIDs = parentItem.getAttachments();
|
||||
const attachments = Zotero.Items.get(attachmentIDs);
|
||||
return attachments.some((a) => a.attachmentPath === path);
|
||||
} catch (error) {
|
||||
Zotero.OrgExportAnnotations.error(
|
||||
"Failed to check for existing org attachment",
|
||||
error as Error
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async exportAttachment(attachment: Zotero.Item): Promise<boolean> {
|
||||
Zotero.OrgExportAnnotations.warn(`exportAttachment called: id=${attachment.id}`);
|
||||
|
||||
if (!this.isAttachmentWithExtractableAnnotations(attachment)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!attachment.parentItem) {
|
||||
Zotero.OrgExportAnnotations.warn("Attachment has no parent item");
|
||||
return false;
|
||||
}
|
||||
|
||||
const notesPath = this.prefs.notesPath;
|
||||
Zotero.OrgExportAnnotations.warn(`notesPath = "${notesPath}"`);
|
||||
if (!notesPath) {
|
||||
throw new Error("Notes path is not configured");
|
||||
}
|
||||
|
||||
if (!Zotero.BetterNotes?.api?.convert?.note2md) {
|
||||
throw new Error("Better Notes plugin is required but not found");
|
||||
}
|
||||
|
||||
await this.prefs.ensureNotesDirectory();
|
||||
|
||||
await this.removeExistingNotes(attachment.parentItem);
|
||||
const note = await this.createNote(attachment);
|
||||
|
||||
if (!note) {
|
||||
Zotero.OrgExportAnnotations.warn("Failed to create note for attachment");
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = attachment.parentItem.getField("title") as string;
|
||||
const citationKey = attachment.parentItem.getField("citationKey") as string;
|
||||
Zotero.OrgExportAnnotations.warn(`title="${title}", citationKey="${citationKey}"`);
|
||||
|
||||
if (!citationKey) {
|
||||
Zotero.OrgExportAnnotations.warn(`Item "${title}" has no citation key, skipping`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const orgPath = PathUtils.join(notesPath, `${citationKey}.org`);
|
||||
Zotero.OrgExportAnnotations.warn(`Converting to org at: ${orgPath}`);
|
||||
|
||||
await convertToOrg(note, notesPath, orgPath, title, this.prefs.pandocPath);
|
||||
|
||||
if (this.prefs.attachOrgFile && !this.existsOrgAttachment(attachment.parentItem, orgPath)) {
|
||||
await Zotero.Attachments.linkFromFile({
|
||||
file: orgPath,
|
||||
parentItemID: attachment.parentItem.id,
|
||||
contentType: "text/org",
|
||||
title: `${title} (Org)`,
|
||||
});
|
||||
}
|
||||
|
||||
Zotero.OrgExportAnnotations.warn(`Exported to ${orgPath}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async exportItem(item: Zotero.Item): Promise<boolean> {
|
||||
Zotero.OrgExportAnnotations.warn(
|
||||
`exportItem called: id=${item.id}, isAttachment=${item.isAttachment()}, isRegularItem=${item.isRegularItem()}`
|
||||
);
|
||||
|
||||
if (item.isAttachment()) {
|
||||
return this.exportAttachment(item);
|
||||
}
|
||||
|
||||
if (!item.isRegularItem()) {
|
||||
Zotero.OrgExportAnnotations.warn(`Item ${item.id}: not a regular item, skipping`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachmentIDs = item.getAttachments();
|
||||
const attachments = Zotero.Items.get(attachmentIDs);
|
||||
Zotero.OrgExportAnnotations.warn(
|
||||
`Item ${item.id}: found ${attachments.length} attachments (IDs: ${attachmentIDs.join(", ")})`
|
||||
);
|
||||
|
||||
let exported = false;
|
||||
for (const attachment of attachments) {
|
||||
if (await this.exportAttachment(attachment)) {
|
||||
exported = true;
|
||||
}
|
||||
}
|
||||
|
||||
return exported;
|
||||
}
|
||||
|
||||
async listAllAttachments(): Promise<Zotero.Item[]> {
|
||||
try {
|
||||
const search = new Zotero.Search();
|
||||
search.libraryID = Zotero.Libraries.userLibraryID;
|
||||
search.addCondition("itemType", "is", "attachment");
|
||||
const itemIDs = await search.search();
|
||||
|
||||
const uniqueIDs = [...new Set(itemIDs)];
|
||||
const items = Zotero.Items.get(uniqueIDs);
|
||||
|
||||
return items
|
||||
.filter((item) => this.isAttachmentWithExtractableAnnotations(item))
|
||||
.filter((item) => item.parentItem !== undefined);
|
||||
} catch (error) {
|
||||
Zotero.OrgExportAnnotations.error("Failed to list attachments", error as Error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async syncAll(): Promise<number> {
|
||||
const attachments = await this.listAllAttachments();
|
||||
let count = 0;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const needsSync = await this.needsOrgSync(attachment);
|
||||
if (needsSync) {
|
||||
try {
|
||||
if (await this.exportAttachment(attachment)) {
|
||||
count++;
|
||||
}
|
||||
} catch (error) {
|
||||
Zotero.OrgExportAnnotations.error(
|
||||
`Failed to export attachment ${attachment.id}`,
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.OrgExportAnnotations.log(`Synced ${count} attachments`);
|
||||
return count;
|
||||
}
|
||||
|
||||
async forceExportAll(): Promise<number> {
|
||||
const attachments = await this.listAllAttachments();
|
||||
let count = 0;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
try {
|
||||
if (await this.exportAttachment(attachment)) {
|
||||
count++;
|
||||
}
|
||||
} catch (error) {
|
||||
Zotero.OrgExportAnnotations.error(
|
||||
`Failed to force export attachment ${attachment.id}`,
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.OrgExportAnnotations.log(`Force exported ${count} attachments`);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
159
src/modules/menu.ts
Normal file
159
src/modules/menu.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { OrgExportAnnotationsAddon } from "../addon";
|
||||
|
||||
const MENU_IDS = {
|
||||
toolsExportAll: "orgexportannotations-menu-tools-export-all",
|
||||
itemExportSelected: "orgexportannotations-menu-item-export",
|
||||
itemSeparator: "orgexportannotations-menu-separator",
|
||||
};
|
||||
|
||||
// Track registered menu IDs for Zotero 8 MenuManager API
|
||||
let registeredMenuIDs: string[] = [];
|
||||
|
||||
export function registerMenus(addon: OrgExportAnnotationsAddon, window: Window): void {
|
||||
// Use new MenuManager API if available (Zotero 8+)
|
||||
if (Zotero.MenuManager) {
|
||||
registerMenusZotero8(addon);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to DOM manipulation for Zotero 7
|
||||
registerMenusZotero7(addon, window);
|
||||
}
|
||||
|
||||
function registerMenusZotero8(addon: OrgExportAnnotationsAddon): void {
|
||||
// Guard against double registration
|
||||
if (registeredMenuIDs.length > 0) {
|
||||
addon.log("MenuManager menus already registered, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Register item context menu
|
||||
Zotero.MenuManager!.registerMenu({
|
||||
menuID: MENU_IDS.itemExportSelected,
|
||||
pluginID: addon.id,
|
||||
target: "main/library/item",
|
||||
menus: [
|
||||
{
|
||||
menuType: "menuitem",
|
||||
icon: addon.rootURI + "content/icons/menu-icon.svg",
|
||||
onShowing: (_event, context) => {
|
||||
context.menuElem?.setAttribute("label", "Export Annotations to Org");
|
||||
},
|
||||
onCommand: () => Zotero.OrgExportAnnotations.hooks.onMenuExportSelected(),
|
||||
},
|
||||
],
|
||||
});
|
||||
registeredMenuIDs.push(MENU_IDS.itemExportSelected);
|
||||
addon.log("Registered item context menu via MenuManager");
|
||||
|
||||
// Register Tools menu item
|
||||
Zotero.MenuManager!.registerMenu({
|
||||
menuID: MENU_IDS.toolsExportAll,
|
||||
pluginID: addon.id,
|
||||
target: "main/menubar/tools",
|
||||
menus: [
|
||||
{
|
||||
menuType: "menuitem",
|
||||
icon: addon.rootURI + "content/icons/menu-icon.svg",
|
||||
onShowing: (_event, context) => {
|
||||
context.menuElem?.setAttribute("label", "Export All Annotations to Org");
|
||||
},
|
||||
onCommand: () => Zotero.OrgExportAnnotations.hooks.onMenuExportAll(),
|
||||
},
|
||||
],
|
||||
});
|
||||
registeredMenuIDs.push(MENU_IDS.toolsExportAll);
|
||||
addon.log("Registered Tools menu item via MenuManager");
|
||||
} catch (error) {
|
||||
addon.error("Failed to register menus via MenuManager", error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
function registerMenusZotero7(addon: OrgExportAnnotationsAddon, window: Window): void {
|
||||
const doc = window.document;
|
||||
|
||||
// Guard against double registration (onStartup and onMainWindowLoad may both fire)
|
||||
if (doc.getElementById(MENU_IDS.itemExportSelected)) {
|
||||
addon.log("Menu items already registered, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Item context menu — DOM append to zotero-itemmenu
|
||||
const itemMenu = doc.getElementById("zotero-itemmenu");
|
||||
if (itemMenu) {
|
||||
const separator = doc.createXULElement("menuseparator");
|
||||
separator.id = MENU_IDS.itemSeparator;
|
||||
itemMenu.appendChild(separator);
|
||||
|
||||
const menuItem = createMenuItem(doc, {
|
||||
id: MENU_IDS.itemExportSelected,
|
||||
label: "Export Annotations to Org",
|
||||
icon: addon.rootURI + "content/icons/menu-icon.svg",
|
||||
oncommand: () => Zotero.OrgExportAnnotations.hooks.onMenuExportSelected(),
|
||||
});
|
||||
itemMenu.appendChild(menuItem);
|
||||
addon.log("Added item context menu item");
|
||||
} else {
|
||||
addon.warn("Could not find zotero-itemmenu element");
|
||||
}
|
||||
|
||||
// Tools menu — direct DOM append
|
||||
const toolsMenu = doc.getElementById("menu_ToolsPopup");
|
||||
if (toolsMenu) {
|
||||
const menuItem = createMenuItem(doc, {
|
||||
id: MENU_IDS.toolsExportAll,
|
||||
label: "Export All Annotations to Org",
|
||||
icon: addon.rootURI + "content/icons/menu-icon.svg",
|
||||
oncommand: () => Zotero.OrgExportAnnotations.hooks.onMenuExportAll(),
|
||||
});
|
||||
toolsMenu.appendChild(menuItem);
|
||||
addon.log("Added Tools menu item");
|
||||
} else {
|
||||
addon.warn("Could not find menu_ToolsPopup element");
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterMenus(window: Window): void {
|
||||
// For Zotero 8+, MenuManager handles cleanup automatically when plugin is disabled
|
||||
// But we can manually unregister if needed
|
||||
if (Zotero.MenuManager && registeredMenuIDs.length > 0) {
|
||||
for (const menuID of registeredMenuIDs) {
|
||||
try {
|
||||
Zotero.MenuManager.unregisterMenu(menuID);
|
||||
} catch (_error) {
|
||||
// Menu may already be unregistered
|
||||
}
|
||||
}
|
||||
registeredMenuIDs = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// For Zotero 7, remove DOM elements
|
||||
const doc = window.document;
|
||||
const idsToRemove = Object.values(MENU_IDS);
|
||||
for (const id of idsToRemove) {
|
||||
const element = doc.getElementById(id);
|
||||
element?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function createMenuItem(
|
||||
doc: Document,
|
||||
options: {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
oncommand: () => void;
|
||||
}
|
||||
): Element {
|
||||
const menuItem = doc.createXULElement("menuitem");
|
||||
menuItem.id = options.id;
|
||||
menuItem.setAttribute("label", options.label);
|
||||
if (options.icon) {
|
||||
menuItem.setAttribute("class", "menuitem-iconic");
|
||||
menuItem.setAttribute("image", options.icon);
|
||||
}
|
||||
menuItem.addEventListener("command", options.oncommand);
|
||||
return menuItem;
|
||||
}
|
||||
73
src/modules/notifier.ts
Normal file
73
src/modules/notifier.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { OrgExportAnnotationsAddon } from "../addon";
|
||||
|
||||
export function registerNotifiers(addon: OrgExportAnnotationsAddon): string[] {
|
||||
const ids: string[] = [];
|
||||
|
||||
const syncNotifierID = Zotero.Notifier.registerObserver(
|
||||
{
|
||||
notify: async (
|
||||
event: string,
|
||||
type: string,
|
||||
_ids: number[],
|
||||
_extraData: Record<string, unknown>
|
||||
) => {
|
||||
if (event === "finish" && type === "sync") {
|
||||
addon.log("Sync completed, checking for exports...");
|
||||
if (addon.prefs.autoExportOnSync) {
|
||||
try {
|
||||
await addon.exporter.syncAll();
|
||||
} catch (error) {
|
||||
addon.error("Auto-export after sync failed", error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
["sync"],
|
||||
"orgexportannotations-sync"
|
||||
);
|
||||
ids.push(syncNotifierID);
|
||||
|
||||
const tabNotifierID = Zotero.Notifier.registerObserver(
|
||||
{
|
||||
notify: async (
|
||||
event: string,
|
||||
type: string,
|
||||
ids: number[],
|
||||
extraData: Record<string, unknown>
|
||||
) => {
|
||||
if (event === "close" && type === "tab") {
|
||||
if (!addon.prefs.exportOnTabClose) return;
|
||||
|
||||
addon.log("Tab closed, checking for export...");
|
||||
|
||||
for (const tabId of ids) {
|
||||
try {
|
||||
const tabInfo = extraData[tabId] as { itemID?: number } | undefined;
|
||||
if (tabInfo?.itemID) {
|
||||
const item = Zotero.Items.get(tabInfo.itemID);
|
||||
if (item) {
|
||||
await addon.exporter.exportItem(item);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
addon.error(`Failed to export on tab close: ${tabId}`, error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
["tab"],
|
||||
"orgexportannotations-tab"
|
||||
);
|
||||
ids.push(tabNotifierID);
|
||||
|
||||
addon.log(`Registered ${ids.length} notifiers`);
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function unregisterNotifiers(ids: string[]): void {
|
||||
for (const id of ids) {
|
||||
Zotero.Notifier.unregisterObserver(id);
|
||||
}
|
||||
}
|
||||
103
src/modules/prefs.ts
Normal file
103
src/modules/prefs.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
const PREF_PREFIX = "extensions.zotero.orgexportannotations";
|
||||
|
||||
export class Prefs {
|
||||
private getPref<T>(key: string, defaultValue: T): T {
|
||||
const fullKey = `${PREF_PREFIX}.${key}`;
|
||||
const value = Zotero.Prefs.get(fullKey, true);
|
||||
return (value as T) ?? defaultValue;
|
||||
}
|
||||
|
||||
private setPref<T>(key: string, value: T): void {
|
||||
const fullKey = `${PREF_PREFIX}.${key}`;
|
||||
Zotero.Prefs.set(fullKey, value, true);
|
||||
}
|
||||
|
||||
get notesPath(): string {
|
||||
const path = this.getPref("notesPath", "");
|
||||
if (path && path.startsWith("~")) {
|
||||
try {
|
||||
// Use Gecko's directory service to get the actual home directory
|
||||
const homeDir = Services.dirsvc.get("Home", Ci.nsIFile).path;
|
||||
return path.replace("~", homeDir);
|
||||
} catch (_error) {
|
||||
// Fallback: strip the Zotero profile-specific suffix from profileDir
|
||||
const home = PathUtils.profileDir.replace(/[/\\][^/\\]+[/\\][^/\\]+$/, "");
|
||||
return path.replace("~", home);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
set notesPath(value: string) {
|
||||
this.setPref("notesPath", value);
|
||||
}
|
||||
|
||||
get pandocPath(): string {
|
||||
return this.getPref("pandocPath", "pandoc");
|
||||
}
|
||||
|
||||
set pandocPath(value: string) {
|
||||
this.setPref("pandocPath", value);
|
||||
}
|
||||
|
||||
get attachOrgFile(): boolean {
|
||||
return this.getPref("attachOrgFile", false);
|
||||
}
|
||||
|
||||
set attachOrgFile(value: boolean) {
|
||||
this.setPref("attachOrgFile", value);
|
||||
}
|
||||
|
||||
get autoExportOnSync(): boolean {
|
||||
return this.getPref("autoExportOnSync", true);
|
||||
}
|
||||
|
||||
set autoExportOnSync(value: boolean) {
|
||||
this.setPref("autoExportOnSync", value);
|
||||
}
|
||||
|
||||
get exportOnTabClose(): boolean {
|
||||
return this.getPref("exportOnTabClose", true);
|
||||
}
|
||||
|
||||
set exportOnTabClose(value: boolean) {
|
||||
this.setPref("exportOnTabClose", value);
|
||||
}
|
||||
|
||||
get showNotification(): boolean {
|
||||
return this.getPref("showNotification", true);
|
||||
}
|
||||
|
||||
set showNotification(value: boolean) {
|
||||
this.setPref("showNotification", value);
|
||||
}
|
||||
|
||||
get debug(): boolean {
|
||||
return this.getPref("debug", false);
|
||||
}
|
||||
|
||||
set debug(value: boolean) {
|
||||
this.setPref("debug", value);
|
||||
}
|
||||
|
||||
async isNotesPathValid(): Promise<boolean> {
|
||||
const path = this.notesPath;
|
||||
if (!path) return false;
|
||||
try {
|
||||
return await IOUtils.exists(path);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async ensureNotesDirectory(): Promise<void> {
|
||||
const path = this.notesPath;
|
||||
if (!path) {
|
||||
throw new Error("Notes path is not configured");
|
||||
}
|
||||
await IOUtils.makeDirectory(path, { ignoreExisting: true });
|
||||
|
||||
const attachmentsPath = PathUtils.join(path, "attachments");
|
||||
await IOUtils.makeDirectory(attachmentsPath, { ignoreExisting: true });
|
||||
}
|
||||
}
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": true,
|
||||
"outDir": ".scaffold/build/addon/content/scripts",
|
||||
"rootDir": "src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"types": ["zotero-types"]
|
||||
},
|
||||
"include": ["src/**/*", "typings/**/*"],
|
||||
"exclude": ["node_modules", ".scaffold"]
|
||||
}
|
||||
109
typings/global.d.ts
vendored
Normal file
109
typings/global.d.ts
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
declare const __env__: "development" | "production";
|
||||
|
||||
declare namespace Zotero {
|
||||
let OrgExportAnnotations: import("../src/addon").OrgExportAnnotationsAddon & {
|
||||
hooks: import("../src/hooks").Hooks;
|
||||
};
|
||||
|
||||
const BetterNotes:
|
||||
| {
|
||||
api: {
|
||||
convert: {
|
||||
note2md(note: Zotero.Item, outputDir: string): Promise<string>;
|
||||
};
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
OrgExportAnnotationsPrefs?: {
|
||||
init(): void;
|
||||
browseNotesPath(): Promise<void>;
|
||||
browsePandocPath(): Promise<void>;
|
||||
testPandoc(): Promise<void>;
|
||||
exportAllNow(): Promise<void>;
|
||||
forceExportAllNow(): Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
interface MenuManagerMenu {
|
||||
menuType: "menuitem" | "separator" | "submenu";
|
||||
l10nID?: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
onCommand?: (event: Event, context: MenuManagerContext) => void;
|
||||
onShowing?: (event: Event, context: MenuManagerContext) => void;
|
||||
menus?: MenuManagerMenu[];
|
||||
}
|
||||
|
||||
interface MenuManagerContext {
|
||||
collectionTreeRow?: unknown;
|
||||
items?: Zotero.Item[];
|
||||
tabType?: string;
|
||||
tabSubType?: string;
|
||||
tabID?: string;
|
||||
menuElem?: Element;
|
||||
setL10nArgs?(l10nArgs: string): void;
|
||||
setEnabled?(enabled: boolean): void;
|
||||
setVisible?(visible: boolean): void;
|
||||
setIcon?(icon: string, darkIcon?: string): void;
|
||||
}
|
||||
|
||||
interface MenuManagerRegistration {
|
||||
menuID: string;
|
||||
pluginID: string;
|
||||
target: string;
|
||||
menus: MenuManagerMenu[];
|
||||
}
|
||||
|
||||
declare namespace Zotero {
|
||||
const MenuManager:
|
||||
| {
|
||||
registerMenu(options: MenuManagerRegistration): void;
|
||||
unregisterMenu(menuID: string): void;
|
||||
updateMenuPopup(
|
||||
menu: Element,
|
||||
target: string,
|
||||
options: { getContext: () => MenuManagerContext }
|
||||
): void;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
declare namespace IOUtils {
|
||||
function remove(path: string): Promise<void>;
|
||||
function exists(path: string): Promise<boolean>;
|
||||
function makeDirectory(path: string, options?: { ignoreExisting?: boolean }): Promise<void>;
|
||||
}
|
||||
|
||||
declare namespace PathUtils {
|
||||
function normalize(path: string): string;
|
||||
function join(...parts: string[]): string;
|
||||
function filename(path: string): string;
|
||||
const profileDir: string;
|
||||
}
|
||||
|
||||
// Gecko/XPCOM globals available in Zotero 7 runtime
|
||||
declare const Services: {
|
||||
dirsvc: {
|
||||
get(prop: string, iface: unknown): { path: string };
|
||||
};
|
||||
io: {
|
||||
newURI(spec: string): unknown;
|
||||
};
|
||||
scriptloader: {
|
||||
loadSubScript(url: string): void;
|
||||
};
|
||||
};
|
||||
|
||||
declare const Ci: {
|
||||
nsIFile: unknown;
|
||||
amIAddonManagerStartup: unknown;
|
||||
};
|
||||
|
||||
declare const Cc: {
|
||||
[contractID: string]: {
|
||||
getService(iface: unknown): unknown;
|
||||
};
|
||||
};
|
||||
36
typings/i10n.d.ts
vendored
Normal file
36
typings/i10n.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
// Generated by zotero-plugin-scaffold
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
export type FluentMessageId =
|
||||
| 'description'
|
||||
| 'error-better-notes-required'
|
||||
| 'error-notes-path-invalid'
|
||||
| 'error-notes-path-not-set'
|
||||
| 'error-pandoc-not-found'
|
||||
| 'menu-export-all'
|
||||
| 'menu-export-selected'
|
||||
| 'menu-preferences'
|
||||
| 'name'
|
||||
| 'notification-config-required'
|
||||
| 'notification-export-complete'
|
||||
| 'notification-export-error'
|
||||
| 'notification-no-items'
|
||||
| 'prefs-attach-org'
|
||||
| 'prefs-attach-org-description'
|
||||
| 'prefs-auto-export-sync'
|
||||
| 'prefs-browse'
|
||||
| 'prefs-debug'
|
||||
| 'prefs-export-now'
|
||||
| 'prefs-export-tab-close'
|
||||
| 'prefs-force-export'
|
||||
| 'prefs-notes-path'
|
||||
| 'prefs-notes-path-description'
|
||||
| 'prefs-pandoc-path'
|
||||
| 'prefs-pandoc-path-description'
|
||||
| 'prefs-section-advanced'
|
||||
| 'prefs-section-behavior'
|
||||
| 'prefs-section-paths'
|
||||
| 'prefs-show-notification'
|
||||
| 'prefs-test-pandoc'
|
||||
| 'prefs-title';
|
||||
19
typings/prefs.d.ts
vendored
Normal file
19
typings/prefs.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
// Generated by zotero-plugin-scaffold
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
// prettier-ignore
|
||||
declare namespace _ZoteroTypes {
|
||||
interface Prefs {
|
||||
PluginPrefsMap: {
|
||||
"notesPath": string;
|
||||
"pandocPath": string;
|
||||
"attachOrgFile": boolean;
|
||||
"autoExportOnSync": boolean;
|
||||
"exportOnTabClose": boolean;
|
||||
"showNotification": boolean;
|
||||
"debug": boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
28
zotero-plugin.config.ts
Normal file
28
zotero-plugin.config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from "zotero-plugin-scaffold";
|
||||
import pkg from "./package.json";
|
||||
|
||||
export default defineConfig({
|
||||
source: ["src", "addon"],
|
||||
dist: ".scaffold/build",
|
||||
name: pkg.config.addonName,
|
||||
id: pkg.config.addonID,
|
||||
namespace: pkg.config.addonRef,
|
||||
build: {
|
||||
esbuildOptions: [
|
||||
{
|
||||
entryPoints: ["src/index.ts"],
|
||||
bundle: true,
|
||||
target: "firefox128",
|
||||
outfile: ".scaffold/build/addon/content/scripts/index.js",
|
||||
},
|
||||
],
|
||||
},
|
||||
server: {
|
||||
asProxy: true,
|
||||
},
|
||||
release: {
|
||||
bumpp: {
|
||||
execute: "npm run build",
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user