From c478f50f6879578650645a282b7fdf8ebed48b98 Mon Sep 17 00:00:00 2001
From: Isabeau Kisler <ikisler@csumb.edu>
Date: Tue, 27 Jun 2017 22:44:05 -0700
Subject: [PATCH] Add preview page edit functionality

Make changes to edit action, add preview_edit action, adjust styles
---
 build/index.php       | 251 ++++++++++++++++++++++++++++++++++++++++--
 module_index.json     |  58 +++++-----
 modules/page-edit.php |  77 +++++++++++--
 theme_default.css     |   2 +
 4 files changed, 338 insertions(+), 50 deletions(-)

diff --git a/build/index.php b/build/index.php
index c9248fd..80e82c3 100644
--- a/build/index.php
+++ b/build/index.php
@@ -288,6 +288,8 @@ textarea { min-height: 10em; line-height: 1.3em; font-size: 1.25rem; }
 textarea, textarea[name=content] + pre, textarea ~ input[type=submit], #search-box { width: calc(100% - 0.3rem); box-sizing: border-box; }
 textarea ~ input[type=submit] { margin: 0.5rem 0; padding: 0.5rem; font-weight: bolder; }
 .editform input[type=text] { width: calc(100% - 0.3rem); box-sizing: border-box; }
+input.edit-page-button[name='submit-edit'] { width: calc(50% - 1.25rem); margin-right: 1rem }
+input.edit-page-button[name='preview-edit'] { width: calc(50% - 1.25rem); margin-left: 1rem }
 .jump-to-comments { position: relative; top: -2.5em; display: block; text-align: right; pointer-events: none; }
 .jump-to-comments > a { pointer-events: all; }
 
@@ -357,9 +359,9 @@ if($settings->css === "auto")
 
 
 
-///////////////////////////////////////////////////////////////////////////////////////////////
-/////////////// Do not edit below this line unless you know what you are doing! ///////////////
-///////////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////
+////// Do not edit below this line unless you know what you are doing! //////
+/////////////////////////////////////////////////////////////////////////////
 $version = "v0.14-dev";
 /// Environment ///
 $env = new stdClass(); // The environment object
@@ -5076,6 +5078,46 @@ register_module([
 	"code" => function() {
 		global $settings, $env;
 		
+		/**
+		 * @api {post} ?action=preview_edit&page={pageName}[&newpage=yes]	Get a preview of the page
+		 * @apiDescription	Gets a preview of the current edit state of a given page
+		 * @apiName 		PreviewPage
+		 * @apiPermission	Anonymous
+		 * 
+		 * @apiUse	PageParameter
+		 * @apiParam	{string}	newpage 	Set to 'yes' if a new page is being created.
+		 */
+
+		/*
+		 *
+		 * ██████  ██████  ███████ ██    ██ ██ ███████ ██     ██
+		 * ██   ██ ██   ██ ██      ██    ██ ██ ██      ██     ██
+		 * ██████  ██████  █████   ██    ██ ██ █████   ██  █  ██
+		 * ██      ██   ██ ██       ██  ██  ██ ██      ██ ███ ██
+		 * ██      ██   ██ ███████   ████   ██ ███████  ███ ███
+		 *
+		 * ███████ ██████  ██ ████████ 
+		 * ██      ██   ██ ██    ██    
+		 * █████   ██   ██ ██    ██    
+		 * ██      ██   ██ ██    ██    
+		 * ███████ ██████  ██    ██    
+		 *
+		 */
+		add_action("preview_edit", function() {
+			global $pageindex, $settings, $env, $actions;
+
+			if(isset($_POST['preview-edit']) && isset($_POST['content'])) {
+				// preview changes
+				get_object_vars($actions)['edit']();
+			}
+			else {
+				// save page
+				get_object_vars($actions)['save']();
+			}
+
+			
+		});
+
 		/**
 		 * @api {get} ?action=edit&page={pageName}[&newpage=yes]	Get an editing page
 		 * @apiDescription	Gets an editing page for a given page. If you don't have permission to edit the page in question, a view source pagee is returned instead.
@@ -5155,7 +5197,7 @@ register_module([
 					exit(page_renderer::render_main("Viewing source for $env->page", $sourceViewContent));
 				}
 				else
-				{
+				{
 					$errorMessage = "<p>The page <code>$env->page</code> does not exist, but you do not have permission to create it.</p><p>If you haven't already, perhaps you should try <a href='index.php?action=login&returnto=" . rawurlencode($_SERVER["REQUEST_URI"]) . "'>logging in</a>.</p>\n";
 					
 					if($isOtherUsersPage) {
@@ -5175,14 +5217,33 @@ register_module([
 			{
 				$content .= "<p><strong>Warning: You are not logged in! Your IP address <em>may</em> be recorded.</strong></p>";
 			}
-			$content .= "<form method='post' action='index.php?action=save&page=" . rawurlencode($page) . "&action=save' class='editform'>
-			<input type='hidden' name='prev-content-hash' value='" . sha1($pagetext) . "' />
-			<textarea name='content' autofocus tabindex='1'>$pagetext</textarea>
-			<pre class='fit-text-mirror'></pre>
-			<input type='text' name='tags' value='$page_tags' placeholder='Enter some tags for the page here. Separate them with commas.' title='Enter some tags for the page here. Separate them with commas.' tabindex='2' />
-			<p class='editing-message'>$settings->editing_message</p>
-			<input name='submit-edit' type='submit' value='Save Page' tabindex='3' />
-		</form>";
+			
+			// Include preview, if set
+			if(isset($_POST['preview-edit']) && isset($_POST['content'])) {
+				// Need this for the prev-content-hash to prevent the conflict page from appearing
+				$old_pagetext = $pagetext;
+
+				// set the page content to the newly edited content
+				$pagetext = $_POST['content'];
+
+				// Set the tags to the new tags, if needed
+				if(isset($_POST['tags']))
+					$page_tags = $_POST['tags'];
+
+				// Insert the "view" part of the page we're editing
+				$content .= parse_page_source($pagetext);
+
+			}
+
+			$content .= "<form method='post' name='edit-form' action='index.php?action=preview_edit&page=' class='editform'>
+					<input type='hidden' name='prev-content-hash' value='" . ((isset($old_pagetext)) ? sha1($old_pagetext) : sha1($pagetext)) . "' />
+					<textarea name='content' autofocus tabindex='1'>$pagetext</textarea>
+					<pre class='fit-text-mirror'></pre>
+					<input type='text' name='tags' value='$page_tags' placeholder='Enter some tags for the page here. Separate them with commas.' title='Enter some tags for the page here. Separate them with commas.' tabindex='2' />
+					<p class='editing-message'>$settings->editing_message</p>
+					<input name='submit-edit' class='edit-page-button' type='submit' value='Save Page' tabindex='3' />
+					<input name='preview-edit' class='edit-page-button' type='submit' value='Preview Changes' tabindex='4' />
+					</form>";
 			// Allow tab characters in the page editor
 			page_renderer::AddJSSnippet("window.addEventListener('load', function(event) {
 	// Adapted from https://jsfiddle.net/2wAzx/13/
@@ -6486,6 +6547,172 @@ register_module([
 
 
 
+
+register_module([
+	"name" => "Old Default Parser",
+	"version" => "0.10",
+	"author" => "Johnny Broadway & Starbeamrainbowlabs",
+	"description" => "The *old* default parser for Pepperminty Wiki. Based on Johnny Broadway's Slimdown (with more than a few modifications). This parser's features are documented in the help page. Superceded by a customised extension of parsedown extra.",
+	"id" => "parser-default-old",
+	"optional" => true,
+	"code" => function() {
+		global $settings;
+		
+		add_parser("default", function($markdown) {
+			return Slimdown::render($markdown);
+		});
+		
+		// Register the help section
+		if($settings->parser != "default")
+			return; // Don't register the help section if we aren't the currently set parser.
+		add_help_section("20-parser-default", "Editor Syntax", "<p>$settings->sitename's editor uses a modified version of slimdown, a flavour of markdown that is implementated using regular expressions. See the credits page for more information and links to the original source for this. A quick reference can be found below:</p>
+		<table>
+			<tr><th>Type This</th><th>To get this</th>
+			<tr><td><code>_italics_</code></td><td><em>italics</em></td></tr>
+			<tr><td><code>*bold*</code></td><td><strong>bold</strong></td></tr>
+			<tr><td><code>~~Strikethrough~~</code></td><td><del>Strikethough</del></td></tr>
+			<tr><td><code>`code`</code></td><td><code>code</code></td></tr>
+			<tr><td><code># Heading</code></td><td><h2>Heading</h2></td></tr>
+			<tr><td><code>## Sub Heading</code></td><td><h3>Sub Heading</h3></td></tr>
+			<tr><td><code>[[Internal Link]]</code></td><td><a href='index.php?page=Internal Link'>Internal Link</a></td></tr>
+			<tr><td><code>[[Display Text|Internal Link]]</code></td><td><a href='index.php?page=Internal Link'>Display Text</a></td></tr>
+			<tr><td><code>[Display text](//google.com/)</code></td><td><a href='//google.com/'>Display Text</a></td></tr>
+			<tr><td><code>&gt; Blockquote<br />&gt; Some text</code></td><td><blockquote> Blockquote<br />Some text</td></tr>
+			<tr><td><code> - Apples<br /> * Oranges</code></td><td><ul><li>Apples</li><li>Oranges</li></ul></td></tr>
+			<tr><td><code>1. This is<br />2. an ordered list</code></td><td><ol><li>This is</li><li>an ordered list</li></ol></td></tr>
+			<tr><td><code>
+		---
+		</code></td><td><hr /></td></tr>
+			<!--<tr><tds><code> - One
+	 - Two
+	 - Three</code></td><td><ul><li>One</li><li>Two</li><li>Three</li></ul></td></tr>-->
+			<tr><td><code>![Alt text](//starbeamrainbowlabs.com/favicon-small.png)</code></td><td><img src='//starbeamrainbowlabs.com/favicon-small.png' alt='Alt text' /></td></code>
+		</table>
+		
+		<p>In addition, the following extra syntax is supported for images:</p>
+		
+		<pre><code>Size the image to at most 250 pixels wide:
+	![Alt text](//starbeamrainbowlabs.com/favicon-small.png 250px)
+	
+	Size the image to at most 120px wide and have it float at the right ahnd size of the page:
+	![Alt text](//starbeamrainbowlabs.com/favicon-small.png 120px right)</code></pre>");
+	}
+]);
+
+/***********************************************************************
+ * ███████ ██      ██ ███    ███ ██████   ██████  ██     ██ ███    ██  *
+ * ██      ██      ██ ████  ████ ██   ██ ██    ██ ██     ██ ████   ██  *
+ * ███████ ██      ██ ██ ████ ██ ██   ██ ██    ██ ██  █  ██ ██ ██  ██  *
+ *      ██ ██      ██ ██  ██  ██ ██   ██ ██    ██ ██ ███ ██ ██  ██ ██  *
+ * ███████ ███████ ██ ██      ██ ██████   ██████   ███ ███  ██   ████  *
+ ***********************************************************************/
+/**
+ * Slimdown - A very basic regex-based Markdown parser. Supports the
+ * following elements (and can be extended via Slimdown::add_rule()):
+ *
+ * - Headers
+ * - Links
+ * - Bold
+ * - Emphasis
+ * - Deletions
+ * - Quotes
+ * - Inline code
+ * - Blockquotes
+ * - Ordered/unordered lists
+ * - Horizontal rules
+ *
+ * Author: Johnny Broadway <johnny@johnnybroadway.com>
+ * Website: https://gist.github.com/jbroadway/2836900
+ * License: MIT
+ */
+
+/**
+ * Modified by Starbeamrainbowlabs (starbeamrainbowlabs)
+ * 
+ 	* Changed bold to use single asterisks
+ 	* Changed italics to use single underscores
+ 	* Added one to add the heading levels (no <h1> tags allowed)
+ 	* Added wiki style internal link parsing
+ 	* Added wiki style internal link parsing with display text
+ 	* Added image support
+ */
+class Slimdown {
+	public static $rules = array (
+		'/\r\n/' => "\n",											// new line normalisation
+		'/^(#+)(.*)/' => 'self::header',								// headers
+		'/(\*+)(.*?)\1/' => '<strong>\2</strong>',					// bold
+		'/(_)(.*?)\1/' => '<em>\2</em>',							// emphasis
+		
+		'/!\[(.*)\]\(([^\s]+)\s(\d+.+)\s(left|right)\)/' => '<img src="\2" alt="\1" style="max-width: \3; float: \4;" />',		// images with size
+		'/!\[(.*)\]\(([^\s]+)\s(\d+.+)\)/' => '<img src="\2" alt="\1" style="max-width: \3;" />',		// images with size
+		'/!\[(.*)\]\((.*)\)/' => '<img src="\2" alt="\1" />',		// basic images
+		
+		'/\[\[([a-zA-Z0-9\_\- ]+)\|([a-zA-Z0-9\_\- ]+)\]\]/' => '<a href=\'index.php?page=\1\'>\2</a>',	//internal links with display text
+		'/\[\[([a-zA-Z0-9\_\- ]+)\]\]/' => '<a href=\'index.php?page=\1\'>\1</a>',	//internal links
+		'/\[([^\[]+)\]\(([^\)]+)\)/' => '<a href=\'\2\' target=\'_blank\'>\1</a>',	// links
+		'/\~\~(.*?)\~\~/' => '<del>\1</del>',						// del
+		'/\:\"(.*?)\"\:/' => '<q>\1</q>',							// quote
+		'/`(.*?)`/' => '<code>\1</code>',							// inline code
+		'/\n\s*(\*|-)(.*)/' => 'self::ul_list',						// ul lists
+		'/\n[0-9]+\.(.*)/' => 'self::ol_list',						// ol lists
+		'/\n(&gt;|\>)(.*)/' => 'self::blockquote',					// blockquotes
+		'/\n-{3,}/' => "\n<hr />",									// horizontal rule
+		'/\n([^\n]+)\n\n/' => 'self::para',							// add paragraphs
+		'/<\/ul>\s?<ul>/' => '',									// fix extra ul
+		'/<\/ol>\s?<ol>/' => '',									// fix extra ol
+		'/<\/blockquote><blockquote>/' => "\n"						// fix extra blockquote
+	);
+	private static function para ($regs) {
+		$line = $regs[1];
+		$trimmed = trim ($line);
+		if (preg_match ('/^<\/?(ul|ol|li|h|p|bl)/', $trimmed)) {
+			return "\n" . $line . "\n";
+		}
+		return sprintf ("\n<p>%s</p>\n", $trimmed);
+	}
+	private static function ul_list ($regs) {
+		$item = $regs[2];
+		return sprintf ("\n<ul>\n\t<li>%s</li>\n</ul>", trim($item));
+	}
+	private static function ol_list ($regs) {
+		$item = $regs[1];
+		return sprintf ("\n<ol>\n\t<li>%s</li>\n</ol>", trim($item));
+	}
+	private static function blockquote ($regs) {
+		$item = $regs[2];
+		return sprintf ("\n<blockquote>%s</blockquote>", trim($item));
+	}
+	private static function header ($regs) {
+		list ($tmp, $chars, $header) = $regs;
+		$level = strlen ($chars);
+		return sprintf ('<h%d>%s</h%d>', $level + 1, trim($header), $level + 1);
+	}
+	
+	/**
+	 * Add a rule.
+	 */
+	public static function add_rule ($regex, $replacement) {
+		self::$rules[$regex] = $replacement;
+	}
+	/**
+	 * Render some Markdown into HTML.
+	 */
+	public static function render ($text) {
+		foreach (self::$rules as $regex => $replacement) {
+			if (is_callable ( $replacement)) {
+				$text = preg_replace_callback ($regex, $replacement, $text);
+			} else {
+				$text = preg_replace ($regex, $replacement, $text);
+			}
+		}
+		return trim ($text);
+	}
+}
+////////////////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////////////////
+
+
+
 
 register_module([
 	"name" => "Parsedown",
diff --git a/module_index.json b/module_index.json
index d7d54d1..3e7f462 100755
--- a/module_index.json
+++ b/module_index.json
@@ -5,7 +5,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a utility action (that anyone can use) called hash that hashes a given string. Useful when changing a user's password.",
         "id": "action-hash",
-        "lastupdate": 1465757909,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -14,7 +14,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Exposes Pepperminty Wiki's new page protection mechanism and makes the protect button in the 'More...' menu on the top bar work.",
         "id": "action-protect",
-        "lastupdate": 1465751051,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -23,7 +23,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds an action called 'random' that redirects you to a random page.",
         "id": "action-random",
-        "lastupdate": 1483390479,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -32,7 +32,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a 'raw' action that shows you the raw source of a page.",
         "id": "action-raw",
-        "lastupdate": 1476906745,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -41,7 +41,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Provides a basic JSON status action that provices a few useful bits of information for API consumption.",
         "id": "api-status",
-        "lastupdate": 1498472652,
+        "lastupdate": 1498534289,
         "optional": false
     },
     {
@@ -50,7 +50,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a sidebar to the left hand side of every page. Add '$settings->sidebar_show = true;' to your configuration, or append '&sidebar=yes' to the url to enable. Adding to the url sets a cookie to remember your setting.",
         "id": "extra-sidebar",
-        "lastupdate": 1496779827,
+        "lastupdate": 1497115498,
         "optional": false
     },
     {
@@ -59,7 +59,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds threaded comments to the bottom of every page.",
         "id": "feature-comments",
-        "lastupdate": 1497799232,
+        "lastupdate": 1498534289,
         "optional": false
     },
     {
@@ -68,7 +68,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "The module everyone has been waiting for! Adds a web based gui that lets mods change the wiki settings.",
         "id": "feature-guiconfig",
-        "lastupdate": 1490040335,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -77,7 +77,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds the ability to keep unlimited page history, limited only by your disk space. Note that this doesn't store file history (yet). Currently depends on feature-recent-changes for rendering of the history page.",
         "id": "feature-history",
-        "lastupdate": 1497791855,
+        "lastupdate": 1498534289,
         "optional": false
     },
     {
@@ -86,7 +86,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds recent changes. Access through the 'recent-changes' action.",
         "id": "feature-recent-changes",
-        "lastupdate": 1495291194,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -95,7 +95,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds support for redirect pages. Uses the same syntax that Mediawiki does.",
         "id": "feature-redirect",
-        "lastupdate": 1452949822,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -104,7 +104,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds proper search functionality to Pepperminty Wiki using an inverted index to provide a full text search engine. If pages don't show up, then you might have hit a stop word. If not, try requesting the `invindex-rebuild` action to rebuild the inverted index from scratch.",
         "id": "feature-search",
-        "lastupdate": 1496781067,
+        "lastupdate": 1497115498,
         "optional": false
     },
     {
@@ -113,7 +113,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds the ability to upload files to Pepperminty Wiki. Uploaded files act as pages and have the special 'File\/' prefix.",
         "id": "feature-upload",
-        "lastupdate": 1497797573,
+        "lastupdate": 1498534289,
         "optional": false
     },
     {
@@ -122,7 +122,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a user preferences page, letting pople do things like change their email address and password.",
         "id": "feature-user-preferences",
-        "lastupdate": 1497799247,
+        "lastupdate": 1498534289,
         "optional": false
     },
     {
@@ -131,7 +131,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds the credits page. You *must* have this module :D",
         "id": "page-credits",
-        "lastupdate": 1496781899,
+        "lastupdate": 1497115498,
         "optional": false
     },
     {
@@ -140,7 +140,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a debug action for administrator use only that collects a load of useful information to make reporting bugs easier.",
         "id": "page-debug-info",
-        "lastupdate": 1483389558,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -149,7 +149,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds an action to allow administrators to delete pages.",
         "id": "page-delete",
-        "lastupdate": 1465837295,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -158,7 +158,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Allows you to edit pages by adding the edit and save actions. You should probably include this one.",
         "id": "page-edit",
-        "lastupdate": 1488730217,
+        "lastupdate": 1498626170,
         "optional": false
     },
     {
@@ -167,7 +167,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a page that you can use to export your wiki as a .zip file. Uses $settings->export_only_allow_admins, which controls whether only admins are allowed to export the wiki.",
         "id": "page-export",
-        "lastupdate": 1472230366,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -176,7 +176,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a rather useful help page. Access through the 'help' action. This module also exposes help content added to Pepperminty Wiki's inbuilt invisible help section system.",
         "id": "page-help",
-        "lastupdate": 1492433537,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -185,7 +185,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a page that lists all the pages in the index along with their metadata.",
         "id": "page-list",
-        "lastupdate": 1489074718,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -194,7 +194,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a pair of actions (login and checklogin) that allow users to login. You need this one if you want your users to be able to login.",
         "id": "page-login",
-        "lastupdate": 1482008539,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -203,7 +203,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds an action to let users user out. For security reasons it is wise to add this module since logging in automatically opens a session that is valid for 30 days.",
         "id": "page-logout",
-        "lastupdate": 1466011660,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -212,7 +212,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds an action to allow administrators to move pages.",
         "id": "page-move",
-        "lastupdate": 1482008539,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -221,7 +221,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds an update page that downloads the latest stable version of Pepperminty Wiki. This module is currently outdated as it doesn't save your module preferences.",
         "id": "page-update",
-        "lastupdate": 1476906745,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -230,7 +230,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Adds a 'user-list' action that generates a list of users. Supports json output with 'format=json' in the queyr string.",
         "id": "page-user-list",
-        "lastupdate": 1487454626,
+        "lastupdate": 1495991402,
         "optional": false
     },
     {
@@ -239,7 +239,7 @@
         "author": "Starbeamrainbowlabs",
         "description": "Allows you to view pages. You really should include this one.",
         "id": "page-view",
-        "lastupdate": 1497797582,
+        "lastupdate": 1498534289,
         "optional": false
     },
     {
@@ -248,7 +248,7 @@
         "author": "Johnny Broadway & Starbeamrainbowlabs",
         "description": "The *old* default parser for Pepperminty Wiki. Based on Johnny Broadway's Slimdown (with more than a few modifications). This parser's features are documented in the help page. Superceded by a customised extension of parsedown extra.",
         "id": "parser-default-old",
-        "lastupdate": 1458824880,
+        "lastupdate": 1495991402,
         "optional": true
     },
     {
@@ -257,7 +257,7 @@
         "author": "Emanuil Rusev & Starbeamrainbowlabs",
         "description": "An upgraded (now default!) parser based on Emanuil Rusev's Parsedown Extra PHP library (https:\/\/github.com\/erusev\/parsedown-extra), which is licensed MIT. Please be careful, as this module adds some weight to your installation, and also *requires* write access to the disk on first load.",
         "id": "parser-parsedown",
-        "lastupdate": 1489056285,
+        "lastupdate": 1495991402,
         "optional": false
     }
 ]
\ No newline at end of file
diff --git a/modules/page-edit.php b/modules/page-edit.php
index a5af3c1..52dd2c4 100755
--- a/modules/page-edit.php
+++ b/modules/page-edit.php
@@ -9,6 +9,46 @@ register_module([
 	"code" => function() {
 		global $settings, $env;
 		
+		/**
+		 * @api {post} ?action=preview_edit&page={pageName}[&newpage=yes]	Get a preview of the page
+		 * @apiDescription	Gets a preview of the current edit state of a given page
+		 * @apiName 		PreviewPage
+		 * @apiPermission	Anonymous
+		 * 
+		 * @apiUse	PageParameter
+		 * @apiParam	{string}	newpage 	Set to 'yes' if a new page is being created.
+		 */
+
+		/*
+		 *
+		 * ██████  ██████  ███████ ██    ██ ██ ███████ ██     ██
+		 * ██   ██ ██   ██ ██      ██    ██ ██ ██      ██     ██
+		 * ██████  ██████  █████   ██    ██ ██ █████   ██  █  ██
+		 * ██      ██   ██ ██       ██  ██  ██ ██      ██ ███ ██
+		 * ██      ██   ██ ███████   ████   ██ ███████  ███ ███
+		 *
+		 * ███████ ██████  ██ ████████ 
+		 * ██      ██   ██ ██    ██    
+		 * █████   ██   ██ ██    ██    
+		 * ██      ██   ██ ██    ██    
+		 * ███████ ██████  ██    ██    
+		 *
+		 */
+		add_action("preview_edit", function() {
+			global $pageindex, $settings, $env, $actions;
+
+			if(isset($_POST['preview-edit']) && isset($_POST['content'])) {
+				// preview changes
+				get_object_vars($actions)['edit']();
+			}
+			else {
+				// save page
+				get_object_vars($actions)['save']();
+			}
+
+			
+		});
+
 		/**
 		 * @api {get} ?action=edit&page={pageName}[&newpage=yes]	Get an editing page
 		 * @apiDescription	Gets an editing page for a given page. If you don't have permission to edit the page in question, a view source pagee is returned instead.
@@ -88,7 +128,7 @@ register_module([
 					exit(page_renderer::render_main("Viewing source for $env->page", $sourceViewContent));
 				}
 				else
-				{
+				{
 					$errorMessage = "<p>The page <code>$env->page</code> does not exist, but you do not have permission to create it.</p><p>If you haven't already, perhaps you should try <a href='index.php?action=login&returnto=" . rawurlencode($_SERVER["REQUEST_URI"]) . "'>logging in</a>.</p>\n";
 					
 					if($isOtherUsersPage) {
@@ -108,14 +148,33 @@ register_module([
 			{
 				$content .= "<p><strong>Warning: You are not logged in! Your IP address <em>may</em> be recorded.</strong></p>";
 			}
-			$content .= "<form method='post' action='index.php?action=save&page=" . rawurlencode($page) . "&action=save' class='editform'>
-			<input type='hidden' name='prev-content-hash' value='" . sha1($pagetext) . "' />
-			<textarea name='content' autofocus tabindex='1'>$pagetext</textarea>
-			<pre class='fit-text-mirror'></pre>
-			<input type='text' name='tags' value='$page_tags' placeholder='Enter some tags for the page here. Separate them with commas.' title='Enter some tags for the page here. Separate them with commas.' tabindex='2' />
-			<p class='editing-message'>$settings->editing_message</p>
-			<input name='submit-edit' type='submit' value='Save Page' tabindex='3' />
-		</form>";
+			
+			// Include preview, if set
+			if(isset($_POST['preview-edit']) && isset($_POST['content'])) {
+				// Need this for the prev-content-hash to prevent the conflict page from appearing
+				$old_pagetext = $pagetext;
+
+				// set the page content to the newly edited content
+				$pagetext = $_POST['content'];
+
+				// Set the tags to the new tags, if needed
+				if(isset($_POST['tags']))
+					$page_tags = $_POST['tags'];
+
+				// Insert the "view" part of the page we're editing
+				$content .= parse_page_source($pagetext);
+
+			}
+
+			$content .= "<form method='post' name='edit-form' action='index.php?action=preview_edit&page=' class='editform'>
+					<input type='hidden' name='prev-content-hash' value='" . ((isset($old_pagetext)) ? sha1($old_pagetext) : sha1($pagetext)) . "' />
+					<textarea name='content' autofocus tabindex='1'>$pagetext</textarea>
+					<pre class='fit-text-mirror'></pre>
+					<input type='text' name='tags' value='$page_tags' placeholder='Enter some tags for the page here. Separate them with commas.' title='Enter some tags for the page here. Separate them with commas.' tabindex='2' />
+					<p class='editing-message'>$settings->editing_message</p>
+					<input name='submit-edit' class='edit-page-button' type='submit' value='Save Page' tabindex='3' />
+					<input name='preview-edit' class='edit-page-button' type='submit' value='Preview Changes' tabindex='4' />
+					</form>";
 			// Allow tab characters in the page editor
 			page_renderer::AddJSSnippet("window.addEventListener('load', function(event) {
 	// Adapted from https://jsfiddle.net/2wAzx/13/
diff --git a/theme_default.css b/theme_default.css
index 48a7f6a..e488a8c 100644
--- a/theme_default.css
+++ b/theme_default.css
@@ -73,6 +73,8 @@ textarea { min-height: 10em; line-height: 1.3em; font-size: 1.25rem; }
 textarea, textarea[name=content] + pre, textarea ~ input[type=submit], #search-box { width: calc(100% - 0.3rem); box-sizing: border-box; }
 textarea ~ input[type=submit] { margin: 0.5rem 0; padding: 0.5rem; font-weight: bolder; }
 .editform input[type=text] { width: calc(100% - 0.3rem); box-sizing: border-box; }
+input.edit-page-button[name='submit-edit'] { width: calc(50% - 1.25rem); margin-right: 1rem }
+input.edit-page-button[name='preview-edit'] { width: calc(50% - 1.25rem); margin-left: 1rem }
 .jump-to-comments { position: relative; top: -2.5em; display: block; text-align: right; pointer-events: none; }
 .jump-to-comments > a { pointer-events: all; }
 
