Ticket #1: added support for imposed sorting rules inheritance by subfolders

- new syntax: target-folder: Reviews/*  <-- applies to folder, subfolders and all descendant folders
- new syntax: target-folder: Reviews/...  <-- applies to folder and subfolders
- special case: target-folder: /*  tells the plugin to apply the specified sorting to entire vault
- enhanced syntax for '.' - can be used to specify relative path now (e.g. target-folder: ./daily)
- added new sorting attribute (sorting: standard) to actually give back the sorting into the hands of standard Obsidian mechanisms
- fixed a minor bug: some of error messages were displayed only in console, not in the ballons
- unit tests for all new and changed functionality
- pending README.md update
This commit is contained in:
SebastianMC 2022-08-30 18:53:58 +02:00
parent b1a43dff3a
commit e972bce007
10 changed files with 679 additions and 64 deletions

View File

@ -1,7 +1,7 @@
{ {
"id": "custom-sort", "id": "custom-sort",
"name": "Custom File Explorer sorting", "name": "Custom File Explorer sorting",
"version": "0.5.189", "version": "0.6.0",
"minAppVersion": "0.12.0", "minAppVersion": "0.12.0",
"description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer", "description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer",
"author": "SebastianMC <SebastianMC.github@gmail.com>", "author": "SebastianMC <SebastianMC.github@gmail.com>",

View File

@ -1,6 +1,6 @@
{ {
"name": "obsidian-custom-sort", "name": "obsidian-custom-sort",
"version": "0.5.189", "version": "0.6.0",
"description": "Custom Sort plugin for Obsidian (https://obsidian.md)", "description": "Custom Sort plugin for Obsidian (https://obsidian.md)",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@ -1,5 +1,3 @@
export const SortSpecFileName: string = 'sortspec.md';
export enum CustomSortGroupType { export enum CustomSortGroupType {
Outsiders, // Not belonging to any of other groups Outsiders, // Not belonging to any of other groups
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
@ -15,7 +13,8 @@ export enum CustomSortOrder {
byModifiedTime, byModifiedTime,
byModifiedTimeReverse, byModifiedTimeReverse,
byCreatedTime, byCreatedTime,
byCreatedTimeReverse byCreatedTimeReverse,
standardObsidian// Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
} }
export interface RecognizedOrderValue { export interface RecognizedOrderValue {
@ -52,9 +51,3 @@ export interface CustomSortSpec {
outsidersFoldersGroupIdx?: number outsidersFoldersGroupIdx?: number
itemsToHide?: Set<string> itemsToHide?: Set<string>
} }
export interface FolderPathToSortSpecMap {
[key: string]: CustomSortSpec
}
export type SortSpecsCollection = FolderPathToSortSpecMap

View File

@ -31,7 +31,10 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime, [CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime,
[CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime, [CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime,
[CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime, [CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime,
[CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime [CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime,
// This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
}; };
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) { function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
@ -53,7 +56,7 @@ const isFolder = (entry: TFile | TFolder) => {
} }
export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec): FolderItemForSorting { export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec): FolderItemForSorting {
let groupIdx: number = 0 let groupIdx: number
let determined: boolean = false let determined: boolean = false
let matchedGroup: string let matchedGroup: string
const aFolder: boolean = isFolder(entry) const aFolder: boolean = isFolder(entry)
@ -168,7 +171,6 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) { export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
let fileExplorer = this.fileExplorer let fileExplorer = this.fileExplorer
const thisFolderPath: string = this.file.path;
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ? const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
this.file.children.filter((entry: TFile | TFolder) => { this.file.children.filter((entry: TFile | TFolder) => {

View File

@ -0,0 +1,120 @@
import {FolderWildcardMatching} from './folder-matching-rules'
type SortingSpec = string
const createMockMatcherRichVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
let p: string
p = '/...'; matcher.addWildcardDefinition(p, `00 ${p}`)
p = '/*'; matcher.addWildcardDefinition(p, `0 ${p}`)
p = 'Reviews/...'; matcher.addWildcardDefinition(p, `1 ${p}`)
p = '/Reviews/*'; matcher.addWildcardDefinition(p, `2 ${p}`)
p = '/Reviews/daily/a/.../'; matcher.addWildcardDefinition(p, `3 ${p}`)
p = 'Reviews/daily/a/*'; matcher.addWildcardDefinition(p, `4 ${p}`)
return matcher
}
const createMockMatcherSimplestVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*')
return matcher
}
const createMockMatcherRootOnlyVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
matcher.addWildcardDefinition('/...', '/...')
return matcher
}
const createMockMatcherRootOnlyDeepVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
matcher.addWildcardDefinition('/*', '/*')
return matcher
}
const createMockMatcherSimpleVersion = (): FolderWildcardMatching<SortingSpec> => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*')
matcher.addWildcardDefinition('/Reviews/daily/...', '/Reviews/daily/...')
return matcher
}
describe('folderMatch', () => {
it.each([
['/', '00 /...'],
['Archive/', '00 /...'],
['Archive', '00 /...'],
['/Archive/2019', '0 /*'],
['Archive/2019/', '0 /*'],
['Archive/2019/Jan', '0 /*'],
['/Reviews', '1 Reviews/...'],
['Reviews/weekly', '1 Reviews/...'],
['Reviews/weekly/w50/', '2 /Reviews/*'],
['/Reviews/daily', '2 /Reviews/*'],
['Reviews/daily/Mon', '2 /Reviews/*'],
['/Reviews/daily/a/', '3 /Reviews/daily/a/.../'],
['Reviews/daily/a/Mon', '3 /Reviews/daily/a/.../'],
['/Reviews/daily/a/Mon/Late', '4 Reviews/daily/a/*'],
['Reviews/daily/a/Tue/Early/9am', '4 Reviews/daily/a/*']
])('%s should match %s', (path: string, rule: string) => {
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRichVersion()
const match: SortingSpec = matcher.folderMatch(path)
const matchFromCache: SortingSpec = matcher.folderMatch(path)
expect(match).toBe(rule)
expect(matchFromCache).toBe(rule)
})
it('should correctly handle no-root definitions', () => {
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherSimplestVersion()
const match1: SortingSpec = matcher.folderMatch('/')
const match2: SortingSpec = matcher.folderMatch('/Reviews')
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
const match4: SortingSpec = matcher.folderMatch('/Reviews/daily/Mon')
const match5: SortingSpec = matcher.folderMatch('/Reviews/daily/Mon')
expect(match1).toBeNull()
expect(match2).toBeNull()
expect(match3).toBe('/Reviews/daily/*')
expect(match4).toBe('/Reviews/daily/*')
expect(match5).toBe('/Reviews/daily/*')
})
it('should correctly handle root-only definition', () => {
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRootOnlyVersion()
const match1: SortingSpec = matcher.folderMatch('/')
const match2: SortingSpec = matcher.folderMatch('/Reviews')
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
expect(match1).toBe('/...')
expect(match2).toBe('/...')
expect(match3).toBeNull()
})
it('should correctly handle root-only deep definition', () => {
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRootOnlyDeepVersion()
const match1: SortingSpec = matcher.folderMatch('/')
const match2: SortingSpec = matcher.folderMatch('/Reviews')
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
expect(match1).toBe('/*')
expect(match2).toBe('/*')
expect(match3).toBe('/*')
})
it('should correctly handle match all and match children definitions for same path', () => {
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherSimpleVersion()
const match1: SortingSpec = matcher.folderMatch('/')
const match2: SortingSpec = matcher.folderMatch('/Reviews/daily/')
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/1')
expect(match1).toBeNull()
expect(match2).toBe('/Reviews/daily/...')
expect(match3).toBe('/Reviews/daily/...')
})
it('should detect duplicate match children definitions for same path', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
matcher.addWildcardDefinition('Archive/2020/...', 'First occurrence')
const result = matcher.addWildcardDefinition('/Archive/2020/.../', 'Duplicate')
expect(result).toEqual({errorMsg: "Duplicate wildcard '...' specification for /Archive/2020/.../"})
})
it('should detect duplicate match all definitions for same path', () => {
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
matcher.addWildcardDefinition('/Archive/2019/*', 'First occurrence')
const result = matcher.addWildcardDefinition('Archive/2019/*', 'Duplicate')
expect(result).toEqual({errorMsg: "Duplicate wildcard '*' specification for Archive/2019/*"})
})
})

View File

@ -0,0 +1,116 @@
export interface FolderPattern {
path: string
deep: boolean
nestingLevel: number
}
export type DeterminedSortingSpec<SortingSpec> = {
spec?: SortingSpec
}
export interface FolderMatchingTreeNode<SortingSpec> {
path?: string
name?: string
matchChildren?: SortingSpec
matchAll?: SortingSpec
subtree: { [key: string]: FolderMatchingTreeNode<SortingSpec> }
}
const SLASH: string = '/'
export const MATCH_CHILDREN_PATH_TOKEN: string = '...'
export const MATCH_ALL_PATH_TOKEN: string = '*'
export const MATCH_CHILDREN_1_SUFFIX: string = `/${MATCH_CHILDREN_PATH_TOKEN}`
export const MATCH_CHILDREN_2_SUFFIX: string = `/${MATCH_CHILDREN_PATH_TOKEN}/`
export const MATCH_ALL_SUFFIX: string = `/${MATCH_ALL_PATH_TOKEN}`
export const splitPath = (path: string): Array<string> => {
return path.split(SLASH).filter((name) => !!name)
}
export interface AddingWildcardFailure {
errorMsg: string
}
export class FolderWildcardMatching<SortingSpec> {
tree: FolderMatchingTreeNode<SortingSpec> = {
subtree: {}
}
// cache
determinedWildcardRules: { [key: string]: DeterminedSortingSpec<SortingSpec> } = {}
addWildcardDefinition = (wilcardDefinition: string, rule: SortingSpec): AddingWildcardFailure | null => {
const pathComponents: Array<string> = splitPath(wilcardDefinition)
const lastComponent: string = pathComponents.pop()
if (lastComponent !== MATCH_ALL_PATH_TOKEN && lastComponent !== MATCH_CHILDREN_PATH_TOKEN) {
return null
}
let leafNode: FolderMatchingTreeNode<SortingSpec> = this.tree
pathComponents.forEach((pathComponent) => {
let subtree: FolderMatchingTreeNode<SortingSpec> = leafNode.subtree[pathComponent]
if (subtree) {
leafNode = subtree
} else {
const newSubtree: FolderMatchingTreeNode<SortingSpec> = {
name: pathComponent,
subtree: {}
}
leafNode.subtree[pathComponent] = newSubtree
leafNode = newSubtree
}
})
if (lastComponent === MATCH_CHILDREN_PATH_TOKEN) {
if (leafNode.matchChildren) {
return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`}
} else {
leafNode.matchChildren = rule
}
} else { // Implicitly: MATCH_ALL_PATH_TOKEN
if (leafNode.matchAll) {
return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`}
} else {
leafNode.matchAll = rule
}
}
}
folderMatch = (folderPath: string): SortingSpec | null => {
const spec: DeterminedSortingSpec<SortingSpec> = this.determinedWildcardRules[folderPath]
if (spec) {
return spec.spec ?? null
} else {
let rule: SortingSpec = this.tree.matchChildren
let inheritedRule: SortingSpec = this.tree.matchAll
const pathComponents: Array<string> = splitPath(folderPath)
let parentNode: FolderMatchingTreeNode<SortingSpec> = this.tree
let lastIdx: number = pathComponents.length - 1
for(let i=0; i<=lastIdx; i++) {
const name: string = pathComponents[i]
let matchedPath: FolderMatchingTreeNode<SortingSpec> = parentNode.subtree[name]
if (matchedPath) {
parentNode = matchedPath
rule = matchedPath?.matchChildren ?? null
inheritedRule = matchedPath.matchAll ?? inheritedRule
} else {
if (i < lastIdx) {
rule = inheritedRule
}
break
}
}
rule = rule ?? inheritedRule
if (rule) {
this.determinedWildcardRules[folderPath] = {spec: rule}
return rule
} else {
this.determinedWildcardRules[folderPath] = {}
return null
}
}
}
}

View File

@ -1,5 +1,6 @@
import { import {
CompoundDashNumberNormalizerFn, CompoundDashRomanNumberNormalizerFn, CompoundDashNumberNormalizerFn,
CompoundDashRomanNumberNormalizerFn,
CompoundDotNumberNormalizerFn, CompoundDotNumberNormalizerFn,
convertPlainStringWithNumericSortingSymbolToRegex, convertPlainStringWithNumericSortingSymbolToRegex,
detectNumericSortingSymbols, detectNumericSortingSymbols,
@ -12,6 +13,7 @@ import {
SortingSpecProcessor SortingSpecProcessor
} from "./sorting-spec-processor" } from "./sorting-spec-processor"
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types"; import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
import {FolderMatchingTreeNode} from "./folder-matching-rules";
const txtInputExampleA: string = ` const txtInputExampleA: string = `
order-asc: a-z order-asc: a-z
@ -348,17 +350,17 @@ describe('SortingSpecProcessor', () => {
it('should generate correct SortSpecs (complex example A)', () => { it('should generate correct SortSpecs (complex example A)', () => {
const inputTxtArr: Array<string> = txtInputExampleA.split('\n') const inputTxtArr: Array<string> = txtInputExampleA.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result).toEqual(expectedSortSpecsExampleA) expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleA)
}) })
it('should generate correct SortSpecs (complex example A verbose)', () => { it('should generate correct SortSpecs (complex example A verbose)', () => {
const inputTxtArr: Array<string> = txtInputExampleAVerbose.split('\n') const inputTxtArr: Array<string> = txtInputExampleAVerbose.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result).toEqual(expectedSortSpecsExampleA) expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleA)
}) })
it('should generate correct SortSpecs (example with numerical sorting symbols)', () => { it('should generate correct SortSpecs (example with numerical sorting symbols)', () => {
const inputTxtArr: Array<string> = txtInputExampleNumericSortingSymbols.split('\n') const inputTxtArr: Array<string> = txtInputExampleNumericSortingSymbols.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result).toEqual(expectedSortSpecsExampleNumericSortingSymbols) expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleNumericSortingSymbols)
}) })
}) })
@ -401,7 +403,36 @@ describe('SortingSpecProcessor', () => {
it('should not duplicate spec if former target-folder had some attribute specified', () => { it('should not duplicate spec if former target-folder had some attribute specified', () => {
const inputTxtArr: Array<string> = txtInputNotDuplicatedSortSpec.split('\n') const inputTxtArr: Array<string> = txtInputNotDuplicatedSortSpec.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result).toEqual(expectedSortSpecsNotDuplicatedSortSpec) expect(result?.sortSpecByPath).toEqual(expectedSortSpecsNotDuplicatedSortSpec)
})
})
const txtInputStandardObsidianSortAttr: string = `
target-folder: AAA
sorting: standard
`
const expectedSortSpecForObsidianStandardSorting: { [key: string]: CustomSortSpec } = {
"AAA": {
defaultOrder: CustomSortOrder.standardObsidian,
groups: [{
order: CustomSortOrder.standardObsidian,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['AAA']
}
}
describe('SortingSpecProcessor', () => {
let processor: SortingSpecProcessor;
beforeEach(() => {
processor = new SortingSpecProcessor();
});
it('should recognize the standard Obsidian sorting attribute for a folder', () => {
const inputTxtArr: Array<string> = txtInputStandardObsidianSortAttr.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForObsidianStandardSorting)
}) })
}) })
@ -435,7 +466,7 @@ describe('SortingSpecProcessor bonus experimental feature', () => {
const inputTxtArr: Array<string> = txtInputItemsToHideWithDupsSortSpec.split('\n') const inputTxtArr: Array<string> = txtInputItemsToHideWithDupsSortSpec.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
// REMARK: be careful with examining Set object // REMARK: be careful with examining Set object
expect(result).toEqual(expectedHiddenItemsSortSpec) expect(result?.sortSpecByPath).toEqual(expectedHiddenItemsSortSpec)
}) })
}) })
@ -490,7 +521,7 @@ describe('SortingSpecProcessor - README.md examples', () => {
const inputTxtArr: Array<string> = txtInputItemsReadmeExample1Spec.split('\n') const inputTxtArr: Array<string> = txtInputItemsReadmeExample1Spec.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
// REMARK: be careful with examining Set object // REMARK: be careful with examining Set object
expect(result).toEqual(expectedReadmeExample1SortSpec) expect(result?.sortSpecByPath).toEqual(expectedReadmeExample1SortSpec)
}) })
}) })
@ -513,43 +544,244 @@ const txtInputTargetFolderAsDot: string = `
// Let me introduce a comment here ;-) to ensure it is ignored // Let me introduce a comment here ;-) to ensure it is ignored
target-folder: . target-folder: .
target-folder: CCC target-folder: CCC
target-folder: ./sub
target-folder: ./*
target-folder: ./...
//target-folder: ./.../
// This comment should be ignored as well // This comment should be ignored as well
` `
const expectedSortSpecsTargetFolderAsDot: { [key: string]: CustomSortSpec } = { const expectedSortSpecToBeMultiplied = {
'mock-folder': {
groups: [{ groups: [{
order: CustomSortOrder.alphabetical, order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.Outsiders type: CustomSortGroupType.Outsiders
}], }],
outsidersGroupIdx: 0, outsidersGroupIdx: 0,
targetFoldersPaths: ['mock-folder', 'CCC'] targetFoldersPaths: ['mock-folder', 'CCC', 'mock-folder/sub', "mock-folder/*", "mock-folder/..."]
},
'CCC': {
groups: [{
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['mock-folder', 'CCC']
}
} }
const expectedSortSpecsTargetFolderAsDot: { [key: string]: CustomSortSpec } = {
'mock-folder': expectedSortSpecToBeMultiplied,
'CCC': expectedSortSpecToBeMultiplied,
'mock-folder/sub': expectedSortSpecToBeMultiplied
}
describe('SortingSpecProcessor edge case', () => { describe('SortingSpecProcessor edge case', () => {
let processor: SortingSpecProcessor; let processor: SortingSpecProcessor;
beforeEach(() => { beforeEach(() => {
processor = new SortingSpecProcessor(); processor = new SortingSpecProcessor();
}); });
it('should not recognize empty spec containing only target folder', () => { it('should recognize empty spec containing only target folder', () => {
const inputTxtArr: Array<string> = txtInputEmptySpecOnlyTargetFolder.split('\n') const inputTxtArr: Array<string> = txtInputEmptySpecOnlyTargetFolder.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result).toEqual(expectedSortSpecsOnlyTargetFolder) expect(result?.sortSpecByPath).toEqual(expectedSortSpecsOnlyTargetFolder)
}) })
it('should not recognize and correctly replace dot as the target folder', () => { it('should recognize and correctly replace dot as the target folder', () => {
const inputTxtArr: Array<string> = txtInputTargetFolderAsDot.split('\n') const inputTxtArr: Array<string> = txtInputTargetFolderAsDot.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result).toEqual(expectedSortSpecsTargetFolderAsDot) expect(result?.sortSpecByPath).toEqual(expectedSortSpecsTargetFolderAsDot)
expect(result?.sortSpecByWildcard).not.toBeNull()
})
})
const txtInputTargetFolderMultiSpecA: string = `
target-folder: .
< a-z
target-folder: ./*
> a-z
target-folder: ./.../
< modified
`
const txtInputTargetFolderMultiSpecB: string = `
target-folder: ./*
> a-z
target-folder: ./.../
< modified
target-folder: .
< a-z
`
const expectedSortSpecForMultiSpecAandB: { [key: string]: CustomSortSpec } = {
'mock-folder': {
defaultOrder: CustomSortOrder.alphabetical,
groups: [{
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['mock-folder']
}
}
const expectedWildcardMatchingTreeForMultiSpecAandB: FolderMatchingTreeNode<CustomSortSpec> = {
subtree: {
"mock-folder": {
matchAll: {
"defaultOrder": CustomSortOrder.alphabeticalReverse,
"groups": [{
"order": CustomSortOrder.alphabeticalReverse,
"type": CustomSortGroupType.Outsiders
}],
"outsidersGroupIdx": 0,
"targetFoldersPaths": ["mock-folder/*"]
},
matchChildren: {
"defaultOrder": CustomSortOrder.byModifiedTime,
"groups": [{
"order": CustomSortOrder.byModifiedTime,
"type": CustomSortGroupType.Outsiders
}],
"outsidersGroupIdx": 0,
"targetFoldersPaths": ["mock-folder/.../"]
},
name: "mock-folder",
subtree: {}
}
}
}
const txtInputTargetFolderMultiSpecC: string = `
target-folder: ./*
> a-z
target-folder: ./.../
`
const expectedSortSpecForMultiSpecC: { [key: string]: CustomSortSpec } = {
'mock-folder': {
groups: [{
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['mock-folder/.../']
}
}
const expectedWildcardMatchingTreeForMultiSpecC: FolderMatchingTreeNode<CustomSortSpec> = {
subtree: {
"mock-folder": {
matchAll: {
"defaultOrder": CustomSortOrder.alphabeticalReverse,
"groups": [{
"order": CustomSortOrder.alphabeticalReverse,
"type": CustomSortGroupType.Outsiders
}],
"outsidersGroupIdx": 0,
"targetFoldersPaths": ["mock-folder/*"]
},
matchChildren: {
"groups": [{
"order": CustomSortOrder.alphabetical,
"type": CustomSortGroupType.Outsiders
}],
"outsidersGroupIdx": 0,
"targetFoldersPaths": ["mock-folder/.../"]
},
name: "mock-folder",
subtree: {}
}
}
}
const txtInputTargetFolderMultiSpecD: string = `
target-folder: ./*
`
const expectedSortSpecForMultiSpecD: { [key: string]: CustomSortSpec } = {
'mock-folder': {
groups: [{
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['mock-folder/*']
}
}
const expectedWildcardMatchingTreeForMultiSpecD: FolderMatchingTreeNode<CustomSortSpec> = {
subtree: {
"mock-folder": {
matchAll: {
"groups": [{
"order": CustomSortOrder.alphabetical,
"type": CustomSortGroupType.Outsiders
}],
"outsidersGroupIdx": 0,
"targetFoldersPaths": ["mock-folder/*"]
},
name: "mock-folder",
subtree: {}
}
}
}
const txtInputTargetFolderMultiSpecE: string = `
target-folder: mock-folder/...
`
const expectedSortSpecForMultiSpecE: { [key: string]: CustomSortSpec } = {
'mock-folder': {
groups: [{
order: CustomSortOrder.alphabetical,
type: CustomSortGroupType.Outsiders
}],
outsidersGroupIdx: 0,
targetFoldersPaths: ['mock-folder/...']
}
}
const expectedWildcardMatchingTreeForMultiSpecE: FolderMatchingTreeNode<CustomSortSpec> = {
subtree: {
"mock-folder": {
matchChildren: {
"groups": [{
"order": CustomSortOrder.alphabetical,
"type": CustomSortGroupType.Outsiders
}],
"outsidersGroupIdx": 0,
"targetFoldersPaths": ["mock-folder/..."]
},
name: "mock-folder",
subtree: {}
}
}
}
describe('SortingSpecProcessor path wildcard priorities', () => {
let processor: SortingSpecProcessor;
beforeEach(() => {
processor = new SortingSpecProcessor();
});
it('should not raise error for multiple spec for the same path and choose correct spec, case A', () => {
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecA.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecAandB)
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecAandB)
})
it('should not raise error for multiple spec for the same path and choose correct spec, case B', () => {
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecB.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecAandB)
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecAandB)
})
it('should not raise error for multiple spec for the same path and choose correct spec, case C', () => {
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecC.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecC)
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecC)
})
it('should not raise error for multiple spec for the same path and choose correct spec, case D', () => {
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecD.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecD)
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecD)
})
it('should not raise error for multiple spec for the same path and choose correct spec, case E', () => {
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecE.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecE)
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecE)
}) })
}) })
@ -600,6 +832,13 @@ target-folder: AAA
const txtInputErrorTooManyNumericSortSymbols: string = ` const txtInputErrorTooManyNumericSortSymbols: string = `
% Chapter\\R+ ... page\\d+ % Chapter\\R+ ... page\\d+
` `
const txtInputErrorNestedStandardObsidianSortAttr: string = `
target-folder: AAA
/ Some folder
sorting: standard
`
const txtInputEmptySpec: string = `` const txtInputEmptySpec: string = ``
describe('SortingSpecProcessor error detection and reporting', () => { describe('SortingSpecProcessor error detection and reporting', () => {
@ -687,6 +926,15 @@ describe('SortingSpecProcessor error detection and reporting', () => {
`${ERR_PREFIX} 9:TooManyNumericSortingSymbols Maximum one numeric sorting indicator allowed per line ${ERR_SUFFIX_IN_LINE(2)}`) `${ERR_PREFIX} 9:TooManyNumericSortingSymbols Maximum one numeric sorting indicator allowed per line ${ERR_SUFFIX_IN_LINE(2)}`)
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('% Chapter\\R+ ... page\\d+ ')) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('% Chapter\\R+ ... page\\d+ '))
}) })
it('should recognize error: nested standard obsidian sorting attribute', () => {
const inputTxtArr: Array<string> = txtInputErrorNestedStandardObsidianSortAttr.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
expect(result).toBeNull()
expect(errorsLogger).toHaveBeenCalledTimes(2)
expect(errorsLogger).toHaveBeenNthCalledWith(1,
`${ERR_PREFIX} 14:StandardObsidianSortAllowedOnlyAtFolderLevel The standard Obsidian sort order is only allowed at a folder level (not nested syntax) ${ERR_SUFFIX_IN_LINE(4)}`)
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard'))
})
it('should recognize empty spec', () => { it('should recognize empty spec', () => {
const inputTxtArr: Array<string> = txtInputEmptySpec.split('\n') const inputTxtArr: Array<string> = txtInputEmptySpec.split('\n')
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')

View File

@ -3,9 +3,9 @@ import {
CustomSortGroupType, CustomSortGroupType,
CustomSortOrder, CustomSortOrder,
CustomSortSpec, CustomSortSpec,
NormalizerFn, RecognizedOrderValue, NormalizerFn,
RegExpSpec, RecognizedOrderValue,
SortSpecsCollection RegExpSpec
} from "./custom-sort-types"; } from "./custom-sort-types";
import {isDefined, last} from "../utils/utils"; import {isDefined, last} from "../utils/utils";
import { import {
@ -20,6 +20,12 @@ import {
NumberRegexStr, NumberRegexStr,
RomanNumberRegexStr RomanNumberRegexStr
} from "./matchers"; } from "./matchers";
import {
FolderWildcardMatching,
MATCH_ALL_SUFFIX,
MATCH_CHILDREN_1_SUFFIX,
MATCH_CHILDREN_2_SUFFIX
} from "./folder-matching-rules"
interface ProcessingContext { interface ProcessingContext {
folderPath: string folderPath: string
@ -54,11 +60,14 @@ export enum ProblemCode {
TooManyNumericSortingSymbols, TooManyNumericSortingSymbols,
NumericalSymbolAdjacentToWildcard, NumericalSymbolAdjacentToWildcard,
ItemToHideExactNameWithExtRequired, ItemToHideExactNameWithExtRequired,
ItemToHideNoSupportForThreeDots ItemToHideNoSupportForThreeDots,
DuplicateWildcardSortSpecForSameFolder,
StandardObsidianSortAllowedOnlyAtFolderLevel
} }
const ContextFreeProblems = new Set<ProblemCode>([ const ContextFreeProblems = new Set<ProblemCode>([
ProblemCode.DuplicateSortSpecForSameFolder ProblemCode.DuplicateSortSpecForSameFolder,
ProblemCode.DuplicateWildcardSortSpecForSameFolder
]) ])
const ThreeDots = '...'; const ThreeDots = '...';
@ -104,7 +113,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
enum Attribute { enum Attribute {
TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ... TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ...
OrderAsc, OrderAsc,
OrderDesc OrderDesc,
OrderStandardObsidian
} }
const AttrLexems: { [key: string]: Attribute } = { const AttrLexems: { [key: string]: Attribute } = {
@ -112,6 +122,7 @@ const AttrLexems: { [key: string]: Attribute } = {
'target-folder:': Attribute.TargetFolder, 'target-folder:': Attribute.TargetFolder,
'order-asc:': Attribute.OrderAsc, 'order-asc:': Attribute.OrderAsc,
'order-desc:': Attribute.OrderDesc, 'order-desc:': Attribute.OrderDesc,
'sorting:': Attribute.OrderStandardObsidian,
// Concise abbreviated equivalents // Concise abbreviated equivalents
'::::': Attribute.TargetFolder, '::::': Attribute.TargetFolder,
'<': Attribute.OrderAsc, '<': Attribute.OrderAsc,
@ -280,6 +291,15 @@ export const convertPlainStringWithNumericSortingSymbolToRegex = (s: string, act
} }
} }
export interface FolderPathToSortSpecMap {
[key: string]: CustomSortSpec
}
export interface SortSpecsCollection {
sortSpecByPath: FolderPathToSortSpecMap
sortSpecByWildcard?: FolderWildcardMatching<CustomSortSpec>
}
interface AdjacencyInfo { interface AdjacencyInfo {
noPrefix: boolean, noPrefix: boolean,
noSuffix: boolean noSuffix: boolean
@ -292,6 +312,43 @@ const checkAdjacency = (sortingSymbolInfo: ExtractedNumericSortingSymbolInfo): A
} }
} }
const endsWithWildcardPatternSuffix = (path: string): boolean => {
return path.endsWith(MATCH_CHILDREN_1_SUFFIX) ||
path.endsWith(MATCH_CHILDREN_2_SUFFIX) ||
path.endsWith(MATCH_ALL_SUFFIX)
}
enum WildcardPriority {
NO_WILDCARD = 1,
MATCH_CHILDREN,
MATCH_ALL
}
const stripWildcardPatternSuffix = (path: string): [path: string, priority: number] => {
if (path.endsWith(MATCH_ALL_SUFFIX)) {
return [
path.slice(0, -MATCH_ALL_SUFFIX.length),
WildcardPriority.MATCH_ALL
]
}
if (path.endsWith(MATCH_CHILDREN_1_SUFFIX)) {
return [
path.slice(0, -MATCH_CHILDREN_1_SUFFIX.length),
WildcardPriority.MATCH_CHILDREN,
]
}
if (path.endsWith(MATCH_CHILDREN_2_SUFFIX)) {
return [
path.slice(0, -MATCH_CHILDREN_2_SUFFIX.length),
WildcardPriority.MATCH_CHILDREN
]
}
return [
path,
WildcardPriority.NO_WILDCARD
]
}
const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case." const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case."
export class SortingSpecProcessor { export class SortingSpecProcessor {
@ -359,13 +416,56 @@ export class SortingSpecProcessor {
if (this.ctx.specs.length > 0) { if (this.ctx.specs.length > 0) {
for (let spec of this.ctx.specs) { for (let spec of this.ctx.specs) {
this._l1s6_postprocessSortSpec(spec) this._l1s6_postprocessSortSpec(spec)
for (let folderPath of spec.targetFoldersPaths) {
collection = collection ?? {}
if (collection[folderPath]) {
this.problem(ProblemCode.DuplicateSortSpecForSameFolder, `Duplicate sorting spec for folder ${folderPath}`)
return null // Failure - not allow duplicate specs for the same folder
} }
collection[folderPath] = spec
let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec>
for (let spec of this.ctx.specs) {
// Consume the folder paths ending with wildcard specs
for (let idx = 0; idx<spec.targetFoldersPaths.length; idx++) {
const path = spec.targetFoldersPaths[idx]
if (endsWithWildcardPatternSuffix(path)) {
sortspecByWildcard = sortspecByWildcard ?? new FolderWildcardMatching<CustomSortSpec>()
const ruleAdded = sortspecByWildcard.addWildcardDefinition(path, spec)
if (ruleAdded?.errorMsg) {
this.problem(ProblemCode.DuplicateWildcardSortSpecForSameFolder, ruleAdded?.errorMsg)
return null // Failure - not allow duplicate wildcard specs for the same folder
}
}
}
}
if (sortspecByWildcard) {
collection = collection ?? { sortSpecByPath:{} }
collection.sortSpecByWildcard = sortspecByWildcard
}
// Helper transient map to deal with rule priorities for the same path
// and also detect non-wildcard duplicates.
// The wildcard duplicates were detected prior to this point, no need to bother about them
const pathMatchPriorityForPath: {[key: string]: WildcardPriority} = {}
for (let spec of this.ctx.specs) {
for (let idx = 0; idx < spec.targetFoldersPaths.length; idx++) {
const originalPath = spec.targetFoldersPaths[idx]
collection = collection ?? { sortSpecByPath: {} }
let detectedWildcardPriority: WildcardPriority
let path: string
[path, detectedWildcardPriority] = stripWildcardPatternSuffix(originalPath)
let storeTheSpec: boolean = true
const preexistingSortSpecPriority: WildcardPriority = pathMatchPriorityForPath[path]
if (preexistingSortSpecPriority) {
if (preexistingSortSpecPriority === WildcardPriority.NO_WILDCARD && detectedWildcardPriority === WildcardPriority.NO_WILDCARD) {
this.problem(ProblemCode.DuplicateSortSpecForSameFolder, `Duplicate sorting spec for folder ${path}`)
return null // Failure - not allow duplicate specs for the same no-wildcard folder path
} else if (detectedWildcardPriority >= preexistingSortSpecPriority) {
// Ignore lower priority rule
storeTheSpec = false
}
}
if (storeTheSpec) {
collection.sortSpecByPath[path] = spec
pathMatchPriorityForPath[path] = detectedWildcardPriority
}
} }
} }
} }
@ -452,7 +552,7 @@ export class SortingSpecProcessor {
this.problem(ProblemCode.TargetFolderNestedSpec, `Nested (indented) specification of target folder is not allowed`) this.problem(ProblemCode.TargetFolderNestedSpec, `Nested (indented) specification of target folder is not allowed`)
return false return false
} }
} else if (attr.attribute === Attribute.OrderAsc || attr.attribute === Attribute.OrderDesc) { } else if (attr.attribute === Attribute.OrderAsc || attr.attribute === Attribute.OrderDesc || attr.attribute === Attribute.OrderStandardObsidian) {
if (attr.nesting === 0) { if (attr.nesting === 0) {
if (!this.ctx.currentSpec) { if (!this.ctx.currentSpec) {
this._l2s2_putNewSpecForNewTargetFolder() this._l2s2_putNewSpecForNewTargetFolder()
@ -474,6 +574,10 @@ export class SortingSpecProcessor {
this.problem(ProblemCode.DuplicateOrderAttr, `Duplicate order specification for a sorting rule of folder ${folderPathsForProblemMsg}`) this.problem(ProblemCode.DuplicateOrderAttr, `Duplicate order specification for a sorting rule of folder ${folderPathsForProblemMsg}`)
return false; return false;
} }
if ((attr.value as RecognizedOrderValue).order === CustomSortOrder.standardObsidian) {
this.problem(ProblemCode.StandardObsidianSortAllowedOnlyAtFolderLevel, `The standard Obsidian sort order is only allowed at a folder level (not nested syntax)`)
return false;
}
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
return true; return true;
@ -635,10 +739,14 @@ export class SortingSpecProcessor {
} }
} }
const CURRENT_FOLDER_PREFIX: string = `${CURRENT_FOLDER_SYMBOL}/`
// Replace the dot-folder names (coming from: 'target-folder: .') with actual folder names // Replace the dot-folder names (coming from: 'target-folder: .') with actual folder names
spec.targetFoldersPaths.forEach((path, idx) => { spec.targetFoldersPaths.forEach((path, idx) => {
if (path === CURRENT_FOLDER_SYMBOL) { if (path === CURRENT_FOLDER_SYMBOL) {
spec.targetFoldersPaths[idx] = this.ctx.folderPath spec.targetFoldersPaths[idx] = this.ctx.folderPath
} else if (path.startsWith(CURRENT_FOLDER_PREFIX)) {
spec.targetFoldersPaths[idx] = `${this.ctx.folderPath}/${path.substr(CURRENT_FOLDER_PREFIX.length)}`
} }
}); });
} }
@ -675,10 +783,19 @@ export class SortingSpecProcessor {
} : null; } : null;
} }
private _l2s1_validateSortingAttrValue = (v: string): RecognizedOrderValue | null => {
// for now only a single fixed lexem
const recognized: boolean = v.trim().toLowerCase() === 'standard'
return recognized ? {
order: CustomSortOrder.standardObsidian
} : null;
}
attrValueValidators: { [key in Attribute]: AttrValueValidatorFn } = { attrValueValidators: { [key in Attribute]: AttrValueValidatorFn } = {
[Attribute.TargetFolder]: this._l2s1_validateTargetFolderAttrValue.bind(this), [Attribute.TargetFolder]: this._l2s1_validateTargetFolderAttrValue.bind(this),
[Attribute.OrderAsc]: this._l2s1_validateOrderAscAttrValue.bind(this), [Attribute.OrderAsc]: this._l2s1_validateOrderAscAttrValue.bind(this),
[Attribute.OrderDesc]: this._l2s1_validateOrderDescAttrValue.bind(this), [Attribute.OrderDesc]: this._l2s1_validateOrderDescAttrValue.bind(this),
[Attribute.OrderStandardObsidian]: this._l2s1_validateSortingAttrValue.bind(this)
} }
_l2s2_convertPlainStringSortingGroupSpecToArraySpec = (spec: string): Array<string> => { _l2s2_convertPlainStringSortingGroupSpecToArraySpec = (spec: string): Array<string> => {

View File

@ -14,14 +14,15 @@ import {
} from 'obsidian'; } from 'obsidian';
import {around} from 'monkey-around'; import {around} from 'monkey-around';
import {folderSort} from './custom-sort/custom-sort'; import {folderSort} from './custom-sort/custom-sort';
import {SortingSpecProcessor} from './custom-sort/sorting-spec-processor'; import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
import {CustomSortSpec, SortSpecsCollection} from './custom-sort/custom-sort-types'; import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
import { import {
addIcons, addIcons,
ICON_SORT_ENABLED_ACTIVE, ICON_SORT_ENABLED_ACTIVE,
ICON_SORT_ENABLED_NOT_APPLIED,
ICON_SORT_SUSPENDED, ICON_SORT_SUSPENDED,
ICON_SORT_SUSPENDED_SYNTAX_ERROR, ICON_SORT_SUSPENDED_SYNTAX_ERROR
ICON_SORT_ENABLED_NOT_APPLIED
} from "./custom-sort/icons"; } from "./custom-sort/icons";
interface CustomSortPluginSettings { interface CustomSortPluginSettings {
@ -87,9 +88,9 @@ export default class CustomSortPlugin extends Plugin {
new Notice(`Parsing custom sorting specification SUCCEEDED!`) new Notice(`Parsing custom sorting specification SUCCEEDED!`)
} else { } else {
if (anySortingSpecFound) { if (anySortingSpecFound) {
errorMessage = `No valid '${SORTINGSPEC_YAML_KEY}:' key(s) in YAML front matter or multiline YAML indentation error or general YAML syntax error` errorMessage = errorMessage ? errorMessage : `No valid '${SORTINGSPEC_YAML_KEY}:' key(s) in YAML front matter or multiline YAML indentation error or general YAML syntax error`
} else { } else {
errorMessage = errorMessage ? errorMessage : `No custom sorting specification found or only empty specification(s)` errorMessage = `No custom sorting specification found or only empty specification(s)`
} }
new Notice(`Parsing custom sorting specification FAILED. Suspending the plugin.\n${errorMessage}`, ERROR_NOTICE_TIMEOUT) new Notice(`Parsing custom sorting specification FAILED. Suspending the plugin.\n${errorMessage}`, ERROR_NOTICE_TIMEOUT)
this.settings.suspended = true this.settings.suspended = true
@ -196,13 +197,30 @@ export default class CustomSortPlugin extends Plugin {
let tmpFolder = new TFolder(Vault, ""); let tmpFolder = new TFolder(Vault, "");
let Folder = fileExplorer.createFolderDom(tmpFolder).constructor; let Folder = fileExplorer.createFolderDom(tmpFolder).constructor;
this.register( this.register(
// TODO: Unit tests please!!! The logic below becomes more and more complex, bugs are captured at run-time...
around(Folder.prototype, { around(Folder.prototype, {
sort(old: any) { sort(old: any) {
return function (...args: any[]) { return function (...args: any[]) {
// quick check for plugin status
if (plugin.settings.suspended) {
return old.call(this, ...args);
}
// if custom sort is not specified, use the UI-selected // if custom sort is not specified, use the UI-selected
const folder: TFolder = this.file const folder: TFolder = this.file
const sortSpec: CustomSortSpec = plugin.sortSpecCache?.[folder.path] let sortSpec: CustomSortSpec = plugin.sortSpecCache?.sortSpecByPath[folder.path]
if (!plugin.settings.suspended && sortSpec) { if (sortSpec) {
if (sortSpec.defaultOrder === CustomSortOrder.standardObsidian) {
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
}
} else if (plugin.sortSpecCache?.sortSpecByWildcard) {
// when no sorting spec found directly by folder path, check for wildcard-based match
sortSpec = plugin.sortSpecCache?.sortSpecByWildcard.folderMatch(folder.path)
if (sortSpec?.defaultOrder === CustomSortOrder.standardObsidian) {
sortSpec = null // A folder subtree can be also explicitly excluded from custom sorting plugin
}
}
if (sortSpec) {
return folderSort.call(this, sortSpec, ...args); return folderSort.call(this, sortSpec, ...args);
} else { } else {
return old.call(this, ...args); return old.call(this, ...args);

View File

@ -1,4 +1,5 @@
{ {
"0.5.188": "0.12.0", "0.5.188": "0.12.0",
"0.5.189": "0.12.0" "0.5.189": "0.12.0",
"0.6.0": "0.12.0"
} }