diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c5bb497..56be5a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [#151](https://github.com/estruyf/vscode-front-matter/issues/151): Detect which site-generator or framework is used - [#152](https://github.com/estruyf/vscode-front-matter/issues/152): Automatically set setting based on the used site-generator or framework +- [#154](https://github.com/estruyf/vscode-front-matter/issues/154): Bulk script support added ### 🐞 Fixes diff --git a/package.json b/package.json index a74b7c0b..45959f04 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,22 @@ "nodeBin": { "type": "string", "description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use." + }, + "bulk": { + "type": "boolean", + "description": "Run the script for all content files" + }, + "output": { + "type": "string", + "enum": [ + "editor", + "notification" + ], + "description": "Define where you want to output your script output. Default is a notification, but you can specify to show it in an editor panel." + }, + "outputType": { + "type": "string", + "description": "The type of output for the editor panel. Can be used to change it to 'markdown' for example" } }, "additionalProperties": false, diff --git a/src/explorerView/ExplorerView.ts b/src/explorerView/ExplorerView.ts index deb91a98..2a3f8a2c 100644 --- a/src/explorerView/ExplorerView.ts +++ b/src/explorerView/ExplorerView.ts @@ -1,8 +1,8 @@ import { DashboardData } from '../models/DashboardData'; import { Template } from '../commands/Template'; -import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants'; +import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants'; import * as os from 'os'; -import { PanelSettings, CustomScript } from '../models/PanelSettings'; +import { PanelSettings, CustomScript as ICustomScript } from '../models/PanelSettings'; import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode"; import { ArticleHelper, Settings } from "../helpers"; import { Command } from "../panelWebView/Command"; @@ -11,10 +11,8 @@ import { Article } from '../commands'; import { TagType } from '../panelWebView/TagType'; import { TaxonomyType } from '../models'; import { exec } from 'child_process'; -import * as path from 'path'; import { fromMarkdown } from 'mdast-util-from-markdown'; import { Content } from 'mdast'; -import { Notifications } from '../helpers/Notifications'; import { COMMAND_NAME } from '../constants/Extension'; import { Folders } from '../commands/Folders'; import { Preview } from '../commands/Preview'; @@ -23,6 +21,7 @@ import { WebviewHelper } from '@estruyf/vscode'; import { Extension } from '../helpers/Extension'; import { Dashboard } from '../commands/Dashboard'; import { ImageHelper } from '../helpers/ImageHelper'; +import { CustomScript } from '../helpers/CustomScript'; const FILE_LIMIT = 10; @@ -352,38 +351,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable { * @param msg */ private runCustomScript(msg: { command: string, data: any}) { - const scripts: CustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS); + const scripts: ICustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS); if (msg?.data?.title && msg?.data?.script && scripts) { - const customScript = scripts.find((s: CustomScript) => s.title === msg.data.title); + const customScript = scripts.find((s: ICustomScript) => s.title === msg.data.title); if (customScript?.script && customScript?.title) { - const editor = window.activeTextEditor; - if (!editor) return; - - const article = ArticleHelper.getFrontMatter(editor); - - const wsFolder = Folders.getWorkspaceFolder(); - if (wsFolder) { - const wsPath = wsFolder.fsPath; - - let articleData = `'${JSON.stringify(article?.data)}'`; - if (os.type() === "Windows_NT") { - articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`; - } - - exec(`${customScript.nodeBin || "node"} ${path.join(wsPath, msg.data.script)} "${wsPath}" "${editor?.document.uri.fsPath}" ${articleData}`, (error, stdout) => { - if (error) { - Notifications.error(`${msg?.data?.title}: ${error.message}`); - return; - } - - window.showInformationMessage(`${msg?.data?.title}: ${stdout || "Executed your custom script."}`, 'Copy output').then(value => { - if (value === 'Copy output') { - vscodeEnv.clipboard.writeText(stdout); - } - }); - }); - } + CustomScript.run(customScript); } } } diff --git a/src/extension.ts b/src/extension.ts index 3d9e416b..ad613b9b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,6 +15,7 @@ import { Extension } from './helpers/Extension'; import { DashboardData } from './models/DashboardData'; import { Settings as SettingsHelper } from './helpers'; import { Content } from './commands/Content'; +import ContentProvider from './providers/ContentProvider'; let frontMatterStatusBar: vscode.StatusBarItem; let statusDebouncer: { (fnc: any, time: number): void; }; @@ -175,6 +176,9 @@ export async function activate(context: vscode.ExtensionContext) { // Inserting an image in Markdown subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage)); + // Create the editor experience for bulk scripts + subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(ContentProvider.scheme, new ContentProvider())); + // Subscribe all commands subscriptions.push( insertTags, diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index 35efdaf6..ffd9a6b1 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -32,9 +32,9 @@ export class ArticleHelper { * Retrieve the file's front matter by its path * @param filePath */ - public static getFrontMatterByPath(filePath: string) { + public static getFrontMatterByPath(filePath: string, surpressNotification: boolean = false) { const file = fs.readFileSync(filePath, { encoding: "utf-8" }); - return ArticleHelper.parseFile(file, filePath); + return ArticleHelper.parseFile(file, filePath, surpressNotification); } /** @@ -223,7 +223,7 @@ export class ArticleHelper { * @param fileContents * @returns */ - private static parseFile(fileContents: string, fileName: string): matter.GrayMatterFile | null { + private static parseFile(fileContents: string, fileName: string, surpressNotification: boolean = false): matter.GrayMatterFile | null { try { const commaSeparated = Settings.get(SETTING_COMMA_SEPARATED_FIELDS); @@ -255,15 +255,17 @@ export class ArticleHelper { await EditorHelper.showFile(fileName) } }]; - - Notifications.error(`There seems to be an issue parsing the content its front matter. FileName: ${basename(fileName)}. ERROR: ${error.message || error}`, ...items).then((result: any) => { - if (result?.title) { - const item = items.find(i => i.title === result.title); - if (item) { - item.action(); + + if (!surpressNotification) { + Notifications.error(`There seems to be an issue parsing the content its front matter. FileName: ${basename(fileName)}. ERROR: ${error.message || error}`, ...items).then((result: any) => { + if (result?.title) { + const item = items.find(i => i.title === result.title); + if (item) { + item.action(); + } } - } - }); + }); + } } return null; } diff --git a/src/helpers/CustomScript.ts b/src/helpers/CustomScript.ts new file mode 100644 index 00000000..477add62 --- /dev/null +++ b/src/helpers/CustomScript.ts @@ -0,0 +1,121 @@ +import { CustomScript as ICustomScript } from '../models/PanelSettings'; +import { window, env as vscodeEnv, ProgressLocation } from 'vscode'; +import { ArticleHelper } from '.'; +import { Folders } from '../commands/Folders'; +import { exec } from 'child_process'; +import matter = require('gray-matter'); +import * as os from 'os'; +import { join } from 'path'; +import { Notifications } from './Notifications'; +import ContentProvider from '../providers/ContentProvider'; + +export class CustomScript { + + public static async run(script: ICustomScript): Promise { + const wsFolder = Folders.getWorkspaceFolder(); + + if (wsFolder) { + const wsPath = wsFolder.fsPath; + + if (script.bulk) { + // Run script on all files + CustomScript.bulkRun(wsPath, script); + } else { + // Run script on current file. + CustomScript.singleRun(wsPath, script); + } + } + } + + private static async singleRun(wsPath: string, script: ICustomScript): Promise { + const editor = window.activeTextEditor; + if (!editor) return; + + const article = ArticleHelper.getFrontMatter(editor); + + if (article) { + const output = await CustomScript.runScript(wsPath, article, editor.document.uri.fsPath, script); + + CustomScript.showOutput(output, script); + } else { + Notifications.warning(`${script.title}: Current article couldn't be retrieved.`); + } + } + + private static async bulkRun(wsPath: string, script: ICustomScript): Promise { + const folders = await Folders.getInfo(); + + if (!folders || folders.length === 0) { + Notifications.warning(`${script.title}: No files found.`); + return; + } + + let output: string[] = []; + + window.withProgress({ + location: ProgressLocation.Notification, + title: `Executing: ${script.title}`, + cancellable: false + }, async (progress, token) => { + for await (const folder of folders) { + if (folder.lastModified.length > 0) { + for await (const file of folder.lastModified) { + try { + const article = ArticleHelper.getFrontMatterByPath(file.filePath, true); + if (article) { + const crntOutput = await CustomScript.runScript(wsPath, article, file.filePath, script); + if (crntOutput) { + output.push(crntOutput); + } + } + } catch (error) { + // Skipping file + } + } + } + } + + CustomScript.showOutput(output.join(`\n`), script); + }); + } + + private static async runScript(wsPath: string, article: matter.GrayMatterFile | null, contentPath: string, script: ICustomScript): Promise { + return new Promise((resolve, reject) => { + let articleData = ""; + if (os.type() === "Windows_NT") { + articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`; + } else { + articleData = JSON.stringify(article?.data).replace(/'/g, "%27"); + articleData = `'${articleData}'`; + } + + console.log(articleData); + + exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${contentPath}" ${articleData}`, (error, stdout) => { + if (error) { + Notifications.error(`${script.title}: ${error.message}`); + resolve(null); + return; + } + + resolve(stdout); + }); + }); + } + + private static showOutput(output: string | null, script: ICustomScript): void { + if (output) { + if (script.output === "editor") { + ContentProvider.show(output, script.title, script.outputType || "text"); + } else { + window.showInformationMessage(`${script.title}: ${output}}`, 'Copy output').then(value => { + if (value === 'Copy output') { + vscodeEnv.clipboard.writeText(output); + } + }); + } + } else { + Notifications.info(`${script.title}: Executed your custom script.`); + } + } +} \ No newline at end of file diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index d1b5513d..f5e73b59 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -72,6 +72,9 @@ export interface CustomScript { title: string; script: string; nodeBin?: string; + bulk?: boolean; + output?: "notification" | "editor"; + outputType?: string; } export interface PreviewSettings { diff --git a/src/providers/ContentProvider.ts b/src/providers/ContentProvider.ts new file mode 100644 index 00000000..755e5406 --- /dev/null +++ b/src/providers/ContentProvider.ts @@ -0,0 +1,27 @@ +import { window, Position, TextDocumentContentProvider, Uri, workspace, WorkspaceEdit, Range, languages, ViewColumn } from "vscode"; + + +export default class ContentProvider implements TextDocumentContentProvider { + + public static get scheme() { return "frontmatter" }; + + provideTextDocumentContent(uri: Uri): string { + return uri.query; + } + + public static async show(data: string, title: string, outputType?: string) { + const apiData = JSON.stringify(data, null, 2); + + const uri = Uri.parse(`${ContentProvider.scheme}:${title} output`); + + const doc = await workspace.openTextDocument(uri); + + await window.showTextDocument(doc, { preview: true, viewColumn: ViewColumn.Beside, preserveFocus: true }); + + const workEdits = new WorkspaceEdit(); + workEdits.replace(doc.uri, new Range(new Position(0, 0), new Position(doc.lineCount, 0)), data); + await workspace.applyEdit(workEdits); + + await languages.setTextDocumentLanguage(doc, outputType || "text"); + } +} \ No newline at end of file