Merge pull request #9 from SebastianMC/merge-upstream-1
Merge upstream obsidianmd/obsidian-sample-plugin + bugfix
This commit is contained in:
commit
3a02ecd65c
|
@ -21,30 +21,20 @@ esbuild.build({
|
||||||
'obsidian',
|
'obsidian',
|
||||||
'electron',
|
'electron',
|
||||||
'@codemirror/autocomplete',
|
'@codemirror/autocomplete',
|
||||||
'@codemirror/closebrackets',
|
|
||||||
'@codemirror/collab',
|
'@codemirror/collab',
|
||||||
'@codemirror/commands',
|
'@codemirror/commands',
|
||||||
'@codemirror/comment',
|
|
||||||
'@codemirror/fold',
|
|
||||||
'@codemirror/gutter',
|
|
||||||
'@codemirror/highlight',
|
|
||||||
'@codemirror/history',
|
|
||||||
'@codemirror/language',
|
'@codemirror/language',
|
||||||
'@codemirror/lint',
|
'@codemirror/lint',
|
||||||
'@codemirror/matchbrackets',
|
|
||||||
'@codemirror/panel',
|
|
||||||
'@codemirror/rangeset',
|
|
||||||
'@codemirror/rectangular-selection',
|
|
||||||
'@codemirror/search',
|
'@codemirror/search',
|
||||||
'@codemirror/state',
|
'@codemirror/state',
|
||||||
'@codemirror/stream-parser',
|
|
||||||
'@codemirror/text',
|
|
||||||
'@codemirror/tooltip',
|
|
||||||
'@codemirror/view',
|
'@codemirror/view',
|
||||||
|
'@lezer/common',
|
||||||
|
'@lezer/highlight',
|
||||||
|
'@lezer/lr',
|
||||||
...builtins],
|
...builtins],
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
watch: !prod,
|
watch: !prod,
|
||||||
target: 'es2016',
|
target: 'es2018',
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
sourcemap: prod ? false : 'inline',
|
sourcemap: prod ? false : 'inline',
|
||||||
minify: prod,
|
minify: prod,
|
||||||
|
|
12
package.json
12
package.json
|
@ -18,15 +18,15 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^28.1.2",
|
"@types/jest": "^28.1.2",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.2.0",
|
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||||
"@typescript-eslint/parser": "^5.2.0",
|
"@typescript-eslint/parser": "5.29.0",
|
||||||
"builtin-modules": "^3.2.0",
|
"builtin-modules": "3.3.0",
|
||||||
"esbuild": "0.13.12",
|
"esbuild": "0.14.47",
|
||||||
"jest": "^28.1.1",
|
"jest": "^28.1.1",
|
||||||
"monkey-around": "^2.3.0",
|
"monkey-around": "^2.3.0",
|
||||||
"obsidian": "^0.15.4",
|
"obsidian": "^0.15.4",
|
||||||
"ts-jest": "^28.0.5",
|
"ts-jest": "^28.0.5",
|
||||||
"tslib": "2.3.1",
|
"tslib": "2.4.0",
|
||||||
"typescript": "4.4.4"
|
"typescript": "4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ export enum CustomSortOrder {
|
||||||
byModifiedTimeReverse,
|
byModifiedTimeReverse,
|
||||||
byCreatedTime,
|
byCreatedTime,
|
||||||
byCreatedTimeReverse,
|
byCreatedTimeReverse,
|
||||||
standardObsidian// Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
|
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
|
||||||
|
default = alphabetical
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecognizedOrderValue {
|
export interface RecognizedOrderValue {
|
||||||
|
@ -22,7 +23,7 @@ export interface RecognizedOrderValue {
|
||||||
secondaryOrder?: CustomSortOrder
|
secondaryOrder?: CustomSortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NormalizerFn = (s: string) => string
|
export type NormalizerFn = (s: string) => string | null
|
||||||
|
|
||||||
export interface RegExpSpec {
|
export interface RegExpSpec {
|
||||||
regex: RegExp
|
regex: RegExp
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {TFile} from 'obsidian';
|
import {TFile, TFolder, Vault} from 'obsidian';
|
||||||
import {determineSortingGroup} from './custom-sort';
|
import {determineSortingGroup} from './custom-sort';
|
||||||
import {CustomSortGroupType, CustomSortSpec} from './custom-sort-types';
|
import {CustomSortGroupType, CustomSortSpec} from './custom-sort-types';
|
||||||
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
|
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
|
||||||
|
@ -12,10 +12,10 @@ const mockTFile = (basename: string, ext: string, size?: number, ctime?: number,
|
||||||
},
|
},
|
||||||
basename: basename,
|
basename: basename,
|
||||||
extension: ext,
|
extension: ext,
|
||||||
vault: null,
|
vault: {} as Vault, // To satisfy TS typechecking
|
||||||
path: `Some parent folder/${basename}.${ext}`,
|
path: `Some parent folder/${basename}.${ext}`,
|
||||||
name: `${basename}.${ext}`,
|
name: `${basename}.${ext}`,
|
||||||
parent: null
|
parent: {} as TFolder // To satisfy TS typechecking
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import {TFile, TFolder, requireApiVersion} from 'obsidian';
|
import {requireApiVersion, TFile, TFolder} from 'obsidian';
|
||||||
import {
|
import {CustomSortGroup, CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
||||||
CustomSortGroup,
|
|
||||||
CustomSortGroupType,
|
|
||||||
CustomSortOrder,
|
|
||||||
CustomSortSpec
|
|
||||||
} from "./custom-sort-types";
|
|
||||||
import {isDefined} from "../utils/utils";
|
import {isDefined} from "../utils/utils";
|
||||||
|
|
||||||
let Collator = new Intl.Collator(undefined, {
|
let Collator = new Intl.Collator(undefined, {
|
||||||
|
@ -15,7 +10,7 @@ let Collator = new Intl.Collator(undefined, {
|
||||||
|
|
||||||
interface FolderItemForSorting {
|
interface FolderItemForSorting {
|
||||||
path: string
|
path: string
|
||||||
groupIdx: number // the index itself represents order for groups
|
groupIdx?: number // the index itself represents order for groups
|
||||||
sortString: string // fragment (or full name) to be used for sorting
|
sortString: string // fragment (or full name) to be used for sorting
|
||||||
matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match'
|
matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match'
|
||||||
ctime: number
|
ctime: number
|
||||||
|
@ -38,15 +33,22 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
|
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
|
||||||
if (itA.groupIdx === itB.groupIdx) {
|
if (itA.groupIdx != undefined && itB.groupIdx != undefined) {
|
||||||
const group: CustomSortGroup = sortSpec.groups[itA.groupIdx]
|
if (itA.groupIdx === itB.groupIdx) {
|
||||||
if (group.regexSpec && group.secondaryOrder && itA.matchGroup === itB.matchGroup) {
|
const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx]
|
||||||
return Sorters[group.secondaryOrder](itA, itB)
|
if (group?.regexSpec && group.secondaryOrder && itA.matchGroup === itB.matchGroup) {
|
||||||
|
return Sorters[group.secondaryOrder ?? CustomSortOrder.default](itA, itB)
|
||||||
|
} else {
|
||||||
|
return Sorters[group?.order ?? CustomSortOrder.default](itA, itB)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Sorters[group.order](itA, itB)
|
return itA.groupIdx - itB.groupIdx;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return itA.groupIdx - itB.groupIdx;
|
// should never happen - groupIdx is not known for at least one of items to compare.
|
||||||
|
// The logic of determining the index always sets some idx
|
||||||
|
// Yet for sanity and to satisfy TS code analyzer a fallback to default behavior below
|
||||||
|
return Sorters[CustomSortOrder.default](itA, itB)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +60,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
|
let groupIdx: number
|
||||||
let determined: boolean = false
|
let determined: boolean = false
|
||||||
let matchedGroup: string
|
let matchedGroup: string | null | undefined
|
||||||
const aFolder: boolean = isFolder(entry)
|
const aFolder: boolean = isFolder(entry)
|
||||||
const aFile: boolean = !aFolder
|
const aFile: boolean = !aFolder
|
||||||
const entryAsTFile: TFile = entry as TFile
|
const entryAsTFile: TFile = entry as TFile
|
||||||
|
@ -77,10 +79,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
determined = true;
|
determined = true;
|
||||||
}
|
}
|
||||||
} else { // regexp is involved
|
} else { // regexp is involved
|
||||||
const match: RegExpMatchArray = group.regexSpec.regex.exec(nameForMatching);
|
const match: RegExpMatchArray | null | undefined = group.regexSpec?.regex.exec(nameForMatching);
|
||||||
if (match) {
|
if (match) {
|
||||||
determined = true
|
determined = true
|
||||||
matchedGroup = group.regexSpec.normalizerFn(match[1]);
|
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -90,10 +92,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
determined = true;
|
determined = true;
|
||||||
}
|
}
|
||||||
} else { // regexp is involved
|
} else { // regexp is involved
|
||||||
const match: RegExpMatchArray = group.regexSpec.regex.exec(nameForMatching);
|
const match: RegExpMatchArray | null | undefined = group.regexSpec?.regex.exec(nameForMatching);
|
||||||
if (match) {
|
if (match) {
|
||||||
determined = true
|
determined = true
|
||||||
matchedGroup = group.regexSpec.normalizerFn(match[1]);
|
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -107,10 +109,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
} else { // regexp is involved as the prefix or as the suffix
|
} else { // regexp is involved as the prefix or as the suffix
|
||||||
if ((group.exactPrefix && nameForMatching.startsWith(group.exactPrefix)) ||
|
if ((group.exactPrefix && nameForMatching.startsWith(group.exactPrefix)) ||
|
||||||
(group.exactSuffix && nameForMatching.endsWith(group.exactSuffix))) {
|
(group.exactSuffix && nameForMatching.endsWith(group.exactSuffix))) {
|
||||||
const match: RegExpMatchArray = group.regexSpec.regex.exec(nameForMatching);
|
const match: RegExpMatchArray | null | undefined = group.regexSpec?.regex.exec(nameForMatching);
|
||||||
if (match) {
|
if (match) {
|
||||||
const fullMatch: string = match[0]
|
const fullMatch: string = match[0]
|
||||||
matchedGroup = group.regexSpec.normalizerFn(match[1]);
|
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||||
// check for overlapping of prefix and suffix match (not allowed)
|
// check for overlapping of prefix and suffix match (not allowed)
|
||||||
if ((fullMatch.length + (group.exactPrefix?.length ?? 0) + (group.exactSuffix?.length ?? 0)) <= nameForMatching.length) {
|
if ((fullMatch.length + (group.exactPrefix?.length ?? 0) + (group.exactSuffix?.length ?? 0)) <= nameForMatching.length) {
|
||||||
determined = true
|
determined = true
|
||||||
|
@ -127,10 +129,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
determined = true;
|
determined = true;
|
||||||
}
|
}
|
||||||
} else { // regexp is involved
|
} else { // regexp is involved
|
||||||
const match: RegExpMatchArray = group.regexSpec.regex.exec(nameForMatching);
|
const match: RegExpMatchArray | null | undefined = group.regexSpec?.regex.exec(nameForMatching);
|
||||||
if (match) {
|
if (match) {
|
||||||
determined = true
|
determined = true
|
||||||
matchedGroup = group.regexSpec.normalizerFn(match[1]);
|
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -144,7 +146,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
}
|
}
|
||||||
|
|
||||||
// the final groupIdx for undetermined folder entry is either the last+1 groupIdx or idx of explicitly defined outsiders group
|
// the final groupIdx for undetermined folder entry is either the last+1 groupIdx or idx of explicitly defined outsiders group
|
||||||
let determinedGroupIdx = groupIdx;
|
let determinedGroupIdx: number | undefined = groupIdx;
|
||||||
|
|
||||||
if (!determined) {
|
if (!determined) {
|
||||||
// Automatically assign the index to outsiders group, if relevant was configured
|
// Automatically assign the index to outsiders group, if relevant was configured
|
||||||
|
@ -174,7 +176,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
|
||||||
|
|
||||||
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) => {
|
||||||
return !sortingSpec.itemsToHide.has(entry.name)
|
return !sortingSpec.itemsToHide!.has(entry.name)
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
this.file.children)
|
this.file.children)
|
||||||
|
|
|
@ -58,18 +58,18 @@ describe('folderMatch', () => {
|
||||||
['Reviews/daily/a/Tue/Early/9am', '4 Reviews/daily/a/*']
|
['Reviews/daily/a/Tue/Early/9am', '4 Reviews/daily/a/*']
|
||||||
])('%s should match %s', (path: string, rule: string) => {
|
])('%s should match %s', (path: string, rule: string) => {
|
||||||
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRichVersion()
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRichVersion()
|
||||||
const match: SortingSpec = matcher.folderMatch(path)
|
const match: SortingSpec | null = matcher.folderMatch(path)
|
||||||
const matchFromCache: SortingSpec = matcher.folderMatch(path)
|
const matchFromCache: SortingSpec | null = matcher.folderMatch(path)
|
||||||
expect(match).toBe(rule)
|
expect(match).toBe(rule)
|
||||||
expect(matchFromCache).toBe(rule)
|
expect(matchFromCache).toBe(rule)
|
||||||
})
|
})
|
||||||
it('should correctly handle no-root definitions', () => {
|
it('should correctly handle no-root definitions', () => {
|
||||||
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherSimplestVersion()
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherSimplestVersion()
|
||||||
const match1: SortingSpec = matcher.folderMatch('/')
|
const match1: SortingSpec | null = matcher.folderMatch('/')
|
||||||
const match2: SortingSpec = matcher.folderMatch('/Reviews')
|
const match2: SortingSpec | null = matcher.folderMatch('/Reviews')
|
||||||
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
|
const match3: SortingSpec | null = matcher.folderMatch('/Reviews/daily/')
|
||||||
const match4: SortingSpec = matcher.folderMatch('/Reviews/daily/Mon')
|
const match4: SortingSpec | null = matcher.folderMatch('/Reviews/daily/Mon')
|
||||||
const match5: SortingSpec = matcher.folderMatch('/Reviews/daily/Mon')
|
const match5: SortingSpec | null = matcher.folderMatch('/Reviews/daily/Mon')
|
||||||
expect(match1).toBeNull()
|
expect(match1).toBeNull()
|
||||||
expect(match2).toBeNull()
|
expect(match2).toBeNull()
|
||||||
expect(match3).toBe('/Reviews/daily/*')
|
expect(match3).toBe('/Reviews/daily/*')
|
||||||
|
@ -78,27 +78,27 @@ describe('folderMatch', () => {
|
||||||
})
|
})
|
||||||
it('should correctly handle root-only definition', () => {
|
it('should correctly handle root-only definition', () => {
|
||||||
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRootOnlyVersion()
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRootOnlyVersion()
|
||||||
const match1: SortingSpec = matcher.folderMatch('/')
|
const match1: SortingSpec | null = matcher.folderMatch('/')
|
||||||
const match2: SortingSpec = matcher.folderMatch('/Reviews')
|
const match2: SortingSpec | null = matcher.folderMatch('/Reviews')
|
||||||
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
|
const match3: SortingSpec | null = matcher.folderMatch('/Reviews/daily/')
|
||||||
expect(match1).toBe('/...')
|
expect(match1).toBe('/...')
|
||||||
expect(match2).toBe('/...')
|
expect(match2).toBe('/...')
|
||||||
expect(match3).toBeNull()
|
expect(match3).toBeNull()
|
||||||
})
|
})
|
||||||
it('should correctly handle root-only deep definition', () => {
|
it('should correctly handle root-only deep definition', () => {
|
||||||
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRootOnlyDeepVersion()
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRootOnlyDeepVersion()
|
||||||
const match1: SortingSpec = matcher.folderMatch('/')
|
const match1: SortingSpec | null = matcher.folderMatch('/')
|
||||||
const match2: SortingSpec = matcher.folderMatch('/Reviews')
|
const match2: SortingSpec | null = matcher.folderMatch('/Reviews')
|
||||||
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
|
const match3: SortingSpec | null = matcher.folderMatch('/Reviews/daily/')
|
||||||
expect(match1).toBe('/*')
|
expect(match1).toBe('/*')
|
||||||
expect(match2).toBe('/*')
|
expect(match2).toBe('/*')
|
||||||
expect(match3).toBe('/*')
|
expect(match3).toBe('/*')
|
||||||
})
|
})
|
||||||
it('should correctly handle match all and match children definitions for same path', () => {
|
it('should correctly handle match all and match children definitions for same path', () => {
|
||||||
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherSimpleVersion()
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherSimpleVersion()
|
||||||
const match1: SortingSpec = matcher.folderMatch('/')
|
const match1: SortingSpec | null = matcher.folderMatch('/')
|
||||||
const match2: SortingSpec = matcher.folderMatch('/Reviews/daily/')
|
const match2: SortingSpec | null = matcher.folderMatch('/Reviews/daily/')
|
||||||
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/1')
|
const match3: SortingSpec | null = matcher.folderMatch('/Reviews/daily/1')
|
||||||
expect(match1).toBeNull()
|
expect(match1).toBeNull()
|
||||||
expect(match2).toBe('/Reviews/daily/...')
|
expect(match2).toBe('/Reviews/daily/...')
|
||||||
expect(match3).toBe('/Reviews/daily/...')
|
expect(match3).toBe('/Reviews/daily/...')
|
||||||
|
|
|
@ -41,9 +41,9 @@ export class FolderWildcardMatching<SortingSpec> {
|
||||||
// cache
|
// cache
|
||||||
determinedWildcardRules: { [key: string]: DeterminedSortingSpec<SortingSpec> } = {}
|
determinedWildcardRules: { [key: string]: DeterminedSortingSpec<SortingSpec> } = {}
|
||||||
|
|
||||||
addWildcardDefinition = (wilcardDefinition: string, rule: SortingSpec): AddingWildcardFailure | null => {
|
addWildcardDefinition = (wilcardDefinition: string, rule: SortingSpec): AddingWildcardFailure | null | undefined => {
|
||||||
const pathComponents: Array<string> = splitPath(wilcardDefinition)
|
const pathComponents: Array<string> = splitPath(wilcardDefinition)
|
||||||
const lastComponent: string = pathComponents.pop()
|
const lastComponent: string | undefined = pathComponents.pop()
|
||||||
if (lastComponent !== MATCH_ALL_PATH_TOKEN && lastComponent !== MATCH_CHILDREN_PATH_TOKEN) {
|
if (lastComponent !== MATCH_ALL_PATH_TOKEN && lastComponent !== MATCH_CHILDREN_PATH_TOKEN) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -82,8 +82,8 @@ export class FolderWildcardMatching<SortingSpec> {
|
||||||
if (spec) {
|
if (spec) {
|
||||||
return spec.spec ?? null
|
return spec.spec ?? null
|
||||||
} else {
|
} else {
|
||||||
let rule: SortingSpec = this.tree.matchChildren
|
let rule: SortingSpec | null | undefined = this.tree.matchChildren
|
||||||
let inheritedRule: SortingSpec = this.tree.matchAll
|
let inheritedRule: SortingSpec | undefined = this.tree.matchAll
|
||||||
const pathComponents: Array<string> = splitPath(folderPath)
|
const pathComponents: Array<string> = splitPath(folderPath)
|
||||||
let parentNode: FolderMatchingTreeNode<SortingSpec> = this.tree
|
let parentNode: FolderMatchingTreeNode<SortingSpec> = this.tree
|
||||||
let lastIdx: number = pathComponents.length - 1
|
let lastIdx: number = pathComponents.length - 1
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe('Plain numbers regexp', () => {
|
||||||
const match: RegExpMatchArray | null = s.match(NumberRegex)
|
const match: RegExpMatchArray | null = s.match(NumberRegex)
|
||||||
if (out) {
|
if (out) {
|
||||||
expect(match).not.toBeNull()
|
expect(match).not.toBeNull()
|
||||||
expect(match[1]).toBe(out)
|
expect(match?.[1]).toBe(out)
|
||||||
} else {
|
} else {
|
||||||
expect(match).toBeNull()
|
expect(match).toBeNull()
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ describe('Plain compound numbers regexp (dot)', () => {
|
||||||
const match: RegExpMatchArray | null = s.match(CompoundNumberDotRegex)
|
const match: RegExpMatchArray | null = s.match(CompoundNumberDotRegex)
|
||||||
if (out) {
|
if (out) {
|
||||||
expect(match).not.toBeNull()
|
expect(match).not.toBeNull()
|
||||||
expect(match[1]).toBe(out)
|
expect(match?.[1]).toBe(out)
|
||||||
} else {
|
} else {
|
||||||
expect(match).toBeNull()
|
expect(match).toBeNull()
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ describe('Plain compound numbers regexp (dash)', () => {
|
||||||
const match: RegExpMatchArray | null = s.match(CompoundNumberDashRegex)
|
const match: RegExpMatchArray | null = s.match(CompoundNumberDashRegex)
|
||||||
if (out) {
|
if (out) {
|
||||||
expect(match).not.toBeNull()
|
expect(match).not.toBeNull()
|
||||||
expect(match[1]).toBe(out)
|
expect(match?.[1]).toBe(out)
|
||||||
} else {
|
} else {
|
||||||
expect(match).toBeNull()
|
expect(match).toBeNull()
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ describe('Plain Roman numbers regexp', () => {
|
||||||
const match: RegExpMatchArray | null = s.match(RomanNumberRegex)
|
const match: RegExpMatchArray | null = s.match(RomanNumberRegex)
|
||||||
if (out) {
|
if (out) {
|
||||||
expect(match).not.toBeNull()
|
expect(match).not.toBeNull()
|
||||||
expect(match[1]).toBe(out)
|
expect(match?.[1]).toBe(out)
|
||||||
} else {
|
} else {
|
||||||
expect(match).toBeNull()
|
expect(match).toBeNull()
|
||||||
}
|
}
|
||||||
|
@ -146,7 +146,7 @@ describe('Roman compound numbers regexp (dot)', () => {
|
||||||
const match: RegExpMatchArray | null = s.match(CompoundRomanNumberDotRegex)
|
const match: RegExpMatchArray | null = s.match(CompoundRomanNumberDotRegex)
|
||||||
if (out) {
|
if (out) {
|
||||||
expect(match).not.toBeNull()
|
expect(match).not.toBeNull()
|
||||||
expect(match[1]).toBe(out)
|
expect(match?.[1]).toBe(out)
|
||||||
} else {
|
} else {
|
||||||
expect(match).toBeNull()
|
expect(match).toBeNull()
|
||||||
}
|
}
|
||||||
|
@ -180,7 +180,7 @@ describe('Roman compound numbers regexp (dash)', () => {
|
||||||
const match: RegExpMatchArray | null = s.match(CompoundRomanNumberDashRegex)
|
const match: RegExpMatchArray | null = s.match(CompoundRomanNumberDashRegex)
|
||||||
if (out) {
|
if (out) {
|
||||||
expect(match).not.toBeNull()
|
expect(match).not.toBeNull()
|
||||||
expect(match[1]).toBe(out)
|
expect(match?.[1]).toBe(out)
|
||||||
} else {
|
} else {
|
||||||
expect(match).toBeNull()
|
expect(match).toBeNull()
|
||||||
}
|
}
|
||||||
|
|
|
@ -506,7 +506,7 @@ describe('SortingSpecProcessor', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputSimplistic1.split('\n')
|
const inputTxtArr: Array<string> = txtInputSimplistic1.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?.sortSpecByPath).toEqual(expectedSortSpecForSimplistic1)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForSimplistic1)
|
||||||
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForSimplistic1)
|
expect(result?.sortSpecByWildcard?.tree).toEqual(expectedWildcardMatchingTreeForSimplistic1)
|
||||||
})
|
})
|
||||||
it('should recognize the simplistic sorting spec to put files first (direct / rule)', () => {
|
it('should recognize the simplistic sorting spec to put files first (direct / rule)', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputSimplistic2.split('\n')
|
const inputTxtArr: Array<string> = txtInputSimplistic2.split('\n')
|
||||||
|
@ -837,31 +837,31 @@ describe('SortingSpecProcessor path wildcard priorities', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecA.split('\n')
|
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecA.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?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecAandB)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecAandB)
|
||||||
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecAandB)
|
expect(result?.sortSpecByWildcard?.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecAandB)
|
||||||
})
|
})
|
||||||
it('should not raise error for multiple spec for the same path and choose correct spec, case B', () => {
|
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 inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecB.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?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecAandB)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecAandB)
|
||||||
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecAandB)
|
expect(result?.sortSpecByWildcard?.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecAandB)
|
||||||
})
|
})
|
||||||
it('should not raise error for multiple spec for the same path and choose correct spec, case C', () => {
|
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 inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecC.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?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecC)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecC)
|
||||||
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecC)
|
expect(result?.sortSpecByWildcard?.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecC)
|
||||||
})
|
})
|
||||||
it('should not raise error for multiple spec for the same path and choose correct spec, case D', () => {
|
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 inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecD.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?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecD)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecD)
|
||||||
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecD)
|
expect(result?.sortSpecByWildcard?.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecD)
|
||||||
})
|
})
|
||||||
it('should not raise error for multiple spec for the same path and choose correct spec, case E', () => {
|
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 inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecE.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?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecE)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecE)
|
||||||
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecE)
|
expect(result?.sortSpecByWildcard?.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecE)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1224,7 +1224,7 @@ describe('convertPlainStringWithNumericSortingSymbolToRegex', () => {
|
||||||
['abc\\d+efg\\d+hij', /abc *(\d+)efg/i], // Double numerical sorting symbol, error case, covered for clarity of implementation detail
|
['abc\\d+efg\\d+hij', /abc *(\d+)efg/i], // Double numerical sorting symbol, error case, covered for clarity of implementation detail
|
||||||
])('should correctly extract from >%s< the numeric sorting symbol (%s)', (s: string, regex: RegExp) => {
|
])('should correctly extract from >%s< the numeric sorting symbol (%s)', (s: string, regex: RegExp) => {
|
||||||
const result = convertPlainStringWithNumericSortingSymbolToRegex(s, RegexpUsedAs.InUnitTest)
|
const result = convertPlainStringWithNumericSortingSymbolToRegex(s, RegexpUsedAs.InUnitTest)
|
||||||
expect(result.regexpSpec.regex).toEqual(regex)
|
expect(result?.regexpSpec.regex).toEqual(regex)
|
||||||
// No need to examine prefix and suffix fields of result, they are secondary and derived from the returned regexp
|
// No need to examine prefix and suffix fields of result, they are secondary and derived from the returned regexp
|
||||||
})
|
})
|
||||||
it('should not process string not containing numeric sorting symbol', () => {
|
it('should not process string not containing numeric sorting symbol', () => {
|
||||||
|
@ -1235,16 +1235,16 @@ describe('convertPlainStringWithNumericSortingSymbolToRegex', () => {
|
||||||
it('should correctly include regex token for string begin', () => {
|
it('should correctly include regex token for string begin', () => {
|
||||||
const input = 'Part\\-D+:'
|
const input = 'Part\\-D+:'
|
||||||
const result = convertPlainStringWithNumericSortingSymbolToRegex(input, RegexpUsedAs.Prefix)
|
const result = convertPlainStringWithNumericSortingSymbolToRegex(input, RegexpUsedAs.Prefix)
|
||||||
expect(result.regexpSpec.regex).toEqual(/^Part *(\d+(?:-\d+)*):/i)
|
expect(result?.regexpSpec.regex).toEqual(/^Part *(\d+(?:-\d+)*):/i)
|
||||||
})
|
})
|
||||||
it('should correctly include regex token for string end', () => {
|
it('should correctly include regex token for string end', () => {
|
||||||
const input = 'Part\\-D+:'
|
const input = 'Part\\-D+:'
|
||||||
const result = convertPlainStringWithNumericSortingSymbolToRegex(input, RegexpUsedAs.Suffix)
|
const result = convertPlainStringWithNumericSortingSymbolToRegex(input, RegexpUsedAs.Suffix)
|
||||||
expect(result.regexpSpec.regex).toEqual(/Part *(\d+(?:-\d+)*):$/i)
|
expect(result?.regexpSpec.regex).toEqual(/Part *(\d+(?:-\d+)*):$/i)
|
||||||
})
|
})
|
||||||
it('should correctly include regex token for string begin and end', () => {
|
it('should correctly include regex token for string begin and end', () => {
|
||||||
const input = 'Part\\.D+:'
|
const input = 'Part\\.D+:'
|
||||||
const result = convertPlainStringWithNumericSortingSymbolToRegex(input, RegexpUsedAs.FullMatch)
|
const result = convertPlainStringWithNumericSortingSymbolToRegex(input, RegexpUsedAs.FullMatch)
|
||||||
expect(result.regexpSpec.regex).toEqual(/^Part *(\d+(?:\.\d+)*):$/i)
|
expect(result?.regexpSpec.regex).toEqual(/^Part *(\d+(?:\.\d+)*):$/i)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -212,10 +212,14 @@ export const detectNumericSortingSymbols = (s: string): boolean => {
|
||||||
return numericSortingSymbolsRegex.test(s)
|
return numericSortingSymbolsRegex.test(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractNumericSortingSymbol = (s: string): string => {
|
export const extractNumericSortingSymbol = (s?: string): string | null => {
|
||||||
numericSortingSymbolsRegex.lastIndex = 0
|
if (s) {
|
||||||
const matches: RegExpMatchArray = numericSortingSymbolsRegex.exec(s)
|
numericSortingSymbolsRegex.lastIndex = 0
|
||||||
return matches ? matches[0] : null
|
const matches: RegExpMatchArray | null = numericSortingSymbolsRegex.exec(s)
|
||||||
|
return matches ? matches[0] : null
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegExpSpecStr {
|
export interface RegExpSpecStr {
|
||||||
|
@ -271,11 +275,11 @@ export enum RegexpUsedAs {
|
||||||
FullMatch
|
FullMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertPlainStringWithNumericSortingSymbolToRegex = (s: string, actAs: RegexpUsedAs): ExtractedNumericSortingSymbolInfo => {
|
export const convertPlainStringWithNumericSortingSymbolToRegex = (s?: string, actAs?: RegexpUsedAs): ExtractedNumericSortingSymbolInfo | null => {
|
||||||
const detectedSymbol: string = extractNumericSortingSymbol(s)
|
const detectedSymbol: string | null = extractNumericSortingSymbol(s)
|
||||||
if (detectedSymbol) {
|
if (detectedSymbol) {
|
||||||
const replacement: RegExpSpecStr = numericSortingSymbolToRegexpStr[detectedSymbol.toLowerCase()]
|
const replacement: RegExpSpecStr = numericSortingSymbolToRegexpStr[detectedSymbol.toLowerCase()]
|
||||||
const [extractedPrefix, extractedSuffix] = s.split(detectedSymbol)
|
const [extractedPrefix, extractedSuffix] = s!.split(detectedSymbol)
|
||||||
const regexPrefix: string = actAs === RegexpUsedAs.Prefix || actAs === RegexpUsedAs.FullMatch ? '^' : ''
|
const regexPrefix: string = actAs === RegexpUsedAs.Prefix || actAs === RegexpUsedAs.FullMatch ? '^' : ''
|
||||||
const regexSuffix: string = actAs === RegexpUsedAs.Suffix || actAs === RegexpUsedAs.FullMatch ? '$' : ''
|
const regexSuffix: string = actAs === RegexpUsedAs.Suffix || actAs === RegexpUsedAs.FullMatch ? '$' : ''
|
||||||
return {
|
return {
|
||||||
|
@ -356,11 +360,11 @@ const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly a
|
||||||
|
|
||||||
export class SortingSpecProcessor {
|
export class SortingSpecProcessor {
|
||||||
ctx: ProcessingContext
|
ctx: ProcessingContext
|
||||||
currentEntryLine: string
|
currentEntryLine: string | null
|
||||||
currentEntryLineIdx: number
|
currentEntryLineIdx: number | null
|
||||||
currentSortingSpecContainerFilePath: string
|
currentSortingSpecContainerFilePath: string | null
|
||||||
problemAlreadyReportedForCurrentLine: boolean
|
problemAlreadyReportedForCurrentLine: boolean | null
|
||||||
recentErrorMessage: string
|
recentErrorMessage: string | null
|
||||||
|
|
||||||
// Helper map to deal with rule priorities for the same path
|
// Helper map to deal with rule priorities for the same path
|
||||||
// and also detect non-wildcard duplicates.
|
// and also detect non-wildcard duplicates.
|
||||||
|
@ -376,8 +380,8 @@ export class SortingSpecProcessor {
|
||||||
parseSortSpecFromText(text: Array<string>,
|
parseSortSpecFromText(text: Array<string>,
|
||||||
folderPath: string,
|
folderPath: string,
|
||||||
sortingSpecFileName: string,
|
sortingSpecFileName: string,
|
||||||
collection?: SortSpecsCollection
|
collection?: SortSpecsCollection | null
|
||||||
): SortSpecsCollection {
|
): SortSpecsCollection | null | undefined {
|
||||||
// reset / init processing state after potential previous invocation
|
// reset / init processing state after potential previous invocation
|
||||||
this.ctx = {
|
this.ctx = {
|
||||||
folderPath: folderPath, // location of the sorting spec file
|
folderPath: folderPath, // location of the sorting spec file
|
||||||
|
@ -404,12 +408,12 @@ export class SortingSpecProcessor {
|
||||||
|
|
||||||
success = false // Empty lines and comments are OK, that's why setting so late
|
success = false // Empty lines and comments are OK, that's why setting so late
|
||||||
|
|
||||||
const attr: ParsedSortingAttribute = this._l1s1_parseAttribute(entryLine);
|
const attr: ParsedSortingAttribute | null = this._l1s1_parseAttribute(entryLine);
|
||||||
if (attr) {
|
if (attr) {
|
||||||
success = this._l1s2_processParsedSortingAttribute(attr);
|
success = this._l1s2_processParsedSortingAttribute(attr);
|
||||||
this.ctx.previousValidEntryWasTargetFolderAttr = success && (attr.attribute === Attribute.TargetFolder)
|
this.ctx.previousValidEntryWasTargetFolderAttr = success && (attr.attribute === Attribute.TargetFolder)
|
||||||
} else if (!this.problemAlreadyReportedForCurrentLine && !this._l1s3_checkForRiskyAttrSyntaxError(entryLine)) {
|
} else if (!this.problemAlreadyReportedForCurrentLine && !this._l1s3_checkForRiskyAttrSyntaxError(entryLine)) {
|
||||||
let group: ParsedSortingGroup = this._l1s4_parseSortingGroupSpec(entryLine);
|
let group: ParsedSortingGroup | null = this._l1s4_parseSortingGroupSpec(entryLine);
|
||||||
if (!this.problemAlreadyReportedForCurrentLine && !group) {
|
if (!this.problemAlreadyReportedForCurrentLine && !group) {
|
||||||
// Default for unrecognized syntax: treat the line as exact name (of file or folder)
|
// Default for unrecognized syntax: treat the line as exact name (of file or folder)
|
||||||
group = {plainSpec: trimmedEntryLine}
|
group = {plainSpec: trimmedEntryLine}
|
||||||
|
@ -433,7 +437,7 @@ export class SortingSpecProcessor {
|
||||||
this._l1s6_postprocessSortSpec(spec)
|
this._l1s6_postprocessSortSpec(spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec>
|
let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec> | undefined
|
||||||
for (let spec of this.ctx.specs) {
|
for (let spec of this.ctx.specs) {
|
||||||
// Consume the folder paths ending with wildcard specs
|
// Consume the folder paths ending with wildcard specs
|
||||||
for (let idx = 0; idx<spec.targetFoldersPaths.length; idx++) {
|
for (let idx = 0; idx<spec.targetFoldersPaths.length; idx++) {
|
||||||
|
@ -553,9 +557,14 @@ export class SortingSpecProcessor {
|
||||||
if (attr.attribute === Attribute.TargetFolder) {
|
if (attr.attribute === Attribute.TargetFolder) {
|
||||||
if (attr.nesting === 0) { // root-level attribute causing creation of new spec or decoration of a previous one
|
if (attr.nesting === 0) { // root-level attribute causing creation of new spec or decoration of a previous one
|
||||||
if (this.ctx.previousValidEntryWasTargetFolderAttr) {
|
if (this.ctx.previousValidEntryWasTargetFolderAttr) {
|
||||||
this.ctx.currentSpec.targetFoldersPaths.push(attr.value)
|
if (this.ctx.currentSpec) {
|
||||||
|
this.ctx.currentSpec.targetFoldersPaths.push(attr.value)
|
||||||
|
} else {
|
||||||
|
// Should never reach this execution path, yet for sanity and clarity:
|
||||||
|
this.ctx.currentSpec = this._l2s2_putNewSpecForNewTargetFolder(attr.value)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._l2s2_putNewSpecForNewTargetFolder(attr.value)
|
this.ctx.currentSpec = this._l2s2_putNewSpecForNewTargetFolder(attr.value)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
@ -565,7 +574,7 @@ export class SortingSpecProcessor {
|
||||||
} else if (attr.attribute === Attribute.OrderAsc || attr.attribute === Attribute.OrderDesc || attr.attribute === Attribute.OrderStandardObsidian) {
|
} 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.ctx.currentSpec = this._l2s2_putNewSpecForNewTargetFolder()
|
||||||
}
|
}
|
||||||
if (this.ctx.currentSpec.defaultOrder) {
|
if (this.ctx.currentSpec.defaultOrder) {
|
||||||
const folderPathsForProblemMsg: string = this.ctx.currentSpec.targetFoldersPaths.join(' :: ');
|
const folderPathsForProblemMsg: string = this.ctx.currentSpec.targetFoldersPaths.join(' :: ');
|
||||||
|
@ -663,7 +672,7 @@ export class SortingSpecProcessor {
|
||||||
|
|
||||||
private _l1s5_processParsedSortGroupSpec(group: ParsedSortingGroup): boolean {
|
private _l1s5_processParsedSortGroupSpec(group: ParsedSortingGroup): boolean {
|
||||||
if (!this.ctx.currentSpec) {
|
if (!this.ctx.currentSpec) {
|
||||||
this._l2s2_putNewSpecForNewTargetFolder()
|
this.ctx.currentSpec = this._l2s2_putNewSpecForNewTargetFolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.plainSpec) {
|
if (group.plainSpec) {
|
||||||
|
@ -679,14 +688,18 @@ export class SortingSpecProcessor {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else { // !group.itemToHide
|
} else { // !group.itemToHide
|
||||||
const newGroup: CustomSortGroup = this._l2s4_consumeParsedSortingGroupSpec(group)
|
const newGroup: CustomSortGroup | null = this._l2s4_consumeParsedSortingGroupSpec(group)
|
||||||
if (newGroup) {
|
if (newGroup) {
|
||||||
if (this._l2s5_adjustSortingGroupForNumericSortingSymbol(newGroup)) {
|
if (this._l2s5_adjustSortingGroupForNumericSortingSymbol(newGroup)) {
|
||||||
this.ctx.currentSpec.groups.push(newGroup)
|
if (this.ctx.currentSpec) {
|
||||||
this.ctx.currentSpecGroup = newGroup
|
this.ctx.currentSpec.groups.push(newGroup)
|
||||||
return true;
|
this.ctx.currentSpecGroup = newGroup
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
@ -699,8 +712,8 @@ export class SortingSpecProcessor {
|
||||||
spec.outsidersGroupIdx = undefined
|
spec.outsidersGroupIdx = undefined
|
||||||
spec.outsidersFilesGroupIdx = undefined
|
spec.outsidersFilesGroupIdx = undefined
|
||||||
spec.outsidersFoldersGroupIdx = undefined
|
spec.outsidersFoldersGroupIdx = undefined
|
||||||
let outsidersGroupForFolders: boolean
|
let outsidersGroupForFolders: boolean | undefined
|
||||||
let outsidersGroupForFiles: boolean
|
let outsidersGroupForFiles: boolean | undefined
|
||||||
|
|
||||||
// process all defined sorting groups
|
// process all defined sorting groups
|
||||||
for (let groupIdx = 0; groupIdx < spec.groups.length; groupIdx++) {
|
for (let groupIdx = 0; groupIdx < spec.groups.length; groupIdx++) {
|
||||||
|
@ -778,7 +791,7 @@ export class SortingSpecProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _l2s1_validateOrderAscAttrValue = (v: string): RecognizedOrderValue | null => {
|
private _l2s1_validateOrderAscAttrValue = (v: string): RecognizedOrderValue | null => {
|
||||||
const recognized: CustomSortOrderAscDescPair = this._l2s1_internal_validateOrderAttrValue(v)
|
const recognized: CustomSortOrderAscDescPair | null = this._l2s1_internal_validateOrderAttrValue(v)
|
||||||
return recognized ? {
|
return recognized ? {
|
||||||
order: recognized.asc,
|
order: recognized.asc,
|
||||||
secondaryOrder: recognized.secondary
|
secondaryOrder: recognized.secondary
|
||||||
|
@ -786,7 +799,7 @@ export class SortingSpecProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _l2s1_validateOrderDescAttrValue = (v: string): RecognizedOrderValue | null => {
|
private _l2s1_validateOrderDescAttrValue = (v: string): RecognizedOrderValue | null => {
|
||||||
const recognized: CustomSortOrderAscDescPair = this._l2s1_internal_validateOrderAttrValue(v)
|
const recognized: CustomSortOrderAscDescPair | null = this._l2s1_internal_validateOrderAttrValue(v)
|
||||||
return recognized ? {
|
return recognized ? {
|
||||||
order: recognized.desc,
|
order: recognized.desc,
|
||||||
secondaryOrder: recognized.secondary
|
secondaryOrder: recognized.secondary
|
||||||
|
@ -833,30 +846,34 @@ export class SortingSpecProcessor {
|
||||||
return [spec];
|
return [spec];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _l2s2_putNewSpecForNewTargetFolder(folderPath?: string) {
|
private _l2s2_putNewSpecForNewTargetFolder(folderPath?: string): CustomSortSpec {
|
||||||
const newSpec: CustomSortSpec = {
|
const newSpec: CustomSortSpec = {
|
||||||
targetFoldersPaths: [folderPath ?? this.ctx.folderPath],
|
targetFoldersPaths: [folderPath ?? this.ctx.folderPath],
|
||||||
groups: []
|
groups: []
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ctx.specs.push(newSpec);
|
this.ctx.specs.push(newSpec);
|
||||||
this.ctx.currentSpec = newSpec;
|
this.ctx.currentSpec = undefined;
|
||||||
this.ctx.currentSpecGroup = undefined;
|
this.ctx.currentSpecGroup = undefined;
|
||||||
|
|
||||||
|
return newSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detection of slippery syntax errors which can confuse user due to false positive parsing with an unexpected sorting result
|
// Detection of slippery syntax errors which can confuse user due to false positive parsing with an unexpected sorting result
|
||||||
|
|
||||||
private _l2s3_consumeParsedItemToHide(spec: ParsedSortingGroup): boolean {
|
private _l2s3_consumeParsedItemToHide(spec: ParsedSortingGroup): boolean {
|
||||||
if (spec.arraySpec.length === 1) {
|
if (spec.arraySpec?.length === 1) {
|
||||||
const theOnly: string = spec.arraySpec[0]
|
const theOnly: string = spec.arraySpec[0]
|
||||||
if (!isThreeDots(theOnly)) {
|
if (!isThreeDots(theOnly)) {
|
||||||
const nameWithExt: string = theOnly.trim()
|
const nameWithExt: string = theOnly.trim()
|
||||||
if (nameWithExt) { // Sanity check
|
if (nameWithExt) { // Sanity check
|
||||||
if (!detectNumericSortingSymbols(nameWithExt)) {
|
if (!detectNumericSortingSymbols(nameWithExt)) {
|
||||||
const itemsToHide: Set<string> = this.ctx.currentSpec.itemsToHide ?? new Set<string>()
|
if (this.ctx.currentSpec) {
|
||||||
itemsToHide.add(nameWithExt)
|
const itemsToHide: Set<string> = this.ctx.currentSpec?.itemsToHide ?? new Set<string>()
|
||||||
this.ctx.currentSpec.itemsToHide = itemsToHide
|
itemsToHide.add(nameWithExt)
|
||||||
return true
|
this.ctx.currentSpec.itemsToHide = itemsToHide
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -864,7 +881,7 @@ export class SortingSpecProcessor {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private _l2s4_consumeParsedSortingGroupSpec = (spec: ParsedSortingGroup): CustomSortGroup => {
|
private _l2s4_consumeParsedSortingGroupSpec = (spec: ParsedSortingGroup): CustomSortGroup | null => {
|
||||||
if (spec.outsidersGroup) {
|
if (spec.outsidersGroup) {
|
||||||
return {
|
return {
|
||||||
type: CustomSortGroupType.Outsiders,
|
type: CustomSortGroupType.Outsiders,
|
||||||
|
@ -874,7 +891,7 @@ export class SortingSpecProcessor {
|
||||||
} // theoretically could match the sorting of matched files
|
} // theoretically could match the sorting of matched files
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spec.arraySpec.length === 1) {
|
if (spec.arraySpec?.length === 1) {
|
||||||
const theOnly: string = spec.arraySpec[0]
|
const theOnly: string = spec.arraySpec[0]
|
||||||
if (isThreeDots(theOnly)) {
|
if (isThreeDots(theOnly)) {
|
||||||
return {
|
return {
|
||||||
|
@ -894,7 +911,7 @@ export class SortingSpecProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (spec.arraySpec.length === 2) {
|
if (spec.arraySpec?.length === 2) {
|
||||||
const theFirst: string = spec.arraySpec[0]
|
const theFirst: string = spec.arraySpec[0]
|
||||||
const theSecond: string = spec.arraySpec[1]
|
const theSecond: string = spec.arraySpec[1]
|
||||||
if (isThreeDots(theFirst) && !isThreeDots(theSecond) && !containsThreeDots(theSecond)) {
|
if (isThreeDots(theFirst) && !isThreeDots(theSecond) && !containsThreeDots(theSecond)) {
|
||||||
|
@ -919,7 +936,7 @@ export class SortingSpecProcessor {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (spec.arraySpec.length === 3) {
|
if (spec.arraySpec?.length === 3) {
|
||||||
const theFirst: string = spec.arraySpec[0]
|
const theFirst: string = spec.arraySpec[0]
|
||||||
const theMiddle: string = spec.arraySpec[1]
|
const theMiddle: string = spec.arraySpec[1]
|
||||||
const theLast: string = spec.arraySpec[2]
|
const theLast: string = spec.arraySpec[2]
|
||||||
|
|
68
src/main.ts
68
src/main.ts
|
@ -42,19 +42,22 @@ const SORTINGSPEC_YAML_KEY: string = 'sorting-spec'
|
||||||
|
|
||||||
const ERROR_NOTICE_TIMEOUT: number = 10000
|
const ERROR_NOTICE_TIMEOUT: number = 10000
|
||||||
|
|
||||||
|
// the monkey-around package doesn't export the below type
|
||||||
|
type MonkeyAroundUninstaller = () => void
|
||||||
|
|
||||||
export default class CustomSortPlugin extends Plugin {
|
export default class CustomSortPlugin extends Plugin {
|
||||||
settings: CustomSortPluginSettings
|
settings: CustomSortPluginSettings
|
||||||
statusBarItemEl: HTMLElement
|
statusBarItemEl: HTMLElement
|
||||||
ribbonIconEl: HTMLElement
|
ribbonIconEl: HTMLElement
|
||||||
|
|
||||||
sortSpecCache: SortSpecsCollection
|
sortSpecCache?: SortSpecsCollection | null
|
||||||
initialAutoOrManualSortingTriggered: boolean
|
initialAutoOrManualSortingTriggered: boolean
|
||||||
|
|
||||||
readAndParseSortingSpec() {
|
readAndParseSortingSpec() {
|
||||||
const mCache: MetadataCache = this.app.metadataCache
|
const mCache: MetadataCache = this.app.metadataCache
|
||||||
let failed: boolean = false
|
let failed: boolean = false
|
||||||
let anySortingSpecFound: boolean = false
|
let anySortingSpecFound: boolean = false
|
||||||
let errorMessage: string
|
let errorMessage: string | null = null
|
||||||
// reset cache
|
// reset cache
|
||||||
this.sortSpecCache = null
|
this.sortSpecCache = null
|
||||||
const processor: SortingSpecProcessor = new SortingSpecProcessor()
|
const processor: SortingSpecProcessor = new SortingSpecProcessor()
|
||||||
|
@ -68,7 +71,7 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
// - files with designated name (sortspec.md by default)
|
// - files with designated name (sortspec.md by default)
|
||||||
// - files with the same name as parent folders (aka folder notes): References/References.md
|
// - files with the same name as parent folders (aka folder notes): References/References.md
|
||||||
// - the file explicitly indicated in documentation, by default Inbox/Inbox.md
|
// - the file explicitly indicated in documentation, by default Inbox/Inbox.md
|
||||||
if (aFile.name === SORTSPEC_FILE_NAME || aFile.basename === parent.name || aFile.path === DEFAULT_SETTINGS.additionalSortspecFile) {
|
if (aFile.name === SORTSPEC_FILE_NAME || aFile.basename === parent.name || aFile.path === this.settings.additionalSortspecFile) {
|
||||||
const sortingSpecTxt: string = mCache.getCache(aFile.path)?.frontmatter?.[SORTINGSPEC_YAML_KEY]
|
const sortingSpecTxt: string = mCache.getCache(aFile.path)?.frontmatter?.[SORTINGSPEC_YAML_KEY]
|
||||||
if (sortingSpecTxt) {
|
if (sortingSpecTxt) {
|
||||||
anySortingSpecFound = true
|
anySortingSpecFound = true
|
||||||
|
@ -229,39 +232,38 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let tmpFolder = new TFolder(Vault, "");
|
let tmpFolder = new TFolder(Vault, "");
|
||||||
let Folder = fileExplorer.createFolderDom(tmpFolder).constructor;
|
let Folder = fileExplorer.createFolderDom(tmpFolder).constructor;
|
||||||
this.register(
|
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
|
||||||
// TODO: Unit tests please!!! The logic below becomes more and more complex, bugs are captured at run-time...
|
// TODO: Unit tests, the logic below becomes more and more complex, bugs are captured at run-time...
|
||||||
around(Folder.prototype, {
|
sort(old: any) {
|
||||||
sort(old: any) {
|
return function (...args: any[]) {
|
||||||
return function (...args: any[]) {
|
// quick check for plugin status
|
||||||
// quick check for plugin status
|
if (plugin.settings.suspended) {
|
||||||
if (plugin.settings.suspended) {
|
return old.call(this, ...args);
|
||||||
return old.call(this, ...args);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
let sortSpec: CustomSortSpec = plugin.sortSpecCache?.sortSpecByPath[folder.path]
|
let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath[folder.path]
|
||||||
if (sortSpec) {
|
if (sortSpec) {
|
||||||
if (sortSpec.defaultOrder === CustomSortOrder.standardObsidian) {
|
if (sortSpec.defaultOrder === CustomSortOrder.standardObsidian) {
|
||||||
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
|
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) {
|
} else if (plugin.sortSpecCache?.sortSpecByWildcard) {
|
||||||
return folderSort.call(this, sortSpec, ...args);
|
// when no sorting spec found directly by folder path, check for wildcard-based match
|
||||||
} else {
|
sortSpec = plugin.sortSpecCache?.sortSpecByWildcard.folderMatch(folder.path)
|
||||||
return old.call(this, ...args);
|
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);
|
||||||
);
|
} else {
|
||||||
|
return old.call(this, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.register(uninstallerOfFolderSortFunctionWrapper)
|
||||||
leaf.detach()
|
leaf.detach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import {TFolder, WorkspaceLeaf} from "obsidian";
|
import {TFolder, WorkspaceLeaf} from "obsidian";
|
||||||
|
|
||||||
|
// Needed to support monkey-patching of the folder sort() function
|
||||||
|
|
||||||
declare module 'obsidian' {
|
declare module 'obsidian' {
|
||||||
export interface ViewRegistry {
|
export interface ViewRegistry {
|
||||||
viewByType: Record<string, (leaf: WorkspaceLeaf) => unknown>;
|
viewByType: Record<string, (leaf: WorkspaceLeaf) => unknown>;
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"lib": [
|
"strictNullChecks": true,
|
||||||
|
"lib": [
|
||||||
"DOM",
|
"DOM",
|
||||||
"ES5",
|
"ES5",
|
||||||
"ES6",
|
"ES6",
|
||||||
|
|
Loading…
Reference in New Issue