├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── .npmrc
├── LICENSE.md
├── README-obsidian-versions-compatibility.md
├── README.md
├── docs
├── advanced-README.md
├── examples
│ ├── 1
│ │ └── sortspec.md
│ ├── 5
│ │ └── sortspec.md
│ ├── basic
│ │ └── sortspec.md
│ └── quickstart
│ │ └── sortspec.md
├── icons
│ ├── icon-active.png
│ ├── icon-error.png
│ ├── icon-general-error.png
│ ├── icon-inactive.png
│ ├── icon-mobile-initial.png
│ ├── icon-not-applied.png
│ └── parsing-succeeded.png
├── img
│ ├── different-sorting-order-per-folder.png
│ ├── separators-by-replete.png
│ ├── sortspec-md-bright.jpg
│ └── sortspec-md-dark.jpg
├── manual.md
├── svg
│ ├── by-suffix.svg
│ ├── files-go-first.svg
│ ├── multi-folder.svg
│ ├── p_a_r_a.svg
│ ├── pin-focus-note.svg
│ ├── priorities-example-a.svg
│ ├── priorities-example-b.svg
│ ├── roman-chapters.svg
│ ├── roman-suffix.svg
│ ├── simplest-example-2.svg
│ ├── simplest-example-3.svg
│ ├── simplest-example.svg
│ ├── syntax-1.svg
│ ├── syntax-2.svg
│ ├── syntax-3.svg
│ └── syntax-4.svg
└── syntax-reference.md
├── esbuild.config.mjs
├── jest.config.js
├── manifest.json
├── package.json
├── src
├── custom-sort-plugin.ts
├── custom-sort
│ ├── custom-sort-types.ts
│ ├── custom-sort-utils.ts
│ ├── custom-sort.ts
│ ├── folder-matching-rules.ts
│ ├── icons.ts
│ ├── macros.ts
│ ├── matchers.ts
│ ├── mdata-extractors.ts
│ └── sorting-spec-processor.ts
├── main.ts
├── settings.ts
├── test
│ ├── int
│ │ ├── dates-in-names.int.test.ts
│ │ └── folder-dates.int.test.ts
│ ├── mocks.ts
│ └── unit
│ │ ├── BookmarksCorePluginSignature.spec.ts
│ │ ├── custom-sort-getComparator.spec.ts
│ │ ├── custom-sort-utils.spec.ts
│ │ ├── custom-sort.spec.ts
│ │ ├── folder-matching-rules.spec.ts
│ │ ├── macros.spec.ts
│ │ ├── matchers.spec.ts
│ │ ├── mdata-extractors.spec.ts
│ │ ├── sorting-spec-processor.spec.ts
│ │ ├── utils.spec.ts
│ │ └── week-of-year.spec.ts
├── types
│ └── types.d.ts
└── utils
│ ├── Bookmarks Core Plugin integration design.md
│ ├── BookmarksCorePluginSignature.ts
│ ├── ObsidianIconFolderPluginSignature.ts
│ ├── utils.ts
│ └── week-of-year.ts
├── styles.css
├── tsconfig.json
├── version-bump.mjs
├── versions.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 4
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | main.js
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": { "node": true },
5 | "plugins": [
6 | "@typescript-eslint"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parserOptions": {
14 | "sourceType": "module"
15 | },
16 | "rules": {
17 | "no-unused-vars": "off",
18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
19 | "@typescript-eslint/ban-ts-comment": "off",
20 | "no-prototype-builtins": "off",
21 | "@typescript-eslint/no-empty-function": "off"
22 | }
23 | }
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "master" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "master" ]
20 | schedule:
21 | - cron: '44 2 * * 5'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'typescript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v2
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 |
52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53 | # queries: security-extended,security-and-quality
54 |
55 |
56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
57 | # If this step fails, then you should remove it and run the build manually (see below)
58 | - name: Autobuild
59 | uses: github/codeql-action/autobuild@v2
60 |
61 | # ℹ️ Command-line programs to run using the OS shell.
62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63 |
64 | # If the Autobuild fails above, remove it and uncomment the following three lines.
65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66 |
67 | # - run: |
68 | # echo "Run, Build Application using script"
69 | # ./location_of_script_within_repo/buildscript.sh
70 |
71 | - name: Perform CodeQL Analysis
72 | uses: github/codeql-action/analyze@v2
73 | with:
74 | category: "/language:${{matrix.language}}"
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vscode
2 | .vscode
3 |
4 | # Intellij
5 | *.iml
6 | .idea
7 |
8 | # npm
9 | node_modules
10 |
11 | # Don't include the compiled main.js file in the repo.
12 | # They should be uploaded to GitHub releases instead.
13 | main.js
14 |
15 | # Exclude sourcemaps
16 | *.map
17 |
18 | # obsidian
19 | data.json
20 |
21 | # Exclude macOS Finder (System Explorer) View States
22 | .DS_Store
23 | /yarn.lock
24 | /.run/test.run.xml
25 | /.run/build.run.xml
26 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/README-obsidian-versions-compatibility.md:
--------------------------------------------------------------------------------
1 | # Obsidian versions compatibility
2 |
3 | > | :exclamation: To avoid issues resulting from breaking changes in Obsidian always use the newest version of the plugin|
4 | > |----------------------------------------------|
5 | > | - Obsidian 1.7.2 - update the plugin to 3.1.0 or newer --> More details in a dedicated section below.|
6 | > | - Obsidian 1.6.3 - update the plugin to 2.1.11 or newer --> More details in a dedicated section below.|
7 | > | - Obsidian 1.6.0 - update the plugin to 2.1.9 or newer --> More details in a dedicated section below.|
8 | > | - Obsidian 1.5.4 - update the plugin to 2.1.7 or newer --> More details in a dedicated section below.|
9 |
10 | ---
11 | > | :exclamation: Breaking changes in Obsidian 1.7.2 - update the plugin to 3.1.0 or newer|
12 | > |----------------------------------------------|
13 | >
14 | > Obsidian team introduced some more breaking changes in a not-backward-compatible way starting from Obsidian 1.7.2.
15 | >
16 | > The observed issues were minor and related to initial automatic application of custom sorting.
17 | > 1. The custom sort plugin is **unable to automatically apply the custom sort on start**
18 | > - when the File Explorer is not visible on app start
19 | > - for the _Lazy Plugin Loader_ plugin occurs by definition
20 | > - prevalent on mobile
21 | >
22 | > The release 3.1.0 fixed some of the scenarios by introducing a delay in initial auto-application
23 | > of custom sorting. The delay is by default set to 1 second and can be increased up to 30 seconds,
24 | > which can be needed for large vaults, large number of plugins or on mobile.
25 | >
26 | > At the same time, when the File Explorer is not visible on app start, the custom sorting can't be
27 | > applied automatically by definition: there is no instance of File Explorer. This is an unfortunate
28 | > side effect of the [deferred views](https://docs.obsidian.md/Plugins/Guides/Understanding+deferred+views)
29 | > introduced by Obsidian 1.7.2.
30 | > There is no cure for this scenario and the custom sorting has to be applied manually when the
31 | > File Explorer is eventually displayed. The simplest way is to click the ribbon icon (on desktop)
32 | > or use the command 'sort-on' on mobile.
33 | >
34 | > 2. The custom sort plugin **keeps showing the notifications** with each change to any note
35 | >
36 | > The release 3.1.0 fixed this fully
37 | >
38 | > ---
39 | > For more details of the observed misbehaviors you can go to:
40 | > - [#161: Find out how to automatically apply custom sort on app start / vault (re)load etc.](https://github.com/SebastianMC/obsidian-custom-sort/issues/161)
41 | > - [#162: \[bug\]\[minor\] Obsidian 1.7.2 breaking changes - when File Explorer is not displayed an attempt to apply custom sort fails with error](https://github.com/SebastianMC/obsidian-custom-sort/issues/162)
42 | > - [#163: Obsidian 1.7.2 - automatic sorting fails when launching Obsidian](https://github.com/SebastianMC/obsidian-custom-sort/issues/163)
43 | > - [#165: Obsidian 1.7.3 - Constant Notifications "Custom sorting ON" and "Parsing custom sorting specification SUCCEEDED!"](https://github.com/SebastianMC/obsidian-custom-sort/issues/165)
44 | > - [#169: Not working on mobile](https://github.com/SebastianMC/obsidian-custom-sort/issues/169)
45 | >
46 |
47 | ---
48 | > | :exclamation: Breaking changes in Obsidian 1.6.3 causing a minor issue - update the plugin to 2.1.11 or newer|
49 | > |----------------------------------------------|
50 | >
51 | > Obsidian team introduced yet another breaking change in 1.6.3 probably related to plugin's lifecycle or File Explorer module lifecycle.
52 | > In result, due to race condition, the custom sort order is not always applied automatically after app reload or vault reload.
53 | > Manual (re)application is needed via ribbon click of via 'sort on' command.
54 | > The [2.1.11](https://github.com/SebastianMC/obsidian-custom-sort/releases/tag/2.1.11) release of the plugin fixes this inconvenience.
55 | >
56 | > For more details of the observed misbehavior (of not updated plugin) you can go to [#147](https://github.com/SebastianMC/obsidian-custom-sort/issues/147)
57 |
58 | ---
59 | > | :exclamation: Breaking changes in Obsidian 1.6.0 (and newer) - update the plugin to 2.1.9 or newer|
60 | > |----------------------------------------------|
61 | >
62 | > Obsidian team introduced some breaking changes in File Explorer sorting code in a not-backward-compatible way starting from Obsidian 1.6.0.
63 | >
64 | > The **custom sort** plugin starting from release **2.1.9** was adjusted to work correctly with these breaking changes in Obsidian 1.6.0 and newer.
65 | > The plugin remains backward compatible, so you can safely update the plugin for Obsidian earlier than 1.6.0
66 | >
67 | > For more details of the observed misbehavior (of not updated plugin) you can go to [#145](https://github.com/SebastianMC/obsidian-custom-sort/issues/145)
68 |
69 | ---
70 | > | :exclamation: Breaking changes in Obsidian 1.5.4 (and newer) - update the plugin to 2.1.7 or newer|
71 | > |----------------------------------------------|
72 | >
73 | > Obsidian team introduced some breaking changes in File Explorer in a not-backward-compatible way starting from Obsidian 1.5.4.
74 | >
75 | > The **custom sort** plugin starting from release **2.1.7** was adjusted to work correctly with these breaking changes in Obsidian 1.5.4 and newer.
76 | > The plugin remains backward compatible, so you can safely update the plugin for Obsidian earlier than 1.5.4
77 | >
78 | > For more details of the observed misbehavior (of not updated plugin) you can go to [#131](https://github.com/SebastianMC/obsidian-custom-sort/issues/131) or [#135](https://github.com/SebastianMC/obsidian-custom-sort/issues/135) or [#139](https://github.com/SebastianMC/obsidian-custom-sort/issues/139)
79 | >
80 |
81 |
--------------------------------------------------------------------------------
/docs/examples/1/sortspec.md:
--------------------------------------------------------------------------------
1 | ---
2 | sorting-spec: |
3 | //
4 | // A simple configuration for obsidian-custom-sort plugin
5 | // (https://github.com/SebastianMC/obsidian-custom-sort)
6 | // It causes the plugin to take over the control of the order of items in the root folder ('/') of the vault
7 | // It explicitly sets the sorting to ascending ('<') alphabetical ('a-z')
8 | // Folders and files are treated equally by the plugin (by default) so expect them intermixed
9 | // after enabling the custom sort plugin
10 | //
11 | target-folder: /
12 | < a-z
13 | ---
14 |
--------------------------------------------------------------------------------
/docs/examples/5/sortspec.md:
--------------------------------------------------------------------------------
1 | ---
2 | sorting-spec: |
3 | target-folder: /
4 | Projects
5 | Areas
6 | Responsibilities
7 | Archive
8 | ---
9 |
--------------------------------------------------------------------------------
/docs/examples/basic/sortspec.md:
--------------------------------------------------------------------------------
1 | ---
2 | sorting-spec: |
3 | order-desc: a-z
4 | ---
5 |
--------------------------------------------------------------------------------
/docs/examples/quickstart/sortspec.md:
--------------------------------------------------------------------------------
1 | ---
2 | sorting-spec: |
3 | //
4 | // A simple configuration for obsidian-custom-sort plugin
5 | // (https://github.com/SebastianMC/obsidian-custom-sort)
6 | // It causes the plugin to take over the control of the order of items in the root folder ('/') of the vault
7 | // It explicitly sets the sorting to descending ('>') alphabetical ('a-z')
8 | // Folders and files are treated equally by the plugin (by default) so expect them intermixed
9 | // in the root vault folder after enabling the custom sort plugin
10 | //
11 | // To play with more examples go to https://github.com/SebastianMC/obsidian-custom-sort#readme
12 |
13 | target-folder: /
14 | > a-z
15 | ---
16 |
--------------------------------------------------------------------------------
/docs/icons/icon-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/icons/icon-active.png
--------------------------------------------------------------------------------
/docs/icons/icon-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/icons/icon-error.png
--------------------------------------------------------------------------------
/docs/icons/icon-general-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/icons/icon-general-error.png
--------------------------------------------------------------------------------
/docs/icons/icon-inactive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/icons/icon-inactive.png
--------------------------------------------------------------------------------
/docs/icons/icon-mobile-initial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/icons/icon-mobile-initial.png
--------------------------------------------------------------------------------
/docs/icons/icon-not-applied.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/icons/icon-not-applied.png
--------------------------------------------------------------------------------
/docs/icons/parsing-succeeded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/icons/parsing-succeeded.png
--------------------------------------------------------------------------------
/docs/img/different-sorting-order-per-folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/img/different-sorting-order-per-folder.png
--------------------------------------------------------------------------------
/docs/img/separators-by-replete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/img/separators-by-replete.png
--------------------------------------------------------------------------------
/docs/img/sortspec-md-bright.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/img/sortspec-md-bright.jpg
--------------------------------------------------------------------------------
/docs/img/sortspec-md-dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianMC/obsidian-custom-sort/17f6a04c3536908fbf3a3333aa42e1921f6dffd2/docs/img/sortspec-md-dark.jpg
--------------------------------------------------------------------------------
/docs/svg/by-suffix.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
71 |
--------------------------------------------------------------------------------
/docs/svg/files-go-first.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
66 |
--------------------------------------------------------------------------------
/docs/svg/multi-folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
64 |
--------------------------------------------------------------------------------
/docs/svg/p_a_r_a.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
71 |
--------------------------------------------------------------------------------
/docs/svg/pin-focus-note.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
64 |
--------------------------------------------------------------------------------
/docs/svg/priorities-example-a.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
--------------------------------------------------------------------------------
/docs/svg/priorities-example-b.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
--------------------------------------------------------------------------------
/docs/svg/roman-chapters.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
--------------------------------------------------------------------------------
/docs/svg/roman-suffix.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
--------------------------------------------------------------------------------
/docs/svg/simplest-example-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
66 |
--------------------------------------------------------------------------------
/docs/svg/simplest-example-3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
66 |
--------------------------------------------------------------------------------
/docs/svg/simplest-example.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
67 |
--------------------------------------------------------------------------------
/docs/svg/syntax-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
277 |
--------------------------------------------------------------------------------
/docs/svg/syntax-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/docs/svg/syntax-3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/docs/svg/syntax-4.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
88 |
--------------------------------------------------------------------------------
/docs/syntax-reference.md:
--------------------------------------------------------------------------------
1 | > Document is partial, creation in progress
2 | > Please refer to [README.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/README.md) for usage examples
3 | > Check [manual.md](https://github.com/SebastianMC/obsidian-custom-sort/blob/master/doc/manual.md), maybe that file has already some content?
4 |
5 | # Table of contents
6 |
7 | TBD
8 |
9 | # Introduction
10 |
11 | The syntax of sorting specification presented here intentionally uses an informal language (instead of a formal grammar definition).
12 | The intention is to make it understandable to non-technical persons.
13 |
14 | # The syntax
15 |
16 | **Visual presentation of the sorting specification structure**
17 |
18 | 
19 |
20 | 
21 |
22 | 
23 |
24 | 
25 |
26 | ## Sorting specification
27 |
28 | - the `sorting specification` has textual format
29 | - typically it is a multiline text
30 | - at the same time, in the simplest case, a single line specification is also possible
31 | - it has to be placed inside the frontmatter YAML in one or more Obsidian notes of the vault
32 | - the YAML key `sorting-spec:` has to be used
33 | - because indentation matters, the recommended multiline YAML syntax is `sorting-spec: |`
34 | - refer to YAML syntax reference for details on how the pipe `|` character works (e.g. https://en.wikipedia.org/wiki/YAML#Basic_components)
35 | - the `sorting specification` comprises one or more [sections](#Section)
36 | - each `section` has to reside in a single location (a single entry in frontmatter YAML of a note)
37 | - multiple `sections` can exist under a single `sorting-spec: |` YAML entry, one after another
38 | - the following Obsidian notes are scanned for `sorting-spec:` YAML entries:
39 | - `sortspec` notes (that is `sortspec.md` files actually) in all folders of the vault, including the root folder
40 | - the so called `folder notes` - by convention these are notes named exactly the same as their parent folder, e.g. `Inbox/Inbox.md`
41 | - this works regardless if any 'Folder note' plugin is installed or not in the vault
42 | - the single designated note, as configured in plugin settings
43 | - all found `sections` in all above locations comprise the `sorting specification` for the vault
44 | - the order doesn't matter
45 | - it is a matter of personal preference whether to put all `sections` in a single YAML entry in a single note in the vault or if spread the specification over frontmatter of many notes
46 | - the Obsidian standard text search for `sorting-spec:` is convenient enough to list all specification entries in the vault
47 | - empty lines inside the `sorting-spec: |` multiline value are ignored. They don't carry any semantic information
48 | - specifically: the empty lines _DON'T_ indicate a new `section`
49 |
50 | ## Section
51 |
52 | - a [section](#Section) specifies sorting for one or more folders of the vault
53 | - a [section](#Section) starts with one or more [target-folder:](#target-folder) lines, each one specifying a folder or a folders (sub)tree
54 | - the [target-folder:](#target-folder) is optional for the very first [section](#Section) in a `sorting-spec:` YAML entry.
55 | In that case it is assumed that the [target-folder:](#target-folder) is the parent folder of the note containing the `sorting-spec:`
56 | - if a [target-folder:](#target-folder) is specified, the above default behavior is not applicable.
57 | - subsequent lines with [target-folder:](#target-folder) are collected and treated as multi-target-folder specification
58 | - empty lines are ignored
59 | - [comments](#Comments) are ignored as well
60 | - one (or more) [target-folder:](#target-folder) lines has to be followed by [sorting instruction(s)](#sorting-instructions) for these folders
61 | - the `sorting instruction(s)` can be ignored only for the last (or only) [target-folder:](#target-folder) line of `sorting-spec:`.
62 | In that case, the default alphabetical sorting is assumed for the specified folders, treating the folders and notes equally
63 | - occurrence of [target-folder:](#target-folder) line after one or more `sorting-instruction(s)` indicates a beginning of a new [section](#Section)
64 |
65 | ## target-folder:
66 |
67 | TBD
68 |
69 | ## Sorting instruction(s)
70 |
71 | ## Comments
72 |
73 | Lines starting with `//` are ignored
74 | > **NOTE:**
75 | >
76 | > Depending on what indentation was used in the very first line of `sorting-spec: |`, it must be preserved also on comment lines.
77 | For example:
78 | ```yaml
79 | sorting-spec: |
80 | target-folder: /
81 | target-folder: Archive
82 | // This is some comment
83 | // This is also a valid comment
84 | > modified
85 | // This is not a valid comment -> indentation is smaller than of the very first line of sorting-spec:
86 | ```
87 |
88 |
89 | ### Supported sorting methods
90 |
91 | #### At folder level only
92 |
93 | - `sorting: standard` - gives back the control on order of items in hands of standard Obsidian mechanisms (UI driven).
94 | Typical (and intended) use: exclude a folder (or folders subtree) from a custom sorting resulting from wilcard-based target folder rule
95 |
96 | #### At folder and group level
97 |
98 | - `< a-z` - alphabetical
99 | - `> a-z` - alphabetical reverse, aka alphabetical descending, 'z' goes before 'a'
100 | - `< true a-z` - true alphabetical, to understand the difference between this one and alphabetical refer to [Alphabetical, Natural and True Alphabetical sorting orders](../README.md#alphabetical-natural-and-true-alphabetical-sorting-orders)
101 | - `> true a-z` - true alphabetical reverse, aka true alphabetical descending, 'z' goes before 'a'
102 | - `< modified` - by modified time, the long untouched item goes first (modified time of folder is assumed the beginning of the world, so folders go first and alphabetical)
103 | - `> modified` - by modified time reverse, the most recently modified item goes first (modified time of folder is assumed the beginning of the world, so folders land in the bottom and alphabetical)
104 | - `< created` - by created time, the oldest item goes first (modified time of folder is assumed the beginning of the world, so folders go first and alphabetical)
105 | - `> created` - by created time reverse, the newest item goes first (modified time of folder is assumed the beginning of the world, so folders land in the bottom and alphabetical)
106 | - `< advanced modified` - by modified time, the long untouched item goes first. For folders, their modification date is derived from the most recently modified direct child file.
107 | For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's modified date
108 | - `> advanced modified` - by modified time reverse, the most recently modified item goes first. For folders, their modification date is derived from the most recently modified direct child file.
109 | For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's modified date
110 | - `< advanced created` - by created time, the oldest item goes first. For folders, their creation date is derived from the oldest (ctime) direct child file.
111 | For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's created date
112 | - `> advanced created` - by created time reverse, the newest item goes first. For folders, their creation date is derived from the oldest (ctime) direct child file.
113 | For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's created date
114 |
115 | #### At group level only (aka secondary sorting rule)
116 |
117 | > Only applicable in edge cases based on numerical symbols, when the regex-based match is equal for more than one item
118 | and need to apply a secondary order on same matches.
119 |
120 | - `< a-z, created`
121 | - `> a-z, created`
122 | - `< a-z, created desc`
123 | - `> a-z, created desc`
124 | - `< a-z, modified`
125 | - `> a-z, modified`
126 | - `< a-z, modified desc`
127 | - `> a-z, modified desc`
128 | - `< a-z, advanced created`
129 | - `> a-z, advanced created`
130 | - `< a-z, advanced created desc`
131 | - `> a-z, advanced created desc`
132 | - `< a-z, advanced created`
133 | - `> a-z, advanced created`
134 | - `< a-z, advanced created desc`
135 | - `> a-z, advanced created desc`
136 |
137 | ### Alternate tokens
138 |
139 | Some tokens have shorter equivalents, which can be used interchangeably:
140 |
141 | - `target-folder:` --> `::::` e.g `target-folder: /` is equivalent to `:::: /`
142 | - `order-asc:` --> `<` e.g. `order-asc: modified` is equivalent to `< modified`
143 | - `order-desc:` --> `>` e.g. `order-desc: a-z` is equivalent to `> a-z`
144 | - `/:files` --> `/:` e.g. `/:files Chapter \.d+ ...` is equivalent to `/: Chapter \.d+ ...`
145 | - `/:files.` --> `/:.` e.g. `/:files. ... \-D+.md` is equivalent to `/:. ... \-D+.md`
146 | - `/folders` --> `/` e.g. `/folders Archive...` is equivalent to `/ Archive...`
147 | - `/folders:files` --> `%` e.g. `/folders:files Chapter...` is equivalent to `% Chapter...`
148 |
149 | Additional shorter equivalents to allow single-liners like `sorting-spec: \< a-z`:
150 | - `order-asc:` --> `\<` e.g. `order-asc: modified` is equivalent to `\< modified`
151 | - `order-desc:` --> `\>` e.g. `order-desc: a-z` is equivalent to `\> a-z`
152 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from 'builtin-modules'
4 |
5 | const banner =
6 | `/*
7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
8 | if you want to view the source, please visit the github repository of this plugin
9 | */
10 | `;
11 |
12 | const prod = (process.argv[2] === "production");
13 |
14 | const context = await esbuild.context({
15 | banner: {
16 | js: banner,
17 | },
18 | entryPoints: ['src/main.ts'],
19 | bundle: true,
20 | external: [
21 | 'obsidian',
22 | 'electron',
23 | '@codemirror/autocomplete',
24 | '@codemirror/collab',
25 | '@codemirror/commands',
26 | '@codemirror/language',
27 | '@codemirror/lint',
28 | '@codemirror/search',
29 | '@codemirror/state',
30 | '@codemirror/view',
31 | '@lezer/common',
32 | '@lezer/highlight',
33 | '@lezer/lr',
34 | ...builtins],
35 | format: 'cjs',
36 | target: 'es2018',
37 | logLevel: "info",
38 | sourcemap: prod ? false : 'inline',
39 | minify: prod,
40 | treeShaking: true,
41 | outfile: 'dist/main.js',
42 | }).catch(() => process.exit(1));
43 |
44 | if (prod) {
45 | await context.rebuild();
46 | process.exit(0);
47 | } else {
48 | await context.watch();
49 | }
50 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | roots: [""],
6 | moduleNameMapper: {
7 | "obsidian": "/node_modules/obsidian/obsidian.d.ts"
8 | },
9 | transformIgnorePatterns: [
10 | 'node_modules/(?!obsidian/.*)'
11 | ]
12 | };
13 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "custom-sort",
3 | "name": "Custom File Explorer sorting",
4 | "version": "3.1.5",
5 | "minAppVersion": "1.7.2",
6 | "description": "Allows for manual and automatic, config-driven reordering and sorting of files and folders in File Explorer",
7 | "author": "SebastianMC",
8 | "authorUrl": "https://github.com/SebastianMC",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-custom-sort",
3 | "version": "3.1.5",
4 | "description": "Custom Sort plugin for Obsidian (https://obsidian.md)",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "version": "node version-bump.mjs && git add manifest.json versions.json",
10 | "test": "jest"
11 | },
12 | "keywords": [
13 | "obsidian",
14 | "custom sorting"
15 | ],
16 | "author": "SebastianMC",
17 | "repository": "https://github.com/SebastianMC/obsidian-custom-sort",
18 | "license": "MIT",
19 | "devDependencies": {
20 | "@types/jest": "^28.1.2",
21 | "@types/node": "^16.11.6",
22 | "@typescript-eslint/eslint-plugin": "5.29.0",
23 | "@typescript-eslint/parser": "5.29.0",
24 | "builtin-modules": "3.3.0",
25 | "esbuild": "0.17.3",
26 | "eslint": "^8.29.0",
27 | "jest": "^29.7.0",
28 | "monkey-around": "^3.0.0",
29 | "obsidian": "^1.7.2",
30 | "ts-jest": "^29.2.5",
31 | "tslib": "2.8.1",
32 | "typescript": "5.7.2"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/custom-sort-plugin.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Plugin
3 | } from 'obsidian'
4 |
5 | export interface CustomSortPluginAPI extends Plugin {
6 | indexNoteBasename(): string|undefined
7 | }
8 |
--------------------------------------------------------------------------------
/src/custom-sort/custom-sort-types.ts:
--------------------------------------------------------------------------------
1 | import {MDataExtractor} from "./mdata-extractors";
2 |
3 | export enum CustomSortGroupType {
4 | Outsiders, // Not belonging to any of other groups
5 | MatchAll, // like a wildard *, used in connection with foldersOnly or filesOnly. The difference between the MatchAll and Outsiders is
6 | ExactName, // ... that MatchAll captures the item (folder, note) and prevents further matching against other rules
7 | ExactPrefix, // ... while the Outsiders captures items which didn't match any of other defined groups
8 | ExactSuffix,
9 | ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
10 | HasMetadataField, // Notes (or folder's notes) containing a specific metadata field
11 | BookmarkedOnly,
12 | HasIcon
13 | }
14 |
15 | export enum CustomSortOrder {
16 | alphabetical = 1, // = 1 to allow: if (customSortOrder) { ...
17 | alphabeticalWithFileExt,
18 | trueAlphabetical,
19 | trueAlphabeticalWithFileExt,
20 | alphabeticalReverse,
21 | alphabeticalReverseWithFileExt,
22 | trueAlphabeticalReverse,
23 | trueAlphabeticalReverseWithFileExt,
24 | byModifiedTime, // New to old
25 | byModifiedTimeAdvanced,
26 | byModifiedTimeAdvancedRecursive,
27 | byModifiedTimeReverse, // Old to new
28 | byModifiedTimeReverseAdvanced,
29 | byModifiedTimeReverseAdvancedRecursive,
30 | byCreatedTime, // New to old
31 | byCreatedTimeAdvanced,
32 | byCreatedTimeAdvancedRecursive,
33 | byCreatedTimeReverse,
34 | byCreatedTimeReverseAdvanced,
35 | byCreatedTimeReverseAdvancedRecursive,
36 | byMetadataFieldAlphabetical,
37 | byMetadataFieldTrueAlphabetical,
38 | byMetadataFieldAlphabeticalReverse,
39 | byMetadataFieldTrueAlphabeticalReverse,
40 | standardObsidian, // whatever user selected in the UI
41 | byBookmarkOrder,
42 | byBookmarkOrderReverse,
43 | fileFirst,
44 | folderFirst,
45 | alphabeticalWithFilesPreferred, // When the (base)names are equal, the file has precedence over a folder
46 | alphabeticalWithFoldersPreferred, // When the (base)names are equal, the file has precedence over a folder,
47 | vscUnicode, // the Visual Studio Code lexicographic order named 'unicode' (which is very misleading, at the same time familiar to VS Code users
48 | vscUnicodeReverse, // ... see compareFilesUnicode function https://github.com/microsoft/vscode/blob/a19b2d5fb0202e00fb930dc850d2695ec512e495/src/vs/base/common/comparers.ts#L80
49 | default = alphabeticalWithFilesPreferred
50 | }
51 |
52 | export type NormalizerFn = (s: string) => string | null
53 | export const IdentityNormalizerFn: NormalizerFn = (s: string) => s
54 |
55 | export interface RegExpSpec {
56 | regex: RegExp
57 | normalizerFn?: NormalizerFn
58 | }
59 |
60 | export interface CustomSort {
61 | order: CustomSortOrder // mandatory
62 | byMetadata?: string
63 | metadataValueExtractor?: MDataExtractor
64 | }
65 |
66 | export interface RecognizedSorting {
67 | primary?: CustomSort
68 | secondary?: CustomSort
69 | }
70 |
71 | export interface CustomSortGroup {
72 | type: CustomSortGroupType
73 | exactText?: string
74 | exactPrefix?: string
75 | regexPrefix?: RegExpSpec
76 | exactSuffix?: string
77 | regexSuffix?: RegExpSpec
78 | sorting?: CustomSort
79 | secondarySorting?: CustomSort
80 | filesOnly?: boolean
81 | matchFilenameWithExt?: boolean
82 | foldersOnly?: boolean
83 | withMetadataFieldName?: string // for 'with-metadata:' grouping
84 | iconName?: string // for integration with obsidian-folder-icon community plugin
85 | priority?: number
86 | combineWithIdx?: number
87 | }
88 |
89 | export interface CustomSortSpec {
90 | // plays only informative role about the original parsed 'target-folder:' values
91 | targetFoldersPaths: Array // For root use '/'
92 | defaultSorting?: CustomSort
93 | defaultSecondarySorting?: CustomSort
94 | groups: Array
95 | groupsShadow?: Array // A shallow copy of groups, used at applying sorting for items in a folder.
96 | // Stores folder-specific values (e.g. macros expanded with folder-specific values)
97 | outsidersGroupIdx?: number
98 | outsidersFilesGroupIdx?: number
99 | outsidersFoldersGroupIdx?: number
100 | itemsToHide?: Set
101 | priorityOrder?: Array // Indexes of groups in evaluation order
102 | implicit?: boolean // spec applied automatically (e.g. auto integration with a plugin)
103 | }
104 |
105 | export const DEFAULT_METADATA_FIELD_FOR_SORTING: string = 'sort-index-value'
106 |
--------------------------------------------------------------------------------
/src/custom-sort/custom-sort-utils.ts:
--------------------------------------------------------------------------------
1 | import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types";
2 |
3 | // Put here to allow unit tests coverage of this specific implicit sorting spec
4 | export const ImplicitSortspecForBookmarksIntegration: string = `
5 | target-folder: /*
6 | bookmarked:
7 | < by-bookmarks-order
8 | sorting: standard
9 | `
10 |
11 | export interface HasSortingTypes {
12 | byBookmarks: number
13 | standardObsidian: number
14 | total: number
15 | }
16 |
17 | export interface HasGroupingTypes {
18 | byBookmarks: number
19 | byIcon: number
20 | total: number
21 | }
22 |
23 | export interface HasSortingOrGrouping {
24 | sorting: HasSortingTypes
25 | grouping: HasGroupingTypes
26 | }
27 |
28 | export const checkByBookmark = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
29 | groupType === CustomSortGroupType.BookmarkedOnly && has.grouping.byBookmarks++;
30 | (order === CustomSortOrder.byBookmarkOrder || order === CustomSortOrder.byBookmarkOrderReverse) && has.sorting.byBookmarks++;
31 | }
32 |
33 | export const checkByIcon = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
34 | groupType === CustomSortGroupType.HasIcon && has.grouping.byIcon++;
35 | }
36 |
37 | export const checkStandardObsidian = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType ) => {
38 | order === CustomSortOrder.standardObsidian && has.sorting.standardObsidian++;
39 | }
40 |
41 | export const doCheck = (has: HasSortingOrGrouping, order?: CustomSortOrder, groupType?: CustomSortGroupType) => {
42 | checkByBookmark(has, order, groupType)
43 | checkByIcon(has, order, groupType)
44 | checkStandardObsidian(has, order, groupType)
45 |
46 | order !== undefined && has.sorting.total++
47 | groupType !== undefined && groupType !== CustomSortGroupType.Outsiders && has.grouping.total++;
48 | }
49 |
50 | export const collectSortingAndGroupingTypes = (sortSpec?: CustomSortSpec|null): HasSortingOrGrouping => {
51 | const has: HasSortingOrGrouping = {
52 | grouping: {
53 | byIcon: 0, byBookmarks: 0, total: 0
54 | },
55 | sorting: {
56 | byBookmarks: 0, standardObsidian: 0, total: 0
57 | }
58 | }
59 | if (!sortSpec) return has
60 | doCheck(has, sortSpec.defaultSorting?.order)
61 | doCheck(has, sortSpec.defaultSecondarySorting?.order)
62 | if (sortSpec.groups) {
63 | for (let group of sortSpec.groups) {
64 | doCheck(has, group.sorting?.order, group.type)
65 | doCheck(has, group.secondarySorting?.order)
66 | }
67 | }
68 | return has
69 | }
70 |
71 | export const hasOnlyByBookmarkOrStandardObsidian = (has: HasSortingOrGrouping): boolean => {
72 | return has.sorting.total === has.sorting.standardObsidian + has.sorting.byBookmarks &&
73 | has.grouping.total === has.grouping.byBookmarks
74 | }
75 |
--------------------------------------------------------------------------------
/src/custom-sort/folder-matching-rules.ts:
--------------------------------------------------------------------------------
1 | export type DeterminedSortingSpec = {
2 | spec?: SortingSpec
3 | }
4 |
5 | export interface FolderMatchingTreeNode {
6 | path?: string
7 | name?: string
8 | matchChildren?: SortingSpec
9 | matchAll?: SortingSpec
10 | subtree: { [key: string]: FolderMatchingTreeNode }
11 | }
12 |
13 | export interface FolderMatchingRegexp {
14 | regexp: RegExp
15 | againstName: boolean
16 | priority: number
17 | logMatches: boolean
18 | sortingSpec: SortingSpec
19 | }
20 |
21 | const SLASH: string = '/'
22 | export const MATCH_CHILDREN_PATH_TOKEN: string = '...'
23 | export const MATCH_ALL_PATH_TOKEN: string = '*'
24 | export const MATCH_CHILDREN_1_SUFFIX: string = `/${MATCH_CHILDREN_PATH_TOKEN}`
25 | export const MATCH_CHILDREN_2_SUFFIX: string = `/${MATCH_CHILDREN_PATH_TOKEN}/`
26 | export const MATCH_ALL_SUFFIX: string = `/${MATCH_ALL_PATH_TOKEN}`
27 |
28 | export const NO_PRIORITY = 0
29 |
30 | export const splitPath = (path: string): Array => {
31 | return path.split(SLASH).filter((name) => !!name)
32 | }
33 |
34 | export interface AddingWildcardFailure {
35 | errorMsg: string
36 | }
37 |
38 | export type CheckIfImplicitSpec = (s: SortingSpec) => boolean
39 |
40 | export class FolderWildcardMatching {
41 |
42 | // mimics the structure of folders, so for example tree.matchAll contains the matchAll flag for the root '/'
43 | tree: FolderMatchingTreeNode = {
44 | subtree: {}
45 | }
46 |
47 | regexps: Array>
48 |
49 | constructor(private checkIfImplicitSpec: CheckIfImplicitSpec) {
50 | }
51 |
52 | // cache
53 | determinedWildcardRules: { [key: string]: DeterminedSortingSpec } = {}
54 |
55 | addWildcardDefinition = (wilcardDefinition: string, rule: SortingSpec): AddingWildcardFailure | null | undefined => {
56 | const pathComponents: Array = splitPath(wilcardDefinition)
57 | const lastComponent: string | undefined = pathComponents.pop()
58 | if (lastComponent !== MATCH_ALL_PATH_TOKEN && lastComponent !== MATCH_CHILDREN_PATH_TOKEN) {
59 | return null
60 | }
61 | let leafNode: FolderMatchingTreeNode = this.tree
62 | pathComponents.forEach((pathComponent) => {
63 | let subtree: FolderMatchingTreeNode = leafNode.subtree[pathComponent]
64 | if (subtree) {
65 | leafNode = subtree
66 | } else {
67 | const newSubtree: FolderMatchingTreeNode = {
68 | name: pathComponent,
69 | subtree: {}
70 | }
71 | leafNode.subtree[pathComponent] = newSubtree
72 | leafNode = newSubtree
73 | }
74 | })
75 | if (lastComponent === MATCH_CHILDREN_PATH_TOKEN) {
76 | if (leafNode.matchChildren && !this.checkIfImplicitSpec(leafNode.matchChildren)) {
77 | return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`}
78 | } else {
79 | leafNode.matchChildren = rule
80 | }
81 | } else { // Implicitly: MATCH_ALL_PATH_TOKEN
82 | if (leafNode.matchAll && !this.checkIfImplicitSpec(leafNode.matchAll)) {
83 | return {errorMsg: `Duplicate wildcard '${lastComponent}' specification for ${wilcardDefinition}`}
84 | } else {
85 | leafNode.matchAll = rule
86 | }
87 | }
88 | }
89 |
90 | addRegexpDefinition = (regexp: RegExp,
91 | againstName: boolean,
92 | priority: number | undefined,
93 | log: boolean | undefined,
94 | rule: SortingSpec
95 | ) => {
96 | const newItem: FolderMatchingRegexp = {
97 | regexp: regexp,
98 | againstName: againstName,
99 | priority: priority || NO_PRIORITY,
100 | sortingSpec: rule,
101 | logMatches: !!log
102 | }
103 | if (this.regexps === undefined || this.regexps.length === 0) {
104 | this.regexps = [newItem]
105 | } else {
106 | // priority is present ==> consciously determine where to insert the regexp
107 | let idx = 0
108 | while (idx < this.regexps.length && this.regexps[idx].priority > newItem.priority) {
109 | idx++
110 | }
111 | this.regexps.splice(idx, 0, newItem)
112 | }
113 | }
114 |
115 | folderMatch = (folderPath: string, folderName?: string): SortingSpec | null => {
116 | const spec: DeterminedSortingSpec = this.determinedWildcardRules[folderPath]
117 |
118 | if (spec) {
119 | return spec.spec ?? null
120 | } else {
121 | let rule: SortingSpec | null | undefined
122 | // regexp matching
123 | if (this.regexps) {
124 | for (let r of this.regexps) {
125 | if (r.againstName && !folderName) {
126 | // exclude the edge case:
127 | // - root folder which has empty name (and path /)
128 | // AND name-matching regexp allows zero-length matches
129 | continue
130 | }
131 | if (r.regexp.test(r.againstName ? (folderName || '') : folderPath)) {
132 | rule = r.sortingSpec
133 | if (r.logMatches) {
134 | const msgDetails: string = (r.againstName) ? `name: ${folderName}` : `path: ${folderPath}`
135 | console.log(`custom-sort plugin - regexp <${r.regexp.source}> matched folder ${msgDetails}`)
136 | }
137 | break
138 | }
139 | }
140 | }
141 |
142 | // simple wildards matching
143 | if (!rule) {
144 | rule = this.tree.matchChildren
145 | let inheritedRule: SortingSpec | undefined = this.tree.matchAll
146 | const pathComponents: Array = splitPath(folderPath)
147 | let parentNode: FolderMatchingTreeNode = this.tree
148 | let lastIdx: number = pathComponents.length - 1
149 | for (let i = 0; i <= lastIdx; i++) {
150 | const name: string = pathComponents[i]
151 | let matchedPath: FolderMatchingTreeNode = parentNode.subtree[name]
152 | if (matchedPath) {
153 | parentNode = matchedPath
154 | rule = matchedPath?.matchChildren ?? null
155 | inheritedRule = matchedPath.matchAll ?? inheritedRule
156 | } else {
157 | if (i < lastIdx) {
158 | rule = inheritedRule
159 | }
160 | break
161 | }
162 | }
163 |
164 | rule ??= inheritedRule
165 | }
166 |
167 | if (rule) {
168 | this.determinedWildcardRules[folderPath] = {spec: rule}
169 | return rule
170 | } else {
171 | this.determinedWildcardRules[folderPath] = {}
172 | return null
173 | }
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/custom-sort/icons.ts:
--------------------------------------------------------------------------------
1 | import {addIcon} from "obsidian";
2 |
3 | export const ICON_SORT_ENABLED_ACTIVE: string = 'custom-sort-icon-active'
4 | export const ICON_SORT_MOBILE_INITIAL: string = 'custom-sort-icon-mobile-initial'
5 | export const ICON_SORT_SUSPENDED: string = 'custom-sort-icon-suspended'
6 | export const ICON_SORT_ENABLED_NOT_APPLIED: string = 'custom-sort-icon-enabled-not-applied'
7 | export const ICON_SORT_SUSPENDED_SYNTAX_ERROR: string = 'custom-sort-icon-syntax-error'
8 | export const ICON_SORT_SUSPENDED_GENERAL_ERROR: string = 'custom-sort-icon-general-error'
9 |
10 | export function addIcons() {
11 | addIcon(ICON_SORT_ENABLED_ACTIVE,
12 | `
13 |
14 |
15 |
16 |
17 | `
18 | )
19 | addIcon(ICON_SORT_MOBILE_INITIAL,
20 | `
21 |
22 |
23 | `
24 | )
25 | addIcon(ICON_SORT_SUSPENDED,
26 | `
27 | `
28 | )
29 | addIcon(ICON_SORT_SUSPENDED_SYNTAX_ERROR,
30 | `
31 |
32 |
33 |
34 |
35 | `
36 | )
37 | addIcon(ICON_SORT_SUSPENDED_GENERAL_ERROR,
38 | `
39 |
40 |
41 |
42 |
43 | `
44 | )
45 | addIcon(ICON_SORT_ENABLED_NOT_APPLIED,
46 | `
47 |
48 |
49 | `
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/custom-sort/macros.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CustomSortSpec
3 | } from "./custom-sort-types";
4 |
5 | const MACRO_PREFIX: string = '{:'
6 | const MACRO_SUFFIX: string = ':}'
7 |
8 | const PARENT_FOLDER_NAME_PLACEHOLDER: string = '%parent-folder-name%'
9 |
10 | const PARENT_FOLDER_NAME_MACRO: string = MACRO_PREFIX + PARENT_FOLDER_NAME_PLACEHOLDER + MACRO_SUFFIX
11 |
12 | export const expandMacrosInString = function(source: string|undefined, parentFolderName?: string|undefined): string|undefined {
13 | if (source && parentFolderName) {
14 | return source.replace(PARENT_FOLDER_NAME_MACRO, parentFolderName)
15 | } else {
16 | return source
17 | }
18 | }
19 |
20 | export const expandMacros = function(sortingSpec: CustomSortSpec, parentFolderName: string|undefined) {
21 | sortingSpec.groupsShadow?.forEach((shadowGroup) => {
22 | if (parentFolderName) { // root has no parent folder, ignore relevant macros for the root
23 | if (shadowGroup.exactText) {
24 | shadowGroup.exactText = expandMacrosInString(shadowGroup.exactText, parentFolderName)
25 | }
26 | if (shadowGroup.exactPrefix) {
27 | shadowGroup.exactPrefix = expandMacrosInString(shadowGroup.exactPrefix, parentFolderName)
28 | }
29 | if (shadowGroup.exactSuffix) {
30 | shadowGroup.exactSuffix = expandMacrosInString(shadowGroup.exactSuffix, parentFolderName)
31 | }
32 | }
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/src/custom-sort/matchers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getDateForWeekOfYear
3 | } from "../utils/week-of-year";
4 |
5 | export const RomanNumberRegexStr: string = ' *([MDCLXVI]+)'; // Roman number
6 | export const CompoundRomanNumberDotRegexStr: string = ' *([MDCLXVI]+(?:\\.[MDCLXVI]+)*)';// Compound Roman number with dot as separator
7 | export const CompoundRomanNumberDashRegexStr: string = ' *([MDCLXVI]+(?:-[MDCLXVI]+)*)'; // Compound Roman number with dash as separator
8 |
9 | export const NumberRegexStr: string = ' *(\\d+)'; // Plain number
10 | export const CompoundNumberDotRegexStr: string = ' *(\\d+(?:\\.\\d+)*)'; // Compound number with dot as separator
11 | export const CompoundNumberDashRegexStr: string = ' *(\\d+(?:-\\d+)*)'; // Compound number with dash as separator
12 |
13 | export const Date_yyyy_mm_dd_RegexStr: string = ' *(\\d{4}-[0-3]*[0-9]-[0-3]*[0-9])'
14 | export const Date_yyyy_dd_mm_RegexStr: string = Date_yyyy_mm_dd_RegexStr
15 |
16 | export const Date_mm_dd_yyyy_RegexStr: string = ' *([0-3]*[0-9]-[0-3]*[0-9]-\\d{4})'
17 | export const Date_dd_mm_yyyy_RegexStr: string = Date_mm_dd_yyyy_RegexStr
18 |
19 | export const Date_dd_Mmm_yyyy_RegexStr: string = ' *([0-3]*[0-9]-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\d{4})'; // Date like 01-Jan-2020
20 | export const Date_Mmm_dd_yyyy_RegexStr: string = ' *((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-[0-3]*[0-9]-\\d{4})'; // Date like Jan-01-2020
21 |
22 | export const Date_yyyy_Www_mm_dd_RegexStr: string = ' *(\\d{4}-W[0-5]*[0-9] \\([0-3]*[0-9]-[0-3]*[0-9]\\))'
23 | export const Date_yyyy_WwwISO_RegexStr: string = ' *(\\d{4}-W[0-5]*[0-9][-+]?)'
24 | export const Date_yyyy_Www_RegexStr: string = Date_yyyy_WwwISO_RegexStr
25 |
26 | export const DOT_SEPARATOR = '.' // ASCII 46
27 | export const DASH_SEPARATOR = '-'
28 |
29 | const SLASH_SEPARATOR = '/' // ASCII 47, right before ASCII 48 = '0'
30 | const GT_SEPARATOR = '>' // ASCII 62, alphabetical sorting in Collator puts it after /
31 | const PIPE_SEPARATOR = '|' // ASCII 124
32 |
33 | const EARLIER_THAN_SLASH_SEPARATOR = DOT_SEPARATOR
34 | const LATER_THAN_SLASH_SEPARATOR = GT_SEPARATOR
35 |
36 | export const DEFAULT_NORMALIZATION_PLACES = 8; // Fixed width of a normalized number (with leading zeros)
37 |
38 | // Property escapes:
39 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes
40 | // https://stackoverflow.com/a/48902765
41 | //
42 | // Using Unicode property escapes to express 'a letter in any modern language'
43 | export const WordInAnyLanguageRegexStr = '(\\p{Letter}+)' // remember about the /u option -> /\p{Letter}+/u
44 |
45 | export const WordInASCIIRegexStr = '([a-zA-Z]+)'
46 |
47 | export function prependWithZeros(s: string|undefined, minLength: number): string {
48 | if ('string' === typeof s) {
49 | if (s.length < minLength) {
50 | const delta: number = minLength - s.length;
51 | return '000000000000000000000000000'.substring(0, delta) + s;
52 | } else {
53 | return s;
54 | }
55 | } else {
56 | return prependWithZeros((s ?? '').toString(), minLength)
57 | }
58 | }
59 |
60 | // Accepts trimmed number (compound or not) as parameter. No internal verification!!!
61 |
62 | export function getNormalizedNumber(s: string = '', separator?: string, places?: number): string | null {
63 | // The strange PIPE_SEPARATOR and trailing // are to allow correct sorting of compound numbers:
64 | // 1-1 should go before 1-1-1 and 1 should go yet earlier.
65 | // That's why the conversion to:
66 | // 1//
67 | // 1|1//
68 | // 1|1|1//
69 | // guarantees correct order (/ = ASCII 47, | = ASCII 124)
70 | if (separator) {
71 | const components: Array = s.split(separator).filter(s => s)
72 | return `${components.map((c) => prependWithZeros(c, places ?? DEFAULT_NORMALIZATION_PLACES)).join(PIPE_SEPARATOR)}${SLASH_SEPARATOR}${SLASH_SEPARATOR}`
73 | } else {
74 | return `${prependWithZeros(s, places ?? DEFAULT_NORMALIZATION_PLACES)}${SLASH_SEPARATOR}${SLASH_SEPARATOR}`
75 | }
76 | }
77 |
78 | function RomanCharToInt(c: string): number {
79 | const Roman: string = '0iIvVxXlLcCdDmM';
80 | const RomanValues: Array = [0, 1, 1, 5, 5, 10, 10, 50, 50, 100, 100, 500, 500, 1000, 1000];
81 | if (c) {
82 | const idx: number = Roman.indexOf(c[0])
83 | return idx > 0 ? RomanValues[idx] : 0;
84 | } else {
85 | return 0;
86 | }
87 | }
88 |
89 | export function romanToIntStr(rs: string): string {
90 | if (rs == null) return '0';
91 |
92 | let num = RomanCharToInt(rs.charAt(0));
93 | let prev, curr;
94 |
95 | for (let i = 1; i < rs.length; i++) {
96 | curr = RomanCharToInt(rs.charAt(i));
97 | prev = RomanCharToInt(rs.charAt(i - 1));
98 | if (curr <= prev) {
99 | num += curr;
100 | } else {
101 | num = num - prev * 2 + curr;
102 | }
103 | }
104 |
105 | return `${num}`;
106 | }
107 |
108 | export function getNormalizedRomanNumber(s: string, separator?: string, places?: number): string | null {
109 | // The strange PIPE_SEPARATOR and trailing // are to allow correct sorting of compound numbers:
110 | // 1-1 should go before 1-1-1 and 1 should go yet earlier.
111 | // That's why the conversion to:
112 | // 1//
113 | // 1|1//
114 | // 1|1|1//
115 | // guarantees correct order (/ = ASCII 47, | = ASCII 124)
116 | if (separator) {
117 | const components: Array = s.split(separator).filter(s => s)
118 | return `${components.map((c) => prependWithZeros(romanToIntStr(c), places ?? DEFAULT_NORMALIZATION_PLACES)).join(PIPE_SEPARATOR)}${SLASH_SEPARATOR}${SLASH_SEPARATOR}`
119 | } else {
120 | return `${prependWithZeros(romanToIntStr(s), places ?? DEFAULT_NORMALIZATION_PLACES)}${SLASH_SEPARATOR}${SLASH_SEPARATOR}`
121 | }
122 | }
123 |
124 | export const DAY_POSITIONS = '00'.length
125 | export const MONTH_POSITIONS = '00'.length
126 | export const YEAR_POSITIONS = '0000'.length
127 |
128 | const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
129 |
130 | export function getNormalizedDate_NormalizerFn_for(separator: string, dayIdx: number, monthIdx: number, yearIdx: number, months?: string[]) {
131 | return (s: string): string | null => {
132 | // Assumption - the regex date matched against input s, no extensive defensive coding needed
133 | const components = s.split(separator)
134 | const day = prependWithZeros(components[dayIdx], DAY_POSITIONS)
135 | const monthValue = months ? `${1 + MONTHS.indexOf(components[monthIdx])}` : components[monthIdx]
136 | const month = prependWithZeros(monthValue, MONTH_POSITIONS)
137 | const year = prependWithZeros(components[yearIdx], YEAR_POSITIONS)
138 | return `${year}-${month}-${day}${SLASH_SEPARATOR}${SLASH_SEPARATOR}`
139 | }
140 | }
141 |
142 | export const getNormalizedDate_yyyy_mm_dd_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 2, 1, 0)
143 | export const getNormalizedDate_yyyy_dd_mm_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 1, 2, 0)
144 | export const getNormalizedDate_mm_dd_yyyy_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 1, 0, 2)
145 | export const getNormalizedDate_dd_mm_yyyy_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 0, 1, 2)
146 | export const getNormalizedDate_dd_Mmm_yyyy_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 0, 1, 2, MONTHS)
147 | export const getNormalizedDate_Mmm_dd_yyyy_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 1, 0, 2, MONTHS)
148 |
149 | const DateExtractor_orderModifier_earlier_than = '-'
150 | const DateExtractor_orderModifier_later_than = '+'
151 |
152 | const DateExtractor_yyyy_Www_mm_dd_Regex = /(\d{4})-W(\d{1,2}) \((\d{2})-(\d{2})\)/
153 | const DateExtractor_yyyy_Www_Regex = /(\d{4})-W(\d{1,2})([-+]?)/
154 |
155 | // Matching groups
156 | const YEAR_IDX = 1
157 | const WEEK_IDX = 2
158 | const MONTH_IDX = 3
159 | const DAY_IDX = 4
160 | const RELATIVE_ORDER_IDX = 3 // For the yyyy-Www only: yyyy-Www- or yyyy-Www+
161 |
162 | const DECEMBER = 12
163 | const JANUARY = 1
164 |
165 | export function getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(consumeWeek: boolean, weeksISO?: boolean) {
166 | return (s: string): string | null => {
167 | // Assumption - the regex date matched against input s, no extensive defensive coding needed
168 | const matches = consumeWeek ? DateExtractor_yyyy_Www_Regex.exec(s) : DateExtractor_yyyy_Www_mm_dd_Regex.exec(s)
169 | const yearStr = matches![YEAR_IDX]
170 | let yearNumber = Number.parseInt(yearStr,10)
171 | let monthNumber: number
172 | let dayNumber: number
173 | let separator = SLASH_SEPARATOR // different values enforce relative > < order of same dates
174 | let useLastDayOfWeek: boolean = false
175 | if (consumeWeek) {
176 | const weekNumberStr = matches![WEEK_IDX]
177 | const weekNumber = Number.parseInt(weekNumberStr, 10)
178 | const orderModifier: string|undefined = matches![RELATIVE_ORDER_IDX]
179 | if (orderModifier === DateExtractor_orderModifier_earlier_than) {
180 | separator = EARLIER_THAN_SLASH_SEPARATOR
181 | } else if (orderModifier === DateExtractor_orderModifier_later_than) {
182 | separator = LATER_THAN_SLASH_SEPARATOR // Will also need to adjust the date to the last day of the week
183 | useLastDayOfWeek = true
184 | }
185 | const dateForWeek = getDateForWeekOfYear(yearNumber, weekNumber, weeksISO, useLastDayOfWeek)
186 | monthNumber = dateForWeek.getMonth()+1 // 1 - 12
187 | dayNumber = dateForWeek.getDate() // 1 - 31
188 | // Be careful with edge dates, which can belong to previous or next year
189 | if (weekNumber === 1) {
190 | if (monthNumber === DECEMBER) {
191 | yearNumber--
192 | }
193 | }
194 | if (weekNumber >= 50) {
195 | if (monthNumber === JANUARY) {
196 | yearNumber++
197 | }
198 | }
199 | } else { // ignore week
200 | monthNumber = Number.parseInt(matches![MONTH_IDX],10)
201 | dayNumber = Number.parseInt(matches![DAY_IDX], 10)
202 | }
203 | return `${prependWithZeros(`${yearNumber}`, YEAR_POSITIONS)}` +
204 | `-${prependWithZeros(`${monthNumber}`, MONTH_POSITIONS)}` +
205 | `-${prependWithZeros(`${dayNumber}`, DAY_POSITIONS)}` +
206 | `${separator}${SLASH_SEPARATOR}`
207 | }
208 | }
209 |
210 | export const getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(false)
211 | export const getNormalizedDate_yyyy_WwwISO_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(true, true)
212 | export const getNormalizedDate_yyyy_Www_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(true, false)
213 |
--------------------------------------------------------------------------------
/src/custom-sort/mdata-extractors.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getNormalizedDate_NormalizerFn_for
3 | } from "./matchers";
4 | import {NormalizerFn} from "./custom-sort-types";
5 |
6 | type ExtractorFn = (mdataValue: string) => string|undefined
7 |
8 | interface DateExtractorSpec {
9 | specPattern: string|RegExp,
10 | extractorFn: ExtractorFn
11 | }
12 |
13 | export interface MDataExtractor {
14 | (mdataValue: string): string|undefined
15 | }
16 |
17 | export interface MDataExtractorParseResult {
18 | m: MDataExtractor
19 | remainder: string
20 | }
21 |
22 | function getGenericPlainRegexpExtractorFn(extractorRegexp: RegExp, extractedValueNormalizer: NormalizerFn) {
23 | return (mdataValue: string): string | undefined => {
24 | const hasMatch = mdataValue?.match(extractorRegexp)
25 | if (hasMatch && hasMatch[0]) {
26 | return extractedValueNormalizer(hasMatch[0]) ?? undefined
27 | } else {
28 | return undefined
29 | }
30 | }
31 | }
32 |
33 | const Extractors: DateExtractorSpec[] = [
34 | { specPattern: 'date(dd/mm/yyyy)',
35 | extractorFn: getGenericPlainRegexpExtractorFn(
36 | new RegExp('\\d{2}/\\d{2}/\\d{4}'),
37 | getNormalizedDate_NormalizerFn_for('/', 0, 1, 2)
38 | )
39 | }, {
40 | specPattern: 'date(mm/dd/yyyy)',
41 | extractorFn: getGenericPlainRegexpExtractorFn(
42 | new RegExp('\\d{2}/\\d{2}/\\d{4}'),
43 | getNormalizedDate_NormalizerFn_for('/', 1, 0, 2)
44 | )
45 | }
46 | ]
47 |
48 | export const tryParseAsMDataExtractorSpec = (s: string): MDataExtractorParseResult|undefined => {
49 | // Simplistic initial implementation of the idea with hardcoded two extractors
50 | for (const extrSpec of Extractors) {
51 | if ('string' === typeof extrSpec.specPattern && s.trim().startsWith(extrSpec.specPattern)) {
52 | return {
53 | m: extrSpec.extractorFn,
54 | remainder: s.substring(extrSpec.specPattern.length).trim()
55 | }
56 | }
57 | }
58 | return undefined
59 | }
60 |
61 | export const _unitTests = {
62 | extractorFnForDate_ddmmyyyy: Extractors.find((it) => it.specPattern === 'date(dd/mm/yyyy)')?.extractorFn!,
63 | extractorFnForDate_mmddyyyy: Extractors.find((it) => it.specPattern === 'date(mm/dd/yyyy)')?.extractorFn!,
64 | }
65 |
--------------------------------------------------------------------------------
/src/settings.ts:
--------------------------------------------------------------------------------
1 | import {App, normalizePath, PluginSettingTab, sanitizeHTMLToDom, Setting} from "obsidian";
2 | import {groupNameForPath} from "./utils/BookmarksCorePluginSignature";
3 | import CustomSortPlugin from "./main";
4 |
5 | export interface CustomSortPluginSettings {
6 | additionalSortspecFile: string
7 | indexNoteNameForFolderNotes: string
8 | suspended: boolean
9 | statusBarEntryEnabled: boolean
10 | notificationsEnabled: boolean
11 | mobileNotificationsEnabled: boolean
12 | automaticBookmarksIntegration: boolean
13 | customSortContextSubmenu: boolean
14 | bookmarksContextMenus: boolean
15 | bookmarksGroupToConsumeAsOrderingReference: string
16 | delayForInitialApplication: number // miliseconds
17 | }
18 |
19 | const MILIS = 1000
20 | const DEFAULT_DELAY_SECONDS = 1
21 | const DELAY_MIN_SECONDS = 0
22 | const DELAY_MAX_SECONDS = 30
23 | const DEFAULT_DELAY = DEFAULT_DELAY_SECONDS * MILIS
24 |
25 | export const DEFAULT_SETTINGS: CustomSortPluginSettings = {
26 | additionalSortspecFile: '',
27 | indexNoteNameForFolderNotes: '',
28 | suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install
29 | statusBarEntryEnabled: true,
30 | notificationsEnabled: true,
31 | mobileNotificationsEnabled: false,
32 | customSortContextSubmenu: true,
33 | automaticBookmarksIntegration: false,
34 | bookmarksContextMenus: false,
35 | bookmarksGroupToConsumeAsOrderingReference: 'sortspec',
36 | delayForInitialApplication: DEFAULT_DELAY
37 | }
38 |
39 | // On API 1.2.x+ enable the bookmarks integration by default
40 | export const DEFAULT_SETTING_FOR_1_2_0_UP: Partial = {
41 | automaticBookmarksIntegration: true,
42 | bookmarksContextMenus: true
43 | }
44 |
45 | const pathToFlatString = (path: string): string => {
46 | return path.replace(/\//g,'_').replace(/\\/g, '_')
47 | }
48 |
49 | export class CustomSortSettingTab extends PluginSettingTab {
50 | plugin: CustomSortPlugin;
51 |
52 | constructor(app: App, plugin: CustomSortPlugin) {
53 | super(app, plugin);
54 | this.plugin = plugin;
55 | }
56 |
57 | display(): void {
58 | const {containerEl} = this;
59 |
60 | containerEl.empty();
61 |
62 | const delayDescr: DocumentFragment = sanitizeHTMLToDom(
63 | 'Number of seconds to wait before applying custom ordering on plugin / app start.'
64 | + ' '
65 | + 'For large vaults, multi-plugin vaults or on mobile the value might need to be increased if you encounter issues with auto-applying'
66 | + ' of custom ordering on start. The delay gives Obsidian additional time to sync notes from cloud storages, to populate notes metadata caches,'
67 | + ' etc.'
68 | + ' '
69 | + 'At the same time if your vault is relatively small or only used on desktop, or not synced with other copies,'
70 | + ' decreasing the delay to 0 could be a safe option.'
71 | + ' '
72 | + `Min: ${DELAY_MIN_SECONDS} sec., max. ${DELAY_MAX_SECONDS} sec.`
73 | )
74 |
75 | new Setting(containerEl)
76 | .setName('Delay for initial automatic application of custom ordering')
77 | .setDesc(delayDescr)
78 | .addText(text => text
79 | .setValue(`${this.plugin.settings.delayForInitialApplication/MILIS}`)
80 | .onChange(async (value) => {
81 | let delayS = parseFloat(value)
82 | delayS = (Number.isNaN(delayS) || !Number.isFinite((delayS))) ? DEFAULT_DELAY_SECONDS : (delayS < DELAY_MIN_SECONDS ? DELAY_MIN_SECONDS :(delayS > DELAY_MAX_SECONDS ? DELAY_MAX_SECONDS : delayS))
83 | delayS = Math.round(delayS*10) / 10 // allow values like 0.2
84 | this.plugin.settings.delayForInitialApplication = delayS * MILIS
85 | await this.plugin.saveSettings()
86 | }))
87 |
88 | const additionalSortspecFileDescr: DocumentFragment = sanitizeHTMLToDom(
89 | 'A note name or note path to scan (YAML frontmatter) for sorting specification in addition to the `sortspec` notes and Folder Notes.'
90 | + ' '
91 | + ' The `.md` filename suffix is optional.'
92 | + ' '
93 | + '
NOTE: After updating this setting remember to refresh the custom sorting via clicking on the ribbon icon or via the sort-on command'
94 | + ' or by restarting Obsidian or reloading the vault
'
95 | )
96 |
97 | new Setting(containerEl)
98 | .setName('Path or name of additional note(s) containing sorting specification')
99 | .setDesc(additionalSortspecFileDescr)
100 | .addText(text => text
101 | .setPlaceholder('e.g. sorting-configuration')
102 | .setValue(this.plugin.settings.additionalSortspecFile)
103 | .onChange(async (value) => {
104 | this.plugin.settings.additionalSortspecFile = value.trim() ? normalizePath(value) : '';
105 | await this.plugin.saveSettings();
106 | }));
107 |
108 | const indexNoteNameDescr: DocumentFragment = sanitizeHTMLToDom(
109 | 'If you employ the Index-File based approach to folder notes (as documented in '
110 | + 'Aidenlx Folder Note preferences'
112 | + ') enter here the index note name, e.g. _about_ or index'
113 | + ' '
114 | + ' The `.md` filename suffix is optional.'
115 | + ' '
116 | + 'This will tell the plugin to read sorting specs and also folders metadata from these files.'
117 | + ' '
118 | + 'The Inside Folder, with Same Name Recommended mode of Folder Notes is handled automatically, no additional configuration needed.'
119 | + '
'
120 | + '
NOTE: After updating this setting remember to refresh the custom sorting via clicking on the ribbon icon or via the sort-on command'
121 | + ' or by restarting Obsidian or reloading the vault
'
122 | )
123 |
124 | new Setting(containerEl)
125 | .setName('Name of index note (Folder Notes support)')
126 | .setDesc(indexNoteNameDescr)
127 | .addText(text => text
128 | .setPlaceholder('e.g. _about_ or index')
129 | .setValue(this.plugin.settings.indexNoteNameForFolderNotes)
130 | .onChange(async (value) => {
131 | this.plugin.settings.indexNoteNameForFolderNotes = value.trim() ? normalizePath(value) : '';
132 | await this.plugin.saveSettings();
133 | }));
134 |
135 | new Setting(containerEl)
136 | .setName('Enable the status bar entry')
137 | .setDesc('The status bar entry shows the label `Custom sort:ON` or `Custom sort:OFF`, representing the current state of the plugin.')
138 | .addToggle(toggle => toggle
139 | .setValue(this.plugin.settings.statusBarEntryEnabled)
140 | .onChange(async (value) => {
141 | this.plugin.settings.statusBarEntryEnabled = value;
142 | if (value) {
143 | // Enabling
144 | if (this.plugin.statusBarItemEl) {
145 | // for sanity
146 | this.plugin.statusBarItemEl.detach()
147 | }
148 | this.plugin.statusBarItemEl = this.plugin.addStatusBarItem();
149 | this.plugin.updateStatusBar()
150 |
151 | } else { // disabling
152 | if (this.plugin.statusBarItemEl) {
153 | this.plugin.statusBarItemEl.detach()
154 | }
155 | }
156 | await this.plugin.saveSettings();
157 | }));
158 |
159 | new Setting(containerEl)
160 | .setName('Enable notifications of plugin state changes')
161 | .setDesc('The plugin can show notifications about its state changes: e.g. when successfully parsed and applied'
162 | + ' the custom sorting specification, or, when the parsing failed. If the notifications are disabled,'
163 | + ' the only indicator of plugin state is the ribbon button icon. The developer console presents the parsing'
164 | + ' error messages regardless if the notifications are enabled or not.')
165 | .addToggle(toggle => toggle
166 | .setValue(this.plugin.settings.notificationsEnabled)
167 | .onChange(async (value) => {
168 | this.plugin.settings.notificationsEnabled = value;
169 | await this.plugin.saveSettings();
170 | }));
171 |
172 | new Setting(containerEl)
173 | .setName('Enable notifications of plugin state changes for mobile devices only')
174 | .setDesc('See above.')
175 | .addToggle(toggle => toggle
176 | .setValue(this.plugin.settings.mobileNotificationsEnabled)
177 | .onChange(async (value) => {
178 | this.plugin.settings.mobileNotificationsEnabled = value;
179 | await this.plugin.saveSettings();
180 | }));
181 |
182 | new Setting(containerEl)
183 | .setName('Enable File Explorer context submenu`Custom sort:`')
184 | .setDesc('Gives access to operations relevant for custom sorting, e.g. applying custom sorting.')
185 | .addToggle(toggle => toggle
186 | .setValue(this.plugin.settings.customSortContextSubmenu)
187 | .onChange(async (value) => {
188 | this.plugin.settings.customSortContextSubmenu = value;
189 | await this.plugin.saveSettings();
190 | }));
191 |
192 | containerEl.createEl('h2', {text: 'Bookmarks integration'});
193 | const bookmarksIntegrationDescription: DocumentFragment = sanitizeHTMLToDom(
194 | 'If enabled, order of files and folders in File Explorer will reflect the order '
195 | + 'of bookmarked items in the bookmarks (core plugin) view. Automatically, without any '
196 | + 'need for sorting configuration. At the same time, it integrates seamlessly with'
197 | + '
sorting-spec:
configurations and they can nicely cooperate.'
198 | + ' '
199 | + '
To separate regular bookmarks from the bookmarks created for sorting, you can put '
200 | + 'the latter in a separate dedicated bookmarks group. The default name of the group is '
201 | + "'" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "' "
202 | + 'and you can change the group name in the configuration field below.'
203 | + ' '
204 | + 'If left empty, all the bookmarked items will be used to impose the order in File Explorer.
'
205 | + '
More information on this functionality in the '
206 | + ''
207 | + 'manual of this custom-sort plugin.'
208 | + '