mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49d3ef00f | ||
|
|
479e84a21e | ||
|
|
4ea0ca06e7 | ||
|
|
d75dc9aff7 | ||
|
|
5e602a20c1 | ||
|
|
05fcf95a00 | ||
|
|
753bb3dc14 | ||
|
|
aa7c201a07 | ||
|
|
767af177e8 | ||
|
|
7415ea786e | ||
|
|
2ca2993786 | ||
|
|
fb95439452 | ||
|
|
b6ac0ea1e6 | ||
|
|
1c0ce6a6f2 | ||
|
|
bdb2179e3e | ||
|
|
bd8cd1f1d6 | ||
|
|
09f97b9c8c | ||
|
|
70c17d5de3 | ||
|
|
1f52b02bf7 | ||
|
|
7d2ecc53af | ||
|
|
66d21cc255 | ||
|
|
13a80b33e3 | ||
|
|
7847464899 | ||
|
|
7db95ca091 | ||
|
|
5c0076b9b2 | ||
|
|
7ea0fbad05 | ||
|
|
2e8472dd75 | ||
|
|
0842133db4 | ||
|
|
e25cb9796a | ||
|
|
648541d9a5 | ||
|
|
57e0e2e7b7 | ||
|
|
3b65bb3cd7 | ||
|
|
829c5c6e64 | ||
|
|
e6ef7555e3 | ||
|
|
2af6c57a49 | ||
|
|
5de91cf683 | ||
|
|
2b7fd1d1e7 | ||
|
|
c179364f2b | ||
|
|
4bee998d9b | ||
|
|
17f390545a | ||
|
|
de569d37d5 | ||
|
|
d59969cbe1 | ||
|
|
f5b636d960 | ||
|
|
7fac27b73e | ||
|
|
aa0ee4708a | ||
|
|
24c26ac855 | ||
|
|
cda217ac76 | ||
|
|
cb42bd4b4b | ||
|
|
d4c5ca1c18 | ||
|
|
61398c4e25 | ||
|
|
b62d1e8177 | ||
|
|
65fc9f38ed | ||
|
|
c8ebac32d3 | ||
|
|
219c4bd657 | ||
|
|
73e58c7b52 | ||
|
|
e4147eed09 | ||
|
|
beef6f36d8 | ||
|
|
f3df0f6856 | ||
|
|
d11dbc9d76 | ||
|
|
bb535961a3 | ||
|
|
0c7e3fb42b | ||
|
|
a6188b0060 | ||
|
|
43a6a22721 | ||
|
|
99405042ed | ||
|
|
76b103cb62 | ||
|
|
be158d4365 | ||
|
|
b31550afe3 | ||
|
|
b845fd422b | ||
|
|
feb6b008e9 | ||
|
|
0fdbada702 | ||
|
|
6d010e48e8 | ||
|
|
18d7fb0c20 | ||
|
|
00dfbcb029 | ||
|
|
89aede03f0 | ||
|
|
a387d5eb89 | ||
|
|
f1ae60f280 | ||
|
|
0e2aea626f | ||
|
|
ef9f1f7f56 | ||
|
|
d57d0c5d45 | ||
|
|
bb9ea9f1b9 | ||
|
|
7d5fde1182 | ||
|
|
33693d9315 | ||
|
|
8c52ef7bde | ||
|
|
9ce7754b1a | ||
|
|
9f2f279c20 | ||
|
|
0568149335 | ||
|
|
dde0231f19 | ||
|
|
b0dcbfd58b | ||
|
|
1b4e39b806 | ||
|
|
1fa73efe11 | ||
|
|
ddefb9f138 | ||
|
|
5e258ac218 | ||
|
|
d2b0228809 | ||
|
|
a164a849da | ||
|
|
710ef136b4 | ||
|
|
d3b7f73c66 | ||
|
|
ee5af88851 | ||
|
|
482cbc3bf6 | ||
|
|
64f1da6355 | ||
|
|
e27adececb | ||
|
|
b391aa3270 | ||
|
|
b58c02b6d0 | ||
|
|
88cad8caa2 | ||
|
|
a04d56fbde | ||
|
|
ec86b079a6 | ||
|
|
838ced0560 | ||
|
|
1c269db91d | ||
|
|
1ed5131abe | ||
|
|
d944319d53 | ||
|
|
146bbbf6a1 | ||
|
|
7bfc72469d | ||
|
|
324184964b | ||
|
|
1f6ea6ac20 | ||
|
|
5a45fdc94f | ||
|
|
b043c22437 | ||
|
|
b48e34ecb0 | ||
|
|
3bdae40ff0 | ||
|
|
94df672f4c | ||
|
|
98c5b56310 | ||
|
|
f38144b8a7 | ||
|
|
231bd89619 | ||
|
|
4907a7aaa9 | ||
|
|
2dc4865581 | ||
|
|
f1ae0d60cc | ||
|
|
2f76de2a28 | ||
|
|
d7282b18eb | ||
|
|
eb22a97198 | ||
|
|
42fe1c2887 | ||
|
|
c02275d20b | ||
|
|
147823bfd0 | ||
|
|
a7f183b6cc | ||
|
|
e10ee11f0e | ||
|
|
fca8d260d5 | ||
|
|
8cecf8d8be | ||
|
|
22ce41c3eb | ||
|
|
cb649a9a97 | ||
|
|
65d430b7cf |
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -10,12 +10,6 @@
|
||||
"values": ["#build", "#deploy", "#skip"]
|
||||
}
|
||||
],
|
||||
"workbench.colorCustomizations": {
|
||||
"titleBar.activeBackground": "#15c2cb",
|
||||
"titleBar.inactiveBackground": "#44ffd299",
|
||||
"titleBar.activeForeground": "#0E131F",
|
||||
"titleBar.inactiveForeground": "#0E131F99"
|
||||
},
|
||||
"files.exclude": {
|
||||
"out": false // set this to true to hide the "out" folder with the compiled JS files
|
||||
},
|
||||
|
||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -1,5 +1,77 @@
|
||||
# Change Log
|
||||
|
||||
## [10.10.0] - 2026-xx-xx
|
||||
|
||||
- Removed the chatbot command and all related code and references
|
||||
- [#983](https://github.com/estruyf/vscode-front-matter/issues/983): Removal of the `frontMatter.sponsors.ai.enabled` features
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#937](https://github.com/estruyf/vscode-front-matter/issues/937): Dashboard "Structure" view for documentation sites *WIP*
|
||||
- [#965](https://github.com/estruyf/vscode-front-matter/issues/965): Added SEO support for the keyword in the first paragraph
|
||||
- [#973](https://github.com/estruyf/vscode-front-matter/issues/973): Support for number fields in the snippets
|
||||
- [#990](https://github.com/estruyf/vscode-front-matter/issues/990): Schema and validation for front matter in markdown files. It can be turned off by the `frontMatter.validation.enabled` setting.
|
||||
- [#1005](https://github.com/estruyf/vscode-front-matter/issues/1005): Support the integrated VSCode browser for the preview command
|
||||
- [#1017](https://github.com/estruyf/vscode-front-matter/issues/1017): Allow adding new values for custom taxonomy fields
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#950](https://github.com/estruyf/vscode-front-matter/issues/950): Fix for template is not applied to new content type when created
|
||||
- [#958](https://github.com/estruyf/vscode-front-matter/issues/958): Fix variable frontmatter leads to error
|
||||
- [#964](https://github.com/estruyf/vscode-front-matter/issues/964): Fix settings page for dark themes
|
||||
- [#969](https://github.com/estruyf/vscode-front-matter/issues/969): Fix typo on welcome screen
|
||||
- [#972](https://github.com/estruyf/vscode-front-matter/issues/972): Fix content view sorting for Modified Date not working as expected
|
||||
- [#979](https://github.com/estruyf/vscode-front-matter/issues/979): Fix unwanted automatic updates in the publishDate field of TOML front matter
|
||||
- [#984](https://github.com/estruyf/vscode-front-matter/issues/984): Fix in `frontMatter.global.timezone` is invalid
|
||||
- [#1004](https://github.com/estruyf/vscode-front-matter/issues/1004): Fix for `mediaDB.json` containing full paths on Windows instead of relative paths
|
||||
- [#1006](https://github.com/estruyf/vscode-front-matter/issues/1006): Fix output channel colorizer schema to only apply to the Front Matter output channel
|
||||
|
||||
## [10.9.0] - 2025-07-01 - [Release notes](https://beta.frontmatter.codes/updates/v10.9.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#962](https://github.com/estruyf/vscode-front-matter/issues/962): Added Simplified Chinese localization thanks to [Randerion(HaoJun0823)](https://github.com/HaoJun0823)
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
- [#922](https://github.com/estruyf/vscode-front-matter/issues/922): Added the `{{slugifiedFileName}}` for better naming
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#933](https://github.com/estruyf/vscode-front-matter/issues/933): Timezone setting integration in the DateTime field
|
||||
- [#942](https://github.com/estruyf/vscode-front-matter/issues/942): Fix to typo on welcome screen thanks to [Stephanie Wertman](https://github.com/stephanie-wertman)
|
||||
- [#957](https://github.com/estruyf/vscode-front-matter/issues/957): Fix media assets retrieval where `mtime` is not defined. Fallback to the `mtimeMs` property if available.
|
||||
|
||||
## [10.8.0] - 2025-02-27 - [Release notes](https://beta.frontmatter.codes/updates/v10.8.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#915](https://github.com/estruyf/vscode-front-matter/issues/915): Added a new setting `frontMatter.panel.openOnSupportedFile` which allows you to open the panel view on supported files
|
||||
- [#921](https://github.com/estruyf/vscode-front-matter/issues/921): Improve the filename sanitization
|
||||
- [#922](https://github.com/estruyf/vscode-front-matter/issues/922): Added `{{fileName}}` and `{{sluggedFileName}}` placeholders for the slug template setting
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Fix for media folder parsing on Windows
|
||||
- Refresh button was not available on the media dashboard when having custom scripts defined
|
||||
- [#909](https://github.com/estruyf/vscode-front-matter/issues/909): Schema fix for the view modes
|
||||
- [#913](https://github.com/estruyf/vscode-front-matter/issues/913): Fix for relative media paths in page bundles
|
||||
- [#914](https://github.com/estruyf/vscode-front-matter/issues/914): Fix sanitizing of default filenames with an `_` in it
|
||||
|
||||
## [10.7.0] - 2024-12-31 - [Release notes](https://beta.frontmatter.codes/updates/v10.7.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#405](https://github.com/estruyf/vscode-front-matter/issues/405): Added new `frontMatter.content.grouping` setting which allows you to define custom "group by" options
|
||||
- [#705](https://github.com/estruyf/vscode-front-matter/issues/705): UX improvements for the panel view
|
||||
- [#887](https://github.com/estruyf/vscode-front-matter/issues/887): Added new `frontMatter.global.timezone` setting, by default it is set to `UTC` for date formatting
|
||||
- [#888](https://github.com/estruyf/vscode-front-matter/issues/888): Added the ability to prompt GitHub Copilot from a custom script/action
|
||||
- [#892](https://github.com/estruyf/vscode-front-matter/issues/892): Added media folder common actions
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#895](https://github.com/estruyf/vscode-front-matter/issues/895): Fix issue with array values in filters
|
||||
|
||||
## [10.6.0] - 2024-11-06 - [Release notes](https://beta.frontmatter.codes/updates/v10.6.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
@@ -117,7 +117,7 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor
|
||||
You can get the extension via:
|
||||
|
||||
- The VS Code marketplace: [VS Code Marketplace - Front Matter](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter`
|
||||
- The extension CLI: `code --install-extension eliostruyf.vscode-front-matter`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter">open extension in VS Code</a>
|
||||
|
||||
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
@@ -129,7 +129,7 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
- Uninstall the main Front Matter version
|
||||
- Install the beta version
|
||||
- VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
|
||||
- The extension CLI: `code --install-extension eliostruyf.vscode-front-matter-beta`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
|
||||
|
||||
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
|
||||
|
||||
153
README.md
153
README.md
@@ -4,7 +4,29 @@
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<h2 align="center">Front Matter a CMS running straight in Visual Studio Code</h2>
|
||||
<h2 align="center">Front Matter - A Headless CMS for Visual Studio Code</h2>
|
||||
|
||||
> **📢 2026 Open Source Priorities Update**
|
||||
>
|
||||
> I love working with and creating open source products, but after careful
|
||||
> evaluation and working with a coach, I've decided to focus my efforts on
|
||||
> creating a better revenue stream. As open-source isn't providing me a
|
||||
> sustainable income, I need to focus my time and effort more strategically on
|
||||
> how to make my work sustainable.
|
||||
>
|
||||
> **Front Matter CMS will continue to be maintained** as I use it daily.
|
||||
> However, major changes will only happen if there's a personal reason, a
|
||||
> company commitment, or significant community support. Feature requests may
|
||||
> take longer to be addressed.
|
||||
>
|
||||
> I'm shifting focus to open source projects that I can learn from or that have
|
||||
> different outcomes, like **Demo Time**, which I use when presenting at
|
||||
> conferences. If you or your company would like to sponsor my work on Front
|
||||
> Matter CMS or other projects, I'd love to discuss how we can collaborate to
|
||||
> make it even better!
|
||||
>
|
||||
> This is not about Front Matter CMS going away, but rather about managing
|
||||
> expectations around feature development timelines.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter" title="Check it out on the Visual Studio Marketplace">
|
||||
@@ -28,11 +50,17 @@
|
||||
|
||||
## ❓ What is Front Matter?
|
||||
|
||||
Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice. Jump right into editing and creating content with Front Matter and be able to preview it straight in VS Code.
|
||||
Front Matter is a CMS that runs within Visual Studio Code. It gives you the
|
||||
power and control of a full-blown CMS while also providing you the flexibility
|
||||
and speed of the static site generator of your choice. Jump right into editing
|
||||
and creating content with Front Matter and be able to preview it straight in VS
|
||||
Code.
|
||||
|
||||
The extension supports various static-site generators and frameworks like Hugo, Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
The extension supports various static-site generators and frameworks like Hugo,
|
||||
Jekyll, Hexo, NextJs, Gatsby, and more.
|
||||
|
||||
A couple of our extension highlights that hopefully get you interested in giving Front Matter a try:
|
||||
A couple of our extension highlights that hopefully get you interested in giving
|
||||
Front Matter a try:
|
||||
|
||||
- Content, data, and media management
|
||||
- Search, filter, sort, etc. all your content
|
||||
@@ -41,30 +69,40 @@ A couple of our extension highlights that hopefully get you interested in giving
|
||||
- Preview your site/content straight in Visual Studio Code
|
||||
- SEO checks for title, description, and keywords
|
||||
- Extensibility
|
||||
- As we know, we cannot support all use cases. We provide a way to extend the functionality of the extension to your needs
|
||||
- As we know, we cannot support all use cases. We provide a way to extend the
|
||||
functionality of the extension to your needs
|
||||
- and many more features ...
|
||||
|
||||
> Missing something? Let us know by opening an issue on the [GitHub repository](https://github.com/estruyf/vscode-front-matter/issues/new/choose)
|
||||
> Missing something? Let us know by opening an issue on the
|
||||
> [GitHub repository](https://github.com/estruyf/vscode-front-matter/issues/new/choose)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://frontmatter.codes/assets/marketplace/v6.0.0/content-preview.png" alt="Site preview" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
> If you see something missing in your article creation flow, please feel free
|
||||
> to reach out.
|
||||
|
||||
**Version 10**
|
||||
|
||||
In version 10, we introduced the new i18n/multilingual support for your content. You can now manage your content in multiple languages, more information can be found in the [multilingual](https://frontmatter.codes/docs/content-creation/multilingual) section of our documentation.
|
||||
In version 10, we introduced the new i18n/multilingual support for your content.
|
||||
You can now manage your content in multiple languages, more information can be
|
||||
found in the
|
||||
[multilingual](https://frontmatter.codes/docs/content-creation/multilingual)
|
||||
section of our documentation.
|
||||
|
||||

|
||||
|
||||
**Version 9**
|
||||
|
||||
The extension is now available in multiple languages: English, German, and Japanese. Want to add your language? Check out the [localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension).
|
||||
The extension is now available in multiple languages: English, German, and
|
||||
Japanese. Want to add your language? Check out the
|
||||
[localization the extension](https://frontmatter.codes/docs/contributing#translating-the-extension).
|
||||
|
||||
**Version 8**
|
||||
|
||||
The taxonomy dashboard got introduced on which you can manage your tags, categories, and custom taxonomy.
|
||||
The taxonomy dashboard got introduced on which you can manage your tags,
|
||||
categories, and custom taxonomy.
|
||||
|
||||

|
||||
|
||||
@@ -76,17 +114,24 @@ Snippets support for Front Matter has been added!
|
||||
|
||||
**Version 6**
|
||||
|
||||
In this version, we introduced the new data files/folders dashboard. You can find more information about the release in our [v6.0.0 release notes](https://frontmatter.codes/updates/v6.0.0).
|
||||
In this version, we introduced the new data files/folders dashboard. You can
|
||||
find more information about the release in our
|
||||
[v6.0.0 release notes](https://frontmatter.codes/updates/v6.0.0).
|
||||
|
||||
<p align="center">
|
||||
<img src="https://frontmatter.codes/assets/marketplace/v6.0.0/data-dashboard.png" alt="Data dashboard" style="display: inline-block" />
|
||||
</p>
|
||||
|
||||
> Data files/folders are pieces of content that do not belong to any markdown content, but live on their own. Most of the time, these data files are used to store additional information about your project/blog/website that will be used to render the content.
|
||||
> Data files/folders are pieces of content that do not belong to any markdown
|
||||
> content, but live on their own. Most of the time, these data files are used to
|
||||
> store additional information about your project/blog/website that will be used
|
||||
> to render the content.
|
||||
|
||||
**Version 5**
|
||||
|
||||
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
The new media dashboard redesign got introduced + support for setting metadata
|
||||
on media files
|
||||
[v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
|
||||
<p align="center">
|
||||
<img src="https://frontmatter.codes/assets/marketplace/v5.9.0/media-dashboard.png" alt="Data dashboard" style="display: inline-block" />
|
||||
@@ -94,15 +139,21 @@ The new media dashboard redesign got introduced + support for setting metadata o
|
||||
|
||||
**Version 4**
|
||||
|
||||
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
Support for Team level settings, content-types, and image support. Get to know
|
||||
more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
|
||||
**Version 3**
|
||||
|
||||
In version v3 we introduced the welcome and dashboard webview. The welcome view allows to get you started using the extension, and the dashboard allows you to manage all your markdown pages in one place. This makes it easy to search, filter, sort, and more.
|
||||
In version v3 we introduced the welcome and dashboard webview. The welcome view
|
||||
allows to get you started using the extension, and the dashboard allows you to
|
||||
manage all your markdown pages in one place. This makes it easy to search,
|
||||
filter, sort, and more.
|
||||
|
||||
**Version 2**
|
||||
|
||||
In version v2 we released the re-designed sidebar panel with improved SEO support. This extension makes it the only extension to manage your Markdown pages for your static sites in Visual Studio Code.
|
||||
In version v2 we released the re-designed sidebar panel with improved SEO
|
||||
support. This extension makes it the only extension to manage your Markdown
|
||||
pages for your static sites in Visual Studio Code.
|
||||
|
||||
<p align="center" style="margin-top: 2rem;">
|
||||
<a href="https://www.producthunt.com/posts/front-matter?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-front-matter" target="_blank">
|
||||
@@ -114,33 +165,47 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor
|
||||
|
||||
You can get the extension via:
|
||||
|
||||
- The VS Code marketplace: [VS Code Marketplace - Front Matter](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter).
|
||||
- The VS Code marketplace:
|
||||
[VS Code Marketplace - Front Matter](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter">open extension in VS Code</a>
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS
|
||||
Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter">open
|
||||
extension in VS Code</a>
|
||||
|
||||
> **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
> **Info**: The docs can be found on
|
||||
> [frontmatter.codes](https://frontmatter.codes).
|
||||
|
||||
### 🧪 Beta version
|
||||
|
||||
If you have the courage to test out the beta features, we made available a beta version as well. You can install this via:
|
||||
If you have the courage to test out the beta features, we made available a beta
|
||||
version as well. You can install this via:
|
||||
|
||||
- Uninstall the main Front Matter version
|
||||
- Install the beta version
|
||||
- VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- VS Code marketplace:
|
||||
[VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta).
|
||||
- The extension CLI: `ext install eliostruyf.vscode-front-matter-beta`
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
|
||||
- Or by clicking on the following link: <a href="" title="open extension in VS
|
||||
Code"
|
||||
data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open
|
||||
extension in VS Code</a>
|
||||
|
||||
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
|
||||
> **Info**: The BETA docs can be found on
|
||||
> [beta.frontmatter.codes](https://beta.frontmatter.codes).
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
All documentation can be found on [frontmatter.codes](https://frontmatter.codes).
|
||||
All documentation can be found on
|
||||
[frontmatter.codes](https://frontmatter.codes).
|
||||
|
||||
Documentation repository: [GitHub - Front Matter DOCs](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
Documentation repository:
|
||||
[GitHub - Front Matter DOCs](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
|
||||
## 💪 Contributing
|
||||
|
||||
Pull requests are welcome. Please open an issue first to discuss what you would like to change, or which problem you would like to fix. This makes it easier for us to follow-up and plan for future releases.
|
||||
Pull requests are welcome. Please open an issue first to discuss what you would
|
||||
like to change, or which problem you would like to fix. This makes it easier for
|
||||
us to follow-up and plan for future releases.
|
||||
|
||||
You can always help us improve the extension in varous ways like:
|
||||
|
||||
@@ -153,7 +218,8 @@ You can always help us improve the extension in varous ways like:
|
||||
- Tutorials
|
||||
- etc.
|
||||
|
||||
Eager to start contributing? Great 🤩, you can contribute to the following projects:
|
||||
Eager to start contributing? Great 🤩, you can contribute to the following
|
||||
projects:
|
||||
|
||||
- [Extension](https://github.com/estruyf/vscode-front-matter)
|
||||
- [Documentation](https://github.com/FrontMatter/web-documentation-nextjs)
|
||||
@@ -161,13 +227,16 @@ Eager to start contributing? Great 🤩, you can contribute to the following pro
|
||||
|
||||
## 👀 Show the work you are using Front Matter
|
||||
|
||||
Are you using Front Matter and are you interested in showing for which websites you use it? You can show your work by opening a [showcase issue](https://github.com/estruyf/vscode-front-matter/issues/new?assignees=&labels=&template=showcase.md&title=Showcase%3A+).
|
||||
Are you using Front Matter and are you interested in showing for which websites
|
||||
you use it? You can show your work by opening a
|
||||
[showcase issue](https://github.com/estruyf/vscode-front-matter/issues/new?assignees=&labels=&template=showcase.md&title=Showcase%3A+).
|
||||
|
||||
You can open showcase issues for the following things:
|
||||
|
||||
- Show the website for which you use Front Matter;
|
||||
- Share an article/video/webcast/... that explains how you use Front Matter;
|
||||
- Got something else to share? Open an issue and we can see where it fits on our website.
|
||||
- Got something else to share? Open an issue and we can see where it fits on our
|
||||
website.
|
||||
|
||||
## 👉 Contributors 🤘
|
||||
|
||||
@@ -185,33 +254,23 @@ You can open showcase issues for the following things:
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center" title="Support by run.events">
|
||||
<a href="https://run.events/?utm_source=frontmatter&utm_campaign=oss">
|
||||
<img src="https://frontmatter.codes/assets/sponsors/runevents-purple.webp" alt="run.events - Event Management Platform" height="50px" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center" title="Powered by Netlify">
|
||||
<a href="https://www.netlify.com?utm_source=vscode-frontmatter&utm_campaign=oss">
|
||||
<img src="https://frontmatter.codes/assets/sponsors/netlify-dark.png" alt="Deploys by Netlify" height="51px" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p align="center">
|
||||
<a href="http://bejs.io/" title="Supported by the BEJS Community">
|
||||
<img src="https://frontmatter.codes/assets/sponsors/bejs-community.png" alt="Supported by the BEJS Community" height="50px"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 📊 Telemetry
|
||||
|
||||
The Front Matter CMS extension only uses telemetry on application crashes. The extension respects the `telemetry.enableTelemetry` setting which you can learn more about in the [Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry).
|
||||
The Front Matter CMS extension only uses telemetry on application crashes. The
|
||||
extension respects the `telemetry.enableTelemetry` setting which you can learn
|
||||
more about in the
|
||||
[Visual Studio Code FAQ](https://aka.ms/vscode-remote/telemetry).
|
||||
|
||||
For crash reports in the webviews, we make use of Sentry to help us understand what went wrong. This data is only used to fix issues and improve the extension. You can find more information about the Sentry implementation in the following files:
|
||||
For crash reports in the webviews, we make use of Sentry to help us understand
|
||||
what went wrong. This data is only used to fix issues and improve the extension.
|
||||
You can find more information about the Sentry implementation in the following
|
||||
files:
|
||||
|
||||
- [Sentry config](https://github.com/estruyf/vscode-front-matter/blob/63e296d62f11be73ac86d9e823084247952a7ddc/src/utils/sentryInit.ts)
|
||||
|
||||
|
||||
@@ -99,11 +99,6 @@
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.seo__status__details,
|
||||
.seo__status__keywords {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.collapsible__body h4 {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"dashboard.header.tabs.taxonomies": "Taxonomien",
|
||||
"dashboard.header.viewSwitch.toGrid": "Zur Rasteransicht wechseln",
|
||||
"dashboard.header.viewSwitch.toList": "Zur Listenansicht wechseln",
|
||||
"dashboard.header.viewSwitch.toStructure": "Zur Strukturansicht wechseln",
|
||||
"dashboard.layout.sponsor.support.msg": "Unterstützen Sie Front Matter",
|
||||
"dashboard.layout.sponsor.review.label": "Bewerten",
|
||||
"dashboard.layout.sponsor.review.msg": "Bewerten Sie Front Matter",
|
||||
@@ -258,9 +259,6 @@
|
||||
"panel.fields.textField.limit": "Feldgrenze erreicht {0}",
|
||||
"panel.fields.wrapperField.unknown": "Unbekannter Feldtyp: {0}",
|
||||
"panel.actions.title": "Aktionen",
|
||||
"panel.articleDetails.title": "Weitere Details",
|
||||
"panel.articleDetails.type": "Typ",
|
||||
"panel.articleDetails.total": "Gesamt",
|
||||
"panel.articleDetails.headings": "Überschriften",
|
||||
"panel.articleDetails.paragraphs": "Absätze",
|
||||
"panel.articleDetails.internalLinks": "Interne Links",
|
||||
@@ -299,16 +297,13 @@
|
||||
"panel.publishAction.publish": "Veröffentlichen",
|
||||
"panel.publishAction.unpublish": "Zurück zu Entwurf",
|
||||
"panel.seoDetails.recommended": "Empfohlen",
|
||||
"panel.seoKeywordInfo.density": "Stichwortdichte {0} *",
|
||||
"panel.seoKeywordInfo.validInfo.label": "Verwendet in Überschrift(en)",
|
||||
"panel.seoKeywordInfo.validInfo.content": "Inhalt",
|
||||
"panel.seoKeywords.title": "Stichwörter",
|
||||
"panel.seoKeywords.header.keyword": "Stichwort",
|
||||
"panel.seoKeywords.header.details": "Details",
|
||||
"panel.seoKeywords.density": "* Eine Stichwortdichte von 1-1,5 % ist in den meisten Fällen ausreichend.",
|
||||
"panel.seoStatus.title": "Empfehlungen",
|
||||
"panel.seoKeywords.density.description": "* Eine Stichwortdichte von 1-1,5 % ist in den meisten Fällen ausreichend.",
|
||||
"panel.seoStatus.header.property": "Eigenschaft",
|
||||
"panel.seoStatus.header.length": "Länge",
|
||||
"panel.seoStatus.header.valid": "Gültig",
|
||||
"panel.seoStatus.seoFieldInfo.characters": "{0} Zeichen",
|
||||
"panel.seoStatus.seoFieldInfo.words": "{0} Wörter",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"dashboard.header.tabs.taxonomies": "Taxonomies",
|
||||
"dashboard.header.viewSwitch.toGrid": "Afficher en grille",
|
||||
"dashboard.header.viewSwitch.toList": "Afficher en liste",
|
||||
"dashboard.header.viewSwitch.toStructure": "Afficher en structure",
|
||||
"dashboard.layout.sponsor.support.msg": "Soutenir Front Matter",
|
||||
"dashboard.layout.sponsor.review.label": "Donnez votre avis",
|
||||
"dashboard.layout.sponsor.review.msg": "Donnez votre avis sur Front Matter",
|
||||
@@ -263,9 +264,6 @@
|
||||
"panel.fields.textField.limit": "Limite de champ atteinte {0}",
|
||||
"panel.fields.wrapperField.unknown": "Type de champ inconnu : {0}",
|
||||
"panel.actions.title": "Actions",
|
||||
"panel.articleDetails.title": "Plus de détails",
|
||||
"panel.articleDetails.type": "Type",
|
||||
"panel.articleDetails.total": "Total",
|
||||
"panel.articleDetails.headings": "En-têtes",
|
||||
"panel.articleDetails.paragraphs": "Paragraphes",
|
||||
"panel.articleDetails.internalLinks": "Liens internes",
|
||||
@@ -304,16 +302,13 @@
|
||||
"panel.publishAction.publish": "Publié",
|
||||
"panel.publishAction.unpublish": "Retourner au brouillon",
|
||||
"panel.seoDetails.recommended": "Recommandé",
|
||||
"panel.seoKeywordInfo.density": "Utilisation du mot clé {0} *",
|
||||
"panel.seoKeywordInfo.validInfo.label": "Utilisé dans le ou les en-tête(s)",
|
||||
"panel.seoKeywordInfo.validInfo.content": "Contenu",
|
||||
"panel.seoKeywords.title": "Mot-clés",
|
||||
"panel.seoKeywords.header.keyword": "Mot-clé",
|
||||
"panel.seoKeywords.header.details": "Détails",
|
||||
"panel.seoKeywords.density": "* Une densité de mot-clé de 1-1.5% est suffisante dans la plupart des cas",
|
||||
"panel.seoStatus.title": "Recommandations",
|
||||
"panel.seoKeywords.density.description": "* Une densité de mot-clé de 1-1.5% est suffisante dans la plupart des cas",
|
||||
"panel.seoStatus.header.property": "Propriété",
|
||||
"panel.seoStatus.header.length": "Longueur",
|
||||
"panel.seoStatus.header.valid": "Valide",
|
||||
"panel.seoStatus.seoFieldInfo.characters": "{0} caractères",
|
||||
"panel.seoStatus.seoFieldInfo.words": "{0} mots",
|
||||
|
||||
@@ -263,9 +263,6 @@
|
||||
"panel.fields.textField.limit": "Limite di campi raggiunto {0}",
|
||||
"panel.fields.wrapperField.unknown": "Tipo di campo sconosciuto: {0}",
|
||||
"panel.actions.title": "Azioni",
|
||||
"panel.articleDetails.title": "Più dettagli",
|
||||
"panel.articleDetails.type": "Digitare",
|
||||
"panel.articleDetails.total": "Totale",
|
||||
"panel.articleDetails.headings": "Intestazioni",
|
||||
"panel.articleDetails.paragraphs": "Paragrafi",
|
||||
"panel.articleDetails.internalLinks": "Collegamenti esterni",
|
||||
@@ -304,16 +301,13 @@
|
||||
"panel.publishAction.publish": "Pubblica",
|
||||
"panel.publishAction.unpublish": "Tornare alla bozza",
|
||||
"panel.seoDetails.recommended": "Raccomandato",
|
||||
"panel.seoKeywordInfo.density": "Utilizzo delle parole chiave {0} *",
|
||||
"panel.seoKeywordInfo.validInfo.label": "Utilizzato nelle rubriche",
|
||||
"panel.seoKeywordInfo.validInfo.content": "Contenuto",
|
||||
"panel.seoKeywords.title": "Parole chiavi",
|
||||
"panel.seoKeywords.header.keyword": "Parola chiave",
|
||||
"panel.seoKeywords.header.details": "Dettagli",
|
||||
"panel.seoKeywords.density": "* Una densità di parole chiave dell'1-1,5% è sufficiente nella maggior parte dei casi.",
|
||||
"panel.seoStatus.title": "Consigli",
|
||||
"panel.seoKeywords.density.description": "* Una densità di parole chiave dell'1-1,5% è sufficiente nella maggior parte dei casi.",
|
||||
"panel.seoStatus.header.property": "Proprietà",
|
||||
"panel.seoStatus.header.length": "Lunghezza",
|
||||
"panel.seoStatus.header.valid": "Valido",
|
||||
"panel.seoStatus.seoFieldInfo.characters": "{0} caratteri",
|
||||
"panel.seoStatus.seoFieldInfo.words": "{0} parole",
|
||||
|
||||
@@ -214,6 +214,7 @@
|
||||
|
||||
"dashboard.header.viewSwitch.toGrid": "グリッド表示",
|
||||
"dashboard.header.viewSwitch.toList": "リスト表示",
|
||||
"dashboard.header.viewSwitch.toStructure": "構造表示",
|
||||
|
||||
"dashboard.layout.sponsor.support.msg": "Front Matterをサポートする",
|
||||
"dashboard.layout.sponsor.review.label": "評価する",
|
||||
@@ -438,9 +439,6 @@
|
||||
|
||||
"panel.actions.title": "コマンド",
|
||||
|
||||
"panel.articleDetails.title": "詳細",
|
||||
"panel.articleDetails.type": "項目",
|
||||
"panel.articleDetails.total": "数",
|
||||
"panel.articleDetails.headings": "見出し",
|
||||
"panel.articleDetails.paragraphs": "パラグラフ",
|
||||
"panel.articleDetails.internalLinks": "内部リンク",
|
||||
@@ -489,18 +487,15 @@
|
||||
|
||||
"panel.seoDetails.recommended": "推奨",
|
||||
|
||||
"panel.seoKeywordInfo.density": "キーワード出現率 {0} *",
|
||||
"panel.seoKeywordInfo.validInfo.label": "見出しへの利用",
|
||||
"panel.seoKeywordInfo.validInfo.content": "本文",
|
||||
|
||||
"panel.seoKeywords.title": "キーワード",
|
||||
"panel.seoKeywords.header.keyword": "キーワード",
|
||||
"panel.seoKeywords.header.details": "詳細",
|
||||
"panel.seoKeywords.density": "* キーワード出現率は通常1~1.5%で十分です。",
|
||||
"panel.seoKeywords.density.description": "* キーワード出現率は通常1~1.5%で十分です。",
|
||||
|
||||
"panel.seoStatus.title": "推奨項目",
|
||||
"panel.seoStatus.header.property": "項目",
|
||||
"panel.seoStatus.header.length": "長さ",
|
||||
"panel.seoStatus.header.valid": "有効",
|
||||
"panel.seoStatus.seoFieldInfo.characters": "{0} 文字",
|
||||
"panel.seoStatus.seoFieldInfo.words": "{0} 語",
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
"settings.view.integration": "Integration",
|
||||
|
||||
"settings.openOnStartup": "Open dashboard on startup",
|
||||
"settings.openPanelForSupportedFiles": "Open panel for supported files",
|
||||
"settings.openPanelForSupportedFiles.label": "Do you want to open the panel for supported files?",
|
||||
"settings.contentTypes": "Content types",
|
||||
"settings.contentFolders": "Content folders",
|
||||
"settings.diagnostic": "Diagnostic",
|
||||
@@ -187,7 +189,7 @@
|
||||
|
||||
"dashboard.header.pagination.first": "First",
|
||||
"dashboard.header.pagination.previous": "Previous",
|
||||
"dashboard.header.pagination.next": "next",
|
||||
"dashboard.header.pagination.next": "Next",
|
||||
"dashboard.header.pagination.last": "Last",
|
||||
|
||||
"dashboard.header.paginationStatus.text": "Showing {0} to {1} of {2} results",
|
||||
@@ -220,6 +222,7 @@
|
||||
|
||||
"dashboard.header.viewSwitch.toGrid": "Change to grid",
|
||||
"dashboard.header.viewSwitch.toList": "Change to list",
|
||||
"dashboard.header.viewSwitch.toStructure": "Change to structure",
|
||||
|
||||
"dashboard.layout.sponsor.support.msg": "Support Front Matter",
|
||||
"dashboard.layout.sponsor.review.label": "Review",
|
||||
@@ -247,6 +250,7 @@
|
||||
|
||||
"dashboard.media.folderItem.contentDirectory": "Content directory",
|
||||
"dashboard.media.folderItem.publicDirectory": "Public directory",
|
||||
"dashboard.media.folderItem.deleteDescription": "Are you sure you want to delete the folder ({0})?",
|
||||
|
||||
"dashboard.media.item.buttom.insert.image": "Insert image",
|
||||
"dashboard.media.item.buttom.insert.snippet": "Insert snippet",
|
||||
@@ -329,7 +333,7 @@
|
||||
"dashboard.steps.stepsToGetStarted.tags.name": "Import all tags and categories (optional)",
|
||||
"dashboard.steps.stepsToGetStarted.tags.description": "Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content?",
|
||||
"dashboard.steps.stepsToGetStarted.git.name": "Do you want to enable Git synchronization?",
|
||||
"dashboard.steps.stepsToGetStarted.git.description": "Enable Git synchronization to eaily sync your changes with your repository.",
|
||||
"dashboard.steps.stepsToGetStarted.git.description": "Enable Git synchronization to easily sync your changes with your repository.",
|
||||
"dashboard.steps.stepsToGetStarted.showDashboard.name": "Show the dashboard",
|
||||
"dashboard.steps.stepsToGetStarted.showDashboard.description": "Once all actions are completed, the dashboard can be loaded.",
|
||||
"dashboard.steps.stepsToGetStarted.template.name": "Use a configuration template",
|
||||
@@ -368,7 +372,7 @@
|
||||
|
||||
"dashboard.welcomeScreen.title": "Manage your static site with Front Matter",
|
||||
"dashboard.welcomeScreen.thanks": "Thank you for using Front Matter!",
|
||||
"dashboard.welcomeScreen.description": "We try to aim to make Front Matter as easy to use as possible, but if you have any questions or suggestions. Please don't hesitate to reach out to us on GitHub.",
|
||||
"dashboard.welcomeScreen.description": "We aim to make Front Matter as easy to use as possible. If you have any questions or suggestions, please contact us on GitHub.",
|
||||
"dashboard.welcomeScreen.link.github.title": "GitHub",
|
||||
"dashboard.welcomeScreen.link.github.label": "GitHub",
|
||||
"dashboard.welcomeScreen.link.documentation.label": "Documentation",
|
||||
@@ -448,9 +452,6 @@
|
||||
|
||||
"panel.actions.title": "Actions",
|
||||
|
||||
"panel.articleDetails.title": "More details",
|
||||
"panel.articleDetails.type": "Type",
|
||||
"panel.articleDetails.total": "Total",
|
||||
"panel.articleDetails.headings": "Headings",
|
||||
"panel.articleDetails.paragraphs": "Paragraphs",
|
||||
"panel.articleDetails.internalLinks": "Internal links",
|
||||
@@ -499,18 +500,21 @@
|
||||
|
||||
"panel.seoDetails.recommended": "Recommended",
|
||||
|
||||
"panel.seoKeywordInfo.density": "Keyword usage {0} *",
|
||||
"panel.seoKeywordInfo.validInfo.label": "Used in heading(s)",
|
||||
"panel.seoKeywords.checks": "Checks",
|
||||
"panel.seoKeywords.density.tableTitle": "Frequency",
|
||||
"panel.seoKeywords.density": "Keyword density",
|
||||
"panel.seoKeywordInfo.validInfo.label": "Heading(s)",
|
||||
"panel.seoKeywordInfo.validInfo.content": "Content",
|
||||
"panel.seoKeywordInfo.validInfo.firstParagraph": "First paragraph",
|
||||
"panel.seoKeywordInfo.density.tooltip": "Recommended frequency: 0.75% - 1.5%",
|
||||
|
||||
"panel.seoKeywords.title": "Keywords",
|
||||
"panel.seoKeywords.header.keyword": "Keyword",
|
||||
"panel.seoKeywords.header.details": "Details",
|
||||
"panel.seoKeywords.density": "* A keyword density of 1-1.5% is sufficient in most cases.",
|
||||
"panel.seoKeywords.density.description": "* A keyword density of 1-1.5% is sufficient in most cases.",
|
||||
|
||||
"panel.seoStatus.title": "Recommendations",
|
||||
"panel.seoStatus.title": "Insights",
|
||||
"panel.seoStatus.header.property": "Property",
|
||||
"panel.seoStatus.header.length": "Length",
|
||||
"panel.seoStatus.header.valid": "Valid",
|
||||
"panel.seoStatus.seoFieldInfo.characters": "{0} chars",
|
||||
"panel.seoStatus.seoFieldInfo.words": "{0} words",
|
||||
@@ -589,7 +593,7 @@
|
||||
"commands.i18n.createOrOpen.quickPick.category.new": "New translations",
|
||||
"commands.i18n.createOrOpen.quickPick.action.create": "Create \"{0}\"",
|
||||
"commands.i18n.translate.progress.title": "Translating content...",
|
||||
|
||||
|
||||
"commands.preview.panel.title": "Preview: {0}",
|
||||
"commands.preview.askUserToPickFolder.title": "Select the folder of the article to preview",
|
||||
|
||||
@@ -776,6 +780,9 @@
|
||||
"listeners.dashboard.dashboardListener.pinItem.coundNotPin.error": "Could not pin item.",
|
||||
"listeners.dashboard.dashboardListener.pinItem.coundNotUnPin.error": "Could not unpin item.",
|
||||
|
||||
"listeners.dashboard.mediaListeners.deleteMediaFolder.progress.title": "Deleting folder...",
|
||||
"listeners.dashboard.mediaListeners.updateMediaFolder.progress.title": "Updating folder...",
|
||||
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.notification": "Template files copied.",
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.progress.title": "Downloading and initializing the template...",
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.download.error": "Failed to download the template.",
|
||||
@@ -796,7 +803,6 @@
|
||||
"listeners.panel.dataListener.createDataFile.error": "No data file id or path defined.",
|
||||
"listeners.panel.dataListener.createDataFile.noFileName": "No filename provided.",
|
||||
|
||||
|
||||
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noEditor.error": "No active editor",
|
||||
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noData.error": "No article data",
|
||||
|
||||
@@ -814,4 +820,4 @@
|
||||
"services.sponsorAi.getTaxonomySuggestions.warning": "The AI taxonomy generation took too long. Please try again later.",
|
||||
|
||||
"services.terminal.openLocalServerTerminal.terminalOption.message": "Starting local server"
|
||||
}
|
||||
}
|
||||
|
||||
822
l10n/bundle.l10n.zh-cn.json
Normal file
822
l10n/bundle.l10n.zh-cn.json
Normal file
@@ -0,0 +1,822 @@
|
||||
{
|
||||
"common.add": "添加",
|
||||
"common.edit": "编辑",
|
||||
"common.delete": "删除",
|
||||
"common.cancel": "取消",
|
||||
"common.apply": "应用",
|
||||
"common.clear": "清除",
|
||||
"common.clear.value": "清除值",
|
||||
"common.search": "搜索",
|
||||
"common.save": "保存",
|
||||
"common.menu": "菜单",
|
||||
"common.insert": "插入",
|
||||
"common.insert.snippet": "插入片段",
|
||||
"common.title": "标题",
|
||||
"common.description": "描述",
|
||||
"common.retry": "重试",
|
||||
"common.update": "更新",
|
||||
"common.information": "信息",
|
||||
"common.important": "重要",
|
||||
"common.sync": "同步",
|
||||
"common.slug": "别名",
|
||||
"common.support": "支持",
|
||||
"common.remove.value": "移除 {0}",
|
||||
"common.filter": "筛选",
|
||||
"common.filter.value": "按 {0} 筛选",
|
||||
"common.error.message": "抱歉,出错了。",
|
||||
"common.openOnWebsite": "在网站上打开",
|
||||
"common.settings": "设置",
|
||||
"common.refreshSettings": "刷新设置",
|
||||
"common.pin": "固定",
|
||||
"common.unpin": "取消固定",
|
||||
"common.noResults": "无结果",
|
||||
"common.error": "抱歉,出错了。",
|
||||
"common.yes": "是",
|
||||
"common.no": "否",
|
||||
"common.openSettings": "打开设置",
|
||||
"common.back": "返回",
|
||||
"common.open": "打开",
|
||||
"common.openWithValue": "打开:{0}",
|
||||
"common.openCustomActions": "打开自定义操作",
|
||||
"common.view": "查看",
|
||||
"common.translate": "翻译",
|
||||
"common.languages": "语言",
|
||||
"common.scripts": "脚本",
|
||||
"common.rename": "重命名",
|
||||
"common.docs": "文档",
|
||||
|
||||
"loading.initPages": "正在加载内容",
|
||||
|
||||
"notifications.outputChannel.link": "输出窗口",
|
||||
"notifications.outputChannel.description": "更多详情请查看 {0}。",
|
||||
|
||||
"settings.view.common": "通用",
|
||||
"settings.view.contentFolders": "内容文件夹",
|
||||
"settings.view.astro": "Astro",
|
||||
"settings.view.integration": "集成",
|
||||
|
||||
"settings.openOnStartup": "启动时打开仪表盘",
|
||||
"settings.openPanelForSupportedFiles": "为支持的文件打开面板",
|
||||
"settings.openPanelForSupportedFiles.label": "是否要为支持的文件打开面板?",
|
||||
"settings.contentTypes": "内容类型",
|
||||
"settings.contentFolders": "内容文件夹",
|
||||
"settings.diagnostic": "诊断",
|
||||
"settings.diagnostic.description": "您可以运行诊断程序来检查整个 Front Matter CMS 配置。",
|
||||
"settings.diagnostic.link": "运行完整诊断",
|
||||
"settings.git": "Git 同步",
|
||||
"settings.git.enabled": "启用 Git 同步,以便轻松将更改同步到您的仓库。",
|
||||
"settings.git.commitMessage": "提交消息",
|
||||
"settings.git.submoduleInfo": "当使用 Git 子模块时,您可以在文档中参考子模块设置。",
|
||||
"settings.git.submoduleLink": "了解更多关于 Git 子模块",
|
||||
"settings.integration.title": "集成",
|
||||
|
||||
"settings.commonSettings.website.title": "网站和 SSG 设置",
|
||||
"settings.commonSettings.previewUrl": "预览 URL",
|
||||
"settings.commonSettings.websiteUrl": "网站 URL",
|
||||
"settings.commonSettings.startCommand": "SSG/框架 启动命令",
|
||||
|
||||
"settings.integrationsView.deepl.title": "DeepL",
|
||||
"settings.integrationsView.deepl.intput.label": "API 密钥",
|
||||
"settings.integrationsView.deepl.intput.placeholder": "输入您的 Deepl API 密钥",
|
||||
|
||||
"settings.integrationsView.azure.title": "Azure AI 翻译服务",
|
||||
"settings.integrationsView.azure.intput.label": "订阅密钥",
|
||||
"settings.integrationsView.azure.intput.placeholder": "输入您的 Azure AI 翻译 - 订阅密钥",
|
||||
"settings.integrationsView.azure.region.label": "区域",
|
||||
"settings.integrationsView.azure.region.placeholder": "输入您的 Azure AI 翻译 - 区域。例如:westeurope",
|
||||
|
||||
"developer.title": "开发者模式",
|
||||
"developer.reload.title": "重新加载仪表盘",
|
||||
"developer.reload.label": "重新加载",
|
||||
"developer.devTools.title": "打开开发者工具",
|
||||
"developer.devTools.label": "开发者工具",
|
||||
|
||||
"field.required": "必填字段",
|
||||
"field.unknown": "未知字段",
|
||||
|
||||
"dashboard.chatbot.answer.answer": "答案",
|
||||
"dashboard.chatbot.answer.resources": "资源",
|
||||
"dashboard.chatbot.answer.warning": "警告:答案可能有误。如有疑问,请查阅文档。",
|
||||
|
||||
"dashboard.chatbot.chatbot.loading": "助手正在准备中",
|
||||
"dashboard.chatbot.chatbot.ready": "我准备好了,您想知道什么?",
|
||||
|
||||
"dashboard.chatbot.chatbox.placeholder": "如何配置 Front Matter?",
|
||||
|
||||
"dashboard.chatbot.header.heading": "询问 Front Matter AI",
|
||||
"dashboard.chatbot.header.description": "我们的 AI 由 mendable.ai 提供支持,已处理文档,可以协助您解决任何关于 Front Matter 的疑问。请随时提问!",
|
||||
|
||||
"dashboard.common.choiceButton.open": "打开选项",
|
||||
|
||||
"dashboard.contents.contentActions.actionMenuButton.title": "菜单",
|
||||
"dashboard.contents.contentActions.menuItem.view": "查看",
|
||||
"dashboard.contents.contentActions.alert.title": "删除:{0}",
|
||||
"dashboard.contents.contentActions.alert.description": "您确定要删除 \"{0}\" 内容吗?",
|
||||
"dashboard.contents.contentActions.translations.create": "创建翻译",
|
||||
"dashboard.contents.contentActions.translations.menu": "翻译",
|
||||
|
||||
"dashboard.contents.item.invalidTitle": "<无效标题>",
|
||||
"dashboard.contents.item.invalidDescription": "<无效描述>",
|
||||
|
||||
"dashboard.contents.list.title": "标题",
|
||||
"dashboard.contents.list.date": "日期",
|
||||
"dashboard.contents.list.status": "状态",
|
||||
|
||||
"dashboard.contents.overview.noMarkdown": "无 Markdown 可显示",
|
||||
"dashboard.contents.overview.noFolders": "请确保在项目中注册了一个内容文件夹,以便 Front Matter 能够找到内容。",
|
||||
"dashboard.contents.overview.pinned": "已固定",
|
||||
|
||||
"dashboard.contents.status.draft": "草稿",
|
||||
"dashboard.contents.status.published": "已发布",
|
||||
"dashboard.contents.status.scheduled": "已排期",
|
||||
|
||||
"dashboard.dataView.dataForm.modify": "修改数据",
|
||||
"dashboard.dataView.dataForm.add": "添加新数据",
|
||||
|
||||
"dashboard.dataView.dataView.select": "选择您的数据类型",
|
||||
"dashboard.dataView.dataView.title": "您的 {0} 数据项",
|
||||
"dashboard.dataView.dataView.add": "添加新条目",
|
||||
"dashboard.dataView.dataView.empty": "未找到 {0} 数据条目",
|
||||
"dashboard.dataView.dataView.createOrModify": "创建或修改您的 {0} 数据",
|
||||
"dashboard.dataView.dataView.getStarted": "选择一个数据类型开始",
|
||||
"dashboard.dataView.dataView.noDataFiles": "未找到数据文件",
|
||||
"dashboard.dataView.dataView.getStarted.link": "阅读更多以开始使用数据文件",
|
||||
"dashboard.dataView.dataView.update.message": "已更新您的数据条目",
|
||||
"dashboard.dataView.dataView.createNew": "创建新数据文件",
|
||||
"dashboard.dataView.dataView.selectDataFolder": "选择数据文件夹",
|
||||
"dashboard.dataView.dataView.closeSelectedDataFile": "关闭数据文件",
|
||||
|
||||
"dashboard.dataView.emptyView.heading": "请先选择您的数据类型",
|
||||
"dashboard.dataView.emptyView.heading.create": "通过创建新的数据文件开始",
|
||||
|
||||
"dashboard.dataView.sortableItem.editButton.title": "编辑 \"{0}\"",
|
||||
"dashboard.dataView.sortableItem.deleteButton.title": "删除 \"{0}\"",
|
||||
"dashboard.dataView.sortableItem.alert.title": "删除数据条目",
|
||||
"dashboard.dataView.sortableItem.alert.description": "您确定要删除该数据条目吗?",
|
||||
|
||||
"dashboard.errorView.description": "请关闭仪表盘并重试。",
|
||||
|
||||
"dashboard.filters.languageFilter.label": "语言",
|
||||
"dashboard.filters.languageFilter.all": "全部",
|
||||
|
||||
"dashboard.header.actionsBar.itemsSelected": "已选 {0} 项",
|
||||
"dashboard.header.actionsBar.selectAll": "全选",
|
||||
"dashboard.header.actionsBar.alertDelete.title": "删除所选文件",
|
||||
"dashboard.header.actionsBar.alertDelete.description": "您确定要删除所选文件吗?",
|
||||
|
||||
"dashboard.header.breadcrumb.home": "首页",
|
||||
|
||||
"dashboard.header.clearFilters.title": "清除筛选、分组和排序",
|
||||
|
||||
"dashboard.header.filter.default": "无筛选条件",
|
||||
|
||||
"dashboard.header.folders.default": "所有类型",
|
||||
"dashboard.header.folders.menuButton.showing": "显示",
|
||||
|
||||
"dashboard.header.grouping.option.none": "无",
|
||||
"dashboard.header.grouping.option.year": "年份",
|
||||
"dashboard.header.grouping.option.draft": "草稿/已发布",
|
||||
"dashboard.header.grouping.menuButton.label": "分组方式",
|
||||
|
||||
"dashboard.header.navigation.allArticles": "所有文章",
|
||||
"dashboard.header.navigation.published": "已发布",
|
||||
"dashboard.header.navigation.scheduled": "已排期",
|
||||
"dashboard.header.navigation.draft": "草稿中",
|
||||
|
||||
"dashboard.header.header.createContent": "创建内容",
|
||||
"dashboard.header.header.createByContentType": "按内容类型创建",
|
||||
"dashboard.header.header.createByTemplate": "按模板创建",
|
||||
|
||||
"dashboard.header.pagination.first": "首页",
|
||||
"dashboard.header.pagination.previous": "上一页",
|
||||
"dashboard.header.pagination.next": "下一页",
|
||||
"dashboard.header.pagination.last": "末页",
|
||||
|
||||
"dashboard.header.paginationStatus.text": "显示第 {0} 到 {1} 条,共 {2} 条结果",
|
||||
|
||||
"dashboard.header.projectSwitcher.label": "项目",
|
||||
|
||||
"dashboard.header.refreshDashboard.label": "刷新仪表盘",
|
||||
|
||||
"dashboard.header.sorting.lastModified.asc": "最后修改时间 (升序)",
|
||||
"dashboard.header.sorting.lastModified.desc": "最后修改时间 (降序)",
|
||||
"dashboard.header.sorting.filename.asc": "按文件名 (升序)",
|
||||
"dashboard.header.sorting.filename.desc": "按文件名 (降序)",
|
||||
"dashboard.header.sorting.published.asc": "发布日期 (升序)",
|
||||
"dashboard.header.sorting.published.desc": "发布日期 (降序)",
|
||||
"dashboard.header.sorting.size.asc": "大小 (升序)",
|
||||
"dashboard.header.sorting.size.desc": "大小 (降序)",
|
||||
"dashboard.header.sorting.caption.asc": "说明文字 (升序)",
|
||||
"dashboard.header.sorting.caption.desc": "说明文字 (降序)",
|
||||
"dashboard.header.sorting.alt.asc": "替代文本 (升序)",
|
||||
"dashboard.header.sorting.alt.desc": "替代文本 (降序)",
|
||||
"dashboard.header.sorting.label": "排序方式",
|
||||
|
||||
"dashboard.header.startup.label": "启动时打开?",
|
||||
|
||||
"dashboard.header.tabs.contents": "内容",
|
||||
"dashboard.header.tabs.media": "媒体",
|
||||
"dashboard.header.tabs.snippets": "片段",
|
||||
"dashboard.header.tabs.data": "数据",
|
||||
"dashboard.header.tabs.taxonomies": "分类法",
|
||||
|
||||
"dashboard.header.viewSwitch.toGrid": "切换到网格视图",
|
||||
"dashboard.header.viewSwitch.toList": "切换到列表视图",
|
||||
"dashboard.header.viewSwitch.toStructure": "切换到结构视图",
|
||||
|
||||
"dashboard.layout.sponsor.support.msg": "支持 Front Matter",
|
||||
"dashboard.layout.sponsor.review.label": "评价",
|
||||
"dashboard.layout.sponsor.review.msg": "评价 Front Matter",
|
||||
|
||||
"dashboard.media.common.title": "标题",
|
||||
"dashboard.media.common.caption": "说明文字",
|
||||
"dashboard.media.common.alt": "替代文本",
|
||||
"dashboard.media.common.size": "大小",
|
||||
|
||||
"dashboard.media.dialog.title": "查看详情",
|
||||
"dashboard.media.panel.close": "关闭面板",
|
||||
"dashboard.media.metadata.panel.title": "更新元数据",
|
||||
"dashboard.media.metadata.panel.description": "请指定您要为文件设置的元数据。",
|
||||
"dashboard.media.metadata.panel.field.fileName": "文件名",
|
||||
"dashboard.media.metadata.panel.form.metadata.title": "元数据",
|
||||
"dashboard.media.metadata.panel.form.information.title": "信息",
|
||||
"dashboard.media.metadata.panel.form.information.createdDate": "创建时间",
|
||||
"dashboard.media.metadata.panel.form.information.modifiedDate": "最后修改时间",
|
||||
"dashboard.media.metadata.panel.form.information.dimensions": "尺寸",
|
||||
"dashboard.media.metadata.panel.form.information.folder": "文件夹",
|
||||
|
||||
"dashboard.media.folderCreation.hexo.create": "创建文章资源文件夹",
|
||||
"dashboard.media.folderCreation.folder.create": "创建新文件夹",
|
||||
|
||||
"dashboard.media.folderItem.contentDirectory": "内容目录",
|
||||
"dashboard.media.folderItem.publicDirectory": "公共目录",
|
||||
"dashboard.media.folderItem.deleteDescription": "您确定要删除该文件夹 ({0}) 吗?",
|
||||
|
||||
"dashboard.media.item.buttom.insert.image": "插入图片",
|
||||
"dashboard.media.item.buttom.insert.snippet": "插入片段",
|
||||
|
||||
"dashboard.media.item.quickAction.insert.field": "为您的 \"{0}\" 字段插入图片",
|
||||
"dashboard.media.item.quickAction.insert.markdown": "使用 Markdown 标记插入图片",
|
||||
"dashboard.media.item.quickAction.copy.path": "复制媒体路径",
|
||||
"dashboard.media.item.quickAction.delete": "删除媒体文件",
|
||||
"dashboard.media.item.menuItem.view": "查看媒体详情",
|
||||
"dashboard.media.item.menuItem.edit.metadata": "编辑元数据",
|
||||
"dashboard.media.item.menuItem.insert.image": "插入图片",
|
||||
"dashboard.media.item.menuItem.reveal.media": "显示媒体位置",
|
||||
"dashboard.media.item.infoDialog.snippet.description": "选择要用于当前媒体文件的媒体片段。",
|
||||
"dashboard.media.item.alert.delete.description": "您确定要从 {0} 文件夹中删除该文件吗?",
|
||||
|
||||
"dashboard.media.media.description": "选择要添加到您内容中的媒体文件。",
|
||||
"dashboard.media.media.dragAndDrop": "您也可以从桌面拖放图片,上传后选择它们。",
|
||||
"dashboard.media.media.folder.upload": "上传到 {0}",
|
||||
"dashboard.media.media.folder.default": "未选择文件夹,您拖放的文件将被添加到 {0} 文件夹",
|
||||
"dashboard.media.media.placeholder": "无媒体文件可显示。按住 [Shift] 键可以拖放新文件。",
|
||||
"dashboard.media.media.contentFolder": "内容文件夹",
|
||||
"dashboard.media.media.publicFolder": "公共文件夹",
|
||||
|
||||
"dashboard.media.mediaHeaderTop.searchbox.placeholder": "在文件夹中搜索",
|
||||
|
||||
"dashboard.media.mediaSnippetForm.formDialog.title": "插入媒体:{0}",
|
||||
"dashboard.media.mediaSnippetForm.formDialog.description": "将 {0} 媒体文件插入当前文章",
|
||||
|
||||
"dashboard.preview.input.placeholder": "输入 URL",
|
||||
"dashboard.preview.button.navigate.title": "导航",
|
||||
"dashboard.preview.button.refresh.title": "刷新",
|
||||
"dashboard.preview.button.open.title": "打开",
|
||||
|
||||
"dashboard.snippetsView.item.type.content": "内容片段",
|
||||
"dashboard.snippetsView.item.type.media": "媒体片段",
|
||||
"dashboard.snippetsView.item.quickAction.editSnippet": "编辑片段",
|
||||
"dashboard.snippetsView.item.quickAction.deleteSnippet": "删除片段",
|
||||
"dashboard.snippetsView.item.quickAction.viewSnippet": "查看片段文件",
|
||||
"dashboard.snippetsView.item.insert.formDialog.title": "插入片段:{0}",
|
||||
"dashboard.snippetsView.item.insert.formDialog.description": "将 {0} 片段插入当前文章",
|
||||
"dashboard.snippetsView.item.edit.formDialog.title": "编辑片段:{0}",
|
||||
"dashboard.snippetsView.item.edit.formDialog.description": "编辑 {0} 片段",
|
||||
"dashboard.snippetsView.item.alert.title": "删除片段:{0}",
|
||||
"dashboard.snippetsView.item.alert.description": "您确定要删除 {0} 片段吗?",
|
||||
|
||||
"dashboard.snippetsView.newForm.snippetInput.title.placeholder": "片段标题",
|
||||
"dashboard.snippetsView.newForm.snippetInput.description.label": "描述",
|
||||
"dashboard.snippetsView.newForm.snippetInput.description.placeholder": "片段描述",
|
||||
"dashboard.snippetsView.newForm.snippetInput.snippet.label": "片段",
|
||||
"dashboard.snippetsView.newForm.snippetInput.snippet.placeholder": "片段内容",
|
||||
"dashboard.snippetsView.newForm.snippetInput.isMediaSnippet.label": "是媒体片段吗?",
|
||||
"dashboard.snippetsView.newForm.snippetInput.isMediaSnippet.checkbox.label": "媒体片段",
|
||||
"dashboard.snippetsView.newForm.snippetInput.isMediaSnippet.checkbox.description": "使用当前片段将媒体文件插入您的内容中。",
|
||||
"dashboard.snippetsView.newForm.snippetInput.docsButton.title": "阅读更多关于使用媒体片段占位符的信息",
|
||||
"dashboard.snippetsView.newForm.snippetInput.docsButton.description": "查看我们的媒体片段占位符文档,了解可以使用的占位符。",
|
||||
|
||||
"dashboard.snippetsView.snippets.ariaLabel": "片段标题",
|
||||
"dashboard.snippetsView.snippets.button.create": "创建新片段",
|
||||
"dashboard.snippetsView.snippets.select.description": "选择要添加到您内容中的片段。",
|
||||
"dashboard.snippetsView.snippets.empty.message": "未找到片段",
|
||||
"dashboard.snippetsView.snippets.readMore": "阅读更多以开始使用片段",
|
||||
"dashboard.snippetsView.snippets.formDialog.title": "创建片段",
|
||||
|
||||
"dashboard.steps.stepsToGetStarted.button.addFolder.title": "添加为 Front Matter 的内容文件夹",
|
||||
"dashboard.steps.stepsToGetStarted.initializeProject.name": "初始化项目",
|
||||
"dashboard.steps.stepsToGetStarted.initializeProject.description": "初始化项目将创建使用 Front Matter CMS 所需的文件和文件夹。点击此操作开始。",
|
||||
"dashboard.steps.stepsToGetStarted.framework.name": "框架预设",
|
||||
"dashboard.steps.stepsToGetStarted.framework.description": "选择您的站点生成器或框架以预填充一些推荐设置。",
|
||||
"dashboard.steps.stepsToGetStarted.framework.select": "选择您的框架",
|
||||
"dashboard.steps.stepsToGetStarted.framework.select.other": "其他",
|
||||
"dashboard.steps.stepsToGetStarted.assetsFolder.name": "您的资源文件夹是什么?",
|
||||
"dashboard.steps.stepsToGetStarted.assetsFolder.description": "选择包含您资源的文件夹。此文件夹将用于存储您文章的所有媒体文件。",
|
||||
"dashboard.steps.stepsToGetStarted.assetsFolder.public.title": "使用 'public' 文件夹",
|
||||
"dashboard.steps.stepsToGetStarted.assetsFolder.assets.title": "使用 Astro 资源文件夹 (src/assets)",
|
||||
"dashboard.steps.stepsToGetStarted.assetsFolder.other.description": "如果您想配置其他文件夹,可以手动在 frontmatter.json 文件中进行设置。",
|
||||
"dashboard.steps.stepsToGetStarted.contentFolders.name": "注册内容文件夹",
|
||||
"dashboard.steps.stepsToGetStarted.contentFolders.description": "将我们在您项目中找到的一个文件夹添加为内容文件夹。设置文件夹后,Front Matter 可以列出所有内容并允许您创建内容。",
|
||||
"dashboard.steps.stepsToGetStarted.contentFolders.label": "包含内容的文件夹:",
|
||||
"dashboard.steps.stepsToGetStarted.contentFolders.information.description": "您也可以通过右键单击资源管理器视图中的文件夹并选择“注册文件夹”来执行此操作。",
|
||||
"dashboard.steps.stepsToGetStarted.tags.name": "导入所有标签和分类(可选)",
|
||||
"dashboard.steps.stepsToGetStarted.tags.description": "现在 Front Matter 知道所有内容文件夹。您是否想从可用内容中导入所有标签和分类?",
|
||||
"dashboard.steps.stepsToGetStarted.git.name": "您要启用 Git 同步吗?",
|
||||
"dashboard.steps.stepsToGetStarted.git.description": "启用 Git 同步以轻松将更改同步到您的仓库。",
|
||||
"dashboard.steps.stepsToGetStarted.showDashboard.name": "显示仪表盘",
|
||||
"dashboard.steps.stepsToGetStarted.showDashboard.description": "所有操作完成后,即可加载仪表盘。",
|
||||
"dashboard.steps.stepsToGetStarted.template.name": "使用配置模板",
|
||||
"dashboard.steps.stepsToGetStarted.template.description": "选择一个模板,用推荐设置预填充 frontmatter.json 文件。",
|
||||
"dashboard.steps.stepsToGetStarted.template.warning": "选择模板会将整个配置应用到您的项目,并关闭此配置视图。",
|
||||
"dashboard.steps.stepsToGetStarted.astroContentTypes.name": "为您的 Astro 内容集合创建内容类型",
|
||||
|
||||
"dashboard.taxonomyView.button.add.title": "将 {0} 添加到分类法设置",
|
||||
"dashboard.taxonomyView.button.tag.title": "标记内容",
|
||||
"dashboard.taxonomyView.button.edit.title": "编辑 {0}",
|
||||
"dashboard.taxonomyView.button.merge.title": "合并 {0}",
|
||||
"dashboard.taxonomyView.button.move.title": "移动到其他分类法类型",
|
||||
"dashboard.taxonomyView.button.delete.title": "删除 {0}",
|
||||
|
||||
"dashboard.taxonomyView.taxonomyLookup.button.title": "显示包含 {1} 中 {0} 的内容",
|
||||
|
||||
"dashboard.taxonomyView.taxonomyManager.description": "创建、编辑和管理您网站的 {0}",
|
||||
"dashboard.taxonomyView.taxonomyManager.button.create": "创建新的 {0} 值",
|
||||
"dashboard.taxonomyView.taxonomyManager.table.heading.name": "名称",
|
||||
"dashboard.taxonomyView.taxonomyManager.table.heading.count": "数量",
|
||||
"dashboard.taxonomyView.taxonomyManager.table.heading.action": "操作",
|
||||
"dashboard.taxonomyView.taxonomyManager.table.row.empty": "未找到 {0}",
|
||||
"dashboard.taxonomyView.taxonomyManager.table.unmapped.title": "在您的设置中缺失",
|
||||
"dashboard.taxonomyView.taxonomyManager.filterInput.placeholder": "筛选",
|
||||
|
||||
"dashboard.taxonomyView.taxonomyTagging.pageTitle": "将您的内容映射到:{0}",
|
||||
"dashboard.taxonomyView.taxonomyTagging.checkbox": "使用 {0} 标记页面",
|
||||
|
||||
"dashboard.taxonomyView.taxonomyView.navigationBar.title": "选择分类法",
|
||||
"dashboard.taxonomyView.taxonomyView.button.import": "导入分类法",
|
||||
"dashboard.taxonomyView.taxonomyView.navigationItem.tags": "标签",
|
||||
"dashboard.taxonomyView.taxonomyView.navigationItem.categories": "分类",
|
||||
|
||||
"dashboard.unkownView.title": "视图不存在",
|
||||
"dashboard.unkownView.description": "您似乎进入了一个不存在的视图。请重新打开仪表盘。",
|
||||
|
||||
"dashboard.welcomeScreen.title": "使用 Front Matter 管理您的静态站点",
|
||||
"dashboard.welcomeScreen.thanks": "感谢您使用 Front Matter!",
|
||||
"dashboard.welcomeScreen.description": "我们致力于让 Front Matter 尽可能易于使用。如果您有任何问题或建议,请在 GitHub 上联系我们。",
|
||||
"dashboard.welcomeScreen.link.github.title": "GitHub",
|
||||
"dashboard.welcomeScreen.link.github.label": "GitHub",
|
||||
"dashboard.welcomeScreen.link.documentation.label": "文档",
|
||||
"dashboard.welcomeScreen.link.sponsor.title": "成为赞助商",
|
||||
"dashboard.welcomeScreen.link.sponsor.label": "赞助",
|
||||
"dashboard.welcomeScreen.link.review.title": "写评价",
|
||||
"dashboard.welcomeScreen.link.review.label": "评价",
|
||||
"dashboard.welcomeScreen.actions.heading": "执行以下步骤以开始使用该扩展",
|
||||
"dashboard.welcomeScreen.actions.description": "您也可以从 Front Matter 侧边栏使用该扩展。在那里您将找到可以专门为您的页面执行的操作。",
|
||||
"dashboard.welcomeScreen.actions.thanks": "我们希望您喜欢 Front Matter!",
|
||||
|
||||
"dashboard.media.detailsSlideOver.unmapped.description": "您是否要重新映射未映射文件的元数据?",
|
||||
|
||||
"dashboard.configuration.astro.astroContentTypes.empty": "未找到 Astro 内容集合。",
|
||||
"dashboard.configuration.astro.astroContentTypes.description": "以下 Astro 内容集合可用于生成内容类型。",
|
||||
|
||||
"panel.git.gitAction.title": "发布更改",
|
||||
"panel.git.gitAction.branch.select": "选择分支",
|
||||
"panel.git.gitAction.input.placeholder": "提交消息",
|
||||
"panel.git.gitAction.button.fetch": "获取",
|
||||
|
||||
"panel.contentType.contentTypeValidator.title": "内容类型",
|
||||
"panel.contentType.contentTypeValidator.hint": "我们注意到内容类型和 front matter 数据之间存在字段差异。\n 您想为此内容创建、更新或设置内容类型吗?",
|
||||
"panel.contentType.contentTypeValidator.button.create": "创建内容类型",
|
||||
"panel.contentType.contentTypeValidator.button.add": "将缺失字段添加到内容类型",
|
||||
"panel.contentType.contentTypeValidator.button.change": "更改文件的内容类型",
|
||||
|
||||
"panel.dataBlock.dataBlockField.group.selected.edit": "正在编辑:{0}",
|
||||
"panel.dataBlock.dataBlockField.group.selected.create": "创建新的 {0}",
|
||||
"panel.dataBlock.dataBlockField.group.select": "选择一个分组",
|
||||
"panel.dataBlock.dataBlockField.add": "添加 {0}",
|
||||
|
||||
"panel.dataBlock.dataBlockRecord.edit": "编辑记录",
|
||||
"panel.dataBlock.dataBlockRecord.delete": "删除记录",
|
||||
|
||||
"panel.dataBlock.dataBlockRecords.label": "记录",
|
||||
|
||||
"panel.dataBlock.dataBlockSelector.label": "区块类型",
|
||||
|
||||
"panel.errorBoundary.fieldBoundary.label": "查看字段失败",
|
||||
|
||||
"panel.fields.choiceField.select": "选择 {0}",
|
||||
"panel.fields.choiceField.clear": "清除值",
|
||||
|
||||
"panel.fields.contentTypeRelationshipField.loading": "正在获取可能的值...",
|
||||
|
||||
"panel.fields.dateTimeField.button.pick": "选择日期",
|
||||
"panel.fields.dateTimeField.time": "时间:",
|
||||
|
||||
"panel.fields.fieldMessage.required": "{0} 字段是必填的",
|
||||
|
||||
"panel.fields.fileField.delete": "删除文件",
|
||||
"panel.fields.fileField.add": "添加您的 {0}",
|
||||
|
||||
"panel.fields.imageFallback.label": "图片加载失败",
|
||||
|
||||
"panel.fields.listField.edit": "编辑记录",
|
||||
"panel.fields.listField.delete": "删除记录",
|
||||
|
||||
"panel.fields.previewImage.remove": "移除图片",
|
||||
|
||||
"panel.fields.previewImageField.add": "添加您的 {0}",
|
||||
|
||||
"panel.fields.slugField.update": "有更新可用",
|
||||
"panel.fields.slugField.generate": "生成别名",
|
||||
|
||||
"panel.fields.textField.ai.message": "使用 Front Matter AI 建议 {0}",
|
||||
"panel.fields.textField.copilot.message": "使用 Copilot 建议 {0}",
|
||||
"panel.fields.textField.ai.generate": "正在生成建议...",
|
||||
"panel.fields.textField.loading": "正在加载字段",
|
||||
"panel.fields.textField.limit": "字段限制已达 {0}",
|
||||
|
||||
"panel.fields.wrapperField.unknown": "未知字段类型:{0}",
|
||||
|
||||
"panel.fields.fieldCustomAction.button.title": "自定义操作",
|
||||
"panel.fields.fieldCustomAction.executing": "正在执行字段操作...",
|
||||
|
||||
"panel.actions.title": "操作",
|
||||
|
||||
"panel.articleDetails.headings": "标题",
|
||||
"panel.articleDetails.paragraphs": "段落",
|
||||
"panel.articleDetails.internalLinks": "内部链接",
|
||||
"panel.articleDetails.externalLinks": "外部链接",
|
||||
"panel.articleDetails.images": "图片",
|
||||
|
||||
"panel.baseView.initialize": "初始化项目",
|
||||
"panel.baseView.actions.title": "操作",
|
||||
"panel.baseView.action.openDashboard": "打开仪表盘",
|
||||
"panel.baseView.action.createContent": "创建内容",
|
||||
"panel.baseView.empty": "打开文件以查看更多操作",
|
||||
|
||||
"panel.fileList.label.singular": "个文件",
|
||||
"panel.fileList.label.plural": "个文件",
|
||||
|
||||
"panel.folderAndFiles.title": "最近修改",
|
||||
|
||||
"panel.globalSettings.title": "全局设置",
|
||||
"panel.globalSettings.action.modifiedDate.label": "修改日期",
|
||||
"panel.globalSettings.action.modifiedDate.description": "自动更新修改日期",
|
||||
"panel.globalSettings.action.frontMatter.label": "Front Matter 高亮",
|
||||
"panel.globalSettings.action.frontMatter.description": "高亮显示 Front Matter",
|
||||
"panel.globalSettings.action.preview.label": "本地预览",
|
||||
"panel.globalSettings.action.preview.placeholder": "例如:{0}",
|
||||
"panel.globalSettings.action.server.label": "本地服务器命令",
|
||||
"panel.globalSettings.action.server.placeholder": "例如:{0}",
|
||||
|
||||
"panel.metadata.title": "元数据",
|
||||
"panel.metadata.focusProblems": "查看问题视图获取更多信息",
|
||||
|
||||
"panel.otherActions.title": "其他操作",
|
||||
"panel.otherActions.writingSettings.enabled": "写作设置已启用",
|
||||
"panel.otherActions.writingSettings.disabled": "启用写作设置",
|
||||
"panel.otherActions.centerMode": "切换居中模式",
|
||||
"panel.otherActions.createTemplate": "创建模板",
|
||||
"panel.otherActions.revealFile": "在文件夹中显示文件",
|
||||
"panel.otherActions.openProject": "显示项目文件夹",
|
||||
"panel.otherActions.documentation": "打开文档",
|
||||
"panel.otherActions.settings": "设置概览",
|
||||
"panel.otherActions.issue": "报告问题",
|
||||
|
||||
"panel.preview.title": "打开预览",
|
||||
|
||||
"panel.publishAction.publish": "发布",
|
||||
"panel.publishAction.unpublish": "恢复为草稿",
|
||||
|
||||
"panel.seoDetails.recommended": "推荐",
|
||||
|
||||
"panel.seoKeywords.checks": "检查项",
|
||||
"panel.seoKeywords.density.tableTitle": "频率",
|
||||
"panel.seoKeywords.density": "关键词密度",
|
||||
"panel.seoKeywordInfo.validInfo.label": "标题",
|
||||
"panel.seoKeywordInfo.validInfo.content": "内容",
|
||||
"panel.seoKeywordInfo.density.tooltip": "推荐频率:0.75% - 1.5%",
|
||||
|
||||
"panel.seoKeywords.title": "关键词",
|
||||
"panel.seoKeywords.header.keyword": "关键词",
|
||||
"panel.seoKeywords.header.details": "详情",
|
||||
"panel.seoKeywords.density.description": "* 在大多数情况下,1-1.5% 的关键词密度就足够了。",
|
||||
|
||||
"panel.seoStatus.title": "洞察",
|
||||
"panel.seoStatus.header.property": "属性",
|
||||
"panel.seoStatus.header.valid": "有效",
|
||||
"panel.seoStatus.seoFieldInfo.characters": "{0} 个字符",
|
||||
"panel.seoStatus.seoFieldInfo.words": "{0} 个单词",
|
||||
"panel.seoStatus.seoFieldInfo.article": "文章长度",
|
||||
"panel.seoStatus.collapsible.title": "SEO 状态",
|
||||
"panel.seoStatus.required": "{0} 或 {1} 是必需的。",
|
||||
|
||||
"panel.slugAction.title": "优化别名",
|
||||
|
||||
"panel.spinner.loading": "加载中...",
|
||||
|
||||
"panel.startServerbutton.start": "启动服务器",
|
||||
"panel.startServerbutton.stop": "停止服务器",
|
||||
|
||||
"panel.tag.add": "将 {0} 添加到您的设置中",
|
||||
|
||||
"panel.tagPicker.inputPlaceholder.empty": "选择您的 {0}",
|
||||
"panel.tagPicker.inputPlaceholder.disabled": "您已达到 {0} 的限制",
|
||||
"panel.tagPicker.ai.suggest": "使用 Front Matter AI 建议 {0}",
|
||||
"panel.tagPicker.copilot.suggest": "使用 GitHub Copilot 建议 {0}",
|
||||
"panel.tagPicker.ai.generating": "正在生成建议...",
|
||||
"panel.tagPicker.limit": "最大:{0}",
|
||||
"panel.tagPicker.unkown": "添加未知标签",
|
||||
|
||||
"panel.tags.tag.warning": "请注意,标签 \"{0}\" 未保存在您的设置中。一旦移除,它将永久消失。",
|
||||
|
||||
"panel.viewPanel.mediaInsert": "继续在媒体仪表盘中选择您想要插入的图片。",
|
||||
|
||||
"commands.article.setDate.error": "解析日期格式时出错。请检查您的 \"{0}\" 设置。",
|
||||
"commands.article.updateSlug.error": "重命名文件失败:{0}",
|
||||
"commands.article.rename.fileNotExists.error": "文件不存在",
|
||||
"commands.article.rename.fileExists.error": "名为 \"{0}\" 的文件已存在",
|
||||
"commands.article.rename.fileName.title": "重命名:{0}",
|
||||
"commands.article.rename.fileName.prompt": "文件名",
|
||||
|
||||
"commands.cache.cleared": "缓存已清除",
|
||||
|
||||
"commands.chatbot.title": "问我任何问题",
|
||||
|
||||
"commands.content.option.contentType.label": "按内容类型创建内容",
|
||||
"commands.content.option.contentType.description": "选择是否要根据可用的内容类型创建新内容",
|
||||
"commands.content.option.template.label": "按模板创建内容",
|
||||
"commands.content.option.template.description": "选择是否要根据可用的模板创建新内容",
|
||||
"commands.content.quickPick.title": "创建内容",
|
||||
"commands.content.quickPick.placeholder": "选择您想要创建新内容的方式",
|
||||
|
||||
"commands.dashboard.title": "仪表盘",
|
||||
|
||||
"commands.folders.addMediaFolder.inputBox.title": "添加媒体文件夹",
|
||||
"commands.folders.addMediaFolder.inputBox.prompt": "您想给您的文件夹起什么名字(使用 \"/\" 创建多级文件夹)?",
|
||||
"commands.folders.addMediaFolder.noFolder.warning": "未指定文件夹名称。",
|
||||
"commands.folders.create.folderExists.warning": "文件夹已注册",
|
||||
"commands.folders.create.input.title": "注册文件夹",
|
||||
"commands.folders.create.input.prompt": "您想为此文件夹指定什么名称?",
|
||||
"commands.folders.create.input.placeholder": "文件夹名称",
|
||||
"commands.folders.create.success": "文件夹已注册",
|
||||
"commands.folders.getWorkspaceFolder.workspaceFolderPick.placeholder": "请选择 Front Matter 要使用的主工作区文件夹。",
|
||||
"commands.folders.get.notificationError.title": "文件夹 \"{0}\" 不存在。请从设置中移除它。",
|
||||
"commands.folders.get.notificationError.remove.action": "移除文件夹",
|
||||
"commands.folders.get.notificationError.create.action": "创建文件夹",
|
||||
|
||||
"commands.i18n.create.warning.noFileSelected": "未选择文件。",
|
||||
"commands.i18n.create.warning.noFile": "无法检索到文件。",
|
||||
"commands.i18n.create.warning.noContentType": "无法检索当前文件的内容类型。",
|
||||
"commands.i18n.create.warning.noConfig": "未找到 i18n 配置。",
|
||||
"commands.i18n.create.error.noLocaleDefinition": "无法检索当前文件的区域设置。",
|
||||
"commands.i18n.create.error.noLocales": "当前文件已翻译为所有可用语言。",
|
||||
"commands.i18n.create.error.noContentFolder": "无法为当前文件定义内容文件夹。",
|
||||
"commands.i18n.create.error.fileExists": "i18n 翻译已存在。",
|
||||
"commands.i18n.create.success.created": "已创建 \"{0}\" i18n 内容文件。",
|
||||
"commands.i18n.create.quickPick.title": "为区域创建内容",
|
||||
"commands.i18n.create.quickPick.placeHolder": "您想为哪个区域创建新内容?",
|
||||
"commands.i18n.createOrOpen.quickPick.title": "打开或创建翻译",
|
||||
"commands.i18n.createOrOpen.quickPick.category.existing": "现有翻译",
|
||||
"commands.i18n.createOrOpen.quickPick.action.open": "打开 \"{0}\"",
|
||||
"commands.i18n.createOrOpen.quickPick.category.new": "新翻译",
|
||||
"commands.i18n.createOrOpen.quickPick.action.create": "创建 \"{0}\"",
|
||||
"commands.i18n.translate.progress.title": "正在翻译内容...",
|
||||
|
||||
"commands.preview.panel.title": "预览:{0}",
|
||||
"commands.preview.askUserToPickFolder.title": "选择要预览的文章所在文件夹",
|
||||
|
||||
"commands.project.initialize.success": "项目初始化成功。",
|
||||
"commands.project.switchProject.title": "您想切换到哪个项目?",
|
||||
"commands.project.createSampleTemplate.info": "示例模板已创建。",
|
||||
|
||||
"commands.settings.create.input.prompt": "插入您要添加到配置中的 {0} 的值。",
|
||||
"commands.settings.create.input.placeholder": "{0} 的名称",
|
||||
"commands.settings.create.warning": "提供的 {0} 已存在。",
|
||||
"commands.settings.create.quickPick.placeholder": "您要将新的 {0} 添加到页面吗?",
|
||||
"commands.settings.export.progress.title": "{0}: 正在导出标签和分类",
|
||||
"commands.settings.export.progress.success": "导出完成。标签:{0} - 分类:{1}。",
|
||||
"commands.settings.remap.quickpick.title": "重新映射",
|
||||
"commands.settings.remap.quickpick.placeholder": "您想重新映射什么?",
|
||||
"commands.settings.remap.noTaxonomy.warning": "未配置 {0}。",
|
||||
"commands.settings.remap.selectTaxonomy.placeholder": "选择要插入的 {0}。",
|
||||
"commands.settings.remap.newOption.input.prompt": "指定您想用哪个 {0} 的值来重新映射 \"{1}\"。如果要从所有文章中移除该 {0},请将输入留空。",
|
||||
"commands.settings.remap.newOption.input.placeholder": "{0} 的名称",
|
||||
"commands.settings.remap.delete.placeholder": "删除 {0} {1}?",
|
||||
|
||||
"commands.statusListener.verifyRequiredFields.diagnostic.emptyField": "{0} 字段是必填的。请为该字段定义一个值。",
|
||||
"commands.statusListener.verifyRequiredFields.notification.error": "以下字段必须包含值:{0}",
|
||||
|
||||
"commands.template.generate.input.title": "模板标题",
|
||||
"commands.template.generate.input.prompt": "您想给模板起什么名字?",
|
||||
"commands.template.generate.input.placeholder": "文章",
|
||||
"commands.template.generate.noTitle.warning": "您未指定模板标题。",
|
||||
"commands.template.generate.keepContents.title": "保留内容",
|
||||
"commands.template.generate.keepContents.placeholder": "您想保留模板的内容吗?",
|
||||
"commands.template.generate.keepContents.noOption.warning": "您没有选择保留模板内容的任何选项。",
|
||||
"commands.template.generate.keepContents.success": "模板已创建,现在可在您的 {0} 文件夹中使用。",
|
||||
"commands.template.getTemplates.warning": "未找到模板。",
|
||||
"commands.template.create.folderPath.warning": "检索到的项目文件夹路径不正确。",
|
||||
"commands.template.create.noTemplates.warning": "未找到模板。",
|
||||
"commands.template.create.selectTemplate.title": "选择一个模板",
|
||||
"commands.template.create.selectTemplate.placeholder": "选择要使用的内容模板",
|
||||
"commands.template.create.selectTemplate.noTemplate.warning": "未选择模板。",
|
||||
"commands.template.create.selectTemplate.notFound.warning": "找不到内容模板。",
|
||||
"commands.template.create.success": "您的新内容现已可用。",
|
||||
|
||||
"commands.wysiwyg.command.unorderedList.label": "无序列表",
|
||||
"commands.wysiwyg.command.unorderedList.detail": "添加无序列表",
|
||||
"commands.wysiwyg.command.orderedList.label": "有序列表",
|
||||
"commands.wysiwyg.command.orderedList.detail": "添加有序列表",
|
||||
"commands.wysiwyg.command.taskList.label": "任务列表",
|
||||
"commands.wysiwyg.command.taskList.detail": "添加任务列表",
|
||||
"commands.wysiwyg.command.code.label": "代码",
|
||||
"commands.wysiwyg.command.code.detail": "添加内联代码片段",
|
||||
"commands.wysiwyg.command.codeblock.label": "代码块",
|
||||
"commands.wysiwyg.command.codeblock.detail": "添加代码块",
|
||||
"commands.wysiwyg.command.blockquote.label": "引用",
|
||||
"commands.wysiwyg.command.blockquote.detail": "添加引用块",
|
||||
"commands.wysiwyg.command.strikethrough.label": "删除线",
|
||||
"commands.wysiwyg.command.strikethrough.detail": "添加删除线文本",
|
||||
"commands.wysiwyg.quickPick.title": "WYSIWYG 选项",
|
||||
"commands.wysiwyg.quickPick.placeholder": "您想插入哪种类型的标记?",
|
||||
"commands.wysiwyg.addHyperlink.hyperlinkInput.title": "WYSIWYG 超链接",
|
||||
"commands.wysiwyg.addHyperlink.hyperlinkInput.prompt": "输入 URL",
|
||||
"commands.wysiwyg.addHyperlink.textInput.title": "WYSIWYG 文本",
|
||||
"commands.wysiwyg.addHyperlink.textInput.prompt": "输入超链接的文本",
|
||||
"commands.wysiwyg.insertText.heading.input.title": "标题级别",
|
||||
"commands.wysiwyg.insertText.heading.input.placeholder": "您想插入哪个标题级别?",
|
||||
|
||||
"helpers.articleHelper.createContent.pageBundle.error": "名为 {0} 的页面捆绑包已存在于 {1} 中。",
|
||||
"helpers.articleHelper.createContent.contentExists.warning": "标题的内容已存在。请指定一个新标题。",
|
||||
"helpers.articleHelper.processCustomPlaceholders.placeholder.error": "处理 {0} 占位符时出错。",
|
||||
"helpers.articleHelper.parseFile.diagnostic.error": "解析 {0} 的 front matter 时出错。",
|
||||
|
||||
"helpers.contentType.generate.noFrontMatter.error": "未找到 front matter 数据来生成内容类型。",
|
||||
"helpers.contentType.generate.override.quickPick.title": "覆盖默认内容类型",
|
||||
"helpers.contentType.generate.override.quickPick.placeholder": "您是否想用当前字段中使用的字段覆盖默认内容类型配置?",
|
||||
"helpers.contentType.generate.contentTypeInput.title": "生成内容类型",
|
||||
"helpers.contentType.generate.contentTypeInput.prompt": "输入要生成的内容类型的名称",
|
||||
"helpers.contentType.generate.contentTypeInput.validation.enterName": "请输入内容类型的名称。",
|
||||
"helpers.contentType.generate.contentTypeInput.validation.nameExists": "已存在具有此名称的内容类型。",
|
||||
"helpers.contentType.generate.noContentTypeName.warning": "您未指定内容类型的名称。",
|
||||
"helpers.contentType.generate.pageBundle.quickPick.title": "用作页面捆绑包",
|
||||
"helpers.contentType.generate.pageBundle.quickPick.placeHolder": "您想将此内容类型用作页面捆绑包吗?",
|
||||
"helpers.contentType.generate.updated.success": "内容类型 {0} 已更新。",
|
||||
"helpers.contentType.generate.generated.success": "内容类型 {0} 已生成。",
|
||||
"helpers.contentType.addMissingFields.noFrontMatter.warning": "未找到 front matter 数据来添加缺失字段。",
|
||||
"helpers.contentType.addMissingFields.updated.success": "内容类型 {0} 已更新。",
|
||||
"helpers.contentType.setContentType.noFrontMatter.warning": "未找到 front matter 数据来设置内容类型。",
|
||||
"helpers.contentType.setContentType.quickPick.title": "选择内容类型",
|
||||
"helpers.contentType.setContentType.quickPick.placeholder": "您想使用哪个内容类型?",
|
||||
"helpers.contentType.create.allowSubContent.title": "您想将其创建为子内容吗?",
|
||||
"helpers.contentType.create.allowSubContent.placeHolder": "您想将其创建为子内容吗?",
|
||||
"helpers.contentType.create.allowSubContent.showOpenDialog.openLabel": "选择文件夹",
|
||||
"helpers.contentType.create.allowSubContent.showOpenDialog.title": "选择创建内容的文件夹",
|
||||
"helpers.contentType.create.pageBundle.title": "创建为页面捆绑包?",
|
||||
"helpers.contentType.create.pageBundle.placeHolder": "您想将子内容创建为页面捆绑包吗?",
|
||||
"helpers.contentType.create.progress.title": "{0}: 正在创建内容...",
|
||||
"helpers.contentType.create.success": "您的新内容已创建。",
|
||||
"helpers.contentType.verify.warning": "内容类型操作在此模式下不可用。",
|
||||
|
||||
"helpers.customScript.executing": "正在执行:{0}",
|
||||
"helpers.customScript.singleRun.article.warning": "{0}: 无法检索到文章。",
|
||||
"helpers.customScript.bulkRun.noFiles.warning": "{0}: 未找到文件",
|
||||
"helpers.customScript.runMediaScript.noFolder.warning": "{0}: 未指定文件夹或媒体路径。",
|
||||
"helpers.customScript.showOutput.frontMatter.success": "{0}: front matter 已更新。",
|
||||
"helpers.customScript.showOutput.copyOutput.action": "复制输出",
|
||||
"helpers.customScript.showOutput.success": "{0}: 已执行您的自定义脚本。",
|
||||
"helpers.customScript.validateCommand.error": "无效命令:{0}",
|
||||
|
||||
"helpers.dataFileHelper.process.error": "处理数据文件时出错。",
|
||||
|
||||
"helpers.extension.getVersion.changelog": "查看更新日志",
|
||||
"helpers.extension.getVersion.starIt": "给它一个 ⭐️",
|
||||
"helpers.extension.getVersion.update.notification": "{0} 已更新至 v{1} — 查看新功能!",
|
||||
"helpers.extension.migrateSettings.templates.quickPick.title": "{0} - 模板",
|
||||
"helpers.extension.migrateSettings.templates.quickPick.placeholder": "您想继续使用模板功能吗?",
|
||||
"helpers.extension.checkIfExtensionCanRun.warning": "Front Matter BETA 无法在安装了稳定版本的情况下使用。请确保您只安装了一个版本。",
|
||||
|
||||
"helpers.mediaHelper.saveFile.folder.error": "我们找不到您选择的文件夹。",
|
||||
"helpers.mediaHelper.saveFile.file.uploaded.success": "文件 {0} 已上传至:{1}",
|
||||
"helpers.mediaHelper.saveFile.file.uploaded.failed": "抱歉,上传 {0} 时出错",
|
||||
"helpers.mediaHelper.deleteFile.file.deletion.failed": "抱歉,删除 {0} 时出错",
|
||||
|
||||
"helpers.mediaLibrary.remove.warning": "名称 \"{0}\" 在文件位置已存在。",
|
||||
"helpers.mediaLibrary.remove.error": "抱歉,将 \"{0}\" 更新为 \"{1}\" 时出错。",
|
||||
|
||||
"helpers.openFileInEditor.error": "无法打开文件。",
|
||||
|
||||
"helpers.questions.contentTitle.aiInput.title": "标题或描述",
|
||||
"helpers.questions.contentTitle.aiInput.prompt": "您想写什么?",
|
||||
"helpers.questions.contentTitle.aiInput.placeholder": "您想写什么?",
|
||||
"helpers.questions.contentTitle.aiInput.quickPick.title.separator": "您的标题/描述",
|
||||
"helpers.questions.contentTitle.aiInput.quickPick.ai.separator": "AI 生成的标题",
|
||||
"helpers.questions.contentTitle.aiInput.quickPick.copilot.separator": "GitHub Copilot 建议",
|
||||
"helpers.questions.contentTitle.aiInput.select.title": "选择一个标题",
|
||||
"helpers.questions.contentTitle.aiInput.select.placeholder": "为您的内容选择一个标题",
|
||||
"helpers.questions.contentTitle.aiInput.failed": "获取 AI 标题失败。请尝试使用您自己的标题或稍后再试。",
|
||||
"helpers.questions.contentTitle.copilotInput.failed": "获取 GitHub Copilot 标题建议失败。请尝试使用您自己的标题或稍后再试。",
|
||||
"helpers.questions.contentTitle.aiInput.warning": "您未指定内容的标题。",
|
||||
"helpers.questions.contentTitle.titleInput.title": "内容标题",
|
||||
"helpers.questions.contentTitle.titleInput.prompt": "您想为要创建的内容使用什么标题?",
|
||||
"helpers.questions.contentTitle.titleInput.placeholder": "内容标题",
|
||||
"helpers.questions.contentTitle.titleInput.warning": "您未指定内容的标题。",
|
||||
"helpers.questions.selectContentFolder.quickPick.title": "选择一个文件夹",
|
||||
"helpers.questions.selectContentFolder.quickPick.placeholder": "选择您要创建内容的位置",
|
||||
"helpers.questions.selectContentFolder.quickPick.noSelection.warning": "您没有选择要创建内容的位置。",
|
||||
"helpers.questions.selectContentType.noContentType.warning": "未找到内容类型。请先创建内容类型。",
|
||||
"helpers.questions.selectContentType.quickPick.title": "内容类型",
|
||||
"helpers.questions.selectContentType.quickPick.placeholder": "选择用于创建新内容的内容类型",
|
||||
"helpers.questions.selectContentType.noSelection.warning": "未选择内容类型。",
|
||||
"helpers.questions.selectContentType.quickPick.error.noContentTypes": "此文件夹没有配置匹配的内容类型。",
|
||||
|
||||
"helpers.seoHelper.checkLength.diagnostic.message": "文章 {0} 长度超过 {1} 个字符(当前长度:{2})。出于 SEO 原因,最好将其控制在 {1} 个字符以内。",
|
||||
|
||||
"helpers.settingsHelper.checkToPromote.message": "您有本地设置。是否要将它们提升为全局设置(\"frontmatter.json\")?",
|
||||
"helpers.settingsHelper.promote.success": "所有设置已提升到团队级别。",
|
||||
"helpers.settingsHelper.readConfig.progress.title": "{0}: 正在读取动态配置文件...",
|
||||
"helpers.settingsHelper.readConfig.error": "读取配置时出错。",
|
||||
"helpers.settingsHelper.refreshConfig.success": "设置已刷新。",
|
||||
"helpers.settingsHelper.safeUpdate.warning": "无法更新设置 \"{0}\",因为您已扩展或拆分 Front Matter CMS 配置。请手动添加您的更改。请查看输出以获取设置更新。",
|
||||
|
||||
"helpers.taxonomyHelper.rename.input.title": "重命名 {0}",
|
||||
"helpers.taxonomyHelper.rename.validate.equalValue": "新值必须与旧值不同。",
|
||||
"helpers.taxonomyHelper.rename.validate.noValue": "必须提供新值。",
|
||||
"helpers.taxonomyHelper.merge.quickPick.title": "将 \"{0}\" 与另一个 {1} 值合并",
|
||||
"helpers.taxonomyHelper.merge.quickPick.placeholder": "选择要合并的 {0} 值",
|
||||
"helpers.taxonomyHelper.delete.quickPick.title": "删除 \"{0}\" {1} 值",
|
||||
"helpers.taxonomyHelper.delete.quickPick.placeholder": "您确定要删除 \"{0}\" {1} 值吗?",
|
||||
"helpers.taxonomyHelper.createNew.input.title": "创建新的 {0} 值",
|
||||
"helpers.taxonomyHelper.createNew.input.placeholder": "输入您要添加的值",
|
||||
"helpers.taxonomyHelper.createNew.input.validate.noValue": "必须提供一个值。",
|
||||
"helpers.taxonomyHelper.createNew.input.validate.exists": "该值已存在。",
|
||||
"helpers.taxonomyHelper.process.insert": "{0}: 正在将 \"{1}\" 插入您选择的页面。",
|
||||
"helpers.taxonomyHelper.process.edit": "{0}: 正在将 \"{1}\" 从 {2} 重命名为 {3}。",
|
||||
"helpers.taxonomyHelper.process.merge": "{0}: 正在将 \"{1}\" 从 {2} 合并到 {3}。",
|
||||
"helpers.taxonomyHelper.process.delete": "{0}: 正在从 {2} 中删除 \"{1}\"。",
|
||||
"helpers.taxonomyHelper.process.insert.success": "插入完成。",
|
||||
"helpers.taxonomyHelper.process.edit.success": "编辑完成。",
|
||||
"helpers.taxonomyHelper.process.merge.success": "合并完成。",
|
||||
"helpers.taxonomyHelper.process.delete.success": "删除完成。",
|
||||
"helpers.taxonomyHelper.move.quickPick.title": "将 \"{0}\" 移动到其他类型",
|
||||
"helpers.taxonomyHelper.move.quickPick.placeholder": "选择要移动到的类型",
|
||||
"helpers.taxonomyHelper.move.progress.title": "{0}: 正在将 \"{1}\" 从 {2} 移动到 \"{3}\"。",
|
||||
"helpers.taxonomyHelper.move.success": "移动完成。",
|
||||
|
||||
"listeners.dashboard.dashboardListener.openConfig.notification": "如果您想查看配置,请打开 \"frontmatter.json\" 文件。",
|
||||
"listeners.dashboard.dashboardListener.pinItem.noPath.error": "未提供路径。",
|
||||
"listeners.dashboard.dashboardListener.pinItem.coundNotPin.error": "无法固定项目。",
|
||||
"listeners.dashboard.dashboardListener.pinItem.coundNotUnPin.error": "无法取消固定项目。",
|
||||
|
||||
"listeners.dashboard.mediaListeners.deleteMediaFolder.progress.title": "正在删除文件夹...",
|
||||
"listeners.dashboard.mediaListeners.updateMediaFolder.progress.title": "正在更新文件夹...",
|
||||
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.notification": "模板文件已复制。",
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.progress.title": "正在下载并初始化模板...",
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.download.error": "下载模板失败。",
|
||||
"listeners.dashboard.settingsListener.triggerTemplate.init.error": "初始化模板失败。",
|
||||
"listeners.dashboard.settingsListener.setSecretValue.message": "设置已更新。",
|
||||
|
||||
"listeners.dashboard.snippetListener.addSnippet.missingFields.warning": "片段缺少标题或正文",
|
||||
"listeners.dashboard.snippetListener.addSnippet.exists.warning": "已存在具有相同标题的片段",
|
||||
"listeners.dashboard.snippetListener.updateSnippet.noSnippets.warning": "没有要更新的片段",
|
||||
|
||||
"listeners.general.gitListener.push.error": "推送子模块失败。",
|
||||
|
||||
"listeners.panel.dataListener.aiSuggestTaxonomy.noEditor.error": "无活动编辑器",
|
||||
"listeners.panel.dataListener.aiSuggestTaxonomy.noData.error": "无文章数据",
|
||||
"listeners.panel.dataListener.getDataFileEntries.noDataFiles.error": "找不到数据文件条目",
|
||||
"listeners.panel.dataListener.pushMetadata.frontMatter.error": "解析您的 front matter 时出错。请检查文件内容。",
|
||||
"listeners.panel.dataListener.createDataFile.inputTitle": "数据文件的名称是什么?",
|
||||
"listeners.panel.dataListener.createDataFile.error": "未定义数据文件 ID 或路径。",
|
||||
"listeners.panel.dataListener.createDataFile.noFileName": "未提供文件名。",
|
||||
|
||||
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noEditor.error": "无活动编辑器",
|
||||
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noData.error": "无文章数据",
|
||||
|
||||
"services.copilot.getChatResponse.error": "未能从 GitHub Copilot 获得响应。",
|
||||
|
||||
"services.modeSwitch.switchMode.quickPick.placeholder": "选择您要使用的模式",
|
||||
"services.modeSwitch.switchMode.quickPick.title": "{0}: 模式选择",
|
||||
"services.modeSwitch.setText.mode": "模式:{0}",
|
||||
|
||||
"services.pagesParser.parsePages.statusBar.text": "正在处理...",
|
||||
"services.pagesParser.parsePages.file.error": "文件错误:{0}",
|
||||
|
||||
"services.sponsorAi.getTitles.warning": "AI 标题生成耗时过长。请稍后再试。",
|
||||
"services.sponsorAi.getDescription.warning": "AI 描述生成耗时过长。请稍后再试。",
|
||||
"services.sponsorAi.getTaxonomySuggestions.warning": "AI 分类法生成耗时过长。请稍后再试。",
|
||||
|
||||
"services.terminal.openLocalServerTerminal.terminalOption.message": "正在启动本地服务器"
|
||||
}
|
||||
761
package-lock.json
generated
761
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
412
package.json
412
package.json
@@ -3,14 +3,15 @@
|
||||
"displayName": "Front Matter CMS",
|
||||
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Docusaurus, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "10.6.0",
|
||||
"version": "10.9.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
"color": "#0e131f",
|
||||
"theme": "dark"
|
||||
},
|
||||
"badges": [{
|
||||
"badges": [
|
||||
{
|
||||
"description": "version",
|
||||
"url": "https://img.shields.io/github/package-json/v/estruyf/vscode-front-matter?color=green&label=vscode-front-matter&style=flat-square",
|
||||
"href": "https://github.com/estruyf/vscode-front-matter"
|
||||
@@ -70,7 +71,8 @@
|
||||
"**/.frontmatter/config/*.json": "jsonc"
|
||||
}
|
||||
},
|
||||
"keybindings": [{
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"key": "alt+d"
|
||||
},
|
||||
@@ -94,19 +96,23 @@
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
"activitybar": [{
|
||||
"id": "frontmatter-explorer",
|
||||
"title": "FM",
|
||||
"icon": "$(fm-logo)"
|
||||
}]
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "frontmatter-explorer",
|
||||
"title": "FM",
|
||||
"icon": "$(fm-logo)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"frontmatter-explorer": [{
|
||||
"id": "frontMatter.explorer",
|
||||
"name": "Front Matter",
|
||||
"icon": "$(fm-logo)",
|
||||
"type": "webview"
|
||||
}]
|
||||
"frontmatter-explorer": [
|
||||
{
|
||||
"id": "frontMatter.explorer",
|
||||
"name": "Front Matter",
|
||||
"icon": "$(fm-logo)",
|
||||
"type": "webview"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"title": "%settings.configuration.title%",
|
||||
@@ -133,12 +139,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"frontMatter.sponsors.ai.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "%setting.frontMatter.sponsors.ai.enabled.markdownDescription%",
|
||||
"scope": "Sponsors"
|
||||
},
|
||||
"frontMatter.extensibility.scripts": {
|
||||
"type": "array",
|
||||
"markdownDescription": "%setting.frontMatter.extensibility.scripts.markdownDescription%",
|
||||
@@ -174,7 +174,8 @@
|
||||
"frontMatter.content.defaultFileType": {
|
||||
"type": "string",
|
||||
"default": "md",
|
||||
"oneOf": [{
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"md",
|
||||
"mdx"
|
||||
@@ -190,7 +191,8 @@
|
||||
"frontMatter.content.defaultSorting": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"oneOf": [{
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"LastModifiedAsc",
|
||||
"LastModifiedDesc",
|
||||
@@ -476,6 +478,34 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"frontMatter.content.grouping": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "%setting.frontMatter.content.grouping.markdownDescription%",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.content.grouping.items.properties.id.description%"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.content.grouping.items.properties.title.description%"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.content.grouping.items.properties.name.description%"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"title",
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.sorting": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
@@ -550,7 +580,8 @@
|
||||
"categories"
|
||||
],
|
||||
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
|
||||
"items": [{
|
||||
"items": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"contentFolders",
|
||||
@@ -624,7 +655,8 @@
|
||||
"command": {
|
||||
"$id": "#scriptCommand",
|
||||
"type": "string",
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"node",
|
||||
"bash",
|
||||
@@ -820,7 +852,8 @@
|
||||
"title",
|
||||
"file"
|
||||
],
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"schema"
|
||||
]
|
||||
@@ -888,7 +921,8 @@
|
||||
"id",
|
||||
"path"
|
||||
],
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"schema"
|
||||
]
|
||||
@@ -1021,8 +1055,9 @@
|
||||
"panel.globalSettings",
|
||||
"panel.seo",
|
||||
"panel.actions",
|
||||
"panel.contentType",
|
||||
"panel.metadata",
|
||||
"panel.contentType",
|
||||
"panel.gitActions",
|
||||
"panel.recentlyModified",
|
||||
"panel.otherActions",
|
||||
"dashboard.snippets.view",
|
||||
@@ -1056,7 +1091,7 @@
|
||||
"error"
|
||||
],
|
||||
"markdownDescription": "%setting.frontMatter.global.notifications.markdownDescription%",
|
||||
"scope": "Templates"
|
||||
"scope": "Global"
|
||||
},
|
||||
"frontMatter.global.disabledNotifications": {
|
||||
"type": "array",
|
||||
@@ -1066,6 +1101,12 @@
|
||||
"requiredFieldValidation"
|
||||
]
|
||||
},
|
||||
"frontMatter.global.timezone": {
|
||||
"default": "UTC",
|
||||
"type": "string",
|
||||
"markdownDescription": "%setting.frontMatter.global.timezone.markdownDescription%",
|
||||
"scope": "Global"
|
||||
},
|
||||
"frontMatter.media.defaultSorting": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
@@ -1129,26 +1170,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": [{
|
||||
"name": "default",
|
||||
"fileTypes": null,
|
||||
"fields": [{
|
||||
"title": "Title",
|
||||
"name": "title",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"title": "Caption",
|
||||
"name": "caption",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"title": "Alt text",
|
||||
"name": "alt",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}],
|
||||
"default": [
|
||||
{
|
||||
"name": "default",
|
||||
"fileTypes": null,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Title",
|
||||
"name": "title",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"title": "Caption",
|
||||
"name": "caption",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"title": "Alt text",
|
||||
"name": "alt",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"scope": "Media"
|
||||
},
|
||||
"frontMatter.media.supportedMimeTypes": {
|
||||
@@ -1164,6 +1208,12 @@
|
||||
},
|
||||
"scope": "Media"
|
||||
},
|
||||
"frontMatter.panel.openOnSupportedFile": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"markdownDescription": "%setting.frontMatter.panel.openOnSupportedFile.markdownDescription%",
|
||||
"scope": "Dashboard"
|
||||
},
|
||||
"frontMatter.panel.freeform": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -1251,7 +1301,8 @@
|
||||
"fileType": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"oneOf": [{
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"md",
|
||||
"mdx"
|
||||
@@ -1397,7 +1448,8 @@
|
||||
"default": "",
|
||||
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%",
|
||||
"not": {
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
@@ -1603,7 +1655,8 @@
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"allOf": [{
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -1815,48 +1868,51 @@
|
||||
"fields"
|
||||
]
|
||||
},
|
||||
"default": [{
|
||||
"name": "default",
|
||||
"pageBundle": false,
|
||||
"fields": [{
|
||||
"title": "Title",
|
||||
"name": "title",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"title": "Description",
|
||||
"name": "description",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"title": "Publishing date",
|
||||
"name": "date",
|
||||
"type": "datetime",
|
||||
"default": "{{now}}",
|
||||
"isPublishDate": true
|
||||
},
|
||||
{
|
||||
"title": "Content preview",
|
||||
"name": "preview",
|
||||
"type": "image"
|
||||
},
|
||||
{
|
||||
"title": "Is in draft",
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"title": "Tags",
|
||||
"name": "tags",
|
||||
"type": "tags"
|
||||
},
|
||||
{
|
||||
"title": "Categories",
|
||||
"name": "categories",
|
||||
"type": "categories"
|
||||
}
|
||||
]
|
||||
}],
|
||||
"default": [
|
||||
{
|
||||
"name": "default",
|
||||
"pageBundle": false,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Title",
|
||||
"name": "title",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"title": "Description",
|
||||
"name": "description",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"title": "Publishing date",
|
||||
"name": "date",
|
||||
"type": "datetime",
|
||||
"default": "{{now}}",
|
||||
"isPublishDate": true
|
||||
},
|
||||
{
|
||||
"title": "Content preview",
|
||||
"name": "preview",
|
||||
"type": "image"
|
||||
},
|
||||
{
|
||||
"title": "Is in draft",
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"title": "Tags",
|
||||
"name": "tags",
|
||||
"type": "tags"
|
||||
},
|
||||
{
|
||||
"title": "Categories",
|
||||
"name": "categories",
|
||||
"type": "categories"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.customTaxonomy": {
|
||||
@@ -1869,7 +1925,8 @@
|
||||
"type": "string",
|
||||
"description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%",
|
||||
"not": {
|
||||
"anyOf": [{
|
||||
"anyOf": [
|
||||
{
|
||||
"const": ""
|
||||
},
|
||||
{
|
||||
@@ -2047,6 +2104,12 @@
|
||||
"markdownDescription": "%setting.frontMatter.templates.prefix.markdownDescription%",
|
||||
"scope": "Templates"
|
||||
},
|
||||
"frontMatter.validation.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"markdownDescription": "%setting.frontMatter.validation.enabled.markdownDescription%",
|
||||
"scope": "Validation"
|
||||
},
|
||||
"frontMatter.website.host": {
|
||||
"type": "string",
|
||||
"markdownDescription": "%setting.frontMatter.website.host.markdownDescription%"
|
||||
@@ -2068,7 +2131,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [{
|
||||
"commands": [
|
||||
{
|
||||
"command": "frontMatter.project.switch",
|
||||
"title": "%command.frontMatter.project.switch%",
|
||||
"category": "Front Matter",
|
||||
@@ -2327,15 +2391,6 @@
|
||||
"category": "Front Matter",
|
||||
"icon": "$(book)"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.chatbot",
|
||||
"title": "%command.frontMatter.chatbot%",
|
||||
"category": "Front Matter",
|
||||
"icon": {
|
||||
"light": "assets/icons/chatbot-light.svg",
|
||||
"dark": "assets/icons/chatbot-dark.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.promoteSettings",
|
||||
"title": "%command.frontMatter.promoteSettings%",
|
||||
@@ -2409,16 +2464,21 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"submenus": [{
|
||||
"id": "frontmatter.submenu",
|
||||
"label": "Front Matter"
|
||||
}],
|
||||
"submenus": [
|
||||
{
|
||||
"id": "frontmatter.submenu",
|
||||
"label": "Front Matter"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"webview/context": [{
|
||||
"command": "workbench.action.webview.openDeveloperTools",
|
||||
"when": "frontMatter:isDevelopment"
|
||||
}],
|
||||
"editor/title": [{
|
||||
"webview/context": [
|
||||
{
|
||||
"command": "workbench.action.webview.openDeveloperTools",
|
||||
"when": "frontMatter:isDevelopment"
|
||||
}
|
||||
],
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "frontMatter.markup.heading",
|
||||
"group": "navigation@-133",
|
||||
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg && activeEditor == 'workbench.editors.files.textFileEditor'"
|
||||
@@ -2497,18 +2557,16 @@
|
||||
"command": "frontMatter.dashboard.close",
|
||||
"group": "navigation@-98",
|
||||
"when": "frontMatter:enabled == true && frontMatter:dashboard:open == true"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.chatbot",
|
||||
"group": "navigation@-97",
|
||||
"when": "resourceFilename == 'frontmatter.json'"
|
||||
}
|
||||
],
|
||||
"explorer/context": [{
|
||||
"submenu": "frontmatter.submenu",
|
||||
"group": "frontmatter@1"
|
||||
}],
|
||||
"frontmatter.submenu": [{
|
||||
"explorer/context": [
|
||||
{
|
||||
"submenu": "frontmatter.submenu",
|
||||
"group": "frontmatter@1"
|
||||
}
|
||||
],
|
||||
"frontmatter.submenu": [
|
||||
{
|
||||
"command": "frontMatter.createFromTemplate",
|
||||
"when": "explorerResourceIsFolder",
|
||||
"group": "frontmatter@1"
|
||||
@@ -2524,7 +2582,8 @@
|
||||
"group": "frontmatter@3"
|
||||
}
|
||||
],
|
||||
"commandPalette": [{
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "frontMatter.init",
|
||||
"when": "frontMatterCanInit"
|
||||
},
|
||||
@@ -2701,16 +2760,12 @@
|
||||
"when": "frontMatter:file:isValid == true"
|
||||
}
|
||||
],
|
||||
"view/title": [{
|
||||
"view/title": [
|
||||
{
|
||||
"command": "frontMatter.docs",
|
||||
"group": "navigation@-1",
|
||||
"when": "view == frontMatter.explorer"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.chatbot",
|
||||
"group": "navigation@0",
|
||||
"when": "view == frontMatter.explorer"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.mode.switch",
|
||||
"group": "navigation@1",
|
||||
@@ -2738,13 +2793,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"languages": [{
|
||||
"id": "frontmatter.project.output",
|
||||
"mimetypes": [
|
||||
"text/x-code-output"
|
||||
]
|
||||
}],
|
||||
"grammars": [{
|
||||
"languages": [
|
||||
{
|
||||
"id": "frontmatter.project.output"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"path": "./syntaxes/hugo.tmLanguage.json",
|
||||
"scopeName": "frontmatter.markdown.hugo",
|
||||
"injectTo": [
|
||||
@@ -2757,45 +2812,48 @@
|
||||
"path": "./syntaxes/frontmatter-output.tmLanguage.json"
|
||||
}
|
||||
],
|
||||
"walkthroughs": [{
|
||||
"id": "frontmatter.welcome",
|
||||
"title": "Get started with Front Matter",
|
||||
"description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.",
|
||||
"steps": [{
|
||||
"id": "frontmatter.welcome.init",
|
||||
"title": "Get started",
|
||||
"description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)",
|
||||
"media": {
|
||||
"markdown": "assets/walkthrough/get-started.md"
|
||||
"walkthroughs": [
|
||||
{
|
||||
"id": "frontmatter.welcome",
|
||||
"title": "Get started with Front Matter",
|
||||
"description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.",
|
||||
"steps": [
|
||||
{
|
||||
"id": "frontmatter.welcome.init",
|
||||
"title": "Get started",
|
||||
"description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)",
|
||||
"media": {
|
||||
"markdown": "assets/walkthrough/get-started.md"
|
||||
},
|
||||
"completionEvents": [
|
||||
"onContext:frontMatterInitialized"
|
||||
]
|
||||
},
|
||||
"completionEvents": [
|
||||
"onContext:frontMatterInitialized"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "frontmatter.welcome.documentation",
|
||||
"title": "Documentation",
|
||||
"description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)",
|
||||
"media": {
|
||||
"markdown": "assets/walkthrough/documentation.md"
|
||||
{
|
||||
"id": "frontmatter.welcome.documentation",
|
||||
"title": "Documentation",
|
||||
"description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)",
|
||||
"media": {
|
||||
"markdown": "assets/walkthrough/documentation.md"
|
||||
},
|
||||
"completionEvents": [
|
||||
"onLink:https://frontmatter.codes/docs"
|
||||
]
|
||||
},
|
||||
"completionEvents": [
|
||||
"onLink:https://frontmatter.codes/docs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "frontmatter.welcome.supporter",
|
||||
"title": "Support the project",
|
||||
"description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)",
|
||||
"media": {
|
||||
"markdown": "assets/walkthrough/support-the-project.md"
|
||||
},
|
||||
"completionEvents": [
|
||||
"onLink:https://github.com/sponsors/estruyf"
|
||||
]
|
||||
}
|
||||
]
|
||||
}]
|
||||
{
|
||||
"id": "frontmatter.welcome.supporter",
|
||||
"title": "Support the project",
|
||||
"description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)",
|
||||
"media": {
|
||||
"markdown": "assets/walkthrough/support-the-project.md"
|
||||
},
|
||||
"completionEvents": [
|
||||
"onLink:https://github.com/sponsors/estruyf"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*",
|
||||
@@ -2855,7 +2913,8 @@
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"clsx": "^2.1.0",
|
||||
"css-loader": "5.2.7",
|
||||
"date-fns": "2.23.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"downshift": "6.0.6",
|
||||
"eslint": "^8.33.0",
|
||||
@@ -2893,6 +2952,7 @@
|
||||
"react-quill": "^2.0.0",
|
||||
"react-router-dom": "^6.8.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-remark": "^10.0.0",
|
||||
@@ -2931,4 +2991,4 @@
|
||||
"vsce": {
|
||||
"dependencies": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"setting.frontMatter.projects.markdownDescription": "Geben Sie die Liste der Projekte an, die in Front Matter CMS geladen werden sollen. [Dokumentation prüfen](https://frontmatter.codes/docs/settings/overview#frontmatter.projects)",
|
||||
"setting.frontMatter.projects.items.properties.name.markdownDescription": "Geben Sie den Namen des Projekts an.",
|
||||
"setting.frontMatter.projects.items.properties.default.markdownDescription": "Geben Sie an, ob dieses Projekt das Standardprojekt zum Laden ist.",
|
||||
"setting.frontMatter.sponsors.ai.enabled.markdownDescription": "Geben Sie an, ob Sie KI-Vorschläge aktivieren möchten. [Dokumentation prüfen](https://frontmatter.codes/docs/settings/overview#frontmatter.sponsors.ai.enabled)",
|
||||
"setting.frontMatter.extensibility.scripts.markdownDescription": "Geben Sie die Liste der Skripte an, die in Front Matter CMS geladen werden sollen. [Dokumentation prüfen](https://frontmatter.codes/docs/settings/overview#frontmatter.extensibility.scripts)",
|
||||
"setting.frontMatter.experimental.markdownDescription": "Geben Sie an, ob Sie experimentelle Funktionen aktivieren möchten. [Dokumentation prüfen](https://frontmatter.codes/docs/settings/overview#frontmatter.experimental)",
|
||||
"setting.frontMatter.extends.markdownDescription": "Geben Sie die Liste der Pfade/URLs an, um die Front Matter CMS-Konfiguration zu erweitern. [Dokumentation prüfen](https://frontmatter.codes/docs/settings/overview#frontmatter.extends)",
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"setting.frontMatter.projects.markdownDescription": "Front Matter CMSを利用するプロジェクトを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.projects)",
|
||||
"setting.frontMatter.projects.items.properties.name.markdownDescription": "プロジェクトの名前を指定します。",
|
||||
"setting.frontMatter.projects.items.properties.default.markdownDescription": "このプロジェクトを読み込む既定のプロジェクトにするかどうかを指定します。",
|
||||
"setting.frontMatter.sponsors.ai.enabled.markdownDescription": "AIによる提案を利用します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.sponsors.ai.enabled)",
|
||||
"setting.frontMatter.extensibility.scripts.markdownDescription": "Front Matter CMSで読み込むスクリプトのリストを指定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.extensibility.scripts)",
|
||||
"setting.frontMatter.experimental.markdownDescription": "実験的な機能をオンにします。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.experimental)",
|
||||
"setting.frontMatter.extends.markdownDescription": "Front Matter CMSの構成を拡張するパス/URLのリストを設定します。[ドキュメントを確認](https://frontmatter.codes/docs/settings/overview#frontmatter.extends)",
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
"setting.frontMatter.projects.markdownDescription": "Specify the list of projects to load in the Front Matter CMS. [Local](https://file%2B.vscode-resource.vscode-cdn.net/Users/eliostruyf/nodejs/frontmatter-test-projects/astro-blog/test.html) - [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.projects) - [View in VS Code](vscode://simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.projects%22%5D)",
|
||||
"setting.frontMatter.projects.items.properties.name.markdownDescription": "Specify the name of the project.",
|
||||
"setting.frontMatter.projects.items.properties.default.markdownDescription": "Specify if this project is the default project to load.",
|
||||
"setting.frontMatter.sponsors.ai.enabled.markdownDescription": "Specify if you want to enable AI suggestions. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.sponsors.ai.enabled) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.sponsors.ai.enabled%22%5D)",
|
||||
"setting.frontMatter.extensibility.scripts.markdownDescription": "Specify the list of scripts to load in the Front Matter CMS. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.extensibility.scripts) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.extensibility.scripts%22%5D)",
|
||||
"setting.frontMatter.experimental.markdownDescription": "Specify if you want to enable the experimental features. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.experimental) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.experimental%22%5D)",
|
||||
"setting.frontMatter.extends.markdownDescription": "Specify the list of paths/URLs to extend the Front Matter CMS config. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.extends) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.extends%22%5D)",
|
||||
@@ -96,6 +95,10 @@
|
||||
"setting.frontMatter.content.publicFolder.properties.relative.description": "Defines if the path to your media files be relative to the content file?",
|
||||
"setting.frontMatter.snippets.wrapper.enabled.markdownDescription": "Specify if you want to wrap the snippets. [Docs](https://frontmatter.codes/docs/settings/overview#frontMatter.snippets.wrapper.enabled) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontMatter.snippets.wrapper.enabled%22%5D)",
|
||||
"setting.frontMatter.content.snippets.markdownDescription": "Define the snippets you want to use in your content. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.snippets) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.snippets%22%5D)",
|
||||
"setting.frontMatter.content.grouping.markdownDescription": "Specify the grouping options for your dashboard content. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.grouping) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.grouping%22%5D)",
|
||||
"setting.frontMatter.content.grouping.items.properties.id.description": "The ID of the grouping option.",
|
||||
"setting.frontMatter.content.grouping.items.properties.title.description": "Title of the grouping which will be shown in the UI.",
|
||||
"setting.frontMatter.content.grouping.items.properties.name.description": "Name of the content-type field to group by.",
|
||||
"setting.frontMatter.content.sorting.markdownDescription": "Define the sorting options for your dashboard content. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.sorting) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.sorting%22%5D)",
|
||||
"setting.frontMatter.content.sorting.items.properties.id.description": "The ID of the sorting option. This will be used for the storing the last used sorting option or the default option.",
|
||||
"setting.frontMatter.content.sorting.items.properties.title.description": "Name of the sorting label",
|
||||
@@ -166,6 +169,7 @@
|
||||
"setting.frontMatter.global.modes.items.properties.features.description": "The features you want to use for your mode.",
|
||||
"setting.frontMatter.global.notifications.markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.global.notifications) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.global.notifications%22%5D)",
|
||||
"setting.frontMatter.global.disabledNotifications.markdownDescription": "This is an array with the notifications types that can be disabled for Front Matter CMS. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.global.disablednotifications) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.global.disablednotifications%22%5D)",
|
||||
"setting.frontMatter.global.timezone.markdownDescription": "Specify the timezone for your date formatting. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.datetimezone) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.datetimezone%22%5D)",
|
||||
"setting.frontMatter.media.defaultSorting.markdownDescription": "Specify the default sorting option for the media dashboard. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.media.defaultsorting) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.media.defaultsorting%22%5D)",
|
||||
"setting.frontMatter.media.supportedMimeTypes.markdownDescription": "Specify the mime types to support for the media files. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.media.supportedmimetypes) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.media.supportedmimetypes%22%5D)",
|
||||
|
||||
@@ -179,6 +183,7 @@
|
||||
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.type.description": "Define the type of field",
|
||||
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.single.description": "Is a single line field",
|
||||
|
||||
"setting.frontMatter.panel.openOnSupportedFile.markdownDescription": "Specifies if you want to open the panel when opening a supported file. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.openonsupportedfile) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.openonsupportedfile%22%5D)",
|
||||
"setting.frontMatter.panel.freeform.markdownDescription": "Specifies if you want to allow yourself from entering unknown tags/categories in the tag picker (when enabled, you will have the option to store them afterwards). Default: true. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.freeform) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.freeform%22%5D)",
|
||||
"setting.frontMatter.panel.actions.disabled.markdownDescription": "Specify the actions you want to disable in the panel. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.actions.disabled) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.actions.disabled%22%5D)",
|
||||
"setting.frontMatter.preview.host.markdownDescription": "Specify the host URL (example: http://localhost:1313) to be used when opening the preview. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.host) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.preview.host%22%5D)",
|
||||
@@ -271,6 +276,7 @@
|
||||
"setting.frontMatter.taxonomy.tags.markdownDescription": "Specifies the tags which can be used in the Front Matter. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.tags) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.tags%22%5D)",
|
||||
"setting.frontMatter.telemetry.disable.markdownDescription": "Specify if you want to disable the telemetry. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.telemetry.disable) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.telemetry.disable%22%5D)",
|
||||
"setting.frontMatter.templates.enabled.markdownDescription": "Specify if you want to use templates. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.enabled) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.enabled%22%5D)",
|
||||
"setting.frontMatter.validation.enabled.markdownDescription": "Specify if you want to enable front matter validation. When enabled, the extension will validate your front matter against the content type schema. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.validation.enabled) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.validation.enabled%22%5D)",
|
||||
"setting.frontMatter.templates.folder.markdownDescription": "Specify the folder to use for your article templates. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.folder) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.folder%22%5D)",
|
||||
"setting.frontMatter.templates.prefix.markdownDescription": "Specify the prefix you want to add for your new article filenames. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.prefix) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.prefix%22%5D)",
|
||||
"setting.frontMatter.dashboard.mediaSnippet.deprecationMessage": "This setting is deprecated and will be removed in the next major version. Please define your media snippet in the `frontMatter.content.snippet` setting.",
|
||||
@@ -287,4 +293,4 @@
|
||||
"setting.frontMatter.git.disableOnBranches.markdownDescription": "Specify the branches on which you want to disable the Git actions. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.git.disableonbranches) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.disableonbranches%22%5D)",
|
||||
"setting.frontMatter.git.requiresCommitMessage.markdownDescription": "Specify if you want to require a commit message when publishing your changes for a specified branch. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.git.requirescommitmessage) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.requirescommitmessage%22%5D)",
|
||||
"setting.frontMatter.copilot.family.markdownDescription": "Specify the LLM family of the Copilot you want to use. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.copilot.family) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.copilot.family%22%5D)"
|
||||
}
|
||||
}
|
||||
|
||||
295
package.nls.zh-cn.json
Normal file
295
package.nls.zh-cn.json
Normal file
@@ -0,0 +1,295 @@
|
||||
{
|
||||
"command.frontMatter.project.switch": "切换项目",
|
||||
"command.frontMatter.config.reload": "重新加载配置",
|
||||
"command.frontMatter.authenticate": "身份验证",
|
||||
"command.frontMatter.contenttype.generate": "从当前文件生成内容类型",
|
||||
"command.frontMatter.contenttype.addMissingFields": "将缺失字段从前端元数据添加到内容类型",
|
||||
"command.frontMatter.contenttype.setContentType": "设置当前文件使用的内容类型",
|
||||
"command.frontMatter.markup.blockquote": "引用",
|
||||
"command.frontMatter.markup.bold": "加粗",
|
||||
"command.frontMatter.dashboard.close": "关闭仪表盘",
|
||||
"command.frontMatter.markup.code": "代码",
|
||||
"command.frontMatter.markup.codeblock": "代码块",
|
||||
"command.frontMatter.markup.hyperlink": "超链接",
|
||||
"command.frontMatter.collapseSections": "折叠部分",
|
||||
"command.frontMatter.initTemplate": "初始化模板文件夹",
|
||||
"command.frontMatter.createTemplate": "从当前文件创建模板",
|
||||
"command.frontMatter.createCategory": "创建分类",
|
||||
"command.frontMatter.createContent": "创建新内容",
|
||||
"command.frontMatter.createTag": "创建标签",
|
||||
"command.frontMatter.diagnostics": "诊断日志",
|
||||
"command.frontMatter.exportTaxonomy": "将所有标签和分类导出到设置",
|
||||
"command.frontMatter.createFromTemplate": "从模板创建新文章",
|
||||
"command.frontMatter.registerFolder": "注册文件夹",
|
||||
"command.frontMatter.unregisterFolder": "取消注册文件夹",
|
||||
"command.frontMatter.generateSlug": "基于内容标题生成别名",
|
||||
"command.frontMatter.markup.heading": "标题",
|
||||
"command.frontMatter.init": "初始化项目",
|
||||
"command.frontMatter.insertCategories": "插入分类",
|
||||
"command.frontMatter.insertMedia": "将媒体插入内容",
|
||||
"command.frontMatter.insertSnippet": "将片段插入内容",
|
||||
"command.frontMatter.insertTags": "插入标签",
|
||||
"command.frontMatter.markup.italic": "斜体",
|
||||
"command.frontMatter.dashboard": "打开仪表盘",
|
||||
"command.frontMatter.dashboard.data": "打开数据仪表盘",
|
||||
"command.frontMatter.dashboard.media": "打开媒体仪表盘",
|
||||
"command.frontMatter.dashboard.snippets": "打开片段仪表盘",
|
||||
"command.frontMatter.dashboard.taxonomy": "打开分类法仪表盘",
|
||||
"command.frontMatter.markup.orderedlist": "有序列表",
|
||||
"command.frontMatter.markup.options": "其他标记选项",
|
||||
"command.frontMatter.preview": "预览内容",
|
||||
"command.frontMatter.docs": "文档",
|
||||
"command.frontMatter.chatbot": "向 Front Matter AI 寻求帮助",
|
||||
"command.frontMatter.promoteSettings": "将设置从本地提升到团队级别",
|
||||
"command.frontMatter.remap": "在所有文章中重新映射或移除标签/分类",
|
||||
"command.frontMatter.setLastModifiedDate": "设置最后修改日期",
|
||||
"command.frontMatter.markup.strikethrough": "删除线",
|
||||
"command.frontMatter.mode.switch": "切换模式",
|
||||
"command.frontMatter.markup.tasklist": "任务列表",
|
||||
"command.frontMatter.markup.unorderedlist": "无序列表",
|
||||
"command.frontMatter.git.sync": "同步",
|
||||
"command.frontMatter.cache.clear": "清除缓存",
|
||||
"command.frontMatter.i18n.create": "创建新翻译",
|
||||
"command.frontMatter.i18n.createOrOpen": "创建或打开翻译",
|
||||
"settings.configuration.title": "Front Matter: 使用 frontmatter.json 进行团队共享设置",
|
||||
"setting.frontMatter.projects.markdownDescription": "指定要在 Front Matter CMS 中加载的项目列表。[本地](https://file%2B.vscode-resource.vscode-cdn.net/Users/eliostruyf/nodejs/frontmatter-test-projects/astro-blog/test.html) - [文档](https://frontmatter.codes/docs/settings/overview#frontmatter.projects) - [在 VS Code 中查看](vscode://simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.projects%22%5D)",
|
||||
"setting.frontMatter.projects.items.properties.name.markdownDescription": "指定项目名称。",
|
||||
"setting.frontMatter.projects.items.properties.default.markdownDescription": "指定此项目是否为默认加载项目。",
|
||||
"setting.frontMatter.extensibility.scripts.markdownDescription": "指定要在 Front Matter CMS 中加载的脚本列表。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.extensibility.scripts) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.extensibility.scripts%22%5D)",
|
||||
"setting.frontMatter.experimental.markdownDescription": "指定是否启用实验性功能。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.experimental) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.experimental%22%5D)",
|
||||
"setting.frontMatter.extends.markdownDescription": "指定扩展 Front Matter CMS 配置的路径/URL 列表。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.extends) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.extends%22%5D)",
|
||||
"setting.frontMatter.content.autoUpdateDate.markdownDescription": "指定是否自动更新文章/页面的修改日期(仅限内容文件夹中的内容)。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.autoupdatedate) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.autoupdatedate%22%5D)",
|
||||
"setting.frontMatter.content.defaultFileType.markdownDescription": "指定要创建内容的默认文件类型。[文档](https://frontmatter.codes/docs/settings/overview%23frontmatter.content.defaultfiletype) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.defaultfiletype%22%5D)",
|
||||
"setting.frontMatter.content.defaultSorting.markdownDescription": "指定内容仪表盘的默认排序选项。您可以使用枚举中的值或定义自己的 ID。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.defaultsorting) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.defaultsorting%22%5D)",
|
||||
"setting.frontMatter.content.draftField.markdownDescription": "定义要用于管理内容的草稿字段。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.draftfield) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.draftfield%22%5D)",
|
||||
"setting.frontMatter.content.draftField.properties.type.description": "要使用的草稿字段类型",
|
||||
"setting.frontMatter.content.draftField.properties.name.description": "要使用的字段名称",
|
||||
"setting.frontMatter.content.draftField.properties.invert.description": "默认情况下,当内容为草稿时,draft 字段值为 true。如需将draft字段设为 false,请将此值设置为 true。",
|
||||
"setting.frontMatter.content.draftField.properties.choices.description": "字段的选择列表",
|
||||
"setting.frontMatter.content.fmHighlight.markdownDescription": "指定是否在 Markdown 文件中高亮显示 Front Matter。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.fmhighlight) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.fmhighlight%22%5D)",
|
||||
"setting.frontMatter.content.hideFm.markdownDescription": "指定是否在 Markdown 文件中隐藏 Front Matter。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.hidefm) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.hidefm%22%5D)",
|
||||
"setting.frontMatter.content.hideFmMessage.markdownDescription": "指定隐藏 Front Matter 时显示的消息。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.hidefmMessage) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.hidefmMessage%22%5D)",
|
||||
"setting.frontMatter.content.pageFolders.markdownDescription": "此文件夹数组定义扩展程序可以检索或创建新页面的位置。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.pagefolders) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.pagefolders%22%5D)",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.title.description": "文件夹名称",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.path.description": "文件夹路径",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.excludeSubdir.description": "排除子目录",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.excludePaths.description": "排除路径(例如 api, _*.*)",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.previewPath.description": "为文件夹定义自定义预览路径。",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.trailingSlash.description": "指定是否在预览 URL 中添加尾部斜杠。",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.filePrefix.description": "定义文件名的前缀。",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.contentTypes.description": "定义当前位置可用的内容类型。如果未定义,则所有内容类型都可用。",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.disableCreation.description": "禁止在文件夹中创建新内容。",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description": "设置页面文件夹的默认区域设置 ID。此文件夹中的所有内容均可翻译为 `frontMatter.content.i18n` 设置中定义的语言。",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.locales.description": "定义页面文件夹的区域设置。这将用于内容翻译。",
|
||||
"setting.frontMatter.content.pageFolders.items.properties.slugTemplate.description": "定义自定义别名模板。",
|
||||
"setting.frontMatter.content.i18n.markdownDescription": "指定要用于网站的区域设置。此设置可在页面文件夹级别覆盖。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.i18n) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.i18n%22%5D)",
|
||||
"setting.frontMatter.content.i18n.items.properties.title.description": "区域设置标题",
|
||||
"setting.frontMatter.content.i18n.items.properties.locale.description": "区域设置代码",
|
||||
"setting.frontMatter.content.i18n.items.properties.path.description": "区域设置文件夹的相对路径",
|
||||
"setting.frontMatter.content.placeholders.markdownDescription": "此占位符数组定义了可在内容类型和模板中使用的占位符,用于自动填充内容的前端元数据。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.placeholders) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.placeholders%22%5D)",
|
||||
"setting.frontMatter.content.placeholders.items.properties.id.description": "占位符 ID,在内容类型或模板中按如下方式使用:{{placeholder}}",
|
||||
"setting.frontMatter.content.placeholders.items.properties.value.description": "占位符的值",
|
||||
"setting.frontMatter.content.placeholders.items.properties.script.description": "用于获取占位符值的要执行的脚本",
|
||||
"setting.frontMatter.content.publicFolder.markdownDescription": "指定所有资源所在的文件夹名称。例如在 Hugo 中这是 `static` 文件夹。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.publicfolder) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.publicfolder%22%5D)",
|
||||
"setting.frontMatter.content.publicFolder.properties.path.description": "指定资源文件夹的路径",
|
||||
"setting.frontMatter.content.publicFolder.properties.relative.description": "定义媒体文件的路径是否相对于内容文件?",
|
||||
"setting.frontMatter.snippets.wrapper.enabled.markdownDescription": "指定是否包装片段。[文档](https://frontmatter.codes/docs/settings/overview#frontMatter.snippets.wrapper.enabled) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontMatter.snippets.wrapper.enabled%22%5D)",
|
||||
"setting.frontMatter.content.snippets.markdownDescription": "定义要在内容中使用的片段。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.snippets) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.snippets%22%5D)",
|
||||
"setting.frontMatter.content.grouping.markdownDescription": "指定仪表盘内容的分组选项。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.grouping) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.grouping%22%5D)",
|
||||
"setting.frontMatter.content.grouping.items.properties.id.description": "分组选项的 ID。",
|
||||
"setting.frontMatter.content.grouping.items.properties.title.description": "将在 UI 中显示的分组标题。",
|
||||
"setting.frontMatter.content.grouping.items.properties.name.description": "要按之分组的 content-type 字段名称。",
|
||||
"setting.frontMatter.content.sorting.markdownDescription": "定义仪表盘内容的排序选项。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.sorting) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.sorting%22%5D)",
|
||||
"setting.frontMatter.content.sorting.items.properties.id.description": "排序选项的 ID。将用于存储上次使用的排序选项或默认选项。",
|
||||
"setting.frontMatter.content.sorting.items.properties.title.description": "排序标签的名称",
|
||||
"setting.frontMatter.content.sorting.items.properties.name.description": "要按之排序的元数据字段名称",
|
||||
"setting.frontMatter.content.sorting.items.properties.order.description": "排序顺序",
|
||||
"setting.frontMatter.content.sorting.items.properties.type.description": "字段值的类型",
|
||||
"setting.frontMatter.content.supportedFileTypes.markdownDescription": "指定要在 Front Matter 中使用的文件类型。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.supportedfiletypes) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.supportedfiletypes%22%5D)",
|
||||
"setting.frontMatter.content.wysiwyg.markdownDescription": "指定是否启用所见即所得 (WYSIWYG) Markdown 控件。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.wysiwyg) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.wysiwyg%22%5D)",
|
||||
"setting.frontMatter.content.filters.markdownDescription": "指定内容仪表盘要使用的筛选器。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.content.filters) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.content.filters%22%5D)",
|
||||
"setting.frontMatter.custom.scripts.markdownDescription": "指定要执行的 Node.js 脚本的路径。当前文件路径将作为参数提供。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.custom.scripts) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.custom.scripts%22%5D)",
|
||||
"setting.frontMatter.custom.scripts.items.properties.id.description": "脚本的 ID。",
|
||||
"setting.frontMatter.custom.scripts.items.properties.title.description": "要为脚本指定的标题。将显示为按钮的标题。",
|
||||
"setting.frontMatter.custom.scripts.items.properties.script.description": "要执行的脚本路径",
|
||||
"setting.frontMatter.custom.scripts.items.properties.nodeBin.description": "node 可执行文件的路径。使用 NVM 时需要此选项,以避免混淆使用哪个 node 版本。(已弃用:改用 command 属性)",
|
||||
"setting.frontMatter.custom.scripts.items.properties.bulk.description": "对所有内容文件运行脚本",
|
||||
"setting.frontMatter.custom.scripts.items.properties.output.description": "定义脚本输出的位置。默认为通知,但可以指定在编辑器面板中显示。",
|
||||
"setting.frontMatter.custom.scripts.items.properties.outputType.description": "编辑器面板的输出类型。例如,可更改为 'markdown'",
|
||||
"setting.frontMatter.custom.scripts.items.properties.type.description": "脚本将使用的类型。",
|
||||
"setting.frontMatter.custom.scripts.items.properties.command.description": "要执行的脚本类型。",
|
||||
"setting.frontMatter.custom.scripts.items.properties.hidden.description": "从 UI 中隐藏操作",
|
||||
"setting.frontMatter.custom.scripts.items.properties.environments.items.properties.type.description": "需要使用脚本的环境类型",
|
||||
"setting.frontMatter.custom.scripts.items.properties.environments.items.properties.script.description": "要执行的脚本路径",
|
||||
"setting.frontMatter.custom.scripts.items.properties.contentTypes.description": "定义脚本将使用的内容类型。如果未定义任何类型,则对所有类型可用。",
|
||||
"setting.frontMatter.dashboard.content.pagination.markdownDescription": "指定是否为内容启用分页。最多可定义页码为 52。默认每页项目数为 `16`。通过设置为 `false` 可禁用分页。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.dashboard.content.pagination) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.dashboard.content.pagination%22%5D)",
|
||||
"setting.frontMatter.dashboard.content.cardTags.markdownDescription": "指定将用于在内容卡片上显示标签的元数据字段名称。为空或 null 时,将从卡片中隐藏标签。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.dashboard.content.cardtags) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.dashboard.content.cardtags%22%5D)",
|
||||
"setting.frontMatter.dashboard.content.card.fields.state.markdownDescription": "指定是否在内容卡片视图上显示状态/草稿状态。[文档](https://frontmatter.codes/docs/settings/overview#frontMatter.dashboard.content.card.fields.state) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontMatter.dashboard.content.card.fields.state%22%5D)",
|
||||
"setting.frontMatter.dashboard.content.card.fields.date.markdownDescription": "指定是否在内容卡片视图上显示日期。[文档](https://frontmatter.codes/docs/settings/overview#frontMatter.dashboard.content.card.fields.date) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontMatter.dashboard.content.card.fields.date%22%5D)",
|
||||
"setting.frontMatter.dashboard.content.card.fields.description.markdownDescription": "指定将用于在内容卡片上显示描述的元数据字段名称。[文档](https://frontmatter.codes/docs/settings/overview#frontMatter.dashboard.content.card.fields.description) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontMatter.dashboard.content.card.fields.description%22%5D)",
|
||||
"setting.frontMatter.dashboard.content.card.fields.title.markdownDescription": "指定将用于在内容卡片上显示标题的元数据字段名称。[文档](https://frontmatter.codes/docs/settings/overview#frontMatter.dashboard.content.card.fields.title) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontMatter.dashboard.content.card.fields.title%22%5D)",
|
||||
"setting.frontMatter.dashboard.mediaSnippet.markdownDescription": "指定自定义媒体插入标记的片段。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.dashboard.mediasnippet) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.dashboard.mediasnippet%22%5D)",
|
||||
"setting.frontMatter.dashboard.mediaSnippet.items.description": "在片段中使用 `{mediaUrl}`、`{caption}`、`{alt}`、`{filename}`、`{mediaHeight}` 和 `{mediaWidth}` 占位符以自动插入媒体信息。",
|
||||
"setting.frontMatter.dashboard.openOnStart.markdownDescription": "指定启动 VS Code 时是否打开仪表盘。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.dashboard.openonstart) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.dashboard.openonstart%22%5D)",
|
||||
"setting.frontMatter.data.files.markdownDescription": "指定要用于网站的数据文件。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.data.files) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.data.files%22%5D)",
|
||||
"setting.frontMatter.data.files.items.properties.id.description": "要为数据文件使用的唯一 ID。",
|
||||
"setting.frontMatter.data.files.items.properties.title.description": "要为数据文件指定的标题。",
|
||||
"setting.frontMatter.data.files.items.properties.labelField.description": "要用作数据条目标签的字段。",
|
||||
"setting.frontMatter.data.files.items.properties.file.description": "要加载的文件路径。仅支持 JSON 或 YAML 文件。",
|
||||
"setting.frontMatter.data.files.items.properties.fileType.description": "定义如何解析文件。默认为 JSON。",
|
||||
"setting.frontMatter.data.files.items.properties.schema.description": "数据的 JSON 模式,将用于渲染数据表单。",
|
||||
"setting.frontMatter.data.files.items.properties.schema.properties.title.description": "表单标题。",
|
||||
"setting.frontMatter.data.files.items.properties.schema.properties.type.description": "定义表单的类型。默认为 'object'。",
|
||||
"setting.frontMatter.data.files.items.properties.schema.properties.required.description": "定义表单的必填字段。",
|
||||
"setting.frontMatter.data.files.items.properties.schema.properties.properties.description": "定义表单的字段。",
|
||||
"setting.frontMatter.data.files.items.properties.type.description": "如果使用数据类型,可指定类型 ID。",
|
||||
"setting.frontMatter.data.files.items.properties.singleEntry.description": "是否要对数据文件使用单个条目。",
|
||||
"setting.frontMatter.data.folders.markdownDescription": "指定要用于网站的数据文件夹。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.data.folders) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.data.folders%22%5D)",
|
||||
"setting.frontMatter.data.folders.items.properties.id.description": "要为数据文件夹使用的唯一 ID。",
|
||||
"setting.frontMatter.data.folders.items.properties.labelField.description": "要用作数据条目标签的字段。",
|
||||
"setting.frontMatter.data.folders.items.properties.path.description": "要加载文件的文件夹路径。",
|
||||
"setting.frontMatter.data.folders.items.properties.type.description": "如果使用数据类型,可指定类型 ID。",
|
||||
"setting.frontMatter.data.folders.items.properties.singleEntry.description": "是否要对文件夹中的数据文件使用单个条目。",
|
||||
"setting.frontMatter.data.folders.items.properties.enableFileCreation.description": "启用文件夹中创建新数据文件。",
|
||||
"setting.frontMatter.data.folders.items.properties.fileType.description": "启用文件创建时定义文件类型。默认为 JSON。",
|
||||
"setting.frontMatter.data.types.markdownDescription": "指定数据类型。这些类型可用于数据文件。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.data.types) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.data.types%22%5D)",
|
||||
"setting.frontMatter.data.types.items.properties.id.description": "要为数据类型使用的唯一 ID。",
|
||||
"setting.frontMatter.file.preserveCasing.markdownDescription": "指定是否保留文件名的标题大小写。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.file.preservecasing) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.file.preservecasing%22%5D)",
|
||||
"setting.frontMatter.framework.id.markdownDescription": "指定用于网站的静态站点生成器或框架的 ID。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.framework.id) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.framework.id%22%5D)",
|
||||
"setting.frontMatter.framework.startCommand.markdownDescription": "指定用于启动静态站点生成器或框架的命令。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.framework.startcommand) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.framework.startcommand%22%5D)",
|
||||
"setting.frontMatter.git.enabled.markdownDescription": "指定是否要为网站使用 Git 操作。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.git.enabled) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.enabled%22%5D)",
|
||||
"setting.frontMatter.git.commitMessage.markdownDescription": "指定同步要使用的提交消息。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.git.commitmessage) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.commitmessage%22%5D)",
|
||||
"setting.frontMatter.git.submodule.pull.markdownDescription": "指定同步时是否拉取子模块。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.git.submodule.pull) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.submodule.pull%22%5D)",
|
||||
"setting.frontMatter.git.submodule.push.markdownDescription": "指定同步时是否推送子模块。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.git.submodule.push) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.submodule.push%22%5D)",
|
||||
"setting.frontMatter.git.submodule.branch.markdownDescription": "指定要签出的子模块分支。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.git.submodule.branch) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.submodule.branch%22%5D)",
|
||||
"setting.frontMatter.git.submodule.folder.markdownDescription": "指定内容的子模块文件夹,当使用多个子模块时可能有用。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.git.submodule.folder) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.submodule.folder%22%5D)",
|
||||
"setting.frontMatter.global.activeMode.markdownDescription": "指定 Front Matter 的激活模式。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.global.activemode) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.global.activemode%22%5D)",
|
||||
"setting.frontMatter.global.modes.markdownDescription": "指定要用于 Front Matter 的模式。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.global.modes) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.global.modes%22%5D)",
|
||||
"setting.frontMatter.global.modes.items.properties.id.description": "模式的 ID。",
|
||||
"setting.frontMatter.global.modes.items.properties.features.description": "要为模式使用的功能。",
|
||||
"setting.frontMatter.global.notifications.markdownDescription": "指定要查看的通知类型。默认情况下,将显示所有通知类型。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.global.notifications) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.global.notifications%22%5D)",
|
||||
"setting.frontMatter.global.disabledNotifications.markdownDescription": "这是一个包含可为 Front Matter CMS 禁用的通知类型的数组。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.global.disablednotifications) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.global.disablednotifications%22%5D)",
|
||||
"setting.frontMatter.global.timezone.markdownDescription": "指定日期格式化的时区。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.datetimezone) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.datetimezone%22%5D)",
|
||||
"setting.frontMatter.media.defaultSorting.markdownDescription": "指定媒体仪表盘的默认排序选项。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.media.defaultsorting) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.media.defaultsorting%22%5D)",
|
||||
"setting.frontMatter.media.supportedMimeTypes.markdownDescription": "指定媒体文件支持的 MIME 类型。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.media.supportedmimetypes) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.media.supportedmimetypes%22%5D)",
|
||||
|
||||
"setting.frontMatter.media.contentTypes.markdownDescription": "指定要在 Front Matter 中使用的媒体内容类型。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.media.contenttypes) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.media.contenttypes%22%5D)",
|
||||
"setting.frontMatter.media.contentTypes.items.description": "定义要在 Front Matter 中使用的媒体内容类型。",
|
||||
"setting.frontMatter.media.contentTypes.items.properties.name.description": "媒体内容类型的名称",
|
||||
"setting.frontMatter.media.contentTypes.items.properties.fileTypes.description": "指定媒体内容类型允许的文件类型",
|
||||
"setting.frontMatter.media.contentTypes.items.properties.fields.description": "定义媒体内容类型的字段",
|
||||
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.title.description": "在 UI 中显示的标题",
|
||||
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.name.description": "要使用的字段名称",
|
||||
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.type.description": "定义字段类型",
|
||||
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.single.description": "是否为单行字段",
|
||||
|
||||
"setting.frontMatter.panel.openOnSupportedFile.markdownDescription": "指定打开支持的文件时是否打开面板。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.openonsupportedfile) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.openonsupportedfile%22%5D)",
|
||||
"setting.frontMatter.panel.freeform.markdownDescription": "指定是否允许在标签选择器中输入未知标签/分类(启用后,之后将可以选择存储它们)。默认值:true。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.freeform) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.freeform%22%5D)",
|
||||
"setting.frontMatter.panel.actions.disabled.markdownDescription": "指定要在面板中禁用的操作。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.actions.disabled) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.actions.disabled%22%5D)",
|
||||
"setting.frontMatter.preview.host.markdownDescription": "指定打开预览时要使用的主机 URL(例如:http://localhost:1313)。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.host) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.preview.host%22%5D)",
|
||||
"setting.frontMatter.preview.trailingSlash.markdownDescription": "指定是否在预览 URL 中添加尾部斜杠。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.trailingslash) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.preview.trailingslash%22%5D)",
|
||||
"setting.frontMatter.preview.pathName.markdownDescription": "指定在主机和别名之间要添加的路径。例如,可用于包含年份/月份,如:`yyyy/MM`。日期将根据文章的日期字段值生成。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.pathname) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.preview.pathname%22%5D)",
|
||||
"setting.frontMatter.site.baseURL.markdownDescription": "指定网站的基本 URL,这将用于 SEO 检查。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.site.baseurl) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.site.baseurl%22%5D)",
|
||||
"setting.frontMatter.taxonomy.alignFilename.markdownDescription": "生成新别名时使文件名与新别名对齐。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.alignfilename) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.alignfilename%22%5D)",
|
||||
"setting.frontMatter.taxonomy.categories.markdownDescription": "指定可在 Front Matter 中使用的分类。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.categories) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.categories%22%5D)",
|
||||
"setting.frontMatter.taxonomy.commaSeparatedFields.markdownDescription": "指定 Front Matter 应视为逗号分隔数组的字段名称。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.commaseparatedfields) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.commaseparatedfields%22%5D)",
|
||||
"setting.frontMatter.taxonomy.commaSeparatedFields.items.description": "要用作逗号分隔数组的字段名称。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.markdownDescription": "指定要用于文章/页面等的内容类型。确保在 front matter 中正确设置了 `type`。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.contenttypes) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.contenttypes%22%5D)",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.description": "定义要在 Front Matter 中使用的内容类型。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.name.description": "定义字段类型",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fileType.description": "指定要创建的内容类型。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.description": "定义内容类型的字段",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.description": "定义要在 Front Matter 中使用的内容类型。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.type.description": "定义字段类型",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.name.description": "要使用的字段名称",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.title.description": "在 UI 中显示的标题",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.description.description": "在 UI 中显示的描述",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.default.description": "默认值",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.description": "定义您的选项",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.id.description": "选项 ID",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.title.description": "选项标题",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description": "是否为单行字段",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description": "是否为所见即所得字段。可设置为 markdown 或 HTML。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.multiple.description": "是否允许多选?",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.isPreviewImage.description": "指定图像字段是否可用作预览。注意,每个内容类型只能有一个预览图像。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.hidden.description": "是否要从元数据部分隐藏该字段?",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description": "分类法字段的 ID。不能包含 \"tags\" 或 \"categories\" 值。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.fileExtensions.description": "指定文件选择器允许的文件扩展名",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.fieldGroup.description": "在 `frontMatter.taxonomy.fieldGroups` 设置中定义的字段组 ID",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataType.description": "在 `frontMatter.data.types` 设置中定义的数据类型 ID",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.numberOptions.description": "指定数字字段的选项",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.numberOptions.properties.isDecimal.description": "指定数字是否为小数",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.numberOptions.properties.min.description": "最小值",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.numberOptions.properties.max.description": "最大值",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.numberOptions.properties.step.description": "步进值",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyLimit.description": "限制可选择分类法的数量。设为 0 表示无限制。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.singleValueAsString.description": "指定是否将单个数组值存储为字符串而非数组。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.isPublishDate.description": "指定该字段是否为发布日期字段",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.isModifiedDate.description": "指定该字段是否为修改日期字段",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileId.description": "指定用于此字段的数据文件 ID",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileKey.description": "指定用于此字段的数据文件键",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dataFileValue.description": "指定将用于显示字段值的属性名称",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.editable.description": "指定字段是否可编辑",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.encodeEmoji.description": "指定字段是否应编码表情符号",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.dateFormat.description": "指定要使用的日期格式",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.required.description": "指定字段是否为必填项",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.contentTypeName.description": "指定用于筛选内容的内容类型名称(用于内容关联字段)",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.contentTypeValue.description": "指定要插入内容关联字段的值",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.sameContentLocale.description": "指定是否仅显示相同区域设置的内容",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.description": "指定显示字段的条件",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.fieldRef.description": "要使用的字段 ID",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.operator.description": "要使用的运算符",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.value.description": "要比较的值",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.caseSensitive.description": "指定比较是否区分大小写。默认值:true",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.actions.description": "指定字段自定义操作",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.pageBundle.description": "指定创建新内容时是否创建文件夹。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.previewPath.description": "为内容类型定义自定义预览路径。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.trailingSlash.description": "指定是否在预览 URL 中添加尾部斜杠。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.slugTemplate.description": "为内容类型定义自定义别名模板。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.template.description": "可用于创建新内容的可选模板。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.postScript.description": "可在新内容创建后使用的可选后置脚本。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.filePrefix.description": "定义文件名的前缀。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.defaultFileName.description": "创建新内容时使用的默认文件名。",
|
||||
"setting.frontMatter.taxonomy.customTaxonomy.markdownDescription": "指定自定义分类法字段数据。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.tags) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.tags%22%5D)",
|
||||
"setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description": "分类法字段的 ID。不能包含 \"tags\" 或 \"categories\" 值。",
|
||||
"setting.frontMatter.taxonomy.customTaxonomy.items.properties.options.description": "可供选择的选项。",
|
||||
"setting.frontMatter.taxonomy.dateField.markdownDescription": "此设置用于定义文章的发布日期字段。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.datefield) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.datefield%22%5D)",
|
||||
"setting.frontMatter.taxonomy.dateFormat.markdownDescription": "指定文章的日期格式。查看 [date-fns 格式](https://date-fns.org/v2.0.1/docs/format) 获取更多信息。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.dateformat) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.dateformat%22%5D)",
|
||||
"setting.frontMatter.taxonomy.fieldGroups.markdownDescription": "定义要用于区块字段或集合字段的字段组。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.fieldgroups) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.fieldgroups%22%5D)",
|
||||
"setting.frontMatter.taxonomy.fieldGroups.items.properties.id.description": "字段组的名称",
|
||||
"setting.frontMatter.taxonomy.fieldGroups.items.properties.labelField.description": "要用作显示值的字段名称",
|
||||
"setting.frontMatter.taxonomy.frontMatterType.markdownDescription": "指定要使用的 Front Matter 类型。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.frontmattertype) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.frontmattertype%22%5D)",
|
||||
"setting.frontMatter.taxonomy.indentArrays.markdownDescription": "指定前端元数据中的数组是否缩进。默认值:true。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.indentarrays) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.indentarrays%22%5D)",
|
||||
"setting.frontMatter.taxonomy.modifiedField.markdownDescription": "此设置用于定义文章的修改日期字段。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.modifiedfield) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.modifiedfield%22%5D)",
|
||||
"setting.frontMatter.taxonomy.quoteStringValues.markdownDescription": "指定是否引用前端元数据中的字符串值。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.quotestringvalues) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.quotestringvalues%22%5D)",
|
||||
"setting.frontMatter.taxonomy.noPropertyValueQuotes.markdownDescription": "指定需要移除引号的属性。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.nopropertyvaluequotes) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.nopropertyvaluequotes%22%5D)",
|
||||
"setting.frontMatter.taxonomy.noPropertyValueQuotes.items.description": "要移除引号的属性名称。",
|
||||
"setting.frontMatter.taxonomy.seoContentLengh.markdownDescription": "指定文章的最佳最小长度。1,760 到 2,400 词是 SEO(2021年) 的绝对理想文章长度(设为 `-1` 可关闭)。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seocontentlengh) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.seocontentlengh%22%5D)",
|
||||
"setting.frontMatter.taxonomy.seoDescriptionField.markdownDescription": "指定页面的 SEO 描述字段名称。默认为 'description'。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seodescriptionfield) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.seodescriptionfield%22%5D)",
|
||||
"setting.frontMatter.taxonomy.seoDescriptionLength.markdownDescription": "指定 SEO 的最佳描述长度(设为 `-1` 可关闭)。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seodescriptionlength) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.seodescriptionlength%22%5D)",
|
||||
"setting.frontMatter.taxonomy.seoSlugLength.markdownDescription": "指定 SEO 的最佳别名长度(设为 `-1` 可关闭)。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seosluglength) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.seosluglength%22%5D)",
|
||||
"setting.frontMatter.taxonomy.seoTitleField.markdownDescription": "指定页面的 SEO 标题字段名称。默认为 'title'。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seotitlefield) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.seotitlefield%22%5D)",
|
||||
"setting.frontMatter.taxonomy.seoTitleLength.markdownDescription": "指定 SEO 的最佳标题长度(设为 `-1` 可关闭)。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seotitlelength) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.seotitlelength%22%5D)",
|
||||
"setting.frontMatter.taxonomy.slugPrefix.markdownDescription": "指定别名的前缀。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugprefix) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.slugprefix%22%5D)",
|
||||
"setting.frontMatter.taxonomy.slugSuffix.markdownDescription": "指定别名的后缀。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugsuffix) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.slugsuffix%22%5D)",
|
||||
"setting.frontMatter.taxonomy.slugTemplate.markdownDescription": "定义要创建内容的自定义别名模板。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugtemplate) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.slugtemplate%22%5D)",
|
||||
"setting.frontMatter.taxonomy.tags.markdownDescription": "指定可在 Front Matter 中使用的标签。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.tags) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.taxonomy.tags%22%5D)",
|
||||
"setting.frontMatter.telemetry.disable.markdownDescription": "指定是否禁用遥测。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.telemetry.disable) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.telemetry.disable%22%5D)",
|
||||
"setting.frontMatter.templates.enabled.markdownDescription": "指定是否使用模板。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.enabled) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.enabled%22%5D)",
|
||||
"setting.frontMatter.templates.folder.markdownDescription": "指定文章模板要使用的文件夹。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.folder) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.folder%22%5D)",
|
||||
"setting.frontMatter.templates.prefix.markdownDescription": "指定要为新文章文件名添加的前缀。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.prefix) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.templates.prefix%22%5D)",
|
||||
"setting.frontMatter.dashboard.mediaSnippet.deprecationMessage": "此设置已弃用,将在下一个主要版本中移除。请在 `frontMatter.content.snippet` 设置中定义媒体片段。",
|
||||
"setting.frontMatter.taxonomy.dateField.deprecationMessage": "此设置已弃用,将在下一个主要版本中移除。请在内容类型的日期字段中使用新的 `isPublishDate` 设置。",
|
||||
"setting.frontMatter.taxonomy.modifiedField.deprecationMessage": "此设置已弃用,将在下一个主要版本中移除。请在内容类型的日期字段中使用新的 `isModifiedDate` 设置。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.customType.description": "指定要使用的自定义字段类型的名称。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.clearEmpty.description": "指定是否应清除空值。",
|
||||
"setting.frontMatter.website.host.markdownDescription": "指定网站的主机 URL。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.website.url) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.website.url%22%5D)",
|
||||
"command.frontMatter.settings.refresh": "刷新 Front Matter 设置",
|
||||
"setting.frontMatter.config.dynamicFilePath.markdownDescription": "指定动态配置文件的路径(例如:[[workspace]]/config.js)。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.config.dynamicfilepath) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.config.dynamicfilepath%22%5D)",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.allowAsSubContent.description": "指定内容类型是否可用作子内容。",
|
||||
"setting.frontMatter.taxonomy.contentTypes.items.properties.isSubContent.description": "指定内容类型是否为子内容。",
|
||||
|
||||
"setting.frontMatter.git.disableOnBranches.markdownDescription": "指定要禁用 Git 操作的分支。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.git.disableonbranches) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.disableonbranches%22%5D)",
|
||||
"setting.frontMatter.git.requiresCommitMessage.markdownDescription": "指定在发布指定分支的更改时是否需要提交消息。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.git.requirescommitmessage) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.requirescommitmessage%22%5D)",
|
||||
"setting.frontMatter.copilot.family.markdownDescription": "指定要使用的 Copilot 的 LLM 系列。[文档](https://frontmatter.codes/docs/settings/overview#frontmatter.copilot.family) - [在 VS Code 中查看](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.copilot.family%22%5D)"
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
SETTING_SLUG_TEMPLATE
|
||||
} from './../constants';
|
||||
import { CustomPlaceholder, Field } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
ArticleHelper,
|
||||
Logger,
|
||||
@@ -38,13 +37,13 @@ import { COMMAND_NAME, DefaultFields } from '../constants';
|
||||
import { DashboardData, SnippetInfo, SnippetRange } from '../models/DashboardData';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { ParsedFrontMatter } from '../parsers';
|
||||
import { FrontMatterParser, ParsedFrontMatter } from '../parsers';
|
||||
import { MediaListener } from '../listeners/panel';
|
||||
import { NavigationType } from '../dashboardWebView/models';
|
||||
import { SNIPPET } from '../constants/Snippet';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { getTitleField } from '../utils';
|
||||
import { formatInTimezone, getTitleField } from '../utils';
|
||||
|
||||
export class Article {
|
||||
/**
|
||||
@@ -150,30 +149,70 @@ export class Article {
|
||||
return;
|
||||
}
|
||||
|
||||
const documentText = document.getText();
|
||||
const isToml = FrontMatterParser.getLanguageFromContent(documentText) === 'toml';
|
||||
const cloneArticle = Object.assign({}, article);
|
||||
const dateField = await ArticleHelper.getModifiedDateField(article);
|
||||
|
||||
let contentType;
|
||||
const dateField = isToml
|
||||
? ((contentType = await ArticleHelper.getContentType(article)),
|
||||
contentType.fields.find((f) => f.isModifiedDate))
|
||||
: await ArticleHelper.getModifiedDateField(article);
|
||||
|
||||
if (isToml) {
|
||||
Logger.verbose(
|
||||
`Article:setLastModifiedDateInner:TOML - updating all datetime fields to preserve format`
|
||||
);
|
||||
}
|
||||
|
||||
Logger.verbose(`Article:setLastModifiedDateInner:DateField - ${JSON.stringify(dateField)}`);
|
||||
|
||||
try {
|
||||
const fieldName = dateField?.name || DefaultFields.LastModified;
|
||||
const fieldValue = Article.formatDate(new Date(), dateField?.dateFormat);
|
||||
cloneArticle.data[fieldName] = fieldValue;
|
||||
|
||||
Logger.verbose(
|
||||
`Article:setLastModifiedDateInner:DateField name - ${fieldName} - value - ${fieldValue}`
|
||||
);
|
||||
|
||||
if (isToml && contentType) {
|
||||
// TOML parser returns datetime literals as Date objects.
|
||||
// Reformat them using each field dateFormat to preserve expected output on save.
|
||||
for (const field of contentType.fields) {
|
||||
if (field.type === 'datetime' && field.name !== fieldName) {
|
||||
const value = cloneArticle.data[field.name];
|
||||
if (value instanceof Date) {
|
||||
cloneArticle.data[field.name] = Article.formatDate(value, field.dateFormat);
|
||||
Logger.verbose(
|
||||
`Article:setLastModifiedDateInner:Reformat field - ${field.name} - value - ${
|
||||
cloneArticle.data[field.name]
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.verbose(`Article:setLastModifiedDateInner:End`);
|
||||
return cloneArticle;
|
||||
} catch (e: unknown) {
|
||||
Notifications.error(
|
||||
l10n.t(LocalizationKey.commandsArticleSetDateError, `${CONFIG_KEY}${SETTING_DATE_FORMAT}`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the new slug
|
||||
*/
|
||||
public static generateSlug(title: string, article?: ParsedFrontMatter, slugTemplate?: string) {
|
||||
public static generateSlug(
|
||||
title: string,
|
||||
article?: ParsedFrontMatter,
|
||||
filePath?: string,
|
||||
slugTemplate?: string
|
||||
) {
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
@@ -182,7 +221,7 @@ export class Article {
|
||||
const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string;
|
||||
|
||||
if (article?.data) {
|
||||
const slug = SlugHelper.createSlug(title, article?.data, slugTemplate);
|
||||
const slug = SlugHelper.createSlug(title, article?.data, filePath, slugTemplate);
|
||||
|
||||
if (typeof slug === 'string') {
|
||||
return {
|
||||
@@ -225,7 +264,12 @@ export class Article {
|
||||
articleDate
|
||||
);
|
||||
|
||||
const slugInfo = Article.generateSlug(articleTitle, article, contentType.slugTemplate);
|
||||
const slugInfo = Article.generateSlug(
|
||||
articleTitle,
|
||||
article,
|
||||
editor.document.uri.fsPath,
|
||||
contentType.slugTemplate
|
||||
);
|
||||
|
||||
if (
|
||||
slugInfo &&
|
||||
@@ -256,7 +300,8 @@ export class Article {
|
||||
article.data[pField.name] = processArticlePlaceholdersFromData(
|
||||
article.data[pField.name],
|
||||
article.data,
|
||||
contentType
|
||||
contentType,
|
||||
editor.document.uri.fsPath
|
||||
);
|
||||
article.data[pField.name] = processTimePlaceholders(
|
||||
article.data[pField.name],
|
||||
@@ -336,7 +381,7 @@ export class Article {
|
||||
} else {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (article?.data) {
|
||||
return SlugHelper.createSlug(article.data[titleField], article.data, slugTemplate);
|
||||
return SlugHelper.createSlug(article.data[titleField], article.data, file, slugTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,10 +452,10 @@ export class Article {
|
||||
|
||||
if (fieldDateFormat) {
|
||||
Logger.verbose(`Article:formatDate:FieldDateFormat - ${fieldDateFormat}`);
|
||||
return format(dateValue, DateHelper.formatUpdate(fieldDateFormat) as string);
|
||||
return formatInTimezone(dateValue, DateHelper.formatUpdate(fieldDateFormat) as string);
|
||||
} else if (dateFormat && typeof dateFormat === 'string') {
|
||||
Logger.verbose(`Article:formatDate:DateFormat - ${dateFormat}`);
|
||||
return format(dateValue, DateHelper.formatUpdate(dateFormat) as string);
|
||||
return formatInTimezone(dateValue, DateHelper.formatUpdate(dateFormat) as string);
|
||||
} else {
|
||||
Logger.verbose(`Article:formatDate:toISOString - ${dateValue}`);
|
||||
return typeof dateValue.toISOString === 'function'
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { PreviewCommands, GeneralCommands } from './../constants';
|
||||
import { join } from 'path';
|
||||
import { commands, Uri, ViewColumn, window } from 'vscode';
|
||||
import { Extension } from '../helpers';
|
||||
import { WebviewHelper } from '@estruyf/vscode';
|
||||
import { getLocalizationFile } from '../utils/getLocalizationFile';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { getWebviewJsFiles } from '../utils';
|
||||
|
||||
export class Chatbot {
|
||||
/**
|
||||
* Open the Chatbot in the editor
|
||||
*/
|
||||
public static async open(extensionPath: string) {
|
||||
// Create the preview webview
|
||||
const webView = window.createWebviewPanel(
|
||||
'frontMatterChatbot',
|
||||
`Front Matter AI - ${l10n.t(LocalizationKey.commandsChatbotTitle)}`,
|
||||
{
|
||||
viewColumn: ViewColumn.Beside,
|
||||
preserveFocus: true
|
||||
},
|
||||
{
|
||||
enableScripts: true
|
||||
}
|
||||
);
|
||||
|
||||
webView.iconPath = {
|
||||
dark: Uri.file(join(extensionPath, 'assets/icons/frontmatter-short-dark.svg')),
|
||||
light: Uri.file(join(extensionPath, 'assets/icons/frontmatter-short-light.svg'))
|
||||
};
|
||||
|
||||
const cspSource = webView.webview.cspSource;
|
||||
|
||||
const fetchLocalization = async (requestId: string) => {
|
||||
if (!requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileContents = await getLocalizationFile();
|
||||
|
||||
webView.webview.postMessage({
|
||||
command: GeneralCommands.toVSCode.getLocalization,
|
||||
requestId,
|
||||
payload: fileContents
|
||||
});
|
||||
};
|
||||
|
||||
webView.webview.onDidReceiveMessage(async (message) => {
|
||||
const { command, requestId, payload, data } = message;
|
||||
|
||||
switch (command) {
|
||||
case PreviewCommands.toVSCode.open:
|
||||
if (payload || data) {
|
||||
commands.executeCommand('vscode.open', payload || data);
|
||||
}
|
||||
break;
|
||||
case GeneralCommands.toVSCode.getLocalization:
|
||||
fetchLocalization(requestId);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const webviewFile = 'dashboard.main.js';
|
||||
const localPort = `9000`;
|
||||
const localServerUrl = `localhost:${localPort}`;
|
||||
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
|
||||
const ext = Extension.getInstance();
|
||||
const isProd = ext.isProductionMode;
|
||||
const version = ext.getVersion();
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
const csp = [
|
||||
`default-src 'none';`,
|
||||
`img-src ${cspSource} http: https:;`,
|
||||
`script-src ${
|
||||
isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`
|
||||
} 'unsafe-eval'`,
|
||||
`style-src ${cspSource} 'self' 'unsafe-inline' http: https:`,
|
||||
`connect-src https://* ${
|
||||
isProd
|
||||
? ``
|
||||
: `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`
|
||||
}`
|
||||
];
|
||||
|
||||
let scriptUris = [];
|
||||
if (isProd) {
|
||||
scriptUris = await getWebviewJsFiles('dashboard', webView.webview);
|
||||
} else {
|
||||
scriptUris.push(`http://${localServerUrl}/${webviewFile}`);
|
||||
}
|
||||
|
||||
// By default, the chatbot is seen as experimental
|
||||
const experimental = true;
|
||||
|
||||
webView.webview.html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join('; ')}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Front Matter Docs Chatbot</title>
|
||||
</head>
|
||||
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden">
|
||||
<div id="app" data-type="chatbot" data-isProd="${isProd}" data-environment="${
|
||||
isBeta ? 'BETA' : 'main'
|
||||
}" data-version="${version.usedVersion}" ${
|
||||
experimental ? `data-experimental="${experimental}"` : ''
|
||||
} style="width:100%;height:100%;margin:0;padding:0;"></div>
|
||||
|
||||
${scriptUris
|
||||
.map((uri) => `<script ${isProd ? `nonce="${nonce}"` : ''} src="${uri}"></script>`)
|
||||
.join('\n')}
|
||||
|
||||
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`chatbot-${version.installedVersion}`}" alt="Daily usage" />
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import { Template } from './Template';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { Extension, Logger, Settings, processTimePlaceholders } from '../helpers';
|
||||
import { existsSync } from 'fs';
|
||||
import { format } from 'date-fns';
|
||||
import { Dashboard } from './Dashboard';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { MediaHelpers } from '../helpers/MediaHelpers';
|
||||
@@ -31,7 +30,7 @@ import { MediaListener, PagesListener, SettingsListener } from '../listeners/das
|
||||
import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
|
||||
import { glob } from 'glob';
|
||||
import { mkdirAsync } from '../utils/mkdirAsync';
|
||||
import { existsAsync, isWindows, lstatAsync } from '../utils';
|
||||
import { existsAsync, formatInTimezone, isWindows, lstatAsync } from '../utils';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { Preview } from './Preview';
|
||||
@@ -85,7 +84,7 @@ export class Folders {
|
||||
prompt: l10n.t(LocalizationKey.commandsFoldersAddMediaFolderInputBoxPrompt),
|
||||
value: startPath,
|
||||
ignoreFocusOut: true,
|
||||
placeHolder: `${format(new Date(), `yyyy/MM`)}`
|
||||
placeHolder: `${formatInTimezone(new Date(), `yyyy/MM`)}`
|
||||
});
|
||||
|
||||
if (!folderName) {
|
||||
@@ -220,7 +219,9 @@ export class Folders {
|
||||
: Folders.getAbsFilePath(assetFolder);
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
const relativePath = relative(parseWinPath(wsFolder.fsPath), parseWinPath(assetFolder));
|
||||
const relativePath = parseWinPath(
|
||||
relative(parseWinPath(wsFolder.fsPath), parseWinPath(assetFolder))
|
||||
);
|
||||
return relativePath === '' ? '/' : relativePath;
|
||||
}
|
||||
}
|
||||
@@ -637,15 +638,23 @@ export class Folders {
|
||||
}
|
||||
}
|
||||
|
||||
// For Windows, we need to make sure the drive letter is lowercased for consistency
|
||||
if (isWindows()) {
|
||||
folders = folders.map((folder) => parseWinPath(folder));
|
||||
}
|
||||
|
||||
// Filter out the workspace folder
|
||||
if (wsFolder) {
|
||||
folders = folders.filter((folder) => folder !== wsFolder.fsPath);
|
||||
folders = folders.filter((folder) => folder !== parseWinPath(wsFolder.fsPath));
|
||||
}
|
||||
|
||||
const uniqueFolders = [...new Set(folders)];
|
||||
const relativeFolderPaths = uniqueFolders.map((folder) =>
|
||||
parseWinPath(relative(parseWinPath(wsFolder.fsPath), folder))
|
||||
);
|
||||
|
||||
Logger.verbose('Folders:getContentFolders:end');
|
||||
return uniqueFolders.map((folder) => relative(wsFolder?.path || '', folder));
|
||||
return relativeFolderPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -861,7 +870,7 @@ export class Folders {
|
||||
try {
|
||||
pattern = isWindows() ? parseWinPath(pattern) : pattern;
|
||||
const folders = await glob(pattern, {
|
||||
ignore: 'node_modules/**',
|
||||
ignore: '**/node_modules/**',
|
||||
dot: true
|
||||
});
|
||||
|
||||
@@ -904,7 +913,7 @@ export class Folders {
|
||||
pattern = isWindows() ? parseWinPath(pattern) : pattern;
|
||||
const files = await glob(pattern, {
|
||||
ignore: [
|
||||
'node_modules/**',
|
||||
'**/node_modules/**',
|
||||
...excludePaths.map((path) => {
|
||||
// path can be a folder name or a wildcard.
|
||||
// If its a folder name, we need to add a wildcard to the end
|
||||
|
||||
@@ -54,6 +54,7 @@ export class Preview {
|
||||
return;
|
||||
}
|
||||
|
||||
const integratedBrowserCommand = await this.getIntegratedBrowserCommand();
|
||||
const browserLiteCommand = await this.getBrowserLiteCommand();
|
||||
|
||||
const editor = window.activeTextEditor;
|
||||
@@ -69,6 +70,12 @@ export class Preview {
|
||||
const slug = await this.getContentSlug(article, editor?.document.uri.fsPath);
|
||||
const localhostUrl = await this.getLocalServerUrl();
|
||||
|
||||
if (integratedBrowserCommand) {
|
||||
const pageUrl = joinUrl(localhostUrl.toString(), slug || '');
|
||||
commands.executeCommand(integratedBrowserCommand, pageUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (browserLiteCommand) {
|
||||
const pageUrl = joinUrl(localhostUrl.toString(), slug || '');
|
||||
commands.executeCommand(browserLiteCommand, pageUrl);
|
||||
@@ -368,6 +375,17 @@ export class Preview {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Browser Lite is installed
|
||||
*/
|
||||
private static async getIntegratedBrowserCommand() {
|
||||
const allCommands = await commands.getCommands(true);
|
||||
if (allCommands.includes(`workbench.action.browser.open`)) {
|
||||
return `workbench.action.browser.open`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the localhost url
|
||||
* @returns
|
||||
|
||||
@@ -4,22 +4,39 @@ import {
|
||||
EXTENSION_NAME,
|
||||
NOTIFICATION_TYPE,
|
||||
SETTING_SEO_DESCRIPTION_LENGTH,
|
||||
SETTING_SEO_TITLE_LENGTH
|
||||
SETTING_SEO_TITLE_LENGTH,
|
||||
SETTING_VALIDATION_ENABLED
|
||||
} from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { ArticleHelper, Notifications, SeoHelper, Settings } from '../helpers';
|
||||
import {
|
||||
ArticleHelper,
|
||||
Notifications,
|
||||
SeoHelper,
|
||||
Settings,
|
||||
FrontMatterValidator,
|
||||
ValidationError
|
||||
} from '../helpers';
|
||||
import { PanelProvider } from '../panelWebView/PanelProvider';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { DataListener } from '../listeners/panel';
|
||||
import { commands } from 'vscode';
|
||||
import { Field } from '../models';
|
||||
import { FrontMatterParser } from '../parsers';
|
||||
import { Preview } from './Preview';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { i18n } from './i18n';
|
||||
import { getDescriptionField, getTitleField } from '../utils';
|
||||
import * as yaml from 'yaml';
|
||||
|
||||
export class StatusListener {
|
||||
private static _validator: FrontMatterValidator | undefined;
|
||||
private static get validator(): FrontMatterValidator {
|
||||
if (!StatusListener._validator) {
|
||||
StatusListener._validator = new FrontMatterValidator();
|
||||
}
|
||||
return StatusListener._validator;
|
||||
}
|
||||
/**
|
||||
* Update the text of the status bar
|
||||
*
|
||||
@@ -70,6 +87,12 @@ export class StatusListener {
|
||||
// Check the required fields
|
||||
if (editor) {
|
||||
StatusListener.verifyRequiredFields(editor, article, collection);
|
||||
|
||||
// Schema validation
|
||||
const validationEnabled = Settings.get<boolean>(SETTING_VALIDATION_ENABLED, true);
|
||||
if (validationEnabled) {
|
||||
await StatusListener.verifySchemaValidation(editor, article, collection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +196,245 @@ export class StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify schema validation
|
||||
* @param editor Text editor
|
||||
* @param article Parsed front matter
|
||||
* @param collection Diagnostic collection
|
||||
*/
|
||||
private static async verifySchemaValidation(
|
||||
editor: vscode.TextEditor,
|
||||
article: ParsedFrontMatter,
|
||||
collection: vscode.DiagnosticCollection
|
||||
) {
|
||||
try {
|
||||
const contentType = await ArticleHelper.getContentType(article);
|
||||
if (!contentType || !contentType.fields || contentType.fields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate against schema
|
||||
const errors = await StatusListener.validator.validate(article.data, contentType);
|
||||
|
||||
if (errors.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = editor.document.getText();
|
||||
const schemaDiagnostics: vscode.Diagnostic[] = [];
|
||||
|
||||
for (const error of errors) {
|
||||
const range = StatusListener.findSchemaErrorRange(editor.document, text, error);
|
||||
|
||||
if (range) {
|
||||
const diagnostic: vscode.Diagnostic = {
|
||||
code: '',
|
||||
message: error.message,
|
||||
range,
|
||||
severity: vscode.DiagnosticSeverity.Warning,
|
||||
source: EXTENSION_NAME
|
||||
};
|
||||
|
||||
schemaDiagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if (schemaDiagnostics.length > 0) {
|
||||
if (collection.has(editor.document.uri)) {
|
||||
const otherDiag = collection.get(editor.document.uri) || [];
|
||||
collection.set(editor.document.uri, [...otherDiag, ...schemaDiagnostics]);
|
||||
} else {
|
||||
collection.set(editor.document.uri, [...schemaDiagnostics]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail validation errors to not disrupt the user experience
|
||||
// Logger can be used here if needed for debugging
|
||||
}
|
||||
}
|
||||
|
||||
private static findSchemaErrorRange(
|
||||
document: vscode.TextDocument,
|
||||
text: string,
|
||||
error: ValidationError
|
||||
): vscode.Range | undefined {
|
||||
const language = FrontMatterParser.getLanguageFromContent(text);
|
||||
|
||||
if (language === 'yaml') {
|
||||
const yamlRange = StatusListener.findYamlSchemaErrorRange(document, text, error);
|
||||
if (yamlRange) {
|
||||
return yamlRange;
|
||||
}
|
||||
}
|
||||
|
||||
return StatusListener.findTextSchemaErrorRange(document, text, error);
|
||||
}
|
||||
|
||||
private static findYamlSchemaErrorRange(
|
||||
document: vscode.TextDocument,
|
||||
text: string,
|
||||
error: ValidationError
|
||||
): vscode.Range | undefined {
|
||||
const frontMatter = StatusListener.getYamlFrontMatter(text);
|
||||
if (!frontMatter) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const path = StatusListener.getValidationPath(error);
|
||||
if (path.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const doc = yaml.parseDocument(frontMatter.content);
|
||||
const node = doc.getIn(path, true) as { range?: [number, number, number] } | null;
|
||||
|
||||
if (!node?.range || node.range.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedRange = StatusListener.normalizeYamlNodeRange(frontMatter.content, node.range);
|
||||
if (!normalizedRange) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new vscode.Range(
|
||||
document.positionAt(frontMatter.startOffset + normalizedRange.start),
|
||||
document.positionAt(frontMatter.startOffset + normalizedRange.end)
|
||||
);
|
||||
}
|
||||
|
||||
private static findTextSchemaErrorRange(
|
||||
document: vscode.TextDocument,
|
||||
text: string,
|
||||
error: ValidationError
|
||||
): vscode.Range | undefined {
|
||||
const path = StatusListener.getValidationPath(error);
|
||||
const fieldName = path.length > 0 ? String(path[path.length - 1]) : '';
|
||||
const arrayIndex =
|
||||
typeof path[path.length - 1] === 'number' ? (path[path.length - 1] as number) : undefined;
|
||||
const searchFieldName =
|
||||
arrayIndex !== undefined ? String(path[path.length - 2] || '') : fieldName;
|
||||
|
||||
if (!searchFieldName || searchFieldName === 'root') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const frontMatterMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
const frontMatterEnd = frontMatterMatch ? frontMatterMatch[0].length : text.length;
|
||||
const searchText = text.substring(0, frontMatterEnd);
|
||||
const fieldIdx = searchText.indexOf(`${searchFieldName}:`);
|
||||
|
||||
if (fieldIdx === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let posStart = document.positionAt(fieldIdx);
|
||||
let posEnd = document.positionAt(fieldIdx + searchFieldName.length);
|
||||
|
||||
if (arrayIndex !== undefined) {
|
||||
const afterField = text.indexOf('\n', fieldIdx) + 1;
|
||||
let remaining = arrayIndex;
|
||||
let searchFrom = afterField;
|
||||
while (searchFrom < frontMatterEnd) {
|
||||
const lineEnd = text.indexOf('\n', searchFrom);
|
||||
const line = text.substring(searchFrom, lineEnd === -1 ? frontMatterEnd : lineEnd);
|
||||
if (/^\s*-\s/.test(line)) {
|
||||
if (remaining === 0) {
|
||||
const valueOffset = line.indexOf('- ') + 2;
|
||||
const rawItemValue = line.substring(valueOffset).trim();
|
||||
const isQuoted =
|
||||
rawItemValue.length > 1 &&
|
||||
((rawItemValue.startsWith('"') && rawItemValue.endsWith('"')) ||
|
||||
(rawItemValue.startsWith("'") && rawItemValue.endsWith("'")));
|
||||
const itemValue = isQuoted ? rawItemValue.slice(1, -1) : rawItemValue;
|
||||
const valueStartOffset = searchFrom + valueOffset + (isQuoted ? 1 : 0);
|
||||
posStart = document.positionAt(valueStartOffset);
|
||||
posEnd = document.positionAt(valueStartOffset + itemValue.length);
|
||||
break;
|
||||
}
|
||||
remaining--;
|
||||
} else if (line.trim() && !/^\s/.test(line)) {
|
||||
break;
|
||||
}
|
||||
searchFrom = (lineEnd === -1 ? frontMatterEnd : lineEnd) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return new vscode.Range(posStart, posEnd);
|
||||
}
|
||||
|
||||
private static getValidationPath(error: ValidationError): Array<string | number> {
|
||||
const path =
|
||||
error.field && error.field !== 'root'
|
||||
? error.field
|
||||
.split('.')
|
||||
.filter(Boolean)
|
||||
.map((segment) => (/^\d+$/.test(segment) ? parseInt(segment, 10) : segment))
|
||||
: [];
|
||||
|
||||
if (error.keyword === 'required' && typeof error.params?.missingProperty === 'string') {
|
||||
return [...path, error.params.missingProperty];
|
||||
}
|
||||
|
||||
if (
|
||||
error.keyword === 'additionalProperties' &&
|
||||
typeof error.params?.additionalProperty === 'string'
|
||||
) {
|
||||
return [...path, error.params.additionalProperty];
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static getYamlFrontMatter(
|
||||
text: string
|
||||
): { content: string; startOffset: number } | undefined {
|
||||
const openMatch = text.match(/^---\r?\n/);
|
||||
if (!openMatch) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startOffset = openMatch[0].length;
|
||||
const closeMatch = /\r?\n---/.exec(text.slice(startOffset));
|
||||
const endOffset = closeMatch ? startOffset + closeMatch.index : text.length;
|
||||
|
||||
return {
|
||||
content: text.slice(startOffset, endOffset),
|
||||
startOffset
|
||||
};
|
||||
}
|
||||
|
||||
private static normalizeYamlNodeRange(
|
||||
source: string,
|
||||
range: [number, number, number]
|
||||
): { start: number; end: number } | undefined {
|
||||
let start = range[0];
|
||||
let end = range[1];
|
||||
|
||||
if (start >= end) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let value = source.slice(start, end);
|
||||
const leadingWhitespace = value.match(/^\s*/)?.[0].length || 0;
|
||||
const trailingWhitespace = value.match(/\s*$/)?.[0].length || 0;
|
||||
|
||||
start += leadingWhitespace;
|
||||
end -= trailingWhitespace;
|
||||
value = source.slice(start, end);
|
||||
|
||||
if (
|
||||
value.length > 1 &&
|
||||
((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'")))
|
||||
) {
|
||||
start += 1;
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
return start < end ? { start, end } : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the line of the field
|
||||
* @param text
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * from './Article';
|
||||
export * from './Backers';
|
||||
export * from './Cache';
|
||||
export * from './Chatbot';
|
||||
export * from './Content';
|
||||
export * from './Dashboard';
|
||||
export * from './Diagnostics';
|
||||
|
||||
26
src/components/common/Tooltip.tsx
Normal file
26
src/components/common/Tooltip.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { Tooltip as TT } from 'react-tooltip'
|
||||
|
||||
export interface ITooltipProps {
|
||||
id: string;
|
||||
render?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
export const Tooltip: React.FunctionComponent<ITooltipProps> = ({
|
||||
id,
|
||||
render
|
||||
}: React.PropsWithChildren<ITooltipProps>) => {
|
||||
|
||||
const tooltipClasses = `!py-[2px] !px-[8px] !rounded-[3px] !border-[var(--vscode-editorHoverWidget-border)] !border !border-solid !bg-[var(--vscode-editorHoverWidget-background)] !text-[var(--vscode-editorHoverWidget-foreground)] !font-normal !opacity-100 shadow-[0_2px_8px_var(--vscode-widget-shadow)] text-left`;
|
||||
|
||||
return (
|
||||
<TT
|
||||
id={id}
|
||||
className={tooltipClasses}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
lineHeight: '19px'
|
||||
}}
|
||||
render={render} />
|
||||
);
|
||||
};
|
||||
@@ -28,7 +28,6 @@ export const COMMAND_NAME = {
|
||||
collapseSections: getCommandName('collapseSections'),
|
||||
preview: getCommandName('preview'),
|
||||
docs: getCommandName('docs'),
|
||||
chatbot: getCommandName('chatbot'),
|
||||
dashboard: getCommandName('dashboard'),
|
||||
dashboardMedia: getCommandName('dashboard.media'),
|
||||
dashboardSnippets: getCommandName('dashboard.snippets'),
|
||||
|
||||
@@ -4,10 +4,10 @@ export const FEATURE_FLAG = {
|
||||
seo: 'panel.seo',
|
||||
actions: 'panel.actions',
|
||||
metadata: 'panel.metadata',
|
||||
recentlyModified: 'panel.recentlyModified',
|
||||
otherActions: 'panel.otherActions',
|
||||
contentType: 'panel.contentType',
|
||||
gitActions: 'panel.gitActions'
|
||||
gitActions: 'panel.gitActions',
|
||||
recentlyModified: 'panel.recentlyModified',
|
||||
otherActions: 'panel.otherActions'
|
||||
},
|
||||
dashboard: {
|
||||
snippets: {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const SETTING_GLOBAL_NOTIFICATIONS = 'global.notifications';
|
||||
export const SETTING_GLOBAL_NOTIFICATIONS_DISABLED = 'global.disabledNotifications';
|
||||
export const SETTING_GLOBAL_MODES = 'global.modes';
|
||||
export const SETTING_GLOBAL_ACTIVE_MODE = 'global.activeMode';
|
||||
export const SETTING_GLOBAL_TIMEZONE = 'global.timezone';
|
||||
|
||||
export const SETTING_TAXONOMY_TAGS = 'taxonomy.tags';
|
||||
export const SETTING_TAXONOMY_CATEGORIES = 'taxonomy.categories';
|
||||
@@ -45,6 +46,7 @@ export const SETTING_TEMPLATES_FOLDER = 'templates.folder';
|
||||
export const SETTING_TEMPLATES_PREFIX = 'templates.prefix';
|
||||
export const SETTING_TEMPLATES_ENABLED = 'templates.enabled';
|
||||
|
||||
export const SETTING_PANEL_OPEN_ON_SUPPORTED_FILE = 'panel.openOnSupportedFile';
|
||||
export const SETTING_PANEL_FREEFORM = 'panel.freeform';
|
||||
export const SETTING_PANEL_ACTIONS_DISABLED = 'panel.actions.disabled';
|
||||
|
||||
@@ -61,6 +63,7 @@ export const SETTING_CONTENT_STATIC_FOLDER = 'content.publicFolder';
|
||||
export const SETTING_CONTENT_FRONTMATTER_HIGHLIGHT = 'content.fmHighlight';
|
||||
export const SETTING_CONTENT_DRAFT_FIELD = 'content.draftField';
|
||||
export const SETTING_CONTENT_SORTING = 'content.sorting';
|
||||
export const SETTING_CONTENT_GROUPING = 'content.grouping';
|
||||
export const SETTING_CONTENT_FILTERS = 'content.filters';
|
||||
export const SETTING_CONTENT_WYSIWYG = 'content.wysiwyg';
|
||||
export const SETTING_CONTENT_PLACEHOLDERS = 'content.placeholders';
|
||||
@@ -117,10 +120,7 @@ export const SETTING_COPILOT_FAMILY = 'copilot.family';
|
||||
|
||||
export const SETTING_LOGGING = 'logging';
|
||||
|
||||
/**
|
||||
* Sponsors only settings
|
||||
*/
|
||||
export const SETTING_SPONSORS_AI_ENABLED = 'sponsors.ai.enabled';
|
||||
export const SETTING_VALIDATION_ENABLED = 'validation.enabled';
|
||||
|
||||
/**
|
||||
* Project override support
|
||||
|
||||
@@ -23,6 +23,7 @@ export enum DashboardMessage {
|
||||
createContent = 'createContent',
|
||||
createByContentType = 'createByContentType',
|
||||
createByTemplate = 'createByTemplate',
|
||||
createContentInFolder = 'createContentInFolder',
|
||||
refreshPages = 'refreshPages',
|
||||
searchPages = 'searchPages',
|
||||
openFile = 'openFile',
|
||||
@@ -31,6 +32,7 @@ export enum DashboardMessage {
|
||||
pinItem = 'pinItem',
|
||||
unpinItem = 'unpinItem',
|
||||
rename = 'rename',
|
||||
moveFile = 'moveFile',
|
||||
|
||||
// Media Dashboard
|
||||
getMedia = 'getMedia',
|
||||
@@ -42,6 +44,8 @@ export enum DashboardMessage {
|
||||
insertMedia = 'insertMedia',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
updateMediaFolder = 'updateMediaFolder',
|
||||
deleteMediaFolder = 'deleteMediaFolder',
|
||||
insertFile = 'insertFile',
|
||||
createHexoAssetFolder = 'createHexoAssetFolder',
|
||||
getUnmappedMedia = 'getUnmappedMedia',
|
||||
|
||||
68
src/dashboardWebView/components/Common/NumberField.tsx
Normal file
68
src/dashboardWebView/components/Common/NumberField.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { XCircleIcon } from '@heroicons/react/24/solid';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface INumberFieldProps {
|
||||
name: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
icon?: JSX.Element;
|
||||
disabled?: boolean;
|
||||
autoFocus?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
export const NumberField: React.FunctionComponent<INumberFieldProps> = ({
|
||||
name,
|
||||
value,
|
||||
placeholder,
|
||||
description,
|
||||
icon,
|
||||
autoFocus,
|
||||
disabled,
|
||||
onChange,
|
||||
onReset
|
||||
}: React.PropsWithChildren<INumberFieldProps>) => {
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex justify-center">
|
||||
{
|
||||
icon && (
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<input
|
||||
type="number"
|
||||
name={name}
|
||||
className={`block w-full py-2 ${icon ? "pl-10" : "pl-2"} pr-2 sm:text-sm appearance-none disabled:opacity-50 rounded bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] border-[var(--frontmatter-border)] focus:border-[var(--vscode-focusBorder)] focus:outline-0`}
|
||||
style={{
|
||||
boxShadow: "none"
|
||||
}}
|
||||
placeholder={placeholder || ""}
|
||||
value={value}
|
||||
autoFocus={!!autoFocus}
|
||||
onChange={(e) => onChange && onChange(e.target.value)}
|
||||
disabled={!!disabled}
|
||||
/>
|
||||
|
||||
{(value && onReset) && (
|
||||
<button onClick={onReset} className="absolute inset-y-0 right-0 pr-3 flex items-center text-[var(--vscode-input-foreground)] hover:text-[var(--vscode-textLink-activeForeground)]">
|
||||
<XCircleIcon className={`h-5 w-5`} aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{
|
||||
description && (
|
||||
<p className="text-xs text-[var(--vscode--settings-headerForeground)] opacity-75 mt-2 mx-2">
|
||||
{description}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,12 @@
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { EyeIcon, GlobeEuropeAfricaIcon, TrashIcon, LanguageIcon, EllipsisHorizontalIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
EyeIcon,
|
||||
GlobeEuropeAfricaIcon,
|
||||
TrashIcon,
|
||||
LanguageIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
ArrowRightCircleIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import * as React from 'react';
|
||||
import { CustomScript, I18nConfig } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
@@ -58,6 +65,11 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
setSelectedItemAction({ path, action: 'delete' });
|
||||
}, [path]);
|
||||
|
||||
const onMove = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItemAction({ path, action: 'move' });
|
||||
}, [path]);
|
||||
|
||||
const onRename = React.useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
messageHandler.send(DashboardMessage.rename, path);
|
||||
@@ -122,6 +134,11 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsMenuItemView)}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={onMove}>
|
||||
<ArrowRightCircleIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>Move to folder</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={onRename}>
|
||||
<RenameIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>{l10n.t(LocalizationKey.commonRename)}</span>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { GeneralCommands } from '../../../constants';
|
||||
import { PageLayout } from '../Layout/PageLayout';
|
||||
import { FilesProvider } from '../../providers/FilesProvider';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { MoveFileDialog } from '../Modals/MoveFileDialog';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { deletePage } from '../../utils';
|
||||
|
||||
@@ -28,12 +29,14 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const { pageItems } = usePages(pages);
|
||||
const [showDeletionAlert, setShowDeletionAlert] = React.useState(false);
|
||||
const [showMoveDialog, setShowMoveDialog] = React.useState(false);
|
||||
const [page, setPage] = useState<Page | undefined>(undefined);
|
||||
const [selectedItemAction, setSelectedItemAction] = useRecoilState(SelectedItemActionAtom);
|
||||
|
||||
const pageFolders = [...new Set(pageItems.map((page) => page.fmFolder))];
|
||||
|
||||
const onDismiss = useCallback(() => {
|
||||
setShowMoveDialog(false);
|
||||
setShowDeletionAlert(false);
|
||||
setSelectedItemAction(undefined);
|
||||
}, []);
|
||||
@@ -46,13 +49,29 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
|
||||
setSelectedItemAction(undefined);
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItemAction && selectedItemAction.path && selectedItemAction.action === 'delete') {
|
||||
const page = pageItems.find((p) => p.fmFilePath === selectedItemAction.path);
|
||||
const onMoveConfirm = useCallback((destinationFolder: string) => {
|
||||
if (page) {
|
||||
Messenger.send(DashboardMessage.moveFile, {
|
||||
filePath: page.fmFilePath,
|
||||
destinationFolder
|
||||
});
|
||||
}
|
||||
setShowMoveDialog(false);
|
||||
setSelectedItemAction(undefined);
|
||||
}, [page]);
|
||||
|
||||
if (page) {
|
||||
setPage(page);
|
||||
setShowDeletionAlert(true);
|
||||
useEffect(() => {
|
||||
if (selectedItemAction && selectedItemAction.path) {
|
||||
const pageItem = pageItems.find((p) => p.fmFilePath === selectedItemAction.path);
|
||||
|
||||
if (pageItem) {
|
||||
setPage(pageItem);
|
||||
|
||||
if (selectedItemAction.action === 'delete') {
|
||||
setShowDeletionAlert(true);
|
||||
} else if (selectedItemAction.action === 'move') {
|
||||
setShowMoveDialog(true);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedItemAction(undefined);
|
||||
@@ -85,6 +104,15 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({
|
||||
|
||||
<img className='hidden' src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Ffrontmatter.codes%2Fmetrics%2Fdashboards&slug=content" alt="Content metrics" />
|
||||
|
||||
{showMoveDialog && page && (
|
||||
<MoveFileDialog
|
||||
page={page}
|
||||
availableFolders={pageFolders}
|
||||
dismiss={onDismiss}
|
||||
trigger={onMoveConfirm}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDeletionAlert && page && (
|
||||
<Alert
|
||||
title={l10n.t(LocalizationKey.dashboardContentsContentActionsAlertTitle, page.title)}
|
||||
|
||||
@@ -17,6 +17,8 @@ export const List: React.FunctionComponent<IListProps> = ({
|
||||
className = `grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5 gap-4`;
|
||||
} else if (view === DashboardViewType.List) {
|
||||
className = `-mx-4`;
|
||||
} else if (view === DashboardViewType.Structure) {
|
||||
className = `structure-view`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,9 +9,9 @@ import { GroupOption } from '../../constants/GroupOption';
|
||||
import { GroupingSelector, PageAtom, PagedItems, ViewSelector } from '../../state';
|
||||
import { Item } from './Item';
|
||||
import { List } from './List';
|
||||
import { StructureView } from './StructureView';
|
||||
import usePagination from '../../hooks/usePagination';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { LocalizationKey, localize } from '../../../localization';
|
||||
import { PinnedItemsAtom } from '../../state/atom/PinnedItems';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
@@ -54,13 +54,18 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
|
||||
|
||||
const groupName = useCallback(
|
||||
(groupId, groupedPages) => {
|
||||
const count = groupedPages[groupId].length;
|
||||
if (grouping === GroupOption.Draft) {
|
||||
return `${groupId} (${groupedPages[groupId].length})`;
|
||||
return `${groupId} (${count})`;
|
||||
} else if (typeof grouping === 'string') {
|
||||
const group = settings?.grouping?.find((g) => g.name === grouping);
|
||||
const prefix = group?.title ? `${group.title}: ` : '';
|
||||
return `${prefix}${groupId} (${count})`;
|
||||
}
|
||||
|
||||
return `${GroupOption[grouping]}: ${groupId} (${groupedPages[groupId].length})`;
|
||||
return `${GroupOption[grouping]}: ${groupId} (${count})`;
|
||||
},
|
||||
[grouping]
|
||||
[grouping, settings?.grouping]
|
||||
);
|
||||
|
||||
const { groupKeys, groupedPages } = useMemo(() => {
|
||||
@@ -68,7 +73,18 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
|
||||
return { groupKeys: [], groupedPages: {} };
|
||||
}
|
||||
|
||||
let groupedPages = groupBy(pages, grouping === GroupOption.Year ? 'fmYear' : 'fmDraft');
|
||||
let groupName: string | undefined;
|
||||
if (grouping === GroupOption.Year) {
|
||||
groupName = 'fmYear';
|
||||
} else if (grouping === GroupOption.Draft) {
|
||||
groupName = 'fmDraft';
|
||||
} else if (typeof grouping === 'string') {
|
||||
groupName = grouping;
|
||||
} else {
|
||||
return { groupKeys: [], groupedPages: {} };
|
||||
}
|
||||
|
||||
let groupedPages = groupBy(pages, groupName);
|
||||
let groupKeys = Object.keys(groupedPages);
|
||||
|
||||
if (grouping === GroupOption.Year) {
|
||||
@@ -96,6 +112,8 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
|
||||
...groupedPages,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
groupKeys = groupKeys.sort();
|
||||
}
|
||||
|
||||
return { groupKeys, groupedPages };
|
||||
@@ -127,15 +145,22 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
|
||||
className={`h-32 mx-auto opacity-90 mb-8 text-[var(--vscode-editor-foreground)]`}
|
||||
/>
|
||||
{settings && settings?.contentFolders?.length > 0 ? (
|
||||
<p className={`text-xl font-medium`}>{l10n.t(LocalizationKey.dashboardContentsOverviewNoMarkdown)}</p>
|
||||
<p className={`text-xl font-medium`}>{localize(LocalizationKey.dashboardContentsOverviewNoMarkdown)}</p>
|
||||
|
||||
) : (
|
||||
<p className={`text-lg font-medium`}>{l10n.t(LocalizationKey.dashboardContentsOverviewNoFolders)}</p>
|
||||
<p className={`text-lg font-medium`}>{localize(LocalizationKey.dashboardContentsOverviewNoFolders)}</p>
|
||||
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Structure view first - it overrides all other display modes
|
||||
if (view === DashboardViewType.Structure) {
|
||||
return <StructureView pages={pages} />;
|
||||
}
|
||||
|
||||
if (grouping !== GroupOption.none) {
|
||||
return (
|
||||
<>
|
||||
@@ -176,7 +201,8 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
|
||||
<div className='mb-8'>
|
||||
<h1 className='text-xl flex space-x-2 items-center mb-4'>
|
||||
<PinIcon className={`-rotate-45`} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardContentsOverviewPinned)}</span>
|
||||
<span>{localize(LocalizationKey.dashboardContentsOverviewPinned)}</span>
|
||||
|
||||
</h1>
|
||||
<List>
|
||||
{pinnedPages.map((page, idx) => (
|
||||
|
||||
79
src/dashboardWebView/components/Contents/StructureItem.tsx
Normal file
79
src/dashboardWebView/components/Contents/StructureItem.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import { Page } from '../../models/Page';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { DateField } from '../Common/DateField';
|
||||
import { ContentActions } from './ContentActions';
|
||||
import { useMemo } from 'react';
|
||||
import { Status } from './Status';
|
||||
import * as React from 'react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import useCard from '../../hooks/useCard';
|
||||
import { ItemSelection } from '../Common/ItemSelection';
|
||||
import { openFile } from '../../utils';
|
||||
import useSelectedItems from '../../hooks/useSelectedItems';
|
||||
import { cn } from '../../../utils/cn';
|
||||
|
||||
export interface IStructureItemProps extends Page { }
|
||||
|
||||
export const StructureItem: React.FunctionComponent<IStructureItemProps> = ({
|
||||
...pageData
|
||||
}: React.PropsWithChildren<IStructureItemProps>) => {
|
||||
const { selectedFiles } = useSelectedItems();
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const draftField = useMemo(() => settings?.draftField, [settings]);
|
||||
const { escapedTitle } = useCard(pageData, settings?.dashboardState?.contents?.cardFields);
|
||||
|
||||
const isSelected = useMemo(() => selectedFiles.includes(pageData.fmFilePath), [selectedFiles, pageData.fmFilePath]);
|
||||
|
||||
const onOpenFile = React.useCallback(() => {
|
||||
openFile(pageData.fmFilePath);
|
||||
}, [pageData.fmFilePath]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
`flex items-center space-x-3 py-1 px-2 rounded cursor-pointer hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-editor-foreground)]`,
|
||||
isSelected && `bg-[var(--vscode-list-activeSelectionBackground)]`
|
||||
)}
|
||||
>
|
||||
<ItemSelection filePath={pageData.fmFilePath} show />
|
||||
|
||||
<MarkdownIcon className="w-4 h-4 text-[var(--vscode-symbolIcon-fileForeground)] flex-shrink-0" />
|
||||
|
||||
<button
|
||||
title={escapedTitle ? l10n.t(LocalizationKey.commonOpenWithValue, escapedTitle) : l10n.t(LocalizationKey.commonOpen)}
|
||||
onClick={onOpenFile}
|
||||
className="flex-1 text-left truncate font-medium"
|
||||
>
|
||||
{escapedTitle}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center space-x-2 flex-shrink-0">
|
||||
{pageData.date && (
|
||||
<DateField
|
||||
value={pageData.date}
|
||||
format={pageData.fmDateFormat}
|
||||
className="text-xs text-[var(--vscode-descriptionForeground)]"
|
||||
/>
|
||||
)}
|
||||
|
||||
{draftField && draftField.name && typeof pageData[draftField.name] !== "undefined" && (
|
||||
<Status draft={pageData[draftField.name]} published={pageData.fmPublished} />
|
||||
)}
|
||||
|
||||
<ContentActions
|
||||
path={pageData.fmFilePath}
|
||||
relPath={pageData.fmRelFileWsPath}
|
||||
contentType={pageData.fmContentType}
|
||||
scripts={settings?.scripts}
|
||||
onOpen={onOpenFile}
|
||||
listView
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
321
src/dashboardWebView/components/Contents/StructureView.tsx
Normal file
321
src/dashboardWebView/components/Contents/StructureView.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import { ChevronRightIcon, FolderIcon, PlusIcon, HomeIcon, ArrowLeftIcon } from '@heroicons/react/24/solid';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Page } from '../../models';
|
||||
import { StructureItem } from './StructureItem';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { SelectedStructureFolderAtom, SettingsSelector } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
|
||||
export interface IStructureViewProps {
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
interface FolderNode {
|
||||
name: string;
|
||||
path: string;
|
||||
children: FolderNode[];
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
|
||||
pages
|
||||
}: React.PropsWithChildren<IStructureViewProps>) => {
|
||||
const [selectedFolder, setSelectedFolder] = useRecoilState(SelectedStructureFolderAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const folderTree = useMemo(() => {
|
||||
const root: FolderNode = {
|
||||
name: '',
|
||||
path: '',
|
||||
children: [],
|
||||
pages: []
|
||||
};
|
||||
|
||||
const folderMap = new Map<string, FolderNode>();
|
||||
folderMap.set('', root);
|
||||
|
||||
// Helper to compute the normalized workspace-relative folder path for a page.
|
||||
// This returns the actual folder path relative to the workspace, not just titles.
|
||||
const computeNormalizedFolderPath = (page: Page): string => {
|
||||
if (!page.fmFolder) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const fmFolder = page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
|
||||
// Use fmRelFilePath which is already workspace-relative
|
||||
if (page.fmRelFilePath) {
|
||||
const relPath = parseWinPath(page.fmRelFilePath).replace(/^\/+|\/+$/g, '');
|
||||
const relDir = relPath.includes('/') ? relPath.substring(0, relPath.lastIndexOf('/')).replace(/^\/+|\/+$/g, '') : '';
|
||||
if (relDir) {
|
||||
return relDir;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use fmFolder title if we can't determine the path
|
||||
return fmFolder;
|
||||
};
|
||||
|
||||
// First pass: create all folder nodes (ensure nodes exist even if a page lacks fmFilePath)
|
||||
for (const page of pages) {
|
||||
if (!page.fmFolder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedPath = computeNormalizedFolderPath(page).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
if (!normalizedPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parts = normalizedPath.split('/').filter(part => part.length > 0);
|
||||
|
||||
let currentPath = '';
|
||||
let currentNode = root;
|
||||
|
||||
for (const part of parts) {
|
||||
const fullPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
|
||||
if (!folderMap.has(fullPath)) {
|
||||
const newNode: FolderNode = {
|
||||
name: part,
|
||||
path: fullPath,
|
||||
children: [],
|
||||
pages: []
|
||||
};
|
||||
folderMap.set(fullPath, newNode);
|
||||
currentNode.children.push(newNode);
|
||||
}
|
||||
|
||||
const nextNode = folderMap.get(fullPath);
|
||||
if (nextNode) {
|
||||
currentNode = nextNode;
|
||||
}
|
||||
currentPath = fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: assign pages to their exact folder node (including subfolders)
|
||||
for (const page of pages) {
|
||||
if (!page.fmFolder) {
|
||||
root.pages.push(page);
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedPath = computeNormalizedFolderPath(page).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const folderNode = normalizedPath ? folderMap.get(normalizedPath) : folderMap.get(page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''));
|
||||
if (folderNode) {
|
||||
folderNode.pages.push(page);
|
||||
} else {
|
||||
// If folder not found, add to root as fallback
|
||||
root.pages.push(page);
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}, [pages]);
|
||||
|
||||
// Filter the folder tree based on the selected folder
|
||||
const displayedNode = useMemo(() => {
|
||||
if (!selectedFolder) {
|
||||
return folderTree;
|
||||
}
|
||||
|
||||
// Find the selected folder node in the tree
|
||||
const findNode = (node: FolderNode, path: string): FolderNode | null => {
|
||||
if (node.path === path) {
|
||||
return node;
|
||||
}
|
||||
for (const child of node.children) {
|
||||
const found = findNode(child, path);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const foundNode = findNode(folderTree, selectedFolder);
|
||||
return foundNode || folderTree;
|
||||
}, [folderTree, selectedFolder]);
|
||||
|
||||
const handleFolderClick = (folderPath: string) => {
|
||||
setSelectedFolder(folderPath);
|
||||
};
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (!selectedFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to parent folder
|
||||
const parts = selectedFolder.split('/');
|
||||
if (parts.length > 1) {
|
||||
const parentPath = parts.slice(0, -1).join('/');
|
||||
setSelectedFolder(parentPath);
|
||||
} else {
|
||||
setSelectedFolder(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHomeClick = () => {
|
||||
setSelectedFolder(null);
|
||||
};
|
||||
|
||||
const handleCreateContent = () => {
|
||||
Messenger.send(DashboardMessage.createContentInFolder, { folderPath: selectedFolder });
|
||||
};
|
||||
|
||||
const renderFolderNode = (node: FolderNode, depth = 0): React.ReactNode => {
|
||||
const hasContent = node.pages.length > 0 || node.children.length > 0;
|
||||
|
||||
if (!hasContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRoot = depth === 0;
|
||||
const paddingLeft = depth * 20;
|
||||
|
||||
if (isRoot) {
|
||||
// For root node, render children and pages directly
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
{/* Root level folders */}
|
||||
{node.children.map(child => renderFolderNode(child, depth + 1))}
|
||||
|
||||
{/* Root level pages */}
|
||||
{node.pages.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-3 text-[var(--vscode-editor-foreground)]">
|
||||
Root Files
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{node.pages.map((page, idx) => (
|
||||
<li key={`${page.slug}-${idx}`}>
|
||||
<StructureItem {...page} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={node.path} className="mb-4">
|
||||
<Disclosure defaultOpen={depth <= 1}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="flex items-center w-full gap-1" style={{ paddingLeft: `${paddingLeft}px` }}>
|
||||
<Disclosure.Button className="flex items-center text-left hover:bg-[var(--vscode-list-hoverBackground)] rounded px-2 py-1 transition-colors">
|
||||
<ChevronRightIcon
|
||||
className={`w-4 h-4 transform transition-transform ${open ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
|
||||
<button
|
||||
onClick={() => handleFolderClick(node.path)}
|
||||
className="flex items-center flex-1 px-2 py-1 hover:bg-[var(--vscode-list-hoverBackground)] rounded transition-colors"
|
||||
title={l10n.t(LocalizationKey.commonOpen)}
|
||||
>
|
||||
<FolderIcon className="w-4 h-4 mr-2 flex-shrink-0 text-[var(--vscode-symbolIcon-folderForeground)]" />
|
||||
<span className="flex items-center font-medium text-[var(--vscode-editor-foreground)] flex-1">
|
||||
<span className="mr-2">{node.name}</span>
|
||||
{node.pages.length > 0 && (
|
||||
<span className="text-sm text-[var(--vscode-descriptionForeground)]">
|
||||
({node.pages.length} {node.pages.length === 1 ? 'file' : 'files'})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<ChevronRightIcon className="w-4 h-4 flex-shrink-0 text-[var(--vscode-descriptionForeground)]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="mt-2">
|
||||
{/* Child folders */}
|
||||
{node.children.map(child => renderFolderNode(child, depth + 1))}
|
||||
|
||||
{/* Pages in this folder */}
|
||||
{node.pages.length > 0 && (
|
||||
<ul className="space-y-1 mb-3">
|
||||
{node.pages.map((page, idx) => (
|
||||
<li key={`${page.slug}-${idx}`} style={{ paddingLeft: `${paddingLeft + 20}px` }}>
|
||||
<StructureItem {...page} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="structure-view">
|
||||
{/* Toolbar */}
|
||||
<div className="mb-4 pb-3 border-b border-[var(--frontmatter-border)]">
|
||||
{/* Breadcrumb navigation */}
|
||||
{selectedFolder && (
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleHomeClick}
|
||||
className="p-1 hover:bg-[var(--vscode-list-hoverBackground)] rounded"
|
||||
title="Home"
|
||||
>
|
||||
<HomeIcon className="w-4 h-4 text-[var(--vscode-descriptionForeground)]" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBackClick}
|
||||
className="flex items-center space-x-1 px-2 py-1 hover:bg-[var(--vscode-list-hoverBackground)] rounded text-sm"
|
||||
title={l10n.t(LocalizationKey.commonBack) || 'Back'}
|
||||
>
|
||||
<ArrowLeftIcon className="w-3 h-3" />
|
||||
<span>{l10n.t(LocalizationKey.commonBack) || 'Back'}</span>
|
||||
</button>
|
||||
<span className="text-sm text-[var(--vscode-descriptionForeground)]">
|
||||
/ {selectedFolder.split('/').join(' / ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create content button */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleCreateContent}
|
||||
disabled={!settings?.initialized}
|
||||
className="inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium focus:outline-none rounded text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50"
|
||||
title={selectedFolder
|
||||
? l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent) + ` in ${selectedFolder}`
|
||||
: l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent)}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4 mr-1" />
|
||||
<span>
|
||||
{selectedFolder
|
||||
? `${l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent)} here`
|
||||
: l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent)}
|
||||
</span>
|
||||
</button>
|
||||
{selectedFolder && (
|
||||
<span className="text-xs text-[var(--vscode-descriptionForeground)]">
|
||||
in {selectedFolder}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Folder tree */}
|
||||
{renderFolderNode(displayedNode)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
38
src/dashboardWebView/components/Header/BooleanOption.tsx
Normal file
38
src/dashboardWebView/components/Header/BooleanOption.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Checkbox as VSCodeCheckbox } from 'vscrui';
|
||||
|
||||
export interface IBooleanOptionProps {
|
||||
value: boolean | undefined | null;
|
||||
name: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const BooleanOption: React.FunctionComponent<IBooleanOptionProps> = ({
|
||||
value,
|
||||
name,
|
||||
label
|
||||
}: React.PropsWithChildren<IBooleanOptionProps>) => {
|
||||
const [isChecked, setIsChecked] = React.useState(false);
|
||||
|
||||
const onChange = React.useCallback((newValue: boolean) => {
|
||||
setIsChecked(newValue);
|
||||
Messenger.send(DashboardMessage.updateSetting, {
|
||||
name: name,
|
||||
value: newValue
|
||||
});
|
||||
}, [name]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsChecked(!!value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<VSCodeCheckbox
|
||||
onChange={onChange}
|
||||
checked={isChecked}>
|
||||
{label}
|
||||
</VSCodeCheckbox>
|
||||
);
|
||||
};
|
||||
@@ -24,17 +24,35 @@ export const Filters: React.FunctionComponent<IFiltersProps> = () => {
|
||||
return otherFilters?.map((filter) => {
|
||||
const filterName = typeof filter === "string" ? filter : filter.name;
|
||||
const filterTitle = typeof filter === "string" ? firstToUpper(filter) : filter.title;
|
||||
const values = filterValues?.[filterName];
|
||||
let values = filterValues?.[filterName];
|
||||
if (!values || values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all the unique values
|
||||
const individualValues = new Set<string>();
|
||||
values.forEach((value) => {
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => individualValues.add(v));
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
individualValues.add(value);
|
||||
}
|
||||
});
|
||||
|
||||
values = Array.from(individualValues);
|
||||
|
||||
return (
|
||||
<Filter
|
||||
key={filterName}
|
||||
label={filterTitle}
|
||||
activeItem={crntFilters[filterName]}
|
||||
items={values}
|
||||
items={values as string[]}
|
||||
onClick={(value) => setCrntFilters((prev) => {
|
||||
const clone = Object.assign({}, prev);
|
||||
if (!clone[filterName] && value) {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { GroupOption } from '../../constants/GroupOption';
|
||||
import { AllPagesAtom, GroupingAtom } from '../../state';
|
||||
import { AllPagesAtom, GroupingAtom, SettingsAtom } from '../../state';
|
||||
import { MenuButton, MenuItem } from '../Menu';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { LocalizationKey, localize } from '../../../localization';
|
||||
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
|
||||
|
||||
export interface IGroupingProps { }
|
||||
@@ -12,28 +11,34 @@ export interface IGroupingProps { }
|
||||
export const Grouping: React.FunctionComponent<
|
||||
IGroupingProps
|
||||
> = () => {
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const [group, setGroup] = useRecoilState(GroupingAtom);
|
||||
const pages = useRecoilValue(AllPagesAtom);
|
||||
|
||||
const GROUP_OPTIONS = React.useMemo(() => {
|
||||
const options: { name: string, id: GroupOption }[] = [];
|
||||
const options: { name: string, id?: GroupOption | string }[] = [];
|
||||
|
||||
if (pages.length > 0) {
|
||||
if (settings?.grouping) {
|
||||
const groups = settings.grouping.map((g) => ({ name: g.title, id: g.name }));
|
||||
options.push(...groups);
|
||||
}
|
||||
|
||||
if (pages.some((x) => x.fmYear)) {
|
||||
options.push({ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionYear), id: GroupOption.Year })
|
||||
options.push({ name: localize(LocalizationKey.dashboardHeaderGroupingOptionYear), id: GroupOption.Year })
|
||||
}
|
||||
|
||||
if (pages.some((x) => x.fmDraft)) {
|
||||
options.push({ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionDraft), id: GroupOption.Draft })
|
||||
options.push({ name: localize(LocalizationKey.dashboardHeaderGroupingOptionDraft), id: GroupOption.Draft })
|
||||
}
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
options.unshift({ name: l10n.t(LocalizationKey.dashboardHeaderGroupingOptionNone), id: GroupOption.none })
|
||||
options.unshift({ name: localize(LocalizationKey.dashboardHeaderGroupingOptionNone), id: GroupOption.none })
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [pages])
|
||||
}, [pages, settings?.grouping])
|
||||
|
||||
const crntGroup = GROUP_OPTIONS.find((x) => x.id === group);
|
||||
|
||||
@@ -43,7 +48,7 @@ export const Grouping: React.FunctionComponent<
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<MenuButton label={l10n.t(LocalizationKey.dashboardHeaderGroupingMenuButtonLabel)} title={crntGroup?.name || ''} />
|
||||
<MenuButton label={localize(LocalizationKey.dashboardHeaderGroupingMenuButtonLabel)} title={crntGroup?.name || ''} />
|
||||
|
||||
<DropdownMenuContent>
|
||||
{GROUP_OPTIONS.map((option) => (
|
||||
|
||||
@@ -2,10 +2,11 @@ import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import usePagination from '../../hooks/usePagination';
|
||||
import { MediaTotalSelector, PageAtom, SettingsAtom } from '../../state';
|
||||
import { MediaTotalSelector, PageAtom, SettingsAtom, ViewSelector } from '../../state';
|
||||
import { PaginationButton } from './PaginationButton';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { DashboardViewType } from '../../models';
|
||||
|
||||
export interface IPaginationProps {
|
||||
totalPages?: number;
|
||||
@@ -17,21 +18,35 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({
|
||||
const [page, setPage] = useRecoilState(PageAtom);
|
||||
const totalMedia = useRecoilValue(MediaTotalSelector);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const view = useRecoilValue(ViewSelector);
|
||||
const { pageSetNr, totalPagesNr } = usePagination(
|
||||
settings?.dashboardState.contents.pagination,
|
||||
totalPages,
|
||||
totalMedia
|
||||
);
|
||||
|
||||
const getButtons = useCallback((): number[] => {
|
||||
const buttons = useMemo((): JSX.Element[] => {
|
||||
const maxButtons = 5;
|
||||
const buttons: number[] = [];
|
||||
const buttons: JSX.Element[] = [];
|
||||
const start = page - maxButtons;
|
||||
const end = page + maxButtons;
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
if (i >= 0 && i <= totalPagesNr) {
|
||||
buttons.push(i);
|
||||
buttons.push(
|
||||
<button
|
||||
key={i}
|
||||
disabled={i === page}
|
||||
onClick={() => {
|
||||
setPage(i);
|
||||
}}
|
||||
className={`max-h-8 rounded ${page === i
|
||||
? `px-2 bg-[var(--vscode-list-activeSelectionBackground)] text-[var(--vscode-list-activeSelectionForeground)]`
|
||||
: `text-[var(--vscode-editor-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)]`}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
@@ -45,6 +60,10 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({
|
||||
setPage(0);
|
||||
}, []);
|
||||
|
||||
if (view === DashboardViewType.Structure) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
|
||||
<PaginationButton
|
||||
@@ -67,20 +86,7 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
{getButtons().map((button) => (
|
||||
<button
|
||||
key={button}
|
||||
disabled={button === page}
|
||||
onClick={() => {
|
||||
setPage(button);
|
||||
}}
|
||||
className={`max-h-8 rounded ${page === button
|
||||
? `px-2 bg-[var(--vscode-list-activeSelectionBackground)] text-[var(--vscode-list-activeSelectionForeground)]`
|
||||
: `text-[var(--vscode-editor-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)]`}`}
|
||||
>
|
||||
{button + 1}
|
||||
</button>
|
||||
))}
|
||||
{buttons}
|
||||
|
||||
<PaginationButton
|
||||
title={l10n.t(LocalizationKey.dashboardHeaderPaginationNext)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { ViewAtom, SettingsSelector } from '../../state';
|
||||
import { Bars4Icon, Squares2X2Icon } from '@heroicons/react/24/solid';
|
||||
import { Bars4Icon, Squares2X2Icon, FolderIcon } from '@heroicons/react/24/solid';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { DashboardViewType } from '../../models';
|
||||
@@ -16,9 +16,7 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (
|
||||
const [view, setView] = useRecoilState(ViewAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const toggleView = () => {
|
||||
const newView =
|
||||
view === DashboardViewType.Grid ? DashboardViewType.List : DashboardViewType.Grid;
|
||||
const handleViewChange = (newView: DashboardViewType) => {
|
||||
setView(newView);
|
||||
Messenger.send(DashboardMessage.setPageViewType, newView);
|
||||
};
|
||||
@@ -36,7 +34,7 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (
|
||||
}`}
|
||||
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToGrid)}
|
||||
type={`button`}
|
||||
onClick={toggleView}
|
||||
onClick={() => handleViewChange(DashboardViewType.Grid)}
|
||||
>
|
||||
<Squares2X2Icon className={`w-4 h-4`} />
|
||||
<span className={`sr-only`}>
|
||||
@@ -44,17 +42,29 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`flex items-center px-2 py-1 rounded-r-sm ${view === DashboardViewType.List ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
|
||||
className={`flex items-center px-2 py-1 ${view === DashboardViewType.List ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
|
||||
}`}
|
||||
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToList)}
|
||||
type={`button`}
|
||||
onClick={toggleView}
|
||||
onClick={() => handleViewChange(DashboardViewType.List)}
|
||||
>
|
||||
<Bars4Icon className={`w-4 h-4`} />
|
||||
<span className={`sr-only`}>
|
||||
{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToList)}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`flex items-center px-2 py-1 rounded-r-sm ${view === DashboardViewType.Structure ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
|
||||
}`}
|
||||
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToStructure)}
|
||||
type={`button`}
|
||||
onClick={() => handleViewChange(DashboardViewType.Structure)}
|
||||
>
|
||||
<FolderIcon className={`w-4 h-4`} />
|
||||
<span className={`sr-only`}>
|
||||
{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToStructure)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,8 +91,9 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
|
||||
|
||||
if (scripts.length > 0) {
|
||||
return (
|
||||
<div className="flex flex-1 justify-start">
|
||||
<div className="flex flex-1 justify-start space-x-2">
|
||||
{renderPostAssetsButton}
|
||||
|
||||
<ChoiceButton
|
||||
title={l10n.t(LocalizationKey.dashboardMediaFolderCreationFolderCreate)}
|
||||
choices={scripts.map((s) => ({
|
||||
@@ -103,6 +104,8 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
|
||||
onClick={onFolderCreation}
|
||||
disabled={!settings?.initialized}
|
||||
/>
|
||||
|
||||
<RefreshDashboardData />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { FolderIcon } from '@heroicons/react/24/solid';
|
||||
import { FolderIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/solid';
|
||||
import { basename, join } from 'path';
|
||||
import * as React from 'react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { LocalizationKey, localize } from '../../../localization';
|
||||
import useMediaFolder from '../../hooks/useMediaFolder';
|
||||
import { QuickAction } from '../Menu';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { useState } from 'react';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
|
||||
export interface IFolderItemProps {
|
||||
folder: string;
|
||||
@@ -17,6 +22,7 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({
|
||||
staticFolder
|
||||
}: React.PropsWithChildren<IFolderItemProps>) => {
|
||||
const { updateFolder } = useMediaFolder();
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
|
||||
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
|
||||
|
||||
@@ -25,28 +31,73 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({
|
||||
[relFolderPath, staticFolder]
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={`group relative hover:bg-[var(--vscode-list-hoverBackground)] text-[var(--vscode-editor-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)]`}
|
||||
>
|
||||
<button
|
||||
title={isContentFolder ? l10n.t(LocalizationKey.dashboardMediaFolderItemContentDirectory) : l10n.t(LocalizationKey.dashboardMediaFolderItemPublicDirectory)}
|
||||
className={`p-4 w-full flex flex-row items-center h-full`}
|
||||
onClick={() => updateFolder(folder)}
|
||||
>
|
||||
<div className="relative mr-4">
|
||||
<FolderIcon className={`h-12 w-12`} />
|
||||
{isContentFolder && (
|
||||
<span className={`font-extrabold absolute bottom-3 left-1/2 transform -translate-x-1/2 text-[var(--frontmatter-text)]`}>
|
||||
C
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
const updateFolderName = React.useCallback(() => {
|
||||
messageHandler.send(DashboardMessage.updateMediaFolder, { folder, wsFolder, staticFolder })
|
||||
}, []);
|
||||
|
||||
<p className="text-sm font-bold pointer-events-none flex items-center text-left overflow-hidden break-words">
|
||||
{basename(relFolderPath)}
|
||||
</p>
|
||||
</button>
|
||||
</li>
|
||||
const onDelete = React.useCallback(() => {
|
||||
setShowAlert(true);
|
||||
}, []);
|
||||
|
||||
const confirmDeletion = React.useCallback(() => {
|
||||
messageHandler.send(DashboardMessage.deleteMediaFolder, { folder });
|
||||
setShowAlert(false);
|
||||
}, [folder]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<li
|
||||
className={`flex flex-col group relative text-[var(--vscode-sideBarTitle-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)] shadow-md hover:shadow-xl dark:shadow-none bg-[var(--vscode-sideBar-background)] hover:bg-[var(--vscode-list-hoverBackground)] border border-[var(--frontmatter-border)] rounded`}
|
||||
>
|
||||
<button
|
||||
title={isContentFolder ? localize(LocalizationKey.dashboardMediaFolderItemContentDirectory) : localize(LocalizationKey.dashboardMediaFolderItemPublicDirectory)}
|
||||
className={`p-4 w-full flex flex-row items-center h-full`}
|
||||
onClick={() => updateFolder(folder)}
|
||||
>
|
||||
<div className="relative mr-4">
|
||||
<FolderIcon className={`h-12 w-12`} />
|
||||
{isContentFolder && (
|
||||
<span className={`font-extrabold absolute bottom-3 left-1/2 transform -translate-x-1/2 text-[var(--frontmatter-text)]`}>
|
||||
C
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm font-bold pointer-events-none flex items-center text-left overflow-hidden break-words">
|
||||
{basename(relFolderPath)}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
{!isContentFolder && (
|
||||
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)]`}>
|
||||
<QuickAction
|
||||
title={localize(LocalizationKey.commonEdit)}
|
||||
className={`text-[var(--frontmatter-secondary-text)]`}
|
||||
onClick={updateFolderName}>
|
||||
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
<span className='sr-only'>{localize(LocalizationKey.dashboardMediaItemMenuItemView)}</span>
|
||||
</QuickAction>
|
||||
|
||||
<QuickAction
|
||||
title={localize(LocalizationKey.dashboardMediaItemQuickActionDelete)}
|
||||
className={`text-[var(--frontmatter-secondary-text)] hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
</QuickAction>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
|
||||
{showAlert && (
|
||||
<Alert
|
||||
title={`${localize(LocalizationKey.commonDelete)}: ${basename(parseWinPath(folder) || '')}`}
|
||||
description={localize(LocalizationKey.dashboardMediaFolderItemDeleteDescription, folder)}
|
||||
okBtnText={localize(LocalizationKey.commonDelete)}
|
||||
cancelBtnText={localize(LocalizationKey.commonCancel)}
|
||||
dismiss={() => setShowAlert(false)}
|
||||
trigger={confirmDeletion}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { QuickAction } from '../Menu';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { LocalizationKey, localize } from '../../../localization';
|
||||
import { ClipboardIcon, CodeBracketIcon, EyeIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/solid';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { SelectedItemActionAtom } from '../../state';
|
||||
@@ -36,25 +35,25 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
|
||||
return (
|
||||
<div className={`py-2 w-full flex items-center justify-evenly border-t border-t-[var(--frontmatter-border)] bg-[var(--frontmatter-sideBar-background)] group-hover:bg-[var(--vscode-list-hoverBackground)]`}>
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)}
|
||||
title={localize(LocalizationKey.dashboardMediaItemMenuItemView)}
|
||||
className={`text-[var(--frontmatter-secondary-text)]`}
|
||||
onClick={() => setSelectedItemAction({
|
||||
path: media.fsPath,
|
||||
action: 'view'
|
||||
})}>
|
||||
<EyeIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemView)}</span>
|
||||
<span className='sr-only'>{localize(LocalizationKey.dashboardMediaItemMenuItemView)}</span>
|
||||
</QuickAction>
|
||||
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}
|
||||
title={localize(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}
|
||||
className={`text-[var(--frontmatter-secondary-text)]`}
|
||||
onClick={() => setSelectedItemAction({
|
||||
path: media.fsPath,
|
||||
action: 'edit'
|
||||
})}>
|
||||
<PencilIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
<span className='sr-only'>{l10n.t(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
|
||||
<span className='sr-only'>{localize(LocalizationKey.dashboardMediaItemMenuItemEditMetadata)}</span>
|
||||
</QuickAction>
|
||||
|
||||
{viewData?.filePath ? (
|
||||
@@ -62,8 +61,8 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
|
||||
<QuickAction
|
||||
title={
|
||||
viewData.metadataInsert && viewData.fieldName
|
||||
? l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertField, viewData.fieldName)
|
||||
: l10n.t(LocalizationKey.dashboardMediaItemQuickActionInsertMarkdown)
|
||||
? localize(LocalizationKey.dashboardMediaItemQuickActionInsertField, viewData.fieldName)
|
||||
: localize(LocalizationKey.dashboardMediaItemQuickActionInsertMarkdown)
|
||||
}
|
||||
className={`text-[var(--frontmatter-secondary-text)]`}
|
||||
onClick={insertIntoArticle}
|
||||
@@ -73,7 +72,7 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
|
||||
|
||||
{viewData?.position && snippets.length > 0 && (
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.commonInsertSnippet)}
|
||||
title={localize(LocalizationKey.commonInsertSnippet)}
|
||||
className={`text-[var(--frontmatter-secondary-text)]`}
|
||||
onClick={insertSnippet}>
|
||||
<CodeBracketIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
@@ -85,7 +84,7 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
|
||||
{
|
||||
relPath && (
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}
|
||||
title={localize(LocalizationKey.dashboardMediaItemQuickActionCopyPath)}
|
||||
className={`text-[var(--frontmatter-secondary-text)]`}
|
||||
onClick={() => copyToClipboard(parseWinPath(relPath) || '')}>
|
||||
<ClipboardIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
@@ -101,7 +100,7 @@ export const FooterActions: React.FunctionComponent<IFooterActionsProps> = ({
|
||||
showTrigger />
|
||||
|
||||
<QuickAction
|
||||
title={l10n.t(LocalizationKey.dashboardMediaItemQuickActionDelete)}
|
||||
title={localize(LocalizationKey.dashboardMediaItemQuickActionDelete)}
|
||||
className={`text-[var(--frontmatter-secondary-text)] hover:text-[var(--vscode-statusBarItem-errorBackground)]`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden="true" />
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PlusIcon,
|
||||
VideoCameraIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { basename } from 'path';
|
||||
import { basename, parse } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
@@ -55,8 +55,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({
|
||||
const { mediaFolder, mediaDetails, isAudio, isImage, isVideo } = useMediaInfo(media);
|
||||
|
||||
const relPath = useMemo(() => {
|
||||
if (viewData?.data?.pageBundle && viewData?.data?.filePath) {
|
||||
const articlePath = viewData?.data?.filePath;
|
||||
const articleDir = parse(parseWinPath(articlePath)).dir;
|
||||
|
||||
const mediaPath = parseWinPath(media.fsPath);
|
||||
if (mediaPath.startsWith(articleDir)) {
|
||||
return getRelPath(media.fsPath, undefined, articleDir);
|
||||
}
|
||||
}
|
||||
return getRelPath(media.fsPath, settings?.staticFolder, settings?.wsFolder);
|
||||
}, [media.fsPath, settings?.staticFolder, settings?.wsFolder]);
|
||||
}, [media.fsPath, settings?.staticFolder, settings?.wsFolder, viewData?.data?.pageBundle, viewData?.data?.filePath]);
|
||||
|
||||
const hasViewData = useMemo(() => {
|
||||
return viewData?.data?.filePath !== undefined;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PagedItems,
|
||||
SelectedMediaFolderAtom,
|
||||
SettingsSelector,
|
||||
SortingAtom,
|
||||
ViewDataSelector
|
||||
} from '../../state';
|
||||
import { Spinner } from '../Common/Spinner';
|
||||
@@ -30,18 +31,18 @@ import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { MediaItemPanel } from './MediaItemPanel';
|
||||
import { FilesProvider } from '../../providers/FilesProvider';
|
||||
import { SortOption } from '../../constants/SortOption';
|
||||
|
||||
export interface IMediaProps { }
|
||||
|
||||
export const Media: React.FunctionComponent<IMediaProps> = (
|
||||
_: React.PropsWithChildren<IMediaProps>
|
||||
) => {
|
||||
export const Media: React.FunctionComponent<IMediaProps> = () => {
|
||||
const { media } = useMedia();
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
|
||||
const folders = useRecoilValue(MediaFoldersAtom);
|
||||
const loading = useRecoilValue(LoadingAtom);
|
||||
const crntSorting = useRecoilValue(SortingAtom);
|
||||
const [, setPagedItems] = useRecoilState(PagedItems);
|
||||
|
||||
const currentStaticFolder = useMemo(() => {
|
||||
@@ -85,11 +86,18 @@ export const Media: React.FunctionComponent<IMediaProps> = (
|
||||
currentStaticFolder &&
|
||||
settings?.staticFolder !== STATIC_FOLDER_PLACEHOLDER.hexo.placeholder
|
||||
) {
|
||||
return folders.filter((f) => parseWinPath(f).includes(currentStaticFolder));
|
||||
const allFolders = folders.filter((f) => parseWinPath(f).includes(currentStaticFolder));
|
||||
if (crntSorting && crntSorting.id === SortOption.FileNameAsc) {
|
||||
return allFolders.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
} else if (crntSorting && crntSorting.id === SortOption.FileNameDesc) {
|
||||
return allFolders.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
||||
} else {
|
||||
return allFolders;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [folders, viewData, currentStaticFolder, settings?.staticFolder]);
|
||||
}, [folders, viewData, currentStaticFolder, settings?.staticFolder, crntSorting]);
|
||||
|
||||
const allMedia = useMemo(() => {
|
||||
let mediaFiles: MediaInfo[] = Object.assign([], media);
|
||||
|
||||
175
src/dashboardWebView/components/Modals/MoveFileDialog.tsx
Normal file
175
src/dashboardWebView/components/Modals/MoveFileDialog.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { FolderIcon, ChevronRightIcon } from '@heroicons/react/24/solid';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { Page } from '../../models';
|
||||
|
||||
export interface IMoveFileDialogProps {
|
||||
page: Page;
|
||||
availableFolders: string[];
|
||||
dismiss: () => void;
|
||||
trigger: (destinationFolder: string) => void;
|
||||
}
|
||||
|
||||
interface FolderNode {
|
||||
name: string;
|
||||
path: string;
|
||||
children: FolderNode[];
|
||||
level: number;
|
||||
}
|
||||
|
||||
export const MoveFileDialog: React.FunctionComponent<IMoveFileDialogProps> = ({
|
||||
page,
|
||||
availableFolders,
|
||||
dismiss,
|
||||
trigger
|
||||
}: React.PropsWithChildren<IMoveFileDialogProps>) => {
|
||||
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
|
||||
// Build folder tree structure
|
||||
const folderTree = useMemo(() => {
|
||||
const root: FolderNode[] = [];
|
||||
const folderMap = new Map<string, FolderNode>();
|
||||
|
||||
for (const folderPath of availableFolders) {
|
||||
const normalized = parseWinPath(folderPath).replace(/^\/+|\/+$/g, '');
|
||||
const parts = normalized.split('/').filter(Boolean);
|
||||
|
||||
let currentPath = '';
|
||||
let currentLevel: FolderNode[] = root;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const fullPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
|
||||
if (!folderMap.has(fullPath)) {
|
||||
const newNode: FolderNode = {
|
||||
name: part,
|
||||
path: fullPath,
|
||||
children: [],
|
||||
level: i
|
||||
};
|
||||
folderMap.set(fullPath, newNode);
|
||||
currentLevel.push(newNode);
|
||||
}
|
||||
|
||||
const node = folderMap.get(fullPath);
|
||||
if (node) {
|
||||
currentLevel = node.children;
|
||||
}
|
||||
currentPath = fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}, [availableFolders]);
|
||||
|
||||
const toggleFolder = (folderPath: string) => {
|
||||
const newExpanded = new Set(expandedFolders);
|
||||
if (newExpanded.has(folderPath)) {
|
||||
newExpanded.delete(folderPath);
|
||||
} else {
|
||||
newExpanded.add(folderPath);
|
||||
}
|
||||
setExpandedFolders(newExpanded);
|
||||
};
|
||||
|
||||
const renderFolderNode = (node: FolderNode): React.ReactNode => {
|
||||
const isExpanded = expandedFolders.has(node.path);
|
||||
const isSelected = selectedFolder === node.path;
|
||||
const hasChildren = node.children.length > 0;
|
||||
const paddingLeft = node.level * 20;
|
||||
|
||||
return (
|
||||
<div key={node.path}>
|
||||
<div
|
||||
className={`flex items-center py-1 px-2 cursor-pointer hover:bg-[var(--vscode-list-hoverBackground)] rounded ${isSelected ? 'bg-[var(--vscode-list-activeSelectionBackground)]' : ''
|
||||
}`}
|
||||
style={{ paddingLeft: `${paddingLeft}px` }}
|
||||
onClick={() => setSelectedFolder(node.path)}
|
||||
>
|
||||
{hasChildren && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleFolder(node.path);
|
||||
}}
|
||||
className="mr-1"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={`w-3 h-3 transform transition-transform ${isExpanded ? 'rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{!hasChildren && <span className="w-3 mr-1"></span>}
|
||||
<FolderIcon className="w-4 h-4 mr-2 text-[var(--vscode-symbolIcon-folderForeground)]" />
|
||||
<span className="text-sm text-[var(--vscode-editor-foreground)]">{node.name}</span>
|
||||
</div>
|
||||
{hasChildren && isExpanded && (
|
||||
<div>
|
||||
{node.children.map((child) => renderFolderNode(child))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleMove = () => {
|
||||
if (selectedFolder) {
|
||||
trigger(selectedFolder);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-expand folders by default (first level)
|
||||
useEffect(() => {
|
||||
const firstLevelFolders = folderTree.map(node => node.path);
|
||||
setExpandedFolders(new Set(firstLevelFolders));
|
||||
}, [folderTree]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50">
|
||||
<div className="bg-[var(--vscode-editor-background)] border border-[var(--frontmatter-border)] rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] flex flex-col">
|
||||
<div className="p-6 border-b border-[var(--frontmatter-border)]">
|
||||
<h2 className="text-xl font-bold text-[var(--vscode-editor-foreground)]">
|
||||
Move File
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-[var(--vscode-descriptionForeground)]">
|
||||
Move <span className="font-medium">{page.title}</span> to a different folder
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="space-y-1">
|
||||
{folderTree.length > 0 ? (
|
||||
folderTree.map((node) => renderFolderNode(node))
|
||||
) : (
|
||||
<p className="text-sm text-[var(--vscode-descriptionForeground)]">
|
||||
No folders available
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-t border-[var(--frontmatter-border)] flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={dismiss}
|
||||
className="px-4 py-2 text-sm font-medium rounded text-[var(--vscode-button-foreground)] bg-[var(--vscode-button-secondaryBackground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]"
|
||||
>
|
||||
{l10n.t(LocalizationKey.commonCancel)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMove}
|
||||
disabled={!selectedFolder}
|
||||
className="px-4 py-2 text-sm font-medium rounded text-[var(--vscode-button-foreground)] bg-[var(--frontmatter-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,11 +6,12 @@ import { useRecoilValue } from 'recoil';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { SettingsInput } from './SettingsInput';
|
||||
import { Button as VSCodeButton } from 'vscrui';
|
||||
import { DOCS_SUBMODULES, FrameworkDetectors, GIT_CONFIG, SETTING_FRAMEWORK_START, SETTING_GIT_COMMIT_MSG, SETTING_GIT_ENABLED, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants';
|
||||
import { DOCS_SUBMODULES, FrameworkDetectors, GIT_CONFIG, SETTING_FRAMEWORK_START, SETTING_GIT_COMMIT_MSG, SETTING_GIT_ENABLED, SETTING_PANEL_OPEN_ON_SUPPORTED_FILE, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SettingsCheckbox } from './SettingsCheckbox';
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
import { BooleanOption } from '../Header/BooleanOption';
|
||||
|
||||
export interface ICommonSettingsProps { }
|
||||
|
||||
@@ -66,13 +67,22 @@ export const CommonSettings: React.FunctionComponent<ICommonSettingsProps> = (pr
|
||||
}, [settings?.lastUpdated]);
|
||||
|
||||
return (
|
||||
<div className='w-full divide-y divide-[var(--frontmatter-border)]'>
|
||||
<div className='w-full divide-y divide-[var(--frontmatter-border)] text-[var(--frontmatter-text)]'>
|
||||
<div className='py-4'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsOpenOnStartup)}</h2>
|
||||
|
||||
<Startup settings={settings} />
|
||||
</div>
|
||||
|
||||
<div className='py-4'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsOpenPanelForSupportedFiles)}</h2>
|
||||
|
||||
<BooleanOption
|
||||
label={l10n.t(LocalizationKey.settingsOpenPanelForSupportedFilesLabel)}
|
||||
name={SETTING_PANEL_OPEN_ON_SUPPORTED_FILE}
|
||||
value={settings?.openPanelForSupportedFiles} />
|
||||
</div>
|
||||
|
||||
<div className='py-4'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsGit)}</h2>
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> =
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='w-full divide-y divide-[var(--frontmatter-border)]'>
|
||||
<div className='w-full divide-y divide-[var(--frontmatter-border)] text-[var(--frontmatter-text)]'>
|
||||
<div className='py-4 space-y-4'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsIntegrationsViewDeeplTitle)}</h2>
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const SettingsView: React.FunctionComponent<ISettingsViewProps> = (_: Rea
|
||||
{
|
||||
id: "view-2",
|
||||
content: (
|
||||
<div className='py-4'>
|
||||
<div className='py-4 text-[var(--frontmatter-text)]'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsContentFolders)}</h2>
|
||||
|
||||
<ContentFolders
|
||||
@@ -67,7 +67,7 @@ export const SettingsView: React.FunctionComponent<ISettingsViewProps> = (_: Rea
|
||||
temp.push({
|
||||
id: "view-3",
|
||||
content: (
|
||||
<div className='py-4'>
|
||||
<div className='py-4 text-[var(--frontmatter-text)]'>
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsContentTypes)}</h2>
|
||||
|
||||
<AstroContentTypes
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { Choice, SnippetField, SnippetInfoField } from '../../../models';
|
||||
import { useEffect } from 'react';
|
||||
import { TextField } from '../Common/TextField';
|
||||
import { NumberField } from '../Common/NumberField';
|
||||
|
||||
export interface ISnippetInputFieldProps {
|
||||
field: SnippetField;
|
||||
@@ -78,6 +79,17 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === 'number') {
|
||||
return (
|
||||
<NumberField
|
||||
name={field.name}
|
||||
value={field.value as string || ''}
|
||||
description={field.description}
|
||||
onChange={(e) => onValueChange(field, e)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextField
|
||||
name={field.name}
|
||||
|
||||
@@ -21,9 +21,7 @@ import { DEFAULT_DASHBOARD_FEATURE_FLAGS } from '../../../constants/DefaultFeatu
|
||||
|
||||
export interface ISnippetsProps { }
|
||||
|
||||
export const Snippets: React.FunctionComponent<ISnippetsProps> = (
|
||||
_: React.PropsWithChildren<ISnippetsProps>
|
||||
) => {
|
||||
export const Snippets: React.FunctionComponent<ISnippetsProps> = () => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
const mode = useRecoilValue(ModeAtom);
|
||||
|
||||
@@ -19,7 +19,7 @@ export const SelectItem: React.FunctionComponent<ISelectItemProps> = ({
|
||||
}: React.PropsWithChildren<ISelectItemProps>) => {
|
||||
return (
|
||||
<div
|
||||
className={`text-sm flex items-center ${isSelected ? 'text-[var(--vscode-textLink-foreground)]' : ''}`}
|
||||
className={`text-sm flex items-center ${isSelected ? 'text-[var(--vscode-textLink-foreground)]' : 'text-[var(--frontmatter-text)]'}`}
|
||||
>
|
||||
<button
|
||||
onClick={onClick}
|
||||
|
||||
@@ -107,7 +107,14 @@ export default function usePages(pages: Page[]) {
|
||||
for (const filter of filterNames) {
|
||||
const filterValue = filters[filter];
|
||||
if (filterValue) {
|
||||
pagesSorted = pagesSorted.filter((page) => page[filter] === filterValue);
|
||||
pagesSorted = pagesSorted.filter((page) => {
|
||||
const value = page[filter];
|
||||
if (Array.isArray(value)) {
|
||||
return value.includes(filterValue);
|
||||
} else {
|
||||
return value === filterValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum DashboardViewType {
|
||||
Grid = 1,
|
||||
List
|
||||
List,
|
||||
Structure
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { I18nConfig } from '../../models';
|
||||
import { ContentFolder, I18nConfig } from '../../models';
|
||||
|
||||
export interface Page {
|
||||
// Properties for caching
|
||||
@@ -20,15 +20,16 @@ export interface Page {
|
||||
fmCategories: string[];
|
||||
fmContentType: string;
|
||||
fmDateFormat: string | undefined;
|
||||
fmPageFolder: ContentFolder | undefined;
|
||||
|
||||
// i18n fields
|
||||
fmDefaultLocale?: boolean;
|
||||
fmLocale?: I18nConfig;
|
||||
fmTranslations?: {
|
||||
fmTranslations?: {
|
||||
[locale: string]: {
|
||||
locale: I18nConfig;
|
||||
path: string;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
title: string;
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Settings {
|
||||
categories: string[];
|
||||
customTaxonomy: CustomTaxonomy[];
|
||||
openOnStart: boolean | null;
|
||||
openPanelForSupportedFiles: boolean | null;
|
||||
versionInfo: VersionInfo;
|
||||
pageViewType: DashboardViewType | undefined;
|
||||
contentTypes: ContentType[];
|
||||
@@ -41,6 +42,7 @@ export interface Settings {
|
||||
draftField: DraftField | null | undefined;
|
||||
customSorting: SortingSetting[] | undefined;
|
||||
filters: (FilterType | { title: string; name: string })[] | undefined;
|
||||
grouping: { title: string; name: string }[] | undefined;
|
||||
dashboardState: DashboardState;
|
||||
scripts: CustomScript[];
|
||||
dataFiles: DataFile[] | undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const FilterValuesAtom = atom<{ [filter: string]: string[] }>({
|
||||
export const FilterValuesAtom = atom<{ [filter: string]: string[] | string[][] }>({
|
||||
key: 'FilterValuesAtom',
|
||||
default: {}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { atom } from 'recoil';
|
||||
export const SelectedItemActionAtom = atom<
|
||||
| {
|
||||
path: string;
|
||||
action: 'view' | 'edit' | 'delete';
|
||||
action: 'view' | 'edit' | 'delete' | 'move';
|
||||
}
|
||||
| undefined
|
||||
>({
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const SelectedStructureFolderAtom = atom<string | null>({
|
||||
key: 'SelectedStructureFolderAtom',
|
||||
default: null
|
||||
});
|
||||
@@ -22,6 +22,7 @@ export * from './SearchAtom';
|
||||
export * from './SearchReadyAtom';
|
||||
export * from './SelectedItemActionAtom';
|
||||
export * from './SelectedMediaFolderAtom';
|
||||
export * from './SelectedStructureFolderAtom';
|
||||
export * from './SettingsAtom';
|
||||
export * from './SortingAtom';
|
||||
export * from './TabAtom';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { selector } from 'recoil';
|
||||
import { SelectedStructureFolderAtom } from '..';
|
||||
|
||||
export const SelectedStructureFolderSelector = selector({
|
||||
key: 'SelectedStructureFolderSelector',
|
||||
get: ({ get }) => {
|
||||
return get(SelectedStructureFolderAtom);
|
||||
}
|
||||
});
|
||||
@@ -8,6 +8,7 @@ export * from './MediaTotalSelector';
|
||||
export * from './PageSelector';
|
||||
export * from './SearchSelector';
|
||||
export * from './SelectedMediaFolderSelector';
|
||||
export * from './SelectedStructureFolderSelector';
|
||||
export * from './SettingsSelector';
|
||||
export * from './SortingSelector';
|
||||
export * from './TabSelector';
|
||||
|
||||
@@ -98,4 +98,17 @@ export const updateCssVariables = (isDarkTheme = true) => {
|
||||
'--frontmatter-border-active',
|
||||
darkenColor(borderColor, isDarkTheme ? -30 : 30) || 'var(--vscode-activityBar-activeBorder)'
|
||||
);
|
||||
|
||||
// SEO - Success/Warning colors
|
||||
const successColor = styles.getPropertyValue('--vscode-charts-green');
|
||||
document.documentElement.style.setProperty(
|
||||
'--frontmatter-success-background',
|
||||
opacityColor(successColor, 0.05) || 'var(--vscode-charts-green)'
|
||||
);
|
||||
|
||||
const warningColor = styles.getPropertyValue('--vscode-statusBarItem-warningBackground');
|
||||
document.documentElement.style.setProperty(
|
||||
'--frontmatter-warning-background',
|
||||
opacityColor(warningColor, 0.05) || 'var(--vscode-statusBarItem-warningBackground)'
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
Article,
|
||||
Settings,
|
||||
StatusListener,
|
||||
Chatbot,
|
||||
Taxonomy
|
||||
} from './commands';
|
||||
import { join } from 'path';
|
||||
@@ -49,7 +48,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const extension = Extension.getInstance(context);
|
||||
|
||||
Logger.info(`Activating ${EXTENSION_NAME} version ${Extension.getInstance().version}...`);
|
||||
Logger.info(`Logging level: ${Logger.getLevel()}`);
|
||||
|
||||
// Set development context
|
||||
if (!Extension.getInstance().isProductionMode) {
|
||||
@@ -195,17 +193,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.docs, () => {
|
||||
vscode.commands.executeCommand(
|
||||
`simpleBrowser.show`,
|
||||
`workbench.action.browser.open`,
|
||||
`https://${extension.isBetaVersion() ? `beta.` : ``}frontmatter.codes/docs`
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// Chat to the bot
|
||||
subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMAND_NAME.chatbot, () => Chatbot.open(extensionPath))
|
||||
);
|
||||
|
||||
// Create the editor experience for bulk scripts
|
||||
subscriptions.push(
|
||||
vscode.workspace.registerTextDocumentContentProvider(
|
||||
@@ -246,13 +239,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
export function deactivate() {}
|
||||
|
||||
const triggerPageUpdate = (location: string) => {
|
||||
const triggerPageUpdate = async (location: string) => {
|
||||
Logger.verbose(`Trigger page update: ${location}`);
|
||||
pageUpdateDebouncer(() => {
|
||||
StatusListener.verify(collection);
|
||||
}, 1000);
|
||||
|
||||
if (location === 'onDidChangeActiveTextEditor') {
|
||||
await PanelProvider.openOnSupportedFile();
|
||||
PanelProvider.getInstance()?.updateCurrentFile();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,12 +149,17 @@ export class ArticleHelper {
|
||||
* @returns A promise that resolves to the contents of the file, or undefined if the file does not exist.
|
||||
*/
|
||||
public static async getContents(filePath: string): Promise<string | undefined> {
|
||||
const file = await workspace.fs.readFile(Uri.file(parseWinPath(filePath)));
|
||||
if (!file) {
|
||||
try {
|
||||
const file = await workspace.fs.readFile(Uri.file(parseWinPath(filePath)));
|
||||
if (!file) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(file);
|
||||
} catch (error) {
|
||||
Logger.error(`ArticleHelper.getContents: Failed to read file ${filePath}: ${error}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(file);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -572,7 +577,7 @@ export class ArticleHelper {
|
||||
await mkdirAsync(newFolder, { recursive: true });
|
||||
newFilePath = join(
|
||||
newFolder,
|
||||
`${sanitize(contentType.defaultFileName ?? `index`)}.${
|
||||
`${sanitize(contentType.defaultFileName || `index`, { isFileName: true })}.${
|
||||
fileExtension || contentType.fileType || fileType
|
||||
}`
|
||||
);
|
||||
@@ -684,7 +689,7 @@ export class ArticleHelper {
|
||||
}
|
||||
|
||||
if (fieldName === 'slug' && (fieldValue === null || fieldValue === '')) {
|
||||
fmData[fieldName] = SlugHelper.createSlug(title, fmData, slugTemplate);
|
||||
fmData[fieldName] = SlugHelper.createSlug(title, fmData, filePath, slugTemplate);
|
||||
}
|
||||
|
||||
fmData[fieldName] = await processArticlePlaceholdersFromPath(fmData[fieldName], filePath);
|
||||
@@ -807,7 +812,8 @@ export class ArticleHelper {
|
||||
const elms: Parent[] | Link[] = this.getAllElms(mdTree);
|
||||
|
||||
const headings = elms.filter((node) => node.type === 'heading');
|
||||
const paragraphs = elms.filter((node) => node.type === 'paragraph').length;
|
||||
const paragraphNodes = elms.filter((node) => node.type === 'paragraph');
|
||||
const paragraphs = paragraphNodes.length;
|
||||
const images = elms.filter((node) => node.type === 'image').length;
|
||||
const links: string[] = elms
|
||||
.filter((node) => node.type === 'link')
|
||||
@@ -836,6 +842,21 @@ export class ArticleHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract first paragraph text for SEO keyword checking
|
||||
let firstParagraph = '';
|
||||
if (paragraphNodes.length > 0) {
|
||||
const firstParagraphNode = paragraphNodes[0];
|
||||
const extractTextFromNode = (node: any): string => {
|
||||
if (node.type === 'text') {
|
||||
return node.value || '';
|
||||
} else if (node.children && Array.isArray(node.children)) {
|
||||
return node.children.map(extractTextFromNode).join('');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
firstParagraph = extractTextFromNode(firstParagraphNode);
|
||||
}
|
||||
|
||||
const wordCount = this.wordCount(0, mdTree);
|
||||
|
||||
return {
|
||||
@@ -846,7 +867,8 @@ export class ArticleHelper {
|
||||
internalLinks,
|
||||
externalLinks: externalLinks.length,
|
||||
wordCount,
|
||||
content: article.content
|
||||
content: article.content,
|
||||
firstParagraph
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import { Folders } from '../commands/Folders';
|
||||
import { Questions } from './Questions';
|
||||
import { Notifications } from './Notifications';
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from '../constants/ContentType';
|
||||
import { Telemetry } from './Telemetry';
|
||||
import { basename } from 'path';
|
||||
import { ParsedFrontMatter } from '../parsers';
|
||||
import { encodeEmoji, existsAsync, fieldWhenClause, getTitleField, writeFileAsync } from '../utils';
|
||||
@@ -408,7 +407,7 @@ export class ContentType {
|
||||
* @param parents
|
||||
* @returns
|
||||
*/
|
||||
public static getFieldValue(data: any, parents: string[]): string | string[] {
|
||||
public static getFieldValue(data: any, parents: string[]): any {
|
||||
let fieldValue = [];
|
||||
let crntPageData = data;
|
||||
|
||||
@@ -575,7 +574,8 @@ export class ContentType {
|
||||
fieldValue === null ||
|
||||
fieldValue === undefined ||
|
||||
fieldValue === '' ||
|
||||
fieldValue.length === 0 ||
|
||||
(Array.isArray(fieldValue) && fieldValue.length === 0) ||
|
||||
(typeof fieldValue === 'string' && fieldValue.length === 0) ||
|
||||
fieldValue === DefaultFieldValues.faultyCustomPlaceholder
|
||||
) {
|
||||
emptyFields.push(fields);
|
||||
@@ -956,8 +956,25 @@ export class ContentType {
|
||||
let templatePath = contentType.template;
|
||||
let templateData: ParsedFrontMatter | null | undefined = null;
|
||||
if (templatePath) {
|
||||
templatePath = Folders.getAbsFilePath(templatePath);
|
||||
templateData = await ArticleHelper.getFrontMatterByPath(templatePath);
|
||||
try {
|
||||
templatePath = Folders.getAbsFilePath(templatePath);
|
||||
templateData = await ArticleHelper.getFrontMatterByPath(templatePath);
|
||||
if (!templateData) {
|
||||
Logger.warning(
|
||||
`ContentType.create: Template file not found or could not be parsed: ${templatePath}`
|
||||
);
|
||||
Notifications.warning(
|
||||
l10n.t(LocalizationKey.commonError) + ` Template not found: ${templatePath}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
`ContentType.create: Error loading template from ${templatePath}: ${error}`
|
||||
);
|
||||
Notifications.error(
|
||||
l10n.t(LocalizationKey.commonError) + ` Template loading failed: ${templatePath}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newFilePath: string | undefined = await ArticleHelper.createContent(
|
||||
@@ -1063,7 +1080,8 @@ export class ContentType {
|
||||
data[field.name] = processArticlePlaceholdersFromData(
|
||||
field.default as string,
|
||||
data,
|
||||
contentType
|
||||
contentType,
|
||||
filePath
|
||||
);
|
||||
data[field.name] = processTimePlaceholders(
|
||||
data[field.name],
|
||||
|
||||
370
src/helpers/ContentTypeSchemaGenerator.ts
Normal file
370
src/helpers/ContentTypeSchemaGenerator.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import { ContentType, Field, FieldType, CustomTaxonomy } from '../models';
|
||||
import { Settings } from '../helpers/SettingsHelper';
|
||||
import { SETTING_TAXONOMY_FIELD_GROUPS, SETTING_TAXONOMY_CUSTOM } from '../constants';
|
||||
import { TaxonomyHelper } from './TaxonomyHelper';
|
||||
import { TaxonomyType } from '../models/TaxonomyType';
|
||||
|
||||
/**
|
||||
* JSON Schema type definition
|
||||
*/
|
||||
export interface JSONSchema {
|
||||
$schema?: string;
|
||||
type?: string | string[];
|
||||
properties?: { [key: string]: JSONSchema };
|
||||
required?: string[];
|
||||
items?: JSONSchema;
|
||||
enum?: any[];
|
||||
format?: string;
|
||||
anyOf?: JSONSchema[];
|
||||
oneOf?: JSONSchema[];
|
||||
allOf?: JSONSchema[];
|
||||
description?: string;
|
||||
default?: any;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JSON Schema from Front Matter Content Type definitions
|
||||
*
|
||||
* This utility converts Front Matter content type definitions into JSON Schema format
|
||||
* which can then be used for validation. It handles all field types supported by
|
||||
* Front Matter CMS including nested fields, blocks, and field groups.
|
||||
*
|
||||
* Field Type Mappings:
|
||||
* - string, slug, image, file, customField → string
|
||||
* - number → number (with optional min/max)
|
||||
* - boolean, draft → boolean
|
||||
* - datetime → string with date-time format
|
||||
* - choice → string with enum (or array if multiple)
|
||||
* - tags, categories, taxonomy, list → array of strings
|
||||
* - fields → nested object with properties
|
||||
* - block → array of objects with oneOf for field groups
|
||||
* - json → any valid JSON type
|
||||
* - dataFile, contentRelationship → string or array
|
||||
*
|
||||
* Features:
|
||||
* - Required field validation
|
||||
* - Type validation
|
||||
* - Enum/choice validation
|
||||
* - Number range validation (min/max)
|
||||
* - Nested object support
|
||||
* - Block field support with multiple field group options
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* const schema = ContentTypeSchemaGenerator.generateSchema(contentType);
|
||||
* // Use schema for validation with AJV or other JSON Schema validators
|
||||
* ```
|
||||
*/
|
||||
export class ContentTypeSchemaGenerator {
|
||||
/**
|
||||
* Generate JSON Schema from a content type
|
||||
* @param contentType The content type to generate schema from
|
||||
* @returns JSON Schema object
|
||||
*/
|
||||
public static async generateSchema(contentType: ContentType): Promise<JSONSchema> {
|
||||
const schema: JSONSchema = {
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
|
||||
if (!contentType.fields || contentType.fields.length === 0) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
// Process each field in the content type
|
||||
for (const field of contentType.fields) {
|
||||
const fieldSchema = await this.generateFieldSchema(field);
|
||||
if (fieldSchema && schema.properties) {
|
||||
schema.properties[field.name] = fieldSchema;
|
||||
|
||||
// Add to required array if field is required
|
||||
if (field.required && schema.required) {
|
||||
schema.required.push(field.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove required array if empty
|
||||
if (schema.required && schema.required.length === 0) {
|
||||
delete schema.required;
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JSON Schema for a single field
|
||||
* @param field The field to generate schema from
|
||||
* @returns JSON Schema object for the field
|
||||
*/
|
||||
private static async generateFieldSchema(field: Field): Promise<JSONSchema | null> {
|
||||
// Skip divider and heading fields as they are UI-only
|
||||
if (field.type === 'divider' || field.type === 'heading') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schema: JSONSchema = {};
|
||||
|
||||
// Add description if available
|
||||
if (field.description) {
|
||||
schema.description = field.description;
|
||||
}
|
||||
|
||||
// Add default value if specified
|
||||
if (field.default !== undefined && field.default !== null) {
|
||||
schema.default = field.default;
|
||||
}
|
||||
|
||||
// Map field type to JSON Schema type
|
||||
switch (field.type) {
|
||||
case 'string':
|
||||
case 'slug':
|
||||
case 'image':
|
||||
case 'file':
|
||||
case 'customField':
|
||||
schema.type = 'string';
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
schema.type = 'number';
|
||||
if (field.numberOptions) {
|
||||
if (field.numberOptions.min !== undefined) {
|
||||
schema.minimum = field.numberOptions.min;
|
||||
}
|
||||
if (field.numberOptions.max !== undefined) {
|
||||
schema.maximum = field.numberOptions.max;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
case 'draft':
|
||||
schema.type = 'boolean';
|
||||
break;
|
||||
|
||||
case 'datetime':
|
||||
schema.type = 'string';
|
||||
schema.format = 'date-time';
|
||||
break;
|
||||
|
||||
case 'choice':
|
||||
if (field.multiple) {
|
||||
schema.type = 'array';
|
||||
schema.items = {
|
||||
type: 'string'
|
||||
};
|
||||
if (field.choices && field.choices.length > 0) {
|
||||
schema.items.enum = this.extractChoiceValues(field.choices);
|
||||
}
|
||||
} else {
|
||||
schema.type = 'string';
|
||||
if (field.choices && field.choices.length > 0) {
|
||||
schema.enum = this.extractChoiceValues(field.choices);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tags': {
|
||||
schema.type = 'array';
|
||||
schema.items = {
|
||||
type: 'string'
|
||||
};
|
||||
|
||||
// Get available tags and add as enum for validation
|
||||
const availableTags = await TaxonomyHelper.get(TaxonomyType.Tag);
|
||||
if (availableTags && availableTags.length > 0) {
|
||||
schema.items.enum = availableTags;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'categories': {
|
||||
schema.type = 'array';
|
||||
schema.items = {
|
||||
type: 'string'
|
||||
};
|
||||
|
||||
// Get available categories and add as enum for validation
|
||||
const availableCategories = await TaxonomyHelper.get(TaxonomyType.Category);
|
||||
if (availableCategories && availableCategories.length > 0) {
|
||||
schema.items.enum = availableCategories;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'taxonomy': {
|
||||
schema.type = 'array';
|
||||
schema.items = {
|
||||
type: 'string'
|
||||
};
|
||||
|
||||
// Get custom taxonomy options if taxonomyId is specified
|
||||
if (field.taxonomyId) {
|
||||
const customTaxonomies = Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM);
|
||||
if (customTaxonomies && customTaxonomies.length > 0) {
|
||||
const taxonomy = customTaxonomies.find((t) => t.id === field.taxonomyId);
|
||||
if (taxonomy && taxonomy.options && taxonomy.options.length > 0) {
|
||||
schema.items.enum = taxonomy.options;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'list':
|
||||
schema.type = 'array';
|
||||
schema.items = {
|
||||
type: 'string'
|
||||
};
|
||||
break;
|
||||
|
||||
case 'fields':
|
||||
schema.type = 'object';
|
||||
schema.properties = {};
|
||||
schema.required = [];
|
||||
|
||||
if (field.fields && field.fields.length > 0) {
|
||||
for (const subField of field.fields) {
|
||||
const subFieldSchema = await this.generateFieldSchema(subField);
|
||||
if (subFieldSchema && schema.properties) {
|
||||
schema.properties[subField.name] = subFieldSchema;
|
||||
|
||||
if (subField.required && schema.required) {
|
||||
schema.required.push(subField.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove required array if empty
|
||||
if (schema.required && schema.required.length === 0) {
|
||||
delete schema.required;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'block': {
|
||||
// Block fields can contain different field groups
|
||||
schema.type = 'array';
|
||||
schema.items = {
|
||||
type: 'object'
|
||||
};
|
||||
|
||||
// Try to get the field group schemas
|
||||
const blockSchemas = await this.getBlockFieldGroupSchemas(field);
|
||||
if (blockSchemas.length > 0) {
|
||||
schema.items = {
|
||||
oneOf: blockSchemas
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'json':
|
||||
// JSON fields can be any valid JSON
|
||||
schema.type = ['object', 'array', 'string', 'number', 'boolean', 'null'];
|
||||
break;
|
||||
|
||||
case 'dataFile':
|
||||
// Data file references are typically strings (IDs or keys)
|
||||
schema.type = 'string';
|
||||
break;
|
||||
|
||||
case 'contentRelationship':
|
||||
// Content relationships can be a string (slug/path) or array of strings
|
||||
if (field.multiple) {
|
||||
schema.type = 'array';
|
||||
schema.items = {
|
||||
type: 'string'
|
||||
};
|
||||
} else {
|
||||
schema.type = 'string';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fieldCollection':
|
||||
// Field collections reference field groups, handle similarly to blocks
|
||||
schema.type = 'array';
|
||||
schema.items = {
|
||||
type: 'object'
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown field type, default to string
|
||||
schema.type = 'string';
|
||||
break;
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract choice values from field choices
|
||||
* @param choices Array of choice strings or objects
|
||||
* @returns Array of choice values
|
||||
*/
|
||||
private static extractChoiceValues(choices: (string | { id?: string | null; title: string })[]): string[] {
|
||||
return choices.map((choice) => {
|
||||
if (typeof choice === 'string') {
|
||||
return choice;
|
||||
} else {
|
||||
return choice.id || choice.title;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schemas for block field groups
|
||||
* @param field The block field
|
||||
* @returns Array of JSON Schemas for each field group
|
||||
*/
|
||||
private static async getBlockFieldGroupSchemas(field: Field): Promise<JSONSchema[]> {
|
||||
const schemas: JSONSchema[] = [];
|
||||
|
||||
if (!field.fieldGroup) {
|
||||
return schemas;
|
||||
}
|
||||
|
||||
const fieldGroupIds = Array.isArray(field.fieldGroup) ? field.fieldGroup : [field.fieldGroup];
|
||||
const fieldGroups = Settings.get(SETTING_TAXONOMY_FIELD_GROUPS) as { id: string; fields: Field[] }[] | undefined;
|
||||
|
||||
if (!fieldGroups || fieldGroups.length === 0) {
|
||||
return schemas;
|
||||
}
|
||||
|
||||
for (const groupId of fieldGroupIds) {
|
||||
const fieldGroup = fieldGroups.find((fg) => fg.id === groupId);
|
||||
if (fieldGroup && fieldGroup.fields) {
|
||||
const groupSchema: JSONSchema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
|
||||
for (const groupField of fieldGroup.fields) {
|
||||
const fieldSchema = await this.generateFieldSchema(groupField);
|
||||
if (fieldSchema && groupSchema.properties) {
|
||||
groupSchema.properties[groupField.name] = fieldSchema;
|
||||
|
||||
if (groupField.required && groupSchema.required) {
|
||||
groupSchema.required.push(groupField.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove required array if empty
|
||||
if (groupSchema.required && groupSchema.required.length === 0) {
|
||||
delete groupSchema.required;
|
||||
}
|
||||
|
||||
schemas.push(groupSchema);
|
||||
}
|
||||
}
|
||||
|
||||
return schemas;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import { ParsedFrontMatter } from '../parsers';
|
||||
import { SETTING_CUSTOM_SCRIPTS } from '../constants';
|
||||
import { evaluateCommand, existsAsync, getPlatform } from '../utils';
|
||||
import { LocalizationKey, localize } from '../localization';
|
||||
import { ScriptAction } from '../models';
|
||||
import { Copilot } from '../services/Copilot';
|
||||
|
||||
export class CustomScript {
|
||||
/**
|
||||
@@ -267,12 +269,7 @@ export class CustomScript {
|
||||
try {
|
||||
const data: {
|
||||
frontmatter?: { [key: string]: any };
|
||||
fmAction?:
|
||||
| 'open'
|
||||
| 'copyMediaMetadata'
|
||||
| 'copyMediaMetadataAndDelete'
|
||||
| 'deleteMedia'
|
||||
| 'fieldAction';
|
||||
fmAction?: ScriptAction;
|
||||
fmPath?: string;
|
||||
fmSourcePath?: string;
|
||||
fmDestinationPath?: string;
|
||||
@@ -425,14 +422,31 @@ export class CustomScript {
|
||||
const fullScript = `${command} "${scriptPath}" ${args}`;
|
||||
Logger.info(localize(LocalizationKey.helpersCustomScriptExecuting, fullScript));
|
||||
|
||||
const output = await CustomScript.processExecution(fullScript, wsPath);
|
||||
return output;
|
||||
}
|
||||
|
||||
// Recursive function to process the execution of the script
|
||||
private static async processExecution(fullScript: string, wsPath: string): Promise<string> {
|
||||
const output: string = await CustomScript.executeScriptAsync(fullScript, wsPath);
|
||||
|
||||
try {
|
||||
const data = JSON.parse(output);
|
||||
if (data.questions) {
|
||||
const { questions, fmAction, fmPrompt } = data as {
|
||||
questions?: {
|
||||
name: string;
|
||||
message: string;
|
||||
default?: string;
|
||||
options?: string[];
|
||||
}[];
|
||||
fmAction?: ScriptAction;
|
||||
fmPrompt?: any; // When the 'promptCopilot' action is used
|
||||
};
|
||||
|
||||
if (questions) {
|
||||
const answers: string[] = [];
|
||||
|
||||
for (const question of data.questions) {
|
||||
for (const question of questions) {
|
||||
if (question.name && question.message) {
|
||||
let answer;
|
||||
if (question.options) {
|
||||
@@ -460,7 +474,16 @@ export class CustomScript {
|
||||
|
||||
if (answers.length > 0) {
|
||||
const newScript = `${fullScript} ${answers.join(' ')}`;
|
||||
return await CustomScript.executeScriptAsync(newScript, wsPath);
|
||||
return await CustomScript.processExecution(newScript, wsPath);
|
||||
}
|
||||
} else if (fmAction) {
|
||||
if (fmAction === 'promptCopilot' && fmPrompt) {
|
||||
const response = await Copilot.promptCopilot(fmPrompt);
|
||||
if (response) {
|
||||
const promptResponse = `promptResponse="${response}"`;
|
||||
const newScript = `${fullScript} ${promptResponse}`;
|
||||
return await CustomScript.processExecution(newScript, wsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ExtensionState,
|
||||
SETTING_CONTENT_DRAFT_FIELD,
|
||||
SETTING_CONTENT_FILTERS,
|
||||
SETTING_CONTENT_GROUPING,
|
||||
SETTING_CONTENT_SORTING,
|
||||
SETTING_CONTENT_SORTING_DEFAULT,
|
||||
SETTING_DASHBOARD_OPENONSTART,
|
||||
@@ -30,7 +31,8 @@ import {
|
||||
SETTING_DASHBOARD_CONTENT_CARD_STATE,
|
||||
SETTING_DASHBOARD_CONTENT_CARD_DESCRIPTION,
|
||||
SETTING_WEBSITE_URL,
|
||||
SETTING_MEDIA_CONTENTTYPES
|
||||
SETTING_MEDIA_CONTENTTYPES,
|
||||
SETTING_PANEL_OPEN_ON_SUPPORTED_FILE
|
||||
} from '../constants';
|
||||
import {
|
||||
DashboardViewType,
|
||||
@@ -107,6 +109,7 @@ export class DashboardSettings {
|
||||
categories: (await TaxonomyHelper.get(TaxonomyType.Category)) || [],
|
||||
customTaxonomy: Settings.get(SETTING_TAXONOMY_CUSTOM, true) || [],
|
||||
openOnStart: Settings.get(SETTING_DASHBOARD_OPENONSTART),
|
||||
openPanelForSupportedFiles: Settings.get(SETTING_PANEL_OPEN_ON_SUPPORTED_FILE),
|
||||
versionInfo: ext.getVersion(),
|
||||
pageViewType: await ext.getState<DashboardViewType | undefined>(
|
||||
ExtensionState.PagesView,
|
||||
@@ -118,6 +121,7 @@ export class DashboardSettings {
|
||||
contentFolders: await Folders.get(),
|
||||
filters:
|
||||
Settings.get<(FilterType | { title: string; name: string })[]>(SETTING_CONTENT_FILTERS),
|
||||
grouping: Settings.get<{ title: string; name: string }[]>(SETTING_CONTENT_GROUPING),
|
||||
crntFramework: Settings.get<string>(SETTING_FRAMEWORK_ID),
|
||||
framework: !isInitialized && wsFolder ? await FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
scripts: Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || [],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parse, parseISO, parseJSON, format } from 'date-fns';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
export class DateHelper {
|
||||
public static formatUpdate(value: string | null | undefined): string | null {
|
||||
@@ -19,6 +20,20 @@ export class DateHelper {
|
||||
return format(date, DateHelper.formatUpdate(dateFormat) as string);
|
||||
}
|
||||
|
||||
public static formatInTimezone(
|
||||
date?: Date,
|
||||
dateFormat?: string,
|
||||
timezone?: string
|
||||
): string | null {
|
||||
if (!date || !dateFormat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return timezone
|
||||
? formatInTimeZone(date, timezone, DateHelper.formatUpdate(dateFormat) as string)
|
||||
: format(date, DateHelper.formatUpdate(dateFormat) as string);
|
||||
}
|
||||
|
||||
public static tryParse(date: any, format?: string): Date | null {
|
||||
if (!date) {
|
||||
return null;
|
||||
|
||||
@@ -137,7 +137,7 @@ export class FrameworkDetector {
|
||||
const assetDir = dirname(absAssetPath);
|
||||
const fileName = parse(absAssetPath);
|
||||
|
||||
relAssetPath = relative(fileDir, assetDir);
|
||||
relAssetPath = parseWinPath(relative(fileDir, assetDir));
|
||||
relAssetPath = join(relAssetPath, `${fileName.name}${fileName.ext}`);
|
||||
}
|
||||
// Support for HEXO image folder
|
||||
@@ -197,7 +197,7 @@ export class FrameworkDetector {
|
||||
const assetDir = dirname(absAssetPath);
|
||||
const fileName = parse(absAssetPath);
|
||||
|
||||
let relAssetPath = relative(fileDir, assetDir);
|
||||
let relAssetPath = parseWinPath(relative(fileDir, assetDir));
|
||||
relAssetPath = join(relAssetPath, `${fileName.name}${fileName.ext}`);
|
||||
return parseWinPath(relAssetPath);
|
||||
}
|
||||
|
||||
216
src/helpers/FrontMatterValidator.ts
Normal file
216
src/helpers/FrontMatterValidator.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import Ajv, { ErrorObject } from 'ajv';
|
||||
import { ContentType } from '../models';
|
||||
import { ContentTypeSchemaGenerator, JSONSchema } from './ContentTypeSchemaGenerator';
|
||||
|
||||
/**
|
||||
* Validation error with location information
|
||||
*/
|
||||
export interface ValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
keyword?: string;
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates front matter data against content type schemas
|
||||
*
|
||||
* This validator uses JSON Schema validation (via AJV) to ensure that front matter
|
||||
* in markdown files conforms to the structure defined in content types.
|
||||
*
|
||||
* Features:
|
||||
* - Automatic schema generation from content type definitions
|
||||
* - Type validation (string, number, boolean, datetime, arrays, etc.)
|
||||
* - Required field validation
|
||||
* - Enum/choice validation
|
||||
* - Number range validation (min/max)
|
||||
* - Nested object validation
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* const validator = new FrontMatterValidator();
|
||||
* const errors = validator.validate(frontMatterData, contentType);
|
||||
* if (errors.length > 0) {
|
||||
* // Handle validation errors
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class FrontMatterValidator {
|
||||
private ajv: Ajv;
|
||||
private schemaCache: Map<string, JSONSchema>;
|
||||
|
||||
constructor() {
|
||||
this.ajv = new Ajv({
|
||||
allErrors: true,
|
||||
verbose: true,
|
||||
strict: false,
|
||||
allowUnionTypes: true
|
||||
});
|
||||
this.schemaCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate front matter data against a content type
|
||||
* @param data The front matter data to validate
|
||||
* @param contentType The content type to validate against
|
||||
* @returns Array of validation errors (empty if valid)
|
||||
*/
|
||||
public async validate(data: any, contentType: ContentType): Promise<ValidationError[]> {
|
||||
if (!contentType || !contentType.fields || contentType.fields.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get or generate schema
|
||||
const schema = await this.getSchema(contentType);
|
||||
if (!schema) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Compile and validate
|
||||
const validate = this.ajv.compile(schema);
|
||||
const valid = validate(data);
|
||||
|
||||
if (valid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert AJV errors to our format
|
||||
return this.convertAjvErrors(validate.errors || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or generate schema for a content type
|
||||
* @param contentType The content type
|
||||
* @returns JSON Schema
|
||||
*/
|
||||
private async getSchema(contentType: ContentType): Promise<JSONSchema | null> {
|
||||
// Check cache first
|
||||
const cacheKey = contentType.name;
|
||||
if (this.schemaCache.has(cacheKey)) {
|
||||
return this.schemaCache.get(cacheKey) || null;
|
||||
}
|
||||
|
||||
// Generate new schema
|
||||
const schema = await ContentTypeSchemaGenerator.generateSchema(contentType);
|
||||
this.schemaCache.set(cacheKey, schema);
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the schema cache
|
||||
*/
|
||||
public clearCache(): void {
|
||||
this.schemaCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert AJV errors to validation errors
|
||||
* @param ajvErrors AJV error objects
|
||||
* @returns Array of validation errors
|
||||
*/
|
||||
private convertAjvErrors(ajvErrors: ErrorObject[]): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
for (const error of ajvErrors) {
|
||||
const field = this.extractFieldName(error.instancePath);
|
||||
const message = this.formatErrorMessage(error, field);
|
||||
|
||||
errors.push({
|
||||
field,
|
||||
message,
|
||||
keyword: error.keyword,
|
||||
params: error.params
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract field name from instance path
|
||||
* @param instancePath The JSON pointer path
|
||||
* @returns Field name
|
||||
*/
|
||||
private extractFieldName(instancePath: string): string {
|
||||
if (!instancePath || instancePath === '') {
|
||||
return 'root';
|
||||
}
|
||||
|
||||
// Remove leading slash and convert to dot notation
|
||||
return instancePath
|
||||
.replace(/^\//, '')
|
||||
.replace(/\//g, '.')
|
||||
.replace(/~1/g, '/')
|
||||
.replace(/~0/g, '~');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format error message for display
|
||||
* @param error AJV error object
|
||||
* @param field Field name
|
||||
* @returns Formatted error message
|
||||
*/
|
||||
private formatErrorMessage(error: ErrorObject, field: string): string {
|
||||
const displayField = field === 'root' ? 'The document' : `Field '${field}'`;
|
||||
|
||||
switch (error.keyword) {
|
||||
case 'required': {
|
||||
const missingProperty = error.params?.missingProperty;
|
||||
return `Missing required field '${missingProperty}'`;
|
||||
}
|
||||
|
||||
case 'type': {
|
||||
const expectedType = error.params?.type;
|
||||
return `${displayField} must be of type ${expectedType}`;
|
||||
}
|
||||
|
||||
case 'enum': {
|
||||
const allowedValues = error.params?.allowedValues;
|
||||
if (allowedValues && Array.isArray(allowedValues)) {
|
||||
return `${displayField} must be one of: ${allowedValues.join(', ')}`;
|
||||
}
|
||||
return `${displayField} has an invalid value`;
|
||||
}
|
||||
|
||||
case 'format': {
|
||||
const format = error.params?.format;
|
||||
return `${displayField} must be in ${format} format`;
|
||||
}
|
||||
|
||||
case 'minimum': {
|
||||
const minimum = error.params?.limit;
|
||||
return `${displayField} must be greater than or equal to ${minimum}`;
|
||||
}
|
||||
|
||||
case 'maximum': {
|
||||
const maximum = error.params?.limit;
|
||||
return `${displayField} must be less than or equal to ${maximum}`;
|
||||
}
|
||||
|
||||
case 'minItems': {
|
||||
const minItems = error.params?.limit;
|
||||
return `${displayField} must have at least ${minItems} items`;
|
||||
}
|
||||
|
||||
case 'maxItems': {
|
||||
const maxItems = error.params?.limit;
|
||||
return `${displayField} must have at most ${maxItems} items`;
|
||||
}
|
||||
|
||||
case 'additionalProperties': {
|
||||
const additionalProperty = error.params?.additionalProperty;
|
||||
return `Unexpected field '${additionalProperty}' is not allowed`;
|
||||
}
|
||||
|
||||
case 'oneOf':
|
||||
return `${displayField} must match exactly one of the allowed schemas`;
|
||||
|
||||
case 'anyOf':
|
||||
return `${displayField} must match at least one of the allowed schemas`;
|
||||
|
||||
default:
|
||||
return error.message || `${displayField} is invalid`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export class Logger {
|
||||
|
||||
private constructor() {
|
||||
const displayName = Extension.getInstance().displayName;
|
||||
Logger.channel = window.createOutputChannel(displayName);
|
||||
Logger.channel = window.createOutputChannel(displayName, 'frontmatter.project.output');
|
||||
commands.registerCommand(COMMAND_NAME.showOutputChannel, () => {
|
||||
Logger.channel?.show();
|
||||
});
|
||||
|
||||
@@ -172,7 +172,14 @@ export class MediaHelpers {
|
||||
}
|
||||
})
|
||||
);
|
||||
files = files.filter((f) => f.mtime !== undefined);
|
||||
files = files
|
||||
.filter((f) => f.mtime !== undefined || f.mtimeMs !== undefined)
|
||||
.map((f) => {
|
||||
if (f.mtime === undefined && f.mtimeMs !== undefined) {
|
||||
return { ...f, mtime: new Date(f.mtimeMs as number) };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
|
||||
// Sort the files
|
||||
if (crntSort?.type === SortType.string) {
|
||||
@@ -207,7 +214,7 @@ export class MediaHelpers {
|
||||
if (selectedFolder) {
|
||||
if (await existsAsync(selectedFolder)) {
|
||||
foldersFromSelection = (await readdirAsync(selectedFolder, { withFileTypes: true }))
|
||||
.filter((dir) => dir.isDirectory())
|
||||
.filter((dir) => dir.isDirectory() && !dir.name.startsWith('.'))
|
||||
.map((dir) => parseWinPath(join(selectedFolder, dir.name)));
|
||||
}
|
||||
}
|
||||
@@ -218,7 +225,7 @@ export class MediaHelpers {
|
||||
const contentPath = contentFolder.path;
|
||||
if (contentPath && (await existsAsync(contentPath))) {
|
||||
const subFolders = (await readdirAsync(contentPath, { withFileTypes: true }))
|
||||
.filter((dir) => dir.isDirectory())
|
||||
.filter((dir) => dir.isDirectory() && !dir.name.startsWith('.'))
|
||||
.map((dir) => parseWinPath(join(contentPath, dir.name)));
|
||||
allContentFolders = [...allContentFolders, ...subFolders];
|
||||
}
|
||||
@@ -236,7 +243,7 @@ export class MediaHelpers {
|
||||
|
||||
if (staticPath && (await existsAsync(staticPath))) {
|
||||
allFolders = (await readdirAsync(staticPath, { withFileTypes: true }))
|
||||
.filter((dir) => dir.isDirectory())
|
||||
.filter((dir) => dir.isDirectory() && !dir.name.startsWith('.'))
|
||||
.map((dir) => parseWinPath(join(staticPath, dir.name)));
|
||||
}
|
||||
|
||||
@@ -422,7 +429,7 @@ export class MediaHelpers {
|
||||
|
||||
// If the image exists in a content folder, the relative path needs to be used
|
||||
if (existsInContent) {
|
||||
const relImgPath = relative(fileDir, imgDir);
|
||||
const relImgPath = parseWinPath(relative(fileDir, imgDir));
|
||||
|
||||
relPath = join(relImgPath, basename(relPath));
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export class MediaLibrary {
|
||||
|
||||
public async get(id: string): Promise<MediaRecord | undefined> {
|
||||
try {
|
||||
const fileId = this.parsePath(id);
|
||||
const fileId = MediaLibrary.parsePath(id);
|
||||
if (await this.db?.exists(fileId)) {
|
||||
return await this.db?.getData(fileId);
|
||||
}
|
||||
@@ -132,14 +132,23 @@ export class MediaLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllByPath(path: string) {
|
||||
try {
|
||||
const data = await this.db?.getData(path);
|
||||
return data;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set(id: string, metadata: any): void {
|
||||
const fileId = this.parsePath(id);
|
||||
const fileId = MediaLibrary.parsePath(id);
|
||||
this.db?.push(fileId, metadata, true);
|
||||
}
|
||||
|
||||
public async rename(oldId: string, newId: string): Promise<void> {
|
||||
const fileId = this.parsePath(oldId);
|
||||
const newFileId = this.parsePath(newId);
|
||||
const fileId = MediaLibrary.parsePath(oldId);
|
||||
const newFileId = MediaLibrary.parsePath(newId);
|
||||
const data = await this.get(fileId);
|
||||
if (data) {
|
||||
this.db?.delete(fileId);
|
||||
@@ -148,7 +157,7 @@ export class MediaLibrary {
|
||||
}
|
||||
|
||||
public async remove(path: string): Promise<void> {
|
||||
const fileId = this.parsePath(path);
|
||||
const fileId = MediaLibrary.parsePath(path);
|
||||
await this.db?.delete(fileId);
|
||||
}
|
||||
|
||||
@@ -174,9 +183,12 @@ export class MediaLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
public parsePath(path: string) {
|
||||
public static parsePath(path: string) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
let absPath = path.replace(parseWinPath(wsFolder?.fsPath || ''), WORKSPACE_PLACEHOLDER);
|
||||
let absPath = parseWinPath(path).replace(
|
||||
parseWinPath(wsFolder?.fsPath || ''),
|
||||
WORKSPACE_PLACEHOLDER
|
||||
);
|
||||
absPath = isWindows() ? absPath.split('\\').join('/') : absPath;
|
||||
return absPath.toLowerCase();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
SETTING_GLOBAL_TIMEZONE,
|
||||
SETTING_PANEL_ACTIONS_DISABLED,
|
||||
SETTING_SPONSORS_AI_ENABLED,
|
||||
SETTING_WEBSITE_URL
|
||||
} from './../constants/settings';
|
||||
import { workspace } from 'vscode';
|
||||
@@ -51,7 +51,6 @@ export class PanelSettings {
|
||||
|
||||
try {
|
||||
return {
|
||||
aiEnabled: Settings.get<boolean>(SETTING_SPONSORS_AI_ENABLED) || false,
|
||||
copilotEnabled: await Copilot.isInstalled(),
|
||||
git: await GitListener.getSettings(),
|
||||
seo: {
|
||||
@@ -68,7 +67,8 @@ export class PanelSettings {
|
||||
updateFileName: !!Settings.get<boolean>(SETTING_SLUG_UPDATE_FILE_NAME)
|
||||
},
|
||||
date: {
|
||||
format: Settings.get<string>(SETTING_DATE_FORMAT) || ''
|
||||
format: Settings.get<string>(SETTING_DATE_FORMAT) || '',
|
||||
timezone: Settings.get<string>(SETTING_GLOBAL_TIMEZONE) || 'UTC'
|
||||
},
|
||||
tags: (await TaxonomyHelper.get(TaxonomyType.Tag)) || [],
|
||||
categories: (await TaxonomyHelper.get(TaxonomyType.Category)) || [],
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { authentication, QuickPickItem, QuickPickItemKind, window } from 'vscode';
|
||||
import { QuickPickItem, QuickPickItemKind, window } from 'vscode';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { SETTING_SPONSORS_AI_ENABLED } from '../constants';
|
||||
import { ContentType } from './ContentType';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Settings } from './SettingsHelper';
|
||||
import { Logger } from './Logger';
|
||||
import { SponsorAi } from '../services/SponsorAI';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { ContentFolder } from '../models';
|
||||
@@ -40,56 +37,28 @@ export class Questions {
|
||||
* @returns
|
||||
*/
|
||||
public static async ContentTitle(showWarning = true): Promise<string | undefined> {
|
||||
const aiEnabled = Settings.get<boolean>(SETTING_SPONSORS_AI_ENABLED);
|
||||
let title: string | undefined = '';
|
||||
const isCopilotInstalled = await Copilot.isInstalled();
|
||||
|
||||
let aiTitles: string[] | undefined;
|
||||
|
||||
if (aiEnabled || isCopilotInstalled) {
|
||||
if (isCopilotInstalled) {
|
||||
title = await window.showInputBox({
|
||||
title: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputTitle),
|
||||
prompt: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPrompt),
|
||||
placeHolder: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPlaceholder),
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (isCopilotInstalled) {
|
||||
title = await window.showInputBox({
|
||||
title: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputTitle),
|
||||
prompt: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPrompt),
|
||||
placeHolder: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPlaceholder),
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (title) {
|
||||
try {
|
||||
aiTitles = await Copilot.suggestTitles(title);
|
||||
} catch (e) {
|
||||
Logger.error((e as Error).message);
|
||||
Notifications.error(
|
||||
l10n.t(LocalizationKey.helpersQuestionsContentTitleCopilotInputFailed)
|
||||
);
|
||||
title = undefined;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const githubAuth = await authentication.getSession('github', ['read:user'], {
|
||||
silent: true
|
||||
});
|
||||
|
||||
if (githubAuth && githubAuth.account.label) {
|
||||
title = await window.showInputBox({
|
||||
title: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputTitle),
|
||||
prompt: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPrompt),
|
||||
placeHolder: l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputPlaceholder),
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (title) {
|
||||
try {
|
||||
aiTitles = await SponsorAi.getTitles(githubAuth.accessToken, title);
|
||||
} catch (e) {
|
||||
Logger.error((e as Error).message);
|
||||
Notifications.error(
|
||||
l10n.t(LocalizationKey.helpersQuestionsContentTitleAiInputFailed)
|
||||
);
|
||||
title = undefined;
|
||||
}
|
||||
}
|
||||
if (title) {
|
||||
try {
|
||||
aiTitles = await Copilot.suggestTitles(title);
|
||||
} catch (e) {
|
||||
Logger.error((e as Error).message);
|
||||
Notifications.error(
|
||||
l10n.t(LocalizationKey.helpersQuestionsContentTitleCopilotInputFailed)
|
||||
);
|
||||
title = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
const illegalRe = /[/?<>\\:*|"!.,;{}[\]()_+=~`@#$%^&]/g;
|
||||
const illegalRe = (isFileName: boolean) =>
|
||||
isFileName ? /[/?<>\\:*|"!.,;{}[\]()+=~`@#$%^&']/g : /[/?<>\\:*|"!.,;{}[\]()_+=~`@#$%^&']/g;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
||||
const reservedRe = /^\.+$/;
|
||||
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||
const windowsTrailingRe = /[. ]+$/;
|
||||
|
||||
function sanitize(input: string, replacement: string) {
|
||||
function normalizeSpecialChars(input: string): string {
|
||||
return input.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
function sanitize(input: string, replacement: string, isFileName?: boolean) {
|
||||
if (typeof input !== 'string') {
|
||||
throw new Error('Input must be string');
|
||||
}
|
||||
|
||||
const sanitized = input
|
||||
.replace(illegalRe, replacement)
|
||||
const normalizedInput = normalizeSpecialChars(input);
|
||||
|
||||
const sanitized = normalizedInput
|
||||
.replace(illegalRe(isFileName || false), replacement)
|
||||
.replace(controlRe, replacement)
|
||||
.replace(reservedRe, replacement)
|
||||
.replace(windowsReservedRe, replacement)
|
||||
@@ -19,11 +26,12 @@ function sanitize(input: string, replacement: string) {
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export default function (input: string, options?: any) {
|
||||
export default function (input: string, options?: { replacement?: string; isFileName?: boolean }) {
|
||||
const replacement = (options && options.replacement) || '';
|
||||
const output = sanitize(input, replacement);
|
||||
const isFileName = options && options.isFileName;
|
||||
const output = sanitize(input, replacement, isFileName);
|
||||
if (replacement === '') {
|
||||
return output;
|
||||
}
|
||||
return sanitize(output, '');
|
||||
return sanitize(output, '', isFileName);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
SETTING_TAXONOMY_TAGS,
|
||||
SETTING_TAXONOMY_CATEGORIES,
|
||||
SETTING_CONTENT_FILTERS,
|
||||
SETTING_CONTENT_I18N
|
||||
SETTING_CONTENT_I18N,
|
||||
SETTING_CONTENT_GROUPING
|
||||
} from '../constants';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { join, basename, dirname, parse } from 'path';
|
||||
@@ -131,6 +132,8 @@ export class Settings {
|
||||
Settings.config = workspace.getConfiguration(CONFIG_KEY);
|
||||
});
|
||||
|
||||
Logger.info(`Logging level: ${Logger.getLevel()}`);
|
||||
|
||||
Settings.onConfigChange();
|
||||
}
|
||||
|
||||
@@ -867,7 +870,8 @@ ${JSON.stringify(value, null, 2)}`,
|
||||
settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED ||
|
||||
settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES ||
|
||||
settingName === SETTING_COMMA_SEPARATED_FIELDS ||
|
||||
settingName === SETTING_CONTENT_FILTERS
|
||||
settingName === SETTING_CONTENT_FILTERS ||
|
||||
settingName === SETTING_CONTENT_GROUPING
|
||||
) {
|
||||
if (typeof originalConfig[key] === 'undefined') {
|
||||
Settings.globalConfig[key] = value;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Settings } from '.';
|
||||
import { parseWinPath, Settings } from '.';
|
||||
import { stopWords, charMap, SETTING_DATE_FORMAT, SETTING_SLUG_TEMPLATE } from '../constants';
|
||||
import { processTimePlaceholders, processFmPlaceholders } from '.';
|
||||
import { parse } from 'path';
|
||||
|
||||
export class SlugHelper {
|
||||
/**
|
||||
@@ -11,6 +12,7 @@ export class SlugHelper {
|
||||
public static createSlug(
|
||||
articleTitle: string,
|
||||
articleData: { [key: string]: any },
|
||||
filePath?: string,
|
||||
slugTemplate?: string
|
||||
): string | null {
|
||||
if (!articleTitle) {
|
||||
@@ -28,6 +30,21 @@ export class SlugHelper {
|
||||
} else if (slugTemplate.includes('{{seoTitle}}')) {
|
||||
const regex = new RegExp('{{seoTitle}}', 'g');
|
||||
slugTemplate = slugTemplate.replace(regex, SlugHelper.slugify(articleTitle));
|
||||
} else if (slugTemplate.includes(`{{fileName}}`)) {
|
||||
const file = parse(filePath || '');
|
||||
const fileName = file.name;
|
||||
const regex = new RegExp('{{fileName}}', 'g');
|
||||
slugTemplate = slugTemplate.replace(regex, fileName);
|
||||
} else if (slugTemplate.includes(`{{sluggedFileName}}`)) {
|
||||
const file = parse(filePath || '');
|
||||
const fileName = SlugHelper.slugify(file.name);
|
||||
const regex = new RegExp('{{sluggedFileName}}', 'g');
|
||||
slugTemplate = slugTemplate.replace(regex, fileName);
|
||||
} else if (slugTemplate.includes(`{{slugifiedFileName}}`)) {
|
||||
const file = parse(filePath || '');
|
||||
const fileName = SlugHelper.slugify(file.name);
|
||||
const regex = new RegExp('{{slugifiedFileName}}', 'g');
|
||||
slugTemplate = slugTemplate.replace(regex, fileName);
|
||||
}
|
||||
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
|
||||
@@ -38,3 +38,5 @@ export * from './processFmPlaceholders';
|
||||
export * from './processI18nPlaceholders';
|
||||
export * from './processPathPlaceholders';
|
||||
export * from './processTimePlaceholders';
|
||||
export * from './ContentTypeSchemaGenerator';
|
||||
export * from './FrontMatterValidator';
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import { isWindows } from '../utils/isWindows';
|
||||
|
||||
export const parseWinPath = (path: string | undefined): string => {
|
||||
return path?.split(`\\`).join(`/`) || '';
|
||||
path = path?.split(`\\`).join(`/`) || '';
|
||||
|
||||
if (isWindows()) {
|
||||
// Check if path starts with a drive letter (e.g., "C:\")
|
||||
if (/^[a-zA-Z]:\\/.test(path)) {
|
||||
// Convert to lowercase drive letter
|
||||
path = path.charAt(0).toLowerCase() + path.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,8 @@ import { SlugHelper } from './SlugHelper';
|
||||
export const processArticlePlaceholdersFromData = (
|
||||
value: string,
|
||||
data: { [key: string]: any },
|
||||
contentType: ContentType
|
||||
contentType: ContentType,
|
||||
filePath?: string
|
||||
): string => {
|
||||
const titleField = getTitleField();
|
||||
if (value.includes('{{title}}') && data[titleField]) {
|
||||
@@ -18,7 +19,7 @@ export const processArticlePlaceholdersFromData = (
|
||||
const regex = new RegExp('{{slug}}', 'g');
|
||||
value = value.replace(
|
||||
regex,
|
||||
SlugHelper.createSlug(data[titleField] || '', data, contentType.slugTemplate) || ''
|
||||
SlugHelper.createSlug(data[titleField] || '', data, filePath, contentType.slugTemplate) || ''
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +51,7 @@ export const processArticlePlaceholdersFromPath = async (
|
||||
SlugHelper.createSlug(
|
||||
article.data[titleField] || '',
|
||||
article.data,
|
||||
filePath,
|
||||
contentType.slugTemplate
|
||||
) || ''
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { format } from 'date-fns';
|
||||
import { DateHelper } from './DateHelper';
|
||||
import { formatInTimezone } from '../utils';
|
||||
|
||||
/**
|
||||
* Replace the datetime placeholders
|
||||
@@ -21,10 +20,8 @@ export const processDateTimePlaceholders = (value: string, articleDate?: Date) =
|
||||
|
||||
if (dateFormat) {
|
||||
if (dateFormat && typeof dateFormat === 'string') {
|
||||
value = value.replace(
|
||||
match,
|
||||
format(articleDate || new Date(), DateHelper.formatUpdate(dateFormat) as string)
|
||||
);
|
||||
const formattedDate = formatInTimezone(articleDate || new Date(), dateFormat);
|
||||
value = value.replace(match, formattedDate);
|
||||
} else {
|
||||
value = value.replace(match, (articleDate || new Date()).toISOString());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { format } from 'date-fns';
|
||||
import { formatInTimezone } from '../utils';
|
||||
|
||||
export const processFmPlaceholders = (value: string, fmData: { [key: string]: any }) => {
|
||||
// Example: {{fm.date}} or {{fm.date | dateFormat 'DD.MM.YYYY'}}
|
||||
@@ -27,7 +27,7 @@ export const processFmPlaceholders = (value: string, fmData: { [key: string]: an
|
||||
|
||||
// Parse the date value and format it
|
||||
if (fieldValue) {
|
||||
const formattedDate = format(new Date(fieldValue), dateFormat);
|
||||
const formattedDate = formatInTimezone(new Date(fieldValue), dateFormat);
|
||||
value = value.replace(match, formattedDate);
|
||||
}
|
||||
} else if (fieldValue) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dirname, relative } from 'path';
|
||||
import { ContentFolder } from '../models';
|
||||
import { parseWinPath } from './parseWinPath';
|
||||
|
||||
export const processPathPlaceholders = (
|
||||
value: string,
|
||||
@@ -11,7 +12,7 @@ export const processPathPlaceholders = (
|
||||
const relPathToken = '{{pathToken.relPath}}';
|
||||
if (value.includes(relPathToken) && contentFolder?.path) {
|
||||
const dirName = dirname(filePath);
|
||||
const relPath = relative(contentFolder.path, dirName);
|
||||
const relPath = parseWinPath(relative(contentFolder.path, dirName));
|
||||
value = value.replace(relPathToken, relPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { format } from 'date-fns';
|
||||
import { DateHelper } from './DateHelper';
|
||||
import { formatInTimezone } from '../utils';
|
||||
|
||||
/**
|
||||
* Replace the time placeholders
|
||||
@@ -13,10 +12,7 @@ export const processTimePlaceholders = (value: string, dateFormat?: string, arti
|
||||
const regex = new RegExp('{{now}}', 'g');
|
||||
|
||||
if (dateFormat && typeof dateFormat === 'string') {
|
||||
value = value.replace(
|
||||
regex,
|
||||
format(articleDate || new Date(), DateHelper.formatUpdate(dateFormat) as string)
|
||||
);
|
||||
value = value.replace(regex, formatInTimezone(articleDate || new Date(), dateFormat));
|
||||
} else {
|
||||
value = value.replace(regex, (articleDate || new Date()).toISOString());
|
||||
}
|
||||
@@ -24,37 +20,37 @@ export const processTimePlaceholders = (value: string, dateFormat?: string, arti
|
||||
|
||||
if (value.includes('{{year}}')) {
|
||||
const regex = new RegExp('{{year}}', 'g');
|
||||
value = value.replace(regex, format(articleDate || new Date(), 'yyyy'));
|
||||
value = value.replace(regex, formatInTimezone(articleDate || new Date(), 'yyyy'));
|
||||
}
|
||||
|
||||
if (value.includes('{{month}}')) {
|
||||
const regex = new RegExp('{{month}}', 'g');
|
||||
value = value.replace(regex, format(articleDate || new Date(), 'MM'));
|
||||
value = value.replace(regex, formatInTimezone(articleDate || new Date(), 'MM'));
|
||||
}
|
||||
|
||||
if (value.includes('{{day}}')) {
|
||||
const regex = new RegExp('{{day}}', 'g');
|
||||
value = value.replace(regex, format(articleDate || new Date(), 'dd'));
|
||||
value = value.replace(regex, formatInTimezone(articleDate || new Date(), 'dd'));
|
||||
}
|
||||
|
||||
if (value.includes('{{hour12}}')) {
|
||||
const regex = new RegExp('{{hour12}}', 'g');
|
||||
value = value.replace(regex, format(articleDate || new Date(), 'hh'));
|
||||
value = value.replace(regex, formatInTimezone(articleDate || new Date(), 'hh'));
|
||||
}
|
||||
|
||||
if (value.includes('{{hour24}}')) {
|
||||
const regex = new RegExp('{{hour24}}', 'g');
|
||||
value = value.replace(regex, format(articleDate || new Date(), 'HH'));
|
||||
value = value.replace(regex, formatInTimezone(articleDate || new Date(), 'HH'));
|
||||
}
|
||||
|
||||
if (value.includes('{{ampm}}')) {
|
||||
const regex = new RegExp('{{ampm}}', 'g');
|
||||
value = value.replace(regex, format(articleDate || new Date(), 'aaa'));
|
||||
value = value.replace(regex, formatInTimezone(articleDate || new Date(), 'aaa'));
|
||||
}
|
||||
|
||||
if (value.includes('{{minute}}')) {
|
||||
const regex = new RegExp('{{minute}}', 'g');
|
||||
value = value.replace(regex, format(articleDate || new Date(), 'mm'));
|
||||
value = value.replace(regex, formatInTimezone(articleDate || new Date(), 'mm'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ import { DashboardMessage } from '../../dashboardWebView/DashboardMessage';
|
||||
import { BaseListener } from './BaseListener';
|
||||
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
|
||||
import { SortingOption } from '../../dashboardWebView/models';
|
||||
import { commands, env, Uri } from 'vscode';
|
||||
import { commands, env, ProgressLocation, Uri, window, workspace } from 'vscode';
|
||||
import { COMMAND_NAME } from '../../constants';
|
||||
import * as os from 'os';
|
||||
import { Folders } from '../../commands';
|
||||
import { PostMessageData, UnmappedMedia } from '../../models';
|
||||
import { FilesHelper, MediaLibrary } from '../../helpers';
|
||||
import { existsAsync, flattenObjectKeys } from '../../utils';
|
||||
import { join, parse } from 'path';
|
||||
import { LocalizationKey, localize } from '../../localization';
|
||||
|
||||
export class MediaListener extends BaseListener {
|
||||
private static timers: { [folder: string]: any } = {};
|
||||
@@ -54,6 +56,12 @@ export class MediaListener extends BaseListener {
|
||||
case DashboardMessage.createMediaFolder:
|
||||
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.payload);
|
||||
break;
|
||||
case DashboardMessage.updateMediaFolder:
|
||||
await this.updateMediaFolder(msg.payload);
|
||||
break;
|
||||
case DashboardMessage.deleteMediaFolder:
|
||||
await this.deleteMediaFolder(msg.payload);
|
||||
break;
|
||||
case DashboardMessage.createHexoAssetFolder:
|
||||
if (msg?.payload.hexoAssetFolderPath) {
|
||||
Folders.createFolder(msg?.payload.hexoAssetFolderPath);
|
||||
@@ -62,6 +70,90 @@ export class MediaListener extends BaseListener {
|
||||
}
|
||||
}
|
||||
|
||||
public static async deleteMediaFolder(msg: { folder: string }) {
|
||||
if (!msg?.folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
title: localize(
|
||||
LocalizationKey.listenersDashboardMediaListenersDeleteMediaFolderProgressTitle
|
||||
),
|
||||
cancellable: false
|
||||
},
|
||||
async () => {
|
||||
const folderPath = parse(msg.folder).dir;
|
||||
|
||||
const mediaLib = MediaLibrary.getInstance();
|
||||
const parsedPath = MediaLibrary.parsePath(msg.folder);
|
||||
const mediaFiles = await mediaLib.getAllByPath(parsedPath);
|
||||
|
||||
for (const fileName of Object.keys(mediaFiles)) {
|
||||
const filePath = join(msg.folder, fileName);
|
||||
await mediaLib.remove(filePath);
|
||||
}
|
||||
|
||||
await workspace.fs.delete(Uri.file(msg.folder), { recursive: true, useTrash: false });
|
||||
await MediaListener.sendMediaFiles(0, folderPath);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static async updateMediaFolder(msg: {
|
||||
folder: string;
|
||||
wsFolder?: string;
|
||||
staticFolder?: string;
|
||||
}) {
|
||||
if (!msg?.folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
title: localize(
|
||||
LocalizationKey.listenersDashboardMediaListenersUpdateMediaFolderProgressTitle
|
||||
),
|
||||
cancellable: false
|
||||
},
|
||||
async () => {
|
||||
const folderName = parse(msg.folder).base;
|
||||
|
||||
const newFolderName = await window.showInputBox({
|
||||
prompt: 'Enter new folder name',
|
||||
value: folderName
|
||||
});
|
||||
|
||||
if (!newFolderName || newFolderName === folderName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newFolderPath = join(parse(msg.folder).dir, newFolderName);
|
||||
|
||||
// Get all media files from the folder
|
||||
const mediaLib = MediaLibrary.getInstance();
|
||||
const parsedPath = MediaLibrary.parsePath(msg.folder);
|
||||
const mediaFiles = await mediaLib.getAllByPath(parsedPath);
|
||||
|
||||
// Update the folder
|
||||
await workspace.fs.rename(Uri.file(msg.folder), Uri.file(newFolderPath), {
|
||||
overwrite: false
|
||||
});
|
||||
|
||||
// Update the media files
|
||||
for (const fileName of Object.keys(mediaFiles)) {
|
||||
const newFilePath = join(newFolderPath, fileName);
|
||||
const oldFilePath = join(msg.folder, fileName);
|
||||
await mediaLib.rename(oldFilePath, newFilePath);
|
||||
}
|
||||
|
||||
await this.sendMediaFiles(0, parse(msg.folder).dir);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the media files to the dashboard
|
||||
* @param page
|
||||
@@ -125,7 +217,7 @@ export class MediaListener extends BaseListener {
|
||||
for (const file of filesEndingWith) {
|
||||
const absPath = FilesHelper.relToAbsPath(file);
|
||||
if (!(await existsAsync(absPath))) {
|
||||
const parsedPath = mediaLib.parsePath(absPath);
|
||||
const parsedPath = MediaLibrary.parsePath(absPath);
|
||||
const metadata = await mediaLib.get(parsedPath);
|
||||
if (metadata) {
|
||||
unmappedFiles.push({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PostMessageData } from './../../models/PostMessageData';
|
||||
import { basename } from 'path';
|
||||
import { basename, join } from 'path';
|
||||
import { commands, FileSystemWatcher, RelativePattern, TextDocument, Uri, workspace } from 'vscode';
|
||||
import { Dashboard } from '../../commands/Dashboard';
|
||||
import { Folders } from '../../commands/Folders';
|
||||
@@ -12,13 +12,24 @@ import {
|
||||
import { DashboardCommand } from '../../dashboardWebView/DashboardCommand';
|
||||
import { DashboardMessage } from '../../dashboardWebView/DashboardMessage';
|
||||
import { Page } from '../../dashboardWebView/models';
|
||||
import { ArticleHelper, Extension, Logger, parseWinPath, Settings } from '../../helpers';
|
||||
import { ContentFolder } from '../../models/ContentFolder';
|
||||
import {
|
||||
ArticleHelper,
|
||||
Extension,
|
||||
Logger,
|
||||
parseWinPath,
|
||||
Settings,
|
||||
ContentType,
|
||||
Notifications
|
||||
} from '../../helpers';
|
||||
import { BaseListener } from './BaseListener';
|
||||
import { DataListener } from '../panel';
|
||||
import Fuse from 'fuse.js';
|
||||
import { PagesParser } from '../../services/PagesParser';
|
||||
import { unlinkAsync, rmdirAsync } from '../../utils';
|
||||
import { LoadingType } from '../../models';
|
||||
import { Questions } from '../../helpers/Questions';
|
||||
import { Template } from '../../commands/Template';
|
||||
|
||||
export class PagesListener extends BaseListener {
|
||||
private static watchers: { [path: string]: FileSystemWatcher } = {};
|
||||
@@ -45,6 +56,9 @@ export class PagesListener extends BaseListener {
|
||||
case DashboardMessage.createByTemplate:
|
||||
await commands.executeCommand(COMMAND_NAME.createByTemplate);
|
||||
break;
|
||||
case DashboardMessage.createContentInFolder:
|
||||
await this.createContentInFolder(msg.payload);
|
||||
break;
|
||||
case DashboardMessage.refreshPages:
|
||||
this.getPagesData(true);
|
||||
break;
|
||||
@@ -57,6 +71,9 @@ export class PagesListener extends BaseListener {
|
||||
case DashboardMessage.rename:
|
||||
ArticleHelper.rename(msg.payload);
|
||||
break;
|
||||
case DashboardMessage.moveFile:
|
||||
await this.moveFile(msg.payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +323,246 @@ export class PagesListener extends BaseListener {
|
||||
this.sendMsg(DashboardCommand.searchPages, pageResults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a file to a different folder
|
||||
* @param payload
|
||||
*/
|
||||
private static async moveFile(payload: { filePath: string; destinationFolder: string }) {
|
||||
if (!payload || !payload.filePath || !payload.destinationFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filePath, destinationFolder } = payload;
|
||||
|
||||
try {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (!wsFolder) {
|
||||
Logger.error('Workspace folder not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all content folders
|
||||
const folders = await Folders.get();
|
||||
if (!folders || folders.length === 0) {
|
||||
Logger.error('No content folders found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the destination folder
|
||||
let targetFolderPath = '';
|
||||
for (const folder of folders) {
|
||||
const absoluteFolderPath = Folders.getFolderPath(Uri.file(folder.path));
|
||||
const relativeFolderPath = parseWinPath(absoluteFolderPath)
|
||||
.replace(parseWinPath(wsFolder.fsPath), '')
|
||||
.replace(/^\/+|\/+$/g, '');
|
||||
|
||||
if (
|
||||
destinationFolder === relativeFolderPath ||
|
||||
destinationFolder.startsWith(relativeFolderPath + '/')
|
||||
) {
|
||||
targetFolderPath = absoluteFolderPath;
|
||||
// Add subfolder if any
|
||||
if (destinationFolder !== relativeFolderPath) {
|
||||
const subPath = destinationFolder
|
||||
.substring(relativeFolderPath.length)
|
||||
.replace(/^\/+|\/+$/g, '');
|
||||
targetFolderPath = join(targetFolderPath, subPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetFolderPath) {
|
||||
Logger.error('Target folder not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the file name
|
||||
const fileName = basename(filePath);
|
||||
const newFilePath = join(targetFolderPath, fileName);
|
||||
|
||||
// Check if target already exists
|
||||
try {
|
||||
await workspace.fs.stat(Uri.file(newFilePath));
|
||||
Logger.error(`File already exists at destination: ${newFilePath}`);
|
||||
return;
|
||||
} catch {
|
||||
// File doesn't exist, which is good
|
||||
}
|
||||
|
||||
// Check if it's a page bundle
|
||||
const article = await ArticleHelper.getFrontMatterByPath(filePath);
|
||||
if (article) {
|
||||
const contentType = await ArticleHelper.getContentType(article);
|
||||
|
||||
if (contentType.pageBundle) {
|
||||
// Move the entire folder
|
||||
const sourceFolder = parseWinPath(filePath).substring(
|
||||
0,
|
||||
parseWinPath(filePath).lastIndexOf('/')
|
||||
);
|
||||
const folderName = basename(sourceFolder);
|
||||
const newFolderPath = join(targetFolderPath, folderName);
|
||||
|
||||
// Move the folder
|
||||
await workspace.fs.rename(Uri.file(sourceFolder), Uri.file(newFolderPath), {
|
||||
overwrite: false
|
||||
});
|
||||
|
||||
Logger.info(`Moved page bundle from ${sourceFolder} to ${newFolderPath}`);
|
||||
} else {
|
||||
// Move just the file
|
||||
await workspace.fs.rename(Uri.file(filePath), Uri.file(newFilePath), {
|
||||
overwrite: false
|
||||
});
|
||||
|
||||
Logger.info(`Moved file from ${filePath} to ${newFilePath}`);
|
||||
}
|
||||
} else {
|
||||
// Move just the file
|
||||
await workspace.fs.rename(Uri.file(filePath), Uri.file(newFilePath), {
|
||||
overwrite: false
|
||||
});
|
||||
|
||||
Logger.info(`Moved file from ${filePath} to ${newFilePath}`);
|
||||
}
|
||||
|
||||
// Refresh the pages data
|
||||
this.getPagesData(true);
|
||||
} catch (error) {
|
||||
Logger.error(`Error moving file: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content in a specific folder
|
||||
* @param payload
|
||||
*/
|
||||
private static async createContentInFolder(payload: { folderPath: string | null }) {
|
||||
if (!payload) {
|
||||
// Fall back to regular content creation
|
||||
await commands.executeCommand(COMMAND_NAME.createContent);
|
||||
return;
|
||||
}
|
||||
|
||||
const { folderPath } = payload;
|
||||
|
||||
// Get all content folders (including those with disableCreation)
|
||||
const allFolders = await Folders.get();
|
||||
|
||||
if (!allFolders || allFolders.length === 0) {
|
||||
await commands.executeCommand(COMMAND_NAME.createContent);
|
||||
return;
|
||||
}
|
||||
|
||||
let targetFolder = null;
|
||||
let subPath = '';
|
||||
|
||||
if (folderPath) {
|
||||
// The folderPath is a relative path like "content/posts" or "blog/en"
|
||||
// We need to find the matching content folder and determine the subfolder
|
||||
Logger.info(`[createContentInFolder] folderPath: ${folderPath}`);
|
||||
|
||||
let bestMatch: { folder: ContentFolder; subPath: string; matchLength: number } | null = null;
|
||||
|
||||
for (const folder of allFolders) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (!wsFolder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absoluteFolderPath = Folders.getFolderPath(Uri.file(folder.path));
|
||||
const relativeFolderPath = parseWinPath(absoluteFolderPath)
|
||||
.replace(parseWinPath(wsFolder.fsPath), '')
|
||||
.replace(/^\/+|\/+$/g, '');
|
||||
|
||||
Logger.info(
|
||||
`[createContentInFolder] Checking folder: ${folder.title}, relativePath: ${relativeFolderPath}`
|
||||
);
|
||||
|
||||
// Check if the folderPath matches or starts with this content folder
|
||||
if (folderPath === relativeFolderPath || folderPath.startsWith(relativeFolderPath + '/')) {
|
||||
const currentSubPath =
|
||||
folderPath !== relativeFolderPath
|
||||
? folderPath.substring(relativeFolderPath.length).replace(/^\/+|\/+$/g, '')
|
||||
: '';
|
||||
|
||||
// Keep track of the best (longest/most specific) match
|
||||
if (!bestMatch || relativeFolderPath.length > bestMatch.matchLength) {
|
||||
bestMatch = {
|
||||
folder,
|
||||
subPath: currentSubPath,
|
||||
matchLength: relativeFolderPath.length
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch) {
|
||||
targetFolder = bestMatch.folder;
|
||||
subPath = bestMatch.subPath;
|
||||
Logger.info(
|
||||
`[createContentInFolder] Best match: ${targetFolder.title}, subPath: ${subPath}`
|
||||
);
|
||||
|
||||
// Check if content creation is disabled for this folder
|
||||
if (targetFolder.disableCreation) {
|
||||
Notifications.error(`Content creation is disabled for folder: ${targetFolder.title}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetFolder) {
|
||||
// If no folder matches, let the user select one (filter out disabled folders)
|
||||
const availableFolders = allFolders.filter((f) => !f.disableCreation);
|
||||
if (availableFolders.length === 0) {
|
||||
await commands.executeCommand(COMMAND_NAME.createContent);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFolder = await Questions.SelectContentFolder();
|
||||
if (!selectedFolder) {
|
||||
return;
|
||||
}
|
||||
targetFolder = allFolders.find((f) => f.path === selectedFolder.path);
|
||||
}
|
||||
|
||||
if (!targetFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the folder path
|
||||
let absoluteFolderPath = Folders.getFolderPath(Uri.file(targetFolder.path));
|
||||
|
||||
// Add the subfolder if any
|
||||
if (subPath) {
|
||||
absoluteFolderPath = join(absoluteFolderPath, subPath);
|
||||
}
|
||||
|
||||
// Check if templates are enabled
|
||||
const templatesEnabled = Settings.get('dashboardState.contents.templatesEnabled');
|
||||
|
||||
if (templatesEnabled) {
|
||||
// Use the template creation flow
|
||||
await Template.create(absoluteFolderPath);
|
||||
} else {
|
||||
// Use the content type creation flow
|
||||
const selectedContentType = await Questions.SelectContentType(
|
||||
targetFolder.contentTypes || []
|
||||
);
|
||||
if (!selectedContentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentTypes = ContentType.getAll();
|
||||
const contentType = contentTypes?.find((ct) => ct.name === selectedContentType);
|
||||
if (contentType) {
|
||||
ContentType['create'](contentType, absoluteFolderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fresh page data
|
||||
*/
|
||||
|
||||
@@ -114,7 +114,12 @@ export class SsgListener extends BaseListener {
|
||||
}
|
||||
|
||||
// https://github.com/withastro/astro/blob/defab70cb2a0c67d5e9153542490d2749046b151/packages/astro/src/content/utils.ts#L450
|
||||
const contentConfig = await workspace.findFiles(`**/src/content/config.*`);
|
||||
let contentConfig = await workspace.findFiles(`**/src/content/config.*`);
|
||||
|
||||
// Also search for content.config.* files (newer pattern)
|
||||
if (contentConfig.length === 0) {
|
||||
contentConfig = await workspace.findFiles(`**/content.config.*`);
|
||||
}
|
||||
|
||||
if (contentConfig.length === 0) {
|
||||
SsgListener.sendRequest(command as any, requestId, []);
|
||||
|
||||
@@ -794,7 +794,9 @@ export class DataListener extends BaseListener {
|
||||
const crntFile = window.activeTextEditor?.document;
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
value =
|
||||
data && contentType ? processArticlePlaceholdersFromData(value, data, contentType) : value;
|
||||
data && contentType
|
||||
? processArticlePlaceholdersFromData(value, data, contentType, crntFile?.uri.fsPath)
|
||||
: value;
|
||||
value = processTimePlaceholders(value, dateFormat);
|
||||
value = processFmPlaceholders(value, data);
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { authentication, window } from 'vscode';
|
||||
import { ArticleHelper, Extension, Settings, TaxonomyHelper } from '../../helpers';
|
||||
import { BlockFieldData, CustomTaxonomyData, PostMessageData, TaxonomyType } from '../../models';
|
||||
import { DataListener } from '.';
|
||||
import { SettingsListener as PanelSettingsListener } from '.';
|
||||
import { SettingsListener as DashboardSettingsListener } from '../dashboard';
|
||||
import { SponsorAi } from '../../services/SponsorAI';
|
||||
import { PanelProvider } from '../../panelWebView/PanelProvider';
|
||||
import { MessageHandlerData } from '@estruyf/vscode';
|
||||
@@ -279,6 +281,9 @@ export class TaxonomyListener extends BaseListener {
|
||||
}
|
||||
|
||||
await Settings.updateCustomTaxonomy(data.id, data.option);
|
||||
|
||||
PanelSettingsListener.getSettings();
|
||||
DashboardSettingsListener.getSettings(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -211,6 +211,14 @@ export enum LocalizationKey {
|
||||
* Open dashboard on startup
|
||||
*/
|
||||
settingsOpenOnStartup = 'settings.openOnStartup',
|
||||
/**
|
||||
* Open panel for supported files
|
||||
*/
|
||||
settingsOpenPanelForSupportedFiles = 'settings.openPanelForSupportedFiles',
|
||||
/**
|
||||
* Do you want to open the panel for supported files?
|
||||
*/
|
||||
settingsOpenPanelForSupportedFilesLabel = 'settings.openPanelForSupportedFiles.label',
|
||||
/**
|
||||
* Content types
|
||||
*/
|
||||
@@ -616,7 +624,7 @@ export enum LocalizationKey {
|
||||
*/
|
||||
dashboardHeaderPaginationPrevious = 'dashboard.header.pagination.previous',
|
||||
/**
|
||||
* next
|
||||
* Next
|
||||
*/
|
||||
dashboardHeaderPaginationNext = 'dashboard.header.pagination.next',
|
||||
/**
|
||||
@@ -719,6 +727,10 @@ export enum LocalizationKey {
|
||||
* Change to list
|
||||
*/
|
||||
dashboardHeaderViewSwitchToList = 'dashboard.header.viewSwitch.toList',
|
||||
/**
|
||||
* Change to structure
|
||||
*/
|
||||
dashboardHeaderViewSwitchToStructure = 'dashboard.header.viewSwitch.toStructure',
|
||||
/**
|
||||
* Support Front Matter
|
||||
*/
|
||||
@@ -807,6 +819,10 @@ export enum LocalizationKey {
|
||||
* Public directory
|
||||
*/
|
||||
dashboardMediaFolderItemPublicDirectory = 'dashboard.media.folderItem.publicDirectory',
|
||||
/**
|
||||
* Are you sure you want to delete the folder ({0})?
|
||||
*/
|
||||
dashboardMediaFolderItemDeleteDescription = 'dashboard.media.folderItem.deleteDescription',
|
||||
/**
|
||||
* Insert image
|
||||
*/
|
||||
@@ -1096,7 +1112,7 @@ export enum LocalizationKey {
|
||||
*/
|
||||
dashboardStepsStepsToGetStartedGitName = 'dashboard.steps.stepsToGetStarted.git.name',
|
||||
/**
|
||||
* Enable Git synchronization to eaily sync your changes with your repository.
|
||||
* Enable Git synchronization to easily sync your changes with your repository.
|
||||
*/
|
||||
dashboardStepsStepsToGetStartedGitDescription = 'dashboard.steps.stepsToGetStarted.git.description',
|
||||
/**
|
||||
@@ -1224,7 +1240,7 @@ export enum LocalizationKey {
|
||||
*/
|
||||
dashboardWelcomeScreenThanks = 'dashboard.welcomeScreen.thanks',
|
||||
/**
|
||||
* We try to aim to make Front Matter as easy to use as possible, but if you have any questions or suggestions. Please don't hesitate to reach out to us on GitHub.
|
||||
* We aim to make Front Matter as easy to use as possible. If you have any questions or suggestions, please contact us on GitHub.
|
||||
*/
|
||||
dashboardWelcomeScreenDescription = 'dashboard.welcomeScreen.description',
|
||||
/**
|
||||
@@ -1448,18 +1464,6 @@ export enum LocalizationKey {
|
||||
* Actions
|
||||
*/
|
||||
panelActionsTitle = 'panel.actions.title',
|
||||
/**
|
||||
* More details
|
||||
*/
|
||||
panelArticleDetailsTitle = 'panel.articleDetails.title',
|
||||
/**
|
||||
* Type
|
||||
*/
|
||||
panelArticleDetailsType = 'panel.articleDetails.type',
|
||||
/**
|
||||
* Total
|
||||
*/
|
||||
panelArticleDetailsTotal = 'panel.articleDetails.total',
|
||||
/**
|
||||
* Headings
|
||||
*/
|
||||
@@ -1613,17 +1617,33 @@ export enum LocalizationKey {
|
||||
*/
|
||||
panelSeoDetailsRecommended = 'panel.seoDetails.recommended',
|
||||
/**
|
||||
* Keyword usage {0} *
|
||||
* Checks
|
||||
*/
|
||||
panelSeoKeywordInfoDensity = 'panel.seoKeywordInfo.density',
|
||||
panelSeoKeywordsChecks = 'panel.seoKeywords.checks',
|
||||
/**
|
||||
* Used in heading(s)
|
||||
* Frequency
|
||||
*/
|
||||
panelSeoKeywordsDensityTableTitle = 'panel.seoKeywords.density.tableTitle',
|
||||
/**
|
||||
* Keyword density
|
||||
*/
|
||||
panelSeoKeywordsDensity = 'panel.seoKeywords.density',
|
||||
/**
|
||||
* Heading(s)
|
||||
*/
|
||||
panelSeoKeywordInfoValidInfoLabel = 'panel.seoKeywordInfo.validInfo.label',
|
||||
/**
|
||||
* Content
|
||||
*/
|
||||
panelSeoKeywordInfoValidInfoContent = 'panel.seoKeywordInfo.validInfo.content',
|
||||
/**
|
||||
* First paragraph
|
||||
*/
|
||||
panelSeoKeywordInfoValidInfoFirstParagraph = 'panel.seoKeywordInfo.validInfo.firstParagraph',
|
||||
/**
|
||||
* Recommended frequency: 0.75% - 1.5%
|
||||
*/
|
||||
panelSeoKeywordInfoDensityTooltip = 'panel.seoKeywordInfo.density.tooltip',
|
||||
/**
|
||||
* Keywords
|
||||
*/
|
||||
@@ -1639,19 +1659,15 @@ export enum LocalizationKey {
|
||||
/**
|
||||
* * A keyword density of 1-1.5% is sufficient in most cases.
|
||||
*/
|
||||
panelSeoKeywordsDensity = 'panel.seoKeywords.density',
|
||||
panelSeoKeywordsDensityDescription = 'panel.seoKeywords.density.description',
|
||||
/**
|
||||
* Recommendations
|
||||
* Insights
|
||||
*/
|
||||
panelSeoStatusTitle = 'panel.seoStatus.title',
|
||||
/**
|
||||
* Property
|
||||
*/
|
||||
panelSeoStatusHeaderProperty = 'panel.seoStatus.header.property',
|
||||
/**
|
||||
* Length
|
||||
*/
|
||||
panelSeoStatusHeaderLength = 'panel.seoStatus.header.length',
|
||||
/**
|
||||
* Valid
|
||||
*/
|
||||
@@ -2576,6 +2592,14 @@ export enum LocalizationKey {
|
||||
* Could not unpin item.
|
||||
*/
|
||||
listenersDashboardDashboardListenerPinItemCoundNotUnPinError = 'listeners.dashboard.dashboardListener.pinItem.coundNotUnPin.error',
|
||||
/**
|
||||
* Deleting folder...
|
||||
*/
|
||||
listenersDashboardMediaListenersDeleteMediaFolderProgressTitle = 'listeners.dashboard.mediaListeners.deleteMediaFolder.progress.title',
|
||||
/**
|
||||
* Updating folder...
|
||||
*/
|
||||
listenersDashboardMediaListenersUpdateMediaFolderProgressTitle = 'listeners.dashboard.mediaListeners.updateMediaFolder.progress.title',
|
||||
/**
|
||||
* Template files copied.
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface MediaInfo {
|
||||
dimensions?: ISizeCalculationResult | undefined;
|
||||
mimeType?: string | undefined;
|
||||
mtime?: Date;
|
||||
mtimeMs?: number;
|
||||
ctime?: Date;
|
||||
size?: number;
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ export interface PanelSettings {
|
||||
dataTypes: DataType[] | undefined;
|
||||
fieldGroups: FieldGroup[] | undefined;
|
||||
commaSeparatedFields: string[];
|
||||
aiEnabled: boolean;
|
||||
copilotEnabled: boolean;
|
||||
contentFolders: ContentFolder[];
|
||||
websiteUrl: string;
|
||||
@@ -177,6 +176,7 @@ export interface WhenClause {
|
||||
|
||||
export interface DateInfo {
|
||||
format: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
export interface SEO {
|
||||
|
||||
7
src/models/ScriptAction.ts
Normal file
7
src/models/ScriptAction.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type ScriptAction =
|
||||
| 'open'
|
||||
| 'copyMediaMetadata'
|
||||
| 'copyMediaMetadataAndDelete'
|
||||
| 'deleteMedia'
|
||||
| 'fieldAction'
|
||||
| 'promptCopilot';
|
||||
@@ -22,6 +22,7 @@ export * from './Mode';
|
||||
export * from './PanelSettings';
|
||||
export * from './PostMessageData';
|
||||
export * from './Project';
|
||||
export * from './ScriptAction';
|
||||
export * from './ShellSetting';
|
||||
export * from './Snippets';
|
||||
export * from './SortOrder';
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
FieldsListener,
|
||||
LocalizationListener
|
||||
} from './../listeners/panel';
|
||||
import { SETTING_EXPERIMENTAL } from '../constants';
|
||||
import { SETTING_EXPERIMENTAL, SETTING_PANEL_OPEN_ON_SUPPORTED_FILE } from '../constants';
|
||||
import {
|
||||
CancellationToken,
|
||||
commands,
|
||||
Disposable,
|
||||
Uri,
|
||||
Webview,
|
||||
@@ -136,6 +137,21 @@ export class PanelProvider implements WebviewViewProvider, Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the panel if the active file is supported.
|
||||
*
|
||||
* @returns {Promise<void>} A promise that resolves when the command execution is complete.
|
||||
*/
|
||||
public static async openOnSupportedFile(): Promise<void> {
|
||||
const openPanel = Settings.get<boolean>(SETTING_PANEL_OPEN_ON_SUPPORTED_FILE);
|
||||
if (openPanel) {
|
||||
const activeFile = ArticleHelper.getActiveFile();
|
||||
if (activeFile) {
|
||||
await commands.executeCommand('frontMatter.explorer.focus');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post data to the panel
|
||||
* @param msg
|
||||
|
||||
@@ -124,12 +124,26 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = () => {
|
||||
<GitAction settings={settings} />
|
||||
</FeatureFlag>
|
||||
|
||||
{!loading && (<CustomView metadata={metadata} />)}
|
||||
|
||||
<FeatureFlag features={mode?.features || DEFAULT_PANEL_FEATURE_FLAGS} flag={FEATURE_FLAG.panel.globalSettings}>
|
||||
<GlobalSettings settings={settings} isBase={!metadata} />
|
||||
</FeatureFlag>
|
||||
|
||||
{
|
||||
!loading && metadata && (
|
||||
<FeatureFlag features={mode?.features || DEFAULT_PANEL_FEATURE_FLAGS} flag={FEATURE_FLAG.panel.metadata}>
|
||||
<Metadata
|
||||
settings={settings}
|
||||
metadata={metadata}
|
||||
focusElm={focusElm}
|
||||
unsetFocus={unsetFocus}
|
||||
features={mode?.features || DEFAULT_PANEL_FEATURE_FLAGS}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
)
|
||||
}
|
||||
|
||||
{!loading && (<CustomView metadata={metadata} />)}
|
||||
|
||||
{
|
||||
!loading && metadata && settings && settings.seo && (
|
||||
<FeatureFlag features={mode?.features || DEFAULT_PANEL_FEATURE_FLAGS} flag={FEATURE_FLAG.panel.seo}>
|
||||
@@ -153,20 +167,6 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = () => {
|
||||
</FeatureFlag>
|
||||
)}
|
||||
|
||||
{
|
||||
!loading && metadata && (
|
||||
<FeatureFlag features={mode?.features || DEFAULT_PANEL_FEATURE_FLAGS} flag={FEATURE_FLAG.panel.metadata}>
|
||||
<Metadata
|
||||
settings={settings}
|
||||
metadata={metadata}
|
||||
focusElm={focusElm}
|
||||
unsetFocus={unsetFocus}
|
||||
features={mode?.features || DEFAULT_PANEL_FEATURE_FLAGS}
|
||||
/>
|
||||
</FeatureFlag>
|
||||
)
|
||||
}
|
||||
|
||||
<FeatureFlag features={mode?.features || DEFAULT_PANEL_FEATURE_FLAGS} flag={FEATURE_FLAG.panel.recentlyModified}>
|
||||
<FolderAndFiles data={folderAndFiles} isBase={!metadata} />
|
||||
</FeatureFlag>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../localization';
|
||||
import { VSCodeTable, VSCodeTableBody, VSCodeTableCell, VSCodeTableHead, VSCodeTableHeader, VSCodeTableRow } from './VSCode/VSCodeTable';
|
||||
import { VSCodeTableCell, VSCodeTableRow } from './VSCode/VSCodeTable';
|
||||
|
||||
export interface IArticleDetailsProps {
|
||||
details: {
|
||||
@@ -11,6 +11,7 @@ export interface IArticleDetailsProps {
|
||||
internalLinks: number;
|
||||
externalLinks: number;
|
||||
images: number;
|
||||
firstParagraph?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,59 +23,42 @@ const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`seo__status__details valid`}>
|
||||
<h4>{l10n.t(LocalizationKey.panelArticleDetailsTitle)}</h4>
|
||||
<>
|
||||
{details?.headings !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsHeadings)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.headings}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
|
||||
<VSCodeTable>
|
||||
<VSCodeTableHeader>
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableHead>
|
||||
{l10n.t(LocalizationKey.panelArticleDetailsType)}
|
||||
</VSCodeTableHead>
|
||||
<VSCodeTableHead>
|
||||
{l10n.t(LocalizationKey.panelArticleDetailsTotal)}
|
||||
</VSCodeTableHead>
|
||||
</VSCodeTableRow>
|
||||
</VSCodeTableHeader>
|
||||
{details?.paragraphs !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsParagraphs)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.paragraphs}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
|
||||
<VSCodeTableBody>
|
||||
{details?.headings !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsHeadings)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.headings}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
{details?.internalLinks !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsInternalLinks)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.internalLinks}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
|
||||
{details?.paragraphs !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsParagraphs)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.paragraphs}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
{details?.externalLinks !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsExternalLinks)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.externalLinks}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
|
||||
{details?.internalLinks !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsInternalLinks)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.internalLinks}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
|
||||
{details?.externalLinks !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsExternalLinks)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.externalLinks}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
|
||||
{details?.images !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsImages)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.images}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
</VSCodeTableBody>
|
||||
</VSCodeTable>
|
||||
</div>
|
||||
{details?.images !== undefined && (
|
||||
<VSCodeTableRow>
|
||||
<VSCodeTableCell>{l10n.t(LocalizationKey.panelArticleDetailsImages)}</VSCodeTableCell>
|
||||
<VSCodeTableCell>{details.images}</VSCodeTableCell>
|
||||
</VSCodeTableRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user