#23 - support for sorting by metadata
- added support for grouping items by the presence of specified metadata - new keyword `metadata:` introduced for that purpose in lexer - if metadata field name is omitted, the default `sort-index-value` is used - on top of the above, added support for sorting by the value of the specified metadata - unit tests of sorting spec processor extended accordingly
This commit is contained in:
parent
4bd3eaadfd
commit
b45d087dfb
|
@ -1,3 +1,5 @@
|
||||||
|
import {Plugin} from "obsidian";
|
||||||
|
|
||||||
export enum CustomSortGroupType {
|
export enum CustomSortGroupType {
|
||||||
Outsiders, // Not belonging to any of other groups
|
Outsiders, // Not belonging to any of other groups
|
||||||
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
|
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
|
||||||
|
@ -5,6 +7,7 @@ export enum CustomSortGroupType {
|
||||||
ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups
|
ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups
|
||||||
ExactSuffix,
|
ExactSuffix,
|
||||||
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
|
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
|
||||||
|
HasMetadataField // Notes (or folder's notes) containing a specific metadata field
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CustomSortOrder {
|
export enum CustomSortOrder {
|
||||||
|
@ -18,6 +21,7 @@ export enum CustomSortOrder {
|
||||||
byCreatedTimeAdvanced,
|
byCreatedTimeAdvanced,
|
||||||
byCreatedTimeReverse,
|
byCreatedTimeReverse,
|
||||||
byCreatedTimeReverseAdvanced,
|
byCreatedTimeReverseAdvanced,
|
||||||
|
byMetadataField,
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -44,7 +48,8 @@ export interface CustomSortGroup {
|
||||||
secondaryOrder?: CustomSortOrder
|
secondaryOrder?: CustomSortOrder
|
||||||
filesOnly?: boolean
|
filesOnly?: boolean
|
||||||
matchFilenameWithExt?: boolean
|
matchFilenameWithExt?: boolean
|
||||||
foldersOnly?: boolean,
|
foldersOnly?: boolean
|
||||||
|
metadataFieldName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomSortSpec {
|
export interface CustomSortSpec {
|
||||||
|
@ -55,4 +60,5 @@ export interface CustomSortSpec {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
|
import {MetadataCache, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
|
||||||
import {CustomSortGroup, CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
import {CustomSortGroup, CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
||||||
import {isDefined} from "../utils/utils";
|
import {isDefined} from "../utils/utils";
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ 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),
|
||||||
|
|
||||||
// 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),
|
||||||
|
@ -70,6 +71,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
|
||||||
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
|
||||||
|
@ -144,10 +146,24 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break
|
||||||
|
case CustomSortGroupType.HasMetadataField:
|
||||||
|
if (group.metadataFieldName) {
|
||||||
|
const mCache: MetadataCache | undefined = spec.plugin?.app.metadataCache
|
||||||
|
if (mCache) {
|
||||||
|
// For folders - scan metadata of 'folder note'
|
||||||
|
const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
|
||||||
|
const metadataValue: string | undefined = mCache.getCache(notePathToScan)?.frontmatter?.[group.metadataFieldName]
|
||||||
|
if (metadataValue) {
|
||||||
|
determined = true
|
||||||
|
sortString = metadataValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
case CustomSortGroupType.MatchAll:
|
case CustomSortGroupType.MatchAll:
|
||||||
determined = true;
|
determined = true;
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
if (determined) {
|
if (determined) {
|
||||||
break;
|
break;
|
||||||
|
@ -171,7 +187,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
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: matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name,
|
sortString: sortString ?? (matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name),
|
||||||
matchGroup: matchedGroup ?? undefined,
|
matchGroup: matchedGroup ?? undefined,
|
||||||
isFolder: aFolder,
|
isFolder: aFolder,
|
||||||
folder: aFolder ? (entry as TFolder) : undefined,
|
folder: aFolder ? (entry as TFolder) : undefined,
|
||||||
|
@ -255,7 +271,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
|
||||||
return itemForSorting
|
return itemForSorting
|
||||||
})
|
})
|
||||||
|
|
||||||
// Finally, for advanced sorting by modified date, for some of the folders the modified date has to be determined
|
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined
|
||||||
determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality)
|
determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality)
|
||||||
|
|
||||||
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
|
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
|
||||||
|
|
|
@ -24,6 +24,12 @@ target-folder: tricky folder
|
||||||
/
|
/
|
||||||
/:
|
/:
|
||||||
|
|
||||||
|
:::: tricky folder 2
|
||||||
|
/: metadata:
|
||||||
|
> modified
|
||||||
|
metadata: Pages
|
||||||
|
> a-z
|
||||||
|
|
||||||
:::: Conceptual model
|
:::: Conceptual model
|
||||||
/: Entities
|
/: Entities
|
||||||
%
|
%
|
||||||
|
@ -71,6 +77,12 @@ target-folder: tricky folder
|
||||||
/folders
|
/folders
|
||||||
/:files
|
/:files
|
||||||
|
|
||||||
|
target-folder: tricky folder 2
|
||||||
|
/:files metadata:
|
||||||
|
> modified
|
||||||
|
% metadata: Pages
|
||||||
|
> a-z
|
||||||
|
|
||||||
:::: Conceptual model
|
:::: Conceptual model
|
||||||
/:files Entities
|
/:files Entities
|
||||||
%
|
%
|
||||||
|
@ -142,6 +154,25 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
|
||||||
outsidersFoldersGroupIdx: 0,
|
outsidersFoldersGroupIdx: 0,
|
||||||
targetFoldersPaths: ['tricky folder']
|
targetFoldersPaths: ['tricky folder']
|
||||||
},
|
},
|
||||||
|
"tricky folder 2": {
|
||||||
|
groups: [{
|
||||||
|
filesOnly: true,
|
||||||
|
metadataFieldName: 'sort-index-value',
|
||||||
|
order: CustomSortOrder.byModifiedTimeReverse,
|
||||||
|
type: CustomSortGroupType.HasMetadataField
|
||||||
|
}, {
|
||||||
|
metadataFieldName: 'Pages',
|
||||||
|
order: CustomSortOrder.alphabeticalReverse,
|
||||||
|
type: CustomSortGroupType.HasMetadataField
|
||||||
|
}, {
|
||||||
|
order: CustomSortOrder.alphabetical,
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 2,
|
||||||
|
targetFoldersPaths: [
|
||||||
|
'tricky folder 2'
|
||||||
|
]
|
||||||
|
},
|
||||||
"Conceptual model": {
|
"Conceptual model": {
|
||||||
groups: [{
|
groups: [{
|
||||||
exactText: "Entities",
|
exactText: "Entities",
|
||||||
|
|
|
@ -173,6 +173,10 @@ const AnyTypeGroupLexeme: string = '%' // See % as a combination of / and :
|
||||||
const HideItemShortLexeme: string = '--%' // See % as a combination of / and :
|
const HideItemShortLexeme: string = '--%' // See % as a combination of / and :
|
||||||
const HideItemVerboseLexeme: string = '/--hide:'
|
const HideItemVerboseLexeme: string = '/--hide:'
|
||||||
|
|
||||||
|
const MetadataFieldIndicatorLexeme: string = 'metadata:'
|
||||||
|
|
||||||
|
const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value'
|
||||||
|
|
||||||
const CommentPrefix: string = '//'
|
const CommentPrefix: string = '//'
|
||||||
|
|
||||||
interface SortingGroupType {
|
interface SortingGroupType {
|
||||||
|
@ -380,6 +384,12 @@ const stripWildcardPatternSuffix = (path: string): [path: string, priority: numb
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simplistic
|
||||||
|
const extractIdentifier = (text: string, defaultResult?: string): string | undefined => {
|
||||||
|
const identifier: string = text.trim().split(' ')?.[0]?.trim()
|
||||||
|
return identifier ? identifier : defaultResult
|
||||||
|
}
|
||||||
|
|
||||||
const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case."
|
const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case."
|
||||||
|
|
||||||
export class SortingSpecProcessor {
|
export class SortingSpecProcessor {
|
||||||
|
@ -925,13 +935,28 @@ export class SortingSpecProcessor {
|
||||||
matchFilenameWithExt: spec.matchFilenameWithExt // Doesn't make sense for matching, yet for multi-match
|
matchFilenameWithExt: spec.matchFilenameWithExt // Doesn't make sense for matching, yet for multi-match
|
||||||
} // theoretically could match the sorting of matched files
|
} // theoretically could match the sorting of matched files
|
||||||
} else {
|
} else {
|
||||||
// For non-three dots single text line assume exact match group
|
// prototyping - only detect the presence of metadata: lexem
|
||||||
return {
|
if (theOnly.startsWith(MetadataFieldIndicatorLexeme)) {
|
||||||
type: CustomSortGroupType.ExactName,
|
const metadataFieldName: string | undefined = extractIdentifier(
|
||||||
exactText: spec.arraySpec[0],
|
theOnly.substring(MetadataFieldIndicatorLexeme.length),
|
||||||
filesOnly: spec.filesOnly,
|
DEFAULT_METADATA_FIELD_FOR_SORTING
|
||||||
foldersOnly: spec.foldersOnly,
|
)
|
||||||
matchFilenameWithExt: spec.matchFilenameWithExt
|
return {
|
||||||
|
type: CustomSortGroupType.HasMetadataField,
|
||||||
|
metadataFieldName: metadataFieldName,
|
||||||
|
filesOnly: spec.filesOnly,
|
||||||
|
foldersOnly: spec.foldersOnly,
|
||||||
|
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-three dots single text line assume exact match group
|
||||||
|
return {
|
||||||
|
type: CustomSortGroupType.ExactName,
|
||||||
|
exactText: theOnly,
|
||||||
|
filesOnly: spec.filesOnly,
|
||||||
|
foldersOnly: spec.foldersOnly,
|
||||||
|
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -292,6 +292,7 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sortSpec) {
|
if (sortSpec) {
|
||||||
|
sortSpec.plugin = plugin
|
||||||
return folderSort.call(this, sortSpec, ...args);
|
return folderSort.call(this, sortSpec, ...args);
|
||||||
} else {
|
} else {
|
||||||
return old.call(this, ...args);
|
return old.call(this, ...args);
|
||||||
|
|
Loading…
Reference in New Issue