#35 - Feature: combining of sorting rules (#36)

- 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:
SebastianMC 2022-11-29 11:17:02 +01:00 committed by GitHub
parent 581f5e9f36
commit 84a5238814
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 590 additions and 66 deletions

View File

@ -30,7 +30,7 @@ sorting-spec: |
The resulting order of notes would be:
![Order of notes w/o priorities](./svg/priorities-example-a.svg)
![Order of notes w/o priorites](./svg/priorities-example-a.svg)
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 `/!!!`

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

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