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:
SebastianMC 2022-09-13 18:47:39 +02:00
parent 307f525f5a
commit c01d069829
9 changed files with 130 additions and 110 deletions

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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,16 +33,23 @@ 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 != undefined && itB.groupIdx != undefined) {
if (itA.groupIdx === itB.groupIdx) { if (itA.groupIdx === itB.groupIdx) {
const group: CustomSortGroup = sortSpec.groups[itA.groupIdx] const group: CustomSortGroup | undefined = sortSpec.groups[itA.groupIdx]
if (group.regexSpec && group.secondaryOrder && itA.matchGroup === itB.matchGroup) { if (group?.regexSpec && group.secondaryOrder && itA.matchGroup === itB.matchGroup) {
return Sorters[group.secondaryOrder](itA, itB) return Sorters[group.secondaryOrder ?? CustomSortOrder.default](itA, itB)
} else { } else {
return Sorters[group.order](itA, itB) return Sorters[group?.order ?? CustomSortOrder.default](itA, itB)
} }
} else { } else {
return itA.groupIdx - itB.groupIdx; return itA.groupIdx - itB.groupIdx;
} }
} else {
// 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)
}
} }
const isFolder = (entry: TFile | TFolder) => { const isFolder = (entry: TFile | TFolder) => {
@ -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)

View File

@ -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/...')

View File

@ -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

View File

@ -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()
} }

View File

@ -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)
}) })
}) })

View File

@ -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 => {
if (s) {
numericSortingSymbolsRegex.lastIndex = 0 numericSortingSymbolsRegex.lastIndex = 0
const matches: RegExpMatchArray = numericSortingSymbolsRegex.exec(s) const matches: RegExpMatchArray | null = numericSortingSymbolsRegex.exec(s)
return matches ? matches[0] : null 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) {
if (this.ctx.currentSpec) {
this.ctx.currentSpec.targetFoldersPaths.push(attr.value) this.ctx.currentSpec.targetFoldersPaths.push(attr.value)
} else { } else {
this._l2s2_putNewSpecForNewTargetFolder(attr.value) // Should never reach this execution path, yet for sanity and clarity:
this.ctx.currentSpec = this._l2s2_putNewSpecForNewTargetFolder(attr.value)
}
} else {
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)) {
if (this.ctx.currentSpec) {
this.ctx.currentSpec.groups.push(newGroup) this.ctx.currentSpec.groups.push(newGroup)
this.ctx.currentSpecGroup = newGroup this.ctx.currentSpecGroup = newGroup
return true; return true;
} else { } else {
return false; return false
}
} else {
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,27 +846,30 @@ 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) {
const itemsToHide: Set<string> = this.ctx.currentSpec?.itemsToHide ?? new Set<string>()
itemsToHide.add(nameWithExt) itemsToHide.add(nameWithExt)
this.ctx.currentSpec.itemsToHide = itemsToHide this.ctx.currentSpec.itemsToHide = itemsToHide
return true return true
@ -861,10 +877,11 @@ 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]

View File

@ -47,14 +47,14 @@ export default class CustomSortPlugin extends Plugin {
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()
@ -241,7 +241,7 @@ export default class CustomSortPlugin extends Plugin {
// 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