mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-12 03:43:01 +02:00
#23 - New command to create article from template
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
## [1.9.0] - 2020-11-25
|
||||
|
||||
- [#23](https://github.com/estruyf/vscode-front-matter/issues/23): Implemented the option to create and use templates for aticle creation (front matter will be updates as well).
|
||||
|
||||
## [1.8.0] - 2020-11-20
|
||||
|
||||
- [#22](https://github.com/estruyf/vscode-front-matter/issues/22): Allow to configure the SEO Title and Description lengths.
|
||||
|
||||
@@ -10,6 +10,16 @@ The extension will automatically verify if your title and description are SEO co
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
## Creating articles from templates
|
||||
|
||||
By default, the extension looks for files stored in a `.templates` folder which should be located in the root of your website project.
|
||||
|
||||
> **Info**: You can overwrite the path, by specifying it with the `frontMatter.templates.folder` setting.
|
||||
|
||||
When adding files in the folder, you'll be able to runt the `Front Matter: New article from template` from a command or explorer menu. It will present you with the article template options. Once you pick one, and specify the title. It creates the file and updates its front matter.
|
||||
|
||||
> **Info**: By default the extension will create articles with a `yyyy-MM-dd` prefix. If you do not want that, or change the date format, you can do this by updating the `frontMatter.templates.prefix` setting.
|
||||
|
||||
## Syntax highlighting for Hugo Shortcodes
|
||||
|
||||

|
||||
|
||||
+25
-1
@@ -46,7 +46,8 @@
|
||||
"onCommand:frontMatter.remap",
|
||||
"onCommand:frontMatter.setDate",
|
||||
"onCommand:frontMatter.setLastModifiedDate",
|
||||
"onCommand:frontMatter.generateSlug"
|
||||
"onCommand:frontMatter.generateSlug",
|
||||
"onCommand:frontMatter.createFromTemplate"
|
||||
],
|
||||
"main": "./dist/extension",
|
||||
"contributes": {
|
||||
@@ -114,6 +115,16 @@
|
||||
"type": "number",
|
||||
"default": 160,
|
||||
"description": "Specifies the optimal description length for SEO (set to `-1` to turn it off)."
|
||||
},
|
||||
"frontMatter.templates.folder": {
|
||||
"type": "string",
|
||||
"default": ".templates",
|
||||
"description": "Specify the folder to use for your article templates."
|
||||
},
|
||||
"frontMatter.templates.prefix": {
|
||||
"type": "string",
|
||||
"default": "yyyy-MM-dd",
|
||||
"description": "Specify the prefix you want to add for your new article filenames."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -153,8 +164,21 @@
|
||||
{
|
||||
"command": "frontMatter.generateSlug",
|
||||
"title": "Front Matter: Generate slug based on article title"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createFromTemplate",
|
||||
"title": "Front Matter: New article from template"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"explorer/context": [
|
||||
{
|
||||
"command": "frontMatter.createFromTemplate",
|
||||
"when": "explorerResourceIsFolder",
|
||||
"group": "Front Matter@1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"grammars": [
|
||||
{
|
||||
"path": "./syntaxes/hugo.tmLanguage.json",
|
||||
|
||||
+23
-10
@@ -4,6 +4,7 @@ import { TaxonomyType } from "../models";
|
||||
import { CONFIG_KEY, SETTING_DATE_FORMAT, EXTENSION_NAME, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_DATE_FIELD } from "../constants/settings";
|
||||
import { format } from "date-fns";
|
||||
import { ArticleHelper, SettingsHelper } from '../helpers';
|
||||
import matter = require('gray-matter');
|
||||
|
||||
|
||||
export class Article {
|
||||
@@ -72,26 +73,19 @@ export class Article {
|
||||
* Sets the article date
|
||||
*/
|
||||
public static async setDate() {
|
||||
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
let article = ArticleHelper.getFrontMatter(editor);
|
||||
if (!article) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateFormat = config.get(SETTING_DATE_FORMAT) as string;
|
||||
const dateField = config.get(SETTING_DATE_FIELD) as string || "date";
|
||||
try {
|
||||
if (dateFormat && typeof dateFormat === "string") {
|
||||
article.data[dateField] = format(new Date(), dateFormat);
|
||||
} else {
|
||||
article.data[dateField] = new Date();
|
||||
}
|
||||
article = this.updateDate(article, true);
|
||||
|
||||
try {
|
||||
ArticleHelper.update(editor, article);
|
||||
} catch (e) {
|
||||
vscode.window.showErrorMessage(`${EXTENSION_NAME}: Something failed while parsing the date format. Check your "${CONFIG_KEY}${SETTING_DATE_FORMAT}" setting.`);
|
||||
@@ -99,6 +93,25 @@ export class Article {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the date in the front matter
|
||||
* @param article
|
||||
*/
|
||||
public static updateDate(article: matter.GrayMatterFile<string>, forceCreate: boolean = false) {
|
||||
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
const dateFormat = config.get(SETTING_DATE_FORMAT) as string;
|
||||
const dateField = config.get(SETTING_DATE_FIELD) as string || "date";
|
||||
|
||||
if (typeof article.data[dateField] !== "undefined" || forceCreate) {
|
||||
if (dateFormat && typeof dateFormat === "string") {
|
||||
article.data[dateField] = format(new Date(), dateFormat);
|
||||
} else {
|
||||
article.data[dateField] = new Date();
|
||||
}
|
||||
}
|
||||
return article;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the article lastmod date
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { CONFIG_KEY, EXTENSION_NAME, SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
|
||||
import { format } from 'date-fns';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { ArticleHelper } from '../helpers';
|
||||
import { Article } from '.';
|
||||
|
||||
export class Template {
|
||||
|
||||
/**
|
||||
* Create from a template
|
||||
*/
|
||||
public static async create(folderPath: string) {
|
||||
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
const folder = config.get<string>(SETTING_TEMPLATES_FOLDER);
|
||||
const prefix = config.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
|
||||
if (!folderPath) {
|
||||
this.showNoTemplates(`Incorrect project folder path retrieved.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!folder) {
|
||||
this.showNoTemplates(`No templates found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const templates = await vscode.workspace.findFiles(`${folder}/**/*`, "**/node_modules/**,**/archetypes/**");
|
||||
if (!templates || templates.length === 0) {
|
||||
this.showNoTemplates(`No templates found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedTemplate = await vscode.window.showQuickPick(templates.map(t => path.basename(t.fsPath)), {
|
||||
placeHolder: `Select the article template to use`
|
||||
});
|
||||
if (!selectedTemplate) {
|
||||
this.showNoTemplates(`No template selected.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const titleValue = await vscode.window.showInputBox({
|
||||
prompt: `What would you like to use as a title for the new article?`,
|
||||
placeHolder: `Article title`
|
||||
});
|
||||
if (!titleValue) {
|
||||
this.showNoTemplates(`You did not specify an article title.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the template read
|
||||
const template = templates.find(t => t.fsPath.endsWith(selectedTemplate));
|
||||
if (!template) {
|
||||
this.showNoTemplates(`Article template could not be found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileExt = path.parse(selectedTemplate).ext;
|
||||
const sanitizedName = sanitize(titleValue.toLowerCase().replace(/ /g, "-"));
|
||||
let newFileName = `${sanitizedName}${fileExt}`;
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
|
||||
}
|
||||
|
||||
const newFilePath = path.join(folderPath, newFileName);
|
||||
if (fs.existsSync(newFilePath)) {
|
||||
this.showNoTemplates(`File already exists, please remove it before creating a new one with the same title.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the new file creation
|
||||
fs.copyFileSync(template.fsPath, newFilePath);
|
||||
|
||||
// Update the properties inside the template
|
||||
let frontMatter = ArticleHelper.getFrontMatterByPath(newFilePath);
|
||||
if (!frontMatter) {
|
||||
this.showNoTemplates(`Something failed when retrieving the newly created file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frontMatter.data) {
|
||||
const fmData = frontMatter.data;
|
||||
if (typeof fmData.title !== "undefined") {
|
||||
fmData.title = titleValue;
|
||||
}
|
||||
if (typeof fmData.slug !== "undefined") {
|
||||
fmData.slug = sanitizedName;
|
||||
}
|
||||
|
||||
frontMatter = Article.updateDate(frontMatter);
|
||||
|
||||
fs.writeFileSync(newFilePath, ArticleHelper.stringifyFrontMatter(frontMatter.content, frontMatter.data), { encoding: "utf8" });
|
||||
}
|
||||
|
||||
const txtDoc = await vscode.workspace.openTextDocument(vscode.Uri.parse(newFilePath));
|
||||
if (txtDoc) {
|
||||
vscode.window.showTextDocument(txtDoc);
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage(`${EXTENSION_NAME}: Your new article has been created.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a warning message when no templates are found
|
||||
*/
|
||||
private static showNoTemplates(value: string) {
|
||||
vscode.window.showWarningMessage(`${EXTENSION_NAME}: ${value}`);
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,7 @@ export const SETTING_REMOVE_QUOTES = "taxonomy.noPropertyValueQuotes";
|
||||
export const SETTING_FRONTMATTER_TYPE = "taxonomy.frontMatterType";
|
||||
|
||||
export const SETTING_SEO_TITLE_LENGTH = "taxonomy.seoTitleLength";
|
||||
export const SETTING_SEO_DESCRIPTION_LENGTH = "taxonomy.seoDescriptionLength";
|
||||
export const SETTING_SEO_DESCRIPTION_LENGTH = "taxonomy.seoDescriptionLength";
|
||||
|
||||
export const SETTING_TEMPLATES_FOLDER = "templates.folder";
|
||||
export const SETTING_TEMPLATES_PREFIX = "templates.prefix";
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Article, Settings, StatusListener } from './commands';
|
||||
import { Template } from './commands/Template';
|
||||
import { TaxonomyType } from './models';
|
||||
|
||||
let frontMatterStatusBar: vscode.StatusBarItem;
|
||||
@@ -45,6 +46,16 @@ export function activate({ subscriptions }: vscode.ExtensionContext) {
|
||||
Article.generateSlug();
|
||||
});
|
||||
|
||||
let createFromTemplate = vscode.commands.registerCommand('frontMatter.createFromTemplate', (e: vscode.Uri) => {
|
||||
let folderPath = "";
|
||||
if (e && e.fsPath) {
|
||||
folderPath = e.fsPath;
|
||||
} else if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
|
||||
folderPath = vscode.workspace.workspaceFolders[0].uri.fsPath;
|
||||
}
|
||||
Template.create(folderPath);
|
||||
});
|
||||
|
||||
const toggleDraftCommand = 'frontMatter.toggleDraft';
|
||||
const toggleDraft = vscode.commands.registerCommand(toggleDraftCommand, async () => {
|
||||
await Article.toggleDraft();
|
||||
@@ -72,6 +83,7 @@ export function activate({ subscriptions }: vscode.ExtensionContext) {
|
||||
subscriptions.push(setDate);
|
||||
subscriptions.push(setLastModifiedDate);
|
||||
subscriptions.push(generateSlug);
|
||||
subscriptions.push(createFromTemplate);
|
||||
subscriptions.push(toggleDraft);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as matter from "gray-matter";
|
||||
import * as fs from "fs";
|
||||
import { stopWords } from '../constants/stopwords-en';
|
||||
import { charMap } from '../constants/charMap';
|
||||
import { CONFIG_KEY, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES } from '../constants';
|
||||
@@ -27,6 +28,27 @@ export class ArticleHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file's front matter by its path
|
||||
* @param filePath
|
||||
*/
|
||||
public static getFrontMatterByPath(filePath: string) {
|
||||
const file = fs.readFileSync(filePath, { encoding: "utf-8" });
|
||||
if (file) {
|
||||
const language: string = getFmLanguage();
|
||||
const langOpts = getFormatOpts(language);
|
||||
let article: matter.GrayMatterFile<string> | null = matter(file, {
|
||||
...TomlEngine,
|
||||
...langOpts
|
||||
});
|
||||
|
||||
if (article && article.data) {
|
||||
return article;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the new information in the file
|
||||
*
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
var illegalRe = /[\/\?<>\\:\*\|"]/g;
|
||||
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
||||
var reservedRe = /^\.+$/;
|
||||
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||
var windowsTrailingRe = /[\. ]+$/;
|
||||
|
||||
function sanitize(input: string, replacement: string) {
|
||||
if (typeof input !== 'string') {
|
||||
throw new Error('Input must be string');
|
||||
}
|
||||
var sanitized = input
|
||||
.replace(illegalRe, replacement)
|
||||
.replace(controlRe, replacement)
|
||||
.replace(reservedRe, replacement)
|
||||
.replace(windowsReservedRe, replacement)
|
||||
.replace(windowsTrailingRe, replacement);
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export default function (input: string, options?: any) {
|
||||
var replacement = (options && options.replacement) || '';
|
||||
var output = sanitize(input, replacement);
|
||||
if (replacement === '') {
|
||||
return output;
|
||||
}
|
||||
return sanitize(output, '');
|
||||
}
|
||||
Reference in New Issue
Block a user