added markdown support and a dropdown for the format selection. The

options other then markdown are plain text and source code (syntax
highlighting). Resolves #25
This commit is contained in:
El RIDO 2015-09-12 17:33:16 +02:00
parent 9dde7f034a
commit 0e53d1ee86
12 changed files with 1518 additions and 55 deletions

View file

@ -14,17 +14,18 @@ discussion = true
; preselect the discussion feature, defaults to false
opendiscussion = false
; enable or disable syntax highlighting, defaults to true
syntaxhighlighting = true
; (optional) set a syntax highlighting theme, as found in css/prettify/
; syntaxhighlightingtheme = "sons-of-obsidian"
; enable or disable the password feature, defaults to true
password = true
; preselect the burn-after-reading feature, defaults to false
burnafterreadingselected = false
; enable or disable the password feature, defaults to true
password = true
; which display mode to preselect by default, defaults to "syntaxhighlighting"
; make sure the value exists in [formatter_options]
defaultformatter = "syntaxhighlighting"
; (optional) set a syntax highlighting theme, as found in css/prettify/
; syntaxhighlightingtheme = "sons-of-obsidian"
; size limit per paste or comment in bytes, defaults to 2 Mibibytes
sizelimit = 2097152
@ -57,6 +58,12 @@ default = "1week"
1year = 31536000
never = 0
[formatter_options]
; Set available formatters, their order and their labels
plaintext = "Plain Text"
syntaxhighlighting = "Source Code"
markdown = "Markdown"
[traffic]
; time limit between calls from the same IP address in seconds
; Set this to 0 to disable rate limiting.

View file

@ -18,16 +18,16 @@ html {
}
body {
font-family: Arial, Helvetica, sans-serif;
font-family: Helvetica, Arial, sans-serif;
font-size: 0.9em;
margin-bottom: 15px;
padding-left: 60px;
padding-right: 60px;
}
a { color: #0f388f; }
h1 {
h1.title {
font-size: 3.5em;
font-weight: bold;
color: #000;
@ -36,7 +36,7 @@ h1 {
cursor: pointer;
}
h1:before {
h1.title:before {
content: attr(title);
position: absolute;
color: rgba(255,255,255,0.15);
@ -45,7 +45,7 @@ h1:before {
cursor: pointer;
}
h2 {
h2.title {
color: #000;
font-size: 1em;
display: inline;
@ -54,15 +54,15 @@ h2 {
position: relative;
bottom: 8px;
}
h3 {
h3.title {
color: #94a3b4;
font-size: 0.7em;
display: inline;
margin-top: 10px;
position: relative;
bottom: 8px;
}
}
#aboutbox {
color: #94a3b4;
@ -76,15 +76,12 @@ h3 {
#aboutbox a { color: #94a3b4; }
#message, #cleartext, .replymessage {
#message, #cleartext, #prettymessage, .replymessage {
clear: both;
color: #000;
background-color: #fff;
white-space: pre-wrap;
font-family: Consolas, "Lucida Console", "DejaVu Sans Mono", Monaco, monospace;
font-size: 9pt;
border: 1px solid #28343F;
padding: 5px;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
@ -93,6 +90,12 @@ h3 {
width: 100%;
}
#message, .replymessage {
padding: 5px;
white-space: pre-wrap;
font-family: Consolas, "Lucida Console", "DejaVu Sans Mono", Monaco, monospace;
}
#status {
clear: both;
padding: 5px 10px;
@ -118,7 +121,7 @@ h3 {
#copyhint { color: #666; font-size: 0.85em; }
button, .button, #expiration {
button, .button, #expiration, #formatter {
color: #fff;
background-color: #323b47;
background-repeat: no-repeat;
@ -177,7 +180,7 @@ button img {
top: 2px;
}
#expiration, #rawtextbutton, #burnafterreadingoption, #opendisc {
#expiration, #formatter, #rawtextbutton, #burnafterreadingoption, #opendisc {
background-color: #414d5a;
padding: 6px 8px;
margin: 0 5px 0 0;
@ -185,14 +188,14 @@ button img {
bottom: 1px; /* WTF ? Why is this shifted by 1 pixel ? */
}
#expiration select {
#expiration select, #formatter select {
color: #eee;
background: transparent;
border: none;
}
#expiration select option {
#expiration select option, #formatter select option {
color:#eee;
background: #414d5a;
}
@ -201,7 +204,7 @@ button img {
padding: 1px 0 1px 0;
}
#remainingtime {
#remainingtime, #password {
color: #94a3b4;
display: inline;
font-size: 0.85em;
@ -278,7 +281,7 @@ input {
min-width: 200px;
}
h4 {
h4.title {
font-size: 1.2em;
color: #94a3b4;
font-style: italic;
@ -385,3 +388,37 @@ img.vizhash {
color: #000000;
font-size: 1.2em;
}
#cleartext {
padding: 10px;
}
#cleartext * {
margin-bottom: 10px;
}
#cleartext ol {
list-style: auto;
margin-left: 15px;
}
#cleartext ul {
list-style: disc;
margin-left: 15px;
}
#cleartext h1, #cleartext h2, #cleartext h3, #cleartext h4, #cleartext h5, #cleartext h6 {
font-weight: bold;
}
#cleartext h1 {
font-size: 2em;
}
#cleartext h2 {
font-size: 1.5em;
}
#cleartext h3 {
font-size: 1.2em;
}

View file

@ -123,5 +123,9 @@
"Could not create paste: %s":
"Konnte Text nicht erstellen: %s",
"Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
"Konnte Text nicht entschlüsseln: Der Schlüssel fehlt in der Adresse (Hast Du eine Umleitung oder einen URL-Verkürzer benutzt, der Teile der Adresse entfernt?)"
"Konnte Text nicht entschlüsseln: Der Schlüssel fehlt in der Adresse (Hast Du eine Umleitung oder einen URL-Verkürzer benutzt, der Teile der Adresse entfernt?)",
"Format": "Format",
"Plain Text": "Nur Text",
"Source Code": "Quellcode",
"Markdown": "Markdown"
}

View file

@ -132,5 +132,9 @@
"PiB": "Pio",
"EiB": "Eio",
"ZiB": "Zio",
"YiB": "Yio"
"YiB": "Yio",
"Format": "Format",
"Plain Text": "texte",
"Source Code": "code source",
"Markdown": "Markdown"
}

View file

@ -123,5 +123,9 @@
"Could not create paste: %s":
"Could not create paste: %s",
"Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)":
"Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)"
"Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)",
"Format": "Format",
"Plain Text": "Plain Text",
"Source Code": "Source Code",
"Markdown": "Markdown"
}

1296
js/showdown.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -183,6 +183,25 @@ $(function() {
}
},
/**
* replace last child of element with message
*
* @param object element : a jQuery wrapped DOM element.
* @param string message : the message to append.
*/
setMessage: function(element, message)
{
var content = element.contents();
if (content.length > 0)
{
content[content.length - 1].nodeValue = ' ' + message;
}
else
{
this.setElementText(element, message);
}
},
/**
* Convert URLs to clickable links.
* URLs to handle:
@ -513,6 +532,44 @@ $(function() {
return password;
},
/**
* use given format on paste, defaults to plain text
*
* @param string format
*/
formatPaste: function(format)
{
switch (format || 'plaintext')
{
case 'markdown':
if (typeof Showdown == 'object')
{
var converter = new Showdown.converter();
this.clearText.html(
converter.makeHtml(this.clearText.html())
);
}
break;
case 'syntaxhighlighting':
if (typeof prettyPrint == 'function') prettyPrint();
default:
// Convert URLs to clickable links.
helper.urls2links(this.clearText);
helper.urls2links(this.prettyPrint);
}
if (format == 'markdown')
{
this.clearText.removeClass('hidden');
this.prettyMessage.addClass('hidden');
}
else
{
this.clearText.addClass('hidden');
this.prettyMessage.removeClass('hidden');
}
if (format == 'plaintext') this.prettyPrint.removeClass('prettyprint');
},
/**
* Show decrypted text in the display area, including discussion (if open)
*
@ -537,11 +594,7 @@ $(function() {
helper.setElementText(this.clearText, cleartext);
helper.setElementText(this.prettyPrint, cleartext);
// Convert URLs to clickable links.
helper.urls2links(this.clearText);
helper.urls2links(this.prettyPrint);
if (typeof prettyPrint == 'function') prettyPrint();
this.formatPaste(comments[0].meta.formatter);
}
catch(err)
{
@ -554,7 +607,6 @@ $(function() {
}
// Display paste expiration / for your eyes only.
var content = this.remainingTime.contents();
if (comments[0].meta.expire_date)
{
var expiration = helper.secondsToHuman(comments[0].meta.remaining_time),
@ -562,7 +614,7 @@ $(function() {
'This document will expire in %d ' + expiration[1] + '.',
'This document will expire in %d ' + expiration[1] + 's.'
];
content[content.length - 1].nodeValue = ' ' + i18n._(expirationLabel, expiration[0]);
helper.setMessage(this.remainingTime, i18n._(expirationLabel, expiration[0]));
this.remainingTime.removeClass('foryoureyesonly')
.removeClass('hidden');
}
@ -572,9 +624,9 @@ $(function() {
.fail(function() {
zerobin.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
});
content[content.length - 1].nodeValue = ' ' + i18n._(
helper.setMessage(this.remainingTime, i18n._(
'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
);
));
this.remainingTime.addClass('foryoureyesonly')
.removeClass('hidden');
// Discourage cloning (as it can't really be prevented).
@ -776,6 +828,7 @@ $(function() {
var data_to_send = {
data: cipherdata,
expire: $('#pasteExpiration').val(),
formatter: $('#pasteFormatter').val(),
burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0,
opendiscussion: this.openDiscussion.is(':checked') ? 1 : 0
};
@ -793,14 +846,11 @@ $(function() {
zerobin.pasteResult.removeClass('hidden');
// We pre-select the link so that the user only has to [Ctrl]+[c] the link.
helper.selectText('pasteurl');
zerobin.showStatus('', false);
helper.setElementText(zerobin.clearText, zerobin.message.val());
helper.setElementText(zerobin.prettyPrint, zerobin.message.val());
// Convert URLs to clickable links.
helper.urls2links(zerobin.clearText);
helper.urls2links(zerobin.prettyPrint);
zerobin.showStatus('', false);
if (typeof prettyPrint == 'function') prettyPrint();
zerobin.formatPaste(data_to_send.formatter);
}
else if (data.status==1)
{
@ -831,6 +881,7 @@ $(function() {
this.prettyMessage.addClass('hidden');
this.sendButton.removeClass('hidden');
this.expiration.removeClass('hidden');
this.formatter.removeClass('hidden');
this.burnAfterReadingOption.removeClass('hidden');
this.openDisc.removeClass('hidden');
this.newButton.removeClass('hidden');
@ -858,6 +909,7 @@ $(function() {
this.rawTextButton.removeClass('hidden');
this.expiration.addClass('hidden');
this.formatter.addClass('hidden');
this.burnAfterReadingOption.addClass('hidden');
this.openDisc.addClass('hidden');
this.newButton.removeClass('hidden');
@ -953,8 +1005,7 @@ $(function() {
else
{
this.errorMessage.removeClass('hidden');
var content = this.errorMessage.contents();
content[content.length - 1].nodeValue = ' ' + message;
helper.setMessage(this.errorMessage, message);
}
this.replyStatus.addClass('errorMessage').text(message);
},
@ -1018,6 +1069,7 @@ $(function() {
this.discussion = $('#discussion');
this.errorMessage = $('#errormessage');
this.expiration = $('#expiration');
this.formatter = $('#formatter');
this.message = $('#message');
this.newButton = $('#newbutton');
this.openDisc = $('#opendisc');

View file

@ -49,6 +49,14 @@ class zerobin
*/
private $_data = '';
/**
* formatter
*
* @access private
* @var string
*/
private $_formatter = 'plaintext';
/**
* error message
*
@ -237,7 +245,7 @@ class zerobin
$meta = array();
// Read expiration date
if (!empty($_POST['expire']))
if (array_key_exists('expire', $_POST) && !empty($_POST['expire']))
{
$selected_expire = (string) $_POST['expire'];
if (array_key_exists($selected_expire, $this->_conf['expire_options']))
@ -252,7 +260,7 @@ class zerobin
}
// Destroy the paste when it is read.
if (!empty($_POST['burnafterreading']))
if (array_key_exists('burnafterreading', $_POST) && !empty($_POST['burnafterreading']))
{
$burnafterreading = $_POST['burnafterreading'];
if ($burnafterreading !== '0')
@ -263,7 +271,11 @@ class zerobin
}
// Read open discussion flag.
if ($this->_conf['main']['discussion'] && !empty($_POST['opendiscussion']))
if (
$this->_getMainConfig('discussion', true) &&
array_key_exists('opendiscussion', $_POST) &&
!empty($_POST['opendiscussion'])
)
{
$opendiscussion = $_POST['opendiscussion'];
if ($opendiscussion !== '0')
@ -273,6 +285,17 @@ class zerobin
}
}
// Read formatter flag.
if (array_key_exists('formatter', $_POST) && !empty($_POST['formatter']))
{
$formatter = $_POST['formatter'];
if (!array_key_exists($formatter, $this->_conf['formatter_options']))
{
$formatter = $this->_getMainConfig('defaultformatter', 'syntaxhighlighting');
}
$meta['formatter'] = $formatter;
}
// You can't have an open discussion on a "Burn after reading" paste:
if (isset($meta['burnafterreading'])) unset($meta['opendiscussion']);
@ -541,6 +564,13 @@ class zerobin
$this->_model()->readComments($dataid)
);
}
// set formatter for for the view.
if (!property_exists($paste->meta, 'formatter'))
{
$paste->meta->formatter = $this->_getMainConfig('defaultformatter', 'syntaxhighlighting');
}
$this->_data = json_encode($messages);
}
}
@ -579,10 +609,14 @@ class zerobin
// label all the expiration options
$expire = array();
foreach ($this->_conf['expire_options'] as $time => $seconds) {
foreach ($this->_conf['expire_options'] as $time => $seconds)
{
$expire[$time] = ($seconds == 0) ? i18n::_(ucfirst($time)): filter::time_humanreadable($time);
}
// translate all the formatter options
$formatters = array_map(array('i18n', 'translate'), $this->_conf['formatter_options']);
$page = new RainTPL;
$page::$path_replace = false;
// we escape it here because ENT_NOQUOTES can't be used in RainTPL templates
@ -592,8 +626,11 @@ class zerobin
$page->assign('VERSION', self::VERSION);
$page->assign('DISCUSSION', $this->_getMainConfig('discussion', true));
$page->assign('OPENDISCUSSION', $this->_getMainConfig('opendiscussion', true));
$page->assign('SYNTAXHIGHLIGHTING', $this->_getMainConfig('syntaxhighlighting', true));
$page->assign('MARKDOWN', array_key_exists('markdown', $formatters));
$page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters));
$page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_getMainConfig('syntaxhighlightingtheme', ''));
$page->assign('FORMATTER', $formatters);
$page->assign('FORMATTERDEFAULT', $this->_getMainConfig('defaultformatter', 'syntaxhighlighting'));
$page->assign('NOTICE', i18n::_($this->_getMainConfig('notice', '')));
$page->assign('BURNAFTERREADINGSELECTED', $this->_getMainConfig('burnafterreadingselected', false));
$page->assign('PASSWORD', $this->_getMainConfig('password', true));

View file

@ -17,7 +17,8 @@
<script type="text/javascript" src="js/rawdeflate-0.5.js"></script>
<script type="text/javascript" src="js/rawinflate-0.3.js"></script>
<script type="text/javascript" src="js/bootstrap-3.3.5.js"></script>{if="$SYNTAXHIGHLIGHTING"}
<script type="text/javascript" src="js/prettify.js?{$VERSION|rawurlencode}"></script>{/if}
<script type="text/javascript" src="js/prettify.js?{$VERSION|rawurlencode}"></script>{/if}{if="$MARKDOWN"}
<script type="text/javascript" src="js/showdown.js?{$VERSION|rawurlencode}"></script>{/if}
<script type="text/javascript" src="js/zerobin.js?{$VERSION|rawurlencode}"></script>
<!--[if lt IE 10]>
<style type="text/css">#ienotice {display:block !important;} #oldienotice {display:block !important;}</style>
@ -80,6 +81,17 @@
<input type="password" id="passwordinput" placeholder="{function="t('Password (recommended)')"}" class="form-control" size="19"/>
</div>
</li>{/if}
<li class="dropdown">
<select id="pasteFormatter" name="pasteFormatter" class="hidden">
{loop="FORMATTER"}
<option value="{$key}"{if="$key == $FORMATTERDEFAULT"} selected="selected"{/if}>{$value}</option>{/loop}
</select>
<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Format')"}: <span id="pasteFormatterDisplay">{$FORMATTER[$FORMATTERDEFAULT]}</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="FORMATTER"}
<li><a href="#" onclick="$('#pasteFormatter').val('{$key}');$('#pasteFormatterDisplay').text('{$value}');return false;">{$value}</a></li>{/loop}
</ul>
</li>
</ul>
<ul class="nav navbar-nav pull-right">
<li>

View file

@ -12,7 +12,8 @@
<script type="text/javascript" src="js/base64-{$BASE64JSVERSION}.js"></script>
<script type="text/javascript" src="js/rawdeflate-0.5.js"></script>
<script type="text/javascript" src="js/rawinflate-0.3.js"></script>{if="$SYNTAXHIGHLIGHTING"}
<script type="text/javascript" src="js/prettify.js?{$VERSION|rawurlencode}"></script>{/if}
<script type="text/javascript" src="js/prettify.js?{$VERSION|rawurlencode}"></script>{/if}{if="$MARKDOWN"}
<script type="text/javascript" src="js/showdown.js?{$VERSION|rawurlencode}"></script>{/if}
<script type="text/javascript" src="js/zerobin.js?{$VERSION|rawurlencode}"></script>
<!--[if lt IE 10]>
<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
@ -24,9 +25,9 @@
{function="t('ZeroBin is a minimalist, opensource online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. More information on the <a href="https://github.com/elrido/ZeroBin/wiki">project page</a>.')"}<br />{if="strlen($NOTICE)"}
<span class="blink"></span> {$NOTICE}{/if}
</div>
<h1 class="reloadlink">{function="t('ZeroBin')"}</h1><br />
<h2>{function="t('Because ignorance is bliss')"}</h2><br />
<h3>{$VERSION}</h3>
<h1 class="title reloadlink">{function="t('ZeroBin')"}</h1><br />
<h2 class="title">{function="t('Because ignorance is bliss')"}</h2><br />
<h3 class="title">{$VERSION}</h3>
<div id="noscript" class="nonworking">{function="t('Javascript is required for ZeroBin to work.<br />Sorry for the inconvenience.')"}</div>
<div id="oldienotice" class="nonworking">{function="t('ZeroBin requires a modern browser to work.')"}</div>
<div id="ienotice">{function="t('Still using Internet Explorer? Do yourself a favor, switch to a modern browser:')"}
@ -63,6 +64,12 @@
<div id="password" class="hidden">
<input type="password" id="passwordinput" placeholder="{function="t('Password (recommended)')"}" size="32" />
</div>{/if}
<div id="formatter" class="button hidden">{function="t('Format')"}:
<select id="pasteFormatter" name="pasteFormatter">
{loop="FORMATTER"}
<option value="{$key}"{if="$key == $FORMATTERDEFAULT"} selected="selected"{/if}>{$value}</option>{/loop}
</select>
</div>
</div>
<div id="pasteresult" class="hidden">
<div id="deletelink"></div>
@ -77,7 +84,7 @@
</section>
<section>
<div id="discussion" class="hidden">
<h4>{function="t('Discussion')"}</h4>
<h4 class="title">{function="t('Discussion')"}</h4>
<div id="comments"></div>
</div>
</section>

View file

@ -33,6 +33,7 @@ class RainTPLTest extends PHPUnit_Framework_TestCase
$page->assign('VERSION', self::$version);
$page->assign('DISCUSSION', true);
$page->assign('OPENDISCUSSION', true);
$page->assign('MARKDOWN', true);
$page->assign('SYNTAXHIGHLIGHTING', true);
$page->assign('SYNTAXHIGHLIGHTINGTHEME', 'sons-of-obsidian');
$page->assign('BURNAFTERREADINGSELECTED', false);

View file

@ -8,6 +8,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
'meta' => array(
'postdate' => 1344803344,
'opendiscussion' => true,
'formatter' => 'syntaxhighlighting',
),
);
@ -202,6 +203,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
helper::createIniFile($this->_conf, $options);
$_POST = self::$paste;
$_POST['expire'] = '5min';
$_POST['formatter'] = 'foo';
$_SERVER['REMOTE_ADDR'] = '::1';
ob_start();
new zerobin;