Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1519 (2023-01-11 18:07:58)

feat: New CSS classes for "themeability"
<!--- Provide a general summary of your changes in the Title above -->

# Description

This PR introduces a vastly improved set of CSS classes and a restructure of how the plugin renders tasks, in both queries and Reading View, to allow a very extensive range of customization options to its look ("themeability").

The best way to understand the changes is the new "styling.md" documentation, which covers in depth the updated structure of rendered tasks and the possibilities to customize their style.

While the changes are relatively fundamental, they are also backwards-compatible, in the sense that in principle without any custom CSS this PR should not change anything about how tasks are rendered.
That being said, because of the changes to internal `div`s and `span`s, there could still be edge cases on which the Electron renderer might choose to display tasks slightly different than before, e.g. when it comes to decisions on where to word-wrap lines. If any such differences are found, I believe they will be to the favor of the new implementation, because now the renderer knows that things like a due date emoticon and its corresponding date are one unit rather than arbitrary two words.

<!--- Describe your changes in detail -->

## Motivation and Context

As described in [this discussion](https://github.com/obsidian-tasks-group/obsidian-tasks/discussions/877), although Tasks is a very powerful tool, when it comes to looks it is severely lagging behind almost any competing tool. And while it's possible to configure some of its aspects with CSS, I quickly discovered that the way it is structured makes this very limited. Thus comes this PR, which makes extensive changes to how tasks are rendered behind the scenes, while also maintaining backwards compatibility for those who just like the way it works right now.

## How has this been tested?

1. Extensive tests that are supposed to cover all the changed functionality.
2. Manual testing throughout weeks of daily usage.

## Screenshots (if appropriate)

![2023-01-11_13-48](https://user-images.githubusercontent.com/10722656/211883047-34bad8b0-388b-4921-84a4-b7e8a1b89764.png)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [ ] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [x] **Documentation** (prefix: `docs` - improvements to any documentation content)
- [ ] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))

Internal changes:

- [ ] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change requires a change to the documentation.
- [x] I have [updated the documentation](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)


esm7: Only now after creating this PR I noticed that there is actually already an existing `styles.md` file :facepalm:
I will merge mine with the existing document.
claremacrae: Brilliant. Thanks very much for this. I've had an initial look and the docs look amazing.

One thing I wondered when reading the docs - I had the impression from some talk or doc on porting to the new Obsidian theming architecture was that it was a good idea to user variable names for colours. For example, so that the colours work well automatically if switching between Light and Dark modes.

This is why I used a variable in this sample CSS:
https://obsidian-tasks-group.github.io/obsidian-tasks/queries/explaining-queries/#styling-explain-results

I wonder if it would be worth doing the same in your new docs? Maybe consider it?
I think it may also play nicely if users or themes have customised the colour values a bit???
claremacrae: > Only now after creating this PR I noticed that there is actually already an existing `styles.md` file 🤦
> I will merge mine with the existing document.

Thanks. I think Advanced is probably a good place for these docs.

It may also have the advantage of removing the 2 conflicts, which would allow CI to run...
claremacrae: I thought I'd have an play with this, so I'm trying out a few of the examples on the docs.

With this one:


```css
/* A special color (bluish) for the 'due' component if it's for today */
.task-due.task-due-today span {
 background: #d2e8fa;
 border-radius: 10px;
 padding: 2px 8px;
}
/* A special color (reddish) for overdue due dates */
[class*="past-"] .task-due span {
 background: #ffbfcc;
 border-radius: 10px;
 padding: 2px 8px;
}
```


And these tasks:

```text
- [ ] #task overdue 📅 2022-01-10
- [ ] #task overdue 📅 2023-01-10
- [ ] #task today 📅 2023-01-11
```

The 2nd overdue task is highlighted, but the first one isn't.

![image](https://user-images.githubusercontent.com/4840096/211909623-53ce4942-a038-48ed-a377-0200a91c361a.png)

I wonder if it's worth a general catch-all way of colouring all overdue tasks??? Or have I missed something that is already there?
esm7: > 

In this screenshot the first "overdue" is a year ago ;)
esm7: > I thought I'd have an play with this, so I'm trying out a few of the examples on the docs.
> 
> With this one:
> 
> ```css
> /* A special color (bluish) for the 'due' component if it's for today */
> .task-due.task-due-today span {
>  background: #d2e8fa;
>  border-radius: 10px;
>  padding: 2px 8px;
> }
> /* A special color (reddish) for overdue due dates */
> [class*="past-"] .task-due span {
>  background: #ffbfcc;
>  border-radius: 10px;
>  padding: 2px 8px;
> }
> ```
> 
> And these tasks:
> 
> ```
> - [ ] #task overdue 📅 2022-01-10
> - [ ] #task overdue 📅 2023-01-10
> - [ ] #task today 📅 2023-01-11
> ```
> 
> The 2nd overdue task is highlighted, but the first one isn't.
> 
> ![image](https://user-images.githubusercontent.com/4840096/211909623-53ce4942-a038-48ed-a377-0200a91c361a.png)
> 
> I wonder if it's worth a general catch-all way of colouring all overdue tasks??? Or have I missed something that is already there?

That's an interesting observation, the way I defined it dates that are more than 7 days ago don't get a specific class, thus are not caught by `[class*="past-"]` (there is even a test to verify this). Maybe it would make more sense to give them a class like `past-far` or something along this line.
claremacrae: > That's an interesting observation, the way I defined it dates that are more than 7 days ago don't get a specific class, thus are not caught by `[class*="past-"]` (there is even a test to verify this). Maybe it would make more sense to give them a class like `past-far` or something along this line.

Yes, it was reading the docs and code, and seeing the 7 day limit, that prompted me to test this out.

Oh how I wish I had no tasks more than 7 days overdue!! 😢 

claremacrae: I do like your suggestion of having a name for 'more than 7 days overdue' - and presumably similar for more than 7 days in the future...
esm7: Fixed all of the above.
claremacrae: Thanks! I see that 3 styles have been removed from the class names table.
I think that means that this is a breaking change and will need to be called 2.0 - is that correct? Hoping I've misunderstood something.
claremacrae: > Thanks! I see that 3 styles have been removed from the class names table.
> I think that means that this is a breaking change and will need to be called 2.0 - is that correct? Hoping I've misunderstood something.

Looking at those classes, it's fine if it is a breaking change. The benefits are so huge.
And the kind of people likely to have modified the previous styles are likely to take advantage of the new ones!

We'd just need to figure out how to document it. (Apologies if you have done that already. I haven't reviewed the latest diffs thoroughly yet)
claremacrae: Just FYI, after this PR is merged, I plan to pull out all the CSS snippets in to separate files in the .obsidian/snippets folder in the demo vault, so that people (including me) can easily try them out. And then I have a way to embed them back in to the documentation.

Then, I'll also use an IDE to format the CSS files for consistent indentation... So don't worry about spending any time on the formatting of the CSS snippets right now...
esm7: > Thanks! I see that 3 styles have been removed from the class names table. I think that means that this is a breaking change and will need to be called 2.0 - is that correct? Hoping I've misunderstood something.

There's no breaking change here, I simply removed from this table the 3 classes that are now listed in "Basic Task Structure", to prevent duplication and confusion.

claremacrae: Ok thanks - brilliant. 
esm7: Do note however that this PR guarantees backwards-compatibility only with the default theme. Since the structure of `DIV`s and `SPAN`s changed, there is no way to guarantee that CSS snippets that extensively use the old structure for their rules will continue to work. Many of them will, because the basics are the same, but it's certainly not full backwards-compatibility for any given existing CSS.
claremacrae: Hi @esm7 Apologies for the slow response on this. I've been working solid on the custom statuses feature for a couple of weeks and am desperate to release it and move on...

I've have read this code twice, though, and expect to only have very minor comments on it.

claremacrae: OK. I have released the statuses work now. I have some real world responsibilities that I need to take care of, and then I will pick this up. 
claremacrae: Note to self: test that this still works:
https://obsidian-tasks-group.github.io/obsidian-tasks/how-to/style-backlinks/
esm7: Awesome. In the meantime I am about to throw some more things to review towards your direction ;)
esm7: @claremacrae I added a small treat: additional CSS classes for the task tags. It allows further nice formatting options and I added documentation and tests accordingly.

Also, on a very different note, I wrote a small feature that I've been yearning to have since forever -- a "go to task" button that opens the file of a task and goes to its line rather than the task's section (which I personally find not so useful). For the sake of my own convenience it branches out from `feat-themeability`, so I'll wait with opening a PR until this PR is merged, but you can look at it [here](https://github.com/esm7/obsidian-tasks/compare/feat-themeability...esm7:obsidian-tasks:feat-goto-task) if you'd like to get an initial impression.
claremacrae: > @claremacrae I added a small treat: additional CSS classes for the task tags. It allows further nice formatting options and I added documentation and tests accordingly.

Oohh. Very interesting. Thank you.

> Also, on a very different note, I wrote a small feature that I've been yearning to have since forever -- a "go to task" button that opens the file of a task and goes to its line rather than the task's section (which I personally find not so useful). For the sake of my own convenience it branches out from `feat-themeability`, so I'll wait with opening a PR until this PR is merged, but you can look at it [here](https://github.com/esm7/obsidian-tasks/compare/feat-themeability...esm7:obsidian-tasks:feat-goto-task) if you'd like to get an initial impression.

I had a quick look at the code. This is gonna make a lot of people very happy indeed!

I haven't spent time understanding how it works yet, but one thing I'm curious about. I'm wondering why introduce a new feature/button for it? I had always assumed that if it became possible to jump to the line, we would just change the target of the backlink.

(Like, I always saw it as a bug that backlink went to the section - it was only because we didn't know how to jump to the specific line)


esm7: > 
> I had a quick look at the code. This is gonna make a lot of people very happy indeed!
> 
> I haven't spent time understanding how it works yet, but one thing I'm curious about. I'm wondering why introduce a new feature/button for it? I had always assumed that if it became possible to jump to the line, we would just change the target of the backlink.
> 
> (Like, I always saw it as a bug that backlink went to the section - it was only because we didn't know how to jump to the specific line)

I'd love to just add this functionality to the existing backlink, but it requires certain trade-offs that maybe not everybody will like. Since I believe the approach in Tasks is to retain backwards compatibility and not break existing functionality, I thought it's safer to add rather than change.
Let's discuss this in depth once I open that other PR :-)

claremacrae: > Note to self: test that this still works:
> https://obsidian-tasks-group.github.io/obsidian-tasks/how-to/style-backlinks/

This PR does break the above documented feature, I've found.

Steps to reproduce:

- Download a build of the Tasks-Demo vault for this PR - I used the one from: https://github.com/obsidian-tasks-group/obsidian-tasks/actions/runs/4005908453
- Open the expanded vault on Obsidian
- Open the file `Styling the Backlinks.md`
- Enable the snippet `tasks-plugin-backlinks-icon.css`

Expected result:

![image](https://user-images.githubusercontent.com/4840096/214688836-dd250f27-a071-4c5f-ba06-6884c913a990.png)

Actual result:

![image](https://user-images.githubusercontent.com/4840096/214688896-6af3a519-53b2-44a9-9e0d-52f98fa81695.png)

We could obviously update the snippet to match the new styling, but there will be users who have already downloaded the snippet, so might it be possible for the old snippet to work with the new code?

(The other snippet written for that help page works: `tasks-plugin-backlinks-small-grey.css`.)
esm7: Oh, indeed this snippet directly references the div structure and thus doesn't work anymore :-(
I can't test this right now, but it should be very easy to fix by simply replacing the `>` (direct child) with a space (general child).
It happens because the `tasks-backlink` span is now inside another span (`tasks-extras`) and not directly below `plugin-tasks-list-item`.
I *can* remove this extra span and make all the "extras" (backlink, edit button, urgency) directly under the list item span, which would make the existing snippet work. However, the reason I put it there is that AFAIK without the extra level there is no way to accomplish the nice multi-line structure shown in the screenshot and examples. At least not with my existing CSS knowledge, and after lots of attempt to do it in other ways...

I acknowledge that a snippet that's included in the documentation is more important to retain compatibility with compared to custom user styles that can break in a similar way if they rely on the div structure. However, I also think that for the benefit of taking Tasks into a beautifully-themed future, it's worth considering all design-related decisions as prone to some changes in the near future, even the new ones in this PR. Because I'm still hopeful there can be some dialog with a much better CSS wizard than I am, who will have amazing ideas for how to make Tasks look, and some changes may be derived from that (hopefully just minor, because I did my best to make this design as generic as possible)...
claremacrae: I've been mulling this over... On reflection, I'm totally comfortable with this being a breaking change - I'll just bump the version number to 2.0.

> it should be very easy to fix by simply replacing the > (direct child) with a space (general child).
It happens because the tasks-backlink span is now inside another span (tasks-extras) and not directly below plugin-tasks-list-item.

Brilliant. That worked perfectly.

Before it's released, the current snippet should be updated - and the docs updated to let people the changes to make to their snippets and why and how - mentioning that pattern you described above.

I have been meaning to make it so that the CSS in the 'styling backlinks' page is pulled in from the separate CSS file rather than being a manual copy-and-paste. This would be a good time to do that...

So, in summary, I think that all that remains is:

- [ ] me to do the code review
- [ ] yourself to consider acting on any feedback
- [ ] me to merge
- [ ] me to fix the CSS snippets in the sample vault
- [ ] me to automate the pulling in of CSS snippets in to docs
- [ ] me to document the breaking change - and the pattern for updating snippets to fix them...


claremacrae: I have just created a 2.0.0-dev branch which I plan to merge this in to.

esm7, I tried changing this PR to merge in to 2.0.0-dev, but got this concerning message so cancelled it...

![image](https://user-images.githubusercontent.com/4840096/215152847-79a0f45d-b29c-4e87-8dbb-0bd3196d1465.png)

I'm unsure what to do. I wonder whether it would be solved by your merging changes up to 2.0.0-dev in to your branch?
claremacrae: (There are a few other changes that I would like to make to include in 2.0.0, and the docs changes I mentioned above will also take time... So the thinking behind merging to a branch is that I would still be able to do patch releases from main until 2.0.0 was ready for release...)
esm7: > I have just created a 2.0.0-dev branch which I plan to merge this in to.
> 
> esm7, I tried changing this PR to merge in to 2.0.0-dev, but got this concerning message so cancelled it...
> 
> ![image](https://user-images.githubusercontent.com/4840096/215152847-79a0f45d-b29c-4e87-8dbb-0bd3196d1465.png)
> 
> I'm unsure what to do. I wonder whether it would be solved by your merging changes up to 2.0.0-dev in to your branch?

I don't think it's a big deal since we resolved all existing comments anyway (and they won't disappear, just become outdated).
claremacrae: Oh - we are both right - there is one 'commits' and one 'comments' 🤣 
Still worried about commits being removed...
TomWol: This is amazing! Thank you so much @esm7 for working on this and @claremacrae for pointing me and others to it.

I am going to be testing this out over the next week. So far I've noticed one thing already:

<img width="302" alt="Tasks-Group-Heading" src="https://user-images.githubusercontent.com/1523169/215332501-5915f225-1812-468d-9b72-847512117fc8.png">

When using `group by`, Tasks still creates headers starting at h4, all with the same `tasks-group-heading` class. The content of that header is still just a single classless <p> element with plaintext. There's no class distinction between  whether you grouped by priority, due date, ..., making customization hard. So if that could be added, that would be amazing.

I have made a separate feature request at #1604 for text improvements to those group headers, as I wonder whether CSS customizations are the way to go for customizing the actual text. Feel free to disagree if you see this differently, I do not have your expertise.

Thank you very much for the hard work! If I think of anything else, I will let you know.
esm7: @TomWol that's a great feedback, I'll add specific classes for this use case.
TomWol: > @TomWol that's a great feedback, I'll add specific classes for this use case.

Thank you! I have a design philosophy question about this case:

I don't think I'm alone in using `group by` and `hide` together. As in, I will `group by priority` and then `hide priority` because otherwise the priority is showing twice. However, once you use `hide priority`, the priority-related classes like `task-priority-high`are no longer there. I understand this is because priority is hidden from the query, I imagine this is the intended behaviour, even when the priority is displayed in the `group by` header as opposed to the individual tasks, correct?

I'm also thinking: right now the HTML elements for group header and group task list are siblings. I am basically learning CSS to be able to customize this plugin for personal use, so pardon me asking this perhaps stupid question: would I be able to style the individual tasks based on the (to be added) group heading classes if these elements are siblings? It seems this behaviour would be covered using something like the general sibling combinator (~), but I thought I'd ask to be sure... Thanks!
esm7: > > @TomWol that's a great feedback, I'll add specific classes for this use case.
> 
> Thank you! I have a design philosophy question about this case:
> 
> I don't think I'm alone in using `group by` and `hide` together. As in, I will `group by priority` and then `hide priority` because otherwise the priority is showing twice. However, once you use `hide priority`, the priority-related classes like `task-priority-high`are no longer there. I understand this is because priority is hidden from the query, I imagine this is the intended behaviour, even when the priority is displayed in the `group by` header as opposed to the individual tasks, correct?
> 
> I'm also thinking: right now the HTML elements for group header and group task list are siblings. I am basically learning CSS to be able to customize this plugin for personal use, so pardon me asking this perhaps stupid question: would I be able to style the individual tasks based on the (to be added) group heading classes if these elements are siblings? It seems this behaviour would be covered using something like the general sibling combinator (~), but I thought I'd ask to be sure... Thanks!

You should have no problem styling your tasks using various CSS selectors in this case (the `has()` operator is relevant here too, maybe more than the general sibling). However, I think we should strive for a cleaner solution to this use case, which makes sense to me to be common even though I never tried it myself. I'll check the option that even for hidden components, their specific classes would be included in the upper task element.

TomWol: Interested in what you come up with about that use case, thanks @esm7!

I found one mistake in the documentation:

In [Colors for Due Today and Overdue](https://github.com/esm7/obsidian-tasks/blob/feat-themeability/docs/advanced/styling.md#colors-for-due-today-and-overdue), the second example rule is:

```css
[class*="past-"] .task-due span {
 background: var(--color-pink);
 border-radius: 10px;
 padding: 2px 8px;
}
```

This is supposed to colour overdue due dates in red. It does that, but also more. It actually looks for a class including `past-` in the upper task element, and then looks for a descendant with a `task-due` class. 
When your scheduled or start date is in the past, the upper task element will also contain `past-`, in which case it will colour a due date as red even though it is not overdue.

I have successfully changed it into the following code, which only detects a `task-due` class also containing `past-`
```css
.task-due[class*="past-"] span {
 background: var(--color-pink);
 border-radius: 10px;
 padding: 2px 8px;
}
```
I have tested this solution in the demo vault and it seems to work, with all variations of tasks with a date in the past working properly. 

(The original rule was also more specific than the first rule in the example - the one which gives the due date a special (non-red) colour if due today - meaning that tasks due today with a scheduled or start date in the past still had red due dates. This also works with my alteration. 

As I am a novice at css, I recommend you try it just to be sure. Thanks!
esm7: > Interested in what you come up with about that use case, thanks @esm7!
> 
> I found one mistake in the documentation:
> 
> In [Colors for Due Today and Overdue](https://github.com/esm7/obsidian-tasks/blob/feat-themeability/docs/advanced/styling.md#colors-for-due-today-and-overdue), the second example rule is:
> 
> ```css
> [class*="past-"] .task-due span {
>  background: var(--color-pink);
>  border-radius: 10px;
>  padding: 2px 8px;
> }
> ```
> 
> This is supposed to colour overdue due dates in red. It does that, but also more. It actually looks for a class including `past-` in the upper task element, and then looks for a descendant with a `task-due` class. When your scheduled or start date is in the past, the upper task element will also contain `past-`, in which case it will colour a due date as red even though it is not overdue.
> 
> I have successfully changed it into the following code, which only detects a `task-due` class also containing `past-`
> 
> ```css
> .task-due[class*="past-"] span {
>  background: var(--color-pink);
>  border-radius: 10px;
>  padding: 2px 8px;
> }
> ```
> 
> I have tested this solution in the demo vault and it seems to work, with all variations of tasks with a date in the past working properly.
> 
> The original rule was also more specific than the first rule in the example - the one which gives the due date a special (non-red) colour if due today - meaning that tasks due today with a scheduled or start date in the past still had red due dates. This also works with my alteration.
> 
> As I am a novice at css, I recommend you try it just to be sure. Thanks!

Great catch, thank you! Will fix the documentation.

I will probably have time next week to work on all the latest follow-ups, and I'll update here.
esm7: Just wanted to update that I worked on the comments here quite a bit today.
I'm adding specific classes like `tasks-layout-hide-priority` for components that are hidden, hidden elements will still have their specific classes added to the task, I'm still trying to figure out grouping, add tests for it all, and also investigate a bunch of questions that came up here.
This adds up to a lot of work :sweat_smile: 
Expecting to continue later this week.
esm7: @claremacrae the work on the feedback above and the "hybrid short mode" request made me think of something.
How about I'll make the hide/show options completely CSS-based?
i.e. always add all the components to the HTML, but for those that are marked as hidden, set a `task-component-hidden` class and hide it with a CSS rule.
Same goes for short mode: it will translate to a single `tasks-query-short-mode` class and CSS rules will hide the actual components.
This will allow much more flexibility in hiding/showing components by CSS and styling tasks based on what's shown.
claremacrae: > @claremacrae the work on the feedback above and the "hybrid short mode" request made me think of something.
> How about I'll make the hide/show options completely CSS-based?
> i.e. always add all the components to the HTML, but for those that are marked as hidden, set a `task-component-hidden` class and hide it with a CSS rule.
> Same goes for short mode: it will translate to a single `tasks-query-short-mode` class and CSS rules will hide the actual components.
> This will allow much more flexibility in hiding/showing components by CSS and styling tasks based on what's shown.

The short answer is, I don't know enough to be able to advise on this.

A longer initial answer would include:

- It feels to me like the scope of this keeps growing, and getting further from release with more to review, more to document, more to test...
- The user feedback has been really helpful, but it does not necessarily have to be all included in a first version
- It's fine to release features that are already useful, and include a note in the docs noting limitations in the initial version
- The more flexibility there is, the more there is to document.
- CSS behaviour is rather harder to reason about, in code, and to write tests for, than typescript code.
- 99% of the performance cost of tasks searches is the rendering of the HTML.
- I still worry that that the more stuff gets added to the HTML, the slower it will get.

I may think of more, but that is what's on my mind after a few minutes of thinking about it...
TomWol: Thank you for your hard work and attention to detail, @esm7, well appreciated.

I do agree with @claremacrae that a hybrid short mode with CSS hiding is a great idea, but for the future and perhaps beyond the scope of this current iteration. 

I think you are pretty close to getting a functional build, from my outside perspective of course.

I think other than the grouping, which I am excited to see what you come up with, these are the biggest things I'm missing:
- Invalid dates are not classified correctly. If you have an invalid due date, the class is `task-due-far`. Scheduled and start dates are also improperly classified as `far`.
- With regards to short mode, the biggest problem I see is that there is hardly any mention of it in the HTML. Short mode is only shown in the `internal-link` class for the actual backlink. I think it should be mentioned in the `task-list-item` at the very least and maybe then the `tasks-backlink` span and the other fields that are shortened like the dates.

The only thing other than that that I would think is a nice extra, but not necessary is this:
- The Unicode characters/icons sometimes don't align with the text. For recurrence for example, the top part of the icon doesn't align with the lowercase text next to it. As the icons are part of the font, changes to the font also affect the icons. Separating the Unicode characters/icons for recurrence, dates, etc. into separate spans would allow for more customizations like that one or even replacing the icons with others via CSS.

I will gladly test any next iteration. I have not seen any performance issues yet, but I do feel like @claremacrae is right to mention it. Will try to run it on my main vault soon.
esm7: Thank you guys for the helpful comments and insights.
I will continue as originally planned, without new huge changes, in the route of adding classes to denote grouping, hidden components and short mode.
Hoping to give you something to test this evening.
esm7: > * Invalid dates are not classified correctly. If you have an invalid due date, the class is `task-due-far`. Scheduled and start dates are also improperly classified as `far`.

@TomWol can you give an example for a kind of invalid date that gives you this result? I tried several types of malformed dates and the only result I'm getting is that it's not recognized as a due date at all, as expected.
claremacrae: > @TomWol can you give an example for a kind of invalid date that gives you this result? I tried several types of malformed dates and the only result I'm getting is that it's not recognized as a due date at all, as expected.

What happens if you view a task with an invalid due date in Live Preview?
TomWol: > > * Invalid dates are not classified correctly. If you have an invalid due date, the class is `task-due-far`. Scheduled and start dates are also improperly classified as `far`.
> 
> @TomWol can you give an example for a kind of invalid date that gives you this result? I tried several types of malformed dates and the only result I'm getting is that it's not recognized as a due date at all, as expected.

I downloaded a test vault for your pull branch via this post: https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1599#issuecomment-1406809021

In the Tasks note in that test vault, there are a few tasks with invalid dates - they are the ones that I'm referencing. Thanks!
esm7: @claremacrae I need your advice.
In order to write tests to the last 3 points in my latest comment above, I need to create a `QueryRenderer` and a `QueryRendererChild`.
I started creating a `QueryRenderer.test.ts`, however, turns out that `QueryRenderer` includes lots of references to the Obsidian API (creating a render child requires a `Plugin` object and a `MarkdownPostProcessorContext` object).
I don't see how I can cover these 3 items without this... I *can* extract the raw rendering part from the `render` method, but it includes many methods of `QueryRendererChild`, so this will require major refactoring of this code (somewhat similarly to how I took out the individual task rendering code into the new `TaskLineRenderer`), which I'm not sure is what we want right now.
What do you suggest to do here?
The easiest path is of course to assume that these tiny bits of additions to `QueryRenderer` will remain uncovered like the rest of this file.
claremacrae: > @claremacrae I need your advice.
> In order to write tests to the last 3 points in my latest comment above, I need to create a `QueryRenderer` and a `QueryRendererChild`.
> I started creating a `QueryRenderer.test.ts`, however, turns out that `QueryRenderer` includes lots of references to the Obsidian API (creating a render child requires a `Plugin` object and a `MarkdownPostProcessorContext` object).
...
> What do you suggest to do here?

Yeah, I know the pain. I have looked at this several times, and just cannot see a way forward. Obsidian was not designed for automated-testability.

We have a workaround in Tasks, though - add step to the smoke tests, to document small amounts of manual testing that can be done when un-testable code changes, and when dependancies change:

https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#how-do-i-smoke-test-the-tasks-plugin

Which links to:

https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/resources/sample_vaults/Tasks-Demo/Manual%20Testing/Smoke%20Testing%20the%20Tasks%20Plugin.md

The idea is to make it quick and easy for people not expert in Tasks behaviour to validate the behaviour...

So off the top of my head - to spark ideas for your consideration .... For the rendering, if there are say 2 or 3 radically different rendering scenarios to test, perhaps the instructions could say something like:

- Check the default theme is loaded
- Turn off all CSS snippets in Settings/Appearance
- [ ] #task With only test-1-feature-blah-snippet.css enabled, confirm that the appearance looks approximately like [small screenshot showing what it should look like]
- [ ] #task With only test-2-feature-blah-snippet.css enabled, confirm that the appearance looks approximately like [small screenshot showing what it should look like]
- [ ] #task **check**: Checked all above steps for **rendering tasks** worked

I don't know what combinations of CSS variables and Reading/Live Preview mode... Ideally this would be so well set up that it would only add 2 or 3 minutes to the smoke testing...

If you wanted more detailed tests, you could also add another new file in the 'Manual Testing' folder, with more descriptions of things to try out. These files are also very useful for creating screenshots for the documentation.

Maybe one of the examples could show the interaction between SlRvb's custom checkbox snippets and the new rendering, as a lot of users will be using that snippet now? (not in the Smoke tests, it's too much detail for that - just if a separate testing file, if you go down that route...)

esm7: Great, I will add the Query changes to there.
Basically the only thing I'm adding in this PR which is not covered in actual tests is these Query items, which can all be verified by inspecting the elements in a simple query. There's no need to verify any appearance etc.
claremacrae: > > Great, I will add the Query changes to there.
> > Basically the only thing I'm adding in this PR which is not covered in actual tests is these Query items, which can all be verified by inspecting the elements in a simple query. There's no need to verify any appearance etc.
> 
> Oh, OK. Then you just need to figure out how to convey to the tested what is the correct info that they should be checking for...

Without assuming that they know how to inspect CSS via development tools...
esm7: Oh I see now that this is in the fully-functional demo vault, meaning I can actually provide a CSS that will make it visible whether or not these features work. Brilliant.
esm7: > > > * Invalid dates are not classified correctly. If you have an invalid due date, the class is `task-due-far`. Scheduled and start dates are also improperly classified as `far`.
> > 
> > 
> > @TomWol can you give an example for a kind of invalid date that gives you this result? I tried several types of malformed dates and the only result I'm getting is that it's not recognized as a due date at all, as expected.
> 
> I downloaded a test vault for your pull branch via this post: [#1599 (comment)](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1599#issuecomment-1406809021)
> 
> In the Tasks note in that test vault, there are a few tasks with invalid dates - they are the ones that I'm referencing. Thanks!

Oh indeed dates that are correctly formatted but are non-existent (like 2023-02-29) cause this issue. Fixed.

esm7: I believe that I addressed all the comments in the PR :partying_face: 
Documentation and tests are complete, but seemingly in the wrong place in the test vault -- waiting for a clarification on that.
This discussion is very long, so if I missed something, I could use some help in locating it...

Please note that I will be less available in the upcoming days to make code changes (I can still reply to comments etc); actually I may not even be able to find meaningful time to work on this next week at all.
I'm crossing my fingers we're getting closer to a release, and that my upcoming reduced availability won't cause a big delay!
claremacrae: 🎉 Hooray - and thank you.

If you could 'resolve conversation' on the things that are complete, that will make it easier to track what remains.

I can pick the renaming of the test files after I merge....

@esm7 Could you please send me a quick email so I have your email address - my address is in my profile page here...
claremacrae: Or send me a friend request on Discord…
TomWol: This is amazing, thank you so much @esm7.

The only thing I'm still missing is specific tasks classes in the `h4` to `h6` group headings. The classes are mentioned in the following sibling `ul` element containing the tasks list, but if you have more than one `group by` it becomes impossible to determine which class belongs to which heading, as they all only have `tasks-group-heading` as class. 

<img width="264" alt="tasks-group-headings-html" src="https://user-images.githubusercontent.com/1523169/218336999-978cd8fc-c00f-4a5f-845b-c7c9676734df.png">

Maybe they would make more sense in the actual headings rather than in the following `ul`? If you have a particular reason 
why you don't see that working, do let me know.

I think this is a particularly interesting one because it will allow you to easily customize headings based on their class, thereby providing a solution for #1628 and probably more. In below screenshot for example, I have replaced `Priority 3: None` with `No Priority` through CSS, but I had to specify for this case that the priority heading was the adjacent sibling as I had no other way of identifying it through classes. If I switch the order of my two group statements, my CSS rule will no longer work.

<img width="613" alt="Tasks-group-headings" src="https://user-images.githubusercontent.com/1523169/218336292-b4e0a5ca-d6b6-4f0f-9c59-b0710a9444b2.png">

If I now also wanted to add the `📅` Unicode character in front of the due date, I would have to again hard-code that and functionality would break in any other query where the `group by` order does not match. Small stuff, but they to me improve accessibility greatly.

Other than that I will not have any further requests. Brilliant work!

esm7: @TomWol I could not find a way to do what you're asking without some major changes to the grouping code. This code is quite complex so it requires a major time investment which I won't have in the upcoming weeks :-( But nothing in the design of this feature prevents someone who is more acquainted with the code, or even myself at a less-busy month, to add this.
TomWol: > @TomWol I could not find a way to do what you're asking without some major changes to the grouping code. This code is quite complex so it requires a major time investment which I won't have in the upcoming weeks :-( But nothing in the design of this feature prevents someone who is more acquainted with the code, or even myself at a less-busy month, to add this.

I understand and I thank you for all your hard work on this, I am really loving the customization options you have given us. 

I will for now create specific workarounds for this using the frontmatter `cssclass` feature. The fact that you are able to do that might be a good thing to mention in the Advanced styling documentation by the way.  Something like:

> If you want to customize individual notes, Obsidian has provided the `cssclass` key for YAML frontmatter. You can simply add the following frontmatter to the top of your note:
> ```
> ---
> cssclass: <replace with custom classes you want to create, separated by commas if more than one>
> ---
> ```
> 
> The classes you add there will be inserted into the note's most outer `div` element, thereby creating CSS ancestors you can use to further customize your layout for that note.
> 
> For example, I can add `cssclass: tasks-red-description` and then define that notes with that `cssclass` should have a bright red text color for the description, like so:
> 
> ```css
> 
> span.task-description {
>     color: black;
> }
> 
> .tasks-red-description span.task-description {
>     color: red;
> }
> ```
> 
> Because the new rule is more specific than the original rule that specifies black text color, the description text color will now become red. This allows you to further modify and refine your layouts on a per-note basis.
claremacrae: > The fact that you are able to do that might be a good thing to mention in the Advanced styling documentation by the way.

Fabulous idea - thanks @TomWol.
esm7: **Sorry in advance for the early users of this branch -- breaking changes!**

As discussed above, in a rather late realization (but better late than never), I converted some of the usage of classes in this branch into data attributes.
Classes like `task-priority-high` are now a much-cleaner data attribute `data-task-priority="high"`.
I updated all the tests and documentation, including all examples.
@claremacrae I also merged from master and for some reason the linter fails on a change that I didn't do, I'm unsure what's the issue there...
claremacrae: > @claremacrae I also merged from master and for some reason the linter fails on a change that I didn't do, I'm unsure what's the issue there...

I believe the problems are the mixture of type and non-type imports on one line:

https://github.com/obsidian-tasks-group/obsidian-tasks/blob/2c506c016aac53257537c904f159681e7ceb3655/src/Commands/ToggleDone.ts#L1

https://github.com/obsidian-tasks-group/obsidian-tasks/blob/2c506c016aac53257537c904f159681e7ceb3655/src/Query/Filter/DateField.ts#L8

In the past they gave errors, but these recent additions did not cause any problems in GitHub Actions or on my machine, so I let them through.

They don't fail on my current machine.

So what I cannot understand is why they now fail on this branch after merging.

Were there any merge conflicts you had to resolve?
TomWol: > **Sorry in advance for the early users of this branch -- breaking changes!**
> 
> As discussed above, in a rather late realization (but better late than never), I converted some of the usage of classes in this branch into data attributes. Classes like `task-priority-high` are now a much-cleaner data attribute `data-task-priority="high"`. I updated all the tests and documentation, including all examples. @claremacrae I also merged from master and for some reason the linter fails on a change that I didn't do, I'm unsure what's the issue there...

Thanks, will try it out soon! 

With regards to the use of priorities, in https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1628#issuecomment-1449087051 @claremacrae mentions that references of priority `none` will be replaced with `normal` in the future (like it is in the Modal) to be more consistent throughout the code and layout. So far you have been using `none` throughout this PR (haven't checked this new commit though), so it might be a good idea to address it in your new code already? Might prevent future layouts from breaking. As usual, thanks a lot!
claremacrae: > They don't fail on my current machine.
> 
> So what I cannot understand is why they now fail on this branch after merging.
> 
> Were there any merge conflicts you had to resolve?

I can reproduce the problem if I do:

```bash
yarn install
yarn lint
```

I get the same output:

```text
yarn lint
yarn run v1.22.17
$ eslint ./src --fix && eslint ./tests --fix && tsc --noEmit --pretty && svelte-check

/Users/clare/Documents/develop/Obsidian/schemar/obsidian-tasks/src/Commands/ToggleDone.ts
  1:24  error  Parsing error: ',' expected  prettier/prettier

/Users/clare/Documents/develop/Obsidian/schemar/obsidian-tasks/src/Query/Filter/DateField.ts
  8:24  error  Parsing error: ',' expected  prettier/prettier
```

And there a bunch of diffs in `yarn.lock` after the `yarn install` command.

claremacrae: When I check out the commit before the merge from main - c2efb51c7e4263e600ac3431e8ac7aec80e76e89 - and do:

```bash
yarn install && yarn lint
```

I get different errors:

```
yarn install v1.22.17
[1/4] 🔍  Resolving packages...
success Already up-to-date.
✨  Done in 0.34s.
yarn run v1.22.17
$ eslint ./src --fix && eslint ./tests --fix && tsc --noEmit --pretty && svelte-check
src/Commands/CreateOrEdit.ts:2:15 - error TS2305: Module '"obsidian"' has no exported member 'MarkdownFileInfo'.

2 import type { MarkdownFileInfo } from 'obsidian';
                ~~~~~~~~~~~~~~~~

src/Commands/index.ts:1:28 - error TS2305: Module '"obsidian"' has no exported member 'MarkdownFileInfo'.

1 import type { App, Editor, MarkdownFileInfo, MarkdownView, Plugin } from 'obsidian';
                             ~~~~~~~~~~~~~~~~

src/Commands/ToggleDone.ts:2:15 - error TS2305: Module '"obsidian"' has no exported member 'MarkdownFileInfo'.

2 import type { MarkdownFileInfo } from 'obsidian';
                ~~~~~~~~~~~~~~~~


Found 3 errors in 3 files.

Errors  Files
     1  src/Commands/CreateOrEdit.ts:2
     1  src/Commands/index.ts:1
     1  src/Commands/ToggleDone.ts:2
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
```

This commit wasn't pushed, so we didn't see that output in GitHub Actions.




Patch:
diff --git a/README.md b/README.md
index d72791f6..02eafcf5 100644
--- a/README.md
+++ b/README.md
@@ -102,8 +102,12 @@ Then see [the Contributing Guide](https://publish.obsidian.md/tasks-contributing
 
 ## Donations
 
-The plugin is completely free to use. If you love it very much and want to pay it forward, please consider donating to an organization of your choice.
-Two example organizations that you could consider donating to are the Wikimedia Foundation and the Electronic Frontiers Foundation:
+The plugin is completely free to use, and very willingly developed and supported by Clare Macrae since May 2022.
 
-1. [Support the Wikimedia Foundation](https://wikimediafoundation.org/support/)
-2. [Support EFF](https://supporters.eff.org/donate/join-eff-today)
+If you have particularly been helped by the Tasks plugin and wish to support it, please see the following link.
+
+- <https://github.com/sponsors/claremacrae>
+
+Donations will go towards my computing costs and licenses for development tools that I use for productivity.
+
+Thank you 🙏.
diff --git a/contributing/.obsidian/community-plugins.json b/contributing/.obsidian/community-plugins.json
index 25fadc74..00a19449 100644
--- a/contributing/.obsidian/community-plugins.json
+++ b/contributing/.obsidian/community-plugins.json
@@ -2,5 +2,7 @@
   "note-refactor-obsidian",
   "obsidian-dangling-links",
   "reveal-active-file-button",
-  "table-editor-obsidian"
+  "table-editor-obsidian",
+  "copy-publish-url",
+  "cmdr"
 ]
\ No newline at end of file
diff --git a/contributing/.obsidian/graph.json b/contributing/.obsidian/graph.json
index 399a83b7..70597976 100644
--- a/contributing/.obsidian/graph.json
+++ b/contributing/.obsidian/graph.json
@@ -1,6 +1,6 @@
 {
   "collapse-filter": false,
-  "search": "",
+  "search": "-file:About",
   "showTags": false,
   "showAttachments": false,
   "hideUnresolved": false,
diff --git a/contributing/.obsidian/plugins/cmdr/data.json b/contributing/.obsidian/plugins/cmdr/data.json
new file mode 100644
index 00000000..efc5efce
--- /dev/null
+++ b/contributing/.obsidian/plugins/cmdr/data.json
@@ -0,0 +1,36 @@
+{
+  "confirmDeletion": true,
+  "showAddCommand": true,
+  "debug": false,
+  "editorMenu": [],
+  "fileMenu": [
+    {
+      "id": "copy-publish-url:open-publish-url",
+      "icon": "book-open",
+      "name": "Open Publish URL in browser",
+      "mode": "any"
+    }
+  ],
+  "leftRibbon": [],
+  "rightRibbon": [],
+  "titleBar": [],
+  "statusBar": [],
+  "pageHeader": [],
+  "macros": [],
+  "explorer": [],
+  "hide": {
+    "statusbar": [],
+    "leftRibbon": []
+  },
+  "spacing": 8,
+  "advancedToolbar": {
+    "rowHeight": 48,
+    "rowCount": 1,
+    "spacing": 0,
+    "buttonWidth": 48,
+    "columnLayout": false,
+    "mappedIcons": [],
+    "tooltips": false,
+    "heightOffset": 0
+  }
+}
\ No newline at end of file
diff --git a/contributing/.obsidian/plugins/cmdr/main.js b/contributing/.obsidian/plugins/cmdr/main.js
new file mode 100644
index 00000000..494039ad
--- /dev/null
+++ b/contributing/.obsidian/plugins/cmdr/main.js
@@ -0,0 +1,9 @@
+/*
+THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
+if you want to view the source, please visit the github repository of this plugin (https://github.com/phibr0/obsidian-commander)
+*/
+
+var Oe=Object.defineProperty,Hn=Object.defineProperties,Rn=Object.getOwnPropertyDescriptor,Fn=Object.getOwnPropertyDescriptors,On=Object.getOwnPropertyNames,Fe=Object.getOwnPropertySymbols;var ht=Object.prototype.hasOwnProperty,$t=Object.prototype.propertyIsEnumerable;var jt=(e,o,t)=>o in e?Oe(e,o,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[o]=t,Ve=(e,o)=>{for(var t in o||(o={}))ht.call(o,t)&&jt(e,t,o[t]);if(Fe)for(var t of Fe(o))$t.call(o,t)&&jt(e,t,o[t]);return e},Yt=(e,o)=>Hn(e,Fn(o));var Gt=(e,o)=>{var t={};for(var n in e)ht.call(e,n)&&o.indexOf(n)<0&&(t[n]=e[n]);if(e!=null&&Fe)for(var n of Fe(e))o.indexOf(n)<0&&$t.call(e,n)&&(t[n]=e[n]);return t};var Vn=(e,o)=>{for(var t in o)Oe(e,t,{get:o[t],enumerable:!0})},zn=(e,o,t,n)=>{if(o&&typeof o=="object"||typeof o=="function")for(let i of On(o))!ht.call(e,i)&&i!==t&&Oe(e,i,{get:()=>o[i],enumerable:!(n=Rn(o,i))||n.enumerable});return e};var Wn=e=>zn(Oe({},"__esModule",{value:!0}),e);var Vi={};Vn(Vi,{default:()=>dt});module.exports=Wn(Vi);var ze=require("obsidian");var bt=require("obsidian");var Kt={};var Qt={};var eo={};var to={"Open Commander Settings":"Commander Einstellungen \xF6ffnen","Open Macro Builder":"Makro Baukasten \xF6ffnen","Change Icon":"Symbol ver\xE4ndern",Rename:"Umbenennen",Delete:"L\xF6schen","Add command":"Befehl hinzuf\xFCgen","Add new":"Neuen Befehl hinzuf\xFCgen","This Command seems to have been removed. {{command_name}}":"Dieser Befehl wurde entfernt. {{command_name}}","Choose a Command to add":"W\xE4hle einen Befehl zum hinzuf\xFCgen","to navigate":"zum navigieren","to choose an icon":"um ein symbol auszuw\xE4hlen","to cancel":"zum abbrechen","Use a custom name":"Nutze einen benutzerdefinierten Namen","Choose a custom Name for your new Command":"W\xE4hle einen benutzerdefinierten Namen f\xFCr deinen neuen Befehl","to save":"zum speichern","Choose a Icon for your new Command":"W\xE4hle ein Symbol f\xFCr deinen neuen Befehl","to choose a custom icon":"um ein benutzerdefiniertes Symbol auszuw\xE4hlen","Remove Command":"Befehl entfernen","Double click to rename":"Zum umbenennen doppelklicken","This device":"Dieses Ger\xE4t","Added by {{plugin_name}}.":"Hinzugef\xFCgt von {{plugin_name}}.","Warning: This is a checked Command, meaning it might not run under every circumstance.":"Warnung: Dieser Befehl wird nur unter bestimmten Vorraussetzungen ausgef\xFChrt.","Move down":"Nach unten","Move up":"Nach oben","Change Mode (Currently: {{current_mode}})":"Modus ver\xE4ndern (Momentan: {{current_mode}})","Are you sure you want to delete the Command?":"Bist du dir sicher, dass du diesen Befehl entfernen m\xF6chtest?","Remove and don't ask again":"Entfernen und Auswahl speichern",Remove:"Entfernen",Cancel:"Abbrechen","Always ask before removing?":"Immer fragen, bevor ein Befehl gel\xF6scht wird?","Always show a Popup to confirm deletion of a Command.":"Zeige immer ein Popup um L\xF6schen zu best\xE4tigen.",'Show "Add Command" Button':'Zeige "Befehl hinzuf\xFCgen" Knopf','Show the "Add Command" Button in every Menu. Requires restart.':'Zeige den "Befehl hinzuf\xFCgen" Knopf in jedem Men\xFC. Erfordert neustart.',"Please restart Obsidian for these changes to take effect.":"Bitte starte Obsidian neu, damit diese \xC4nderungen in Kraft treten.","Enable debugging":"Aktiviere debugging","Enable console output.":"Aktiviere Konsolen-Output (F\xFCr Entwickler)",General:"Allgemein","Editor Menu":"Editor-Men\xFC","File Menu":"Datei-Men\xFC","Left Ribbon":"Band","Right Ribbon":"Rechtes Band",Titlebar:"Titelleiste",Statusbar:"Statusleiste","Page Header":"Kopfzeile","Support development":"Entwicklung unterst\xFCtzen","No commands here!":"Keine Befehle da!","Would you like to add one now?":"M\xF6chtest du jetzt einen hinzuf\xFCgen?","Hide Commands":"Befehle verstecken","Choose new":"W\xE4hle neu","Hide Commands of other Plugins":"Hide Commands of other Plugins",Icon:"Symbol",Name:"Name","Custom Name":"Benutzerdefinierter Name","Add command to all devices":"F\xFCge Befehl allen Ger\xE4ten hinzu","Add command only to mobile devices":"F\xFCge Befehl nur Mobilen Ger\xE4ten hinzu","Add command only to desktop devices":"F\xFCge Befehl nur Desktop Ger\xE4ten hinzu","Add command only to this device":"F\xFCge Befehl nur diesem Ger\xE4t hinzu",Done:"Fertig","By Johnny\u2728 and phibr0":"Von Johnny\u2728 und phibr0","Leave feedback":"Feedback geben",Donate:"Spenden","Share feedback, issues, and ideas with our feedback form.":"Teile Feedback, Probleme und Ideen mit unserem Feedback Formular!","Consider donating to support development.":"Spende um die Entwicklung zu unterst\xFCtzen.",Save:"Speichern","This Command is not available on this device.":"Dieser Befehl ist auf diesem Ger\xE4t nicht verf\xFCgbar.",Show:"Anzeigen",Hide:"Verstecken","Hide other Commands":"Andere Befehle verstecken","Double click to enter custom value":"Doppelklicken um eigenen Wert einzutragen","Choose custom spacing for Command Buttons":"W\xE4hle den Abstand zwischen Befehlen","Change the spacing between commands. You can set different values on mobile and desktop.":"Ver\xE4ndert den Abstand zwischen Befehlen.",Warning:"Achtung","As of Obsidian 0.16.0 you need to explicitly enable the View Header.":'Ab Obsidian Version 0.16.0 m\xFCssen Sie den "View Header" explizit aktivieren. Anschlie\xDFend muss Obsidian neugestartet werden.',"Open Appearance Settings":"\xD6ffne Darstellungs-Einstellungen",Explorer:"Explorer"};var vt={"Open Commander Settings":"Open Commander Settings","Open Macro Builder":"Open Macro Builder","Change Icon":"Change Icon",Rename:"Rename",Delete:"Delete","Add command":"Add command","Add new":"Add new command","This Command seems to have been removed. {{command_name}}":"This Command seems to have been removed. {{command_name}}","Choose a Command to add":"Choose a Command to add","to navigate":"to navigate","to choose an icon":"to choose an icon","to cancel":"to cancel","Use a custom name":"Use a custom name","Choose a custom Name for your new Command":"Choose a custom Name for your new Command","to save":"to save","Choose a Icon for your new Command":"Choose a Icon for your new Command","to choose a custom icon":"to choose a custom icon","Remove Command":"Remove Command","Double click to rename":"Double click to rename","This device":"This device","Added by {{plugin_name}}.":"Added by {{plugin_name}}.","Warning: This is a checked Command, meaning it might not run under every circumstance.":"Warning: This is a checked Command, meaning it might not run under every circumstance.","Move down":"Move down","Move up":"Move up","Change Mode (Currently: {{current_mode}})":"Change Mode (Currently: {{current_mode}})","Are you sure you want to delete the Command?":"Are you sure you want to delete the Command?","Remove and don't ask again":"Remove and don't ask again",Remove:"Remove",Cancel:"Cancel","Always ask before removing?":"Always ask before removing?","Always show a Popup to confirm deletion of a Command.":"Always show a Popup to confirm deletion of a Command.",'Show "Add Command" Button':'Show "Add Command" Button','Show the "Add Command" Button in every Menu. Requires restart.':'Show the "Add Command" Button in every Menu. Requires restart.',"Please restart Obsidian for these changes to take effect.":"Please restart Obsidian for these changes to take effect.","Enable debugging":"Enable debugging","Enable console output.":"Enable console output.",General:"General","Editor Menu":"Editor Menu","File Menu":"File Menu","Left Ribbon":"Ribbon","Right Ribbon":"Right Ribbon",Titlebar:"Titlebar",Statusbar:"Status Bar","Page Header":"Tab Bar","Support development":"Support development","No commands here!":"No commands here!","Would you like to add one now?":"Would you like to add one now?","Hide Commands":"Hide Commands","Choose new":"Choose new","Hide Commands of other Plugins":"Hide Commands of other Plugins",Icon:"Icon",Name:"Name","Custom Name":"Custom Name","Add command to all devices":"Add command to all devices","Add command only to mobile devices":"Add command only to mobile devices","Add command only to desktop devices":"Add command only to desktop devices","Add command only to this device":"Add command only to this device",Done:"Done","By Johnny\u2728 and phibr0":"By Johnny\u2728 and phibr0","Leave feedback":"Leave feedback",Donate:"Donate","Share feedback, issues, and ideas with our feedback form.":"Share feedback, issues, and ideas with our feedback form.","Consider donating to support development.":"Consider donating to support development.",Save:"Save","This Command is not available on this device.":"This Command is not available on this device.",Show:"Show",Hide:"Hide","Hide other Commands":"Hide other Commands","Double click to enter custom value":"Double click to enter custom value","Choose custom spacing for Command Buttons":"Choose custom spacing for Command Buttons","Change the spacing between commands. You can set different values on mobile and desktop.":"Change the spacing between commands.",Warning:"Warning","As of Obsidian 0.16.0 you need to explicitly enable the View Header.":"As of Obsidian 0.16.0 you need to explicitly enable the Tab Title Bar. Once enabled, you might need to restart Obsidian.","Open Appearance Settings":"Open Appearance Settings",Explorer:"Explorer"};var oo={};var no={};var io={};var ao={};var ro={};var so={};var co={};var lo={};var mo={};var uo={};var po={};var fo={};var ho={};var vo={};var go={};var bo={};var _o={"Open Commander Settings":"\u6253\u5F00 Commander \u8BBE\u7F6E","Open Macro Builder":"\u6253\u5F00\u5B8F\u6307\u4EE4\u751F\u6210\u5668","Change Icon":"\u66F4\u6362\u56FE\u6807",Rename:"\u91CD\u547D\u540D",Delete:"\u5220\u9664","Add command":"\u6DFB\u52A0\u547D\u4EE4","Add new":"\u6DFB\u52A0\u65B0\u547D\u4EE4","This Command seems to have been removed. {{command_name}}":"\u8BE5\u547D\u4EE4\u4F3C\u4E4E\u5DF2\u88AB\u79FB\u9664\u3002{{command_name}}","Choose a Command to add":"\u9009\u62E9\u4E00\u4E2A\u547D\u4EE4\u5E76\u6DFB\u52A0","to navigate":"\u5BFC\u822A","to choose an icon":"\u9009\u4E2D\u4E00\u4E2A\u56FE\u6807","to cancel":"\u53D6\u6D88","Use a custom name":"\u4F7F\u7528\u4E00\u4E2A\u81EA\u5B9A\u4E49\u540D\u79F0","Choose a custom Name for your new Command":"\u4E3A\u4F60\u7684\u65B0\u547D\u4EE4\u9009\u62E9\u4E00\u4E2A\u81EA\u5B9A\u4E49\u540D\u79F0","to save":"\u4FDD\u5B58","Choose a Icon for your new Command":"\u4E3A\u4F60\u7684\u65B0\u547D\u4EE4\u9009\u62E9\u4E00\u4E2A\u56FE\u6807","to choose a custom icon":"\u9009\u62E9\u4E00\u4E2A\u81EA\u5B9A\u4E49\u56FE\u6807","Remove Command":"\u79FB\u9664\u547D\u4EE4","Double click to rename":"\u53CC\u51FB\u4EE5\u91CD\u547D\u540D","This device":"\u8BE5\u8BBE\u5907","Added by {{plugin_name}}.":"\u7531{{plugin_name}}\u6DFB\u52A0\u3002","Warning: This is a checked Command, meaning it might not run under every circumstance.":"\u8B66\u544A\uFF1A\u8FD9\u662F\u4E00\u4E2A\u53D7\u68C0\u7684\u547D\u4EE4\uFF0C\u8FD9\u610F\u5473\u7740\u5B83\u672A\u5FC5\u80FD\u5728\u6240\u4EE5\u73AF\u5883\u4E0B\u8FD0\u884C\u3002","Move down":"\u5411\u4E0B\u79FB\u52A8","Move up":"\u5411\u4E0A\u79FB\u52A8","Change Mode (Currently: {{current_mode}})":"\u5207\u6362\u6A21\u5F0F\uFF08\u5F53\u524D\uFF1A{{current_mode}}\uFF09","Are you sure you want to delete the Command?":"\u662F\u5426\u786E\u8BA4\u79FB\u9664\u8BE5\u547D\u4EE4\uFF1F","Remove and don't ask again":"\u79FB\u9664\u4E14\u4E0D\u8981\u518D\u8BE2\u95EE",Remove:"\u79FB\u9664",Cancel:"\u53D6\u6D88","Always ask before removing?":"\u5728\u79FB\u9664\u524D\u603B\u662F\u8BE2\u95EE\uFF1F","Always show a Popup to confirm deletion of a Command.":"\u5728\u786E\u8BA4\u79FB\u9664\u547D\u4EE4\u524D\u603B\u662F\u5F39\u7A97\u3002",'Show "Add Command" Button':"\u663E\u793A\u201C\u6DFB\u52A0\u547D\u4EE4\u201D\u6309\u94AE",'Show the "Add Command" Button in every Menu. Requires restart.':"\u5728\u6BCF\u4E2A\u83DC\u5355\u90FD\u5C55\u793A\u201C\u6DFB\u52A0\u547D\u4EE4\u201D\u6309\u94AE\u3002\u9700\u8981\u91CD\u542F\u3002","Please restart Obsidian for these changes to take effect.":"\u8BF7\u91CD\u542F Obsidian \u4EE5\u4F7F\u8FD9\u4E9B\u66F4\u6539\u751F\u6548\u3002","Enable debugging":"\u542F\u7528\u9664\u9519","Enable console output.":"\u542F\u7528\u63A7\u5236\u53F0\u8F93\u51FA\u3002",General:"\u901A\u7528","Editor Menu":"\u7F16\u8F91\u5668\u83DC\u5355","File Menu":"\u6587\u4EF6\u83DC\u5355","Left Ribbon":"\u5DE6\u4FA7\u8FB9\u680F","Right Ribbon":"\u53F3\u4FA7\u8FB9\u680F",Titlebar:"\u6807\u9898\u680F",Statusbar:"\u72B6\u6001\u680F","Page Header":"\u9875\u9996","Support development":"\u652F\u6301\u5F00\u53D1","No commands here!":"\u8FD9\u91CC\u6CA1\u6709\u547D\u4EE4\uFF01","Would you like to add one now?":"\u4F60\u73B0\u5728\u60F3\u8981\u52A0\u4E00\u4E2A\u5417\uFF1F","Hide Commands":"\u9690\u85CF\u547D\u4EE4","Choose new":"\u9009\u62E9\u65B0\u7684","Hide Commands of other Plugins":"\u9690\u85CF\u5176\u4ED6\u63D2\u4EF6\u7684\u547D\u4EE4",Icon:"\u56FE\u6807",Name:"\u540D\u79F0","Custom Name":"\u81EA\u5B9A\u4E49\u540D\u79F0","Add command to all devices":"\u5411\u6240\u6709\u8BBE\u5907\u6DFB\u52A0\u547D\u4EE4","Add command only to mobile devices":"\u53EA\u5411\u79FB\u52A8\u8BBE\u5907\u6DFB\u52A0\u547D\u4EE4","Add command only to desktop devices":"\u53EA\u5411\u684C\u9762\u8BBE\u5907\u6DFB\u52A0\u547D\u4EE4","Add command only to this device":"\u53EA\u5411\u5F53\u524D\u8BBE\u5907\u6DFB\u52A0\u547D\u4EE4",Done:"\u5B8C\u6210","By Johnny\u2728 and phibr0":"\u7531 Johnny\u2728 \u548C phibr0 \u5F00\u53D1","Leave feedback":"\u7559\u4E0B\u53CD\u9988",Donate:"\u6350\u8D60","Share feedback, issues, and ideas with our feedback form.":"\u4EE5\u6211\u4EEC\u7684\u53CD\u9988\u8868\uFF0C\u5206\u4EAB\u53CD\u9988\u3001\u8BAE\u9898\u6216\u8005\u4F60\u7684\u60F3\u6CD5\u3002","Consider donating to support development.":"\u8003\u8651\u6350\u8D60\u4EE5\u652F\u6301\u5F00\u53D1\u3002",Save:"\u4FDD\u5B58","This Command is not available on this device.":"\u8FD9\u4E00\u547D\u4EE4\u5728\u5F53\u524D\u8BBE\u5907\u4E0D\u53EF\u7528\u3002",Show:"\u663E\u793A",Hide:"\u9690\u85CF","Hide other Commands":"\u9690\u85CF\u5176\u4F59\u547D\u4EE4","Double click to enter custom value":"\u53CC\u51FB\u4EE5\u6DFB\u52A0\u81EA\u5B9A\u4E49\u503C","Choose custom spacing for Command Buttons":"\u4E3A\u547D\u4EE4\u6309\u94AE\u9009\u62E9\u81EA\u5B9A\u4E49\u95F4\u8DDD","Change the spacing between commands. You can set different values on mobile and desktop.":"\u6539\u53D8\u547D\u4EE4\u4E4B\u95F4\u7684\u95F4\u8DDD\u3002\u4F60\u53EF\u4EE5\u4E3A\u79FB\u52A8\u548C\u684C\u9762\u8BBE\u5907\u8BBE\u7F6E\u4E0D\u540C\u7684\u503C\u3002"};var Co={};var ui={ar:Kt,cs:Qt,da:eo,de:to,en:vt,"en-gb":oo,es:no,fr:io,hi:ao,id:ro,it:so,ja:co,ko:lo,nl:mo,nn:uo,pl:po,pt:fo,"pt-br":ho,ro:vo,ru:go,tr:bo,"zh-cn":_o,"zh-tw":Co},gt=ui[bt.moment.locale()];function u(e){return gt||console.error("Error: dictionary locale not found",bt.moment.locale()),gt&&gt[e]||vt[e]}var ne=class extends ze.FuzzySuggestModal{constructor(t){super(app);this.plugin=t,this.commands=Object.values(app.commands.commands),this.setPlaceholder(u("Choose a Command to add")),this.setInstructions([{command:"\u2191\u2193",purpose:u("to navigate")},{command:"\u21B5",purpose:u("to choose an icon")},{command:"esc",purpose:u("to cancel")}])}async awaitSelection(){return this.open(),new Promise((t,n)=>{this.onChooseItem=i=>t(i),this.onClose=()=>window.setTimeout(()=>n("No Command selected"),0)})}renderSuggestion(t,n){if(n.addClass("mod-complex"),n.createDiv({cls:"suggestion-content"}).createDiv({cls:"suggestion-title"}).setText(t.item.name),t.item.icon){let a=n.createDiv({cls:"suggestion-aux"});(0,ze.setIcon)(a.createSpan({cls:"suggestion-flair"}),t.item.icon)}}getItems(){return this.commands}getItemText(t){return t.name}onChooseItem(t,n){}};var yo={confirmDeletion:!0,showAddCommand:!0,debug:!1,editorMenu:[],fileMenu:[],leftRibbon:[],rightRibbon:[],titleBar:[],statusBar:[],pageHeader:[],macros:[],explorer:[],hide:{statusbar:[],leftRibbon:[]},spacing:8,advancedToolbar:{rowHeight:48,rowCount:1,spacing:0,buttonWidth:48,columnLayout:!1,mappedIcons:[],tooltips:!1,heightOffset:0}},wo=["activity","airplay","alarm-check","alarm-clock-off","alarm-clock","alarm-minus","alarm-plus","album","alert-circle","alert-octagon","alert-triangle","align-center-horizontal","align-center-vertical","align-center","align-end-horizontal","align-end-vertical","align-horizontal-distribute-center","align-horizontal-distribute-end","align-horizontal-distribute-start","align-horizontal-justify-center","align-horizontal-justify-end","align-horizontal-justify-start","align-horizontal-space-around","align-horizontal-space-between","align-justify","align-left","align-right","align-start-horizontal","align-start-vertical","align-vertical-distribute-center","align-vertical-distribute-end","align-vertical-distribute-start","align-vertical-justify-center","align-vertical-justify-end","align-vertical-justify-start","align-vertical-space-around","align-vertical-space-between","anchor","aperture","archive","arrow-big-down","arrow-big-left","arrow-big-right","arrow-big-up","arrow-down-circle","arrow-down-left","arrow-down-right","arrow-down","arrow-left-circle","arrow-left-right","arrow-left","arrow-right-circle","arrow-right","arrow-up-circle","arrow-up-left","arrow-up-right","arrow-up","asterisk","at-sign","award","axe","banknote","bar-chart-2","bar-chart","baseline","battery-charging","battery-full","battery-low","battery-medium","battery","beaker","bell-minus","bell-off","bell-plus","bell-ring","bell","bike","binary","bitcoin","bluetooth-connected","bluetooth-off","bluetooth-searching","bluetooth","bold","book-open","book","bookmark-minus","bookmark-plus","bookmark","bot","box-select","box","briefcase","brush","bug","building-2","building","bus","calculator","calendar","camera-off","camera","car","carrot","cast","check-circle-2","check-circle","check-square","check","chevron-down","chevron-first","chevron-last","chevron-left","chevron-right","chevron-up","chevrons-down-up","chevrons-down","chevrons-left","chevrons-right","chevrons-up-down","chevrons-up","chrome","circle-slashed","circle","clipboard-check","clipboard-copy","clipboard-list","clipboard-x","clipboard","clock-1","clock-10","clock-11","clock-12","clock-2","clock-3","clock-4","clock-5","clock-6","clock-7","clock-8","clock-9","lucide-clock","cloud-drizzle","cloud-fog","cloud-hail","cloud-lightning","cloud-moon","cloud-off","cloud-rain-wind","cloud-rain","cloud-snow","cloud-sun","lucide-cloud","cloudy","clover","code-2","code","codepen","codesandbox","coffee","coins","columns","command","compass","contact","contrast","cookie","copy","copyleft","copyright","corner-down-left","corner-down-right","corner-left-down","corner-left-up","corner-right-down","corner-right-up","corner-up-left","corner-up-right","cpu","credit-card","crop","lucide-cross","crosshair","crown","currency","database","delete","dice-1","dice-2","dice-3","dice-4","dice-5","dice-6","disc","divide-circle","divide-square","divide","dollar-sign","download-cloud","download","dribbble","droplet","droplets","drumstick","edit-2","edit-3","edit","egg","equal-not","equal","eraser","euro","expand","external-link","eye-off","eye","facebook","fast-forward","feather","figma","file-check-2","file-check","file-code","file-digit","file-input","file-minus-2","file-minus","file-output","file-plus-2","file-plus","file-search","file-text","file-x-2","file-x","file","files","film","filter","flag-off","flag-triangle-left","flag-triangle-right","flag","flame","flashlight-off","flashlight","flask-conical","flask-round","folder-minus","folder-open","folder-plus","lucide-folder","form-input","forward","frame","framer","frown","function-square","gamepad-2","gamepad","gauge","gavel","gem","ghost","gift","git-branch-plus","git-branch","git-commit","git-fork","git-merge","git-pull-request","github","gitlab","glasses","globe-2","globe","grab","graduation-cap","grid","grip-horizontal","grip-vertical","hammer","hand-metal","hand","hard-drive","hard-hat","hash","haze","headphones","heart","help-circle","hexagon","highlighter","history","home","image-minus","image-off","image-plus","image","import","inbox","indent","indian-rupee","infinity","lucide-info","inspect","instagram","italic","japanese-yen","key","keyboard","landmark","lucide-languages","laptop-2","laptop","lasso-select","lasso","layers","layout-dashboard","layout-grid","layout-list","layout-template","layout","library","life-buoy","lightbulb-off","lightbulb","link-2-off","link-2","lucide-link","linkedin","list-checks","list-minus","list-ordered","list-plus","list-x","list","loader-2","loader","locate-fixed","locate-off","locate","lock","log-in","log-out","mail","map-pin","map","maximize-2","maximize","megaphone","meh","menu","message-circle","message-square","mic-off","mic","minimize-2","minimize","minus-circle","minus-square","minus","monitor-off","monitor-speaker","monitor","moon","more-horizontal","more-vertical","mountain-snow","mountain","mouse-pointer-2","mouse-pointer-click","mouse-pointer","mouse","move-diagonal-2","move-diagonal","move-horizontal","move-vertical","move","music","navigation-2","navigation","network","octagon","option","outdent","package-check","package-minus","package-plus","package-search","package-x","package","palette","palmtree","paperclip","pause-circle","pause-octagon","pause","pen-tool","lucide-pencil","percent","person-standing","phone-call","phone-forwarded","phone-incoming","phone-missed","phone-off","phone-outgoing","phone","pie-chart","piggy-bank","lucide-pin","pipette","plane","play-circle","play","plug-zap","plus-circle","plus-square","plus","pocket","podcast","pointer","pound-sterling","power-off","power","printer","qr-code","quote","radio-receiver","radio","redo","refresh-ccw","refresh-cw","regex","repeat-1","repeat","reply-all","reply","rewind","rocket","rocking-chair","rotate-ccw","rotate-cw","rss","ruler","russian-ruble","save","scale","scan-line","scan","scissors","screen-share-off","screen-share","lucide-search","send","separator-horizontal","separator-vertical","server-crash","server-off","server","settings-2","settings","share-2","share","sheet","shield-alert","shield-check","shield-close","shield-off","shield","shirt","shopping-bag","shopping-cart","shovel","shrink","shuffle","sidebar-close","sidebar-open","sidebar","sigma","signal-high","signal-low","signal-medium","signal-zero","signal","skip-back","skip-forward","skull","slack","slash","sliders","smartphone-charging","smartphone","smile","snowflake","sort-asc","sort-desc","speaker","sprout","square","star-half","lucide-star","stop-circle","stretch-horizontal","stretch-vertical","strikethrough","subscript","sun","sunrise","sunset","superscript","swiss-franc","switch-camera","table","tablet","tag","target","tent","terminal-square","terminal","text-cursor-input","text-cursor","thermometer-snowflake","thermometer-sun","thermometer","thumbs-down","thumbs-up","ticket","timer-off","timer-reset","timer","toggle-left","toggle-right","tornado","trash-2","lucide-trash","trello","trending-down","trending-up","triangle","truck","tv-2","tv","twitch","twitter","type","umbrella","underline","undo","unlink-2","unlink","unlock","upload-cloud","upload","user-check","user-minus","user-plus","user-x","user","users","verified","vibrate","video-off","video","view","voicemail","volume-1","volume-2","volume-x","volume","wallet","wand","watch","waves","webcam","wifi-off","wifi","wind","wrap-text","wrench","x-circle","x-octagon","x-square","x","youtube","zap-off","zap","zoom-in","zoom-out","search-large"];var We=require("obsidian");var R=class extends We.FuzzySuggestModal{constructor(t){super(app);this.plugin=t,this.setPlaceholder(u("Choose a Icon for your new Command")),this.setInstructions([{command:"\u2191\u2193",purpose:u("to navigate")},{command:"\u21B5",purpose:u("to choose a custom icon")},{command:"esc",purpose:u("to cancel")}])}async awaitSelection(){return this.open(),new Promise((t,n)=>{this.onChooseItem=i=>t(i),this.onClose=()=>window.setTimeout(()=>n("No Icon selected"),0)})}renderSuggestion(t,n){n.addClass("mod-complex"),n.createDiv({cls:"suggestion-content"}).createDiv({cls:"suggestion-title"}).setText(t.item.replace(/-/g," ").replace(/(^\w{1})|(\s+\w{1})/g,c=>c.toUpperCase()));let a=n.createDiv({cls:"suggestion-aux"});(0,We.setIcon)(a.createSpan({cls:"suggestion-flair"}),t.item)}getItems(){return wo}getItemText(t){return t}onChooseItem(t,n){}};var et=require("obsidian");var ko=require("obsidian");var Z=class extends ko.SuggestModal{constructor(t){super(app);this.defaultName=t;this.setPlaceholder(u("Use a custom name")),this.resultContainerEl.style.display="none",this.setInstructions([{command:"",purpose:u("Choose a custom Name for your new Command")},{command:"\u21B5",purpose:u("to save")},{command:"esc",purpose:u("to cancel")}])}onOpen(){var i;super.onOpen(),this.inputEl.value=this.defaultName;let t=createDiv({cls:"cmdr-name-input-wrapper"});(i=this.inputEl.parentNode)==null||i.insertBefore(t,this.inputEl),t.appendChild(this.inputEl),t.parentElement.style.display="block";let n=createEl("button",{text:u("Save"),cls:"mod-cta"});n.onclick=a=>this.selectSuggestion(this.inputEl.value,a),t.appendChild(n)}async awaitSelection(){return this.open(),new Promise((t,n)=>{this.onChooseSuggestion=i=>t(i),this.onClose=()=>window.setTimeout(()=>n("No Name selected"),0)})}getSuggestions(t){return[t]}renderSuggestion(t,n){}onChooseSuggestion(t,n){}};var Je,C,So,pi,Ce,Mo,Po,Ze={},Lo=[],fi=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function ie(e,o){for(var t in o)e[t]=o[t];return e}function To(e){var o=e.parentNode;o&&o.removeChild(e)}function r(e,o,t){var n,i,a,c={};for(a in o)a=="key"?n=o[a]:a=="ref"?i=o[a]:c[a]=o[a];if(arguments.length>2&&(c.children=arguments.length>3?Je.call(arguments,2):t),typeof e=="function"&&e.defaultProps!=null)for(a in e.defaultProps)c[a]===void 0&&(c[a]=e.defaultProps[a]);return Ue(e,c,n,i,null)}function Ue(e,o,t,n,i){var a={type:e,props:o,key:t,ref:n,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:i==null?++So:i};return i==null&&C.vnode!=null&&C.vnode(a),a}function S(e){return e.children}function G(e,o){this.props=e,this.context=o}function ye(e,o){if(o==null)return e.__?ye(e.__,e.__.__k.indexOf(e)+1):null;for(var t;o<e.__k.length;o++)if((t=e.__k[o])!=null&&t.__e!=null)return t.__e;return typeof e.type=="function"?ye(e):null}function No(e){var o,t;if((e=e.__)!=null&&e.__c!=null){for(e.__e=e.__c.base=null,o=0;o<e.__k.length;o++)if((t=e.__k[o])!=null&&t.__e!=null){e.__e=e.__c.base=t.__e;break}return No(e)}}function _t(e){(!e.__d&&(e.__d=!0)&&Ce.push(e)&&!qe.__r++||Mo!==C.debounceRendering)&&((Mo=C.debounceRendering)||setTimeout)(qe)}function qe(){for(var e;qe.__r=Ce.length;)e=Ce.sort(function(o,t){return o.__v.__b-t.__v.__b}),Ce=[],e.some(function(o){var t,n,i,a,c,d;o.__d&&(c=(a=(t=o).__v).__e,(d=t.__P)&&(n=[],(i=ie({},a)).__v=a.__v+1,Ct(d,a,i,t.__n,d.ownerSVGElement!==void 0,a.__h!=null?[c]:null,n,c==null?ye(a):c,a.__h),Ho(n,a),a.__e!=c&&No(a)))})}function Ao(e,o,t,n,i,a,c,d,m,h){var s,g,_,f,b,y,v,L=n&&n.__k||Lo,w=L.length;for(t.__k=[],s=0;s<o.length;s++)if((f=t.__k[s]=(f=o[s])==null||typeof f=="boolean"?null:typeof f=="string"||typeof f=="number"||typeof f=="bigint"?Ue(null,f,null,null,f):Array.isArray(f)?Ue(S,{children:f},null,null,null):f.__b>0?Ue(f.type,f.props,f.key,f.ref?f.ref:null,f.__v):f)!=null){if(f.__=t,f.__b=t.__b+1,(_=L[s])===null||_&&f.key==_.key&&f.type===_.type)L[s]=void 0;else for(g=0;g<w;g++){if((_=L[g])&&f.key==_.key&&f.type===_.type){L[g]=void 0;break}_=null}Ct(e,f,_=_||Ze,i,a,c,d,m,h),b=f.__e,(g=f.ref)&&_.ref!=g&&(v||(v=[]),_.ref&&v.push(_.ref,null,f),v.push(g,f.__c||b,f)),b!=null?(y==null&&(y=b),typeof f.type=="function"&&f.__k===_.__k?f.__d=m=Bo(f,m,e):m=Do(e,f,_,L,b,m),typeof t.type=="function"&&(t.__d=m)):m&&_.__e==m&&m.parentNode!=e&&(m=ye(_))}for(t.__e=y,s=w;s--;)L[s]!=null&&Fo(L[s],L[s]);if(v)for(s=0;s<v.length;s++)Ro(v[s],v[++s],v[++s])}function Bo(e,o,t){for(var n,i=e.__k,a=0;i&&a<i.length;a++)(n=i[a])&&(n.__=e,o=typeof n.type=="function"?Bo(n,o,t):Do(t,n,n,i,n.__e,o));return o}function we(e,o){return o=o||[],e==null||typeof e=="boolean"||(Array.isArray(e)?e.some(function(t){we(t,o)}):o.push(e)),o}function Do(e,o,t,n,i,a){var c,d,m;if(o.__d!==void 0)c=o.__d,o.__d=void 0;else if(t==null||i!=a||i.parentNode==null)e:if(a==null||a.parentNode!==e)e.appendChild(i),c=null;else{for(d=a,m=0;(d=d.nextSibling)&&m<n.length;m+=1)if(d==i)break e;e.insertBefore(i,a),c=a}return c!==void 0?c:i.nextSibling}function hi(e,o,t,n,i){var a;for(a in t)a==="children"||a==="key"||a in o||Xe(e,a,null,t[a],n);for(a in o)i&&typeof o[a]!="function"||a==="children"||a==="key"||a==="value"||a==="checked"||t[a]===o[a]||Xe(e,a,o[a],t[a],n)}function Eo(e,o,t){o[0]==="-"?e.setProperty(o,t):e[o]=t==null?"":typeof t!="number"||fi.test(o)?t:t+"px"}function Xe(e,o,t,n,i){var a;e:if(o==="style")if(typeof t=="string")e.style.cssText=t;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(o in n)t&&o in t||Eo(e.style,o,"");if(t)for(o in t)n&&t[o]===n[o]||Eo(e.style,o,t[o])}else if(o[0]==="o"&&o[1]==="n")a=o!==(o=o.replace(/Capture$/,"")),o=o.toLowerCase()in e?o.toLowerCase().slice(2):o.slice(2),e.l||(e.l={}),e.l[o+a]=t,t?n||e.addEventListener(o,a?Io:xo,a):e.removeEventListener(o,a?Io:xo,a);else if(o!=="dangerouslySetInnerHTML"){if(i)o=o.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(o!=="href"&&o!=="list"&&o!=="form"&&o!=="tabIndex"&&o!=="download"&&o in e)try{e[o]=t==null?"":t;break e}catch(c){}typeof t=="function"||(t==null||t===!1&&o.indexOf("-")==-1?e.removeAttribute(o):e.setAttribute(o,t))}}function xo(e){this.l[e.type+!1](C.event?C.event(e):e)}function Io(e){this.l[e.type+!0](C.event?C.event(e):e)}function Ct(e,o,t,n,i,a,c,d,m){var h,s,g,_,f,b,y,v,L,w,H,ee,De,ue,pe,U=o.type;if(o.constructor!==void 0)return null;t.__h!=null&&(m=t.__h,d=o.__e=t.__e,o.__h=null,a=[d]),(h=C.__b)&&h(o);try{e:if(typeof U=="function"){if(v=o.props,L=(h=U.contextType)&&n[h.__c],w=h?L?L.props.value:h.__:n,t.__c?y=(s=o.__c=t.__c).__=s.__E:("prototype"in U&&U.prototype.render?o.__c=s=new U(v,w):(o.__c=s=new G(v,w),s.constructor=U,s.render=gi),L&&L.sub(s),s.props=v,s.state||(s.state={}),s.context=w,s.__n=n,g=s.__d=!0,s.__h=[],s._sb=[]),s.__s==null&&(s.__s=s.state),U.getDerivedStateFromProps!=null&&(s.__s==s.state&&(s.__s=ie({},s.__s)),ie(s.__s,U.getDerivedStateFromProps(v,s.__s))),_=s.props,f=s.state,g)U.getDerivedStateFromProps==null&&s.componentWillMount!=null&&s.componentWillMount(),s.componentDidMount!=null&&s.__h.push(s.componentDidMount);else{if(U.getDerivedStateFromProps==null&&v!==_&&s.componentWillReceiveProps!=null&&s.componentWillReceiveProps(v,w),!s.__e&&s.shouldComponentUpdate!=null&&s.shouldComponentUpdate(v,s.__s,w)===!1||o.__v===t.__v){for(s.props=v,s.state=s.__s,o.__v!==t.__v&&(s.__d=!1),s.__v=o,o.__e=t.__e,o.__k=t.__k,o.__k.forEach(function(fe){fe&&(fe.__=o)}),H=0;H<s._sb.length;H++)s.__h.push(s._sb[H]);s._sb=[],s.__h.length&&c.push(s);break e}s.componentWillUpdate!=null&&s.componentWillUpdate(v,s.__s,w),s.componentDidUpdate!=null&&s.__h.push(function(){s.componentDidUpdate(_,f,b)})}if(s.context=w,s.props=v,s.__v=o,s.__P=e,ee=C.__r,De=0,"prototype"in U&&U.prototype.render){for(s.state=s.__s,s.__d=!1,ee&&ee(o),h=s.render(s.props,s.state,s.context),ue=0;ue<s._sb.length;ue++)s.__h.push(s._sb[ue]);s._sb=[]}else do s.__d=!1,ee&&ee(o),h=s.render(s.props,s.state,s.context),s.state=s.__s;while(s.__d&&++De<25);s.state=s.__s,s.getChildContext!=null&&(n=ie(ie({},n),s.getChildContext())),g||s.getSnapshotBeforeUpdate==null||(b=s.getSnapshotBeforeUpdate(_,f)),pe=h!=null&&h.type===S&&h.key==null?h.props.children:h,Ao(e,Array.isArray(pe)?pe:[pe],o,t,n,i,a,c,d,m),s.base=o.__e,o.__h=null,s.__h.length&&c.push(s),y&&(s.__E=s.__=null),s.__e=!1}else a==null&&o.__v===t.__v?(o.__k=t.__k,o.__e=t.__e):o.__e=vi(t.__e,o,t,n,i,a,c,m);(h=C.diffed)&&h(o)}catch(fe){o.__v=null,(m||a!=null)&&(o.__e=d,o.__h=!!m,a[a.indexOf(d)]=null),C.__e(fe,o,t)}}function Ho(e,o){C.__c&&C.__c(o,e),e.some(function(t){try{e=t.__h,t.__h=[],e.some(function(n){n.call(t)})}catch(n){C.__e(n,t.__v)}})}function vi(e,o,t,n,i,a,c,d){var m,h,s,g=t.props,_=o.props,f=o.type,b=0;if(f==="svg"&&(i=!0),a!=null){for(;b<a.length;b++)if((m=a[b])&&"setAttribute"in m==!!f&&(f?m.localName===f:m.nodeType===3)){e=m,a[b]=null;break}}if(e==null){if(f===null)return document.createTextNode(_);e=i?document.createElementNS("http://www.w3.org/2000/svg",f):document.createElement(f,_.is&&_),a=null,d=!1}if(f===null)g===_||d&&e.data===_||(e.data=_);else{if(a=a&&Je.call(e.childNodes),h=(g=t.props||Ze).dangerouslySetInnerHTML,s=_.dangerouslySetInnerHTML,!d){if(a!=null)for(g={},b=0;b<e.attributes.length;b++)g[e.attributes[b].name]=e.attributes[b].value;(s||h)&&(s&&(h&&s.__html==h.__html||s.__html===e.innerHTML)||(e.innerHTML=s&&s.__html||""))}if(hi(e,_,g,i,d),s)o.__k=[];else if(b=o.props.children,Ao(e,Array.isArray(b)?b:[b],o,t,n,i&&f!=="foreignObject",a,c,a?a[0]:t.__k&&ye(t,0),d),a!=null)for(b=a.length;b--;)a[b]!=null&&To(a[b]);d||("value"in _&&(b=_.value)!==void 0&&(b!==e.value||f==="progress"&&!b||f==="option"&&b!==g.value)&&Xe(e,"value",b,g.value,!1),"checked"in _&&(b=_.checked)!==void 0&&b!==e.checked&&Xe(e,"checked",b,g.checked,!1))}return e}function Ro(e,o,t){try{typeof e=="function"?e(o):e.current=o}catch(n){C.__e(n,t)}}function Fo(e,o,t){var n,i;if(C.unmount&&C.unmount(e),(n=e.ref)&&(n.current&&n.current!==e.__e||Ro(n,null,o)),(n=e.__c)!=null){if(n.componentWillUnmount)try{n.componentWillUnmount()}catch(a){C.__e(a,o)}n.base=n.__P=null,e.__c=void 0}if(n=e.__k)for(i=0;i<n.length;i++)n[i]&&Fo(n[i],o,t||typeof e.type!="function");t||e.__e==null||To(e.__e),e.__=e.__e=e.__d=void 0}function gi(e,o,t){return this.constructor(e,t)}function W(e,o,t){var n,i,a;C.__&&C.__(e,o),i=(n=typeof t=="function")?null:t&&t.__k||o.__k,a=[],Ct(o,e=(!n&&t||o).__k=r(S,null,[e]),i||Ze,Ze,o.ownerSVGElement!==void 0,!n&&t?[t]:i?null:o.firstChild?Je.call(o.childNodes):null,a,!n&&t?t:i?i.__e:o.firstChild,n),Ho(a,e)}function je(e,o){var t={__c:o="__cC"+Po++,__:e,Consumer:function(n,i){return n.children(i)},Provider:function(n){var i,a;return this.getChildContext||(i=[],(a={})[o]=this,this.getChildContext=function(){return a},this.shouldComponentUpdate=function(c){this.props.value!==c.value&&i.some(_t)},this.sub=function(c){i.push(c);var d=c.componentWillUnmount;c.componentWillUnmount=function(){i.splice(i.indexOf(c),1),d&&d.call(c)}}),n.children}};return t.Provider.__=t.Consumer.contextType=t}Je=Lo.slice,C={__e:function(e,o,t,n){for(var i,a,c;o=o.__;)if((i=o.__c)&&!i.__)try{if((a=i.constructor)&&a.getDerivedStateFromError!=null&&(i.setState(a.getDerivedStateFromError(e)),c=i.__d),i.componentDidCatch!=null&&(i.componentDidCatch(e,n||{}),c=i.__d),c)return i.__E=i}catch(d){e=d}throw e}},So=0,pi=function(e){return e!=null&&e.constructor===void 0},G.prototype.setState=function(e,o){var t;t=this.__s!=null&&this.__s!==this.state?this.__s:this.__s=ie({},this.state),typeof e=="function"&&(e=e(ie({},t),this.props)),e&&ie(t,e),e!=null&&this.__v&&(o&&this._sb.push(o),_t(this))},G.prototype.forceUpdate=function(e){this.__v&&(this.__e=!0,e&&this.__h.push(e),_t(this))},G.prototype.render=S,Ce=[],qe.__r=0,Po=0;var ke,F,yt,Oo,Ge=0,Xo=[],$e=[],Vo=C.__b,zo=C.__r,Wo=C.diffed,Uo=C.__c,Zo=C.unmount;function Ke(e,o){C.__h&&C.__h(F,e,Ge||o),Ge=0;var t=F.__H||(F.__H={__:[],__h:[]});return e>=t.__.length&&t.__.push({__V:$e}),t.__[e]}function B(e){return Ge=1,Jo(jo,e)}function Jo(e,o,t){var n=Ke(ke++,2);if(n.t=e,!n.__c&&(n.__=[t?t(o):jo(void 0,o),function(a){var c=n.__N?n.__N[0]:n.__[0],d=n.t(c,a);c!==d&&(n.__N=[d,n.__[1]],n.__c.setState({}))}],n.__c=F,!F.u)){F.u=!0;var i=F.shouldComponentUpdate;F.shouldComponentUpdate=function(a,c,d){if(!n.__c.__H)return!0;var m=n.__c.__H.__.filter(function(s){return s.__c});if(m.every(function(s){return!s.__N}))return!i||i.call(this,a,c,d);var h=!1;return m.forEach(function(s){if(s.__N){var g=s.__[0];s.__=s.__N,s.__N=void 0,g!==s.__[0]&&(h=!0)}}),!(!h&&n.__c.props===a)&&(!i||i.call(this,a,c,d))}}return n.__N||n.__}function O(e,o){var t=Ke(ke++,3);!C.__s&&Mt(t.__H,o)&&(t.__=e,t.i=o,F.__H.__h.push(t))}function kt(e,o){var t=Ke(ke++,4);!C.__s&&Mt(t.__H,o)&&(t.__=e,t.i=o,F.__h.push(t))}function K(e){return Ge=5,Qe(function(){return{current:e}},[])}function Qe(e,o){var t=Ke(ke++,7);return Mt(t.__H,o)?(t.__V=e(),t.i=o,t.__h=e,t.__V):t.__}function bi(){for(var e;e=Xo.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(Ye),e.__H.__h.forEach(wt),e.__H.__h=[]}catch(o){e.__H.__h=[],C.__e(o,e.__v)}}C.__b=function(e){F=null,Vo&&Vo(e)},C.__r=function(e){zo&&zo(e),ke=0;var o=(F=e.__c).__H;o&&(yt===F?(o.__h=[],F.__h=[],o.__.forEach(function(t){t.__N&&(t.__=t.__N),t.__V=$e,t.__N=t.i=void 0})):(o.__h.forEach(Ye),o.__h.forEach(wt),o.__h=[])),yt=F},C.diffed=function(e){Wo&&Wo(e);var o=e.__c;o&&o.__H&&(o.__H.__h.length&&(Xo.push(o)!==1&&Oo===C.requestAnimationFrame||((Oo=C.requestAnimationFrame)||_i)(bi)),o.__H.__.forEach(function(t){t.i&&(t.__H=t.i),t.__V!==$e&&(t.__=t.__V),t.i=void 0,t.__V=$e})),yt=F=null},C.__c=function(e,o){o.some(function(t){try{t.__h.forEach(Ye),t.__h=t.__h.filter(function(n){return!n.__||wt(n)})}catch(n){o.some(function(i){i.__h&&(i.__h=[])}),o=[],C.__e(n,t.__v)}}),Uo&&Uo(e,o)},C.unmount=function(e){Zo&&Zo(e);var o,t=e.__c;t&&t.__H&&(t.__H.__.forEach(function(n){try{Ye(n)}catch(i){o=i}}),t.__H=void 0,o&&C.__e(o,t.__v))};var qo=typeof requestAnimationFrame=="function";function _i(e){var o,t=function(){clearTimeout(n),qo&&cancelAnimationFrame(o),setTimeout(e)},n=setTimeout(t,100);qo&&(o=requestAnimationFrame(t))}function Ye(e){var o=F,t=e.__c;typeof t=="function"&&(e.__c=void 0,t()),F=o}function wt(e){var o=F;e.__c=e.__(),F=o}function Mt(e,o){return!e||e.length!==o.length||o.some(function(t,n){return t!==e[n]})}function jo(e,o){return typeof o=="function"?o(e):o}var Et={};(function e(o,t,n,i){var a=!!(o.Worker&&o.Blob&&o.Promise&&o.OffscreenCanvas&&o.OffscreenCanvasRenderingContext2D&&o.HTMLCanvasElement&&o.HTMLCanvasElement.prototype.transferControlToOffscreen&&o.URL&&o.URL.createObjectURL);function c(){}function d(p){var l=t.exports.Promise,I=l!==void 0?l:o.Promise;return typeof I=="function"?new I(p):(p(c,c),null)}var m=function(){var p=Math.floor(16.666666666666668),l,I,M={},T=0;return typeof requestAnimationFrame=="function"&&typeof cancelAnimationFrame=="function"?(l=function(P){var E=Math.random();return M[E]=requestAnimationFrame(function x(D){T===D||T+p-1<D?(T=D,delete M[E],P()):M[E]=requestAnimationFrame(x)}),E},I=function(P){M[P]&&cancelAnimationFrame(M[P])}):(l=function(P){return setTimeout(P,p)},I=function(P){return clearTimeout(P)}),{frame:l,cancel:I}}(),h=function(){var p,l,I={};function M(T){function P(E,x){T.postMessage({options:E||{},callback:x})}T.init=function(x){var D=x.transferControlToOffscreen();T.postMessage({canvas:D},[D])},T.fire=function(x,D,te){if(l)return P(x,null),l;var N=Math.random().toString(36).slice(2);return l=d(function(Y){function $(A){A.data.callback===N&&(delete I[N],T.removeEventListener("message",$),l=null,te(),Y())}T.addEventListener("message",$),P(x,N),I[N]=$.bind(null,{data:{callback:N}})}),l},T.reset=function(){T.postMessage({reset:!0});for(var x in I)I[x](),delete I[x]}}return function(){if(p)return p;if(!n&&a){var T=["var CONFETTI, SIZE = {}, module = {};","("+e.toString()+")(this, module, true, SIZE);","onmessage = function(msg) {","  if (msg.data.options) {","    CONFETTI(msg.data.options).then(function () {","      if (msg.data.callback) {","        postMessage({ callback: msg.data.callback });","      }","    });","  } else if (msg.data.reset) {","    CONFETTI && CONFETTI.reset();","  } else if (msg.data.resize) {","    SIZE.width = msg.data.resize.width;","    SIZE.height = msg.data.resize.height;","  } else if (msg.data.canvas) {","    SIZE.width = msg.data.canvas.width;","    SIZE.height = msg.data.canvas.height;","    CONFETTI = module.exports.create(msg.data.canvas);","  }","}"].join(`
+`);try{p=new Worker(URL.createObjectURL(new Blob([T])))}catch(P){return typeof console!==void 0&&typeof console.warn=="function"&&console.warn("\u{1F38A} Could not load worker",P),null}M(p)}return p}}(),s={particleCount:50,angle:90,spread:45,startVelocity:45,decay:.9,gravity:1,drift:0,ticks:200,x:.5,y:.5,shapes:["square","circle"],zIndex:100,colors:["#26ccff","#a25afd","#ff5e7e","#88ff5a","#fcff42","#ffa62d","#ff36ff"],disableForReducedMotion:!1,scalar:1};function g(p,l){return l?l(p):p}function _(p){return p!=null}function f(p,l,I){return g(p&&_(p[l])?p[l]:s[l],I)}function b(p){return p<0?0:Math.floor(p)}function y(p,l){return Math.floor(Math.random()*(l-p))+p}function v(p){return parseInt(p,16)}function L(p){return p.map(w)}function w(p){var l=String(p).replace(/[^0-9a-f]/gi,"");return l.length<6&&(l=l[0]+l[0]+l[1]+l[1]+l[2]+l[2]),{r:v(l.substring(0,2)),g:v(l.substring(2,4)),b:v(l.substring(4,6))}}function H(p){var l=f(p,"origin",Object);return l.x=f(l,"x",Number),l.y=f(l,"y",Number),l}function ee(p){p.width=document.documentElement.clientWidth,p.height=document.documentElement.clientHeight}function De(p){var l=p.getBoundingClientRect();p.width=l.width,p.height=l.height}function ue(p){var l=document.createElement("canvas");return l.style.position="fixed",l.style.top="0px",l.style.left="0px",l.style.pointerEvents="none",l.style.zIndex=p,l}function pe(p,l,I,M,T,P,E,x,D){p.save(),p.translate(l,I),p.rotate(P),p.scale(M,T),p.arc(0,0,1,E,x,D),p.restore()}function U(p){var l=p.angle*(Math.PI/180),I=p.spread*(Math.PI/180);return{x:p.x,y:p.y,wobble:Math.random()*10,wobbleSpeed:Math.min(.11,Math.random()*.1+.05),velocity:p.startVelocity*.5+Math.random()*p.startVelocity,angle2D:-l+(.5*I-Math.random()*I),tiltAngle:(Math.random()*(.75-.25)+.25)*Math.PI,color:p.color,shape:p.shape,tick:0,totalTicks:p.ticks,decay:p.decay,drift:p.drift,random:Math.random()+2,tiltSin:0,tiltCos:0,wobbleX:0,wobbleY:0,gravity:p.gravity*3,ovalScalar:.6,scalar:p.scalar}}function fe(p,l){l.x+=Math.cos(l.angle2D)*l.velocity+l.drift,l.y+=Math.sin(l.angle2D)*l.velocity+l.gravity,l.wobble+=l.wobbleSpeed,l.velocity*=l.decay,l.tiltAngle+=.1,l.tiltSin=Math.sin(l.tiltAngle),l.tiltCos=Math.cos(l.tiltAngle),l.random=Math.random()+2,l.wobbleX=l.x+10*l.scalar*Math.cos(l.wobble),l.wobbleY=l.y+10*l.scalar*Math.sin(l.wobble);var I=l.tick++/l.totalTicks,M=l.x+l.random*l.tiltCos,T=l.y+l.random*l.tiltSin,P=l.wobbleX+l.random*l.tiltCos,E=l.wobbleY+l.random*l.tiltSin;if(p.fillStyle="rgba("+l.color.r+", "+l.color.g+", "+l.color.b+", "+(1-I)+")",p.beginPath(),l.shape==="circle")p.ellipse?p.ellipse(l.x,l.y,Math.abs(P-M)*l.ovalScalar,Math.abs(E-T)*l.ovalScalar,Math.PI/10*l.wobble,0,2*Math.PI):pe(p,l.x,l.y,Math.abs(P-M)*l.ovalScalar,Math.abs(E-T)*l.ovalScalar,Math.PI/10*l.wobble,0,2*Math.PI);else if(l.shape==="star")for(var x=Math.PI/2*3,D=4*l.scalar,te=8*l.scalar,N=l.x,Y=l.y,$=5,A=Math.PI/$;$--;)N=l.x+Math.cos(x)*te,Y=l.y+Math.sin(x)*te,p.lineTo(N,Y),x+=A,N=l.x+Math.cos(x)*D,Y=l.y+Math.sin(x)*D,p.lineTo(N,Y),x+=A;else p.moveTo(Math.floor(l.x),Math.floor(l.y)),p.lineTo(Math.floor(l.wobbleX),Math.floor(T)),p.lineTo(Math.floor(P),Math.floor(E)),p.lineTo(Math.floor(M),Math.floor(l.wobbleY));return p.closePath(),p.fill(),l.tick<l.totalTicks}function Sn(p,l,I,M,T){var P=l.slice(),E=p.getContext("2d"),x,D,te=d(function(N){function Y(){x=D=null,E.clearRect(0,0,M.width,M.height),T(),N()}function $(){n&&!(M.width===i.width&&M.height===i.height)&&(M.width=p.width=i.width,M.height=p.height=i.height),!M.width&&!M.height&&(I(p),M.width=p.width,M.height=p.height),E.clearRect(0,0,M.width,M.height),P=P.filter(function(A){return fe(E,A)}),P.length?x=m.frame($):Y()}x=m.frame($),D=Y});return{addFettis:function(N){return P=P.concat(N),te},canvas:p,promise:te,reset:function(){x&&m.cancel(x),D&&D()}}}function Wt(p,l){var I=!p,M=!!f(l||{},"resize"),T=f(l,"disableForReducedMotion",Boolean),P=a&&!!f(l||{},"useWorker"),E=P?h():null,x=I?ee:De,D=p&&E?!!p.__confetti_initialized:!1,te=typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion)").matches,N;function Y(A,ut,pt){for(var he=f(A,"particleCount",b),He=f(A,"angle",Number),Re=f(A,"spread",Number),re=f(A,"startVelocity",Number),Pn=f(A,"decay",Number),Ln=f(A,"gravity",Number),Tn=f(A,"drift",Number),Zt=f(A,"colors",L),Nn=f(A,"ticks",Number),qt=f(A,"shapes"),An=f(A,"scalar"),Xt=H(A),Jt=he,ft=[],Bn=p.width*Xt.x,Dn=p.height*Xt.y;Jt--;)ft.push(U({x:Bn,y:Dn,angle:He,spread:Re,startVelocity:re,color:Zt[Jt%Zt.length],shape:qt[y(0,qt.length)],ticks:Nn,decay:Pn,gravity:Ln,drift:Tn,scalar:An}));return N?N.addFettis(ft):(N=Sn(p,ft,x,ut,pt),N.promise)}function $(A){var ut=T||f(A,"disableForReducedMotion",Boolean),pt=f(A,"zIndex",Number);if(ut&&te)return d(function(re){re()});I&&N?p=N.canvas:I&&!p&&(p=ue(pt),document.body.appendChild(p)),M&&!D&&x(p);var he={width:p.width,height:p.height};E&&!D&&E.init(p),D=!0,E&&(p.__confetti_initialized=!0);function He(){if(E){var re={getBoundingClientRect:function(){if(!I)return p.getBoundingClientRect()}};x(re),E.postMessage({resize:{width:re.width,height:re.height}});return}he.width=he.height=null}function Re(){N=null,M&&o.removeEventListener("resize",He),I&&p&&(document.body.removeChild(p),p=null,D=!1)}return M&&o.addEventListener("resize",He,!1),E?E.fire(A,he,Re):Y(A,he,Re)}return $.reset=function(){E&&E.reset(),N&&N.reset()},$}var mt;function Ut(){return mt||(mt=Wt(null,{useWorker:!0,resize:!0})),mt}t.exports=function(){return Ut().apply(this,arguments)},t.exports.reset=function(){Ut().reset()},t.exports.create=Wt})(function(){return typeof window!="undefined"?window:typeof self!="undefined"?self:this||{}}(),Et,!1);var $o=Et.exports,$a=Et.exports.create;async function q(e){let o=await new ne(e).awaitSelection(),t;o.hasOwnProperty("icon")||(t=await new R(e).awaitSelection());let n=await new Z(o.name).awaitSelection();return{id:o.id,icon:t!=null?t:o.icon,name:n||o.name,mode:"any"}}function Q(e){var o;return(o=app.commands.commands[e])!=null?o:null}function k(n){var i=n,{icon:e,size:o}=i,t=Gt(i,["icon","size"]);let a=K(null);return kt(()=>{(0,et.setIcon)(a.current,e)},[e,o]),r("div",Ve({ref:a},t))}function j(e){let{isMobile:o,appId:t}=app;return e==="any"||e===t||e==="mobile"&&o||e==="desktop"&&!o}function Me(e){var t,n;let o="";for(let i of(t=e.hide.leftRibbon)!=null?t:[])o+=`div.side-dock-ribbon-action[aria-label="${i}"] {display: none !important; content-visibility: hidden;}`;for(let i of e.hide.statusbar)o+=`div.status-bar-item.plugin-${i} {display: none !important; content-visibility: hidden;}`;(n=document.head.querySelector("style#cmdr"))==null||n.remove(),o&&document.head.appendChild(createEl("style",{attr:{id:"cmdr"},text:o,type:"text/css"}))}async function xt({target:e}){let o=activeDocument.createElement("canvas");activeDocument.body.appendChild(o),o.style.position="fixed",o.style.width="100vw",o.style.height="100vh",o.style.top="0px",o.style.left="0px",o.style["pointer-events"]="none",o.style["z-index"]="100";let t=$o.create(o,{resize:!0,useWorker:!0}),n=e.getBoundingClientRect();await t({particleCount:et.Platform.isDesktop?160:80,startVelocity:55,spread:75,angle:90,drift:-1,ticks:250,origin:{x:(n.x+n.width/2)/activeWindow.innerWidth,y:(n.y+n.height/2)/activeWindow.innerHeight}}),o.remove()}function tt(e){activeDocument.body.style.setProperty("--cmdr-spacing",`${e}px`)}function Ee(e){let o=Object.keys(app.commands.commands).filter(n=>n.startsWith("cmdr:macro-"));for(let n of o)app.commands.removeCommand(n);let t=e.settings.macros;for(let[n,i]of Object.entries(t))e.addCommand({id:`macro-${n}`,name:i.name,callback:()=>{e.executeMacro(parseInt(n))}})}function oe(e){var n,i;let{classList:o,style:t}=document.body;t.setProperty("--at-button-height",((n=e.rowHeight)!=null?n:48)+"px"),t.setProperty("--at-button-width",((i=e.buttonWidth)!=null?i:48)+"px"),t.setProperty("--at-row-count",e.rowCount.toString()),t.setProperty("--at-spacing",e.spacing+"px"),t.setProperty("--at-offset",e.heightOffset+"px"),o.toggle("AT-multirow",e.rowCount>1),o.toggle("AT-row",!e.columnLayout),o.toggle("AT-column",e.columnLayout),o.toggle("AT-no-toolbar",e.rowCount===0)}function Yo(){let{classList:e,style:o}=document.body;o.removeProperty("--at-button-height"),o.removeProperty("--at-button-width"),o.removeProperty("--at-row-count"),o.removeProperty("--at-spacing"),o.removeProperty("--at-offset"),e.remove("AT-multirow"),e.remove("AT-row"),e.remove("AT-column"),e.remove("AT-no-toolbar"),e.remove("advanced-toolbar")}function ot(e){e.mappedIcons.forEach(o=>{let t=app.commands.commands[o.commandID];t?t.icon=o.iconID:e.mappedIcons.remove(o)})}var In=require("obsidian");var xe=require("obsidian");var Ko=require("obsidian");function Go({modal:e}){return r(S,null,r("p",null,u("Are you sure you want to delete the Command?")),r("div",{className:"modal-button-container"},r("button",{className:"mod-warning",onClick:async()=>{e.plugin.settings.confirmDeletion=!1,e.plugin.saveSettings(),e.remove=!0,e.close()}},u("Remove and don't ask again")),r("button",{className:"mod-warning",onClick:()=>{e.remove=!0,e.close()}},u("Remove")),r("button",{onClick:()=>{e.remove=!1,e.close()}},u("Cancel"))))}var V=class extends Ko.Modal{constructor(t){super(app);this.plugin=t}async onOpen(){this.titleEl.innerText=u("Remove Command"),this.containerEl.style.zIndex="99",this.reactComponent=r(Go,{modal:this}),W(this.reactComponent,this.contentEl)}async didChooseRemove(){return this.open(),new Promise(t=>{this.onClose=()=>{var n;return t((n=this.remove)!=null?n:!1)}})}onClose(){W(null,this.contentEl)}};var X=class{constructor(o,t){this.plugin=o,this.pairs=t}};var ve=class extends X{constructor(t,n){super(t,n);this.actions=new Map;this.init(),this.plugin.register(()=>this.actions.forEach((i,a)=>this.removeAction(a)))}getFileExplorers(){return app.workspace.getLeavesOfType("file-explorer")}init(){app.workspace.onLayoutReady(()=>{for(let t of this.pairs)j(t.mode)&&(app.workspace.onLayoutReady(()=>{this.getFileExplorers().forEach(i=>{this.addAction(t,i)})}),this.plugin.registerEvent(app.workspace.on("layout-change",()=>{this.getFileExplorers().forEach(i=>{this.addAction(t,i)})})))})}reorder(){this.actions.forEach((t,n)=>this.removeAction(n,!0)),this.init()}async addCommand(t){this.pairs.push(t),app.workspace.onLayoutReady(()=>{this.getFileExplorers().forEach(i=>{this.addAction(t,i)})}),this.plugin.registerEvent(app.workspace.on("layout-change",()=>{this.getFileExplorers().forEach(i=>{this.addAction(t,i)})})),await this.plugin.saveSettings()}async removeCommand(t){this.pairs.remove(t),this.removeAction(t),await this.plugin.saveSettings()}buttonExists(t,n){return[...t.view.containerEl.querySelectorAll("div.nav-buttons-container > .cmdr.clickable-icon")].some(i=>i.getAttribute("data-cmdr")===n.icon+n.name)}addAction(t,n){var m,h,s,g,_;if(this.buttonExists(n,t))return;let i=createDiv({cls:"cmdr clickable-icon",attr:{"aria-label-position":"top","aria-label":t.name,"data-cmdr":t.icon+t.name}});this.actions.set(t,i),i.style.color=t.color==="#000000"||t.color===void 0?"inherit":t.color;let a=!1,c=()=>{i.empty(),(0,xe.setIcon)(i,t.icon),i.onclick=()=>app.commands.executeCommandById(t.id)},d=()=>{i.empty(),(0,xe.setIcon)(i,"trash"),i.onclick=async()=>{(!this.plugin.settings.confirmDeletion||await new V(this.plugin).didChooseRemove())&&this.removeCommand(t)}};i.addEventListener("mouseleave",()=>{c(),a=!1}),i.addEventListener("mousemove",f=>{f.preventDefault(),f.stopImmediatePropagation(),f.shiftKey&&(a||d(),a=!0)}),i.addEventListener("contextmenu",f=>{f.stopImmediatePropagation(),new xe.Menu().addItem(b=>{b.setTitle(u("Add command")).setIcon("command").onClick(async()=>{let y=await q(this.plugin);this.addCommand(y)})}).addSeparator().addItem(b=>{b.setTitle(u("Change Icon")).setIcon("box").onClick(async()=>{let y=await new R(this.plugin).awaitSelection();y&&y!==t.icon&&(t.icon=y,await this.plugin.saveSettings(),this.reorder())})}).addItem(b=>{b.setTitle(u("Rename")).setIcon("text-cursor-input").onClick(async()=>{let y=await new Z(t.name).awaitSelection();y&&y!==t.name&&(t.name=y,await this.plugin.saveSettings(),this.reorder())})}).addItem(b=>{b.dom.addClass("is-warning"),b.setTitle(u("Delete")).setIcon("lucide-trash").onClick(async()=>{(!this.plugin.settings.confirmDeletion||await new V(this.plugin).didChooseRemove())&&this.removeCommand(t)})}).showAtMouseEvent(f)}),c(),(_=(g=(s=(h=(m=n.view)==null?void 0:m.containerEl)==null?void 0:h.querySelector)==null?void 0:s.call(h,"div.nav-buttons-container"))==null?void 0:g.appendChild)==null||_.call(g,i)}removeAction(t,n=!1){let i=this.actions.get(t);if(!!i){if(n){i.remove(),this.actions.delete(t);return}i.addClass("cmdr-ribbon-removing"),i.addEventListener("transitionend",async()=>{i.remove(),this.actions.delete(t)})}}};var se=require("obsidian");var nt=class extends X{async addCommand(o){this.pairs.push(o),await this.plugin.saveSettings()}async removeCommand(o){this.pairs.remove(o),await this.plugin.saveSettings()}reorder(){}addRemovableCommand(o,t,n,i,a){return c=>{var f;c.dom.addClass("cmdr"),c.dom.style.color=t.color==="#000000"||t.color===void 0?"inherit":t.color,c.setSection("cmdr"),c.dom.style.display="flex";let d=createDiv({cls:"cmdr-menu-more-options"}),m=null;d.addEventListener("click",b=>{b.preventDefault(),b.stopImmediatePropagation(),m?(m.hide(),m=null):m=new se.Menu().addItem(y=>{y.setTitle(u("Change Icon")).setIcon("box").onClick(async()=>{let v=await new R(n).awaitSelection();v&&v!==t.icon&&(t.icon=v,await n.saveSettings())})}).addItem(y=>{y.setTitle(u("Rename")).setIcon("text-cursor-input").onClick(async()=>{let v=await new Z(t.name).awaitSelection();v&&v!==t.name&&(t.name=v,await n.saveSettings())})}).addItem(y=>{y.dom.addClass("is-warning"),y.setTitle(u("Delete")).setIcon("lucide-trash").onClick(async()=>{(!n.settings.confirmDeletion||await new V(n).didChooseRemove())&&_()})}).showAtMouseEvent(b)}),(0,se.setIcon)(d,"more-vertical"),c.dom.append(d),c.setTitle((f=t.name)!=null?f:o.name).setIcon(t.icon).onClick(()=>app.commands.executeCommandById(t.id));let h=!1,s=()=>{d.style.display="none"},g=()=>{d.style.display="block"},_=async()=>{c.dom.addClass("cmdr-removing"),i.registerDomEvent(c.dom,"transitionend",()=>{c.dom.remove()}),a.remove(t),await n.saveSettings()};i.registerDomEvent(c.dom,"mousemove",b=>{b.preventDefault(),b.stopImmediatePropagation(),h||g(),h=!0}),i.registerDomEvent(c.dom,"mouseleave",()=>{s(),h=!1}),s()}}addCommandAddButton(o,t,n){o.settings.showAddCommand&&t.addItem(i=>{i.setTitle(u("Add command")).setIcon("plus-circle").setSection("cmdr").onClick(async()=>{try{let a=await q(o);n.push(a),await o.saveSettings()}catch(a){console.log(a)}})})}},Ie=class extends nt{applyEditorMenuCommands(o){return async(t,n,i)=>{this.addCommandAddButton(o,t,o.settings.editorMenu);for(let a of o.settings.editorMenu){let c=Q(a.id);!c||!j(a.mode)||c.checkCallback&&!c.checkCallback(!0)||c.editorCheckCallback&&!c.editorCheckCallback(!0,n,i)||t.addItem(this.addRemovableCommand.call(this,c,a,o,t,o.settings.editorMenu))}}}},Se=class extends nt{applyFileMenuCommands(o){return async(t,n,i,a)=>{this.addCommandAddButton(o,t,o.settings.fileMenu);for(let c of o.settings.fileMenu){let d=Q(c.id);if(!!d&&!(d.checkCallback&&!d.checkCallback(!0))){if(d.editorCallback){if(!((a==null?void 0:a.view)instanceof se.MarkdownView))continue}else if(d.editorCheckCallback)if((a==null?void 0:a.view)instanceof se.MarkdownView){if(!d.editorCheckCallback(!0,a.view.editor,a.view))continue}else continue;t.addItem(this.addRemovableCommand.call(this,d,c,o,t,o.settings.fileMenu))}}}}};var ce=require("obsidian");var ge=class extends X{constructor(t,n){super(t,n);this.buttons=new WeakMap;this.init()}addPageHeaderButton(t,n){let{id:i,icon:a,name:c}=n,{view:d}=t;if(!(d instanceof ce.ItemView))return;let m=this.buttonsFor(t,!0);if(!m||m.has(i))return;let h=d.addAction(a,c,()=>{app.workspace.setActiveLeaf(t,{focus:!0}),app.commands.executeCommandById(i)});m.set(i,h),h.addClasses(["cmdr-page-header",i]),h.style.color=n.color==="#000000"||n.color===void 0?"inherit":n.color,h.addEventListener("contextmenu",s=>{s.stopImmediatePropagation(),new ce.Menu().addItem(g=>{g.setTitle(u("Add command")).setIcon("command").onClick(async()=>{let _=await q(this.plugin);this.addCommand(_)})}).addSeparator().addItem(g=>{g.setTitle(u("Change Icon")).setIcon("box").onClick(async()=>{let _=await new R(this.plugin).awaitSelection();_&&_!==n.icon&&(n.icon=_,await this.plugin.saveSettings(),this.reorder())})}).addItem(g=>{g.setTitle(u("Rename")).setIcon("text-cursor-input").onClick(async()=>{let _=await new Z(n.name).awaitSelection();_&&_!==n.name&&(n.name=_,await this.plugin.saveSettings(),this.reorder())})}).addItem(g=>{g.dom.addClass("is-warning"),g.setTitle(u("Delete")).setIcon("lucide-trash").onClick(async()=>{(!this.plugin.settings.confirmDeletion||await new V(this.plugin).didChooseRemove())&&this.removeCommand(n)})}).showAtMouseEvent(s)})}init(){this.plugin.register(()=>{this.removeButtonsFromAllLeaves()}),this.plugin.registerEvent(app.workspace.on("layout-change",()=>{this.addButtonsToAllLeaves()})),app.workspace.onLayoutReady(()=>setTimeout(()=>this.addButtonsToAllLeaves(),100))}addAdderButton(t){var c;let{view:n}=t,i="cmdr-adder";if(!(n instanceof ce.ItemView)||(c=this.buttons.get(n))!=null&&c.has(i))return;let a=n.addAction("plus",u("Add new"),async()=>{this.addCommand(await q(this.plugin))});a.addClasses(["cmdr",i]),this.buttons.has(n)||this.buttons.set(n,new Map),this.buttons.get(n).set(i,a)}addButtonsToAllLeaves(t=!1){activeWindow.requestAnimationFrame(()=>app.workspace.iterateAllLeaves(n=>this.addButtonsToLeaf(n,t)))}removeButtonsFromAllLeaves(){activeWindow.requestAnimationFrame(()=>app.workspace.iterateAllLeaves(t=>this.removeButtonsFromLeaf(t)))}buttonsFor(t,n=!1){if(t.view instanceof ce.ItemView)return n&&!this.buttons.has(t.view)&&this.buttons.set(t.view,new Map),this.buttons.get(t.view)}addButtonsToLeaf(t,n=!1){var i;if(t.view instanceof ce.ItemView){if(n)this.removeButtonsFromLeaf(t);else if((i=this.buttonsFor(t))!=null&&i.size)return;for(let a=this.pairs.length-1;a>=0;a--){let c=this.pairs[a];j(c.mode)&&this.addPageHeaderButton(t,c)}this.plugin.settings.showAddCommand&&this.addAdderButton(t)}}removeButtonsFromLeaf(t){let n=this.buttonsFor(t);if(n){for(let i of n.values())i.detach();n==null||n.clear()}}reorder(){this.addButtonsToAllLeaves(!0)}async addCommand(t){this.pairs.push(t),this.addButtonsToAllLeaves(!0),await this.plugin.saveSettings()}async removeCommand(t){this.pairs.remove(t),this.addButtonsToAllLeaves(!0),await this.plugin.saveSettings()}};var le=require("obsidian");var be=class extends X{constructor(t,n){super(t,n);this.actions=new Map;this.addBtn=createDiv({cls:"cmdr status-bar-item cmdr-adder",attr:{"aria-label-position":"top","aria-label":u("Add new")}});this.init(),this.plugin.register(()=>this.actions.forEach((i,a)=>this.removeAction(a)))}init(){app.workspace.onLayoutReady(()=>{this.container=app.statusBar.containerEl;for(let t of this.pairs)Q(t.id)||this.pairs.remove(t),j(t.mode)&&this.addAction(t);this.plugin.saveSettings(),this.plugin.registerDomEvent(this.container,"contextmenu",t=>{t.target===this.container&&new le.Menu().addItem(n=>{n.setTitle(u("Add command")).setIcon("command").onClick(async()=>{let i=await q(this.plugin);this.addCommand(i)})}).showAtMouseEvent(t)}),this.plugin.register(()=>this.addBtn.remove()),(0,le.setIcon)(this.addBtn,"plus"),this.addBtn.onclick=async()=>{let t=await q(this.plugin);this.addCommand(t),this.reorder()},this.plugin.settings.showAddCommand&&this.container.prepend(this.addBtn)})}reorder(){this.addBtn.remove(),this.actions.forEach((t,n)=>this.removeAction(n,!0)),this.init()}async addCommand(t){this.pairs.push(t),this.addAction(t),await this.plugin.saveSettings()}async removeCommand(t){this.pairs.remove(t),this.removeAction(t),await this.plugin.saveSettings()}addAction(t){let n=createDiv({cls:"cmdr status-bar-item clickable-icon",attr:{"aria-label-position":"top","aria-label":t.name}});this.actions.set(t,n),n.style.color=t.color==="#000000"||t.color===void 0?"inherit":t.color;let i=!1,a=()=>{n.empty(),(0,le.setIcon)(n,t.icon),n.onclick=()=>app.commands.executeCommandById(t.id)},c=()=>{n.empty(),(0,le.setIcon)(n,"trash"),n.onclick=async()=>{(!this.plugin.settings.confirmDeletion||await new V(this.plugin).didChooseRemove())&&this.removeCommand(t)}};n.addEventListener("mouseleave",()=>{a(),i=!1}),n.addEventListener("mousemove",d=>{d.preventDefault(),d.stopImmediatePropagation(),d.shiftKey&&(i||c(),i=!0)}),n.addEventListener("contextmenu",d=>{d.stopImmediatePropagation(),new le.Menu().addItem(m=>{m.setTitle(u("Add command")).setIcon("command").onClick(async()=>{let h=await q(this.plugin);this.addCommand(h)})}).addSeparator().addItem(m=>{m.setTitle(u("Change Icon")).setIcon("box").onClick(async()=>{let h=await new R(this.plugin).awaitSelection();h&&h!==t.icon&&(t.icon=h,await this.plugin.saveSettings(),this.reorder())})}).addItem(m=>{m.setTitle(u("Rename")).setIcon("text-cursor-input").onClick(async()=>{let h=await new Z(t.name).awaitSelection();h&&h!==t.name&&(t.name=h,await this.plugin.saveSettings(),this.reorder())})}).addItem(m=>{m.dom.addClass("is-warning"),m.setTitle(u("Delete")).setIcon("lucide-trash").onClick(async()=>{(!this.plugin.settings.confirmDeletion||await new V(this.plugin).didChooseRemove())&&this.removeCommand(t)})}).showAtMouseEvent(d)}),a(),this.container.prepend(n)}removeAction(t,n=!1){let i=this.actions.get(t);if(!!i){if(n){i.remove(),this.actions.delete(t);return}i.addClass("cmdr-ribbon-removing"),i.addEventListener("transitionend",async()=>{i.remove(),this.actions.delete(t)})}}};var Qo=require("obsidian");var ct=require("obsidian");var J=require("obsidian");var Lt=require("obsidian");var en=["https://github.com/jsmorabito","https://github.com/phibr0","https://www.youtube.com/watch?v=dQw4w9WgXcQ"];function It(){let[e,o]=B(0);return r("div",{className:"cmdr-credits"},r("span",{onClick:()=>{o(t=>t+1),location.replace(en[e%en.length])}},u("By Johnny\u2728 and phibr0")))}function yi(e,o){for(var t in o)e[t]=o[t];return e}function Pt(e,o){for(var t in e)if(t!=="__source"&&!(t in o))return!0;for(var n in o)if(n!=="__source"&&e[n]!==o[n])return!0;return!1}function tn(e){this.props=e}function dn(e,o){function t(i){var a=this.props.ref,c=a==i.ref;return!c&&a&&(a.call?a(null):a.current=null),o?!o(this.props,i)||!c:Pt(this.props,i)}function n(i){return this.shouldComponentUpdate=t,r(e,i)}return n.displayName="Memo("+(e.displayName||e.name)+")",n.prototype.isReactComponent=!0,n.__f=!0,n}(tn.prototype=new G).isPureReactComponent=!0,tn.prototype.shouldComponentUpdate=function(e,o){return Pt(this.props,e)||Pt(this.state,o)};var on=C.__b;C.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),on&&on(e)};var xs=typeof Symbol!="undefined"&&Symbol.for&&Symbol.for("react.forward_ref")||3911;var wi=C.__e;C.__e=function(e,o,t,n){if(e.then){for(var i,a=o;a=a.__;)if((i=a.__c)&&i.__c)return o.__e==null&&(o.__e=t.__e,o.__k=t.__k),i.__c(e,o)}wi(e,o,t,n)};var nn=C.unmount;function mn(e,o,t){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=yi({},e)).__c!=null&&(e.__c.__P===t&&(e.__c.__P=o),e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return mn(n,o,t)})),e}function un(e,o,t){return e&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return un(n,o,t)}),e.__c&&e.__c.__P===o&&(e.__e&&t.insertBefore(e.__e,e.__d),e.__c.__e=!0,e.__c.__P=t)),e}function St(){this.__u=0,this.t=null,this.__b=null}function pn(e){var o=e.__.__c;return o&&o.__a&&o.__a(e)}function it(){this.u=null,this.o=null}C.unmount=function(e){var o=e.__c;o&&o.__R&&o.__R(),o&&e.__h===!0&&(e.type=null),nn&&nn(e)},(St.prototype=new G).__c=function(e,o){var t=o.__c,n=this;n.t==null&&(n.t=[]),n.t.push(t);var i=pn(n.__v),a=!1,c=function(){a||(a=!0,t.__R=null,i?i(d):d())};t.__R=c;var d=function(){if(!--n.__u){if(n.state.__a){var h=n.state.__a;n.__v.__k[0]=un(h,h.__c.__P,h.__c.__O)}var s;for(n.setState({__a:n.__b=null});s=n.t.pop();)s.forceUpdate()}},m=o.__h===!0;n.__u++||m||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(c,c)},St.prototype.componentWillUnmount=function(){this.t=[]},St.prototype.render=function(e,o){if(this.__b){if(this.__v.__k){var t=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=mn(this.__b,t,n.__O=n.__P)}this.__b=null}var i=o.__a&&r(S,null,e.fallback);return i&&(i.__h=null),[r(S,null,o.__a?null:e.children),i]};var an=function(e,o,t){if(++t[1]===t[0]&&e.o.delete(o),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.o.size))for(t=e.u;t;){for(;t.length>3;)t.pop()();if(t[1]<t[0])break;e.u=t=t[2]}};(it.prototype=new G).__a=function(e){var o=this,t=pn(o.__v),n=o.o.get(e);return n[0]++,function(i){var a=function(){o.props.revealOrder?(n.push(i),an(o,e,n)):i()};t?t(a):a()}},it.prototype.render=function(e){this.u=null,this.o=new Map;var o=we(e.children);e.revealOrder&&e.revealOrder[0]==="b"&&o.reverse();for(var t=o.length;t--;)this.o.set(o[t],this.u=[1,0,this.u]);return e.children},it.prototype.componentDidUpdate=it.prototype.componentDidMount=function(){var e=this;this.o.forEach(function(o,t){an(e,t,o)})};var ki=typeof Symbol!="undefined"&&Symbol.for&&Symbol.for("react.element")||60103,Mi=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,Ei=typeof document!="undefined",xi=function(e){return(typeof Symbol!="undefined"&&typeof Symbol()=="symbol"?/fil|che|rad/i:/fil|che|ra/i).test(e)};G.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(e){Object.defineProperty(G.prototype,e,{configurable:!0,get:function(){return this["UNSAFE_"+e]},set:function(o){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:o})}})});var rn=C.event;function Ii(){}function Si(){return this.cancelBubble}function Pi(){return this.defaultPrevented}C.event=function(e){return rn&&(e=rn(e)),e.persist=Ii,e.isPropagationStopped=Si,e.isDefaultPrevented=Pi,e.nativeEvent=e};var Li,sn={configurable:!0,get:function(){return this.class}},cn=C.vnode;C.vnode=function(e){var o=e.type,t=e.props,n=t;if(typeof o=="string"){var i=o.indexOf("-")===-1;for(var a in n={},t){var c=t[a];Ei&&a==="children"&&o==="noscript"||a==="value"&&"defaultValue"in t&&c==null||(a==="defaultValue"&&"value"in t&&t.value==null?a="value":a==="download"&&c===!0?c="":/ondoubleclick/i.test(a)?a="ondblclick":/^onchange(textarea|input)/i.test(a+o)&&!xi(t.type)?a="oninput":/^onfocus$/i.test(a)?a="onfocusin":/^onblur$/i.test(a)?a="onfocusout":/^on(Ani|Tra|Tou|BeforeInp|Compo)/.test(a)?a=a.toLowerCase():i&&Mi.test(a)?a=a.replace(/[A-Z0-9]/g,"-$&").toLowerCase():c===null&&(c=void 0),/^oninput$/i.test(a)&&(a=a.toLowerCase(),n[a]&&(a="oninputCapture")),n[a]=c)}o=="select"&&n.multiple&&Array.isArray(n.value)&&(n.value=we(t.children).forEach(function(d){d.props.selected=n.value.indexOf(d.props.value)!=-1})),o=="select"&&n.defaultValue!=null&&(n.value=we(t.children).forEach(function(d){d.props.selected=n.multiple?n.defaultValue.indexOf(d.props.value)!=-1:n.defaultValue==d.props.value})),e.props=n,t.class!=t.className&&(sn.enumerable="className"in t,t.className!=null&&(n.class=t.className),Object.defineProperty(n,"className",sn))}e.$$typeof=ki,cn&&cn(e)};var ln=C.__r;C.__r=function(e){ln&&ln(e),Li=e.__c};var fn='<svg viewbox="0 0 118 105" width="118" xmlns="http://www.w3.org/2000/svg" height="105" style="-webkit-print-color-adjust:exact" fill="none"><defs><clipPath id="a" class="frame-clip"><rect rx="0" ry="0" width="118" height="105"/></clipPath></defs><g clip-path="url(#a)"><rect rx="0" ry="0" width="118" height="105" class="frame-background"/><g class="frame-children"><g class="any-key" style="fill:#000"><path d="M35.2 1C25.7 1 18 8.7 18 18.2v51.6C18 79.3 25.7 87 35.2 87h45.6C90.3 87 98 79.3 98 69.8V18.2C98 8.7 90.3 1 80.8 1H35.2Zm0 4h45.6C88.1 5 94 10.9 94 18.2v37.6C94 63.1 88.1 69 80.8 69H35.2C27.9 69 22 63.1 22 55.8V18.2C22 10.9 27.9 5 35.2 5ZM56 19v14.5L43.6 26l-2.1 3.4L54.1 37l-12.6 7.6 2.1 3.4L56 40.5V55h4V40.5L72.4 48l2.1-3.4L61.9 37l12.6-7.6-2.1-3.4L60 33.5V19h-4ZM22 66.8c3.2 3.8 7.9 6.2 13.2 6.2h45.6c5.3 0 10-2.4 13.2-6.2v3C94 77.1 88.1 83 80.8 83H35.2C27.9 83 22 77.1 22 69.8v-3Z" style="fill:var(--text-accent);fill-opacity:1"/><path d="M35.2 1C25.7 1 18 8.7 18 18.2v51.6C18 79.3 25.7 87 35.2 87h45.6C90.3 87 98 79.3 98 69.8V18.2C98 8.7 90.3 1 80.8 1H35.2Zm0 4h45.6C88.1 5 94 10.9 94 18.2v37.6C94 63.1 88.1 69 80.8 69H35.2C27.9 69 22 63.1 22 55.8V18.2C22 10.9 27.9 5 35.2 5ZM56 19v14.5L43.6 26l-2.1 3.4L54.1 37l-12.6 7.6 2.1 3.4L56 40.5V55h4V40.5L72.4 48l2.1-3.4L61.9 37l12.6-7.6-2.1-3.4L60 33.5V19h-4ZM22 66.8c3.2 3.8 7.9 6.2 13.2 6.2h45.6c5.3 0 10-2.4 13.2-6.2v3C94 77.1 88.1 83 80.8 83H35.2C27.9 83 22 77.1 22 69.8v-3Z" style="fill:none;stroke-width:1;stroke:var(--text-accent);stroke-opacity:1" class="stroke-shape"/></g><path d="M20.11 53.587 3 63.9"/><g class="stroke-shape"><defs><marker refX="3" fill-opacity="1" orient="auto-start-reverse" id="b" viewBox="0 0 6 6" fill="var(--text-accent)" refY="3" markerWidth="6" markerHeight="6"><path d="M3 2.5a.5.5 0 0 1 0 1"/></marker></defs><path d="M20.11 53.587 3 63.9" style="fill:none;stroke-width:5;stroke:var(--text-accent);stroke-opacity:1;marker-end:url(#b)"/></g><path d="m96 53.567 19.161-11.55"/><g class="stroke-shape"><defs><marker refX="3" fill-opacity="1" orient="auto-start-reverse" id="c" viewBox="0 0 6 6" fill="var(--text-accent)" refY="3" markerWidth="6" markerHeight="6"><path d="M3 2.5a.5.5 0 0 1 0 1"/></marker></defs><path d="m96 53.567 19.161-11.55" style="fill:none;stroke-width:5;stroke:var(--text-accent);stroke-opacity:1;marker-end:url(#c)"/></g><path d="M20.11 53.587 3 63.9"/><g class="stroke-shape"><defs><marker refX="3" fill-opacity="1" orient="auto-start-reverse" id="d" viewBox="0 0 6 6" fill="var(--text-accent)" refY="3" markerWidth="6" markerHeight="6"><path d="M3 2.5a.5.5 0 0 1 0 1"/></marker></defs><path d="M20.11 53.587 3 63.9" style="fill:none;stroke-width:5;stroke:var(--text-accent);stroke-opacity:1;marker-end:url(#d)"/></g><path d="M42 84v18h12"/><g class="stroke-shape"><defs><marker refX="3" fill-opacity="1" orient="auto-start-reverse" id="e" viewBox="0 0 6 6" fill="var(--text-accent)" refY="3" markerWidth="6" markerHeight="6"><path d="M3 2.5a.5.5 0 0 1 0 1"/></marker></defs><path d="M42 84v18h12" style="fill:none;stroke-width:5;stroke:var(--text-accent);stroke-opacity:1;marker-end:url(#e)"/></g><path d="M74 84v18h12"/><g class="stroke-shape"><defs><marker refX="3" fill-opacity="1" orient="auto-start-reverse" id="f" viewBox="0 0 6 6" fill="var(--text-accent)" refY="3" markerWidth="6" markerHeight="6"><path d="M3 2.5a.5.5 0 0 1 0 1"/></marker></defs><path d="M74 84v18h12" style="fill:none;stroke-width:5;stroke:var(--text-accent);stroke-opacity:1;marker-end:url(#f)"/></g><path d="m96 53.567 19.161-11.55"/><g class="stroke-shape"><defs><marker refX="3" fill-opacity="1" orient="auto-start-reverse" id="g" viewBox="0 0 6 6" fill="var(--text-accent)" refY="3" markerWidth="6" markerHeight="6"><path d="M3 2.5a.5.5 0 0 1 0 1"/></marker></defs><path d="m96 53.567 19.161-11.55" style="fill:none;stroke-width:5;stroke:var(--text-accent);stroke-opacity:1;marker-end:url(#g)"/></g><path d="M42 84v18h12"/><g class="stroke-shape"><defs><marker refX="3" fill-opacity="1" orient="auto-start-reverse" id="h" viewBox="0 0 6 6" fill="var(--text-accent)" refY="3" markerWidth="6" markerHeight="6"><path d="M3 2.5a.5.5 0 0 1 0 1"/></marker></defs><path d="M42 84v18h12" style="fill:none;stroke-width:5;stroke:var(--text-accent);stroke-opacity:1;marker-end:url(#h)"/></g><path d="M74 84v18h12"/><g class="stroke-shape"><defs><marker refX="3" fill-opacity="1" orient="auto-start-reverse" id="i" viewBox="0 0 6 6" fill="var(--text-accent)" refY="3" markerWidth="6" markerHeight="6"><path d="M3 2.5a.5.5 0 0 1 0 1"/></marker></defs><path d="M74 84v18h12" style="fill:none;stroke-width:5;stroke:var(--text-accent);stroke-opacity:1;marker-end:url(#i)"/></g></g></g></svg>';var hn='<svg width="124" height="189" viewBox="0 0 124 189" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M120 175.999L104.432 133.999C74.7524 140.282 54.9222 146.129 23.5771 137.861C16.3206 150.681 13.2565 163.179 6 175.999C11.7765 173.272 14.6163 173.349 19.0573 175.999C25.2389 172.439 27.3603 173.689 31.1101 175.999C39.3142 169.983 43.4376 171.766 50.696 175.999C57.2083 171.119 60.7022 171.597 66.7665 175.999C76.3874 170.399 80.6872 172.41 88.3505 175.994L88.3612 175.999C94.0886 172.481 97.1438 172.819 102.423 175.999C109.021 172.023 112.937 173.03 120 175.999Z" fill="#A80000" stroke="#A80000" stroke-width="4" /><path d="M37.156 80.2386L85.6308 78.676L53.8425 8.1636L37.156 80.2386Z" fill="#B50D0D" stroke="#B50D0D" stroke-width="4" /><ellipse cx="85" cy="173.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="93" cy="175.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="101" cy="173.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="112" cy="175.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="116" cy="177.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="76" cy="175.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="67" cy="173.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="58" cy="173.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="49" cy="175.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="42" cy="173.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="37" cy="175.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="31" cy="174.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="29" cy="173.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="25" cy="175.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="20" cy="173.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="14" cy="175.499" rx="8" ry="6.5" fill="#E9E9E9" /><ellipse cx="8" cy="177.499" rx="8" ry="6.5" fill="#E9E9E9" /><path d="M47 166.999V183.999H59" stroke="#28CC39" stroke-width="5" stroke-linecap="round" /><path d="M79 166.999V183.999H91" stroke="#28CC39" stroke-width="5" stroke-linecap="round" /><path d="M40.2 82.9993C30.7 82.9993 23 90.6993 23 100.199V151.799C23 161.299 30.7 168.999 40.2 168.999H85.8C95.3 168.999 103 161.299 103 151.799V100.199C103 90.6993 95.3 82.9993 85.8 82.9993H40.2ZM40.2 86.9993H85.8C93.1 86.9993 99 92.8993 99 100.199V137.799C99 145.099 93.1 150.999 85.8 150.999H40.2C32.9 150.999 27 145.099 27 137.799V100.199C27 92.8993 32.9 86.9993 40.2 86.9993ZM61 100.999V115.499L48.6 107.999L46.5 111.399L59.1 118.999L46.5 126.599L48.6 129.999L61 122.499V136.999H65V122.499L77.4 129.999L79.5 126.599L66.9 118.999L79.5 111.399L77.4 107.999L65 115.499V100.999H61ZM27 148.799C30.2 152.599 34.9 154.999 40.2 154.999H85.8C91.1 154.999 95.8 152.599 99 148.799V151.799C99 159.099 93.1 164.999 85.8 164.999H40.2C32.9 164.999 27 159.099 27 151.799V148.799Z" fill="#28CC39" stroke="#28CC39" /><path d="M25 135.999L7.99997 146.603" stroke="#28CC39" stroke-width="5" stroke-linecap="round" /><path d="M120.104 123.488L101 135.614" stroke="#28CC39" stroke-width="5" stroke-linecap="round" /><path d="M84.5 177.999V173.499H75.5V177.999H84.5ZM80 183.999H75.5V188.499H80V183.999ZM90 188.499C92.4853 188.499 94.5 186.485 94.5 183.999C94.5 181.514 92.4853 179.499 90 179.499V188.499ZM75.5 177.999V183.999H84.5V177.999H75.5ZM80 188.499H90V179.499H80V188.499Z" fill="#303030" /><path d="M52.5 177.999V173.499H43.5V177.999H52.5ZM48 183.999H43.5V188.499H48V183.999ZM58 188.499C60.4853 188.499 62.5 186.485 62.5 183.999C62.5 181.514 60.4853 179.499 58 179.499V188.499ZM43.5 177.999V183.999H52.5V177.999H43.5ZM48 188.499H58V179.499H48V188.499Z" fill="#303030" /><ellipse cx="38.1555" cy="80.2062" rx="8" ry="6.5" transform="rotate(-1.84634 38.1555 80.2062)" fill="#E9E9E9" /><ellipse cx="43.0885" cy="78.046" rx="8" ry="6.5" transform="rotate(-1.84634 43.0885 78.046)" fill="#E9E9E9" /><ellipse cx="46.1513" cy="79.9483" rx="8" ry="6.5" transform="rotate(-1.84634 46.1513 79.9483)" fill="#E9E9E9" /><ellipse cx="54.0827" cy="77.692" rx="8" ry="6.5" transform="rotate(-1.84634 54.0827 77.692)" fill="#E9E9E9" /><ellipse cx="59.1445" cy="79.5299" rx="8" ry="6.5" transform="rotate(-1.84634 59.1445 79.5299)" fill="#E9E9E9" /><ellipse cx="67.0759" cy="77.2731" rx="8" ry="6.5" transform="rotate(-1.84634 67.0759 77.2731)" fill="#E9E9E9" /><ellipse cx="70.1389" cy="79.1754" rx="8" ry="6.5" transform="rotate(-1.84634 70.1389 79.1754)" fill="#E9E9E9" /><ellipse cx="80.0692" cy="76.8541" rx="8" ry="6.5" transform="rotate(-1.84634 80.0692 76.8541)" fill="#E9E9E9" /><ellipse cx="83.1321" cy="78.7565" rx="8" ry="6.5" transform="rotate(-1.84634 83.1321 78.7565)" fill="#E9E9E9" /><ellipse cx="53.8585" cy="7.66343" rx="8" ry="6.5" transform="rotate(-1.84634 53.8585 7.66343)" fill="#E9E9E9" /><path d="M104.5 127.999C75.5109 146.65 55.8196 154.503 21.5 133.999" stroke="#750000" stroke-width="4" /><path d="M68.2248 148.783C69.0243 149.525 69.5328 150.357 69.7415 151.062C69.9573 151.791 69.8141 152.195 69.6516 152.37C69.4892 152.545 69.0976 152.718 68.3543 152.557C67.6357 152.402 66.7679 151.957 65.9684 151.215C65.1688 150.473 64.6603 149.641 64.4517 148.936C64.2359 148.207 64.379 147.803 64.5415 147.628C64.7039 147.453 65.0955 147.28 65.8389 147.441C66.5574 147.596 67.4252 148.041 68.2248 148.783Z" stroke="#750000" stroke-width="2" /><path d="M62.5372 151.611C61.7935 152.57 60.9314 153.229 60.1818 153.547C59.398 153.88 58.9595 153.766 58.7766 153.624C58.5937 153.482 58.3744 153.086 58.5013 152.244C58.6227 151.439 59.0467 150.44 59.7903 149.481C60.534 148.522 61.3961 147.863 62.1457 147.545C62.9296 147.212 63.3681 147.326 63.551 147.468C63.7339 147.61 63.9532 148.006 63.8262 148.848C63.7048 149.653 63.2809 150.652 62.5372 151.611Z" stroke="#750000" stroke-width="2" /></svg>';var vn='<svg width="152" height="220" viewBox="0 0 127 184" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.82568 174.501L23.3257 131.001C52.8749 137.508 72.6181 143.564 103.826 135.001C111.05 148.279 114.101 161.223 121.326 174.501C115.575 171.676 112.747 171.756 108.326 174.501C102.171 170.814 100.059 172.108 96.3257 174.501C88.1576 168.27 84.0522 170.116 76.8257 174.501C70.342 169.446 66.8634 169.941 60.8257 174.501C51.247 168.701 46.9661 170.784 39.3364 174.496L39.3257 174.501C33.6234 170.857 30.5816 171.207 25.3257 174.501C18.7562 170.383 14.8574 171.426 7.82568 174.501Z" fill="#8B6CEF" stroke="#8B6CEF" stroke-width="4" /><path d="M80.3257 164.501V181.501H68.3257" stroke="#FF820F" stroke-width="5" stroke-linecap="round" /><path d="M48.3257 164.501V181.501H36.3257" stroke="#FF820F" stroke-width="5" stroke-linecap="round" /><path d="M87.1257 80.501C96.6257 80.501 104.326 88.201 104.326 97.701V149.301C104.326 158.801 96.6257 166.501 87.1257 166.501H41.5257C32.0257 166.501 24.3257 158.801 24.3257 149.301V97.701C24.3257 88.201 32.0257 80.501 41.5257 80.501H87.1257ZM87.1257 84.501H41.5257C34.2257 84.501 28.3257 90.401 28.3257 97.701V135.301C28.3257 142.601 34.2257 148.501 41.5257 148.501H87.1257C94.4257 148.501 100.326 142.601 100.326 135.301V97.701C100.326 90.401 94.4257 84.501 87.1257 84.501ZM66.3257 98.501V113.001L78.7257 105.501L80.8257 108.901L68.2257 116.501L80.8257 124.101L78.7257 127.501L66.3257 120.001V134.501H62.3257V120.001L49.9257 127.501L47.8257 124.101L60.4257 116.501L47.8257 108.901L49.9257 105.501L62.3257 113.001V98.501H66.3257ZM100.326 146.301C97.1257 150.101 92.4257 152.501 87.1257 152.501H41.5257C36.2257 152.501 31.5257 150.101 28.3257 146.301V149.301C28.3257 156.601 34.2257 162.501 41.5257 162.501H87.1257C94.4257 162.501 100.326 156.601 100.326 149.301V146.301Z" fill="#FF820F" stroke="#FF820F" /><path d="M102.326 133.501L119.326 144.105" stroke="#FF820F" stroke-width="5" stroke-linecap="round" /><path d="M7.22161 120.99L26.3257 133.116" stroke="#FF820F" stroke-width="5" stroke-linecap="round" /><path d="M9.32568 136.501L3.32568 105.501" stroke="black" stroke-width="4" /><path d="M3.36682 105.807L1.95891 99.5009" stroke="white" stroke-width="4" /><path d="M39.8257 78.501H88.3257L58.8257 7.00098L39.8257 78.501Z" fill="#8B6CEF" stroke="#8B6CEF" stroke-width="4" /><path d="M70.3257 57.119L69.6786 59.1104L69.5664 59.4559H69.2031H67.1092L68.8032 60.6866L69.0971 60.9002L68.9849 61.2457L68.3378 63.237L70.0318 62.0063L70.3257 61.7928L70.6196 62.0063L72.3136 63.237L71.6665 61.2457L71.5543 60.9002L71.8481 60.6866L73.5421 59.4559H71.4483H71.085L70.9727 59.1104L70.3257 57.119Z" fill="#FFF50F" stroke="#FFF50F" /><path d="M58.3537 35.403L55.5616 39.5509L55.3588 39.8523L55.0095 39.7525L50.2018 38.3788L53.2839 42.316L53.5078 42.602L53.3049 42.9034L50.5129 47.0512L55.2098 45.3367L55.551 45.2121L55.7749 45.4982L58.857 49.4353L58.6778 44.4385L58.6647 44.0755L59.006 43.9509L63.7029 42.2364L58.8952 40.8627L58.5459 40.7629L58.5329 40.3999L58.3537 35.403Z" fill="#FFF50F" stroke="#FFF50F" /><path d="M22.8257 125.501C51.8148 144.152 71.5061 151.504 105.826 131.001" stroke="#5845CF" stroke-width="4" /><path d="M58.101 145.285C57.3014 146.027 56.7929 146.859 56.5843 147.564C56.3685 148.293 56.5117 148.696 56.6741 148.871C56.8366 149.046 57.2281 149.219 57.9715 149.059C58.69 148.903 59.5579 148.458 60.3574 147.716C61.1569 146.975 61.6654 146.142 61.8741 145.437C62.0899 144.708 61.9467 144.305 61.7843 144.13C61.6218 143.955 61.2302 143.782 60.4869 143.942C59.7683 144.098 58.9005 144.543 58.101 145.285Z" stroke="#5845CF" stroke-width="2" /><path d="M63.7886 148.113C64.5322 149.072 65.3943 149.731 66.1439 150.049C66.9278 150.381 67.3663 150.268 67.5492 150.126C67.7321 149.984 67.9514 149.588 67.8244 148.746C67.703 147.94 67.2791 146.941 66.5354 145.982C65.7917 145.023 64.9296 144.364 64.18 144.046C63.3962 143.714 62.9577 143.827 62.7748 143.969C62.5919 144.111 62.3726 144.508 62.4995 145.349C62.6209 146.155 63.0449 147.154 63.7886 148.113Z" stroke="#5845CF" stroke-width="2" /><ellipse rx="41.5" ry="4" transform="matrix(-1 0 0 1 62.8257 79.501)" fill="#8B6CEF" /><path d="M48.7999 64.3399L48.7051 67.8856L48.6954 68.2487L48.3471 68.3517L44.9456 69.3573L48.2885 70.5431L48.6309 70.6645L48.6212 71.0276L48.5264 74.5733L50.6872 71.7605L50.9085 71.4724L51.2508 71.5939L54.5937 72.7796L52.5863 69.8554L52.3807 69.5559L52.602 69.2679L54.7627 66.455L51.3613 67.4606L51.0129 67.5636L50.8073 67.2641L48.7999 64.3399Z" fill="#FFF50F" stroke="#FFF50F" /></svg>';var gn=require("obsidian"),Bi={9:vn,11:hn};function Di(){var e;return r("div",{class:"cmdr-icon-wrapper",dangerouslySetInnerHTML:{__html:(e=Bi[(0,gn.moment)().month()])!=null?e:fn}})}var _e=dn(Di);function Tt({manifest:e}){let o=r("button",{className:"mod-cta",onClick:n=>{xt(n),setTimeout(()=>location.replace("https://forms.gle/hPjn61G9bqqFb3256"),Math.random()*800+500)}},r(k,{icon:"message-square",size:20}),u("Leave feedback")),t=r("button",{className:"mod-cta",onClick:n=>{xt(n),setTimeout(()=>location.replace("https://ko-fi.com/phibr0"),Math.random()*800+500)}},r(k,{icon:"coffee",size:20}),u("Support development"));return r("div",{className:"cmdr-about"},Lt.Platform.isMobile&&[r("hr",null),o,t],Lt.Platform.isDesktop&&[r("div",{className:"setting-item mod-toggle",style:{width:"100%",borderTop:"1px solid var(--background-modifier-border)",paddingTop:"18px"}},r("div",{className:"setting-item-info"},r("div",{className:"setting-item-name"},u("Leave feedback")),r("div",{className:"setting-item-description"},u("Share feedback, issues, and ideas with our feedback form."))),r("div",{className:"setting-item-control"},o)),r("div",{className:"setting-item mod-toggle",style:{width:"100%"}},r("div",{className:"setting-item-info"},r("div",{className:"setting-item-name"},u("Donate")),r("div",{className:"setting-item-description"},u("Consider donating to support development."))),r("div",{className:"setting-item-control"},t)),r("hr",null)],r(_e,null),r("b",null,e.name),r(It,null),r("a",{className:"cmdr-version",href:"https://github.com/phibr0/obsidian-commander/releases/tag/"+e.version},e.version))}var z=require("obsidian");function Nt(e,o){if(e.empty(),new z.Setting(e).setName("Toolbar Row Count").setDesc("Set how many Rows the Mobile Toolbar should have. Set this to 0 to remove the Toolbar.").addSlider(n=>n.setLimits(0,5,1).setValue(o.settings.advancedToolbar.rowCount).setDynamicTooltip().onChange(async i=>{o.settings.advancedToolbar.rowCount=i,await o.saveSettings(),oe(o.settings.advancedToolbar)})),new z.Setting(e).setName("Column Layout").setDesc("Use a column based layout instead of the default row. This makes it easier to arrange the Commands.").addToggle(n=>n.setValue(o.settings.advancedToolbar.columnLayout).onChange(async i=>{o.settings.advancedToolbar.columnLayout=i,await o.saveSettings(),oe(o.settings.advancedToolbar)})),new z.Setting(e).setName("Bottom Offset").setDesc("Offset the Toolbar from the Bottom of the Screen. This is useful if the toolbar is partially obscured by other UI Elements.").addSlider(n=>n.setLimits(0,32,1).setValue(o.settings.advancedToolbar.heightOffset).setDynamicTooltip().onChange(async i=>{o.settings.advancedToolbar.heightOffset=i,await o.saveSettings(),oe(o.settings.advancedToolbar)})),z.Platform.isMobile){let n=document.createDocumentFragment();n.appendChild(createEl("h3",{text:"Custom Icons"})),e.appendChild(n),o.getCommandsWithoutIcons().forEach(i=>{new z.Setting(e).setName(i.name).setDesc(`ID: ${i.id}`).addButton(a=>{var d;let c=a.buttonEl.createDiv({cls:"AT-settings-icon"});if(i.icon)(0,z.setIcon)(c,i.icon);else{let m=(d=o.settings.advancedToolbar.mappedIcons.find(h=>h.commandID===i.id))==null?void 0:d.iconID;m?(0,z.setIcon)(c,m):a.setButtonText("No Icon")}a.onClick(async()=>{let m=await new R(o).awaitSelection(),h=o.settings.advancedToolbar.mappedIcons.find(s=>s.commandID===i.id);h?h.iconID=m:o.settings.advancedToolbar.mappedIcons.push({commandID:i.id,iconID:m}),await o.saveSettings(),ot(o.settings.advancedToolbar),Nt(e,o)})}).addExtraButton(a=>{a.setIcon("reset").setTooltip("Reset to default - Requires a restart").onClick(async()=>{o.settings.advancedToolbar.mappedIcons=o.settings.advancedToolbar.mappedIcons.filter(c=>c.commandID!==i.id),delete i.icon,delete app.commands.commands[i.id].icon,await o.saveSettings(),Nt(e,o),new z.Notice("If the default Icon doesn't appear, you might have to restart Obsidian.")})})})}let t=e.appendChild(createEl("div",{cls:"cmdr-sep-con",attr:{style:"margin-top: 64px"}}));t.appendChild(createEl("div",{text:"Advanced Settings",attr:{style:"margin-bottom: 8px; font-weight: bold"}})),new z.Setting(t).setName("Button Height").setDesc("Change the Height of each Button inside the Mobile Toolbar (in px).").addText(n=>{var i,a;return n.setValue((a=(i=o.settings.advancedToolbar.rowHeight)==null?void 0:i.toString())!=null?a:"48").setPlaceholder("48").onChange(async c=>{let d=Number(c),m=isNaN(d);n.inputEl.toggleClass("is-invalid",m),m||(o.settings.advancedToolbar.rowHeight=d,await o.saveSettings(),oe(o.settings.advancedToolbar))})}),new z.Setting(t).setName("Button Width").setDesc("Change the Width of each Button inside the Mobile Toolbar (in px).").addText(n=>{var i,a;return n.setValue((a=(i=o.settings.advancedToolbar.buttonWidth)==null?void 0:i.toString())!=null?a:"48").setPlaceholder("48").onChange(async c=>{let d=Number(c),m=isNaN(d);n.inputEl.toggleClass("is-invalid",m),m||(o.settings.advancedToolbar.buttonWidth=d,await o.saveSettings(),oe(o.settings.advancedToolbar))})}),new z.Setting(t).setName("Toolbar Extra Spacing").setDesc("Some Themes need extra spacing in the toolbar. If your Toolbar doesn't wrap properly, try increasing this value.").addSlider(n=>n.setLimits(0,64,1).setValue(o.settings.advancedToolbar.spacing).setDynamicTooltip().onChange(async i=>{o.settings.advancedToolbar.spacing=i,await o.saveSettings(),oe(o.settings.advancedToolbar)}))}function At({plugin:e}){let o=K(null);return O(()=>(o.current&&Nt(o.current,e),()=>o.current&&o.current.empty()),[]),r(S,null,r("div",{className:"cmdr-sep-con callout","data-callout":"info"},r("span",{className:"cmdr-callout-warning"},r(k,{icon:"alert-circle"})," ","Info"),r("p",{className:"cmdr-warning-description"},"The Toolbar is only available in Obsidian Mobile. ",z.Platform.isMobile&&r(S,null,"To configure which Commands show up in the Toolbar, open the Mobile Settings.")),z.Platform.isMobile&&r("button",{onClick:()=>{app.setting.openTabById("mobile")},className:"mod-cta"},"Open Mobile Settings")),r("div",{ref:o,style:{paddingBottom:"128px"}}))}var me=require("obsidian");var _n=require("obsidian");var bn=require("obsidian");var at=({initialColor:e,onChange:o})=>{let t=K(null);return O(()=>(t.current&&new bn.ColorComponent(t.current).setValue(e).onChange(o),()=>{var n,i;return(i=(n=t.current)==null?void 0:n.empty)==null?void 0:i.call(n)}),[o,e]),r("div",{ref:t,className:"cmdr-flex cmdr-items-center"})};function Bt({modal:e}){var o;return O(()=>{let t=()=>{this.forceUpdate()};return addEventListener("cmdr-icon-changed",t),()=>removeEventListener("cmdr-icon-changed",t)},[]),r("div",{className:"cmdr-mobile-modify-grid"},r("div",{className:"cmdr-mobile-modify-option",onClick:e.handleNewIcon},r("span",null,u("Icon")),r("span",{className:"cmdr-flex cmdr-gap-1"},r(k,{icon:e.pair.icon,size:20,className:"clickable-icon",style:{marginRight:"0px"}}),r(at,{initialColor:(o=e.pair.color)!=null?o:"#000",onChange:e.handleColorChange}))),r("div",{className:"cmdr-mobile-modify-option"},r("span",null,u("Name")),r("input",{onBlur:({currentTarget:t})=>e.handleRename(t.value),type:"text",placeholder:u("Custom Name"),value:e.pair.name})),r("div",{className:"cmdr-mobile-modify-option"},r("select",{className:"dropdown",value:e.pair.mode,onChange:({currentTarget:t})=>e.handleModeChange(t.value)},r("option",{value:"any"},u("Add command to all devices")),r("option",{value:"mobile"},u("Add command only to mobile devices")),r("option",{value:"desktop"},u("Add command only to desktop devices")),r("option",{value:app.appId},u("Add command only to this device")))),r("div",{className:"modal-button-container"},r("button",{className:"mod-cta",onClick:()=>e.close()},u("Done"))))}var de=class extends _n.Modal{constructor(t,n,i,a,c){super(app);this.pair=t;this.handleRename=n;this.handleNewIcon=i;this.handleModeChange=a;this.handleColorChange=c}async onOpen(){this.titleEl.innerText=this.pair.name,this.reactComponent=r(Bt,{modal:this}),W(this.reactComponent,this.contentEl)}onClose(){W(null,this.contentEl)}};function Pe({value:e,handleChange:o,ariaLabel:t}){let[n,i]=B(!1),a=K(null),[c,d]=B(0);return O(()=>{var m,h;(m=a==null?void 0:a.current)==null||m.select(),(h=a==null?void 0:a.current)==null||h.focus()}),r("div",{class:"cmdr-editable"},n?r("input",{type:"text",value:e,style:{width:c+25+"px"},onKeyDown:m=>{m.key==="Enter"&&m.target.value.length>0&&(i(!1),o(m))},onBlur:()=>i(!1),ref:a}):r("span",{onDblClick:({target:m})=>{d(m==null?void 0:m.offsetWidth),i(!0)},"aria-label":t},e))}function Dt({pair:e,handleRemove:o,handleDown:t,handleUp:n,handleNewIcon:i,handleRename:a,handleModeChange:c,handleColorChange:d,sortable:m=!0}){var v;let h=Q(e.id);if(!h)return r(S,null,me.Platform.isDesktop&&r("div",{className:"setting-item mod-toggle"},r(k,{icon:"alert-triangle",size:20,className:"cmdr-icon clickable-icon mod-warning"}),r("div",{className:"setting-item-info"},r("div",{className:"setting-item-name"},e.name),r("div",{className:"setting-item-description"},u("This Command is not available on this device."))),r("div",{className:"setting-item-control"},r("button",{className:"mod-warning",style:"display: flex",onClick:o,"aria-label":u("Delete")},r(k,{icon:"lucide-trash"})))),me.Platform.isMobile&&r("div",{className:"mobile-option-setting-item",onClick:()=>{new me.Notice(u("This Command is not available on this device."))}},r("span",{className:"mobile-option-setting-item-remove-icon",onClick:o},r(k,{icon:"minus-with-circle",size:22,style:{color:"var(--text-error)"}})),r("span",{className:"mobile-option-setting-item-option-icon mod-warning"},r(k,{icon:"alert-triangle",size:22})),r("span",{className:"mobile-option-setting-item-name"},e.name)));let s=h.id.split(":").first(),g=app.plugins.manifests[s],_=!g,f=h.hasOwnProperty("checkCallback")||h.hasOwnProperty("editorCheckCallback"),b=Hi(e.mode),y=e.mode.match(/desktop|mobile|any/)?e.mode[0].toUpperCase()+e.mode.substring(1):u("This device");return r(S,null,me.Platform.isDesktop&&r("div",{className:"setting-item mod-toggle"},r(k,{icon:e.icon,size:20,"aria-label":u("Choose new"),onClick:i,className:"cmdr-icon clickable-icon"}),r("div",{className:"setting-item-info"},r("div",{className:"setting-item-name"},r(Pe,{ariaLabel:u("Double click to rename"),handleChange:({target:L})=>{a(L==null?void 0:L.value)},value:e.name}),e.name!==h.name&&r("span",{style:"margin-left: .8ex"},"(",h.name,")")),r("div",{className:"setting-item-description"},u("Added by {{plugin_name}}.".replace("{{plugin_name}}",_?"Obsidian":g.name))," ",f?u("Warning: This is a checked Command, meaning it might not run under every circumstance."):"")),r("div",{className:"setting-item-control"},r(at,{initialColor:(v=e.color)!=null?v:"#000",onChange:d}),m&&r(S,null,r(k,{icon:"arrow-down",className:"setting-editor-extra-setting-button clickable-icon",onClick:t,"aria-label":u("Move down")}),r(k,{icon:"arrow-up",className:"setting-editor-extra-setting-button clickable-icon",onClick:n,"aria-label":u("Move up")})),r(k,{icon:b,className:"setting-editor-extra-setting-button clickable-icon",onClick:()=>c(),"aria-label":u("Change Mode (Currently: {{current_mode}})").replace("{{current_mode}}",y)}),r("button",{className:"mod-warning",style:"display: flex",onClick:o,"aria-label":u("Delete")},r(k,{icon:"lucide-trash"})))),me.Platform.isMobile&&r("div",{className:"mobile-option-setting-item"},r("span",{className:"mobile-option-setting-item-remove-icon",onClick:o},r(k,{icon:"minus-with-circle",size:22,style:{color:"var(--text-error)"}})),r("span",{className:"mobile-option-setting-item-option-icon"},r(k,{icon:e.icon,size:22,onClick:()=>{new de(e,a,i,c,d).open()}})),r("span",{className:"mobile-option-setting-item-name",onClick:()=>{new de(e,a,i,c,d).open()}},e.name,e.name!==h.name&&r("span",{className:"cmdr-option-setting-name"},"(",h.name,")")),r("span",{className:"mobile-option-setting-item-option-icon"},m&&r(S,null,r(k,{icon:"arrow-down",className:"clickable-icon",onClick:t}),r(k,{icon:"arrow-up",className:"clickable-icon",onClick:n})),r(k,{icon:"three-horizontal-bars",className:"clickable-icon",onClick:()=>{new de(e,a,i,c,d).open()}}))))}function Hi(e){return e==="mobile"?"smartphone":e==="desktop"?"monitor":e==="any"?"cmdr-all-devices":"airplay"}function Ht(e,o,t){let n=o<0?e.length+o:o;if(n>=0&&n<e.length){let i=t<0?e.length+t:t,[a]=e.splice(o,1);e.splice(i,0,a)}}var Cn=require("obsidian");var Ri=je(null);function ae({manager:e,plugin:o,children:t,sortable:n=!0}){return r(S,null,r(Ri.Provider,{value:e},r("div",{className:"cmdr-sep-con"},e.pairs.map((i,a)=>{if(i.mode.match(/desktop|mobile|any/)||i.mode===app.appId)return r(Dt,{sortable:n,key:i.id,pair:i,handleRemove:async()=>{(!o.settings.confirmDeletion||await new V(o).didChooseRemove())&&(await e.removeCommand(i),this.forceUpdate())},handleUp:()=>{Ht(e.pairs,a,a-1),e.reorder(),this.forceUpdate()},handleDown:()=>{Ht(e.pairs,a,a+1),e.reorder(),this.forceUpdate()},handleRename:async c=>{i.name=c,await o.saveSettings(),e.reorder(),this.forceUpdate()},handleNewIcon:async()=>{let c=await new R(o).awaitSelection();c&&c!==i.icon&&(i.icon=c,await o.saveSettings(),e.reorder(),this.forceUpdate()),dispatchEvent(new Event("cmdr-icon-changed"))},handleModeChange:async c=>{let d=["any","desktop","mobile",app.appId],m=d.indexOf(i.mode);m===3&&(m=-1),i.mode=c||d[m+1],await o.saveSettings(),e.reorder(),this.forceUpdate()},handleColorChange:async c=>{i.color=c,await o.saveSettings(),e.reorder()}})})),!e.pairs.some(i=>j(i.mode)||i.mode.match(/mobile|desktop/))&&r("div",{class:"cmdr-commands-empty"},r(_e,null),r("h3",null,u("No commands here!")),r("span",null,u("Would you like to add one now?"))),Cn.Platform.isMobile&&r("hr",null),r("div",{className:"cmdr-add-new-wrapper"},r("button",{className:"mod-cta",onClick:async()=>{let i=await q(o);await e.addCommand(i),e.reorder(),this.forceUpdate()}},u("Add command")))),t)}function rt({title:e,children:o}){let[t,n]=B(!1);return r("div",{className:"cmdr-accordion cmdr-sep-con","aria-expanded":t},r("div",{className:"cmdr-accordion-header cmdr-mb-1",onClick:()=>{n(!t)}},r(k,{className:"cmdr-accordion-chevron clickable-icon",icon:"chevron-down",size:24}),r("span",null,e)),r("div",{className:"cmdr-accordion-content",style:{maxHeight:[o].flat().length*120+"px"}},o))}function Rt({name:e,description:o,children:t,className:n}){return r("div",{className:`setting-item ${n}`},r("div",{className:"setting-item-info"},r("div",{className:"setting-item-name"},e),r("div",{className:"setting-item-description"},o)),r("div",{className:"setting-item-control"},t))}function Ft(e){let[o,t]=B(e.value);return r(Rt,{name:e.name,description:e.description,className:"mod-toggle"},r("div",{className:`checkbox-container ${o?"is-enabled":""}`,onClick:()=>{t(!o),e.changeHandler(o)}}))}function Ot({name:e,description:o,changeHandler:t,value:n,hideLabel:i,showLabel:a}){let[c,d]=B(n);return r(Rt,{name:e,description:o,className:"mod-toggle"},r(k,{"aria-label":c?a:i,icon:c?"eye-off":"eye",size:20,className:"clickable-icon",onClick:()=>{d(!c),t(c)}}))}function st(e){var n,i,a;let[o,t]=B(e.value);return r(Rt,{description:e.description,name:e.name,className:"cmdr-slider"},r("div",null,r(Pe,{ariaLabel:u("Double click to enter custom value"),value:o.toString(),handleChange:({target:c})=>{let d=Number(c.value);!isNaN(d)&&o!==d&&(t(d),e.changeHandler(d))}}),r("input",{class:"slider",type:"range",min:(n=e.min)!=null?n:"0",max:(i=e.max)!=null?i:"32",step:(a=e.step)!=null?a:"1",value:o,onPointerMove:({target:c})=>{o!==c.value&&(t(c.value),e.changeHandler(c.value))}})))}function yn({plugin:e}){let[o,t]=B([]),n=e.settings.hide.leftRibbon;return O(()=>{t(app.workspace.leftRibbon.items.map(i=>({name:i.title,icon:i.icon})))},[]),r(S,null,r("hr",null),r(rt,{title:u("Hide other Commands")},o.map(i=>r(Ot,{name:i.name,description:"",hideLabel:u("Hide"),showLabel:u("Show"),changeHandler:async a=>{a?n.contains(i.name)&&n.remove(i.name):n.push(i.name),Me(e.settings),await e.saveSettings()},value:n.contains(i.name)}))))}function wn({plugin:e}){let o=e.settings.hide.statusbar,[t,n]=B([]);return O(()=>{let a=[...app.statusBar.containerEl.getElementsByClassName("status-bar-item")].map(c=>[...c.classList].find(d=>d.startsWith("plugin-"))).filter(c=>c).map(c=>c.substring(7));n(a.map(c=>app.plugins.manifests[c]||{id:c,name:c.replace(/-/g," ").replace(/(^\w{1})|(\s+\w{1})/g,d=>d.toUpperCase()),description:"Core Plugin"}))},[]),r(S,null,r("hr",null),r(rt,{title:u("Hide other Commands")},t.map(i=>r(Ot,{name:i.name,description:i.description,value:o.contains(i.id),hideLabel:u("Hide"),showLabel:u("Show"),changeHandler:async a=>{a?o.contains(i.id)&&o.remove(i.id):o.push(i.id),Me(e.settings),await e.saveSettings()}}))))}var En=require("obsidian");var Mn=require("obsidian");function kn({plugin:e,macro:o,onSave:t,onCancel:n}){let[i,a]=B(o.name||"Macro Name"),[c,d]=B(o.icon||"star"),[m,h]=B(o.startup||!1),[s,g]=B(JSON.parse(JSON.stringify(o.macro))||[]),_=this.forceUpdate.bind(this),f=async()=>{let y=await new ne(e).awaitSelection();y&&g([...s,{action:0,commandId:y.id}])},b=async()=>{g([...s,{action:1,delay:250}])};return r("div",null,r("div",{class:"setting-item cmdr-mm-item"},r("div",null,r("span",null,"Name"),r("input",{type:"text",placeholder:"Macro Name",value:i,onChange:y=>a(y.currentTarget.value),width:"100%"})),r("div",null,r("span",null,"Icon"),r("button",{onClick:async()=>d(await new R(e).awaitSelection())},r(k,{icon:c})))),s.map((y,v)=>{switch(y.action){case 0:let L=Q(y.commandId);return r("div",{class:"setting-item cmdr-mm-item"},r("div",null,r("button",{onClick:async()=>{let w=await new ne(e).awaitSelection();g(s.map((H,ee)=>ee===v?Yt(Ve({},H),{commandId:w.id}):H))}},(L==null?void 0:L.name)||"Cannot find Command")),r("div",null,r("div",{class:"cmdr-mm-action-options"},r(k,{class:"clickable-icon",icon:"arrow-down",onClick:()=>{if(v===s.length-1)return;let w=[...s],H=w[v];w[v]=w[v+1],w[v+1]=H,g(w)}}),r(k,{class:"clickable-icon",icon:"arrow-up",onClick:()=>{if(v===0)return;let w=[...s],H=w[v];w[v]=w[v-1],w[v-1]=H,g(w)}}),r(k,{class:"clickable-icon",icon:"cross",onClick:()=>{g(s.filter((w,H)=>H!==v))}}))));case 1:return r("div",{class:"setting-item cmdr-mm-item"},r("div",null,r(st,{name:"Delay",min:0,max:1e4,step:50,description:"Delay in milliseconds",value:y.delay,changeHandler:w=>y.delay=w})),r("div",null,r("div",{class:"cmdr-mm-action-options"},r(k,{class:"clickable-icon",icon:"arrow-down",onClick:()=>{if(v===s.length-1)return;let w=[...s],H=w[v];w[v]=w[v+1],w[v+1]=H,g(w)}}),r(k,{class:"clickable-icon",icon:"arrow-up",onClick:()=>{if(v===0)return;let w=[...s],H=w[v];w[v]=w[v-1],w[v-1]=H,g(w)}}),r(k,{class:"clickable-icon",icon:"cross",onClick:()=>{g(s.filter((w,H)=>H!==v))}}))));case 2:return r("div",null,"Editor: ",y.action);case 3:return r("div",null,"Loop: ",y.times)}}),r("div",{className:"setting-item cmdr-mm-actions cmdr-justify-between"},r("div",{className:"cmdr-flex cmdr-items-center cmdr-justify-self-start"},r("input",{type:"checkbox",id:"checkbox",checked:m,onChange:({target:y})=>{var v;h((v=y==null?void 0:y.checked)!=null?v:!1)}}),r("label",{htmlFor:"checkbox"},"Auto-Run on Startup")),r("div",null,r("button",{onClick:f},"Add Command"),r("button",{onClick:b},"Add Delay"))),r("div",{className:"cmdr-mm-control"},r("button",{class:s.length===0?"disabled":"mod-cta",disabled:s.length===0,onClick:()=>s.length&&t({macro:s,name:i,icon:c,startup:m})},"Save"),r("button",{onClick:n},"Cancel")))}var Le=class extends Mn.Modal{constructor(t,n,i){super(app);this.macro=n,this.plugin=t,this.onSave=i}onOpen(){this.titleEl.setText("Macro Builder"),W(r(kn,{plugin:this.plugin,macro:this.macro,onSave:this.onSave,onCancel:this.close.bind(this)}),this.contentEl)}onClose(){W(null,this.contentEl)}};function Vt({plugin:e,macros:o}){let t=(i,a)=>{let c=m=>{o.splice(a!==void 0?a:o.length,a!==void 0?1:0,m),e.saveSettings(),this.forceUpdate(),Ee(e),d.close()},d=new Le(e,i,c);d.open()},n=i=>{o.splice(i,1),e.saveSettings(),this.forceUpdate(),Ee(e)};return r(S,null,r("div",{className:"cmdr-sep-con"},o.map((i,a)=>r("div",{class:"setting-item mod-toggle"},r("div",{className:"setting-item-info"},r("div",{className:"setting-item-name"},i.name),r("div",{className:"setting-item-description"},i.macro.length," Actions")),r("div",{className:"setting-item-control"},r("button",{"aria-label":"Edit Macro",onClick:()=>t(i,a)},r(k,{icon:"lucide-pencil"})),r("button",{"aria-label":"Delete",class:"mod-warning",onClick:async()=>{(!e.settings.confirmDeletion||await new V(e).didChooseRemove())&&n(a)}},r(k,{icon:"trash"})))))),!o.length&&r("div",{class:"cmdr-commands-empty"},r(_e,null),r("h3",null,"No Macros yet!"),r("span",null,u("Would you like to add one now?"))),En.Platform.isMobile&&r("hr",null),r("div",{className:"cmdr-add-new-wrapper"},r("button",{class:"mod-cta",onClick:()=>t({name:"",macro:[],icon:"star"})},"Add Macro")))}function Te({plugin:e,mobileMode:o}){let[t,n]=B(0),[i,a]=B(!0),c=({key:m,shiftKey:h})=>{h&&m==="Tab"?t>0?n((t-1)%d.length):n(d.length-1):m==="Tab"&&n((t+1)%d.length)};O(()=>(addEventListener("keydown",c),()=>removeEventListener("keydown",c)),[t]),J.Platform.isMobile&&O(()=>{let m=document.querySelector(".modal-setting-back-button"),h=m.cloneNode(!0);m.parentNode.replaceChild(h,m),a(!0)},[]),O(()=>{let m=document.querySelector(".modal-setting-back-button");!m||(i?(m.parentElement.lastChild.textContent="Commander",m.onclick=()=>app.setting.closeActiveTab()):(m.parentElement.lastChild.textContent=d[t].name,m.onclick=()=>a(!0)))},[i]);let d=Qe(()=>[{name:u("General"),tab:r(S,null,r(Ft,{name:u("Always ask before removing?"),description:u("Always show a Popup to confirm deletion of a Command."),value:e.settings.confirmDeletion,changeHandler:async m=>{e.settings.confirmDeletion=!m,await e.saveSettings()}}),r(Ft,{value:e.settings.showAddCommand,name:u('Show "Add Command" Button'),description:'Show the "Add Command" Button in every Menu.',changeHandler:async m=>{e.settings.showAddCommand=!m,e.manager.pageHeader.reorder(),await e.saveSettings()}}),r(st,{value:e.settings.spacing,name:u("Choose custom spacing for Command Buttons"),description:u("Change the spacing between commands. You can set different values on mobile and desktop."),changeHandler:async m=>{tt(m),e.settings.spacing=m,await e.saveSettings()}}))},{name:u("Left Ribbon"),tab:r(ae,{manager:e.manager.leftRibbon,plugin:e,sortable:!1},r(yn,{plugin:e}),r("div",{className:"cmdr-sep-con callout","data-callout":"warning"},r("span",{className:"cmdr-callout-warning"},r(k,{icon:"alert-triangle"})," ","Reordering and Sorting"),r("p",{className:"cmdr-warning-description"},"As of Obsidian 1.1.0 you can reorder the Buttons in the left ribbon by dragging. This will replace the old sorting feature.")))},{name:u("Page Header"),tab:r(ae,{manager:e.manager.pageHeader,plugin:e},r("hr",null),r("div",{className:"cmdr-sep-con callout","data-callout":"warning"},r("span",{className:"cmdr-callout-warning"},r(k,{icon:"alert-triangle"})," ",u("Warning")),r("p",{className:"cmdr-warning-description"},u("As of Obsidian 0.16.0 you need to explicitly enable the View Header.")),r("button",{onClick:()=>{app.setting.openTabById("appearance"),setTimeout(()=>{var m,h,s,g;app.setting.activeTab.containerEl.scroll({behavior:"smooth",top:250}),(g=(s=(h=(m=app.setting.activeTab.containerEl.querySelectorAll(".setting-item-heading")[1].nextSibling)==null?void 0:m.nextSibling)==null?void 0:h.nextSibling)==null?void 0:s.addClass)==null||g.call(s,"cmdr-cta")},50)},className:"mod-cta"},u("Open Appearance Settings"))))},{name:u("Statusbar"),tab:r(ae,{manager:e.manager.statusBar,plugin:e},r(wn,{plugin:e}))},{name:u("Editor Menu"),tab:r(ae,{manager:e.manager.editorMenu,plugin:e})},{name:u("File Menu"),tab:r(ae,{manager:e.manager.fileMenu,plugin:e})},{name:u("Explorer"),tab:r(ae,{manager:e.manager.explorerManager,plugin:e},r("hr",null),r("div",{className:"cmdr-sep-con callout","data-callout":"warning"},r("span",{className:"cmdr-callout-warning"},r(k,{icon:"alert-triangle"})," ",u("Warning")),r("p",{className:"cmdr-warning-description"},"When clicking on a Command in the Explorer, the Explorer view will become focused. This might interfere with Commands that are supposed to be executed on an active File/Explorer.")))},{name:J.Platform.isMobile?"Mobile Toolbar":"Toolbar",tab:r(At,{plugin:e})},{name:"Macros",tab:r(Vt,{plugin:e,macros:e.settings.macros})}],[]);return r(S,null,J.Platform.isDesktop&&r("div",{className:"cmdr-setting-title"},r("h1",null,e.manifest.name)),(J.Platform.isDesktop||i)&&r(Oi,{tabs:d,activeTab:t,setActiveTab:n,setOpen:a}),r("div",{class:`cmdr-setting-content ${o?"cmdr-mobile":""}`},(J.Platform.isDesktop||!i)&&d[t].tab,(J.Platform.isMobile&&i||J.Platform.isDesktop&&t===0)&&r(Tt,{manifest:e.manifest})))}function Oi({tabs:e,activeTab:o,setActiveTab:t,setOpen:n}){let i=K(null),a=c=>{var d;c.preventDefault(),(d=i.current)==null||d.scrollBy({left:c.deltaY>0?16:-16})};return O(()=>{let c=i.current;if(!(!c||J.Platform.isMobile))return c.addEventListener("wheel",a),()=>c.removeEventListener("wheel",a)},[]),O(()=>{var c;return(c=document.querySelector(".cmdr-tab-active"))==null?void 0:c.scrollIntoView({behavior:"smooth",block:"nearest"})},[o]),r("nav",{class:`cmdr-setting-header ${J.Platform.isMobile?"cmdr-mobile":""}`,ref:i},r("div",{class:`cmdr-setting-tab-group ${J.Platform.isMobile?"vertical-tab-header-group-items":""}`},e.map((c,d)=>r("div",{className:`cmdr-tab ${o===d?"cmdr-tab-active":""} ${J.Platform.isMobile?"vertical-tab-nav-item":""}`,onClick:()=>{t(d),n(!1)}},c.name,J.Platform.isMobile&&r(k,{className:"vertical-tab-nav-item-chevron cmdr-block",icon:"chevron-right",size:24})))),J.Platform.isDesktop&&r("div",{className:"cmdr-fill"}))}var Ne=class extends ct.PluginSettingTab{constructor(t){super(app,t);this.plugin=t}display(){W(r(Te,{plugin:this.plugin,mobileMode:ct.Platform.isMobile}),this.containerEl)}hide(){W(null,this.containerEl)}};var lt=require("obsidian");var Ae=class extends lt.Modal{constructor(t){super(app);this.plugin=t,this.containerEl.addClass("cmdr-setting-modal")}onOpen(){let t=lt.Platform.isMobile;W(r(Te,{plugin:this.plugin,mobileMode:t}),this.contentEl)}onClose(){W(null,this.contentEl)}};var xn=require("obsidian");function zt(){(0,xn.addIcon)("cmdr-all-devices",'<g style="fill: currentColor;"><path d="M 12.5 16.667969 L 83.332031 16.667969 C 87.9375 16.667969 91.667969 20.398438 91.667969 25 L 91.667969 33.332031 L 75 33.332031 L 75 25 L 20.832031 25 L 20.832031 75 L 58.332031 75 L 58.332031 83.332031 L 12.5 83.332031 C 7.898438 83.332031 4.167969 79.601562 4.167969 75 L 4.167969 25 C 4.167969 20.398438 7.898438 16.667969 12.5 16.667969 M 70.832031 41.667969 L 95.832031 41.667969 C 98.132812 41.667969 100 43.53125 100 45.832031 L 100 87.5 C 100 89.800781 98.132812 91.667969 95.832031 91.667969 L 70.832031 91.667969 C 68.53125 91.667969 66.667969 89.800781 66.667969 87.5 L 66.667969 45.832031 C 66.667969 43.53125 68.53125 41.667969 70.832031 41.667969 M 75 50 L 75 79.167969 L 91.667969 79.167969 L 91.667969 50 Z M 75 50 "/></g>')}var Be=class extends X{constructor(t){super(t,t.settings.leftRibbon);this.plugin=t,this.plugin.settings.leftRibbon.forEach(n=>this.addCommand(n,!1)),app.workspace.onLayoutReady(()=>{})}async addCommand(t,n=!0){if(n&&(this.plugin.settings.leftRibbon.push(t),await this.plugin.saveSettings()),j(t.mode)){this.plugin.addRibbonIcon(t.icon,t.name,()=>app.commands.executeCommandById(t.id));let i=app.workspace.leftRibbon.items.find(a=>a.icon===t.icon&&a.name===a.name);i&&(i.buttonEl.style.color=t.color==="#000000"||t.color===void 0?"inherit":t.color),this.plugin.register(()=>this.removeCommand(t,!1))}}async removeCommand(t,n=!0){n&&(this.plugin.settings.leftRibbon.remove(t),await this.plugin.saveSettings());let i=app.workspace.leftRibbon.items.find(a=>a.icon===t.icon&&a.name===a.name);i&&i.buttonEl.remove(),app.workspace.leftRibbon.items.remove(i)}reorder(){this.plugin.settings.leftRibbon.forEach(t=>{this.removeCommand(t,!1),this.addCommand(t,!1)})}};var dt=class extends In.Plugin{async executeStartupMacros(){this.settings.macros.forEach((t,n)=>{t.startup&&this.executeMacro(n)})}async executeMacro(t){let n=this.settings.macros[t];if(!n)throw new Error("Macro not found");for(let i of n.macro)switch(i.action){case 0:{await app.commands.executeCommandById(i.commandId);continue}case 1:{await new Promise(a=>setTimeout(a,i.delay));continue}case 2:continue;case 3:{for(let a=0;a<i.times;a++)await app.commands.executeCommandById(i.commandId);continue}}}async onload(){var t,n;await this.loadSettings(),(n=(t=this.settings.hide).leftRibbon)!=null||(t.leftRibbon=[]),zt(),this.manager={editorMenu:new Ie(this,this.settings.editorMenu),fileMenu:new Se(this,this.settings.fileMenu),leftRibbon:new Be(this),statusBar:new be(this,this.settings.statusBar),pageHeader:new ge(this,this.settings.pageHeader),explorerManager:new ve(this,this.settings.explorer)},this.addSettingTab(new Ne(this)),this.addCommand({name:u("Open Commander Settings"),id:"open-commander-settings",callback:()=>new Ae(this).open()}),this.registerEvent(app.workspace.on("editor-menu",this.manager.editorMenu.applyEditorMenuCommands(this))),this.registerEvent(app.workspace.on("file-menu",this.manager.fileMenu.applyFileMenuCommands(this))),app.workspace.onLayoutReady(()=>{Me(this.settings),Ee(this),tt(this.settings.spacing),oe(this.settings.advancedToolbar),ot(this.settings.advancedToolbar),this.executeStartupMacros()})}onunload(){var t;(t=document.head.querySelector("style#cmdr"))==null||t.remove(),Yo()}async loadSettings(){let t=Object.assign({},yo,await this.loadData());this.settings=t}async saveSettings(){await this.saveData(this.settings)}listActiveToolbarCommands(){return this.app.vault.getConfig("mobileToolbarCommands")}getCommands(){let t=[];return this.listActiveToolbarCommands().forEach(n=>{let i=this.app.commands.commands[n];i&&t.push(i)}),t}getCommandsWithoutIcons(t=!0){let n=[];return this.getCommands().forEach(i=>{i&&!i.icon&&n.push(i)}),t&&this.getCommands().forEach(i=>{this.settings.advancedToolbar.mappedIcons.find(a=>a.commandID===i.id)&&n.push(i)}),n}};
+
+/* by phibr0 */
diff --git a/contributing/.obsidian/plugins/cmdr/manifest.json b/contributing/.obsidian/plugins/cmdr/manifest.json
new file mode 100644
index 00000000..42f160d3
--- /dev/null
+++ b/contributing/.obsidian/plugins/cmdr/manifest.json
@@ -0,0 +1,11 @@
+{
+	"id": "cmdr",
+	"name": "Commander",
+	"version": "0.4.9",
+	"minAppVersion": "1.1.0",
+	"description": "Customize your workspace by adding commands everywhere, create Macros and supercharge your mobile toolbar.",
+	"author": "jsmorabito & phibr0",
+	"authorUrl": "https://github.com/phibr0",
+	"fundingUrl": "https://ko-fi.com/phibr0",
+	"isDesktopOnly": false
+}
diff --git a/contributing/.obsidian/plugins/cmdr/styles.css b/contributing/.obsidian/plugins/cmdr/styles.css
new file mode 100644
index 00000000..05bc3224
--- /dev/null
+++ b/contributing/.obsidian/plugins/cmdr/styles.css
@@ -0,0 +1,1 @@
+.cmdr{transition:transform .4s 0s}.cmdr.status-bar-item{margin:0}.cmdr-removing{transform:translate(100%);z-index:0}.cmdr-ribbon-removing{transform:scale(0)}.cmdr-setting-title{display:flex;align-items:baseline;justify-content:space-between}.cmdr-setting-title h1{font-weight:900;margin-top:6px;margin-bottom:12px}.cmdr-option-setting-name{margin-left:.8ex}.cmdr-base-component{padding:0 20px;margin-bottom:-18px}.cmdr-menu-more-options{color:var(--text-muted);position:absolute;right:6px;padding-top:2px;transform:scale(.9);transition:all .15s ease}.cmdr-menu-more-options:hover{color:var(--text-primary)}.cmdr-mobile .cmdr-credits{place-content:center}.cmdr-fill{border:2px solid #0000;width:100%}.cmdr-icon{height:20px;align-self:center}.cmdr-setting-modal>.modal{height:60vh;width:80vw}.cmdr-editable{display:inline-block}.cmdr-editable input{padding:0;border:none;height:unset;background-color:initial!important;border-bottom:1px solid var(--interactive-accent);border-radius:0}.cmdr-setting-content{height:calc(100% - 10rem)}.cmdr-setting-content .setting-item:first{border-top:none}.cmdr-commands-empty{display:flex;place-items:center;flex-flow:column;margin:2.5rem 0 1.25rem;place-content:center}.cmdr-commands-empty svg{width:100%}.cmdr-commands-empty h3{margin:8px 0;font-weight:900}.cmdr-sep-con:not(:empty){background-color:var(--background-primary);border:1px solid var(--background-modifier-border);padding:12px;border-radius:12px}.cmdr-sep-con:not(:empty) .setting-item:first-of-type{padding-top:0}.cmdr-sep-con:not(:empty) .setting-item:last-of-type{padding-bottom:0}.cmdr-sep-con:not(:empty) .cmdr-callout-warning{padding:var(--callout-title-padding);display:flex;gap:var(--size-4-1);font-size:var(--callout-title-size);color:rgb(var(--callout-color));line-height:var(--line-height-tight);font-weight:700}.cmdr-sep-con:not(:empty) p.cmdr-warning-description{margin:8px 0}.cmdr-accordion{margin-bottom:16px}.cmdr-accordion .cmdr-accordion-chevron{margin-left:0;margin-right:4px;height:24px}.cmdr-accordion .cmdr-accordion-chevron>svg{transition:all .25s ease}.cmdr-accordion .cmdr-accordion-content{max-height:1000px;transition:all .3s ease-in-out;overflow:hidden;margin-left:14px;padding-left:14px}.cmdr-accordion .cmdr-accordion-content .setting-item{padding-top:12px}.cmdr-accordion[aria-expanded=false] .cmdr-accordion-chevron>svg{transform:rotate(-90deg)}.cmdr-accordion[aria-expanded=false] .cmdr-accordion-content{max-height:0!important;transition:max-height .3s cubic-bezier(0,1,0,1)}.cmdr-accordion .cmdr-accordion-header{display:flex;cursor:var(--cursor)}.cmdr-accordion .cmdr-accordion-header span{font-weight:600;margin-top:0;align-self:end}.cmdr-add-new-wrapper{width:100%;margin-top:12px;margin-bottom:2rem;display:flex;justify-content:center;align-items:center}.cmdr-add-new-wrapper button{margin:0 24px}.cmdr-adder{opacity:0;transition:opacity .12s ease}.cmdr-adder:hover{opacity:1}.cmdr-adder.view-action{order:-1001}.cmdr-page-header{order:-1000}.cmdr-macro-builder .modal{height:80vh;width:95vw;padding:0}.cmdr-macro-builder .modal-close-button{z-index:99}.cmdr-macro-builder .modal-content{margin-top:0}.cmdr-macro-builder .cmdr-macro-icon{display:flex;justify-content:center}.cmdr-macro-builder .cmdr-macro-control{position:absolute;z-index:5;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;width:3em;top:0}.cmdr-macro-builder .cmdr-macro-control .cmdr-macro-button{background-color:var(--background-secondary-alt);aspect-ratio:1;display:flex;justify-content:center;align-items:center;border-radius:2px;outline:1px solid var(--background-modifier-border);padding:2px}.cmdr-setting-header{margin-bottom:24px;overflow-y:hidden;overflow-x:auto}.cmdr-setting-header .cmdr-setting-tab-group{display:flex;align-items:flex-end;border-bottom:2px solid var(--background-modifier-border)}.cmdr-setting-header.cmdr-mobile .cmdr-setting-tab-group{display:flex;flex-direction:column;align-items:flex-start;border:none}.cmdr-setting-header.cmdr-mobile .cmdr-setting-tab-group .cmdr-tab{border-left:none!important;border-right:none!important;border-top:none!important;transform:none;transition:color .2s ease;width:100%;display:flex;font-weight:500;justify-content:space-between}.cmdr-setting-header.cmdr-mobile .cmdr-setting-tab-group .cmdr-tab:first-child{margin-left:0}.cmdr-setting-header.cmdr-mobile .cmdr-setting-tab-group .cmdr-tab.cmdr-tab-active{transform:none}.cmdr-setting-header .cmdr-tab{padding:6px 8px;font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap;border-left:2px solid #0000;border-right:2px solid #0000}.cmdr-setting-header .cmdr-tab:first-child{margin-left:6px}.cmdr-setting-header .cmdr-tab.cmdr-tab-active{transform:translateY(2px);border-radius:2px;border:2px solid var(--background-modifier-border);border-bottom-color:var(--background-primary)}.is-tablet .cmdr-tab{border:none!important;margin-bottom:2px;padding:10px;font-size:1.08em}.is-tablet .cmdr-tab,.is-tablet .cmdr-tab .cmdr-block{display:flex;align-items:center}.cmdr-mobile-modify-grid{padding:22px}.cmdr-mobile-modify-grid .cmdr-mobile-modify-option{display:flex;width:100%;justify-content:space-between;align-items:center;margin-bottom:16px}.cmdr-mobile-modify-grid .cmdr-mobile-modify-option>input{max-width:70%}.cmdr-mobile-modify-grid .cmdr-mobile-modify-option .dropdown{margin:auto}.cmdr-credits{display:flex}.cmdr-credits>span{color:var(--text-muted);font-size:14px}.cmdr-about{display:flex;flex-direction:column;align-items:center;padding-bottom:64px}.cmdr-about hr{width:100%;margin-top:0}.cmdr-about button{display:flex;align-items:center;height:2.6em;font-weight:700;gap:16px;border-radius:6px;margin-bottom:8px}.cmdr-about button:last-of-type{margin-bottom:32px}.cmdr-about .setting-item button{margin:0}.cmdr-about .cmdr-icon-wrapper{display:flex}.cmdr-about .cmdr-icon-wrapper>svg{width:90px;height:90px}.cmdr-about .cmdr-version{text-decoration:none;font-size:12px;color:var(--text-muted)}.cmdr-name-input-wrapper{display:flex;gap:8px;align-items:center}.cmdr-name-input-wrapper button{margin:8px}.cmdr-icon.clickable-icon.mod-warning{cursor:default}.cmdr-hider-title{display:flex;flex-direction:column;align-items:center}.cmdr-hider-title h3{margin-top:0;margin-bottom:8px;font-weight:900}.cmdr-hider-title p{margin-top:0;margin-bottom:26px;width:50%;text-align:center}.setting-item-control .cmdr-editable{width:2em;text-align:center}.setting-item-control .cmdr-editable input,.setting-item-control .cmdr-editable span{width:2em;color:var(--text-normal)}.cmdr-slider .setting-item-control>div{width:100%}.cmdr-slider input.slider{margin-left:4px}.view-action:not(:last-child){margin-right:var(--cmdr-spacing)}.status-bar-item:not(:last-child),.titlebar-button:not(:last-child){margin-right:calc(var(--cmdr-spacing) - 8px)}.side-dock-ribbon-action:not(:last-child){margin-bottom:calc(var(--cmdr-spacing) - 8px)}.cmdr-cta{position:relative}.cmdr-cta:after{content:"";position:absolute;top:-10%;width:104%;left:-2%;height:120%;outline:2px solid var(--color-accent);border-radius:1em;pointer-events:none}.cmdr-mm-item{display:flex;flex-direction:row!important;align-items:center!important;justify-content:space-between;padding:24px 0}.cmdr-mm-item>:first-child{flex-grow:1}.cmdr-mm-item>:last-child{display:flex}.cmdr-mm-item>*{display:flex;flex-direction:column}.cmdr-mm-item>*>span{margin-right:4px;margin-bottom:2px;font-size:12px;color:var(--text-muted);font-weight:700;margin-left:2px}.cmdr-mm-item .cmdr-slider{padding-bottom:0}.cmdr-mm-item .cmdr-slider .cmdr-editable{width:auto;font-size:smaller}.cmdr-mm-item .cmdr-slider .cmdr-editable span{color:var(--text-muted)}.cmdr-mm-actions{width:100%;display:flex;flex-direction:row!important;justify-content:end;gap:8px}.cmdr-mm-actions button{all:unset;cursor:pointer;color:var(--text-accent);padding:6px;border-radius:4px;transition:all .15s ease;text-transform:uppercase;font-weight:700;font-size:small}.cmdr-mm-actions button:hover{color:var(--text-accent-hover);background-color:var(--background-secondary)}.cmdr-mm-actions button:before{content:"+ "}.cmdr-mm-actions button:active{transform:scale(.98)}.cmdr-mm-control{margin-top:24px;width:100%;display:flex;justify-content:center;gap:8px}.cmdr-mm-action-text{opacity:0}.cmdr-mm-action-options{display:flex}.body{--at-button-height:48;--at-row-count:0}.AT-multirow.AT-row .mobile-toolbar-options-container{flex-flow:column wrap}.is-mobile .mobile-toolbar{position:sticky!important;z-index:5;margin-bottom:var(--at-offset)}.AT-always-shown .is-pinned .workspace-drawer-inner{margin-bottom:calc(var(--at-button-height)*var(--at-row-count) + var(--at-spacing))}.AT-multirow.AT-column .mobile-toolbar-options-container{flex-flow:row wrap;overflow-y:auto}.mobile-toolbar-option{height:var(--at-button-height);min-width:var(--at-button-width)}.AT-multirow .mobile-toolbar,.AT-multirow .mobile-toolbar-options-container{height:calc(var(--at-button-height)*var(--at-row-count) + var(--at-spacing))!important}.AT-no-toolbar .mobile-toolbar{display:none}.cmdr-mb-1{margin-bottom:.25rem}.cmdr-block{display:block}.cmdr-flex{display:flex}.cmdr-items-center{align-items:center}.cmdr-justify-between{justify-content:space-between}.cmdr-gap-1{gap:.25rem}.cmdr-justify-self-start{justify-self:start}
\ No newline at end of file
diff --git a/contributing/.obsidian/plugins/copy-publish-url/data.json b/contributing/.obsidian/plugins/copy-publish-url/data.json
new file mode 100644
index 00000000..ef680a59
--- /dev/null
+++ b/contributing/.obsidian/plugins/copy-publish-url/data.json
@@ -0,0 +1,9 @@
+{
+  "homeNote": "Welcome",
+  "publishPath": "https://publish.obsidian.md/tasks-contributing/",
+  "enableContext": true,
+  "enableOpenUrl": true,
+  "enableGithub": false,
+  "remoteUrl": "",
+  "branch": "main"
+}
\ No newline at end of file
diff --git a/contributing/.obsidian/plugins/copy-publish-url/main.js b/contributing/.obsidian/plugins/copy-publish-url/main.js
new file mode 100644
index 00000000..db7e4f33
--- /dev/null
+++ b/contributing/.obsidian/plugins/copy-publish-url/main.js
@@ -0,0 +1,6 @@
+/*
+THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
+If you want to view the source, visit the plugins’ github repository.
+*/
+
+var d=Object.create;var o=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var C=Object.getPrototypeOf,y=Object.prototype.hasOwnProperty;var p=s=>o(s,"__esModule",{value:!0});var P=(s,t)=>{p(s);for(var e in t)o(s,e,{get:t[e],enumerable:!0})},U=(s,t,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of f(t))!y.call(s,i)&&i!=="default"&&o(s,i,{get:()=>t[i],enumerable:!(e=g(t,i))||e.enumerable});return s},c=s=>U(p(o(s!=null?d(C(s)):{},"default",s&&s.__esModule&&"default"in s?{get:()=>s.default,enumerable:!0}:{value:s,enumerable:!0})),s);P(exports,{default:()=>u});var l=c(require("obsidian"));var a=c(require("obsidian")),r=class extends a.PluginSettingTab{constructor(t,e){super(t,e);this.plugin=e}display(){let{containerEl:t}=this,{settings:e}=this.plugin;t.empty(),t.createEl("h2",{text:"Publish and GitHub URL plugin settings"}),new a.Setting(t).setName("Index note of your published vault").setDesc("Please use the relative path from the vault root. You do not need to include the .md extension.").addText(i=>{i.setPlaceholder("Index").setValue(e.homeNote).onChange(async n=>{n.trim().slice(-3)===".md"?e.homeNote=n.trim().slice(0,-3):e.homeNote=n.trim(),await this.plugin.saveSettings()})}),new a.Setting(t).setName("Publish base path").setDesc("Please enter the base path of your publish site.").addText(i=>{i.setPlaceholder("https://publish.obsidian.md/help/").setValue(e.publishPath).onChange(async n=>{n.trim().slice(-1)==="/"?e.publishPath=n.trim():e.publishPath=n.trim()+"/",await this.plugin.saveSettings()})}),new a.Setting(t).setName("Show in file menu").setDesc("Enable it to show the Copy Publish URL action in the file menu.").addToggle(i=>{i.setValue(e.enableContext),i.onChange(async n=>{e.enableContext=n,await this.plugin.saveSettings(),n?this.plugin.fileMenuEvent(!0):this.plugin.fileMenuEvent(!1)})}),new a.Setting(t).setName("Open current note in browser").setDesc("Enable it to get a command to open the current note on the Obsidian Publish site.").addToggle(i=>{i.setValue(this.plugin.settings.enableOpenUrl),i.onChange(async n=>{this.plugin.settings.enableOpenUrl=n,await this.plugin.saveSettings(),n?this.plugin.addCommand(this.plugin.returnOpenCommand()):this.app.commands.removeCommand(`${this.plugin.manifest.id}:open-publish-url`)})}),new a.Setting(t).setName("GitHub Commit History Command").setDesc("Enable it to add a command for opening the commit history of the current note on GitHub.").addToggle(i=>{i.setValue(e.enableGithub),i.onChange(async n=>{e.enableGithub=n,await this.plugin.saveSettings(),n?this.plugin.addCommand(this.plugin.returnGithubOpenCommand()):this.app.commands.removeCommand(`${this.plugin.manifest.id}:open-git-history`),this.display()})}),this.plugin.settings.enableGithub&&(new a.Setting(t).setName("GitHub repository URL").setDesc("Please enter the URL of your GitHub repository.").addText(i=>{i.setPlaceholder("https://github.com/obsidian-community/obsidian-hub").setValue(e.remoteUrl).onChange(async n=>{n.trim().slice(-1)==="/"?e.remoteUrl=n.trim():e.remoteUrl=n.trim()+"/",await this.plugin.saveSettings()})}),new a.Setting(t).setName("Branch name").setDesc("Please enter the branch name.").addText(i=>{i.setPlaceholder("main").setValue(e.branch).onChange(async n=>{e.branch=n.trim(),await this.plugin.saveSettings()})}))}};var w={homeNote:"",publishPath:"",enableContext:!1,enableOpenUrl:!0,enableGithub:!1,remoteUrl:"",branch:"main"};function m(s,t){let e=s.metadataCache.getFileCache(t),i=e==null?void 0:e.frontmatter;if(i!==void 0)try{return i.publish!==!1}catch(n){return!0}else return!0}var u=class extends l.Plugin{constructor(){super(...arguments);this.returnOpenCommand=()=>({id:"open-publish-url",name:"Open Publish URL in browser",checkCallback:this.giveCallback(this.openPublishUrl.bind(this))});this.returnCopyCommand=()=>({id:"copy-publish-url",name:"Copy Publish URL",checkCallback:this.giveCallback(this.copyPublishUrl.bind(this))});this.returnGithubOpenCommand=()=>({id:"open-git-history",name:"Open Commit History on GitHub",checkCallback:this.giveGithubCallback(this.openGithubHistory.bind(this))});this.fileMenuCallbackFunc=(t,e,i)=>{if(e instanceof l.TFile){if(m(this.app,e)){t.addSeparator();let h=e.path;t.addItem(b=>{b.setTitle("Copy Publish URL").setIcon("link").onClick(async()=>{await this.copyPublishUrl(h)})})}else return!1;t.addSeparator()}}}getPublishUrl(t){let e=this.settings.publishPath,i=t.slice(0,-3);return i===this.settings.homeNote&&i.includes("/")&&(i=i.split("/").last()),e=encodeURI(e+i),e=e.replace(/%20/g,"+"),e}async copyPublishUrl(t){let e=this.getPublishUrl(t);await navigator.clipboard.writeText(e),new l.Notice("Publish Url copied to your clipboard")}openPublishUrl(t){let e=this.getPublishUrl(t);window.open(e)}openGithubHistory(t){let i=this.settings.remoteUrl+`commits/${this.settings.branch}/${t}`,n=encodeURI(i);window.open(n)}giveCallback(t){return e=>{let i=this.app.workspace.getActiveFile();return i?(e||(async()=>{if(!m(this.app,i)){new l.Notice("This note contains the publish: false flag.");return}let h=i.path;await t(h)})(),!0):!1}}giveGithubCallback(t){return e=>{let i=this.app.workspace.getActiveFile();return i?(e||(async()=>{let n=i.path;await t(n)})(),!0):!1}}fileMenuEvent(t){t?this.registerEvent(this.app.workspace.on("file-menu",this.fileMenuCallbackFunc)):this.app.workspace.off("file-menu",this.fileMenuCallbackFunc)}async onload(){console.log("loading Copy Publish URL plugin"),await this.loadSettings(),this.addCommand(this.returnCopyCommand()),this.settings.enableOpenUrl&&this.addCommand(this.returnOpenCommand()),this.settings.enableContext&&this.fileMenuEvent(!0),this.settings.enableGithub&&this.addCommand(this.returnGithubOpenCommand()),this.addSettingTab(new r(this.app,this))}onunload(){console.log("unloading Copy Publish URL plugin")}async loadSettings(){this.settings=Object.assign({},w,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};
diff --git a/contributing/.obsidian/plugins/copy-publish-url/manifest.json b/contributing/.obsidian/plugins/copy-publish-url/manifest.json
new file mode 100644
index 00000000..e2b48d13
--- /dev/null
+++ b/contributing/.obsidian/plugins/copy-publish-url/manifest.json
@@ -0,0 +1,10 @@
+{
+	"id": "copy-publish-url",
+	"name": "Publish and GitHub URL",
+	"version": "0.4.4",
+	"minAppVersion": "0.12.15",
+	"description": "Copy or open the URL of the corresponding note on your Publish site. You can also open its Git commit history on GitHub.",
+	"author": "kometenstaub",
+	"authorUrl": "https://github.com/kometenstaub",
+	"isDesktopOnly": false
+}
diff --git a/contributing/Dependencies/Dependency Groups.md b/contributing/Dependencies/Dependency Groups.md
index f4507252..731a035d 100644
--- a/contributing/Dependencies/Dependency Groups.md
+++ b/contributing/Dependencies/Dependency Groups.md
@@ -1,9 +1,13 @@
 # Dependency Groups
 
 Several dependencies come in groups (for example, `@typescript/eslint*` or ones containing the word `jest`) that may need to be updated together.
+
 For example, `ts-jest` relies on having a matching major version with `jest` and its types (`@types/jest`).
+
 Every jest-related package that shares a major version number with `ts-jest`, `jest` etc must have an available upgrade to the new major version before any of them can be upgraded.
+
 Otherwise, automated testing may fail due to version mismatch.
 
 Dependabot does not know how to handle groups like this, so the maintainer must keep track of this.
+
 `yarn outdated` is a useful command-line tool for seeing whether there are upgrades available.
diff --git a/contributing/Dependencies/Notes and Special Cases.md b/contributing/Dependencies/Notes and Special Cases.md
index 2fe941b3..a5b1cff5 100644
--- a/contributing/Dependencies/Notes and Special Cases.md
+++ b/contributing/Dependencies/Notes and Special Cases.md
@@ -1,12 +1,18 @@
 # Notes and Special Cases
 
 Dependabot will not offer PRs for every package.
+
 For example, if a package is pinned to an exact version (for example,`obsidian`) that is far behind the current, dependabot may not give any notification of an upgrade.
+
 Use `yarn outdated` every so often to see if there are any upgrades available.
 
 Note that yarn's `upgrade` and `upgrade-interactive` commands will respect the project's `package.json` which pins either major versions or a specific number.
+
 `yarn outdated` gives a better indication of what upgrades are available.
+
 To upgrade major version numbers or a dependency where a fixed version is used, you must
-manually edit the `package.json` file. **Note**: Remember `yarn` after any edits to `package.json` to ensure the `yarn.lock` file is updated.
+manually edit the `package.json` file.
+
+**Note**: Remember `yarn` after any edits to `package.json` to ensure the `yarn.lock` file is updated.
 
 Updates to the `obsidian` package may require additional changes to [manifest.json](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/manifest.json) and [versions.json](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/versions.json) and should be handled with care so that Tasks users who are not on the latest version of Obsidian have time to update.
diff --git a/contributing/Dependencies/Overview of dependencies and `package.json`.md b/contributing/Dependencies/Overview of dependencies and `package.json`.md
index ee274db4..aefa75e1 100644
--- a/contributing/Dependencies/Overview of dependencies and `package.json`.md
+++ b/contributing/Dependencies/Overview of dependencies and `package.json`.md
@@ -1,19 +1,36 @@
 # Overview of dependencies and `package.json`
 
+## package.json
+
 The [package.json](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/package.json) file is the human-editable interface for dependency management.
+
 Acceptable dependency versions are specified using [semver](https://semver.org/) version ranges.
 This project pins certain dependencies to exact version numbers, and others to major version ranges.
 
 After any chance to the `package.json` file, either by manual edit or by a command such as `yarn add -D newDevDependency` and then saving the file, run `yarn` to update the `yarn.lock` file.
+
+## yarn.lock
+
 The `yarn.lock` file is machine-generated - based on the version constraints in `package.json` - and should not be edited manually.
+
 It lists the specific versions of all installed dependencies (and their dependencies, recursively)
 currently used by the project.
 
 If there are ever git merge conflicts in `yarn.lock`, best practice is to rename the conflicted file, ensure `package.json` is correct, and then run `yarn` to regenerate the `yarn.lock` file.
+
 The newly-generated file can then be committed to resolve the merge conflict.
 
+## dependencies and devDependencies
+
 `package.json` separates dependencies only used in the development, testing, and building process ("devDependencies") from those contained as part of the plugin's `main.js` file because they are used at runtime ("dependencies").
+
+### Obsidian and codemirror dependencies
+
 However, Obsidian's plugin architecture handles linking in the Obsidian API and its dependencies (such as `@codemirror/*` packages), so those are not part of the runtime "dependencies" in `package.json` and must also be marked as "external" in the build system configuration (`esbuild.config.mjs`).
+
 Some subset of the packages marked "external" in [esbuild.config.mjs](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/esbuild.config.mjs) will be listed as "devDependencies" because their APIs are used in the plugin or its tests.
+
 Therefore, "devDependency" vs. "dependency" separation is not a sufficient indicator of whether a package
 needs manual "[[How do I smoke-test the Tasks plugin|smoke testing]]" of runtime behavior.
+
+See also [[Thought-Process for Deciding Whether a Dependency Needs Manual Testing]].
diff --git a/contributing/Dependencies/Summary of Dependency Upgrades and Dependabot.md b/contributing/Dependencies/Summary of Dependency Upgrades and Dependabot.md
index ee9e2a5c..bcc77d3a 100644
--- a/contributing/Dependencies/Summary of Dependency Upgrades and Dependabot.md
+++ b/contributing/Dependencies/Summary of Dependency Upgrades and Dependabot.md
@@ -1,11 +1,16 @@
 # Summary of Dependency Upgrades and Dependabot
 
 Keeping dependencies up to date ensures the best experience and security for Tasks plugin users.
+
 This project uses Dependabot to help automate dependency updates, but some dependencies require manual testing
 before they can be merged.
+
 See [[How do I smoke-test the Tasks plugin]] for how to manually test.
 
-Multiple depdendency upgrades can be smoke-tested together in a batch.
+Multiple dependency upgrades can be smoke-tested together in a batch.
+
 An easy way to do this is to make a local branch that merges _only the changes to the `package.json`_ from each of the different upgrade PRs (either manually by examining the change to `package.json` in each PR and then editing your local version to match, or via git).
+
 Then run `yarn` to update the `yarn.lock` file, and this should avoid merge conflicts.
+
 After linting, testing, and smoke-testing (using `yarn build:dev` and manually copying the `main.js` to the local test vault), you can merge the individual upgrade PRs (leaving time for dependabot to rebase them between each individual merge) and delete the local branch.
diff --git a/docs/advanced/styling.md b/docs/advanced/styling.md
index d77006a7..f708f1ef 100644
--- a/docs/advanced/styling.md
+++ b/docs/advanced/styling.md
@@ -7,21 +7,398 @@ has_toc: false
 ---
 
 # Styling Tasks
+{: .no_toc }
 
-Each task entry has CSS styles that allow you to change the look and feel of how the tasks are displayed. The
-following styles are available.
+<details open markdown="block">
+  <summary>
+    Table of contents
+  </summary>
+  {: .text-delta }
+1. TOC
+{:toc}
+</details>
+
+---
+
+## Introduction
+
+In rendered queries and Reading View, the Tasks plugin adds detailed CSS classes and data attributes that represent many of each task's content, to allow for very extensive styling options via CSS.
+Not only each component in a rendered task line is tagged with classes to differentiate it, many components also add classes and data attributes that represent the actual content of the task, so CSS rules can refer to data such as the relative due date of a task or its specific priority.
+
+## Basic Task Structure
+
+{: .released }
+The following description relates to a restructuring of the rendered tasks that was introduced in Tasks X.Y.Z.
+
+The Tasks plugin renders a task in the following structure (this refers to query results, but the Reading View is the same except the top-most containers):
+
+```markdown
+- Obsidian code block (div class="block-language-tasks")
+  - Results list (ul class="plugin-tasks-query-result") OR Reading View list (ul class="contains-task-list")
+    - Task (li class="task-list-item" + attributes like data-task-priority="medium" data-task-due="past-1d" + data-task="[custom_status]" + data-line="[line]")
+      - Task checkbox (li class="task-list-item-checkbox")
+      - Task content (span class="tasks-list-text")
+        - Task description and tags (span class="task-description")
+          - Internal span
+            - Each tag in the description is wrapped in <a href class="tag" data-tag-name="[tag-name]">
+        - Task priority (span class="task-priority" + data-task-priority attribute)
+          - Internal span
+        - Task recurrence rule (span class="task-recurring")
+          - Internal span
+        - Task created date (span class="task-created" + data-task-created attribute)
+          - Internal span
+        - ... start date, scheduled date, due date and done date in this order
+      - Task extras (link, edit button) (span class="task-extras")
+  - Tasks count (div class="tasks-count")
+```
+
+As can be seen above, the basic task `li` contains a checkbox and a content span.
+The content span contains a list of **component** spans: description, priority, recurrence, created date, start date, scheduled date, due date and done date in this order.
+
+Each component span is marked with a **generic class**, which denotes the type of the component, and in some cases a **data attribute** that represents the component's content itself.
+
+Within each component span there is an additional "internal" span, which is the one holding the actual component text.
+The reason for this additional internal span is that it allows CSS styles that closely wrap the text itself, rather than its container box, e.g. for the purpose of drawing a highlight or a box that is exactly in the size of the text.
+
+## Generic Classes and Data Attributes
+
+{: .released }
+Data attributes were introduced in Tasks X.Y.Z.
+
+Each rendered task component (description, priority, recurrence rule etc) includes a **generic class** that denotes this type of component.
+The generic classes are:
+
+- `task-description`
+- `task-priority`
+- `task-due`
+- `task-created`
+- `task-start`
+- `task-scheduled`
+- `task-done`
+- `task-recurring`
+
+In addition to the generic classes, there are [**data attributes**](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) that represent the content of the various task components.
+
+A **priority data attributes** named `data-task-priority` represents the specific priority of a class. It can be `high`, `medium`, `low` or `normal`.
+The `normal` value is special: it is added as a default to a task's upper `task-list-item` even if there is no priority field set for that task.
+
+A **date attribute** represents a due, created, start, scheduled or done date in a format relative to the current date.
+The date attributes are `data-task-due`, `data-task-created`, `data-task-start`, `data-task-scheduled` and `data-task-done` and are populated with a relative expression that denotes the number of days this field represents compared to today:
+
+- `data-task-due="today"` (or `data-task-start="today"`, `data-task-start="today"` etc) represents today.
+- `data-task-due="future-1d"` (or `data-task-start="future-1d"`) represents 1 day in the future, i.e. tomorrow.
+- `data-task-due="past-1d"` (or `data-task-start="past-1d"`) represents 1 day in the past, i.e. yesterday.
+- These attributes are added up to 7 days in the future or past, e.g. `data-task-scheduled="future-7d"` or `data-task-due="past-7d"`.
+- Dates that are further than 7 days in the future or past are given a `far` postfix, e.g. `data-task-scheduled="future-far"` or `data-task-due="past-far"`.
+
+A **tag data attribute** repeats each tag's content as a data attribute, for the purpose of applying formatting according to specific tags.
+The tag `<a>` elements are added a `data-tag-name` attribute with a *sanitized* version of the tag name, which basically means that characters that are illegal to use in HTML attributes (e.g. `&`, `"`) are replaced with dashes.
+
+Data attributes are added to both their corresponding components (e.g. to the due date component) and also to the complete task `li`, to make it easy for a CSS rule to style a complete task according to some property (e.g. color differently the complete task if it's due today, color a task according to a tag) or just one relevant component.
+
+An exception is the tag data attribute which is added only to the tag's `<a>` element within the rendered description -- however you can still use a CSS `:has` selector to format an entire task's description according to a tag, as demonstrated in the examples below.
+
+{: .warning }
+The CSS `:has` selector is available with Obsidian installer version 1.1.9 and newer. You can run the Obsidian command `Show debug info` to see your current installer version.
+
+**Tip:** [CSS wildcard selectors](https://www.geeksforgeeks.org/wildcard-selectors-and-in-css-for-classes/) are a good way to select all past dates or future dates at once -- just use `.task-due[data-task-due^="past-"]` to address all overdue tasks, for example. Examples that utilize this can be found below.
+
+## Hidden Components, Groups & Short Mode
+
+**Hidden components**, e.g. a `hide priority` line in a query, will generate the following:
+
+- The query container (`class="plugin-tasks-query-result"`) will include a `tasks-layout-hide...` class, e.g. `tasks-layout-hide-priority`.
+- Although the priority will not be rendered in the query, the upper task element (`li class="task-list-item"`) will still be added the attribute of hidden components, e.g. `data-task-priority="high"`.
+
+**Short mode** will add a `tasks-layout-short-mode` class to the query container.
+
+**Grouping rules** will add a `data-task-group-by` attribute to the query container, e.g. `data-task-group-by="due,scheduled"`.
+
+## More Classes
+
+The following additional components have the following classes:
 
 | Class                          | Usage                                                                                                           |
 | ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
-| plugin-tasks-query-result      | This is applied to the UL used to hold all the tasks, each task is stored in a LI.                              |
 | plugin-tasks-query-explanation | This is applied to the PRE showing the query's explanation when the `explain` instruction is used.              |
-| plugin-tasks-list-item         | This is applied to the LI that holds each task and the INPUT element for it.                                    |
 | tasks-backlink                 | This is applied to the SPAN that wraps the backlink if displayed on the task.                                   |
 | tasks-edit                     | This is applied to the SPAN that wraps the edit button/icon shown next to the task that opens the task edit UI. |
 | tasks-urgency                  | This is applied to the SPAN that wraps the urgency score if displayed on the task.                              |
-| task-list-item-checkbox        | This is applied to the INPUT element for the task.                                                              |
 | tasks-group-heading            | This is applied to H4, H5 and H6 group headings                                                                 |
 
 {: .released }
 `tasks-group-heading` was introduced in Tasks 1.6.0.<br>
 `plugin-tasks-query-explanation` was introduced in Tasks 1.19.0.
+
+## Examples
+
+The following examples can be used as [Obsidian CSS snippets](https://help.obsidian.md/How+to/Add+custom+styles#Use+Themes+and+or+CSS+snippets).
+
+**Tip:** the following examples use CSS variables (`--var(...)`) provided by Obsidian instead of concrete color codes to maximize the chance that the result will be in-line with your chosen theme. You may of course use specific colors if so you choose.
+
+### General Formatting
+
+Making tags, internal links and the recurrence rules of tasks to appear in gray:
+
+```css
+.tasks-list-text a.tag {
+ color: var(--list-marker-color);
+}
+.tasks-backlink a.internal-link {
+ color: var(--list-marker-color);
+}
+.task-recurring {
+ color: var(--list-marker-color);
+}
+```
+
+### Priority as a Checkbox Color
+
+The following rules remove the Tasks priority emoticon and render the tasks' checkboxes in red, orange, blue and cyan according to the tasks' priority:
+
+```css
+.task-list-item[data-task-priority="high"] input[type=checkbox] {
+ box-shadow: 0px 0px 2px 2px var(--color-red);
+ border-color: var(--color-red);
+}
+.task-list-item[data-task-priority="medium"] input[type=checkbox] {
+    box-shadow: 0px 0px 2px 2px var(--color-orange);
+    border-color: var(--color-orange);
+}
+.task-list-item[data-task-priority="normal"] input[type=checkbox] {
+ box-shadow: 0px 0px 2px 2px var(--color-blue);
+ border-color: var(--color-blue);
+}
+.task-list-item[data-task-priority="low"] input[type=checkbox] {
+ box-shadow: 0px 0px 2px 2px var(--color-cyan);
+ border-color: var(--color-cyan);
+}
+/* This part removes the regular priority emoticon */
+span.task-priority {
+ display: none;
+}
+```
+
+### Styling Tasks with Custom Statuses
+
+To create a green halo around the checkbox of tasks with a `/` custom status, add the following CSS snippet:
+
+```css
+li.task-list-item[data-task="/"] .task-list-item-checkbox {
+        box-shadow: 0 0 10px green;
+}
+```
+
+### Colors for Due Today and Overdue
+
+The following rules mark 'today' due dates as blue and past due dates as red:
+
+```css
+/* A special color for the 'due' component if it's for today */
+.task-due[data-task-due="today"] span {
+ background: var(--code-property);
+ border-radius: 10px;
+ padding: 2px 8px;
+}
+/* A special color for overdue due dates */
+.task-due[data-task-due^="past-"] span {
+ background: var(--color-pink);
+ border-radius: 10px;
+ padding: 2px 8px;
+}
+```
+
+### Highlight for a Specific Tag
+
+The following rule adds a green glow around `#task/atHome` tags inside the description:
+
+```css
+a.tag[data-tag-name="#task/atHome"] {
+    box-shadow: 0 0 5px green;
+}
+```
+
+The following rule adds a rounded red background to the description of a task if it contains the tag `#task/strategic`:
+
+```css
+.task-description span:has(.tag[data-tag-name="#task/strategic"]) {
+ background: #ffbfcc;
+ border-radius: 10px;
+ padding: 2px 8px;
+}
+```
+
+### Circle Checkboxes
+
+The following renders checkboxes as circles instead of squares:
+
+```css
+ul > li.plugin-tasks-list-item .task-list-item-checkbox {
+     margin-inline-start: 0;
+  margin: 5px 2px;
+  border-radius: 50%;
+}
+```
+
+### Grid Layout
+
+The following organizes the task structure into a 3-line grid, on which:
+
+- the description is in the first line,
+- and the various components are on the second,
+- the urgency, backlink and edit button are, if displayed, on the third.
+
+```css
+ul > li.plugin-tasks-list-item {
+    grid-template-columns: 25px auto;
+    display: grid;
+    align-items: top;
+}
+span.task-description {
+    grid-row: 1;
+ grid-column: 1/10;
+}
+span.tasks-backlink {
+    grid-row: 2;
+    grid-column: 2;
+ font-size: small;
+}
+span.task-recurring {
+    grid-row: 2;
+ font-size: small;
+ width: max-content;
+}
+span.task-due {
+    grid-row: 2;
+ font-size: small;
+ width: max-content;
+}
+span.task-done {
+    grid-row: 2;
+ font-size: small;
+ width: max-content;
+}
+.tasks-list-text {
+    position: relative;
+    display: inline-grid;
+ width: max-content;
+ grid-column-gap: 10px;
+}
+span.task-extras {
+    grid-row: 2;
+    grid-column: 2;
+ font-size: small;
+}
+```
+
+### Complete Example
+
+The following can be used as a base for a full CSS snippet:
+
+```css
+/* I like tags to appear in gray so they won't grab too much attention */
+.tasks-list-text a.tag {
+ color: var(--list-marker-color);
+}
+
+/* Set internal links to gray too instead of Obsidian's default */
+.tasks-backlink a.internal-link {
+ color: var(--list-marker-color);
+}
+
+/* Paint the recurrence rule in gray so it will be less distracting */
+.task-recurring {
+ color: var(--list-marker-color);
+}
+
+/* List indentation values that seem to work well for me */
+ul.contains-task-list.plugin-tasks-query-result {
+    padding: 0 10px;
+}
+
+/* This seems to be needed for the task description to word-wrap correctly if they're too long */
+span.tasks-list-text {
+ width: auto;
+}
+
+/* Represent tasks' priority with colorful round checkboxes instead of the priority emoticons */
+.task-list-item[data-task-priority="high"] input[type=checkbox] {
+ box-shadow: 0px 0px 2px 2px var(--color-red);
+ border-color: var(--color-red);
+}
+.task-list-item[data-task-priority="low"] input[type=checkbox] {
+ box-shadow: 0px 0px 2px 2px var(--color-blue);
+ border-color: var(--color-blue);
+}
+.task-list-item[data-task-priority="medium"] input[type=checkbox] {
+ box-shadow: 0px 0px 2px 2px var(--color-orange);
+ border-color: var(--color-orange);
+}
+/* This part removes the regular priority emoticon */
+span.task-priority {
+ display: none;
+}
+
+/* A special color for the 'due' component if it's for today */
+.task-due[data-task-due="today"] span {
+ background: var(--code-property);
+ border-radius: 10px;
+ padding: 2px 8px;
+}
+/* A special color for overdue due dates */
+.task-due[data-task-due^="past-"] span {
+ background: var(--color-pink);
+ border-radius: 10px;
+ padding: 2px 8px;
+}
+
+/* Make checkboxes a circle instead of a square */
+ul > li.plugin-tasks-list-item .task-list-item-checkbox {
+     margin-inline-start: 0;
+  margin: 5px 2px;
+  border-radius: 50%;
+}
+
+/* The following section organizes the task components in a grid, so the description will be on the first row
+ * of each item and most components will be in the 2nd row. */
+ul > li.plugin-tasks-list-item {
+    grid-template-columns: 25px auto;
+    display: grid;
+    align-items: top;
+}
+span.task-description {
+    grid-row: 1;
+ grid-column: 1/10;
+}
+span.tasks-backlink {
+    grid-row: 2;
+    grid-column: 2;
+ font-size: small;
+}
+span.task-recurring {
+    grid-row: 2;
+ font-size: small;
+ width: max-content;
+}
+span.task-due {
+    grid-row: 2;
+ font-size: small;
+ width: max-content;
+}
+span.task-done {
+    grid-row: 2;
+ font-size: small;
+ width: max-content;
+}
+.tasks-list-text {
+    position: relative;
+    display: inline-grid;
+ width: max-content;
+ grid-column-gap: 10px;
+}
+span.task-extras {
+    grid-row: 2;
+    grid-column: 2;
+ font-size: small;
+}
+```
diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md
index d15c5399..049f38b5 100644
--- a/docs/getting-started/index.md
+++ b/docs/getting-started/index.md
@@ -149,6 +149,22 @@ Completing a task by clicking its checkbox from a `tasks` query block _will_ wor
 Warning
 {: .label .label-yellow}
 
+Tasks cannot read tasks that are in **Obsidian Canvas cards**.
+
+---
+
+Warning
+{: .label .label-yellow}
+
+Tasks does not display Tasks query blocks that are in **Obsidian Canvas cards**.
+
+We are tracking this in [issue #1732](https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1732).
+
+---
+
+Warning
+{: .label .label-yellow}
+
 Tasks cannot read tasks that are **inside code blocks**, such as the ones used by the **Admonitions plugin**. Use Obsidian's built-in callouts instead.
 
 ---
diff --git a/manifest.json b/manifest.json
index d3859d3f..a1b56abf 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,10 +1,11 @@
 {
   "id": "obsidian-tasks-plugin",
   "name": "Tasks",
-  "version": "2.0.0",
+  "version": "2.0.1",
   "minAppVersion": "0.14.6",
   "description": "Task management for Obsidian",
   "author": "Martin Schenck and Clare Macrae",
   "authorUrl": "https://github.com/obsidian-tasks-group",
+  "fundingUrl": "https://github.com/sponsors/claremacrae",
   "isDesktopOnly": false
 }
diff --git a/package.json b/package.json
index ae1f1305..ed9a947a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "obsidian-tasks",
-  "version": "2.0.0",
+  "version": "2.0.1",
   "description": "Task management for the Obsidian knowledge base",
   "main": "main.js",
   "scripts": {
diff --git a/src/Config/Settings.ts b/src/Config/Settings.ts
index 6393f16f..7bdaecb4 100644
--- a/src/Config/Settings.ts
+++ b/src/Config/Settings.ts
@@ -1,7 +1,9 @@
+import { makeDefaultSuggestionBuilder } from '../Suggestor/Suggestor';
 import { DEFAULT_SYMBOLS } from '../TaskSerializer/DefaultTaskSerializer';
 import { StatusConfiguration } from '../StatusConfiguration';
 import { Status } from '../Status';
 import { DefaultTaskSerializer, type TaskSerializer } from '../TaskSerializer';
+import type { SuggestionBuilder } from '../Suggestor';
 import { DebugSettings } from './DebugSettings';
 import { StatusSettings } from './StatusSettings';
 import { Feature } from './Feature';
@@ -24,11 +26,17 @@ interface TaskFormat {
     displayName: string;
     /** {@link TaskSerializer} responsible for reading Tasks from text and writing them back into text */
     taskSerializer: TaskSerializer;
+    /** Function that generates Intellisense-like suggestions as a user is typing a Task */
+    buildSuggestions?: SuggestionBuilder;
 }
 
 /** Map of all defined {@link TaskFormat}s */
 export const TASK_FORMATS = {
-    tasksPluginEmoji: { displayName: 'Default', taskSerializer: new DefaultTaskSerializer(DEFAULT_SYMBOLS) },
+    tasksPluginEmoji: {
+        displayName: 'Default',
+        taskSerializer: new DefaultTaskSerializer(DEFAULT_SYMBOLS),
+        buildSuggestions: makeDefaultSuggestionBuilder(DEFAULT_SYMBOLS),
+    },
 } as const;
 
 export type TASK_FORMATS = typeof TASK_FORMATS; // For convenience to make some typing easier
diff --git a/src/InlineRenderer.ts b/src/InlineRenderer.ts
index 977f1941..8ea225ac 100644
--- a/src/InlineRenderer.ts
+++ b/src/InlineRenderer.ts
@@ -11,6 +11,10 @@ export class InlineRenderer {
 
     public markdownPostProcessor = this._markdownPostProcessor.bind(this);
 
+    /**
+     * This renders a file's task list when rendered in Reading View, using roughly the same pipeline
+     * of QueryRenderer (e.g. it removes the global filter and handles other formatting).
+     */
     private async _markdownPostProcessor(element: HTMLElement, context: MarkdownPostProcessorContext): Promise<void> {
         const { globalFilter } = getSettings();
         const renderedElements = element.findAll('.task-list-item').filter((taskItem) => {
diff --git a/src/Query/Group.ts b/src/Query/Group.ts
index 377caf5a..525a53f6 100644
--- a/src/Query/Group.ts
+++ b/src/Query/Group.ts
@@ -156,7 +156,7 @@ export class Group {
         if (filename === null) {
             return ['Unknown Location'];
         }
-        return ['[[' + Group.escapeMarkdownCharacters(filename) + ']]'];
+        return ['[[' + filename + ']]'];
     }
 
     private static groupByRoot(task: Task): string[] {
diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts
index 7aca89a2..8c2e5838 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -10,6 +10,7 @@ import { TaskModal } from './TaskModal';
 import type { TasksEvents } from './TasksEvents';
 import type { Task } from './Task';
 import { DateFallback } from './DateFallback';
+import { TaskLayout } from './TaskLayout';
 
 export class QueryRenderer {
     private readonly app: App;
@@ -25,18 +26,15 @@ export class QueryRenderer {
     public addQueryRenderChild = this._addQueryRenderChild.bind(this);
 
     private async _addQueryRenderChild(source: string, element: HTMLElement, context: MarkdownPostProcessorContext) {
-        const child = new QueryRenderChild({
-            app: this.app,
-            events: this.events,
-            container: element,
-            source,
-            filePath: context.sourcePath,
-        });
-
-        context.addChild(child);
-
-        child.load();
-        child.onload();
+        context.addChild(
+            new QueryRenderChild({
+                app: this.app,
+                events: this.events,
+                container: element,
+                source,
+                filePath: context.sourcePath,
+            }),
+        );
     }
 }
 
@@ -188,8 +186,12 @@ class QueryRenderChild extends MarkdownRenderChild {
     }): Promise<{ taskList: HTMLUListElement; tasksCount: number }> {
         const tasksCount = tasks.length;
 
+        const layout = new TaskLayout(this.query.layoutOptions);
         const taskList = content.createEl('ul');
         taskList.addClasses(['contains-task-list', 'plugin-tasks-query-result']);
+        taskList.addClasses(layout.specificClasses);
+        const groupingAttribute = this.getGroupingAttribute();
+        if (groupingAttribute && groupingAttribute.length > 0) taskList.dataset.taskGroupBy = groupingAttribute;
         for (let i = 0; i < tasksCount; i++) {
             const task = tasks[i];
             const isFilenameUnique = this.isFilenameUnique({ task });
@@ -199,6 +201,7 @@ class QueryRenderChild extends MarkdownRenderChild {
                 listIndex: i,
                 layoutOptions: this.query.layoutOptions,
                 isFilenameUnique,
+                taskLayout: layout,
             });
 
             // Remove all footnotes. They don't re-appear in another document.
@@ -207,16 +210,18 @@ class QueryRenderChild extends MarkdownRenderChild {
 
             const shortMode = this.query.layoutOptions.shortMode;
 
+            const extrasSpan = listItem.createSpan('task-extras');
+
             if (!this.query.layoutOptions.hideUrgency) {
-                this.addUrgency(listItem, task);
+                this.addUrgency(extrasSpan, task);
             }
 
             if (!this.query.layoutOptions.hideBacklinks) {
-                this.addBacklinks(listItem, task, shortMode, isFilenameUnique);
+                this.addBacklinks(extrasSpan, task, shortMode, isFilenameUnique);
             }
 
             if (!this.query.layoutOptions.hideEditButton) {
-                this.addEditButton(listItem, task);
+                this.addEditButton(extrasSpan, task);
             }
 
             taskList.appendChild(listItem);
@@ -353,4 +358,12 @@ class QueryRenderChild extends MarkdownRenderChild {
 
         return allFilesWithSameName.length < 2;
     }
+
+    private getGroupingAttribute() {
+        const groupingRules: string[] = [];
+        for (const group of this.query.grouping) {
+            groupingRules.push(group.property);
+        }
+        return groupingRules.join(',');
+    }
 }
diff --git a/src/Suggestor/EditorSuggestorPopup.ts b/src/Suggestor/EditorSuggestorPopup.ts
index 1c8de52d..27d2be0f 100644
--- a/src/Suggestor/EditorSuggestorPopup.ts
+++ b/src/Suggestor/EditorSuggestorPopup.ts
@@ -1,10 +1,9 @@
 import { App, Editor, EditorSuggest, TFile } from 'obsidian';
 import type { EditorPosition, EditorSuggestContext, EditorSuggestTriggerInfo } from 'obsidian';
 
-import type { Settings } from '../Config/Settings';
+import { type Settings, getUserSelectedTaskFormat } from '../Config/Settings';
 import * as task from '../Task';
-import { buildSuggestions } from './Suggestor';
-import type { SuggestInfo } from './Suggestor';
+import type { SuggestInfo } from '.';
 
 export type SuggestInfoWithContext = SuggestInfo & {
     context: EditorSuggestContext;
@@ -38,13 +37,11 @@ export class EditorSuggestor extends EditorSuggest<SuggestInfoWithContext> {
         const line = context.query;
         const currentCursor = context.editor.getCursor();
 
-        const suggestions: SuggestInfo[] = buildSuggestions(line, currentCursor.ch, this.settings);
+        const suggestions: SuggestInfo[] =
+            getUserSelectedTaskFormat().buildSuggestions?.(line, currentCursor.ch, this.settings) ?? [];
 
         // Add the editor context to all the suggestions
-        const suggestionsWithContext: SuggestInfoWithContext[] = [];
-        for (const suggestion of suggestions) suggestionsWithContext.push({ ...suggestion, context: context });
-
-        return suggestionsWithContext;
+        return suggestions.map((s) => ({ ...s, context }));
     }
 
     renderSuggestion(value: SuggestInfoWithContext, el: HTMLElement) {
diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts
index b293b50b..09c42f30 100644
--- a/src/Suggestor/Suggestor.ts
+++ b/src/Suggestor/Suggestor.ts
@@ -1,96 +1,86 @@
 import type { Settings } from '../Config/Settings';
-import { TASK_FORMATS } from '../Config/Settings';
 import { DateParser } from '../Query/DateParser';
 import { doAutocomplete } from '../DateAbbreviations';
 import { Recurrence } from '../Recurrence';
+import type { DefaultTaskSerializerSymbols } from '../TaskSerializer/DefaultTaskSerializer';
 import { TaskRegularExpressions } from '../Task';
-const { symbols } = TASK_FORMATS.tasksPluginEmoji.taskSerializer;
-const datePrefixCharacters = `${symbols.startDateSymbol}${symbols.scheduledDateSymbol}${symbols.dueDateSymbol}`;
+import type { SuggestInfo, SuggestionBuilder } from '.';
 
-/*
- * A suggestion presented to the user and some metadata about it.
- */
-export type SuggestInfo = {
-    suggestionType?: 'match' | 'default' | 'empty';
-    // What to display to the user
-    displayText: string;
-    // What to append to the note
-    appendText: string;
-    // At what index in the line to do the insertion (if not specified, the cursor location is used)
-    insertAt?: number;
-    // How many characters to skip from the original line (e.g. if replacing existing text)
-    insertSkip?: number;
-};
-
-/*
- * Return a list of suggestions, either generic or more fine-grained to the words at the cursor.
- */
-export function buildSuggestions(line: string, cursorPos: number, settings: Settings): SuggestInfo[] {
-    let suggestions: SuggestInfo[] = [];
+export function makeDefaultSuggestionBuilder(symbols: DefaultTaskSerializerSymbols): SuggestionBuilder {
+    const datePrefixCharacters = `${symbols.startDateSymbol}${symbols.scheduledDateSymbol}${symbols.dueDateSymbol}`;
+    /*
+     * Return a list of suggestions, either generic or more fine-grained to the words at the cursor.
+     */
+    return (line: string, cursorPos: number, settings: Settings): SuggestInfo[] => {
+        let suggestions: SuggestInfo[] = [];
 
-    // Step 1: add date suggestions if relevant
-    suggestions = suggestions.concat(addDatesSuggestions(line, cursorPos, settings));
+        // Step 1: add date suggestions if relevant
+        suggestions = suggestions.concat(addDatesSuggestions(line, cursorPos, settings, datePrefixCharacters));
 
-    // Step 2: add recurrence suggestions if relevant
-    suggestions = suggestions.concat(addRecurrenceSuggestions(line, cursorPos, settings));
+        // Step 2: add recurrence suggestions if relevant
+        suggestions = suggestions.concat(addRecurrenceSuggestions(line, cursorPos, settings, symbols.recurrenceSymbol));
 
-    // Step 3: add more general suggestions ('due', 'recurrence' etc)
-    const morePossibleSuggestions = getPossibleComponentSuggestions(line, settings);
-    // We now filter the general suggestions according to the word at the cursor. If there's
-    // something to match, we filter the suggestions accordingly, so the user can get more specific
-    // results according to what she's typing.
-    // If there's no good match, present the suggestions as they are
-    const wordMatch = matchByPosition(line, /([a-zA-Z'_-]*)/g, cursorPos);
-    let addedSuggestions = false;
-    if (wordMatch && wordMatch.length > 0) {
-        const wordUnderCursor = wordMatch[0];
-        if (wordUnderCursor.length >= Math.max(1, settings.autoSuggestMinMatch)) {
-            const filteredSuggestions = morePossibleSuggestions.filter((suggestInfo) =>
-                suggestInfo.displayText.toLowerCase().includes(wordUnderCursor.toLowerCase()),
-            );
-            for (const filtered of filteredSuggestions) {
-                suggestions.push({
-                    suggestionType: 'match',
-                    displayText: filtered.displayText,
-                    appendText: filtered.appendText,
-                    insertAt: wordMatch.index,
-                    insertSkip: wordUnderCursor.length,
-                });
-                addedSuggestions = true;
+        // Step 3: add more general suggestions ('due', 'recurrence' etc)
+        const morePossibleSuggestions = getPossibleComponentSuggestions(line, settings, symbols);
+        // We now filter the general suggestions according to the word at the cursor. If there's
+        // something to match, we filter the suggestions accordingly, so the user can get more specific
+        // results according to what she's typing.
+        // If there's no good match, present the suggestions as they are
+        const wordMatch = matchByPosition(line, /([a-zA-Z'_-]*)/g, cursorPos);
+        let addedSuggestions = false;
+        if (wordMatch && wordMatch.length > 0) {
+            const wordUnderCursor = wordMatch[0];
+            if (wordUnderCursor.length >= Math.max(1, settings.autoSuggestMinMatch)) {
+                const filteredSuggestions = morePossibleSuggestions.filter((suggestInfo) =>
+                    suggestInfo.displayText.toLowerCase().includes(wordUnderCursor.toLowerCase()),
+                );
+                for (const filtered of filteredSuggestions) {
+                    suggestions.push({
+                        suggestionType: 'match',
+                        displayText: filtered.displayText,
+                        appendText: filtered.appendText,
+                        insertAt: wordMatch.index,
+                        insertSkip: wordUnderCursor.length,
+                    });
+                    addedSuggestions = true;
+                }
             }
         }
-    }
-    // That's where we're adding all the suggestions in case there's nothing specific to match
-    // (and we're allowed by the settings to bring back a zero-sized match)
-    if (!addedSuggestions && settings.autoSuggestMinMatch === 0)
-        suggestions = suggestions.concat(morePossibleSuggestions);
+        // That's where we're adding all the suggestions in case there's nothing specific to match
+        // (and we're allowed by the settings to bring back a zero-sized match)
+        if (!addedSuggestions && settings.autoSuggestMinMatch === 0)
+            suggestions = suggestions.concat(morePossibleSuggestions);
 
-    // Unless we have a suggestion that is a match for something the user is currently typing, add
-    // an 'Enter' entry in the beginning of the menu, so an Enter press will move to the next line
-    // rather than insert a suggestion
-    if (suggestions.length > 0 && !suggestions.some((value) => value.suggestionType === 'match')) {
-        // No actual match, only default ones
-        suggestions.unshift({
-            suggestionType: 'empty',
-            displayText: '⏎',
-            appendText: '\n',
-        });
-    }
-
-    // Either way, after all the aggregations above, never suggest more than the max items
-    suggestions = suggestions.slice(0, settings.autoSuggestMaxItems);
+        // Unless we have a suggestion that is a match for something the user is currently typing, add
+        // an 'Enter' entry in the beginning of the menu, so an Enter press will move to the next line
+        // rather than insert a suggestion
+        if (suggestions.length > 0 && !suggestions.some((value) => value.suggestionType === 'match')) {
+            // No actual match, only default ones
+            suggestions.unshift({
+                suggestionType: 'empty',
+                displayText: '⏎',
+                appendText: '\n',
+            });
+        }
 
-    return suggestions;
-}
+        // Either way, after all the aggregations above, never suggest more than the max items
+        suggestions = suggestions.slice(0, settings.autoSuggestMaxItems);
 
-function hasPriority(line: string) {
-    if (Object.values(symbols.prioritySymbols).some((value) => value.length > 0 && line.includes(value))) return true;
+        return suggestions;
+    };
 }
 
 /*
  * Get suggestions for generic task components, e.g. a priority or a 'due' symbol
  */
-function getPossibleComponentSuggestions(line: string, _settings: Settings): SuggestInfo[] {
+function getPossibleComponentSuggestions(
+    line: string,
+    _settings: Settings,
+    symbols: DefaultTaskSerializerSymbols,
+): SuggestInfo[] {
+    const hasPriority = (line: string) =>
+        Object.values(symbols.prioritySymbols).some((value) => value.length > 0 && line.includes(value));
+
     const suggestions: SuggestInfo[] = [];
 
     if (!line.includes(symbols.dueDateSymbol))
@@ -139,7 +129,12 @@ function getPossibleComponentSuggestions(line: string, _settings: Settings): Sug
  * Generic predefined suggestions, in turn, also have two options: either filtered (if the user started typing
  * something where a date is expected) or unfiltered
  */
-function addDatesSuggestions(line: string, cursorPos: number, settings: Settings): SuggestInfo[] {
+function addDatesSuggestions(
+    line: string,
+    cursorPos: number,
+    settings: Settings,
+    datePrefixCharacters: string,
+): SuggestInfo[] {
     const genericSuggestions = [
         'today',
         'tomorrow',
@@ -224,7 +219,7 @@ function addDatesSuggestions(line: string, cursorPos: number, settings: Settings
  * Generic predefined suggestions, in turn, also have two options: either filtered (if the user started typing
  * something where a recurrence is expected) or unfiltered
  */
-function addRecurrenceSuggestions(line: string, cursorPos: number, settings: Settings) {
+function addRecurrenceSuggestions(line: string, cursorPos: number, settings: Settings, recurrenceSymbol: string) {
     const genericSuggestions = [
         'every',
         'every day',
@@ -242,7 +237,7 @@ function addRecurrenceSuggestions(line: string, cursorPos: number, settings: Set
     ];
 
     const results: SuggestInfo[] = [];
-    const recurrenceRegex = new RegExp(`(${symbols.recurrenceSymbol})\\s*([0-9a-zA-Z ]*)`, 'ug');
+    const recurrenceRegex = new RegExp(`(${recurrenceSymbol})\\s*([0-9a-zA-Z ]*)`, 'ug');
     const recurrenceMatch = matchByPosition(line, recurrenceRegex, cursorPos);
     if (recurrenceMatch && recurrenceMatch.length >= 2) {
         const recurrencePrefix = recurrenceMatch[1];
diff --git a/src/Suggestor/index.ts b/src/Suggestor/index.ts
new file mode 100644
index 00000000..1c135a1a
--- /dev/null
+++ b/src/Suggestor/index.ts
@@ -0,0 +1,21 @@
+import type { Settings } from '../Config/Settings';
+
+/*
+ * A suggestion presented to the user and some metadata about it.
+ */
+export type SuggestInfo = {
+    suggestionType?: 'match' | 'default' | 'empty';
+    // What to display to the user
+    displayText: string;
+    // What to append to the note
+    appendText: string;
+    // At what index in the line to do the insertion (if not specified, the cursor location is used)
+    insertAt?: number;
+    // How many characters to skip from the original line (e.g. if replacing existing text)
+    insertSkip?: number;
+};
+
+/*
+ * Return a list of suggestions, either generic or more fine-grained to the words at the cursor.
+ */
+export type SuggestionBuilder = (line: string, cursorPos: number, settings: Settings) => SuggestInfo[];
diff --git a/src/TaskLayout.ts b/src/TaskLayout.ts
index 8d7cb75e..078261c9 100644
--- a/src/TaskLayout.ts
+++ b/src/TaskLayout.ts
@@ -46,7 +46,9 @@ export class TaskLayout {
         'blockLink',
     ];
     public layoutComponents: TaskLayoutComponent[];
+    public hiddenComponents: TaskLayoutComponent[] = [];
     public options: LayoutOptions;
+    public specificClasses: string[] = [];
 
     constructor(options?: LayoutOptions, components?: TaskLayoutComponent[]) {
         if (options) {
@@ -59,7 +61,6 @@ export class TaskLayout {
         } else {
             this.layoutComponents = this.defaultLayout;
         }
-
         this.layoutComponents = this.applyOptions(this.options);
     }
 
@@ -67,13 +68,17 @@ export class TaskLayout {
      * Return a new list of components with the given options applied.
      */
     applyOptions(layoutOptions: LayoutOptions): TaskLayoutComponent[] {
-        // Remove a component from the taskComponents array if the given layoutOption criteria is met
+        // Remove a component from the taskComponents array if the given layoutOption criteria is met,
+        // and add to the layout's specific classes list the class that denotes that this component
+        // isn't in the layout
         const removeIf = (
             taskComponents: TaskLayoutComponent[],
             shouldRemove: boolean,
             componentToRemove: TaskLayoutComponent,
         ) => {
             if (shouldRemove) {
+                this.specificClasses.push(`tasks-layout-hide-${componentToRemove}`);
+                this.hiddenComponents.push(componentToRemove);
                 return taskComponents.filter((element) => element != componentToRemove);
             } else {
                 return taskComponents;
@@ -89,6 +94,7 @@ export class TaskLayout {
         newComponents = removeIf(newComponents, layoutOptions.hideScheduledDate, 'scheduledDate');
         newComponents = removeIf(newComponents, layoutOptions.hideDueDate, 'dueDate');
         newComponents = removeIf(newComponents, layoutOptions.hideDoneDate, 'doneDate');
+        if (layoutOptions.shortMode) this.specificClasses.push('tasks-layout-short-mode');
         return newComponents;
     }
 }
diff --git a/src/TaskLineRenderer.ts b/src/TaskLineRenderer.ts
index 9a236208..9d59f075 100644
--- a/src/TaskLineRenderer.ts
+++ b/src/TaskLineRenderer.ts
@@ -13,8 +13,24 @@ export type TaskLineRenderDetails = {
     listIndex: number;
     layoutOptions?: LayoutOptions;
     isFilenameUnique?: boolean;
+    taskLayout?: TaskLayout;
 };
 
+export const LayoutClasses: { [c in TaskLayoutComponent]: string } = {
+    description: 'task-description',
+    priority: 'task-priority',
+    dueDate: 'task-due',
+    startDate: 'task-start',
+    createdDate: 'task-created',
+    scheduledDate: 'task-scheduled',
+    doneDate: 'task-done',
+    recurrenceRule: 'task-recurring',
+    blockLink: '',
+};
+
+const MAX_DAY_VALUE_RANGE = 7;
+const DAY_VALUE_OVER_RANGE_POSTFIX = 'far';
+
 /**
  * The function used to render a Markdown task line into an existing HTML element.
  */
@@ -47,11 +63,11 @@ export async function renderTaskLine(
     //  when running tests, and we want the tests to be able to create the full div and span structure,
     //  so had to convert all of these to the equivalent but more elaborate document.createElement() and
     //  appendChild() calls.
-
     const textSpan = document.createElement('span');
     li.appendChild(textSpan);
     textSpan.classList.add('tasks-list-text');
-    await taskToHtml(task, renderDetails, textSpan, textRenderer);
+    const attributes = await taskToHtml(task, renderDetails, textSpan, textRenderer);
+    for (const key in attributes) li.dataset[key] = attributes[key];
 
     // NOTE: this area is mentioned in `CONTRIBUTING.md` under "How does Tasks handle status changes". When
     // moving the code, remember to update that reference too.
@@ -98,27 +114,59 @@ async function taskToHtml(
     renderDetails: TaskLineRenderDetails,
     parentElement: HTMLElement,
     textRenderer: TextRenderer,
-) {
-    let taskAsString = '';
+): Promise<AttributesDictionary> {
+    let allAttributes: AttributesDictionary = {};
     const taskLayout = new TaskLayout(renderDetails.layoutOptions);
     const emojiSerializer = TASK_FORMATS.tasksPluginEmoji.taskSerializer;
+    // Render and build classes for all the task's visible components
     for (const component of taskLayout.layoutComponents) {
         let componentString = emojiSerializer.componentToString(task, taskLayout, component);
         if (componentString) {
             if (component === 'description') componentString = removeGlobalFilterIfNeeded(componentString);
-            taskAsString += componentString;
+            // Create the text span that will hold the rendered component
+            const span = document.createElement('span');
+            parentElement.appendChild(span);
+            if (span) {
+                // Inside that text span, we are creating another internal span, that will hold the text itself.
+                // This may seem redundant, and by default it indeed does nothing, but we do it to allow the CSS
+                // to differentiate between the container of the text and the text itself, so it will be possible
+                // to do things like surrouding only the text (rather than its whole placeholder) with a highlight
+                const internalSpan = document.createElement('span');
+                span.appendChild(internalSpan);
+                await renderComponentText(internalSpan, componentString, component, task, textRenderer);
+                const [genericClasses, dataAttributes] = getComponentClassesAndData(component, task);
+                addInternalClasses(component, internalSpan);
+                // Add the generic classes that apply to what this component is (priority, due date etc)
+                span.classList.add(...genericClasses);
+                // Add the attributes to the component ('priority-medium', 'due-past-1d' etc)
+                for (const key in dataAttributes) span.dataset[key] = dataAttributes[key];
+                allAttributes = { ...allAttributes, ...dataAttributes };
+            }
         }
     }
 
-    const { debugSettings } = getSettings();
-    if (debugSettings.showTaskHiddenData) {
-        // Add some debug output to enable hidden information in the task to be inspected.
-        taskAsString += `<br>🐛 <b>${task.lineNumber}</b> . ${task.sectionStart} . ${task.sectionIndex} . '<code>${task.originalMarkdown}</code>'<br>'<code>${task.path}</code>' > '<code>${task.precedingHeader}</code>'<br>`;
+    // Now build classes for the hidden task components without rendering them
+    for (const component of taskLayout.hiddenComponents) {
+        const [_, dataAttributes] = getComponentClassesAndData(component, task);
+        allAttributes = { ...allAttributes, ...dataAttributes };
     }
 
-    await renderComponentText(parentElement, taskAsString, 'description', task, textRenderer);
+    // If a task has no priority field set, its priority will not be rendered as part of the loop above and
+    // it will not be set a priority data attribute.
+    // In such a case we want the upper task LI element to mark the task has a 'normal' priority.
+    // So if the priority was not rendered, force it through the pipe of getting the component data for the
+    // priority field.
+    if (allAttributes.taskPriority === undefined) {
+        const [_, dataAttributes] = getComponentClassesAndData('priority', task);
+        allAttributes = { ...allAttributes, ...dataAttributes };
+    }
+
+    return allAttributes;
 }
 
+/*
+ * Renders the given component into the given HTML span element.
+ */
 async function renderComponentText(
     span: HTMLSpanElement,
     componentString: string,
@@ -127,6 +175,11 @@ async function renderComponentText(
     textRenderer: TextRenderer,
 ) {
     if (component === 'description') {
+        const { debugSettings } = getSettings();
+        if (debugSettings.showTaskHiddenData) {
+            // Add some debug output to enable hidden information in the task to be inspected.
+            componentString += `<br>🐛 <b>${task.lineNumber}</b> . ${task.sectionStart} . ${task.sectionIndex} . '<code>${task.originalMarkdown}</code>'<br>'<code>${task.path}</code>' > '<code>${task.precedingHeader}</code>'<br>`;
+        }
         await textRenderer(componentString, span, task.path);
 
         // If the task is a block quote, the block quote wraps the p-tag that contains the content.
@@ -160,6 +213,141 @@ async function renderComponentText(
     }
 }
 
+export type AttributesDictionary = { [key: string]: string };
+
+/**
+ * This function returns two lists -- genericClasses and dataAttributes -- that describe the
+ * given component.
+ * The genericClasses describe what the component is, e.g. a due date or a priority, and are one of the
+ * options in LayoutClasses.
+ * The dataAttributes describe the content of the component, e.g. `data-task-priority="medium"`, `data-task-due="past-1d"` etc.
+ */
+function getComponentClassesAndData(component: TaskLayoutComponent, task: Task): [string[], AttributesDictionary] {
+    const genericClasses: string[] = [];
+    const dataAttributes: AttributesDictionary = {};
+    const setDateAttribute = (date: Moment, attributeName: string) => {
+        const dateValue = dateToAttribute(date);
+        if (dateValue) dataAttributes[attributeName] = dateValue;
+    };
+    switch (component) {
+        case 'description':
+            genericClasses.push(LayoutClasses.description);
+            break;
+        case 'priority': {
+            let priorityValue = null;
+            if (task.priority === taskModule.Priority.High) priorityValue = 'high';
+            else if (task.priority === taskModule.Priority.Medium) priorityValue = 'medium';
+            else if (task.priority === taskModule.Priority.Low) priorityValue = 'low';
+            else priorityValue = 'normal';
+            dataAttributes['taskPriority'] = priorityValue;
+            genericClasses.push(LayoutClasses.priority);
+            break;
+        }
+        case 'createdDate': {
+            const date = task.createdDate;
+            if (date) {
+                genericClasses.push(LayoutClasses.createdDate);
+                setDateAttribute(date, 'taskCreated');
+            }
+            break;
+        }
+        case 'dueDate': {
+            const date = task.dueDate;
+            if (date) {
+                genericClasses.push(LayoutClasses.dueDate);
+                setDateAttribute(date, 'taskDue');
+            }
+            break;
+        }
+        case 'startDate': {
+            const date = task.startDate;
+            if (date) {
+                genericClasses.push(LayoutClasses.startDate);
+                setDateAttribute(date, 'taskStart');
+            }
+            break;
+        }
+        case 'scheduledDate': {
+            const date = task.scheduledDate;
+            if (date) {
+                genericClasses.push(LayoutClasses.scheduledDate);
+                setDateAttribute(date, 'taskScheduled');
+            }
+            break;
+        }
+        case 'doneDate': {
+            const date = task.doneDate;
+            if (date) {
+                genericClasses.push(LayoutClasses.doneDate);
+                setDateAttribute(date, 'taskDone');
+            }
+            break;
+        }
+        case 'recurrenceRule': {
+            genericClasses.push(LayoutClasses.recurrenceRule);
+            break;
+        }
+    }
+    return [genericClasses, dataAttributes];
+}
+
+/*
+ * Adds internal classes for various components (right now just tags actually), meaning that we modify the existing
+ * rendered element to add classes inside it.
+ * In the case of tags, Obsidian renders a Markdown description with <a class="tag"> elements for tags. We want to
+ * enable users to style these, so we modify the rendered Markdown by adding the specific tag classes for these <a>
+ * elements.
+ */
+function addInternalClasses(component: TaskLayoutComponent, renderedComponent: HTMLSpanElement) {
+    if (component === 'description') {
+        const tags = renderedComponent.getElementsByClassName('tag');
+        for (let i = 0; i < tags.length; i++) {
+            const tagName = tags[i].textContent;
+            if (tagName) {
+                const className = tagToAttributeValue(tagName);
+                const element = tags[i] as HTMLElement;
+                if (className) element.dataset.tagName = className;
+            }
+        }
+    }
+}
+
+/**
+ * Translate a relative date to a CSS class: 'today', 'future-1d' (for tomorrow), 'past-1d' (for yesterday)
+ * etc.
+ * A cutoff (in days) is defined in MAX_DAY_VALUE_RANGE, from beyond that a generic 'far' postfix will be added.
+ * (the cutoff exists because we don't want to flood the DOM with potentially hundreds of unique classes.)
+ */
+function dateToAttribute(date: Moment) {
+    const today = window.moment().startOf('day');
+    let result = '';
+    const diffDays = today.diff(date, 'days');
+    if (isNaN(diffDays)) return null;
+    if (diffDays === 0) return 'today';
+    else if (diffDays > 0) result += 'past-';
+    else if (diffDays < 0) result += 'future-';
+    if (Math.abs(diffDays) <= MAX_DAY_VALUE_RANGE) {
+        result += Math.abs(diffDays).toString() + 'd';
+    } else {
+        result += DAY_VALUE_OVER_RANGE_POSTFIX;
+    }
+    return result;
+}
+
+/*
+ * Sanitize tag names so they will be valid attribute values according to the HTML spec:
+ * https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
+ */
+function tagToAttributeValue(tag: string) {
+    // eslint-disable-next-line no-control-regex
+    const illegalChars = /["&\x00\r\n]/g;
+    let sanitizedTag = tag.replace(illegalChars, '-');
+    // And if after sanitazation the name starts with dashes or underscores, remove them.
+    sanitizedTag = sanitizedTag.replace(/^[-_]+/, '');
+    if (sanitizedTag.length > 0) return sanitizedTag;
+    else return null;
+}
+
 function addTooltip({
     task,
     element,
diff --git a/versions.json b/versions.json
index fc3a80ea..70297e05 100644
--- a/versions.json
+++ b/versions.json
@@ -54,5 +54,6 @@
   "1.23.0": "0.14.6",
   "1.24.0": "0.14.6",
   "1.25.0": "0.14.6",
-  "2.0.0": "0.14.6"
+  "2.0.0": "0.14.6",
+  "2.0.1": "0.14.6"
 }


Test patch:
diff --git a/contributing/Dependencies/Thought-Process for Deciding Whether a Dependency Needs Manual Testing.md b/contributing/Dependencies/Thought-Process for Deciding Whether a Dependency Needs Manual Testing.md
index ba82e74b..4fcfc470 100644
--- a/contributing/Dependencies/Thought-Process for Deciding Whether a Dependency Needs Manual Testing.md
+++ b/contributing/Dependencies/Thought-Process for Deciding Whether a Dependency Needs Manual Testing.md
@@ -2,19 +2,33 @@
 
 Look at the `package.json` entry for a package and search for which files import the package.
 
-- When in doubt, smoke-test. Smoke-testing of multiple dependency upgrades can be done in a batch, to reduce the time spent on this process.
-- **Definitely smoke test**: Anything that is involved in producing the built output to users. For example:
-  - everything in the "dependencies" (rather than "devDependencies") list in `package.json`
-  - `esbuild` (the build system)
-  - anything imported by the esbuild config file `esbuild.config.mjs` (for example, `builtin-modules`, `svelte-preprocess`)
-  - `obsidian` (also see [[Notes and Special Cases]] below)
-  - all `@codemirror/*`
-  - `moment`
-- **Automated testing sufficient**: Our linting, formatting, and testing code does not affect the built output and is run automatically on each PR, so it does not need smoke tests. An automated test fail for an upgrade to one of these packages may be an indication of a newly found linter error in the code and should be investigated. However, if all the automatic checks pass, these packages can be merged right away:
-  - `markdownlint`
-  - anything with `eslint` in it (including `@typescript/eslint*`, which version bump weekly regardless of whether they have any changes)
-  - `svelte-check` (but not other svelte things, which are used in the build system)
-  - anything with `prettier`
-  - `lefthook`
-  - anything with `jest` in it (but see the note on [[Dependency Groups]] for details).
-- For anything else, where and how is it being used? If it's only in tests, or only used by developers, no need to smoke test.
+- When in doubt, [[How do I smoke-test the Tasks plugin|smoke-test]].
+- Smoke-testing of multiple dependency upgrades can be done in a batch, to reduce the time spent on this process.
+
+## Definitely smoke test
+
+Anything that is involved in producing the built output to users. For example:
+
+- everything in the "dependencies" (rather than "devDependencies") list in `package.json`
+- `esbuild` (the build system)
+- anything imported by the esbuild config file `esbuild.config.mjs` (for example, `builtin-modules`, `svelte-preprocess`)
+- `obsidian` (also see [[Notes and Special Cases]] below)
+- all `@codemirror/*`
+- `moment`
+
+## Automated testing sufficient
+
+Our linting, formatting, and testing code does not affect the built output and is run automatically on each PR, so it does not need smoke tests.
+
+An automated test fail for an upgrade to one of these packages may be an indication of a newly found linter error in the code and should be investigated.
+
+However, if all the automatic checks pass, these packages can be merged right away:
+
+- `markdownlint`
+- anything with `eslint` in it (including `@typescript/eslint*`, which version bump weekly regardless of whether they have any changes)
+- `svelte-check` (but not other svelte things, which are used in the build system)
+- anything with `prettier`
+- `lefthook`
+- anything with `jest` in it (but see the note on [[Dependency Groups]] for details).
+- For anything else, where and how is it being used?
+  - If it's only in tests, or only used by developers, no need to smoke test.
diff --git a/resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-smoke-test-query-styling.css b/resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-smoke-test-query-styling.css
new file mode 100644
index 00000000..79da1832
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/.obsidian/snippets/tasks-plugin-smoke-test-query-styling.css
@@ -0,0 +1,13 @@
+/* This file is only for use in smoke-testing the Tasks plugin, and not intended for use by users. */
+
+.block-language-tasks:has(ul.plugin-tasks-query-result[data-task-group-by="priority"]) .tasks-group-heading {
+    color: red;
+}
+
+.block-language-tasks .tasks-layout-short-mode {
+    background-color: aqua;
+}
+
+ul.contains-task-list.tasks-layout-hide-priority {
+    color: red;
+}
diff --git a/resources/sample_vaults/Tasks-Demo/Manual Testing/Smoke Testing the Tasks Plugin.md b/resources/sample_vaults/Tasks-Demo/Manual Testing/Smoke Testing the Tasks Plugin.md
index c93d3287..d634e988 100644
--- a/resources/sample_vaults/Tasks-Demo/Manual Testing/Smoke Testing the Tasks Plugin.md
+++ b/resources/sample_vaults/Tasks-Demo/Manual Testing/Smoke Testing the Tasks Plugin.md
@@ -103,6 +103,12 @@ heading includes Rendering of Task Blocks
 
 ---
 
+### Styling of Rendered Task Blocks
+
+- [ ] #task **check**: Open the file [[Styling of Queries]] and follow the steps there
+
+---
+
 ### Create or edit Task modal
 
 - This text should copied in to the task Description, after following steps below
diff --git a/resources/sample_vaults/Tasks-Demo/Manual Testing/Styling of Queries.md b/resources/sample_vaults/Tasks-Demo/Manual Testing/Styling of Queries.md
new file mode 100644
index 00000000..028a5450
--- /dev/null
+++ b/resources/sample_vaults/Tasks-Demo/Manual Testing/Styling of Queries.md
@@ -0,0 +1,46 @@
+# Styling of Queries
+
+> [!Warning]
+> These tests require the `.has` selector.
+> The CSS `:has` selector is available with **Obsidian installer version 1.1.9 and newer**. You can run the Obsidian command `Show debug info` to see your current installer version.
+
+---
+
+To test styling of queries, follow these steps, viewing this file either in **Reading** Mode or **Live Preview** Mode:
+
+- [ ] **1. Open the Obsidian settings of the Demo vault and under Appearance | CSS Snippets, turn on `tasks-plugin-smoke-test-query-styling`.**
+
+- [ ] **2.** **Test 'group by' classes** - the following query result should have **red headings** named 'Priority 1: High' and 'Priority 4: Low'.
+
+```tasks
+path includes Styling of Queries
+group by priority
+```
+
+- [ ] **3. Test 'group by' classes #2** - the following should have a **black** heading named 'No due date':
+
+```tasks
+path includes Styling of Queries
+group by due
+```
+
+- [ ] **4. Test short mode classes** - the following should have an aqua background:
+
+```tasks
+path includes Styling of Queries
+short mode
+```
+
+- [ ] **5. Test 'hidden' query classes** - the following lines (except the backlinks) should be colored **red**:
+
+```tasks
+path includes Styling of Queries
+hide priority
+```
+
+- [ ] 6. Open the Obsidian settings of the Demo vault and under Appearance | CSS Snippets, turn **off** `tasks-plugin-smoke-test-query-styling`.
+
+## Tasks for Reference
+
+- [ ] #task Task with high priority ⏫
+- [ ] #task Task with low priority 🔽
diff --git a/tests/EditorSuggest.test.ts b/tests/EditorSuggest.test.ts
deleted file mode 100644
index 0325a996..00000000
--- a/tests/EditorSuggest.test.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-import moment from 'moment';
-import { TASK_FORMATS, getSettings } from '../src/Config/Settings';
-import { buildSuggestions } from '../src/Suggestor/Suggestor';
-import type { SuggestInfo } from '../src/Suggestor/Suggestor';
-
-window.moment = moment;
-
-describe('auto-complete', () => {
-    it('offers basic completion options for an empty task', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] ';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 5, originalSettings);
-        expect(suggestions).toEqual([
-            { suggestionType: 'empty', displayText: '⏎', appendText: '\n' },
-            { displayText: '📅 due date', appendText: '📅 ' },
-            { displayText: '🛫 start date', appendText: '🛫 ' },
-            { displayText: '⏳ scheduled date', appendText: '⏳ ' },
-            { displayText: '⏫ high priority', appendText: '⏫ ' },
-            { displayText: '🔼 medium priority', appendText: '🔼 ' },
-        ]);
-    });
-
-    it('offers generic due date completions', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] some task 📅';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 17, originalSettings);
-        expect(suggestions[0].displayText).toContain('today');
-        expect(suggestions[1].displayText).toContain('tomorrow');
-        expect(suggestions.length).toEqual(6);
-    });
-
-    it('offers specific due date completions', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] some task 📅 to';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 20, originalSettings);
-        expect(suggestions[0].displayText).toContain('today');
-        expect(suggestions[1].displayText).toContain('tomorrow');
-    });
-
-    it('offers generic recurrence completions', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] some task 🔁';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 17, originalSettings);
-        expect(suggestions[0].displayText).toEqual('every');
-        expect(suggestions[1].displayText).toEqual('every day');
-        expect(suggestions[2].displayText).toEqual('every week');
-    });
-
-    it('offers specific recurrence completions', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        const line = '- [ ] some task 🔁 every w';
-        const suggestions: SuggestInfo[] = buildSuggestions(line, 25, originalSettings);
-        expect(suggestions[0].displayText).toEqual('every week');
-        expect(suggestions[1].displayText).toEqual('every week on Sunday');
-        expect(suggestions[2].displayText).toEqual('every week on Monday');
-    });
-
-    it('respects the minimal match setting', () => {
-        // Arrange
-        const originalSettings = getSettings();
-        originalSettings.autoSuggestMinMatch = 2;
-        let line = '- [ ] some task 🔁 e';
-        let suggestions: SuggestInfo[] = buildSuggestions(line, 19, originalSettings);
-        expect(suggestions.length).toEqual(0);
-        line = '- [ ] some task 🔁 ev';
-        suggestions = buildSuggestions(line, 20, originalSettings);
-        expect(suggestions[0].displayText).toEqual('every');
-        expect(suggestions[1].displayText).toEqual('every day');
-    });
-
-    // Test disabled until I can figure out:
-    // 1. how to set the date.
-    // 2. how to set maxGenericSuggestions in Suggestor.ts to higher than 5.
-    // See suggestions in https://github.com/obsidian-tasks-group/obsidian-tasks/issues/861#issuecomment-1180788860
-    it.skip('show all suggested text', () => {
-        // Arrange
-        const { dueDateSymbol, scheduledDateSymbol, startDateSymbol, recurrenceSymbol } =
-            TASK_FORMATS.tasksPluginEmoji.taskSerializer.symbols;
-        const originalSettings = getSettings();
-        originalSettings.autoSuggestMaxItems = 200;
-
-        // This does not change the date used in the suggestions below.
-        // It was a failed attempt at allowing this test to be independent of date of the run.
-        // const todaySpy = jest
-        //     .spyOn(Date, 'now')
-        //     .mockReturnValue(moment('2022-06-11').valueOf());
-
-        const lines = [
-            '- [ ] some task',
-            `- [ ] some task ${recurrenceSymbol} `,
-            `- [ ] some task ${dueDateSymbol} `,
-            `- [ ] some task ${scheduledDateSymbol} `,
-            `- [ ] some task ${startDateSymbol} `,
-        ];
-        const allSuggestions: string[] = [];
-        for (const line of lines) {
-            // allSuggestions.push(`| ${line} | |`);
-            const suggestions: SuggestInfo[] = buildSuggestions(line, line.length - 1, originalSettings);
-            for (const suggestion of suggestions) {
-                // The 'new line' replacement adds a trailing space at the end of a line,
-                // which causes auto-formatting to then make the test fail.
-                // So we replace the 'new line' character with some fixed text.
-                let replacementText = `${suggestion.appendText}`;
-                if (replacementText === '\n') {
-                    replacementText = '<new line>';
-                }
-                // Format for pasting in to a Markdown table:
-                const suggestionAsText = `| ${suggestion.displayText} | ${replacementText} |`;
-                if (!allSuggestions.includes(suggestionAsText)) {
-                    allSuggestions.push(suggestionAsText);
-                }
-            }
-        }
-
-        // CAUTION: In obsidian, when using this feature on a 'Monday' expands to the execution date.
-        // In these tests, it expands to the Next Monday.
-
-        expect('\n' + allSuggestions.join('\n') + '\n').toMatchInlineSnapshot(`
-            "
-            | ⏎ | <new line> |
-            | 📅 due date | 📅  |
-            | 🛫 start date | 🛫  |
-            | ⏳ scheduled date | ⏳  |
-            | ⏫ high priority | ⏫  |
-            | 🔼 medium priority | 🔼  |
-            | 🔽 low priority | 🔽  |
-            | 🔁 recurring (repeat) | 🔁  |
-            | every | 🔁 every  |
-            | every day | 🔁 every day  |
-            | every week | 🔁 every week  |
-            | every month | 🔁 every month  |
-            | every month on the | 🔁 every month on the  |
-            | every year | 🔁 every year  |
-            | every week on Sunday | 🔁 every week on Sunday  |
-            | every week on Monday | 🔁 every week on Monday  |
-            | every week on Tuesday | 🔁 every week on Tuesday  |
-            | every week on Wednesday | 🔁 every week on Wednesday  |
-            | every week on Thursday | 🔁 every week on Thursday  |
-            | every week on Friday | 🔁 every week on Friday  |
-            | every week on Saturday | 🔁 every week on Saturday  |
-            | today (2022-07-11) | 📅 2022-07-11  |
-            | tomorrow (2022-07-12) | 📅 2022-07-12  |
-            | Sunday (2022-07-17) | 📅 2022-07-17  |
-            | Monday (2022-07-18) | 📅 2022-07-18  |
-            | Tuesday (2022-07-12) | 📅 2022-07-12  |
-            | Wednesday (2022-07-13) | 📅 2022-07-13  |
-            | Thursday (2022-07-14) | 📅 2022-07-14  |
-            | Friday (2022-07-15) | 📅 2022-07-15  |
-            | Saturday (2022-07-16) | 📅 2022-07-16  |
-            | next week (2022-07-18) | 📅 2022-07-18  |
-            | next month (2022-08-11) | 📅 2022-08-11  |
-            | next year (2023-07-11) | 📅 2023-07-11  |
-            | today (2022-07-11) | ⏳ 2022-07-11  |
-            | tomorrow (2022-07-12) | ⏳ 2022-07-12  |
-            | Sunday (2022-07-17) | ⏳ 2022-07-17  |
-            | Monday (2022-07-18) | ⏳ 2022-07-18  |
-            | Tuesday (2022-07-12) | ⏳ 2022-07-12  |
-            | Wednesday (2022-07-13) | ⏳ 2022-07-13  |
-            | Thursday (2022-07-14) | ⏳ 2022-07-14  |
-            | Friday (2022-07-15) | ⏳ 2022-07-15  |
-            | Saturday (2022-07-16) | ⏳ 2022-07-16  |
-            | next week (2022-07-18) | ⏳ 2022-07-18  |
-            | next month (2022-08-11) | ⏳ 2022-08-11  |
-            | next year (2023-07-11) | ⏳ 2023-07-11  |
-            | today (2022-07-11) | 🛫 2022-07-11  |
-            | tomorrow (2022-07-12) | 🛫 2022-07-12  |
-            | Sunday (2022-07-17) | 🛫 2022-07-17  |
-            | Monday (2022-07-18) | 🛫 2022-07-18  |
-            | Tuesday (2022-07-12) | 🛫 2022-07-12  |
-            | Wednesday (2022-07-13) | 🛫 2022-07-13  |
-            | Thursday (2022-07-14) | 🛫 2022-07-14  |
-            | Friday (2022-07-15) | 🛫 2022-07-15  |
-            | Saturday (2022-07-16) | 🛫 2022-07-16  |
-            | next week (2022-07-18) | 🛫 2022-07-18  |
-            | next month (2022-08-11) | 🛫 2022-08-11  |
-            | next year (2023-07-11) | 🛫 2023-07-11  |
-            "
-        `);
-
-        // todaySpy.mockClear();
-    });
-});
diff --git a/tests/Group.test.ts b/tests/Group.test.ts
index 23c0fc22..a6e03d52 100644
--- a/tests/Group.test.ts
+++ b/tests/Group.test.ts
@@ -194,22 +194,22 @@ describe('Grouping tasks', () => {
         // Assert
         expect(groups.toString()).toMatchInlineSnapshot(`
             "
-            Group names: [folder\\_a/folder\\_b/,[[file\\_c]]]
+            Group names: [folder\\_a/folder\\_b/,[[file_c]]]
             #### folder\\_a/folder\\_b/
-            ##### [[file\\_c]]
+            ##### [[file_c]]
             - [ ] Task 3 - but path is 1st, alphabetically
 
             ---
 
-            Group names: [folder\\_b/folder\\_c/,[[file\\_c]]]
+            Group names: [folder\\_b/folder\\_c/,[[file_c]]]
             #### folder\\_b/folder\\_c/
-            ##### [[file\\_c]]
+            ##### [[file_c]]
             - [ ] Task 1 - but path is 2nd, alphabetically
 
             ---
 
-            Group names: [folder\\_b/folder\\_c/,[[file\\_d]]]
-            ##### [[file\\_d]]
+            Group names: [folder\\_b/folder\\_c/,[[file_d]]]
+            ##### [[file_d]]
             - [ ] Task 2 - but path is 2nd, alphabetically
 
             ---
@@ -307,7 +307,7 @@ describe('Group names', () => {
         {
             groupBy: 'filename',
             taskLine: '- [ ] a',
-            expectedGroupNames: ['[[\\_c\\_]]'], // underscores in file names are escaped
+            expectedGroupNames: ['[[_c_]]'], // underscores in links shall not be escaped
             path: 'a/b/_c_.md',
         },
 
diff --git a/tests/Suggestor/Suggestor.test.ts b/tests/Suggestor/Suggestor.test.ts
new file mode 100644
index 00000000..76c62301
--- /dev/null
+++ b/tests/Suggestor/Suggestor.test.ts
@@ -0,0 +1,191 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+import { getSettings } from '../../src/Config/Settings';
+import type { SuggestInfo } from '../../src/Suggestor';
+import { makeDefaultSuggestionBuilder } from '../../src/Suggestor/Suggestor';
+import { DEFAULT_SYMBOLS } from '../../src/TaskSerializer/DefaultTaskSerializer';
+
+window.moment = moment;
+
+describe.each([{ name: 'emoji', symbols: DEFAULT_SYMBOLS }])("auto-complete with '$name' symbols", ({ symbols }) => {
+    const buildSuggestions = makeDefaultSuggestionBuilder(symbols);
+    const { dueDateSymbol, scheduledDateSymbol, startDateSymbol, recurrenceSymbol, prioritySymbols } = symbols;
+    it('offers basic completion options for an empty task', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = '- [ ] ';
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 5, originalSettings);
+        expect(suggestions).toEqual([
+            { suggestionType: 'empty', displayText: '⏎', appendText: '\n' },
+            { displayText: `${dueDateSymbol} due date`, appendText: `${dueDateSymbol} ` },
+            { displayText: `${startDateSymbol} start date`, appendText: `${startDateSymbol} ` },
+            { displayText: `${scheduledDateSymbol} scheduled date`, appendText: `${scheduledDateSymbol} ` },
+            { displayText: `${prioritySymbols.High} high priority`, appendText: `${prioritySymbols.High} ` },
+            { displayText: `${prioritySymbols.Medium} medium priority`, appendText: `${prioritySymbols.Medium} ` },
+        ]);
+    });
+
+    it('offers generic due date completions', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = `- [ ] some task ${dueDateSymbol}`;
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 17, originalSettings);
+        expect(suggestions[0].displayText).toContain('today');
+        expect(suggestions[1].displayText).toContain('tomorrow');
+        expect(suggestions.length).toEqual(6);
+    });
+
+    it('offers specific due date completions', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = `- [ ] some task ${dueDateSymbol} to`;
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 20, originalSettings);
+        expect(suggestions[0].displayText).toContain('today');
+        expect(suggestions[1].displayText).toContain('tomorrow');
+    });
+
+    it('offers generic recurrence completions', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = `- [ ] some task ${recurrenceSymbol}`;
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 17, originalSettings);
+        expect(suggestions[0].displayText).toEqual('every');
+        expect(suggestions[1].displayText).toEqual('every day');
+        expect(suggestions[2].displayText).toEqual('every week');
+    });
+
+    it('offers specific recurrence completions', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        const line = `- [ ] some task ${recurrenceSymbol} every w`;
+        const suggestions: SuggestInfo[] = buildSuggestions(line, 25, originalSettings);
+        expect(suggestions[0].displayText).toEqual('every week');
+        expect(suggestions[1].displayText).toEqual('every week on Sunday');
+        expect(suggestions[2].displayText).toEqual('every week on Monday');
+    });
+
+    it('respects the minimal match setting', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        originalSettings.autoSuggestMinMatch = 2;
+        let line = `- [ ] some task ${recurrenceSymbol} e`;
+        let suggestions: SuggestInfo[] = buildSuggestions(line, 19, originalSettings);
+        expect(suggestions.length).toEqual(0);
+        line = `- [ ] some task ${recurrenceSymbol} ev`;
+        suggestions = buildSuggestions(line, 20, originalSettings);
+        expect(suggestions[0].displayText).toEqual('every');
+        expect(suggestions[1].displayText).toEqual('every day');
+    });
+
+    // Test disabled until I can figure out:
+    // 1. how to set the date.
+    // 2. how to set maxGenericSuggestions in Suggestor.ts to higher than 5.
+    // See suggestions in https://github.com/obsidian-tasks-group/obsidian-tasks/issues/861#issuecomment-1180788860
+    it.skip('show all suggested text', () => {
+        // Arrange
+        const originalSettings = getSettings();
+        originalSettings.autoSuggestMaxItems = 200;
+
+        // This does not change the date used in the suggestions below.
+        // It was a failed attempt at allowing this test to be independent of date of the run.
+        // const todaySpy = jest
+        //     .spyOn(Date, 'now')
+        //     .mockReturnValue(moment('2022-06-11').valueOf());
+
+        const lines = [
+            '- [ ] some task',
+            `- [ ] some task ${recurrenceSymbol} `,
+            `- [ ] some task ${dueDateSymbol} `,
+            `- [ ] some task ${scheduledDateSymbol} `,
+            `- [ ] some task ${startDateSymbol} `,
+        ];
+        const allSuggestions: string[] = [];
+        for (const line of lines) {
+            // allSuggestions.push(`| ${line} | |`);
+            const suggestions: SuggestInfo[] = buildSuggestions(line, line.length - 1, originalSettings);
+            for (const suggestion of suggestions) {
+                // The 'new line' replacement adds a trailing space at the end of a line,
+                // which causes auto-formatting to then make the test fail.
+                // So we replace the 'new line' character with some fixed text.
+                let replacementText = `${suggestion.appendText}`;
+                if (replacementText === '\n') {
+                    replacementText = '<new line>';
+                }
+                // Format for pasting in to a Markdown table:
+                const suggestionAsText = `| ${suggestion.displayText} | ${replacementText} |`;
+                if (!allSuggestions.includes(suggestionAsText)) {
+                    allSuggestions.push(suggestionAsText);
+                }
+            }
+        }
+
+        // CAUTION: In obsidian, when using this feature on a 'Monday' expands to the execution date.
+        // In these tests, it expands to the Next Monday.
+
+        expect('\n' + allSuggestions.join('\n') + '\n').toMatchInlineSnapshot(`
+            "
+            | ⏎ | <new line> |
+            | ${dueDateSymbol} due date | ${dueDateSymbol}  |
+            | ${startDateSymbol} start date | ${startDateSymbol}  |
+            | ${scheduledDateSymbol} scheduled date | ${scheduledDateSymbol}  |
+            | ${prioritySymbols.High} high priority | ${prioritySymbols.High}  |
+            | ${prioritySymbols.Medium} medium priority | ${prioritySymbols.Medium}  |
+            | ${prioritySymbols.Low} low priority | ${prioritySymbols.Low}  |
+            | ${recurrenceSymbol} recurring (repeat) | ${recurrenceSymbol}  |
+            | every | ${recurrenceSymbol} every  |
+            | every day | ${recurrenceSymbol} every day  |
+            | every week | ${recurrenceSymbol} every week  |
+            | every month | ${recurrenceSymbol} every month  |
+            | every month on the | ${recurrenceSymbol} every month on the  |
+            | every year | ${recurrenceSymbol} every year  |
+            | every week on Sunday | ${recurrenceSymbol} every week on Sunday  |
+            | every week on Monday | ${recurrenceSymbol} every week on Monday  |
+            | every week on Tuesday | ${recurrenceSymbol} every week on Tuesday  |
+            | every week on Wednesday | ${recurrenceSymbol} every week on Wednesday  |
+            | every week on Thursday | ${recurrenceSymbol} every week on Thursday  |
+            | every week on Friday | ${recurrenceSymbol} every week on Friday  |
+            | every week on Saturday | ${recurrenceSymbol} every week on Saturday  |
+            | today (2022-07-11) | ${dueDateSymbol} 2022-07-11  |
+            | tomorrow (2022-07-12) | ${dueDateSymbol} 2022-07-12  |
+            | Sunday (2022-07-17) | ${dueDateSymbol} 2022-07-17  |
+            | Monday (2022-07-18) | ${dueDateSymbol} 2022-07-18  |
+            | Tuesday (2022-07-12) | ${dueDateSymbol} 2022-07-12  |
+            | Wednesday (2022-07-13) | ${dueDateSymbol} 2022-07-13  |
+            | Thursday (2022-07-14) | ${dueDateSymbol} 2022-07-14  |
+            | Friday (2022-07-15) | ${dueDateSymbol} 2022-07-15  |
+            | Saturday (2022-07-16) | ${dueDateSymbol} 2022-07-16  |
+            | next week (2022-07-18) | ${dueDateSymbol} 2022-07-18  |
+            | next month (2022-08-11) | ${dueDateSymbol} 2022-08-11  |
+            | next year (2023-07-11) | ${dueDateSymbol} 2023-07-11  |
+            | today (2022-07-11) | ${scheduledDateSymbol} 2022-07-11  |
+            | tomorrow (2022-07-12) | ${scheduledDateSymbol} 2022-07-12  |
+            | Sunday (2022-07-17) | ${scheduledDateSymbol} 2022-07-17  |
+            | Monday (2022-07-18) | ${scheduledDateSymbol} 2022-07-18  |
+            | Tuesday (2022-07-12) | ${scheduledDateSymbol} 2022-07-12  |
+            | Wednesday (2022-07-13) | ${scheduledDateSymbol} 2022-07-13  |
+            | Thursday (2022-07-14) | ${scheduledDateSymbol} 2022-07-14  |
+            | Friday (2022-07-15) | ${scheduledDateSymbol} 2022-07-15  |
+            | Saturday (2022-07-16) | ${scheduledDateSymbol} 2022-07-16  |
+            | next week (2022-07-18) | ${scheduledDateSymbol} 2022-07-18  |
+            | next month (2022-08-11) | ${scheduledDateSymbol} 2022-08-11  |
+            | next year (2023-07-11) | ${scheduledDateSymbol} 2023-07-11  |
+            | today (2022-07-11) | ${startDateSymbol} 2022-07-11  |
+            | tomorrow (2022-07-12) | ${startDateSymbol} 2022-07-12  |
+            | Sunday (2022-07-17) | ${startDateSymbol} 2022-07-17  |
+            | Monday (2022-07-18) | ${startDateSymbol} 2022-07-18  |
+            | Tuesday (2022-07-12) | ${startDateSymbol} 2022-07-12  |
+            | Wednesday (2022-07-13) | ${startDateSymbol} 2022-07-13  |
+            | Thursday (2022-07-14) | ${startDateSymbol} 2022-07-14  |
+            | Friday (2022-07-15) | ${startDateSymbol} 2022-07-15  |
+            | Saturday (2022-07-16) | ${startDateSymbol} 2022-07-16  |
+            | next week (2022-07-18) | ${startDateSymbol} 2022-07-18  |
+            | next month (2022-08-11) | ${startDateSymbol} 2022-08-11  |
+            | next year (2023-07-11) | ${startDateSymbol} 2023-07-11  |
+            "
+        `);
+
+        // todaySpy.mockClear();
+    });
+});
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index d65e1db9..5a22aa47 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -464,6 +464,22 @@ describe('to string', () => {
         const expectedLine = '- [x] this is a done task #tagone #journal/daily 📅 2021-09-12 ✅ 2021-06-20';
         expect(task.toFileLineString()).toStrictEqual(expectedLine);
     });
+
+    it('retains the global filter', () => {
+        // Arrange
+        const line = '- [ ] This is a task with #t as a global filter and also #t/some tags';
+
+        updateSettings({ globalFilter: '#t' });
+        // Act
+        const task: Task = fromLine({
+            line,
+        }) as Task;
+
+        // Assert
+        const expectedLine = 'This is a task with #t as a global filter and also #t/some tags';
+        expect(task.toString()).toStrictEqual(expectedLine);
+        resetSettings();
+    });
 });
 
 describe('toggle done', () => {
diff --git a/tests/TaskLineRenderer.test.ts b/tests/TaskLineRenderer.test.ts
index 550e0aeb..92b182eb 100644
--- a/tests/TaskLineRenderer.test.ts
+++ b/tests/TaskLineRenderer.test.ts
@@ -2,11 +2,14 @@
  * @jest-environment jsdom
  */
 import moment from 'moment';
+import { LayoutClasses, renderTaskLine } from '../src/TaskLineRenderer';
+import type { AttributesDictionary, TextRenderer } from '../src/TaskLineRenderer';
 import { DebugSettings } from '../src/Config/DebugSettings';
-import { renderTaskLine } from '../src/TaskLineRenderer';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
 import { LayoutOptions } from '../src/TaskLayout';
 import type { Task } from '../src/Task';
+import { TaskRegularExpressions } from '../src/Task';
+import { DateParser } from '../src/Query/DateParser';
 import { fromLine } from './TestHelpers';
 
 jest.mock('obsidian');
@@ -16,11 +19,13 @@ window.moment = moment;
  * Creates a dummy 'parent element' to host a task render, renders a task inside it,
  * and returns it for inspection.
  */
-async function createMockParentAndRender(task: Task, layoutOptions?: LayoutOptions) {
+async function createMockParentAndRender(task: Task, layoutOptions?: LayoutOptions, mockTextRenderer?: TextRenderer) {
     const parentElement = document.createElement('div');
-    const mockTextRenderer = async (text: string, element: HTMLSpanElement, _path: string) => {
-        element.innerText = text;
-    };
+    // Our default text renderer for this method is a simplistic flat text
+    if (!mockTextRenderer)
+        mockTextRenderer = async (text: string, element: HTMLSpanElement, _path: string) => {
+            element.innerText = text;
+        };
     await renderTaskLine(
         task,
         {
@@ -41,7 +46,20 @@ function getTextSpan(parentElement: HTMLElement) {
 
 function getDescriptionText(parentElement: HTMLElement) {
     const textSpan = getTextSpan(parentElement);
-    return textSpan.innerText;
+    return (textSpan.children[0].children[0] as HTMLElement).innerText;
+}
+
+/*
+ * Returns a list of the task components that are not the description, as strings.
+ */
+function getOtherLayoutComponents(parentElement: HTMLElement): string[] {
+    const textSpan = getTextSpan(parentElement);
+    const components: string[] = [];
+    for (const childSpan of Array.from(textSpan.children)) {
+        if (childSpan.classList.contains(LayoutClasses.description)) continue;
+        if (childSpan?.textContent) components.push(childSpan.textContent);
+    }
+    return components;
 }
 
 describe('task line rendering', () => {
@@ -73,7 +91,20 @@ describe('task line rendering', () => {
         const textSpan = li.children[1];
         expect(textSpan.nodeName).toEqual('SPAN');
         expect(textSpan.classList.contains('tasks-list-text')).toBeTruthy();
-        expect((textSpan as HTMLSpanElement).innerText).toEqual('This is a simple task');
+
+        // Check that the text span contains a single description span
+        expect(textSpan.children.length).toEqual(1);
+        const descriptionSpan = textSpan.children[0];
+        expect(descriptionSpan.nodeName).toEqual('SPAN');
+        expect(descriptionSpan.className).toEqual('task-description');
+
+        // Check that the description span contains an internal span (see taskToHtml for an explanation why it's there)
+        expect(descriptionSpan.children.length).toEqual(1);
+        const internalDescriptionSpan = descriptionSpan.children[0];
+        expect(internalDescriptionSpan.nodeName).toEqual('SPAN');
+
+        // Check that eventually the correct text was rendered
+        expect((internalDescriptionSpan as HTMLSpanElement).innerText).toEqual('This is a simple task');
     });
 
     it('hides the global filter if and only if required', async () => {
@@ -98,7 +129,8 @@ describe('task line rendering', () => {
     const testLayoutOptions = async (
         taskLine: string,
         layoutOptions: Partial<LayoutOptions>,
-        expectedRender: string,
+        expectedDescription: string,
+        expectedComponents: string[],
     ) => {
         const task = fromLine({
             line: taskLine,
@@ -108,14 +140,17 @@ describe('task line rendering', () => {
         const fullLayoutOptions = { ...new LayoutOptions(), ...layoutOptions };
         const parentRender = await createMockParentAndRender(task, fullLayoutOptions);
         const renderedDescription = getDescriptionText(parentRender);
-        expect(renderedDescription).toEqual(expectedRender);
+        const renderedComponents = getOtherLayoutComponents(parentRender);
+        expect(renderedDescription).toEqual(expectedDescription);
+        expect(renderedComponents).toEqual(expectedComponents);
     };
 
     it('renders correctly with the default layout options', async () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             {},
-            'Full task ⏫ 🔁 every day 🛫 2022-07-04 ⏳ 2022-07-03 📅 2022-07-02',
+            'Full task',
+            [' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
@@ -123,7 +158,8 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hidePriority: true },
-            'Full task 🔁 every day 🛫 2022-07-04 ⏳ 2022-07-03 📅 2022-07-02',
+            'Full task',
+            [' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
@@ -131,7 +167,8 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 ➕ 2022-07-05 🔁 every day',
             { hideCreatedDate: true },
-            'Full task ⏫ 🔁 every day 🛫 2022-07-04 ⏳ 2022-07-03 📅 2022-07-02',
+            'Full task',
+            [' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
@@ -139,7 +176,8 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hideStartDate: true },
-            'Full task ⏫ 🔁 every day ⏳ 2022-07-03 📅 2022-07-02',
+            'Full task',
+            [' ⏫', ' 🔁 every day', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
@@ -147,7 +185,8 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hideScheduledDate: true },
-            'Full task ⏫ 🔁 every day 🛫 2022-07-04 📅 2022-07-02',
+            'Full task',
+            [' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' 📅 2022-07-02'],
         );
     });
 
@@ -155,7 +194,8 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hideDueDate: true },
-            'Full task ⏫ 🔁 every day 🛫 2022-07-04 ⏳ 2022-07-03',
+            'Full task',
+            [' ⏫', ' 🔁 every day', ' 🛫 2022-07-04', ' ⏳ 2022-07-03'],
         );
     });
 
@@ -163,32 +203,49 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
             { hideRecurrenceRule: true },
-            'Full task ⏫ 🛫 2022-07-04 ⏳ 2022-07-03 📅 2022-07-02',
+            'Full task',
+            [' ⏫', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
+        );
+    });
+
+    it('marks nonexistent task priority as "normal" priority', async () => {
+        await testLiAttributes(
+            '- [ ] Full task 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            {},
+            { taskPriority: 'normal' },
         );
     });
 
     it('renders a done task correctly with the default layout', async () => {
         await testLayoutOptions(
-            '- [x] Full task ✅ 2022-07-05 ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            '- [x] Full task ✅ 2022-07-05 ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 ➕ 2022-07-05 🔁 every day',
             {},
-            'Full task ⏫ 🔁 every day 🛫 2022-07-04 ⏳ 2022-07-03 📅 2022-07-02 ✅ 2022-07-05',
+            'Full task',
+            [
+                ' ⏫',
+                ' 🔁 every day',
+                ' ➕ 2022-07-05',
+                ' 🛫 2022-07-04',
+                ' ⏳ 2022-07-03',
+                ' 📅 2022-07-02',
+                ' ✅ 2022-07-05',
+            ],
         );
     });
 
     it('renders a done task without the done date', async () => {
         await testLayoutOptions(
-            '- [x] Full task ✅ 2022-07-05 ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            '- [x] Full task ✅ 2022-07-05 ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 ➕ 2022-07-05 🔁 every day',
             { hideDoneDate: true },
-            'Full task ⏫ 🔁 every day 🛫 2022-07-04 ⏳ 2022-07-03 📅 2022-07-02',
+            'Full task',
+            [' ⏫', ' 🔁 every day', ' ➕ 2022-07-05', ' 🛫 2022-07-04', ' ⏳ 2022-07-03', ' 📅 2022-07-02'],
         );
     });
 
     it('writes a placeholder message if a date is invalid', async () => {
-        await testLayoutOptions(
-            '- [ ] Task with invalid due date 📅 2023-13-02',
-            {},
-            'Task with invalid due date 📅 Invalid date',
-        );
+        await testLayoutOptions('- [ ] Task with invalid due date 📅 2023-13-02', {}, 'Task with invalid due date', [
+            ' 📅 Invalid date',
+        ]);
     });
 
     it('renders debug info if requested', async () => {
@@ -197,7 +254,8 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Task with invalid due date 📅 2023-11-02',
             {},
-            "Task with invalid due date 📅 2023-11-02<br>🐛 <b>0</b> . 0 . 0 . '<code>- [ ] Task with invalid due date 📅 2023-11-02</code>'<br>'<code>a/b/c.d</code>' > '<code>Previous Heading</code>'<br>",
+            "Task with invalid due date<br>🐛 <b>0</b> . 0 . 0 . '<code>- [ ] Task with invalid due date 📅 2023-11-02</code>'<br>'<code>a/b/c.d</code>' > '<code>Previous Heading</code>'<br>",
+            [' 📅 2023-11-02'],
         );
     });
 
@@ -205,7 +263,300 @@ describe('task line rendering', () => {
         await testLayoutOptions(
             '- [ ] Task with invalid recurrence rule 🔁 every month on the 32nd',
             {},
-            'Task with invalid recurrence rule 🔁 every month on the 32th',
+            'Task with invalid recurrence rule',
+            [' 🔁 every month on the 32th'],
+        );
+    });
+
+    const testComponentClasses = async (
+        taskLine: string,
+        layoutOptions: Partial<LayoutOptions>,
+        mainClass: string,
+        attributes: AttributesDictionary,
+    ) => {
+        const task = fromLine({
+            line: taskLine,
+        });
+        const fullLayoutOptions = { ...new LayoutOptions(), ...layoutOptions };
+        const parentRender = await createMockParentAndRender(task, fullLayoutOptions);
+
+        const textSpan = getTextSpan(parentRender);
+        let found = false;
+        for (const childSpan of Array.from(textSpan.children)) {
+            if (childSpan.classList.contains(mainClass)) {
+                found = true;
+                const spanElement = childSpan as HTMLSpanElement;
+                // Now verify the attributes
+                for (const key in attributes) {
+                    expect(spanElement.dataset[key]).toEqual(attributes[key]);
+                }
+            }
+        }
+        expect(found).toBeTruthy();
+    };
+
+    const testLiAttributes = async (
+        taskLine: string,
+        layoutOptions: Partial<LayoutOptions>,
+        attributes: AttributesDictionary,
+    ) => {
+        const task = fromLine({
+            line: taskLine,
+        });
+        const fullLayoutOptions = { ...new LayoutOptions(), ...layoutOptions };
+        const parentRender = await createMockParentAndRender(task, fullLayoutOptions);
+        const li = parentRender.children[0] as HTMLElement;
+        for (const key in attributes) {
+            expect(li.dataset[key]).toEqual(attributes[key]);
+        }
+    };
+
+    const testHiddenComponentClasses = async (
+        taskLine: string,
+        layoutOptions: Partial<LayoutOptions>,
+        hiddenGenericClass: string,
+        attributes: AttributesDictionary,
+    ) => {
+        const task = fromLine({
+            line: taskLine,
+        });
+        const fullLayoutOptions = { ...new LayoutOptions(), ...layoutOptions };
+        const parentRender = await createMockParentAndRender(task, fullLayoutOptions);
+
+        const textSpan = getTextSpan(parentRender);
+        for (const childSpan of Array.from(textSpan.children)) {
+            expect(childSpan.classList.contains(hiddenGenericClass)).toBeFalsy();
+        }
+        const li = parentRender.children[0] as HTMLElement;
+        // Now verify the attributes
+        for (const key in attributes) {
+            expect(li.dataset[key]).toEqual(attributes[key]);
+        }
+    };
+
+    it('renders priority with its correct classes', async () => {
+        await testComponentClasses(
+            '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            {},
+            LayoutClasses.priority,
+            { taskPriority: 'high' },
+        );
+        await testComponentClasses(
+            '- [ ] Full task 🔼 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            {},
+            LayoutClasses.priority,
+            { taskPriority: 'medium' },
+        );
+        await testComponentClasses(
+            '- [ ] Full task 🔽 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            {},
+            LayoutClasses.priority,
+            { taskPriority: 'low' },
+        );
+    });
+
+    it('renders recurrence with its correct classes', async () => {
+        await testComponentClasses(
+            '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            {},
+            LayoutClasses.recurrenceRule,
+            {},
+        );
+    });
+
+    it('adds a correct "today" CSS class to dates', async () => {
+        const today = DateParser.parseDate('today').format(TaskRegularExpressions.dateFormat);
+        await testComponentClasses(`- [ ] Full task ⏫ ➕ ${today}`, {}, LayoutClasses.createdDate, {
+            taskCreated: 'today',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 📅 ${today}`, {}, LayoutClasses.dueDate, { taskDue: 'today' });
+        await testComponentClasses(`- [ ] Full task ⏫ ⏳ ${today}`, {}, LayoutClasses.scheduledDate, {
+            taskScheduled: 'today',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 🛫 ${today}`, {}, LayoutClasses.startDate, {
+            taskStart: 'today',
+        });
+        await testComponentClasses(`- [x] Done task ✅ ${today}`, {}, LayoutClasses.doneDate, { taskDone: 'today' });
+    });
+
+    it('adds a correct "future-1d" CSS class to dates', async () => {
+        const future = DateParser.parseDate('tomorrow').format(TaskRegularExpressions.dateFormat);
+        await testComponentClasses(`- [ ] Full task ⏫ ➕ ${future}`, {}, LayoutClasses.createdDate, {
+            taskCreated: 'future-1d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 📅 ${future}`, {}, LayoutClasses.dueDate, {
+            taskDue: 'future-1d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ ⏳ ${future}`, {}, LayoutClasses.scheduledDate, {
+            taskScheduled: 'future-1d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 🛫 ${future}`, {}, LayoutClasses.startDate, {
+            taskStart: 'future-1d',
+        });
+        await testComponentClasses(`- [x] Done task ✅ ${future}`, {}, LayoutClasses.doneDate, {
+            taskDone: 'future-1d',
+        });
+    });
+
+    it('adds a correct "future-7d" CSS class to dates', async () => {
+        const future = DateParser.parseDate('in 7 days').format(TaskRegularExpressions.dateFormat);
+        await testComponentClasses(`- [ ] Full task ⏫ ➕ ${future}`, {}, LayoutClasses.createdDate, {
+            taskCreated: 'future-7d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 📅 ${future}`, {}, LayoutClasses.dueDate, {
+            taskDue: 'future-7d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ ⏳ ${future}`, {}, LayoutClasses.scheduledDate, {
+            taskScheduled: 'future-7d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 🛫 ${future}`, {}, LayoutClasses.startDate, {
+            taskStart: 'future-7d',
+        });
+        await testComponentClasses(`- [x] Done task ✅ ${future}`, {}, LayoutClasses.doneDate, {
+            taskDone: 'future-7d',
+        });
+    });
+
+    it('adds a correct "past-1d" CSS class to dates', async () => {
+        const past = DateParser.parseDate('yesterday').format(TaskRegularExpressions.dateFormat);
+        await testComponentClasses(`- [ ] Full task ⏫ ➕ ${past}`, {}, LayoutClasses.createdDate, {
+            taskCreated: 'past-1d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 📅 ${past}`, {}, LayoutClasses.dueDate, { taskDue: 'past-1d' });
+        await testComponentClasses(`- [ ] Full task ⏫ ⏳ ${past}`, {}, LayoutClasses.scheduledDate, {
+            taskScheduled: 'past-1d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 🛫 ${past}`, {}, LayoutClasses.startDate, {
+            taskStart: 'past-1d',
+        });
+        await testComponentClasses(`- [x] Done task ✅ ${past}`, {}, LayoutClasses.doneDate, { taskDone: 'past-1d' });
+    });
+
+    it('adds a correct "past-7d" CSS class to dates', async () => {
+        const past = DateParser.parseDate('7 days ago').format(TaskRegularExpressions.dateFormat);
+        await testComponentClasses(`- [ ] Full task ⏫ ➕ ${past}`, {}, LayoutClasses.createdDate, {
+            taskCreated: 'past-7d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 📅 ${past}`, {}, LayoutClasses.dueDate, { taskDue: 'past-7d' });
+        await testComponentClasses(`- [ ] Full task ⏫ ⏳ ${past}`, {}, LayoutClasses.scheduledDate, {
+            taskScheduled: 'past-7d',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 🛫 ${past}`, {}, LayoutClasses.startDate, {
+            taskStart: 'past-7d',
+        });
+        await testComponentClasses(`- [x] Done task ✅ ${past}`, {}, LayoutClasses.doneDate, { taskDone: 'past-7d' });
+    });
+
+    it('adds the classes "...future-far" and "...past-far" to dates that are further than 7 days', async () => {
+        const future = DateParser.parseDate('in 8 days').format(TaskRegularExpressions.dateFormat);
+        await testComponentClasses(`- [ ] Full task ⏫ ➕ ${future}`, {}, LayoutClasses.createdDate, {
+            taskCreated: 'future-far',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 📅 ${future}`, {}, LayoutClasses.dueDate, {
+            taskDue: 'future-far',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ ⏳ ${future}`, {}, LayoutClasses.scheduledDate, {
+            taskScheduled: 'future-far',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 🛫 ${future}`, {}, LayoutClasses.startDate, {
+            taskStart: 'future-far',
+        });
+        await testComponentClasses(`- [x] Done task ✅ ${future}`, {}, LayoutClasses.doneDate, {
+            taskDone: 'future-far',
+        });
+        const past = DateParser.parseDate('8 days ago').format(TaskRegularExpressions.dateFormat);
+        await testComponentClasses(`- [ ] Full task ⏫ ➕ ${past}`, {}, LayoutClasses.createdDate, {
+            taskCreated: 'past-far',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 📅 ${past}`, {}, LayoutClasses.dueDate, { taskDue: 'past-far' });
+        await testComponentClasses(`- [ ] Full task ⏫ ⏳ ${past}`, {}, LayoutClasses.scheduledDate, {
+            taskScheduled: 'past-far',
+        });
+        await testComponentClasses(`- [ ] Full task ⏫ 🛫 ${past}`, {}, LayoutClasses.startDate, {
+            taskStart: 'past-far',
+        });
+        await testComponentClasses(`- [x] Done task ✅ ${past}`, {}, LayoutClasses.doneDate, { taskDone: 'past-far' });
+    });
+
+    it('does not add specific classes to invalid dates', async () => {
+        await testComponentClasses('- [ ] Full task ⏫ 📅 2023-02-29', {}, LayoutClasses.dueDate, {});
+    });
+
+    it('does not render hidden components but sets their specific classes to the upper li element', async () => {
+        await testHiddenComponentClasses(
+            '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            { hidePriority: true },
+            LayoutClasses.priority,
+            { taskPriority: 'high' },
         );
+        await testHiddenComponentClasses(
+            '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 ➕ 2022-07-04 🔁 every day',
+            { hideCreatedDate: true },
+            LayoutClasses.createdDate,
+            { taskCreated: 'past-far' },
+        );
+        await testHiddenComponentClasses(
+            '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            { hideDueDate: true },
+            LayoutClasses.dueDate,
+            { taskDue: 'past-far' },
+        );
+        await testHiddenComponentClasses(
+            '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            { hideScheduledDate: true },
+            LayoutClasses.scheduledDate,
+            { taskScheduled: 'past-far' },
+        );
+        await testHiddenComponentClasses(
+            '- [ ] Full task ⏫ 📅 2022-07-02 ⏳ 2022-07-03 🛫 2022-07-04 🔁 every day',
+            { hideStartDate: true },
+            LayoutClasses.startDate,
+            { taskStart: 'past-far' },
+        );
+    });
+
+    // Unlike the default renderer in createMockParentAndRender, this one accepts a raw HTML rather
+    // than a text, used for the following tests
+    const mockInnerHtmlRenderer = async (text: string, element: HTMLSpanElement, _path: string) => {
+        element.innerHTML = text;
+    };
+
+    /*
+     * In this test we try to imitate Obsidian's Markdown renderer more thoroughly than other tests,
+     * so we can verify that the rendering code adds the correct tag classes inside the rendered
+     * Markdown.
+     * Note that this test, just like the code that it tests, assumed a specific rendered structure
+     * by Obsidian, which is not guaranteed by the API.
+     */
+    it('adds tag attributes inside the description span', async () => {
+        const taskLine = '- [ ] Class with <a class="tag">#someTag</a>';
+        const task = fromLine({
+            line: taskLine,
+        });
+        const parentRender = await createMockParentAndRender(task, new LayoutOptions(), mockInnerHtmlRenderer);
+
+        const textSpan = getTextSpan(parentRender);
+        const descriptionSpan = textSpan.children[0].children[0] as HTMLElement;
+        expect(descriptionSpan.textContent).toEqual('Class with #someTag');
+        const tagSpan = descriptionSpan.children[0] as HTMLSpanElement;
+        expect(tagSpan.textContent).toEqual('#someTag');
+        expect(tagSpan.classList[0]).toEqual('tag');
+        expect(tagSpan.dataset.tagName).toEqual('#someTag');
+    });
+
+    it('sanitizes tag names when put into data attributes', async () => {
+        const taskLine = '- [ ] Class with <a class="tag">#illegal"data&attribute</a>';
+        const task = fromLine({
+            line: taskLine,
+        });
+        const parentRender = await createMockParentAndRender(task, new LayoutOptions(), mockInnerHtmlRenderer);
+
+        const textSpan = getTextSpan(parentRender);
+        const descriptionSpan = textSpan.children[0].children[0] as HTMLElement;
+        expect(descriptionSpan.textContent).toEqual('Class with #illegal"data&attribute');
+        const tagSpan = descriptionSpan.children[0] as HTMLSpanElement;
+        expect(tagSpan.textContent).toEqual('#illegal"data&attribute');
+        expect(tagSpan.classList[0]).toEqual('tag');
+        expect(tagSpan.dataset.tagName).toEqual('#illegal-data-attribute');
     });
 });