1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-25 14:50:29 +00:00

[markdown] use pagedown for view, edit/preview.

* modified Markdown.Editor.js and pagedown.css
* offered 'help' popup and 'fenced code block'
This commit is contained in:
llj
2013-03-25 20:27:17 +08:00
parent b3687748df
commit dc14675a10
9 changed files with 4518 additions and 48 deletions

61
media/css/pagedown.css Normal file
View File

@@ -0,0 +1,61 @@
#wmd-button-bar {
height: 20px;
padding:10px 5px 5px;
border-bottom:1px solid #ddd;
}
#wmd-input {
font-family: monospace;
line-height:1.3;
width: 746px;
padding: 4px 5px;
resize:none;
vertical-align:top;
border:0;
}
#wmd-preview {
padding:0;
min-height:600px;
}
.wmd-button-row {
position: relative;
}
.wmd-spacer {
width: 1px;
height: 20px;
margin-left: 14px;
position: absolute;
background-color: Silver;
}
.wmd-button {
width: 20px;
height: 20px;
padding-left: 2px;
padding-right: 3px;
position: absolute;
cursor: pointer;
}
.wmd-button span {
background-image: url(../img/wmd-buttons.png);
background-repeat: no-repeat;
background-position: 0px 0px;
display: inline-block;
width: 20px;
height: 20px;
}
.wmd-spacer1 {
left: 50px;
}
.wmd-spacer2 {
left: 175px;
}
.wmd-spacer3 {
left: 300px;
}
.wmd-prompt-background {
background-color: Black;
}
.wmd-prompt-dialog {
text-align:center;
background-color: #fff;
padding-bottom:5px;
}

View File

@@ -42,7 +42,6 @@ textarea {
} }
input { input {
height:22px; height:22px;
/*line-height:22px;*/
border: 1px solid #ddd; border: 1px solid #ddd;
margin:3px 0; margin:3px 0;
outline:0; outline:0;
@@ -52,12 +51,14 @@ button {
display:inline-block;/*for ie*/ display:inline-block;/*for ie*/
} }
input[type=submit], input[type=submit],
input[type=button],
input.submit { input.submit {
height:27px;/*for ff*/ height:27px;/*for ff*/
line-height:1.5; line-height:1.5;
} }
button, button,
input[type=submit], input[type=submit],
input[type=button],
input.submit { input.submit {
padding:3px; padding:3px;
background: #efefef; background: #efefef;
@@ -86,6 +87,7 @@ input.submit {
margin-top:8px; margin-top:8px;
} }
input[type=submit]:hover, input[type=submit]:hover,
input[type=button]:hover,
button:hover { button:hover {
cursor:pointer; cursor:pointer;
background-color:#fff; background-color:#fff;
@@ -149,6 +151,7 @@ p {
.w100 { width: 100%; } .w100 { width: 100%; }
.vh { visibility:hidden; } .vh { visibility:hidden; }
.vam { vertical-align:middle; } .vam { vertical-align:middle; }
.italic { font-style:italic; }
.notification { .notification {
padding:5px; padding:5px;
background:#FDF; background:#FDF;
@@ -760,6 +763,7 @@ textarea:-moz-placeholder {/* for FF */
.article p { .article p {
margin:0.8em 0; margin:0.8em 0;
} }
#md-edit-help ul,
.article ul { .article ul {
list-style-type:disc; list-style-type:disc;
padding-left:2em; padding-left:2em;
@@ -1746,13 +1750,16 @@ textarea:-moz-placeholder {/* for FF */
#docu-view, #docu-view,
#svg-view, #svg-view,
#pdf, #pdf,
#md-view { #md-view,
#md-edit {
background:#fff; background:#fff;
} }
#md-view { #md-view,
#md-edit {
width:756px; width:756px;
} }
#md-view h2, #md-view h2,
#wmd-preview h2,
#wiki-content h2 { #wiki-content h2 {
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
} }
@@ -1824,6 +1831,9 @@ textarea:-moz-placeholder {/* for FF */
#sf, #md-view { #sf, #md-view {
padding:40px 96px; padding:40px 96px;
} }
#md-edit-help {
width:550px;
}
/*overwrite aloha.css*/ /*overwrite aloha.css*/
#file-edit .aloha-editable-active, .aloha-editable-active[contenteditable="true"]:focus { #file-edit .aloha-editable-active, .aloha-editable-active[contenteditable="true"]:focus {
outline: none!important; outline: none!important;

BIN
media/img/wmd-buttons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

File diff suppressed because it is too large Load Diff

2203
media/js/Markdown.Editor.js Normal file

File diff suppressed because it is too large Load Diff

593
media/js/Markdown.Extra.js Normal file
View File

@@ -0,0 +1,593 @@
(function () {
// A quick way to make sure we're only keeping span-level tags when we need to.
// This isn't supposed to be foolproof. It's just a quick way to make sure we
// keep all span-level tags returned by a pagedown converter. It should allow
// all span-level tags through, with or without attributes.
var inlineTags = new RegExp(['^(<\\/?(a|abbr|acronym|applet|area|b|basefont|',
'bdo|big|button|cite|code|del|dfn|em|figcaption|',
'font|i|iframe|img|input|ins|kbd|label|map|',
'mark|meter|object|param|progress|q|ruby|rp|rt|s|',
'samp|script|select|small|span|strike|strong|',
'sub|sup|textarea|time|tt|u|var|wbr)[^>]*>|',
'<(br)\\s?\\/?>)$'].join(''), 'i');
/******************************************************************
* Utility Functions *
*****************************************************************/
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
function rtrim(str) {
return str.replace(/\s+$/g, '');
}
// Remove one level of indentation from text. Indent is 4 spaces.
function outdent(text) {
return text.replace(new RegExp('^(\\t|[ ]{1,4})', 'gm'), '');
}
function contains(str, substr) {
return str.indexOf(substr) != -1;
}
// Sanitize html, removing tags that aren't in the whitelist
function sanitizeHtml(html, whitelist) {
return html.replace(/<[^>]*>?/gi, function(tag) {
return tag.match(whitelist) ? tag : '';
});
}
// Merge two arrays, keeping only unique elements.
function union(x, y) {
var obj = {};
for (var i = 0; i < x.length; i++)
obj[x[i]] = x[i];
for (i = 0; i < y.length; i++)
obj[y[i]] = y[i];
var res = [];
for (var k in obj) {
if (obj.hasOwnProperty(k))
res.push(obj[k]);
}
return res;
}
// JS regexes don't support \A or \Z, so we add sentinels, as Pagedown
// does. In this case, we add the ascii codes for start of text (STX) and
// end of text (ETX), an idea borrowed from:
// https://github.com/tanakahisateru/js-markdown-extra
function addAnchors(text) {
if(text.charAt(0) != '\x02')
text = '\x02' + text;
if(text.charAt(text.length - 1) != '\x03')
text = text + '\x03';
return text;
}
// Remove STX and ETX sentinels.
function removeAnchors(text) {
if(text.charAt(0) == '\x02')
text = text.substr(1);
if(text.charAt(text.length - 1) == '\x03')
text = text.substr(0, text.length - 1);
return text;
}
// Convert markdown within an element, retaining only span-level tags
// (An inefficient version of Pagedown's runSpanGamut. We rely on a
// pagedown coverter to do the complete conversion, and then retain
// only the specified tags -- inline in this case).
function convertSpans(text, converter) {
text = denormalize(text);
var html = converter.makeHtml(text);
return sanitizeHtml(html, inlineTags);
}
// Convert internal markdown using the stock pagedown converter
function convertAll(text, converter) {
text = denormalize(text);
return converter.makeHtml(text);
}
// We use convertSpans and convertAll to convert markdown inside of Markdown Extra
// elements we create. Since this markdown has already been through the pagedown
// normalization process before our hooks were called, we need to do some
// denormalization before sending it back through a different Pagedown converter.
function denormalize(text) {
// Restore dollar signs and tildes
text = text.replace(/~D/g, "$$");
text = text.replace(/~T/g, "~");
return text;
}
// Convert escaped special characters to HTML decimal entity codes.
function processEscapes(text) {
// Markdown extra adds two escapable characters, `:` and `|`
// If escaped, we convert them to html entities so our
// regexes don't recognize them. Markdown doesn't support escaping
// the escape character, e.g. `\\`, which make this even simpler.
return text.replace(/\\\|/g, '&#124;').replace(/\\:/g, '&#58;');
}
// Determine if the given pagedown converter performs sanitization
// on postConversion
function isSanitizing(converter) {
// call the converter's postConversion hook and see if it sanitizes its input
return converter.hooks.postConversion("<table>") === "";
}
/******************************************************************
* Markdown.Extra *
*****************************************************************/
Markdown.Extra = function() {
// For converting internal markdown (in tables for instance).
// This is necessary since these methods are meant to be called as
// preConversion hooks, and the Markdown converter passed to init()
// won't convert any markdown contained in the html tags we return.
this.converter = null;
// Stores html blocks we generate in hooks so that
// they're not destroyed if the user is using a sanitizing converter
this.hashBlocks = [];
// Special attribute blocks for fenced code blocks and headers enabled.
this.attributeBlocks = false;
// Fenced code block options
this.googleCodePrettify = false;
this.highlightJs = false;
// Table options
this.tableClass = '';
this.tabWidth = 4;
};
Markdown.Extra.init = function(converter, options) {
// Each call to init creates a new instance of Markdown.Extra so it's
// safe to have multiple converters, with different options, on a single page
var extra = new Markdown.Extra();
var transformations = [];
options = options || {};
options.extensions = options.extensions || ["all"];
if (contains(options.extensions, "all")) {
transformations.push("all");
extra.attributeBlocks = true;
} else {
if (contains(options.extensions, "tables"))
transformations.push("tables");
if (contains(options.extensions, "fenced_code_gfm"))
transformations.push("fencedCodeBlocks");
if (contains(options.extensions, "def_list"))
transformations.push("definitionLists");
if (contains(options.extensions, "attr_list"))
extra.attributeBlocks = true;
}
// preBlockGamut also gives us access to a hook so we can run the
// block gamut recursively, however we don't need it at this point
converter.hooks.chain("preBlockGamut", function(text) {
return extra.doConversion(transformations, text);
});
converter.hooks.chain("postConversion", function(text) {
return extra.finishConversion(text);
});
if ("highlighter" in options) {
extra.googleCodePrettify = options.highlighter === 'prettify';
extra.highlightJs = options.highlighter === 'highlight';
}
if ("table_class" in options) {
extra.tableClass = options.table_class;
}
// we can't just use the same converter that the user passes in, as
// Pagedown forbids it (doing so could cause an infinite loop)
extra.converter = isSanitizing(converter) ? Markdown.getSanitizingConverter()
: new Markdown.Converter();
// Caller usually won't need this, but it's handy for testing.
return extra;
};
// Setup state vars, do conversion
Markdown.Extra.prototype.doConversion = function(transformations, text) {
this.hashBlocks = [];
text = processEscapes(text);
if (this.attributeBlocks)
text = this.hashAttributeBlocks(text);
for(var i = 0; i < transformations.length; i++)
text = this[transformations[i]](text);
return text + '\n';
};
// Clear state vars that may use unnecessary memory. Unhash blocks we
// stored, apply attribute blocks if necessary, and return converted text.
Markdown.Extra.prototype.finishConversion = function(text) {
text = this.unHashExtraBlocks(text);
if (this.attributeBlocks)
text = this.applyAttributeBlocks(text);
this.hashBlocks = [];
return text;
};
// Return a placeholder containing a key, which is the block's index in the
// hashBlocks array. We wrap our output in a <p> tag here so Pagedown won't.
Markdown.Extra.prototype.hashExtraBlock = function(block) {
return '\n<p>~X' + (this.hashBlocks.push(block) - 1) + 'X</p>\n';
};
// Replace placeholder blocks in `text` with their corresponding
// html blocks in the hashBlocks array.
Markdown.Extra.prototype.unHashExtraBlocks = function(text) {
var self = this;
text = text.replace(/<p>~X(\d+)X<\/p>/g, function(wholeMatch, m1) {
var key = parseInt(m1, 10);
return self.hashBlocks[key];
});
return text;
};
/******************************************************************
* Attribute Blocks *
*****************************************************************/
// Extract attribute blocks, move them above the element they will be
// applied to, and hash them for later.
Markdown.Extra.prototype.hashAttributeBlocks = function(text) {
// TODO: use sentinels. Should we just add/remove them in doConversion?
// TODO: better matches for id / class attributes
var attrBlock = "\\{\\s*[.|#][^}]+\\}";
var hdrAttributesA = new RegExp("^(#{1,6}.*\\s*#{0,6})\\s+(" + attrBlock + ")\\s*(\\n|0x03)", "gm");
var hdrAttributesB = new RegExp("^(.*\\s.*)\\s+(" + attrBlock + ")\\s*\\n" +
"(?=[\\-|=]+\\s*(\\n|0x03))", "gm"); // underline lookahead
var fcbAttributes = new RegExp("^(```[^{]*)\\s+(" + attrBlock + ")\\s*\\n" +
"(?=([\\s\\S]*?)\\n```\\s*(\\n|0x03))", "gm");
var self = this;
function attributeCallback(wholeMatch, pre, attr) {
return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n";
}
text = text.replace(hdrAttributesA, attributeCallback); // ## headers
text = text.replace(hdrAttributesB, attributeCallback); // underline headers
return text.replace(fcbAttributes, attributeCallback);
};
Markdown.Extra.prototype.applyAttributeBlocks = function(text) {
var self = this;
var blockRe = new RegExp('<p>~XX(\\d+)XX</p>[\\s\\S]*' +
'(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*</\\2>))', "gm");
text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) {
if (!tag) // no following header or fenced code block.
return '';
// get attributes list from hash
var key = parseInt(k, 10);
var attributes = self.hashBlocks[key];
// get id
var id = attributes.match(/#[^\s{}]+/g) || [];
var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : '';
// get classes and merge with existing classes
var classes = attributes.match(/\.[^\s{}]+/g) || [];
for (var i = 0; i < classes.length; i++) // Remove leading dot
classes[i] = classes[i].substr(1, classes[i].length - 1);
var classStr = '';
if (cls)
classes = union(classes, [cls]);
if (classes.length > 0)
classStr = ' class="' + classes.join(' ') + '"';
return "<" + tag + idStr + classStr + rest;
});
return text;
};
/******************************************************************
* Tables *
*****************************************************************/
// Find and convert Markdown Extra tables into html.
Markdown.Extra.prototype.tables = function(text) {
var self = this;
var leadingPipe = new RegExp(
['^' ,
'[ ]{0,3}' , // Allowed whitespace
'[|]' , // Initial pipe
'(.+)\\n' , // $1: Header Row
'[ ]{0,3}' , // Allowed whitespace
'[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator
'(' , // $3: Table Body
'(?:[ ]*[|].*\\n?)*' , // Table rows
')',
'(?:\\n|$)' // Stop at final newline
].join(''),
'gm'
);
var noLeadingPipe = new RegExp(
['^' ,
'[ ]{0,3}' , // Allowed whitespace
'(\\S.*[|].*)\\n' , // $1: Header Row
'[ ]{0,3}' , // Allowed whitespace
'([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator
'(' , // $3: Table Body
'(?:.*[|].*\\n?)*' , // Table rows
')' ,
'(?:\\n|$)' // Stop at final newline
].join(''),
'gm'
);
text = text.replace(leadingPipe, doTable);
text = text.replace(noLeadingPipe, doTable);
// $1 = header, $2 = separator, $3 = body
function doTable(match, header, separator, body, offset, string) {
// remove any leading pipes and whitespace
header = header.replace(/^ *[|]/m, '');
separator = separator.replace(/^ *[|]/m, '');
body = body.replace(/^ *[|]/gm, '');
// remove trailing pipes and whitespace
header = header.replace(/[|] *$/m, '');
separator = separator.replace(/[|] *$/m, '');
body = body.replace(/[|] *$/gm, '');
// determine column alignments
alignspecs = separator.split(/ *[|] */);
align = [];
for (var i = 0; i < alignspecs.length; i++) {
var spec = alignspecs[i];
if (spec.match(/^ *-+: *$/m))
align[i] = ' style="text-align:right;"';
else if (spec.match(/^ *:-+: *$/m))
align[i] = ' style="text-align:center;"';
else if (spec.match(/^ *:-+ *$/m))
align[i] = ' style="text-align:left;"';
else align[i] = '';
}
// TODO: parse spans in header and rows before splitting, so that pipes
// inside of tags are not interpreted as separators
var headers = header.split(/ *[|] */);
var colCount = headers.length;
// build html
var cls = self.tableClass ? ' class="' + self.tableClass + '"' : '';
var html = ['<table', cls, '>\n', '<thead>\n', '<tr>\n'].join('');
// build column headers.
for (i = 0; i < colCount; i++) {
var headerHtml = convertSpans(trim(headers[i]), self.converter);
html += [" <th", align[i], ">", headerHtml, "</th>\n"].join('');
}
html += "</tr>\n</thead>\n";
// build rows
var rows = body.split('\n');
for (i = 0; i < rows.length; i++) {
if (rows[i].match(/^\s*$/)) // can apply to final row
continue;
// ensure number of rowCells matches colCount
var rowCells = rows[i].split(/ *[|] */);
var lenDiff = colCount - rowCells.length;
for (var j = 0; j < lenDiff; j++)
rowCells.push('');
html += "<tr>\n";
for (j = 0; j < colCount; j++) {
var colHtml = convertSpans(trim(rowCells[j]), self.converter);
html += [" <td", align[j], ">", colHtml, "</td>\n"].join('');
}
html += "</tr>\n";
}
html += "</table>\n";
// replace html with placeholder until postConversion step
return self.hashExtraBlock(html);
}
return text;
};
/******************************************************************
* Fenced Code Blocks (gfm) *
******************************************************************/
// Find and convert gfm-inspired fenced code blocks into html.
Markdown.Extra.prototype.fencedCodeBlocks = function(text) {
function encodeCode(code) {
code = code.replace(/&/g, "&amp;");
code = code.replace(/</g, "&lt;");
code = code.replace(/>/g, "&gt;");
return code;
}
var self = this;
text = text.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g, function(match, m1, m2) {
var language = m1, codeblock = m2;
// adhere to specified options
var preclass = self.googleCodePrettify ? ' class="prettyprint"' : '';
var codeclass = '';
if (language) {
if (self.googleCodePrettify || self.highlightJs) {
// use html5 language- class names. supported by both prettify and highlight.js
codeclass = ' class="language-' + language + '"';
} else {
codeclass = ' class="' + language + '"';
}
}
var html = ['<pre', preclass, '><code', codeclass, '>',
encodeCode(codeblock), '</code></pre>'].join('');
// replace codeblock with placeholder until postConversion step
return self.hashExtraBlock(html);
});
return text;
};
Markdown.Extra.prototype.all = function(text) {
text = this.tables(text);
text = this.fencedCodeBlocks(text);
return text;
};
/******************************************************************
* Definition Lists *
******************************************************************/
// Find and convert markdown extra definition lists into html.
Markdown.Extra.prototype.definitionLists = function(text) {
var wholeList = new RegExp(
['(\\x02\\n?|\\n\\n)' ,
'(?:' ,
'(' , // $1 = whole list
'(' , // $2
'[ ]{0,3}' ,
'((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term
'\\n?' ,
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
'([\\s\\S]+?)' ,
'(' , // $4
'(?=\\0x03)' , // \z
'|' ,
'(?=' ,
'\\n{2,}' ,
'(?=\\S)' ,
'(?!' , // Negative lookahead for another term
'[ ]{0,3}' ,
'(?:\\S.*\\n)+?' , // defined term
'\\n?' ,
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
'(?!' , // Negative lookahead for another definition
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
')' ,
')' ,
')' ,
')'
].join(''),
'gm'
);
var self = this;
text = addAnchors(text);
text = text.replace(wholeList, function(match, pre, list) {
var result = trim(self.processDefListItems(list));
result = "<dl>\n" + result + "\n</dl>";
return pre + self.hashExtraBlock(result) + "\n\n";
});
return removeAnchors(text);
};
// Process the contents of a single definition list, splitting it
// into individual term and definition list items.
Markdown.Extra.prototype.processDefListItems = function(listStr) {
var self = this;
var dt = new RegExp(
['(\\x02\\n?|\\n\\n+)' , // leading line
'(' , // definition terms = $1
'[ ]{0,3}' , // leading whitespace
'(?![:][ ]|[ ])' , // negative lookahead for a definition
// mark (colon) or more whitespace
'(?:\\S.*\\n)+?' , // actual term (not whitespace)
')' ,
'(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed
].join(''), // with a definition mark
'gm'
);
var dd = new RegExp(
['\\n(\\n+)?' , // leading line = $1
'(' , // marker space = $2
'[ ]{0,3}' , // whitespace before colon
'[:][ ]+' , // definition mark (colon)
')' ,
'([\\s\\S]+?)' , // definition text = $3
'(?=\\n*' , // stop at next definition mark,
'(?:' , // next term or end of text
'\\n[ ]{0,3}[:][ ]|' ,
'<dt>|\\x03' , // \z
')' ,
')'
].join(''),
'gm'
);
listStr = addAnchors(listStr);
// trim trailing blank lines:
listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n");
// Process definition terms.
listStr = listStr.replace(dt, function(match, pre, termsStr) {
var terms = trim(termsStr).split("\n");
var text = '';
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
// process spans inside dt
term = convertSpans(trim(term), self.converter);
text += "\n<dt>" + term + "</dt>";
}
return text + "\n";
});
// Process actual definitions.
listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) {
if (leadingLine || def.match(/\n{2,}/)) {
// replace marker with the appropriate whitespace indentation
def = Array(markerSpace.length + 1).join(' ') + def;
// process markdown inside definition
// TODO?: currently doesn't apply extensions
def = outdent(def) + "\n\n";
def = "\n" + convertAll(def, self.converter) + "\n";
} else {
// convert span-level markdown inside definition
def = rtrim(def);
def = convertSpans(outdent(def), self.converter);
}
return "\n<dd>" + def + "</dd>\n";
});
return removeAnchors(listStr);
};
})();

View File

@@ -0,0 +1,108 @@
(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|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
// <a href="url..." optional title>|</a>
var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-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;
function sanitizeTag(tag) {
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_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

@@ -4,19 +4,23 @@
{% block extra_style %} {% block extra_style %}
{% if filetype == 'Sf' %} {% if filetype == 'Sf' %}
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}aloha-0.22.7/css/aloha.css" /> <link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}aloha-0.22.7/css/aloha.css" />
{% else %} {% endif %}
{% if filetype == 'Text' %}
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}codemirror/codemirror.css" /> <link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}codemirror/codemirror.css" />
<style type="text/css"> <style type="text/css">
.CodeMirror-focused pre.CodeMirror-cursor { .CodeMirror-focused pre.CodeMirror-cursor { visibility: visible; }
visibility: visible; .CodeMirror-scroll { height:auto; min-height:700px; }
}
.CodeMirror-scroll {
height:auto;
min-height:700px;
}
</style> </style>
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}codemirror/monokai.css" /> <link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}codemirror/monokai.css" />
{% endif %} {% endif %}
{% if filetype == 'Markdown' %}
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/pagedown.css" />
<style type="text/css">
#top-bar, #header, #edit-hd { display:none; }
#main { min-height:0; }
.withpd { padding:40px 96px; }
</style>
{% endif %}
<style type="text/css"> <style type="text/css">
#main { #main {
width:100%; width:100%;
@@ -40,12 +44,14 @@
background:#f4f4f4; background:#f4f4f4;
border-top:1px solid #ededed; border-top:1px solid #ededed;
} }
#sf, #md-view, #edit-tip { #sf, #md-edit, #edit-tip {
box-shadow:0 0 6px #ccc; box-shadow:0 0 6px #ccc;
min-height:620px;
border:1px solid #ccc; border:1px solid #ccc;
margin:0 auto; margin:0 auto;
} }
#sf {
min-height:620px;
}
#edit-tip { #edit-tip {
min-height:200px; min-height:200px;
padding:10px; padding:10px;
@@ -61,11 +67,7 @@
z-index:1010;/*make seaf image show below path-op*/ z-index:1010;/*make seaf image show below path-op*/
} }
.CodeMirror { .CodeMirror {
{% if filetype == 'Markdown' or fileext == 'txt' or fileext == 'text' %}
width:818px;
{% else %}
width:950px; width:950px;
{% endif %}
margin:0 auto; margin:0 auto;
box-shadow:0 0 6px #272822; box-shadow:0 0 6px #272822;
} }
@@ -85,7 +87,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</p> </p>
<div id="op-after-edit" class="fright hide"> <div id="op-after-edit" class="fright vh">
{% if filetype == 'Markdown' %} {% if filetype == 'Markdown' %}
<button id="source-code-btn" class="hide">{% trans "Continue editing" %}</button> <button id="source-code-btn" class="hide">{% trans "Continue editing" %}</button>
<button id="preview-btn">{% trans "Preview" %}</button> <button id="preview-btn">{% trans "Preview" %}</button>
@@ -94,7 +96,7 @@
<a href="{{ SITE_ROOT }}repo/{{ repo.id }}/files/?p={{ path }}" id="file-edit-cancel">{% trans "Cancel" %}</a> <a href="{{ SITE_ROOT }}repo/{{ repo.id }}/files/?p={{ path }}" id="file-edit-cancel">{% trans "Cancel" %}</a>
</div> </div>
</div> </div>
<div id="file-edit"> <div id="file-edit"{% if filetype == 'Markdown' %} class="hide"{% endif %}>
{% include 'snippets/file_encoding.html' %} {% include 'snippets/file_encoding.html' %}
{% if err %} {% if err %}
<div id="edit-tip" class="article"> <div id="edit-tip" class="article">
@@ -102,11 +104,74 @@
</div> </div>
{% else %} {% else %}
{% if file_content != None %} {% if file_content != None %}
{% if filetype == 'Text' or filetype == 'Markdown' %} {% if filetype == 'Text' %}
<textarea id="docu-view" class="hide">{{ file_content|escape }}</textarea> <textarea id="docu-view" class="hide">{{ file_content|escape }}</textarea>
{% if filetype == 'Markdown' %}
<div id="md-view" class="article hide"></div>{% comment %} for preview {%endcomment%}
{% endif %} {% endif %}
{% if filetype == 'Markdown' %}
<div id="md-edit">
<div id="wmd-button-bar"></div>
<textarea class="wmd-input" id="wmd-input">{{ file_content|escape }}</textarea>
<div id="wmd-preview" class="article hide"></div>
</div>
<table id="md-edit-help" class="hide">
<tbody>
<tr>
<th>{% trans "Enter this" %}</th>
<th>{% trans "To see this" %}</th>
</tr>
<tr>
<td><pre>**{% trans "bold" %}** {% trans "text" %}</pre></td>
<td><strong class="bold">{% trans "bold" %}</strong> {% trans "text" %}</td>
</tr>
<tr>
<td><pre>*{% trans "italics" %}* {% trans "text" %}</pre></td>
<td><span class="italic">{% trans "italics" %}</span> {% trans "text" %}</td>
</tr>
<tr>
<td><pre>{% trans "Header" %}
====== </pre></td>
<td>
<h3>{% trans "Header" %}</h3>
</td>
</tr>
<tr>
<td><pre>{% trans "Smaller header" %}
--------- </pre></td>
<td><h4>{% trans "Smaller header" %}</h4></td>
</tr>
<tr>
<td><pre>[{% trans "Link something" %}](http://cloud.seafile.com/)</pre></td>
<td><a href="http://cloud.seafile.com/">{% trans "Link something" %}</a></td>
</tr>
<tr>
<td><pre>![{% trans "alt text" %}](http://cloud.seafile.com/media/img/logo.png)</pre></td>
<td><img src="http://cloud.seafile.com/media/img/logo.png" alt="" /></td>
</tr>
<tr>
<td><pre>* {% trans "unordered list" %}
* {% trans "leave empty lines around" %}
</pre></td>
<td>
<ul>
<li>{% trans "unordered list" %}</li>
<li>{% trans "leave empty lines around" %}</li>
</ul>
</td>
</tr>
<tr>
<td><pre>1. {% trans "ordered list" %}
2. {% trans "leave empty lines" %}
</pre></td>
<td>
<ol>
<li>{% trans "ordered list" %}</li>
<li>{% trans "leave empty lines" %}</li>
</ol>
</td>
</tr>
</tbody></table>
{% endif %} {% endif %}
{% if filetype == 'Sf' %} {% if filetype == 'Sf' %}
@@ -131,6 +196,7 @@
{% block extra_script %} {% block extra_script %}
{% if not err and file_content != None %} {% if not err and file_content != None %}
{% if filetype == 'Sf' %} {% if filetype == 'Sf' %}
<script type="text/javascript" src="{{MEDIA_URL}}aloha-0.22.7/lib/require.js"></script> <script type="text/javascript" src="{{MEDIA_URL}}aloha-0.22.7/lib/require.js"></script>
<script type="text/javascript"> <script type="text/javascript">
@@ -143,12 +209,19 @@ switch($('#lang-context').data('lang')) {
} }
</script> </script>
<script type="text/javascript" src="{{MEDIA_URL}}aloha-0.22.7/lib/aloha.js" data-aloha-plugins="common/format, common/abbr, common/align, common/characterpicker, common/dom-to-xhtml, common/image, common/link, common/list, common/table, common/undo, common/ui, extra/textcolor"></script> <script type="text/javascript" src="{{MEDIA_URL}}aloha-0.22.7/lib/aloha.js" data-aloha-plugins="common/format, common/abbr, common/align, common/characterpicker, common/dom-to-xhtml, common/image, common/link, common/list, common/table, common/undo, common/ui, extra/textcolor"></script>
{% else %} {% endif %}
{% if filetype == 'Text' %}
<script type="text/javascript" src="{{MEDIA_URL}}codemirror/codemirror-2.36.js"></script> <script type="text/javascript" src="{{MEDIA_URL}}codemirror/codemirror-2.36.js"></script>
{% endif %}
{% if filetype == 'Markdown' %} {% if filetype == 'Markdown' %}
<script type="text/javascript" src="{{MEDIA_URL}}js/showdown.js"></script> <script type="text/javascript" src="{{MEDIA_URL}}js/Markdown.Converter.js"></script>
{% endif %} <script type="text/javascript" src="{{MEDIA_URL}}js/Markdown.Sanitizer.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/Markdown.Editor.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/Markdown.Extra.js"></script>
{% endif %} {% endif %}
{% endif %} {% endif %}
<script type="text/javascript"> <script type="text/javascript">
{% if not err and file_content != None %} {% if not err and file_content != None %}
@@ -170,12 +243,11 @@ Aloha.ready(function() {
$('#sf').aloha().focus(); $('#sf').aloha().focus();
} }
}); });
{% else %} {% endif %}
{% if filetype == 'Text' %}
var editor = CodeMirror.fromTextArea($('#docu-view')[0], { var editor = CodeMirror.fromTextArea($('#docu-view')[0], {
{% include "snippets/editor_set_mode.html" %} {% include "snippets/editor_set_mode.html" %}
{% if filetype == 'Markdown' %}
mode: 'markdown',
{% endif %}
theme: 'monokai', theme: 'monokai',
indentUnit: 4, indentUnit: 4,
lineNumbers: true, lineNumbers: true,
@@ -193,24 +265,41 @@ var editor = CodeMirror.fromTextArea($('#docu-view')[0], {
autofocus: true autofocus: true
}); });
{% endif %} {% endif %}
$('#op-after-edit').removeClass('hide');
{% if filetype == 'Markdown' %} {% if filetype == 'Markdown' %}
$('#source-code-btn').click(function() { var converter = Markdown.getSanitizingConverter();
$('#md-view, #source-code-btn').addClass('hide'); converter.hooks.chain("preBlockGamut", function (text, rbg) {
$('.CodeMirror, #preview-btn').removeClass('hide'); return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
editor.focus(); return "<blockquote>" + rbg(inner) + "</blockquote>\n";
}); });
$('#preview-btn').click(function() {
var content = editor.getValue();
var converter = new Showdown.converter();
$('.CodeMirror, #preview-btn').addClass('hide');
$('#md-view').html(converter.makeHtml(content)).removeClass('hide');
$('#md-view').children(':first').css('margin-top', '0');
$('#source-code-btn').removeClass('hide');
}); });
Markdown.Extra.init(converter, {extensions: ["fenced_code_gfm"]});
var editor = new Markdown.Editor(converter,'',{handler:mdEditHelp, title:'{% trans "Help" %}'});
editor.run();
var file_edit = $('#file-edit');
var file_edit_styles = {'min-height':0, 'padding-bottom':50, 'height':$(window).height() - $('#path-op').outerHeight() - parseInt(file_edit.css('padding-top')) - 51};
file_edit.css(file_edit_styles).removeClass('hide');
$('#wmd-input').css({'height':file_edit.height() - $('#file-enc-conf').outerHeight(true) - $('#wmd-button-bar').outerHeight() - parseInt($('#wmd-input').css('padding-top'))*2});
$('#preview-btn, #source-code-btn').click(function() {
$('#wmd-button-bar, #wmd-input, #wmd-preview, #source-code-btn, #preview-btn').toggleClass('hide');
$('#md-edit').toggleClass('withpd');
if (!$('#wmd-input').hasClass('hide')) {
file_edit.css(file_edit_styles);
} else {
file_edit.removeAttr('style');
}
});
$('#wmd-preview').children(':first').css('margin-top', '0');
function mdEditHelp() {
$('#md-edit-help').modal();
}
{% endif %} {% endif %}
$('#op-after-edit').removeClass('vh');
$('#file-edit-submit').click(function () { $('#file-edit-submit').click(function () {
disable($(this)); disable($(this));
editSubmit(); editSubmit();
@@ -218,9 +307,13 @@ $('#file-edit-submit').click(function () {
function editSubmit() { function editSubmit() {
{% if filetype == 'Sf' %} {% if filetype == 'Sf' %}
var content = $('#sf').html(); var content = $('#sf').html();
{% else %} {% endif %}
{% if filetype == 'Text' %}
var content = editor.getValue(); var content = editor.getValue();
{% endif %} {% endif %}
{% if filetype == 'Markdown' %}
var content = $('#wmd-input').val();
{% endif %}
$.ajax({ $.ajax({
type: "POST", type: "POST",

View File

@@ -39,11 +39,13 @@
{% endblock %} {% endblock %}
{% block extra_script %}{{ block.super }} {% block extra_script %}{{ block.super }}
<script type="text/javascript" src="{{MEDIA_URL}}js/showdown.js"></script> <script type="text/javascript" src="{{MEDIA_URL}}js/Markdown.Converter.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/Markdown.Sanitizer.js"></script>
<script type="text/javascript" src="{{MEDIA_URL}}js/Markdown.Extra.js"></script>
<script type="text/javascript"> <script type="text/javascript">
{% ifnotequal file_content None %} {% ifnotequal file_content None %}
var converter = new Showdown.converter(); var converter = new Markdown.getSanitizingConverter();
Markdown.Extra.init(converter, {extensions: ["fenced_code_gfm"]});
$('#md-view').html(converter.makeHtml('{{ file_content|escapejs }}')).children(':first').css('margin-top', '0'); $('#md-view').html(converter.makeHtml('{{ file_content|escapejs }}')).children(':first').css('margin-top', '0');
{% endifnotequal %} {% endifnotequal %}