diff --git a/README.md b/README.md index 96df714..08e229c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -## Obsidian & Ghost +# Obsidian & Ghost A simple plugin for Obsidian to publish to Ghost site with a single click. ## How to use + - Create a custom integration follow this [link](https://ghost.org/integrations/custom-integrations/). You would need an **Admin API Key** and **API URL**. - Once you install the plugin, you would find an option "Obsidian Ghost Publish" under "Plugin options" and copy-paste it in the textbox. - That's it! you now are able to publish by click on the ghost icon on the left hand or use the command pallete. -### How to use + +### How to run on dev - Clone this repo. - `npm i` or `yarn` to install dependencies @@ -16,7 +18,6 @@ A simple plugin for Obsidian to publish to Ghost site with a single click. - Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`. - ### Issues & Requests - For feature requests, please take use of Discussions. diff --git a/manifest.json b/manifest.json index 8b4a965..71e0fac 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "obsidian-ghost-publish", "name": "Obsidian Ghost Publish", - "version": "1.0.0", + "version": "1.2.0", "minAppVersion": "0.12.0", "description": "Single click to publish to Ghost", "author": "@jaynguyens ", "authorUrl": "https://nguyens.co", "isDesktopOnly": false -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 692e741..2f53426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "obsidian-ghost-publish", "version": "1.0.0", "license": "MIT", + "dependencies": { + "gray-matter": "^4.0.3" + }, "devDependencies": { "@types/jsonwebtoken": "^8.5.8", "@types/markdown-it": "^12.2.3", @@ -1058,6 +1061,18 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", @@ -1121,6 +1136,17 @@ "node": ">=0.10.0" } }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1308,6 +1334,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1372,6 +1432,14 @@ "dev": true, "peer": true }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1488,6 +1556,14 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1873,6 +1949,18 @@ } ] }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", @@ -1920,6 +2008,11 @@ "node": ">=8" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1933,6 +2026,14 @@ "node": ">=8" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2836,6 +2937,11 @@ "eslint-visitor-keys": "^3.3.0" } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "esquery": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", @@ -2885,6 +2991,14 @@ "dev": true, "peer": true }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3038,6 +3152,36 @@ "slash": "^3.0.0" } }, + "gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "requires": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3087,6 +3231,11 @@ "dev": true, "peer": true }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3186,6 +3335,11 @@ "safe-buffer": "^5.0.1" } }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3462,6 +3616,15 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, + "section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "requires": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + } + }, "semver": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", @@ -3494,6 +3657,11 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3504,6 +3672,11 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=" + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index ddf93d6..4cd0262 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-ghost-publish", - "version": "1.0.0", + "version": "1.2.0", "description": "Obsidian plugin for easy publish to ghost with a single click.", "main": "src/main.js", "scripts": { @@ -23,10 +23,11 @@ "@typescript-eslint/parser": "^5.2.0", "builtin-modules": "^3.2.0", "esbuild": "0.13.12", + "jsonwebtoken": "^8.5.1", + "markdown-it": "^12.3.2", "obsidian": "^0.14.4", "tslib": "2.3.1", "typescript": "4.4.4", - "jsonwebtoken": "^8.5.1", - "markdown-it": "^12.3.2" + "gray-matter": "^4.0.3" } -} \ No newline at end of file +} diff --git a/src/main.ts b/src/main.ts index 8ad4d78..68e7e6f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,8 @@ -import { MarkdownView, Plugin } from 'obsidian'; - -import { DEFAULT_SETTINGS, SettingsProp } from './types/index'; -import { SettingTab } from './settingTab' -import { publishPost } from './methods/publishPost'; - +import { MarkdownView, Plugin } from "obsidian"; +import { DEFAULT_SETTINGS, SettingsProp } from "./types/index"; +import { SettingTab } from "./settingTab"; +import { publishPost } from "./methods/publishPost"; export default class MyPlugin extends Plugin { settings: SettingsProp; @@ -15,29 +13,39 @@ export default class MyPlugin extends Plugin { // 2 ways to publish post: // 1. Click on the ghost icon on the left - this.addRibbonIcon('ghost', 'Publish Ghost', () => { + this.addRibbonIcon("ghost", "Publish Ghost", () => { const view = this.app.workspace.getActiveViewOfType(MarkdownView); - - publishPost(view, this.settings) + // matter + publishPost(view, this.settings); }); // 2. Run the by command + P this.addCommand({ - id: 'publish', - name: 'Publish current document', + id: "publish", + name: "Publish current document", editorCallback: (_, view: MarkdownView) => { - publishPost(view, this.settings) - } + publishPost(view, this.settings); + }, }); // This adds a settings tab so the user can configure various aspects of the plugin this.addSettingTab(new SettingTab(this.app, this)); // When registering intervals, this function will automatically clear the interval when the plugin is disabled. - this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000)); + this.registerInterval( + window.setInterval(() => console.log("setInterval"), 5 * 60 * 1000) + ); } - onunload() { } - async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) } - async saveSettings() { await this.saveData(this.settings) } -} \ No newline at end of file + onunload() {} + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData() + ); + } + async saveSettings() { + await this.saveData(this.settings); + } +} diff --git a/src/methods/publishPost.ts b/src/methods/publishPost.ts index 97d8300..db59ded 100644 --- a/src/methods/publishPost.ts +++ b/src/methods/publishPost.ts @@ -1,67 +1,78 @@ -import { SettingsProp, ViewProp, ContentProp } from './../types/index'; +import { SettingsProp, ViewProp, ContentProp } from "./../types/index"; import { Notice, request } from "obsidian"; -import { sign } from 'jsonwebtoken'; -const MarkdownIt = require("markdown-it") +import { sign } from "jsonwebtoken"; -const md = new MarkdownIt() +const matter = require("gray-matter"); +const MarkdownIt = require("markdown-it"); + +const md = new MarkdownIt(); export const publishPost = async (view: ViewProp, settings: SettingsProp) => { + const version = "v4"; - const content = { - title: view.file.basename, - data: view.data - } + // Admin API key goes here + const key = settings.adminToken; - const version = 'v4' + // Split the key into ID and SECRET + const [id, secret] = key.split(":"); - // Admin API key goes here - const key = settings.adminToken; + // Create the token (including decoding secret) + const token = sign({}, Buffer.from(secret, "hex"), { + keyid: id, + algorithm: "HS256", + expiresIn: "5m", + audience: `/${version}/admin/`, + }); - // Split the key into ID and SECRET - const [id, secret] = key.split(':'); + // get frontmatter + const m = matter(`${view.data}`); - // Create the token (including decoding secret) - const token = sign({}, Buffer.from(secret, 'hex'), { - keyid: id, - algorithm: 'HS256', - expiresIn: '5m', - audience: `/${version}/admin/` - }); + const frontmatter = { + title: m.data.title || view.file.basename, + tags: m.data.tags || undefined, + featured: m.data.featured || false, + status: m.data.status || "draft", + excerpt: m.data.excerpt || undefined, + feature_image: m.data.feature_image || undefined, + }; - const contentPost = (content: ContentProp, settings: SettingsProp) => ({ - "posts": [{ - "title": content.title, - "html": md.render(content.data), - "status": settings.publishStatus - }] - }) + const contentPost = (frontmatter: ContentProp) => ({ + posts: [ + { + ...frontmatter, + html: md.render(m.content), + }, + ], + }); - const body = contentPost(content, settings); + const body = contentPost(frontmatter); + const result = await request({ + url: `${settings.url}/ghost/api/${version}/admin/posts/?source=html`, + method: "POST", + contentType: "application/json", + headers: { + "Access-Control-Allow-Origin": "app://obsidian.md", + "Access-Control-Allow-Methods": "POST", + "Content-Type": "application/json;charset=utf-8", + Authorization: `Ghost ${token}`, + }, + body: JSON.stringify(body), + }); - const result = await request({ - url: `${settings.url}/ghost/api/${version}/admin/posts/?source=html`, - method: "POST", - contentType: "application/json", - headers: { - 'Access-Control-Allow-Origin': 'app://obsidian.md', - 'Access-Control-Allow-Methods': 'POST', - 'Content-Type': 'application/json;charset=utf-8', - 'Authorization': `Ghost ${token}` - }, - body: JSON.stringify(body) - }) + const json = JSON.parse(result); - const json = JSON.parse(result) + if (json?.posts) { + new Notice( + `"${json?.posts?.[0]?.title}" has been ${json?.posts?.[0]?.status} successful!` + ); + } else { + new Notice(`${json.errors[0].context || json.errors[0].message}`); + new Notice( + `${json.errors[0]?.details[0].message} - ${json.errors[0]?.details[0].params.allowedValues}` + ); + } - if (json?.errors) { - new Notice(`${json.errors[0].type}! ${json.errors[0].message}`) - } - - if (json?.posts) { - new Notice(`"${json?.posts?.[0]?.title}" has been ${json?.posts?.[0]?.status} successful!`) - } - - return json -} \ No newline at end of file + return json; +}; diff --git a/src/settingTab/index.ts b/src/settingTab/index.ts index a398d46..1e18082 100644 --- a/src/settingTab/index.ts +++ b/src/settingTab/index.ts @@ -1,51 +1,40 @@ -import { App, PluginSettingTab, Setting } from 'obsidian'; -import MyPlugin from 'src/main'; +import { App, PluginSettingTab, Setting } from "obsidian"; +import MyPlugin from "src/main"; export class SettingTab extends PluginSettingTab { - plugin: MyPlugin; + plugin: MyPlugin; - constructor(app: App, plugin: MyPlugin) { - super(app, plugin); - this.plugin = plugin; - } + constructor(app: App, plugin: MyPlugin) { + super(app, plugin); + this.plugin = plugin; + } - display(): void { - const { containerEl } = this; + display(): void { + const { containerEl } = this; - containerEl.empty(); - containerEl.createEl('h2', { text: 'Obsidian Ghost Integration' }); + containerEl.empty(); + containerEl.createEl("h2", { text: "Obsidian Ghost Integration" }); - new Setting(containerEl) - .setName('API URL') - .addText(text => text - .setPlaceholder('nguyens.co') - .setValue(this.plugin.settings.url) - .onChange(async (value) => { - console.log('Blog URL: ' + value); - this.plugin.settings.url = value; - await this.plugin.saveSettings(); - })); + new Setting(containerEl).setName("API URL").addText((text) => + text + .setPlaceholder("nguyens.co") + .setValue(this.plugin.settings.url) + .onChange(async (value) => { + console.log("Blog URL: " + value); + this.plugin.settings.url = value; + await this.plugin.saveSettings(); + }) + ); - new Setting(containerEl) - .setName('Admin API Key') - .addText(text => text - .setPlaceholder('6251555c94ca6') - .setValue(this.plugin.settings.adminToken) - .onChange(async (value) => { - console.log('admin api key: ' + value); - this.plugin.settings.adminToken = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Post publish status') - .addDropdown(dropdown => dropdown - .addOption('draft', 'Draft') - .addOption('published', 'Publish') - .setValue(this.plugin.settings.publishStatus) - .onChange(async (value) => { - this.plugin.settings.publishStatus = value; - await this.plugin.saveSettings(); - })) - } + new Setting(containerEl).setName("Admin API Key").addText((text) => + text + .setPlaceholder("6251555c94ca6") + .setValue(this.plugin.settings.adminToken) + .onChange(async (value) => { + console.log("admin api key: " + value); + this.plugin.settings.adminToken = value; + await this.plugin.saveSettings(); + }) + ); + } } diff --git a/src/types/index.ts b/src/types/index.ts index 3dd6d09..c71d54f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,24 +1,25 @@ export interface SettingsProp { - url: string, - adminToken: string, - publishStatus: string + url: string; + adminToken: string; } export const DEFAULT_SETTINGS: SettingsProp = { - url: '', - adminToken: '', - publishStatus: 'draft' -} - + url: "", + adminToken: "", +}; export interface ViewProp { - file: { - basename: string - }, - data: string + file: { + basename: string; + }; + data: string; } export interface ContentProp { - title: string, - data: string + title: string; + tags: string[]; + featured: boolean; + status: "published" | "draft"; + excerpt: string; + feature_image: string; }