#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(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
|
||||
})
|
||||
it( 'in simple case - group-level comparison fails, folder-level fails, ui-selected in effect', () => {
|
||||
const a = getBaseItemForSorting()
|
||||
const b= getBaseItemForSorting({
|
||||
mtime: a.mtime + 100 // Make be fresher than a
|
||||
})
|
||||
const result = Math.sign(comparator(a,b))
|
||||
expect(result).toBe(B_GOES_FIRST)
|
||||
expect(sp).toBeCalledTimes(3)
|
||||
expect(sp).toHaveBeenNthCalledWith(1, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byModifiedTime, SortingLevelId.forPrimary)
|
||||
expect(sp).toHaveBeenNthCalledWith(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
|
||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.standardObsidian, OS_byModifiedTime, SortingLevelId.forUISelected)
|
||||
})
|
||||
it( 'in simple case - group-level comparison fails, folder-level fails, ui-selected fails, the last resort default comes into play - case A', () => {
|
||||
it( 'in simple case - group-level comparison fails, folder-level fails, the last resort default comes into play - case A', () => {
|
||||
const a = getBaseItemForSorting({
|
||||
sortString: 'Second'
|
||||
})
|
||||
|
@ -165,11 +153,10 @@ describe('getComparator', () => {
|
|||
})
|
||||
const result = comparator(a,b)
|
||||
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(2, CustomSortOrder.byCreatedTime, OS_byModifiedTime, SortingLevelId.forDerivedPrimary)
|
||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.standardObsidian, OS_byModifiedTime, SortingLevelId.forUISelected)
|
||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
|
||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)
|
||||
})
|
||||
})
|
||||
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(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({
|
||||
sortString: 'Second'
|
||||
})
|
||||
|
@ -200,12 +187,11 @@ describe('getComparator', () => {
|
|||
})
|
||||
const result = comparator(a,b)
|
||||
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(2, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byModifiedTimeReverse, SortingLevelId.forSecondary)
|
||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary )
|
||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.standardObsidian, OS_byModifiedTimeReverse, SortingLevelId.forUISelected)
|
||||
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
|
||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)
|
||||
})
|
||||
})
|
||||
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(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({
|
||||
sortString: 'Second'
|
||||
})
|
||||
|
@ -233,12 +219,11 @@ describe('getComparator', () => {
|
|||
})
|
||||
const result = comparator(a,b)
|
||||
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(2, CustomSortOrder.byCreatedTime, OS_byModifiedTimeReverse, SortingLevelId.forDerivedPrimary)
|
||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byModifiedTimeReverse, SortingLevelId.forDerivedSecondary)
|
||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.standardObsidian, OS_byModifiedTimeReverse, SortingLevelId.forUISelected)
|
||||
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
|
||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)
|
||||
})
|
||||
})
|
||||
describe('at group and at target folder level (aka derived)', () => {
|
||||
|
@ -247,7 +232,7 @@ describe('getComparator', () => {
|
|||
beforeEach(() => {
|
||||
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({
|
||||
path: 'test 1', // Not used in comparisons, used only to identify source of compared metadata
|
||||
metadataFieldValue: 'm',
|
||||
|
@ -264,13 +249,12 @@ describe('getComparator', () => {
|
|||
})
|
||||
const result = Math.sign(comparator(a,b))
|
||||
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(2, CustomSortOrder.byMetadataFieldAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forSecondary)
|
||||
expect(sp).toHaveBeenNthCalledWith(3, CustomSortOrder.byMetadataFieldTrueAlphabetical, OS_byCreatedTime, SortingLevelId.forDerivedPrimary)
|
||||
expect(sp).toHaveBeenNthCalledWith(4, CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse, OS_byCreatedTime, SortingLevelId.forDerivedSecondary)
|
||||
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.standardObsidian, OS_byCreatedTime, SortingLevelId.forUISelected)
|
||||
expect(sp).toHaveBeenNthCalledWith(6, CustomSortOrder.default, undefined, SortingLevelId.forLastResort)
|
||||
expect(sp).toHaveBeenNthCalledWith(5, CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)
|
||||
expect(mdataGetter).toHaveBeenCalledTimes(8)
|
||||
expect(mdataGetter).toHaveBeenNthCalledWith(1, expect.objectContaining({path: 'test 1'}), SortingLevelId.forPrimary)
|
||||
expect(mdataGetter).toHaveNthReturnedWith(1, 'm')
|
||||
|
|
|
@ -35,8 +35,9 @@ export enum CustomSortOrder {
|
|||
|
||||
export interface RecognizedOrderValue {
|
||||
order: CustomSortOrder
|
||||
secondaryOrder?: CustomSortOrder
|
||||
applyToMetadataField?: string
|
||||
secondaryOrder?: CustomSortOrder
|
||||
secondaryApplyToMetadataField?: string
|
||||
}
|
||||
|
||||
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).
|
||||
order?: CustomSortOrder
|
||||
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
|
||||
byMetadataFieldSecondary?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse
|
||||
filesOnly?: boolean
|
||||
matchFilenameWithExt?: boolean
|
||||
foldersOnly?: boolean
|
||||
|
|
|
@ -71,8 +71,7 @@ export enum SortingLevelId {
|
|||
forSecondary,
|
||||
forDerivedPrimary,
|
||||
forDerivedSecondary,
|
||||
forUISelected,
|
||||
forLastResort
|
||||
forDefaultWhenUnspecified
|
||||
}
|
||||
|
||||
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.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.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
|
||||
let SortersForSecondary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||
const SortersForSecondary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forSecondary),
|
||||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forSecondary),
|
||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forSecondary),
|
||||
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forSecondary)
|
||||
};
|
||||
|
||||
let SortersForDerivedPrimary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||
const SortersForDerivedPrimary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
||||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedPrimary),
|
||||
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forDerivedPrimary)
|
||||
};
|
||||
|
||||
let SortersForDerivedSecondary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||
const SortersForDerivedSecondary: { [key in CustomSortOrder]?: SorterFn } = {
|
||||
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
||||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forDerivedSecondary),
|
||||
|
@ -243,10 +242,8 @@ export const getComparator = (sortSpec: CustomSortSpec, currentUIselectedSorting
|
|||
if (folderLevel !== EQUAL_OR_UNCOMPARABLE) return folderLevel
|
||||
const folderLevelSecondary: number = sortSpec.defaultSecondaryOrder ? getSorterFnFor(sortSpec.defaultSecondaryOrder, currentUIselectedSorting, SortingLevelId.forDerivedSecondary)(itA, itB) : EQUAL_OR_UNCOMPARABLE
|
||||
if (folderLevelSecondary !== EQUAL_OR_UNCOMPARABLE) return folderLevelSecondary
|
||||
const uiSelected: number = currentUIselectedSorting ? getSorterFnFor(CustomSortOrder.standardObsidian, currentUIselectedSorting, SortingLevelId.forUISelected)(itA, itB) : EQUAL_OR_UNCOMPARABLE
|
||||
if (uiSelected !== EQUAL_OR_UNCOMPARABLE) return uiSelected
|
||||
const lastResort: number = getSorterFnFor(CustomSortOrder.default, undefined, SortingLevelId.forLastResort)(itA, itB)
|
||||
return lastResort
|
||||
const defaultForUnspecified: number = getSorterFnFor(CustomSortOrder.default, undefined, SortingLevelId.forDefaultWhenUnspecified)(itA, itB)
|
||||
return defaultForUnspecified
|
||||
} else {
|
||||
return itA.groupIdx - itB.groupIdx;
|
||||
}
|
||||
|
@ -538,7 +535,7 @@ export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSor
|
|||
const groupIdx: number | undefined = item.groupIdx
|
||||
if (groupIdx !== undefined) {
|
||||
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
|
||||
groupSortRequiresFolderDate = sortOrderNeedsFolderDates(groupOrder)
|
||||
groupSortRequiresFolderDate = !!groupOrder && sortOrderNeedsFolderDates(groupOrder)
|
||||
}
|
||||
}
|
||||
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 ThreeDotsLength = ThreeDots.length;
|
||||
|
||||
const DEFAULT_SORT_ORDER = CustomSortOrder.alphabetical
|
||||
|
||||
interface CustomSortOrderAscDescPair {
|
||||
asc: CustomSortOrder,
|
||||
desc: CustomSortOrder,
|
||||
secondary?: CustomSortOrder
|
||||
applyToMetadataField?: string
|
||||
asc: CustomSortOrder
|
||||
desc: CustomSortOrder
|
||||
}
|
||||
|
||||
interface CustomSortOrderSpec {
|
||||
order: CustomSortOrder
|
||||
byMetadataField?: string
|
||||
}
|
||||
|
||||
const MAX_SORT_LEVEL: number = 1
|
||||
|
||||
// remember about .toLowerCase() before comparison!
|
||||
const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
||||
'a-z': {asc: CustomSortOrder.alphabetical, desc: CustomSortOrder.alphabeticalReverse},
|
||||
|
@ -115,75 +118,105 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
|||
'modified': {asc: CustomSortOrder.byModifiedTime, desc: CustomSortOrder.byModifiedTimeReverse},
|
||||
'advanced modified': {asc: CustomSortOrder.byModifiedTimeAdvanced, desc: CustomSortOrder.byModifiedTimeReverseAdvanced},
|
||||
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
|
||||
|
||||
// Advanced, for edge cases of secondary sorting, when if regexp match is the same, override the alphabetical sorting by full name
|
||||
'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
|
||||
}
|
||||
'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||
'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||
}
|
||||
|
||||
const OrderByMetadataLexeme: string = 'by-metadata:'
|
||||
|
||||
const OrderLevelsSeparator: string = ','
|
||||
|
||||
enum Attribute {
|
||||
TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ...
|
||||
OrderAsc,
|
||||
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 AttrLexems: { [key: string]: Attribute } = {
|
||||
// Verbose attr names
|
||||
[TargetFolderLexeme]: Attribute.TargetFolder,
|
||||
'order-asc:': Attribute.OrderAsc,
|
||||
'order-desc:': Attribute.OrderDesc,
|
||||
'sorting:': Attribute.OrderStandardObsidian,
|
||||
// Concise abbreviated equivalents
|
||||
'::::': Attribute.TargetFolder,
|
||||
const OrderDirectionAttrLexemes: { [key: string]: OrderAttribute } = {
|
||||
'<': Attribute.OrderAsc,
|
||||
'\\<': Attribute.OrderAsc, // to allow single-liners in YAML
|
||||
'>': Attribute.OrderDesc,
|
||||
'\\>': 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 = '.'
|
||||
|
||||
interface ParsedSortingAttribute {
|
||||
|
@ -192,7 +225,7 @@ interface ParsedSortingAttribute {
|
|||
value?: any
|
||||
}
|
||||
|
||||
type AttrValueValidatorFn = (v: string) => any | null;
|
||||
type AttrValueValidatorFn = (v: string, attr: Attribute, attrLexeme: string) => any|AttrError|null;
|
||||
|
||||
const FilesGroupVerboseLexeme: string = '/:files'
|
||||
const FilesGroupShortLexeme: string = '/:'
|
||||
|
@ -707,6 +740,11 @@ export const consumeFolderByRegexpExpression = (expression: string): ConsumedFol
|
|||
}
|
||||
}
|
||||
|
||||
class AttrError {
|
||||
constructor(public errorMsg: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Simplistic
|
||||
const extractIdentifier = (text: string, defaultResult?: string): string | undefined => {
|
||||
const identifier: string = text.trim().split(' ')?.[0]?.trim()
|
||||
|
@ -911,22 +949,24 @@ export class SortingSpecProcessor {
|
|||
}
|
||||
const firstLexeme: string = lineTrimmedStart.substring(0, indexOfSpace)
|
||||
const firstLexemeLowerCase: string = firstLexeme.toLowerCase()
|
||||
const recognizedAttr: Attribute = AttrLexems[firstLexemeLowerCase]
|
||||
const recognizedAttr: Attribute = AttrLexemes[firstLexemeLowerCase]
|
||||
|
||||
if (recognizedAttr) {
|
||||
const attrValue: string = lineTrimmedStart.substring(indexOfSpace).trim()
|
||||
if (attrValue) {
|
||||
const validator: AttrValueValidatorFn = this.attrValueValidators[recognizedAttr]
|
||||
if (validator) {
|
||||
const validValue = validator(attrValue);
|
||||
if (validValue) {
|
||||
const validValue = validator(attrValue, recognizedAttr, firstLexeme);
|
||||
if (validValue instanceof AttrError) {
|
||||
this.problem(ProblemCode.InvalidAttributeValue, validValue.errorMsg || ErrorMsgForAttribute[recognizedAttr])
|
||||
} else if (validValue) {
|
||||
return {
|
||||
nesting: nestingLevel,
|
||||
attribute: recognizedAttr,
|
||||
value: validValue
|
||||
}
|
||||
} else {
|
||||
this.problem(ProblemCode.InvalidAttributeValue, `Invalid value of the attribute "${firstLexeme}"`)
|
||||
this.problem(ProblemCode.InvalidAttributeValue, ErrorMsgForAttribute[recognizedAttr])
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
|
@ -936,7 +976,7 @@ export class SortingSpecProcessor {
|
|||
}
|
||||
}
|
||||
} 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)
|
||||
|
@ -960,7 +1000,7 @@ export class SortingSpecProcessor {
|
|||
this.problem(ProblemCode.TargetFolderNestedSpec, `Nested (indented) specification of target folder is not allowed`)
|
||||
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 (!this.ctx.currentSpec) {
|
||||
this.ctx.currentSpec = this.putNewSpecForNewTargetFolder()
|
||||
|
@ -972,6 +1012,8 @@ export class SortingSpecProcessor {
|
|||
}
|
||||
this.ctx.currentSpec.defaultOrder = (attr.value as RecognizedOrderValue).order
|
||||
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;
|
||||
} 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) {
|
||||
|
@ -986,6 +1028,7 @@ export class SortingSpecProcessor {
|
|||
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
|
||||
this.ctx.currentSpecGroup.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
|
||||
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
|
||||
this.ctx.currentSpecGroup.byMetadataFieldSecondary = (attr.value as RecognizedOrderValue).secondaryApplyToMetadataField
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -996,7 +1039,7 @@ export class SortingSpecProcessor {
|
|||
const lineTrimmedStart: string = line.trimStart()
|
||||
const lineTrimmedStartLowerCase: string = lineTrimmedStart.toLowerCase()
|
||||
// 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)) {
|
||||
const originalAttrLexeme: string = lineTrimmedStart.substring(0, attrLexeme.length)
|
||||
if (lineTrimmedStartLowerCase.length === attrLexeme.length) {
|
||||
|
@ -1291,6 +1334,8 @@ export class SortingSpecProcessor {
|
|||
if (anyCombinedGroupPresent) {
|
||||
let orderForCombinedGroup: CustomSortOrder | undefined
|
||||
let byMetadataFieldForCombinedGroup: string | undefined
|
||||
let secondaryOrderForCombinedGroup: CustomSortOrder | undefined
|
||||
let secondaryByMetadataFieldForCombinedGroup: string | undefined
|
||||
let idxOfCurrentCombinedGroup: number | undefined = undefined
|
||||
for (let i = spec.groups.length - 1; i >= 0; 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)
|
||||
group.order = orderForCombinedGroup
|
||||
group.byMetadataField = byMetadataFieldForCombinedGroup
|
||||
group.secondaryOrder = secondaryOrderForCombinedGroup
|
||||
group.byMetadataFieldSecondary = secondaryByMetadataFieldForCombinedGroup
|
||||
} else { // the first group of combined (counting from the end)
|
||||
idxOfCurrentCombinedGroup = group.combineWithIdx
|
||||
orderForCombinedGroup = group.order // could be undefined
|
||||
byMetadataFieldForCombinedGroup = group.byMetadataField // could be undefined
|
||||
secondaryOrderForCombinedGroup = group.secondaryOrder // could be undefined
|
||||
secondaryByMetadataFieldForCombinedGroup = group.byMetadataFieldSecondary // could be undefined
|
||||
}
|
||||
} else {
|
||||
// for sanity
|
||||
idxOfCurrentCombinedGroup = undefined
|
||||
orderForCombinedGroup = 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 (spec.priorityOrder) {
|
||||
// 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
|
||||
|
||||
private validateTargetFolderAttrValue = (v: string): string | null => {
|
||||
private validateTargetFolderAttrValue: AttrValueValidatorFn = (v: string, attr: Attribute, attrLexeme: string): string | null => {
|
||||
if (v) {
|
||||
const trimmed: string = v.trim();
|
||||
return trimmed ? trimmed : null; // Can't use ?? - it treats '' as a valid value
|
||||
return trimmed || null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private internalValidateOrderAttrValue = (v: string): CustomSortOrderAscDescPair | null => {
|
||||
v = v.trim();
|
||||
let orderLiteral: string = v
|
||||
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
|
||||
private internalValidateOrderAttrValue = (sortOrderSpecText: string, prefixLexeme: string): Array<CustomSortOrderSpec>|AttrError|null => {
|
||||
if (sortOrderSpecText.indexOf(CommentPrefix) >= 0) {
|
||||
sortOrderSpecText = sortOrderSpecText.substring(0, sortOrderSpecText.indexOf(CommentPrefix))
|
||||
}
|
||||
|
||||
let attr: CustomSortOrderAscDescPair | null = orderLiteral ? OrderLiterals[orderLiteral.toLowerCase()] : null
|
||||
if (attr) {
|
||||
if (applyToMetadata &&
|
||||
(attr.asc === CustomSortOrder.alphabetical || attr.desc === CustomSortOrder.alphabeticalReverse ||
|
||||
attr.asc === CustomSortOrder.trueAlphabetical || attr.desc === CustomSortOrder.trueAlphabeticalReverse )) {
|
||||
const sortLevels: Array<string> = `${prefixLexeme||''} ${sortOrderSpecText}`.trim().split(OrderLevelsSeparator)
|
||||
let sortOrderSpec: Array<CustomSortOrderSpec> = []
|
||||
|
||||
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
|
||||
attr = {
|
||||
...attr,
|
||||
asc: trueAlphabetical ? CustomSortOrder.byMetadataFieldTrueAlphabetical : CustomSortOrder.byMetadataFieldAlphabetical,
|
||||
desc: trueAlphabetical ? CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse : CustomSortOrder.byMetadataFieldAlphabeticalReverse
|
||||
// The direction (asc or desc lexeme) can come before the order literal
|
||||
// and for level 0 it always comes first (otherwise this validator would not be invoked)
|
||||
const hasDirectionPrefix: HasOrderAttrLexeme|undefined = startsWithOrderAttrLexeme(orderSpec)
|
||||
orderSpec = hasDirectionPrefix ? orderSpec.substring(hasDirectionPrefix.lexeme.length).trim() : orderSpec
|
||||
|
||||
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 attr ? {...attr, ...metadataSpec} : null
|
||||
return sortOrderSpec
|
||||
}
|
||||
|
||||
private validateOrderAscAttrValue = (v: string): RecognizedOrderValue | null => {
|
||||
const recognized: CustomSortOrderAscDescPair | null = this.internalValidateOrderAttrValue(v)
|
||||
return recognized ? {
|
||||
order: recognized.asc,
|
||||
secondaryOrder: recognized.secondary,
|
||||
applyToMetadataField: recognized.applyToMetadataField
|
||||
} : 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;
|
||||
private validateOrderAttrValue: AttrValueValidatorFn = (v: string, attr: Attribute, attrLexeme: string): RecognizedOrderValue|AttrError|null => {
|
||||
const recognized: Array<CustomSortOrderSpec>|AttrError|null = this.internalValidateOrderAttrValue(v, attrLexeme)
|
||||
return recognized ? (recognized instanceof AttrError ? recognized : {
|
||||
order: recognized[0].order,
|
||||
applyToMetadataField: recognized[0].byMetadataField,
|
||||
secondaryOrder: recognized[1]?.order,
|
||||
secondaryApplyToMetadataField: recognized[1]?.byMetadataField
|
||||
}) : null;
|
||||
}
|
||||
|
||||
attrValueValidators: { [key in Attribute]: AttrValueValidatorFn } = {
|
||||
[Attribute.TargetFolder]: this.validateTargetFolderAttrValue.bind(this),
|
||||
[Attribute.OrderAsc]: this.validateOrderAscAttrValue.bind(this),
|
||||
[Attribute.OrderDesc]: this.validateOrderDescAttrValue.bind(this),
|
||||
[Attribute.OrderStandardObsidian]: this.validateSortingAttrValue.bind(this)
|
||||
[Attribute.OrderAsc]: this.validateOrderAttrValue.bind(this),
|
||||
[Attribute.OrderDesc]: this.validateOrderAttrValue.bind(this),
|
||||
[Attribute.OrderUnspecified]: this.validateOrderAttrValue.bind(this)
|
||||
}
|
||||
|
||||
convertPlainStringSortingGroupSpecToArraySpec = (spec: string): Array<string> => {
|
||||
|
|
Loading…
Reference in New Issue