#74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement

Tons of updates:
- full integration with standard sorting at folder level and at sorting group level
- refined support for implicit sorting for bookmarks plugin integration
- documentation update (partial, sketchy)
This commit is contained in:
SebastianMC 2023-04-18 11:35:10 +02:00
parent cc73b4d3f1
commit 71ab76652c
10 changed files with 398 additions and 186 deletions

View File

@ -181,6 +181,48 @@ sorting-spec: |
The artificial separator `---+---` defines a sorting group, which will not match any folders or files
and is used here to logically separate the series of combined groups into to logical sets
## Bookmarks plugin integration
Integration with the __Bookmarks core plugin__ allows for ordering of items via drag & drop in Bookmarks view and reflecting the same order in File Explorer automatically
TODO: the simple scenario presented on movie
A separate group of bookmarks is designated by default, to separate from ...
If at least one item in folder is bookmarked and the auto-enabled, the order of the items becomes managed by the custom sort plugin
Auto-integration works without any need for sorting configuration files. Under the hood it is equivalent to applying the following global sorting specification:
```yaml
---
sorting-spec: |
target-folder: /*
bookmarked:
< by-bookmarks-order
sorting: standard
---
```
Auto-integration doesn't apply to folders, for which explicit sorting specification is defined in YAML.
In that case, if you want to employ the grouping and/or ordering by bookmarks order, you need to use explicit syntax:
```yaml
---
sorting-spec: |
target-folder: My folder
bookmarked:
< by-bookmarks-order
---
```
TODO: more instructions plus movie of advanced integration, where bookmarks reflect the folders structure
Also hints for updating
A folder is excluded from auto-integration if:
- has custom sorting spec
- you can (if needed) enable the auto-integration for part of the items
- has explicitly applied 'sorting: standard'
## Matching starred items
The Obsidian core plugin `Starred` allows the user to 'star' files\

View File

@ -1,5 +1,3 @@
import {MetadataCache, 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
@ -30,7 +28,7 @@ export enum CustomSortOrder {
byMetadataFieldTrueAlphabetical,
byMetadataFieldAlphabeticalReverse,
byMetadataFieldTrueAlphabeticalReverse,
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
standardObsidian, // whatever user selected in the UI
byBookmarkOrder,
byBookmarkOrderReverse,
default = alphabetical
@ -80,10 +78,7 @@ export interface CustomSortSpec {
outsidersFoldersGroupIdx?: number
itemsToHide?: Set<string>
priorityOrder?: Array<number> // Indexes of groups in evaluation order
// For internal transient use
plugin?: Plugin // to hand over the access to App instance to the sorting engine
_mCache?: MetadataCache
implicit?: boolean // spec applied automatically (e.g. auto integration with a plugin)
}
export const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value'

View File

@ -7,7 +7,7 @@ import {
FolderItemForSorting,
matchGroupRegex, sorterByBookmarkOrder, sorterByMetadataField,
SorterFn,
Sorters
getSorterFnFor, ProcessingContext
} from './custom-sort';
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types';
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
@ -716,7 +716,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: "metadataField1",
exactPrefix: 'Ref'
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -732,7 +734,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -753,7 +755,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: "metadataField1",
exactPrefix: 'Ref'
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -769,7 +773,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -790,7 +794,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: "metadataField1",
exactPrefix: 'Ref'
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -806,7 +812,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -827,7 +833,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: "metadataField1",
exactPrefix: 'Ref'
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -843,7 +851,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(folder, sortSpec)
const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -876,7 +884,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(file, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -907,7 +915,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(file, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -938,7 +946,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -977,7 +985,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1016,7 +1024,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1058,7 +1066,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(file, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1093,7 +1101,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(file, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1127,7 +1135,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(file, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1162,7 +1170,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(file, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1193,7 +1201,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1235,7 +1243,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1280,7 +1288,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1323,7 +1331,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1369,7 +1377,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1412,7 +1420,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1458,7 +1466,7 @@ describe('determineSortingGroup', () => {
// when
const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
})
} as ProcessingContext)
// then
expect(result).toEqual({
@ -1491,7 +1499,9 @@ describe('determineSortingGroup', () => {
byMetadataField: 'metadata-field-for-sorting',
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabetical
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -1507,7 +1517,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -1530,7 +1540,9 @@ describe('determineSortingGroup', () => {
byMetadataField: 'metadata-field-for-sorting',
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -1546,7 +1558,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -1569,7 +1581,9 @@ describe('determineSortingGroup', () => {
byMetadataField: 'metadata-field-for-sorting',
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldTrueAlphabetical
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -1585,7 +1599,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -1608,7 +1622,9 @@ describe('determineSortingGroup', () => {
byMetadataField: 'metadata-field-for-sorting',
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -1624,7 +1640,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -1647,7 +1663,9 @@ describe('determineSortingGroup', () => {
exactPrefix: 'Ref',
byMetadataField: 'metadata-field-for-sorting',
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -1663,7 +1681,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(folder, sortSpec)
const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -1687,6 +1705,10 @@ describe('determineSortingGroup', () => {
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabetical
}],
defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder'
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -1698,13 +1720,11 @@ describe('determineSortingGroup', () => {
}
}[path]
}
} as MetadataCache,
defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder',
} as MetadataCache
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -1726,7 +1746,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField,
order: CustomSortOrder.byMetadataFieldAlphabetical,
withMetadataFieldName: 'field-used-with-with-metadata-syntax'
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -1742,7 +1764,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -1764,7 +1786,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.ExactPrefix,
exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabetical
}],
}]
}
const ctx: Partial<ProcessingContext> = {
_mCache: {
getCache: function (path: string): CachedMetadata | undefined {
return {
@ -1780,7 +1804,7 @@ describe('determineSortingGroup', () => {
}
// when
const result = determineSortingGroup(file, sortSpec)
const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then
expect(result).toEqual({
@ -2094,7 +2118,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
const itemB: Partial<FolderItemForSorting> = {
metadataFieldValue: 'B'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2114,7 +2138,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
metadataFieldValue: 'Aaa',
sortString: 'a123'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2135,7 +2159,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
const itemB: Partial<FolderItemForSorting> = {
sortString: 'n123'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2153,7 +2177,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
const itemB: Partial<FolderItemForSorting> = {
sortString: 'ccc '
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2176,7 +2200,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = {
metadataFieldValue: 'B'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2196,7 +2220,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
metadataFieldValue: 'Aaa',
sortString: 'a123'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2217,7 +2241,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = {
sortString: 'n123'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2236,7 +2260,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = {
sortString: 'n123'
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2254,7 +2278,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = {
sortString: 'ccc '
}
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse)
// when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)

View File

@ -1,8 +1,7 @@
import {FrontMatterCache, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
import {determineStarredStatusOf, getStarredPlugin, Starred_PluginInstance} from '../utils/StarredPluginSignature';
import {FrontMatterCache, MetadataCache, Plugin, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
import {determineStarredStatusOf, Starred_PluginInstance} from '../utils/StarredPluginSignature';
import {
determineIconOf,
getIconFolderPlugin,
ObsidianIconFolder_PluginInstance
} from '../utils/ObsidianIconFolderPluginSignature'
import {
@ -17,10 +16,21 @@ import {
import {isDefined} from "../utils/utils";
import {
Bookmarks_PluginInstance,
determineBookmarkOrder,
getBookmarksPlugin
determineBookmarkOrder
} from "../utils/BookmarksCorePluginSignature";
export interface ProcessingContext {
// For internal transient use
plugin?: Plugin // to hand over the access to App instance to the sorting engine
_mCache?: MetadataCache
starredPluginInstance?: Starred_PluginInstance
bookmarksPlugin: {
instance?: Bookmarks_PluginInstance,
groupNameForSorting?: string
}
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
}
let CollatorCompare = new Intl.Collator(undefined, {
usage: "sort",
sensitivity: "base",
@ -95,7 +105,7 @@ export const sorterByBookmarkOrder:(reverseOrder?: boolean, trueAlphabetical?: b
}
}
export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString),
[CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(b.sortString, a.sortString),
@ -115,19 +125,50 @@ export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
[CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder),
// 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 getSorterFor() function below should protect against it
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
};
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
const StandardObsidianToCustomSort: {[key: string]: CustomSortOrder} = {
"alphabetical": CustomSortOrder.alphabetical,
"alphabeticalReverse": CustomSortOrder.alphabeticalReverse,
"byModifiedTime": CustomSortOrder.byModifiedTimeReverse, // In Obsidian labeled as 'Modified time (new to old)'
"byModifiedTimeReverse": CustomSortOrder.byModifiedTime, // In Obsidian labeled as 'Modified time (old to new)'
"byCreatedTime": CustomSortOrder.byCreatedTimeReverse, // In Obsidian labeled as 'Created time (new to old)'
"byCreatedTimeReverse": CustomSortOrder.byCreatedTime // In Obsidian labeled as 'Created time (old to new)'
}
// Standard Obsidian comparator keeps folders in the top sorted alphabetically
const StandardObsidianComparator = (order: CustomSortOrder): SorterFn => {
const customSorterFn = Sorters[order]
return (a: FolderItemForSorting, b: FolderItemForSorting): number => {
return a.isFolder || b.isFolder
?
(a.isFolder && !b.isFolder ? -1 : (b.isFolder && !a.isFolder ? 1 : Sorters[CustomSortOrder.alphabetical](a,b)))
:
customSorterFn(a, b);
}
}
export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string): SorterFn => {
if (sorting === CustomSortOrder.standardObsidian) {
sorting = StandardObsidianToCustomSort[currentUIselectedSorting ?? 'alphabetical'] ?? CustomSortOrder.alphabetical
return StandardObsidianComparator(sorting)
} else {
return Sorters[sorting]
}
}
function getComparator(sortSpec: CustomSortSpec, currentUIselectedSorting?: string): SorterFn {
const compareTwoItems = (itA: FolderItemForSorting, itB: FolderItemForSorting) => {
if (itA.groupIdx != undefined && itB.groupIdx != undefined) {
if (itA.groupIdx === itB.groupIdx) {
const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx]
const matchingGroupPresentOnBothSidesAndEqual: boolean = itA.matchGroup !== undefined && itA.matchGroup === itB.matchGroup
if (matchingGroupPresentOnBothSidesAndEqual && group.secondaryOrder) {
return Sorters[group.secondaryOrder ?? CustomSortOrder.default](itA, itB)
return getSorterFnFor(group.secondaryOrder ?? CustomSortOrder.default, currentUIselectedSorting)(itA, itB)
} else {
return Sorters[group?.order ?? CustomSortOrder.default](itA, itB)
return getSorterFnFor(group?.order ?? CustomSortOrder.default, currentUIselectedSorting)(itA, itB)
}
} else {
return itA.groupIdx - itB.groupIdx;
@ -136,8 +177,10 @@ function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, s
// should never happen - groupIdx is not known for at least one of items to compare.
// The logic of determining the index always sets some idx
// Yet for sanity and to satisfy TS code analyzer a fallback to default behavior below
return Sorters[CustomSortOrder.default](itA, itB)
return getSorterFnFor(CustomSortOrder.default, currentUIselectedSorting)(itA, itB)
}
}
return compareTwoItems
}
const isFolder = (entry: TAbstractFile) => {
@ -171,13 +214,7 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string):
return [false, undefined, undefined]
}
export interface Context {
starredPluginInstance?: Starred_PluginInstance
bookmarksPluginInstance?: Bookmarks_PluginInstance
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
}
export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: Context): FolderItemForSorting {
export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: ProcessingContext): FolderItemForSorting {
let groupIdx: number
let determined: boolean = false
let matchedGroup: string | null | undefined
@ -261,10 +298,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
break
case CustomSortGroupType.HasMetadataField:
if (group.withMetadataFieldName) {
if (spec._mCache) {
if (ctx?._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
const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter
const hasMetadata: boolean | undefined = frontMatterCache?.hasOwnProperty(group.withMetadataFieldName)
if (hasMetadata) {
@ -282,8 +319,8 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
}
break
case CustomSortGroupType.BookmarkedOnly:
if (ctx?.bookmarksPluginInstance) {
const bookmarkOrder: number | undefined = determineBookmarkOrder(entry.path, ctx.bookmarksPluginInstance)
if (ctx?.bookmarksPlugin?.instance) {
const bookmarkOrder: number | undefined = determineBookmarkOrder(entry.path, ctx.bookmarksPlugin?.instance, ctx.bookmarksPlugin?.groupNameForSorting)
if (bookmarkOrder) { // safe ==> orders intentionally start from 1
determined = true
bookmarkedIdx = bookmarkOrder
@ -362,10 +399,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
metadataFieldName = DEFAULT_METADATA_FIELD_FOR_SORTING
}
if (metadataFieldName) {
if (spec._mCache) {
if (ctx?._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
const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter
metadataValueToSortBy = frontMatterCache?.[metadataFieldName]
}
}
@ -455,7 +492,7 @@ export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSor
// Order by bookmarks order can be applied independently of grouping by bookmarked status
// This function determines the bookmarked order if the sorting criteria (of group or entire folder) requires it
export const determineBookmarksOrderIfNeeded = (folderItems: Array<FolderItemForSorting>, sortingSpec: CustomSortSpec, plugin: Bookmarks_PluginInstance) => {
export const determineBookmarksOrderIfNeeded = (folderItems: Array<FolderItemForSorting>, sortingSpec: CustomSortSpec, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => {
if (!plugin) return
folderItems.forEach((item) => {
@ -469,17 +506,13 @@ export const determineBookmarksOrderIfNeeded = (folderItems: Array<FolderItemFor
}
}
if (folderDefaultSortRequiresBookmarksOrder || groupSortRequiresBookmarksOrder) {
item.bookmarkedIdx = determineBookmarkOrder(item.path, plugin)
item.bookmarkedIdx = determineBookmarkOrder(item.path, plugin, bookmarksGroup)
}
})
}
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
export const folderSort = function (sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
let fileExplorer = this.fileExplorer
sortingSpec._mCache = sortingSpec.plugin?.app.metadataCache
const starredPluginInstance: Starred_PluginInstance | undefined = getStarredPlugin(sortingSpec?.plugin?.app)
const bookmarksPluginInstance: Bookmarks_PluginInstance | undefined = getBookmarksPlugin(sortingSpec?.plugin?.app)
const iconFolderPluginInstance: ObsidianIconFolder_PluginInstance | undefined = getIconFolderPlugin(sortingSpec?.plugin?.app)
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
this.file.children.filter((entry: TFile | TFolder) => {
@ -488,24 +521,20 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
:
this.file.children)
.map((entry: TFile | TFolder) => {
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, {
starredPluginInstance: starredPluginInstance,
bookmarksPluginInstance: bookmarksPluginInstance,
iconFolderPluginInstance: iconFolderPluginInstance
})
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, ctx)
return itemForSorting
})
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined
determineFolderDatesIfNeeded(folderItems, sortingSpec)
if (bookmarksPluginInstance) {
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, bookmarksPluginInstance)
if (ctx.bookmarksPlugin?.instance) {
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPlugin.instance, ctx.bookmarksPlugin.groupNameForSorting)
}
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
return compareTwoItems(itA, itB, sortingSpec);
});
const comparator: SorterFn = getComparator(sortingSpec, fileExplorer.sortOrder)
folderItems.sort(comparator)
const items = folderItems
.map((item: FolderItemForSorting) => fileExplorer.fileItems[item.path])
@ -515,8 +544,4 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
} else {
this.children = items;
}
// release risky references
sortingSpec._mCache = undefined
sortingSpec.plugin = undefined
};

View File

@ -2,8 +2,10 @@ import {FolderWildcardMatching} from './folder-matching-rules'
type SortingSpec = string
const checkIfImplicitSpec = (s: SortingSpec) => false
const createMockMatcherRichVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
let p: string
p = '/...'; matcher.addWildcardDefinition(p, `00 ${p}`)
p = '/*'; matcher.addWildcardDefinition(p, `0 ${p}`)
@ -19,25 +21,25 @@ const PRIO2 = 2
const PRIO3 = 3
const createMockMatcherSimplestVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*')
return matcher
}
const createMockMatcherRootOnlyVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addWildcardDefinition('/...', '/...')
return matcher
}
const createMockMatcherRootOnlyDeepVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addWildcardDefinition('/*', '/*')
return matcher
}
const createMockMatcherSimpleVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*')
matcher.addWildcardDefinition('/Reviews/daily/...', '/Reviews/daily/...')
return matcher
@ -108,21 +110,21 @@ describe('folderMatch', () => {
expect(match3).toBe('/Reviews/daily/...')
})
it('should detect duplicate match children definitions for same path', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addWildcardDefinition('Archive/2020/...', 'First occurrence')
const result = matcher.addWildcardDefinition('/Archive/2020/.../', 'Duplicate')
expect(result).toEqual({errorMsg: "Duplicate wildcard '...' specification for /Archive/2020/.../"})
})
it('should detect duplicate match all definitions for same path', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addWildcardDefinition('/Archive/2019/*', 'First occurrence')
const result = matcher.addWildcardDefinition('Archive/2019/*', 'Duplicate')
expect(result).toEqual({errorMsg: "Duplicate wildcard '*' specification for Archive/2019/*"})
})
it('regexp-match by name works (order of regexp doesn\'t matter) case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`)
matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`)
matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -134,7 +136,7 @@ describe('folderMatch', () => {
expect(match2).toBe('r2')
})
it('regexp-match by name works (order of regexp doesn\'t matter) reversed case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`)
matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`)
matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -146,7 +148,7 @@ describe('folderMatch', () => {
expect(match2).toBe('r2')
})
it('regexp-match by path works (order of regexp doesn\'t matter) case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`)
matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`)
matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -158,7 +160,7 @@ describe('folderMatch', () => {
expect(match2).toBe('r1')
})
it('regexp-match by path works (order of regexp doesn\'t matter) reversed case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`)
matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`)
matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -170,7 +172,7 @@ describe('folderMatch', () => {
expect(match2).toBe('r1')
})
it('regexp-match by path and name for root level - order of regexp decides - case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`)
matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`)
matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -179,7 +181,7 @@ describe('folderMatch', () => {
expect(match).toBe('r2')
})
it('regexp-match by path and name for root level - order of regexp decides - reversed case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`)
matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`)
matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -188,7 +190,7 @@ describe('folderMatch', () => {
expect(match).toBe('r1')
})
it('regexp-match priorities - order of definitions irrelevant - unique priorities - case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 3, false, `r1p3`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`)
@ -198,7 +200,7 @@ describe('folderMatch', () => {
expect(match).toBe('r1p3')
})
it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`)
@ -208,7 +210,7 @@ describe('folderMatch', () => {
expect(match).toBe('r1p3')
})
it('regexp-match priorities - order of definitions irrelevant - duplicate priorities - case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3a`)
matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3b`)
matcher.addRegexpDefinition(/^daily$/, true, 2, false, `r2p2a`)
@ -221,7 +223,7 @@ describe('folderMatch', () => {
expect(match).toBe('r1p3b')
})
it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`)
@ -231,14 +233,14 @@ describe('folderMatch', () => {
expect(match).toBe('r1p3')
})
it('regexp-match - edge case of matching the root folder - match by path', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addRegexpDefinition(/^\/$/, false, undefined, false, `r1`)
// Path w/o leading / - this is how Obsidian supplies the path
const match: SortingSpec | null = matcher.folderMatch('/', '')
expect(match).toBe('r1')
})
it('regexp-match - edge case of matching the root folder - match by name not possible', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
// Tricky regexp which can return zero length matches
matcher.addRegexpDefinition(/.*/, true, undefined, false, `r1`)
matcher.addWildcardDefinition('/*', `w1`)
@ -247,7 +249,7 @@ describe('folderMatch', () => {
expect(match).toBe('w1')
})
it('regexp-match - edge case of no match when only regexp rules present', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
// Tricky regexp which can return zero length matches
matcher.addRegexpDefinition(/abc/, true, undefined, false, `r1`)
// Path w/o leading / - this is how Obsidian supplies the path

View File

@ -35,6 +35,8 @@ export interface AddingWildcardFailure {
errorMsg: string
}
export type CheckIfImplicitSpec<SortingSpec> = (s: SortingSpec) => boolean
export class FolderWildcardMatching<SortingSpec> {
// mimics the structure of folders, so for example tree.matchAll contains the matchAll flag for the root '/'
@ -44,6 +46,9 @@ export class FolderWildcardMatching<SortingSpec> {
regexps: Array<FolderMatchingRegexp<SortingSpec>>
constructor(private checkIfImplicitSpec: CheckIfImplicitSpec<SortingSpec>) {
}
// cache
determinedWildcardRules: { [key: string]: DeterminedSortingSpec<SortingSpec> } = {}
@ -68,13 +73,13 @@ export class FolderWildcardMatching<SortingSpec> {
}
})
if (lastComponent === MATCH_CHILDREN_PATH_TOKEN) {
if (leafNode.matchChildren) {
if (leafNode.matchChildren && !this.checkIfImplicitSpec(leafNode.matchChildren)) {
return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`}
} else {
leafNode.matchChildren = rule
}
} else { // Implicitly: MATCH_ALL_PATH_TOKEN
if (leafNode.matchAll) {
if (leafNode.matchAll && !this.checkIfImplicitSpec(leafNode.matchAll)) {
return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`}
} else {
leafNode.matchAll = rule

View File

@ -527,16 +527,23 @@ describe('SortingSpecProcessor', () => {
const txtInputStandardObsidianSortAttr: string = `
target-folder: AAA
sorting: standard
/ Some folder
sorting: standard
`
const expectedSortSpecForObsidianStandardSorting: { [key: string]: CustomSortSpec } = {
"AAA": {
defaultOrder: CustomSortOrder.standardObsidian,
groups: [{
exactText: 'Some folder',
foldersOnly: true,
order: CustomSortOrder.standardObsidian,
type: CustomSortGroupType.ExactName
}, {
order: CustomSortOrder.standardObsidian,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
outsidersGroupIdx: 1,
targetFoldersPaths: ['AAA']
}
}
@ -1687,11 +1694,13 @@ const txtInputErrorTooManyNumericSortSymbols: string = `
% Chapter\\R+ ... page\\d+
`
/* No longer applicable
const txtInputErrorNestedStandardObsidianSortAttr: string = `
target-folder: AAA
/ Some folder
sorting: standard
`
*/
const txtInputErrorPriorityEmptyFilePattern: string = `
/!! /:
@ -1797,6 +1806,7 @@ describe('SortingSpecProcessor error detection and reporting', () => {
`${ERR_PREFIX} 9:TooManySortingSymbols Maximum one sorting symbol allowed per line ${ERR_SUFFIX_IN_LINE(2)}`)
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('% Chapter\\R+ ... page\\d+ '))
})
/* Problem no longer applicable
it('should recognize error: nested standard obsidian sorting attribute', () => {
const inputTxtArr: Array<string> = txtInputErrorNestedStandardObsidianSortAttr.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
@ -1806,6 +1816,7 @@ describe('SortingSpecProcessor error detection and reporting', () => {
`${ERR_PREFIX} 14:StandardObsidianSortAllowedOnlyAtFolderLevel The standard Obsidian sort order is only allowed at a folder level (not nested syntax) ${ERR_SUFFIX_IN_LINE(4)}`)
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard'))
})
*/
it('should recognize error: priority indicator alone', () => {
const inputTxtArr: Array<string> = `
/!

View File

@ -37,6 +37,7 @@ interface ProcessingContext {
specs: Array<CustomSortSpec>
currentSpec?: CustomSortSpec
currentSpecGroup?: CustomSortGroup
implicitSpec?: boolean
// Support for specific conditions (intentionally not generic approach)
previousValidEntryWasTargetFolderAttr?: boolean // Entry in previous non-empty valid line
@ -69,7 +70,7 @@ export enum ProblemCode {
ItemToHideExactNameWithExtRequired,
ItemToHideNoSupportForThreeDots,
DuplicateWildcardSortSpecForSameFolder,
StandardObsidianSortAllowedOnlyAtFolderLevel,
ProblemNoLongerApplicable_StandardObsidianSortAllowedOnlyAtFolderLevel, // Placeholder kept to avoid refactoring of many unit tests (hardcoded error codes)
PriorityNotAllowedOnOutsidersGroup,
TooManyPriorityPrefixes,
CombiningNotAllowedOnOutsidersGroup,
@ -580,7 +581,7 @@ const ensureCollectionHasSortSpecByName = (collection?: SortSpecsCollection | nu
const ensureCollectionHasSortSpecByWildcard = (collection?: SortSpecsCollection | null) => {
collection = collection ?? {}
if (!collection.sortSpecByWildcard) {
collection.sortSpecByWildcard = new FolderWildcardMatching<CustomSortSpec>()
collection.sortSpecByWildcard = new FolderWildcardMatching<CustomSortSpec>((spec: CustomSortSpec) => !!spec.implicit)
}
return collection
}
@ -605,35 +606,38 @@ const endsWithWildcardPatternSuffix = (path: string): boolean => {
enum WildcardPriority {
NO_WILDCARD = 1,
NO_WILDCARD_IMPLICIT,
MATCH_CHILDREN,
MATCH_ALL
MATCH_CHILDREN_IMPLICIT,
MATCH_ALL,
MATCH_ALL_IMPLICIT
}
const stripWildcardPatternSuffix = (path: string): {path: string, detectedWildcardPriority: number} => {
const stripWildcardPatternSuffix = (path: string, ofImplicitSpec: boolean): {path: string, detectedWildcardPriority: number} => {
if (path.endsWith(MATCH_ALL_SUFFIX)) {
path = path.slice(0, -MATCH_ALL_SUFFIX.length)
return {
path: path.length > 0 ? path : '/',
detectedWildcardPriority: WildcardPriority.MATCH_ALL
detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_ALL_IMPLICIT : WildcardPriority.MATCH_ALL
}
}
if (path.endsWith(MATCH_CHILDREN_1_SUFFIX)) {
path = path.slice(0, -MATCH_CHILDREN_1_SUFFIX.length)
return {
path: path.length > 0 ? path : '/',
detectedWildcardPriority: WildcardPriority.MATCH_CHILDREN,
detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_CHILDREN_IMPLICIT : WildcardPriority.MATCH_CHILDREN
}
}
if (path.endsWith(MATCH_CHILDREN_2_SUFFIX)) {
path = path.slice(0, -MATCH_CHILDREN_2_SUFFIX.length)
return {
path: path.length > 0 ? path : '/',
detectedWildcardPriority: WildcardPriority.MATCH_CHILDREN
detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.MATCH_CHILDREN_IMPLICIT : WildcardPriority.MATCH_CHILDREN
}
}
return {
path: path,
detectedWildcardPriority: WildcardPriority.NO_WILDCARD
detectedWildcardPriority: ofImplicitSpec ? WildcardPriority.NO_WILDCARD_IMPLICIT : WildcardPriority.NO_WILDCARD
}
}
@ -730,12 +734,14 @@ export class SortingSpecProcessor {
parseSortSpecFromText(text: Array<string>,
folderPath: string,
sortingSpecFileName: string,
collection?: SortSpecsCollection | null
collection?: SortSpecsCollection | null,
implicitSpec?: boolean
): SortSpecsCollection | null | undefined {
// reset / init processing state after potential previous invocation
this.ctx = {
folderPath: folderPath, // location of the sorting spec file
specs: []
specs: [],
implicitSpec: implicitSpec
};
this.currentEntryLine = null
this.currentEntryLineIdx = null
@ -842,7 +848,7 @@ export class SortingSpecProcessor {
for (let idx = 0; idx < spec.targetFoldersPaths.length; idx++) {
const originalPath = spec.targetFoldersPaths[idx]
if (!originalPath.startsWith(MatchFolderNameLexeme) && !originalPath.startsWith(MatchFolderByRegexpLexeme)) {
const {path, detectedWildcardPriority} = stripWildcardPatternSuffix(originalPath)
const {path, detectedWildcardPriority} = stripWildcardPatternSuffix(originalPath, !!spec.implicit)
let storeTheSpec: boolean = true
const preexistingSortSpecPriority: WildcardPriority = this.pathMatchPriorityForPath[path]
if (preexistingSortSpecPriority) {
@ -974,10 +980,6 @@ export class SortingSpecProcessor {
this.problem(ProblemCode.DuplicateOrderAttr, `Duplicate order specification for a sorting rule of folder ${folderPathsForProblemMsg}`)
return false;
}
if ((attr.value as RecognizedOrderValue).order === CustomSortOrder.standardObsidian) {
this.problem(ProblemCode.StandardObsidianSortAllowedOnlyAtFolderLevel, `The standard Obsidian sort order is only allowed at a folder level (not nested syntax)`)
return false;
}
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
@ -1453,7 +1455,8 @@ export class SortingSpecProcessor {
private putNewSpecForNewTargetFolder(folderPath?: string): CustomSortSpec {
const newSpec: CustomSortSpec = {
targetFoldersPaths: [folderPath ?? this.ctx.folderPath],
groups: []
groups: [],
implicit: this.ctx.implicitSpec
}
this.ctx.specs.push(newSpec);

View File

@ -1,6 +1,6 @@
import {
App,
FileExplorerView,
FileExplorerView, Menu, MenuItem,
MetadataCache,
normalizePath,
Notice,
@ -13,10 +13,10 @@ import {
TAbstractFile,
TFile,
TFolder,
Vault
Vault, WorkspaceLeaf
} from 'obsidian';
import {around} from 'monkey-around';
import {folderSort} from './custom-sort/custom-sort';
import {folderSort, ProcessingContext} from './custom-sort/custom-sort';
import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
@ -29,6 +29,9 @@ import {
ICON_SORT_SUSPENDED_GENERAL_ERROR,
ICON_SORT_SUSPENDED_SYNTAX_ERROR
} from "./custom-sort/icons";
import {getStarredPlugin} from "./utils/StarredPluginSignature";
import {getBookmarksPlugin} from "./utils/BookmarksCorePluginSignature";
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
interface CustomSortPluginSettings {
additionalSortspecFile: string
@ -36,7 +39,8 @@ interface CustomSortPluginSettings {
statusBarEntryEnabled: boolean
notificationsEnabled: boolean
mobileNotificationsEnabled: boolean
enableAutomaticBookmarksOrderIntegration: boolean
automaticBookmarksIntegration: boolean
bookmarksGroupToConsumeAsOrderingReference: string
}
const DEFAULT_SETTINGS: CustomSortPluginSettings = {
@ -45,7 +49,8 @@ const DEFAULT_SETTINGS: CustomSortPluginSettings = {
statusBarEntryEnabled: true,
notificationsEnabled: true,
mobileNotificationsEnabled: false,
enableAutomaticBookmarksOrderIntegration: false
automaticBookmarksIntegration: false,
bookmarksGroupToConsumeAsOrderingReference: 'sortspec'
}
const SORTSPEC_FILE_NAME: string = 'sortspec.md'
@ -53,6 +58,13 @@ const SORTINGSPEC_YAML_KEY: string = 'sorting-spec'
const ERROR_NOTICE_TIMEOUT: number = 10000
const ImplicitSortspecForBookmarksIntegration: string = `
target-folder: /*
bookmarked:
< by-bookmarks-order
sorting: standard
`
// the monkey-around package doesn't export the below type
type MonkeyAroundUninstaller = () => void
@ -82,12 +94,13 @@ export default class CustomSortPlugin extends Plugin {
this.sortSpecCache = null
const processor: SortingSpecProcessor = new SortingSpecProcessor()
if (this.settings.enableAutomaticBookmarksOrderIntegration) {
if (this.settings.automaticBookmarksIntegration) {
this.sortSpecCache = processor.parseSortSpecFromText(
'target-folder: /*\n< by-bookmarks-order'.split('\n'),
ImplicitSortspecForBookmarksIntegration.split('\n'),
'System internal path', // Dummy unused value, there are no errors in the internal spec
'System internal file', // Dummy unused value, there are no errors in the internal spec
this.sortSpecCache
this.sortSpecCache,
true // Implicit sorting spec generation
)
console.log('Auto injected sort spec')
console.log(this.sortSpecCache)
@ -296,6 +309,29 @@ export default class CustomSortPlugin extends Plugin {
}
})
);
this.registerEvent(
this.app.workspace.on("file-menu", (menu: Menu, file: TAbstractFile, source: string, leaf?: WorkspaceLeaf) => {
const bookmarkThisMenuItem = (item: MenuItem) => {
// TODO: if already bookmarked in the 'custom sort' group (or its descendants) don't show
item.setTitle('Custom sort: bookmark for sorting.');
item.setIcon('hashtag');
item.onClick(() => {
console.log(`custom-sort: bookmark this clicked ${source}`)
});
};
const bookmarkAllMenuItem = (item: MenuItem) => {
item.setTitle('Custom sort: bookmark all siblings for sorting.');
item.setIcon('hashtag');
item.onClick(() => {
console.log(`custom-sort: bookmark all siblings clicked ${source}`)
});
};
menu.addItem(bookmarkThisMenuItem)
menu.addItem(bookmarkAllMenuItem)
})
)
}
registerCommands() {
@ -335,6 +371,10 @@ export default class CustomSortPlugin extends Plugin {
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
sort(old: any) {
return function (...args: any[]) {
console.log(this)
console.log(this.fileExplorer.sortOrder)
// quick check for plugin status
if (plugin.settings.suspended) {
return old.call(this, ...args);
@ -349,21 +389,34 @@ export default class CustomSortPlugin extends Plugin {
const folder: TFolder = this.file
let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath?.[folder.path]
sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName?.[folder.name]
if (sortSpec) {
if (sortSpec.defaultOrder === CustomSortOrder.standardObsidian) {
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
}
} else if (plugin.sortSpecCache?.sortSpecByWildcard) {
if (!sortSpec && plugin.sortSpecCache?.sortSpecByWildcard) {
// when no sorting spec found directly by folder path, check for wildcard-based match
sortSpec = plugin.sortSpecCache?.sortSpecByWildcard.folderMatch(folder.path, folder.name)
if (sortSpec?.defaultOrder === CustomSortOrder.standardObsidian) {
/* SM??? if (sortSpec?.defaultOrder === CustomSortOrder.standardObsidian) {
explicitlyStandardSort = true
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
}*/
}
}
// TODO: ensure that explicitly configured standard sort excludes the auto-applied on-the-fly
if (sortSpec) {
sortSpec.plugin = plugin
return folderSort.call(this, sortSpec, ...args);
console.log(`Sortspec for folder ${folder.path}`)
console.log(sortSpec)
const ctx: ProcessingContext = {
_mCache: plugin.app.metadataCache,
starredPluginInstance: getStarredPlugin(plugin.app),
bookmarksPlugin: {
instance: plugin.settings.automaticBookmarksIntegration ? getBookmarksPlugin(this.app) : undefined,
groupNameForSorting: plugin.settings.bookmarksGroupToConsumeAsOrderingReference
},
iconFolderPluginInstance: getIconFolderPlugin(this.app),
plugin: plugin
}
return folderSort.call(this, sortSpec, ctx);
} else {
console.log(`NO Sortspec for folder ${folder.path}`)
return old.call(this, ...args);
}
};
@ -490,17 +543,67 @@ class CustomSortSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
const bookmarksIntegrationDescription: DocumentFragment = sanitizeHTMLToDom(
'If enabled, order of files and folders in File Explorer will reflect the order '
+ 'of bookmarked items in the bookmarks (core plugin) view.'
+ '<br>'
+ '<p>To separate regular bookmarks from the bookmarks created for sorting, you can put '
+ 'the latter in a separate dedicated bookmarks group. The default name of the group is '
+ "'" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "' "
+ 'and you can change the group name in the configuration field below.'
+ '<br>'
+ 'If left empty, all the bookmarked items will be used to impose the order in File Explorer.</p>'
+ '<p>More information on this functionality in the '
+ '<a href="https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/manual.md#bookmarks-plugin-integration">'
+ 'manual</a> of this custom-sort plugin.'
+ '</p>'
)
new Setting(containerEl)
.setName('Enable automatic integration with core Bookmarks plugin')
.setName('Automatic integration with core Bookmarks plugin (for indirect drag & drop ordering)')
// TODO: add a nice description here
.setDesc('Details TBD. TODO: add a nice description here')
.setDesc(bookmarksIntegrationDescription)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableAutomaticBookmarksOrderIntegration)
.setValue(this.plugin.settings.automaticBookmarksIntegration)
.onChange(async (value) => {
this.plugin.settings.enableAutomaticBookmarksOrderIntegration = value;
this.plugin.settings.automaticBookmarksIntegration = value;
await this.plugin.saveSettings();
}));
// TODO: expose additional configuration setting to specify group path in Bookmarks, if auto-integration with bookmarks is enabled
new Setting(containerEl)
.setName('Name of the group in Bookmarks from which to read the order of items')
.setDesc('See above.')
.addText(text => text
.setPlaceholder('e.g. Group for sorting')
.setValue(this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
.onChange(async (value) => {
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value.trim();
await this.plugin.saveSettings();
}));
}
}
// TODO: clear bookmarks cache upon each tap on ribbon or on the command of 'sorting-on'
// TODO: clear bookmarks cache upon each context menu - before and after (maybe after is not needed, implicitlty empty after first clearing)
// TODO: if a folder doesn't have any bookmarked items, it should remain under control of standard obsidian sorting
// TODO: in discussion sections add (and pin) announcement "DRAG & DROP ORDERING AVAILABLE VIA THE BOOKMARKS CORE PLUGIN INTEGRATION"
// TODO: in community, add update message with announcement of drag & drop support via Bookmarks plugin
// TODO: if folder has explicit sorting: standard, don't apply bookmarks
// TODO: fix error
// bookmarks integration - for root folder and for other folders
// (check for the case:
// target-folder: /*
// sorting: standard
// TODO: unbookmarked items in partially bookmarked -> can it apply the system sort ???
// TODO: unblock syntax 'sorting: standard' also for groups --> since I have access to currently configured sorting :-)
// TODO: bug? On auto-bookmark integration strange behavior

View File

@ -86,27 +86,29 @@ export const getBookmarksPlugin = (app?: App): Bookmarks_PluginInstance | undefi
}
}
type TraverseCallback = (item: BookmarkedItem) => boolean | void
type TraverseCallback = (item: BookmarkedItem, groupPath: string) => boolean | void
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callback: TraverseCallback) => {
const recursiveTraversal = (collection: Array<BookmarkedItem>) => {
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupPath: string) => {
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
const item = collectionRef[idx];
if (callback(item)) return;
if ('group' === item.type) recursiveTraversal(item.items);
if (callback(item, groupPath)) return;
if ('group' === item.type) recursiveTraversal(item.items, `${groupPath}${groupPath ? '/' : ''}${item.title}`);
}
};
recursiveTraversal(items);
recursiveTraversal(items, '');
}
// TODO: extend this function to take a scope as parameter: a path to Bookmarks group to start from
// Initially consuming all bookmarks is ok - finally the starting point (group) should be configurable
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance): OrderedBookmarks | undefined => {
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): OrderedBookmarks | undefined => {
const bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]()
if (bookmarks) {
const orderedBookmarks: OrderedBookmarks = {}
let order: number = 0
const consumeItem = (item: BookmarkedItem) => {
const groupNamePrefix: string = bookmarksGroup ? `${bookmarksGroup}/` : ''
const consumeItem = (item: BookmarkedItem, groupPath: string) => {
if (groupNamePrefix && !groupPath.startsWith(groupNamePrefix)) {
return
}
const isFile: boolean = item.type === 'file'
const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath
const isFolder: boolean = item.type === 'folder'
@ -133,9 +135,9 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance): OrderedBookmarks
// undefined ==> item not found in bookmarks
// > 0 ==> item found in bookmarks at returned position
// Intentionally not returning 0 to allow simple syntax of processing the result
export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginInstance): number | undefined => {
export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): number | undefined => {
if (!bookmarksCache) {
bookmarksCache = getOrderedBookmarks(plugin)
bookmarksCache = getOrderedBookmarks(plugin, bookmarksGroup)
bookmarksCacheTimestamp = Date.now()
}