27 changed files with 5748 additions and 632 deletions
@ -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: |
run: |
||||||
TEST_MODE=1 go run *.go |
go run *.go |
||||||
|
|
||||||
tests: |
tests: |
||||||
go run test/main.go |
go run test/main.go |
||||||
|
|
||||||
db: |
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; |
||||||
|
}; |
||||||
|
} |
||||||
|
})(); |
||||||
@ -1,5 +1,2 @@ |
|||||||
User-agent: * |
User-agent: * |
||||||
Disallow: /new |
Disallow: /* |
||||||
Disallow: /*edit |
|
||||||
Disallow: /*export |
|
||||||
Disallow: /*stats |
|
||||||
@ -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 @@ |
|||||||
|
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) |
|
||||||
} |
|
||||||
@ -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