diff --git a/esbuild.config.mjs b/esbuild.config.mjs index fca6656..eac3f92 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -21,30 +21,20 @@ esbuild.build({ 'obsidian', 'electron', '@codemirror/autocomplete', - '@codemirror/closebrackets', '@codemirror/collab', '@codemirror/commands', - '@codemirror/comment', - '@codemirror/fold', - '@codemirror/gutter', - '@codemirror/highlight', - '@codemirror/history', '@codemirror/language', '@codemirror/lint', - '@codemirror/matchbrackets', - '@codemirror/panel', - '@codemirror/rangeset', - '@codemirror/rectangular-selection', '@codemirror/search', '@codemirror/state', - '@codemirror/stream-parser', - '@codemirror/text', - '@codemirror/tooltip', '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', ...builtins], format: 'cjs', watch: !prod, - target: 'es2016', + target: 'es2018', logLevel: "info", sourcemap: prod ? false : 'inline', minify: prod, diff --git a/package.json b/package.json index 80f762b..5b6b952 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,15 @@ "devDependencies": { "@types/jest": "^28.1.2", "@types/node": "^16.11.6", - "@typescript-eslint/eslint-plugin": "^5.2.0", - "@typescript-eslint/parser": "^5.2.0", - "builtin-modules": "^3.2.0", - "esbuild": "0.13.12", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.14.47", "jest": "^28.1.1", "monkey-around": "^2.3.0", "obsidian": "^0.15.4", "ts-jest": "^28.0.5", - "tslib": "2.3.1", - "typescript": "4.4.4" + "tslib": "2.4.0", + "typescript": "4.7.4" } } diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 5866ca7..ed79c19 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -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 diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index 2d53d60..e70c65f 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -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 } } diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 0791e68..460de86 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -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 = (sortingSpec.itemsToHide ? this.file.children.filter((entry: TFile | TFolder) => { - return !sortingSpec.itemsToHide.has(entry.name) + return !sortingSpec.itemsToHide!.has(entry.name) }) : this.file.children) diff --git a/src/custom-sort/folder-matching-rules.spec.ts b/src/custom-sort/folder-matching-rules.spec.ts index 836a56b..909c969 100644 --- a/src/custom-sort/folder-matching-rules.spec.ts +++ b/src/custom-sort/folder-matching-rules.spec.ts @@ -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 = 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 = 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 = 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 = 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 = 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/...') diff --git a/src/custom-sort/folder-matching-rules.ts b/src/custom-sort/folder-matching-rules.ts index 722edce..62d3d75 100644 --- a/src/custom-sort/folder-matching-rules.ts +++ b/src/custom-sort/folder-matching-rules.ts @@ -41,9 +41,9 @@ export class FolderWildcardMatching { // cache determinedWildcardRules: { [key: string]: DeterminedSortingSpec } = {} - addWildcardDefinition = (wilcardDefinition: string, rule: SortingSpec): AddingWildcardFailure | null => { + addWildcardDefinition = (wilcardDefinition: string, rule: SortingSpec): AddingWildcardFailure | null | undefined => { const pathComponents: Array = 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 { 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 = splitPath(folderPath) let parentNode: FolderMatchingTreeNode = this.tree let lastIdx: number = pathComponents.length - 1 diff --git a/src/custom-sort/matchers.spec.ts b/src/custom-sort/matchers.spec.ts index 41cbdb9..a9fffae 100644 --- a/src/custom-sort/matchers.spec.ts +++ b/src/custom-sort/matchers.spec.ts @@ -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() } diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index f013118..d680158 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -506,7 +506,7 @@ describe('SortingSpecProcessor', () => { const inputTxtArr: Array = 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 = txtInputSimplistic2.split('\n') @@ -837,31 +837,31 @@ describe('SortingSpecProcessor path wildcard priorities', () => { const inputTxtArr: Array = 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 = 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 = 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 = 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 = 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) }) }) diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index d3490d9..3f0050d 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -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, 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 + let sortspecByWildcard: FolderWildcardMatching | undefined for (let spec of this.ctx.specs) { // Consume the folder paths ending with wildcard specs for (let idx = 0; idx { - 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 = this.ctx.currentSpec.itemsToHide ?? new Set() - itemsToHide.add(nameWithExt) - this.ctx.currentSpec.itemsToHide = itemsToHide - return true + if (this.ctx.currentSpec) { + const itemsToHide: Set = this.ctx.currentSpec?.itemsToHide ?? new Set() + 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] diff --git a/src/main.ts b/src/main.ts index 1c7fc9a..14d573b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,19 +42,22 @@ const SORTINGSPEC_YAML_KEY: string = 'sorting-spec' 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 { settings: CustomSortPluginSettings 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() @@ -68,7 +71,7 @@ export default class CustomSortPlugin extends Plugin { // - files with designated name (sortspec.md by default) // - 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 - 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] if (sortingSpecTxt) { anySortingSpecFound = true @@ -229,39 +232,38 @@ export default class CustomSortPlugin extends Plugin { // @ts-ignore let tmpFolder = new TFolder(Vault, ""); let Folder = fileExplorer.createFolderDom(tmpFolder).constructor; - this.register( - // TODO: Unit tests please!!! The logic below becomes more and more complex, bugs are captured at run-time... - around(Folder.prototype, { - sort(old: any) { - return function (...args: any[]) { - // quick check for plugin status - if (plugin.settings.suspended) { - return old.call(this, ...args); - } + const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, { + // TODO: Unit tests, the logic below becomes more and more complex, bugs are captured at run-time... + sort(old: 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 - const folder: TFolder = this.file - let sortSpec: CustomSortSpec = plugin.sortSpecCache?.sortSpecByPath[folder.path] - 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 custom sort is not specified, use the UI-selected + const folder: TFolder = this.file + 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 } - if (sortSpec) { - return folderSort.call(this, sortSpec, ...args); - } else { - return old.call(this, ...args); + } 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); + } else { + return old.call(this, ...args); + } + }; + } + }) + this.register(uninstallerOfFolderSortFunctionWrapper) leaf.detach() } diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 3fe852c..58e02fa 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -1,5 +1,7 @@ import {TFolder, WorkspaceLeaf} from "obsidian"; +// Needed to support monkey-patching of the folder sort() function + declare module 'obsidian' { export interface ViewRegistry { viewByType: Record unknown>; diff --git a/tsconfig.json b/tsconfig.json index ed4da99..1558e9e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "moduleResolution": "node", "importHelpers": true, "isolatedModules": true, - "lib": [ + "strictNullChecks": true, + "lib": [ "DOM", "ES5", "ES6",