#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 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 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 ## Matching starred items
The Obsidian core plugin `Starred` allows the user to 'star' files\ 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 { 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
@ -30,7 +28,7 @@ export enum CustomSortOrder {
byMetadataFieldTrueAlphabetical, byMetadataFieldTrueAlphabetical,
byMetadataFieldAlphabeticalReverse, byMetadataFieldAlphabeticalReverse,
byMetadataFieldTrueAlphabeticalReverse, 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, byBookmarkOrder,
byBookmarkOrderReverse, byBookmarkOrderReverse,
default = alphabetical default = alphabetical
@ -80,10 +78,7 @@ export interface CustomSortSpec {
outsidersFoldersGroupIdx?: number outsidersFoldersGroupIdx?: number
itemsToHide?: Set<string> itemsToHide?: Set<string>
priorityOrder?: Array<number> // Indexes of groups in evaluation order priorityOrder?: Array<number> // Indexes of groups in evaluation order
implicit?: boolean // spec applied automatically (e.g. auto integration with a plugin)
// For internal transient use
plugin?: Plugin // to hand over the access to App instance to the sorting engine
_mCache?: MetadataCache
} }
export const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value' export const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value'

View File

@ -7,7 +7,7 @@ import {
FolderItemForSorting, FolderItemForSorting,
matchGroupRegex, sorterByBookmarkOrder, sorterByMetadataField, matchGroupRegex, sorterByBookmarkOrder, sorterByMetadataField,
SorterFn, SorterFn,
Sorters getSorterFnFor, ProcessingContext
} from './custom-sort'; } from './custom-sort';
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types';
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
@ -716,7 +716,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField, type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: "metadataField1", withMetadataFieldName: "metadataField1",
exactPrefix: 'Ref' exactPrefix: 'Ref'
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -732,7 +734,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -753,7 +755,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField, type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: "metadataField1", withMetadataFieldName: "metadataField1",
exactPrefix: 'Ref' exactPrefix: 'Ref'
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -769,7 +773,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -790,7 +794,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField, type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: "metadataField1", withMetadataFieldName: "metadataField1",
exactPrefix: 'Ref' exactPrefix: 'Ref'
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -806,7 +812,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -827,7 +833,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField, type: CustomSortGroupType.HasMetadataField,
withMetadataFieldName: "metadataField1", withMetadataFieldName: "metadataField1",
exactPrefix: 'Ref' exactPrefix: 'Ref'
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -843,7 +851,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(folder, sortSpec) const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -876,7 +884,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(file, sortSpec, { const result = determineSortingGroup(file, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance starredPluginInstance: starredPluginInstance as Starred_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -907,7 +915,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(file, sortSpec, { const result = determineSortingGroup(file, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance starredPluginInstance: starredPluginInstance as Starred_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -938,7 +946,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance starredPluginInstance: starredPluginInstance as Starred_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -977,7 +985,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance starredPluginInstance: starredPluginInstance as Starred_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1016,7 +1024,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
starredPluginInstance: starredPluginInstance as Starred_PluginInstance starredPluginInstance: starredPluginInstance as Starred_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1058,7 +1066,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(file, sortSpec, { const result = determineSortingGroup(file, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1093,7 +1101,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(file, sortSpec, { const result = determineSortingGroup(file, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1127,7 +1135,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(file, sortSpec, { const result = determineSortingGroup(file, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1162,7 +1170,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(file, sortSpec, { const result = determineSortingGroup(file, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1193,7 +1201,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1235,7 +1243,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1280,7 +1288,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1323,7 +1331,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1369,7 +1377,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1412,7 +1420,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1458,7 +1466,7 @@ describe('determineSortingGroup', () => {
// when // when
const result = determineSortingGroup(folder, sortSpec, { const result = determineSortingGroup(folder, sortSpec, {
iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance
}) } as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1491,7 +1499,9 @@ describe('determineSortingGroup', () => {
byMetadataField: 'metadata-field-for-sorting', byMetadataField: 'metadata-field-for-sorting',
exactPrefix: 'Ref', exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabetical order: CustomSortOrder.byMetadataFieldAlphabetical
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -1507,7 +1517,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1530,7 +1540,9 @@ describe('determineSortingGroup', () => {
byMetadataField: 'metadata-field-for-sorting', byMetadataField: 'metadata-field-for-sorting',
exactPrefix: 'Ref', exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse order: CustomSortOrder.byMetadataFieldAlphabeticalReverse
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -1546,7 +1558,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1569,7 +1581,9 @@ describe('determineSortingGroup', () => {
byMetadataField: 'metadata-field-for-sorting', byMetadataField: 'metadata-field-for-sorting',
exactPrefix: 'Ref', exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldTrueAlphabetical order: CustomSortOrder.byMetadataFieldTrueAlphabetical
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -1585,7 +1599,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1608,7 +1622,9 @@ describe('determineSortingGroup', () => {
byMetadataField: 'metadata-field-for-sorting', byMetadataField: 'metadata-field-for-sorting',
exactPrefix: 'Ref', exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse order: CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -1624,7 +1640,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1647,7 +1663,9 @@ describe('determineSortingGroup', () => {
exactPrefix: 'Ref', exactPrefix: 'Ref',
byMetadataField: 'metadata-field-for-sorting', byMetadataField: 'metadata-field-for-sorting',
order: CustomSortOrder.byMetadataFieldAlphabeticalReverse order: CustomSortOrder.byMetadataFieldAlphabeticalReverse
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -1663,7 +1681,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(folder, sortSpec) const result = determineSortingGroup(folder, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1687,6 +1705,10 @@ describe('determineSortingGroup', () => {
exactPrefix: 'Ref', exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabetical order: CustomSortOrder.byMetadataFieldAlphabetical
}], }],
defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder'
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -1698,13 +1720,11 @@ describe('determineSortingGroup', () => {
} }
}[path] }[path]
} }
} as MetadataCache, } as MetadataCache
defaultOrder: CustomSortOrder.byMetadataFieldAlphabeticalReverse,
byMetadataField: 'metadata-field-for-sorting-specified-on-target-folder',
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1726,7 +1746,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.HasMetadataField, type: CustomSortGroupType.HasMetadataField,
order: CustomSortOrder.byMetadataFieldAlphabetical, order: CustomSortOrder.byMetadataFieldAlphabetical,
withMetadataFieldName: 'field-used-with-with-metadata-syntax' withMetadataFieldName: 'field-used-with-with-metadata-syntax'
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -1742,7 +1764,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -1764,7 +1786,9 @@ describe('determineSortingGroup', () => {
type: CustomSortGroupType.ExactPrefix, type: CustomSortGroupType.ExactPrefix,
exactPrefix: 'Ref', exactPrefix: 'Ref',
order: CustomSortOrder.byMetadataFieldAlphabetical order: CustomSortOrder.byMetadataFieldAlphabetical
}], }]
}
const ctx: Partial<ProcessingContext> = {
_mCache: { _mCache: {
getCache: function (path: string): CachedMetadata | undefined { getCache: function (path: string): CachedMetadata | undefined {
return { return {
@ -1780,7 +1804,7 @@ describe('determineSortingGroup', () => {
} }
// when // when
const result = determineSortingGroup(file, sortSpec) const result = determineSortingGroup(file, sortSpec, ctx as ProcessingContext)
// then // then
expect(result).toEqual({ expect(result).toEqual({
@ -2094,7 +2118,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
const itemB: Partial<FolderItemForSorting> = { const itemB: Partial<FolderItemForSorting> = {
metadataFieldValue: 'B' metadataFieldValue: 'B'
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2114,7 +2138,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
metadataFieldValue: 'Aaa', metadataFieldValue: 'Aaa',
sortString: 'a123' sortString: 'a123'
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2135,7 +2159,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
const itemB: Partial<FolderItemForSorting> = { const itemB: Partial<FolderItemForSorting> = {
sortString: 'n123' sortString: 'n123'
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2153,7 +2177,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabetical', () => {
const itemB: Partial<FolderItemForSorting> = { const itemB: Partial<FolderItemForSorting> = {
sortString: 'ccc ' sortString: 'ccc '
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2176,7 +2200,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = { const itemB: Partial<FolderItemForSorting> = {
metadataFieldValue: 'B' metadataFieldValue: 'B'
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2196,7 +2220,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
metadataFieldValue: 'Aaa', metadataFieldValue: 'Aaa',
sortString: 'a123' sortString: 'a123'
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2217,7 +2241,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = { const itemB: Partial<FolderItemForSorting> = {
sortString: 'n123' sortString: 'n123'
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabetical)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2236,7 +2260,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = { const itemB: Partial<FolderItemForSorting> = {
sortString: 'n123' sortString: 'n123'
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
@ -2254,7 +2278,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
const itemB: Partial<FolderItemForSorting> = { const itemB: Partial<FolderItemForSorting> = {
sortString: 'ccc ' sortString: 'ccc '
} }
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse] const sorter: SorterFn = getSorterFnFor(CustomSortOrder.byMetadataFieldAlphabeticalReverse)
// when // when
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting) 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 {FrontMatterCache, MetadataCache, Plugin, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
import {determineStarredStatusOf, getStarredPlugin, Starred_PluginInstance} from '../utils/StarredPluginSignature'; import {determineStarredStatusOf, Starred_PluginInstance} from '../utils/StarredPluginSignature';
import { import {
determineIconOf, determineIconOf,
getIconFolderPlugin,
ObsidianIconFolder_PluginInstance ObsidianIconFolder_PluginInstance
} from '../utils/ObsidianIconFolderPluginSignature' } from '../utils/ObsidianIconFolderPluginSignature'
import { import {
@ -17,10 +16,21 @@ import {
import {isDefined} from "../utils/utils"; import {isDefined} from "../utils/utils";
import { import {
Bookmarks_PluginInstance, Bookmarks_PluginInstance,
determineBookmarkOrder, determineBookmarkOrder
getBookmarksPlugin
} from "../utils/BookmarksCorePluginSignature"; } 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, { let CollatorCompare = new Intl.Collator(undefined, {
usage: "sort", usage: "sort",
sensitivity: "base", 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.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(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), [CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(b.sortString, a.sortString),
@ -115,29 +125,62 @@ export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder), [CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
[CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder), [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), [CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
}; };
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) { const StandardObsidianToCustomSort: {[key: string]: CustomSortOrder} = {
if (itA.groupIdx != undefined && itB.groupIdx != undefined) { "alphabetical": CustomSortOrder.alphabetical,
if (itA.groupIdx === itB.groupIdx) { "alphabeticalReverse": CustomSortOrder.alphabeticalReverse,
const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx] "byModifiedTime": CustomSortOrder.byModifiedTimeReverse, // In Obsidian labeled as 'Modified time (new to old)'
const matchingGroupPresentOnBothSidesAndEqual: boolean = itA.matchGroup !== undefined && itA.matchGroup === itB.matchGroup "byModifiedTimeReverse": CustomSortOrder.byModifiedTime, // In Obsidian labeled as 'Modified time (old to new)'
if (matchingGroupPresentOnBothSidesAndEqual && group.secondaryOrder) { "byCreatedTime": CustomSortOrder.byCreatedTimeReverse, // In Obsidian labeled as 'Created time (new to old)'
return Sorters[group.secondaryOrder ?? CustomSortOrder.default](itA, itB) "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 getSorterFnFor(group.secondaryOrder ?? CustomSortOrder.default, currentUIselectedSorting)(itA, itB)
} else {
return getSorterFnFor(group?.order ?? CustomSortOrder.default, currentUIselectedSorting)(itA, itB)
}
} else { } else {
return Sorters[group?.order ?? CustomSortOrder.default](itA, itB) return itA.groupIdx - itB.groupIdx;
} }
} else { } else {
return itA.groupIdx - itB.groupIdx; // 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 getSorterFnFor(CustomSortOrder.default, currentUIselectedSorting)(itA, itB)
} }
} else {
// 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 compareTwoItems
} }
const isFolder = (entry: TAbstractFile) => { const isFolder = (entry: TAbstractFile) => {
@ -171,13 +214,7 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string):
return [false, undefined, undefined] return [false, undefined, undefined]
} }
export interface Context { export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: ProcessingContext): FolderItemForSorting {
starredPluginInstance?: Starred_PluginInstance
bookmarksPluginInstance?: Bookmarks_PluginInstance
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
}
export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: Context): FolderItemForSorting {
let groupIdx: number let groupIdx: number
let determined: boolean = false let determined: boolean = false
let matchedGroup: string | null | undefined let matchedGroup: string | null | undefined
@ -261,10 +298,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
break break
case CustomSortGroupType.HasMetadataField: case CustomSortGroupType.HasMetadataField:
if (group.withMetadataFieldName) { if (group.withMetadataFieldName) {
if (spec._mCache) { if (ctx?._mCache) {
// For folders - scan metadata of 'folder note' // For folders - scan metadata of 'folder note'
const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md` const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
const 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) const hasMetadata: boolean | undefined = frontMatterCache?.hasOwnProperty(group.withMetadataFieldName)
if (hasMetadata) { if (hasMetadata) {
@ -282,8 +319,8 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
} }
break break
case CustomSortGroupType.BookmarkedOnly: case CustomSortGroupType.BookmarkedOnly:
if (ctx?.bookmarksPluginInstance) { if (ctx?.bookmarksPlugin?.instance) {
const bookmarkOrder: number | undefined = determineBookmarkOrder(entry.path, ctx.bookmarksPluginInstance) const bookmarkOrder: number | undefined = determineBookmarkOrder(entry.path, ctx.bookmarksPlugin?.instance, ctx.bookmarksPlugin?.groupNameForSorting)
if (bookmarkOrder) { // safe ==> orders intentionally start from 1 if (bookmarkOrder) { // safe ==> orders intentionally start from 1
determined = true determined = true
bookmarkedIdx = bookmarkOrder bookmarkedIdx = bookmarkOrder
@ -362,10 +399,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
metadataFieldName = DEFAULT_METADATA_FIELD_FOR_SORTING metadataFieldName = DEFAULT_METADATA_FIELD_FOR_SORTING
} }
if (metadataFieldName) { if (metadataFieldName) {
if (spec._mCache) { if (ctx?._mCache) {
// For folders - scan metadata of 'folder note' // For folders - scan metadata of 'folder note'
const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md` const notePathToScan: string = aFile ? entry.path : `${entry.path}/${entry.name}.md`
const frontMatterCache: FrontMatterCache | undefined = spec._mCache.getCache(notePathToScan)?.frontmatter const frontMatterCache: FrontMatterCache | undefined = ctx._mCache.getCache(notePathToScan)?.frontmatter
metadataValueToSortBy = frontMatterCache?.[metadataFieldName] 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 // 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 // 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 if (!plugin) return
folderItems.forEach((item) => { folderItems.forEach((item) => {
@ -469,17 +506,13 @@ export const determineBookmarksOrderIfNeeded = (folderItems: Array<FolderItemFor
} }
} }
if (folderDefaultSortRequiresBookmarksOrder || groupSortRequiresBookmarksOrder) { 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 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 ? const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
this.file.children.filter((entry: TFile | TFolder) => { this.file.children.filter((entry: TFile | TFolder) => {
@ -488,24 +521,20 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
: :
this.file.children) this.file.children)
.map((entry: TFile | TFolder) => { .map((entry: TFile | TFolder) => {
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, { const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, ctx)
starredPluginInstance: starredPluginInstance,
bookmarksPluginInstance: bookmarksPluginInstance,
iconFolderPluginInstance: iconFolderPluginInstance
})
return itemForSorting return itemForSorting
}) })
// Finally, for advanced sorting by modified date, for some 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) determineFolderDatesIfNeeded(folderItems, sortingSpec)
if (bookmarksPluginInstance) { if (ctx.bookmarksPlugin?.instance) {
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, bookmarksPluginInstance) determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPlugin.instance, ctx.bookmarksPlugin.groupNameForSorting)
} }
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) { const comparator: SorterFn = getComparator(sortingSpec, fileExplorer.sortOrder)
return compareTwoItems(itA, itB, sortingSpec);
}); folderItems.sort(comparator)
const items = folderItems const items = folderItems
.map((item: FolderItemForSorting) => fileExplorer.fileItems[item.path]) .map((item: FolderItemForSorting) => fileExplorer.fileItems[item.path])
@ -515,8 +544,4 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
} else { } else {
this.children = items; 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 type SortingSpec = string
const checkIfImplicitSpec = (s: SortingSpec) => false
const createMockMatcherRichVersion = (): FolderWildcardMatching<SortingSpec> => { const createMockMatcherRichVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching() const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
let p: string let p: string
p = '/...'; matcher.addWildcardDefinition(p, `00 ${p}`) p = '/...'; matcher.addWildcardDefinition(p, `00 ${p}`)
p = '/*'; matcher.addWildcardDefinition(p, `0 ${p}`) p = '/*'; matcher.addWildcardDefinition(p, `0 ${p}`)
@ -19,25 +21,25 @@ const PRIO2 = 2
const PRIO3 = 3 const PRIO3 = 3
const createMockMatcherSimplestVersion = (): FolderWildcardMatching<SortingSpec> => { const createMockMatcherSimplestVersion = (): 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 return matcher
} }
const createMockMatcherRootOnlyVersion = (): FolderWildcardMatching<SortingSpec> => { const createMockMatcherRootOnlyVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching() const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addWildcardDefinition('/...', '/...') matcher.addWildcardDefinition('/...', '/...')
return matcher return matcher
} }
const createMockMatcherRootOnlyDeepVersion = (): FolderWildcardMatching<SortingSpec> => { const createMockMatcherRootOnlyDeepVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching() const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching(checkIfImplicitSpec)
matcher.addWildcardDefinition('/*', '/*') matcher.addWildcardDefinition('/*', '/*')
return matcher return matcher
} }
const createMockMatcherSimpleVersion = (): FolderWildcardMatching<SortingSpec> => { 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/*')
matcher.addWildcardDefinition('/Reviews/daily/...', '/Reviews/daily/...') matcher.addWildcardDefinition('/Reviews/daily/...', '/Reviews/daily/...')
return matcher return matcher
@ -108,21 +110,21 @@ describe('folderMatch', () => {
expect(match3).toBe('/Reviews/daily/...') expect(match3).toBe('/Reviews/daily/...')
}) })
it('should detect duplicate match children definitions for same path', () => { 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') matcher.addWildcardDefinition('Archive/2020/...', 'First occurrence')
const result = matcher.addWildcardDefinition('/Archive/2020/.../', 'Duplicate') const result = matcher.addWildcardDefinition('/Archive/2020/.../', 'Duplicate')
expect(result).toEqual({errorMsg: "Duplicate wildcard '...' specification for /Archive/2020/.../"}) expect(result).toEqual({errorMsg: "Duplicate wildcard '...' specification for /Archive/2020/.../"})
}) })
it('should detect duplicate match all definitions for same path', () => { 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') matcher.addWildcardDefinition('/Archive/2019/*', 'First occurrence')
const result = matcher.addWildcardDefinition('Archive/2019/*', 'Duplicate') const result = matcher.addWildcardDefinition('Archive/2019/*', 'Duplicate')
expect(result).toEqual({errorMsg: "Duplicate wildcard '*' specification for Archive/2019/*"}) expect(result).toEqual({errorMsg: "Duplicate wildcard '*' specification for Archive/2019/*"})
}) })
it('regexp-match by name works (order of regexp doesn\'t matter) case A', () => { 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$/, false, undefined, false, `r1`)
matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`)
matcher.addWildcardDefinition('/Reviews/*', `w1`) matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -134,7 +136,7 @@ describe('folderMatch', () => {
expect(match2).toBe('r2') expect(match2).toBe('r2')
}) })
it('regexp-match by name works (order of regexp doesn\'t matter) reversed case A', () => { 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$/, true, undefined, false, `r2`)
matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`)
matcher.addWildcardDefinition('/Reviews/*', `w1`) matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -146,7 +148,7 @@ describe('folderMatch', () => {
expect(match2).toBe('r2') expect(match2).toBe('r2')
}) })
it('regexp-match by path works (order of regexp doesn\'t matter) case A', () => { 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$/, false, undefined, false, `r1`)
matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`)
matcher.addWildcardDefinition('/Reviews/*', `w1`) matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -158,7 +160,7 @@ describe('folderMatch', () => {
expect(match2).toBe('r1') expect(match2).toBe('r1')
}) })
it('regexp-match by path works (order of regexp doesn\'t matter) reversed case A', () => { 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$/, true, undefined, false, `r2`)
matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`)
matcher.addWildcardDefinition('/Reviews/*', `w1`) matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -170,7 +172,7 @@ describe('folderMatch', () => {
expect(match2).toBe('r1') expect(match2).toBe('r1')
}) })
it('regexp-match by path and name for root level - order of regexp decides - case A', () => { 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$/, false, undefined, false, `r1`)
matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`)
matcher.addWildcardDefinition('/Reviews/*', `w1`) matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -179,7 +181,7 @@ describe('folderMatch', () => {
expect(match).toBe('r2') expect(match).toBe('r2')
}) })
it('regexp-match by path and name for root level - order of regexp decides - reversed case A', () => { 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$/, true, undefined, false, `r2`)
matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`)
matcher.addWildcardDefinition('/Reviews/*', `w1`) matcher.addWildcardDefinition('/Reviews/*', `w1`)
@ -188,7 +190,7 @@ describe('folderMatch', () => {
expect(match).toBe('r1') expect(match).toBe('r1')
}) })
it('regexp-match priorities - order of definitions irrelevant - unique priorities - case A', () => { 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, 3, false, `r1p3`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`)
@ -198,7 +200,7 @@ describe('folderMatch', () => {
expect(match).toBe('r1p3') expect(match).toBe('r1p3')
}) })
it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => { 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, undefined, false, `r4pNone`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`)
@ -208,7 +210,7 @@ describe('folderMatch', () => {
expect(match).toBe('r1p3') expect(match).toBe('r1p3')
}) })
it('regexp-match priorities - order of definitions irrelevant - duplicate priorities - case A', () => { 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, `r1p3a`)
matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3b`) matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3b`)
matcher.addRegexpDefinition(/^daily$/, true, 2, false, `r2p2a`) matcher.addRegexpDefinition(/^daily$/, true, 2, false, `r2p2a`)
@ -221,7 +223,7 @@ describe('folderMatch', () => {
expect(match).toBe('r1p3b') expect(match).toBe('r1p3b')
}) })
it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => { 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, undefined, false, `r4pNone`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`)
matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`)
@ -231,14 +233,14 @@ describe('folderMatch', () => {
expect(match).toBe('r1p3') expect(match).toBe('r1p3')
}) })
it('regexp-match - edge case of matching the root folder - match by path', () => { 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`) matcher.addRegexpDefinition(/^\/$/, false, undefined, false, `r1`)
// Path w/o leading / - this is how Obsidian supplies the path // Path w/o leading / - this is how Obsidian supplies the path
const match: SortingSpec | null = matcher.folderMatch('/', '') const match: SortingSpec | null = matcher.folderMatch('/', '')
expect(match).toBe('r1') expect(match).toBe('r1')
}) })
it('regexp-match - edge case of matching the root folder - match by name not possible', () => { 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 // Tricky regexp which can return zero length matches
matcher.addRegexpDefinition(/.*/, true, undefined, false, `r1`) matcher.addRegexpDefinition(/.*/, true, undefined, false, `r1`)
matcher.addWildcardDefinition('/*', `w1`) matcher.addWildcardDefinition('/*', `w1`)
@ -247,7 +249,7 @@ describe('folderMatch', () => {
expect(match).toBe('w1') expect(match).toBe('w1')
}) })
it('regexp-match - edge case of no match when only regexp rules present', () => { 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 // Tricky regexp which can return zero length matches
matcher.addRegexpDefinition(/abc/, true, undefined, false, `r1`) matcher.addRegexpDefinition(/abc/, true, undefined, false, `r1`)
// Path w/o leading / - this is how Obsidian supplies the path // Path w/o leading / - this is how Obsidian supplies the path

View File

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

View File

@ -527,16 +527,23 @@ describe('SortingSpecProcessor', () => {
const txtInputStandardObsidianSortAttr: string = ` const txtInputStandardObsidianSortAttr: string = `
target-folder: AAA target-folder: AAA
sorting: standard sorting: standard
/ Some folder
sorting: standard
` `
const expectedSortSpecForObsidianStandardSorting: { [key: string]: CustomSortSpec } = { const expectedSortSpecForObsidianStandardSorting: { [key: string]: CustomSortSpec } = {
"AAA": { "AAA": {
defaultOrder: CustomSortOrder.standardObsidian, defaultOrder: CustomSortOrder.standardObsidian,
groups: [{ groups: [{
exactText: 'Some folder',
foldersOnly: true,
order: CustomSortOrder.standardObsidian,
type: CustomSortGroupType.ExactName
}, {
order: CustomSortOrder.standardObsidian, order: CustomSortOrder.standardObsidian,
type: CustomSortGroupType.Outsiders type: CustomSortGroupType.Outsiders
}], }],
outsidersGroupIdx: 0, outsidersGroupIdx: 1,
targetFoldersPaths: ['AAA'] targetFoldersPaths: ['AAA']
} }
} }
@ -1687,11 +1694,13 @@ const txtInputErrorTooManyNumericSortSymbols: string = `
% Chapter\\R+ ... page\\d+ % Chapter\\R+ ... page\\d+
` `
/* No longer applicable
const txtInputErrorNestedStandardObsidianSortAttr: string = ` const txtInputErrorNestedStandardObsidianSortAttr: string = `
target-folder: AAA target-folder: AAA
/ Some folder / Some folder
sorting: standard sorting: standard
` `
*/
const txtInputErrorPriorityEmptyFilePattern: string = ` 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)}`) `${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+ ')) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('% Chapter\\R+ ... page\\d+ '))
}) })
/* Problem no longer applicable
it('should recognize error: nested standard obsidian sorting attribute', () => { it('should recognize error: nested standard obsidian sorting attribute', () => {
const inputTxtArr: Array<string> = txtInputErrorNestedStandardObsidianSortAttr.split('\n') const inputTxtArr: Array<string> = txtInputErrorNestedStandardObsidianSortAttr.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') 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)}`) `${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')) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard'))
}) })
*/
it('should recognize error: priority indicator alone', () => { it('should recognize error: priority indicator alone', () => {
const inputTxtArr: Array<string> = ` const inputTxtArr: Array<string> = `
/! /!

View File

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

View File

@ -1,6 +1,6 @@
import { import {
App, App,
FileExplorerView, FileExplorerView, Menu, MenuItem,
MetadataCache, MetadataCache,
normalizePath, normalizePath,
Notice, Notice,
@ -13,10 +13,10 @@ import {
TAbstractFile, TAbstractFile,
TFile, TFile,
TFolder, TFolder,
Vault Vault, WorkspaceLeaf
} from 'obsidian'; } from 'obsidian';
import {around} from 'monkey-around'; 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 {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types'; import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
@ -29,6 +29,9 @@ import {
ICON_SORT_SUSPENDED_GENERAL_ERROR, ICON_SORT_SUSPENDED_GENERAL_ERROR,
ICON_SORT_SUSPENDED_SYNTAX_ERROR ICON_SORT_SUSPENDED_SYNTAX_ERROR
} from "./custom-sort/icons"; } from "./custom-sort/icons";
import {getStarredPlugin} from "./utils/StarredPluginSignature";
import {getBookmarksPlugin} from "./utils/BookmarksCorePluginSignature";
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
interface CustomSortPluginSettings { interface CustomSortPluginSettings {
additionalSortspecFile: string additionalSortspecFile: string
@ -36,7 +39,8 @@ interface CustomSortPluginSettings {
statusBarEntryEnabled: boolean statusBarEntryEnabled: boolean
notificationsEnabled: boolean notificationsEnabled: boolean
mobileNotificationsEnabled: boolean mobileNotificationsEnabled: boolean
enableAutomaticBookmarksOrderIntegration: boolean automaticBookmarksIntegration: boolean
bookmarksGroupToConsumeAsOrderingReference: string
} }
const DEFAULT_SETTINGS: CustomSortPluginSettings = { const DEFAULT_SETTINGS: CustomSortPluginSettings = {
@ -45,7 +49,8 @@ const DEFAULT_SETTINGS: CustomSortPluginSettings = {
statusBarEntryEnabled: true, statusBarEntryEnabled: true,
notificationsEnabled: true, notificationsEnabled: true,
mobileNotificationsEnabled: false, mobileNotificationsEnabled: false,
enableAutomaticBookmarksOrderIntegration: false automaticBookmarksIntegration: false,
bookmarksGroupToConsumeAsOrderingReference: 'sortspec'
} }
const SORTSPEC_FILE_NAME: string = 'sortspec.md' 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 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 // the monkey-around package doesn't export the below type
type MonkeyAroundUninstaller = () => void type MonkeyAroundUninstaller = () => void
@ -82,12 +94,13 @@ export default class CustomSortPlugin extends Plugin {
this.sortSpecCache = null this.sortSpecCache = null
const processor: SortingSpecProcessor = new SortingSpecProcessor() const processor: SortingSpecProcessor = new SortingSpecProcessor()
if (this.settings.enableAutomaticBookmarksOrderIntegration) { if (this.settings.automaticBookmarksIntegration) {
this.sortSpecCache = processor.parseSortSpecFromText( 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 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 '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('Auto injected sort spec')
console.log(this.sortSpecCache) 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() { registerCommands() {
@ -335,6 +371,10 @@ export default class CustomSortPlugin extends Plugin {
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, { const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
sort(old: any) { sort(old: any) {
return function (...args: any[]) { return function (...args: any[]) {
console.log(this)
console.log(this.fileExplorer.sortOrder)
// quick check for plugin status // quick check for plugin status
if (plugin.settings.suspended) { if (plugin.settings.suspended) {
return old.call(this, ...args); return old.call(this, ...args);
@ -349,21 +389,34 @@ export default class CustomSortPlugin extends Plugin {
const folder: TFolder = this.file const folder: TFolder = this.file
let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath?.[folder.path] let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath?.[folder.path]
sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName?.[folder.name] sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName?.[folder.name]
if (sortSpec) {
if (sortSpec.defaultOrder === CustomSortOrder.standardObsidian) { if (!sortSpec && plugin.sortSpecCache?.sortSpecByWildcard) {
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
}
} else if (plugin.sortSpecCache?.sortSpecByWildcard) {
// when no sorting spec found directly by folder path, check for wildcard-based match // when no sorting spec found directly by folder path, check for wildcard-based match
sortSpec = plugin.sortSpecCache?.sortSpecByWildcard.folderMatch(folder.path, folder.name) 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 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) { if (sortSpec) {
sortSpec.plugin = plugin console.log(`Sortspec for folder ${folder.path}`)
return folderSort.call(this, sortSpec, ...args); 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 { } else {
console.log(`NO Sortspec for folder ${folder.path}`)
return old.call(this, ...args); return old.call(this, ...args);
} }
}; };
@ -490,17 +543,67 @@ class CustomSortSettingTab extends PluginSettingTab {
await this.plugin.saveSettings(); 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) 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 // TODO: add a nice description here
.setDesc('Details TBD. TODO: add a nice description here') .setDesc(bookmarksIntegrationDescription)
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.plugin.settings.enableAutomaticBookmarksOrderIntegration) .setValue(this.plugin.settings.automaticBookmarksIntegration)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.enableAutomaticBookmarksOrderIntegration = value; this.plugin.settings.automaticBookmarksIntegration = value;
await this.plugin.saveSettings(); 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 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++) { for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
const item = collectionRef[idx]; const item = collectionRef[idx];
if (callback(item)) return; if (callback(item, groupPath)) return;
if ('group' === item.type) recursiveTraversal(item.items); 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 const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): OrderedBookmarks | undefined => {
// Initially consuming all bookmarks is ok - finally the starting point (group) should be configurable
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance): OrderedBookmarks | undefined => {
const bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]() const bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]()
if (bookmarks) { if (bookmarks) {
const orderedBookmarks: OrderedBookmarks = {} const orderedBookmarks: OrderedBookmarks = {}
let order: number = 0 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 isFile: boolean = item.type === 'file'
const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath
const isFolder: boolean = item.type === 'folder' const isFolder: boolean = item.type === 'folder'
@ -133,9 +135,9 @@ const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance): OrderedBookmarks
// undefined ==> item not found in bookmarks // undefined ==> item not found in bookmarks
// > 0 ==> item found in bookmarks at returned position // > 0 ==> item found in bookmarks at returned position
// Intentionally not returning 0 to allow simple syntax of processing the result // 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) { if (!bookmarksCache) {
bookmarksCache = getOrderedBookmarks(plugin) bookmarksCache = getOrderedBookmarks(plugin, bookmarksGroup)
bookmarksCacheTimestamp = Date.now() bookmarksCacheTimestamp = Date.now()
} }