removed shit
added beautiful editor yolo
This commit is contained in:
@@ -5,3 +5,4 @@ database.sqlite-journal
|
||||
vendor
|
||||
database.sqlite
|
||||
notehub
|
||||
.idea
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
FROM golang:1.14.3-alpine
|
||||
WORKDIR /go/src/app
|
||||
RUN apk --no-cache add curl make sqlite gcc musl-dev git
|
||||
|
||||
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
COPY . .
|
||||
RUN dep ensure
|
||||
|
||||
RUN make db
|
||||
EXPOSE 3000
|
||||
CMD ["make", "run"]
|
||||
@@ -1,8 +1,9 @@
|
||||
run:
|
||||
TEST_MODE=1 go run *.go
|
||||
go run *.go
|
||||
|
||||
tests:
|
||||
go run test/main.go
|
||||
|
||||
db:
|
||||
echo 'CREATE TABLE "notes" (`id` VARCHAR(6) UNIQUE PRIMARY KEY, `text` TEXT, `published` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `edited` TIMESTAMP DEFAULT NULL, `password` VARCHAR(16), `views` INTEGER DEFAULT 0);' | sqlite3 database.sqlite
|
||||
mkdir data
|
||||
echo 'CREATE TABLE "notes" (`id` VARCHAR(6) UNIQUE PRIMARY KEY, `text` TEXT, `published` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `edited` TIMESTAMP DEFAULT NULL, `password` VARCHAR(16), `views` INTEGER DEFAULT 0);' | sqlite3 data/database.sqlite
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# Terms of Service
|
||||
|
||||
### 1. Terms
|
||||
|
||||
By accessing the web site at <u>https://notehub.org</u>, you are agreeing to be
|
||||
bound by these web site Terms and Conditions of Use, all applicable laws and
|
||||
regulations, and agree that you are responsible for compliance with any
|
||||
applicable local laws. If you do not agree with any of these terms, you are
|
||||
prohibited from using or accessing this site. The materials contained in this
|
||||
web site are protected by applicable copyright and trademark law.
|
||||
|
||||
### 2. Use License
|
||||
|
||||
a. Under this license you may not:
|
||||
1. attempt to __modify, manipulate__ or __extend__ any software contained on NoteHub's web site;
|
||||
2. create _any_ kind of content unlawful to __possess__, __produce__ or __distribute__ under your local law;
|
||||
3. create 'hop' notes containing a collection of links to external resources;
|
||||
|
||||
b. This license shall automatically terminate if you violate any of these
|
||||
restrictions and may be terminated by NoteHub at any time. Upon terminating
|
||||
your viewing of these materials or upon the termination of this license, you
|
||||
must destroy any downloaded materials in your possession whether in electronic
|
||||
or printed format.
|
||||
|
||||
### 3. Disclaimer
|
||||
|
||||
a. The materials on NoteHub's web site are provided "as is". NoteHub makes no
|
||||
warranties, expressed or implied, and hereby disclaims and negates all other
|
||||
warranties, including without limitation, implied warranties or conditions of
|
||||
merchantability, fitness for a particular purpose, or non-infringement of
|
||||
intellectual property or other violation of rights.
|
||||
|
||||
Further, NoteHub does not warrant or make any representations concerning the
|
||||
accuracy, likely results, or reliability of the use of the materials on its
|
||||
Internet web site or otherwise relating to such materials or on any sites
|
||||
linked to this site.
|
||||
|
||||
### 4. Limitations
|
||||
|
||||
In no event shall NoteHub or its suppliers be liable for any damages
|
||||
(including, without limitation, damages for loss of data or profit, or due to
|
||||
business interruption,) arising out of the use or inability to use the
|
||||
materials on NoteHub's Internet site, even if NoteHub or a NoteHub authorized
|
||||
representative has been notified orally or in writing of the possibility of
|
||||
such damage. Because some jurisdictions do not allow limitations on implied
|
||||
warranties, or limitations of liability for consequential or incidental
|
||||
damages, these limitations may not apply to you.
|
||||
|
||||
### 5. Revisions and Errata
|
||||
|
||||
The materials appearing on NoteHub's web site could include technical,
|
||||
typographical, or photographic errors. NoteHub does not warrant that any of the
|
||||
materials on its web site are accurate, complete, or current. NoteHub may make
|
||||
changes to the materials contained on its web site at any time without notice.
|
||||
NoteHub does not, however, make any commitment to update the materials.
|
||||
|
||||
### 6. Links
|
||||
|
||||
NoteHub has not reviewed all of the sites linked to its Internet web site and
|
||||
is not responsible for the contents of any such linked site. The inclusion of
|
||||
any link does not imply endorsement by NoteHub of the site. Use of any such
|
||||
linked web site is at the user's own risk.
|
||||
|
||||
### 7. Site Terms of Use Modifications
|
||||
|
||||
NoteHub may revise these terms of use for its web site at any time without
|
||||
notice. By using this web site you are agreeing to be bound by the then
|
||||
current version of these Terms and Conditions of Use.
|
||||
|
||||
### 8. Governing Law
|
||||
|
||||
Any claim relating to NoteHub's web site shall be governed by the laws of
|
||||
Germany without regard to its conflict of law provisions.
|
||||
|
||||
General Terms and Conditions applicable to Use of a Web Site.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,874 @@
|
||||
(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 *
|
||||
*****************************************************************/
|
||||
|
||||
// patch for ie7
|
||||
if (!Array.indexOf) {
|
||||
Array.prototype.indexOf = function(obj) {
|
||||
for (var i = 0; i < this.length; i++) {
|
||||
if (this[i] == obj) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
function convertSpans(text, extra) {
|
||||
return sanitizeHtml(convertAll(text, extra), inlineTags);
|
||||
}
|
||||
|
||||
// Convert internal markdown using the stock pagedown converter
|
||||
function convertAll(text, extra) {
|
||||
var result = extra.blockGamutHookCallback(text);
|
||||
// We need to perform these operations since we skip the steps in the converter
|
||||
result = unescapeSpecialChars(result);
|
||||
result = result.replace(/~D/g, "$$").replace(/~T/g, "~");
|
||||
result = extra.previousPostConversion(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Convert escaped special characters
|
||||
function processEscapesStep1(text) {
|
||||
// Markdown extra adds two escapable characters, `:` and `|`
|
||||
return text.replace(/\\\|/g, '~I').replace(/\\:/g, '~i');
|
||||
}
|
||||
function processEscapesStep2(text) {
|
||||
return text.replace(/~I/g, '|').replace(/~i/g, ':');
|
||||
}
|
||||
|
||||
// Duplicated from PageDown converter
|
||||
function unescapeSpecialChars(text) {
|
||||
// Swap back in all the special characters we've hidden.
|
||||
text = text.replace(/~E(\d+)E/g, function(wholeMatch, m1) {
|
||||
var charCodeToReplace = parseInt(m1);
|
||||
return String.fromCharCode(charCodeToReplace);
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
function slugify(text) {
|
||||
return text.toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
||||
.replace(/\-\-+/g, '-') // Replace multiple - with single -
|
||||
.replace(/^-+/, '') // Trim - from start of text
|
||||
.replace(/-+$/, ''); // Trim - from end of text
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
* 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 = [];
|
||||
|
||||
// Stores footnotes
|
||||
this.footnotes = {};
|
||||
this.usedFootnotes = [];
|
||||
|
||||
// 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 postNormalizationTransformations = [];
|
||||
var preBlockGamutTransformations = [];
|
||||
var postSpanGamutTransformations = [];
|
||||
var postConversionTransformations = ["unHashExtraBlocks"];
|
||||
|
||||
options = options || {};
|
||||
options.extensions = options.extensions || ["all"];
|
||||
if (contains(options.extensions, "all")) {
|
||||
options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "smartypants", "strikethrough", "newlines"];
|
||||
}
|
||||
preBlockGamutTransformations.push("wrapHeaders");
|
||||
if (contains(options.extensions, "attr_list")) {
|
||||
postNormalizationTransformations.push("hashFcbAttributeBlocks");
|
||||
preBlockGamutTransformations.push("hashHeaderAttributeBlocks");
|
||||
postConversionTransformations.push("applyAttributeBlocks");
|
||||
extra.attributeBlocks = true;
|
||||
}
|
||||
if (contains(options.extensions, "fenced_code_gfm")) {
|
||||
// This step will convert fcb inside list items and blockquotes
|
||||
preBlockGamutTransformations.push("fencedCodeBlocks");
|
||||
// This extra step is to prevent html blocks hashing and link definition/footnotes stripping inside fcb
|
||||
postNormalizationTransformations.push("fencedCodeBlocks");
|
||||
}
|
||||
if (contains(options.extensions, "tables")) {
|
||||
preBlockGamutTransformations.push("tables");
|
||||
}
|
||||
if (contains(options.extensions, "def_list")) {
|
||||
preBlockGamutTransformations.push("definitionLists");
|
||||
}
|
||||
if (contains(options.extensions, "footnotes")) {
|
||||
postNormalizationTransformations.push("stripFootnoteDefinitions");
|
||||
preBlockGamutTransformations.push("doFootnotes");
|
||||
postConversionTransformations.push("printFootnotes");
|
||||
}
|
||||
if (contains(options.extensions, "smartypants")) {
|
||||
postConversionTransformations.push("runSmartyPants");
|
||||
}
|
||||
if (contains(options.extensions, "strikethrough")) {
|
||||
postSpanGamutTransformations.push("strikethrough");
|
||||
}
|
||||
if (contains(options.extensions, "newlines")) {
|
||||
postSpanGamutTransformations.push("newlines");
|
||||
}
|
||||
|
||||
converter.hooks.chain("postNormalization", function(text) {
|
||||
return extra.doTransform(postNormalizationTransformations, text) + '\n';
|
||||
});
|
||||
|
||||
converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) {
|
||||
// Keep a reference to the block gamut callback to run recursively
|
||||
extra.blockGamutHookCallback = blockGamutHookCallback;
|
||||
text = processEscapesStep1(text);
|
||||
text = extra.doTransform(preBlockGamutTransformations, text) + '\n';
|
||||
text = processEscapesStep2(text);
|
||||
return text;
|
||||
});
|
||||
|
||||
converter.hooks.chain("postSpanGamut", function(text) {
|
||||
return extra.doTransform(postSpanGamutTransformations, text);
|
||||
});
|
||||
|
||||
// Keep a reference to the hook chain running before doPostConversion to apply on hashed extra blocks
|
||||
extra.previousPostConversion = converter.hooks.postConversion;
|
||||
converter.hooks.chain("postConversion", function(text) {
|
||||
text = extra.doTransform(postConversionTransformations, text);
|
||||
// Clear state vars that may use unnecessary memory
|
||||
extra.hashBlocks = [];
|
||||
extra.footnotes = {};
|
||||
extra.usedFootnotes = [];
|
||||
return 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;
|
||||
}
|
||||
|
||||
extra.converter = converter;
|
||||
|
||||
// Caller usually won't need this, but it's handy for testing.
|
||||
return extra;
|
||||
};
|
||||
|
||||
// Do transformations
|
||||
Markdown.Extra.prototype.doTransform = function(transformations, text) {
|
||||
for(var i = 0; i < transformations.length; i++)
|
||||
text = this[transformations[i]](text);
|
||||
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';
|
||||
};
|
||||
Markdown.Extra.prototype.hashExtraInline = function(block) {
|
||||
return '~X' + (this.hashBlocks.push(block) - 1) + 'X';
|
||||
};
|
||||
|
||||
// Replace placeholder blocks in `text` with their corresponding
|
||||
// html blocks in the hashBlocks array.
|
||||
Markdown.Extra.prototype.unHashExtraBlocks = function(text) {
|
||||
var self = this;
|
||||
function recursiveUnHash() {
|
||||
var hasHash = false;
|
||||
text = text.replace(/(?:<p>)?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) {
|
||||
hasHash = true;
|
||||
var key = parseInt(m1, 10);
|
||||
return self.hashBlocks[key];
|
||||
});
|
||||
if(hasHash === true) {
|
||||
recursiveUnHash();
|
||||
}
|
||||
}
|
||||
recursiveUnHash();
|
||||
return text;
|
||||
};
|
||||
|
||||
// Wrap headers to make sure they won't be in def lists
|
||||
Markdown.Extra.prototype.wrapHeaders = function(text) {
|
||||
function wrap(text) {
|
||||
return '\n' + text + '\n';
|
||||
}
|
||||
text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap);
|
||||
text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap);
|
||||
text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap);
|
||||
return text;
|
||||
};
|
||||
|
||||
|
||||
/******************************************************************
|
||||
* Attribute Blocks *
|
||||
*****************************************************************/
|
||||
|
||||
// TODO: use sentinels. Should we just add/remove them in doConversion?
|
||||
// TODO: better matches for id / class attributes
|
||||
var attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}";
|
||||
var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm");
|
||||
var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
|
||||
"(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead
|
||||
var fcbAttributes = new RegExp("^(```[^`\\n]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
|
||||
"(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm");
|
||||
|
||||
// Extract headers attribute blocks, move them above the element they will be
|
||||
// applied to, and hash them for later.
|
||||
Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) {
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// Extract FCB attribute blocks, move them above the element they will be
|
||||
// applied to, and hash them for later.
|
||||
Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) {
|
||||
// TODO: use sentinels. Should we just add/remove them in doConversion?
|
||||
// TODO: better matches for id / class attributes
|
||||
|
||||
var self = this;
|
||||
function attributeCallback(wholeMatch, pre, attr) {
|
||||
return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n";
|
||||
}
|
||||
|
||||
return text.replace(fcbAttributes, attributeCallback);
|
||||
};
|
||||
|
||||
Markdown.Extra.prototype.applyAttributeBlocks = function(text) {
|
||||
var self = this;
|
||||
var blockRe = new RegExp('<p>~XX(\\d+)XX</p>[\\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
|
||||
var alignspecs = separator.split(/ *[|] */);
|
||||
var align = [];
|
||||
for (var i = 0; i < alignspecs.length; i++) {
|
||||
var spec = alignspecs[i];
|
||||
if (spec.match(/^ *-+: *$/m))
|
||||
align[i] = ' align="right"';
|
||||
else if (spec.match(/^ *:-+: *$/m))
|
||||
align[i] = ' align="center"';
|
||||
else if (spec.match(/^ *:-+ *$/m))
|
||||
align[i] = ' 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);
|
||||
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);
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
/******************************************************************
|
||||
* Footnotes *
|
||||
*****************************************************************/
|
||||
|
||||
// Strip footnote, store in hashes.
|
||||
Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) {
|
||||
var self = this;
|
||||
|
||||
text = text.replace(
|
||||
/\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g,
|
||||
function(wholeMatch, m1, m2) {
|
||||
m1 = slugify(m1);
|
||||
m2 += "\n";
|
||||
m2 = m2.replace(/^[ ]{0,3}/g, "");
|
||||
self.footnotes[m1] = m2;
|
||||
return "\n";
|
||||
});
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
|
||||
// Find and convert footnotes references.
|
||||
Markdown.Extra.prototype.doFootnotes = function(text) {
|
||||
var self = this;
|
||||
if(self.isConvertingFootnote === true) {
|
||||
return text;
|
||||
}
|
||||
|
||||
var footnoteCounter = 0;
|
||||
text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) {
|
||||
var id = slugify(m1);
|
||||
var footnote = self.footnotes[id];
|
||||
if (footnote === undefined) {
|
||||
return wholeMatch;
|
||||
}
|
||||
footnoteCounter++;
|
||||
self.usedFootnotes.push(id);
|
||||
var html = '<a href="#fn:' + id + '" id="fnref:' + id
|
||||
+ '" title="See footnote" class="footnote">' + footnoteCounter
|
||||
+ '</a>';
|
||||
return self.hashExtraInline(html);
|
||||
});
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
// Print footnotes at the end of the document
|
||||
Markdown.Extra.prototype.printFootnotes = function(text) {
|
||||
var self = this;
|
||||
|
||||
if (self.usedFootnotes.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
text += '\n\n<div class="footnotes">\n<hr>\n<ol>\n\n';
|
||||
for(var i=0; i<self.usedFootnotes.length; i++) {
|
||||
var id = self.usedFootnotes[i];
|
||||
var footnote = self.footnotes[id];
|
||||
self.isConvertingFootnote = true;
|
||||
var formattedfootnote = convertSpans(footnote, self);
|
||||
delete self.isConvertingFootnote;
|
||||
text += '<li id="fn:'
|
||||
+ id
|
||||
+ '">'
|
||||
+ formattedfootnote
|
||||
+ ' <a href="#fnref:'
|
||||
+ id
|
||||
+ '" title="Return to article" class="reversefootnote">↩</a></li>\n\n';
|
||||
}
|
||||
text += '</ol>\n</div>';
|
||||
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, "&");
|
||||
code = code.replace(/</g, "<");
|
||||
code = code.replace(/>/g, ">");
|
||||
// These were escaped by PageDown before postNormalization
|
||||
code = code.replace(/~D/g, "$$");
|
||||
code = code.replace(/~T/g, "~");
|
||||
return code;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
text = text.replace(/(?:^|\n)```([^`\n]*)\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) {
|
||||
var language = trim(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;
|
||||
};
|
||||
|
||||
|
||||
/******************************************************************
|
||||
* SmartyPants *
|
||||
******************************************************************/
|
||||
|
||||
Markdown.Extra.prototype.educatePants = function(text) {
|
||||
var self = this;
|
||||
var result = '';
|
||||
var blockOffset = 0;
|
||||
// Here we parse HTML in a very bad manner
|
||||
text.replace(/(?:<!--[\s\S]*?-->)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) {
|
||||
var token = text.substring(blockOffset, offset);
|
||||
result += self.applyPants(token);
|
||||
self.smartyPantsLastChar = result.substring(result.length - 1);
|
||||
blockOffset = offset + wholeMatch.length;
|
||||
if(!m1) {
|
||||
// Skip commentary
|
||||
result += wholeMatch;
|
||||
return;
|
||||
}
|
||||
// Skip special tags
|
||||
if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) {
|
||||
m4 = self.educatePants(m4);
|
||||
}
|
||||
else {
|
||||
self.smartyPantsLastChar = m4.substring(m4.length - 1);
|
||||
}
|
||||
result += m1 + m2 + m3 + m4 + m5;
|
||||
});
|
||||
var lastToken = text.substring(blockOffset);
|
||||
result += self.applyPants(lastToken);
|
||||
self.smartyPantsLastChar = result.substring(result.length - 1);
|
||||
return result;
|
||||
};
|
||||
|
||||
function revertPants(wholeMatch, m1) {
|
||||
var blockText = m1;
|
||||
blockText = blockText.replace(/&\#8220;/g, "\"");
|
||||
blockText = blockText.replace(/&\#8221;/g, "\"");
|
||||
blockText = blockText.replace(/&\#8216;/g, "'");
|
||||
blockText = blockText.replace(/&\#8217;/g, "'");
|
||||
blockText = blockText.replace(/&\#8212;/g, "---");
|
||||
blockText = blockText.replace(/&\#8211;/g, "--");
|
||||
blockText = blockText.replace(/&\#8230;/g, "...");
|
||||
return blockText;
|
||||
}
|
||||
|
||||
Markdown.Extra.prototype.applyPants = function(text) {
|
||||
// Dashes
|
||||
text = text.replace(/---/g, "—").replace(/--/g, "–");
|
||||
// Ellipses
|
||||
text = text.replace(/\.\.\./g, "…").replace(/\.\s\.\s\./g, "…");
|
||||
// Backticks
|
||||
text = text.replace(/``/g, "“").replace (/''/g, "”");
|
||||
|
||||
if(/^'$/.test(text)) {
|
||||
// Special case: single-character ' token
|
||||
if(/\S/.test(this.smartyPantsLastChar)) {
|
||||
return "’";
|
||||
}
|
||||
return "‘";
|
||||
}
|
||||
if(/^"$/.test(text)) {
|
||||
// Special case: single-character " token
|
||||
if(/\S/.test(this.smartyPantsLastChar)) {
|
||||
return "”";
|
||||
}
|
||||
return "“";
|
||||
}
|
||||
|
||||
// Special case if the very first character is a quote
|
||||
// followed by punctuation at a non-word-break. Close the quotes by brute force:
|
||||
text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "’");
|
||||
text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "”");
|
||||
|
||||
// Special case for double sets of quotes, e.g.:
|
||||
// <p>He said, "'Quoted' words in a larger quote."</p>
|
||||
text = text.replace(/"'(?=\w)/g, "“‘");
|
||||
text = text.replace(/'"(?=\w)/g, "‘“");
|
||||
|
||||
// Special case for decade abbreviations (the '80s):
|
||||
text = text.replace(/'(?=\d{2}s)/g, "’");
|
||||
|
||||
// Get most opening single quotes:
|
||||
text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1‘");
|
||||
|
||||
// Single closing quotes:
|
||||
text = text.replace(/([^\s\[\{\(\-])'/g, "$1’");
|
||||
text = text.replace(/'(?=\s|s\b)/g, "’");
|
||||
|
||||
// Any remaining single quotes should be opening ones:
|
||||
text = text.replace(/'/g, "‘");
|
||||
|
||||
// Get most opening double quotes:
|
||||
text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1“");
|
||||
|
||||
// Double closing quotes:
|
||||
text = text.replace(/([^\s\[\{\(\-])"/g, "$1”");
|
||||
text = text.replace(/"(?=\s)/g, "”");
|
||||
|
||||
// Any remaining quotes should be opening ones.
|
||||
text = text.replace(/"/ig, "“");
|
||||
return text;
|
||||
};
|
||||
|
||||
// Find and convert markdown extra definition lists into html.
|
||||
Markdown.Extra.prototype.runSmartyPants = function(text) {
|
||||
this.smartyPantsLastChar = '';
|
||||
text = this.educatePants(text);
|
||||
// Clean everything inside html tags (some of them may have been converted due to our rough html parsing)
|
||||
text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants);
|
||||
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);
|
||||
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) + "\n";
|
||||
} else {
|
||||
// convert span-level markdown inside definition
|
||||
def = rtrim(def);
|
||||
def = convertSpans(outdent(def), self);
|
||||
}
|
||||
|
||||
return "\n<dd>" + def + "</dd>\n";
|
||||
});
|
||||
|
||||
return removeAnchors(listStr);
|
||||
};
|
||||
|
||||
|
||||
/***********************************************************
|
||||
* Strikethrough *
|
||||
************************************************************/
|
||||
|
||||
Markdown.Extra.prototype.strikethrough = function(text) {
|
||||
// Pretty much duplicated from _DoItalicsAndBold
|
||||
return text.replace(/([\W_]|^)~T~T(?=\S)([^\r]*?\S[\*_]*)~T~T([\W_]|$)/g,
|
||||
"$1<del>$2</del>$3");
|
||||
};
|
||||
|
||||
|
||||
/***********************************************************
|
||||
* New lines *
|
||||
************************************************************/
|
||||
|
||||
Markdown.Extra.prototype.newlines = function(text) {
|
||||
// We have to ignore already converted newlines and line breaks in sub-list items
|
||||
return text.replace(/(<(?:br|\/li)>)?\n/g, function(wholeMatch, previousTag) {
|
||||
return previousTag ? wholeMatch : " <br>\n";
|
||||
});
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -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(?: start="\d+")?|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;
|
||||
}
|
||||
})();
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,358 @@
|
||||
<!--
|
||||
#
|
||||
# Writing is an in-browser text editor, supporting LaTeX (MathJax) and Markdown, designed to be lightweight and, unlike some other similar solutions,
|
||||
# fast to display (no delay when writing, no flickering when writing math), as close as possible to the math.stackexchange.com editor.
|
||||
#
|
||||
# author: Joseph Ernest (twitter: @JosephErnest)
|
||||
# url: https://github.com/josephernest/writing
|
||||
# license: MIT license
|
||||
# based on: Pagedown (https://code.google.com/archive/p/pagedown/, https://github.com/balpha/pagedown)
|
||||
# Pagedown Extra (https://github.com/jmcmanus/pagedown-extra)
|
||||
# MathJax (https://www.mathjax.org/)
|
||||
# StackOverflow's editor (https://gist.github.com/gdalgas/a652bce3a173ddc59f66)
|
||||
#
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="fixedheight texroman">
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<title>Editor</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style type="text/css">
|
||||
@font-face { font-family: texroman; src: url(cmunrm.otf); font-weight: 400; font-style: normal; font-stretch: normal; }
|
||||
@font-face { font-family: texroman; src: url(cmunrb.otf); font-weight: 700; font-style: normal; font-stretch: normal; }
|
||||
|
||||
html { font-family: sans-serif; }
|
||||
* { margin: 0; padding: 0; border: 0; outline: 0; }
|
||||
.texroman { font-family: texroman !important; }
|
||||
.unselectable { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
|
||||
.fixedheight { height: 100%; }
|
||||
.column { padding: 20px; }
|
||||
#wmd-button-bar { display: none; }
|
||||
#wmd-input { float: left; box-sizing: border-box; width: 50%; resize: horizontal; font-size: 14px; border-right: 1px solid #ddd; height: 100%; overflow: y-scroll; }
|
||||
#wmd-preview { overflow-y: auto; overflow-x: hidden; font-size: 15px; height: 100%; box-sizing: border-box; min-height: 100vh; }
|
||||
#wmd-preview li { margin-left: 20px; }
|
||||
#wmd-preview code, #wmd-input { font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif; }
|
||||
#wmd-preview p code, #wmd-preview li code { background-color: #f5f5f5; white-space: pre-wrap; padding: 3px 5px; font-size: 13px; }
|
||||
#wmd-preview pre { background-color: #f5f5f5; padding: 5px; margin: 0.5em 0 1em; overflow-x: auto; word-wrap: normal; font-size: 13px; }
|
||||
#wmd-preview blockquote { background-color: #defcff; padding: 10px; margin: 0.5em 0 1em; overflow-x: auto; word-wrap: normal; border-left: 2px solid #0ae2f5; }
|
||||
#wmd-preview blockquote p { margin-bottom: 0; }
|
||||
#wmd-preview hr { background-color: #ddd; color: #ddd; height: 1px; margin-bottom: 15px; margin-top: 15px; }
|
||||
#wmd-preview h1 { margin-bottom: 0.5em; font-size: 1.4em; }
|
||||
#wmd-preview h2 { margin-bottom: 0.5em; font-size: 1.2em; }
|
||||
#wmd-preview h3 { margin-bottom: 0.5em; font-size: 1em; }
|
||||
#wmd-preview p { margin-bottom: 1em; line-height: 1.25; }
|
||||
#wmd-preview ul { margin-bottom: 1.5em; line-height: 1.25; }
|
||||
#wmd-preview img { max-width: 100%; max-height: 100%; }
|
||||
#wmd-preview table { display: block; width: 100%; overflow: auto; border-collapse: collapse; margin-bottom: 0.5em; }
|
||||
#wmd-preview table th { font-weight: bold; }
|
||||
#wmd-preview table th, #wmd-preview table td { padding: 6px 13px; border: 1px solid #ddd; }
|
||||
#wmd-preview table tr { background-color: #fff; border-top: 1px solid #ccc; }
|
||||
#wmd-preview table tr:nth-child(2n) { background-color: #f8f8f8; }
|
||||
#wmd-preview a { text-decoration: none; color: #07c; }
|
||||
#wmd-preview .pagebreak { border-bottom: 1px dashed #eee; padding-top: 20px; margin-bottom: 30px; }
|
||||
#helpicon { position: absolute; bottom: 0; left: 0; margin: 5px; color: #ccc; cursor: pointer; font-family: sans-serif; font-size: 15px; }
|
||||
#help { display: none; position: fixed; top: 10%; height: 70%; left: 25%; max-width: 50%; overflow: hidden; background-color: white; border: 1px solid #ccc; padding: 15px 30px 20px 30px; overflow-y: auto; }
|
||||
#help pre { word-wrap: break-word !important; white-space: pre-wrap !important; }
|
||||
#closeicon { position: fixed; top: 10%; left: 25%; margin: 5px 8px; color: #ccc; cursor: pointer; font-family: sans-serif; }
|
||||
#openFileInput { position: absolute; display: none; }
|
||||
|
||||
@media print {
|
||||
#helpicon { display: none; }
|
||||
#wmd-preview .pagebreak { opacity: 0; }
|
||||
}
|
||||
|
||||
.dark-mode #wmd-input, .dark-mode #wmd-preview { background-color: #212121; color: #FAFAFA; border-color: #757575;}
|
||||
.dark-mode #wmd-preview a, .dark-mode #help a, .dark-mode .wmd-prompt-dialog a { color: #90CAF9; }
|
||||
.dark-mode #wmd-preview p code, .dark-mode #wmd-preview li code, .dark-mode #wmd-preview pre { background-color: #424242; }
|
||||
.dark-mode #wmd-preview table th, .dark-mode #wmd-preview table td { border-color: #757575; }
|
||||
.dark-mode #wmd-preview table tr { background-color: #424242; border-color: #757575; }
|
||||
.dark-mode #help { background-color: #424242; border-color: #757575; color: #FAFAFA; }
|
||||
.dark-mode .wmd-prompt-dialog { background-color: #424242; color: #FAFAFA; }
|
||||
.dark-mode .wmd-prompt-dialog input { background-color: #212121; color: #FAFAFA; }
|
||||
.dark-mode #wmd-preview blockquote { background-color: #00796B; border-color: #004D40; }
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body class="fixedheight">
|
||||
<script type="text/javascript" src="Markdown.Converter.js"></script>
|
||||
<script type="text/javascript" src="Markdown.Sanitizer.js"></script>
|
||||
<script type="text/javascript" src="Markdown.Editor.js"></script>
|
||||
<script type="text/javascript" src="Markdown.Extra.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS_HTML-full"></script>
|
||||
<script type="text/javascript" src="mathjax-editing_writing.js"></script>
|
||||
<!-- <script type="text/javascript" src="jspdf.min.js"></script> -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
|
||||
<input id="openFileInput" type="file" />
|
||||
<div id="wmd-button-bar" class="wmd-button-bar"></div>
|
||||
<textarea id="wmd-input" class="column wmd-input" spellcheck="false"></textarea>
|
||||
<div id="wmd-preview" class="column wmd-preview">
|
||||
<noscript>This text editor requires JavaScript.</noscript>
|
||||
</div>
|
||||
<div id="helpicon" class="unselectable">?</div>
|
||||
<div id="help">
|
||||
<div id="closeicon" class="unselectable">X</div>
|
||||
<pre>
|
||||
<a href="https://github.com/josephernest/writing">Writing</a> is a lightweight distraction-free text editor.
|
||||
Write text on the left, and the result is displayed on the right.
|
||||
|
||||
Commands
|
||||
--------
|
||||
CTRL + D: toggle display mode (editor only, preview only or both-at-the-same-time)
|
||||
CTRL + P: print or export as PDF
|
||||
CTRL + S: save source code as .MD file
|
||||
CTRL + SHIFT + O: open .MD file
|
||||
CTRL + SHIFT + 7: send to the storage
|
||||
|
||||
CTRL + SHIFT + L: enable / disable LaTeX (i.e. math formulas)
|
||||
CTRL + SHIFT + D: toggle dark mode
|
||||
CTRL + SHIFT + R: toggle roman (LaTex-like) or sans-serif font
|
||||
CTRL + SHIFT + H: show this help dialog
|
||||
|
||||
F11: full-screen (in most browsers)
|
||||
|
||||
Markdown syntax
|
||||
---------------
|
||||
#Title
|
||||
##Subtitle
|
||||
This is *italic* and this is **bold**.
|
||||
This is a [link](http://www.example.com/) and this is an .
|
||||
Write code with `...` or by adding a 4-whitespace indent to the paragraph.
|
||||
> This is a quote.
|
||||
|
||||
LaTeX syntax
|
||||
------------
|
||||
This formula $x^2+1$ will be displayed inline.
|
||||
This formula $$x^2+1$$ will be displayed in a new paragraph.
|
||||
|
||||
Specific syntax
|
||||
---------------
|
||||
\pagebreak will trigger a pagebreak when printing / exporting to PDF.
|
||||
|
||||
About
|
||||
-----
|
||||
Made by <a href="https://twitter.com/JosephErnest">@JosephErnest</a>
|
||||
<a href="https://github.com/josephernest/writing">https://github.com/josephernest/writing</a>
|
||||
Uses <a href="https://code.google.com/archive/p/pagedown/">Pagedown</a>, <a href="https://github.com/jmcmanus/pagedown-extra">Pagedown Extra</a>, <a href="https://www.mathjax.org/">MathJax</a>, StackOverflow's <a href="https://gist.github.com/gdalgas/a652bce3a173ddc59f66">editor</a> code and the <a href="http://cm-unicode.sourceforge.net/">Computer Modern</a> font.
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
togglemathjax = function(enabled) {
|
||||
if (enabled) {
|
||||
if (!latexenabledonce)
|
||||
{
|
||||
MathJax.Hub.Config(
|
||||
{"HTML-CSS": { preferredFont: "TeX", availableFonts: ["STIX","TeX"], linebreaks: { automatic: true }, EqnChunk: (MathJax.Hub.Browser.isMobile ? 10 : 50) },
|
||||
tex2jax: { inlineMath: [ ["$", "$"], ["\\\\(","\\\\)"] ], displayMath: [ ["$$","$$"], ["\\[", "\\]"] ], processEscapes: true, ignoreClass: "tex2jax_ignore|dno" },
|
||||
TeX: { noUndefined: { attributes: { mathcolor: "red", mathbackground: "#FFEEEE", mathsize: "90%" } }, Macros: { href: "{}" } },
|
||||
messageStyle: "none", skipStartupTypeset: true });
|
||||
mjpd1.mathjaxEditing.prepareWmdForMathJax(editor, '', [["$", "$"]]);
|
||||
latexenabledonce = true;
|
||||
if (editor.refreshPreview !== undefined)
|
||||
editor.refreshPreview();
|
||||
}
|
||||
else {
|
||||
MathJax.Hub.queue.pending = 0;
|
||||
MathJax.Hub.Queue(["Typeset", MathJax.Hub, "wmd-preview"]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
MathJax.Hub.queue.pending = 1;
|
||||
if (editor.refreshPreview !== undefined)
|
||||
editor.refreshPreview();
|
||||
else {
|
||||
MathJax.Hub.Config({ skipStartupTypeset: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggledarkmode = function(enabled){
|
||||
$('body').toggleClass('dark-mode',enabled);
|
||||
}
|
||||
|
||||
if (localStorage.getItem("writing") !== null) {
|
||||
$('#wmd-input').val(localStorage.getItem("writing"));
|
||||
}
|
||||
|
||||
openFile = function(e) {
|
||||
readFile(e.target.files[0]);
|
||||
}
|
||||
|
||||
readFile = function(file){ // https://stackoverflow.com/a/26298948/1422096
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var contents = e.target.result;
|
||||
$('#wmd-input').val(contents); // display file content
|
||||
editor.refreshPreview();
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
document.getElementById('openFileInput').addEventListener('change', openFile, false);
|
||||
|
||||
$('body').on('drag dragstart dragend dragover dragenter dragleave drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
})
|
||||
.on('drop', function(e) {
|
||||
readFile(e.originalEvent.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
$('#wmd-input').on('input', function() {
|
||||
localStorage.setItem("writing", $('#wmd-input').val());
|
||||
});
|
||||
|
||||
$('#wmd-input').focus();
|
||||
|
||||
$('#helpicon').click(function() {
|
||||
$('#help').show();
|
||||
});
|
||||
|
||||
$('#closeicon, #wmd-input, #wmd-preview').click(function() {
|
||||
$('#help').hide();
|
||||
});
|
||||
|
||||
$(document).on('keydown', function(e) {
|
||||
if (e.keyCode == 80 && (e.ctrlKey || e.metaKey)) { // CTRL + P
|
||||
if (mode != 1) {
|
||||
mode = 1;
|
||||
$('#wmd-input').hide();
|
||||
$('#wmd-preview').show();
|
||||
$('body').removeClass('fixedheight');
|
||||
$('html').removeClass('fixedheight');
|
||||
toggledarkmode(false);
|
||||
e.preventDefault();
|
||||
window.print();
|
||||
toggledarkmode(darkmodeenabled);
|
||||
return false;
|
||||
}
|
||||
//var doc = new jsPDF();
|
||||
//var specialElementHandlers = {'#editor': function (element, renderer) { return true; } };
|
||||
//doc.fromHTML($('#wmd-preview').html(), 15, 15, { 'width': 170, 'elementHandlers': specialElementHandlers });
|
||||
//doc.save('file.pdf');
|
||||
/*var restorepage = $('body').html();
|
||||
var printcontent = $('#wmd-preview').clone();
|
||||
$('body').empty().html(printcontent);
|
||||
window.print();
|
||||
$('body').html(restorepage);
|
||||
e.preventDefault();
|
||||
return false;*/
|
||||
}
|
||||
else if (e.keyCode == 83 && (e.ctrlKey || e.metaKey)) { // CTRL + S
|
||||
var blob = new Blob([$('#wmd-input').val()], {type: 'text'}); // https://stackoverflow.com/a/33542499/1422096
|
||||
if (window.navigator.msSaveOrOpenBlob) {
|
||||
window.navigator.msSaveBlob(blob, 'newfile.md');
|
||||
}
|
||||
else {
|
||||
var elem = window.document.createElement('a');
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
elem.download = 'newfile.md';
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
else if (e.keyCode == 68 && (e.ctrlKey || e.metaKey) && !e.shiftKey) { // CTRL + D
|
||||
mode += 1; if (mode == 3) mode = 0;
|
||||
if (mode == 1) {
|
||||
$('#wmd-input').hide();
|
||||
$('#wmd-preview').show();
|
||||
$('body').removeClass('fixedheight');
|
||||
$('html').removeClass('fixedheight');
|
||||
}
|
||||
else if (mode == 2) {
|
||||
$('#wmd-preview').hide();
|
||||
$('#wmd-input').show().css('float', 'none').css('width', '100%').focus();
|
||||
$('body').addClass('fixedheight');
|
||||
$('html').addClass('fixedheight');
|
||||
}
|
||||
else {
|
||||
$('#wmd-input').show().css('float', 'left').css('width', '50%').focus();
|
||||
$('#wmd-preview').show();
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
else if (e.keyCode == 72 && (e.ctrlKey || e.metaKey) && e.shiftKey) { // CTRL + H
|
||||
$('#help').show();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
else if (e.keyCode == 55 && (e.ctrlKey || e.metaKey) && e.shiftKey) { // CTRL + SHIFT + 7
|
||||
const text = $('#wmd-input').val();
|
||||
let name = text.split("\n")[0].replaceAll("#", "")
|
||||
const xhr = new XMLHttpRequest();
|
||||
const cb = function (status, responseRaw) {
|
||||
const response = JSON.parse(responseRaw);
|
||||
if (status < 400 && response.Success) {
|
||||
localStorage.removeItem("writing");
|
||||
window.location.replace("/" + response.Payload)
|
||||
} else {
|
||||
$('feedback').innerHTML = status + ": " + response.Payload;
|
||||
}
|
||||
}
|
||||
xhr.open('POST', "/")
|
||||
xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) return cb(xhr.status, xhr.responseText) };
|
||||
const data = new FormData();
|
||||
data.append("text", text)
|
||||
data.append("name", name)
|
||||
xhr.send(data);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
else if (e.keyCode == 68 && (e.ctrlKey || e.metaKey) && e.shiftKey) { // CTRL + SHIFT + D
|
||||
darkmodeenabled = !darkmodeenabled;
|
||||
localStorage.setItem("darkmode", darkmodeenabled ? "1" : "0");
|
||||
toggledarkmode(darkmodeenabled);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
else if (e.keyCode == 82 && (e.ctrlKey || e.metaKey) && e.shiftKey) { // CTRL + SHIFT + R
|
||||
$('html').toggleClass('texroman');
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
else if (e.keyCode == 76 && (e.ctrlKey || e.metaKey) && e.shiftKey) { // CTRL + SHIFT + L
|
||||
latexenabled = !latexenabled;
|
||||
localStorage.setItem("latex", latexenabled ? "1" : "0");
|
||||
togglemathjax(latexenabled);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
else if (e.keyCode == 79 && (e.ctrlKey || e.metaKey) && e.shiftKey) { // CTRL + SHIFT + O
|
||||
$('#openFileInput').click();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
else if (e.keyCode == 27) { // ESC
|
||||
$('#help').hide();
|
||||
}
|
||||
});
|
||||
|
||||
var mode = 0;
|
||||
var latexenabledonce = false;
|
||||
var latexenabled = localStorage.getItem("latex") !== "0";
|
||||
var darkmodeenabled = localStorage.getItem("darkmode") == "1";
|
||||
var converter = Markdown.getSanitizingConverter();
|
||||
Markdown.Extra.init(converter);
|
||||
var editor = new Markdown.Editor(converter, '');
|
||||
var mjpd1 = new mjpd();
|
||||
togglemathjax(latexenabled);
|
||||
toggledarkmode(darkmodeenabled);
|
||||
editor.run();
|
||||
</script>
|
||||
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-2312083-14"></script><script>window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-2312083-14');</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,375 @@
|
||||
// Comes from: http://dev.stackoverflow.com/content/js/mathjax-editing.js (MIT-License)
|
||||
// Version downloaded 2016-11-21
|
||||
//
|
||||
// Two things modified:
|
||||
//
|
||||
// - StackExchange.mathjaxEditing = (function () {
|
||||
// + function mjpd() { this.mathjaxEditing = (function () {
|
||||
// - converterObject.hooks.chain("preSafe", replaceMath);
|
||||
// + converterObject.hooks.chain("postConversion", replaceMath);
|
||||
// - return { prepareWmdForMathJax: prepareWmdForMathJax };})();
|
||||
// + return { prepareWmdForMathJax: prepareWmdForMathJax } })(); }
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
function mjpd() {
|
||||
this.mathjaxEditing = (function () {
|
||||
var ready = false; // true after initial typeset is complete
|
||||
var pending = null; // non-null when typesetting has been queued
|
||||
var inline = "$"; // the inline math delimiter
|
||||
var blocks, start, end, last, braces, indent; // used in searching for math
|
||||
var math; // stores math until markdone is done
|
||||
var HUB = MathJax.Hub, TEX, NOERRORS;
|
||||
|
||||
//
|
||||
// Runs after initial typeset
|
||||
//
|
||||
HUB.Queue(function () {
|
||||
TEX = MathJax.InputJax.TeX;
|
||||
NOERRORS = TEX.config.noErrors;
|
||||
ready = true;
|
||||
HUB.processUpdateTime = 50; // reduce update time so that we can cancel easier
|
||||
HUB.processSectionDelay = 0; // don't pause between input and output phases
|
||||
MathJax.Extension["fast-preview"].Disable(); // disable fast-preview
|
||||
HUB.Config({
|
||||
// reduce chunk for more frequent updates
|
||||
"HTML-CSS": {
|
||||
EqnChunk: 10,
|
||||
EqnChunkFactor: 1
|
||||
},
|
||||
CommonHTML: {
|
||||
EqnChunk: 10,
|
||||
EqnChunkFactor: 1
|
||||
},
|
||||
SVG: {
|
||||
EqnChunk: 10,
|
||||
EqnChunkFactor: 1
|
||||
}
|
||||
});
|
||||
if (pending) return RestartMJ(pending, "Typeset");
|
||||
});
|
||||
|
||||
//
|
||||
// These get called before and after typsetting
|
||||
//
|
||||
function preTypeset() {
|
||||
NOERRORS.disabled = true; // disable noErrors (error will be shown)
|
||||
TEX.resetEquationNumbers(); // reset labels
|
||||
}
|
||||
function postTypeset() {
|
||||
NOERRORS.disabled = false; // don't show errors when not editing
|
||||
}
|
||||
|
||||
//
|
||||
// The pattern for math delimiters and special symbols
|
||||
// needed for searching for math in the page.
|
||||
//
|
||||
var SPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@|`+)/i;
|
||||
|
||||
//
|
||||
// The math is in blocks i through j, so
|
||||
// collect it into one block and clear the others.
|
||||
// Replace &, <, and > by named entities.
|
||||
// For IE, put <br> at the ends of comments since IE removes \n.
|
||||
// Clear the current math positions and store the index of the
|
||||
// math, then push the math string onto the storage array.
|
||||
//
|
||||
function processMath(i, j) {
|
||||
var block = blocks.slice(i, j + 1).join("")
|
||||
.replace(/&/g, "&") // use HTML entity for &
|
||||
.replace(/</g, "<") // use HTML entity for <
|
||||
.replace(/>/g, ">") // use HTML entity for >
|
||||
;
|
||||
if (indent) block = block.replace(/\n /g, "\n");
|
||||
if (HUB.Browser.isMSIE) {
|
||||
block = block.replace(/(%[^\n]*)\n/g, "$1<br/>\n");
|
||||
}
|
||||
while (j > i) blocks[j--] = "";
|
||||
blocks[i] = "@@" + math.length + "@@";
|
||||
math.push(block);
|
||||
start = end = last = null;
|
||||
}
|
||||
|
||||
|
||||
var capturingStringSplit;
|
||||
if ("aba".split(/(b)/).length === 3) {
|
||||
capturingStringSplit = function (str, regex) { return str.split(regex); };
|
||||
}
|
||||
else { // IE8
|
||||
capturingStringSplit = function (str, regex) {
|
||||
var result = [], match;
|
||||
if (!regex.global) {
|
||||
var source = regex.toString(),
|
||||
flags = "";
|
||||
source = source.replace(/^\/(.*)\/([im]*)$/, function (wholematch, re, fl) { flags = fl; return re; });
|
||||
regex = new RegExp(source, flags + "g");
|
||||
}
|
||||
regex.lastIndex = 0;
|
||||
var lastPos = 0;
|
||||
while ((match = regex.exec(str))) {
|
||||
result.push(str.substring(lastPos, match.index));
|
||||
result.push.apply(result, match.slice(1));
|
||||
lastPos = match.index + match[0].length;
|
||||
}
|
||||
result.push(str.substring(lastPos));
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Break up the text into its component parts and search
|
||||
// through them for math delimiters, braces, linebreaks, etc.
|
||||
// Math delimiters must match and braces must balance.
|
||||
// Don't allow math to pass through a double linebreak
|
||||
// (which will be a paragraph).
|
||||
// Handle backticks (don't do math inside them)
|
||||
//
|
||||
function removeMath(text) {
|
||||
start = end = last = indent = null; // for tracking math delimiters
|
||||
math = []; // stores math strings for latter
|
||||
|
||||
blocks = capturingStringSplit(text.replace(/\r\n?/g, "\n"), SPLIT);
|
||||
|
||||
for (var i = 1, m = blocks.length; i < m; i += 2) {
|
||||
var block = blocks[i];
|
||||
if (block.charAt(0) === "@") {
|
||||
//
|
||||
// Things that look like our math markers will get
|
||||
// stored and then retrieved along with the math.
|
||||
//
|
||||
blocks[i] = "@@" + math.length + "@@";
|
||||
math.push(block);
|
||||
}
|
||||
else if (start) {
|
||||
//
|
||||
// If we are in math or backticks,
|
||||
// look for the end delimiter,
|
||||
// but don't go past double line breaks,
|
||||
// and balance braces within the math,
|
||||
// but don't process math inside backticks.
|
||||
//
|
||||
if (block === end) {
|
||||
if (braces > 0) {
|
||||
last = i;
|
||||
}
|
||||
else if (braces === 0) {
|
||||
processMath(start, i);
|
||||
}
|
||||
else {
|
||||
start = end = last = null;
|
||||
}
|
||||
}
|
||||
else if (block.match(/\n.*\n/) || i + 2 >= m) {
|
||||
if (last) {
|
||||
i = last;
|
||||
if (braces >= 0) processMath(start, i);
|
||||
}
|
||||
start = end = last = null;
|
||||
braces = 0;
|
||||
}
|
||||
else if (block === "{" && braces >= 0) {
|
||||
braces++;
|
||||
}
|
||||
else if (block === "}" && braces > 0) {
|
||||
braces--;
|
||||
}
|
||||
}
|
||||
else {
|
||||
//
|
||||
// Look for math start delimiters and when
|
||||
// found, set up the end delimiter.
|
||||
//
|
||||
if (block === inline || block === "$$") {
|
||||
start = i;
|
||||
end = block;
|
||||
braces = 0;
|
||||
}
|
||||
else if (block.substr(1, 5) === "begin") {
|
||||
start = i;
|
||||
end = "\\end" + block.substr(6);
|
||||
braces = 0;
|
||||
}
|
||||
else if (block.charAt(0) === "`") {
|
||||
start = last = i;
|
||||
end = block;
|
||||
braces = -1; // no brace balancing
|
||||
}
|
||||
else if (block.charAt(0) === "\n") {
|
||||
if (block.match(/ $/)) indent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (last) processMath(start, last);
|
||||
return blocks.join("");
|
||||
}
|
||||
|
||||
//
|
||||
// Put back the math strings that were saved,
|
||||
// and clear the math array (no need to keep it around).
|
||||
//
|
||||
function replaceMath(text) {
|
||||
text = text.replace(/@@(\d+)@@/g, function (match, n) {
|
||||
return math[n];
|
||||
});
|
||||
math = null;
|
||||
return text;
|
||||
}
|
||||
|
||||
//
|
||||
// This is run to restart MathJax after it has finished
|
||||
// the previous run (that may have been canceled)
|
||||
//
|
||||
function RestartMJ(preview, method) {
|
||||
pending = false;
|
||||
HUB.cancelTypeset = false; // won't need to do this in the future
|
||||
HUB.Queue(
|
||||
preTypeset,
|
||||
[method, HUB, preview],
|
||||
postTypeset
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// When the preview changes, cancel MathJax and restart,
|
||||
// if we haven't done that already.
|
||||
//
|
||||
function UpdateMJ(preview, method) {
|
||||
if (!pending) {
|
||||
pending = preview;
|
||||
if (ready) {
|
||||
HUB.Cancel();
|
||||
HUB.Queue([RestartMJ, preview, method]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Save the preview ID and the inline math delimiter.
|
||||
// Create a converter for the editor and register a preConversion hook
|
||||
// to handle escaping the math.
|
||||
// Create a preview refresh hook to handle starting MathJax.
|
||||
// Check if any errors are being displayed (in case there were
|
||||
// errors in the initial display, which doesn't go through
|
||||
// onPreviewRefresh), and reprocess if there are.
|
||||
//
|
||||
function prepareWmdForMathJax(editorObject, wmdId, delimiters) {
|
||||
var preview = document.getElementById("wmd-preview" + wmdId);
|
||||
inline = delimiters[0][0];
|
||||
|
||||
var converterObject = editorObject.getConverter();
|
||||
converterObject.hooks.chain("preConversion", removeMath);
|
||||
converterObject.hooks.chain("postConversion", replaceMath);
|
||||
editorObject.hooks.chain("onPreviewRefresh", function () {
|
||||
UpdateMJ(preview, "Typeset");
|
||||
});
|
||||
|
||||
HUB.Queue(function () {
|
||||
if (preview && preview.querySelector(".mjx-noError")) {
|
||||
RestartMJ(preview, "Reprocess");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
prepareWmdForMathJax: prepareWmdForMathJax
|
||||
}
|
||||
})();
|
||||
}
|
||||
//
|
||||
// Set up MathJax to allow canceling of typesetting, if it
|
||||
// doesn't already have that.
|
||||
//
|
||||
(function () {
|
||||
var HUB = MathJax.Hub;
|
||||
|
||||
if (!HUB.Cancel) {
|
||||
|
||||
HUB.cancelTypeset = false;
|
||||
var CANCELMESSAGE = "MathJax Canceled";
|
||||
|
||||
HUB.Register.StartupHook("HTML-CSS Jax Config", function () {
|
||||
var HTMLCSS = MathJax.OutputJax["HTML-CSS"],
|
||||
TRANSLATE = HTMLCSS.Translate;
|
||||
HTMLCSS.Augment({
|
||||
Translate: function (script, state) {
|
||||
if (HUB.cancelTypeset || state.cancelled) {
|
||||
throw Error(CANCELMESSAGE)
|
||||
}
|
||||
return TRANSLATE.call(HTMLCSS, script, state);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
HUB.Register.StartupHook("SVG Jax Config", function () {
|
||||
var SVG = MathJax.OutputJax["SVG"],
|
||||
TRANSLATE = SVG.Translate;
|
||||
SVG.Augment({
|
||||
Translate: function (script, state) {
|
||||
if (HUB.cancelTypeset || state.cancelled) {
|
||||
throw Error(CANCELMESSAGE)
|
||||
}
|
||||
return TRANSLATE.call(SVG, script, state);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
HUB.Register.StartupHook("CommonHTML Jax Config", function () {
|
||||
var CHTML = MathJax.OutputJax.CommonHTML,
|
||||
TRANSLATE = CHTML.Translate;
|
||||
CHTML.Augment({
|
||||
Translate: function (script, state) {
|
||||
if (HUB.cancelTypeset || state.cancelled) {
|
||||
throw Error(CANCELMESSAGE);
|
||||
}
|
||||
return TRANSLATE.call(CHTML, script, state);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
HUB.Register.StartupHook("PreviewHTML Jax Config", function () {
|
||||
var PHTML = MathJax.OutputJax.PreviewHTML,
|
||||
TRANSLATE = PHTML.Translate;
|
||||
PHTML.Augment({
|
||||
Translate: function (script, state) {
|
||||
if (HUB.cancelTypeset || state.cancelled) {
|
||||
throw Error(CANCELMESSAGE);
|
||||
}
|
||||
return TRANSLATE.call(PHTML, script, state);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
HUB.Register.StartupHook("TeX Jax Config", function () {
|
||||
var TEX = MathJax.InputJax.TeX,
|
||||
TRANSLATE = TEX.Translate;
|
||||
TEX.Augment({
|
||||
Translate: function (script, state) {
|
||||
if (HUB.cancelTypeset || state.cancelled) {
|
||||
throw Error(CANCELMESSAGE)
|
||||
}
|
||||
return TRANSLATE.call(TEX, script, state);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var PROCESSERROR = HUB.processError;
|
||||
HUB.processError = function (error, state, type) {
|
||||
if (error.message !== CANCELMESSAGE) {
|
||||
return PROCESSERROR.call(HUB, error, state, type)
|
||||
}
|
||||
MathJax.Message.Clear(0, 0);
|
||||
state.jaxIDs = [];
|
||||
state.jax = {};
|
||||
state.scripts = [];
|
||||
state.i = state.j = 0;
|
||||
state.cancelled = true;
|
||||
return null;
|
||||
};
|
||||
|
||||
HUB.Cancel = function () {
|
||||
this.cancelTypeset = true;
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -8,34 +8,16 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="hero">
|
||||
<h1>NoteHub</h1>
|
||||
<h2>Pastebin for One-Off Markdown Publishing</h2>
|
||||
<br>
|
||||
<a class="landing-button demo" href="/demo" style="color: white">See Demo Note</a>
|
||||
<a class="landing-button" href="/new" style="color: white">New Note</a>
|
||||
<a class="landing-button" href="/list" style="color: white">All Notes</a>
|
||||
<br>
|
||||
<a class="landing-button" href="/editor" style="color: white">Nice Editor</a>
|
||||
|
||||
|
||||
</div>
|
||||
<div id="dashed-line"></div>
|
||||
<article class="bottom-space">
|
||||
<h2>Changelog</h2>
|
||||
<ul>
|
||||
<li><strong>2017-09</strong>: NoteHub 3.0 rewritten in Go.</li>
|
||||
<li><strong>2016-03</strong>: Note deletion feature added.</li>
|
||||
<li><strong>2015-10</strong>: NoteHub 2.0 rewritten in Node.js.</li>
|
||||
<li><strong>2015-10</strong>: NoteHub API and note styling discontinued due to low adoption.</li>
|
||||
<li><strong>2014-09</strong>: text size setting added</li>
|
||||
<li><strong>2014-07</strong>: deprecated all API versions less than 1.4 & performance improvements.</li>
|
||||
<li><strong>2014-03</strong>: note expiration implemented.</li>
|
||||
<li><strong>2014-02</strong>: a simple JS-client for API testing added.</li>
|
||||
<li><strong>2014-01</strong>: NoteHub API, mobile friendly styling and more.</li>
|
||||
<li><strong>2013-03</strong>: new color themes.</li>
|
||||
<li><strong>2012-07</strong>: password protection for note editing added.</li>
|
||||
<li><strong>2012-06</strong>: NoteHub 1.0 released as a result of an <a href="/story">experiment</a>.</li>
|
||||
</ul>
|
||||
</article>
|
||||
<footer>
|
||||
<a href="https://github.com/chmllr/notehub">source code</a> ·
|
||||
<a href="https://github.com/chmllr/notehub/issues">feedback</a> ·
|
||||
<a href="/TOS.md">terms of service</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+10
-13
@@ -2,28 +2,25 @@
|
||||
|
||||
function $(id) { return document.getElementById(id) }
|
||||
|
||||
function toggleButton() { $('publish-button').disabled = !$('tos').checked }
|
||||
|
||||
function submitForm(token) {
|
||||
var id = $("id").value;
|
||||
var text = $("text").value;
|
||||
var deletion = id != "" && text == "";
|
||||
function submitForm() {
|
||||
const id = $("id").value;
|
||||
const text = $("text").value;
|
||||
const name = $("name").value;
|
||||
const deletion = id !== "" && text === "";
|
||||
if (deletion && !confirm("Do you want to delete this note?")) {
|
||||
return;
|
||||
}
|
||||
var resp = post("/", {
|
||||
const resp = post("/", {
|
||||
"id": id,
|
||||
"text": text,
|
||||
"tos": $("tos").value,
|
||||
"password": $("password").value,
|
||||
"token": token
|
||||
"name": name,
|
||||
"password": $("password").value
|
||||
}, function (status, responseRaw) {
|
||||
var response = JSON.parse(responseRaw);
|
||||
const response = JSON.parse(responseRaw);
|
||||
if (status < 400 && response.Success) {
|
||||
window.location.replace(deletion ? "/" : "/" + response.Payload)
|
||||
} else {
|
||||
grecaptcha.reset();
|
||||
$('feedback').innerHTML = status + ": " + response.Payload;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
+3
-10
@@ -1,20 +1,13 @@
|
||||
"use strict";
|
||||
|
||||
function post(url, vals, cb) {
|
||||
var data = new FormData();
|
||||
for (var key in vals) {
|
||||
const data = new FormData();
|
||||
for (const key in vals) {
|
||||
data.append(key, vals[key]);
|
||||
}
|
||||
var xhr = new XMLHttpRequest();
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url)
|
||||
xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) return cb(xhr.status, xhr.responseText) };
|
||||
xhr.send(data);
|
||||
}
|
||||
|
||||
function report(id) {
|
||||
var resp = prompt("Please shortly explain the problem with this note.");
|
||||
if (resp) {
|
||||
post('/' + id + '/report', { "report": resp })
|
||||
alert("Thank you!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /new
|
||||
Disallow: /*edit
|
||||
Disallow: /*export
|
||||
Disallow: /*stats
|
||||
Disallow: /*
|
||||
+19
-31
@@ -9,7 +9,7 @@ html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #353a3a;
|
||||
background: #c5caca;
|
||||
background: #EBEBEB;
|
||||
}
|
||||
|
||||
#hero {
|
||||
@@ -18,24 +18,11 @@ html, body {
|
||||
padding-top: 5em;
|
||||
}
|
||||
|
||||
.ui-border {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
a {
|
||||
border-bottom: 1px dotted;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: #086;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #097;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #075;
|
||||
color: #52489C;
|
||||
}
|
||||
|
||||
.button {
|
||||
@@ -43,13 +30,13 @@ a:visited {
|
||||
}
|
||||
|
||||
.ui-elem {
|
||||
background: #fff;
|
||||
background: whitesmoke;
|
||||
font-size: 1em;
|
||||
opacity: 0.8;
|
||||
padding: 0.3em;
|
||||
border-radius: 3px;
|
||||
font-weight: 300;
|
||||
border: 1px solid #333;
|
||||
border: 3px black solid;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.landing-button {
|
||||
@@ -57,13 +44,15 @@ a:visited {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
white-space: nowrap;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0.5em;
|
||||
background: #0a6;
|
||||
font-size: 1.5em;
|
||||
text-decoration: none;
|
||||
font-weight: 300;
|
||||
color: #52489C;
|
||||
border: 3px solid black;
|
||||
background-color: #59C3C3;
|
||||
border-radius: 25px;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.landing-button.demo {
|
||||
@@ -73,7 +62,9 @@ a:visited {
|
||||
}
|
||||
|
||||
.landing-button:hover {
|
||||
background: #0b7;
|
||||
-webkit-box-shadow: 5px 5px 15px 5px rgba(0, 0, 0, 0.44);
|
||||
box-shadow: 5px 5px 15px 5px rgba(0, 0, 0, 0.44);
|
||||
transform: scale(1.12)
|
||||
}
|
||||
|
||||
.landing-button.demo:hover {
|
||||
@@ -136,10 +127,7 @@ h6 {
|
||||
|
||||
article {
|
||||
text-align: justify;
|
||||
margin-top: 3em;
|
||||
margin-bottom: 5em;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin: 3em auto 5em;
|
||||
}
|
||||
|
||||
article img {
|
||||
@@ -192,7 +180,7 @@ pre, code {
|
||||
font-family: monospace;
|
||||
font-size: 1.1em;
|
||||
border-radius: 3px;
|
||||
background-color: #b5baba;
|
||||
background-color: #EBEBEB;
|
||||
}
|
||||
|
||||
pre {
|
||||
@@ -205,7 +193,7 @@ table {
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #b5baba;
|
||||
background-color: #EBEBEB;
|
||||
line-height: 2.5em;
|
||||
padding: 0.3em;
|
||||
}
|
||||
@@ -221,16 +209,16 @@ td {
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: #b5baba;
|
||||
background-color: whitesmoke;
|
||||
font-size: 1.2em;
|
||||
border-radius: 5px;
|
||||
flex: 1 0;
|
||||
margin: 3em;
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
textarea, fieldset {
|
||||
border: none;
|
||||
border: 3px black solid;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<link href="/style.css" rel="stylesheet" type="text/css" />
|
||||
<script src='https://www.google.com/recaptcha/api.js'></script>
|
||||
<script src='/new.js'></script>
|
||||
<script src='/note.js'></script>
|
||||
</head>
|
||||
@@ -16,11 +15,12 @@
|
||||
<fieldset>
|
||||
<input id="id" value="{{.ID}}" type="hidden" />
|
||||
<input class="ui-elem" id="password" placeholder="Password for editing" type="text" autocomplete="off" />
|
||||
<label style="margin-right: 1em">
|
||||
<input id="tos" type="checkbox" onClick="toggleButton()" />
|
||||
Accept <a href="/TOS.md">Terms of Service</a>
|
||||
</label>
|
||||
<button class="button ui-elem" disabled id="publish-button" type="button" onclick="grecaptcha.execute()">
|
||||
{{if not .ID}}
|
||||
<input class="ui-elem" id="name" placeholder="Note name" type="text" autocomplete="off" />
|
||||
{{else}}
|
||||
<input class="ui-elem" id="name" placeholder="Note name" type="text" autocomplete="off" hidden value=""/>
|
||||
{{end}}
|
||||
<button class="button ui-elem" id="publish-button" type="button" onclick="submitForm()">
|
||||
{{if .ID}}Update{{else}}Publish{{end}} Note
|
||||
</button>
|
||||
<span id="feedback"></span>
|
||||
@@ -29,13 +29,8 @@
|
||||
<footer>
|
||||
<a href="/">⌂ notehub</a> ·
|
||||
<a href="https://github.com/chmllr/NoteHub">source code</a> ·
|
||||
<a href="/TOS.md">terms of service</a>
|
||||
</footer>
|
||||
<div class="g-recaptcha"
|
||||
data-sitekey="6LfamjEUAAAAAANI45H3fpWG_xaSAcpYhENN4EnO"
|
||||
data-callback="submitForm"
|
||||
data-size="invisible">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{{end}}
|
||||
@@ -0,0 +1,23 @@
|
||||
{{define "List"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>NoteHub List</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<link href="/style.css" rel="stylesheet" type="text/css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="hero">
|
||||
{{range .}}
|
||||
<a class="landing-button" style="color: white" href="/{{.ID}}">{{.ID }}</a>
|
||||
<br>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="dashed-line"></div>
|
||||
<footer>
|
||||
<a href="/">⌂ notehub</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -28,9 +28,7 @@
|
||||
<a href="/{{.ID}}/stats">stats</a> ·
|
||||
<a href="/{{.ID}}/edit">edit</a> ·
|
||||
<a href="/{{.ID}}/export">export</a> ·
|
||||
<a href="javascript:void(0)" onclick="report({{.ID}})">report abuse</a> ·
|
||||
{{end}}
|
||||
<a href="/TOS.md">tos</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
notes:
|
||||
build: .
|
||||
volumes:
|
||||
- data:/data
|
||||
ports:
|
||||
- "127.0.0.1:8877:3000"
|
||||
restart: always
|
||||
volumes:
|
||||
data:
|
||||
@@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
)
|
||||
|
||||
func email(id, text string) error {
|
||||
smtpServer := os.Getenv("SMTP_SERVER")
|
||||
auth := smtp.PlainAuth("", os.Getenv("SMTP_USER"), os.Getenv("SMTP_PASSWORD"), smtpServer)
|
||||
to := []string{os.Getenv("NOTEHUB_ADMIN_EMAIL")}
|
||||
msg := []byte("Subject: Note reported\r\n\r\n" +
|
||||
fmt.Sprintf("Note https://notehub.org/%s was reported: %q\r\n", id, text))
|
||||
return smtp.SendMail(smtpServer+":587", auth, to[0], to, msg)
|
||||
}
|
||||
@@ -29,6 +29,7 @@ var (
|
||||
|
||||
errorUnathorised = errors.New("password is wrong")
|
||||
errorBadRequest = errors.New("password is empty")
|
||||
errorNameExists = errors.New("name exists")
|
||||
)
|
||||
|
||||
func (n *Note) prepare() {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -19,7 +17,6 @@ import (
|
||||
"github.com/labstack/gommon/log"
|
||||
)
|
||||
|
||||
var TEST_MODE = false
|
||||
|
||||
type Template struct{ templates *template.Template }
|
||||
|
||||
@@ -31,14 +28,12 @@ func main() {
|
||||
e := echo.New()
|
||||
e.Logger.SetLevel(log.DEBUG)
|
||||
|
||||
db, err := sql.Open("sqlite3", "./database.sqlite")
|
||||
db, err := sql.Open("sqlite3", "./data/database.sqlite")
|
||||
if err != nil {
|
||||
e.Logger.Error(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
TEST_MODE = os.Getenv("TEST_MODE") != ""
|
||||
|
||||
adsFName := os.Getenv("ADS")
|
||||
var ads template.HTML
|
||||
if adsFName != "" {
|
||||
@@ -60,14 +55,15 @@ func main() {
|
||||
e.File("/note.js", "assets/public/note.js")
|
||||
e.File("/index.html", "assets/public/index.html")
|
||||
e.File("/", "assets/public/index.html")
|
||||
e.File("/Markdown.Converter.js", "assets/public/editor/Markdown.Converter.js")
|
||||
e.File("/Markdown.Editor.js", "assets/public/editor/Markdown.Editor.js")
|
||||
e.File("/Markdown.Extra.js", "assets/public/editor/Markdown.Extra.js")
|
||||
e.File("/Markdown.Sanitizer.js", "assets/public/editor/Markdown.Sanitizer.js")
|
||||
e.File("/mathjax-editing_writing.js", "assets/public/editor/mathjax-editing_writing.js")
|
||||
e.File("/cmunrb.otf", "assets/public/editor/cmunrb.otf")
|
||||
e.File("/cmunrm.otf", "assets/public/editor/cmunrm.otf")
|
||||
e.File("/editor", "assets/public/editor/index.html")
|
||||
|
||||
e.GET("/TOS.md", func(c echo.Context) error {
|
||||
n, code := md2html(c, "TOS")
|
||||
if code != http.StatusOK {
|
||||
c.String(code, statuses[code])
|
||||
}
|
||||
return c.Render(code, "Page", n)
|
||||
})
|
||||
|
||||
e.GET("/:id", func(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
@@ -130,9 +126,6 @@ func main() {
|
||||
if report != "" {
|
||||
id := c.Param("id")
|
||||
c.Logger().Infof("note %s was reported: %s", id, report)
|
||||
if err := email(id, report); err != nil {
|
||||
c.Logger().Errorf("couldn't send email: %v", err)
|
||||
}
|
||||
}
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
})
|
||||
@@ -142,6 +135,15 @@ func main() {
|
||||
return c.Render(http.StatusOK, "Form", nil)
|
||||
})
|
||||
|
||||
e.GET("/list", func(c echo.Context) error {
|
||||
c.Logger().Debug("GET /list")
|
||||
notes, err := loadAll(c, db)
|
||||
if err != http.StatusOK {
|
||||
return c.String(err, "Error happened")
|
||||
}
|
||||
return c.Render(http.StatusOK, "List", notes)
|
||||
})
|
||||
|
||||
type postResp struct {
|
||||
Success bool
|
||||
Payload string
|
||||
@@ -149,27 +151,13 @@ func main() {
|
||||
|
||||
e.POST("/", func(c echo.Context) error {
|
||||
c.Logger().Debug("POST /")
|
||||
if !TEST_MODE && !checkRecaptcha(c, c.FormValue("token")) {
|
||||
code := http.StatusForbidden
|
||||
return c.JSON(code, postResp{false, statuses[code] + ": robot check failed"})
|
||||
}
|
||||
if c.FormValue("tos") != "on" {
|
||||
code := http.StatusPreconditionFailed
|
||||
c.Logger().Errorf("POST / error: %d", code)
|
||||
return c.JSON(code, postResp{false, statuses[code]})
|
||||
}
|
||||
id := c.FormValue("id")
|
||||
text := c.FormValue("text")
|
||||
l := len(text)
|
||||
if (id == "" || id != "" && l != 0) && (10 > l || l > 50000) {
|
||||
code := http.StatusBadRequest
|
||||
c.Logger().Errorf("POST / error: %d", code)
|
||||
return c.JSON(code, postResp{false, statuses[code] + ": note length not accepted"})
|
||||
}
|
||||
n := &Note{
|
||||
ID: id,
|
||||
Text: text,
|
||||
Password: c.FormValue("password"),
|
||||
Name: c.FormValue("name"),
|
||||
}
|
||||
n, err = save(c, db, n)
|
||||
if err != nil {
|
||||
@@ -179,6 +167,8 @@ func main() {
|
||||
code = http.StatusUnauthorized
|
||||
} else if err == errorBadRequest {
|
||||
code = http.StatusBadRequest
|
||||
} else if err == errorNameExists {
|
||||
code = http.StatusBadRequest
|
||||
}
|
||||
c.Logger().Errorf("POST / error: %d", code)
|
||||
return c.JSON(code, postResp{false, statuses[code] + ": " + err.Error()})
|
||||
@@ -201,31 +191,3 @@ func main() {
|
||||
}
|
||||
e.Logger.Fatal(e.StartServer(s))
|
||||
}
|
||||
|
||||
func checkRecaptcha(c echo.Context, captchaResp string) bool {
|
||||
resp, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", url.Values{
|
||||
"secret": []string{os.Getenv("RECAPTCHA_SECRET")},
|
||||
"response": []string{captchaResp},
|
||||
"remoteip": []string{c.Request().RemoteAddr},
|
||||
})
|
||||
if err != nil {
|
||||
c.Logger().Errorf("captcha response verification failed: %v", err)
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respJson := &struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorCodes []string `json:"error-codes"`
|
||||
}{}
|
||||
s, err := ioutil.ReadAll(resp.Body)
|
||||
err = json.Unmarshal(s, respJson)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("captcha response parse recaptcha response: %v", err)
|
||||
return false
|
||||
}
|
||||
if !respJson.Success {
|
||||
c.Logger().Warnf("captcha validation failed: %v", respJson.ErrorCodes)
|
||||
}
|
||||
return respJson.Success
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,5 @@ func incViews(n *Note, db *sql.DB) {
|
||||
}
|
||||
}
|
||||
stats.Store(n.ID, views+1)
|
||||
if TEST_MODE {
|
||||
flush(db)
|
||||
}
|
||||
_, _ = flush(db)
|
||||
}
|
||||
|
||||
+39
-3
@@ -32,7 +32,7 @@ var (
|
||||
)
|
||||
|
||||
type Note struct {
|
||||
ID, Title, Text, Password, DeprecatedPassword, Encoded string
|
||||
ID, Title, Text, Password, DeprecatedPassword, Encoded, Name string
|
||||
Published, Edited time.Time
|
||||
Views int
|
||||
Content, Ads template.HTML
|
||||
@@ -109,13 +109,16 @@ func insert(c echo.Context, db *sql.DB, n *Note) (*Note, error) {
|
||||
}
|
||||
stmt, _ := tx.Prepare("insert into notes(id, text, password) values(?, ?, ?)")
|
||||
defer stmt.Close()
|
||||
id := randId()
|
||||
id := n.Name
|
||||
if id == "" {
|
||||
id = randId()
|
||||
}
|
||||
_, err = stmt.Exec(id, n.Text, n.Password)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") {
|
||||
c.Logger().Infof("collision on id %s", id)
|
||||
return save(c, db, n)
|
||||
return nil, errorNameExists
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -168,3 +171,36 @@ func load(c echo.Context, db *sql.DB) (*Note, int) {
|
||||
n.prepare()
|
||||
return n, http.StatusOK
|
||||
}
|
||||
|
||||
func loadAll(c echo.Context, db *sql.DB) ([]Note, int) {
|
||||
|
||||
c.Logger().Debug("loading notes")
|
||||
stmt, _ := db.Prepare("select * from notes")
|
||||
defer stmt.Close()
|
||||
rows, _ := stmt.Query()
|
||||
var notes []Note
|
||||
for rows.Next() {
|
||||
var id, text, password string
|
||||
var published time.Time
|
||||
var editedVal interface{}
|
||||
var views int
|
||||
if err := rows.Scan(&id, &text, &published, &editedVal, &password, &views); err != nil {
|
||||
code := http.StatusNotFound
|
||||
return nil, code
|
||||
}
|
||||
n := &Note{
|
||||
ID: id,
|
||||
Text: text,
|
||||
Views: views,
|
||||
Published: published,
|
||||
}
|
||||
if editedVal != nil {
|
||||
n.Edited = editedVal.(time.Time)
|
||||
}
|
||||
n.prepare()
|
||||
notes = append(notes, *n)
|
||||
}
|
||||
|
||||
|
||||
return notes, http.StatusOK
|
||||
}
|
||||
|
||||
-377
@@ -1,377 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
simplejson "github.com/bitly/go-simplejson"
|
||||
"github.com/verdverm/frisby"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
service := "http://localhost:3000"
|
||||
|
||||
frisby.Create("Test Notehub landing page").
|
||||
Get(service).
|
||||
Send().
|
||||
ExpectHeader("Content-Type", "text/html; charset=utf-8").
|
||||
ExpectStatus(200).
|
||||
ExpectContent("Pastebin for One-Off Markdown Publishing")
|
||||
|
||||
frisby.Create("Test Notehub TOS Page").
|
||||
Get(service+"/TOS.md").
|
||||
Send().
|
||||
ExpectHeader("Content-Type", "text/html; charset=UTF-8").
|
||||
ExpectStatus(200).
|
||||
ExpectContent("Terms of Service")
|
||||
|
||||
frisby.Create("Test /new page").
|
||||
Get(service+"/new").
|
||||
Send().
|
||||
ExpectHeader("Content-Type", "text/html; charset=UTF-8").
|
||||
ExpectStatus(200).
|
||||
ExpectContent("Publish Note")
|
||||
|
||||
frisby.Create("Test non-existing page").
|
||||
Get(service+"/xxyyzz").
|
||||
Send().
|
||||
ExpectStatus(404).
|
||||
ExpectHeader("Content-Type", "text/plain; charset=UTF-8").
|
||||
ExpectContent("Not found")
|
||||
|
||||
frisby.Create("Test non-existing page: query params").
|
||||
Get(service + "/xxyyzz?q=v"). // TODO: test the same for valid note
|
||||
Send().
|
||||
ExpectStatus(404).
|
||||
ExpectContent("Not found")
|
||||
|
||||
frisby.Create("Test non-existing page: alphabet violation").
|
||||
Get(service + "/login.php").
|
||||
Send().
|
||||
ExpectStatus(404).
|
||||
ExpectContent("Not found")
|
||||
|
||||
frisby.Create("Test publishing: no input").
|
||||
Post(service+"/").
|
||||
Send().
|
||||
ExpectStatus(412).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Precondition failed")
|
||||
|
||||
frisby.Create("Test publishing attempt: only TOS set").
|
||||
Post(service+"/").
|
||||
SetData("tos", "on").
|
||||
Send().
|
||||
ExpectStatus(400).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Bad request: note length not accepted")
|
||||
|
||||
testNote := "# Hello World!\nThis is a _test_ note!"
|
||||
|
||||
tooLongNote := testNote
|
||||
for len(tooLongNote) < 50000 {
|
||||
tooLongNote += tooLongNote
|
||||
}
|
||||
|
||||
frisby.Create("Test publishing: too long note").
|
||||
Post(service+"/").
|
||||
SetData("tos", "on").
|
||||
SetData("text", tooLongNote).
|
||||
Send().
|
||||
ExpectStatus(400).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Bad request: note length not accepted")
|
||||
|
||||
var id string
|
||||
frisby.Create("Test publishing: correct inputs; no password").
|
||||
Post(service+"/").
|
||||
SetData("tos", "on").
|
||||
SetData("text", testNote).
|
||||
Send().
|
||||
ExpectStatus(201).
|
||||
ExpectJson("Success", true).
|
||||
AfterJson(func(F *frisby.Frisby, json *simplejson.Json, err error) {
|
||||
noteID, err := json.Get("Payload").String()
|
||||
if err != nil {
|
||||
F.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
id = noteID
|
||||
})
|
||||
|
||||
testNoteHTML := "<h1>Hello World!</h1>\n<p>This is a <em>test</em> note!</p>"
|
||||
frisby.Create("Test retrieval of new note").
|
||||
Get(service + "/" + id).
|
||||
Send().
|
||||
// simulate 3 requests (for stats)
|
||||
Get(service + "/" + id).
|
||||
Send().
|
||||
Get(service + "/" + id).
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectContent(testNoteHTML)
|
||||
|
||||
frisby.Create("Test export of new note").
|
||||
Get(service+"/"+id+"/export").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectHeader("Content-type", "text/plain; charset=UTF-8").
|
||||
ExpectContent(testNote)
|
||||
|
||||
frisby.Create("Test opening fake service on note").
|
||||
Get(service + "/" + id + "/asd").
|
||||
Send().
|
||||
ExpectStatus(404).
|
||||
ExpectContent("Not Found")
|
||||
|
||||
// TODO: fix this
|
||||
// frisby.Create("Test opening fake service on note 2").
|
||||
// Get(service + "/" + id + "/exports").
|
||||
// Send().
|
||||
// ExpectStatus(404).
|
||||
// ExpectContent("Not Found")
|
||||
|
||||
frisby.Create("Test stats of new note").
|
||||
Get(service + "/" + id + "/stats").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectContent("Views: 4").
|
||||
ExpectContent("Published")
|
||||
|
||||
frisby.Create("Test edit page of new note").
|
||||
Get(service+"/"+id+"/edit").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectHeader("Content-type", "text/html; charset=UTF-8").
|
||||
ExpectContent(testNote)
|
||||
|
||||
frisby.Create("Test invalid editing attempt: empty inputs").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
Send().
|
||||
ExpectStatus(412)
|
||||
|
||||
frisby.Create("Test invalid editing attempt: tos only").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
Send().
|
||||
ExpectStatus(400).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Bad request: password is empty")
|
||||
|
||||
frisby.Create("Test invalid editing attempt: tos and password").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
SetData("password", "aazzss").
|
||||
Send().
|
||||
ExpectStatus(401).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Unauthorized: password is wrong")
|
||||
|
||||
frisby.Create("Test invalid editing attempt: tos and password, but too short note").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
SetData("text", "Test").
|
||||
SetData("password", "aazzss").
|
||||
Send().
|
||||
ExpectStatus(400).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Bad request: note length not accepted")
|
||||
|
||||
frisby.Create("Test publishing: correct inputs; with password").
|
||||
Post(service+"/").
|
||||
SetData("tos", "on").
|
||||
SetData("password", "aa11qq").
|
||||
SetData("text", testNote).
|
||||
Send().
|
||||
ExpectStatus(201).
|
||||
ExpectJson("Success", true).
|
||||
AfterJson(func(F *frisby.Frisby, json *simplejson.Json, err error) {
|
||||
noteID, err := json.Get("Payload").String()
|
||||
if err != nil {
|
||||
F.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
id = noteID
|
||||
})
|
||||
|
||||
updatedNote := strings.Replace(testNote, "is a", "is an updated", -1)
|
||||
frisby.Create("Test invalid editing attempt: tos only").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
Send().
|
||||
ExpectStatus(400).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Bad request: password is empty")
|
||||
|
||||
frisby.Create("Test invalid editing attempt: tos and wrong password").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
SetData("text", updatedNote).
|
||||
SetData("password", "aazzss").
|
||||
Send().
|
||||
ExpectStatus(401).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Unauthorized: password is wrong")
|
||||
|
||||
frisby.Create("Test editing: valid inputs, no tos").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("text", updatedNote).
|
||||
SetData("password", "aa11qq").
|
||||
Send().
|
||||
ExpectStatus(412).
|
||||
ExpectJson("Success", false).
|
||||
ExpectJson("Payload", "Precondition failed")
|
||||
|
||||
frisby.Create("Test editing: valid inputs").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
SetData("text", updatedNote).
|
||||
SetData("password", "aa11qq").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectJson("Success", true)
|
||||
|
||||
frisby.Create("Test retrieval of updated note").
|
||||
Get(service + "/" + id).
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectContent(strings.Replace(testNoteHTML, "is a", "is an updated", -1))
|
||||
|
||||
frisby.Create("Test export of new note").
|
||||
Get(service+"/"+id+"/export").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectHeader("Content-type", "text/plain; charset=UTF-8").
|
||||
ExpectContent(updatedNote)
|
||||
|
||||
frisby.Create("Test deletion: valid inputs").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
SetData("text", "").
|
||||
SetData("password", "aa11qq").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectJson("Success", true)
|
||||
|
||||
frisby.Create("Test retrieval of deleted note").
|
||||
Get(service + "/" + id).
|
||||
Send().
|
||||
ExpectStatus(404)
|
||||
|
||||
fraudNote := "http://n.co https://a.co ftp://b.co"
|
||||
|
||||
frisby.Create("Test publishing fraudulent note").
|
||||
Post(service+"/").
|
||||
SetData("tos", "on").
|
||||
SetData("password", "aa22qq").
|
||||
SetData("text", fraudNote).
|
||||
Send().
|
||||
ExpectStatus(201).
|
||||
ExpectJson("Success", true).
|
||||
AfterJson(func(F *frisby.Frisby, json *simplejson.Json, err error) {
|
||||
noteID, err := json.Get("Payload").String()
|
||||
if err != nil {
|
||||
F.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
id = noteID
|
||||
})
|
||||
|
||||
frisby.Create("Test new fraudulent note").
|
||||
Get(service+"/"+id).
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectHeader("Content-type", "text/html; charset=UTF-8").
|
||||
ExpectContent(`<a href="http://n.co">http://n.co</a> <a href="https://a.co">https://a.co</a> <a href="ftp://b.co">ftp://b.co</a>`)
|
||||
|
||||
frisby.Create("Test export of fraudulent note").
|
||||
Get(service+"/"+id+"/export").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectHeader("Content-type", "text/plain; charset=UTF-8").
|
||||
ExpectContent(fraudNote)
|
||||
|
||||
// access fraudulent note more than 100 times
|
||||
f := frisby.Create("Test export of fraudulent note again")
|
||||
for i := 0; i < 100; i++ {
|
||||
f.Get(service + "/" + id).Send()
|
||||
}
|
||||
|
||||
frisby.Create("Test stats of fradulent note").
|
||||
Get(service + "/" + id + "/stats").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectContent("Views: 102").
|
||||
ExpectContent("Published")
|
||||
|
||||
frisby.Create("Test export of fraudulent note").
|
||||
Get(service+"/"+id+"/export").
|
||||
Send().
|
||||
ExpectStatus(403).
|
||||
ExpectHeader("Content-type", "text/plain; charset=UTF-8").
|
||||
ExpectContent("Forbidden")
|
||||
|
||||
frisby.Create("Test deletion of fraudulent note: wrong password inputs").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
SetData("text", "").
|
||||
SetData("password", "aa11qq").
|
||||
Send().
|
||||
ExpectStatus(401).
|
||||
ExpectJson("Success", false)
|
||||
|
||||
frisby.Create("Test deletion of fraudulent note: correct password inputs").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
SetData("text", "").
|
||||
SetData("password", "aa22qq").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectJson("Success", true)
|
||||
|
||||
frisby.Create("Test publishing malicious note").
|
||||
Post(service+"/").
|
||||
SetData("tos", "on").
|
||||
SetData("password", "qwerty").
|
||||
SetData("text", "Foo <script>alert(1)</script> Bar <iframe src=''></iframe>").
|
||||
Send().
|
||||
ExpectStatus(201).
|
||||
ExpectJson("Success", true).
|
||||
AfterJson(func(F *frisby.Frisby, json *simplejson.Json, err error) {
|
||||
noteID, err := json.Get("Payload").String()
|
||||
if err != nil {
|
||||
F.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
id = noteID
|
||||
})
|
||||
|
||||
frisby.Create("Test export of fraudulent note").
|
||||
Get(service + "/" + id).
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectContent("Foo Bar")
|
||||
|
||||
frisby.Create("Test deletion of malicious note").
|
||||
Post(service+"/").
|
||||
SetData("id", id).
|
||||
SetData("tos", "on").
|
||||
SetData("text", "").
|
||||
SetData("password", "qwerty").
|
||||
Send().
|
||||
ExpectStatus(200).
|
||||
ExpectJson("Success", true)
|
||||
|
||||
frisby.Global.PrintReport()
|
||||
}
|
||||
Reference in New Issue
Block a user