diff --git a/.env.example b/.env.example index c7293bd..b15a71b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ DB_HOST = 127.0.0.1 DB_NAME = catask -DB_USER = -DB_PASS = -ADMIN_PASSWORD = -APP_SECRET = +DB_USER = +DB_PASS = +DB_PORT = +ADMIN_PASSWORD = +APP_SECRET = diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4e7b184 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +## 1.4.0 + +### New features +* alerts show up as notifications in top-right corner of the screen +* ability to set rules +* live preview of some options in admin panel +* remaining character count under the question input box +* added alerts for "copy link" and "copy to clipboard" actions +* ctrl-enter shortcut to answer question, the same shortcut is used to save changes in admin panel +* live unyanswered question count in inbox +* option to disable confirmation when deleting questions from inbox +* instance description now supports markdown + +### Fixes +* improved accessibility of some elements +* emoji support by switching to utf8mb4 charset + +### Miscellaneous +* code cleanup + +### Known bugs +* notification for "return to inbox" option triggers twice (visual bug) diff --git a/README.md b/README.md index 3e7046f..9a23df4 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,25 @@ a work-in-progress minimal single-user q&a software > catask is alpha software, therefore bugs are expected to happen ## Prerequisites -- MySQL/MariaDB -- Python 3.10+ (3.12+ recommended) +- MySQL/MariaDB +- Python 3.10+ (3.12+ recommended) ## Install -Clone this repository: `git clone https://git.gay/mst/catask.git` +Clone this repository: `git clone https://git.gay/mst/catask.git` ### VPS-specific -Go into the cloned repository, create a virtual environment and activate it: +Go into the cloned repository, create a virtual environment and activate it: #### Linux ```python -m venv venv && . venv/bin/activate``` #### Windows (PowerShell) -```python -m venv venv; .\venv\Scripts\activate``` +```python -m venv venv; .\venv\Scripts\activate``` --- After that, install required packages: -```pip install -r requirements.txt``` +```pip install -r requirements.txt``` ### Shared hosting-specific If your shared hosting provider supports [WSGI](https://w.wiki/_vTN2), [FastCGI](https://w.wiki/9EeQ), or something similar, use it (technically any CGI protocol could work) @@ -40,6 +40,7 @@ First, rename `.env.example` to `.env` and `config.example.json` to `config.json `DB_NAME` - database name `DB_USER` - database user `DB_PASS` - database password +`DB_PORT` - database port (usually 3306) `ADMIN_PASSWORD` - password to access admin panel `APP_SECRET` - application secret, generate one with this command: `python -c 'import secrets; print(secrets.token_hex())'` @@ -49,15 +50,19 @@ Configure in Admin panel after installing (located at `https://yourdomain.tld/ad --- After you're done configuring CatAsk, init the database: `flask init-db` -If that doesn't work (e.g. tables are missing), try importing schema.sql into the created database manually +If that doesn't work (e.g. tables are missing), try importing schema.sql into the created database manually ## Usage Use one of these commands to run CatAsk: `flask run` or `gunicorn -w 4 app:app` (if you have gunicorn installed) or `python app.py` If you want CatAsk to be accessible on a specific host address, specify a `--host` option to `flask run` (e.g. `--host 0.0.0.0`) +For debugging, run `flask run` with `--debug` argument (`flask run --debug`) Admin login page is located at `https://yourdomain.tld/admin/login/` -Runs on `127.0.0.1:5000` (`flask run` or `python app.py`) or `127.0.0.1:8000` (`gunicorn -w 4 app:app`), may work in a production environment +Runs on `127.0.0.1:5000` (`flask run` or `python app.py`) or `127.0.0.1:8000` (`gunicorn -w 4 app:app`), may work in a production environment ### Caddy This repository contains an example Caddyfile that runs CatAsk on catask.localhost by reverse proxying it to 127.0.0.1:5000, you can modify it as needed +## Updating +For instructions with updating from one version to another, check [UPDATE.md](https://git.gay/mst/catask/src/branch/main/UPDATE.md) file +Check [CHANGELOG.md](https://git.gay/mst/catask/src/branch/main/CHANGELOG.md) file for release notes diff --git a/UPDATE.md b/UPDATE.md new file mode 100644 index 0000000..e7d03ff --- /dev/null +++ b/UPDATE.md @@ -0,0 +1,6 @@ +## 1.3.0 -> 1.4.0 +first, pull the update from the repository: `git pull` +second, you'll need to update the database schema as it was changed slightly in this update, run these queries in mysql/mariadb console: +```USE ;``` +```ALTER TABLE answers CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;``` +```ALTER TABLE questions CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;``` diff --git a/app.py b/app.py index c3f5ec5..5990156 100644 --- a/app.py +++ b/app.py @@ -39,7 +39,7 @@ def initDatabase(): print("Connected successfully") conn.database = dbName - + except mysql.connector.Error as error: if error.errno == errorcode.ER_ACCESS_DENIED_ERROR: print("Bad credentials") @@ -48,6 +48,7 @@ def initDatabase(): user=os.environ.get("DB_USER"), password=os.environ.get("DB_PASS"), host=os.environ.get("DB_HOST"), + port=os.environ.get("DB_PORT"), database='mysql' ) cursor = conn.cursor() @@ -56,7 +57,7 @@ def initDatabase(): else: print("Error:", error) return - + with open('schema.sql', 'r') as schema_file: schema = schema_file.read() try: @@ -124,7 +125,7 @@ def index(): 'question': question, 'answers': question_answers }) - + cursor.close() conn.close() return render_template('index.html', combined=combined, urllib=urllib, trimContent=func.trimContent, metadata=metadata, getRandomWord=func.getRandomWord, formatRelativeTime=func.formatRelativeTime) @@ -136,7 +137,7 @@ def inbox(): cursor = conn.cursor(dictionary=True) cursor.execute("SELECT * FROM questions WHERE answered=%s ORDER BY creation_date DESC", (False,)) questions = cursor.fetchall() - + cursor.close() conn.close() return render_template('inbox.html', questions=questions, formatRelativeTime=func.formatRelativeTime) @@ -156,7 +157,7 @@ def seeAskedQuestions(): cursor = conn.cursor(dictionary=True) cursor.execute("SELECT * FROM questions WHERE asker_id=%s ORDER BY creation_date DESC", (asker_id,)) questions = cursor.fetchall() - + cursor.close() conn.close() return render_template('asked_questions.html', questions=questions, formatRelativeTime=func.formatRelativeTime) @@ -235,9 +236,9 @@ def addQuestion(): from_who = request.form.get('from_who', cfg['anonName']) question = request.form.get('question', '') antispam = request.form.get('antispam', '') - # reserved for version 1.4.0 + # reserved for version 1.5.0 or later # private = request.form.get('private') - + if not question: abort(400, "Question field must not be empty") if not antispam: @@ -260,13 +261,13 @@ def addQuestion(): cursor.execute("INSERT INTO questions (from_who, content, answered) VALUES (%s, %s, %s)", (from_who, question, False,)) cursor.close() conn.close() - + return {'message': 'Question asked successfully!'}, 201 @api_bp.route('/delete_question/', methods=['DELETE']) @loginRequired def deleteQuestion(): - question_id = request.args.get('question_id', '') + question_id = request.args.get('question_id', '') if not question_id: abort(400, "Missing 'question_id' attribute or 'question_id' is empty") @@ -275,7 +276,7 @@ def deleteQuestion(): cursor.execute("DELETE FROM questions WHERE id=%s", (question_id,)) cursor.close() conn.close() - + return {'message': 'Successfully deleted question.'}, 200 @api_bp.route('/return_to_inbox/', methods=['POST']) @@ -295,17 +296,17 @@ def returnToInbox(): 'content': row[1], 'creation_date': row[2] } - + cursor.execute("DELETE FROM questions WHERE id=%s", (question_id,)) cursor.execute("INSERT INTO questions (from_who, content, creation_date, answered) VALUES (%s, %s, %s, %s)", (question["from_who"], question["content"], question["creation_date"], False,)) cursor.close() conn.close() - + return {'message': 'Successfully returned question to inbox.'}, 200 @api_bp.route('/pin_question/', methods=['POST']) def pinQuestion(): - question_id = request.args.get('question_id', '') + question_id = request.args.get('question_id', '') if not question_id: abort(400, "Missing 'question_id' attribute or 'question_id' is empty") @@ -314,12 +315,12 @@ def pinQuestion(): cursor.execute("UPDATE questions SET pinned=%s WHERE id=%s", (True, question_id)) cursor.close() conn.close() - + return {'message': 'Successfully pinned question.'}, 200 @api_bp.route('/unpin_question/', methods=['POST']) def unpinQuestion(): - question_id = request.args.get('question_id', '') + question_id = request.args.get('question_id', '') if not question_id: abort(400, "Missing 'question_id' attribute or 'question_id' is empty") @@ -328,7 +329,7 @@ def unpinQuestion(): cursor.execute("UPDATE questions SET pinned=%s WHERE id=%s", (False, question_id)) cursor.close() conn.close() - + return {'message': 'Successfully unpinned question.'}, 200 @api_bp.route('/add_answer/', methods=['POST']) @@ -358,6 +359,22 @@ def addAnswer(): return jsonify({'message': 'Answer added successfully!'}), 201 +# reserved for 1.6.0 or later +""" +@api_bp.route('/question_count/', methods=['GET']) +def questionCount(): + conn = func.connectToDb() + cursor = conn.cursor() + cursor.execute("SELECT COUNT(*) FROM questions WHERE answered=%s", (False,)) + question_count = cursor.fetchone() + + cursor.close() + conn.close() + return str(question_count[0]) +""" + +# unused +""" @api_bp.route('/all_questions/', methods=['GET']) def listQuestions(): answered = request.args.get('answered', False) @@ -378,6 +395,7 @@ def listAnswers(): cursor.close() conn.close() return jsonify(answers) +""" @api_bp.route('/view_question/', methods=['GET']) def viewQuestion(): @@ -420,7 +438,7 @@ def updateConfig(): for nested_key in nested_keys[:-1]: current_dict = current_dict.setdefault(nested_key, {}) - + # Convert the checkbox value 'True'/'False' strings to actual booleans if value.lower() == 'true': value = True diff --git a/config.example.json b/config.example.json index 80257a9..128bfa4 100644 --- a/config.example.json +++ b/config.example.json @@ -9,5 +9,6 @@ "anonName": "Anonymous", "lockInbox": false, "allowAnonQuestions": true, - "showQuestionCount": false + "showQuestionCount": false, + "noDeleteConfirm": false } diff --git a/constants.py b/constants.py index bde2d0a..7e7d823 100644 --- a/constants.py +++ b/constants.py @@ -2,6 +2,6 @@ antiSpamFile = 'wordlist.txt' blacklistFile = 'word_blacklist.txt' configFile = 'config.json' appName = 'CatAsk' -version = '1.3.0' +version = '1.4.0' # id (identifier) is to be interpreted as described in https://semver.org/#spec-item-9 version_id = '-alpha' diff --git a/functions.py b/functions.py index b22bb66..14b6620 100644 --- a/functions.py +++ b/functions.py @@ -43,6 +43,7 @@ dbHost = os.environ.get("DB_HOST") dbUser = os.environ.get("DB_USER") dbPass = os.environ.get("DB_PASS") dbName = os.environ.get("DB_NAME") +dbPort = os.environ.get("DB_PORT") def createDatabase(cursor, dbName): try: @@ -58,6 +59,7 @@ def connectToDb(): user=dbUser, password=dbPass, database=dbName, + port=dbPort, pool_name=f"{const.appName}-pool", pool_size=32, autocommit=True diff --git a/roadmap.md b/roadmap.md index 2e16cbb..d5a095a 100644 --- a/roadmap.md +++ b/roadmap.md @@ -1,10 +1,12 @@ # CatAsk stable roadmap +* [ ] content warnings +* [x] move to toastify for alerts * [x] make stuff more accessible * [ ] implement private questions -* [x] deleting answered questions OR returning them to inbox like retrospring does -* [ ] bulk deleting questions from inbox -* [ ] blocking askers by ip -* [x] make an admin page -* [x] implement an optional blacklist of words +* [x] deleting answered questions OR returning them to inbox like retrospring does +* [ ] bulk deleting questions from inbox +* [ ] blocking askers by ip +* [x] make an admin page +* [x] implement an optional blacklist of words * [ ] add more customization options (theme + favicon) diff --git a/schema.sql b/schema.sql index 6fe590f..06eb50f 100644 --- a/schema.sql +++ b/schema.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS answers ( question_id INT NOT NULL, creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS questions ( id INT PRIMARY KEY AUTO_INCREMENT, @@ -13,9 +13,9 @@ CREATE TABLE IF NOT EXISTS questions ( answered BOOLEAN NOT NULL DEFAULT FALSE, answer_id INT, pinned BOOLEAN NOT NULL DEFAULT FALSE - -- below is reserved for version 1.4.0 + -- below is reserved for version 1.5.0 or later -- private BOOLEAN NOT NULL DEFAULT FALSE -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ALTER TABLE questions ADD CONSTRAINT fk_answer_id FOREIGN KEY (answer_id) REFERENCES answers(id) ON DELETE CASCADE; diff --git a/static/css/style.css b/static/css/style.css index e002012..378d289 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -15,7 +15,7 @@ [data-bs-theme=light] { --bs-primary: #6345d9; --bs-primary-rgb: 99, 69, 217; - --bs-primary-subtle: color-mix(in srgb, var(--bs-primary) 10%, transparent); + --bs-primary-bg-subtle: color-mix(in srgb, var(--bs-primary) 20%, transparent); --bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, white); --bs-danger: #dc3545; --bs-danger-bg-subtle: #f8e6e8; @@ -25,6 +25,7 @@ --bs-basic-btn-hover-bg-strong: color-mix(in srgb, var(--bs-body-bg) 90%, black); --bs-basic-btn-active-bg: color-mix(in srgb, var(--bs-body-bg) 92%, black); --bs-basic-btn-active-bg-strong: color-mix(in srgb, var(--bs-body-bg) 87%, black); + --bs-primary-text-emphasis: color-mix(in srgb, var(--bs-primary), black); } [data-bs-theme=dark] { @@ -32,7 +33,7 @@ --bs-body-bg-rgb: 32, 32, 32; --bs-primary: #7f62f0; --bs-primary-rgb: 127,98,240; - --bs-primary-subtle: color-mix(in srgb, var(--bs-primary) 10%, transparent); + --bs-primary-bg-subtle: color-mix(in srgb, var(--bs-primary) 20%, transparent); --bs-danger-bg-subtle: #2c0b0e; --bs-link-color: color-mix(in srgb, var(--bs-primary) 65%, white); --bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, white); @@ -40,6 +41,7 @@ --bs-basic-btn-hover-bg-strong: color-mix(in srgb, var(--bs-body-bg) 90%, white); --bs-basic-btn-active-bg: color-mix(in srgb, var(--bs-body-bg) 92%, white); --bs-basic-btn-active-bg-strong: color-mix(in srgb, var(--bs-body-bg) 87%, white); + --bs-primary-text-emphasis: color-mix(in srgb, var(--bs-primary), white); } [data-bs-theme=light] .light-invert, @@ -47,11 +49,21 @@ filter: invert(); } +.btn-check:checked + .btn-secondary, .btn-check:checked + .btn-outline-secondary { + --bs-btn-active-bg: var(--bs-basic-btn-active-bg-strong) !important; + --bs-btn-active-color: var(--bs-body-color); + background-color: var(--bs-btn-active-bg) !important; +} +.btn-outline-secondary { + --bs-btn-color: var(--bs-body-color); + --bs-btn-border-color: var(--bs-basic-btn-active-bg-strong); +} + .btn { --bs-btn-border-radius: .5rem; } -.btn:not(.btn-primary,.btn-danger,.btn-success):focus-visible { - outline: revert; +.btn.disabled, .btn:disabled, fieldset:disabled .btn { + background-color: var(--bs-btn-disabled-bg); } [data-bs-theme=light] .btn-primary { @@ -61,15 +73,17 @@ --bs-btn-hover-border-color: var(--bs-btn-hover-bg); --bs-btn-active-bg: color-mix(in srgb, var(--bs-btn-bg) 80%, black); --bs-btn-active-border-color: var(--bs-btn-active-bg); + --bs-btn-disabled-bg: color-mix(in srgb, var(--bs-primary) 70%, white); } [data-bs-theme=dark] .btn-primary { - --bs-btn-bg: color-mix(in srgb, var(--bs-primary) 80%, black); + --bs-btn-bg: color-mix(in srgb, var(--bs-primary) 65%, black); --bs-btn-border-color: var(--bs-btn-bg); --bs-btn-hover-bg: color-mix(in srgb, var(--bs-btn-bg) 90%, black); --bs-btn-hover-border-color: var(--bs-btn-hover-bg); --bs-btn-active-bg: color-mix(in srgb, var(--bs-btn-bg) 80%, black); --bs-btn-active-border-color: var(--bs-btn-active-bg); + --bs-btn-disabled-bg: color-mix(in srgb, var(--bs-primary) 70%, black); } .btn.btn-primary { @@ -79,17 +93,17 @@ padding: 7px 13px; } -.btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):hover, .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):focus { +.btn:not(.btn-primary, .btn-outline-warning, .btn-success, .btn-secondary, .btn-danger,.btn-outline-danger):hover, .btn:not(.btn-primary, .btn-success, .btn-secondary, .btn-outline-warning, .btn-danger,.btn-outline-danger):focus { background-color: var(--bs-basic-btn-hover-bg); } -.btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):active { +.btn:not(.btn-primary, .btn-outline-warning, .btn-success, .btn-secondary, .btn-danger,.btn-outline-danger):active { background-color: var(--bs-basic-btn-active-bg); } -.card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):hover, .card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):focus { +.card-footer .btn:not(.btn-primary, .btn-outline-warning, .btn-success, .btn-secondary, .btn-danger, .btn-outline-danger):hover, .card-footer .btn:not(.btn-primary, .btn-success, .btn-outline-warning, .btn-secondary, .btn-danger,.btn-outline-danger):focus { background-color: var(--bs-basic-btn-hover-bg-strong) !important; color: var(--bs-body-color) !important; } -.card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):active { +.card-footer .btn:not(.btn-primary, .btn-outline-warning, .btn-secondary, .btn-success, .btn-danger, .btn-outline-danger):active { background-color: var(--bs-basic-btn-active-bg-strong) !important; } .btn-check:checked + .btn, .btn.active, .btn.show, .btn:first-child:active, :not(.btn-check) + .btn:active { @@ -113,12 +127,12 @@ a:hover { } [data-bs-theme=light] .dropdown-item:hover, [data-bs-theme=light] .dropdown-item:focus { - --bs-dropdown-link-hover-bg: var(--bs-primary-subtle); + --bs-dropdown-link-hover-bg: color-mix(in srgb, var(--bs-primary) 10%, transparent); --bs-dropdown-link-hover-color: var(--bs-primary); } [data-bs-theme=dark] .dropdown-item:hover, [data-bs-theme=dark] .dropdown-item:focus { - --bs-dropdown-link-hover-bg: var(--bs-primary-subtle); + --bs-dropdown-link-hover-bg: color-mix(in srgb, var(--bs-primary) 10%, transparent); --bs-dropdown-link-hover-color: color-mix(in srgb, var(--bs-primary) 70%, white); } @@ -154,9 +168,11 @@ a:hover { --bs-dropdown-link-active-color: white; } -.form-control:focus, .form-check-input:focus { +.form-control:focus, .form-check-input:focus, +.accordion-button:focus, .btn:focus-visible { box-shadow: 0 0 0 .25rem color-mix(in srgb, var(--bs-primary) 25%, transparent); border-color: color-mix(in srgb, var(--bs-primary), transparent); + outline: 0; } .list-group-item.active { @@ -176,6 +192,9 @@ a:hover { .markdown-content p { margin: 0; } +.markdown-content ol { + margin-bottom: 0; +} .htmx-indicator { display: none; @@ -190,3 +209,24 @@ a:hover { border-color: var(--bs-primary); } +.nav-underline .nav-link.active, .nav-underline .show > .nav-link { + font-weight: 500; +} + +.spinner-border-sm { + --bs-spinner-border-width: .15em; +} +/* #dee2e6 */ +[data-bs-theme="dark"] .accordion-button::after { + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23dee2e6'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23d7d8ea'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); +} + +#rules li:not(:first-child, :last-child) { + margin: .5em 0; +} + +#rules li::marker { + font-weight: bold; + font-size: 1.5em; +} diff --git a/static/css/toastify.css b/static/css/toastify.css new file mode 100644 index 0000000..4a495f1 --- /dev/null +++ b/static/css/toastify.css @@ -0,0 +1,91 @@ +/*! + * Toastify js 1.12.0 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ + +.toastify { + /* padding: 12px 20px; */ + /* color: #ffffff; */ + /* display: inline-block; */ + /* box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3); */ + /* background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5); */ + /* background: linear-gradient(135deg, #73a5ff, #5477f5); */ + /* padding: .5em .8em; */ + position: fixed; + opacity: 0; + transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1); + /* border-radius: 2px; */ + /* cursor: pointer; */ + /* text-decoration: none; */ + max-width: calc(50% - 20px); + z-index: 2147483647; +} + +.toastify.on { + opacity: 1; +} + +.toast-close { + background: transparent; + border: 0; + color: white; + cursor: pointer; + font-family: inherit; + font-size: 1em; + opacity: 0.4; + padding: 0 5px; +} + +.toastify-right { + right: 15px; +} + +.toastify-left { + left: 15px; +} + +.toastify-top { + top: -150px; +} + +.toastify-bottom { + bottom: -150px; +} + +.toastify-rounded { + border-radius: 25px; +} + +.toastify-avatar { + width: 1.5em; + height: 1.5em; + margin: -7px 5px; + border-radius: 2px; +} + +.toastify-center { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + max-width: -moz-fit-content; +} + +@media only screen and (max-width: 360px) { + .toastify-right, .toastify-left { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + } +} +@media screen and (max-width: 700px) { + .toastify { + max-width: 100%; + } +} diff --git a/static/js/marked.min.js b/static/js/marked.min.js new file mode 100644 index 0000000..f724cc4 --- /dev/null +++ b/static/js/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v14.1.2 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/(^|[^\[])\^/g;function p(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(h,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function u(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch{return null}return e}const k={exec:()=>null};function g(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^(?: {1,4}| {0,3}\t)/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:f(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=f(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:f(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=f(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l/.test(e[l]))i.push(e[l]),t=!0;else{if(t)break;i.push(e[l])}e=e.slice(l);const o=i.join("\n"),a=o.replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1").replace(/^ {0,3}>[ \t]?/gm,"");n=n?`${n}\n${o}`:o,s=s?`${s}\n${a}`:a;const c=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(a,r,!0),this.lexer.state.top=c,0===e.length)break;const h=r[r.length-1];if("code"===h?.type)break;if("blockquote"===h?.type){const t=h,i=t.raw+"\n"+e.join("\n"),l=this.blockquote(i);r[r.length-1]=l,n=n.substring(0,n.length-t.raw.length)+l.raw,s=s.substring(0,s.length-t.text.length)+l.text;break}if("list"!==h?.type);else{const t=h,i=t.raw+"\n"+e.join("\n"),l=this.list(i);r[r.length-1]=l,n=n.substring(0,n.length-h.raw.length)+l.raw,s=s.substring(0,s.length-t.raw.length)+l.raw,e=i.substring(r[r.length-1].raw.length).split("\n")}}return{type:"blockquote",raw:n,tokens:r,text:s}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(/[^ ]/),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&/^[ \t]*$/.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,p-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,p-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,p-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,p-1)}}#`),l=new RegExp(`^ {0,${Math.min(3,p-1)}}<[a-z].*>`,"i");for(;e;){const u=e.split("\n",1)[0];let k;if(c=u,this.options.pedantic?(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," "),k=c):k=c.replace(/\t/g," "),r.test(c))break;if(i.test(c))break;if(l.test(c))break;if(t.test(c))break;if(n.test(c))break;if(k.search(/[^ ]/)>=p||!c.trim())o+="\n"+k.slice(p);else{if(h)break;if(a.replace(/\t/g," ").search(/[^ ]/)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=u+"\n",e=e.substring(u.length+1),a=k.slice(p)}}r.loose||(l?r.loose=!0:/\n[ \t]*\n[ \t]*$/.test(s)&&(l=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:s,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}r.items[r.items.length-1].raw=r.items[r.items.length-1].raw.trimEnd(),r.items[r.items.length-1].text=r.items[r.items.length-1].text.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=g(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=f(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),d(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return d(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const b=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,w=/(?:[*+-]|\d{1,9}[.)])/,m=p(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,w).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),y=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,$=/(?!\s*\])(?:\\.|[^\[\]\\])+/,z=p(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",$).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),T=p(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,w).getRegex(),R="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",_=/|$))/,A=p("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",_).replace("tag",R).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),S=p(y).replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex(),I={blockquote:p(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",S).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:z,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:b,html:A,lheading:m,list:T,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:S,table:k,text:/^[^\n]+/},E=p("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex(),q={...I,table:E,paragraph:p(y).replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",E).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex()},Z={...I,html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",_).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:k,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:p(y).replace("hr",b).replace("heading"," *#{1,6} *[^\n]").replace("lheading",m).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},P=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,L=/^( {2,}|\\)\n(?!\s*$)/,v="\\p{P}\\p{S}",Q=p(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,v).getRegex(),B=p(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,v).getRegex(),M=p("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,v).getRegex(),O=p("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,v).getRegex(),j=p(/\\([punct])/,"gu").replace(/punct/g,v).getRegex(),D=p(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),C=p(_).replace("(?:--\x3e|$)","--\x3e").getRegex(),H=p("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",C).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),U=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,X=p(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",U).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),F=p(/^!?\[(label)\]\[(ref)\]/).replace("label",U).replace("ref",$).getRegex(),N=p(/^!?\[(ref)\](?:\[\])?/).replace("ref",$).getRegex(),G={_backpedal:k,anyPunctuation:j,autolink:D,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:L,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:k,emStrongLDelim:B,emStrongRDelimAst:M,emStrongRDelimUnd:O,escape:P,link:X,nolink:N,punctuation:Q,reflink:F,reflinkSearch:p("reflink|nolink(?!\\()","g").replace("reflink",F).replace("nolink",N).getRegex(),tag:H,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0)))))if(s=this.tokenizer.space(e))e=e.substring(s.raw.length),1===s.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(s);else if(s=this.tokenizer.code(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?t.push(s):(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.fences(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.heading(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.hr(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.blockquote(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.list(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.html(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.def(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title}):(r.raw+="\n"+s.raw,r.text+="\n"+s.raw,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.table(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.lheading(e))e=e.substring(s.raw.length),t.push(s);else{if(i=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(i=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i)))r=t[t.length-1],n&&"paragraph"===r?.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);else if(s=this.tokenizer.text(e))e=e.substring(s.raw.length),r=t[t.length-1],r&&"text"===r.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class te{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(/^\S*/)?.[0],r=e.replace(/\n$/,"")+"\n";return s?'
'+(n?r:c(r,!0))+"
\n":"
"+(n?r:c(r,!0))+"
\n"}blockquote({tokens:e}){return`
\n${this.parser.parse(e)}
\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return"
\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t\n"+s+"\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?e.tokens.length>0&&"paragraph"===e.tokens[0].type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+e.tokens[0].tokens[0].text)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" "}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • \n`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t="",n="";for(let t=0;t${s}`),"\n\n"+t+"\n"+s+"
    \n"}tablerow({text:e}){return`\n${e}\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${e}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=u(e);if(null===r)return s;let i='
    ",i}image({href:e,title:t,text:n}){const s=u(e);if(null===s)return n;let r=`${n}{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new te(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new x(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new re;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];re.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ee.lex(e,t??this.defaults)}parser(e,t){return se.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?ee.lex:ee.lexInline,o=r.hooks?r.hooks.provideParser():e?se.parse:se.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const le=new ie;function oe(e,t){return le.parse(e,t)}oe.options=oe.setOptions=function(e){return le.setOptions(e),oe.defaults=le.defaults,n(oe.defaults),oe},oe.getDefaults=t,oe.defaults=e.defaults,oe.use=function(...e){return le.use(...e),oe.defaults=le.defaults,n(oe.defaults),oe},oe.walkTokens=function(e,t){return le.walkTokens(e,t)},oe.parseInline=le.parseInline,oe.Parser=se,oe.parser=se.parse,oe.Renderer=te,oe.TextRenderer=ne,oe.Lexer=ee,oe.lexer=ee.lex,oe.Tokenizer=x,oe.Hooks=re,oe.parse=oe;const ae=oe.options,ce=oe.setOptions,he=oe.use,pe=oe.walkTokens,ue=oe.parseInline,ke=oe,ge=se.parse,fe=ee.lex;e.Hooks=re,e.Lexer=ee,e.Marked=ie,e.Parser=se,e.Renderer=te,e.TextRenderer=ne,e.Tokenizer=x,e.getDefaults=t,e.lexer=fe,e.marked=oe,e.options=ae,e.parse=ke,e.parseInline=ue,e.parser=ge,e.setOptions=ce,e.use=he,e.walkTokens=pe})); diff --git a/static/js/toastify.min.js b/static/js/toastify.min.js new file mode 100644 index 0000000..021d28c --- /dev/null +++ b/static/js/toastify.min.js @@ -0,0 +1,15 @@ +/** + * Minified by jsDelivr using Terser v5.14.1. + * Original file: /npm/toastify-js@1.12.0/src/toastify.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! + * Toastify js 1.12.0 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ +!function(t,o){"object"==typeof module&&module.exports?module.exports=o():t.Toastify=o()}(this,(function(t){var o=function(t){return new o.lib.init(t)};function i(t,o){return o.offset[t]?isNaN(o.offset[t])?o.offset[t]:o.offset[t]+"px":"0px"}function s(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&t.className.trim().split(/\s+/gi).indexOf(o)>-1)}return o.defaults={oldestFirst:!0,text:"Toastify is awesome!",node:void 0,duration:3e3,selector:void 0,callback:function(){},destination:void 0,newWindow:!1,close:!1,gravity:"toastify-top",positionLeft:!1,position:"",backgroundColor:"",avatar:"",className:"",stopOnFocus:!0,onClick:function(){},offset:{x:0,y:0},escapeMarkup:!0,ariaLive:"polite",style:{background:""}},o.lib=o.prototype={toastify:"1.12.0",constructor:o,init:function(t){return t||(t={}),this.options={},this.toastElement=null,this.options.text=t.text||o.defaults.text,this.options.node=t.node||o.defaults.node,this.options.duration=0===t.duration?0:t.duration||o.defaults.duration,this.options.selector=t.selector||o.defaults.selector,this.options.callback=t.callback||o.defaults.callback,this.options.destination=t.destination||o.defaults.destination,this.options.newWindow=t.newWindow||o.defaults.newWindow,this.options.close=t.close||o.defaults.close,this.options.gravity="bottom"===t.gravity?"toastify-bottom":o.defaults.gravity,this.options.positionLeft=t.positionLeft||o.defaults.positionLeft,this.options.position=t.position||o.defaults.position,this.options.backgroundColor=t.backgroundColor||o.defaults.backgroundColor,this.options.avatar=t.avatar||o.defaults.avatar,this.options.className=t.className||o.defaults.className,this.options.stopOnFocus=void 0===t.stopOnFocus?o.defaults.stopOnFocus:t.stopOnFocus,this.options.onClick=t.onClick||o.defaults.onClick,this.options.offset=t.offset||o.defaults.offset,this.options.escapeMarkup=void 0!==t.escapeMarkup?t.escapeMarkup:o.defaults.escapeMarkup,this.options.ariaLive=t.ariaLive||o.defaults.ariaLive,this.options.style=t.style||o.defaults.style,t.backgroundColor&&(this.options.style.background=t.backgroundColor),this},buildToast:function(){if(!this.options)throw"Toastify is not initialized";var t=document.createElement("div");for(var o in t.className="toastify on "+this.options.className,this.options.position?t.className+=" toastify-"+this.options.position:!0===this.options.positionLeft?(t.className+=" toastify-left",console.warn("Property `positionLeft` will be depreciated in further versions. Please use `position` instead.")):t.className+=" toastify-right",t.className+=" "+this.options.gravity,this.options.backgroundColor&&console.warn('DEPRECATION NOTICE: "backgroundColor" is being deprecated. Please use the "style.background" property.'),this.options.style)t.style[o]=this.options.style[o];if(this.options.ariaLive&&t.setAttribute("aria-live",this.options.ariaLive),this.options.node&&this.options.node.nodeType===Node.ELEMENT_NODE)t.appendChild(this.options.node);else if(this.options.escapeMarkup?t.innerText=this.options.text:t.innerHTML=this.options.text,""!==this.options.avatar){var s=document.createElement("img");s.src=this.options.avatar,s.className="toastify-avatar","left"==this.options.position||!0===this.options.positionLeft?t.appendChild(s):t.insertAdjacentElement("afterbegin",s)}if(!0===this.options.close){var e=document.createElement("button");e.type="button",e.setAttribute("aria-label","Close"),e.className="btn-close",e.addEventListener("click",function(t){t.stopPropagation(),this.removeElement(this.toastElement),window.clearTimeout(this.toastElement.timeOutValue)}.bind(this));var n=window.innerWidth>0?window.innerWidth:screen.width;("left"==this.options.position||!0===this.options.positionLeft)&&n>360?t.insertAdjacentElement("afterbegin",e):t.appendChild(e)}if(this.options.stopOnFocus&&this.options.duration>0){var a=this;t.addEventListener("mouseover",(function(o){window.clearTimeout(t.timeOutValue)})),t.addEventListener("mouseleave",(function(){t.timeOutValue=window.setTimeout((function(){a.removeElement(t)}),a.options.duration)}))}if(void 0!==this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),!0===this.options.newWindow?window.open(this.options.destination,"_blank"):window.location=this.options.destination}.bind(this)),"function"==typeof this.options.onClick&&void 0===this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),this.options.onClick()}.bind(this)),"object"==typeof this.options.offset){var l=i("x",this.options),r=i("y",this.options),p="left"==this.options.position?l:"-"+l,d="toastify-top"==this.options.gravity?r:"-"+r;t.style.transform="translate("+p+","+d+")"}return t},showToast:function(){var t;if(this.toastElement=this.buildToast(),!(t="string"==typeof this.options.selector?document.getElementById(this.options.selector):this.options.selector instanceof HTMLElement||"undefined"!=typeof ShadowRoot&&this.options.selector instanceof ShadowRoot?this.options.selector:document.body))throw"Root element is not defined";var i=o.defaults.oldestFirst?t.firstChild:t.lastChild;return t.insertBefore(this.toastElement,i),o.reposition(),this.options.duration>0&&(this.toastElement.timeOutValue=window.setTimeout(function(){this.removeElement(this.toastElement)}.bind(this),this.options.duration)),this},hideToast:function(){this.toastElement.timeOutValue&&clearTimeout(this.toastElement.timeOutValue),this.removeElement(this.toastElement)},removeElement:function(t){t.className=t.className.replace(" on",""),window.setTimeout(function(){this.options.node&&this.options.node.parentNode&&this.options.node.parentNode.removeChild(this.options.node),t.parentNode&&t.parentNode.removeChild(t),this.options.callback.call(t),o.reposition()}.bind(this),400)}},o.reposition=function(){for(var t,o={top:15,bottom:15},i={top:15,bottom:15},e={top:15,bottom:15},n=document.getElementsByClassName("toastify"),a=0;a0?window.innerWidth:screen.width)<=360?(n[a].style[t]=e[t]+"px",e[t]+=l+15):!0===s(n[a],"toastify-left")?(n[a].style[t]=o[t]+"px",o[t]+=l+15):(n[a].style[t]=i[t]+"px",i[t]+=l+15)}return this},o.lib.init.prototype=o.lib,o})); +//# sourceMappingURL=/sm/e1ebbfe1bf0b0061f0726ebc83434e1c2f8308e6354c415fd05ecccdaad47617.map diff --git a/templates/admin/index.html b/templates/admin/index.html index a13b7a8..a64bd94 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -1,28 +1,47 @@ {% extends 'base.html' %} {% block title %}Admin{% endblock %} {% set adminLink = 'active' %} +{% block additionalHeadItems %} + +{% endblock %} {% block content %}

    Admin panel

    +
    -
    +Skip to preview + +Skip to preview +
    +

    Instance

    - - + +

    Title of this CatAsk instance

    - - + +

    Description of this CatAsk instance

    - + + +

    Rules of this CatAsk instance

    +
    +
    +

    Image that's going to be used in a link preview

    - +

    Full URL to homepage of this CatAsk instance without a trailing slash

    @@ -37,41 +56,63 @@

    This name will be used for questions asked to you by anonymous users

    + + + + + + + + + + +
    -
    -
    +
    + + + +
    -
    -

    -
    +
    -

    Blacklisted words for questions; one word per line

    +

    Blacklisted words for questions; one word per line

    -
    +
    +
    +

    Preview

    + +
    + The preview is not guaranteed to render the same as on other pages, because it uses a different renderer (marked (js) while everything else uses mistune (python))
    The reason for this is live markdown rendering support
    +
    + +

    {{ cfg.instance.title }}

    + {% autoescape off %} +

    {{ cfg.instance.description | render_markdown }}

    + {% endautoescape %} +
    +
    +
    +

    + +

    +
    +
    +
    {{ cfg.instance.rules | render_markdown }}
    +
    +
    +
    +
    +
    +
    + {% endblock %} {% block scripts %} + + + {% endblock %} diff --git a/templates/base.html b/templates/base.html index 7cbe41d..03852a6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -36,7 +36,8 @@ - {% block title %}{% endblock %} | {{ const.appName }} + {% block additionalHeadItems %}{% endblock %} + {% block title %}{% endblock %} | {{ cfg.instanceTitle }} Skip to content @@ -47,6 +48,7 @@ {% endif %} diff --git a/templates/inbox.html b/templates/inbox.html index 74dec2a..37419df 100644 --- a/templates/inbox.html +++ b/templates/inbox.html @@ -1,13 +1,16 @@ {% extends 'base.html' %} {% block title %}Inbox {% if len(questions) > 0 %}({{ len(questions) }}){% endif %}{% endblock %} {% set inboxLink = 'active' %} +{% block additionalHeadItems %} + +{% endblock %} {% block content %} {% if questions != [] %} -

    {{ len(questions) }} question(s)

    +

    {{ len(questions) }} question(s)

    {% for question in questions %}
    -
    +
    @@ -19,8 +22,8 @@
    {# - reserved for version 1.4.0 or later - + reserved for version 1.5.0 or later + {% if question.private %} Private {% endif %} @@ -31,22 +34,27 @@
    {{ question.content | render_markdown }}
    -
    +
    - + {% if not cfg.noDeleteConfirm %} + {% else %} + + {% endif %}
    + {% if not cfg.noDeleteConfirm %} + {% endif %}
    {% endfor %}
    @@ -72,28 +81,34 @@ {% endif %} {% endblock %} {% block scripts %} + diff --git a/templates/index.html b/templates/index.html index 9cad472..f9d9ccf 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,12 +1,33 @@ {% extends 'base.html' %} {% block title %}Home{% endblock %} {% set homeLink = 'active' %} +{% block additionalHeadItems %} + +{% endblock %} {% block content %}
    -

    {{ cfg.instance.title }}

    - {% autoescape off %} -

    {{ cfg.instance.description }}

    - {% endautoescape %} +
    +

    {{ cfg.instance.title }}

    + {% autoescape off %} +

    {{ cfg.instance.description | render_markdown }}

    + {% endautoescape %} +
    +
    +
    +
    +

    + +

    +
    +
    +
    {{ cfg.instance.rules | render_markdown }}
    +
    +
    +
    +
    +
    @@ -16,12 +37,13 @@

    Ask a question

    - +
    - + +

    Markdown supported {{ cfg.charLimit }}

    @@ -30,18 +52,18 @@
    {#
    - reserved for version 1.4.0 -
    #} - @@ -184,6 +206,7 @@
    {% endblock %} {% block scripts %} + {% endblock %} diff --git a/wakatime-cli.zip b/wakatime-cli.zip new file mode 100644 index 0000000..6454256 Binary files /dev/null and b/wakatime-cli.zip differ