Files
vscode-front-matter/src/helpers/TaxonomyHelper.ts
T
2023-11-06 14:57:03 -08:00

606 lines
18 KiB
TypeScript

import { getTaxonomyField } from './getTaxonomyField';
import {
EXTENSION_NAME,
LocalStore,
SETTING_TAXONOMY_CATEGORIES,
SETTING_TAXONOMY_CUSTOM,
SETTING_TAXONOMY_TAGS
} from '../constants';
import { CustomTaxonomy, TaxonomyType, ContentType as IContentType } from '../models';
import { FilesHelper } from './FilesHelper';
import { ProgressLocation, window } from 'vscode';
import { parseWinPath } from './parseWinPath';
import { FrontMatterParser } from '../parsers';
import { DumpOptions } from 'js-yaml';
import { Settings } from './SettingsHelper';
import { Notifications } from './Notifications';
import { ArticleHelper } from './ArticleHelper';
import { ContentType } from './ContentType';
import { readFileAsync, writeFileAsync } from '../utils';
import { Config, JsonDB } from 'node-json-db';
import { Folders } from '../commands';
import { join } from 'path';
import { SettingsListener as PanelSettingsListener } from '../listeners/panel';
import { SettingsListener as DashboardSettingsListener } from '../listeners/dashboard';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../localization';
export class TaxonomyHelper {
private static db: JsonDB;
/**
* Initialize the database
* @returns
*/
public static initDb() {
const wsFolder = Folders.getWorkspaceFolder();
if (!wsFolder) {
return;
}
const dbFolder = join(
parseWinPath(wsFolder?.fsPath || ''),
LocalStore.rootFolder,
LocalStore.databaseFolder
);
const dbPath = join(dbFolder, LocalStore.taxonomyDatabaseFile);
TaxonomyHelper.db = new JsonDB(new Config(dbPath, true, false, '/'));
}
/**
* Get all the taxonomy values
*/
public static async getAll() {
if (!TaxonomyHelper.db) {
return;
}
const taxonomyData = {
tags: (await TaxonomyHelper.get(TaxonomyType.Tag)) || [],
categories: (await TaxonomyHelper.get(TaxonomyType.Category)) || [],
customTaxonomy: Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM) || []
};
return taxonomyData;
}
/**
* Get the taxonomy settings
*
* @param type
* @param options
*/
public static async get(type: TaxonomyType): Promise<string[] | undefined> {
if (!TaxonomyHelper.db) {
return;
}
const tagType = TaxonomyHelper.getTaxonomyDbPath(type);
let taxonomy: string[] = [];
if (await TaxonomyHelper.db.exists(tagType)) {
taxonomy = await TaxonomyHelper.db.getObject<string[]>(tagType);
}
return taxonomy;
}
/**
* Update the taxonomy settings
*
* @param type
* @param options
*/
public static async update(type: TaxonomyType, options: string[]) {
if (!TaxonomyHelper.db) {
return;
}
const tagType = TaxonomyHelper.getTaxonomyDbPath(type);
options = [...new Set(options)];
options = options.sort().filter((o) => !!o);
await TaxonomyHelper.db.push(tagType, options, true);
// Trigger the update of the taxonomy
PanelSettingsListener.getSettings();
DashboardSettingsListener.getSettings(true);
}
/**
* Get the Taxonomy path of the db entry
* @param type
* @returns
*/
public static getTaxonomyDbPath(type: TaxonomyType) {
let tagType = type === TaxonomyType.Tag ? SETTING_TAXONOMY_TAGS : SETTING_TAXONOMY_CATEGORIES;
tagType = tagType.replace('.', '/');
return `/${tagType}`;
}
/**
* Rename an taxonomy value
* @param data
* @returns
*/
public static async rename(data: { type: string; value: string }) {
const { type, value } = data;
const answer = await window.showInputBox({
title: l10n.t(LocalizationKey.helpersTaxonomyHelperRenameInputTitle, value),
value,
validateInput: (text) => {
if (text === value) {
return l10n.t(LocalizationKey.helpersTaxonomyHelperRenameValidateEqualValue);
}
if (!text) {
return l10n.t(LocalizationKey.helpersTaxonomyHelperRenameValidateNoValue);
}
return null;
},
ignoreFocusOut: true
});
if (!answer) {
return;
}
this.process('edit', this.getTypeFromString(type), value, answer);
}
/**
* Merge a taxonomy value with another one
* @param data
* @returns
*/
public static async merge(data: { type: string; value: string }) {
const { type, value } = data;
const taxonomyType = this.getTypeFromString(type);
let options = [];
if (taxonomyType === TaxonomyType.Tag || taxonomyType === TaxonomyType.Category) {
options = (await TaxonomyHelper.get(taxonomyType)) || [];
} else {
options = Settings.getCustomTaxonomy(taxonomyType);
}
const answer = await window.showQuickPick(
options.filter((o) => o !== value),
{
title: l10n.t(LocalizationKey.helpersTaxonomyHelperMergeQuickPickTitle, value, type),
placeHolder: l10n.t(LocalizationKey.helpersTaxonomyHelperMergeQuickPickPlaceholder, type),
ignoreFocusOut: true
}
);
if (!answer) {
return;
}
this.process('merge', taxonomyType, value, answer);
}
/**
* Delete a taxonomy value
* @param data
*/
public static async delete(data: { type: string; value: string }) {
const { type, value } = data;
const answer = await window.showQuickPick(
[l10n.t(LocalizationKey.commonYes), l10n.t(LocalizationKey.commonNo)],
{
title: l10n.t(LocalizationKey.helpersTaxonomyHelperDeleteQuickPickTitle, value, type),
placeHolder: l10n.t(
LocalizationKey.helpersTaxonomyHelperDeleteQuickPickPlaceholder,
value,
type
),
ignoreFocusOut: true
}
);
if (!answer || answer === l10n.t(LocalizationKey.commonNo)) {
return;
}
this.process('delete', this.getTypeFromString(type), value, undefined);
}
/**
* Add the taxonomy value to the settings
* @param data
*/
public static addTaxonomy(data: { type: string; value: string }) {
const { type, value } = data;
this.addToSettings(this.getTypeFromString(type), value, value);
}
/**
* Create new taxonomy value
* @param data
*/
public static async createNew(data: { type: string }) {
const { type } = data;
const taxonomyType = this.getTypeFromString(type);
const options = await this.getTaxonomyOptions(taxonomyType);
const newOption = await window.showInputBox({
title: l10n.t(LocalizationKey.helpersTaxonomyHelperCreateNewInputTitle, type),
placeHolder: l10n.t(LocalizationKey.helpersTaxonomyHelperCreateNewInputPlaceholder),
ignoreFocusOut: true,
validateInput: (text) => {
if (!text) {
return l10n.t(LocalizationKey.helpersTaxonomyHelperCreateNewInputValidateNoValue);
}
if (options.includes(text)) {
return l10n.t(LocalizationKey.helpersTaxonomyHelperCreateNewInputValidateExists);
}
return null;
}
});
if (!newOption) {
return;
}
this.addToSettings(taxonomyType, newOption, newOption);
}
/**
* Process the taxonomy changes
* @param type
* @param taxonomyType
* @param oldValue
* @param newValue
* @returns
*/
public static async process(
type: 'edit' | 'merge' | 'delete',
taxonomyType: TaxonomyType | string,
oldValue: string,
newValue?: string
) {
// Retrieve all the markdown files
const allFiles = await FilesHelper.getAllFiles();
if (!allFiles) {
return;
}
let taxonomyName: string;
if (taxonomyType === TaxonomyType.Tag) {
taxonomyName = 'tags';
} else if (taxonomyType === TaxonomyType.Category) {
taxonomyName = 'categories';
} else {
taxonomyName = taxonomyType;
}
let progressText = ``;
if (type === 'edit') {
progressText = l10n.t(
LocalizationKey.helpersTaxonomyHelperProcessEdit,
EXTENSION_NAME,
oldValue,
taxonomyName,
newValue || ''
);
} else if (type === 'merge') {
progressText = l10n.t(
LocalizationKey.helpersTaxonomyHelperProcessMerge,
EXTENSION_NAME,
oldValue,
taxonomyName,
newValue || ''
);
} else if (type === 'delete') {
progressText = l10n.t(
LocalizationKey.helpersTaxonomyHelperProcessDelete,
EXTENSION_NAME,
oldValue,
taxonomyName
);
}
window.withProgress(
{
location: ProgressLocation.Notification,
title: progressText,
cancellable: false
},
async (progress) => {
// Set the initial progress
const progressNr = allFiles.length / 100;
progress.report({ increment: 0 });
let i = 0;
for (const file of allFiles) {
progress.report({ increment: ++i / progressNr });
const mdFile = await readFileAsync(parseWinPath(file.fsPath), {
encoding: 'utf8'
});
if (mdFile) {
try {
const article = FrontMatterParser.fromFile(mdFile);
const contentType = ArticleHelper.getContentType(article);
let fieldNames: string[] = this.getFieldsHierarchy(taxonomyType, contentType);
if (fieldNames.length > 0 && article && article.data) {
const { data } = article;
let taxonomies: string | string[] = ContentType.getFieldValue(data, fieldNames);
if (typeof taxonomies === 'string') {
taxonomies = taxonomies.split(`,`);
}
if (taxonomies && taxonomies.length > 0) {
const idx = taxonomies.findIndex((o) => o === oldValue);
if (idx !== -1) {
if (newValue) {
taxonomies[idx] = newValue;
} else {
taxonomies = taxonomies.filter((o) => o !== oldValue);
}
const newTaxValue = [...new Set(taxonomies)].sort();
ContentType.setFieldValue(data, fieldNames, newTaxValue);
const spaces = window.activeTextEditor?.options?.tabSize;
// Update the file
await writeFileAsync(
parseWinPath(file.fsPath),
FrontMatterParser.toFile(article.content, article.data, mdFile, {
indent: spaces || 2
} as DumpOptions as any),
{ encoding: 'utf8' }
);
}
}
}
} catch (e) {
// Continue with the next file
}
}
}
await this.addToSettings(taxonomyType, oldValue, newValue);
if (type === 'edit') {
Notifications.info(l10n.t(LocalizationKey.helpersTaxonomyHelperProcessEditSuccess));
} else if (type === 'merge') {
Notifications.info(l10n.t(LocalizationKey.helpersTaxonomyHelperProcessMergeSuccess));
} else if (type === 'delete') {
Notifications.info(l10n.t(LocalizationKey.helpersTaxonomyHelperProcessDeleteSuccess));
}
}
);
}
/**
* Move a taxonomy value to another taxonomy type
* @param data
* @returns
*/
public static async move(data: { type: string; value: string }) {
const { type, value } = data;
const customTaxs = Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM, true) || [];
let options = ['tags', 'categories', ...customTaxs.map((t) => t.id)];
options = options.filter((o) => o !== type);
const answer = await window.showQuickPick(options, {
title: l10n.t(LocalizationKey.helpersTaxonomyHelperMoveQuickPickTitle, value),
placeHolder: l10n.t(LocalizationKey.helpersTaxonomyHelperMoveQuickPickPlaceholder),
ignoreFocusOut: true
});
if (!answer) {
return;
}
const oldType = this.getTypeFromString(type);
const newType = this.getTypeFromString(answer);
window.withProgress(
{
location: ProgressLocation.Notification,
title: l10n.t(
LocalizationKey.helpersTaxonomyHelperMoveProgressTitle,
EXTENSION_NAME,
value,
type,
answer
),
cancellable: false
},
async (progress) => {
// Retrieve all the markdown files
const allFiles = await FilesHelper.getAllFiles();
if (!allFiles) {
return;
}
// Set the initial progress
const progressNr = allFiles.length / 100;
progress.report({ increment: 0 });
let i = 0;
for (const file of allFiles) {
progress.report({ increment: ++i / progressNr });
const mdFile = await readFileAsync(parseWinPath(file.fsPath), {
encoding: 'utf8'
});
if (mdFile) {
try {
const article = FrontMatterParser.fromFile(mdFile);
const contentType = ArticleHelper.getContentType(article);
let oldFieldNames: string[] = this.getFieldsHierarchy(oldType, contentType);
let newFieldNames: string[] = this.getFieldsHierarchy(newType, contentType, true);
if (oldFieldNames.length > 0 && newFieldNames.length > 0 && article && article.data) {
const { data } = article;
let oldTaxonomies: string | string[] =
ContentType.getFieldValue(data, oldFieldNames) || [];
let newTaxonomies: string | string[] =
ContentType.getFieldValue(data, newFieldNames) || [];
if (typeof oldTaxonomies === 'string') {
oldTaxonomies = oldTaxonomies.split(',');
}
if (typeof newTaxonomies === 'string') {
newTaxonomies = newTaxonomies.split(',');
}
if (oldTaxonomies && oldTaxonomies.length > 0) {
const idx = oldTaxonomies.findIndex((o) => o === value);
if (idx !== -1) {
newTaxonomies.push(value);
const newTaxonomiesValues = [...new Set(newTaxonomies)].sort();
ContentType.setFieldValue(data, newFieldNames, newTaxonomiesValues);
const spaces = window.activeTextEditor?.options?.tabSize;
// Update the file
await writeFileAsync(
parseWinPath(file.fsPath),
FrontMatterParser.toFile(article.content, article.data, mdFile, {
indent: spaces || 2
} as DumpOptions as any),
{ encoding: 'utf8' }
);
}
}
}
} catch (e) {
// Continue with the next file
}
}
}
await this.addToSettings(newType, value, value);
await this.process('delete', oldType, value);
Notifications.info(l10n.t(LocalizationKey.helpersTaxonomyHelperMoveSuccess));
}
);
}
/**
* Retrieve the fields for the taxonomy field
* @returns
*/
private static getFieldsHierarchy(
taxonomyType: TaxonomyType | string,
contentType: IContentType,
fallback: boolean = false
): string[] {
let fieldNames: string[] = [];
if (taxonomyType === TaxonomyType.Tag) {
fieldNames = ContentType.findFieldByType(contentType.fields, 'tags');
} else if (taxonomyType === TaxonomyType.Category) {
fieldNames = ContentType.findFieldByType(contentType.fields, 'categories');
} else {
const taxFieldName = getTaxonomyField(taxonomyType, contentType);
fieldNames = taxFieldName ? [taxFieldName] : [];
}
if (fallback && fieldNames.length === 0) {
let taxFieldName;
if (taxonomyType === TaxonomyType.Tag) {
taxFieldName = getTaxonomyField('tags', contentType);
} else if (taxonomyType === TaxonomyType.Category) {
taxFieldName = getTaxonomyField('categories', contentType);
}
if (taxFieldName) {
fieldNames = [taxFieldName];
}
}
return fieldNames;
}
/**
* Add the taxonomy value to the settings
* @param taxonomyType
* @param oldValue
* @param newValue
*/
private static async addToSettings(
taxonomyType: TaxonomyType | string,
oldValue: string,
newValue?: string
) {
// Update the settings
let options = await this.getTaxonomyOptions(taxonomyType);
const idx = options.findIndex((o) => o === oldValue);
if (newValue) {
// Add or update the new option
if (idx !== -1) {
options[idx] = newValue;
} else {
options.push(newValue);
}
} else {
// Remove the selected option
options = options.filter((o) => o !== oldValue);
}
if (taxonomyType === TaxonomyType.Tag || taxonomyType === TaxonomyType.Category) {
TaxonomyHelper.update(taxonomyType, options);
} else {
await Settings.updateCustomTaxonomyOptions(taxonomyType, options);
}
}
/**
* Get the taxonomy options
* @param taxonomyType
* @returns
*/
private static async getTaxonomyOptions(taxonomyType: TaxonomyType | string) {
let options = [];
if (taxonomyType === TaxonomyType.Tag || taxonomyType === TaxonomyType.Category) {
options = (await TaxonomyHelper.get(taxonomyType)) || [];
} else {
options = Settings.getCustomTaxonomy(taxonomyType);
}
return options;
}
/**
* Retrieve the taxonomy type based from the string
* @param taxonomyType
* @returns
*/
private static getTypeFromString(taxonomyType: string): TaxonomyType | string {
if (taxonomyType === 'tags') {
return TaxonomyType.Tag;
} else if (taxonomyType === 'categories') {
return TaxonomyType.Category;
} else {
return taxonomyType;
}
}
}