- 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)
This commit is contained in:
parent
1300caf291
commit
581f5e9f36
|
@ -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:
|
||||
|
||||

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

|
||||
|
||||
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'
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="-.5 -.5 314.5 187" width="314.5" height="187">
|
||||
<defs/>
|
||||
<metadata> Produced by OmniGraffle 7.20\n2022-08-06 14:27:13 +0000</metadata>
|
||||
<g id="Roman_sections" stroke-opacity="1" fill-opacity="1" stroke="none" fill="none" stroke-dasharray="none">
|
||||
<title>Roman sections</title>
|
||||
<g id="Roman_sections_Layer_1">
|
||||
<title>Layer 1</title>
|
||||
<g id="Graphic_8">
|
||||
<rect x="0" y="27" width="314" height="159.5" fill="#e9e9e9"/>
|
||||
</g>
|
||||
<g id="Graphic_2">
|
||||
<text transform="translate(18 29.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Some folder</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_4">
|
||||
<text transform="translate(35 51.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Alpha</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_5">
|
||||
<text transform="translate(35 73.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Archive April</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_6">
|
||||
<text transform="translate(35 95.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Archive Mai</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_7">
|
||||
<text transform="translate(35 117.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Archive March</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_9">
|
||||
<rect x="0" y="0" width="175.5" height="27" fill="white"/>
|
||||
<rect x="0" y="0" width="175.5" height="27" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
|
||||
<text transform="translate(5 2.753441)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-weight="bold" font-size="17" fill="black" x="0" y="17">My Vault</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_27">
|
||||
<text transform="translate(35 139.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Beta</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_30">
|
||||
<path d="M 4.5 35.56818 L 7 40.56818 L 9.5 35.56818 Z" fill="black"/>
|
||||
</g>
|
||||
<g id="Graphic_32">
|
||||
<text transform="translate(35 161.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Gamma</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="-.5 -.5 314.5 187" width="314.5" height="187">
|
||||
<defs/>
|
||||
<metadata> Produced by OmniGraffle 7.20\n2022-08-06 14:27:13 +0000</metadata>
|
||||
<g id="Roman_sections" stroke-opacity="1" fill-opacity="1" stroke="none" fill="none" stroke-dasharray="none">
|
||||
<title>Roman sections</title>
|
||||
<g id="Roman_sections_Layer_1">
|
||||
<title>Layer 1</title>
|
||||
<g id="Graphic_8">
|
||||
<rect x="0" y="27" width="314" height="159.5" fill="#e9e9e9"/>
|
||||
</g>
|
||||
<g id="Graphic_2">
|
||||
<text transform="translate(18 29.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Some folder</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_4">
|
||||
<text transform="translate(35 51.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Alpha</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_5">
|
||||
<text transform="translate(35 73.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Beta</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_6">
|
||||
<text transform="translate(35 95.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Gamma</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_7">
|
||||
<text transform="translate(35 117.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Archive April</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_9">
|
||||
<rect x="0" y="0" width="175.5" height="27" fill="white"/>
|
||||
<rect x="0" y="0" width="175.5" height="27" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/>
|
||||
<text transform="translate(5 2.753441)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-weight="bold" font-size="17" fill="black" x="0" y="17">My Vault</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_27">
|
||||
<text transform="translate(35 139.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Archive Mai</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="Graphic_30">
|
||||
<path d="M 4.5 35.56818 L 7 40.56818 L 9.5 35.56818 Z" fill="black"/>
|
||||
</g>
|
||||
<g id="Graphic_32">
|
||||
<text transform="translate(35 161.804)" fill="black">
|
||||
<tspan font-family="Helvetica Neue" font-size="14" fill="black" x="0" y="13">Archive March</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -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<string>
|
||||
plugin?: Plugin // to hand over the access to App instance to the sorting engine
|
||||
priorityOrder?: Array<number> // 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
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<string> = 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<string> = 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<string> = 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<string> = 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<string> = 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<string> = 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<string> = txtInputEmptySpec.split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
|
|
|
@ -46,6 +46,7 @@ interface ParsedSortingGroup {
|
|||
arraySpec?: Array<string>
|
||||
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<ProblemCode>([
|
||||
|
@ -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.priorityOrder.length; idx++) {
|
||||
if (groupPriority > spec.groups[spec.priorityOrder[idx]].priority!) {
|
||||
spec.priorityOrder.splice(idx, 0, groupIdx)
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
spec.priorityOrder.push(groupIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue