Added ability to add multi conditions

This commit is contained in:
Signynt 2025-07-09 10:54:13 +02:00
parent 2e8a1feaa5
commit aa6a81e6cf

320
main.ts
View file

@ -20,6 +20,7 @@ enum RuleType {
Folder = 'folder',
Tag = 'tag',
Property = 'property',
Multi = 'multi',
}
/** Defines the source of the content for a rule (e.g., direct text input or a markdown file). */
@ -37,6 +38,26 @@ enum RenderLocation {
// --- Interfaces ---
/**
* Represents a single condition for a 'Multi' rule type.
*/
interface SubCondition {
/** The type of condition (folder, tag, or property). */
type: 'folder' | 'tag' | 'property';
/** For 'folder' type: path to the folder. */
path?: string;
/** For 'folder' type: whether to match subfolders. */
recursive?: boolean;
/** For 'tag' type: the tag name (without '#'). */
tag?: string;
/** For 'tag' type: whether to match subtags. */
includeSubtags?: boolean;
/** For 'property' type: the name of the frontmatter property. */
propertyName?: string;
/** For 'property' type: the value the frontmatter property should have. */
propertyValue?: string;
}
/**
* Represents a rule for injecting dynamic content into Markdown views.
* Each rule specifies matching criteria (type: folder/tag/property), content source (text/file),
@ -61,6 +82,8 @@ interface Rule {
propertyName?: string;
/** For 'property' type: the value the frontmatter property should have. */
propertyValue?: string;
/** For 'multi' type: an array of sub-conditions. The rule matches if ANY of these conditions are met. */
conditions?: SubCondition[];
/** The source from which to get the content (direct text or a file). */
contentSource: ContentSource;
/** Direct text content if contentSource is 'text'. */
@ -767,7 +790,7 @@ export default class VirtualFooterPlugin extends Plugin {
const fileCache = this.app.metadataCache.getFileCache(file);
// Pre-fetch tags if any tag-based rules exist and are enabled
const hasEnabledTagRule = this.settings.rules.some(r => r.enabled && r.type === RuleType.Tag);
const hasEnabledTagRule = this.settings.rules.some(r => r.enabled && (r.type === RuleType.Tag || r.type === RuleType.Multi));
if (hasEnabledTagRule && fileCache) {
const allTagsInFileWithHash = getAllTags(fileCache);
fileTags = allTagsInFileWithHash ? allTagsInFileWithHash.map(tag => tag.substring(1)) : [];
@ -779,53 +802,40 @@ export default class VirtualFooterPlugin extends Plugin {
}
let isMatch = false;
const ruleRecursive = currentRule.recursive === undefined ? true : currentRule.recursive;
// --- Match by Folder ---
if (currentRule.type === RuleType.Folder && currentRule.path !== undefined) {
if (currentRule.path === "") { // Matches all files
isMatch = true;
} else if (currentRule.path === "/") { // Matches root folder
isMatch = ruleRecursive ? true : (file.parent?.isRoot() ?? false);
} else {
let normalizedRuleFolderPath = currentRule.path.endsWith('/') ? currentRule.path.slice(0, -1) : currentRule.path;
if (ruleRecursive) {
isMatch = file.path.startsWith(normalizedRuleFolderPath + '/');
} else {
isMatch = file.parent?.path === normalizedRuleFolderPath;
}
}
if (currentRule.type === RuleType.Folder) {
isMatch = this._checkFolderMatch(file, currentRule);
}
// --- Match by Tag ---
} else if (currentRule.type === RuleType.Tag && currentRule.tag && fileTags) {
const ruleTag = currentRule.tag;
const includeSubtags = currentRule.includeSubtags ?? false;
for (const fileTag of fileTags) {
if (includeSubtags) {
if (fileTag === ruleTag || fileTag.startsWith(ruleTag + '/')) {
isMatch = true;
break;
}
} else {
if (fileTag === ruleTag) {
isMatch = true;
break;
}
}
}
else if (currentRule.type === RuleType.Tag) {
isMatch = this._checkTagMatch(fileTags, currentRule);
}
// --- Match by Property ---
} else if (currentRule.type === RuleType.Property && currentRule.propertyName && fileCache?.frontmatter) {
const propertyKey = currentRule.propertyName;
const expectedPropertyValue = currentRule.propertyValue;
const actualPropertyValue = fileCache.frontmatter[propertyKey];
if (actualPropertyValue !== undefined && actualPropertyValue !== null) {
if (typeof actualPropertyValue === 'string') {
isMatch = actualPropertyValue === expectedPropertyValue;
} else if (Array.isArray(actualPropertyValue)) {
// For arrays, check if the expected value is one of the items
isMatch = actualPropertyValue.map(String).includes(expectedPropertyValue!);
} else if (typeof actualPropertyValue === 'number' || typeof actualPropertyValue === 'boolean') {
isMatch = String(actualPropertyValue) === expectedPropertyValue;
else if (currentRule.type === RuleType.Property) {
isMatch = this._checkPropertyMatch(fileCache?.frontmatter, currentRule);
}
// --- Match by Multi ---
else if (currentRule.type === RuleType.Multi) {
if (currentRule.conditions && currentRule.conditions.length > 0) {
// Rule matches if ANY of the sub-conditions match (OR logic)
for (const condition of currentRule.conditions) {
if (condition.type === 'folder') {
if (this._checkFolderMatch(file, condition)) {
isMatch = true;
break;
}
} else if (condition.type === 'tag') {
if (this._checkTagMatch(fileTags, condition)) {
isMatch = true;
break;
}
} else if (condition.type === 'property') {
if (this._checkPropertyMatch(fileCache?.frontmatter, condition)) {
isMatch = true;
break;
}
}
}
}
}
@ -838,6 +848,61 @@ export default class VirtualFooterPlugin extends Plugin {
return allApplicable;
}
private _checkFolderMatch(file: TFile, rule: { path?: string, recursive?: boolean }): boolean {
if (rule.path === undefined) return false;
const ruleRecursive = rule.recursive === undefined ? true : rule.recursive;
if (rule.path === "") { // Matches all files
return true;
} else if (rule.path === "/") { // Matches root folder
return ruleRecursive ? true : (file.parent?.isRoot() ?? false);
} else {
let normalizedRuleFolderPath = rule.path.endsWith('/') ? rule.path.slice(0, -1) : rule.path;
if (ruleRecursive) {
return file.path.startsWith(normalizedRuleFolderPath + '/');
} else {
return file.parent?.path === normalizedRuleFolderPath;
}
}
}
private _checkTagMatch(fileTags: string[] | null, rule: { tag?: string, includeSubtags?: boolean }): boolean {
if (!rule.tag || !fileTags) return false;
const ruleTag = rule.tag;
const includeSubtags = rule.includeSubtags ?? false;
for (const fileTag of fileTags) {
if (includeSubtags) {
if (fileTag === ruleTag || fileTag.startsWith(ruleTag + '/')) {
return true;
}
} else {
if (fileTag === ruleTag) {
return true;
}
}
}
return false;
}
private _checkPropertyMatch(frontmatter: any, rule: { propertyName?: string, propertyValue?: string }): boolean {
if (!rule.propertyName || !frontmatter) return false;
const propertyKey = rule.propertyName;
const expectedPropertyValue = rule.propertyValue;
const actualPropertyValue = frontmatter[propertyKey];
if (actualPropertyValue !== undefined && actualPropertyValue !== null) {
if (typeof actualPropertyValue === 'string') {
return actualPropertyValue === expectedPropertyValue;
} else if (Array.isArray(actualPropertyValue)) {
// For arrays, check if the expected value is one of the items
return actualPropertyValue.map(String).includes(expectedPropertyValue!);
} else if (typeof actualPropertyValue === 'number' || typeof actualPropertyValue === 'boolean') {
return String(actualPropertyValue) === expectedPropertyValue;
}
}
return false;
}
/**
* Fetches the content for a given rule, either from direct text or from a specified file.
* @param rule The rule for which to fetch content.
@ -988,6 +1053,8 @@ export default class VirtualFooterPlugin extends Plugin {
} else if (migratedRule.type === RuleType.Property) {
migratedRule.propertyName = loadedRule.propertyName || '';
migratedRule.propertyValue = loadedRule.propertyValue || '';
} else if (migratedRule.type === RuleType.Multi) {
migratedRule.conditions = loadedRule.conditions || [];
}
// Populate content source-specific fields
@ -1008,32 +1075,31 @@ export default class VirtualFooterPlugin extends Plugin {
rule.enabled = typeof rule.enabled === 'boolean' ? rule.enabled : DEFAULT_SETTINGS.rules[0].enabled!;
rule.type = rule.type || DEFAULT_SETTINGS.rules[0].type;
// Clean up all type-specific fields before re-populating
const conditions = rule.conditions; // Preserve conditions for multi-type
delete rule.path;
delete rule.recursive;
delete rule.tag;
delete rule.includeSubtags;
delete rule.propertyName;
delete rule.propertyValue;
delete rule.conditions;
// Normalize based on RuleType
if (rule.type === RuleType.Folder) {
rule.path = rule.path === undefined ? (DEFAULT_SETTINGS.rules[0].path || '') : rule.path;
// 'recursive' is always true if path is "" (all files)
rule.recursive = rule.path === "" ? true : (typeof rule.recursive === 'boolean' ? rule.recursive : true);
// Delete fields not applicable to Folder type
delete rule.tag;
delete rule.includeSubtags;
delete rule.propertyName;
delete rule.propertyValue;
} else if (rule.type === RuleType.Tag) {
rule.tag = rule.tag === undefined ? '' : rule.tag;
rule.includeSubtags = typeof rule.includeSubtags === 'boolean' ? rule.includeSubtags : false;
delete rule.path;
delete rule.recursive;
delete rule.propertyName;
delete rule.propertyValue;
} else if (rule.type === RuleType.Property) {
rule.propertyName = rule.propertyName === undefined ? '' : rule.propertyName;
rule.propertyValue = rule.propertyValue === undefined ? '' : rule.propertyValue;
delete rule.path;
delete rule.recursive;
delete rule.tag;
delete rule.includeSubtags;
} else if (rule.type === RuleType.Multi) {
rule.conditions = Array.isArray(conditions) ? conditions : [];
}
// Normalize content source and related fields
// Normalize content source and related fields
rule.contentSource = rule.contentSource || DEFAULT_SETTINGS.rules[0].contentSource;
rule.footerText = rule.footerText || ''; // Retain name for compatibility
@ -1363,14 +1429,22 @@ class VirtualFooterSettingTab extends PluginSettingTab {
// --- Rule Type Setting ---
new Setting(ruleContentContainer)
.setName('Rule type')
.setDesc('Apply this rule based on folder, tag, or property.')
.setDesc('Apply this rule based on folder, tag, property, or a combination.')
.addDropdown(dropdown => dropdown
.addOption(RuleType.Folder, 'Folder')
.addOption(RuleType.Tag, 'Tag')
.addOption(RuleType.Property, 'Property')
.addOption(RuleType.Multi, 'Multi-condition')
.setValue(rule.type)
.onChange(async (value: string) => {
rule.type = value as RuleType;
// When switching to Multi, we might want to convert the old rule
if (rule.type === RuleType.Multi) {
const oldRule = { ...rule };
rule.conditions = [];
// This logic is complex, so for now we just start with a clean slate.
// A more advanced version could auto-convert the previous simple rule.
}
this.plugin.normalizeRule(rule); // Re-normalize for type-specific fields
await this.plugin.saveSettings();
this.display(); // Re-render to show/hide type-specific settings
@ -1472,6 +1546,8 @@ class VirtualFooterSettingTab extends PluginSettingTab {
rule.propertyValue = value;
await this.plugin.saveSettings();
}));
} else if (rule.type === RuleType.Multi) {
this.renderMultiConditionControls(rule, ruleContentContainer);
}
// --- Content Source Settings ---
@ -1623,4 +1699,130 @@ class VirtualFooterSettingTab extends PluginSettingTab {
this.display(); // Re-render to reflect deletion and update indices
}));
}
private renderMultiConditionControls(rule: Rule, containerEl: HTMLElement): void {
new Setting(containerEl)
.setName('Conditions')
.setDesc('This rule will apply if ANY of the following conditions are met (OR logic).');
const conditionsContainer = containerEl.createDiv('virtual-footer-conditions-container');
rule.conditions?.forEach((condition, index) => {
this.renderSubConditionControls(condition, index, rule, conditionsContainer);
});
new Setting(containerEl)
.addButton(button => button
.setButtonText('Add condition')
.setCta()
.onClick(async () => {
rule.conditions = rule.conditions || [];
rule.conditions.push({ type: 'folder', path: '', recursive: true });
await this.plugin.saveSettings();
this.display();
}));
}
private renderSubConditionControls(condition: SubCondition, index: number, rule: Rule, containerEl: HTMLElement): void {
const conditionDiv = containerEl.createDiv('virtual-footer-sub-condition-item');
const setting = new Setting(conditionDiv)
.addDropdown(dropdown => dropdown
.addOption('folder', 'Folder')
.addOption('tag', 'Tag')
.addOption('property', 'Property')
.setValue(condition.type)
.onChange(async (value: 'folder' | 'tag' | 'property') => {
condition.type = value;
// Reset fields when type changes
delete condition.path;
delete condition.recursive;
delete condition.tag;
delete condition.includeSubtags;
delete condition.propertyName;
delete condition.propertyValue;
await this.plugin.saveSettings();
this.display();
})
);
if (condition.type === 'folder') {
setting.addText(text => {
text.setPlaceholder('Folder path')
.setValue(condition.path || '')
.onChange(async (value) => {
condition.path = value;
await this.plugin.saveSettings();
});
new MultiSuggest(text.inputEl, this.getAvailableFolderPaths(), (selected) => {
condition.path = selected;
text.setValue(selected);
this.plugin.saveSettings();
}, this.plugin.app);
});
setting.addToggle(toggle => toggle
.setTooltip('Include subfolders')
.setValue(condition.recursive ?? true)
.onChange(async (value) => {
condition.recursive = value;
await this.plugin.saveSettings();
})
);
} else if (condition.type === 'tag') {
setting.addText(text => {
text.setPlaceholder('Tag value (no #)')
.setValue(condition.tag || '')
.onChange(async (value) => {
condition.tag = value.startsWith('#') ? value.substring(1) : value;
await this.plugin.saveSettings();
});
new MultiSuggest(text.inputEl, this.getAvailableTags(), (selected) => {
const normalized = selected.startsWith('#') ? selected.substring(1) : selected;
condition.tag = normalized;
text.setValue(normalized);
this.plugin.saveSettings();
}, this.plugin.app);
});
setting.addToggle(toggle => toggle
.setTooltip('Include subtags')
.setValue(condition.includeSubtags ?? false)
.onChange(async (value) => {
condition.includeSubtags = value;
await this.plugin.saveSettings();
})
);
} else if (condition.type === 'property') {
setting.addText(text => {
text.setPlaceholder('Property name')
.setValue(condition.propertyName || '')
.onChange(async (value) => {
condition.propertyName = value;
await this.plugin.saveSettings();
});
new MultiSuggest(text.inputEl, this.getAvailablePropertyNames(), (selected) => {
condition.propertyName = selected;
text.setValue(selected);
this.plugin.saveSettings();
}, this.plugin.app);
});
setting.addText(text => text
.setPlaceholder('Property value')
.setValue(condition.propertyValue || '')
.onChange(async (value) => {
condition.propertyValue = value;
await this.plugin.saveSettings();
})
);
}
setting.addButton(button => button
.setIcon('trash')
.setTooltip('Delete condition')
.setWarning()
.onClick(async () => {
rule.conditions?.splice(index, 1);
await this.plugin.saveSettings();
this.display();
})
);
}
}