Compare commits

...

137 Commits

Author SHA1 Message Date
Elio Struyf
c49d3ef00f fix: update taxonomy settings retrieval in TaxonomyListener #1017 2026-03-26 17:13:22 +01:00
Elio Struyf
479e84a21e Merge pull request #1015 from mvanhorn/fix/hide-dotfolders-in-media
fix: hide dot-prefixed folders in media panel
2026-03-24 13:30:54 +01:00
Elio Struyf
4ea0ca06e7 Merge branch 'beta' into fix/hide-dotfolders-in-media 2026-03-24 13:30:43 +01:00
Matt Van Horn
d75dc9aff7 fix: hide dot-prefixed folders in media panel
Filter out directories starting with '.' when listing folders
in the media panel. These hidden folders (like .git, .hidden)
were appearing in the media browser unexpectedly.

Added !dir.name.startsWith('.') to three readdirAsync filter
chains in MediaHelpers.ts: selectedFolder scan (line 217),
content folder scan (line 228), and static folder scan (line 246).

Fixes #509

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 01:12:26 -07:00
Elio Struyf
5e602a20c1 fix: enhance date handling in setLastModifiedDateInner for TOML support #979 2026-03-14 12:07:46 +01:00
Elio Struyf
05fcf95a00 Merge branch 'beta' of github.com:estruyf/vscode-front-matter into beta 2026-03-14 11:50:28 +01:00
Elio Struyf
753bb3dc14 fix: add entry for content view sorting issue in changelog 2026-03-14 11:40:59 +01:00
Elio Struyf
aa7c201a07 Merge pull request #1009 from estruyf/copilot/fix-content-view-sorting-issue
Fix content view Modified Date sorting to use frontmatter field value instead of file mtime
2026-03-14 11:40:17 +01:00
Elio Struyf
767af177e8 fix: update text color for settings and select item components to improve visibility in dark themes Issue: Settings Page is Unreadable in Dark Themes
Fixes #964
2026-03-14 11:38:21 +01:00
copilot-swe-agent[bot]
7415ea786e fix: pass dateFormat to DateHelper.tryParse when parsing modified date field
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2026-03-14 10:33:03 +00:00
copilot-swe-agent[bot]
2ca2993786 Initial plan 2026-03-14 10:30:18 +00:00
Elio Struyf
fb95439452 fix: improve error handling for file reading and template loading in ArticleHelper and ContentType #950 2026-03-14 11:26:53 +01:00
Elio Struyf
b6ac0ea1e6 refactor: improve handling of field values and remove unused variables in PagesParser #958 2026-03-14 11:22:32 +01:00
Elio Struyf
1c0ce6a6f2 Merge pull request #1007 from estruyf/revert-960-copilot/fix-958
Revert "Fix variable frontmatter error - handle both string and object formats for preview image fields"
2026-03-14 11:16:55 +01:00
Elio Struyf
bdb2179e3e Revert "Fix variable frontmatter error - handle both string and object formats for preview image fields" 2026-03-14 11:16:37 +01:00
Elio Struyf
bd8cd1f1d6 Merge pull request #960 from estruyf/copilot/fix-958
Fix variable frontmatter error - handle both string and object formats for preview image fields
2026-03-14 11:16:27 +01:00
Elio Struyf
09f97b9c8c Merge branch 'copilot/enhance-schema-validation-yaml' into beta 2026-03-14 11:13:50 +01:00
Elio Struyf
70c17d5de3 Enhance schema validation for YAML front matter and improve error range detection 2026-03-14 11:12:59 +01:00
Elio Struyf
1f52b02bf7 Refactor code formatting and improve schema validation logic in StatusListener 2026-03-14 11:07:47 +01:00
Elio Struyf
7d2ecc53af Add front matter validation setting and update schema validation logic 2026-03-14 11:07:36 +01:00
Elio Struyf
66d21cc255 fix: update changelog to correctly reflect output channel colorizer schema fix 2026-03-14 10:53:37 +01:00
Elio Struyf
13a80b33e3 fix: update output channel creation to include specific ID for frontmatter project #1006 2026-03-14 10:50:32 +01:00
Elio Struyf
7847464899 refactor: improve StructureView layout and enhance folder selection logic in PagesListener 2026-03-06 17:32:46 +01:00
Elio Struyf
7db95ca091 refactor: remove chatbot functionality and related localization keys 2026-03-06 16:56:27 +01:00
Elio Struyf
5c0076b9b2 refactor: clean up whitespace and formatting in multiple components 2026-03-06 16:51:47 +01:00
Elio Struyf
7ea0fbad05 feat: add move file functionality and create content in folder dialog 2026-03-06 16:51:41 +01:00
Elio Struyf
2e8472dd75 Merge branch 'beta' of github.com:estruyf/vscode-front-matter into beta 2026-03-06 16:51:14 +01:00
Elio Struyf
0842133db4 Enhancement: Support the new integrated browser instead of the lite browser
Fixes #1005
2026-03-06 16:50:02 +01:00
Elio Struyf
e25cb9796a Remove AI sponsor feature and related settings; update changelog and localization files #983 2026-03-06 16:35:46 +01:00
Elio Struyf
648541d9a5 Merge pull request #992 from estruyf/copilot/fix-timezone-issue
Fix timezone setting ignored when formatting dates
2026-03-06 16:27:14 +01:00
Elio Struyf
57e0e2e7b7 Refactor MediaLibrary and MediaListener to use static parsePath method for improved consistency 2026-03-06 16:19:42 +01:00
Elio Struyf
3b65bb3cd7 Remove BEJS Community sponsorship section from README.md 2026-01-26 14:30:51 +01:00
Elio Struyf
829c5c6e64 Remove run.events sponsorship section from README.md 2026-01-26 14:30:33 +01:00
Elio Struyf
e6ef7555e3 Update README.md to clarify focus on sustainable revenue for open source projects 2026-01-26 14:23:09 +01:00
Elio Struyf
2af6c57a49 Update README.md with 2026 Open Source Priorities and content improvements 2026-01-26 14:20:48 +01:00
copilot-swe-agent[bot]
5de91cf683 Add validation for tags, categories, and custom taxonomy against known values
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-12-04 11:02:40 +00:00
copilot-swe-agent[bot]
2b7fd1d1e7 Improve field location detection and add comprehensive documentation
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-12-03 15:12:58 +00:00
copilot-swe-agent[bot]
c179364f2b Address code review feedback and improve validation
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-12-03 15:07:43 +00:00
copilot-swe-agent[bot]
4bee998d9b Add schema generation and validation infrastructure
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-12-03 15:02:49 +00:00
copilot-swe-agent[bot]
17f390545a Fix timezone issue by providing default 'UTC' value
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-12-03 15:02:31 +00:00
copilot-swe-agent[bot]
de569d37d5 Initial plan 2025-12-03 14:54:14 +00:00
copilot-swe-agent[bot]
d59969cbe1 Initial plan 2025-12-03 14:53:46 +00:00
Elio Struyf
f5b636d960 Enhance Copilot extension detection and update model family to gpt-4.1 2025-12-01 09:38:49 +01:00
Elio Struyf
7fac27b73e Enhancement: Dashboard "Structure" for Docs
Fixes #937
2025-09-09 19:52:33 +02:00
Elio Struyf
aa0ee4708a Merge branch 'copilot/fix-937' into beta 2025-09-09 19:51:43 +02:00
Elio Struyf
24c26ac855 Refactor StructureView and Overview components for improved readability; remove unused sorting logic and adjust layout styles 2025-09-09 19:50:21 +02:00
Elio Struyf
cda217ac76 Enhance StructureView to normalize folder paths and improve page assignment logic 2025-09-09 19:14:38 +02:00
Elio Struyf
cb42bd4b4b Merge branches 'copilot/fix-937' and 'copilot/fix-937' of github.com:estruyf/vscode-front-matter into copilot/fix-937 2025-09-09 16:11:44 +02:00
copilot-swe-agent[bot]
d4c5ca1c18 Fix folder path normalization in Structure view for proper nesting
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-09-09 13:56:45 +00:00
Elio Struyf
61398c4e25 Merge branch 'copilot/fix-937' of github.com:estruyf/vscode-front-matter into copilot/fix-937 2025-09-09 15:47:24 +02:00
copilot-swe-agent[bot]
b62d1e8177 Fix folder hierarchy rendering in Structure view
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-09-09 13:32:16 +00:00
copilot-swe-agent[bot]
65fc9f38ed Fix Item rendering for Structure view type
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-09-09 13:17:44 +00:00
Elio Struyf
c8ebac32d3 Update CHANGELOG for version 10.10.0: Add SEO keyword support enhancement 2025-09-09 09:42:13 +02:00
Elio Struyf
219c4bd657 Merge branch 'copilot/fix-965' into beta 2025-09-09 09:39:45 +02:00
copilot-swe-agent[bot]
73e58c7b52 Add multi-language localization support for Structure view
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-09-08 20:16:45 +00:00
copilot-swe-agent[bot]
e4147eed09 Implement Structure view for dashboard with folder hierarchy display
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-09-08 20:11:43 +00:00
copilot-swe-agent[bot]
beef6f36d8 Implement first paragraph keyword check for SEO validation
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-09-08 20:07:41 +00:00
copilot-swe-agent[bot]
f3df0f6856 Initial plan 2025-09-08 19:53:11 +00:00
copilot-swe-agent[bot]
d11dbc9d76 Initial plan 2025-09-08 19:52:20 +00:00
Elio Struyf
bb535961a3 Update CHANGELOG for version 10.10.0: Add enhancements and fixes sections 2025-09-08 21:49:21 +02:00
Elio Struyf
0c7e3fb42b Enhancement: Support for numbers (int) in Snippets
Fixes #973
2025-09-08 21:48:30 +02:00
Elio Struyf
a6188b0060 Add entry for version 10.10.0 to CHANGELOG with typo fix on welcome screen 2025-07-15 23:27:28 +02:00
Elio Struyf
43a6a22721 Fix typo #969 2025-07-15 23:26:45 +02:00
Elio Struyf
99405042ed Refactor date change handling in DateTimeField to ensure proper timezone formatting and fallback to ISO string 2025-07-14 21:24:31 +02:00
Elio Struyf
76b103cb62 Merge branch 'beta' of github.com:estruyf/vscode-front-matter into beta 2025-07-03 20:17:55 +02:00
Elio Struyf
be158d4365 Update docs 2025-07-03 20:17:44 +02:00
Elio Struyf
b31550afe3 Merge pull request #967 from estruyf/beta
10.9.0 merge
2025-07-01 09:15:38 +02:00
Elio Struyf
b845fd422b 10.9.0 2025-07-01 09:14:52 +02:00
Elio Struyf
feb6b008e9 Merge branch 'beta' of github.com:estruyf/vscode-front-matter into beta 2025-07-01 09:14:31 +02:00
Elio Struyf
0fdbada702 Merge pull request #962 from HaoJun0823/patch-1
Added Simplified Chinese localization
2025-07-01 09:13:29 +02:00
Elio Struyf
6d010e48e8 Add Simplified Chinese localization to CHANGELOG for version 10.9.0 2025-07-01 09:12:49 +02:00
Elio Struyf
18d7fb0c20 Update CHANGELOG for version 10.9.0 release date and section adjustments 2025-07-01 09:10:19 +02:00
Randerion(HaoJun0823)
00dfbcb029 Add simplified Chinese version of package.nls.zh-cn.json (Create package.nls.zh-cn.json) 2025-06-23 19:55:32 +08:00
Randerion(HaoJun0823)
89aede03f0 Added Simplified Chinese localization (Create bundle.l10n.zh-cn.json)
I translated and tested it, it seems to be ok :)
Although you have an i18n branch but it seems to be much older than main, so I submitted this PR.

The only thing I found is that there is no localization label for "When registering a folder as a content folder", this part is still in English.

If there is no problem, I think I will make traditional Chinese later, it's not hard.
2025-06-23 19:27:59 +08:00
copilot-swe-agent[bot]
a387d5eb89 Fix ContentType validation to handle non-string/non-array field values safely
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-06-20 12:48:08 +00:00
copilot-swe-agent[bot]
f1ae60f280 Fix variable frontmatter error - handle both string and object formats for preview image fields
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-06-20 12:46:44 +00:00
copilot-swe-agent[bot]
0e2aea626f Initial plan for issue 2025-06-20 12:36:28 +00:00
Elio Struyf
ef9f1f7f56 Issue: Check why media files are not loading in the latest version #957 2025-06-17 10:00:24 +02:00
Elio Struyf
d57d0c5d45 Merge pull request #955 from estruyf/copilot/fix-932
Support both content.config.* and src/content/config.* patterns for Astro content collections
2025-06-13 14:52:25 +02:00
copilot-swe-agent[bot]
bb9ea9f1b9 Support both content.config.* and src/content/config.* patterns for Astro content collections
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-06-13 12:47:05 +00:00
copilot-swe-agent[bot]
7d5fde1182 Initial plan for issue 2025-06-13 12:39:38 +00:00
Elio Struyf
33693d9315 Update changelog 2025-04-16 11:27:53 +02:00
Elio Struyf
8c52ef7bde Merge branch 'main' into beta 2025-04-16 11:26:00 +02:00
Elio Struyf
9ce7754b1a Merge pull request #943 from stephanie-wertman/sw-welcome-text-typo
Improvements to Welcome screen
2025-04-16 11:25:12 +02:00
Stephanie Wertman
9f2f279c20 Rephrase welcome message in localization.enum.ts
Rephrase welcome message to join sentence fragments and improve tone.
2025-04-15 12:06:16 -07:00
Stephanie Wertman
0568149335 Rephrase welcome message in bundle.l10n.json
Rephrase welcome message to join sentence fragments and improve tone.
2025-04-15 12:04:02 -07:00
Elio Struyf
dde0231f19 #933 - Timezone integration in DateField 2025-03-26 08:57:01 +01:00
Elio Struyf
b0dcbfd58b Feat: update changelog and add support for {{slugifiedFileName}} in slug generation #922 2025-03-24 08:44:47 +01:00
Elio Struyf
1b4e39b806 Merge pull request #924 from estruyf/beta
v10.8.0 release
2025-02-27 12:09:01 +01:00
Elio Struyf
1fa73efe11 Updated changelog 2025-02-27 11:59:28 +01:00
Elio Struyf
ddefb9f138 Copilot testing 2025-02-26 20:28:52 +01:00
Elio Struyf
5e258ac218 Feat: add file path support for slug generation and enhance slug template placeholders #922 2025-02-15 16:41:02 +01:00
Elio Struyf
d2b0228809 Feat: improve filename sanitization and add normalization for special characters #921 2025-02-14 09:35:12 +01:00
Elio Struyf
a164a849da Fix: add refresh button to media dashboard when custom scripts are defined 2025-02-12 15:01:13 +01:00
Elio Struyf
710ef136b4 Fix: improve media folder parsing on Windows and update path handling in various modules 2025-02-12 14:53:56 +01:00
Elio Struyf
d3b7f73c66 Refactor: update import paths for parseWinPath and integrate it into joinUrl function 2025-02-12 12:41:22 +01:00
Elio Struyf
ee5af88851 Fix: ensure Windows drive letters are lowercased and improve path parsing 2025-02-12 12:27:24 +01:00
Elio Struyf
482cbc3bf6 Issue: [[&mediaUrl]] placeholder in Media snippets is not relative #913 2025-02-06 11:40:22 +01:00
Elio Struyf
64f1da6355 Enhancement: auto switch to editor panel when opening a markdown file #915 2025-02-06 11:08:36 +01:00
Elio Struyf
e27adececb Issue: "panel.gitActions" option in view modes is not present in the JSON schema #909 2025-02-06 10:24:43 +01:00
Elio Struyf
b391aa3270 10.8.0 2025-02-06 10:20:39 +01:00
Elio Struyf
b58c02b6d0 Issue: Stripping underscore in default filename #914 2025-02-06 10:20:31 +01:00
Elio Struyf
88cad8caa2 Merge pull request #897 from estruyf/beta
v10.7.0 release
2024-12-31 15:28:43 +01:00
Elio Struyf
a04d56fbde 10.7.0 2024-12-31 15:24:47 +01:00
Elio Struyf
ec86b079a6 Update changelog 2024-12-31 15:23:28 +01:00
Elio Struyf
838ced0560 Remove logging 2024-12-30 11:40:12 +01:00
Elio Struyf
1c269db91d Issue: [BUG] Filtering on field with multiple values does not work as expected #895 2024-12-30 11:39:12 +01:00
Elio Struyf
1ed5131abe Change logging output 2024-12-29 13:18:30 +01:00
Elio Struyf
d944319d53 #896 - Fix glob node_modules ignore 2024-12-29 13:08:43 +01:00
Elio Struyf
146bbbf6a1 Added some verbose logging 2024-12-27 18:07:51 +01:00
Elio Struyf
7bfc72469d Merge pull request #893 from estruyf/issue/892
Add media folder actions and localization updates #892
2024-11-29 15:43:31 +01:00
Elio Struyf
324184964b Update media 2024-11-29 15:42:47 +01:00
Elio Struyf
1f6ea6ac20 Sorting on folders 2024-11-29 15:41:50 +01:00
Elio Struyf
5a45fdc94f Added progress 2024-11-29 15:19:22 +01:00
Elio Struyf
b043c22437 Add media folder actions and localization updates #892 2024-11-29 12:05:08 +01:00
Elio Struyf
b48e34ecb0 Remove CenterIcon ref 2024-11-28 18:24:54 +01:00
Elio Struyf
3bdae40ff0 Updated center layout icon 2024-11-28 18:19:42 +01:00
Elio Struyf
94df672f4c Updated yellow to warning 2024-11-28 18:18:55 +01:00
Elio Struyf
98c5b56310 Fix errors 2024-11-28 09:19:48 +01:00
Elio Struyf
f38144b8a7 Merge branch 'T3sT3ro-beta' into beta 2024-11-28 09:05:03 +01:00
Elio Struyf
231bd89619 Added density tooltip 2024-11-28 09:04:25 +01:00
Elio Struyf
4907a7aaa9 Merge branch 'beta' of github.com:T3sT3ro/vscode-front-matter into T3sT3ro-beta 2024-11-28 08:46:12 +01:00
Tooster
2dc4865581 #705 - fix styles - align checks badge padding and margin 2024-11-28 00:30:45 +01:00
Tooster
f1ae0d60cc #705 - UI tweaks and accessibility changes 2024-11-28 00:10:27 +01:00
Elio Struyf
2f76de2a28 #405 - Implement custom grouping option 2024-11-27 16:00:34 +01:00
Elio Struyf
d7282b18eb Update key 2024-11-27 15:58:57 +01:00
Elio Struyf
eb22a97198 Optimizations 2024-11-27 15:58:38 +01:00
Elio Struyf
42fe1c2887 #705 - Style changes 2024-11-25 16:49:47 +01:00
Elio Struyf
c02275d20b #705 - Style fixes 2024-11-25 13:41:43 +01:00
Elio Struyf
147823bfd0 #705 - update density 2024-11-25 12:00:07 +01:00
Elio Struyf
a7f183b6cc #705 - further improve keywords section 2024-11-25 11:59:22 +01:00
Elio Struyf
e10ee11f0e Fix title field on keywords section #705 2024-11-24 18:03:39 +01:00
Elio Struyf
fca8d260d5 Enhance SEO components with keyword management and styling updates #705 2024-11-22 09:27:46 +01:00
Elio Struyf
8cecf8d8be #705 - change order of metadata view 2024-11-21 16:40:24 +01:00
Elio Struyf
22ce41c3eb #705 - UX improvements for SEO panel 2024-11-21 16:14:25 +01:00
Elio Struyf
cb649a9a97 Add timezone formatting support and related settings #887 2024-11-14 16:30:46 +01:00
Elio Struyf
65d430b7cf Add GitHub Copilot prompt functionality #888 2024-11-13 10:24:24 +01:00
123 changed files with 5200 additions and 1259 deletions

View File

@@ -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
},

View File

@@ -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

View File

@@ -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
View File

@@ -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.
![Multilingual support](https://beta.frontmatter.codes/releases/v10.0.0/multilingual-content.png)
**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.
![Taxonomy dashboard](https://frontmatter.codes/assets/marketplace/v8.1.0/taxonomy-dashboard.png)
@@ -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)

View File

@@ -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;

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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} 語",

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}
}

View File

@@ -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)",

View File

@@ -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)",

View File

@@ -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
View 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 词是 SEO2021年 的绝对理想文章长度(设为 `-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)"
}

View File

@@ -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'

View File

@@ -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>
`;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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';

View 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} />
);
};

View File

@@ -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'),

View File

@@ -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: {

View File

@@ -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

View File

@@ -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',

View 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>
)
}
</>
);
};

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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 (

View File

@@ -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) => (

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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) {

View File

@@ -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) => (

View File

@@ -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)}

View File

@@ -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>
);
};

View File

@@ -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>
);
}

View File

@@ -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}
/>
)}
</>
);
};

View File

@@ -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" />

View File

@@ -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;

View File

@@ -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);

View 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>
);
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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);

View File

@@ -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}

View File

@@ -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;
}
});
}
}
}

View File

@@ -1,4 +1,5 @@
export enum DashboardViewType {
Grid = 1,
List
List,
Structure
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: {}
});

View File

@@ -3,7 +3,7 @@ import { atom } from 'recoil';
export const SelectedItemActionAtom = atom<
| {
path: string;
action: 'view' | 'edit' | 'delete';
action: 'view' | 'edit' | 'delete' | 'move';
}
| undefined
>({

View File

@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const SelectedStructureFolderAtom = atom<string | null>({
key: 'SelectedStructureFolderAtom',
default: null
});

View File

@@ -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';

View File

@@ -0,0 +1,9 @@
import { selector } from 'recoil';
import { SelectedStructureFolderAtom } from '..';
export const SelectedStructureFolderSelector = selector({
key: 'SelectedStructureFolderSelector',
get: ({ get }) => {
return get(SelectedStructureFolderAtom);
}
});

View File

@@ -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';

View File

@@ -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)'
);
};

View File

@@ -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();
}
};

View File

@@ -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
};
}

View File

@@ -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],

View 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;
}
}

View File

@@ -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) {

View File

@@ -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) || [],

View File

@@ -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;

View File

@@ -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);
}

View 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`;
}
}
}

View File

@@ -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();
});

View File

@@ -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));

View File

@@ -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();
}

View File

@@ -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)) || [],

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -38,3 +38,5 @@ export * from './processFmPlaceholders';
export * from './processI18nPlaceholders';
export * from './processPathPlaceholders';
export * from './processTimePlaceholders';
export * from './ContentTypeSchemaGenerator';
export * from './FrontMatterValidator';

View File

@@ -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;
};

View File

@@ -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
) || ''
);

View File

@@ -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());
}

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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'));
}
}

View File

@@ -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({

View File

@@ -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
*/

View File

@@ -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, []);

View File

@@ -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);

View File

@@ -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);
}
/**

View File

@@ -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.
*/

View File

@@ -16,6 +16,7 @@ export interface MediaInfo {
dimensions?: ISizeCalculationResult | undefined;
mimeType?: string | undefined;
mtime?: Date;
mtimeMs?: number;
ctime?: Date;
size?: number;

View File

@@ -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 {

View File

@@ -0,0 +1,7 @@
export type ScriptAction =
| 'open'
| 'copyMediaMetadata'
| 'copyMediaMetadataAndDelete'
| 'deleteMedia'
| 'fieldAction'
| 'promptCopilot';

View File

@@ -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';

View File

@@ -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

View File

@@ -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>

View File

@@ -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