#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 {
|
||||
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
|
||||
|
@ -5,6 +7,7 @@ export enum CustomSortGroupType {
|
|||
ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups
|
||||
ExactSuffix,
|
||||
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 {
|
||||
|
@ -18,6 +21,7 @@ export enum CustomSortOrder {
|
|||
byCreatedTimeAdvanced,
|
||||
byCreatedTimeReverse,
|
||||
byCreatedTimeReverseAdvanced,
|
||||
byMetadataField,
|
||||
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
|
||||
default = alphabetical
|
||||
}
|
||||
|
@ -44,7 +48,8 @@ export interface CustomSortGroup {
|
|||
secondaryOrder?: CustomSortOrder
|
||||
filesOnly?: boolean
|
||||
matchFilenameWithExt?: boolean
|
||||
foldersOnly?: boolean,
|
||||
foldersOnly?: boolean
|
||||
metadataFieldName?: string
|
||||
}
|
||||
|
||||
export interface CustomSortSpec {
|
||||
|
@ -55,4 +60,5 @@ export interface CustomSortSpec {
|
|||
outsidersFilesGroupIdx?: number
|
||||
outsidersFoldersGroupIdx?: number
|
||||
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 {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.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.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
|
||||
[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 determined: boolean = false
|
||||
let matchedGroup: string | null | undefined
|
||||
let sortString: string | undefined
|
||||
const aFolder: boolean = isFolder(entry)
|
||||
const aFile: boolean = !aFolder
|
||||
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]);
|
||||
}
|
||||
}
|
||||
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:
|
||||
determined = true;
|
||||
break;
|
||||
break
|
||||
}
|
||||
if (determined) {
|
||||
break;
|
||||
|
@ -171,7 +187,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
return {
|
||||
// idx of the matched group or idx of Outsiders group or the largest index (= groups count+1)
|
||||
groupIdx: determinedGroupIdx,
|
||||
sortString: matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name,
|
||||
sortString: sortString ?? (matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name),
|
||||
matchGroup: matchedGroup ?? undefined,
|
||||
isFolder: aFolder,
|
||||
folder: aFolder ? (entry as TFolder) : undefined,
|
||||
|
@ -255,7 +271,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
|
|||
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)
|
||||
|
||||
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
|
||||
/: Entities
|
||||
%
|
||||
|
@ -71,6 +77,12 @@ target-folder: tricky folder
|
|||
/folders
|
||||
/:files
|
||||
|
||||
target-folder: tricky folder 2
|
||||
/:files metadata:
|
||||
> modified
|
||||
% metadata: Pages
|
||||
> a-z
|
||||
|
||||
:::: Conceptual model
|
||||
/:files Entities
|
||||
%
|
||||
|
@ -142,6 +154,25 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
|
|||
outsidersFoldersGroupIdx: 0,
|
||||
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": {
|
||||
groups: [{
|
||||
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 HideItemVerboseLexeme: string = '/--hide:'
|
||||
|
||||
const MetadataFieldIndicatorLexeme: string = 'metadata:'
|
||||
|
||||
const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value'
|
||||
|
||||
const CommentPrefix: string = '//'
|
||||
|
||||
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."
|
||||
|
||||
export class SortingSpecProcessor {
|
||||
|
@ -925,14 +935,29 @@ export class SortingSpecProcessor {
|
|||
matchFilenameWithExt: spec.matchFilenameWithExt // Doesn't make sense for matching, yet for multi-match
|
||||
} // theoretically could match the sorting of matched files
|
||||
} else {
|
||||
// For non-three dots single text line assume exact match group
|
||||
// prototyping - only detect the presence of metadata: lexem
|
||||
if (theOnly.startsWith(MetadataFieldIndicatorLexeme)) {
|
||||
const metadataFieldName: string | undefined = extractIdentifier(
|
||||
theOnly.substring(MetadataFieldIndicatorLexeme.length),
|
||||
DEFAULT_METADATA_FIELD_FOR_SORTING
|
||||
)
|
||||
return {
|
||||
type: CustomSortGroupType.ExactName,
|
||||
exactText: spec.arraySpec[0],
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spec.arraySpec?.length === 2) {
|
||||
|
|
|
@ -292,6 +292,7 @@ export default class CustomSortPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
if (sortSpec) {
|
||||
sortSpec.plugin = plugin
|
||||
return folderSort.call(this, sortSpec, ...args);
|
||||
} else {
|
||||
return old.call(this, ...args);
|
||||
|
|
Loading…
Reference in New Issue