Files
vscode-front-matter/src/helpers/ContentType.ts

1246 lines
36 KiB
TypeScript

import { ModeListener } from './../listeners/general/ModeListener';
import { PagesListener } from './../listeners/dashboard';
import {
ArticleHelper,
CustomScript,
Extension,
Logger,
Settings,
processArticlePlaceholdersFromData,
processTimePlaceholders
} from '.';
import {
COMMAND_NAME,
DefaultFieldValues,
DefaultFields,
EXTENSION_NAME,
FEATURE_FLAG,
SETTING_CONTENT_DRAFT_FIELD,
SETTING_DATE_FORMAT,
SETTING_FRAMEWORK_ID,
SETTING_TAXONOMY_CONTENT_TYPES,
SETTING_TAXONOMY_FIELD_GROUPS
} from '../constants';
import {
ContentType as IContentType,
DraftField,
Field,
FieldGroup,
FieldType,
ScriptType
} from '../models';
import { Uri, commands, window, ProgressLocation, workspace } from 'vscode';
import { Folders } from '../commands/Folders';
import { Questions } from './Questions';
import { Notifications } from './Notifications';
import { DEFAULT_CONTENT_TYPE_NAME } from '../constants/ContentType';
import { basename } from 'path';
import { ParsedFrontMatter } from '../parsers';
import { encodeEmoji, existsAsync, fieldWhenClause, getTitleField, writeFileAsync } from '../utils';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
export class ContentType {
/**
* Registers the commands related to content types.
*
* @param subscriptions - The array of subscriptions to which the commands will be added.
*/
public static async registerCommands() {
const ext = Extension.getInstance();
const subscriptions = ext.subscriptions;
subscriptions.push(
commands.registerCommand(COMMAND_NAME.createByContentType, ContentType.createContent)
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.generateContentType, ContentType.generate)
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.addMissingFields, ContentType.addMissingFields)
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.setContentType, ContentType.setContentType)
);
}
/**
* Retrieve the draft field
* @returns
*/
public static getDraftField() {
const draftField = Settings.get<DraftField | null | undefined>(SETTING_CONTENT_DRAFT_FIELD);
if (draftField) {
return draftField;
}
return null;
}
/**
* Retrieve the field its status
* @param data
* @returns
*/
public static async getDraftStatus(article: ParsedFrontMatter) {
const contentType = await ArticleHelper.getContentType(article);
const draftSetting = ContentType.getDraftField();
const draftField = contentType.fields.find((f) => f.type === 'draft');
let fieldValue = null;
if (draftField && article?.data) {
fieldValue = article?.data[draftField.name];
} else if (draftSetting && article?.data && article?.data[draftSetting.name]) {
fieldValue = article?.data[draftSetting.name];
}
if (draftSetting && fieldValue !== null) {
if (draftSetting.type === 'boolean') {
return fieldValue ? 'Draft' : 'Published';
} else {
return fieldValue;
}
}
return null;
}
/**
* Create content based on content types
* @returns
*/
public static async createContent() {
const selectedFolder = await Questions.SelectContentFolder();
if (!selectedFolder) {
return;
}
const contentTypes = ContentType.getAll();
let folders = await Folders.get();
folders = folders.filter((f) => !f.disableCreation);
const folder = folders.find((f) => f.path === selectedFolder.path);
if (!folder) {
return;
}
const selectedContentType = await Questions.SelectContentType(folder.contentTypes || []);
if (!selectedContentType) {
return;
}
if (contentTypes && folder) {
const folderPath = Folders.getFolderPath(Uri.file(folder.path));
const contentType = contentTypes.find((ct) => ct.name === selectedContentType);
if (folderPath && contentType) {
ContentType.create(contentType, folderPath);
}
}
}
/**
* Retrieve all content types
* @returns
*/
public static getAll() {
const cts = Settings.get<IContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
for (const ct of cts || []) {
ct.fields = ContentType.mergeFields(ct.fields || []);
}
return cts;
}
/**
* Merge the collection fields
* @param contentType
* @returns
*/
public static mergeFields(fields: Field[]) {
if (!fields) {
return [];
}
// Check if there is a field collection
const fcFields = ContentType.findAllFieldsByType(fields || [], 'fieldCollection');
if (fcFields.length > 0) {
const fieldGroups = Settings.get<FieldGroup[]>(SETTING_TAXONOMY_FIELD_GROUPS);
if (fieldGroups && fieldGroups.length > 0) {
for (const cField of fcFields) {
for (const fieldName of cField) {
const field = fields.find((f) => f.name === fieldName);
if (field && field.type === 'fieldCollection') {
const fieldGroup = fieldGroups.find((fg) => fg.id === field.fieldGroup);
if (fieldGroup) {
const fieldIdx = fields.findIndex((f) => f.name === field.name);
fields.splice(fieldIdx, 1, ...fieldGroup.fields);
}
} else if (field && field.type === 'fields') {
field.fields = field.fields || [];
}
}
}
}
}
return fields;
}
/**
* Generate a content type
*/
public static async generate() {
if (!(await ContentType.verify())) {
return;
}
const content = ArticleHelper.getCurrent();
const editor = window.activeTextEditor;
const filePath = editor?.document.uri.fsPath;
if (!content || !content.data) {
Notifications.warning(l10n.t(LocalizationKey.helpersContentTypeGenerateNoFrontMatterError));
return;
}
const override = await window.showQuickPick(
[l10n.t(LocalizationKey.commonYes), l10n.t(LocalizationKey.commonNo)],
{
title: l10n.t(LocalizationKey.helpersContentTypeGenerateOverrideQuickPickTitle),
placeHolder: l10n.t(LocalizationKey.helpersContentTypeGenerateOverrideQuickPickPlaceholder),
ignoreFocusOut: true
}
);
const overrideBool = override === l10n.t(LocalizationKey.commonYes);
let contentTypeName: string | undefined = `default`;
// Ask for the new content type name
if (!overrideBool) {
contentTypeName = await window.showInputBox({
title: l10n.t(LocalizationKey.helpersContentTypeGenerateContentTypeInputTitle),
placeHolder: l10n.t(LocalizationKey.helpersContentTypeGenerateContentTypeInputPrompt),
prompt: l10n.t(LocalizationKey.helpersContentTypeGenerateContentTypeInputPrompt),
ignoreFocusOut: true,
validateInput: (value: string) => {
if (!value) {
return l10n.t(
LocalizationKey.helpersContentTypeGenerateContentTypeInputValidationEnterName
);
}
const contentTypes = ContentType.getAll();
if (
contentTypes &&
contentTypes.find((ct) => ct.name.toLowerCase() === value.toLowerCase())
) {
return l10n.t(
LocalizationKey.helpersContentTypeGenerateContentTypeInputValidationNameExists
);
}
return null;
}
});
if (!contentTypeName) {
Notifications.warning(
l10n.t(LocalizationKey.helpersContentTypeGenerateNoContentTypeNameWarning)
);
return;
}
}
// Ask if the content type needs to be used as a page bundle
let pageBundle = false;
const fileName = filePath ? basename(filePath) : undefined;
if (fileName?.startsWith(`index.`)) {
const pageBundleAnswer = await window.showQuickPick(
[l10n.t(LocalizationKey.commonYes), l10n.t(LocalizationKey.commonNo)],
{
title: l10n.t(LocalizationKey.helpersContentTypeGeneratePageBundleQuickPickTitle),
placeHolder: l10n.t(
LocalizationKey.helpersContentTypeGeneratePageBundleQuickPickPlaceHolder
),
ignoreFocusOut: true
}
);
pageBundle = pageBundleAnswer === l10n.t(LocalizationKey.commonYes);
}
const fields = await ContentType.generateFields(content.data);
// Update the type field in the page
if (!overrideBool && editor) {
content.data[DefaultFields.ContentType] = contentTypeName;
ArticleHelper.update(editor, content);
}
const newContentType: IContentType = {
name: contentTypeName,
pageBundle,
fields
};
const contentTypes = ContentType.getAll() || [];
if (overrideBool) {
const index = contentTypes.findIndex((ct) => ct.name === contentTypeName);
contentTypes[index].fields = fields;
} else {
contentTypes.push(newContentType);
}
await Settings.safeUpdate(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
const configPath = await Settings.projectConfigPath();
const notificationAction = await Notifications.info(
overrideBool
? l10n.t(LocalizationKey.helpersContentTypeGenerateUpdatedSuccess, contentTypeName)
: l10n.t(LocalizationKey.helpersContentTypeGenerateGeneratedSuccess, contentTypeName),
configPath && (await existsAsync(configPath))
? l10n.t(LocalizationKey.commonOpenSettings)
: undefined
);
if (
notificationAction === l10n.t(LocalizationKey.commonOpenSettings) &&
configPath &&
(await existsAsync(configPath))
) {
commands.executeCommand('vscode.open', Uri.file(configPath));
}
}
/**
* Add missing fields to the content type
*/
public static async addMissingFields() {
if (!(await ContentType.verify())) {
return;
}
const article = ArticleHelper.getCurrent();
if (!article || !article.data) {
Notifications.warning(
l10n.t(LocalizationKey.helpersContentTypeAddMissingFieldsNoFrontMatterWarning)
);
return;
}
const contentType = await ArticleHelper.getContentType(article);
const updatedFields = await ContentType.generateFields(article.data, contentType.fields);
const contentTypes = ContentType.getAll() || [];
const index = contentTypes.findIndex((ct) => ct.name === contentType.name);
contentTypes[index].fields = updatedFields;
await Settings.safeUpdate(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
const configPath = await Settings.projectConfigPath();
const notificationAction = await Notifications.info(
l10n.t(LocalizationKey.helpersContentTypeAddMissingFieldsUpdatedSuccess, contentType.name),
configPath && (await existsAsync(configPath))
? l10n.t(LocalizationKey.commonOpenSettings)
: undefined
);
if (
notificationAction === l10n.t(LocalizationKey.commonOpenSettings) &&
configPath &&
(await existsAsync(configPath))
) {
commands.executeCommand('vscode.open', Uri.file(configPath));
}
}
/**
* Set the content type to be used for the current file
*/
public static async setContentType() {
if (!(await ContentType.verify())) {
return;
}
const content = ArticleHelper.getCurrent();
const contentTypes = ContentType.getAll() || [];
if (!content || !content.data) {
Notifications.warning(
l10n.t(LocalizationKey.helpersContentTypeSetContentTypeNoFrontMatterWarning)
);
return;
}
const ctAnswer = await window.showQuickPick(
contentTypes.map((ct) => ct.name),
{
title: l10n.t(LocalizationKey.helpersContentTypeSetContentTypeQuickPickTitle),
placeHolder: l10n.t(LocalizationKey.helpersContentTypeSetContentTypeQuickPickPlaceholder),
ignoreFocusOut: true
}
);
if (!ctAnswer) {
return;
}
content.data[DefaultFields.ContentType] = ctAnswer;
const editor = window.activeTextEditor;
ArticleHelper.update(editor!, content);
}
/**
* Retrieve the field value
* @param data
* @param parents
* @returns
*/
public static getFieldValue(data: any, parents: string[]): any {
let fieldValue = [];
let crntPageData = data;
for (let i = 0; i < parents.length; i++) {
const crntField = parents[i];
if (i === parents.length - 1) {
fieldValue = crntPageData[crntField];
} else {
if (!crntPageData[crntField]) {
continue;
}
crntPageData = crntPageData[crntField];
}
}
return fieldValue;
}
/**
* Set the field value
* @param data
* @param parents
* @returns
*/
public static setFieldValue(data: any, parents: string[], value: any) {
let crntPageData = data;
for (let i = 0; i < parents.length; i++) {
const crntField = parents[i];
if (i === parents.length - 1) {
crntPageData[crntField] = value;
} else {
if (!crntPageData[crntField]) {
continue;
}
crntPageData = crntPageData[crntField];
}
}
return data;
}
/**
* Find the field by its name
* @param fields
* @param name
* @returns
*/
public static findFieldByName(fields: Field[], name: string): Field | undefined {
for (const field of fields) {
if (field.name === name) {
return field;
} else if (field.type === 'fields' && field.fields) {
const subField = this.findFieldByName(field.fields, name);
if (subField) {
return subField;
}
}
}
}
/**
* Find the field by its type
* @param fields
* @param type
* @param parents
* @returns
*/
public static findFieldByType(
fields: Field[],
type: FieldType,
parents: string[] = []
): string[] {
for (const field of fields) {
if (field.type === type) {
parents = [...parents, field.name];
return parents;
} else if (field.type === 'fields' && field.fields) {
const subFields = this.findFieldByType(field.fields, type, parents);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
}
}
return parents;
}
/**
* Find all the fields by type
* @param fields
* @param type
* @returns
*/
public static findAllFieldsByType(fields: Field[], type: FieldType): string[][] {
let parents: string[][] = [];
for (const field of fields) {
if (field.type === type) {
parents.push([field.name]);
} else if (field.type === 'fields' && field.fields) {
const subFields = this.findAllFieldsByType(field.fields, type);
if (subFields.length > 0) {
parents = [...parents, ...subFields.map((f) => [field.name, ...f])];
}
}
}
return parents;
}
/**
* Find the preview field in the fields
* @param ctFields
* @param parents
* @returns
*/
public static findPreviewField(ctFields: Field[], parents: string[] = []): string[] {
for (const field of ctFields) {
if (field.isPreviewImage && field.type === 'image') {
parents = [...parents, field.name];
return parents;
} else if (field.type === 'fields' && field.fields) {
const subFields = this.findPreviewField(field.fields);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
} else if (field.type === 'block') {
const subFields = this.findPreviewInBlockField(field);
if (subFields.length > 0) {
return [...parents, field.name, ...subFields];
}
}
}
return parents;
}
/**
* Find the required fields
*/
public static async findEmptyRequiredFields(
article: ParsedFrontMatter
): Promise<Field[][] | undefined> {
const contentType = await ArticleHelper.getContentType(article);
if (!contentType) {
return;
}
const allRequiredFields = ContentType.findRequiredFieldsDeep(contentType.fields);
const emptyFields: Field[][] = [];
for (const fields of allRequiredFields) {
const fieldValue = this.getFieldValue(
article.data,
fields.map((f) => f.name)
);
if (
fieldValue === null ||
fieldValue === undefined ||
fieldValue === '' ||
(Array.isArray(fieldValue) && fieldValue.length === 0) ||
(typeof fieldValue === 'string' && fieldValue.length === 0) ||
fieldValue === DefaultFieldValues.faultyCustomPlaceholder
) {
emptyFields.push(fields);
}
}
return emptyFields || [];
}
/**
* Find fields by type (deep search)
* @param fields
* @param type
* @param parents
* @param parentFields
* @returns
*/
public static findFieldsByTypeDeep(
fields: Field[],
type: FieldType,
parents: Field[][] = [],
parentFields: Field[] = []
): Field[][] {
for (const field of fields) {
if (field.type === type) {
parents.push([...parentFields, field]);
} else if (field.type === 'fields' && field.fields) {
this.findFieldsByTypeDeep(field.fields, type, parents, [...parentFields, field]);
} else if (field.type === 'block') {
const groups =
field.fieldGroup && Array.isArray(field.fieldGroup)
? field.fieldGroup
: [field.fieldGroup];
const blocks = Settings.get<FieldGroup[]>(SETTING_TAXONOMY_FIELD_GROUPS);
if (groups && blocks) {
let found = false;
for (const group of groups) {
const block = blocks.find((block) => block.id === group);
if (!block) {
continue;
}
let newParents: Field[][] = [];
if (!found) {
newParents = this.findFieldsByTypeDeep(block?.fields, type, parents, [
...parentFields,
field
]);
}
if (newParents.length > 0) {
found = true;
}
}
}
}
}
return parents;
}
/**
* Retrieve the block field groups
* @param field
* @returns
*/
public static getBlockFieldGroups(field: Field): FieldGroup[] {
const groups =
field.fieldGroup && Array.isArray(field.fieldGroup) ? field.fieldGroup : [field.fieldGroup];
if (!groups) {
return [];
}
const blocks = Settings.get<FieldGroup[]>(SETTING_TAXONOMY_FIELD_GROUPS);
if (!blocks) {
return [];
}
const foundBlocks = [];
for (const group of groups) {
const block = blocks.find((block) => block.id === group);
if (!block) {
continue;
}
foundBlocks.push(block);
}
return foundBlocks;
}
/**
* Find all the required fields in the content type
* @param fields
* @param parents
* @returns
*/
private static findRequiredFieldsDeep(
fields: Field[],
parents: Field[][] = [],
parentFields: Field[] = []
): Field[][] {
for (const field of fields) {
if (field.required) {
parents.push([...parentFields, field]);
}
if (field.type === 'fields' && field.fields) {
this.findRequiredFieldsDeep(field.fields, parents, [...parentFields, field]);
}
}
return parents;
}
/**
* Look for the preview image in the block field
* @param field
* @param parents
* @returns
*/
private static findPreviewInBlockField(field: Field) {
const groups =
field.fieldGroup && Array.isArray(field.fieldGroup) ? field.fieldGroup : [field.fieldGroup];
if (!groups) {
return [];
}
const blocks = Settings.get<FieldGroup[]>(SETTING_TAXONOMY_FIELD_GROUPS);
if (!blocks) {
return [];
}
let found = false;
for (const group of groups) {
const block = blocks.find((block) => block.id === group);
if (!block) {
continue;
}
let newParents: string[] = [];
if (!found) {
newParents = this.findPreviewField(block?.fields, []);
}
if (newParents.length > 0) {
found = true;
return newParents;
}
}
return [];
}
/**
* Generate the fields from the data
* @param data
* @param fields
* @returns
*/
private static async generateFields(data: any, fields: any[] = []) {
for (const field in data) {
const fieldData = data[field];
if (fields.some((f) => f.name === field)) {
continue;
}
if (field.toLowerCase() === 'tag' || field.toLowerCase() === 'tags') {
fields.push({
title: field,
name: field,
type: 'tags'
} as Field);
} else if (field.toLowerCase() === 'category' || field.toLowerCase() === 'categories') {
fields.push({
title: field,
name: field,
type: 'categories'
} as Field);
} else if (
fieldData &&
fieldData instanceof Array &&
fieldData.length > 0 &&
typeof fieldData[0] === 'string'
) {
fields.push({
title: field,
name: field,
type: 'choice',
choices: fieldData
} as Field);
} else if (
fieldData &&
fieldData instanceof Array &&
fieldData.length > 0 &&
typeof fieldData[0] === 'object'
) {
const newFields = await ContentType.generateFields(fieldData);
// Combine all the fields for the field group
const blockFields: Field[] = [];
for (const newField of newFields) {
for (const field of newField.fields) {
if (blockFields.some((f) => f.name === field.name)) {
continue;
}
blockFields.push(field);
}
}
// Generate a new field group
const fieldGroups = Settings.get<FieldGroup[]>(SETTING_TAXONOMY_FIELD_GROUPS) || [];
const fieldGroupName = `${field}_group`;
const newFieldGroup: FieldGroup = {
id: fieldGroupName,
fields: blockFields
};
fieldGroups.push(newFieldGroup);
await Settings.safeUpdate(SETTING_TAXONOMY_FIELD_GROUPS, fieldGroups, true);
fields.push({
title: field,
name: field,
type: 'block',
fieldGroup: [fieldGroupName]
} as Field);
} else if (fieldData && fieldData instanceof Object) {
const newFields = await ContentType.generateFields(fieldData);
fields.push({
title: field,
name: field,
type: 'fields',
fields: newFields
} as Field);
} else {
if (field.toLowerCase().includes('image')) {
fields.push({
title: field,
name: field,
type: 'image'
} as Field);
} else if (typeof fieldData === 'number') {
fields.push({
title: field,
name: field,
type: 'number'
} as Field);
} else if (typeof fieldData === 'boolean') {
if (field.toLowerCase() === 'draft') {
fields.push({
title: field,
name: field,
type: 'draft'
} as Field);
} else {
fields.push({
title: field,
name: field,
type: 'boolean'
} as Field);
}
} else if (!isNaN(new Date(fieldData).getDate())) {
fields.push({
title: field,
name: field,
type: 'datetime'
} as Field);
} else if (field.toLowerCase() === 'draft') {
fields.push({
title: field,
name: field,
type: 'draft'
} as Field);
} else if (field.toLowerCase() === 'slug') {
// Do nothing
} else {
fields.push({
title: field,
name: field,
type: typeof fieldData
} as Field);
}
}
}
return fields;
}
/**
* Create a new file with the specified content type
* @param contentType
* @param folderPath
* @returns
*/
private static async create(contentType: IContentType, folderPath: string) {
window.withProgress(
{
location: ProgressLocation.Notification,
title: l10n.t(LocalizationKey.helpersContentTypeCreateProgressTitle, EXTENSION_NAME),
cancellable: false
},
async () => {
if (contentType.isSubContent || contentType.allowAsSubContent) {
let showDialog = true;
if (contentType.allowAsSubContent) {
const subContentAnswer = await window.showQuickPick(
[l10n.t(LocalizationKey.commonNo), l10n.t(LocalizationKey.commonYes)],
{
title: l10n.t(LocalizationKey.helpersContentTypeCreateAllowSubContentTitle),
placeHolder: l10n.t(
LocalizationKey.helpersContentTypeCreateAllowSubContentPlaceHolder
),
ignoreFocusOut: true
}
);
showDialog = subContentAnswer === l10n.t(LocalizationKey.commonYes);
}
if (showDialog) {
const folderLocation = await window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri: Uri.file(folderPath),
openLabel: l10n.t(
LocalizationKey.helpersContentTypeCreateAllowSubContentShowOpenDialogOpenLabel
),
title: l10n.t(
LocalizationKey.helpersContentTypeCreateAllowSubContentShowOpenDialogTitle
)
});
if (!folderLocation || folderLocation.length === 0) {
return;
}
folderPath = folderLocation[0].fsPath;
if (contentType.pageBundle) {
const createAsPageBundle = await window.showQuickPick(
[l10n.t(LocalizationKey.commonNo), l10n.t(LocalizationKey.commonYes)],
{
title: l10n.t(LocalizationKey.helpersContentTypeCreatePageBundleTitle),
placeHolder: l10n.t(
LocalizationKey.helpersContentTypeCreatePageBundlePlaceHolder
),
ignoreFocusOut: true
}
);
if (createAsPageBundle === l10n.t(LocalizationKey.commonNo)) {
contentType.pageBundle = false;
}
}
}
}
let titleValue = await Questions.ContentTitle();
if (!titleValue) {
return;
}
titleValue = titleValue.trim();
const titleFieldName = getTitleField();
// Check if the title needs to encode the emoji's used in it
const titleField = contentType.fields.find((f) => f.name === titleFieldName);
if (titleField && titleField.encodeEmoji) {
titleValue = encodeEmoji(titleValue);
}
let templatePath = contentType.template;
let templateData: ParsedFrontMatter | null | undefined = null;
if (templatePath) {
try {
templatePath = Folders.getAbsFilePath(templatePath);
templateData = await ArticleHelper.getFrontMatterByPath(templatePath);
if (!templateData) {
Logger.warning(
`ContentType.create: Template file not found or could not be parsed: ${templatePath}`
);
Notifications.warning(
l10n.t(LocalizationKey.commonError) + ` Template not found: ${templatePath}`
);
}
} catch (error) {
Logger.error(
`ContentType.create: Error loading template from ${templatePath}: ${error}`
);
Notifications.error(
l10n.t(LocalizationKey.commonError) + ` Template loading failed: ${templatePath}`
);
}
}
const newFilePath: string | undefined = await ArticleHelper.createContent(
contentType,
folderPath,
titleValue
);
if (!newFilePath) {
return;
}
if (contentType.name === 'default') {
const crntFramework = Settings.get<string>(SETTING_FRAMEWORK_ID);
if (crntFramework?.toLowerCase() === 'jekyll') {
const idx = contentType.fields.findIndex((f) => f.name === 'draft');
if (idx > -1) {
contentType.fields.splice(idx, 1);
}
}
}
let data: any = await this.processFields(
contentType,
titleValue,
templateData?.data || {},
newFilePath,
!!contentType.clearEmpty,
contentType
);
// Set the content type
data[DefaultFields.ContentType] = contentType.name;
const article: ParsedFrontMatter = {
content: '',
data: Object.assign({}, data),
path: newFilePath
};
data = await ArticleHelper.updateDates(article);
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
data[DefaultFields.ContentType] = contentType.name;
} else {
// Default content type, remove the content type field
delete data[DefaultFields.ContentType];
}
const content = ArticleHelper.stringifyFrontMatter(templateData?.content || ``, data);
await writeFileAsync(newFilePath, content, { encoding: 'utf8' });
// Check if the content type has a post script to execute
if (contentType.postScript) {
const scripts = await CustomScript.getScripts();
const script = scripts.find((s) => s.id === contentType.postScript);
if (script && (script.type === ScriptType.Content || !script?.type)) {
await CustomScript.run(script, newFilePath);
const doc = await workspace.openTextDocument(Uri.file(newFilePath));
await doc.save();
}
}
await commands.executeCommand('vscode.open', Uri.file(newFilePath));
Notifications.info(l10n.t(LocalizationKey.helpersContentTypeCreateSuccess));
// Trigger a refresh for the dashboard
PagesListener.refresh();
}
);
}
/**
* Process all content type fields
* @param contentType
* @param data
*/
private static async processFields(
obj: IContentType | Field,
titleValue: string,
data: any,
filePath: string,
clearEmpty: boolean,
contentType: IContentType,
isRoot = true
): Promise<any> {
if (obj.fields) {
const titleField = getTitleField();
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
for (const field of obj.fields) {
if (!fieldWhenClause(field, data, obj.fields)) {
Logger.info(`Field ${field.name} not added because of when clause`);
continue;
}
if (field.name === titleField) {
if (field.default) {
data[field.name] = processArticlePlaceholdersFromData(
field.default as string,
data,
contentType,
filePath
);
data[field.name] = processTimePlaceholders(
data[field.name],
field.dateFormat || dateFormat
);
data[field.name] = await ArticleHelper.processCustomPlaceholders(
data[field.name],
titleValue,
filePath
);
} else if (isRoot) {
data[field.name] = titleValue;
} else if (!clearEmpty) {
data[field.name] = '';
}
} else {
if (field.type === 'fields') {
data[field.name] = await this.processFields(
field,
titleValue,
{},
filePath,
clearEmpty,
contentType,
false
);
if (clearEmpty && Object.keys(data[field.name]).length === 0) {
delete data[field.name];
}
} else {
const defaultValue = field.default;
if (typeof defaultValue === 'string') {
data[field.name] = await ContentType.processFieldPlaceholders(
defaultValue,
data,
contentType,
field.dateFormat || dateFormat,
titleValue,
filePath
);
} else if (defaultValue && Array.isArray(defaultValue)) {
const defaultValues = [];
for (let value of defaultValue as string[]) {
if (typeof value === 'string') {
value = await ContentType.processFieldPlaceholders(
value,
data,
contentType,
field.dateFormat || dateFormat,
titleValue,
filePath
);
}
defaultValues.push(value);
}
data[field.name] = defaultValues;
} else if (typeof defaultValue !== 'undefined') {
data[field.name] = defaultValue;
} else {
const draftField = ContentType.getDraftField();
if (
field.type === 'draft' &&
(draftField?.type === 'boolean' || draftField?.type === undefined)
) {
data[field.name] = true;
} else {
// Check the field types
switch (field.type) {
case 'choice':
if (field.multiple) {
data[field.name] = [];
} else if (!clearEmpty) {
data[field.name] = '';
}
break;
case 'boolean':
if (!clearEmpty) {
data[field.name] = false;
}
break;
case 'number':
if (!clearEmpty) {
data[field.name] = 0;
}
break;
case 'datetime':
if (!clearEmpty) {
data[field.name] = null;
}
break;
case 'list':
case 'tags':
case 'categories':
case 'taxonomy':
if (!clearEmpty) {
data[field.name] = [];
}
break;
case 'string':
case 'slug':
case 'image':
case 'file':
default:
if (!clearEmpty) {
data[field.name] = '';
}
break;
}
}
}
}
}
}
}
return data;
}
/**
* Processes the field placeholders in the given value.
*
* @param defaultValue - The default value for the field.
* @param data - The data object containing the field values.
* @param contentType - The content type object.
* @param dateFormat - The date format string.
* @param title - The title string.
* @param filePath - The file path string.
* @returns The processed value with field placeholders replaced.
*/
private static async processFieldPlaceholders(
defaultValue: string,
data: any,
contentType: IContentType,
dateFormat: string,
title: string,
filePath: string
) {
let value = processArticlePlaceholdersFromData(defaultValue, data, contentType);
value = processTimePlaceholders(value, dateFormat);
value = await ArticleHelper.processCustomPlaceholders(value, title, filePath);
return value;
}
/**
* Verify if the content type feature is enabled
* @returns
*/
private static async verify() {
const hasFeature = await ModeListener.hasFeature(FEATURE_FLAG.panel.contentType);
if (!hasFeature) {
Notifications.warning(l10n.t(LocalizationKey.helpersContentTypeVerifyWarning));
return false;
}
return true;
}
}