Merge branch 'issue/566' into dev

This commit is contained in:
Elio Struyf
2023-04-14 13:52:47 +02:00
8 changed files with 263 additions and 125 deletions
+183 -98
View File
@@ -12,7 +12,7 @@ import {
} from './../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from 'path';
import { commands, env, Uri, ViewColumn, window } from 'vscode';
import { commands, env, Uri, ViewColumn, window, WebviewPanel } from 'vscode';
import { Extension, parseWinPath, processKnownPlaceholders, Settings } from '../helpers';
import { ContentFolder, ContentType, PreviewSettings } from '../models';
import { format } from 'date-fns';
@@ -21,8 +21,13 @@ import { Article } from '.';
import { urlJoin } from 'url-join-ts';
import { WebviewHelper } from '@estruyf/vscode';
import { Folders } from './Folders';
import { DataListener } from '../listeners/panel';
import { ParsedFrontMatter } from '../parsers';
export class Preview {
public static filePath: string | undefined = undefined;
public static webviews: { [filePath: string]: WebviewPanel } = {};
/**
* Init the preview
*/
@@ -42,13 +47,179 @@ export class Preview {
}
const editor = window.activeTextEditor;
const crntFilePath = editor?.document.uri.fsPath;
this.filePath = crntFilePath;
if (crntFilePath && this.webviews[crntFilePath]) {
this.webviews[crntFilePath].reveal();
return;
}
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
const slug = await this.getContentSlug(article, editor?.document.uri.fsPath);
// Create the preview webview
const webView = window.createWebviewPanel(
'frontMatterPreview',
article?.data?.title ? `Preview: ${article?.data?.title}` : 'FrontMatter Preview',
{
viewColumn: ViewColumn.Beside,
preserveFocus: true
},
{
enableScripts: true
}
);
if (crntFilePath) {
this.webviews[crntFilePath] = webView;
}
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 localhostUrl = await this.getLocalServerUrl();
const cspSource = webView.webview.cspSource;
webView.onDidDispose(() => {
this.filePath = undefined;
if (crntFilePath) {
delete this.webviews[crntFilePath];
}
webView.dispose();
});
webView.onDidChangeViewState(async (e) => {
if (e.webviewPanel.visible) {
this.filePath = crntFilePath;
if (crntFilePath) {
const article = await ArticleHelper.getFrontMatterByPath(crntFilePath);
DataListener.pushMetadata(article?.data);
}
}
});
webView.webview.onDidReceiveMessage((message) => {
switch (message.command) {
case PreviewCommands.toVSCode.open:
if (message.data) {
commands.executeCommand('vscode.open', message.data);
}
return;
}
});
const dashboardFile = 'dashboardWebView.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 extensionUri = ext.extensionPath;
const csp = [
`default-src 'none';`,
`img-src ${localhostUrl} ${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://o1022172.ingest.sentry.io ${
isProd
? ``
: `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`
}`,
`frame-src ${localhostUrl} ${cspSource} http: https:;`
];
let scriptUri = '';
if (isProd) {
scriptUri = webView.webview
.asWebviewUri(Uri.joinPath(extensionUri, 'dist', dashboardFile))
.toString();
} else {
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
}
// Get experimental setting
const experimental = Settings.get(SETTING_EXPERIMENTAL);
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 Preview</title>
</head>
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden">
<div id="app" data-type="preview" data-url="${urlJoin(
localhostUrl.toString(),
slug || ''
)}" 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>
<script ${isProd ? `nonce="${nonce}"` : ''} src="${scriptUri}"></script>
</body>
</html>
`;
Telemetry.send(TelemetryEvent.openPreview);
}
/**
* Update the url of the preview webview
* @param filePath
* @param slug
*/
public static async updatePageUrl(filePath: string, slug?: string) {
const webView = this.webviews[filePath];
if (webView) {
const localhost = await this.getLocalServerUrl();
const article = await ArticleHelper.getFrontMatterByPath(filePath);
const slug = await this.getContentSlug(article, filePath);
webView.webview.postMessage({
command: PreviewCommands.toWebview.updateUrl,
payload: urlJoin(localhost.toString(), slug || '')
});
}
}
/**
* Retrieve the slug of the content
* @param article
* @param filePath
* @returns
*/
private static async getContentSlug(
article: ParsedFrontMatter | null,
filePath?: string
): Promise<string | undefined> {
if (!filePath) {
return;
}
let slug = article?.data ? article.data.slug : '';
const settings = this.getSettings();
let pathname = settings.pathname;
let selectedFolder: ContentFolder | undefined | null = null;
const filePath = parseWinPath(editor?.document.uri.fsPath);
filePath = parseWinPath(filePath);
let contentType: ContentType | undefined = undefined;
if (article?.data) {
@@ -143,104 +314,18 @@ export class Preview {
slug = slug.substring(0, slug.endsWith('_index') ? slug.length - 6 : slug.length - 5);
}
// Create the preview webview
const webView = window.createWebviewPanel(
'frontMatterPreview',
article?.data?.title ? `Preview: ${article?.data?.title}` : 'FrontMatter Preview',
{
viewColumn: ViewColumn.Beside,
preserveFocus: true
},
{
enableScripts: true
}
);
return slug;
}
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 crntUrl = settings.host.startsWith('http') ? settings.host : `http://${settings.host}`;
/**
* Retrieve the localhost url
* @returns
*/
private static async getLocalServerUrl() {
const settings = Preview.getSettings();
const crntUrl = settings?.host?.startsWith('http') ? settings.host : `http://${settings.host}`;
const localhostUrl = await env.asExternalUri(Uri.parse(crntUrl));
const cspSource = webView.webview.cspSource;
webView.webview.onDidReceiveMessage((message) => {
switch (message.command) {
case PreviewCommands.toVSCode.open:
if (message.data) {
commands.executeCommand('vscode.open', message.data);
}
return;
}
});
const dashboardFile = 'dashboardWebView.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 extensionUri = ext.extensionPath;
const csp = [
`default-src 'none';`,
`img-src ${localhostUrl} ${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://o1022172.ingest.sentry.io ${
isProd
? ``
: `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`
}`,
`frame-src ${localhostUrl} ${cspSource} http: https:;`
];
let scriptUri = '';
if (isProd) {
scriptUri = webView.webview
.asWebviewUri(Uri.joinPath(extensionUri, 'dist', dashboardFile))
.toString();
} else {
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
}
// Get experimental setting
const experimental = Settings.get(SETTING_EXPERIMENTAL);
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 Preview</title>
</head>
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden">
<div id="app" data-type="preview" data-url="${urlJoin(
localhostUrl.toString(),
slug || ''
)}" 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>
<script ${isProd ? `nonce="${nonce}"` : ''} src="${scriptUri}"></script>
</body>
</html>
`;
Telemetry.send(TelemetryEvent.openPreview);
return localhostUrl;
}
/**
+17 -5
View File
@@ -15,6 +15,7 @@ import { ContentType } from '../helpers/ContentType';
import { DataListener } from '../listeners/panel';
import { commands } from 'vscode';
import { Field } from '../models';
import { Preview } from './Preview';
export class StatusListener {
/**
@@ -36,11 +37,20 @@ export class StatusListener {
}
const editor = vscode.window.activeTextEditor;
if (editor && ArticleHelper.isSupportedFile()) {
let document = editor?.document;
if (!document) {
const filePath = Preview.filePath;
if (filePath) {
document = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath));
}
}
if (document && ArticleHelper.isSupportedFile(document)) {
try {
commands.executeCommand('setContext', CONTEXT.isValidFile, true);
const article = ArticleHelper.getFrontMatter(editor);
const article = await ArticleHelper.getFrontMatterByPath(document.uri.fsPath);
// Update the StatusBar based on the article draft state
if (article && typeof article.data['draft'] !== 'undefined') {
@@ -65,16 +75,18 @@ export class StatusListener {
const descriptionField =
(Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string) || DefaultFields.Description;
if (article.data[titleField] && titleLength > -1) {
if (editor && article.data[titleField] && titleLength > -1) {
SeoHelper.checkLength(editor, collection, article, titleField, titleLength);
}
if (article.data[descriptionField] && descLength > -1) {
if (editor && article.data[descriptionField] && descLength > -1) {
SeoHelper.checkLength(editor, collection, article, descriptionField, descLength);
}
// Check the required fields
StatusListener.verifyRequiredFields(editor, article, collection);
if (editor) {
StatusListener.verifyRequiredFields(editor, article, collection);
}
}
const panel = ExplorerView.getInstance();
+3 -1
View File
@@ -2,5 +2,7 @@ export const PreviewCommands = {
toVSCode: {
open: `preview.open`
},
fromVSCode: {}
toWebview: {
updateUrl: `preview.updateUrl`
}
};
@@ -4,6 +4,7 @@ import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { PreviewCommands } from '../../../constants';
import useThemeColors from '../../hooks/useThemeColors';
import { EventData } from '@estruyf/vscode/dist/models';
export interface IPreviewProps {
url: string | null;
@@ -35,10 +36,24 @@ export const Preview: React.FunctionComponent<IPreviewProps> = ({
iframeRef.current!.src = navUrl;
};
const msgListener = (message: MessageEvent<EventData<any>>) => {
if (message.data.command === PreviewCommands.toWebview.updateUrl) {
setCrntUrl(message.data.payload);
}
};
useEffect(() => {
setCrntUrl(url);
}, [url]);
useEffect(() => {
Messenger.listen(msgListener);
return () => {
Messenger.unlisten(msgListener);
};
})
return (
<div className="w-full h-full bg-white">
<div
@@ -77,7 +92,7 @@ export const Preview: React.FunctionComponent<IPreviewProps> = ({
<iframe
ref={iframeRef}
src={url || ''}
src={crntUrl || url || ''}
className={`w-full border-0`}
style={{
height: 'calc(100% - 30px)',
+7 -8
View File
@@ -111,12 +111,11 @@ export class ArticleHelper {
*/
public static async updateByPath(path: string, article: ParsedFrontMatter) {
const file = await workspace.openTextDocument(Uri.parse(path));
const editor = await window.showTextDocument(file);
if (file && editor) {
if (file) {
const update = this.generateUpdate(file, article);
await editor.edit((builder) => builder.replace(update.range, update.newText));
await workspace.fs.writeFile(file.uri, new TextEncoder().encode(update.newText));
}
}
@@ -570,18 +569,18 @@ export class ArticleHelper {
* Get the details of the current article
* @returns
*/
public static getDetails() {
public static async getDetails(filePath: string) {
const baseUrl = Settings.get<string>(SETTING_SITE_BASEURL);
const editor = window.activeTextEditor;
if (!editor) {
if (!filePath) {
return null;
}
if (!ArticleHelper.isSupportedFile()) {
const document = await workspace.openTextDocument(filePath);
if (!ArticleHelper.isSupportedFile(document)) {
return null;
}
const article = ArticleHelper.getFrontMatter(editor);
const article = await ArticleHelper.getFrontMatterByPath(filePath);
if (article && article.content) {
let content = article.content;
+2 -1
View File
@@ -6,6 +6,7 @@ import { Field } from '../models';
import { existsSync } from 'fs';
import { Folders } from '../commands/Folders';
import { parseWinPath } from './parseWinPath';
import { Preview } from '../commands';
export class ImageHelper {
/**
@@ -15,7 +16,7 @@ export class ImageHelper {
* @returns
*/
public static allRelToAbs(field: Field, value: string | string[] | undefined) {
const filePath = window.activeTextEditor?.document.uri.fsPath;
const filePath = window.activeTextEditor?.document.uri.fsPath || Preview.filePath;
if (!filePath) {
return;
}
+31 -8
View File
@@ -14,7 +14,7 @@ import {
SETTING_DATE_FORMAT,
SETTING_TAXONOMY_CONTENT_TYPES
} from '../../constants';
import { Article } from '../../commands';
import { Article, Preview } from '../../commands';
import { ParsedFrontMatter } from '../../parsers';
import { processKnownPlaceholders } from '../../helpers/PlaceholderHelper';
import { PostMessageData } from '../../models';
@@ -84,16 +84,18 @@ export class DataListener extends BaseListener {
* Triggers a metadata change in the panel
* @param metadata
*/
public static pushMetadata(metadata: any) {
public static async pushMetadata(metadata: any) {
const wsFolder = Folders.getWorkspaceFolder();
const filePath = window.activeTextEditor?.document.uri.fsPath;
const filePath = window.activeTextEditor?.document.uri.fsPath || Preview.filePath;
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
const contentTypes = Settings.get<string>(SETTING_TAXONOMY_CONTENT_TYPES);
let articleDetails = null;
try {
articleDetails = ArticleHelper.getDetails();
if (filePath) {
articleDetails = await ArticleHelper.getDetails(filePath);
}
} catch (e) {
Logger.error(`DataListener::pushMetadata: ${(e as Error).message}`);
}
@@ -136,6 +138,10 @@ export class DataListener extends BaseListener {
}
}
if (filePath && updatedMetadata[DefaultFields.Slug]) {
Preview.updatePageUrl(filePath, updatedMetadata[DefaultFields.Slug]);
}
this.sendMsg(Command.metadata, updatedMetadata);
DataListener.lastMetadataUpdate = updatedMetadata;
@@ -148,11 +154,13 @@ export class DataListener extends BaseListener {
field,
parents,
value,
filePath,
blockData
}: {
field: string;
value: any;
parents?: string[];
filePath?: string;
blockData?: BlockFieldData;
fieldData?: { multiple: boolean; value: string[] };
}) {
@@ -161,11 +169,21 @@ export class DataListener extends BaseListener {
}
const editor = window.activeTextEditor;
if (!editor) {
return;
let article;
if (filePath) {
article = await ArticleHelper.getFrontMatterByPath(filePath);
} else {
if (!editor) {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
}
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
@@ -216,7 +234,12 @@ export class DataListener extends BaseListener {
parentObj[field] = value;
}
ArticleHelper.update(editor, article);
if (editor) {
ArticleHelper.update(editor, article);
} else if (filePath) {
await ArticleHelper.updateByPath(filePath, article);
}
this.pushMetadata(article.data);
}
+4 -3
View File
@@ -31,7 +31,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({
}: React.PropsWithChildren<IMetadataProps>) => {
const contentType = useContentType(settings, metadata);
const sendUpdate = (field: string | undefined, value: any, parents: string[]) => {
const sendUpdate = React.useCallback((field: string | undefined, value: any, parents: string[]) => {
if (!field) {
return;
}
@@ -39,9 +39,10 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({
Messenger.send(CommandToCode.updateMetadata, {
field,
parents,
value
value,
filePath: metadata.filePath
});
};
}, [metadata.filePath]);
if (!settings) {
return null;