* #23 - support for sorting by metadata - added support for grouping items by the presence of specified metadata - new keyword `with-metadata:` introduced for that purpose in lexer - if metadata field name is omitted, the default `sort-index-value` is used - added support for sorting items by notes and folders metadata - new keyword 'by-metadata:' introduced for that purpose - if metadata field name is omitted, the default `sort-index-value` is used (or metadata name inheritance is used) - unit tests of sorting spec processor extended accordingly - documentation and code example in README.md - extended to also support true alphabetical on metadata fields - release unnecessary references after sorting completed
This commit is contained in:
parent
9541202b40
commit
fabd586348
84
README.md
84
README.md
|
@ -7,6 +7,7 @@ Take full control of the order of your notes and folders:
|
||||||
- support for fully manual order
|
- support for fully manual order
|
||||||
- list notes and folders names explicitly, or use prefixes or suffixes only
|
- list notes and folders names explicitly, or use prefixes or suffixes only
|
||||||
- wildcard names matching supported
|
- wildcard names matching supported
|
||||||
|
- group and sort notes and folders by notes custom metadata
|
||||||
- support for automatic sorting by standard and non-standard rules
|
- support for automatic sorting by standard and non-standard rules
|
||||||
- mixing manual and automatic ordering also supported
|
- mixing manual and automatic ordering also supported
|
||||||
- order by compound numbers in prefix, in suffix (e.g date in suffix) or inbetween
|
- order by compound numbers in prefix, in suffix (e.g date in suffix) or inbetween
|
||||||
|
@ -15,7 +16,7 @@ Take full control of the order of your notes and folders:
|
||||||
- different sorting rules per group even inside the same folder
|
- different sorting rules per group even inside the same folder
|
||||||
- simple to use yet versatile configuration options
|
- simple to use yet versatile configuration options
|
||||||
- order configuration stored directly in your note(s) front matter
|
- order configuration stored directly in your note(s) front matter
|
||||||
- use a dedicated key in YAML
|
- use a dedicated `sorting-spec:` key in YAML
|
||||||
- folders not set up for the custom order remain on the standard Obsidian sorting
|
- folders not set up for the custom order remain on the standard Obsidian sorting
|
||||||
- support for imposing inheritance of order specifications with flexible exclusion and overriding logic
|
- support for imposing inheritance of order specifications with flexible exclusion and overriding logic
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ Take full control of the order of your notes and folders:
|
||||||
- [Example 11: Sample book structure with compound Roman number suffixes](#example-11-sample-book-structure-with-compound-roman-number-suffixes)
|
- [Example 11: Sample book structure with compound Roman number suffixes](#example-11-sample-book-structure-with-compound-roman-number-suffixes)
|
||||||
- [Example 12: Apply same sorting to all folders in the vault](#example-12-apply-same-sorting-to-all-folders-in-the-vault)
|
- [Example 12: Apply same sorting to all folders in the vault](#example-12-apply-same-sorting-to-all-folders-in-the-vault)
|
||||||
- [Example 13: Sorting rules inheritance by subfolders](#example-13-sorting-rules-inheritance-by-subfolders)
|
- [Example 13: Sorting rules inheritance by subfolders](#example-13-sorting-rules-inheritance-by-subfolders)
|
||||||
|
- [Example 14: Grouping and sorting by metadata value](#example-14-grouping-and-sorting-by-metadata-value)
|
||||||
- [Alphabetical, Natural and True Alphabetical sorting orders](#alphabetical-natural-and-true-alphabetical-sorting-orders)
|
- [Alphabetical, Natural and True Alphabetical sorting orders](#alphabetical-natural-and-true-alphabetical-sorting-orders)
|
||||||
- [Location of sorting specification YAML entry](#location-of-sorting-specification-yaml-entry)
|
- [Location of sorting specification YAML entry](#location-of-sorting-specification-yaml-entry)
|
||||||
- [Ribbon icon](#ribbon-icon)
|
- [Ribbon icon](#ribbon-icon)
|
||||||
|
@ -412,6 +414,86 @@ sorting-spec: |
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Example 14: Grouping and sorting by metadata value
|
||||||
|
|
||||||
|
Notes can contain metadata, let me use the example inspired by the [Feature Request #23](https://github.com/SebastianMC/obsidian-custom-sort/issues/23).
|
||||||
|
Namely, someone can create notes when reading a book and use the `Pages` metadata field. In that field s/he enters page(s) number(s) of the book, for reference.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
Pages: 6
|
||||||
|
...
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
Pages: 7,8
|
||||||
|
...
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
Pages: 12-15
|
||||||
|
...
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Using this plugin you can sort notes by the value of the specific metadata, for example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
sorting-spec: |
|
||||||
|
target-folder: Remarks from 'The Little Prince' book
|
||||||
|
< a-z by-metadata: Pages
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
In that approach, the notes containing the metadata `Pages` will go first, sorted alphabetically by the value of that metadata.
|
||||||
|
The remaining notes (not having the metadata) will go below, sorted alphabetically by default.
|
||||||
|
|
||||||
|
In the above example the syntax `by-metadata: Pages` was used to tell the plugin about the metadata field name for sorting.
|
||||||
|
The specified sorting `< a-z` is obviously alphabetical, and in this specific context it tells to sort by the value of the specified metadata (and not by the note or folder name).
|
||||||
|
|
||||||
|
In a more advanced fine-tuned approach you can explicitly group notes having some metadata and sort by that (or other) metadata:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
sorting-spec: |
|
||||||
|
target-folder: Remarks from 'The Little Prince' book
|
||||||
|
with-metadata: Pages
|
||||||
|
< a-z by-metadata: Pages
|
||||||
|
...
|
||||||
|
> modified
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example the syntax `with-metadata: Pages` was used to tell the plugin about the metadata field name for grouping.
|
||||||
|
The specified sorting `< a-z` is obviously alphabetical, and in this specific context it tells to sort by the value of the specified metadata (and not by the note or folder name).
|
||||||
|
Then the remaining notes (not having the `Pages` metadata) are sorted by modification date descending.
|
||||||
|
|
||||||
|
> NOTE
|
||||||
|
>
|
||||||
|
> The grouping and sorting by metadata is not refreshed automatically after change of the metadata in note(s) to avoid impact on Obsidian performance.
|
||||||
|
> After editing of metadata of some note(s) you have to explicitly click the plugin ribbon button to refresh the sorting. Or issue the command `sort on`. Or close and reopen the vault. Or restart Obsidian.
|
||||||
|
> This behavior is intentionally different from other grouping and sorting rules, which stay active and up-to-date once enabled.
|
||||||
|
|
||||||
|
> NOTE
|
||||||
|
>
|
||||||
|
> For folders, metadata of their 'folder note' is scanned (if present)
|
||||||
|
|
||||||
|
> NOTE
|
||||||
|
>
|
||||||
|
> The `with-metadata:` keyword can be used with other specifiers like `/:files with-metadata: Pages` or `/folders with-metadata: Pages`
|
||||||
|
> If the metadata name is omitted, the default `sort-index-value` metadata name is assumed.
|
||||||
|
|
||||||
## Alphabetical, Natural and True Alphabetical sorting orders
|
## Alphabetical, Natural and True Alphabetical sorting orders
|
||||||
|
|
||||||
The 'A-Z' sorting (visible in Obsidian UI of file explorer) at some point before the 1.0.0 release of Obsidian actually became the so-called 'natural' sort order.
|
The 'A-Z' sorting (visible in Obsidian UI of file explorer) at some point before the 1.0.0 release of Obsidian actually became the so-called 'natural' sort order.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {MetadataCache, Plugin} from "obsidian";
|
||||||
|
|
||||||
export enum CustomSortGroupType {
|
export enum CustomSortGroupType {
|
||||||
Outsiders, // Not belonging to any of other groups
|
Outsiders, // Not belonging to any of other groups
|
||||||
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
|
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
|
||||||
|
@ -5,6 +7,7 @@ export enum CustomSortGroupType {
|
||||||
ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups
|
ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups
|
||||||
ExactSuffix,
|
ExactSuffix,
|
||||||
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
|
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
|
||||||
|
HasMetadataField // Notes (or folder's notes) containing a specific metadata field
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CustomSortOrder {
|
export enum CustomSortOrder {
|
||||||
|
@ -20,6 +23,10 @@ export enum CustomSortOrder {
|
||||||
byCreatedTimeAdvanced,
|
byCreatedTimeAdvanced,
|
||||||
byCreatedTimeReverse,
|
byCreatedTimeReverse,
|
||||||
byCreatedTimeReverseAdvanced,
|
byCreatedTimeReverseAdvanced,
|
||||||
|
byMetadataFieldAlphabetical,
|
||||||
|
byMetadataFieldTrueAlphabetical,
|
||||||
|
byMetadataFieldAlphabeticalReverse,
|
||||||
|
byMetadataFieldTrueAlphabeticalReverse,
|
||||||
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
|
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
|
||||||
default = alphabetical
|
default = alphabetical
|
||||||
}
|
}
|
||||||
|
@ -27,6 +34,7 @@ export enum CustomSortOrder {
|
||||||
export interface RecognizedOrderValue {
|
export interface RecognizedOrderValue {
|
||||||
order: CustomSortOrder
|
order: CustomSortOrder
|
||||||
secondaryOrder?: CustomSortOrder
|
secondaryOrder?: CustomSortOrder
|
||||||
|
applyToMetadataField?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NormalizerFn = (s: string) => string | null
|
export type NormalizerFn = (s: string) => string | null
|
||||||
|
@ -43,18 +51,27 @@ 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
|
||||||
secondaryOrder?: CustomSortOrder
|
secondaryOrder?: CustomSortOrder
|
||||||
filesOnly?: boolean
|
filesOnly?: boolean
|
||||||
matchFilenameWithExt?: boolean
|
matchFilenameWithExt?: boolean
|
||||||
foldersOnly?: boolean,
|
foldersOnly?: boolean
|
||||||
|
withMetadataFieldName?: string // for 'with-metadata:'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomSortSpec {
|
export interface CustomSortSpec {
|
||||||
targetFoldersPaths: Array<string> // For root use '/'
|
targetFoldersPaths: Array<string> // For root use '/'
|
||||||
defaultOrder?: CustomSortOrder
|
defaultOrder?: CustomSortOrder
|
||||||
|
byMetadataField?: string // for 'by-metadata:' if the defaultOrder is by metadata alphabetical or reverse
|
||||||
groups: Array<CustomSortGroup>
|
groups: Array<CustomSortGroup>
|
||||||
outsidersGroupIdx?: number
|
outsidersGroupIdx?: number
|
||||||
outsidersFilesGroupIdx?: number
|
outsidersFilesGroupIdx?: number
|
||||||
outsidersFoldersGroupIdx?: number
|
outsidersFoldersGroupIdx?: number
|
||||||
itemsToHide?: Set<string>
|
itemsToHide?: Set<string>
|
||||||
|
plugin?: Plugin // to hand over the access to App instance to the sorting engine
|
||||||
|
|
||||||
|
// For internal transient use
|
||||||
|
_mCache?: MetadataCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value'
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import {TFile, TFolder, Vault} from 'obsidian';
|
import {CachedMetadata, MetadataCache, Pos, TFile, TFolder, Vault} from 'obsidian';
|
||||||
import {
|
import {
|
||||||
DEFAULT_FOLDER_CTIME,
|
DEFAULT_FOLDER_CTIME,
|
||||||
|
DEFAULT_FOLDER_MTIME,
|
||||||
determineFolderDatesIfNeeded,
|
determineFolderDatesIfNeeded,
|
||||||
determineSortingGroup,
|
determineSortingGroup,
|
||||||
FolderItemForSorting
|
FolderItemForSorting,
|
||||||
|
SorterFn,
|
||||||
|
Sorters
|
||||||
} from './custom-sort';
|
} from './custom-sort';
|
||||||
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from './custom-sort-types';
|
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from './custom-sort-types';
|
||||||
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
|
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
|
||||||
|
@ -28,7 +31,7 @@ const mockTFolder = (name: string, children?: Array<TFolder|TFile>, parent?: TFo
|
||||||
return {
|
return {
|
||||||
isRoot(): boolean { return name === '/' },
|
isRoot(): boolean { return name === '/' },
|
||||||
vault: {} as Vault, // To satisfy TS typechecking
|
vault: {} as Vault, // To satisfy TS typechecking
|
||||||
path: `/${name}`,
|
path: `${name}`,
|
||||||
name: name,
|
name: name,
|
||||||
parent: parent ?? ({} as TFolder), // To satisfy TS typechecking
|
parent: parent ?? ({} as TFolder), // To satisfy TS typechecking
|
||||||
children: children ?? []
|
children: children ?? []
|
||||||
|
@ -50,6 +53,11 @@ const mockTFolderWithChildren = (name: string): TFolder => {
|
||||||
return mockTFolder('Mock parent folder', [child1, child2, child3, child4, child5])
|
return mockTFolder('Mock parent folder', [child1, child2, child3, child4, child5])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MockedLoc: Pos = {
|
||||||
|
start: {col:0,offset:0,line:0},
|
||||||
|
end: {col:0,offset:0,line:0}
|
||||||
|
}
|
||||||
|
|
||||||
describe('determineSortingGroup', () => {
|
describe('determineSortingGroup', () => {
|
||||||
describe('CustomSortGroupType.ExactHeadAndTail', () => {
|
describe('CustomSortGroupType.ExactHeadAndTail', () => {
|
||||||
it('should correctly recognize head and tail', () => {
|
it('should correctly recognize head and tail', () => {
|
||||||
|
@ -288,7 +296,6 @@ describe('determineSortingGroup', () => {
|
||||||
exactPrefix: 'Pref'
|
exactPrefix: 'Pref'
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const result = determineSortingGroup(file, sortSpec)
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
@ -304,6 +311,483 @@ describe('determineSortingGroup', () => {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe('CustomSortGroupType.byMetadataFieldAlphabetical', () => {
|
||||||
|
it('should ignore the file item if it has no direct metadata', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
|
withMetadataFieldName: "metadataField1",
|
||||||
|
exactPrefix: 'Ref'
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
"References": {
|
||||||
|
frontmatter: {
|
||||||
|
metadataField1InvalidField: "directMetadataOnFile",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 1, // The lastIdx+1, group not determined
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
it('should ignore the folder item if it has no metadata on folder note', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
|
withMetadataFieldName: "metadataField1",
|
||||||
|
exactPrefix: 'Ref'
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
"References": {
|
||||||
|
frontmatter: {
|
||||||
|
metadataField1: undefined,
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 1, // lastIdx + 1, group not determined
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
it('should correctly include the File item if has direct metadata (group not sorted by metadata', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
|
withMetadataFieldName: "metadataField1",
|
||||||
|
exactPrefix: 'Ref'
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'Some parent folder/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
"metadataField1": "directMetadataOnFile",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md'
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
it('should correctly include the Folder item if it has folder note metadata (group not sorted by metadata', () => {
|
||||||
|
// given
|
||||||
|
const folder: TFolder = mockTFolder('References');
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
|
withMetadataFieldName: "metadataField1",
|
||||||
|
exactPrefix: 'Ref'
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'References/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
"metadataField1": "directMetadataOnFile",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(folder, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: true,
|
||||||
|
sortString: "References",
|
||||||
|
ctimeNewest: DEFAULT_FOLDER_CTIME,
|
||||||
|
ctimeOldest: DEFAULT_FOLDER_CTIME,
|
||||||
|
mtime: DEFAULT_FOLDER_MTIME,
|
||||||
|
path: 'References',
|
||||||
|
folder: folder
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('when sort by metadata is involved', () => {
|
||||||
|
it('should correctly read direct metadata from File item (order by metadata set on group) alph', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.ExactPrefix,
|
||||||
|
byMetadataField: 'metadata-field-for-sorting',
|
||||||
|
exactPrefix: 'Ref',
|
||||||
|
order: CustomSortOrder.byMetadataFieldAlphabetical
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'Some parent folder/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
"metadata-field-for-sorting": "direct metadata on file",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md',
|
||||||
|
metadataFieldValue: 'direct metadata on file'
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
it('should correctly read direct metadata from File item (order by metadata set on group) alph rev', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.ExactPrefix,
|
||||||
|
byMetadataField: 'metadata-field-for-sorting',
|
||||||
|
exactPrefix: 'Ref',
|
||||||
|
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'Some parent folder/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
"metadata-field-for-sorting": "direct metadata on file",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md',
|
||||||
|
metadataFieldValue: 'direct metadata on file'
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
it('should correctly read direct metadata from File item (order by metadata set on group) true alph', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.ExactPrefix,
|
||||||
|
byMetadataField: 'metadata-field-for-sorting',
|
||||||
|
exactPrefix: 'Ref',
|
||||||
|
order: CustomSortOrder.byMetadataFieldTrueAlphabetical
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'Some parent folder/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
"metadata-field-for-sorting": "direct metadata on file",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md',
|
||||||
|
metadataFieldValue: 'direct metadata on file'
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
it('should correctly read direct metadata from File item (order by metadata set on group) true alph rev', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.ExactPrefix,
|
||||||
|
byMetadataField: 'metadata-field-for-sorting',
|
||||||
|
exactPrefix: 'Ref',
|
||||||
|
order: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'Some parent folder/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
"metadata-field-for-sorting": "direct metadata on file",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md',
|
||||||
|
metadataFieldValue: 'direct metadata on file'
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
it('should correctly read direct metadata from folder note item (order by metadata set on group)', () => {
|
||||||
|
// given
|
||||||
|
const folder: TFolder = mockTFolder('References');
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.ExactPrefix,
|
||||||
|
exactPrefix: 'Ref',
|
||||||
|
byMetadataField: 'metadata-field-for-sorting',
|
||||||
|
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'References/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
'metadata-field-for-sorting': "metadata on folder note",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(folder, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: true,
|
||||||
|
sortString: "References",
|
||||||
|
ctimeNewest: DEFAULT_FOLDER_CTIME,
|
||||||
|
ctimeOldest: DEFAULT_FOLDER_CTIME,
|
||||||
|
mtime: DEFAULT_FOLDER_MTIME,
|
||||||
|
path: 'References',
|
||||||
|
metadataFieldValue: 'metadata on folder note',
|
||||||
|
folder: folder
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
it('should correctly read direct metadata from File item (order by metadata set on target folder)', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.ExactPrefix,
|
||||||
|
exactPrefix: 'Ref',
|
||||||
|
order: CustomSortOrder.byMetadataFieldAlphabetical
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'Some parent folder/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
"metadata-field-for-sorting-specified-on-target-folder": "direct metadata on file, not obvious",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache,
|
||||||
|
defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
|
||||||
|
byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder',
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md',
|
||||||
|
metadataFieldValue: 'direct metadata on file, not obvious'
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified on group)', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
|
order: CustomSortOrder.byMetadataFieldAlphabetical,
|
||||||
|
withMetadataFieldName: 'field-used-with-with-metadata-syntax'
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'Some parent folder/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
'field-used-with-with-metadata-syntax': "direct metadata on file, tricky",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md',
|
||||||
|
metadataFieldValue: 'direct metadata on file, tricky'
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
it('should correctly read direct metadata from File item (order by metadata set on group, no metadata name specified anywhere)', () => {
|
||||||
|
// given
|
||||||
|
const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333);
|
||||||
|
const sortSpec: CustomSortSpec = {
|
||||||
|
targetFoldersPaths: ['/'],
|
||||||
|
groups: [{
|
||||||
|
type: CustomSortGroupType.ExactPrefix,
|
||||||
|
exactPrefix: 'Ref',
|
||||||
|
order: CustomSortOrder.byMetadataFieldAlphabetical
|
||||||
|
}],
|
||||||
|
_mCache: {
|
||||||
|
getCache: function (path: string): CachedMetadata | undefined {
|
||||||
|
return {
|
||||||
|
'Some parent folder/References.md': {
|
||||||
|
frontmatter: {
|
||||||
|
'sort-index-value': "direct metadata on file, under default name",
|
||||||
|
position: MockedLoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[path]
|
||||||
|
}
|
||||||
|
} as MetadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = determineSortingGroup(file, sortSpec)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
groupIdx: 0,
|
||||||
|
isFolder: false,
|
||||||
|
sortString: "References.md",
|
||||||
|
ctimeNewest: MOCK_TIMESTAMP + 222,
|
||||||
|
ctimeOldest: MOCK_TIMESTAMP + 222,
|
||||||
|
mtime: MOCK_TIMESTAMP + 333,
|
||||||
|
path: 'Some parent folder/References.md',
|
||||||
|
metadataFieldValue: 'direct metadata on file, under default name'
|
||||||
|
} as FolderItemForSorting);
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('determineFolderDatesIfNeeded', () => {
|
describe('determineFolderDatesIfNeeded', () => {
|
||||||
|
@ -377,3 +861,171 @@ describe('determineFolderDatesIfNeeded', () => {
|
||||||
expect(result.mtime).toEqual(TIMESTAMP_NEWEST)
|
expect(result.mtime).toEqual(TIMESTAMP_NEWEST)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const SORT_FIRST_GOES_EARLIER: number = -1
|
||||||
|
const SORT_FIRST_GOES_LATER: number = 1
|
||||||
|
const SORT_ITEMS_ARE_EQUAL: number = 0
|
||||||
|
|
||||||
|
describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
|
||||||
|
it('should correctly order alphabetically when metadata on both items is present', () => {
|
||||||
|
// given
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'A'
|
||||||
|
}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'B'
|
||||||
|
}
|
||||||
|
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result1).toBe(SORT_FIRST_GOES_EARLIER)
|
||||||
|
expect(result2).toBe(SORT_FIRST_GOES_LATER)
|
||||||
|
})
|
||||||
|
it('should correctly fallback to alphabetical by name when metadata on both items is present and equal', () => {
|
||||||
|
// given
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'Aaa',
|
||||||
|
sortString: 'n123'
|
||||||
|
}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'Aaa',
|
||||||
|
sortString: 'a123'
|
||||||
|
}
|
||||||
|
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||||
|
const result3: number = sorter(itemB as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result1).toBe(SORT_FIRST_GOES_LATER)
|
||||||
|
expect(result2).toBe(SORT_FIRST_GOES_EARLIER)
|
||||||
|
expect(result3).toBe(SORT_ITEMS_ARE_EQUAL)
|
||||||
|
})
|
||||||
|
it('should put the item with metadata earlier if the second one has no metadata ', () => {
|
||||||
|
// given
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'n159',
|
||||||
|
sortString: 'n123'
|
||||||
|
}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {
|
||||||
|
sortString: 'n123'
|
||||||
|
}
|
||||||
|
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result1).toBe(SORT_FIRST_GOES_EARLIER)
|
||||||
|
expect(result2).toBe(SORT_FIRST_GOES_LATER)
|
||||||
|
})
|
||||||
|
it('should correctly fallback to alphabetical if no metadata on both items', () => {
|
||||||
|
// given
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {
|
||||||
|
sortString: 'ccc'
|
||||||
|
}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {
|
||||||
|
sortString: 'ccc '
|
||||||
|
}
|
||||||
|
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||||
|
const result3: number = sorter(itemB as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result1).toBe(SORT_FIRST_GOES_EARLIER)
|
||||||
|
expect(result2).toBe(SORT_FIRST_GOES_LATER)
|
||||||
|
expect(result3).toBe(SORT_ITEMS_ARE_EQUAL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
|
||||||
|
it('should correctly order alphabetically reverse when metadata on both items is present', () => {
|
||||||
|
// given
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'A'
|
||||||
|
}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'B'
|
||||||
|
}
|
||||||
|
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result1).toBe(SORT_FIRST_GOES_LATER)
|
||||||
|
expect(result2).toBe(SORT_FIRST_GOES_EARLIER)
|
||||||
|
})
|
||||||
|
it('should correctly fallback to alphabetical reverse by name when metadata on both items is present and equal', () => {
|
||||||
|
// given
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'Aaa',
|
||||||
|
sortString: 'n123'
|
||||||
|
}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: 'Aaa',
|
||||||
|
sortString: 'a123'
|
||||||
|
}
|
||||||
|
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||||
|
const result3: number = sorter(itemB as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result1).toBe(SORT_FIRST_GOES_EARLIER)
|
||||||
|
expect(result2).toBe(SORT_FIRST_GOES_LATER)
|
||||||
|
expect(result3).toBe(SORT_ITEMS_ARE_EQUAL)
|
||||||
|
})
|
||||||
|
it('should put the item with metadata earlier if the second one has no metadata ', () => {
|
||||||
|
// given
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {
|
||||||
|
metadataFieldValue: '15',
|
||||||
|
sortString: 'n123'
|
||||||
|
}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {
|
||||||
|
sortString: 'n123'
|
||||||
|
}
|
||||||
|
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result1).toBe(SORT_FIRST_GOES_EARLIER)
|
||||||
|
expect(result2).toBe(SORT_FIRST_GOES_LATER)
|
||||||
|
})
|
||||||
|
it('should correctly fallback to alphabetical reverse if no metadata on both items', () => {
|
||||||
|
// given
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {
|
||||||
|
sortString: 'ccc'
|
||||||
|
}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {
|
||||||
|
sortString: 'ccc '
|
||||||
|
}
|
||||||
|
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||||
|
const result3: number = sorter(itemB as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result1).toBe(SORT_FIRST_GOES_LATER)
|
||||||
|
expect(result2).toBe(SORT_FIRST_GOES_EARLIER)
|
||||||
|
expect(result3).toBe(SORT_ITEMS_ARE_EQUAL)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import {requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
|
import {FrontMatterCache, MetadataCache, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
|
||||||
import {CustomSortGroup, CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
import {
|
||||||
|
CustomSortGroup,
|
||||||
|
CustomSortGroupType,
|
||||||
|
CustomSortOrder,
|
||||||
|
CustomSortSpec,
|
||||||
|
DEFAULT_METADATA_FIELD_FOR_SORTING
|
||||||
|
} from "./custom-sort-types";
|
||||||
import {isDefined} from "../utils/utils";
|
import {isDefined} from "../utils/utils";
|
||||||
|
|
||||||
let Collator = new Intl.Collator(undefined, {
|
let CollatorCompare = new Intl.Collator(undefined, {
|
||||||
usage: "sort",
|
usage: "sort",
|
||||||
sensitivity: "base",
|
sensitivity: "base",
|
||||||
numeric: true,
|
numeric: true,
|
||||||
}).compare;
|
}).compare;
|
||||||
|
|
||||||
let CollatorTrueAlphabetical = new Intl.Collator(undefined, {
|
let CollatorTrueAlphabeticalCompare = new Intl.Collator(undefined, {
|
||||||
usage: "sort",
|
usage: "sort",
|
||||||
sensitivity: "base",
|
sensitivity: "base",
|
||||||
numeric: false,
|
numeric: false,
|
||||||
|
@ -18,6 +24,7 @@ export interface FolderItemForSorting {
|
||||||
path: string
|
path: string
|
||||||
groupIdx?: number // the index itself represents order for groups
|
groupIdx?: number // the index itself represents order for groups
|
||||||
sortString: string // fragment (or full name) to be used for sorting
|
sortString: string // fragment (or full name) to be used for sorting
|
||||||
|
metadataFieldValue?: string // relevant to metadata-based sorting only
|
||||||
matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match'
|
matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match'
|
||||||
ctimeOldest: number // for a file, both ctime values are the same. For folder they can be different:
|
ctimeOldest: number // for a file, both ctime values are the same. For folder they can be different:
|
||||||
ctimeNewest: number // ctimeOldest = ctime of oldest child file, ctimeNewest = ctime of newest child file
|
ctimeNewest: number // ctimeOldest = ctime of oldest child file, ctimeNewest = ctime of newest child file
|
||||||
|
@ -26,24 +33,57 @@ export interface FolderItemForSorting {
|
||||||
folder?: TFolder
|
folder?: TFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
||||||
|
export type CollatorCompareFn = (a: string, b: string) => number
|
||||||
|
|
||||||
let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
// Syntax sugar
|
||||||
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
|
const TrueAlphabetical: boolean = true
|
||||||
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabetical(a.sortString, b.sortString),
|
const ReverseOrder: boolean = true
|
||||||
[CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(b.sortString, a.sortString),
|
const StraightOrder: boolean = false
|
||||||
[CustomSortOrder.trueAlphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabetical(b.sortString, a.sortString),
|
|
||||||
[CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (a.mtime - b.mtime),
|
const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
|
||||||
|
const collatorCompareFn: CollatorCompareFn = trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare
|
||||||
|
return (a: FolderItemForSorting, b: FolderItemForSorting) => {
|
||||||
|
if (reverseOrder) {
|
||||||
|
[a, b] = [b, a]
|
||||||
|
}
|
||||||
|
if (a.metadataFieldValue && b.metadataFieldValue) {
|
||||||
|
const sortResult: number = collatorCompareFn(a.metadataFieldValue, b.metadataFieldValue)
|
||||||
|
if (sortResult === 0) {
|
||||||
|
// Fallback -> requested sort by metadata and both items have the same metadata value
|
||||||
|
return collatorCompareFn(a.sortString, b.sortString) // switch to alphabetical sort by note/folder titles
|
||||||
|
} else {
|
||||||
|
return sortResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Item with metadata goes before the w/o metadata
|
||||||
|
if (a.metadataFieldValue) return reverseOrder ? 1 : -1
|
||||||
|
if (b.metadataFieldValue) return reverseOrder ? -1 : 1
|
||||||
|
// Fallback -> requested sort by metadata, yet none of two items contain it, use alphabetical by name
|
||||||
|
return collatorCompareFn(a.sortString, b.sortString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
|
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
||||||
|
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString),
|
||||||
|
[CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(b.sortString, a.sortString),
|
||||||
|
[CustomSortOrder.trueAlphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(b.sortString, a.sortString),
|
||||||
|
[CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? CollatorCompare(a.sortString, b.sortString) : (a.mtime - b.mtime),
|
||||||
[CustomSortOrder.byModifiedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime,
|
[CustomSortOrder.byModifiedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime,
|
||||||
[CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (b.mtime - a.mtime),
|
[CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? CollatorCompare(a.sortString, b.sortString) : (b.mtime - a.mtime),
|
||||||
[CustomSortOrder.byModifiedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime,
|
[CustomSortOrder.byModifiedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime,
|
||||||
[CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (a.ctimeNewest - b.ctimeNewest),
|
[CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? CollatorCompare(a.sortString, b.sortString) : (a.ctimeNewest - b.ctimeNewest),
|
||||||
[CustomSortOrder.byCreatedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctimeNewest - b.ctimeNewest,
|
[CustomSortOrder.byCreatedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctimeNewest - b.ctimeNewest,
|
||||||
[CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (b.ctimeOldest - a.ctimeOldest),
|
[CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? CollatorCompare(a.sortString, b.sortString) : (b.ctimeOldest - a.ctimeOldest),
|
||||||
[CustomSortOrder.byCreatedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctimeOldest - a.ctimeOldest,
|
[CustomSortOrder.byCreatedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctimeOldest - a.ctimeOldest,
|
||||||
|
[CustomSortOrder.byMetadataFieldAlphabetical]: sorterByMetadataField(StraightOrder),
|
||||||
|
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical),
|
||||||
|
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder),
|
||||||
|
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical),
|
||||||
|
|
||||||
// This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all
|
// This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all
|
||||||
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
|
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
||||||
};
|
};
|
||||||
|
|
||||||
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
|
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
|
||||||
|
@ -71,6 +111,11 @@ const isFolder = (entry: TAbstractFile) => {
|
||||||
return !!((entry as any).isRoot);
|
return !!((entry as any).isRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isByMetadata = (order: CustomSortOrder | undefined) => {
|
||||||
|
return order === CustomSortOrder.byMetadataFieldAlphabetical || order === CustomSortOrder.byMetadataFieldAlphabeticalReverse ||
|
||||||
|
order === CustomSortOrder.byMetadataFieldTrueAlphabetical || order === CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse
|
||||||
|
}
|
||||||
|
|
||||||
export const DEFAULT_FOLDER_MTIME: number = 0
|
export const DEFAULT_FOLDER_MTIME: number = 0
|
||||||
export const DEFAULT_FOLDER_CTIME: number = 0
|
export const DEFAULT_FOLDER_CTIME: number = 0
|
||||||
|
|
||||||
|
@ -78,6 +123,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
let groupIdx: number
|
let groupIdx: number
|
||||||
let determined: boolean = false
|
let determined: boolean = false
|
||||||
let matchedGroup: string | null | undefined
|
let matchedGroup: string | null | undefined
|
||||||
|
let metadataValueToSortBy: string | undefined
|
||||||
const aFolder: boolean = isFolder(entry)
|
const aFolder: boolean = isFolder(entry)
|
||||||
const aFile: boolean = !aFolder
|
const aFile: boolean = !aFolder
|
||||||
const entryAsTFile: TFile = entry as TFile
|
const entryAsTFile: TFile = entry as TFile
|
||||||
|
@ -152,10 +198,24 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
case CustomSortGroupType.HasMetadataField:
|
||||||
|
if (group.withMetadataFieldName) {
|
||||||
|
if (spec._mCache) {
|
||||||
|
// For folders - scan metadata of 'folder note'
|
||||||
|
const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
|
||||||
|
const frontMatterCache: FrontMatterCache | undefined = spec._mCache.getCache(notePathToScan)?.frontmatter
|
||||||
|
const hasMetadata: boolean | undefined = frontMatterCache?.hasOwnProperty(group.withMetadataFieldName)
|
||||||
|
|
||||||
|
if (hasMetadata) {
|
||||||
|
determined = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
case CustomSortGroupType.MatchAll:
|
case CustomSortGroupType.MatchAll:
|
||||||
determined = true;
|
determined = true;
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
if (determined) {
|
if (determined) {
|
||||||
break;
|
break;
|
||||||
|
@ -169,10 +229,50 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
// 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) {
|
||||||
determinedGroupIdx = spec.outsidersFilesGroupIdx;
|
determinedGroupIdx = spec.outsidersFilesGroupIdx;
|
||||||
|
determined = true
|
||||||
} else if (isDefined(spec.outsidersFoldersGroupIdx) && aFolder) {
|
} else if (isDefined(spec.outsidersFoldersGroupIdx) && aFolder) {
|
||||||
determinedGroupIdx = spec.outsidersFoldersGroupIdx;
|
determinedGroupIdx = spec.outsidersFoldersGroupIdx;
|
||||||
|
determined = true
|
||||||
} else if (isDefined(spec.outsidersGroupIdx)) {
|
} else if (isDefined(spec.outsidersGroupIdx)) {
|
||||||
determinedGroupIdx = spec.outsidersGroupIdx;
|
determinedGroupIdx = spec.outsidersGroupIdx;
|
||||||
|
determined = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The not obvious logic of determining the value of metadata field to use its value for sorting
|
||||||
|
// - the sorting spec processor automatically populates the order field of CustomSortingGroup for each group
|
||||||
|
// - yet defensive code should assume some default
|
||||||
|
// - if the order in group is by metadata (and only in that case):
|
||||||
|
// - if byMetadata field name is defined for the group -> use it. Done even if value empty or not present.
|
||||||
|
// - else, if byMetadata field name is defined for the Sorting spec (folder level, for all groups) -> use it. Done even if value empty or not present.
|
||||||
|
// - else, if withMetadata field name is defined for the group -> use it. Done even if value empty or not present.
|
||||||
|
// - otherwise, fallback to the default metadata field name (hardcoded in the plugin as 'sort-index-value')
|
||||||
|
|
||||||
|
// TODO: in manual of plugin, in details, explain these nuances. Let readme.md contain only the basic simple example and reference to manual.md section
|
||||||
|
|
||||||
|
if (determined && determinedGroupIdx !== undefined) { // <-- defensive code, maybe too defensive
|
||||||
|
const group: CustomSortGroup = spec.groups[determinedGroupIdx];
|
||||||
|
if (isByMetadata(group?.order)) {
|
||||||
|
let metadataFieldName: string | undefined = group.byMetadataField
|
||||||
|
if (!metadataFieldName) {
|
||||||
|
if (isByMetadata(spec.defaultOrder)) {
|
||||||
|
metadataFieldName = spec.byMetadataField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!metadataFieldName) {
|
||||||
|
metadataFieldName = group.withMetadataFieldName
|
||||||
|
}
|
||||||
|
if (!metadataFieldName) {
|
||||||
|
metadataFieldName = DEFAULT_METADATA_FIELD_FOR_SORTING
|
||||||
|
}
|
||||||
|
if (metadataFieldName) {
|
||||||
|
if (spec._mCache) {
|
||||||
|
// For folders - scan metadata of 'folder note'
|
||||||
|
const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
|
||||||
|
const frontMatterCache: FrontMatterCache | undefined = spec._mCache.getCache(notePathToScan)?.frontmatter
|
||||||
|
metadataValueToSortBy = frontMatterCache?.[metadataFieldName]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +280,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
// idx of the matched group or idx of Outsiders group or the largest index (= groups count+1)
|
// idx of the matched group or idx of Outsiders group or the largest index (= groups count+1)
|
||||||
groupIdx: determinedGroupIdx,
|
groupIdx: determinedGroupIdx,
|
||||||
sortString: matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name,
|
sortString: matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name,
|
||||||
|
metadataFieldValue: metadataValueToSortBy,
|
||||||
matchGroup: matchedGroup ?? undefined,
|
matchGroup: matchedGroup ?? undefined,
|
||||||
isFolder: aFolder,
|
isFolder: aFolder,
|
||||||
folder: aFolder ? (entry as TFolder) : undefined,
|
folder: aFolder ? (entry as TFolder) : undefined,
|
||||||
|
@ -247,6 +348,7 @@ export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSor
|
||||||
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
|
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
|
||||||
let fileExplorer = this.fileExplorer
|
let fileExplorer = this.fileExplorer
|
||||||
const sortingGroupsCardinality: {[key: number]: number} = {}
|
const sortingGroupsCardinality: {[key: number]: number} = {}
|
||||||
|
sortingSpec._mCache = sortingSpec.plugin?.app.metadataCache
|
||||||
|
|
||||||
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
|
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
|
||||||
this.file.children.filter((entry: TFile | TFolder) => {
|
this.file.children.filter((entry: TFile | TFolder) => {
|
||||||
|
@ -263,7 +365,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
|
||||||
return itemForSorting
|
return itemForSorting
|
||||||
})
|
})
|
||||||
|
|
||||||
// Finally, for advanced sorting by modified date, for some of the folders the modified date has to be determined
|
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined
|
||||||
determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality)
|
determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality)
|
||||||
|
|
||||||
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
|
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
|
||||||
|
@ -278,4 +380,8 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
|
||||||
} else {
|
} else {
|
||||||
this.children = items;
|
this.children = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// release risky references
|
||||||
|
sortingSpec._mCache = undefined
|
||||||
|
sortingSpec.plugin = undefined
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const DEFAULT_NORMALIZATION_PLACES = 8; // Fixed width of a normalized n
|
||||||
export function prependWithZeros(s: string, minLength: number) {
|
export function prependWithZeros(s: string, minLength: number) {
|
||||||
if (s.length < minLength) {
|
if (s.length < minLength) {
|
||||||
const delta: number = minLength - s.length;
|
const delta: number = minLength - s.length;
|
||||||
return '000000000000000000000000000'.substr(0, delta) + s;
|
return '000000000000000000000000000'.substring(0, delta) + s;
|
||||||
} else {
|
} else {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,12 @@ target-folder: tricky folder
|
||||||
/
|
/
|
||||||
/:
|
/:
|
||||||
|
|
||||||
|
:::: tricky folder 2
|
||||||
|
/: with-metadata:
|
||||||
|
< a-z by-metadata: Some-dedicated-field
|
||||||
|
with-metadata: Pages
|
||||||
|
> a-z by-metadata:
|
||||||
|
|
||||||
:::: Conceptual model
|
:::: Conceptual model
|
||||||
/: Entities
|
/: Entities
|
||||||
%
|
%
|
||||||
|
@ -71,6 +77,12 @@ target-folder: tricky folder
|
||||||
/folders
|
/folders
|
||||||
/:files
|
/:files
|
||||||
|
|
||||||
|
target-folder: tricky folder 2
|
||||||
|
/:files with-metadata:
|
||||||
|
< a-z by-metadata: Some-dedicated-field
|
||||||
|
% with-metadata: Pages
|
||||||
|
> a-z by-metadata:
|
||||||
|
|
||||||
:::: Conceptual model
|
:::: Conceptual model
|
||||||
/:files Entities
|
/:files Entities
|
||||||
%
|
%
|
||||||
|
@ -142,6 +154,26 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
|
||||||
outsidersFoldersGroupIdx: 0,
|
outsidersFoldersGroupIdx: 0,
|
||||||
targetFoldersPaths: ['tricky folder']
|
targetFoldersPaths: ['tricky folder']
|
||||||
},
|
},
|
||||||
|
"tricky folder 2": {
|
||||||
|
groups: [{
|
||||||
|
filesOnly: true,
|
||||||
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
|
withMetadataFieldName: 'sort-index-value',
|
||||||
|
order: CustomSortOrder.byMetadataFieldAlphabetical,
|
||||||
|
byMetadataField: 'Some-dedicated-field',
|
||||||
|
}, {
|
||||||
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
|
withMetadataFieldName: 'Pages',
|
||||||
|
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse
|
||||||
|
}, {
|
||||||
|
order: CustomSortOrder.alphabetical,
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 2,
|
||||||
|
targetFoldersPaths: [
|
||||||
|
'tricky folder 2'
|
||||||
|
]
|
||||||
|
},
|
||||||
"Conceptual model": {
|
"Conceptual model": {
|
||||||
groups: [{
|
groups: [{
|
||||||
exactText: "Entities",
|
exactText: "Entities",
|
||||||
|
@ -437,30 +469,54 @@ describe('SortingSpecProcessor', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const txtInputTrueAlphabeticalSortAttr: string = `
|
const txtInputTrueAlphabeticalSortAttr: string = `
|
||||||
target-folder: AAA
|
target-folder: True Alpha
|
||||||
< true a-z
|
< true a-z
|
||||||
target-folder: BBB
|
target-folder: True Alpha Rev
|
||||||
> true a-z
|
> true a-z
|
||||||
|
target-folder: by-meta True Alpha
|
||||||
|
< true a-z by-metadata:
|
||||||
|
target-folder: by-meta True Alpha Rev
|
||||||
|
> true a-z by-metadata: Some-attr
|
||||||
`
|
`
|
||||||
|
|
||||||
const expectedSortSpecForTrueAlphabeticalSorting: { [key: string]: CustomSortSpec } = {
|
const expectedSortSpecForTrueAlphabeticalSorting: { [key: string]: CustomSortSpec } = {
|
||||||
"AAA": {
|
"True Alpha": {
|
||||||
defaultOrder: CustomSortOrder.trueAlphabetical,
|
defaultOrder: CustomSortOrder.trueAlphabetical,
|
||||||
groups: [{
|
groups: [{
|
||||||
order: CustomSortOrder.trueAlphabetical,
|
order: CustomSortOrder.trueAlphabetical,
|
||||||
type: CustomSortGroupType.Outsiders
|
type: CustomSortGroupType.Outsiders
|
||||||
}],
|
}],
|
||||||
outsidersGroupIdx: 0,
|
outsidersGroupIdx: 0,
|
||||||
targetFoldersPaths: ['AAA']
|
targetFoldersPaths: ['True Alpha']
|
||||||
},
|
},
|
||||||
"BBB": {
|
"True Alpha Rev": {
|
||||||
defaultOrder: CustomSortOrder.trueAlphabeticalReverse,
|
defaultOrder: CustomSortOrder.trueAlphabeticalReverse,
|
||||||
groups: [{
|
groups: [{
|
||||||
order: CustomSortOrder.trueAlphabeticalReverse,
|
order: CustomSortOrder.trueAlphabeticalReverse,
|
||||||
type: CustomSortGroupType.Outsiders
|
type: CustomSortGroupType.Outsiders
|
||||||
}],
|
}],
|
||||||
outsidersGroupIdx: 0,
|
outsidersGroupIdx: 0,
|
||||||
targetFoldersPaths: ['BBB']
|
targetFoldersPaths: ['True Alpha Rev']
|
||||||
|
},
|
||||||
|
"by-meta True Alpha": {
|
||||||
|
defaultOrder: CustomSortOrder.byMetadataFieldTrueAlphabetical,
|
||||||
|
groups: [{
|
||||||
|
order: CustomSortOrder.byMetadataFieldTrueAlphabetical,
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 0,
|
||||||
|
targetFoldersPaths: ['by-meta True Alpha']
|
||||||
|
},
|
||||||
|
"by-meta True Alpha Rev": {
|
||||||
|
defaultOrder: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse,
|
||||||
|
byMetadataField: 'Some-attr',
|
||||||
|
groups: [{
|
||||||
|
order: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse,
|
||||||
|
byMetadataField: 'Some-attr',
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 0,
|
||||||
|
targetFoldersPaths: ['by-meta True Alpha Rev']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
CustomSortGroupType,
|
CustomSortGroupType,
|
||||||
CustomSortOrder,
|
CustomSortOrder,
|
||||||
CustomSortSpec,
|
CustomSortSpec,
|
||||||
|
DEFAULT_METADATA_FIELD_FOR_SORTING,
|
||||||
NormalizerFn,
|
NormalizerFn,
|
||||||
RecognizedOrderValue,
|
RecognizedOrderValue,
|
||||||
RegExpSpec
|
RegExpSpec
|
||||||
|
@ -79,6 +80,7 @@ interface CustomSortOrderAscDescPair {
|
||||||
asc: CustomSortOrder,
|
asc: CustomSortOrder,
|
||||||
desc: CustomSortOrder,
|
desc: CustomSortOrder,
|
||||||
secondary?: CustomSortOrder
|
secondary?: CustomSortOrder
|
||||||
|
applyToMetadataField?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// remember about .toLowerCase() before comparison!
|
// remember about .toLowerCase() before comparison!
|
||||||
|
@ -133,6 +135,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OrderByMetadataLexeme: string = 'by-metadata:'
|
||||||
|
|
||||||
enum Attribute {
|
enum Attribute {
|
||||||
TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ...
|
TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ...
|
||||||
OrderAsc,
|
OrderAsc,
|
||||||
|
@ -174,6 +178,8 @@ const AnyTypeGroupLexeme: string = '%' // See % as a combination of / and :
|
||||||
const HideItemShortLexeme: string = '--%' // See % as a combination of / and :
|
const HideItemShortLexeme: string = '--%' // See % as a combination of / and :
|
||||||
const HideItemVerboseLexeme: string = '/--hide:'
|
const HideItemVerboseLexeme: string = '/--hide:'
|
||||||
|
|
||||||
|
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
|
||||||
|
|
||||||
const CommentPrefix: string = '//'
|
const CommentPrefix: string = '//'
|
||||||
|
|
||||||
interface SortingGroupType {
|
interface SortingGroupType {
|
||||||
|
@ -381,6 +387,12 @@ const stripWildcardPatternSuffix = (path: string): [path: string, priority: numb
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simplistic
|
||||||
|
const extractIdentifier = (text: string, defaultResult?: string): string | undefined => {
|
||||||
|
const identifier: string = text.trim().split(' ')?.[0]?.trim()
|
||||||
|
return identifier ? identifier : defaultResult
|
||||||
|
}
|
||||||
|
|
||||||
const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case."
|
const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case."
|
||||||
|
|
||||||
export class SortingSpecProcessor {
|
export class SortingSpecProcessor {
|
||||||
|
@ -607,6 +619,7 @@ export class SortingSpecProcessor {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.ctx.currentSpec.defaultOrder = (attr.value as RecognizedOrderValue).order
|
this.ctx.currentSpec.defaultOrder = (attr.value as RecognizedOrderValue).order
|
||||||
|
this.ctx.currentSpec.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
|
||||||
return true;
|
return true;
|
||||||
} else if (attr.nesting > 0) { // For now only distinguishing nested (indented) and not-nested (not-indented), the depth doesn't matter
|
} else if (attr.nesting > 0) { // For now only distinguishing nested (indented) and not-nested (not-indented), the depth doesn't matter
|
||||||
if (!this.ctx.currentSpec || !this.ctx.currentSpecGroup) {
|
if (!this.ctx.currentSpec || !this.ctx.currentSpecGroup) {
|
||||||
|
@ -623,6 +636,7 @@ export class SortingSpecProcessor {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
|
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
|
||||||
|
this.ctx.currentSpecGroup.byMetadataField = (attr.value as RecognizedOrderValue).applyToMetadataField
|
||||||
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
|
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -636,7 +650,7 @@ export class SortingSpecProcessor {
|
||||||
// no space present, check for potential syntax errors
|
// no space present, check for potential syntax errors
|
||||||
for (let attrLexeme of Object.keys(AttrLexems)) {
|
for (let attrLexeme of Object.keys(AttrLexems)) {
|
||||||
if (lineTrimmedStartLowerCase.startsWith(attrLexeme)) {
|
if (lineTrimmedStartLowerCase.startsWith(attrLexeme)) {
|
||||||
const originalAttrLexeme: string = lineTrimmedStart.substr(0, attrLexeme.length)
|
const originalAttrLexeme: string = lineTrimmedStart.substring(0, attrLexeme.length)
|
||||||
if (lineTrimmedStartLowerCase.length === attrLexeme.length) {
|
if (lineTrimmedStartLowerCase.length === attrLexeme.length) {
|
||||||
this.problem(ProblemCode.MissingAttributeValue, `Attribute "${originalAttrLexeme}" requires a value to follow`)
|
this.problem(ProblemCode.MissingAttributeValue, `Attribute "${originalAttrLexeme}" requires a value to follow`)
|
||||||
return true
|
return true
|
||||||
|
@ -784,6 +798,7 @@ export class SortingSpecProcessor {
|
||||||
for (let group of spec.groups) {
|
for (let group of spec.groups) {
|
||||||
if (!group.order) {
|
if (!group.order) {
|
||||||
group.order = spec.defaultOrder ?? DEFAULT_SORT_ORDER
|
group.order = spec.defaultOrder ?? DEFAULT_SORT_ORDER
|
||||||
|
group.byMetadataField = spec.byMetadataField
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -794,7 +809,7 @@ export class SortingSpecProcessor {
|
||||||
if (path === CURRENT_FOLDER_SYMBOL) {
|
if (path === CURRENT_FOLDER_SYMBOL) {
|
||||||
spec.targetFoldersPaths[idx] = this.ctx.folderPath
|
spec.targetFoldersPaths[idx] = this.ctx.folderPath
|
||||||
} else if (path.startsWith(CURRENT_FOLDER_PREFIX)) {
|
} else if (path.startsWith(CURRENT_FOLDER_PREFIX)) {
|
||||||
spec.targetFoldersPaths[idx] = `${this.ctx.folderPath}/${path.substr(CURRENT_FOLDER_PREFIX.length)}`
|
spec.targetFoldersPaths[idx] = `${this.ctx.folderPath}/${path.substring(CURRENT_FOLDER_PREFIX.length)}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -812,14 +827,49 @@ export class SortingSpecProcessor {
|
||||||
|
|
||||||
private internalValidateOrderAttrValue = (v: string): CustomSortOrderAscDescPair | null => {
|
private internalValidateOrderAttrValue = (v: string): CustomSortOrderAscDescPair | null => {
|
||||||
v = v.trim();
|
v = v.trim();
|
||||||
return v ? OrderLiterals[v.toLowerCase()] : null
|
let orderLiteral: string = v
|
||||||
|
let metadataSpec: Partial<CustomSortOrderAscDescPair> = {}
|
||||||
|
let applyToMetadata: boolean = false
|
||||||
|
|
||||||
|
if (v.indexOf(OrderByMetadataLexeme) > 0) { // Intentionally > 0 -> not allow the metadata lexeme alone
|
||||||
|
const pieces: Array<string> = v.split(OrderByMetadataLexeme)
|
||||||
|
// there are at least two pieces by definition, prefix and suffix of the metadata lexeme
|
||||||
|
orderLiteral = pieces[0]?.trim()
|
||||||
|
let metadataFieldName: string = pieces[1]?.trim()
|
||||||
|
if (metadataFieldName) {
|
||||||
|
metadataSpec.applyToMetadataField = metadataFieldName
|
||||||
|
}
|
||||||
|
applyToMetadata = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let attr: CustomSortOrderAscDescPair | null = orderLiteral ? OrderLiterals[orderLiteral.toLowerCase()] : null
|
||||||
|
if (attr) {
|
||||||
|
if (applyToMetadata &&
|
||||||
|
(attr.asc === CustomSortOrder.alphabetical || attr.desc === CustomSortOrder.alphabeticalReverse ||
|
||||||
|
attr.asc === CustomSortOrder.trueAlphabetical || attr.desc === CustomSortOrder.trueAlphabeticalReverse )) {
|
||||||
|
|
||||||
|
const trueAlphabetical: boolean = attr.asc === CustomSortOrder.trueAlphabetical || attr.desc === CustomSortOrder.trueAlphabeticalReverse
|
||||||
|
|
||||||
|
// Create adjusted copy
|
||||||
|
attr = {
|
||||||
|
...attr,
|
||||||
|
asc: trueAlphabetical ? CustomSortOrder.byMetadataFieldTrueAlphabetical : CustomSortOrder.byMetadataFieldAlphabetical,
|
||||||
|
desc: trueAlphabetical ? CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse : CustomSortOrder.byMetadataFieldAlphabeticalReverse
|
||||||
|
}
|
||||||
|
} else { // For orders different from alphabetical (and reverse) a reference to metadata is not supported
|
||||||
|
metadataSpec.applyToMetadataField = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attr ? {...attr, ...metadataSpec} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateOrderAscAttrValue = (v: string): RecognizedOrderValue | null => {
|
private validateOrderAscAttrValue = (v: string): RecognizedOrderValue | null => {
|
||||||
const recognized: CustomSortOrderAscDescPair | null = this.internalValidateOrderAttrValue(v)
|
const recognized: CustomSortOrderAscDescPair | null = this.internalValidateOrderAttrValue(v)
|
||||||
return recognized ? {
|
return recognized ? {
|
||||||
order: recognized.asc,
|
order: recognized.asc,
|
||||||
secondaryOrder: recognized.secondary
|
secondaryOrder: recognized.secondary,
|
||||||
|
applyToMetadataField: recognized.applyToMetadataField
|
||||||
} : null;
|
} : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -827,7 +877,8 @@ export class SortingSpecProcessor {
|
||||||
const recognized: CustomSortOrderAscDescPair | null = this.internalValidateOrderAttrValue(v)
|
const recognized: CustomSortOrderAscDescPair | null = this.internalValidateOrderAttrValue(v)
|
||||||
return recognized ? {
|
return recognized ? {
|
||||||
order: recognized.desc,
|
order: recognized.desc,
|
||||||
secondaryOrder: recognized.secondary
|
secondaryOrder: recognized.secondary,
|
||||||
|
applyToMetadataField: recognized.applyToMetadataField
|
||||||
} : null;
|
} : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -852,7 +903,7 @@ export class SortingSpecProcessor {
|
||||||
return [ThreeDots]
|
return [ThreeDots]
|
||||||
}
|
}
|
||||||
if (spec.startsWith(ThreeDots)) {
|
if (spec.startsWith(ThreeDots)) {
|
||||||
return [ThreeDots, spec.substr(ThreeDotsLength)];
|
return [ThreeDots, spec.substring(ThreeDotsLength)];
|
||||||
}
|
}
|
||||||
if (spec.endsWith(ThreeDots)) {
|
if (spec.endsWith(ThreeDots)) {
|
||||||
return [spec.substring(0, spec.length - ThreeDotsLength), ThreeDots];
|
return [spec.substring(0, spec.length - ThreeDotsLength), ThreeDots];
|
||||||
|
@ -863,7 +914,7 @@ export class SortingSpecProcessor {
|
||||||
return [
|
return [
|
||||||
spec.substring(0, idx),
|
spec.substring(0, idx),
|
||||||
ThreeDots,
|
ThreeDots,
|
||||||
spec.substr(idx + ThreeDotsLength)
|
spec.substring(idx + ThreeDotsLength)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -926,14 +977,28 @@ export class SortingSpecProcessor {
|
||||||
matchFilenameWithExt: spec.matchFilenameWithExt // Doesn't make sense for matching, yet for multi-match
|
matchFilenameWithExt: spec.matchFilenameWithExt // Doesn't make sense for matching, yet for multi-match
|
||||||
} // theoretically could match the sorting of matched files
|
} // theoretically could match the sorting of matched files
|
||||||
} else {
|
} else {
|
||||||
// For non-three dots single text line assume exact match group
|
if (theOnly.startsWith(MetadataFieldIndicatorLexeme)) {
|
||||||
|
const metadataFieldName: string | undefined = extractIdentifier(
|
||||||
|
theOnly.substring(MetadataFieldIndicatorLexeme.length),
|
||||||
|
DEFAULT_METADATA_FIELD_FOR_SORTING
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
type: CustomSortGroupType.ExactName,
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
exactText: spec.arraySpec[0],
|
withMetadataFieldName: metadataFieldName,
|
||||||
filesOnly: spec.filesOnly,
|
filesOnly: spec.filesOnly,
|
||||||
foldersOnly: spec.foldersOnly,
|
foldersOnly: spec.foldersOnly,
|
||||||
matchFilenameWithExt: spec.matchFilenameWithExt
|
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// For non-three dots single text line assume exact match group
|
||||||
|
return {
|
||||||
|
type: CustomSortGroupType.ExactName,
|
||||||
|
exactText: theOnly,
|
||||||
|
filesOnly: spec.filesOnly,
|
||||||
|
foldersOnly: spec.foldersOnly,
|
||||||
|
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (spec.arraySpec?.length === 2) {
|
if (spec.arraySpec?.length === 2) {
|
||||||
|
|
|
@ -292,6 +292,7 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sortSpec) {
|
if (sortSpec) {
|
||||||
|
sortSpec.plugin = plugin
|
||||||
return folderSort.call(this, sortSpec, ...args);
|
return folderSort.call(this, sortSpec, ...args);
|
||||||
} else {
|
} else {
|
||||||
return old.call(this, ...args);
|
return old.call(this, ...args);
|
||||||
|
|
Loading…
Reference in New Issue