From c12cf641b12199fd02d2bc890b54fa9fd8a877b3 Mon Sep 17 00:00:00 2001 From: Lukas Wolfsteiner Date: Mon, 6 Jul 2026 11:31:42 +0200 Subject: [PATCH] feat(blocks): support show and hide field options in raindrop blocks Add comma-separated show and hide keys to fenced raindrop blocks, layered on top of the result display settings. Unknown field names produce a warning above the rendered results. Collection titles are fetched lazily and cached, only when a block or the explorer shows the collection field. Co-authored-by: Cursor --- README.md | 12 ++++++++++++ src/block-parser.ts | 30 ++++++++++++++++++++++++++++++ src/main.ts | 37 ++++++++++++++++++++++++++++++++++++- src/raindrop-view.ts | 4 ++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d11e97f..c13654c 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,18 @@ Options: - `search`: Raindrop.io search term. This is passed through to Raindrop.io. - `sort`: Raindrop.io sort value, for example `-created`. Defaults to **Default sort**. - `limit`: Number of links to request, clamped between 1 and 100. Defaults to **Default limit**. +- `show`: Comma-separated fields to show in addition to the **Result display** settings. +- `hide`: Comma-separated fields to hide, applied after `show`. + +Valid field names for `show` and `hide`: `cover`, `domain`, `created`, `excerpt`, `tags`, `collection`. Unknown names produce a warning above the results. For example: + +````markdown +```raindrop +tag: obsidian +hide: excerpt, tags +show: collection +``` +```` You can combine `tag` and `search`; the plugin joins them into one Raindrop.io search term. diff --git a/src/block-parser.ts b/src/block-parser.ts index e4b1c2b..79d0f6d 100644 --- a/src/block-parser.ts +++ b/src/block-parser.ts @@ -1,9 +1,13 @@ +import { isRaindropFieldName, RAINDROP_FIELD_NAMES, type RaindropDisplayFields } from "./renderer"; + export interface RaindropBlockOptions { collectionId?: number; search?: string; tag?: string; sort?: string; limit?: number; + showFields?: (keyof RaindropDisplayFields)[]; + hideFields?: (keyof RaindropDisplayFields)[]; } export interface ParsedRaindropBlock { @@ -32,6 +36,28 @@ function clampLimit(limit: number, { min, max }: { min: number; max: number }): return Math.max(min, Math.min(max, Math.floor(limit))); } +function parseFieldList( + raw: string | undefined, + key: "show" | "hide", + warnings: string[], +): (keyof RaindropDisplayFields)[] | undefined { + if (raw === undefined) return undefined; + + const fields: (keyof RaindropDisplayFields)[] = []; + for (const entry of raw.split(",")) { + const name = entry.trim().toLowerCase(); + if (!name) continue; + + if (isRaindropFieldName(name)) { + if (!fields.includes(name)) fields.push(name); + } else { + warnings.push(`Unknown field \`${name}\` in \`${key}\`; expected one of ${RAINDROP_FIELD_NAMES.join(", ")}.`); + } + } + + return fields.length > 0 ? fields : undefined; +} + export function parseRaindropBlock(source: string): ParsedRaindropBlock { const warnings: string[] = []; const kv = parseKeyValueLines(source); @@ -51,6 +77,8 @@ export function parseRaindropBlock(source: string): ParsedRaindropBlock { const tag = kv.tag?.trim() || undefined; const search = kv.search?.trim() || undefined; const sort = kv.sort?.trim() || undefined; + const showFields = parseFieldList(kv.show, "show", warnings); + const hideFields = parseFieldList(kv.hide, "hide", warnings); return { options: { @@ -59,6 +87,8 @@ export function parseRaindropBlock(source: string): ParsedRaindropBlock { search, sort, limit, + showFields, + hideFields, }, warnings, }; diff --git a/src/main.ts b/src/main.ts index 0cc58aa..4c11e65 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import { import { RaindropApi } from "./raindrop-api"; import { buildRaindropSearchQuery, formatRaindropTagFilter } from "./raindrop-search"; import { RaindropSideView } from "./raindrop-view"; -import { DEFAULT_DISPLAY_FIELDS, renderRaindropItems, renderRaindropStatus } from "./renderer"; +import { DEFAULT_DISPLAY_FIELDS, renderRaindropItems, renderRaindropStatus, resolveRaindropDisplayFields } from "./renderer"; import { DEFAULT_SETTINGS, isRaindropTagClickBehavior, RaindropSettingTab, RaindropViewSettings } from "./settings"; interface GlobalSearchPluginInstance { @@ -23,6 +23,8 @@ export default class RaindropViewPlugin extends Plugin { activeMarkdownFile: TFile | null = null; private refreshTimeoutId: number | null = null; private readonly refreshDebounceMs = 200; + private collectionTitles: Map | null = null; + private collectionTitlesRequest: Promise | undefined> | null = null; async onload(): Promise { await this.loadSettings(); @@ -94,9 +96,32 @@ export default class RaindropViewPlugin extends Plugin { } async saveSettings(): Promise { + // The access token may have changed, so drop cached collection titles. + this.collectionTitles = null; + this.collectionTitlesRequest = null; await this.saveData(this.settings); } + async getCollectionTitles(api: RaindropApi): Promise | undefined> { + if (this.collectionTitles) return this.collectionTitles; + + this.collectionTitlesRequest ??= api + .listCollections() + .then((titles) => { + this.collectionTitles = titles; + return titles; + }) + .catch(() => { + // Collection titles are optional; results render without them. + return undefined; + }) + .finally(() => { + this.collectionTitlesRequest = null; + }); + + return this.collectionTitlesRequest; + } + async openRaindropView(searchQuery?: string, context?: string): Promise { this.captureActiveMarkdownFile(); @@ -138,6 +163,12 @@ export default class RaindropViewPlugin extends Plugin { return; } + const fields = resolveRaindropDisplayFields( + this.settings.displayFields ?? DEFAULT_DISPLAY_FIELDS, + parsed.options.showFields, + parsed.options.hideFields, + ); + const limit = parsed.options.limit ?? this.settings.defaultLimit; const items = await api.listRaindrops({ collectionId: parsed.options.collectionId ?? this.settings.defaultCollectionId, @@ -149,9 +180,13 @@ export default class RaindropViewPlugin extends Plugin { perpage: limit, }); + const collectionTitles = fields.collection ? await this.getCollectionTitles(api) : undefined; + renderRaindropItems(container, items, { title, warnings: parsed.warnings, + fields, + collectionTitles, onTagClick: (tag) => { void this.handleRaindropTagClick(tag, sourcePath); }, diff --git a/src/raindrop-view.ts b/src/raindrop-view.ts index c684d0e..4ae1f53 100644 --- a/src/raindrop-view.ts +++ b/src/raindrop-view.ts @@ -210,8 +210,12 @@ export class RaindropSideView extends ItemView { }); this.items = reset ? batch : [...this.items, ...batch]; + const fields = this.plugin.settings.displayFields; + const collectionTitles = fields.collection ? await this.plugin.getCollectionTitles(api) : undefined; renderRaindropItems(this.resultsEl, this.items, { title: this.query ? "Filtered Raindrop.io links" : "All Raindrop.io links", + fields, + collectionTitles, onTagClick: (tag) => { void this.plugin.handleRaindropTagClick(tag, this.getSourceFile()?.path); },