27 changed files with 5748 additions and 632 deletions
@ -0,0 +1,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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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; |
||||
}; |
||||
} |
||||
})(); |
||||
@ -1,5 +1,2 @@
@@ -1,5 +1,2 @@
|
||||
User-agent: * |
||||
Disallow: /new |
||||
Disallow: /*edit |
||||
Disallow: /*export |
||||
Disallow: /*stats |
||||
Disallow: /* |
||||
@ -0,0 +1,23 @@
@@ -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}} |
||||
@ -0,0 +1,11 @@
@@ -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 @@
@@ -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) |
||||
} |
||||
@ -1,377 +0,0 @@
@@ -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() |
||||
} |
||||
Loading…
Reference in new issue