How to use Markdown in WordPress and preserve spaces in code blocks

It’s easy to add Markdown support to WordPress. However, it does have a nefarious quirk. In fact, even if you can input Markdown text from the Text panel, and it gets rendered just fine in the blog, the harsh truth is that, as soon as you inadvertently switch to the Visual panel, all the white space in your code blocks gets wiped out, losing all the indentation you had put in.

This looks like a typical reason for writing a plugin to fix that.

Use a shell plugin

Given that I often need small WordPress adjustments like this one, in the past I developed one ring to rule them all: Custom Stuff.

<?php
/*
Plugin Name: Custom Stuff
Plugin URI: http://andowebsit.es/
Description: Custom stuff for my blog.
Author: Andrea Ercolino
Author URI: http://andowebsit.es/about
Version: 1.0
*/

function custom_stuff_header() {
    require 'header.php';
}
add_action( 'wp_head',  'custom_stuff_header', 10, 0 );


function custom_stuff_footer() {
    require 'footer.php';
}
add_action( 'wp_footer',  'custom_stuff_footer', 10, 0 );

 (file wordpress/wp-content/plugins/custom-stuff/custom-stuff.php)

which is just a simple PHP script that basically declares itself as a WordPress plugin and all it does is to require other PHP scripts where the real action takes place.

Design a usable solution

What we need is a way to make WordPress aware that a post is written in Markdown thus no Visual editor will ever be allowed for it.

My solution is based on the user_can_richedit hook and the Shortcode API. It works like this:

  1. At the start of a Markdown text, you add the shortcode.
  2. In editing mode, the hook handler determines whether or not a post begins with that shortcode. If it does, a false is returned, thus effectively forbidding the Vsual editor.
  3. In reading mode, that shortcode is just replaced by an empty string.

Code

<?php

function custom_stuff_no_richedit_if_content_asks( $default ) {
    global $post;
    $content = $post ? $post->post_content : '';
    $start = '';
    if ( substr($content, 0, strlen($start)) === $start ) {
        return false;
    }
    return $default;
}
add_filter( 'user_can_richedit', 'custom_stuff_no_richedit_if_content_asks', 10, 1 );

function custom_stuff_nothing() {
    return '';
}
add_shortcode( 'no_richedit', 'custom_stuff_nothing', 10, 0 );

 (file wordpress/wp-content/plugins/custom-stuff/no-richedit.php)

<?php
/*
Plugin Name: Custom Stuff
Plugin URI: http://andowebsit.es/
Description: Custom stuff for my blog.
Author: Andrea Ercolino
Author URI: http://andowebsit.es/about
Version: 1.1
*/

function custom_stuff_header() {
    require 'header.php';
}
add_action( 'wp_head',  'custom_stuff_header', 10, 0 );


function custom_stuff_footer() {
    require 'footer.php';
}
add_action( 'wp_footer',  'custom_stuff_footer', 10, 0 );

require 'no-richedit.php';

 (file wordpress/wp-content/plugins/custom-stuff/custom-stuff.php)

Example

This is how WordPress looks like when the shortcode is used. Notice that there is no Visual editor panel

This is how WordPress looks like when the shortcode is not used.

Only one thing to remember

There is only one thing to remember then. When adding a post, before introducing any relevant Markdown, switch to the Text panel, add the [no-richedit] shortcode and save a draft. Then the Visual editor won’t appear anymore for that post. (until you remove the shortcode and save again)

How to cache results in TypeScript

This is an example of a caching mechanism that I added to an Angular 2+ page.

Problem

At a certain point, during the development of a visual calculator, I noticed that the computation of results corresponding to moving sliders on the page had become much slower than before.

Solution

To mitigate the problem I introduced a caching mechanism based on:

  1. selecting slow methods, whose results we’re going to cache;
  2. wrapping those methods into a check:
  • if there is a cached result, use it
  • otherwise, compute it now, save it, then use it;
  1. deleting cached results as soon as they become stale.

A nice thing about this cache mechanism is that it’s standard.

  • We don’t need to change the code of the app.
  • A new cached result is computed only when unavailable.
  • Cached results become unavailable after certain events.

A nice thing of my implementation is that it automatically computes which cached values to invalidate, based on their dependencies lists.

  • This means that we only have to define the dependencies lists, which is very easy. Just look at the method’s definition and list all of its inputs (params, function calls, and contextual values).
escalivada(): number {
  return (this.escabeche() - this.gazpacho.value) / this.paella();
}

Dependencies: escabeche, gazpacho, paella.

Code

import * as _ from 'lodash';

export class CachedResults {

    private cache: any = {};

    private invalidations: any;

    static getPathsOfTheTransitiveClosure(edges) {
        // TODO check for dependency loops and throw an error
        const result = [];
        _.forEach(edges, edge => {
            let [from,] = edge;
            let path = [from];
            let found = edge;
            while (found) {
                let [, to] = found;
                path.push(to);
                found = edges.find(([x,]) => x === to);
            }
            result.push(path);
        });
        return result;
    }

    constructor(
        private definitions: any,
    ) {
        this.invalidations = this.getInvalidations();
    }

    use(methodName, fn) {
        if (!this.cache[methodName]) {
            this.cache[methodName] = fn(...args);
        }
        return this.cache[methodName];
    }

    invalidate(changed) {
        const invalidCache = this.invalidations[changed];
        invalidCache.forEach(x => { delete this.cache[x] });
    }

    private getInvalidations() {
        const arcs = this.getArcsFromInputToComputed();
        const paths = CachedResults.getPathsOfTheTransitiveClosure(arcs);
        return this.getPathsFromRoots(paths);
    }

    private getComputedValues() {
        return Object.keys(this.definitions);
    }

    private getArcsFromInputToComputed() {
        const result = [];
        _.forEach(this.definitions, (inputs, computed) => {
            _.forEach(inputs, input => {
                result.push([input, computed]);
            });
        });
        return result;
    }

    private getRootValues() {
        const allInputs = [];
        _.forEach(this.definitions, inputs => {
            allInputs.push(...inputs);
        });
        const computed = this.getComputedValues();
        const origins = _.difference(allInputs, computed);
        return _.uniq(origins);
    }

    private getPathsFromRoots(paths) {
        const roots = this.getRootValues();
        const result = {};
        roots.forEach(origin => {
            const path = paths.find(([x,]) => x === origin);
            const [, ...rest] = path;
            result[origin] = rest;
        });
        return result;
    }

}

Usage

import { CachedResults } from '...';
//...
cache: CachedResults;
//...
constructor() {
    this.cache = new CachedResults({
        paella: 'arrozNegre'.split(' '),
        escabeche: 'paella arrozCubana'.split(' '),
        arrozCubana: 'gachas arrozNegre gazpacho'.split(' '),
    });
}
//...
escalivada(): number {
    return this.cache.use('escalivada', () => {
        return (this.escabeche() - this.gazpacho.value) / this.paella();
    });
}
//...
<input id="arrozNegre" max="30" min="5" step="1" type="range" />