#89 - Support for multi-level sorting

- full runtime handling (application) of multi-level sorting
- full unit tests coverage of new functions
- metadata-based sorting extended to be applicable at each of sorting level, possibly with different metadata + full unit tests coverage
- having the run-time part ready, the missing part is the extending the sorting-spec-processor.ts
This commit is contained in:
SebastianMC 2023-09-19 15:34:39 +02:00
parent 24af493734
commit c5cd18f498
4 changed files with 802 additions and 61 deletions

View File

@ -0,0 +1,294 @@
import {
FolderItemForSorting,
getComparator,
getSorterFnFor,
getMdata,
OS_byCreatedTime,
OS_byModifiedTime,
OS_byModifiedTimeReverse, SortingLevelId
} from './custom-sort';
import * as CustomSortModule from './custom-sort';
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from './custom-sort-types';
const MOCK_TIMESTAMP: number = 1656417542418
const FlatLevelSortSpec: CustomSortSpec = {
groups: [{ // Not relevant in unit test
exactText: "Nothing",
filesOnly: true,
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.ExactName
},{ // prepared for unit test
exactPrefix: "Fi",
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
type: CustomSortGroupType.ExactPrefix
},{ // Not relevant in unit test
type: CustomSortGroupType.Outsiders,
order: CustomSortOrder.byCreatedTime
}],
outsidersGroupIdx: 2,
defaultOrder: CustomSortOrder.byCreatedTime,
targetFoldersPaths: ['parent folder']
}
const MultiLevelSortSpecGroupLevel: CustomSortSpec = {
groups: [{ // Not relevant in unit test
exactText: "Nothing",
filesOnly: true,
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.ExactName
},{ // prepared for unit test
exactPrefix: "Fi",
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
secondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical,
type: CustomSortGroupType.ExactPrefix
},{ // Not relevant in unit test
type: CustomSortGroupType.Outsiders,
order: CustomSortOrder.byCreatedTime
}],
outsidersGroupIdx: 2,
defaultOrder: CustomSortOrder.byCreatedTime,
targetFoldersPaths: ['parent folder']
}
const MultiLevelSortSpecTargetFolderLevel: CustomSortSpec = {
groups: [{ // Not relevant in unit test
exactText: "Nothing",
filesOnly: true,
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.ExactName
},{ // prepared for unit test
exactPrefix: "Fi",
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
type: CustomSortGroupType.ExactPrefix
},{ // Not relevant in unit test
type: CustomSortGroupType.Outsiders,
order: CustomSortOrder.byCreatedTime
}],
outsidersGroupIdx: 2,
defaultOrder: CustomSortOrder.byCreatedTime,
defaultSecondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse,
targetFoldersPaths: ['parent folder']
}
const MultiLevelSortSpecAndTargetFolderLevel: CustomSortSpec = {
groups: [{ // Not relevant in unit test
exactText: "Nothing",
filesOnly: true,
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.ExactName
},{ // prepared for unit test
exactPrefix: "Fi",
order: CustomSortOrder.byMetadataFieldAlphabetical,
secondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
type: CustomSortGroupType.ExactPrefix
},{ // Not relevant in unit test
type: CustomSortGroupType.Outsiders,
order: CustomSortOrder.byCreatedTime
}],
outsidersGroupIdx: 2,
defaultOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical,
defaultSecondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse,
targetFoldersPaths: ['parent folder']
}
const A_GOES_FIRST: number = -1
const B_GOES_FIRST: number = 1
const AB_EQUAL: number = 0
const BaseItemForSorting1: FolderItemForSorting = {
groupIdx: 1,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'parent folder/References.md',
metadataFieldValue: 'direct metadata on file, under default name',
metadataFieldValueSecondary: 'only used if secondary sort by metadata is used',
metadataFieldValueForDerived: 'only used if derived primary sort by metadata is used',
metadataFieldValueForDerivedSecondary: 'only used if derived secondary sort by metadata is used'
}
function getBaseItemForSorting(overrides?: Partial<FolderItemForSorting>): FolderItemForSorting {
return Object.assign({}, BaseItemForSorting1, overrides)
}
describe('getComparator', () => {
const sp = jest.spyOn(CustomSortModule, 'getSorterFnFor')
const collatorCmp = jest.spyOn(CustomSortModule, 'CollatorCompare')
beforeEach(() => {
sp.mockClear()
})
describe('should correctly handle flat sorting spec', () => {
const comparator = getComparator(FlatLevelSortSpec, OS_byModifiedTime)
it( 'in simple case - group-level comparison succeeds', () => {
const a = getBaseItemForSorting({
metadataFieldValue: 'value X'
})
const b= getBaseItemForSorting({
metadataFieldValue: 'value Y'
})
const result = comparator(a,b)
expect(result).toBe(B_GOES_FIRST)
expect(sp).toBeCalledTimes(1)
expect(sp).toBeCalledWith(CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
})
it( 'in simple case - group-level comparison fails, use folder-level', () => {
const a = getBaseItemForSorting()
const b= getBaseItemForSorting({
ctime: a.ctime - 100
})
const result = Math.sign(comparator(a,b))
expect(result).toBe(B_GOES_FIRST)
expect(sp).toBeCalledTimes(2)
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
})
it( 'in simple case - group-level comparison fails, folder-level fails, ui-selected in effect', () => {
const a = getBaseItemForSorting()
const b= getBaseItemForSorting({
mtime: a.mtime + 100 // Make be fresher than a
})
const result = Math.sign(comparator(a,b))
expect(result).toBe(B_GOES_FIRST)
expect(sp).toBeCalledTimes(3)
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.standardObsidian, OS_byModifiedTime, SortingLevelId.forUISelected)
})
it( 'in simple case - group-level comparison fails, folder-level fails, ui-selected fails, the last resort default comes into play - case A', () => {
const a = getBaseItemForSorting({
sortString: 'Second'
})
const b= getBaseItemForSorting({
sortString: 'First'
})
const result = comparator(a,b)
expect(result).toBe(B_GOES_FIRST)
expect(sp).toBeCalledTimes(4)
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.standardObsidian, OS_byModifiedTime, SortingLevelId.forUISelected)
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
})
})
describe('should correctly handle secondary sorting spec', () => {
beforeEach(() => {
sp.mockClear()
})
describe('at group level', () => {
const comparator = getComparator(MultiLevelSortSpecGroupLevel, OS_byModifiedTimeReverse)
it('in simple case - secondary sort comparison succeeds', () => {
const a = getBaseItemForSorting({
metadataFieldValueSecondary: 'This goes 1'
})
const b = getBaseItemForSorting({
metadataFieldValueSecondary: 'This goes 2'
})
const result = comparator(a, b)
expect(result).toBe(A_GOES_FIRST)
expect(sp).toBeCalledTimes(2)
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary)
})
it( 'in complex case - secondary sort comparison fails, last resort comes into play', () => {
const a = getBaseItemForSorting({
sortString: 'Second'
})
const b= getBaseItemForSorting({
sortString: 'First'
})
const result = comparator(a,b)
expect(result).toBe(B_GOES_FIRST)
expect(sp).toBeCalledTimes(5)
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary)
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary )
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.standardObsidian, OS_byModifiedTimeReverse, SortingLevelId.forUISelected)
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
})
})
describe('at target folder level (aka derived)', () => {
const comparator = getComparator(MultiLevelSortSpecTargetFolderLevel, OS_byModifiedTimeReverse)
it('in simple case - derived secondary sort comparison succeeds', () => {
const a = getBaseItemForSorting({
metadataFieldValueForDerivedSecondary: 'This goes 2 first (reverse is in effect)'
})
const b = getBaseItemForSorting({
metadataFieldValueForDerivedSecondary: 'This goes 1 second (reverse is in effect)'
})
const result = comparator(a, b)
expect(result).toBe(A_GOES_FIRST)
expect(sp).toBeCalledTimes(3)
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary)
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary)
})
it( 'in complex case - secondary sort comparison fails, last resort comes into play', () => {
const a = getBaseItemForSorting({
sortString: 'Second'
})
const b= getBaseItemForSorting({
sortString: 'First'
})
const result = comparator(a,b)
expect(result).toBe(B_GOES_FIRST)
expect(sp).toBeCalledTimes(5)
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary)
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary)
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.standardObsidian, OS_byModifiedTimeReverse, SortingLevelId.forUISelected)
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
})
})
describe('at group and at target folder level (aka derived)', () => {
const comparator = getComparator(MultiLevelSortSpecAndTargetFolderLevel, OS_byCreatedTime)
const mdataGetter = jest.spyOn(CustomSortModule, 'getMdata')
beforeEach(() => {
mdataGetter.mockClear()
})
it('most complex case - last resort comest into play, all sort levels present, all involve metadata', () => {
const a = getBaseItemForSorting({
path: 'test 1', // Not used in comparisons, used only to identify source of compared metadata
metadataFieldValue: 'm',
metadataFieldValueSecondary: 'ms',
metadataFieldValueForDerived: 'dm',
metadataFieldValueForDerivedSecondary: 'dms'
})
const b= getBaseItemForSorting({
path: 'test 2', // Not used in comparisons, used only to identify source of compared metadata
metadataFieldValue: 'm',
metadataFieldValueSecondary: 'ms',
metadataFieldValueForDerived: 'dm',
metadataFieldValueForDerivedSecondary: 'dms'
})
const result = Math.sign(comparator(a,b))
expect(result).toBe(AB_EQUAL)
expect(sp).toBeCalledTimes(6)
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabetical, OS_byCreatedTime, SortingLevelId.forPrimary)
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forSecondary)
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byCreatedTime, SortingLevelId.forDerivedPrimary)
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forDerivedSecondary)
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.standardObsidian, OS_byCreatedTime, SortingLevelId.forUISelected)
expect(sp).toHaveBeenNthCalledWith(6, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
expect(mdataGetter).toHaveBeenCalledTimes(8)
expect(mdataGetter).toHaveBeenNthCalledWith(1, expect.objectContaining({path: 'test 1'}), SortingLevelId.forPrimary)
expect(mdataGetter).toHaveNthReturnedWith(1, 'm')
expect(mdataGetter).toHaveBeenNthCalledWith(2, expect.objectContaining({path: 'test 2'}), SortingLevelId.forPrimary)
expect(mdataGetter).toHaveNthReturnedWith(2, 'm')
expect(mdataGetter).toHaveBeenNthCalledWith(3, expect.objectContaining({path: 'test 1'}), SortingLevelId.forSecondary)
expect(mdataGetter).toHaveNthReturnedWith(3, 'ms')
expect(mdataGetter).toHaveBeenNthCalledWith(4, expect.objectContaining({path: 'test 2'}), SortingLevelId.forSecondary)
expect(mdataGetter).toHaveNthReturnedWith(4, 'ms')
expect(mdataGetter).toHaveBeenNthCalledWith(5, expect.objectContaining({path: 'test 1'}), SortingLevelId.forDerivedPrimary)
expect(mdataGetter).toHaveNthReturnedWith(5, 'dm')
expect(mdataGetter).toHaveBeenNthCalledWith(6, expect.objectContaining({path: 'test 2'}), SortingLevelId.forDerivedPrimary)
expect(mdataGetter).toHaveNthReturnedWith(6, 'dm')
expect(mdataGetter).toHaveBeenNthCalledWith(7, expect.objectContaining({path: 'test 1'}), SortingLevelId.forDerivedSecondary)
expect(mdataGetter).toHaveNthReturnedWith(7, 'dms')
expect(mdataGetter).toHaveBeenNthCalledWith(8, expect.objectContaining({path: 'test 2'}), SortingLevelId.forDerivedSecondary)
expect(mdataGetter).toHaveNthReturnedWith(8, 'dms')
})
})
})
})

View File

@ -57,6 +57,7 @@ export interface CustomSortGroup {
overrideTitle?: boolean // instead of title, use a derived text for sorting (e.g. regexp matching group). overrideTitle?: boolean // instead of title, use a derived text for sorting (e.g. regexp matching group).
order?: CustomSortOrder order?: CustomSortOrder
byMetadataField?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse byMetadataField?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse
byMetadataFieldSecondary?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse
secondaryOrder?: CustomSortOrder secondaryOrder?: CustomSortOrder
filesOnly?: boolean filesOnly?: boolean
matchFilenameWithExt?: boolean matchFilenameWithExt?: boolean
@ -71,7 +72,9 @@ export interface CustomSortSpec {
// plays only informative role about the original parsed 'target-folder:' values // plays only informative role about the original parsed 'target-folder:' values
targetFoldersPaths: Array<string> // For root use '/' targetFoldersPaths: Array<string> // For root use '/'
defaultOrder?: CustomSortOrder defaultOrder?: CustomSortOrder
byMetadataField?: string // for 'by-metadata:' if the defaultOrder is by metadata alphabetical or reverse defaultSecondaryOrder?: CustomSortOrder
byMetadataField?: string // for 'by-metadata:' if the defaultOrder is by metadata
byMetadataFieldSecondary?: string
groups: Array<CustomSortGroup> groups: Array<CustomSortGroup>
groupsShadow?: Array<CustomSortGroup> // A shallow copy of groups, used at applying sorting for items in a folder. groupsShadow?: Array<CustomSortGroup> // A shallow copy of groups, used at applying sorting for items in a folder.
// Stores folder-specific values (e.g. macros expanded with folder-specific values) // Stores folder-specific values (e.g. macros expanded with folder-specific values)

View File

@ -6,11 +6,11 @@ import {
determineSortingGroup, determineSortingGroup,
EQUAL_OR_UNCOMPARABLE, EQUAL_OR_UNCOMPARABLE,
FolderItemForSorting, FolderItemForSorting,
matchGroupRegex,
sorterByMetadataField,
SorterFn,
getSorterFnFor, getSorterFnFor,
ProcessingContext matchGroupRegex,
ProcessingContext,
sorterByMetadataField,
SorterFn
} from './custom-sort'; } from './custom-sort';
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types';
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
@ -1881,7 +1881,7 @@ describe('determineSortingGroup', () => {
ctime: MOCK_TIMESTAMP + 222, ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md', path: 'Some parent folder/References.md',
metadataFieldValue: 'direct metadata on file, not obvious' metadataFieldValueForDerived: 'direct metadata on file, not obvious'
} as FolderItemForSorting); } as FolderItemForSorting);
}) })
it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified on group)', () => { it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified on group)', () => {
@ -1966,6 +1966,406 @@ describe('determineSortingGroup', () => {
}) })
}) })
describe('when sort by metadata is involved (specified in secondary sort, for group of for target folder)', () => {
it('should correctly read direct metadata from File item (order by metadata set on group) alph', () => {
// given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.ExactPrefix,
byMetadataFieldSecondary: 'metadata-field-for-sorting',
exactPrefix: 'Ref',
order: CustomSortOrder.alphabetical,
secondaryOrder: CustomSortOrder.byMetadataFieldAlphabetical
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'Some parent folder/References.md': {
frontmatter: {
"metadata-field-for-sorting": "direct metadata on file",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md',
metadataFieldValueSecondary: 'direct metadata on file'
} as FolderItemForSorting);
})
it('should correctly read direct metadata from File item (order by metadata set on group) alph rev', () => {
// given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.ExactPrefix,
byMetadataFieldSecondary: 'metadata-field-for-sorting',
exactPrefix: 'Ref',
order: CustomSortOrder.alphabeticalReverse,
secondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'Some parent folder/References.md': {
frontmatter: {
"metadata-field-for-sorting": "direct metadata on file",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md',
metadataFieldValueSecondary: 'direct metadata on file'
} as FolderItemForSorting);
})
it('should correctly read direct metadata from File item (order by metadata set on group) true alph', () => {
// given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.ExactPrefix,
byMetadataField: 'non-existing-mdata',
byMetadataFieldSecondary: 'metadata-field-for-sorting',
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldTrueAlphabetical,
secondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'Some parent folder/References.md': {
frontmatter: {
"metadata-field-for-sorting": "direct metadata on file",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md',
metadataFieldValueSecondary: 'direct metadata on file'
} as FolderItemForSorting);
})
it('should correctly read direct metadata from File item (order by metadata set on group) true alph rev (dbl mdata)', () => {
// given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.ExactPrefix,
byMetadataField: 'metadata-field-for-sorting',
byMetadataFieldSecondary: 'metadata-field-for-sorting secondary',
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldTrueAlphabetical,
secondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'Some parent folder/References.md': {
frontmatter: {
"metadata-field-for-sorting": "direct metadata on file",
"metadata-field-for-sorting secondary": "direct another metadata on file",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md',
metadataFieldValue: 'direct metadata on file',
metadataFieldValueSecondary: 'direct another metadata on file'
} as FolderItemForSorting);
})
it('should correctly read direct metadata from folder note item (order by metadata set on group)', () => {
// given
const folder: TFolder = mockTFolder('References');
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.ExactPrefix,
exactPrefix: 'Ref',
byMetadataFieldSecondary: 'metadata-field-for-sorting',
order: CustomSortOrder.standardObsidian,
secondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'References/References.md': {
frontmatter: {
'metadata-field-for-sorting': "metadata on folder note",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: true,
sortString: "References",
ctime: DEFAULT_FOLDER_CTIME,
mtime: DEFAULT_FOLDER_MTIME,
path: 'References',
metadataFieldValueSecondary: 'metadata on folder note',
folder: folder
} as FolderItemForSorting);
})
it('should correctly read direct metadata from File item (order by metadata set on target folder)', () => {
// given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.ExactPrefix,
exactPrefix: 'Ref',
order: CustomSortOrder.trueAlphabetical,
secondaryOrder: CustomSortOrder.byMetadataFieldAlphabetical
}],
defaultOrder: CustomSortOrder.byCreatedTime,
defaultSecondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
byMetadataFieldSecondary: 'metadata-field-for-sorting-specified-on-target-folder'
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'Some parent folder/References.md': {
frontmatter: {
"metadata-field-for-sorting-specified-on-target-folder": "direct metadata on file, not obvious",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md',
metadataFieldValueForDerivedSecondary: 'direct metadata on file, not obvious'
} as FolderItemForSorting);
})
it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified on group)', () => {
// given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.HasMetadataField,
order: CustomSortOrder.standardObsidian,
secondaryOrder: CustomSortOrder.byMetadataFieldAlphabetical,
withMetadataFieldName: 'field-used-with-with-metadata-syntax'
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'Some parent folder/References.md': {
frontmatter: {
'field-used-with-with-metadata-syntax': "direct metadata on file, tricky",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md',
metadataFieldValueSecondary: 'direct metadata on file, tricky',
} as FolderItemForSorting);
})
it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified anywhere)', () => {
// given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.ExactPrefix,
exactPrefix: 'Ref',
order: CustomSortOrder.byCreatedTimeReverse,
secondaryOrder: CustomSortOrder.byMetadataFieldAlphabetical
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'Some parent folder/References.md': {
frontmatter: {
'sort-index-value': "direct metadata on file, under default name",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md',
metadataFieldValueSecondary: 'direct metadata on file, under default name',
} as FolderItemForSorting);
})
})
describe('when sort by metadata is involved, at every level', () => {
it('should correctly read direct metadata from File item (order by metadata set at each level)', () => {
// given
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
const sortSpec: CustomSortSpec = {
targetFoldersPaths: ['/'],
groups: [{
type: CustomSortGroupType.ExactPrefix,
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabetical,
byMetadataField: 'mdata-for-primary',
secondaryOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
byMetadataFieldSecondary: 'mdata-for-secondary'
}],
defaultOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical,
byMetadataField: 'mdata-for-default-primary',
defaultSecondaryOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse,
byMetadataFieldSecondary: 'mdata-for-default-secondary'
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
'Some parent folder/References.md': {
frontmatter: {
'mdata-for-primary': "filemdata 1",
'mdata-for-secondary': "filemdata 2",
'mdata-for-default-primary': "filemdata 3",
'mdata-for-default-secondary': "filemdata 4",
position: MockedLoc
}
}
}[path]
}
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
groupIdx: 0,
isFolder: false,
sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md',
metadataFieldValue: 'filemdata 1',
metadataFieldValueSecondary: 'filemdata 2',
metadataFieldValueForDerived: 'filemdata 3',
metadataFieldValueForDerivedSecondary: 'filemdata 4',
} as FolderItemForSorting);
})
})
it('should correctly apply priority group', () => { it('should correctly apply priority group', () => {
// given // given
const file: TFile = mockTFile('Abcdef!', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); const file: TFile = mockTFile('Abcdef!', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);

View File

@ -40,13 +40,13 @@ export interface ProcessingContext {
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
} }
let CollatorCompare = new Intl.Collator(undefined, { export const CollatorCompare = new Intl.Collator(undefined, {
usage: "sort", usage: "sort",
sensitivity: "base", sensitivity: "base",
numeric: true, numeric: true,
}).compare; }).compare;
let CollatorTrueAlphabeticalCompare = new Intl.Collator(undefined, { export const CollatorTrueAlphabeticalCompare = new Intl.Collator(undefined, {
usage: "sort", usage: "sort",
sensitivity: "base", sensitivity: "base",
numeric: false, numeric: false,
@ -56,13 +56,25 @@ export interface FolderItemForSorting {
path: string path: string
groupIdx?: number // the index itself represents order for groups groupIdx?: number // the index itself represents order for groups
sortString: string // fragment (or full name) to be used for sorting sortString: string // fragment (or full name) to be used for sorting
metadataFieldValue?: string // relevant to metadata-based sorting only metadataFieldValue?: string // relevant to metadata-based group sorting only
metadataFieldValueSecondary?: string // relevant to secondary metadata-based sorting only
metadataFieldValueForDerived?: string // relevant to metadata-based sorting-spec level sorting only
metadataFieldValueForDerivedSecondary?: string // relevant to metadata-based sorting-spec level secondary sorting only
ctime: number // for a file ctime is obvious, for a folder = ctime of the oldest child file ctime: number // for a file ctime is obvious, for a folder = ctime of the oldest child file
mtime: number // for a file mtime is obvious, for a folder = date of most recently modified child file mtime: number // for a file mtime is obvious, for a folder = date of most recently modified child file
isFolder: boolean isFolder: boolean
folder?: TFolder folder?: TFolder
} }
export enum SortingLevelId {
forPrimary,
forSecondary,
forDerivedPrimary,
forDerivedSecondary,
forUISelected,
forLastResort
}
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
export type PlainSorterFn = (a: TAbstractFile, b: TAbstractFile) => number export type PlainSorterFn = (a: TAbstractFile, b: TAbstractFile) => number
export type PlainFileOnlySorterFn = (a: TFile, b: TFile) => number export type PlainFileOnlySorterFn = (a: TFile, b: TFile) => number
@ -75,19 +87,30 @@ const StraightOrder: boolean = false
export const EQUAL_OR_UNCOMPARABLE: number = 0 export const EQUAL_OR_UNCOMPARABLE: number = 0
export const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => { export const getMdata = (it: FolderItemForSorting, mdataId?: SortingLevelId) => {
switch (mdataId) {
case SortingLevelId.forSecondary: return it.metadataFieldValueSecondary
case SortingLevelId.forDerivedPrimary: return it.metadataFieldValueForDerived
case SortingLevelId.forDerivedSecondary: return it.metadataFieldValueForDerivedSecondary
case SortingLevelId.forPrimary:
default: return it.metadataFieldValue
}
}
export const sorterByMetadataField = (reverseOrder?: boolean, trueAlphabetical?: boolean, sortLevelId?: SortingLevelId): SorterFn => {
const collatorCompareFn: CollatorCompareFn = trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare const collatorCompareFn: CollatorCompareFn = trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare
return (a: FolderItemForSorting, b: FolderItemForSorting) => { return (a: FolderItemForSorting, b: FolderItemForSorting) => {
let [amdata, bmdata] = [getMdata(a, sortLevelId), getMdata(b, sortLevelId)]
if (reverseOrder) { if (reverseOrder) {
[a, b] = [b, a] [amdata, bmdata] = [bmdata, amdata]
} }
if (a.metadataFieldValue && b.metadataFieldValue) { if (amdata && bmdata) {
const sortResult: number = collatorCompareFn(a.metadataFieldValue, b.metadataFieldValue) const sortResult: number = collatorCompareFn(amdata, bmdata)
return sortResult return sortResult
} }
// Item with metadata goes before the w/o metadata // Item with metadata goes before the w/o metadata
if (a.metadataFieldValue) return -1 if (amdata) return -1
if (b.metadataFieldValue) return 1 if (bmdata) return 1
return EQUAL_OR_UNCOMPARABLE return EQUAL_OR_UNCOMPARABLE
} }
@ -106,21 +129,43 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.byCreatedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime, [CustomSortOrder.byCreatedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime,
[CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? CollatorCompare(a.sortString, b.sortString) : (b.ctime - a.ctime), [CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? CollatorCompare(a.sortString, b.sortString) : (b.ctime - a.ctime),
[CustomSortOrder.byCreatedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime, [CustomSortOrder.byCreatedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime,
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder), [CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forPrimary),
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical), [CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forPrimary),
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder), [CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forPrimary),
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical), [CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary),
// This is a fallback entry which should not be used - the getSorterFor() function below should protect against it // 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), [CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
}; };
// Some sorters are different when used in primary vs. secondary sorting order
let SortersForSecondary: { [key in CustomSortOrder]?: SorterFn } = {
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forSecondary),
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forSecondary),
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forSecondary),
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forSecondary)
};
let SortersForDerivedPrimary: { [key in CustomSortOrder]?: SorterFn } = {
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary),
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary),
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary),
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary)
};
let SortersForDerivedSecondary: { [key in CustomSortOrder]?: SorterFn } = {
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary),
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedSecondary),
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary),
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forDerivedSecondary)
};
// OS - Obsidian Sort // OS - Obsidian Sort
const OS_alphabetical = 'alphabetical' const OS_alphabetical = 'alphabetical'
const OS_alphabeticalReverse = 'alphabeticalReverse' const OS_alphabeticalReverse = 'alphabeticalReverse'
const OS_byModifiedTime = 'byModifiedTime' export const OS_byModifiedTime = 'byModifiedTime'
const OS_byModifiedTimeReverse = 'byModifiedTimeReverse' export const OS_byModifiedTimeReverse = 'byModifiedTimeReverse'
const OS_byCreatedTime = 'byCreatedTime' export const OS_byCreatedTime = 'byCreatedTime'
const OS_byCreatedTimeReverse = 'byCreatedTimeReverse' const OS_byCreatedTimeReverse = 'byCreatedTimeReverse'
export const ObsidianStandardDefaultSortingName = OS_alphabetical export const ObsidianStandardDefaultSortingName = OS_alphabetical
@ -169,29 +214,38 @@ export const StandardPlainObsidianComparator = (order: string): PlainSorterFn =>
} }
} }
export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string): SorterFn => { export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string, sortLevelId?: SortingLevelId): SorterFn => {
if (sorting === CustomSortOrder.standardObsidian) { if (sorting === CustomSortOrder.standardObsidian) {
sorting = StandardObsidianToCustomSort[currentUIselectedSorting ?? 'alphabetical'] ?? CustomSortOrder.alphabetical sorting = StandardObsidianToCustomSort[currentUIselectedSorting ?? 'alphabetical'] ?? CustomSortOrder.alphabetical
return StandardObsidianComparator(sorting) return StandardObsidianComparator(sorting)
} else { } else {
return Sorters[sorting] // Some sorters have to know at which sorting level they are used
switch(sortLevelId) {
case SortingLevelId.forSecondary: return SortersForSecondary[sorting] ?? Sorters[sorting]
case SortingLevelId.forDerivedPrimary: return SortersForDerivedPrimary[sorting] ?? Sorters[sorting]
case SortingLevelId.forDerivedSecondary: return SortersForDerivedSecondary[sorting] ?? Sorters[sorting]
case SortingLevelId.forPrimary:
default: return Sorters[sorting]
}
} }
} }
function getComparator(sortSpec: CustomSortSpec, currentUIselectedSorting?: string): SorterFn { export const getComparator = (sortSpec: CustomSortSpec, currentUIselectedSorting?: string): SorterFn => {
const compareTwoItems = (itA: FolderItemForSorting, itB: FolderItemForSorting) => { const compareTwoItems = (itA: FolderItemForSorting, itB: FolderItemForSorting) => {
if (itA.groupIdx != undefined && itB.groupIdx != undefined) { if (itA.groupIdx != undefined && itB.groupIdx != undefined) {
if (itA.groupIdx === itB.groupIdx) { if (itA.groupIdx === itB.groupIdx) {
const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx] const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx]
const primary: number = group?.order ? getSorterFnFor(group.order, currentUIselectedSorting)(itA, itB) : EQUAL_OR_UNCOMPARABLE const primary: number = group?.order ? getSorterFnFor(group.order, currentUIselectedSorting, SortingLevelId.forPrimary)(itA, itB) : EQUAL_OR_UNCOMPARABLE
if (primary !== EQUAL_OR_UNCOMPARABLE) return primary if (primary !== EQUAL_OR_UNCOMPARABLE) return primary
const secondary: number = group?.secondaryOrder ? getSorterFnFor(group.secondaryOrder, currentUIselectedSorting)(itA, itB) : EQUAL_OR_UNCOMPARABLE const secondary: number = group?.secondaryOrder ? getSorterFnFor(group.secondaryOrder, currentUIselectedSorting, SortingLevelId.forSecondary)(itA, itB) : EQUAL_OR_UNCOMPARABLE
if (secondary !== EQUAL_OR_UNCOMPARABLE) return secondary if (secondary !== EQUAL_OR_UNCOMPARABLE) return secondary
const folderLevel: number = sortSpec.defaultOrder ? getSorterFnFor(sortSpec.defaultOrder, currentUIselectedSorting)(itA, itB) : EQUAL_OR_UNCOMPARABLE const folderLevel: number = sortSpec.defaultOrder ? getSorterFnFor(sortSpec.defaultOrder, currentUIselectedSorting, SortingLevelId.forDerivedPrimary)(itA, itB) : EQUAL_OR_UNCOMPARABLE
if (folderLevel !== EQUAL_OR_UNCOMPARABLE) return folderLevel if (folderLevel !== EQUAL_OR_UNCOMPARABLE) return folderLevel
const uiSelected: number = currentUIselectedSorting ? getSorterFnFor(CustomSortOrder.standardObsidian, currentUIselectedSorting)(itA, itB) : EQUAL_OR_UNCOMPARABLE const folderLevelSecondary: number = sortSpec.defaultSecondaryOrder ? getSorterFnFor(sortSpec.defaultSecondaryOrder, currentUIselectedSorting, SortingLevelId.forDerivedSecondary)(itA, itB) : EQUAL_OR_UNCOMPARABLE
if (folderLevelSecondary !== EQUAL_OR_UNCOMPARABLE) return folderLevelSecondary
const uiSelected: number = currentUIselectedSorting ? getSorterFnFor(CustomSortOrder.standardObsidian, currentUIselectedSorting, SortingLevelId.forUISelected)(itA, itB) : EQUAL_OR_UNCOMPARABLE
if (uiSelected !== EQUAL_OR_UNCOMPARABLE) return uiSelected if (uiSelected !== EQUAL_OR_UNCOMPARABLE) return uiSelected
const lastResort: number = getSorterFnFor(CustomSortOrder.default)(itA, itB) const lastResort: number = getSorterFnFor(CustomSortOrder.default, undefined, SortingLevelId.forLastResort)(itA, itB)
return lastResort return lastResort
} else { } else {
return itA.groupIdx - itB.groupIdx; return itA.groupIdx - itB.groupIdx;
@ -243,7 +297,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
let groupIdx: number let groupIdx: number
let determined: boolean = false let determined: boolean = false
let derivedText: string | null | undefined let derivedText: string | null | undefined
let metadataValueToSortBy: string | undefined
const aFolder: boolean = isFolder(entry) const aFolder: boolean = isFolder(entry)
const aFile: boolean = !aFolder const aFile: boolean = !aFolder
const entryAsTFile: TFile = entry as TFile const entryAsTFile: TFile = entry as TFile
@ -399,39 +453,26 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
} }
} }
// The not obvious logic of determining the value of metadata field to use its value for sorting let metadataValueToSortBy: string | undefined
// - the sorting spec processor automatically populates the order field of CustomSortingGroup for each group let metadataValueSecondaryToSortBy: string | undefined
// - yet defensive code should assume some default let metadataValueDerivedPrimaryToSortBy: string | undefined
// - if the order in group is by metadata (and only in that case): let metadataValueDerivedSecondaryToSortBy: string | undefined
// - if byMetadata field name is defined for the group -> use it. Done even if value empty or not present.
// - else, if byMetadata field name is defined for the Sorting spec (folder level, for all groups) -> use it. Done even if value empty or not present.
// - else, if withMetadata field name is defined for the group -> use it. Done even if value empty or not present.
// - otherwise, fallback to the default metadata field name (hardcoded in the plugin as 'sort-index-value')
// TODO: in manual of plugin, in details, explain these nuances. Let readme.md contain only the basic simple example and reference to manual.md section
if (determined && determinedGroupIdx !== undefined) { // <-- defensive code, maybe too defensive if (determined && determinedGroupIdx !== undefined) { // <-- defensive code, maybe too defensive
const group: CustomSortGroup = spec.groups[determinedGroupIdx]; const group: CustomSortGroup = spec.groups[determinedGroupIdx];
if (isByMetadata(group?.order)) { const isPrimaryOrderByMetadata: boolean = isByMetadata(group?.order)
let metadataFieldName: string | undefined = group.byMetadataField const isSecondaryOrderByMetadata: boolean = isByMetadata(group?.secondaryOrder)
if (!metadataFieldName) { const isDerivedPrimaryByMetadata: boolean = isByMetadata(spec.defaultOrder)
if (isByMetadata(spec.defaultOrder)) { const isDerivedSecondaryByMetadata: boolean = isByMetadata(spec.defaultSecondaryOrder)
metadataFieldName = spec.byMetadataField if (isPrimaryOrderByMetadata || isSecondaryOrderByMetadata || isDerivedPrimaryByMetadata || isDerivedSecondaryByMetadata) {
} if (ctx?._mCache) {
} // For folders - scan metadata of 'folder note'
if (!metadataFieldName) { const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
metadataFieldName = group.withMetadataFieldName const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter
} if (isPrimaryOrderByMetadata) metadataValueToSortBy = frontMatterCache?.[group?.byMetadataField || group?.withMetadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING]
if (!metadataFieldName) { if (isSecondaryOrderByMetadata) metadataValueSecondaryToSortBy = frontMatterCache?.[group?.byMetadataFieldSecondary || group?.withMetadataFieldName || DEFAULT_METADATA_FIELD_FOR_SORTING]
metadataFieldName = DEFAULT_METADATA_FIELD_FOR_SORTING if (isDerivedPrimaryByMetadata) metadataValueDerivedPrimaryToSortBy = frontMatterCache?.[spec.byMetadataField || DEFAULT_METADATA_FIELD_FOR_SORTING]
} if (isDerivedSecondaryByMetadata) metadataValueDerivedSecondaryToSortBy = frontMatterCache?.[spec.byMetadataFieldSecondary || DEFAULT_METADATA_FIELD_FOR_SORTING]
if (metadataFieldName) {
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 = ctx._mCache.getCache(notePathToScan)?.frontmatter
metadataValueToSortBy = frontMatterCache?.[metadataFieldName]
}
} }
} }
} }
@ -441,6 +482,9 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
groupIdx: determinedGroupIdx, groupIdx: determinedGroupIdx,
sortString: derivedText ?? entry.name, sortString: derivedText ?? entry.name,
metadataFieldValue: metadataValueToSortBy, metadataFieldValue: metadataValueToSortBy,
metadataFieldValueSecondary: metadataValueSecondaryToSortBy,
metadataFieldValueForDerived: metadataValueDerivedPrimaryToSortBy,
metadataFieldValueForDerivedSecondary: metadataValueDerivedSecondaryToSortBy,
isFolder: aFolder, isFolder: aFolder,
folder: aFolder ? (entry as TFolder) : undefined, folder: aFolder ? (entry as TFolder) : undefined,
path: entry.path, path: entry.path,