Files
zotero-notes-export-org/AGENTS.md
T
Ignacio Ballesteros 8393cca8dd
CI / build (push) Successful in 21s
feat: add Zotero 10 support
Bump strict_max_version to 10.0.* and esbuild target to firefox140
(Zotero 9/10 Firefox 140 ESR base). Update docs accordingly.
2026-09-29 17:34:28 +02:00

287 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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–10
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));
```