diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 45ae241..88cb347 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -79,10 +79,7 @@ export interface CustomSortSpec { outsidersFoldersGroupIdx?: number itemsToHide?: Set priorityOrder?: Array // Indexes of groups in evaluation order - - // For internal transient use - plugin?: Plugin // to hand over the access to App instance to the sorting engine - _mCache?: MetadataCache + implicit?: boolean // spec applied automatically (e.g. auto integration with a plugin) } export const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value' diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index 2890f5f..285096c 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -9,7 +9,7 @@ import { sorterByMetadataField, SorterFn, getSorterFnFor, - Sorters + ProcessingContext } from './custom-sort'; import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; @@ -744,7 +744,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: "metadataField1", exactPrefix: 'Ref' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -760,7 +762,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -781,7 +783,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: "metadataField1", exactPrefix: 'Ref' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -797,7 +801,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -818,7 +822,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: "metadataField1", exactPrefix: 'Ref' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -834,7 +840,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -855,7 +861,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: "metadataField1", exactPrefix: 'Ref' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -871,7 +879,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(folder, sortSpec) + const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -904,7 +912,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -935,7 +943,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -966,7 +974,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1005,7 +1013,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1044,7 +1052,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { starredPluginInstance: starredPluginInstance as Starred_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1086,7 +1094,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1121,7 +1129,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1155,7 +1163,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1190,7 +1198,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(file, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1221,7 +1229,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1263,7 +1271,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1308,7 +1316,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1351,7 +1359,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1397,7 +1405,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1440,7 +1448,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1486,7 +1494,7 @@ describe('determineSortingGroup', () => { // when const result = determineSortingGroup(folder, sortSpec, { iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance - }) + } as ProcessingContext) // then expect(result).toEqual({ @@ -1519,7 +1527,9 @@ describe('determineSortingGroup', () => { byMetadataField: 'metadata-field-for-sorting', exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldAlphabetical - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1535,7 +1545,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1558,7 +1568,9 @@ describe('determineSortingGroup', () => { byMetadataField: 'metadata-field-for-sorting', exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldAlphabeticalReverse - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1574,7 +1586,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1597,7 +1609,9 @@ describe('determineSortingGroup', () => { byMetadataField: 'metadata-field-for-sorting', exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldTrueAlphabetical - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1613,7 +1627,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1636,7 +1650,9 @@ describe('determineSortingGroup', () => { byMetadataField: 'metadata-field-for-sorting', exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1652,7 +1668,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1675,7 +1691,9 @@ describe('determineSortingGroup', () => { exactPrefix: 'Ref', byMetadataField: 'metadata-field-for-sorting', order: CustomSortOrder.byMetadataFieldAlphabeticalReverse - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1691,7 +1709,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(folder, sortSpec) + const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1715,6 +1733,10 @@ describe('determineSortingGroup', () => { exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldAlphabetical }], + defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse, + byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder' + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1726,13 +1748,11 @@ describe('determineSortingGroup', () => { } }[path] } - } as MetadataCache, - defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse, - byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder', + } as MetadataCache } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1754,7 +1774,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.HasMetadataField, order: CustomSortOrder.byMetadataFieldAlphabetical, withMetadataFieldName: 'field-used-with-with-metadata-syntax' - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1770,7 +1792,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -1792,7 +1814,9 @@ describe('determineSortingGroup', () => { type: CustomSortGroupType.ExactPrefix, exactPrefix: 'Ref', order: CustomSortOrder.byMetadataFieldAlphabetical - }], + }] + } + const ctx: Partial = { _mCache: { getCache: function (path: string): CachedMetadata | undefined { return { @@ -1808,7 +1832,7 @@ describe('determineSortingGroup', () => { } // when - const result = determineSortingGroup(file, sortSpec) + const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext) // then expect(result).toEqual({ @@ -2122,7 +2146,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => { const itemB: Partial = { metadataFieldValue: 'B' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2142,7 +2166,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => { metadataFieldValue: 'Aaa', sortString: 'a123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2163,7 +2187,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => { const itemB: Partial = { sortString: 'n123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2181,7 +2205,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => { const itemB: Partial = { sortString: 'ccc ' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2204,7 +2228,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { const itemB: Partial = { metadataFieldValue: 'B' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2224,7 +2248,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { metadataFieldValue: 'Aaa', sortString: 'a123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2236,25 +2260,6 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { expect(result2).toBe(SORT_FIRST_GOES_LATER) expect(result3).toBe(SORT_ITEMS_ARE_EQUAL) }) - it('should put the item with metadata below the second one w/o metadata (this is reverse order)', () => { - // given - const itemA: Partial = { - metadataFieldValue: '15', - sortString: 'n123' - } - const itemB: Partial = { - sortString: 'n123' - } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] - - // when - const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) - const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting) - - // then - expect(result1).toBe(SORT_FIRST_GOES_LATER) - expect(result2).toBe(SORT_FIRST_GOES_EARLIER) - }) it('should put the item with metadata later if the second one has no metadata (reverse order)', () => { // given const itemA: Partial = { @@ -2264,7 +2269,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { const itemB: Partial = { sortString: 'n123' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) @@ -2282,7 +2287,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => { const itemB: Partial = { sortString: 'ccc ' } - const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] + const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse) // when const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 0e4847c..af8f1b4 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -3,6 +3,7 @@ import { CommunityPlugin, FrontMatterCache, InstalledPlugin, + MetadataCache, requireApiVersion, TAbstractFile, TFile, @@ -39,6 +40,15 @@ import { } from "./macros"; import {Obj} from "tern"; +export interface ProcessingContext { + // For internal transient use + plugin?: Plugin // to hand over the access to App instance to the sorting engine + _mCache?: MetadataCache + starredPluginInstance?: Starred_PluginInstance + + iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance +} + let CollatorCompare = new Intl.Collator(undefined, { usage: "sort", sensitivity: "base", @@ -96,7 +106,7 @@ export const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: b } } -export let Sorters: { [key in CustomSortOrder]: SorterFn } = { +let Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString), [CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString), [CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(b.sortString, a.sortString), @@ -236,12 +246,7 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string): return [false, undefined, undefined] } -export interface Context { - starredPluginInstance?: Starred_PluginInstance - iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance -} - -export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: Context): FolderItemForSorting { +export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: ProcessingContext): FolderItemForSorting { let groupIdx: number let determined: boolean = false let matchedGroup: string | null | undefined @@ -324,10 +329,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus break case CustomSortGroupType.HasMetadataField: if (group.withMetadataFieldName) { - if (spec._mCache) { + if (ctx?._mCache) { // For folders - scan metadata of 'folder note' const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md` - const frontMatterCache: FrontMatterCache | undefined = spec._mCache.getCache(notePathToScan)?.frontmatter + const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter const hasMetadata: boolean | undefined = frontMatterCache?.hasOwnProperty(group.withMetadataFieldName) if (hasMetadata) { @@ -417,10 +422,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus metadataFieldName = DEFAULT_METADATA_FIELD_FOR_SORTING } if (metadataFieldName) { - if (spec._mCache) { + if (ctx?._mCache) { // For folders - scan metadata of 'folder note' const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md` - const frontMatterCache: FrontMatterCache | undefined = spec._mCache.getCache(notePathToScan)?.frontmatter + const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter metadataValueToSortBy = frontMatterCache?.[metadataFieldName] } } @@ -496,11 +501,8 @@ export const determineFolderDatesIfNeeded = (folderItems: Array Object.assign({} as CustomSortGroup, group)) @@ -516,10 +518,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] : this.file.children) .map((entry: TFile | TFolder) => { - const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, { - starredPluginInstance: starredPluginInstance, - iconFolderPluginInstance: iconFolderPluginInstance - }) + const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, ctx) return itemForSorting }) @@ -538,8 +537,4 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] } else { this.children = items; } - - // release risky references - sortingSpec._mCache = undefined - sortingSpec.plugin = undefined }; diff --git a/src/custom-sort/folder-matching-rules.spec.ts b/src/custom-sort/folder-matching-rules.spec.ts index 8cb8af9..d52db4c 100644 --- a/src/custom-sort/folder-matching-rules.spec.ts +++ b/src/custom-sort/folder-matching-rules.spec.ts @@ -2,8 +2,10 @@ import {FolderWildcardMatching} from './folder-matching-rules' type SortingSpec = string +const checkIfImplicitSpec = (s: SortingSpec) => false + const createMockMatcherRichVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) let p: string p = '/...'; matcher.addWildcardDefinition(p, `00 ${p}`) p = '/*'; matcher.addWildcardDefinition(p, `0 ${p}`) @@ -19,25 +21,25 @@ const PRIO2 = 2 const PRIO3 = 3 const createMockMatcherSimplestVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*') return matcher } const createMockMatcherRootOnlyVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/...', '/...') return matcher } const createMockMatcherRootOnlyDeepVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/*', '/*') return matcher } const createMockMatcherSimpleVersion = (): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*') matcher.addWildcardDefinition('/Reviews/daily/...', '/Reviews/daily/...') return matcher @@ -108,21 +110,21 @@ describe('folderMatch', () => { expect(match3).toBe('/Reviews/daily/...') }) it('should detect duplicate match children definitions for same path', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('Archive/2020/...', 'First occurrence') const result = matcher.addWildcardDefinition('/Archive/2020/.../', 'Duplicate') expect(result).toEqual({errorMsg: "Duplicate wildcard '...' specification for /Archive/2020/.../"}) }) it('should detect duplicate match all definitions for same path', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addWildcardDefinition('/Archive/2019/*', 'First occurrence') const result = matcher.addWildcardDefinition('Archive/2019/*', 'Duplicate') expect(result).toEqual({errorMsg: "Duplicate wildcard '*' specification for Archive/2019/*"}) }) it('regexp-match by name works (order of regexp doesn\'t matter) case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -134,7 +136,7 @@ describe('folderMatch', () => { expect(match2).toBe('r2') }) it('regexp-match by name works (order of regexp doesn\'t matter) reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -146,7 +148,7 @@ describe('folderMatch', () => { expect(match2).toBe('r2') }) it('regexp-match by path works (order of regexp doesn\'t matter) case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -158,7 +160,7 @@ describe('folderMatch', () => { expect(match2).toBe('r1') }) it('regexp-match by path works (order of regexp doesn\'t matter) reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -170,7 +172,7 @@ describe('folderMatch', () => { expect(match2).toBe('r1') }) it('regexp-match by path and name for root level - order of regexp decides - case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -179,7 +181,7 @@ describe('folderMatch', () => { expect(match).toBe('r2') }) it('regexp-match by path and name for root level - order of regexp decides - reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addWildcardDefinition('/Reviews/*', `w1`) @@ -188,7 +190,7 @@ describe('folderMatch', () => { expect(match).toBe('r1') }) it('regexp-match priorities - order of definitions irrelevant - unique priorities - case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^freq\/daily$/, false, 3, false, `r1p3`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) @@ -198,7 +200,7 @@ describe('folderMatch', () => { expect(match).toBe('r1p3') }) it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) @@ -208,7 +210,7 @@ describe('folderMatch', () => { expect(match).toBe('r1p3') }) it('regexp-match priorities - order of definitions irrelevant - duplicate priorities - case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3a`) matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3b`) matcher.addRegexpDefinition(/^daily$/, true, 2, false, `r2p2a`) @@ -221,7 +223,7 @@ describe('folderMatch', () => { expect(match).toBe('r1p3b') }) it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) @@ -231,14 +233,14 @@ describe('folderMatch', () => { expect(match).toBe('r1p3') }) it('regexp-match - edge case of matching the root folder - match by path', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) matcher.addRegexpDefinition(/^\/$/, false, undefined, false, `r1`) // Path w/o leading / - this is how Obsidian supplies the path const match: SortingSpec | null = matcher.folderMatch('/', '') expect(match).toBe('r1') }) it('regexp-match - edge case of matching the root folder - match by name not possible', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) // Tricky regexp which can return zero length matches matcher.addRegexpDefinition(/.*/, true, undefined, false, `r1`) matcher.addWildcardDefinition('/*', `w1`) @@ -247,7 +249,7 @@ describe('folderMatch', () => { expect(match).toBe('w1') }) it('regexp-match - edge case of no match when only regexp rules present', () => { - const matcher: FolderWildcardMatching = new FolderWildcardMatching() + const matcher: FolderWildcardMatching = new FolderWildcardMatching(checkIfImplicitSpec) // Tricky regexp which can return zero length matches matcher.addRegexpDefinition(/abc/, true, undefined, false, `r1`) // Path w/o leading / - this is how Obsidian supplies the path diff --git a/src/custom-sort/folder-matching-rules.ts b/src/custom-sort/folder-matching-rules.ts index bd18721..55f558b 100644 --- a/src/custom-sort/folder-matching-rules.ts +++ b/src/custom-sort/folder-matching-rules.ts @@ -35,6 +35,8 @@ export interface AddingWildcardFailure { errorMsg: string } +export type CheckIfImplicitSpec = (s: SortingSpec) => boolean + export class FolderWildcardMatching { // mimics the structure of folders, so for example tree.matchAll contains the matchAll flag for the root '/' @@ -44,6 +46,9 @@ export class FolderWildcardMatching { regexps: Array> + constructor(private checkIfImplicitSpec: CheckIfImplicitSpec) { + } + // cache determinedWildcardRules: { [key: string]: DeterminedSortingSpec } = {} @@ -68,13 +73,13 @@ export class FolderWildcardMatching { } }) if (lastComponent === MATCH_CHILDREN_PATH_TOKEN) { - if (leafNode.matchChildren) { + if (leafNode.matchChildren && !this.checkIfImplicitSpec(leafNode.matchChildren)) { return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`} } else { leafNode.matchChildren = rule } } else { // Implicitly: MATCH_ALL_PATH_TOKEN - if (leafNode.matchAll) { + if (leafNode.matchAll && !this.checkIfImplicitSpec(leafNode.matchAll)) { return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`} } else { leafNode.matchAll = rule diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index a2785df..8018e72 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -1677,6 +1677,11 @@ const txtInputErrorPriorityEmptyPattern: string = ` ` const txtInputEmptySpec: string = `` +const txtInputOnlyCommentsSpec: string = ` +// Some comment + +// Another comment below empty line +` describe('SortingSpecProcessor error detection and reporting', () => { let processor: SortingSpecProcessor; @@ -1915,7 +1920,7 @@ describe('SortingSpecProcessor error detection and reporting', () => { `${ERR_PREFIX} 22:PriorityPrefixAfterGroupTypePrefix Priority prefix must be used before sorting group type indicator ${ERR_SUFFIX_IN_LINE(2)}`) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/folders /+ /! Hello')) }) - it('should recognize error: combine prefix after sorting group type prefixe', () => { + it('should recognize error: combine prefix after sorting group type prefix', () => { const inputTxtArr: Array = ` /folders /+ Hello `.replace(/\t/gi, '').split('\n') @@ -1932,6 +1937,12 @@ describe('SortingSpecProcessor error detection and reporting', () => { expect(result).toBeNull() expect(errorsLogger).toHaveBeenCalledTimes(0) }) + it('should recognize empty spec', () => { + const inputTxtArr: Array = txtInputOnlyCommentsSpec.split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result).toBeNull() + expect(errorsLogger).toHaveBeenCalledTimes(0) + }) it.each([ '% \\.d+...', '% ...\\d+', diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index e5854cf..112be63 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -37,6 +37,7 @@ interface ProcessingContext { specs: Array currentSpec?: CustomSortSpec currentSpecGroup?: CustomSortGroup + implicitSpec?: boolean // Support for specific conditions (intentionally not generic approach) previousValidEntryWasTargetFolderAttr?: boolean // Entry in previous non-empty valid line @@ -577,7 +578,7 @@ const ensureCollectionHasSortSpecByName = (collection?: SortSpecsCollection | nu const ensureCollectionHasSortSpecByWildcard = (collection?: SortSpecsCollection | null) => { collection = collection ?? {} if (!collection.sortSpecByWildcard) { - collection.sortSpecByWildcard = new FolderWildcardMatching() + collection.sortSpecByWildcard = new FolderWildcardMatching((spec: CustomSortSpec) => !!spec.implicit) } return collection } @@ -602,35 +603,38 @@ const endsWithWildcardPatternSuffix = (path: string): boolean => { enum WildcardPriority { NO_WILDCARD = 1, + NO_WILDCARD_IMPLICIT, MATCH_CHILDREN, - MATCH_ALL + MATCH_CHILDREN_IMPLICIT, + MATCH_ALL, + MATCH_ALL_IMPLICIT } -const stripWildcardPatternSuffix = (path: string): {path: string, detectedWildcardPriority: number} => { +const stripWildcardPatternSuffix = (path: string, ofImplicitSpec: boolean): {path: string, detectedWildcardPriority: number} => { if (path.endsWith(MATCH_ALL_SUFFIX)) { path = path.slice(0, -MATCH_ALL_SUFFIX.length) return { path: path.length > 0 ? path : '/', - detectedWildcardPriority: WildcardPriority.MATCH_ALL + detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_ALL_IMPLICIT : WildcardPriority.MATCH_ALL } } if (path.endsWith(MATCH_CHILDREN_1_SUFFIX)) { path = path.slice(0, -MATCH_CHILDREN_1_SUFFIX.length) return { path: path.length > 0 ? path : '/', - detectedWildcardPriority: WildcardPriority.MATCH_CHILDREN, + detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_CHILDREN_IMPLICIT : WildcardPriority.MATCH_CHILDREN } } if (path.endsWith(MATCH_CHILDREN_2_SUFFIX)) { path = path.slice(0, -MATCH_CHILDREN_2_SUFFIX.length) return { path: path.length > 0 ? path : '/', - detectedWildcardPriority: WildcardPriority.MATCH_CHILDREN + detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_CHILDREN_IMPLICIT : WildcardPriority.MATCH_CHILDREN } } return { path: path, - detectedWildcardPriority: WildcardPriority.NO_WILDCARD + detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.NO_WILDCARD_IMPLICIT : WildcardPriority.NO_WILDCARD } } @@ -727,12 +731,14 @@ export class SortingSpecProcessor { parseSortSpecFromText(text: Array, folderPath: string, sortingSpecFileName: string, - collection?: SortSpecsCollection | null + collection?: SortSpecsCollection | null, + implicitSpec?: boolean ): SortSpecsCollection | null | undefined { // reset / init processing state after potential previous invocation this.ctx = { folderPath: folderPath, // location of the sorting spec file - specs: [] + specs: [], + implicitSpec: implicitSpec }; this.currentEntryLine = null this.currentEntryLineIdx = null @@ -839,7 +845,7 @@ export class SortingSpecProcessor { for (let idx = 0; idx < spec.targetFoldersPaths.length; idx++) { const originalPath = spec.targetFoldersPaths[idx] if (!originalPath.startsWith(MatchFolderNameLexeme) && !originalPath.startsWith(MatchFolderByRegexpLexeme)) { - const {path, detectedWildcardPriority} = stripWildcardPatternSuffix(originalPath) + const {path, detectedWildcardPriority} = stripWildcardPatternSuffix(originalPath, !!spec.implicit) let storeTheSpec: boolean = true const preexistingSortSpecPriority: WildcardPriority = this.pathMatchPriorityForPath[path] if (preexistingSortSpecPriority) { @@ -1446,7 +1452,8 @@ export class SortingSpecProcessor { private putNewSpecForNewTargetFolder(folderPath?: string): CustomSortSpec { const newSpec: CustomSortSpec = { targetFoldersPaths: [folderPath ?? this.ctx.folderPath], - groups: [] + groups: [], + implicit: this.ctx.implicitSpec } this.ctx.specs.push(newSpec);