#89 - Support for multi-level sorting
- full runtime processor extension plus necessary syntax adjustments - backward compatibility with semi-two-levels - extended meaning of sorting: lexeme - extended and more fine-grained error messages for sorting order specifications
This commit is contained in:
parent
085f5cc459
commit
1f0baebc41
|
@ -144,19 +144,7 @@ describe('getComparator', () => {
|
||||||
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
|
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
|
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', () => {
|
it( 'in simple case - group-level comparison fails, folder-level fails, the last resort default comes into play - case A', () => {
|
||||||
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({
|
const a = getBaseItemForSorting({
|
||||||
sortString: 'Second'
|
sortString: 'Second'
|
||||||
})
|
})
|
||||||
|
@ -165,11 +153,10 @@ describe('getComparator', () => {
|
||||||
})
|
})
|
||||||
const result = comparator(a,b)
|
const result = comparator(a,b)
|
||||||
expect(result).toBe(B_GOES_FIRST)
|
expect(result).toBe(B_GOES_FIRST)
|
||||||
expect(sp).toBeCalledTimes(4)
|
expect(sp).toBeCalledTimes(3)
|
||||||
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
|
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
|
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.standardObsidian, OS_byModifiedTime, SortingLevelId.forUISelected)
|
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)
|
||||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('should correctly handle secondary sorting spec', () => {
|
describe('should correctly handle secondary sorting spec', () => {
|
||||||
|
@ -191,7 +178,7 @@ describe('getComparator', () => {
|
||||||
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
|
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary)
|
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary)
|
||||||
})
|
})
|
||||||
it( 'in complex case - secondary sort comparison fails, last resort comes into play', () => {
|
it( 'in complex case - secondary sort comparison fails, last resort default comes into play', () => {
|
||||||
const a = getBaseItemForSorting({
|
const a = getBaseItemForSorting({
|
||||||
sortString: 'Second'
|
sortString: 'Second'
|
||||||
})
|
})
|
||||||
|
@ -200,12 +187,11 @@ describe('getComparator', () => {
|
||||||
})
|
})
|
||||||
const result = comparator(a,b)
|
const result = comparator(a,b)
|
||||||
expect(result).toBe(B_GOES_FIRST)
|
expect(result).toBe(B_GOES_FIRST)
|
||||||
expect(sp).toBeCalledTimes(5)
|
expect(sp).toBeCalledTimes(4)
|
||||||
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
|
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary)
|
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary )
|
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary )
|
||||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.standardObsidian, OS_byModifiedTimeReverse, SortingLevelId.forUISelected)
|
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)
|
||||||
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('at target folder level (aka derived)', () => {
|
describe('at target folder level (aka derived)', () => {
|
||||||
|
@ -224,7 +210,7 @@ describe('getComparator', () => {
|
||||||
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary)
|
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary)
|
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary)
|
||||||
})
|
})
|
||||||
it( 'in complex case - secondary sort comparison fails, last resort comes into play', () => {
|
it( 'in complex case - secondary sort comparison fails, last resort default comes into play', () => {
|
||||||
const a = getBaseItemForSorting({
|
const a = getBaseItemForSorting({
|
||||||
sortString: 'Second'
|
sortString: 'Second'
|
||||||
})
|
})
|
||||||
|
@ -233,12 +219,11 @@ describe('getComparator', () => {
|
||||||
})
|
})
|
||||||
const result = comparator(a,b)
|
const result = comparator(a,b)
|
||||||
expect(result).toBe(B_GOES_FIRST)
|
expect(result).toBe(B_GOES_FIRST)
|
||||||
expect(sp).toBeCalledTimes(5)
|
expect(sp).toBeCalledTimes(4)
|
||||||
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
|
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary)
|
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary)
|
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.standardObsidian, OS_byModifiedTimeReverse, SortingLevelId.forUISelected)
|
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)
|
||||||
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('at group and at target folder level (aka derived)', () => {
|
describe('at group and at target folder level (aka derived)', () => {
|
||||||
|
@ -247,7 +232,7 @@ describe('getComparator', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mdataGetter.mockClear()
|
mdataGetter.mockClear()
|
||||||
})
|
})
|
||||||
it('most complex case - last resort comest into play, all sort levels present, all involve metadata', () => {
|
it('most complex case - last resort default comes into play, all sort levels present, all involve metadata', () => {
|
||||||
const a = getBaseItemForSorting({
|
const a = getBaseItemForSorting({
|
||||||
path: 'test 1', // Not used in comparisons, used only to identify source of compared metadata
|
path: 'test 1', // Not used in comparisons, used only to identify source of compared metadata
|
||||||
metadataFieldValue: 'm',
|
metadataFieldValue: 'm',
|
||||||
|
@ -264,13 +249,12 @@ describe('getComparator', () => {
|
||||||
})
|
})
|
||||||
const result = Math.sign(comparator(a,b))
|
const result = Math.sign(comparator(a,b))
|
||||||
expect(result).toBe(AB_EQUAL)
|
expect(result).toBe(AB_EQUAL)
|
||||||
expect(sp).toBeCalledTimes(6)
|
expect(sp).toBeCalledTimes(5)
|
||||||
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabetical, OS_byCreatedTime, SortingLevelId.forPrimary)
|
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabetical, OS_byCreatedTime, SortingLevelId.forPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forSecondary)
|
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forSecondary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byCreatedTime, SortingLevelId.forDerivedPrimary)
|
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byCreatedTime, SortingLevelId.forDerivedPrimary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forDerivedSecondary)
|
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forDerivedSecondary)
|
||||||
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.standardObsidian, OS_byCreatedTime, SortingLevelId.forUISelected)
|
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)
|
||||||
expect(sp).toHaveBeenNthCalledWith(6, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
|
|
||||||
expect(mdataGetter).toHaveBeenCalledTimes(8)
|
expect(mdataGetter).toHaveBeenCalledTimes(8)
|
||||||
expect(mdataGetter).toHaveBeenNthCalledWith(1, expect.objectContaining({path: 'test 1'}), SortingLevelId.forPrimary)
|
expect(mdataGetter).toHaveBeenNthCalledWith(1, expect.objectContaining({path: 'test 1'}), SortingLevelId.forPrimary)
|
||||||
expect(mdataGetter).toHaveNthReturnedWith(1, 'm')
|
expect(mdataGetter).toHaveNthReturnedWith(1, 'm')
|
||||||
|
|
|
@ -35,8 +35,9 @@ export enum CustomSortOrder {
|
||||||
|
|
||||||
export interface RecognizedOrderValue {
|
export interface RecognizedOrderValue {
|
||||||
order: CustomSortOrder
|
order: CustomSortOrder
|
||||||
secondaryOrder?: CustomSortOrder
|
|
||||||
applyToMetadataField?: string
|
applyToMetadataField?: string
|
||||||
|
secondaryOrder?: CustomSortOrder
|
||||||
|
secondaryApplyToMetadataField?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NormalizerFn = (s: string) => string | null
|
export type NormalizerFn = (s: string) => string | null
|
||||||
|
@ -57,8 +58,8 @@ 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
|
||||||
|
byMetadataFieldSecondary?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse
|
||||||
filesOnly?: boolean
|
filesOnly?: boolean
|
||||||
matchFilenameWithExt?: boolean
|
matchFilenameWithExt?: boolean
|
||||||
foldersOnly?: boolean
|
foldersOnly?: boolean
|
||||||
|
|
|
@ -71,8 +71,7 @@ export enum SortingLevelId {
|
||||||
forSecondary,
|
forSecondary,
|
||||||
forDerivedPrimary,
|
forDerivedPrimary,
|
||||||
forDerivedSecondary,
|
forDerivedSecondary,
|
||||||
forUISelected,
|
forDefaultWhenUnspecified
|
||||||
forLastResort
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
||||||
|
@ -116,7 +115,7 @@ export const sorterByMetadataField = (reverseOrder?: boolean, trueAlphabetical?:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
const Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
||||||
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(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),
|
[CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(b.sortString, a.sortString),
|
||||||
|
@ -139,21 +138,21 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Some sorters are different when used in primary vs. secondary sorting order
|
// Some sorters are different when used in primary vs. secondary sorting order
|
||||||
let SortersForSecondary: { [key in CustomSortOrder]?: SorterFn } = {
|
const SortersForSecondary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||||
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forSecondary),
|
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forSecondary),
|
||||||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forSecondary),
|
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forSecondary),
|
||||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forSecondary),
|
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forSecondary),
|
||||||
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forSecondary)
|
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forSecondary)
|
||||||
};
|
};
|
||||||
|
|
||||||
let SortersForDerivedPrimary: { [key in CustomSortOrder]?: SorterFn } = {
|
const SortersForDerivedPrimary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||||
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
||||||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
||||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
||||||
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary)
|
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary)
|
||||||
};
|
};
|
||||||
|
|
||||||
let SortersForDerivedSecondary: { [key in CustomSortOrder]?: SorterFn } = {
|
const SortersForDerivedSecondary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||||
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
||||||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
||||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
||||||
|
@ -243,10 +242,8 @@ export const getComparator = (sortSpec: CustomSortSpec, currentUIselectedSorting
|
||||||
if (folderLevel !== EQUAL_OR_UNCOMPARABLE) return folderLevel
|
if (folderLevel !== EQUAL_OR_UNCOMPARABLE) return folderLevel
|
||||||
const folderLevelSecondary: number = sortSpec.defaultSecondaryOrder ? getSorterFnFor(sortSpec.defaultSecondaryOrder, currentUIselectedSorting, SortingLevelId.forDerivedSecondary)(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
|
if (folderLevelSecondary !== EQUAL_OR_UNCOMPARABLE) return folderLevelSecondary
|
||||||
const uiSelected: number = currentUIselectedSorting ? getSorterFnFor(CustomSortOrder.standardObsidian, currentUIselectedSorting, SortingLevelId.forUISelected)(itA, itB) : EQUAL_OR_UNCOMPARABLE
|
const defaultForUnspecified: number = getSorterFnFor(CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)(itA, itB)
|
||||||
if (uiSelected !== EQUAL_OR_UNCOMPARABLE) return uiSelected
|
return defaultForUnspecified
|
||||||
const lastResort: number = getSorterFnFor(CustomSortOrder.default, undefined, SortingLevelId.forLastResort)(itA, itB)
|
|
||||||
return lastResort
|
|
||||||
} else {
|
} else {
|
||||||
return itA.groupIdx - itB.groupIdx;
|
return itA.groupIdx - itB.groupIdx;
|
||||||
}
|
}
|
||||||
|
@ -538,7 +535,7 @@ export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSor
|
||||||
const groupIdx: number | undefined = item.groupIdx
|
const groupIdx: number | undefined = item.groupIdx
|
||||||
if (groupIdx !== undefined) {
|
if (groupIdx !== undefined) {
|
||||||
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
|
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
|
||||||
groupSortRequiresFolderDate = sortOrderNeedsFolderDates(groupOrder)
|
groupSortRequiresFolderDate = !!groupOrder && sortOrderNeedsFolderDates(groupOrder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (folderDefaultSortRequiresFolderDate || groupSortRequiresFolderDate) {
|
if (folderDefaultSortRequiresFolderDate || groupSortRequiresFolderDate) {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -98,15 +98,18 @@ const ContextFreeProblems = new Set<ProblemCode>([
|
||||||
const ThreeDots = '...';
|
const ThreeDots = '...';
|
||||||
const ThreeDotsLength = ThreeDots.length;
|
const ThreeDotsLength = ThreeDots.length;
|
||||||
|
|
||||||
const DEFAULT_SORT_ORDER = CustomSortOrder.alphabetical
|
|
||||||
|
|
||||||
interface CustomSortOrderAscDescPair {
|
interface CustomSortOrderAscDescPair {
|
||||||
asc: CustomSortOrder,
|
asc: CustomSortOrder
|
||||||
desc: CustomSortOrder,
|
desc: CustomSortOrder
|
||||||
secondary?: CustomSortOrder
|
|
||||||
applyToMetadataField?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CustomSortOrderSpec {
|
||||||
|
order: CustomSortOrder
|
||||||
|
byMetadataField?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_SORT_LEVEL: number = 1
|
||||||
|
|
||||||
// remember about .toLowerCase() before comparison!
|
// remember about .toLowerCase() before comparison!
|
||||||
const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
||||||
'a-z': {asc: CustomSortOrder.alphabetical, desc: CustomSortOrder.alphabeticalReverse},
|
'a-z': {asc: CustomSortOrder.alphabetical, desc: CustomSortOrder.alphabeticalReverse},
|
||||||
|
@ -115,75 +118,105 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
||||||
'modified': {asc: CustomSortOrder.byModifiedTime, desc: CustomSortOrder.byModifiedTimeReverse},
|
'modified': {asc: CustomSortOrder.byModifiedTime, desc: CustomSortOrder.byModifiedTimeReverse},
|
||||||
'advanced modified': {asc: CustomSortOrder.byModifiedTimeAdvanced, desc: CustomSortOrder.byModifiedTimeReverseAdvanced},
|
'advanced modified': {asc: CustomSortOrder.byModifiedTimeAdvanced, desc: CustomSortOrder.byModifiedTimeReverseAdvanced},
|
||||||
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
|
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
|
||||||
|
'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||||
// Advanced, for edge cases of secondary sorting, when if regexp match is the same, override the alphabetical sorting by full name
|
'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||||
'a-z, created': {
|
|
||||||
asc: CustomSortOrder.alphabetical,
|
|
||||||
desc: CustomSortOrder.alphabeticalReverse,
|
|
||||||
secondary: CustomSortOrder.byCreatedTime
|
|
||||||
},
|
|
||||||
'a-z, created desc': {
|
|
||||||
asc: CustomSortOrder.alphabetical,
|
|
||||||
desc: CustomSortOrder.alphabeticalReverse,
|
|
||||||
secondary: CustomSortOrder.byCreatedTimeReverse
|
|
||||||
},
|
|
||||||
'a-z, modified': {
|
|
||||||
asc: CustomSortOrder.alphabetical,
|
|
||||||
desc: CustomSortOrder.alphabeticalReverse,
|
|
||||||
secondary: CustomSortOrder.byModifiedTime
|
|
||||||
},
|
|
||||||
'a-z, modified desc': {
|
|
||||||
asc: CustomSortOrder.alphabetical,
|
|
||||||
desc: CustomSortOrder.alphabeticalReverse,
|
|
||||||
secondary: CustomSortOrder.byModifiedTimeReverse
|
|
||||||
},
|
|
||||||
'a-z, advanced created': {
|
|
||||||
asc: CustomSortOrder.alphabetical,
|
|
||||||
desc: CustomSortOrder.alphabeticalReverse,
|
|
||||||
secondary: CustomSortOrder.byCreatedTimeAdvanced
|
|
||||||
},
|
|
||||||
'a-z, advanced created desc': {
|
|
||||||
asc: CustomSortOrder.alphabetical,
|
|
||||||
desc: CustomSortOrder.alphabeticalReverse,
|
|
||||||
secondary: CustomSortOrder.byCreatedTimeReverseAdvanced
|
|
||||||
},
|
|
||||||
'a-z, advanced modified': {
|
|
||||||
asc: CustomSortOrder.alphabetical,
|
|
||||||
desc: CustomSortOrder.alphabeticalReverse,
|
|
||||||
secondary: CustomSortOrder.byModifiedTimeAdvanced
|
|
||||||
},
|
|
||||||
'a-z, advanced modified desc': {
|
|
||||||
asc: CustomSortOrder.alphabetical,
|
|
||||||
desc: CustomSortOrder.alphabeticalReverse,
|
|
||||||
secondary: CustomSortOrder.byModifiedTimeReverseAdvanced
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderByMetadataLexeme: string = 'by-metadata:'
|
const OrderByMetadataLexeme: string = 'by-metadata:'
|
||||||
|
|
||||||
|
const OrderLevelsSeparator: string = ','
|
||||||
|
|
||||||
enum Attribute {
|
enum Attribute {
|
||||||
TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ...
|
TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ...
|
||||||
OrderAsc,
|
OrderAsc,
|
||||||
OrderDesc,
|
OrderDesc,
|
||||||
OrderStandardObsidian
|
OrderUnspecified
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderAttribute = Exclude<Attribute, Attribute.TargetFolder>
|
||||||
|
|
||||||
|
const SortingOrderSpecInvalid: string = 'Invalid sorting order'
|
||||||
|
|
||||||
|
const ErrorMsgForAttribute: { [key in Attribute]: string } = {
|
||||||
|
[Attribute.TargetFolder]: 'Invalid target folder specification',
|
||||||
|
[Attribute.OrderAsc]: SortingOrderSpecInvalid,
|
||||||
|
[Attribute.OrderDesc]: SortingOrderSpecInvalid,
|
||||||
|
[Attribute.OrderUnspecified]: SortingOrderSpecInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
const TargetFolderLexeme: string = 'target-folder:'
|
const TargetFolderLexeme: string = 'target-folder:'
|
||||||
|
|
||||||
const AttrLexems: { [key: string]: Attribute } = {
|
const OrderDirectionAttrLexemes: { [key: string]: OrderAttribute } = {
|
||||||
// Verbose attr names
|
|
||||||
[TargetFolderLexeme]: Attribute.TargetFolder,
|
|
||||||
'order-asc:': Attribute.OrderAsc,
|
|
||||||
'order-desc:': Attribute.OrderDesc,
|
|
||||||
'sorting:': Attribute.OrderStandardObsidian,
|
|
||||||
// Concise abbreviated equivalents
|
|
||||||
'::::': Attribute.TargetFolder,
|
|
||||||
'<': Attribute.OrderAsc,
|
'<': Attribute.OrderAsc,
|
||||||
'\\<': Attribute.OrderAsc, // to allow single-liners in YAML
|
'\\<': Attribute.OrderAsc, // to allow single-liners in YAML
|
||||||
'>': Attribute.OrderDesc,
|
'>': Attribute.OrderDesc,
|
||||||
'\\>': Attribute.OrderDesc // to allow single-liners in YAML
|
'\\>': Attribute.OrderDesc // to allow single-liners in YAML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OrderDirectionPrefixAttrLexemes: { [key: string]: OrderAttribute } = {
|
||||||
|
...OrderDirectionAttrLexemes,
|
||||||
|
'order-asc:': Attribute.OrderAsc,
|
||||||
|
'order-desc:': Attribute.OrderDesc,
|
||||||
|
'sorting:': Attribute.OrderUnspecified,
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderDirectionPostfixAttrLexemes: { [key: string]: OrderAttribute } = {
|
||||||
|
...OrderDirectionAttrLexemes,
|
||||||
|
'order-asc': Attribute.OrderAsc,
|
||||||
|
'order-desc': Attribute.OrderDesc,
|
||||||
|
'asc': Attribute.OrderAsc,
|
||||||
|
'desc': Attribute.OrderDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TargetFolderLexemes: { [key: string]: Attribute } = {
|
||||||
|
[TargetFolderLexeme]: Attribute.TargetFolder,
|
||||||
|
'::::': Attribute.TargetFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttrLexemes: { [key: string]: Attribute } = {
|
||||||
|
...OrderDirectionPrefixAttrLexemes,
|
||||||
|
...OrderDirectionPostfixAttrLexemes,
|
||||||
|
...TargetFolderLexemes
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HasOrderAttrLexeme {
|
||||||
|
lexeme: string
|
||||||
|
attr: OrderAttribute
|
||||||
|
}
|
||||||
|
|
||||||
|
const startsWithOrderAttrLexeme = (s: string, postfixLexemes?: boolean): HasOrderAttrLexeme|undefined => {
|
||||||
|
const hasLexeme= Object.keys(postfixLexemes ? OrderDirectionPostfixAttrLexemes : OrderDirectionPrefixAttrLexemes)
|
||||||
|
.find((lexeme) => {
|
||||||
|
return s?.toLowerCase().startsWith(lexeme)
|
||||||
|
})
|
||||||
|
return hasLexeme ?
|
||||||
|
{lexeme: hasLexeme, attr: postfixLexemes ? OrderDirectionPostfixAttrLexemes[hasLexeme] : OrderDirectionPrefixAttrLexemes[hasLexeme]}
|
||||||
|
:
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HasOrderNameLiteral {
|
||||||
|
literal: string
|
||||||
|
order: CustomSortOrderAscDescPair
|
||||||
|
}
|
||||||
|
|
||||||
|
const startsWithOrderNameLiteral = (s: string): HasOrderNameLiteral|undefined => {
|
||||||
|
const hasLiteral= Object.keys(OrderLiterals).find((literal) => {
|
||||||
|
return s?.toLowerCase().startsWith(literal)
|
||||||
|
})
|
||||||
|
return hasLiteral ?
|
||||||
|
{literal: hasLiteral, order: OrderLiterals[hasLiteral]}
|
||||||
|
:
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrdersSupportedByMetadata: { [key in CustomSortOrder]?: CustomSortOrder} = {
|
||||||
|
[CustomSortOrder.alphabetical]: CustomSortOrder.byMetadataFieldAlphabetical,
|
||||||
|
[CustomSortOrder.alphabeticalReverse]: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
|
||||||
|
[CustomSortOrder.trueAlphabetical]: CustomSortOrder.byMetadataFieldTrueAlphabetical,
|
||||||
|
[CustomSortOrder.trueAlphabeticalReverse]: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse
|
||||||
|
}
|
||||||
|
|
||||||
const CURRENT_FOLDER_SYMBOL: string = '.'
|
const CURRENT_FOLDER_SYMBOL: string = '.'
|
||||||
|
|
||||||
interface ParsedSortingAttribute {
|
interface ParsedSortingAttribute {
|
||||||
|
@ -192,7 +225,7 @@ interface ParsedSortingAttribute {
|
||||||
value?: any
|
value?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttrValueValidatorFn = (v: string) => any | null;
|
type AttrValueValidatorFn = (v: string, attr: Attribute, attrLexeme: string) => any|AttrError|null;
|
||||||
|
|
||||||
const FilesGroupVerboseLexeme: string = '/:files'
|
const FilesGroupVerboseLexeme: string = '/:files'
|
||||||
const FilesGroupShortLexeme: string = '/:'
|
const FilesGroupShortLexeme: string = '/:'
|
||||||
|
@ -707,6 +740,11 @@ export const consumeFolderByRegexpExpression = (expression: string): ConsumedFol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AttrError {
|
||||||
|
constructor(public errorMsg: string) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Simplistic
|
// Simplistic
|
||||||
const extractIdentifier = (text: string, defaultResult?: string): string | undefined => {
|
const extractIdentifier = (text: string, defaultResult?: string): string | undefined => {
|
||||||
const identifier: string = text.trim().split(' ')?.[0]?.trim()
|
const identifier: string = text.trim().split(' ')?.[0]?.trim()
|
||||||
|
@ -911,22 +949,24 @@ export class SortingSpecProcessor {
|
||||||
}
|
}
|
||||||
const firstLexeme: string = lineTrimmedStart.substring(0, indexOfSpace)
|
const firstLexeme: string = lineTrimmedStart.substring(0, indexOfSpace)
|
||||||
const firstLexemeLowerCase: string = firstLexeme.toLowerCase()
|
const firstLexemeLowerCase: string = firstLexeme.toLowerCase()
|
||||||
const recognizedAttr: Attribute = AttrLexems[firstLexemeLowerCase]
|
const recognizedAttr: Attribute = AttrLexemes[firstLexemeLowerCase]
|
||||||
|
|
||||||
if (recognizedAttr) {
|
if (recognizedAttr) {
|
||||||
const attrValue: string = lineTrimmedStart.substring(indexOfSpace).trim()
|
const attrValue: string = lineTrimmedStart.substring(indexOfSpace).trim()
|
||||||
if (attrValue) {
|
if (attrValue) {
|
||||||
const validator: AttrValueValidatorFn = this.attrValueValidators[recognizedAttr]
|
const validator: AttrValueValidatorFn = this.attrValueValidators[recognizedAttr]
|
||||||
if (validator) {
|
if (validator) {
|
||||||
const validValue = validator(attrValue);
|
const validValue = validator(attrValue, recognizedAttr, firstLexeme);
|
||||||
if (validValue) {
|
if (validValue instanceof AttrError) {
|
||||||
|
this.problem(ProblemCode.InvalidAttributeValue, validValue.errorMsg || ErrorMsgForAttribute[recognizedAttr])
|
||||||
|
} else if (validValue) {
|
||||||
return {
|
return {
|
||||||
nesting: nestingLevel,
|
nesting: nestingLevel,
|
||||||
attribute: recognizedAttr,
|
attribute: recognizedAttr,
|
||||||
value: validValue
|
value: validValue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.problem(ProblemCode.InvalidAttributeValue, `Invalid value of the attribute "${firstLexeme}"`)
|
this.problem(ProblemCode.InvalidAttributeValue, ErrorMsgForAttribute[recognizedAttr])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
@ -936,7 +976,7 @@ export class SortingSpecProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.problem(ProblemCode.MissingAttributeValue, `Attribute "${firstLexeme}" requires a value to follow`)
|
this.problem(ProblemCode.MissingAttributeValue, `${ErrorMsgForAttribute[recognizedAttr]}: "${firstLexeme}" requires a value to follow`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null; // Seemingly not an attribute or not a valid attribute expression (respective syntax error could have been logged)
|
return null; // Seemingly not an attribute or not a valid attribute expression (respective syntax error could have been logged)
|
||||||
|
@ -960,7 +1000,7 @@ export class SortingSpecProcessor {
|
||||||
this.problem(ProblemCode.TargetFolderNestedSpec, `Nested (indented) specification of target folder is not allowed`)
|
this.problem(ProblemCode.TargetFolderNestedSpec, `Nested (indented) specification of target folder is not allowed`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if (attr.attribute === Attribute.OrderAsc || attr.attribute === Attribute.OrderDesc || attr.attribute === Attribute.OrderStandardObsidian) {
|
} else if (attr.attribute === Attribute.OrderAsc || attr.attribute === Attribute.OrderDesc || attr.attribute === Attribute.OrderUnspecified) {
|
||||||
if (attr.nesting === 0) {
|
if (attr.nesting === 0) {
|
||||||
if (!this.ctx.currentSpec) {
|
if (!this.ctx.currentSpec) {
|
||||||
this.ctx.currentSpec = this.putNewSpecForNewTargetFolder()
|
this.ctx.currentSpec = this.putNewSpecForNewTargetFolder()
|
||||||
|
@ -972,6 +1012,8 @@ export class SortingSpecProcessor {
|
||||||
}
|
}
|
||||||
this.ctx.currentSpec.defaultOrder = (attr.value as RecognizedOrderValue).order
|
this.ctx.currentSpec.defaultOrder = (attr.value as RecognizedOrderValue).order
|
||||||
this.ctx.currentSpec.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
|
this.ctx.currentSpec.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
|
||||||
|
this.ctx.currentSpec.defaultSecondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
|
||||||
|
this.ctx.currentSpec.byMetadataFieldSecondary = (attr.value as RecognizedOrderValue).secondaryApplyToMetadataField
|
||||||
return true;
|
return true;
|
||||||
} else if (attr.nesting > 0) { // For now only distinguishing nested (indented) and not-nested (not-indented), the depth doesn't matter
|
} else if (attr.nesting > 0) { // For now only distinguishing nested (indented) and not-nested (not-indented), the depth doesn't matter
|
||||||
if (!this.ctx.currentSpec || !this.ctx.currentSpecGroup) {
|
if (!this.ctx.currentSpec || !this.ctx.currentSpecGroup) {
|
||||||
|
@ -986,6 +1028,7 @@ export class SortingSpecProcessor {
|
||||||
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
|
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
|
||||||
this.ctx.currentSpecGroup.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
|
this.ctx.currentSpecGroup.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
|
||||||
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
|
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
|
||||||
|
this.ctx.currentSpecGroup.byMetadataFieldSecondary = (attr.value as RecognizedOrderValue).secondaryApplyToMetadataField
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -996,7 +1039,7 @@ export class SortingSpecProcessor {
|
||||||
const lineTrimmedStart: string = line.trimStart()
|
const lineTrimmedStart: string = line.trimStart()
|
||||||
const lineTrimmedStartLowerCase: string = lineTrimmedStart.toLowerCase()
|
const lineTrimmedStartLowerCase: string = lineTrimmedStart.toLowerCase()
|
||||||
// no space present, check for potential syntax errors
|
// no space present, check for potential syntax errors
|
||||||
for (let attrLexeme of Object.keys(AttrLexems)) {
|
for (let attrLexeme of Object.keys(AttrLexemes)) {
|
||||||
if (lineTrimmedStartLowerCase.startsWith(attrLexeme)) {
|
if (lineTrimmedStartLowerCase.startsWith(attrLexeme)) {
|
||||||
const originalAttrLexeme: string = lineTrimmedStart.substring(0, attrLexeme.length)
|
const originalAttrLexeme: string = lineTrimmedStart.substring(0, attrLexeme.length)
|
||||||
if (lineTrimmedStartLowerCase.length === attrLexeme.length) {
|
if (lineTrimmedStartLowerCase.length === attrLexeme.length) {
|
||||||
|
@ -1291,6 +1334,8 @@ export class SortingSpecProcessor {
|
||||||
if (anyCombinedGroupPresent) {
|
if (anyCombinedGroupPresent) {
|
||||||
let orderForCombinedGroup: CustomSortOrder | undefined
|
let orderForCombinedGroup: CustomSortOrder | undefined
|
||||||
let byMetadataFieldForCombinedGroup: string | undefined
|
let byMetadataFieldForCombinedGroup: string | undefined
|
||||||
|
let secondaryOrderForCombinedGroup: CustomSortOrder | undefined
|
||||||
|
let secondaryByMetadataFieldForCombinedGroup: string | undefined
|
||||||
let idxOfCurrentCombinedGroup: number | undefined = undefined
|
let idxOfCurrentCombinedGroup: number | undefined = undefined
|
||||||
for (let i = spec.groups.length - 1; i >= 0; i--) {
|
for (let i = spec.groups.length - 1; i >= 0; i--) {
|
||||||
const group: CustomSortGroup = spec.groups[i]
|
const group: CustomSortGroup = spec.groups[i]
|
||||||
|
@ -1299,28 +1344,26 @@ export class SortingSpecProcessor {
|
||||||
if (group.combineWithIdx === idxOfCurrentCombinedGroup) { // a subsequent (2nd, 3rd, ...) group of combined (counting from the end)
|
if (group.combineWithIdx === idxOfCurrentCombinedGroup) { // a subsequent (2nd, 3rd, ...) group of combined (counting from the end)
|
||||||
group.order = orderForCombinedGroup
|
group.order = orderForCombinedGroup
|
||||||
group.byMetadataField = byMetadataFieldForCombinedGroup
|
group.byMetadataField = byMetadataFieldForCombinedGroup
|
||||||
|
group.secondaryOrder = secondaryOrderForCombinedGroup
|
||||||
|
group.byMetadataFieldSecondary = secondaryByMetadataFieldForCombinedGroup
|
||||||
} else { // the first group of combined (counting from the end)
|
} else { // the first group of combined (counting from the end)
|
||||||
idxOfCurrentCombinedGroup = group.combineWithIdx
|
idxOfCurrentCombinedGroup = group.combineWithIdx
|
||||||
orderForCombinedGroup = group.order // could be undefined
|
orderForCombinedGroup = group.order // could be undefined
|
||||||
byMetadataFieldForCombinedGroup = group.byMetadataField // could be undefined
|
byMetadataFieldForCombinedGroup = group.byMetadataField // could be undefined
|
||||||
|
secondaryOrderForCombinedGroup = group.secondaryOrder // could be undefined
|
||||||
|
secondaryByMetadataFieldForCombinedGroup = group.byMetadataFieldSecondary // could be undefined
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// for sanity
|
// for sanity
|
||||||
idxOfCurrentCombinedGroup = undefined
|
idxOfCurrentCombinedGroup = undefined
|
||||||
orderForCombinedGroup = undefined
|
orderForCombinedGroup = undefined
|
||||||
byMetadataFieldForCombinedGroup = undefined
|
byMetadataFieldForCombinedGroup = undefined
|
||||||
|
secondaryOrderForCombinedGroup = undefined
|
||||||
|
secondaryByMetadataFieldForCombinedGroup = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate sorting order down the hierarchy for more clean sorting logic later on
|
|
||||||
for (let group of spec.groups) {
|
|
||||||
if (!group.order) {
|
|
||||||
group.order = spec.defaultOrder ?? DEFAULT_SORT_ORDER
|
|
||||||
group.byMetadataField = spec.byMetadataField
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any priority sorting group was present in the spec, determine the groups evaluation order
|
// If any priority sorting group was present in the spec, determine the groups evaluation order
|
||||||
if (spec.priorityOrder) {
|
if (spec.priorityOrder) {
|
||||||
// priorityOrder array already contains at least one priority group, so append all non-priority groups for the final order
|
// priorityOrder array already contains at least one priority group, so append all non-priority groups for the final order
|
||||||
|
@ -1349,85 +1392,119 @@ export class SortingSpecProcessor {
|
||||||
|
|
||||||
// level 2 parser functions defined in order of occurrence and dependency
|
// level 2 parser functions defined in order of occurrence and dependency
|
||||||
|
|
||||||
private validateTargetFolderAttrValue = (v: string): string | null => {
|
private validateTargetFolderAttrValue: AttrValueValidatorFn = (v: string, attr: Attribute, attrLexeme: string): string | null => {
|
||||||
if (v) {
|
if (v) {
|
||||||
const trimmed: string = v.trim();
|
const trimmed: string = v.trim();
|
||||||
return trimmed ? trimmed : null; // Can't use ?? - it treats '' as a valid value
|
return trimmed || null;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private internalValidateOrderAttrValue = (v: string): CustomSortOrderAscDescPair | null => {
|
private internalValidateOrderAttrValue = (sortOrderSpecText: string, prefixLexeme: string): Array<CustomSortOrderSpec>|AttrError|null => {
|
||||||
v = v.trim();
|
if (sortOrderSpecText.indexOf(CommentPrefix) >= 0) {
|
||||||
let orderLiteral: string = v
|
sortOrderSpecText = sortOrderSpecText.substring(0, sortOrderSpecText.indexOf(CommentPrefix))
|
||||||
let metadataSpec: Partial<CustomSortOrderAscDescPair> = {}
|
}
|
||||||
|
|
||||||
|
const sortLevels: Array<string> = `${prefixLexeme||''} ${sortOrderSpecText}`.trim().split(OrderLevelsSeparator)
|
||||||
|
let sortOrderSpec: Array<CustomSortOrderSpec> = []
|
||||||
|
|
||||||
|
// Max two levels are supported, excess levels specs are ignored
|
||||||
|
for (let level: number = 0; level <= MAX_SORT_LEVEL && level < sortLevels.length; level++) {
|
||||||
|
let orderNameForErrorMsg = level === 0 ? 'Primary' : 'Secondary'
|
||||||
|
let orderSpec: string = sortLevels[level].trim()
|
||||||
let applyToMetadata: boolean = false
|
let applyToMetadata: boolean = false
|
||||||
|
|
||||||
if (v.indexOf(OrderByMetadataLexeme) > 0) { // Intentionally > 0 -> not allow the metadata lexeme alone
|
// The direction (asc or desc lexeme) can come before the order literal
|
||||||
const pieces: Array<string> = v.split(OrderByMetadataLexeme)
|
// and for level 0 it always comes first (otherwise this validator would not be invoked)
|
||||||
// there are at least two pieces by definition, prefix and suffix of the metadata lexeme
|
const hasDirectionPrefix: HasOrderAttrLexeme|undefined = startsWithOrderAttrLexeme(orderSpec)
|
||||||
orderLiteral = pieces[0]?.trim()
|
orderSpec = hasDirectionPrefix ? orderSpec.substring(hasDirectionPrefix.lexeme.length).trim() : orderSpec
|
||||||
let metadataFieldName: string = pieces[1]?.trim()
|
|
||||||
if (metadataFieldName) {
|
let orderName: HasOrderNameLiteral|undefined = startsWithOrderNameLiteral(orderSpec)
|
||||||
metadataSpec.applyToMetadataField = metadataFieldName
|
orderSpec = orderName ? orderSpec.substring(orderName.literal.length).trim() : orderSpec
|
||||||
}
|
|
||||||
|
// Order direction, for level > 0 can also occur after order name or can be omitted
|
||||||
|
const hasDirectionPostfix: HasOrderAttrLexeme|undefined = (orderName) ? startsWithOrderAttrLexeme(orderSpec, true) : undefined
|
||||||
|
orderSpec = hasDirectionPostfix ? orderSpec.substring(hasDirectionPostfix.lexeme.length).trim() : orderSpec
|
||||||
|
|
||||||
|
let metadataName: string|undefined
|
||||||
|
if (orderSpec.startsWith(OrderByMetadataLexeme)) {
|
||||||
applyToMetadata = true
|
applyToMetadata = true
|
||||||
|
metadataName = orderSpec.substring(OrderByMetadataLexeme.length).trim() || undefined
|
||||||
|
orderSpec = '' // metadataName is unparsed, consumes the remainder string, even if malformed, e.g. with infix spaces
|
||||||
}
|
}
|
||||||
|
|
||||||
let attr: CustomSortOrderAscDescPair | null = orderLiteral ? OrderLiterals[orderLiteral.toLowerCase()] : null
|
// check for any superfluous text
|
||||||
if (attr) {
|
const superfluousText = orderSpec.trim()||undefined
|
||||||
if (applyToMetadata &&
|
if (superfluousText) {
|
||||||
(attr.asc === CustomSortOrder.alphabetical || attr.desc === CustomSortOrder.alphabeticalReverse ||
|
return new AttrError(`${orderNameForErrorMsg} sorting order contains unrecognized text: >>> ${superfluousText} <<<`)
|
||||||
attr.asc === CustomSortOrder.trueAlphabetical || attr.desc === CustomSortOrder.trueAlphabeticalReverse )) {
|
|
||||||
|
|
||||||
const trueAlphabetical: boolean = attr.asc === CustomSortOrder.trueAlphabetical || attr.desc === CustomSortOrder.trueAlphabeticalReverse
|
|
||||||
|
|
||||||
// Create adjusted copy
|
|
||||||
attr = {
|
|
||||||
...attr,
|
|
||||||
asc: trueAlphabetical ? CustomSortOrder.byMetadataFieldTrueAlphabetical : CustomSortOrder.byMetadataFieldAlphabetical,
|
|
||||||
desc: trueAlphabetical ? CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse : CustomSortOrder.byMetadataFieldAlphabeticalReverse
|
|
||||||
}
|
}
|
||||||
} else { // For orders different from alphabetical (and reverse) a reference to metadata is not supported
|
|
||||||
metadataSpec.applyToMetadataField = undefined
|
// check consistency of prefix and postfix orders, if both are present
|
||||||
|
if (hasDirectionPrefix && hasDirectionPostfix) {
|
||||||
|
if (hasDirectionPrefix.attr !== Attribute.OrderUnspecified && hasDirectionPostfix.attr !== Attribute.OrderUnspecified)
|
||||||
|
if (hasDirectionPrefix.attr !== hasDirectionPostfix.attr)
|
||||||
|
{
|
||||||
|
return new AttrError(`${orderNameForErrorMsg} sorting direction ${hasDirectionPrefix.lexeme} and ${hasDirectionPostfix.lexeme} are contradicting`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return attr ? {...attr, ...metadataSpec} : null
|
let order: CustomSortOrder|undefined
|
||||||
|
if (orderName) {
|
||||||
|
const direction: OrderAttribute = hasDirectionPrefix ? hasDirectionPrefix.attr : (
|
||||||
|
hasDirectionPostfix ? hasDirectionPostfix.attr : Attribute.OrderAsc
|
||||||
|
)
|
||||||
|
switch (direction) {
|
||||||
|
case Attribute.OrderAsc: order = orderName.order.asc
|
||||||
|
break
|
||||||
|
case Attribute.OrderDesc: order = orderName.order.desc
|
||||||
|
break
|
||||||
|
case Attribute.OrderUnspecified:
|
||||||
|
if (hasDirectionPostfix) {
|
||||||
|
order = hasDirectionPostfix.attr === Attribute.OrderAsc ? orderName.order.asc : orderName.order.desc
|
||||||
|
} else {
|
||||||
|
order = orderName.order.asc
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
order = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateOrderAscAttrValue = (v: string): RecognizedOrderValue | null => {
|
if (applyToMetadata) {
|
||||||
const recognized: CustomSortOrderAscDescPair | null = this.internalValidateOrderAttrValue(v)
|
if (order) {
|
||||||
return recognized ? {
|
order = OrdersSupportedByMetadata[order]
|
||||||
order: recognized.asc,
|
}
|
||||||
secondaryOrder: recognized.secondary,
|
if (!order) {
|
||||||
applyToMetadataField: recognized.applyToMetadataField
|
return new AttrError(`Sorting by metadata requires one of alphabetical orders`)
|
||||||
} : null;
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// order name not specified, this is a general syntax error
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
sortOrderSpec[level] = {
|
||||||
|
order: order!,
|
||||||
|
byMetadataField: metadataName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortOrderSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateOrderDescAttrValue = (v: string): RecognizedOrderValue | null => {
|
private validateOrderAttrValue: AttrValueValidatorFn = (v: string, attr: Attribute, attrLexeme: string): RecognizedOrderValue|AttrError|null => {
|
||||||
const recognized: CustomSortOrderAscDescPair | null = this.internalValidateOrderAttrValue(v)
|
const recognized: Array<CustomSortOrderSpec>|AttrError|null = this.internalValidateOrderAttrValue(v, attrLexeme)
|
||||||
return recognized ? {
|
return recognized ? (recognized instanceof AttrError ? recognized : {
|
||||||
order: recognized.desc,
|
order: recognized[0].order,
|
||||||
secondaryOrder: recognized.secondary,
|
applyToMetadataField: recognized[0].byMetadataField,
|
||||||
applyToMetadataField: recognized.applyToMetadataField
|
secondaryOrder: recognized[1]?.order,
|
||||||
} : null;
|
secondaryApplyToMetadataField: recognized[1]?.byMetadataField
|
||||||
}
|
}) : null;
|
||||||
|
|
||||||
private validateSortingAttrValue = (v: string): RecognizedOrderValue | null => {
|
|
||||||
// for now only a single fixed lexem
|
|
||||||
const recognized: boolean = v.trim().toLowerCase() === 'standard'
|
|
||||||
return recognized ? {
|
|
||||||
order: CustomSortOrder.standardObsidian
|
|
||||||
} : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attrValueValidators: { [key in Attribute]: AttrValueValidatorFn } = {
|
attrValueValidators: { [key in Attribute]: AttrValueValidatorFn } = {
|
||||||
[Attribute.TargetFolder]: this.validateTargetFolderAttrValue.bind(this),
|
[Attribute.TargetFolder]: this.validateTargetFolderAttrValue.bind(this),
|
||||||
[Attribute.OrderAsc]: this.validateOrderAscAttrValue.bind(this),
|
[Attribute.OrderAsc]: this.validateOrderAttrValue.bind(this),
|
||||||
[Attribute.OrderDesc]: this.validateOrderDescAttrValue.bind(this),
|
[Attribute.OrderDesc]: this.validateOrderAttrValue.bind(this),
|
||||||
[Attribute.OrderStandardObsidian]: this.validateSortingAttrValue.bind(this)
|
[Attribute.OrderUnspecified]: this.validateOrderAttrValue.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
convertPlainStringSortingGroupSpecToArraySpec = (spec: string): Array<string> => {
|
convertPlainStringSortingGroupSpecToArraySpec = (spec: string): Array<string> => {
|
||||||
|
|
Loading…
Reference in New Issue