#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: 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 `/!!!` 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 exactPrefix?: string
exactSuffix?: string exactSuffix?: string
order?: CustomSortOrder 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 secondaryOrder?: CustomSortOrder
filesOnly?: boolean filesOnly?: boolean
matchFilenameWithExt?: boolean matchFilenameWithExt?: boolean
foldersOnly?: boolean foldersOnly?: boolean
withMetadataFieldName?: string // for 'with-metadata:' withMetadataFieldName?: string // for 'with-metadata:' grouping
priority?: number priority?: number
combineWithIdx?: number
} }
export interface CustomSortSpec { export interface CustomSortSpec {

View File

@ -834,6 +834,111 @@ describe('determineSortingGroup', () => {
path: 'Some parent folder/Abcdef!.md' 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', () => { describe('determineFolderDatesIfNeeded', () => {

View File

@ -230,6 +230,14 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
const idxAfterLastGroupIdx: number = spec.groups.length const idxAfterLastGroupIdx: number = spec.groups.length
let determinedGroupIdx: number | undefined = determined ? groupIdx! : idxAfterLastGroupIdx 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) { if (!determined) {
// Automatically assign the index to outsiders group, if relevant was configured // Automatically assign the index to outsiders group, if relevant was configured
if (isDefined(spec.outsidersFilesGroupIdx) && aFile) { if (isDefined(spec.outsidersFilesGroupIdx) && aFile) {

View File

@ -7,7 +7,7 @@ import {
escapeRegexUnsafeCharacters, escapeRegexUnsafeCharacters,
extractNumericSortingSymbol, extractNumericSortingSymbol,
hasMoreThanOneNumericSortingSymbol, hasMoreThanOneNumericSortingSymbol,
NumberNormalizerFn, ProblemCode, NumberNormalizerFn,
RegexpUsedAs, RegexpUsedAs,
RomanNumberNormalizerFn, RomanNumberNormalizerFn,
SortingSpecProcessor 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', () => { describe('SortingSpecProcessor', () => {
let processor: SortingSpecProcessor; let processor: SortingSpecProcessor;
beforeEach(() => { beforeEach(() => {
@ -877,8 +913,127 @@ describe('SortingSpecProcessor', () => {
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForPriorityGroups2) expect(result?.sortSpecByPath).toEqual(expectedSortSpecForPriorityGroups2)
expect(result?.sortSpecByWildcard).toBeUndefined() 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 = ` const txtInputTargetFolderMultiSpecA: string = `
target-folder: . target-folder: .
@ -1231,10 +1386,6 @@ target-folder: AAA
sorting: standard sorting: standard
` `
const txtInputErrorPriorityAlone: string = `
/!
`
const txtInputErrorPriorityEmptyFilePattern: string = ` const txtInputErrorPriorityEmptyFilePattern: string = `
/!! /: /!! /:
` `
@ -1344,7 +1495,9 @@ describe('SortingSpecProcessor error detection and reporting', () => {
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard')) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard'))
}) })
it('should recognize error: priority indicator alone', () => { 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') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result).toBeNull() expect(result).toBeNull()
expect(errorsLogger).toHaveBeenCalledTimes(2) 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)}`) `${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('/!')) 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', () => { it('should recognize error: priority indicator with empty file pattern', () => {
const inputTxtArr: Array<string> = txtInputErrorPriorityEmptyFilePattern.split('\n') const inputTxtArr: Array<string> = txtInputErrorPriorityEmptyFilePattern.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') 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)}`) `${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('/! %')) 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', () => { it('should recognize empty spec', () => {
const inputTxtArr: Array<string> = txtInputEmptySpec.split('\n') const inputTxtArr: Array<string> = txtInputEmptySpec.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') 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 outsidersGroup?: boolean // Mutually exclusive with plainSpec and arraySpec
itemToHide?: boolean itemToHide?: boolean
priority?: number priority?: number
combine?: boolean
} }
export enum ProblemCode { export enum ProblemCode {
@ -65,12 +66,21 @@ export enum ProblemCode {
ItemToHideNoSupportForThreeDots, ItemToHideNoSupportForThreeDots,
DuplicateWildcardSortSpecForSameFolder, DuplicateWildcardSortSpecForSameFolder,
StandardObsidianSortAllowedOnlyAtFolderLevel, StandardObsidianSortAllowedOnlyAtFolderLevel,
PriorityNotAllowedOnOutsidersGroup PriorityNotAllowedOnOutsidersGroup,
TooManyPriorityPrefixes,
CombiningNotAllowedOnOutsidersGroup,
TooManyCombinePrefixes,
ModifierPrefixesOnlyOnOutsidersGroup,
OnlyLastCombinedGroupCanSpecifyOrder,
TooManyGroupTypePrefixes,
PriorityPrefixAfterGroupTypePrefix,
CombinePrefixAfterGroupTypePrefix
} }
const ContextFreeProblems = new Set<ProblemCode>([ const ContextFreeProblems = new Set<ProblemCode>([
ProblemCode.DuplicateSortSpecForSameFolder, ProblemCode.DuplicateSortSpecForSameFolder,
ProblemCode.DuplicateWildcardSortSpecForSameFolder ProblemCode.DuplicateWildcardSortSpecForSameFolder,
ProblemCode.OnlyLastCombinedGroupCanSpecifyOrder
]) ])
const ThreeDots = '...'; const ThreeDots = '...';
@ -199,6 +209,12 @@ const SortingGroupPriorityPrefixes: { [key: string]: number } = {
[PriorityModifierPrio3Lexeme]: PRIO_3 [PriorityModifierPrio3Lexeme]: PRIO_3
} }
const CombineGroupLexeme: string = '/+'
const CombiningGroupPrefixes: Array<string> = [
CombineGroupLexeme
]
interface SortingGroupType { interface SortingGroupType {
filesOnly?: boolean filesOnly?: boolean
filenameWithExt?: boolean // The text matching criteria should apply to filename + extension filenameWithExt?: boolean // The text matching criteria should apply to filename + extension
@ -490,7 +506,9 @@ export class SortingSpecProcessor {
if (success) { if (success) {
if (this.ctx.specs.length > 0) { if (this.ctx.specs.length > 0) {
for (let spec of this.ctx.specs) { for (let spec of this.ctx.specs) {
this.postprocessSortSpec(spec) if (!this.postprocessSortSpec(spec)) {
return null
}
} }
let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec> | undefined let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec> | undefined
@ -690,78 +708,149 @@ export class SortingSpecProcessor {
return null 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 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)) { for (const priorityPrefix of Object.keys(SortingGroupPriorityPrefixes)) {
if (s.startsWith(priorityPrefix + ' ')) { if (s === priorityPrefix || s.startsWith(priorityPrefix + ' ')) {
groupPriority = SortingGroupPriorityPrefixes[priorityPrefix] groupPriority = SortingGroupPriorityPrefixes[priorityPrefix]
groupPriorityPrefixesCount ++
prefixRecognized = true
doContinue = true
if (groupType) {
priorityPrefixAfterGroupTypePrefix = true
}
s = s.substring(priorityPrefix.length).trim() s = s.substring(priorityPrefix.length).trim()
break break
} }
} }
const prefixAlone: SortingGroupType = SortingGroupPrefixes[s] if (doContinue) continue
if (prefixAlone) {
if (prefixAlone.itemToHide) { for (let combinePrefix of CombiningGroupPrefixes) {
this.problem(ProblemCode.ItemToHideExactNameWithExtRequired, 'Exact name with ext of file or folders to hide is required') 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 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') this.problem(ProblemCode.PriorityNotAllowedOnOutsidersGroup, 'Priority is not allowed for sorting group with empty match-pattern')
return null 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 { return {
outsidersGroup: true, outsidersGroup: true,
filesOnly: prefixAlone.filesOnly, filesOnly: groupType.filesOnly,
foldersOnly: prefixAlone.foldersOnly foldersOnly: groupType.foldersOnly
}
} }
} }
} }
for (const prefix of Object.keys(SortingGroupPrefixes)) { if (groupType) {
if (s.startsWith(prefix + ' ')) { if (groupType.itemToHide) {
const sortingGroupType: SortingGroupType = SortingGroupPrefixes[prefix]
if (sortingGroupType.itemToHide) {
return { return {
itemToHide: true, itemToHide: true,
plainSpec: s.substring(prefix.length + 1), plainSpec: s,
filesOnly: sortingGroupType.filesOnly, filesOnly: groupType.filesOnly,
foldersOnly: sortingGroupType.foldersOnly foldersOnly: groupType.foldersOnly
} }
} else { // !sortingGroupType.itemToHide } else { // !sortingGroupType.itemToHide
return { return {
plainSpec: s.substring(prefix.length + 1), plainSpec: s,
filesOnly: sortingGroupType.filesOnly, filesOnly: groupType.filesOnly,
foldersOnly: sortingGroupType.foldersOnly, foldersOnly: groupType.foldersOnly,
matchFilenameWithExt: sortingGroupType.filenameWithExt, matchFilenameWithExt: groupType.filenameWithExt,
priority: groupPriority ?? undefined priority: groupPriority ?? undefined,
} combine: combineGroup
} }
} }
} }
if (groupPriority) { if ((groupPriority || combineGroup) && s !== '' ) {
if (s === '') { // Edge case: line with only priority prefix or combine prefix and no other known syntax, yet some content
// 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
return { return {
plainSpec: s, plainSpec: s,
priority: groupPriority priority: groupPriority,
} combine: combineGroup
} }
} }
return null; 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 { private processParsedSortGroupSpec(group: ParsedSortingGroup): boolean {
if (!this.ctx.currentSpec) { if (!this.ctx.currentSpec) {
this.ctx.currentSpec = this.putNewSpecForNewTargetFolder() this.ctx.currentSpec = this.putNewSpecForNewTargetFolder()
@ -791,7 +880,10 @@ export class SortingSpecProcessor {
newGroup.priority = group.priority newGroup.priority = group.priority
this.addExpediteGroupInfo(this.ctx.currentSpec, group.priority, groupIdx) this.addExpediteGroupInfo(this.ctx.currentSpec, group.priority, groupIdx)
} }
// Consume combined group
if (group.combine) {
newGroup.combineWithIdx = this.COMBINING_INDICATOR_IDX
}
return true; return true;
} else { } else {
return false 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 // clean up to prevent false warnings in console
spec.outsidersGroupIdx = undefined spec.outsidersGroupIdx = undefined
spec.outsidersFilesGroupIdx = 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 // Populate sorting order down the hierarchy for more clean sorting logic later on
for (let group of spec.groups) { for (let group of spec.groups) {
if (!group.order) { if (!group.order) {
@ -883,6 +1024,8 @@ export class SortingSpecProcessor {
spec.targetFoldersPaths[idx] = `${this.ctx.folderPath}/${path.substring(CURRENT_FOLDER_PREFIX.length)}` 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 // level 2 parser functions defined in order of occurrence and dependency