Compare commits

...

20 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
a387d5eb89 Fix ContentType validation to handle non-string/non-array field values safely
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-06-20 12:48:08 +00:00
copilot-swe-agent[bot]
f1ae60f280 Fix variable frontmatter error - handle both string and object formats for preview image fields
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-06-20 12:46:44 +00:00
copilot-swe-agent[bot]
0e2aea626f Initial plan for issue 2025-06-20 12:36:28 +00:00
Elio Struyf
9ce7754b1a Merge pull request #943 from stephanie-wertman/sw-welcome-text-typo
Improvements to Welcome screen
2025-04-16 11:25:12 +02:00
Stephanie Wertman
9f2f279c20 Rephrase welcome message in localization.enum.ts
Rephrase welcome message to join sentence fragments and improve tone.
2025-04-15 12:06:16 -07:00
Stephanie Wertman
0568149335 Rephrase welcome message in bundle.l10n.json
Rephrase welcome message to join sentence fragments and improve tone.
2025-04-15 12:04:02 -07:00
Elio Struyf
1b4e39b806 Merge pull request #924 from estruyf/beta
v10.8.0 release
2025-02-27 12:09:01 +01:00
Elio Struyf
1fa73efe11 Updated changelog 2025-02-27 11:59:28 +01:00
Elio Struyf
ddefb9f138 Copilot testing 2025-02-26 20:28:52 +01:00
Elio Struyf
5e258ac218 Feat: add file path support for slug generation and enhance slug template placeholders #922 2025-02-15 16:41:02 +01:00
Elio Struyf
d2b0228809 Feat: improve filename sanitization and add normalization for special characters #921 2025-02-14 09:35:12 +01:00
Elio Struyf
a164a849da Fix: add refresh button to media dashboard when custom scripts are defined 2025-02-12 15:01:13 +01:00
Elio Struyf
710ef136b4 Fix: improve media folder parsing on Windows and update path handling in various modules 2025-02-12 14:53:56 +01:00
Elio Struyf
d3b7f73c66 Refactor: update import paths for parseWinPath and integrate it into joinUrl function 2025-02-12 12:41:22 +01:00
Elio Struyf
ee5af88851 Fix: ensure Windows drive letters are lowercased and improve path parsing 2025-02-12 12:27:24 +01:00
Elio Struyf
482cbc3bf6 Issue: [[&mediaUrl]] placeholder in Media snippets is not relative #913 2025-02-06 11:40:22 +01:00
Elio Struyf
64f1da6355 Enhancement: auto switch to editor panel when opening a markdown file #915 2025-02-06 11:08:36 +01:00
Elio Struyf
e27adececb Issue: "panel.gitActions" option in view modes is not present in the JSON schema #909 2025-02-06 10:24:43 +01:00
Elio Struyf
b391aa3270 10.8.0 2025-02-06 10:20:39 +01:00
Elio Struyf
b58c02b6d0 Issue: Stripping underscore in default filename #914 2025-02-06 10:20:31 +01:00
31 changed files with 252 additions and 62 deletions

View File

@@ -1,5 +1,21 @@
# Change Log
## [10.8.0] - 2025-02-27 - [Release notes](https://beta.frontmatter.codes/updates/v10.8.0)
### 🎨 Enhancements
- [#915](https://github.com/estruyf/vscode-front-matter/issues/915): Added a new setting `frontMatter.panel.openOnSupportedFile` which allows you to open the panel view on supported files
- [#921](https://github.com/estruyf/vscode-front-matter/issues/921): Improve the filename sanitization
- [#922](https://github.com/estruyf/vscode-front-matter/issues/922): Added `{{fileName}}` and `{{sluggedFileName}}` placeholders for the slug template setting
### 🐞 Fixes
- Fix for media folder parsing on Windows
- Refresh button was not available on the media dashboard when having custom scripts defined
- [#909](https://github.com/estruyf/vscode-front-matter/issues/909): Schema fix for the view modes
- [#913](https://github.com/estruyf/vscode-front-matter/issues/913): Fix for relative media paths in page bundles
- [#914](https://github.com/estruyf/vscode-front-matter/issues/914): Fix sanitizing of default filenames with an `_` in it
## [10.7.0] - 2024-12-31 - [Release notes](https://beta.frontmatter.codes/updates/v10.7.0)
### 🎨 Enhancements

View File

@@ -56,6 +56,8 @@
"settings.view.integration": "Integration",
"settings.openOnStartup": "Open dashboard on startup",
"settings.openPanelForSupportedFiles": "Open panel for supported files",
"settings.openPanelForSupportedFiles.label": "Do you want to open the panel for supported files?",
"settings.contentTypes": "Content types",
"settings.contentFolders": "Content folders",
"settings.diagnostic": "Diagnostic",
@@ -369,7 +371,7 @@
"dashboard.welcomeScreen.title": "Manage your static site with Front Matter",
"dashboard.welcomeScreen.thanks": "Thank you for using Front Matter!",
"dashboard.welcomeScreen.description": "We try to aim to make Front Matter as easy to use as possible, but if you have any questions or suggestions. Please don't hesitate to reach out to us on GitHub.",
"dashboard.welcomeScreen.description": "We aim to make Front Matter as easy to use as possible. If you have any questions or suggestions, please contact us on GitHub.",
"dashboard.welcomeScreen.link.github.title": "GitHub",
"dashboard.welcomeScreen.link.github.label": "GitHub",
"dashboard.welcomeScreen.link.documentation.label": "Documentation",
@@ -799,7 +801,6 @@
"listeners.panel.dataListener.createDataFile.error": "No data file id or path defined.",
"listeners.panel.dataListener.createDataFile.noFileName": "No filename provided.",
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noEditor.error": "No active editor",
"listeners.panel.taxonomyListener.aiSuggestTaxonomy.noData.error": "No article data",
@@ -817,4 +818,4 @@
"services.sponsorAi.getTaxonomySuggestions.warning": "The AI taxonomy generation took too long. Please try again later.",
"services.terminal.openLocalServerTerminal.terminalOption.message": "Starting local server"
}
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "vscode-front-matter-beta",
"version": "10.7.0",
"version": "10.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vscode-front-matter-beta",
"version": "10.7.0",
"version": "10.8.0",
"license": "MIT",
"devDependencies": {
"@actions/core": "^1.10.0",

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": "10.7.0",
"version": "10.8.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -1061,8 +1061,9 @@
"panel.globalSettings",
"panel.seo",
"panel.actions",
"panel.contentType",
"panel.metadata",
"panel.contentType",
"panel.gitActions",
"panel.recentlyModified",
"panel.otherActions",
"dashboard.snippets.view",
@@ -1213,6 +1214,12 @@
},
"scope": "Media"
},
"frontMatter.panel.openOnSupportedFile": {
"type": "boolean",
"default": false,
"markdownDescription": "%setting.frontMatter.panel.openOnSupportedFile.markdownDescription%",
"scope": "Dashboard"
},
"frontMatter.panel.freeform": {
"type": "boolean",
"default": true,

View File

@@ -184,6 +184,7 @@
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.type.description": "Define the type of field",
"setting.frontMatter.media.contentTypes.items.properties.fields.properties.single.description": "Is a single line field",
"setting.frontMatter.panel.openOnSupportedFile.markdownDescription": "Specifies if you want to open the panel when opening a supported file. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.openonsupportedfile) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.openonsupportedfile%22%5D)",
"setting.frontMatter.panel.freeform.markdownDescription": "Specifies if you want to allow yourself from entering unknown tags/categories in the tag picker (when enabled, you will have the option to store them afterwards). Default: true. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.freeform) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.freeform%22%5D)",
"setting.frontMatter.panel.actions.disabled.markdownDescription": "Specify the actions you want to disable in the panel. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.panel.actions.disabled) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.panel.actions.disabled%22%5D)",
"setting.frontMatter.preview.host.markdownDescription": "Specify the host URL (example: http://localhost:1313) to be used when opening the preview. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.preview.host) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.preview.host%22%5D)",
@@ -292,4 +293,4 @@
"setting.frontMatter.git.disableOnBranches.markdownDescription": "Specify the branches on which you want to disable the Git actions. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.git.disableonbranches) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.disableonbranches%22%5D)",
"setting.frontMatter.git.requiresCommitMessage.markdownDescription": "Specify if you want to require a commit message when publishing your changes for a specified branch. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.git.requirescommitmessage) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.git.requirescommitmessage%22%5D)",
"setting.frontMatter.copilot.family.markdownDescription": "Specify the LLM family of the Copilot you want to use. [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.copilot.family) - [View in VS Code](command:simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.copilot.family%22%5D)"
}
}

View File

@@ -172,7 +172,12 @@ export class Article {
/**
* Generate the new slug
*/
public static generateSlug(title: string, article?: ParsedFrontMatter, slugTemplate?: string) {
public static generateSlug(
title: string,
article?: ParsedFrontMatter,
filePath?: string,
slugTemplate?: string
) {
if (!title) {
return;
}
@@ -181,7 +186,7 @@ export class Article {
const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string;
if (article?.data) {
const slug = SlugHelper.createSlug(title, article?.data, slugTemplate);
const slug = SlugHelper.createSlug(title, article?.data, filePath, slugTemplate);
if (typeof slug === 'string') {
return {
@@ -224,7 +229,12 @@ export class Article {
articleDate
);
const slugInfo = Article.generateSlug(articleTitle, article, contentType.slugTemplate);
const slugInfo = Article.generateSlug(
articleTitle,
article,
editor.document.uri.fsPath,
contentType.slugTemplate
);
if (
slugInfo &&
@@ -255,7 +265,8 @@ export class Article {
article.data[pField.name] = processArticlePlaceholdersFromData(
article.data[pField.name],
article.data,
contentType
contentType,
editor.document.uri.fsPath
);
article.data[pField.name] = processTimePlaceholders(
article.data[pField.name],
@@ -335,7 +346,7 @@ export class Article {
} else {
const article = ArticleHelper.getFrontMatter(editor);
if (article?.data) {
return SlugHelper.createSlug(article.data[titleField], article.data, slugTemplate);
return SlugHelper.createSlug(article.data[titleField], article.data, file, slugTemplate);
}
}
}

View File

@@ -219,7 +219,9 @@ export class Folders {
: Folders.getAbsFilePath(assetFolder);
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
const relativePath = relative(parseWinPath(wsFolder.fsPath), parseWinPath(assetFolder));
const relativePath = parseWinPath(
relative(parseWinPath(wsFolder.fsPath), parseWinPath(assetFolder))
);
return relativePath === '' ? '/' : relativePath;
}
}
@@ -636,15 +638,23 @@ export class Folders {
}
}
// For Windows, we need to make sure the drive letter is lowercased for consistency
if (isWindows()) {
folders = folders.map((folder) => parseWinPath(folder));
}
// Filter out the workspace folder
if (wsFolder) {
folders = folders.filter((folder) => folder !== wsFolder.fsPath);
folders = folders.filter((folder) => folder !== parseWinPath(wsFolder.fsPath));
}
const uniqueFolders = [...new Set(folders)];
const relativeFolderPaths = uniqueFolders.map((folder) =>
parseWinPath(relative(parseWinPath(wsFolder.fsPath), folder))
);
Logger.verbose('Folders:getContentFolders:end');
return uniqueFolders.map((folder) => relative(wsFolder?.path || '', folder));
return relativeFolderPaths;
}
/**

View File

@@ -4,10 +4,10 @@ export const FEATURE_FLAG = {
seo: 'panel.seo',
actions: 'panel.actions',
metadata: 'panel.metadata',
recentlyModified: 'panel.recentlyModified',
otherActions: 'panel.otherActions',
contentType: 'panel.contentType',
gitActions: 'panel.gitActions'
gitActions: 'panel.gitActions',
recentlyModified: 'panel.recentlyModified',
otherActions: 'panel.otherActions'
},
dashboard: {
snippets: {

View File

@@ -46,6 +46,7 @@ export const SETTING_TEMPLATES_FOLDER = 'templates.folder';
export const SETTING_TEMPLATES_PREFIX = 'templates.prefix';
export const SETTING_TEMPLATES_ENABLED = 'templates.enabled';
export const SETTING_PANEL_OPEN_ON_SUPPORTED_FILE = 'panel.openOnSupportedFile';
export const SETTING_PANEL_FREEFORM = 'panel.freeform';
export const SETTING_PANEL_ACTIONS_DISABLED = 'panel.actions.disabled';

View File

@@ -0,0 +1,38 @@
import * as React from 'react';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { Checkbox as VSCodeCheckbox } from 'vscrui';
export interface IBooleanOptionProps {
value: boolean | undefined | null;
name: string;
label: string;
}
export const BooleanOption: React.FunctionComponent<IBooleanOptionProps> = ({
value,
name,
label
}: React.PropsWithChildren<IBooleanOptionProps>) => {
const [isChecked, setIsChecked] = React.useState(false);
const onChange = React.useCallback((newValue: boolean) => {
setIsChecked(newValue);
Messenger.send(DashboardMessage.updateSetting, {
name: name,
value: newValue
});
}, [name]);
React.useEffect(() => {
setIsChecked(!!value);
}, [value]);
return (
<VSCodeCheckbox
onChange={onChange}
checked={isChecked}>
{label}
</VSCodeCheckbox>
);
};

View File

@@ -91,8 +91,9 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
if (scripts.length > 0) {
return (
<div className="flex flex-1 justify-start">
<div className="flex flex-1 justify-start space-x-2">
{renderPostAssetsButton}
<ChoiceButton
title={l10n.t(LocalizationKey.dashboardMediaFolderCreationFolderCreate)}
choices={scripts.map((s) => ({
@@ -103,6 +104,8 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (
onClick={onFolderCreation}
disabled={!settings?.initialized}
/>
<RefreshDashboardData />
</div>
);
}

View File

@@ -7,7 +7,7 @@ import {
PlusIcon,
VideoCameraIcon,
} from '@heroicons/react/24/outline';
import { basename } from 'path';
import { basename, parse } from 'path';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
@@ -55,8 +55,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({
const { mediaFolder, mediaDetails, isAudio, isImage, isVideo } = useMediaInfo(media);
const relPath = useMemo(() => {
if (viewData?.data?.pageBundle && viewData?.data?.filePath) {
const articlePath = viewData?.data?.filePath;
const articleDir = parse(parseWinPath(articlePath)).dir;
const mediaPath = parseWinPath(media.fsPath);
if (mediaPath.startsWith(articleDir)) {
return getRelPath(media.fsPath, undefined, articleDir);
}
}
return getRelPath(media.fsPath, settings?.staticFolder, settings?.wsFolder);
}, [media.fsPath, settings?.staticFolder, settings?.wsFolder]);
}, [media.fsPath, settings?.staticFolder, settings?.wsFolder, viewData?.data?.pageBundle, viewData?.data?.filePath]);
const hasViewData = useMemo(() => {
return viewData?.data?.filePath !== undefined;

View File

@@ -6,11 +6,12 @@ import { useRecoilValue } from 'recoil';
import { SettingsSelector } from '../../state';
import { SettingsInput } from './SettingsInput';
import { Button as VSCodeButton } from 'vscrui';
import { DOCS_SUBMODULES, FrameworkDetectors, GIT_CONFIG, SETTING_FRAMEWORK_START, SETTING_GIT_COMMIT_MSG, SETTING_GIT_ENABLED, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants';
import { DOCS_SUBMODULES, FrameworkDetectors, GIT_CONFIG, SETTING_FRAMEWORK_START, SETTING_GIT_COMMIT_MSG, SETTING_GIT_ENABLED, SETTING_PANEL_OPEN_ON_SUPPORTED_FILE, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { SettingsCheckbox } from './SettingsCheckbox';
import { ChevronRightIcon } from '@heroicons/react/24/outline';
import { BooleanOption } from '../Header/BooleanOption';
export interface ICommonSettingsProps { }
@@ -73,6 +74,15 @@ export const CommonSettings: React.FunctionComponent<ICommonSettingsProps> = (pr
<Startup settings={settings} />
</div>
<div className='py-4'>
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsOpenPanelForSupportedFiles)}</h2>
<BooleanOption
label={l10n.t(LocalizationKey.settingsOpenPanelForSupportedFilesLabel)}
name={SETTING_PANEL_OPEN_ON_SUPPORTED_FILE}
value={settings?.openPanelForSupportedFiles} />
</div>
<div className='py-4'>
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsGit)}</h2>

View File

@@ -31,6 +31,7 @@ export interface Settings {
categories: string[];
customTaxonomy: CustomTaxonomy[];
openOnStart: boolean | null;
openPanelForSupportedFiles: boolean | null;
versionInfo: VersionInfo;
pageViewType: DashboardViewType | undefined;
contentTypes: ContentType[];

View File

@@ -245,13 +245,14 @@ export async function activate(context: vscode.ExtensionContext) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function deactivate() {}
const triggerPageUpdate = (location: string) => {
const triggerPageUpdate = async (location: string) => {
Logger.verbose(`Trigger page update: ${location}`);
pageUpdateDebouncer(() => {
StatusListener.verify(collection);
}, 1000);
if (location === 'onDidChangeActiveTextEditor') {
await PanelProvider.openOnSupportedFile();
PanelProvider.getInstance()?.updateCurrentFile();
}
};

View File

@@ -572,7 +572,7 @@ export class ArticleHelper {
await mkdirAsync(newFolder, { recursive: true });
newFilePath = join(
newFolder,
`${sanitize(contentType.defaultFileName ?? `index`)}.${
`${sanitize(contentType.defaultFileName || `index`, { isFileName: true })}.${
fileExtension || contentType.fileType || fileType
}`
);
@@ -684,7 +684,7 @@ export class ArticleHelper {
}
if (fieldName === 'slug' && (fieldValue === null || fieldValue === '')) {
fmData[fieldName] = SlugHelper.createSlug(title, fmData, slugTemplate);
fmData[fieldName] = SlugHelper.createSlug(title, fmData, filePath, slugTemplate);
}
fmData[fieldName] = await processArticlePlaceholdersFromPath(fmData[fieldName], filePath);

View File

@@ -408,7 +408,7 @@ export class ContentType {
* @param parents
* @returns
*/
public static getFieldValue(data: any, parents: string[]): string | string[] {
public static getFieldValue(data: any, parents: string[]): any {
let fieldValue = [];
let crntPageData = data;
@@ -575,7 +575,8 @@ export class ContentType {
fieldValue === null ||
fieldValue === undefined ||
fieldValue === '' ||
fieldValue.length === 0 ||
(Array.isArray(fieldValue) && fieldValue.length === 0) ||
(typeof fieldValue === 'string' && fieldValue.length === 0) ||
fieldValue === DefaultFieldValues.faultyCustomPlaceholder
) {
emptyFields.push(fields);
@@ -1063,7 +1064,8 @@ export class ContentType {
data[field.name] = processArticlePlaceholdersFromData(
field.default as string,
data,
contentType
contentType,
filePath
);
data[field.name] = processTimePlaceholders(
data[field.name],

View File

@@ -31,7 +31,8 @@ import {
SETTING_DASHBOARD_CONTENT_CARD_STATE,
SETTING_DASHBOARD_CONTENT_CARD_DESCRIPTION,
SETTING_WEBSITE_URL,
SETTING_MEDIA_CONTENTTYPES
SETTING_MEDIA_CONTENTTYPES,
SETTING_PANEL_OPEN_ON_SUPPORTED_FILE
} from '../constants';
import {
DashboardViewType,
@@ -108,6 +109,7 @@ export class DashboardSettings {
categories: (await TaxonomyHelper.get(TaxonomyType.Category)) || [],
customTaxonomy: Settings.get(SETTING_TAXONOMY_CUSTOM, true) || [],
openOnStart: Settings.get(SETTING_DASHBOARD_OPENONSTART),
openPanelForSupportedFiles: Settings.get(SETTING_PANEL_OPEN_ON_SUPPORTED_FILE),
versionInfo: ext.getVersion(),
pageViewType: await ext.getState<DashboardViewType | undefined>(
ExtensionState.PagesView,
@@ -119,8 +121,7 @@ export class DashboardSettings {
contentFolders: await Folders.get(),
filters:
Settings.get<(FilterType | { title: string; name: string })[]>(SETTING_CONTENT_FILTERS),
grouping:
Settings.get<{ title: string; name: string }[]>(SETTING_CONTENT_GROUPING),
grouping: Settings.get<{ title: string; name: string }[]>(SETTING_CONTENT_GROUPING),
crntFramework: Settings.get<string>(SETTING_FRAMEWORK_ID),
framework: !isInitialized && wsFolder ? await FrameworkDetector.get(wsFolder.fsPath) : null,
scripts: Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || [],

View File

@@ -137,7 +137,7 @@ export class FrameworkDetector {
const assetDir = dirname(absAssetPath);
const fileName = parse(absAssetPath);
relAssetPath = relative(fileDir, assetDir);
relAssetPath = parseWinPath(relative(fileDir, assetDir));
relAssetPath = join(relAssetPath, `${fileName.name}${fileName.ext}`);
}
// Support for HEXO image folder
@@ -197,7 +197,7 @@ export class FrameworkDetector {
const assetDir = dirname(absAssetPath);
const fileName = parse(absAssetPath);
let relAssetPath = relative(fileDir, assetDir);
let relAssetPath = parseWinPath(relative(fileDir, assetDir));
relAssetPath = join(relAssetPath, `${fileName.name}${fileName.ext}`);
return parseWinPath(relAssetPath);
}

View File

@@ -422,7 +422,7 @@ export class MediaHelpers {
// If the image exists in a content folder, the relative path needs to be used
if (existsInContent) {
const relImgPath = relative(fileDir, imgDir);
const relImgPath = parseWinPath(relative(fileDir, imgDir));
relPath = join(relImgPath, basename(relPath));

View File

@@ -1,17 +1,24 @@
const illegalRe = /[/?<>\\:*|"!.,;{}[\]()_+=~`@#$%^&]/g;
const illegalRe = (isFileName: boolean) =>
isFileName ? /[/?<>\\:*|"!.,;{}[\]()+=~`@#$%^&']/g : /[/?<>\\:*|"!.,;{}[\]()_+=~`@#$%^&']/g;
// eslint-disable-next-line no-control-regex
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
const reservedRe = /^\.+$/;
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
const windowsTrailingRe = /[. ]+$/;
function sanitize(input: string, replacement: string) {
function normalizeSpecialChars(input: string): string {
return input.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
function sanitize(input: string, replacement: string, isFileName?: boolean) {
if (typeof input !== 'string') {
throw new Error('Input must be string');
}
const sanitized = input
.replace(illegalRe, replacement)
const normalizedInput = normalizeSpecialChars(input);
const sanitized = normalizedInput
.replace(illegalRe(isFileName || false), replacement)
.replace(controlRe, replacement)
.replace(reservedRe, replacement)
.replace(windowsReservedRe, replacement)
@@ -19,11 +26,12 @@ function sanitize(input: string, replacement: string) {
return sanitized;
}
export default function (input: string, options?: any) {
export default function (input: string, options?: { replacement?: string; isFileName?: boolean }) {
const replacement = (options && options.replacement) || '';
const output = sanitize(input, replacement);
const isFileName = options && options.isFileName;
const output = sanitize(input, replacement, isFileName);
if (replacement === '') {
return output;
}
return sanitize(output, '');
return sanitize(output, '', isFileName);
}

View File

@@ -1,6 +1,7 @@
import { Settings } from '.';
import { parseWinPath, Settings } from '.';
import { stopWords, charMap, SETTING_DATE_FORMAT, SETTING_SLUG_TEMPLATE } from '../constants';
import { processTimePlaceholders, processFmPlaceholders } from '.';
import { parse } from 'path';
export class SlugHelper {
/**
@@ -11,6 +12,7 @@ export class SlugHelper {
public static createSlug(
articleTitle: string,
articleData: { [key: string]: any },
filePath?: string,
slugTemplate?: string
): string | null {
if (!articleTitle) {
@@ -28,6 +30,16 @@ export class SlugHelper {
} else if (slugTemplate.includes('{{seoTitle}}')) {
const regex = new RegExp('{{seoTitle}}', 'g');
slugTemplate = slugTemplate.replace(regex, SlugHelper.slugify(articleTitle));
} else if (slugTemplate.includes(`{{fileName}}`)) {
const file = parse(filePath || '');
const fileName = file.name;
const regex = new RegExp('{{fileName}}', 'g');
slugTemplate = slugTemplate.replace(regex, fileName);
} else if (slugTemplate.includes(`{{sluggedFileName}}`)) {
const file = parse(filePath || '');
const fileName = SlugHelper.slugify(file.name);
const regex = new RegExp('{{sluggedFileName}}', 'g');
slugTemplate = slugTemplate.replace(regex, fileName);
}
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;

View File

@@ -1,3 +1,15 @@
import { isWindows } from '../utils/isWindows';
export const parseWinPath = (path: string | undefined): string => {
return path?.split(`\\`).join(`/`) || '';
path = path?.split(`\\`).join(`/`) || '';
if (isWindows()) {
// Check if path starts with a drive letter (e.g., "C:\")
if (/^[a-zA-Z]:\\/.test(path)) {
// Convert to lowercase drive letter
path = path.charAt(0).toLowerCase() + path.slice(1);
}
}
return path;
};

View File

@@ -6,7 +6,8 @@ import { SlugHelper } from './SlugHelper';
export const processArticlePlaceholdersFromData = (
value: string,
data: { [key: string]: any },
contentType: ContentType
contentType: ContentType,
filePath?: string
): string => {
const titleField = getTitleField();
if (value.includes('{{title}}') && data[titleField]) {
@@ -18,7 +19,7 @@ export const processArticlePlaceholdersFromData = (
const regex = new RegExp('{{slug}}', 'g');
value = value.replace(
regex,
SlugHelper.createSlug(data[titleField] || '', data, contentType.slugTemplate) || ''
SlugHelper.createSlug(data[titleField] || '', data, filePath, contentType.slugTemplate) || ''
);
}
@@ -50,6 +51,7 @@ export const processArticlePlaceholdersFromPath = async (
SlugHelper.createSlug(
article.data[titleField] || '',
article.data,
filePath,
contentType.slugTemplate
) || ''
);

View File

@@ -1,5 +1,6 @@
import { dirname, relative } from 'path';
import { ContentFolder } from '../models';
import { parseWinPath } from './parseWinPath';
export const processPathPlaceholders = (
value: string,
@@ -11,7 +12,7 @@ export const processPathPlaceholders = (
const relPathToken = '{{pathToken.relPath}}';
if (value.includes(relPathToken) && contentFolder?.path) {
const dirName = dirname(filePath);
const relPath = relative(contentFolder.path, dirName);
const relPath = parseWinPath(relative(contentFolder.path, dirName));
value = value.replace(relPathToken, relPath);
}

View File

@@ -794,7 +794,9 @@ export class DataListener extends BaseListener {
const crntFile = window.activeTextEditor?.document;
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
value =
data && contentType ? processArticlePlaceholdersFromData(value, data, contentType) : value;
data && contentType
? processArticlePlaceholdersFromData(value, data, contentType, crntFile?.uri.fsPath)
: value;
value = processTimePlaceholders(value, dateFormat);
value = processFmPlaceholders(value, data);

View File

@@ -211,6 +211,14 @@ export enum LocalizationKey {
* Open dashboard on startup
*/
settingsOpenOnStartup = 'settings.openOnStartup',
/**
* Open panel for supported files
*/
settingsOpenPanelForSupportedFiles = 'settings.openPanelForSupportedFiles',
/**
* Do you want to open the panel for supported files?
*/
settingsOpenPanelForSupportedFilesLabel = 'settings.openPanelForSupportedFiles.label',
/**
* Content types
*/
@@ -1228,7 +1236,7 @@ export enum LocalizationKey {
*/
dashboardWelcomeScreenThanks = 'dashboard.welcomeScreen.thanks',
/**
* We try to aim to make Front Matter as easy to use as possible, but if you have any questions or suggestions. Please don't hesitate to reach out to us on GitHub.
* We aim to make Front Matter as easy to use as possible. If you have any questions or suggestions, please contact us on GitHub.
*/
dashboardWelcomeScreenDescription = 'dashboard.welcomeScreen.description',
/**

View File

@@ -9,9 +9,10 @@ import {
FieldsListener,
LocalizationListener
} from './../listeners/panel';
import { SETTING_EXPERIMENTAL } from '../constants';
import { SETTING_EXPERIMENTAL, SETTING_PANEL_OPEN_ON_SUPPORTED_FILE } from '../constants';
import {
CancellationToken,
commands,
Disposable,
Uri,
Webview,
@@ -136,6 +137,21 @@ export class PanelProvider implements WebviewViewProvider, Disposable {
});
}
/**
* Opens the panel if the active file is supported.
*
* @returns {Promise<void>} A promise that resolves when the command execution is complete.
*/
public static async openOnSupportedFile(): Promise<void> {
const openPanel = Settings.get<boolean>(SETTING_PANEL_OPEN_ON_SUPPORTED_FILE);
if (openPanel) {
const activeFile = ArticleHelper.getActiveFile();
if (activeFile) {
await commands.executeCommand('frontMatter.explorer.focus');
}
}
}
/**
* Post data to the panel
* @param msg

View File

@@ -263,7 +263,9 @@ Example: SEO, website optimization, digital marketing.`
* @returns A Promise that resolves to the chat model.
*/
private static async getModel(retry = 0): Promise<LanguageModelChat | undefined> {
// const models = await lm.selectChatModels();
// const models = await lm.selectChatModels({
// vendor: 'copilot'
// });
// console.log(models);
const [model] = await lm.selectChatModels({
vendor: 'copilot',

View File

@@ -333,19 +333,32 @@ export class PagesParser {
// Revalidate as the array could have been empty
if (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;
// Handle both string and object formats for the field value
let imageValue: string;
if (typeof fieldValue === 'string') {
imageValue = fieldValue;
} else if (typeof fieldValue === 'object' && fieldValue.src) {
// Handle object format like { src: "filename.jpg", title: "title" }
imageValue = fieldValue.src;
} else {
let staticPath = join(wsFolder.fsPath, staticFolder || '', fieldValue);
// Skip processing if the value is neither a string nor an object with src
imageValue = null;
}
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
const crntFilePath = parseWinPath(filePath);
const pathWithoutExtension = crntFilePath.replace(extname(crntFilePath), '');
staticPath = join(pathWithoutExtension, fieldValue);
}
if (imageValue) {
// Check if the value already starts with https - if that is the case, it is an external image
if (imageValue.startsWith('http')) {
page.fmPreviewImage = imageValue;
} else {
let staticPath = join(wsFolder.fsPath, staticFolder || '', imageValue);
const contentFolderPath = join(dirname(filePath), fieldValue);
if (staticFolder === STATIC_FOLDER_PLACEHOLDER.hexo.placeholder) {
const crntFilePath = parseWinPath(filePath);
const pathWithoutExtension = crntFilePath.replace(extname(crntFilePath), '');
staticPath = join(pathWithoutExtension, imageValue);
}
const contentFolderPath = join(dirname(filePath), imageValue);
let previewUri = null;
if (await existsAsync(staticPath)) {
@@ -367,6 +380,7 @@ export class PagesParser {
page['fmPreviewImage'] = previewPath || '';
}
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
import { urlJoin } from 'url-join-ts';
import { parseWinPath } from '../helpers';
export const joinUrl = (baseUrl: string | undefined, ...paths: any[]): string => {
const url = urlJoin(baseUrl, ...paths);
@@ -9,5 +10,5 @@ export const joinUrl = (baseUrl: string | undefined, ...paths: any[]): string =>
return url + '/';
}
return url;
return parseWinPath(url);
};