diff --git a/README.md b/README.md index 2ada2bc..ff68996 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# cavern -A simple personal blog system. +# Cavern +> Explore those deep inside the cave. +A simple blog system. + +## Feature + +## Requirements + +## Install + +## Libraries \ No newline at end of file diff --git a/account.php b/account.php new file mode 100644 index 0000000..dd1c3b8 --- /dev/null +++ b/account.php @@ -0,0 +1,223 @@ +query("INSERT INTO `user` (`username`, `pwd`, `name`, `email`) VALUES ('%s', '%s', '%s', '%s')", array($username, cavern_password_hash($_POST['password'], $username), htmlspecialchars($_POST['name']), $_POST['email'])); + header('axios-location: index.php?ok=reg'); + } else { + http_response_code(400); + header('axios-location: index.php?err=miss'); + } + exit; + } else { + http_response_code(409); // 409 Conflict + header('axios-location: account.php?new&err=used'); + exit; + } +} else if (isset($_SESSION['cavern_username']) && isset($_POST['username']) && isset($_POST['old']) && (isset($_POST['name']) || isset($_POST['new']))) { + // modify account data + if (!validate_csrf()) { + http_response_code(403); + header('axios-location: account.php'); + exit; + } + $username = $_POST['username']; + if ($username != $_SESSION['cavern_username']) { + // not the same person + http_response_code(403); + header('axios-location: account.php?err=edit'); + exit; + } else { + // confirm old password and mofify account data + $original = cavern_query_result("SELECT * FROM `user` WHERE `username`='%s'", array($username)); + if (!hash_equals(cavern_password_hash($_POST['old'], $username), $original['row']['pwd']) || $original['num_rows'] == 0) { + http_response_code(403); + header('axios-location: account.php?err=old'); + exit; + } else { + if (trim($_POST['new']) != '') { + $password = cavern_password_hash($_POST['new'], $username); + $SQL->query("UPDATE `user` SET `pwd`='%s' WHERE `username`='%s'", array($password, $username)); + } + if (trim($_POST['name']) != '' && strlen($_POST['name']) <= 40) { + $SQL->query("UPDATE `user` SET `name`='%s' WHERE `username`='%s'", array(htmlspecialchars($_POST['name']), $username)); + } else { + http_response_code(400); + header('axios-location: account.php?err=miss'); + exit; + } + if (trim($_POST['email']) != '' && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) { + $emailExist = cavern_query_result("SELECT * FROM `user` WHERE NOT `username`='%s' AND `email`='%s'", array($username, $_POST["email"])); + if ($emailExist['num_rows'] == 0) { + $SQL->query("UPDATE `user` SET `email`='%s' WHERE `username`='%s'", array($_POST['email'], $username)); + } else { + http_response_code(400); + header('axios-location: account.php?err=used'); + exit; + } + } else { + http_response_code(400); + header('axios-location: account.php?err=miss'); + exit; + } + header('axios-location: account.php?ok=edit'); + exit; + } + } +} else if (!isset($_SESSION['cavern_username']) && !isset($_GET['new'])) { + // if mode isn't definded, redirect to register page + header('Location: account.php?new'); + exit; +} else if (isset($_SESSION['cavern_username']) && isset($_GET['new'])) { + // if someone is logged in, then redirect to account setting page + header('Location: account.php'); + exit; +} + +// create new account +if (isset($_GET['new'])) { + $view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], "註冊"); + if (!$blog['register']) { + $view->show_message('inverted negative', "抱歉,目前暫停註冊"); + $view->render(); + exit; + } + + if (isset($_GET['err'])) { + if ($_GET['err'] == "miss") { + $view->show_message('inverted negative', "請正確填寫所有欄位"); + } else if ($_GET['err'] == "used") { + $view->show_message('inverted negative', "此使用者名稱或是信箱已被使用"); + } + } + + $view->add_script("./include/js/security.js"); + $view->add_script("./include/js/account.js"); +?> +
+
+
註冊
+
+ + + 上限20字元 (小寫英文、數字、底線以及連字號)。首字元必須為英文。 + 你未來將無法更改這項設定。 +
+
+ + + 上限40字元。 +
+
+ + +
+
+ + +
+
+ + + 用於辨識頭貼。(Powered by Gravatar +
+ +
+
+render(); +} else { +// edit account data + $username = $_SESSION['cavern_username']; + $result = cavern_query_result("SELECT * FROM `user` WHERE `username`='%s'", array($username)); + $name = $result['row']['name']; + $email = $result['row']['email']; + + $view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "帳號"); + $view->add_script_source("ts('.ts.dropdown').dropdown();"); + $view->add_script("./include/js/security.js"); + $view->add_script("./include/js/account.js"); + + if (isset($_GET['err'])) { + switch ($_GET['err']) { + case 'edit': + $view->show_message('inverted negative', "修改失敗"); + break; + case 'old': + $view->show_message('inverted negative', "舊密碼錯誤"); + break; + case "miss": + $view->show_message('inverted negative', "請正確填寫所有欄位"); + break; + case "used": + $view->show_message('inverted negative', "此信箱已被其他帳號使用"); + break; + } + } + if (isset($_GET['ok'])) { + if ($_GET['ok'] == "edit") { + $view->show_message('inverted positive', "修改成功!"); + } + } +?> +
+
+
編輯帳號
+
+
+ +
+ +
+
?
+
+
+
+ + +
+
+ + + 上限40字元。 +
+
+
+
+ + + 透過電子信箱,在 Gravatar 更改你的頭貼。 +
+
+ + +
+
+ + + 留空則不修改。 +
+
+ + + 重複新密碼。 +
+ +
+
+render(); +} +?> \ No newline at end of file diff --git a/admin/ajax/config.php b/admin/ajax/config.php new file mode 100644 index 0000000..d6f2562 --- /dev/null +++ b/admin/ajax/config.php @@ -0,0 +1,60 @@ +valid) { + send_error(403, "novalid"); +} else if (!($user->level >= 8)) { + send_error(403, "nopermission"); +} + +if ($_SERVER["REQUEST_METHOD"] == "POST") { + // modify blog settings + if (!validate_csrf()) { + send_error(403, "csrf"); + } + + $config_filename = "../../config.php"; + $template_filename = "../../config.template"; + + if (!is_writable($config_filename)) { + send_error(500, "notwritable"); + } + + try { + $limit = abs(intval(@$_POST["limit"])); + $content = file_get_contents($template_filename); + $new_content = strtr($content, array( + "{blog_name}" => addslashes(@$_POST["name"]), + "{limit}" => ($limit != 0 ? $limit : 10), + "{register}" => (@$_POST["register"] === "true" ? "true" : "false") + )); + + file_put_contents($config_filename, $new_content); + $result = json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000))); + } catch (Exception $e) { + http_response_code(500); + $result = json_encode(array('status' => $e->getMessage(), "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000))); + } + + header('Content-Type: application/json'); + echo $result; + exit; +} else if ($_SERVER["REQUEST_METHOD"] == "GET") { + // fetch settings + header('Content-Type: application/json'); + echo json_encode(array_merge(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000)), $blog)); + exit; +} + +function send_error($code, $message) { + http_response_code($code); + header('Content-Type: application/json'); + echo json_encode(array('status' => $message)); + exit; +} diff --git a/admin/ajax/statistics.php b/admin/ajax/statistics.php new file mode 100644 index 0000000..5da8f46 --- /dev/null +++ b/admin/ajax/statistics.php @@ -0,0 +1,20 @@ +valid) { + send_error(403, "novalid"); +} else if (!($user->level >= 8)) { + send_error(403, "nopermission"); +} + +$post_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `post`")['row']['count']); +$user_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `user`")['row']['count']); +$comment_count = intval(cavern_query_result("SELECT COUNT(*) AS `count` FROM `comment`")['row']['count']); + +header('Content-Type: application/json'); +echo json_encode(array("fetch" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "name" => $blog['name'], "post" => $post_count, "user" => $user_count, "comment" => $comment_count)); \ No newline at end of file diff --git a/admin/ajax/user.php b/admin/ajax/user.php new file mode 100644 index 0000000..4dfe44b --- /dev/null +++ b/admin/ajax/user.php @@ -0,0 +1,114 @@ +valid) { + send_error(403, "novalid"); +} else if (!($user->level >= 8)) { + send_error(403, "nopermission"); +} + +if ($_SERVER["REQUEST_METHOD"] == "POST") { + // modify account data + if (!validate_csrf()) { + send_error(403, "csrf"); + } + + if (isset($_POST['username']) && (isset($_POST['name']) || isset($_POST['password']))) { + // modify account data + $username = trim($_POST['username']); + + try { + $target_user = new User($username); + } catch (NoUserException $e) { + send_error(404, "nouser"); + } + + if (trim($_POST['password']) != '') { + $password = cavern_password_hash($_POST['password'], $username); + $SQL->query("UPDATE `user` SET `pwd`='%s' WHERE `username`='%s'", array($password, $username)); + } + if (trim($_POST['name']) != '' && strlen($_POST['name']) <= 40) { + $SQL->query("UPDATE `user` SET `name`='%s' WHERE `username`='%s'", array(htmlspecialchars($_POST['name']), $username)); + } else { + send_error(400, "noname"); + } + if (trim($_POST['email']) != '' && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) { + $emailExist = cavern_query_result("SELECT * FROM `user` WHERE NOT `username`='%s' AND `email`='%s'", array($username, $_POST["email"])); + if ($emailExist['num_rows'] == 0) { + $SQL->query("UPDATE `user` SET `email`='%s' WHERE `username`='%s'", array($_POST['email'], $username)); + } else { + send_error(400, "emailused"); + } + } else { + send_error(400, "noemail"); + } + + $SQL->query("UPDATE `user` SET `muted`='%d' WHERE `username`='%s'", array($_POST['muted'], $username)); + + header("Content-Type: application/json"); + echo json_encode(array("status" => TRUE, "modified" => $username)); + exit; + } +} else if ($_SERVER["REQUEST_METHOD"] == "GET") { + // fetch user list (we can fetch single user data from ajax) + $user_list = array(); + + $user_query = cavern_query_result("SELECT * FROM `user`", array()); + if ($user_query['num_rows'] > 0) { + do { + $data = $user_query['row']; + + $user_list[] = array( + "id" => intval($data['id']), + "username" => $data['username'], + "name" => $data['name'], + "email" => $data['email'], + "role" => cavern_level_to_role($data['level']) + ); + } while ($user_query['row'] = $user_query['query']->fetch_assoc()); + } + header('Content-Type: application/json'); + echo json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "list" => $user_list)); + exit; +} else if ($_SERVER["REQUEST_METHOD"] == "DELETE") { + // delete user account + $username = trim($_GET['username']); + + try { + $target_user = new User($username); + } catch (NoUserException $e) { + send_error(404, "nouser"); + } + + // you cannot delete site owner + if ($target_user->level === 9) { + send_error(403, "deleteowner"); + } + + /* cleanup user data */ + // Although we set foreign key, in fact `ON CASCADE` cannot fire trigger + // like cleanup + $SQL->query("DELETE FROM `like` WHERE `username`='%s'", array($target_user->username)); + // comment cleanup + $SQL->query("DELETE FROM `comment` WHERE `username`='%s'", array($target_user->username)); + + // now we can delete the user data + $SQL->query("DELETE FROM `user` WHERE `username`='%s'", array($target_user->username)); + + header('Content-Type: application/json'); + echo json_encode(array('status' => TRUE, "time" => round($_SERVER["REQUEST_TIME_FLOAT"] * 1000), "deleted" => $username)); + exit; +} + +function send_error($code, $message) { + http_response_code($code); + header('Content-Type: application/json'); + echo json_encode(array('status' => $message)); + exit; +} diff --git a/admin/component/config.js b/admin/component/config.js new file mode 100644 index 0000000..ae88bb0 --- /dev/null +++ b/admin/component/config.js @@ -0,0 +1,17 @@ +(() => { + function create(tag) { + return document.createElement(tag); + } + + pageManager.register("config", function () { + return { + render: function (...args) { + pageManager.setHeader("設定"); + setTimeout(() => { + pageManager.document.innerHTML = "config"; + pageManager.setLoaderState(false) + }, 500); + } + } + }); +})(); diff --git a/admin/component/post.js b/admin/component/post.js new file mode 100644 index 0000000..38a5e7d --- /dev/null +++ b/admin/component/post.js @@ -0,0 +1,17 @@ +(() => { + function create(tag) { + return document.createElement(tag); + } + + pageManager.register("post", function () { + return { + render: function (...args) { + pageManager.setHeader("文章"); + setTimeout(() => { + pageManager.document.innerHTML = args[0] + args[1]; + pageManager.setLoaderState(false) + }, 500); + } + } + }); +})(); diff --git a/admin/component/statistics.js b/admin/component/statistics.js new file mode 100644 index 0000000..13b5331 --- /dev/null +++ b/admin/component/statistics.js @@ -0,0 +1,70 @@ +(() => { + function fetchStatistics() { + axios.request({ + method: "GET", + url: "ajax/statistics.php" + }).then(function (res) { + renderPage(res.data); + }).catch(function (err) { + if (err.response) { + console.error(err.response.status); + } + }); + } + + function renderPage(data) { + let iconName = { + "post": "file text", + "user": "users", + "comment": "comments" + }; + + let statLabel = { + "post": "文章總數", + "user": "使用者總數", + "comment": "留言總數" + }; + + let statTemplate = `
{{ value }}
{{ label }}
` + + let cardContainer = create('div'); cardContainer.className = "ts stackable three cards"; + + for (let key in data) { + if (Object.keys(statLabel).indexOf(key) != -1) { + let card = create('div'); card.className = "ts card"; + let content = create('div'); content.className = "content"; + let symbol = create('div'); symbol.className = "symbol"; + let icon = create('i'); icon.className = `${iconName[key]} icon`; + + content.innerHTML = statTemplate.replace("{{ value }}", data[key]).replace("{{ label }}", statLabel[key]); + symbol.appendChild(icon); + card.appendChild(content); + card.appendChild(symbol); + + cardContainer.appendChild(card); + } else if (key === "name") { + // blog name + } + } + + // finish up + setTimeout(() => { + pageManager.document.innerHTML = ""; + pageManager.document.appendChild(cardContainer); + pageManager.setLoaderState(false) + }, 500); + } + + function create(tag) { + return document.createElement(tag); + } + + pageManager.register("statistics", function () { + return { + render: function (...args) { + pageManager.setHeader("總覽"); + fetchStatistics(); + } + } + }); +})(); diff --git a/admin/component/user.js b/admin/component/user.js new file mode 100644 index 0000000..332bf30 --- /dev/null +++ b/admin/component/user.js @@ -0,0 +1,17 @@ +(() => { + function create(tag) { + return document.createElement(tag); + } + + pageManager.register("user", function () { + return { + render: function (...args) { + pageManager.setHeader("使用者"); + setTimeout(() => { + pageManager.document.innerHTML = args[0]; + pageManager.setLoaderState(false) + }, 500); + } + } + }); +})(); diff --git a/admin/dashboard.js b/admin/dashboard.js new file mode 100644 index 0000000..ce6adaf --- /dev/null +++ b/admin/dashboard.js @@ -0,0 +1,114 @@ +$(document).ready(function () { + var $sidebar = $('.ts.sidebar'); + let mq = window.matchMedia("(max-width: 768px)"); + mq.addListener(sidebarOnMobile); + + sidebarOnMobile(mq); // run first + function sidebarOnMobile(q) { + if (q.matches) { + /* mobile -> hide sidebar */ + $sidebar.toggleClass("animating", true); + $sidebar.toggleClass("visible", false); + } else { + /* non-mobile -> show sidebar */ + $sidebar.toggleClass("animating", false); + $sidebar.toggleClass("visible", true); + } + } + + $('button#toggleSidebar').on('click', function (e) { + $sidebar.toggleClass('visible'); + }); +}); + +function Manager(element) { + this.components = {}; + this.cache = {}; // args cache for components just be on load + + this.document = element; +} + +Manager.prototype = { + load: function(name, ...args) { + this.setLoaderState(true); + if (Object.keys(this.components).indexOf(name) == -1) { + // cache args + this.cache[name] = args; + script = document.createElement("script"); + script.src = `component/${name}.js`; + document.body.appendChild(script); + } else { + this.components[name].render(...args); + } + }, + register: function(name, init) { + this.components[name] = init(); + let args = this.cache[name]; + this.components[name].render(...args); + delete this.cache[name]; + }, + setHeader: function (title) { + $('#header').text(title); + }, + setLoaderState: function (state) { + $('.pusher .dimmer').toggleClass('active', state); + } +} + +let pageManager = new Manager(document.querySelector('#content')); + +var root = "./", useHash = true, hash = "#"; +let router = new Navigo(root, useHash, hash); + +if (location.href.slice(-1) === "/") { + // catch to navigo + router.navigate("/"); +} + +router.on({ + "/": function () { + // system overview + render("statistics"); + }, + "/post": function () { + render("post", "page", 1); + }, + "/post/:pid": function (params) { + render("post", "pid", params.pid); + }, + "/post/page/:page": function (params) { + render("post", "page", params.page); + }, + "/user": function () { + render("user", "list"); + }, + "/user/add": function () { + render("user", "add"); + }, + "/user/:username": function (params) { + render("user", "username", params.username); + }, + "/config": function () { + render("config"); + } +}).resolve(); + +router.updatePageLinks(); + +function render(page, ...args) { + switch (page) { + case "user": + pageManager.load("user", ...args); + break; + case "post": + pageManager.load("post", ...args); + break; + case "config": + pageManager.load("config", ...args); + break; + case "statistics": + default: + pageManager.load("statistics", ...args); + break; + } +} \ No newline at end of file diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..97cbd0d --- /dev/null +++ b/admin/index.php @@ -0,0 +1,28 @@ +valid) { + http_response_code(403); + header("Location: ../index.php?err=account"); + exit; +} + +if (!$user->islogin) { + http_response_code(401); + header('Location: ../login.php?next=admin'); + exit; +} else if ($user->level < 8) { + http_response_code(403); + header('Location: ../index.php?err=permission'); + exit; +} + +$view = new View('./theme/dashboard.html', 'theme/avatar.php', '', $blog['name'], "管理介面"); +$view->render(); \ No newline at end of file diff --git a/admin/theme/admin.css b/admin/theme/admin.css new file mode 100644 index 0000000..d89097e --- /dev/null +++ b/admin/theme/admin.css @@ -0,0 +1,38 @@ +/* nav bar */ +button#toggleSidebar { + border-radius: 0; + border-bottom-right-radius: .28571rem; + visibility: hidden; +} + +.pusher > nav { + z-index: 14; + position: sticky; + position: -webkit-sticky; /* safari */ + left: 0; + top: 0; +} + +/* content */ +#header { + margin-top: 5px; +} + +@media (max-width: 768px) { + .ts.visible.sidebar:not(.overlapped) ~ .pusher.squeezable { + /* RWD fix */ + width: 100vw; + left: 0; + } + + button#toggleSidebar { + visibility: visible; + } + + .visible.sidebar ~ .pusher > nav { + width: calc(100vw - 230px); + transform: translate3d(230px, 0, 0); + transition: transform .45s cubic-bezier(0.23, 1, 0.32, 1); + will-change: transform; + } +} \ No newline at end of file diff --git a/admin/theme/avatar.php b/admin/theme/avatar.php new file mode 100644 index 0000000..85e7ad3 --- /dev/null +++ b/admin/theme/avatar.php @@ -0,0 +1,12 @@ + +
+ +

+
name ?>
+
\ No newline at end of file diff --git a/admin/theme/dashboard.html b/admin/theme/dashboard.html new file mode 100644 index 0000000..05d9d34 --- /dev/null +++ b/admin/theme/dashboard.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + {part} | {title} + + + + +
+
+
+
+ +
+ +
+
+
+ + + + + + {script} + + \ No newline at end of file diff --git a/ajax/comment.php b/ajax/comment.php new file mode 100644 index 0000000..106c926 --- /dev/null +++ b/ajax/comment.php @@ -0,0 +1,238 @@ +valid) { + http_response_code(403); + header("Content-Type: applcation/json"); + echo json_encode(array('status' => 'novalid')); + exit; +} + +if (!isset($_GET['pid']) && !isset($_GET['del']) && !isset($_POST['pid']) && !isset($_POST['edit'])) { + send_error(404, "error"); +} else { + if (isset($_GET['pid']) && trim($_GET['pid']) != "") { + if (isset($_SESSION['cavern_comment_time']) && $_SERVER['REQUEST_TIME'] - $_SESSION['cavern_comment_time'] > 10) { + // after 10 seconds + $_SESSION['cavern_comment_time'] = NULL; + unset($_SESSION['cavern_comment_time']); + } + $data = process_comments($_GET['pid']); + } else { + if (!$user->islogin) { // guest + send_error(401, "nologin"); + } + if (!validate_csrf()) { + send_error(403, "csrf"); + } + + if (isset($_GET['del']) && trim($_GET['del']) != "") { + // delete comment + $result = cavern_query_result("SELECT * FROM `comment` WHERE `id`='%d'", array($_GET['del'])); + if ($result['num_rows'] < 1) { + send_error(404, "error"); + } + + $author = $result['row']['username']; + + if ($author !== $user->username) { + send_error(403, false); + } + + $SQL->query("DELETE FROM `comment` WHERE `id`='%d' AND `username`='%s'", array($_GET['del'], $user->username)); + $data = array( + "status" => TRUE, + "time" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000) + ); + } else if (isset($_POST['content'])) { + if (isset($_POST['pid']) && trim($_POST['pid']) && isset($_SESSION['cavern_comment_time']) && $_SERVER['REQUEST_TIME'] - $_SESSION['cavern_comment_time'] < 10) { + // user can create one comment per 10 seconds + $remain_second = 10 - ($_SERVER['REQUEST_TIME'] - $_SESSION['cavern_comment_time']); + header('Retry-After: ' . $remain_second); + send_error(429, "ratelimit"); + } + + if ($user->muted) { + send_error(403, "muted"); + } + + if (trim($_POST['content']) != "") { + if (isset($_POST['pid']) && trim($_POST['pid']) != "") { + // new comment + try { + $article = new Article(intval($_POST['pid'])); + } catch (NoPostException $e) { + send_error(404, "error"); + } + + http_response_code(201); // 201 Created + $time = date('Y-m-d H:i:s'); + $SQL->query("INSERT INTO `comment` (`pid`, `username`, `time`, `content`) VALUES ('%d', '%s', '%s', '%s')", array($_POST['pid'], $user->username, $time, htmlspecialchars($_POST['content']))); + $comment_id = $SQL->insert_id(); + $data = array( + "status" => TRUE, + "comment_id" => $comment_id, + "time" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000) + ); + + /* notification */ + + // notify tagged user + // the user who tag himself is unnecessary to notify + $username_list = parse_user_tag($_POST['content']); + foreach ($username_list as $key => $id) { + if ($id == $user->username) continue; + cavern_notify_user($id, "{{$user->name}}@{$user->username} 在 [{$article->title}] 的留言中提到了你", "post.php?pid={$article->pid}#comment-$comment_id", "comment"); + } + + // notify commenters + $commenters = cavern_query_result("SELECT `username` FROM `comment` WHERE `pid` = '%d'", array($_POST['pid'])); + if ($commenters['num_rows'] > 0) { + do { + $u = $commenters['row']['username']; + if (!in_array($u, $username_list) && $u != $article->author && $u != $user->username) { + cavern_notify_user($u, "在你回應的文章 [{$article->title}] 中有了新的回應", "post.php?pid={$article->pid}#comment-$comment_id", "comment"); + } + } while ($commenters['row'] = $commenters['query']->fetch_assoc()); + } + + // notify liked user + /* we won't inform the author for his like on his own post + and no notice for his own comment */ + $likers = cavern_query_result("SELECT `username` FROM `like` WHERE `pid` = '%d'", array($_POST['pid'])); + if ($likers['num_rows'] > 0) { + do { + $u = $likers['row']['username']; + if (!in_array($u, $username_list) && $u != $article->author && $u != $user->username) { + cavern_notify_user($u, "在你喜歡的文章 [{$article->title}] 中有了新的回應", "post.php?pid={$article->pid}#comment-$comment_id", "comment"); + } + } while ($likers['row'] = $likers['query']->fetch_assoc()); + } + + // notify post author + /* we won't inform the author if he has been notified for being tagged + also, we won't notify the author for his own comment */ + if (!in_array($article->author, $username_list) && $article->author != $user->username) { + cavern_notify_user($article->author, "{{$user->name}}@{$user->username} 回應了 [{$article->title}]", "post.php?pid={$article->pid}#comment-$comment_id", "comment"); + } + + // only new comment should be limited + $_SESSION['cavern_comment_time'] = $_SERVER['REQUEST_TIME']; + } else if (isset($_POST['edit']) && trim($_POST['edit']) != "") { + // edit comment + $query = cavern_query_result("SELECT * FROM `comment` WHERE `id` = '%d'", array($_POST['edit'])); + if ($query['num_rows'] < 1) { + send_error(404, "error"); + } + if ($query['row']['username'] !== $user->username) { + send_error(403, "author"); + } + + $time = date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']); + $SQL->query("UPDATE `comment` SET `content`='%s', `modified`='%s' WHERE `id`='%d' AND `username`='%s'", array(htmlspecialchars($_POST['content']), $time, $_POST['edit'], $user->username)); + $data = array( + "status" => TRUE, + "comment_id" => $_POST['edit'], + "time" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000) + ); + } else { + send_error(400, "empty"); + } + } else { + send_error(400, "empty"); + } + } + } +} + +header('Content-Type: application/json'); +echo json_encode($data); +exit; + +function process_comments($pid) { + if (isset($_SESSION["cavern_username"])) { + $user = new User($_SESSION["cavern_username"]); + } else { + $user = new User(""); // guest + } + + if (cavern_query_result("SELECT * FROM `post` WHERE `pid`=%d", array($pid))['num_rows'] < 1) { + http_response_code(404); + $json = array('status' => 'error', 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)); // to fit javascript unit + return $json; + } + + if (isset($_COOKIE['cavern_commentLastFetch'])) { + $last_fetch_time = $_COOKIE['cavern_commentLastFetch']; + } + + $email_hash = array(); + $names = array(); + $id_list = array(); + $modified = array(); + $comments = array(); + $result = cavern_query_result("SELECT * FROM `comment` WHERE `pid`='%d'", array($pid)); + $json = array('status' => TRUE, 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)); // to fit javascript unit + + if ($result['num_rows'] > 0) { + do { + $username = $result['row']['username']; + if (!isset($names[$username])) { + $target_user = new User($username); + $name = $target_user->name; + $email = $target_user->email; + + $names[$username] = $name; + $email_hash[$username] = md5(strtolower($email)); + } + + $comment = array( + "id" => $result['row']['id'], + "username" => $username, + "markdown" => $result['row']['content'], + "time" => $result['row']['time'], + "modified" => (is_null($result['row']['modified']) ? FALSE : $result['row']['modified']) + // if the comment has been modified, set this value as modified time; otherwise, set to FALSE + ); + + if ($user->islogin && $user->username === $username) { + $comment['actions'] = array("reply", "edit", "del"); + } else if ($user->islogin) { + $comment['actions'] = array("reply"); + } else { + $comment['actions'] = array(); + } + $id_list[] = $comment['id']; // append id + $comments[] = $comment; // append comment + + if (!is_null($result['row']['modified']) && isset($last_fetch_time)) { + if (strtotime($result['row']['modified']) - $last_fetch_time > 0) { + $modified[] = $comment["id"]; + } + } + } while ($result['row'] = $result['query']->fetch_assoc()); + } + $json['idList'] = $id_list; + $json['modified'] = $modified; + $json['comments'] = $comments; + $json['names'] = $names; + $json['hash'] = $email_hash; + + return $json; +} + +function send_error($code, $message) { + http_response_code($code); + header('Content-Type: application/json'); + echo json_encode(array('status' => $message, 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000))); // to fit javascript timestamp + exit; +} +?> \ No newline at end of file diff --git a/ajax/like.php b/ajax/like.php new file mode 100644 index 0000000..b6fc615 --- /dev/null +++ b/ajax/like.php @@ -0,0 +1,109 @@ +valid) { + http_response_code(403); + header("Content-Type: applcation/json"); + echo json_encode(array('status' => 'novalid')); + exit; +} + +if (!isset($_GET['pid'])) { + http_response_code(404); + $data = array('status' => 'error'); +} else { + $pid = $_GET['pid']; + + $article = cavern_query_result("SELECT * FROM `post` WHERE `pid`='%d'", array($pid)); + if ($article['num_rows'] < 1) { + http_response_code(404); + echo json_encode(array('status' => 'nopost', 'id' => $pid)); + exit; + } + + $likes_query = process_like($pid, $user); + + $islike = $likes_query[0]; + $likes = $likes_query[1]; + $likers = $likes_query[2]; + + if (isset($_GET['fetch'])) { + // fetch likes + $data = array('status' => 'fetch', 'id' => $pid, 'likes' => $likes, 'likers' => $likers); + header('Content-Type: application/json'); + echo json_encode($data); + exit; + } else if (!$user->islogin) { + // ask guest to login + $data = array('status' => 'nologin', 'id' => $pid, 'likes' => $likes, 'likers' => $likers); + http_response_code(401); + header('Content-Type: application/json'); + echo json_encode($data); + exit; + } else { + // user like actions + if (!validate_csrf()) { // csrf attack! + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode(array('status' => 'csrf', 'id' => $pid, 'likes' => $likes, 'likers' => $likers)); + exit; + } + + if ($islike) { + // unlike + $SQL->query("DELETE FROM `like` WHERE `pid`='%d' AND `username`='%s'", array($pid, $user->username)); + $result = process_like($pid, $user); + $likes = $result[1]; + $likers = $result[2]; + $data = array('status' => FALSE, 'id' => $pid, 'likes' => $likes, 'likers' => $likers); + } else { + // like + $SQL->query("INSERT INTO `like` (`pid`, `username`) VALUES ('%d', '%s')", array($pid, $user->username)); + $result = process_like($pid, $user); + $likes = $result[1]; + $likers = $result[2]; + $data = array('status' => TRUE, 'id' => $pid, 'likes' => $likes, 'likers' => $likers); + + /* notification */ + // notify article author + // we should notify author this only once + $author = $article['row']['username']; + $notification_query = cavern_query_result("SELECT * FROM `notification` WHERE `username`='%s' AND `url`='%s' AND `type`='%s'", array($author, "post.php?pid=$pid", "like")); + if (!($notification_query['num_rows'] > 0) && $user->username !== $author) { + cavern_notify_user($author, "{{$user->name}}@{$user->username} 推了 [{$article['row']['title']}]", "post.php?pid=$pid", "like"); + } + } + } +} + +function process_like($pid, $user) { + $islike = false; + $likers = array(); + $likes_query = cavern_query_result("SELECT * FROM `like` WHERE `pid`='%d'", array($pid)); + + if ($likes_query['num_rows'] < 1){ + $likes = 0; + } else { + $likes = $likes_query['num_rows']; + do { + $likers[] = $likes_query['row']['username']; + if ($user->username === $likes_query['row']['username']) { + $islike = true; + } + } while ($likes_query['row'] = $likes_query['query']->fetch_assoc()); + } + + return array($islike, $likes, array_unique($likers)); +} + +header('Content-Type: application/json'); +echo json_encode($data); +exit; +?> \ No newline at end of file diff --git a/ajax/notification.php b/ajax/notification.php new file mode 100644 index 0000000..efe7a78 --- /dev/null +++ b/ajax/notification.php @@ -0,0 +1,51 @@ +query("UPDATE `notification` SET `read` = 1 WHERE `read` = 0 AND `username` = '%s'", array($_SESSION['cavern_username'])); // read all comments + } else if (isset($_GET['count'])) { + $query = cavern_query_result("SELECT COUNT(*) AS `count` FROM `notification` WHERE `username` = '%s' AND `read` = 0", array($_SESSION['cavern_username'])); + $count = $query['row']['count']; + $data = array("status" => TRUE, "fetch" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000), "unread_count" => $count); + } + } else { + send_error(401, "nologin"); + } +} else { + send_error(404, "error"); +} + +header('Content-Type: application/json'); +echo json_encode($data); +exit; + +function process_notifications($limit) { + $result = cavern_query_result("SELECT * FROM `notification` WHERE `username` = '%s' ORDER BY `time` DESC LIMIT %d" ,array($_SESSION['cavern_username'], $limit)); + $json = array('status' => TRUE, 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)); // to fit javascript unit + + $feeds = array(); + + if ($result['num_rows'] > 0) { + do { + $feeds[] = $result['row']; + } while ($result['row'] = $result['query']->fetch_assoc()); + } + + $json['feeds'] = $feeds; + return $json; +} + +function send_error($code, $message) { + http_response_code($code); + header('Content-Type: application/json'); + echo json_encode(array('status' => $message, 'fetch' => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000))); // to fit javascript timestamp + exit; +} +?> \ No newline at end of file diff --git a/ajax/posts.php b/ajax/posts.php new file mode 100644 index 0000000..90590ce --- /dev/null +++ b/ajax/posts.php @@ -0,0 +1,107 @@ + round($_SERVER['REQUEST_TIME_FLOAT'] * 1000)); + +$user = validate_user(); +if (!$user->valid) { + $data["status"] = "invalid"; + http_response_code(403); + header('Content-Type: application/json'); + echo json_encode($data); + exit; +} + +if (isset($_GET['pid'])) { + // get data of single post + $pid = abs($_GET['pid']); + try { + $article = new Article($pid); + } catch (NoPostException $e) { + // post not found + http_response_code(404); + $data['message'] = $e->getMessage(); + header('Content-Type: application/json'); + echo json_encode($data); + exit; + } + + $post = array( + 'author' => $article->author, + 'name' => $article->name, + 'title' => $article->title, + 'content' => $article->content, + 'time' => $article->time, + 'likes_count' => $article->likes_count, + 'comments_count' => $article->comments_count, + 'islike' => $article->is_like($user) + ); + + $data['post'] = $post; +} else { + // get posts list + if (isset($_GET['limit']) && trim($_GET['limit']) != ""){ + $limit = intval($_GET['limit']); + } else { + $limit = intval($blog['limit']); + } + + if (isset($_GET['page']) && trim($_GET['page']) != "") { + $page = intval($_GET['page']); + $limit_start = abs(($page - 1) * $limit); + } else if (isset($_GET['username']) && trim($_GET['username']) != "") { + $mode = "username"; + } else { + $page = 1; + $limit_start = 0; + } + + if (isset($mode) && $mode == "username") { + $post_list = article_list(cavern_query_result( + "SELECT `post`.*, `user`.name FROM `post` INNER JOIN `user` ON `post`.username = `user`.username WHERE `post`.username = '%s' ORDER BY `time`", + array($_GET['username']) + )); + $all_posts_count = cavern_query_result("SELECT COUNT(*) AS `count` FROM `post` WHERE `username` = '%s'", array($_GET['username']))['row']['count']; + } else { + $post_list = article_list(cavern_query_result( + "SELECT `post`.*, `user`.name FROM `post` INNER JOIN `user` ON `post`.username = `user`.username ORDER BY `time` DESC LIMIT %d,%d", + array($limit_start, $limit) + )); + $all_posts_count = cavern_query_result("SELECT COUNT(*) AS `count` FROM `post`")['row']['count']; + + $data['page_limit'] = $limit; + $data['page'] = $page; + } + + $data['all_posts_count'] = intval($all_posts_count); + + $posts = array(); + + foreach ($post_list as $_key => $article) { + $post = array( + 'username' => $article->author, + 'name' => $article->name, + 'pid' => $article->pid, + 'title' => $article->title, + 'content' => $article->content, + 'time' => $article->time, + 'likes_count' => $article->likes_count, + 'comments_count' => $article->comments_count, + 'islike' => $article->is_like($user) + ); + + $posts[] = $post; // append post + } + + $data["posts"] = $posts; +} + +header('Content-Type: application/json'); +echo json_encode($data); +exit; +?> \ No newline at end of file diff --git a/ajax/user.php b/ajax/user.php new file mode 100644 index 0000000..3763ae4 --- /dev/null +++ b/ajax/user.php @@ -0,0 +1,60 @@ +valid) { + send_error(403, "invalid", $user->islogin); +} + +if (isset($_GET['username']) && trim($_GET['username']) != "") { + // query other user's profile + $username = trim($_GET['username']); +} else if ($user->islogin) { + // query the profile of the user himself + $username = $user->username; +} else { + // username isn't provided + send_error(404, "error"); +} + +try { + $target_user = new User($username); +} catch (NoUserException $_e) { + send_error(404, "nouser", $user->islogin); +} + +$posts = cavern_query_result("SELECT * FROM `post` WHERE `username`='%s'", array($username)); +$posts_count = ($posts['num_rows'] > 0 ? $posts['num_rows'] : 0); + +$data = array( + "username" => $target_user->username, + "name" => $target_user->name, + "level" => $target_user->level, + "role" => cavern_level_to_role($target_user->level), + "hash" => md5(strtolower($target_user->email)), + "muted" => $target_user->muted, + "posts_count" => $posts_count +); + +// user himself and admin can see user's email +if ($user->username === $target_user->username || $user->level >= 8) { + $data["email"] = $target_user->email; +} + +$data["login"] = $user->islogin; +$data["fetch"] = round($_SERVER['REQUEST_TIME_FLOAT'] * 1000); // fit javascript timestamp + +header('Content-Type: application/json'); +echo json_encode($data); +exit; + +function send_error($code, $message, $islogin) { + http_response_code($code); + header('Content-Type: application/json'); + echo json_encode(array("status" => $message, "login" => $islogin, "fetch" => round($_SERVER['REQUEST_TIME_FLOAT'] * 1000))); // to fit javascript timestamp + exit; +} \ No newline at end of file diff --git a/config.php b/config.php new file mode 100644 index 0000000..5fd9307 --- /dev/null +++ b/config.php @@ -0,0 +1,14 @@ + diff --git a/config.template b/config.template new file mode 100644 index 0000000..49c5305 --- /dev/null +++ b/config.template @@ -0,0 +1,14 @@ + diff --git a/connection/SQL.php b/connection/SQL.php new file mode 100644 index 0000000..7203cd9 --- /dev/null +++ b/connection/SQL.php @@ -0,0 +1,16 @@ +query("SET NAMES 'utf8mb4'"); diff --git a/include/article.php b/include/article.php new file mode 100644 index 0000000..5422da0 --- /dev/null +++ b/include/article.php @@ -0,0 +1,80 @@ +pid = $data; + $query = cavern_query_result("SELECT `post`.*, `user`.name FROM `post` INNER JOIN `user` ON `post`.username = `user`.username WHERE `pid`=%d", array($this->pid)); + if ($query['num_rows'] > 0) { + $result = $query['row']; + } else { + // post doesn't exist + throw new NoPostException('There is no post with pid '.$this->pid); + } + } else if (is_array($data)) { + /* pass the sql result directly */ + $result = $data; + $this->pid = $result['pid']; + } + + if (isset($result['name'])) $this->name = $result['name']; else $this->name = ""; + $this->author = $result['username']; + $this->title = $result['title']; + $this->content = $result['content']; + $this->time = $result['time']; + $this->likes_count = $result['like']; + $this->comments_count = $result['comment']; + } + + public function __get($name) { + return $this->$name; + } + + public function is_like(User $user) { + if ($this->likes_count > 0 && $user->islogin) { + $like_query = cavern_query_result("SELECT * FROM `like` WHERE `pid`='%d' AND `username`='%s'", array($this->pid, $_SESSION['cavern_username'])); + if ($like_query['num_rows'] > 0) { + $this->islike = true; + } + } + + return $this->islike; + } + + public function modify(User $user, $name, $value) { + // article author and admin can edit post + if ($user->islogin && ($user->username === $this->author || $user->level >= 8)) { + $this->$name = $value; + } else { + return false; + } + } + + public function save() { + global $SQL; + $SQL->query("UPDATE `post` SET `title`='%s', `content`='%s' WHERE `pid`='%d' AND `username`='%s'", array(htmlspecialchars($_POST['title']), htmlspecialchars($_POST['content']), $this->pid, $this->author)); + } +} + +function article_list($query_result) { + $article_list = array(); + + if ($query_result['num_rows'] > 0) { + do { + $article_list[] = new Article($query_result['row']); + } while ($query_result['row'] = $query_result['query']->fetch_assoc()); + } + + return $article_list; +} \ No newline at end of file diff --git a/include/css/cavern.css b/include/css/cavern.css new file mode 100644 index 0000000..5ca8c95 --- /dev/null +++ b/include/css/cavern.css @@ -0,0 +1,440 @@ +/* General style */ +* { + letter-spacing: .02em; +} + +img { + max-width: 100%; + object-fit: contain; +} + +div.table.wrapper { + overflow-x: auto; + width: 100%; +} + +div.table.wrapper:not(:last-child) { + margin-bottom: .75em; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + border: none; + margin-top: 0 !important; + padding-bottom: 0; +} + +.markdown-body h1:not(.ts):not(.unstyled) { + font-size: 1.6em; +} + +.markdown-body h2:not(.ts):not(.unstyled) { + font-size: 1.5em; +} + +.markdown-body h3:not(.ts):not(.unstyled) { + font-size: 1.4em; +} + +.markdown-body h4:not(.ts):not(.unstyled) { + font-size: 1.3em; +} + +.markdown-body h5:not(.ts):not(.unstyled) { + font-size: 1.2em; +} + +.markdown-body h6:not(.ts):not(.unstyled) { + font-size: 1.1em; +} + +.markdown-body > .markdown + * { /* .markdown is invisible and the first element in a post, so neighbor of .markdown is the first visible element */ + margin-top: 0 !important; +} + +.markdown-body > *:last-child:not(div) { + margin-bottom: 0 !important; +} + +.editormd-html-preview { + background-color: inherit; +} + +.editormd-html-preview code { /* inline code */ + font-size: 12px; +} + +.editormd-html-preview a { + word-break: break-all; +} + +.editormd-html-preview img { + margin: .4em 0; +} + +img.emoji { + vertical-align: middle; +} + +/* fonts */ +/* prevent this rule from overwriting the style of of KaTeX */ +.markdown-body *:not(span), .markdown-body > :not(.editormd-tex) span, +/* editor font */ +[class*="CodeMirror"] *, [class*="cm"] * { + font-family: Consolas,"SF Pro TC","SF Pro Text","SF Pro Icons","PingFang TC","Helvetica Neue","Helvetica","Microsoft JhengHei","Segoe UI",Ubuntu,微軟正黑體,"LiHei Pro","Droid Sans Fallback",Roboto,"Helvetica Neue","Droid Sans","Arial",sans-serif; +} + +.katex * { /* hack */ + font-family: KaTeX_Main, Times New Roman, serif; +} + +/* sweet alert */ +.swal2-popup h2.swal2-title { + margin: 0 0 .4em; +} + +/* menu */ +#menu button.login.button { + margin: 5px; +} + +/* notification */ +#menu .notification.icon.item i.icon { + margin-right: 0; +} + +#menu .notification.icon.item span.counter { + display: block; + padding: .1em .2em; + font-size: 12px; + color: white; + background-color: #F03434; + border-radius: .2em; + position: absolute; + top: .25em; + right: .25em; +} + +.notification.container { + display: none; + position: absolute; + z-index: 11; /* to overlap editormd */ + top: 1em; + right: .2em; + width: calc(100vw - 2.16em); + max-width: 400px; + height: 85vh; + max-height: 500px; + background-color: white; + border-radius: .28571rem; + box-shadow: 0 0 3px 0 #888888; +} + +.active.notification.container { + display: flex; + flex-direction: column; +} + +.notification.container > .ts.segment:first-child { + background-color: #EEE; +} + +.notification.container .ts.feed { + overflow-y: auto; + flex-grow: 1; + margin: 0; +} + +.notification.container .ts.feed .event { + padding-left: .8em; + padding-right: .8em; +} + +.notification.container .ts.feed .unread.event { + background-color: #e4f2f5; +} + +.notification.container .ts.feed .event:hover { + background-color: #e2edef; +} + +.notification.click.handler { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + z-index: 1; +} + +.notification.container .ts.fluid.bottom:last-child { + flex-shrink: 0; +} + +.ts.dividing.header .notification.description { + font-size: .6em; + color: gray; + margin-left: .5em; +} + +/* main */ +#main { + padding: 10px 0 20px; +} + +/* content */ +#content { + min-height: 50vh; +} + +/* pages */ +.loading#cards ~ #pages { + display: none; +} + +/* cards */ +#cards { + padding: 1em 0; +} + +.loading#cards { + display: none; +} + +.ts.card > .content > .header:not(.ts) { + font-size: 1.65em; +} + +.ts.card > .content > .description.markdown-body { + background: transparent; + font-size: inherit; + padding: 0; +} + +.ts.card > .content > .description.markdown-body a { + word-break: break-all; +} + +/* post */ +#content .ts.grid > #header > .ts.header { + /* align this with post content */ + padding-left: 15px; +} + +#content .ts.grid > .action.column { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#content > .ts.segments:not(:last-child) { + margin-bottom: 20px; +} + +#post { + font-size: 15px; + margin-top: 1em; +} + +.markdown-body ul:first-child, .markdown-body ol:first-child { + margin-top: 0; +} + +.markdown-body#post h1:not(.ts) { + font-size: 2em; +} + +.markdown-body#post h2:not(.ts) { + font-size: 1.85em; +} + +.markdown-body#post h3:not(.ts) { + font-size: 1.7em; +} + +.markdown-body#post h4:not(.ts) { + font-size: 1.55em; +} + +.markdown-body#post h5:not(.ts) { + font-size: 1.4em; +} + +.markdown-body#post h6:not(.ts) { + font-size: 1.25em; +} + +#toc { + min-height: 8em; + max-height: calc(95vh - 3em); + overflow-x: auto; +} + +/* code block */ +pre.prettyprint ol.linenums:not(.ts) { + counter-reset: code 0; + margin-left: 0; +} + +pre.prettyprint ol.linenums:not(.ts) > li::before { + counter-increment: code; + content: counter(code); + + /* line numbers align */ + right: 100%; + margin-left: 0; + padding-right: .5em; +} + +pre.prettyprint ol.linenums:not(.ts) > li > code { + min-height: 1em; /* fixing collapsed empty line */ +} + +pre.prettyprint ol.linenums:not(.ts) > li > code > span { + font-family: "YaHei Consolas Hybrid", 'Consolas', "Meiryo UI", "Malgun Gothic", "Segoe UI", "Trebuchet MS", Helvetica, monospace; +} + +/* post editor */ +#edit .action.column { + display: flex; + justify-content: center; + align-items: center; +} + +#markdownEditor:not(.editormd-fullscreen) { + margin-top: 1em; +} + +.editormd-fullscreen { + z-index: 10; +} + +/* comments */ +.ts.comments { + min-height: 6em; + max-width: 100%; +} + +.ts.comments .comment { + padding: 0.25em 0 0.25em; + margin: 0.25em 0 0.25em; +} + +.ts.no-comment.segment:not(.active), .ts.active.loader + .fetch.button { + display: none; +} + +.comment.header { + width: 100%; + position: sticky; + position: -webkit-sticky; + padding-top: .8em; + top: 0; + background-color: white; + z-index: 2; +} + +.ts.comment.divider { + margin-top: .5em; +} + +.stretched.header.column { + display: flex; + justify-content: center; + flex-direction: column; +} + +.comment .markdown-body { + padding: .2em 0; +} + +.comment .markdown-body img { + margin: .4em 0; +} + +.comment img.emoji { + height: 16px; + width: 16px; +} + +.emphasized.comment { + animation: commentEmphasize 2s ease-in .1s; +} + +/* comment editor */ +#comment > .ts.segment:first-child { + padding-top: 1em; +} + +#comment > .ts.segment:first-child > .ts.tabbed.menu { + padding: 0 1em; +} + +#comment > .ts.segment:not(:first-child) { + border-top: none; + border-bottom-left-radius: inherit; + border-bottom-right-radius: inherit; + border-bottom: 1px solid #e9e9e9; +} + +#comment .ts.button { + margin-top: 1em; +} + +#preview { + min-height: 15em; +} + +/* account */ +.ts.label.avatar.tooltip { + border: 0; + border-radius: .21429rem; +} + +.ts.form .disabled.field { + cursor: not-allowed; + pointer-events: initial; +} + +.ts.form .disabled.field input { + pointer-events: none; +} + +/* profile */ +#avatar { + width: 7.5em; +} + +/* sidebar */ +#sidebar .ts.header .avatar.image { + margin-right: 5px; +} + +#sidebar .ts.header .negative.sub.header { + color: #CE5F58; +} + +/* footer */ +footer { + padding-bottom: 15px; +} + +footer .ts.divider { + width: 80%; + margin: auto; +} + +@keyframes commentEmphasize { + from { + background-color: #e4f2f5; + } + + to { + background-color: unset; + } +} \ No newline at end of file diff --git a/include/db.php b/include/db.php new file mode 100644 index 0000000..9182d07 --- /dev/null +++ b/include/db.php @@ -0,0 +1,84 @@ + +Copyright (C) 2012-2017 太陽部落格站長 Secret + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, version 3. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. +*/ + +class Database { + private $conn; + private $addr; + private $user; + private $pass; + private $db; + + public function __construct($addr,$user,$pass,$db){ + $this->addr = $addr; + $this->user = $user; + $this->pass = $pass; + $this->db = $db; + + $this->conn = new mysqli($addr,$user,$pass,$db); + + if($this->conn->connect_error !== null){ + throw new Exception($this->conn->connect_error); + } + } + + private function reconnect(){ + $this->conn = new mysqli($this->addr,$this->user,$this->pass,$this->db); + } + + private function checkConn(){ + return $this->conn->ping(); + } + + public function query($query,$data = array()){ + if(!$this->checkConn()) $this->reconnect(); + + foreach($data as $k=>$d){ + $data[$k] = $this->conn->real_escape_string($d); + } + + $result = $this->conn->query(vsprintf($query,$data)); + + if($result === false){ + throw new Exception($this->conn->error); + } + + return $result; + } + + public function insert_id() { + return $this->conn->insert_id; + } +}; \ No newline at end of file diff --git a/include/function.php b/include/function.php new file mode 100644 index 0000000..ad746f7 --- /dev/null +++ b/include/function.php @@ -0,0 +1,117 @@ +query("SELECT `username`, `pwd` FROM `user` WHERE `username` = '%s' AND `pwd` = '%s'",array($username, cavern_password_hash($password, $username))); + if ($login->num_rows > 0) { + $_SESSION['cavern_username'] = $username; + return 1; + } + else { + return -1; + } + } else { + return -1; + } +} + +function cavern_logout() { + $_SESSION['cavern_username'] = NULL; + unset($_SESSION['cavern_username']); + return 1; +} + +function cavern_password_hash($value, $salt) { + $temp = substr(sha1(strrev($value).$salt), 0, 24); + return hash('sha512', $temp.$value); +} + +function cavern_query_result($query, $data=array()) { + global $SQL; + $result['query'] = $SQL->query($query, $data); + $result['row'] = $result['query']->fetch_assoc(); + $result['num_rows'] = $result['query']->num_rows; + if ($result['num_rows'] > 0) { + return $result; + } else { + return -1; + } +} + +function cavern_level_to_role($level) { + switch ($level) { + case 9: + $role = "站長"; + break; + case 8: + $role = "管理員"; + break; + case 1: + $role = "作者"; + break; + case 0: + $role = "會員"; + break; + default: + $role = "麥克雞塊"; + break; + } + return $role; +} + +function cavern_greeting() { + $hour = date('G'); + if ($hour >= 21 || $hour < 5) { + $greeting = "晚安"; + } else if ($hour >= 12) { + $greeting = "午安"; + } else if ($hour >= 5 && $hour < 12) { + $greeting = "早安"; + } + return $greeting; +} + +function cavern_pages($now_page, $total, $limit) { + $text='
'; + $text.=''; + $text.='
'; + return $text; +} + +function sumarize($string, $limit) { + $count = 0; + $text = ""; + $content_start = FALSE; + foreach (explode("\n", $string) as $line) { + if (trim($line) != "" && $content_start == FALSE) { + $content_start = TRUE; // don't count the empty line until the main content + } + if (!$content_start) { + continue; + } + $count++; + $text.=$line."\n"; + if ($count == $limit || mb_strlen($text) >= 200) { + if (mb_strlen($text) >= 200) { + $text = mb_substr($text, 0, 200)."...\n"; + } + $text.="...(還有更多)\n"; + break; + } + } + return $text; +} \ No newline at end of file diff --git a/include/js/account.js b/include/js/account.js new file mode 100644 index 0000000..a62c6d7 --- /dev/null +++ b/include/js/account.js @@ -0,0 +1,102 @@ + if (document.newacc) { + // Register + let form = document.newacc; + eventListenerInitialize(form, [form.password, form.repeat]); + $(form.username).on('change', function () { + let self = this; + if (!/^[a-z][a-z0-9_-]*$/.test(self.value) || (self.value.length > 20 || self.value == "")) { + setFieldStatus(self, "error", "請依照格式輸入"); + setFieldLabel(self, ""); + } else { + setFieldStatus(self, ""); // reset + setFieldLabel(self, ""); + + axios.request({ + method: "GET", + url: `ajax/user.php?username=${this.value}`, + responseType: "json" + }).then(function (_res) { + // username exist + setFieldStatus(self, "error", "此帳號已被使用"); + setFieldLabel(self, "此帳號已被使用"); + }).catch(function (_error) { + // username not exist + setFieldStatus(self, "success"); + setFieldLabel(self, "此帳號可以使用"); + }); + } + }); +} else if (document.editacc) { + // Manage Profile + let form = document.editacc; + eventListenerInitialize(form, [form.new, form.repeat]); + $(form.new).on('input', function () { + if (this.value == "") { + form.repeat.removeAttribute("required"); + setFieldStatus(form.repeat, "", "", false); + } else { + form.repeat.setAttribute("required", "required"); + } + }); +} + +function eventListenerInitialize (form, inputs) { + // first is password input, second is repeat input + inputs.forEach(function (el) { + $(el).on('input', function (_e) { + if (inputs[0].value == inputs[1].value && inputs[0].value != "") { + setFieldStatus(inputs[1], "success"); + } else { + setFieldStatus(inputs[1], "error", "密碼不正確,請再試一次。"); + } + }); + }); + + $(form).on('submit', function (e) { + e.preventDefault(); + if (inputs[0].value != inputs[1].value) { + inputs[1].setCustomValidity("密碼不正確,請再試一次。"); + inputs[1].focus(); + return undefined; + } + + let fd = new URLSearchParams(new FormData(this)).toString(); + axios.request({ + method: "POST", + data: fd, + url: "account.php", + headers: { + 'Content-Type': "application/x-www-form-urlencoded" + } + }).then(function (res) { + location.href = res.headers["axios-location"]; + }).catch(function (error) { + if (error.response) { + location.href = error.response.headers["axios-location"]; + } else { + ts('.snackbar').snackbar({ + content: "發送失敗。" + }); + } + }); + }); +} + +function setFieldStatus(el, status, validity="", required=true) { + el.parentElement.className = (required) ? `${status} required field` : `${status} field`; + el.setCustomValidity(validity); +} + +function setFieldLabel(el, text) { + let sibling = el.nextElementSibling; + if (sibling.tagName == "SMALL" && text != "") { + let span = document.createElement('span'); + span.className = "message"; + span.innerText = text; + el.parentElement.insertBefore(span, sibling); + } else if (sibling.tagName == "SPAN" && text == "") { + $(sibling).remove(); + } else if (sibling.tagName != "SMALL") { + sibling.innerText = text; + } +} \ No newline at end of file diff --git a/include/js/cards.js b/include/js/cards.js new file mode 100644 index 0000000..7882dc1 --- /dev/null +++ b/include/js/cards.js @@ -0,0 +1,28 @@ +const cdnjs = "https://cdnjs.cloudflare.com/ajax/libs"; + +// Load Libraries +const libraries = [ + cdnjs + "/marked/0.5.1/marked.min.js", + cdnjs + "/prettify/r298/prettify.min.js", + cdnjs + "/underscore.js/1.9.1/underscore-min.js" +]; + +loadJS(libraries).then(function () { + editormd.$marked = marked; + editormd.loadFiles.js.push(...libraries.map(url => url.slice(0, -3))); // remove ".js" + document.querySelectorAll('.ts.card .description').forEach(function(el) { + let id = el.getAttribute('id'); + parseMarkdown(id, el.children[0].textContent, { + toc: false, + flowChart: false, + sequenceDiagram: false, + htmlDecode : "script,iframe,style|on*" + }).children('.markdown').hide(); + }); + postProcess(); + setTimeout(function () { + // show cards + $('.loading#cards').removeClass('loading'); + $('#content .active.loader').removeClass('active'); + }, 500); +}); \ No newline at end of file diff --git a/include/js/comment.js b/include/js/comment.js new file mode 100644 index 0000000..727c014 --- /dev/null +++ b/include/js/comment.js @@ -0,0 +1,328 @@ +const pid = $('#post').attr('data-id'); + +var post = { // post cache + fetchTime: undefined, + comments: [], + idList: [] // ids of comment +} + +// Fetch +$('.fetch.button').click(function(_e) { + fetchComments(); +}); + +var fetchTimer = setInterval(fetchComments, 5*60*1000); // polling comments per 5 minutes + +function fetchComments() { + $('.ts.inline.loader').addClass('active'); + if (!pid) { + console.error('An error occurred while fetching comments.'); + snackbar("無法載入留言。"); + return undefined; + } + + if (post.fetchTime) document.cookie = `cavern_commentLastFetch=${post.fetchTime}; Max-Age=10`; + + axios.request({ + method: "GET", + url: `ajax/comment.php?pid=${pid}`, + responseType: "json" + }).then(function (res) { + let data = res.data; + let t = new Date(data.fetch); + post.fetchTime = Math.ceil(data.fetch / 1000); // php timestamp + $('span.fetch.time').text(`Last fetch: ${t.getHours()}:${ t.getMinutes() < 10 ? '0' + t.getMinutes() : t.getMinutes() }`); + parseComments(data); + }).catch(function (error) { + if (error.response) { + let res = error.response; + console.error(`An error occurred while fetching comments of pid ${pid}, status ${res.status}`); + } else { + console.error(`An error occurred while fetching comments of pid ${pid}`); + } + snackbar("無法載入留言。"); + }); + setTimeout(() => { $('.ts.inline.loader').removeClass('active'); }, 250); +} + +function parseComments(data) { + const commentTemplate = `
{{ name }}
`; + + let add = data.idList.filter(function(item) { return post.idList.indexOf(item) < 0 }); // id list of new comments + let remove = post.idList.filter(function(item) { return data.idList.indexOf(item) < 0 }); // id list of removed comments + + for (postId of remove) { + $(`.ts.comments div[data-comment="${postId}"]`).remove(); + } + + for (c of data.comments) { + if (add.indexOf(c.id) == -1 && data.modified.indexOf(c.id) == -1) { + continue; + } + + if (add.indexOf(c.id) != -1) { + // render new comment + let node = commentTemplate.replace(/{{ id }}/gm, c.id).replace('{{ time }}', c.time).replace(/{{ username }}/gm, c.username).replace('{{ name }}', data.names[c.username]).replace('{{ hash }}', data.hash[c.username]); + $(node).appendTo('.ts.comments'); + if (c.actions.length != 0) { + let actions = document.createElement('div'); + actions.className = "actions"; + $(`div[data-comment="${c.id}"] .content`).append(actions); + for (act of c.actions) { + switch (act) { + case "reply": + actions.insertAdjacentHTML('beforeend',`回覆`); + break; + case "del": + actions.insertAdjacentHTML('beforeend',`刪除`); + break; + case "edit": + actions.insertAdjacentHTML('beforeend',`編輯`); + break; + default: + break; + } + } + } + } else if (data.modified.indexOf(c.id) != -1) { + // empty the old content + $(`#markdown-comment-${c.id}`).html(''); + } + + if (c.modified) { + let $metadata = $(`div[data-comment="${c.id}"] .metadata`); + if ($metadata.children('.modified').length) { + $metadata.children('.modified').attr('title', c.modified); + } else { + $metadata.append(`
已編輯
`); + } + } + + parseMarkdown(`markdown-comment-${c.id}`, _.unescape(c.markdown), { + toc: false + }); + } + + post.comments = data.comments; // cache data + post.idList = data.idList; // cache data + postProcess(); + + /* jump to the comment and emphasize it */ + if (location.hash && location.hash.startsWith("#comment-")) { + let commentID = location.hash; + if (!$(commentID).length) { + snackbar("留言已刪除或是不存在。") + } else { + $(window).scrollTop($(commentID).offset().top - $('.comment.header').outerHeight() - 10); + $(commentID).addClass('emphasized'); + } + } + + if (data.idList.length == 0) { + $('.ts.no-comment.segment').addClass('active'); + } else { + $('.ts.no-comment.segment').removeClass('active'); + } +} + +// Comment Editor & Preview +(function () { + let commentContainer = document.querySelector('#comment'); + let textarea = commentContainer.querySelector('textarea'); + $('.ts.tabbed.menu a.item[data-tab="preview"]').click(function() { + let comment = textarea.value; + if (comment.trim() != '') { + // reset the container + $('#preview').html(''); + parseMarkdown('preview', comment, { + toc: false + }); + postProcess(); + } else { + $('#preview').html('Nothing to preview!'); + } + }); + + $('#comment textarea').keydown(function (e) { + if (e.ctrlKey && (e.keyCode == 10 || e.keyCode == 13)) { // Ctrl-Enter pressed; Chrome: keyCode == 10 + document.querySelector('#comment div[data-tab="textarea"] button.submit.positive').click(); // send comment + } + }); + + // Edit + $('.ts.comments').on('click', '.edit', function(e) { + if (!textarea.disabled) { + let el = e.currentTarget; + let id = el.dataset.comment; + editorEditComment(textarea, id); + } else { + snackbar("你已被禁言。"); + } + }); + + // Reply + $('.ts.comments').on('click', '.reply', function(e) { + if (!textarea.disabled) { + let el = e.currentTarget; + textarea.value += ` @${el.dataset.username} `; + textarea.focus(); + } else { + snackbar("你已被禁言。"); + } + }); + + function editorInitialize(edtior) { + delete commentContainer.dataset.editId; + if ($('#comment .action.buttons button.cancel').length) { + $('#comment .action.buttons button.cancel').remove(); + } + if ($('#comment .menu .indicator').length) { + $('#comment .menu .indicator').remove(); + } + edtior.value = ""; // empty the textarea + } + + function editorEditComment(editor, commentId) { + if (post.idList.indexOf(commentId) == -1) { + snackbar('留言已刪除。'); + return undefined; + } + commentContainer.dataset.editId = commentId; + if (!$('#comment .action.buttons button.cancel').length) { + let cancelButton = document.createElement('button'); + cancelButton.classList.add('ts', 'cancel', 'button'); + cancelButton.innerText = "取消"; + commentContainer.querySelector('.action.buttons').appendChild(cancelButton); + cancelButton.addEventListener('click', function () { + editorInitialize(editor); + }); + } + if (!$('#comment .menu .indicator').length) { + let indicator = document.createElement('div'); + indicator.classList.add('right', 'indicator', 'item'); + indicator.innerText = `Editing: ${commentId}`; + commentContainer.querySelector('.menu').appendChild(indicator); + } else { + $('#comment .menu .indicator').text(`Editing: ${commentId}`); + } + editor.value = post.comments[post.idList.indexOf(commentId)].markdown; + editor.focus(); + } + + // Send Comment + let commentLock = false; + const commentRate = 10; // 1 comment per 10 seconds + + $('#comment div[data-tab="textarea"] button.submit.positive').click(function() { + var _this = this; + let content = textarea.value; + if (content.trim() == "") { + snackbar("留言不能為空!"); + return false; + } + if (commentLock) { + snackbar(`每 ${commentRate} 秒只能發一則留言。`); + return false; + } else if (!commentContainer.dataset.editId) { + // only new comment should be limited + commentLock = true; + } + + if (commentContainer.dataset.editId) { + // edit comment + var commentData = new URLSearchParams({ + "edit": commentContainer.dataset.editId, + "content": content + }).toString(); + } else { + // new comment + var commentData = new URLSearchParams({ + "pid": pid, + "content": content + }).toString(); + } + + axios.request({ + method: "POST", + url: "ajax/comment.php", + data: commentData, + responseType: "json" + }).then(function (res) { + editorInitialize(textarea); + console.log(`Comment sent succeessfully! Comment id is ${res.data["comment_id"]}`); + setTimeout(function() { commentLock = false }, commentRate * 1000); // sec -> microsecond + fetchComments(); + }).catch(function (error) { + commentLock = false; // unlock the textarea + if (error.response) { + let res = error.response; + let data = res.data; + console.error(`An error occurred while sending comments of pid ${pid}, status ${res.status}`); + switch (data.status) { + case "empty": + snackbar("留言不能為空!"); + break; + case "ratelimit": + let remainSeconds = res.headers['retry-after']; + snackbar(`每 ${commentRate} 秒只能發一則留言。請 ${remainSeconds} 秒後再試!`); + break; + case "muted": + snackbar("你已被禁言。"); + $('#comment .ts.fluid.input').addClass('disabled'); + $(textarea).attr("placeholder", "你被禁言了。").val(""); // empty the textarea + $(_this).addClass('disabled').text("你被禁言了"); + break; + case "author": + snackbar("你不能編輯別人的留言!"); + break; + case "nologin": + snackbar("請先登入。"); + break; + default: + snackbar("發送失敗。"); + break; + } + fetchComments(); + } else { + console.error(`An error occurred while sending comments of pid ${pid}`); + } + }); + }); +})(); + +// Delete +$('.ts.comments').on('click', '.delete', function(e) { + let el = e.currentTarget; + let id = el.dataset.comment; + swal({ + type: 'question', + title: '確定要刪除嗎?', + showCancelButton: true, + confirmButtonText: '確定', + cancelButtonText: '取消', + }).then((result) => { + if (result.value) { // confirm + axios.request({ + method: "GET", + url: "ajax/comment.php?del=" + id, + responseType: "json" + }).then(function (_res) { + fetchComments(); + }).catch(function (error) { + if (error.response) { + let res = error.response; + console.error(`An error occurred while deleting comment of id ${id}, status ${res.status}`); + } else { + console.error(`An error occurred while deleting comment of id ${id}`); + } + snackbar('刪除失敗。'); + }); + } + }); +}); + +function snackbar(message) { + ts('.snackbar').snackbar({ + content: message + }); +} \ No newline at end of file diff --git a/include/js/edit.js b/include/js/edit.js new file mode 100644 index 0000000..f760c04 --- /dev/null +++ b/include/js/edit.js @@ -0,0 +1,176 @@ +let edtior; +editormd.urls = { + atLinkBase : "user.php?username=" +}; + +// create editor instance +editor = editormd('markdownEditor', { + height: 450, + path: "https://pandao.github.io/editor.md/lib/", + markdown: document.edit.content.value, + htmlDecode : "script,iframe|on*", + placeholder: '', + styleActiveLine: false, + "font-size": '14px', + emoji: true, + taskList: true, + tex: true, + flowChart: true, + sequenceDiagram: true, + watch: false, + lineNumbers: false, + lineWrapping: false, + toolbarAutoFixed: false, + toolbarIcons : function() { + return [ + "search", "|", + "undo", "redo", "|", + "bold", "del", "italic", "|", + "list-ul", "list-ol", "emoji", "html-entities", "|", + "link", "image", "|", + "preview", "fullscreen", "||", + "help", "info", + ] + }, + toolbarIconsClass: { + preview: 'fa-eye' + }, + onload: function() { + var __this__ = this; + $('ul.editormd-menu').addClass('unstyled'); // remove style of TocasUI + $('ul.editormd-menu i[name="emoji"]').parent().click(function () { // remove style of TocasUI from emoji tab (hack) + setTimeout(()=>{ $('ul.editormd-tab-head').addClass('unstyled'); }, 300); + }); + this.resize(); + loadDraft(); + + document.edit.title.addEventListener("keydown", function () { + saveDraft(__this__); + }) + + this.cm.on("change", function(_cm, _changeObj) { + saveDraft(__this__); + }); + }, + onresize: function() { + if (this.state.preview) { + requestAnimationFrame(()=>{ + this.previewed(); + this.previewing(); + }); + } + }, + onpreviewing: function() { + this.save(); + // use tocas-ui style tables + $('table').each((_i,e) => { + $(e).addClass('ts celled table').css('display','table'); + }); + + // prevent user from destroying page style + var parser = new cssjs(); + let stylesheets = document.querySelectorAll('.markdown-body style'); + for (let style of stylesheets) { + let ruleSource = style.innerHTML; + let cssObject = parser.parseCSS(ruleSource); + for (let rule of cssObject) { + let valid = false; + let validPrefix = [".markdown-body ", ".editormd-preview-container ", ".markdown-body.editormd-preview-container ", ".editormd-preview-container.markdown-body "]; + validPrefix.forEach((e, _i) => { + valid = valid || rule.selector.startsWith(e); + }); + + if (!rule.selector.startsWith('@')) { // '@keyframe' & '@import' + if (!valid) { + rule.selector = ".editormd-preview-container " + rule.selector; + } + } + } + style.innerHTML = parser.getCSSForEditor(cssObject); + } + } +}); + +// save draft data +function saveDraft(editor) { + localStorage.setItem('cavern_draft_title', document.edit.title.value); + localStorage.setItem('cavern_draft_id', document.edit.pid.value); + localStorage.setItem('cavern_draft_content', editor.getMarkdown()); + localStorage.setItem('cavern_draft_time', new Date().getTime()); +} + +// Ask if user want to load draft +function loadDraft() { + if ($('#pid').val() == localStorage.getItem('cavern_draft_id')) { + swal({ + type: 'question', + title: '要載入上次備份嗎?', + showCancelButton: true, + confirmButtonText: '載入', + cancelButtonText: '取消', + }).then((result) => { + if (result.value) { // confirm + document.edit.title.value = localStorage.getItem('cavern_draft_title'); + editor.setValue(localStorage.getItem('cavern_draft_content')); + } + }); + } +} + +// Post an article +$(document.edit).on('submit', function(e) { + e.preventDefault(); + var _this = this; + let fd = new URLSearchParams(new FormData(this)).toString(); + axios.request({ + method: "POST", + data: fd, + url: "post.php", + headers: { + 'Content-Type': "application/x-www-form-urlencoded" + } + }).then(function (res) { + if (_this.pid.value == localStorage.getItem('cavern_draft_id')) { + ['id', 'title', 'content', 'time'].forEach((name) => { + localStorage.removeItem(`cavern_draft_${name}`); + }); + } + location.href = res.headers["axios-location"]; + }).catch(function (error) { + if (error.response) { + location.href = error.response.headers["axios-location"]; + } + }); +}); + +// Delete post confirm message +$('.action.column .delete').on('click', function(e) { + e.preventDefault(); + var el = this; + var href = el.getAttribute('href'); + swal({ + type: 'question', + title: '確定要刪除嗎?', + showCancelButton: true, + confirmButtonText: '確定', + cancelButtonText: '取消', + }).then((result) => { + if (result.value) { // confirm + axios.request({ + method: "GET", + url: href + }).then(function (res) { + location.href = res.headers["axios-location"]; + }).catch(function (error){ + if (error.response) { + location.href = error.response.headers["axios-location"]; + } + }); + } + }); +}); + +// session keep alive +setInterval(function() { + $.get('ajax/posts.php'); +}, 5*60*1000); diff --git a/include/js/lib/css.min.js b/include/js/lib/css.min.js new file mode 100644 index 0000000..612cb14 --- /dev/null +++ b/include/js/lib/css.min.js @@ -0,0 +1,3 @@ +/*! https://github.com/jotform/css.js 27-02-2018 */ + +!function(e){"use strict";var t=function(){this.cssImportStatements=[],this.cssKeyframeStatements=[],this.cssRegex=new RegExp("([\\s\\S]*?){([\\s\\S]*?)}","gi"),this.cssMediaQueryRegex="((@media [\\s\\S]*?){([\\s\\S]*?}\\s*?)})",this.cssKeyframeRegex="((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})",this.combinedCSSRegex="((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})",this.cssCommentsRegex="(\\/\\*[\\s\\S]*?\\*\\/)",this.cssImportStatementRegex=new RegExp("@import .*?;","gi")};t.prototype.stripComments=function(e){var t=new RegExp(this.cssCommentsRegex,"gi");return e.replace(t,"")},t.prototype.parseCSS=function(e){if(void 0===e)return[];for(var t=[];;){var s=this.cssImportStatementRegex.exec(e);if(null===s)break;this.cssImportStatements.push(s[0]),t.push({selector:"@imports",type:"imports",styles:s[0]})}e=e.replace(this.cssImportStatementRegex,"");for(var r,i=new RegExp(this.cssKeyframeRegex,"gi");null!==(r=i.exec(e));)t.push({selector:"@keyframes",type:"keyframes",styles:r[0]});e=e.replace(i,"");for(var n=new RegExp(this.combinedCSSRegex,"gi");null!==(r=n.exec(e));){var o="";o=void 0===r[2]?r[5].split("\r\n").join("\n").trim():r[2].split("\r\n").join("\n").trim();var l=new RegExp(this.cssCommentsRegex,"gi"),p=l.exec(o);if(null!==p&&(o=o.replace(l,"").trim()),-1!==(o=o.replace(/\n+/,"\n")).indexOf("@media")){var a={selector:o,type:"media",subStyles:this.parseCSS(r[3]+"\n}")};null!==p&&(a.comments=p[0]),t.push(a)}else{var c={selector:o,rules:this.parseRules(r[6])};"@font-face"===o&&(c.type="font-face"),null!==p&&(c.comments=p[0]),t.push(c)}}return t},t.prototype.parseRules=function(e){var t=[];e=(e=e.split("\r\n").join("\n")).split(";");for(var s=0;s0&&t.push({directive:"",value:r,defective:!0})}return t},t.prototype.findCorrespondingRule=function(e,t,s){void 0===s&&(s=!1);for(var r=!1,i=0;i-1;o--)if(e[o].selector===r){i=e[o];break}if(!1===i)e.push(t);else if("media"!==t.type)for(var l=0;l-1||n.selector.indexOf("keyframes")>-1||n.selector.indexOf("@import")>-1||n.selector.indexOf(".form-all")>-1||n.selector.indexOf("#stage")>-1))if("media"!==n.type){for(var o=n.selector.split(","),l=[],p=0;p Using fontAwesome icon web fonts; + // Support Editor.md logo icon emoji :editormd-logo: :editormd-logo-1x: > 1~8x; + tex : false, // TeX(LaTeX), based on KaTeX + flowChart : false, // flowChart.js only support IE9+ + sequenceDiagram : false, // sequenceDiagram.js only support IE9+ + previewCodeHighlight : true, + + toolbar : true, // show/hide toolbar + toolbarAutoFixed : true, // on window scroll auto fixed position + toolbarIcons : "full", + toolbarTitles : {}, + toolbarHandlers : { + ucwords : function() { + return editormd.toolbarHandlers.ucwords; + }, + lowercase : function() { + return editormd.toolbarHandlers.lowercase; + } + }, + toolbarCustomIcons : { // using html tag create toolbar icon, unused default tag. + lowercase : "a", + "ucwords" : "Aa" + }, + toolbarIconsClass : { + undo : "fa-undo", + redo : "fa-repeat", + bold : "fa-bold", + del : "fa-strikethrough", + italic : "fa-italic", + quote : "fa-quote-left", + uppercase : "fa-font", + h1 : editormd.classPrefix + "bold", + h2 : editormd.classPrefix + "bold", + h3 : editormd.classPrefix + "bold", + h4 : editormd.classPrefix + "bold", + h5 : editormd.classPrefix + "bold", + h6 : editormd.classPrefix + "bold", + "list-ul" : "fa-list-ul", + "list-ol" : "fa-list-ol", + hr : "fa-minus", + link : "fa-link", + "reference-link" : "fa-anchor", + image : "fa-picture-o", + code : "fa-code", + "preformatted-text" : "fa-file-code-o", + "code-block" : "fa-file-code-o", + table : "fa-table", + datetime : "fa-clock-o", + emoji : "fa-smile-o", + "html-entities" : "fa-copyright", + pagebreak : "fa-newspaper-o", + "goto-line" : "fa-terminal", // fa-crosshairs + watch : "fa-eye-slash", + unwatch : "fa-eye", + preview : "fa-desktop", + search : "fa-search", + fullscreen : "fa-arrows-alt", + clear : "fa-eraser", + help : "fa-question-circle", + info : "fa-info-circle" + }, + toolbarIconTexts : {}, + + lang : { + name : "zh-cn", + description : "开源在线Markdown编辑器
Open source online Markdown editor.", + tocTitle : "目录", + toolbar : { + undo : "撤销(Ctrl+Z)", + redo : "重做(Ctrl+Y)", + bold : "粗体", + del : "删除线", + italic : "斜体", + quote : "引用", + ucwords : "将每个单词首字母转成大写", + uppercase : "将所选转换成大写", + lowercase : "将所选转换成小写", + h1 : "标题1", + h2 : "标题2", + h3 : "标题3", + h4 : "标题4", + h5 : "标题5", + h6 : "标题6", + "list-ul" : "无序列表", + "list-ol" : "有序列表", + hr : "横线", + link : "链接", + "reference-link" : "引用链接", + image : "添加图片", + code : "行内代码", + "preformatted-text" : "预格式文本 / 代码块(缩进风格)", + "code-block" : "代码块(多语言风格)", + table : "添加表格", + datetime : "日期时间", + emoji : "Emoji表情", + "html-entities" : "HTML实体字符", + pagebreak : "插入分页符", + "goto-line" : "跳转到行", + watch : "关闭实时预览", + unwatch : "开启实时预览", + preview : "全窗口预览HTML(按 Shift + ESC还原)", + fullscreen : "全屏(按ESC还原)", + clear : "清空", + search : "搜索", + help : "使用帮助", + info : "关于" + editormd.title + }, + buttons : { + enter : "确定", + cancel : "取消", + close : "关闭" + }, + dialog : { + link : { + title : "添加链接", + url : "链接地址", + urlTitle : "链接标题", + urlEmpty : "错误:请填写链接地址。" + }, + referenceLink : { + title : "添加引用链接", + name : "引用名称", + url : "链接地址", + urlId : "链接ID", + urlTitle : "链接标题", + nameEmpty: "错误:引用链接的名称不能为空。", + idEmpty : "错误:请填写引用链接的ID。", + urlEmpty : "错误:请填写引用链接的URL地址。" + }, + image : { + title : "添加图片", + url : "图片地址", + link : "图片链接", + alt : "图片描述", + uploadButton : "本地上传", + imageURLEmpty : "错误:图片地址不能为空。", + uploadFileEmpty : "错误:上传的图片不能为空。", + formatNotAllowed : "错误:只允许上传图片文件,允许上传的图片文件格式有:" + }, + preformattedText : { + title : "添加预格式文本或代码块", + emptyAlert : "错误:请填写预格式文本或代码的内容。" + }, + codeBlock : { + title : "添加代码块", + selectLabel : "代码语言:", + selectDefaultText : "请选择代码语言", + otherLanguage : "其他语言", + unselectedLanguageAlert : "错误:请选择代码所属的语言类型。", + codeEmptyAlert : "错误:请填写代码内容。" + }, + htmlEntities : { + title : "HTML 实体字符" + }, + help : { + title : "使用帮助" + } + } + } + }; + + editormd.classNames = { + tex : editormd.classPrefix + "tex" + }; + + editormd.dialogZindex = 99999; + + editormd.$katex = null; + editormd.$marked = null; + editormd.$CodeMirror = null; + editormd.$prettyPrint = null; + + var timer, flowchartTimer; + + editormd.prototype = editormd.fn = { + state : { + watching : false, + loaded : false, + preview : false, + fullscreen : false + }, + + /** + * 构造函数/实例初始化 + * Constructor / instance initialization + * + * @param {String} id 编辑器的ID + * @param {Object} [options={}] 配置选项 Key/Value + * @returns {editormd} 返回editormd的实例对象 + */ + + init : function (id, options) { + + options = options || {}; + + if (typeof id === "object") + { + options = id; + } + + var _this = this; + var classPrefix = this.classPrefix = editormd.classPrefix; + var settings = this.settings = $.extend(true, editormd.defaults, options); + + id = (typeof id === "object") ? settings.id : id; + + var editor = this.editor = $("#" + id); + + this.id = id; + this.lang = settings.lang; + + var classNames = this.classNames = { + textarea : { + html : classPrefix + "html-textarea", + markdown : classPrefix + "markdown-textarea" + } + }; + + settings.pluginPath = (settings.pluginPath === "") ? settings.path + "../plugins/" : settings.pluginPath; + + this.state.watching = (settings.watch) ? true : false; + + if ( !editor.hasClass("editormd") ) { + editor.addClass("editormd"); + } + + editor.css({ + width : (typeof settings.width === "number") ? settings.width + "px" : settings.width, + height : (typeof settings.height === "number") ? settings.height + "px" : settings.height + }); + + if (settings.autoHeight) + { + editor.css("height", "auto"); + } + + var markdownTextarea = this.markdownTextarea = editor.children("textarea"); + + if (markdownTextarea.length < 1) + { + editor.append(""); + markdownTextarea = this.markdownTextarea = editor.children("textarea"); + } + + markdownTextarea.addClass(classNames.textarea.markdown).attr("placeholder", settings.placeholder); + + if (typeof markdownTextarea.attr("name") === "undefined" || markdownTextarea.attr("name") === "") + { + markdownTextarea.attr("name", (settings.name !== "") ? settings.name : id + "-markdown-doc"); + } + + var appendElements = [ + (!settings.readOnly) ? "" : "", + ( (settings.saveHTMLToTextarea) ? "" : "" ), + "
", + "
", + "
" + ].join("\n"); + + editor.append(appendElements).addClass(classPrefix + "vertical"); + + if (settings.theme !== "") + { + editor.addClass(classPrefix + "theme-" + settings.theme); + } + + this.mask = editor.children("." + classPrefix + "mask"); + this.containerMask = editor.children("." + classPrefix + "container-mask"); + + if (settings.markdown !== "") + { + markdownTextarea.val(settings.markdown); + } + + if (settings.appendMarkdown !== "") + { + markdownTextarea.val(markdownTextarea.val() + settings.appendMarkdown); + } + + this.htmlTextarea = editor.children("." + classNames.textarea.html); + this.preview = editor.children("." + classPrefix + "preview"); + this.previewContainer = this.preview.children("." + classPrefix + "preview-container"); + + if (settings.previewTheme !== "") + { + this.preview.addClass(classPrefix + "preview-theme-" + settings.previewTheme); + } + + if (typeof define === "function" && define.amd) + { + if (typeof katex !== "undefined") + { + editormd.$katex = katex; + } + + if (settings.searchReplace && !settings.readOnly) + { + editormd.loadCSS(settings.path + "codemirror/addon/dialog/dialog"); + editormd.loadCSS(settings.path + "codemirror/addon/search/matchesonscrollbar"); + } + } + + if ((typeof define === "function" && define.amd) || !settings.autoLoadModules) + { + if (typeof CodeMirror !== "undefined") { + editormd.$CodeMirror = CodeMirror; + } + + if (typeof marked !== "undefined") { + editormd.$marked = marked; + } + + this.setCodeMirror().setToolbar().loadedDisplay(); + } + else + { + this.loadQueues(); + } + + return this; + }, + + /** + * 所需组件加载队列 + * Required components loading queue + * + * @returns {editormd} 返回editormd的实例对象 + */ + + loadQueues : function() { + var _this = this; + var settings = this.settings; + var loadPath = settings.path; + + var cdnjs = "https://cdnjs.cloudflare.com/ajax/libs"; + + var loadFlowChartOrSequenceDiagram = function() { + + if (editormd.isIE8) + { + _this.loadedDisplay(); + + return ; + } + + if (settings.flowChart || settings.sequenceDiagram) + { + editormd.loadScript(cdnjs + "/raphael/2.2.7/raphael.min", function() { + + editormd.loadScript(cdnjs + "/underscore.js/1.9.1/underscore-min", function() { + + if (!settings.flowChart && settings.sequenceDiagram) + { + editormd.loadScript(cdnjs + "/js-sequence-diagrams/1.0.6/sequence-diagram-min", function() { + _this.loadedDisplay(); + }); + } + else if (settings.flowChart && !settings.sequenceDiagram) + { + editormd.loadScript(cdnjs + "/flowchart/1.11.3/flowchart.min", function() { + editormd.loadScript(loadPath + "jquery.flowchart.min", function() { + _this.loadedDisplay(); + }); + }); + } + else if (settings.flowChart && settings.sequenceDiagram) + { + editormd.loadScript(cdnjs + "/flowchart/1.11.3/flowchart.min", function() { + editormd.loadScript(loadPath + "jquery.flowchart.min", function() { + editormd.loadScript(cdnjs + "/js-sequence-diagrams/1.0.6/sequence-diagram-min", function() { + _this.loadedDisplay(); + }); + }); + }); + } + }); + + }); + } + else + { + _this.loadedDisplay(); + } + }; + + editormd.loadCSS(loadPath + "codemirror/codemirror.min"); + + if (settings.searchReplace && !settings.readOnly) + { + editormd.loadCSS(loadPath + "codemirror/addon/dialog/dialog"); + editormd.loadCSS(loadPath + "codemirror/addon/search/matchesonscrollbar"); + } + + if (settings.codeFold) + { + editormd.loadCSS(loadPath + "codemirror/addon/fold/foldgutter"); + } + + editormd.loadScript(loadPath + "codemirror/codemirror.min", function() { + editormd.$CodeMirror = CodeMirror; + + editormd.loadScript(loadPath + "codemirror/modes.min", function() { + + editormd.loadScript(loadPath + "codemirror/addons.min", function() { + + _this.setCodeMirror(); + + if (settings.mode !== "gfm" && settings.mode !== "markdown") + { + _this.loadedDisplay(); + + return false; + } + + _this.setToolbar(); + + editormd.loadScript(cdnjs + "/marked/0.5.1/marked.min", function() { + + editormd.$marked = marked; + + if (settings.previewCodeHighlight) + { + editormd.loadScript(cdnjs + "/prettify/r298/prettify.min", function() { + loadFlowChartOrSequenceDiagram(); + }); + } + else + { + loadFlowChartOrSequenceDiagram(); + } + }); + + }); + + }); + + }); + + return this; + }, + + /** + * 设置 Editor.md 的整体主题,主要是工具栏 + * Setting Editor.md theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setTheme : function(theme) { + var editor = this.editor; + var oldTheme = this.settings.theme; + var themePrefix = this.classPrefix + "theme-"; + + editor.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme); + + this.settings.theme = theme; + + return this; + }, + + /** + * 设置 CodeMirror(编辑区)的主题 + * Setting CodeMirror (Editor area) theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setEditorTheme : function(theme) { + var settings = this.settings; + settings.editorTheme = theme; + + if (theme !== "default") + { + editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme); + } + + this.cm.setOption("theme", theme); + + return this; + }, + + /** + * setEditorTheme() 的别名 + * setEditorTheme() alias + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirrorTheme : function (theme) { + this.setEditorTheme(theme); + + return this; + }, + + /** + * 设置 Editor.md 的主题 + * Setting Editor.md theme + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setPreviewTheme : function(theme) { + var preview = this.preview; + var oldTheme = this.settings.previewTheme; + var themePrefix = this.classPrefix + "preview-theme-"; + + preview.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme); + + this.settings.previewTheme = theme; + + return this; + }, + + /** + * 配置和初始化CodeMirror组件 + * CodeMirror initialization + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirror : function() { + var settings = this.settings; + var editor = this.editor; + + if (settings.editorTheme !== "default") + { + editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme); + } + + var codeMirrorConfig = { + mode : settings.mode, + theme : settings.editorTheme, + tabSize : settings.tabSize, + dragDrop : false, + autofocus : settings.autoFocus, + autoCloseTags : settings.autoCloseTags, + readOnly : (settings.readOnly) ? "nocursor" : false, + indentUnit : settings.indentUnit, + lineNumbers : settings.lineNumbers, + lineWrapping : settings.lineWrapping, + extraKeys : { + "Ctrl-Q": function(cm) { + cm.foldCode(cm.getCursor()); + } + }, + foldGutter : settings.codeFold, + gutters : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + matchBrackets : settings.matchBrackets, + indentWithTabs : settings.indentWithTabs, + styleActiveLine : settings.styleActiveLine, + styleSelectedText : settings.styleSelectedText, + autoCloseBrackets : settings.autoCloseBrackets, + showTrailingSpace : settings.showTrailingSpace, + inputStyle : 'textarea', + highlightSelectionMatches : ( (!settings.matchWordHighlight) ? false : { showToken: (settings.matchWordHighlight === "onselected") ? false : /\w/ } ) + }; + + this.codeEditor = this.cm = editormd.$CodeMirror.fromTextArea(this.markdownTextarea[0], codeMirrorConfig); + this.codeMirror = this.cmElement = editor.children(".CodeMirror"); + + if (settings.value !== "") + { + this.cm.setValue(settings.value); + } + + this.codeMirror.css({ + fontSize : settings.fontSize, + width : (!settings.watch) ? "100%" : "50%" + }); + + if (settings.autoHeight) + { + this.codeMirror.css("height", "auto"); + this.cm.setOption("viewportMargin", Infinity); + } + + if (!settings.lineNumbers) + { + this.codeMirror.find(".CodeMirror-gutters").css("border-right", "none"); + } + + return this; + }, + + /** + * 获取CodeMirror的配置选项 + * Get CodeMirror setting options + * + * @returns {Mixed} return CodeMirror setting option value + */ + + getCodeMirrorOption : function(key) { + return this.cm.getOption(key); + }, + + /** + * 配置和重配置CodeMirror的选项 + * CodeMirror setting options / resettings + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setCodeMirrorOption : function(key, value) { + + this.cm.setOption(key, value); + + return this; + }, + + /** + * 添加 CodeMirror 键盘快捷键 + * Add CodeMirror keyboard shortcuts key map + * + * @returns {editormd} 返回editormd的实例对象 + */ + + addKeyMap : function(map, bottom) { + this.cm.addKeyMap(map, bottom); + + return this; + }, + + /** + * 移除 CodeMirror 键盘快捷键 + * Remove CodeMirror keyboard shortcuts key map + * + * @returns {editormd} 返回editormd的实例对象 + */ + + removeKeyMap : function(map) { + this.cm.removeKeyMap(map); + + return this; + }, + + /** + * 跳转到指定的行 + * Goto CodeMirror line + * + * @param {String|Intiger} line line number or "first"|"last" + * @returns {editormd} 返回editormd的实例对象 + */ + + gotoLine : function (line) { + + var settings = this.settings; + + if (!settings.gotoLine) + { + return this; + } + + var cm = this.cm; + var editor = this.editor; + var count = cm.lineCount(); + var preview = this.preview; + + if (typeof line === "string") + { + if(line === "last") + { + line = count; + } + + if (line === "first") + { + line = 1; + } + } + + if (typeof line !== "number") + { + alert("Error: The line number must be an integer."); + return this; + } + + line = parseInt(line) - 1; + + if (line > count) + { + alert("Error: The line number range 1-" + count); + + return this; + } + + cm.setCursor( {line : line, ch : 0} ); + + var scrollInfo = cm.getScrollInfo(); + var clientHeight = scrollInfo.clientHeight; + var coords = cm.charCoords({line : line, ch : 0}, "local"); + + cm.scrollTo(null, (coords.top + coords.bottom - clientHeight) / 2); + + if (settings.watch) + { + var cmScroll = this.codeMirror.find(".CodeMirror-scroll")[0]; + var height = $(cmScroll).height(); + var scrollTop = cmScroll.scrollTop; + var percent = (scrollTop / cmScroll.scrollHeight); + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= cmScroll.scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop(preview[0].scrollHeight * percent); + } + } + + cm.focus(); + + return this; + }, + + /** + * 扩展当前实例对象,可同时设置多个或者只设置一个 + * Extend editormd instance object, can mutil setting. + * + * @returns {editormd} this(editormd instance object.) + */ + + extend : function() { + if (typeof arguments[1] !== "undefined") + { + if (typeof arguments[1] === "function") + { + arguments[1] = $.proxy(arguments[1], this); + } + + this[arguments[0]] = arguments[1]; + } + + if (typeof arguments[0] === "object" && typeof arguments[0].length === "undefined") + { + $.extend(true, this, arguments[0]); + } + + return this; + }, + + /** + * 设置或扩展当前实例对象,单个设置 + * Extend editormd instance object, one by one + * + * @param {String|Object} key option key + * @param {String|Object} value option value + * @returns {editormd} this(editormd instance object.) + */ + + set : function (key, value) { + + if (typeof value !== "undefined" && typeof value === "function") + { + value = $.proxy(value, this); + } + + this[key] = value; + + return this; + }, + + /** + * 重新配置 + * Resetting editor options + * + * @param {String|Object} key option key + * @param {String|Object} value option value + * @returns {editormd} this(editormd instance object.) + */ + + config : function(key, value) { + var settings = this.settings; + + if (typeof key === "object") + { + settings = $.extend(true, settings, key); + } + + if (typeof key === "string") + { + settings[key] = value; + } + + this.settings = settings; + this.recreate(); + + return this; + }, + + /** + * 注册事件处理方法 + * Bind editor event handle + * + * @param {String} eventType event type + * @param {Function} callback 回调函数 + * @returns {editormd} this(editormd instance object.) + */ + + on : function(eventType, callback) { + var settings = this.settings; + + if (typeof settings["on" + eventType] !== "undefined") + { + settings["on" + eventType] = $.proxy(callback, this); + } + + return this; + }, + + /** + * 解除事件处理方法 + * Unbind editor event handle + * + * @param {String} eventType event type + * @returns {editormd} this(editormd instance object.) + */ + + off : function(eventType) { + var settings = this.settings; + + if (typeof settings["on" + eventType] !== "undefined") + { + settings["on" + eventType] = function(){}; + } + + return this; + }, + + /** + * 显示工具栏 + * Display toolbar + * + * @param {Function} [callback=function(){}] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + showToolbar : function(callback) { + var settings = this.settings; + + if(settings.readOnly) { + return this; + } + + if (settings.toolbar && (this.toolbar.length < 1 || this.toolbar.find("." + this.classPrefix + "menu").html() === "") ) + { + this.setToolbar(); + } + + settings.toolbar = true; + + this.toolbar.show(); + this.resize(); + + $.proxy(callback || function(){}, this)(); + + return this; + }, + + /** + * 隐藏工具栏 + * Hide toolbar + * + * @param {Function} [callback=function(){}] 回调函数 + * @returns {editormd} this(editormd instance object.) + */ + + hideToolbar : function(callback) { + var settings = this.settings; + + settings.toolbar = false; + this.toolbar.hide(); + this.resize(); + + $.proxy(callback || function(){}, this)(); + + return this; + }, + + /** + * 页面滚动时工具栏的固定定位 + * Set toolbar in window scroll auto fixed position + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbarAutoFixed : function(fixed) { + + var state = this.state; + var editor = this.editor; + var toolbar = this.toolbar; + var settings = this.settings; + + if (typeof fixed !== "undefined") + { + settings.toolbarAutoFixed = fixed; + } + + var autoFixedHandle = function(){ + var $window = $(window); + var top = $window.scrollTop(); + + if (!settings.toolbarAutoFixed) + { + return false; + } + + if (top - editor.offset().top > 10 && top < editor.height()) + { + toolbar.css({ + position : "fixed", + width : editor.width() + "px", + left : ($window.width() - editor.width()) / 2 + "px" + }); + } + else + { + toolbar.css({ + position : "absolute", + width : "100%", + left : 0 + }); + } + }; + + if (!state.fullscreen && !state.preview && settings.toolbar && settings.toolbarAutoFixed) + { + $(window).bind("scroll", autoFixedHandle); + } + + return this; + }, + + /** + * 配置和初始化工具栏 + * Set toolbar and Initialization + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbar : function() { + var settings = this.settings; + + if(settings.readOnly) { + return this; + } + + var editor = this.editor; + var preview = this.preview; + var classPrefix = this.classPrefix; + + var toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar"); + + if (settings.toolbar && toolbar.length < 1) + { + var toolbarHTML = "
    "; + + editor.append(toolbarHTML); + toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar"); + } + + if (!settings.toolbar) + { + toolbar.hide(); + + return this; + } + + toolbar.show(); + + var icons = (typeof settings.toolbarIcons === "function") ? settings.toolbarIcons() + : ((typeof settings.toolbarIcons === "string") ? editormd.toolbarModes[settings.toolbarIcons] : settings.toolbarIcons); + + var toolbarMenu = toolbar.find("." + this.classPrefix + "menu"), menu = ""; + var pullRight = false; + + for (var i = 0, len = icons.length; i < len; i++) + { + var name = icons[i]; + + if (name === "||") + { + pullRight = true; + } + else if (name === "|") + { + menu += "
  • |
  • "; + } + else + { + var isHeader = (/h(\d)/.test(name)); + var index = name; + + if (name === "watch" && !settings.watch) { + index = "unwatch"; + } + + var title = settings.lang.toolbar[index]; + var iconTexts = settings.toolbarIconTexts[index]; + var iconClass = settings.toolbarIconsClass[index]; + + title = (typeof title === "undefined") ? "" : title; + iconTexts = (typeof iconTexts === "undefined") ? "" : iconTexts; + iconClass = (typeof iconClass === "undefined") ? "" : iconClass; + + var menuItem = pullRight ? "
  • " : "
  • "; + + if (typeof settings.toolbarCustomIcons[name] !== "undefined" && typeof settings.toolbarCustomIcons[name] !== "function") + { + menuItem += settings.toolbarCustomIcons[name]; + } + else + { + menuItem += ""; + menuItem += ""+((isHeader) ? name.toUpperCase() : ( (iconClass === "") ? iconTexts : "") ) + ""; + menuItem += ""; + } + + menuItem += "
  • "; + + menu = pullRight ? menuItem + menu : menu + menuItem; + } + } + + toolbarMenu.html(menu); + + toolbarMenu.find("[title=\"Lowercase\"]").attr("title", settings.lang.toolbar.lowercase); + toolbarMenu.find("[title=\"ucwords\"]").attr("title", settings.lang.toolbar.ucwords); + + this.setToolbarHandler(); + this.setToolbarAutoFixed(); + + return this; + }, + + /** + * 工具栏图标事件处理对象序列 + * Get toolbar icons event handlers + * + * @param {Object} cm CodeMirror的实例对象 + * @param {String} name 要获取的事件处理器名称 + * @returns {Object} 返回处理对象序列 + */ + + dialogLockScreen : function() { + $.proxy(editormd.dialogLockScreen, this)(); + + return this; + }, + + dialogShowMask : function(dialog) { + $.proxy(editormd.dialogShowMask, this)(dialog); + + return this; + }, + + getToolbarHandles : function(name) { + var toolbarHandlers = this.toolbarHandlers = editormd.toolbarHandlers; + + return (name && typeof toolbarIconHandlers[name] !== "undefined") ? toolbarHandlers[name] : toolbarHandlers; + }, + + /** + * 工具栏图标事件处理器 + * Bind toolbar icons event handle + * + * @returns {editormd} 返回editormd的实例对象 + */ + + setToolbarHandler : function() { + var _this = this; + var settings = this.settings; + + if (!settings.toolbar || settings.readOnly) { + return this; + } + + var toolbar = this.toolbar; + var cm = this.cm; + var classPrefix = this.classPrefix; + var toolbarIcons = this.toolbarIcons = toolbar.find("." + classPrefix + "menu > li > a"); + var toolbarIconHandlers = this.getToolbarHandles(); + + toolbarIcons.on(editormd.mouseOrTouch("click", "touchend"), function(event) { + + var icon = $(this).children(".fa"); + var name = icon.attr("name"); + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (name === "") { + return ; + } + + _this.activeIcon = icon; + + if (typeof toolbarIconHandlers[name] !== "undefined") + { + $.proxy(toolbarIconHandlers[name], _this)(cm); + } + else + { + if (typeof settings.toolbarHandlers[name] !== "undefined") + { + $.proxy(settings.toolbarHandlers[name], _this)(cm, icon, cursor, selection); + } + } + + if (name !== "link" && name !== "reference-link" && name !== "image" && name !== "code-block" && + name !== "preformatted-text" && name !== "watch" && name !== "preview" && name !== "search" && name !== "fullscreen" && name !== "info") + { + cm.focus(); + } + + return false; + + }); + + return this; + }, + + /** + * 动态创建对话框 + * Creating custom dialogs + * + * @param {Object} options 配置项键值对 Key/Value + * @returns {dialog} 返回创建的dialog的jQuery实例对象 + */ + + createDialog : function(options) { + return $.proxy(editormd.createDialog, this)(options); + }, + + /** + * 创建关于Editor.md的对话框 + * Create about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + createInfoDialog : function() { + var _this = this; + var editor = this.editor; + var classPrefix = this.classPrefix; + + var infoDialogHTML = [ + "
    ", + "
    ", + "

    " + editormd.title + "v" + editormd.version + "

    ", + "

    " + this.lang.description + "

    ", + "

    " + editormd.homePage + "

    ", + "

    Copyright © 2015 Pandao, The MIT License.

    ", + "
    ", + "", + "
    " + ].join("\n"); + + editor.append(infoDialogHTML); + + var infoDialog = this.infoDialog = editor.children("." + classPrefix + "dialog-info"); + + infoDialog.find("." + classPrefix + "dialog-close").bind(editormd.mouseOrTouch("click", "touchend"), function() { + _this.hideInfoDialog(); + }); + + infoDialog.css("border", (editormd.isIE8) ? "1px solid #ddd" : "").css("z-index", editormd.dialogZindex).show(); + + this.infoDialogPosition(); + + return this; + }, + + /** + * 关于Editor.md对话居中定位 + * Editor.md dialog position handle + * + * @returns {editormd} 返回editormd的实例对象 + */ + + infoDialogPosition : function() { + var infoDialog = this.infoDialog; + + var _infoDialogPosition = function() { + infoDialog.css({ + top : ($(window).height() - infoDialog.height()) / 2 + "px", + left : ($(window).width() - infoDialog.width()) / 2 + "px" + }); + }; + + _infoDialogPosition(); + + $(window).resize(_infoDialogPosition); + + return this; + }, + + /** + * 显示关于Editor.md + * Display about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + showInfoDialog : function() { + + $("html,body").css("overflow-x", "hidden"); + + var _this = this; + var editor = this.editor; + var settings = this.settings; + var infoDialog = this.infoDialog = editor.children("." + this.classPrefix + "dialog-info"); + + if (infoDialog.length < 1) + { + this.createInfoDialog(); + } + + this.lockScreen(true); + + this.mask.css({ + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }).show(); + + infoDialog.css("z-index", editormd.dialogZindex).show(); + + this.infoDialogPosition(); + + return this; + }, + + /** + * 隐藏关于Editor.md + * Hide about Editor.md dialog + * + * @returns {editormd} 返回editormd的实例对象 + */ + + hideInfoDialog : function() { + $("html,body").css("overflow-x", ""); + this.infoDialog.hide(); + this.mask.hide(); + this.lockScreen(false); + + return this; + }, + + /** + * 锁屏 + * lock screen + * + * @param {Boolean} lock Boolean 布尔值,是否锁屏 + * @returns {editormd} 返回editormd的实例对象 + */ + + lockScreen : function(lock) { + editormd.lockScreen(lock); + this.resize(); + + return this; + }, + + /** + * 编辑器界面重建,用于动态语言包或模块加载等 + * Recreate editor + * + * @returns {editormd} 返回editormd的实例对象 + */ + + recreate : function() { + var _this = this; + var editor = this.editor; + var settings = this.settings; + + this.codeMirror.remove(); + + this.setCodeMirror(); + + if (!settings.readOnly) + { + if (editor.find(".editormd-dialog").length > 0) { + editor.find(".editormd-dialog").remove(); + } + + if (settings.toolbar) + { + this.getToolbarHandles(); + this.setToolbar(); + } + } + + this.loadedDisplay(true); + + return this; + }, + + /** + * 高亮预览HTML的pre代码部分 + * highlight of preview codes + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewCodeHighlight : function() { + var settings = this.settings; + var previewContainer = this.previewContainer; + + if (settings.previewCodeHighlight) + { + previewContainer.find("pre").addClass("prettyprint linenums"); + + if (typeof prettyPrint !== "undefined") + { + prettyPrint(); + } + } + + return this; + }, + + /** + * 解析TeX(KaTeX)科学公式 + * TeX(KaTeX) Renderer + * + * @returns {editormd} 返回editormd的实例对象 + */ + + katexRender : function() { + + if (timer === null && !this.state.preview) + { + return this; + } + + this.previewContainer.find("." + editormd.classNames.tex).each(function(){ + var tex = $(this); + editormd.$katex.render(tex.text(), tex[0]); + + tex.find(".katex").css("font-size", "1.35em"); + }); + + return this; + }, + + /** + * 解析和渲染流程图及时序图 + * FlowChart and SequenceDiagram Renderer + * + * @returns {editormd} 返回editormd的实例对象 + */ + + flowChartAndSequenceDiagramRender : function() { + var $this = this; + var settings = this.settings; + var previewContainer = this.previewContainer; + + if (editormd.isIE8) { + return this; + } + + if (settings.flowChart) { + if (flowchartTimer === null) { + return this; + } + + previewContainer.find(".flowchart").flowChart(); + } + + if (settings.sequenceDiagram) { + previewContainer.find(".sequence-diagram").sequenceDiagram({theme: "simple"}); + } + + var preview = $this.preview; + var codeMirror = $this.codeMirror; + var codeView = codeMirror.find(".CodeMirror-scroll"); + + var height = codeView.height(); + var scrollTop = codeView.scrollTop(); + var percent = (scrollTop / codeView[0].scrollHeight); + var tocHeight = 0; + + preview.find(".markdown-toc-list").each(function(){ + tocHeight += $(this).height(); + }); + + var tocMenuHeight = preview.find(".editormd-toc-menu").height(); + tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight; + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= codeView[0].scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop((preview[0].scrollHeight + tocHeight + tocMenuHeight) * percent); + } + + return this; + }, + + /** + * 注册键盘快捷键处理 + * Register CodeMirror keyMaps (keyboard shortcuts). + * + * @param {Object} keyMap KeyMap key/value {"(Ctrl/Shift/Alt)-Key" : function(){}} + * @returns {editormd} return this + */ + + registerKeyMaps : function(keyMap) { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + var toolbarHandlers = editormd.toolbarHandlers; + var disabledKeyMaps = settings.disabledKeyMaps; + + keyMap = keyMap || null; + + if (keyMap) + { + for (var i in keyMap) + { + if ($.inArray(i, disabledKeyMaps) < 0) + { + var map = {}; + map[i] = keyMap[i]; + + cm.addKeyMap(keyMap); + } + } + } + else + { + for (var k in editormd.keyMaps) + { + var _keyMap = editormd.keyMaps[k]; + var handle = (typeof _keyMap === "string") ? $.proxy(toolbarHandlers[_keyMap], _this) : $.proxy(_keyMap, _this); + + if ($.inArray(k, ["F9", "F10", "F11"]) < 0 && $.inArray(k, disabledKeyMaps) < 0) + { + var _map = {}; + _map[k] = handle; + + cm.addKeyMap(_map); + } + } + + $(window).keydown(function(event) { + + var keymaps = { + "120" : "F9", + "121" : "F10", + "122" : "F11" + }; + + if ( $.inArray(keymaps[event.keyCode], disabledKeyMaps) < 0 ) + { + switch (event.keyCode) + { + case 120: + $.proxy(toolbarHandlers["watch"], _this)(); + return false; + break; + + case 121: + $.proxy(toolbarHandlers["preview"], _this)(); + return false; + break; + + case 122: + $.proxy(toolbarHandlers["fullscreen"], _this)(); + return false; + break; + + default: + break; + } + } + }); + } + + return this; + }, + + /** + * 绑定同步滚动 + * + * @returns {editormd} return this + */ + + bindScrollEvent : function() { + + var _this = this; + var preview = this.preview; + var settings = this.settings; + var codeMirror = this.codeMirror; + var mouseOrTouch = editormd.mouseOrTouch; + + if (!settings.syncScrolling) { + return this; + } + + var cmBindScroll = function() { + codeMirror.find(".CodeMirror-scroll").bind(mouseOrTouch("scroll", "touchmove"), function(event) { + var height = $(this).height(); + var scrollTop = $(this).scrollTop(); + var percent = (scrollTop / $(this)[0].scrollHeight); + + var tocHeight = 0; + + preview.find(".markdown-toc-list").each(function(){ + tocHeight += $(this).height(); + }); + + var tocMenuHeight = preview.find(".editormd-toc-menu").height(); + tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight; + + if (scrollTop === 0) + { + preview.scrollTop(0); + } + else if (scrollTop + height >= $(this)[0].scrollHeight - 16) + { + preview.scrollTop(preview[0].scrollHeight); + } + else + { + preview.scrollTop((preview[0].scrollHeight + tocHeight + tocMenuHeight) * percent); + } + + $.proxy(settings.onscroll, _this)(event); + }); + }; + + var cmUnbindScroll = function() { + codeMirror.find(".CodeMirror-scroll").unbind(mouseOrTouch("scroll", "touchmove")); + }; + + var previewBindScroll = function() { + + preview.bind(mouseOrTouch("scroll", "touchmove"), function(event) { + var height = $(this).height(); + var scrollTop = $(this).scrollTop(); + var percent = (scrollTop / $(this)[0].scrollHeight); + var codeView = codeMirror.find(".CodeMirror-scroll"); + + if(scrollTop === 0) + { + codeView.scrollTop(0); + } + else if (scrollTop + height >= $(this)[0].scrollHeight) + { + codeView.scrollTop(codeView[0].scrollHeight); + } + else + { + codeView.scrollTop(codeView[0].scrollHeight * percent); + } + + $.proxy(settings.onpreviewscroll, _this)(event); + }); + + }; + + var previewUnbindScroll = function() { + preview.unbind(mouseOrTouch("scroll", "touchmove")); + }; + + codeMirror.bind({ + mouseover : cmBindScroll, + mouseout : cmUnbindScroll, + touchstart : cmBindScroll, + touchend : cmUnbindScroll + }); + + if (settings.syncScrolling === "single") { + return this; + } + + preview.bind({ + mouseover : previewBindScroll, + mouseout : previewUnbindScroll, + touchstart : previewBindScroll, + touchend : previewUnbindScroll + }); + + return this; + }, + + bindChangeEvent : function() { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + + if (!settings.syncScrolling) { + return this; + } + + cm.on("change", function(_cm, changeObj) { + + if (settings.watch) + { + _this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px"); + } + + timer = setTimeout(function() { + clearTimeout(timer); + _this.save(); + timer = null; + }, settings.delay); + }); + + return this; + }, + + /** + * 加载队列完成之后的显示处理 + * Display handle of the module queues loaded after. + * + * @param {Boolean} recreate 是否为重建编辑器 + * @returns {editormd} 返回editormd的实例对象 + */ + + loadedDisplay : function(recreate) { + + recreate = recreate || false; + + var _this = this; + var editor = this.editor; + var preview = this.preview; + var settings = this.settings; + + this.containerMask.hide(); + + this.save(); + + if (settings.watch) { + preview.show(); + } + + editor.data("oldWidth", editor.width()).data("oldHeight", editor.height()); // 为了兼容Zepto + + this.resize(); + this.registerKeyMaps(); + + $(window).resize(function(){ + _this.resize(); + }); + + this.bindScrollEvent().bindChangeEvent(); + + if (!recreate) + { + $.proxy(settings.onload, this)(); + } + + this.state.loaded = true; + + return this; + }, + + /** + * 设置编辑器的宽度 + * Set editor width + * + * @param {Number|String} width 编辑器宽度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + width : function(width) { + + this.editor.css("width", (typeof width === "number") ? width + "px" : width); + this.resize(); + + return this; + }, + + /** + * 设置编辑器的高度 + * Set editor height + * + * @param {Number|String} height 编辑器高度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + height : function(height) { + + this.editor.css("height", (typeof height === "number") ? height + "px" : height); + this.resize(); + + return this; + }, + + /** + * 调整编辑器的尺寸和布局 + * Resize editor layout + * + * @param {Number|String} [width=null] 编辑器宽度值 + * @param {Number|String} [height=null] 编辑器高度值 + * @returns {editormd} 返回editormd的实例对象 + */ + + resize : function(width, height) { + + width = width || null; + height = height || null; + + var state = this.state; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var codeMirror = this.codeMirror; + + if (width) + { + editor.css("width", (typeof width === "number") ? width + "px" : width); + } + + if (settings.autoHeight && !state.fullscreen && !state.preview) + { + editor.css("height", "auto"); + codeMirror.css("height", "auto"); + } + else + { + if (height) + { + editor.css("height", (typeof height === "number") ? height + "px" : height); + } + + if (state.fullscreen) + { + editor.height($(window).height()); + } + + if (settings.toolbar && !settings.readOnly) + { + codeMirror.css("margin-top", toolbar.height() + 1).height(editor.height() - toolbar.height()); + } + else + { + codeMirror.css("margin-top", 0).height(editor.height()); + } + } + + if(settings.watch) + { + codeMirror.width(editor.width() / 2); + preview.width((!state.preview) ? editor.width() / 2 : editor.width()); + + this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px"); + + if (settings.toolbar && !settings.readOnly) + { + preview.css("top", toolbar.height() + 1); + } + else + { + preview.css("top", 0); + } + + if (settings.autoHeight && !state.fullscreen && !state.preview) + { + preview.height(""); + } + else + { + var previewHeight = (settings.toolbar && !settings.readOnly) ? editor.height() - toolbar.height() : editor.height(); + + preview.height(previewHeight); + } + } + else + { + codeMirror.width(editor.width()); + preview.hide(); + } + + if (state.loaded) + { + $.proxy(settings.onresize, this)(); + } + + return this; + }, + + /** + * 解析和保存Markdown代码 + * Parse & Saving Markdown source code + * + * @returns {editormd} 返回editormd的实例对象 + */ + + save : function() { + + if (timer === null && !this.state.preview) + { + return this; + } + + var _this = this; + var state = this.state; + var settings = this.settings; + var cm = this.cm; + var cmValue = cm.getValue(); + var previewContainer = this.previewContainer; + + if (settings.mode !== "gfm" && settings.mode !== "markdown") + { + this.markdownTextarea.val(cmValue); + + return this; + } + + var marked = editormd.$marked; + var markdownToC = this.markdownToC = []; + var rendererOptions = this.markedRendererOptions = { + toc : settings.toc, + tocm : settings.tocm, + tocStartLevel : settings.tocStartLevel, + pageBreak : settings.pageBreak, + taskList : settings.taskList, + emoji : settings.emoji, + tex : settings.tex, + atLink : settings.atLink, // for @link + emailLink : settings.emailLink, // for mail address auto link + flowChart : settings.flowChart, + sequenceDiagram : settings.sequenceDiagram, + previewCodeHighlight : settings.previewCodeHighlight, + }; + + var markedOptions = this.markedOptions = { + renderer : editormd.markedRenderer(markdownToC, rendererOptions), + gfm : true, + tables : true, + breaks : true, + pedantic : false, + sanitize : (settings.htmlDecode) ? false : true, // 关闭忽略HTML标签,即开启识别HTML标签,默认为false + smartLists : true, + smartypants : true + }; + + marked.setOptions(markedOptions); + + var newMarkdownDoc = editormd.$marked(cmValue, markedOptions); + + //console.info("cmValue", cmValue, newMarkdownDoc); + + newMarkdownDoc = editormd.filterHTMLTags(newMarkdownDoc, settings.htmlDecode); + + //console.error("cmValue", cmValue, newMarkdownDoc); + + this.markdownTextarea.text(cmValue); + + cm.save(); + + if (settings.saveHTMLToTextarea) + { + this.htmlTextarea.text(newMarkdownDoc); + } + + if(settings.watch || (!settings.watch && state.preview)) + { + previewContainer.html(newMarkdownDoc); + + this.previewCodeHighlight(); + + if (settings.toc) + { + var tocContainer = (settings.tocContainer === "") ? previewContainer : $(settings.tocContainer); + var tocMenu = tocContainer.find("." + this.classPrefix + "toc-menu"); + + tocContainer.attr("previewContainer", (settings.tocContainer === "") ? "true" : "false"); + + if (settings.tocContainer !== "" && tocMenu.length > 0) + { + tocMenu.remove(); + } + + editormd.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel); + + if (settings.tocDropdown || tocContainer.find("." + this.classPrefix + "toc-menu").length > 0) + { + editormd.tocDropdownMenu(tocContainer, (settings.tocTitle !== "") ? settings.tocTitle : this.lang.tocTitle); + } + + if (settings.tocContainer !== "") + { + previewContainer.find(".markdown-toc").css("border", "none"); + } + } + + if (settings.tex) + { + if (!editormd.kaTeXLoaded && settings.autoLoadModules) + { + editormd.loadKaTeX(function() { + editormd.$katex = katex; + editormd.kaTeXLoaded = true; + _this.katexRender(); + }); + } + else + { + editormd.$katex = katex; + this.katexRender(); + } + } + + if (settings.flowChart || settings.sequenceDiagram) + { + flowchartTimer = setTimeout(function(){ + clearTimeout(flowchartTimer); + _this.flowChartAndSequenceDiagramRender(); + flowchartTimer = null; + }, 10); + } + + if (state.loaded) + { + $.proxy(settings.onchange, this)(); + } + } + + return this; + }, + + /** + * 聚焦光标位置 + * Focusing the cursor position + * + * @returns {editormd} 返回editormd的实例对象 + */ + + focus : function() { + this.cm.focus(); + + return this; + }, + + /** + * 设置光标的位置 + * Set cursor position + * + * @param {Object} cursor 要设置的光标位置键值对象,例:{line:1, ch:0} + * @returns {editormd} 返回editormd的实例对象 + */ + + setCursor : function(cursor) { + this.cm.setCursor(cursor); + + return this; + }, + + /** + * 获取当前光标的位置 + * Get the current position of the cursor + * + * @returns {Cursor} 返回一个光标Cursor对象 + */ + + getCursor : function() { + return this.cm.getCursor(); + }, + + /** + * 设置光标选中的范围 + * Set cursor selected ranges + * + * @param {Object} from 开始位置的光标键值对象,例:{line:1, ch:0} + * @param {Object} to 结束位置的光标键值对象,例:{line:1, ch:0} + * @returns {editormd} 返回editormd的实例对象 + */ + + setSelection : function(from, to) { + + this.cm.setSelection(from, to); + + return this; + }, + + /** + * 获取光标选中的文本 + * Get the texts from cursor selected + * + * @returns {String} 返回选中文本的字符串形式 + */ + + getSelection : function() { + return this.cm.getSelection(); + }, + + /** + * 设置光标选中的文本范围 + * Set the cursor selection ranges + * + * @param {Array} ranges cursor selection ranges array + * @returns {Array} return this + */ + + setSelections : function(ranges) { + this.cm.setSelections(ranges); + + return this; + }, + + /** + * 获取光标选中的文本范围 + * Get the cursor selection ranges + * + * @returns {Array} return selection ranges array + */ + + getSelections : function() { + return this.cm.getSelections(); + }, + + /** + * 替换当前光标选中的文本或在当前光标处插入新字符 + * Replace the text at the current cursor selected or insert a new character at the current cursor position + * + * @param {String} value 要插入的字符值 + * @returns {editormd} 返回editormd的实例对象 + */ + + replaceSelection : function(value) { + this.cm.replaceSelection(value); + + return this; + }, + + /** + * 在当前光标处插入新字符 + * Insert a new character at the current cursor position + * + * 同replaceSelection()方法 + * With the replaceSelection() method + * + * @param {String} value 要插入的字符值 + * @returns {editormd} 返回editormd的实例对象 + */ + + insertValue : function(value) { + this.replaceSelection(value); + + return this; + }, + + /** + * 追加markdown + * append Markdown to editor + * + * @param {String} md 要追加的markdown源文档 + * @returns {editormd} 返回editormd的实例对象 + */ + + appendMarkdown : function(md) { + var settings = this.settings; + var cm = this.cm; + + cm.setValue(cm.getValue() + md); + + return this; + }, + + /** + * 设置和传入编辑器的markdown源文档 + * Set Markdown source document + * + * @param {String} md 要传入的markdown源文档 + * @returns {editormd} 返回editormd的实例对象 + */ + + setMarkdown : function(md) { + this.cm.setValue(md || this.settings.markdown); + + return this; + }, + + /** + * 获取编辑器的markdown源文档 + * Set Editor.md markdown/CodeMirror value + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getMarkdown : function() { + return this.cm.getValue(); + }, + + /** + * 获取编辑器的源文档 + * Get CodeMirror value + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getValue : function() { + return this.cm.getValue(); + }, + + /** + * 设置编辑器的源文档 + * Set CodeMirror value + * + * @param {String} value set code/value/string/text + * @returns {editormd} 返回editormd的实例对象 + */ + + setValue : function(value) { + this.cm.setValue(value); + + return this; + }, + + /** + * 清空编辑器 + * Empty CodeMirror editor container + * + * @returns {editormd} 返回editormd的实例对象 + */ + + clear : function() { + this.cm.setValue(""); + + return this; + }, + + /** + * 获取解析后存放在Textarea的HTML源码 + * Get parsed html code from Textarea + * + * @returns {String} 返回HTML源码 + */ + + getHTML : function() { + if (!this.settings.saveHTMLToTextarea) + { + alert("Error: settings.saveHTMLToTextarea == false"); + + return false; + } + + return this.htmlTextarea.val(); + }, + + /** + * getHTML()的别名 + * getHTML (alias) + * + * @returns {String} Return html code 返回HTML源码 + */ + + getTextareaSavedHTML : function() { + return this.getHTML(); + }, + + /** + * 获取预览窗口的HTML源码 + * Get html from preview container + * + * @returns {editormd} 返回editormd的实例对象 + */ + + getPreviewedHTML : function() { + if (!this.settings.watch) + { + alert("Error: settings.watch == false"); + + return false; + } + + return this.previewContainer.html(); + }, + + /** + * 开启实时预览 + * Enable real-time watching + * + * @returns {editormd} 返回editormd的实例对象 + */ + + watch : function(callback) { + var settings = this.settings; + + if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) + { + return this; + } + + this.state.watching = settings.watch = true; + this.preview.show(); + + if (this.toolbar) + { + var watchIcon = settings.toolbarIconsClass.watch; + var unWatchIcon = settings.toolbarIconsClass.unwatch; + + var icon = this.toolbar.find(".fa[name=watch]"); + icon.parent().attr("title", settings.lang.toolbar.watch); + icon.removeClass(unWatchIcon).addClass(watchIcon); + } + + this.codeMirror.css("border-right", "1px solid #ddd").width(this.editor.width() / 2); + + timer = 0; + + this.save().resize(); + + if (!settings.onwatch) + { + settings.onwatch = callback || function() {}; + } + + $.proxy(settings.onwatch, this)(); + + return this; + }, + + /** + * 关闭实时预览 + * Disable real-time watching + * + * @returns {editormd} 返回editormd的实例对象 + */ + + unwatch : function(callback) { + var settings = this.settings; + this.state.watching = settings.watch = false; + this.preview.hide(); + + if (this.toolbar) + { + var watchIcon = settings.toolbarIconsClass.watch; + var unWatchIcon = settings.toolbarIconsClass.unwatch; + + var icon = this.toolbar.find(".fa[name=watch]"); + icon.parent().attr("title", settings.lang.toolbar.unwatch); + icon.removeClass(watchIcon).addClass(unWatchIcon); + } + + this.codeMirror.css("border-right", "none").width(this.editor.width()); + + this.resize(); + + if (!settings.onunwatch) + { + settings.onunwatch = callback || function() {}; + } + + $.proxy(settings.onunwatch, this)(); + + return this; + }, + + /** + * 显示编辑器 + * Show editor + * + * @param {Function} [callback=function()] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + show : function(callback) { + callback = callback || function() {}; + + var _this = this; + this.editor.show(0, function() { + $.proxy(callback, _this)(); + }); + + return this; + }, + + /** + * 隐藏编辑器 + * Hide editor + * + * @param {Function} [callback=function()] 回调函数 + * @returns {editormd} 返回editormd的实例对象 + */ + + hide : function(callback) { + callback = callback || function() {}; + + var _this = this; + this.editor.hide(0, function() { + $.proxy(callback, _this)(); + }); + + return this; + }, + + /** + * 隐藏编辑器部分,只预览HTML + * Enter preview html state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewing : function() { + + var _this = this; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var codeMirror = this.codeMirror; + var previewContainer = this.previewContainer; + + if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) { + return this; + } + + if (settings.toolbar && toolbar) { + toolbar.toggle(); + toolbar.find(".fa[name=preview]").toggleClass("active"); + } + + codeMirror.toggle(); + + var escHandle = function(event) { + if (event.shiftKey && event.keyCode === 27) { + _this.previewed(); + } + }; + + if (codeMirror.css("display") === "none") // 为了兼容Zepto,而不使用codeMirror.is(":hidden") + { + this.state.preview = true; + + if (this.state.fullscreen) { + preview.css("background", "#fff"); + } + + editor.find("." + this.classPrefix + "preview-close-btn").show().bind(editormd.mouseOrTouch("click", "touchend"), function(){ + _this.previewed(); + }); + + if (!settings.watch) + { + this.save(); + } + else + { + previewContainer.css("padding", ""); + } + + previewContainer.addClass(this.classPrefix + "preview-active"); + + preview.show().css({ + position : "", + top : 0, + width : editor.width(), + height : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() + }); + + if (this.state.loaded) + { + $.proxy(settings.onpreviewing, this)(); + } + + $(window).bind("keyup", escHandle); + } + else + { + $(window).unbind("keyup", escHandle); + this.previewed(); + } + }, + + /** + * 显示编辑器部分,退出只预览HTML + * Exit preview html state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + previewed : function() { + + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var previewContainer = this.previewContainer; + var previewCloseBtn = editor.find("." + this.classPrefix + "preview-close-btn"); + + this.state.preview = false; + + this.codeMirror.show(); + + if (settings.toolbar) { + toolbar.show(); + } + + preview[(settings.watch) ? "show" : "hide"](); + + previewCloseBtn.hide().unbind(editormd.mouseOrTouch("click", "touchend")); + + previewContainer.removeClass(this.classPrefix + "preview-active"); + + if (settings.watch) + { + previewContainer.css("padding", "20px"); + } + + preview.css({ + background : null, + position : "absolute", + width : editor.width() / 2, + height : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() - toolbar.height(), + top : (settings.toolbar) ? toolbar.height() : 0 + }); + + if (this.state.loaded) + { + $.proxy(settings.onpreviewed, this)(); + } + + return this; + }, + + /** + * 编辑器全屏显示 + * Fullscreen show + * + * @returns {editormd} 返回editormd的实例对象 + */ + + fullscreen : function() { + + var _this = this; + var state = this.state; + var editor = this.editor; + var preview = this.preview; + var toolbar = this.toolbar; + var settings = this.settings; + var fullscreenClass = this.classPrefix + "fullscreen"; + + if (toolbar) { + toolbar.find(".fa[name=fullscreen]").parent().toggleClass("active"); + } + + var escHandle = function(event) { + if (!event.shiftKey && event.keyCode === 27) + { + if (state.fullscreen) + { + _this.fullscreenExit(); + } + } + }; + + if (!editor.hasClass(fullscreenClass)) + { + state.fullscreen = true; + + $("html,body").css("overflow", "hidden"); + + editor.css({ + width : $(window).width(), + height : $(window).height() + }).addClass(fullscreenClass); + + this.resize(); + + $.proxy(settings.onfullscreen, this)(); + + $(window).bind("keyup", escHandle); + } + else + { + $(window).unbind("keyup", escHandle); + this.fullscreenExit(); + } + + return this; + }, + + /** + * 编辑器退出全屏显示 + * Exit fullscreen state + * + * @returns {editormd} 返回editormd的实例对象 + */ + + fullscreenExit : function() { + + var editor = this.editor; + var settings = this.settings; + var toolbar = this.toolbar; + var fullscreenClass = this.classPrefix + "fullscreen"; + + this.state.fullscreen = false; + + if (toolbar) { + toolbar.find(".fa[name=fullscreen]").parent().removeClass("active"); + } + + $("html,body").css("overflow", ""); + + editor.css({ + width : editor.data("oldWidth"), + height : editor.data("oldHeight") + }).removeClass(fullscreenClass); + + this.resize(); + + $.proxy(settings.onfullscreenExit, this)(); + + return this; + }, + + /** + * 加载并执行插件 + * Load and execute the plugin + * + * @param {String} name plugin name / function name + * @param {String} path plugin load path + * @returns {editormd} 返回editormd的实例对象 + */ + + executePlugin : function(name, path) { + + var _this = this; + var cm = this.cm; + var settings = this.settings; + + path = settings.pluginPath + path; + + if (typeof define === "function") + { + if (typeof this[name] === "undefined") + { + alert("Error: " + name + " plugin is not found, you are not load this plugin."); + + return this; + } + + this[name](cm); + + return this; + } + + if ($.inArray(path, editormd.loadFiles.plugin) < 0) + { + editormd.loadPlugin(path, function() { + editormd.loadPlugins[name] = _this[name]; + _this[name](cm); + }); + } + else + { + $.proxy(editormd.loadPlugins[name], this)(cm); + } + + return this; + }, + + /** + * 搜索替换 + * Search & replace + * + * @param {String} command CodeMirror serach commands, "find, fintNext, fintPrev, clearSearch, replace, replaceAll" + * @returns {editormd} return this + */ + + search : function(command) { + var settings = this.settings; + + if (!settings.searchReplace) + { + alert("Error: settings.searchReplace == false"); + return this; + } + + if (!settings.readOnly) + { + this.cm.execCommand(command || "find"); + } + + return this; + }, + + searchReplace : function() { + this.search("replace"); + + return this; + }, + + searchReplaceAll : function() { + this.search("replaceAll"); + + return this; + } + }; + + editormd.fn.init.prototype = editormd.fn; + + /** + * 锁屏 + * lock screen when dialog opening + * + * @returns {void} + */ + + editormd.dialogLockScreen = function() { + var settings = this.settings || {dialogLockScreen : true}; + + if (settings.dialogLockScreen) + { + $("html,body").css("overflow", "hidden"); + this.resize(); + } + }; + + /** + * 显示透明背景层 + * Display mask layer when dialog opening + * + * @param {Object} dialog dialog jQuery object + * @returns {void} + */ + + editormd.dialogShowMask = function(dialog) { + var editor = this.editor; + var settings = this.settings || {dialogShowMask : true}; + + dialog.css({ + top : ($(window).height() - dialog.height()) / 2 + "px", + left : ($(window).width() - dialog.width()) / 2 + "px" + }); + + if (settings.dialogShowMask) { + editor.children("." + this.classPrefix + "mask").css("z-index", parseInt(dialog.css("z-index")) - 1).show(); + } + }; + + editormd.toolbarHandlers = { + undo : function() { + this.cm.undo(); + }, + + redo : function() { + this.cm.redo(); + }, + + bold : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("**" + selection + "**"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + del : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("~~" + selection + "~~"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + italic : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("*" + selection + "*"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + quote : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("> " + selection); + cm.setCursor(cursor.line, cursor.ch + 2); + } + else + { + cm.replaceSelection("> " + selection); + } + + //cm.replaceSelection("> " + selection); + //cm.setCursor(cursor.line, (selection === "") ? cursor.ch + 2 : cursor.ch + selection.length + 2); + }, + + ucfirst : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(editormd.firstUpperCase(selection)); + cm.setSelections(selections); + }, + + ucwords : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(editormd.wordsFirstUpperCase(selection)); + cm.setSelections(selections); + }, + + uppercase : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(selection.toUpperCase()); + cm.setSelections(selections); + }, + + lowercase : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var selections = cm.listSelections(); + + cm.replaceSelection(selection.toLowerCase()); + cm.setSelections(selections); + }, + + h1 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("# " + selection); + cm.setCursor(cursor.line, cursor.ch + 2); + } + else + { + cm.replaceSelection("# " + selection); + } + }, + + h2 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("## " + selection); + cm.setCursor(cursor.line, cursor.ch + 3); + } + else + { + cm.replaceSelection("## " + selection); + } + }, + + h3 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("### " + selection); + cm.setCursor(cursor.line, cursor.ch + 4); + } + else + { + cm.replaceSelection("### " + selection); + } + }, + + h4 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("#### " + selection); + cm.setCursor(cursor.line, cursor.ch + 5); + } + else + { + cm.replaceSelection("#### " + selection); + } + }, + + h5 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("##### " + selection); + cm.setCursor(cursor.line, cursor.ch + 6); + } + else + { + cm.replaceSelection("##### " + selection); + } + }, + + h6 : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (cursor.ch !== 0) + { + cm.setCursor(cursor.line, 0); + cm.replaceSelection("###### " + selection); + cm.setCursor(cursor.line, cursor.ch + 7); + } + else + { + cm.replaceSelection("###### " + selection); + } + }, + + "list-ul" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (selection === "") + { + cm.replaceSelection("- " + selection); + } + else + { + var selectionText = selection.split("\n"); + + for (var i = 0, len = selectionText.length; i < len; i++) + { + selectionText[i] = (selectionText[i] === "") ? "" : "- " + selectionText[i]; + } + + cm.replaceSelection(selectionText.join("\n")); + } + }, + + "list-ol" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if(selection === "") + { + cm.replaceSelection("1. " + selection); + } + else + { + var selectionText = selection.split("\n"); + + for (var i = 0, len = selectionText.length; i < len; i++) + { + selectionText[i] = (selectionText[i] === "") ? "" : (i+1) + ". " + selectionText[i]; + } + + cm.replaceSelection(selectionText.join("\n")); + } + }, + + hr : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection(((cursor.ch !== 0) ? "\n\n" : "\n") + "------------\n\n"); + }, + + tex : function() { + if (!this.settings.tex) + { + alert("settings.tex === false"); + return this; + } + + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("$$" + selection + "$$"); + + if(selection === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + }, + + link : function() { + this.executePlugin("linkDialog", "link-dialog/link-dialog"); + }, + + "reference-link" : function() { + this.executePlugin("referenceLinkDialog", "reference-link-dialog/reference-link-dialog"); + }, + + pagebreak : function() { + if (!this.settings.pageBreak) + { + alert("settings.pageBreak === false"); + return this; + } + + var cm = this.cm; + var selection = cm.getSelection(); + + cm.replaceSelection("\r\n[========]\r\n"); + }, + + image : function() { + this.executePlugin("imageDialog", "image-dialog/image-dialog"); + }, + + code : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection("`" + selection + "`"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + "code-block" : function() { + this.executePlugin("codeBlockDialog", "code-block-dialog/code-block-dialog"); + }, + + "preformatted-text" : function() { + this.executePlugin("preformattedTextDialog", "preformatted-text-dialog/preformatted-text-dialog"); + }, + + table : function() { + this.executePlugin("tableDialog", "table-dialog/table-dialog"); + }, + + datetime : function() { + var cm = this.cm; + var selection = cm.getSelection(); + var date = new Date(); + var langName = this.settings.lang.name; + var datefmt = editormd.dateFormat() + " " + editormd.dateFormat((langName === "zh-cn" || langName === "zh-tw") ? "cn-week-day" : "week-day"); + + cm.replaceSelection(datefmt); + }, + + emoji : function() { + this.executePlugin("emojiDialog", "emoji-dialog/emoji-dialog"); + }, + + "html-entities" : function() { + this.executePlugin("htmlEntitiesDialog", "html-entities-dialog/html-entities-dialog"); + }, + + "goto-line" : function() { + this.executePlugin("gotoLineDialog", "goto-line-dialog/goto-line-dialog"); + }, + + watch : function() { + this[this.settings.watch ? "unwatch" : "watch"](); + }, + + preview : function() { + this.previewing(); + }, + + fullscreen : function() { + this.fullscreen(); + }, + + clear : function() { + this.clear(); + }, + + search : function() { + this.search(); + }, + + help : function() { + this.executePlugin("helpDialog", "help-dialog/help-dialog"); + }, + + info : function() { + this.showInfoDialog(); + } + }; + + editormd.keyMaps = { + "Ctrl-1" : "h1", + "Ctrl-2" : "h2", + "Ctrl-3" : "h3", + "Ctrl-4" : "h4", + "Ctrl-5" : "h5", + "Ctrl-6" : "h6", + "Ctrl-B" : "bold", // if this is string == editormd.toolbarHandlers.xxxx + "Ctrl-D" : "datetime", + + "Ctrl-E" : function() { // emoji + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (!this.settings.emoji) + { + alert("Error: settings.emoji == false"); + return ; + } + + cm.replaceSelection(":" + selection + ":"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + "Ctrl-Alt-G" : "goto-line", + "Ctrl-H" : "hr", + "Ctrl-I" : "italic", + "Ctrl-K" : "code", + + "Ctrl-L" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + var title = (selection === "") ? "" : " \""+selection+"\""; + + cm.replaceSelection("[" + selection + "]("+title+")"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + "Ctrl-U" : "list-ul", + + "Shift-Ctrl-A" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + if (!this.settings.atLink) + { + alert("Error: settings.atLink == false"); + return ; + } + + cm.replaceSelection("@" + selection); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + }, + + "Shift-Ctrl-C" : "code", + "Shift-Ctrl-Q" : "quote", + "Shift-Ctrl-S" : "del", + "Shift-Ctrl-K" : "tex", // KaTeX + + "Shift-Alt-C" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + cm.replaceSelection(["```", selection, "```"].join("\n")); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 3); + } + }, + + "Shift-Ctrl-Alt-C" : "code-block", + "Shift-Ctrl-H" : "html-entities", + "Shift-Alt-H" : "help", + "Shift-Ctrl-E" : "emoji", + "Shift-Ctrl-U" : "uppercase", + "Shift-Alt-U" : "ucwords", + "Shift-Ctrl-Alt-U" : "ucfirst", + "Shift-Alt-L" : "lowercase", + + "Shift-Ctrl-I" : function() { + var cm = this.cm; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + + var title = (selection === "") ? "" : " \""+selection+"\""; + + cm.replaceSelection("![" + selection + "]("+title+")"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 4); + } + }, + + "Shift-Ctrl-Alt-I" : "image", + "Shift-Ctrl-L" : "link", + "Shift-Ctrl-O" : "list-ol", + "Shift-Ctrl-P" : "preformatted-text", + "Shift-Ctrl-T" : "table", + "Shift-Alt-P" : "pagebreak", + "F9" : "watch", + "F10" : "preview", + "F11" : "fullscreen", + }; + + /** + * 清除字符串两边的空格 + * Clear the space of strings both sides. + * + * @param {String} str string + * @returns {String} trimed string + */ + + var trim = function(str) { + return (!String.prototype.trim) ? str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") : str.trim(); + }; + + editormd.trim = trim; + + /** + * 所有单词首字母大写 + * Words first to uppercase + * + * @param {String} str string + * @returns {String} string + */ + + var ucwords = function (str) { + return str.toLowerCase().replace(/\b(\w)|\s(\w)/g, function($1) { + return $1.toUpperCase(); + }); + }; + + editormd.ucwords = editormd.wordsFirstUpperCase = ucwords; + + /** + * 字符串首字母大写 + * Only string first char to uppercase + * + * @param {String} str string + * @returns {String} string + */ + + var firstUpperCase = function(str) { + return str.toLowerCase().replace(/\b(\w)/, function($1){ + return $1.toUpperCase(); + }); + }; + + var ucfirst = firstUpperCase; + + editormd.firstUpperCase = editormd.ucfirst = firstUpperCase; + + editormd.urls = { + atLinkBase : "https://github.com/" + }; + + editormd.regexs = { + atLink : /@(\w+)/g, + email : /(\w+)@(\w+)\.(\w+)\.?(\w+)?/g, + emailLink : /(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g, + emoji : /:([\w\+-]+):/g, + emojiDatetime : /(\d{2}:\d{2}:\d{2})/g, + twemoji : /:(tw-([\w]+)-?(\w+)?):/g, + fontAwesome : /:(fa-([\w]+)(-(\w+)){0,}):/g, + editormdLogo : /:(editormd-logo-?(\w+)?):/g, + pageBreak : /^\[[=]{8,}\]$/ + }; + + // Emoji graphics files url path + editormd.emoji = { + path : "https://www.webfx.com/tools/emoji-cheat-sheet/graphics/emojis/", + ext : ".png" + }; + + // Twitter Emoji (Twemoji) graphics files url path + editormd.twemoji = { + path : "https://twemoji.maxcdn.com/36x36/", + ext : ".png" + }; + + /** + * 自定义marked的解析器 + * Custom Marked renderer rules + * + * @param {Array} markdownToC 传入用于接收TOC的数组 + * @returns {Renderer} markedRenderer 返回marked的Renderer自定义对象 + */ + + editormd.markedRenderer = function(markdownToC, options) { + var defaults = { + toc : true, // Table of contents + tocm : false, + tocStartLevel : 1, // Said from H1 to create ToC + pageBreak : true, + atLink : true, // for @link + emailLink : true, // for mail address auto link + taskList : false, // Enable Github Flavored Markdown task lists + emoji : false, // :emoji: , Support Twemoji, fontAwesome, Editor.md logo emojis. + tex : false, // TeX(LaTeX), based on KaTeX + flowChart : false, // flowChart.js only support IE9+ + sequenceDiagram : false, // sequenceDiagram.js only support IE9+ + }; + + var settings = $.extend(defaults, options || {}); + var marked = editormd.$marked; + var markedRenderer = new marked.Renderer(); + markdownToC = markdownToC || []; + + var regexs = editormd.regexs; + var atLinkReg = regexs.atLink; + var emojiReg = regexs.emoji; + var emailReg = regexs.email; + var emailLinkReg = regexs.emailLink; + var twemojiReg = regexs.twemoji; + var faIconReg = regexs.fontAwesome; + var editormdLogoReg = regexs.editormdLogo; + var pageBreakReg = regexs.pageBreak; + + markedRenderer.emoji = function(text) { + + text = text.replace(editormd.regexs.emojiDatetime, function($1) { + return $1.replace(/:/g, ":"); + }); + + var matchs = text.match(emojiReg); + + if (!matchs || !settings.emoji) { + return text; + } + + for (var i = 0, len = matchs.length; i < len; i++) + { + if (matchs[i] === ":+1:") { + matchs[i] = ":\\+1:"; + } + + text = text.replace(new RegExp(matchs[i]), function($1, $2){ + var faMatchs = $1.match(faIconReg); + var name = $1.replace(/:/g, ""); + + if (faMatchs) + { + for (var fa = 0, len1 = faMatchs.length; fa < len1; fa++) + { + var faName = faMatchs[fa].replace(/:/g, ""); + + return ""; + } + } + else + { + var emdlogoMathcs = $1.match(editormdLogoReg); + var twemojiMatchs = $1.match(twemojiReg); + + if (emdlogoMathcs) + { + for (var x = 0, len2 = emdlogoMathcs.length; x < len2; x++) + { + var logoName = emdlogoMathcs[x].replace(/:/g, ""); + return ""; + } + } + else if (twemojiMatchs) + { + for (var t = 0, len3 = twemojiMatchs.length; t < len3; t++) + { + var twe = twemojiMatchs[t].replace(/:/g, "").replace("tw-", ""); + return "\"twemoji-""; + } + } + else if (name === "thinking") // cavern emoji + { + return "\":thinking:\""; + } + else + { + var src = (name === "+1") ? "plus1" : name; + src = (src === "black_large_square") ? "black_square" : src; + src = (src === "moon") ? "waxing_gibbous_moon" : src; + + return "\":""; + } + } + }); + } + + return text; + }; + + markedRenderer.atLink = function(text) { + + if (atLinkReg.test(text)) + { + if (settings.atLink) + { + text = text.replace(emailReg, function($1, $2, $3, $4) { + return $1.replace(/@/g, "_#_@_#_"); + }); + + text = text.replace(atLinkReg, function($1, $2) { + return "" + $1 + ""; + }).replace(/_#_@_#_/g, "@"); + } + + if (settings.emailLink) + { + text = text.replace(emailLinkReg, function($1, $2, $3, $4, $5) { + return (!$2 && $.inArray($5, "jpg|jpeg|png|gif|webp|ico|icon|pdf".split("|")) < 0) ? ""+$1+"" : $1; + }); + } + + return text; + } + + return text; + }; + + markedRenderer.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) { + return ""; + } + } + + var out = "" + text.replace(/@/g, "@") + ""; + } + + if (title) { + out += " title=\"" + title + "\""; + } + + out += ">" + text + ""; + + return out; + }; + + markedRenderer.heading = function(text, level, raw) { + + var linkText = text; + var hasLinkReg = /\s*\]*)\>(.*)\<\/a\>\s*/; + var getLinkTextReg = /\s*\]+)\>([^\>]*)\<\/a\>\s*/g; + + if (hasLinkReg.test(text)) + { + var tempText = []; + text = text.split(/\]+)\>([^\>]*)\<\/a\>/); + + for (var i = 0, len = text.length; i < len; i++) + { + tempText.push(text[i].replace(/\s*href\=\"(.*)\"\s*/g, "")); + } + + text = tempText.join(" "); + } + + text = trim(text); + + var escapedText = text.toLowerCase().replace(/[^\w]+/g, "-"); + var toc = { + text : text, + level : level, + slug : escapedText + }; + + var isChinese = /^[\u4e00-\u9fa5]+$/.test(text); + var id = (isChinese) ? escape(text).replace(/\%/g, "") : text.toLowerCase().replace(/[^\w]+/g, "-"); + + markdownToC.push(toc); + + var headingHTML = ""; + + headingHTML += ""; + headingHTML += ""; + headingHTML += (hasLinkReg) ? this.atLink(this.emoji(linkText)) : this.atLink(this.emoji(text)); + headingHTML += ""; + + return headingHTML; + }; + + markedRenderer.pageBreak = function(text) { + if (pageBreakReg.test(text) && settings.pageBreak) + { + text = "
    "; + } + + return text; + }; + + markedRenderer.paragraph = function(text) { + var isTeXInline = /\$\$(.*)\$\$/g.test(text); + var isTeXLine = /^\$\$(.*)\$\$$/.test(text); + var isTeXAddClass = (isTeXLine) ? " class=\"" + editormd.classNames.tex + "\"" : ""; + var isToC = (settings.tocm) ? /^(\[TOC\]|\[TOCM\])$/.test(text) : /^\[TOC\]$/.test(text); + var isToCMenu = /^\[TOCM\]$/.test(text); + + if (!isTeXLine && isTeXInline) + { + text = text.replace(/(\$\$([^\$]*)\$\$)+/g, function($1, $2) { + return "" + $2.replace(/\$/g, "") + ""; + }); + } + else + { + text = (isTeXLine) ? text.replace(/\$/g, "") : text; + } + + var tocHTML = "
    " + text + "
    "; + + return (isToC) ? ( (isToCMenu) ? "
    " + tocHTML + "

    " : tocHTML ) + : ( (pageBreakReg.test(text)) ? this.pageBreak(text) : "" + this.atLink(this.emoji(text)) + "

    \n" ); + }; + + markedRenderer.code = function (code, lang, escaped) { + + if (lang === "seq" || lang === "sequence") + { + return "
    " + code + "
    "; + } + else if ( lang === "flow") + { + return "
    " + code + "
    "; + } + else if ( lang === "math" || lang === "latex" || lang === "katex") + { + return "

    " + code + "

    "; + } + else + { + + return marked.Renderer.prototype.code.apply(this, arguments); + } + }; + + markedRenderer.tablecell = function(content, flags) { + var type = (flags.header) ? "th" : "td"; + var tag = (flags.align) ? "<" + type +" style=\"text-align:" + flags.align + "\">" : "<" + type + ">"; + + return tag + this.atLink(this.emoji(content)) + "\n"; + }; + + markedRenderer.listitem = function(text) { + if (settings.taskList && /^\s*\[[x\s]\]\s*/.test(text)) + { + text = text.replace(/^\s*\[\s\]\s*/, " ") + .replace(/^\s*\[x\]\s*/, " "); + + return "
  • " + this.atLink(this.emoji(text)) + "
  • "; + } + else + { + return "
  • " + this.atLink(this.emoji(text)) + "
  • "; + } + }; + + return markedRenderer; + }; + + /** + * + * 生成TOC(Table of Contents) + * Creating ToC (Table of Contents) + * + * @param {Array} toc 从marked获取的TOC数组列表 + * @param {Element} container 插入TOC的容器元素 + * @param {Integer} startLevel Hx 起始层级 + * @returns {Object} tocContainer 返回ToC列表容器层的jQuery对象元素 + */ + + editormd.markdownToCRenderer = function(toc, container, tocDropdown, startLevel) { + + var html = ""; + var lastLevel = 0; + var classPrefix = this.classPrefix; + + startLevel = startLevel || 1; + + for (var i = 0, len = toc.length; i < len; i++) + { + var text = toc[i].text; + var level = toc[i].level; + + if (level < startLevel) { + continue; + } + + if (level > lastLevel) + { + html += ""; + } + else if (level < lastLevel) + { + html += (new Array(lastLevel - level + 2)).join(""); + } + else + { + html += ""; + } + + html += "
  • " + text + "
      "; + lastLevel = level; + } + + var tocContainer = container.find(".markdown-toc"); + + if ((tocContainer.length < 1 && container.attr("previewContainer") === "false")) + { + var tocHTML = "
      "; + + tocHTML = (tocDropdown) ? "
      " + tocHTML + "
      " : tocHTML; + + container.html(tocHTML); + + tocContainer = container.find(".markdown-toc"); + } + + if (tocDropdown) + { + tocContainer.wrap("

      "); + } + + tocContainer.html("
        ").children(".markdown-toc-list").html(html.replace(/\r?\n?\\<\/ul\>/g, "")); + + return tocContainer; + }; + + /** + * + * 生成TOC下拉菜单 + * Creating ToC dropdown menu + * + * @param {Object} container 插入TOC的容器jQuery对象元素 + * @param {String} tocTitle ToC title + * @returns {Object} return toc-menu object + */ + + editormd.tocDropdownMenu = function(container, tocTitle) { + + tocTitle = tocTitle || "Table of Contents"; + + var zindex = 400; + var tocMenus = container.find("." + this.classPrefix + "toc-menu"); + + tocMenus.each(function() { + var $this = $(this); + var toc = $this.children(".markdown-toc"); + var icon = ""; + var btn = "" + icon + tocTitle + ""; + var menu = toc.children("ul"); + var list = menu.find("li"); + + toc.append(btn); + + list.first().before("
      • " + tocTitle + " " + icon + "

      • "); + + $this.mouseover(function(){ + menu.show(); + + list.each(function(){ + var li = $(this); + var ul = li.children("ul"); + + if (ul.html() === "") + { + ul.remove(); + } + + if (ul.length > 0 && ul.html() !== "") + { + var firstA = li.children("a").first(); + + if (firstA.children(".fa").length < 1) + { + firstA.append( $(icon).css({ float:"right", paddingTop:"4px" }) ); + } + } + + li.mouseover(function(){ + ul.css("z-index", zindex).show(); + zindex += 1; + }).mouseleave(function(){ + ul.hide(); + }); + }); + }).mouseleave(function(){ + menu.hide(); + }); + }); + + return tocMenus; + }; + + /** + * 简单地过滤指定的HTML标签 + * Filter custom html tags + * + * @param {String} html 要过滤HTML + * @param {String} filters 要过滤的标签 + * @returns {String} html 返回过滤的HTML + */ + + editormd.filterHTMLTags = function(html, filters) { + + if (typeof html !== "string") { + html = new String(html).valueOf(); + } + + if (typeof filters !== "string") { + return html; + } + + var expression = filters.split("|"); + var filterTags = expression[0].split(","); + var attrs = expression[1]; + + for (var i = 0, len = filterTags.length; i < len; i++) + { + var tag = filterTags[i]; + + html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>([^\>]*)\<\s*\/" + tag + "\s*\>", "igm"), ""); + } + + //return html; + + if (typeof attrs !== "undefined") + { + var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/ig; + + if (attrs === "*") + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { + return "<" + $2 + ">" + $4 + ""; + }); + } + else if (attrs === "on*") + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) { + var el = $("<" + $2 + ">" + $4 + ""); + var _attrs = $($1)[0].attributes; + var $attrs = {}; + + $.each(_attrs, function(i, e) { + if (e.nodeName !== '"') $attrs[e.nodeName] = e.nodeValue; + }); + + $.each($attrs, function(i) { + if (i.indexOf("on") === 0) { + delete $attrs[i]; + } + }); + + el.attr($attrs); + + var text = (typeof el[1] !== "undefined") ? $(el[1]).text() : ""; + + return el[0].outerHTML + text; + }); + } + else + { + html = html.replace(htmlTagRegex, function($1, $2, $3, $4) { + var filterAttrs = attrs.split(","); + var el = $($1); + el.html($4); + + $.each(filterAttrs, function(i) { + el.attr(filterAttrs[i], null); + }); + + return el[0].outerHTML; + }); + } + } + + return html; + }; + + /** + * 将Markdown文档解析为HTML用于前台显示 + * Parse Markdown to HTML for Font-end preview. + * + * @param {String} id 用于显示HTML的对象ID + * @param {Object} [options={}] 配置选项,可选 + * @returns {Object} div 返回jQuery对象元素 + */ + + editormd.markdownToHTML = function(id, options) { + var defaults = { + gfm : true, + toc : true, + tocm : false, + tocStartLevel : 1, + tocTitle : "目录", + tocDropdown : false, + tocContainer : "", + markdown : "", + markdownSourceCode : false, + htmlDecode : false, + autoLoadKaTeX : true, + pageBreak : true, + atLink : true, // for @link + emailLink : true, // for mail address auto link + tex : false, + taskList : false, // Github Flavored Markdown task lists + emoji : false, + flowChart : false, + sequenceDiagram : false, + previewCodeHighlight : true + }; + + editormd.$marked = marked; + + var div = $("#" + id); + var settings = div.settings = $.extend(true, defaults, options || {}); + var saveTo = div.find("textarea"); + + if (saveTo.length < 1) + { + div.append(""); + saveTo = div.find("textarea"); + } + + var markdownDoc = (settings.markdown === "") ? saveTo.val() : settings.markdown; + var markdownToC = []; + + var rendererOptions = { + toc : settings.toc, + tocm : settings.tocm, + tocStartLevel : settings.tocStartLevel, + taskList : settings.taskList, + emoji : settings.emoji, + tex : settings.tex, + pageBreak : settings.pageBreak, + atLink : settings.atLink, // for @link + emailLink : settings.emailLink, // for mail address auto link + flowChart : settings.flowChart, + sequenceDiagram : settings.sequenceDiagram, + previewCodeHighlight : settings.previewCodeHighlight, + }; + + var markedOptions = { + renderer : editormd.markedRenderer(markdownToC, rendererOptions), + gfm : settings.gfm, + tables : true, + breaks : true, + pedantic : false, + sanitize : (settings.htmlDecode) ? false : true, // 是否忽略HTML标签,即是否开启HTML标签解析,为了安全性,默认不开启 + smartLists : true, + smartypants : true + }; + + markdownDoc = new String(markdownDoc).valueOf(); + + var markdownParsed = marked(markdownDoc, markedOptions); + + markdownParsed = editormd.filterHTMLTags(markdownParsed, settings.htmlDecode); + + if (settings.markdownSourceCode) { + saveTo.text(markdownDoc); + } else { + saveTo.remove(); + } + + div.addClass("markdown-body " + this.classPrefix + "html-preview").append(markdownParsed); + + var tocContainer = (settings.tocContainer !== "") ? $(settings.tocContainer) : div; + + if (settings.tocContainer !== "") + { + tocContainer.attr("previewContainer", false); + } + + if (settings.toc) + { + div.tocContainer = this.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel); + + if (settings.tocDropdown || div.find("." + this.classPrefix + "toc-menu").length > 0) + { + this.tocDropdownMenu(div, settings.tocTitle); + } + + if (settings.tocContainer !== "") + { + div.find(".editormd-toc-menu, .editormd-markdown-toc").remove(); + } + } + + if (settings.previewCodeHighlight) + { + div.find("pre").addClass("prettyprint linenums"); + prettyPrint(); + } + + if (!editormd.isIE8) + { + if (settings.flowChart) { + div.find(".flowchart").flowChart(); + } + + if (settings.sequenceDiagram) { + div.find(".sequence-diagram").sequenceDiagram({theme: "simple"}); + } + } + + if (settings.tex) + { + var katexHandle = function() { + div.find("." + editormd.classNames.tex).each(function(){ + var tex = $(this); + katex.render(tex.text(), tex[0]); + tex.find(".katex").css("font-size", "1.35em"); + }); + }; + + if (settings.autoLoadKaTeX && !editormd.$katex && !editormd.kaTeXLoaded) + { + this.loadKaTeX(function() { + editormd.$katex = katex; + editormd.kaTeXLoaded = true; + katexHandle(); + }); + } + else + { + katexHandle(); + } + } + + div.getMarkdown = function() { + return saveTo.val(); + }; + + return div; + }; + + // Editor.md themes, change toolbar themes etc. + // added @1.5.0 + editormd.themes = ["default", "dark"]; + + // Preview area themes + // added @1.5.0 + editormd.previewThemes = ["default", "dark"]; + + // CodeMirror / editor area themes + // @1.5.0 rename -> editorThemes, old version -> themes + editormd.editorThemes = [ + "default", "3024-day", "3024-night", + "ambiance", "ambiance-mobile", + "base16-dark", "base16-light", "blackboard", + "cobalt", + "eclipse", "elegant", "erlang-dark", + "lesser-dark", + "mbo", "mdn-like", "midnight", "monokai", + "neat", "neo", "night", + "paraiso-dark", "paraiso-light", "pastel-on-dark", + "rubyblue", + "solarized", + "the-matrix", "tomorrow-night-eighties", "twilight", + "vibrant-ink", + "xq-dark", "xq-light" + ]; + + editormd.loadPlugins = {}; + + editormd.loadFiles = { + js : [], + css : [], + plugin : [] + }; + + /** + * 动态加载Editor.md插件,但不立即执行 + * Load editor.md plugins + * + * @param {String} fileName 插件文件路径 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadPlugin = function(fileName, callback, into) { + callback = callback || function() {}; + + this.loadScript(fileName, function() { + editormd.loadFiles.plugin.push(fileName); + callback(); + }, into); + }; + + /** + * 动态加载CSS文件的方法 + * Load css file method + * + * @param {String} fileName CSS文件名 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadCSS = function(fileName, callback, into) { + into = into || "head"; + callback = callback || function() {}; + + var css = document.createElement("link"); + css.type = "text/css"; + css.rel = "stylesheet"; + css.onload = css.onreadystatechange = function() { + editormd.loadFiles.css.push(fileName); + callback(); + }; + + css.href = fileName + ".css"; + + if(into === "head") { + document.getElementsByTagName("head")[0].appendChild(css); + } else { + document.body.appendChild(css); + } + }; + + editormd.isIE = (navigator.appName == "Microsoft Internet Explorer"); + editormd.isIE8 = (editormd.isIE && navigator.appVersion.match(/8./i) == "8."); + + /** + * 动态加载JS文件的方法 + * Load javascript file method + * + * @param {String} fileName JS文件名 + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + * @param {String} [into="head"] 嵌入页面的位置 + */ + + editormd.loadScript = function(fileName, callback, into) { + + into = into || "head"; + callback = callback || function() {}; + + var script = null; + script = document.createElement("script"); + script.id = fileName.replace(/[\./]+/g, "-"); + script.type = "text/javascript"; + script.src = fileName + ".js"; + + if (editormd.isIE8) + { + script.onreadystatechange = function() { + if(script.readyState) + { + if (script.readyState === "loaded" || script.readyState === "complete") + { + script.onreadystatechange = null; + editormd.loadFiles.js.push(fileName); + callback(); + } + } + }; + } + else + { + script.onload = function() { + editormd.loadFiles.js.push(fileName); + callback(); + }; + } + + if (into === "head") { + document.getElementsByTagName("head")[0].appendChild(script); + } else { + document.body.appendChild(script); + } + }; + + // 使用国外的CDN,加载速度有时会很慢,或者自定义URL + // You can custom KaTeX load url. + editormd.katexURL = { + css : "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.10.1/katex.min", + js : "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.10.1/katex.min" + }; + + editormd.kaTeXLoaded = false; + + /** + * 加载KaTeX文件 + * load KaTeX files + * + * @param {Function} [callback=function()] 加载成功后执行的回调函数 + */ + + editormd.loadKaTeX = function (callback) { + editormd.loadCSS(editormd.katexURL.css, function(){ + editormd.loadScript(editormd.katexURL.js, callback || function(){}); + }); + }; + + /** + * 锁屏 + * lock screen + * + * @param {Boolean} lock Boolean 布尔值,是否锁屏 + * @returns {void} + */ + + editormd.lockScreen = function(lock) { + $("html,body").css("overflow", (lock) ? "hidden" : ""); + }; + + /** + * 动态创建对话框 + * Creating custom dialogs + * + * @param {Object} options 配置项键值对 Key/Value + * @returns {dialog} 返回创建的dialog的jQuery实例对象 + */ + + editormd.createDialog = function(options) { + var defaults = { + name : "", + width : 420, + height: 240, + title : "", + drag : true, + closed : true, + content : "", + mask : true, + maskStyle : { + backgroundColor : "#fff", + opacity : 0.1 + }, + lockScreen : true, + footer : true, + buttons : false + }; + + options = $.extend(true, defaults, options); + + var $this = this; + var editor = this.editor; + var classPrefix = editormd.classPrefix; + var guid = (new Date()).getTime(); + var dialogName = ( (options.name === "") ? classPrefix + "dialog-" + guid : options.name); + var mouseOrTouch = editormd.mouseOrTouch; + + var html = "
        "; + + if (options.title !== "") + { + html += "
        "; + html += "" + options.title + ""; + html += "
        "; + } + + if (options.closed) + { + html += ""; + } + + html += "
        " + options.content; + + if (options.footer || typeof options.footer === "string") + { + html += "
        " + ( (typeof options.footer === "boolean") ? "" : options.footer) + "
        "; + } + + html += "
        "; + + html += "
        "; + html += "
        "; + html += "
        "; + + editor.append(html); + + var dialog = editor.find("." + dialogName); + + dialog.lockScreen = function(lock) { + if (options.lockScreen) + { + $("html,body").css("overflow", (lock) ? "hidden" : ""); + $this.resize(); + } + + return dialog; + }; + + dialog.showMask = function() { + if (options.mask) + { + editor.find("." + classPrefix + "mask").css(options.maskStyle).css("z-index", editormd.dialogZindex - 1).show(); + } + return dialog; + }; + + dialog.hideMask = function() { + if (options.mask) + { + editor.find("." + classPrefix + "mask").hide(); + } + + return dialog; + }; + + dialog.loading = function(show) { + var loading = dialog.find("." + classPrefix + "dialog-mask"); + loading[(show) ? "show" : "hide"](); + + return dialog; + }; + + dialog.lockScreen(true).showMask(); + + dialog.show().css({ + zIndex : editormd.dialogZindex, + border : (editormd.isIE8) ? "1px solid #ddd" : "", + width : (typeof options.width === "number") ? options.width + "px" : options.width, + height : (typeof options.height === "number") ? options.height + "px" : options.height + }); + + var dialogPosition = function(){ + dialog.css({ + top : ($(window).height() - dialog.height()) / 2 + "px", + left : ($(window).width() - dialog.width()) / 2 + "px" + }); + }; + + dialogPosition(); + + $(window).resize(dialogPosition); + + dialog.children("." + classPrefix + "dialog-close").bind(mouseOrTouch("click", "touchend"), function() { + dialog.hide().lockScreen(false).hideMask(); + }); + + if (typeof options.buttons === "object") + { + var footer = dialog.footer = dialog.find("." + classPrefix + "dialog-footer"); + + for (var key in options.buttons) + { + var btn = options.buttons[key]; + var btnClassName = classPrefix + key + "-btn"; + + footer.append(""); + btn[1] = $.proxy(btn[1], dialog); + footer.children("." + btnClassName).bind(mouseOrTouch("click", "touchend"), btn[1]); + } + } + + if (options.title !== "" && options.drag) + { + var posX, posY; + var dialogHeader = dialog.children("." + classPrefix + "dialog-header"); + + if (!options.mask) { + dialogHeader.bind(mouseOrTouch("click", "touchend"), function(){ + editormd.dialogZindex += 2; + dialog.css("z-index", editormd.dialogZindex); + }); + } + + dialogHeader.mousedown(function(e) { + e = e || window.event; //IE + posX = e.clientX - parseInt(dialog[0].style.left); + posY = e.clientY - parseInt(dialog[0].style.top); + + document.onmousemove = moveAction; + }); + + var userCanSelect = function (obj) { + obj.removeClass(classPrefix + "user-unselect").off("selectstart"); + }; + + var userUnselect = function (obj) { + obj.addClass(classPrefix + "user-unselect").on("selectstart", function(event) { // selectstart for IE + return false; + }); + }; + + var moveAction = function (e) { + e = e || window.event; //IE + + var left, top, nowLeft = parseInt(dialog[0].style.left), nowTop = parseInt(dialog[0].style.top); + + if( nowLeft >= 0 ) { + if( nowLeft + dialog.width() <= $(window).width()) { + left = e.clientX - posX; + } else { + left = $(window).width() - dialog.width(); + document.onmousemove = null; + } + } else { + left = 0; + document.onmousemove = null; + } + + if( nowTop >= 0 ) { + top = e.clientY - posY; + } else { + top = 0; + document.onmousemove = null; + } + + + document.onselectstart = function() { + return false; + }; + + userUnselect($("body")); + userUnselect(dialog); + dialog[0].style.left = left + "px"; + dialog[0].style.top = top + "px"; + }; + + document.onmouseup = function() { + userCanSelect($("body")); + userCanSelect(dialog); + + document.onselectstart = null; + document.onmousemove = null; + }; + + dialogHeader.touchDraggable = function() { + var offset = null; + var start = function(e) { + var orig = e.originalEvent; + var pos = $(this).parent().position(); + + offset = { + x : orig.changedTouches[0].pageX - pos.left, + y : orig.changedTouches[0].pageY - pos.top + }; + }; + + var move = function(e) { + e.preventDefault(); + var orig = e.originalEvent; + + $(this).parent().css({ + top : orig.changedTouches[0].pageY - offset.y, + left : orig.changedTouches[0].pageX - offset.x + }); + }; + + this.bind("touchstart", start).bind("touchmove", move); + }; + + dialogHeader.touchDraggable(); + } + + editormd.dialogZindex += 2; + + return dialog; + }; + + /** + * 鼠标和触摸事件的判断/选择方法 + * MouseEvent or TouchEvent type switch + * + * @param {String} [mouseEventType="click"] 供选择的鼠标事件 + * @param {String} [touchEventType="touchend"] 供选择的触摸事件 + * @returns {String} EventType 返回事件类型名称 + */ + + editormd.mouseOrTouch = function(mouseEventType, touchEventType) { + mouseEventType = mouseEventType || "click"; + touchEventType = touchEventType || "touchend"; + + var eventType = mouseEventType; + + try { + document.createEvent("TouchEvent"); + eventType = touchEventType; + } catch(e) {} + + return eventType; + }; + + /** + * 日期时间的格式化方法 + * Datetime format method + * + * @param {String} [format=""] 日期时间的格式,类似PHP的格式 + * @returns {String} datefmt 返回格式化后的日期时间字符串 + */ + + editormd.dateFormat = function(format) { + format = format || ""; + + var addZero = function(d) { + return (d < 10) ? "0" + d : d; + }; + + var date = new Date(); + var year = date.getFullYear(); + var year2 = year.toString().slice(2, 4); + var month = addZero(date.getMonth() + 1); + var day = addZero(date.getDate()); + var weekDay = date.getDay(); + var hour = addZero(date.getHours()); + var min = addZero(date.getMinutes()); + var second = addZero(date.getSeconds()); + var ms = addZero(date.getMilliseconds()); + var datefmt = ""; + + var ymd = year2 + "-" + month + "-" + day; + var fymd = year + "-" + month + "-" + day; + var hms = hour + ":" + min + ":" + second; + + switch (format) + { + case "UNIX Time" : + datefmt = date.getTime(); + break; + + case "UTC" : + datefmt = date.toUTCString(); + break; + + case "yy" : + datefmt = year2; + break; + + case "year" : + case "yyyy" : + datefmt = year; + break; + + case "month" : + case "mm" : + datefmt = month; + break; + + case "cn-week-day" : + case "cn-wd" : + var cnWeekDays = ["日", "一", "二", "三", "四", "五", "六"]; + datefmt = "星期" + cnWeekDays[weekDay]; + break; + + case "week-day" : + case "wd" : + var weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + datefmt = weekDays[weekDay]; + break; + + case "day" : + case "dd" : + datefmt = day; + break; + + case "hour" : + case "hh" : + datefmt = hour; + break; + + case "min" : + case "ii" : + datefmt = min; + break; + + case "second" : + case "ss" : + datefmt = second; + break; + + case "ms" : + datefmt = ms; + break; + + case "yy-mm-dd" : + datefmt = ymd; + break; + + case "yyyy-mm-dd" : + datefmt = fymd; + break; + + case "yyyy-mm-dd h:i:s ms" : + case "full + ms" : + datefmt = fymd + " " + hms + " " + ms; + break; + + case "full" : + case "yyyy-mm-dd h:i:s" : + default: + datefmt = fymd + " " + hms; + break; + } + + return datefmt; + }; + + return editormd; + +})); diff --git a/include/js/lib/tocas.js b/include/js/lib/tocas.js new file mode 100644 index 0000000..223ba77 --- /dev/null +++ b/include/js/lib/tocas.js @@ -0,0 +1,74 @@ +var Tocas,animationEnd,bindModalButtons,closeModal,contractDropdown,detectDropdown,expandDropdown,quadrant,slider_progressColor,slider_trackColor,z_dropdownActive,z_dropdownHovered,z_dropdownMenu;Tocas=(function(){var compact,dropzoneNumber,emptyArray,filter,isArray,isEmptyOrWhiteSpace,isObject,queue,slice,tocas,ts;ts=void 0;emptyArray=[];slice=emptyArray.slice;filter=emptyArray.filter;queue=[];tocas={};isArray=Array.isArray||function(obj){return obj instanceof Array;};isObject=function(obj){return obj instanceof Object;};isEmptyOrWhiteSpace=function(str){return str===null||str.match(/^\s*$/)!==null;};dropzoneNumber=0;compact=function(array){return filter.call(array,function(item){return item!==null;});};tocas.init=function(selector,context){var dom;dom=void 0;if(typeof selector==='string'){if(selector[0]==='<'){return tocas.fragment(selector);} +selector=selector.trim();if(typeof context!=='undefined'){return ts(selector).find(context);} +dom=tocas.select(document,selector);}else if(tocas.isTocas(selector)){return selector;}else{if(isArray(selector)){dom=compact(selector);}else if(isObject(selector)){dom=[selector];selector=null;}} +return tocas.Tocas(dom,selector);};tocas.fragment=function(selector){var $element,attrObj,attrs,content,contentMatch,contentRegEx,hasAttr,hasContent,i,mainAll,mainAttrs,mainElement,match,noContent,regEx;noContent=/^<([^\/].*?)>$/;regEx=/(?:<)(.*?)( .*?)?(?:>)/;match=regEx.exec(selector);mainAll=match[0];mainElement=match[1];mainAttrs=match[2];hasAttr=typeof mainAttrs!=='undefined';hasContent=!mainAll.match(noContent);if(hasContent){contentRegEx=new RegExp(mainAll+'(.*?)(?:)$');contentMatch=contentRegEx.exec(selector);content=contentMatch[1];} +if(hasAttr){attrs=mainAttrs.split(/(?:\s)?(.*?)=(?:"|')(.*?)(?:"|')/).filter(Boolean);attrObj={};i=0;while(i=0){objClassList.splice(index,1);}else{objClassList.push(list[i]);} +this.className=list[i].join(' ');}}});};ts.fn.getCss=function(property){var err;try{if(0 in this){return document.defaultView.getComputedStyle(this[0],null).getPropertyValue(property);}else{return null;}}catch(error){err=error;return null;}};ts.fn.remove=function(){return this.each(function(){this.parentNode.removeChild(this);});};ts.fn.children=function(){var list;list=[];this.each(function(i,el){list.push.apply(list,el.children);});return ts(list);};ts.fn.find=function(selector){var list;if(typeof selector!=='string'){return null;} +list=[];this.each(function(i,el){list.push.apply(list,el.querySelectorAll(selector));});if(list.length){return ts(list);}else{return null;}};ts.fn.parent=function(){if(0 in this){return ts(this[0].parentNode);}else{return null;}};ts.fn.parents=function(selector){var selector;var selector;var parents,that;that=this;selector=selector||null;parents=[];if(selector!==null){selector=ts(selector);} +while(that){that=ts(that).parent()[0];if(!that){break;} +if(selector===null||selector!==null&&Array.prototype.indexOf.call(selector,that)!==-1){parents.push(that);}} +return ts(parents);};ts.fn.closest=function(selector){var selector;var that;that=this;selector=ts(selector);while(true){that=ts(that).parent()[0];if(!that){return null;} +if(Array.prototype.indexOf.call(selector,that)!==-1){return ts(that);}}};ts.fn.contains=function(wants){var isTrue,selector;selector=ts(wants);isTrue=false;this.each(function(i,el){var children,si;children=el.childNodes;si=0;while(siheightHalf){return 3;}else if(position.left>widthHalf&&position.top>heightHalf){return 4;}else if(position.left>widthHalf&&position.top0||typeof this==='undefined'){return;} +modal=this;tsModal=ts(this);tsDimmer=tsModal.closest('.ts.modals.dimmer');closeBtn=tsModal.find('.close.icon');if(tsDimmer===null){return;} +if(option==='show'){ts('body').attr('data-modal-lock','true');tsDimmer.addClass('active').addClass('opening').one(animationEnd,function(){return ts(this).removeClass('opening');}).on('click',function(e){if(ts(modal).hasClass('closable')){if(e.target===this){return closeModal(modal);}}});if(closeBtn!==null){closeBtn.on('click',function(){return closeModal(modal);});} +bindModalButtons(modal,'.positive, .approve, .ok','.negative, .deny, .cancel',function(){return true;},function(){return true;},false);return tsModal.attr('open','open').addClass('opening').one(animationEnd,function(){return tsModal.removeClass('opening');});}else if(option==='hide'){return closeModal(this);}else if(typeof option==='object'){approve=option.approve||'.positive, .approve, .ok';deny=option.deny||'.negative, .deny, .cancel';onDeny=option.onDeny||function(){return true;};onApprove=option.onApprove||function(){return true;};modal=this;return bindModalButtons(modal,approve,deny,onApprove,onDeny,true);}});};ts.fn.sidebar=function(options,selector,eventName){var closable,closeVisibleSidebars,dimPage,exclusive,pusher,scrollLock;dimPage=(options!=null?options.dimPage:void 0)||false;exclusive=(options!=null?options.exclusive:void 0)||false;scrollLock=(options!=null?options.scrollLock:void 0)||false;closable=(options!=null?options.closable:void 0)||true;pusher=document.querySelector('.pusher');closeVisibleSidebars=function(){ts('.ts.sidebar.visible:not(.static)').addClass('animating').removeClass('visible').one(animationEnd,function(){return ts(this).removeClass('animating');});return ts('.pusher').removeClass('dimmed').removeAttr('data-pusher-lock');};if(pusher.getAttribute('data-closable-bind')!=='true'){pusher.addEventListener('click',function(e){if(pusher.getAttribute('data-sidebar-closing')!=='true'){return closeVisibleSidebars();}});} +pusher.setAttribute('data-closable-bind',true);return this.each(function(){var that;if(options==='toggle'||options==='hide'||options==='show'){ts(this).addClass('animating');pusher.setAttribute('data-sidebar-closing','true');setTimeout(function(){return pusher.removeAttribute('data-sidebar-closing');},300);if(this.getAttribute('data-dim-page')===null){this.setAttribute('data-dim-page',dimPage);} +if(this.getAttribute('data-scroll-lock')===null){this.setAttribute('data-scroll-lock',scrollLock);} +if(!ts(this).hasClass('visible')&&options==='hide'){ts(this).removeClass('animating');} +if((ts(this).hasClass('visible')&&options==='toggle')||options==='hide'){ts('.pusher').removeClass('dimmed').removeAttr('data-pusher-lock');return ts(this).removeClass('visible').one(animationEnd,function(){return ts(this).removeClass('animating');});}else{if(this.getAttribute('data-exclusive')==='true'){closeVisibleSidebars();} +if(this.getAttribute('data-dim-page')==='true'){ts('.pusher').addClass('dimmed');} +if(this.getAttribute('data-scroll-lock')==='true'){ts('.pusher').attr('data-pusher-lock','true');} +return ts(this).addClass('visible').removeClass('animating');}}else if(options==='attach events'){that=this;switch(eventName){case'show':return ts(selector).attr('data-sidebar-trigger','true').on('click',function(){return ts(that).sidebar('show');});case'hide':return ts(selector).attr('data-sidebar-trigger','true').on('click',function(){return ts(that).sidebar('hide');});case'toggle':return ts(selector).attr('data-sidebar-trigger','true').on('click',function(){return ts(that).sidebar('toggle');});}}else if(typeof options==='object'){this.setAttribute('data-closable',closable);this.setAttribute('data-scroll-lock',scrollLock);this.setAttribute('data-exclusive',exclusive);return this.setAttribute('data-dim-page',dimPage);}});};ts.fn.tab=function(option){return this.each(function(){var onSwitch;onSwitch=(option!=null?option.onSwitch:void 0)||function(){};return ts(this).on('click',function(){var tabGroup,tabName;if(ts(this).hasClass('active')){return;} +tabName=ts(this).attr('data-tab');if(tabName===null){return;} +tabGroup=ts(this).attr('data-tab-group');onSwitch(tabName,tabGroup);if(tabGroup===null){ts('[data-tab]:not(.tab):not([data-tab-group])').removeClass('active');ts('[data-tab]:not([data-tab-group])').removeClass('active');ts(`.tab[data-tab='${tabName}']:not([data-tab-group])`).addClass('active');}else{ts(`[data-tab-group='${tabGroup}']:not(.tab)`).removeClass('active');ts(`.tab[data-tab-group='${tabGroup}']`).removeClass('active');ts(`.tab[data-tab='${tabName}'][data-tab-group='${tabGroup}']`).addClass('active');} +return ts(this).addClass('active');});});};ts.fn.popup=function(){return this.each(function(){var android,iOS,userAgent,winPhone;userAgent=navigator.userAgent||navigator.vendor||window.opera;winPhone=new RegExp("windows phone","i");android=new RegExp("android","i");iOS=new RegExp("iPad|iPhone|iPod","i");if(winPhone.test(userAgent)||android.test(userAgent)||(iOS.test(userAgent)&&!window.MSStream)){return ts(this).addClass('untooltipped');}});};ts.fn.slider=function(option){var counter,modify,outerCounter;outerCounter=option!=null?option.outerCounter:void 0;counter=option!=null?option.counter:void 0;modify=function(sliderEl,inputEl,counter,outerCounter){var counterEl,value;value=(inputEl.value-inputEl.getAttribute('min'))/(inputEl.getAttribute('max'-inputEl.getAttribute('min')));if(value===Number.POSITIVE_INFINITY){value=inputEl.value / 100;} +if(counter!=null){counterEl=ts(sliderEl).find(counter);if(counterEl!=null){counterEl[0].innerText=inputEl.value;}} +if(outerCounter!=null){ts(outerCounter).innerText=inputEl.value;} +return ts(inputEl).css('background-image',`-webkit-gradient(linear,left top,right top,color-stop(${value},${slider_progressColor}),color-stop(${value},${slider_trackColor}))`);};return this.each(function(){var inputEl,sliderEl;sliderEl=this;inputEl=ts(this).find('input[type="range"]');modify(this,inputEl[0],counter,outerCounter);return inputEl.on('input',function(){return modify(sliderEl,this,counter,outerCounter);});});};ts.fn.editable=function(option){var autoClose,autoReplace,inputWrapper,onEdit,onEdited;autoReplace=(option!=null?option.autoReplace:void 0)||true;onEdit=(option!=null?option.onEdit:void 0)||function(){};onEdited=(option!=null?option.onEdited:void 0)||function(){};autoClose=(option!=null?option.autoClose:void 0)||true;inputWrapper=this;if(autoClose){ts(document).on('click',function(event){if(ts(event.target).closest('.ts.input')===null){return inputWrapper.each(function(){var contenteditable,input,text;input=ts(this).find('input');contenteditable=ts(this).find('[contenteditable]');text=ts(this).find('.text')[0];if(autoReplace){if(input!=null){text.innerText=input[0].value;}else if(contenteditable!=null){text.innerText=contenteditable[0].value;}} +onEdited(this);return ts(this).removeClass('editing');});}});} +return this.each(function(){var contenteditable,input;input=ts(this).find('input');contenteditable=ts(this).find('[contenteditable]');return ts(this).on('click',function(){ts(this).addClass('editing');onEdit(this);if(input!=null){return input[0].focus();}else if(contenteditable!=null){return contenteditable[0].focus();}});});};ts.fn.message=function(){return this.each(function(){return ts(this).find('i.close').on('click',function(){return ts(this).closest('.ts.message').addClass('hidden');});});};ts.fn.snackbar=function(option){var action,actionEmphasis,content,hoverStay,interval,onAction,onClose;content=(option!=null?option.content:void 0)||null;action=(option!=null?option.action:void 0)||null;actionEmphasis=(option!=null?option.actionEmphasis:void 0)||null;onClose=(option!=null?option.onClose:void 0)||function(){};onAction=(option!=null?option.onAction:void 0)||function(){};hoverStay=(option!=null?option.hoverStay:void 0)||false;interval=3500;if(content===null){return;} +return this.each(function(){var ActionEl,close,contentEl,snackbar;snackbar=this;contentEl=ts(snackbar).find('.content');ActionEl=ts(snackbar).find('.action');ts(snackbar).removeClass('active animating').addClass('active animating').one(animationEnd,function(){return ts(this).removeClass('animating');}).attr('data-mouseon','false');contentEl[0].innerText=content;if(ActionEl!=null){ActionEl[0].innerText=action;} +if((actionEmphasis!=null)&&(ActionEl!=null)){ActionEl.removeClass('primary info warning negative positive').addClass(actionEmphasis);} +close=function(){ts(snackbar).removeClass('active').addClass('animating').one(animationEnd,function(){ts(this).removeClass('animating');return onClose(snackbar,content,action);});return clearTimeout(snackbar.snackbarTimer);};if(ActionEl!=null){ActionEl.off('click');ActionEl.on('click',function(){close();return onAction(snackbar,content,action);});} +if(hoverStay){ts(snackbar).on('mouseenter',function(){return ts(this).attr('data-mouseon','true');});ts(snackbar).on('mouseleave',function(){return ts(this).attr('data-mouseon','false');});} +clearTimeout(snackbar.snackbarTimer);return snackbar.snackbarTimer=setTimeout(function(){var hoverChecker;if(hoverStay){return hoverChecker=setInterval(function(){if(ts(snackbar).attr('data-mouseon')==='false'){close();return clearInterval(hoverChecker);}},600);}else{return close();}},interval);});};ts.fn.contextmenu=function(option){var menu;menu=(option!=null?option.menu:void 0)||null;ts(document).on('click',function(event){return ts('.ts.contextmenu.visible').removeClass('visible').addClass('hidden animating').one(animationEnd,function(){return ts(this).removeClass('visible animating downward upward rightward leftward');});});return this.each(function(){return ts(this).on('contextmenu',function(e){var h,r,w;event.preventDefault();ts(menu).addClass('visible');r=ts(menu)[0].getBoundingClientRect();ts(menu).removeClass('visible');w=window.innerWidth / 2;h=window.innerHeight / 2;ts(menu).removeClass('downward upward rightward leftward');if(e.clientXh){ts(menu).addClass('upward rightward').css('left',e.clientX+'px').css('top',e.clientY-r.height+'px');}else if(e.clientX>w&&e.clientY>h){ts(menu).addClass('upward leftward').css('left',e.clientX-r.width+'px').css('top',e.clientY-r.height+'px');}else if(e.clientX>w&&e.clientYOpen source online Markdown editor.", + tocTitle : "選單", + toolbar : { + undo : "復原(Ctrl+Z)", + redo : "重做(Ctrl+Y)", + bold : "粗體", + del : "刪除線", + italic : "斜體", + quote : "引用", + ucwords : "將所選的每個單字首字母轉成大寫", + uppercase : "將所選文字轉成大寫", + lowercase : "將所選文字轉成小寫", + h1 : "標題1", + h2 : "標題2", + h3 : "標題3", + h4 : "標題4", + h5 : "標題5", + h6 : "標題6", + "list-ul" : "無序清單", + "list-ol" : "有序清單", + hr : "分隔線", + link : "連結", + "reference-link" : "引用連結", + image : "圖片", + code : "行內代碼", + "preformatted-text" : "預格式文本 / 代碼塊(縮進風格)", + "code-block" : "代碼塊(多語言風格)", + table : "添加表格", + datetime : "日期時間", + emoji : "Emoji 表情", + "html-entities" : "HTML 實體字符", + pagebreak : "插入分頁符", + watch : "關閉實時預覽", + unwatch : "開啟實時預覽", + preview : "預覽(按 Shift + ESC 退出)", + fullscreen : "全螢幕(按 ESC 退出)", + clear : "清空", + search : "搜尋", + help : "幫助", + info : "關於" + exports.title + }, + buttons : { + enter : "確定", + cancel : "取消", + close : "關閉" + }, + dialog : { + link : { + title : "添加連結", + url : "連結位址", + urlTitle : "連結標題", + urlEmpty : "錯誤:請填寫連結位址。" + }, + referenceLink : { + title : "添加引用連結", + name : "引用名稱", + url : "連結位址", + urlId : "連結ID", + urlTitle : "連結標題", + nameEmpty: "錯誤:引用連結的名稱不能為空。", + idEmpty : "錯誤:請填寫引用連結的ID。", + urlEmpty : "錯誤:請填寫引用連結的URL地址。" + }, + image : { + title : "添加圖片", + url : "圖片位址", + link : "圖片連結", + alt : "圖片描述", + uploadButton : "本地上傳", + imageURLEmpty : "錯誤:圖片地址不能為空。", + uploadFileEmpty : "錯誤:上傳的圖片不能為空!", + formatNotAllowed : "錯誤:只允許上傳圖片文件,允許上傳的圖片文件格式有:" + }, + preformattedText : { + title : "添加預格式文本或代碼塊", + emptyAlert : "錯誤:請填寫預格式文本或代碼的內容。" + }, + codeBlock : { + title : "添加代碼塊", + selectLabel : "代碼語言:", + selectDefaultText : "請語言代碼語言", + otherLanguage : "其他語言", + unselectedLanguageAlert : "錯誤:請選擇代碼所屬的語言類型。", + codeEmptyAlert : "錯誤:請填寫代碼內容。" + }, + htmlEntities : { + title : "HTML實體字符" + }, + help : { + title : "幫助" + } + } + }; + + exports.defaults.lang = lang; + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); \ No newline at end of file diff --git a/include/js/like.js b/include/js/like.js new file mode 100644 index 0000000..6d9552a --- /dev/null +++ b/include/js/like.js @@ -0,0 +1,46 @@ +$('#content').on('click', 'button.like.button', function(e){ + var el = e.currentTarget; + var id = el.dataset.id; + axios.request({ + method: "GET", + url: "./ajax/like.php?pid=" + id, + responseType: "json", + }).then(function (res) { + var data = res.data; + if (data.status == true) { + $(`button.like.button[data-id="${data.id}"]`).html( + ' ' + data.likes + ); + } else if (data.status == false) { + $(`button.like.button[data-id="${data.id}"]`).html( + ' ' + data.likes + ); + } + }).catch(function (error) { + if (error.response) { + let data = error.response.data; + if (data.status == 'nologin') { + $(`button.like.button[data-id="${data.id}"]`).html( + ' ' + data.likes + ); + swal({ + type: 'warning', + title: '請先登入!', + text: '登入以按讚或發表留言。', + showCancelButton: true, + confirmButtonText: '登入', + cancelButtonText: '取消', + }).then((result) => { + if (result.value) { // confirm + location.href = 'login.php'; + } + }); + } + } else { + $(`button.like.button[data-id="${id}"]`).html( + ' ' + "--" + ); + console.error(`An error occurred when get likes of pid ${id}, status ${error.response.status}`); + } + }); +}); \ No newline at end of file diff --git a/include/js/markdown.js b/include/js/markdown.js new file mode 100644 index 0000000..873c03d --- /dev/null +++ b/include/js/markdown.js @@ -0,0 +1,40 @@ +editormd.urls = { + atLinkBase : "user.php?username=" +}; + +function postProcess(...callbacks) { + tableStyling(); + linkSanitize(); + + callbacks.forEach(func => { + func(); + }); + + function linkSanitize() { + $('.markdown-body a').each((_i, e) => { + href = (e.getAttribute('href')) ? _.unescape(e.getAttribute('href').toLowerCase()) : ""; + if (href.indexOf('javascript:') != -1) { + e.setAttribute('href', '#'); + } + }); + } + + function tableStyling() { + $('table').each((_i,e) => { + $(e).addClass("ts celled table").css('display', 'table').wrap('
        '); + }); + } +} + +function parseMarkdown(id, markdown, options) { + let defaultOptions = { + htmlDecode : "script,iframe|on*", + toc: true, + emoji: true, + taskList: true, + tex: true, + flowChart: true, + sequenceDiagram: true + } + return editormd.markdownToHTML(id, $.extend(true, defaultOptions, options, { markdown: markdown })); +} \ No newline at end of file diff --git a/include/js/notification.js b/include/js/notification.js new file mode 100644 index 0000000..a07458d --- /dev/null +++ b/include/js/notification.js @@ -0,0 +1,126 @@ +var notifications = { + toFetch: true, + unreadCount: 0, + feeds: [] +}; + +$('#menu .notification.icon.item').on('click', function (e) { + e.preventDefault(); + let el = e.currentTarget; + + let $wrapper = $('#notification-wrapper'); + let $container = $('.notification.container'); + + if ($container.hasClass('active')) { + // dismiss the notification window + $('.notification.click.handler').remove(); + } else { + // render the notification window + let handler = document.createElement('div') + handler.className = "notification click handler"; + $wrapper.after(handler); + handler.addEventListener('click', function (e) { + el.click(); + }); + setNotificationCounter(0); // remove counter + if (notifications.toFetch){ + fetchNotification(); + } + } + + el.classList.toggle('active'); + $container.toggleClass('active'); +}); + +function fetchNotificationCount() { + axios.request({ + method: 'GET', + url: "./ajax/notification.php?count", + responseType: 'json' + }).then(function (res) { + let count = res.data['unread_count']; + setNotificationCounter(count); + if (count != notifications.unreadCount) { + // if count changes, fetching notifications while next click + notifications.toFetch = true; + notifications.unreadCount = count; + } + }).catch(function (_error) { + console.error("Error occurred while fetching notification count."); + }); +} + +window.addEventListener('load', function () { + fetchNotificationCount(); +}); +var notificationFetchTimer = setInterval(fetchNotificationCount, 1 * 5 * 1000); // fetch notification count every 1 minute + +function fetchNotification() { + axios.request({ + method: 'GET', + url: './ajax/notification.php?fetch', + responseType: 'json' + }).then(function (res) { + parseNotification(res.data); + notifications.toFetch = false; + }).catch(function (error) { + console.log("Error occurred while fetching notification count."); + }); +} + +function parseNotification(data) { + const feedTemplate = `
        {{ time }}
        {{ message }}
        `; + let $feed = $('.ts.feed'); + + $feed.html(""); // container clean up + + for (f of data.feeds) { + let message = parseMessage(f.message, f.url); + let node = feedTemplate.replace("{{ time }}", f.time).replace("{{ message }}", message); + $node = $(node).appendTo($feed); + + if (f.read == 0) { + $node.addClass('unread'); + } + } + + notifications.feeds = data.feeds; // cache data + + function parseMessage(message, url) { + let regex = { + "username": /\{([^\{\}]+)\}@(\w+)/g, + "url": /\[([^\[\[]*)\]/g + }; + + return message.replace(regex.username, function (_match, name, id, _offset, _string) { + return `${name}`; + }).replace(regex.url, function (_match, title, _offset, _string) { + return `${title}`; + }); + } +} + +function setNotificationCounter(count) { + let $notify = $('#menu .notification.icon.item'); + let $icon = $notify.children('i.icon'); + let $counter = $notify.children('span.counter'); + + if (count == 0) { + if ($counter.length) { + $counter.remove(); + } + $icon.toggleClass('outline', true); // set icon style + } else { + if ($counter.length) { + $counter.text(count); + } else { + let counter = document.createElement('span'); + counter.className = "counter"; + counter.textContent = count; + $notify.append(counter); + } + $icon.toggleClass('outline', false); // set icon style + } + + return count; +} \ No newline at end of file diff --git a/include/js/post.js b/include/js/post.js new file mode 100644 index 0000000..8e50ba7 --- /dev/null +++ b/include/js/post.js @@ -0,0 +1,86 @@ +const cdnjs = "https://cdnjs.cloudflare.com/ajax/libs"; + +// Load Libraries +const libraries = [ + cdnjs + "/marked/0.5.1/marked.min.js", + cdnjs + "/prettify/r298/prettify.min.js", + cdnjs + "/raphael/2.2.7/raphael.min.js", + cdnjs + "/underscore.js/1.9.1/underscore-min.js", + cdnjs + "/flowchart/1.11.3/flowchart.min.js", + "https://pandao.github.io/editor.md/lib/jquery.flowchart.min.js", + cdnjs + "/js-sequence-diagrams/1.0.6/sequence-diagram-min.js" +]; + +loadJS(libraries).then(function () { + editormd.$marked = marked; + editormd.loadFiles.js.push(...libraries.map(url => url.slice(0, -3))); // remove ".js" + parsePost(); + fetchComments(); + postProcess(sanitizeStyleTag()); + + function sanitizeStyleTag() { // prevent the style tag in post from destorying the style of page + return function() { + var parser = new cssjs(); + let stylesheets = document.querySelectorAll('#post style'); + for (let style of stylesheets) { + let ruleSource = style.innerHTML; + let cssObject = parser.parseCSS(ruleSource); + for (let rule of cssObject) { + let valid = false; + let validPrefix = ["#post ", "#post.markdown-body ", "#post.editormd-html-preview "]; + validPrefix.forEach((e, _i) => { + valid = valid || rule.selector.startsWith(e); + }); + + if (!rule.selector.startsWith('@')) { // '@keyframe' & '@import' + if (valid) { + // do nothing + } else if (rule.selector.startsWith('.markdown-body ') || rule.selector.startsWith(".editormd-html-preview")) { + rule.selector = "#post" + rule.selector; + } else { + rule.selector = "#post " + rule.selector; + } + } + } + style.innerHTML = parser.getCSSForEditor(cssObject); + } + } + } +}); + +function parsePost() { + var postContent = document.querySelector('#post .markdown').textContent; + + if (postContent.search(/.{0}\[TOC\]\n/) != -1) { // if TOC is used in post + $('#sidebar .ts.fluid.input').after(`
        目錄
        `); + } + + parseMarkdown('post', postContent, { + tocDropdown: false, + tocContainer: '#toc' + }).children('.markdown').hide(); + $('#toc').removeClass('loading'); +} + +// Delete post confirm message +$('.action.column .delete').on('click', function(e) { + e.preventDefault(); + var el = this; + var next = el.getAttribute('href'); + swal({ + type: 'question', + title: '確定要刪除嗎?', + showCancelButton: true, + confirmButtonText: '確定', + cancelButtonText: '取消', + }).then((result) => { + if (result.value) { // confirm + axios.request({ + method: "GET", + url: next + }).then(function (res) { + location.href = res.headers["axios-location"]; + }); + } + }); +}); \ No newline at end of file diff --git a/include/js/security.js b/include/js/security.js new file mode 100644 index 0000000..5062a5c --- /dev/null +++ b/include/js/security.js @@ -0,0 +1,19 @@ +axios.defaults.withCredentials = true; + +axios.interceptors.request.use(function (config) { + var crypto = window.crypto || window.msCrypto; + let csrfToken = btoa(String(crypto.getRandomValues(new Uint32Array(1))[0])); + document.cookie = `${axios.defaults.xsrfCookieName}=${csrfToken}`; + return config; +}, function (error) { + return Promise.reject(error); +}); + +$("#logout").on("click", function (e) { + e.preventDefault(); + axios.get("login.php?logout").then(function (res) { + location.href = res.headers["axios-location"]; + }).catch(function (error) { + console.log(error); + }); +}); \ No newline at end of file diff --git a/include/notification.php b/include/notification.php new file mode 100644 index 0000000..4c62efe --- /dev/null +++ b/include/notification.php @@ -0,0 +1,21 @@ +query("INSERT INTO `notification` (`username`, `message`, `url`, `type`, `time`) VALUES ('%s', '%s', '%s', '%s', '%s')", array($username, $message, $url, $type, $time)); +} + +function parse_user_tag($markdown) { + $regex = array( + "code_block" => "/(`{1,3}[^`]*`{1,3})/", + "email" => "/[^@\s]*@[^@\s]*\.[^@\s]*/", + "username" => "/@(\w+)/" + ); + + $tmp = preg_replace($regex["code_block"], " ", $markdown); + $tmp = preg_replace($regex["email"], " ", $tmp); + + preg_match_all($regex["username"], $tmp, $username_list); + + return array_unique($username_list[1]); +} \ No newline at end of file diff --git a/include/security.php b/include/security.php new file mode 100644 index 0000000..d8c7d2f --- /dev/null +++ b/include/security.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/include/user.php b/include/user.php new file mode 100644 index 0000000..827cb8c --- /dev/null +++ b/include/user.php @@ -0,0 +1,65 @@ + 0){ + $this->valid = true; + + $data = $query['row']; + $this->username = $data["username"]; + $this->name = $data['name']; + $this->level = $data['level']; + $this->muted = ($data['muted'] == 1 ? true : false); + $this->email = $data['email']; + } else { + throw new NoUserException($username); + } + + if ($this->username === @$_SESSION["cavern_username"]) { + $this->islogin = true; + } + } else { + // even though the user hasn't logged in, he is still a valid user + $this->username = ""; + $this->valid = true; + } + } + + public function __get($name) { + return $this->$name; + } +} + +function validate_user() { + if (isset($_SESSION['cavern_username'])) { + $username = $_SESSION['cavern_username']; + } else { + $username = ""; + } + + try { + $user = new User($username); + } catch (NoUserException $e) {} + + if (!$user->valid) { + session_destroy(); + } + return $user; +} + +?> \ No newline at end of file diff --git a/include/view.php b/include/view.php new file mode 100644 index 0000000..c918fd6 --- /dev/null +++ b/include/view.php @@ -0,0 +1,100 @@ + +Copyright (C) 2012-2017 太陽部落格站長 Secret + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, version 3. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. +*/ + +class View { + private $master_content; + private $nav_content; + private $sidebar_content; + private $message = array(); + private $script = array(); + private $title; + private $part; + + public function __construct($master,$nav,$sidebar,$title,$part) { + $this->load($master,$nav,$sidebar); + $this->title = $title; + $this->part = $part; + ob_start(); + } + + private function load($master,$nav,$sidebar) { + ob_start(); + include($master); + $this->master_content = ob_get_contents(); + ob_end_clean(); + + ob_start(); + include($nav); + $this->nav_content = ob_get_contents(); + ob_end_clean(); + + if ($sidebar!='') { + ob_start(); + include($sidebar); + $this->sidebar_content = ob_get_contents(); + ob_end_clean(); + } + } + + public function add_script($src) { + $this->script[] = ""; + } + + public function add_script_source($source) { + $this->script[] = ""; + } + + public function show_message($class, $msg) { + $this->message[] = "

        $msg

        "; + } + + public function render() { + $content = ob_get_contents(); + ob_end_clean(); + + echo strtr($this->master_content, array( + '{title}' => $this->title, + '{part}' => $this->part, + '{script}' => join(PHP_EOL, $this->script), + '{nav}' => $this->nav_content, + '{sidebar}' => $this->sidebar_content, + '{message}' => join(PHP_EOL, $this->message), + '{content}' => $content + )); + @ob_flush(); + flush(); + } +}; \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..d1b1baf --- /dev/null +++ b/index.php @@ -0,0 +1,132 @@ +valid) { + http_response_code(403); + header("Location: index.php?err=account"); + exit; +} + +$all_posts_count = cavern_query_result("SELECT COUNT(*) AS `count` FROM `post`")['row']['count']; + +if (isset($_GET['page']) && trim($_GET['page']) != "") { + $limit_start = abs((intval($_GET['page']) - 1) * $blog['limit']); + if ($limit_start > $all_posts_count) { // we don't have that much posts + header('Location: index.php'); + exit; + } +} else { + $limit_start = 0; +} + +$post_list = article_list(cavern_query_result( + "SELECT `post`.*, `user`.name FROM `post` INNER JOIN `user` ON `post`.username = `user`.username ORDER BY `time` DESC LIMIT %d,%d", + array($limit_start, $blog['limit'])) +); + +if ($user->islogin) { + $view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "首頁"); + $view->add_script_source("ts('.ts.dropdown:not(.basic)').dropdown();"); +} else { + $view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], "首頁"); +} + +$view->add_script("https://unpkg.com/load-js@1.2.0"); +$view->add_script("./include/js/lib/editormd.js"); +$view->add_script("./include/js/security.js"); +$view->add_script('./include/js/markdown.js'); +$view->add_script('./include/js/cards.js'); +$view->add_script('./include/js/like.js'); + +// ok message +if (isset($_GET['ok'])) { + switch ($_GET['ok']) { + case 'login': + if ($user->islogin) { + // only show welcome message if user is logged in + $greeting = cavern_greeting(); + $view->show_message('inverted positive', "{$greeting}!我的朋友!"); + } + break; + case 'reg': + $view->show_message('inverted primary', '註冊成功'); + break; + case 'logout': + if (!$user->islogin) { + // only show message if user is logged out + $view->show_message('inverted info', '已登出'); + } + break; + } +} + +// error message +if (isset($_GET['err'])) { + switch ($_GET['err']) { + case 'account': + $view->show_message('inverted negative', '帳號不存在'); + break; + case 'login': + $view->show_message('inverted negative', '帳號或密碼錯誤'); + break; + case 'permission': + $view->show_message('warning', '帳號權限不足'); + break; + case 'post': + $view->show_message('negative', '找不到文章'); + break; + case 'nologin': + $view->show_message('warning', '請先登入'); + break; + } +} + +if (sizeOf($post_list) > 0) { ?> +
        載入中
        +
        + $article) { +?> + +
        +
        +
        +
        + + + Read + +
        +
        +
        title ?>
        +
        + name ?> + time)) ?> +
        +
        +
        +content, 5) ?> +
        +
        +
        +
        + comments_count ?> 則留言 +
        +
        + + +
        + show_message('inverted info', '沒有文章,趕快去新增一個吧!'); +} + +$view->render(); +?> diff --git a/login.php b/login.php new file mode 100644 index 0000000..8bd2054 --- /dev/null +++ b/login.php @@ -0,0 +1,118 @@ + 'csrf')); + } + } else if (isset($_GET['next']) && $_GET['next'] == "admin") { + header("Location: ./admin/"); + } else { + header('Location: index.php'); + } + exit; +} + +if ((isset($_POST['username'])) && (isset($_POST['password'])) && ($_POST['username']!='') && ($_POST['password']!='')) { + if (cavern_login($_POST['username'], $_POST['password']) == 1) { + if (isset($_POST['next']) && trim($_POST['next']) == "admin") { + header('Location: ./admin/'); + } else if ((isset($_POST['next']) && filter_var($_POST['next'], FILTER_VALIDATE_URL)) || isset($_SERVER['HTTP_REFERER'])) { + // redirect to previous page before login + $next = (isset($_POST['next']) ? $_POST['next'] : $_SERVER['HTTP_REFERER']); // users login directly from navbar + $url_data = parse_url($next); + + $len = strlen("index.php"); + if (mb_substr($url_data['path'], -$len) === "index.php") { + // the user was viewing the index page, so we just redirect him to index page + header('Location: index.php?ok=login'); + } else { + if (!isset($url_data['query'])) { + $url_data['query'] = "ok=login"; + } else if (!strpos($url_data['query'], "ok=login")) { + // for those already have url queries, such as 'post.php?pid=1' + $url_data['query'] .= "&ok=login"; + } + + $url = "{$url_data['path']}?{$url_data['query']}"; + header("Location: $url"); + } + } else { + // previous page doesn't exist, so we just redirect to default page + header('Location: index.php?ok=login'); + } + } else { + header('Location: index.php?err=login'); + } + exit; +} else { + $admin = (isset($_GET['next']) && trim($_GET['next']) == "admin"); +?> + + + + + + + 登入 | <?php echo $blog['name']; ?> + + + > +
        +

        + +
        +

        +
        segment"> +
        +
        + + +
        +
        + + +
        + "> +
        + + 註冊 +
        +
        +
        +
        + + + \ No newline at end of file diff --git a/notification.php b/notification.php new file mode 100644 index 0000000..21e1621 --- /dev/null +++ b/notification.php @@ -0,0 +1,57 @@ +add_script_source("ts('.ts.dropdown:not(.basic)').dropdown();"); + $view->add_script("./include/js/security.js"); + + $notice_list = cavern_query_result("SELECT * FROM `notification` WHERE `username` = '%s' ORDER BY `time` DESC", array($_SESSION['cavern_username'])); + + if ($notice_list['num_rows'] > 0) { + $regex = array( + "username" => "/\{([^\{\}]+)\}@(\w+)/", + "url" => "/\[([^\[\[]*)\]/" + ); +?> +
        通知 #僅顯示最近 100 則通知
        +
        + + + + + + + + +$1', $message); + $message = preg_replace($regex["url"], "$1", $message); +?> + + + + +fetch_assoc()); ?> + +
        內容日期
        +
        + show_message('inverted info', '目前沒有通知。'); + } + + $view->render(); +} else { + http_response_code(204); + header('Location: index.php?err=nologin'); + exit; +} +?> \ No newline at end of file diff --git a/post.php b/post.php new file mode 100644 index 0000000..ad38acb --- /dev/null +++ b/post.php @@ -0,0 +1,460 @@ +valid) { + http_response_code(403); + header("Location: ../index.php?err=account"); + exit; +} + +if ($user->islogin && isset($_POST['pid']) && isset($_POST['title']) && isset($_POST['content'])) { + if ($user->level < 1 || $user->muted == 1) { + http_response_code(403); + header('axios-location: post.php?err=level'); + exit; + } + + if (!validate_csrf()) { + http_response_code(403); + header('axios-location: post.php?err=level'); + } + + if ($_POST['pid'] == "-1") { + // new post + if (trim($_POST['content']) == "") { + http_response_code(400); + header('axios-location: post.php?err=empty'); + exit; + } + + if (trim($_POST['title']) == "") { + $_POST['title'] = "(無標題)"; + } + + $current = date('Y-m-d H:i:s'); + $SQL->query("INSERT INTO `post` (`title`, `content`, `time`, `username`) VALUES ('%s', '%s', '%s', '%s')", array(htmlspecialchars($_POST['title']), htmlspecialchars($_POST['content']), $current, $user->username)); + $pid = $SQL->insert_id(); + + // notify tagged user + // the user who tag himself is unnecessary to notify + $username_list = parse_user_tag($_POST['content']); + foreach ($username_list as $key => $id) { + if ($id == $user->username) continue; + cavern_notify_user($id, "{{$user->name}}@{$user->username} 在 [{$_POST['title']}] 中提到了你", "post.php?pid=$pid"); + } + + http_response_code(201); // 201 Created + header('axios-location: post.php?pid='.$pid); + exit; + } else { + // edit old post + $pid = abs($_POST['pid']); + + try { + $post = new Article($pid); + } catch (NoPostException $e) { + // post not found + http_response_code(404); + header("axios-location: index.php?err=post"); + exit; + } + + if ($post->author !== $user->username && $user->level < 8) { + http_response_code(403); + header('axios-location: post.php?err=edit'); + exit; + } + + if (trim($_POST['content']) == "") { + http_response_code(400); + header('axios-location: post.php?err=empty'); + exit; + } + + if (trim($_POST['title']) == "") { + $_POST['title'] = "(無標題)"; + } + + $post->modify($user, "title", htmlspecialchars($_POST['title'])); + $post->modify($user, "content", htmlspecialchars($_POST['content'])); + + $post->save(); + header('axios-location: post.php?pid='.$_POST['pid']); + exit; + } +} + +if ($user->islogin && isset($_GET['del']) && trim($_GET['del']) != '') { + if (!validate_csrf()) { + http_response_code(403); + header('axios-location: post.php?err=level'); + exit; + } + + try { + $post = new Article(intval($_GET['del'])); + } catch (NoPostException $e) { + http_response_code(404); + header('axios-location: index.php?err=post'); + exit; + } + + if ($post->author !== $user->username && $user->level < 8) { + http_response_code(403); + header('axios-location: post.php?err=del'); + exit; + } else { + $SQL->query("DELETE FROM `post` WHERE `pid`='%d' AND `username` = '%s'", array($_GET['del'], $_SESSION['cavern_username'])); + http_response_code(204); + header('axios-location: post.php?ok=del'); + exit; + } +} else if (!$user->islogin && isset($_GET['del'])) { + http_response_code(204); + header('axios-location: index.php?err=nologin'); + exit; +} + +// View +if (isset($_GET['pid'])) { + $pid = abs($_GET['pid']); + + try { + $post = new Article($pid); + } catch (NoPostException $e) { + http_response_code(404); + header('Location: index.php?err=post'); + exit; + } + + if ($user->islogin) { + $view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], $post->title); + $view->add_script_source("ts('.ts.dropdown:not(.basic)').dropdown();"); + $owner_view = ($post->author === $user->username); + } else { + $view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], $post->title); + $owner_view = FALSE; + } + + $view->add_script("https://unpkg.com/load-js@1.2.0"); + $view->add_script("./include/js/lib/editormd.js"); + $view->add_script("./include/js/lib/css.min.js"); + $view->add_script("./include/js/security.js"); + $view->add_script("./include/js/markdown.js"); + $view->add_script("./include/js/comment.js"); + $view->add_script("./include/js/post.js"); + $view->add_script("./include/js/like.js"); + $view->add_script_source("ts('.ts.tabbed.menu .item').tab();"); + + if (isset($_GET['ok'])) { + if ($_GET['ok'] == "login" && $user->islogin) { + $greeting = cavern_greeting(); + $view->show_message("inverted positive", "{$greeting}!我的朋友!"); + } + } + + ?> +
        grid"> + +
        +
        + + + + + + + + + +
        +
        +
        +
        +
        +
        content ?>
        +
        +
        + time ?> +
        +
        +
        +
        +
        +
        留言
        +
        +
        + Last fetch: --:--  +
        + +
        +
        +
        +
        +
        +
        現在還沒有留言!
        +
        +
        +
        + +
        +
        + islogin) { + if ($user->muted) { + $disabled = " disabled"; + $placeholder = "你被禁言了。"; + $button_text = "你被禁言了"; + } else { + $disabled = ""; + $placeholder = "留言,然後開戰。"; + $button_text = "留言"; + } + } else { + $disabled = " disabled"; + $placeholder = "請先登入"; + $button_text = "留言"; + } ?> +
        + +
        +
        + +
        +
        +
        +
        + render(); + +} else if (isset($_GET['new']) || isset($_GET['edit'])) { + // New or Edit + if (!$user->islogin) { + header('Location: index.php?err=nologin'); + exit; + } + + if ($user->level < 1 || $user->muted) { + header('Location: post.php?err=level'); + exit; + } + + if (isset($_GET['new'])) { + $mode = "new"; + + $pid = -1; + $title = ""; + $content = ""; + } else if (isset($_GET['edit'])) { + $mode = "edit"; + + $pid = abs($_GET['edit']); + try { + $post = new Article($pid); + } catch (NoPostException $e) { + http_response_code(404); + header('Location: index.php?err=post'); + exit; + } + + if ($post->author != $user->username) { + http_response_code(403); + header('Location: post.php?err=edit'); + exit; + } + } + + $title = $post->title; + $content = $post->content; + + $view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], ($title == "" ? "文章" : $title)); + + $view->add_script_source("ts('.ts.dropdown:not(.basic)').dropdown();"); + $view->add_script("./include/js/lib/editormd.js"); + $view->add_script("./include/js/lib/zh-tw.js"); + $view->add_script("./include/js/lib/css.min.js"); + $view->add_script("./include/js/security.js"); + $view->add_script("./include/js/edit.js"); +?> +
        +
        +
        +
        + +
        +
        +
        +
        + + + 刪除 + + 取消 +
        +
        +
        +
        + +
        + +
        + render(); + +} else { +// List all + if (!$user->islogin && (!isset($_GET['username']) || trim($_GET['username']) == "")) { + http_response_code(403); + header('Location: index.php?err=nologin'); + exit; + } + + if (isset($_GET['username']) && trim($_GET['username']) != "") { + $username = trim($_GET['username']); + + try { + $target_user = new User($username); + } catch (NoUserException $e) { + http_response_code(404); + header('Location: user.php?err=no'); + exit; + } + + $post_list = article_list(cavern_query_result( + "SELECT * FROM `post` WHERE `username`='%s' ORDER BY `time`", + array($username)) + ); + } else if ($user->islogin) { + $username = $user->username; + $post_list = article_list(cavern_query_result( + "SELECT * FROM `post` WHERE `username`='%s' ORDER BY `time`", + array($username)) + ); + } + + $owner_view = ($user->islogin && $username === $user->username); + + if ($user->islogin) { + $view = new View('theme/default.html','theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "文章"); + $view->add_script_source("$('tbody').on('click', 'a.negative.button', function(e) { + e.preventDefault(); + let el = e.currentTarget; + let href = el.getAttribute('href'); + swal({ + type: 'question', + title: '確定要刪除嗎?', + showCancelButton: true, + confirmButtonText: '確定', + cancelButtonText: '取消', + }).then((result) => { + if (result.value) { // confirm + axios.request({ + method: 'GET', + maxRedirects: 0, + url: href + }).then(function (res) { + location.href = res.headers['axios-location']; + }); + } + }); + });"); + } else { + $view = new View('theme/default.html','theme/nav/default.html', 'theme/sidebar.php', $blog['name'], "文章"); + } + + $view->add_script("./include/js/security.js"); + $view->add_script_source("ts('.ts.dropdown').dropdown();\nts('.ts.sortable.table').tablesort();"); + + if (isset($_GET['ok'])) { + if ($_GET['ok'] == "del") { + $view->show_message("inverted positive", "刪除成功"); + } else if ($_GET['ok'] == "login" && $user->islogin) { + $greeting = cavern_greeting(); + $view->show_message("inverted positive", "{$greeting}!我的朋友!"); + } + } + if (isset($_GET['err'])) { + switch ($_GET['err']) { + case 'del': + $view->show_message("inverted negative", "刪除失敗"); + break; + case 'edit': + $view->show_message("inverted negative", "編輯失敗"); + break; + case 'empty': + $view->show_message("warning", "文章內容不能為空!"); + break; + case 'level': + $view->show_message("inverted negative", "你沒有權限發文!"); + break; + } + } +?> +
        + 文章 + + + +
        +
        + + + + + + + + + + + + + + 0) { + foreach ($post_list as $key => $article) { + ?> + + + + + + + + + + + + + + + +
        標題留言日期管理
        title ?>likes_count ?>comments_count ?>time ?> + + +
        沒有文章
        +
        + render(); +} +?> \ No newline at end of file diff --git a/theme/default.html b/theme/default.html new file mode 100644 index 0000000..d7dab17 --- /dev/null +++ b/theme/default.html @@ -0,0 +1,51 @@ + + + + + + {part} | {title} + + + + + + + + + + + + {nav} +
        +
        +
        {title}
        +
        Welcome to {title}
        +
        +
        +
        +
        +
        + {message} + {content} +
        + +
        +
        + + +
        +
        + +
        + + + {script} + + \ No newline at end of file diff --git a/theme/nav/default.html b/theme/nav/default.html new file mode 100644 index 0000000..7728dd1 --- /dev/null +++ b/theme/nav/default.html @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/theme/nav/util.php b/theme/nav/util.php new file mode 100644 index 0000000..5a02743 --- /dev/null +++ b/theme/nav/util.php @@ -0,0 +1,28 @@ + + +
        +
        +
        通知
        +
        + 看所有通知 +
        +
        + \ No newline at end of file diff --git a/theme/sidebar.php b/theme/sidebar.php new file mode 100644 index 0000000..f4480b1 --- /dev/null +++ b/theme/sidebar.php @@ -0,0 +1,40 @@ +islogin){ ?> +
        + 登入或是註冊 +
        + + + name ?> +muted) { ?> +
        + 你已被禁言! +
        + +
        + + +
        + + +
        + +
        名稱
        +
        +

        項目

        +

        項目

        +

        項目

        +
        + +
        名稱
        +
        +

        項目

        +

        項目

        +

        項目

        +
        \ No newline at end of file diff --git a/theme/thinking.png b/theme/thinking.png new file mode 100644 index 0000000..3a14204 Binary files /dev/null and b/theme/thinking.png differ diff --git a/user.php b/user.php new file mode 100644 index 0000000..727e71c --- /dev/null +++ b/user.php @@ -0,0 +1,108 @@ + 0) { + $name = $result['row']['name']; + $level = $result['row']['level']; + $email = md5(strtolower($result['row']['email'])); + $role = cavern_level_to_role($level); + $posts = cavern_query_result("SELECT * FROM `post` WHERE `username`='%s'", array($username)); + $posts_count = ($posts['num_rows'] > 0 ? $posts['num_rows'] : 0); + } else { + http_response_code(404); + header('Location: user.php?err=no'); + exit; + } + + if (isset($_SESSION['cavern_username'])) { + $view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], $name); + $view->add_script_source("ts('.ts.dropdown').dropdown();"); + } else { + $view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], $name); + } + $view->add_script("./include/js/security.js"); + + if (isset($_GET['err'])) { + if ($_GET['err'] == "no") { + $view->show_message('negative', "找不到使用者"); + $view->render(); + exit; + } + } +?> +
        的個人資料
        +
        +
        +
        + +
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + +
        基本資料
        使用者名稱
        暱稱
        權限
        + + + + + + + + + + + + + + + +
        統計
        文章數
        看他的文章
        +
        +
        +
        +render(); +} else { + if (isset($_GET['err'])) { + if (isset($_SESSION['cavern_username'])) { + $view = new View('theme/default.html', 'theme/nav/util.php', 'theme/sidebar.php', $blog['name'], "使用者"); + $view->add_script_source("ts('.ts.dropdown').dropdown();"); + } else { + $view = new View('theme/default.html', 'theme/nav/default.html', 'theme/sidebar.php', $blog['name'], "使用者"); + } + $view->add_script("./include/js/security.js"); + + if ($_GET['err'] == "no") { + $view->show_message('negative', "找不到使用者"); + $view->render(); + exit; + } + } else { + header('Location: user.php?err=no'); + exit; + } +} +?>