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:
parent
b1a43dff3a
commit
e972bce007
|
@ -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>",
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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/*"})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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')
|
||||||
|
|
|
@ -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> => {
|
||||||
|
|
34
src/main.ts
34
src/main.ts
|
@ -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);
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue