Merge from upstream git@github.com:obsidianmd/obsidian-sample-plugin.git
- because of strict null check adjusted the code where necessary
This commit is contained in:
parent
307f525f5a
commit
c01d069829
|
@ -14,7 +14,8 @@ export enum CustomSortOrder {
|
|||
byModifiedTimeReverse,
|
||||
byCreatedTime,
|
||||
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 {
|
||||
|
@ -22,7 +23,7 @@ export interface RecognizedOrderValue {
|
|||
secondaryOrder?: CustomSortOrder
|
||||
}
|
||||
|
||||
export type NormalizerFn = (s: string) => string
|
||||
export type NormalizerFn = (s: string) => string | null
|
||||
|
||||
export interface RegExpSpec {
|
||||
regex: RegExp
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {TFile} from 'obsidian';
|
||||
import {TFile, TFolder, Vault} from 'obsidian';
|
||||
import {determineSortingGroup} from './custom-sort';
|
||||
import {CustomSortGroupType, CustomSortSpec} from './custom-sort-types';
|
||||
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
|
||||
|
@ -12,10 +12,10 @@ const mockTFile = (basename: string, ext: string, size?: number, ctime?: number,
|
|||
},
|
||||
basename: basename,
|
||||
extension: ext,
|
||||
vault: null,
|
||||
vault: {} as Vault, // To satisfy TS typechecking
|
||||
path: `Some parent folder/${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 {
|
||||
CustomSortGroup,
|
||||
CustomSortGroupType,
|
||||
CustomSortOrder,
|
||||
CustomSortSpec
|
||||
} from "./custom-sort-types";
|
||||
import {requireApiVersion, TFile, TFolder} from 'obsidian';
|
||||
import {CustomSortGroup, CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
||||
import {isDefined} from "../utils/utils";
|
||||
|
||||
let Collator = new Intl.Collator(undefined, {
|
||||
|
@ -15,7 +10,7 @@ let Collator = new Intl.Collator(undefined, {
|
|||
|
||||
interface FolderItemForSorting {
|
||||
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
|
||||
matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match'
|
||||
ctime: number
|
||||
|
@ -38,15 +33,22 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
|||
};
|
||||
|
||||
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
|
||||
if (itA.groupIdx === itB.groupIdx) {
|
||||
const group: CustomSortGroup = sortSpec.groups[itA.groupIdx]
|
||||
if (group.regexSpec && group.secondaryOrder && itA.matchGroup === itB.matchGroup) {
|
||||
return Sorters[group.secondaryOrder](itA, itB)
|
||||
if (itA.groupIdx != undefined && itB.groupIdx != undefined) {
|
||||
if (itA.groupIdx === itB.groupIdx) {
|
||||
const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx]
|
||||
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 {
|
||||
return Sorters[group.order](itA, itB)
|
||||
return itA.groupIdx - itB.groupIdx;
|
||||
}
|
||||
} 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 {
|
||||
let groupIdx: number
|
||||
let determined: boolean = false
|
||||
let matchedGroup: string
|
||||
let matchedGroup: string | null | undefined
|
||||
const aFolder: boolean = isFolder(entry)
|
||||
const aFile: boolean = !aFolder
|
||||
const entryAsTFile: TFile = entry as TFile
|
||||
|
@ -77,10 +79,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
determined = true;
|
||||
}
|
||||
} else { // regexp is involved
|
||||
const match: RegExpMatchArray = group.regexSpec.regex.exec(nameForMatching);
|
||||
const match: RegExpMatchArray | null | undefined = group.regexSpec?.regex.exec(nameForMatching);
|
||||
if (match) {
|
||||
determined = true
|
||||
matchedGroup = group.regexSpec.normalizerFn(match[1]);
|
||||
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -90,10 +92,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
determined = true;
|
||||
}
|
||||
} else { // regexp is involved
|
||||
const match: RegExpMatchArray = group.regexSpec.regex.exec(nameForMatching);
|
||||
const match: RegExpMatchArray | null | undefined = group.regexSpec?.regex.exec(nameForMatching);
|
||||
if (match) {
|
||||
determined = true
|
||||
matchedGroup = group.regexSpec.normalizerFn(match[1]);
|
||||
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||
}
|
||||
}
|
||||
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
|
||||
if ((group.exactPrefix && nameForMatching.startsWith(group.exactPrefix)) ||
|
||||
(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) {
|
||||
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)
|
||||
if ((fullMatch.length + (group.exactPrefix?.length ?? 0) + (group.exactSuffix?.length ?? 0)) <= nameForMatching.length) {
|
||||
determined = true
|
||||
|
@ -127,10 +129,10 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
determined = true;
|
||||
}
|
||||
} else { // regexp is involved
|
||||
const match: RegExpMatchArray = group.regexSpec.regex.exec(nameForMatching);
|
||||
const match: RegExpMatchArray | null | undefined = group.regexSpec?.regex.exec(nameForMatching);
|
||||
if (match) {
|
||||
determined = true
|
||||
matchedGroup = group.regexSpec.normalizerFn(match[1]);
|
||||
matchedGroup = group.regexSpec?.normalizerFn(match[1]);
|
||||
}
|
||||
}
|
||||
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
|
||||
let determinedGroupIdx = groupIdx;
|
||||
let determinedGroupIdx: number | undefined = groupIdx;
|
||||
|
||||
if (!determined) {
|
||||
// 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 ?
|
||||
this.file.children.filter((entry: TFile | TFolder) => {
|
||||
return !sortingSpec.itemsToHide.has(entry.name)
|
||||
return !sortingSpec.itemsToHide!.has(entry.name)
|
||||
})
|
||||
:
|
||||
this.file.children)
|
||||
|
|
|
@ -58,18 +58,18 @@ describe('folderMatch', () => {
|
|||
['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)
|
||||
const match: SortingSpec | null = matcher.folderMatch(path)
|
||||
const matchFromCache: SortingSpec | null = 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')
|
||||
const match1: SortingSpec | null = matcher.folderMatch('/')
|
||||
const match2: SortingSpec | null = matcher.folderMatch('/Reviews')
|
||||
const match3: SortingSpec | null = matcher.folderMatch('/Reviews/daily/')
|
||||
const match4: SortingSpec | null = matcher.folderMatch('/Reviews/daily/Mon')
|
||||
const match5: SortingSpec | null = matcher.folderMatch('/Reviews/daily/Mon')
|
||||
expect(match1).toBeNull()
|
||||
expect(match2).toBeNull()
|
||||
expect(match3).toBe('/Reviews/daily/*')
|
||||
|
@ -78,27 +78,27 @@ describe('folderMatch', () => {
|
|||
})
|
||||
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/')
|
||||
const match1: SortingSpec | null = matcher.folderMatch('/')
|
||||
const match2: SortingSpec | null = matcher.folderMatch('/Reviews')
|
||||
const match3: SortingSpec | null = 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/')
|
||||
const match1: SortingSpec | null = matcher.folderMatch('/')
|
||||
const match2: SortingSpec | null = matcher.folderMatch('/Reviews')
|
||||
const match3: SortingSpec | null = 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')
|
||||
const match1: SortingSpec | null = matcher.folderMatch('/')
|
||||
const match2: SortingSpec | null = matcher.folderMatch('/Reviews/daily/')
|
||||
const match3: SortingSpec | null = matcher.folderMatch('/Reviews/daily/1')
|
||||
expect(match1).toBeNull()
|
||||
expect(match2).toBe('/Reviews/daily/...')
|
||||
expect(match3).toBe('/Reviews/daily/...')
|
||||
|
|
|
@ -41,9 +41,9 @@ export class FolderWildcardMatching<SortingSpec> {
|
|||
// cache
|
||||
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 lastComponent: string = pathComponents.pop()
|
||||
const lastComponent: string | undefined = pathComponents.pop()
|
||||
if (lastComponent !== MATCH_ALL_PATH_TOKEN && lastComponent !== MATCH_CHILDREN_PATH_TOKEN) {
|
||||
return null
|
||||
}
|
||||
|
@ -82,8 +82,8 @@ export class FolderWildcardMatching<SortingSpec> {
|
|||
if (spec) {
|
||||
return spec.spec ?? null
|
||||
} else {
|
||||
let rule: SortingSpec = this.tree.matchChildren
|
||||
let inheritedRule: SortingSpec = this.tree.matchAll
|
||||
let rule: SortingSpec | null | undefined = this.tree.matchChildren
|
||||
let inheritedRule: SortingSpec | undefined = this.tree.matchAll
|
||||
const pathComponents: Array<string> = splitPath(folderPath)
|
||||
let parentNode: FolderMatchingTreeNode<SortingSpec> = this.tree
|
||||
let lastIdx: number = pathComponents.length - 1
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('Plain numbers regexp', () => {
|
|||
const match: RegExpMatchArray | null = s.match(NumberRegex)
|
||||
if (out) {
|
||||
expect(match).not.toBeNull()
|
||||
expect(match[1]).toBe(out)
|
||||
expect(match?.[1]).toBe(out)
|
||||
} else {
|
||||
expect(match).toBeNull()
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ describe('Plain compound numbers regexp (dot)', () => {
|
|||
const match: RegExpMatchArray | null = s.match(CompoundNumberDotRegex)
|
||||
if (out) {
|
||||
expect(match).not.toBeNull()
|
||||
expect(match[1]).toBe(out)
|
||||
expect(match?.[1]).toBe(out)
|
||||
} else {
|
||||
expect(match).toBeNull()
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ describe('Plain compound numbers regexp (dash)', () => {
|
|||
const match: RegExpMatchArray | null = s.match(CompoundNumberDashRegex)
|
||||
if (out) {
|
||||
expect(match).not.toBeNull()
|
||||
expect(match[1]).toBe(out)
|
||||
expect(match?.[1]).toBe(out)
|
||||
} else {
|
||||
expect(match).toBeNull()
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ describe('Plain Roman numbers regexp', () => {
|
|||
const match: RegExpMatchArray | null = s.match(RomanNumberRegex)
|
||||
if (out) {
|
||||
expect(match).not.toBeNull()
|
||||
expect(match[1]).toBe(out)
|
||||
expect(match?.[1]).toBe(out)
|
||||
} else {
|
||||
expect(match).toBeNull()
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ describe('Roman compound numbers regexp (dot)', () => {
|
|||
const match: RegExpMatchArray | null = s.match(CompoundRomanNumberDotRegex)
|
||||
if (out) {
|
||||
expect(match).not.toBeNull()
|
||||
expect(match[1]).toBe(out)
|
||||
expect(match?.[1]).toBe(out)
|
||||
} else {
|
||||
expect(match).toBeNull()
|
||||
}
|
||||
|
@ -180,7 +180,7 @@ describe('Roman compound numbers regexp (dash)', () => {
|
|||
const match: RegExpMatchArray | null = s.match(CompoundRomanNumberDashRegex)
|
||||
if (out) {
|
||||
expect(match).not.toBeNull()
|
||||
expect(match[1]).toBe(out)
|
||||
expect(match?.[1]).toBe(out)
|
||||
} else {
|
||||
expect(match).toBeNull()
|
||||
}
|
||||
|
|
|
@ -506,7 +506,7 @@ describe('SortingSpecProcessor', () => {
|
|||
const inputTxtArr: Array<string> = txtInputSimplistic1.split('\n')
|
||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||
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)', () => {
|
||||
const inputTxtArr: Array<string> = txtInputSimplistic2.split('\n')
|
||||
|
@ -837,31 +837,31 @@ describe('SortingSpecProcessor path wildcard priorities', () => {
|
|||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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
|
||||
])('should correctly extract from >%s< the numeric sorting symbol (%s)', (s: string, regex: RegExp) => {
|
||||
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
|
||||
})
|
||||
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', () => {
|
||||
const input = 'Part\\-D+:'
|
||||
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', () => {
|
||||
const input = 'Part\\-D+:'
|
||||
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', () => {
|
||||
const input = 'Part\\.D+:'
|
||||
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)
|
||||
}
|
||||
|
||||
export const extractNumericSortingSymbol = (s: string): string => {
|
||||
numericSortingSymbolsRegex.lastIndex = 0
|
||||
const matches: RegExpMatchArray = numericSortingSymbolsRegex.exec(s)
|
||||
return matches ? matches[0] : null
|
||||
export const extractNumericSortingSymbol = (s?: string): string | null => {
|
||||
if (s) {
|
||||
numericSortingSymbolsRegex.lastIndex = 0
|
||||
const matches: RegExpMatchArray | null = numericSortingSymbolsRegex.exec(s)
|
||||
return matches ? matches[0] : null
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export interface RegExpSpecStr {
|
||||
|
@ -271,11 +275,11 @@ export enum RegexpUsedAs {
|
|||
FullMatch
|
||||
}
|
||||
|
||||
export const convertPlainStringWithNumericSortingSymbolToRegex = (s: string, actAs: RegexpUsedAs): ExtractedNumericSortingSymbolInfo => {
|
||||
const detectedSymbol: string = extractNumericSortingSymbol(s)
|
||||
export const convertPlainStringWithNumericSortingSymbolToRegex = (s?: string, actAs?: RegexpUsedAs): ExtractedNumericSortingSymbolInfo | null => {
|
||||
const detectedSymbol: string | null = extractNumericSortingSymbol(s)
|
||||
if (detectedSymbol) {
|
||||
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 regexSuffix: string = actAs === RegexpUsedAs.Suffix || actAs === RegexpUsedAs.FullMatch ? '$' : ''
|
||||
return {
|
||||
|
@ -356,11 +360,11 @@ const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly a
|
|||
|
||||
export class SortingSpecProcessor {
|
||||
ctx: ProcessingContext
|
||||
currentEntryLine: string
|
||||
currentEntryLineIdx: number
|
||||
currentSortingSpecContainerFilePath: string
|
||||
problemAlreadyReportedForCurrentLine: boolean
|
||||
recentErrorMessage: string
|
||||
currentEntryLine: string | null
|
||||
currentEntryLineIdx: number | null
|
||||
currentSortingSpecContainerFilePath: string | null
|
||||
problemAlreadyReportedForCurrentLine: boolean | null
|
||||
recentErrorMessage: string | null
|
||||
|
||||
// Helper map to deal with rule priorities for the same path
|
||||
// and also detect non-wildcard duplicates.
|
||||
|
@ -376,8 +380,8 @@ export class SortingSpecProcessor {
|
|||
parseSortSpecFromText(text: Array<string>,
|
||||
folderPath: string,
|
||||
sortingSpecFileName: string,
|
||||
collection?: SortSpecsCollection
|
||||
): SortSpecsCollection {
|
||||
collection?: SortSpecsCollection | null
|
||||
): SortSpecsCollection | null | undefined {
|
||||
// reset / init processing state after potential previous invocation
|
||||
this.ctx = {
|
||||
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
|
||||
|
||||
const attr: ParsedSortingAttribute = this._l1s1_parseAttribute(entryLine);
|
||||
const attr: ParsedSortingAttribute | null = this._l1s1_parseAttribute(entryLine);
|
||||
if (attr) {
|
||||
success = this._l1s2_processParsedSortingAttribute(attr);
|
||||
this.ctx.previousValidEntryWasTargetFolderAttr = success && (attr.attribute === Attribute.TargetFolder)
|
||||
} 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) {
|
||||
// Default for unrecognized syntax: treat the line as exact name (of file or folder)
|
||||
group = {plainSpec: trimmedEntryLine}
|
||||
|
@ -433,7 +437,7 @@ export class SortingSpecProcessor {
|
|||
this._l1s6_postprocessSortSpec(spec)
|
||||
}
|
||||
|
||||
let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec>
|
||||
let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec> | undefined
|
||||
for (let spec of this.ctx.specs) {
|
||||
// Consume the folder paths ending with wildcard specs
|
||||
for (let idx = 0; idx<spec.targetFoldersPaths.length; idx++) {
|
||||
|
@ -553,9 +557,14 @@ export class SortingSpecProcessor {
|
|||
if (attr.attribute === Attribute.TargetFolder) {
|
||||
if (attr.nesting === 0) { // root-level attribute causing creation of new spec or decoration of a previous one
|
||||
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 {
|
||||
this._l2s2_putNewSpecForNewTargetFolder(attr.value)
|
||||
this.ctx.currentSpec = this._l2s2_putNewSpecForNewTargetFolder(attr.value)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
|
@ -565,7 +574,7 @@ export class SortingSpecProcessor {
|
|||
} else if (attr.attribute === Attribute.OrderAsc || attr.attribute === Attribute.OrderDesc || attr.attribute === Attribute.OrderStandardObsidian) {
|
||||
if (attr.nesting === 0) {
|
||||
if (!this.ctx.currentSpec) {
|
||||
this._l2s2_putNewSpecForNewTargetFolder()
|
||||
this.ctx.currentSpec = this._l2s2_putNewSpecForNewTargetFolder()
|
||||
}
|
||||
if (this.ctx.currentSpec.defaultOrder) {
|
||||
const folderPathsForProblemMsg: string = this.ctx.currentSpec.targetFoldersPaths.join(' :: ');
|
||||
|
@ -663,7 +672,7 @@ export class SortingSpecProcessor {
|
|||
|
||||
private _l1s5_processParsedSortGroupSpec(group: ParsedSortingGroup): boolean {
|
||||
if (!this.ctx.currentSpec) {
|
||||
this._l2s2_putNewSpecForNewTargetFolder()
|
||||
this.ctx.currentSpec = this._l2s2_putNewSpecForNewTargetFolder()
|
||||
}
|
||||
|
||||
if (group.plainSpec) {
|
||||
|
@ -679,14 +688,18 @@ export class SortingSpecProcessor {
|
|||
return true
|
||||
}
|
||||
} else { // !group.itemToHide
|
||||
const newGroup: CustomSortGroup = this._l2s4_consumeParsedSortingGroupSpec(group)
|
||||
const newGroup: CustomSortGroup | null = this._l2s4_consumeParsedSortingGroupSpec(group)
|
||||
if (newGroup) {
|
||||
if (this._l2s5_adjustSortingGroupForNumericSortingSymbol(newGroup)) {
|
||||
this.ctx.currentSpec.groups.push(newGroup)
|
||||
this.ctx.currentSpecGroup = newGroup
|
||||
return true;
|
||||
if (this.ctx.currentSpec) {
|
||||
this.ctx.currentSpec.groups.push(newGroup)
|
||||
this.ctx.currentSpecGroup = newGroup
|
||||
return true;
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
|
@ -699,8 +712,8 @@ export class SortingSpecProcessor {
|
|||
spec.outsidersGroupIdx = undefined
|
||||
spec.outsidersFilesGroupIdx = undefined
|
||||
spec.outsidersFoldersGroupIdx = undefined
|
||||
let outsidersGroupForFolders: boolean
|
||||
let outsidersGroupForFiles: boolean
|
||||
let outsidersGroupForFolders: boolean | undefined
|
||||
let outsidersGroupForFiles: boolean | undefined
|
||||
|
||||
// process all defined sorting groups
|
||||
for (let groupIdx = 0; groupIdx < spec.groups.length; groupIdx++) {
|
||||
|
@ -778,7 +791,7 @@ export class SortingSpecProcessor {
|
|||
}
|
||||
|
||||
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 ? {
|
||||
order: recognized.asc,
|
||||
secondaryOrder: recognized.secondary
|
||||
|
@ -786,7 +799,7 @@ export class SortingSpecProcessor {
|
|||
}
|
||||
|
||||
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 ? {
|
||||
order: recognized.desc,
|
||||
secondaryOrder: recognized.secondary
|
||||
|
@ -833,30 +846,34 @@ export class SortingSpecProcessor {
|
|||
return [spec];
|
||||
}
|
||||
|
||||
private _l2s2_putNewSpecForNewTargetFolder(folderPath?: string) {
|
||||
private _l2s2_putNewSpecForNewTargetFolder(folderPath?: string): CustomSortSpec {
|
||||
const newSpec: CustomSortSpec = {
|
||||
targetFoldersPaths: [folderPath ?? this.ctx.folderPath],
|
||||
groups: []
|
||||
}
|
||||
|
||||
this.ctx.specs.push(newSpec);
|
||||
this.ctx.currentSpec = newSpec;
|
||||
this.ctx.currentSpec = 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
|
||||
|
||||
private _l2s3_consumeParsedItemToHide(spec: ParsedSortingGroup): boolean {
|
||||
if (spec.arraySpec.length === 1) {
|
||||
if (spec.arraySpec?.length === 1) {
|
||||
const theOnly: string = spec.arraySpec[0]
|
||||
if (!isThreeDots(theOnly)) {
|
||||
const nameWithExt: string = theOnly.trim()
|
||||
if (nameWithExt) { // Sanity check
|
||||
if (!detectNumericSortingSymbols(nameWithExt)) {
|
||||
const itemsToHide: Set<string> = this.ctx.currentSpec.itemsToHide ?? new Set<string>()
|
||||
itemsToHide.add(nameWithExt)
|
||||
this.ctx.currentSpec.itemsToHide = itemsToHide
|
||||
return true
|
||||
if (this.ctx.currentSpec) {
|
||||
const itemsToHide: Set<string> = this.ctx.currentSpec?.itemsToHide ?? new Set<string>()
|
||||
itemsToHide.add(nameWithExt)
|
||||
this.ctx.currentSpec.itemsToHide = itemsToHide
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -864,7 +881,7 @@ export class SortingSpecProcessor {
|
|||
return false
|
||||
}
|
||||
|
||||
private _l2s4_consumeParsedSortingGroupSpec = (spec: ParsedSortingGroup): CustomSortGroup => {
|
||||
private _l2s4_consumeParsedSortingGroupSpec = (spec: ParsedSortingGroup): CustomSortGroup | null => {
|
||||
if (spec.outsidersGroup) {
|
||||
return {
|
||||
type: CustomSortGroupType.Outsiders,
|
||||
|
@ -874,7 +891,7 @@ export class SortingSpecProcessor {
|
|||
} // theoretically could match the sorting of matched files
|
||||
}
|
||||
|
||||
if (spec.arraySpec.length === 1) {
|
||||
if (spec.arraySpec?.length === 1) {
|
||||
const theOnly: string = spec.arraySpec[0]
|
||||
if (isThreeDots(theOnly)) {
|
||||
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 theSecond: string = spec.arraySpec[1]
|
||||
if (isThreeDots(theFirst) && !isThreeDots(theSecond) && !containsThreeDots(theSecond)) {
|
||||
|
@ -919,7 +936,7 @@ export class SortingSpecProcessor {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
if (spec.arraySpec.length === 3) {
|
||||
if (spec.arraySpec?.length === 3) {
|
||||
const theFirst: string = spec.arraySpec[0]
|
||||
const theMiddle: string = spec.arraySpec[1]
|
||||
const theLast: string = spec.arraySpec[2]
|
||||
|
|
|
@ -47,14 +47,14 @@ export default class CustomSortPlugin extends Plugin {
|
|||
statusBarItemEl: HTMLElement
|
||||
ribbonIconEl: HTMLElement
|
||||
|
||||
sortSpecCache: SortSpecsCollection
|
||||
sortSpecCache?: SortSpecsCollection | null
|
||||
initialAutoOrManualSortingTriggered: boolean
|
||||
|
||||
readAndParseSortingSpec() {
|
||||
const mCache: MetadataCache = this.app.metadataCache
|
||||
let failed: boolean = false
|
||||
let anySortingSpecFound: boolean = false
|
||||
let errorMessage: string
|
||||
let errorMessage: string | null = null
|
||||
// reset cache
|
||||
this.sortSpecCache = null
|
||||
const processor: SortingSpecProcessor = new SortingSpecProcessor()
|
||||
|
@ -241,7 +241,7 @@ export default class CustomSortPlugin extends Plugin {
|
|||
|
||||
// if custom sort is not specified, use the UI-selected
|
||||
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.defaultOrder === CustomSortOrder.standardObsidian) {
|
||||
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
|
||||
|
|
Loading…
Reference in New Issue