Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 887139d82c86eb1773a5f9af8ddff3538f450401 @magnetikonline committed Dec 26, 2012
Showing with 509 additions and 0 deletions.
  1. +62 −0 README.md
  2. +427 −0 index.php
  3. +5 −0 rewrite.apache.conf
  4. +15 −0 rewrite.nginx.conf
62 README.md
@@ -0,0 +1,62 @@
+# GitHub Markdown render
+Display [Markdown](http://github.github.com/github-flavored-markdown/) formatted documents on your local development web server using GitHub's [Markdown Rendering API](http://developer.github.com/v3/markdown/) and CSS to mimic the visual display on GitHub itself.
+
+Handy for authoring & previewing **README.md** files (or any Markdown for that matter) for project repositories, avoiding additional noisy `git push` actions in your commit logs due to Markdown typos/errors.
+
+**Note:** this is intended for local development use only, probably not a good idea for production usage due to GitHub API rate limits per user.
+
+## Requires
+- PHP 5.4+ (developed against PHP 5.4.10)
+- [PHP cURL extension](http://php.net/manual/en/book.curl.php) (more than likely part of your PHP install)
+- Nginx or Apache URL rewrite support
+
+## Usage
+Your project(s) Markdown files are accessible on your local web server in plain text, for example:
+
+ http://localhost/projects/ghmarkdownrender/README.md
+ http://localhost/projects/thummer/README.md
+ http://localhost/projects/unrarallthefiles/README.md
+ http://localhost/projects/webserverinstall.ubuntu12.04/install.md
+
+To view using the same parsing and styling as GitHub project pages, request using a querystring switch:
+
+ http://localhost/projects/ghmarkdownrender/README.md?ghmd
+ http://localhost/projects/thummer/README.md?ghmd
+ http://localhost/projects/unrarallthefiles/README.md?ghmd
+ http://localhost/projects/webserverinstall.ubuntu12.04/install.md?ghmd
+
+Rendered HTML is cached in a PHP session based on **\*.md** modification time to reduce repeated GitHub API calls for the same Markdown file content.
+
+## Install
+
+### Configure index.php
+The following constants need to be configured at the top of `index.php` in the `GitHubMarkdownRender` class:
+
+<table>
+ <tr>
+ <td>GITHUB_USERNAME</td>
+ <td>Your GitHub username. <a href="http://developer.github.com/v3/#rate-limiting">Anonymous GitHub API calls are limited to 60 per hour</a>, providing your credentials ramps this up to a much more suitable 5000 requests per hour.</td>
+ </tr>
+ <tr>
+ <td>GITHUB_PASSWORD</td>
+ <td>See above.</td>
+ </tr>
+ <tr>
+ <td>DOC_ROOT</td>
+ <td>Your local web server document root. (Assuming you are serving up all your project(s) directories over your default virtual host.)</td>
+ </tr>
+</table>
+
+### Setup URL rewrite rules
+Next, setup URL rewrite for your default virtual host so all requests to **/local/path/*.md?ghmd** are rewritten to `/path/to/ghmarkdownrender/index.php`. Refer to the supplied `rewrite.nginx.conf` & `rewrite.apache.conf` for examples.
+
+**Note:**
+- You may want to have requested raw Markdown files (e.g. `http://localhost/projects/ghmarkdownrender/README.md`) served up with a MIME type such as `text/plain` for convenience.
+ - Nginx by default serves up unknown file types based on extension as `application/octet-stream`, forcing a browser download - see `/etc/nginx/mime.types` and modify to suit.
+- I haven't had a chance to test `rewrite.apache.conf` it should do the trick, would appreciate a pull-request if it needs fixing.
+
+### Test
+You should now be able to call a Markdown document with a querystring of `?ghmd` to receive a familiar GitHub style Markdown display. The page footer will also display the total/available API rate limits, or if rendering was cached based on file modification time.
+
+## CSS style issues
+Markdown display CSS has been lifted (deliberately) from GitHub.com. It's quite possible there are some CSS styles missing, if so would appreciate examples and/or pull requests to fix.
427 index.php
@@ -0,0 +1,427 @@
+<?php
+// index.php
+
+
+
+class GitHubMarkdownRender {
+
+ const API_URL = 'https://api.github.com/markdown/raw';
+ const CONTENT_TYPE = 'text/x-markdown';
+ const USER_AGENT = 'magnetikonline/ghmarkdownrender 1.0';
+ const MARKDOWN_EXT = '.md';
+ const CACHE_SESSION_KEY = 'ghmarkdownrender';
+
+ const GITHUB_USERNAME = 'username';
+ const GITHUB_PASSWORD = 'password';
+ const DOC_ROOT = '/path/to/docroot';
+
+
+
+ public function execute() {
+
+ // validate DOC_ROOT exists
+ if (!is_dir(self::DOC_ROOT)) {
+ $this->renderErrorMessage(
+ '<p>Given <strong>DOC_ROOT</strong> of <strong>' . htmlspecialchars(self::DOC_ROOT) . '</strong> ' .
+ 'is not a valid directory, ensure it matches that of your local web server.</p>'
+ );
+
+ return;
+ }
+
+ // get requested local markdown page and check file exists
+ if (($markdownFilePath = $this->getRequestedPageFilePath()) === false) {
+ $this->renderErrorMessage(
+ '<p>Unable to determine requested Markdown page.</p>' .
+ '<p>URI must end with an <strong>' . self::MARKDOWN_EXT . '</strong> file extension.</p>'
+ );
+
+ return;
+ }
+
+ if (!is_file($markdownFilePath)) {
+ // can't find markdown file on disk
+ $this->renderErrorMessage(
+ '<p>Unable to open <strong>' . htmlspecialchars($markdownFilePath) . '</strong></p>' .
+ '<p>Ensure <strong>DOC_ROOT</strong> matches that of your local web server.</p>'
+ );
+
+ return;
+ }
+
+ // check PHP session for cached markdown response
+ $html = $this->getMarkdownHtmlFromCache($markdownFilePath);
+ if ($html !== false) {
+ // render markdown HTML from cache
+ echo(
+ $this->getHtmlPageHeader() .
+ $html .
+ $this->getHtmlPageFooter('Rendered from cache')
+ );
+
+ return;
+ }
+
+ // make request to GitHub API passing markdown file source
+ $response = $this->parseGitHubMarkdownResponse(
+ $this->doGitHubMarkdownRequest(file_get_contents($markdownFilePath))
+ );
+
+ if (!$response['ok']) {
+ // error calling API
+ $this->renderErrorMessage(
+ '<p>Unable to access GitHub API</p>' .
+ '<ul>' .
+ '<li>Check your <strong>GITHUB_USERNAME</strong> and <strong>GITHUB_PASSWORD</strong> are correct</li>' .
+ '<li>Is GitHub/GitHub API endpoint <strong>' . htmlspecialchars(self::API_URL) . '</strong> accessable?</li>' .
+ '<li>Has rate limit been exceeded? If so, wait until next hour</li>' .
+ '</ul>'
+ );
+
+ return;
+ }
+
+ // save markdown HTML back to cache
+ $this->setMarkdownHtmlToCache($markdownFilePath,$response['html']);
+
+ // render markdown HTML from API response
+ echo(
+ $this->getHtmlPageHeader() .
+ $response['html'] .
+ $this->getHtmlPageFooter(
+ 'Rendered from GitHub Markdown API. ' .
+ '<strong>Rate limit:</strong> ' . $response['rateLimit'] . ' // ' .
+ '<strong>Rate remain:</strong> ' . $response['rateRemain']
+ )
+ );
+ }
+
+ private function getRequestedPageFilePath() {
+
+ // get request URI, strip any querystring from end (used to trigger Markdown rendering from web server rewrite rule)
+ $requestURI = trim($_SERVER['REQUEST_URI']);
+ $requestURI = preg_replace('/\?.+$/','',$requestURI);
+
+ // request URI must end with self::MARKDOWN_EXT
+ return (preg_match('/\\' . self::MARKDOWN_EXT . '$/',$requestURI))
+ ? self::DOC_ROOT . $requestURI
+ : false;
+ }
+
+ private function renderErrorMessage($errorHtml) {
+
+ echo(
+ $this->getHtmlPageHeader() .
+ '<h1>Error</h1>' .
+ $errorHtml .
+ $this->getHtmlPageFooter()
+ );
+ }
+
+ private function getHtmlPageHeader() {
+
+ return <<<EOT
+<!DOCTYPE html>
+
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>GitHub Markdown render</title>
+
+ <style>
+ body {
+ background: #fff;
+ color: #333;
+ font: 14px/1.6 Helvetica,arial,freesans,clean,sans-serif;
+ margin: 20px;
+ padding: 0;
+ }
+
+ #frame {
+ background: #eee;
+ border-radius: 3px;
+ margin: 0 auto;
+ padding: 3px;
+ width: 914px;
+ }
+
+ #markdown {
+ background: #fff;
+ border: 1px solid #ccc;
+ padding: 30px;
+ }
+
+ #markdown > :first-child {
+ margin-top: 0;
+ }
+
+ h1,h2,h3,h4,h5,h6 {
+ font-weight: bold;
+ margin: 20px 0 10px;
+ padding: 0;
+ }
+
+ h1 {
+ color: #000;
+ font-size: 28px;
+ }
+
+ h2 {
+ border-bottom: 1px solid #ccc;
+ color: #000;
+ font-size: 24px;
+ }
+
+ h3 {
+ font-size: 18px;
+ }
+
+ h4 {
+ font-size: 18px;
+ }
+
+ h5,h6 {
+ font-size: 14px;
+ }
+
+ h6 {
+ color: #777;
+ }
+
+ #markdown > h1:first-child,
+ #markdown > h2:first-child,
+ #markdown > h1:first-child + h2,
+ #markdown > h3:first-child,
+ #markdown > h4:first-child,
+ #markdown > h5:first-child,
+ #markdown > h6:first-child {
+ margin-top: 0;
+ }
+
+ blockquote,dl,ol,p,pre,table,ul {
+ border: 0;
+ margin: 15px 0;
+ padding: 0;
+ }
+
+ ul,ol {
+ padding-left: 30px;
+ }
+
+ ol li > :first-child,
+ ol li ul:first-of-type,
+ ul li > :first-child,
+ ul li ul:first-of-type {
+ margin-top: 0;
+ }
+
+ ol ol,ol ul,ul ol,ul ul {
+ margin-bottom: 0;
+ }
+
+ h1 + p,h2 + p,h3 + p,h4 + p,h5 + p,h6 + p {
+ margin-top: 0;
+ }
+
+ table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ font-size: 100%;
+ font: inherit;
+ }
+
+ table tr {
+ border-top: 1px solid #ccc;
+ background-color: #fff;
+ }
+
+ table tr:nth-child(2n) {
+ background-color: #f8f8f8;
+ }
+
+ table th,
+ table td {
+ border: 1px solid #ccc;
+ padding: 6px 13px;
+ }
+
+ table th {
+ font-weight: bold;
+ }
+
+ pre,code,tt {
+ font-family: Consolas,"Liberation Mono",Courier,monospace;
+ font-size: 12px;
+ }
+
+ code,tt {
+ background-color: #f8f8f8;
+ border-radius: 3px;
+ border: 1px solid #eaeaea;
+ margin: 0 2px;
+ padding: 0 5px;
+ }
+
+ pre {
+ background-color: #f8f8f8;
+ border-radius: 3px;
+ border: 1px solid #ccc;
+ font-size: 13px;
+ line-height: 19px;
+ overflow: auto;
+ padding: 6px 10px;
+ }
+
+ pre > code,pre > tt {
+ background: transparent;
+ border: 0;
+ margin: 0;
+ padding: 0;
+ }
+
+ pre > code {
+ white-space: pre;
+ }
+
+ a {
+ color: #4183c4;
+ text-decoration: none;
+ }
+
+ a:hover {
+ text-decoration: underline;
+ }
+
+ #footer {
+ color: #777;
+ font-size: 11px;
+ margin: 10px auto;
+ text-align: right;
+ white-space: nowrap;
+ width: 914px;
+ }
+ </style>
+</head>
+
+<body>
+
+<div id="frame"><div id="markdown">
+EOT;
+ }
+
+ private function getHtmlPageFooter($footerMessageHtml = false) {
+
+ return
+ '</div></div>' .
+ (($footerMessageHtml !== false)
+ ? '<p id="footer">' . $footerMessageHtml . '</p>'
+ : ''
+ ) .
+ '</body></html>';
+ }
+
+ private function getMarkdownHtmlFromCache($markdownFilePath) {
+
+ // start session, look for file path in session space
+ session_start();
+ if (!isset($_SESSION[self::CACHE_SESSION_KEY][$markdownFilePath])) return false;
+
+ // file path exists - compare file modification time to that in cache
+ $cacheData = $_SESSION[self::CACHE_SESSION_KEY][$markdownFilePath];
+ return ($cacheData['timestamp'] == filemtime($markdownFilePath))
+ ? $cacheData['html']
+ : false;
+ }
+
+ private function setMarkdownHtmlToCache($markdownFilePath,$html) {
+
+ if (!isset($_SESSION[self::CACHE_SESSION_KEY])) {
+ // create new session cache structure
+ $_SESSION[self::CACHE_SESSION_KEY] = [];
+ }
+
+ $_SESSION[self::CACHE_SESSION_KEY][$markdownFilePath] = [
+ 'timestamp' => filemtime($markdownFilePath),
+ 'html' => $html
+ ];
+ }
+
+ private function doGitHubMarkdownRequest($markdownSource) {
+
+ $curl = curl_init();
+ curl_setopt_array(
+ $curl,
+ [
+ CURLOPT_HEADER => true,
+ CURLOPT_HTTPHEADER => [
+ 'Content-Type: ' . self::CONTENT_TYPE,
+ 'User-Agent: ' . self::USER_AGENT
+ ],
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $markdownSource,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_URL => self::API_URL,
+ CURLOPT_USERPWD => sprintf('%s:%s',self::GITHUB_USERNAME,self::GITHUB_PASSWORD)
+ ]
+ );
+
+ $response = curl_exec($curl);
+ curl_close($curl);
+
+ return $response;
+ }
+
+ private function parseGitHubMarkdownResponse($response) {
+
+ $seenHeader = false;
+ $httpStatusOk = false;
+ $rateLimit = 0;
+ $rateRemain = 0;
+
+ while (true) {
+ // seek next CRLF, if not found bail out
+ $nextEOLpos = strpos($response,"\r\n");
+ if ($nextEOLpos === false) break;
+
+ // extract header line and pop off from $response
+ $headerLine = substr($response,0,$nextEOLpos);
+ $response = substr($response,$nextEOLpos + 2);
+
+ if ($seenHeader && (trim($headerLine) == '')) {
+ // end of HTTP headers, bail out
+ break;
+ }
+
+ if (!$seenHeader && preg_match('/^[a-zA-Z-]+:/',$headerLine)) {
+ // have seen a header item - able to bail out once next blank line detected
+ $seenHeader = true;
+ }
+
+ if (preg_match('/^Status: (\d+)/',$headerLine,$match)) {
+ // save HTTP response status, expecting 200 (OK)
+ $httpStatusOk = (intval($match[1]) == 200);
+ }
+
+ if (preg_match('/^X-RateLimit-Limit: (\d+)$/',$headerLine,$match)) {
+ // save total allowed request count
+ $rateLimit = intval($match[1]);
+ }
+
+ if (preg_match('/^X-RateLimit-Remaining: (\d+)$/',$headerLine,$match)) {
+ // save request count remaining
+ $rateRemain = intval($match[1]);
+ }
+ }
+
+ return [
+ 'ok' => ($httpStatusOk && $rateLimit && $rateRemain),
+ 'rateLimit' => $rateLimit,
+ 'rateRemain' => $rateRemain,
+ 'html' => $response
+ ];
+ }
+}
+
+
+$gitHubMarkdownRender = new GitHubMarkdownRender();
+$gitHubMarkdownRender->execute();
5 rewrite.apache.conf
@@ -0,0 +1,5 @@
+# note: this is untested, happy to accept pull requests with fix(es)
+RewriteCond %{REQUEST_URI} \.md$
+RewriteCond %{REQUEST_FILENAME} -f
+RewriteCond %{QUERY_STRING} =ghmd
+RewriteRule . /path/to/ghmarkdownrender/index.php [L]
15 rewrite.nginx.conf
@@ -0,0 +1,15 @@
+location ~ "\.md$" {
+ set $render "yes";
+
+ if (!-f $request_filename) {
+ set $render "";
+ }
+
+ if ($args != "ghmd") {
+ set $render "";
+ }
+
+ if ($render) {
+ rewrite "^" /path/to/ghmarkdownrender/index.php last;
+ }
+}

0 comments on commit 887139d

Please sign in to comment.
Something went wrong with that request. Please try again.