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 <cursoragent@cursor.com>
This commit is contained in:
Lukas Wolfsteiner 2026-07-06 11:31:42 +02:00
parent fc920c58a2
commit c12cf641b1
4 changed files with 82 additions and 1 deletions

View file

@ -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.

View file

@ -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,
};

View file

@ -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<number, string> | null = null;
private collectionTitlesRequest: Promise<Map<number, string> | undefined> | null = null;
async onload(): Promise<void> {
await this.loadSettings();
@ -94,9 +96,32 @@ export default class RaindropViewPlugin extends Plugin {
}
async saveSettings(): Promise<void> {
// 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<Map<number, string> | 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<void> {
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);
},

View file

@ -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);
},