Merge pull request #2 from SebastianMC/1-inheritance-of-sorting-rules-for-subfolders
Ticket #1: added support for imposed sorting rules inheritance by subfolders
This commit is contained in:
commit
1e30312e33
95
README.md
95
README.md
|
@ -17,11 +17,13 @@ Take full control of the order of your notes and folders:
|
||||||
- order configuration stored directly in your note(s) front matter
|
- order configuration stored directly in your note(s) front matter
|
||||||
- use a dedicated key in YAML
|
- use a dedicated key in YAML
|
||||||
- folders not set up for the custom order remain on the standard Obsidian sorting
|
- folders not set up for the custom order remain on the standard Obsidian sorting
|
||||||
|
- support for imposing inheritance of order specifications with flexible exclusion and overriding logic
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
- [TL;DR Usage](#tldr-usage)
|
- [TL;DR Usage](#tldr-usage)
|
||||||
- [Simple case 1: in root folder sort items by a rule, intermixing folders and files](#simple-case-1-in-root-folder-sort-items-by-a-rule-intermixing-folders-and-files)
|
-
|
||||||
|
- [Simple case 1: in root folder sort entries alphabetically treating folders and files equally](#simple-case-1-in-root-folder-sort-entries-alphabetically-treating-folders-and-files-equally)
|
||||||
- [Simple case 2: impose manual order of some items in root folder](#simple-case-2-impose-manual-order-of-some-items-in-root-folder)
|
- [Simple case 2: impose manual order of some items in root folder](#simple-case-2-impose-manual-order-of-some-items-in-root-folder)
|
||||||
- [Example 3: In root folder, let files go first and folders get pushed to the bottom](#example-3-in-root-folder-let-files-go-first-and-folders-get-pushed-to-the-bottom)
|
- [Example 3: In root folder, let files go first and folders get pushed to the bottom](#example-3-in-root-folder-let-files-go-first-and-folders-get-pushed-to-the-bottom)
|
||||||
- [Example 4: In root folder, pin a focus note, then Inbox folder, and push archive to the bottom](#example-4-in-root-folder-pin-a-focus-note-then-inbox-folder-and-push-archive-to-the-bottom)
|
- [Example 4: In root folder, pin a focus note, then Inbox folder, and push archive to the bottom](#example-4-in-root-folder-pin-a-focus-note-then-inbox-folder-and-push-archive-to-the-bottom)
|
||||||
|
@ -32,6 +34,8 @@ Take full control of the order of your notes and folders:
|
||||||
- [Example 9: Sort by numerical suffix](#example-9-sort-by-numerical-suffix)
|
- [Example 9: Sort by numerical suffix](#example-9-sort-by-numerical-suffix)
|
||||||
- [Example 10: Sample book structure with Roman numbered chapters](#example-10-sample-book-structure-with-roman-numbered-chapters)
|
- [Example 10: Sample book structure with Roman numbered chapters](#example-10-sample-book-structure-with-roman-numbered-chapters)
|
||||||
- [Example 11: Sample book structure with compound Roman number suffixes](#example-11-sample-book-structure-with-compound-roman-number-suffixes)
|
- [Example 11: Sample book structure with compound Roman number suffixes](#example-11-sample-book-structure-with-compound-roman-number-suffixes)
|
||||||
|
- [Example 12: Apply same sorting to all folders in the vault](#example-12-apply-same-sorting-to-all-folders-in-the-vault)
|
||||||
|
- [Example 13: Sorting rules inheritance by subfolders](#example-13-sorting-rules-inheritance-by-subfolders)
|
||||||
- [Location of sorting specification YAML entry](#location-of-sorting-specification-yaml-entry)
|
- [Location of sorting specification YAML entry](#location-of-sorting-specification-yaml-entry)
|
||||||
- [Ribbon icon](#ribbon-icon)
|
- [Ribbon icon](#ribbon-icon)
|
||||||
- [Installing the plugin](#installing-the-plugin)
|
- [Installing the plugin](#installing-the-plugin)
|
||||||
|
@ -44,6 +48,31 @@ Take full control of the order of your notes and folders:
|
||||||
For full version of the manual go to [./docs/manual.md]() and [./docs/syntax-reference.md]()
|
For full version of the manual go to [./docs/manual.md]() and [./docs/syntax-reference.md]()
|
||||||
> REMARK: as of this version of documentation, the manual and syntax reference are empty :-)
|
> REMARK: as of this version of documentation, the manual and syntax reference are empty :-)
|
||||||
|
|
||||||
|
> **Quickstart**
|
||||||
|
>
|
||||||
|
> 1. Download the [sortspec.md](./docs/examples/quickstart/sortspec.md?plain=1) file and put it in any folder of your vault,
|
||||||
|
can be the root folder. That file contains a basic custom sorting specification.
|
||||||
|
>
|
||||||
|
> 2. Enable the plugin in obsidian.
|
||||||
|
>
|
||||||
|
> 3. Click the ribbon button () to tell the plugin to read the sorting
|
||||||
|
specification from `sortspec` note (the `sortspec.md` file which you downloaded a second ago).
|
||||||
|
> - The observable effect should be the change of appearance of the ribbon button to
|
||||||
|
() and reordering
|
||||||
|
of items in root vault folder to reverse alphabetical with folders and files treated equally.
|
||||||
|
> - The notification balloon should confirm success: 
|
||||||
|
> 4. Click the ribbon button again to suspend the plugin. The ribbon button should toggle its appearance again
|
||||||
|
and the order of files and folders in the root folder of your vault should get back to the order selected in
|
||||||
|
Obsidian UI
|
||||||
|
> 5. Happy custom sorting !!! Remember to click the ribbon button twice each time after sorting specification
|
||||||
|
change. This will suspend and re-enable the custom sorting, plus parse and apply the updated specification
|
||||||
|
>
|
||||||
|
> - If you don't have any
|
||||||
|
subfolder in the root folder, create one to observe the plugin at work.
|
||||||
|
>
|
||||||
|
> NOTE: the appearances of ribbon button also includes 
|
||||||
|
and . For the meaning of them please refer to [ribbon icon](#ribbon_icon) section below
|
||||||
|
|
||||||
Below go examples of (some of) the key features, ready to copy & paste to your vault.
|
Below go examples of (some of) the key features, ready to copy & paste to your vault.
|
||||||
|
|
||||||
For simplicity (if you are examining the plugin for the first time) copy and paste the below YAML snippets to the front
|
For simplicity (if you are examining the plugin for the first time) copy and paste the below YAML snippets to the front
|
||||||
|
@ -58,9 +87,15 @@ Click the [ribbon icon](#ribbon_icon) again to disable custom sorting and switch
|
||||||
The [ribbon icon](#ribbon_icon) acts also as the visual indicator of the current state of the plugin - see
|
The [ribbon icon](#ribbon_icon) acts also as the visual indicator of the current state of the plugin - see
|
||||||
the [ribbon icon](#ribbon_icon) section for details
|
the [ribbon icon](#ribbon_icon) section for details
|
||||||
|
|
||||||
### Simple case 1: in root folder sort items by a rule, intermixing folders and files
|
### Simple case 1: in root folder sort entries alphabetically treating folders and files equally
|
||||||
|
|
||||||
The specified rule is to sort items alphabetically
|
The specified rule is to sort items alphabetically in the root folder of the vault
|
||||||
|
|
||||||
|
The line `target-folder: /` specifies to which folder apply the sorting rules which follow.
|
||||||
|
|
||||||
|
The `/` indicates the root folder of the vault in File Explorer
|
||||||
|
|
||||||
|
And `< a-z` sets the order to alphabetical ascending
|
||||||
|
|
||||||
> IMPORTANT: indentation matters in all the examples
|
> IMPORTANT: indentation matters in all the examples
|
||||||
|
|
||||||
|
@ -322,6 +357,60 @@ the result is:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### Example 12: Apply same sorting to all folders in the vault
|
||||||
|
|
||||||
|
Apply the alphabetical sorting to all folders in the Vault. The alphabetical sorting treats the folders and files equally
|
||||||
|
(which is different from the standard Obsidian sort, which groups folders in the top of File Explorer)
|
||||||
|
|
||||||
|
This involves the wildcard suffix syntax `*` which means _apply the sorting rule to the specified folder
|
||||||
|
and all of its subfolders, including descendants. In other words, this is imposing a deep inheritance
|
||||||
|
of sorting specification.
|
||||||
|
Applying the wildcard suffix to root folder path `/*` actually means _apply the sorting to all folders in the vault_
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
sorting-spec: |
|
||||||
|
target-folder: /*
|
||||||
|
< a-z
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 13: Sorting rules inheritance by subfolders
|
||||||
|
|
||||||
|
A more advanced example showing finetuned options of manipulating of sorting rules inheritance:
|
||||||
|
|
||||||
|
You can read the below YAML specification as:
|
||||||
|
- all items in all folders in the vault (`target-folder: /*`) should be sorted alphabetically (files and folders treated equally)
|
||||||
|
- yet, items in the `Reviews` folder and its direct subfolders (like `Reviews/daily`) should be ordered by modification date
|
||||||
|
- the syntax `Reviews/...` means: the items in `Reviews` folder and its direct subfolders (and no deeper)
|
||||||
|
- the more nested folder like `Reviews/daily/morning` inherit the rule specified for root folder `/*`
|
||||||
|
- Note, that a more specific (or more nested or more focused) rule overrides the more generic inherited one
|
||||||
|
- at the same time, the folder `Archive` and `Inbox` sort their items by creation date
|
||||||
|
- this is because specifying direct name in `target-folder: Archive` has always the highest priority and overrides any inheritance
|
||||||
|
- and finally, the folders `Reviews/Attachments` and `TODOs` are explicitly excluded from the control of the custom sort
|
||||||
|
plugin and use the standard Obsidian UI sorting, as selected in the UI
|
||||||
|
- the special syntax `sorting: standard` tells the plugin to refrain from ordering items in specified folders
|
||||||
|
- again, specifying the folder by name in `target-folder: TODOs` overrides any inherited sorting rules
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
sorting-spec: |
|
||||||
|
target-folder: /*
|
||||||
|
< a-z
|
||||||
|
|
||||||
|
target-folder: Reviews/...
|
||||||
|
< modified
|
||||||
|
|
||||||
|
target-folder: Archive
|
||||||
|
target-folder: Inbox
|
||||||
|
< created
|
||||||
|
|
||||||
|
target-folder: Reviews/Attachments
|
||||||
|
target-folder: TODOs
|
||||||
|
sorting: standard
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
## Location of sorting specification YAML entry
|
## Location of sorting specification YAML entry
|
||||||
|
|
||||||
You can keep the custom sorting specifications in any of the following locations (or in all of them):
|
You can keep the custom sorting specifications in any of the following locations (or in all of them):
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
sorting-spec: |
|
||||||
|
//
|
||||||
|
// A simple configuration for obsidian-custom-sort plugin
|
||||||
|
// (https://github.com/SebastianMC/obsidian-custom-sort)
|
||||||
|
// It causes the plugin to take over the control of the order of items in the root folder ('/') of the vault
|
||||||
|
// It explicitly sets the sorting to descending ('>') alphabetical ('a-z')
|
||||||
|
// Folders and files are treated equally by the plugin (by default) so expect them intermixed
|
||||||
|
// in the root vault folder after enabling the custom sort plugin
|
||||||
|
//
|
||||||
|
// To play with more examples go to https://github.com/SebastianMC/obsidian-custom-sort#readme
|
||||||
|
|
||||||
|
target-folder: /
|
||||||
|
< a-z
|
||||||
|
---
|
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "custom-sort",
|
"id": "custom-sort",
|
||||||
"name": "Custom File Explorer sorting",
|
"name": "Custom File Explorer sorting",
|
||||||
"version": "0.5.189",
|
"version": "0.6.0",
|
||||||
"minAppVersion": "0.12.0",
|
"minAppVersion": "0.12.0",
|
||||||
"description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer",
|
"description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer",
|
||||||
"author": "SebastianMC <SebastianMC.github@gmail.com>",
|
"author": "SebastianMC <SebastianMC.github@gmail.com>",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-custom-sort",
|
"name": "obsidian-custom-sort",
|
||||||
"version": "0.5.189",
|
"version": "0.6.0",
|
||||||
"description": "Custom Sort plugin for Obsidian (https://obsidian.md)",
|
"description": "Custom Sort plugin for Obsidian (https://obsidian.md)",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
export const SortSpecFileName: string = 'sortspec.md';
|
|
||||||
|
|
||||||
export enum CustomSortGroupType {
|
export enum CustomSortGroupType {
|
||||||
Outsiders, // Not belonging to any of other groups
|
Outsiders, // Not belonging to any of other groups
|
||||||
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
|
MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
|
||||||
|
@ -15,7 +13,8 @@ export enum CustomSortOrder {
|
||||||
byModifiedTime,
|
byModifiedTime,
|
||||||
byModifiedTimeReverse,
|
byModifiedTimeReverse,
|
||||||
byCreatedTime,
|
byCreatedTime,
|
||||||
byCreatedTimeReverse
|
byCreatedTimeReverse,
|
||||||
|
standardObsidian// Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecognizedOrderValue {
|
export interface RecognizedOrderValue {
|
||||||
|
@ -52,9 +51,3 @@ export interface CustomSortSpec {
|
||||||
outsidersFoldersGroupIdx?: number
|
outsidersFoldersGroupIdx?: number
|
||||||
itemsToHide?: Set<string>
|
itemsToHide?: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FolderPathToSortSpecMap {
|
|
||||||
[key: string]: CustomSortSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SortSpecsCollection = FolderPathToSortSpecMap
|
|
||||||
|
|
|
@ -31,7 +31,10 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
[CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime,
|
[CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime,
|
||||||
[CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime,
|
[CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime,
|
||||||
[CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime,
|
[CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime,
|
||||||
[CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime
|
[CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime,
|
||||||
|
|
||||||
|
// This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all
|
||||||
|
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString),
|
||||||
};
|
};
|
||||||
|
|
||||||
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
|
function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, sortSpec: CustomSortSpec) {
|
||||||
|
@ -53,7 +56,7 @@ const isFolder = (entry: TFile | TFolder) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec): FolderItemForSorting {
|
export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec): FolderItemForSorting {
|
||||||
let groupIdx: number = 0
|
let groupIdx: number
|
||||||
let determined: boolean = false
|
let determined: boolean = false
|
||||||
let matchedGroup: string
|
let matchedGroup: string
|
||||||
const aFolder: boolean = isFolder(entry)
|
const aFolder: boolean = isFolder(entry)
|
||||||
|
@ -168,7 +171,6 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
||||||
|
|
||||||
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
|
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
|
||||||
let fileExplorer = this.fileExplorer
|
let fileExplorer = this.fileExplorer
|
||||||
const thisFolderPath: string = this.file.path;
|
|
||||||
|
|
||||||
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
|
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
|
||||||
this.file.children.filter((entry: TFile | TFolder) => {
|
this.file.children.filter((entry: TFile | TFolder) => {
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import {FolderWildcardMatching} from './folder-matching-rules'
|
||||||
|
|
||||||
|
type SortingSpec = string
|
||||||
|
|
||||||
|
const createMockMatcherRichVersion = (): FolderWildcardMatching<SortingSpec> => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
|
||||||
|
let p: string
|
||||||
|
p = '/...'; matcher.addWildcardDefinition(p, `00 ${p}`)
|
||||||
|
p = '/*'; matcher.addWildcardDefinition(p, `0 ${p}`)
|
||||||
|
p = 'Reviews/...'; matcher.addWildcardDefinition(p, `1 ${p}`)
|
||||||
|
p = '/Reviews/*'; matcher.addWildcardDefinition(p, `2 ${p}`)
|
||||||
|
p = '/Reviews/daily/a/.../'; matcher.addWildcardDefinition(p, `3 ${p}`)
|
||||||
|
p = 'Reviews/daily/a/*'; matcher.addWildcardDefinition(p, `4 ${p}`)
|
||||||
|
return matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockMatcherSimplestVersion = (): FolderWildcardMatching<SortingSpec> => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
|
||||||
|
matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*')
|
||||||
|
return matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockMatcherRootOnlyVersion = (): FolderWildcardMatching<SortingSpec> => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
|
||||||
|
matcher.addWildcardDefinition('/...', '/...')
|
||||||
|
return matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockMatcherRootOnlyDeepVersion = (): FolderWildcardMatching<SortingSpec> => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
|
||||||
|
matcher.addWildcardDefinition('/*', '/*')
|
||||||
|
return matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMockMatcherSimpleVersion = (): FolderWildcardMatching<SortingSpec> => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
|
||||||
|
matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*')
|
||||||
|
matcher.addWildcardDefinition('/Reviews/daily/...', '/Reviews/daily/...')
|
||||||
|
return matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('folderMatch', () => {
|
||||||
|
it.each([
|
||||||
|
['/', '00 /...'],
|
||||||
|
['Archive/', '00 /...'],
|
||||||
|
['Archive', '00 /...'],
|
||||||
|
['/Archive/2019', '0 /*'],
|
||||||
|
['Archive/2019/', '0 /*'],
|
||||||
|
['Archive/2019/Jan', '0 /*'],
|
||||||
|
['/Reviews', '1 Reviews/...'],
|
||||||
|
['Reviews/weekly', '1 Reviews/...'],
|
||||||
|
['Reviews/weekly/w50/', '2 /Reviews/*'],
|
||||||
|
['/Reviews/daily', '2 /Reviews/*'],
|
||||||
|
['Reviews/daily/Mon', '2 /Reviews/*'],
|
||||||
|
['/Reviews/daily/a/', '3 /Reviews/daily/a/.../'],
|
||||||
|
['Reviews/daily/a/Mon', '3 /Reviews/daily/a/.../'],
|
||||||
|
['/Reviews/daily/a/Mon/Late', '4 Reviews/daily/a/*'],
|
||||||
|
['Reviews/daily/a/Tue/Early/9am', '4 Reviews/daily/a/*']
|
||||||
|
])('%s should match %s', (path: string, rule: string) => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRichVersion()
|
||||||
|
const match: SortingSpec = matcher.folderMatch(path)
|
||||||
|
const matchFromCache: SortingSpec = matcher.folderMatch(path)
|
||||||
|
expect(match).toBe(rule)
|
||||||
|
expect(matchFromCache).toBe(rule)
|
||||||
|
})
|
||||||
|
it('should correctly handle no-root definitions', () => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherSimplestVersion()
|
||||||
|
const match1: SortingSpec = matcher.folderMatch('/')
|
||||||
|
const match2: SortingSpec = matcher.folderMatch('/Reviews')
|
||||||
|
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
|
||||||
|
const match4: SortingSpec = matcher.folderMatch('/Reviews/daily/Mon')
|
||||||
|
const match5: SortingSpec = matcher.folderMatch('/Reviews/daily/Mon')
|
||||||
|
expect(match1).toBeNull()
|
||||||
|
expect(match2).toBeNull()
|
||||||
|
expect(match3).toBe('/Reviews/daily/*')
|
||||||
|
expect(match4).toBe('/Reviews/daily/*')
|
||||||
|
expect(match5).toBe('/Reviews/daily/*')
|
||||||
|
})
|
||||||
|
it('should correctly handle root-only definition', () => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRootOnlyVersion()
|
||||||
|
const match1: SortingSpec = matcher.folderMatch('/')
|
||||||
|
const match2: SortingSpec = matcher.folderMatch('/Reviews')
|
||||||
|
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
|
||||||
|
expect(match1).toBe('/...')
|
||||||
|
expect(match2).toBe('/...')
|
||||||
|
expect(match3).toBeNull()
|
||||||
|
})
|
||||||
|
it('should correctly handle root-only deep definition', () => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherRootOnlyDeepVersion()
|
||||||
|
const match1: SortingSpec = matcher.folderMatch('/')
|
||||||
|
const match2: SortingSpec = matcher.folderMatch('/Reviews')
|
||||||
|
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/')
|
||||||
|
expect(match1).toBe('/*')
|
||||||
|
expect(match2).toBe('/*')
|
||||||
|
expect(match3).toBe('/*')
|
||||||
|
})
|
||||||
|
it('should correctly handle match all and match children definitions for same path', () => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = createMockMatcherSimpleVersion()
|
||||||
|
const match1: SortingSpec = matcher.folderMatch('/')
|
||||||
|
const match2: SortingSpec = matcher.folderMatch('/Reviews/daily/')
|
||||||
|
const match3: SortingSpec = matcher.folderMatch('/Reviews/daily/1')
|
||||||
|
expect(match1).toBeNull()
|
||||||
|
expect(match2).toBe('/Reviews/daily/...')
|
||||||
|
expect(match3).toBe('/Reviews/daily/...')
|
||||||
|
})
|
||||||
|
it('should detect duplicate match children definitions for same path', () => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
|
||||||
|
matcher.addWildcardDefinition('Archive/2020/...', 'First occurrence')
|
||||||
|
const result = matcher.addWildcardDefinition('/Archive/2020/.../', 'Duplicate')
|
||||||
|
|
||||||
|
expect(result).toEqual({errorMsg: "Duplicate wildcard '...' specification for /Archive/2020/.../"})
|
||||||
|
})
|
||||||
|
it('should detect duplicate match all definitions for same path', () => {
|
||||||
|
const matcher: FolderWildcardMatching<SortingSpec> = new FolderWildcardMatching()
|
||||||
|
matcher.addWildcardDefinition('/Archive/2019/*', 'First occurrence')
|
||||||
|
const result = matcher.addWildcardDefinition('Archive/2019/*', 'Duplicate')
|
||||||
|
|
||||||
|
expect(result).toEqual({errorMsg: "Duplicate wildcard '*' specification for Archive/2019/*"})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,116 @@
|
||||||
|
export interface FolderPattern {
|
||||||
|
path: string
|
||||||
|
deep: boolean
|
||||||
|
nestingLevel: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeterminedSortingSpec<SortingSpec> = {
|
||||||
|
spec?: SortingSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FolderMatchingTreeNode<SortingSpec> {
|
||||||
|
path?: string
|
||||||
|
name?: string
|
||||||
|
matchChildren?: SortingSpec
|
||||||
|
matchAll?: SortingSpec
|
||||||
|
subtree: { [key: string]: FolderMatchingTreeNode<SortingSpec> }
|
||||||
|
}
|
||||||
|
|
||||||
|
const SLASH: string = '/'
|
||||||
|
export const MATCH_CHILDREN_PATH_TOKEN: string = '...'
|
||||||
|
export const MATCH_ALL_PATH_TOKEN: string = '*'
|
||||||
|
export const MATCH_CHILDREN_1_SUFFIX: string = `/${MATCH_CHILDREN_PATH_TOKEN}`
|
||||||
|
export const MATCH_CHILDREN_2_SUFFIX: string = `/${MATCH_CHILDREN_PATH_TOKEN}/`
|
||||||
|
export const MATCH_ALL_SUFFIX: string = `/${MATCH_ALL_PATH_TOKEN}`
|
||||||
|
|
||||||
|
|
||||||
|
export const splitPath = (path: string): Array<string> => {
|
||||||
|
return path.split(SLASH).filter((name) => !!name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddingWildcardFailure {
|
||||||
|
errorMsg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FolderWildcardMatching<SortingSpec> {
|
||||||
|
|
||||||
|
tree: FolderMatchingTreeNode<SortingSpec> = {
|
||||||
|
subtree: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache
|
||||||
|
determinedWildcardRules: { [key: string]: DeterminedSortingSpec<SortingSpec> } = {}
|
||||||
|
|
||||||
|
addWildcardDefinition = (wilcardDefinition: string, rule: SortingSpec): AddingWildcardFailure | null => {
|
||||||
|
const pathComponents: Array<string> = splitPath(wilcardDefinition)
|
||||||
|
const lastComponent: string = pathComponents.pop()
|
||||||
|
if (lastComponent !== MATCH_ALL_PATH_TOKEN && lastComponent !== MATCH_CHILDREN_PATH_TOKEN) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let leafNode: FolderMatchingTreeNode<SortingSpec> = this.tree
|
||||||
|
pathComponents.forEach((pathComponent) => {
|
||||||
|
let subtree: FolderMatchingTreeNode<SortingSpec> = leafNode.subtree[pathComponent]
|
||||||
|
if (subtree) {
|
||||||
|
leafNode = subtree
|
||||||
|
} else {
|
||||||
|
const newSubtree: FolderMatchingTreeNode<SortingSpec> = {
|
||||||
|
name: pathComponent,
|
||||||
|
subtree: {}
|
||||||
|
}
|
||||||
|
leafNode.subtree[pathComponent] = newSubtree
|
||||||
|
leafNode = newSubtree
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (lastComponent === MATCH_CHILDREN_PATH_TOKEN) {
|
||||||
|
if (leafNode.matchChildren) {
|
||||||
|
return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`}
|
||||||
|
} else {
|
||||||
|
leafNode.matchChildren = rule
|
||||||
|
}
|
||||||
|
} else { // Implicitly: MATCH_ALL_PATH_TOKEN
|
||||||
|
if (leafNode.matchAll) {
|
||||||
|
return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`}
|
||||||
|
} else {
|
||||||
|
leafNode.matchAll = rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
folderMatch = (folderPath: string): SortingSpec | null => {
|
||||||
|
const spec: DeterminedSortingSpec<SortingSpec> = this.determinedWildcardRules[folderPath]
|
||||||
|
|
||||||
|
if (spec) {
|
||||||
|
return spec.spec ?? null
|
||||||
|
} else {
|
||||||
|
let rule: SortingSpec = this.tree.matchChildren
|
||||||
|
let inheritedRule: SortingSpec = this.tree.matchAll
|
||||||
|
const pathComponents: Array<string> = splitPath(folderPath)
|
||||||
|
let parentNode: FolderMatchingTreeNode<SortingSpec> = this.tree
|
||||||
|
let lastIdx: number = pathComponents.length - 1
|
||||||
|
for(let i=0; i<=lastIdx; i++) {
|
||||||
|
const name: string = pathComponents[i]
|
||||||
|
let matchedPath: FolderMatchingTreeNode<SortingSpec> = parentNode.subtree[name]
|
||||||
|
if (matchedPath) {
|
||||||
|
parentNode = matchedPath
|
||||||
|
rule = matchedPath?.matchChildren ?? null
|
||||||
|
inheritedRule = matchedPath.matchAll ?? inheritedRule
|
||||||
|
} else {
|
||||||
|
if (i < lastIdx) {
|
||||||
|
rule = inheritedRule
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rule = rule ?? inheritedRule
|
||||||
|
|
||||||
|
if (rule) {
|
||||||
|
this.determinedWildcardRules[folderPath] = {spec: rule}
|
||||||
|
return rule
|
||||||
|
} else {
|
||||||
|
this.determinedWildcardRules[folderPath] = {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
CompoundDashNumberNormalizerFn, CompoundDashRomanNumberNormalizerFn,
|
CompoundDashNumberNormalizerFn,
|
||||||
|
CompoundDashRomanNumberNormalizerFn,
|
||||||
CompoundDotNumberNormalizerFn,
|
CompoundDotNumberNormalizerFn,
|
||||||
convertPlainStringWithNumericSortingSymbolToRegex,
|
convertPlainStringWithNumericSortingSymbolToRegex,
|
||||||
detectNumericSortingSymbols,
|
detectNumericSortingSymbols,
|
||||||
|
@ -12,6 +13,7 @@ import {
|
||||||
SortingSpecProcessor
|
SortingSpecProcessor
|
||||||
} from "./sorting-spec-processor"
|
} from "./sorting-spec-processor"
|
||||||
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
|
||||||
|
import {FolderMatchingTreeNode} from "./folder-matching-rules";
|
||||||
|
|
||||||
const txtInputExampleA: string = `
|
const txtInputExampleA: string = `
|
||||||
order-asc: a-z
|
order-asc: a-z
|
||||||
|
@ -348,17 +350,17 @@ describe('SortingSpecProcessor', () => {
|
||||||
it('should generate correct SortSpecs (complex example A)', () => {
|
it('should generate correct SortSpecs (complex example A)', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputExampleA.split('\n')
|
const inputTxtArr: Array<string> = txtInputExampleA.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
expect(result).toEqual(expectedSortSpecsExampleA)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleA)
|
||||||
})
|
})
|
||||||
it('should generate correct SortSpecs (complex example A verbose)', () => {
|
it('should generate correct SortSpecs (complex example A verbose)', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputExampleAVerbose.split('\n')
|
const inputTxtArr: Array<string> = txtInputExampleAVerbose.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
expect(result).toEqual(expectedSortSpecsExampleA)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleA)
|
||||||
})
|
})
|
||||||
it('should generate correct SortSpecs (example with numerical sorting symbols)', () => {
|
it('should generate correct SortSpecs (example with numerical sorting symbols)', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputExampleNumericSortingSymbols.split('\n')
|
const inputTxtArr: Array<string> = txtInputExampleNumericSortingSymbols.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
expect(result).toEqual(expectedSortSpecsExampleNumericSortingSymbols)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleNumericSortingSymbols)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -401,7 +403,36 @@ describe('SortingSpecProcessor', () => {
|
||||||
it('should not duplicate spec if former target-folder had some attribute specified', () => {
|
it('should not duplicate spec if former target-folder had some attribute specified', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputNotDuplicatedSortSpec.split('\n')
|
const inputTxtArr: Array<string> = txtInputNotDuplicatedSortSpec.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
expect(result).toEqual(expectedSortSpecsNotDuplicatedSortSpec)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecsNotDuplicatedSortSpec)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const txtInputStandardObsidianSortAttr: string = `
|
||||||
|
target-folder: AAA
|
||||||
|
sorting: standard
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedSortSpecForObsidianStandardSorting: { [key: string]: CustomSortSpec } = {
|
||||||
|
"AAA": {
|
||||||
|
defaultOrder: CustomSortOrder.standardObsidian,
|
||||||
|
groups: [{
|
||||||
|
order: CustomSortOrder.standardObsidian,
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 0,
|
||||||
|
targetFoldersPaths: ['AAA']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SortingSpecProcessor', () => {
|
||||||
|
let processor: SortingSpecProcessor;
|
||||||
|
beforeEach(() => {
|
||||||
|
processor = new SortingSpecProcessor();
|
||||||
|
});
|
||||||
|
it('should recognize the standard Obsidian sorting attribute for a folder', () => {
|
||||||
|
const inputTxtArr: Array<string> = txtInputStandardObsidianSortAttr.split('\n')
|
||||||
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForObsidianStandardSorting)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -435,7 +466,7 @@ describe('SortingSpecProcessor bonus experimental feature', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputItemsToHideWithDupsSortSpec.split('\n')
|
const inputTxtArr: Array<string> = txtInputItemsToHideWithDupsSortSpec.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
// REMARK: be careful with examining Set object
|
// REMARK: be careful with examining Set object
|
||||||
expect(result).toEqual(expectedHiddenItemsSortSpec)
|
expect(result?.sortSpecByPath).toEqual(expectedHiddenItemsSortSpec)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -490,7 +521,7 @@ describe('SortingSpecProcessor - README.md examples', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputItemsReadmeExample1Spec.split('\n')
|
const inputTxtArr: Array<string> = txtInputItemsReadmeExample1Spec.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
// REMARK: be careful with examining Set object
|
// REMARK: be careful with examining Set object
|
||||||
expect(result).toEqual(expectedReadmeExample1SortSpec)
|
expect(result?.sortSpecByPath).toEqual(expectedReadmeExample1SortSpec)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -513,43 +544,244 @@ const txtInputTargetFolderAsDot: string = `
|
||||||
// Let me introduce a comment here ;-) to ensure it is ignored
|
// Let me introduce a comment here ;-) to ensure it is ignored
|
||||||
target-folder: .
|
target-folder: .
|
||||||
target-folder: CCC
|
target-folder: CCC
|
||||||
|
target-folder: ./sub
|
||||||
|
target-folder: ./*
|
||||||
|
target-folder: ./...
|
||||||
|
//target-folder: ./.../
|
||||||
// This comment should be ignored as well
|
// This comment should be ignored as well
|
||||||
`
|
`
|
||||||
|
|
||||||
const expectedSortSpecsTargetFolderAsDot: { [key: string]: CustomSortSpec } = {
|
const expectedSortSpecToBeMultiplied = {
|
||||||
'mock-folder': {
|
|
||||||
groups: [{
|
groups: [{
|
||||||
order: CustomSortOrder.alphabetical,
|
order: CustomSortOrder.alphabetical,
|
||||||
type: CustomSortGroupType.Outsiders
|
type: CustomSortGroupType.Outsiders
|
||||||
}],
|
}],
|
||||||
outsidersGroupIdx: 0,
|
outsidersGroupIdx: 0,
|
||||||
targetFoldersPaths: ['mock-folder', 'CCC']
|
targetFoldersPaths: ['mock-folder', 'CCC', 'mock-folder/sub', "mock-folder/*", "mock-folder/..."]
|
||||||
},
|
|
||||||
'CCC': {
|
|
||||||
groups: [{
|
|
||||||
order: CustomSortOrder.alphabetical,
|
|
||||||
type: CustomSortGroupType.Outsiders
|
|
||||||
}],
|
|
||||||
outsidersGroupIdx: 0,
|
|
||||||
targetFoldersPaths: ['mock-folder', 'CCC']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expectedSortSpecsTargetFolderAsDot: { [key: string]: CustomSortSpec } = {
|
||||||
|
'mock-folder': expectedSortSpecToBeMultiplied,
|
||||||
|
'CCC': expectedSortSpecToBeMultiplied,
|
||||||
|
'mock-folder/sub': expectedSortSpecToBeMultiplied
|
||||||
|
}
|
||||||
|
|
||||||
describe('SortingSpecProcessor edge case', () => {
|
describe('SortingSpecProcessor edge case', () => {
|
||||||
let processor: SortingSpecProcessor;
|
let processor: SortingSpecProcessor;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
processor = new SortingSpecProcessor();
|
processor = new SortingSpecProcessor();
|
||||||
});
|
});
|
||||||
it('should not recognize empty spec containing only target folder', () => {
|
it('should recognize empty spec containing only target folder', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputEmptySpecOnlyTargetFolder.split('\n')
|
const inputTxtArr: Array<string> = txtInputEmptySpecOnlyTargetFolder.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
expect(result).toEqual(expectedSortSpecsOnlyTargetFolder)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecsOnlyTargetFolder)
|
||||||
})
|
})
|
||||||
it('should not recognize and correctly replace dot as the target folder', () => {
|
it('should recognize and correctly replace dot as the target folder', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputTargetFolderAsDot.split('\n')
|
const inputTxtArr: Array<string> = txtInputTargetFolderAsDot.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
expect(result).toEqual(expectedSortSpecsTargetFolderAsDot)
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecsTargetFolderAsDot)
|
||||||
|
expect(result?.sortSpecByWildcard).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const txtInputTargetFolderMultiSpecA: string = `
|
||||||
|
target-folder: .
|
||||||
|
< a-z
|
||||||
|
target-folder: ./*
|
||||||
|
> a-z
|
||||||
|
target-folder: ./.../
|
||||||
|
< modified
|
||||||
|
`
|
||||||
|
|
||||||
|
const txtInputTargetFolderMultiSpecB: string = `
|
||||||
|
target-folder: ./*
|
||||||
|
> a-z
|
||||||
|
target-folder: ./.../
|
||||||
|
< modified
|
||||||
|
target-folder: .
|
||||||
|
< a-z
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedSortSpecForMultiSpecAandB: { [key: string]: CustomSortSpec } = {
|
||||||
|
'mock-folder': {
|
||||||
|
defaultOrder: CustomSortOrder.alphabetical,
|
||||||
|
groups: [{
|
||||||
|
order: CustomSortOrder.alphabetical,
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 0,
|
||||||
|
targetFoldersPaths: ['mock-folder']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedWildcardMatchingTreeForMultiSpecAandB: FolderMatchingTreeNode<CustomSortSpec> = {
|
||||||
|
subtree: {
|
||||||
|
"mock-folder": {
|
||||||
|
matchAll: {
|
||||||
|
"defaultOrder": CustomSortOrder.alphabeticalReverse,
|
||||||
|
"groups": [{
|
||||||
|
"order": CustomSortOrder.alphabeticalReverse,
|
||||||
|
"type": CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
"outsidersGroupIdx": 0,
|
||||||
|
"targetFoldersPaths": ["mock-folder/*"]
|
||||||
|
},
|
||||||
|
matchChildren: {
|
||||||
|
"defaultOrder": CustomSortOrder.byModifiedTime,
|
||||||
|
"groups": [{
|
||||||
|
"order": CustomSortOrder.byModifiedTime,
|
||||||
|
"type": CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
"outsidersGroupIdx": 0,
|
||||||
|
"targetFoldersPaths": ["mock-folder/.../"]
|
||||||
|
},
|
||||||
|
name: "mock-folder",
|
||||||
|
subtree: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const txtInputTargetFolderMultiSpecC: string = `
|
||||||
|
target-folder: ./*
|
||||||
|
> a-z
|
||||||
|
target-folder: ./.../
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedSortSpecForMultiSpecC: { [key: string]: CustomSortSpec } = {
|
||||||
|
'mock-folder': {
|
||||||
|
groups: [{
|
||||||
|
order: CustomSortOrder.alphabetical,
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 0,
|
||||||
|
targetFoldersPaths: ['mock-folder/.../']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedWildcardMatchingTreeForMultiSpecC: FolderMatchingTreeNode<CustomSortSpec> = {
|
||||||
|
subtree: {
|
||||||
|
"mock-folder": {
|
||||||
|
matchAll: {
|
||||||
|
"defaultOrder": CustomSortOrder.alphabeticalReverse,
|
||||||
|
"groups": [{
|
||||||
|
"order": CustomSortOrder.alphabeticalReverse,
|
||||||
|
"type": CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
"outsidersGroupIdx": 0,
|
||||||
|
"targetFoldersPaths": ["mock-folder/*"]
|
||||||
|
},
|
||||||
|
matchChildren: {
|
||||||
|
"groups": [{
|
||||||
|
"order": CustomSortOrder.alphabetical,
|
||||||
|
"type": CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
"outsidersGroupIdx": 0,
|
||||||
|
"targetFoldersPaths": ["mock-folder/.../"]
|
||||||
|
},
|
||||||
|
name: "mock-folder",
|
||||||
|
subtree: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const txtInputTargetFolderMultiSpecD: string = `
|
||||||
|
target-folder: ./*
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedSortSpecForMultiSpecD: { [key: string]: CustomSortSpec } = {
|
||||||
|
'mock-folder': {
|
||||||
|
groups: [{
|
||||||
|
order: CustomSortOrder.alphabetical,
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 0,
|
||||||
|
targetFoldersPaths: ['mock-folder/*']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedWildcardMatchingTreeForMultiSpecD: FolderMatchingTreeNode<CustomSortSpec> = {
|
||||||
|
subtree: {
|
||||||
|
"mock-folder": {
|
||||||
|
matchAll: {
|
||||||
|
"groups": [{
|
||||||
|
"order": CustomSortOrder.alphabetical,
|
||||||
|
"type": CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
"outsidersGroupIdx": 0,
|
||||||
|
"targetFoldersPaths": ["mock-folder/*"]
|
||||||
|
},
|
||||||
|
name: "mock-folder",
|
||||||
|
subtree: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const txtInputTargetFolderMultiSpecE: string = `
|
||||||
|
target-folder: mock-folder/...
|
||||||
|
`
|
||||||
|
|
||||||
|
const expectedSortSpecForMultiSpecE: { [key: string]: CustomSortSpec } = {
|
||||||
|
'mock-folder': {
|
||||||
|
groups: [{
|
||||||
|
order: CustomSortOrder.alphabetical,
|
||||||
|
type: CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
outsidersGroupIdx: 0,
|
||||||
|
targetFoldersPaths: ['mock-folder/...']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedWildcardMatchingTreeForMultiSpecE: FolderMatchingTreeNode<CustomSortSpec> = {
|
||||||
|
subtree: {
|
||||||
|
"mock-folder": {
|
||||||
|
matchChildren: {
|
||||||
|
"groups": [{
|
||||||
|
"order": CustomSortOrder.alphabetical,
|
||||||
|
"type": CustomSortGroupType.Outsiders
|
||||||
|
}],
|
||||||
|
"outsidersGroupIdx": 0,
|
||||||
|
"targetFoldersPaths": ["mock-folder/..."]
|
||||||
|
},
|
||||||
|
name: "mock-folder",
|
||||||
|
subtree: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SortingSpecProcessor path wildcard priorities', () => {
|
||||||
|
let processor: SortingSpecProcessor;
|
||||||
|
beforeEach(() => {
|
||||||
|
processor = new SortingSpecProcessor();
|
||||||
|
});
|
||||||
|
it('should not raise error for multiple spec for the same path and choose correct spec, case A', () => {
|
||||||
|
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecA.split('\n')
|
||||||
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecAandB)
|
||||||
|
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecAandB)
|
||||||
|
})
|
||||||
|
it('should not raise error for multiple spec for the same path and choose correct spec, case B', () => {
|
||||||
|
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecB.split('\n')
|
||||||
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecAandB)
|
||||||
|
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecAandB)
|
||||||
|
})
|
||||||
|
it('should not raise error for multiple spec for the same path and choose correct spec, case C', () => {
|
||||||
|
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecC.split('\n')
|
||||||
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecC)
|
||||||
|
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecC)
|
||||||
|
})
|
||||||
|
it('should not raise error for multiple spec for the same path and choose correct spec, case D', () => {
|
||||||
|
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecD.split('\n')
|
||||||
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecD)
|
||||||
|
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecD)
|
||||||
|
})
|
||||||
|
it('should not raise error for multiple spec for the same path and choose correct spec, case E', () => {
|
||||||
|
const inputTxtArr: Array<string> = txtInputTargetFolderMultiSpecE.split('\n')
|
||||||
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result?.sortSpecByPath).toEqual(expectedSortSpecForMultiSpecE)
|
||||||
|
expect(result?.sortSpecByWildcard.tree).toEqual(expectedWildcardMatchingTreeForMultiSpecE)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -600,6 +832,13 @@ target-folder: AAA
|
||||||
const txtInputErrorTooManyNumericSortSymbols: string = `
|
const txtInputErrorTooManyNumericSortSymbols: string = `
|
||||||
% Chapter\\R+ ... page\\d+
|
% Chapter\\R+ ... page\\d+
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const txtInputErrorNestedStandardObsidianSortAttr: string = `
|
||||||
|
target-folder: AAA
|
||||||
|
/ Some folder
|
||||||
|
sorting: standard
|
||||||
|
`
|
||||||
|
|
||||||
const txtInputEmptySpec: string = ``
|
const txtInputEmptySpec: string = ``
|
||||||
|
|
||||||
describe('SortingSpecProcessor error detection and reporting', () => {
|
describe('SortingSpecProcessor error detection and reporting', () => {
|
||||||
|
@ -687,6 +926,15 @@ describe('SortingSpecProcessor error detection and reporting', () => {
|
||||||
`${ERR_PREFIX} 9:TooManyNumericSortingSymbols Maximum one numeric sorting indicator allowed per line ${ERR_SUFFIX_IN_LINE(2)}`)
|
`${ERR_PREFIX} 9:TooManyNumericSortingSymbols Maximum one numeric sorting indicator allowed per line ${ERR_SUFFIX_IN_LINE(2)}`)
|
||||||
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('% Chapter\\R+ ... page\\d+ '))
|
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('% Chapter\\R+ ... page\\d+ '))
|
||||||
})
|
})
|
||||||
|
it('should recognize error: nested standard obsidian sorting attribute', () => {
|
||||||
|
const inputTxtArr: Array<string> = txtInputErrorNestedStandardObsidianSortAttr.split('\n')
|
||||||
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result).toBeNull()
|
||||||
|
expect(errorsLogger).toHaveBeenCalledTimes(2)
|
||||||
|
expect(errorsLogger).toHaveBeenNthCalledWith(1,
|
||||||
|
`${ERR_PREFIX} 14:StandardObsidianSortAllowedOnlyAtFolderLevel The standard Obsidian sort order is only allowed at a folder level (not nested syntax) ${ERR_SUFFIX_IN_LINE(4)}`)
|
||||||
|
expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(' sorting: standard'))
|
||||||
|
})
|
||||||
it('should recognize empty spec', () => {
|
it('should recognize empty spec', () => {
|
||||||
const inputTxtArr: Array<string> = txtInputEmptySpec.split('\n')
|
const inputTxtArr: Array<string> = txtInputEmptySpec.split('\n')
|
||||||
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
@ -709,6 +957,24 @@ describe('SortingSpecProcessor error detection and reporting', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const txtInputTargetFolderCCC: string = `
|
||||||
|
target-folder: CCC
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('SortingSpecProcessor advanced error detection', () => {
|
||||||
|
it('should retain state of duplicates detection in the instance', () => {
|
||||||
|
let processor: SortingSpecProcessor = new SortingSpecProcessor(errorsLogger);
|
||||||
|
errorsLogger.mockReset()
|
||||||
|
const inputTxtArr: Array<string> = txtInputTargetFolderCCC.split('\n')
|
||||||
|
const result1 = processor.parseSortSpecFromText(inputTxtArr, 'another-mock-folder', 'sortspec.md')
|
||||||
|
const result2 = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md')
|
||||||
|
expect(result1).not.toBeNull()
|
||||||
|
expect(result2).toBeNull()
|
||||||
|
expect(errorsLogger).toHaveBeenCalledTimes(1)
|
||||||
|
expect(errorsLogger).toHaveBeenCalledWith(`${ERR_PREFIX} 2:DuplicateSortSpecForSameFolder Duplicate sorting spec for folder CCC ${ERR_SUFFIX}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('convertPlainStringSortingGroupSpecToArraySpec', () => {
|
describe('convertPlainStringSortingGroupSpecToArraySpec', () => {
|
||||||
let processor: SortingSpecProcessor;
|
let processor: SortingSpecProcessor;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -3,9 +3,9 @@ import {
|
||||||
CustomSortGroupType,
|
CustomSortGroupType,
|
||||||
CustomSortOrder,
|
CustomSortOrder,
|
||||||
CustomSortSpec,
|
CustomSortSpec,
|
||||||
NormalizerFn, RecognizedOrderValue,
|
NormalizerFn,
|
||||||
RegExpSpec,
|
RecognizedOrderValue,
|
||||||
SortSpecsCollection
|
RegExpSpec
|
||||||
} from "./custom-sort-types";
|
} from "./custom-sort-types";
|
||||||
import {isDefined, last} from "../utils/utils";
|
import {isDefined, last} from "../utils/utils";
|
||||||
import {
|
import {
|
||||||
|
@ -20,6 +20,12 @@ import {
|
||||||
NumberRegexStr,
|
NumberRegexStr,
|
||||||
RomanNumberRegexStr
|
RomanNumberRegexStr
|
||||||
} from "./matchers";
|
} from "./matchers";
|
||||||
|
import {
|
||||||
|
FolderWildcardMatching,
|
||||||
|
MATCH_ALL_SUFFIX,
|
||||||
|
MATCH_CHILDREN_1_SUFFIX,
|
||||||
|
MATCH_CHILDREN_2_SUFFIX
|
||||||
|
} from "./folder-matching-rules"
|
||||||
|
|
||||||
interface ProcessingContext {
|
interface ProcessingContext {
|
||||||
folderPath: string
|
folderPath: string
|
||||||
|
@ -54,11 +60,14 @@ export enum ProblemCode {
|
||||||
TooManyNumericSortingSymbols,
|
TooManyNumericSortingSymbols,
|
||||||
NumericalSymbolAdjacentToWildcard,
|
NumericalSymbolAdjacentToWildcard,
|
||||||
ItemToHideExactNameWithExtRequired,
|
ItemToHideExactNameWithExtRequired,
|
||||||
ItemToHideNoSupportForThreeDots
|
ItemToHideNoSupportForThreeDots,
|
||||||
|
DuplicateWildcardSortSpecForSameFolder,
|
||||||
|
StandardObsidianSortAllowedOnlyAtFolderLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContextFreeProblems = new Set<ProblemCode>([
|
const ContextFreeProblems = new Set<ProblemCode>([
|
||||||
ProblemCode.DuplicateSortSpecForSameFolder
|
ProblemCode.DuplicateSortSpecForSameFolder,
|
||||||
|
ProblemCode.DuplicateWildcardSortSpecForSameFolder
|
||||||
])
|
])
|
||||||
|
|
||||||
const ThreeDots = '...';
|
const ThreeDots = '...';
|
||||||
|
@ -104,7 +113,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
||||||
enum Attribute {
|
enum Attribute {
|
||||||
TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ...
|
TargetFolder = 1, // Starting from 1 to allow: if (attribute) { ...
|
||||||
OrderAsc,
|
OrderAsc,
|
||||||
OrderDesc
|
OrderDesc,
|
||||||
|
OrderStandardObsidian
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttrLexems: { [key: string]: Attribute } = {
|
const AttrLexems: { [key: string]: Attribute } = {
|
||||||
|
@ -112,6 +122,7 @@ const AttrLexems: { [key: string]: Attribute } = {
|
||||||
'target-folder:': Attribute.TargetFolder,
|
'target-folder:': Attribute.TargetFolder,
|
||||||
'order-asc:': Attribute.OrderAsc,
|
'order-asc:': Attribute.OrderAsc,
|
||||||
'order-desc:': Attribute.OrderDesc,
|
'order-desc:': Attribute.OrderDesc,
|
||||||
|
'sorting:': Attribute.OrderStandardObsidian,
|
||||||
// Concise abbreviated equivalents
|
// Concise abbreviated equivalents
|
||||||
'::::': Attribute.TargetFolder,
|
'::::': Attribute.TargetFolder,
|
||||||
'<': Attribute.OrderAsc,
|
'<': Attribute.OrderAsc,
|
||||||
|
@ -280,6 +291,15 @@ export const convertPlainStringWithNumericSortingSymbolToRegex = (s: string, act
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FolderPathToSortSpecMap {
|
||||||
|
[key: string]: CustomSortSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortSpecsCollection {
|
||||||
|
sortSpecByPath: FolderPathToSortSpecMap
|
||||||
|
sortSpecByWildcard?: FolderWildcardMatching<CustomSortSpec>
|
||||||
|
}
|
||||||
|
|
||||||
interface AdjacencyInfo {
|
interface AdjacencyInfo {
|
||||||
noPrefix: boolean,
|
noPrefix: boolean,
|
||||||
noSuffix: boolean
|
noSuffix: boolean
|
||||||
|
@ -292,6 +312,43 @@ const checkAdjacency = (sortingSymbolInfo: ExtractedNumericSortingSymbolInfo): A
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const endsWithWildcardPatternSuffix = (path: string): boolean => {
|
||||||
|
return path.endsWith(MATCH_CHILDREN_1_SUFFIX) ||
|
||||||
|
path.endsWith(MATCH_CHILDREN_2_SUFFIX) ||
|
||||||
|
path.endsWith(MATCH_ALL_SUFFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WildcardPriority {
|
||||||
|
NO_WILDCARD = 1,
|
||||||
|
MATCH_CHILDREN,
|
||||||
|
MATCH_ALL
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripWildcardPatternSuffix = (path: string): [path: string, priority: number] => {
|
||||||
|
if (path.endsWith(MATCH_ALL_SUFFIX)) {
|
||||||
|
return [
|
||||||
|
path.slice(0, -MATCH_ALL_SUFFIX.length),
|
||||||
|
WildcardPriority.MATCH_ALL
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (path.endsWith(MATCH_CHILDREN_1_SUFFIX)) {
|
||||||
|
return [
|
||||||
|
path.slice(0, -MATCH_CHILDREN_1_SUFFIX.length),
|
||||||
|
WildcardPriority.MATCH_CHILDREN,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (path.endsWith(MATCH_CHILDREN_2_SUFFIX)) {
|
||||||
|
return [
|
||||||
|
path.slice(0, -MATCH_CHILDREN_2_SUFFIX.length),
|
||||||
|
WildcardPriority.MATCH_CHILDREN
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
path,
|
||||||
|
WildcardPriority.NO_WILDCARD
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case."
|
const ADJACENCY_ERROR: string = "Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case."
|
||||||
|
|
||||||
export class SortingSpecProcessor {
|
export class SortingSpecProcessor {
|
||||||
|
@ -302,6 +359,11 @@ export class SortingSpecProcessor {
|
||||||
problemAlreadyReportedForCurrentLine: boolean
|
problemAlreadyReportedForCurrentLine: boolean
|
||||||
recentErrorMessage: string
|
recentErrorMessage: string
|
||||||
|
|
||||||
|
// Helper map to deal with rule priorities for the same path
|
||||||
|
// and also detect non-wildcard duplicates.
|
||||||
|
// The wildcard duplicates were detected prior to this point, no need to bother about them
|
||||||
|
pathMatchPriorityForPath: {[key: string]: WildcardPriority} = {}
|
||||||
|
|
||||||
// Logger parameter exposed to support unit testing of error cases as well as capturing error messages
|
// Logger parameter exposed to support unit testing of error cases as well as capturing error messages
|
||||||
// for in-app presentation
|
// for in-app presentation
|
||||||
constructor(private errorLogger?: typeof console.log) {
|
constructor(private errorLogger?: typeof console.log) {
|
||||||
|
@ -313,10 +375,17 @@ export class SortingSpecProcessor {
|
||||||
sortingSpecFileName: string,
|
sortingSpecFileName: string,
|
||||||
collection?: SortSpecsCollection
|
collection?: SortSpecsCollection
|
||||||
): SortSpecsCollection {
|
): SortSpecsCollection {
|
||||||
|
// 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
|
||||||
specs: []
|
specs: []
|
||||||
};
|
};
|
||||||
|
this.currentEntryLine = null
|
||||||
|
this.currentEntryLineIdx = null
|
||||||
|
this.currentSortingSpecContainerFilePath = null
|
||||||
|
this.problemAlreadyReportedForCurrentLine = null
|
||||||
|
this.recentErrorMessage = null
|
||||||
|
|
||||||
let success: boolean = false;
|
let success: boolean = false;
|
||||||
let lineIdx: number = 0;
|
let lineIdx: number = 0;
|
||||||
for (let entryLine of text) {
|
for (let entryLine of text) {
|
||||||
|
@ -359,13 +428,51 @@ export class SortingSpecProcessor {
|
||||||
if (this.ctx.specs.length > 0) {
|
if (this.ctx.specs.length > 0) {
|
||||||
for (let spec of this.ctx.specs) {
|
for (let spec of this.ctx.specs) {
|
||||||
this._l1s6_postprocessSortSpec(spec)
|
this._l1s6_postprocessSortSpec(spec)
|
||||||
for (let folderPath of spec.targetFoldersPaths) {
|
|
||||||
collection = collection ?? {}
|
|
||||||
if (collection[folderPath]) {
|
|
||||||
this.problem(ProblemCode.DuplicateSortSpecForSameFolder, `Duplicate sorting spec for folder ${folderPath}`)
|
|
||||||
return null // Failure - not allow duplicate specs for the same folder
|
|
||||||
}
|
}
|
||||||
collection[folderPath] = spec
|
|
||||||
|
let sortspecByWildcard: FolderWildcardMatching<CustomSortSpec>
|
||||||
|
for (let spec of this.ctx.specs) {
|
||||||
|
// Consume the folder paths ending with wildcard specs
|
||||||
|
for (let idx = 0; idx<spec.targetFoldersPaths.length; idx++) {
|
||||||
|
const path = spec.targetFoldersPaths[idx]
|
||||||
|
if (endsWithWildcardPatternSuffix(path)) {
|
||||||
|
sortspecByWildcard = sortspecByWildcard ?? new FolderWildcardMatching<CustomSortSpec>()
|
||||||
|
const ruleAdded = sortspecByWildcard.addWildcardDefinition(path, spec)
|
||||||
|
if (ruleAdded?.errorMsg) {
|
||||||
|
this.problem(ProblemCode.DuplicateWildcardSortSpecForSameFolder, ruleAdded?.errorMsg)
|
||||||
|
return null // Failure - not allow duplicate wildcard specs for the same folder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortspecByWildcard) {
|
||||||
|
collection = collection ?? { sortSpecByPath:{} }
|
||||||
|
collection.sortSpecByWildcard = sortspecByWildcard
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let spec of this.ctx.specs) {
|
||||||
|
for (let idx = 0; idx < spec.targetFoldersPaths.length; idx++) {
|
||||||
|
const originalPath = spec.targetFoldersPaths[idx]
|
||||||
|
collection = collection ?? { sortSpecByPath: {} }
|
||||||
|
let detectedWildcardPriority: WildcardPriority
|
||||||
|
let path: string
|
||||||
|
[path, detectedWildcardPriority] = stripWildcardPatternSuffix(originalPath)
|
||||||
|
let storeTheSpec: boolean = true
|
||||||
|
const preexistingSortSpecPriority: WildcardPriority = this.pathMatchPriorityForPath[path]
|
||||||
|
if (preexistingSortSpecPriority) {
|
||||||
|
if (preexistingSortSpecPriority === WildcardPriority.NO_WILDCARD && detectedWildcardPriority === WildcardPriority.NO_WILDCARD) {
|
||||||
|
this.problem(ProblemCode.DuplicateSortSpecForSameFolder, `Duplicate sorting spec for folder ${path}`)
|
||||||
|
return null // Failure - not allow duplicate specs for the same no-wildcard folder path
|
||||||
|
} else if (detectedWildcardPriority >= preexistingSortSpecPriority) {
|
||||||
|
// Ignore lower priority rule
|
||||||
|
storeTheSpec = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (storeTheSpec) {
|
||||||
|
collection.sortSpecByPath[path] = spec
|
||||||
|
this.pathMatchPriorityForPath[path] = detectedWildcardPriority
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,7 +559,7 @@ export class SortingSpecProcessor {
|
||||||
this.problem(ProblemCode.TargetFolderNestedSpec, `Nested (indented) specification of target folder is not allowed`)
|
this.problem(ProblemCode.TargetFolderNestedSpec, `Nested (indented) specification of target folder is not allowed`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else if (attr.attribute === Attribute.OrderAsc || attr.attribute === Attribute.OrderDesc) {
|
} else if (attr.attribute === Attribute.OrderAsc || attr.attribute === Attribute.OrderDesc || attr.attribute === Attribute.OrderStandardObsidian) {
|
||||||
if (attr.nesting === 0) {
|
if (attr.nesting === 0) {
|
||||||
if (!this.ctx.currentSpec) {
|
if (!this.ctx.currentSpec) {
|
||||||
this._l2s2_putNewSpecForNewTargetFolder()
|
this._l2s2_putNewSpecForNewTargetFolder()
|
||||||
|
@ -474,6 +581,10 @@ export class SortingSpecProcessor {
|
||||||
this.problem(ProblemCode.DuplicateOrderAttr, `Duplicate order specification for a sorting rule of folder ${folderPathsForProblemMsg}`)
|
this.problem(ProblemCode.DuplicateOrderAttr, `Duplicate order specification for a sorting rule of folder ${folderPathsForProblemMsg}`)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if ((attr.value as RecognizedOrderValue).order === CustomSortOrder.standardObsidian) {
|
||||||
|
this.problem(ProblemCode.StandardObsidianSortAllowedOnlyAtFolderLevel, `The standard Obsidian sort order is only allowed at a folder level (not nested syntax)`)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
|
this.ctx.currentSpecGroup.order = (attr.value as RecognizedOrderValue).order
|
||||||
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
|
this.ctx.currentSpecGroup.secondaryOrder = (attr.value as RecognizedOrderValue).secondaryOrder
|
||||||
return true;
|
return true;
|
||||||
|
@ -635,10 +746,14 @@ export class SortingSpecProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CURRENT_FOLDER_PREFIX: string = `${CURRENT_FOLDER_SYMBOL}/`
|
||||||
|
|
||||||
// Replace the dot-folder names (coming from: 'target-folder: .') with actual folder names
|
// Replace the dot-folder names (coming from: 'target-folder: .') with actual folder names
|
||||||
spec.targetFoldersPaths.forEach((path, idx) => {
|
spec.targetFoldersPaths.forEach((path, idx) => {
|
||||||
if (path === CURRENT_FOLDER_SYMBOL) {
|
if (path === CURRENT_FOLDER_SYMBOL) {
|
||||||
spec.targetFoldersPaths[idx] = this.ctx.folderPath
|
spec.targetFoldersPaths[idx] = this.ctx.folderPath
|
||||||
|
} else if (path.startsWith(CURRENT_FOLDER_PREFIX)) {
|
||||||
|
spec.targetFoldersPaths[idx] = `${this.ctx.folderPath}/${path.substr(CURRENT_FOLDER_PREFIX.length)}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -675,10 +790,19 @@ export class SortingSpecProcessor {
|
||||||
} : null;
|
} : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _l2s1_validateSortingAttrValue = (v: string): RecognizedOrderValue | null => {
|
||||||
|
// for now only a single fixed lexem
|
||||||
|
const recognized: boolean = v.trim().toLowerCase() === 'standard'
|
||||||
|
return recognized ? {
|
||||||
|
order: CustomSortOrder.standardObsidian
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
attrValueValidators: { [key in Attribute]: AttrValueValidatorFn } = {
|
attrValueValidators: { [key in Attribute]: AttrValueValidatorFn } = {
|
||||||
[Attribute.TargetFolder]: this._l2s1_validateTargetFolderAttrValue.bind(this),
|
[Attribute.TargetFolder]: this._l2s1_validateTargetFolderAttrValue.bind(this),
|
||||||
[Attribute.OrderAsc]: this._l2s1_validateOrderAscAttrValue.bind(this),
|
[Attribute.OrderAsc]: this._l2s1_validateOrderAscAttrValue.bind(this),
|
||||||
[Attribute.OrderDesc]: this._l2s1_validateOrderDescAttrValue.bind(this),
|
[Attribute.OrderDesc]: this._l2s1_validateOrderDescAttrValue.bind(this),
|
||||||
|
[Attribute.OrderStandardObsidian]: this._l2s1_validateSortingAttrValue.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
_l2s2_convertPlainStringSortingGroupSpecToArraySpec = (spec: string): Array<string> => {
|
_l2s2_convertPlainStringSortingGroupSpecToArraySpec = (spec: string): Array<string> => {
|
||||||
|
|
37
src/main.ts
37
src/main.ts
|
@ -14,14 +14,15 @@ import {
|
||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
import {around} from 'monkey-around';
|
import {around} from 'monkey-around';
|
||||||
import {folderSort} from './custom-sort/custom-sort';
|
import {folderSort} from './custom-sort/custom-sort';
|
||||||
import {SortingSpecProcessor} from './custom-sort/sorting-spec-processor';
|
import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
|
||||||
import {CustomSortSpec, SortSpecsCollection} from './custom-sort/custom-sort-types';
|
import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addIcons,
|
addIcons,
|
||||||
ICON_SORT_ENABLED_ACTIVE,
|
ICON_SORT_ENABLED_ACTIVE,
|
||||||
|
ICON_SORT_ENABLED_NOT_APPLIED,
|
||||||
ICON_SORT_SUSPENDED,
|
ICON_SORT_SUSPENDED,
|
||||||
ICON_SORT_SUSPENDED_SYNTAX_ERROR,
|
ICON_SORT_SUSPENDED_SYNTAX_ERROR
|
||||||
ICON_SORT_ENABLED_NOT_APPLIED
|
|
||||||
} from "./custom-sort/icons";
|
} from "./custom-sort/icons";
|
||||||
|
|
||||||
interface CustomSortPluginSettings {
|
interface CustomSortPluginSettings {
|
||||||
|
@ -54,6 +55,8 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
let errorMessage: string
|
let errorMessage: string
|
||||||
// reset cache
|
// reset cache
|
||||||
this.sortSpecCache = null
|
this.sortSpecCache = null
|
||||||
|
const processor: SortingSpecProcessor = new SortingSpecProcessor()
|
||||||
|
|
||||||
Vault.recurseChildren(this.app.vault.getRoot(), (file: TAbstractFile) => {
|
Vault.recurseChildren(this.app.vault.getRoot(), (file: TAbstractFile) => {
|
||||||
if (failed) return
|
if (failed) return
|
||||||
if (file instanceof TFile) {
|
if (file instanceof TFile) {
|
||||||
|
@ -67,7 +70,6 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
const sortingSpecTxt: string = mCache.getCache(aFile.path)?.frontmatter?.[SORTINGSPEC_YAML_KEY]
|
const sortingSpecTxt: string = mCache.getCache(aFile.path)?.frontmatter?.[SORTINGSPEC_YAML_KEY]
|
||||||
if (sortingSpecTxt) {
|
if (sortingSpecTxt) {
|
||||||
anySortingSpecFound = true
|
anySortingSpecFound = true
|
||||||
const processor: SortingSpecProcessor = new SortingSpecProcessor()
|
|
||||||
this.sortSpecCache = processor.parseSortSpecFromText(
|
this.sortSpecCache = processor.parseSortSpecFromText(
|
||||||
sortingSpecTxt.split('\n'),
|
sortingSpecTxt.split('\n'),
|
||||||
parent.path,
|
parent.path,
|
||||||
|
@ -87,9 +89,9 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
new Notice(`Parsing custom sorting specification SUCCEEDED!`)
|
new Notice(`Parsing custom sorting specification SUCCEEDED!`)
|
||||||
} else {
|
} else {
|
||||||
if (anySortingSpecFound) {
|
if (anySortingSpecFound) {
|
||||||
errorMessage = `No valid '${SORTINGSPEC_YAML_KEY}:' key(s) in YAML front matter or multiline YAML indentation error or general YAML syntax error`
|
errorMessage = errorMessage ? errorMessage : `No valid '${SORTINGSPEC_YAML_KEY}:' key(s) in YAML front matter or multiline YAML indentation error or general YAML syntax error`
|
||||||
} else {
|
} else {
|
||||||
errorMessage = errorMessage ? errorMessage : `No custom sorting specification found or only empty specification(s)`
|
errorMessage = `No custom sorting specification found or only empty specification(s)`
|
||||||
}
|
}
|
||||||
new Notice(`Parsing custom sorting specification FAILED. Suspending the plugin.\n${errorMessage}`, ERROR_NOTICE_TIMEOUT)
|
new Notice(`Parsing custom sorting specification FAILED. Suspending the plugin.\n${errorMessage}`, ERROR_NOTICE_TIMEOUT)
|
||||||
this.settings.suspended = true
|
this.settings.suspended = true
|
||||||
|
@ -196,13 +198,30 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
let tmpFolder = new TFolder(Vault, "");
|
let tmpFolder = new TFolder(Vault, "");
|
||||||
let Folder = fileExplorer.createFolderDom(tmpFolder).constructor;
|
let Folder = fileExplorer.createFolderDom(tmpFolder).constructor;
|
||||||
this.register(
|
this.register(
|
||||||
|
// TODO: Unit tests please!!! The logic below becomes more and more complex, bugs are captured at run-time...
|
||||||
around(Folder.prototype, {
|
around(Folder.prototype, {
|
||||||
sort(old: any) {
|
sort(old: any) {
|
||||||
return function (...args: any[]) {
|
return function (...args: any[]) {
|
||||||
|
// quick check for plugin status
|
||||||
|
if (plugin.settings.suspended) {
|
||||||
|
return old.call(this, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
// if custom sort is not specified, use the UI-selected
|
// if custom sort is not specified, use the UI-selected
|
||||||
const folder: TFolder = this.file
|
const folder: TFolder = this.file
|
||||||
const sortSpec: CustomSortSpec = plugin.sortSpecCache?.[folder.path]
|
let sortSpec: CustomSortSpec = plugin.sortSpecCache?.sortSpecByPath[folder.path]
|
||||||
if (!plugin.settings.suspended && sortSpec) {
|
if (sortSpec) {
|
||||||
|
if (sortSpec.defaultOrder === CustomSortOrder.standardObsidian) {
|
||||||
|
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
|
||||||
|
}
|
||||||
|
} else if (plugin.sortSpecCache?.sortSpecByWildcard) {
|
||||||
|
// when no sorting spec found directly by folder path, check for wildcard-based match
|
||||||
|
sortSpec = plugin.sortSpecCache?.sortSpecByWildcard.folderMatch(folder.path)
|
||||||
|
if (sortSpec?.defaultOrder === CustomSortOrder.standardObsidian) {
|
||||||
|
sortSpec = null // A folder subtree can be also explicitly excluded from custom sorting plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sortSpec) {
|
||||||
return folderSort.call(this, sortSpec, ...args);
|
return folderSort.call(this, sortSpec, ...args);
|
||||||
} else {
|
} else {
|
||||||
return old.call(this, ...args);
|
return old.call(this, ...args);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"0.5.188": "0.12.0",
|
"0.5.188": "0.12.0",
|
||||||
"0.5.189": "0.12.0"
|
"0.5.189": "0.12.0",
|
||||||
|
"0.6.0": "0.12.0"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue