init: sync with dev

This commit is contained in:
Pongstr
2023-02-01 16:13:57 +02:00
25 changed files with 513 additions and 112 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
run: node scripts/beta-release.js $GITHUB_RUN_ID
- name: Publish
run: npx vsce publish -p ${{ secrets.VSCE_PAT }} --baseImagesUrl https://raw.githubusercontent.com/estruyf/vscode-front-matter/dev
run: npx @vscode/vsce publish -p ${{ secrets.VSCE_PAT }} --baseImagesUrl https://raw.githubusercontent.com/estruyf/vscode-front-matter/dev
- name: Publish to open-vsx.org
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_PAT }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
run: node scripts/main-release.js
- name: Publish
run: npx vsce publish -p ${{ secrets.VSCE_PAT }}
run: npx @vscode/vsce publish -p ${{ secrets.VSCE_PAT }}
- name: Publish to open-vsx.org
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_PAT }}
+22
View File
@@ -1,5 +1,27 @@
# Change Log
## [8.3.0] - 2022-xx-xx
### ✨ New features
- [#407](https://github.com/estruyf/vscode-front-matter/issues/407): External config support
### 🎨 Enhancements
- [#474](https://github.com/estruyf/vscode-front-matter/issues/474): Allow to define the file prefix on content types
- [#484](https://github.com/estruyf/vscode-front-matter/issues/484): Support for overriding scripts per environment type
- [#494](https://github.com/estruyf/vscode-front-matter/issues/494): Support for external image URLs in previews
- [#497](https://github.com/estruyf/vscode-front-matter/issues/497): Support for movie media previews in the content dashboard
### ⚡️ Optimizations
### 🐞 Fixes
- [#469](https://github.com/estruyf/vscode-front-matter/issues/469): Fix for using the root folder as content folder
- [#470](https://github.com/estruyf/vscode-front-matter/issues/470): Fix `initialize project` dashboard description
- [#482](https://github.com/estruyf/vscode-front-matter/issues/482): Update the description when you want to overwrite the default content type description
- [#493](https://github.com/estruyf/vscode-front-matter/issues/493): Fix an issue where a custom placeholder value is replaced by an `array` instead of a `string`
## [8.2.0] - 2022-12-08 - [Release notes](https://beta.frontmatter.codes/updates/v8.2.0)
### ✨ New features
+3 -3
View File
@@ -10,12 +10,12 @@
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter" title="Check it out on the Visual Studio Marketplace">
<img src="https://vsmarketplacebadge.apphb.com/version/eliostruyf.vscode-front-matter.svg" alt="Visual Studio Marketplace" style="display: inline-block" />
<img src="https://vsmarketplacebadges.dev/version/eliostruyf.vscode-front-matter.svg" alt="Visual Studio Marketplace" style="display: inline-block" />
</a>
<img src="https://vsmarketplacebadge.apphb.com/installs/eliostruyf.vscode-front-matter.svg" alt="Number of installs" style="display: inline-block;margin-left:10px" />
<img src="https://vsmarketplacebadges.dev/installs/eliostruyf.vscode-front-matter.svg" alt="Number of installs" style="display: inline-block;margin-left:10px" />
<img src="https://vsmarketplacebadge.apphb.com/rating/eliostruyf.vscode-front-matter.svg" alt="Ratings" style="display: inline-block;margin-left:10px" />
<img src="https://vsmarketplacebadges.dev/rating/eliostruyf.vscode-front-matter.svg" alt="Ratings" style="display: inline-block;margin-left:10px" />
<a href="https://github.com/sponsors/estruyf" title="Become a sponsor" style="margin-left:10px">
<img src="https://img.shields.io/github/sponsors/estruyf?color=%23CE2E7C&logo=github&style=flat" alt="Sponsor the project" style="display: inline-block" />
+16 -8
View File
@@ -8,12 +8,12 @@
<p align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter" title="Check it out on the Visual Studio Marketplace">
<img src="https://vsmarketplacebadge.apphb.com/version/eliostruyf.vscode-front-matter.svg" alt="Visual Studio Marketplace" style="display: inline-block" />
<img src="https://vsmarketplacebadges.dev/version/eliostruyf.vscode-front-matter.svg" alt="Visual Studio Marketplace" style="display: inline-block" />
</a>
<img src="https://vsmarketplacebadge.apphb.com/installs/eliostruyf.vscode-front-matter.svg" alt="Number of installs" style="display: inline-block;margin-left:10px" />
<img src="https://vsmarketplacebadges.dev/installs/eliostruyf.vscode-front-matter.svg" alt="Number of installs" style="display: inline-block;margin-left:10px" />
<img src="https://vsmarketplacebadge.apphb.com/rating/eliostruyf.vscode-front-matter.svg" alt="Ratings" style="display: inline-block;margin-left:10px" />
<img src="https://vsmarketplacebadges.dev/rating/eliostruyf.vscode-front-matter.svg" alt="Ratings" style="display: inline-block;margin-left:10px" />
<a href="https://github.com/sponsors/estruyf" title="Become a sponsor" style="margin-left:10px">
<img src="https://img.shields.io/github/sponsors/estruyf?color=%23CE2E7C&logo=github&style=flat" alt="Sponsor the project" style="display: inline-block" />
@@ -163,21 +163,29 @@ You can open showcase issues for the following things:
<p align="center">
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" alt="Front Matter contributors" />
</a>
</p>
## 🖤 Backers & Sponsors 👇 🤘
<p align="center">
<img src="https://frontmatter.codes/api/img-sponsors" />
<img src="https://frontmatter.codes/api/img-sponsors" alt="Front Matter sponsors" />
</p>
<br />
<p align="center" title="Powered by Vercel">
<a href="https://vercel.com/?utm_source=vscode-frontmatter&utm_campaign=oss">
<img src="https://frontmatter.codes/assets/sponsors/powered-by-vercel.png" alt="Powered by Vercel" />
</a>
</p>
<br />
<p align="center">
<a href="https://vercel.com/?utm_source=vscode-frontmatter&utm_campaign=oss">
<img src="https://frontmatter.codes/assets/sponsors/powered-by-vercel.png" />
<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>
@@ -190,6 +198,6 @@ You can open showcase issues for the following things:
<p align="center">
<a href="https://visitorbadge.io">
<img src="https://api.visitorbadge.io/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" />
<img src="https://api.visitorbadge.io/api/VisitorHit?user=estruyf&repo=vscode-front-matter&countColor=%23F05450&labelColor=%230E131F" height="25px" alt="Front Matter visitors" />
</a>
</p>
+40 -1
View File
@@ -3,7 +3,7 @@
"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": "8.2.0",
"version": "8.3.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -79,6 +79,14 @@
"configuration": {
"title": "Front Matter: use frontmatter.json for shared team settings",
"properties": {
"frontMatter.extends": {
"type": "array",
"markdownDescription": "Specify the list of paths/URLs to extend the Front Matter CMS config. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.extends)",
"default": [],
"items": {
"type": "string"
}
},
"frontMatter.content.autoUpdateDate": {
"type": "boolean",
"default": false,
@@ -454,6 +462,30 @@
"type": "boolean",
"description": "Hide the action from the UI",
"default": false
},
"environments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"macos",
"linux",
"windows"
],
"description": "The environment type for which the script needs to be used"
},
"script": {
"type": "string",
"description": "Path to the script to execute"
},
"command": {
"$ref": "#scriptCommand"
}
}
}
}
},
"additionalProperties": false,
@@ -1260,6 +1292,13 @@
"type": "string",
"default": "",
"description": "An optional post script that can be used after new content creation."
},
"filePrefix": {
"type": [
"null",
"string"
],
"description": "Defines a prefix for the file name."
}
},
"additionalProperties": false,
+43
View File
@@ -0,0 +1,43 @@
const packageJson = require('../package.json');
for (const key of Object.keys(packageJson.contributes.configuration.properties)) {
const type = packageJson.contributes.configuration.properties[key].type;
if (type.includes('object') || type.includes('array')) {
console.log(`${key} - ${packageJson.contributes.configuration.properties[key].type}`);
}
}
// TO IGNORE
// frontMatter.extends - array
// frontMatter.dashboard.mediaSnippet - array
// TO PROCESS AS A WHOLE OBJECT
// frontMatter.content.draftField - object
// frontMatter.content.supportedFileTypes - array
// frontMatter.global.notifications - array
// frontMatter.global.disabledNotificaitons - array
// frontMatter.media.supportedMimeTypes - array
// frontMatter.taxonomy.commaSeparatedFields - array
// MERGE ARRAYS
// frontMatter.taxonomy.categories - array
// frontMatter.taxonomy.tags - array
// frontMatter.taxonomy.noPropertyValueQuotes - array
// PROCESS ITEM BY ITEM
// frontMatter.custom.scripts - array - id
// frontMatter.taxonomy.contentTypes - array,null - name
// frontMatter.data.files - array - id
// frontMatter.data.folders - array - id
// frontMatter.data.types - array - id
// frontMatter.content.pageFolders - array - path
// frontMatter.content.placeholders - array - id
// frontMatter.content.sorting - array - id
// frontMatter.global.modes - array - id
// frontMatter.taxonomy.fieldGroups - array - id
// frontMatter.taxonomy.customTaxonomy - array - id
// frontMatter.content.snippets - object
+4 -8
View File
@@ -198,7 +198,6 @@ export class Article {
Telemetry.send(TelemetryEvent.generateSlug);
const updateFileName = Settings.get(SETTING_SLUG_UPDATE_FILE_NAME) as string;
let filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const editor = vscode.window.activeTextEditor;
if (!editor) {
@@ -210,13 +209,10 @@ export class Article {
return;
}
// Retrieve the file prefix from the folder
const filePrefixOnFolder = Folders.getFilePrefixBeFilePath(editor.document.uri.fsPath);
if (typeof filePrefixOnFolder !== "undefined") {
filePrefix = filePrefixOnFolder;
}
let filePrefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const contentType = ArticleHelper.getContentType(article.data);
filePrefix = ArticleHelper.getFilePrefix(editor.document.uri.fsPath, contentType);
const titleField = "title";
const articleTitle: string = article.data[titleField];
const slugInfo = Article.generateSlug(articleTitle);
@@ -259,7 +255,7 @@ export class Article {
let newFileName = `${slugName}${ext}`;
if (filePrefix && typeof filePrefix === "string") {
newFileName = `${format(new Date(), DateHelper.formatUpdate(filePrefix) as string)}-${newFileName}`;
newFileName = `${filePrefix}-${newFileName}`;
}
const newPath = editor.document.uri.fsPath.replace(fileName, newFileName);
+11
View File
@@ -13,11 +13,22 @@ export class Cache {
);
}
public static async get<T>(key: string, type: "workspace" | "global"): Promise<T | undefined> {
const ext = Extension.getInstance();
const cache = await ext.getState<T>(key, type);
return cache || undefined;
}
public static async set(key: string, data: any, type: "workspace" | "global") {
await Extension.getInstance().setState(key, data, "workspace");
}
private static async clear() {
const ext = Extension.getInstance();
await ext.setState(ExtensionState.Dashboard.Pages.Cache, undefined, "workspace");
await ext.setState(ExtensionState.Dashboard.Pages.Index, undefined, "workspace");
await ext.setState(ExtensionState.Settings.Extends, undefined, "workspace");
Notifications.info("Cache cleared");
}
+2 -1
View File
@@ -199,7 +199,8 @@ export class Dashboard {
const csp = [
`default-src 'none';`,
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'`,
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline' https://*`,
`media-src ${`vscode-file://vscode-app`} ${webView.cspSource} 'self' 'unsafe-inline' https://*`,
`script-src ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`} 'unsafe-eval'`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
`font-src ${webView.cspSource}`,
+7 -2
View File
@@ -253,14 +253,19 @@ export class Folders {
try {
let projectStart = parseWinPath(folder.path).replace(wsFolder, "");
if (projectStart) {
if (typeof projectStart === 'string') {
projectStart = projectStart.replace(/\\/g, '/');
projectStart = projectStart.startsWith('/') ? projectStart.substring(1) : projectStart;
let files: Uri[] = [];
for (const fileType of (supportedFiles || DEFAULT_FILE_TYPES)) {
const filePath = join(projectStart, folder.excludeSubdir ? '/' : '**', `*${fileType.startsWith('.') ? '' : '.'}${fileType}`);
let filePath = join(projectStart, folder.excludeSubdir ? '/' : '**', `*${fileType.startsWith('.') ? '' : '.'}${fileType}`);
if (projectStart === '' && folder.excludeSubdir) {
filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`;
}
const foundFiles = await workspace.findFiles(filePath, '**/node_modules/**');
files = [...files, ...foundFiles];
}
+4
View File
@@ -19,6 +19,10 @@ export const ExtensionState = {
}
},
Settings: {
Extends: `frontMatter:Settings:Extends`,
},
Updates: {
v7_0_0: {
dateFields: `frontMatter:Updates:v7.0.0:dateFields`
+2
View File
@@ -2,6 +2,8 @@ export const EXTENSION_NAME = "Front Matter";
export const CONFIG_KEY = "frontMatter";
export const SETTING_EXTENDS = "extends";
export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications";
export const SETTING_GLOBAL_NOTIFICATIONS_DISABLED = "global.disabledNotifications";
export const SETTING_GLOBAL_MODES = "global.modes";
@@ -321,10 +321,14 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}, [media, isImageFile, isVideoFile, isAudioFile]);
const renderMedia = useMemo(() => {
if (isVideoFile || isAudioFile) {
if (isAudioFile) {
return null;
}
if (isVideoFile) {
return <video src={media.vsPath} className="mx-auto object-cover" controls muted />;
}
if (isImageFile) {
return <img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />;
}
@@ -66,7 +66,7 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
{
id: `welcome-init`,
name: 'Initialize project',
description: <>Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.</>,
description: <>Initialize the project will create the required files and folders for using the Front Matter CMS. <b>Start by clicking on this action</b>.</>,
status: settings.initialized ? Status.Completed : Status.NotStarted,
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
},
+1 -1
View File
@@ -173,7 +173,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const csp = [
`default-src 'none';`,
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'`,
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline' https://*`,
`script-src 'unsafe-eval' ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`}`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
`font-src ${webView.cspSource}`,
+35 -10
View File
@@ -331,17 +331,10 @@ export class ArticleHelper {
public static async createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): Promise<string | undefined> {
FrontMatterParser.currentContent = null;
let prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const fileType = Settings.get<string>(SETTING_CONTENT_DEFAULT_FILETYPE);
const filePrefixOnFolder = Folders.getFilePrefixByFolderPath(folderPath);
if (typeof filePrefixOnFolder !== "undefined") {
prefix = filePrefixOnFolder;
}
if (prefix && typeof prefix === "string") {
prefix = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}`;
}
let prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
prefix = ArticleHelper.getFilePrefix(folderPath, contentType);
// Name of the file or folder to create
let sanitizedName = ArticleHelper.sanitize(titleValue);
@@ -379,6 +372,36 @@ export class ArticleHelper {
return newFilePath;
}
/**
* Retrieve the file prefix
* @param filePath
* @param contentType
* @returns
*/
public static getFilePrefix(filePath?: string, contentType?: ContentType): string | undefined {
let prefix = undefined;
// Retrieve the file prefix from the folder
if (filePath) {
const filePrefixOnFolder = Folders.getFilePrefixByFolderPath(filePath);
if (typeof filePrefixOnFolder !== "undefined") {
prefix = filePrefixOnFolder;
}
}
// Retrieve the file prefix from the content type
if (contentType && typeof contentType.filePrefix !== "undefined") {
prefix = contentType.filePrefix;
}
// Process the prefix date formatting
if (prefix && typeof prefix === "string") {
prefix = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}`;
}
return prefix;
}
/**
* Update placeholder values in the front matter content
* @param data
@@ -437,7 +460,9 @@ export class ArticleHelper {
// Do nothing
}
} else {
output = output.split("\n");
if (output.includes("\n")) {
output = output.split("\n");
}
}
placeHolderValue = output;
+1 -1
View File
@@ -121,7 +121,7 @@ export class ContentType {
const override = await window.showQuickPick(["Yes", "No"], {
title: "Override default content type",
placeHolder: "Do you want to override the default content type?",
placeHolder: "Do you want to overwrite the default content type configuration with the fields used in the current field?",
ignoreFocusOut: true
});
const overrideBool = override === "Yes";
+50 -6
View File
@@ -1,10 +1,10 @@
import { Settings } from './SettingsHelper';
import { CommandType } from './../models/PanelSettings';
import { CommandType, EnvironmentType } from './../models/PanelSettings';
import { CustomScript as ICustomScript, ScriptType } from '../models/PanelSettings';
import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
import { ArticleHelper, Logger, Telemetry } from '.';
import { Folders } from '../commands/Folders';
import { exec } from 'child_process';
import { Folders, WORKSPACE_PLACEHOLDER } from '../commands/Folders';
import { exec, execSync } from 'child_process';
import * as os from 'os';
import { join } from 'path';
import { Notifications } from './Notifications';
@@ -186,7 +186,8 @@ export class CustomScript {
try {
let articleData = "";
if (os.type() === "Windows_NT") {
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
const jsonData = JSON.stringify(article?.data)
articleData = `'${jsonData.replace(/"/g, `\"`)}'`;
} else {
articleData = JSON.stringify(article?.data).replace(/'/g, "%27");
articleData = `'${articleData}'`;
@@ -269,14 +270,41 @@ export class CustomScript {
*/
public static async executeScript(script: ICustomScript, wsPath: string, args: string): Promise<string> {
return new Promise(async (resolve, reject) => {
const osType = os.type();
// Check the command to use
let command = script.nodeBin || "node";
if (script.command && script.command !== CommandType.Node) {
command = script.command;
}
const scriptPath = join(wsPath, script.script);
let scriptPath = join(wsPath, script.script);
if (script.script.includes(WORKSPACE_PLACEHOLDER)) {
scriptPath = Folders.getAbsFilePath(script.script);
}
// Check if there is an environments overwrite required
if (script.environments) {
let crntType: EnvironmentType | null = null;
if (osType === "Windows_NT") {
crntType = "windows"
} else if (osType === "Darwin") {
crntType = "macos"
} else {
crntType = "linux"
}
const environment = script.environments.find(e => e.type === crntType);
if (environment && environment.script && environment.command) {
if (await CustomScript.validateCommand(environment.command)) {
command = environment.command;
scriptPath = join(wsPath, environment.script);
if (environment.script.includes(WORKSPACE_PLACEHOLDER)) {
scriptPath = Folders.getAbsFilePath(environment.script);
}
}
}
}
if (!await existsAsync(scriptPath)) {
reject(new Error(`Script not found: ${scriptPath}`));
@@ -301,4 +329,20 @@ export class CustomScript {
});
});
}
/**
* Validate if the command is exists
* @param command
* @returns
*/
private static async validateCommand(command: string) {
try {
execSync(command);
return true;
} catch (e) {
Logger.error(`Invalid command: ${command}`);
return false;
}
}
}
+4 -4
View File
@@ -27,14 +27,14 @@ export class ImageHelper {
if (Array.isArray(value)) {
previewUri = value.map(v => ({
original: v,
absPath: ImageHelper.relToAbs(filePath, v)
absPath: v.startsWith("http") ? v : ImageHelper.relToAbs(filePath, v)
}));
}
} else {
if (typeof value === "string") {
return {
original: value,
absPath: ImageHelper.relToAbs(filePath, value)
absPath: value.startsWith("http") ? value : ImageHelper.relToAbs(filePath, value)
};
}
}
@@ -122,12 +122,12 @@ export class ImageHelper {
if (field.multiple && imageData instanceof Array) {
const preview = imageData.map(preview => preview && preview.absPath ? ({
...preview,
webviewUrl: panel.getWebview()?.asWebviewUri(preview.absPath).toString()
webviewUrl: typeof preview.absPath === "string" ? preview.absPath : panel.getWebview()?.asWebviewUri(preview.absPath).toString()
}) : null);
parentObj[field.name] = preview || [];
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
const preview = panel.getWebview()?.asWebviewUri(imageData.absPath);
const preview = typeof imageData.absPath === "string" ? imageData.absPath : panel.getWebview()?.asWebviewUri(imageData.absPath);
parentObj[field.name] = {
...imageData,
webviewUrl: preview ? preview.toString() : null
+208 -40
View File
@@ -3,8 +3,8 @@ import { Telemetry } from './Telemetry';
import { Notifications } from './Notifications';
import { commands, Uri, workspace, window } from 'vscode';
import * as vscode from 'vscode';
import { ContentFolder, ContentType, CustomPlaceholder, CustomTaxonomy, DataFile, DataFolder, DataType, TaxonomyType } from '../models';
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, ExtensionState, SETTING_TAXONOMY_CUSTOM, TelemetryEvent, COMMAND_NAME, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_SNIPPETS, SETTING_CONTENT_PLACEHOLDERS, SETTING_CUSTOM_SCRIPTS, SETTING_DATA_FILES, SETTING_DATA_TYPES, SETTING_DATA_FOLDERS } from '../constants';
import { ContentType, CustomTaxonomy, TaxonomyType } from '../models';
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, ExtensionState, SETTING_TAXONOMY_CUSTOM, TelemetryEvent, COMMAND_NAME, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_SNIPPETS, SETTING_CONTENT_PLACEHOLDERS, SETTING_CUSTOM_SCRIPTS, SETTING_DATA_FILES, SETTING_DATA_TYPES, SETTING_DATA_FOLDERS, SETTING_EXTENDS, SETTING_CONTENT_SORTING, SETTING_GLOBAL_MODES, SETTING_TAXONOMY_FIELD_GROUPS, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_GLOBAL_NOTIFICATIONS, SETTING_GLOBAL_NOTIFICATIONS_DISABLED, SETTING_MEDIA_SUPPORTED_MIMETYPES, SETTING_COMMA_SEPARATED_FIELDS, SETTING_REMOVE_QUOTES } from '../constants';
import { Folders } from '../commands/Folders';
import { join, basename, dirname, parse } from 'path';
import { existsSync } from 'fs';
@@ -12,7 +12,8 @@ import { Extension } from './Extension';
import { debounceCallback } from './DebounceCallback';
import { Logger } from './Logger';
import * as jsoncParser from 'jsonc-parser';
import { existsAsync, readFileAsync, writeFileAsync } from '../utils';
import { existsAsync, fetchWithTimeout, readFileAsync, writeFileAsync } from '../utils';
import { Cache } from '../commands';
export class Settings {
public static globalFile = "frontmatter.json";
@@ -432,6 +433,9 @@ export class Settings {
Settings.globalConfig = undefined;
}
// Check if the config got external configs
await Settings.processExternalConfig();
// Read the files from the config folder
let configFiles = await workspace.findFiles(`**/${Settings.globalConfigFolder}/**/*.json`);
if (configFiles.length === 0) {
@@ -440,7 +444,6 @@ export class Settings {
// Sort the files by fsPath
configFiles = configFiles.sort((a, b) => a.fsPath.localeCompare(b.fsPath));
for await (const configFile of configFiles) {
await Settings.processConfigFile(configFile);
}
@@ -453,6 +456,25 @@ export class Settings {
Settings.readConfigPromise = undefined;
}
/**
* Process the external configs
*/
private static async processExternalConfig() {
const extendsConfigName = `${CONFIG_KEY}.${SETTING_EXTENDS}`;
if (!Settings.globalConfig || !Settings.globalConfig[extendsConfigName]) {
return;
}
const originalConfig = Object.assign({}, Settings.globalConfig);
const extendsConfig: string[] = Settings.globalConfig[extendsConfigName];
for (const externalConfig of extendsConfig) {
if (externalConfig.endsWith(`.json`)) {
const config = await Settings.getExternalConfig(externalConfig);
await Settings.extendConfig(config, originalConfig);
}
}
}
/**
* Process the config file
* @param configFile
@@ -482,45 +504,137 @@ export class Settings {
Settings.globalConfig = {};
}
// Array settings
if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CUSTOM_SCRIPTS)) {
const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] || [];
Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] = [...crntValue, configJson];
}
// Content types
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CONTENT_TYPES)) {
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CONTENT_TYPES, "name", configJson);
}
// Data files
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FILES)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FILES, "id", configJson);
}
// Data folders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FOLDERS, "id", configJson);
}
// Data types
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_TYPES)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_TYPES, "id", configJson);
}
// Page folders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PAGE_FOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PAGE_FOLDERS, "path", configJson);
}
// Placeholders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PLACEHOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PLACEHOLDERS, "id", configJson);
}
// Object settings
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_SNIPPETS)) {
Settings.updateGlobalConfigObjectByNameSetting(SETTING_CONTENT_SNIPPETS, configFilePath, configJson, filePath);
}
Settings.updateGlobalConfigSetting(relSettingName, configJson, configFilePath, filePath);
} catch (e) {
Logger.error(`Error reading config file: ${configFile.fsPath}`);
Logger.error((e as Error).message);
}
}
/**
* Extend the config with external config data
* @param config
* @param originalConfig The original config data is used to make sure we don't override settings coming from the fontmatter.json file.
* @returns
*/
private static async extendConfig(config: any, originalConfig: any) {
if (!config) {
return;
}
// We need to loop through the config to make sure the objects and arrays are merged
for (const key in config) {
if (config.hasOwnProperty(key)) {
const value = config[key];
const settingName = key.replace(`${CONFIG_KEY}.`, '');
if (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean') {
if (typeof originalConfig[key] === 'undefined') {
Settings.globalConfig[key] = value;
}
}
// Objects and arrays to override
else if (settingName === SETTING_CONTENT_DRAFT_FIELD ||
settingName === SETTING_CONTENT_SUPPORTED_FILETYPES ||
settingName === SETTING_GLOBAL_NOTIFICATIONS ||
settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED ||
settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES ||
settingName === SETTING_COMMA_SEPARATED_FIELDS) {
if (typeof originalConfig[key] === 'undefined') {
Settings.globalConfig[key] = value;
}
}
else if (typeof value === 'object' && value !== null) {
// Check if array
if (Array.isArray(value)) {
if (settingName === SETTING_TAXONOMY_CATEGORIES ||
settingName === SETTING_TAXONOMY_TAGS ||
settingName === SETTING_REMOVE_QUOTES) {
// Merge the arrays
Settings.globalConfig[key] = [...(Settings.globalConfig[key] || []), ...(originalConfig[key] || []), ...value];
// Filter out the doubles
Settings.globalConfig[key] = Settings.globalConfig[key].filter((item: any, index: number) => {
return Settings.globalConfig[key].indexOf(item) === index;
}, Settings.globalConfig[key]);
} else {
for (const item of value) {
Settings.updateGlobalConfigSetting(settingName, item);
}
}
} else if (settingName === SETTING_CONTENT_SNIPPETS) {
for (const itemKey in value) {
const crntValue = Settings.globalConfig[key] || {};
if (!crntValue[itemKey]) {
Settings.globalConfig[key] = { ...crntValue, ...{ [itemKey]: value[itemKey] } };
}
}
}
}
}
}
}
/**
* Update the global config array/object settings
* @param relSettingName
* @param configJson
*/
private static updateGlobalConfigSetting<T>(relSettingName: string, configJson: any, configFilePath?: string, filePath?: string): void {
// Custom scripts
if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CUSTOM_SCRIPTS)) {
// const crntValue = Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] || [];
// Settings.globalConfig[`${CONFIG_KEY}.${SETTING_CUSTOM_SCRIPTS}`] = [...crntValue, configJson];
Settings.updateGlobalConfigArraySetting(SETTING_CUSTOM_SCRIPTS, "id", configJson, "script");
}
// Content types
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CONTENT_TYPES)) {
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CONTENT_TYPES, "name", configJson);
}
// Data files
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FILES)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FILES, "id", configJson);
}
// Data folders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_FOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_FOLDERS, "id", configJson);
}
// Data types
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_DATA_TYPES)) {
Settings.updateGlobalConfigArraySetting(SETTING_DATA_TYPES, "id", configJson);
}
// Page folders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PAGE_FOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PAGE_FOLDERS, "path", configJson);
}
// Placeholders
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_PLACEHOLDERS)) {
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_PLACEHOLDERS, "id", configJson);
}
// Sorting
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_SORTING)) {
Settings.updateGlobalConfigArraySetting(SETTING_CONTENT_SORTING, "id", configJson);
}
// Modes
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_GLOBAL_MODES)) {
Settings.updateGlobalConfigArraySetting(SETTING_GLOBAL_MODES, "id", configJson);
}
// Field groups
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_FIELD_GROUPS)) {
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_FIELD_GROUPS, "id", configJson);
}
// Custom taxonomy
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_TAXONOMY_CUSTOM)) {
Settings.updateGlobalConfigArraySetting(SETTING_TAXONOMY_CUSTOM, "id", configJson);
}
// Snippets
else if (Settings.isEqualOrStartsWith(relSettingName, SETTING_CONTENT_SNIPPETS) && configFilePath && filePath) {
Settings.updateGlobalConfigObjectByNameSetting(SETTING_CONTENT_SNIPPETS, configFilePath, configJson, filePath);
}
}
/**
* Check if the setting name is equal or starts with the reference setting name
* @param value
@@ -540,11 +654,18 @@ export class Settings {
* @param fieldName
* @param configJson
*/
private static updateGlobalConfigArraySetting<T>(settingName: string, fieldName: string, configJson: any): void {
private static updateGlobalConfigArraySetting<T>(settingName: string, fieldName: string, configJson: any, fallbackFieldName?: string): void {
const crntValue: T[] = Settings.globalConfig[`${CONFIG_KEY}.${settingName}`] || [];
// Check if folder is already added
const itemIdx = crntValue.findIndex((item: any) => item[fieldName] === configJson[fieldName]);
const itemIdx = crntValue.findIndex((item: any) => {
if (typeof item[fieldName] !== "undefined") {
return item[fieldName] === configJson[fieldName];
} else if (fallbackFieldName && typeof item[fallbackFieldName] !== "undefined") {
return item[fallbackFieldName] === configJson[fallbackFieldName];
} else {
return false;
}
});
if (itemIdx === -1) {
crntValue.push(configJson);
}
@@ -606,4 +727,51 @@ export class Settings {
l();
});
}
/**
* Retrieve the external configuration
* @param configPath
* @returns
*/
private static async getExternalConfig(configPath: string): Promise<any> {
let config: any = undefined;
if (configPath.startsWith('https://')) {
try {
let cachedResponse = await Cache.get<{[config: string]: { expires: number, data: any }}>(ExtensionState.Settings.Extends, "workspace");
if (cachedResponse && cachedResponse[configPath] && cachedResponse[configPath].expires > new Date().getTime()) {
config = cachedResponse[configPath].data;
} else {
const response = await fetchWithTimeout(configPath, { method: 'GET' });
if (response.ok) {
config = await response.json();
if (!cachedResponse) {
cachedResponse = {};
}
cachedResponse[configPath] = {
expires: (new Date(new Date().getTime() + (1000 * 60 * 10))).getTime(),
data: config
};
await Cache.set(ExtensionState.Settings.Extends, cachedResponse, "workspace");
}
}
} catch (e) {
Logger.error(`Error fetching external config "${configPath}".`);
}
} else {
const absConfigPath = join(Folders.getWorkspaceFolder()?.fsPath || '', configPath);
if (await existsAsync(absConfigPath)) {
const configTxt = await readFileAsync(absConfigPath, 'utf8');
config = jsoncParser.parse(configTxt);
} else {
Logger.error(`External config "${configPath}" not found.`);
}
}
return config;
}
}
+10
View File
@@ -49,6 +49,7 @@ export interface ContentType {
pageBundle?: boolean;
template?: string;
postScript?: string;
filePrefix?: string;
}
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block" | "file" | "dataFile" | "list" | "slug" | "divider" | "heading";
@@ -148,6 +149,15 @@ export interface CustomScript {
type?: ScriptType;
command?: CommandType | string;
hidden?: boolean;
environments?: EnvironmentScript[];
}
export type EnvironmentType = "windows" | "macos" | "linux";
export interface EnvironmentScript {
type: EnvironmentType;
script: string;
command: CommandType | string;
}
export interface PreviewSettings {
+28 -23
View File
@@ -238,31 +238,36 @@ export class PagesParser {
// Revalidate as the array could have been empty
if (fieldValue) {
let staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
// Check if the value already starts with https - if that is the case, it is an external image
if (fieldValue.startsWith("http")) {
page.fmPreviewImage = fieldValue;
} else {
let staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
const crntFilePath = parseWinPath(filePath)
const pathWithoutExtension = crntFilePath.replace(extname(crntFilePath), '');
staticPath = join(pathWithoutExtension, fieldValue);
}
const contentFolderPath = join(dirname(filePath), fieldValue);
let previewUri = null;
if (await existsAsync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (await existsAsync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
let previewPath = Dashboard.getWebview()?.asWebviewUri(previewUri);
if (!previewPath) {
previewPath = PagesParser.getWebviewUri(previewUri);
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
const crntFilePath = parseWinPath(filePath)
const pathWithoutExtension = crntFilePath.replace(extname(crntFilePath), '');
staticPath = join(pathWithoutExtension, fieldValue);
}
const contentFolderPath = join(dirname(filePath), fieldValue);
let previewUri = null;
if (await existsAsync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (await existsAsync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
let previewPath = Dashboard.getWebview()?.asWebviewUri(previewUri);
if (!previewPath) {
previewPath = PagesParser.getWebviewUri(previewUri);
}
page["fmPreviewImage"] = previewPath?.toString() || "";
}
page["fmPreviewImage"] = previewPath?.toString() || "";
}
}
}
+13
View File
@@ -0,0 +1,13 @@
import fetch from 'node-fetch';
export const fetchWithTimeout = async (url: string, options: any, timeout = 5000) => {
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(id);
return response;
} catch (error) {
throw new Error(`Request timed out: ${url}`);
}
}
+1
View File
@@ -1,5 +1,6 @@
export * from './copyFileAsync';
export * from './existsAsync';
export * from './fetchWithTimeout';
export * from './fieldWhenClause';
export * from './mkdirAsync';
export * from './readFileAsync';