Skip to content

Instantly share code, notes, and snippets.

@Orbifold
Created June 9, 2019 17:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Orbifold/d1dbccb20058afb0de31340ddc7a9ff5 to your computer and use it in GitHub Desktop.
Save Orbifold/d1dbccb20058afb0de31340ddc7a9ff5 to your computer and use it in GitHub Desktop.
Script to publish Quiver markdown notebooks to WordPress.
// You need to export first a notebook from Quiver (https://happenapps.com) to markdown.
// Use the directory path below to publish all the markdown files and the images to a WordPress site.
// Make sure you install lodash, wordpress and fs-extra.
// Set the ur, username and password of WordPress and run `node QuiverPublisher.js'.
// Note that images are deleted and recreated since the overwrite flag of the xmlrpc wp-method does not work.
const wordpress = require('wordpress');
const fs = require('fs-extra');
const _ = require('lodash');
const path = require('path');
/**
* See the Wordpress API here: https://github.com/scottgonzalez/node-wordpress
*/
class Publisher {
constructor() {
this.posts = [];
this.media = [];
this.url = 'http://domain.com/';
this.client = wordpress.createClient({
url: this.url,
username: 'user',
password: 'pass'
});
}
async init() {
return this.refreshServerData();
}
async refreshServerData() {
this.posts = [];
this.media = [];
return new Promise(async (resolve, reject) => {
await Promise.all([
this.loadExistingPosts(),
this.loadExistingMedia(),
]);
resolve();
});
}
async loadExistingPosts() {
return new Promise((resolve, reject) => {
this.client.getPosts((error, posts) => {
this.posts = posts;
resolve();
});
});
}
async loadExistingMedia() {
return new Promise((resolve, reject) => {
this.client.getMediaLibrary((error, media) => {
this.media = media;
resolve();
});
});
}
async postTitleExists(title) {
const titleLower = title.toLowerCase();
return new Promise((resolve, reject) => {
for (let i = 0; i < this.posts.length; i++) {
const p = this.posts[i];
if (p.title.toLowerCase() === titleLower) {
return resolve(p.id);
}
}
resolve(null);
}
);
}
async upsertPost(post, imageDir) {
const id = await this.postTitleExists(post.title);
const self = this;
function dateParts(dDate = new Date()) {
var today = dDate;
var dd = today.getDate();
var mm = today.getMonth() + 1; //January is 0!
var yyyy = today.getFullYear().toString();
if (dd < 10) {
dd = '0' + dd
}
if (mm < 10) {
mm = '0' + mm
}
return [yyyy, mm];
}
function convertMarkdownImagesToHtml() {
let content = post.content;
const images = [];
const rx = /(?:!\[.*?\]\(resources\/.*?\))/g;
const strings = Array.from(content.match(rx) || []);
let name, html;
for (let i = 0; i < strings.length; i++) {
let im = strings[i];
if (im.indexOf('=') > -1) {
const parts = im.split('=');
const first = parts[0];
const dims = parts[1].replace(')', '').split('x');
const width = dims[0].trim();
const height = dims[1].trim();
name = first.replace(/(?:!\[.*?\]\(resources\/)/, '').trim();
const dp = dateParts();
html = `<img src="${self.url}wp-content/uploads/${dp[0]}/${dp[1]}/${name}" style="width:${width}px; height:${height}px;">`;
content = content.replace(im, html);
} else {
name = im.replace(/(?:!\[.*?\]\(resources\/)/, '').replace(')', '');
html = `<img src="${self.url}/wp-content/uploads/$dp[0]}/${dp[1]}/${name}">`;
content = content.replace(im, html);
}
images.push(name);
}
return {
htmlContent: content,
imageNames: images
};
}
async function processImages(imageNames) {
for (let i = 0; i < imageNames.length; i++) {
const im = imageNames[i];
const imPath = path.join(imageDir, im);
await self.upsertImage(im, imPath, post.id);
}
}
return new Promise((resolve, reject) => {
const mdContent = post.content;
const {htmlContent, imageNames} = convertMarkdownImagesToHtml();
post.content = htmlContent;
if (_.isNil(id)) {
this.client.newPost(post, async (error, newId) => {
if (error) {
reject(error);
} else {
await processImages(imageNames);
resolve(newId);
}
});
} else {
this.client.editPost(id, post, async (error) => {
if (error) {
reject(error);
} else {
await processImages(imageNames);
resolve(id);
}
});
}
});
}
async upsertImage(name, imagePath, parent = null) {
if (!fs.existsSync(imagePath)) {
throw new Error(`The image '${imagePath}' does not exist.`);
}
const imId = await this.imageNameExists(name);
const self = this;
return new Promise((resolve, reject) => {
function uploadImage(name, imagePath) {
var file = fs.readFileSync(imagePath);
const imageType = `image/${imagePath.slice(-3)}`;
const im = {
name: name,
type: imageType,
overwrite: 'true',
parent: parent,
bits: file
};
self.client.uploadFile(im, (error, file) => {
if (error) {
reject(error);
} else {
resolve(file.attachmentId);
}
})
}
if (_.isNil(imId)) { // does not exist
uploadImage(name, imagePath);
} else {
// delete first
this.client.deletePost(imId, (error) => {
if (error) {
return reject(error)
} else {
uploadImage(name, imagePath);
}
})
}
});
}
async imageNameExists(name) {
const lowerName = name.toLowerCase();
return new Promise((resolve, reject) => {
for (let i = 0; i < this.media.length; i++) {
const item = this.media[i];
if (item.title.toLowerCase() === lowerName) {
return resolve(item.attachmentId);
}
}
resolve(null);
});
}
getRegexMatches(string, regex, groupNumber) {
groupNumber || (groupNumber = 1); // default to the first capturing group
const matches = [];
let match;
while (match = regex.exec(string)) {
matches.push(match[groupNumber].trim());
}
return matches;
}
async getMarkdownImages(content) {
const rx = /(?:!\[.*?\]\(resources\/(.*?)(\)|=))/g;
return Promise.resolve(this.getRegexMatches(content, rx, 1));
}
async getDirPosts(sourcePath) {
const dirPosts = [];
const sources = fs.readdirSync(sourcePath);
return new Promise(function (resolve, reject) {
for (let i = 0; i < sources.length; i++) {
const p = path.join(sourceDir, sources[i]);
if (fs.statSync(p).isFile() && p.slice(-3) === '.md') {
const post = {
title: sources[i].replace('.md', ''),
type: 'post',
status: 'publish',
author: '1',
content: fs.readFileSync(p, 'utf8')
};
dirPosts.push(post);
}
}
resolve(dirPosts);
});
}
}
const sourceDir = '/Users/yourname/yourdir';
const pub = new Publisher();
pub.init().then(async () => {
const articles = await pub.getDirPosts(sourceDir);
if (articles.length > 0) {
for (let i = 0; i < articles.length; i++) {
const post = articles[i];
await pub.upsertPost(post, path.join(sourceDir, 'resources'));
console.log(post.title);
}
await pub.refreshServerData();
console.log('All done.')
} else {
console.log('No articles found in given dir.')
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment