Browse Source

removed shit

added beautiful editor
yolo
master
Yaroslav 5 years ago
parent
commit
ecd7a89402
  1. 1
      .gitignore
  2. 11
      Dockerfile
  3. 5
      Makefile
  4. 76
      assets/markdown/TOS.md
  5. 1631
      assets/public/editor/Markdown.Converter.js
  6. 2245
      assets/public/editor/Markdown.Editor.js
  7. 874
      assets/public/editor/Markdown.Extra.js
  8. 108
      assets/public/editor/Markdown.Sanitizer.js
  9. BIN
      assets/public/editor/cmunrb.otf
  10. BIN
      assets/public/editor/cmunrm.otf
  11. 358
      assets/public/editor/index.html
  12. 375
      assets/public/editor/mathjax-editing_writing.js
  13. 28
      assets/public/index.html
  14. 23
      assets/public/new.js
  15. 13
      assets/public/note.js
  16. 5
      assets/public/robots.txt
  17. 50
      assets/public/style.css
  18. 19
      assets/templates/form.html
  19. 23
      assets/templates/list.html
  20. 2
      assets/templates/note.html
  21. 11
      docker-compose.yml
  22. 16
      email.go
  23. 1
      render.go
  24. 80
      server.go
  25. 4
      stats.go
  26. 42
      storage.go
  27. 377
      test/main.go

1
.gitignore vendored

@ -5,3 +5,4 @@ database.sqlite-journal
vendor vendor
database.sqlite database.sqlite
notehub notehub
.idea

11
Dockerfile

@ -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"]

5
Makefile

@ -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

76
assets/markdown/TOS.md

@ -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.

1631
assets/public/editor/Markdown.Converter.js

File diff suppressed because it is too large Load Diff

2245
assets/public/editor/Markdown.Editor.js

File diff suppressed because it is too large Load Diff

874
assets/public/editor/Markdown.Extra.js

@ -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">&#8617;</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, "&amp;");
code = code.replace(/</g, "&lt;");
code = code.replace(/>/g, "&gt;");
// 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, "&#8212;").replace(/--/g, "&#8211;");
// Ellipses
text = text.replace(/\.\.\./g, "&#8230;").replace(/\.\s\.\s\./g, "&#8230;");
// Backticks
text = text.replace(/``/g, "&#8220;").replace (/''/g, "&#8221;");
if(/^'$/.test(text)) {
// Special case: single-character ' token
if(/\S/.test(this.smartyPantsLastChar)) {
return "&#8217;";
}
return "&#8216;";
}
if(/^"$/.test(text)) {
// Special case: single-character " token
if(/\S/.test(this.smartyPantsLastChar)) {
return "&#8221;";
}
return "&#8220;";
}
// 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)/, "&#8217;");
text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "&#8221;");
// Special case for double sets of quotes, e.g.:
// <p>He said, "'Quoted' words in a larger quote."</p>
text = text.replace(/"'(?=\w)/g, "&#8220;&#8216;");
text = text.replace(/'"(?=\w)/g, "&#8216;&#8220;");
// Special case for decade abbreviations (the '80s):
text = text.replace(/'(?=\d{2}s)/g, "&#8217;");
// Get most opening single quotes:
text = text.replace(/(\s|&nbsp;|--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1&#8216;");
// Single closing quotes:
text = text.replace(/([^\s\[\{\(\-])'/g, "$1&#8217;");
text = text.replace(/'(?=\s|s\b)/g, "&#8217;");
// Any remaining single quotes should be opening ones:
text = text.replace(/'/g, "&#8216;");
// Get most opening double quotes:
text = text.replace(/(\s|&nbsp;|--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1&#8220;");
// Double closing quotes:
text = text.replace(/([^\s\[\{\(\-])"/g, "$1&#8221;");
text = text.replace(/"(?=\s)/g, "&#8221;");
// Any remaining quotes should be opening ones.
text = text.replace(/"/ig, "&#8220;");
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";
});
};
})();

108
assets/public/editor/Markdown.Sanitizer.js

@ -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;
}
})();

BIN
assets/public/editor/cmunrb.otf

Binary file not shown.

BIN
assets/public/editor/cmunrm.otf

Binary file not shown.

358
assets/public/editor/index.html

@ -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 ![image](imagelink.jpg).
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>

375
assets/public/editor/mathjax-editing_writing.js

@ -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, "&amp;") // use HTML entity for &
.replace(/</g, "&lt;") // use HTML entity for <
.replace(/>/g, "&gt;") // 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;
};
}
})();

28
assets/public/index.html

@ -8,34 +8,16 @@
</head> </head>
<body> <body>
<div id="hero"> <div id="hero">
<h1>NoteHub</h1>
<h2>Pastebin for One-Off Markdown Publishing</h2>
<br>
<a class="landing-button demo" href="/demo" style="color: white">See Demo Note</a>
<a class="landing-button" href="/new" style="color: white">New Note</a> <a class="landing-button" href="/new" style="color: white">New Note</a>
<a class="landing-button" href="/list" style="color: white">All Notes</a>
<br>
<a class="landing-button" href="/editor" style="color: white">Nice Editor</a>
</div> </div>
<div id="dashed-line"></div> <div id="dashed-line"></div>
<article class="bottom-space">
<h2>Changelog</h2>
<ul>
<li><strong>2017-09</strong>: NoteHub 3.0 rewritten in Go.</li>
<li><strong>2016-03</strong>: Note deletion feature added.</li>
<li><strong>2015-10</strong>: NoteHub 2.0 rewritten in Node.js.</li>
<li><strong>2015-10</strong>: NoteHub API and note styling discontinued due to low adoption.</li>
<li><strong>2014-09</strong>: text size setting added</li>
<li><strong>2014-07</strong>: deprecated all API versions less than 1.4 &amp; performance improvements.</li>
<li><strong>2014-03</strong>: note expiration implemented.</li>
<li><strong>2014-02</strong>: a simple JS-client for API testing added.</li>
<li><strong>2014-01</strong>: NoteHub API, mobile friendly styling and more.</li>
<li><strong>2013-03</strong>: new color themes.</li>
<li><strong>2012-07</strong>: password protection for note editing added.</li>
<li><strong>2012-06</strong>: NoteHub 1.0 released as a result of an <a href="/story">experiment</a>.</li>
</ul>
</article>
<footer> <footer>
<a href="https://github.com/chmllr/notehub">source code</a> &middot; <a href="https://github.com/chmllr/notehub">source code</a> &middot;
<a href="https://github.com/chmllr/notehub/issues">feedback</a> &middot;
<a href="/TOS.md">terms of service</a>
</footer> </footer>
</body> </body>
</html> </html>

23
assets/public/new.js

@ -2,28 +2,25 @@
function $(id) { return document.getElementById(id) } function $(id) { return document.getElementById(id) }
function toggleButton() { $('publish-button').disabled = !$('tos').checked } function submitForm() {
const id = $("id").value;
function submitForm(token) { const text = $("text").value;
var id = $("id").value; const name = $("name").value;
var text = $("text").value; const deletion = id !== "" && text === "";
var deletion = id != "" && text == "";
if (deletion && !confirm("Do you want to delete this note?")) { if (deletion && !confirm("Do you want to delete this note?")) {
return; return;
} }
var resp = post("/", { const resp = post("/", {
"id": id, "id": id,
"text": text, "text": text,
"tos": $("tos").value, "name": name,
"password": $("password").value, "password": $("password").value
"token": token
}, function (status, responseRaw) { }, function (status, responseRaw) {
var response = JSON.parse(responseRaw); const response = JSON.parse(responseRaw);
if (status < 400 && response.Success) { if (status < 400 && response.Success) {
window.location.replace(deletion ? "/" : "/" + response.Payload) window.location.replace(deletion ? "/" : "/" + response.Payload)
} else { } else {
grecaptcha.reset();
$('feedback').innerHTML = status + ": " + response.Payload; $('feedback').innerHTML = status + ": " + response.Payload;
} }
}) });
} }

13
assets/public/note.js

@ -1,20 +1,13 @@
"use strict"; "use strict";
function post(url, vals, cb) { function post(url, vals, cb) {
var data = new FormData(); const data = new FormData();
for (var key in vals) { for (const key in vals) {
data.append(key, vals[key]); data.append(key, vals[key]);
} }
var xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('POST', url) xhr.open('POST', url)
xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) return cb(xhr.status, xhr.responseText) }; xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) return cb(xhr.status, xhr.responseText) };
xhr.send(data); xhr.send(data);
} }
function report(id) {
var resp = prompt("Please shortly explain the problem with this note.");
if (resp) {
post('/' + id + '/report', { "report": resp })
alert("Thank you!")
}
}

5
assets/public/robots.txt

@ -1,5 +1,2 @@
User-agent: * User-agent: *
Disallow: /new Disallow: /*
Disallow: /*edit
Disallow: /*export
Disallow: /*stats

50
assets/public/style.css

@ -9,7 +9,7 @@ html, body {
padding: 0; padding: 0;
margin: 0; margin: 0;
color: #353a3a; color: #353a3a;
background: #c5caca; background: #EBEBEB;
} }
#hero { #hero {
@ -18,24 +18,11 @@ html, body {
padding-top: 5em; padding-top: 5em;
} }
.ui-border {
border-radius: 3px;
border: 1px solid #333;
}
a { a {
border-bottom: 1px dotted; border-bottom: 1px dotted;
text-decoration: none; text-decoration: none;
font-weight: bold; font-weight: bold;
color: #086; color: #52489C;
}
a:hover {
color: #097;
}
a:visited {
color: #075;
} }
.button { .button {
@ -43,13 +30,13 @@ a:visited {
} }
.ui-elem { .ui-elem {
background: #fff; background: whitesmoke;
font-size: 1em; font-size: 1em;
opacity: 0.8; opacity: 0.8;
padding: 0.3em; padding: 0.3em;
border-radius: 3px;
font-weight: 300; font-weight: 300;
border: 1px solid #333; border: 3px black solid;
border-radius: 10px;
} }
.landing-button { .landing-button {
@ -57,13 +44,15 @@ a:visited {
display: inline-block; display: inline-block;
width: 10em; width: 10em;
white-space: nowrap; white-space: nowrap;
border-radius: 3px;
border: none;
margin: 0.5em; margin: 0.5em;
background: #0a6;
font-size: 1.5em; font-size: 1.5em;
text-decoration: none; text-decoration: none;
font-weight: 300; font-weight: 300;
color: #52489C;
border: 3px solid black;
background-color: #59C3C3;
border-radius: 25px;
transition: 0.5s;
} }
.landing-button.demo { .landing-button.demo {
@ -73,7 +62,9 @@ a:visited {
} }
.landing-button:hover { .landing-button:hover {
background: #0b7; -webkit-box-shadow: 5px 5px 15px 5px rgba(0, 0, 0, 0.44);
box-shadow: 5px 5px 15px 5px rgba(0, 0, 0, 0.44);
transform: scale(1.12)
} }
.landing-button.demo:hover { .landing-button.demo:hover {
@ -136,10 +127,7 @@ h6 {
article { article {
text-align: justify; text-align: justify;
margin-top: 3em; margin: 3em auto 5em;
margin-bottom: 5em;
margin-right: auto;
margin-left: auto;
} }
article img { article img {
@ -192,7 +180,7 @@ pre, code {
font-family: monospace; font-family: monospace;
font-size: 1.1em; font-size: 1.1em;
border-radius: 3px; border-radius: 3px;
background-color: #b5baba; background-color: #EBEBEB;
} }
pre { pre {
@ -205,7 +193,7 @@ table {
} }
th { th {
background-color: #b5baba; background-color: #EBEBEB;
line-height: 2.5em; line-height: 2.5em;
padding: 0.3em; padding: 0.3em;
} }
@ -221,16 +209,16 @@ td {
} }
textarea { textarea {
background-color: #b5baba; background-color: whitesmoke;
font-size: 1.2em; font-size: 1.2em;
border-radius: 5px;
flex: 1 0; flex: 1 0;
margin: 3em; margin: 3em;
padding: 2em; padding: 2em;
} }
textarea, fieldset { textarea, fieldset {
border: none; border: 3px black solid;
border-radius: 10px;
} }
fieldset { fieldset {

19
assets/templates/form.html

@ -6,7 +6,6 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" /> <meta content="width=device-width, initial-scale=1.0" name="viewport" />
<link href="/style.css" rel="stylesheet" type="text/css" /> <link href="/style.css" rel="stylesheet" type="text/css" />
<script src='https://www.google.com/recaptcha/api.js'></script>
<script src='/new.js'></script> <script src='/new.js'></script>
<script src='/note.js'></script> <script src='/note.js'></script>
</head> </head>
@ -16,11 +15,12 @@
<fieldset> <fieldset>
<input id="id" value="{{.ID}}" type="hidden" /> <input id="id" value="{{.ID}}" type="hidden" />
<input class="ui-elem" id="password" placeholder="Password for editing" type="text" autocomplete="off" /> <input class="ui-elem" id="password" placeholder="Password for editing" type="text" autocomplete="off" />
<label style="margin-right: 1em"> {{if not .ID}}
<input id="tos" type="checkbox" onClick="toggleButton()" /> <input class="ui-elem" id="name" placeholder="Note name" type="text" autocomplete="off" />
Accept <a href="/TOS.md">Terms of Service</a> {{else}}
</label> <input class="ui-elem" id="name" placeholder="Note name" type="text" autocomplete="off" hidden value=""/>
<button class="button ui-elem" disabled id="publish-button" type="button" onclick="grecaptcha.execute()"> {{end}}
<button class="button ui-elem" id="publish-button" type="button" onclick="submitForm()">
{{if .ID}}Update{{else}}Publish{{end}} Note {{if .ID}}Update{{else}}Publish{{end}} Note
</button> </button>
<span id="feedback"></span> <span id="feedback"></span>
@ -29,13 +29,8 @@
<footer> <footer>
<a href="/">&#8962; notehub</a> &middot; <a href="/">&#8962; notehub</a> &middot;
<a href="https://github.com/chmllr/NoteHub">source code</a> &middot; <a href="https://github.com/chmllr/NoteHub">source code</a> &middot;
<a href="/TOS.md">terms of service</a>
</footer> </footer>
<div class="g-recaptcha"
data-sitekey="6LfamjEUAAAAAANI45H3fpWG_xaSAcpYhENN4EnO"
data-callback="submitForm"
data-size="invisible">
</div>
</body> </body>
</html> </html>
{{end}} {{end}}

23
assets/templates/list.html

@ -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="/">&#8962; notehub</a>
</footer>
</body>
</html>
{{end}}

2
assets/templates/note.html

@ -28,9 +28,7 @@
<a href="/{{.ID}}/stats">stats</a> &middot; <a href="/{{.ID}}/stats">stats</a> &middot;
<a href="/{{.ID}}/edit">edit</a> &middot; <a href="/{{.ID}}/edit">edit</a> &middot;
<a href="/{{.ID}}/export">export</a> &middot; <a href="/{{.ID}}/export">export</a> &middot;
<a href="javascript:void(0)" onclick="report({{.ID}})">report abuse</a> &middot;
{{end}} {{end}}
<a href="/TOS.md">tos</a>
</footer> </footer>
</body> </body>
</html> </html>

11
docker-compose.yml

@ -0,0 +1,11 @@
version: "3.9"
services:
notes:
build: .
volumes:
- data:/data
ports:
- "127.0.0.1:8877:3000"
restart: always
volumes:
data:

16
email.go

@ -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
render.go

@ -29,6 +29,7 @@ var (
errorUnathorised = errors.New("password is wrong") errorUnathorised = errors.New("password is wrong")
errorBadRequest = errors.New("password is empty") errorBadRequest = errors.New("password is empty")
errorNameExists = errors.New("name exists")
) )
func (n *Note) prepare() { func (n *Note) prepare() {

80
server.go

@ -1,13 +1,11 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"os" "os"
"time" "time"
@ -19,7 +17,6 @@ import (
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
) )
var TEST_MODE = false
type Template struct{ templates *template.Template } type Template struct{ templates *template.Template }
@ -31,14 +28,12 @@ func main() {
e := echo.New() e := echo.New()
e.Logger.SetLevel(log.DEBUG) e.Logger.SetLevel(log.DEBUG)
db, err := sql.Open("sqlite3", "./database.sqlite") db, err := sql.Open("sqlite3", "./data/database.sqlite")
if err != nil { if err != nil {
e.Logger.Error(err) e.Logger.Error(err)
} }
defer db.Close() defer db.Close()
TEST_MODE = os.Getenv("TEST_MODE") != ""
adsFName := os.Getenv("ADS") adsFName := os.Getenv("ADS")
var ads template.HTML var ads template.HTML
if adsFName != "" { if adsFName != "" {
@ -60,14 +55,15 @@ func main() {
e.File("/note.js", "assets/public/note.js") e.File("/note.js", "assets/public/note.js")
e.File("/index.html", "assets/public/index.html") e.File("/index.html", "assets/public/index.html")
e.File("/", "assets/public/index.html") e.File("/", "assets/public/index.html")
e.File("/Markdown.Converter.js", "assets/public/editor/Markdown.Converter.js")
e.File("/Markdown.Editor.js", "assets/public/editor/Markdown.Editor.js")
e.File("/Markdown.Extra.js", "assets/public/editor/Markdown.Extra.js")
e.File("/Markdown.Sanitizer.js", "assets/public/editor/Markdown.Sanitizer.js")
e.File("/mathjax-editing_writing.js", "assets/public/editor/mathjax-editing_writing.js")
e.File("/cmunrb.otf", "assets/public/editor/cmunrb.otf")
e.File("/cmunrm.otf", "assets/public/editor/cmunrm.otf")
e.File("/editor", "assets/public/editor/index.html")
e.GET("/TOS.md", func(c echo.Context) error {
n, code := md2html(c, "TOS")
if code != http.StatusOK {
c.String(code, statuses[code])
}
return c.Render(code, "Page", n)
})
e.GET("/:id", func(c echo.Context) error { e.GET("/:id", func(c echo.Context) error {
id := c.Param("id") id := c.Param("id")
@ -130,9 +126,6 @@ func main() {
if report != "" { if report != "" {
id := c.Param("id") id := c.Param("id")
c.Logger().Infof("note %s was reported: %s", id, report) c.Logger().Infof("note %s was reported: %s", id, report)
if err := email(id, report); err != nil {
c.Logger().Errorf("couldn't send email: %v", err)
}
} }
return c.NoContent(http.StatusNoContent) return c.NoContent(http.StatusNoContent)
}) })
@ -142,6 +135,15 @@ func main() {
return c.Render(http.StatusOK, "Form", nil) return c.Render(http.StatusOK, "Form", nil)
}) })
e.GET("/list", func(c echo.Context) error {
c.Logger().Debug("GET /list")
notes, err := loadAll(c, db)
if err != http.StatusOK {
return c.String(err, "Error happened")
}
return c.Render(http.StatusOK, "List", notes)
})
type postResp struct { type postResp struct {
Success bool Success bool
Payload string Payload string
@ -149,27 +151,13 @@ func main() {
e.POST("/", func(c echo.Context) error { e.POST("/", func(c echo.Context) error {
c.Logger().Debug("POST /") c.Logger().Debug("POST /")
if !TEST_MODE && !checkRecaptcha(c, c.FormValue("token")) {
code := http.StatusForbidden
return c.JSON(code, postResp{false, statuses[code] + ": robot check failed"})
}
if c.FormValue("tos") != "on" {
code := http.StatusPreconditionFailed
c.Logger().Errorf("POST / error: %d", code)
return c.JSON(code, postResp{false, statuses[code]})
}
id := c.FormValue("id") id := c.FormValue("id")
text := c.FormValue("text") text := c.FormValue("text")
l := len(text)
if (id == "" || id != "" && l != 0) && (10 > l || l > 50000) {
code := http.StatusBadRequest
c.Logger().Errorf("POST / error: %d", code)
return c.JSON(code, postResp{false, statuses[code] + ": note length not accepted"})
}
n := &Note{ n := &Note{
ID: id, ID: id,
Text: text, Text: text,
Password: c.FormValue("password"), Password: c.FormValue("password"),
Name: c.FormValue("name"),
} }
n, err = save(c, db, n) n, err = save(c, db, n)
if err != nil { if err != nil {
@ -179,6 +167,8 @@ func main() {
code = http.StatusUnauthorized code = http.StatusUnauthorized
} else if err == errorBadRequest { } else if err == errorBadRequest {
code = http.StatusBadRequest code = http.StatusBadRequest
} else if err == errorNameExists {
code = http.StatusBadRequest
} }
c.Logger().Errorf("POST / error: %d", code) c.Logger().Errorf("POST / error: %d", code)
return c.JSON(code, postResp{false, statuses[code] + ": " + err.Error()}) return c.JSON(code, postResp{false, statuses[code] + ": " + err.Error()})
@ -201,31 +191,3 @@ func main() {
} }
e.Logger.Fatal(e.StartServer(s)) e.Logger.Fatal(e.StartServer(s))
} }
func checkRecaptcha(c echo.Context, captchaResp string) bool {
resp, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", url.Values{
"secret": []string{os.Getenv("RECAPTCHA_SECRET")},
"response": []string{captchaResp},
"remoteip": []string{c.Request().RemoteAddr},
})
if err != nil {
c.Logger().Errorf("captcha response verification failed: %v", err)
return false
}
defer resp.Body.Close()
respJson := &struct {
Success bool `json:"success"`
ErrorCodes []string `json:"error-codes"`
}{}
s, err := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(s, respJson)
if err != nil {
c.Logger().Errorf("captcha response parse recaptcha response: %v", err)
return false
}
if !respJson.Success {
c.Logger().Warnf("captcha validation failed: %v", respJson.ErrorCodes)
}
return respJson.Success
}

4
stats.go

@ -53,7 +53,5 @@ func incViews(n *Note, db *sql.DB) {
} }
} }
stats.Store(n.ID, views+1) stats.Store(n.ID, views+1)
if TEST_MODE { _, _ = flush(db)
flush(db)
}
} }

42
storage.go

@ -32,7 +32,7 @@ var (
) )
type Note struct { type Note struct {
ID, Title, Text, Password, DeprecatedPassword, Encoded string ID, Title, Text, Password, DeprecatedPassword, Encoded, Name string
Published, Edited time.Time Published, Edited time.Time
Views int Views int
Content, Ads template.HTML Content, Ads template.HTML
@ -109,13 +109,16 @@ func insert(c echo.Context, db *sql.DB, n *Note) (*Note, error) {
} }
stmt, _ := tx.Prepare("insert into notes(id, text, password) values(?, ?, ?)") stmt, _ := tx.Prepare("insert into notes(id, text, password) values(?, ?, ?)")
defer stmt.Close() defer stmt.Close()
id := randId() id := n.Name
if id == "" {
id = randId()
}
_, err = stmt.Exec(id, n.Text, n.Password) _, err = stmt.Exec(id, n.Text, n.Password)
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") { if strings.HasPrefix(err.Error(), "UNIQUE constraint failed") {
c.Logger().Infof("collision on id %s", id) c.Logger().Infof("collision on id %s", id)
return save(c, db, n) return nil, errorNameExists
} }
return nil, err return nil, err
} }
@ -168,3 +171,36 @@ func load(c echo.Context, db *sql.DB) (*Note, int) {
n.prepare() n.prepare()
return n, http.StatusOK return n, http.StatusOK
} }
func loadAll(c echo.Context, db *sql.DB) ([]Note, int) {
c.Logger().Debug("loading notes")
stmt, _ := db.Prepare("select * from notes")
defer stmt.Close()
rows, _ := stmt.Query()
var notes []Note
for rows.Next() {
var id, text, password string
var published time.Time
var editedVal interface{}
var views int
if err := rows.Scan(&id, &text, &published, &editedVal, &password, &views); err != nil {
code := http.StatusNotFound
return nil, code
}
n := &Note{
ID: id,
Text: text,
Views: views,
Published: published,
}
if editedVal != nil {
n.Edited = editedVal.(time.Time)
}
n.prepare()
notes = append(notes, *n)
}
return notes, http.StatusOK
}

377
test/main.go

@ -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…
Cancel
Save