74 integration with bookmarks core plugin (#84)
* #74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement - added new plugin setting to enable auto-integration with bookmarks - 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) - context menu for 'bookmark this' and 'bookmark+siblings' for sorting - order of bookmarked siblings reflects the current sorting in File Explorer, whatever it is (!!!) - handler for 'changed' event of the vault to reflect files/folders locations change in the bookmarks automatically, whenever applicable - adjusted behavior of by-bookmark-comparator to adhere to the multi-level sorting support implemented in #89 and merged recently to this branch. Basically, each comparator is responsible only for its own comparison and should no fallback into other default comparisons - instead it should return 0, which indicates item which are equal from the perspective of the comparator - adjusted bookmarks integration to the merged from master multi-level sorting support and its implications - fix suggested by github code check - advanced performance optimizations: bookmarks cache finetuning, switching to Obsidian sort code if the implicit bookmark-integration sorting spec is in effect for a folder and there are no bookmarked items in the folder - update of treatment of whitespace in parent path extraction: spaces allowed as leading/trailing in path elements - increased coverage of the new functionality with unit tests - basic manual tests done - next step: real-life usage tests
This commit is contained in:
parent
23b50da6cf
commit
fe4f28b46b
|
@ -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\
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"jest": "^28.1.1",
|
"jest": "^28.1.1",
|
||||||
"monkey-around": "^2.3.0",
|
"monkey-around": "^2.3.0",
|
||||||
"obsidian": "^0.15.4",
|
"obsidian": "^0.15.4",
|
||||||
|
"obsidian-1.4.11": "npm:obsidian@1.4.11",
|
||||||
"ts-jest": "^28.0.5",
|
"ts-jest": "^28.0.5",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import {
|
import {
|
||||||
FolderItemForSorting,
|
FolderItemForSorting,
|
||||||
getComparator,
|
getComparator,
|
||||||
getSorterFnFor,
|
|
||||||
getMdata,
|
|
||||||
OS_byCreatedTime,
|
OS_byCreatedTime,
|
||||||
OS_byModifiedTime,
|
OS_byModifiedTime,
|
||||||
OS_byModifiedTimeReverse, SortingLevelId
|
OS_byModifiedTimeReverse, SortingLevelId
|
||||||
|
|
|
@ -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
|
||||||
|
@ -9,6 +7,7 @@ export enum CustomSortGroupType {
|
||||||
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
|
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
|
||||||
HasMetadataField, // Notes (or folder's notes) containing a specific metadata field
|
HasMetadataField, // Notes (or folder's notes) containing a specific metadata field
|
||||||
StarredOnly,
|
StarredOnly,
|
||||||
|
BookmarkedOnly,
|
||||||
HasIcon
|
HasIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +28,9 @@ 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,
|
||||||
|
byBookmarkOrderReverse,
|
||||||
default = alphabetical
|
default = alphabetical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
import {
|
||||||
|
CustomSortGroupType,
|
||||||
|
CustomSortOrder,
|
||||||
|
CustomSortSpec
|
||||||
|
} from "./custom-sort-types";
|
||||||
|
import {
|
||||||
|
collectSortingAndGroupingTypes,
|
||||||
|
hasOnlyByBookmarkOrStandardObsidian,
|
||||||
|
HasSortingOrGrouping,
|
||||||
|
ImplicitSortspecForBookmarksIntegration
|
||||||
|
} from "./custom-sort-utils";
|
||||||
|
import {SortingSpecProcessor, SortSpecsCollection} from "./sorting-spec-processor";
|
||||||
|
|
||||||
|
type NM = number
|
||||||
|
|
||||||
|
const getHas = (gTotal?: NM, gBkmrk?: NM, gStar?: NM, gIcon?: NM, sTot?: NM, sBkmrk?: NM, sStd?: NM) => {
|
||||||
|
const has: HasSortingOrGrouping = {
|
||||||
|
grouping: {
|
||||||
|
total: gTotal ||0,
|
||||||
|
byBookmarks: gBkmrk ||0,
|
||||||
|
byStarred: gStar ||0,
|
||||||
|
byIcon: gIcon ||0
|
||||||
|
},
|
||||||
|
sorting: {
|
||||||
|
total: sTot ||0,
|
||||||
|
byBookmarks: sBkmrk ||0,
|
||||||
|
standardObsidian: sStd ||0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('hasOnlyByBookmarkOrStandardObsidian and collectSortingAndGroupingTypes', () => {
|
||||||
|
it('should handle empty spec correctly', () => {
|
||||||
|
const spec: Partial<CustomSortSpec>|undefined|null = undefined
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas()
|
||||||
|
const has = collectSortingAndGroupingTypes(spec)
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
expect(hasOnly).toBeTruthy()
|
||||||
|
})
|
||||||
|
it('should handle empty spec correctly (null variant)', () => {
|
||||||
|
const spec: Partial<CustomSortSpec>|undefined|null = null
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas()
|
||||||
|
const has = collectSortingAndGroupingTypes(spec)
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(hasOnly).toBeTruthy()
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
})
|
||||||
|
it('should handle spec with empty orders correctly', () => {
|
||||||
|
const spec: Partial<CustomSortSpec>|undefined = {
|
||||||
|
groups: [
|
||||||
|
{type: CustomSortGroupType.Outsiders, filesOnly: true},
|
||||||
|
{type: CustomSortGroupType.Outsiders}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas()
|
||||||
|
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(hasOnly).toBeTruthy()
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
})
|
||||||
|
it('should detect not matching default order', () => {
|
||||||
|
const spec: Partial<CustomSortSpec>|undefined = {
|
||||||
|
defaultOrder: CustomSortOrder.default,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.ExactName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.Outsiders,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 1, 0, 0)
|
||||||
|
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(hasOnly).toBeFalsy()
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
})
|
||||||
|
it('should detect not matching default secondary order', () => {
|
||||||
|
const spec: Partial<CustomSortSpec>|undefined = {
|
||||||
|
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||||
|
defaultSecondaryOrder: CustomSortOrder.default,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.BookmarkedOnly,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.Outsiders,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 2, 1, 0)
|
||||||
|
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(hasOnly).toBeFalsy()
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
})
|
||||||
|
it('should detect not matching order in group', () => {
|
||||||
|
const spec: Partial<CustomSortSpec>|undefined = {
|
||||||
|
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||||
|
defaultSecondaryOrder: CustomSortOrder.standardObsidian,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.ExactName,
|
||||||
|
order: CustomSortOrder.byCreatedTimeReverse
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.Outsiders,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 3, 1, 1)
|
||||||
|
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(hasOnly).toBeFalsy()
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
})
|
||||||
|
it('should detect not matching secondary order in group', () => {
|
||||||
|
const spec: Partial<CustomSortSpec>|undefined = {
|
||||||
|
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||||
|
defaultSecondaryOrder: CustomSortOrder.standardObsidian,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.ExactName,
|
||||||
|
order: CustomSortOrder.byBookmarkOrderReverse,
|
||||||
|
secondaryOrder: CustomSortOrder.standardObsidian
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.Outsiders,
|
||||||
|
order: CustomSortOrder.byBookmarkOrder,
|
||||||
|
secondaryOrder: CustomSortOrder.alphabetical
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas(1, 0, 0, 0, 6, 3, 2)
|
||||||
|
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(hasOnly).toBeFalsy()
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
})
|
||||||
|
it('should detect matching orders at all levels', () => {
|
||||||
|
const spec: Partial<CustomSortSpec>|undefined = {
|
||||||
|
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||||
|
defaultSecondaryOrder: CustomSortOrder.standardObsidian,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.BookmarkedOnly,
|
||||||
|
order: CustomSortOrder.byBookmarkOrderReverse,
|
||||||
|
secondaryOrder: CustomSortOrder.standardObsidian
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.Outsiders,
|
||||||
|
order: CustomSortOrder.byBookmarkOrder,
|
||||||
|
secondaryOrder: CustomSortOrder.byBookmarkOrderReverse
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 6, 4, 2)
|
||||||
|
const has = collectSortingAndGroupingTypes(spec as CustomSortSpec)
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(hasOnly).toBeTruthy()
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ImplicitSortspecForBookmarksIntegration', () => {
|
||||||
|
it('should correctly be recognized as only bookmark and obsidian standard', () => {
|
||||||
|
const processor: SortingSpecProcessor = new SortingSpecProcessor();
|
||||||
|
const inputTxtArr: Array<string> = ImplicitSortspecForBookmarksIntegration.replace(/\t/gi, '').split('\n')
|
||||||
|
const spec: SortSpecsCollection|null|undefined = processor.parseSortSpecFromText(
|
||||||
|
inputTxtArr,
|
||||||
|
'mock-folder',
|
||||||
|
'custom-name-note.md',
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
const expectedHas: HasSortingOrGrouping = getHas(1, 1, 0, 0, 2, 1, 1)
|
||||||
|
const has = collectSortingAndGroupingTypes(spec?.sortSpecByPath!['/'])
|
||||||
|
const hasOnly = hasOnlyByBookmarkOrStandardObsidian(has)
|
||||||
|
expect(hasOnly).toBeTruthy()
|
||||||
|
expect(has).toEqual(expectedHas)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO - czy tamto sprawdzanie dla itemów w rootowym filderze hasBookmarkInFolder dobrze zadziala
|
|
@ -0,0 +1,80 @@
|
||||||
|
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
||||||
|
|
||||||
|
// Put here to allow unit tests coverage of this specific implicit sorting spec
|
||||||
|
export const ImplicitSortspecForBookmarksIntegration: string = `
|
||||||
|
target-folder: /*
|
||||||
|
bookmarked:
|
||||||
|
< by-bookmarks-order
|
||||||
|
sorting: standard
|
||||||
|
`
|
||||||
|
|
||||||
|
export interface HasSortingTypes {
|
||||||
|
byBookmarks: number
|
||||||
|
standardObsidian: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HasGroupingTypes {
|
||||||
|
byBookmarks: number
|
||||||
|
byStarred: number
|
||||||
|
byIcon: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HasSortingOrGrouping {
|
||||||
|
sorting: HasSortingTypes
|
||||||
|
grouping: HasGroupingTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkByBookmark = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
|
||||||
|
groupType === CustomSortGroupType.BookmarkedOnly && has.grouping.byBookmarks++;
|
||||||
|
(order === CustomSortOrder.byBookmarkOrder || order === CustomSortOrder.byBookmarkOrderReverse) && has.sorting.byBookmarks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkByStarred = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
|
||||||
|
groupType === CustomSortGroupType.StarredOnly && has.grouping.byStarred++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkByIcon = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
|
||||||
|
groupType === CustomSortGroupType.HasIcon && has.grouping.byIcon++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkStandardObsidian = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
|
||||||
|
order === CustomSortOrder.standardObsidian && has.sorting.standardObsidian++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const doCheck = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType) => {
|
||||||
|
checkByBookmark(has, order, groupType)
|
||||||
|
checkByStarred(has, order, groupType)
|
||||||
|
checkByIcon(has, order, groupType)
|
||||||
|
checkStandardObsidian(has, order, groupType)
|
||||||
|
|
||||||
|
order !== undefined && has.sorting.total++
|
||||||
|
groupType !== undefined && groupType !== CustomSortGroupType.Outsiders && has.grouping.total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const collectSortingAndGroupingTypes = (sortSpec?: CustomSortSpec|null): HasSortingOrGrouping => {
|
||||||
|
const has: HasSortingOrGrouping = {
|
||||||
|
grouping: {
|
||||||
|
byIcon: 0, byStarred: 0, byBookmarks: 0, total: 0
|
||||||
|
},
|
||||||
|
sorting: {
|
||||||
|
byBookmarks: 0, standardObsidian: 0, total: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sortSpec) return has
|
||||||
|
doCheck(has, sortSpec.defaultOrder)
|
||||||
|
doCheck(has, sortSpec.defaultSecondaryOrder)
|
||||||
|
if (sortSpec.groups) {
|
||||||
|
for (let group of sortSpec.groups) {
|
||||||
|
doCheck(has, group.order, group.type)
|
||||||
|
doCheck(has, group.secondaryOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasOnlyByBookmarkOrStandardObsidian = (has: HasSortingOrGrouping): boolean => {
|
||||||
|
return has.sorting.total === has.sorting.standardObsidian + has.sorting.byBookmarks &&
|
||||||
|
has.grouping.total === has.grouping.byBookmarks
|
||||||
|
}
|
|
@ -8,13 +8,25 @@ import {
|
||||||
FolderItemForSorting,
|
FolderItemForSorting,
|
||||||
getSorterFnFor,
|
getSorterFnFor,
|
||||||
matchGroupRegex,
|
matchGroupRegex,
|
||||||
ProcessingContext,
|
ProcessingContext,
|
||||||
|
sorterByBookmarkOrder,
|
||||||
sorterByMetadataField,
|
sorterByMetadataField,
|
||||||
SorterFn
|
SorterFn
|
||||||
} from './custom-sort';
|
} from './custom-sort';
|
||||||
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types';
|
import {
|
||||||
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
|
CustomSortGroupType,
|
||||||
import {findStarredFile_pathParam, Starred_PluginInstance} from "../utils/StarredPluginSignature";
|
CustomSortOrder,
|
||||||
|
CustomSortSpec,
|
||||||
|
RegExpSpec
|
||||||
|
} from './custom-sort-types';
|
||||||
|
import {
|
||||||
|
CompoundDashNumberNormalizerFn,
|
||||||
|
CompoundDotRomanNumberNormalizerFn
|
||||||
|
} from "./sorting-spec-processor";
|
||||||
|
import {
|
||||||
|
findStarredFile_pathParam,
|
||||||
|
Starred_PluginInstance
|
||||||
|
} from "../utils/StarredPluginSignature";
|
||||||
import {
|
import {
|
||||||
ObsidianIconFolder_PluginInstance,
|
ObsidianIconFolder_PluginInstance,
|
||||||
ObsidianIconFolderPlugin_Data
|
ObsidianIconFolderPlugin_Data
|
||||||
|
@ -2854,3 +2866,39 @@ describe('sorterByMetadataField', () => {
|
||||||
expect(result).toBe(order)
|
expect(result).toBe(order)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('sorterByBookmarkOrder', () => {
|
||||||
|
it.each([
|
||||||
|
[true,10,20,-1, 'a', 'a'],
|
||||||
|
[true,20,10,1, 'b', 'b'],
|
||||||
|
[true,30,30,0, 'c', 'c'], // not possible in reality - each bookmark order is unique by definition - covered for clarity
|
||||||
|
[true,1,1,0, 'd', 'e'], // ----//----
|
||||||
|
[true,2,2,0, 'e', 'd'], // ----//----
|
||||||
|
[true,3,undefined,-1, 'a','a'],
|
||||||
|
[true,undefined,4,1, 'b','b'],
|
||||||
|
[true,undefined,undefined,0, 'a','a'],
|
||||||
|
[true,undefined,undefined,0, 'a','b'],
|
||||||
|
[true,undefined,undefined,0, 'd','c'],
|
||||||
|
[false,10,20,1, 'a', 'a'],
|
||||||
|
[false,20,10,-1, 'b', 'b'],
|
||||||
|
[false,30,30,0, 'c', 'c'], // not possible in reality - each bookmark order is unique by definition - covered for clarity
|
||||||
|
[false,1,1,0, 'd', 'e'], // ------//-----
|
||||||
|
[false,2,2,0, 'e', 'd'], // ------//-----
|
||||||
|
[false,3,undefined,1, 'a','a'],
|
||||||
|
[false,undefined,4,-1, 'b','b'],
|
||||||
|
[false,undefined,undefined,0, 'a','a'],
|
||||||
|
[false,undefined,undefined,0, 'a','b'],
|
||||||
|
[false,undefined,undefined,0, 'd','c'],
|
||||||
|
|
||||||
|
])('straight order %s, comparing %s and %s should return %s for sortStrings %s and %s',
|
||||||
|
(straight: boolean, bookmarkA: number|undefined, bookmarkB: number|undefined, order: number, sortStringA: string, sortStringB) => {
|
||||||
|
const sorterFn = sorterByBookmarkOrder(!straight, false)
|
||||||
|
const itemA: Partial<FolderItemForSorting> = {bookmarkedIdx: bookmarkA, sortString: sortStringA}
|
||||||
|
const itemB: Partial<FolderItemForSorting> = {bookmarkedIdx: bookmarkB, sortString: sortStringB}
|
||||||
|
const result = sorterFn(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||||
|
const normalizedResult = result < 0 ? -1 : ((result > 0) ? 1 : result)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(normalizedResult).toBe(order)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -9,11 +9,11 @@ import {
|
||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
import {
|
import {
|
||||||
determineStarredStatusOf,
|
determineStarredStatusOf,
|
||||||
Starred_PluginInstance,
|
Starred_PluginInstance
|
||||||
} from '../utils/StarredPluginSignature';
|
} from '../utils/StarredPluginSignature';
|
||||||
import {
|
import {
|
||||||
determineIconOf,
|
determineIconOf,
|
||||||
ObsidianIconFolder_PluginInstance,
|
ObsidianIconFolder_PluginInstance
|
||||||
} from '../utils/ObsidianIconFolderPluginSignature'
|
} from '../utils/ObsidianIconFolderPluginSignature'
|
||||||
import {
|
import {
|
||||||
CustomSortGroup,
|
CustomSortGroup,
|
||||||
|
@ -30,13 +30,16 @@ import {
|
||||||
import {
|
import {
|
||||||
expandMacros
|
expandMacros
|
||||||
} from "./macros";
|
} from "./macros";
|
||||||
|
import {
|
||||||
|
BookmarksPluginInterface
|
||||||
|
} from "../utils/BookmarksCorePluginSignature";
|
||||||
|
|
||||||
export interface ProcessingContext {
|
export interface ProcessingContext {
|
||||||
// For internal transient use
|
// For internal transient use
|
||||||
plugin?: Plugin // to hand over the access to App instance to the sorting engine
|
plugin?: Plugin // to hand over the access to App instance to the sorting engine
|
||||||
_mCache?: MetadataCache
|
_mCache?: MetadataCache
|
||||||
starredPluginInstance?: Starred_PluginInstance
|
starredPluginInstance?: Starred_PluginInstance
|
||||||
|
bookmarksPluginInstance?: BookmarksPluginInterface,
|
||||||
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
|
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +67,7 @@ export interface FolderItemForSorting {
|
||||||
mtime: number // for a file mtime is obvious, for a folder = date of most recently modified child file
|
mtime: number // for a file mtime is obvious, for a folder = date of most recently modified child file
|
||||||
isFolder: boolean
|
isFolder: boolean
|
||||||
folder?: TFolder
|
folder?: TFolder
|
||||||
|
bookmarkedIdx?: number // derived from Bookmarks core plugin position
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SortingLevelId {
|
export enum SortingLevelId {
|
||||||
|
@ -115,6 +119,23 @@ export const sorterByMetadataField = (reverseOrder?: boolean, trueAlphabetical?:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sorterByBookmarkOrder:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
|
||||||
|
return (a: FolderItemForSorting, b: FolderItemForSorting) => {
|
||||||
|
if (reverseOrder) {
|
||||||
|
[a, b] = [b, a]
|
||||||
|
}
|
||||||
|
if (a.bookmarkedIdx && b.bookmarkedIdx) {
|
||||||
|
// By design the bookmark idx is unique per each item, so no need for secondary sorting if they are equal
|
||||||
|
return a.bookmarkedIdx - b.bookmarkedIdx
|
||||||
|
}
|
||||||
|
// Item with bookmark order goes before the w/o bookmark info
|
||||||
|
if (a.bookmarkedIdx) return -1
|
||||||
|
if (b.bookmarkedIdx) return 1
|
||||||
|
|
||||||
|
return EQUAL_OR_UNCOMPARABLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
const 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),
|
||||||
|
@ -132,6 +153,8 @@ const Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forPrimary),
|
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical, SortingLevelId.forPrimary),
|
||||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forPrimary),
|
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, !TrueAlphabetical, SortingLevelId.forPrimary),
|
||||||
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary),
|
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary),
|
||||||
|
[CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
|
||||||
|
[CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder),
|
||||||
|
|
||||||
// This is a fallback entry which should not be used - the getSorterFor() function below should protect against it
|
// 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),
|
||||||
|
@ -294,6 +317,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
let groupIdx: number
|
let groupIdx: number
|
||||||
let determined: boolean = false
|
let determined: boolean = false
|
||||||
let derivedText: string | null | undefined
|
let derivedText: string | null | undefined
|
||||||
|
let bookmarkedIdx: number | undefined
|
||||||
|
|
||||||
const aFolder: boolean = isFolder(entry)
|
const aFolder: boolean = isFolder(entry)
|
||||||
const aFile: boolean = !aFolder
|
const aFile: boolean = !aFolder
|
||||||
|
@ -402,6 +426,14 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case CustomSortGroupType.BookmarkedOnly:
|
||||||
|
if (ctx?.bookmarksPluginInstance) {
|
||||||
|
const bookmarkOrder: number | undefined = ctx?.bookmarksPluginInstance.determineBookmarkOrder(entry.path)
|
||||||
|
if (bookmarkOrder) { // safe ==> orders intentionally start from 1
|
||||||
|
determined = true
|
||||||
|
bookmarkedIdx = bookmarkOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
case CustomSortGroupType.HasIcon:
|
case CustomSortGroupType.HasIcon:
|
||||||
if(ctx?.iconFolderPluginInstance) {
|
if(ctx?.iconFolderPluginInstance) {
|
||||||
let iconName: string | undefined = determineIconOf(entry, ctx.iconFolderPluginInstance)
|
let iconName: string | undefined = determineIconOf(entry, ctx.iconFolderPluginInstance)
|
||||||
|
@ -486,7 +518,8 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
folder: aFolder ? (entry as TFolder) : undefined,
|
folder: aFolder ? (entry as TFolder) : undefined,
|
||||||
path: entry.path,
|
path: entry.path,
|
||||||
ctime: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME,
|
ctime: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME,
|
||||||
mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME
|
mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME,
|
||||||
|
bookmarkedIdx: bookmarkedIdx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,6 +536,17 @@ export const sortOrderNeedsFolderDates = (order: CustomSortOrder | undefined, se
|
||||||
|| SortOrderRequiringFolderDate.has(secondary ?? CustomSortOrder.standardObsidian)
|
|| SortOrderRequiringFolderDate.has(secondary ?? CustomSortOrder.standardObsidian)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SortOrderRequiringBookmarksOrder = new Set<CustomSortOrder>([
|
||||||
|
CustomSortOrder.byBookmarkOrder,
|
||||||
|
CustomSortOrder.byBookmarkOrderReverse
|
||||||
|
])
|
||||||
|
|
||||||
|
export const sortOrderNeedsBookmarksOrder = (order: CustomSortOrder | undefined, secondary?: CustomSortOrder): boolean => {
|
||||||
|
// The CustomSortOrder.standardObsidian used as default because it doesn't require bookmarks order
|
||||||
|
return SortOrderRequiringBookmarksOrder.has(order ?? CustomSortOrder.standardObsidian)
|
||||||
|
|| SortOrderRequiringBookmarksOrder.has(secondary ?? CustomSortOrder.standardObsidian)
|
||||||
|
}
|
||||||
|
|
||||||
// Syntax sugar for readability
|
// Syntax sugar for readability
|
||||||
export type ModifiedTime = number
|
export type ModifiedTime = number
|
||||||
export type CreatedTime = number
|
export type CreatedTime = number
|
||||||
|
@ -546,13 +590,33 @@ 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: BookmarksPluginInterface) => {
|
||||||
|
if (!plugin) return
|
||||||
|
|
||||||
|
folderItems.forEach((item) => {
|
||||||
|
const folderDefaultSortRequiresBookmarksOrder: boolean = !!(sortingSpec.defaultOrder && sortOrderNeedsBookmarksOrder(sortingSpec.defaultOrder, sortingSpec.defaultSecondaryOrder))
|
||||||
|
let groupSortRequiresBookmarksOrder: boolean = false
|
||||||
|
if (!folderDefaultSortRequiresBookmarksOrder) {
|
||||||
|
const groupIdx: number | undefined = item.groupIdx
|
||||||
|
if (groupIdx !== undefined) {
|
||||||
|
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
|
||||||
|
const groupSecondaryOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].secondaryOrder
|
||||||
|
groupSortRequiresBookmarksOrder = sortOrderNeedsBookmarksOrder(groupOrder, groupSecondaryOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (folderDefaultSortRequiresBookmarksOrder || groupSortRequiresBookmarksOrder) {
|
||||||
|
item.bookmarkedIdx = plugin.determineBookmarkOrder(item.path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const folderSort = function (sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
|
export const folderSort = function (sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
|
||||||
let fileExplorer = this.fileExplorer
|
let fileExplorer = this.fileExplorer
|
||||||
|
|
||||||
// shallow copy of groups
|
// shallow copy of groups and expand folder-specific macros on them
|
||||||
sortingSpec.groupsShadow = sortingSpec.groups?.map((group) => Object.assign({} as CustomSortGroup, group))
|
sortingSpec.groupsShadow = sortingSpec.groups?.map((group) => Object.assign({} as CustomSortGroup, group))
|
||||||
|
|
||||||
// expand folder-specific macros
|
|
||||||
const parentFolderName: string|undefined = this.file.name
|
const parentFolderName: string|undefined = this.file.name
|
||||||
expandMacros(sortingSpec, parentFolderName)
|
expandMacros(sortingSpec, parentFolderName)
|
||||||
|
|
||||||
|
@ -570,6 +634,10 @@ export const folderSort = function (sortingSpec: CustomSortSpec, ctx: Processing
|
||||||
// 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 (ctx.bookmarksPluginInstance) {
|
||||||
|
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPluginInstance)
|
||||||
|
}
|
||||||
|
|
||||||
const comparator: SorterFn = getComparator(sortingSpec, fileExplorer.sortOrder)
|
const comparator: SorterFn = getComparator(sortingSpec, fileExplorer.sortOrder)
|
||||||
|
|
||||||
folderItems.sort(comparator)
|
folderItems.sort(comparator)
|
||||||
|
@ -583,3 +651,41 @@ export const folderSort = function (sortingSpec: CustomSortSpec, ctx: Processing
|
||||||
this.children = items;
|
this.children = items;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Returns a sorted copy of the input array, intentionally to keep it intact
|
||||||
|
export const sortFolderItemsForBookmarking = function (folder: TFolder, items: Array<TAbstractFile>, sortingSpec: CustomSortSpec|null|undefined, ctx: ProcessingContext, uiSortOrder: string): Array<TAbstractFile> {
|
||||||
|
if (sortingSpec) {
|
||||||
|
const folderItemsByPath: { [key: string]: TAbstractFile } = {}
|
||||||
|
|
||||||
|
// shallow copy of groups and expand folder-specific macros on them
|
||||||
|
sortingSpec.groupsShadow = sortingSpec.groups?.map((group) => Object.assign({} as CustomSortGroup, group))
|
||||||
|
const parentFolderName: string|undefined = folder.name
|
||||||
|
expandMacros(sortingSpec, parentFolderName)
|
||||||
|
|
||||||
|
const folderItems: Array<FolderItemForSorting> = items.map((entry: TFile | TFolder) => {
|
||||||
|
folderItemsByPath[entry.path] = entry
|
||||||
|
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 (ctx.bookmarksPluginInstance) {
|
||||||
|
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPluginInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
const comparator: SorterFn = getComparator(sortingSpec, uiSortOrder)
|
||||||
|
|
||||||
|
folderItems.sort(comparator)
|
||||||
|
|
||||||
|
const sortedItems: Array<TAbstractFile> = folderItems.map((entry) => folderItemsByPath[entry.path])
|
||||||
|
|
||||||
|
return sortedItems
|
||||||
|
} else { // No custom sorting or the custom sort disabled - apply standard Obsidian sorting (internally 1:1 recreated implementation)
|
||||||
|
const folderItems: Array<TAbstractFile> = items.map((entry: TFile | TFolder) => entry)
|
||||||
|
const plainSorterFn: PlainSorterFn = StandardPlainObsidianComparator(uiSortOrder)
|
||||||
|
folderItems.sort(plainSorterFn)
|
||||||
|
return folderItems
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import {FolderWildcardMatching} from './folder-matching-rules'
|
import {
|
||||||
|
FolderWildcardMatching
|
||||||
|
} from './folder-matching-rules'
|
||||||
|
|
||||||
type SortingSpec = string
|
type SortingSpec = string
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import {expandMacros, expandMacrosInString} from "./macros";
|
import {
|
||||||
|
expandMacros,
|
||||||
|
expandMacrosInString
|
||||||
|
} from "./macros";
|
||||||
import * as MacrosModule from './macros'
|
import * as MacrosModule from './macros'
|
||||||
import {CustomSortGroup, CustomSortSpec} from "./custom-sort-types";
|
import {
|
||||||
|
CustomSortGroup,
|
||||||
|
CustomSortSpec
|
||||||
|
} from "./custom-sort-types";
|
||||||
|
|
||||||
describe('expandMacrosInString', () => {
|
describe('expandMacrosInString', () => {
|
||||||
it.each([
|
it.each([
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
WordInASCIIRegexStr,
|
WordInASCIIRegexStr,
|
||||||
WordInAnyLanguageRegexStr
|
WordInAnyLanguageRegexStr
|
||||||
} from "./matchers";
|
} from "./matchers";
|
||||||
import {SortingSpecProcessor} from "./sorting-spec-processor";
|
|
||||||
|
|
||||||
describe('Plain numbers regexp', () => {
|
describe('Plain numbers regexp', () => {
|
||||||
let regexp: RegExp;
|
let regexp: RegExp;
|
||||||
|
|
|
@ -14,8 +14,16 @@ import {
|
||||||
RomanNumberNormalizerFn,
|
RomanNumberNormalizerFn,
|
||||||
SortingSpecProcessor
|
SortingSpecProcessor
|
||||||
} from "./sorting-spec-processor"
|
} from "./sorting-spec-processor"
|
||||||
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types";
|
import {
|
||||||
import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules";
|
CustomSortGroupType,
|
||||||
|
CustomSortOrder,
|
||||||
|
CustomSortSpec,
|
||||||
|
IdentityNormalizerFn
|
||||||
|
} from "./custom-sort-types";
|
||||||
|
import {
|
||||||
|
FolderMatchingRegexp,
|
||||||
|
FolderMatchingTreeNode
|
||||||
|
} from "./folder-matching-rules";
|
||||||
|
|
||||||
const txtInputExampleA: string = `
|
const txtInputExampleA: string = `
|
||||||
order-asc: a-z
|
order-asc: a-z
|
||||||
|
@ -37,6 +45,13 @@ starred:
|
||||||
/:files starred:
|
/:files starred:
|
||||||
/folders starred:
|
/folders starred:
|
||||||
|
|
||||||
|
:::: folder of bookmarks
|
||||||
|
< by-bookmarks-order
|
||||||
|
/: bookmarked:
|
||||||
|
< by-bookmarks-order
|
||||||
|
/ Abc
|
||||||
|
> by-bookmarks-order
|
||||||
|
|
||||||
:::: Conceptual model
|
:::: Conceptual model
|
||||||
/: Entities
|
/: Entities
|
||||||
%
|
%
|
||||||
|
@ -95,6 +110,13 @@ target-folder: tricky folder 2
|
||||||
/:files starred:
|
/:files starred:
|
||||||
/folders starred:
|
/folders starred:
|
||||||
|
|
||||||
|
target-folder: folder of bookmarks
|
||||||
|
order-asc: by-bookmarks-order
|
||||||
|
/:files bookmarked:
|
||||||
|
order-asc: by-bookmarks-order
|
||||||
|
/folders Abc
|
||||||
|
order-desc: by-bookmarks-order
|
||||||
|
|
||||||
:::: Conceptual model
|
:::: Conceptual model
|
||||||
/:files Entities
|
/:files Entities
|
||||||
%
|
%
|
||||||
|
@ -194,6 +216,29 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
|
||||||
'tricky folder 2'
|
'tricky folder 2'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"folder of bookmarks": {
|
||||||
|
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
filesOnly: true,
|
||||||
|
order: CustomSortOrder.byBookmarkOrder,
|
||||||
|
type: CustomSortGroupType.BookmarkedOnly
|
||||||
|
},
|
||||||
|
{
|
||||||
|
exactText: "Abc",
|
||||||
|
foldersOnly: true,
|
||||||
|
order: CustomSortOrder.byBookmarkOrderReverse,
|
||||||
|
type: CustomSortGroupType.ExactName
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}
|
||||||
|
],
|
||||||
|
outsidersGroupIdx: 2,
|
||||||
|
targetFoldersPaths: [
|
||||||
|
"folder of bookmarks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"Conceptual model": {
|
"Conceptual model": {
|
||||||
groups: [{
|
groups: [{
|
||||||
exactText: "Entities",
|
exactText: "Entities",
|
||||||
|
|
|
@ -120,6 +120,7 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
||||||
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
|
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
|
||||||
'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||||
'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian},
|
||||||
|
'by-bookmarks-order': {asc: CustomSortOrder.byBookmarkOrder, desc: CustomSortOrder.byBookmarkOrderReverse},
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderByMetadataLexeme: string = 'by-metadata:'
|
const OrderByMetadataLexeme: string = 'by-metadata:'
|
||||||
|
@ -241,6 +242,8 @@ const HideItemVerboseLexeme: string = '/--hide:'
|
||||||
|
|
||||||
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
|
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
|
||||||
|
|
||||||
|
const BookmarkedItemIndicatorLexeme: string = 'bookmarked:'
|
||||||
|
|
||||||
const StarredItemsIndicatorLexeme: string = 'starred:'
|
const StarredItemsIndicatorLexeme: string = 'starred:'
|
||||||
|
|
||||||
const IconIndicatorLexeme: string = 'with-icon:'
|
const IconIndicatorLexeme: string = 'with-icon:'
|
||||||
|
@ -1600,6 +1603,13 @@ export class SortingSpecProcessor {
|
||||||
foldersOnly: spec.foldersOnly,
|
foldersOnly: spec.foldersOnly,
|
||||||
matchFilenameWithExt: spec.matchFilenameWithExt
|
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||||
}
|
}
|
||||||
|
} else if (theOnly.startsWith(BookmarkedItemIndicatorLexeme)) {
|
||||||
|
return {
|
||||||
|
type: CustomSortGroupType.BookmarkedOnly,
|
||||||
|
filesOnly: spec.filesOnly,
|
||||||
|
foldersOnly: spec.foldersOnly,
|
||||||
|
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||||
|
}
|
||||||
} else if (theOnly.startsWith(IconIndicatorLexeme)) {
|
} else if (theOnly.startsWith(IconIndicatorLexeme)) {
|
||||||
const iconName: string | undefined = extractIdentifier(theOnly.substring(IconIndicatorLexeme.length))
|
const iconName: string | undefined = extractIdentifier(theOnly.substring(IconIndicatorLexeme.length))
|
||||||
return {
|
return {
|
||||||
|
|
290
src/main.ts
290
src/main.ts
|
@ -1,27 +1,38 @@
|
||||||
import {
|
import {
|
||||||
|
apiVersion,
|
||||||
App,
|
App,
|
||||||
FileExplorerView,
|
FileExplorerView,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
MetadataCache,
|
MetadataCache,
|
||||||
Notice,
|
|
||||||
normalizePath,
|
normalizePath,
|
||||||
|
Notice,
|
||||||
Platform,
|
Platform,
|
||||||
Plugin,
|
Plugin,
|
||||||
PluginSettingTab,
|
PluginSettingTab,
|
||||||
|
requireApiVersion,
|
||||||
sanitizeHTMLToDom,
|
sanitizeHTMLToDom,
|
||||||
setIcon,
|
setIcon,
|
||||||
Setting,
|
Setting,
|
||||||
TAbstractFile,
|
TAbstractFile,
|
||||||
TFile,
|
TFile,
|
||||||
TFolder,
|
TFolder,
|
||||||
Vault
|
Vault, WorkspaceLeaf
|
||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
import {around} from 'monkey-around';
|
import {around} from 'monkey-around';
|
||||||
import {
|
import {
|
||||||
folderSort,
|
folderSort,
|
||||||
|
ObsidianStandardDefaultSortingName,
|
||||||
ProcessingContext,
|
ProcessingContext,
|
||||||
|
sortFolderItemsForBookmarking
|
||||||
} from './custom-sort/custom-sort';
|
} from './custom-sort/custom-sort';
|
||||||
import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
|
import {
|
||||||
import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
|
SortingSpecProcessor,
|
||||||
|
SortSpecsCollection
|
||||||
|
} from './custom-sort/sorting-spec-processor';
|
||||||
|
import {
|
||||||
|
CustomSortSpec
|
||||||
|
} from './custom-sort/custom-sort-types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addIcons,
|
addIcons,
|
||||||
|
@ -33,9 +44,19 @@ import {
|
||||||
ICON_SORT_SUSPENDED_SYNTAX_ERROR
|
ICON_SORT_SUSPENDED_SYNTAX_ERROR
|
||||||
} from "./custom-sort/icons";
|
} from "./custom-sort/icons";
|
||||||
import {getStarredPlugin} from "./utils/StarredPluginSignature";
|
import {getStarredPlugin} from "./utils/StarredPluginSignature";
|
||||||
|
import {
|
||||||
|
BookmarksPluginInterface,
|
||||||
|
getBookmarksPlugin,
|
||||||
|
groupNameForPath
|
||||||
|
} from "./utils/BookmarksCorePluginSignature";
|
||||||
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
|
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
|
||||||
import {lastPathComponent} from "./utils/utils";
|
import {lastPathComponent} from "./utils/utils";
|
||||||
|
import {
|
||||||
|
collectSortingAndGroupingTypes,
|
||||||
|
hasOnlyByBookmarkOrStandardObsidian,
|
||||||
|
HasSortingOrGrouping,
|
||||||
|
ImplicitSortspecForBookmarksIntegration
|
||||||
|
} from "./custom-sort/custom-sort-utils";
|
||||||
|
|
||||||
interface CustomSortPluginSettings {
|
interface CustomSortPluginSettings {
|
||||||
additionalSortspecFile: string
|
additionalSortspecFile: string
|
||||||
|
@ -43,6 +64,9 @@ interface CustomSortPluginSettings {
|
||||||
statusBarEntryEnabled: boolean
|
statusBarEntryEnabled: boolean
|
||||||
notificationsEnabled: boolean
|
notificationsEnabled: boolean
|
||||||
mobileNotificationsEnabled: boolean
|
mobileNotificationsEnabled: boolean
|
||||||
|
automaticBookmarksIntegration: boolean
|
||||||
|
bookmarksContextMenus: boolean
|
||||||
|
bookmarksGroupToConsumeAsOrderingReference: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: CustomSortPluginSettings = {
|
const DEFAULT_SETTINGS: CustomSortPluginSettings = {
|
||||||
|
@ -50,7 +74,16 @@ const DEFAULT_SETTINGS: CustomSortPluginSettings = {
|
||||||
suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install
|
suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install
|
||||||
statusBarEntryEnabled: true,
|
statusBarEntryEnabled: true,
|
||||||
notificationsEnabled: true,
|
notificationsEnabled: true,
|
||||||
mobileNotificationsEnabled: false
|
mobileNotificationsEnabled: false,
|
||||||
|
automaticBookmarksIntegration: false,
|
||||||
|
bookmarksContextMenus: false,
|
||||||
|
bookmarksGroupToConsumeAsOrderingReference: 'sortspec'
|
||||||
|
}
|
||||||
|
|
||||||
|
// On API 1.2.x+ enable the bookmarks integration by default
|
||||||
|
const DEFAULT_SETTING_FOR_1_2_0_UP: Partial<CustomSortPluginSettings> = {
|
||||||
|
automaticBookmarksIntegration: true,
|
||||||
|
bookmarksContextMenus: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const SORTSPEC_FILE_NAME: string = 'sortspec.md'
|
const SORTSPEC_FILE_NAME: string = 'sortspec.md'
|
||||||
|
@ -87,6 +120,16 @@ 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.automaticBookmarksIntegration) {
|
||||||
|
this.sortSpecCache = processor.parseSortSpecFromText(
|
||||||
|
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,
|
||||||
|
true // Implicit sorting spec generation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Vault.recurseChildren(app.vault.getRoot(), (file: TAbstractFile) => {
|
Vault.recurseChildren(app.vault.getRoot(), (file: TAbstractFile) => {
|
||||||
if (failed) return
|
if (failed) return
|
||||||
if (file instanceof TFile) {
|
if (file instanceof TFile) {
|
||||||
|
@ -198,6 +241,12 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Syntax sugar
|
||||||
|
const ForceFlushCache = true
|
||||||
|
if (!this.settings.suspended) {
|
||||||
|
getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference, ForceFlushCache)
|
||||||
|
}
|
||||||
|
|
||||||
if (fileExplorerView) {
|
if (fileExplorerView) {
|
||||||
if (this.fileExplorerFolderPatched) {
|
if (this.fileExplorerFolderPatched) {
|
||||||
fileExplorerView.requestSort();
|
fileExplorerView.requestSort();
|
||||||
|
@ -290,6 +339,131 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.registerEvent(
|
||||||
|
app.workspace.on("file-menu", (menu: Menu, file: TAbstractFile, source: string, leaf?: WorkspaceLeaf) => {
|
||||||
|
if (!this.settings.bookmarksContextMenus) return; // Don't show the context menus at all
|
||||||
|
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (!bookmarksPlugin) return; // Don't show context menu if bookmarks plugin not available and not enabled
|
||||||
|
|
||||||
|
const bookmarkThisMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: bookmark for sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
bookmarksPlugin.bookmarkFolderItem(file)
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const unbookmarkThisMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: UNbookmark from sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
bookmarksPlugin.unbookmarkFolderItem(file)
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const bookmarkAllMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: bookmark+siblings for sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
const orderedChildren: Array<TAbstractFile> = plugin.orderedFolderItemsForBookmarking(file.parent, bookmarksPlugin)
|
||||||
|
bookmarksPlugin.bookmarkSiblings(orderedChildren)
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const unbookmarkAllMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: UNbookmark+all siblings from sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
const orderedChildren: Array<TAbstractFile> = file.parent.children.map((entry: TFile | TFolder) => entry)
|
||||||
|
bookmarksPlugin.unbookmarkSiblings(orderedChildren)
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemAlreadyBookmarkedForSorting: boolean = bookmarksPlugin.isBookmarkedForSorting(file)
|
||||||
|
if (!itemAlreadyBookmarkedForSorting) {
|
||||||
|
menu.addItem(bookmarkThisMenuItem)
|
||||||
|
} else {
|
||||||
|
menu.addItem(unbookmarkThisMenuItem)
|
||||||
|
}
|
||||||
|
menu.addItem(bookmarkAllMenuItem)
|
||||||
|
menu.addItem(unbookmarkAllMenuItem)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (requireApiVersion('1.4.11')) {
|
||||||
|
this.registerEvent(
|
||||||
|
// "files-menu" event was exposed in 1.4.11
|
||||||
|
// @ts-ignore
|
||||||
|
app.workspace.on("files-menu", (menu: Menu, files: TAbstractFile[], source: string, leaf?: WorkspaceLeaf) => {
|
||||||
|
if (!this.settings.bookmarksContextMenus) return; // Don't show the context menus at all
|
||||||
|
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (!bookmarksPlugin) return; // Don't show context menu if bookmarks plugin not available and not enabled
|
||||||
|
|
||||||
|
const bookmarkSelectedMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: bookmark selected for sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
files.forEach((file) => {
|
||||||
|
bookmarksPlugin.bookmarkFolderItem(file)
|
||||||
|
})
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const unbookmarkSelectedMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: UNbookmark selected from sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
files.forEach((file) => {
|
||||||
|
bookmarksPlugin.unbookmarkFolderItem(file)
|
||||||
|
})
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.addItem(bookmarkSelectedMenuItem)
|
||||||
|
menu.addItem(unbookmarkSelectedMenuItem)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerEvent(
|
||||||
|
app.vault.on("rename", (file: TAbstractFile, oldPath: string) => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
bookmarksPlugin.updateSortingBookmarksAfterItemRenamed(file, oldPath)
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app.vault.on("delete", (file: TAbstractFile) => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
bookmarksPlugin.updateSortingBookmarksAfterItemDeleted(file)
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCommands() {
|
registerCommands() {
|
||||||
|
@ -328,12 +502,12 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
return sortSpec
|
return sortSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
createProcessingContextForSorting(): ProcessingContext {
|
createProcessingContextForSorting(has: HasSortingOrGrouping): ProcessingContext {
|
||||||
const ctx: ProcessingContext = {
|
const ctx: ProcessingContext = {
|
||||||
_mCache: app.metadataCache,
|
_mCache: app.metadataCache,
|
||||||
starredPluginInstance: getStarredPlugin(),
|
starredPluginInstance: has.grouping.byStarred ? getStarredPlugin() : undefined,
|
||||||
|
bookmarksPluginInstance: has.grouping.byBookmarks || has.sorting.byBookmarks ? getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference, false, true) : undefined,
|
||||||
iconFolderPluginInstance: getIconFolderPlugin(),
|
iconFolderPluginInstance: has.grouping.byIcon ? getIconFolderPlugin() : undefined,
|
||||||
plugin: this
|
plugin: this
|
||||||
}
|
}
|
||||||
return ctx
|
return ctx
|
||||||
|
@ -365,8 +539,18 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
const folder: TFolder = this.file
|
const folder: TFolder = this.file
|
||||||
let sortSpec: CustomSortSpec | null | undefined = plugin.determineSortSpecForFolder(folder.path, folder.name)
|
let sortSpec: CustomSortSpec | null | undefined = plugin.determineSortSpecForFolder(folder.path, folder.name)
|
||||||
|
|
||||||
|
// Performance optimization
|
||||||
|
// Primary intention: when the implicit bookmarks integration is enabled, remain on std Obsidian, if no need to involve bookmarks
|
||||||
|
let has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec)
|
||||||
|
if (hasOnlyByBookmarkOrStandardObsidian(has)) {
|
||||||
|
const bookmarksPlugin: BookmarksPluginInterface|undefined = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference, false, true)
|
||||||
|
if ( !bookmarksPlugin?.bookmarksIncludeItemsInFolder(folder.path)) {
|
||||||
|
sortSpec = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sortSpec) {
|
if (sortSpec) {
|
||||||
return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting());
|
return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting(has));
|
||||||
} else {
|
} else {
|
||||||
return old.call(this, ...args);
|
return old.call(this, ...args);
|
||||||
}
|
}
|
||||||
|
@ -380,6 +564,24 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderedFolderItemsForBookmarking(folder: TFolder, bookmarksPlugin: BookmarksPluginInterface): Array<TAbstractFile> {
|
||||||
|
let sortSpec: CustomSortSpec | null | undefined = undefined
|
||||||
|
if (!this.settings.suspended) {
|
||||||
|
sortSpec = this.determineSortSpecForFolder(folder.path, folder.name)
|
||||||
|
}
|
||||||
|
let uiSortOrder: string = this.getFileExplorer()?.sortOrder || ObsidianStandardDefaultSortingName
|
||||||
|
|
||||||
|
const has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec)
|
||||||
|
|
||||||
|
return sortFolderItemsForBookmarking(
|
||||||
|
folder,
|
||||||
|
folder.children,
|
||||||
|
sortSpec,
|
||||||
|
this.createProcessingContextForSorting(has),
|
||||||
|
uiSortOrder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Credits go to https://github.com/nothingislost/obsidian-bartender
|
// Credits go to https://github.com/nothingislost/obsidian-bartender
|
||||||
getFileExplorer(): FileExplorerView | undefined {
|
getFileExplorer(): FileExplorerView | undefined {
|
||||||
let fileExplorer: FileExplorerView | undefined = app.workspace.getLeavesOfType("file-explorer")?.first()
|
let fileExplorer: FileExplorerView | undefined = app.workspace.getLeavesOfType("file-explorer")?.first()
|
||||||
|
@ -397,7 +599,12 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
const data: any = await this.loadData() || {}
|
||||||
|
const isFreshInstall: boolean = Object.keys(data).length === 0
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||||
|
if (requireApiVersion('1.2.0')) {
|
||||||
|
this.settings = Object.assign(this.settings, DEFAULT_SETTING_FOR_1_2_0_UP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
|
@ -405,6 +612,10 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathToFlatString = (path: string): string => {
|
||||||
|
return path.replace(/\//g,'_').replace(/\\/g, '_')
|
||||||
|
}
|
||||||
|
|
||||||
class CustomSortSettingTab extends PluginSettingTab {
|
class CustomSortSettingTab extends PluginSettingTab {
|
||||||
plugin: CustomSortPlugin;
|
plugin: CustomSortPlugin;
|
||||||
|
|
||||||
|
@ -492,5 +703,60 @@ class CustomSortSettingTab extends PluginSettingTab {
|
||||||
this.plugin.settings.mobileNotificationsEnabled = value;
|
this.plugin.settings.mobileNotificationsEnabled = value;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
containerEl.createEl('h2', {text: 'Bookmarks integration'});
|
||||||
|
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. Automatically, without any '
|
||||||
|
+ 'need for sorting configuration. At the same time, it integrates seamlessly with'
|
||||||
|
+ ' <pre style="display: inline;">sorting-spec:</pre> configurations and they can nicely cooperate.'
|
||||||
|
+ '<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 '
|
||||||
|
+ "'<i>" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "</i>' "
|
||||||
|
+ '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('Automatic integration with core Bookmarks plugin (for indirect drag & drop ordering)')
|
||||||
|
.setDesc(bookmarksIntegrationDescription)
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.automaticBookmarksIntegration)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.automaticBookmarksIntegration = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
value = groupNameForPath(value.trim()).trim()
|
||||||
|
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value ? pathToFlatString(normalizePath(value)) : '';
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bookmarksIntegrationContextMenusDescription: DocumentFragment = sanitizeHTMLToDom(
|
||||||
|
'Enable <i>Custom-sort: bookmark for sorting</i> and <i>Custom-sort: bookmark+siblings for sorting.</i> entries '
|
||||||
|
+ 'in context menu in File Explorer'
|
||||||
|
)
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Context menus for Bookmarks integration')
|
||||||
|
.setDesc(bookmarksIntegrationContextMenusDescription)
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.bookmarksContextMenus)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.bookmarksContextMenus = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
Integration with Bookmarks core plugin:
|
||||||
|
- support two approaches _at the same time_:
|
||||||
|
- (A) structured bookmarks inside a dedicated bookmarks group, and
|
||||||
|
- (B) a flat list of bookmarks inside the dedicated bookmarks group
|
||||||
|
|
||||||
|
For (A):
|
||||||
|
- preferred
|
||||||
|
- a folder is represented by a group in bookmarks
|
||||||
|
- a file is represented by a file-with-block
|
||||||
|
- this also applied to non-md files, like jpg and others
|
||||||
|
- guarantees _'hiding'_ the bookmarks-for-sorting from regular bookmarks usage scenarios
|
||||||
|
- bookmark entries for sorting are encapsulated in the dedicated group
|
||||||
|
- they don't interfere with bookmarking of files and folders via standard bookmarking
|
||||||
|
- only exact location of file bookmark / group matches for sorting order in file explorer
|
||||||
|
- the contextual bookmark menus always work in (A) mode
|
||||||
|
- the contextual menus create / modify the bookmarks structure on-the-fly
|
||||||
|
|
||||||
|
For (B):
|
||||||
|
- discouraged, yet supported (exception for some edge cases)
|
||||||
|
- typically a result of manual bookmarks management
|
||||||
|
- for small number of items seems reasonable
|
||||||
|
- for flat vaults it could look same as for (A)
|
||||||
|
- groups don't have a 'path' attribute, their path is determined by their location
|
||||||
|
- bookmarked folders represent folders if inside the bookmarks group for sorting
|
||||||
|
- yet in this way they interfere with regular bookmarks scenario
|
||||||
|
- file bookmarks work correctly in non-interfering way thanks to the _'artificial block reference'_
|
||||||
|
- file bookmarks not having the _'artificial block ref'_ work as well
|
||||||
|
- if they are in the designated bookmarks group
|
||||||
|
- if there isn't a duplicate, which has the _'artificial block ref'_
|
||||||
|
- yet in this way they interfere with regular bookmarks scenario
|
||||||
|
|
||||||
|
-[ ] TODO: review again the 'item moved' and 'item deleted' scenarios (they look ok, check if they don't delete/move too much)
|
||||||
|
- [x] fundamental question 1: should 'move' create a bookmark entry/structure if it is not covered by bookmarks?
|
||||||
|
- Answer: the moved item is removed from bookmarks. If it is a group with descendants not transparent for sorting,
|
||||||
|
it is renamed to become transparent for sorting.
|
||||||
|
By design, the order of items is property of the parent folder (the container) and not the items
|
||||||
|
- [x] fundamental question 2: should 'move' create a bookmark entry if moved item was not bookmarked, yet is moved to a folder covered by bookmarks?
|
||||||
|
- Answer: same as for previous point.
|
||||||
|
- [x] review from (A) and (B) perspective
|
||||||
|
- Answer: scenario (A) is fully handled by 'item moved' and 'item deleted'.
|
||||||
|
scenario (B) is partially handled for 'item moved'. Details to be read from code (too complex to cover here)
|
||||||
|
- [x] consider deletion of item outside of bookmarks sorting container group
|
||||||
|
Answer: bookmark items outside of bookmarks sorting container are not manipulated by custom-sort plugin
|
||||||
|
to not interfere with standard Bookmarks scenarios
|
||||||
|
- [x] consider moving an item outside of bookmarks group
|
||||||
|
- Answer: question not relevant. Items are moved in file explorer and bookmarks only reflect that, if needed.
|
||||||
|
Hence there is no concept of 'moving an item outside of bookmarks group' - bookmarks group only exists in bookmarks
|
||||||
|
- [x] edge case: bookmarked item is a group, the deleted/moved is a file, not a folder --> what to do?
|
||||||
|
- Answer: for moved files, only file bookmarks are scanned (and handles), for moved folders, only groups are scanned (and handled).
|
||||||
|
- [x] delete all instances at any level of bookmarks structure in 'delete' handler
|
||||||
|
- Answer: only instances of (A) or (B) are deleted. Items outside of bookmarks container for sorting or
|
||||||
|
in invalid locations in bookmarks hierarchy are ignored
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,623 @@
|
||||||
|
import {
|
||||||
|
InstalledPlugin,
|
||||||
|
PluginInstance,
|
||||||
|
TAbstractFile,
|
||||||
|
TFile,
|
||||||
|
TFolder
|
||||||
|
} from "obsidian";
|
||||||
|
import {
|
||||||
|
extractParentFolderPath,
|
||||||
|
lastPathComponent
|
||||||
|
} from "./utils";
|
||||||
|
import {Arr} from "tern";
|
||||||
|
import * as process from "process";
|
||||||
|
|
||||||
|
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
|
||||||
|
|
||||||
|
const BookmarksPlugin_items_collectionName = 'items'
|
||||||
|
|
||||||
|
type Path = string
|
||||||
|
|
||||||
|
// Only relevant types of bookmarked items considered here
|
||||||
|
// The full set of types also includes 'search', canvas, graph, maybe more to come
|
||||||
|
type BookmarkedItem = BookmarkedFile | BookmarkedFolder | BookmarkedGroup
|
||||||
|
|
||||||
|
// Either a file, a folder or header/block inside a file
|
||||||
|
interface BookmarkItemSuperset {
|
||||||
|
path: Path
|
||||||
|
title?: string
|
||||||
|
ctime: number
|
||||||
|
subpath?: string // Anchor within the file (heading and/or block ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkWithPath extends Pick<BookmarkItemSuperset, 'path'> {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkedFile extends BookmarkItemSuperset {
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkedFolder extends Omit<BookmarkItemSuperset, 'subpath'> {
|
||||||
|
type: 'folder'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkedGroup extends Omit<BookmarkItemSuperset, 'subpath'|'path'> {
|
||||||
|
type: 'group'
|
||||||
|
items: Array<BookmarkedItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookmarkedItemPath = string
|
||||||
|
|
||||||
|
export interface OrderedBookmarkedItemWithMetadata {
|
||||||
|
isGroup?: boolean
|
||||||
|
path: BookmarkedItemPath
|
||||||
|
hasSortingIndicator?: boolean
|
||||||
|
order: number
|
||||||
|
bookmarkPathMatches?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderedBookmarkedItem = Pick<OrderedBookmarkedItemWithMetadata, 'order'>
|
||||||
|
export type Order = number
|
||||||
|
|
||||||
|
export interface OrderedBookmarks {
|
||||||
|
[key: BookmarkedItemPath]: Order
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderedBookmarksWithMetadata {
|
||||||
|
[key: BookmarkedItemPath]: OrderedBookmarkedItemWithMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Bookmarks_PluginInstance extends PluginInstance {
|
||||||
|
[BookmarksPlugin_getBookmarks_methodName]: () => Array<BookmarkedItem> | undefined
|
||||||
|
[BookmarksPlugin_items_collectionName]: Array<BookmarkedItem>
|
||||||
|
saveData(): void
|
||||||
|
onItemsChanged(saveData: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookmarksPluginInterface {
|
||||||
|
determineBookmarkOrder(path: string): number|undefined
|
||||||
|
bookmarkFolderItem(item: TAbstractFile): void
|
||||||
|
unbookmarkFolderItem(item: TAbstractFile): void
|
||||||
|
saveDataAndUpdateBookmarkViews(updateBookmarkViews: boolean): void
|
||||||
|
bookmarkSiblings(siblings: Array<TAbstractFile>, inTheTop?: boolean): void
|
||||||
|
unbookmarkSiblings(siblings: Array<TAbstractFile>): void
|
||||||
|
updateSortingBookmarksAfterItemRenamed(renamedItem: TAbstractFile, oldPath: string): void
|
||||||
|
updateSortingBookmarksAfterItemDeleted(deletedItem: TAbstractFile): void
|
||||||
|
isBookmarkedForSorting(item: TAbstractFile): boolean
|
||||||
|
|
||||||
|
// To support performance optimization
|
||||||
|
bookmarksIncludeItemsInFolder(folderPath: string): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkSubtreeForOnlyTransparentGroups = (items: Array<BookmarkedItem>): boolean => {
|
||||||
|
if (!items || items?.length === 0) return true
|
||||||
|
for (let it of items) {
|
||||||
|
if (it.type !== 'group' || !it.title || !isGroupTransparentForSorting(it.title)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// it is a group transparent for sorting
|
||||||
|
const isEmptyOrTransparent: boolean = checkSubtreeForOnlyTransparentGroups(it.items)
|
||||||
|
if (!isEmptyOrTransparent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants = (group: BookmarkedGroup): boolean => {
|
||||||
|
return checkSubtreeForOnlyTransparentGroups(group.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
||||||
|
|
||||||
|
plugin: Bookmarks_PluginInstance|undefined
|
||||||
|
groupNameForSorting: string|undefined
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result:
|
||||||
|
// 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
|
||||||
|
//
|
||||||
|
// Parameterless invocation enforces cache population, if empty
|
||||||
|
determineBookmarkOrder = (path?: string): Order | undefined => {
|
||||||
|
if (!bookmarksCache) {
|
||||||
|
[bookmarksCache, bookmarksFoldersCoverage] = getOrderedBookmarks(this.plugin!, this.groupNameForSorting)
|
||||||
|
bookmarksCacheTimestamp = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path && path.length > 0) {
|
||||||
|
const bookmarkedItemPosition: Order | undefined = bookmarksCache?.[path]
|
||||||
|
|
||||||
|
return (bookmarkedItemPosition && bookmarkedItemPosition > 0) ? bookmarkedItemPosition : undefined
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkFolderItem = (item: TAbstractFile) => {
|
||||||
|
this.bookmarkSiblings([item], true)
|
||||||
|
}
|
||||||
|
|
||||||
|
unbookmarkFolderItem = (item: TAbstractFile) => {
|
||||||
|
this.unbookmarkSiblings([item])
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDataAndUpdateBookmarkViews = (updateBookmarkViews: boolean = true) => {
|
||||||
|
this.plugin!.onItemsChanged(true)
|
||||||
|
if (updateBookmarkViews) {
|
||||||
|
const bookmarksLeafs = app.workspace.getLeavesOfType('bookmarks')
|
||||||
|
bookmarksLeafs?.forEach((leaf) => {
|
||||||
|
(leaf.view as any)?.update?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkSiblings = (siblings: Array<TAbstractFile>, inTheTop?: boolean) => {
|
||||||
|
if (siblings.length === 0) return // for sanity
|
||||||
|
|
||||||
|
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(
|
||||||
|
siblings[0].path,
|
||||||
|
CreateIfMissing,
|
||||||
|
this.plugin!,
|
||||||
|
this.groupNameForSorting
|
||||||
|
)
|
||||||
|
|
||||||
|
if (bookmarksContainer) { // for sanity, the group should be always created if missing
|
||||||
|
siblings.forEach((aSibling) => {
|
||||||
|
const siblingName = lastPathComponent(aSibling.path)
|
||||||
|
const groupTransparentForSorting = bookmarksContainer.items.find((it) => (
|
||||||
|
it.type === 'group' && groupNameForPath(it.title||'') === siblingName && isGroupTransparentForSorting(it.title)
|
||||||
|
))
|
||||||
|
if (groupTransparentForSorting) {
|
||||||
|
// got a group transparent for sorting
|
||||||
|
groupTransparentForSorting.title = groupNameForPath(groupTransparentForSorting.title||'')
|
||||||
|
} else if (!bookmarksContainer.items.find((it) =>
|
||||||
|
((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path) ||
|
||||||
|
(it.type === 'group' && it.title === siblingName))) {
|
||||||
|
const newEntry: BookmarkedItem = (aSibling instanceof TFolder) ? createBookmarkGroupEntry(siblingName) : createBookmarkFileEntry(aSibling.path);
|
||||||
|
if (inTheTop) {
|
||||||
|
bookmarksContainer.items.unshift(newEntry)
|
||||||
|
} else {
|
||||||
|
bookmarksContainer.items.push(newEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unbookmarkSiblings = (siblings: Array<TAbstractFile>) => {
|
||||||
|
if (siblings.length === 0) return // for sanity
|
||||||
|
|
||||||
|
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(
|
||||||
|
siblings[0].path,
|
||||||
|
DontCreateIfMissing,
|
||||||
|
this.plugin!,
|
||||||
|
this.groupNameForSorting
|
||||||
|
)
|
||||||
|
|
||||||
|
if (bookmarksContainer) { // for sanity
|
||||||
|
const bookmarkedItemsToRemove: Array<BookmarkedItem> = []
|
||||||
|
siblings.forEach((aSibling) => {
|
||||||
|
const siblingName = lastPathComponent(aSibling.path)
|
||||||
|
const aGroup = bookmarksContainer.items.find(
|
||||||
|
(it) => (it.type === 'group' && groupNameForPath(it.title||'') === siblingName)
|
||||||
|
)
|
||||||
|
if (aGroup) {
|
||||||
|
const canBeRemoved = bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(aGroup as BookmarkedGroup)
|
||||||
|
if (canBeRemoved) {
|
||||||
|
bookmarksContainer.items.remove(aGroup)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(bookmarksContainer, this.plugin!, this.groupNameForSorting)
|
||||||
|
} else {
|
||||||
|
if (!isGroupTransparentForSorting(aGroup.title)) {
|
||||||
|
aGroup.title = groupNameTransparentForSorting(aGroup.title||'')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const aFileOrFolder = bookmarksContainer.items.find(
|
||||||
|
(it) => ((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path)
|
||||||
|
)
|
||||||
|
if (aFileOrFolder) {
|
||||||
|
bookmarksContainer.items.remove(aFileOrFolder)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(bookmarksContainer, this.plugin!, this.groupNameForSorting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSortingBookmarksAfterItemRenamed = (renamedItem: TAbstractFile, oldPath: string): void => {
|
||||||
|
updateSortingBookmarksAfterItemRenamed(this.plugin!, renamedItem, oldPath, this.groupNameForSorting)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSortingBookmarksAfterItemDeleted = (deletedItem: TAbstractFile): void => {
|
||||||
|
updateSortingBookmarksAfterItemDeleted(this.plugin!, deletedItem, this.groupNameForSorting)
|
||||||
|
}
|
||||||
|
|
||||||
|
isBookmarkedForSorting = (item: TAbstractFile): boolean => {
|
||||||
|
const itemsCollection: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(item.path, DontCreateIfMissing, this.plugin!, this.groupNameForSorting)
|
||||||
|
if (itemsCollection) {
|
||||||
|
if (item instanceof TFile) {
|
||||||
|
return itemsCollection.items?.some((bkmrk) => bkmrk.type === 'file' && bkmrk.path === item.path)
|
||||||
|
} else {
|
||||||
|
const folderName: string = lastPathComponent(item.path)
|
||||||
|
return itemsCollection.items?.some((bkmrk) =>
|
||||||
|
(bkmrk.type === 'group' && bkmrk.title === folderName) ||
|
||||||
|
(bkmrk.type === 'folder' && bkmrk.path === item.path)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarksIncludeItemsInFolder = (folderPath: string): boolean => {
|
||||||
|
return !! bookmarksFoldersCoverage?.[folderPath]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BookmarksCorePluginId: string = 'bookmarks'
|
||||||
|
|
||||||
|
export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache?: boolean, ensureCachePopulated?: boolean): BookmarksPluginInterface | undefined => {
|
||||||
|
invalidateExpiredBookmarksCache(forceFlushCache)
|
||||||
|
const installedBookmarksPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(BookmarksCorePluginId)
|
||||||
|
if (installedBookmarksPlugin && installedBookmarksPlugin.enabled && installedBookmarksPlugin.instance) {
|
||||||
|
const bookmarksPluginInstance: Bookmarks_PluginInstance = installedBookmarksPlugin.instance as Bookmarks_PluginInstance
|
||||||
|
// defensive programming, in case Obsidian changes its internal APIs
|
||||||
|
if (typeof bookmarksPluginInstance?.[BookmarksPlugin_getBookmarks_methodName] === 'function' &&
|
||||||
|
Array.isArray(bookmarksPluginInstance?.[BookmarksPlugin_items_collectionName])) {
|
||||||
|
bookmarksPlugin.plugin = bookmarksPluginInstance
|
||||||
|
bookmarksPlugin.groupNameForSorting = bookmarksGroupName
|
||||||
|
if (ensureCachePopulated && !bookmarksCache) {
|
||||||
|
bookmarksPlugin.determineBookmarkOrder()
|
||||||
|
}
|
||||||
|
return bookmarksPlugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache can outlive the wrapper instances
|
||||||
|
let bookmarksCache: OrderedBookmarks | undefined = undefined
|
||||||
|
let bookmarksCacheTimestamp: number | undefined = undefined
|
||||||
|
type FolderPath = string
|
||||||
|
type FoldersCoverage = { [key: FolderPath]: boolean }
|
||||||
|
let bookmarksFoldersCoverage: FoldersCoverage | undefined = undefined
|
||||||
|
|
||||||
|
const bookmarksPlugin: BookmarksPluginWrapper = new BookmarksPluginWrapper()
|
||||||
|
|
||||||
|
const CacheExpirationMilis = 1000 // One second seems to be reasonable
|
||||||
|
|
||||||
|
const invalidateExpiredBookmarksCache = (force?: boolean): void => {
|
||||||
|
if (bookmarksCache) {
|
||||||
|
let flush: boolean = true
|
||||||
|
if (!force && !!bookmarksCacheTimestamp) {
|
||||||
|
if (Date.now() - CacheExpirationMilis <= bookmarksCacheTimestamp) {
|
||||||
|
flush = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flush) {
|
||||||
|
bookmarksCache = undefined
|
||||||
|
bookmarksCacheTimestamp = undefined
|
||||||
|
bookmarksFoldersCoverage = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TraverseCallback = (item: BookmarkedItem, parentsGroupsPath: string) => boolean | void
|
||||||
|
|
||||||
|
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callbackConsumeItem: TraverseCallback) => {
|
||||||
|
if (!Array.isArray(items)) return
|
||||||
|
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupsPath: string) => {
|
||||||
|
if (!Array.isArray(collection)) return
|
||||||
|
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
|
||||||
|
const item = collectionRef[idx];
|
||||||
|
if (callbackConsumeItem(item, groupsPath)) return;
|
||||||
|
if ('group' === item.type) {
|
||||||
|
const groupNameToUseInPath: string = groupNameForPath(item.title || '')
|
||||||
|
recursiveTraversal(item.items, `${groupsPath}${groupsPath?'/':''}${groupNameToUseInPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
recursiveTraversal(items, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR = '#^-'
|
||||||
|
|
||||||
|
const ROOT_FOLDER_PATH = '/'
|
||||||
|
|
||||||
|
const TRANSPARENT_FOR_SORTING_PREFIX = '\\\\'
|
||||||
|
|
||||||
|
const isGroupTransparentForSorting = (name?: string): boolean => {
|
||||||
|
return !!name?.startsWith(TRANSPARENT_FOR_SORTING_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupNameTransparentForSorting = (name: string): string => {
|
||||||
|
return isGroupTransparentForSorting(name) ? name : `${TRANSPARENT_FOR_SORTING_PREFIX}${name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const groupNameForPath = (name: string): string => {
|
||||||
|
return isGroupTransparentForSorting(name) ? name.substring(TRANSPARENT_FOR_SORTING_PREFIX.length) : name
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): [OrderedBookmarks, FoldersCoverage] | [undefined, undefined] => {
|
||||||
|
let bookmarksItems: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_items_collectionName]
|
||||||
|
let bookmarksCoveredFolders: FoldersCoverage = {}
|
||||||
|
if (bookmarksItems && Array.isArray(bookmarksItems)) {
|
||||||
|
if (bookmarksGroupName) {
|
||||||
|
// scanning only top level items because by design the bookmarks group for sorting is a top level item
|
||||||
|
const bookmarksGroup: BookmarkedGroup|undefined = bookmarksItems.find(
|
||||||
|
(item) => item.type === 'group' && item.title === bookmarksGroupName
|
||||||
|
) as BookmarkedGroup
|
||||||
|
bookmarksItems = bookmarksGroup ? bookmarksGroup.items : undefined
|
||||||
|
}
|
||||||
|
if (bookmarksItems) {
|
||||||
|
const orderedBookmarksWithMetadata: OrderedBookmarksWithMetadata = {}
|
||||||
|
let order: number = 1 // Intentionally start > 0 to allow easy check: if (order) ...
|
||||||
|
const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => {
|
||||||
|
if ('group' === item.type) {
|
||||||
|
if (!isGroupTransparentForSorting(item.title)) {
|
||||||
|
const path: string = `${parentGroupsPath}${parentGroupsPath ? '/' : ''}${item.title}`
|
||||||
|
const alreadyConsumed = orderedBookmarksWithMetadata[path]
|
||||||
|
if (alreadyConsumed) {
|
||||||
|
if (alreadyConsumed.isGroup) return // Defensive programming
|
||||||
|
if (alreadyConsumed.hasSortingIndicator) return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedBookmarksWithMetadata[path] = {
|
||||||
|
path: path,
|
||||||
|
order: order++,
|
||||||
|
isGroup: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ('file' === item.type || 'folder' === item.type) {
|
||||||
|
const itemWithPath = (item as BookmarkWithPath)
|
||||||
|
const itemFile = 'file' === item.type ? (item as BookmarkedFile) : undefined
|
||||||
|
const alreadyConsumed = orderedBookmarksWithMetadata[itemWithPath.path]
|
||||||
|
const hasSortingIndicator: boolean|undefined = itemFile ? itemFile.subpath === ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR : undefined
|
||||||
|
const parentFolderPathOfBookmarkedItem = extractParentFolderPath(itemWithPath.path)
|
||||||
|
const bookmarkPathMatches: boolean = parentGroupsPath === parentFolderPathOfBookmarkedItem
|
||||||
|
const bookmarkPathIsRoot: boolean = !(parentGroupsPath?.length > 0)
|
||||||
|
|
||||||
|
// Bookmarks not in root (group) or in matching path are ignored
|
||||||
|
if (!bookmarkPathMatches && !bookmarkPathIsRoot) return
|
||||||
|
|
||||||
|
// For bookmarks in root or in matching path, apply the prioritized duplicate elimination logic
|
||||||
|
if (alreadyConsumed) {
|
||||||
|
if (hasSortingIndicator) {
|
||||||
|
if (alreadyConsumed.hasSortingIndicator && alreadyConsumed.bookmarkPathMatches) return
|
||||||
|
if (alreadyConsumed.hasSortingIndicator && !bookmarkPathMatches) return
|
||||||
|
} else { // no sorting indicator on new
|
||||||
|
if (alreadyConsumed.hasSortingIndicator) return
|
||||||
|
if (!bookmarkPathMatches || alreadyConsumed.bookmarkPathMatches || alreadyConsumed.isGroup) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedBookmarksWithMetadata[itemWithPath.path] = {
|
||||||
|
path: itemWithPath.path,
|
||||||
|
order: order++,
|
||||||
|
isGroup: false,
|
||||||
|
bookmarkPathMatches: bookmarkPathMatches,
|
||||||
|
hasSortingIndicator: hasSortingIndicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverseBookmarksCollection(bookmarksItems, consumeItem)
|
||||||
|
|
||||||
|
const orderedBookmarks: OrderedBookmarks = {}
|
||||||
|
|
||||||
|
for (let path in orderedBookmarksWithMetadata) {
|
||||||
|
orderedBookmarks[path] = orderedBookmarksWithMetadata[path].order
|
||||||
|
const parentFolderPath: Path = extractParentFolderPath(path)
|
||||||
|
bookmarksCoveredFolders[parentFolderPath.length > 0 ? parentFolderPath : ROOT_FOLDER_PATH] = true
|
||||||
|
}
|
||||||
|
return [orderedBookmarks, bookmarksCoveredFolders]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [undefined, undefined]
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBookmarkFileEntry = (path: string): BookmarkedFile => {
|
||||||
|
// Artificial subpath added intentionally to prevent Bookmarks context menu from finding this item in bookmarks
|
||||||
|
// and - in turn - allow bookmarking it by the user for regular (non sorting) purposes
|
||||||
|
return { type: "file", ctime: Date.now(), path: path, subpath: ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR }
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBookmarkGroupEntry = (title: string): BookmarkedGroup => {
|
||||||
|
return { type: "group", ctime: Date.now(), items: [], title: title }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookmarkedParentFolder {
|
||||||
|
pathOfGroup?: Path // undefined when the container is the root of bookmarks
|
||||||
|
group?: BookmarkedGroup // undefined when the item is at root level of bookmarks
|
||||||
|
items: Array<BookmarkedItem> // reference to group.items or to root collection of bookmarks
|
||||||
|
}
|
||||||
|
|
||||||
|
const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: boolean, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): BookmarkedParentFolder|undefined => {
|
||||||
|
let items = plugin?.[BookmarksPlugin_items_collectionName]
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) return undefined
|
||||||
|
|
||||||
|
if (!itemPath || !itemPath.trim()) return undefined // for sanity
|
||||||
|
|
||||||
|
const parentPath: string = extractParentFolderPath(itemPath)
|
||||||
|
|
||||||
|
const parentPathComponents: Array<string> = parentPath ? parentPath.split('/')! : []
|
||||||
|
|
||||||
|
if (bookmarksGroup) {
|
||||||
|
parentPathComponents.unshift(bookmarksGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
let group: BookmarkedGroup|undefined = undefined
|
||||||
|
|
||||||
|
parentPathComponents.forEach((pathSegment, index) => {
|
||||||
|
group = items.find((it) => it.type === 'group' && groupNameForPath(it.title||'') === pathSegment) as BookmarkedGroup
|
||||||
|
if (!group) {
|
||||||
|
if (createIfMissing) {
|
||||||
|
const theSortingBookmarksContainerGroup = (bookmarksGroup && index === 0)
|
||||||
|
const groupName: string = theSortingBookmarksContainerGroup ? pathSegment : groupNameTransparentForSorting(pathSegment)
|
||||||
|
group = createBookmarkGroupEntry(groupName)
|
||||||
|
items.push(group)
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items = group.items
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: items,
|
||||||
|
group: group,
|
||||||
|
pathOfGroup: parentPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateIfMissing = true
|
||||||
|
const DontCreateIfMissing = false
|
||||||
|
|
||||||
|
const renameGroup = (group: BookmarkedGroup, newName: string, makeTransparentForSorting: boolean|undefined) => {
|
||||||
|
if (makeTransparentForSorting === true) {
|
||||||
|
group.title = groupNameTransparentForSorting(newName)
|
||||||
|
} else if (makeTransparentForSorting === false) {
|
||||||
|
group.title = newName
|
||||||
|
} else { // no transparency status, retain the status as-is
|
||||||
|
group.title = isGroupTransparentForSorting(group.title) ? groupNameTransparentForSorting(newName) : newName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupBookmarkTreeFromTransparentEmptyGroups = (parentGroup: BookmarkedParentFolder|undefined, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => {
|
||||||
|
|
||||||
|
if (!parentGroup) return // invalid invocation - exit
|
||||||
|
if (!parentGroup.group) return // root folder of the bookmarks - do not touch items in root folder
|
||||||
|
|
||||||
|
if (checkSubtreeForOnlyTransparentGroups(parentGroup.items)) {
|
||||||
|
parentGroup.group.items = []
|
||||||
|
|
||||||
|
const parentContainerOfGroup = findGroupForItemPathInBookmarks(
|
||||||
|
parentGroup.pathOfGroup || '',
|
||||||
|
DontCreateIfMissing,
|
||||||
|
plugin,
|
||||||
|
bookmarksGroup
|
||||||
|
)
|
||||||
|
if (parentContainerOfGroup) {
|
||||||
|
parentContainerOfGroup.group?.items?.remove(parentGroup.group)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(parentContainerOfGroup, plugin, bookmarksGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSortingBookmarksAfterItemRenamed = (plugin: Bookmarks_PluginInstance, renamedItem: TAbstractFile, oldPath: string, bookmarksGroup?: string) => {
|
||||||
|
|
||||||
|
if (renamedItem.path === oldPath) return; // sanity
|
||||||
|
|
||||||
|
const aFolder: boolean = renamedItem instanceof TFolder
|
||||||
|
const aFile: boolean = !aFolder
|
||||||
|
const oldParentPath: string = extractParentFolderPath(oldPath)
|
||||||
|
const oldName: string = lastPathComponent(oldPath)
|
||||||
|
const newParentPath: string = extractParentFolderPath(renamedItem.path)
|
||||||
|
const newName: string = lastPathComponent(renamedItem.path)
|
||||||
|
const moved: boolean = oldParentPath !== newParentPath
|
||||||
|
const renamed: boolean = oldName !== newName
|
||||||
|
|
||||||
|
// file renames are handled automatically by Obsidian in bookmarks, no need for additional actions
|
||||||
|
if (aFile && renamed) return
|
||||||
|
|
||||||
|
const originalContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(oldPath, DontCreateIfMissing, plugin, bookmarksGroup)
|
||||||
|
|
||||||
|
if (!originalContainer) return;
|
||||||
|
|
||||||
|
const item: BookmarkedItem|undefined = aFolder ?
|
||||||
|
originalContainer.items.find((it) => (
|
||||||
|
it.type === 'group' && groupNameForPath(it.title||'') === oldName
|
||||||
|
))
|
||||||
|
: // aFile
|
||||||
|
originalContainer.items.find((it) => (
|
||||||
|
it.type === 'file' && it.path === renamedItem.path
|
||||||
|
))
|
||||||
|
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
// The renamed/moved item was located in bookmarks, actions depend on item type
|
||||||
|
if (aFile) {
|
||||||
|
if (moved) { // sanity
|
||||||
|
originalContainer.group?.items.remove(item)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||||
|
}
|
||||||
|
} else { // a group
|
||||||
|
const aGroup: BookmarkedGroup = item as BookmarkedGroup
|
||||||
|
|
||||||
|
if (bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(aGroup)) {
|
||||||
|
if (moved) { // sanity
|
||||||
|
originalContainer.group?.items.remove(aGroup)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||||
|
} else if (renamed) {
|
||||||
|
renameGroup(aGroup, newName, undefined)
|
||||||
|
}
|
||||||
|
} else { // group has some descendants not transparent for sorting
|
||||||
|
if (moved) {
|
||||||
|
originalContainer.group?.items.remove(aGroup)
|
||||||
|
const targetContainer: BookmarkedParentFolder | undefined = findGroupForItemPathInBookmarks(renamedItem.path, CreateIfMissing, plugin, bookmarksGroup)
|
||||||
|
if (targetContainer) {
|
||||||
|
targetContainer.group?.items.push(aGroup)
|
||||||
|
// the group in new location becomes by design transparent for sorting.
|
||||||
|
// The sorting order is a property of the parent folder, not the item itself
|
||||||
|
renameGroup(aGroup, groupNameForPath(aGroup.title||''), true)
|
||||||
|
}
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renamed) {
|
||||||
|
// unrealistic scenario when a folder is moved and renamed at the same time
|
||||||
|
renameGroup(aGroup, newName, undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSortingBookmarksAfterItemDeleted = (plugin: Bookmarks_PluginInstance, deletedItem: TAbstractFile, bookmarksGroup?: string) => {
|
||||||
|
|
||||||
|
// Obsidian automatically deletes all bookmark instances of a file, nothing to be done here
|
||||||
|
if (deletedItem instanceof TFile) return;
|
||||||
|
|
||||||
|
let items = plugin[BookmarksPlugin_items_collectionName]
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) return
|
||||||
|
|
||||||
|
const aFolder: boolean = deletedItem instanceof TFolder
|
||||||
|
const aFile: boolean = !aFolder
|
||||||
|
|
||||||
|
// Delete all instances of deleted item from two handled locations:
|
||||||
|
// - in bookmark groups hierarchy matching the item path in file explorer
|
||||||
|
// - in the bookmark group designated as container for bookmarks (immediate children)
|
||||||
|
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(deletedItem.path, DontCreateIfMissing, plugin, bookmarksGroup)
|
||||||
|
const itemInRootFolder = !!extractParentFolderPath(deletedItem.path)
|
||||||
|
const bookmarksRootContainer: BookmarkedParentFolder|undefined =
|
||||||
|
(bookmarksGroup && !itemInRootFolder) ? findGroupForItemPathInBookmarks('intentionally-in-root-path', DontCreateIfMissing, plugin, bookmarksGroup) : undefined
|
||||||
|
|
||||||
|
if (!bookmarksContainer && !bookmarksRootContainer) return;
|
||||||
|
|
||||||
|
[bookmarksContainer, bookmarksRootContainer].forEach((container) => {
|
||||||
|
const bookmarkEntriesToRemove: Array<BookmarkedItem> = []
|
||||||
|
container?.items.forEach((it) => {
|
||||||
|
if (aFolder && it.type === 'group' && groupNameForPath(it.title||'') === deletedItem.name) {
|
||||||
|
bookmarkEntriesToRemove.push(it)
|
||||||
|
}
|
||||||
|
if (aFile && it.type === 'file' && it.path === deletedItem.path) {
|
||||||
|
bookmarkEntriesToRemove.push(it)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bookmarkEntriesToRemove.forEach((itemToRemove) =>{
|
||||||
|
container?.group?.items.remove(itemToRemove)
|
||||||
|
})
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(container, plugin, bookmarksGroup)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _unitTests = {
|
||||||
|
getOrderedBookmarks: getOrderedBookmarks,
|
||||||
|
bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants: bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants,
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups: cleanupBookmarkTreeFromTransparentEmptyGroups,
|
||||||
|
findGroupForItemPathInBookmarks: findGroupForItemPathInBookmarks
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
import {lastPathComponent, extractParentFolderPath} from "./utils";
|
import {
|
||||||
|
lastPathComponent,
|
||||||
|
extractParentFolderPath
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
describe('lastPathComponent and extractParentFolderPath', () => {
|
describe('lastPathComponent and extractParentFolderPath', () => {
|
||||||
it.each([
|
it.each([
|
||||||
|
@ -6,14 +9,14 @@ describe('lastPathComponent and extractParentFolderPath', () => {
|
||||||
['a/subfolder', 'a', 'subfolder'],
|
['a/subfolder', 'a', 'subfolder'],
|
||||||
['parent/child', 'parent', 'child'],
|
['parent/child', 'parent', 'child'],
|
||||||
['','',''],
|
['','',''],
|
||||||
[' ','',''],
|
[' ','',' '],
|
||||||
['/strange', '', 'strange'],
|
['/strange', '', 'strange'],
|
||||||
['a/b/c/', 'a/b/c', ''],
|
['a/b/c/', 'a/b/c', ''],
|
||||||
['d d d/e e e/f f f/ggg ggg', 'd d d/e e e/f f f', 'ggg ggg'],
|
['d d d/e e e/f f f/ggg ggg', 'd d d/e e e/f f f', 'ggg ggg'],
|
||||||
['/','',''],
|
['/','',''],
|
||||||
[' / ','',''],
|
[' / ',' ',' '],
|
||||||
[' /','',''],
|
[' /',' ',''],
|
||||||
['/ ','','']
|
['/ ','',' ']
|
||||||
])('should from %s extract %s and %s', (path: string, parentPath: string, lastComponent: string) => {
|
])('should from %s extract %s and %s', (path: string, parentPath: string, lastComponent: string) => {
|
||||||
const extractedParentPath: string = extractParentFolderPath(path)
|
const extractedParentPath: string = extractParentFolderPath(path)
|
||||||
const extractedLastComponent: string = lastPathComponent(path)
|
const extractedLastComponent: string = lastPathComponent(path)
|
||||||
|
|
|
@ -9,10 +9,10 @@ export function last<T>(o: Array<T>): T | undefined {
|
||||||
|
|
||||||
export function lastPathComponent(path: string): string {
|
export function lastPathComponent(path: string): string {
|
||||||
const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/')
|
const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/')
|
||||||
return lastPathSeparatorIdx >= 0 ? path.substring(lastPathSeparatorIdx + 1).trim() : path.trim()
|
return lastPathSeparatorIdx >= 0 ? path.substring(lastPathSeparatorIdx + 1) : path
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractParentFolderPath(path: string): string {
|
export function extractParentFolderPath(path: string): string {
|
||||||
const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/')
|
const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/')
|
||||||
return lastPathSeparatorIdx > 0 ? path.substring(0, lastPathSeparatorIdx).trim() : ''
|
return lastPathSeparatorIdx > 0 ? path.substring(0, lastPathSeparatorIdx) : ''
|
||||||
}
|
}
|
||||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -763,6 +763,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/tern" "*"
|
"@types/tern" "*"
|
||||||
|
|
||||||
|
"@types/codemirror@5.60.8":
|
||||||
|
version "5.60.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.8.tgz#b647d04b470e8e1836dd84b2879988fc55c9de68"
|
||||||
|
integrity sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==
|
||||||
|
dependencies:
|
||||||
|
"@types/tern" "*"
|
||||||
|
|
||||||
"@types/estree@*":
|
"@types/estree@*":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
|
||||||
|
@ -2396,6 +2403,14 @@ npm-run-path@^4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key "^3.0.0"
|
path-key "^3.0.0"
|
||||||
|
|
||||||
|
"obsidian-1.4.11@npm:obsidian@1.4.11":
|
||||||
|
version "1.4.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.4.11.tgz#5cba594c83a74ebad58b630c610265018abdadaa"
|
||||||
|
integrity sha512-BCVYTvaXxElJMl6MMbDdY/CGK+aq18SdtDY/7vH8v6BxCBQ6KF4kKxL0vG9UZ0o5qh139KpUoJHNm+6O5dllKA==
|
||||||
|
dependencies:
|
||||||
|
"@types/codemirror" "5.60.8"
|
||||||
|
moment "2.29.4"
|
||||||
|
|
||||||
obsidian@^0.15.4:
|
obsidian@^0.15.4:
|
||||||
version "0.15.9"
|
version "0.15.9"
|
||||||
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-0.15.9.tgz#b6e0b566952643db6b55f7e74fbba6d7140fe4a2"
|
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-0.15.9.tgz#b6e0b566952643db6b55f7e74fbba6d7140fe4a2"
|
||||||
|
|
Loading…
Reference in New Issue