- full unit tests coverage of the new functionality - refactor of the parser to allow more flexible syntax and be able to detect more errors - introduced many new errors recognized by the parser
This commit is contained in:
parent
581f5e9f36
commit
84a5238814
|
@ -30,7 +30,7 @@ sorting-spec: |
|
|||
|
||||
The resulting order of notes would be:
|
||||
|
||||

|
||||

|
||||
|
||||
However, a group can be assigned a higher priority in the sorting spec. In result, folder items will be matched against them _before_ any other rules. To impose a priority on a group use the prefix `/!` or `/!!` or `/!!!`
|
||||
|
||||
|
|
|
@ -51,13 +51,14 @@ export interface CustomSortGroup {
|
|||
exactPrefix?: string
|
||||
exactSuffix?: string
|
||||
order?: CustomSortOrder
|
||||
byMetadataField?: string // for 'by-metadata:' if the order is by metadata alphabetical or reverse
|
||||
byMetadataField?: string // for 'by-metadata:' sorting if the order is by metadata alphabetical or reverse
|
||||
secondaryOrder?: CustomSortOrder
|
||||
filesOnly?: boolean
|
||||
matchFilenameWithExt?: boolean
|
||||
foldersOnly?: boolean
|
||||
withMetadataFieldName?: string // for 'with-metadata:'
|
||||
withMetadataFieldName?: string // for 'with-metadata:' grouping
|
||||
priority?: number
|
||||
combineWithIdx?: number
|
||||
}
|
||||
|
||||
export interface CustomSortSpec {
|
||||
|
|
|
@ -834,6 +834,111 @@ describe('determineSortingGroup', () => {
|
|||
path: 'Some parent folder/Abcdef!.md'
|
||||
});
|
||||
})
|
||||
it('should correctly recognize and apply combined group', () => {
|
||||
// given
|
||||
const file1: TFile = mockTFile('Hello :-) ha', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||
const file2: TFile = mockTFile('Hello World :-)', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||
const sortSpec: CustomSortSpec = {
|
||||
groups: [{
|
||||
exactSuffix: "def!",
|
||||
order: CustomSortOrder.alphabeticalReverse,
|
||||
type: CustomSortGroupType.ExactSuffix
|
||||
}, {
|
||||
exactPrefix: "Hello :-)",
|
||||
order: CustomSortOrder.alphabeticalReverse,
|
||||
type: CustomSortGroupType.ExactPrefix,
|
||||
combineWithIdx: 1
|
||||
}, {
|
||||
exactText: "Hello World :-)",
|
||||
order: CustomSortOrder.alphabeticalReverse,
|
||||
type: CustomSortGroupType.ExactName,
|
||||
combineWithIdx: 1
|
||||
}, {
|
||||
filesOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.MatchAll
|
||||
}, {
|
||||
foldersOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.MatchAll
|
||||
}, {
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.Outsiders
|
||||
}],
|
||||
outsidersGroupIdx: 5,
|
||||
targetFoldersPaths: ['/']
|
||||
}
|
||||
|
||||
// when
|
||||
const result1 = determineSortingGroup(file1, sortSpec)
|
||||
const result2 = determineSortingGroup(file2, sortSpec)
|
||||
|
||||
// then
|
||||
expect(result1).toEqual({
|
||||
groupIdx: 1, // Imposed by combined groups
|
||||
isFolder: false,
|
||||
sortString: "Hello :-) ha.md",
|
||||
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||
mtime: MOCK_TIMESTAMP + 333,
|
||||
path: 'Some parent folder/Hello :-) ha.md'
|
||||
});
|
||||
expect(result2).toEqual({
|
||||
groupIdx: 1, // Imposed by combined groups
|
||||
isFolder: false,
|
||||
sortString: "Hello World :-).md",
|
||||
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||
mtime: MOCK_TIMESTAMP + 333,
|
||||
path: 'Some parent folder/Hello World :-).md'
|
||||
});
|
||||
})
|
||||
it('should correctly recognize and apply combined group in connection with priorities', () => {
|
||||
// given
|
||||
const file: TFile = mockTFile('Hello :-)', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||
const sortSpec: CustomSortSpec = {
|
||||
groups: [{
|
||||
filesOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.MatchAll
|
||||
}, {
|
||||
foldersOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.MatchAll
|
||||
}, {
|
||||
exactSuffix: "def!",
|
||||
order: CustomSortOrder.alphabeticalReverse,
|
||||
type: CustomSortGroupType.ExactSuffix,
|
||||
combineWithIdx: 2
|
||||
}, {
|
||||
exactText: "Hello :-)",
|
||||
order: CustomSortOrder.alphabeticalReverse,
|
||||
type: CustomSortGroupType.ExactName,
|
||||
priority: 1,
|
||||
combineWithIdx: 2
|
||||
}, {
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.Outsiders
|
||||
}],
|
||||
outsidersGroupIdx: 4,
|
||||
priorityOrder: [3,0,1,2],
|
||||
targetFoldersPaths: ['/']
|
||||
}
|
||||
|
||||
// when
|
||||
const result = determineSortingGroup(file, sortSpec)
|
||||
|
||||
// then
|
||||
expect(result).toEqual({
|
||||
groupIdx: 2, // Imposed by combined groups
|
||||
isFolder: false,
|
||||
sortString: "Hello :-).md",
|
||||
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||
mtime: MOCK_TIMESTAMP + 333,
|
||||
path: 'Some parent folder/Hello :-).md'
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
describe('determineFolderDatesIfNeeded', () => {
|
||||
|
|
|
@ -230,6 +230,14 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
const idxAfterLastGroupIdx: number = spec.groups.length
|
||||
let determinedGroupIdx: number | undefined = determined ? groupIdx! : idxAfterLastGroupIdx
|
||||
|
||||
// Redirection to the first group of combined, if detected
|
||||
if (determined) {
|
||||
const combinedGroupIdx: number | undefined = spec.groups[determinedGroupIdx].combineWithIdx
|
||||
if (combinedGroupIdx !== undefined) {
|
||||
determinedGroupIdx = combinedGroupIdx
|
||||
}
|
||||
}
|
||||
|
||||
if (!determined) {
|
||||
// Automatically assign the index to outsiders group, if relevant was configured
|
||||
if (isDefined(spec.outsidersFilesGroupIdx) && aFile) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
escapeRegexUnsafeCharacters,
|
||||
extractNumericSortingSymbol,
|
||||
hasMoreThanOneNumericSortingSymbol,
|
||||
NumberNormalizerFn, ProblemCode,
|
||||
NumberNormalizerFn,
|
||||
RegexpUsedAs,
|
||||
RomanNumberNormalizerFn,
|
||||
SortingSpecProcessor
|
||||
|
@ -860,6 +860,42 @@ const expectedSortSpecForPriorityGroups2: { [key: string]: CustomSortSpec } = {
|
|||
}
|
||||
}
|
||||
|
||||
const expectedSortSpecForPriorityAndCombineGroups: { [key: string]: CustomSortSpec } = {
|
||||
"/": {
|
||||
groups: [{
|
||||
combineWithIdx: 0,
|
||||
exactPrefix: "Fi",
|
||||
filesOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
priority: 1,
|
||||
type: CustomSortGroupType.ExactPrefix
|
||||
}, {
|
||||
combineWithIdx: 0,
|
||||
exactPrefix: "Fo",
|
||||
foldersOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
priority: 2,
|
||||
type: CustomSortGroupType.ExactPrefix
|
||||
}, {
|
||||
exactSuffix: "def!",
|
||||
priority: 3,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.ExactSuffix
|
||||
}, {
|
||||
exactText: "Anything",
|
||||
order: CustomSortOrder.alphabetical,
|
||||
priority: 1,
|
||||
type: CustomSortGroupType.ExactName
|
||||
}, {
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.Outsiders
|
||||
}],
|
||||
outsidersGroupIdx: 4,
|
||||
targetFoldersPaths: ['/'],
|
||||
priorityOrder: [2,1,0,3]
|
||||
}
|
||||
}
|
||||
|
||||
describe('SortingSpecProcessor', () => {
|
||||
let processor: SortingSpecProcessor;
|
||||
beforeEach(() => {
|
||||
|
@ -877,8 +913,127 @@ describe('SortingSpecProcessor', () => {
|
|||
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForPriorityGroups2)
|
||||
expect(result?.sortSpecByWildcard).toBeUndefined()
|
||||
})
|
||||
it('should recognize the combine and priority prefixes in any order example 1', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
target-folder: /
|
||||
/! /+ /:files Fi...
|
||||
/!! /+ /folders Fo...
|
||||
/!!! ...def!
|
||||
/! Anything
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForPriorityAndCombineGroups)
|
||||
expect(result?.sortSpecByWildcard).toBeUndefined()
|
||||
})
|
||||
it('should recognize the combine and priority prefixes in any order example 2', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
target-folder: /
|
||||
/+ /! /:files Fi...
|
||||
/+ /!! /folders Fo...
|
||||
/!!! ...def!
|
||||
/! Anything
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForPriorityAndCombineGroups)
|
||||
expect(result?.sortSpecByWildcard).toBeUndefined()
|
||||
})
|
||||
it('should accept the combine operator in single line only', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
target-folder: /
|
||||
/+ /:files Fi...
|
||||
/folders Fo...
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result?.sortSpecByPath).toEqual({
|
||||
"/": {
|
||||
groups: [{
|
||||
combineWithIdx: 0,
|
||||
exactPrefix: "Fi",
|
||||
filesOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.ExactPrefix
|
||||
}, {
|
||||
exactPrefix: "Fo",
|
||||
foldersOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.ExactPrefix
|
||||
}, {
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.Outsiders
|
||||
}],
|
||||
outsidersGroupIdx: 2,
|
||||
targetFoldersPaths: ['/']
|
||||
}
|
||||
})
|
||||
expect(result?.sortSpecByWildcard).toBeUndefined()
|
||||
})
|
||||
it('should correctly parse combine operator, apply default sorting and explicit sorting', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
target-folder: /
|
||||
Nothing
|
||||
> a-z
|
||||
/+ /:files Fi...
|
||||
/+ /folders Fo...
|
||||
... Separator
|
||||
/+ Abc...
|
||||
/+ ...Def
|
||||
/+ ...
|
||||
> modified
|
||||
Unreachable line
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result?.sortSpecByPath).toEqual({
|
||||
"/": {
|
||||
groups: [{
|
||||
exactText: "Nothing",
|
||||
order: CustomSortOrder.alphabeticalReverse,
|
||||
type: CustomSortGroupType.ExactName
|
||||
}, {
|
||||
combineWithIdx: 1,
|
||||
exactPrefix: "Fi",
|
||||
filesOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.ExactPrefix
|
||||
}, {
|
||||
combineWithIdx: 1,
|
||||
exactPrefix: "Fo",
|
||||
foldersOnly: true,
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.ExactPrefix
|
||||
}, {
|
||||
exactSuffix: " Separator",
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.ExactSuffix
|
||||
}, {
|
||||
combineWithIdx: 4,
|
||||
exactPrefix: "Abc",
|
||||
order: CustomSortOrder.byModifiedTimeReverse,
|
||||
type: CustomSortGroupType.ExactPrefix
|
||||
}, {
|
||||
combineWithIdx: 4,
|
||||
exactSuffix: "Def",
|
||||
order: CustomSortOrder.byModifiedTimeReverse,
|
||||
type: CustomSortGroupType.ExactSuffix
|
||||
}, {
|
||||
combineWithIdx: 4,
|
||||
order: CustomSortOrder.byModifiedTimeReverse,
|
||||
type: CustomSortGroupType.MatchAll
|
||||
}, {
|
||||
exactText: "Unreachable line",
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.ExactName
|
||||
}, {
|
||||
order: CustomSortOrder.alphabetical,
|
||||
type: CustomSortGroupType.Outsiders
|
||||
}],
|
||||
outsidersGroupIdx: 8,
|
||||
targetFoldersPaths: ['/']
|
||||
}
|
||||
})
|
||||
expect(result?.sortSpecByWildcard).toBeUndefined()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
const txtInputTargetFolderMultiSpecA: string = `
|
||||
target-folder: .
|
||||
|
@ -1231,10 +1386,6 @@ target-folder: AAA
|
|||
sorting: standard
|
||||
`
|
||||
|
||||
const txtInputErrorPriorityAlone: string = `
|
||||
/!
|
||||
`
|
||||
|
||||
const txtInputErrorPriorityEmptyFilePattern: string = `
|
||||
/!! /:
|
||||
`
|
||||
|
@ -1344,7 +1495,9 @@ describe('SortingSpecProcessor error detection and reporting', () => {
|
|||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard'))
|
||||
})
|
||||
it('should recognize error: priority indicator alone', () => {
|
||||
const inputTxtArr: Array<string> = txtInputErrorPriorityAlone.split('\n')
|
||||
const inputTxtArr: Array<string> = `
|
||||
/!
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
|
@ -1352,6 +1505,28 @@ describe('SortingSpecProcessor error detection and reporting', () => {
|
|||
`${ERR_PREFIX} 15:PriorityNotAllowedOnOutsidersGroup Priority is not allowed for sorting group with empty match-pattern ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/!'))
|
||||
})
|
||||
it('should recognize error: multiple priority indicators alone', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/! /!! /!!!
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 16:TooManyPriorityPrefixes Only one priority prefix allowed on sorting group ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/! /!! /!!!'))
|
||||
})
|
||||
it('should recognize error: multiple priority indicators', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/!!! /!!! Abc\.d+ ...
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 16:TooManyPriorityPrefixes Only one priority prefix allowed on sorting group ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/!!! /!!! Abc\.d+ ...'))
|
||||
})
|
||||
it('should recognize error: priority indicator with empty file pattern', () => {
|
||||
const inputTxtArr: Array<string> = txtInputErrorPriorityEmptyFilePattern.split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
|
@ -1379,6 +1554,98 @@ describe('SortingSpecProcessor error detection and reporting', () => {
|
|||
`${ERR_PREFIX} 15:PriorityNotAllowedOnOutsidersGroup Priority is not allowed for sorting group with empty match-pattern ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/! %'))
|
||||
})
|
||||
it('should recognize error of combining: sorting order on first group', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/+ Abc
|
||||
> modified
|
||||
/+ /:files def
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(1)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 20:OnlyLastCombinedGroupCanSpecifyOrder Predecessor group of combined group cannot contain order specification. Put it at the last of group in combined groups ${ERR_SUFFIX}`)
|
||||
})
|
||||
it('should recognize error of combining: sorting order not on last group', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/+ Abc
|
||||
/+ ...Def
|
||||
/+ Ghi...
|
||||
> modified
|
||||
/+ /:files def
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(1)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 20:OnlyLastCombinedGroupCanSpecifyOrder Predecessor group of combined group cannot contain order specification. Put it at the last of group in combined groups ${ERR_SUFFIX}`)
|
||||
})
|
||||
it('should recognize error of combining: combining not allowed for outsiders group', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/+ %
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 17:CombiningNotAllowedOnOutsidersGroup Combining is not allowed for sorting group with empty match-pattern ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/+ %'))
|
||||
})
|
||||
it('should recognize error of combining: combining not allowed for outsiders priority group', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/+ /! /
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 15:PriorityNotAllowedOnOutsidersGroup Priority is not allowed for sorting group with empty match-pattern ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/+ /! /'))
|
||||
})
|
||||
it('should recognize error of combining: multiple combine operators', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/+ /! /+ /: Something
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 18:TooManyCombinePrefixes Only one combining prefix allowed on sorting group ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/+ /! /+ /: Something'))
|
||||
})
|
||||
it('should recognize error: too many sorting group type prefixes', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/folders /:files Hello
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 21:TooManyGroupTypePrefixes Only one sorting group type prefix allowed on sorting group ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/folders /:files Hello'))
|
||||
})
|
||||
it('should recognize error: priority prefix after sorting group type prefixe', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/folders /+ /! Hello
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 22:PriorityPrefixAfterGroupTypePrefix Priority prefix must be used before sorting group type indicator ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/folders /+ /! Hello'))
|
||||
})
|
||||
it('should recognize error: combine prefix after sorting group type prefixe', () => {
|
||||
const inputTxtArr: Array<string> = `
|
||||
/folders /+ Hello
|
||||
`.replace(/\t/gi, '').split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
expect(result).toBeNull()
|
||||
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||
`${ERR_PREFIX} 23:CombinePrefixAfterGroupTypePrefix Combining prefix must be used before sorting group type indicator ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('/folders /+ Hello'))
|
||||
})
|
||||
it('should recognize empty spec', () => {
|
||||
const inputTxtArr: Array<string> = txtInputEmptySpec.split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
|
|
|
@ -47,6 +47,7 @@ interface ParsedSortingGroup {
|
|||
outsidersGroup?: boolean // Mutually exclusive with plainSpec and arraySpec
|
||||
itemToHide?: boolean
|
||||
priority?: number
|
||||
combine?: boolean
|
||||
}
|
||||
|
||||
export enum ProblemCode {
|
||||
|
@ -65,12 +66,21 @@ export enum ProblemCode {
|
|||
ItemToHideNoSupportForThreeDots,
|
||||
DuplicateWildcardSortSpecForSameFolder,
|
||||
StandardObsidianSortAllowedOnlyAtFolderLevel,
|
||||
PriorityNotAllowedOnOutsidersGroup
|
||||
PriorityNotAllowedOnOutsidersGroup,
|
||||
TooManyPriorityPrefixes,
|
||||
CombiningNotAllowedOnOutsidersGroup,
|
||||
TooManyCombinePrefixes,
|
||||
ModifierPrefixesOnlyOnOutsidersGroup,
|
||||
OnlyLastCombinedGroupCanSpecifyOrder,
|
||||
TooManyGroupTypePrefixes,
|
||||
PriorityPrefixAfterGroupTypePrefix,
|
||||
CombinePrefixAfterGroupTypePrefix
|
||||
}
|
||||
|
||||
const ContextFreeProblems = new Set<ProblemCode>([
|
||||
ProblemCode.DuplicateSortSpecForSameFolder,
|
||||
ProblemCode.DuplicateWildcardSortSpecForSameFolder
|
||||
ProblemCode.DuplicateWildcardSortSpecForSameFolder,
|
||||
ProblemCode.OnlyLastCombinedGroupCanSpecifyOrder
|
||||
])
|
||||
|
||||
const ThreeDots = '...';
|
||||
|
@ -199,6 +209,12 @@ const SortingGroupPriorityPrefixes: { [key: string]: number } = {
|
|||
[PriorityModifierPrio3Lexeme]: PRIO_3
|
||||
}
|
||||
|
||||
const CombineGroupLexeme: string = '/+'
|
||||
|
||||
const CombiningGroupPrefixes: Array<string> = [
|
||||
CombineGroupLexeme
|
||||
]
|
||||
|
||||
interface SortingGroupType {
|
||||
filesOnly?: boolean
|
||||
filenameWithExt?: boolean // The text matching criteria should apply to filename + extension
|
||||
|
@ -490,7 +506,9 @@ export class SortingSpecProcessor {
|
|||
if (success) {
|
||||
if (this.ctx.specs.length > 0) {
|
||||
for (let spec of this.ctx.specs) {
|
||||
this.postprocessSortSpec(spec)
|
||||
if (!this.postprocessSortSpec(spec)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec> | undefined
|
||||
|
@ -690,78 +708,149 @@ export class SortingSpecProcessor {
|
|||
return null
|
||||
}
|
||||
|
||||
const priorityPrefixAlone: number = SortingGroupPriorityPrefixes[s]
|
||||
if (priorityPrefixAlone) {
|
||||
this.problem(ProblemCode.PriorityNotAllowedOnOutsidersGroup, 'Priority is not allowed for sorting group with empty match-pattern')
|
||||
return null
|
||||
}
|
||||
|
||||
let groupPriority: number | undefined = undefined
|
||||
let groupPriorityPrefixesCount: number = 0
|
||||
let combineGroup: boolean | undefined = undefined
|
||||
let combineGroupPrefixesCount: number = 0
|
||||
let groupType: SortingGroupType | undefined = undefined
|
||||
let groupTypePrefixesCount: number = 0
|
||||
let priorityPrefixAfterGroupTypePrefix: boolean = false
|
||||
let combinePrefixAfterGroupTypePrefix: boolean = false
|
||||
|
||||
let prefixRecognized: boolean | undefined = undefined
|
||||
while (prefixRecognized === undefined || prefixRecognized) {
|
||||
let doContinue: boolean = false // to support 'continue' on external loop from nested loop
|
||||
|
||||
for (const priorityPrefix of Object.keys(SortingGroupPriorityPrefixes)) {
|
||||
if (s.startsWith(priorityPrefix + ' ')) {
|
||||
if (s === priorityPrefix || s.startsWith(priorityPrefix + ' ')) {
|
||||
groupPriority = SortingGroupPriorityPrefixes[priorityPrefix]
|
||||
groupPriorityPrefixesCount ++
|
||||
prefixRecognized = true
|
||||
doContinue = true
|
||||
if (groupType) {
|
||||
priorityPrefixAfterGroupTypePrefix = true
|
||||
}
|
||||
s = s.substring(priorityPrefix.length).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const prefixAlone: SortingGroupType = SortingGroupPrefixes[s]
|
||||
if (prefixAlone) {
|
||||
if (prefixAlone.itemToHide) {
|
||||
this.problem(ProblemCode.ItemToHideExactNameWithExtRequired, 'Exact name with ext of file or folders to hide is required')
|
||||
if (doContinue) continue
|
||||
|
||||
for (let combinePrefix of CombiningGroupPrefixes) {
|
||||
if (s === combinePrefix || s.startsWith(combinePrefix + ' ')) {
|
||||
combineGroup = true
|
||||
combineGroupPrefixesCount ++
|
||||
prefixRecognized = true
|
||||
doContinue = true
|
||||
if (groupType) {
|
||||
combinePrefixAfterGroupTypePrefix = true
|
||||
}
|
||||
s = s.substring(combinePrefix.length).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (doContinue) continue
|
||||
|
||||
for (const sortingGroupTypePrefix of Object.keys(SortingGroupPrefixes)) {
|
||||
if (s === sortingGroupTypePrefix || s.startsWith(sortingGroupTypePrefix + ' ')) {
|
||||
groupType = SortingGroupPrefixes[sortingGroupTypePrefix]
|
||||
groupTypePrefixesCount++
|
||||
prefixRecognized = true
|
||||
doContinue = true
|
||||
s = s.substring(sortingGroupTypePrefix.length).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (doContinue) continue
|
||||
|
||||
prefixRecognized = false
|
||||
}
|
||||
|
||||
if (groupPriorityPrefixesCount > 1) {
|
||||
this.problem(ProblemCode.TooManyPriorityPrefixes, 'Only one priority prefix allowed on sorting group')
|
||||
return null
|
||||
} else { // !prefixAlone.itemToHide
|
||||
if (groupPriority) {
|
||||
}
|
||||
|
||||
if (s === '' && groupPriority) {
|
||||
this.problem(ProblemCode.PriorityNotAllowedOnOutsidersGroup, 'Priority is not allowed for sorting group with empty match-pattern')
|
||||
return null
|
||||
} else {
|
||||
}
|
||||
|
||||
if (combineGroupPrefixesCount > 1) {
|
||||
this.problem(ProblemCode.TooManyCombinePrefixes, 'Only one combining prefix allowed on sorting group')
|
||||
return null
|
||||
}
|
||||
|
||||
if (s === '' && combineGroup) {
|
||||
this.problem(ProblemCode.CombiningNotAllowedOnOutsidersGroup, 'Combining is not allowed for sorting group with empty match-pattern')
|
||||
return null
|
||||
}
|
||||
|
||||
if (groupTypePrefixesCount > 1) {
|
||||
this.problem(ProblemCode.TooManyGroupTypePrefixes, 'Only one sorting group type prefix allowed on sorting group')
|
||||
return null
|
||||
}
|
||||
|
||||
if (priorityPrefixAfterGroupTypePrefix) {
|
||||
this.problem(ProblemCode.PriorityPrefixAfterGroupTypePrefix, 'Priority prefix must be used before sorting group type indicator')
|
||||
return null
|
||||
}
|
||||
|
||||
if (combinePrefixAfterGroupTypePrefix) {
|
||||
this.problem(ProblemCode.CombinePrefixAfterGroupTypePrefix, 'Combining prefix must be used before sorting group type indicator')
|
||||
return null
|
||||
}
|
||||
|
||||
if (s === '' && groupType) { // alone alone alone
|
||||
if (groupType.itemToHide) {
|
||||
this.problem(ProblemCode.ItemToHideExactNameWithExtRequired, 'Exact name with ext of file or folders to hide is required')
|
||||
return null
|
||||
} else { // !sortingGroupIndicatorPrefixAlone.itemToHide
|
||||
return {
|
||||
outsidersGroup: true,
|
||||
filesOnly: prefixAlone.filesOnly,
|
||||
foldersOnly: prefixAlone.foldersOnly
|
||||
}
|
||||
filesOnly: groupType.filesOnly,
|
||||
foldersOnly: groupType.foldersOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of Object.keys(SortingGroupPrefixes)) {
|
||||
if (s.startsWith(prefix + ' ')) {
|
||||
const sortingGroupType: SortingGroupType = SortingGroupPrefixes[prefix]
|
||||
if (sortingGroupType.itemToHide) {
|
||||
if (groupType) {
|
||||
if (groupType.itemToHide) {
|
||||
return {
|
||||
itemToHide: true,
|
||||
plainSpec: s.substring(prefix.length + 1),
|
||||
filesOnly: sortingGroupType.filesOnly,
|
||||
foldersOnly: sortingGroupType.foldersOnly
|
||||
plainSpec: s,
|
||||
filesOnly: groupType.filesOnly,
|
||||
foldersOnly: groupType.foldersOnly
|
||||
}
|
||||
} else { // !sortingGroupType.itemToHide
|
||||
return {
|
||||
plainSpec: s.substring(prefix.length + 1),
|
||||
filesOnly: sortingGroupType.filesOnly,
|
||||
foldersOnly: sortingGroupType.foldersOnly,
|
||||
matchFilenameWithExt: sortingGroupType.filenameWithExt,
|
||||
priority: groupPriority ?? undefined
|
||||
}
|
||||
plainSpec: s,
|
||||
filesOnly: groupType.filesOnly,
|
||||
foldersOnly: groupType.foldersOnly,
|
||||
matchFilenameWithExt: groupType.filenameWithExt,
|
||||
priority: groupPriority ?? undefined,
|
||||
combine: combineGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groupPriority) {
|
||||
if (s === '') {
|
||||
// Edge case: line with only priority prefix and no other content
|
||||
this.problem(ProblemCode.PriorityNotAllowedOnOutsidersGroup, 'Priority is not allowed for sorting group with empty match-pattern')
|
||||
return null
|
||||
} else {
|
||||
// Edge case: line with only priority prefix and no other known syntax, yet some content
|
||||
if ((groupPriority || combineGroup) && s !== '' ) {
|
||||
// Edge case: line with only priority prefix or combine prefix and no other known syntax, yet some content
|
||||
return {
|
||||
plainSpec: s,
|
||||
priority: groupPriority
|
||||
}
|
||||
priority: groupPriority,
|
||||
combine: combineGroup
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Artificial value used to indicate not-undefined value in if (COMBINING_INDICATOR_IDX) { ... }
|
||||
COMBINING_INDICATOR_IDX: number = -1
|
||||
|
||||
private processParsedSortGroupSpec(group: ParsedSortingGroup): boolean {
|
||||
if (!this.ctx.currentSpec) {
|
||||
this.ctx.currentSpec = this.putNewSpecForNewTargetFolder()
|
||||
|
@ -791,7 +880,10 @@ export class SortingSpecProcessor {
|
|||
newGroup.priority = group.priority
|
||||
this.addExpediteGroupInfo(this.ctx.currentSpec, group.priority, groupIdx)
|
||||
}
|
||||
|
||||
// Consume combined group
|
||||
if (group.combine) {
|
||||
newGroup.combineWithIdx = this.COMBINING_INDICATOR_IDX
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false
|
||||
|
@ -805,7 +897,7 @@ export class SortingSpecProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
private postprocessSortSpec(spec: CustomSortSpec): void {
|
||||
private postprocessSortSpec(spec: CustomSortSpec): boolean {
|
||||
// clean up to prevent false warnings in console
|
||||
spec.outsidersGroupIdx = undefined
|
||||
spec.outsidersFilesGroupIdx = undefined
|
||||
|
@ -853,6 +945,55 @@ export class SortingSpecProcessor {
|
|||
})
|
||||
}
|
||||
|
||||
// Process 'combined groups'
|
||||
let anyCombinedGroupPresent: boolean = false
|
||||
let currentCombinedGroupIdx: number | undefined = undefined
|
||||
for (let i=0; i<spec.groups.length; i++) {
|
||||
const group: CustomSortGroup = spec.groups[i]
|
||||
if (group.combineWithIdx === this.COMBINING_INDICATOR_IDX) { // Here we expect the COMBINING_INDICATOR_IDX artificial value or undefined
|
||||
if (currentCombinedGroupIdx === undefined) {
|
||||
currentCombinedGroupIdx = i
|
||||
} else {
|
||||
// Ensure that the preceding group doesn't contain sorting order
|
||||
if (spec.groups[i - 1].order) {
|
||||
this.problem(ProblemCode.OnlyLastCombinedGroupCanSpecifyOrder, 'Predecessor group of combined group cannot contain order specification. Put it at the last of group in combined groups')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
group.combineWithIdx = currentCombinedGroupIdx
|
||||
anyCombinedGroupPresent = true
|
||||
} else {
|
||||
currentCombinedGroupIdx = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Populate sorting order within combined groups
|
||||
if (anyCombinedGroupPresent) {
|
||||
let orderForCombinedGroup: CustomSortOrder | undefined
|
||||
let byMetadataFieldForCombinedGroup: string | undefined
|
||||
let idxOfCurrentCombinedGroup: number | undefined = undefined
|
||||
for (let i = spec.groups.length - 1; i >= 0; i--) {
|
||||
const group: CustomSortGroup = spec.groups[i]
|
||||
|
||||
if (group.combineWithIdx !== undefined) {
|
||||
if (group.combineWithIdx === idxOfCurrentCombinedGroup) { // a subsequent (2nd, 3rd, ...) group of combined (counting from the end)
|
||||
group.order = orderForCombinedGroup
|
||||
group.byMetadataField = byMetadataFieldForCombinedGroup
|
||||
} 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
|
||||
}
|
||||
} else {
|
||||
// for sanity
|
||||
idxOfCurrentCombinedGroup = undefined
|
||||
orderForCombinedGroup = undefined
|
||||
byMetadataFieldForCombinedGroup = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate sorting order down the hierarchy for more clean sorting logic later on
|
||||
for (let group of spec.groups) {
|
||||
if (!group.order) {
|
||||
|
@ -883,6 +1024,8 @@ export class SortingSpecProcessor {
|
|||
spec.targetFoldersPaths[idx] = `${this.ctx.folderPath}/${path.substring(CURRENT_FOLDER_PREFIX.length)}`
|
||||
}
|
||||
});
|
||||
|
||||
return true // success indicator
|
||||
}
|
||||
|
||||
// level 2 parser functions defined in order of occurrence and dependency
|
||||
|
|
Loading…
Reference in New Issue