mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Changes for new panel view
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
.DS_Store
|
||||
dist
|
||||
dist
|
||||
todo.md
|
||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -16,7 +16,7 @@
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: webpack"
|
||||
"preLaunchTask": "npm: build:ext"
|
||||
},
|
||||
{
|
||||
"name": "Extension Tests",
|
||||
|
||||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -18,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "webpack",
|
||||
"script": "build:ext",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
||||
17
assets/frontmatter.svg
Normal file
17
assets/frontmatter.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 28 28" style="enable-background:new 0 0 28 28;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{font-family:'Futura-CondensedExtraBold';}
|
||||
.st1{font-size:11.3081px;}
|
||||
.st2{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st3{font-family:'Futura-CondensedMedium';}
|
||||
.st4{font-size:10px;}
|
||||
.st5{fill:none;}
|
||||
</style>
|
||||
<text transform="matrix(0.7959 0 0 1 1.6978 11.391)" class="st0 st1">FRONT</text>
|
||||
<rect class="st2" width="28" height="28"/>
|
||||
<text transform="matrix(1 0 0 1 1.9325 24.496)" class="st3 st4">MATTER</text>
|
||||
<rect x="-33.5" y="14" class="st5" width="8.6" height="14"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 880 B |
23
assets/media/main.js
Normal file
23
assets/media/main.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
(function () {
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.type) {
|
||||
case 'addColor':
|
||||
addColor();
|
||||
break;
|
||||
case 'clearColors':
|
||||
colors = [];
|
||||
updateColorList(colors);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
window.onload = function() {
|
||||
vscode.postMessage({ command: 'get-data' });
|
||||
console.log('Ready to accept data.');
|
||||
};
|
||||
}());
|
||||
30
assets/media/reset.css
Normal file
30
assets/media/reset.css
Normal file
@@ -0,0 +1,30 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
ol,
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
132
assets/media/styles.css
Normal file
132
assets/media/styles.css
Normal file
@@ -0,0 +1,132 @@
|
||||
/* Styling: https://code.visualstudio.com/api/references/theme-color */
|
||||
|
||||
@-webkit-keyframes load7 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
@keyframes load7 {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner,
|
||||
.spinner:before,
|
||||
.spinner:after {
|
||||
border-radius: 50%;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
animation-fill-mode: both;
|
||||
animation: load7 1.8s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
color: var(--vscode-panelSectionHeader-foreground);
|
||||
font-size: 10px;
|
||||
margin: 80px auto;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
transform: translateZ(0);
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.spinner:before,
|
||||
.spinner:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.spinner:before {
|
||||
left: -3.5em;
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.spinner:after {
|
||||
left: 3.5em;
|
||||
}
|
||||
|
||||
|
||||
.frontmatter h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.frontmatter p,
|
||||
.frontmatter h4,
|
||||
.frontmatter ul {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.seo__status__details {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.not-valid {
|
||||
color: var(--vscode-errorForeground);
|
||||
}
|
||||
|
||||
.article__actions {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.article__action {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.article__tags {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.article__tags ul {
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
}
|
||||
|
||||
.article__tags li {
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
}
|
||||
|
||||
.article__tags li:active {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.article__tags li[data-focus="true"] {
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.article__tags li[aria-disabled="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.article__tags__items {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.article__tags__items__btn {
|
||||
display: inline-block;
|
||||
margin-bottom: .5rem;
|
||||
margin-right: .5rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.article__tags__items__btn span {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.article__tags__items__pill_notexists {
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
}
|
||||
101
assets/media/vscode.css
Normal file
101
assets/media/vscode.css
Normal file
@@ -0,0 +1,101 @@
|
||||
:root {
|
||||
--container-paddding: 20px;
|
||||
--input-padding-vertical: 6px;
|
||||
--input-padding-horizontal: 4px;
|
||||
--input-margin-vertical: 4px;
|
||||
--input-margin-horizontal: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0 var(--container-paddding);
|
||||
color: var(--vscode-foreground);
|
||||
font-size: var(--vscode-font-size);
|
||||
font-weight: var(--vscode-font-weight);
|
||||
font-family: var(--vscode-font-family);
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: var(--container-paddding);
|
||||
}
|
||||
|
||||
body > *,
|
||||
form > * {
|
||||
margin-block-start: var(--input-margin-vertical);
|
||||
margin-block-end: var(--input-margin-vertical);
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline-color: var(--vscode-focusBorder) !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active {
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
outline: 1px solid transparent;
|
||||
outline-offset: 2px !important;
|
||||
color: var(--vscode-button-foreground);
|
||||
background: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: var(--vscode-button-background);
|
||||
filter: brightness(40%);
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
button.secondary:disabled {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
filter: brightness(40%);
|
||||
}
|
||||
|
||||
input:not([type='checkbox']),
|
||||
textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: none;
|
||||
font-family: var(--vscode-font-family);
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
color: var(--vscode-input-foreground);
|
||||
outline-color: var(--vscode-input-border);
|
||||
background-color: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
}
|
||||
957
package-lock.json
generated
957
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -47,10 +47,31 @@
|
||||
"onCommand:frontMatter.setDate",
|
||||
"onCommand:frontMatter.setLastModifiedDate",
|
||||
"onCommand:frontMatter.generateSlug",
|
||||
"onCommand:frontMatter.createFromTemplate"
|
||||
"onCommand:frontMatter.createFromTemplate",
|
||||
"onView:frontMatter.explorer"
|
||||
],
|
||||
"main": "./dist/extension",
|
||||
"contributes": {
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "frontmatter-explorer",
|
||||
"title": "FrontMatter",
|
||||
"icon": "assets/frontmatter.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"frontmatter-explorer": [
|
||||
{
|
||||
"id": "frontMatter.explorer",
|
||||
"name": "FrontMatter",
|
||||
"icon": "assets/frontmatter.svg",
|
||||
"contextualTitle": "FrontMatter",
|
||||
"type": "webview"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Front Matter: Configuration",
|
||||
"properties": {
|
||||
@@ -191,8 +212,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "webpack --mode production",
|
||||
"webpack": "webpack --mode development",
|
||||
"webpack-dev": "webpack --mode development --watch",
|
||||
"build:ext": "webpack --mode development",
|
||||
"dev:ext": "webpack --mode development --watch",
|
||||
"test-compile": "tsc -p ./"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -200,19 +221,27 @@
|
||||
"@types/js-yaml": "3.12.1",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/node": "^10.12.21",
|
||||
"@types/vscode": "^1.37.0",
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/vscode": "1.51.0",
|
||||
"date-fns": "2.0.1",
|
||||
"glob": "^7.1.4",
|
||||
"gray-matter": "4.0.2",
|
||||
"html-loader": "1.3.2",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"mocha": "^6.1.4",
|
||||
"ts-loader": "8.0.3",
|
||||
"tslint": "^5.12.1",
|
||||
"typescript": "4.0.2",
|
||||
"vscode-test": "^1.0.2",
|
||||
"webpack": "4.44.1",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-cli": "3.3.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "2.2.3"
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@material-ui/core": "4.11.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.56",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as vscode from 'vscode';
|
||||
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 { ArticleHelper, SettingsHelper, SlugHelper } from '../helpers';
|
||||
import matter = require('gray-matter');
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ export class Article {
|
||||
}
|
||||
|
||||
const articleTitle: string = article.data["title"];
|
||||
const slug = ArticleHelper.createSlug(articleTitle);
|
||||
const slug = SlugHelper.createSlug(articleTitle);
|
||||
if (slug) {
|
||||
article.data["slug"] = `${prefix}${slug}${suffix}`;
|
||||
ArticleHelper.update(editor, article);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../c
|
||||
import * as vscode from 'vscode';
|
||||
import { CONFIG_KEY } from '../constants';
|
||||
import { ArticleHelper, SeoHelper } from '../helpers';
|
||||
import { ExplorerView } from '../webview/ExplorerView';
|
||||
|
||||
export class StatusListener {
|
||||
|
||||
@@ -38,8 +39,8 @@ export class StatusListener {
|
||||
|
||||
// Retrieve the SEO config properties
|
||||
const config = vscode.workspace.getConfiguration(CONFIG_KEY);
|
||||
let titleLength = config.get(SETTING_SEO_TITLE_LENGTH) as number || -1;
|
||||
let descLength = config.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1;
|
||||
const titleLength = config.get(SETTING_SEO_TITLE_LENGTH) as number || -1;
|
||||
const descLength = config.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1;
|
||||
|
||||
if (article.data.title && titleLength > -1) {
|
||||
SeoHelper.checkLength(editor, collection, article, "title", titleLength);
|
||||
@@ -49,6 +50,12 @@ export class StatusListener {
|
||||
SeoHelper.checkLength(editor, collection, article, "description", descLength);
|
||||
}
|
||||
}
|
||||
|
||||
const panel = ExplorerView.getInstance();
|
||||
if (panel && panel.visible) {
|
||||
panel.pushMetadata(article!.data);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (e) {
|
||||
// Nothing to do
|
||||
|
||||
@@ -2,14 +2,22 @@ import * as vscode from 'vscode';
|
||||
import { Article, Settings, StatusListener } from './commands';
|
||||
import { Template } from './commands/Template';
|
||||
import { TaxonomyType } from './models';
|
||||
import { ExplorerView } from './webview/ExplorerView';
|
||||
|
||||
let frontMatterStatusBar: vscode.StatusBarItem;
|
||||
let debouncer: { (fnc: any, time: number): void; };
|
||||
let collection: vscode.DiagnosticCollection;
|
||||
|
||||
export function activate({ subscriptions }: vscode.ExtensionContext) {
|
||||
export function activate({ subscriptions, extensionUri }: vscode.ExtensionContext) {
|
||||
collection = vscode.languages.createDiagnosticCollection('frontMatter');
|
||||
|
||||
const explorerSidebar = ExplorerView.getInstance(extensionUri);
|
||||
let explorerView = vscode.window.registerWebviewViewProvider(ExplorerView.viewType, explorerSidebar, {
|
||||
webviewOptions: {
|
||||
retainContextWhenHidden: true
|
||||
}
|
||||
});
|
||||
|
||||
let insertTags = vscode.commands.registerCommand('frontMatter.insertTags', () => {
|
||||
Article.insert(TaxonomyType.Tag);
|
||||
});
|
||||
@@ -75,6 +83,7 @@ export function activate({ subscriptions }: vscode.ExtensionContext) {
|
||||
|
||||
// Subscribe all commands
|
||||
subscriptions.push(insertTags);
|
||||
subscriptions.push(explorerView);
|
||||
subscriptions.push(insertCategories);
|
||||
subscriptions.push(createTag);
|
||||
subscriptions.push(createCategory);
|
||||
|
||||
@@ -1,8 +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';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
import { TomlEngine, getFmLanguage, getFormatOpts } from './TomlEngine';
|
||||
@@ -98,62 +96,4 @@ export class ArticleHelper {
|
||||
indent: spaces || 2
|
||||
} as DumpOptions as any));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the slug
|
||||
*
|
||||
* @param articleTitle
|
||||
*/
|
||||
public static createSlug(articleTitle: string): string | null {
|
||||
if (!articleTitle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove links, periods, commas, semi-colons, etc.
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
private static removePunctuation(value: string): string {
|
||||
const punctuationless = value.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
// Remove double spaces
|
||||
return punctuationless.replace(/\s{2,}/g," ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove stop words
|
||||
*
|
||||
* @param words
|
||||
*/
|
||||
private static removeStopWords(words: string[]) {
|
||||
const validWords: string[] = [];
|
||||
for (const word of words) {
|
||||
if (stopWords.indexOf(word.toLowerCase()) === -1) {
|
||||
validWords.push(word);
|
||||
}
|
||||
}
|
||||
return validWords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace characters from title
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
private static replaceCharacters(value: string) {
|
||||
const characters = [...value];
|
||||
return characters.map(c => charMap[c] || c).join("");
|
||||
}
|
||||
}
|
||||
63
src/helpers/SlugHelper.ts
Normal file
63
src/helpers/SlugHelper.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { stopWords } from '../constants/stopwords-en';
|
||||
import { charMap } from '../constants/charMap';
|
||||
|
||||
export class SlugHelper {
|
||||
|
||||
/**
|
||||
* Generate the slug
|
||||
*
|
||||
* @param articleTitle
|
||||
*/
|
||||
public static createSlug(articleTitle: string): string | null {
|
||||
if (!articleTitle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove links, periods, commas, semi-colons, etc.
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
private static removePunctuation(value: string): string {
|
||||
const punctuationless = value.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
// Remove double spaces
|
||||
return punctuationless.replace(/\s{2,}/g," ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove stop words
|
||||
*
|
||||
* @param words
|
||||
*/
|
||||
private static removeStopWords(words: string[]) {
|
||||
const validWords: string[] = [];
|
||||
for (const word of words) {
|
||||
if (stopWords.indexOf(word.toLowerCase()) === -1) {
|
||||
validWords.push(word);
|
||||
}
|
||||
}
|
||||
return validWords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace characters from title
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
private static replaceCharacters(value: string) {
|
||||
const characters = [...value];
|
||||
return characters.map(c => charMap[c] || c).join("");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
export * from './ArticleHelper';
|
||||
export * from './FilesHelper';
|
||||
export * from './Sanitize';
|
||||
export * from './SeoHelper';
|
||||
export * from './SettingsHelper';
|
||||
export * from './SlugHelper';
|
||||
export * from './StringHelpers';
|
||||
export * from './TomlEngine';
|
||||
|
||||
17
src/models/PanelSettings.ts
Normal file
17
src/models/PanelSettings.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
export interface PanelSettings {
|
||||
seo: SEO;
|
||||
slug: Slug;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
export interface SEO {
|
||||
title: number;
|
||||
description: number;
|
||||
}
|
||||
|
||||
export interface Slug {
|
||||
prefix: number;
|
||||
suffix: number;
|
||||
}
|
||||
5
src/viewpanel/Command.ts
Normal file
5
src/viewpanel/Command.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum Command {
|
||||
loading = "loading",
|
||||
metadata = "metadata",
|
||||
settings = "settings"
|
||||
}
|
||||
8
src/viewpanel/CommandToCode.ts
Normal file
8
src/viewpanel/CommandToCode.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export enum CommandToCode {
|
||||
getData = "get-data",
|
||||
updateSlug = 'update-slug',
|
||||
updateDate = 'update-date',
|
||||
publish = 'publish',
|
||||
updateTags = "update-tags",
|
||||
updateCategories = "update-categories"
|
||||
}
|
||||
4
src/viewpanel/TagType.ts
Normal file
4
src/viewpanel/TagType.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum TagType {
|
||||
tags = "Tags",
|
||||
categories = "Categories"
|
||||
}
|
||||
45
src/viewpanel/ViewPanel.tsx
Normal file
45
src/viewpanel/ViewPanel.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import { Actions } from './components/Actions';
|
||||
import { SeoStatus } from './components/SeoStatus';
|
||||
import { Spinner } from './components/Spinner';
|
||||
import { TagPicker } from './components/TagPicker';
|
||||
import useMessages from './hooks/useMessages';
|
||||
import { TagType } from './TagType';
|
||||
|
||||
export interface IViewPanelProps {
|
||||
}
|
||||
|
||||
export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (props: React.PropsWithChildren<IViewPanelProps>) => {
|
||||
const { loading, metadata, settings } = useMessages();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Spinner />
|
||||
);
|
||||
}
|
||||
|
||||
if (!metadata || Object.keys(metadata).length === 0) {
|
||||
return (
|
||||
<div className="frontmatter">
|
||||
<p>Current view/file is not supported by FrontMatter.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="frontmatter">
|
||||
{
|
||||
settings && settings.seo && <SeoStatus seo={settings.seo} data={metadata} />
|
||||
}
|
||||
{
|
||||
settings && metadata && <Actions metadata={metadata} settings={settings} />
|
||||
}
|
||||
{
|
||||
(settings && settings.tags && settings.tags.length > 0) && <TagPicker type={TagType.tags} crntSelected={metadata.tags || []} options={settings.tags} />
|
||||
}
|
||||
{
|
||||
(settings && settings.categories && settings.categories.length > 0) && <TagPicker type={TagType.categories} crntSelected={metadata.categories || []} options={settings.categories} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
30
src/viewpanel/components/Actions.tsx
Normal file
30
src/viewpanel/components/Actions.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { PanelSettings } from '../../models/PanelSettings';
|
||||
import { DateAction } from './DateAction';
|
||||
import { PublishAction } from './PublishAction';
|
||||
import { SlugAction } from './SlugAction';
|
||||
|
||||
export interface IActionsProps {
|
||||
metadata: any;
|
||||
settings: PanelSettings;
|
||||
}
|
||||
|
||||
export const Actions: React.FunctionComponent<IActionsProps> = (props: React.PropsWithChildren<IActionsProps>) => {
|
||||
const { metadata, settings } = props;
|
||||
|
||||
if (!metadata || Object.keys(metadata).length === 0 || !settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`article__actions`}>
|
||||
<h3>Actions</h3>
|
||||
|
||||
{ metadata && metadata.title && <SlugAction value={metadata.title} crntValue={metadata.slug} slugOpts={settings.slug} /> }
|
||||
|
||||
<DateAction />
|
||||
|
||||
{ metadata && typeof metadata.draft !== undefined && <PublishAction draft={metadata.draft} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
src/viewpanel/components/DateAction.tsx
Normal file
21
src/viewpanel/components/DateAction.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
|
||||
import * as React from 'react';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import useMessages from '../hooks/useMessages';
|
||||
|
||||
export interface IDateActionProps {}
|
||||
|
||||
export const DateAction: React.FunctionComponent<IDateActionProps> = (props: React.PropsWithChildren<IDateActionProps>) => {
|
||||
const { sendMessage } = useMessages();
|
||||
|
||||
const setDate = () => {
|
||||
sendMessage(CommandToCode.updateDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`article__action`}>
|
||||
<button onClick={setDate}>Set current date</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
24
src/viewpanel/components/PublishAction.tsx
Normal file
24
src/viewpanel/components/PublishAction.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
import * as React from 'react';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import useMessages from '../hooks/useMessages';
|
||||
|
||||
export interface IPublishActionProps {
|
||||
draft: boolean;
|
||||
}
|
||||
|
||||
export const PublishAction: React.FunctionComponent<IPublishActionProps> = (props: React.PropsWithChildren<IPublishActionProps>) => {
|
||||
const { sendMessage } = useMessages();
|
||||
const { draft } = props;
|
||||
|
||||
const publish = () => {
|
||||
sendMessage(CommandToCode.publish);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`article__action`}>
|
||||
<button onClick={publish} className={`${draft ? "" : "secondary"}`}>{draft ? "Publish" : "Revert to draft"}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
src/viewpanel/components/SeoDetails.tsx
Normal file
21
src/viewpanel/components/SeoDetails.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ISeoDetailsProps {
|
||||
allowedLength: number;
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const SeoDetails: React.FunctionComponent<ISeoDetailsProps> = (props: React.PropsWithChildren<ISeoDetailsProps>) => {
|
||||
const { allowedLength, title, value } = props;
|
||||
|
||||
return (
|
||||
<div className={`seo__status__details ${value.length <= allowedLength ? "valid" : "not-valid"}`}>
|
||||
<h4><strong>{title}</strong></h4>
|
||||
<ul>
|
||||
<li><b>Length</b>: {value.length}</li>
|
||||
<li><b>Allowed length</b>: {allowedLength}</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
25
src/viewpanel/components/SeoStatus.tsx
Normal file
25
src/viewpanel/components/SeoStatus.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { SEO } from '../../models/PanelSettings';
|
||||
import { SeoDetails } from './SeoDetails';
|
||||
|
||||
export interface ISeoStatusProps {
|
||||
seo: SEO;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export const SeoStatus: React.FunctionComponent<ISeoStatusProps> = (props: React.PropsWithChildren<ISeoStatusProps>) => {
|
||||
const { data, seo } = props;
|
||||
const { title, description } = data;
|
||||
|
||||
if (!title && !description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="seo__status">
|
||||
<h3>SEO Status</h3>
|
||||
{ (title && seo.title > 0) && <SeoDetails title="Title" allowedLength={seo.title} value={title} /> }
|
||||
{ (description && seo.description > 0) && <SeoDetails title="Description" allowedLength={seo.description} value={description} /> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
29
src/viewpanel/components/SlugAction.tsx
Normal file
29
src/viewpanel/components/SlugAction.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import { SlugHelper } from '../../helpers/SlugHelper';
|
||||
import { Slug } from '../../models/PanelSettings';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import useMessages from '../hooks/useMessages';
|
||||
|
||||
export interface ISlugActionProps {
|
||||
value: string;
|
||||
crntValue: string;
|
||||
slugOpts: Slug;
|
||||
}
|
||||
|
||||
export const SlugAction: React.FunctionComponent<ISlugActionProps> = (props: React.PropsWithChildren<ISlugActionProps>) => {
|
||||
const { value, crntValue, slugOpts } = props;
|
||||
const { sendMessage } = useMessages();
|
||||
|
||||
let slug = SlugHelper.createSlug(value);
|
||||
slug = `${slugOpts.prefix}${slug}${slugOpts.suffix}`;
|
||||
|
||||
const optimize = () => {
|
||||
sendMessage(CommandToCode.updateSlug);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`article__action`}>
|
||||
<button onClick={optimize} disabled={crntValue === slug}>Optimize slug</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
9
src/viewpanel/components/Spinner.tsx
Normal file
9
src/viewpanel/components/Spinner.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ISpinnerProps {}
|
||||
|
||||
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
|
||||
return (
|
||||
<div className="spinner">Loading...</div>
|
||||
);
|
||||
};
|
||||
17
src/viewpanel/components/Tag.tsx
Normal file
17
src/viewpanel/components/Tag.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ITagProps {
|
||||
className: string;
|
||||
value: string;
|
||||
title: string;
|
||||
|
||||
onRemove: (tags: string) => void;
|
||||
}
|
||||
|
||||
export const Tag: React.FunctionComponent<ITagProps> = (props: React.PropsWithChildren<ITagProps>) => {
|
||||
const { value, className, title, onRemove } = props;
|
||||
|
||||
return (
|
||||
<button title={title} className={`article__tags__items__btn ${className}`} onClick={() => onRemove(value)}>{value} <span>x</span></button>
|
||||
);
|
||||
};
|
||||
97
src/viewpanel/components/TagPicker.tsx
Normal file
97
src/viewpanel/components/TagPicker.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import * as React from 'react';
|
||||
import useAutocomplete from '@material-ui/lab/useAutocomplete';
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles';
|
||||
import { Tags } from './Tags';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import useMessages from '../hooks/useMessages';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
import { TagType } from '../TagType';
|
||||
|
||||
export interface ITagPickerProps {
|
||||
type: string;
|
||||
crntSelected: string[];
|
||||
options: string[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
label: {
|
||||
display: 'block',
|
||||
},
|
||||
input: {
|
||||
width: 200,
|
||||
},
|
||||
listbox: {
|
||||
width: 200,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
zIndex: 1,
|
||||
position: 'absolute',
|
||||
listStyle: 'none',
|
||||
overflow: 'auto',
|
||||
maxHeight: 200,
|
||||
border: '1px solid rgba(0,0,0,.8)'
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const TagPicker: React.FunctionComponent<ITagPickerProps> = (props: React.PropsWithChildren<ITagPickerProps>) => {
|
||||
const { type, crntSelected, options } = props;
|
||||
const [ selected, setSelected ] = React.useState<string[]>([]);
|
||||
const prevSelected = usePrevious(crntSelected);
|
||||
const { sendMessage } = useMessages();
|
||||
|
||||
const classes = useStyles();
|
||||
const { getRootProps, getInputProps, getListboxProps, getOptionProps, groupedOptions } = useAutocomplete({
|
||||
id: 'use-autocomplete',
|
||||
options: options,
|
||||
multiple: true,
|
||||
autoComplete: true,
|
||||
value: crntSelected,
|
||||
getOptionDisabled: (option) => selected.includes(option),
|
||||
onChange: (e, values: string[]) => {
|
||||
const uniqValues = Array.from(new Set([...selected, ...values]));
|
||||
setSelected(uniqValues);
|
||||
sendUpdate(uniqValues);
|
||||
}
|
||||
});
|
||||
|
||||
const onRemove = (tag: string) => {
|
||||
const newSelection = selected.filter(s => s !== tag);
|
||||
setSelected(newSelection);
|
||||
sendUpdate(newSelection);
|
||||
};
|
||||
|
||||
const sendUpdate = (values: string[]) => {
|
||||
const cmdType = type === TagType.tags ? CommandToCode.updateTags : CommandToCode.updateCategories;
|
||||
sendMessage(cmdType, values);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (prevSelected !== crntSelected) {
|
||||
setSelected(crntSelected);
|
||||
}
|
||||
}, [crntSelected]);
|
||||
|
||||
return (
|
||||
<div className={`article__tags`}>
|
||||
<h3>{type}</h3>
|
||||
|
||||
<div {...getRootProps()}>
|
||||
<input className={classes.input} {...getInputProps()} placeholder={`Pick your ${type.toLowerCase()}`}/>
|
||||
</div>
|
||||
|
||||
{
|
||||
groupedOptions.length > 0 ? (
|
||||
<ul className={classes.listbox} {...getListboxProps()}>
|
||||
{groupedOptions.map((option, index) => (
|
||||
<li key={index} {...getOptionProps({ option, index })}>{option}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null
|
||||
}
|
||||
|
||||
<Tags values={selected} onRemove={onRemove} options={options} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
23
src/viewpanel/components/Tags.tsx
Normal file
23
src/viewpanel/components/Tags.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import { Tag } from './Tag';
|
||||
|
||||
export interface ITagsProps {
|
||||
values: string[];
|
||||
options: string[];
|
||||
|
||||
onRemove: (tags: string) => void;
|
||||
}
|
||||
|
||||
export const Tags: React.FunctionComponent<ITagsProps> = (props: React.PropsWithChildren<ITagsProps>) => {
|
||||
const { values, options, onRemove } = props;
|
||||
|
||||
return (
|
||||
<div className={`article__tags__items`}>
|
||||
{
|
||||
values.map(t => (
|
||||
<Tag key={t.replace(/ /g, "_")} value={t} className={`${options.includes(t) ? 'article__tags__items__pill_exists' : 'article__tags__items__pill_notexists'}`} onRemove={onRemove} title={`${options.includes(t) ? `Remove ${t}` : `Be aware, this tag "${t}" is not saved in your settings.`}`} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
54
src/viewpanel/hooks/useMessages.tsx
Normal file
54
src/viewpanel/hooks/useMessages.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PanelSettings } from '../../models/PanelSettings';
|
||||
import { Command } from '../Command';
|
||||
import { CommandToCode } from '../CommandToCode';
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
getState: () => T;
|
||||
setState: (data: T) => void;
|
||||
postMessage: (msg: unknown) => void;
|
||||
};
|
||||
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
export default function useMessages() {
|
||||
const [metadata, setMetadata] = useState<any>({});
|
||||
const [settings, setSettings] = useState<PanelSettings>();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
|
||||
switch (message.command) {
|
||||
case Command.metadata:
|
||||
setMetadata(message.data);
|
||||
setLoading(false);
|
||||
break;
|
||||
case Command.settings:
|
||||
setSettings(message.data);
|
||||
setLoading(false);
|
||||
break;
|
||||
case Command.loading:
|
||||
setLoading(message.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
vscode.postMessage({ command: CommandToCode.getData });
|
||||
}, ['']);
|
||||
|
||||
return {
|
||||
metadata,
|
||||
settings,
|
||||
loading,
|
||||
sendMessage: (command: CommandToCode, data?: any) => {
|
||||
if (data) {
|
||||
vscode.postMessage({ command, data });
|
||||
} else {
|
||||
vscode.postMessage({ command });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
11
src/viewpanel/hooks/usePrevious.tsx
Normal file
11
src/viewpanel/hooks/usePrevious.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
6
src/viewpanel/index.tsx
Normal file
6
src/viewpanel/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { ViewPanel } from "./ViewPanel";
|
||||
|
||||
const elm = document.querySelector("#app");
|
||||
render(<ViewPanel />, elm);
|
||||
226
src/webview/ExplorerView.ts
Normal file
226
src/webview/ExplorerView.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { PanelSettings } from './../models/PanelSettings';
|
||||
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace } from "vscode";
|
||||
import { CONFIG_KEY, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from "../constants";
|
||||
import { ArticleHelper } from "../helpers";
|
||||
import { Command } from "../viewpanel/Command";
|
||||
import { CommandToCode } from '../viewpanel/CommandToCode';
|
||||
import { Article } from '../commands';
|
||||
import { TagType } from '../viewpanel/TagType';
|
||||
|
||||
|
||||
|
||||
export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
public static readonly viewType = "frontMatter.explorer";
|
||||
private static instance: ExplorerView;
|
||||
|
||||
private panel: WebviewView | null = null;
|
||||
private disposable: Disposable | null = null;
|
||||
|
||||
private constructor(private readonly extPath: Uri) {}
|
||||
|
||||
/**
|
||||
* Creates the singleton instance for the panel
|
||||
* @param extPath
|
||||
*/
|
||||
public static getInstance(extPath?: Uri): ExplorerView {
|
||||
if (!ExplorerView.instance) {
|
||||
ExplorerView.instance = new ExplorerView(extPath as Uri);
|
||||
}
|
||||
|
||||
return ExplorerView.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the visibility of the webview
|
||||
*/
|
||||
get visible() {
|
||||
return this.panel ? this.panel.visible : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Webview panel dispose
|
||||
*/
|
||||
public dispose() {
|
||||
if (this.disposable) {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default resolve webview panel
|
||||
* @param webviewView
|
||||
* @param context
|
||||
* @param token
|
||||
*/
|
||||
public async resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken): Promise<void> {
|
||||
|
||||
this.panel = webviewView;
|
||||
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
localResourceRoots: [this.extPath]
|
||||
};
|
||||
|
||||
webviewView.webview.html = this.getWebviewContent(webviewView.webview);
|
||||
|
||||
this.disposable = Disposable.from(
|
||||
webviewView.onDidDispose(() => { webviewView.webview.html = ""; }, this),
|
||||
// window.onDidChangeWindowState(() => { console.log(`onDidChangeWindowState visible`, this.visible); }, this)
|
||||
);
|
||||
|
||||
webviewView.webview.onDidReceiveMessage(msg => {
|
||||
switch(msg.command) {
|
||||
case CommandToCode.getData:
|
||||
this.getSettings();
|
||||
this.getFileData();
|
||||
break;
|
||||
case CommandToCode.updateSlug:
|
||||
Article.generateSlug();
|
||||
break;
|
||||
case CommandToCode.updateDate:
|
||||
Article.setDate();
|
||||
break;
|
||||
case CommandToCode.publish:
|
||||
Article.toggleDraft();
|
||||
break;
|
||||
case CommandToCode.updateTags:
|
||||
this.updateTags(TagType.tags, msg.data || []);
|
||||
break;
|
||||
case CommandToCode.updateCategories:
|
||||
this.updateTags(TagType.categories, msg.data || []);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
webviewView.onDidChangeVisibility(() => {
|
||||
if (this.visible) {
|
||||
this.getFileData();
|
||||
}
|
||||
});
|
||||
|
||||
window.onDidChangeActiveTextEditor(() => {
|
||||
this.postWebviewMessage({ command: Command.loading, data: true });
|
||||
if (this.visible) {
|
||||
this.getFileData();
|
||||
}
|
||||
}, this);
|
||||
|
||||
workspace.onDidChangeConfiguration(() => {
|
||||
this.getSettings();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a metadata change in the panel
|
||||
* @param metadata
|
||||
*/
|
||||
public pushMetadata(metadata: any) {
|
||||
this.postWebviewMessage({ command: Command.metadata, data: metadata });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the extension settings
|
||||
*/
|
||||
private getSettings() {
|
||||
const config = workspace.getConfiguration(CONFIG_KEY);
|
||||
|
||||
this.postWebviewMessage({
|
||||
command: Command.settings,
|
||||
data: {
|
||||
seo: {
|
||||
title: config.get(SETTING_SEO_TITLE_LENGTH) as number || -1,
|
||||
description: config.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1
|
||||
},
|
||||
slug: {
|
||||
prefix: config.get(SETTING_SLUG_PREFIX) || "",
|
||||
suffix: config.get(SETTING_SLUG_SUFFIX) || ""
|
||||
},
|
||||
tags: config.get(SETTING_TAXONOMY_TAGS) || [],
|
||||
categories: config.get(SETTING_TAXONOMY_CATEGORIES) || []
|
||||
} as PanelSettings
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file its front matter
|
||||
*/
|
||||
private getFileData() {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
this.postWebviewMessage({ command: Command.metadata, data: article!.data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tags in the current document
|
||||
* @param tagType
|
||||
* @param values
|
||||
*/
|
||||
private updateTags(tagType: TagType, values: string[]) {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (article && article.data) {
|
||||
article.data[tagType.toLowerCase()] = values || [];
|
||||
ArticleHelper.update(editor, article);
|
||||
this.postWebviewMessage({ command: Command.metadata, data: article.data });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post data to the panel
|
||||
* @param msg
|
||||
*/
|
||||
private postWebviewMessage(msg: { command: Command, data: any }) {
|
||||
this.panel!.webview.postMessage(msg);
|
||||
}
|
||||
|
||||
|
||||
private getNonce() {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 32; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the webview HTML contents
|
||||
* @param webView
|
||||
*/
|
||||
private getWebviewContent(webView: Webview): string {
|
||||
const styleVSCodeUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'vscode.css'));
|
||||
const styleResetUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'reset.css'));
|
||||
const stylesUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'assets/media', 'styles.css'));
|
||||
const scriptUri = webView.asWebviewUri(Uri.joinPath(this.extPath, 'dist', 'bundle.js'));
|
||||
const nonce = this.getNonce();
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webView.cspSource} 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="${styleResetUri}" rel="stylesheet">
|
||||
<link href="${styleVSCodeUri}" rel="stylesheet">
|
||||
<link href="${stylesUri}" rel="stylesheet">
|
||||
|
||||
<title>FrontMatter</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,13 @@
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": [
|
||||
"es6"
|
||||
"es6",
|
||||
"DOM"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"strict": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
@@ -4,38 +4,54 @@
|
||||
|
||||
const path = require('path');
|
||||
|
||||
/**@type {import('webpack').Configuration}*/
|
||||
const config = {
|
||||
target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
|
||||
|
||||
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
|
||||
output: {
|
||||
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
devtoolModuleFilenameTemplate: '../[resource-path]'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
externals: {
|
||||
vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
|
||||
},
|
||||
resolve: {
|
||||
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
module.exports = [
|
||||
{
|
||||
name: 'extension',
|
||||
target: 'node',
|
||||
entry: './src/extension.ts',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
devtoolModuleFilenameTemplate: '../[resource-path]'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
externals: {
|
||||
vscode: 'commonjs vscode'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx']
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
use: [{
|
||||
loader: 'ts-loader'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'viewpanel',
|
||||
target: 'web',
|
||||
entry: './src/viewpanel/index.tsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx']
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: [{
|
||||
loader: 'ts-loader'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
module.exports = config;
|
||||
];
|
||||
Reference in New Issue
Block a user