github-gist

Gist-Schnipsel von Github in WordPress einbinden

In letzter Zeit wurde ich immer häufiger gefragt, wie ich denn die Codeschnipsel in meine Erklärungen hier einbinde. Folgend des Rätsels Lösung.

Das Konzept

Zunächst werden die Codeschnipsel als Gist auf github.com ausgelagert.
Um diese in den Beiträgen einbinden zu können, müssen die Schnipsel von Github in WordPress importiert werden.

Im Backend soll dafür ein Eingabefeld für die Gist-ID angelegt werden, welches die Gist-Daten in ein benutzerdefiniertes Feld per AJAX lädt.
Die Dateinamen sollen dann an die gewünschte Stelle eingesetzt werden und im Frontend umgewandelt werden.

Die Umsetzung

Metabox

Zunächst die benötigte Metabox für das Backend. Im Eingabefeld wird die Gist-ID eingefügt bzw. aus der Datenbank geladen. Per Button werden die Daten von Github in die Datenbank als benutzerdefinierte Felder gespeichert. Die Dateinamen werden als Liste im Format {Dateiname} ausgegeben.

PHP / RAW / github:gist
<?php
/*
 * Add a metabox the post edit screen with an input field for the gist ID
 * and show the file list.
 */
function ds_gist_metabox_cb( $data ) {
	wp_nonce_field( 'ds_gist', 'ds-gist-nonce' );

	$gist_id   = get_post_meta( $data->ID, '_gist_id', true ) ;
	$gist_id   = ! empty( $gist_id ) ? $gist_id : '';
	$gist_data = get_post_meta( $data->ID, '_gist_data', true );
	$gist_data = ! empty( $gist_data ) ? $gist_data : array();

	$files = '';
	if ( ! empty( $gist_data ) ) {
		foreach ( $gist_data as $file => $data )
			$files .= '<li><code>{' . $file . '}</code></li>';
	}
	?>
	<p>
		<label>Gist ID: <input type="text" class="small-text" style="width: 180px;" name="gist-id" id="gist-id" value="<?php echo esc_attr( $gist_id ); ?>" /></label>
		<input type="button" value="Fetch" id="gist-update" class="button" />
		<img src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" class="ajax-loading" id="gist-ajax-loading" alt="" />
	</p>
	<ul id="gist-files">
		<?php echo $files;?>
	</ul>
	<?php
}

AJAX Callback

Der AJAX Callback dient dazu, die Codeschnipsel in die lokale Datenbank zu übertragen.
Der Callback übernimmt auch das Umformen der empfangen Daten, denn die Gist API gibt mehrere Dateien leider nur als einen String zurück. Dadurch wird es erst möglich, die Schnipsel mit {Dateiname} einzubinden.
Gespeichert werden die Daten als benutzerdefinierte Felder unter den Namen _gist_id bzw. _gist_data.

PHP / RAW / github:gist
<?php
/*
 * Replace {Filename} with associated snippet from the
 * custom fields. 
 */
function ds_gistify() {
	$post_id = intval( $_POST['post_id'] );
	$gist_id = sanitize_key( $_POST['gist_id'] );
	if ( empty( $post_id ) || empty( $gist_id ) )
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => 'Empty Post ID or Gist ID.'
		) ) );

	check_ajax_referer( 'ds_gist' );

	$gist_body = wp_remote_retrieve_body(
		wp_remote_get(
				$gist_url = sprintf( "https://gist.github.com/%s.json", $gist_id )
		)
	);
	$gist_body = json_decode( $gist_body );

	if ( empty( $gist_body ) )
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => 'Empty Body'
		) ) );

	if ( ! empty( $gist_body->error ) ) {
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => $gist_body->error
		) ) );
	}

	update_post_meta( $post_id, '_gist_id', $gist_id );

	$gist_files = $gist_body->files;

	preg_match_all( '/<pre>(.+?)</pre>/is', $gist_body->div, $gist_divs );
	$gist_divs = $gist_divs[0];

	foreach ( $gist_files as $i => $gist_file )
		$gist_data[ $gist_file ] = '<div class="gist-syntax">' . $gist_divs[$i] . '</div>';

	foreach ( $gist_data as $file => $gist_item ) {
		$gist_new_meta = sprintf(
			'<div><p class="gist-meta"><a class="gist-raw" href="%s">RAW</a> · <a class="gist-download" href="%s">Download</a> · <a class="gist-github" href="%s">Gist@GitHub</a></p></div>',
			esc_url( "https://gist.github.com/raw/{$gist_id}/{$file}" ),
			esc_url( "https://gist.github.com/gists/{$gist_id}/download"),
			esc_url( "https://gist.github.com/{$gist_id}")
		);
		$gist_data[ $file ] .= $gist_new_meta;
	}

	update_post_meta( $post_id, '_gist_data', $gist_data );

	die( json_encode( array(
		'id'    => $gist_id,
		'files' => $gist_files
	) ) );
}

jQuery Handler

Der AJAX Callback wird mit Hilfe der Javascript Bibliothek jQuery ausgeführt. Beim Klick des Buttons beginnt die Ausführung.

JavaScript / RAW / github:gist
// Handle AJAX call
( function( $ ) {
	$( '#gist-update' ).click( function( e ) {
		e.preventDefault();

		$( '#gist-ajax-loading' ).css( 'visibility', 'visible' );

		data = {
			'action' : 'gist',
			'_ajax_nonce' : $('#ds-gist-nonce').val(),
			'post_id' : $('#post_ID').val(),
			'gist_id' : $('#gist-id').val()
		};

		$.post( ajaxurl, data,
			function( res ) {
				res = $.parseJSON( res );

				if ( res.error ) {
					$( '#gist-ajax-loading' ).css( 'visibility', 'hidden' );
					alert( 'Gist ID "' + res.id + '": ' + res.error );
					return;
				}

				if ( res.files ) {
					$( '#gist-files' ).empty();
					$.each( res.files, function( key, val ) {
						$( '#gist-files' ).append( '<li><code>{' + val + '}</code></li>' );
					});
					$( '#gist-ajax-loading' ).css( 'visibility', 'hidden' );
				}

			}
		)
	});
} )( jQuery );

Umwandlung im Frontend

Im eigentlichen Artikel befinden sich die Codeschnipsel noch im Format {Dateiname}. Diese müssen nun mit dem eigentlich Schnipsel aus dem benutzerdefinierten Feld ersetzt werden.

PHP / RAW / github:gist
<?php
/*
 * AJAX callback function which handles the API response. Saves the data
 * into custom fields
 */
function ds_convert_to_gist( $content ) {
	$gist_data = get_post_meta( get_the_ID(), '_gist_data', true );

	if ( empty( $gist_data ) )
		return $content;

	wp_enqueue_style('gist-css');

	foreach ( $gist_data as $file => $file_data ) {
		$files[] = '/{' . $file . '}/';
		$data[]  = $file_data;
	}

	$content = preg_replace(
		$files,
		$data,
		$content
	);

	return $content;
}

Hooks und Skripts

Die Funktionen müssen jetzt noch in das WordPress System eingebunden werden, außerdem das Gist Stylesheet sowie die benötigte Javascript Datei registriert und eingebunden werden.

PHP / RAW / github:gist
<?php
/*
 * Hook into the backend and load scripts and
 * init metabox.
 */
function ds_gist2wordpress_admin() {
		add_action( 'wp_ajax_gist', 'ds_gistify' );

		add_action( 'admin_print_scripts-post.php', 'ds_gist_js' );
		add_action( 'admin_print_scripts-post-new.php', 'ds_gist_js' );

		wp_register_script(
			'gist-js',
			get_bloginfo( 'template_url' ) . "/gist.js", // Path, needs some update from you
			array( 'jquery' ),
			0.1,
			true
		);

		add_meta_box(
				'ds_gist',
				'Gist',
				'ds_gist_metabox_cb',
				'post'
		);
}
add_action( 'admin_init', 'ds_gist2wordpress_admin' );


/*
 * Hook into the frontend and load scripts.
 */
function ds_gist2wordpress() {
		wp_register_style(
			'gist-css',
			'https://gist.github.com/stylesheets/gist/embed.css'
		);

		add_filter( 'the_content', 'ds_convert_to_gist', 99 );
}
add_action( 'init', 'ds_gist2wordpress' );


function ds_gist_js() {
	wp_enqueue_script( 'gist-js' );
}

Das Ergebnis

Zum Schluss nochmal alles zusammen, wie es zum Beispiel in einem Theme oder in einem Plugin aussehen könnte.

PHP / RAW / github:gist
<?php
/*
 * Hook into the backend and load scripts and
 * init metabox.
 */
function ds_gist2wordpress_admin() {
	add_action( 'wp_ajax_gist', 'ds_gistify' );

	add_action( 'admin_print_scripts-post.php', 'ds_gist_js' );
	add_action( 'admin_print_scripts-post-new.php', 'ds_gist_js' );

	wp_register_script(
		'gist-js',
		get_bloginfo( 'template_url' ) . "/gist.js",
		array( 'jquery' ),
		0.1,
		true
	);

	add_meta_box(
			'ds_gist',
			'Gist',
			'ds_gist_metabox_cb',
			'post'
	);
}
add_action( 'admin_init', 'ds_gist2wordpress_admin' );

/*
 * Hook into the frontend and load scripts.
 */
function ds_gist2wordpress() {
		wp_register_style(
			'gist-css',
			'https://gist.github.com/stylesheets/gist/embed.css'
		);

		add_filter( 'the_content', 'ds_convert_to_gist', 99 );
}
add_action( 'init', 'ds_gist2wordpress' );

/*
 * Helper for enqeueing script
 */
function ds_gist_js() {
	wp_enqueue_script( 'gist-js' );
}

/*
 * Add a metabox the post edit screen with an input field for the gist ID
 * and show the file list.
 */
function ds_gist_metabox_cb( $data ) {
	wp_nonce_field( 'ds_gist', 'ds-gist-nonce' );

	$gist_id   = get_post_meta( $data->ID, '_gist_id', true ) ;
	$gist_id   = ! empty( $gist_id ) ? $gist_id : '';
	$gist_data = get_post_meta( $data->ID, '_gist_data', true );
	$gist_data = ! empty( $gist_data ) ? $gist_data : array();

	$files = '';
	if ( ! empty( $gist_data ) ) {
		foreach ( $gist_data as $file => $data )
			$files .= '<li><code>{' . $file . '}</code></li>';
	}
	?>
	<p>
		<label>Gist ID: <input type="text" class="small-text" style="width: 180px;" name="gist-id" id="gist-id" value="<?php echo esc_attr( $gist_id ); ?>" /></label>
		<input type="button" value="Fetch" id="gist-update" class="button" />
		<img src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" class="ajax-loading" id="gist-ajax-loading" alt="" />
	</p>
	<ul id="gist-files">
		<?php echo $files;?>
	</ul>
	<?php
}

/*
 * Replace {Filename} with associated snippet from the
 * custom fields. 
 */
function ds_gistify() {
	$post_id = intval( $_POST['post_id'] );
	$gist_id = sanitize_key( $_POST['gist_id'] );
	if ( empty( $post_id ) || empty( $gist_id ) )
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => 'Empty Post ID or Gist ID.'
		) ) );

	check_ajax_referer( 'ds_gist' );

	$gist_body = wp_remote_retrieve_body(
		wp_remote_get(
				$gist_url = sprintf( "https://gist.github.com/%s.json", $gist_id )
		)
	);
	$gist_body = json_decode( $gist_body );

	if ( empty( $gist_body ) )
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => 'Empty Body'
		) ) );

	if ( ! empty( $gist_body->error ) ) {
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => $gist_body->error
		) ) );
	}

	update_post_meta( $post_id, '_gist_id', $gist_id );

	$gist_files = $gist_body->files;

	preg_match_all( '/<pre>(.+?)</pre>/is', $gist_body->div, $gist_divs );
	$gist_divs = $gist_divs[0];

	foreach ( $gist_files as $i => $gist_file )
		$gist_data[ $gist_file ] = '<div class="gist-syntax">' . $gist_divs[$i] . '</div>';

	foreach ( $gist_data as $file => $gist_item ) {
		$gist_new_meta = sprintf(
			'<div><p class="gist-meta"><a class="gist-raw" href="%s">RAW</a> · <a class="gist-download" href="%s">Download</a> · <a class="gist-github" href="%s">Gist@GitHub</a></p></div>',
			esc_url( "https://gist.github.com/raw/{$gist_id}/{$file}" ),
			esc_url( "https://gist.github.com/gists/{$gist_id}/download"),
			esc_url( "https://gist.github.com/{$gist_id}")
		);
		$gist_data[ $file ] .= $gist_new_meta;
	}

	update_post_meta( $post_id, '_gist_data', $gist_data );

	die( json_encode( array(
		'id'    => $gist_id,
		'files' => $gist_files
	) ) );
}

/*
 * AJAX callback function which handles the API response. Saves the data
 * into custom fields
 */
function ds_convert_to_gist( $content ) {
	$gist_data = get_post_meta( get_the_ID(), '_gist_data', true );

	if ( empty( $gist_data ) )
		return $content;

	wp_enqueue_style('gist-css');

	foreach ( $gist_data as $file => $file_data ) {
		$files[] = '/{' . $file . '}/';
		$data[]  = $file_data;
	}

	$content = preg_replace(
		$files,
		$data,
		$content
	);

	return $content;
}

2 thoughts on “Gist-Schnipsel von Github in WordPress einbinden”

  1. Hi Dominik,

    keine üble Lösung. Ich habe mir letztens auch darüber gedanken gemacht, wie ich zukünftig gists auf der (noch nicht) relaunchten pixelfans.de darstelle.

    Dabei bin ich über ein Plugin gestolpert, dass oEmbed nutzt und einfach nur die URL des gists im content erwartet. (http://wordpress.org/extend/plugins/oembed-gist/)

    Schön dabei ist, dass die Ausgabe gleich mit git-typischem syntax-highlighting daherkommt, weniger schön dass dafür das markup der Seite mit Unmengen an spans aufgeblasen wird.

    Ich bin fast sicher, dass Du von diesem Plugin auch gehört oder gelesen hast, bevor Du mit der Entwicklung Deines eigenen begonnen hast, daher meine Frage:

    Warum eine Eigenentwicklung?

    Viele Grüße
    carsten

    1. Hi Carsten,
      erster Grund war, dass ich es auch selbst mal probieren wollte und es direkt ins Theme integriert habe bzw ein Bestandteil des Post-Types ist.

      Zweiter Grund ist das Vermeiden von externen Skripten, die in die Seite eingebunden werden müssen.

Leave a Reply