forked from iarv/vscode-front-matter
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9618a89528 | ||
|
|
14f0af2754 | ||
|
|
ebe248670d | ||
|
|
511960c4a9 | ||
|
|
31fd1f93ce | ||
|
|
6625b69170 | ||
|
|
9e8533fbb8 | ||
|
|
9c9cbb7dcb | ||
|
|
079a13e161 | ||
|
|
69c1e587d0 | ||
|
|
3996252531 | ||
|
|
4fddda65e6 | ||
|
|
5916344092 | ||
|
|
b96722dd69 | ||
|
|
263ccab311 | ||
|
|
3571af82c7 | ||
|
|
c60520c0ff | ||
|
|
b473431eae | ||
|
|
cbf434f741 | ||
|
|
04c401207f | ||
|
|
7291e6aac6 | ||
|
|
a7aab96f0e | ||
|
|
f500749644 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,31 @@
|
||||
# Change Log
|
||||
|
||||
## [5.4.0] - 2021-11-05
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#166](https://github.com/estruyf/vscode-front-matter/issues/166): Added preview button to the panel base view
|
||||
- [#167](https://github.com/estruyf/vscode-front-matter/issues/167): Allow to set the preview path per content type
|
||||
|
||||
## [5.3.1] - 2021-10-29
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#163](https://github.com/estruyf/vscode-front-matter/issues/163): Setting workspace state instead of global state for the media view
|
||||
|
||||
## [5.3.0] - 2021-10-28 - [Release Notes](https://beta.frontmatter.codes/updates/v5.3.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#158](https://github.com/estruyf/vscode-front-matter/issues/158): Add support for non-boolean draft/publish status fields
|
||||
- [#159](https://github.com/estruyf/vscode-front-matter/issues/159): Enhancements to SEO checks: Slug check, keyword details, more article information
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Value check when generating slug from title
|
||||
- Fix for date time formatting with `DD` and `YYYY` tokens
|
||||
- Fix in tag space replacing when object is passed
|
||||
|
||||
## [5.2.0] - 2021-10-19
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
@@ -48,6 +48,10 @@ Our main extension features are:
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 5**
|
||||
|
||||
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
|
||||
**Version 4**
|
||||
|
||||
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
|
||||
@@ -46,6 +46,10 @@ Our main extension features are:
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 5**
|
||||
|
||||
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
|
||||
**Version 4**
|
||||
|
||||
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
|
||||
@@ -356,8 +356,18 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.table__cell__seo_details {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.table__cell__validation {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table__cell__validation div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.table__cell__validation .valid {
|
||||
@@ -368,6 +378,15 @@
|
||||
color: #E6AF2E;
|
||||
}
|
||||
|
||||
.table__cell__validation div span + span {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.seo__status__note {
|
||||
font-size: 10px;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
/* Fields */
|
||||
.field__toggle {
|
||||
position: relative;
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "5.2.0",
|
||||
"version": "5.4.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -6386,6 +6386,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"url-join-ts": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/url-join-ts/-/url-join-ts-1.0.5.tgz",
|
||||
"integrity": "sha512-u+5gi7JyOwhj58ZKwkmkzFGHuepTpmwjqfUDGVjsJJstsCz63CJAINixgJaDcMbmuyWPJIxbtBpIfaDgOQ9KMQ==",
|
||||
"dev": true
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
|
||||
63
package.json
63
package.json
@@ -3,7 +3,7 @@
|
||||
"displayName": "Front Matter",
|
||||
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "5.2.0",
|
||||
"version": "5.4.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -97,6 +97,43 @@
|
||||
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.draftField": {
|
||||
"type": "object",
|
||||
"markdownDescription": "Define the draft field you want to use to manage your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.draftField)",
|
||||
"default": {
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
},
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"boolean",
|
||||
"choice"
|
||||
],
|
||||
"description": ""
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the field to use"
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"description": "List of choices for the field",
|
||||
"items": {
|
||||
"type": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.fmHighlight": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -219,6 +256,12 @@
|
||||
"markdownDescription": "Specify the path you want to add after the host and before your slug. This can be used for instance to include the year/month like: `yyyy/MM`. The date will be generated based on the article its date field value. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.preview.pathname)",
|
||||
"scope": "Site preview"
|
||||
},
|
||||
"frontMatter.site.baseURL": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Specify the base URL of your site, this will be used for SEO checks. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.site.baseURL)",
|
||||
"scope": "Site"
|
||||
},
|
||||
"frontMatter.taxonomy.alignFilename": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
@@ -273,7 +316,8 @@
|
||||
"image",
|
||||
"choice",
|
||||
"tags",
|
||||
"categories"
|
||||
"categories",
|
||||
"draft"
|
||||
],
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
@@ -340,6 +384,14 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if you want to create a folder when creating new content."
|
||||
},
|
||||
"previewPath": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
],
|
||||
"default": null,
|
||||
"description": "Defines a custom preview path for the content type."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -456,6 +508,12 @@
|
||||
"markdownDescription": "Specifies the optimal description length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.seodescriptionlength)",
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.seoSlugLength": {
|
||||
"type": "number",
|
||||
"default": 75,
|
||||
"markdownDescription": "Specifies the optimal slug length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.seoSlugLength)",
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.seoTitleLength": {
|
||||
"type": "number",
|
||||
"default": 60,
|
||||
@@ -726,6 +784,7 @@
|
||||
"ts-loader": "8.0.3",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "4.0.2",
|
||||
"url-join-ts": "^1.0.5",
|
||||
"wc-react": "github:estruyf/wc-react",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-cli": "3.3.12"
|
||||
|
||||
@@ -5,11 +5,12 @@ import { format } from "date-fns";
|
||||
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
|
||||
import matter = require('gray-matter');
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { extname, basename } from 'path';
|
||||
import { extname, basename, parse, dirname } from 'path';
|
||||
import { COMMAND_NAME, DefaultFields } from '../constants';
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
|
||||
|
||||
export class Article {
|
||||
@@ -172,7 +173,7 @@ export class Article {
|
||||
|
||||
let newFileName = `${slugName}${ext}`;
|
||||
if (filePrefix && typeof filePrefix === "string") {
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(filePrefix))}-${newFileName}`;
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(filePrefix) as string)}-${newFileName}`;
|
||||
}
|
||||
|
||||
const newPath = editor.document.uri.fsPath.replace(fileName, newFileName);
|
||||
@@ -183,7 +184,7 @@ export class Article {
|
||||
await vscode.workspace.fs.rename(editor.document.uri, vscode.Uri.file(newPath), {
|
||||
overwrite: false
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
Notifications.error(`Failed to rename file: ${e?.message || e}`);
|
||||
}
|
||||
}
|
||||
@@ -191,6 +192,31 @@ export class Article {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the slug from the front matter
|
||||
*/
|
||||
public static getSlug() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = parseWinPath(editor.document.fileName);
|
||||
|
||||
if (!file.endsWith(`.md`) && !file.endsWith(`.mdx`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedFile = parse(file);
|
||||
|
||||
if (parsedFile.name.toLowerCase() !== "index") {
|
||||
return parsedFile.name;
|
||||
}
|
||||
|
||||
const folderName = basename(dirname(file));
|
||||
return folderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the page its draft mode
|
||||
*/
|
||||
@@ -244,7 +270,7 @@ export class Article {
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
|
||||
if (dateFormat && typeof dateFormat === "string") {
|
||||
return format(dateValue, DateHelper.formatUpdate(dateFormat));
|
||||
return format(dateValue, DateHelper.formatUpdate(dateFormat) as string);
|
||||
} else {
|
||||
return typeof dateValue.toISOString === 'function' ? dateValue.toISOString() : dateValue?.toString();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID } from '../constants';
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID, SETTINGS_CONTENT_DRAFT_FIELD } from '../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { basename, dirname, extname, join, parse } from "path";
|
||||
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
|
||||
import { Settings as SettingsHelper } from '../helpers';
|
||||
import { Framework, TaxonomyType } from '../models';
|
||||
import { DraftField, Framework, TaxonomyType } from '../models';
|
||||
import { Folders } from './Folders';
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
|
||||
@@ -25,6 +25,7 @@ import imageSize from 'image-size';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { FrameworkDetector } from '../helpers/FrameworkDetector';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
@@ -161,7 +162,7 @@ export class Dashboard {
|
||||
}
|
||||
break;
|
||||
case DashboardMessage.setPageViewType:
|
||||
Extension.getInstance().setState(ExtensionState.PagesView, msg.data);
|
||||
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
|
||||
break;
|
||||
case DashboardMessage.getMedia:
|
||||
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder);
|
||||
@@ -275,9 +276,10 @@ export class Dashboard {
|
||||
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
|
||||
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
|
||||
versionInfo: ext.getVersion(),
|
||||
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView),
|
||||
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView, "workspace"),
|
||||
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
|
||||
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
draftField: SettingsHelper.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
|
||||
contentFolders: Folders.get().map(f => f.path),
|
||||
crntFramework: SettingsHelper.get<string>(SETTINGS_FRAMEWORK_ID),
|
||||
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
@@ -323,7 +325,7 @@ export class Dashboard {
|
||||
|
||||
// If the static folder is not set, retreive the last opened location
|
||||
if (!selectedFolder) {
|
||||
const stateValue = await Extension.getInstance().getState<string | undefined>(ExtensionState.SelectedFolder);
|
||||
const stateValue = await Extension.getInstance().getState<string | undefined>(ExtensionState.SelectedFolder, "workspace");
|
||||
|
||||
if (stateValue !== HOME_PAGE_NAVIGATION_ID) {
|
||||
// Support for page bundles
|
||||
@@ -438,7 +440,7 @@ export class Dashboard {
|
||||
}
|
||||
|
||||
// Store the last opened folder
|
||||
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder);
|
||||
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder, "workspace");
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.media,
|
||||
@@ -479,7 +481,7 @@ export class Dashboard {
|
||||
fmModified: file.mtime,
|
||||
fmFilePath: file.filePath,
|
||||
fmFileName: file.fileName,
|
||||
fmDraft: article?.data.draft ? "Draft" : "Published",
|
||||
fmDraft: ContentType.getDraftStatus(article?.data),
|
||||
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
|
||||
// Make sure these are always set
|
||||
title: article?.data.title,
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Settings } from '../helpers';
|
||||
import { PreviewSettings } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { Article } from '.';
|
||||
import { urlJoin } from 'url-join-ts';
|
||||
|
||||
|
||||
export class Preview {
|
||||
@@ -32,12 +34,25 @@ export class Preview {
|
||||
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
|
||||
let slug = article?.data ? article.data.slug : "";
|
||||
|
||||
if (settings.pathname) {
|
||||
let pathname = settings.pathname;
|
||||
if (article?.data) {
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
if (contentType && contentType.previewPath) {
|
||||
pathname = contentType.previewPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
slug = Article.getSlug();
|
||||
}
|
||||
|
||||
if (pathname) {
|
||||
const articleDate = ArticleHelper.getDate(article);
|
||||
|
||||
try {
|
||||
slug = join(format(articleDate || new Date(), DateHelper.formatUpdate(settings.pathname)), slug);
|
||||
slug = join(format(articleDate || new Date(), DateHelper.formatUpdate(pathname) as string), slug);
|
||||
} catch (error) {
|
||||
slug = join(settings.pathname, slug);
|
||||
slug = join(pathname, slug);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,9 +128,9 @@ export class Preview {
|
||||
</head>
|
||||
<body>
|
||||
<div class="slug">
|
||||
<input type="text" value="${join(localhostUrl.toString(), slug)}" disabled />
|
||||
<input type="text" value="${urlJoin(localhostUrl.toString(), slug || '')}" disabled />
|
||||
</div>
|
||||
<iframe src="${join(localhostUrl.toString(), slug)}" >
|
||||
<iframe src="${urlJoin(localhostUrl.toString(), slug || '')}" >
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
@@ -132,4 +147,4 @@ export class Preview {
|
||||
pathname
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
|
||||
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DefaultFields } from '../constants';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
|
||||
export class StatusListener {
|
||||
|
||||
@@ -15,6 +16,11 @@ export class StatusListener {
|
||||
public static async verify(frontMatterSB: vscode.StatusBarItem, collection: vscode.DiagnosticCollection) {
|
||||
const draftMsg = "in draft";
|
||||
const publishMsg = "to publish";
|
||||
|
||||
const draft = ContentType.getDraftField();
|
||||
if (!draft || draft.type !== "boolean") {
|
||||
frontMatterSB.hide();
|
||||
}
|
||||
|
||||
let editor = vscode.window.activeTextEditor;
|
||||
if (editor && ArticleHelper.isMarkdownFile()) {
|
||||
|
||||
@@ -5,6 +5,7 @@ export const DEFAULT_CONTENT_TYPE_NAME = 'default';
|
||||
export const DEFAULT_CONTENT_TYPE: ContentType = {
|
||||
"name": "default",
|
||||
"pageBundle": false,
|
||||
"previewPath": null,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Title",
|
||||
@@ -29,7 +30,7 @@ export const DEFAULT_CONTENT_TYPE: ContentType = {
|
||||
{
|
||||
"title": "Is in draft",
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
"type": "draft"
|
||||
},
|
||||
{
|
||||
"title": "Tags",
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
export const DefaultFields = {
|
||||
PublishingDate: `date`,
|
||||
LastModified: `lastmod`,
|
||||
Description: `description`
|
||||
Description: `description`,
|
||||
Slug: `slug`
|
||||
};
|
||||
|
||||
@@ -20,6 +20,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_SLUG_LENGTH = "taxonomy.seoSlugLength";
|
||||
export const SETTING_SEO_DESCRIPTION_LENGTH = "taxonomy.seoDescriptionLength";
|
||||
export const SETTING_SEO_CONTENT_MIN_LENGTH = "taxonomy.seoContentLengh";
|
||||
export const SETTING_SEO_DESCRIPTION_FIELD = "taxonomy.seoDescriptionField";
|
||||
@@ -38,12 +39,15 @@ export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
|
||||
export const SETTINGS_CONTENT_PAGE_FOLDERS = "content.pageFolders";
|
||||
export const SETTINGS_CONTENT_STATIC_FOLDER = "content.publicFolder";
|
||||
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
|
||||
export const SETTINGS_CONTENT_DRAFT_FIELD = "content.draftField";
|
||||
|
||||
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
|
||||
export const SETTINGS_FRAMEWORK_ID = "framework.id";
|
||||
|
||||
export const SETTING_SITE_BASEURL = "site.baseURL";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
@@ -41,7 +41,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
|
||||
<div className="p-4 w-full">
|
||||
<div className={`flex justify-between items-center`}>
|
||||
<Status draft={!!draft} />
|
||||
<Status draft={draft} />
|
||||
|
||||
<DateField value={date} />
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
<DateField value={date} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Status draft={!!draft} />
|
||||
<Status draft={draft} />
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Tab } from '../constants/Tab';
|
||||
import { TabAtom } from '../state';
|
||||
import { SettingsAtom, TabAtom } from '../state';
|
||||
|
||||
export interface INavigationProps {
|
||||
totalPages: number;
|
||||
@@ -15,19 +15,47 @@ export const tabs = [
|
||||
|
||||
export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPages}: React.PropsWithChildren<INavigationProps>) => {
|
||||
const [ crntTab, setCrntTab ] = useRecoilState(TabAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
return (
|
||||
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tab.id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tab.id)}
|
||||
>
|
||||
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))}
|
||||
{
|
||||
settings?.draftField?.type === "boolean" ? (
|
||||
tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tab.id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tab.id)}
|
||||
>
|
||||
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`${tabs[0].id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tabs[0].id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tabs[0].id)}
|
||||
>
|
||||
{tabs[0].name}{(tabs[0].id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
|
||||
{
|
||||
settings?.draftField?.choices?.map((value, idx) => (
|
||||
<button
|
||||
key={`${value}-${idx}`}
|
||||
className={`${value === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm first-letter:uppercase`}
|
||||
aria-current={value === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(value)}
|
||||
>
|
||||
{value}{(value === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsAtom } from '../state';
|
||||
|
||||
export interface IStatusProps {
|
||||
draft: boolean;
|
||||
draft: boolean | string;
|
||||
}
|
||||
|
||||
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
if (settings?.draftField && settings.draftField.type === "choice") {
|
||||
if (draft) {
|
||||
return <span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draft}</span>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Tab } from '../constants/Tab';
|
||||
import { Page } from '../models/Page';
|
||||
import Fuse from 'fuse.js';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { CategorySelector, FolderSelector, SearchSelector, SortingSelector, TabSelector, TagSelector } from '../state';
|
||||
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingSelector, TabSelector, TagSelector } from '../state';
|
||||
|
||||
const fuseOptions: Fuse.IFuseOptions<Page> = {
|
||||
keys: [
|
||||
@@ -16,6 +16,7 @@ const fuseOptions: Fuse.IFuseOptions<Page> = {
|
||||
|
||||
export default function usePages(pages: Page[]) {
|
||||
const [ pageItems, setPageItems ] = useState<Page[]>([]);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const tab = useRecoilValue(TabSelector);
|
||||
const sorting = useRecoilValue(SortingSelector);
|
||||
const folder = useRecoilValue(FolderSelector);
|
||||
@@ -24,6 +25,8 @@ export default function usePages(pages: Page[]) {
|
||||
const category = useRecoilValue(CategorySelector);
|
||||
|
||||
useEffect(() => {
|
||||
const draftField = settings?.draftField;
|
||||
|
||||
// Check if search needs to be performed
|
||||
let searchedPages = pages;
|
||||
if (search) {
|
||||
@@ -34,12 +37,22 @@ export default function usePages(pages: Page[]) {
|
||||
|
||||
// Filter the pages
|
||||
let pagesToShow: Page[] = Object.assign([], searchedPages);
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = searchedPages.filter(page => !page.draft);
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = searchedPages.filter(page => !!page.draft);
|
||||
|
||||
if (draftField && draftField.type === 'choice') {
|
||||
if (tab !== Tab.All) {
|
||||
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
const draftFieldName = draftField?.name || "draft";
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the pages
|
||||
@@ -69,7 +82,7 @@ export default function usePages(pages: Page[]) {
|
||||
}
|
||||
|
||||
setPageItems(pagesSorted);
|
||||
}, [ pages, tab, sorting, folder, search, tag, category ]);
|
||||
}, [ settings?.draftField, pages, tab, sorting, folder, search, tag, category ]);
|
||||
|
||||
return {
|
||||
pageItems
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { VersionInfo } from '../../models/VersionInfo';
|
||||
import { ViewType } from '../state';
|
||||
import { ContentFolder } from '../../models/ContentFolder';
|
||||
import { ContentType, Framework } from '../../models';
|
||||
import { ContentType, DraftField, Framework } from '../../models';
|
||||
|
||||
export interface Settings {
|
||||
beta: boolean;
|
||||
@@ -19,4 +19,5 @@ export interface Settings {
|
||||
contentFolders: string[];
|
||||
crntFramework: string;
|
||||
framework: Framework | null | undefined;
|
||||
draftField: DraftField | null | undefined;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { atom } from 'recoil';
|
||||
import { Tab } from '../../constants/Tab';
|
||||
|
||||
export const TabAtom = atom<Tab>({
|
||||
export const TabAtom = atom<Tab | string>({
|
||||
key: 'TabAtom',
|
||||
default: Tab.All
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { Template } from '../commands/Template';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS, SETTINGS_CONTENT_DRAFT_FIELD, SETTING_SEO_SLUG_LENGTH, SETTING_SITE_BASEURL } from '../constants';
|
||||
import * as os from 'os';
|
||||
import { PanelSettings, CustomScript as ICustomScript } from '../models/PanelSettings';
|
||||
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode";
|
||||
@@ -9,7 +9,7 @@ import { Command } from "../panelWebView/Command";
|
||||
import { CommandToCode } from '../panelWebView/CommandToCode';
|
||||
import { Article } from '../commands';
|
||||
import { TagType } from '../panelWebView/TagType';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { DraftField, TaxonomyType } from '../models';
|
||||
import { exec } from 'child_process';
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||
import { Content } from 'mdast';
|
||||
@@ -22,6 +22,7 @@ import { Extension } from '../helpers/Extension';
|
||||
import { Dashboard } from '../commands/Dashboard';
|
||||
import { ImageHelper } from '../helpers/ImageHelper';
|
||||
import { CustomScript } from '../helpers/CustomScript';
|
||||
import { Link, Parent } from 'mdast-util-from-markdown/lib';
|
||||
|
||||
const FILE_LIMIT = 10;
|
||||
|
||||
@@ -269,6 +270,15 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check slug
|
||||
if (!updatedMetadata[DefaultFields.Slug]) {
|
||||
const slug = Article.getSlug();
|
||||
|
||||
if (slug) {
|
||||
updatedMetadata[DefaultFields.Slug] = slug;
|
||||
}
|
||||
}
|
||||
|
||||
this.postWebviewMessage({ command: Command.metadata, data: {
|
||||
...updatedMetadata
|
||||
@@ -380,6 +390,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
data: {
|
||||
seo: {
|
||||
title: Settings.get(SETTING_SEO_TITLE_LENGTH) as number || -1,
|
||||
slug: Settings.get(SETTING_SEO_SLUG_LENGTH) as number || -1,
|
||||
description: Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1,
|
||||
content: Settings.get(SETTING_SEO_CONTENT_MIN_LENGTH) as number || -1,
|
||||
descriptionField: Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description
|
||||
@@ -403,7 +414,8 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
preview: Preview.getSettings(),
|
||||
commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [],
|
||||
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
dashboardViewData: Dashboard.viewData
|
||||
dashboardViewData: Dashboard.viewData,
|
||||
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD)
|
||||
} as PanelSettings
|
||||
});
|
||||
}
|
||||
@@ -475,6 +487,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* Get article details
|
||||
*/
|
||||
private getArticleDetails() {
|
||||
const baseUrl = Settings.get<string>(SETTING_SITE_BASEURL);
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return null;
|
||||
@@ -491,13 +504,36 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
content = content.replace(/({{(.*?)}})/g, ''); // remove hugo shortcodes
|
||||
|
||||
const mdTree = fromMarkdown(content);
|
||||
const headings = mdTree.children.filter(node => node.type === 'heading').length;
|
||||
const paragraphs = mdTree.children.filter(node => node.type === 'paragraph').length;
|
||||
const elms: Parent[] | Link[] = this.getAllElms(mdTree);
|
||||
|
||||
const headings = elms.filter(node => node.type === 'heading');
|
||||
const paragraphs = elms.filter(node => node.type === 'paragraph').length;
|
||||
const images = elms.filter(node => node.type === 'image').length;
|
||||
const links: string[] = elms.filter(node => node.type === 'link').map(node => (node as Link).url);
|
||||
|
||||
const internalLinks = links.filter(link => !link.startsWith('http') || (baseUrl && link.toLowerCase().includes((baseUrl || "").toLowerCase()))).length;
|
||||
let externalLinks = links.filter(link => link.startsWith('http'));
|
||||
if (baseUrl) {
|
||||
externalLinks = externalLinks.filter(link => !link.toLowerCase().includes(baseUrl.toLowerCase()));
|
||||
}
|
||||
|
||||
const headers = [];
|
||||
for (const header of headings) {
|
||||
const text = header?.children?.filter((node: any) => node.type === 'text').map((node: any) => node.value).join(" ");
|
||||
if (text) {
|
||||
headers.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
const wordCount = this.wordCount(0, mdTree);
|
||||
|
||||
return {
|
||||
headings,
|
||||
headings: headings.length,
|
||||
headingsText: headers,
|
||||
paragraphs,
|
||||
images,
|
||||
internalLinks,
|
||||
externalLinks: externalLinks.length,
|
||||
wordCount,
|
||||
content: article.content
|
||||
};
|
||||
@@ -506,6 +542,21 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
return null;
|
||||
}
|
||||
|
||||
private getAllElms(node: Content | any, allElms?: any[]): any[] {
|
||||
if (!allElms) {
|
||||
allElms = [];
|
||||
}
|
||||
|
||||
if (node.children?.length > 0) {
|
||||
for (const child of node.children) {
|
||||
allElms.push(Object.assign({}, child));
|
||||
this.getAllElms(child, allElms);
|
||||
}
|
||||
}
|
||||
|
||||
return allElms;
|
||||
}
|
||||
|
||||
private counts(acc: any, node: any) {
|
||||
// add 1 to an initial or existing value
|
||||
acc[node.type] = (acc[node.type] || 0) + 1;
|
||||
|
||||
@@ -126,7 +126,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
});
|
||||
|
||||
// Settings promotion command
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, () => { console.log('promote'); SettingsHelper.promote(); }));
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, SettingsHelper.promote ));
|
||||
|
||||
// Collapse all sections in the webview
|
||||
const collapseAll = vscode.commands.registerCommand(COMMAND_NAME.collapseSections, () => {
|
||||
|
||||
@@ -204,7 +204,7 @@ export class ArticleHelper {
|
||||
let newFileName = `${sanitizedName}.md`;
|
||||
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix))}-${newFileName}`;
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}-${newFileName}`;
|
||||
}
|
||||
|
||||
newFilePath = join(folderPath, newFileName);
|
||||
@@ -251,7 +251,6 @@ export class ArticleHelper {
|
||||
const items = [{
|
||||
title: "Check file",
|
||||
action: async () => {
|
||||
console.log(fileName);
|
||||
await EditorHelper.showFile(fileName)
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -1,18 +1,59 @@
|
||||
import { ArticleHelper, Settings } from ".";
|
||||
import { SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from "../constants";
|
||||
import { ContentType as IContentType } from '../models';
|
||||
import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
|
||||
import { ContentType as IContentType, DraftField } from '../models';
|
||||
import { Uri, workspace, window } from 'vscode';
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Questions } from "./Questions";
|
||||
import { format } from "date-fns";
|
||||
import { join } from "path";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { writeFileSync } from "fs";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
|
||||
|
||||
|
||||
export class ContentType {
|
||||
|
||||
/**
|
||||
* Retrieve the draft field
|
||||
* @returns
|
||||
*/
|
||||
public static getDraftField() {
|
||||
const draftField = Settings.get<DraftField | null | undefined>(SETTINGS_CONTENT_DRAFT_FIELD);
|
||||
if (draftField) {
|
||||
return draftField;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the field its status
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
public static getDraftStatus(data: { [field: string]: any }) {
|
||||
const contentType = ArticleHelper.getContentType(data);
|
||||
const draftSetting = ContentType.getDraftField();
|
||||
|
||||
const draftField = contentType.fields.find(f => f.type === "draft");
|
||||
|
||||
let fieldValue = null;
|
||||
|
||||
if (draftField) {
|
||||
fieldValue = data[draftField.name];
|
||||
} else if (draftSetting && data && data[draftSetting.name]) {
|
||||
fieldValue = data[draftSetting.name];
|
||||
}
|
||||
|
||||
if (draftSetting && fieldValue) {
|
||||
if (draftSetting.type === "boolean") {
|
||||
return fieldValue ? "Draft" : "Published";
|
||||
} else {
|
||||
return fieldValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content based on content types
|
||||
* @returns
|
||||
|
||||
@@ -89,8 +89,6 @@ export class CustomScript {
|
||||
articleData = `'${articleData}'`;
|
||||
}
|
||||
|
||||
console.log(articleData);
|
||||
|
||||
exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${contentPath}" ${articleData}`, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${script.title}: ${error.message}`);
|
||||
|
||||
@@ -3,7 +3,11 @@ import { parse, parseISO, parseJSON } from "date-fns";
|
||||
|
||||
export class DateHelper {
|
||||
|
||||
public static formatUpdate(value: string) {
|
||||
public static formatUpdate(value: string | null | undefined): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value.replace(/YYYY/g, 'yyyy');
|
||||
value = value.replace(/DD/g, 'dd');
|
||||
return value;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class Settings {
|
||||
* Check if the setting is present in the workspace and ask to promote them to the global settings
|
||||
*/
|
||||
public static async checkToPromote() {
|
||||
const isPromoted = await Extension.getInstance().getState<boolean | undefined>(ExtensionState.SettingPromoted);
|
||||
const isPromoted = await Extension.getInstance().getState<boolean | undefined>(ExtensionState.SettingPromoted, "workspace");
|
||||
if (!isPromoted) {
|
||||
if (Settings.hasSettings()) {
|
||||
window.showInformationMessage(`You have local settings. Would you like to promote them to the global settings ("frontmatter.json")?`, 'Yes', 'No').then(async (result) => {
|
||||
@@ -47,7 +47,7 @@ export class Settings {
|
||||
}
|
||||
|
||||
if (result === "No" || result === "Yes") {
|
||||
Extension.getInstance().setState(ExtensionState.SettingPromoted, true);
|
||||
Extension.getInstance().setState(ExtensionState.SettingPromoted, true, "workspace");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,14 +14,18 @@ export class SlugHelper {
|
||||
|
||||
// Remove punctuation from input string, and split it into words.
|
||||
let cleanTitle = this.removePunctuation(articleTitle);
|
||||
cleanTitle = cleanTitle.toLowerCase();
|
||||
// Split into words
|
||||
let words = cleanTitle.split(/\s/);
|
||||
// Removing stop words
|
||||
words = this.removeStopWords(words);
|
||||
cleanTitle = words.join("-");
|
||||
cleanTitle = this.replaceCharacters(cleanTitle);
|
||||
return cleanTitle;
|
||||
if (cleanTitle) {
|
||||
cleanTitle = cleanTitle.toLowerCase();
|
||||
// Split into words
|
||||
let words = cleanTitle.split(/\s/);
|
||||
// Removing stop words
|
||||
words = this.removeStopWords(words);
|
||||
cleanTitle = words.join("-");
|
||||
cleanTitle = this.replaceCharacters(cleanTitle);
|
||||
return cleanTitle;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,6 +34,10 @@ export class SlugHelper {
|
||||
* @param value
|
||||
*/
|
||||
private static removePunctuation(value: string): string {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const punctuationless = value?.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
// Remove double spaces
|
||||
return punctuationless?.replace(/\s{2,}/g," ");
|
||||
|
||||
5
src/models/DraftField.ts
Normal file
5
src/models/DraftField.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface DraftField {
|
||||
name: string;
|
||||
type: "boolean" | "choice";
|
||||
choices?: string[];
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FileType } from "vscode";
|
||||
import { DraftField } from ".";
|
||||
import { Choice } from "./Choice";
|
||||
import { DashboardData } from "./DashboardData";
|
||||
|
||||
@@ -17,19 +18,21 @@ export interface PanelSettings {
|
||||
preview: PreviewSettings;
|
||||
contentTypes: ContentType[];
|
||||
dashboardViewData: DashboardData | undefined;
|
||||
draftField: DraftField;
|
||||
}
|
||||
|
||||
export interface ContentType {
|
||||
name: string;
|
||||
fields: Field[];
|
||||
|
||||
previewPath?: string | null;
|
||||
pageBundle?: boolean;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
title?: string;
|
||||
name: string;
|
||||
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories";
|
||||
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft";
|
||||
choices?: string[] | Choice[];
|
||||
single?: boolean;
|
||||
multiple?: boolean;
|
||||
@@ -43,6 +46,7 @@ export interface DateInfo {
|
||||
|
||||
export interface SEO {
|
||||
title: number;
|
||||
slug: number;
|
||||
description: number;
|
||||
content: number;
|
||||
descriptionField: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './Choice';
|
||||
export * from './ContentFolder';
|
||||
export * from './DashboardData';
|
||||
export * from './DraftField';
|
||||
export * from './Framework';
|
||||
export * from './MediaPaths';
|
||||
export * from './PanelSettings';
|
||||
|
||||
@@ -27,8 +27,8 @@ const Actions: React.FunctionComponent<IActionsProps> = (props: React.PropsWithC
|
||||
|
||||
{
|
||||
(settings && settings.scripts && settings.scripts.length > 0) && (
|
||||
settings.scripts.map((value) => (
|
||||
<CustomScript key={value.title.replace(/ /g, '')} {...value} />
|
||||
settings.scripts.map((value, idx) => (
|
||||
<CustomScript key={value?.title?.replace(/ /g, '') || idx} {...value} />
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ export interface IArticleDetailsProps {
|
||||
headings: number;
|
||||
paragraphs: number;
|
||||
wordCount: number;
|
||||
internalLinks: number;
|
||||
externalLinks: number;
|
||||
images: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +45,33 @@ const ArticleDetails: React.FunctionComponent<IArticleDetailsProps> = ({details}
|
||||
</VsTableRow>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
details?.internalLinks !== undefined && (
|
||||
<VsTableRow>
|
||||
<VsTableCell>Internal links</VsTableCell>
|
||||
<VsTableCell>{details.internalLinks}</VsTableCell>
|
||||
</VsTableRow>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
details?.externalLinks !== undefined && (
|
||||
<VsTableRow>
|
||||
<VsTableCell>External links</VsTableCell>
|
||||
<VsTableCell>{details.externalLinks}</VsTableCell>
|
||||
</VsTableRow>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
details?.images !== undefined && (
|
||||
<VsTableRow>
|
||||
<VsTableCell>Images</VsTableCell>
|
||||
<VsTableCell>{details.images}</VsTableCell>
|
||||
</VsTableRow>
|
||||
)
|
||||
}
|
||||
</VsTableBody>
|
||||
</VsTable>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,10 @@ const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndF
|
||||
MessageHelper.sendMessage(CommandToCode.createContent);
|
||||
};
|
||||
|
||||
const openPreview = () => {
|
||||
MessageHelper.sendMessage(CommandToCode.openPreview);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="frontmatter">
|
||||
<div className={`ext_actions`}>
|
||||
@@ -37,6 +41,7 @@ const BaseView: React.FunctionComponent<IBaseViewProps> = ({settings, folderAndF
|
||||
<button onClick={openDashboard}>Open dashboard</button>
|
||||
<button onClick={initProject} disabled={settings?.isInitialized}>Initialize project</button>
|
||||
<button onClick={createContent} disabled={!settings?.isInitialized}>Create new content</button>
|
||||
<button onClick={openPreview} disabled={!settings?.preview?.host}>Open site preview</button>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({lab
|
||||
selected={dateValue as Date || new Date()}
|
||||
onChange={onDateChange}
|
||||
timeInputLabel="Time:"
|
||||
dateFormat={format || "MM/dd/yyyy HH:mm"}
|
||||
dateFormat={DateHelper.formatUpdate(format) || "MM/dd/yyyy HH:mm"}
|
||||
customInput={(<CustomInput />)}
|
||||
showTimeInput
|
||||
/>
|
||||
|
||||
40
src/panelWebView/components/Fields/DraftField.tsx
Normal file
40
src/panelWebView/components/Fields/DraftField.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { RocketIcon } from '../Icons/RocketIcon';
|
||||
import { VsLabel } from '../VscodeComponents';
|
||||
import { ChoiceField } from './ChoiceField';
|
||||
import { Toggle } from './Toggle';
|
||||
|
||||
export interface IDraftFieldProps {
|
||||
label: string;
|
||||
type: "boolean" | "choice";
|
||||
value: boolean | string | null | undefined;
|
||||
|
||||
choices?: string[];
|
||||
|
||||
onChanged: (value: string | boolean) => void;
|
||||
}
|
||||
|
||||
export const DraftField: React.FunctionComponent<IDraftFieldProps> = ({ label, type, value, choices, onChanged }: React.PropsWithChildren<IDraftFieldProps>) => {
|
||||
|
||||
if (type === "boolean") {
|
||||
return (
|
||||
<Toggle
|
||||
label={label}
|
||||
checked={!!value}
|
||||
onChanged={(checked) => onChanged(checked)} />
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "choice") {
|
||||
return (
|
||||
<ChoiceField
|
||||
label={label}
|
||||
selected={value as string}
|
||||
choices={choices as string[]}
|
||||
multiSelect={false}
|
||||
onChange={value => onChanged(value as string)} />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import { ChoiceField } from './Fields/ChoiceField';
|
||||
import useContentType from '../../hooks/useContentType';
|
||||
import { DateHelper } from '../../helpers/DateHelper';
|
||||
import FieldBoundary from './ErrorBoundary/FieldBoundary';
|
||||
import { DraftField } from './Fields/DraftField';
|
||||
|
||||
export interface IMetadataProps {
|
||||
settings: PanelSettings | undefined;
|
||||
@@ -172,6 +173,20 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
|
||||
unsetFocus={unsetFocus} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else if (field.type === 'draft') {
|
||||
const draftField = settings?.draftField;
|
||||
const value = metadata[field.name];
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<DraftField
|
||||
label={field.title || field.name}
|
||||
type={draftField.type}
|
||||
choices={draftField.choices || []}
|
||||
value={value as boolean | string | null | undefined}
|
||||
onChanged={(value: boolean | string) => sendUpdate(field.name, value)} />
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const SeoFieldInfo: React.FunctionComponent<ISeoFieldInfoProps> = ({ title, valu
|
||||
<VsTableCell className={`table__cell table__title`}>{title}</VsTableCell>
|
||||
<VsTableCell className={`table__cell`}>{value}/{recommendation}</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
{ isValid !== undefined ? <ValidInfo isValid={isValid} /> : <span>-</span> }
|
||||
{ isValid !== undefined ? <ValidInfo label={undefined} isValid={isValid} /> : <span>-</span> }
|
||||
</VsTableCell>
|
||||
</VsTableRow>
|
||||
);
|
||||
|
||||
@@ -8,9 +8,39 @@ export interface ISeoKeywordInfoProps {
|
||||
description: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
wordCount?: number;
|
||||
headings?: string[];
|
||||
}
|
||||
|
||||
const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({keyword, title, description, slug, content}: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
|
||||
const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({keyword, title, description, slug, content, wordCount, headings}: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
|
||||
|
||||
const density = () => {
|
||||
if (!wordCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pattern = new RegExp('\\b' + keyword.toLowerCase() + '\\b', 'ig');
|
||||
const count = (content.match(pattern) || []).length;
|
||||
const density = (count / wordCount) * 100;
|
||||
const densityTitle = `Keyword usage ${density.toFixed(2)}% *`;
|
||||
|
||||
if (density < 0.75) {
|
||||
return <ValidInfo label={densityTitle} isValid={false} />
|
||||
} else if (density >= 0.75 && density < 1.5) {
|
||||
return <ValidInfo label={densityTitle} isValid={true} />
|
||||
} else {
|
||||
return <ValidInfo label={densityTitle} isValid={false} />
|
||||
}
|
||||
};
|
||||
|
||||
const checkHeadings = () => {
|
||||
if (!headings || headings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exists = headings.filter(heading => heading.split(' ').findIndex(word => word.toLowerCase() === keyword.toLowerCase()) !== -1);
|
||||
return <ValidInfo label={`Used in heading(s)`} isValid={exists.length > 0} />;
|
||||
};
|
||||
|
||||
if (!keyword) {
|
||||
return null;
|
||||
@@ -19,17 +49,33 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({keyword,
|
||||
return (
|
||||
<VsTableRow>
|
||||
<VsTableCell className={`table__cell`}>{keyword}</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
<ValidInfo isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
<ValidInfo isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
<ValidInfo isValid={!!slug && (slug.toLowerCase().includes(keyword.toLowerCase()) || slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase()))} />
|
||||
</VsTableCell>
|
||||
<VsTableCell className={`table__cell table__cell__validation`}>
|
||||
<ValidInfo isValid={!!content && content.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
<VsTableCell className={`table__cell table__cell__validation table__cell__seo_details`}>
|
||||
<div>
|
||||
<ValidInfo label={`Title`} isValid={!!title && title.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</div>
|
||||
<div>
|
||||
<ValidInfo label={`Description`} isValid={!!description && description.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</div>
|
||||
<div>
|
||||
<ValidInfo label={`Slug`} isValid={!!slug && (slug.toLowerCase().includes(keyword.toLowerCase()) || slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase()))} />
|
||||
</div>
|
||||
<div>
|
||||
<ValidInfo label={`Content`} isValid={!!content && content.toLowerCase().includes(keyword.toLowerCase())} />
|
||||
</div>
|
||||
{
|
||||
headings && headings.length > 0 && (
|
||||
<div>
|
||||
{checkHeadings()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
wordCount && (
|
||||
<div>
|
||||
{density()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</VsTableCell>
|
||||
</VsTableRow>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { SeoKeywordInfo } from './SeoKeywordInfo';
|
||||
import { VsTable, VsTableBody, VsTableCell, VsTableHeader, VsTableHeaderCell, VsTableRow } from './VscodeComponents';
|
||||
import { VsTable, VsTableBody, VsTableHeader, VsTableHeaderCell } from './VscodeComponents';
|
||||
|
||||
export interface ISeoKeywordsProps {
|
||||
keywords: string[] | null;
|
||||
@@ -9,6 +9,8 @@ export interface ISeoKeywordsProps {
|
||||
description: string;
|
||||
slug: string;
|
||||
content: string;
|
||||
headings?: string[];
|
||||
wordCount?: number;
|
||||
}
|
||||
|
||||
const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...data}: React.PropsWithChildren<ISeoKeywordsProps>) => {
|
||||
@@ -37,13 +39,10 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...d
|
||||
<div className={`seo__status__keywords`}>
|
||||
<h4>Keywords</h4>
|
||||
|
||||
<VsTable bordered>
|
||||
<VsTable bordered columns={["30%", "auto"]}>
|
||||
<VsTableHeader slot="header">
|
||||
<VsTableHeaderCell className={`table__cell`}>Keyword</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Title</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Description</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Slug</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Content</VsTableHeaderCell>
|
||||
<VsTableHeaderCell className={`table__cell`}>Details</VsTableHeaderCell>
|
||||
</VsTableHeader>
|
||||
<VsTableBody slot="body">
|
||||
{
|
||||
@@ -55,6 +54,14 @@ const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({keywords, ...d
|
||||
}
|
||||
</VsTableBody>
|
||||
</VsTable>
|
||||
|
||||
{
|
||||
data.wordCount && (
|
||||
<div className={`seo__status__note`}>
|
||||
* A keyword density of 1-1.5% is sufficient in most cases.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface ISeoStatusProps {
|
||||
|
||||
const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsWithChildren<ISeoStatusProps>) => {
|
||||
const { data, seo } = props;
|
||||
const { title } = data;
|
||||
const { title, slug } = data;
|
||||
const [ isOpen, setIsOpen ] = React.useState(true);
|
||||
const tableRef = React.useRef<HTMLElement>();
|
||||
const pushUpdate = React.useRef((value: boolean) => {
|
||||
@@ -65,6 +65,11 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsW
|
||||
<SeoFieldInfo title={`title`} value={title.length} recommendation={`${seo.title} chars`} isValid={title.length <= seo.title} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(slug && seo.slug > 0) && (
|
||||
<SeoFieldInfo title={`slug`} value={slug.length} recommendation={`${seo.slug} chars`} isValid={slug.length <= seo.slug} />
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(data[descriptionField] && seo.description > 0) && (
|
||||
@@ -85,6 +90,8 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsW
|
||||
title={title}
|
||||
description={data[descriptionField]}
|
||||
slug={data.slug}
|
||||
headings={data?.articleDetails?.headingsText}
|
||||
wordCount={data?.articleDetails?.wordCount}
|
||||
content={data?.articleDetails?.content} />
|
||||
|
||||
<ArticleDetails details={data.articleDetails} />
|
||||
|
||||
@@ -18,7 +18,7 @@ const Tags: React.FunctionComponent<ITagsProps> = (props: React.PropsWithChildre
|
||||
const unknownTags = values.filter(v => !options.includes(v));
|
||||
|
||||
const generateKey = (tag: string, idx: number) => {
|
||||
if (tag) {
|
||||
if (tag && typeof tag === 'string') {
|
||||
return `${tag.replace(/ /g, "_")}-${idx}`;
|
||||
}
|
||||
return `tag-${idx}`;
|
||||
|
||||
@@ -3,10 +3,11 @@ import { CheckIcon } from './Icons/CheckIcon';
|
||||
import { WarningIcon } from './Icons/WarningIcon';
|
||||
|
||||
export interface IValidInfoProps {
|
||||
label?: string;
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({isValid}: React.PropsWithChildren<IValidInfoProps>) => {
|
||||
const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({label, isValid}: React.PropsWithChildren<IValidInfoProps>) => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
@@ -16,6 +17,7 @@ const ValidInfo: React.FunctionComponent<IValidInfoProps> = ({isValid}: React.Pr
|
||||
<span className="warning"><WarningIcon /></span>
|
||||
)
|
||||
}
|
||||
{ label && <span>{label}</span> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user