#23 - support for sorting by metadata

- more advanced version of implementation: with-metadata and by-metadata support
- readme update
This commit is contained in:
SebastianMC 2022-11-09 18:52:41 +01:00
parent fcaedf9ea5
commit 10bf6e42c7
7 changed files with 734 additions and 46 deletions

View File

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

View File

@ -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'

View File

@ -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)
})
})

View File

@ -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) => {

View File

@ -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;
} }

View File

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

View File

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