Created
June 9, 2019 17:07
-
-
Save Orbifold/d1dbccb20058afb0de31340ddc7a9ff5 to your computer and use it in GitHub Desktop.
Script to publish Quiver markdown notebooks to WordPress.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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