#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:
SebastianMC 2022-11-03 17:51:42 +01:00
parent 4bd3eaadfd
commit b45d087dfb
5 changed files with 92 additions and 13 deletions

View File

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

View File

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

View File

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

View File

@ -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,13 +935,28 @@ 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
return {
type: CustomSortGroupType.ExactName,
exactText: spec.arraySpec[0],
filesOnly: spec.filesOnly,
foldersOnly: spec.foldersOnly,
matchFilenameWithExt: spec.matchFilenameWithExt
// 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.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
}
}
}
}

View File

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