Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Add Cacheable Gists to Your WordPress Site
.gist {
color: #e4322e;
}
.gistdiv {
padding: 0;
margin: 0;
}
.gist .gist-file {
border: none;
font-family: 'Inconsolata', "Bitstream Vera Sans Mono", monospace;
margin-bottom: 1em;
}
.gist .gist-file .gist-meta {
overflow: hidden;
font-size: 14px;
font-weight: 300;
padding: .5em;
color: #666;
font-family: 'Open Sans', sans-serif;
background-color: transparent;
}
.gist .gist-file .gist-meta a {
color: #2FC2EF;
}
.gist .gist-file .gist-meta a:visited {
color: #2FC2EF;
}
.gist .gist-file .gist-data {
overflow: auto;
word-wrap: normal;
background-color: #F8F8F8;
border-top: 1px solid #DDD;
border-bottom: 1px solid #DDD;
font-size: 100%;
}
.gist .gist-file .gist-data pre {
font-family: 'Inconsolata', 'Bitstream Vera Sans Mono', 'Courier', monospace;
background: transparent !important;
margin: 0 !important;
border: none !important;
padding: .25em .5em .5em .5em !important;
}
.gist .gist-file .gist-data .gist-highlight {
background: transparent !important;
}
.gist .gist-file .gist-data .gist-line-numbers {
background-color: #ececec;
color: #aaa;
border-right: 1px solid #ddd;
text-align: right;
}
.gist .gist-file .gist-data .gist-line-numbers span {
clear: right;
display: block;
}
.gist-syntax {
background: #ffffff;
}
.gist-syntax .c {
color: #999988;
font-style: italic
}
/* Comment */
.gist-syntax .err {
color: #a61717;
background-color: #e3d2d2
}
/* Error */
.gist-syntax .k {
color: #a79d00;
font-weight: normal
}
/* Keyword */
.gist-syntax .o {
color: #849a22;
font-weight: bold
}
/* Operator */
.gist-syntax .cm {
color: #999988;
font-style: italic
}
/* Comment.Multiline */
.gist-syntax .cp {
color: #dc322f;
font-weight: normal
}
/* Comment.Preproc */
.gist-syntax .c1 {
color: #999988;
font-style: italic
}
/* Comment.Single */
.gist-syntax .cs {
color: #999999;
font-weight: bold;
font-style: italic
}
/* Comment.Special */
.gist-syntax .gd {
color: #000000;
background-color: #ffdddd
}
/* Generic.Deleted */
.gist-syntax .gd .x {
color: #000000;
background-color: #ffaaaa
}
/* Generic.Deleted.Specific */
.gist-syntax .ge {
color: #000000;
font-style: italic
}
/* Generic.Emph */
.gist-syntax .gr {
color: #aa0000
}
/* Generic.Error */
.gist-syntax .gh {
color: #999999;
}
/* Generic.Heading */
.gist-syntax .gi {
color: #000000;
background-color: #ddffdd
}
/* Generic.Inserted */
.gist-syntax .gi .x {
color: #000000;
background-color: #aaffaa;
}
/* Generic.Inserted.Specific */
.gist-syntax .go {
color: #888888;
}
/* Generic.Output */
.gist-syntax .gp {
color: #555555;
}
/* Generic.Prompt */
.gist-syntax .gs {
font-weight: bold
}
/* Generic.Strong */
.gist-syntax .gu {
color: #aaaaaa;
}
/* Generic.Subheading */
.gist-syntax .gt {
color: #aa0000;
}
/* Generic.Traceback */
.gist-syntax .kc {
color: #000000;
font-weight: bold
}
/* Keyword.Constant */
.gist-syntax .kd {
color: #000000;
font-weight: bold
}
/* Keyword.Declaration */
.gist-syntax .kp {
color: #000000;
font-weight: bold
}
/* Keyword.Pseudo */
.gist-syntax .kr {
color: #000000;
font-weight: bold
}
/* Keyword.Reserved */
.gist-syntax .kt {
color: #445588;
font-weight: bold
}
/* Keyword.Type */
.gist-syntax .m {
color: #009999
}
/* Literal.Number */
.gist-syntax .s {
color: #d14
}
/* Literal.String */
.gist-syntax .na {
color: #657b83
}
/* Name.Attribute */
.gist-syntax .nb {
color: #0086B3
}
/* Name.Builtin */
.gist-syntax .nc {
color: #445588;
font-weight: bold
}
/* Name.Class */
.gist-syntax .no {
color: #008080
}
/* Name.Constant */
.gist-syntax .ni {
color: #800080
}
/* Name.Entity */
.gist-syntax .ne {
color: #990000;
font-weight: bold
}
/* Name.Exception */
.gist-syntax .nf {
color: #657b83;
font-weight: normal
}
/* Name.Function */
.gist-syntax .nn {
color: #555555
}
/* Name.Namespace */
.gist-syntax .nt {
color: #000080
}
/* Name.Tag */
.gist-syntax .nv {
color: #258ad1
}
/* Name.Variable */
.gist-syntax .ow {
color: #000000;
font-weight: bold
}
/* Operator.Word */
.gist-syntax .w {
color: #bbbbbb
}
/* Text.Whitespace */
.gist-syntax .mf {
color: #009999
}
/* Literal.Number.Float */
.gist-syntax .mh {
color: #009999
}
/* Literal.Number.Hex */
.gist-syntax .mi {
color: #4cc0cb
}
/* Literal.Number.Integer */
.gist-syntax .mo {
color: #009999
}
/* Literal.Number.Oct */
.gist-syntax .sb {
color: #d14
}
/* Literal.String.Backtick */
.gist-syntax .sc {
color: #d14
}
/* Literal.String.Char */
.gist-syntax .sd {
color: #d14
}
/* Literal.String.Doc */
.gist-syntax .s2 {
color: #d14
}
/* Literal.String.Double */
.gist-syntax .se {
color: #d14
}
/* Literal.String.Escape */
.gist-syntax .sh {
color: #d14
}
/* Literal.String.Heredoc */
.gist-syntax .si {
color: #d14
}
/* Literal.String.Interpol */
.gist-syntax .sx {
color: #d14
}
/* Literal.String.Other */
.gist-syntax .sr {
color: #009926
}
/* Literal.String.Regex */
.gist-syntax .s1 {
color: #4cc0cb
}
/* Literal.String.Single */
.gist-syntax .ss {
color: #990073
}
/* Literal.String.Symbol */
.gist-syntax .bp {
color: #999999
}
/* Name.Builtin.Pseudo */
.gist-syntax .vc {
color: #008080
}
/* Name.Variable.Class */
.gist-syntax .vg {
color: #008080
}
/* Name.Variable.Global */
.gist-syntax .vi {
color: #008080
}
/* Name.Variable.Instance */
.gist-syntax .il {
color: #009999
}
/* Function call */
.gist-syntax .nx {
color: #687b89;
}
/* HTML */
.gist-syntax .x {
color: #258bd2;
}
<?php
/**
* Namespacing for Pretty Cacheable Gists.
*
* Pretty Cacheable Gists caches gists locally and adds a nice stylesheet to make them prettier.
* The Gist embed is detected on save and added as a post of type "cgist". When the post is rendered,
* rather than show embed the Gist via Javascript, it is loaded from the cgist post's meta data. This
* allows for quicker rendering and easy caching.
*/
class cgistPrettyCacheableGists {
/**
* Key for the cache incrementor.
*
* @var string
*/
private $_incrementor_key = 'ztol-gist-incrementor';
/**
* Key for the transient that holds the queued Gist IDs.
*
* @var string
*/
private $_queued_gist_ids_key = 'ztol-queued-gists';
/**
* Queue the events.
*/
public function __construct() {
add_action( 'init', array( $this, 'init_actions' ) );
add_action( 'save_post', array( $this, 'queue_gists' ), 99, 2 );
add_filter( 'the_content', array( $this, 'render_gist' ), 100 );
add_action( 'wp_print_styles', array( $this, 'wp_print_styles' ) );
}
/**
* Run functions on init.
*
* @return void
*/
public function init_actions() {
$this->_register_post_type();
$this->_register_taxonomy();
add_action( 'ztol_create_gists', array( $this, 'create_gists' ) );
}
/**
* Register the CPT.
*
* @return void
*/
private function _register_post_type() {
$args = array(
'labels' => array(
'name' => 'Gists',
'singular_name' => 'Gist',
'add_new' => 'Add New Gist',
'add_new_item' => 'Add New Gist',
'edit_item' => 'Edit Gist',
'new_item' => 'New Gist',
'all_items' => 'All Gists',
'view_item' => 'View Gist',
'search_items' => 'Search Gists',
'not_found' => 'No gists found',
'not_found_in_trash' => 'No gists found in Trash',
'parent_item_colon' => '',
'menu_name' => 'Gists'
),
'description' => 'Gists used in content throughout the site.',
'public' => true,
'publicly_queryable' => false,
'exclude_from_search' => true,
'rewrite' => false,
'query_var' => false,
'menu_position' => 5,
'can_export' => true,
'supports' => array( 'title' ),
'taxonomies' => array( 'cgist-language' )
);
register_post_type( 'cgist', $args );
}
/**
* Register the taxonomy.
*
* @return void
*/
private function _register_taxonomy() {
register_taxonomy(
'cgist-language',
array(
'cgist'
),
array(
'hierarchical' => true,
'labels' => array(
'name' => 'Languages',
'singular_name' => 'Language',
'search_items' => 'Languages',
'all_items' => 'All Languages',
'parent_item' => 'Parent Language',
'parent_item_colon' => 'Parent Language:',
'edit_item' => 'Edit Language',
'update_item' => 'Update Language',
'add_new_item' => 'Add New Language',
'new_item_name' => 'New Language Name',
'menu_name' => 'Language',
),
'show_ui' => true,
'query_var' => true,
'rewrite' => array(
'slug' => 'genre'
),
)
);
}
/**
* Get linked Gists and queue for gist creation.
*
* Examines post content for Gist link. If it is found, the ID is extracted and added
* to a queue. An event is scheduled that will loop through the queue and process the
* each ID into a post of cgist type.
*
* @param $post_id
* @return void
*/
public function queue_gists( $post_id ) {
if ( ! $post = get_post( $post_id ) )
return;
if ( ! $gist_ids = $this->_extract_gist_id( $post->post_content ) )
return;
$clean_gist_ids = array();
foreach ( $gist_ids as $id )
$clean_gist_ids[] = absint( $id );
$this->_add_gists_to_queue( $clean_gist_ids );
}
/**
* Add id(s) to the Gist queue and schedules even to process Gist IDs.
*
* @param $ids array|int
*/
private function _add_gists_to_queue( $ids ) {
if ( ! is_array( $ids ) && ! absint( $ids ) )
return;
if ( ! is_array( $ids ) )
$ids = (array) $ids;
if ( empty( $ids ) )
return;
$currently_queued_ids = get_transient( $this->_queued_gist_ids_key );
// Merge with previous values if present. Remove duplicates if they exist
$ids_to_queue = false !== $currently_queued_ids ? array_unique( array_merge( $currently_queued_ids, $ids ) ) : $ids;
if ( ! empty( $ids_to_queue ) )
set_transient( $this->_queued_gist_ids_key, $ids_to_queue );
if ( get_transient( $this->_queued_gist_ids_key ) && ! wp_next_scheduled( 'ztol_create_gists' ) )
wp_schedule_single_event( time(), 'ztol_create_gists' );
}
/**
* Extract the Gist URL and create a new post of cgist type.
*
* This function extracts a Gist URL from the post content, extracts the ID from that
* URL, makes a request for that Gist, and finally adds the Gist as a new post.
*
* @return void
*/
public function create_gists() {
$gist_ids = get_transient( $this->_queued_gist_ids_key );
if ( false == $gist_ids )
return;
$posts = array();
// Foreach Gist ID, get the Gist information
foreach ( $gist_ids as $id ) {
// Verify that it doesn't already exist
if ( ! $this->_get_gist( $id ) ) {
// Get the Gist information
if ( $post_data = $this->_retrieve_gist_information( $id ) ) {
// Get the nice embeddable format
if ( $embed_html = $this->_retrieve_embed_content( $id ) ) {
$post_data['post_meta']['embed_content'] = $embed_html;
$posts[] = $post_data;
}
}
}
}
// Foreach Gist, add a new post
foreach ( $posts as $post_data )
$this->_insert_gist( $post_data );
// Since posts are added, remove the queued IDs
delete_transient( $this->_queued_gist_ids_key );
}
/**
* Get Gist ID from a Gist URL in a post's content.
*
* @param $content
* @return array|bool
*/
private function _extract_gist_id( $content ) {
if ( empty( $content ) )
return false;
// Match a regular gist URL (e.g., https://gist.github.com/1149945)
$pattern = '#[^"]https:\/\/gist\.github\.com\/([0-9]+)[^"]#';
preg_match_all( $pattern, $content, $matches );
if ( empty( $matches ) )
return false;
$clean_matches = array();
foreach ( $matches[1] as $value ) {
if ( $clean_val = absint( $value ) )
$clean_matches[] = $clean_val;
}
if ( ! empty( $clean_matches ) )
return $clean_matches;
else
return false;
}
/**
* Gets the code from the Gist.
*
* @param $gist_body
* @return bool|string
*/
private function _clean_gist_embed_content( $gist_body ) {
$decoded = json_decode( $gist_body );
if ( isset( $decoded->div ) )
return $decoded->div;
else
return false;
}
/**
* Get Gist code from Gist JSON endpoint.
*
* I use this to get the actual code because it returns the code in the syntax highlighted
* version of the code. The code returned in the other API call returns the raw code.
*
* @param $gist_id
* @return bool|string
*/
private function _retrieve_embed_content( $gist_id ) {
if ( ! $gist_id = absint( $gist_id ) )
return false;
$url = 'https://gist.github.com/' . absint( $gist_id ) . '.json';
$response = wp_remote_get( $url );
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 != $response_code )
return false;
$body = wp_remote_retrieve_body( $response );
$html = $this->_clean_gist_embed_content( $body );
if ( ! empty( $html ) )
return $html;
else
return false;
}
/**
* Get ALL of the Gist details from the GitHub API.
*
* Note that this API call is different than the previous method. This returns
* all of the information available for the Gist. Not all of the information is used.
*
* @param $gist_id
* @return array|bool
*/
private function _retrieve_gist_information( $gist_id ) {
if ( ! $gist_id = absint( $gist_id ) )
return false;
$url = 'https://api.github.com/gists/' . $gist_id;
$response = wp_remote_get( $url );
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 != $response_code )
return false;
$body = wp_remote_retrieve_body( $response );
$decoded = json_decode( $body );
// Currently only supports single file gists
foreach ( $decoded->files as $filename => $information ) {
$file = $filename;
break;
}
if ( ! isset( $file ) )
return false;
// Get file type for later use
$file_types = $this->_get_file_types();
$file_type = isset( $file_types[ wp_strip_all_tags( $decoded->files->$file->type ) ] ) ? $file_types[ wp_strip_all_tags( $decoded->files->$file->type ) ] : 'PHP';
$data = array(
'post' => array(
'post_title' => wp_strip_all_tags( $decoded->description )
),
'post_meta' => array(
'gist_id' => absint( $decoded->id ),
'url' => esc_url_raw( $decoded->html_url ),
'raw_file_url' => esc_url_raw( $decoded->files->$file->raw_url ),
'raw_content' => $decoded->files->$file->content
),
'taxonomies' => array(
'cgist-language' => array(
$file_type
)
)
);
return $data;
}
/**
* Saves the Gist as a post of cgist type.
*
* Creates a new post for the Gist and saves the necessary metadata for later use.
* This function also adds the taxonomy terms for the Gist.
*
* @param $data
* @return bool|int|WP_Error
*/
private function _insert_gist( $data ) {
if ( ! isset( $data['post_meta']['gist_id'] ) || ! absint( $data['post_meta']['gist_id'] ) )
return false;
// Verify that gist doesn't already exist
if ( $this->_get_gist( $data['post_meta']['gist_id'] ) )
return false;
$defaults = array( 'post_status' => 'publish', 'post_type' => 'cgist' );
$post_data = wp_parse_args( $data['post'], $defaults );
// Insert the post
if ( ! $id = wp_insert_post( $post_data ) )
return false;
// Add the taxonomies
foreach ( $data['taxonomies'] as $taxonomy => $term ) {
$concatenated_terms = implode( ',', $term );
wp_set_object_terms( $id, $concatenated_terms, $taxonomy );
}
// Add the postmeta
foreach ( $data['post_meta'] as $key => $value )
update_post_meta( $id, '_cgist_' . $key, $value );
return $id;
}
/**
* Mime-type to human readable relationships.
*
* @return array|void
*/
private function _get_file_types() {
return apply_filters( 'cgist_file_types', array(
'application/httpd-php' => 'PHP',
'text/plain' => 'Text',
'application/ruby' => 'Ruby'
) );
}
/**
* Render the Gist content on page view.
*
* If a Gist URL is detected, it is displayed from the saved cgist content. If the
* cgist cannot be found, it is displayed as an embedded Gist.
*
* @param $content
* @return bool|mixed
*/
public function render_gist( $content ) {
global $wp_query;
$pattern = '#[^"]https:\/\/gist\.github\.com\/([0-9]+)[^"]#';
preg_match_all( $pattern, $content, $matches );
if ( empty( $matches ) )
return false;
foreach ( $matches[1] as $id ) {
$id = absint( $id );
if ( ! $id )
continue;
$embed = $this->_get_gist_content( $id );
if ( is_single() || ( is_home() && 0 == $wp_query->current_post ) )
$content = str_replace( 'https://gist.github.com/' . $id, $embed, $content );
else
$content = str_replace( 'https://gist.github.com/' . $id, '', $content );
}
return $content;
}
/**
* Gets a cgist.
*
* @param $id
* @return bool|WP_Query
*/
private function _get_gist( $id ) {
if ( ! absint( $id ) )
return false;
$existing_gists = new WP_Query( array(
'post_type' => 'cgist',
'posts_per_page' => 1,
'meta_query' => array(
array(
'key' => '_cgist_gist_id',
'value' => $id,
'type' => 'NUMERIC'
)
)
) );
if ( $existing_gists->post_count > 0)
return $existing_gists;
return false;
}
/**
* Get the Gist embed content from cache or generate it.
*
* @param $id
* @return string
*/
private function _get_gist_content( $id ) {
// Generate key
$key = 'ztol-gist-' . md5( absint( $id ) . $this->_get_incrementor_value() );
// Get content
$gist_content = get_transient( $key );
// If not there, generate it
if ( false === $gist_content ) {
$gist = $this->_get_gist( $id );
/**
* If the Gist is not found in the local database, queue the Gist to be added from GitHub. Cache that value for
* only 5 minutes. Queue the Gist to be created in the system. If the Gist is found, load from local database and
* cache for 1 week.
*/
if ( ! $gist ) {
$gist_content = '<script src="https://gist.github.com/' . $id . '.js"> </script>';
$this->_add_gists_to_queue( $id );
set_transient( $key, $gist_content, 300 );
} else {
$gist_content = get_post_meta( $gist->posts[0]->ID, '_cgist_embed_content', true );
set_transient( $key, $gist_content, 86400 );
}
}
return $gist_content;
}
/**
* Update the incrementor.
*
* Used to invalidate the breadcrumb cache whenever and event occurs that should
* signal the update of the cache.
*
* @return int
*/
public function update_incrementor() {
$time = time();
set_transient( $this->_incrementor_key, $time );
return $time;
}
/**
* Get current incrementor value.
*
* Returns the value of the incrementor if it exists. If the incrementor does not exist,
* it will update the incrementor and effectively invalidate the cache.
*
* @return int
*/
private function _get_incrementor_value() {
if ( $incrementor = get_transient( $this->_incrementor_key ) )
return $incrementor;
else
return $this->update_incrementor();
}
/**
* Add the "pretty" stylesheet.
*/
public function wp_print_styles() {
wp_enqueue_style( 'tollmanz_gist_kinda_solarized', get_stylesheet_directory_uri() . '/css/gist.css', array(), '1.0', 'all' );
}
}
global $cgistPrettyCacheableGists;
$cgistPrettyCacheableGists = new cgistPrettyCacheableGists();
/**
* Publicly callable version of cgistCacheableGists::update_incrementor.
*/
function tollmanz_invalidate_gists() {
global $cgistPrettyCacheableGists;
$cgistPrettyCacheableGists->update_incrementor();
}
@jkudish

This comment has been minimized.

Copy link

commented Jun 3, 2012

Really neat way to integrate gists into your site :)

Made a fork at https://gist.github.com/2865210 with some really minor edits:

  • added i18n to text strings (though I'm no loading a text-domain since it's not a plugin for now)
  • minor code standard adjustments (minor spacing adjustments mostly)
  • adjusted if statements that were in format ! $a = $b to $a != $b - more readable that way IMO
  • replaced use of $existing_gists->post_count ... with $existing_gists->have_posts()
  • replaced wp_print_styles with wp_enqueue_scripts, which is the correct hook to use for enqueuing stuff
@tollmanz

This comment has been minimized.

Copy link
Owner Author

commented Jun 4, 2012

Joey,

Thanks for the feedback as always! My thoughts:

  • i18n was intentionally not added as its intent was for me and me only (but I think you understand that ;) )
  • Thank you!
  • These are actually different than you are thinking, but the point remains that you could not read it and that is a problem. The easiest way to follow would be:
    $post = get_post( $post_id ); if ( false === $post ) return;
  • Nice catch
  • Another good call! Always get messed up due to no wp_enqueue_styles.
@jkudish

This comment has been minimized.

Copy link

commented Jun 4, 2012

i18n was intentionally not added as its intent was for me and me only (but I think you understand that ;) )

Yeah I figured as much, but easy enough to add :)

These are actually different than you are thinking, but the point remains that you could not read it and that is a problem. The easiest way to follow would be: $post = get_post( $post_id ); if ( false === $post ) return;

Right... I've always hated assigning variables inside if statements because of the lack of readability it creates, and that just confirms my hate for them :)

@carldanley

This comment has been minimized.

Copy link

commented Aug 27, 2012

awesome job on this class! works awesome. only thing i had to change was this:

  • for wp_remote_get(), i had to add an argument for 'sslverify' => false so that my server would actually retrieve the gist contents. otherwise, it failed and wouldn't ever cache it.

I will def. be thinking of more features as I keep using this. great job, it's exactly what I needed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.