From 581f5e9f36ae43bfab4821c2f3899262ca686724 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 29 Nov 2022 10:17:15 +0100 Subject: [PATCH] #29 - Feature: priorities of sorting rules (#31) - Implementation with full coverage of unit tests - Documentation update with details about priorities (as an advanced feature, only in manual.md, not in README.md) --- docs/manual.md | 66 +++++++ docs/svg/priorities-example-a.svg | 60 ++++++ docs/svg/priorities-example-b.svg | 60 ++++++ src/custom-sort/custom-sort-types.ts | 4 +- src/custom-sort/custom-sort.spec.ts | 46 +++++ src/custom-sort/custom-sort.ts | 13 +- .../sorting-spec-processor.spec.ts | 184 +++++++++++++++++- src/custom-sort/sorting-spec-processor.ts | 108 +++++++++- 8 files changed, 520 insertions(+), 21 deletions(-) create mode 100644 docs/svg/priorities-example-a.svg create mode 100644 docs/svg/priorities-example-b.svg diff --git a/docs/manual.md b/docs/manual.md index b9968e6..7f92291 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1,3 +1,69 @@ Yet to be filled with content ;-) See [syntax-reference.md](./syntax-reference.md), maybe that file has already some content? + +--- +Some sections added ad-hoc, to be integrated later + +# Advanced features + +## Priorities of sorting groups + +At run-time, when the custom sorting is triggered (explicitly or automatically) each folder item (a file or a sub-folder) is evaluated against the sorting groups. +The evaluation (matching) is done in the order in which the sorting groups are defined in `sorting-spec: |` for the folder. + +That means, for example, that the sorting group `/:files ...` will match _all_ files - in turn, none of files has a chance to match further rule + +Consider the below example: +```yaml +--- +sorting-spec: | + target-folder: Some folder + // The below sorting group captures (matches) all files + /:files ... + // The below sorting group should (theoretically) capture files with names starting with 'Archive' word + // yet none of files will have a chance to reach the rule, because the previous sorting group will match all files + // Hence, the below sorting group is void + /:files Archive... +--- +``` + +The resulting order of notes would be: + +![Order of notes w/o priorities](./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 `/!!!` + +The modified example would be: +```yaml +--- +sorting-spec: | + target-folder: Some folder + // The below sorting group captures (matches) all files + /:files ... + // The below sorting group captures files with names starting with 'Archive' word + // and thanks to the priority indicator prefix '/!' folder items are matched against it + // before matching the previous sorting group + /! /:files Archive... +--- +``` + +and it would result in the expected order of items: + +![Order of notes with group priorites](./svg/priorities-example-b.svg) + +For clarity: the three available prefixes `/!` and `/!!` and `/!!!` allow for futher finetuning of sorting groups matching order, the `/!!!` representing the highest priority value + +> A SIDE NOTE +> +> In the above simplistic example, correct grouping of items can also be achieved in a different way: +> instead of using priorities, the first sorting group could be expressed differently as `/:files` (no following `...` wildcard): +> ```yaml +> --- +> sorting-spec: | +> target-folder: Some folder +> /:files +> /:files Archive... +> --- +> ``` +> The sorting group expressed as `/:files` alone acts as a sorting group 'catch-all-files, which don't match any other sorting rule for the folder' diff --git a/docs/svg/priorities-example-a.svg b/docs/svg/priorities-example-a.svg new file mode 100644 index 0000000..4b911c2 --- /dev/null +++ b/docs/svg/priorities-example-a.svg @@ -0,0 +1,60 @@ + + + + + Produced by OmniGraffle 7.20\n2022-08-06 14:27:13 +0000 + + Roman sections + + Layer 1 + + + + + + Some folder + + + + + Alpha + + + + + Archive April + + + + + Archive Mai + + + + + Archive March + + + + + + + My Vault + + + + + Beta + + + + + + + + Gamma + + + + + diff --git a/docs/svg/priorities-example-b.svg b/docs/svg/priorities-example-b.svg new file mode 100644 index 0000000..35246a8 --- /dev/null +++ b/docs/svg/priorities-example-b.svg @@ -0,0 +1,60 @@ + + + + + Produced by OmniGraffle 7.20\n2022-08-06 14:27:13 +0000 + + Roman sections + + Layer 1 + + + + + + Some folder + + + + + Alpha + + + + + Beta + + + + + Gamma + + + + + Archive April + + + + + + + My Vault + + + + + Archive Mai + + + + + + + + Archive March + + + + + diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 1ce15b3..5515373 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -57,6 +57,7 @@ export interface CustomSortGroup { matchFilenameWithExt?: boolean foldersOnly?: boolean withMetadataFieldName?: string // for 'with-metadata:' + priority?: number } export interface CustomSortSpec { @@ -68,9 +69,10 @@ export interface CustomSortSpec { outsidersFilesGroupIdx?: number outsidersFoldersGroupIdx?: number itemsToHide?: Set - plugin?: Plugin // to hand over the access to App instance to the sorting engine + priorityOrder?: Array // Indexes of groups in evaluation order // For internal transient use + plugin?: Plugin // to hand over the access to App instance to the sorting engine _mCache?: MetadataCache } diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index 3809eb3..5ae7501 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -788,6 +788,52 @@ describe('determineSortingGroup', () => { } as FolderItemForSorting); }) }) + + it('should correctly apply priority group', () => { + // given + const file: TFile = mockTFile('Abcdef!', '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!", + priority: 2, + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactSuffix + }, { + exactText: "Abcdef!", + order: CustomSortOrder.alphabetical, + priority: 3, + type: CustomSortGroupType.ExactName + }, { + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 4, + targetFoldersPaths: ['/'], + priorityOrder: [3,2,0,1] + } + + // when + const result = determineSortingGroup(file, sortSpec) + + // then + expect(result).toEqual({ + groupIdx: 3, + isFolder: false, + sortString: "Abcdef!.md", + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/Abcdef!.md' + }); + }) }) describe('determineFolderDatesIfNeeded', () => { diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 4f3f6e8..1fe007c 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -129,8 +129,13 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus const entryAsTFile: TFile = entry as TFile const basename: string = aFolder ? entry.name : entryAsTFile.basename - for (groupIdx = 0; groupIdx < spec.groups.length; groupIdx++) { + // When priorities come in play, the ordered list of groups to check could be shorter + // than the actual full set of defined groups, because the outsiders group are not + // in the ordered list (aka priorityOrder array) + const numOfGroupsToCheck: number = spec.priorityOrder ? spec.priorityOrder.length : spec.groups.length + for (let idx = 0; idx < numOfGroupsToCheck; idx++) { matchedGroup = null + groupIdx = spec.priorityOrder ? spec.priorityOrder[idx] : idx const group: CustomSortGroup = spec.groups[groupIdx]; if (group.foldersOnly && aFile) continue; if (group.filesOnly && aFolder) continue; @@ -218,12 +223,12 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus break } if (determined) { - break; + break; // No need to check other sorting groups } } - // the final groupIdx for undetermined folder entry is either the last+1 groupIdx or idx of explicitly defined outsiders group - let determinedGroupIdx: number | undefined = groupIdx; + const idxAfterLastGroupIdx: number = spec.groups.length + let determinedGroupIdx: number | undefined = determined ? groupIdx! : idxAfterLastGroupIdx if (!determined) { // Automatically assign the index to outsiders group, if relevant was configured diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index 34c899d..7085c96 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -7,7 +7,7 @@ import { escapeRegexUnsafeCharacters, extractNumericSortingSymbol, hasMoreThanOneNumericSortingSymbol, - NumberNormalizerFn, + NumberNormalizerFn, ProblemCode, RegexpUsedAs, RomanNumberNormalizerFn, SortingSpecProcessor @@ -535,7 +535,7 @@ describe('SortingSpecProcessor', () => { const txtInputSimplistic1: string = ` target-folder: /* /:files -//folders +/folders ` const expectedSortSpecForSimplistic1: { [key: string]: CustomSortSpec } = { @@ -545,11 +545,12 @@ const expectedSortSpecForSimplistic1: { [key: string]: CustomSortSpec } = { order: CustomSortOrder.alphabetical, type: CustomSortGroupType.Outsiders }, { + foldersOnly: true, order: CustomSortOrder.alphabetical, type: CustomSortGroupType.Outsiders }], outsidersFilesGroupIdx: 0, - outsidersGroupIdx: 1, + outsidersFoldersGroupIdx: 1, targetFoldersPaths: ['/*'] } } @@ -561,11 +562,12 @@ const expectedWildcardMatchingTreeForSimplistic1 = { order: CustomSortOrder.alphabetical, type: CustomSortGroupType.Outsiders }, { + foldersOnly: true, order: CustomSortOrder.alphabetical, type: CustomSortGroupType.Outsiders }], outsidersFilesGroupIdx: 0, - outsidersGroupIdx: 1, + outsidersFoldersGroupIdx: 1, targetFoldersPaths: ['/*'] }, "subtree": {} @@ -574,7 +576,7 @@ const expectedWildcardMatchingTreeForSimplistic1 = { const txtInputSimplistic2: string = ` target-folder: / /:files -//folders +/folders ` const expectedSortSpecForSimplistic2: { [key: string]: CustomSortSpec } = { @@ -584,11 +586,12 @@ const expectedSortSpecForSimplistic2: { [key: string]: CustomSortSpec } = { order: CustomSortOrder.alphabetical, type: CustomSortGroupType.Outsiders }, { + foldersOnly: true, order: CustomSortOrder.alphabetical, type: CustomSortGroupType.Outsiders }], outsidersFilesGroupIdx: 0, - outsidersGroupIdx: 1, + outsidersFoldersGroupIdx: 1, targetFoldersPaths: ['/'] } } @@ -760,6 +763,123 @@ describe('SortingSpecProcessor edge case', () => { }) }) +const txtInputPriorityGroups1: string = ` +target-folder: / +/:files +/folders +/! /:files Fi... +/!! /folders Fo... +/!!! ...def! +Plain text +/! % Anything +` + +const txtInputPriorityGroups2: string = ` +target-folder: / +/! /:files Fi... +/!! /folders Fo... +/!!! ...def! +/! Anything +` + +const expectedSortSpecForPriorityGroups1: { [key: string]: CustomSortSpec } = { + "/": { + groups: [{ + filesOnly: true, + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.Outsiders + }, { + foldersOnly: true, + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.Outsiders + }, { + exactPrefix: "Fi", + filesOnly: true, + order: CustomSortOrder.alphabetical, + priority: 1, + type: CustomSortGroupType.ExactPrefix + }, { + exactPrefix: "Fo", + foldersOnly: true, + order: CustomSortOrder.alphabetical, + priority: 2, + type: CustomSortGroupType.ExactPrefix + }, { + exactSuffix: "def!", + priority: 3, + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactSuffix + }, { + exactText: "Plain text", + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactName + },{ + exactText: "Anything", + order: CustomSortOrder.alphabetical, + priority: 1, + type: CustomSortGroupType.ExactName + }], + outsidersFilesGroupIdx: 0, + outsidersFoldersGroupIdx: 1, + targetFoldersPaths: ['/'], + priorityOrder: [4,3,2,6,5] + } +} + +const expectedSortSpecForPriorityGroups2: { [key: string]: CustomSortSpec } = { + "/": { + groups: [{ + exactPrefix: "Fi", + filesOnly: true, + order: CustomSortOrder.alphabetical, + priority: 1, + type: CustomSortGroupType.ExactPrefix + }, { + 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(() => { + processor = new SortingSpecProcessor(); + }); + it('should recognize the sorting groups with priority example 1', () => { + const inputTxtArr: Array = txtInputPriorityGroups1.split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result?.sortSpecByPath).toEqual(expectedSortSpecForPriorityGroups1) + expect(result?.sortSpecByWildcard).toBeUndefined() + }) + it('should recognize the sorting groups with priority example 2', () => { + const inputTxtArr: Array = txtInputPriorityGroups2.split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result?.sortSpecByPath).toEqual(expectedSortSpecForPriorityGroups2) + expect(result?.sortSpecByWildcard).toBeUndefined() + }) +}) + + const txtInputTargetFolderMultiSpecA: string = ` target-folder: . < a-z @@ -1111,6 +1231,22 @@ target-folder: AAA sorting: standard ` +const txtInputErrorPriorityAlone: string = ` +/! +` + +const txtInputErrorPriorityEmptyFilePattern: string = ` +/!! /: +` + +const txtInputErrorPriorityEmptyFolderPattern: string = ` +/!!! / +` + +const txtInputErrorPriorityEmptyPattern: string = ` +/! % +` + const txtInputEmptySpec: string = `` describe('SortingSpecProcessor error detection and reporting', () => { @@ -1207,6 +1343,42 @@ describe('SortingSpecProcessor error detection and reporting', () => { `${ERR_PREFIX} 14:StandardObsidianSortAllowedOnlyAtFolderLevel The standard Obsidian sort order is only allowed at a folder level (not nested syntax) ${ERR_SUFFIX_IN_LINE(4)}`) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard')) }) + it('should recognize error: priority indicator alone', () => { + const inputTxtArr: Array = txtInputErrorPriorityAlone.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: priority indicator with empty file pattern', () => { + const inputTxtArr: Array = txtInputErrorPriorityEmptyFilePattern.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: priority indicator with empty folder pattern', () => { + const inputTxtArr: Array = txtInputErrorPriorityEmptyFolderPattern.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: priority indicator with empty pattern', () => { + const inputTxtArr: Array = txtInputErrorPriorityEmptyPattern.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 empty spec', () => { const inputTxtArr: Array = txtInputEmptySpec.split('\n') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index f4aa3f1..da1f85a 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -46,6 +46,7 @@ interface ParsedSortingGroup { arraySpec?: Array outsidersGroup?: boolean // Mutually exclusive with plainSpec and arraySpec itemToHide?: boolean + priority?: number } export enum ProblemCode { @@ -63,7 +64,8 @@ export enum ProblemCode { ItemToHideExactNameWithExtRequired, ItemToHideNoSupportForThreeDots, DuplicateWildcardSortSpecForSameFolder, - StandardObsidianSortAllowedOnlyAtFolderLevel + StandardObsidianSortAllowedOnlyAtFolderLevel, + PriorityNotAllowedOnOutsidersGroup } const ContextFreeProblems = new Set([ @@ -174,7 +176,8 @@ const FilesWithExtGroupVerboseLexeme: string = '/:files.' const FilesWithExtGroupShortLexeme: string = '/:.' const FoldersGroupVerboseLexeme: string = '/folders' const FoldersGroupShortLexeme: string = '/' -const AnyTypeGroupLexeme: string = '%' // See % as a combination of / and : +const AnyTypeGroupLexemeShort: string = '%' // See % as a combination of / and : +const AnyTypeGroupLexeme: string = '/%' // See % as a combination of / and : const HideItemShortLexeme: string = '--%' // See % as a combination of / and : const HideItemVerboseLexeme: string = '/--hide:' @@ -182,11 +185,26 @@ const MetadataFieldIndicatorLexeme: string = 'with-metadata:' const CommentPrefix: string = '//' +const PriorityModifierPrio1Lexeme: string = '/!' +const PriorityModifierPrio2Lexeme: string = '/!!' +const PriorityModifierPrio3Lexeme: string = '/!!!' + +const PRIO_1: number = 1 +const PRIO_2: number = 2 +const PRIO_3: number = 3 + +const SortingGroupPriorityPrefixes: { [key: string]: number } = { + [PriorityModifierPrio1Lexeme]: PRIO_1, + [PriorityModifierPrio2Lexeme]: PRIO_2, + [PriorityModifierPrio3Lexeme]: PRIO_3 +} + interface SortingGroupType { filesOnly?: boolean filenameWithExt?: boolean // The text matching criteria should apply to filename + extension foldersOnly?: boolean itemToHide?: boolean + priority?: number } const SortingGroupPrefixes: { [key: string]: SortingGroupType } = { @@ -196,6 +214,7 @@ const SortingGroupPrefixes: { [key: string]: SortingGroupType } = { [FilesWithExtGroupVerboseLexeme]: {filesOnly: true, filenameWithExt: true}, [FoldersGroupShortLexeme]: {foldersOnly: true}, [FoldersGroupVerboseLexeme]: {foldersOnly: true}, + [AnyTypeGroupLexemeShort]: {}, [AnyTypeGroupLexeme]: {}, [HideItemShortLexeme]: {itemToHide: true}, [HideItemVerboseLexeme]: {itemToHide: true} @@ -664,23 +683,43 @@ export class SortingSpecProcessor { } private parseSortingGroupSpec = (line: string): ParsedSortingGroup | null => { - const s: string = line.trim() + let s: string = line.trim() if (hasMoreThanOneNumericSortingSymbol(s)) { this.problem(ProblemCode.TooManyNumericSortingSymbols, 'Maximum one numeric sorting indicator allowed per line') 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 + for (const priorityPrefix of Object.keys(SortingGroupPriorityPrefixes)) { + if (s.startsWith(priorityPrefix + ' ')) { + groupPriority = SortingGroupPriorityPrefixes[priorityPrefix] + 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') return null } else { // !prefixAlone.itemToHide - return { - outsidersGroup: true, - filesOnly: prefixAlone.filesOnly, - foldersOnly: prefixAlone.foldersOnly + if (groupPriority) { + this.problem(ProblemCode.PriorityNotAllowedOnOutsidersGroup, 'Priority is not allowed for sorting group with empty match-pattern') + return null + } else { + return { + outsidersGroup: true, + filesOnly: prefixAlone.filesOnly, + foldersOnly: prefixAlone.foldersOnly + } } } } @@ -700,12 +739,26 @@ export class SortingSpecProcessor { plainSpec: s.substring(prefix.length + 1), filesOnly: sortingGroupType.filesOnly, foldersOnly: sortingGroupType.foldersOnly, - matchFilenameWithExt: sortingGroupType.filenameWithExt + matchFilenameWithExt: sortingGroupType.filenameWithExt, + priority: groupPriority ?? undefined } } } } + 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 + return { + plainSpec: s, + priority: groupPriority + } + } + } return null; } @@ -731,8 +784,14 @@ export class SortingSpecProcessor { if (newGroup) { if (this.adjustSortingGroupForNumericSortingSymbol(newGroup)) { if (this.ctx.currentSpec) { - this.ctx.currentSpec.groups.push(newGroup) + const groupIdx = this.ctx.currentSpec.groups.push(newGroup) - 1 this.ctx.currentSpecGroup = newGroup + // Consume group with priority + if (group.priority && group.priority > 0) { + newGroup.priority = group.priority + this.addExpediteGroupInfo(this.ctx.currentSpec, group.priority, groupIdx) + } + return true; } else { return false @@ -794,7 +853,7 @@ export class SortingSpecProcessor { }) } - // Populate sorting order for a bit more efficient sorting later on + // Populate sorting order down the hierarchy for more clean sorting logic later on for (let group of spec.groups) { if (!group.order) { group.order = spec.defaultOrder ?? DEFAULT_SORT_ORDER @@ -802,6 +861,18 @@ export class SortingSpecProcessor { } } + // If any priority sorting group was present in the spec, determine the groups evaluation order + if (spec.priorityOrder) { + // priorityOrder array already contains at least one priority group, so append all non-priority groups for the final order + // (Outsiders groups are ignored intentionally) + for (let idx=0; idx < spec.groups.length; idx++) { + const group: CustomSortGroup = spec.groups[idx] + if (group.priority === undefined && group.type !== CustomSortGroupType.Outsiders) { + spec.priorityOrder.push(idx) + } + } + } + const CURRENT_FOLDER_PREFIX: string = `${CURRENT_FOLDER_SYMBOL}/` // Replace the dot-folder names (coming from: 'target-folder: .') with actual folder names @@ -1108,4 +1179,21 @@ export class SortingSpecProcessor { } return true } + + private addExpediteGroupInfo = (spec: CustomSortSpec, groupPriority: number, groupIdx: number) => { + if (!spec.priorityOrder) { + spec.priorityOrder = [] + } + let inserted: boolean = false + for (let idx=0; idx spec.groups[spec.priorityOrder[idx]].priority!) { + spec.priorityOrder.splice(idx, 0, groupIdx) + inserted = true + break + } + } + if (!inserted) { + spec.priorityOrder.push(groupIdx) + } + } }