Add markdown support for comments and repo descriptions

This commit is contained in:
Joseph Schorr 2013-09-30 19:08:24 -04:00
parent e4653cd7cf
commit 461f324e09
12 changed files with 3816 additions and 14 deletions

View file

@ -32,10 +32,17 @@
margin-right: 8px;
}
.editable {
position: relative;
}
.editable i {
position: absolute;
top: 0px;
right: -40px;
opacity: 0.2;
font-size: 85%;
margin-left: 10px;
display: inline-block;
transition: opacity 500ms ease-in-out;
@ -244,6 +251,10 @@ p.editable:hover i {
min-width: 300px;
}
.repo .description p {
margin: 0px;
}
.repo thead td {
padding: 4px;
color: #999;
@ -257,6 +268,17 @@ p.editable:hover i {
margin: 10px;
}
.repo .images .image-id {
font-size: 85%;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
.repo .images p {
margin: 0px;
}
.navbar-nav > li > .user-dropdown {
padding-top: 9px;
padding-bottom: 9px;
@ -348,6 +370,33 @@ p.editable:hover i {
border: inherit;
}
/* Overrides for the markdown editor. */
.wmd-panel .btn-toolbar {
margin-bottom: 10px;
}
.wmd-panel textarea {
outline: none;
}
.wmd-panel.wmd-preview:before {
display: inline-block;
content: "Preview";
background: #eee;
padding: 4px;
position: absolute;
top: 0px;
right: 0px;
}
.wmd-panel.wmd-preview {
position: relative;
border: 1px solid #eee;
margin-top: 10px;
padding: 10px;
}
/* Overrides for typeahead to work with bootstrap 3. */

View file

@ -1,3 +1,40 @@
function getFirstTextLine(commentString) {
if (!commentString) { return; }
var lines = commentString.split('\n');
var MARKDOWN_CHARS = {
'#': true,
'-': true,
'>': true,
'`': true
};
for (var i = 0; i < lines.length; ++i) {
// Skip code lines.
if (lines[i].indexOf(' ') == 0) {
continue;
}
// Skip empty lines.
if ($.trim(lines[i]).length == 0) {
continue;
}
// Skip control lines.
if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) {
continue;
}
return getMarkedDown(lines[i]);
}
return '';
}
function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
function HeaderCtrl($scope, UserService) {
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser;
@ -25,7 +62,7 @@ function HeaderCtrl($scope, UserService) {
template += '<i class="icon-hdd icon-large"></i>'
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
if (datum.repo.description) {
template += '<span class="description">' + datum.repo.description + '</span>'
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
}
template += '</div>'
@ -45,6 +82,15 @@ function RepoListCtrl($scope, Restangular) {
repositoryFetch.getList().then(function(resp) {
$scope.repositories = resp.repositories;
});
$scope.getCommentFirstLine = function(commentString) {
return getMarkedDown(getFirstTextLine(commentString));
};
$scope.getMarkedDown = function(string) {
if (!string) { return ''; }
return getMarkedDown(string);
};
}
function LandingCtrl($scope) {
@ -72,19 +118,36 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) {
$scope.editDescription = function() {
if (!$scope.repo.can_write) { return; }
$('#descriptionEdit')[0].value = $scope.repo.description || '';
if (!$scope.markdownDescriptionEditor) {
var converter = Markdown.getSanitizingConverter();
var editor = new Markdown.Editor(converter, '-description');
editor.run();
$scope.markdownDescriptionEditor = editor;
}
$('#wmd-input-description')[0].value = $scope.repo.description;
$('#editModal').modal({});
};
$scope.saveDescription = function() {
$('#editModal').modal('hide');
$scope.repo.description = $('#descriptionEdit')[0].value;
$scope.repo.description = $('#wmd-input-description')[0].value;
$scope.repo.put();
};
$scope.parseDate = function(dateString) {
return Date.parse(dateString);
};
$scope.getCommentFirstLine = function(commentString) {
return getMarkedDown(getFirstTextLine(commentString));
};
$scope.getMarkedDown = function(string) {
if (!string) { return ''; }
return getMarkedDown(string);
};
$scope.listImages = function() {
if ($scope.imageHistory) { return; }

32
static/lib/pagedown/LICENSE.txt Executable file
View file

@ -0,0 +1,32 @@
A javascript port of Markdown, as used on Stack Overflow
and the rest of Stack Exchange network.
Largely based on showdown.js by John Fraser (Attacklab).
Original Markdown Copyright (c) 2004-2005 John Gruber
<http://daringfireball.net/projects/markdown/>
Original Showdown code copyright (c) 2007 John Fraser
Modifications and bugfixes (c) 2009 Dana Robinson
Modifications and bugfixes (c) 2009-2011 Stack Exchange Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
.wmd-panel {
width: 100%;
}
.wmd-input {
height: 300px;
width: 100%;
box-sizing: border-box;
-webkit-box-sizing:border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
}
.wmd-preview {
.well;
width: 100%;
box-sizing: border-box;
-webkit-box-sizing:border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
}
.wmd-panel .btn-toolbar {
margin-bottom: 0;
padding: 0;
width: 100%;
}
.icon-link,
.icon-blockquote,
.icon-code,
.icon-bullet-list,
.icon-list,
.icon-header,
.icon-hr-line,
.icon-undo {
background-image: url(Markdown.Editor.Icons.png);
}
.icon-link { background-position: 0 0; }
.icon-blockquote { background-position: -24px 0; }
.icon-code { background-position: -48px 0; }
.icon-bullet-list { background-position: -72px 0; }
.icon-list { background-position: -96px 0; }
.icon-header { background-position: -120px 0; }
.icon-hr-line { background-position: -144px 0; }
.icon-undo { background-position: -168px 0; }
.wmd-prompt-background
{
background-color: Black;
}
.wmd-prompt-dialog
{
border: 1px solid #999999;
background-color: #F5F5F5;
}
.wmd-prompt-dialog > div {
font-size: 0.8em;
font-family: arial, helvetica, sans-serif;
}
.wmd-prompt-dialog > form > input[type="text"] {
border: 1px solid #999999;
color: black;
}
.wmd-prompt-dialog > form > input[type="button"]{
border: 1px solid #888888;
font-family: trebuchet MS, helvetica, sans-serif;
font-size: 0.8em;
font-weight: bold;
}

View file

@ -0,0 +1,111 @@
(function () {
var output, Converter;
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
output = exports;
Converter = require("./Markdown.Converter").Converter;
} else {
output = window.Markdown;
Converter = output.Converter;
}
output.getSanitizingConverter = function () {
var converter = new Converter();
converter.hooks.chain("postConversion", sanitizeHtml);
converter.hooks.chain("postConversion", balanceTags);
return converter;
}
function sanitizeHtml(html) {
return html.replace(/<[^>]*>?/gi, sanitizeTag);
}
// (tags that can be opened/closed) | (tags that stand alone)
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
// <a href="url..." optional title>|</a>
var a_white = /^(<a\shref="(https?:(\/\/|\/)|ftp:(\/\/|\/)|mailto:|magnet:)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
// <img src="url..." optional width optional height optional alt optional title
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
// <pre optional class="prettyprint linenums">|</pre> for twitter bootstrap
var pre_white = /^(<pre(\sclass="prettyprint linenums")?>|<\/pre>)$/i;
function sanitizeTag(tag) {
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(pre_white))
return tag;
else
return "";
}
/// <summary>
/// attempt to balance HTML tags in the html string
/// by removing any unmatched opening or closing tags
/// IMPORTANT: we *assume* HTML has *already* been
/// sanitized and is safe/sane before balancing!
///
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
/// </summary>
function balanceTags(html) {
if (html == "")
return "";
var re = /<\/?\w+[^>]*(\s|$|>)/g;
// convert everything to lower case; this makes
// our case insensitive comparisons easier
var tags = html.toLowerCase().match(re);
// no HTML tags present? nothing to do; exit now
var tagcount = (tags || []).length;
if (tagcount == 0)
return html;
var tagname, tag;
var ignoredtags = "<p><img><br><li><hr>";
var match;
var tagpaired = [];
var tagremove = [];
var needsRemoval = false;
// loop through matched tags in forward order
for (var ctag = 0; ctag < tagcount; ctag++) {
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
// skip any already paired tags
// and skip tags in our ignore list; assume they're self-closed
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
continue;
tag = tags[ctag];
match = -1;
if (!/^<\//.test(tag)) {
// this is an opening tag
// search forwards (next tags), look for closing tags
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
match = ntag;
break;
}
}
}
if (match == -1)
needsRemoval = tagremove[ctag] = true; // mark for removal
else
tagpaired[match] = true; // mark paired
}
if (!needsRemoval)
return html;
// delete all orphaned tags from the string
var ctag = 0;
html = html.replace(re, function (match) {
var res = tagremove[ctag] ? "" : match;
ctag++;
return res;
});
return html;
}
})();

View file

@ -3,6 +3,6 @@
<div class="repo-listing" ng-repeat="repository in repositories">
<i class="icon-hdd icon-large"></i>
<a ng-href="#/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
<div class="description">{{repository.description}}</div>
<div class="description" ng-bind-html-unsafe="getCommentFirstLine(repository.description)"></div>
</div>
</div>

View file

@ -41,8 +41,10 @@
<!-- Description -->
<p ng-class="'description lead ' + (repo.can_write ? 'editable' : 'noteditable')" ng-click="editDescription()"><span class="content">{{repo.description}}</span><i class="icon-edit"></i></p>
<p ng-class="'description lead ' + (repo.can_write ? 'editable' : 'noteditable')" ng-click="editDescription()">
<span class="content" ng-bind-html-unsafe="getMarkedDown(repo.description)"></span>
<i class="icon-edit"></i>
</p>
<!-- Tab bar -->
<ul class="nav nav-tabs">
@ -72,8 +74,7 @@
<div ng-show="currentTag.image.comment">
<strong>Description:</strong>
<blockquote style="margin-top: 10px;">
{{ currentTag.image.comment || '' }}
<blockquote style="margin-top: 10px;" ng-bind-html-unsafe="getMarkedDown(currentTag.image.comment)">
</blockquote>
</div>
</div>
@ -87,17 +88,17 @@
<table class="images">
<thead>
<tr>
<td>ID</td>
<td>Created</td>
<td>Comment</td>
<td>ID</td>
</tr>
</thead>
<tr ng-repeat="image in imageHistory">
<td class="image-id" title="{{ image.id }}">{{ image.id }}</td>
<td><span am-time-ago="parseDate(image.created)"></span></td>
<td>{{ image.comment }}</td>
<td>{{ image.id }}</td>
</tr>
<td ng-bind-html-unsafe="getCommentFirstLine(image.comment)"></td>
</tr>
</table>
</div>
</div>
@ -111,7 +112,12 @@
<h4 class="modal-title">Edit Repository Description</h4>
</div>
<div class="modal-body">
<textarea id="descriptionEdit" placeholder="Enter description">{{ repo.description }}</textarea>
<div class="wmd-panel">
<div id="wmd-button-bar-description"></div>
<textarea class="wmd-input" id="wmd-input-description" placeholder="Enter description">{{ repo.description }}</textarea>
</div>
<div id="wmd-preview-description" class="wmd-panel wmd-preview"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>

View file

@ -20,6 +20,9 @@
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
<script src="static/lib/angular-moment.min.js"></script>
<script src="static/lib/pagedown/Markdown.Converter.js"></script>
<script src="static/lib/pagedown/Markdown.Editor.js"></script>
<script src="static/lib/pagedown/Markdown.Sanitizer.js"></script>
<script src="static/js/ZeroClipboard.min.js"></script>
<script src="static/js/typeahead.min.js"></script>