diff options
| -rw-r--r-- | public/views/admin.html | 5 | ||||
| -rw-r--r-- | public/views/createPost.html | 185 | ||||
| -rw-r--r-- | public/views/dashboard.html | 32 | ||||
| -rw-r--r-- | routes/admin.js | 4 | ||||
| -rw-r--r-- | routes/posts.js | 17 | ||||
| -rw-r--r-- | static/assets/css/custom.css | 1 | ||||
| -rw-r--r-- | static/assets/js/marked.js | 1401 | ||||
| -rw-r--r-- | static/assets/js/pages/admin.js | 10 | ||||
| -rw-r--r-- | static/assets/js/pages/authCheck.js | 10 | ||||
| -rw-r--r-- | static/assets/js/pages/config.js | 10 |
10 files changed, 1639 insertions, 36 deletions
diff --git a/public/views/admin.html b/public/views/admin.html index 2e9e485..489a736 100644 --- a/public/views/admin.html +++ b/public/views/admin.html @@ -95,8 +95,9 @@ <script src="/static/assets/js/jquery.js"></script> <script src="/static/assets/js/bootstrap-transition.js"></script> <script src="/static/assets/js/bootstrap-collapse.js"></script> - <script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-app.js" defer></script> - <script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-auth.js" defer></script> + <script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-app.js"></script> + <script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-auth.js"></script> + <script src="/static/assets/js/pages/config.js"></script> <script src="/static/assets/js/pages/admin.js"></script> </body> diff --git a/public/views/createPost.html b/public/views/createPost.html new file mode 100644 index 0000000..e01ad55 --- /dev/null +++ b/public/views/createPost.html @@ -0,0 +1,185 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link href="/static/assets/css/bootstrap.css" rel="stylesheet"> + <link href="/static/assets/css/bootstrap-responsive.css" rel="stylesheet"> + <link rel="stylesheet" + href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/styles/base16/seti-ui.min.css"> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Fira+Code&display=swap" rel="stylesheet"> + <link href="/static/assets/css/custom.css" rel="stylesheet"> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" + integrity="sha384-KiWOvVjnN8qwAZbuQyWDIbfCLFhLXNETzBQjA/92pIowpC0d2O3nppDGQVgwd2nB" crossorigin="anonymous"> + <link rel="shortcut icon" href="/static/images/favicon.png"> + <title>Dashboard - Add New Post</title> + <style type="text/css"> + input, + textarea { + width: 100%; + } + + code, + pre { + font-family: 'Fira Code', monospace !important; + font-size: 14px; + background-color: #1c1c1c; + color: #D4D4D5; + } + + pre { + padding: 10px; + } + </style> +</head> + +<body> + <div class="navbar navbar-inverse navbar-fixed-top"> + <div class="navbar-inner"> + <div class="container"> + <button type="button" class="btn btn-navbar" data-toggle="collapse" + data-target=".nav-collapse"></button> + <a class="brand" href="../">That Computer Scientist</a> + <div class="nav-collapse collapse"> + <ul class="nav"> + <li><a href="../">Home</a></li> + <li><a href="../about">About</a></li> + <li><a href="../repos">Repositories</a></li> + <li class="active"><a href="./">Administration</a></li> + </ul> + </div> + </div> + </div> + </div> + <div class="container margin-top-375" style="margin-top: 3.75rem;"> + <div class="container"> + <div class="row"> + <div class="span3 bs-docs-sidebar"> + <ul class="nav nav-list bs-docs-sidenav affix-top"> + <li><a href="../admin/dashboard">All Posts</a></li> + <li class="active"><a href="/admin/dashboard/new">Add Post</a></li> + <li><a href="#">Manage Comments</a></li> + <li><a href="#">Manage Users</a></li> + <li><a id="logout">Logout</a></li> + </ul> + </div> + <div class="span9"> + <header class="page-header"> + <h1>Create New Post</h1> + </header> + <div class="span9" style="padding: 0; margin: 0;"> + <div id="createPost"> + <div class="form-group"> + <label for="title">Title</label> + <input type="text" class="form-control" id="title" name="title" placeholder="Title" + style=" margin-left: 0;"> + </div> + <div class="form-group"> + <label for="content">Content</label> + <textarea class="form-control" id="content" name="content" rows="10" + placeholder="Content"></textarea> + </div> + <div class="container"> + <h1 style="margin-left: 0; padding-left: 0; transform: translate(-20px, 0px);">Render + Preview</h1> + <div class="span9" id="renderPreview" + style="padding: 10px 0px; margin: 20px 0px 20px -20px"></div> + </div> + <div class="form-group"> + <label for="tags">Tags</label> + <input type="text" class="form-control" id="tags" name="tags" placeholder="Tags" + style=" margin-left: 0;"> + </div> + <div class="form-group"> + <label for="publishDate">Publish Date</label> + <input type="date" class="form-control" id="publishDate" name="publishDate" + placeholder="Publish Date" style=" margin-left: 0;"> + </div> + <div id="error" class="alert alert-error hidden"> + <strong>Error!</strong> Please fill out all fields. + </div> + <button class="btn btn-primary" id="publishPost">Create New Post</button> + </div> + </div> + </div> + </div> + </div> + </div> + <script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-app.js"></script> + <script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-auth.js"></script> + <script src="/static/assets/js/jquery.js"></script> + <script src="/static/assets/js/bootstrap-transition.js"></script> + <script src="/static/assets/js/bootstrap-collapse.js"></script> + <script src="/static/assets/js/pages/config.js"></script> + <script src="/static/assets/js/pages/authCheck.js"></script> + <script src="/static/assets/js/marked.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.0/highlight.min.js"></script> + <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js" + integrity="sha384-0fdwu/T/EQMsQlrHCCHoH10pkPLlKA1jL5dFyUOvB3lfeT2540/2g6YgSi2BL14p" + crossorigin="anonymous"></script> + <script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js" + integrity="sha384-+XBljXPPiv+OzfbB3cVmLHf4hdUFHlWNZN5spNQ7rmHTXpd7WvJum6fIACpNNfIR" + crossorigin="anonymous"></script> + <script> + window.addEventListener("DOMContentLoaded", () => { + const content = document.getElementById('content'); + const renderPreview = document.getElementById('renderPreview'); + marked.setOptions({ + highlight: function (code) { + return hljs.highlightAuto(code).value; + } + }); + content.addEventListener('input', () => { + renderPreview.innerHTML = marked.parse(content.value); + renderMathInElement(renderPreview, { + // customised options + // • auto-render specific keys, e.g.: + delimiters: [ + { left: '$$', right: '$$', display: true }, + { left: '$', right: '$', display: false }, + { left: '\\(', right: '\\)', display: false }, + { left: '\\[', right: '\\]', display: true } + ], + // • rendering keys, e.g.: + throwOnError: false + }); + }); + }); + $(document).on('click', '#publishPost', () => { + document.getElementById('error').classList.add('hidden'); + const htmlRender = $('#renderPreview').html(); + const title = $('#title').val(); + const publishDate = $('#publishDate').val(); + const tags = $('#tags').val(); + if (title === '' || publishDate === '') { + document.getElementById('error').classList.remove('hidden'); + return; + } else { + // Publish post to api/blog/new + const body = { + title: title, + publishDate: publishDate, + tags: tags, + content: htmlRender + }; + $.ajax({ + url: '/api/blog/new', + type: 'POST', + data: body, + success: (data) => { + window.location.href = '/admin/dashboard'; + }, + error: (err) => { + console.log(err); + } + }); + } + }); + </script> +</body> + +</html>
\ No newline at end of file diff --git a/public/views/dashboard.html b/public/views/dashboard.html index 58ac028..3791678 100644 --- a/public/views/dashboard.html +++ b/public/views/dashboard.html @@ -9,7 +9,7 @@ <link href="/static/assets/css/bootstrap-responsive.css" rel="stylesheet"> <link href="/static/assets/css/custom.css" rel="stylesheet"> <link rel="shortcut icon" href="/static/images/favicon.png"> - <title>Document</title> + <title>Dashboard - All Posts</title> </head> <body> @@ -36,7 +36,7 @@ <div class="span3 bs-docs-sidebar"> <ul class="nav nav-list bs-docs-sidenav affix-top"> <li class="active"><a href="../admin/dashboard">All Posts</a></li> - <li><a href="#">Add Post</a></li> + <li><a href="/admin/dashboard/new">Add Post</a></li> <li><a href="#">Manage Comments</a></li> <li><a href="#">Manage Users</a></li> <li><a id="logout">Logout</a></li> @@ -45,6 +45,7 @@ <div class="span9"> <header class="page-header"> <h1>All Posts</h1> + <div id="posts"></div> </header> </div> </div> @@ -52,28 +53,11 @@ </div> <script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/7.14.1/firebase-auth.js"></script> - <script> - const firebaseConfig = { - apiKey: "AIzaSyBCmKUnEmm8hLR9ZcFWPYbYiplbP6yUzfU", - authDomain: "thatcomputerscientist-e9cf2.firebaseapp.com", - projectId: "thatcomputerscientist-e9cf2", - storageBucket: "thatcomputerscientist-e9cf2.appspot.com", - messagingSenderId: "178402875544", - appId: "1:178402875544:web:8c9d8880d3ef495a5658ed", - measurementId: "G-JECWWZG5J6" - }; - firebase.initializeApp(firebaseConfig); - firebase.auth().onAuthStateChanged((user) => { - if (!user) { - window.location.assign("/admin/"); - } - }); - document.getElementById("logout").addEventListener("click", () => { - firebase.auth().signOut().then(() => { - window.location.assign("/admin/"); - }); - }); - </script> + <script src="/static/assets/js/jquery.js"></script> + <script src="/static/assets/js/bootstrap-transition.js"></script> + <script src="/static/assets/js/bootstrap-collapse.js"></script> + <script src="/static/assets/js/pages/config.js"></script> + <script src="/static/assets/js/pages/authCheck.js"></script> </body> </html>
\ No newline at end of file diff --git a/routes/admin.js b/routes/admin.js index d7b79a5..b6afab7 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -5,6 +5,10 @@ router.get("/dashboard", function (req, res) { res.render("dashboard.html"); }); +router.get("/dashboard/new", function (req, res) { + res.render("createPost.html"); +}) + router.get("/", (req, res) => { // Send admin.html from public folder res.render("admin.html"); diff --git a/routes/posts.js b/routes/posts.js index e862680..d81327f 100644 --- a/routes/posts.js +++ b/routes/posts.js @@ -20,4 +20,21 @@ router.get("/posts", (req, res) => { }); }); +router.post("/new", (req, res) => { + const { title, content, tags, publishDate } = req.body; + const store = firebase.firestore(); + const post = { + title, + content, + tags: String(tags).split(",").length > 0 ? String(tags).split(",") : [], + publishDate, + }; + let query = store.collection("posts"); + query.add(post).then(() => { + res.json({ success: true }); + }).catch((err) => { + res.json({ success: false, err }); + }); +}); + module.exports = router; diff --git a/static/assets/css/custom.css b/static/assets/css/custom.css index 446d894..709d6b0 100644 --- a/static/assets/css/custom.css +++ b/static/assets/css/custom.css @@ -7,6 +7,7 @@ button { cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAzElEQVRYR+2X0Q6AIAhF5f8/2jYXZkwEjNSVvVUjDpcrGgT7FUkI2D9xRfQETwNIiWO85wfINfQUEyxBG2ArsLwC0jioGt5zFcwF4OYDPi/mBYKm4t0U8ATgRm3ThFoAqkhNgWkA0jJLvaOVSs7j3qMnSgXWBMiWPXe94QqMBMBc1VZIvaTu5u5pQewq0EqNZvIEMCmxAawK0DNkay9QmfFNAJUXfgGgUkLaE7j/h8fnASkxHTz0DGIBMCnBeeM7AArpUd3mz2x3C7wADglA8BcWMZhZAAAAAElFTkSuQmCC) 14 0, pointer !important; + outline: none !important; } .cx { diff --git a/static/assets/js/marked.js b/static/assets/js/marked.js new file mode 100644 index 0000000..2985cd0 --- /dev/null +++ b/static/assets/js/marked.js @@ -0,0 +1,1401 @@ +/** + * marked - a markdown parser + * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) + * https://github.com/chjj/marked + */ + +(function () { + /** + * Block-Level Grammar + */ + + var block = { + newline: /^\n+/, + code: /^\n( {4}[^\n]+\n*)+/, + fences: noop, + hr: /^( *[-*_]){3,} *(?:\n+|$)/, + heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, + nptable: noop, + lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, + blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, + list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, + def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, + table: noop, + paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, + text: /^[^\n]+/, + }; + + block.bullet = /(?:[*+-]|\d+\.)/; + block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; + block.item = replace(block.item, "gm")(/bull/g, block.bullet)(); + + block.list = replace(block.list)(/bull/g, block.bullet)( + "hr", + "\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))" + )("def", "\\n+(?=" + block.def.source + ")")(); + + block.blockquote = replace(block.blockquote)("def", block.def)(); + + block._tag = + "(?!(?:" + + "a|em|strong|small|s|cite|q|dfn|abbr|data|time|code" + + "|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo" + + "|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b"; + + block.html = replace(block.html)("comment", /<!--[\s\S]*?-->/)( + "closed", + /<(tag)[\s\S]+?<\/\1>/ + )("closing", /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)(/tag/g, block._tag)(); + + block.paragraph = replace(block.paragraph)("hr", block.hr)( + "heading", + block.heading + )("lheading", block.lheading)("blockquote", block.blockquote)( + "tag", + "<" + block._tag + )("def", block.def)(); + + /** + * Normal Block Grammar + */ + + block.normal = merge({}, block); + + /** + * GFM Block Grammar + */ + + block.gfm = merge({}, block.normal, { + fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/, + paragraph: /^/, + heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/, + }); + + block.gfm.paragraph = replace(block.paragraph)( + "(?!", + "(?!" + + block.gfm.fences.source.replace("\\1", "\\2") + + "|" + + block.list.source.replace("\\1", "\\3") + + "|" + )(); + + /** + * GFM + Tables Block Grammar + */ + + block.tables = merge({}, block.gfm, { + nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, + table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/, + }); + + /** + * Block Lexer + */ + + function Lexer(options) { + this.tokens = []; + this.tokens.links = {}; + this.options = options || marked.defaults; + // Sort math delimiters, because parsing of $$..$$ must come before $..$ etc. + if (Array.isArray(this.options.mathDelimiters)) { + this.options.mathDelimiters.sort(function (a, b) { + return b[0].length - a[0].length; + }); + } + this.rules = block.normal; + + if (this.options.gfm) { + if (this.options.tables) { + this.rules = block.tables; + } else { + this.rules = block.gfm; + } + } + } + + /** + * Expose Block Rules + */ + + Lexer.rules = block; + + /** + * Static Lex Method + */ + + Lexer.lex = function (src, options) { + var lexer = new Lexer(options); + return lexer.lex(src); + }; + + /** + * Preprocessing + */ + + Lexer.prototype.lex = function (src) { + src = src + .replace(/\r\n|\r/g, "\n") + .replace(/\t/g, " ") + .replace(/\u00a0/g, " ") + .replace(/\u2424/g, "\n"); + + return this.token(src, true); + }; + + /** + * Lexing + */ + + Lexer.prototype.token = function (src, top, bq) { + var src = src.replace(/^ +$/gm, ""), + next, + loose, + cap, + bull, + b, + item, + space, + i, + l; + + while (src) { + // newline + if ((cap = this.rules.newline.exec(src))) { + src = src.substring(cap[0].length); + if (cap[0].length > 1) { + this.tokens.push({ + type: "space", + }); + } + } + + // code + if ((cap = this.rules.code.exec(src))) { + src = src.substring(cap[0].length); + cap = cap[0].replace(/^ {4}/gm, ""); + this.tokens.push({ + type: "code", + text: !this.options.pedantic ? cap.replace(/\n+$/, "") : cap, + }); + continue; + } + + // fences (gfm) + if ((cap = this.rules.fences.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: "code", + lang: cap[2], + text: cap[3] || "", + }); + continue; + } + + // heading + if ((cap = this.rules.heading.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: "heading", + depth: cap[1].length, + text: cap[2], + }); + continue; + } + + // table no leading pipe (gfm) + if (top && (cap = this.rules.nptable.exec(src))) { + src = src.substring(cap[0].length); + + item = { + type: "table", + header: cap[1].replace(/^ *| *\| *$/g, "").split(/ *\| */), + align: cap[2].replace(/^ *|\| *$/g, "").split(/ *\| */), + cells: cap[3].replace(/\n$/, "").split("\n"), + }; + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = "right"; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = "center"; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = "left"; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = item.cells[i].split(/ *\| */); + } + + this.tokens.push(item); + + continue; + } + + // lheading + if ((cap = this.rules.lheading.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: "heading", + depth: cap[2] === "=" ? 1 : 2, + text: cap[1], + }); + continue; + } + + // hr + if ((cap = this.rules.hr.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: "hr", + }); + continue; + } + + // blockquote + if ((cap = this.rules.blockquote.exec(src))) { + src = src.substring(cap[0].length); + + this.tokens.push({ + type: "blockquote_start", + }); + + cap = cap[0].replace(/^ *> ?/gm, ""); + + // Pass `top` to keep the current + // "toplevel" state. This is exactly + // how markdown.pl works. + this.token(cap, top, true); + + this.tokens.push({ + type: "blockquote_end", + }); + + continue; + } + + // list + if ((cap = this.rules.list.exec(src))) { + src = src.substring(cap[0].length); + bull = cap[2]; + + this.tokens.push({ + type: "list_start", + ordered: bull.length > 1, + }); + + // Get each top-level item. + cap = cap[0].match(this.rules.item); + + next = false; + l = cap.length; + i = 0; + + for (; i < l; i++) { + item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) +/, ""); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf("\n ")) { + space -= item.length; + item = !this.options.pedantic + ? item.replace(new RegExp("^ {1," + space + "}", "gm"), "") + : item.replace(/^ {1,4}/gm, ""); + } + + // Determine whether the next list item belongs here. + // Backpedal if it does not belong in this list. + if (this.options.smartLists && i !== l - 1) { + b = block.bullet.exec(cap[i + 1])[0]; + if (bull !== b && !(bull.length > 1 && b.length > 1)) { + src = cap.slice(i + 1).join("\n") + src; + i = l - 1; + } + } + + // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + loose = next || /\n\n(?!\s*$)/.test(item); + if (i !== l - 1) { + next = item.charAt(item.length - 1) === "\n"; + if (!loose) loose = next; + } + + this.tokens.push({ + type: loose ? "loose_item_start" : "list_item_start", + }); + + // Recurse. + this.token(item, false, bq); + + this.tokens.push({ + type: "list_item_end", + }); + } + + this.tokens.push({ + type: "list_end", + }); + + continue; + } + + // html + if ((cap = this.rules.html.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: this.options.sanitize ? "paragraph" : "html", + pre: + !this.options.sanitizer && + (cap[1] === "pre" || cap[1] === "script" || cap[1] === "style"), + text: cap[0], + }); + continue; + } + + // def + if (!bq && top && (cap = this.rules.def.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.links[cap[1].toLowerCase()] = { + href: cap[2], + title: cap[3], + }; + continue; + } + + // table (gfm) + if (top && (cap = this.rules.table.exec(src))) { + src = src.substring(cap[0].length); + + item = { + type: "table", + header: cap[1].replace(/^ *| *\| *$/g, "").split(/ *\| */), + align: cap[2].replace(/^ *|\| *$/g, "").split(/ *\| */), + cells: cap[3].replace(/(?: *\| *)?\n$/, "").split("\n"), + }; + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = "right"; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = "center"; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = "left"; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = item.cells[i] + .replace(/^ *\| *| *\| *$/g, "") + .split(/ *\| */); + } + + this.tokens.push(item); + + continue; + } + + // top-level paragraph + if (top && (cap = this.rules.paragraph.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: "paragraph", + text: + cap[1].charAt(cap[1].length - 1) === "\n" + ? cap[1].slice(0, -1) + : cap[1], + }); + continue; + } + + // text + if ((cap = this.rules.text.exec(src))) { + // Top-level should never reach here. + src = src.substring(cap[0].length); + this.tokens.push({ + type: "text", + text: cap[0], + }); + continue; + } + + if (src) { + throw new Error("Infinite loop on byte: " + src.charCodeAt(0)); + } + } + + return this.tokens; + }; + + /** + * Inline-Level Grammar + */ + + var inline = { + escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, + autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, + url: noop, + tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, + link: /^!?\[(inside)\]\(href\)/, + reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, + nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, + strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, + em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, + code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, + br: /^ {2,}\n(?!\s*$)/, + del: noop, + text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/, + }; + + inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/; + inline._href = /\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/; + + inline.link = replace(inline.link)("inside", inline._inside)( + "href", + inline._href + )(); + + inline.reflink = replace(inline.reflink)("inside", inline._inside)(); + + /** + * Normal Inline Grammar + */ + + inline.normal = merge({}, inline); + + /** + * Pedantic Inline Grammar + */ + + inline.pedantic = merge({}, inline.normal, { + strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, + }); + + /** + * GFM Inline Grammar + */ + + inline.gfm = merge({}, inline.normal, { + escape: replace(inline.escape)("])", "~|])")(), + url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, + del: /^~~(?=\S)([\s\S]*?\S)~~/, + text: replace(inline.text)("]|", "~]|")("|", "|https?://|")(), + }); + + /** + * GFM + Line Breaks Inline Grammar + */ + + inline.breaks = merge({}, inline.gfm, { + br: replace(inline.br)("{2,}", "*")(), + text: replace(inline.gfm.text)("{2,}", "*")(), + }); + + /** + * Inline Lexer & Compiler + */ + + function InlineLexer(links, options) { + this.options = options || marked.defaults; + this.links = links; + this.rules = inline.normal; + this.renderer = this.options.renderer || new Renderer(); + this.renderer.options = this.options; + + if (!this.links) { + throw new Error("Tokens array requires a `links` property."); + } + + if (this.options.gfm) { + if (this.options.breaks) { + this.rules = inline.breaks; + } else { + this.rules = inline.gfm; + } + } else if (this.options.pedantic) { + this.rules = inline.pedantic; + } + + // Make rules compatible with specified math delimiters + if ( + this.options.mathDelimiters && + Array.isArray(this.options.mathDelimiters) + ) { + var regexEscape = function (s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + }; + var openingMathDelimiters = []; + for (var i = 0; i < this.options.mathDelimiters.length; i++) { + if ( + Array.isArray(this.options.mathDelimiters[i]) && + this.options.mathDelimiters[i].length === 2 + ) { + openingMathDelimiters.push( + regexEscape(this.options.mathDelimiters[i][0]) + ); + } + } + this.rules.text = replace(this.rules.text)( + "]|", + "]|(" + openingMathDelimiters.join("|") + ")|" + )(); + } + } + + /** + * Expose Inline Rules + */ + + InlineLexer.rules = inline; + + /** + * Static Lexing/Compiling Method + */ + + InlineLexer.output = function (src, links, options) { + var inline = new InlineLexer(links, options); + return inline.output(src); + }; + + /** + * Lexing/Compiling + */ + + InlineLexer.prototype.output = function (src) { + var out = "", + link, + text, + href, + cap; + + while (src) { + // Math wrapped in specified math delimiters is passed through unprocessed, + // For use with MathJax / KaTeX etc. + if (this.options.mathDelimiters) { + for (var i = 0; i < this.options.mathDelimiters.length; i++) { + var delim = this.options.mathDelimiters[i]; + if (delim === "beginend") { + // \begin{}..\end{} + if (src.substr(0, 7) === "\\begin{") { + var idx = 7; + // traverse nested \begin{}..\end{} + var traverse = function () { + while (src.substr(idx, 5) !== "\\end{") { + if (src.substr(idx, 7) === "\\begin{") { + idx += 7; + traverse(); + } else { + idx++; + } + } + idx += 5; + while (src[idx] !== "}") { + idx++; + } + idx++; + }; + traverse(); + out += this.renderer.math(src.substring(0, idx), "begin", "end"); // TODO, extract begin and end as \begin{foo} and \end{foo}. + src = src.substring(idx); + continue; + } + } else if ( + Array.isArray(delim) && + delim.length === 2 && + src.substr(0, delim[0].length) === delim[0] + ) { + var idx = delim[0].length; + while (true) { + if (idx > src.length) { + break; + } + // Allow escaping closing delimiter + if ( + delim[1].length === 1 && + src.substr(idx, 2) === "\\" + delim[1] + ) { + idx += 2; + } else if (src.substr(idx, delim[1].length) !== delim[1]) { + idx++; + } else { + break; + } + } + idx += delim[1].length; + out += this.renderer.math( + src.substring(0, idx), + delim[0], + delim[1] + ); + src = src.substring(idx); + continue; + } + } + } + + // escape + if ((cap = this.rules.escape.exec(src))) { + src = src.substring(cap[0].length); + out += cap[1]; + continue; + } + + // autolink + if ((cap = this.rules.autolink.exec(src))) { + src = src.substring(cap[0].length); + if (cap[2] === "@") { + text = + cap[1].charAt(6) === ":" + ? this.mangle(cap[1].substring(7)) + : this.mangle(cap[1]); + href = this.mangle("mailto:") + text; + } else { + text = escape(cap[1]); + href = text; + } + out += this.renderer.link(href, null, text); + continue; + } + + // url (gfm) + if (!this.inLink && (cap = this.rules.url.exec(src))) { + src = src.substring(cap[0].length); + text = escape(cap[1]); + href = text; + out += this.renderer.link(href, null, text); + continue; + } + + // tag + if ((cap = this.rules.tag.exec(src))) { + if (!this.inLink && /^<a /i.test(cap[0])) { + this.inLink = true; + } else if (this.inLink && /^<\/a>/i.test(cap[0])) { + this.inLink = false; + } + src = src.substring(cap[0].length); + out += this.options.sanitize + ? this.options.sanitizer + ? this.options.sanitizer(cap[0]) + : escape(cap[0]) + : cap[0]; + continue; + } + + // link + if ((cap = this.rules.link.exec(src))) { + src = src.substring(cap[0].length); + this.inLink = true; + out += this.outputLink(cap, { + href: cap[2], + title: cap[3], + }); + this.inLink = false; + continue; + } + + // reflink, nolink + if ( + (cap = this.rules.reflink.exec(src)) || + (cap = this.rules.nolink.exec(src)) + ) { + src = src.substring(cap[0].length); + link = (cap[2] || cap[1]).replace(/\s+/g, " "); + link = this.links[link.toLowerCase()]; + if (!link || !link.href) { + out += cap[0].charAt(0); + src = cap[0].substring(1) + src; + continue; + } + this.inLink = true; + out += this.outputLink(cap, link); + this.inLink = false; + continue; + } + + // strong + if ((cap = this.rules.strong.exec(src))) { + src = src.substring(cap[0].length); + out += this.renderer.strong(this.output(cap[2] || cap[1])); + continue; + } + + // em + if ((cap = this.rules.em.exec(src))) { + src = src.substring(cap[0].length); + out += this.renderer.em(this.output(cap[2] || cap[1])); + continue; + } + + // code + if ((cap = this.rules.code.exec(src))) { + src = src.substring(cap[0].length); + out += this.renderer.codespan(escape(cap[2], true)); + continue; + } + + // br + if ((cap = this.rules.br.exec(src))) { + src = src.substring(cap[0].length); + out += this.renderer.br(); + continue; + } + + // del (gfm) + if ((cap = this.rules.del.exec(src))) { + src = src.substring(cap[0].length); + out += this.renderer.del(this.output(cap[1])); + continue; + } + + // text + if ((cap = this.rules.text.exec(src))) { + src = src.substring(cap[0].length); + out += this.renderer.text(escape(this.smartypants(cap[0]))); + continue; + } + + if (src) { + throw new Error("Infinite loop on byte: " + src.charCodeAt(0)); + } + } + + return out; + }; + + /** + * Compile Link + */ + + InlineLexer.prototype.outputLink = function (cap, link) { + var href = escape(link.href), + title = link.title ? escape(link.title) : null; + + return cap[0].charAt(0) !== "!" + ? this.renderer.link(href, title, this.output(cap[1])) + : this.renderer.image(href, title, escape(cap[1])); + }; + + /** + * Smartypants Transformations + */ + + InlineLexer.prototype.smartypants = function (text) { + if (!this.options.smartypants) return text; + return ( + text + // em-dashes + .replace(/---/g, "\u2014") + // en-dashes + .replace(/--/g, "\u2013") + // opening singles + .replace(/(^|[-\u2014/(\[{"\s])'/g, "$1\u2018") + // closing singles & apostrophes + .replace(/'/g, "\u2019") + // opening doubles + .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, "$1\u201c") + // closing doubles + .replace(/"/g, "\u201d") + // ellipses + .replace(/\.{3}/g, "\u2026") + ); + }; + + /** + * Mangle Links + */ + + InlineLexer.prototype.mangle = function (text) { + if (!this.options.mangle) return text; + var out = "", + l = text.length, + i = 0, + ch; + + for (; i < l; i++) { + ch = text.charCodeAt(i); + if (Math.random() > 0.5) { + ch = "x" + ch.toString(16); + } + out += "&#" + ch + ";"; + } + + return out; + }; + + /** + * Renderer + */ + + function Renderer(options) { + this.options = options || {}; + } + + Renderer.prototype.code = function (code, lang, escaped) { + if (this.options.highlight) { + var out = this.options.highlight(code, lang); + if (out != null && out !== code) { + escaped = true; + code = out; + } + } + + if (!lang) { + return ( + "<pre><code>" + + (escaped ? code : escape(code, true)) + + "\n</code></pre>" + ); + } + + return ( + '<pre><code class="' + + this.options.langPrefix + + escape(lang, true) + + '">' + + (escaped ? code : escape(code, true)) + + "\n</code></pre>\n" + ); + }; + + Renderer.prototype.blockquote = function (quote) { + return "<blockquote>\n" + quote + "</blockquote>\n"; + }; + + Renderer.prototype.html = function (html) { + return html; + }; + + Renderer.prototype.heading = function (text, level, raw) { + return ( + "<h" + + level + + ' id="' + + this.options.headerPrefix + + raw.toLowerCase().replace(/[^\w]+/g, "-") + + '">' + + text + + "</h" + + level + + ">\n" + ); + }; + + Renderer.prototype.hr = function () { + return this.options.xhtml ? "<hr/>\n" : "<hr>\n"; + }; + + Renderer.prototype.list = function (body, ordered) { + var type = ordered ? "ol" : "ul"; + return "<" + type + ">\n" + body + "</" + type + ">\n"; + }; + + Renderer.prototype.listitem = function (text) { + return "<li>" + text + "</li>\n"; + }; + + Renderer.prototype.paragraph = function (text) { + return "<p>" + text + "</p>\n"; + }; + + Renderer.prototype.table = function (header, body) { + return ( + "<table>\n" + + "<thead>\n" + + header + + "</thead>\n" + + "<tbody>\n" + + body + + "</tbody>\n" + + "</table>\n" + ); + }; + + Renderer.prototype.tablerow = function (content) { + return "<tr>\n" + content + "</tr>\n"; + }; + + Renderer.prototype.tablecell = function (content, flags) { + var type = flags.header ? "th" : "td"; + var tag = flags.align + ? "<" + type + ' style="text-align:' + flags.align + '">' + : "<" + type + ">"; + return tag + content + "</" + type + ">\n"; + }; + + // span level renderer + Renderer.prototype.strong = function (text) { + return "<strong>" + text + "</strong>"; + }; + + Renderer.prototype.em = function (text) { + return "<em>" + text + "</em>"; + }; + + Renderer.prototype.codespan = function (text) { + return "<code>" + text + "</code>"; + }; + + Renderer.prototype.br = function () { + return this.options.xhtml ? "<br/>" : "<br>"; + }; + + Renderer.prototype.del = function (text) { + return "<del>" + text + "</del>"; + }; + + Renderer.prototype.link = function (href, title, text) { + if (this.options.sanitize) { + try { + var prot = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, "") + .toLowerCase(); + } catch (e) { + return ""; + } + if ( + prot.indexOf("javascript:") === 0 || + prot.indexOf("vbscript:") === 0 || + prot.indexOf("data:") === 0 + ) { + return ""; + } + } + var out = '<a href="' + href + '"'; + if (title) { + out += ' title="' + title + '"'; + } + out += ">" + text + "</a>"; + return out; + }; + + Renderer.prototype.image = function (href, title, text) { + var out = '<img src="' + href + '" alt="' + text + '"'; + if (title) { + out += ' title="' + title + '"'; + } + out += this.options.xhtml ? "/>" : ">"; + return out; + }; + + Renderer.prototype.text = function (text) { + return text; + }; + + Renderer.prototype.math = function (text, begin, end) { + return text; + + //// Example usage for inline TeX-processing: + // if ( begin === "$" || begin === "\\(" ) { + // return katex.renderToString(text, { displayMode: false }); + // } else if ( begin === "$$" || begin === "\\[" ) { + // return katex.renderToString(text, { displayMode: true }); + // } else { + // return katex.renderToString(begin + text + end, { displayMode: true }); + // } + }; + + /** + * Parsing & Compiling + */ + + function Parser(options) { + this.tokens = []; + this.token = null; + this.options = options || marked.defaults; + this.options.renderer = this.options.renderer || new Renderer(); + this.renderer = this.options.renderer; + this.renderer.options = this.options; + } + + /** + * Static Parse Method + */ + + Parser.parse = function (src, options, renderer) { + var parser = new Parser(options, renderer); + return parser.parse(src); + }; + + /** + * Parse Loop + */ + + Parser.prototype.parse = function (src) { + this.inline = new InlineLexer(src.links, this.options, this.renderer); + this.tokens = src.reverse(); + + var out = ""; + while (this.next()) { + out += this.tok(); + } + + return out; + }; + + /** + * Next Token + */ + + Parser.prototype.next = function () { + return (this.token = this.tokens.pop()); + }; + + /** + * Preview Next Token + */ + + Parser.prototype.peek = function () { + return this.tokens[this.tokens.length - 1] || 0; + }; + + /** + * Parse Text Tokens + */ + + Parser.prototype.parseText = function () { + var body = this.token.text; + + while (this.peek().type === "text") { + body += "\n" + this.next().text; + } + + return this.inline.output(body); + }; + + /** + * Parse Current Token + */ + + Parser.prototype.tok = function () { + switch (this.token.type) { + case "space": { + return ""; + } + case "hr": { + return this.renderer.hr(); + } + case "heading": { + return this.renderer.heading( + this.inline.output(this.token.text), + this.token.depth, + this.token.text + ); + } + case "code": { + return this.renderer.code( + this.token.text, + this.token.lang, + this.token.escaped + ); + } + case "table": { + var header = "", + body = "", + i, + row, + cell, + flags, + j; + + // header + cell = ""; + for (i = 0; i < this.token.header.length; i++) { + flags = { header: true, align: this.token.align[i] }; + cell += this.renderer.tablecell( + this.inline.output(this.token.header[i]), + { header: true, align: this.token.align[i] } + ); + } + header += this.renderer.tablerow(cell); + + for (i = 0; i < this.token.cells.length; i++) { + row = this.token.cells[i]; + + cell = ""; + for (j = 0; j < row.length; j++) { + cell += this.renderer.tablecell(this.inline.output(row[j]), { + header: false, + align: this.token.align[j], + }); + } + + body += this.renderer.tablerow(cell); + } + return this.renderer.table(header, body); + } + case "blockquote_start": { + var body = ""; + + while (this.next().type !== "blockquote_end") { + body += this.tok(); + } + + return this.renderer.blockquote(body); + } + case "list_start": { + var body = "", + ordered = this.token.ordered; + + while (this.next().type !== "list_end") { + body += this.tok(); + } + + return this.renderer.list(body, ordered); + } + case "list_item_start": { + var body = ""; + + while (this.next().type !== "list_item_end") { + body += this.token.type === "text" ? this.parseText() : this.tok(); + } + + return this.renderer.listitem(body); + } + case "loose_item_start": { + var body = ""; + + while (this.next().type !== "list_item_end") { + body += this.tok(); + } + + return this.renderer.listitem(body); + } + case "html": { + var html = + !this.token.pre && !this.options.pedantic + ? this.inline.output(this.token.text) + : this.token.text; + return this.renderer.html(html); + } + case "paragraph": { + return this.renderer.paragraph(this.inline.output(this.token.text)); + } + case "text": { + return this.renderer.paragraph(this.parseText()); + } + } + }; + + /** + * Helpers + */ + + function escape(html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function unescape(html) { + // explicitly match decimal, hex, and named HTML entities + return html.replace( + /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, + function (_, n) { + n = n.toLowerCase(); + if (n === "colon") return ":"; + if (n.charAt(0) === "#") { + return n.charAt(1) === "x" + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ""; + } + ); + } + + function replace(regex, opt) { + regex = regex.source; + opt = opt || ""; + return function self(name, val) { + if (!name) return new RegExp(regex, opt); + val = val.source || val; + val = val.replace(/(^|[^\[])\^/g, "$1"); + regex = regex.replace(name, val); + return self; + }; + } + + function noop() {} + noop.exec = noop; + + function merge(obj) { + var i = 1, + target, + key; + + for (; i < arguments.length; i++) { + target = arguments[i]; + for (key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + obj[key] = target[key]; + } + } + } + + return obj; + } + + /** + * Marked + */ + + function marked(src, opt, callback) { + if (callback || typeof opt === "function") { + if (!callback) { + callback = opt; + opt = null; + } + + opt = merge({}, marked.defaults, opt || {}); + + var highlight = opt.highlight, + tokens, + pending, + i = 0; + + try { + tokens = Lexer.lex(src, opt); + } catch (e) { + return callback(e); + } + + pending = tokens.length; + + var done = function (err) { + if (err) { + opt.highlight = highlight; + return callback(err); + } + + var out; + + try { + out = Parser.parse(tokens, opt); + } catch (e) { + err = e; + } + + opt.highlight = highlight; + + return err ? callback(err) : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + + if (!pending) return done(); + + for (; i < tokens.length; i++) { + (function (token) { + if (token.type !== "code") { + return --pending || done(); + } + return highlight(token.text, token.lang, function (err, code) { + if (err) return done(err); + if (code == null || code === token.text) { + return --pending || done(); + } + token.text = code; + token.escaped = true; + --pending || done(); + }); + })(tokens[i]); + } + + return; + } + try { + if (opt) opt = merge({}, marked.defaults, opt); + return Parser.parse(Lexer.lex(src, opt), opt); + } catch (e) { + e.message += "\nPlease report this to https://github.com/chjj/marked."; + if ((opt || marked.defaults).silent) { + return ( + "<p>An error occured:</p><pre>" + + escape(e.message + "", true) + + "</pre>" + ); + } + throw e; + } + } + + /** + * Options + */ + + marked.options = marked.setOptions = function (opt) { + merge(marked.defaults, opt); + return marked; + }; + + marked.defaults = { + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: false, + sanitizer: null, + mangle: true, + smartLists: false, + silent: false, + highlight: null, + langPrefix: "lang-", + smartypants: false, + headerPrefix: "", + mathDelimiters: null, + renderer: new Renderer(), + xhtml: false, + }; + + /** + * Expose + */ + + marked.Parser = Parser; + marked.parser = Parser.parse; + + marked.Renderer = Renderer; + + marked.Lexer = Lexer; + marked.lexer = Lexer.lex; + + marked.InlineLexer = InlineLexer; + marked.inlineLexer = InlineLexer.output; + + marked.parse = marked; + + if (typeof module !== "undefined" && typeof exports === "object") { + module.exports = marked; + } else if (typeof define === "function" && define.amd) { + define(function () { + return marked; + }); + } else { + this.marked = marked; + } +}.call( + (function () { + return this || (typeof window !== "undefined" ? window : global); + })() +)); diff --git a/static/assets/js/pages/admin.js b/static/assets/js/pages/admin.js index 8dc8578..028601a 100644 --- a/static/assets/js/pages/admin.js +++ b/static/assets/js/pages/admin.js @@ -2,16 +2,6 @@ const loginInfo = document.getElementById("login-info"); const loginForm = document.getElementById("login-page"); window.addEventListener("DOMContentLoaded", () => { - const firebaseConfig = { - apiKey: "AIzaSyBCmKUnEmm8hLR9ZcFWPYbYiplbP6yUzfU", - authDomain: "thatcomputerscientist-e9cf2.firebaseapp.com", - projectId: "thatcomputerscientist-e9cf2", - storageBucket: "thatcomputerscientist-e9cf2.appspot.com", - messagingSenderId: "178402875544", - appId: "1:178402875544:web:8c9d8880d3ef495a5658ed", - measurementId: "G-JECWWZG5J6", - }; - firebase.initializeApp(firebaseConfig); const signInButton = document.getElementById("signInButton"); signInButton.addEventListener("click", () => { const email = document.getElementById("inputEmail").value; diff --git a/static/assets/js/pages/authCheck.js b/static/assets/js/pages/authCheck.js new file mode 100644 index 0000000..722fe0c --- /dev/null +++ b/static/assets/js/pages/authCheck.js @@ -0,0 +1,10 @@ +firebase.auth().onAuthStateChanged((user) => { + if (!user) { + window.location.assign("/admin/"); + } +}); +document.getElementById("logout").addEventListener("click", () => { + firebase.auth().signOut().then(() => { + window.location.assign("/admin/"); + }); +}); diff --git a/static/assets/js/pages/config.js b/static/assets/js/pages/config.js new file mode 100644 index 0000000..256173a --- /dev/null +++ b/static/assets/js/pages/config.js @@ -0,0 +1,10 @@ +const firebaseConfig = { + apiKey: "AIzaSyBCmKUnEmm8hLR9ZcFWPYbYiplbP6yUzfU", + authDomain: "thatcomputerscientist-e9cf2.firebaseapp.com", + projectId: "thatcomputerscientist-e9cf2", + storageBucket: "thatcomputerscientist-e9cf2.appspot.com", + messagingSenderId: "178402875544", + appId: "1:178402875544:web:8c9d8880d3ef495a5658ed", + measurementId: "G-JECWWZG5J6", +}; +firebase.initializeApp(firebaseConfig); |
