mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
443 lines
13 KiB
TypeScript
443 lines
13 KiB
TypeScript
import {
|
|
SETTING_PREVIEW_HOST,
|
|
SETTING_PREVIEW_PATHNAME,
|
|
CONTEXT,
|
|
PreviewCommands,
|
|
SETTING_EXPERIMENTAL,
|
|
SETTING_DATE_FORMAT,
|
|
GeneralCommands,
|
|
SETTING_PREVIEW_TRAILING_SLASH
|
|
} from './../constants';
|
|
import { join, parse } from 'path';
|
|
import { commands, env, Uri, ViewColumn, window, WebviewPanel, extensions } from 'vscode';
|
|
import {
|
|
ArticleHelper,
|
|
Extension,
|
|
parseWinPath,
|
|
processI18nPlaceholders,
|
|
processTimePlaceholders,
|
|
processFmPlaceholders,
|
|
processPathPlaceholders,
|
|
Settings,
|
|
processDateTimePlaceholders
|
|
} from '../helpers';
|
|
import { ContentFolder, ContentType, PreviewSettings } from '../models';
|
|
import { Article } from '.';
|
|
import { WebviewHelper } from '@estruyf/vscode';
|
|
import { Folders } from './Folders';
|
|
import { ParsedFrontMatter } from '../parsers';
|
|
import { getLocalizationFile } from '../utils/getLocalizationFile';
|
|
import * as l10n from '@vscode/l10n';
|
|
import { LocalizationKey } from '../localization';
|
|
import { getTitleField, getWebviewJsFiles, joinUrl } from '../utils';
|
|
import { i18n } from './i18n';
|
|
|
|
export class Preview {
|
|
public static filePath: string | undefined = undefined;
|
|
public static webviews: { [filePath: string]: WebviewPanel } = {};
|
|
|
|
/**
|
|
* Init the preview
|
|
*/
|
|
public static async init() {
|
|
const settings = Preview.getSettings();
|
|
await commands.executeCommand('setContext', CONTEXT.canOpenPreview, !!settings.host);
|
|
}
|
|
|
|
/**
|
|
* Open the markdown preview in the editor
|
|
*/
|
|
public static async open(extensionPath: string) {
|
|
const settings = Preview.getSettings();
|
|
|
|
if (!settings.host) {
|
|
return;
|
|
}
|
|
|
|
const integratedBrowserCommand = await this.getIntegratedBrowserCommand();
|
|
const browserLiteCommand = await this.getBrowserLiteCommand();
|
|
|
|
const editor = window.activeTextEditor;
|
|
const crntFilePath = editor?.document.uri.fsPath;
|
|
this.filePath = crntFilePath;
|
|
|
|
if (crntFilePath && this.webviews[crntFilePath] && !browserLiteCommand) {
|
|
this.webviews[crntFilePath].reveal();
|
|
return;
|
|
}
|
|
|
|
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
|
|
const slug = await this.getContentSlug(article, editor?.document.uri.fsPath);
|
|
const localhostUrl = await this.getLocalServerUrl();
|
|
|
|
if (integratedBrowserCommand) {
|
|
const pageUrl = joinUrl(localhostUrl.toString(), slug || '');
|
|
commands.executeCommand(integratedBrowserCommand, pageUrl);
|
|
return;
|
|
}
|
|
|
|
if (browserLiteCommand) {
|
|
const pageUrl = joinUrl(localhostUrl.toString(), slug || '');
|
|
commands.executeCommand(browserLiteCommand, pageUrl);
|
|
return;
|
|
}
|
|
|
|
const titleField = getTitleField();
|
|
|
|
// Create the preview webview
|
|
const webView = window.createWebviewPanel(
|
|
'frontMatterPreview',
|
|
article?.data && article?.data[titleField]
|
|
? l10n.t(LocalizationKey.commandsPreviewPanelTitle, article?.data[titleField])
|
|
: 'Front Matter 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 cspSource = webView.webview.cspSource;
|
|
|
|
webView.onDidDispose(() => {
|
|
if (crntFilePath && this.webviews[crntFilePath]) {
|
|
delete this.webviews[crntFilePath];
|
|
}
|
|
webView.dispose();
|
|
});
|
|
|
|
const fetchLocalization = async (requestId: string) => {
|
|
if (!requestId) {
|
|
return;
|
|
}
|
|
|
|
const fileContents = await getLocalizationFile();
|
|
|
|
webView.webview.postMessage({
|
|
command: GeneralCommands.toVSCode.getLocalization,
|
|
requestId,
|
|
payload: fileContents
|
|
});
|
|
};
|
|
|
|
webView.webview.onDidReceiveMessage(async (message) => {
|
|
const { command, payload, requestId } = message;
|
|
|
|
switch (command) {
|
|
case PreviewCommands.toVSCode.open:
|
|
if (payload) {
|
|
commands.executeCommand('vscode.open', payload);
|
|
}
|
|
break;
|
|
case GeneralCommands.toVSCode.getLocalization:
|
|
fetchLocalization(requestId);
|
|
break;
|
|
}
|
|
});
|
|
|
|
const webviewFile = 'dashboard.main.js';
|
|
const localPort = `9000`;
|
|
const localServerUrl = `localhost:${localPort}`;
|
|
|
|
const nonce = WebviewHelper.getNonce();
|
|
|
|
const ext = Extension.getInstance();
|
|
const isProd = ext.isProductionMode;
|
|
const version = ext.getVersion();
|
|
const isBeta = ext.isBetaVersion();
|
|
|
|
const csp = [
|
|
`default-src 'none';`,
|
|
`img-src ${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 scriptUris = [];
|
|
if (isProd) {
|
|
scriptUris = await getWebviewJsFiles('dashboard', webView.webview);
|
|
} else {
|
|
scriptUris.push(`http://${localServerUrl}/${webviewFile}`);
|
|
}
|
|
|
|
// 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="${joinUrl(
|
|
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>
|
|
|
|
${scriptUris
|
|
.map((uri) => `<script ${isProd ? `nonce="${nonce}"` : ''} src="${uri}"></script>`)
|
|
.join('\n')}
|
|
|
|
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`preview-${version.installedVersion}`}" alt="Daily usage" />
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Update the url of the preview webview
|
|
* @param filePath
|
|
* @param slug
|
|
*/
|
|
public static async updatePageUrl(filePath: string, _: 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: joinUrl(localhost.toString(), slug || '')
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the slug of the content
|
|
* @param article
|
|
* @param filePath
|
|
* @returns
|
|
*/
|
|
public static async getContentSlug(
|
|
article: ParsedFrontMatter | null | undefined,
|
|
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;
|
|
filePath = parseWinPath(filePath);
|
|
|
|
let contentType: ContentType | undefined = undefined;
|
|
if (article?.data) {
|
|
contentType = await ArticleHelper.getContentType(article);
|
|
}
|
|
|
|
// Get the folder of the article by the file path
|
|
selectedFolder = await Folders.getPageFolderByFilePath(filePath);
|
|
|
|
if (!selectedFolder && contentType) {
|
|
selectedFolder = await Folders.getFolderByContentType(contentType, filePath);
|
|
}
|
|
|
|
if (selectedFolder && selectedFolder.previewPath) {
|
|
pathname = selectedFolder.previewPath;
|
|
}
|
|
|
|
// Check if there is a pathname defined on content type level
|
|
if (article?.data) {
|
|
if (contentType && contentType.previewPath) {
|
|
pathname = contentType.previewPath;
|
|
}
|
|
}
|
|
|
|
if (!slug) {
|
|
slug = Article.getSlug(pathname);
|
|
}
|
|
|
|
const locale = await i18n.getLocale(filePath);
|
|
if (locale && locale.path === slug) {
|
|
slug = '';
|
|
}
|
|
|
|
if (pathname) {
|
|
// Known placeholders
|
|
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
|
pathname = processTimePlaceholders(pathname, dateFormat);
|
|
|
|
// Custom placeholders
|
|
pathname = await ArticleHelper.processCustomPlaceholders(
|
|
pathname,
|
|
article?.data?.title,
|
|
filePath
|
|
);
|
|
|
|
// Process the path placeholders - {{pathToken.<integer>}}
|
|
if (filePath) {
|
|
const wsFolder = Folders.getWorkspaceFolder();
|
|
// Get relative file path
|
|
const folderPath = wsFolder ? parseWinPath(wsFolder.fsPath) : '';
|
|
const relativePath = filePath.replace(folderPath, '');
|
|
pathname = processPathPlaceholders(pathname, relativePath, filePath, selectedFolder);
|
|
pathname = processI18nPlaceholders(pathname, selectedFolder);
|
|
|
|
const file = parse(filePath);
|
|
if (file.name.toLowerCase() === 'index') {
|
|
const cleanPathName = pathname.endsWith('/')
|
|
? pathname.substring(0, pathname.length - 1)
|
|
: pathname;
|
|
|
|
if (cleanPathName.endsWith(slug) || !pathname || pathname === '/') {
|
|
slug = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Support front matter placeholders - {{fm.<field>}}
|
|
pathname = article?.data ? processFmPlaceholders(pathname, article?.data) : pathname;
|
|
|
|
try {
|
|
const articleDate = await ArticleHelper.getDate(article);
|
|
pathname = processDateTimePlaceholders(pathname, articleDate);
|
|
slug = join(pathname, slug);
|
|
} catch (error) {
|
|
slug = join(pathname, slug);
|
|
}
|
|
}
|
|
|
|
// Make sure there are no backslashes in the slug
|
|
slug = parseWinPath(slug);
|
|
|
|
// Verify if the slug doesn't end with _index or index
|
|
if (slug.endsWith('_index') || slug.endsWith('index')) {
|
|
slug = slug.substring(0, slug.endsWith('_index') ? slug.length - 6 : slug.length - 5);
|
|
}
|
|
|
|
// Add the trailing slash
|
|
let trailingSlash = false;
|
|
if (settings.trailingSlash !== undefined) {
|
|
trailingSlash = settings.trailingSlash;
|
|
}
|
|
|
|
if (selectedFolder && selectedFolder.trailingSlash !== undefined) {
|
|
trailingSlash = selectedFolder.trailingSlash;
|
|
}
|
|
|
|
if (contentType && contentType.trailingSlash !== undefined) {
|
|
trailingSlash = contentType.trailingSlash;
|
|
}
|
|
|
|
if (trailingSlash && !slug.endsWith('/')) {
|
|
slug = `${slug}/`;
|
|
}
|
|
|
|
return join(slug);
|
|
}
|
|
|
|
/**
|
|
* Check if Browser Lite is installed
|
|
*/
|
|
private static async getBrowserLiteCommand() {
|
|
const ext = extensions.getExtension(`antfu.browse-lite`);
|
|
if (ext && ext.packageJSON) {
|
|
const hasCommand = ext.packageJSON.contributes?.commands?.find(
|
|
(c: { command: string }) => c.command === 'browse-lite.open'
|
|
);
|
|
if (hasCommand) {
|
|
return 'browse-lite.open';
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Check if Browser Lite is installed
|
|
*/
|
|
private static async getIntegratedBrowserCommand() {
|
|
const allCommands = await commands.getCommands(true);
|
|
if (allCommands.includes(`workbench.action.browser.open`)) {
|
|
return `workbench.action.browser.open`;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the localhost url
|
|
* @returns
|
|
*/
|
|
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));
|
|
return localhostUrl;
|
|
}
|
|
|
|
/**
|
|
* Retrieve all settings related to the preview command
|
|
*/
|
|
public static getSettings(): PreviewSettings {
|
|
const host = Settings.get<string>(SETTING_PREVIEW_HOST);
|
|
const pathname = Settings.get<string>(SETTING_PREVIEW_PATHNAME);
|
|
const trailingSlash = Settings.get<boolean>(SETTING_PREVIEW_TRAILING_SLASH);
|
|
|
|
return {
|
|
host,
|
|
pathname,
|
|
trailingSlash
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Ask the user to select the folder of the article to preview
|
|
* @param crntFolders
|
|
* @returns
|
|
*/
|
|
public static async askUserToPickFolder(
|
|
crntFolders: ContentFolder[]
|
|
): Promise<ContentFolder | undefined> {
|
|
let selectedFolder: ContentFolder | undefined = undefined;
|
|
|
|
if (crntFolders.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
// Ask the user to select the folder
|
|
const folderNames = crntFolders.map((folder) => folder.title);
|
|
const selectedFolderName = await window.showQuickPick(folderNames, {
|
|
canPickMany: false,
|
|
title: l10n.t(LocalizationKey.commandsPreviewAskUserToPickFolderTitle)
|
|
});
|
|
|
|
if (selectedFolderName) {
|
|
selectedFolder = crntFolders.find((folder) => folder.title === selectedFolderName);
|
|
}
|
|
|
|
return selectedFolder;
|
|
}
|
|
}
|