From fe4f28b46b7fbc9eef098e8c52caf4810eaf798c Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:16:30 +0200 Subject: [PATCH 01/10] 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 --- docs/manual.md | 42 + package.json | 1 + .../custom-sort-getComparator.spec.ts | 2 - src/custom-sort/custom-sort-types.ts | 7 +- src/custom-sort/custom-sort-utils.spec.ts | 187 +++ src/custom-sort/custom-sort-utils.ts | 80 ++ src/custom-sort/custom-sort.spec.ts | 56 +- src/custom-sort/custom-sort.ts | 120 +- src/custom-sort/folder-matching-rules.spec.ts | 4 +- src/custom-sort/macros.spec.ts | 10 +- src/custom-sort/matchers.spec.ts | 1 - .../sorting-spec-processor.spec.ts | 49 +- src/custom-sort/sorting-spec-processor.ts | 10 + src/main.ts | 290 ++++- ...ookmarks Core Plugin integration design.md | 55 + .../BookmarksCorePluginSignature.spec.ts | 1065 +++++++++++++++++ src/utils/BookmarksCorePluginSignature.ts | 623 ++++++++++ src/utils/utils.spec.ts | 13 +- src/utils/utils.ts | 4 +- yarn.lock | 15 + 20 files changed, 2593 insertions(+), 41 deletions(-) create mode 100644 src/custom-sort/custom-sort-utils.spec.ts create mode 100644 src/custom-sort/custom-sort-utils.ts create mode 100644 src/utils/Bookmarks Core Plugin integration design.md create mode 100644 src/utils/BookmarksCorePluginSignature.spec.ts create mode 100644 src/utils/BookmarksCorePluginSignature.ts diff --git a/docs/manual.md b/docs/manual.md index 8e0a4fb..a712be5 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -181,6 +181,48 @@ sorting-spec: | The artificial separator `---+---` defines a sorting group, which will not match any folders or files and is used here to logically separate the series of combined groups into to logical sets +## Bookmarks plugin integration + +Integration with the __Bookmarks core plugin__ allows for ordering of items via drag & drop in Bookmarks view and reflecting the same order in File Explorer automatically + +TODO: the simple scenario presented on movie + +A separate group of bookmarks is designated by default, to separate from ... + +If at least one item in folder is bookmarked and the auto-enabled, the order of the items becomes managed by the custom sort plugin + +Auto-integration works without any need for sorting configuration files. Under the hood it is equivalent to applying the following global sorting specification: +```yaml +--- +sorting-spec: | + target-folder: /* + bookmarked: + < by-bookmarks-order + sorting: standard +--- +``` + +Auto-integration doesn't apply to folders, for which explicit sorting specification is defined in YAML. +In that case, if you want to employ the grouping and/or ordering by bookmarks order, you need to use explicit syntax: + +```yaml +--- +sorting-spec: | + target-folder: My folder + bookmarked: + < by-bookmarks-order +--- +``` + +TODO: more instructions plus movie of advanced integration, where bookmarks reflect the folders structure + +Also hints for updating + +A folder is excluded from auto-integration if: +- has custom sorting spec + - you can (if needed) enable the auto-integration for part of the items +- has explicitly applied 'sorting: standard' + ## Matching starred items The Obsidian core plugin `Starred` allows the user to 'star' files\ diff --git a/package.json b/package.json index aee27ae..9655eb6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "jest": "^28.1.1", "monkey-around": "^2.3.0", "obsidian": "^0.15.4", + "obsidian-1.4.11": "npm:obsidian@1.4.11", "ts-jest": "^28.0.5", "tslib": "2.4.0", "typescript": "4.7.4" diff --git a/src/custom-sort/custom-sort-getComparator.spec.ts b/src/custom-sort/custom-sort-getComparator.spec.ts index 1afb404..d942af3 100644 --- a/src/custom-sort/custom-sort-getComparator.spec.ts +++ b/src/custom-sort/custom-sort-getComparator.spec.ts @@ -1,8 +1,6 @@ import { FolderItemForSorting, getComparator, - getSorterFnFor, - getMdata, OS_byCreatedTime, OS_byModifiedTime, OS_byModifiedTimeReverse, SortingLevelId diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 6bafbb1..aa80327 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -1,5 +1,3 @@ -import {MetadataCache, Plugin} from "obsidian"; - export enum CustomSortGroupType { Outsiders, // Not belonging to any of other groups MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is @@ -9,6 +7,7 @@ export enum CustomSortGroupType { 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 StarredOnly, + BookmarkedOnly, HasIcon } @@ -29,7 +28,9 @@ export enum CustomSortOrder { byMetadataFieldTrueAlphabetical, byMetadataFieldAlphabeticalReverse, byMetadataFieldTrueAlphabeticalReverse, - standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI + standardObsidian, // whatever user selected in the UI + byBookmarkOrder, + byBookmarkOrderReverse, default = alphabetical } diff --git a/src/custom-sort/custom-sort-utils.spec.ts b/src/custom-sort/custom-sort-utils.spec.ts new file mode 100644 index 0000000..4c55453 --- /dev/null +++ b/src/custom-sort/custom-sort-utils.spec.ts @@ -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|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|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|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|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|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|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|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|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 = 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 diff --git a/src/custom-sort/custom-sort-utils.ts b/src/custom-sort/custom-sort-utils.ts new file mode 100644 index 0000000..e3f0ed0 --- /dev/null +++ b/src/custom-sort/custom-sort-utils.ts @@ -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 +} diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index bc07fed..d0ee7c8 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -8,13 +8,25 @@ import { FolderItemForSorting, getSorterFnFor, matchGroupRegex, - ProcessingContext, + ProcessingContext, + sorterByBookmarkOrder, sorterByMetadataField, SorterFn } from './custom-sort'; -import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; -import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; -import {findStarredFile_pathParam, Starred_PluginInstance} from "../utils/StarredPluginSignature"; +import { + CustomSortGroupType, + CustomSortOrder, + CustomSortSpec, + RegExpSpec +} from './custom-sort-types'; +import { + CompoundDashNumberNormalizerFn, + CompoundDotRomanNumberNormalizerFn +} from "./sorting-spec-processor"; +import { + findStarredFile_pathParam, + Starred_PluginInstance +} from "../utils/StarredPluginSignature"; import { ObsidianIconFolder_PluginInstance, ObsidianIconFolderPlugin_Data @@ -2854,3 +2866,39 @@ describe('sorterByMetadataField', () => { 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 = {bookmarkedIdx: bookmarkA, sortString: sortStringA} + const itemB: Partial = {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) + }) +}) diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 93268c1..6d1fb23 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -9,11 +9,11 @@ import { } from 'obsidian'; import { determineStarredStatusOf, - Starred_PluginInstance, + Starred_PluginInstance } from '../utils/StarredPluginSignature'; import { determineIconOf, - ObsidianIconFolder_PluginInstance, + ObsidianIconFolder_PluginInstance } from '../utils/ObsidianIconFolderPluginSignature' import { CustomSortGroup, @@ -30,13 +30,16 @@ import { import { expandMacros } from "./macros"; +import { + BookmarksPluginInterface +} from "../utils/BookmarksCorePluginSignature"; export interface ProcessingContext { // For internal transient use plugin?: Plugin // to hand over the access to App instance to the sorting engine _mCache?: MetadataCache starredPluginInstance?: Starred_PluginInstance - + bookmarksPluginInstance?: BookmarksPluginInterface, 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 isFolder: boolean folder?: TFolder + bookmarkedIdx?: number // derived from Bookmarks core plugin position } 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 } = { [CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(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.byMetadataFieldAlphabeticalReverse]: 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 [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 determined: boolean = false let derivedText: string | null | undefined + let bookmarkedIdx: number | undefined const aFolder: boolean = isFolder(entry) const aFile: boolean = !aFolder @@ -402,6 +426,14 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus } } 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: if(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, path: entry.path, 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) } +const SortOrderRequiringBookmarksOrder = new Set([ + 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 export type ModifiedTime = number export type CreatedTime = number @@ -546,13 +590,33 @@ export const determineFolderDatesIfNeeded = (folderItems: Array, 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) { 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)) - - // expand folder-specific macros const parentFolderName: string|undefined = this.file.name 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 determineFolderDatesIfNeeded(folderItems, sortingSpec) + if (ctx.bookmarksPluginInstance) { + determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPluginInstance) + } + const comparator: SorterFn = getComparator(sortingSpec, fileExplorer.sortOrder) folderItems.sort(comparator) @@ -583,3 +651,41 @@ export const folderSort = function (sortingSpec: CustomSortSpec, ctx: Processing this.children = items; } }; + +// Returns a sorted copy of the input array, intentionally to keep it intact +export const sortFolderItemsForBookmarking = function (folder: TFolder, items: Array, sortingSpec: CustomSortSpec|null|undefined, ctx: ProcessingContext, uiSortOrder: string): Array { + 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 = 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 = 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 = items.map((entry: TFile | TFolder) => entry) + const plainSorterFn: PlainSorterFn = StandardPlainObsidianComparator(uiSortOrder) + folderItems.sort(plainSorterFn) + return folderItems + } +}; diff --git a/src/custom-sort/folder-matching-rules.spec.ts b/src/custom-sort/folder-matching-rules.spec.ts index a69ccdd..1cabf1f 100644 --- a/src/custom-sort/folder-matching-rules.spec.ts +++ b/src/custom-sort/folder-matching-rules.spec.ts @@ -1,4 +1,6 @@ -import {FolderWildcardMatching} from './folder-matching-rules' +import { + FolderWildcardMatching +} from './folder-matching-rules' type SortingSpec = string diff --git a/src/custom-sort/macros.spec.ts b/src/custom-sort/macros.spec.ts index 487fadf..3011360 100644 --- a/src/custom-sort/macros.spec.ts +++ b/src/custom-sort/macros.spec.ts @@ -1,6 +1,12 @@ -import {expandMacros, expandMacrosInString} from "./macros"; +import { + expandMacros, + expandMacrosInString +} from "./macros"; import * as MacrosModule from './macros' -import {CustomSortGroup, CustomSortSpec} from "./custom-sort-types"; +import { + CustomSortGroup, + CustomSortSpec +} from "./custom-sort-types"; describe('expandMacrosInString', () => { it.each([ diff --git a/src/custom-sort/matchers.spec.ts b/src/custom-sort/matchers.spec.ts index 823c0f8..aef520e 100644 --- a/src/custom-sort/matchers.spec.ts +++ b/src/custom-sort/matchers.spec.ts @@ -12,7 +12,6 @@ import { WordInASCIIRegexStr, WordInAnyLanguageRegexStr } from "./matchers"; -import {SortingSpecProcessor} from "./sorting-spec-processor"; describe('Plain numbers regexp', () => { let regexp: RegExp; diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index 2e2d3be..8656ccd 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -14,8 +14,16 @@ import { RomanNumberNormalizerFn, SortingSpecProcessor } from "./sorting-spec-processor" -import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types"; -import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules"; +import { + CustomSortGroupType, + CustomSortOrder, + CustomSortSpec, + IdentityNormalizerFn +} from "./custom-sort-types"; +import { + FolderMatchingRegexp, + FolderMatchingTreeNode +} from "./folder-matching-rules"; const txtInputExampleA: string = ` order-asc: a-z @@ -37,6 +45,13 @@ starred: /:files starred: /folders starred: +:::: folder of bookmarks +< by-bookmarks-order +/: bookmarked: + < by-bookmarks-order +/ Abc + > by-bookmarks-order + :::: Conceptual model /: Entities % @@ -95,6 +110,13 @@ target-folder: tricky folder 2 /:files 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 /:files Entities % @@ -194,6 +216,29 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = { '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": { groups: [{ exactText: "Entities", diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index bef3c97..8a41145 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -120,6 +120,7 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = { 'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced}, 'standard': {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:' @@ -241,6 +242,8 @@ const HideItemVerboseLexeme: string = '/--hide:' const MetadataFieldIndicatorLexeme: string = 'with-metadata:' +const BookmarkedItemIndicatorLexeme: string = 'bookmarked:' + const StarredItemsIndicatorLexeme: string = 'starred:' const IconIndicatorLexeme: string = 'with-icon:' @@ -1600,6 +1603,13 @@ export class SortingSpecProcessor { foldersOnly: spec.foldersOnly, 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)) { const iconName: string | undefined = extractIdentifier(theOnly.substring(IconIndicatorLexeme.length)) return { diff --git a/src/main.ts b/src/main.ts index 022f4d9..3ab223a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,27 +1,38 @@ import { + apiVersion, App, FileExplorerView, + Menu, + MenuItem, MetadataCache, - Notice, normalizePath, + Notice, Platform, Plugin, PluginSettingTab, + requireApiVersion, sanitizeHTMLToDom, setIcon, Setting, TAbstractFile, TFile, TFolder, - Vault + Vault, WorkspaceLeaf } from 'obsidian'; import {around} from 'monkey-around'; import { folderSort, + ObsidianStandardDefaultSortingName, ProcessingContext, + sortFolderItemsForBookmarking } from './custom-sort/custom-sort'; -import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor'; -import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types'; +import { + SortingSpecProcessor, + SortSpecsCollection +} from './custom-sort/sorting-spec-processor'; +import { + CustomSortSpec +} from './custom-sort/custom-sort-types'; import { addIcons, @@ -33,9 +44,19 @@ import { ICON_SORT_SUSPENDED_SYNTAX_ERROR } from "./custom-sort/icons"; import {getStarredPlugin} from "./utils/StarredPluginSignature"; - +import { + BookmarksPluginInterface, + getBookmarksPlugin, + groupNameForPath +} from "./utils/BookmarksCorePluginSignature"; import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature"; import {lastPathComponent} from "./utils/utils"; +import { + collectSortingAndGroupingTypes, + hasOnlyByBookmarkOrStandardObsidian, + HasSortingOrGrouping, + ImplicitSortspecForBookmarksIntegration +} from "./custom-sort/custom-sort-utils"; interface CustomSortPluginSettings { additionalSortspecFile: string @@ -43,6 +64,9 @@ interface CustomSortPluginSettings { statusBarEntryEnabled: boolean notificationsEnabled: boolean mobileNotificationsEnabled: boolean + automaticBookmarksIntegration: boolean + bookmarksContextMenus: boolean + bookmarksGroupToConsumeAsOrderingReference: string } 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 statusBarEntryEnabled: 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 = { + automaticBookmarksIntegration: true, + bookmarksContextMenus: true } const SORTSPEC_FILE_NAME: string = 'sortspec.md' @@ -87,6 +120,16 @@ export default class CustomSortPlugin extends Plugin { this.sortSpecCache = null 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) => { if (failed) return 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 (this.fileExplorerFolderPatched) { 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 = 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 = 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() { @@ -328,12 +502,12 @@ export default class CustomSortPlugin extends Plugin { return sortSpec } - createProcessingContextForSorting(): ProcessingContext { + createProcessingContextForSorting(has: HasSortingOrGrouping): ProcessingContext { const ctx: ProcessingContext = { _mCache: app.metadataCache, - starredPluginInstance: getStarredPlugin(), - - iconFolderPluginInstance: getIconFolderPlugin(), + starredPluginInstance: has.grouping.byStarred ? getStarredPlugin() : undefined, + bookmarksPluginInstance: has.grouping.byBookmarks || has.sorting.byBookmarks ? getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference, false, true) : undefined, + iconFolderPluginInstance: has.grouping.byIcon ? getIconFolderPlugin() : undefined, plugin: this } return ctx @@ -365,8 +539,18 @@ export default class CustomSortPlugin extends Plugin { const folder: TFolder = this.file 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) { - return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting()); + return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting(has)); } else { return old.call(this, ...args); } @@ -380,6 +564,24 @@ export default class CustomSortPlugin extends Plugin { } } + orderedFolderItemsForBookmarking(folder: TFolder, bookmarksPlugin: BookmarksPluginInterface): Array { + 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 getFileExplorer(): FileExplorerView | undefined { let fileExplorer: FileExplorerView | undefined = app.workspace.getLeavesOfType("file-explorer")?.first() @@ -397,7 +599,12 @@ export default class CustomSortPlugin extends Plugin { } 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() { @@ -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 { plugin: CustomSortPlugin; @@ -492,5 +703,60 @@ class CustomSortSettingTab extends PluginSettingTab { this.plugin.settings.mobileNotificationsEnabled = value; 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' + + '
sorting-spec:
configurations and they can nicely cooperate.' + + '
' + + '

To separate regular bookmarks from the bookmarks created for sorting, you can put ' + + 'the latter in a separate dedicated bookmarks group. The default name of the group is ' + + "'" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "' " + + 'and you can change the group name in the configuration field below.' + + '
' + + 'If left empty, all the bookmarked items will be used to impose the order in File Explorer.

' + + '

More information on this functionality in the ' + + '' + + 'manual of this custom-sort plugin.' + + '

' + ) + + 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 Custom-sort: bookmark for sorting and Custom-sort: bookmark+siblings for sorting. 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(); + })) } } diff --git a/src/utils/Bookmarks Core Plugin integration design.md b/src/utils/Bookmarks Core Plugin integration design.md new file mode 100644 index 0000000..7307afa --- /dev/null +++ b/src/utils/Bookmarks Core Plugin integration design.md @@ -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 + + + diff --git a/src/utils/BookmarksCorePluginSignature.spec.ts b/src/utils/BookmarksCorePluginSignature.spec.ts new file mode 100644 index 0000000..a83d0eb --- /dev/null +++ b/src/utils/BookmarksCorePluginSignature.spec.ts @@ -0,0 +1,1065 @@ +import { + _unitTests, + BookmarkedParentFolder, + OrderedBookmarks +} from "./BookmarksCorePluginSignature"; +import {extractParentFolderPath} from "./utils"; + +type Bookmarks_PluginInstance = any + +const getEmptyBookmarksMock = (): Bookmarks_PluginInstance => ({} as Bookmarks_PluginInstance) + +const getNullBookmarksMock = (): Bookmarks_PluginInstance => ({ + items: null + } as unknown as Bookmarks_PluginInstance) + +const getBookmarksMock = (items: any): Bookmarks_PluginInstance => ({ + items: items +} as Bookmarks_PluginInstance) + +const getBrokenBookmarksMock1 = (): Bookmarks_PluginInstance => ({ + items: 123 +} as Bookmarks_PluginInstance) + +const getBrokenBookmarksMock2 = (): Bookmarks_PluginInstance => ({ + items: () => { return [] } +} as Bookmarks_PluginInstance) + +const derivePath = (previousPath: string|undefined, path: string): string => { + const match = path?.match(/(^\s+\/)/) + if (match && previousPath) { + const lengthToTake = match[1].length + let derivedPart = previousPath.substring(0, lengthToTake) + derivedPart = derivedPart.endsWith('/') ? derivedPart : `${derivedPart}/` + const newPathSuffix = path.substring(lengthToTake) + return `${derivedPart}${newPathSuffix}` + } else { + return path + } +} + +interface PathAndOrder { + path: string + order: number +} + +class bkCacheMock { + entries: Array = [] + coveredPaths: {[key: string]: boolean} = {} + + getCache() { + const cache: OrderedBookmarks = {} + this.entries.forEach((it) => { + cache[it.path] = it.order + }) + return cache + } + getPathsCoverage() { + return this.coveredPaths + } + getExpected() { + return [this.getCache(), this.getPathsCoverage()] + } + add(path: string, order: number): bkCacheMock { + path = derivePath(this.entries.slice(-1)?.[0]?.path, path) + this.entries.push({ + path: path, + order: order + }) + this.coveredPaths[extractParentFolderPath(path) || '/'] = true + return this + } +} + +const attachRemoveFunction = (arr: Array): Array => { + arr.remove = (itemToRemove: any) => { + if (itemToRemove) { + let index + do { + index = arr?.findIndex((it) => it === itemToRemove) + if (index !== -1) { + arr?.splice(index, 1) + } + } while (index !== -1) + } + } + return arr +} + +const consumeBkMock = (tree: any): Array => { + const bkmrks: Array = attachRemoveFunction([]) + const pathOf = (s: string) => { + const match = s.match(/^\d+:(.+)$/) + return match ? match[1] : s + } + const typeOf = (s: string) => { + const match = s.match(/(file|folder)/) + return match ? match[1] : 'unknown' + } + const hasIndicator = (s: string) => { + return s.endsWith('^') + } + const consumeLevel = (entries: any, container?: Array) => { + Object.keys(entries).forEach((entryKey) => { + container = container ?? attachRemoveFunction([]) + const value = entries[entryKey] + if ('string' === typeof value) { // file or folder + const path = pathOf(entryKey) + container.push({ + type: typeOf(value), + path: pathOf(entryKey), + subpath: hasIndicator(value) ? '#^-' : undefined + }) + } else { // group + container.push({ + type: 'group', + items: consumeLevel(value), + title: entryKey + }) + } + }) + return container || attachRemoveFunction([]) + } + consumeLevel(tree, bkmrks) + return bkmrks +} + +describe('getOrderedBookmarks - basic scenarios', () => { + it('should correctly handle no bookmarks plugin scenario', () => { + const result = _unitTests.getOrderedBookmarks(null!) + expect(result).toEqual([undefined, undefined]) + }) + it('should correctly handle no bookmarks scenario', () => { + const result = _unitTests.getOrderedBookmarks(getEmptyBookmarksMock()) + expect(result).toEqual([undefined, undefined]) + }) + it('should correctly handle edge case of the bookmarks plugin responding with null', () => { + const result = _unitTests.getOrderedBookmarks(getNullBookmarksMock()) + expect(result).toEqual([undefined, undefined]) + }) + it('should correctly handle bookmarks plugin responding with empty collection', () => { + const items: [] = [] + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items)) + expect(result).toEqual([{}, {}]) + }) + it('should correctly handle basic scenario - one bookmarked item at root level - file', () => { + const items: Array = consumeBkMock({'some note.md': 'file'}) + const expected = new bkCacheMock() + .add('some note.md', 1) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items)) + expect(result).toEqual(expected) + }) + it('should correctly handle basic scenario - one bookmarked item at root level - file with subpath', () => { + const items: Array = consumeBkMock({'some note.md': 'file^'}) + const expected = new bkCacheMock() + .add('some note.md', 1) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items)) + expect(result).toEqual(expected) + }) + it('should correctly handle basic scenario - one bookmarked item at root level - folder', () => { + const items: Array = consumeBkMock({'some folder': 'folder'}) + const expected = new bkCacheMock() + .add('some folder', 1) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items)) + expect(result).toEqual(expected) + }) + it('should correctly handle basic scenario - one bookmarked item at root level - group', () => { + const items: Array = consumeBkMock({'sortspec': {}}) + const expected = new bkCacheMock() + .add('sortspec', 1) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items)) + expect(result).toEqual(expected) + }) + it('should correctly handle basic scenario - only the group with expected name - the container', () => { + const items: Array = consumeBkMock({'sortspec': {}}) + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), 'sortspec') + expect(result).toEqual([{}, {}]) + }) + it('should correctly handle basic scenario - one bookmarked item in the group of expected name', () => { + const items: [] = [] + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items)) + expect(result).toEqual([{}, {}]) + }) +}) + +describe('getOrderedBookmarks edge cases', () => { + it('should correctly handle bookmarks plugin not returning collection (a number)', () => { + const result = _unitTests.getOrderedBookmarks(getBrokenBookmarksMock1()) + expect(result).toEqual([undefined, undefined]) + }) + it('should correctly handle bookmarks plugin not returning collection (a function)', () => { + const result = _unitTests.getOrderedBookmarks(getBrokenBookmarksMock1()) + expect(result).toEqual([undefined, undefined]) + }) + it('edge case - group vs group', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\folder l1": {}, // order 1 + "folder l1": {} + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('edge case - folder vs group', () => { + const items = consumeBkMock({ + "sortspec": { + "1:folder l1": "folder", // order 1 + "folder l1": {} // order 2 + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 2) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('edge case - file w/ and w/o indicator vs group - group overwrites w/o indicator, regardless of matching (or not) path of file', () => { + const items = consumeBkMock({ + "sortspec": { + "1:artificial": "file", // order 1 + "artificial": {}, // order 2 + "subf": { // order 3 + "subf/item in subf": "file", // order 4 + "item in subf": {} // order 5 + } + } + }) + + const expected = new bkCacheMock() + .add("artificial", 2) + .add("subf", 3) + .add(" /item in subf", 5) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('edge case - file w/ and w/o indicator vs group - group is not overwritten by w/o indicator', () => { + const items = consumeBkMock({ + "sortspec": { + "artificial": {}, // order 1 + "1:artificial": "file", + "\\\\subf": { + "item in subf": {}, // order 2 + "subf/item in subf": "file", + } + } + }) + + const expected = new bkCacheMock() + .add("artificial", 1) + .add("subf/item in subf", 2) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('edge case - file w/ and w/o indicator vs group - group doesn`t overwrite w/ indicator', () => { + const items = consumeBkMock({ + "sortspec": { + "1:artificial": "file", // order 1 + "2:artificial": "file^", // order 2 + "artificial": {} // order 3 + } + }) + + const expected = new bkCacheMock() + .add("artificial", 2) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('edge case - file w/ and w/o indicator vs group - w/ indicator overwrites group', () => { + const items = consumeBkMock({ + "sortspec": { + "1:artificial": "file", // order 1 + "artificial": {}, // order 2 + "2:artificial": "file^", // order 3 + } + }) + + const expected = new bkCacheMock() + .add("artificial", 3) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('edge case - folder is treated as a file w/o indicator - scenario 1', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group": { + "1:group/artificial": "folder", // order 1 + "2:group/artificial": "file", // order 2 + } + } + }) + + const expected = new bkCacheMock() + .add("group/artificial", 1) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('edge case - folder is treated as a file w/o indicator - scenario 2', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group": { + "1:group/artificial": "folder", // order 1 + "2:group/artificial": "file^", // order 2 + } + } + }) + + const expected = new bkCacheMock() + .add("group/artificial", 2) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) +}) + +const DEFAULT_BKMRK_FOLDER = 'sortspec' + +describe('getOrderedBookmarks', () => { + describe('files', () => { + it('case 1 - both w/ indicator and not matching path. Ignore the latter.', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1/not relevant for test.md": "file^", // order 1 + "1:folder l1/folder l2/file 1 at level 2.md": "file^", // order 2 + "2:folder l1/folder l2/file 1 at level 2.md": "file^" // order 3 -> reject, a duplicate + } + }) + const expected = new bkCacheMock() + .add('folder l1/not relevant for test.md', 1) + .add(' /folder l2/file 1 at level 2.md', 2) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 2 - both w/ indicator, matching path wins.', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\folder l1": { + "folder l1/folder l2/file 1 at level 2.md": "file^", // ignore => invalid location + "\\\\folder l2": { + "folder l1/folder l2/not relevant for test.md": "file^", // order 1 + "\\\\folder l3": { + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 2, to be taken -> location match + }, + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", + "2:folder l1/folder l2/folder l3/file 1 at level 3.md": "folder", + } + }, + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // reject => already found in correct location + } + }) + + const expected = new bkCacheMock() + .add('folder l1/folder l2/not relevant for test.md', 1) + .add(" /folder l3/file 1 at level 3.md", 2) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 3 - w/ indicator wins', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 1 -> overwritten by w/ indicator + "folder l1/folder l2/not relevant for test.md": "file^", // order 2 + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 3 -> taken, overwrites item w/o indicator + } + }) + + const expected = new bkCacheMock() + .add('folder l1/folder l2/not relevant for test.md', 2) + .add(" /folder l3/file 1 at level 3.md", 3) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 4 - w/ indicator wins', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1/folder l2/not relevant for test.md": "file^", // order 1 + "folder l1": { // order 2 + "\\\\folder l2": { + "\\\\folder l3": { + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 3 -> overwritten by w/ indicator + } + } + }, + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 4 -> taken, overwrites item w/o indicator + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 2) + .add("folder l1/folder l2/not relevant for test.md", 1) + .add(" /folder l3/file 1 at level 3.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 5 - both w/ indicator, matching path wins', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1/folder l2/not relevant for test.md": "file^", // order 1 + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 2 -> overwritten by matching path + "\\\\folder l1": { + "folder l2": { // order 3 + "\\\\folder l3": { + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 4 -> overwrites item at root path + } + } + }, + } + }) + + const expected = new bkCacheMock() + .add("folder l1/folder l2", 3) + .add("folder l1/folder l2/not relevant for test.md", 1) + .add(" /folder l3/file 1 at level 3.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 6 - both w/ indicator and matching path. Ignore the latter', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\folder l1": { + "folder l2": { // order 1 + "\\\\folder l3": { + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 2 -> taken + "2:folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // rejected as duplicate + }, + "folder l1/folder l2/not relevant for test.md": "file", // order 3 + } + }, + } + }) + + const expected = new bkCacheMock() + .add("folder l1/folder l2", 1) + .add("folder l1/folder l2/not relevant for test.md", 3) + .add(" /folder l3/file 1 at level 3.md", 2) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 7 - w/ indicator wins', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 1 -> overwritten by w/ indicator + "folder l1": { // order 2 + "\\\\folder l2": { + "folder l3": { // order 3 + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 4 -> taken, overwrites w/o indicator + }, + "folder l1/folder l2/not relevant for test.md": "file", // order 5 + } + }, + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 2) + .add("folder l1/folder l2/folder l3", 3) + .add("folder l1/folder l2/not relevant for test.md", 5) + .add(" /folder l3/file 1 at level 3.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 8 - w/ indicator wins', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "\\\\folder l2": { + "folder l3": { // order 2 + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 3 -> overwritten by w/ indicator + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 4 -> taken, overwrites w/o indicator + }, + "folder l1/folder l2/not relevant for test.md": "file", // order 5 + } + }, + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add("folder l1/folder l2/folder l3", 2) + .add("folder l1/folder l2/not relevant for test.md", 5) + .add(" /folder l3/file 1 at level 3.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 9 - Item w/o indicator never overwrites the one with the indicator', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "\\\\folder l2": { + "folder l3": { // order 2 + }, + "folder l1/folder l2/not relevant for test.md": "file", // order 3 + } + }, + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 4 -> taken, + "2:folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // ignored as duplicate w/o indicator + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add("folder l1/folder l2/folder l3", 2) + .add("folder l1/folder l2/not relevant for test.md", 3) + .add(" /folder l3/file 1 at level 3.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 10 - Item w/o indicator never overwrites the one with the indicator', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "\\\\folder l2": { + "folder l3": { // order 2 + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 3 -> taken, + }, + "folder l1/folder l2/not relevant for test.md": "file", // order 4 + } + }, + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // ignored as duplicate w/o indicator and not matching path + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add("folder l1/folder l2/folder l3", 2) + .add("folder l1/folder l2/not relevant for test.md", 4) + .add(" /folder l3/file 1 at level 3.md", 3) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 11 - both w/o indicator and not matching path. Ignore the latter.', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "\\\\folder l2": { + "folder l3": { // order 2 + }, + }, + }, + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 3 -> taken + "2:folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // ignored as duplicate w/o indicator and not matching path + "folder l1/folder l2/not relevant for test.md": "folder", // order 4 + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add("folder l1/folder l2/folder l3", 2) + .add(" /folder l3/file 1 at level 3.md", 3) + .add("folder l1/folder l2/not relevant for test.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 12 - both w/o indicator, matching path is not overwritten by not matching', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "\\\\folder l2": { + "folder l3": { // order 2 + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 3 -> taken + }, + }, + }, + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // ignored as duplicate w/o indicator and not matching path + "folder l1/folder l2/not relevant for test.md": "folder", // order 4 + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add("folder l1/folder l2/folder l3", 2) + .add(" /folder l3/file 1 at level 3.md", 3) + .add("folder l1/folder l2/not relevant for test.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 13 - w/ indicator wins', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "\\\\folder l2": { + "folder l3": { // order 2 + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 3 -> overwritten by w/ indicator + }, + }, + }, + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 4 -> w/ indicator overwrites w/o indicator + "folder l1/folder l2/not relevant for test.md": "folder", // order 5 + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add("folder l1/folder l2/folder l3", 2) + .add(" /folder l3/file 1 at level 3.md", 4) + .add("folder l1/folder l2/not relevant for test.md", 5) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 14 - w/ indicator wins', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "\\\\folder l2": { + "folder l3": { // order 2 + "1:folder l1/folder l2/folder l3/file 1 at level 3.md": "file^", // order 3 + "2:folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // duplicate, ignored regardless of w/ indicator and matching path + }, + }, + }, + + "folder l1/folder l2/not relevant for test.md": "folder", // order 5 + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add(" /folder l2/folder l3", 2) + .add(" /file 1 at level 3.md", 3) + .add(" /not relevant for test.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 15 - both w/o indicator, matching path wins', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 1 -> overwritten by matching path + "folder l1": { // order 2 + "\\\\folder l2": { + "folder l3": { // order 3 + "folder l1/folder l2/folder l3/file 1 at level 3.md": "file", // order 4 -> overwrites not matching path + }, + }, + }, + "folder l1/folder l2/not relevant for test.md": "folder", // order 5 + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 2) + .add("folder l1/folder l2/folder l3", 3) + .add(" /file 1 at level 3.md", 4) + .add(" /not relevant for test.md", 5) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 15 - both w/o indicator and matching path. Ignore the latter.', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "folder l2": { // order 2 + "folder l3": { // order 3 + }, + "1:folder l1/folder l2/file 1 at level 2.md": "file", // order 4 -> overwrites not matching path + "2:folder l1/folder l2/file 1 at level 2.md": "file", // ignored as duplicate + }, + }, + "folder l1/folder l2/not relevant for test.md": "folder", // order 5 + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add(" /folder l2", 2) + .add(" /folder l3", 3) + .add(" /file 1 at level 2.md", 4) + .add(" /not relevant for test.md", 5) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + it('case 16 and 17 - ignore invalid locations', () => { + const items = consumeBkMock({ + "sortspec": { + "folder l1": { // order 1 + "folder l1/folder l2/file 1 at level 2.md": "file^", + "folder l2": { // order 2 + "folder l3": { // order 3 + "folder l1/folder l2/file 1 at level 2.md": "file^", + }, + "folder l1/file 1 at level 1.md": "file", + }, + }, + "folder l1/folder l2/not relevant for test.md": "folder", // order 4 + } + }) + + const expected = new bkCacheMock() + .add("folder l1", 1) + .add(" /folder l2", 2) + .add(" /folder l3", 3) + .add(" /not relevant for test.md", 4) + .getExpected() + const result = _unitTests.getOrderedBookmarks(getBookmarksMock(items), DEFAULT_BKMRK_FOLDER) + expect(result).toEqual(expected) + }) + }) +}) + +/* +Duplicate elimination logic matrix + +Originally table created in Excel, then copied & pasted toObsidian, which creates a good md format +Then converted from md to reStructuredText grid format via https://tableconvert.com/markdown-to-restructuredtext +(a great tables generator/converter in various formats) + ++---------+--------+---------------------+---------------------+----------------+--+---------------------+---------------------+--------+--+-------------+----------------------------------------------------------------------+ +| Case id | | alreadyConsumed | | | | new | | | | reject new? | Comment / scenario | +| | Object | hasSortingIndicator | bookmarkPathMatches | path=/ | | hasSortingIndicator | bookmarkPathMatches | path=/ | | | | ++=========+========+=====================+=====================+================+==+=====================+=====================+========+==+=============+======================================================================+ +| 1 | file | yes | no | yes | | yes | no | yes | | reject | both w/ indicator and not matching path. Ignore the latter. | +| 2 | file | yes | yes | N/R | | yes | no | yes | | reject | both w/ indicator, matching path wins | +| 3 | file | no | no | yes | | yes | no | yes | | take | w/ indicator wins | +| 4 | file | no | yes | N/R | | yes | no | yes | | take | w/ indicator wins | +| 5 | file | yes | no | yes | | yes | yes | N/R | | take | both w/ indicator, matching path wins | +| 6 | file | yes | yes | N/R | | yes | yes | N/R | | reject | both w/ indicator and matching path. Ignore the latter | +| 7 | file | no | no | yes | | yes | yes | N/R | | take | w/ indicator wins | +| 8 | file | no | yes | N/R | | yes | yes | N/R | | take | w/ indicator wins | +| 9 | file | yes | no | yes | | no | no | yes | | reject | Item w/o indicator never overwrites the one with the indicator | +| 10 | file | yes | yes | N/R | | no | no | yes | | reject | Item w/o indicator never overwrites the one with the indicator | +| 11 | file | no | no | yes | | no | no | yes | | reject | both w/o indicator and not matching path. Ignore the latter. | +| 12 | file | no | yes | N/R | | no | no | yes | | reject | both w/o indicator, matching path is not overwritten by not matching | +| 13 | file | yes | no | yes | | no | yes | N/R | | reject | w/ indicator wins | +| 14 | file | yes | yes | N/R | | no | yes | N/R | | reject | w/ indicator wins | +| 15 | file | no | no | yes | | no | yes | N/R | | take | both w/o indicator, matching path wins | +| 16 | file | no | yes | N/R | | no | yes | N/R | | reject | both w/o indicator and matching path. Ignore the latter. | +| | | | | | | | | | | | | +| 17 | file | Doesn't matter | Doesn't matter | Doesn't matter | | no | no | no | | reject | An item in neither of correct bookmarks locations. | +| 18 | file | Doesn't matter | Doesn't matter | Doesn't matter | | yes | no | no | | reject | An item in neither of correct bookmarks locations. | ++---------+--------+---------------------+---------------------+----------------+--+---------------------+---------------------+--------+--+-------------+----------------------------------------------------------------------+ + + +- N/R = Not Relevant +- file and folder bookmark of the same path ==> not distinguished. Folders are a subset of the above matrix. + +edge cases when path of group or folder or file overlaps + - group vs group - the first one wins, to have consistent rules (should never occur, defensive programming) + - group always wins over folder or a file w/o indicator + - group vs file w/ indicator - w/ indicator always wins + - folder vs file - not distinguished, folder treated as a file w/o indicator + +Group name with double backslash prefix: \\groupName represent a bookmark not used for sorting, only used for the structure + */ + +describe('bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants', () => { + it('empty (undefined)', () => { + const sortspecGroup = { + type: 'group', + title: 'sortspec' + } + + const result = _unitTests.bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(sortspecGroup as any) + expect(result).toBeTruthy() + }) + it('empty (zero length array)', () => { + const items = consumeBkMock({ + "sortspec": { + } + }) + + const result = _unitTests.bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(items[0]) + expect(result).toBeTruthy() + }) + it('complex empty structure', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "\\\\group l3.1": {} + }, + "\\\\group l2.2": {} + }, + "\\\\group l1.2": { + "\\\\group l2.1": {} + } + } + }) + + const result = _unitTests.bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(items[0]) + expect(result).toBeTruthy() + }) + it('complex structure - one nested not transparent group', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "group l3.1": {} + }, + "\\\\group l2.2": {} + }, + "\\\\group l1.2": { + "\\\\group l2.1": {} + } + } + }) + + const result = _unitTests.bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(items[0]) + expect(result).toBeFalsy() + }) + it('complex structure - one nested file', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "\\\\group l3.1": {} + }, + "\\\\group l2.2": { + "some file (invalid location, yet anyways a file)": "file" + } + }, + "\\\\group l1.2": { + "\\\\group l2.1": {} + } + } + }) + + const result = _unitTests.bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(items[0]) + expect(result).toBeFalsy() + }) + it('complex structure - one not nested folder', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "\\\\group l3.1": {} + }, + "\\\\group l2.2": {} + }, + "\\\\group l1.2": { + "\\\\group l2.1": {} + }, + "a folder (must be manual)": "folder" + } + }) + + const result = _unitTests.bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(items[0]) + expect(result).toBeFalsy() + }) +}) + +describe('cleanupBookmarkTreeFromTransparentEmptyGroups', () => { + it('should delete the structure up to the bookmark container group (starting point deep in hierarchy)', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "\\\\group l3.1": { + "\\\\deepest": {} + } + }, + "\\\\group l2.2": {} + }, + "\\\\group l1.2": { + "\\\\group l2.1": {} + } + } + }) + const plugin = getBookmarksMock(items) + const parentFolder: BookmarkedParentFolder|undefined = _unitTests.findGroupForItemPathInBookmarks( + 'group l1.1/group l2.1/group l3.1/deepest', + false, + plugin, + 'sortspec' + ) + _unitTests.cleanupBookmarkTreeFromTransparentEmptyGroups( + parentFolder, + plugin, + 'sortspec' + ) + expect(JSON.parse(JSON.stringify(items))).toEqual(JSON.parse(JSON.stringify(consumeBkMock({ + "sortspec": {} + })))) + }) + it('should delete 1st level group and not delete the bookmark container group (starting point flat)', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.2": {} + } + }) + const plugin = getBookmarksMock(items) + const parentFolder: BookmarkedParentFolder|undefined = _unitTests.findGroupForItemPathInBookmarks( + 'group l1.2', + false, + plugin, + 'sortspec' + ) + _unitTests.cleanupBookmarkTreeFromTransparentEmptyGroups( + parentFolder, + plugin, + 'sortspec' + ) + expect(JSON.parse(JSON.stringify(items))).toEqual(JSON.parse(JSON.stringify(consumeBkMock({ + "sortspec": {} + })))) + }) + it('should delete the structure up to the non-empty group', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "\\\\group l3.1": { + "\\\\deepest": {} + } + }, + "\\\\group l2.2": {} + }, + "\\\\group l1.2": { + "\\\\group l2.1": {}, + "group l2.2": {} + } + } + }) + const plugin = getBookmarksMock(items) + const parentFolder: BookmarkedParentFolder|undefined = _unitTests.findGroupForItemPathInBookmarks( + 'group l1.1/group l2.1/group l3.1/deepest', + false, + plugin, + 'sortspec' + ) + _unitTests.cleanupBookmarkTreeFromTransparentEmptyGroups( + parentFolder, + plugin, + 'sortspec' + ) + expect(JSON.parse(JSON.stringify(items))).toEqual(JSON.parse(JSON.stringify(consumeBkMock({ + "sortspec": { + "\\\\group l1.2": { + "\\\\group l2.1": {}, + "group l2.2": {} + } + } + })))) + }) + it('should not delete the structure because of sibling', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "\\\\group l3.1": { + "\\\\deepest 1": {}, + "deepest 2": "file^" + + } + }, + "\\\\group l2.2": { + "\\\\deepest 1": {} + } + }, + "\\\\group l1.2": { + "\\\\group l2.1": { + "\\\\deepest 1": {} + } + } + } + }) + const plugin = getBookmarksMock(items) + const parentFolder: BookmarkedParentFolder|undefined = _unitTests.findGroupForItemPathInBookmarks( + 'group l1.1/group l2.1/group l3.1/deepest', + false, + plugin, + 'sortspec' + ) + _unitTests.cleanupBookmarkTreeFromTransparentEmptyGroups( + parentFolder, + plugin, + 'sortspec' + ) + expect(JSON.parse(JSON.stringify(items))).toEqual(JSON.parse(JSON.stringify(consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "\\\\group l3.1": { + "\\\\deepest 1": {}, + "deepest 2": "file^" + + } + }, + "\\\\group l2.2": { + "\\\\deepest 1": {} + } + }, + "\\\\group l1.2": { + "\\\\group l2.1": { + "\\\\deepest 1": {} + } + } + } + })))) + }) + it('should delete the structure after multi-clean invocation', () => { + const items = consumeBkMock({ + "sortspec": { + "\\\\group l1.1": { + "\\\\group l2.1": { + "\\\\group l3.1": { + "\\\\deepest 1": {} + } + }, + "\\\\group l2.2": { + "\\\\deepest 2": {} + } + }, + "\\\\group l1.2": { + "\\\\group l2.1": { + "\\\\deepest 3": {} + } + } + } + }) + const plugin = getBookmarksMock(items) + const parentFolder1: BookmarkedParentFolder|undefined = _unitTests.findGroupForItemPathInBookmarks( + 'group l1.1/group l2.1/group l3.1/deepest 1', + false, + plugin, + 'sortspec' + ) + _unitTests.cleanupBookmarkTreeFromTransparentEmptyGroups( + parentFolder1, + plugin, + 'sortspec' + ) + const parentFolder2: BookmarkedParentFolder|undefined = _unitTests.findGroupForItemPathInBookmarks( + 'group l1.2/group l2.1/deepest 3', + false, + plugin, + 'sortspec' + ) + _unitTests.cleanupBookmarkTreeFromTransparentEmptyGroups( + parentFolder2, + plugin, + 'sortspec' + ) + expect(JSON.parse(JSON.stringify(items))).toEqual(JSON.parse(JSON.stringify(consumeBkMock({ + "sortspec": {} + })))) + }) +}) diff --git a/src/utils/BookmarksCorePluginSignature.ts b/src/utils/BookmarksCorePluginSignature.ts new file mode 100644 index 0000000..a2fff74 --- /dev/null +++ b/src/utils/BookmarksCorePluginSignature.ts @@ -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 { +} + +interface BookmarkedFile extends BookmarkItemSuperset { + type: 'file' +} + +interface BookmarkedFolder extends Omit { + type: 'folder' +} + +interface BookmarkedGroup extends Omit { + type: 'group' + items: Array +} + +export type BookmarkedItemPath = string + +export interface OrderedBookmarkedItemWithMetadata { + isGroup?: boolean + path: BookmarkedItemPath + hasSortingIndicator?: boolean + order: number + bookmarkPathMatches?: boolean +} + +export type OrderedBookmarkedItem = Pick +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 | undefined + [BookmarksPlugin_items_collectionName]: Array + 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, inTheTop?: boolean): void + unbookmarkSiblings(siblings: Array): 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): 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, 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) => { + 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 = [] + 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, callbackConsumeItem: TraverseCallback) => { + if (!Array.isArray(items)) return + const recursiveTraversal = (collection: Array, 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 | 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 // 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 = 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 = [] + 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 +} diff --git a/src/utils/utils.spec.ts b/src/utils/utils.spec.ts index 5189d85..eeb8eb5 100644 --- a/src/utils/utils.spec.ts +++ b/src/utils/utils.spec.ts @@ -1,4 +1,7 @@ -import {lastPathComponent, extractParentFolderPath} from "./utils"; +import { + lastPathComponent, + extractParentFolderPath +} from "./utils"; describe('lastPathComponent and extractParentFolderPath', () => { it.each([ @@ -6,14 +9,14 @@ describe('lastPathComponent and extractParentFolderPath', () => { ['a/subfolder', 'a', 'subfolder'], ['parent/child', 'parent', 'child'], ['','',''], - [' ','',''], + [' ','',' '], ['/strange', '', 'strange'], ['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'], ['/','',''], - [' / ','',''], - [' /','',''], - ['/ ','',''] + [' / ',' ',' '], + [' /',' ',''], + ['/ ','',' '] ])('should from %s extract %s and %s', (path: string, parentPath: string, lastComponent: string) => { const extractedParentPath: string = extractParentFolderPath(path) const extractedLastComponent: string = lastPathComponent(path) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5afece4..4e767d5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -9,10 +9,10 @@ export function last(o: Array): T | undefined { export function lastPathComponent(path: string): string { 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 { const lastPathSeparatorIdx = (path ?? '').lastIndexOf('/') - return lastPathSeparatorIdx > 0 ? path.substring(0, lastPathSeparatorIdx).trim() : '' + return lastPathSeparatorIdx > 0 ? path.substring(0, lastPathSeparatorIdx) : '' } diff --git a/yarn.lock b/yarn.lock index 4a78e36..ba84ad8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -763,6 +763,13 @@ dependencies: "@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@*": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" @@ -2396,6 +2403,14 @@ npm-run-path@^4.0.1: dependencies: 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: version "0.15.9" resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-0.15.9.tgz#b6e0b566952643db6b55f7e74fbba6d7140fe4a2" From a16f8cc06f6672287659da18da206d28c04f98dd Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:33:45 +0200 Subject: [PATCH 02/10] Fix of broken internal links in README.md --- README.md | 12 ++++++------ advanced-README.md => docs/advanced-README.md | 0 2 files changed, 6 insertions(+), 6 deletions(-) rename advanced-README.md => docs/advanced-README.md (100%) diff --git a/README.md b/README.md index d12fb69..b35e3cf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ > This is a simple version of README which highlights the **basic scenario and most commonly used feature** > -> The [long and much more detailed advanced-README.md is here](./advanced-README.md) +> The [long and much more detailed advanced-README.md is here](./docs/advanced-README.md) --- ## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) @@ -60,11 +60,11 @@ An illustrative image which shows the reverse alphabetical order applied to the > - changing the sorting order via the standard Obsidian UI button won't affect your folder, unless... > - ...unless you deactivate the custom sorting via clicking the ribbon button to make it (![Inactive](./docs/icons/icon-inactive.png)) > - for clarity: the underlying file of the note `sortspec` is obviously `sortspec.md` -> - in case of troubles refer to the [TL;DR section of advanced README.md](./advanced-README.md#tldr-usage) +> - in case of troubles refer to the [TL;DR section of advanced README.md](./docs/advanced-README.md#tldr-usage) > - feel free to experiment! The plugin works in a non-destructive fashion, and it doesn't modify the content of your vault. > It only changes the order in which the files and folders are displayed in File Explorer > - indentation matters in YAML -> the two leading spaces in ` order-desc: a-z` are intentional and required -> - this common example only touches the surface of the rich capabilities of this custom sorting plugin. For more details go to [advanced version of README.md](./advanced-README.md) +> - this common example only touches the surface of the rich capabilities of this custom sorting plugin. For more details go to [advanced version of README.md](./docs/advanced-README.md) --- ## Basic scenario 2: explicitly list folders and files in the order which you want @@ -137,7 +137,7 @@ The list of basic automatic sorting orders includes: ## Manual sorting The **manual ordering of notes and folders** is also done via the sorting configuration. -Refer to the [TL;DR section of advanced README.md](./advanced-README.md#tldr-usage) for examples and instructions +Refer to the [TL;DR section of advanced README.md](./docs/advanced-README.md#tldr-usage) for examples and instructions ## Ribbon icon @@ -157,7 +157,7 @@ On small-screen mobile devices (phones) the icon is static: - ![Static icon](./docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change -For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](./advanced-README.md#ribbon-icon) +For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](./docs/advanced-README.md#ribbon-icon) ## Small screen mobile devices remarks @@ -175,7 +175,7 @@ The plugin could and should be installed from the official Obsidian Community Pl or directly in the Obsidian app itself. Search the plugin by its name 'CUSTOM FILE EXPLORER SORTING' -> For other installation methods refer to [Installing the plugin section of advanced-README.md](./advanced-README.md#installing-the-plugin) +> For other installation methods refer to [Installing the plugin section of advanced-README.md](./docs/advanced-README.md#installing-the-plugin) ## Credits diff --git a/advanced-README.md b/docs/advanced-README.md similarity index 100% rename from advanced-README.md rename to docs/advanced-README.md From c3c8453561106056a7c642fb5ca0ee53287c2469 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:36:04 +0200 Subject: [PATCH 03/10] Fix of broken internal links in README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b35e3cf..6199a3b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ > This is a simple version of README which highlights the **basic scenario and most commonly used feature** > -> The [long and much more detailed advanced-README.md is here](./docs/advanced-README.md) +> The [long and much more detailed advanced-README.md is here](./blob/master/docs/advanced-README.md) --- ## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) @@ -60,11 +60,11 @@ An illustrative image which shows the reverse alphabetical order applied to the > - changing the sorting order via the standard Obsidian UI button won't affect your folder, unless... > - ...unless you deactivate the custom sorting via clicking the ribbon button to make it (![Inactive](./docs/icons/icon-inactive.png)) > - for clarity: the underlying file of the note `sortspec` is obviously `sortspec.md` -> - in case of troubles refer to the [TL;DR section of advanced README.md](./docs/advanced-README.md#tldr-usage) +> - in case of troubles refer to the [TL;DR section of advanced README.md](./blob/master/docs/advanced-README.md#tldr-usage) > - feel free to experiment! The plugin works in a non-destructive fashion, and it doesn't modify the content of your vault. > It only changes the order in which the files and folders are displayed in File Explorer > - indentation matters in YAML -> the two leading spaces in ` order-desc: a-z` are intentional and required -> - this common example only touches the surface of the rich capabilities of this custom sorting plugin. For more details go to [advanced version of README.md](./docs/advanced-README.md) +> - this common example only touches the surface of the rich capabilities of this custom sorting plugin. For more details go to [advanced version of README.md](./blob/master/docs/advanced-README.md) --- ## Basic scenario 2: explicitly list folders and files in the order which you want @@ -137,7 +137,7 @@ The list of basic automatic sorting orders includes: ## Manual sorting The **manual ordering of notes and folders** is also done via the sorting configuration. -Refer to the [TL;DR section of advanced README.md](./docs/advanced-README.md#tldr-usage) for examples and instructions +Refer to the [TL;DR section of advanced README.md](./blob/master/docs/advanced-README.md#tldr-usage) for examples and instructions ## Ribbon icon @@ -157,7 +157,7 @@ On small-screen mobile devices (phones) the icon is static: - ![Static icon](./docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change -For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](./docs/advanced-README.md#ribbon-icon) +For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](./blob/master/docs/advanced-README.md#ribbon-icon) ## Small screen mobile devices remarks @@ -175,7 +175,7 @@ The plugin could and should be installed from the official Obsidian Community Pl or directly in the Obsidian app itself. Search the plugin by its name 'CUSTOM FILE EXPLORER SORTING' -> For other installation methods refer to [Installing the plugin section of advanced-README.md](./docs/advanced-README.md#installing-the-plugin) +> For other installation methods refer to [Installing the plugin section of advanced-README.md](./blob/master/docs/advanced-README.md#installing-the-plugin) ## Credits From 61958721cb7205587f4508042383b260de41840c Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:43:42 +0200 Subject: [PATCH 04/10] Fix of broken internal links in README.md --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6199a3b..72bb5a3 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ sorting-spec: | --- ``` -Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png) or ![Static icon](./docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting specification and apply it. -The sorting should be applied to the folder. On desktops and tablets the ribbon icon should turn (![Active](./docs/icons/icon-active.png)) +Click the ribbon button (![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png) or ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting specification and apply it. +The sorting should be applied to the folder. On desktops and tablets the ribbon icon should turn (![Active](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-active.png)) !!! **Done!** !!! @@ -48,17 +48,17 @@ You should see the files and sub-folders in your folder sorted in reverse alphab An illustrative image which shows the reverse alphabetical order applied to the root folder of some vault: -![Basic example](./docs/svg/simplest-example-3.svg) +![Basic example](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/simplest-example-3.svg) --- ### Remarks > Remarks: -> - your new `sortspec` note should [look like this](./docs/examples/basic/sortspec.md?plain=1) except for the syntax highlighting, which could differ +> - your new `sortspec` note should [look like this](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/examples/basic/sortspec.md?plain=1) except for the syntax highlighting, which could differ > - you will notice that the folders and files are treated equally and thus intermixed > - the behavior depends on what files and subfolders you have in your folder > - changing the sorting order via the standard Obsidian UI button won't affect your folder, unless... -> - ...unless you deactivate the custom sorting via clicking the ribbon button to make it (![Inactive](./docs/icons/icon-inactive.png)) +> - ...unless you deactivate the custom sorting via clicking the ribbon button to make it (![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png)) > - for clarity: the underlying file of the note `sortspec` is obviously `sortspec.md` > - in case of troubles refer to the [TL;DR section of advanced README.md](./blob/master/docs/advanced-README.md#tldr-usage) > - feel free to experiment! The plugin works in a non-destructive fashion, and it doesn't modify the content of your vault. @@ -145,17 +145,17 @@ Click the ribbon icon to toggle the plugin between enabled and suspended states. States of the ribbon icon on large-screen devices (desktops, laptops and tablets like iPad): -- ![Inactive](./docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. -- ![Active](./docs/icons/icon-active.png) Plugin active, custom sorting applied. -- ![Error](./docs/icons/icon-error.png) Syntax error in custom sorting configuration. -- ![General Error](./docs/icons/icon-general-error.png) Plugin suspended. General error. -- ![Sorting not applied](./docs/icons/icon-not-applied.png) Plugin enabled, but the custom sorting was not applied. -- ![Static icon](./docs/icons/icon-mobile-initial.png) (Only on large-screen mobile devices like iPad). +- ![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. +- ![Active](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-active.png) Plugin active, custom sorting applied. +- ![Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-error.png) Syntax error in custom sorting configuration. +- ![General Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-general-error.png) Plugin suspended. General error. +- ![Sorting not applied](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-not-applied.png) Plugin enabled, but the custom sorting was not applied. +- ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) (Only on large-screen mobile devices like iPad). Plugin enabled. but the custom sorting was not applied. On small-screen mobile devices (phones) the icon is static: -- ![Static icon](./docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change +- ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](./blob/master/docs/advanced-README.md#ribbon-icon) @@ -164,7 +164,7 @@ For more details on the icon states refer to [Ribbon icon section of the advance - you might need to activate the custom sorting on your mobile separately, even if on a shared vault the custom sorting was activated on desktop - the Obsidian command palette being easily available (swipe down gesture on small-screen mobiles) allows for quick steering of the plugin via commands: sort-on and sort-off. This could be easier than navigating to and expanding the ribbon -- the ribbon icon is static (![Static icon](./docs/icons/icon-mobile-initial.png)) and doesn't reflect the state of custom sorting. +- the ribbon icon is static (![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png)) and doesn't reflect the state of custom sorting. You can enable the _plugin state changes_ notifications in settings, for the mobile devices only ## Installing the plugin @@ -186,7 +186,7 @@ in [obsidian-bartender](https://github.com/nothingislost/obsidian-bartender) Do you want to have a nice-looking horizontal separators in File Explorer like this? -![separators](./docs/img/separators-by-replete.png) +![separators](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/img/separators-by-replete.png) If so, head on to [Instruction and more context](https://github.com/SebastianMC/obsidian-custom-sort/discussions/57#discussioncomment-4983763) by [@replete](https://github.com/replete)\ From ea6ae911c2eec00bbf78767f1c960aec625be2bb Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:44:42 +0200 Subject: [PATCH 05/10] Fix of broken internal links in advanced-README.md --- docs/advanced-README.md | 56 ++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/advanced-README.md b/docs/advanced-README.md index 3e6d472..1bad0b3 100644 --- a/docs/advanced-README.md +++ b/docs/advanced-README.md @@ -52,11 +52,11 @@ Take full control of the order of your notes and folders: ## TL;DR Usage -For full version of the manual go to [manual](./docs/manual.md) and [syntax-reference](./docs/syntax-reference.md) +For full version of the manual go to [manual](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/manual.md) and [syntax-reference](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/syntax-reference.md) > **Quickstart** > -> 1. Download the **RAW CONTENT** of [sortspec.md](./docs/examples/quickstart/sortspec.md?plain=1) file and put it in any folder of your vault, +> 1. Download the **RAW CONTENT** of [sortspec.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/examples/quickstart/sortspec.md?plain=1) file and put it in any folder of your vault, can be the root folder. Ensure the exact file name is `sortspec.md`. That file contains a basic custom sorting specification under the `sorting-spec:` name in the YAML frontmatter. > > IMPORTANT: follow the above link to 'sortspec.md' and download (or copy & paste) the __RAW__ content of that file, not the HTML displayed by github. > > Afterwards double check that the content of `sortspec.md` file is not an HTML and: @@ -65,19 +65,19 @@ can be the root folder. Ensure the exact file name is `sortspec.md`. That file c > > - indentation is correct (consult images below). In YAML the indentation matters. > > > > In other words, ensure, that the final `sortspec.md` file in your vault (which is the `sortspec` Obsidian note) looks exactly like below: -> > ![sortspec.md](./docs/img/sortspec-md-bright.jpg) +> > ![sortspec.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/img/sortspec-md-bright.jpg) > > > > or if you are a fan of dark mode (line numbers shown for clarity only, they aren't part of the file content): > > -> > ![sortspec.md](./docs/img/sortspec-md-dark.jpg) +> > ![sortspec.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/img/sortspec-md-dark.jpg) > 2. Enable the plugin in obsidian. > -> 3. Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png) or ![Mobile](./docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting +> 3. Click the ribbon button (![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png) or ![Mobile](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting specification from `sortspec` note (the `sortspec.md` file which you downloaded a second ago). > - The observable effect should be reordering of items in root vault folder to reverse alphabetical with folders and files treated equally. And on computers and tablets be the change of appearance of the ribbon button to -![Active](./docs/icons/icon-active.png) (on desktop and tablet only) and -> - The notification balloon should confirm success: ![Success](./docs/icons/parsing-succeeded.png) +![Active](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-active.png) (on desktop and tablet only) and +> - The notification balloon should confirm success: ![Success](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/parsing-succeeded.png) > 4. Click the ribbon button again to suspend the plugin. The ribbon button should toggle its appearance again and the order of files and folders in the root folder of your vault should get back to the order selected in Obsidian UI @@ -87,8 +87,8 @@ change. This will suspend and re-enable the custom sorting, plus parse and apply > - If you don't have any subfolder in the root folder, create one to observe the plugin at work. > -> NOTE: the appearances of ribbon button also includes ![Not applied](./docs/icons/icon-not-applied.png) -and ![Error](./docs/icons/icon-error.png). For the meaning of them please refer to [ribbon icon](#ribbon_icon) section below +> NOTE: the appearances of ribbon button also includes ![Not applied](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-not-applied.png) +and ![Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-error.png). For the meaning of them please refer to [ribbon icon](#ribbon_icon) section below Below go examples of (some of) the key features, ready to copy & paste to your vault. @@ -123,11 +123,11 @@ sorting-spec: | < a-z --- ``` -(View or download the raw content of [sortspec.md](./docs/examples/1/sortspec.md?plain=1) file of this example) +(View or download the raw content of [sortspec.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/examples/1/sortspec.md?plain=1) file of this example) which can result in: -![Simplest example](./docs/svg/simplest-example.svg) +![Simplest example](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/simplest-example.svg) ### Simple case 2: impose manual order of some items in root folder @@ -149,7 +149,7 @@ sorting-spec: | produces: -![Simplest example](./docs/svg/simplest-example-2.svg) +![Simplest example](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/simplest-example-2.svg) ### Example 3: In root folder, let files go first and folders get pushed to the bottom @@ -177,7 +177,7 @@ sorting-spec: | will order items as: -![Files go first example](./docs/svg/files-go-first.svg) +![Files go first example](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/files-go-first.svg) ### Example 4: In root folder, pin a focus note, then Inbox folder, and push archive to the bottom @@ -205,7 +205,7 @@ sorting-spec: | and the result will be: -![Result of the example](./docs/svg/pin-focus-note.svg) +![Result of the example](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/pin-focus-note.svg) > Remarks for the `target-folder:` > @@ -233,11 +233,11 @@ sorting-spec: | --- ``` -(View or download the raw content of [sortspec.md](./docs/examples/5/sortspec.md?plain=1) file of this example) +(View or download the raw content of [sortspec.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/examples/5/sortspec.md?plain=1) file of this example) which will have the effect of: -![Result of the example](./docs/svg/p_a_r_a.svg) +![Result of the example](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/p_a_r_a.svg) ### Example 6: P.A.R.A. example with smart syntax @@ -257,7 +257,7 @@ sorting-spec: | It will give exactly the same order as in previous example: -![Result of the example](./docs/svg/p_a_r_a.svg) +![Result of the example](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/p_a_r_a.svg) ``` REMARK: the wildcard expression '...' can be used only once per line @@ -303,7 +303,7 @@ sorting-spec: | will have the effect of: -![Result of the example](./docs/svg/multi-folder.svg) +![Result of the example](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/multi-folder.svg) ### Example 9: Sort by numerical suffix @@ -330,7 +330,7 @@ alphabetical ascending. The effect is: -![Order by numerical suffix](./docs/svg/by-suffix.svg) +![Order by numerical suffix](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/by-suffix.svg) ### Example 10: Sample book structure with Roman numbered chapters @@ -352,7 +352,7 @@ sorting-spec: | it gives: -![Book - Roman chapters](./docs/svg/roman-chapters.svg) +![Book - Roman chapters](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/roman-chapters.svg) ### Example 11: Sample book structure with compound Roman number suffixes @@ -372,7 +372,7 @@ sorting-spec: | the result is: -![Book - Roman compound suffixes](./docs/svg/roman-suffix.svg) +![Book - Roman compound suffixes](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/svg/roman-suffix.svg) ### Example 12: Apply same sorting to all folders in the vault @@ -572,31 +572,31 @@ Click the ribbon icon to toggle the plugin between enabled and suspended states. States of the ribbon icon on large-screen devices (desktops, laptops and tablets like iPad): -- ![Inactive](./docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. +- ![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. - Click to enable and apply custom sorting. - Note: parsing of the custom sorting specification happens after clicking the icon. If the specification contains errors, they will show up in the notice baloon and also in developer console. -- ![Active](./docs/icons/icon-active.png) Plugin active, custom sorting applied. +- ![Active](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-active.png) Plugin active, custom sorting applied. - Click to suspend and return to the standard Obsidian sorting in File Explorer. -- ![Error](./docs/icons/icon-error.png) Syntax error in custom sorting configuration. +- ![Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-error.png) Syntax error in custom sorting configuration. - Fix the problem in specification and click the ribbon icon to re-enable custom sorting. - If syntax error is not fixed, the notice baloon with show error details. Syntax error details are also visible in the developer console -- ![General Error](./docs/icons/icon-general-error.png) Plugin suspended. General error. +- ![General Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-general-error.png) Plugin suspended. General error. - File Explorer not available or other type of general error - File Explorer is a core Obsidian plugin (named __Files__) and thus can be disabled in Obsidian settings - Some community plugins (like __MAKE.md__) also disable the File Explorer by default - See obsidinan developer console for detailed error message - To fix the problem, enable the File Explorer (in Obsidian or in the community plugin responsible for hididing it) -- ![Sorting not applied](./docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. +- ![Sorting not applied](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. - This can happen when reinstalling the plugin and in similar cases - Click the ribbon icon twice to re-enable the custom sorting. -- ![Static icon](./docs/icons/icon-mobile-initial.png) Only on large-screen mobile devices like iPad. +- ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) Only on large-screen mobile devices like iPad. - Plugin enabled. but the custom sorting was not applied. On small-screen mobile devices (phones) the icon is static: -- ![Static icon](./docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change +- ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change - Click to enable and apply custom sorting or to disable custom sorting - To get notified about custom sort plugin state, enable the mobile-specific notifications in plugin settings From 5b499536c088f1d63ef58a51c2bbae77d48bc476 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:53:02 +0200 Subject: [PATCH 06/10] Fix of broken internal links in README.md -> relative links made absolute --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 72bb5a3..b1422e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ > This is a simple version of README which highlights the **basic scenario and most commonly used feature** > -> The [long and much more detailed advanced-README.md is here](./blob/master/docs/advanced-README.md) +> The [long and much more detailed advanced-README.md is here](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.m) --- ## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) @@ -60,18 +60,18 @@ An illustrative image which shows the reverse alphabetical order applied to the > - changing the sorting order via the standard Obsidian UI button won't affect your folder, unless... > - ...unless you deactivate the custom sorting via clicking the ribbon button to make it (![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png)) > - for clarity: the underlying file of the note `sortspec` is obviously `sortspec.md` -> - in case of troubles refer to the [TL;DR section of advanced README.md](./blob/master/docs/advanced-README.md#tldr-usage) +> - in case of troubles refer to the [TL;DR section of advanced README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md#tldr-usage) > - feel free to experiment! The plugin works in a non-destructive fashion, and it doesn't modify the content of your vault. > It only changes the order in which the files and folders are displayed in File Explorer > - indentation matters in YAML -> the two leading spaces in ` order-desc: a-z` are intentional and required -> - this common example only touches the surface of the rich capabilities of this custom sorting plugin. For more details go to [advanced version of README.md](./blob/master/docs/advanced-README.md) +> - this common example only touches the surface of the rich capabilities of this custom sorting plugin. For more details go to [advanced version of README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md) --- ## Basic scenario 2: explicitly list folders and files in the order which you want This comes from the suggestion by [TheOneLight](https://github.com/TheOneLight) in [this discussion](https://github.com/SebastianMC/obsidian-custom-sort/discussions/95#discussioncomment-7048584) -Take the instructions from the **[Basic scenario 1](https://github.com/SebastianMC/obsidian-custom-sort#basic-scenario-set-the-custom-sorting-order-for-a-specific-folder)** above and replace the YAML content with: +Take the instructions from the **[Basic scenario 1](#basic-scenario-set-the-custom-sorting-order-for-a-specific-folder)** above and replace the YAML content with: ```yaml --- @@ -137,7 +137,7 @@ The list of basic automatic sorting orders includes: ## Manual sorting The **manual ordering of notes and folders** is also done via the sorting configuration. -Refer to the [TL;DR section of advanced README.md](./blob/master/docs/advanced-README.md#tldr-usage) for examples and instructions +Refer to the [TL;DR section of advanced README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md#tldr-usage) for examples and instructions ## Ribbon icon @@ -157,7 +157,7 @@ On small-screen mobile devices (phones) the icon is static: - ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change -For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](./blob/master/docs/advanced-README.md#ribbon-icon) +For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md#ribbon-icon) ## Small screen mobile devices remarks @@ -175,7 +175,7 @@ The plugin could and should be installed from the official Obsidian Community Pl or directly in the Obsidian app itself. Search the plugin by its name 'CUSTOM FILE EXPLORER SORTING' -> For other installation methods refer to [Installing the plugin section of advanced-README.md](./blob/master/docs/advanced-README.md#installing-the-plugin) +> For other installation methods refer to [Installing the plugin section of advanced-README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md#installing-the-plugin) ## Credits From 50ec4e30a8e4339f1da8ea43a26641046762fc67 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:53:55 +0200 Subject: [PATCH 07/10] Fix of broken internal links in README.md -> relative links made absolute --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1422e7..2a53a66 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ > This is a simple version of README which highlights the **basic scenario and most commonly used feature** > -> The [long and much more detailed advanced-README.md is here](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.m) +> The [long and much more detailed advanced-README.md is here](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md) --- ## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) From 0e20e586eaddfd327f7f67b88f37db8cb9071db3 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:05:38 +0200 Subject: [PATCH 08/10] Fix of broken internal links in README.md -> relative links made absolute --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2a53a66..c70b2c4 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ sorting-spec: | --- ``` -Click the ribbon button (![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png) or ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting specification and apply it. -The sorting should be applied to the folder. On desktops and tablets the ribbon icon should turn (![Active](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-active.png)) +Click the ribbon button (![Inactive](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-inactive.png) or ![Static icon](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting specification and apply it. +The sorting should be applied to the folder. On desktops and tablets the ribbon icon should turn (![Active](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-active.png)) !!! **Done!** !!! @@ -58,7 +58,7 @@ An illustrative image which shows the reverse alphabetical order applied to the > - you will notice that the folders and files are treated equally and thus intermixed > - the behavior depends on what files and subfolders you have in your folder > - changing the sorting order via the standard Obsidian UI button won't affect your folder, unless... -> - ...unless you deactivate the custom sorting via clicking the ribbon button to make it (![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png)) +> - ...unless you deactivate the custom sorting via clicking the ribbon button to make it ![Inactive](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-inactive.png) > - for clarity: the underlying file of the note `sortspec` is obviously `sortspec.md` > - in case of troubles refer to the [TL;DR section of advanced README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md#tldr-usage) > - feel free to experiment! The plugin works in a non-destructive fashion, and it doesn't modify the content of your vault. @@ -145,17 +145,17 @@ Click the ribbon icon to toggle the plugin between enabled and suspended states. States of the ribbon icon on large-screen devices (desktops, laptops and tablets like iPad): -- ![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. -- ![Active](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-active.png) Plugin active, custom sorting applied. -- ![Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-error.png) Syntax error in custom sorting configuration. -- ![General Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-general-error.png) Plugin suspended. General error. -- ![Sorting not applied](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-not-applied.png) Plugin enabled, but the custom sorting was not applied. -- ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) (Only on large-screen mobile devices like iPad). +- ![Inactive](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. +- ![Active](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-active.png) Plugin active, custom sorting applied. +- ![Error](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-error.png) Syntax error in custom sorting configuration. +- ![General Error](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-general-error.png) Plugin suspended. General error. +- ![Sorting not applied](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-not-applied.png) Plugin enabled, but the custom sorting was not applied. +- ![Static icon](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-mobile-initial.png) (Only on large-screen mobile devices like iPad). Plugin enabled. but the custom sorting was not applied. On small-screen mobile devices (phones) the icon is static: -- ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change +- ![Static icon](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md#ribbon-icon) @@ -164,7 +164,7 @@ For more details on the icon states refer to [Ribbon icon section of the advance - you might need to activate the custom sorting on your mobile separately, even if on a shared vault the custom sorting was activated on desktop - the Obsidian command palette being easily available (swipe down gesture on small-screen mobiles) allows for quick steering of the plugin via commands: sort-on and sort-off. This could be easier than navigating to and expanding the ribbon -- the ribbon icon is static (![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png)) and doesn't reflect the state of custom sorting. +- the ribbon icon is static (![Static icon](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-mobile-initial.png)) and doesn't reflect the state of custom sorting. You can enable the _plugin state changes_ notifications in settings, for the mobile devices only ## Installing the plugin @@ -186,7 +186,7 @@ in [obsidian-bartender](https://github.com/nothingislost/obsidian-bartender) Do you want to have a nice-looking horizontal separators in File Explorer like this? -![separators](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/img/separators-by-replete.png) +![separators](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/img/separators-by-replete.png) If so, head on to [Instruction and more context](https://github.com/SebastianMC/obsidian-custom-sort/discussions/57#discussioncomment-4983763) by [@replete](https://github.com/replete)\ From 36f124f44f3a7c8df31925a358858699d750bd93 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:10:41 +0200 Subject: [PATCH 09/10] Fix of broken internal links in advanced-README.md -> relative links made absolute --- docs/advanced-README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/advanced-README.md b/docs/advanced-README.md index 1bad0b3..ec48c88 100644 --- a/docs/advanced-README.md +++ b/docs/advanced-README.md @@ -1,6 +1,6 @@ # Advanced version of README.md for advanced users -The [simplified README.md is here](./README.md) +The [simplified README.md is here](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/README.md) ## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) @@ -65,19 +65,19 @@ can be the root folder. Ensure the exact file name is `sortspec.md`. That file c > > - indentation is correct (consult images below). In YAML the indentation matters. > > > > In other words, ensure, that the final `sortspec.md` file in your vault (which is the `sortspec` Obsidian note) looks exactly like below: -> > ![sortspec.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/img/sortspec-md-bright.jpg) +> > ![sortspec.md](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs//img/sortspec-md-bright.jpg) > > > > or if you are a fan of dark mode (line numbers shown for clarity only, they aren't part of the file content): > > -> > ![sortspec.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/img/sortspec-md-dark.jpg) +> > ![sortspec.md](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs//img/sortspec-md-dark.jpg) > 2. Enable the plugin in obsidian. > -> 3. Click the ribbon button (![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png) or ![Mobile](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting +> 3. Click the ribbon button (![Inactive](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-inactive.png) or ![Mobile](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting specification from `sortspec` note (the `sortspec.md` file which you downloaded a second ago). > - The observable effect should be reordering of items in root vault folder to reverse alphabetical with folders and files treated equally. And on computers and tablets be the change of appearance of the ribbon button to -![Active](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-active.png) (on desktop and tablet only) and -> - The notification balloon should confirm success: ![Success](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/parsing-succeeded.png) +![Active](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-active.png) (on desktop and tablet only) and +> - The notification balloon should confirm success: ![Success](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/parsing-succeeded.png) > 4. Click the ribbon button again to suspend the plugin. The ribbon button should toggle its appearance again and the order of files and folders in the root folder of your vault should get back to the order selected in Obsidian UI @@ -87,8 +87,8 @@ change. This will suspend and re-enable the custom sorting, plus parse and apply > - If you don't have any subfolder in the root folder, create one to observe the plugin at work. > -> NOTE: the appearances of ribbon button also includes ![Not applied](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-not-applied.png) -and ![Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-error.png). For the meaning of them please refer to [ribbon icon](#ribbon_icon) section below +> NOTE: the appearances of ribbon button also includes ![Not applied](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-not-applied.png) +and ![Error](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-error.png). For the meaning of them please refer to [ribbon icon](#ribbon_icon) section below Below go examples of (some of) the key features, ready to copy & paste to your vault. @@ -572,31 +572,31 @@ Click the ribbon icon to toggle the plugin between enabled and suspended states. States of the ribbon icon on large-screen devices (desktops, laptops and tablets like iPad): -- ![Inactive](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. +- ![Inactive](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. - Click to enable and apply custom sorting. - Note: parsing of the custom sorting specification happens after clicking the icon. If the specification contains errors, they will show up in the notice baloon and also in developer console. -- ![Active](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-active.png) Plugin active, custom sorting applied. +- ![Active](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-active.png) Plugin active, custom sorting applied. - Click to suspend and return to the standard Obsidian sorting in File Explorer. -- ![Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-error.png) Syntax error in custom sorting configuration. +- ![Error](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-error.png) Syntax error in custom sorting configuration. - Fix the problem in specification and click the ribbon icon to re-enable custom sorting. - If syntax error is not fixed, the notice baloon with show error details. Syntax error details are also visible in the developer console -- ![General Error](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-general-error.png) Plugin suspended. General error. +- ![General Error](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-general-error.png) Plugin suspended. General error. - File Explorer not available or other type of general error - File Explorer is a core Obsidian plugin (named __Files__) and thus can be disabled in Obsidian settings - Some community plugins (like __MAKE.md__) also disable the File Explorer by default - See obsidinan developer console for detailed error message - To fix the problem, enable the File Explorer (in Obsidian or in the community plugin responsible for hididing it) -- ![Sorting not applied](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. +- ![Sorting not applied](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. - This can happen when reinstalling the plugin and in similar cases - Click the ribbon icon twice to re-enable the custom sorting. -- ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) Only on large-screen mobile devices like iPad. +- ![Static icon](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-mobile-initial.png) Only on large-screen mobile devices like iPad. - Plugin enabled. but the custom sorting was not applied. On small-screen mobile devices (phones) the icon is static: -- ![Static icon](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change +- ![Static icon](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change - Click to enable and apply custom sorting or to disable custom sorting - To get notified about custom sort plugin state, enable the mobile-specific notifications in plugin settings From 167f6db952d366e501a0f7e20bdd84ae144c2d72 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:16:12 +0200 Subject: [PATCH 10/10] Fix of broken internal links in *.md -> relative links made absolute --- docs/manual.md | 6 +++--- docs/syntax-reference.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/manual.md b/docs/manual.md index a712be5..642868a 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1,6 +1,6 @@ > Document is partial, creation in progress -> Please refer to [README.md](../README.md) and [advanced-README.md](../advanced-README.md) for more usage examples -> Check also [syntax-reference.md](./syntax-reference.md) +> Please refer to [README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/README.md) and [advanced-README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/advanced-README.md) for more usage examples +> Check also [syntax-reference.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/syntax-reference.md) --- Some sections added ad-hoc, to be integrated later @@ -11,7 +11,7 @@ Some sections added ad-hoc, to be integrated later Do you want to have a nice-looking horizontal separators in File Explorer like this? -![separators](./img/separators-by-replete.png) +![separators](https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/master/docs/img/separators-by-replete.png) If so, head on to [Instruction and more context](https://github.com/SebastianMC/obsidian-custom-sort/discussions/57#discussioncomment-4983763) by [@replete](https://github.com/replete) diff --git a/docs/syntax-reference.md b/docs/syntax-reference.md index e5c0771..fe8e12f 100644 --- a/docs/syntax-reference.md +++ b/docs/syntax-reference.md @@ -1,6 +1,6 @@ > Document is partial, creation in progress -> Please refer to [README.md](../README.md) for usage examples -> Check [manual.md](./manual.md), maybe that file has already some content? +> Please refer to [README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/README.md) for usage examples +> Check [manual.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/doc/manual.md), maybe that file has already some content? # Table of contents