diff --git a/CHANGELOG.md b/CHANGELOG.md index dda9344d..63741968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,17 @@ # Change Log -## [6.2.0] - 2022-03-xx +## [7.0.0] - 2022-03-xx + +### ✨ Features + +- [#175](https://github.com/estruyf/vscode-front-matter/issues/175): New snippet support + dashboard ### 🎨 Enhancements - Light color theme enhancements to media cards - Light color theme enhancements to folder cards - [#272](https://github.com/estruyf/vscode-front-matter/issues/272): New slide over panel for showing details of media files +- [#276](https://github.com/estruyf/vscode-front-matter/issues/276): Add a Front Matter walkthrough for VS Code ## [6.1.1] - 2022-03-02 diff --git a/assets/empty.svg b/assets/empty.svg new file mode 100644 index 00000000..8872d161 --- /dev/null +++ b/assets/empty.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/assets/icons/scissors-dark.svg b/assets/icons/scissors-dark.svg new file mode 100644 index 00000000..514110d3 --- /dev/null +++ b/assets/icons/scissors-dark.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/icons/scissors-light.svg b/assets/icons/scissors-light.svg new file mode 100644 index 00000000..780b674e --- /dev/null +++ b/assets/icons/scissors-light.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/walkthrough/documentation.md b/assets/walkthrough/documentation.md new file mode 100644 index 00000000..d5751e00 --- /dev/null +++ b/assets/walkthrough/documentation.md @@ -0,0 +1,3 @@ +## Documentation + +Our documentation can be found at: [https://frontmatter.codes/docs](https://frontmatter.codes/docs) \ No newline at end of file diff --git a/assets/walkthrough/get-started.md b/assets/walkthrough/get-started.md new file mode 100644 index 00000000..9cfcfb31 --- /dev/null +++ b/assets/walkthrough/get-started.md @@ -0,0 +1,11 @@ +## Getting started + +Thanks for installing Front Matter! + +To get started, open our dashboard which will guide you through the initialization process of your project. + +When you haven't initialized your project yet, you will see the Front Matter's welcome screen on which you will have to perform the following steps: + +- Project initialization +- Content folders registration +- Framework initialization diff --git a/assets/walkthrough/support-the-project.md b/assets/walkthrough/support-the-project.md new file mode 100644 index 00000000..5b9362cd --- /dev/null +++ b/assets/walkthrough/support-the-project.md @@ -0,0 +1,8 @@ +## Support the project + +Front Matter is an open source project and we are always looking for new contributors, supporters, and partners. If you are interested in backing the project, please consider supporting it by donating. You can donate at via the following links: + +- [GitHub Sponsors](https://github.com/sponsors/estruyf) +- [Open Collective](https://opencollective.com/frontmatter) + +> Each sponsor/backer will be mentioned on the [Front Matter](https://frontmatter.codes) website and on the [GitHub repository](https://github.com/estruyf/vscode-front-matter). \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 91ef98a8..8c0fa19b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-front-matter-beta", - "version": "6.2.0", + "version": "7.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-front-matter-beta", - "version": "6.2.0", + "version": "7.0.0", "license": "MIT", "dependencies": { "node-fetch": "^2.6.7" diff --git a/package.json b/package.json index 66d151bc..481bcd07 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Front Matter", "description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...", "icon": "assets/frontmatter-teal-128x128.png", - "version": "6.2.0", + "version": "7.0.0", "preview": false, "publisher": "eliostruyf", "galleryBanner": { @@ -47,11 +47,6 @@ "activationEvents": [ "workspaceContains:**/.frontmatter", "workspaceContains:**/frontmatter.json", - "onCommand:frontMatter.init", - "onCommand:frontMatter.dashboard", - "onCommand:frontMatter.dashboard.data", - "onCommand:frontMatter.dashboard.media", - "onCommand:workbench.view.extension.frontmatter-explorer", "onView:frontMatter.explorer", "onStartupFinished" ], @@ -224,6 +219,33 @@ "markdownDescription": "Specify the folder name where all your assets are located. For instance in Hugo this is the `static` folder. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.publicfolder)", "scope": "Content" }, + "frontMatter.content.snippets": { + "type": "object", + "markdownDescription": "Define the snippets you want to use in your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.snippets)", + "additionalProperties": { + "type": "object", + "required": [ + "body" + ], + "properties": { + "body": { + "markdownDescription": "The snippet content.", + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + } + }, + "description": { + "description": "The snippet description.", + "type": "string" + } + }, + "additionalProperties": false + } + }, "frontMatter.content.sorting": { "type": "array", "default": [], @@ -1094,7 +1116,7 @@ }, { "command": "frontMatter.markup.blockquote", - "title": "Codeblock", + "title": "Blockquote", "category": "Front matter", "icon": { "light": "assets/icons/blockquote-light.svg", @@ -1185,6 +1207,15 @@ "light": "/assets/icons/media-light.svg" } }, + { + "command": "frontMatter.insertSnippet", + "title": "Insert snippet into your content", + "category": "Front matter", + "icon": { + "dark": "/assets/icons/scissors-dark.svg", + "light": "/assets/icons/scissors-light.svg" + } + }, { "command": "frontMatter.insertTags", "title": "Insert tags", @@ -1217,6 +1248,15 @@ "light": "/assets/icons/frontmatter-small-light.svg" } }, + { + "command": "frontMatter.dashboard.snippets", + "title": "Open snippets dashboard", + "category": "Front matter", + "icon": { + "dark": "/assets/icons/frontmatter-small-dark.svg", + "light": "/assets/icons/frontmatter-small-light.svg" + } + }, { "command": "frontMatter.dashboard.media", "title": "Open media dashboard", @@ -1292,32 +1332,32 @@ "editor/title": [ { "command": "frontMatter.markup.heading", - "group": "navigation@-132", + "group": "navigation@-133", "when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg" }, { "command": "frontMatter.markup.bold", - "group": "navigation@-131", + "group": "navigation@-132", "when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg" }, { "command": "frontMatter.markup.italic", - "group": "navigation@-130", + "group": "navigation@-131", "when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg" }, { "command": "frontMatter.markup.strikethrough", - "group": "navigation@-129", + "group": "navigation@-130", "when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg" }, { - "command": "frontMatter.markup.blockquote", - "group": "navigation@-128", - "when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg" + "command": "frontMatter.insertSnippet", + "group": "navigation@-129", + "when": "resourceLangId == markdown" }, { "command": "frontMatter.insertImage", - "group": "navigation@-127", + "group": "navigation@-128", "when": "resourceLangId == markdown" }, { @@ -1350,6 +1390,11 @@ "group": "1_markup@5", "when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg" }, + { + "command": "frontMatter.markup.blockquote", + "group": "1_markup@6", + "when": "resourceLangId == markdown && frontMatter:markdown:wysiwyg" + }, { "command": "frontMatter.dashboard", "group": "navigation@-98", @@ -1471,6 +1516,48 @@ "text.html.markdown" ] } + ], + "walkthroughs": [ + { + "id": "frontmatter.welcome", + "title": "Get started with Front Matter", + "description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.", + "steps": [ + { + "id": "frontmatter.welcome.init", + "title": "Get started", + "description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)", + "media": { + "markdown": "assets/walkthrough/get-started.md" + }, + "completionEvents": [ + "onContext:frontMatterInitialized" + ] + }, + { + "id": "frontmatter.welcome.documentation", + "title": "Documentation", + "description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)", + "media": { + "markdown": "assets/walkthrough/documentation.md" + }, + "completionEvents": [ + "onLink:https://frontmatter.codes/docs" + ] + }, + { + "id": "frontmatter.welcome.supporter", + "title": "Support the project", + "description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)", + "media": { + "markdown": "assets/walkthrough/support-the-project.md" + }, + "completionEvents": [ + "onLink:https://github.com/sponsors/estruyf" + ] + } + ] + } ] }, "scripts": { diff --git a/src/commands/Article.ts b/src/commands/Article.ts index c737a136..a7afe679 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -1,5 +1,5 @@ import { isValidFile } from './../helpers/isValidFile'; -import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTINGS_CONTENT_PLACEHOLDERS, TelemetryEvent } from './../constants'; +import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_CONTENT_PLACEHOLDERS, TelemetryEvent } from './../constants'; import * as vscode from 'vscode'; import { Field, TaxonomyType } from "../models"; import { format } from "date-fns"; @@ -13,6 +13,7 @@ import { parseWinPath } from '../helpers/parseWinPath'; import { Telemetry } from '../helpers/Telemetry'; import { ParsedFrontMatter } from '../parsers'; import { MediaListener } from '../listeners/panel'; +import { NavigationType } from '../dashboardWebView/models'; export class Article { @@ -200,7 +201,7 @@ export class Article { } // Update the fields containing a custom placeholder that depends on slug - const placeholders = Settings.get<{id: string, value: string}[]>(SETTINGS_CONTENT_PLACEHOLDERS); + const placeholders = Settings.get<{id: string, value: string}[]>(SETTING_CONTENT_PLACEHOLDERS); const customPlaceholders = placeholders?.filter(p => p.value.includes("{{slug}}")); for (const customPlaceholder of (customPlaceholders || [])) { const customPlaceholderFields = contentType.fields.filter(f => f.default === `{{${customPlaceholder.id}}}`); @@ -340,6 +341,29 @@ export class Article { MediaListener.getMediaSelection(); } + /** + * Insert a snippet into the article + */ + public static async insertSnippet() { + let editor = vscode.window.activeTextEditor; + if (!editor) { + return; + } + + const position = editor.selection.active; + const selectionText = editor.document.getText(editor.selection); + + await vscode.commands.executeCommand(COMMAND_NAME.dashboard, { + type: NavigationType.Snippets, + data: { + filePath: editor.document.uri.fsPath, + fieldName: basename(editor.document.uri.fsPath), + position, + selection: selectionText + } + } as DashboardData); + } + /** * Get the current article */ diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index a06c1b8d..66cad2f1 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -1,4 +1,4 @@ -import { SETTINGS_DASHBOARD_OPENONSTART, CONTEXT } from '../constants'; +import { SETTING_DASHBOARD_OPENONSTART, CONTEXT } from '../constants'; import { join } from "path"; import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from "vscode"; import { Logger, Settings as SettingsHelper } from '../helpers'; @@ -8,7 +8,7 @@ import { WebviewHelper } from '@estruyf/vscode'; import { DashboardData } from '../models/DashboardData'; import { ExplorerView } from '../explorerView/ExplorerView'; import { MediaLibrary } from '../helpers/MediaLibrary'; -import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener } from '../listeners/dashboard'; +import { DashboardListener, MediaListener, SettingsListener, TelemetryListener, DataListener, PagesListener, ExtensionListener, SnippetListener } from '../listeners/dashboard'; import { MediaListener as PanelMediaListener } from '../listeners/panel' export class Dashboard { @@ -24,7 +24,7 @@ export class Dashboard { * Init the dashboard */ public static async init() { - const openOnStartup = SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART); + const openOnStartup = SettingsHelper.get(SETTING_DASHBOARD_OPENONSTART); if (openOnStartup) { Dashboard.open(); } @@ -143,6 +143,7 @@ export class Dashboard { SettingsListener.process(msg); DataListener.process(msg); TelemetryListener.process(msg); + SnippetListener.process(msg); }); } diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index ad664ce2..7650be95 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -1,5 +1,5 @@ import { Questions } from './../helpers/Questions'; -import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_CONTENT_SUPPORTED_FILETYPES, TelemetryEvent } from './../constants'; +import { SETTING_CONTENT_PAGE_FOLDERS, SETTING_CONTENT_STATIC_FOLDER, SETTING_CONTENT_SUPPORTED_FILETYPES, TelemetryEvent } from './../constants'; import { commands, Uri, workspace, window } from "vscode"; import { basename, join } from "path"; import { ContentFolder, FileInfo, FolderInfo } from "../models"; @@ -26,7 +26,7 @@ export class Folders { */ public static async addMediaFolder(data?: {selectedFolder?: string}) { let wsFolder = Folders.getWorkspaceFolder(); - const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER); + const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER); let startPath = ""; @@ -210,7 +210,7 @@ export class Folders { * Get the registered folders information */ public static async getInfo(limit?: number): Promise { - const supportedFiles = Settings.get(SETTINGS_CONTENT_SUPPORTED_FILETYPES); + const supportedFiles = Settings.get(SETTING_CONTENT_SUPPORTED_FILETYPES); const folders = Folders.get(); if (folders && folders.length > 0) { let folderInfo: FolderInfo[] = []; @@ -281,7 +281,7 @@ export class Folders { */ public static get(): ContentFolder[] { const wsFolder = Folders.getWorkspaceFolder(); - const folders: ContentFolder[] = Settings.get(SETTINGS_CONTENT_PAGE_FOLDERS) as ContentFolder[]; + const folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[]; return folders.map(folder => ({ ...folder, @@ -301,7 +301,7 @@ export class Folders { path: Folders.relWsFolder(folder, wsFolder) })); - await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true); + await Settings.update(SETTING_CONTENT_PAGE_FOLDERS, folderDetails, true); // Reinitialize the folder listeners PagesListener.startWatchers(); diff --git a/src/commands/Project.ts b/src/commands/Project.ts index b6dddbd2..0fbc2fb2 100644 --- a/src/commands/Project.ts +++ b/src/commands/Project.ts @@ -6,7 +6,8 @@ import { Notifications } from "../helpers/Notifications"; import { Template } from "./Template"; import { Folders } from "./Folders"; import { Settings } from "../helpers"; -import { SETTINGS_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants"; +import { SETTING_CONTENT_DEFAULT_FILETYPE, TelemetryEvent } from "../constants"; +import { SettingsListener } from '../listeners/dashboard'; export class Project { @@ -29,7 +30,7 @@ categories: [] public static async init(sampleTemplate: boolean = true) { try { Settings.createTeamSettings(); - const fileType = Settings.get(SETTINGS_CONTENT_DEFAULT_FILETYPE); + const fileType = Settings.get(SETTING_CONTENT_DEFAULT_FILETYPE); const folder = Template.getSettings(); const templatePath = Project.templatePath(); @@ -50,6 +51,8 @@ categories: [] } Telemetry.send(TelemetryEvent.initialization) + + SettingsListener.getSettings(); } catch (err: any) { Notifications.error(`Sorry, something went wrong - ${err?.message || err}`); } diff --git a/src/commands/Template.ts b/src/commands/Template.ts index 65d3b7f6..d164ed0f 100644 --- a/src/commands/Template.ts +++ b/src/commands/Template.ts @@ -2,7 +2,7 @@ import { Questions } from './../helpers/Questions'; import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; -import { SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, TelemetryEvent } from '../constants'; +import { SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, TelemetryEvent } from '../constants'; import { ArticleHelper, Settings } from '../helpers'; import { Article } from '.'; import { Notifications } from '../helpers/Notifications'; @@ -23,6 +23,10 @@ export class Template { public static async init() { const isInitialized = await Template.isInitialized(); await vscode.commands.executeCommand('setContext', CONTEXT.canInit, !isInitialized); + + if (isInitialized) { + await vscode.commands.executeCommand('setContext', CONTEXT.initialized, true); + } } /** @@ -52,7 +56,7 @@ export class Template { public static async generate() { const folder = Template.getSettings(); const editor = vscode.window.activeTextEditor; - const fileType = Settings.get(SETTINGS_CONTENT_DEFAULT_FILETYPE); + const fileType = Settings.get(SETTING_CONTENT_DEFAULT_FILETYPE); if (folder && editor && ArticleHelper.isMarkdownFile()) { const article = ArticleHelper.getFrontMatter(editor); diff --git a/src/commands/Wysiwyg.ts b/src/commands/Wysiwyg.ts index 66367c37..657335d2 100644 --- a/src/commands/Wysiwyg.ts +++ b/src/commands/Wysiwyg.ts @@ -1,5 +1,5 @@ import { commands, window, Selection, QuickPickItem } from "vscode"; -import { COMMAND_NAME, CONTEXT, SETTINGS_CONTENT_WYSIWYG } from "../constants"; +import { COMMAND_NAME, CONTEXT, SETTING_CONTENT_WYSIWYG } from "../constants"; import { Settings } from "../helpers"; enum MarkupType { @@ -24,7 +24,7 @@ export class Wysiwyg { */ public static async registerCommands(subscriptions: any) { - const wysiwygEnabled = Settings.get(SETTINGS_CONTENT_WYSIWYG); + const wysiwygEnabled = Settings.get(SETTING_CONTENT_WYSIWYG); if (!wysiwygEnabled) { return; @@ -54,6 +54,7 @@ export class Wysiwyg { { label: "$(tasklist) Task list", detail: "Add a task list", alwaysShow: true }, { label: "$(code) Code", detail: "Add inline code snippet", alwaysShow: true }, { label: "$(symbol-namespace) Code block", detail: "Add a code block", alwaysShow: true }, + { label: "$(quote) Blockquote", detail: "Add a blockquote", alwaysShow: true }, ] const option = await window.showQuickPick([ ...qpItems ], { @@ -73,6 +74,8 @@ export class Wysiwyg { await this.addMarkup(MarkupType.code); } else if (option.label === qpItems[4].label) { await this.addMarkup(MarkupType.codeblock); + } else if (option.label === qpItems[5].label) { + await this.addMarkup(MarkupType.blockquote); } } })); diff --git a/src/constants/Extension.ts b/src/constants/Extension.ts index 561a0541..e0446325 100644 --- a/src/constants/Extension.ts +++ b/src/constants/Extension.ts @@ -29,13 +29,17 @@ export const COMMAND_NAME = { preview: getCommandName("preview"), dashboard: getCommandName("dashboard"), dashboardMedia: getCommandName("dashboard.media"), + dashboardSnippets: getCommandName("dashboard.snippets"), dashboardData: getCommandName("dashboard.data"), dashboardClose: getCommandName("dashboard.close"), promote: getCommandName("promoteSettings"), - insertImage: getCommandName("insertImage"), createFolder: getCommandName("createFolder"), diagnostics: getCommandName("diagnostics"), + // Insert dashboards + insertImage: getCommandName("insertImage"), + insertSnippet: getCommandName("insertSnippet"), + // WYSIWYG bold: getCommandName("markup.bold"), italic: getCommandName("markup.italic"), diff --git a/src/constants/SnippetVariables.ts b/src/constants/SnippetVariables.ts new file mode 100644 index 00000000..5cdc2f2c --- /dev/null +++ b/src/constants/SnippetVariables.ts @@ -0,0 +1,7 @@ + + +export const SnippetVariables = { + FM_SELECTED_TEXT: 'FM_SELECTED_TEXT', + FM_TEXT: 'FM_TEXT_', + FM_MULTILINE: 'FM_MULTILINE_', +}; \ No newline at end of file diff --git a/src/constants/TelemetryEvent.ts b/src/constants/TelemetryEvent.ts index dfca5182..56a0aae5 100644 --- a/src/constants/TelemetryEvent.ts +++ b/src/constants/TelemetryEvent.ts @@ -4,6 +4,7 @@ export const TelemetryEvent = { openContentDashboard: 'openContentDashboard', openMediaDashboard: 'openMediaDashboard', openDataDashboard: 'openDataDashboard', + openSnippetsDashboard: 'openSnippetsDashboard', closeDashboard: 'closeDashboard', generateSlug: 'generateSlug', createContentFromTemplate: 'createContentFromTemplate', diff --git a/src/constants/charCode.ts b/src/constants/charCode.ts new file mode 100644 index 00000000..771b2c03 --- /dev/null +++ b/src/constants/charCode.ts @@ -0,0 +1,439 @@ +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + * + * SOURCE: https://github.com/microsoft/vscode/blob/32b031eeefc4fd27a21659d35070967bfe965bcc/src/vs/base/common/charCode.ts + */ + export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030A, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030B, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030C, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030D, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030E, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030F, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031A, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031B, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031C, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031D, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031E, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031F, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032A, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032B, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032C, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032D, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032E, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032F, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033A, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033B, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033C, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033D, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033E, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033F, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034A, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034B, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034C, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034D, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034E, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034F, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035A, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035B, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035C, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035D, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035E, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035F, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036A, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036B, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036C, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036D, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036E, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036F, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00A8, // U+00A8 DIAERESIS + U_MACRON = 0x00AF, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00B4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00B8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02D4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02D5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02D8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02D9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02DA, // U+02DA RING ABOVE + U_OGONEK = 0x02DB, // U+02DB OGONEK + U_SMALL_TILDE = 0x02DC, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02DD, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ED, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02F3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02F7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02F8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02FD, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1FBD, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1FBF, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1FC0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1FCD, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1FCE, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1FDD, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1FDE, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1FEF, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA + + U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP + U_LEFT_CORNER_BRACKET = 0x300C, // U+300C LEFT CORNER BRACKET + U_RIGHT_CORNER_BRACKET = 0x300D, // U+300D RIGHT CORNER BRACKET + U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET + U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET + + + U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, + + U_FULLWIDTH_SEMICOLON = 0xFF1B, // U+FF1B FULLWIDTH SEMICOLON + U_FULLWIDTH_COMMA = 0xFF0C, // U+FF0C FULLWIDTH COMMA +} \ No newline at end of file diff --git a/src/constants/context.ts b/src/constants/context.ts index 8f029de2..6f5b7689 100644 --- a/src/constants/context.ts +++ b/src/constants/context.ts @@ -1,5 +1,6 @@ export const CONTEXT = { canInit: "frontMatterCanInit", + initialized: "frontMatterInitialized", canOpenPreview: "frontMatterCanOpenPreview", canOpenDashboard: "frontMatterCanOpenDashboard", isEnabled: "frontMatter:enabled", diff --git a/src/constants/index.ts b/src/constants/index.ts index 9ae33331..4c6ae167 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -7,7 +7,9 @@ export * from './FrameworkDetectors'; export * from './Links'; export * from './LocalStore'; export * from './Navigation'; +export * from './SnippetVariables'; export * from './TelemetryEvent'; +export * from './charCode'; export * from './charMap'; export * from './context'; export * from './settings'; diff --git a/src/constants/settings.ts b/src/constants/settings.ts index d6664e6f..36d358b6 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -43,35 +43,36 @@ export const SETTING_PREVIEW_PATHNAME = "preview.pathName"; export const SETTING_CUSTOM_SCRIPTS = "custom.scripts"; 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_CONTENT_SORTING = "content.sorting"; -export const SETTINGS_CONTENT_WYSIWYG = "content.wysiwyg"; -export const SETTINGS_CONTENT_PLACEHOLDERS = "content.placeholders"; +export const SETTING_CONTENT_PAGE_FOLDERS = "content.pageFolders"; +export const SETTING_CONTENT_STATIC_FOLDER = "content.publicFolder"; +export const SETTING_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight"; +export const SETTING_CONTENT_DRAFT_FIELD = "content.draftField"; +export const SETTING_CONTENT_SORTING = "content.sorting"; +export const SETTING_CONTENT_WYSIWYG = "content.wysiwyg"; +export const SETTING_CONTENT_PLACEHOLDERS = "content.placeholders"; +export const SETTING_CONTENT_SNIPPETS = "content.snippets"; -export const SETTINGS_CONTENT_SORTING_DEFAULT = "content.defaultSorting"; -export const SETTINGS_MEDIA_SORTING_DEFAULT = "content.defaultSorting"; +export const SETTING_CONTENT_SORTING_DEFAULT = "content.defaultSorting"; +export const SETTING_MEDIA_SORTING_DEFAULT = "content.defaultSorting"; -export const SETTINGS_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType"; -export const SETTINGS_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes"; +export const SETTING_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType"; +export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes"; -export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart"; -export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet"; +export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart"; +export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet"; -export const SETTINGS_DATA_FILES = "data.files"; -export const SETTINGS_DATA_FOLDERS = "data.folders"; -export const SETTINGS_DATA_TYPES = "data.types"; +export const SETTING_DATA_FILES = "data.files"; +export const SETTING_DATA_FOLDERS = "data.folders"; +export const SETTING_DATA_TYPES = "data.types"; -export const SETTINGS_FILE_PRESERVE_CASING = "file.preserveCasing"; +export const SETTING_FILE_PRESERVE_CASING = "file.preserveCasing"; -export const SETTINGS_FRAMEWORK_ID = "framework.id"; -export const SETTINGS_FRAMEWORK_START = "framework.startCommand"; +export const SETTING_FRAMEWORK_ID = "framework.id"; +export const SETTING_FRAMEWORK_START = "framework.startCommand"; export const SETTING_SITE_BASEURL = "site.baseURL"; /** * @deprecated */ -export const SETTINGS_CONTENT_FOLDERS = "content.folders"; +export const SETTING_CONTENT_FOLDERS = "content.folders"; diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index c64974a0..305bbda4 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -25,4 +25,7 @@ export enum DashboardMessage { getDataEntries = 'getDataEntries', putDataEntries = 'putDataEntries', sendTelemetry = 'sendTelemetry', + insertSnippet = 'insertSnippet', + addSnippet = 'addSnippet', + updateSnippet = 'updateSnippet', } \ No newline at end of file diff --git a/src/dashboardWebView/components/Contents/Contents.tsx b/src/dashboardWebView/components/Contents/Contents.tsx index 25c2718e..9982119f 100644 --- a/src/dashboardWebView/components/Contents/Contents.tsx +++ b/src/dashboardWebView/components/Contents/Contents.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { useRecoilValue } from 'recoil'; import { Page } from '../../models'; import { SettingsSelector } from '../../state'; -import { Header } from '../Header'; import { Overview } from './Overview'; import { Spinner } from '../Spinner'; import { SponsorMsg } from '../SponsorMsg'; @@ -11,6 +10,7 @@ import { useEffect } from 'react'; import { Messenger } from '@estruyf/vscode/dist/client'; import { DashboardMessage } from '../../DashboardMessage'; import { TelemetryEvent } from '../../../constants'; +import { PageLayout } from '../Layout/PageLayout'; export interface IContentsProps { pages: Page[]; @@ -30,17 +30,14 @@ export const Contents: React.FunctionComponent = ({pages, loadin }, []); return ( - - - + { loading ? : } - + ); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index 29d62e18..05b8b6d4 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -23,7 +23,7 @@ export const Item: React.FunctionComponent = ({ fmFilePath, date, ti if (view === DashboardViewType.Grid) { return ( - { diff --git a/src/dashboardWebView/components/Dashboard.tsx b/src/dashboardWebView/components/Dashboard.tsx index 5cfbd97c..d999a298 100644 --- a/src/dashboardWebView/components/Dashboard.tsx +++ b/src/dashboardWebView/components/Dashboard.tsx @@ -9,6 +9,7 @@ import { Contents } from './Contents/Contents'; import { Media } from './Media/Media'; import { NavigationType } from '../models'; import { DataView } from './DataView'; +import { Snippets } from './SnippetsView/Snippets'; export interface IDashboardProps { showWelcome: boolean; @@ -31,6 +32,14 @@ export const Dashboard: React.FunctionComponent = ({showWelcome return ; } + if (view === NavigationType.Snippets) { + return ( + + + + ); + } + if (view === NavigationType.Media) { return ( diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index b665cd94..07cbe395 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -21,6 +21,7 @@ import { CustomScript } from '../../../models'; import { LightningBoltIcon, PlusIcon } from '@heroicons/react/outline'; export interface IHeaderProps { + header?: React.ReactNode; settings: Settings | null; // Navigation @@ -30,7 +31,7 @@ export interface IHeaderProps { folders?: string[]; } -export const Header: React.FunctionComponent = ({totalPages, folders, settings }: React.PropsWithChildren) => { +export const Header: React.FunctionComponent = ({header, totalPages, folders, settings }: React.PropsWithChildren) => { const [ crntTag, setCrntTag ] = useRecoilState(TagAtom); const [ crntCategory, setCrntCategory ] = useRecoilState(CategoryAtom); const [ view, setView ] = useRecoilState(DashboardViewAtom); @@ -148,6 +149,10 @@ export const Header: React.FunctionComponent = ({totalPages, folde > ) } + + { + header + } ); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/Tabs.tsx b/src/dashboardWebView/components/Header/Tabs.tsx index 35e664f4..fefe21a0 100644 --- a/src/dashboardWebView/components/Header/Tabs.tsx +++ b/src/dashboardWebView/components/Header/Tabs.tsx @@ -1,4 +1,4 @@ -import { DatabaseIcon, PhotographIcon } from '@heroicons/react/outline'; +import { DatabaseIcon, PhotographIcon, ScissorsIcon } from '@heroicons/react/outline'; import * as React from 'react'; import { useRecoilValue } from 'recoil'; import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon'; @@ -29,6 +29,13 @@ export const Tabs: React.FunctionComponent = ({ onNavigate }: React. Media + + + Snippets + + { (settings?.dataFiles && settings.dataFiles.length > 0) && ( diff --git a/src/dashboardWebView/components/Layout/PageLayout.tsx b/src/dashboardWebView/components/Layout/PageLayout.tsx new file mode 100644 index 00000000..634bf70e --- /dev/null +++ b/src/dashboardWebView/components/Layout/PageLayout.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { useRecoilValue } from 'recoil'; +import { SettingsSelector } from '../../state'; +import { Header } from '../Header'; + +export interface IPageLayoutProps { + header?: React.ReactNode; + folders?: string[] | undefined + totalPages?: number | undefined +} + +export const PageLayout: React.FunctionComponent = ({ header, folders, totalPages, children }: React.PropsWithChildren) => { + const settings = useRecoilValue(SettingsSelector); + + return ( + + + + + { children } + + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Media/Item.tsx b/src/dashboardWebView/components/Media/Item.tsx index 4d46bc75..ba79099c 100644 --- a/src/dashboardWebView/components/Media/Item.tsx +++ b/src/dashboardWebView/components/Media/Item.tsx @@ -205,7 +205,7 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi return ( <> - + diff --git a/src/dashboardWebView/components/Media/Media.tsx b/src/dashboardWebView/components/Media/Media.tsx index 7f33f42e..d00769cc 100644 --- a/src/dashboardWebView/components/Media/Media.tsx +++ b/src/dashboardWebView/components/Media/Media.tsx @@ -3,7 +3,6 @@ import {UploadIcon} from '@heroicons/react/outline'; import * as React from 'react'; import { useRecoilValue } from 'recoil'; import { LoadingAtom, MediaFoldersAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state'; -import { Header } from '../Header'; import { Spinner } from '../Spinner'; import { SponsorMsg } from '../SponsorMsg'; import { Item } from './Item'; @@ -16,6 +15,7 @@ import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMat import { FolderItem } from './FolderItem'; import useMedia from '../../hooks/useMedia'; import { TelemetryEvent } from '../../../constants'; +import { PageLayout } from '../Layout/PageLayout'; export interface IMediaProps {} @@ -56,16 +56,13 @@ export const Media: React.FunctionComponent = (props: React.PropsWi }); return ( - - - - - + + { viewData?.data?.filePath && ( - Select the image you want to use for your article. - You can also drag and drop images from your desktop and select that once uploaded. + Select the media file to add to your content. + You can also drag and drop images from your desktop and select them once uploaded. ) } @@ -123,6 +120,6 @@ export const Media: React.FunctionComponent = (props: React.PropsWi - + ); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Modals/Metadata.tsx b/src/dashboardWebView/components/Modals/FormDialog.tsx similarity index 82% rename from src/dashboardWebView/components/Modals/Metadata.tsx rename to src/dashboardWebView/components/Modals/FormDialog.tsx index df9d5109..701dc06d 100644 --- a/src/dashboardWebView/components/Modals/Metadata.tsx +++ b/src/dashboardWebView/components/Modals/FormDialog.tsx @@ -2,7 +2,7 @@ import { Dialog, Transition } from '@headlessui/react'; import * as React from 'react'; import { Fragment, useRef } from 'react'; -export interface IMetadataProps { +export interface IFormDialogProps { title: string; description: string; okBtnText: string; @@ -13,11 +13,10 @@ export interface IMetadataProps { trigger: () => void; } -export const Metadata: React.FunctionComponent = ({title, description, cancelBtnText, okBtnText, dismiss, isSaveDisabled, trigger, children}: React.PropsWithChildren) => { +export const FormDialog: React.FunctionComponent = ({title, description, cancelBtnText, okBtnText, dismiss, isSaveDisabled, trigger, children}: React.PropsWithChildren) => { const cancelButtonRef = useRef(null); - - + return ( dismiss()}> @@ -67,7 +66,7 @@ export const Metadata: React.FunctionComponent = ({title, descri trigger()} disabled={isSaveDisabled} > @@ -75,7 +74,7 @@ export const Metadata: React.FunctionComponent = ({title, descri dismiss()} ref={cancelButtonRef} > diff --git a/src/dashboardWebView/components/SnippetsView/Item.tsx b/src/dashboardWebView/components/SnippetsView/Item.tsx new file mode 100644 index 00000000..db964fc5 --- /dev/null +++ b/src/dashboardWebView/components/SnippetsView/Item.tsx @@ -0,0 +1,183 @@ +import { Messenger } from '@estruyf/vscode/dist/client'; +import { CodeIcon, DotsHorizontalIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline'; +import * as React from 'react'; +import { useCallback, useRef, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { DashboardMessage } from '../../DashboardMessage'; +import { SettingsSelector, ViewDataSelector } from '../../state'; +import { Alert } from '../Modals/Alert'; +import { FormDialog } from '../Modals/FormDialog'; +import { NewForm } from './NewForm'; +import SnippetForm, { SnippetFormHandle } from './SnippetForm'; + +export interface IItemProps { + title: string; + snippet: any; +} + +export const Item: React.FunctionComponent = ({ title, snippet }: React.PropsWithChildren) => { + const viewData = useRecoilValue(ViewDataSelector); + const settings = useRecoilValue(SettingsSelector); + const [ showInsertDialog, setShowInsertDialog ] = useState(false); + const [ showEditDialog, setShowEditDialog ] = useState(false); + const [ showAlert, setShowAlert ] = React.useState(false); + + const [ snippetTitle, setSnippetTitle ] = useState(''); + const [ snippetDescription, setSnippetDescription ] = useState(''); + const [ snippetOriginalBody, setSnippetOriginalBody ] = useState(''); + + const formRef = useRef(null); + + const insertToArticle = () => { + formRef.current?.onSave(); + setShowInsertDialog(false); + }; + + const reset = () => { + setShowEditDialog(false); + setSnippetTitle(''); + setSnippetDescription(''); + setSnippetOriginalBody(''); + }; + + const onOpenEdit = useCallback(() => { + setSnippetTitle(title); + setSnippetDescription(snippet.description); + setSnippetOriginalBody(typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`)); + setShowEditDialog(true); + }, [snippet]); + + const onSnippetUpdate = useCallback(() => { + if (!snippetTitle || !snippetOriginalBody) { + reset(); + return; + } + + const snippets = Object.assign({}, settings?.snippets || {}); + const snippetLines = snippetOriginalBody.split("\n"); + const snippetContents = { + description: snippetDescription || '', + body: snippetLines.length === 1 ? snippetLines[0] : snippetLines + }; + + if (title === snippetTitle) { + snippets[title] = snippetContents; + } else { + delete snippets[title]; + snippets[snippetTitle] = snippetContents; + } + + Messenger.send(DashboardMessage.updateSnippet, { snippets }); + + reset(); + }, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody]); + + const onDelete = useCallback(() => { + const snippets = Object.assign({}, settings?.snippets || {}); + delete snippets[title]; + + Messenger.send(DashboardMessage.updateSnippet, { snippets }); + + setShowAlert(false); + }, [settings?.snippets, title]); + + return ( + <> + + + + + + {title} + + + + + + + + + { + viewData?.data?.filePath && ( + <> + setShowInsertDialog(true)}> + + Insert snippet + + > + ) + } + + + + Edit snippet + + + setShowAlert(true)}> + + Delete snippet + + + + + + {snippet.description} + + + { + showInsertDialog && ( + setShowInsertDialog(false)} + okBtnText='Insert' + cancelBtnText='Cancel'> + + + + + ) + } + + { + showEditDialog && ( + + + setSnippetTitle(value)} + onDescriptionUpdate={(value: string) => setSnippetDescription(value)} + onBodyUpdate={(value: string) => setSnippetOriginalBody(value)} /> + + + ) + } + + { + showAlert && ( + setShowAlert(false)} + trigger={onDelete} /> + ) + } + > + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/SnippetsView/NewForm.tsx b/src/dashboardWebView/components/SnippetsView/NewForm.tsx new file mode 100644 index 00000000..6d866231 --- /dev/null +++ b/src/dashboardWebView/components/SnippetsView/NewForm.tsx @@ -0,0 +1,109 @@ +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/outline'; +import * as React from 'react'; +import { SnippetVariables } from '../../../constants'; + +export interface INewFormProps { + title: string; + description: string; + body: string; + + onTitleUpdate: (value: string) => void; + onDescriptionUpdate: (value: string) => void; + onBodyUpdate: (value: string) => void; +} + +export const NewForm: React.FunctionComponent = ({ title, description, body, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren) => { + const [ showDetails, setShowDetails ] = React.useState(false); + + return ( + + + + Title * + + + onTitleUpdate(e.currentTarget.value)} + /> + + + + + + Description + + + onDescriptionUpdate(e.currentTarget.value)} + /> + + + + + + Snippet * + + + onBodyUpdate(e.currentTarget.value)} + /> + + + + + + Placeholders guidelines + + { + showDetails ? ( + setShowDetails(false)}> + + + ) : ( + setShowDetails(true)}> + + + ) + } + + + + + + Insert selected text (can still be updated) + {`\${${SnippetVariables.FM_SELECTED_TEXT}}`} + + + + Variable without default + {`\${variable}`} + + + + Variable with default + {`\${variable:default}`} + + + + Variable with choices + {`\${variable|choice 1,choice 2,choice 3|}`} + + + + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx new file mode 100644 index 00000000..f476f471 --- /dev/null +++ b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx @@ -0,0 +1,171 @@ +import { Messenger } from '@estruyf/vscode/dist/client'; +import * as React from 'react'; +import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { SnippetVariables } from '../../../constants'; +import { Choice, SnippetParser, Variable, VariableResolver } from '../../../helpers/SnippetParser'; +import { SnippetField } from '../../../models'; +import { DashboardMessage } from '../../DashboardMessage'; +import { ViewDataSelector } from '../../state'; +import { SnippetInputField } from './SnippetInputField'; + + +export interface ISnippetFormProps { + snippet: any; + selection: string | undefined; +} + +export interface SnippetFormHandle { + onSave: () => void; +} + +const SnippetForm: React.ForwardRefRenderFunction = ({ snippet, selection }, ref) => { + const viewData = useRecoilValue(ViewDataSelector); + const [ fields, setFields ] = useState([]); + + const onTextChange = useCallback((field: SnippetField, value: string) => { + setFields(prevFields => prevFields.map(f => f.name === field.name ? { ...f, value } : f)); + }, [setFields]); + + const insertSelectionValue = useCallback((value: string) => { + if (selection && value === SnippetVariables.FM_SELECTED_TEXT) { + return selection; + } + + return; + }, [selection]); + + const snippetBody = useMemo(() => { + let body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`); + + for (const field of fields) { + body = body.replace(field.tmString, field.value); + } + + return body; + }, [fields, selection]); + + const shouldShowField = (fieldName: string, idx: number, allFields: SnippetField[]) => { + const crntField = allFields.findIndex(f => f.name === fieldName); + if (crntField < idx) { + return false; + } + return true; + } + + const fieldNameRender = (fieldName: string) => { + if (fieldName === SnippetVariables.FM_SELECTED_TEXT) { + return 'Selected Text'; + } + + if (fieldName.startsWith(SnippetVariables.FM_TEXT)) { + return fieldName.replace(SnippetVariables.FM_TEXT, ''); + } + + if (fieldName.startsWith(SnippetVariables.FM_MULTILINE)) { + return fieldName.replace(SnippetVariables.FM_MULTILINE, ''); + } + + return fieldName; + } + + useImperativeHandle(ref, () => ({ + onSave() { + if (!snippetBody) { + return; + } + + Messenger.send(DashboardMessage.insertSnippet, { + file: viewData?.data?.filePath, + snippet: snippetBody + }); + } + })); + + useEffect(() => { + // Defines the type of field that needs to be rendered + const getFieldType = (fieldName: string) => { + if (fieldName.startsWith(SnippetVariables.FM_MULTILINE)) { + return 'textarea'; + } + + return 'text'; + } + + // Get all placeholder variables from the snippet + const snippetParser = new SnippetParser(); + const body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`); + + const parsed = snippetParser.parse(body); + const placeholders = parsed.placeholderInfo.all; + + const allFields: any[] = []; + + for (const placeholder of placeholders) { + const tmString = placeholder.toTextmateString(); + + // If only a variable is defined, it will not contain children + if (placeholder.children.length === 0) { + allFields.push({ + type: getFieldType(placeholder.index as string), + name: placeholder.index, + value: insertSelectionValue(placeholder.index as string) || '', + tmString + }); + } else { + // Children are defined, so it means it is a choice field or the variable has a default value + for (const child of placeholder.children as any[]) { + if (child instanceof Choice) { + const options = child.options.map(o => o.value); + + allFields.push({ + type: 'select', + name: placeholder.index, + value: (child as any).value || options[0] || "", + options, + tmString + }); + } else { + allFields.push({ + type: getFieldType(placeholder.index as string), + name: placeholder.index, + value: insertSelectionValue((child as any).value as string) || insertSelectionValue(placeholder.index as string) || (child as any).value || "", + tmString + }); + } + } + } + } + + setFields(allFields); + }, []); + + return ( + + + {snippetBody} + + + + { + fields.map((field: SnippetField, index: number, allFields: SnippetField[]) => ( + shouldShowField(field.name, index, allFields) && ( + + + {fieldNameRender(field.name)} + + + + + + ) + )) + } + + + ); +}; + +export default React.forwardRef(SnippetForm); \ No newline at end of file diff --git a/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx b/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx new file mode 100644 index 00000000..50a79579 --- /dev/null +++ b/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { ChevronDownIcon } from '@heroicons/react/outline'; +import { SnippetField } from '../../../models'; +import { SnippetVariables } from '../../../constants'; + +export interface ISnippetInputFieldProps { + field: SnippetField; + onValueChange: (field: SnippetField, value: string) => void +} + +export const SnippetInputField: React.FunctionComponent = ({ field, onValueChange }: React.PropsWithChildren) => { + + if (field.type === 'select') { + return ( + + onValueChange(field, e.target.value)}> + { + field.options?.map((option: string, index: number) => ( + {option} + )) + } + + + + + ) + } + + if (field.type === 'textarea') { + return ( + onValueChange(field, e.currentTarget.value)} + /> + ) + } + + return ( + onValueChange(field, e.currentTarget.value)} + /> + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/SnippetsView/Snippets.tsx b/src/dashboardWebView/components/SnippetsView/Snippets.tsx new file mode 100644 index 00000000..f794ca26 --- /dev/null +++ b/src/dashboardWebView/components/SnippetsView/Snippets.tsx @@ -0,0 +1,121 @@ +import { Messenger } from '@estruyf/vscode/dist/client'; +import { CodeIcon, PlusSmIcon } from '@heroicons/react/outline'; +import * as React from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { DashboardMessage } from '../../DashboardMessage'; +import { SettingsSelector, ViewDataSelector } from '../../state'; +import { PageLayout } from '../Layout/PageLayout'; +import { FormDialog } from '../Modals/FormDialog'; +import { Item } from './Item'; +import { NewForm } from './NewForm'; + +export interface ISnippetsProps {} + +export const Snippets: React.FunctionComponent = (props: React.PropsWithChildren) => { + const settings = useRecoilValue(SettingsSelector); + const viewData = useRecoilValue(ViewDataSelector); + const [ snippetTitle, setSnippetTitle ] = useState(''); + const [ snippetDescription, setSnippetDescription ] = useState(''); + const [ snippetBody, setSnippetBody ] = useState(''); + const [ showCreateDialog, setShowCreateDialog ] = useState(false); + + const snippetKeys = useMemo(() => Object.keys(settings?.snippets) || [], [settings?.snippets]); + const snippets = settings?.snippets || {}; + + const onSnippetAdd = useCallback(() => { + if (!snippetTitle || !snippetBody) { + reset(); + return; + } + + Messenger.send(DashboardMessage.addSnippet, { + title: snippetTitle, + description: snippetDescription || '', + body: snippetBody + }); + + reset(); + }, [snippetTitle, snippetDescription, snippetBody]); + + const reset = () => { + setShowCreateDialog(false); + setSnippetTitle(''); + setSnippetDescription(''); + setSnippetBody(''); + }; + + return ( + + + setShowCreateDialog(true)}> + + Create new snippet + + + + )}> + + { + viewData?.data?.filePath && ( + + Select the snippet to add to your content. + + ) + } + + { + snippetKeys && snippetKeys.length > 0 ? ( + + { + snippetKeys.map((snippetKey: any, index: number) => ( + + )) + } + + ) : ( + + + + No snippets found + + + ) + } + + { + showCreateDialog && ( + + + setSnippetTitle(value)} + onDescriptionUpdate={(value: string) => setSnippetDescription(value)} + onBodyUpdate={(value: string) => setSnippetBody(value)} /> + + + ) + } + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Startup.tsx b/src/dashboardWebView/components/Startup.tsx index 07156d94..8cb95fbe 100644 --- a/src/dashboardWebView/components/Startup.tsx +++ b/src/dashboardWebView/components/Startup.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { SETTINGS_DASHBOARD_OPENONSTART } from '../../constants'; +import { SETTING_DASHBOARD_OPENONSTART } from '../../constants'; import { Messenger } from '@estruyf/vscode/dist/client'; import { DashboardMessage } from '../DashboardMessage'; import { Settings } from '../models/Settings'; @@ -13,7 +13,7 @@ export const Startup: React.FunctionComponent = ({settings}: Reac const onChange = (e: React.ChangeEvent) => { setIsChecked(e.target.checked); - Messenger.send(DashboardMessage.updateSetting, { name: SETTINGS_DASHBOARD_OPENONSTART, value: e.target.checked }); + Messenger.send(DashboardMessage.updateSetting, { name: SETTING_DASHBOARD_OPENONSTART, value: e.target.checked }); }; React.useEffect(() => { diff --git a/src/dashboardWebView/hooks/useMessages.tsx b/src/dashboardWebView/hooks/useMessages.tsx index fec94a32..32bbebdb 100644 --- a/src/dashboardWebView/hooks/useMessages.tsx +++ b/src/dashboardWebView/hooks/useMessages.tsx @@ -28,6 +28,8 @@ export default function useMessages() { setView(NavigationType.Contents); } else if (message.data.data?.type === NavigationType.Data) { setView(NavigationType.Data); + } else if (message.data.data?.type === NavigationType.Snippets) { + setView(NavigationType.Snippets); } break; case DashboardCommand.settings: diff --git a/src/dashboardWebView/models/NavigationType.ts b/src/dashboardWebView/models/NavigationType.ts index eba82bf9..2fd1da24 100644 --- a/src/dashboardWebView/models/NavigationType.ts +++ b/src/dashboardWebView/models/NavigationType.ts @@ -2,4 +2,5 @@ export enum NavigationType { Contents = "contents", Media = "media", Data = "data", + Snippets = "snippets", } \ No newline at end of file diff --git a/src/dashboardWebView/models/Settings.ts b/src/dashboardWebView/models/Settings.ts index 0dca39c7..e91aaf0f 100644 --- a/src/dashboardWebView/models/Settings.ts +++ b/src/dashboardWebView/models/Settings.ts @@ -29,6 +29,7 @@ export interface Settings { dataFiles: DataFile[] | undefined; dataTypes: DataType[] | undefined; isBacker: boolean | undefined; + snippets: any | undefined; } export interface DashboardState { diff --git a/src/extension.ts b/src/extension.ts index 76aac326..a17b6dac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,6 +22,7 @@ import { Diagnostics } from './commands/Diagnostics'; import { PagesListener } from './listeners/dashboard'; import { Backers } from './commands/Backers'; import { DataListener, SettingsListener } from './listeners/panel'; +import { NavigationType } from './dashboardWebView/models'; let frontMatterStatusBar: vscode.StatusBarItem; let statusDebouncer: { (fnc: any, time: number): void; }; @@ -57,7 +58,7 @@ export async function activate(context: vscode.ExtensionContext) { subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => { Telemetry.send(TelemetryEvent.openContentDashboard); if (!data) { - Dashboard.open({ type: "contents" }); + Dashboard.open({ type: NavigationType.Contents }); } else { Dashboard.open(data); } @@ -65,12 +66,17 @@ export async function activate(context: vscode.ExtensionContext) { subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardMedia, (data?: DashboardData) => { Telemetry.send(TelemetryEvent.openMediaDashboard); - Dashboard.open({ type: "media" }); + Dashboard.open({ type: NavigationType.Media }); + })); + + subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardSnippets, (data?: DashboardData) => { + Telemetry.send(TelemetryEvent.openSnippetsDashboard); + Dashboard.open({ type: NavigationType.Snippets }); })); subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardData, (data?: DashboardData) => { Telemetry.send(TelemetryEvent.openDataDashboard); - Dashboard.open({ type: "data" }); + Dashboard.open({ type: NavigationType.Data }); })); subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardClose, (data?: DashboardData) => { @@ -206,6 +212,9 @@ export async function activate(context: vscode.ExtensionContext) { // Inserting an image in Markdown subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage)); + // Inserting a snippet in Markdown + subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertSnippet, Article.insertSnippet)); + // Create the editor experience for bulk scripts subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(ContentProvider.scheme, new ContentProvider())); diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index 71ab9e66..5f9ca6e0 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -2,7 +2,7 @@ import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider' import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType'; import * as vscode from 'vscode'; import * as fs from "fs"; -import { DefaultFields, SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTINGS_CONTENT_PLACEHOLDERS, SETTINGS_CONTENT_SUPPORTED_FILETYPES, SETTINGS_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from '../constants'; +import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from '../constants'; import { DumpOptions } from 'js-yaml'; import { FrontMatterParser, ParsedFrontMatter } from '../parsers'; import { Extension, Logger, Settings, SlugHelper } from '.'; @@ -143,7 +143,7 @@ export class ArticleHelper { */ public static isMarkdownFile(document: vscode.TextDocument | undefined | null = null) { const supportedLanguages = ["markdown", "mdx"]; - const fileTypes = Settings.get(SETTINGS_CONTENT_SUPPORTED_FILETYPES); + const fileTypes = Settings.get(SETTING_CONTENT_SUPPORTED_FILETYPES); const supportedFileExtensions = fileTypes ? fileTypes.map(f => f.startsWith(`.`) ? f : `.${f}`) : DEFAULT_FILE_TYPES; const languageId = document?.languageId?.toLowerCase(); const isSupportedLanguage = languageId && supportedLanguages.includes(languageId); @@ -227,7 +227,7 @@ export class ArticleHelper { * @returns */ public static sanitize(value: string): string { - const preserveCasing = Settings.get(SETTINGS_FILE_PRESERVE_CASING) as boolean; + const preserveCasing = Settings.get(SETTING_FILE_PRESERVE_CASING) as boolean; return sanitize((preserveCasing ? value : value.toLowerCase()).replace(/ /g, "-")); } @@ -240,7 +240,7 @@ export class ArticleHelper { */ public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): string | undefined { const prefix = Settings.get(SETTING_TEMPLATES_PREFIX); - const fileType = Settings.get(SETTINGS_CONTENT_DEFAULT_FILETYPE); + const fileType = Settings.get(SETTING_CONTENT_DEFAULT_FILETYPE); // Name of the file or folder to create const sanitizedName = ArticleHelper.sanitize(titleValue); @@ -336,7 +336,7 @@ export class ArticleHelper { */ public static processCustomPlaceholders(value: string, title: string) { if (value && typeof value === "string") { - const placeholders = Settings.get<{id: string, value: string}[]>(SETTINGS_CONTENT_PLACEHOLDERS); + const placeholders = Settings.get<{id: string, value: string}[]>(SETTING_CONTENT_PLACEHOLDERS); if (placeholders && placeholders.length > 0) { for (const placeholder of placeholders) { if (value.includes(`{{${placeholder.id}}}`)) { diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index 89693f95..875ebd23 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -1,6 +1,6 @@ import { PagesListener } from './../listeners/dashboard'; import { ArticleHelper, Settings } from "."; -import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants"; +import { SETTING_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants"; import { ContentType as IContentType, DraftField, Field } from '../models'; import { Uri, commands } from 'vscode'; import { Folders } from "../commands/Folders"; @@ -18,7 +18,7 @@ export class ContentType { * @returns */ public static getDraftField() { - const draftField = Settings.get(SETTINGS_CONTENT_DRAFT_FIELD); + const draftField = Settings.get(SETTING_CONTENT_DRAFT_FIELD); if (draftField) { return draftField; } diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index de5faaaa..af8005ff 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -2,7 +2,7 @@ import { basename, join } from "path"; import { workspace } from "vscode"; import { Folders } from "../commands/Folders"; import { Template } from "../commands/Template"; -import { CONTEXT, ExtensionState, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DATA_FILES, SETTINGS_DATA_FOLDERS, SETTINGS_DATA_TYPES, SETTINGS_FRAMEWORK_ID, SETTINGS_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants"; +import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS } from "../constants"; import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models"; import { CustomScript, DraftField, ScriptType, SortingSetting, TaxonomyType } from "../models"; import { DataFile } from "../models/DataFile"; @@ -23,35 +23,36 @@ export class DashboardSettings { return { beta: ext.isBetaVersion(), wsFolder: wsFolder ? wsFolder.fsPath : '', - staticFolder: Settings.get(SETTINGS_CONTENT_STATIC_FOLDER), + staticFolder: Settings.get(SETTING_CONTENT_STATIC_FOLDER), folders: Folders.get(), initialized: isInitialized, tags: Settings.getTaxonomy(TaxonomyType.Tag), categories: Settings.getTaxonomy(TaxonomyType.Category), - openOnStart: Settings.get(SETTINGS_DASHBOARD_OPENONSTART), + openOnStart: Settings.get(SETTING_DASHBOARD_OPENONSTART), versionInfo: ext.getVersion(), pageViewType: await ext.getState(ExtensionState.PagesView, "workspace"), - mediaSnippet: Settings.get(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [], + mediaSnippet: Settings.get(SETTING_DASHBOARD_MEDIA_SNIPPET) || [], contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [], - draftField: Settings.get(SETTINGS_CONTENT_DRAFT_FIELD), - customSorting: Settings.get(SETTINGS_CONTENT_SORTING), + draftField: Settings.get(SETTING_CONTENT_DRAFT_FIELD), + customSorting: Settings.get(SETTING_CONTENT_SORTING), contentFolders: Folders.get(), - crntFramework: Settings.get(SETTINGS_FRAMEWORK_ID), + crntFramework: Settings.get(SETTING_FRAMEWORK_ID), framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null, scripts: (Settings.get(SETTING_CUSTOM_SCRIPTS) || []), dashboardState: { contents: { sorting: await ext.getState(ExtensionState.Dashboard.Contents.Sorting, "workspace"), - defaultSorting: Settings.get(SETTINGS_CONTENT_SORTING_DEFAULT) + defaultSorting: Settings.get(SETTING_CONTENT_SORTING_DEFAULT) }, media: { sorting: await ext.getState(ExtensionState.Dashboard.Media.Sorting, "workspace"), - defaultSorting: Settings.get(SETTINGS_MEDIA_SORTING_DEFAULT), + defaultSorting: Settings.get(SETTING_MEDIA_SORTING_DEFAULT), selectedFolder: await ext.getState(ExtensionState.SelectedFolder, "workspace") } }, dataFiles: await this.getDataFiles(), - dataTypes: Settings.get(SETTINGS_DATA_TYPES), + dataTypes: Settings.get(SETTING_DATA_TYPES), + snippets: Settings.get(SETTING_CONTENT_SNIPPETS), isBacker: await ext.getState(CONTEXT.backer, 'global') } as ISettings } @@ -62,8 +63,8 @@ export class DashboardSettings { */ private static async getDataFiles(): Promise { const wsPath = Folders.getWorkspaceFolder()?.fsPath; - const files = Settings.get(SETTINGS_DATA_FILES); - const folders = Settings.get(SETTINGS_DATA_FOLDERS); + const files = Settings.get(SETTING_DATA_FILES); + const folders = Settings.get(SETTING_DATA_FOLDERS); let clonedFiles = Object.assign([], files); if (folders) { diff --git a/src/helpers/Extension.ts b/src/helpers/Extension.ts index eba367b0..dd5f2698 100644 --- a/src/helpers/Extension.ts +++ b/src/helpers/Extension.ts @@ -2,7 +2,7 @@ import { existsSync, renameSync } from "fs"; import { basename, join } from "path"; import { extensions, Uri, ExtensionContext, window, workspace, commands, ExtensionMode, DiagnosticCollection, languages } from "vscode"; import { Folders, WORKSPACE_PLACEHOLDER } from "../commands/Folders"; -import { EXTENSION_NAME, GITHUB_LINK, SETTINGS_CONTENT_FOLDERS, SETTINGS_CONTENT_PAGE_FOLDERS, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, DEFAULT_CONTENT_TYPE_NAME, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, DefaultFields, LocalStore, SETTING_TEMPLATES_FOLDER } from "../constants"; +import { EXTENSION_NAME, GITHUB_LINK, SETTING_CONTENT_FOLDERS, SETTING_CONTENT_PAGE_FOLDERS, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, DEFAULT_CONTENT_TYPE_NAME, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, DefaultFields, LocalStore, SETTING_TEMPLATES_FOLDER } from "../constants"; import { ContentType } from "../models"; import { Notifications } from "./Notifications"; import { parseWinPath } from "./parseWinPath"; @@ -137,7 +137,7 @@ export class Extension { // Migration to version 3.1.0 if (major < 3 || (major === 3 && minor < 1)) { - const folders = Settings.get(SETTINGS_CONTENT_FOLDERS); + const folders = Settings.get(SETTING_CONTENT_FOLDERS); if (folders && folders.length > 0) { const workspace = Folders.getWorkspaceFolder(); const projectFolder = basename(workspace?.fsPath || ""); @@ -147,7 +147,7 @@ export class Extension { path: `${WORKSPACE_PLACEHOLDER}${folder.fsPath.split(projectFolder).slice(1).join('')}`.split('\\').join('/') })); - await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, paths); + await Settings.update(SETTING_CONTENT_PAGE_FOLDERS, paths); } } diff --git a/src/helpers/ImageHelper.ts b/src/helpers/ImageHelper.ts index d5567e51..73c8be0a 100644 --- a/src/helpers/ImageHelper.ts +++ b/src/helpers/ImageHelper.ts @@ -5,7 +5,7 @@ import { Field } from '../models'; import { existsSync } from 'fs'; import { Folders } from '../commands/Folders'; import { Settings } from './SettingsHelper'; -import { SETTINGS_CONTENT_STATIC_FOLDER } from '../constants'; +import { SETTING_CONTENT_STATIC_FOLDER } from '../constants'; import { parseWinPath } from './parseWinPath'; export class ImageHelper { @@ -51,7 +51,7 @@ export class ImageHelper { */ public static relToAbs(filePath: string, value: string) { const wsFolder = Folders.getWorkspaceFolder(); - const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER); + const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER); const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "", value); const contentFolderPath = filePath ? join(dirname(filePath), value) : null; @@ -73,7 +73,7 @@ export class ImageHelper { */ public static absToRel(imgValue: string) { const wsFolder = Folders.getWorkspaceFolder(); - const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER); + const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER); let relPath = imgValue || ""; if (imgValue) { diff --git a/src/helpers/MediaHelpers.ts b/src/helpers/MediaHelpers.ts index 3383977b..bd1bfede 100644 --- a/src/helpers/MediaHelpers.ts +++ b/src/helpers/MediaHelpers.ts @@ -1,7 +1,7 @@ import { decodeBase64Image, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from "."; import { Dashboard } from "../commands/Dashboard"; import { Folders } from "../commands/Folders"; -import { ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTINGS_CONTENT_STATIC_FOLDER } from "../constants"; +import { ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_CONTENT_STATIC_FOLDER } from "../constants"; import { SortingOption } from "../dashboardWebView/models"; import { MediaInfo, MediaPaths, SortOrder, SortType } from "../models"; import { basename, extname, join, parse, dirname } from "path"; @@ -26,7 +26,7 @@ export class MediaHelpers { */ public static async getMedia(page: number = 0, requestedFolder: string = '', sort: SortingOption | null = null) { const wsFolder = Folders.getWorkspaceFolder(); - const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER); + const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER); const contentFolders = Folders.get(); const viewData = Dashboard.viewData; let selectedFolder = requestedFolder; @@ -199,7 +199,7 @@ export class MediaHelpers { public static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) { if (fileName && contents) { const wsFolder = Folders.getWorkspaceFolder(); - const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER); + const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER); const wsPath = wsFolder ? wsFolder.fsPath : ""; let absFolderPath = join(wsPath, staticFolder || ""); diff --git a/src/helpers/PanelSettings.ts b/src/helpers/PanelSettings.ts index 3d860625..6664baf6 100644 --- a/src/helpers/PanelSettings.ts +++ b/src/helpers/PanelSettings.ts @@ -3,7 +3,7 @@ import { Extension, Settings } from "." import { Dashboard } from "../commands/Dashboard" import { Preview } from "../commands/Preview" import { Template } from "../commands/Template" -import { CONTEXT, DefaultFields, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTINGS_DATA_TYPES, SETTINGS_FRAMEWORK_ID, SETTINGS_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_COMMA_SEPARATED_FIELDS, SETTING_CUSTOM_SCRIPTS, SETTING_DATE_FORMAT, SETTING_PANEL_FREEFORM, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_SLUG_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_FIELD_GROUPS, SETTING_TAXONOMY_TAGS } from "../constants" +import { CONTEXT, DefaultFields, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_COMMA_SEPARATED_FIELDS, SETTING_CUSTOM_SCRIPTS, SETTING_DATE_FORMAT, SETTING_PANEL_FREEFORM, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_SLUG_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_FIELD_GROUPS, SETTING_TAXONOMY_TAGS } from "../constants" import { CustomScript, DataType, DraftField, FieldGroup, PanelSettings as IPanelSettings, ScriptType } from "../models" export class PanelSettings { @@ -33,18 +33,18 @@ export class PanelSettings { isInitialized: await Template.isInitialized(), modifiedDateUpdate: Settings.get(SETTING_AUTO_UPDATE_DATE) || false, writingSettingsEnabled: this.isWritingSettingsEnabled() || false, - fmHighlighting: Settings.get(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT), + fmHighlighting: Settings.get(SETTING_CONTENT_FRONTMATTER_HIGHLIGHT), preview: Preview.getSettings(), commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [], contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [], dashboardViewData: Dashboard.viewData, - draftField: Settings.get(SETTINGS_CONTENT_DRAFT_FIELD), + draftField: Settings.get(SETTING_CONTENT_DRAFT_FIELD), isBacker: await Extension.getInstance().getState(CONTEXT.backer, 'global'), - framework: Settings.get(SETTINGS_FRAMEWORK_ID), + framework: Settings.get(SETTING_FRAMEWORK_ID), commands: { - start: Settings.get(SETTINGS_FRAMEWORK_START) + start: Settings.get(SETTING_FRAMEWORK_START) }, - dataTypes: Settings.get(SETTINGS_DATA_TYPES), + dataTypes: Settings.get(SETTING_DATA_TYPES), fieldGroups: Settings.get(SETTING_TAXONOMY_FIELD_GROUPS), } } diff --git a/src/helpers/SnippetParser.ts b/src/helpers/SnippetParser.ts new file mode 100644 index 00000000..9b3c2591 --- /dev/null +++ b/src/helpers/SnippetParser.ts @@ -0,0 +1,977 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// SOURCE: https://github.com/microsoft/vscode/tree/main/src/vs/editor/contrib/snippet/browser + +import { CharCode } from '../constants/charCode'; + +export const enum TokenType { + Dollar, + Colon, + Comma, + CurlyOpen, + CurlyClose, + Backslash, + Forwardslash, + Pipe, + Int, + VariableName, + Format, + Plus, + Dash, + QuestionMark, + EOF +} + +export interface Token { + type: TokenType; + pos: number; + len: number; +} + + +export class Scanner { + + private static _table: { [ch: number]: TokenType } = { + [CharCode.DollarSign]: TokenType.Dollar, + [CharCode.Colon]: TokenType.Colon, + [CharCode.Comma]: TokenType.Comma, + [CharCode.OpenCurlyBrace]: TokenType.CurlyOpen, + [CharCode.CloseCurlyBrace]: TokenType.CurlyClose, + [CharCode.Backslash]: TokenType.Backslash, + [CharCode.Slash]: TokenType.Forwardslash, + [CharCode.Pipe]: TokenType.Pipe, + [CharCode.Plus]: TokenType.Plus, + [CharCode.Dash]: TokenType.Dash, + [CharCode.QuestionMark]: TokenType.QuestionMark, + }; + + static isDigitCharacter(ch: number): boolean { + return ch >= CharCode.Digit0 && ch <= CharCode.Digit9; + } + + static isVariableCharacter(ch: number): boolean { + return ch === CharCode.Underline + || (ch >= CharCode.a && ch <= CharCode.z) + || (ch >= CharCode.A && ch <= CharCode.Z); + } + + value: string = ''; + pos: number = 0; + + text(value: string) { + this.value = value; + this.pos = 0; + } + + tokenText(token: Token): string { + return this.value.substr(token.pos, token.len); + } + + next(): Token { + + if (this.pos >= this.value.length) { + return { type: TokenType.EOF, pos: this.pos, len: 0 }; + } + + let pos = this.pos; + let len = 0; + let ch = this.value.charCodeAt(pos); + let type: TokenType; + + // static types + type = Scanner._table[ch]; + if (typeof type === 'number') { + this.pos += 1; + return { type, pos, len: 1 }; + } + + // number + if (Scanner.isDigitCharacter(ch)) { + type = TokenType.Int; + do { + len += 1; + ch = this.value.charCodeAt(pos + len); + } while (Scanner.isDigitCharacter(ch)); + + this.pos += len; + return { type, pos, len }; + } + + // variable name + if (Scanner.isVariableCharacter(ch)) { + type = TokenType.VariableName; + do { + ch = this.value.charCodeAt(pos + (++len)); + } while (Scanner.isVariableCharacter(ch) || Scanner.isDigitCharacter(ch)); + + this.pos += len; + return { type, pos, len }; + } + + + // format + type = TokenType.Format; + do { + len += 1; + ch = this.value.charCodeAt(pos + len); + } while ( + !isNaN(ch) + && typeof Scanner._table[ch] === 'undefined' // not static token + && !Scanner.isDigitCharacter(ch) // not number + && !Scanner.isVariableCharacter(ch) // not variable + ); + + this.pos += len; + return { type, pos, len }; + } +} + +export abstract class Marker { + + readonly _markerBrand: any; + + public parent!: Marker; + protected _children: Marker[] = []; + + appendChild(child: Marker): this { + if (child instanceof Text && this._children[this._children.length - 1] instanceof Text) { + // this and previous child are text -> merge them + (this._children[this._children.length - 1]).value += child.value; + } else { + // normal adoption of child + child.parent = this; + this._children.push(child); + } + return this; + } + + replace(child: Marker, others: Marker[]): void { + const { parent } = child; + const idx = parent.children.indexOf(child); + const newChildren = parent.children.slice(0); + newChildren.splice(idx, 1, ...others); + parent._children = newChildren; + + (function _fixParent(children: Marker[], parent: Marker) { + for (const child of children) { + child.parent = parent; + _fixParent(child.children, child); + } + })(others, parent); + } + + get children(): Marker[] { + return this._children; + } + + get snippet(): TextmateSnippet | undefined { + let candidate: Marker = this; + while (true) { + if (!candidate) { + return undefined; + } + if (candidate instanceof TextmateSnippet) { + return candidate; + } + candidate = candidate.parent; + } + } + + toString(): string { + return this.children.reduce((prev, cur) => prev + cur.toString(), ''); + } + + abstract toTextmateString(): string; + + len(): number { + return 0; + } + + abstract clone(): Marker; +} + +export class Text extends Marker { + + static escape(value: string): string { + return value.replace(/\$|}|\\/g, '\\$&'); + } + + constructor(public value: string) { + super(); + } + override toString() { + return this.value; + } + toTextmateString(): string { + return Text.escape(this.value); + } + override len(): number { + return this.value.length; + } + clone(): Text { + return new Text(this.value); + } +} + +export abstract class TransformableMarker extends Marker { + public transform?: Transform; +} + +export class Placeholder extends TransformableMarker { + + constructor(public index: number | string) { + super(); + } + + get isFinalTabstop() { + return this.index === 0; + } + + get choice(): Choice | undefined { + return this._children.length === 1 && this._children[0] instanceof Choice + ? this._children[0] as Choice + : undefined; + } + + toTextmateString(): string { + let transformString = ''; + + if (this.transform) { + transformString = this.transform.toTextmateString(); + } + + if (this.children.length === 0 && !this.transform) { + return `\${${this.index}}`; + } else if (this.children.length === 0) { + return `\${${this.index}${transformString}}`; + } else if (this.choice) { + return `\${${this.index}|${this.choice.toTextmateString()}|${transformString}}`; + } else { + return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`; + } + } + + clone(): Placeholder { + let ret = new Placeholder(this.index); + if (this.transform) { + ret.transform = this.transform.clone(); + } + ret._children = this.children.map(child => child.clone()); + return ret; + } +} + +export class Choice extends Marker { + + readonly options: Text[] = []; + + override appendChild(marker: Marker): this { + if (marker instanceof Text) { + marker.parent = this; + this.options.push(marker); + } + return this; + } + + override toString() { + return this.options[0].value; + } + + toTextmateString(): string { + return this.options + .map(option => option.value.replace(/\||,/g, '\\$&')) + .join(','); + } + + override len(): number { + return this.options[0].len(); + } + + clone(): Choice { + let ret = new Choice(); + this.options.forEach(ret.appendChild, ret); + return ret; + } +} + +export class Transform extends Marker { + + regexp: RegExp = new RegExp(''); + + resolve(value: string): string { + const _this = this; + let didMatch = false; + let ret = value.replace(this.regexp, function () { + didMatch = true; + return _this._replace(Array.prototype.slice.call(arguments, 0, -2)); + }); + // when the regex didn't match and when the transform has + // else branches, then run those + if (!didMatch && this._children.some(child => child instanceof FormatString && Boolean(child.elseValue))) { + ret = this._replace([]); + } + return ret; + } + + private _replace(groups: string[]): string { + let ret = ''; + for (const marker of this._children) { + if (marker instanceof FormatString) { + let value = groups[marker.index] || ''; + value = marker.resolve(value); + ret += value; + } else { + ret += marker.toString(); + } + } + return ret; + } + + override toString(): string { + return ''; + } + + toTextmateString(): string { + return `/${this.regexp.source}/${this.children.map(c => c.toTextmateString())}/${(this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : '')}`; + } + + clone(): Transform { + let ret = new Transform(); + ret.regexp = new RegExp(this.regexp.source, '' + (this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : '')); + ret._children = this.children.map(child => child.clone()); + return ret; + } + +} + +export class FormatString extends Marker { + + constructor( + readonly index: number, + readonly shorthandName?: string, + readonly ifValue?: string, + readonly elseValue?: string, + ) { + super(); + } + + resolve(value?: string): string { + if (this.shorthandName === 'upcase') { + return !value ? '' : value.toLocaleUpperCase(); + } else if (this.shorthandName === 'downcase') { + return !value ? '' : value.toLocaleLowerCase(); + } else if (this.shorthandName === 'capitalize') { + return !value ? '' : (value[0].toLocaleUpperCase() + value.substr(1)); + } else if (this.shorthandName === 'pascalcase') { + return !value ? '' : this._toPascalCase(value); + } else if (this.shorthandName === 'camelcase') { + return !value ? '' : this._toCamelCase(value); + } else if (Boolean(value) && typeof this.ifValue === 'string') { + return this.ifValue; + } else if (!Boolean(value) && typeof this.elseValue === 'string') { + return this.elseValue; + } else { + return value || ''; + } + } + + private _toPascalCase(value: string): string { + const match = value.match(/[a-z0-9]+/gi); + if (!match) { + return value; + } + return match.map(word => { + return word.charAt(0).toUpperCase() + + word.substr(1).toLowerCase(); + }) + .join(''); + } + + private _toCamelCase(value: string): string { + const match = value.match(/[a-z0-9]+/gi); + if (!match) { + return value; + } + return match.map((word, index) => { + if (index === 0) { + return word.toLowerCase(); + } else { + return word.charAt(0).toUpperCase() + + word.substr(1).toLowerCase(); + } + }) + .join(''); + } + + toTextmateString(): string { + let value = '${'; + value += this.index; + if (this.shorthandName) { + value += `:/${this.shorthandName}`; + + } else if (this.ifValue && this.elseValue) { + value += `:?${this.ifValue}:${this.elseValue}`; + } else if (this.ifValue) { + value += `:+${this.ifValue}`; + } else if (this.elseValue) { + value += `:-${this.elseValue}`; + } + value += '}'; + return value; + } + + clone(): FormatString { + let ret = new FormatString(this.index, this.shorthandName, this.ifValue, this.elseValue); + return ret; + } +} + +export class Variable extends TransformableMarker { + + constructor(public name: string) { + super(); + } + + resolve(resolver: VariableResolver): boolean { + let value = resolver.resolve(this); + if (this.transform) { + value = this.transform.resolve(value || ''); + } + if (value !== undefined) { + this._children = [new Text(value)]; + return true; + } + return false; + } + + toTextmateString(): string { + let transformString = ''; + if (this.transform) { + transformString = this.transform.toTextmateString(); + } + if (this.children.length === 0) { + return `\${${this.name}${transformString}}`; + } else { + return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`; + } + } + + clone(): Variable { + const ret = new Variable(this.name); + if (this.transform) { + ret.transform = this.transform.clone(); + } + ret._children = this.children.map(child => child.clone()); + return ret; + } +} + +export interface VariableResolver { + resolve(variable: Variable): string | undefined; +} + +function walk(marker: Marker[], visitor: (marker: Marker) => boolean): void { + const stack = [...marker]; + while (stack.length > 0) { + const marker = stack.shift()!; + const recurse = visitor(marker); + if (!recurse) { + break; + } + stack.unshift(...marker.children); + } +} + +export class TextmateSnippet extends Marker { + + private _placeholders?: { all: Placeholder[]; last?: Placeholder }; + + get placeholderInfo() { + if (!this._placeholders) { + // fill in placeholders + let all: Placeholder[] = []; + let last: Placeholder | undefined; + this.walk(function (candidate) { + if (candidate instanceof Placeholder) { + all.push(candidate); + last = !last || last.index < candidate.index ? candidate : last; + } + return true; + }); + this._placeholders = { all, last }; + } + return this._placeholders; + } + + get placeholders(): Placeholder[] { + const { all } = this.placeholderInfo; + return all; + } + + offset(marker: Marker): number { + let pos = 0; + let found = false; + this.walk(candidate => { + if (candidate === marker) { + found = true; + return false; + } + pos += candidate.len(); + return true; + }); + + if (!found) { + return -1; + } + return pos; + } + + fullLen(marker: Marker): number { + let ret = 0; + walk([marker], marker => { + ret += marker.len(); + return true; + }); + return ret; + } + + enclosingPlaceholders(placeholder: Placeholder): Placeholder[] { + let ret: Placeholder[] = []; + let { parent } = placeholder; + while (parent) { + if (parent instanceof Placeholder) { + ret.push(parent); + } + parent = parent.parent; + } + return ret; + } + + resolveVariables(resolver: VariableResolver): this { + this.walk(candidate => { + if (candidate instanceof Variable) { + if (candidate.resolve(resolver)) { + this._placeholders = undefined; + } + } + return true; + }); + return this; + } + + override appendChild(child: Marker) { + this._placeholders = undefined; + return super.appendChild(child); + } + + override replace(child: Marker, others: Marker[]): void { + this._placeholders = undefined; + return super.replace(child, others); + } + + toTextmateString(): string { + return this.children.reduce((prev, cur) => prev + cur.toTextmateString(), ''); + } + + clone(): TextmateSnippet { + let ret = new TextmateSnippet(); + this._children = this.children.map(child => child.clone()); + return ret; + } + + walk(visitor: (marker: Marker) => boolean): void { + walk(this.children, visitor); + } +} + +export class SnippetParser { + + static escape(value: string): string { + return value.replace(/\$|}|\\/g, '\\$&'); + } + + static guessNeedsClipboard(template: string): boolean { + return /\${?CLIPBOARD/.test(template); + } + + private _scanner: Scanner = new Scanner(); + private _token: Token = { type: TokenType.EOF, pos: 0, len: 0 }; + + text(value: string): string { + return this.parse(value).toString(); + } + + parse(value: string, insertFinalTabstop?: boolean, enforceFinalTabstop?: boolean): TextmateSnippet { + + this._scanner.text(value); + this._token = this._scanner.next(); + + const snippet = new TextmateSnippet(); + while (this._parse(snippet)) { + // nothing + } + + // fill in values for placeholders. the first placeholder of an index + // that has a value defines the value for all placeholders with that index + const placeholderDefaultValues = new Map(); + const incompletePlaceholders: Placeholder[] = []; + let placeholderCount = 0; + snippet.walk(marker => { + if (marker instanceof Placeholder) { + placeholderCount += 1; + if (marker.isFinalTabstop) { + placeholderDefaultValues.set(0, undefined); + } else if (!placeholderDefaultValues.has(marker.index) && marker.children.length > 0) { + placeholderDefaultValues.set(marker.index, marker.children); + } else { + incompletePlaceholders.push(marker); + } + } + return true; + }); + for (const placeholder of incompletePlaceholders) { + const defaultValues = placeholderDefaultValues.get(placeholder.index); + if (defaultValues) { + const clone = new Placeholder(placeholder.index); + clone.transform = placeholder.transform; + for (const child of defaultValues) { + clone.appendChild(child.clone()); + } + snippet.replace(placeholder, [clone]); + } + } + + if (!enforceFinalTabstop) { + enforceFinalTabstop = placeholderCount > 0 && insertFinalTabstop; + } + + if (!placeholderDefaultValues.has(0) && enforceFinalTabstop) { + // the snippet uses placeholders but has no + // final tabstop defined -> insert at the end + snippet.appendChild(new Placeholder(0)); + } + + return snippet; + } + + private _accept(type?: TokenType): boolean; + private _accept(type: TokenType | undefined, value: true): string; + private _accept(type: TokenType, value?: boolean): boolean | string { + if (type === undefined || this._token.type === type) { + let ret = !value ? true : this._scanner.tokenText(this._token); + this._token = this._scanner.next(); + return ret; + } + return false; + } + + private _backTo(token: Token): false { + this._scanner.pos = token.pos + token.len; + this._token = token; + return false; + } + + private _until(type: TokenType): false | string { + const start = this._token; + while (this._token.type !== type) { + if (this._token.type === TokenType.EOF) { + return false; + } else if (this._token.type === TokenType.Backslash) { + const nextToken = this._scanner.next(); + if (nextToken.type !== TokenType.Dollar + && nextToken.type !== TokenType.CurlyClose + && nextToken.type !== TokenType.Backslash) { + return false; + } + } + this._token = this._scanner.next(); + } + const value = this._scanner.value.substring(start.pos, this._token.pos).replace(/\\(\$|}|\\)/g, '$1'); + this._token = this._scanner.next(); + return value; + } + + private _parse(marker: Marker): boolean { + return this._parseComplexVariable(marker) || this._parseAnything(marker); + } + + private _parseChoiceElement(parent: Choice): boolean { + const token = this._token; + const values: string[] = []; + + while (true) { + if (this._token.type === TokenType.Comma || this._token.type === TokenType.Pipe) { + break; + } + let value: string; + if (value = this._accept(TokenType.Backslash, true)) { + // \, \|, or \\ + value = this._accept(TokenType.Comma, true) + || this._accept(TokenType.Pipe, true) + || this._accept(TokenType.Backslash, true) + || value; + } else { + value = this._accept(undefined, true); + } + if (!value) { + // EOF + this._backTo(token); + return false; + } + values.push(value); + } + + if (values.length === 0) { + this._backTo(token); + return false; + } + + parent.appendChild(new Text(values.join(''))); + return true; + } + + // ${foo:}, ${foo} -> variable + private _parseComplexVariable(parent: Marker): boolean { + let name: string; + const token = this._token; + const match = this._accept(TokenType.Dollar) + && this._accept(TokenType.CurlyOpen) + && (name = this._accept(TokenType.VariableName, true)); + + if (!match) { + return this._backTo(token); + } + + const placeholder = new Placeholder(String(name!)); + + if (this._accept(TokenType.Colon)) { + // ${foo:} + while (true) { + + // ...} -> done + if (this._accept(TokenType.CurlyClose)) { + parent.appendChild(placeholder); + return true; + } + + if (this._parse(placeholder)) { + continue; + } + + // fallback + parent.appendChild(new Text('${' + name! + ':')); + placeholder.children.forEach(parent.appendChild, parent); + return true; + } + + } else if (this._accept(TokenType.Pipe)) { + const choice = new Choice(); + + while (true) { + if (this._parseChoiceElement(choice)) { + + if (this._accept(TokenType.Comma)) { + // opt, -> more + continue; + } + + if (this._accept(TokenType.Pipe)) { + placeholder.appendChild(choice); + if (this._accept(TokenType.CurlyClose)) { + // ..|} -> done + parent.appendChild(placeholder); + return true; + } + } + } + + this._backTo(token); + return false; + } + + } else if (this._accept(TokenType.Forwardslash)) { + // ${foo///} + if (this._parseTransform(placeholder)) { + parent.appendChild(placeholder); + return true; + } + + this._backTo(token); + return false; + + } else if (this._accept(TokenType.CurlyClose)) { + // ${foo} + parent.appendChild(placeholder); + return true; + + } else { + // ${foo <- missing curly or colon + return this._backTo(token); + } + } + + private _parseTransform(parent: TransformableMarker): boolean { + // ...//} + + let transform = new Transform(); + let regexValue = ''; + let regexOptions = ''; + + // (1) /regex + while (true) { + if (this._accept(TokenType.Forwardslash)) { + break; + } + + let escaped: string; + if (escaped = this._accept(TokenType.Backslash, true)) { + escaped = this._accept(TokenType.Forwardslash, true) || escaped; + regexValue += escaped; + continue; + } + + if (this._token.type !== TokenType.EOF) { + regexValue += this._accept(undefined, true); + continue; + } + return false; + } + + // (2) /format + while (true) { + if (this._accept(TokenType.Forwardslash)) { + break; + } + + let escaped: string; + if (escaped = this._accept(TokenType.Backslash, true)) { + escaped = this._accept(TokenType.Backslash, true) || this._accept(TokenType.Forwardslash, true) || escaped; + transform.appendChild(new Text(escaped)); + continue; + } + + if (this._parseFormatString(transform) || this._parseAnything(transform)) { + continue; + } + return false; + } + + // (3) /option + while (true) { + if (this._accept(TokenType.CurlyClose)) { + break; + } + if (this._token.type !== TokenType.EOF) { + regexOptions += this._accept(undefined, true); + continue; + } + return false; + } + + try { + transform.regexp = new RegExp(regexValue, regexOptions); + } catch (e) { + // invalid regexp + return false; + } + + parent.transform = transform; + return true; + } + + private _parseFormatString(parent: Transform): boolean { + + const token = this._token; + if (!this._accept(TokenType.Dollar)) { + return false; + } + + let complex = false; + if (this._accept(TokenType.CurlyOpen)) { + complex = true; + } + + let index = this._accept(TokenType.Int, true); + + if (!index) { + this._backTo(token); + return false; + + } else if (!complex) { + // $1 + parent.appendChild(new FormatString(Number(index))); + return true; + + } else if (this._accept(TokenType.CurlyClose)) { + // ${1} + parent.appendChild(new FormatString(Number(index))); + return true; + + } else if (!this._accept(TokenType.Colon)) { + this._backTo(token); + return false; + } + + if (this._accept(TokenType.Forwardslash)) { + // ${1:/upcase} + let shorthand = this._accept(TokenType.VariableName, true); + if (!shorthand || !this._accept(TokenType.CurlyClose)) { + this._backTo(token); + return false; + } else { + parent.appendChild(new FormatString(Number(index), shorthand)); + return true; + } + + } else if (this._accept(TokenType.Plus)) { + // ${1:+} + let ifValue = this._until(TokenType.CurlyClose); + if (ifValue) { + parent.appendChild(new FormatString(Number(index), undefined, ifValue, undefined)); + return true; + } + + } else if (this._accept(TokenType.Dash)) { + // ${2:-} + let elseValue = this._until(TokenType.CurlyClose); + if (elseValue) { + parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue)); + return true; + } + + } else if (this._accept(TokenType.QuestionMark)) { + // ${2:?:} + let ifValue = this._until(TokenType.Colon); + if (ifValue) { + let elseValue = this._until(TokenType.CurlyClose); + if (elseValue) { + parent.appendChild(new FormatString(Number(index), undefined, ifValue, elseValue)); + return true; + } + } + + } else { + // ${1:} + let elseValue = this._until(TokenType.CurlyClose); + if (elseValue) { + parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue)); + return true; + } + } + + this._backTo(token); + return false; + } + + private _parseAnything(marker: Marker): boolean { + if (this._token.type !== TokenType.EOF) { + marker.appendChild(new Text(this._scanner.tokenText(this._token))); + this._accept(undefined); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/helpers/isValidFile.ts b/src/helpers/isValidFile.ts index 80ebdf4c..a7ccf599 100644 --- a/src/helpers/isValidFile.ts +++ b/src/helpers/isValidFile.ts @@ -1,11 +1,11 @@ import { DEFAULT_FILE_TYPES } from './../constants/DefaultFileTypes'; import { Settings } from "."; -import { SETTINGS_CONTENT_SUPPORTED_FILETYPES } from "../constants"; +import { SETTING_CONTENT_SUPPORTED_FILETYPES } from "../constants"; import { extname } from 'path'; export const isValidFile = (fileName: string) => { - let supportedFiles = Settings.get(SETTINGS_CONTENT_SUPPORTED_FILETYPES) || DEFAULT_FILE_TYPES; + let supportedFiles = Settings.get(SETTING_CONTENT_SUPPORTED_FILETYPES) || DEFAULT_FILE_TYPES; supportedFiles = supportedFiles.map(f => f.startsWith(`.`) ? f : `.${f}`); // Get the extension of the file path diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts index eeea4fe8..3aef855d 100644 --- a/src/listeners/dashboard/PagesListener.ts +++ b/src/listeners/dashboard/PagesListener.ts @@ -4,7 +4,7 @@ import { basename, dirname, join } from "path"; import { commands, FileSystemWatcher, RelativePattern, Uri, workspace } from "vscode"; import { Dashboard } from "../../commands/Dashboard"; import { Folders } from "../../commands/Folders"; -import { COMMAND_NAME, DefaultFields, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants"; +import { COMMAND_NAME, DefaultFields, SETTING_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants"; import { DashboardCommand } from "../../dashboardWebView/DashboardCommand"; import { DashboardMessage } from "../../dashboardWebView/DashboardMessage"; import { Page } from "../../dashboardWebView/models"; @@ -140,7 +140,7 @@ export class PagesListener extends BaseListener { const wsFolder = Folders.getWorkspaceFolder(); const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description; const dateField = Settings.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate; - const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER); + const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER); const page: Page = { ...article.data, diff --git a/src/listeners/dashboard/SettingsListener.ts b/src/listeners/dashboard/SettingsListener.ts index 18be3859..806e4585 100644 --- a/src/listeners/dashboard/SettingsListener.ts +++ b/src/listeners/dashboard/SettingsListener.ts @@ -1,4 +1,4 @@ -import { SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_FRAMEWORK_ID } from "../../constants"; +import { SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID } from "../../constants"; import { DashboardCommand } from "../../dashboardWebView/DashboardCommand"; import { DashboardMessage } from "../../dashboardWebView/DashboardMessage"; import { DashboardSettings, Settings } from "../../helpers"; @@ -54,15 +54,15 @@ export class SettingsListener extends BaseListener { * @param frameworkId */ private static setFramework(frameworkId: string | null) { - Settings.update(SETTINGS_FRAMEWORK_ID, frameworkId, true); + Settings.update(SETTING_FRAMEWORK_ID, frameworkId, true); if (frameworkId) { const allFrameworks = FrameworkDetector.getAll(); const framework = allFrameworks.find((f: Framework) => f.name === frameworkId); if (framework) { - Settings.update(SETTINGS_CONTENT_STATIC_FOLDER, framework.static, true); + Settings.update(SETTING_CONTENT_STATIC_FOLDER, framework.static, true); } else { - Settings.update(SETTINGS_CONTENT_STATIC_FOLDER, "", true); + Settings.update(SETTING_CONTENT_STATIC_FOLDER, "", true); } } } diff --git a/src/listeners/dashboard/SnippetListener.ts b/src/listeners/dashboard/SnippetListener.ts new file mode 100644 index 00000000..44940330 --- /dev/null +++ b/src/listeners/dashboard/SnippetListener.ts @@ -0,0 +1,88 @@ +import { EditorHelper } from "@estruyf/vscode"; +import { Position, window } from "vscode"; +import { Dashboard } from "../../commands/Dashboard"; +import { SETTING_CONTENT_SNIPPETS } from "../../constants"; +import { DashboardMessage } from "../../dashboardWebView/DashboardMessage"; +import { Notifications, Settings } from "../../helpers"; +import { BaseListener } from "./BaseListener"; + + +export class SnippetListener extends BaseListener { + + public static process(msg: { command: DashboardMessage, data: any }) { + super.process(msg); + + switch(msg.command) { + case DashboardMessage.addSnippet: + this.addSnippet(msg.data); + break; + case DashboardMessage.updateSnippet: + this.updateSnippet(msg.data); + break; + case DashboardMessage.insertSnippet: + this.insertSnippet(msg.data); + break; + } + } + + private static async addSnippet(data: any) { + const { title, description, body } = data; + + if (!title || !body) { + Notifications.warning("Snippet missing title or body"); + return; + } + + const snippets = Settings.get(SETTING_CONTENT_SNIPPETS); + if (snippets && snippets[title]) { + Notifications.warning("Snippet with the same title already exists"); + return; + } + + const snippetLines = body.split("\n"); + + snippets[title] = { + description, + body: snippetLines.length === 1 ? snippetLines[0] : snippetLines + }; + + Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true); + } + + private static async updateSnippet(data: any) { + const { snippets } = data; + + if (!snippets) { + Notifications.warning("No snippets to update"); + return; + } + + Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true); + } + + private static async insertSnippet(data: any) { + const { file, snippet } = data; + + if (!file || !snippet) { + return; + } + + await EditorHelper.showFile(data.file); + Dashboard.resetViewData(); + + const editor = window.activeTextEditor; + const position = editor?.selection?.active; + if (!position) { + return; + } + + const selection = editor?.selection; + await editor?.edit(builder => { + if (selection !== undefined) { + builder.replace(selection, snippet); + } else { + builder.insert(position, snippet); + } + }); + } +} \ No newline at end of file diff --git a/src/listeners/dashboard/index.ts b/src/listeners/dashboard/index.ts index ab3dbdfa..dcef562a 100644 --- a/src/listeners/dashboard/index.ts +++ b/src/listeners/dashboard/index.ts @@ -1,7 +1,9 @@ +export * from './BaseListener'; export * from './DashboardListener'; export * from './DataListener'; export * from './ExtensionListener'; export * from './MediaListener'; export * from './PagesListener'; export * from './SettingsListener'; +export * from './SnippetListener'; export * from './TelemetryListener'; diff --git a/src/listeners/panel/SettingsListener.ts b/src/listeners/panel/SettingsListener.ts index 9bb9f414..416f0043 100644 --- a/src/listeners/panel/SettingsListener.ts +++ b/src/listeners/panel/SettingsListener.ts @@ -1,5 +1,5 @@ import { commands, workspace } from "vscode"; -import { EXTENSION_BETA_ID, EXTENSION_ID, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTINGS_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_PREVIEW_HOST } from "../../constants"; +import { EXTENSION_BETA_ID, EXTENSION_ID, SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_PREVIEW_HOST } from "../../constants"; import { Extension, Settings } from "../../helpers"; import { PanelSettings } from "../../helpers/PanelSettings"; import { Command } from "../../panelWebView/Command"; @@ -30,13 +30,13 @@ export class SettingsListener extends BaseListener { this.updateSetting(SETTING_AUTO_UPDATE_DATE, msg.data || false); break; case CommandToCode.updateFmHighlight: - this.updateSetting(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, (msg.data !== null && msg.data !== undefined) ? msg.data : false); + this.updateSetting(SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, (msg.data !== null && msg.data !== undefined) ? msg.data : false); break; case CommandToCode.updatePreviewUrl: this.updateSetting(SETTING_PREVIEW_HOST, msg.data || ""); break; case CommandToCode.updateStartCommand: - this.updateSetting(SETTINGS_FRAMEWORK_START, msg.data || ""); + this.updateSetting(SETTING_FRAMEWORK_START, msg.data || ""); break; } } diff --git a/src/models/DashboardData.ts b/src/models/DashboardData.ts index df0f8f93..2b60696a 100644 --- a/src/models/DashboardData.ts +++ b/src/models/DashboardData.ts @@ -1,4 +1,6 @@ +import { NavigationType } from '../dashboardWebView/models'; + export interface DashboardData { - type: "contents" | "media" | "data"; + type: NavigationType; data?: any; } \ No newline at end of file diff --git a/src/models/SnippetField.ts b/src/models/SnippetField.ts new file mode 100644 index 00000000..53f93ad5 --- /dev/null +++ b/src/models/SnippetField.ts @@ -0,0 +1,7 @@ +export interface SnippetField { + name: string; + value: string; + type: 'text' | 'textarea' | 'select'; + tmString: string; + options?: string[]; +} \ No newline at end of file diff --git a/src/models/index.ts b/src/models/index.ts index 680f6091..12b49e42 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -10,6 +10,7 @@ export * from './DraftField'; export * from './Framework'; export * from './MediaPaths'; export * from './PanelSettings'; +export * from './SnippetField'; export * from './SortOrder'; export * from './SortType'; export * from './SortingSetting'; diff --git a/src/providers/MarkdownFoldingProvider.ts b/src/providers/MarkdownFoldingProvider.ts index fbf4235a..35a33054 100644 --- a/src/providers/MarkdownFoldingProvider.ts +++ b/src/providers/MarkdownFoldingProvider.ts @@ -1,7 +1,7 @@ import { ArticleHelper } from '../helpers'; import { languages, TextEditorDecorationType } from 'vscode'; import { CancellationToken, FoldingContext, FoldingRange, FoldingRangeKind, FoldingRangeProvider, Range, TextDocument, window, Position } from 'vscode'; -import { SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTINGS_CONTENT_SUPPORTED_FILETYPES, SETTING_FRONTMATTER_TYPE } from '../constants'; +import { SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FRONTMATTER_TYPE } from '../constants'; import { Settings } from '../helpers'; import { FrontMatterDecorationProvider } from './FrontMatterDecorationProvider'; @@ -12,7 +12,7 @@ export class MarkdownFoldingProvider implements FoldingRangeProvider { private static decType: TextEditorDecorationType | null = null; public static register() { - const supportedFiles = Settings.get(SETTINGS_CONTENT_SUPPORTED_FILETYPES); + const supportedFiles = Settings.get(SETTING_CONTENT_SUPPORTED_FILETYPES); languages.registerFoldingRangeProvider({ language: 'markdown', scheme: 'file' }, new MarkdownFoldingProvider()); @@ -41,7 +41,7 @@ export class MarkdownFoldingProvider implements FoldingRangeProvider { const isSupported = ArticleHelper.isMarkdownFile(activeDoc); if (isSupported) { - const fmHighlight = Settings.get(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT); + const fmHighlight = Settings.get(SETTING_CONTENT_FRONTMATTER_HIGHLIGHT); const range = this.getFrontMatterRange();
Select the image you want to use for your article.
You can also drag and drop images from your desktop and select that once uploaded.
Select the media file to add to your content.
You can also drag and drop images from your desktop and select them once uploaded.
{snippet.description}
+ {snippetBody} +
Select the snippet to add to your content.
No snippets found