#23 - support for sorting by metadata
- more advanced version of implementation: with-metadata and by-metadata support - readme update
This commit is contained in:
parent
fcaedf9ea5
commit
10bf6e42c7
29
README.md
29
README.md
|
@ -416,7 +416,8 @@ sorting-spec: |
|
||||||
### Example 14: Grouping and sorting by metadata value
|
### 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).
|
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
|
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:
|
For example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -444,20 +445,38 @@ Pages: 12-15
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
Using this plugin you can group and sort notes by the value of the specific metadata, for example:
|
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
|
```yaml
|
||||||
---
|
---
|
||||||
sorting-spec: |
|
sorting-spec: |
|
||||||
target-folder: Remarks from 'The Little Prince' book
|
target-folder: Remarks from 'The Little Prince' book
|
||||||
with-metadata: Pages
|
with-metadata: Pages
|
||||||
< a-z
|
< 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 and sorting.
|
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).
|
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).
|
||||||
Any other sorting from the supported set of rules can be applied, also these not related to the metadata value
|
Then the remaining notes (not having the `Pages` metadata) are sorted by modification date descending.
|
||||||
|
|
||||||
> NOTE
|
> NOTE
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Plugin} from "obsidian";
|
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
|
||||||
|
@ -21,7 +21,8 @@ export enum CustomSortOrder {
|
||||||
byCreatedTimeAdvanced,
|
byCreatedTimeAdvanced,
|
||||||
byCreatedTimeReverse,
|
byCreatedTimeReverse,
|
||||||
byCreatedTimeReverseAdvanced,
|
byCreatedTimeReverseAdvanced,
|
||||||
byMetadataField,
|
byMetadataFieldAlphabetical,
|
||||||
|
byMetadataFieldAlphabeticalReverse,
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -29,6 +30,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
|
||||||
|
@ -45,20 +47,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
|
||||||
metadataFieldName?: string
|
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
|
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,363 @@ 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)', () => {
|
||||||
|
// 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 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 +741,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,5 +1,11 @@
|
||||||
import {MetadataCache, 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 Collator = new Intl.Collator(undefined, {
|
||||||
|
@ -12,6 +18,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
|
||||||
|
@ -20,9 +27,9 @@ export interface FolderItemForSorting {
|
||||||
folder?: TFolder
|
folder?: TFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
||||||
|
|
||||||
let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
|
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
|
||||||
[CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(b.sortString, a.sortString),
|
[CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(b.sortString, a.sortString),
|
||||||
[CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (a.mtime - b.mtime),
|
[CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (a.mtime - b.mtime),
|
||||||
|
@ -33,7 +40,38 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
[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) ? Collator(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.byMetadataField]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
|
[CustomSortOrder.byMetadataFieldAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => {
|
||||||
|
if (a.metadataFieldValue && b.metadataFieldValue) {
|
||||||
|
const sortResult: number = Collator(a.metadataFieldValue, b.metadataFieldValue)
|
||||||
|
if (sortResult === 0) {
|
||||||
|
// Fallback -> requested sort by metadata and both items have the same metadata value
|
||||||
|
return Collator(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 -1
|
||||||
|
if (b.metadataFieldValue) return 1
|
||||||
|
// Fallback -> requested sort by metadata, yet none of two items contain it, use alphabetical by name
|
||||||
|
return Collator(a.sortString, b.sortString)
|
||||||
|
},
|
||||||
|
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => {
|
||||||
|
if (a.metadataFieldValue && b.metadataFieldValue) {
|
||||||
|
const sortResult: number = Collator(b.metadataFieldValue, a.metadataFieldValue)
|
||||||
|
if (sortResult === 0) {
|
||||||
|
// Fallback -> requested sort by metadata and both items have the same metadata value
|
||||||
|
return Collator(b.sortString, a.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 -1
|
||||||
|
if (b.metadataFieldValue) return 1
|
||||||
|
// Fallback -> requested sort by metadata, yet none of two items contain it, use alphabetical reverse by name
|
||||||
|
return Collator(b.sortString, a.sortString)
|
||||||
|
},
|
||||||
|
|
||||||
// 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) => Collator(a.sortString, b.sortString),
|
||||||
|
@ -64,6 +102,10 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -71,7 +113,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 sortString: string | 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
|
||||||
|
@ -148,15 +190,15 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case CustomSortGroupType.HasMetadataField:
|
case CustomSortGroupType.HasMetadataField:
|
||||||
if (group.metadataFieldName) {
|
if (group.withMetadataFieldName) {
|
||||||
const mCache: MetadataCache | undefined = spec.plugin?.app.metadataCache
|
if (spec._mCache) {
|
||||||
if (mCache) {
|
|
||||||
// For folders - scan metadata of 'folder note'
|
// For folders - scan metadata of 'folder note'
|
||||||
const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
|
const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
|
||||||
const metadataValue: string | undefined = mCache.getCache(notePathToScan)?.frontmatter?.[group.metadataFieldName]
|
const frontMatterCache: FrontMatterCache | undefined = spec._mCache.getCache(notePathToScan)?.frontmatter
|
||||||
if (metadataValue) {
|
const hasMetadata: boolean | undefined = frontMatterCache?.hasOwnProperty(group.withMetadataFieldName)
|
||||||
|
|
||||||
|
if (hasMetadata) {
|
||||||
determined = true
|
determined = true
|
||||||
sortString = metadataValue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,17 +219,64 @@ 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
|
||||||
|
|
||||||
|
// One of nuances: if metadata value is not present or empty -> is this treated same or not?
|
||||||
|
// Other nuance: the fallback to compare titles -> exactly when (I suspect when there are no metadata or empty values. Because if one item has some metadata value and the other doesn't have any, the one with the metadata value goes first (for alphabetical) or goes last (for alphabetical reverse) - maybe in both cases should go last?
|
||||||
|
//
|
||||||
|
// I should probably write a test for this specific comparator
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 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: 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,
|
||||||
|
@ -255,6 +344,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) => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,9 @@ target-folder: tricky folder
|
||||||
|
|
||||||
:::: tricky folder 2
|
:::: tricky folder 2
|
||||||
/: with-metadata:
|
/: with-metadata:
|
||||||
> modified
|
< a-z by-metadata: Some-dedicated-field
|
||||||
with-metadata: Pages
|
with-metadata: Pages
|
||||||
> a-z
|
> a-z by-metadata:
|
||||||
|
|
||||||
:::: Conceptual model
|
:::: Conceptual model
|
||||||
/: Entities
|
/: Entities
|
||||||
|
@ -79,9 +79,9 @@ target-folder: tricky folder
|
||||||
|
|
||||||
target-folder: tricky folder 2
|
target-folder: tricky folder 2
|
||||||
/:files with-metadata:
|
/:files with-metadata:
|
||||||
> modified
|
< a-z by-metadata: Some-dedicated-field
|
||||||
% with-metadata: Pages
|
% with-metadata: Pages
|
||||||
> a-z
|
> a-z by-metadata:
|
||||||
|
|
||||||
:::: Conceptual model
|
:::: Conceptual model
|
||||||
/:files Entities
|
/:files Entities
|
||||||
|
@ -157,13 +157,14 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
|
||||||
"tricky folder 2": {
|
"tricky folder 2": {
|
||||||
groups: [{
|
groups: [{
|
||||||
filesOnly: true,
|
filesOnly: true,
|
||||||
metadataFieldName: 'sort-index-value',
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
order: CustomSortOrder.byModifiedTimeReverse,
|
withMetadataFieldName: 'sort-index-value',
|
||||||
type: CustomSortGroupType.HasMetadataField
|
order: CustomSortOrder.byMetadataFieldAlphabetical,
|
||||||
|
byMetadataField: 'Some-dedicated-field',
|
||||||
}, {
|
}, {
|
||||||
metadataFieldName: 'Pages',
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
order: CustomSortOrder.alphabeticalReverse,
|
withMetadataFieldName: 'Pages',
|
||||||
type: CustomSortGroupType.HasMetadataField
|
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse
|
||||||
}, {
|
}, {
|
||||||
order: CustomSortOrder.alphabetical,
|
order: CustomSortOrder.alphabetical,
|
||||||
type: CustomSortGroupType.Outsiders
|
type: CustomSortGroupType.Outsiders
|
||||||
|
|
|
@ -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!
|
||||||
|
@ -132,6 +134,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,
|
||||||
|
@ -175,8 +179,6 @@ const HideItemVerboseLexeme: string = '/--hide:'
|
||||||
|
|
||||||
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
|
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
|
||||||
|
|
||||||
const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value'
|
|
||||||
|
|
||||||
const CommentPrefix: string = '//'
|
const CommentPrefix: string = '//'
|
||||||
|
|
||||||
interface SortingGroupType {
|
interface SortingGroupType {
|
||||||
|
@ -616,6 +618,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) {
|
||||||
|
@ -632,6 +635,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;
|
||||||
}
|
}
|
||||||
|
@ -645,7 +649,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
|
||||||
|
@ -803,7 +807,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)}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -821,14 +825,46 @@ 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)) {
|
||||||
|
|
||||||
|
// Create adjusted copy
|
||||||
|
attr = {
|
||||||
|
...attr,
|
||||||
|
asc: CustomSortOrder.byMetadataFieldAlphabetical,
|
||||||
|
desc: 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -836,7 +872,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -861,7 +898,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];
|
||||||
|
@ -872,7 +909,7 @@ export class SortingSpecProcessor {
|
||||||
return [
|
return [
|
||||||
spec.substring(0, idx),
|
spec.substring(0, idx),
|
||||||
ThreeDots,
|
ThreeDots,
|
||||||
spec.substr(idx + ThreeDotsLength)
|
spec.substring(idx + ThreeDotsLength)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -942,7 +979,7 @@ export class SortingSpecProcessor {
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
type: CustomSortGroupType.HasMetadataField,
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
metadataFieldName: metadataFieldName,
|
withMetadataFieldName: metadataFieldName,
|
||||||
filesOnly: spec.filesOnly,
|
filesOnly: spec.filesOnly,
|
||||||
foldersOnly: spec.foldersOnly,
|
foldersOnly: spec.foldersOnly,
|
||||||
matchFilenameWithExt: spec.matchFilenameWithExt
|
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||||
|
|
Loading…
Reference in New Issue