Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions web/src/lib/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,20 @@ const getCompiledPattern = (pattern: string): RegExp | null => {
return re;
};

const matchesTagPattern = (tag: string, pattern: string, re: RegExp): boolean => {
if (re.test(tag)) {
return true;
}

// A tag hierarchy exposes its prefixes as tags too. Treat a trailing /.*
// rule as matching the prefix itself by testing the empty suffix.
return pattern.endsWith("/.*") && re.test(`${tag}/`);
};

/**
* Finds the first matching TagMetadata for a given tag name by treating each
* key in tagsSetting.tags as an anchored regex pattern (^pattern$).
* key in tagsSetting.tags as an anchored regex pattern (^pattern$). A trailing
* /.* pattern also matches its hierarchy prefix.
*
* Lookup order:
* 1. Exact key match (O(1) fast path, backward-compatible).
Expand All @@ -43,7 +54,7 @@ export const findTagMetadata = (tag: string, tagsSetting: UserSetting_TagsSettin
// Regex path: treat each key as an anchored pattern.
for (const [pattern, metadata] of Object.entries(tagsSetting.tags)) {
const re = getCompiledPattern(pattern);
if (re?.test(tag)) {
if (re && matchesTagPattern(tag, pattern, re)) {
return metadata;
}
}
Expand Down
19 changes: 19 additions & 0 deletions web/tests/tag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,22 @@ describe("exact tag keys", () => {
expect(findTagMetadata("toString", setting)).toBe(metadata);
});
});

describe("hierarchical tag rules", () => {
it("matches the hierarchy prefix as well as its descendants", () => {
const metadata = { blurContent: true } as UserSetting_TagMetadata;
const setting = { tags: { "tagA/.*": metadata } } as UserSetting_TagsSetting;

expect(findTagMetadata("tagA", setting)).toBe(metadata);
expect(findTagMetadata("tagA/child", setting)).toBe(metadata);
expect(findTagMetadata("tagAB", setting)).toBeUndefined();
});

it("keeps ordinary exact patterns from matching descendants", () => {
const metadata = { blurContent: true } as UserSetting_TagMetadata;
const setting = { tags: { tagA: metadata } } as UserSetting_TagsSetting;

expect(findTagMetadata("tagA", setting)).toBe(metadata);
expect(findTagMetadata("tagA/child", setting)).toBeUndefined();
});
});