diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 45e09ee..688cd4a 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -35,7 +35,11 @@ export enum CustomSortOrder { standardObsidian, // whatever user selected in the UI byBookmarkOrder, byBookmarkOrderReverse, - default = alphabetical + fileFirst, + folderFirst, + alphabeticalWithFilesPreferred, // When the (base)names are equal, the file has precedence over a folder + alphabeticalWithFoldersPreferred, // When the (base)names are equal, the file has precedence over a folder + default = alphabeticalWithFilesPreferred } export interface RecognizedOrderValue { diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index d0b5154..5b4cb5c 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -22,6 +22,9 @@ import { sorterByMetadataField, SorterFn } from './custom-sort'; +import { + _unitTests +} from './custom-sort' import { CustomSortGroupType, CustomSortOrder, @@ -2959,3 +2962,38 @@ describe('sorterByFolderCDate', () => { expect(normalizedResultR2).toBe(orderReverseRevParams) }) }) + +describe('fileGoesFirstWhenSameBasenameAsFolder', () => { + const file = 'file' + const folder = 'folder' + it.each([ + // main scenario - file goes first unconditionally before folder with the same name + [0, file, folder, -1, 1], + [0, folder, file, 1, -1], + // Not possible - two folders with the same name - the test only documents the behavior for clarity + [0, folder, folder, 0, 0], + // Realistic yet useless - two files with the same basename, + [0, file, file, 0, 0], + // Obvious cases - text compare returned !== 0, simply pass through + [1, file, file, 1, 1], + [1, file, folder, 1, 1], + [1, folder, file, 1, 1], + [1, folder, folder, 1, 1], + [-1, file, file, -1, -1], + [-1, file, folder, -1, -1], + [-1, folder, file, -1, -1], + [-1, folder, folder, -1, -1], + ])('text compare %s of %s %s gives %s (files first) and %s (folders first)', + (textCompare: number, aIsFolder: string, bIsFolder: string, filePreferredOder: number, folderPreferredOrder: number) => { + // given + const a: Partial = { isFolder: aIsFolder === folder } + const b: Partial = { isFolder: bIsFolder === folder } + + const resultFilePreferred: number = _unitTests.fileGoesFirstWhenSameBasenameAsFolder(textCompare, a as FolderItemForSorting, b as FolderItemForSorting) + const resultFolderPreferred: number = _unitTests.folderGoesFirstWhenSameBasenameAsFolder(textCompare, a as FolderItemForSorting, b as FolderItemForSorting) + + // then + expect(resultFilePreferred).toBe(filePreferredOder) + expect(resultFolderPreferred).toBe(folderPreferredOrder) + }) +}) diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 08de1b0..8432da7 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -169,8 +169,18 @@ export const sorterByFolderMDate:(reverseOrder?: boolean) => SorterFn = (reverse } } +type FIFS = FolderItemForSorting + +const fileGoesFirstWhenSameBasenameAsFolder = (stringCompareResult: number, a: FIFS, b: FIFS) => + (!!stringCompareResult) ? stringCompareResult : (a.isFolder === b.isFolder ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? 1 : -1) ); + +const folderGoesFirstWhenSameBasenameAsFolder = (stringCompareResult: number, a: FIFS, b: FIFS) => + (!!stringCompareResult) ? stringCompareResult : (a.isFolder === b.isFolder ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? -1 : 1) ); + const Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString), + [CustomSortOrder.alphabeticalWithFilesPreferred]: (a: FIFS, b: FIFS) => fileGoesFirstWhenSameBasenameAsFolder(CollatorCompare(a.sortString, b.sortString),a,b), + [CustomSortOrder.alphabeticalWithFoldersPreferred]: (a: FIFS, b: FIFS) => fileGoesFirstWhenSameBasenameAsFolder(CollatorCompare(a.sortString, b.sortString),a,b), [CustomSortOrder.alphabeticalWithFileExt]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortStringWithExt, b.sortStringWithExt), [CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString), [CustomSortOrder.trueAlphabeticalWithFileExt]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortStringWithExt, b.sortStringWithExt), @@ -192,9 +202,11 @@ const Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary), [CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder), [CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder), + [CustomSortOrder.fileFirst]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? 1 : -1), + [CustomSortOrder.folderFirst]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? -1 : 0), - // This is a fallback entry which should not be used - the getSorterFor() function below should protect against it - [CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString), + // A fallback entry which should not be used - the getSorterFor() function below should protect against it + [CustomSortOrder.standardObsidian]: (a: FIFS, b: FIFS) => CollatorCompare(a.sortString, b.sortString), }; // Some sorters are different when used in primary vs. secondary sorting order @@ -729,3 +741,8 @@ export const sortFolderItemsForBookmarking = function (folder: TFolder, items: A return folderItems } }; + +export const _unitTests = { + fileGoesFirstWhenSameBasenameAsFolder: fileGoesFirstWhenSameBasenameAsFolder, + folderGoesFirstWhenSameBasenameAsFolder: folderGoesFirstWhenSameBasenameAsFolder +} diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index fde99c8..c0c793f 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -14,16 +14,8 @@ import { RomanNumberNormalizerFn, SortingSpecProcessor } from "./sorting-spec-processor" -import { - CustomSortGroupType, - CustomSortOrder, - CustomSortSpec, - IdentityNormalizerFn -} from "./custom-sort-types"; -import { - FolderMatchingRegexp, - FolderMatchingTreeNode -} from "./folder-matching-rules"; +import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types"; +import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules"; const txtInputExampleA: string = ` order-asc: a-z @@ -527,6 +519,49 @@ describe('SortingSpecProcessor', () => { }) }) +const txtInputFilesOrFoldersPreferred: string = ` +target-folder: AAA +< a-z, files-first +subitems1... + > folders-first, true a-z. +subitems2... + < created, folders-first +` + +const expectedSortSpecForFilesOrFoldersPreferred: { [key: string]: CustomSortSpec } = { + "AAA": { + defaultOrder: CustomSortOrder.alphabetical, + defaultSecondaryOrder: CustomSortOrder.fileFirst, + groups: [{ + exactPrefix: 'subitems1', + order: CustomSortOrder.folderFirst, + secondaryOrder: CustomSortOrder.trueAlphabeticalWithFileExt, + type: CustomSortGroupType.ExactPrefix + },{ + exactPrefix: 'subitems2', + order: CustomSortOrder.byCreatedTime, + secondaryOrder: CustomSortOrder.folderFirst, + type: CustomSortGroupType.ExactPrefix + }, { + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 2, + targetFoldersPaths: ['AAA'] + } +} + +describe('SortingSpecProcessor', () => { + let processor: SortingSpecProcessor; + beforeEach(() => { + processor = new SortingSpecProcessor(); + }); + it('should recognize the files / folders preferred as a primary and secondary orders', () => { + const inputTxtArr: Array = txtInputFilesOrFoldersPreferred.split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result?.sortSpecByPath).toEqual(expectedSortSpecForFilesOrFoldersPreferred) + }) +}) + const txtInputTrueAlphabeticalSortAttr: string = ` target-folder: True Alpha < true a-z diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 2109e28..19f9846 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -123,6 +123,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = { 'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian}, 'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian}, 'by-bookmarks-order': {asc: CustomSortOrder.byBookmarkOrder, desc: CustomSortOrder.byBookmarkOrderReverse}, + 'files-first': {asc: CustomSortOrder.fileFirst, desc: CustomSortOrder.fileFirst}, + 'folders-first': {asc: CustomSortOrder.folderFirst, desc: CustomSortOrder.folderFirst} } const OrderByMetadataLexeme: string = 'by-metadata:'