#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:
SebastianMC 2023-09-26 18:56:44 +02:00
parent 085f5cc459
commit 1f0baebc41
5 changed files with 862 additions and 299 deletions

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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> = {}
let applyToMetadata: boolean = false
if (v.indexOf(OrderByMetadataLexeme) > 0) { // Intentionally > 0 -> not allow the metadata lexeme alone
const pieces: Array<string> = v.split(OrderByMetadataLexeme)
// there are at least two pieces by definition, prefix and suffix of the metadata lexeme
orderLiteral = pieces[0]?.trim()
let metadataFieldName: string = pieces[1]?.trim()
if (metadataFieldName) {
metadataSpec.applyToMetadataField = metadataFieldName
}
applyToMetadata = true
} }
let attr: CustomSortOrderAscDescPair | null = orderLiteral ? OrderLiterals[orderLiteral.toLowerCase()] : null const sortLevels: Array<string> = `${prefixLexeme||''} ${sortOrderSpecText}`.trim().split(OrderLevelsSeparator)
if (attr) { let sortOrderSpec: Array<CustomSortOrderSpec> = []
if (applyToMetadata &&
(attr.asc === CustomSortOrder.alphabetical || attr.desc === CustomSortOrder.alphabeticalReverse ||
attr.asc === CustomSortOrder.trueAlphabetical || attr.desc === CustomSortOrder.trueAlphabeticalReverse )) {
const trueAlphabetical: boolean = attr.asc === CustomSortOrder.trueAlphabetical || attr.desc === CustomSortOrder.trueAlphabeticalReverse // 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
// Create adjusted copy // The direction (asc or desc lexeme) can come before the order literal
attr = { // and for level 0 it always comes first (otherwise this validator would not be invoked)
...attr, const hasDirectionPrefix: HasOrderAttrLexeme|undefined = startsWithOrderAttrLexeme(orderSpec)
asc: trueAlphabetical ? CustomSortOrder.byMetadataFieldTrueAlphabetical : CustomSortOrder.byMetadataFieldAlphabetical, orderSpec = hasDirectionPrefix ? orderSpec.substring(hasDirectionPrefix.lexeme.length).trim() : orderSpec
desc: trueAlphabetical ? CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse : CustomSortOrder.byMetadataFieldAlphabeticalReverse
let orderName: HasOrderNameLiteral|undefined = startsWithOrderNameLiteral(orderSpec)
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
metadataName = orderSpec.substring(OrderByMetadataLexeme.length).trim() || undefined
orderSpec = '' // metadataName is unparsed, consumes the remainder string, even if malformed, e.g. with infix spaces
}
// check for any superfluous text
const superfluousText = orderSpec.trim()||undefined
if (superfluousText) {
return new AttrError(`${orderNameForErrorMsg} sorting order contains unrecognized text: >>> ${superfluousText} <<<`)
}
// 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`)
}
}
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
} }
} else { // For orders different from alphabetical (and reverse) a reference to metadata is not supported
metadataSpec.applyToMetadataField = undefined if (applyToMetadata) {
if (order) {
order = OrdersSupportedByMetadata[order]
}
if (!order) {
return new AttrError(`Sorting by metadata requires one of alphabetical orders`)
}
}
} else {
// order name not specified, this is a general syntax error
return null
}
sortOrderSpec[level] = {
order: order!,
byMetadataField: metadataName
} }
} }
return sortOrderSpec
return attr ? {...attr, ...metadataSpec} : null
} }
private validateOrderAscAttrValue = (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.asc, 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 validateOrderDescAttrValue = (v: string): RecognizedOrderValue | null => {
const recognized: CustomSortOrderAscDescPair | null = this.internalValidateOrderAttrValue(v)
return recognized ? {
order: recognized.desc,
secondaryOrder: recognized.secondary,
applyToMetadataField: recognized.applyToMetadataField
} : 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> => {