diff --git a/README.md b/README.md
index 08ce0bf5..b02a9877 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,12 @@
-
+
-
+
-
+
@@ -163,21 +163,29 @@ You can open showcase issues for the following things:
-
+
## 🖤 Backers & Sponsors 👇 🤘
-
+
+
+
+
+
+
+
+
+
-
-
+
+
@@ -190,6 +198,6 @@ You can open showcase issues for the following things:
-
+
\ No newline at end of file
diff --git a/package.json b/package.json
index ab6d919a..724c6fe4 100644
--- a/package.json
+++ b/package.json
@@ -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,
diff --git a/scripts/settings-export.js b/scripts/settings-export.js
new file mode 100644
index 00000000..047b8f06
--- /dev/null
+++ b/scripts/settings-export.js
@@ -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
\ No newline at end of file
diff --git a/src/commands/Article.ts b/src/commands/Article.ts
index 9b3f3370..8baec070 100644
--- a/src/commands/Article.ts
+++ b/src/commands/Article.ts
@@ -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(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(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);
diff --git a/src/commands/Cache.ts b/src/commands/Cache.ts
index 5175f5b8..67b6b387 100644
--- a/src/commands/Cache.ts
+++ b/src/commands/Cache.ts
@@ -13,11 +13,22 @@ export class Cache {
);
}
+ public static async get(key: string, type: "workspace" | "global"): Promise {
+ const ext = Extension.getInstance();
+ const cache = await ext.getState(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");
}
diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts
index e9ee8da0..c6057b1f 100644
--- a/src/commands/Dashboard.ts
+++ b/src/commands/Dashboard.ts
@@ -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}`,
diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts
index 48f40470..d1ee95f9 100644
--- a/src/commands/Folders.ts
+++ b/src/commands/Folders.ts
@@ -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];
}
diff --git a/src/constants/ExtensionState.ts b/src/constants/ExtensionState.ts
index 76aad254..c72eac71 100644
--- a/src/constants/ExtensionState.ts
+++ b/src/constants/ExtensionState.ts
@@ -19,6 +19,10 @@ export const ExtensionState = {
}
},
+ Settings: {
+ Extends: `frontMatter:Settings:Extends`,
+ },
+
Updates: {
v7_0_0: {
dateFields: `frontMatter:Updates:v7.0.0:dateFields`
diff --git a/src/constants/settings.ts b/src/constants/settings.ts
index ca046281..740aac2b 100644
--- a/src/constants/settings.ts
+++ b/src/constants/settings.ts
@@ -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";
diff --git a/src/dashboardWebView/components/Media/Item.tsx b/src/dashboardWebView/components/Media/Item.tsx
index 6435b82b..3ade9e88 100644
--- a/src/dashboardWebView/components/Media/Item.tsx
+++ b/src/dashboardWebView/components/Media/Item.tsx
@@ -321,10 +321,14 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi
}, [media, isImageFile, isVideoFile, isAudioFile]);
const renderMedia = useMemo(() => {
- if (isVideoFile || isAudioFile) {
+ if (isAudioFile) {
return null;
}
+ if (isVideoFile) {
+ return ;
+ }
+
if (isImageFile) {
return
;
}
diff --git a/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx b/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx
index bb5f67c1..4f2ce1e1 100644
--- a/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx
+++ b/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx
@@ -66,7 +66,7 @@ export const StepsToGetStarted: React.FunctionComponent
{
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. Start by clicking on this action.>,
+ description: <>Initialize the project will create the required files and folders for using the Front Matter CMS. Start by clicking on this action.>,
status: settings.initialized ? Status.Completed : Status.NotStarted,
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
},
diff --git a/src/explorerView/ExplorerView.ts b/src/explorerView/ExplorerView.ts
index 58966fdb..115330ee 100644
--- a/src/explorerView/ExplorerView.ts
+++ b/src/explorerView/ExplorerView.ts
@@ -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}`,
diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts
index 258b285b..64c16559 100644
--- a/src/helpers/ArticleHelper.ts
+++ b/src/helpers/ArticleHelper.ts
@@ -331,17 +331,10 @@ export class ArticleHelper {
public static async createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): Promise {
FrontMatterParser.currentContent = null;
- let prefix = Settings.get(SETTING_TEMPLATES_PREFIX);
const fileType = Settings.get(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(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;
diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts
index 351d8493..85153a81 100644
--- a/src/helpers/ContentType.ts
+++ b/src/helpers/ContentType.ts
@@ -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";
diff --git a/src/helpers/CustomScript.ts b/src/helpers/CustomScript.ts
index e80c893c..f3ad84e3 100644
--- a/src/helpers/CustomScript.ts
+++ b/src/helpers/CustomScript.ts
@@ -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 {
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;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/helpers/ImageHelper.ts b/src/helpers/ImageHelper.ts
index 78a8925f..a6c962c6 100644
--- a/src/helpers/ImageHelper.ts
+++ b/src/helpers/ImageHelper.ts
@@ -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
diff --git a/src/helpers/SettingsHelper.ts b/src/helpers/SettingsHelper.ts
index 04e69e03..610991f6 100644
--- a/src/helpers/SettingsHelper.ts
+++ b/src/helpers/SettingsHelper.ts
@@ -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(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(settingName: string, fieldName: string, configJson: any): void {
+ private static updateGlobalConfigArraySetting(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 {
+ 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;
+ }
}
\ No newline at end of file
diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts
index b438fcff..ebd13131 100644
--- a/src/models/PanelSettings.ts
+++ b/src/models/PanelSettings.ts
@@ -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 {
diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts
index 0120e4b1..b7091173 100644
--- a/src/services/PagesParser.ts
+++ b/src/services/PagesParser.ts
@@ -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() || "";
}
}
}
diff --git a/src/utils/fetchWithTimeout.ts b/src/utils/fetchWithTimeout.ts
new file mode 100644
index 00000000..dbcc8da0
--- /dev/null
+++ b/src/utils/fetchWithTimeout.ts
@@ -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}`);
+ }
+}
\ No newline at end of file
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 9b5f7e53..09cf93fe 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,5 +1,6 @@
export * from './copyFileAsync';
export * from './existsAsync';
+export * from './fetchWithTimeout';
export * from './fieldWhenClause';
export * from './mkdirAsync';
export * from './readFileAsync';