diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36dabec..c55a459 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ name: Release plugin version -on: +on: workflow_dispatch: push: branches: [master, main, beta] @@ -31,10 +31,34 @@ jobs: run: | npm audit --audit-level=high --production npm audit signatures - continue-on-error: true # Don't fail the build, but report issues + continue-on-error: true # Don't fail the build, but report issues - name: Build plugin run: npm run build - name: Release update env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release + - name: Configure Git + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + run: | + git config --global user.name "${{ github.actor }}" + git config --global user.email "${{ github.actor }}`@users.noreply.github.com`" + - name: Commit manifest.json (main only) + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + run: | + if git diff --quiet -- manifest.json; then + echo "manifest.json unchanged; skipping commit" + exit 0 + fi + + git add manifest.json + git commit -m "chore: 🔖 sync manifest version [skip ci]" + git push + - name: Generate artifact attestation (main release) + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + uses: actions/attest@v4 + with: + subject-path: | + main.js + manifest.json + src/ui/styles/styles.css diff --git a/.releaserc.yaml b/.releaserc.yaml index e91d1cc..65afa06 100644 --- a/.releaserc.yaml +++ b/.releaserc.yaml @@ -17,17 +17,4 @@ plugins: - path: main.js - path: manifest.json - path: src/ui/styles/styles.css - # Uncomment the following block if semantic release *should* commit the - # updated `manifest.json`, `versions.json` and `package.json` files with the new version number. - - - "@semantic-release/git" - - assets: - - manifest.json - - package.json - - package-lock.json - message: >- - chore: 🔖 set `package.json`, `manifest.json` and - `package-lock.json` to ${nextRelease.version} [skip ci] - - - ${nextRelease.notes} tagFormat: "${version}" diff --git a/.vscode/settings.json b/.vscode/settings.json index 10c6e04..796b5a7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,26 +1,3 @@ { - "chat.tools.terminal.autoApprove": { - "/^obsidian vault=\"Obsidian Sandbox\" plugin:reload id=readwise-mirror$/": { - "approve": true, - "matchCommandLine": true - }, - "/^obsidian vault=\"Obsidian Sandbox\" plugin id=readwise-mirror$/": { - "approve": true, - "matchCommandLine": true - }, - "/^obsidian vault=\"Obsidian Sandbox\" commands filter=readwise$/": { - "approve": true, - "matchCommandLine": true - }, - "/^obsidian vault=\"Obsidian Sandbox\" command id=readwise-mirror:reset-last-updated$/": { - "approve": true, - "matchCommandLine": true - }, - "/^obsidian vault=\"Obsidian Sandbox\" command id=readwise-mirror:update$/": { - "approve": true, - "matchCommandLine": true - } - }, "jest.configPath": "jest.config.js" } - diff --git a/manifest.json b/manifest.json index 040e594..b9866ed 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "readwise-mirror", "name": "Readwise Mirror", - "version": "2.4.1", + "version": "2.4.2-beta.4", "minAppVersion": "1.6.6", "description": "Mirror your Readwise library directly to your vault.", "author": "jsonmartin, johannrichard", diff --git a/package-lock.json b/package-lock.json index 55bf11e..a9d0049 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "readwise-mirror", - "version": "2.4.1", + "version": "2.4.2-beta.4", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 8dc5548..f63fa96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "readwise-mirror", - "version": "2.4.1", + "version": "2.4.2-beta.4", "description": "This is a plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/src/constants.ts b/src/constants.ts index 5085869..bfea2c4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -61,7 +61,8 @@ Summary: {{ summary }} # Highlights `, - highlightTemplate: `{{ text }}{%- if category == 'books' %} ([{{ location }}]({{ location_url }})){%- endif %}{%- if color %} %% Color: {{ color }} %%{%- endif %} ^{{id}}{%- if note %} + highlightTemplate: `{% atomize id=id, basename=id, embed=true %} +{{ text }}{%- if category == 'books' %} ([{{ location }}]({{ location_url }})){%- endif %}{%- if color %} %% Color: {{ color }} %%{%- endif %} ^{{id}}{%- if note %} Note: {{ note }} {%- endif %}{%- if tags %} @@ -73,6 +74,7 @@ Tags: {{ tags }} {%- endif %} --- +{% endatomize %} `, useSlugify: false, slugifySeparator: '-', @@ -96,7 +98,6 @@ Tags: {{ tags }} }; export const FRONTMATTER_TO_ESCAPE = ['title', 'sanitized_title', 'author', 'authorStr']; -export const EMPTY_FRONTMATTER: string = '---\n---\n'; // Core Template export const NUNJUCKS_CORE_TEMPLATE = `{%- block header -%} diff --git a/src/main.ts b/src/main.ts index eb0f3c1..802ed8d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -78,7 +78,10 @@ export default class ReadwiseMirror extends Plugin { // exposed methods notice: (message: string, duration?: number) => this.notify.notice(message, duration), setStatusBarText: (message: string) => this.notify.setStatusBarText(message), - saveAndApplySettings: () => this.saveAndApplySettings(), + saveAndApplySettings: () => { + this.settings = ctx.settings; + return this.saveAndApplySettings(); + }, }; return ctx; } @@ -103,8 +106,8 @@ export default class ReadwiseMirror extends Plugin { private async initializeUI() { try { - this.addSettingTab(new ReadwiseMirrorSettingTab(this, this.ctx, this.env)); await this.loadAndApplySettings(); + this.addSettingTab(new ReadwiseMirrorSettingTab(this, this.ctx, this.env)); this.logger.debug('Readwise Mirror plugin loaded.'); // Instantiate controller and attach to context diff --git a/src/services/atomizer.ts b/src/services/atomizer.ts index aeee3a6..b6c7c5d 100644 --- a/src/services/atomizer.ts +++ b/src/services/atomizer.ts @@ -149,7 +149,7 @@ export class AtomizeExtension implements nunjucks.Extension { switch (this.pass) { case 'FIRST': { - return new nunjucks.runtime.SafeString(`%%! atomize id=${_id}, basename="${basename.replace(/^\n+|\n+$/g, '').trim()}", embed=${embed} !%% + return new nunjucks.runtime.SafeString(`%%! atomize id=${_id}, basename="${String(basename ?? _id).replace(/^\n+|\n+$/g, '').trim()}", embed=${embed} !%% %%! frontmatter !%% ${frontmatter} %%! endfrontmatter !%% @@ -163,7 +163,8 @@ ${content} } // Sanitize filename - const _basename = filenamify(basename.replace(/^\n+|\n+$/g, '').trim() ?? _id.toString(), { + const rawBasename = (basename ?? _id.toString()); + const _basename = filenamify(String(rawBasename).replace(/^\n+|\n+$/g, '').trim() || _id.toString(), { replacement: '-', maxLength: 252, }) diff --git a/src/services/controller.ts b/src/services/controller.ts index 1f014f5..2e04397 100644 --- a/src/services/controller.ts +++ b/src/services/controller.ts @@ -4,6 +4,7 @@ import type { Library } from 'types/library'; import type { TTrackedFile } from 'types/readwise-note'; import { getTrackingUrl, isFileInFolder, normalizeFilename } from 'utils/file-utils'; import { humanReadableFormat } from 'utils/format-utils'; +import { hasAtomizeBlocks } from 'utils/template-utils'; import { isInReadwiseLibrary, isTrackedReadwiseNote } from 'utils/tracking-utils'; import type ReadwiseMirror from '../main'; import type { PluginContext } from '../types/plugin-context'; @@ -58,6 +59,21 @@ export class Controller { } } + private prepareAtomicHighlightsCategory(library: Library): void { + if (!this.ctx.settings.atomicHighlights) { + return; + } + + library.categories.add('Highlight'); + + if (!hasAtomizeBlocks(this.ctx.settings.highlightTemplate)) { + this.ctx.notice( + 'Readwise: Atomic highlights enabled but your highlight template has no atomize blocks. No atomic notes will be created.', + 10000 + ); + } + } + public async sync() { // Equivalent to plugin.sync() if (this.ctx.syncLock?.isAcquired('library-sync')) { @@ -87,6 +103,7 @@ export class Controller { library = await this.api.downloadUpdates(this.ctx.settings.lastUpdated); } // ...existing filtering and writing logic... + this.prepareAtomicHighlightsCategory(library); await this.plugin.writeLibraryToMarkdown(library); if (this.ctx.settings.logFile) await this.plugin.writeLogToMarkdown(library); this.ctx.settings.lastUpdated = new Date().toISOString(); @@ -149,9 +166,7 @@ export class Controller { this.ctx.logger.debug(`Readwise: downloading current book with ID ${trackedFile.readwiseId}...`); const library = await this.api.downloadSingleBook(trackedFile.readwiseId); if (Object.keys(library.books).length > 0) { - if (this.ctx.settings.atomicHighlights) { - library.categories.add('Highlight'); - } + this.prepareAtomicHighlightsCategory(library); await this.plugin.writeLibraryToMarkdown(library); if (this.ctx.settings.logFile) await this.plugin.writeLogToMarkdown(library); @@ -379,9 +394,7 @@ export class Controller { try { const library = await this.api.downloadMultipleBooks(bookIds); if (Object.keys(library.books).length > 0) { - if (this.ctx.settings.atomicHighlights) { - library.categories.add('Highlight'); - } + this.prepareAtomicHighlightsCategory(library); if (this.ctx.settings.syncNotifications) this.ctx.notice(`Readwise: writing ${Object.keys(library.books).length} updated books to markdown...`); diff --git a/src/services/frontmatter-manager.ts b/src/services/frontmatter-manager.ts index e27427b..f06c065 100644 --- a/src/services/frontmatter-manager.ts +++ b/src/services/frontmatter-manager.ts @@ -1,7 +1,7 @@ import { type FileManager, parseYaml, type TFile } from 'obsidian'; import { Frontmatter, type FrontmatterData, FrontmatterError } from 'services/frontmatter'; import { renderFrontmatterTemplate } from 'services/template-rendering'; -import { EMPTY_FRONTMATTER, READWISE_URI_FIELD } from 'src/constants'; +import { READWISE_URI_FIELD } from 'src/constants'; import type { AtomicFile, BaseFile, ReadwiseDocument } from 'types/document'; import type { PluginContext } from 'types/plugin-context'; import type { ReadwiseEnvironment } from './readwise-environment'; @@ -100,18 +100,21 @@ export class FrontmatterManager { * @returns The frontmatter record */ public getBaseFrontmatter(metadata: ReadwiseDocument): Frontmatter { - // Render a template if frontmatter is managed or file tracking is set - if (!this.settings.frontMatter && !this.settings.trackFiles) { + // Frontmatter parsing is only needed when frontmatter output is enabled. + // Tracking fields are injected later in getFrontmatter. + if (!this.settings.frontMatter) { return new Frontmatter(); } try { // Get frontmatter template string - // Add Sync properties - const frontmatterTemplate = this.settings.frontMatter ? this.settings.frontMatterTemplate : EMPTY_FRONTMATTER; + const frontmatterTemplate = this.settings.frontMatterTemplate; this.logger.debug(`Processing merged frontmatter template\n${frontmatterTemplate}`); // Render and parse the template into YAML const renderedTemplate = renderFrontmatterTemplate(frontmatterTemplate, this.env, metadata); + if (!renderedTemplate.trim()) { + return new Frontmatter(); + } const yaml: unknown = parseYaml(renderedTemplate); if (typeof yaml !== 'object' || yaml === null) { diff --git a/src/services/readwise-api.ts b/src/services/readwise-api.ts index 526c9ae..8be0499 100644 --- a/src/services/readwise-api.ts +++ b/src/services/readwise-api.ts @@ -121,7 +121,7 @@ export default class ReadwiseApi { ): Promise { const url = `${API_ENDPOINT}/${contentType}?`; let data: Record | undefined; - let nextPageCursor: string | undefined; + let nextPageCursor: number | undefined; let rateLimitRetries = 0; const results: Export[] = []; @@ -138,8 +138,8 @@ export default class ReadwiseApi { if (bookId && bookId.length > 0) { queryParams.append('ids', bookId.join(',')); } - if (nextPageCursor) { - queryParams.append('pageCursor', nextPageCursor); + if (nextPageCursor !== undefined) { + queryParams.append('pageCursor', nextPageCursor.toString()); } if (contentType === 'export' && includeDeleted) { queryParams.append('includeDeleted', 'true'); @@ -196,8 +196,8 @@ export default class ReadwiseApi { this.ctx.logger.warn('No results found in the response data.'); } const rawNextPageCursor = pageData.nextPageCursor; - nextPageCursor = typeof rawNextPageCursor === 'string' ? rawNextPageCursor : ''; - if (!nextPageCursor) { + nextPageCursor = typeof rawNextPageCursor === 'number' ? rawNextPageCursor : undefined; + if (nextPageCursor === undefined) { break; } this.ctx.logger.debug(`There are more records left, proceeding to next page: ${nextPageCursor}`); diff --git a/src/services/readwise-document-mapper.ts b/src/services/readwise-document-mapper.ts index 1c9123a..cf48b27 100644 --- a/src/services/readwise-document-mapper.ts +++ b/src/services/readwise-document-mapper.ts @@ -64,8 +64,8 @@ export function buildReadwiseDocument( ? `[[${author}]]` : ''; - const created = createdDate(filteredHighlights); - const updated = updatedDate(filteredHighlights); + const created = createdDate(highlights); + const updated = updatedDate(highlights); const lastHighlightAt = lastHighlightedDate(filteredHighlights); return { diff --git a/src/ui/settings-tab.ts b/src/ui/settings-tab.ts index 1547410..2c1b47a 100644 --- a/src/ui/settings-tab.ts +++ b/src/ui/settings-tab.ts @@ -16,6 +16,7 @@ import type { PluginContext } from 'types/plugin-context'; import type { TemplateValidationResult } from 'types/utilities'; import { WarningDialog } from 'ui/dialog'; import { sanitizeFrontmatterTemplate, validateFrontmatterTemplate } from 'utils/frontmatter-utils'; +import { hasAtomizeBlocks } from 'utils/template-utils'; interface SettingsTab { id: string; @@ -556,8 +557,8 @@ export default class ReadwiseMirrorSettingTab extends PluginSettingTab { .setPlaceholder('Readwise') .setValue(this.ctx.settings.baseFolderName) .onChange(async (value) => { - if (!value) return; - this.ctx.settings.baseFolderName = value; + const normalized = value.trim(); + this.ctx.settings.baseFolderName = normalized || DEFAULT_SETTINGS.baseFolderName; await this.ctx.saveAndApplySettings(); }) ); @@ -641,6 +642,26 @@ export default class ReadwiseMirrorSettingTab extends PluginSettingTab { attr: { style: 'color: var(--text-error);' }, }); } + + if (this.ctx.settings.atomicHighlights && !hasAtomizeBlocks(this.ctx.settings.highlightTemplate)) { + fragment.createEl('br'); + fragment.createEl('br'); + const warningSpan = fragment.createSpan({ + attr: { style: 'color: var(--text-warning);' }, + }); + warningSpan.appendText( + 'Your highlight template does not contain atomize blocks. Atomic highlights will not be created until you add ' + ); + warningSpan.createEl('code', { text: '{% atomize %}...{% endatomize %}' }); + warningSpan.appendText(' blocks. See the '); + warningSpan + .createEl('a', { + text: 'Wiki', + href: 'https://github.com/jsonMartin/readwise-mirror/wiki/Guide:-Atomic-highlights', + }) + .setAttr('target', '_blank'); + warningSpan.appendText(' for details.'); + } }) ) .addToggle((toggle) => { @@ -676,6 +697,7 @@ export default class ReadwiseMirrorSettingTab extends PluginSettingTab { void (async () => { this.ctx.settings.atomicHighlights = true; await this.ctx.saveAndApplySettings(); + this.display(); })(); } else { toggle.setValue(false); @@ -685,6 +707,7 @@ export default class ReadwiseMirrorSettingTab extends PluginSettingTab { } else { this.ctx.settings.atomicHighlights = false; await this.ctx.saveAndApplySettings(); + this.display(); } }); } diff --git a/src/utils/plugin-commands.ts b/src/utils/plugin-commands.ts index fc3a222..7aa0bf7 100644 --- a/src/utils/plugin-commands.ts +++ b/src/utils/plugin-commands.ts @@ -2,6 +2,7 @@ import type { Command } from 'obsidian'; import spacetime from 'spacetime'; import { Controller } from '../services/controller'; import type { PluginContext } from '../types/plugin-context'; +import { humanReadableFormat } from './format-utils'; function toErrorMessage(err: unknown): string { if (err instanceof Error) return err.message; @@ -114,6 +115,7 @@ export function getPluginCommands(ctr: Controller, ctx: PluginContext): Command[ void ctx .saveAndApplySettings() .catch((err: unknown) => ctx.notice(`Failed to save settings: ${toErrorMessage(err)}`)); + ctx.setStatusBarText(`Readwise: Synced ${humanReadableFormat(ctx.settings.lastUpdated)}`); } return true; } diff --git a/src/utils/template-utils.ts b/src/utils/template-utils.ts new file mode 100644 index 0000000..78cde01 --- /dev/null +++ b/src/utils/template-utils.ts @@ -0,0 +1,9 @@ +/** + * Checks whether a highlight template contains atomize blocks. + * Detects both syntaxes: + * - Standard Nunjucks: {% atomize or {%- atomize (FIRST pass in ReadwiseEnvironment) + * - Custom atomizer: %%! atomize (SECOND pass delimiters) + */ +export function hasAtomizeBlocks(template: string): boolean { + return /{%-?\s*atomize/.test(template) || /%%!\s*atomize/.test(template); +}