Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save KevinBatdorf/5742e4df2682f00f69fc2d1e542ad3cd to your computer and use it in GitHub Desktop.
Save KevinBatdorf/5742e4df2682f00f69fc2d1e542ad3cd to your computer and use it in GitHub Desktop.
Adds a live preview window for support on WP.org
// ==UserScript==
// @name WP Support Live Preview
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://wordpress.org/support/*
// @grant none
// ==/UserScript==
(function($) {
'use strict';
// TODO: Handle @username
// TODO: extract all styles to here instead of relying on BBPress
document.head.insertAdjacentHTML("beforeend", `
<style>
.wp-editor-container {
border: 0 !important;
}
#qt_bbp_reply_content_toolbar,
#qt_bbp_topic_content_toolbar {
border-left: 1px solid #ccc !important;
border-right: 1px solid #ccc !important;
}
#bbp-live-preview ul li {
list-style-type: square !important;
margin-left: 10px !important;
}
#bbp-live-preview code {
font-size: 14.4px;
}
</style>
`)
const setActiveState = function(element) {
element.style.color = 'black'
}
const setInactiveState = function(element) {
element.style.color = '#999999'
}
const toHtmlEntities = function(string, whiteList = []) {
let tagOpen = false
return string.replace(/[\u00A0-\u9999<>\&]/gim, function(match, offset, string) {
const itemFound = whiteList.some(item => {
if (tagOpen) {
//console.log('tag closed')
tagOpen = false
return true
}
if (string.substring(offset + 1, offset + 1 + item.length) == item) {
//console.log('tag opened', string.substring(offset + 1, offset + 1 + item.length), item)
tagOpen = true
return true
}
if (string.substring(offset + 1, offset + 2 + item.length) == '/' + item) {
//console.log('tag opened', string.substring(offset + 1, offset + 2 + item.length), item)
tagOpen = true
return true
}
if (string.substring(offset - item.length, offset) == item) return true
return false
})
return itemFound ? match : '&#' + match.charCodeAt(0) + ';';
})
}
const formatUrlAndEmail = function(string) {
// https://stackoverflow.com/questions/22539616/how-to-wrap-urls-with-anchor-tag-and-ignore-urls-that-already-have-anchor-tags-i
const url = /(http|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?/gi;
const mailto = /(mailto:[a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/gi;
return $('<div>').html(string).contents().each(function () {
console.log($(this))
if (this.nodeType === 3) { // if node is a textNode
$(this).replaceWith(function () {
return this.nodeValue.replace(url, function (m) {
return '<a href="' + m + '" rel="nofollow">' + m + '</a>';
}).replace(mailto, function(m) {
return '<a href="' + m + '" rel="nofollow" target="_blank">' + m + '</a>';
});
})
}
}).end().html();
}
const Previewer = function(wpEditor) {
this.domElement = document.createElement('div')
this.domElement.id = 'bbp-live-preview'
// Add styles
this.domElement.style.border = '1px solid #e5e5e5'
//this.domElement.style.borderTop = '0'
this.domElement.style.background = 'white'
this.domElement.style.padding = '0 8px'
this.domElement.style.margin = '0'
this.domElement.style.minHeight = '267px'
this.domElement.style.fontSize = '0.8rem'
this.domElement.style.display = 'none'
this.domElement.style.lineHeight = '21.6px'
// Add class to inherit some styling
this.domElement.classList.add('bbp-replies')
this.domElement.classList.add('bbp-reply-content')
// Add container below textarea
wpEditor.parentNode.appendChild(this.domElement)
// Set initial value
this.value = 'Nothing to preview'
this.domElement.innerHTML = this.value
// Create buttons
const buttons = document.createElement('div')
buttons.style.background = '#f0f0f0'
buttons.style.padding = '0.5rem 0 0'
buttons.style.border = '1px solid #ccc'
// Write button
const writeButton = document.createElement('button')
writeButton.innerText = "Write"
writeButton.style.background = '#f0f0f0'
writeButton.style.border = '0'
writeButton.style.padding = '0'
writeButton.style.margin = '0 1rem 5px'
setActiveState(writeButton)
buttons.appendChild(writeButton)
const previewButton = document.createElement('button')
previewButton.innerText = 'Preview'
previewButton.style.background = '#f0f0f0'
previewButton.style.border = '0'
previewButton.style.padding = '0'
previewButton.style.margin = '0 0 5px'
setInactiveState(previewButton)
buttons.appendChild(previewButton)
// Add buttons
wpEditor.parentNode.parentNode.insertBefore(buttons, wpEditor.parentNode)
wpEditor.parentNode.style.position = 'relative'
// Add button events
writeButton.addEventListener('click', event => {
event.preventDefault()
setInactiveState(previewButton)
setActiveState(writeButton)
this.domElement.style.display = 'none'
wpEditor.style.display = 'block'
document.querySelector('.quicktags-toolbar').style.display = 'block'
})
previewButton.addEventListener('click', event => {
event.preventDefault()
Preview.update(wpEditor.value)
setInactiveState(writeButton)
setActiveState(previewButton)
this.domElement.style.display = 'block'
wpEditor.style.display = 'none'
document.querySelector('.quicktags-toolbar').style.display = 'none'
})
}
Previewer.prototype.update = function(markup) {
if (markup === this.value) return
this.value = markup
// Formatting
let formatted = formatUrlAndEmail(this.value)
// For code and pre, get all the matches first. This preserved whitespace
let matches = formatted.matchAll(/[\r\n]\`\s*([^\`]*)\s*\`/g)
for (const match of matches) {
formatted = formatted.replace(match[0], '<pre><code>{{MATCH}}</code></pre>')
formatted = formatted.replace('{{MATCH}}', match[1])
}
matches = formatted.matchAll(/\`\s*([^\`]*)\s*\`/g);
for (const match of matches) {
formatted = formatted.replace(match[0], '<code>{{MATCH}}</code>')
formatted = formatted.replace('{{MATCH}}', match[1])
}
formatted = toHtmlEntities(formatted, ['pre', 'ul', 'li'])
formatted = formatted.replace(/(?:\r\n|\r|\n)/g, '<br>')
this.domElement.innerHTML = this.value.length ? formatted : 'Nothing to preview'
// Wrap all unwrapped tags with a paragraph tag (uses jQuery)
$(this.domElement).contents().filter(function() {
if (!JSON.stringify(this).length) return false
return 3 === this.nodeType
}).wrap('<p style="margin:1em 0"></p>')
$(this.domElement).children('a').wrap('<p style="margin:1em 0"></p>')
$(this.domElement).find('> *').not('pre').find('br').remove()
$(this.domElement).find('> br').remove()
// Encode everything then put back
this.domElement.innerHTML = toHtmlEntities(this.domElement.innerHTML, ['gt', 'lt'])
this.domElement.innerHTML = this.domElement.innerText
}
const wpEditor = document.querySelector('.bbp-the-content')
const Preview = new Previewer(wpEditor)
})(jQuery);
@KevinBatdorf
Copy link
Author

Some notes.

  1. This isn't perfect nor complete. It doesn't cover images or usernames, for example.
  2. The code needs to be refactored. This was done quickly in just a couple of hours and it works well enough for me. However, I'll probably update it when I encounter issues in my daily use.
  3. There's a demo video https://youtu.be/sWMMqocrlKw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment