#23 - New command to create article from template

This commit is contained in:
Elio Struyf
2020-11-25 11:15:53 +01:00
parent 3c4d9feea8
commit 6b798281ac
9 changed files with 238 additions and 12 deletions
+4
View File
@@ -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
View File
@@ -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
![Shortcode syntax highlighting](./assets/syntax-highlighting.png)
+25 -1
View File
@@ -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
View File
@@ -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
*/
+111
View File
@@ -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}`);
}
}
+4 -1
View File
@@ -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";
+12
View File
@@ -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);
}
+22
View File
@@ -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
*
+27
View 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, '');
}