From 49fba8aa1f47cae33526443d3798015ded6d517c Mon Sep 17 00:00:00 2001 From: INOUE Takuya Date: Fri, 20 Jan 2023 00:06:51 +0900 Subject: [PATCH 01/26] fix .eslintignore (#48) --- .eslintignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintignore b/.eslintignore index 32909b2e9..e019f3ca3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ -npm node_modules -build \ No newline at end of file +node_modules/ + +main.js From c27ea8395229c568f6c9bf44a84926629ed9b9ee Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 25 Jan 2023 13:30:50 +0100 Subject: [PATCH 02/26] Documentation updates --- README.md | 643 ++++---------------------------- advanced-README.md | 643 ++++++++++++++++++++++++++++++++ docs/examples/basic/sortspec.md | 4 + docs/svg/simplest-example-3.svg | 65 ++++ 4 files changed, 784 insertions(+), 571 deletions(-) create mode 100644 advanced-README.md create mode 100644 docs/examples/basic/sortspec.md create mode 100644 docs/svg/simplest-example-3.svg diff --git a/README.md b/README.md index c802ab636..df5228a3d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +# Simplified README.md + +> This is a simple version of README which highlights the **basic scenario and most commonly used feature** +> +> The [long and much more detailed README.md is here](./advanced-README.md) + ## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) Take full control of the order of your notes and folders: @@ -10,7 +16,7 @@ Take full control of the order of your notes and folders: - group and sort notes and folders by notes custom metadata - support for automatic sorting by standard and non-standard rules - mixing manual and automatic ordering also supported -- order by compound numbers in prefix, in suffix (e.g date in suffix) or inbetween +- order by compound numbers in prefix, in suffix (e.g. date in suffix) or inbetween - Roman numbers support, also compound Roman numbers - grouping by prefix or suffix or prefix and suffix - different sorting rules per group even inside the same folder @@ -20,547 +26,88 @@ Take full control of the order of your notes and folders: - 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 - -- [TL;DR Usage](#tldr-usage) - - [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) - - [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 5: P.A.R.A. method example](#example-5-para-method-example) - - [Example 6: P.A.R.A. example with smart syntax](#example-6-para-example-with-smart-syntax) - - [Example 7: Apply the same sorting rules to two folders](#example-7-apply-the-same-sorting-rules-to-two-folders) - - [Example 8: Specify rules for multiple folders](#example-8-specify-rules-for-multiple-folders) - - [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 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) - - [Example 14: Grouping and sorting by metadata value](#example-14-grouping-and-sorting-by-metadata-value) -- [Alphabetical, Natural and True Alphabetical sorting orders](#alphabetical-natural-and-true-alphabetical-sorting-orders) -- [Location of sorting specification YAML entry](#location-of-sorting-specification-yaml-entry) -- [Ribbon icon](#ribbon-icon) -- [Installing the plugin](#installing-the-plugin) - - [From the official Obsidian Community Plugins page](#from-the-official-obsidian-community-plugins-page) - - [Installing the plugin using BRAT](#installing-the-plugin-using-brat) - - [Manually installing the plugin](#manually-installing-the-plugin) -- [Credits](#credits) - -## TL;DR Usage - -For full version of the manual go to [manual](./docs/manual.md) and [syntax-reference](./docs/syntax-reference.md) - -> **Quickstart** -> -> 1. Download the **RAW CONTENT** of [sortspec.md](./docs/examples/quickstart/sortspec.md?plain=1) file and put it in any folder of your vault, -can be the root folder. Ensure the exact file name is `sortspec.md`. That file contains a basic custom sorting specification under the `sorting-spec:` name in the YAML frontmatter. -> > IMPORTANT: follow the above link to 'sortspec.md' and download (or copy & paste) the __RAW__ content of that file, not the HTML displayed by github. -> > Afterwards double check that the content of `sortspec.md` file is not an HTML and: -> > - it starts exactly with the line `---` -> > - it and ends with the line `---` followed by one or two blank lines -> > - indentation is correct (consult images below). In YAML the indentation matters. -> > -> > In other words, ensure, that the final `sortspec.md` file in your vault (which is the `sortspec` Obsidian note) looks exactly like below: -> > ![sortspec.md](./docs/img/sortspec-md-bright.jpg) -> > -> > or if you are a fan of dark mode (line numbers shown for clarity only, they aren't part of the file content): -> > -> > ![sortspec.md](./docs/img/sortspec-md-dark.jpg) -> 2. Enable the plugin in obsidian. -> -> 3. Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png)) 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 -(![Active](./docs/icons/icon-active.png)) and reordering -of items in root vault folder to reverse alphabetical with folders and files treated equally. -> - The notification balloon should confirm success: ![Success](./docs/icons/parsing-succeeded.png) -> 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 ![Not applied](./docs/icons/icon-not-applied.png) -and ![Error](./docs/icons/icon-error.png). 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. - -For simplicity (if you are examining the plugin for the first time) copy and paste the below YAML snippets to the front -matter of the `sortspec` note (which is `sortspec.md` file under the hood). Create such note at any location in your -vault if you don't have one. - -Each time after creating or updating the sorting specification click the [ribbon icon](#ribbon_icon) to parse the -specification and actually apply the custom sorting in File Explorer - -Click the [ribbon icon](#ribbon_icon) again to disable custom sorting and switch back to the standard Obsidian sorting. - -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 - -### Simple case 1: in root folder sort entries alphabetically treating folders and files equally - -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 - -```yaml ---- -sorting-spec: | - target-folder: / - < a-z ---- -``` -(View or download the raw content of [sortspec.md](./docs/examples/1/sortspec.md?plain=1) file of this example) - -which can result in: - -![Simplest example](./docs/svg/simplest-example.svg) - -### Simple case 2: impose manual order of some items in root folder - -The specification here lists items (files and folders) by name in the desired order - -Notice, that only a subset of items was listed. Unlisted items go after the specified ones, if the specification -doesn't say otherwise - -```yaml ---- -sorting-spec: | - target-folder: / - Note 1 - Z Archive - Some note - Some folder ---- -``` - -produces: - -![Simplest example](./docs/svg/simplest-example-2.svg) - -### Example 3: In root folder, let files go first and folders get pushed to the bottom - -Files go first, sorted by modification date descending (newest note in the top) - -Then go folders, sorted in reverse alphabetical order - -> IMPORTANT: Again, indentation matters in all of the examples. Notice that the order specification `< modified` for -> the `/:files` and the order `> a-z` for `/folders` are indented by one more space. The indentation says the order -> applies -> to the group and not to the 'target-folder' directly. -> -> And yes, each group can have a different order in the same parent folder - -```yaml ---- -sorting-spec: | - target-folder: / - /:files - < modified - /folders - > a-z ---- -``` - -will order items as: - -![Files go first example](./docs/svg/files-go-first.svg) - -### Example 4: In root folder, pin a focus note, then Inbox folder, and push archive to the bottom - -The specification below says: - -- first go items which name starts with 'Focus' (e.g. the notes to pin to the top) - - notice the usage of '...' wildcard -- then goes an item named 'Inbox' (my Inbox folder) -- then go all items not matching any of the above or below rules/names/patterns - - the special symbol `%` has that meaning -- then, second to the bottom goes the 'Archive' (a folder which doesn't need focus) -- and finally, in the very bottom, the `sortspec.md` file, which probably contains this sorting specification ;-) - -```yaml ---- -sorting-spec: | - target-folder: . - Focus... - Inbox - % - Archive - sortspec ---- -``` - -and the result will be: - -![Result of the example](./docs/svg/pin-focus-note.svg) - -> Remarks for the `target-folder:` -> -> In this example the dot '.' symbol was used `target-folder: .` which means _apply the sorting specification to the -folder which contains the note with the specification_. -> -> If the `target-folder:` line is omitted, the specification will be applied to the parent folder of the note, which has -> the same effect as `target-folder: .` - -### Example 5: P.A.R.A. method example - -The P.A.R.A. system for organizing digital information is based on the four specifically named folders ordered as in the -acronym: Projects — Areas — Resources — Archives - -To put folders in the desired order you can simply list them by name in the needed sequence: - -```yaml ---- -sorting-spec: | - target-folder: / - Projects - Areas - Responsibilities - Archive ---- -``` - -(View or download the raw content of [sortspec.md](./docs/examples/5/sortspec.md?plain=1) file of this example) - -which will have the effect of: - -![Result of the example](./docs/svg/p_a_r_a.svg) - -### Example 6: P.A.R.A. example with smart syntax - -Instead of listing full names of folders or notes, you can use the prefix or suffix of prefix+suffix notation with the -special syntax of '...' which acts as a wildcard here, matching any sequence of characters: - -```yaml ---- -sorting-spec: | - target-folder: / - Pro... - A...s - Res...es - ...ive ---- -``` - -It will give exactly the same order as in previous example: - -![Result of the example](./docs/svg/p_a_r_a.svg) - -``` -REMARK: the wildcard expression '...' can be used only once per line -``` - -### Example 7: Apply the same sorting rules to two folders - -Let's tell a few folders to sort their child notes and child folders by created date reverse order (newer go first) - -```yaml ---- -sorting-spec: | - target-folder: Some subfolder - target-folder: Archive - target-folder: Archive/2021/Completed projects - > created ---- -``` - -No visualization for this example needed - -### Example 8: Specify rules for multiple folders - -The specification can contain rules and orders for more than one folder - -Personally I find convenient to keep sorting specification of all folders in a vault in a single place, e.g. in a -dedicated note Inbox/Inbox.md - -```yaml ---- -sorting-spec: | - target-folder: / - Pro... - Archive - - target-folder: Projects - Top Secret - - target-folder: Archive - > a-z ---- -``` - -will have the effect of: - -![Result of the example](./docs/svg/multi-folder.svg) - -### Example 9: Sort by numerical suffix - -This is interesting. - -Sorting by numerical prefix is easy and doesn't require any additional plugin in Obsidian. -At the same time sorting by numerical suffix is not feasible without a plugin like this one. - -Use the specification like below to order notes in 'Inbox' subfolder of 'Data' folder by the numerical suffix indicated -by the 'part' token (an arbitrary example) - -```yaml ---- -sorting-spec: | - target-folder: Data/Inbox - ... part \d+ - < a-z ---- -``` - -the line `... part \d+` says: group all notes and folders with name ending with 'part' followed by a number. Then order -them by the number. And for clarity the subsequent (indented) line is added ` < a-z` which sets the order to -alphabetical ascending. - -The effect is: - -![Order by numerical suffix](./docs/svg/by-suffix.svg) - -### Example 10: Sample book structure with Roman numbered chapters - -Roman numbers are also supported. This example uses the `\R+` token in connection with the wildcard `...` - -The line `Chapter \.R+ ...` says: notes (or folders) with a name starting with 'Chapter ' followed by a Roman number (e.g. I, or iii or x) should be grouped. -Then ` < a-z` (the leading space indentation is important) tells to use ascending order by that number (alphabetical is equivalent to ascending for numbers) - -```yaml ---- -sorting-spec: | - target-folder: Book - Preface - Chapter \R+ ... - < a-z - Epi... ---- -``` - -it gives: - -![Book - Roman chapters](./docs/svg/roman-chapters.svg) - -### Example 11: Sample book structure with compound Roman number suffixes - -Roman compound numbers are also supported. This example uses the `\.R+` token (a Roman compound number with '.' as separator) in connection with the wildcard `...` (and the important SPACE inbetween). - -The line `... \.R+` says: notes (or folders) with a name ending with a compound Roman number (e.g. I, or i.iii or iv.vii.x) should be grouped with ascending order by that compound number (no additional specification of sorting defaults to alphabetical or ascending for numbers) - -```yaml ---- -sorting-spec: | - target-folder: Research pub - Summ... - ... \.R+ - Final... ---- -``` - -the result is: - -![Book - Roman compound suffixes](./docs/svg/roman-suffix.svg) +## Basic scenario: set the custom sorting order for a specific folder -### Example 12: Apply same sorting to all folders in the vault +Create a new note named `sortspec` in the folder for which you want to configure the sorting -Apply the same advanced modified date sorting to all folders in the Vault. The advanced modified sorting treats the folders - and files equally (which is different from the standard Obsidian sort, which groups folders in the top of File Explorer) - The modified date for a folder is derived from its newest direct child file (if any), otherwise a folder is considered old - -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_ +In the top of the new note put the following YAML front matter text: ```yaml --- sorting-spec: | - target-folder: /* - > advanced modified ---- -``` - -### 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 ---- -``` - -### Example 14: Grouping and sorting by metadata value - -Notes can contain metadata, let me use the example inspired by the [Feature Request #23](https://github.com/SebastianMC/obsidian-custom-sort/issues/23). -Namely, someone can create notes when reading a book and use the `Pages` metadata field. In that field s/he enters page(s) number(s) of the book, for reference. - -For example: - -```yaml ---- -Pages: 6 -... + order-desc: a-z --- ``` -or +Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png)) to tell the plugin to read the sorting specification and apply it. +The ribbon icon should turn (![Active](./docs/icons/icon-active.png)) and the sorting should be applied to the folder -```yaml ---- -Pages: 7,8 -... ---- -``` +!!! **Done!** !!! -or +You should see the files and sub-folders in your folder sorted in reverse alphabetical order, folders and files intermixed -```yaml ---- -Pages: 12-15 -... ---- -``` +An illustrative image which shows the reverse alphabetical order applied to the root folder of some vault: -Using this plugin you can sort notes by the value of the specific metadata, for example: +![Basic example](./docs/svg/simplest-example-3.svg) -```yaml ---- -sorting-spec: | - target-folder: Remarks from 'The Little Prince' book - < a-z by-metadata: Pages ---- -``` +### Remarks -In that approach, the notes containing the metadata `Pages` will go first, sorted alphabetically by the value of that metadata. -The remaining notes (not having the metadata) will go below, sorted alphabetically by default. +> Remarks: +> - your new `sortspec` note should [look like this](./docs/examples/basic/sortspec.md?plain=1) except for the syntax highlighting, which could differ +> - you will notice that the folders and files are treated equally and thus intermixed +> - the behavior depends on what files and subfolders you have in your folder +> - changing the sorting order via the standard Obsidian UI button won't affect your folder, unless... +> - ...unless you deactivate the custom sorting via clicking the ribbon button to make it (![Inactive](./docs/icons/icon-inactive.png)) +> - for clarity: the underlying file of the note `sortspec` is obviously `sortspec.md` +> - in case of troubles refer to the [TL;DR section of advanced README.md](./advanced-README.md#tldr-usage) +> - feel free to experiment! The plugin works in a non-destructive fashion, and it doesn't modify the content of your vault. +> It only changes the order in which the files and folders are displayed in File Explorer +> - indentation matters in YAML -> the two leading spaces in ` order-desc: a-z` are intentional and required +> - this common example only touches the surface of the rich capabilities of this custom sorting plugin. For more details go to [advanced version of README.md](./advanced-README.md) -In the above example the syntax `by-metadata: Pages` was used to tell the plugin about the metadata field name for sorting. -The specified sorting `< a-z` is obviously alphabetical, and in this specific context it tells to sort by the value of the specified metadata (and not by the note or folder name). +## Basic automatic sorting methods -In a more advanced fine-tuned approach you can explicitly group notes having some metadata and sort by that (or other) metadata: +The list of basic automatic sorting orders includes: +- ` order-asc: a-z` - **alphabetical order**, aka natural + - 'a' goes before 'z' and numbers are treated specifically and 2 goes before 11 +- ` order-desc: a-z` - **reverse alphabetical order**, aka reverse natural, aka descending alphabetical + - 'z' goes before 'a' and numbers are treated specifically and 11 goes before 2 +- ` order-asc: true a-z` - **true alphabetical order** + - 'a' goes before 'z' and numbers are treated as texts and 11 goes before 2 +- ` order-desc: true a-z` - **true reverse alphabetical order**, aka descending true alphabetical + - 'z' goes before 'a' and numbers are treated as texts and 11 goes before 2 +- ` order-asc: created` - **by creation date** + - the oldest notes go first. Sub-folders pushed to the top, alphabetically +- ` order-desc: created` - **by creation date, descending** + - the newest notes go first. Sub-folders pushed to the bottom, alphabetically +- ` order-asc: advanced created` - **by creation date, also for folders** + - the oldest notes and sub-folders go first + - for sub-folders the creation date of the oldest contained note is taken as folder's creation date + - sub-folders not containing any notes are pushed to the top, alphabetically +- ` order-desc: advanced created` - **by creation date, descending, also for folders** + - the newest notes and sub-folders go first + - for sub-folders the creation date of the newest contained note is taken as folder's creation date + - sub-folders not containing any notes are pushed to the bottom, alphabetically +- ` order-asc: modified` - **by modification date** + - the most dusty notes go first. Sub-folders pushed to the top, alphabetically +- ` order-desc: modified` - **by modification date, descending** + - the most recently modified notes go first. Sub-folders pushed to the bottom, alphabetically +- ` order-asc: advanced modified` - **by modification date, also for folders** + - the most dusty notes and sub-folders go first + - for sub-folders the modification date of the most dusty contained note is taken as folder's modification date + - sub-folders not containing any notes are pushed to the top, alphabetically +- ` order-desc: advanced modified` - **by modification date, descending, also for folders** + - the most recently modified notes and sub-folders go first + - for sub-folders the modification date of the most recently modified contained note is taken as folder's modification date + - sub-folders not containing any notes are pushed to the bottom, alphabetically -```yaml ---- -sorting-spec: | - target-folder: Remarks from 'The Little Prince' book - with-metadata: Pages - < a-z by-metadata: Pages - ... - > modified ---- -``` +> Remark: +> In the above list the `-asc` stems from `Ascending` and `-desc` stems from `Descending` -In the above example the syntax `with-metadata: Pages` was used to tell the plugin about the metadata field name for grouping. -The specified sorting `< a-z` is obviously alphabetical, and in this specific context it tells to sort by the value of the specified metadata (and not by the note or folder name). -Then the remaining notes (not having the `Pages` metadata) are sorted by modification date descending. - -> NOTE -> -> The grouping and sorting by metadata is not refreshed automatically after change of the metadata in note(s) to avoid impact on Obsidian performance. -> After editing of metadata of some note(s) you have to explicitly click the plugin ribbon button to refresh the sorting. Or issue the command `sort on`. Or close and reopen the vault. Or restart Obsidian. -> This behavior is intentionally different from other grouping and sorting rules, which stay active and up-to-date once enabled. - -> NOTE -> -> For folders, metadata of their 'folder note' is scanned (if present) - -> NOTE -> -> The `with-metadata:` keyword can be used with other specifiers like `/:files with-metadata: Pages` or `/folders with-metadata: Pages` -> If the metadata name is omitted, the default `sort-index-value` metadata name is assumed. - -## Alphabetical, Natural and True Alphabetical sorting orders - -The 'A-Z' sorting (visible in Obsidian UI of file explorer) at some point before the 1.0.0 release of Obsidian actually became the so-called 'natural' sort order. -For explanation of the term go to [Natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order) on Wikipedia. -The plugin follows the convention and the sorting specified by `< a-z` or `> a-z` triggers the _'natural sort order'_. - -To allow the true alphabetical sort order, as suggested by the ticket [27: Not alphanumeric, but natural sort order?](https://github.com/SebastianMC/obsidian-custom-sort/issues/27) -a distinct syntax was introduced: `< true a-z` and `> true a-z` - -What is the difference? -Using the example from the mentioned ticket: the items '0x01FF', '0x02FF' and '0x0200' sorted in _natural order_ go as: -- 0x01FF -> the number 01 in the text is recognized -- 0x02FF -> the number 02 in the text is recognized -- 0x0200 -> the number 0200 in the text is recognized and it causes the third position of the item, because 0200 > 02 - -The same items when sorted in _true alphabetical_ order go as: -- 0x01FF -- 0x0200 -- 0x02FF -> the character 'F' following '2' goes after the character '0', that's why 0x02FF follows the 0x0200 - -You can use the order `< true a-z` or `> true a-z` to trigger the true alphabetical sorting, like in the ticket: -```yaml -sorting-spec: | - target-folder: MaDo/... - > true a-z - target-folder: MaDo/Sandbox/SortingBug - < true a-z -``` +## Manual sorting -## Location of sorting specification YAML entry - -You can keep the custom sorting specifications in any of the following locations (or in all of them): - -- in the front matter of the `sortspec` note (which is the `sortspec.md` file under the hood) - - you can keep one global `sortspec` note or one `sortspec` in each folder for which you set up a custom sorting - - YAML in front matter of all existing `sortspec` notes is scanned, so feel free to choose your preferred approach -- in the front matter of the - so called - _folder note_. For instance '/References/References.md' - - the 'folder note' is a concept of note named exactly as its parent folder, e.g. `references` note ( - actually `references.md` file) residing inside the `/references/` folder - - there are popular Obsidian plugins which allow convenient access and editing of folder note, plus hiding it in the - notes list -- in the front matter of a **designated note** configured in setting - - in settings page of the plugin in obsidian you can set the exact path to the designated note - - by default, it is `Inbox/Inbox.md` - - feel free to adjust it to your preferences - - primary intention is to use this setting as the reminder note to yourself, to easily locate the note containing - sorting specifications for the vault - -A sorting specification for a folder has to reside in a single YAML entry in one of the listed locations. -At the same time, you can put specifications for different target folders into different notes, according to your -preference. -My personal approach is to keep the sorting specification for all desired folders in a single note ( -e.g. `Inbox/Inbox.md`). And for clarity, I keep the name of that designated note in the plugin settings, for easy -reference. - - +The **manual ordering of notes and folders** is also done via the sorting configuration. +Refer to the [TL;DR section of advanced README.md](./advanced-README.md#tldr-usage) for examples and instructions ## Ribbon icon @@ -569,24 +116,12 @@ Click the ribbon icon to toggle the plugin between enabled and suspended states. States of the ribbon icon: - ![Inactive](./docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. - - Click to enable and apply custom sorting. - - Note: parsing of the custom sorting specification happens after clicking the icon. If the specification contains - errors, they will show up in the notice baloon and also in developer console. - ![Active](./docs/icons/icon-active.png) Plugin active, custom sorting applied. - - Click to suspend and return to the standard Obsidian sorting in File Explorer. - ![Error](./docs/icons/icon-error.png) Syntax error in custom sorting configuration. - - Fix the problem in specification and click the ribbon icon to re-enable custom sorting. - - If syntax error is not fixed, the notice baloon with show error details. Syntax error details are also visible in - the developer console - ![General Error](./docs/icons/icon-general-error.png) Plugin suspended. General error. - - File Explorer not available or other type of general error - - File Explorer is a core Obsidian plugin (named __Files__) and thus can be disabled in Obsidian settings - - Some community plugins (like __MAKE.md__) also disable the File Explorer by default - - See obsidinan developer console for detailed error message - - To fix the problem, enable the File Explorer (in Obsidian or in the community plugin responsible for hididing it) - ![Sorting not applied](./docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. - - This can happen when reinstalling the plugin and in similar cases - - Click the ribbon icon twice to re-enable the custom sorting. + +For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](./advanced-README.md#ribbon-icon) ## Installing the plugin @@ -596,41 +131,7 @@ The plugin could and should be installed from the official Obsidian Community Pl or directly in the Obsidian app itself. Search the plugin by its name 'CUSTOM FILE EXPLORER SORTING' -### Installing the plugin using BRAT - -> NOTE -> -> BRAT installation is supported yet no longer needed since reaching the official list at https://obsidian.md/plugins - -1. Install the BRAT plugin - 1. Open `Settings` -> `Community Plugins` - 2. Disable restricted (formerly 'safe') mode, if enabled - 3. *Browse*, and search for "BRAT" - 4. Install the latest version of **Obsidian 42 - BRAT** -2. Open BRAT settings (`Settings` -> `Obsidian 42 - BRAT`) - 1. Scroll to the `Beta Plugin List` section - 2. `Add Beta Plugin` - 3. Specify this repository: `SebastianMC/obsidian-custom-sort` -3. Enable the `Custom File Explorer sorting` plugin (`Settings` -> `Community Plugins`) - -### Manually installing the plugin - -> NOTE -> -> Manual installation is no longer needed since reaching the official list at https://obsidian.md/plugins - -1. Go to Github for releases: https://github.com/SebastianMC/obsidian-custom-sort/releases -2. Download the latest (or desired) Release from the Releases section of the GitHub Repository -3. Copy the downloaded files `main.js` and `manifest.json` over to your - vault `VaultFolder/.obsidian/plugins/custom-sort/`. - - you might need to manually create the `/custom-sort/` folder under `VaultFolder/.obsidian/plugins/` -4. Reload Obsidian -5. If prompted about Restricted (formerly 'Safe') Mode, you can disable restricted mode and enable the plugin. - -Otherwise, go to `Settings` -> `Community plugins`, make sure restricted mode is off and enable the plugin from - there. - -> Note: The `.obsidian` folder may be hidden. -> On macOS, you should be able to press Command+Shift+Dot to show the folder in Finder. +> For other installation methods refer to [Installing the plugin section of advanced-README.md](./advanced-README.md#installing-the-plugin) ## Credits diff --git a/advanced-README.md b/advanced-README.md new file mode 100644 index 000000000..5131b8d9f --- /dev/null +++ b/advanced-README.md @@ -0,0 +1,643 @@ +# Advanced version of README.md for advanced users + +The [simplified README.md is here](./README.md) + +## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) + +Take full control of the order of your notes and folders: + +- treat folders and files equally or distinctively, you decide +- fine-grained folder-level or even notes-group-level specification +- support for fully manual order + - list notes and folders names explicitly, or use prefixes or suffixes only + - wildcard names matching supported +- group and sort notes and folders by notes custom metadata +- support for automatic sorting by standard and non-standard rules +- mixing manual and automatic ordering also supported +- order by compound numbers in prefix, in suffix (e.g date in suffix) or inbetween +- Roman numbers support, also compound Roman numbers +- grouping by prefix or suffix or prefix and suffix + - different sorting rules per group even inside the same folder +- simple to use yet versatile configuration options +- order configuration stored directly in your note(s) front matter + - use a dedicated `sorting-spec:` key in YAML +- 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 + +- [TL;DR Usage](#tldr-usage) + - [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) + - [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 5: P.A.R.A. method example](#example-5-para-method-example) + - [Example 6: P.A.R.A. example with smart syntax](#example-6-para-example-with-smart-syntax) + - [Example 7: Apply the same sorting rules to two folders](#example-7-apply-the-same-sorting-rules-to-two-folders) + - [Example 8: Specify rules for multiple folders](#example-8-specify-rules-for-multiple-folders) + - [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 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) + - [Example 14: Grouping and sorting by metadata value](#example-14-grouping-and-sorting-by-metadata-value) +- [Alphabetical, Natural and True Alphabetical sorting orders](#alphabetical-natural-and-true-alphabetical-sorting-orders) +- [Location of sorting specification YAML entry](#location-of-sorting-specification-yaml-entry) +- [Ribbon icon](#ribbon-icon) +- [Installing the plugin](#installing-the-plugin) + - [From the official Obsidian Community Plugins page](#from-the-official-obsidian-community-plugins-page) + - [Installing the plugin using BRAT](#installing-the-plugin-using-brat) + - [Manually installing the plugin](#manually-installing-the-plugin) +- [Credits](#credits) + +## TL;DR Usage + +For full version of the manual go to [manual](./docs/manual.md) and [syntax-reference](./docs/syntax-reference.md) + +> **Quickstart** +> +> 1. Download the **RAW CONTENT** of [sortspec.md](./docs/examples/quickstart/sortspec.md?plain=1) file and put it in any folder of your vault, +can be the root folder. Ensure the exact file name is `sortspec.md`. That file contains a basic custom sorting specification under the `sorting-spec:` name in the YAML frontmatter. +> > IMPORTANT: follow the above link to 'sortspec.md' and download (or copy & paste) the __RAW__ content of that file, not the HTML displayed by github. +> > Afterwards double check that the content of `sortspec.md` file is not an HTML and: +> > - it starts exactly with the line `---` +> > - it and ends with the line `---` followed by one or two blank lines +> > - indentation is correct (consult images below). In YAML the indentation matters. +> > +> > In other words, ensure, that the final `sortspec.md` file in your vault (which is the `sortspec` Obsidian note) looks exactly like below: +> > ![sortspec.md](./docs/img/sortspec-md-bright.jpg) +> > +> > or if you are a fan of dark mode (line numbers shown for clarity only, they aren't part of the file content): +> > +> > ![sortspec.md](./docs/img/sortspec-md-dark.jpg) +> 2. Enable the plugin in obsidian. +> +> 3. Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png)) 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 +(![Active](./docs/icons/icon-active.png)) and reordering +of items in root vault folder to reverse alphabetical with folders and files treated equally. +> - The notification balloon should confirm success: ![Success](./docs/icons/parsing-succeeded.png) +> 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 ![Not applied](./docs/icons/icon-not-applied.png) +and ![Error](./docs/icons/icon-error.png). 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. + +For simplicity (if you are examining the plugin for the first time) copy and paste the below YAML snippets to the front +matter of the `sortspec` note (which is `sortspec.md` file under the hood). Create such note at any location in your +vault if you don't have one. + +Each time after creating or updating the sorting specification click the [ribbon icon](#ribbon_icon) to parse the +specification and actually apply the custom sorting in File Explorer + +Click the [ribbon icon](#ribbon_icon) again to disable custom sorting and switch back to the standard Obsidian sorting. + +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 + +### Simple case 1: in root folder sort entries alphabetically treating folders and files equally + +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 + +```yaml +--- +sorting-spec: | + target-folder: / + < a-z +--- +``` +(View or download the raw content of [sortspec.md](./docs/examples/1/sortspec.md?plain=1) file of this example) + +which can result in: + +![Simplest example](./docs/svg/simplest-example.svg) + +### Simple case 2: impose manual order of some items in root folder + +The specification here lists items (files and folders) by name in the desired order + +Notice, that only a subset of items was listed. Unlisted items go after the specified ones, if the specification +doesn't say otherwise + +```yaml +--- +sorting-spec: | + target-folder: / + Note 1 + Z Archive + Some note + Some folder +--- +``` + +produces: + +![Simplest example](./docs/svg/simplest-example-2.svg) + +### Example 3: In root folder, let files go first and folders get pushed to the bottom + +Files go first, sorted by modification date descending (newest note in the top) + +Then go folders, sorted in reverse alphabetical order + +> IMPORTANT: Again, indentation matters in all of the examples. Notice that the order specification `< modified` for +> the `/:files` and the order `> a-z` for `/folders` are indented by one more space. The indentation says the order +> applies +> to the group and not to the 'target-folder' directly. +> +> And yes, each group can have a different order in the same parent folder + +```yaml +--- +sorting-spec: | + target-folder: / + /:files + < modified + /folders + > a-z +--- +``` + +will order items as: + +![Files go first example](./docs/svg/files-go-first.svg) + +### Example 4: In root folder, pin a focus note, then Inbox folder, and push archive to the bottom + +The specification below says: + +- first go items which name starts with 'Focus' (e.g. the notes to pin to the top) + - notice the usage of '...' wildcard +- then goes an item named 'Inbox' (my Inbox folder) +- then go all items not matching any of the above or below rules/names/patterns + - the special symbol `%` has that meaning +- then, second to the bottom goes the 'Archive' (a folder which doesn't need focus) +- and finally, in the very bottom, the `sortspec.md` file, which probably contains this sorting specification ;-) + +```yaml +--- +sorting-spec: | + target-folder: . + Focus... + Inbox + % + Archive + sortspec +--- +``` + +and the result will be: + +![Result of the example](./docs/svg/pin-focus-note.svg) + +> Remarks for the `target-folder:` +> +> In this example the dot '.' symbol was used `target-folder: .` which means _apply the sorting specification to the +folder which contains the note with the specification_. +> +> If the `target-folder:` line is omitted, the specification will be applied to the parent folder of the note, which has +> the same effect as `target-folder: .` + +### Example 5: P.A.R.A. method example + +The P.A.R.A. system for organizing digital information is based on the four specifically named folders ordered as in the +acronym: Projects — Areas — Resources — Archives + +To put folders in the desired order you can simply list them by name in the needed sequence: + +```yaml +--- +sorting-spec: | + target-folder: / + Projects + Areas + Responsibilities + Archive +--- +``` + +(View or download the raw content of [sortspec.md](./docs/examples/5/sortspec.md?plain=1) file of this example) + +which will have the effect of: + +![Result of the example](./docs/svg/p_a_r_a.svg) + +### Example 6: P.A.R.A. example with smart syntax + +Instead of listing full names of folders or notes, you can use the prefix or suffix of prefix+suffix notation with the +special syntax of '...' which acts as a wildcard here, matching any sequence of characters: + +```yaml +--- +sorting-spec: | + target-folder: / + Pro... + A...s + Res...es + ...ive +--- +``` + +It will give exactly the same order as in previous example: + +![Result of the example](./docs/svg/p_a_r_a.svg) + +``` +REMARK: the wildcard expression '...' can be used only once per line +``` + +### Example 7: Apply the same sorting rules to two folders + +Let's tell a few folders to sort their child notes and child folders by created date reverse order (newer go first) + +```yaml +--- +sorting-spec: | + target-folder: Some subfolder + target-folder: Archive + target-folder: Archive/2021/Completed projects + > created +--- +``` + +No visualization for this example needed + +### Example 8: Specify rules for multiple folders + +The specification can contain rules and orders for more than one folder + +Personally I find convenient to keep sorting specification of all folders in a vault in a single place, e.g. in a +dedicated note Inbox/Inbox.md + +```yaml +--- +sorting-spec: | + target-folder: / + Pro... + Archive + + target-folder: Projects + Top Secret + + target-folder: Archive + > a-z +--- +``` + +will have the effect of: + +![Result of the example](./docs/svg/multi-folder.svg) + +### Example 9: Sort by numerical suffix + +This is interesting. + +Sorting by numerical prefix is easy and doesn't require any additional plugin in Obsidian. +At the same time sorting by numerical suffix is not feasible without a plugin like this one. + +Use the specification like below to order notes in 'Inbox' subfolder of 'Data' folder by the numerical suffix indicated +by the 'part' token (an arbitrary example) + +```yaml +--- +sorting-spec: | + target-folder: Data/Inbox + ... part \d+ + < a-z +--- +``` + +the line `... part \d+` says: group all notes and folders with name ending with 'part' followed by a number. Then order +them by the number. And for clarity the subsequent (indented) line is added ` < a-z` which sets the order to +alphabetical ascending. + +The effect is: + +![Order by numerical suffix](./docs/svg/by-suffix.svg) + +### Example 10: Sample book structure with Roman numbered chapters + +Roman numbers are also supported. This example uses the `\R+` token in connection with the wildcard `...` + +The line `Chapter \.R+ ...` says: notes (or folders) with a name starting with 'Chapter ' followed by a Roman number (e.g. I, or iii or x) should be grouped. +Then ` < a-z` (the leading space indentation is important) tells to use ascending order by that number (alphabetical is equivalent to ascending for numbers) + +```yaml +--- +sorting-spec: | + target-folder: Book + Preface + Chapter \R+ ... + < a-z + Epi... +--- +``` + +it gives: + +![Book - Roman chapters](./docs/svg/roman-chapters.svg) + +### Example 11: Sample book structure with compound Roman number suffixes + +Roman compound numbers are also supported. This example uses the `\.R+` token (a Roman compound number with '.' as separator) in connection with the wildcard `...` (and the important SPACE inbetween). + +The line `... \.R+` says: notes (or folders) with a name ending with a compound Roman number (e.g. I, or i.iii or iv.vii.x) should be grouped with ascending order by that compound number (no additional specification of sorting defaults to alphabetical or ascending for numbers) + +```yaml +--- +sorting-spec: | + target-folder: Research pub + Summ... + ... \.R+ + Final... +--- +``` + +the result is: + +![Book - Roman compound suffixes](./docs/svg/roman-suffix.svg) + +### Example 12: Apply same sorting to all folders in the vault + +Apply the same advanced modified date sorting to all folders in the Vault. The advanced modified sorting treats the folders + and files equally (which is different from the standard Obsidian sort, which groups folders in the top of File Explorer) + The modified date for a folder is derived from its newest direct child file (if any), otherwise a folder is considered old + +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: /* + > advanced modified +--- +``` + +### 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 +--- +``` + +### Example 14: Grouping and sorting by metadata value + +Notes can contain metadata, let me use the example inspired by the [Feature Request #23](https://github.com/SebastianMC/obsidian-custom-sort/issues/23). +Namely, someone can create notes when reading a book and use the `Pages` metadata field. In that field s/he enters page(s) number(s) of the book, for reference. + +For example: + +```yaml +--- +Pages: 6 +... +--- +``` + +or + +```yaml +--- +Pages: 7,8 +... +--- +``` + +or + +```yaml +--- +Pages: 12-15 +... +--- +``` + +Using this plugin you can sort notes by the value of the specific metadata, for example: + +```yaml +--- +sorting-spec: | + target-folder: Remarks from 'The Little Prince' book + < a-z by-metadata: Pages +--- +``` + +In that approach, the notes containing the metadata `Pages` will go first, sorted alphabetically by the value of that metadata. +The remaining notes (not having the metadata) will go below, sorted alphabetically by default. + +In the above example the syntax `by-metadata: Pages` was used to tell the plugin about the metadata field name for sorting. +The specified sorting `< a-z` is obviously alphabetical, and in this specific context it tells to sort by the value of the specified metadata (and not by the note or folder name). + +In a more advanced fine-tuned approach you can explicitly group notes having some metadata and sort by that (or other) metadata: + +```yaml +--- +sorting-spec: | + target-folder: Remarks from 'The Little Prince' book + with-metadata: Pages + < a-z by-metadata: Pages + ... + > modified +--- +``` + +In the above example the syntax `with-metadata: Pages` was used to tell the plugin about the metadata field name for grouping. +The specified sorting `< a-z` is obviously alphabetical, and in this specific context it tells to sort by the value of the specified metadata (and not by the note or folder name). +Then the remaining notes (not having the `Pages` metadata) are sorted by modification date descending. + +> NOTE +> +> The grouping and sorting by metadata is not refreshed automatically after change of the metadata in note(s) to avoid impact on Obsidian performance. +> After editing of metadata of some note(s) you have to explicitly click the plugin ribbon button to refresh the sorting. Or issue the command `sort on`. Or close and reopen the vault. Or restart Obsidian. +> This behavior is intentionally different from other grouping and sorting rules, which stay active and up-to-date once enabled. + +> NOTE +> +> For folders, metadata of their 'folder note' is scanned (if present) + +> NOTE +> +> The `with-metadata:` keyword can be used with other specifiers like `/:files with-metadata: Pages` or `/folders with-metadata: Pages` +> If the metadata name is omitted, the default `sort-index-value` metadata name is assumed. + +## Alphabetical, Natural and True Alphabetical sorting orders + +The 'A-Z' sorting (visible in Obsidian UI of file explorer) at some point before the 1.0.0 release of Obsidian actually became the so-called 'natural' sort order. +For explanation of the term go to [Natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order) on Wikipedia. +The plugin follows the convention and the sorting specified by `< a-z` or `> a-z` triggers the _'natural sort order'_. + +To allow the true alphabetical sort order, as suggested by the ticket [27: Not alphanumeric, but natural sort order?](https://github.com/SebastianMC/obsidian-custom-sort/issues/27) +a distinct syntax was introduced: `< true a-z` and `> true a-z` + +What is the difference? +Using the example from the mentioned ticket: the items '0x01FF', '0x02FF' and '0x0200' sorted in _natural order_ go as: +- 0x01FF -> the number 01 in the text is recognized +- 0x02FF -> the number 02 in the text is recognized +- 0x0200 -> the number 0200 in the text is recognized and it causes the third position of the item, because 0200 > 02 + +The same items when sorted in _true alphabetical_ order go as: +- 0x01FF +- 0x0200 +- 0x02FF -> the character 'F' following '2' goes after the character '0', that's why 0x02FF follows the 0x0200 + +You can use the order `< true a-z` or `> true a-z` to trigger the true alphabetical sorting, like in the ticket: +```yaml +sorting-spec: | + target-folder: MaDo/... + > true a-z + target-folder: MaDo/Sandbox/SortingBug + < true a-z +``` + +## Location of sorting specification YAML entry + +You can keep the custom sorting specifications in any of the following locations (or in all of them): + +- in the front matter of the `sortspec` note (which is the `sortspec.md` file under the hood) + - you can keep one global `sortspec` note or one `sortspec` in each folder for which you set up a custom sorting + - YAML in front matter of all existing `sortspec` notes is scanned, so feel free to choose your preferred approach +- in the front matter of the - so called - _folder note_. For instance '/References/References.md' + - the 'folder note' is a concept of note named exactly as its parent folder, e.g. `references` note ( + actually `references.md` file) residing inside the `/references/` folder + - there are popular Obsidian plugins which allow convenient access and editing of folder note, plus hiding it in the + notes list +- in the front matter of a **designated note** configured in setting + - in settings page of the plugin in obsidian you can set the exact path to the designated note + - by default, it is `Inbox/Inbox.md` + - feel free to adjust it to your preferences + - primary intention is to use this setting as the reminder note to yourself, to easily locate the note containing + sorting specifications for the vault + +A sorting specification for a folder has to reside in a single YAML entry in one of the listed locations. +At the same time, you can put specifications for different target folders into different notes, according to your +preference. +My personal approach is to keep the sorting specification for all desired folders in a single note ( +e.g. `Inbox/Inbox.md`). And for clarity, I keep the name of that designated note in the plugin settings, for easy +reference. + + + +## Ribbon icon + +Click the ribbon icon to toggle the plugin between enabled and suspended states. + +States of the ribbon icon: + +- ![Inactive](./docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. + - Click to enable and apply custom sorting. + - Note: parsing of the custom sorting specification happens after clicking the icon. If the specification contains + errors, they will show up in the notice baloon and also in developer console. +- ![Active](./docs/icons/icon-active.png) Plugin active, custom sorting applied. + - Click to suspend and return to the standard Obsidian sorting in File Explorer. +- ![Error](./docs/icons/icon-error.png) Syntax error in custom sorting configuration. + - Fix the problem in specification and click the ribbon icon to re-enable custom sorting. + - If syntax error is not fixed, the notice baloon with show error details. Syntax error details are also visible in + the developer console +- ![General Error](./docs/icons/icon-general-error.png) Plugin suspended. General error. + - File Explorer not available or other type of general error + - File Explorer is a core Obsidian plugin (named __Files__) and thus can be disabled in Obsidian settings + - Some community plugins (like __MAKE.md__) also disable the File Explorer by default + - See obsidinan developer console for detailed error message + - To fix the problem, enable the File Explorer (in Obsidian or in the community plugin responsible for hididing it) +- ![Sorting not applied](./docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. + - This can happen when reinstalling the plugin and in similar cases + - Click the ribbon icon twice to re-enable the custom sorting. + +## Installing the plugin + +### From the official Obsidian Community Plugins page + +The plugin could and should be installed from the official Obsidian Community Plugins list at https://obsidian.md/plugins +or directly in the Obsidian app itself. +Search the plugin by its name 'CUSTOM FILE EXPLORER SORTING' + +### Installing the plugin using BRAT + +> NOTE +> +> BRAT installation is supported yet no longer needed since reaching the official list at https://obsidian.md/plugins + +1. Install the BRAT plugin + 1. Open `Settings` -> `Community Plugins` + 2. Disable restricted (formerly 'safe') mode, if enabled + 3. *Browse*, and search for "BRAT" + 4. Install the latest version of **Obsidian 42 - BRAT** +2. Open BRAT settings (`Settings` -> `Obsidian 42 - BRAT`) + 1. Scroll to the `Beta Plugin List` section + 2. `Add Beta Plugin` + 3. Specify this repository: `SebastianMC/obsidian-custom-sort` +3. Enable the `Custom File Explorer sorting` plugin (`Settings` -> `Community Plugins`) + +### Manually installing the plugin + +> NOTE +> +> Manual installation is no longer needed since reaching the official list at https://obsidian.md/plugins + +1. Go to Github for releases: https://github.com/SebastianMC/obsidian-custom-sort/releases +2. Download the latest (or desired) Release from the Releases section of the GitHub Repository +3. Copy the downloaded files `main.js` and `manifest.json` over to your + vault `VaultFolder/.obsidian/plugins/custom-sort/`. + - you might need to manually create the `/custom-sort/` folder under `VaultFolder/.obsidian/plugins/` +4. Reload Obsidian +5. If prompted about Restricted (formerly 'Safe') Mode, you can disable restricted mode and enable the plugin. + -Otherwise, go to `Settings` -> `Community plugins`, make sure restricted mode is off and enable the plugin from + there. + +> Note: The `.obsidian` folder may be hidden. +> On macOS, you should be able to press Command+Shift+Dot to show the folder in Finder. + +## Credits + +Thanks to [Nothingislost](https://github.com/nothingislost) for the monkey-patching ideas of File Explorer +in [obsidian-bartender](https://github.com/nothingislost/obsidian-bartender) + diff --git a/docs/examples/basic/sortspec.md b/docs/examples/basic/sortspec.md new file mode 100644 index 000000000..f4de2df7c --- /dev/null +++ b/docs/examples/basic/sortspec.md @@ -0,0 +1,4 @@ +--- +sorting-spec: | + order-desc: a-z +--- diff --git a/docs/svg/simplest-example-3.svg b/docs/svg/simplest-example-3.svg new file mode 100644 index 000000000..3616e62d1 --- /dev/null +++ b/docs/svg/simplest-example-3.svg @@ -0,0 +1,65 @@ + + + + + Produced by OmniGraffle 7.20\n2022-08-05 22:12:33 +0000 + + Simplest 2 + + Layer 1 + + + + + + Z note + + + + + XYZ archive folder + + + + + Some note + + + + + Some folder + + + + + A.11 folder + + + + + + + My + Vault + + + + + + + + + A.2 Note + + + + + + + + + + + From 9e87754eeec4b76ed99750fef8b4eb71f04e033e Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 25 Jan 2023 13:33:48 +0100 Subject: [PATCH 03/26] Documentation update --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index df5228a3d..858bd3a47 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ -# Simplified README.md - > This is a simple version of README which highlights the **basic scenario and most commonly used feature** > -> The [long and much more detailed README.md is here](./advanced-README.md) +> The [long and much more detailed advanced-README.md is here](./advanced-README.md) ## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) From c5ad33ea14564e9f2fffd070b24fab7d491f8b86 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 25 Jan 2023 13:46:32 +0100 Subject: [PATCH 04/26] Documentation update --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 858bd3a47..425146c6b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ > > The [long and much more detailed advanced-README.md is here](./advanced-README.md) +--- ## Freely arrange notes and folders in File Explorer (https://obsidian.md plugin) Take full control of the order of your notes and folders: @@ -24,6 +25,7 @@ Take full control of the order of your notes and folders: - 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 +--- ## Basic scenario: set the custom sorting order for a specific folder Create a new note named `sortspec` in the folder for which you want to configure the sorting @@ -48,6 +50,7 @@ An illustrative image which shows the reverse alphabetical order applied to the ![Basic example](./docs/svg/simplest-example-3.svg) +--- ### Remarks > Remarks: @@ -63,6 +66,7 @@ An illustrative image which shows the reverse alphabetical order applied to the > - indentation matters in YAML -> the two leading spaces in ` order-desc: a-z` are intentional and required > - this common example only touches the surface of the rich capabilities of this custom sorting plugin. For more details go to [advanced version of README.md](./advanced-README.md) +--- ## Basic automatic sorting methods The list of basic automatic sorting orders includes: From 0b5e5a2f6e0425f75a55666f2fb7fb8f704b53be Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Wed, 25 Jan 2023 18:49:50 +0000 Subject: [PATCH 05/26] Upgrade esbuild to v0.17.x (#47) --- esbuild.config.mjs | 52 ++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 8e2dad07d..b13282bae 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,6 @@ import esbuild from "esbuild"; import process from "process"; -import builtins from 'builtin-modules' +import builtins from "builtin-modules"; const banner = `/* @@ -9,34 +9,40 @@ if you want to view the source, please visit the github repository of this plugi */ `; -const prod = (process.argv[2] === 'production'); +const prod = (process.argv[2] === "production"); -esbuild.build({ +const context = await esbuild.context({ banner: { js: banner, }, - entryPoints: ['main.ts'], + entryPoints: ["main.ts"], bundle: true, external: [ - 'obsidian', - 'electron', - '@codemirror/autocomplete', - '@codemirror/collab', - '@codemirror/commands', - '@codemirror/language', - '@codemirror/lint', - '@codemirror/search', - '@codemirror/state', - '@codemirror/view', - '@lezer/common', - '@lezer/highlight', - '@lezer/lr', + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", ...builtins], - format: 'cjs', - watch: !prod, - target: 'es2018', + format: "cjs", + target: "es2018", logLevel: "info", - sourcemap: prod ? false : 'inline', + sourcemap: prod ? false : "inline", treeShaking: true, - outfile: 'main.js', -}).catch(() => process.exit(1)); + outfile: "main.js", +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} \ No newline at end of file diff --git a/package.json b/package.json index 3ada21950..6a00766d9 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "builtin-modules": "3.3.0", - "esbuild": "0.14.47", + "esbuild": "0.17.3", "obsidian": "latest", "tslib": "2.4.0", "typescript": "4.7.4" From 8e397797fcad0e5476caac2611f081120b56888c Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Thu, 2 Feb 2023 17:32:57 +0100 Subject: [PATCH 06/26] Merged from upstream --- esbuild.config.mjs | 1 - src/main.ts | 4 +- yarn.lock | 260 ++++++++++++++++++++++++--------------------- 3 files changed, 139 insertions(+), 126 deletions(-) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index c13d69319..4a45c889f 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -33,7 +33,6 @@ const context = await esbuild.context({ '@lezer/lr', ...builtins], format: 'cjs', - watch: !prod, target: 'es2018', logLevel: "info", sourcemap: prod ? false : 'inline', diff --git a/src/main.ts b/src/main.ts index d6d406d7d..56def40da 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,13 +32,15 @@ interface CustomSortPluginSettings { suspended: boolean statusBarEntryEnabled: boolean notificationsEnabled: boolean + allowRegexpInTargetFolder: boolean } const DEFAULT_SETTINGS: CustomSortPluginSettings = { additionalSortspecFile: '', suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install statusBarEntryEnabled: true, - notificationsEnabled: true + notificationsEnabled: true, + allowRegexpInTargetFolder: false } const SORTSPEC_FILE_NAME: string = 'sortspec.md' diff --git a/yarn.lock b/yarn.lock index a46287523..4a78e3665 100644 --- a/yarn.lock +++ b/yarn.lock @@ -290,6 +290,116 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@esbuild/android-arm64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz#35d045f69c9b4cf3f8efcd1ced24a560213d3346" + integrity sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ== + +"@esbuild/android-arm@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.3.tgz#4986d26306a7440078d42b3bf580d186ef714286" + integrity sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg== + +"@esbuild/android-x64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.3.tgz#a1928cd681e4055103384103c8bd34df7b9c7b19" + integrity sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A== + +"@esbuild/darwin-arm64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz#e4af2b392e5606a4808d3a78a99d38c27af39f1d" + integrity sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw== + +"@esbuild/darwin-x64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz#cbcbfb32c8d5c86953f215b48384287530c5a38e" + integrity sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA== + +"@esbuild/freebsd-arm64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz#90ec1755abca4c3ffe1ad10819cd9d31deddcb89" + integrity sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w== + +"@esbuild/freebsd-x64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz#8760eedc466af253c3ed0dfa2940d0e59b8b0895" + integrity sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg== + +"@esbuild/linux-arm64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz#13916fc8873115d7d546656e19037267b12d4567" + integrity sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g== + +"@esbuild/linux-arm@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz#15f876d127b244635ddc09eaaa65ae97bc472a63" + integrity sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ== + +"@esbuild/linux-ia32@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz#6691f02555d45b698195c81c9070ab4e521ef005" + integrity sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA== + +"@esbuild/linux-loong64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz#f77ef657f222d8b3a8fbd530a09e40976c458d48" + integrity sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA== + +"@esbuild/linux-mips64el@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz#fa38833cfc8bfaadaa12b243257fe6d19d0f6f79" + integrity sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ== + +"@esbuild/linux-ppc64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz#c157a602b627c90d174743e4b0dfb7630b101dbf" + integrity sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ== + +"@esbuild/linux-riscv64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz#7bf79614bd544bd932839b1fcff6cf1f8f6bdf1a" + integrity sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow== + +"@esbuild/linux-s390x@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz#6bb50c5a2613d31ce1137fe5c249ecadbecccdea" + integrity sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug== + +"@esbuild/linux-x64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz#aa140d99f0d9e0af388024823bfe4558d73fbbf9" + integrity sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg== + +"@esbuild/netbsd-x64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz#b6ae9948b03e4c95dc581c68358fb61d9d12a625" + integrity sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg== + +"@esbuild/openbsd-x64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz#cda007233e211fc9154324bfa460540cfc469408" + integrity sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA== + +"@esbuild/sunos-x64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz#f1385b092000c662d360775f3fad80943d2169c4" + integrity sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q== + +"@esbuild/win32-arm64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz#14e9dd9b1b55aa991f80c120fef0c4492d918801" + integrity sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A== + +"@esbuild/win32-ia32@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz#de584423513d13304a6925e01233499a37a4e075" + integrity sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg== + +"@esbuild/win32-x64@0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz#2f69ea6b37031b0d1715dd2da832a8ae5eb36e74" + integrity sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g== + "@eslint/eslintrc@^1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" @@ -1188,131 +1298,33 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -esbuild-android-64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.47.tgz#ef95b42c67bcf4268c869153fa3ad1466c4cea6b" - integrity sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g== - -esbuild-android-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.47.tgz#4ebd7ce9fb250b4695faa3ee46fd3b0754ecd9e6" - integrity sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ== - -esbuild-darwin-64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.47.tgz#e0da6c244f497192f951807f003f6a423ed23188" - integrity sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA== - -esbuild-darwin-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.47.tgz#cd40fd49a672fca581ed202834239dfe540a9028" - integrity sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw== - -esbuild-freebsd-64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.47.tgz#8da6a14c095b29c01fc8087a16cb7906debc2d67" - integrity sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ== - -esbuild-freebsd-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.47.tgz#ad31f9c92817ff8f33fd253af7ab5122dc1b83f6" - integrity sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ== - -esbuild-linux-32@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.47.tgz#de085e4db2e692ea30c71208ccc23fdcf5196c58" - integrity sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw== - -esbuild-linux-64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.47.tgz#2a9321bbccb01f01b04cebfcfccbabeba3658ba1" - integrity sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw== - -esbuild-linux-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.47.tgz#b9da7b6fc4b0ca7a13363a0c5b7bb927e4bc535a" - integrity sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw== - -esbuild-linux-arm@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.47.tgz#56fec2a09b9561c337059d4af53625142aded853" - integrity sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA== - -esbuild-linux-mips64le@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.47.tgz#9db21561f8f22ed79ef2aedb7bbef082b46cf823" - integrity sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg== - -esbuild-linux-ppc64le@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.47.tgz#dc3a3da321222b11e96e50efafec9d2de408198b" - integrity sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w== - -esbuild-linux-riscv64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.47.tgz#9bd6dcd3dca6c0357084ecd06e1d2d4bf105335f" - integrity sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g== - -esbuild-linux-s390x@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.47.tgz#a458af939b52f2cd32fc561410d441a51f69d41f" - integrity sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw== - -esbuild-netbsd-64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.47.tgz#6388e785d7e7e4420cb01348d7483ab511b16aa8" - integrity sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ== - -esbuild-openbsd-64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz#309af806db561aa886c445344d1aacab850dbdc5" - integrity sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw== - -esbuild-sunos-64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.47.tgz#3f19612dcdb89ba6c65283a7ff6e16f8afbf8aaa" - integrity sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ== - -esbuild-windows-32@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.47.tgz#a92d279c8458d5dc319abcfeb30aa49e8f2e6f7f" - integrity sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ== - -esbuild-windows-64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz#2564c3fcf0c23d701edb71af8c52d3be4cec5f8a" - integrity sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ== - -esbuild-windows-arm64@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.47.tgz#86d9db1a22d83360f726ac5fba41c2f625db6878" - integrity sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ== - -esbuild@0.14.47: - version "0.14.47" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.47.tgz#0d6415f6bd8eb9e73a58f7f9ae04c5276cda0e4d" - integrity sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA== +esbuild@0.17.3: + version "0.17.3" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.3.tgz#d9aa02a3bc441ed35f9569cd9505812ae3fcae61" + integrity sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g== optionalDependencies: - esbuild-android-64 "0.14.47" - esbuild-android-arm64 "0.14.47" - esbuild-darwin-64 "0.14.47" - esbuild-darwin-arm64 "0.14.47" - esbuild-freebsd-64 "0.14.47" - esbuild-freebsd-arm64 "0.14.47" - esbuild-linux-32 "0.14.47" - esbuild-linux-64 "0.14.47" - esbuild-linux-arm "0.14.47" - esbuild-linux-arm64 "0.14.47" - esbuild-linux-mips64le "0.14.47" - esbuild-linux-ppc64le "0.14.47" - esbuild-linux-riscv64 "0.14.47" - esbuild-linux-s390x "0.14.47" - esbuild-netbsd-64 "0.14.47" - esbuild-openbsd-64 "0.14.47" - esbuild-sunos-64 "0.14.47" - esbuild-windows-32 "0.14.47" - esbuild-windows-64 "0.14.47" - esbuild-windows-arm64 "0.14.47" + "@esbuild/android-arm" "0.17.3" + "@esbuild/android-arm64" "0.17.3" + "@esbuild/android-x64" "0.17.3" + "@esbuild/darwin-arm64" "0.17.3" + "@esbuild/darwin-x64" "0.17.3" + "@esbuild/freebsd-arm64" "0.17.3" + "@esbuild/freebsd-x64" "0.17.3" + "@esbuild/linux-arm" "0.17.3" + "@esbuild/linux-arm64" "0.17.3" + "@esbuild/linux-ia32" "0.17.3" + "@esbuild/linux-loong64" "0.17.3" + "@esbuild/linux-mips64el" "0.17.3" + "@esbuild/linux-ppc64" "0.17.3" + "@esbuild/linux-riscv64" "0.17.3" + "@esbuild/linux-s390x" "0.17.3" + "@esbuild/linux-x64" "0.17.3" + "@esbuild/netbsd-x64" "0.17.3" + "@esbuild/openbsd-x64" "0.17.3" + "@esbuild/sunos-x64" "0.17.3" + "@esbuild/win32-arm64" "0.17.3" + "@esbuild/win32-ia32" "0.17.3" + "@esbuild/win32-x64" "0.17.3" escalade@^3.1.1: version "3.1.1" From 8512f1b4cbe60e8e55142850d74493bd6581bb8f Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Mon, 6 Feb 2023 23:38:27 +0100 Subject: [PATCH 07/26] #50 - regexp and by-name matching support for target-folder - complete implementation - full unit tests coverage - NO update to documentation (yet to be done) --- src/custom-sort/custom-sort-types.ts | 1 + src/custom-sort/folder-matching-rules.spec.ts | 170 ++++++++++++++ src/custom-sort/folder-matching-rules.ts | 103 +++++++-- .../sorting-spec-processor.spec.ts | 206 +++++++++++++++++ src/custom-sort/sorting-spec-processor.ts | 209 ++++++++++++++---- src/main.ts | 9 +- 6 files changed, 628 insertions(+), 70 deletions(-) diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 7eadf36f6..f50f91678 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -64,6 +64,7 @@ export interface CustomSortGroup { } export interface CustomSortSpec { + // plays only informative role about the original parsed 'target-folder:' values targetFoldersPaths: Array // For root use '/' defaultOrder?: CustomSortOrder byMetadataField?: string // for 'by-metadata:' if the defaultOrder is by metadata alphabetical or reverse diff --git a/src/custom-sort/folder-matching-rules.spec.ts b/src/custom-sort/folder-matching-rules.spec.ts index 909c96991..ff9f84973 100644 --- a/src/custom-sort/folder-matching-rules.spec.ts +++ b/src/custom-sort/folder-matching-rules.spec.ts @@ -14,6 +14,43 @@ const createMockMatcherRichVersion = (): FolderWildcardMatching => return matcher } +const PRIO1 = 1 +const PRIO2 = 2 +const PRIO3 = 3 + +const createMockMatcherRichWithRegexpVersion = (usePriorities?: boolean): FolderWildcardMatching => { + const matcher: FolderWildcardMatching = createMockMatcherRichVersion() + let p: RegExp + const {row3, row4, row5, row6} = usePriorities ? + {row3: PRIO1, row4: PRIO1, row5: PRIO2, row6: PRIO3} + : + {row3:undefined, row4:undefined, row5:undefined, row6:undefined} + p = /.../; matcher.addRegexpDefinition(p, true, undefined, false, `r1`) + p = /^\/$/; matcher.addRegexpDefinition(p, false, undefined, false, `r2`) + p = /^Arc..ve$/; matcher.addRegexpDefinition(p, true, row3, false, `r3`) + p = /^Arc..ve$/; matcher.addRegexpDefinition(p, false, row4, false, `r4`) + p = /Reviews\/daily\/a\/.../; matcher.addRegexpDefinition(p, true, row5, false, `r5`) + p = /Reviews\/daily\/a\/*/; matcher.addRegexpDefinition(p, true, row6, false, `r6`) + return matcher +} + +/* +tests needed: +√ regexp-match by name works (ensure regexp input is the name) +√ regexp-match by path works (ensure regexp input is the path) +√ regexp-match by name works and has priority over wildcard +√ regexp-match by path works and has priority over wildcard +√ regexp-match by name vs by path is equal, order of definition matters (test two variants) +- priority /!!!: is higher over /!!: +- priority /!!: is higher over /!: +- priority /!: is higher over no-priority + - test adding priorities in all possible orders + - within the same priority the order of definition matters + +- edge case -> root folder, has no name, has specific path '/', matching by name should not work, only by path + - what is the root folder name ??? + */ + const createMockMatcherSimplestVersion = (): FolderWildcardMatching => { const matcher: FolderWildcardMatching = new FolderWildcardMatching() matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*') @@ -117,4 +154,137 @@ describe('folderMatch', () => { expect(result).toEqual({errorMsg: "Duplicate wildcard '*' specification for Archive/2019/*"}) }) + it('regexp-match by name works (order of regexp doesn\'t matter) case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) + matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) + matcher.addWildcardDefinition('/Reviews/*', `w1`) + // Path with leading / + const match1: SortingSpec | null = matcher.folderMatch('/Reviews/daily', 'daily') + // Path w/o leading / - this is how Obsidian supplies the path + const match2: SortingSpec | null = matcher.folderMatch('Reviews/daily', 'daily') + expect(match1).toBe('r2') + expect(match2).toBe('r2') + }) + it('regexp-match by name works (order of regexp doesn\'t matter) reversed case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) + matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) + matcher.addWildcardDefinition('/Reviews/*', `w1`) + // Path with leading / + const match1: SortingSpec | null = matcher.folderMatch('/Reviews/daily', 'daily') + // Path w/o leading / - this is how Obsidian supplies the path + const match2: SortingSpec | null = matcher.folderMatch('Reviews/daily', 'daily') + expect(match1).toBe('r2') + expect(match2).toBe('r2') + }) + it('regexp-match by path works (order of regexp doesn\'t matter) case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`) + matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`) + matcher.addWildcardDefinition('/Reviews/*', `w1`) + // Path with leading / + const match1: SortingSpec | null = matcher.folderMatch('/Reviews/daily', 'daily') + // Path w/o leading / - this is how Obsidian supplies the path + const match2: SortingSpec | null = matcher.folderMatch('Reviews/daily', 'daily') + expect(match1).toBe('w1') // The path-based regexp doesn't match the leading / + expect(match2).toBe('r1') + }) + it('regexp-match by path works (order of regexp doesn\'t matter) reversed case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^Reviews\/daily$/, true, undefined, false, `r2`) + matcher.addRegexpDefinition(/^Reviews\/daily$/, false, undefined, false, `r1`) + matcher.addWildcardDefinition('/Reviews/*', `w1`) + // Path with leading / + const match1: SortingSpec | null = matcher.folderMatch('/Reviews/daily', 'daily') + // Path w/o leading / - this is how Obsidian supplies the path + const match2: SortingSpec | null = matcher.folderMatch('Reviews/daily', 'daily') + expect(match1).toBe('w1') // The path-based regexp doesn't match the leading / + expect(match2).toBe('r1') + }) + it('regexp-match by path and name for root level - order of regexp decides - case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) + matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) + matcher.addWildcardDefinition('/Reviews/*', `w1`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('daily', 'daily') + expect(match).toBe('r2') + }) + it('regexp-match by path and name for root level - order of regexp decides - reversed case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r2`) + matcher.addRegexpDefinition(/^daily$/, false, undefined, false, `r1`) + matcher.addWildcardDefinition('/Reviews/*', `w1`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('daily', 'daily') + expect(match).toBe('r1') + }) + it('regexp-match priorities - order of definitions irrelevant - unique priorities - case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^freq\/daily$/, false, 3, false, `r1p3`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('freq/daily', 'daily') + expect(match).toBe('r1p3') + }) + it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, 3, false, `r1p3`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('freq/daily', 'daily') + expect(match).toBe('r1p3') + }) + it('regexp-match priorities - order of definitions irrelevant - duplicate priorities - case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3a`) + matcher.addRegexpDefinition(/^daily$/, true, 3, false, `r1p3b`) + matcher.addRegexpDefinition(/^daily$/, true, 2, false, `r2p2a`) + matcher.addRegexpDefinition(/^daily$/, true, 2, false, `r2p2b`) + matcher.addRegexpDefinition(/^daily$/, true, 1, false, `r3p1a`) + matcher.addRegexpDefinition(/^daily$/, true, 1, false, `r3p1b`) + matcher.addRegexpDefinition(/^daily$/, true, undefined, false, `r4pNone`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('daily', 'daily') + expect(match).toBe('r1p3b') + }) + it('regexp-match priorities - order of definitions irrelevant - unique priorities - reversed case A', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^freq\/daily$/, false, undefined, false, `r4pNone`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, 1, false, `r3p1`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, 2, false, `r2p2`) + matcher.addRegexpDefinition(/^freq\/daily$/, false, 3, false, `r1p3`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('freq/daily', 'daily') + expect(match).toBe('r1p3') + }) + it('regexp-match - edge case of matching the root folder - match by path', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + matcher.addRegexpDefinition(/^\/$/, false, undefined, false, `r1`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('/', '') + expect(match).toBe('r1') + }) + it('regexp-match - edge case of matching the root folder - match by name not possible', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + // Tricky regexp which can return zero length matches + matcher.addRegexpDefinition(/.*/, true, undefined, false, `r1`) + matcher.addWildcardDefinition('/*', `w1`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('/', '') + expect(match).toBe('w1') + }) + it('regexp-match - edge case of no match when only regexp rules present', () => { + const matcher: FolderWildcardMatching = new FolderWildcardMatching() + // Tricky regexp which can return zero length matches + matcher.addRegexpDefinition(/abc/, true, undefined, false, `r1`) + // Path w/o leading / - this is how Obsidian supplies the path + const match: SortingSpec | null = matcher.folderMatch('/', '') + expect(match).toBeNull() + }) }) diff --git a/src/custom-sort/folder-matching-rules.ts b/src/custom-sort/folder-matching-rules.ts index 62d3d7536..750028e59 100644 --- a/src/custom-sort/folder-matching-rules.ts +++ b/src/custom-sort/folder-matching-rules.ts @@ -1,8 +1,4 @@ -export interface FolderPattern { - path: string - deep: boolean - nestingLevel: number -} +import * as regexpp from "regexpp"; export type DeterminedSortingSpec = { spec?: SortingSpec @@ -16,6 +12,14 @@ export interface FolderMatchingTreeNode { subtree: { [key: string]: FolderMatchingTreeNode } } +export interface FolderMatchingRegexp { + regexp: RegExp + againstName: boolean + priority: number + logMatches: boolean + sortingSpec: SortingSpec +} + const SLASH: string = '/' export const MATCH_CHILDREN_PATH_TOKEN: string = '...' export const MATCH_ALL_PATH_TOKEN: string = '*' @@ -23,6 +27,7 @@ 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 NO_PRIORITY = 0 export const splitPath = (path: string): Array => { return path.split(SLASH).filter((name) => !!name) @@ -34,10 +39,13 @@ export interface AddingWildcardFailure { export class FolderWildcardMatching { + // mimics the structure of folders, so for example tree.matchAll contains the matchAll flag for the root '/' tree: FolderMatchingTreeNode = { subtree: {} } + regexps: Array> + // cache determinedWildcardRules: { [key: string]: DeterminedSortingSpec } = {} @@ -76,33 +84,82 @@ export class FolderWildcardMatching { } } - folderMatch = (folderPath: string): SortingSpec | null => { + addRegexpDefinition = (regexp: RegExp, + againstName: boolean, + priority: number | undefined, + log: boolean | undefined, + rule: SortingSpec + ) => { + const newItem: FolderMatchingRegexp = { + regexp: regexp, + againstName: againstName, + priority: priority || NO_PRIORITY, + sortingSpec: rule, + logMatches: !!log + } + if (this.regexps === undefined || this.regexps.length === 0) { + this.regexps = [newItem] + } else { + // priority is present ==> consciously determine where to insert the regexp + let idx = 0 + while (idx < this.regexps.length && this.regexps[idx].priority > newItem.priority) { + idx++ + } + this.regexps.splice(idx, 0, newItem) + } + } + + folderMatch = (folderPath: string, folderName?: string): SortingSpec | null => { const spec: DeterminedSortingSpec = this.determinedWildcardRules[folderPath] if (spec) { return spec.spec ?? null } else { - let rule: SortingSpec | null | undefined = this.tree.matchChildren - let inheritedRule: SortingSpec | undefined = this.tree.matchAll - const pathComponents: Array = splitPath(folderPath) - let parentNode: FolderMatchingTreeNode = this.tree - let lastIdx: number = pathComponents.length - 1 - for(let i=0; i<=lastIdx; i++) { - const name: string = pathComponents[i] - let matchedPath: FolderMatchingTreeNode = parentNode.subtree[name] - if (matchedPath) { - parentNode = matchedPath - rule = matchedPath?.matchChildren ?? null - inheritedRule = matchedPath.matchAll ?? inheritedRule - } else { - if (i < lastIdx) { - rule = inheritedRule + let rule: SortingSpec | null | undefined + // regexp matching + if (this.regexps) { + for (let r of this.regexps) { + if (r.againstName && !folderName) { + // exclude the edge case: + // - root folder which has empty name (and path /) + // AND name-matching regexp allows zero-length matches + continue + } + if (r.regexp.test(r.againstName ? (folderName || '') : folderPath)) { + rule = r.sortingSpec + if (r.logMatches) { + const msgDetails: string = (r.againstName) ? `name: ${folderName}` : `path: ${folderPath}` + console.log(`custom-sort plugin - regexp <${r.regexp.source}> matched folder ${msgDetails}`) + } + break } - break } } - rule = rule ?? inheritedRule + // simple wildards matching + if (!rule) { + rule = this.tree.matchChildren + let inheritedRule: SortingSpec | undefined = this.tree.matchAll + const pathComponents: Array = splitPath(folderPath) + let parentNode: FolderMatchingTreeNode = this.tree + let lastIdx: number = pathComponents.length - 1 + for (let i = 0; i <= lastIdx; i++) { + const name: string = pathComponents[i] + let matchedPath: FolderMatchingTreeNode = 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} diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index 482acaabe..be656bcbd 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -780,6 +780,170 @@ describe('SortingSpecProcessor edge case', () => { }) }) +const txtInputTargetFolderByName: string = ` +target-folder: name: TheName +< a-z +` + +const txtInputTargetFolderWithRegex: string = ` +> advanced modified +target-folder: name: TheName +< a-z +target-folder: regexp: r1 +target-folder: regexp: /!!: r2* +target-folder: regexp: for-name: r3.{2-3}$ +target-folder: regexp: for-name: /!: r4\\d +target-folder: regexp: for-name: /!!: ^r5[^[]+ +target-folder: regexp: for-name: /!!!: ^r6/+$ +target-folder: regexp: debug: r7 + +target-folder: regexp: for-name: debug: r8 (aa|bb|cc) +target-folder: regexp: for-name: /!!!: debug: r9 [abc]+ +target-folder: regexp: /!: debug: ^r10 /[^/]/.+$ +` + +const expectedSortSpecTargetFolderRegexAndName1 = { + defaultOrder: CustomSortOrder.byModifiedTimeReverseAdvanced, + groups: [{ + order: CustomSortOrder.byModifiedTimeReverseAdvanced, + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 0, + targetFoldersPaths: ['mock-folder'] +} + +const expectedSortSpecTargetFolderByName = { + defaultOrder: CustomSortOrder.alphabetical, + groups: [{ + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 0, + targetFoldersPaths: ['name: TheName'] +} + +const expectedSortSpecsTargetFolderByPathInRegexTestCase: { [key: string]: CustomSortSpec } = { + 'mock-folder': expectedSortSpecTargetFolderRegexAndName1 +} + +const expectedSortSpecsTargetFolderByName: { [key: string]: CustomSortSpec } = { + 'TheName': expectedSortSpecTargetFolderByName +} + +const expectedSortSpecForRegexpTextCase = { + groups: [{ + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 0, + targetFoldersPaths: [ + "regexp: r1", + "regexp: /!!: r2*", + "regexp: for-name: r3.{2-3}$", + "regexp: for-name: /!: r4\\d", + "regexp: for-name: /!!: ^r5[^[]+", + "regexp: for-name: /!!!: ^r6/+$", + "regexp: debug: r7 +", + "regexp: for-name: debug: r8 (aa|bb|cc)", + "regexp: for-name: /!!!: debug: r9 [abc]+", + "regexp: /!: debug: ^r10 /[^/]/.+$" + ] +} + +const expectedTargetFolderRegexpArr = [ + { + regexp: /r9 [abc]+/, + againstName: true, + priority: 3, + logMatches: true, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /^r6\/+$/, + againstName: true, + priority: 3, + logMatches: false, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /^r5[^[]+/, + againstName: true, + priority: 2, + logMatches: false, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /r2*/, + againstName: false, + priority: 2, + logMatches: false, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /^r10 \/[^/]\/.+$/, + againstName: false, + priority: 1, + logMatches: true, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /r4\d/, + againstName: true, + priority: 1, + logMatches: false, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /r8 (aa|bb|cc)/, + againstName: true, + priority: 0, + logMatches: true, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /r7 +/, + againstName: false, + priority: 0, + logMatches: true, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /r3.{2-3}$/, + againstName: true, + priority: 0, + logMatches: false, + sortingSpec: expectedSortSpecForRegexpTextCase + }, + { + regexp: /r1/, + againstName: false, + priority: 0, + logMatches: false, + sortingSpec: expectedSortSpecForRegexpTextCase + } +] + +describe('SortingSpecProcessor target-folder by name and regex', () => { + let processor: SortingSpecProcessor; + beforeEach(() => { + processor = new SortingSpecProcessor(); + }); + it('should correctly handle the by-name only target-folder', () => { + const inputTxtArr: Array = txtInputTargetFolderByName.split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result?.sortSpecByPath).toEqual({}) + expect(result?.sortSpecByName).toEqual(expectedSortSpecsTargetFolderByName) + expect(result?.sortSpecByWildcard).not.toBeNull() + }) + it('should recognize and correctly parse target folder by name with and w/o regexp variants', () => { + const inputTxtArr: Array = txtInputTargetFolderWithRegex.split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result?.sortSpecByPath).toEqual(expectedSortSpecsTargetFolderByPathInRegexTestCase) + expect(result?.sortSpecByName).toEqual(expectedSortSpecsTargetFolderByName) + expect(result?.sortSpecByWildcard?.tree).toEqual({subtree: {}}) + expect(result?.sortSpecByWildcard?.regexps).toEqual(expectedTargetFolderRegexpArr) + }) +}) + const txtInputPriorityGroups1: string = ` target-folder: / /:files @@ -1694,6 +1858,48 @@ describe('SortingSpecProcessor error detection and reporting', () => { expect(result).not.toBeNull() expect(errorsLogger).not.toHaveBeenCalled() }) + it('should recognize empty regexp of target-folder:', () => { + const inputTxtArr: Array = ` + target-folder: regexp: + `.replace(/\t/gi, '').split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result).toBeNull() + expect(errorsLogger).toHaveBeenCalledTimes(1) + expect(errorsLogger).toHaveBeenNthCalledWith(1, + `${ERR_PREFIX} 27:InvalidOrEmptyFolderMatchingRegexp Invalid or empty folder regexp expression <> ${ERR_SUFFIX}`) + }) + it('should recognize error in regexp of target-folder:', () => { + const inputTxtArr: Array = ` + target-folder: regexp: bla ( + `.replace(/\t/gi, '').split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result).toBeNull() + expect(errorsLogger).toHaveBeenCalledTimes(1) + expect(errorsLogger).toHaveBeenNthCalledWith(1, + `${ERR_PREFIX} 27:InvalidOrEmptyFolderMatchingRegexp Invalid or empty folder regexp expression ${ERR_SUFFIX}`) + }) + it('should recognize empty name in target-folder: name:', () => { + const inputTxtArr: Array = ` + target-folder: name: + `.replace(/\t/gi, '').split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result).toBeNull() + expect(errorsLogger).toHaveBeenCalledTimes(1) + expect(errorsLogger).toHaveBeenNthCalledWith(1, + `${ERR_PREFIX} 26:EmptyFolderNameToMatch Empty 'target-folder: name:' value ${ERR_SUFFIX}`) + }) + it('should recognize duplicate name in target-folder: name:', () => { + const inputTxtArr: Array = ` + target-folder: name: 123 + target-folder: name: xyz + target-folder: name: 123 + `.replace(/\t/gi, '').split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result).toBeNull() + expect(errorsLogger).toHaveBeenCalledTimes(1) + expect(errorsLogger).toHaveBeenNthCalledWith(1, + `${ERR_PREFIX} 25:DuplicateByNameSortSpecForFolder Duplicate 'target-folder: name:' definition for the same name <123> ${ERR_SUFFIX}`) + }) }) const txtInputTargetFolderCCC: string = ` diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index c695d7298..591a397ab 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -25,7 +25,8 @@ import { FolderWildcardMatching, MATCH_ALL_SUFFIX, MATCH_CHILDREN_1_SUFFIX, - MATCH_CHILDREN_2_SUFFIX + MATCH_CHILDREN_2_SUFFIX, + NO_PRIORITY } from "./folder-matching-rules" interface ProcessingContext { @@ -75,13 +76,19 @@ export enum ProblemCode { TooManyGroupTypePrefixes, PriorityPrefixAfterGroupTypePrefix, CombinePrefixAfterGroupTypePrefix, - InlineRegexInPrefixAndSuffix + InlineRegexInPrefixAndSuffix, + DuplicateByNameSortSpecForFolder, + EmptyFolderNameToMatch, + InvalidOrEmptyFolderMatchingRegexp } const ContextFreeProblems = new Set([ ProblemCode.DuplicateSortSpecForSameFolder, ProblemCode.DuplicateWildcardSortSpecForSameFolder, - ProblemCode.OnlyLastCombinedGroupCanSpecifyOrder + ProblemCode.OnlyLastCombinedGroupCanSpecifyOrder, + ProblemCode.DuplicateByNameSortSpecForFolder, + ProblemCode.EmptyFolderNameToMatch, + ProblemCode.InvalidOrEmptyFolderMatchingRegexp ]) const ThreeDots = '...'; @@ -157,9 +164,11 @@ enum Attribute { OrderStandardObsidian } +const TargetFolderLexeme: string = 'target-folder:' + const AttrLexems: { [key: string]: Attribute } = { // Verbose attr names - 'target-folder:': Attribute.TargetFolder, + [TargetFolderLexeme]: Attribute.TargetFolder, 'order-asc:': Attribute.OrderAsc, 'order-desc:': Attribute.OrderDesc, 'sorting:': Attribute.OrderStandardObsidian, @@ -203,6 +212,10 @@ const PriorityModifierPrio1Lexeme: string = '/!' const PriorityModifierPrio2Lexeme: string = '/!!' const PriorityModifierPrio3Lexeme: string = '/!!!' +const PriorityModifierPrio1TargetFolderLexeme: string = '/!:' +const PriorityModifierPrio2TargetFolderLexeme: string = '/!!:' +const PriorityModifierPrio3TargetFolderLexeme: string = '/!!!:' + const PRIO_1: number = 1 const PRIO_2: number = 2 const PRIO_3: number = 3 @@ -213,6 +226,12 @@ const SortingGroupPriorityPrefixes: { [key: string]: number } = { [PriorityModifierPrio3Lexeme]: PRIO_3 } +const TargetFolderRegexpPriorityPrefixes: { [key: string]: number } = { + [PriorityModifierPrio1TargetFolderLexeme]: PRIO_1, + [PriorityModifierPrio2TargetFolderLexeme]: PRIO_2, + [PriorityModifierPrio3TargetFolderLexeme]: PRIO_3 +} + const CombineGroupLexeme: string = '/+' const CombiningGroupPrefixes: Array = [ @@ -491,15 +510,35 @@ export const convertInlineRegexSymbolsAndEscapeTheRest = (s: string): RegexAsStr return regexAsString.join('') } +export const MatchFolderNameLexeme: string = 'name:' +export const MatchFolderByRegexpLexeme: string = 'regexp:' +export const RegexpAgainstFolderName: string = 'for-name:' +export const DebugFolderRegexMatchesLexeme: string = 'debug:' + +type FolderPath = string +type FolderName = string + export interface FolderPathToSortSpecMap { - [key: string]: CustomSortSpec + [key: FolderPath]: CustomSortSpec +} + +export interface FolderNameToSortSpecMap { + [key: FolderName]: CustomSortSpec } export interface SortSpecsCollection { sortSpecByPath: FolderPathToSortSpecMap + sortSpecByName: FolderNameToSortSpecMap sortSpecByWildcard?: FolderWildcardMatching } +export const newSortSpecsCollection = (): SortSpecsCollection => { + return { + sortSpecByPath: {}, + sortSpecByName: {} + } +} + interface AdjacencyInfo { noPrefix: boolean, noSuffix: boolean @@ -524,32 +563,78 @@ enum WildcardPriority { MATCH_ALL } -const stripWildcardPatternSuffix = (path: string): [path: string, priority: number] => { +const stripWildcardPatternSuffix = (path: string): {path: string, detectedWildcardPriority: number} => { if (path.endsWith(MATCH_ALL_SUFFIX)) { path = path.slice(0, -MATCH_ALL_SUFFIX.length) - return [ - path.length > 0 ? path : '/', - WildcardPriority.MATCH_ALL - ] + return { + path: path.length > 0 ? path : '/', + detectedWildcardPriority: WildcardPriority.MATCH_ALL + } } if (path.endsWith(MATCH_CHILDREN_1_SUFFIX)) { path = path.slice(0, -MATCH_CHILDREN_1_SUFFIX.length) - return [ - path.length > 0 ? path : '/', - WildcardPriority.MATCH_CHILDREN, - ] + return { + path: path.length > 0 ? path : '/', + detectedWildcardPriority: WildcardPriority.MATCH_CHILDREN, + } } if (path.endsWith(MATCH_CHILDREN_2_SUFFIX)) { path = path.slice(0, -MATCH_CHILDREN_2_SUFFIX.length) - return [ - path.length > 0 ? path : '/', - WildcardPriority.MATCH_CHILDREN - ] + return { + path: path.length > 0 ? path : '/', + detectedWildcardPriority: WildcardPriority.MATCH_CHILDREN + } + } + return { + path: path, + detectedWildcardPriority: WildcardPriority.NO_WILDCARD + } +} + +const eatPrefixIfPresent = (expression: string, prefix: string, onDetected: () => void): string => { + const detected: boolean = expression.startsWith(prefix) + if (detected) { + onDetected() + return expression.substring(prefix.length).trim() + } else { + return expression + } +} + +const consumeFolderByRegexpExpression = (expression: string): {regexp: RegExp, againstName: boolean, priority: number | undefined, log: boolean | undefined} => { + let againstName: boolean = false + let priority: number | undefined + let logMatches: boolean | undefined + + // For simplicity, strict imposed order of regexp-specific attributes + expression = eatPrefixIfPresent(expression, RegexpAgainstFolderName, () => { + againstName = true + }) + + for (const priorityPrefix of Object.keys(TargetFolderRegexpPriorityPrefixes)) { + expression = eatPrefixIfPresent(expression, priorityPrefix, () => { + priority = TargetFolderRegexpPriorityPrefixes[priorityPrefix] + }) + if (priority) { + break + } + } + + expression = eatPrefixIfPresent(expression, DebugFolderRegexMatchesLexeme, () => { + logMatches = true + }) + + // do not allow empty regexp + if (!expression || expression.trim() === '') { + throw new Error('Empty regexp') + } + + return { + regexp: new RegExp(expression), + againstName: againstName, + priority: priority === undefined ? NO_PRIORITY : priority, + log: !!logMatches } - return [ - path, - WildcardPriority.NO_WILDCARD - ] } // Simplistic @@ -641,12 +726,52 @@ export class SortingSpecProcessor { } } + let sortspecByName: FolderNameToSortSpecMap | undefined + for (let spec of this.ctx.specs) { + // Consume the folder names prefixed by the designated lexeme + for (let idx = 0; idx` ) + return null // Failure - not allow duplicate by folderNameToMatch specs for the same folder folderNameToMatch + } else { + sortspecByName[folderNameToMatch] = spec + } + } + } + } + + if (sortspecByName) { + collection = collection ?? newSortSpecsCollection() + collection.sortSpecByName = sortspecByName + } + let sortspecByWildcard: FolderWildcardMatching | undefined for (let spec of this.ctx.specs) { - // Consume the folder paths ending with wildcard specs + // Consume the folder paths ending with wildcard specs or regexp-based for (let idx = 0; idx() + const folderByRegexpExpression: string = path.substring(MatchFolderByRegexpLexeme.length).trim() + try { + const {regexp, againstName, priority, log} = consumeFolderByRegexpExpression(folderByRegexpExpression) + sortspecByWildcard.addRegexpDefinition(regexp, againstName, priority, log, spec) + } catch (e) { + this.problem(ProblemCode.InvalidOrEmptyFolderMatchingRegexp, + `Invalid or empty folder regexp expression <${folderByRegexpExpression}>`) + return null + } + } else if (endsWithWildcardPatternSuffix(path)) { sortspecByWildcard = sortspecByWildcard ?? new FolderWildcardMatching() const ruleAdded = sortspecByWildcard.addWildcardDefinition(path, spec) if (ruleAdded?.errorMsg) { @@ -658,31 +783,31 @@ export class SortingSpecProcessor { } if (sortspecByWildcard) { - collection = collection ?? { sortSpecByPath:{} } + collection = collection ?? newSortSpecsCollection() 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 (!originalPath.startsWith(MatchFolderNameLexeme) && !originalPath.startsWith(MatchFolderByRegexpLexeme)) { + collection = collection ?? newSortSpecsCollection() + const {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 } - } - if (storeTheSpec) { - collection.sortSpecByPath[path] = spec - this.pathMatchPriorityForPath[path] = detectedWildcardPriority } } } diff --git a/src/main.ts b/src/main.ts index 56def40da..5600446b9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,15 +32,13 @@ interface CustomSortPluginSettings { suspended: boolean statusBarEntryEnabled: boolean notificationsEnabled: boolean - allowRegexpInTargetFolder: boolean } const DEFAULT_SETTINGS: CustomSortPluginSettings = { additionalSortspecFile: '', suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install statusBarEntryEnabled: true, - notificationsEnabled: true, - allowRegexpInTargetFolder: false + notificationsEnabled: true } const SORTSPEC_FILE_NAME: string = 'sortspec.md' @@ -323,15 +321,16 @@ export default class CustomSortPlugin extends Plugin { // if custom sort is not specified, use the UI-selected const folder: TFolder = this.file let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath[folder.path] + sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName[folder.name] 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) + sortSpec = plugin.sortSpecCache?.sortSpecByWildcard.folderMatch(folder.path, folder.name) if (sortSpec?.defaultOrder === CustomSortOrder.standardObsidian) { - sortSpec = null // A folder subtree can be also explicitly excluded from custom sorting plugin + sortSpec = null // A folder is explicitly excluded from custom sorting plugin } } if (sortSpec) { From ea018db5748e0036bbda79ebc95861830a3e48d8 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Mon, 6 Feb 2023 23:42:56 +0100 Subject: [PATCH 08/26] #50 - regexp and by-name matching support for target-folder - removed unused code --- src/custom-sort/folder-matching-rules.spec.ts | 33 ------------------- src/custom-sort/folder-matching-rules.ts | 2 -- 2 files changed, 35 deletions(-) diff --git a/src/custom-sort/folder-matching-rules.spec.ts b/src/custom-sort/folder-matching-rules.spec.ts index ff9f84973..8cb8af94b 100644 --- a/src/custom-sort/folder-matching-rules.spec.ts +++ b/src/custom-sort/folder-matching-rules.spec.ts @@ -18,39 +18,6 @@ const PRIO1 = 1 const PRIO2 = 2 const PRIO3 = 3 -const createMockMatcherRichWithRegexpVersion = (usePriorities?: boolean): FolderWildcardMatching => { - const matcher: FolderWildcardMatching = createMockMatcherRichVersion() - let p: RegExp - const {row3, row4, row5, row6} = usePriorities ? - {row3: PRIO1, row4: PRIO1, row5: PRIO2, row6: PRIO3} - : - {row3:undefined, row4:undefined, row5:undefined, row6:undefined} - p = /.../; matcher.addRegexpDefinition(p, true, undefined, false, `r1`) - p = /^\/$/; matcher.addRegexpDefinition(p, false, undefined, false, `r2`) - p = /^Arc..ve$/; matcher.addRegexpDefinition(p, true, row3, false, `r3`) - p = /^Arc..ve$/; matcher.addRegexpDefinition(p, false, row4, false, `r4`) - p = /Reviews\/daily\/a\/.../; matcher.addRegexpDefinition(p, true, row5, false, `r5`) - p = /Reviews\/daily\/a\/*/; matcher.addRegexpDefinition(p, true, row6, false, `r6`) - return matcher -} - -/* -tests needed: -√ regexp-match by name works (ensure regexp input is the name) -√ regexp-match by path works (ensure regexp input is the path) -√ regexp-match by name works and has priority over wildcard -√ regexp-match by path works and has priority over wildcard -√ regexp-match by name vs by path is equal, order of definition matters (test two variants) -- priority /!!!: is higher over /!!: -- priority /!!: is higher over /!: -- priority /!: is higher over no-priority - - test adding priorities in all possible orders - - within the same priority the order of definition matters - -- edge case -> root folder, has no name, has specific path '/', matching by name should not work, only by path - - what is the root folder name ??? - */ - const createMockMatcherSimplestVersion = (): FolderWildcardMatching => { const matcher: FolderWildcardMatching = new FolderWildcardMatching() matcher.addWildcardDefinition('/Reviews/daily/*', '/Reviews/daily/*') diff --git a/src/custom-sort/folder-matching-rules.ts b/src/custom-sort/folder-matching-rules.ts index 750028e59..bd187215e 100644 --- a/src/custom-sort/folder-matching-rules.ts +++ b/src/custom-sort/folder-matching-rules.ts @@ -1,5 +1,3 @@ -import * as regexpp from "regexpp"; - export type DeterminedSortingSpec = { spec?: SortingSpec } From afcb5056338a5a27f22318596d1a50e87f6b57b2 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Mon, 6 Feb 2023 23:59:14 +0100 Subject: [PATCH 09/26] #50 - regexp and by-name matching support for target-folder - code readability (implicit structure turned into explicit interface) --- src/custom-sort/sorting-spec-processor.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 591a397ab..00f1aa607 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -22,6 +22,7 @@ import { RomanNumberRegexStr } from "./matchers"; import { + FolderMatchingRegexp, FolderWildcardMatching, MATCH_ALL_SUFFIX, MATCH_CHILDREN_1_SUFFIX, @@ -601,7 +602,14 @@ const eatPrefixIfPresent = (expression: string, prefix: string, onDetected: () = } } -const consumeFolderByRegexpExpression = (expression: string): {regexp: RegExp, againstName: boolean, priority: number | undefined, log: boolean | undefined} => { +export interface ConsumedFolderMatchingRegexp { + regexp: RegExp + againstName: boolean + priority: number | undefined + log: boolean | undefined +} + +export const consumeFolderByRegexpExpression = (expression: string): ConsumedFolderMatchingRegexp => { let againstName: boolean = false let priority: number | undefined let logMatches: boolean | undefined @@ -764,8 +772,8 @@ export class SortingSpecProcessor { sortspecByWildcard = sortspecByWildcard ?? new FolderWildcardMatching() const folderByRegexpExpression: string = path.substring(MatchFolderByRegexpLexeme.length).trim() try { - const {regexp, againstName, priority, log} = consumeFolderByRegexpExpression(folderByRegexpExpression) - sortspecByWildcard.addRegexpDefinition(regexp, againstName, priority, log, spec) + const r: ConsumedFolderMatchingRegexp = consumeFolderByRegexpExpression(folderByRegexpExpression) + sortspecByWildcard.addRegexpDefinition(r.regexp, r.againstName, r.priority, r.log, spec) } catch (e) { this.problem(ProblemCode.InvalidOrEmptyFolderMatchingRegexp, `Invalid or empty folder regexp expression <${folderByRegexpExpression}>`) From 51733476e7800d459b8dedcdbe0606e772d1087c Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 7 Feb 2023 14:11:50 +0100 Subject: [PATCH 10/26] #50 - regexp and by-name matching support for target-folder - more flexible syntax for target-folder modifiers: allow them in any order and also allow duplicates --- .../sorting-spec-processor.spec.ts | 64 ++++++++++++++++++- src/custom-sort/sorting-spec-processor.ts | 38 +++++++---- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index be656bcbd..de04efabf 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -1,7 +1,7 @@ import { CompoundDashNumberNormalizerFn, CompoundDashRomanNumberNormalizerFn, - CompoundDotNumberNormalizerFn, + CompoundDotNumberNormalizerFn, ConsumedFolderMatchingRegexp, consumeFolderByRegexpExpression, convertPlainStringToRegex, detectNumericSortingSymbols, escapeRegexUnsafeCharacters, @@ -13,7 +13,7 @@ import { SortingSpecProcessor } from "./sorting-spec-processor" import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types"; -import {FolderMatchingTreeNode} from "./folder-matching-rules"; +import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules"; const txtInputExampleA: string = ` order-asc: a-z @@ -849,7 +849,7 @@ const expectedSortSpecForRegexpTextCase = { ] } -const expectedTargetFolderRegexpArr = [ +const expectedTargetFolderRegexpArr: Array> = [ { regexp: /r9 [abc]+/, againstName: true, @@ -944,6 +944,64 @@ describe('SortingSpecProcessor target-folder by name and regex', () => { }) }) +const NOPRIO = 0 +const PRIO1 = 1 +const PRIO2 = 2 +const PRIO3 = 3 + +const consumedTargetFolderRegexp: Array = [ + { + regexp: /r4\d/, + againstName: true, + priority: undefined, + log: true + }, { + regexp: /r4\d/, + againstName: true, + priority: PRIO1, + log: true + }, { + regexp: /r4\d/, + againstName: true, + priority: PRIO2, + log: true + }, { + regexp: /r4\d/, + againstName: true, + priority: PRIO3, + log: true + }, +] + +describe( 'consumeFolderByRegexpExpression', () => { + // and accept priority in any order + // the last one is in effect + // and accept multiple + it.each([ + // Plain cases + ['for-name: /!: debug: r4\\d', PRIO1], + ['for-name: /!: debug: r4\\d', PRIO1], + ['/!!: for-name: debug: r4\\d', PRIO2], + ['/!: debug: for-name: r4\\d', PRIO1], + ['debug: for-name: /!!!: r4\\d', PRIO3], + ['debug: /!: for-name: r4\\d', PRIO1], + // Cases with duplication of same + ['for-name: for-name: /!: debug: r4\\d', PRIO1], + ['for-name: /!: /!: debug: debug: r4\\d', PRIO1], + ['/!!: for-name: /!!: debug: r4\\d', PRIO2], + ['/!: debug: debug: for-name: r4\\d', PRIO1], + ['debug: for-name: /!!!:/!!!: r4\\d', PRIO3], + ['debug: /!: for-name: /!: r4\\d', PRIO1], + // Cases with duplication of different priority + ['debug: /!!!: for-name: /!: r4\\d', PRIO1], + ['debug: /!: for-name: /!!: r4\\d', PRIO2], + ['debug: /!: for-name: /!!: /!!!: /!: /!!!: r4\\d', PRIO3], + ])('should recognize all modifiers in >%s< of priority %s', (regexpExpr: string, prio: number) => { + const result: ConsumedFolderMatchingRegexp = consumeFolderByRegexpExpression(regexpExpr) + expect(result).toEqual(consumedTargetFolderRegexp[prio]) + }) +}) + const txtInputPriorityGroups1: string = ` target-folder: / /:files diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 00f1aa607..3aeb28c39 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -614,23 +614,33 @@ export const consumeFolderByRegexpExpression = (expression: string): ConsumedFol let priority: number | undefined let logMatches: boolean | undefined - // For simplicity, strict imposed order of regexp-specific attributes - expression = eatPrefixIfPresent(expression, RegexpAgainstFolderName, () => { - againstName = true - }) - - for (const priorityPrefix of Object.keys(TargetFolderRegexpPriorityPrefixes)) { - expression = eatPrefixIfPresent(expression, priorityPrefix, () => { - priority = TargetFolderRegexpPriorityPrefixes[priorityPrefix] + let nextRoundNeeded: boolean + + do { + nextRoundNeeded = false + + expression = eatPrefixIfPresent(expression, RegexpAgainstFolderName, () => { + againstName = true + nextRoundNeeded = true }) - if (priority) { - break + + for (const priorityPrefix of Object.keys(TargetFolderRegexpPriorityPrefixes)) { + let doBreak: boolean = false + expression = eatPrefixIfPresent(expression, priorityPrefix, () => { + priority = TargetFolderRegexpPriorityPrefixes[priorityPrefix] + nextRoundNeeded = true + doBreak = true + }) + if (doBreak) { + break + } } - } - expression = eatPrefixIfPresent(expression, DebugFolderRegexMatchesLexeme, () => { - logMatches = true - }) + expression = eatPrefixIfPresent(expression, DebugFolderRegexMatchesLexeme, () => { + logMatches = true + nextRoundNeeded = true + }) + } while (nextRoundNeeded) // do not allow empty regexp if (!expression || expression.trim() === '') { From 4a27ef03d26075d22f7403ff6e86378c4f622f62 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:13:32 +0100 Subject: [PATCH 11/26] #50 - regexp and by-name matching support for target-folder - documentation update --- docs/manual.md | 185 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 1 deletion(-) diff --git a/docs/manual.md b/docs/manual.md index b6f1cb120..79e091b25 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -70,7 +70,7 @@ For clarity: the three available prefixes `/!` and `/!!` and `/!!!` allow for fu ## Simple wildcards -Currently, the below simple wildcard syntax is supported: +Currently, the below simple wildcard syntax is supported for sorting group: ### A single digit (exactly one) @@ -248,3 +248,186 @@ sorting-spec: | /! starred: --- ``` + +## Options for target-folder: matching + +The `target-folder:` has the following variants, listed in the order of precedence: + +1. match by the **exact folder path** (the default) +2. match by the **exact folder name** +3. match by **regexp** (for experts, be careful!) +4. match by **wildcard suffix** (aka match folders subtree) + +If a folder in the vault matches more than one `target-folder:` definitions, +the above list shows the precedence, e.g. 1. has precedence over 2., 3. and 4. for example. +In other words, match by exact folder path always wins, then goes the match by folder exact name, +and so on. + +If a folder in the vault matches more than one `target-folder:` definitions of the same type, +see the detailed description below for the behavior + +### By folder path (the default) + +If no additional modifiers follow the `target-folder:`, the remaining part of the line +is treated as an exact folder path (leading and trailing spaces are ignored, +infix spaces are treated literally as part of the folder path) + +Within the same vault duplicate definitions of same path in `target-folder:` are detected +and error is raised in that case, indicating the duplicated path + +Examples of `target-folder:` with match by the exact folder path: + +- `target-folder: My Folder` + - this refers to the folder in the root of the vault and only to it +- `target-folder: Archive/My Folder` + - matches the `My Folder` sub-folder in the `Archive` folder (a sub-folder of the root) +- `target-folder: .` + - this refers to path of the folder where the sorting specification resides (the specification containing the line, + keep in mind that the sorting specification can reside in multiple locations in multiple notes) +- `target-folder: ./Some Subfolder` + - this refers to path of a sub-folder of the folder where the sorting specification resides (the specification containing the line, + keep in mind that the sorting specification can reside in multiple locations in multiple notes) + +### By folder name + +The modifier `name:` tells the `target-folder:` to match the folder name and not the full folder path + +This is an exact match of the full folder name, no partial matching + +Within the same vault duplicate definitions of same name in `target-folder: name:` are detected +and error is raised in that case, indicating the duplicated folder name in sorting specification + +Examples of `target-folder:` with match by the exact folder name: + +- `target-folder: name: My Folder` + - matches all the folders with the name `My Folder` regardless of their location within the vault + +### By regexp (expert feature) + +> WARNING!!! This is an EXPERT FEATURE. +> +> Involving and constructing the regexp-s requires at least basic knowledge about the potential pitfalls.\ +> If you introduce a heavy _regexp-backtracking_ it can **kill performance of Obsidian and even make it unresponsive**\ +> If you don't know what the _regexp-backtracking_ is, be careful when using regexp for `target-folder:` + +The modifier `regexp:` tells the `target-folder:` to involve the specified regular expressions in matching + +Additional dependent modifiers are supported for `regexp:`: +- `for-name:` + - tells the matching to be done against the folder name, not the full path +- `debug:` + - tells the regexp to report its match in the developer console, so that you can easily investigate + why the regexp matches (or why it doesn't match) as expected +- `/!:` `/!!:` `/!!!:` + - sets the priority of the regexp + +By default, the regexp is matched against the full path of the folder, unless the `for-name:` modifiers tells otherwise. + +By default, the regexp-es have no priority and are evaluated in the order of their definition.\ +If you store `sorting-spec:` configurations in notes spread all over the vault, +consider the order of `target-folder: regexp:` to be undefined and - if needed - use +explicit priority modifiers (`/!:` `/!!:` `/!!!:`) to impose the desired order of matching. + - a regexp with modifier `/!!!:` if evaluated before all other regexps, regardless of where they are configured + - if two or more regexps are stamped with `/!!!:`, they are matched in the order in which they were defined.\ + Within a single YAML section of a note the order is obvious.\ + For sorting specifications spread over many notes in the vault consider the order to be undefined. + - a regexp with modifier `/!!:` if evaluated after any `/!!!:` and before all other regexps + - the same logic as described above applies when multiple regexps have the `/!!:` stamp + - a regexp with modifier `/!:` indicates the lowest of explicitly defined priorities.\ + Such a regexp is matched after all priority-stamped regexps, before the regexps not having + any explicit priority stamp + +The escape character is \ - the standard one in regexp world. + +Examples of `target-folder:` with match by regexp: + +- `target-folder: regexp: reading` + - matches any folder which contains the word `reading` in its path or name +- `target-folder: regexp: \d?\d-\d?\d-\d\d\d\d$` + - matches any folder which ends with date-alike numerical expression, e.g.: + - `1-1-2023` + - `Archive/Monthly/12/05-12-2022` + - `Inbox/Not digested notes from 20-7-2019` +- `target-folder: regexp: for-name: I am everywhere` + - matches all folders which contain the phrase `I am everywhere` in their name, e.g.: + - `Reports/Not processed/What the I am everywhere report from Paul means?` + - `Chapters/I am everywhere` +- `target-folder: regexp: for-name: ^I am (everyw)?here$` + - matches all folders with name exactly `I am everywhere` or `I am here` +- `target-folder: regexp: for-name: debug: ^...$` + - matches all folders with name comprising exactly 3 character + - when a folder is matched, a diagnostic line is written to the console - `debug:` modifiers enables the logging +- `target-folder: regexp: debug: ^.{13,15}$` + - matches all folders with path length between 13 and 15 characters + - diagnostic line is written to the console due to `debug:` +- `target-folder: regexp: for-name: /!: ^[aA]` + - matches all folders with name starting with `a` or `A` + - the priority `/!:` modifier causes the matching to be done before all other regexps + which don't have any priority +- `target-folder: regexp: /!!!: debug: for-name: abc|def|ghi` + - matches all folders with name containing the sequence `abc` or `def` or `ghi` + - the modifier `/!!!:` imposes the highest priority of regexp matching + - `debug:` tells to report each matching folder in the console +- `target-folder: regexp: ^[^/]+/[^/]+$` + - matches all folders which are at the 2nd level of vault tree, e.g.: + - `Inbox/Priority input` + - `Archive/2021` +- `target-folder: regexp: ^[^\/]+(\/[^\/]+){2}$` + - matches all folders which are at the 3rd level of vault tree, e.g.: + - `Archive/2019/05` + - `Aaaa/Bbbb/Test test` + +### By wildcard + +In the default usage of `target-folder:` with the exact full folder path, if the path contains +the `/...` or `/*` suffix its meaning is extended to: +- match the folder and all its immediate (child) subfolders - `/...` suffix +- match the folder and all its subfolders at any level (all descendants, the entire subtree) - `/*` suffix + +For example: + +- `target-folder: /*` + - matches all folders in the vault (the root folder and all its descendants) +- `target-folder: /...` + - matches the root folder and its immediate children (aka immediate subfolders of the root) + +If the sorting specification contains duplicate wildcard-ed path in `target-folder:` +an error is raised, indicating the duplicate path + +If a folder is matched by two (or more) wildcarded paths, the one with more path segments +(the deeper one) wins. For example: +- a folder `Book/Chapters/12/a` is matched by: + - (a) `target-folder: Book/*`, and + - (b) `target-folder: Book/Chapters/*` + - In this case the (b) wins, because it contains a deeper path + +If the depth of matches specification is the same, the `/...` takes precedence over `/*` +- a folder `Book/Appendix/III` is matched by: + - (a) `target-folder: Book/Appendix/...`, and + - (b) `target-folder: Book/Appendix/*` + - In this case the (a) wins + +## Excluding folders from custom sorting + +Having the ability to wildard- and regexp-based match of `target-folder:` in some cases +you might want to exclude folder(s) from custom sorting. + +This can be done by combination of the `target-folder:` (in any of its variants) +and specification of the sort order as `sorting: standard` + +An example piece of YAML frontmatter could look like: + +```yaml +--- +sorting-spec: | + + // ... some sorting specification above + + target-folder: Reviews/Attachments + target-folder: TODOs + sorting: standard + + // ... some sorting specification below + +--- +``` From 103821c71276eb1de0f7f98a8383a0081bd801c6 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:18:07 +0100 Subject: [PATCH 12/26] #50 - regexp and by-name matching support for target-folder - documentation updates --- docs/manual.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/manual.md b/docs/manual.md index 79e091b25..cd3cccd04 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1,5 +1,5 @@ > Document is partial, creation in progress -> Please refer to [README.md](../README.md) for more usage examples +> Please refer to [README.md](../README.md) and [advanced-README.md](../advanced-README.md) for more usage examples > Check also [syntax-reference.md](./syntax-reference.md) --- @@ -421,13 +421,13 @@ An example piece of YAML frontmatter could look like: --- sorting-spec: | - // ... some sorting specification above + // ... some sorting specification above - target-folder: Reviews/Attachments - target-folder: TODOs - sorting: standard + target-folder: Reviews/Attachments + target-folder: TODOs + sorting: standard - // ... some sorting specification below + // ... some sorting specification below --- ``` From f9154c214aa02076bb0fd19d047c2c16b6b4e7d7 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:50:22 +0100 Subject: [PATCH 13/26] Version bump before release --- manifest.json | 2 +- package.json | 2 +- versions.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index a395d8c97..dec7270a0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "custom-sort", "name": "Custom File Explorer sorting", - "version": "1.5.0", + "version": "1.6.0", "minAppVersion": "0.15.0", "description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer", "author": "SebastianMC", diff --git a/package.json b/package.json index 6819898db..7e26d61ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-custom-sort", - "version": "1.5.0", + "version": "1.6.0", "description": "Custom Sort plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index 9806a779e..8c85121ae 100644 --- a/versions.json +++ b/versions.json @@ -20,5 +20,6 @@ "1.2.0": "0.15.0", "1.3.0": "0.15.0", "1.4.0": "0.15.0", - "1.5.0": "0.15.0" + "1.5.0": "0.15.0", + "1.6.0": "0.15.0" } From 56e23bc5ea39f635d706a7395bd12e31045135d3 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Fri, 10 Feb 2023 13:39:52 +0100 Subject: [PATCH 14/26] #53 - Allow for different folder note naming scheme - updated settings description for clarity - minor extension of the code to allow both _about_ and _about_.md in the settings - Version bump before release --- manifest.json | 2 +- package.json | 2 +- src/main.ts | 35 ++++++++++++++++++++++++++--------- versions.json | 3 ++- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/manifest.json b/manifest.json index dec7270a0..14d69697b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "custom-sort", "name": "Custom File Explorer sorting", - "version": "1.6.0", + "version": "1.6.1", "minAppVersion": "0.15.0", "description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer", "author": "SebastianMC", diff --git a/package.json b/package.json index 7e26d61ff..e949c172d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-custom-sort", - "version": "1.6.0", + "version": "1.6.1", "description": "Custom Sort plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/src/main.ts b/src/main.ts index 5600446b9..2396c7c06 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { normalizePath, Plugin, PluginSettingTab, + sanitizeHTMLToDom, setIcon, Setting, TAbstractFile, @@ -86,12 +87,13 @@ export default class CustomSortPlugin extends Plugin { // - the file(s) explicitly configured by user in plugin settings // Be human-friendly and accept both .md and .md.md file extensions // (the latter representing a typical confusion between note name vs underlying file name) - if (aFile.name === SORTSPEC_FILE_NAME || - aFile.name === `${SORTSPEC_FILE_NAME}.md` || - aFile.basename === parent.name || - aFile.basename === this.settings.additionalSortspecFile || - aFile.path === this.settings.additionalSortspecFile || - aFile.path === `${this.settings.additionalSortspecFile}.md` + if (aFile.name === SORTSPEC_FILE_NAME || // file name == sortspec.md ? + aFile.name === `${SORTSPEC_FILE_NAME}.md` || // file name == sortspec.md.md ? + aFile.basename === parent.name || // Folder Note mode: inside folder, same name + aFile.basename === this.settings.additionalSortspecFile || // when user configured _about_ + aFile.name === this.settings.additionalSortspecFile || // when user configured _about_.md + aFile.path === this.settings.additionalSortspecFile || // when user configured Inbox/sort.md + aFile.path === `${this.settings.additionalSortspecFile}.md` // when user configured Inbox/sort ) { const sortingSpecTxt: string = mCache.getCache(aFile.path)?.frontmatter?.[SORTINGSPEC_YAML_KEY] if (sortingSpecTxt) { @@ -390,11 +392,26 @@ class CustomSortSettingTab extends PluginSettingTab { containerEl.createEl('h2', {text: 'Settings for Custom File Explorer Sorting Plugin'}); + const additionalSortspecFileDescr: DocumentFragment = sanitizeHTMLToDom( + 'A note name or note path to scan (YAML frontmatter) for sorting specification in addition to the `sortspec` notes and Folder Notes*.' + + '
' + + ' The `.md` filename suffix is optional.' + + '

(*) if you employ the Index-File based approach to folder notes (as documented in ' + + 'Aidenlx Folder Note preferences' + + ') you can enter here the index note name, e.g. _about_' + + '
' + + 'The Inside Folder, with Same Name Recommended mode of Folder Notes is handled automatically, no additional configuration needed.' + + '

' + + '

NOTE: After updating this setting remember to refresh the custom sorting via clicking on the ribbon icon or via the sort-on command' + + ' or by restarting Obsidian or reloading the vault

' + ) + new Setting(containerEl) - .setName('Path to the designated note containing sorting specification') - .setDesc('The YAML front matter of this note will be scanned for sorting specification, in addition to the `sortspec` notes and folder notes. The `.md` filename suffix is optional.') + .setName('Path or name of additional note(s) containing sorting specification') + .setDesc(additionalSortspecFileDescr) .addText(text => text - .setPlaceholder('e.g. Inbox/sort') + .setPlaceholder('e.g. _about_') .setValue(this.plugin.settings.additionalSortspecFile) .onChange(async (value) => { this.plugin.settings.additionalSortspecFile = value.trim() ? normalizePath(value) : ''; diff --git a/versions.json b/versions.json index 8c85121ae..6c802368c 100644 --- a/versions.json +++ b/versions.json @@ -21,5 +21,6 @@ "1.3.0": "0.15.0", "1.4.0": "0.15.0", "1.5.0": "0.15.0", - "1.6.0": "0.15.0" + "1.6.0": "0.15.0", + "1.6.1": "0.15.0" } From 1cb8b2b05b7c6f353891bd76fd5d5eee356dedc7 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 11 Feb 2023 17:40:31 +0100 Subject: [PATCH 15/26] Mobile-specific tweaks, e.g. new ribbon icon and documentation updates --- README.md | 22 ++++++++++++++++---- advanced-README.md | 18 +++++++++++----- docs/icons/icon-mobile-initial.png | Bin 0 -> 2176 bytes src/custom-sort/icons.ts | 7 +++++++ src/main.ts | 32 ++++++++++++++++++++++++----- 5 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 docs/icons/icon-mobile-initial.png diff --git a/README.md b/README.md index 425146c6b..cc519454a 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ sorting-spec: | --- ``` -Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png)) to tell the plugin to read the sorting specification and apply it. -The ribbon icon should turn (![Active](./docs/icons/icon-active.png)) and the sorting should be applied to the folder +Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png) or ![Static icon](./docs/icons/icon-mobile-initial.png) on phone) to tell the plugin to read the sorting specification and apply it. +The sorting should be applied to the folder. On desktops and tablets the ribbon icon should turn (![Active](./docs/icons/icon-active.png)) !!! **Done!** !!! @@ -115,16 +115,30 @@ Refer to the [TL;DR section of advanced README.md](./advanced-README.md#tldr-usa Click the ribbon icon to toggle the plugin between enabled and suspended states. -States of the ribbon icon: +States of the ribbon icon on large-screen devices (desktops, laptops and tablets like iPad): - ![Inactive](./docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. - ![Active](./docs/icons/icon-active.png) Plugin active, custom sorting applied. - ![Error](./docs/icons/icon-error.png) Syntax error in custom sorting configuration. - ![General Error](./docs/icons/icon-general-error.png) Plugin suspended. General error. -- ![Sorting not applied](./docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. +- ![Sorting not applied](./docs/icons/icon-not-applied.png) Plugin enabled, but the custom sorting was not applied. +- ![Static icon](./docs/icons/icon-mobile-initial.png) (Only on large-screen mobile devices like iPad). + Plugin enabled. but the custom sorting was not applied. + +On small-screen mobile devices (phones) the icon is static: + +- ![Static icon](./docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change For more details on the icon states refer to [Ribbon icon section of the advanced-README.md](./advanced-README.md#ribbon-icon) +## Small screen mobile devices remarks + +- enable mobile-specific notifications +- use the 'sort on' Obsidian command palette being easily available + (swipe down gesture on small-screen mobiles) allows for quick steering of the plugin via commands: sort-on and sort-off + +could need to activate separately, even if on shared vault and active on desktop + ## Installing the plugin ### From the official Obsidian Community Plugins page diff --git a/advanced-README.md b/advanced-README.md index 5131b8d9f..3e6d4729d 100644 --- a/advanced-README.md +++ b/advanced-README.md @@ -72,11 +72,11 @@ can be the root folder. Ensure the exact file name is `sortspec.md`. That file c > > ![sortspec.md](./docs/img/sortspec-md-dark.jpg) > 2. Enable the plugin in obsidian. > -> 3. Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png)) to tell the plugin to read the sorting +> 3. Click the ribbon button (![Inactive](./docs/icons/icon-inactive.png) or ![Mobile](./docs/icons/icon-mobile-initial.png) on phone) 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 -(![Active](./docs/icons/icon-active.png)) and reordering -of items in root vault folder to reverse alphabetical with folders and files treated equally. +> - The observable effect should be reordering of items in root vault folder to reverse alphabetical with folders and files treated equally. +And on computers and tablets be the change of appearance of the ribbon button to +![Active](./docs/icons/icon-active.png) (on desktop and tablet only) and > - The notification balloon should confirm success: ![Success](./docs/icons/parsing-succeeded.png) > 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 @@ -570,7 +570,7 @@ reference. Click the ribbon icon to toggle the plugin between enabled and suspended states. -States of the ribbon icon: +States of the ribbon icon on large-screen devices (desktops, laptops and tablets like iPad): - ![Inactive](./docs/icons/icon-inactive.png) Plugin suspended. Custom sorting NOT applied. - Click to enable and apply custom sorting. @@ -591,6 +591,14 @@ States of the ribbon icon: - ![Sorting not applied](./docs/icons/icon-not-applied.png) Plugin enabled but the custom sorting was not applied. - This can happen when reinstalling the plugin and in similar cases - Click the ribbon icon twice to re-enable the custom sorting. +- ![Static icon](./docs/icons/icon-mobile-initial.png) Only on large-screen mobile devices like iPad. + - Plugin enabled. but the custom sorting was not applied. + +On small-screen mobile devices (phones) the icon is static: + +- ![Static icon](./docs/icons/icon-mobile-initial.png) The icon acts as a button to toggle between enabled and disabled. Its appearance doesn't change + - Click to enable and apply custom sorting or to disable custom sorting + - To get notified about custom sort plugin state, enable the mobile-specific notifications in plugin settings ## Installing the plugin diff --git a/docs/icons/icon-mobile-initial.png b/docs/icons/icon-mobile-initial.png new file mode 100644 index 0000000000000000000000000000000000000000..6dde367b2a82bf557896585c32228802301fb24b GIT binary patch literal 2176 zcmZ`)30M=?7M_Fv8Uo5vg|G-=5d@Neq8K592|Iz9fI=;5gdm}W1QH-Df>rjihzbOX zvIuSzsVoJWqG%OK(A2Wzfv;iK;6reNCB;ZVy;mE@3HEmX8`~b#~={MUlTkCtSDA2l}!t!6B!(K6fKdw6#y)Y z;!bt?{6f~b>t9cKh|AV6G_cPh4D`X{Vpb;pc+=$?8~9&x!L01Jmm~h!!6ZTa`eFaCO2z2X`14R|nmlB%Qdo|Zj}Exz>uq>a zlvB+7cI}9{kzIt<_|+#r_uf0vkFMNxzBsJnbxBYkJTkN0%jVLq#T3=jTzW(6$)S`X4a`V!6tJ0zei6fUR=A1g&Ctf)iA)T4)vYBg3Fd z=@V8^hx|3QL9>?a8O#9yH4v+5LxA!MT>yYW8394uAd)XGl*L5T!dOS>XdaWTPytRn z90-|oF3p(7jEd&qc+RL51`foEWem!Ag~I*X85Kk#8xvTubmQ%4EEv)0+_@8jwa3`o+ky;RPGU5d#CGYX|h^l^NslgkMIlqs6Cx-D>n7=;I8hsI()C!_Ni z{~}X(KFC(~^&y>;A{fp;mO%##R+NReb6QF8e`}x0b5caXkr_OCR1lHDq(^fU8i(yp zn9n*sD-8ZO;XfR!LMM#kl>TvQA6l}q3ibpKcfx$^FCIRv^AjHcV4J*%t^uCvbtOKQ zaV(wg|HKp|ql=d1S}N{{9)O^oo9>%Ws`EUQYc_=o^%WgQ@O81|8cXxi-_5ObI!5s~ zU(Pv?PFKk0--{1?*SvIaE=iIkX_g3>LZNVOp>S3%4>8cls;H>wk;!Cj2PstQ-T8*W zLkM7(vvYA%lf4xh{VF2o@z_{3V%@rA)tT8@>%zjq(`98=85tQ1Z{JFxaJAIbR4qL{ ztG2eb@rfhzyN!U7k`jx~&d#RJ&W7VlOG`-DoRN{yMbg zKA%qpWj7xpo8@Ie zu|QzV&Worj2F52QTFrqSy~+=x_J|h6Z{BpIXJyqZ5xyJ#^XcxYA!wxXFC%W-dU$DR zhv0B{tw3-9i9}95dD4}E^gryBmYDe4thMeO9->t|Cb)Rf9fd;G*47feeSCIbDa;?W zo%o~v@n#okYg^mqhK7cPy$1~4OiWBDhYmH0LS_C8IXvw7x#KTxG&h?D1_n|B0&3rx z8X$L>ni9&(%eU!TE`{%e#K6=Mv|7ab%nePV2M>z7x?GsytR}9J|F&>0x5bx45=tZ` z8w_z}FTzB2cbr}ME`OO*k4{Xa4=2RMnS-rA{Wc~@B9Vxurs@y~M9+f<`L8BNU50*j zOT6#)!@gGB)@=pY$(}3q_5FF|r_<9Hef|8T9Udg-hR2UX+8S$WYATphFf&4AmP{=6 zGo*qalkz}1y$;&fBavKB`oqxG-sXF~`4lFTR|cq^~1ClNbddK-hNB$j|UyyY;tmP`uh4% zs+*bzaR8er-|NV9(`(;6QB zl)}8p60MoUp6-c>P=81CuVtQjJFTnip!{#UXx;G4d>LRdUYSC9l4m!gaUU*XC2YS5 Y;otGN +` + ) + addIcon(ICON_SORT_MOBILE_INITIAL, + ` + + ` ) addIcon(ICON_SORT_SUSPENDED, diff --git a/src/main.ts b/src/main.ts index 2396c7c06..11ea059e0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { MetadataCache, Notice, normalizePath, + Platform, Plugin, PluginSettingTab, sanitizeHTMLToDom, @@ -23,6 +24,7 @@ import { addIcons, ICON_SORT_ENABLED_ACTIVE, ICON_SORT_ENABLED_NOT_APPLIED, + ICON_SORT_MOBILE_INITIAL, ICON_SORT_SUSPENDED, ICON_SORT_SUSPENDED_GENERAL_ERROR, ICON_SORT_SUSPENDED_SYNTAX_ERROR @@ -33,13 +35,15 @@ interface CustomSortPluginSettings { suspended: boolean statusBarEntryEnabled: boolean notificationsEnabled: boolean + mobileNotificationsEnabled: boolean } const DEFAULT_SETTINGS: CustomSortPluginSettings = { additionalSortspecFile: '', suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install statusBarEntryEnabled: true, - notificationsEnabled: true + notificationsEnabled: true, + mobileNotificationsEnabled: false } const SORTSPEC_FILE_NAME: string = 'sortspec.md' @@ -53,8 +57,8 @@ type MonkeyAroundUninstaller = () => void export default class CustomSortPlugin extends Plugin { settings: CustomSortPluginSettings statusBarItemEl: HTMLElement - ribbonIconEl: HTMLElement - ribbonIconStateInaccurate: boolean + ribbonIconEl: HTMLElement // On small-screen mobile devices this is useless (ribbon is re-created on-the-fly) + ribbonIconStateInaccurate: boolean // each time when displayed sortSpecCache?: SortSpecsCollection | null initialAutoOrManualSortingTriggered: boolean @@ -62,7 +66,7 @@ export default class CustomSortPlugin extends Plugin { fileExplorerFolderPatched: boolean showNotice(message: string, timeout?: number) { - if (this.settings.notificationsEnabled) { + if (this.settings.notificationsEnabled || (Platform.isMobile && this.settings.mobileNotificationsEnabled)) { new Notice(message, timeout) } } @@ -202,6 +206,8 @@ export default class CustomSortPlugin extends Plugin { } if (updateRibbonBtnIcon) { + // REMARK: on small-screen mobile devices this is void, the handle to ribbon
Element is useless, + // as the ribbon (and its icons) get re-created each time when re-displayed (expanded) setIcon(this.ribbonIconEl, iconToSet) } @@ -222,8 +228,14 @@ export default class CustomSortPlugin extends Plugin { addIcons(); // Create an icon button in the left ribbon. + // REMARK: on small-screen mobile devices, the ribbon is dynamically re-created each time when displayed + // in result, the handle to the ribbon
Element is useless this.ribbonIconEl = this.addRibbonIcon( - this.settings.suspended ? ICON_SORT_SUSPENDED : ICON_SORT_ENABLED_NOT_APPLIED, + Platform.isDesktop ? + (this.settings.suspended ? ICON_SORT_SUSPENDED : ICON_SORT_ENABLED_NOT_APPLIED) + : + ICON_SORT_MOBILE_INITIAL // REMARK: on small-screen mobile devices this icon stays permanent + , 'Toggle custom sorting', (evt: MouseEvent) => { // Clicking the icon toggles between the states of custom sort plugin this.switchPluginStateTo(this.settings.suspended) @@ -454,5 +466,15 @@ class CustomSortSettingTab extends PluginSettingTab { this.plugin.settings.notificationsEnabled = value; await this.plugin.saveSettings(); })); + + new Setting(containerEl) + .setName('Enable notifications of plugin state changes for mobile devices only') + .setDesc('See above.') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.mobileNotificationsEnabled) + .onChange(async (value) => { + this.plugin.settings.mobileNotificationsEnabled = value; + await this.plugin.saveSettings(); + })); } } From 45be88cce1616b7ec7f01d345e92f5552ee3fc70 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 11 Feb 2023 17:41:12 +0100 Subject: [PATCH 16/26] Version bump for release --- manifest.json | 2 +- package.json | 2 +- versions.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 14d69697b..b0e531d76 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "custom-sort", "name": "Custom File Explorer sorting", - "version": "1.6.1", + "version": "1.6.2", "minAppVersion": "0.15.0", "description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer", "author": "SebastianMC", diff --git a/package.json b/package.json index e949c172d..15ebad014 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-custom-sort", - "version": "1.6.1", + "version": "1.6.2", "description": "Custom Sort plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index 6c802368c..f9b91d179 100644 --- a/versions.json +++ b/versions.json @@ -22,5 +22,6 @@ "1.4.0": "0.15.0", "1.5.0": "0.15.0", "1.6.0": "0.15.0", - "1.6.1": "0.15.0" + "1.6.1": "0.15.0", + "1.6.2": "0.15.0" } From 5775ddb6bd364b0fa335fc30dbcd843183c1ef2a Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Sat, 11 Feb 2023 17:49:49 +0100 Subject: [PATCH 17/26] Documentation updates (remarks for small screen mobile devices) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cc519454a..8e7d4c602 100644 --- a/README.md +++ b/README.md @@ -133,11 +133,11 @@ For more details on the icon states refer to [Ribbon icon section of the advance ## Small screen mobile devices remarks -- enable mobile-specific notifications -- use the 'sort on' Obsidian command palette being easily available - (swipe down gesture on small-screen mobiles) allows for quick steering of the plugin via commands: sort-on and sort-off - -could need to activate separately, even if on shared vault and active on desktop +- you might need to activate the custom sorting on your mobile separately, even if on a shared vault the custom sorting was activated on desktop +- the Obsidian command palette being easily available (swipe down gesture on small-screen mobiles) allows for quick steering of the plugin via commands: sort-on and sort-off. +This could be easier than navigating to and expanding the ribbon +- the ribbon icon is static (![Static icon](./docs/icons/icon-mobile-initial.png)) and doesn't reflect the state of custom sorting. +You can enable the _plugin state changes_ notifications in settings, for the mobile devices only ## Installing the plugin From f444614ddc32ec1bdeda1f4645be2867b5502c83 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 15 Feb 2023 22:27:57 +0100 Subject: [PATCH 18/26] #58 - Some target-folder: get ignored when sorting specs are read from two or more notes - fixed the bug --- .../sorting-spec-processor.spec.ts | 2 +- src/custom-sort/sorting-spec-processor.ts | 59 ++++++++++--------- src/main.ts | 4 +- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index de04efabf..623f4e0aa 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -930,7 +930,7 @@ describe('SortingSpecProcessor target-folder by name and regex', () => { it('should correctly handle the by-name only target-folder', () => { const inputTxtArr: Array = txtInputTargetFolderByName.split('\n') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') - expect(result?.sortSpecByPath).toEqual({}) + expect(result?.sortSpecByPath).toBeUndefined() expect(result?.sortSpecByName).toEqual(expectedSortSpecsTargetFolderByName) expect(result?.sortSpecByWildcard).not.toBeNull() }) diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 3aeb28c39..6a5e33540 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -528,16 +528,33 @@ export interface FolderNameToSortSpecMap { } export interface SortSpecsCollection { - sortSpecByPath: FolderPathToSortSpecMap - sortSpecByName: FolderNameToSortSpecMap + sortSpecByPath?: FolderPathToSortSpecMap + sortSpecByName?: FolderNameToSortSpecMap sortSpecByWildcard?: FolderWildcardMatching } -export const newSortSpecsCollection = (): SortSpecsCollection => { - return { - sortSpecByPath: {}, - sortSpecByName: {} +const ensureCollectionHasSortSpecByPath = (collection?: SortSpecsCollection | null) => { + collection = collection ?? {} + if (!collection.sortSpecByPath) { + collection.sortSpecByPath = {} + } + return collection +} + +const ensureCollectionHasSortSpecByName = (collection?: SortSpecsCollection | null) => { + collection = collection ?? {} + if (!collection.sortSpecByName) { + collection.sortSpecByName = {} } + return collection +} + +const ensureCollectionHasSortSpecByWildcard = (collection?: SortSpecsCollection | null) => { + collection = collection ?? {} + if (!collection.sortSpecByWildcard) { + collection.sortSpecByWildcard = new FolderWildcardMatching() + } + return collection } interface AdjacencyInfo { @@ -744,7 +761,6 @@ export class SortingSpecProcessor { } } - let sortspecByName: FolderNameToSortSpecMap | undefined for (let spec of this.ctx.specs) { // Consume the folder names prefixed by the designated lexeme for (let idx = 0; idx` ) return null // Failure - not allow duplicate by folderNameToMatch specs for the same folder folderNameToMatch } else { - sortspecByName[folderNameToMatch] = spec + collection.sortSpecByName![folderNameToMatch] = spec } } } } - if (sortspecByName) { - collection = collection ?? newSortSpecsCollection() - collection.sortSpecByName = sortspecByName - } - - let sortspecByWildcard: FolderWildcardMatching | undefined for (let spec of this.ctx.specs) { // Consume the folder paths ending with wildcard specs or regexp-based for (let idx = 0; idx() + collection = ensureCollectionHasSortSpecByWildcard(collection) const folderByRegexpExpression: string = path.substring(MatchFolderByRegexpLexeme.length).trim() try { const r: ConsumedFolderMatchingRegexp = consumeFolderByRegexpExpression(folderByRegexpExpression) - sortspecByWildcard.addRegexpDefinition(r.regexp, r.againstName, r.priority, r.log, spec) + collection.sortSpecByWildcard!.addRegexpDefinition(r.regexp, r.againstName, r.priority, r.log, spec) } catch (e) { this.problem(ProblemCode.InvalidOrEmptyFolderMatchingRegexp, `Invalid or empty folder regexp expression <${folderByRegexpExpression}>`) return null } } else if (endsWithWildcardPatternSuffix(path)) { - sortspecByWildcard = sortspecByWildcard ?? new FolderWildcardMatching() - const ruleAdded = sortspecByWildcard.addWildcardDefinition(path, spec) + collection = ensureCollectionHasSortSpecByWildcard(collection) + const ruleAdded = collection.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 @@ -800,16 +810,10 @@ export class SortingSpecProcessor { } } - if (sortspecByWildcard) { - collection = collection ?? newSortSpecsCollection() - collection.sortSpecByWildcard = sortspecByWildcard - } - for (let spec of this.ctx.specs) { for (let idx = 0; idx < spec.targetFoldersPaths.length; idx++) { const originalPath = spec.targetFoldersPaths[idx] if (!originalPath.startsWith(MatchFolderNameLexeme) && !originalPath.startsWith(MatchFolderByRegexpLexeme)) { - collection = collection ?? newSortSpecsCollection() const {path, detectedWildcardPriority} = stripWildcardPatternSuffix(originalPath) let storeTheSpec: boolean = true const preexistingSortSpecPriority: WildcardPriority = this.pathMatchPriorityForPath[path] @@ -823,7 +827,8 @@ export class SortingSpecProcessor { } } if (storeTheSpec) { - collection.sortSpecByPath[path] = spec + collection = ensureCollectionHasSortSpecByPath(collection) + collection.sortSpecByPath![path] = spec this.pathMatchPriorityForPath[path] = detectedWildcardPriority } } diff --git a/src/main.ts b/src/main.ts index 11ea059e0..093d27acc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -334,8 +334,8 @@ export default class CustomSortPlugin extends Plugin { // if custom sort is not specified, use the UI-selected const folder: TFolder = this.file - let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath[folder.path] - sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName[folder.name] + let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath?.[folder.path] + sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName?.[folder.name] if (sortSpec) { if (sortSpec.defaultOrder === CustomSortOrder.standardObsidian) { sortSpec = null // A folder is explicitly excluded from custom sorting plugin From 72a2febc49d06bb90a3c6ec97f78cd01f588cd12 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 15 Feb 2023 22:37:31 +0100 Subject: [PATCH 19/26] Version bump before release --- manifest.json | 2 +- package.json | 2 +- versions.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index b0e531d76..ef3696de0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "custom-sort", "name": "Custom File Explorer sorting", - "version": "1.6.2", + "version": "1.6.3", "minAppVersion": "0.15.0", "description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer", "author": "SebastianMC", diff --git a/package.json b/package.json index 15ebad014..2fc3b177b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-custom-sort", - "version": "1.6.2", + "version": "1.6.3", "description": "Custom Sort plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index f9b91d179..c2db195dc 100644 --- a/versions.json +++ b/versions.json @@ -23,5 +23,6 @@ "1.5.0": "0.15.0", "1.6.0": "0.15.0", "1.6.1": "0.15.0", - "1.6.2": "0.15.0" + "1.6.2": "0.15.0", + "1.6.3": "0.15.0" } From 6149afcadc5e8e1f227bd2c64cbe9218a98fa9ae Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 15 Feb 2023 23:25:55 +0100 Subject: [PATCH 20/26] Documentation update - preview --- docs/manual.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/manual.md b/docs/manual.md index cd3cccd04..b5529a11e 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -5,6 +5,16 @@ --- Some sections added ad-hoc, to be integrated later +# Hints, tips & tricks + +## Adding visual separators in File Explorer + +by @replete + +[Instruction and more context](https://github.com/SebastianMC/obsidian-custom-sort/discussions/57#discussioncomment-4983763) + +![separators](https://user-images.githubusercontent.com/812139/219181165-09f41420-c7f5-4c3c-a1a9-3116c5c4c9d2.png) + # Advanced features ## Priorities of sorting groups From 18faf70b5eed5e43a5a454026fb289df844f1c34 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 15 Feb 2023 23:41:08 +0100 Subject: [PATCH 21/26] Documentation update - added mention of visual CSS-based separators by @replete --- README.md | 15 +++++++++++++++ docs/img/separators-by-replete.png | Bin 0 -> 32524 bytes docs/manual.md | 13 ++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 docs/img/separators-by-replete.png diff --git a/README.md b/README.md index 8e7d4c602..e02964104 100644 --- a/README.md +++ b/README.md @@ -154,3 +154,18 @@ Search the plugin by its name 'CUSTOM FILE EXPLORER SORTING' Thanks to [Nothingislost](https://github.com/nothingislost) for the monkey-patching ideas of File Explorer in [obsidian-bartender](https://github.com/nothingislost/obsidian-bartender) +## ...and before you go, maybe you'd like the visual separators in File Explorer? + +Do you want to have a nice-looking horizontal separators in File Explorer like this? + +![separators](./docs/img/separators-by-replete.png) + +If so, head on to [Instruction and more context](https://github.com/SebastianMC/obsidian-custom-sort/discussions/57#discussioncomment-4983763) +by [@replete](https://github.com/replete)\ +Quick & easy! + +This feature is not dependent on the Custom Sorting plugin. +At the same time I'm mentioning it here because it is a side effect of a discussion with [@replete](https://github.com/replete). +We were considering a direct support of the Separators in the plugin. Eventually this boiled down to a very +concise and smart CSS-snippet based solution, independent of the plugin. Go, see, copy to the CSS-snippets in Obsidian +and enjoy the more grouped look diff --git a/docs/img/separators-by-replete.png b/docs/img/separators-by-replete.png new file mode 100644 index 0000000000000000000000000000000000000000..17d080d1016908e612b4cc64c3898db188ad7a09 GIT binary patch literal 32524 zcmZU319WE1vS>K5t;rWmoP6QLwkCEawr$(Ct%+?;Y}>Z2m;c=R&N=J7y;iTT?&_*) z?XK$T3YGmOiU5lX3jzXyATA~(|CP^xfPgMRfBS;i92;XmKoCbv1qEfr1qA`JcGgCw z7KR`os)30rFdkBikKQ##MS6?s5>O4d?VkJ+aiOZj;&lKxhE&%$-8d;@vRGt|QnPa5 zbIE|1oBZje@Vt@+Co}WXhI;gd`qsC}j!Wq{)~9!#PjS1+O#4ap$F(|-O9Xyqs%o~( zCk~O8B{n+^*~N#@yh1Y0urMs59aYsi+SQs#R=XM=n|#w??Bh@T{P*ikKI z;Q&1rucO5&oXI)GrIdM?>)ObIv78}+Jb%JjJ97@Pt?lEG*&(Q>(-{jWLwBdCRP=)e z;ba*r%YIVmGrML(53h@t-lE3eXF2;k9L(QyK}HyMFlZI9$7mY1aONaP(o`T<5G!I@ zG?bCPO<7T2O~no?lImdMk|-P9k+UEx2hAT%g@JcB`eq7i$0ju`)?*o_ycR~SQ4_0! zx39Wh!R5TqMU48fvt@0>KG~4~XdMsmh@d@0u#!a9h(Z=3&}Xkwvh;Jw4*@t;)f=_c zvbcLCs`@=?Z_ay|WRGkq{?Nm|Ub?g@G*1L7vMb%IHt2sSy#cW+@v;|6P^M~)lGSg` zWJ-6-PfJ-lKkC21ZD>9(yaZw4o}09H4!1T{vHo7M*x+Q63LNJ()3s(lS}#%VsaXD5 zvEpQ+QoR0-h``~oT2ntOZzpl+F(Uq`$JR(`yp?su3vK+jV@B_Jm+qo6$!goT*S)We z^~kGja5#syCtP44byi^4?OWhHjQiT@>qgrP`a>pi)s`FO(6J96_nlm}=cV@$@ki7&Vl%z*_3Uj#huJh8H+bKeMnWFQWWJK!k zJev&Mo_Zo$2hhd0{@ayA7ob^B5gDnOyp9tmJZ&2pFWK|y8rE9x&j^^LwD4+k zF1B?)Xp@{&2hfK`2Qd%Ip|#nYC~OlQ8}iv+Z&}O{%f~+_jZc#*fFF7Fd#4D_5*o=` z%Pz*Vb+q$M0~Y4>9k(h>&NiHFmnZl4uWe|LOAkg~_6D1G@1lrfA5PCb2*FN-JYMzp z(}#mu1dqJe3&V}Ytyx+QM*Yk6_LaA2o!R!f`~8v0zcfv4_Ggfqyggi`+aDesKk?43 zMApq*+io-IcpuyG9IdB-gdeTfAF)vO-Wne^ooE)%{&`@M;s1Pgz-r>1sXqc;yZ^fuULW`k@G+OoQuZ zUFO|!zUzw!LSg&D^*6cP{TN+S6 z!dBa-#ry+(uR$&RN6u|;#x2Fn+55>m9z5rVD*Nb(&8LsKzz!@{O{!*(myrTO3*G@O zgtnhI zjLP2mY6z5o)}s7{=at}7^f}s!rmBz9Z@eAP8iIK!vrARtiA{fgo^RY$8BK1wuC!a- zQqxY0pj(0_`!~@%!OJRSTBuZx1+b_`C^G-c`nx&~q4=+r~$ z_{_=b`?)uK8|PcxuEWcA#Cqy#W;!p0ZG|0{3#KvcxYo}+fU&F9_RwfP<~}W6hn>{* zlH=~rh|Rn06PQ!7%2$*1f(Alw9#_XsqAnAOkGY!JwOIvY((#Pz;~h1*x@Yz`;~7m? zt)}N5I830FI(jFYllYGODWfs}#SL8GsPdSWn(W3r>DUuE-LwX@D(@uDjFva|KD$y)uK>C)OI=e&so zDF{D|gX?LiKHofg-!s|U<0EvQoHd)nq3C8=>yRqb`oVK6um@}KH&we{yWPEL*B{zS z$WMa~ucv|gbWtnrj@g^Tls2?W%lC$N&kNtfCIX~S*NjK^yZBt0MXvO_wHMKyY&oF7 zXWWO?r%atU-rK8nzdI}s`)vfq<)B6Xeh}vbx=CK-&*o3P^J7>KU&+{9)h85+*c~xg z?-7U{JwfKwU+8dMHO#YtsBN;Ee$z!8wJ4YtMJgVo{lh&&jU&-=noi*)#MIovwmq%3 zLTT&y0cta|v_z>t+ifA0y~9c1ZpB2_34uND_?0UD~V|9nCsh*fq z2;pRq`s2aj!wgJC?r`OvTd1;QDY<#SKV_O+mR<{xa$%q0OjiRWNoDLaXcRS}1>x|A z!%zp0F#TeI=!JC{26##P4??@++6TcML4#_aRX+QOun(1#_f>$cj;)QjXI*!dZY z*k~5E55`RTL4kiPSY%SL4ujfzXP0*#a`eljfaJK`eGZhaf{fj{AA)L3X�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 Ts#Me4&G=*_6vQh<^#lG71qnUF literal 0 HcmV?d00001 diff --git a/docs/manual.md b/docs/manual.md index b5529a11e..8e0a4fbb2 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -9,11 +9,18 @@ Some sections added ad-hoc, to be integrated later ## Adding visual separators in File Explorer -by @replete +Do you want to have a nice-looking horizontal separators in File Explorer like this? -[Instruction and more context](https://github.com/SebastianMC/obsidian-custom-sort/discussions/57#discussioncomment-4983763) +![separators](./img/separators-by-replete.png) -![separators](https://user-images.githubusercontent.com/812139/219181165-09f41420-c7f5-4c3c-a1a9-3116c5c4c9d2.png) +If so, head on to [Instruction and more context](https://github.com/SebastianMC/obsidian-custom-sort/discussions/57#discussioncomment-4983763) +by [@replete](https://github.com/replete) + +This feature is not dependent on the Custom Sorting plugin. +At the same time I'm mentioning it here because it is a side effect of a discussion with [@replete](https://github.com/replete). +We were considering a direct support of the Separators in the plugin. Eventually this boiled down to a very +concise and smart CSS-snippet based solution, independent of the plugin. Go, see, copy to the CSS-snippets in Obsidian +and enjoy the more grouped look # Advanced features From 01af14b1744db593a21d1a4a646f961f5af4b73b Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 28 Feb 2023 23:57:08 +0100 Subject: [PATCH 22/26] #60 - Simplified integration with obsidian-icon-folder plugin - the plugin integration and matching part completed - unit tests --- src/custom-sort/custom-sort-types.ts | 4 +- src/custom-sort/custom-sort.spec.ts | 456 ++++++++++++++++++ src/custom-sort/custom-sort.ts | 56 +-- src/types/types.d.ts | 17 + .../ObsidianIconFolderPluginSignature.ts | 48 ++ src/utils/StarredPluginSignature.ts | 34 +- 6 files changed, 586 insertions(+), 29 deletions(-) create mode 100644 src/utils/ObsidianIconFolderPluginSignature.ts diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index f50f91678..05d86e70a 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -8,7 +8,8 @@ export enum CustomSortGroupType { ExactSuffix, ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title HasMetadataField, // Notes (or folder's notes) containing a specific metadata field - StarredOnly + StarredOnly, + IconFolderPlugin } export enum CustomSortOrder { @@ -59,6 +60,7 @@ export interface CustomSortGroup { matchFilenameWithExt?: boolean foldersOnly?: boolean withMetadataFieldName?: string // for 'with-metadata:' grouping + folderIconName?: string // for integration with obsidian-folder-icon community plugin priority?: number combineWithIdx?: number } diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index fabb1238c..fa89b25e5 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -12,6 +12,10 @@ import { import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, RegExpSpec} from './custom-sort-types'; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; import {findStarredFile_pathParam, Starred_PluginInstance} from "../utils/StarredPluginSignature"; +import { + ObsidianIconFolder_PluginInstance, + ObsidianIconFolderPlugin_Data +} from "../utils/ObsidianIconFolderPluginSignature"; const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => { return { @@ -1067,6 +1071,458 @@ describe('determineSortingGroup', () => { expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(2) }) }) + describe('CustomSortGroupType.IconFolderPlugin', () => { + it('should not match file w/o icon', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return {settings: {}} // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + }) + } + + // when + const result = determineSortingGroup(file, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 1, // The lastIdx+1, group not determined + isFolder: false, + sortString: "References.md", + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md' + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should not match file with icon of different name', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin, + folderIconName: 'IncorrectIconName' + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'Some parent folder/References.md': 'CorrectIconName' + } + }) + } + + // when + const result = determineSortingGroup(file, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 1, // The lastIdx+1, group not determined + isFolder: false, + sortString: "References.md", + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md' + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should match file with any icon', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'Some parent folder/References.md': 'Irrelevant icon name, only presence matters' + } + }) + } + + // when + const result = determineSortingGroup(file, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md' + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should match file with icon of expected name', () => { + // given + const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin, + folderIconName: 'CorrectIconName' + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'Some parent folder/References.md': 'CorrectIconName' + } + }) + } + + // when + const result = determineSortingGroup(file, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: false, + sortString: "References.md", + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, + mtime: MOCK_TIMESTAMP + 333, + path: 'Some parent folder/References.md' + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should not match folder w/o icon', () => { + // given + const folder: TFolder = mockTFolder('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return {settings: {}} // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 1, // The lastIdx+1, group not determined + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: [], + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should match folder with any icon (icon specified by string alone)', () => { + // given + const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'TestEmptyFolder': 'Irrelevant icon name, only presence matters' + } + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: expect.any(Array), + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should match folder with any icon (icon specified together with inheritance)', () => { + // given + const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'TestEmptyFolder': { + iconName: 'ConfiguredIcon', + inheritanceIcon: 'ConfiguredInheritanceIcon' + } + } + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: expect.any(Array), + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should match folder with specified icon (icon specified by string alone)', () => { + // given + const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin, + folderIconName: 'ConfiguredIcon-by-string' + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'TestEmptyFolder': 'ConfiguredIcon-by-string' + } + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: expect.any(Array), + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should match folder with specified icon (icon specified together with inheritance)', () => { + // given + const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin, + folderIconName: 'ConfiguredIcon' + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'TestEmptyFolder': { + iconName: 'ConfiguredIcon', + inheritanceIcon: 'ConfiguredInheritanceIcon' + } + } + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 0, + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: expect.any(Array), + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should not match folder with different icon (icon specified by string alone)', () => { + // given + const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin, + folderIconName: 'ConfiguredIcon-by-string' + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'TestEmptyFolder': 'AnotherConfiguredIcon-by-string' + } + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 1, // lastIdx+1 - no match + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: expect.any(Array), + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + it('should not match folder with different icon (icon specified together with inheritance)', () => { + // given + const folder: TFolder = mockTFolderWithChildren('TestEmptyFolder'); + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.IconFolderPlugin, + folderIconName: 'ConfiguredIcon' + }] + } + const obsidianIconFolderPluginInstance: Partial = { + getData: jest.fn( function(): ObsidianIconFolderPlugin_Data { + return { + settings: {}, // The obsidian-folder-icon plugin keeps the settings there indeed ;-) + 'TestEmptyFolder': { + iconName: 'OtherConfiguredIcon', + inheritanceIcon: 'ConfiguredInheritanceIcon' + } + } + }) + } + + // when + const result = determineSortingGroup(folder, sortSpec, { + iconFolderPluginInstance: obsidianIconFolderPluginInstance as ObsidianIconFolder_PluginInstance + }) + + // then + expect(result).toEqual({ + groupIdx: 1, // lastIdx+1 - no match + isFolder: true, + sortString: "TestEmptyFolder", + ctimeNewest: 0, + ctimeOldest: 0, + mtime: 0, + path: 'TestEmptyFolder', + folder: { + children: expect.any(Array), + isRoot: expect.any(Function), + name: "TestEmptyFolder", + parent: {}, + path: "TestEmptyFolder", + vault: {} + } + }); + expect(obsidianIconFolderPluginInstance.getData).toHaveBeenCalledTimes(1) + }) + }) describe('when sort by metadata is involved', () => { it('should correctly read direct metadata from File item (order by metadata set on group) alph', () => { // given diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index c09021f3f..3e38730ba 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -1,5 +1,6 @@ import { App, + CommunityPlugin, FrontMatterCache, InstalledPlugin, requireApiVersion, @@ -8,9 +9,19 @@ import { TFolder } from 'obsidian'; import { + determineStarredStatusOf, + getStarredPlugin, Starred_PluginInstance, StarredPlugin_findStarredFile_methodName -} from '../utils/StarredPluginSignature' +} from '../utils/StarredPluginSignature'; +import { + determineIconOf, + getIconFolderPlugin, + FolderIconObject, + ObsidianIconFolder_PluginInstance, + ObsidianIconFolderPlugin_Data, + ObsidianIconFolderPlugin_getData_methodName +} from '../utils/ObsidianIconFolderPluginSignature' import { CustomSortGroup, CustomSortGroupType, @@ -154,6 +165,7 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string): export interface Context { starredPluginInstance?: Starred_PluginInstance + iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance } export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec, ctx?: Context): FolderItemForSorting { @@ -253,17 +265,24 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus break case CustomSortGroupType.StarredOnly: if (ctx?.starredPluginInstance) { - let starred: boolean - if (aFile) { - starred = !!ctx.starredPluginInstance[StarredPlugin_findStarredFile_methodName]({path: entry.path}) - } else { // aFolder - starred = determineStarredStatusOfFolder(entry as TFolder, ctx.starredPluginInstance) - } + let starred: boolean = determineStarredStatusOf(entry, aFile, ctx.starredPluginInstance) if (starred) { determined = true } } break + case CustomSortGroupType.IconFolderPlugin: + if(ctx?.iconFolderPluginInstance) { + let iconName: string | undefined = determineIconOf(entry, ctx.iconFolderPluginInstance) + if (iconName) { + if (group.folderIconName) { + determined = iconName === group.folderIconName + } else { + determined = true + } + } + } + break case CustomSortGroupType.MatchAll: determined = true; break @@ -389,25 +408,6 @@ export const determineDatesForFolder = (folder: TFolder, now: number): [Modified return [mtimeOfFolder, ctimeNewestOfFolder, ctimeOldestOfFolder] } -export const StarredCorePluginId: string = 'starred' - -export const getStarredPlugin = (app?: App): Starred_PluginInstance | undefined => { - const starredPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(StarredCorePluginId) - if (starredPlugin && starredPlugin.enabled && starredPlugin.instance) { - const starredPluginInstance: Starred_PluginInstance = starredPlugin.instance as Starred_PluginInstance - // defensive programming, in case Obsidian changes its internal APIs - if (typeof starredPluginInstance?.[StarredPlugin_findStarredFile_methodName] === 'function') { - return starredPluginInstance - } - } -} - -export const determineStarredStatusOfFolder = (folder: TFolder, starredPluginInstance: Starred_PluginInstance): boolean => { - return folder.children.some((folderItem) => { - return !isFolder(folderItem) && starredPluginInstance[StarredPlugin_findStarredFile_methodName]({path: folderItem.path}) - }) -} - export const determineFolderDatesIfNeeded = (folderItems: Array, sortingSpec: CustomSortSpec) => { const Now: number = Date.now() folderItems.forEach((item) => { @@ -427,6 +427,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] let fileExplorer = this.fileExplorer sortingSpec._mCache = sortingSpec.plugin?.app.metadataCache const starredPluginInstance: Starred_PluginInstance | undefined = getStarredPlugin(sortingSpec?.plugin?.app) + const iconFolderPluginInstance: ObsidianIconFolder_PluginInstance | undefined = getIconFolderPlugin(sortingSpec?.plugin?.app) const folderItems: Array = (sortingSpec.itemsToHide ? this.file.children.filter((entry: TFile | TFolder) => { @@ -436,7 +437,8 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] this.file.children) .map((entry: TFile | TFolder) => { const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, { - starredPluginInstance: starredPluginInstance + starredPluginInstance: starredPluginInstance, + iconFolderPluginInstance: iconFolderPluginInstance }) return itemForSorting }) diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 240f25633..ef295f589 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -12,7 +12,24 @@ declare module 'obsidian' { id: string; } + export type CommunityPluginId = string + + // undocumented internal interface - for experimental features + export interface CommunityPlugin { + manifest: { + id: CommunityPluginId + } + _loaded: boolean + } + + // undocumented internal interface - for experimental features + export interface CommunityPlugins { + enabledPlugins: Set + plugins: {[key: CommunityPluginId]: CommunityPlugin} + } + export interface App { + plugins: CommunityPlugins; internalPlugins: InternalPlugins; // undocumented internal API - for experimental features viewRegistry: ViewRegistry; } diff --git a/src/utils/ObsidianIconFolderPluginSignature.ts b/src/utils/ObsidianIconFolderPluginSignature.ts new file mode 100644 index 000000000..f1f6e3012 --- /dev/null +++ b/src/utils/ObsidianIconFolderPluginSignature.ts @@ -0,0 +1,48 @@ +import {App, CommunityPlugin, TAbstractFile, TFile, TFolder} from "obsidian"; +import {Starred_PluginInstance} from "./StarredPluginSignature"; + +// For https://github.com/FlorianWoelki/obsidian-icon-folder + +export const ObsidianIconFolderPlugin_getData_methodName = 'getData' + +export interface FolderIconObject { + iconName: string | null; + inheritanceIcon: string; +} + +export type ObsidianIconFolderPlugin_Data = Record + +export interface ObsidianIconFolder_PluginInstance extends CommunityPlugin { + [ObsidianIconFolderPlugin_getData_methodName]: () => ObsidianIconFolderPlugin_Data +} + +// https://github.com/FlorianWoelki/obsidian-icon-folder/blob/fd9c7df1486744450cec3d7ee9cee2b34d008e56/manifest.json#L2 +export const ObsidianIconFolderPluginId: string = 'obsidian-icon-folder' + +export const getIconFolderPlugin = (app?: App): ObsidianIconFolder_PluginInstance | undefined => { + const iconFolderPlugin: CommunityPlugin | undefined = app?.plugins?.plugins?.[ObsidianIconFolderPluginId] + if (iconFolderPlugin && iconFolderPlugin._loaded && app?.plugins?.enabledPlugins?.has(ObsidianIconFolderPluginId)) { + const iconFolderPluginInstance: ObsidianIconFolder_PluginInstance = iconFolderPlugin as ObsidianIconFolder_PluginInstance + // defensive programming, in case the community plugin changes its internal APIs + if (typeof iconFolderPluginInstance?.[ObsidianIconFolderPlugin_getData_methodName] === 'function') { + return iconFolderPluginInstance + } + } +} + +// Intentionally partial and simplified, only detect icons configured directly, +// ignoring any icon inheritance or regexp-based applied icons +export const determineIconOf = (entry: TAbstractFile, iconFolderPluginInstance: ObsidianIconFolder_PluginInstance): string | undefined => { + const iconsData: ObsidianIconFolderPlugin_Data | undefined = iconFolderPluginInstance[ObsidianIconFolderPlugin_getData_methodName]() + const entryForPath: any = iconsData?.[entry.path] + // Icons configured directly + if (typeof entryForPath === 'string') { + return entryForPath + } else if (typeof (entryForPath as FolderIconObject)?.iconName === 'string') { + return (entryForPath as FolderIconObject)?.iconName ?? undefined + } else { + return undefined + } +} + + diff --git a/src/utils/StarredPluginSignature.ts b/src/utils/StarredPluginSignature.ts index db5df0228..0a541ae7b 100644 --- a/src/utils/StarredPluginSignature.ts +++ b/src/utils/StarredPluginSignature.ts @@ -1,4 +1,4 @@ -import {PluginInstance, TFile} from "obsidian"; +import {App, InstalledPlugin, PluginInstance, TAbstractFile, TFile, TFolder} from "obsidian"; export const StarredPlugin_findStarredFile_methodName = 'findStarredFile' @@ -9,3 +9,35 @@ export interface findStarredFile_pathParam { export interface Starred_PluginInstance extends PluginInstance { [StarredPlugin_findStarredFile_methodName]: (filePath: findStarredFile_pathParam) => TFile | null } + +export const StarredCorePluginId: string = 'starred' + +export const getStarredPlugin = (app?: App): Starred_PluginInstance | undefined => { + const starredPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(StarredCorePluginId) + if (starredPlugin && starredPlugin.enabled && starredPlugin.instance) { + const starredPluginInstance: Starred_PluginInstance = starredPlugin.instance as Starred_PluginInstance + // defensive programming, in case Obsidian changes its internal APIs + if (typeof starredPluginInstance?.[StarredPlugin_findStarredFile_methodName] === 'function') { + return starredPluginInstance + } + } +} + +const isFolder = (entry: TAbstractFile) => { + // The plain obvious 'entry instanceof TFolder' doesn't work inside Jest unit tests, hence a workaround below + return !!((entry as any).isRoot); +} + +export const determineStarredStatusOfFolder = (folder: TFolder, starredPluginInstance: Starred_PluginInstance): boolean => { + return folder.children.some((folderItem) => { + return !isFolder(folderItem) && starredPluginInstance[StarredPlugin_findStarredFile_methodName]({path: folderItem.path}) + }) +} + +export const determineStarredStatusOf = (entry: TFile | TFolder, aFile: boolean, starredPluginInstance: Starred_PluginInstance) => { + if (aFile) { + return !!starredPluginInstance[StarredPlugin_findStarredFile_methodName]({path: entry.path}) + } else { // aFolder + return determineStarredStatusOfFolder(entry as TFolder, starredPluginInstance) + } +} From bd875fa8041ba399c1c0e84b748bfb36f86bc031 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:42:35 +0100 Subject: [PATCH 23/26] #60 - Simplified integration with obsidian-icon-folder plugin - extended the parser with support of new lexeme with-icon:, with or w/o parameter - unit tests --- src/custom-sort/custom-sort-types.ts | 4 +-- src/custom-sort/custom-sort.spec.ts | 36 +++++++++---------- src/custom-sort/custom-sort.ts | 6 ++-- .../sorting-spec-processor.spec.ts | 18 ++++++++-- src/custom-sort/sorting-spec-processor.ts | 11 ++++++ 5 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 05d86e70a..0612b0019 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -9,7 +9,7 @@ export enum CustomSortGroupType { ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title HasMetadataField, // Notes (or folder's notes) containing a specific metadata field StarredOnly, - IconFolderPlugin + HasIcon } export enum CustomSortOrder { @@ -60,7 +60,7 @@ export interface CustomSortGroup { matchFilenameWithExt?: boolean foldersOnly?: boolean withMetadataFieldName?: string // for 'with-metadata:' grouping - folderIconName?: string // for integration with obsidian-folder-icon community plugin + iconName?: string // for integration with obsidian-folder-icon community plugin priority?: number combineWithIdx?: number } diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index fa89b25e5..8b93d2317 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -1071,14 +1071,14 @@ describe('determineSortingGroup', () => { expect(starredPluginInstance.findStarredFile).toHaveBeenCalledTimes(2) }) }) - describe('CustomSortGroupType.IconFolderPlugin', () => { + describe('CustomSortGroupType.HasIcon', () => { it('should not match file w/o icon', () => { // given const file: TFile = mockTFile('References', 'md', 111, MOCK_TIMESTAMP + 222, MOCK_TIMESTAMP + 333); const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin + type: CustomSortGroupType.HasIcon }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1110,8 +1110,8 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin, - folderIconName: 'IncorrectIconName' + type: CustomSortGroupType.HasIcon, + iconName: 'IncorrectIconName' }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1146,7 +1146,7 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin + type: CustomSortGroupType.HasIcon }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1181,8 +1181,8 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin, - folderIconName: 'CorrectIconName' + type: CustomSortGroupType.HasIcon, + iconName: 'CorrectIconName' }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1217,7 +1217,7 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin + type: CustomSortGroupType.HasIcon }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1257,7 +1257,7 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin + type: CustomSortGroupType.HasIcon }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1300,7 +1300,7 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin + type: CustomSortGroupType.HasIcon }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1346,8 +1346,8 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin, - folderIconName: 'ConfiguredIcon-by-string' + type: CustomSortGroupType.HasIcon, + iconName: 'ConfiguredIcon-by-string' }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1390,8 +1390,8 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin, - folderIconName: 'ConfiguredIcon' + type: CustomSortGroupType.HasIcon, + iconName: 'ConfiguredIcon' }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1437,8 +1437,8 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin, - folderIconName: 'ConfiguredIcon-by-string' + type: CustomSortGroupType.HasIcon, + iconName: 'ConfiguredIcon-by-string' }] } const obsidianIconFolderPluginInstance: Partial = { @@ -1481,8 +1481,8 @@ describe('determineSortingGroup', () => { const sortSpec: CustomSortSpec = { targetFoldersPaths: ['/'], groups: [{ - type: CustomSortGroupType.IconFolderPlugin, - folderIconName: 'ConfiguredIcon' + type: CustomSortGroupType.HasIcon, + iconName: 'ConfiguredIcon' }] } const obsidianIconFolderPluginInstance: Partial = { diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 3e38730ba..0f3d04b1a 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -271,12 +271,12 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus } } break - case CustomSortGroupType.IconFolderPlugin: + case CustomSortGroupType.HasIcon: if(ctx?.iconFolderPluginInstance) { let iconName: string | undefined = determineIconOf(entry, ctx.iconFolderPluginInstance) if (iconName) { - if (group.folderIconName) { - determined = iconName === group.folderIconName + if (group.iconName) { + determined = iconName === group.iconName } else { determined = true } diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index 623f4e0aa..e181b3690 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -1,7 +1,9 @@ import { CompoundDashNumberNormalizerFn, CompoundDashRomanNumberNormalizerFn, - CompoundDotNumberNormalizerFn, ConsumedFolderMatchingRegexp, consumeFolderByRegexpExpression, + CompoundDotNumberNormalizerFn, + ConsumedFolderMatchingRegexp, + consumeFolderByRegexpExpression, convertPlainStringToRegex, detectNumericSortingSymbols, escapeRegexUnsafeCharacters, @@ -29,6 +31,8 @@ target-folder: tricky folder < a-z by-metadata: Some-dedicated-field with-metadata: Pages > a-z by-metadata: +/: with-icon: +with-icon: RiClock24 starred: /:files starred: /folders starred: @@ -85,6 +89,8 @@ target-folder: tricky folder 2 < a-z by-metadata: Some-dedicated-field % with-metadata: Pages > a-z by-metadata: +/:files with-icon: +/folders:files with-icon: RiClock24 /folders:files starred: /:files starred: /folders starred: @@ -171,6 +177,14 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = { type: CustomSortGroupType.HasMetadataField, withMetadataFieldName: 'Pages', order: CustomSortOrder.byMetadataFieldAlphabeticalReverse + }, { + type: CustomSortGroupType.HasIcon, + order: CustomSortOrder.alphabetical, + filesOnly: true + }, { + type: CustomSortGroupType.HasIcon, + order: CustomSortOrder.alphabetical, + iconName: 'RiClock24' }, { type: CustomSortGroupType.StarredOnly, order: CustomSortOrder.alphabetical @@ -186,7 +200,7 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = { order: CustomSortOrder.alphabetical, type: CustomSortGroupType.Outsiders }], - outsidersGroupIdx: 5, + outsidersGroupIdx: 7, targetFoldersPaths: [ 'tricky folder 2' ] diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 6a5e33540..e6c31f037 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -207,6 +207,8 @@ const MetadataFieldIndicatorLexeme: string = 'with-metadata:' const StarredItemsIndicatorLexeme: string = 'starred:' +const IconIndicatorLexeme: string = 'with-icon:' + const CommentPrefix: string = '//' const PriorityModifierPrio1Lexeme: string = '/!' @@ -1489,6 +1491,15 @@ export class SortingSpecProcessor { foldersOnly: spec.foldersOnly, matchFilenameWithExt: spec.matchFilenameWithExt } + } else if (theOnly.startsWith(IconIndicatorLexeme)) { + const iconName: string | undefined = extractIdentifier(theOnly.substring(IconIndicatorLexeme.length)) + return { + type: CustomSortGroupType.HasIcon, + iconName: iconName, + filesOnly: spec.filesOnly, + foldersOnly: spec.foldersOnly, + matchFilenameWithExt: spec.matchFilenameWithExt + } } else if (theOnly.startsWith(StarredItemsIndicatorLexeme)) { return { type: CustomSortGroupType.StarredOnly, From fe68c554b88910d542d6cdae342b49f663cd8a5b Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Wed, 1 Mar 2023 11:04:46 +0100 Subject: [PATCH 24/26] Version bump before release --- manifest.json | 2 +- package.json | 2 +- versions.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index ef3696de0..52a50d151 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "custom-sort", "name": "Custom File Explorer sorting", - "version": "1.6.3", + "version": "1.7.0", "minAppVersion": "0.15.0", "description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer", "author": "SebastianMC", diff --git a/package.json b/package.json index 2fc3b177b..f54c1fe6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-custom-sort", - "version": "1.6.3", + "version": "1.7.0", "description": "Custom Sort plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index c2db195dc..2eb024907 100644 --- a/versions.json +++ b/versions.json @@ -24,5 +24,6 @@ "1.6.0": "0.15.0", "1.6.1": "0.15.0", "1.6.2": "0.15.0", - "1.6.3": "0.15.0" + "1.6.3": "0.15.0", + "1.7.0": "0.15.0" } From e788c92543aa4d76da58673bd196e80771716eb4 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 7 Mar 2023 19:01:44 +0100 Subject: [PATCH 25/26] #67 - feature alphabetic wildcard - sorting symbol \a+ for ASCII word - sorting symbol \A+ for any modern language word (involved advanced unicode regexp) --- src/custom-sort/custom-sort-types.ts | 1 + src/custom-sort/matchers.spec.ts | 109 ++++++++++++++++-- src/custom-sort/matchers.ts | 27 +++-- .../sorting-spec-processor.spec.ts | 52 ++++++--- src/custom-sort/sorting-spec-processor.ts | 89 ++++++++------ 5 files changed, 203 insertions(+), 75 deletions(-) diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 0612b0019..9a88b7fa7 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -40,6 +40,7 @@ export interface RecognizedOrderValue { } export type NormalizerFn = (s: string) => string | null +export const IdentityNormalizerFn: NormalizerFn = (s: string) => s export interface RegExpSpec { regex: RegExp diff --git a/src/custom-sort/matchers.spec.ts b/src/custom-sort/matchers.spec.ts index a9fffaed6..823c0f855 100644 --- a/src/custom-sort/matchers.spec.ts +++ b/src/custom-sort/matchers.spec.ts @@ -3,15 +3,22 @@ import { getNormalizedRomanNumber, prependWithZeros, romanToIntStr, - NumberRegex, - CompoundNumberDotRegex, - CompoundNumberDashRegex, - RomanNumberRegex, - CompoundRomanNumberDotRegex, - CompoundRomanNumberDashRegex + NumberRegexStr, + CompoundNumberDotRegexStr, + CompoundNumberDashRegexStr, + RomanNumberRegexStr, + CompoundRomanNumberDotRegexStr, + CompoundRomanNumberDashRegexStr, + WordInASCIIRegexStr, + WordInAnyLanguageRegexStr } from "./matchers"; +import {SortingSpecProcessor} from "./sorting-spec-processor"; describe('Plain numbers regexp', () => { + let regexp: RegExp; + beforeEach(() => { + regexp = new RegExp('^' + NumberRegexStr, 'i'); + }); it.each([ ['', null], [' ', null], @@ -23,7 +30,7 @@ describe('Plain numbers regexp', () => { ['9', '9'], ['7328964783268794325496783', '7328964783268794325496783'] ])('%s => %s', (s: string, out: string | null) => { - const match: RegExpMatchArray | null = s.match(NumberRegex) + const match: RegExpMatchArray | null = s.match(regexp) if (out) { expect(match).not.toBeNull() expect(match?.[1]).toBe(out) @@ -34,6 +41,10 @@ describe('Plain numbers regexp', () => { }) describe('Plain compound numbers regexp (dot)', () => { + let regexp: RegExp; + beforeEach(() => { + regexp = new RegExp('^' + CompoundNumberDotRegexStr, 'i'); + }); it.each([ ['', null], [' ', null], @@ -55,7 +66,7 @@ describe('Plain compound numbers regexp (dot)', () => { ['56.78.-.1abc', '56.78'], ['56.78-.1abc', '56.78'], ])('%s => %s', (s: string, out: string | null) => { - const match: RegExpMatchArray | null = s.match(CompoundNumberDotRegex) + const match: RegExpMatchArray | null = s.match(regexp) if (out) { expect(match).not.toBeNull() expect(match?.[1]).toBe(out) @@ -66,6 +77,10 @@ describe('Plain compound numbers regexp (dot)', () => { }) describe('Plain compound numbers regexp (dash)', () => { + let regexp: RegExp; + beforeEach(() => { + regexp = new RegExp('^' + CompoundNumberDashRegexStr, 'i'); + }); it.each([ ['', null], [' ', null], @@ -87,7 +102,7 @@ describe('Plain compound numbers regexp (dash)', () => { ['56-78-.-1abc', '56-78'], ['56-78.-1abc', '56-78'], ])('%s => %s', (s: string, out: string | null) => { - const match: RegExpMatchArray | null = s.match(CompoundNumberDashRegex) + const match: RegExpMatchArray | null = s.match(regexp) if (out) { expect(match).not.toBeNull() expect(match?.[1]).toBe(out) @@ -98,6 +113,10 @@ describe('Plain compound numbers regexp (dash)', () => { }) describe('Plain Roman numbers regexp', () => { + let regexp: RegExp; + beforeEach(() => { + regexp = new RegExp('^' + RomanNumberRegexStr, 'i'); + }); it.each([ ['', null], [' ', null], @@ -109,7 +128,7 @@ describe('Plain Roman numbers regexp', () => { ['iiiii', 'iiiii'], ['viviviv794325496783', 'viviviv'] ])('%s => %s', (s: string, out: string | null) => { - const match: RegExpMatchArray | null = s.match(RomanNumberRegex) + const match: RegExpMatchArray | null = s.match(regexp) if (out) { expect(match).not.toBeNull() expect(match?.[1]).toBe(out) @@ -120,6 +139,10 @@ describe('Plain Roman numbers regexp', () => { }) describe('Roman compound numbers regexp (dot)', () => { + let regexp: RegExp; + beforeEach(() => { + regexp = new RegExp('^' + CompoundRomanNumberDotRegexStr, 'i'); + }); it.each([ ['', null], [' ', null], @@ -143,7 +166,7 @@ describe('Roman compound numbers regexp (dot)', () => { ['xvx.d-.iabc', 'xvx.d'], ['xvx.d..iabc', 'xvx.d'], ])('%s => %s', (s: string, out: string | null) => { - const match: RegExpMatchArray | null = s.match(CompoundRomanNumberDotRegex) + const match: RegExpMatchArray | null = s.match(regexp) if (out) { expect(match).not.toBeNull() expect(match?.[1]).toBe(out) @@ -154,6 +177,10 @@ describe('Roman compound numbers regexp (dot)', () => { }) describe('Roman compound numbers regexp (dash)', () => { + let regexp: RegExp; + beforeEach(() => { + regexp = new RegExp('^' + CompoundRomanNumberDashRegexStr, 'i'); + }); it.each([ ['', null], [' ', null], @@ -177,7 +204,65 @@ describe('Roman compound numbers regexp (dash)', () => { ['xvx-d.-iabc', 'xvx-d'], ['xvx-d--iabc', 'xvx-d'] ])('%s => %s', (s: string, out: string | null) => { - const match: RegExpMatchArray | null = s.match(CompoundRomanNumberDashRegex) + const match: RegExpMatchArray | null = s.match(regexp) + if (out) { + expect(match).not.toBeNull() + expect(match?.[1]).toBe(out) + } else { + expect(match).toBeNull() + } + }) +}) + +describe('ASCII word regexp', () => { + let regexp: RegExp; + beforeEach(() => { + regexp = new RegExp('^' + WordInASCIIRegexStr, 'i'); + }); + it.each([ + ['', null], + [' ', null], + [' I', null], // leading spaces are not swallowed + ['I ', 'I'], // trailing spaces are swallowed + ['Abc', 'Abc'], + ['Sun', 'Sun'], + ['Hello123', 'Hello'], + ['John_', 'John'], + ['Title.', 'Title'], + ['Deutschstäder', 'Deutschst'], + ['ItalianoàèéìòùÈ', 'Italiano'], + ['PolskićśńĄł', 'Polski'] + ])('%s => %s', (s: string, out: string | null) => { + const match: RegExpMatchArray | null = s.match(regexp) + if (out) { + expect(match).not.toBeNull() + expect(match?.[1]).toBe(out) + } else { + expect(match).toBeNull() + } + }) +}) + +describe('Unicode word regexp', () => { + let regexp: RegExp; + beforeEach(() => { + regexp = new RegExp('^' + WordInAnyLanguageRegexStr, 'ui'); + }); + it.each([ + ['', null], + [' ', null], + [' I', null], // leading spaces are not swallowed + ['I ', 'I'], // trailing characters are ignored in unit test + ['Abc', 'Abc'], + ['Sun', 'Sun'], + ['Hello123', 'Hello'], + ['John_', 'John'], + ['Title.', 'Title'], + ['Deutschstäder_', 'Deutschstäder'], + ['ItalianoàèéìòùÈ', 'ItalianoàèéìòùÈ'], + ['PolskićśńĄł', 'PolskićśńĄł'] + ])('%s => %s', (s: string, out: string | null) => { + const match: RegExpMatchArray | null = s.match(regexp) if (out) { expect(match).not.toBeNull() expect(match?.[1]).toBe(out) diff --git a/src/custom-sort/matchers.ts b/src/custom-sort/matchers.ts index b1fde5052..0f0ae3726 100644 --- a/src/custom-sort/matchers.ts +++ b/src/custom-sort/matchers.ts @@ -1,16 +1,10 @@ -export const RomanNumberRegex: RegExp = /^ *([MDCLXVI]+)/i; // Roman number -export const RomanNumberRegexStr: string = ' *([MDCLXVI]+)'; -export const CompoundRomanNumberDotRegex: RegExp = /^ *([MDCLXVI]+(?:\.[MDCLXVI]+)*)/i; // Compound Roman number with dot as separator -export const CompoundRomanNumberDotRegexStr: string = ' *([MDCLXVI]+(?:\\.[MDCLXVI]+)*)'; -export const CompoundRomanNumberDashRegex: RegExp = /^ *([MDCLXVI]+(?:-[MDCLXVI]+)*)/i; // Compound Roman number with dash as separator -export const CompoundRomanNumberDashRegexStr: string = ' *([MDCLXVI]+(?:-[MDCLXVI]+)*)'; +export const RomanNumberRegexStr: string = ' *([MDCLXVI]+)'; // Roman number +export const CompoundRomanNumberDotRegexStr: string = ' *([MDCLXVI]+(?:\\.[MDCLXVI]+)*)';// Compound Roman number with dot as separator +export const CompoundRomanNumberDashRegexStr: string = ' *([MDCLXVI]+(?:-[MDCLXVI]+)*)'; // Compound Roman number with dash as separator -export const NumberRegex: RegExp = /^ *(\d+)/; // Plain number -export const NumberRegexStr: string = ' *(\\d+)'; -export const CompoundNumberDotRegex: RegExp = /^ *(\d+(?:\.\d+)*)/; // Compound number with dot as separator -export const CompoundNumberDotRegexStr: string = ' *(\\d+(?:\\.\\d+)*)'; -export const CompoundNumberDashRegex: RegExp = /^ *(\d+(?:-\d+)*)/; // Compound number with dash as separator -export const CompoundNumberDashRegexStr: string = ' *(\\d+(?:-\\d+)*)'; +export const NumberRegexStr: string = ' *(\\d+)'; // Plain number +export const CompoundNumberDotRegexStr: string = ' *(\\d+(?:\\.\\d+)*)'; // Compound number with dot as separator +export const CompoundNumberDashRegexStr: string = ' *(\\d+(?:-\\d+)*)'; // Compound number with dash as separator export const DOT_SEPARATOR = '.' export const DASH_SEPARATOR = '-' @@ -20,6 +14,15 @@ const PIPE_SEPARATOR = '|' // ASCII 124 export const DEFAULT_NORMALIZATION_PLACES = 8; // Fixed width of a normalized number (with leading zeros) +// Property escapes: +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes +// https://stackoverflow.com/a/48902765 +// +// Using Unicode property escapes to express 'a letter in any modern language' +export const WordInAnyLanguageRegexStr = '(\\p{Letter}+)' // remember about the /u option -> /\p{Letter}+/u + +export const WordInASCIIRegexStr = '([a-zA-Z]+)' + export function prependWithZeros(s: string, minLength: number) { if (s.length < minLength) { const delta: number = minLength - s.length; diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index e181b3690..f77427039 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -5,16 +5,16 @@ import { ConsumedFolderMatchingRegexp, consumeFolderByRegexpExpression, convertPlainStringToRegex, - detectNumericSortingSymbols, + detectSortingSymbols, escapeRegexUnsafeCharacters, - extractNumericSortingSymbol, - hasMoreThanOneNumericSortingSymbol, + extractSortingSymbol, + hasMoreThanOneSortingSymbol, NumberNormalizerFn, RegexpUsedAs, RomanNumberNormalizerFn, SortingSpecProcessor } from "./sorting-spec-processor" -import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types"; +import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types"; import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules"; const txtInputExampleA: string = ` @@ -347,7 +347,7 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = { } } -const expectedSortSpecsExampleNumericSortingSymbols: { [key: string]: CustomSortSpec } = { +const expectedSortSpecsExampleSortingSymbols: { [key: string]: CustomSortSpec } = { "mock-folder": { groups: [{ foldersOnly: true, @@ -388,21 +388,37 @@ const expectedSortSpecsExampleNumericSortingSymbols: { [key: string]: CustomSort regex: / *(\d+)plain syntax\?\?\?$/i, normalizerFn: NumberNormalizerFn } + }, { + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactName, + regexPrefix: { + regex: /^Here goes ASCII word ([a-zA-Z]+)$/i, + normalizerFn: IdentityNormalizerFn + } + }, { + order: CustomSortOrder.alphabetical, + type: CustomSortGroupType.ExactName, + regexPrefix: { + regex: /^(\p{Letter}+)\. is for any modern language word$/iu, + normalizerFn: IdentityNormalizerFn + } }, { type: CustomSortGroupType.Outsiders, order: CustomSortOrder.alphabetical, }], targetFoldersPaths: ['mock-folder'], - outsidersGroupIdx: 5 + outsidersGroupIdx: 7 } } -const txtInputExampleNumericSortingSymbols: string = ` +const txtInputExampleSortingSymbols: string = ` /folders Chapter \\.d+ ... /:files ...section \\-r+. % Appendix \\-d+ (attachments) Plain syntax\\R+ ... works? And this kind of... \\D+plain syntax??? +Here goes ASCII word \\a+ +\\A+. is for any modern language word ` describe('SortingSpecProcessor', () => { @@ -420,10 +436,10 @@ describe('SortingSpecProcessor', () => { const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleA) }) - it('should generate correct SortSpecs (example with numerical sorting symbols)', () => { - const inputTxtArr: Array = txtInputExampleNumericSortingSymbols.split('\n') + it('should generate correct SortSpecs (example with sorting symbols)', () => { + const inputTxtArr: Array = txtInputExampleSortingSymbols.split('\n') const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') - expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleNumericSortingSymbols) + expect(result?.sortSpecByPath).toEqual(expectedSortSpecsExampleSortingSymbols) }) }) @@ -1735,7 +1751,7 @@ describe('SortingSpecProcessor error detection and reporting', () => { expect(result).toBeNull() expect(errorsLogger).toHaveBeenCalledTimes(2) expect(errorsLogger).toHaveBeenNthCalledWith(1, - `${ERR_PREFIX} 9:TooManyNumericSortingSymbols Maximum one numeric sorting indicator allowed per line ${ERR_SUFFIX_IN_LINE(2)}`) + `${ERR_PREFIX} 9:TooManySortingSymbols Maximum one sorting symbol allowed per line ${ERR_SUFFIX_IN_LINE(2)}`) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT('% Chapter\\R+ ... page\\d+ ')) }) it('should recognize error: nested standard obsidian sorting attribute', () => { @@ -1916,7 +1932,7 @@ describe('SortingSpecProcessor error detection and reporting', () => { expect(result).toBeNull() expect(errorsLogger).toHaveBeenCalledTimes(2) expect(errorsLogger).toHaveBeenNthCalledWith(1, - `${ERR_PREFIX} 10:NumericalSymbolAdjacentToWildcard Numerical sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case. ${ERR_SUFFIX_IN_LINE(1)}`) + `${ERR_PREFIX} 10:SortingSymbolAdjacentToWildcard Sorting symbol must not be directly adjacent to a wildcard because of potential performance problem. An additional explicit separator helps in such case. ${ERR_SUFFIX_IN_LINE(1)}`) expect(errorsLogger).toHaveBeenNthCalledWith(2, ERR_LINE_TXT(s)) }) it.each([ @@ -2092,7 +2108,7 @@ describe('escapeRegexUnsafeCharacters', () => { }) }) -describe('detectNumericSortingSymbols', () => { +describe('detectSortingSymbols', () => { it.each([ ['', false], ['d+', false], @@ -2107,12 +2123,12 @@ describe('detectNumericSortingSymbols', () => { ['\\d+abcd\\d+efgh', true], ['\\d+\\.D+\\-d+\\R+\\.r+\\-R+ \\d+', true] ])('should correctly detect in >%s< (%s) sorting regex symbols', (s: string, b: boolean) => { - const result = detectNumericSortingSymbols(s) + const result = detectSortingSymbols(s) expect(result).toBe(b) }) }) -describe('hasMoreThanOneNumericSortingSymbol', () => { +describe('hasMoreThanOneSortingSymbol', () => { it.each([ ['', false], [' d+', false], @@ -2128,12 +2144,12 @@ describe('hasMoreThanOneNumericSortingSymbol', () => { ['\\R+abcd\\.R+efgh', true], ['\\d+\\.D+\\-d+\\R+\\.r+\\-R+ \\d+', true] ])('should correctly detect in >%s< (%s) sorting regex symbols', (s: string, b: boolean) => { - const result = hasMoreThanOneNumericSortingSymbol(s) + const result = hasMoreThanOneSortingSymbol(s) expect(result).toBe(b) }) }) -describe('extractNumericSortingSymbol', () => { +describe('extractSortingSymbol', () => { it.each([ ['', null], ['d+', null], @@ -2144,7 +2160,7 @@ describe('extractNumericSortingSymbol', () => { ['--\\.D+\\d+', '\\.D+'], ['wdwqwqe\\d+\\.D+\\-d+\\R+\\.r+\\-R+ \\d+', '\\d+'] ])('should correctly extract from >%s< the numeric sorting symbol (%s)', (s: string, ss: string) => { - const result = extractNumericSortingSymbol(s) + const result = extractSortingSymbol(s) expect(result).toBe(ss) }) }) diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index e6c31f037..937f5ba62 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -4,6 +4,7 @@ import { CustomSortOrder, CustomSortSpec, DEFAULT_METADATA_FIELD_FOR_SORTING, + IdentityNormalizerFn, NormalizerFn, RecognizedOrderValue, RegExpSpec @@ -19,10 +20,11 @@ import { getNormalizedNumber, getNormalizedRomanNumber, NumberRegexStr, - RomanNumberRegexStr + RomanNumberRegexStr, + WordInAnyLanguageRegexStr, + WordInASCIIRegexStr } from "./matchers"; import { - FolderMatchingRegexp, FolderWildcardMatching, MATCH_ALL_SUFFIX, MATCH_CHILDREN_1_SUFFIX, @@ -62,8 +64,8 @@ export enum ProblemCode { NoSpaceBetweenAttributeAndValue, InvalidAttributeValue, TargetFolderNestedSpec, - TooManyNumericSortingSymbols, - NumericalSymbolAdjacentToWildcard, + TooManySortingSymbols, + SortingSymbolAdjacentToWildcard, ItemToHideExactNameWithExtRequired, ItemToHideNoSupportForThreeDots, DuplicateWildcardSortSpecForSameFolder, @@ -279,6 +281,9 @@ const NumberRegexSymbol: string = '\\d+' // Plain number const CompoundNumberDotRegexSymbol: string = '\\.d+' // Compound number with dot as separator const CompoundNumberDashRegexSymbol: string = '\\-d+' // Compound number with dash as separator +const WordInASCIIRegexSymbol: string = '\\a+' +const WordInAnyLanguageRegexSymbol: string = '\\A+' + const InlineRegexSymbol_Digit1: string = '\\d' const InlineRegexSymbol_Digit2: string = '\\[0-9]' const InlineRegexSymbol_0_to_3: string = '\\[0-3]' @@ -289,16 +294,18 @@ export const escapeRegexUnsafeCharacters = (s: string): string => { return s.replace(UnsafeRegexCharsRegex, '\\$&') } -const numericSortingSymbolsArr: Array = [ +const sortingSymbolsArr: Array = [ escapeRegexUnsafeCharacters(NumberRegexSymbol), escapeRegexUnsafeCharacters(RomanNumberRegexSymbol), escapeRegexUnsafeCharacters(CompoundNumberDotRegexSymbol), escapeRegexUnsafeCharacters(CompoundNumberDashRegexSymbol), escapeRegexUnsafeCharacters(CompoundRomanNumberDotRegexSymbol), escapeRegexUnsafeCharacters(CompoundRomanNumberDashRegexSymbol), + escapeRegexUnsafeCharacters(WordInASCIIRegexSymbol), + escapeRegexUnsafeCharacters(WordInAnyLanguageRegexSymbol) ] -const numericSortingSymbolsRegex = new RegExp(numericSortingSymbolsArr.join('|'), 'gi') +const sortingSymbolsRegex = new RegExp(sortingSymbolsArr.join('|'), 'gi') const inlineRegexSymbolsArrEscapedForRegex: Array = [ escapeRegexUnsafeCharacters(InlineRegexSymbol_Digit1), @@ -315,13 +322,13 @@ const inlineRegexSymbolsToRegexExpressionsArr: { [key: string]: string} = { const inlineRegexSymbolsDetectionRegex = new RegExp(inlineRegexSymbolsArrEscapedForRegex.join('|'), 'gi') -export const hasMoreThanOneNumericSortingSymbol = (s: string): boolean => { - numericSortingSymbolsRegex.lastIndex = 0 - return numericSortingSymbolsRegex.test(s) && numericSortingSymbolsRegex.test(s) +export const hasMoreThanOneSortingSymbol = (s: string): boolean => { + sortingSymbolsRegex.lastIndex = 0 + return sortingSymbolsRegex.test(s) && sortingSymbolsRegex.test(s) } -export const detectNumericSortingSymbols = (s: string): boolean => { - numericSortingSymbolsRegex.lastIndex = 0 - return numericSortingSymbolsRegex.test(s) +export const detectSortingSymbols = (s: string): boolean => { + sortingSymbolsRegex.lastIndex = 0 + return sortingSymbolsRegex.test(s) } export const detectInlineRegex = (s?: string): boolean => { @@ -329,10 +336,10 @@ export const detectInlineRegex = (s?: string): boolean => { return s ? inlineRegexSymbolsDetectionRegex.test(s) : false } -export const extractNumericSortingSymbol = (s?: string): string | null => { +export const extractSortingSymbol = (s?: string): string | null => { if (s) { - numericSortingSymbolsRegex.lastIndex = 0 - const matches: RegExpMatchArray | null = numericSortingSymbolsRegex.exec(s) + sortingSymbolsRegex.lastIndex = 0 + const matches: RegExpMatchArray | null = sortingSymbolsRegex.exec(s) return matches ? matches[0] : null } else { return null @@ -343,6 +350,7 @@ export interface RegExpSpecStr { regexpStr: string normalizerFn: NormalizerFn advancedRegexType: AdvancedRegexType + unicodeRegex?: boolean } // Exposed as named exports to allow unit testing @@ -360,10 +368,12 @@ export enum AdvancedRegexType { CompoundDashNumber, RomanNumber, CompoundDotRomanNumber, - CompoundDashRomanNumber + CompoundDashRomanNumber, + WordInASCII, + WordInAnyLanguage } -const numericSortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { +const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { [RomanNumberRegexSymbol.toLowerCase()]: { regexpStr: RomanNumberRegexStr, normalizerFn: RomanNumberNormalizerFn, @@ -393,6 +403,17 @@ const numericSortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { regexpStr: CompoundNumberDashRegexStr, normalizerFn: CompoundDashNumberNormalizerFn, advancedRegexType: AdvancedRegexType.CompoundDashNumber + }, + [WordInASCIIRegexSymbol]: { // Intentionally retain character case + regexpStr: WordInASCIIRegexStr, + normalizerFn: IdentityNormalizerFn, + advancedRegexType: AdvancedRegexType.WordInASCII + }, + [WordInAnyLanguageRegexSymbol]: { // Intentionally retain character case + regexpStr: WordInAnyLanguageRegexStr, + normalizerFn: IdentityNormalizerFn, + advancedRegexType: AdvancedRegexType.WordInAnyLanguage, + unicodeRegex: true } } @@ -435,17 +456,19 @@ export const convertPlainStringToFullMatchRegex = (s: string): RegexMatcherInfo export const convertPlainStringToRegex = (s: string, actAs: RegexpUsedAs): RegexMatcherInfo | null => { const regexMatchesStart: boolean = [RegexpUsedAs.Prefix, RegexpUsedAs.FullMatch].includes(actAs) const regexMatchesEnding: boolean = [RegexpUsedAs.Suffix, RegexpUsedAs.FullMatch].includes(actAs) - const detectedSymbol: string | null = extractNumericSortingSymbol(s) + const detectedSymbol: string | null = extractSortingSymbol(s) if (detectedSymbol) { - const replacement: RegExpSpecStr = numericSortingSymbolToRegexpStr[detectedSymbol.toLowerCase()] + // for some sorting symbols lower- and upper-case syntax has different meaning, for some others not + const replacement: RegExpSpecStr = sortingSymbolToRegexpStr[detectedSymbol] ?? sortingSymbolToRegexpStr[detectedSymbol.toLowerCase()] const [extractedPrefix, extractedSuffix] = s!.split(detectedSymbol) const regexPrefix: string = regexMatchesStart ? '^' : '' const regexSuffix: string = regexMatchesEnding ? '$' : '' const escapedProcessedPrefix: string = convertInlineRegexSymbolsAndEscapeTheRest(extractedPrefix) const escapedProcessedSuffix: string = convertInlineRegexSymbolsAndEscapeTheRest(extractedSuffix) + const regexFlags: string = replacement.unicodeRegex ? 'ui' : 'i' return { regexpSpec: { - regex: new RegExp(`${regexPrefix}${escapedProcessedPrefix}${replacement.regexpStr}${escapedProcessedSuffix}${regexSuffix}`, 'i'), + regex: new RegExp(`${regexPrefix}${escapedProcessedPrefix}${replacement.regexpStr}${escapedProcessedSuffix}${regexSuffix}`, regexFlags), normalizerFn: replacement.normalizerFn }, prefix: extractedPrefix, @@ -680,7 +703,7 @@ const extractIdentifier = (text: string, defaultResult?: string): string | undef return identifier ? identifier : defaultResult } -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 = "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 { ctx: ProcessingContext @@ -983,8 +1006,8 @@ export class SortingSpecProcessor { private parseSortingGroupSpec = (line: string): ParsedSortingGroup | null => { let s: string = line.trim() - if (hasMoreThanOneNumericSortingSymbol(s)) { - this.problem(ProblemCode.TooManyNumericSortingSymbols, 'Maximum one numeric sorting indicator allowed per line') + if (hasMoreThanOneSortingSymbol(s)) { + this.problem(ProblemCode.TooManySortingSymbols, 'Maximum one sorting symbol allowed per line') return null } @@ -1151,7 +1174,7 @@ export class SortingSpecProcessor { if (group.itemToHide) { if (!this.consumeParsedItemToHide(group)) { - this.problem(ProblemCode.ItemToHideNoSupportForThreeDots, 'For hiding of file or folder, the exact name with ext is required and no numeric sorting indicator allowed') + this.problem(ProblemCode.ItemToHideNoSupportForThreeDots, 'For hiding of file or folder, the exact name with ext is required and no sorting symbols allowed') return false } else { return true @@ -1159,7 +1182,7 @@ export class SortingSpecProcessor { } else { // !group.itemToHide const newGroup: CustomSortGroup | null = this.consumeParsedSortingGroupSpec(group) if (newGroup) { - if (this.adjustSortingGroupForNumericSortingSymbol(newGroup)) { + if (this.adjustSortingGroupForSortingSymbol(newGroup)) { if (this.ctx.currentSpec) { const groupIdx = this.ctx.currentSpec.groups.push(newGroup) - 1 this.ctx.currentSpecGroup = newGroup @@ -1445,7 +1468,7 @@ export class SortingSpecProcessor { if (!isThreeDots(theOnly)) { const nameWithExt: string = theOnly.trim() if (nameWithExt) { // Sanity check - if (!detectNumericSortingSymbols(nameWithExt)) { + if (!detectSortingSymbols(nameWithExt)) { if (this.ctx.currentSpec) { const itemsToHide: Set = this.ctx.currentSpec?.itemsToHide ?? new Set() itemsToHide.add(nameWithExt) @@ -1572,17 +1595,17 @@ export class SortingSpecProcessor { // Returns true if no regex will be involved (hence no adjustment) or if correctly adjusted with regex private adjustSortingGroupForRegexBasedMatchers = (group: CustomSortGroup): boolean => { - return this.adjustSortingGroupForNumericSortingSymbol(group) + return this.adjustSortingGroupForSortingSymbol(group) } - // Returns true if no numeric sorting symbol (hence no adjustment) or if correctly adjusted with regex - private adjustSortingGroupForNumericSortingSymbol = (group: CustomSortGroup): boolean => { + // Returns true if no sorting symbol (hence no adjustment) or if correctly adjusted with regex + private adjustSortingGroupForSortingSymbol = (group: CustomSortGroup): boolean => { switch (group.type) { case CustomSortGroupType.ExactPrefix: const regexInPrefix = convertPlainStringToLeftRegex(group.exactPrefix!) if (regexInPrefix) { if (regexInPrefix.containsAdvancedRegex && checkAdjacency(regexInPrefix).noSuffix) { - this.problem(ProblemCode.NumericalSymbolAdjacentToWildcard, ADJACENCY_ERROR) + this.problem(ProblemCode.SortingSymbolAdjacentToWildcard, ADJACENCY_ERROR) return false; } delete group.exactPrefix @@ -1593,7 +1616,7 @@ export class SortingSpecProcessor { const regexInSuffix = convertPlainStringToRightRegex(group.exactSuffix!) if (regexInSuffix) { if (regexInSuffix.containsAdvancedRegex && checkAdjacency(regexInSuffix).noPrefix) { - this.problem(ProblemCode.NumericalSymbolAdjacentToWildcard, ADJACENCY_ERROR) + this.problem(ProblemCode.SortingSymbolAdjacentToWildcard, ADJACENCY_ERROR) return false; } delete group.exactSuffix @@ -1604,7 +1627,7 @@ export class SortingSpecProcessor { const regexInHead = convertPlainStringToLeftRegex(group.exactPrefix!) if (regexInHead) { if (regexInHead.containsAdvancedRegex && checkAdjacency(regexInHead).noSuffix) { - this.problem(ProblemCode.NumericalSymbolAdjacentToWildcard, ADJACENCY_ERROR) + this.problem(ProblemCode.SortingSymbolAdjacentToWildcard, ADJACENCY_ERROR) return false; } delete group.exactPrefix @@ -1613,7 +1636,7 @@ export class SortingSpecProcessor { const regexInTail = convertPlainStringToRightRegex(group.exactSuffix!) if (regexInTail) { if (regexInTail.containsAdvancedRegex && checkAdjacency(regexInTail).noPrefix) { - this.problem(ProblemCode.NumericalSymbolAdjacentToWildcard, ADJACENCY_ERROR) + this.problem(ProblemCode.SortingSymbolAdjacentToWildcard, ADJACENCY_ERROR) return false; } delete group.exactSuffix From c94b8d931561e166fc1994950a6c428c89b5327a Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 7 Mar 2023 19:09:35 +0100 Subject: [PATCH 26/26] Version bump before release --- manifest.json | 2 +- package.json | 2 +- versions.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 52a50d151..849d03cec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "custom-sort", "name": "Custom File Explorer sorting", - "version": "1.7.0", + "version": "1.7.1", "minAppVersion": "0.15.0", "description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer", "author": "SebastianMC", diff --git a/package.json b/package.json index f54c1fe6a..fbdf2ca8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-custom-sort", - "version": "1.7.0", + "version": "1.7.1", "description": "Custom Sort plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/versions.json b/versions.json index 2eb024907..ba1f58751 100644 --- a/versions.json +++ b/versions.json @@ -25,5 +25,6 @@ "1.6.1": "0.15.0", "1.6.2": "0.15.0", "1.6.3": "0.15.0", - "1.7.0": "0.15.0" + "1.7.0": "0.15.0", + "1.7.1": "0.15.0" }