From 229551a218900c006118e7b7d75e416fab696981 Mon Sep 17 00:00:00 2001 From: Filoji Date: Wed, 22 Jul 2020 17:48:01 +0200 Subject: [PATCH 01/94] Add attachement.py --- app/models/attachement.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 app/models/attachement.py diff --git a/app/models/attachement.py b/app/models/attachement.py new file mode 100644 index 0000000..0634847 --- /dev/null +++ b/app/models/attachement.py @@ -0,0 +1,32 @@ +from app import db +from hashlib import sha256 +import os + +class Attachement(db.Model): + __tablename__ = 'attachement' + id = db.Column(db.Integer, primary_key=True) + + # Original name of the file + name = db.Column(db.Unicode(64)) + + # Hash of the value + hashed = db.Column(db.Unicode(64)) + + # The comment linked with + comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False) + comment = db.relationship('Comment', backref=backref('attachments', lazy='dynamic')) + + # The size of the file + size = db.Column(db.Integer) + + def __init__(self, file, comment): + self.name = file.filename + self.size = os.stat(file).st_size + self.hashed = str(hash_file(file)) + self.comment = comment + + def hash_file(file): + with open(file,"rb") as f: + bytes = f.read() # read entire file as bytes + hashed = hashlib.sha256(bytes).hexdigest() + return hashed \ No newline at end of file From 1689f73e896b113ba065251c08d4b0117601e65b Mon Sep 17 00:00:00 2001 From: Filoji Date: Wed, 22 Jul 2020 17:51:23 +0200 Subject: [PATCH 02/94] Correct errors --- app/models/attachement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/attachement.py b/app/models/attachement.py index 0634847..cbc5ace 100644 --- a/app/models/attachement.py +++ b/app/models/attachement.py @@ -22,11 +22,11 @@ class Attachement(db.Model): def __init__(self, file, comment): self.name = file.filename self.size = os.stat(file).st_size - self.hashed = str(hash_file(file)) + self.hashed = hash_file(file) self.comment = comment def hash_file(file): with open(file,"rb") as f: bytes = f.read() # read entire file as bytes - hashed = hashlib.sha256(bytes).hexdigest() + hashed = sha256(bytes).hexdigest() return hashed \ No newline at end of file From 20622315bde73fab53537e558ab05ab49c2588ad Mon Sep 17 00:00:00 2001 From: Filoji Date: Thu, 23 Jul 2020 10:01:57 +0200 Subject: [PATCH 03/94] Corrections --- app/models/{attachement.py => attachment.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename app/models/{attachement.py => attachment.py} (88%) diff --git a/app/models/attachement.py b/app/models/attachment.py similarity index 88% rename from app/models/attachement.py rename to app/models/attachment.py index cbc5ace..099076f 100644 --- a/app/models/attachement.py +++ b/app/models/attachment.py @@ -2,8 +2,8 @@ from app import db from hashlib import sha256 import os -class Attachement(db.Model): - __tablename__ = 'attachement' +class Attachment(db.Model): + __tablename__ = 'attachment' id = db.Column(db.Integer, primary_key=True) # Original name of the file @@ -22,7 +22,7 @@ class Attachement(db.Model): def __init__(self, file, comment): self.name = file.filename self.size = os.stat(file).st_size - self.hashed = hash_file(file) + self.hashed = self.hash_file(file) self.comment = comment def hash_file(file): From 4117ec4b15f7d1d18d56850e4cd4c3e2e59acb03 Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 23 Jul 2020 20:46:30 +0200 Subject: [PATCH 04/94] =?UTF-8?q?user:=20FIX=20probl=C3=A8me=20de=20perf?= =?UTF-8?q?=20=C3=A0=20l'affichage=20Le=20serveur=20aime=20pas=20devoir=20?= =?UTF-8?q?recalculer=20=C3=A0=20chaque=20fois=20le=20nombre=20de=20commen?= =?UTF-8?q?taires=20de=20chaque=20topic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/account/user.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/templates/account/user.html b/app/templates/account/user.html index 147bc76..96bb349 100644 --- a/app/templates/account/user.html +++ b/app/templates/account/user.html @@ -67,14 +67,12 @@ Titre Forum Création - Commentaires {% for t in member.topics %} {{ t.title }} {{ t.forum.name }} Le {{ t.date_created|date }} - {{ t.thread.comments.count() }} {% endfor %} From 0896a6b16324f5d8a42e30787886ca8ac43cbda6 Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 25 Jul 2020 18:06:49 +0200 Subject: [PATCH 05/94] passwords: enhances passwords rules - based on entropy (min 60 bits) - adds a coloured progress bar if Js is enabled --- app/forms/account.py | 2 +- app/models/users.py | 10 ++-- app/routes/account/account.py | 9 ++-- app/routes/admin/account.py | 7 +-- app/static/css/form.css | 25 ++++++++++ app/static/scripts/entropy.js | 42 +++++++++++++++++ app/templates/account/account.html | 1 + app/templates/account/register.html | 1 + app/templates/account/reset_password.html | 1 + app/templates/admin/edit_account.html | 1 + app/utils/render.py | 2 - app/utils/validators.py | 56 ++++++++++------------- config.py | 5 -- 13 files changed, 112 insertions(+), 50 deletions(-) create mode 100644 app/static/scripts/entropy.js diff --git a/app/forms/account.py b/app/forms/account.py index 936c760..227fafc 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -72,7 +72,7 @@ class UpdateAccountForm(FlaskForm): ], ) password = PasswordField( - 'Mot de passe', + 'Nouveau mot de passe', validators=[ Optional(), vd.password, diff --git a/app/models/users.py b/app/models/users.py index 807f847..e01567e 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -35,6 +35,10 @@ class User(UserMixin, db.Model): # Other fields populated automatically through relations: # relationship populated from the Post class. + # Minimum and maximum user name length + NAME_MINLEN = 3 + NAME_MAXLEN = 32 + __mapper_args__ = { 'polymorphic_identity': __tablename__, 'polymorphic_on': type @@ -54,7 +58,7 @@ class Guest(User): id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # Reusable username, cannot be chosen as the name of a member # but will be distinguished at rendering time if a member take it later - name = db.Column(db.Unicode(64)) + name = db.Column(db.Unicode(User.NAME_MAXLEN)) def __init__(self, name): self.name = name @@ -73,8 +77,8 @@ class Member(User): id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # Primary attributes (needed for the system to work) - name = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True) - norm = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True, + name = db.Column(db.Unicode(User.NAME_MAXLEN), index=True) + norm = db.Column(db.Unicode(User.NAME_MAXLEN), index=True, unique=True) email = db.Column(db.Unicode(120), index=True, unique=True) email_confirmed = db.Column(db.Boolean) diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 015a422..b248161 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -35,7 +35,8 @@ def edit_account(): else: flash('Erreur lors de la modification', 'error') - return render('account/account.html', form=form) + return render('account/account.html', scripts=["+scripts/entropy.js"], + form=form) @app.route('/compte/reinitialiser', methods=['GET', 'POST']) @guest_only @@ -74,7 +75,8 @@ def reset_password(token): else: flash('Erreur lors de la modification', 'error') - return render('account/reset_password.html', form=form) + return render('account/reset_password.html', + scripts=["+scripts/entropy.js"], form=form) @app.route('/compte/supprimer', methods=['GET', 'POST']) @@ -113,7 +115,8 @@ def register(): send_validation_mail(member.name, member.email) return redirect(url_for('validation') + "?email=" + form.email.data) - return render('account/register.html', title='Register', form=form) + return render('account/register.html', title='Register', + scripts=["+scripts/entropy.js"], form=form) @app.route('/inscription/validation', methods=['GET']) diff --git a/app/routes/admin/account.py b/app/routes/admin/account.py index f8ddc5a..3859d6a 100644 --- a/app/routes/admin/account.py +++ b/app/routes/admin/account.py @@ -107,9 +107,10 @@ def adm_edit_account(user_id): for g in user.groups: groups_owned.add(f"g{g.id}") - return render('admin/edit_account.html', user=user, form=form, - trophy_form=trophy_form, trophies_owned=trophies_owned, - group_form=group_form, groups_owned=groups_owned) + return render('admin/edit_account.html', scripts=["+scripts/entropy.js"], + user=user, form=form, trophy_form=trophy_form, + trophies_owned=trophies_owned, group_form=group_form, + groups_owned=groups_owned) @app.route('/admin/compte//supprimer', methods=['GET', 'POST']) diff --git a/app/static/css/form.css b/app/static/css/form.css index 262f9b4..6fa683f 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -50,6 +50,31 @@ resize: vertical; } +.form progress.entropy { + display: none; /* display with Js enabled */ + width: 100%; margin-top: 5px; + background: var(--background); + border: var(--border); +} +.form progress.entropy.low::-moz-progress-bar { + background: var(--error); +} +.form progress.entropy.low::-webkit-progress-bar { + background: var(--error); +} +.form progress.entropy.medium::-moz-progress-bar { + background: var(--warn); +} +.form progress.entropy.medium::-webkit-progress-bar { + background: var(--warn); +} +.form progress.entropy.high::-moz-progress-bar { + background: var(--ok); +} +.form progress.entropy.high::-webkit-progress-bar { + background: var(--ok); +} + .form input[type="checkbox"], .form input[type="radio"] { display: inline; diff --git a/app/static/scripts/entropy.js b/app/static/scripts/entropy.js new file mode 100644 index 0000000..3465557 --- /dev/null +++ b/app/static/scripts/entropy.js @@ -0,0 +1,42 @@ +function entropy(password) { + var chars = [ + "abcdefghijklmnopqrstuvwxyz", + "ABCDFEGHIJKLMNOPQRSTUVWXYZ", + "0123456789", + " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", // OWASP special chars + "áàâéèêíìîóòôúùûç" + ]; + + used = new Set(); + for(c in password) { + for(k in chars) { + if(chars[k].includes(password[c])) { + used.add(chars[k]); + } + } + } + return Math.log(Math.pow(Array.from(used).join("").length, password.length)) / Math.log(2); +} + +function update_entropy(ev) { + var i = document.querySelector(".entropy").previousElementSibling; + var p = document.querySelector(".entropy"); + var e = entropy(i.value); + + p.classList.remove('low'); + p.classList.remove('medium'); + p.classList.remove('high'); + + if(e < 60) { + p.classList.add('low'); + } else if(e < 100) { + p.classList.add('medium'); + } else { + p.classList.add('high'); + } + + p.value = e; +} + +document.querySelector(".entropy").previousElementSibling.addEventListener('input', update_entropy); +document.querySelector(".entropy").style.display = "block"; diff --git a/app/templates/account/account.html b/app/templates/account/account.html index 0f92345..3b3bc80 100644 --- a/app/templates/account/account.html +++ b/app/templates/account/account.html @@ -30,6 +30,7 @@
{{ form.password.label }} {{ form.password(placeholder='************') }} + {% for error in form.password.errors %} {{ error }} {% endfor %} diff --git a/app/templates/account/register.html b/app/templates/account/register.html index 5d1ca31..6efe74f 100644 --- a/app/templates/account/register.html +++ b/app/templates/account/register.html @@ -24,6 +24,7 @@
{{ form.password.label }} {{ form.password() }} + {% for error in form.password.errors %} {{ error }} {% endfor %} diff --git a/app/templates/account/reset_password.html b/app/templates/account/reset_password.html index a9a94d2..c095437 100644 --- a/app/templates/account/reset_password.html +++ b/app/templates/account/reset_password.html @@ -10,6 +10,7 @@ {{ form.password.label }}
{{ form.password.description }}
{{ form.password() }} + {% for error in form.password.errors %} {{ error }} {% endfor %} diff --git a/app/templates/admin/edit_account.html b/app/templates/admin/edit_account.html index af3f4d0..2f67edf 100644 --- a/app/templates/admin/edit_account.html +++ b/app/templates/admin/edit_account.html @@ -45,6 +45,7 @@ {{ form.password.label }}
{{ form.password.description }}
{{ form.password(placeholder='************') }} + {% for error in form.password.errors %} {{ error }} {% endfor %} diff --git a/app/utils/render.py b/app/utils/render.py index db0f2b1..f2f9f75 100644 --- a/app/utils/render.py +++ b/app/utils/render.py @@ -36,14 +36,12 @@ def render(*args, styles=[], scripts=[], **kwargs): ] for s in styles: - print(s[1:]) if s[0] == '-': styles_.remove(s[1:]) if s[0] == '+': styles_.append(s[1:]) for s in scripts: - print(s[1:]) if s[0] == '-': scripts_.remove(s[1:]) if s[0] == '+': diff --git a/app/utils/validators.py b/app/utils/validators.py index 8820b91..f805537 100644 --- a/app/utils/validators.py +++ b/app/utils/validators.py @@ -1,9 +1,10 @@ from flask_login import current_user from wtforms.validators import ValidationError from PIL import Image -from app.models.users import Member +from app.models.users import Member, User from app.utils.valid_name import valid_name from app.utils.unicode_names import normalize +from math import log import app.utils.ldap as ldap from config import V5Config @@ -14,10 +15,10 @@ def name_valid(form, name): msg = { "too-short": "Le nom d'utilisateur doit faire au moins " - f"{V5Config.USER_NAME_MINLEN} caractères.", + f"{User.USER_NAME_MINLEN} caractères.", "too-long": "Le nom d'utilisateur doit faire au plus " - f"{V5Config.USER_NAME_MAXLEN} caractères.", + f"{User.USER_NAME_MAXLEN} caractères.", "cant-normalize": "Ce nom d'utilisateur contient des caractères interdits. Les " "caractères autorisés sont les lettres, lettres accentuées, " @@ -63,37 +64,26 @@ def password(form, password): if len(password.data) == 0: return - errors = [] - if len(password.data) < V5Config.PASSWORD_MINLEN: - errors.append('Le mot de passe doit faire au moins ' - f'{V5Config.PASSWORD_MINLEN} caractères.') + def entropy(password): + """Estimate entropy of a password, in bits""" + # If you edit this function, please edit accordingly the JS one + # in static/script/entropy.js + chars = [ + "abcdefghijklmnopqrstuvwxyz", + "ABCDFEGHIJKLMNOPQRSTUVWXYZ", + "0123456789", + " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars + "áàâéèêíìîóòôúùûç", + ] + used = set() + for c in password: + for i in chars: + if c in i: + used.add(i) + return log(len(''.join(used)) ** len(password), 2) - checks = set() - for c in password.data: - if c in "abcdefghijklmnopqrstuvwxyz": - checks.add('lower') - elif c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": - checks.add('upper') - elif c in "0123456789": - checks.add('numeric') - else: - checks.add('other') - - missing = [] - if 'lower' not in checks: - missing.append('une minuscule') - if 'upper' not in checks: - missing.append('une majuscule') - if 'numeric' not in checks: - missing.append('un chiffre') - if 'other' not in checks: - missing.append('un caractère spécial') - - if missing != []: - errors.append('Le mot de passe doit aussi contenir ' + ', '.join(missing) + '.') - - if errors != []: - raise ValidationError(' '.join(errors)) + if entropy(password.data) < 60: + raise ValidationError("Mot de passe pas assez complexe") def avatar(form, avatar): diff --git a/config.py b/config.py index f0c0e54..490ee76 100644 --- a/config.py +++ b/config.py @@ -30,11 +30,6 @@ class DefaultConfig(object): FORBIDDEN_USERNAMES = ["admin", "root", "webmaster", "contact"] # Unauthorized message (@priv_required) UNAUTHORIZED_MSG = "Vous n'avez pas l'autorisation d'effectuer cette action !" - # Minimum and maximum user name length - USER_NAME_MINLEN = 3 - USER_NAME_MAXLEN = 32 - # Minimum password length for new users and new passwords - PASSWORD_MINLEN = 10 # Maximum thread name length THREAD_NAME_MAXLEN = 32 # Amount of comments per thread page From e35910ee7666db06f0514dcc6e4ca5ada2c4b941 Mon Sep 17 00:00:00 2001 From: Darks Date: Sun, 26 Jul 2020 16:50:07 +0200 Subject: [PATCH 06/94] config: refactor configuration values - System/host config values stay in `config.py` - Application config values moves in corresponding models - BREAK: AVATAR_FOLDER becomes DATA_FOLDER. Edit your local config if needed --- app/models/privs.py | 5 ++--- app/models/thread.py | 2 ++ app/models/topic.py | 3 +-- app/models/users.py | 7 ++++--- app/routes/account/login.py | 4 ++-- app/routes/forum/index.py | 2 +- app/routes/forum/topic.py | 6 +++--- app/routes/users.py | 7 ++++--- app/utils/valid_name.py | 10 ++++++---- app/utils/validators.py | 4 ++-- config.py | 25 ++----------------------- 11 files changed, 29 insertions(+), 46 deletions(-) diff --git a/app/models/privs.py b/app/models/privs.py index 426f755..692b3ae 100644 --- a/app/models/privs.py +++ b/app/models/privs.py @@ -2,7 +2,6 @@ # models.privs: Database models for groups and privilege management from app import db -from config import V5Config # Privileges are represented by strings (slugs), for instance "post-news" or # "delete-own-posts". Belonging to a group automatically grants a user the @@ -19,7 +18,7 @@ class SpecialPrivilege(db.Model): # Member that is granted the privilege mid = db.Column(db.Integer, db.ForeignKey('member.id'), index=True) # Privilege name - priv = db.Column(db.String(V5Config.PRIVS_MAXLEN)) + priv = db.Column(db.String(64)) def __init__(self, member, priv): self.mid = member.id @@ -85,7 +84,7 @@ class GroupPrivilege(db.Model): id = db.Column(db.Integer, primary_key=True) gid = db.Column(db.Integer, db.ForeignKey('group.id')) - priv = db.Column(db.String(V5Config.PRIVS_MAXLEN)) + priv = db.Column(db.String(64)) def __init__(self, group, priv): self.gid = group.id diff --git a/app/models/thread.py b/app/models/thread.py index bd1236b..1f69f3c 100644 --- a/app/models/thread.py +++ b/app/models/thread.py @@ -15,6 +15,8 @@ class Thread(db.Model): # Other fields populated automatically through relations: # The list of comments (of type Comment) + COMMENTS_PER_PAGE = 20 + def __init__(self): """ Create a empty Thread. Normally threads are not meant to be empty, so diff --git a/app/models/topic.py b/app/models/topic.py index fe18b9d..414d7ca 100644 --- a/app/models/topic.py +++ b/app/models/topic.py @@ -1,6 +1,5 @@ from app import db from app.models.post import Post -from config import V5Config class Topic(Post): __tablename__ = 'topic' @@ -10,7 +9,7 @@ class Topic(Post): id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True) # Topic title - title = db.Column(db.Unicode(V5Config.THREAD_NAME_MAXLEN)) + title = db.Column(db.Unicode(32)) # Parent forum forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False) diff --git a/app/models/users.py b/app/models/users.py index e01567e..2e626e1 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -213,7 +213,7 @@ class Member(User): def set_avatar(self, avatar): # Save old avatar filepath - old_avatar = V5Config.AVATARS_FOLDER + self.avatar + old_avatar = os.path.join(V5Config.DATA_FOLDER, "avatars", self.avatar) # Resize & convert image size = 128, 128 im = Image.open(avatar) @@ -225,7 +225,8 @@ class Member(User): db.session.merge(self) db.session.commit() # Save the new avatar - im.save(V5Config.AVATARS_FOLDER + self.avatar, 'PNG') + im.save(os.path.join(V5Config.DATA_FOLDER, "avatars", self.avatar), + 'PNG') # If nothing has failed, remove old one (allow failure to regularize # exceptional situations like missing avatar or folder migration) try: @@ -458,7 +459,7 @@ class Member(User): # TODO: Trophy "actif" if context in ["on-profile-update", None]: - if isfile(V5Config.AVATARS_FOLDER + self.avatar): + if isfile(os.path.join(V5Config.DATA_FOLDED, "avatar", self.avatar)): self.add_trophy("Artiste") else: self.del_trophy("Artiste") diff --git a/app/routes/account/login.py b/app/routes/account/login.py index 37d1f03..2c4238e 100644 --- a/app/routes/account/login.py +++ b/app/routes/account/login.py @@ -7,7 +7,7 @@ from app.models.users import Member from app.models.privs import Group from app.utils.render import render from app.utils.send_mail import send_validation_mail -from config import V5Config +import datetime @app.route('/connexion', methods=['GET', 'POST']) @@ -46,7 +46,7 @@ def login(): # Login & update time-based trophies login_user(member, remember=form.remember_me.data, - duration=V5Config.REMEMBER_COOKIE_DURATION) + duration=datetime.timedelta(days=7)) member.update_trophies("on-login") # Redirect safely (https://huit.re/open-redirect) diff --git a/app/routes/forum/index.py b/app/routes/forum/index.py index 6c46298..f87910d 100644 --- a/app/routes/forum/index.py +++ b/app/routes/forum/index.py @@ -58,7 +58,7 @@ def forum_page(f): # Update member's xp and trophies if current_user.is_authenticated: - current_user.add_xp(V5Config.XP_POINTS['topic']) + current_user.add_xp(2) # 2 points for a topic current_user.update_trophies('new-post') flash('Le sujet a bien été créé', 'ok') diff --git a/app/routes/forum/topic.py b/app/routes/forum/topic.py index ac264be..bdd4ab6 100644 --- a/app/routes/forum/topic.py +++ b/app/routes/forum/topic.py @@ -39,7 +39,7 @@ def forum_topic(f, page): # Update member's xp and trophies if current_user.is_authenticated: - current_user.add_xp(V5Config.XP_POINTS['comment']) + current_user.add_xp(1) # 1 point for a comment current_user.update_trophies('new-post') flash('Message envoyé', 'ok') @@ -54,9 +54,9 @@ def forum_topic(f, page): if page == -1: page = (t.thread.comments.count() - 1) \ - // V5Config.COMMENTS_PER_PAGE + 1 + // Thread.COMMENTS_PER_PAGE + 1 comments = t.thread.comments.paginate(page, - V5Config.COMMENTS_PER_PAGE, True) + Thread.COMMENTS_PER_PAGE, True) return render('/forum/topic.html', t=t, form=form, comments=comments) diff --git a/app/routes/users.py b/app/routes/users.py index 72a57b0..5f8a91c 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -1,4 +1,4 @@ -from flask import redirect, url_for, send_from_directory +from flask import redirect, url_for, send_file from werkzeug.utils import secure_filename import os.path from app import app @@ -25,6 +25,7 @@ def user_by_id(user_id): @app.route('/avatar/') def avatar(filename): filename = secure_filename(filename) # No h4ckers allowed - if os.path.isfile(V5Config.AVATARS_FOLDER + filename): - return send_from_directory(V5Config.AVATARS_FOLDER, filename) + filepath = os.path.join(V5Config.DATA_FOLDER, "avatars", filename) + if os.path.isfile(filepath): + return send_file(filepath) return redirect(url_for('static', filename='images/default_avatar.png')) diff --git a/app/utils/valid_name.py b/app/utils/valid_name.py index f3d6642..158cc40 100644 --- a/app/utils/valid_name.py +++ b/app/utils/valid_name.py @@ -1,5 +1,5 @@ -from config import V5Config from app.utils.unicode_names import normalize +from app.models.users import User import re def valid_name(name, msg=False): @@ -18,11 +18,13 @@ def valid_name(name, msg=False): errors = [] + FORBIDDEN_USERNAMES = ["admin", "root", "webmaster", "contact"] + # Rule 1 - if len(name) < V5Config.USER_NAME_MINLEN: + if len(name) < User.NAME_MINLEN: errors.append("too-short") - if len(name) > V5Config.USER_NAME_MAXLEN: + if len(name) > User.NAME_MAXLEN: errors.append("too-long") # Rule 2 @@ -37,7 +39,7 @@ def valid_name(name, msg=False): errors.append("no-letter") # Rule 4 - if normalized_name in V5Config.FORBIDDEN_USERNAMES: + if normalized_name in FORBIDDEN_USERNAMES: errors.append("forbidden") return True if errors == [] else errors diff --git a/app/utils/validators.py b/app/utils/validators.py index f805537..73f2c2f 100644 --- a/app/utils/validators.py +++ b/app/utils/validators.py @@ -15,10 +15,10 @@ def name_valid(form, name): msg = { "too-short": "Le nom d'utilisateur doit faire au moins " - f"{User.USER_NAME_MINLEN} caractères.", + f"{User.NAME_MINLEN} caractères.", "too-long": "Le nom d'utilisateur doit faire au plus " - f"{User.USER_NAME_MAXLEN} caractères.", + f"{User.NAME_MAXLEN} caractères.", "cant-normalize": "Ce nom d'utilisateur contient des caractères interdits. Les " "caractères autorisés sont les lettres, lettres accentuées, " diff --git a/config.py b/config.py index 490ee76..ae5df4d 100644 --- a/config.py +++ b/config.py @@ -1,5 +1,4 @@ import os -import datetime try: from local_config import LocalConfig @@ -24,26 +23,6 @@ class DefaultConfig(object): """Every value here can be overrided in the local_config.py class""" # Domain DOMAIN = "v5.planet-casio.com" - # Length allocated to privilege names (slugs) - PRIVS_MAXLEN = 64 - # Forbidden user names - FORBIDDEN_USERNAMES = ["admin", "root", "webmaster", "contact"] - # Unauthorized message (@priv_required) - UNAUTHORIZED_MSG = "Vous n'avez pas l'autorisation d'effectuer cette action !" - # Maximum thread name length - THREAD_NAME_MAXLEN = 32 - # Amount of comments per thread page - COMMENTS_PER_PAGE = 20 - # Remember-me cookie duration time - REMEMBER_COOKIE_DURATION = datetime.timedelta(days=7) - # XP points for content posting (and deletion) - XP_POINTS = { - 'topic': 2, - 'program': 5, - 'tutorial': 5, - 'comment': 1, - 'contest': 10, - } # Database name DB_NAME = "pcv5" # LDAP usage @@ -53,8 +32,8 @@ class DefaultConfig(object): LDAP_ORGANIZATION = "o=planet-casio" # Secret key used to authenticate tokens. **USE YOURS!** SECRET_KEY = "a-random-secret-key" - # Avatars folder - AVATARS_FOLDER = '/avatar/folder/' + # Uploaded data folder + DATA_FOLDER = '/var/www/uploads' # Enable guest post ENABLE_GUEST_POST = True # Disable email confimation From b79bac1fc4cbbe25392861448c0f03960bd6ac90 Mon Sep 17 00:00:00 2001 From: Darks Date: Sun, 26 Jul 2020 16:55:12 +0200 Subject: [PATCH 07/94] config: fix bug before it appears --- app/models/users.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/users.py b/app/models/users.py index 2e626e1..7b7cfed 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -459,7 +459,8 @@ class Member(User): # TODO: Trophy "actif" if context in ["on-profile-update", None]: - if isfile(os.path.join(V5Config.DATA_FOLDED, "avatar", self.avatar)): + if isfile(os.path.join( + V5Config.DATA_FOLDER, "avatars", self.avatar)): self.add_trophy("Artiste") else: self.del_trophy("Artiste") From b108ce4cfe1c193f0b9c74639cbd376ce069aed3 Mon Sep 17 00:00:00 2001 From: Darks Date: Wed, 29 Jul 2020 00:57:06 +0200 Subject: [PATCH 08/94] titles: add displayed title (#65) - with forms for user and admins --- app/forms/account.py | 16 ++++++++++ app/models/users.py | 8 ++++- app/routes/account/account.py | 5 ++++ app/routes/admin/account.py | 7 ++++- app/static/css/form.css | 4 +++ app/static/css/widgets.css | 1 + app/templates/account/account.html | 7 +++++ app/templates/admin/edit_account.html | 7 +++++ app/templates/widgets/user.html | 2 +- app/utils/validators.py | 19 ++++++++++++ ...d2eaf0413_add_displayed_title_to_member.py | 30 +++++++++++++++++++ 11 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 migrations/versions/001d2eaf0413_add_displayed_title_to_member.py diff --git a/app/forms/account.py b/app/forms/account.py index 227fafc..3115ddb 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -110,6 +110,14 @@ class UpdateAccountForm(FlaskForm): Optional(), ] ) + title = SelectField( + 'Titre', + coerce=int, + validators=[ + Optional(), + vd.own_title, + ] + ) newsletter = BooleanField( 'Inscription à la newsletter', description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.', @@ -226,6 +234,14 @@ class AdminUpdateAccountForm(FlaskForm): Optional(), ], ) + title = SelectField( + 'Titre', + coerce=int, + validators=[ + Optional(), + # Admin can set any title to any member! + ] + ) newsletter = BooleanField( 'Inscription à la newsletter', description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.', diff --git a/app/models/users.py b/app/models/users.py index 7b7cfed..63493c2 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -7,7 +7,7 @@ from PIL import Image from app import app, db from app.models.privs import SpecialPrivilege, Group, GroupMember, \ GroupPrivilege -from app.models.trophies import Trophy, TrophyMember +from app.models.trophies import Trophy, TrophyMember, Title from app.models.notification import Notification import app.utils.unicode_names as unicode_names from app.utils.notify import notify @@ -105,6 +105,10 @@ class Member(User): signature = db.Column(db.UnicodeText) birthday = db.Column(db.Date) + # Displayed title, if set + title_id = db.Column(db.Integer, db.ForeignKey('title.id'), nullable=True) + title = db.relationship('Title', foreign_keys=title_id) + # Settings newsletter = db.Column(db.Boolean, default=False) @@ -204,6 +208,8 @@ class Member(User): self.newsletter = data["newsletter"] if "avatar" in data: self.set_avatar(data["avatar"]) + if "title" in data: + self.title = Title.query.get(data["title"]) # For admins only if "email_confirmed" in data: diff --git a/app/routes/account/account.py b/app/routes/account/account.py index b248161..8a7e378 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -4,6 +4,7 @@ from app import app, db from app.forms.account import UpdateAccountForm, RegistrationForm, \ DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm from app.models.users import Member +from app.models.trophies import Title from app.utils.render import render from app.utils.send_mail import send_validation_mail, send_reset_password_mail from app.utils.priv_required import guest_only @@ -16,6 +17,9 @@ from config import V5Config @login_required def edit_account(): form = UpdateAccountForm() + titles = [(t.id, t.name) for t in current_user.trophies if isinstance(t, Title)] + titles.insert(0, (-1, "Membre")) + form.title.choices = titles if form.submit.data: if form.validate_on_submit(): current_user.update( @@ -25,6 +29,7 @@ def edit_account(): birthday=form.birthday.data, signature=form.signature.data, bio=form.biography.data, + title=form.title.data, newsletter=form.newsletter.data ) db.session.merge(current_user) diff --git a/app/routes/admin/account.py b/app/routes/admin/account.py index 3859d6a..1abd8c0 100644 --- a/app/routes/admin/account.py +++ b/app/routes/admin/account.py @@ -3,7 +3,7 @@ from flask_login import current_user from wtforms import BooleanField from app.utils.priv_required import priv_required from app.models.users import Member -from app.models.trophies import Trophy +from app.models.trophies import Trophy, Title from app.models.privs import Group from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \ AdminAccountEditTrophyForm, AdminAccountEditGroupForm @@ -35,6 +35,10 @@ def adm_edit_account(user_id): setattr(GroupForm, "user_groups", [f'g{g.id}' for g in user.groups]) group_form = GroupForm(prefix="group") + titles = [(t.id, t.name) for t in Title.query.all()] + titles.insert(0, (-1, "Membre")) + form.title.choices = titles + if form.submit.data: if form.validate_on_submit(): newname = form.username.data @@ -52,6 +56,7 @@ def adm_edit_account(user_id): password=form.password.data or None, birthday=form.birthday.data, signature=form.signature.data, + title=form.title.data, bio=form.biography.data, newsletter=form.newsletter.data, xp=form.xp.data or None, diff --git a/app/static/css/form.css b/app/static/css/form.css index 6fa683f..36137a3 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -26,6 +26,7 @@ .form input[type='password'], .form input[type='search'], .form textarea, +.form select, .trophies-panel > div { display: block; width: 100%; padding: 6px 8px; @@ -49,6 +50,9 @@ max-width: 100%; resize: vertical; } +.form select { + width: auto; +} .form progress.entropy { display: none; /* display with Js enabled */ diff --git a/app/static/css/widgets.css b/app/static/css/widgets.css index 8d06ef7..399819f 100644 --- a/app/static/css/widgets.css +++ b/app/static/css/widgets.css @@ -3,6 +3,7 @@ .profile { display: flex; align-items: center; + width: 265px; } .profile-avatar { width: 128px; diff --git a/app/templates/account/account.html b/app/templates/account/account.html index 3b3bc80..03981da 100644 --- a/app/templates/account/account.html +++ b/app/templates/account/account.html @@ -51,6 +51,13 @@

À propos

+
+ {{ form.title.label }} + {{ form.title }} + {% for error in form.title.errors %} + {{ error }} + {% endfor %} +
{{ form.birthday.label }} {{ form.birthday(value=current_user.birthday) }} diff --git a/app/templates/admin/edit_account.html b/app/templates/admin/edit_account.html index 2f67edf..df17371 100644 --- a/app/templates/admin/edit_account.html +++ b/app/templates/admin/edit_account.html @@ -61,6 +61,13 @@

À propos

+
+ {{ form.title.label }} + {{ form.title }} + {% for error in form.title.errors %} + {{ error }} + {% endfor %} +
{{ form.birthday.label }} {{ form.birthday(value=user.birthday) }} diff --git a/app/templates/widgets/user.html b/app/templates/widgets/user.html index 74692bb..e0df4d6 100644 --- a/app/templates/widgets/user.html +++ b/app/templates/widgets/user.html @@ -4,7 +4,7 @@ Avatar de {{ user.name }}
-
Membre
+
{{ user.title.name if user.title else "Membre" }}
Niveau {{ user.level[0] }} ({{ user.xp }})
N{{ user.level[0] }} ({{ user.xp }})
diff --git a/app/utils/validators.py b/app/utils/validators.py index 73f2c2f..c214aac 100644 --- a/app/utils/validators.py +++ b/app/utils/validators.py @@ -2,9 +2,11 @@ from flask_login import current_user from wtforms.validators import ValidationError from PIL import Image from app.models.users import Member, User +from app.models.trophies import Title from app.utils.valid_name import valid_name from app.utils.unicode_names import normalize from math import log +from werkzeug.exceptions import NotFound import app.utils.ldap as ldap from config import V5Config @@ -117,3 +119,20 @@ def id_exists(object): def css(form, css): """Check if input is valid and sane CSS""" pass + +def own_title(form, title): + # Everyone can use "Member" + if title.data == -1: + return True + + try: + t = Title.query.get_or_404(title.data) + except NotFound: + return False + except ValueError: + return False + + if t in current_user.trophies: + return True + else: + return False diff --git a/migrations/versions/001d2eaf0413_add_displayed_title_to_member.py b/migrations/versions/001d2eaf0413_add_displayed_title_to_member.py new file mode 100644 index 0000000..68cd25f --- /dev/null +++ b/migrations/versions/001d2eaf0413_add_displayed_title_to_member.py @@ -0,0 +1,30 @@ +"""Add displayed title to Member + +Revision ID: 001d2eaf0413 +Revises: acf72cf31eea +Create Date: 2020-07-29 00:13:27.775769 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '001d2eaf0413' +down_revision = 'acf72cf31eea' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('member', sa.Column('title_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'member', 'title', ['title_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'member', type_='foreignkey') + op.drop_column('member', 'title_id') + # ### end Alembic commands ### From a4efe29a7d01ee73a1c19f87076092e646273cf5 Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 30 Jul 2020 14:49:18 +0200 Subject: [PATCH 09/94] __init__.py: refactor registration of assets --- app/__init__.py | 38 ++++++++----------------------------- app/models/__init__.py | 5 +++++ app/processors/__init__.py | 5 +++++ app/processors/stats.py | 8 ++++++++ app/processors/utilities.py | 2 ++ app/routes/__init__.py | 6 ++++++ app/utils/__init__.py | 5 +++++ 7 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 app/models/__init__.py create mode 100644 app/processors/__init__.py create mode 100644 app/processors/stats.py create mode 100644 app/routes/__init__.py create mode 100644 app/utils/__init__.py diff --git a/app/__init__.py b/app/__init__.py index 01ca7d3..a231147 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,11 +1,9 @@ -from flask import Flask, g +from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager from flask_mail import Mail from config import Config -import time -import slugify app = Flask(__name__) app.config.from_object(Config) @@ -22,36 +20,16 @@ login = LoginManager(app) login.login_view = 'login' login.login_message = "Veuillez vous authentifier avant de continuer." +# Register converters (needed for routing) from app.utils.converters import * app.url_map.converters['forum'] = ForumConverter app.url_map.converters['topicpage'] = TopicPageConverter -@app.before_request -def request_time(): - g.request_start_time = time.time() - g.request_time = lambda: "%.5fs" % (time.time() - g.request_start_time) +# Register routes +from app import routes +# Register utils +from app import utils -from app.processors.menu import menu_processor -from app.processors.utilities import utilities_processor - -from app import models # IDK why this is here, but it works -from app.models.comment import Comment -from app.models.thread import Thread -from app.models.forum import Forum -from app.models.topic import Topic -from app.models.notification import Notification - -from app.routes import index, search, users, tools # To load routes at initialization -from app.routes.account import login, account, notification -from app.routes.admin import index, groups, account, trophies, forums -from app.routes.forum import index, topic - -from app.utils import pluralize # To use pluralize into the templates -from app.utils import date -from app.utils import is_title - -# Add slugify into the available functions in every template -app.jinja_env.globals.update( - slugify=slugify.slugify -) +# Register processors +from app import processors diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..9849a04 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,5 @@ +from app.models.comment import Comment +from app.models.thread import Thread +from app.models.forum import Forum +from app.models.topic import Topic +from app.models.notification import Notification diff --git a/app/processors/__init__.py b/app/processors/__init__.py new file mode 100644 index 0000000..d29ee40 --- /dev/null +++ b/app/processors/__init__.py @@ -0,0 +1,5 @@ +# Register processors here + +from app.processors.menu import menu_processor +from app.processors.utilities import utilities_processor +from app.processors.stats import request_time diff --git a/app/processors/stats.py b/app/processors/stats.py new file mode 100644 index 0000000..0a2c893 --- /dev/null +++ b/app/processors/stats.py @@ -0,0 +1,8 @@ +from flask import g +from time import time +from app import app + +@app.before_request +def request_time(): + g.request_start_time = time() + g.request_time = lambda: "%.5fs" % (time() - g.request_start_time) diff --git a/app/processors/utilities.py b/app/processors/utilities.py index 1fa5e62..ad6dbde 100644 --- a/app/processors/utilities.py +++ b/app/processors/utilities.py @@ -1,6 +1,7 @@ from app import app from flask import url_for from config import V5Config +from slugify import slugify @app.context_processor def utilities_processor(): @@ -10,4 +11,5 @@ def utilities_processor(): # enumerate=enumerate, _url_for = lambda route, args, **other: url_for(route, **args, **other), V5Config = V5Config, + slugify=slugify, ) diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..9bc4576 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,6 @@ +# Register routes here + +from app.routes import index, search, users, tools +from app.routes.account import login, account, notification +from app.routes.admin import index, groups, account, trophies, forums +from app.routes.forum import index, topic diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..03b88c2 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1,5 @@ +# Register utils here + +from app.utils import pluralize +from app.utils import date +from app.utils import is_title From a2767c43439487d3928d500d7584516bc14d04d5 Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 30 Jul 2020 14:50:08 +0200 Subject: [PATCH 10/94] widget_user: add title style if there is one --- app/templates/widgets/user.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/templates/widgets/user.html b/app/templates/widgets/user.html index e0df4d6..8866930 100644 --- a/app/templates/widgets/user.html +++ b/app/templates/widgets/user.html @@ -4,7 +4,11 @@ Avatar de {{ user.name }}
-
{{ user.title.name if user.title else "Membre" }}
+ {% if user.title %} +
{{ user.title.name }}
+ {% else %} +
Membre
+ {% endif %}
Niveau {{ user.level[0] }} ({{ user.xp }})
N{{ user.level[0] }} ({{ user.xp }})
From ea39b5d787ffd176615310749b1135e8fca0e0ff Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 1 Aug 2020 15:09:07 +0200 Subject: [PATCH 11/94] templates: fix indent and tags of forum index --- app/templates/forum/index.html | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/templates/forum/index.html b/app/templates/forum/index.html index 6e6d52e..619c362 100644 --- a/app/templates/forum/index.html +++ b/app/templates/forum/index.html @@ -15,25 +15,25 @@ .

- {% if main_forum == None %} + {% if main_forum == None %}

Il n'y a aucun forum.

- {% else %} + {% else %} - {% for l1 in main_forum.sub_forums %} + {% for l1 in main_forum.sub_forums %} - + - {% if l1.sub_forums == [] %} - - - - {% endif %} + {% if l1.sub_forums == [] %} + + + + {% endif %} - {% for l2 in l1.sub_forums %} - - - - {% endfor %} + {% for l2 in l1.sub_forums %} + + + + {% endfor %}
{{ l1.name }}Nombre de sujets
{{ l1.name }}Nombre de sujets
{{ l1.name }}{{ l1.topics | length }}
{{ l1.descr }}
{{ l1.name }}{{ l1.topics | length }}
{{ l1.descr }}
{{ l2.name }}{{ l2.topics | length }}
{{ l2.descr }}
{{ l2.name }}{{ l2.topics | length }}
{{ l2.descr }}
{% endfor %} From d3f137b92d0b79614c8ac8aea4618a61f11f76bb Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 1 Aug 2020 15:10:46 +0200 Subject: [PATCH 12/94] programs: add models for programs and topic promotions (#20) Not tested yet since there is a lack of tools to manipulate comments, topics and programs. The /programmes routes is used to display a list of all programs, temporarily. --- app/models/__init__.py | 1 + app/models/program.py | 43 +++++++++++++++++++ app/models/topic.py | 15 +++++-- app/routes/__init__.py | 1 + app/routes/programs/index.py | 8 ++++ app/templates/programs/index.html | 21 +++++++++ ...61fa6af4e_add_a_base_model_for_programs.py | 35 +++++++++++++++ ...510_add_promotion_information_to_topics.py | 30 +++++++++++++ 8 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 app/models/program.py create mode 100644 app/routes/programs/index.py create mode 100644 app/templates/programs/index.html create mode 100644 migrations/versions/c5561fa6af4e_add_a_base_model_for_programs.py create mode 100644 migrations/versions/c7779a558510_add_promotion_information_to_topics.py diff --git a/app/models/__init__.py b/app/models/__init__.py index 9849a04..fd484d2 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -3,3 +3,4 @@ from app.models.thread import Thread from app.models.forum import Forum from app.models.topic import Topic from app.models.notification import Notification +from app.models.program import Program diff --git a/app/models/program.py b/app/models/program.py new file mode 100644 index 0000000..b16a484 --- /dev/null +++ b/app/models/program.py @@ -0,0 +1,43 @@ +from app import db +from app.models.post import Post + +class Program(Post): + __tablename__ = 'program' + __mapper_args__ = {'polymorphic_identity': __tablename__} + + # ID of underlying Post object + id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True) + + # Program name + title = db.Column(db.Unicode(128)) + + # TODO: Category (games/utilities/lessons) + # TODO: Tags + # TODO: Compatible calculator models + + thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) + thread = db.relationship('Thread', foreign_keys=thread_id) + + # TODO: Number of views, statistics, attached files, etc + + def __init__(self, author, title, thread): + """ + Create a Program. + + Arguments: + author -- post author (User, though only Members can post) + title -- program title (unicode string) + thread -- discussion thread attached to the topic + """ + + Post.__init__(self, author) + self.title = title + self.thread = thread + + @staticmethod + def from_topic(topic): + p = Program(topic.author, topic.title, topic.thread) + topic.promotion = p + + def __repr__(self): + return f'' diff --git a/app/models/topic.py b/app/models/topic.py index 414d7ca..2c656d3 100644 --- a/app/models/topic.py +++ b/app/models/topic.py @@ -3,13 +3,22 @@ from app.models.post import Post class Topic(Post): __tablename__ = 'topic' - __mapper_args__ = {'polymorphic_identity': __tablename__} - # ID of the underlying [Post] object id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True) + __mapper_args__ = { + 'polymorphic_identity': __tablename__, + 'primary_key': id, + 'inherit_condition': id == Post.id + } + + # Post that the topic was promoted into. If this is not None, then the + # topic was published into a project and a redirection should be emitted + promotion_id = db.Column(db.Integer,db.ForeignKey('post.id'),nullable=True) + promotion = db.relationship('Post', foreign_keys=promotion_id) + # Topic title - title = db.Column(db.Unicode(32)) + title = db.Column(db.Unicode(128)) # Parent forum forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False) diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 9bc4576..de98e92 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -4,3 +4,4 @@ from app.routes import index, search, users, tools from app.routes.account import login, account, notification from app.routes.admin import index, groups, account, trophies, forums from app.routes.forum import index, topic +from app.routes.programs import index diff --git a/app/routes/programs/index.py b/app/routes/programs/index.py new file mode 100644 index 0000000..7d267c1 --- /dev/null +++ b/app/routes/programs/index.py @@ -0,0 +1,8 @@ +from app import app, db +from app.models.program import Program +from app.utils.render import render + +@app.route('/programmes') +def program_index(): + programs = Program.query.all() + return render('/programs/index.html') diff --git a/app/templates/programs/index.html b/app/templates/programs/index.html new file mode 100644 index 0000000..f141ddf --- /dev/null +++ b/app/templates/programs/index.html @@ -0,0 +1,21 @@ +{% extends "base/base.html" %} + +{% block title %} +

Programmes de Planète Casio

+{% endblock %} + +{% block content %} +
+

Tous les programmes

+ + + + {% for p in programs %} + + + + + {% endfor %} +
IDNomAuteurPublié le
{{ p.id }}{{ p.name }}{{ p.author.name }}{{ p.date_created }}
+
+{% endblock %} diff --git a/migrations/versions/c5561fa6af4e_add_a_base_model_for_programs.py b/migrations/versions/c5561fa6af4e_add_a_base_model_for_programs.py new file mode 100644 index 0000000..632f980 --- /dev/null +++ b/migrations/versions/c5561fa6af4e_add_a_base_model_for_programs.py @@ -0,0 +1,35 @@ +"""add a base model for programs + +Revision ID: c5561fa6af4e +Revises: c7779a558510 +Create Date: 2020-08-01 14:52:52.878440 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c5561fa6af4e' +down_revision = 'c7779a558510' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('program', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.Unicode(length=128), nullable=True), + sa.Column('thread_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['id'], ['post.id'], ), + sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('program') + # ### end Alembic commands ### diff --git a/migrations/versions/c7779a558510_add_promotion_information_to_topics.py b/migrations/versions/c7779a558510_add_promotion_information_to_topics.py new file mode 100644 index 0000000..40d8783 --- /dev/null +++ b/migrations/versions/c7779a558510_add_promotion_information_to_topics.py @@ -0,0 +1,30 @@ +"""add promotion information to topics + +Revision ID: c7779a558510 +Revises: 001d2eaf0413 +Create Date: 2020-08-01 11:27:23.298821 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c7779a558510' +down_revision = '001d2eaf0413' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('topic', sa.Column('promotion_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'topic', 'post', ['promotion_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'topic', type_='foreignkey') + op.drop_column('topic', 'promotion_id') + # ### end Alembic commands ### From 8a95d43eb013575544e967164ae7a9b4283b8837 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 1 Aug 2020 15:28:36 +0200 Subject: [PATCH 13/94] templates: represent actual level in XP bar in user widget This is much more useful in determining a user's activity than the current level progression. Since the XP bar is the second most visible item apart from the avatar, it's more relevant. --- app/static/css/theme.css | 1 + app/static/css/widgets.css | 7 +++++++ app/templates/widgets/user.html | 6 +++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/static/css/theme.css b/app/static/css/theme.css index cd2756e..e1f1954 100644 --- a/app/static/css/theme.css +++ b/app/static/css/theme.css @@ -105,5 +105,6 @@ footer { --background: #e0e0e0; --border: 1px solid #c0c0c0; --background-xp: #f85555; + --background-xp-100: #d03333; --border-xp: 1px solid #d03333; } diff --git a/app/static/css/widgets.css b/app/static/css/widgets.css index 399819f..6e7f6c8 100644 --- a/app/static/css/widgets.css +++ b/app/static/css/widgets.css @@ -31,12 +31,19 @@ background: var(--background); border: var(--border); } +.profile-xp-100 { + background: var(--background-xp); + border: var(--border-xp); +} .profile-xp div { height: 10px; background: var(--background-xp); border: var(--border-xp); margin: -1px; } +.profile-xp-100 div { + background: var(--background-xp-100); +} .profile.guest { flex-direction: column; diff --git a/app/templates/widgets/user.html b/app/templates/widgets/user.html index 8866930..0b50e23 100644 --- a/app/templates/widgets/user.html +++ b/app/templates/widgets/user.html @@ -11,7 +11,11 @@ {% endif %}
Niveau {{ user.level[0] }} ({{ user.xp }})
N{{ user.level[0] }} ({{ user.xp }})
-
+ {% if user.level[0] <= 100 %} +
+ {% else %} +
+ {% endif %}
{% else %} From 79600e85980619213e3c4b84fb4c9ab7d830938e Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 1 Aug 2020 15:44:47 +0200 Subject: [PATCH 14/94] templates: add a dynamic date format More reader-friendly (in French at least). --- app/templates/forum/topic.html | 2 +- app/utils/date.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/templates/forum/topic.html b/app/templates/forum/topic.html index 8ea0d4e..89052f0 100644 --- a/app/templates/forum/topic.html +++ b/app/templates/forum/topic.html @@ -26,7 +26,7 @@
{% if c.date_created != c.date_modified %} Posté le {{ c.date_created|date }} (Modifié le {{ c.date_modified|date }}) {% else %} - Posté le {{ c.date_created|date }} + Posté le {{ c.date_created|dyndate }} {% endif %} | # | Modifier diff --git a/app/utils/date.py b/app/utils/date.py index 3bf0a85..22b140c 100644 --- a/app/utils/date.py +++ b/app/utils/date.py @@ -1,4 +1,5 @@ from app import app +from datetime import datetime @app.template_filter('date') def filter_date(date, format="%Y-%m-%d à %H:%M"): @@ -6,4 +7,20 @@ def filter_date(date, format="%Y-%m-%d à %H:%M"): Print a date in a human-readable format. """ + if format == "dynamic": + d = "1er" if date.day == 1 else int(date.day) + m = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", + "Août", "Septembre", "Octobre", "Novembre","Décembre"] \ + [date.month - 1] + + # Omit current year in the dynamic format + if date.year == datetime.now().year: + format = f"{d} {m} à %H:%M" + else: + format = f"{d} {m} %Y à %H:%M" + return date.strftime(format) + +@app.template_filter('dyndate') +def filter_dyndate(date): + return filter_date(date, format="dynamic") From 7e114691835080ed43001df39ab38fa353f515b3 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 1 Aug 2020 17:25:08 +0200 Subject: [PATCH 15/94] forum: add basic comment edition with preview Limitations: * Works only for registered users * Does not redirect to the proper MainPost page * Does not check permissions --- app/forms/forum.py | 9 ++++++ app/routes/__init__.py | 1 + app/routes/account/account.py | 2 +- app/routes/posts/edit.py | 34 ++++++++++++++++++++++ app/templates/forum/edit_comment.html | 41 +++++++++++++++++++++++++++ app/templates/forum/topic.html | 2 +- 6 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 app/routes/posts/edit.py create mode 100644 app/templates/forum/edit_comment.html diff --git a/app/forms/forum.py b/app/forms/forum.py index 7162415..2000283 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -21,3 +21,12 @@ class CommentForm(FlaskForm): class AnonymousCommentForm(CommentForm): pseudo = StringField('Pseudo', validators=[DataRequired(), vd.name_valid, vd.name_available]) + + +class CommentEditForm(CommentForm): + submit = SubmitField('Valider les modifications') + preview = SubmitField('Prévisualiser') + +class AnonymousCommentEditForm(CommentEditForm): + pseudo = StringField('Pseudo', + validators=[DataRequired(), vd.name_valid, vd.name_available]) diff --git a/app/routes/__init__.py b/app/routes/__init__.py index de98e92..ff0d0d9 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -5,3 +5,4 @@ from app.routes.account import login, account, notification from app.routes.admin import index, groups, account, trophies, forums from app.routes.forum import index, topic from app.routes.programs import index +from app.routes.posts import edit diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 8a7e378..18fad37 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -130,7 +130,7 @@ def validation(): try: mail = request.args['email'] except Exception as e: - print("Error: {e}") + print(f"Error: {e}") abort(404) if current_user.is_authenticated: diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py new file mode 100644 index 0000000..5b3c1b9 --- /dev/null +++ b/app/routes/posts/edit.py @@ -0,0 +1,34 @@ +from app import app, db +from app.models.post import Post +from app.utils.render import render +from app.forms.forum import CommentEditForm, AnonymousCommentEditForm +from flask import redirect, url_for, abort +from flask_login import login_required, current_user + +@app.route('/post/', methods=['GET','POST']) +# TODO: Allow guest edit of posts +@login_required +def edit_post(postid): + p = Post.query.filter_by(id=postid).first_or_404() + + # TODO: Check whether privileged user has access to board + if p.author != current_user and not current_user.priv("edit-posts"): + abort(403) + + if p.type == "comment": + form = CommentEditForm() + + if form.validate_on_submit(): + p.text = form.message.data + + if form.submit.data: + db.session.add(p) + db.session.commit() + + # TODO: Proper redirection + return redirect(url_for('index')) + + form.message.data = p.text + return render('forum/edit_comment.html', comment=p, form=form) + else: + abort(404) diff --git a/app/templates/forum/edit_comment.html b/app/templates/forum/edit_comment.html new file mode 100644 index 0000000..6127823 --- /dev/null +++ b/app/templates/forum/edit_comment.html @@ -0,0 +1,41 @@ +{% extends "base/base.html" %} +{% import "widgets/editor.html" as widget_editor %} +{% import "widgets/user.html" as widget_user %} + +{% block title %} +Forum de Planète Casio » Édition de commentaire +{% endblock %} + +{% block content %} +
+

Édition de commentaire

+ +

Commentaire actuel

+ + + + + +
{{ widget_user.profile(comment.author) }}
{{ comment.text }}
+ +
+

Nouveau commentaire

+
+ {{ form.hidden_tag() }} + + {% if form.pseudo %} + {{ form.pseudo.label }} + {{ form.pseudo }} + {% for error in form.pseudo.errors %} + {{ error }} + {% endfor %} + {% endif %} + + {{ widget_editor.text_editor(form.message, label=False) }} + +
{{ form.preview(class_='bg-ok') }}
+
{{ form.submit(class_='bg-ok') }}
+
+
+
+{% endblock %} diff --git a/app/templates/forum/topic.html b/app/templates/forum/topic.html index 89052f0..d2b6914 100644 --- a/app/templates/forum/topic.html +++ b/app/templates/forum/topic.html @@ -29,7 +29,7 @@ Posté le {{ c.date_created|dyndate }} {% endif %} | # - | Modifier + | Modifier | Supprimer
From 56a584c53540f9a01f9280ac8bbb1156b71cfcd6 Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 1 Aug 2020 21:26:06 +0200 Subject: [PATCH 16/94] =?UTF-8?q?attachments:=20added=20attachments=20syst?= =?UTF-8?q?em=20Still=20need=20some=20work=20on=20it=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 4 +- app/data/groups.yaml | 7 +-- app/forms/forum.py | 30 ++++++----- app/models/attachment.py | 44 +++++++++++----- app/routes/__init__.py | 4 +- app/routes/admin/attachments.py | 13 +++++ app/routes/development.py | 25 +++++++++ app/routes/forum/index.py | 23 +++++++-- app/routes/forum/topic.py | 14 +++++ app/routes/users.py | 8 --- app/templates/admin/attachments.html | 45 ++++++++++++++++ app/templates/admin/index.html | 5 +- app/templates/forum/forum.html | 5 ++ app/templates/forum/topic.html | 9 +++- app/templates/widgets/attachments.html | 16 ++++++ app/utils/__init__.py | 5 -- app/utils/filesize.py | 9 ++++ app/utils/filters/__init__.py | 3 ++ app/utils/{ => filters}/date.py | 0 app/utils/{ => filters}/is_title.py | 0 app/utils/{ => filters}/pluralize.py | 0 .../{validators.py => validators/__init__.py} | 12 ++--- app/utils/validators/file.py | 51 +++++++++++++++++++ .../cd4868f312c5_added_attachments.py | 35 +++++++++++++ 24 files changed, 304 insertions(+), 63 deletions(-) create mode 100644 app/routes/admin/attachments.py create mode 100644 app/routes/development.py create mode 100644 app/templates/admin/attachments.html create mode 100644 app/templates/widgets/attachments.html delete mode 100644 app/utils/__init__.py create mode 100644 app/utils/filesize.py create mode 100644 app/utils/filters/__init__.py rename app/utils/{ => filters}/date.py (100%) rename app/utils/{ => filters}/is_title.py (100%) rename app/utils/{ => filters}/pluralize.py (100%) rename app/utils/{validators.py => validators/__init__.py} (98%) create mode 100644 app/utils/validators/file.py create mode 100644 migrations/versions/cd4868f312c5_added_attachments.py diff --git a/app/__init__.py b/app/__init__.py index a231147..aaf0713 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -28,8 +28,8 @@ app.url_map.converters['topicpage'] = TopicPageConverter # Register routes from app import routes -# Register utils -from app import utils +# Register filters +from app.utils import filters # Register processors from app import processors diff --git a/app/data/groups.yaml b/app/data/groups.yaml index fbc8f4f..4e34b0c 100644 --- a/app/data/groups.yaml +++ b/app/data/groups.yaml @@ -11,7 +11,7 @@ shoutbox-kick shoutbox-ban unlimited-pms footer-statistics community-login access-admin-panel edit-account delete-account edit-trophies - delete_notification + delete_notification no-upload-limits - name: Modérateur css: "color: green;" @@ -21,7 +21,7 @@ move-public-content extract-posts delete-notes delete-tests shoutbox-kick shoutbox-ban - unlimited-pms + unlimited-pms no-upload-limits - name: Développeur css: "color: #4169e1;" @@ -31,7 +31,7 @@ scheduled-posting edit-static-content unlimited-pms footer-statistics community-login - access-admin-panel + access-admin-panel no-upload-limits - name: Rédacteur css: "color: blue;" @@ -41,6 +41,7 @@ upload-shared-files delete-shared-files scheduled-posting showcase-content edit-static-content + no-upload-limits - name: Responsable communauté css: "color: DarkOrange;" diff --git a/app/forms/forum.py b/app/forms/forum.py index 7162415..58e820e 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -1,23 +1,25 @@ from flask_wtf import FlaskForm -from wtforms import StringField, FormField, SubmitField, TextAreaField -from wtforms.validators import DataRequired, Length +from wtforms import StringField, FormField, SubmitField, TextAreaField, \ + MultipleFileField +from wtforms.validators import DataRequired, Length, Optional import app.utils.validators as vd -class TopicCreationForm(FlaskForm): - title = StringField('Nom du sujet', - validators=[DataRequired(), Length(min=3, max=32)]) - message = TextAreaField('Message principal', validators=[DataRequired()]) - submit = SubmitField('Créer le sujet') - -class AnonymousTopicCreationForm(TopicCreationForm): - pseudo = StringField('Pseudo', - validators=[DataRequired(), vd.name_valid, vd.name_available]) - - class CommentForm(FlaskForm): - message = TextAreaField('Commentaire', validators=[DataRequired()]) + message = TextAreaField('Message', validators=[DataRequired()]) + attachments = MultipleFileField('Pièces-jointes', + validators=[vd.file.optional, vd.file.count, vd.file.extension, + vd.file.size]) submit = SubmitField('Commenter') class AnonymousCommentForm(CommentForm): pseudo = StringField('Pseudo', validators=[DataRequired(), vd.name_valid, vd.name_available]) + + +class TopicCreationForm(CommentForm): + title = StringField('Nom du sujet', + validators=[DataRequired(), Length(min=3, max=32)]) + submit = SubmitField('Créer le sujet') + +class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm): + pass diff --git a/app/models/attachment.py b/app/models/attachment.py index 099076f..c237fab 100644 --- a/app/models/attachment.py +++ b/app/models/attachment.py @@ -1,5 +1,8 @@ +from werkzeug.utils import secure_filename +from sqlalchemy.orm import backref from app import db -from hashlib import sha256 +from app.utils.filesize import filesize +from config import V5Config import os class Attachment(db.Model): @@ -9,24 +12,39 @@ class Attachment(db.Model): # Original name of the file name = db.Column(db.Unicode(64)) - # Hash of the value - hashed = db.Column(db.Unicode(64)) - # The comment linked with comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False) - comment = db.relationship('Comment', backref=backref('attachments', lazy='dynamic')) + comment = db.relationship('Comment', backref=backref('attachments')) # The size of the file size = db.Column(db.Integer) + # Storage file path + @property + def path(self): + return os.path.join(V5Config.DATA_FOLDER, "attachments", + f"{self.id:05}", self.name) + + @property + def url(self): + return f"/fichiers/{self.id:05}/{self.name}" + + def __init__(self, file, comment): - self.name = file.filename - self.size = os.stat(file).st_size - self.hashed = self.hash_file(file) + self.name = secure_filename(file.filename) + self.size = filesize(file) self.comment = comment - def hash_file(file): - with open(file,"rb") as f: - bytes = f.read() # read entire file as bytes - hashed = sha256(bytes).hexdigest() - return hashed \ No newline at end of file + def set_file(self, file): + os.mkdir(os.path.dirname(self.path)) + file.save(self.path) + + def edit_file(self, file): + file.name = secure_filename(file.filename) + self.set_file(file) + + def delete_file(self): + try: + os.delete(self.path) + except FileNotFoundError: + pass diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 9bc4576..b23dd5b 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,6 +1,6 @@ # Register routes here -from app.routes import index, search, users, tools +from app.routes import index, search, users, tools, development from app.routes.account import login, account, notification -from app.routes.admin import index, groups, account, trophies, forums +from app.routes.admin import index, groups, account, trophies, forums, attachments from app.routes.forum import index, topic diff --git a/app/routes/admin/attachments.py b/app/routes/admin/attachments.py new file mode 100644 index 0000000..b8b3bdf --- /dev/null +++ b/app/routes/admin/attachments.py @@ -0,0 +1,13 @@ +from app import app +from app.models.attachment import Attachment +from app.utils.priv_required import priv_required +from app.utils.render import render + +# TODO: add pagination & moderation tools (deletion) + +@app.route('/admin/fichiers', methods=['GET']) +@priv_required('access-admin-panel') +def adm_attachments(): + attachments = Attachment.query.all() + + return render('admin/attachments.html', attachments=attachments) diff --git a/app/routes/development.py b/app/routes/development.py new file mode 100644 index 0000000..0cbad14 --- /dev/null +++ b/app/routes/development.py @@ -0,0 +1,25 @@ +from flask import send_file, redirect, url_for, abort +from werkzeug.utils import secure_filename +from app import app +from config import V5Config +import os + +# These routes are used in development +# In production, those files should be served by the web server (nginx) + +@app.route('/avatar/') +def avatar(filename): + filename = secure_filename(filename) # No h4ckers allowed + filepath = os.path.join(V5Config.DATA_FOLDER, "avatars", filename) + if os.path.isfile(filepath): + return send_file(filepath) + return redirect(url_for('static', filename='images/default_avatar.png')) + +@app.route('/fichiers//') +def attachment(path, name): + file = os.path.join(V5Config.DATA_FOLDER, "attachments", + secure_filename(path), secure_filename(name)) + if os.path.isfile(file): + return send_file(file) + else: + abort(404) diff --git a/app/routes/forum/index.py b/app/routes/forum/index.py index f87910d..4e8176d 100644 --- a/app/routes/forum/index.py +++ b/app/routes/forum/index.py @@ -10,6 +10,7 @@ from app.models.topic import Topic from app.models.thread import Thread from app.models.comment import Comment from app.models.users import Guest +from app.models.attachment import Attachment @app.route('/forum/') @@ -34,17 +35,18 @@ def forum_page(f): or ("/actus" not in f.url and not f.sub_forums)) and ( V5Config.ENABLE_GUEST_POST or current_user.is_authenticated): - # First create the thread, then the comment, then the topic - th = Thread() - db.session.add(th) - db.session.commit() - + # Manage author if current_user.is_authenticated: author = current_user else: author = Guest(form.pseudo.data) db.session.add(author) + # First create the thread, then the comment, then the topic + th = Thread() + db.session.add(th) + db.session.commit() + c = Comment(author, form.message.data, th) db.session.add(c) db.session.commit() @@ -56,6 +58,17 @@ def forum_page(f): db.session.add(t) db.session.commit() + # Manage files + attachments = [] + for file in form.attachments.data: + if file.filename != "": + a = Attachment(file, c) + attachments.append((a, file)) + db.session.add(a) + db.session.commit() + for a, file in attachments: + a.set_file(file) + # Update member's xp and trophies if current_user.is_authenticated: current_user.add_xp(2) # 2 points for a topic diff --git a/app/routes/forum/topic.py b/app/routes/forum/topic.py index bdd4ab6..f3ab5ec 100644 --- a/app/routes/forum/topic.py +++ b/app/routes/forum/topic.py @@ -10,6 +10,7 @@ from app.models.topic import Topic from app.models.thread import Thread from app.models.comment import Comment from app.models.users import Guest +from app.models.attachment import Attachment @app.route('/forum//', methods=['GET', 'POST']) @@ -27,16 +28,29 @@ def forum_topic(f, page): if form.validate_on_submit() and \ (V5Config.ENABLE_GUEST_POST or current_user.is_authenticated): + # Manage author if current_user.is_authenticated: author = current_user else: author = Guest(form.pseudo.data) db.session.add(author) + # Create comment c = Comment(author, form.message.data, t.thread) db.session.add(c) db.session.commit() + # Manage files + attachments = [] + for file in form.attachments.data: + if file.filename != "": + a = Attachment(file, c) + attachments.append((a, file)) + db.session.add(a) + db.session.commit() + for a, file in attachments: + a.set_file(file) + # Update member's xp and trophies if current_user.is_authenticated: current_user.add_xp(1) # 1 point for a comment diff --git a/app/routes/users.py b/app/routes/users.py index 5f8a91c..7172654 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -21,11 +21,3 @@ def user(username): def user_by_id(user_id): member = Member.query.filter_by(id=user_id).first_or_404() return redirect(url_for('user', username=member.name)) - -@app.route('/avatar/') -def avatar(filename): - filename = secure_filename(filename) # No h4ckers allowed - filepath = os.path.join(V5Config.DATA_FOLDER, "avatars", filename) - if os.path.isfile(filepath): - return send_file(filepath) - return redirect(url_for('static', filename='images/default_avatar.png')) diff --git a/app/templates/admin/attachments.html b/app/templates/admin/attachments.html new file mode 100644 index 0000000..f5f468e --- /dev/null +++ b/app/templates/admin/attachments.html @@ -0,0 +1,45 @@ +{% extends "base/base.html" %} + +{% block title %} +Panneau d'administration »

Pièces jointes

+{% endblock %} + +{% block content %} +
+

Cette page présente une vue d'ensemble des pièces-jointes postées sur le site.

+ +

Pièces jointes

+ + + + + {% for a in attachments %} + + + + + + + {% endfor %} +
IDNomAuteurTaille
{{ a.id }}{{ a.name }}{{ a.comment.author.name }}{{ a.size }}
+ +

Liste des groupes

+ + + + + {% for group in groups %} + + {% endfor %} +
GroupeMembresPrivilèges
{{ group.name }} + {% for user in group.members %} + {{ user.name }} + {% endfor %} + + {% for priv in group.privs() %} + {{ priv }} + {{- ', ' if not loop.last }} + {% endfor %} +
+
+{% endblock %} diff --git a/app/templates/admin/index.html b/app/templates/admin/index.html index 03f32f9..eef615f 100644 --- a/app/templates/admin/index.html +++ b/app/templates/admin/index.html @@ -8,9 +8,10 @@

Pages générales du panneau d'administration :

{% endblock %} diff --git a/app/templates/forum/forum.html b/app/templates/forum/forum.html index 3ce57e9..1dcac5c 100644 --- a/app/templates/forum/forum.html +++ b/app/templates/forum/forum.html @@ -69,6 +69,11 @@ {{ widget_editor.text_editor(form.message) }} + {{ form.attachments }} + {% for error in form.attachments.errors %} + {{ error }} + {% endfor %} +
{{ form.submit(class_='bg-ok') }}
diff --git a/app/templates/forum/topic.html b/app/templates/forum/topic.html index 8ea0d4e..1b6265b 100644 --- a/app/templates/forum/topic.html +++ b/app/templates/forum/topic.html @@ -2,6 +2,7 @@ {% import "widgets/editor.html" as widget_editor %} {% import "widgets/user.html" as widget_user %} {% import "widgets/pagination.html" as widget_pagination with context %} +{% import "widgets/attachments.html" as widget_attachments %} {% block title %} Forum de Planète Casio » {{ t.forum.name }} »

{{ t.title }}

@@ -34,10 +35,11 @@

{{ c.text }}

+ {{ widget_attachments.attachments(c) }} {% elif loop.index0 != 0 %}
Ce message est le top comment
{% endif %} - + {% endfor %} @@ -60,6 +62,11 @@ {{ widget_editor.text_editor(form.message, label=False) }} + {{ form.attachments }} + {% for error in form.attachments.errors %} + {{ error }} + {% endfor %} +
{{ form.submit(class_='bg-ok') }}
{% endif %} diff --git a/app/templates/widgets/attachments.html b/app/templates/widgets/attachments.html new file mode 100644 index 0000000..c10ab09 --- /dev/null +++ b/app/templates/widgets/attachments.html @@ -0,0 +1,16 @@ +{% macro attachments(comment) %} +{% if comment.attachments %} +Pièces-jointes +
+ + + {% for a in comment.attachments %} + + + + + {% endfor %} +
NomTaille
{{ a.name }}{{ a.size }}
+
+{% endif %} +{% endmacro %} diff --git a/app/utils/__init__.py b/app/utils/__init__.py deleted file mode 100644 index 03b88c2..0000000 --- a/app/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Register utils here - -from app.utils import pluralize -from app.utils import date -from app.utils import is_title diff --git a/app/utils/filesize.py b/app/utils/filesize.py new file mode 100644 index 0000000..56e900e --- /dev/null +++ b/app/utils/filesize.py @@ -0,0 +1,9 @@ +import os +from random import getrandbits + +def filesize(file): + """Return the filesize. Save in /tmp and delete it when done""" + file.seek(0, os.SEEK_END) + size = file.tell() + file.seek(0) + return size diff --git a/app/utils/filters/__init__.py b/app/utils/filters/__init__.py new file mode 100644 index 0000000..54f614b --- /dev/null +++ b/app/utils/filters/__init__.py @@ -0,0 +1,3 @@ +# Register filters here + +from app.utils.filters import date, is_title, pluralize diff --git a/app/utils/date.py b/app/utils/filters/date.py similarity index 100% rename from app/utils/date.py rename to app/utils/filters/date.py diff --git a/app/utils/is_title.py b/app/utils/filters/is_title.py similarity index 100% rename from app/utils/is_title.py rename to app/utils/filters/is_title.py diff --git a/app/utils/pluralize.py b/app/utils/filters/pluralize.py similarity index 100% rename from app/utils/pluralize.py rename to app/utils/filters/pluralize.py diff --git a/app/utils/validators.py b/app/utils/validators/__init__.py similarity index 98% rename from app/utils/validators.py rename to app/utils/validators/__init__.py index c214aac..b93b9e6 100644 --- a/app/utils/validators.py +++ b/app/utils/validators/__init__.py @@ -8,8 +8,12 @@ from app.utils.unicode_names import normalize from math import log from werkzeug.exceptions import NotFound import app.utils.ldap as ldap + from config import V5Config +from app.utils.validators.file import * + +# TODO: clean this shit into split files def name_valid(form, name): valid = valid_name(name.data) @@ -35,7 +39,6 @@ def name_valid(form, name): err = ' '.join(msg.get(code, default) for code in valid) raise ValidationError(err) - def name_available(form, name): # If the name is invalid, name_valid() will return a meaningful message try: @@ -53,14 +56,11 @@ def name_available(form, name): if member is not None: raise ValidationError("Ce nom d'utilisateur est indisponible.") - - def email(form, email): member = Member.query.filter_by(email=email.data).first() if member is not None: raise ValidationError('Adresse email déjà utilisée.') - def password(form, password): # To avoid errors in forms where password is optionnal if len(password.data) == 0: @@ -87,14 +87,12 @@ def password(form, password): if entropy(password.data) < 60: raise ValidationError("Mot de passe pas assez complexe") - def avatar(form, avatar): try: Image.open(avatar.data) except IOError: raise ValidationError("Avatar invalide") - def old_password(form, field): if field.data: if not form.old_password.data: @@ -102,7 +100,6 @@ def old_password(form, field): if not current_user.check_password(form.old_password.data): raise ValidationError('Mot de passe actuel erroné.') - def id_exists(object): """Check if an id exists in a table""" def _id_exists(form, id): @@ -115,7 +112,6 @@ def id_exists(object): raise ValidationError('L\'id n\'existe pas dans la BDD') return _id_exists - def css(form, css): """Check if input is valid and sane CSS""" pass diff --git a/app/utils/validators/file.py b/app/utils/validators/file.py new file mode 100644 index 0000000..747ca0e --- /dev/null +++ b/app/utils/validators/file.py @@ -0,0 +1,51 @@ +from flask_login import current_user +from wtforms.validators import ValidationError, StopValidation +from werkzeug.utils import secure_filename +from app.utils.filesize import filesize +import re + +def optional(form, files): + if(len(files.data) == 0 or files.data[0].filename == ""): + raise StopValidation() + +def count(form, files): + if current_user.is_authenticated: + if current_user.priv("no-upload-limits"): + return + if len(files.data) > 100: # 100 files for a authenticated user + raise ValidationError("100 fichiers maximum autorisés") + else: + if len(files.data) > 3: + raise ValidationError("3 fichiers maximum autorisés") + +def extension(form, files): + valid_extensions = [ + "g[123][a-z]|cpa|c1a|fxi|cat|mcs|xcp|fls", # Casio files + "png|jpg|jpeg|bmp|tiff|gif|xcf", # Images + "[ch](pp|\+\+|xx)?|s|py|bide|lua|lc", # Source code + "txt|md|tex|pdf|odt|ods|docx|xlsx", # Office files + "zip|7z|tar|bz2?|t?gz|xz|zst", # Archives + ] + r = re.compile("|".join(valid_extensions), re.IGNORECASE) + errors = [] + + for f in files.data: + name = secure_filename(f.filename) + ext = name.split(".")[-1] + if not r.fullmatch(ext): + errors.append("." + ext) + + if len(errors) > 0: + raise ValidationError(f"Extension(s) invalide(s) ({', '.join(errors)})") + +def size(form, files): + """There is no global limit to file sizes""" + size = sum([filesize(f) for f in files.data]) + if current_user.is_authenticated: + if current_user.priv("no-upload-limits"): + return + if size > 5e6: # 5 Mo per comment for an authenticated user + raise ValidationError("Fichiers trop lourds (max 5 Mo)") + else: + if size > 500e3: # 500 ko per comment for a guest + raise ValidationError("Fichiers trop lourds (max 500 ko)") diff --git a/migrations/versions/cd4868f312c5_added_attachments.py b/migrations/versions/cd4868f312c5_added_attachments.py new file mode 100644 index 0000000..547b909 --- /dev/null +++ b/migrations/versions/cd4868f312c5_added_attachments.py @@ -0,0 +1,35 @@ +"""Added attachments + +Revision ID: cd4868f312c5 +Revises: 001d2eaf0413 +Create Date: 2020-08-01 19:22:12.405038 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cd4868f312c5' +down_revision = '001d2eaf0413' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('attachment', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.Unicode(length=64), nullable=True), + sa.Column('comment_id', sa.Integer(), nullable=False), + sa.Column('size', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['comment_id'], ['comment.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('attachment') + # ### end Alembic commands ### From b51ca8291f06c315d53a82cfbcac2854b1d30050 Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 1 Aug 2020 21:54:21 +0200 Subject: [PATCH 17/94] Bug fixed --- app/forms/forum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/forms/forum.py b/app/forms/forum.py index ea577a2..ebaf849 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -19,7 +19,7 @@ class AnonymousCommentForm(CommentForm): class CommentEditForm(CommentForm): submit = SubmitField('Modifier') -class AnonymousCommentEditForm(CommentForm, AnonymousCommentFor): +class AnonymousCommentEditForm(CommentEditForm, AnonymousCommentForm): pass From 03c577316f2a9eaa71acc9dfc3466ada8913ec80 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sun, 2 Aug 2020 11:01:08 +0200 Subject: [PATCH 18/94] thread: add a reference to the owner post --- app/models/program.py | 3 ++- app/models/thread.py | 13 +++++++++++++ app/models/topic.py | 3 ++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/models/program.py b/app/models/program.py index b16a484..ef762f2 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -16,7 +16,8 @@ class Program(Post): # TODO: Compatible calculator models thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) - thread = db.relationship('Thread', foreign_keys=thread_id) + thread = db.relationship('Thread', foreign_keys=thread_id, + back_populates='owner_program') # TODO: Number of views, statistics, attached files, etc diff --git a/app/models/thread.py b/app/models/thread.py index 1f69f3c..6b62341 100644 --- a/app/models/thread.py +++ b/app/models/thread.py @@ -12,6 +12,11 @@ class Thread(db.Model): top_comment_id = db.Column(db.Integer, db.ForeignKey('comment.id')) top_comment = db.relationship('Comment', foreign_keys=top_comment_id) + # Post owning the thread, set only by Topic, Program, etc. In general, you + # should use [owner_post] which groups them together. + owner_topic = db.relationship('Topic') + owner_program = db.relationship('Program') + # Other fields populated automatically through relations: # The list of comments (of type Comment) @@ -39,5 +44,13 @@ class Thread(db.Model): self.top_comment = top_comment + @property + def owner_post(self): + if self.owner_topic != []: + return self.owner_topic[0] + if self.owner_program != []: + return self.owner_program[0] + return None + def __repr__(self): return f'' diff --git a/app/models/topic.py b/app/models/topic.py index 2c656d3..7b9465f 100644 --- a/app/models/topic.py +++ b/app/models/topic.py @@ -26,7 +26,8 @@ class Topic(Post): # Associated thread thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) - thread = db.relationship('Thread', foreign_keys=thread_id) + thread = db.relationship('Thread', foreign_keys=thread_id, + back_populates='owner_topic') # Number of views in the forum views = db.Column(db.Integer) From a83cef5970696d5596df43fff2f72e967aace812 Mon Sep 17 00:00:00 2001 From: Darks Date: Sun, 2 Aug 2020 15:03:08 +0200 Subject: [PATCH 19/94] attachments: fix #67 --- app/forms/forum.py | 2 +- app/utils/validators/file.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/forms/forum.py b/app/forms/forum.py index ebaf849..c9ab038 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -8,7 +8,7 @@ class CommentForm(FlaskForm): message = TextAreaField('Message', validators=[DataRequired()]) attachments = MultipleFileField('Pièces-jointes', validators=[vd.file.optional, vd.file.count, vd.file.extension, - vd.file.size]) + vd.file.size, vd.file.namelength]) submit = SubmitField('Commenter') preview = SubmitField('Prévisualiser') diff --git a/app/utils/validators/file.py b/app/utils/validators/file.py index 747ca0e..fd1f743 100644 --- a/app/utils/validators/file.py +++ b/app/utils/validators/file.py @@ -49,3 +49,13 @@ def size(form, files): else: if size > 500e3: # 500 ko per comment for a guest raise ValidationError("Fichiers trop lourds (max 500 ko)") + +def namelength(form, files): + errors = [] + for f in files.data: + name = secure_filename(f.filename) + if len(name) > 64: + errors.append(f.filename) + if len(errors) > 0: + raise ValidationError(f"Noms trop longs, 64 caractères max " \ + f"({', '.join(errors)})") From 3a35c26bd7acbf495b2a3ebf946b79a1fbe69685 Mon Sep 17 00:00:00 2001 From: Darks Date: Sun, 2 Aug 2020 17:28:43 +0200 Subject: [PATCH 20/94] forum: add pagination to topic list (#64) --- app/models/forum.py | 3 +++ app/models/topic.py | 4 +++- app/routes/forum/index.py | 10 ++++++++-- app/templates/forum/forum.html | 17 ++++++++++++----- app/templates/widgets/pagination.html | 18 +++++++++++++++--- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/app/models/forum.py b/app/models/forum.py index acc2fff..48aaffc 100644 --- a/app/models/forum.py +++ b/app/models/forum.py @@ -22,6 +22,9 @@ class Forum(db.Model): # Other fields populated automatically through relations: # List of topics in this exact forum (of type Topic) + # Some configuration + TOPICS_PER_PAGE = 30 + def __init__(self, url, name, prefix, descr="", parent=None): self.url = url self.name = name diff --git a/app/models/topic.py b/app/models/topic.py index 7b9465f..4afe5db 100644 --- a/app/models/topic.py +++ b/app/models/topic.py @@ -1,5 +1,6 @@ from app import db from app.models.post import Post +from sqlalchemy.orm import backref class Topic(Post): __tablename__ = 'topic' @@ -22,7 +23,8 @@ class Topic(Post): # Parent forum forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False) - forum = db.relationship('Forum', backref='topics',foreign_keys=forum_id) + forum = db.relationship('Forum', + backref=backref('topics', lazy='dynamic'),foreign_keys=forum_id) # Associated thread thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) diff --git a/app/routes/forum/index.py b/app/routes/forum/index.py index 4e8176d..892ad90 100644 --- a/app/routes/forum/index.py +++ b/app/routes/forum/index.py @@ -18,7 +18,8 @@ def forum_index(): return render('/forum/index.html') @app.route('/forum//', methods=['GET', 'POST']) -def forum_page(f): +@app.route('/forum//p/', methods=['GET', 'POST']) +def forum_page(f, page=1): if current_user.is_authenticated: form = TopicCreationForm() else: @@ -77,4 +78,9 @@ def forum_page(f): flash('Le sujet a bien été créé', 'ok') return redirect(url_for('forum_topic', f=f, page=(t,1))) - return render('/forum/forum.html', f=f, form=form) + # Paginate topic pages + # TODO: order by last comment date + topics = f.topics.order_by(Topic.date_created.desc()).paginate( + page, Forum.TOPICS_PER_PAGE, True) + + return render('/forum/forum.html', f=f, topics=topics, form=form) diff --git a/app/templates/forum/forum.html b/app/templates/forum/forum.html index 1dcac5c..ae59fda 100644 --- a/app/templates/forum/forum.html +++ b/app/templates/forum/forum.html @@ -1,5 +1,6 @@ {% extends "base/base.html" %} {% import "widgets/editor.html" as widget_editor %} +{% import "widgets/pagination.html" as widget_pagination with context %} {% block title %} Forum de Planète Casio »

{{ f.name }}

@@ -9,13 +10,16 @@

{{ f.descr }}

- {% if f.topics %} + {% if topics.items %}

Sujets

+ + {{ widget_pagination.paginate(topics, 'forum_page', None, {'f': f}) }} + - + - {% for t in f.topics %} + {% for t in topics.items %} @@ -23,8 +27,11 @@ {% endfor %}
SujetAuteurDate de créationCommentairesVues
CommentairesVues
{{ t.title }} {{ t.author.name }} {{ t.date_created | date }}{{ t.views }}
+ + {{ widget_pagination.paginate(topics, 'forum_page', None, {'f': f}) }} + {% elif not f.sub_forums %} -

Il n'y a aucun topic sur ce forum ! Animons-le vite !

+

Il n'y a aucun topic sur ce forum ! Animons-le vite !

{% endif %} {% if f.sub_forums %} @@ -34,7 +41,7 @@ {% for sf in f.sub_forums %} {{ sf.name }} - {{ sf.topics | length }} + {{ sf.topics.count() }} {{ sf.descr }} {% endfor %} diff --git a/app/templates/widgets/pagination.html b/app/templates/widgets/pagination.html index 1a3a89e..e12425d 100644 --- a/app/templates/widgets/pagination.html +++ b/app/templates/widgets/pagination.html @@ -1,21 +1,33 @@ {% macro paginate(objects, route, obj, route_args) %} {% endmacro %} From 801bd188ae51bbf35a810d1d75671cc2da0898c9 Mon Sep 17 00:00:00 2001 From: Darks Date: Wed, 5 Aug 2020 22:16:58 +0200 Subject: [PATCH 21/94] feat: add .env and .flaskenv to gitignore Because there is no reason to track them --- .gitignore | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 93d2f8b..b7d5fc6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,12 @@ app/__pycache__/ app/static/avatars/ app/static/images/trophies/ -## Devlopement files +# Development files + +# Flask env +.env +.flaskenv # virtualenv requirements.txt venv/ @@ -14,6 +18,7 @@ venv/ Pipfile Pipfile.lock + ## Deployment files # uWSGI configuration file @@ -25,10 +30,12 @@ update.sh # Config to set up some server specific config local_config.py + ## Wiki wiki/ + ## Personal folder exclude/ From 3fb3ee40d2666d8c3caac83f09b655a3c033b298 Mon Sep 17 00:00:00 2001 From: Darks Date: Wed, 5 Aug 2020 22:52:56 +0200 Subject: [PATCH 22/94] fix: better PEP8 Flake8 returns less errors. I ignored lots of them though --- V5.py | 4 +--- app/forms/account.py | 3 +-- app/forms/forum.py | 5 ++--- app/models/forum.py | 4 ++-- app/models/post.py | 1 - app/models/program.py | 4 ++-- app/models/thread.py | 4 ++-- app/models/topic.py | 8 ++++---- app/models/users.py | 10 +++------- app/processors/menu.py | 4 +--- app/processors/utilities.py | 4 ++-- app/routes/development.py | 2 +- app/routes/forum/index.py | 2 +- app/routes/forum/topic.py | 2 +- app/utils/converters.py | 3 +-- app/utils/filesize.py | 2 +- app/utils/ldap.py | 12 ++++++++---- app/utils/notify.py | 2 +- app/utils/render.py | 3 --- app/utils/validators/__init__.py | 2 +- app/utils/validators/file.py | 16 ++++++++-------- master.py | 6 +++--- 22 files changed, 46 insertions(+), 57 deletions(-) diff --git a/V5.py b/V5.py index f395e0e..5745db6 100644 --- a/V5.py +++ b/V5.py @@ -1,6 +1,4 @@ -from app import app, db -from app.models.users import User, Guest, Member, Group, GroupPrivilege -from app.models.topic import Topic +from app import app @app.shell_context_processor diff --git a/app/forms/account.py b/app/forms/account.py index 3115ddb..72b22c5 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -1,9 +1,8 @@ from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField, SelectField from wtforms.fields.html5 import DateField, EmailField -from wtforms.validators import DataRequired, InputRequired, Optional, Email, EqualTo +from wtforms.validators import DataRequired, Optional, Email, EqualTo from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty -from app.models.trophies import Trophy import app.utils.validators as vd diff --git a/app/forms/forum.py b/app/forms/forum.py index c9ab038..01480ff 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -1,7 +1,6 @@ from flask_wtf import FlaskForm -from wtforms import StringField, FormField, SubmitField, TextAreaField, \ - MultipleFileField -from wtforms.validators import DataRequired, Length, Optional +from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField +from wtforms.validators import DataRequired, Length import app.utils.validators as vd class CommentForm(FlaskForm): diff --git a/app/models/forum.py b/app/models/forum.py index 48aaffc..d661b4b 100644 --- a/app/models/forum.py +++ b/app/models/forum.py @@ -16,8 +16,8 @@ class Forum(db.Model): # Relationships parent_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=True) - parent = db.relationship('Forum', backref='sub_forums', remote_side=id, - lazy=True, foreign_keys=parent_id) + parent = db.relationship('Forum', backref='sub_forums', remote_side=id, + lazy=True, foreign_keys=parent_id) # Other fields populated automatically through relations: # List of topics in this exact forum (of type Topic) diff --git a/app/models/post.py b/app/models/post.py index 88daabb..0f6dc2c 100644 --- a/app/models/post.py +++ b/app/models/post.py @@ -1,5 +1,4 @@ from app import db -from app.models.users import User from datetime import datetime diff --git a/app/models/program.py b/app/models/program.py index ef762f2..8ce6095 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -16,8 +16,8 @@ class Program(Post): # TODO: Compatible calculator models thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) - thread = db.relationship('Thread', foreign_keys=thread_id, - back_populates='owner_program') + thread = db.relationship('Thread', foreign_keys=thread_id, + back_populates='owner_program') # TODO: Number of views, statistics, attached files, etc diff --git a/app/models/thread.py b/app/models/thread.py index 6b62341..e623fee 100644 --- a/app/models/thread.py +++ b/app/models/thread.py @@ -10,11 +10,11 @@ class Thread(db.Model): # Top comment top_comment_id = db.Column(db.Integer, db.ForeignKey('comment.id')) - top_comment = db.relationship('Comment', foreign_keys=top_comment_id) + top_comment = db.relationship('Comment', foreign_keys=top_comment_id) # Post owning the thread, set only by Topic, Program, etc. In general, you # should use [owner_post] which groups them together. - owner_topic = db.relationship('Topic') + owner_topic = db.relationship('Topic') owner_program = db.relationship('Program') # Other fields populated automatically through relations: diff --git a/app/models/topic.py b/app/models/topic.py index 4afe5db..1dcd8a8 100644 --- a/app/models/topic.py +++ b/app/models/topic.py @@ -16,19 +16,19 @@ class Topic(Post): # Post that the topic was promoted into. If this is not None, then the # topic was published into a project and a redirection should be emitted promotion_id = db.Column(db.Integer,db.ForeignKey('post.id'),nullable=True) - promotion = db.relationship('Post', foreign_keys=promotion_id) + promotion = db.relationship('Post', foreign_keys=promotion_id) # Topic title title = db.Column(db.Unicode(128)) # Parent forum forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False) - forum = db.relationship('Forum', - backref=backref('topics', lazy='dynamic'),foreign_keys=forum_id) + forum = db.relationship('Forum', + backref=backref('topics', lazy='dynamic'), foreign_keys=forum_id) # Associated thread thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False) - thread = db.relationship('Thread', foreign_keys=thread_id, + thread = db.relationship('Thread', foreign_keys=thread_id, back_populates='owner_topic') # Number of views in the forum diff --git a/app/models/users.py b/app/models/users.py index 63493c2..40c45de 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -1,5 +1,4 @@ from datetime import date -from flask import flash from flask_login import UserMixin from sqlalchemy import func as SQLfunc from os.path import isfile @@ -10,12 +9,10 @@ from app.models.privs import SpecialPrivilege, Group, GroupMember, \ from app.models.trophies import Trophy, TrophyMember, Title from app.models.notification import Notification import app.utils.unicode_names as unicode_names -from app.utils.notify import notify import app.utils.ldap as ldap from config import V5Config import werkzeug.security -import re import math import app import os @@ -78,8 +75,7 @@ class Member(User): # Primary attributes (needed for the system to work) name = db.Column(db.Unicode(User.NAME_MAXLEN), index=True) - norm = db.Column(db.Unicode(User.NAME_MAXLEN), index=True, - unique=True) + norm = db.Column(db.Unicode(User.NAME_MAXLEN), index=True, unique=True) email = db.Column(db.Unicode(120), index=True, unique=True) email_confirmed = db.Column(db.Boolean) password_hash = db.Column(db.String(255)) @@ -107,7 +103,7 @@ class Member(User): # Displayed title, if set title_id = db.Column(db.Integer, db.ForeignKey('title.id'), nullable=True) - title = db.relationship('Title', foreign_keys=title_id) + title = db.relationship('Title', foreign_keys=title_id) # Settings newsletter = db.Column(db.Boolean, default=False) @@ -158,7 +154,7 @@ class Member(User): if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first(): return True return db.session.query(Group, GroupPrivilege).filter( - Group.id.in_([ g.id for g in self.groups ]), + Group.id.in_([g.id for g in self.groups]), GroupPrivilege.gid==Group.id, GroupPrivilege.priv==priv).first() is not None diff --git a/app/processors/menu.py b/app/processors/menu.py index eae5737..d7b2007 100644 --- a/app/processors/menu.py +++ b/app/processors/menu.py @@ -3,9 +3,7 @@ from app.forms.login import LoginForm from app.forms.search import SearchForm from app.models.forum import Forum from app.models.topic import Topic -from app.models.thread import Thread -from app.models.comment import Comment -from app.models.users import Member + @app.context_processor def menu_processor(): diff --git a/app/processors/utilities.py b/app/processors/utilities.py index ad6dbde..7200fb8 100644 --- a/app/processors/utilities.py +++ b/app/processors/utilities.py @@ -9,7 +9,7 @@ def utilities_processor(): return dict( len=len, # enumerate=enumerate, - _url_for = lambda route, args, **other: url_for(route, **args, **other), - V5Config = V5Config, + _url_for=lambda route, args, **other: url_for(route, **args, **other), + V5Config=V5Config, slugify=slugify, ) diff --git a/app/routes/development.py b/app/routes/development.py index 0cbad14..f4f4cc0 100644 --- a/app/routes/development.py +++ b/app/routes/development.py @@ -9,7 +9,7 @@ import os @app.route('/avatar/') def avatar(filename): - filename = secure_filename(filename) # No h4ckers allowed + filename = secure_filename(filename) # No h4ckers allowed filepath = os.path.join(V5Config.DATA_FOLDER, "avatars", filename) if os.path.isfile(filepath): return send_file(filepath) diff --git a/app/routes/forum/index.py b/app/routes/forum/index.py index 892ad90..8843824 100644 --- a/app/routes/forum/index.py +++ b/app/routes/forum/index.py @@ -72,7 +72,7 @@ def forum_page(f, page=1): # Update member's xp and trophies if current_user.is_authenticated: - current_user.add_xp(2) # 2 points for a topic + current_user.add_xp(2) # 2 points for a topic current_user.update_trophies('new-post') flash('Le sujet a bien été créé', 'ok') diff --git a/app/routes/forum/topic.py b/app/routes/forum/topic.py index f3ab5ec..d150c65 100644 --- a/app/routes/forum/topic.py +++ b/app/routes/forum/topic.py @@ -53,7 +53,7 @@ def forum_topic(f, page): # Update member's xp and trophies if current_user.is_authenticated: - current_user.add_xp(1) # 1 point for a comment + current_user.add_xp(1) # 1 point for a comment current_user.update_trophies('new-post') flash('Message envoyé', 'ok') diff --git a/app/utils/converters.py b/app/utils/converters.py index 7fb7a81..3caae7b 100644 --- a/app/utils/converters.py +++ b/app/utils/converters.py @@ -20,8 +20,7 @@ from werkzeug.routing import BaseConverter, ValidationError from app.models.forum import Forum from app.models.topic import Topic from slugify import slugify -import re -import sys + class ForumConverter(BaseConverter): diff --git a/app/utils/filesize.py b/app/utils/filesize.py index 56e900e..888afff 100644 --- a/app/utils/filesize.py +++ b/app/utils/filesize.py @@ -1,5 +1,5 @@ import os -from random import getrandbits + def filesize(file): """Return the filesize. Save in /tmp and delete it when done""" diff --git a/app/utils/ldap.py b/app/utils/ldap.py index c3d21fb..4e62639 100644 --- a/app/utils/ldap.py +++ b/app/utils/ldap.py @@ -1,9 +1,11 @@ import ldap from ldap.modlist import addModlist, modifyModlist +from app.utils.unicode_names import normalize from config import V5Config def get_member(username): - """ Get informations about member. Username must be normalized! """ + """ Get informations about member""" + username = normalize(username) # Never safe enough conn = ldap.initialize("ldap://localhost") # Search for user r = conn.search_s(V5Config.LDAP_ORGANIZATION, ldap.SCOPE_SUBTREE, @@ -17,13 +19,15 @@ def get_member(username): def edit(user, fields): """ Edit a user. Fields is {'name': ['value'], …} """ conn = ldap.initialize("ldap://localhost") + # TODO: do this # Connect as root - # conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD) + # conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}', + # V5Config.LDAP_PASSWORD) # old_value = {"userPassword": ["my_old_password"]} # new_value = {"userPassword": ["my_new_password"]} - modlist = ldap.modlist.modifyModlist(old_value, new_value) - con.modify_s(dn, modlist) + # modlist = modifyModlist(old_value, new_value) + # conn.modify_s(dn, modlist) def set_email(user, email): diff --git a/app/utils/notify.py b/app/utils/notify.py index 2a98dc8..86d9f80 100644 --- a/app/utils/notify.py +++ b/app/utils/notify.py @@ -1,6 +1,6 @@ from app import db from app.models.notification import Notification -# from app.models.users import Member +from app.models.users import Member def notify(user, message, href=None): """ Notify a user (by id, name or object reference) with a message. diff --git a/app/utils/render.py b/app/utils/render.py index f2f9f75..262944f 100644 --- a/app/utils/render.py +++ b/app/utils/render.py @@ -1,7 +1,4 @@ from flask import render_template -from app.forms.login import LoginForm -from app.forms.search import SearchForm -from app.models.forum import Forum def render(*args, styles=[], scripts=[], **kwargs): # TODO: debugguer cette merde : au logout, ça foire diff --git a/app/utils/validators/__init__.py b/app/utils/validators/__init__.py index b93b9e6..c02d089 100644 --- a/app/utils/validators/__init__.py +++ b/app/utils/validators/__init__.py @@ -74,7 +74,7 @@ def password(form, password): "abcdefghijklmnopqrstuvwxyz", "ABCDFEGHIJKLMNOPQRSTUVWXYZ", "0123456789", - " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars + " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars "áàâéèêíìîóòôúùûç", ] used = set() diff --git a/app/utils/validators/file.py b/app/utils/validators/file.py index fd1f743..c120ef6 100644 --- a/app/utils/validators/file.py +++ b/app/utils/validators/file.py @@ -12,7 +12,7 @@ def count(form, files): if current_user.is_authenticated: if current_user.priv("no-upload-limits"): return - if len(files.data) > 100: # 100 files for a authenticated user + if len(files.data) > 100: # 100 files for a authenticated user raise ValidationError("100 fichiers maximum autorisés") else: if len(files.data) > 3: @@ -20,11 +20,11 @@ def count(form, files): def extension(form, files): valid_extensions = [ - "g[123][a-z]|cpa|c1a|fxi|cat|mcs|xcp|fls", # Casio files - "png|jpg|jpeg|bmp|tiff|gif|xcf", # Images - "[ch](pp|\+\+|xx)?|s|py|bide|lua|lc", # Source code - "txt|md|tex|pdf|odt|ods|docx|xlsx", # Office files - "zip|7z|tar|bz2?|t?gz|xz|zst", # Archives + "g[123][a-z]|cpa|c1a|fxi|cat|mcs|xcp|fls", # Casio files + "png|jpg|jpeg|bmp|tiff|gif|xcf", # Images + "[ch](pp|\+\+|xx)?|s|py|bide|lua|lc", # Source code + "txt|md|tex|pdf|odt|ods|docx|xlsx", # Office files + "zip|7z|tar|bz2?|t?gz|xz|zst", # Archives ] r = re.compile("|".join(valid_extensions), re.IGNORECASE) errors = [] @@ -44,10 +44,10 @@ def size(form, files): if current_user.is_authenticated: if current_user.priv("no-upload-limits"): return - if size > 5e6: # 5 Mo per comment for an authenticated user + if size > 5e6: # 5 Mo per comment for an authenticated user raise ValidationError("Fichiers trop lourds (max 5 Mo)") else: - if size > 500e3: # 500 ko per comment for a guest + if size > 500e3: # 500 ko per comment for a guest raise ValidationError("Fichiers trop lourds (max 500 ko)") def namelength(form, files): diff --git a/master.py b/master.py index 5e617c8..f03ccc8 100755 --- a/master.py +++ b/master.py @@ -3,16 +3,16 @@ from app import app, db from app.models.users import Member, Group, GroupPrivilege from app.models.privs import SpecialPrivilege -from app.models.trophies import Trophy, Title, TrophyMember +from app.models.trophies import Trophy, Title from app.models.forum import Forum from app.utils import unicode_names import os import sys import yaml -import readline import slugify from PIL import Image + help_msg = """ This is the Planète Casio master shell. Type 'exit' or C-D to leave. @@ -186,7 +186,7 @@ def create_trophies(): print(f"Created {len(tr)} trophies.") # Create their icons in /app/static/images/trophies - names = [ slugify.slugify(t["name"]) for t in tr ] + names = [slugify.slugify(t["name"]) for t in tr] src = os.path.join(app.root_path, "data", "trophies.png") dst = os.path.join(app.root_path, "static", "images", "trophies") From 262d907c3291a05d8ac22c80b20bc75c734f8afc Mon Sep 17 00:00:00 2001 From: Darks Date: Wed, 5 Aug 2020 23:26:36 +0200 Subject: [PATCH 23/94] fix(forms): replace DataRequired by InputRequired See [the difference](https://stackoverflow.com/questions/23982917/flask-wtforms-difference-between-datarequired-and-inputrequired) --- app/forms/account.py | 18 +++++++++--------- app/forms/forum.py | 8 ++++---- app/forms/login.py | 6 +++--- app/forms/search.py | 6 +++--- app/forms/trophies.py | 6 +++--- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/forms/account.py b/app/forms/account.py index 72b22c5..ac887f1 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -1,7 +1,7 @@ from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField, SelectField from wtforms.fields.html5 import DateField, EmailField -from wtforms.validators import DataRequired, Optional, Email, EqualTo +from wtforms.validators import InputRequired, Optional, Email, EqualTo from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty import app.utils.validators as vd @@ -11,7 +11,7 @@ class RegistrationForm(FlaskForm): 'Pseudonyme', description='Ce nom est définitif !', validators=[ - DataRequired(), + InputRequired(), vd.name_valid, vd.name_available, ], @@ -19,7 +19,7 @@ class RegistrationForm(FlaskForm): email = EmailField( 'Adresse Email', validators=[ - DataRequired(), + InputRequired(), Email(message="Adresse email invalide."), vd.email, ], @@ -27,21 +27,21 @@ class RegistrationForm(FlaskForm): password = PasswordField( 'Mot de passe', validators=[ - DataRequired(), + InputRequired(), vd.password, ], ) password2 = PasswordField( 'Répéter le mot de passe', validators=[ - DataRequired(), + InputRequired(), EqualTo('password', message="Les mots de passe doivent être identiques."), ], ) guidelines = BooleanField( """J'accepte les CGU""", validators=[ - DataRequired(), + InputRequired(), ], ) newsletter = BooleanField( @@ -128,14 +128,14 @@ class DeleteAccountForm(FlaskForm): delete = BooleanField( 'Confirmer la suppression', validators=[ - DataRequired(), + InputRequired(), ], description='Attention, cette opération est irréversible !' ) old_password = PasswordField( 'Mot de passe', validators=[ - DataRequired(), + InputRequired(), vd.old_password, ], ) @@ -268,7 +268,7 @@ class AdminDeleteAccountForm(FlaskForm): delete = BooleanField( 'Confirmer la suppression', validators=[ - DataRequired(), + InputRequired(), ], description='Attention, cette opération est irréversible !', ) diff --git a/app/forms/forum.py b/app/forms/forum.py index 01480ff..328ff07 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -1,10 +1,10 @@ from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField -from wtforms.validators import DataRequired, Length +from wtforms.validators import InputRequired, Length import app.utils.validators as vd class CommentForm(FlaskForm): - message = TextAreaField('Message', validators=[DataRequired()]) + message = TextAreaField('Message', validators=[InputRequired()]) attachments = MultipleFileField('Pièces-jointes', validators=[vd.file.optional, vd.file.count, vd.file.extension, vd.file.size, vd.file.namelength]) @@ -13,7 +13,7 @@ class CommentForm(FlaskForm): class AnonymousCommentForm(CommentForm): pseudo = StringField('Pseudo', - validators=[DataRequired(), vd.name_valid, vd.name_available]) + validators=[InputRequired(), vd.name_valid, vd.name_available]) class CommentEditForm(CommentForm): submit = SubmitField('Modifier') @@ -24,7 +24,7 @@ class AnonymousCommentEditForm(CommentEditForm, AnonymousCommentForm): class TopicCreationForm(CommentForm): title = StringField('Nom du sujet', - validators=[DataRequired(), Length(min=3, max=32)]) + validators=[InputRequired(), Length(min=3, max=32)]) submit = SubmitField('Créer le sujet') class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm): diff --git a/app/forms/login.py b/app/forms/login.py index 72bec53..498a319 100644 --- a/app/forms/login.py +++ b/app/forms/login.py @@ -1,19 +1,19 @@ from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField -from wtforms.validators import DataRequired +from wtforms.validators import InputRequired class LoginForm(FlaskForm): username = StringField( 'Identifiant', validators=[ - DataRequired(), + InputRequired(), ], ) password = PasswordField( 'Mot de passe', validators=[ - DataRequired(), + InputRequired(), ], ) remember_me = BooleanField( diff --git a/app/forms/search.py b/app/forms/search.py index 6ebac62..5ab431f 100644 --- a/app/forms/search.py +++ b/app/forms/search.py @@ -1,7 +1,7 @@ from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.fields.html5 import DateField -from wtforms.validators import DataRequired, Optional +from wtforms.validators import InputRequired, Optional # TODO: compléter le formulaire de recherche avancée @@ -9,7 +9,7 @@ class AdvancedSearchForm(FlaskForm): q = StringField( 'Rechercher :', validators=[ - DataRequired(), + InputRequired(), ], ) date = DateField( @@ -27,6 +27,6 @@ class SearchForm(FlaskForm): q = StringField( 'Rechercher', validators=[ - DataRequired(), + InputRequired(), ], ) diff --git a/app/forms/trophies.py b/app/forms/trophies.py index 62825c9..4a77b02 100644 --- a/app/forms/trophies.py +++ b/app/forms/trophies.py @@ -1,6 +1,6 @@ from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, BooleanField -from wtforms.validators import DataRequired, Optional +from wtforms.validators import InputRequired, Optional from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty @@ -8,7 +8,7 @@ class TrophyForm(FlaskForm): name = StringField( 'Nom', validators=[ - DataRequired(), + InputRequired(), ], ) icon = FileField( @@ -43,7 +43,7 @@ class DeleteTrophyForm(FlaskForm): delete = BooleanField( 'Confirmer la suppression', validators=[ - DataRequired(), + InputRequired(), ], description='Attention, cette opération est irréversible !', ) From 107c89155310f1278c621f06c54dd541f60b3e7f Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 6 Aug 2020 00:04:47 +0200 Subject: [PATCH 24/94] fix(validators): cleaned a bit the directory --- app/forms/account.py | 29 +++++----- app/forms/forum.py | 6 ++- app/utils/validators/__init__.py | 93 ++------------------------------ app/utils/validators/file.py | 23 +++++++- app/utils/validators/name.py | 49 +++++++++++++++++ app/utils/validators/password.py | 38 +++++++++++++ 6 files changed, 134 insertions(+), 104 deletions(-) create mode 100644 app/utils/validators/name.py create mode 100644 app/utils/validators/password.py diff --git a/app/forms/account.py b/app/forms/account.py index ac887f1..e9bee41 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -12,8 +12,8 @@ class RegistrationForm(FlaskForm): description='Ce nom est définitif !', validators=[ InputRequired(), - vd.name_valid, - vd.name_available, + vd.name.valid, + vd.name.available, ], ) email = EmailField( @@ -28,7 +28,7 @@ class RegistrationForm(FlaskForm): 'Mot de passe', validators=[ InputRequired(), - vd.password, + vd.password.is_strong, ], ) password2 = PasswordField( @@ -58,7 +58,8 @@ class UpdateAccountForm(FlaskForm): 'Avatar', validators=[ Optional(), - vd.avatar, + vd.file.is_image, + vd.file.avatar_size, ], ) email = EmailField( @@ -67,15 +68,15 @@ class UpdateAccountForm(FlaskForm): Optional(), Email(message="Addresse email invalide."), vd.email, - vd.old_password, + vd.password.old_password, ], ) password = PasswordField( 'Nouveau mot de passe', validators=[ Optional(), - vd.password, - vd.old_password, + vd.password.is_strong, + vd.password.old_password, ], ) password2 = PasswordField( @@ -136,7 +137,7 @@ class DeleteAccountForm(FlaskForm): 'Mot de passe', validators=[ InputRequired(), - vd.old_password, + vd.password.old_password, ], ) submit = SubmitField( @@ -160,7 +161,7 @@ class ResetPasswordForm(FlaskForm): 'Mot de passe', validators=[ Optional(), - vd.password, + vd.password.is_strong, ], ) password2 = PasswordField( @@ -178,14 +179,16 @@ class AdminUpdateAccountForm(FlaskForm): 'Pseudonyme', validators=[ Optional(), - vd.name_valid, + vd.name.valid, + vd.name.available, ], ) avatar = FileField( 'Avatar', validators=[ Optional(), - vd.avatar, + vd.file.is_image, + vd.file.avatar_size, ], ) email = EmailField( @@ -198,7 +201,7 @@ class AdminUpdateAccountForm(FlaskForm): ) email_confirmed = BooleanField( "Confirmer l'email", - description="Si décoché, l'utilisateur devra demander explicitement un email "\ + description="Si décoché, l'utilisateur devra demander explicitement un email " "de validation, ou faire valider son adresse email par un administrateur.", ) password = PasswordField( @@ -206,7 +209,7 @@ class AdminUpdateAccountForm(FlaskForm): description="L'ancien mot de passe ne pourra pas être récupéré !", validators=[ Optional(), - vd.password, + vd.password.is_strong, ], ) xp = DecimalField( diff --git a/app/forms/forum.py b/app/forms/forum.py index 328ff07..4cf4d2d 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -11,13 +11,16 @@ class CommentForm(FlaskForm): submit = SubmitField('Commenter') preview = SubmitField('Prévisualiser') + class AnonymousCommentForm(CommentForm): pseudo = StringField('Pseudo', - validators=[InputRequired(), vd.name_valid, vd.name_available]) + validators=[InputRequired(), vd.name.valid, vd.name.available]) + class CommentEditForm(CommentForm): submit = SubmitField('Modifier') + class AnonymousCommentEditForm(CommentEditForm, AnonymousCommentForm): pass @@ -27,5 +30,6 @@ class TopicCreationForm(CommentForm): validators=[InputRequired(), Length(min=3, max=32)]) submit = SubmitField('Créer le sujet') + class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm): pass diff --git a/app/utils/validators/__init__.py b/app/utils/validators/__init__.py index c02d089..c7d2e70 100644 --- a/app/utils/validators/__init__.py +++ b/app/utils/validators/__init__.py @@ -1,104 +1,19 @@ from flask_login import current_user from wtforms.validators import ValidationError -from PIL import Image -from app.models.users import Member, User +from app.models.users import Member from app.models.trophies import Title -from app.utils.valid_name import valid_name -from app.utils.unicode_names import normalize -from math import log from werkzeug.exceptions import NotFound -import app.utils.ldap as ldap - -from config import V5Config from app.utils.validators.file import * +from app.utils.validators.name import * +from app.utils.validators.password import * -# TODO: clean this shit into split files - -def name_valid(form, name): - valid = valid_name(name.data) - default = "Nom d'utilisateur invalide (erreur interne)" - msg = { - "too-short": - "Le nom d'utilisateur doit faire au moins " - f"{User.NAME_MINLEN} caractères.", - "too-long": - "Le nom d'utilisateur doit faire au plus " - f"{User.NAME_MAXLEN} caractères.", - "cant-normalize": - "Ce nom d'utilisateur contient des caractères interdits. Les " - "caractères autorisés sont les lettres, lettres accentuées, " - 'chiffres ainsi que "-" (tiret), "." (point), "~" (tilde) et ' - '"_" (underscore).', - "no-letter": - "Le nom d'utilisateur doit contenir au moins une lettre.", - "forbidden": - "Ce nom d'utilisateur est interdit." - } - if valid is not True: - err = ' '.join(msg.get(code, default) for code in valid) - raise ValidationError(err) - -def name_available(form, name): - # If the name is invalid, name_valid() will return a meaningful message - try: - norm = normalize(name.data) - except ValueError: - return - - member = Member.query.filter_by(norm=norm).first() - if member is not None: - raise ValidationError("Ce nom d'utilisateur est indisponible.") - - # Double check with LDAP if needed - if V5Config.USE_LDAP: - member = ldap.get_member(norm) - if member is not None: - raise ValidationError("Ce nom d'utilisateur est indisponible.") def email(form, email): member = Member.query.filter_by(email=email.data).first() if member is not None: raise ValidationError('Adresse email déjà utilisée.') -def password(form, password): - # To avoid errors in forms where password is optionnal - if len(password.data) == 0: - return - - def entropy(password): - """Estimate entropy of a password, in bits""" - # If you edit this function, please edit accordingly the JS one - # in static/script/entropy.js - chars = [ - "abcdefghijklmnopqrstuvwxyz", - "ABCDFEGHIJKLMNOPQRSTUVWXYZ", - "0123456789", - " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars - "áàâéèêíìîóòôúùûç", - ] - used = set() - for c in password: - for i in chars: - if c in i: - used.add(i) - return log(len(''.join(used)) ** len(password), 2) - - if entropy(password.data) < 60: - raise ValidationError("Mot de passe pas assez complexe") - -def avatar(form, avatar): - try: - Image.open(avatar.data) - except IOError: - raise ValidationError("Avatar invalide") - -def old_password(form, field): - if field.data: - if not form.old_password.data: - raise ValidationError('Votre ancien mot de passe est requis pour cette modification.') - if not current_user.check_password(form.old_password.data): - raise ValidationError('Mot de passe actuel erroné.') def id_exists(object): """Check if an id exists in a table""" @@ -112,10 +27,12 @@ def id_exists(object): raise ValidationError('L\'id n\'existe pas dans la BDD') return _id_exists + def css(form, css): """Check if input is valid and sane CSS""" pass + def own_title(form, title): # Everyone can use "Member" if title.data == -1: diff --git a/app/utils/validators/file.py b/app/utils/validators/file.py index c120ef6..7c2da7b 100644 --- a/app/utils/validators/file.py +++ b/app/utils/validators/file.py @@ -2,12 +2,15 @@ from flask_login import current_user from wtforms.validators import ValidationError, StopValidation from werkzeug.utils import secure_filename from app.utils.filesize import filesize +from PIL import Image import re + def optional(form, files): if(len(files.data) == 0 or files.data[0].filename == ""): raise StopValidation() + def count(form, files): if current_user.is_authenticated: if current_user.priv("no-upload-limits"): @@ -18,6 +21,7 @@ def count(form, files): if len(files.data) > 3: raise ValidationError("3 fichiers maximum autorisés") + def extension(form, files): valid_extensions = [ "g[123][a-z]|cpa|c1a|fxi|cat|mcs|xcp|fls", # Casio files @@ -36,7 +40,9 @@ def extension(form, files): errors.append("." + ext) if len(errors) > 0: - raise ValidationError(f"Extension(s) invalide(s) ({', '.join(errors)})") + raise ValidationError("Extension(s) invalide(s)" + f"({', '.join(errors)})") + def size(form, files): """There is no global limit to file sizes""" @@ -50,6 +56,7 @@ def size(form, files): if size > 500e3: # 500 ko per comment for a guest raise ValidationError("Fichiers trop lourds (max 500 ko)") + def namelength(form, files): errors = [] for f in files.data: @@ -57,5 +64,17 @@ def namelength(form, files): if len(name) > 64: errors.append(f.filename) if len(errors) > 0: - raise ValidationError(f"Noms trop longs, 64 caractères max " \ + raise ValidationError("Noms trop longs, 64 caractères max " f"({', '.join(errors)})") + + +def is_image(form, avatar): + try: + Image.open(avatar.data) + except IOError: + raise ValidationError("Avatar invalide") + + +def avatar_size(form, file): + if filesize(file.data) > 200e3: + raise ValidationError("Fichier trop lourd (max 200 ko)") diff --git a/app/utils/validators/name.py b/app/utils/validators/name.py new file mode 100644 index 0000000..b86f23f --- /dev/null +++ b/app/utils/validators/name.py @@ -0,0 +1,49 @@ +from wtforms.validators import ValidationError +from app.utils.valid_name import valid_name +from app.models.users import User, Member +import app.utils.ldap as ldap +from app.utils.unicode_names import normalize +from config import V5Config + + +def valid(form, name): + valid = valid_name(name.data) + default = "Nom d'utilisateur invalide (erreur interne)" + msg = { + "too-short": + "Le nom d'utilisateur doit faire au moins " + f"{User.NAME_MINLEN} caractères.", + "too-long": + "Le nom d'utilisateur doit faire au plus " + f"{User.NAME_MAXLEN} caractères.", + "cant-normalize": + "Ce nom d'utilisateur contient des caractères interdits. Les " + "caractères autorisés sont les lettres, lettres accentuées, " + 'chiffres ainsi que "-" (tiret), "." (point), "~" (tilde) et ' + '"_" (underscore).', + "no-letter": + "Le nom d'utilisateur doit contenir au moins une lettre.", + "forbidden": + "Ce nom d'utilisateur est interdit." + } + if valid is not True: + err = ' '.join(msg.get(code, default) for code in valid) + raise ValidationError(err) + + +def available(form, name): + # If the name is invalid, name_valid() will return a meaningful message + try: + norm = normalize(name.data) + except ValueError: + return + + member = Member.query.filter_by(norm=norm).first() + if member is not None: + raise ValidationError("Ce nom d'utilisateur est indisponible.") + + # Double check with LDAP if needed + if V5Config.USE_LDAP: + member = ldap.get_member(norm) + if member is not None: + raise ValidationError("Ce nom d'utilisateur est indisponible.") diff --git a/app/utils/validators/password.py b/app/utils/validators/password.py new file mode 100644 index 0000000..7f1b9ab --- /dev/null +++ b/app/utils/validators/password.py @@ -0,0 +1,38 @@ +from wtforms.validators import ValidationError +from flask_login import current_user +from math import log + + +def is_strong(form, password): + # To avoid errors in forms where password is optionnal + if len(password.data) == 0: + return + + def entropy(password): + """Estimate entropy of a password, in bits""" + # If you edit this function, please edit accordingly the JS one + # in static/script/entropy.js + chars = [ + "abcdefghijklmnopqrstuvwxyz", + "ABCDFEGHIJKLMNOPQRSTUVWXYZ", + "0123456789", + " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars + "áàâéèêíìîóòôúùûç", + ] + used = set() + for c in password: + for i in chars: + if c in i: + used.add(i) + return log(len(''.join(used)) ** len(password), 2) + + if entropy(password.data) < 60: + raise ValidationError("Mot de passe pas assez complexe") + + +def old_password(form, field): + if field.data: + if not form.old_password.data: + raise ValidationError('Votre ancien mot de passe est requis pour cette modification.') + if not current_user.check_password(form.old_password.data): + raise ValidationError('Mot de passe actuel erroné.') From a7bde62ff23d29a937fd160a23258acac53fa5cc Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 6 Aug 2020 21:05:49 +0200 Subject: [PATCH 25/94] feat(trophies): added programs count --- app/models/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/users.py b/app/models/users.py index 40c45de..faf4add 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -112,6 +112,7 @@ class Member(User): trophies = db.relationship('Trophy', secondary=TrophyMember, back_populates='owners') topics = db.relationship('Topic') + programs = db.relationship('Program') comments = db.relationship('Comment') # Displayed title @@ -373,8 +374,7 @@ class Member(User): progress(levels, post_count) if context in ["new-program", None]: - # TODO: Amount of programs by the user - program_count = 0 + program_count = self.programs.count() levels = { 5: "Programmeur du dimanche", From 7395835e719cdcb2c266b01ebe44b23ccec80c3d Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 6 Aug 2020 21:19:01 +0200 Subject: [PATCH 26/94] refact: models files are nom uniform (all singular) --- app/forms/{trophies.py => trophy.py} | 0 app/models/{privs.py => priv.py} | 2 +- app/models/{trophies.py => trophy.py} | 0 app/models/{users.py => user.py} | 4 ++-- app/routes/account/account.py | 4 ++-- app/routes/account/login.py | 4 ++-- app/routes/admin/account.py | 6 +++--- app/routes/admin/groups.py | 4 ++-- app/routes/admin/trophies.py | 4 ++-- app/routes/forum/index.py | 2 +- app/routes/forum/topic.py | 2 +- app/routes/users.py | 4 ++-- app/utils/filters/is_title.py | 2 +- app/utils/notify.py | 2 +- app/utils/valid_name.py | 2 +- app/utils/validators/__init__.py | 4 ++-- app/utils/validators/name.py | 2 +- master.py | 6 +++--- 18 files changed, 27 insertions(+), 27 deletions(-) rename app/forms/{trophies.py => trophy.py} (100%) rename app/models/{privs.py => priv.py} (97%) rename app/models/{trophies.py => trophy.py} (100%) rename app/models/{users.py => user.py} (99%) diff --git a/app/forms/trophies.py b/app/forms/trophy.py similarity index 100% rename from app/forms/trophies.py rename to app/forms/trophy.py diff --git a/app/models/privs.py b/app/models/priv.py similarity index 97% rename from app/models/privs.py rename to app/models/priv.py index 692b3ae..ff96ae4 100644 --- a/app/models/privs.py +++ b/app/models/priv.py @@ -1,5 +1,5 @@ # Planète Casio v5 -# models.privs: Database models for groups and privilege management +# models.priv: Database models for groups and privilege management from app import db diff --git a/app/models/trophies.py b/app/models/trophy.py similarity index 100% rename from app/models/trophies.py rename to app/models/trophy.py diff --git a/app/models/users.py b/app/models/user.py similarity index 99% rename from app/models/users.py rename to app/models/user.py index faf4add..13c8ee2 100644 --- a/app/models/users.py +++ b/app/models/user.py @@ -4,9 +4,9 @@ from sqlalchemy import func as SQLfunc from os.path import isfile from PIL import Image from app import app, db -from app.models.privs import SpecialPrivilege, Group, GroupMember, \ +from app.models.priv import SpecialPrivilege, Group, GroupMember, \ GroupPrivilege -from app.models.trophies import Trophy, TrophyMember, Title +from app.models.trophy import Trophy, TrophyMember, Title from app.models.notification import Notification import app.utils.unicode_names as unicode_names import app.utils.ldap as ldap diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 18fad37..f5df8d5 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -3,8 +3,8 @@ from flask_login import login_required, current_user, logout_user from app import app, db from app.forms.account import UpdateAccountForm, RegistrationForm, \ DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm -from app.models.users import Member -from app.models.trophies import Title +from app.model.user import Member +from app.models.trophy import Title from app.utils.render import render from app.utils.send_mail import send_validation_mail, send_reset_password_mail from app.utils.priv_required import guest_only diff --git a/app/routes/account/login.py b/app/routes/account/login.py index 2c4238e..13709c6 100644 --- a/app/routes/account/login.py +++ b/app/routes/account/login.py @@ -3,8 +3,8 @@ from flask_login import login_user, logout_user, login_required, current_user from urllib.parse import urlparse, urljoin from app import app from app.forms.login import LoginForm -from app.models.users import Member -from app.models.privs import Group +from app.model.user import Member +from app.models.priv import Group from app.utils.render import render from app.utils.send_mail import send_validation_mail import datetime diff --git a/app/routes/admin/account.py b/app/routes/admin/account.py index 1abd8c0..d6bf49c 100644 --- a/app/routes/admin/account.py +++ b/app/routes/admin/account.py @@ -2,9 +2,9 @@ from flask import flash, redirect, url_for, request from flask_login import current_user from wtforms import BooleanField from app.utils.priv_required import priv_required -from app.models.users import Member -from app.models.trophies import Trophy, Title -from app.models.privs import Group +from app.model.user import Member +from app.models.trophy import Trophy, Title +from app.models.priv import Group from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \ AdminAccountEditTrophyForm, AdminAccountEditGroupForm from app.utils.render import render diff --git a/app/routes/admin/groups.py b/app/routes/admin/groups.py index 35a03cb..611997c 100644 --- a/app/routes/admin/groups.py +++ b/app/routes/admin/groups.py @@ -1,8 +1,8 @@ from app.utils.priv_required import priv_required from flask_wtf import FlaskForm from wtforms import SubmitField -from app.models.users import Member, Group, GroupPrivilege -from app.models.privs import SpecialPrivilege +from app.model.user import Member, Group, GroupPrivilege +from app.models.priv import SpecialPrivilege from app.utils.render import render from app import app, db import yaml diff --git a/app/routes/admin/trophies.py b/app/routes/admin/trophies.py index 5eb59d0..80265bc 100644 --- a/app/routes/admin/trophies.py +++ b/app/routes/admin/trophies.py @@ -1,7 +1,7 @@ from flask import request, flash, redirect, url_for from app.utils.priv_required import priv_required -from app.models.trophies import Trophy, Title -from app.forms.trophies import TrophyForm, DeleteTrophyForm +from app.models.trophy import Trophy, Title +from app.forms.trophy import TrophyForm, DeleteTrophyForm from app.utils.render import render from app import app, db diff --git a/app/routes/forum/index.py b/app/routes/forum/index.py index 8843824..55a97ff 100644 --- a/app/routes/forum/index.py +++ b/app/routes/forum/index.py @@ -9,7 +9,7 @@ from app.models.forum import Forum from app.models.topic import Topic from app.models.thread import Thread from app.models.comment import Comment -from app.models.users import Guest +from app.model.user import Guest from app.models.attachment import Attachment diff --git a/app/routes/forum/topic.py b/app/routes/forum/topic.py index d150c65..f855936 100644 --- a/app/routes/forum/topic.py +++ b/app/routes/forum/topic.py @@ -9,7 +9,7 @@ from app.models.forum import Forum from app.models.topic import Topic from app.models.thread import Thread from app.models.comment import Comment -from app.models.users import Guest +from app.model.user import Guest from app.models.attachment import Attachment diff --git a/app/routes/users.py b/app/routes/users.py index 7172654..a1691af 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -2,8 +2,8 @@ from flask import redirect, url_for, send_file from werkzeug.utils import secure_filename import os.path from app import app -from app.models.users import Member -from app.models.trophies import Trophy +from app.model.user import Member +from app.models.trophy import Trophy from app.utils import unicode_names from app.utils.render import render from config import V5Config diff --git a/app/utils/filters/is_title.py b/app/utils/filters/is_title.py index bee2c42..15e3c39 100644 --- a/app/utils/filters/is_title.py +++ b/app/utils/filters/is_title.py @@ -1,5 +1,5 @@ from app import app -from app.models.trophies import Title +from app.models.trophy import Title @app.template_filter('is_title') diff --git a/app/utils/notify.py b/app/utils/notify.py index 86d9f80..c26df9e 100644 --- a/app/utils/notify.py +++ b/app/utils/notify.py @@ -1,6 +1,6 @@ from app import db from app.models.notification import Notification -from app.models.users import Member +from app.model.user import Member def notify(user, message, href=None): """ Notify a user (by id, name or object reference) with a message. diff --git a/app/utils/valid_name.py b/app/utils/valid_name.py index 158cc40..bf8ef39 100644 --- a/app/utils/valid_name.py +++ b/app/utils/valid_name.py @@ -1,5 +1,5 @@ from app.utils.unicode_names import normalize -from app.models.users import User +from app.model.user import User import re def valid_name(name, msg=False): diff --git a/app/utils/validators/__init__.py b/app/utils/validators/__init__.py index c7d2e70..0d97e69 100644 --- a/app/utils/validators/__init__.py +++ b/app/utils/validators/__init__.py @@ -1,7 +1,7 @@ from flask_login import current_user from wtforms.validators import ValidationError -from app.models.users import Member -from app.models.trophies import Title +from app.model.user import Member +from app.models.trophy import Title from werkzeug.exceptions import NotFound from app.utils.validators.file import * diff --git a/app/utils/validators/name.py b/app/utils/validators/name.py index b86f23f..6a98c6f 100644 --- a/app/utils/validators/name.py +++ b/app/utils/validators/name.py @@ -1,6 +1,6 @@ from wtforms.validators import ValidationError from app.utils.valid_name import valid_name -from app.models.users import User, Member +from app.model.user import User, Member import app.utils.ldap as ldap from app.utils.unicode_names import normalize from config import V5Config diff --git a/master.py b/master.py index f03ccc8..e959fcc 100755 --- a/master.py +++ b/master.py @@ -1,9 +1,9 @@ #! /usr/bin/python3 from app import app, db -from app.models.users import Member, Group, GroupPrivilege -from app.models.privs import SpecialPrivilege -from app.models.trophies import Trophy, Title +from app.model.user import Member, Group, GroupPrivilege +from app.models.priv import SpecialPrivilege +from app.models.trophy import Trophy, Title from app.models.forum import Forum from app.utils import unicode_names import os From c5e99807e37fbe14e5c9894426ce9fc3e53379c3 Mon Sep 17 00:00:00 2001 From: Darks Date: Tue, 25 Aug 2020 22:57:45 +0200 Subject: [PATCH 27/94] fix a typo in many files --- app/routes/account/account.py | 2 +- app/routes/account/login.py | 2 +- app/routes/admin/account.py | 2 +- app/routes/admin/groups.py | 2 +- app/routes/forum/index.py | 2 +- app/routes/forum/topic.py | 2 +- app/routes/users.py | 2 +- app/utils/notify.py | 2 +- app/utils/valid_name.py | 2 +- app/utils/validators/__init__.py | 2 +- app/utils/validators/name.py | 2 +- master.py | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/routes/account/account.py b/app/routes/account/account.py index f5df8d5..532c472 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -3,7 +3,7 @@ from flask_login import login_required, current_user, logout_user from app import app, db from app.forms.account import UpdateAccountForm, RegistrationForm, \ DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm -from app.model.user import Member +from app.models.user import Member from app.models.trophy import Title from app.utils.render import render from app.utils.send_mail import send_validation_mail, send_reset_password_mail diff --git a/app/routes/account/login.py b/app/routes/account/login.py index 13709c6..058219f 100644 --- a/app/routes/account/login.py +++ b/app/routes/account/login.py @@ -3,7 +3,7 @@ from flask_login import login_user, logout_user, login_required, current_user from urllib.parse import urlparse, urljoin from app import app from app.forms.login import LoginForm -from app.model.user import Member +from app.models.user import Member from app.models.priv import Group from app.utils.render import render from app.utils.send_mail import send_validation_mail diff --git a/app/routes/admin/account.py b/app/routes/admin/account.py index d6bf49c..1d4c04c 100644 --- a/app/routes/admin/account.py +++ b/app/routes/admin/account.py @@ -2,7 +2,7 @@ from flask import flash, redirect, url_for, request from flask_login import current_user from wtforms import BooleanField from app.utils.priv_required import priv_required -from app.model.user import Member +from app.models.user import Member from app.models.trophy import Trophy, Title from app.models.priv import Group from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \ diff --git a/app/routes/admin/groups.py b/app/routes/admin/groups.py index 611997c..7e667cd 100644 --- a/app/routes/admin/groups.py +++ b/app/routes/admin/groups.py @@ -1,7 +1,7 @@ from app.utils.priv_required import priv_required from flask_wtf import FlaskForm from wtforms import SubmitField -from app.model.user import Member, Group, GroupPrivilege +from app.models.user import Member, Group, GroupPrivilege from app.models.priv import SpecialPrivilege from app.utils.render import render from app import app, db diff --git a/app/routes/forum/index.py b/app/routes/forum/index.py index 55a97ff..aa0d209 100644 --- a/app/routes/forum/index.py +++ b/app/routes/forum/index.py @@ -9,7 +9,7 @@ from app.models.forum import Forum from app.models.topic import Topic from app.models.thread import Thread from app.models.comment import Comment -from app.model.user import Guest +from app.models.user import Guest from app.models.attachment import Attachment diff --git a/app/routes/forum/topic.py b/app/routes/forum/topic.py index f855936..173e066 100644 --- a/app/routes/forum/topic.py +++ b/app/routes/forum/topic.py @@ -9,7 +9,7 @@ from app.models.forum import Forum from app.models.topic import Topic from app.models.thread import Thread from app.models.comment import Comment -from app.model.user import Guest +from app.models.user import Guest from app.models.attachment import Attachment diff --git a/app/routes/users.py b/app/routes/users.py index a1691af..3cb4779 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -2,7 +2,7 @@ from flask import redirect, url_for, send_file from werkzeug.utils import secure_filename import os.path from app import app -from app.model.user import Member +from app.models.user import Member from app.models.trophy import Trophy from app.utils import unicode_names from app.utils.render import render diff --git a/app/utils/notify.py b/app/utils/notify.py index c26df9e..245885f 100644 --- a/app/utils/notify.py +++ b/app/utils/notify.py @@ -1,6 +1,6 @@ from app import db from app.models.notification import Notification -from app.model.user import Member +from app.models.user import Member def notify(user, message, href=None): """ Notify a user (by id, name or object reference) with a message. diff --git a/app/utils/valid_name.py b/app/utils/valid_name.py index bf8ef39..5b94228 100644 --- a/app/utils/valid_name.py +++ b/app/utils/valid_name.py @@ -1,5 +1,5 @@ from app.utils.unicode_names import normalize -from app.model.user import User +from app.models.user import User import re def valid_name(name, msg=False): diff --git a/app/utils/validators/__init__.py b/app/utils/validators/__init__.py index 0d97e69..08899e4 100644 --- a/app/utils/validators/__init__.py +++ b/app/utils/validators/__init__.py @@ -1,6 +1,6 @@ from flask_login import current_user from wtforms.validators import ValidationError -from app.model.user import Member +from app.models.user import Member from app.models.trophy import Title from werkzeug.exceptions import NotFound diff --git a/app/utils/validators/name.py b/app/utils/validators/name.py index 6a98c6f..9ec6b69 100644 --- a/app/utils/validators/name.py +++ b/app/utils/validators/name.py @@ -1,6 +1,6 @@ from wtforms.validators import ValidationError from app.utils.valid_name import valid_name -from app.model.user import User, Member +from app.models.user import User, Member import app.utils.ldap as ldap from app.utils.unicode_names import normalize from config import V5Config diff --git a/master.py b/master.py index e959fcc..dd588f0 100755 --- a/master.py +++ b/master.py @@ -1,7 +1,7 @@ #! /usr/bin/python3 from app import app, db -from app.model.user import Member, Group, GroupPrivilege +from app.models.user import Member, Group, GroupPrivilege from app.models.priv import SpecialPrivilege from app.models.trophy import Trophy, Title from app.models.forum import Forum From df745fd4a62f1be97f4a430b498bd2bfd4c54fb3 Mon Sep 17 00:00:00 2001 From: Darks Date: Tue, 25 Aug 2020 23:05:54 +0200 Subject: [PATCH 28/94] ldap: add sub-organization feature --- app/utils/ldap.py | 22 +++++++++++----------- config.py | 3 ++- local_config.py.default | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/utils/ldap.py b/app/utils/ldap.py index 4e62639..78eaddb 100644 --- a/app/utils/ldap.py +++ b/app/utils/ldap.py @@ -8,8 +8,8 @@ def get_member(username): username = normalize(username) # Never safe enough conn = ldap.initialize("ldap://localhost") # Search for user - r = conn.search_s(V5Config.LDAP_ORGANIZATION, ldap.SCOPE_SUBTREE, - f'(cn={username})') + r = conn.search_s(f"{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}", + ldap.SCOPE_SUBTREE, f'(cn={username})') if len(r) > 0: return r[0] else: @@ -21,7 +21,7 @@ def edit(user, fields): conn = ldap.initialize("ldap://localhost") # TODO: do this # Connect as root - # conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}', + # conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ENV}', # V5Config.LDAP_PASSWORD) # old_value = {"userPassword": ["my_old_password"]} # new_value = {"userPassword": ["my_new_password"]} @@ -38,9 +38,9 @@ def set_password(user, password): """ Set password for a user. """ conn = ldap.initialize("ldap://localhost") # Connect as root - conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}', + conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}', V5Config.LDAP_PASSWORD) - conn.passwd_s(f"cn={user.norm},{V5Config.LDAP_ORGANIZATION}", + conn.passwd_s(f"cn={user.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}", None, password) @@ -48,8 +48,8 @@ def check_password(user, password): """ Try to login a user through LDAP register. """ conn = ldap.initialize("ldap://localhost") try: - conn.simple_bind_s(f"cn={user.norm},{V5Config.LDAP_ORGANIZATION}", - password) + conn.simple_bind_s(f"cn={user.norm},{V5Config.LDAP_ENV}," \ + f"{V5Config.LDAP_ROOT}", password) except ldap.INVALID_CREDENTIALS: return False return True @@ -62,10 +62,10 @@ def add_member(member): return conn = ldap.initialize("ldap://localhost") # Connect as root - conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}', + conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}', V5Config.LDAP_PASSWORD) # Create fields - dn = f'cn={member.norm},{V5Config.LDAP_ORGANIZATION}' + dn = f'cn={member.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}' modlist = addModlist({ 'objectClass': [bytes('inetOrgPerson', 'UTF8')], 'cn': [bytes(member.norm, 'UTF8')], @@ -83,9 +83,9 @@ def delete_member(member): """ Remove a member from LDAP register """ conn = ldap.initialize("ldap://localhost") # Connect as root - conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}', + conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}', V5Config.LDAP_PASSWORD) # Create fields - dn = f'cn={member.norm},{V5Config.LDAP_ORGANIZATION}' + dn = f'cn={member.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}' # Delete the user conn.delete_s(dn) diff --git a/config.py b/config.py index ae5df4d..be5e06f 100644 --- a/config.py +++ b/config.py @@ -29,7 +29,8 @@ class DefaultConfig(object): USE_LDAP = False # LDAP configuration LDAP_PASSWORD = "openldap" - LDAP_ORGANIZATION = "o=planet-casio" + LDAP_ROOT = "o=planet-casio" + LDAP_ENV = "o=prod" # Secret key used to authenticate tokens. **USE YOURS!** SECRET_KEY = "a-random-secret-key" # Uploaded data folder diff --git a/local_config.py.default b/local_config.py.default index 7885f5f..22986fe 100644 --- a/local_config.py.default +++ b/local_config.py.default @@ -5,7 +5,7 @@ class LocalConfig(object): DB_NAME = "pcv5" USE_LDAP = True LDAP_PASSWORD = "openldap" - LDAP_ORGANIZATION = "o=planet-casio" + LDAP_ENV = "o=prod" SECRET_KEY = "a-random-secret-key" # CHANGE THIS VALUE *NOW* AVATARS_FOLDER = '/home/pc/data/avatars/' ENABLE_GUEST_POST = True From da78d0da701af44c0f9602b2018eb218a093b874 Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 27 Aug 2020 23:12:31 +0200 Subject: [PATCH 29/94] admin: add a page to get configuration used --- app/routes/__init__.py | 3 ++- app/routes/admin/config.py | 13 +++++++++++++ app/templates/admin/config.html | 18 ++++++++++++++++++ app/templates/admin/index.html | 1 + 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 app/routes/admin/config.py create mode 100644 app/templates/admin/config.html diff --git a/app/routes/__init__.py b/app/routes/__init__.py index c2a59af..488f0bd 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -2,7 +2,8 @@ from app.routes import index, search, users, tools, development from app.routes.account import login, account, notification -from app.routes.admin import index, groups, account, trophies, forums, attachments +from app.routes.admin import index, groups, account, trophies, forums, \ + attachments, config from app.routes.forum import index, topic from app.routes.programs import index from app.routes.posts import edit diff --git a/app/routes/admin/config.py b/app/routes/admin/config.py new file mode 100644 index 0000000..102287d --- /dev/null +++ b/app/routes/admin/config.py @@ -0,0 +1,13 @@ +from app.utils.priv_required import priv_required +from app.utils.render import render +from app import app +from config import V5Config + +@app.route('/admin/config', methods=['GET']) +@priv_required('access-admin-panel') +def adm_config(): + config = {k: getattr(V5Config, k) for k in [ + "DOMAIN", "DB_NAME", "USE_LDAP", "LDAP_ROOT", "LDAP_ENV", + "ENABLE_GUEST_POST", "ENABLE_EMAIL_CONFIRMATION", "SEND_MAILS" + ]} + return render('admin/config.html', config=config) diff --git a/app/templates/admin/config.html b/app/templates/admin/config.html new file mode 100644 index 0000000..104a9cf --- /dev/null +++ b/app/templates/admin/config.html @@ -0,0 +1,18 @@ +{% extends "base/base.html" %} + +{% block title %} +Panneau d'administration »

Configuration du site

+{% endblock %} + +{% block content %} +
+

Configuration du site

+ + + + {% for k in config %} + + {% endfor %} +
NomValeur
{{ k }}{{ config[k] }}
+
+{% endblock %} diff --git a/app/templates/admin/index.html b/app/templates/admin/index.html index eef615f..0b2b8c4 100644 --- a/app/templates/admin/index.html +++ b/app/templates/admin/index.html @@ -12,6 +12,7 @@
  • Titres et trophées
  • Arbre des forums
  • Pièces-jointes
  • +
  • Configuration du site
  • {% endblock %} From 80bbcac8db94b74a747ec5d009aada06842fb80a Mon Sep 17 00:00:00 2001 From: Louis Gatin Date: Tue, 1 Sep 2020 14:32:30 +0200 Subject: [PATCH 30/94] themes: added Flammingkite theme Still need to implement custom theme selection --- app/static/css/themes/FK_dark_theme.css | 113 ++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 app/static/css/themes/FK_dark_theme.css diff --git a/app/static/css/themes/FK_dark_theme.css b/app/static/css/themes/FK_dark_theme.css new file mode 100644 index 0000000..712b7a8 --- /dev/null +++ b/app/static/css/themes/FK_dark_theme.css @@ -0,0 +1,113 @@ +/* Theme metadata +@NAME: FK's Dark Theme +@AUTHOR: Flammingkite +*/ + + +/* +#22292c = gris bleuté, menu original +#1c2122 = gris foncé, intérieur du menu +*/ + +:root { + --background: #1c2124; /*22292c, 1c2124, 1E1E1E, 242424,*/ + --text: #f2f2f2; + + --links: #fe2d2d; + + --ok: #149641; + --ok-text: #ffffff; + --ok-active: #0f7331; + + --warn: #f59f25; + --warn-text: #ffffff; + --warn-active: #ea9720; + + --error: #d23a2f; + --error-text: #ffffff; + --error-active: #b32a20; + + --info: #2e7aec; + --info-text: #ffffff; + --info-active: #215ab0; + + --hr-border: 1px solid #b0b0b0; +} + +.form { + --background: #ffffff; + --text: #000000; + --border: 1px solid #c8c8c8; + --border-focused: #7cade0; + --shadow-focused: rgba(87, 143, 228, 0.5); +} + +.editor button { + --background: #ffffff; + --text: #000000; + --border: 1px solid rgba(0, 0, 0, 0); + --border-focused: 1px solid rgba(0, 0, 0, .5); +} + +#light-menu { + --background: #1c2124; /*1c2124, 22292c*/ + --text: #ffffff; + --icons: #ffffff; + --shadow: 0 0 4px rgba(255, 255, 255, 0.15); + + --logo-bg: #bf1c11; + --logo-shadow: 0 0 2px rgba(0, 0, 0, .7); + --logo-active: #d72411; +} + +#menu { + --background: #1c2124; + --text: #ffffff; + --icons: #ffffff; + --shadow: 0 0 8px rgba(0, 0, 0, 0.3); + + --input-bg: #22292c; + --input-text: #ffffff; + --input-border: 1px solid #474747; +} + + + +header { + --background: #0d1215; /*5a5a5a*/ + --text: #000000; + --border: 1px solid #d0d0d0; +} + +footer { + --background: rgba(0, 0, 0, 1); /* #ffffff */ + --text: #a0a0a0; + --border: #d0d0d0; +} + +.flash { + --background: #ffffff; + --text: #212121; + --shadow: 0 1px 12px rgba(0, 0, 0, 0.3); + + /* Uncomment to inherit :root values + --ok: #149641; + --warn: #f59f25; + --error: #d23a2f; + --info: #2e7aec; */ + + --btn-bg: rgba(0, 0, 0, 0); + --btn-text: #000000; + --btn-bg-active: rgba(0, 0, 0, .15); +} + +.profile-xp { + --background: #e0e0e0; + --border: 1px solid #c0c0c0; + --background-xp: #f85555; + --border-xp: 1px solid #d03333; +} + + +table tr:nth-child(even) { --background: rgba(255, 255, 255, 0.15); } +table tr:nth-child(odd) { --background: #1c2124; } /* 22292c = background, 1c2124, 1e1e1e*/ From f15b33ae731601f4abc7e67df2ce8aea24b309cd Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 17 Sep 2020 15:08:36 +0200 Subject: [PATCH 31/94] =?UTF-8?q?Ajout=20de=20l'exemple=20d'activation/d?= =?UTF-8?q?=C3=A9sactivation=20de=20l'envoi=20des=20mails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- local_config.py.default | 1 + 1 file changed, 1 insertion(+) diff --git a/local_config.py.default b/local_config.py.default index 22986fe..91a67ee 100644 --- a/local_config.py.default +++ b/local_config.py.default @@ -9,3 +9,4 @@ class LocalConfig(object): SECRET_KEY = "a-random-secret-key" # CHANGE THIS VALUE *NOW* AVATARS_FOLDER = '/home/pc/data/avatars/' ENABLE_GUEST_POST = True + SEND_MAILS = True From cbdffc12244d2431eee7b56b0f1cd2a1f4668f8b Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 17 Sep 2020 15:18:38 +0200 Subject: [PATCH 32/94] =?UTF-8?q?Ajout=20de=20la=20possibilit=C3=A9=20d'ac?= =?UTF-8?q?tiver=20un=20compte=20par=20la=20cli.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Une fonction permet de modifier le status de vérification de l'adresse mail de l'utilisateur spécifié en paramètre. On peut exploiter la fonction via le script `master.py` --- master.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/master.py b/master.py index dd588f0..dce4592 100755 --- a/master.py +++ b/master.py @@ -42,6 +42,8 @@ Type 'add-group #' to add a new member to a group. Type 'create-trophies' to reset trophies and titles and their icons. Type 'create-forums' to reset the forum tree. + +Type 'enable-user' to enable a email-disabled account. """ # @@ -257,6 +259,17 @@ def add_group(member, group): db.session.add(m) db.session.commit() +def enable_user(member): + norm = unicode_names.normalize(member) + m = Member.query.filter_by(norm=norm).first() + if m is None: + print(f"error: no member has a normalized name of '{norm}'") + return + + m.email_confirmed = True + db.session.add(m) + db.session.commit() + # # Main program # @@ -274,6 +287,7 @@ commands = { "create-trophies": create_trophies, "create-forums": create_forums, "add-group": add_group, + "enable-user": enable_user, } while True: From c4197168062d6343c5a28fa825f681d0e9db5c09 Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 17 Sep 2020 22:36:52 +0200 Subject: [PATCH 33/94] =?UTF-8?q?Utilisation=20de=20https://github.com/spa?= =?UTF-8?q?rksuite/simplemde-markdown-editor=20comme=20=C3=A9diteur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout des fichiers dans les fichiers statiques. (min.css & min.js). Supression des scripts custom d'édition. Remplacement par les bon appels de fonctions dans app/templates/widgets/editor.html --- app/static/css/simplemde.min.css | 7 +++++ app/static/scripts/simplemde.min.js | 15 ++++++++++ app/templates/widgets/editor.html | 44 ++++++++++++----------------- app/utils/render.py | 3 +- 4 files changed, 41 insertions(+), 28 deletions(-) create mode 100644 app/static/css/simplemde.min.css create mode 100644 app/static/scripts/simplemde.min.js diff --git a/app/static/css/simplemde.min.css b/app/static/css/simplemde.min.css new file mode 100644 index 0000000..d62f4d7 --- /dev/null +++ b/app/static/css/simplemde.min.css @@ -0,0 +1,7 @@ +/** + * simplemde v1.11.2 + * Copyright Next Step Webs, Inc. + * @link https://github.com/NextStepWebs/simplemde-markdown-editor + * @license MIT + */ +.CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:none;font-variant-ligatures:none}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;min-height:300px;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-scroll{min-height:300px}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)} \ No newline at end of file diff --git a/app/static/scripts/simplemde.min.js b/app/static/scripts/simplemde.min.js new file mode 100644 index 0000000..50c624f --- /dev/null +++ b/app/static/scripts/simplemde.min.js @@ -0,0 +1,15 @@ +/** + * simplemde v1.11.2 + * Copyright Next Step Webs, Inc. + * @link https://github.com/NextStepWebs/simplemde-markdown-editor + * @license MIT + */ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.SimpleMDE=e()}}(function(){var e;return function t(e,n,r){function i(a,l){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!l&&s)return s(a,!0);if(o)return o(a,!0);var c=new Error("Cannot find module '"+a+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,r)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;at;++t)s[t]=e[t],c[e.charCodeAt(t)]=t;c["-".charCodeAt(0)]=62,c["_".charCodeAt(0)]=63}function i(e){var t,n,r,i,o,a,l=e.length;if(l%4>0)throw new Error("Invalid string. Length must be a multiple of 4");o="="===e[l-2]?2:"="===e[l-1]?1:0,a=new u(3*l/4-o),r=o>0?l-4:l;var s=0;for(t=0,n=0;r>t;t+=4,n+=3)i=c[e.charCodeAt(t)]<<18|c[e.charCodeAt(t+1)]<<12|c[e.charCodeAt(t+2)]<<6|c[e.charCodeAt(t+3)],a[s++]=i>>16&255,a[s++]=i>>8&255,a[s++]=255&i;return 2===o?(i=c[e.charCodeAt(t)]<<2|c[e.charCodeAt(t+1)]>>4,a[s++]=255&i):1===o&&(i=c[e.charCodeAt(t)]<<10|c[e.charCodeAt(t+1)]<<4|c[e.charCodeAt(t+2)]>>2,a[s++]=i>>8&255,a[s++]=255&i),a}function o(e){return s[e>>18&63]+s[e>>12&63]+s[e>>6&63]+s[63&e]}function a(e,t,n){for(var r,i=[],a=t;n>a;a+=3)r=(e[a]<<16)+(e[a+1]<<8)+e[a+2],i.push(o(r));return i.join("")}function l(e){for(var t,n=e.length,r=n%3,i="",o=[],l=16383,c=0,u=n-r;u>c;c+=l)o.push(a(e,c,c+l>u?u:c+l));return 1===r?(t=e[n-1],i+=s[t>>2],i+=s[t<<4&63],i+="=="):2===r&&(t=(e[n-2]<<8)+e[n-1],i+=s[t>>10],i+=s[t>>4&63],i+=s[t<<2&63],i+="="),o.push(i),o.join("")}n.toByteArray=i,n.fromByteArray=l;var s=[],c=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array;r()},{}],2:[function(e,t,n){},{}],3:[function(e,t,n){(function(t){"use strict";function r(){try{var e=new Uint8Array(1);return e.foo=function(){return 42},42===e.foo()&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(t){return!1}}function i(){return a.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function o(e,t){if(i()=t?o(e,t):void 0!==n?"string"==typeof r?o(e,t).fill(n,r):o(e,t).fill(n):o(e,t)}function u(e,t){if(s(t),e=o(e,0>t?0:0|m(t)),!a.TYPED_ARRAY_SUPPORT)for(var n=0;t>n;n++)e[n]=0;return e}function f(e,t,n){if("string"==typeof n&&""!==n||(n="utf8"),!a.isEncoding(n))throw new TypeError('"encoding" must be a valid string encoding');var r=0|v(t,n);return e=o(e,r),e.write(t,n),e}function h(e,t){var n=0|m(t.length);e=o(e,n);for(var r=0;n>r;r+=1)e[r]=255&t[r];return e}function d(e,t,n,r){if(t.byteLength,0>n||t.byteLength=i())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i().toString(16)+" bytes");return 0|e}function g(e){return+e!=e&&(e=0),a.alloc(+e)}function v(e,t){if(a.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"binary":case"raw":case"raws":return n;case"utf8":case"utf-8":case void 0:return q(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return $(e).length;default:if(r)return q(e).length;t=(""+t).toLowerCase(),r=!0}}function y(e,t,n){var r=!1;if((void 0===t||0>t)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),0>=n)return"";if(n>>>=0,t>>>=0,t>=n)return"";for(e||(e="utf8");;)switch(e){case"hex":return I(this,t,n);case"utf8":case"utf-8":return N(this,t,n);case"ascii":return E(this,t,n);case"binary":return O(this,t,n);case"base64":return M(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return P(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function x(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function b(e,t,n,r){function i(e,t){return 1===o?e[t]:e.readUInt16BE(t*o)}var o=1,a=e.length,l=t.length;if(void 0!==r&&(r=String(r).toLowerCase(),"ucs2"===r||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;o=2,a/=2,l/=2,n/=2}for(var s=-1,c=0;a>n+c;c++)if(i(e,n+c)===i(t,-1===s?0:c-s)){if(-1===s&&(s=c),c-s+1===l)return(n+s)*o}else-1!==s&&(c-=c-s),s=-1;return-1}function w(e,t,n,r){n=Number(n)||0;var i=e.length-n;r?(r=Number(r),r>i&&(r=i)):r=i;var o=t.length;if(o%2!==0)throw new Error("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;r>a;a++){var l=parseInt(t.substr(2*a,2),16);if(isNaN(l))return a;e[n+a]=l}return a}function k(e,t,n,r){return V(q(t,e.length-n),e,n,r)}function S(e,t,n,r){return V(G(t),e,n,r)}function C(e,t,n,r){return S(e,t,n,r)}function L(e,t,n,r){return V($(t),e,n,r)}function T(e,t,n,r){return V(Y(t,e.length-n),e,n,r)}function M(e,t,n){return 0===t&&n===e.length?X.fromByteArray(e):X.fromByteArray(e.slice(t,n))}function N(e,t,n){n=Math.min(e.length,n);for(var r=[],i=t;n>i;){var o=e[i],a=null,l=o>239?4:o>223?3:o>191?2:1;if(n>=i+l){var s,c,u,f;switch(l){case 1:128>o&&(a=o);break;case 2:s=e[i+1],128===(192&s)&&(f=(31&o)<<6|63&s,f>127&&(a=f));break;case 3:s=e[i+1],c=e[i+2],128===(192&s)&&128===(192&c)&&(f=(15&o)<<12|(63&s)<<6|63&c,f>2047&&(55296>f||f>57343)&&(a=f));break;case 4:s=e[i+1],c=e[i+2],u=e[i+3],128===(192&s)&&128===(192&c)&&128===(192&u)&&(f=(15&o)<<18|(63&s)<<12|(63&c)<<6|63&u,f>65535&&1114112>f&&(a=f))}}null===a?(a=65533,l=1):a>65535&&(a-=65536,r.push(a>>>10&1023|55296),a=56320|1023&a),r.push(a),i+=l}return A(r)}function A(e){var t=e.length;if(Q>=t)return String.fromCharCode.apply(String,e);for(var n="",r=0;t>r;)n+=String.fromCharCode.apply(String,e.slice(r,r+=Q));return n}function E(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;n>i;i++)r+=String.fromCharCode(127&e[i]);return r}function O(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;n>i;i++)r+=String.fromCharCode(e[i]);return r}function I(e,t,n){var r=e.length;(!t||0>t)&&(t=0),(!n||0>n||n>r)&&(n=r);for(var i="",o=t;n>o;o++)i+=U(e[o]);return i}function P(e,t,n){for(var r=e.slice(t,n),i="",o=0;oe)throw new RangeError("offset is not uint");if(e+t>n)throw new RangeError("Trying to access beyond buffer length")}function D(e,t,n,r,i,o){if(!a.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||o>t)throw new RangeError('"value" argument is out of bounds');if(n+r>e.length)throw new RangeError("Index out of range")}function H(e,t,n,r){0>t&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);o>i;i++)e[n+i]=(t&255<<8*(r?i:1-i))>>>8*(r?i:1-i)}function W(e,t,n,r){0>t&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);o>i;i++)e[n+i]=t>>>8*(r?i:3-i)&255}function B(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(0>n)throw new RangeError("Index out of range")}function _(e,t,n,r,i){return i||B(e,t,n,4,3.4028234663852886e38,-3.4028234663852886e38),Z.write(e,t,n,r,23,4),n+4}function F(e,t,n,r,i){return i||B(e,t,n,8,1.7976931348623157e308,-1.7976931348623157e308),Z.write(e,t,n,r,52,8),n+8}function z(e){if(e=j(e).replace(ee,""),e.length<2)return"";for(;e.length%4!==0;)e+="=";return e}function j(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}function U(e){return 16>e?"0"+e.toString(16):e.toString(16)}function q(e,t){t=t||1/0;for(var n,r=e.length,i=null,o=[],a=0;r>a;a++){if(n=e.charCodeAt(a),n>55295&&57344>n){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(56320>n){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=(i-55296<<10|n-56320)+65536}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,128>n){if((t-=1)<0)break;o.push(n)}else if(2048>n){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(65536>n){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(1114112>n))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function G(e){for(var t=[],n=0;n>8,i=n%256,o.push(i),o.push(r);return o}function $(e){return X.toByteArray(z(e))}function V(e,t,n,r){for(var i=0;r>i&&!(i+n>=t.length||i>=e.length);i++)t[i+n]=e[i];return i}function K(e){return e!==e}var X=e("base64-js"),Z=e("ieee754"),J=e("isarray");n.Buffer=a,n.SlowBuffer=g,n.INSPECT_MAX_BYTES=50,a.TYPED_ARRAY_SUPPORT=void 0!==t.TYPED_ARRAY_SUPPORT?t.TYPED_ARRAY_SUPPORT:r(),n.kMaxLength=i(),a.poolSize=8192,a._augment=function(e){return e.__proto__=a.prototype,e},a.from=function(e,t,n){return l(null,e,t,n)},a.TYPED_ARRAY_SUPPORT&&(a.prototype.__proto__=Uint8Array.prototype,a.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&a[Symbol.species]===a&&Object.defineProperty(a,Symbol.species,{value:null,configurable:!0})),a.alloc=function(e,t,n){return c(null,e,t,n)},a.allocUnsafe=function(e){return u(null,e)},a.allocUnsafeSlow=function(e){return u(null,e)},a.isBuffer=function(e){return!(null==e||!e._isBuffer)},a.compare=function(e,t){if(!a.isBuffer(e)||!a.isBuffer(t))throw new TypeError("Arguments must be Buffers");if(e===t)return 0;for(var n=e.length,r=t.length,i=0,o=Math.min(n,r);o>i;++i)if(e[i]!==t[i]){n=e[i],r=t[i];break}return r>n?-1:n>r?1:0},a.isEncoding=function(e){switch(String(e).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"raw":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},a.concat=function(e,t){if(!J(e))throw new TypeError('"list" argument must be an Array of Buffers');if(0===e.length)return a.alloc(0);var n;if(void 0===t)for(t=0,n=0;nt;t+=2)x(this,t,t+1);return this},a.prototype.swap32=function(){var e=this.length;if(e%4!==0)throw new RangeError("Buffer size must be a multiple of 32-bits");for(var t=0;e>t;t+=4)x(this,t,t+3),x(this,t+1,t+2);return this},a.prototype.toString=function(){var e=0|this.length;return 0===e?"":0===arguments.length?N(this,0,e):y.apply(this,arguments)},a.prototype.equals=function(e){if(!a.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e?!0:0===a.compare(this,e)},a.prototype.inspect=function(){var e="",t=n.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,t).match(/.{2}/g).join(" "),this.length>t&&(e+=" ... ")),""},a.prototype.compare=function(e,t,n,r,i){if(!a.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),0>t||n>e.length||0>r||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(t>>>=0,n>>>=0,r>>>=0,i>>>=0,this===e)return 0;for(var o=i-r,l=n-t,s=Math.min(o,l),c=this.slice(r,i),u=e.slice(t,n),f=0;s>f;++f)if(c[f]!==u[f]){o=c[f],l=u[f];break}return l>o?-1:o>l?1:0},a.prototype.indexOf=function(e,t,n){if("string"==typeof t?(n=t,t=0):t>2147483647?t=2147483647:-2147483648>t&&(t=-2147483648),t>>=0,0===this.length)return-1;if(t>=this.length)return-1;if(0>t&&(t=Math.max(this.length+t,0)),"string"==typeof e&&(e=a.from(e,n)),a.isBuffer(e))return 0===e.length?-1:b(this,e,t,n);if("number"==typeof e)return a.TYPED_ARRAY_SUPPORT&&"function"===Uint8Array.prototype.indexOf?Uint8Array.prototype.indexOf.call(this,e,t):b(this,[e],t,n);throw new TypeError("val must be string, number or Buffer")},a.prototype.includes=function(e,t,n){return-1!==this.indexOf(e,t,n)},a.prototype.write=function(e,t,n,r){if(void 0===t)r="utf8",n=this.length,t=0;else if(void 0===n&&"string"==typeof t)r=t,n=this.length,t=0;else{if(!isFinite(t))throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");t=0|t,isFinite(n)?(n=0|n,void 0===r&&(r="utf8")):(r=n,n=void 0)}var i=this.length-t;if((void 0===n||n>i)&&(n=i),e.length>0&&(0>n||0>t)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return w(this,e,t,n);case"utf8":case"utf-8":return k(this,e,t,n);case"ascii":return S(this,e,t,n);case"binary":return C(this,e,t,n);case"base64":return L(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},a.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var Q=4096;a.prototype.slice=function(e,t){var n=this.length;e=~~e,t=void 0===t?n:~~t,0>e?(e+=n,0>e&&(e=0)):e>n&&(e=n),0>t?(t+=n,0>t&&(t=0)):t>n&&(t=n),e>t&&(t=e);var r;if(a.TYPED_ARRAY_SUPPORT)r=this.subarray(e,t),r.__proto__=a.prototype;else{var i=t-e;r=new a(i,void 0);for(var o=0;i>o;o++)r[o]=this[o+e]}return r},a.prototype.readUIntLE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e],i=1,o=0;++o0&&(i*=256);)r+=this[e+--t]*i;return r},a.prototype.readUInt8=function(e,t){return t||R(e,1,this.length),this[e]},a.prototype.readUInt16LE=function(e,t){return t||R(e,2,this.length),this[e]|this[e+1]<<8},a.prototype.readUInt16BE=function(e,t){return t||R(e,2,this.length),this[e]<<8|this[e+1]},a.prototype.readUInt32LE=function(e,t){return t||R(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},a.prototype.readUInt32BE=function(e,t){return t||R(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},a.prototype.readIntLE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e],i=1,o=0;++o=i&&(r-=Math.pow(2,8*t)),r},a.prototype.readIntBE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return i*=128,o>=i&&(o-=Math.pow(2,8*t)),o},a.prototype.readInt8=function(e,t){return t||R(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},a.prototype.readInt16LE=function(e,t){t||R(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},a.prototype.readInt16BE=function(e,t){t||R(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},a.prototype.readInt32LE=function(e,t){return t||R(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},a.prototype.readInt32BE=function(e,t){return t||R(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},a.prototype.readFloatLE=function(e,t){return t||R(e,4,this.length),Z.read(this,e,!0,23,4)},a.prototype.readFloatBE=function(e,t){return t||R(e,4,this.length),Z.read(this,e,!1,23,4)},a.prototype.readDoubleLE=function(e,t){return t||R(e,8,this.length),Z.read(this,e,!0,52,8)},a.prototype.readDoubleBE=function(e,t){return t||R(e,8,this.length),Z.read(this,e,!1,52,8)},a.prototype.writeUIntLE=function(e,t,n,r){if(e=+e,t=0|t,n=0|n,!r){var i=Math.pow(2,8*n)-1;D(this,e,t,n,i,0)}var o=1,a=0;for(this[t]=255&e;++a=0&&(a*=256);)this[t+o]=e/a&255;return t+n},a.prototype.writeUInt8=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,1,255,0),a.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},a.prototype.writeUInt16LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,65535,0),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):H(this,e,t,!0),t+2},a.prototype.writeUInt16BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,65535,0),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):H(this,e,t,!1),t+2},a.prototype.writeUInt32LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,4294967295,0),a.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):W(this,e,t,!0),t+4},a.prototype.writeUInt32BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,4294967295,0),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):W(this,e,t,!1),t+4},a.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t=0|t,!r){var i=Math.pow(2,8*n-1);D(this,e,t,n,i-1,-i)}var o=0,a=1,l=0;for(this[t]=255&e;++oe&&0===l&&0!==this[t+o-1]&&(l=1),this[t+o]=(e/a>>0)-l&255;return t+n},a.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t=0|t,!r){var i=Math.pow(2,8*n-1);D(this,e,t,n,i-1,-i)}var o=n-1,a=1,l=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)0>e&&0===l&&0!==this[t+o+1]&&(l=1),this[t+o]=(e/a>>0)-l&255;return t+n},a.prototype.writeInt8=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,1,127,-128),a.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),0>e&&(e=255+e+1),this[t]=255&e,t+1},a.prototype.writeInt16LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,32767,-32768),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):H(this,e,t,!0),t+2},a.prototype.writeInt16BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,32767,-32768),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):H(this,e,t,!1),t+2},a.prototype.writeInt32LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,2147483647,-2147483648),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):W(this,e,t,!0),t+4},a.prototype.writeInt32BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,2147483647,-2147483648),0>e&&(e=4294967295+e+1),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):W(this,e,t,!1),t+4},a.prototype.writeFloatLE=function(e,t,n){return _(this,e,t,!0,n)},a.prototype.writeFloatBE=function(e,t,n){return _(this,e,t,!1,n)},a.prototype.writeDoubleLE=function(e,t,n){return F(this,e,t,!0,n)},a.prototype.writeDoubleBE=function(e,t,n){return F(this,e,t,!1,n)},a.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&n>r&&(r=n),r===n)return 0;if(0===e.length||0===this.length)return 0;if(0>t)throw new RangeError("targetStart out of bounds");if(0>n||n>=this.length)throw new RangeError("sourceStart out of bounds");if(0>r)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-tn&&r>t)for(i=o-1;i>=0;i--)e[i+t]=this[i+n];else if(1e3>o||!a.TYPED_ARRAY_SUPPORT)for(i=0;o>i;i++)e[i+t]=this[i+n];else Uint8Array.prototype.set.call(e,this.subarray(n,n+o),t);return o},a.prototype.fill=function(e,t,n,r){if("string"==typeof e){if("string"==typeof t?(r=t,t=0,n=this.length):"string"==typeof n&&(r=n,n=this.length),1===e.length){var i=e.charCodeAt(0);256>i&&(e=i)}if(void 0!==r&&"string"!=typeof r)throw new TypeError("encoding must be a string");if("string"==typeof r&&!a.isEncoding(r))throw new TypeError("Unknown encoding: "+r)}else"number"==typeof e&&(e=255&e);if(0>t||this.length=n)return this;t>>>=0,n=void 0===n?this.length:n>>>0,e||(e=0);var o;if("number"==typeof e)for(o=t;n>o;o++)this[o]=e;else{var l=a.isBuffer(e)?e:q(new a(e,r).toString()),s=l.length;for(o=0;n-t>o;o++)this[o+t]=l[o%s]}return this};var ee=/[^+\/0-9A-Za-z-_]/g}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"base64-js":1,ieee754:15,isarray:16}],4:[function(e,t,n){"use strict";function r(e){return e=e||{},"function"!=typeof e.codeMirrorInstance||"function"!=typeof e.codeMirrorInstance.defineMode?void console.log("CodeMirror Spell Checker: You must provide an instance of CodeMirror via the option `codeMirrorInstance`"):(String.prototype.includes||(String.prototype.includes=function(){return-1!==String.prototype.indexOf.apply(this,arguments)}),void e.codeMirrorInstance.defineMode("spell-checker",function(t){if(!r.aff_loading){r.aff_loading=!0;var n=new XMLHttpRequest;n.open("GET","https://cdn.jsdelivr.net/codemirror.spell-checker/latest/en_US.aff",!0),n.onload=function(){4===n.readyState&&200===n.status&&(r.aff_data=n.responseText,r.num_loaded++,2==r.num_loaded&&(r.typo=new i("en_US",r.aff_data,r.dic_data,{platform:"any"})))},n.send(null)}if(!r.dic_loading){r.dic_loading=!0;var o=new XMLHttpRequest;o.open("GET","https://cdn.jsdelivr.net/codemirror.spell-checker/latest/en_US.dic",!0),o.onload=function(){4===o.readyState&&200===o.status&&(r.dic_data=o.responseText,r.num_loaded++,2==r.num_loaded&&(r.typo=new i("en_US",r.aff_data,r.dic_data,{platform:"any"})))},o.send(null)}var a='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~ ',l={token:function(e){var t=e.peek(),n="";if(a.includes(t))return e.next(),null;for(;null!=(t=e.peek())&&!a.includes(t);)n+=t,e.next();return r.typo&&!r.typo.check(n)?"spell-error":null}},s=e.codeMirrorInstance.getMode(t,t.backdrop||"text/plain");return e.codeMirrorInstance.overlayMode(s,l,!0)}))}var i=e("typo-js");r.num_loaded=0,r.aff_loading=!1,r.dic_loading=!1,r.aff_data="",r.dic_data="",r.typo,t.exports=r},{"typo-js":18}],5:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";function t(e){var t=e.getWrapperElement();e.state.fullScreenRestore={scrollTop:window.pageYOffset,scrollLeft:window.pageXOffset,width:t.style.width,height:t.style.height},t.style.width="",t.style.height="auto",t.className+=" CodeMirror-fullscreen",document.documentElement.style.overflow="hidden",e.refresh()}function n(e){var t=e.getWrapperElement();t.className=t.className.replace(/\s*CodeMirror-fullscreen\b/,""),document.documentElement.style.overflow="";var n=e.state.fullScreenRestore;t.style.width=n.width,t.style.height=n.height,window.scrollTo(n.scrollLeft,n.scrollTop),e.refresh()}e.defineOption("fullScreen",!1,function(r,i,o){o==e.Init&&(o=!1),!o!=!i&&(i?t(r):n(r))})})},{"../../lib/codemirror":10}],6:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){function t(e){e.state.placeholder&&(e.state.placeholder.parentNode.removeChild(e.state.placeholder),e.state.placeholder=null)}function n(e){t(e);var n=e.state.placeholder=document.createElement("pre");n.style.cssText="height: 0; overflow: visible",n.className="CodeMirror-placeholder";var r=e.getOption("placeholder");"string"==typeof r&&(r=document.createTextNode(r)),n.appendChild(r),e.display.lineSpace.insertBefore(n,e.display.lineSpace.firstChild)}function r(e){o(e)&&n(e)}function i(e){var r=e.getWrapperElement(),i=o(e);r.className=r.className.replace(" CodeMirror-empty","")+(i?" CodeMirror-empty":""),i?n(e):t(e)}function o(e){return 1===e.lineCount()&&""===e.getLine(0)}e.defineOption("placeholder","",function(n,o,a){var l=a&&a!=e.Init;if(o&&!l)n.on("blur",r),n.on("change",i),n.on("swapDoc",i),i(n);else if(!o&&l){n.off("blur",r),n.off("change",i),n.off("swapDoc",i),t(n);var s=n.getWrapperElement();s.className=s.className.replace(" CodeMirror-empty","")}o&&!n.hasFocus()&&r(n)})})},{"../../lib/codemirror":10}],7:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";var t=/^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))(\s*)/,n=/^(\s*)(>[> ]*|[*+-]|(\d+)[.)])(\s*)$/,r=/[*+-]\s/;e.commands.newlineAndIndentContinueMarkdownList=function(i){if(i.getOption("disableInput"))return e.Pass;for(var o=i.listSelections(),a=[],l=0;l")>=0?d[2]:parseInt(d[3],10)+1+d[4];a[l]="\n"+p+g+m}}i.replaceSelections(a)}})},{"../../lib/codemirror":10}],8:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.overlayMode=function(t,n,r){return{startState:function(){return{base:e.startState(t),overlay:e.startState(n),basePos:0,baseCur:null,overlayPos:0,overlayCur:null,streamSeen:null}},copyState:function(r){return{base:e.copyState(t,r.base),overlay:e.copyState(n,r.overlay),basePos:r.basePos,baseCur:null,overlayPos:r.overlayPos,overlayCur:null}},token:function(e,i){return(e!=i.streamSeen||Math.min(i.basePos,i.overlayPos)=n.line,d=h?n:s(f,0),p=e.markText(u,d,{className:o});if(null==r?i.push(p):i.splice(r++,0,p),h)break;a=f}}function i(e){for(var t=e.state.markedSelection,n=0;n1)return o(e);var t=e.getCursor("start"),n=e.getCursor("end"),a=e.state.markedSelection;if(!a.length)return r(e,t,n);var s=a[0].find(),u=a[a.length-1].find();if(!s||!u||n.line-t.line=0||c(n,s.from)<=0)return o(e);for(;c(t,s.from)>0;)a.shift().clear(),s=a[0].find();for(c(t,s.from)<0&&(s.to.line-t.line0&&(n.line-u.from.linebo&&setTimeout(function(){s.display.input.reset(!0)},20),jt(this),Ki(),bt(this),this.curOp.forceUpdate=!0,Xr(this,i),r.autofocus&&!Ao||s.hasFocus()?setTimeout(Bi(vn,this),20):yn(this);for(var u in ta)ta.hasOwnProperty(u)&&ta[u](this,r[u],na);k(this),r.finishInit&&r.finishInit(this);for(var f=0;fbo&&(r.gutters.style.zIndex=-1,r.scroller.style.paddingRight=0),wo||go&&Ao||(r.scroller.draggable=!0),e&&(e.appendChild?e.appendChild(r.wrapper):e(r.wrapper)),r.viewFrom=r.viewTo=t.first,r.reportedViewFrom=r.reportedViewTo=t.first,r.view=[],r.renderedView=null,r.externalMeasured=null,r.viewOffset=0,r.lastWrapHeight=r.lastWrapWidth=0,r.updateLineNumbers=null,r.nativeBarWidth=r.barHeight=r.barWidth=0,r.scrollbarsClipped=!1,r.lineNumWidth=r.lineNumInnerWidth=r.lineNumChars=null,r.alignWidgets=!1,r.cachedCharWidth=r.cachedTextHeight=r.cachedPaddingH=null, +r.maxLine=null,r.maxLineLength=0,r.maxLineChanged=!1,r.wheelDX=r.wheelDY=r.wheelStartX=r.wheelStartY=null,r.shift=!1,r.selForContextMenu=null,r.activeTouch=null,n.init(r)}function n(t){t.doc.mode=e.getMode(t.options,t.doc.modeOption),r(t)}function r(e){e.doc.iter(function(e){e.stateAfter&&(e.stateAfter=null),e.styles&&(e.styles=null)}),e.doc.frontier=e.doc.first,_e(e,100),e.state.modeGen++,e.curOp&&Dt(e)}function i(e){e.options.lineWrapping?(Ja(e.display.wrapper,"CodeMirror-wrap"),e.display.sizer.style.minWidth="",e.display.sizerWidth=null):(Za(e.display.wrapper,"CodeMirror-wrap"),h(e)),a(e),Dt(e),lt(e),setTimeout(function(){y(e)},100)}function o(e){var t=yt(e.display),n=e.options.lineWrapping,r=n&&Math.max(5,e.display.scroller.clientWidth/xt(e.display)-3);return function(i){if(kr(e.doc,i))return 0;var o=0;if(i.widgets)for(var a=0;at.maxLineLength&&(t.maxLineLength=n,t.maxLine=e)})}function d(e){var t=Pi(e.gutters,"CodeMirror-linenumbers");-1==t&&e.lineNumbers?e.gutters=e.gutters.concat(["CodeMirror-linenumbers"]):t>-1&&!e.lineNumbers&&(e.gutters=e.gutters.slice(0),e.gutters.splice(t,1))}function p(e){var t=e.display,n=t.gutters.offsetWidth,r=Math.round(e.doc.height+qe(e.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:e.options.fixedGutter?n:0,docHeight:r,scrollHeight:r+Ye(e)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:n}}function m(e,t,n){this.cm=n;var r=this.vert=ji("div",[ji("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),i=this.horiz=ji("div",[ji("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");e(r),e(i),Ea(r,"scroll",function(){r.clientHeight&&t(r.scrollTop,"vertical")}),Ea(i,"scroll",function(){i.clientWidth&&t(i.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,xo&&8>bo&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")}function g(){}function v(t){t.display.scrollbars&&(t.display.scrollbars.clear(),t.display.scrollbars.addClass&&Za(t.display.wrapper,t.display.scrollbars.addClass)),t.display.scrollbars=new e.scrollbarModel[t.options.scrollbarStyle](function(e){t.display.wrapper.insertBefore(e,t.display.scrollbarFiller),Ea(e,"mousedown",function(){t.state.focused&&setTimeout(function(){t.display.input.focus()},0)}),e.setAttribute("cm-not-content","true")},function(e,n){"horizontal"==n?on(t,e):rn(t,e)},t),t.display.scrollbars.addClass&&Ja(t.display.wrapper,t.display.scrollbars.addClass)}function y(e,t){t||(t=p(e));var n=e.display.barWidth,r=e.display.barHeight;x(e,t);for(var i=0;4>i&&n!=e.display.barWidth||r!=e.display.barHeight;i++)n!=e.display.barWidth&&e.options.lineWrapping&&O(e),x(e,p(e)),n=e.display.barWidth,r=e.display.barHeight}function x(e,t){var n=e.display,r=n.scrollbars.update(t);n.sizer.style.paddingRight=(n.barWidth=r.right)+"px",n.sizer.style.paddingBottom=(n.barHeight=r.bottom)+"px",n.heightForcer.style.borderBottom=r.bottom+"px solid transparent",r.right&&r.bottom?(n.scrollbarFiller.style.display="block",n.scrollbarFiller.style.height=r.bottom+"px",n.scrollbarFiller.style.width=r.right+"px"):n.scrollbarFiller.style.display="",r.bottom&&e.options.coverGutterNextToScrollbar&&e.options.fixedGutter?(n.gutterFiller.style.display="block",n.gutterFiller.style.height=r.bottom+"px",n.gutterFiller.style.width=t.gutterWidth+"px"):n.gutterFiller.style.display=""}function b(e,t,n){var r=n&&null!=n.top?Math.max(0,n.top):e.scroller.scrollTop;r=Math.floor(r-Ue(e));var i=n&&null!=n.bottom?n.bottom:r+e.wrapper.clientHeight,o=ni(t,r),a=ni(t,i);if(n&&n.ensure){var l=n.ensure.from.line,s=n.ensure.to.line;o>l?(o=l,a=ni(t,ri(Zr(t,l))+e.wrapper.clientHeight)):Math.min(s,t.lastLine())>=a&&(o=ni(t,ri(Zr(t,s))-e.wrapper.clientHeight),a=s)}return{from:o,to:Math.max(a,o+1)}}function w(e){var t=e.display,n=t.view;if(t.alignWidgets||t.gutters.firstChild&&e.options.fixedGutter){for(var r=C(t)-t.scroller.scrollLeft+e.doc.scrollLeft,i=t.gutters.offsetWidth,o=r+"px",a=0;a=n.viewFrom&&t.visible.to<=n.viewTo&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo)&&n.renderedView==n.view&&0==zt(e))return!1;k(e)&&(Wt(e),t.dims=P(e));var i=r.first+r.size,o=Math.max(t.visible.from-e.options.viewportMargin,r.first),a=Math.min(i,t.visible.to+e.options.viewportMargin);n.viewFroma&&n.viewTo-a<20&&(a=Math.min(i,n.viewTo)),Wo&&(o=br(e.doc,o),a=wr(e.doc,a));var l=o!=n.viewFrom||a!=n.viewTo||n.lastWrapHeight!=t.wrapperHeight||n.lastWrapWidth!=t.wrapperWidth;Ft(e,o,a),n.viewOffset=ri(Zr(e.doc,n.viewFrom)),e.display.mover.style.top=n.viewOffset+"px";var s=zt(e);if(!l&&0==s&&!t.force&&n.renderedView==n.view&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo))return!1;var c=Gi();return s>4&&(n.lineDiv.style.display="none"),R(e,n.updateLineNumbers,t.dims),s>4&&(n.lineDiv.style.display=""),n.renderedView=n.view,c&&Gi()!=c&&c.offsetHeight&&c.focus(),Ui(n.cursorDiv),Ui(n.selectionDiv),n.gutters.style.height=n.sizer.style.minHeight=0,l&&(n.lastWrapHeight=t.wrapperHeight,n.lastWrapWidth=t.wrapperWidth,_e(e,400)),n.updateLineNumbers=null,!0}function N(e,t){for(var n=t.viewport,r=!0;(r&&e.options.lineWrapping&&t.oldDisplayWidth!=$e(e)||(n&&null!=n.top&&(n={top:Math.min(e.doc.height+qe(e.display)-Ve(e),n.top)}),t.visible=b(e.display,e.doc,n),!(t.visible.from>=e.display.viewFrom&&t.visible.to<=e.display.viewTo)))&&M(e,t);r=!1){O(e);var i=p(e);Re(e),y(e,i),E(e,i)}t.signal(e,"update",e),e.display.viewFrom==e.display.reportedViewFrom&&e.display.viewTo==e.display.reportedViewTo||(t.signal(e,"viewportChange",e,e.display.viewFrom,e.display.viewTo),e.display.reportedViewFrom=e.display.viewFrom,e.display.reportedViewTo=e.display.viewTo)}function A(e,t){var n=new L(e,t);if(M(e,n)){O(e),N(e,n);var r=p(e);Re(e),y(e,r),E(e,r),n.finish()}}function E(e,t){e.display.sizer.style.minHeight=t.docHeight+"px",e.display.heightForcer.style.top=t.docHeight+"px",e.display.gutters.style.height=t.docHeight+e.display.barHeight+Ye(e)+"px"}function O(e){for(var t=e.display,n=t.lineDiv.offsetTop,r=0;rbo){var a=o.node.offsetTop+o.node.offsetHeight;i=a-n,n=a}else{var l=o.node.getBoundingClientRect();i=l.bottom-l.top}var s=o.line.height-i;if(2>i&&(i=yt(t)),(s>.001||-.001>s)&&(ei(o.line,i),I(o.line),o.rest))for(var c=0;c=t&&f.lineNumber;f.changes&&(Pi(f.changes,"gutter")>-1&&(h=!1),D(e,f,c,n)),h&&(Ui(f.lineNumber),f.lineNumber.appendChild(document.createTextNode(S(e.options,c)))),l=f.node.nextSibling}else{var d=U(e,f,c,n);a.insertBefore(d,l)}c+=f.size}for(;l;)l=r(l)}function D(e,t,n,r){for(var i=0;ibo&&(e.node.style.zIndex=2)),e.node}function W(e){var t=e.bgClass?e.bgClass+" "+(e.line.bgClass||""):e.line.bgClass;if(t&&(t+=" CodeMirror-linebackground"),e.background)t?e.background.className=t:(e.background.parentNode.removeChild(e.background),e.background=null);else if(t){var n=H(e);e.background=n.insertBefore(ji("div",null,t),n.firstChild)}}function B(e,t){var n=e.display.externalMeasured;return n&&n.line==t.line?(e.display.externalMeasured=null,t.measure=n.measure,n.built):Br(e,t)}function _(e,t){var n=t.text.className,r=B(e,t);t.text==t.node&&(t.node=r.pre),t.text.parentNode.replaceChild(r.pre,t.text),t.text=r.pre,r.bgClass!=t.bgClass||r.textClass!=t.textClass?(t.bgClass=r.bgClass,t.textClass=r.textClass,F(t)):n&&(t.text.className=n)}function F(e){W(e),e.line.wrapClass?H(e).className=e.line.wrapClass:e.node!=e.text&&(e.node.className="");var t=e.textClass?e.textClass+" "+(e.line.textClass||""):e.line.textClass;e.text.className=t||""}function z(e,t,n,r){if(t.gutter&&(t.node.removeChild(t.gutter),t.gutter=null),t.gutterBackground&&(t.node.removeChild(t.gutterBackground),t.gutterBackground=null),t.line.gutterClass){var i=H(t);t.gutterBackground=ji("div",null,"CodeMirror-gutter-background "+t.line.gutterClass,"left: "+(e.options.fixedGutter?r.fixedPos:-r.gutterTotalWidth)+"px; width: "+r.gutterTotalWidth+"px"),i.insertBefore(t.gutterBackground,t.text)}var o=t.line.gutterMarkers;if(e.options.lineNumbers||o){var i=H(t),a=t.gutter=ji("div",null,"CodeMirror-gutter-wrapper","left: "+(e.options.fixedGutter?r.fixedPos:-r.gutterTotalWidth)+"px");if(e.display.input.setUneditable(a),i.insertBefore(a,t.text),t.line.gutterClass&&(a.className+=" "+t.line.gutterClass),!e.options.lineNumbers||o&&o["CodeMirror-linenumbers"]||(t.lineNumber=a.appendChild(ji("div",S(e.options,n),"CodeMirror-linenumber CodeMirror-gutter-elt","left: "+r.gutterLeft["CodeMirror-linenumbers"]+"px; width: "+e.display.lineNumInnerWidth+"px"))),o)for(var l=0;l1)if(Fo&&Fo.text.join("\n")==t){if(r.ranges.length%Fo.text.length==0){s=[];for(var c=0;c=0;c--){var u=r.ranges[c],f=u.from(),h=u.to();u.empty()&&(n&&n>0?f=Bo(f.line,f.ch-n):e.state.overwrite&&!a?h=Bo(h.line,Math.min(Zr(o,h.line).text.length,h.ch+Ii(l).length)):Fo&&Fo.lineWise&&Fo.text.join("\n")==t&&(f=h=Bo(f.line,0)));var d=e.curOp.updateInput,p={from:f,to:h,text:s?s[c%s.length]:l,origin:i||(a?"paste":e.state.cutIncoming?"cut":"+input")};Tn(e.doc,p),Ci(e,"inputRead",e,p)}t&&!a&&Q(e,t),Bn(e),e.curOp.updateInput=d,e.curOp.typing=!0,e.state.pasteIncoming=e.state.cutIncoming=!1}function J(e,t){var n=e.clipboardData&&e.clipboardData.getData("text/plain");return n?(e.preventDefault(),t.isReadOnly()||t.options.disableInput||At(t,function(){Z(t,n,0,null,"paste")}),!0):void 0}function Q(e,t){if(e.options.electricChars&&e.options.smartIndent)for(var n=e.doc.sel,r=n.ranges.length-1;r>=0;r--){var i=n.ranges[r];if(!(i.head.ch>100||r&&n.ranges[r-1].head.line==i.head.line)){var o=e.getModeAt(i.head),a=!1;if(o.electricChars){for(var l=0;l-1){a=Fn(e,i.head.line,"smart");break}}else o.electricInput&&o.electricInput.test(Zr(e.doc,i.head.line).text.slice(0,i.head.ch))&&(a=Fn(e,i.head.line,"smart"));a&&Ci(e,"electricInput",e,i.head.line)}}}function ee(e){for(var t=[],n=[],r=0;ri?c.map:u[i],a=0;ai?e.line:e.rest[i]),f=o[a]+r;return(0>r||l!=t)&&(f=o[a+(r?1:0)]),Bo(s,f)}}}var i=e.text.firstChild,o=!1;if(!t||!Va(i,t))return ae(Bo(ti(e.line),0),!0);if(t==i&&(o=!0,t=i.childNodes[n],n=0,!t)){var a=e.rest?Ii(e.rest):e.line;return ae(Bo(ti(a),a.text.length),o)}var l=3==t.nodeType?t:null,s=t;for(l||1!=t.childNodes.length||3!=t.firstChild.nodeType||(l=t.firstChild,n&&(n=l.nodeValue.length));s.parentNode!=i;)s=s.parentNode;var c=e.measure,u=c.maps,f=r(l,s,n);if(f)return ae(f,o);for(var h=s.nextSibling,d=l?l.nodeValue.length-n:0;h;h=h.nextSibling){if(f=r(h,h.firstChild,0))return ae(Bo(f.line,f.ch-d),o);d+=h.textContent.length}for(var p=s.previousSibling,d=n;p;p=p.previousSibling){if(f=r(p,p.firstChild,-1))return ae(Bo(f.line,f.ch+d),o);d+=h.textContent.length}}function ce(e,t,n,r,i){function o(e){return function(t){return t.id==e}}function a(t){if(1==t.nodeType){var n=t.getAttribute("cm-text");if(null!=n)return""==n&&(n=t.textContent.replace(/\u200b/g,"")),void(l+=n);var u,f=t.getAttribute("cm-marker");if(f){var h=e.findMarks(Bo(r,0),Bo(i+1,0),o(+f));return void(h.length&&(u=h[0].find())&&(l+=Jr(e.doc,u.from,u.to).join(c)))}if("false"==t.getAttribute("contenteditable"))return;for(var d=0;d=0){var a=K(o.from(),i.from()),l=V(o.to(),i.to()),s=o.empty()?i.from()==i.head:o.from()==o.head;t>=r&&--t,e.splice(--r,2,new fe(s?l:a,s?a:l))}}return new ue(e,t)}function de(e,t){return new ue([new fe(e,t||e)],0)}function pe(e,t){return Math.max(e.first,Math.min(t,e.first+e.size-1))}function me(e,t){if(t.linen?Bo(n,Zr(e,n).text.length):ge(t,Zr(e,t.line).text.length)}function ge(e,t){var n=e.ch;return null==n||n>t?Bo(e.line,t):0>n?Bo(e.line,0):e}function ve(e,t){return t>=e.first&&t=t.ch:l.to>t.ch))){if(i&&(Pa(s,"beforeCursorEnter"),s.explicitlyCleared)){if(o.markedSpans){--a;continue}break}if(!s.atomic)continue;if(n){var c,u=s.find(0>r?1:-1);if((0>r?s.inclusiveRight:s.inclusiveLeft)&&(u=Pe(e,u,-r,u&&u.line==t.line?o:null)),u&&u.line==t.line&&(c=_o(u,n))&&(0>r?0>c:c>0))return Oe(e,u,t,r,i)}var f=s.find(0>r?-1:1);return(0>r?s.inclusiveLeft:s.inclusiveRight)&&(f=Pe(e,f,r,f.line==t.line?o:null)),f?Oe(e,f,t,r,i):null}}return t}function Ie(e,t,n,r,i){var o=r||1,a=Oe(e,t,n,o,i)||!i&&Oe(e,t,n,o,!0)||Oe(e,t,n,-o,i)||!i&&Oe(e,t,n,-o,!0);return a?a:(e.cantEdit=!0,Bo(e.first,0))}function Pe(e,t,n,r){return 0>n&&0==t.ch?t.line>e.first?me(e,Bo(t.line-1)):null:n>0&&t.ch==(r||Zr(e,t.line)).text.length?t.line=e.display.viewTo||l.to().linet&&(t=0),t=Math.round(t),r=Math.round(r),l.appendChild(ji("div",null,"CodeMirror-selected","position: absolute; left: "+e+"px; top: "+t+"px; width: "+(null==n?u-e:n)+"px; height: "+(r-t)+"px"))}function i(t,n,i){function o(n,r){return ht(e,Bo(t,n),"div",f,r)}var l,s,f=Zr(a,t),h=f.text.length;return eo(ii(f),n||0,null==i?h:i,function(e,t,a){var f,d,p,m=o(e,"left");if(e==t)f=m,d=p=m.left;else{if(f=o(t-1,"right"),"rtl"==a){var g=m;m=f,f=g}d=m.left,p=f.right}null==n&&0==e&&(d=c),f.top-m.top>3&&(r(d,m.top,null,m.bottom),d=c,m.bottoms.bottom||f.bottom==s.bottom&&f.right>s.right)&&(s=f),c+1>d&&(d=c),r(d,f.top,p-d,f.bottom)}),{start:l,end:s}}var o=e.display,a=e.doc,l=document.createDocumentFragment(),s=Ge(e.display),c=s.left,u=Math.max(o.sizerWidth,$e(e)-o.sizer.offsetLeft)-s.right,f=t.from(),h=t.to();if(f.line==h.line)i(f.line,f.ch,h.ch);else{var d=Zr(a,f.line),p=Zr(a,h.line),m=yr(d)==yr(p),g=i(f.line,f.ch,m?d.text.length+1:null).end,v=i(h.line,m?0:null,h.ch).start;m&&(g.top0?t.blinker=setInterval(function(){t.cursorDiv.style.visibility=(n=!n)?"":"hidden"},e.options.cursorBlinkRate):e.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function _e(e,t){e.doc.mode.startState&&e.doc.frontier=e.display.viewTo)){var n=+new Date+e.options.workTime,r=sa(t.mode,je(e,t.frontier)),i=[];t.iter(t.frontier,Math.min(t.first+t.size,e.display.viewTo+500),function(o){if(t.frontier>=e.display.viewFrom){var a=o.styles,l=o.text.length>e.options.maxHighlightLength,s=Rr(e,o,l?sa(t.mode,r):r,!0);o.styles=s.styles;var c=o.styleClasses,u=s.classes;u?o.styleClasses=u:c&&(o.styleClasses=null);for(var f=!a||a.length!=o.styles.length||c!=u&&(!c||!u||c.bgClass!=u.bgClass||c.textClass!=u.textClass),h=0;!f&&hn?(_e(e,e.options.workDelay),!0):void 0}),i.length&&At(e,function(){for(var t=0;ta;--l){if(l<=o.first)return o.first;var s=Zr(o,l-1);if(s.stateAfter&&(!n||l<=o.frontier))return l;var c=Fa(s.text,null,e.options.tabSize);(null==i||r>c)&&(i=l-1,r=c)}return i}function je(e,t,n){var r=e.doc,i=e.display;if(!r.mode.startState)return!0;var o=ze(e,t,n),a=o>r.first&&Zr(r,o-1).stateAfter;return a=a?sa(r.mode,a):ca(r.mode),r.iter(o,t,function(n){Hr(e,n.text,a);var l=o==t-1||o%5==0||o>=i.viewFrom&&o2&&o.push((s.bottom+c.top)/2-n.top)}}o.push(n.bottom-n.top)}}function Xe(e,t,n){if(e.line==t)return{map:e.measure.map,cache:e.measure.cache};for(var r=0;rn)return{map:e.measure.maps[r],cache:e.measure.caches[r],before:!0}}function Ze(e,t){t=yr(t);var n=ti(t),r=e.display.externalMeasured=new Pt(e.doc,t,n);r.lineN=n;var i=r.built=Br(e,r);return r.text=i.pre,qi(e.display.lineMeasure,i.pre),r}function Je(e,t,n,r){return tt(e,et(e,t),n,r)}function Qe(e,t){if(t>=e.display.viewFrom&&t=n.lineN&&tt?(i=0,o=1,a="left"):c>t?(i=t-s,o=i+1):(l==e.length-3||t==c&&e[l+3]>t)&&(o=c-s,i=o-1,t>=c&&(a="right")),null!=i){if(r=e[l+2],s==c&&n==(r.insertLeft?"left":"right")&&(a=n),"left"==n&&0==i)for(;l&&e[l-2]==e[l-3]&&e[l-1].insertLeft;)r=e[(l-=3)+2],a="left";if("right"==n&&i==c-s)for(;lu;u++){for(;l&&zi(t.line.text.charAt(o.coverStart+l));)--l;for(;o.coverStart+sbo&&0==l&&s==o.coverEnd-o.coverStart)i=a.parentNode.getBoundingClientRect();else if(xo&&e.options.lineWrapping){var f=qa(a,l,s).getClientRects();i=f.length?f["right"==r?f.length-1:0]:qo}else i=qa(a,l,s).getBoundingClientRect()||qo;if(i.left||i.right||0==l)break;s=l,l-=1,c="right"}xo&&11>bo&&(i=it(e.display.measure,i))}else{l>0&&(c=r="right");var f;i=e.options.lineWrapping&&(f=a.getClientRects()).length>1?f["right"==r?f.length-1:0]:a.getBoundingClientRect()}if(xo&&9>bo&&!l&&(!i||!i.left&&!i.right)){var h=a.parentNode.getClientRects()[0];i=h?{left:h.left,right:h.left+xt(e.display),top:h.top,bottom:h.bottom}:qo}for(var d=i.top-t.rect.top,p=i.bottom-t.rect.top,m=(d+p)/2,g=t.view.measure.heights,u=0;un.from?a(e-1):a(e,r)}r=r||Zr(e.doc,t.line),i||(i=et(e,r));var s=ii(r),c=t.ch;if(!s)return a(c);var u=co(s,c),f=l(c,u);return null!=al&&(f.other=l(c,al)),f}function pt(e,t){var n=0,t=me(e.doc,t);e.options.lineWrapping||(n=xt(e.display)*t.ch);var r=Zr(e.doc,t.line),i=ri(r)+Ue(e.display);return{left:n,right:n,top:i,bottom:i+r.height}}function mt(e,t,n,r){var i=Bo(e,t);return i.xRel=r,n&&(i.outside=!0),i}function gt(e,t,n){var r=e.doc;if(n+=e.display.viewOffset,0>n)return mt(r.first,0,!0,-1);var i=ni(r,n),o=r.first+r.size-1;if(i>o)return mt(r.first+r.size-1,Zr(r,o).text.length,!0,1);0>t&&(t=0);for(var a=Zr(r,i);;){var l=vt(e,a,i,t,n),s=gr(a),c=s&&s.find(0,!0);if(!s||!(l.ch>c.from.ch||l.ch==c.from.ch&&l.xRel>0))return l;i=ti(a=c.to.line)}}function vt(e,t,n,r,i){function o(r){var i=dt(e,Bo(n,r),"line",t,c);return l=!0,a>i.bottom?i.left-s:ag)return mt(n,d,v,1);for(;;){if(u?d==h||d==fo(t,h,1):1>=d-h){for(var y=p>r||g-r>=r-p?h:d,x=r-(y==h?p:g);zi(t.text.charAt(y));)++y;var b=mt(n,y,y==h?m:v,-1>x?-1:x>1?1:0);return b}var w=Math.ceil(f/2),k=h+w;if(u){k=h;for(var S=0;w>S;++S)k=fo(t,k,1)}var C=o(k);C>r?(d=k,g=C,(v=l)&&(g+=1e3),f=w):(h=k,p=C,m=l,f-=w)}}function yt(e){if(null!=e.cachedTextHeight)return e.cachedTextHeight;if(null==zo){zo=ji("pre");for(var t=0;49>t;++t)zo.appendChild(document.createTextNode("x")),zo.appendChild(ji("br"));zo.appendChild(document.createTextNode("x"))}qi(e.measure,zo);var n=zo.offsetHeight/50;return n>3&&(e.cachedTextHeight=n),Ui(e.measure),n||1}function xt(e){if(null!=e.cachedCharWidth)return e.cachedCharWidth;var t=ji("span","xxxxxxxxxx"),n=ji("pre",[t]);qi(e.measure,n);var r=t.getBoundingClientRect(),i=(r.right-r.left)/10;return i>2&&(e.cachedCharWidth=i),i||10}function bt(e){e.curOp={cm:e,viewChanged:!1,startHeight:e.doc.height,forceUpdate:!1,updateInput:null,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Yo},Go?Go.ops.push(e.curOp):e.curOp.ownsGroup=Go={ops:[e.curOp],delayedCallbacks:[]}}function wt(e){var t=e.delayedCallbacks,n=0;do{for(;n=n.viewTo)||n.maxLineChanged&&t.options.lineWrapping,e.update=e.mustUpdate&&new L(t,e.mustUpdate&&{top:e.scrollTop,ensure:e.scrollToPos},e.forceUpdate)}function Lt(e){e.updatedDisplay=e.mustUpdate&&M(e.cm,e.update)}function Tt(e){var t=e.cm,n=t.display;e.updatedDisplay&&O(t),e.barMeasure=p(t),n.maxLineChanged&&!t.options.lineWrapping&&(e.adjustWidthTo=Je(t,n.maxLine,n.maxLine.text.length).left+3,t.display.sizerWidth=e.adjustWidthTo,e.barMeasure.scrollWidth=Math.max(n.scroller.clientWidth,n.sizer.offsetLeft+e.adjustWidthTo+Ye(t)+t.display.barWidth),e.maxScrollLeft=Math.max(0,n.sizer.offsetLeft+e.adjustWidthTo-$e(t))),(e.updatedDisplay||e.selectionChanged)&&(e.preparedSelection=n.input.prepareSelection(e.focus))}function Mt(e){var t=e.cm;null!=e.adjustWidthTo&&(t.display.sizer.style.minWidth=e.adjustWidthTo+"px",e.maxScrollLefto;o=r){var a=new Pt(e.doc,Zr(e.doc,o),o);r=o+a.size,i.push(a)}return i}function Dt(e,t,n,r){null==t&&(t=e.doc.first),null==n&&(n=e.doc.first+e.doc.size),r||(r=0);var i=e.display;if(r&&nt)&&(i.updateLineNumbers=t),e.curOp.viewChanged=!0,t>=i.viewTo)Wo&&br(e.doc,t)i.viewFrom?Wt(e):(i.viewFrom+=r,i.viewTo+=r);else if(t<=i.viewFrom&&n>=i.viewTo)Wt(e);else if(t<=i.viewFrom){var o=_t(e,n,n+r,1);o?(i.view=i.view.slice(o.index),i.viewFrom=o.lineN,i.viewTo+=r):Wt(e)}else if(n>=i.viewTo){var o=_t(e,t,t,-1);o?(i.view=i.view.slice(0,o.index),i.viewTo=o.lineN):Wt(e)}else{var a=_t(e,t,t,-1),l=_t(e,n,n+r,1);a&&l?(i.view=i.view.slice(0,a.index).concat(Rt(e,a.lineN,l.lineN)).concat(i.view.slice(l.index)),i.viewTo+=r):Wt(e)}var s=i.externalMeasured;s&&(n=i.lineN&&t=r.viewTo)){var o=r.view[Bt(e,t)];if(null!=o.node){var a=o.changes||(o.changes=[]);-1==Pi(a,n)&&a.push(n)}}}function Wt(e){e.display.viewFrom=e.display.viewTo=e.doc.first,e.display.view=[],e.display.viewOffset=0}function Bt(e,t){if(t>=e.display.viewTo)return null;if(t-=e.display.viewFrom,0>t)return null;for(var n=e.display.view,r=0;rt)return r}function _t(e,t,n,r){var i,o=Bt(e,t),a=e.display.view;if(!Wo||n==e.doc.first+e.doc.size)return{index:o,lineN:n};for(var l=0,s=e.display.viewFrom;o>l;l++)s+=a[l].size;if(s!=t){if(r>0){if(o==a.length-1)return null;i=s+a[o].size-t,o++}else i=s-t;t+=i,n+=i}for(;br(e.doc,n)!=n;){if(o==(0>r?0:a.length-1))return null;n+=r*a[o-(0>r?1:0)].size,o+=r}return{index:o,lineN:n}}function Ft(e,t,n){var r=e.display,i=r.view;0==i.length||t>=r.viewTo||n<=r.viewFrom?(r.view=Rt(e,t,n),r.viewFrom=t):(r.viewFrom>t?r.view=Rt(e,t,r.viewFrom).concat(r.view):r.viewFromn&&(r.view=r.view.slice(0,Bt(e,n)))),r.viewTo=n}function zt(e){for(var t=e.display.view,n=0,r=0;r400}var i=e.display;Ea(i.scroller,"mousedown",Et(e,$t)),xo&&11>bo?Ea(i.scroller,"dblclick",Et(e,function(t){if(!Ti(e,t)){var n=Yt(e,t);if(n&&!Jt(e,t)&&!Gt(e.display,t)){Ma(t);var r=e.findWordAt(n);be(e.doc,r.anchor,r.head)}}})):Ea(i.scroller,"dblclick",function(t){Ti(e,t)||Ma(t)}),Do||Ea(i.scroller,"contextmenu",function(t){xn(e,t)});var o,a={end:0};Ea(i.scroller,"touchstart",function(t){if(!Ti(e,t)&&!n(t)){clearTimeout(o);var r=+new Date;i.activeTouch={start:r,moved:!1,prev:r-a.end<=300?a:null},1==t.touches.length&&(i.activeTouch.left=t.touches[0].pageX,i.activeTouch.top=t.touches[0].pageY)}}),Ea(i.scroller,"touchmove",function(){i.activeTouch&&(i.activeTouch.moved=!0)}),Ea(i.scroller,"touchend",function(n){var o=i.activeTouch;if(o&&!Gt(i,n)&&null!=o.left&&!o.moved&&new Date-o.start<300){var a,l=e.coordsChar(i.activeTouch,"page");a=!o.prev||r(o,o.prev)?new fe(l,l):!o.prev.prev||r(o,o.prev.prev)?e.findWordAt(l):new fe(Bo(l.line,0),me(e.doc,Bo(l.line+1,0))),e.setSelection(a.anchor,a.head),e.focus(),Ma(n)}t()}),Ea(i.scroller,"touchcancel",t),Ea(i.scroller,"scroll",function(){i.scroller.clientHeight&&(rn(e,i.scroller.scrollTop),on(e,i.scroller.scrollLeft,!0),Pa(e,"scroll",e))}),Ea(i.scroller,"mousewheel",function(t){an(e,t)}),Ea(i.scroller,"DOMMouseScroll",function(t){an(e,t)}),Ea(i.wrapper,"scroll",function(){i.wrapper.scrollTop=i.wrapper.scrollLeft=0}),i.dragFunctions={enter:function(t){Ti(e,t)||Aa(t)},over:function(t){Ti(e,t)||(tn(e,t),Aa(t))},start:function(t){en(e,t)},drop:Et(e,Qt),leave:function(t){Ti(e,t)||nn(e)}};var l=i.input.getField();Ea(l,"keyup",function(t){pn.call(e,t)}),Ea(l,"keydown",Et(e,hn)),Ea(l,"keypress",Et(e,mn)),Ea(l,"focus",Bi(vn,e)),Ea(l,"blur",Bi(yn,e))}function Ut(t,n,r){var i=r&&r!=e.Init;if(!n!=!i){var o=t.display.dragFunctions,a=n?Ea:Ia;a(t.display.scroller,"dragstart",o.start),a(t.display.scroller,"dragenter",o.enter),a(t.display.scroller,"dragover",o.over),a(t.display.scroller,"dragleave",o.leave),a(t.display.scroller,"drop",o.drop)}}function qt(e){var t=e.display;t.lastWrapHeight==t.wrapper.clientHeight&&t.lastWrapWidth==t.wrapper.clientWidth||(t.cachedCharWidth=t.cachedTextHeight=t.cachedPaddingH=null,t.scrollbarsClipped=!1,e.setSize())}function Gt(e,t){for(var n=wi(t);n!=e.wrapper;n=n.parentNode)if(!n||1==n.nodeType&&"true"==n.getAttribute("cm-ignore-events")||n.parentNode==e.sizer&&n!=e.mover)return!0}function Yt(e,t,n,r){var i=e.display;if(!n&&"true"==wi(t).getAttribute("cm-not-content"))return null;var o,a,l=i.lineSpace.getBoundingClientRect();try{o=t.clientX-l.left,a=t.clientY-l.top}catch(t){return null}var s,c=gt(e,o,a);if(r&&1==c.xRel&&(s=Zr(e.doc,c.line).text).length==c.ch){var u=Fa(s,s.length,e.options.tabSize)-s.length;c=Bo(c.line,Math.max(0,Math.round((o-Ge(e.display).left)/xt(e.display))-u))}return c}function $t(e){var t=this,n=t.display;if(!(Ti(t,e)||n.activeTouch&&n.input.supportsTouch())){if(n.shift=e.shiftKey,Gt(n,e))return void(wo||(n.scroller.draggable=!1,setTimeout(function(){n.scroller.draggable=!0},100)));if(!Jt(t,e)){var r=Yt(t,e);switch(window.focus(),ki(e)){case 1:t.state.selectingText?t.state.selectingText(e):r?Vt(t,e,r):wi(e)==n.scroller&&Ma(e);break;case 2:wo&&(t.state.lastMiddleDown=+new Date),r&&be(t.doc,r),setTimeout(function(){n.input.focus()},20),Ma(e);break;case 3:Do?xn(t,e):gn(t)}}}}function Vt(e,t,n){xo?setTimeout(Bi(X,e),0):e.curOp.focus=Gi();var r,i=+new Date;Uo&&Uo.time>i-400&&0==_o(Uo.pos,n)?r="triple":jo&&jo.time>i-400&&0==_o(jo.pos,n)?(r="double",Uo={time:i,pos:n}):(r="single",jo={time:i,pos:n});var o,a=e.doc.sel,l=Eo?t.metaKey:t.ctrlKey;e.options.dragDrop&&el&&!e.isReadOnly()&&"single"==r&&(o=a.contains(n))>-1&&(_o((o=a.ranges[o]).from(),n)<0||n.xRel>0)&&(_o(o.to(),n)>0||n.xRel<0)?Kt(e,t,n,l):Xt(e,t,n,r,l)}function Kt(e,t,n,r){var i=e.display,o=+new Date,a=Et(e,function(l){wo&&(i.scroller.draggable=!1),e.state.draggingText=!1,Ia(document,"mouseup",a),Ia(i.scroller,"drop",a),Math.abs(t.clientX-l.clientX)+Math.abs(t.clientY-l.clientY)<10&&(Ma(l),!r&&+new Date-200=p;p++){var v=Zr(c,p).text,y=za(v,s,o);s==d?i.push(new fe(Bo(p,y),Bo(p,y))):v.length>y&&i.push(new fe(Bo(p,y),Bo(p,za(v,d,o))))}i.length||i.push(new fe(n,n)),Te(c,he(h.ranges.slice(0,f).concat(i),f),{origin:"*mouse",scroll:!1}),e.scrollIntoView(t)}else{var x=u,b=x.anchor,w=t;if("single"!=r){if("double"==r)var k=e.findWordAt(t);else var k=new fe(Bo(t.line,0),me(c,Bo(t.line+1,0)));_o(k.anchor,b)>0?(w=k.head,b=K(x.from(),k.anchor)):(w=k.anchor,b=V(x.to(),k.head))}var i=h.ranges.slice(0);i[f]=new fe(me(c,b),w),Te(c,he(i,f),Ba)}}function a(t){var n=++y,i=Yt(e,t,!0,"rect"==r);if(i)if(0!=_o(i,g)){e.curOp.focus=Gi(),o(i);var l=b(s,c);(i.line>=l.to||i.linev.bottom?20:0;u&&setTimeout(Et(e,function(){y==n&&(s.scroller.scrollTop+=u,a(t))}),50)}}function l(t){e.state.selectingText=!1,y=1/0,Ma(t),s.input.focus(),Ia(document,"mousemove",x),Ia(document,"mouseup",w),c.history.lastSelOrigin=null}var s=e.display,c=e.doc;Ma(t);var u,f,h=c.sel,d=h.ranges;if(i&&!t.shiftKey?(f=c.sel.contains(n),u=f>-1?d[f]:new fe(n,n)):(u=c.sel.primary(),f=c.sel.primIndex),Oo?t.shiftKey&&t.metaKey:t.altKey)r="rect",i||(u=new fe(n,n)),n=Yt(e,t,!0,!0),f=-1;else if("double"==r){var p=e.findWordAt(n);u=e.display.shift||c.extend?xe(c,u,p.anchor,p.head):p}else if("triple"==r){var m=new fe(Bo(n.line,0),me(c,Bo(n.line+1,0)));u=e.display.shift||c.extend?xe(c,u,m.anchor,m.head):m}else u=xe(c,u,n);i?-1==f?(f=d.length,Te(c,he(d.concat([u]),f),{scroll:!1,origin:"*mouse"})):d.length>1&&d[f].empty()&&"single"==r&&!t.shiftKey?(Te(c,he(d.slice(0,f).concat(d.slice(f+1)),0),{scroll:!1,origin:"*mouse"}),h=c.sel):ke(c,f,u,Ba):(f=0,Te(c,new ue([u],0),Ba),h=c.sel);var g=n,v=s.wrapper.getBoundingClientRect(),y=0,x=Et(e,function(e){ki(e)?a(e):l(e)}),w=Et(e,l);e.state.selectingText=w,Ea(document,"mousemove",x),Ea(document,"mouseup",w)}function Zt(e,t,n,r){try{var i=t.clientX,o=t.clientY}catch(t){return!1}if(i>=Math.floor(e.display.gutters.getBoundingClientRect().right))return!1;r&&Ma(t);var a=e.display,l=a.lineDiv.getBoundingClientRect();if(o>l.bottom||!Ni(e,n))return bi(t);o-=l.top-a.viewOffset;for(var s=0;s=i){var u=ni(e.doc,o),f=e.options.gutters[s];return Pa(e,n,e,u,f,t),bi(t)}}}function Jt(e,t){return Zt(e,t,"gutterClick",!0)}function Qt(e){var t=this;if(nn(t),!Ti(t,e)&&!Gt(t.display,e)){Ma(e),xo&&($o=+new Date);var n=Yt(t,e,!0),r=e.dataTransfer.files;if(n&&!t.isReadOnly())if(r&&r.length&&window.FileReader&&window.File)for(var i=r.length,o=Array(i),a=0,l=function(e,r){if(!t.options.allowDropFileTypes||-1!=Pi(t.options.allowDropFileTypes,e.type)){var l=new FileReader;l.onload=Et(t,function(){var e=l.result;if(/[\x00-\x08\x0e-\x1f]{2}/.test(e)&&(e=""),o[r]=e,++a==i){n=me(t.doc,n);var s={from:n,to:n,text:t.doc.splitLines(o.join(t.doc.lineSeparator())),origin:"paste"};Tn(t.doc,s),Le(t.doc,de(n,Qo(s)))}}),l.readAsText(e)}},s=0;i>s;++s)l(r[s],s);else{if(t.state.draggingText&&t.doc.sel.contains(n)>-1)return t.state.draggingText(e),void setTimeout(function(){t.display.input.focus()},20);try{var o=e.dataTransfer.getData("Text");if(o){if(t.state.draggingText&&!(Eo?e.altKey:e.ctrlKey))var c=t.listSelections();if(Me(t.doc,de(n,n)),c)for(var s=0;sa.clientWidth,s=a.scrollHeight>a.clientHeight;if(r&&l||i&&s){if(i&&Eo&&wo)e:for(var c=t.target,u=o.view;c!=a;c=c.parentNode)for(var f=0;fh?d=Math.max(0,d+h-50):p=Math.min(e.doc.height,p+h+50),A(e,{top:d,bottom:p})}20>Vo&&(null==o.wheelStartX?(o.wheelStartX=a.scrollLeft,o.wheelStartY=a.scrollTop,o.wheelDX=r,o.wheelDY=i,setTimeout(function(){if(null!=o.wheelStartX){var e=a.scrollLeft-o.wheelStartX,t=a.scrollTop-o.wheelStartY,n=t&&o.wheelDY&&t/o.wheelDY||e&&o.wheelDX&&e/o.wheelDX;o.wheelStartX=o.wheelStartY=null,n&&(Ko=(Ko*Vo+n)/(Vo+1),++Vo)}},200)):(o.wheelDX+=r,o.wheelDY+=i))}}function ln(e,t,n){if("string"==typeof t&&(t=ua[t],!t))return!1;e.display.input.ensurePolled();var r=e.display.shift,i=!1;try{e.isReadOnly()&&(e.state.suppressEdits=!0),n&&(e.display.shift=!1),i=t(e)!=Ha}finally{e.display.shift=r,e.state.suppressEdits=!1}return i}function sn(e,t,n){for(var r=0;rbo&&27==e.keyCode&&(e.returnValue=!1);var n=e.keyCode;t.display.shift=16==n||e.shiftKey;var r=un(t,e);Co&&(Jo=r?n:null,!r&&88==n&&!rl&&(Eo?e.metaKey:e.ctrlKey)&&t.replaceSelection("",null,"cut")),18!=n||/\bCodeMirror-crosshair\b/.test(t.display.lineDiv.className)||dn(t)}}function dn(e){function t(e){18!=e.keyCode&&e.altKey||(Za(n,"CodeMirror-crosshair"),Ia(document,"keyup",t),Ia(document,"mouseover",t))}var n=e.display.lineDiv;Ja(n,"CodeMirror-crosshair"),Ea(document,"keyup",t),Ea(document,"mouseover",t)}function pn(e){16==e.keyCode&&(this.doc.sel.shift=!1),Ti(this,e)}function mn(e){var t=this;if(!(Gt(t.display,e)||Ti(t,e)||e.ctrlKey&&!e.altKey||Eo&&e.metaKey)){var n=e.keyCode,r=e.charCode;if(Co&&n==Jo)return Jo=null,void Ma(e);if(!Co||e.which&&!(e.which<10)||!un(t,e)){var i=String.fromCharCode(null==r?n:r);fn(t,e,i)||t.display.input.onKeyPress(e)}}}function gn(e){e.state.delayingBlurEvent=!0,setTimeout(function(){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1,yn(e))},100)}function vn(e){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1),"nocursor"!=e.options.readOnly&&(e.state.focused||(Pa(e,"focus",e),e.state.focused=!0,Ja(e.display.wrapper,"CodeMirror-focused"),e.curOp||e.display.selForContextMenu==e.doc.sel||(e.display.input.reset(),wo&&setTimeout(function(){e.display.input.reset(!0)},20)),e.display.input.receivedFocus()),Be(e))}function yn(e){e.state.delayingBlurEvent||(e.state.focused&&(Pa(e,"blur",e),e.state.focused=!1,Za(e.display.wrapper,"CodeMirror-focused")),clearInterval(e.display.blinker),setTimeout(function(){e.state.focused||(e.display.shift=!1)},150))}function xn(e,t){Gt(e.display,t)||bn(e,t)||Ti(e,t,"contextmenu")||e.display.input.onContextMenu(t)}function bn(e,t){return Ni(e,"gutterContextMenu")?Zt(e,t,"gutterContextMenu",!1):!1}function wn(e,t){if(_o(e,t.from)<0)return e;if(_o(e,t.to)<=0)return Qo(t);var n=e.line+t.text.length-(t.to.line-t.from.line)-1,r=e.ch;return e.line==t.to.line&&(r+=Qo(t).ch-t.to.ch),Bo(n,r)}function kn(e,t){for(var n=[],r=0;r=0;--i)Mn(e,{from:r[i].from,to:r[i].to,text:i?[""]:t.text});else Mn(e,t)}}function Mn(e,t){if(1!=t.text.length||""!=t.text[0]||0!=_o(t.from,t.to)){var n=kn(e,t);ci(e,t,n,e.cm?e.cm.curOp.id:NaN),En(e,t,n,or(e,t));var r=[];Kr(e,function(e,n){n||-1!=Pi(r,e.history)||(xi(e.history,t),r.push(e.history)),En(e,t,null,or(e,t))})}}function Nn(e,t,n){if(!e.cm||!e.cm.state.suppressEdits){for(var r,i=e.history,o=e.sel,a="undo"==t?i.done:i.undone,l="undo"==t?i.undone:i.done,s=0;s=0;--s){var f=r.changes[s];if(f.origin=t,u&&!Ln(e,f,!1))return void(a.length=0);c.push(ai(e,f));var h=s?kn(e,f):Ii(a);En(e,f,h,lr(e,f)),!s&&e.cm&&e.cm.scrollIntoView({from:f.from,to:Qo(f)});var d=[];Kr(e,function(e,t){t||-1!=Pi(d,e.history)||(xi(e.history,f),d.push(e.history)),En(e,f,null,lr(e,f))})}}}}function An(e,t){if(0!=t&&(e.first+=t,e.sel=new ue(Ri(e.sel.ranges,function(e){return new fe(Bo(e.anchor.line+t,e.anchor.ch),Bo(e.head.line+t,e.head.ch))}),e.sel.primIndex),e.cm)){Dt(e.cm,e.first,e.first-t,t);for(var n=e.cm.display,r=n.viewFrom;re.lastLine())){if(t.from.lineo&&(t={from:t.from,to:Bo(o,Zr(e,o).text.length),text:[t.text[0]],origin:t.origin}),t.removed=Jr(e,t.from,t.to),n||(n=kn(e,t)),e.cm?On(e.cm,t,r):Yr(e,t,r),Me(e,n,Wa)}}function On(e,t,n){var r=e.doc,i=e.display,a=t.from,l=t.to,s=!1,c=a.line;e.options.lineWrapping||(c=ti(yr(Zr(r,a.line))),r.iter(c,l.line+1,function(e){return e==i.maxLine?(s=!0,!0):void 0})),r.sel.contains(t.from,t.to)>-1&&Mi(e),Yr(r,t,n,o(e)),e.options.lineWrapping||(r.iter(c,a.line+t.text.length,function(e){var t=f(e);t>i.maxLineLength&&(i.maxLine=e,i.maxLineLength=t,i.maxLineChanged=!0,s=!1)}),s&&(e.curOp.updateMaxLine=!0)),r.frontier=Math.min(r.frontier,a.line),_e(e,400);var u=t.text.length-(l.line-a.line)-1;t.full?Dt(e):a.line!=l.line||1!=t.text.length||Gr(e.doc,t)?Dt(e,a.line,l.line+1,u):Ht(e,a.line,"text");var h=Ni(e,"changes"),d=Ni(e,"change");if(d||h){var p={from:a,to:l,text:t.text,removed:t.removed,origin:t.origin};d&&Ci(e,"change",e,p),h&&(e.curOp.changeObjs||(e.curOp.changeObjs=[])).push(p)}e.display.selForContextMenu=null}function In(e,t,n,r,i){if(r||(r=n),_o(r,n)<0){var o=r;r=n,n=o}"string"==typeof t&&(t=e.splitLines(t)),Tn(e,{from:n,to:r,text:t,origin:i})}function Pn(e,t){if(!Ti(e,"scrollCursorIntoView")){var n=e.display,r=n.sizer.getBoundingClientRect(),i=null;if(t.top+r.top<0?i=!0:t.bottom+r.top>(window.innerHeight||document.documentElement.clientHeight)&&(i=!1),null!=i&&!Mo){var o=ji("div","​",null,"position: absolute; top: "+(t.top-n.viewOffset-Ue(e.display))+"px; height: "+(t.bottom-t.top+Ye(e)+n.barHeight)+"px; left: "+t.left+"px; width: 2px;");e.display.lineSpace.appendChild(o),o.scrollIntoView(i),e.display.lineSpace.removeChild(o)}}}function Rn(e,t,n,r){null==r&&(r=0);for(var i=0;5>i;i++){var o=!1,a=dt(e,t),l=n&&n!=t?dt(e,n):a,s=Hn(e,Math.min(a.left,l.left),Math.min(a.top,l.top)-r,Math.max(a.left,l.left),Math.max(a.bottom,l.bottom)+r),c=e.doc.scrollTop,u=e.doc.scrollLeft;if(null!=s.scrollTop&&(rn(e,s.scrollTop),Math.abs(e.doc.scrollTop-c)>1&&(o=!0)),null!=s.scrollLeft&&(on(e,s.scrollLeft),Math.abs(e.doc.scrollLeft-u)>1&&(o=!0)),!o)break}return a}function Dn(e,t,n,r,i){var o=Hn(e,t,n,r,i);null!=o.scrollTop&&rn(e,o.scrollTop),null!=o.scrollLeft&&on(e,o.scrollLeft)}function Hn(e,t,n,r,i){var o=e.display,a=yt(e.display);0>n&&(n=0);var l=e.curOp&&null!=e.curOp.scrollTop?e.curOp.scrollTop:o.scroller.scrollTop,s=Ve(e),c={};i-n>s&&(i=n+s);var u=e.doc.height+qe(o),f=a>n,h=i>u-a;if(l>n)c.scrollTop=f?0:n;else if(i>l+s){var d=Math.min(n,(h?u:i)-s);d!=l&&(c.scrollTop=d)}var p=e.curOp&&null!=e.curOp.scrollLeft?e.curOp.scrollLeft:o.scroller.scrollLeft,m=$e(e)-(e.options.fixedGutter?o.gutters.offsetWidth:0),g=r-t>m;return g&&(r=t+m),10>t?c.scrollLeft=0:p>t?c.scrollLeft=Math.max(0,t-(g?0:10)):r>m+p-3&&(c.scrollLeft=r+(g?0:10)-m),c}function Wn(e,t,n){null==t&&null==n||_n(e),null!=t&&(e.curOp.scrollLeft=(null==e.curOp.scrollLeft?e.doc.scrollLeft:e.curOp.scrollLeft)+t),null!=n&&(e.curOp.scrollTop=(null==e.curOp.scrollTop?e.doc.scrollTop:e.curOp.scrollTop)+n)}function Bn(e){_n(e);var t=e.getCursor(),n=t,r=t;e.options.lineWrapping||(n=t.ch?Bo(t.line,t.ch-1):t,r=Bo(t.line,t.ch+1)),e.curOp.scrollToPos={from:n,to:r,margin:e.options.cursorScrollMargin,isCursor:!0}}function _n(e){var t=e.curOp.scrollToPos;if(t){e.curOp.scrollToPos=null;var n=pt(e,t.from),r=pt(e,t.to),i=Hn(e,Math.min(n.left,r.left),Math.min(n.top,r.top)-t.margin,Math.max(n.right,r.right),Math.max(n.bottom,r.bottom)+t.margin);e.scrollTo(i.scrollLeft,i.scrollTop)}}function Fn(e,t,n,r){var i,o=e.doc;null==n&&(n="add"),"smart"==n&&(o.mode.indent?i=je(e,t):n="prev");var a=e.options.tabSize,l=Zr(o,t),s=Fa(l.text,null,a);l.stateAfter&&(l.stateAfter=null);var c,u=l.text.match(/^\s*/)[0];if(r||/\S/.test(l.text)){if("smart"==n&&(c=o.mode.indent(i,l.text.slice(u.length),l.text),c==Ha||c>150)){if(!r)return;n="prev"}}else c=0,n="not";"prev"==n?c=t>o.first?Fa(Zr(o,t-1).text,null,a):0:"add"==n?c=s+e.options.indentUnit:"subtract"==n?c=s-e.options.indentUnit:"number"==typeof n&&(c=s+n),c=Math.max(0,c);var f="",h=0;if(e.options.indentWithTabs)for(var d=Math.floor(c/a);d;--d)h+=a,f+=" ";if(c>h&&(f+=Oi(c-h)),f!=u)return In(o,f,Bo(t,0),Bo(t,u.length),"+input"),l.stateAfter=null,!0;for(var d=0;d=0;t--)In(e.doc,"",r[t].from,r[t].to,"+delete");Bn(e)})}function Un(e,t,n,r,i){function o(){var t=l+n;return t=e.first+e.size?!1:(l=t,u=Zr(e,t))}function a(e){var t=(i?fo:ho)(u,s,n,!0);if(null==t){if(e||!o())return!1;s=i?(0>n?io:ro)(u):0>n?u.text.length:0}else s=t;return!0}var l=t.line,s=t.ch,c=n,u=Zr(e,l);if("char"==r)a();else if("column"==r)a(!0);else if("word"==r||"group"==r)for(var f=null,h="group"==r,d=e.cm&&e.cm.getHelper(t,"wordChars"),p=!0;!(0>n)||a(!p);p=!1){var m=u.text.charAt(s)||"\n",g=_i(m,d)?"w":h&&"\n"==m?"n":!h||/\s/.test(m)?null:"p";if(!h||p||g||(g="s"),f&&f!=g){0>n&&(n=1,a());break}if(g&&(f=g),n>0&&!a(!p))break}var v=Ie(e,Bo(l,s),t,c,!0);return _o(t,v)||(v.hitSide=!0),v}function qn(e,t,n,r){var i,o=e.doc,a=t.left;if("page"==r){var l=Math.min(e.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight);i=t.top+n*(l-(0>n?1.5:.5)*yt(e.display))}else"line"==r&&(i=n>0?t.bottom+3:t.top-3);for(;;){var s=gt(e,a,i);if(!s.outside)break;if(0>n?0>=i:i>=o.height){s.hitSide=!0;break}i+=5*n}return s}function Gn(t,n,r,i){e.defaults[t]=n,r&&(ta[t]=i?function(e,t,n){n!=na&&r(e,t,n)}:r)}function Yn(e){for(var t,n,r,i,o=e.split(/-(?!$)/),e=o[o.length-1],a=0;a0||0==a&&o.clearWhenEmpty!==!1)return o;if(o.replacedWith&&(o.collapsed=!0,o.widgetNode=ji("span",[o.replacedWith],"CodeMirror-widget"),r.handleMouseEvents||o.widgetNode.setAttribute("cm-ignore-events","true"),r.insertLeft&&(o.widgetNode.insertLeft=!0)),o.collapsed){if(vr(e,t.line,t,n,o)||t.line!=n.line&&vr(e,n.line,t,n,o))throw new Error("Inserting collapsed marker partially overlapping an existing one");Wo=!0}o.addToHistory&&ci(e,{from:t,to:n,origin:"markText"},e.sel,NaN);var l,s=t.line,c=e.cm;if(e.iter(s,n.line+1,function(e){c&&o.collapsed&&!c.options.lineWrapping&&yr(e)==c.display.maxLine&&(l=!0),o.collapsed&&s!=t.line&&ei(e,0),nr(e,new Qn(o,s==t.line?t.ch:null,s==n.line?n.ch:null)),++s}),o.collapsed&&e.iter(t.line,n.line+1,function(t){kr(e,t)&&ei(t,0)}),o.clearOnEnter&&Ea(o,"beforeCursorEnter",function(){o.clear()}),o.readOnly&&(Ho=!0,(e.history.done.length||e.history.undone.length)&&e.clearHistory()),o.collapsed&&(o.id=++ga,o.atomic=!0),c){if(l&&(c.curOp.updateMaxLine=!0),o.collapsed)Dt(c,t.line,n.line+1);else if(o.className||o.title||o.startStyle||o.endStyle||o.css)for(var u=t.line;u<=n.line;u++)Ht(c,u,"text");o.atomic&&Ae(c.doc),Ci(c,"markerAdded",c,o)}return o}function Kn(e,t,n,r,i){r=Wi(r),r.shared=!1;var o=[Vn(e,t,n,r,i)],a=o[0],l=r.widgetNode;return Kr(e,function(e){l&&(r.widgetNode=l.cloneNode(!0)),o.push(Vn(e,me(e,t),me(e,n),r,i));for(var s=0;s=t:o.to>t);(r||(r=[])).push(new Qn(a,o.from,s?null:o.to))}}return r}function ir(e,t,n){if(e)for(var r,i=0;i=t:o.to>t);if(l||o.from==t&&"bookmark"==a.type&&(!n||o.marker.insertLeft)){var s=null==o.from||(a.inclusiveLeft?o.from<=t:o.from0&&l)for(var f=0;ff;++f)p.push(m);p.push(s)}return p}function ar(e){for(var t=0;t0)){var u=[s,1],f=_o(c.from,l.from),h=_o(c.to,l.to);(0>f||!a.inclusiveLeft&&!f)&&u.push({from:c.from,to:l.from}),(h>0||!a.inclusiveRight&&!h)&&u.push({from:l.to,to:c.to}),i.splice.apply(i,u),s+=u.length-1}}return i}function cr(e){var t=e.markedSpans;if(t){for(var n=0;n=0&&0>=f||0>=u&&f>=0)&&(0>=u&&(s.marker.inclusiveRight&&i.inclusiveLeft?_o(c.to,n)>=0:_o(c.to,n)>0)||u>=0&&(s.marker.inclusiveRight&&i.inclusiveLeft?_o(c.from,r)<=0:_o(c.from,r)<0)))return!0}}}function yr(e){for(var t;t=mr(e);)e=t.find(-1,!0).line;return e}function xr(e){for(var t,n;t=gr(e);)e=t.find(1,!0).line,(n||(n=[])).push(e);return n}function br(e,t){var n=Zr(e,t),r=yr(n);return n==r?t:ti(r)}function wr(e,t){if(t>e.lastLine())return t;var n,r=Zr(e,t);if(!kr(e,r))return t;for(;n=gr(r);)r=n.find(1,!0).line;return ti(r)+1}function kr(e,t){var n=Wo&&t.markedSpans;if(n)for(var r,i=0;io;o++){i&&(i[0]=e.innerMode(t,r).mode);var a=t.token(n,r);if(n.pos>n.start)return a}throw new Error("Mode "+t.name+" failed to advance stream.")}function Ir(e,t,n,r){function i(e){return{start:f.start,end:f.pos,string:f.current(),type:o||null,state:e?sa(a.mode,u):u}}var o,a=e.doc,l=a.mode;t=me(a,t);var s,c=Zr(a,t.line),u=je(e,t.line,n),f=new ma(c.text,e.options.tabSize);for(r&&(s=[]);(r||f.pose.options.maxHighlightLength?(l=!1,a&&Hr(e,t,r,f.pos),f.pos=t.length,s=null):s=Ar(Or(n,f,r,h),o),h){var d=h[0].name;d&&(s="m-"+(s?d+" "+s:d))}if(!l||u!=s){for(;cc;){var r=i[s];r>e&&i.splice(s,1,e,i[s+1],r),s+=2,c=Math.min(e,r)}if(t)if(l.opaque)i.splice(n,s-n,e,"cm-overlay "+t),s=n+2;else for(;s>n;n+=2){var o=i[n+1];i[n+1]=(o?o+" ":"")+"cm-overlay "+t}},o)}return{styles:i,classes:o.bgClass||o.textClass?o:null}}function Dr(e,t,n){if(!t.styles||t.styles[0]!=e.state.modeGen){var r=je(e,ti(t)),i=Rr(e,t,t.text.length>e.options.maxHighlightLength?sa(e.doc.mode,r):r);t.stateAfter=r,t.styles=i.styles,i.classes?t.styleClasses=i.classes:t.styleClasses&&(t.styleClasses=null),n===e.doc.frontier&&e.doc.frontier++}return t.styles}function Hr(e,t,n,r){var i=e.doc.mode,o=new ma(t,e.options.tabSize);for(o.start=o.pos=r||0,""==t&&Er(i,n);!o.eol();)Or(i,o,n),o.start=o.pos}function Wr(e,t){if(!e||/^\s*$/.test(e))return null;var n=t.addModeClass?ka:wa;return n[e]||(n[e]=e.replace(/\S+/g,"cm-$&"))}function Br(e,t){var n=ji("span",null,null,wo?"padding-right: .1px":null),r={pre:ji("pre",[n],"CodeMirror-line"),content:n,col:0,pos:0,cm:e,splitSpaces:(xo||wo)&&e.getOption("lineWrapping")};t.measure={};for(var i=0;i<=(t.rest?t.rest.length:0);i++){var o,a=i?t.rest[i-1]:t.line;r.pos=0,r.addToken=Fr,Ji(e.display.measure)&&(o=ii(a))&&(r.addToken=jr(r.addToken,o)),r.map=[];var l=t!=e.display.externalMeasured&&ti(a);qr(a,r,Dr(e,a,l)),a.styleClasses&&(a.styleClasses.bgClass&&(r.bgClass=$i(a.styleClasses.bgClass,r.bgClass||"")),a.styleClasses.textClass&&(r.textClass=$i(a.styleClasses.textClass,r.textClass||""))),0==r.map.length&&r.map.push(0,0,r.content.appendChild(Zi(e.display.measure))),0==i?(t.measure.map=r.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(r.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(wo){var s=r.content.lastChild;(/\bcm-tab\b/.test(s.className)||s.querySelector&&s.querySelector(".cm-tab"))&&(r.content.className="cm-tab-wrap-hack")}return Pa(e,"renderLine",e,t.line,r.pre),r.pre.className&&(r.textClass=$i(r.pre.className,r.textClass||"")),r}function _r(e){var t=ji("span","•","cm-invalidchar");return t.title="\\u"+e.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function Fr(e,t,n,r,i,o,a){if(t){var l=e.splitSpaces?t.replace(/ {3,}/g,zr):t,s=e.cm.state.specialChars,c=!1;if(s.test(t))for(var u=document.createDocumentFragment(),f=0;;){s.lastIndex=f;var h=s.exec(t),d=h?h.index-f:t.length-f;if(d){var p=document.createTextNode(l.slice(f,f+d));xo&&9>bo?u.appendChild(ji("span",[p])):u.appendChild(p),e.map.push(e.pos,e.pos+d,p),e.col+=d,e.pos+=d}if(!h)break;if(f+=d+1," "==h[0]){var m=e.cm.options.tabSize,g=m-e.col%m,p=u.appendChild(ji("span",Oi(g),"cm-tab"));p.setAttribute("role","presentation"),p.setAttribute("cm-text"," "),e.col+=g}else if("\r"==h[0]||"\n"==h[0]){var p=u.appendChild(ji("span","\r"==h[0]?"␍":"␤","cm-invalidchar"));p.setAttribute("cm-text",h[0]),e.col+=1}else{var p=e.cm.options.specialCharPlaceholder(h[0]);p.setAttribute("cm-text",h[0]),xo&&9>bo?u.appendChild(ji("span",[p])):u.appendChild(p),e.col+=1}e.map.push(e.pos,e.pos+1,p),e.pos++}else{e.col+=t.length;var u=document.createTextNode(l);e.map.push(e.pos,e.pos+t.length,u),xo&&9>bo&&(c=!0),e.pos+=t.length}if(n||r||i||c||a){var v=n||"";r&&(v+=r),i&&(v+=i);var y=ji("span",[u],v,a);return o&&(y.title=o),e.content.appendChild(y)}e.content.appendChild(u)}}function zr(e){for(var t=" ",n=0;nc&&h.from<=c)break}if(h.to>=u)return e(n,r,i,o,a,l,s);e(n,r.slice(0,h.to-c),i,o,null,l,s),o=null,r=r.slice(h.to-c),c=h.to}}}function Ur(e,t,n,r){var i=!r&&n.widgetNode;i&&e.map.push(e.pos,e.pos+t,i),!r&&e.cm.display.input.needsContentAttribute&&(i||(i=e.content.appendChild(document.createElement("span"))),i.setAttribute("cm-marker",n.id)),i&&(e.cm.display.input.setUneditable(i),e.content.appendChild(i)),e.pos+=t}function qr(e,t,n){var r=e.markedSpans,i=e.text,o=0;if(r)for(var a,l,s,c,u,f,h,d=i.length,p=0,m=1,g="",v=0;;){if(v==p){s=c=u=f=l="",h=null,v=1/0;for(var y,x=[],b=0;bp||k.collapsed&&w.to==p&&w.from==p)?(null!=w.to&&w.to!=p&&v>w.to&&(v=w.to,c=""),k.className&&(s+=" "+k.className),k.css&&(l=(l?l+";":"")+k.css),k.startStyle&&w.from==p&&(u+=" "+k.startStyle),k.endStyle&&w.to==v&&(y||(y=[])).push(k.endStyle,w.to),k.title&&!f&&(f=k.title),k.collapsed&&(!h||dr(h.marker,k)<0)&&(h=w)):w.from>p&&v>w.from&&(v=w.from)}if(y)for(var b=0;b=d)break;for(var S=Math.min(d,v);;){if(g){var C=p+g.length;if(!h){var L=C>S?g.slice(0,S-p):g;t.addToken(t,L,a?a+s:s,u,p+L.length==v?c:"",f,l)}if(C>=S){g=g.slice(S-p),p=S;break}p=C,u=""}g=i.slice(o,o=n[m++]),a=Wr(n[m++],t.cm.options)}}else for(var m=1;mn;++n)o.push(new ba(c[n],i(n),r));return o}var l=t.from,s=t.to,c=t.text,u=Zr(e,l.line),f=Zr(e,s.line),h=Ii(c),d=i(c.length-1),p=s.line-l.line;if(t.full)e.insert(0,a(0,c.length)),e.remove(c.length,e.size-c.length);else if(Gr(e,t)){var m=a(0,c.length-1);o(f,f.text,d),p&&e.remove(l.line,p),m.length&&e.insert(l.line,m)}else if(u==f)if(1==c.length)o(u,u.text.slice(0,l.ch)+h+u.text.slice(s.ch),d);else{var m=a(1,c.length-1);m.push(new ba(h+u.text.slice(s.ch),d,r)),o(u,u.text.slice(0,l.ch)+c[0],i(0)),e.insert(l.line+1,m)}else if(1==c.length)o(u,u.text.slice(0,l.ch)+c[0]+f.text.slice(s.ch),i(0)),e.remove(l.line+1,p);else{o(u,u.text.slice(0,l.ch)+c[0],i(0)),o(f,h+f.text.slice(s.ch),d);var m=a(1,c.length-1);p>1&&e.remove(l.line+1,p-1),e.insert(l.line+1,m)}Ci(e,"change",e,t)}function $r(e){this.lines=e,this.parent=null;for(var t=0,n=0;tt||t>=e.size)throw new Error("There is no line "+(t+e.first)+" in the document.");for(var n=e;!n.lines;)for(var r=0;;++r){var i=n.children[r],o=i.chunkSize();if(o>t){n=i;break}t-=o}return n.lines[t]}function Jr(e,t,n){var r=[],i=t.line;return e.iter(t.line,n.line+1,function(e){var o=e.text;i==n.line&&(o=o.slice(0,n.ch)),i==t.line&&(o=o.slice(t.ch)),r.push(o),++i}),r}function Qr(e,t,n){var r=[];return e.iter(t,n,function(e){r.push(e.text)}),r}function ei(e,t){var n=t-e.height;if(n)for(var r=e;r;r=r.parent)r.height+=n}function ti(e){if(null==e.parent)return null;for(var t=e.parent,n=Pi(t.lines,e),r=t.parent;r;t=r,r=r.parent)for(var i=0;r.children[i]!=t;++i)n+=r.children[i].chunkSize();return n+t.first}function ni(e,t){var n=e.first;e:do{for(var r=0;rt){e=i;continue e}t-=o,n+=i.chunkSize()}return n}while(!e.lines);for(var r=0;rt)break;t-=l}return n+r}function ri(e){e=yr(e);for(var t=0,n=e.parent,r=0;r1&&!e.done[e.done.length-2].ranges?(e.done.pop(),Ii(e.done)):void 0}function ci(e,t,n,r){var i=e.history;i.undone.length=0;var o,a=+new Date;if((i.lastOp==r||i.lastOrigin==t.origin&&t.origin&&("+"==t.origin.charAt(0)&&e.cm&&i.lastModTime>a-e.cm.options.historyEventDelay||"*"==t.origin.charAt(0)))&&(o=si(i,i.lastOp==r))){var l=Ii(o.changes);0==_o(t.from,t.to)&&0==_o(t.from,l.to)?l.to=Qo(t):o.changes.push(ai(e,t))}else{var s=Ii(i.done);for(s&&s.ranges||hi(e.sel,i.done),o={changes:[ai(e,t)],generation:i.generation},i.done.push(o);i.done.length>i.undoDepth;)i.done.shift(),i.done[0].ranges||i.done.shift()}i.done.push(n),i.generation=++i.maxGeneration,i.lastModTime=i.lastSelTime=a,i.lastOp=i.lastSelOp=r,i.lastOrigin=i.lastSelOrigin=t.origin,l||Pa(e,"historyAdded")}function ui(e,t,n,r){var i=t.charAt(0);return"*"==i||"+"==i&&n.ranges.length==r.ranges.length&&n.somethingSelected()==r.somethingSelected()&&new Date-e.history.lastSelTime<=(e.cm?e.cm.options.historyEventDelay:500)}function fi(e,t,n,r){var i=e.history,o=r&&r.origin;n==i.lastSelOp||o&&i.lastSelOrigin==o&&(i.lastModTime==i.lastSelTime&&i.lastOrigin==o||ui(e,o,Ii(i.done),t))?i.done[i.done.length-1]=t:hi(t,i.done),i.lastSelTime=+new Date,i.lastSelOrigin=o,i.lastSelOp=n,r&&r.clearRedo!==!1&&li(i.undone)}function hi(e,t){var n=Ii(t);n&&n.ranges&&n.equals(e)||t.push(e)}function di(e,t,n,r){var i=t["spans_"+e.id],o=0;e.iter(Math.max(e.first,n),Math.min(e.first+e.size,r),function(n){n.markedSpans&&((i||(i=t["spans_"+e.id]={}))[o]=n.markedSpans),++o})}function pi(e){if(!e)return null;for(var t,n=0;n-1&&(Ii(l)[f]=u[f],delete u[f])}}}return i}function vi(e,t,n,r){n0?r.slice():Oa:r||Oa}function Ci(e,t){function n(e){return function(){e.apply(null,o)}}var r=Si(e,t,!1);if(r.length){var i,o=Array.prototype.slice.call(arguments,2);Go?i=Go.delayedCallbacks:Ra?i=Ra:(i=Ra=[],setTimeout(Li,0));for(var a=0;a0}function Ai(e){e.prototype.on=function(e,t){Ea(this,e,t)},e.prototype.off=function(e,t){Ia(this,e,t)}}function Ei(){this.id=null}function Oi(e){for(;ja.length<=e;)ja.push(Ii(ja)+" ");return ja[e]}function Ii(e){return e[e.length-1]}function Pi(e,t){for(var n=0;n-1&&Ya(e)?!0:t.test(e):Ya(e)}function Fi(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t])return!1;return!0}function zi(e){return e.charCodeAt(0)>=768&&$a.test(e)}function ji(e,t,n,r){var i=document.createElement(e);if(n&&(i.className=n),r&&(i.style.cssText=r),"string"==typeof t)i.appendChild(document.createTextNode(t));else if(t)for(var o=0;o0;--t)e.removeChild(e.firstChild);return e}function qi(e,t){return Ui(e).appendChild(t)}function Gi(){for(var e=document.activeElement;e&&e.root&&e.root.activeElement;)e=e.root.activeElement;return e}function Yi(e){return new RegExp("(^|\\s)"+e+"(?:$|\\s)\\s*")}function $i(e,t){for(var n=e.split(" "),r=0;r2&&!(xo&&8>bo))}var n=Ka?ji("span","​"):ji("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");return n.setAttribute("cm-text",""),n}function Ji(e){if(null!=Xa)return Xa;var t=qi(e,document.createTextNode("AخA")),n=qa(t,0,1).getBoundingClientRect();if(!n||n.left==n.right)return!1;var r=qa(t,1,2).getBoundingClientRect();return Xa=r.right-n.right<3}function Qi(e){if(null!=il)return il;var t=qi(e,ji("span","x")),n=t.getBoundingClientRect(),r=qa(t,0,1).getBoundingClientRect();return il=Math.abs(n.left-r.left)>1}function eo(e,t,n,r){if(!e)return r(t,n,"ltr");for(var i=!1,o=0;ot||t==n&&a.to==t)&&(r(Math.max(a.from,t),Math.min(a.to,n),1==a.level?"rtl":"ltr"),i=!0)}i||r(t,n,"ltr")}function to(e){return e.level%2?e.to:e.from}function no(e){return e.level%2?e.from:e.to}function ro(e){var t=ii(e);return t?to(t[0]):0}function io(e){var t=ii(e);return t?no(Ii(t)):e.text.length}function oo(e,t){var n=Zr(e.doc,t),r=yr(n);r!=n&&(t=ti(r));var i=ii(r),o=i?i[0].level%2?io(r):ro(r):0;return Bo(t,o)}function ao(e,t){for(var n,r=Zr(e.doc,t);n=gr(r);)r=n.find(1,!0).line,t=null;var i=ii(r),o=i?i[0].level%2?ro(r):io(r):r.text.length;return Bo(null==t?ti(r):t,o)}function lo(e,t){var n=oo(e,t.line),r=Zr(e.doc,n.line),i=ii(r);if(!i||0==i[0].level){var o=Math.max(0,r.text.search(/\S/)),a=t.line==n.line&&t.ch<=o&&t.ch;return Bo(n.line,a?0:o)}return n}function so(e,t,n){var r=e[0].level;return t==r?!0:n==r?!1:n>t}function co(e,t){al=null;for(var n,r=0;rt)return r;if(i.from==t||i.to==t){if(null!=n)return so(e,i.level,e[n].level)?(i.from!=i.to&&(al=n),r):(i.from!=i.to&&(al=r),n);n=r}}return n}function uo(e,t,n,r){if(!r)return t+n;do t+=n;while(t>0&&zi(e.text.charAt(t)));return t}function fo(e,t,n,r){var i=ii(e);if(!i)return ho(e,t,n,r);for(var o=co(i,t),a=i[o],l=uo(e,t,a.level%2?-n:n,r);;){if(l>a.from&&l0==a.level%2?a.to:a.from);if(a=i[o+=n],!a)return null;l=n>0==a.level%2?uo(e,a.to,-1,r):uo(e,a.from,1,r)}}function ho(e,t,n,r){var i=t+n;if(r)for(;i>0&&zi(e.text.charAt(i));)i+=n;return 0>i||i>e.text.length?null:i}var po=navigator.userAgent,mo=navigator.platform,go=/gecko\/\d/i.test(po),vo=/MSIE \d/.test(po),yo=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(po),xo=vo||yo,bo=xo&&(vo?document.documentMode||6:yo[1]),wo=/WebKit\//.test(po),ko=wo&&/Qt\/\d+\.\d+/.test(po),So=/Chrome\//.test(po),Co=/Opera\//.test(po),Lo=/Apple Computer/.test(navigator.vendor),To=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(po),Mo=/PhantomJS/.test(po),No=/AppleWebKit/.test(po)&&/Mobile\/\w+/.test(po),Ao=No||/Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(po),Eo=No||/Mac/.test(mo),Oo=/\bCrOS\b/.test(po),Io=/win/i.test(mo),Po=Co&&po.match(/Version\/(\d*\.\d*)/);Po&&(Po=Number(Po[1])),Po&&Po>=15&&(Co=!1,wo=!0);var Ro=Eo&&(ko||Co&&(null==Po||12.11>Po)),Do=go||xo&&bo>=9,Ho=!1,Wo=!1;m.prototype=Wi({update:function(e){var t=e.scrollWidth>e.clientWidth+1,n=e.scrollHeight>e.clientHeight+1,r=e.nativeBarWidth;if(n){this.vert.style.display="block",this.vert.style.bottom=t?r+"px":"0";var i=e.viewHeight-(t?r:0);this.vert.firstChild.style.height=Math.max(0,e.scrollHeight-e.clientHeight+i)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=n?r+"px":"0",this.horiz.style.left=e.barLeft+"px";var o=e.viewWidth-e.barLeft-(n?r:0);this.horiz.firstChild.style.width=e.scrollWidth-e.clientWidth+o+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&e.clientHeight>0&&(0==r&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:n?r:0,bottom:t?r:0}},setScrollLeft:function(e){this.horiz.scrollLeft!=e&&(this.horiz.scrollLeft=e),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz)},setScrollTop:function(e){this.vert.scrollTop!=e&&(this.vert.scrollTop=e),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert)},zeroWidthHack:function(){var e=Eo&&!To?"12px":"18px";this.horiz.style.height=this.vert.style.width=e,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new Ei,this.disableVert=new Ei},enableZeroWidthBar:function(e,t){function n(){var r=e.getBoundingClientRect(),i=document.elementFromPoint(r.left+1,r.bottom-1);i!=e?e.style.pointerEvents="none":t.set(1e3,n)}e.style.pointerEvents="auto",t.set(1e3,n)},clear:function(){var e=this.horiz.parentNode;e.removeChild(this.horiz),e.removeChild(this.vert)}},m.prototype),g.prototype=Wi({update:function(){return{bottom:0,right:0}},setScrollLeft:function(){},setScrollTop:function(){},clear:function(){}},g.prototype),e.scrollbarModel={"native":m,"null":g},L.prototype.signal=function(e,t){Ni(e,t)&&this.events.push(arguments)},L.prototype.finish=function(){for(var e=0;e=9&&n.hasSelection&&(n.hasSelection=null),n.poll()}),Ea(o,"paste",function(e){Ti(r,e)||J(e,r)||(r.state.pasteIncoming=!0,n.fastPoll())}),Ea(o,"cut",t),Ea(o,"copy",t),Ea(e.scroller,"paste",function(t){Gt(e,t)||Ti(r,t)||(r.state.pasteIncoming=!0,n.focus())}),Ea(e.lineSpace,"selectstart",function(t){Gt(e,t)||Ma(t)}),Ea(o,"compositionstart",function(){var e=r.getCursor("from");n.composing&&n.composing.range.clear(),n.composing={start:e,range:r.markText(e,r.getCursor("to"),{className:"CodeMirror-composing"})}}),Ea(o,"compositionend",function(){n.composing&&(n.poll(),n.composing.range.clear(),n.composing=null)})},prepareSelection:function(){var e=this.cm,t=e.display,n=e.doc,r=De(e);if(e.options.moveInputWithCursor){var i=dt(e,n.sel.primary().head,"div"),o=t.wrapper.getBoundingClientRect(),a=t.lineDiv.getBoundingClientRect();r.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,i.top+a.top-o.top)),r.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,i.left+a.left-o.left))}return r},showSelection:function(e){var t=this.cm,n=t.display;qi(n.cursorDiv,e.cursors),qi(n.selectionDiv,e.selection),null!=e.teTop&&(this.wrapper.style.top=e.teTop+"px",this.wrapper.style.left=e.teLeft+"px")},reset:function(e){if(!this.contextMenuPending){var t,n,r=this.cm,i=r.doc;if(r.somethingSelected()){this.prevInput="";var o=i.sel.primary();t=rl&&(o.to().line-o.from().line>100||(n=r.getSelection()).length>1e3);var a=t?"-":n||r.getSelection();this.textarea.value=a,r.state.focused&&Ua(this.textarea),xo&&bo>=9&&(this.hasSelection=a)}else e||(this.prevInput=this.textarea.value="",xo&&bo>=9&&(this.hasSelection=null));this.inaccurateSelection=t}},getField:function(){return this.textarea},supportsTouch:function(){return!1},focus:function(){if("nocursor"!=this.cm.options.readOnly&&(!Ao||Gi()!=this.textarea))try{this.textarea.focus()}catch(e){}},blur:function(){this.textarea.blur()},resetPosition:function(){this.wrapper.style.top=this.wrapper.style.left=0; +},receivedFocus:function(){this.slowPoll()},slowPoll:function(){var e=this;e.pollingFast||e.polling.set(this.cm.options.pollInterval,function(){e.poll(),e.cm.state.focused&&e.slowPoll()})},fastPoll:function(){function e(){var r=n.poll();r||t?(n.pollingFast=!1,n.slowPoll()):(t=!0,n.polling.set(60,e))}var t=!1,n=this;n.pollingFast=!0,n.polling.set(20,e)},poll:function(){var e=this.cm,t=this.textarea,n=this.prevInput;if(this.contextMenuPending||!e.state.focused||nl(t)&&!n&&!this.composing||e.isReadOnly()||e.options.disableInput||e.state.keySeq)return!1;var r=t.value;if(r==n&&!e.somethingSelected())return!1;if(xo&&bo>=9&&this.hasSelection===r||Eo&&/[\uf700-\uf7ff]/.test(r))return e.display.input.reset(),!1;if(e.doc.sel==e.display.selForContextMenu){var i=r.charCodeAt(0);if(8203!=i||n||(n="​"),8666==i)return this.reset(),this.cm.execCommand("undo")}for(var o=0,a=Math.min(n.length,r.length);a>o&&n.charCodeAt(o)==r.charCodeAt(o);)++o;var l=this;return At(e,function(){Z(e,r.slice(o),n.length-o,null,l.composing?"*compose":null),r.length>1e3||r.indexOf("\n")>-1?t.value=l.prevInput="":l.prevInput=r,l.composing&&(l.composing.range.clear(),l.composing.range=e.markText(l.composing.start,e.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},ensurePolled:function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},onKeyPress:function(){xo&&bo>=9&&(this.hasSelection=null),this.fastPoll()},onContextMenu:function(e){function t(){if(null!=a.selectionStart){var e=i.somethingSelected(),t="​"+(e?a.value:"");a.value="⇚",a.value=t,r.prevInput=e?"":"​",a.selectionStart=1,a.selectionEnd=t.length,o.selForContextMenu=i.doc.sel}}function n(){if(r.contextMenuPending=!1,r.wrapper.style.cssText=f,a.style.cssText=u,xo&&9>bo&&o.scrollbars.setScrollTop(o.scroller.scrollTop=s),null!=a.selectionStart){(!xo||xo&&9>bo)&&t();var e=0,n=function(){o.selForContextMenu==i.doc.sel&&0==a.selectionStart&&a.selectionEnd>0&&"​"==r.prevInput?Et(i,ua.selectAll)(i):e++<10?o.detectingSelectAll=setTimeout(n,500):o.input.reset()};o.detectingSelectAll=setTimeout(n,200)}}var r=this,i=r.cm,o=i.display,a=r.textarea,l=Yt(i,e),s=o.scroller.scrollTop;if(l&&!Co){var c=i.options.resetSelectionOnContextMenu;c&&-1==i.doc.sel.contains(l)&&Et(i,Te)(i.doc,de(l),Wa);var u=a.style.cssText,f=r.wrapper.style.cssText;r.wrapper.style.cssText="position: absolute";var h=r.wrapper.getBoundingClientRect();if(a.style.cssText="position: absolute; width: 30px; height: 30px; top: "+(e.clientY-h.top-5)+"px; left: "+(e.clientX-h.left-5)+"px; z-index: 1000; background: "+(xo?"rgba(255, 255, 255, .05)":"transparent")+"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",wo)var d=window.scrollY;if(o.input.focus(),wo&&window.scrollTo(null,d),o.input.reset(),i.somethingSelected()||(a.value=r.prevInput=" "),r.contextMenuPending=!0,o.selForContextMenu=i.doc.sel,clearTimeout(o.detectingSelectAll),xo&&bo>=9&&t(),Do){Aa(e);var p=function(){Ia(window,"mouseup",p),setTimeout(n,20)};Ea(window,"mouseup",p)}else setTimeout(n,50)}},readOnlyChanged:function(e){e||this.reset()},setUneditable:Di,needsContentAttribute:!1},ne.prototype),ie.prototype=Wi({init:function(e){function t(e){if(!Ti(r,e)){if(r.somethingSelected())Fo={lineWise:!1,text:r.getSelections()},"cut"==e.type&&r.replaceSelection("",null,"cut");else{if(!r.options.lineWiseCopyCut)return;var t=ee(r);Fo={lineWise:!0,text:t.text},"cut"==e.type&&r.operation(function(){r.setSelections(t.ranges,0,Wa),r.replaceSelection("",null,"cut")})}if(e.clipboardData&&!No)e.preventDefault(),e.clipboardData.clearData(),e.clipboardData.setData("text/plain",Fo.text.join("\n"));else{var n=re(),i=n.firstChild;r.display.lineSpace.insertBefore(n,r.display.lineSpace.firstChild),i.value=Fo.text.join("\n");var o=document.activeElement;Ua(i),setTimeout(function(){r.display.lineSpace.removeChild(n),o.focus()},50)}}}var n=this,r=n.cm,i=n.div=e.lineDiv;te(i),Ea(i,"paste",function(e){Ti(r,e)||J(e,r)}),Ea(i,"compositionstart",function(e){var t=e.data;if(n.composing={sel:r.doc.sel,data:t,startData:t},t){var i=r.doc.sel.primary(),o=r.getLine(i.head.line),a=o.indexOf(t,Math.max(0,i.head.ch-t.length));a>-1&&a<=i.head.ch&&(n.composing.sel=de(Bo(i.head.line,a),Bo(i.head.line,a+t.length)))}}),Ea(i,"compositionupdate",function(e){n.composing.data=e.data}),Ea(i,"compositionend",function(e){var t=n.composing;t&&(e.data==t.startData||/\u200b/.test(e.data)||(t.data=e.data),setTimeout(function(){t.handled||n.applyComposition(t),n.composing==t&&(n.composing=null)},50))}),Ea(i,"touchstart",function(){n.forceCompositionEnd()}),Ea(i,"input",function(){n.composing||!r.isReadOnly()&&n.pollContent()||At(n.cm,function(){Dt(r)})}),Ea(i,"copy",t),Ea(i,"cut",t)},prepareSelection:function(){var e=De(this.cm,!1);return e.focus=this.cm.state.focused,e},showSelection:function(e,t){e&&this.cm.display.view.length&&((e.focus||t)&&this.showPrimarySelection(),this.showMultipleSelections(e))},showPrimarySelection:function(){var e=window.getSelection(),t=this.cm.doc.sel.primary(),n=le(this.cm,e.anchorNode,e.anchorOffset),r=le(this.cm,e.focusNode,e.focusOffset);if(!n||n.bad||!r||r.bad||0!=_o(K(n,r),t.from())||0!=_o(V(n,r),t.to())){var i=oe(this.cm,t.from()),o=oe(this.cm,t.to());if(i||o){var a=this.cm.display.view,l=e.rangeCount&&e.getRangeAt(0);if(i){if(!o){var s=a[a.length-1].measure,c=s.maps?s.maps[s.maps.length-1]:s.map;o={node:c[c.length-1],offset:c[c.length-2]-c[c.length-3]}}}else i={node:a[0].measure.map[2],offset:0};try{var u=qa(i.node,i.offset,o.offset,o.node)}catch(f){}u&&(!go&&this.cm.state.focused?(e.collapse(i.node,i.offset),u.collapsed||e.addRange(u)):(e.removeAllRanges(),e.addRange(u)),l&&null==e.anchorNode?e.addRange(l):go&&this.startGracePeriod()),this.rememberSelection()}}},startGracePeriod:function(){var e=this;clearTimeout(this.gracePeriod),this.gracePeriod=setTimeout(function(){e.gracePeriod=!1,e.selectionChanged()&&e.cm.operation(function(){e.cm.curOp.selectionChanged=!0})},20)},showMultipleSelections:function(e){qi(this.cm.display.cursorDiv,e.cursors),qi(this.cm.display.selectionDiv,e.selection)},rememberSelection:function(){var e=window.getSelection();this.lastAnchorNode=e.anchorNode,this.lastAnchorOffset=e.anchorOffset,this.lastFocusNode=e.focusNode,this.lastFocusOffset=e.focusOffset},selectionInEditor:function(){var e=window.getSelection();if(!e.rangeCount)return!1;var t=e.getRangeAt(0).commonAncestorContainer;return Va(this.div,t)},focus:function(){"nocursor"!=this.cm.options.readOnly&&this.div.focus()},blur:function(){this.div.blur()},getField:function(){return this.div},supportsTouch:function(){return!0},receivedFocus:function(){function e(){t.cm.state.focused&&(t.pollSelection(),t.polling.set(t.cm.options.pollInterval,e))}var t=this;this.selectionInEditor()?this.pollSelection():At(this.cm,function(){t.cm.curOp.selectionChanged=!0}),this.polling.set(this.cm.options.pollInterval,e)},selectionChanged:function(){var e=window.getSelection();return e.anchorNode!=this.lastAnchorNode||e.anchorOffset!=this.lastAnchorOffset||e.focusNode!=this.lastFocusNode||e.focusOffset!=this.lastFocusOffset},pollSelection:function(){if(!this.composing&&!this.gracePeriod&&this.selectionChanged()){var e=window.getSelection(),t=this.cm;this.rememberSelection();var n=le(t,e.anchorNode,e.anchorOffset),r=le(t,e.focusNode,e.focusOffset);n&&r&&At(t,function(){Te(t.doc,de(n,r),Wa),(n.bad||r.bad)&&(t.curOp.selectionChanged=!0)})}},pollContent:function(){var e=this.cm,t=e.display,n=e.doc.sel.primary(),r=n.from(),i=n.to();if(r.linet.viewTo-1)return!1;var o;if(r.line==t.viewFrom||0==(o=Bt(e,r.line)))var a=ti(t.view[0].line),l=t.view[0].node;else var a=ti(t.view[o].line),l=t.view[o-1].node.nextSibling;var s=Bt(e,i.line);if(s==t.view.length-1)var c=t.viewTo-1,u=t.lineDiv.lastChild;else var c=ti(t.view[s+1].line)-1,u=t.view[s+1].node.previousSibling;for(var f=e.doc.splitLines(ce(e,l,u,a,c)),h=Jr(e.doc,Bo(a,0),Bo(c,Zr(e.doc,c).text.length));f.length>1&&h.length>1;)if(Ii(f)==Ii(h))f.pop(),h.pop(),c--;else{if(f[0]!=h[0])break;f.shift(),h.shift(),a++}for(var d=0,p=0,m=f[0],g=h[0],v=Math.min(m.length,g.length);v>d&&m.charCodeAt(d)==g.charCodeAt(d);)++d;for(var y=Ii(f),x=Ii(h),b=Math.min(y.length-(1==f.length?d:0),x.length-(1==h.length?d:0));b>p&&y.charCodeAt(y.length-p-1)==x.charCodeAt(x.length-p-1);)++p;f[f.length-1]=y.slice(0,y.length-p),f[0]=f[0].slice(d);var w=Bo(a,d),k=Bo(c,h.length?Ii(h).length-p:0);return f.length>1||f[0]||_o(w,k)?(In(e.doc,f,w,k,"+input"),!0):void 0},ensurePolled:function(){this.forceCompositionEnd()},reset:function(){this.forceCompositionEnd()},forceCompositionEnd:function(){this.composing&&!this.composing.handled&&(this.applyComposition(this.composing),this.composing.handled=!0,this.div.blur(),this.div.focus())},applyComposition:function(e){this.cm.isReadOnly()?Et(this.cm,Dt)(this.cm):e.data&&e.data!=e.startData&&Et(this.cm,Z)(this.cm,e.data,0,e.sel)},setUneditable:function(e){e.contentEditable="false"},onKeyPress:function(e){e.preventDefault(),this.cm.isReadOnly()||Et(this.cm,Z)(this.cm,String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),0)},readOnlyChanged:function(e){this.div.contentEditable=String("nocursor"!=e)},onContextMenu:Di,resetPosition:Di,needsContentAttribute:!0},ie.prototype),e.inputStyles={textarea:ne,contenteditable:ie},ue.prototype={primary:function(){return this.ranges[this.primIndex]},equals:function(e){if(e==this)return!0;if(e.primIndex!=this.primIndex||e.ranges.length!=this.ranges.length)return!1;for(var t=0;t=0&&_o(e,r.to())<=0)return n}return-1}},fe.prototype={from:function(){return K(this.anchor,this.head)},to:function(){return V(this.anchor,this.head)},empty:function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch}};var zo,jo,Uo,qo={left:0,right:0,top:0,bottom:0},Go=null,Yo=0,$o=0,Vo=0,Ko=null;xo?Ko=-.53:go?Ko=15:So?Ko=-.7:Lo&&(Ko=-1/3);var Xo=function(e){var t=e.wheelDeltaX,n=e.wheelDeltaY;return null==t&&e.detail&&e.axis==e.HORIZONTAL_AXIS&&(t=e.detail),null==n&&e.detail&&e.axis==e.VERTICAL_AXIS?n=e.detail:null==n&&(n=e.wheelDelta),{x:t,y:n}};e.wheelEventPixels=function(e){var t=Xo(e);return t.x*=Ko,t.y*=Ko,t};var Zo=new Ei,Jo=null,Qo=e.changeEnd=function(e){return e.text?Bo(e.from.line+e.text.length-1,Ii(e.text).length+(1==e.text.length?e.from.ch:0)):e.to};e.prototype={constructor:e,focus:function(){window.focus(),this.display.input.focus()},setOption:function(e,t){var n=this.options,r=n[e];n[e]==t&&"mode"!=e||(n[e]=t,ta.hasOwnProperty(e)&&Et(this,ta[e])(this,t,r))},getOption:function(e){return this.options[e]},getDoc:function(){return this.doc},addKeyMap:function(e,t){this.state.keyMaps[t?"push":"unshift"]($n(e))},removeKeyMap:function(e){for(var t=this.state.keyMaps,n=0;nn&&(Fn(this,i.head.line,e,!0),n=i.head.line,r==this.doc.sel.primIndex&&Bn(this));else{var o=i.from(),a=i.to(),l=Math.max(n,o.line);n=Math.min(this.lastLine(),a.line-(a.ch?0:1))+1;for(var s=l;n>s;++s)Fn(this,s,e);var c=this.doc.sel.ranges;0==o.ch&&t.length==c.length&&c[r].from().ch>0&&ke(this.doc,r,new fe(o,c[r].to()),Wa)}}}),getTokenAt:function(e,t){return Ir(this,e,t)},getLineTokens:function(e,t){return Ir(this,Bo(e),t,!0)},getTokenTypeAt:function(e){e=me(this.doc,e);var t,n=Dr(this,Zr(this.doc,e.line)),r=0,i=(n.length-1)/2,o=e.ch;if(0==o)t=n[2];else for(;;){var a=r+i>>1;if((a?n[2*a-1]:0)>=o)i=a;else{if(!(n[2*a+1]l?t:0==l?null:t.slice(0,l-1)},getModeAt:function(t){var n=this.doc.mode;return n.innerMode?e.innerMode(n,this.getTokenAt(t).state).mode:n},getHelper:function(e,t){return this.getHelpers(e,t)[0]},getHelpers:function(e,t){var n=[];if(!la.hasOwnProperty(t))return n;var r=la[t],i=this.getModeAt(e);if("string"==typeof i[t])r[i[t]]&&n.push(r[i[t]]);else if(i[t])for(var o=0;oi&&(e=i,r=!0),n=Zr(this.doc,e)}else n=e;return ut(this,n,{top:0,left:0},t||"page").top+(r?this.doc.height-ri(n):0)},defaultTextHeight:function(){return yt(this.display)},defaultCharWidth:function(){return xt(this.display)},setGutterMarker:Ot(function(e,t,n){return zn(this.doc,e,"gutter",function(e){var r=e.gutterMarkers||(e.gutterMarkers={});return r[t]=n,!n&&Fi(r)&&(e.gutterMarkers=null),!0})}),clearGutter:Ot(function(e){var t=this,n=t.doc,r=n.first;n.iter(function(n){n.gutterMarkers&&n.gutterMarkers[e]&&(n.gutterMarkers[e]=null,Ht(t,r,"gutter"),Fi(n.gutterMarkers)&&(n.gutterMarkers=null)),++r})}),lineInfo:function(e){if("number"==typeof e){if(!ve(this.doc,e))return null;var t=e;if(e=Zr(this.doc,e),!e)return null}else{var t=ti(e);if(null==t)return null}return{line:t,handle:e,text:e.text,gutterMarkers:e.gutterMarkers,textClass:e.textClass,bgClass:e.bgClass,wrapClass:e.wrapClass,widgets:e.widgets}},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(e,t,n,r,i){var o=this.display;e=dt(this,me(this.doc,e));var a=e.bottom,l=e.left;if(t.style.position="absolute",t.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(t),o.sizer.appendChild(t),"over"==r)a=e.top;else if("above"==r||"near"==r){var s=Math.max(o.wrapper.clientHeight,this.doc.height),c=Math.max(o.sizer.clientWidth,o.lineSpace.clientWidth);("above"==r||e.bottom+t.offsetHeight>s)&&e.top>t.offsetHeight?a=e.top-t.offsetHeight:e.bottom+t.offsetHeight<=s&&(a=e.bottom),l+t.offsetWidth>c&&(l=c-t.offsetWidth)}t.style.top=a+"px",t.style.left=t.style.right="","right"==i?(l=o.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==i?l=0:"middle"==i&&(l=(o.sizer.clientWidth-t.offsetWidth)/2),t.style.left=l+"px"),n&&Dn(this,l,a,l+t.offsetWidth,a+t.offsetHeight)},triggerOnKeyDown:Ot(hn),triggerOnKeyPress:Ot(mn),triggerOnKeyUp:pn,execCommand:function(e){return ua.hasOwnProperty(e)?ua[e].call(null,this):void 0},triggerElectric:Ot(function(e){Q(this,e)}),findPosH:function(e,t,n,r){var i=1;0>t&&(i=-1,t=-t);for(var o=0,a=me(this.doc,e);t>o&&(a=Un(this.doc,a,i,n,r),!a.hitSide);++o);return a},moveH:Ot(function(e,t){var n=this;n.extendSelectionsBy(function(r){return n.display.shift||n.doc.extend||r.empty()?Un(n.doc,r.head,e,t,n.options.rtlMoveVisually):0>e?r.from():r.to()},_a)}),deleteH:Ot(function(e,t){var n=this.doc.sel,r=this.doc;n.somethingSelected()?r.replaceSelection("",null,"+delete"):jn(this,function(n){var i=Un(r,n.head,e,t,!1);return 0>e?{from:i,to:n.head}:{from:n.head,to:i}})}),findPosV:function(e,t,n,r){var i=1,o=r;0>t&&(i=-1,t=-t);for(var a=0,l=me(this.doc,e);t>a;++a){var s=dt(this,l,"div");if(null==o?o=s.left:s.left=o,l=qn(this,s,i,n),l.hitSide)break}return l},moveV:Ot(function(e,t){var n=this,r=this.doc,i=[],o=!n.display.shift&&!r.extend&&r.sel.somethingSelected();if(r.extendSelectionsBy(function(a){if(o)return 0>e?a.from():a.to();var l=dt(n,a.head,"div");null!=a.goalColumn&&(l.left=a.goalColumn),i.push(l.left);var s=qn(n,l,e,t);return"page"==t&&a==r.sel.primary()&&Wn(n,null,ht(n,s,"div").top-l.top),s},_a),i.length)for(var a=0;a0&&l(n.charAt(r-1));)--r;for(;i.5)&&a(this),Pa(this,"refresh",this)}),swapDoc:Ot(function(e){var t=this.doc;return t.cm=null,Xr(this,e),lt(this),this.display.input.reset(),this.scrollTo(e.scrollLeft,e.scrollTop),this.curOp.forceScroll=!0,Ci(this,"swapDoc",this,t),t}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Ai(e);var ea=e.defaults={},ta=e.optionHandlers={},na=e.Init={toString:function(){return"CodeMirror.Init"}};Gn("value","",function(e,t){e.setValue(t)},!0),Gn("mode",null,function(e,t){e.doc.modeOption=t,n(e)},!0),Gn("indentUnit",2,n,!0),Gn("indentWithTabs",!1),Gn("smartIndent",!0),Gn("tabSize",4,function(e){r(e),lt(e),Dt(e)},!0),Gn("lineSeparator",null,function(e,t){if(e.doc.lineSep=t,t){var n=[],r=e.doc.first;e.doc.iter(function(e){for(var i=0;;){var o=e.text.indexOf(t,i);if(-1==o)break;i=o+t.length,n.push(Bo(r,o))}r++});for(var i=n.length-1;i>=0;i--)In(e.doc,t,n[i],Bo(n[i].line,n[i].ch+t.length))}}),Gn("specialChars",/[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g,function(t,n,r){t.state.specialChars=new RegExp(n.source+(n.test(" ")?"":"| "),"g"),r!=e.Init&&t.refresh()}),Gn("specialCharPlaceholder",_r,function(e){e.refresh()},!0),Gn("electricChars",!0),Gn("inputStyle",Ao?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),Gn("rtlMoveVisually",!Io),Gn("wholeLineUpdateBefore",!0),Gn("theme","default",function(e){l(e),s(e)},!0),Gn("keyMap","default",function(t,n,r){var i=$n(n),o=r!=e.Init&&$n(r);o&&o.detach&&o.detach(t,i),i.attach&&i.attach(t,o||null)}),Gn("extraKeys",null),Gn("lineWrapping",!1,i,!0),Gn("gutters",[],function(e){d(e.options),s(e)},!0),Gn("fixedGutter",!0,function(e,t){e.display.gutters.style.left=t?C(e.display)+"px":"0",e.refresh()},!0),Gn("coverGutterNextToScrollbar",!1,function(e){y(e)},!0),Gn("scrollbarStyle","native",function(e){v(e),y(e),e.display.scrollbars.setScrollTop(e.doc.scrollTop),e.display.scrollbars.setScrollLeft(e.doc.scrollLeft)},!0),Gn("lineNumbers",!1,function(e){d(e.options),s(e)},!0),Gn("firstLineNumber",1,s,!0),Gn("lineNumberFormatter",function(e){return e},s,!0),Gn("showCursorWhenSelecting",!1,Re,!0),Gn("resetSelectionOnContextMenu",!0),Gn("lineWiseCopyCut",!0),Gn("readOnly",!1,function(e,t){"nocursor"==t?(yn(e),e.display.input.blur(),e.display.disabled=!0):e.display.disabled=!1,e.display.input.readOnlyChanged(t)}),Gn("disableInput",!1,function(e,t){t||e.display.input.reset()},!0),Gn("dragDrop",!0,Ut),Gn("allowDropFileTypes",null),Gn("cursorBlinkRate",530),Gn("cursorScrollMargin",0),Gn("cursorHeight",1,Re,!0),Gn("singleCursorHeightPerLine",!0,Re,!0),Gn("workTime",100),Gn("workDelay",100),Gn("flattenSpans",!0,r,!0),Gn("addModeClass",!1,r,!0),Gn("pollInterval",100),Gn("undoDepth",200,function(e,t){e.doc.history.undoDepth=t}),Gn("historyEventDelay",1250),Gn("viewportMargin",10,function(e){e.refresh()},!0),Gn("maxHighlightLength",1e4,r,!0),Gn("moveInputWithCursor",!0,function(e,t){t||e.display.input.resetPosition()}),Gn("tabindex",null,function(e,t){e.display.input.getField().tabIndex=t||""}),Gn("autofocus",null);var ra=e.modes={},ia=e.mimeModes={};e.defineMode=function(t,n){e.defaults.mode||"null"==t||(e.defaults.mode=t),arguments.length>2&&(n.dependencies=Array.prototype.slice.call(arguments,2)),ra[t]=n},e.defineMIME=function(e,t){ia[e]=t},e.resolveMode=function(t){if("string"==typeof t&&ia.hasOwnProperty(t))t=ia[t];else if(t&&"string"==typeof t.name&&ia.hasOwnProperty(t.name)){var n=ia[t.name];"string"==typeof n&&(n={name:n}),t=Hi(n,t),t.name=n.name}else if("string"==typeof t&&/^[\w\-]+\/[\w\-]+\+xml$/.test(t))return e.resolveMode("application/xml");return"string"==typeof t?{name:t}:t||{name:"null"}},e.getMode=function(t,n){var n=e.resolveMode(n),r=ra[n.name];if(!r)return e.getMode(t,"text/plain");var i=r(t,n);if(oa.hasOwnProperty(n.name)){var o=oa[n.name];for(var a in o)o.hasOwnProperty(a)&&(i.hasOwnProperty(a)&&(i["_"+a]=i[a]),i[a]=o[a])}if(i.name=n.name,n.helperType&&(i.helperType=n.helperType),n.modeProps)for(var a in n.modeProps)i[a]=n.modeProps[a];return i},e.defineMode("null",function(){return{token:function(e){e.skipToEnd()}}}),e.defineMIME("text/plain","null");var oa=e.modeExtensions={};e.extendMode=function(e,t){var n=oa.hasOwnProperty(e)?oa[e]:oa[e]={};Wi(t,n)},e.defineExtension=function(t,n){e.prototype[t]=n},e.defineDocExtension=function(e,t){Ca.prototype[e]=t},e.defineOption=Gn;var aa=[];e.defineInitHook=function(e){aa.push(e)};var la=e.helpers={};e.registerHelper=function(t,n,r){la.hasOwnProperty(t)||(la[t]=e[t]={_global:[]}),la[t][n]=r},e.registerGlobalHelper=function(t,n,r,i){e.registerHelper(t,n,i),la[t]._global.push({pred:r,val:i})};var sa=e.copyState=function(e,t){if(t===!0)return t;if(e.copyState)return e.copyState(t);var n={};for(var r in t){var i=t[r];i instanceof Array&&(i=i.concat([])),n[r]=i}return n},ca=e.startState=function(e,t,n){return e.startState?e.startState(t,n):!0};e.innerMode=function(e,t){for(;e.innerMode;){var n=e.innerMode(t);if(!n||n.mode==e)break;t=n.state,e=n.mode}return n||{mode:e,state:t}};var ua=e.commands={selectAll:function(e){e.setSelection(Bo(e.firstLine(),0),Bo(e.lastLine()),Wa)},singleSelection:function(e){e.setSelection(e.getCursor("anchor"),e.getCursor("head"),Wa)},killLine:function(e){jn(e,function(t){if(t.empty()){var n=Zr(e.doc,t.head.line).text.length;return t.head.ch==n&&t.head.line0)i=new Bo(i.line,i.ch+1),e.replaceRange(o.charAt(i.ch-1)+o.charAt(i.ch-2),Bo(i.line,i.ch-2),i,"+transpose");else if(i.line>e.doc.first){var a=Zr(e.doc,i.line-1).text;a&&e.replaceRange(o.charAt(0)+e.doc.lineSeparator()+a.charAt(a.length-1),Bo(i.line-1,a.length-1),Bo(i.line,1),"+transpose")}n.push(new fe(i,i))}e.setSelections(n)})},newlineAndIndent:function(e){At(e,function(){for(var t=e.listSelections().length,n=0;t>n;n++){var r=e.listSelections()[n];e.replaceRange(e.doc.lineSeparator(),r.anchor,r.head,"+input"),e.indentLine(r.from().line+1,null,!0)}Bn(e)})},openLine:function(e){e.replaceSelection("\n","start")},toggleOverwrite:function(e){e.toggleOverwrite()}},fa=e.keyMap={};fa.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},fa.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},fa.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Alt-F":"goWordRight","Alt-B":"goWordLeft","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-D":"delWordAfter","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars","Ctrl-O":"openLine"},fa.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]},fa["default"]=Eo?fa.macDefault:fa.pcDefault,e.normalizeKeyMap=function(e){var t={};for(var n in e)if(e.hasOwnProperty(n)){var r=e[n];if(/^(name|fallthrough|(de|at)tach)$/.test(n))continue;if("..."==r){delete e[n];continue}for(var i=Ri(n.split(" "),Yn),o=0;o=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.post},eatSpace:function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},skipToEnd:function(){this.pos=this.string.length},skipTo:function(e){var t=this.string.indexOf(e,this.pos);return t>-1?(this.pos=t,!0):void 0},backUp:function(e){this.pos-=e},column:function(){return this.lastColumnPos0?null:(r&&t!==!1&&(this.pos+=r[0].length),r)}var i=function(e){return n?e.toLowerCase():e},o=this.string.substr(this.pos,e.length);return i(o)==i(e)?(t!==!1&&(this.pos+=e.length),!0):void 0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}}};var ga=0,va=e.TextMarker=function(e,t){this.lines=[],this.type=t,this.doc=e,this.id=++ga};Ai(va),va.prototype.clear=function(){if(!this.explicitlyCleared){var e=this.doc.cm,t=e&&!e.curOp;if(t&&bt(e),Ni(this,"clear")){var n=this.find();n&&Ci(this,"clear",n.from,n.to)}for(var r=null,i=null,o=0;oe.display.maxLineLength&&(e.display.maxLine=s,e.display.maxLineLength=c,e.display.maxLineChanged=!0)}null!=r&&e&&this.collapsed&&Dt(e,r,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,e&&Ae(e.doc)),e&&Ci(e,"markerCleared",e,this),t&&kt(e),this.parent&&this.parent.clear()}},va.prototype.find=function(e,t){null==e&&"bookmark"==this.type&&(e=1);for(var n,r,i=0;in;++n){var i=this.lines[n];this.height-=i.height,Nr(i),Ci(i,"delete")}this.lines.splice(e,t)},collapse:function(e){e.push.apply(e,this.lines)},insertInner:function(e,t,n){this.height+=n,this.lines=this.lines.slice(0,e).concat(t).concat(this.lines.slice(e));for(var r=0;re;++e)if(n(this.lines[e]))return!0}},Vr.prototype={chunkSize:function(){return this.size},removeInner:function(e,t){this.size-=t;for(var n=0;ne){var o=Math.min(t,i-e),a=r.height;if(r.removeInner(e,o),this.height-=a-r.height,i==o&&(this.children.splice(n--,1),r.parent=null),0==(t-=o))break;e=0}else e-=i}if(this.size-t<25&&(this.children.length>1||!(this.children[0]instanceof $r))){var l=[];this.collapse(l),this.children=[new $r(l)],this.children[0].parent=this}},collapse:function(e){for(var t=0;t=e){if(i.insertInner(e,t,n),i.lines&&i.lines.length>50){for(var a=i.lines.length%25+25,l=a;l10);e.parent.maybeSpill()}},iterN:function(e,t,n){for(var r=0;re){var a=Math.min(t,o-e);if(i.iterN(e,a,n))return!0;if(0==(t-=a))break;e=0}else e-=o}}};var Sa=0,Ca=e.Doc=function(e,t,n,r){if(!(this instanceof Ca))return new Ca(e,t,n,r);null==n&&(n=0),Vr.call(this,[new $r([new ba("",null)])]),this.first=n,this.scrollTop=this.scrollLeft=0,this.cantEdit=!1,this.cleanGeneration=1,this.frontier=n;var i=Bo(n,0);this.sel=de(i),this.history=new oi(null),this.id=++Sa,this.modeOption=t,this.lineSep=r,this.extend=!1,"string"==typeof e&&(e=this.splitLines(e)),Yr(this,{from:i,to:i,text:e}),Te(this,de(i),Wa)};Ca.prototype=Hi(Vr.prototype,{constructor:Ca,iter:function(e,t,n){n?this.iterN(e-this.first,t-e,n):this.iterN(this.first,this.first+this.size,e)},insert:function(e,t){for(var n=0,r=0;r=0;o--)Tn(this,r[o]);l?Le(this,l):this.cm&&Bn(this.cm)}),undo:It(function(){Nn(this,"undo")}),redo:It(function(){Nn(this,"redo")}),undoSelection:It(function(){Nn(this,"undo",!0)}),redoSelection:It(function(){Nn(this,"redo",!0)}),setExtending:function(e){this.extend=e},getExtending:function(){return this.extend},historySize:function(){for(var e=this.history,t=0,n=0,r=0;r=e.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(e,t,n){e=me(this,e),t=me(this,t);var r=[],i=e.line;return this.iter(e.line,t.line+1,function(o){var a=o.markedSpans;if(a)for(var l=0;l=s.to||null==s.from&&i!=e.line||null!=s.from&&i==t.line&&s.from>=t.ch||n&&!n(s.marker)||r.push(s.marker.parent||s.marker)}++i}),r},getAllMarks:function(){var e=[];return this.iter(function(t){var n=t.markedSpans;if(n)for(var r=0;re?(t=e,!0):(e-=o,void++n)}),me(this,Bo(n,t))},indexFromPos:function(e){e=me(this,e);var t=e.ch;if(e.linet&&(t=e.from),null!=e.to&&e.tol||l>=t)return a+(t-o);a+=l-o,a+=n-a%n,o=l+1}},za=e.findColumn=function(e,t,n){for(var r=0,i=0;;){var o=e.indexOf(" ",r);-1==o&&(o=e.length);var a=o-r;if(o==e.length||i+a>=t)return r+Math.min(a,t-i);if(i+=o-r,i+=n-i%n,r=o+1,i>=t)return r}},ja=[""],Ua=function(e){e.select()};No?Ua=function(e){e.selectionStart=0,e.selectionEnd=e.value.length}:xo&&(Ua=function(e){try{e.select()}catch(t){}});var qa,Ga=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,Ya=e.isWordChar=function(e){return/\w/.test(e)||e>"€"&&(e.toUpperCase()!=e.toLowerCase()||Ga.test(e))},$a=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;qa=document.createRange?function(e,t,n,r){var i=document.createRange();return i.setEnd(r||e,n),i.setStart(e,t),i}:function(e,t,n){var r=document.body.createTextRange();try{r.moveToElementText(e.parentNode)}catch(i){return r}return r.collapse(!0),r.moveEnd("character",n),r.moveStart("character",t),r};var Va=e.contains=function(e,t){if(3==t.nodeType&&(t=t.parentNode),e.contains)return e.contains(t);do if(11==t.nodeType&&(t=t.host),t==e)return!0;while(t=t.parentNode)};xo&&11>bo&&(Gi=function(){try{return document.activeElement}catch(e){return document.body}});var Ka,Xa,Za=e.rmClass=function(e,t){var n=e.className,r=Yi(t).exec(n);if(r){var i=n.slice(r.index+r[0].length);e.className=n.slice(0,r.index)+(i?r[1]+i:"")}},Ja=e.addClass=function(e,t){var n=e.className;Yi(t).test(n)||(e.className+=(n?" ":"")+t)},Qa=!1,el=function(){if(xo&&9>bo)return!1;var e=ji("div");return"draggable"in e||"dragDrop"in e}(),tl=e.splitLines=3!="\n\nb".split(/\n/).length?function(e){for(var t=0,n=[],r=e.length;r>=t;){var i=e.indexOf("\n",t);-1==i&&(i=e.length);var o=e.slice(t,"\r"==e.charAt(i-1)?i-1:i),a=o.indexOf("\r");-1!=a?(n.push(o.slice(0,a)),t+=a+1):(n.push(o),t=i+1)}return n}:function(e){return e.split(/\r\n?|\n/)},nl=window.getSelection?function(e){try{return e.selectionStart!=e.selectionEnd}catch(t){return!1}}:function(e){try{var t=e.ownerDocument.selection.createRange()}catch(n){}return t&&t.parentElement()==e?0!=t.compareEndPoints("StartToEnd",t):!1},rl=function(){var e=ji("div");return"oncopy"in e?!0:(e.setAttribute("oncopy","return;"),"function"==typeof e.oncopy)}(),il=null,ol=e.keyNames={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",61:"=",91:"Mod",92:"Mod",93:"Mod",106:"*",107:"=",109:"-",110:".",111:"/",127:"Delete",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63232:"Up",63233:"Down",63234:"Left",63235:"Right",63272:"Delete",63273:"Home",63275:"End",63276:"PageUp",63277:"PageDown",63302:"Insert"};!function(){for(var e=0;10>e;e++)ol[e+48]=ol[e+96]=String(e);for(var e=65;90>=e;e++)ol[e]=String.fromCharCode(e);for(var e=1;12>=e;e++)ol[e+111]=ol[e+63235]="F"+e}();var al,ll=function(){function e(e){return 247>=e?n.charAt(e):e>=1424&&1524>=e?"R":e>=1536&&1773>=e?r.charAt(e-1536):e>=1774&&2220>=e?"r":e>=8192&&8203>=e?"w":8204==e?"b":"L"}function t(e,t,n){this.level=e,this.from=t,this.to=n}var n="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",r="rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm",i=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,o=/[stwN]/,a=/[LRr]/,l=/[Lb1n]/,s=/[1n]/,c="L";return function(n){if(!i.test(n))return!1;for(var r,u=n.length,f=[],h=0;u>h;++h)f.push(r=e(n.charCodeAt(h)));for(var h=0,d=c;u>h;++h){var r=f[h];"m"==r?f[h]=d:d=r}for(var h=0,p=c;u>h;++h){var r=f[h];"1"==r&&"r"==p?f[h]="n":a.test(r)&&(p=r,"r"==r&&(f[h]="R"))}for(var h=1,d=f[0];u-1>h;++h){var r=f[h];"+"==r&&"1"==d&&"1"==f[h+1]?f[h]="1":","!=r||d!=f[h+1]||"1"!=d&&"n"!=d||(f[h]=d),d=r}for(var h=0;u>h;++h){var r=f[h];if(","==r)f[h]="N";else if("%"==r){for(var m=h+1;u>m&&"%"==f[m];++m);for(var g=h&&"!"==f[h-1]||u>m&&"1"==f[m]?"1":"N",v=h;m>v;++v)f[v]=g;h=m-1}}for(var h=0,p=c;u>h;++h){var r=f[h];"L"==p&&"1"==r?f[h]="L":a.test(r)&&(p=r)}for(var h=0;u>h;++h)if(o.test(f[h])){for(var m=h+1;u>m&&o.test(f[m]);++m);for(var y="L"==(h?f[h-1]:c),x="L"==(u>m?f[m]:c),g=y||x?"L":"R",v=h;m>v;++v)f[v]=g;h=m-1}for(var b,w=[],h=0;u>h;)if(l.test(f[h])){var k=h;for(++h;u>h&&l.test(f[h]);++h);w.push(new t(0,k,h))}else{var S=h,C=w.length;for(++h;u>h&&"L"!=f[h];++h);for(var v=S;h>v;)if(s.test(f[v])){v>S&&w.splice(C,0,new t(1,S,v));var L=v;for(++v;h>v&&s.test(f[v]);++v);w.splice(C,0,new t(2,L,v)),S=v}else++v;h>S&&w.splice(C,0,new t(1,S,h))}return 1==w[0].level&&(b=n.match(/^\s+/))&&(w[0].from=b[0].length,w.unshift(new t(0,0,b[0].length))),1==Ii(w).level&&(b=n.match(/\s+$/))&&(Ii(w).to-=b[0].length,w.push(new t(0,u-b[0].length,u))),2==w[0].level&&w.unshift(new t(1,w[0].to,w[0].to)),w[0].level!=Ii(w).level&&w.push(new t(w[0].level,u,u)),w}}();return e.version="5.15.2",e})},{}],11:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../markdown/markdown"),t("../../addon/mode/overlay")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../markdown/markdown","../../addon/mode/overlay"],i):i(CodeMirror)}(function(e){"use strict";var t=/^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i;e.defineMode("gfm",function(n,r){function i(e){return e.code=!1,null}var o=0,a={startState:function(){return{code:!1,codeBlock:!1,ateSpace:!1}},copyState:function(e){return{code:e.code,codeBlock:e.codeBlock,ateSpace:e.ateSpace}},token:function(e,n){if(n.combineTokens=null,n.codeBlock)return e.match(/^```+/)?(n.codeBlock=!1,null):(e.skipToEnd(),null);if(e.sol()&&(n.code=!1),e.sol()&&e.match(/^```+/))return e.skipToEnd(),n.codeBlock=!0,null;if("`"===e.peek()){e.next();var i=e.pos;e.eatWhile("`");var a=1+e.pos-i;return n.code?a===o&&(n.code=!1):(o=a,n.code=!0),null}if(n.code)return e.next(),null;if(e.eatSpace())return n.ateSpace=!0,null;if((e.sol()||n.ateSpace)&&(n.ateSpace=!1,r.gitHubSpice!==!1)){if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/))return n.combineTokens=!0,"link";if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/))return n.combineTokens=!0,"link"}return e.match(t)&&"]("!=e.string.slice(e.start-2,e.start)&&(0==e.start||/\W/.test(e.string.charAt(e.start-1)))?(n.combineTokens=!0,"link"):(e.next(),null)},blankLine:i},l={underscoresBreakWords:!1,taskLists:!0,fencedCodeBlocks:"```",strikethrough:!0};for(var s in r)l[s]=r[s];return l.name="markdown",e.overlayMode(e.getMode(n,l),a)},"markdown"),e.defineMIME("text/x-gfm","gfm")})},{"../../addon/mode/overlay":8,"../../lib/codemirror":10,"../markdown/markdown":12}],12:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../xml/xml"),t("../meta")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../xml/xml","../meta"],i):i(CodeMirror)}(function(e){"use strict";e.defineMode("markdown",function(t,n){function r(n){if(e.findModeByName){var r=e.findModeByName(n);r&&(n=r.mime||r.mimes[0])}var i=e.getMode(t,n);return"null"==i.name?null:i}function i(e,t,n){return t.f=t.inline=n,n(e,t)}function o(e,t,n){return t.f=t.block=n,n(e,t)}function a(e){return!e||!/\S/.test(e.string)}function l(e){return e.linkTitle=!1,e.em=!1,e.strong=!1,e.strikethrough=!1,e.quote=0,e.indentedCode=!1,k&&e.f==c&&(e.f=p,e.block=s),e.trailingSpace=0,e.trailingSpaceNewLine=!1,e.prevLine=e.thisLine,e.thisLine=null,null}function s(t,o){var l=t.sol(),s=o.list!==!1,c=o.indentedCode;o.indentedCode=!1,s&&(o.indentationDiff>=0?(o.indentationDiff<4&&(o.indentation-=o.indentationDiff),o.list=null):o.indentation>0?o.list=null:o.list=!1);var f=null;if(o.indentationDiff>=4)return t.skipToEnd(),c||a(o.prevLine)?(o.indentation-=4,o.indentedCode=!0,S.code):null;if(t.eatSpace())return null;if((f=t.match(A))&&f[1].length<=6)return o.header=f[1].length,n.highlightFormatting&&(o.formatting="header"),o.f=o.inline,h(o);if(!(a(o.prevLine)||o.quote||s||c)&&(f=t.match(E)))return o.header="="==f[0].charAt(0)?1:2,n.highlightFormatting&&(o.formatting="header"),o.f=o.inline,h(o);if(t.eat(">"))return o.quote=l?1:o.quote+1,n.highlightFormatting&&(o.formatting="quote"),t.eatSpace(),h(o);if("["===t.peek())return i(t,o,y);if(t.match(L,!0))return o.hr=!0,S.hr;if((a(o.prevLine)||s)&&(t.match(T,!1)||t.match(M,!1))){var d=null;for(t.match(T,!0)?d="ul":(t.match(M,!0),d="ol"),o.indentation=t.column()+t.current().length,o.list=!0;o.listStack&&t.column()")>-1)&&(n.f=p,n.block=s,n.htmlState=null)}return r}function u(e,t){return t.fencedChars&&e.match(t.fencedChars,!1)?(t.localMode=t.localState=null,t.f=t.block=f,null):t.localMode?t.localMode.token(e,t.localState):(e.skipToEnd(),S.code)}function f(e,t){e.match(t.fencedChars),t.block=s,t.f=p,t.fencedChars=null,n.highlightFormatting&&(t.formatting="code-block"),t.code=1;var r=h(t);return t.code=0,r}function h(e){var t=[];if(e.formatting){t.push(S.formatting),"string"==typeof e.formatting&&(e.formatting=[e.formatting]);for(var r=0;r=e.quote?t.push(S.formatting+"-"+e.formatting[r]+"-"+e.quote):t.push("error"))}if(e.taskOpen)return t.push("meta"),t.length?t.join(" "):null;if(e.taskClosed)return t.push("property"),t.length?t.join(" "):null;if(e.linkHref?t.push(S.linkHref,"url"):(e.strong&&t.push(S.strong),e.em&&t.push(S.em),e.strikethrough&&t.push(S.strikethrough),e.linkText&&t.push(S.linkText),e.code&&t.push(S.code)),e.header&&t.push(S.header,S.header+"-"+e.header),e.quote&&(t.push(S.quote),!n.maxBlockquoteDepth||n.maxBlockquoteDepth>=e.quote?t.push(S.quote+"-"+e.quote):t.push(S.quote+"-"+n.maxBlockquoteDepth)),e.list!==!1){var i=(e.listStack.length-1)%3;i?1===i?t.push(S.list2):t.push(S.list3):t.push(S.list1)}return e.trailingSpaceNewLine?t.push("trailing-space-new-line"):e.trailingSpace&&t.push("trailing-space-"+(e.trailingSpace%2?"a":"b")),t.length?t.join(" "):null}function d(e,t){return e.match(O,!0)?h(t):void 0}function p(t,r){var i=r.text(t,r);if("undefined"!=typeof i)return i;if(r.list)return r.list=null,h(r);if(r.taskList){var a="x"!==t.match(N,!0)[1];return a?r.taskOpen=!0:r.taskClosed=!0,n.highlightFormatting&&(r.formatting="task"),r.taskList=!1,h(r)}if(r.taskOpen=!1,r.taskClosed=!1,r.header&&t.match(/^#+$/,!0))return n.highlightFormatting&&(r.formatting="header"), +h(r);var l=t.sol(),s=t.next();if(r.linkTitle){r.linkTitle=!1;var u=s;"("===s&&(u=")"),u=(u+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1");var f="^\\s*(?:[^"+u+"\\\\]+|\\\\\\\\|\\\\.)"+u;if(t.match(new RegExp(f),!0))return S.linkHref}if("`"===s){var d=r.formatting;n.highlightFormatting&&(r.formatting="code"),t.eatWhile("`");var p=t.current().length;if(0==r.code)return r.code=p,h(r);if(p==r.code){var v=h(r);return r.code=0,v}return r.formatting=d,h(r)}if(r.code)return h(r);if("\\"===s&&(t.next(),n.highlightFormatting)){var y=h(r),x=S.formatting+"-escape";return y?y+" "+x:x}if("!"===s&&t.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return t.match(/\[[^\]]*\]/),r.inline=r.f=g,S.image;if("["===s&&t.match(/[^\]]*\](\(.*\)| ?\[.*?\])/,!1))return r.linkText=!0,n.highlightFormatting&&(r.formatting="link"),h(r);if("]"===s&&r.linkText&&t.match(/\(.*?\)| ?\[.*?\]/,!1)){n.highlightFormatting&&(r.formatting="link");var y=h(r);return r.linkText=!1,r.inline=r.f=g,y}if("<"===s&&t.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkInline}if("<"===s&&t.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkEmail}if("<"===s&&t.match(/^(!--|\w)/,!1)){var b=t.string.indexOf(">",t.pos);if(-1!=b){var k=t.string.substring(t.start,b);/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(k)&&(r.md_inside=!0)}return t.backUp(1),r.htmlState=e.startState(w),o(t,r,c)}if("<"===s&&t.match(/^\/\w*?>/))return r.md_inside=!1,"tag";var C=!1;if(!n.underscoresBreakWords&&"_"===s&&"_"!==t.peek()&&t.match(/(\w)/,!1)){var L=t.pos-2;if(L>=0){var T=t.string.charAt(L);"_"!==T&&T.match(/(\w)/,!1)&&(C=!0)}}if("*"===s||"_"===s&&!C)if(l&&" "===t.peek());else{if(r.strong===s&&t.eat(s)){n.highlightFormatting&&(r.formatting="strong");var v=h(r);return r.strong=!1,v}if(!r.strong&&t.eat(s))return r.strong=s,n.highlightFormatting&&(r.formatting="strong"),h(r);if(r.em===s){n.highlightFormatting&&(r.formatting="em");var v=h(r);return r.em=!1,v}if(!r.em)return r.em=s,n.highlightFormatting&&(r.formatting="em"),h(r)}else if(" "===s&&(t.eat("*")||t.eat("_"))){if(" "===t.peek())return h(r);t.backUp(1)}if(n.strikethrough)if("~"===s&&t.eatWhile(s)){if(r.strikethrough){n.highlightFormatting&&(r.formatting="strikethrough");var v=h(r);return r.strikethrough=!1,v}if(t.match(/^[^\s]/,!1))return r.strikethrough=!0,n.highlightFormatting&&(r.formatting="strikethrough"),h(r)}else if(" "===s&&t.match(/^~~/,!0)){if(" "===t.peek())return h(r);t.backUp(2)}return" "===s&&(t.match(/ +$/,!1)?r.trailingSpace++:r.trailingSpace&&(r.trailingSpaceNewLine=!0)),h(r)}function m(e,t){var r=e.next();if(">"===r){t.f=t.inline=p,n.highlightFormatting&&(t.formatting="link");var i=h(t);return i?i+=" ":i="",i+S.linkInline}return e.match(/^[^>]+/,!0),S.linkInline}function g(e,t){if(e.eatSpace())return null;var r=e.next();return"("===r||"["===r?(t.f=t.inline=v("("===r?")":"]",0),n.highlightFormatting&&(t.formatting="link-string"),t.linkHref=!0,h(t)):"error"}function v(e){return function(t,r){var i=t.next();if(i===e){r.f=r.inline=p,n.highlightFormatting&&(r.formatting="link-string");var o=h(r);return r.linkHref=!1,o}return t.match(P[e]),r.linkHref=!0,h(r)}}function y(e,t){return e.match(/^([^\]\\]|\\.)*\]:/,!1)?(t.f=x,e.next(),n.highlightFormatting&&(t.formatting="link"),t.linkText=!0,h(t)):i(e,t,p)}function x(e,t){if(e.match(/^\]:/,!0)){t.f=t.inline=b,n.highlightFormatting&&(t.formatting="link");var r=h(t);return t.linkText=!1,r}return e.match(/^([^\]\\]|\\.)+/,!0),S.linkText}function b(e,t){return e.eatSpace()?null:(e.match(/^[^\s]+/,!0),void 0===e.peek()?t.linkTitle=!0:e.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,!0),t.f=t.inline=p,S.linkHref+" url")}var w=e.getMode(t,"text/html"),k="null"==w.name;void 0===n.highlightFormatting&&(n.highlightFormatting=!1),void 0===n.maxBlockquoteDepth&&(n.maxBlockquoteDepth=0),void 0===n.underscoresBreakWords&&(n.underscoresBreakWords=!0),void 0===n.taskLists&&(n.taskLists=!1),void 0===n.strikethrough&&(n.strikethrough=!1),void 0===n.tokenTypeOverrides&&(n.tokenTypeOverrides={});var S={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"tag",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough"};for(var C in S)S.hasOwnProperty(C)&&n.tokenTypeOverrides[C]&&(S[C]=n.tokenTypeOverrides[C]);var L=/^([*\-_])(?:\s*\1){2,}\s*$/,T=/^[*\-+]\s+/,M=/^[0-9]+([.)])\s+/,N=/^\[(x| )\](?=\s)/,A=n.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,E=/^ *(?:\={1,}|-{1,})\s*$/,O=/^[^#!\[\]*_\\<>` "'(~]+/,I=new RegExp("^("+(n.fencedCodeBlocks===!0?"~~~+|```+":n.fencedCodeBlocks)+")[ \\t]*([\\w+#-]*)"),P={")":/^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,"]":/^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\\]]|\\.)*\])*?(?=\])/},R={startState:function(){return{f:s,prevLine:null,thisLine:null,block:s,htmlState:null,indentation:0,inline:p,text:d,formatting:!1,linkText:!1,linkHref:!1,linkTitle:!1,code:0,em:!1,strong:!1,header:0,hr:!1,taskList:!1,list:!1,listStack:[],quote:0,trailingSpace:0,trailingSpaceNewLine:!1,strikethrough:!1,fencedChars:null}},copyState:function(t){return{f:t.f,prevLine:t.prevLine,thisLine:t.thisLine,block:t.block,htmlState:t.htmlState&&e.copyState(w,t.htmlState),indentation:t.indentation,localMode:t.localMode,localState:t.localMode?e.copyState(t.localMode,t.localState):null,inline:t.inline,text:t.text,formatting:!1,linkTitle:t.linkTitle,code:t.code,em:t.em,strong:t.strong,strikethrough:t.strikethrough,header:t.header,hr:t.hr,taskList:t.taskList,list:t.list,listStack:t.listStack.slice(0),quote:t.quote,indentedCode:t.indentedCode,trailingSpace:t.trailingSpace,trailingSpaceNewLine:t.trailingSpaceNewLine,md_inside:t.md_inside,fencedChars:t.fencedChars}},token:function(e,t){if(t.formatting=!1,e!=t.thisLine){var n=t.header||t.hr;if(t.header=0,t.hr=!1,e.match(/^\s*$/,!0)||n){if(l(t),!n)return null;t.prevLine=null}t.prevLine=t.thisLine,t.thisLine=e,t.taskList=!1,t.trailingSpace=0,t.trailingSpaceNewLine=!1,t.f=t.block;var r=e.match(/^\s*/,!0)[0].replace(/\t/g," ").length;if(t.indentationDiff=Math.min(r-t.indentation,4),t.indentation=t.indentation+t.indentationDiff,r>0)return null}return t.f(e,t)},innerMode:function(e){return e.block==c?{state:e.htmlState,mode:w}:e.localState?{state:e.localState,mode:e.localMode}:{state:e,mode:R}},blankLine:l,getType:h,fold:"markdown"};return R},"xml"),e.defineMIME("text/x-markdown","markdown")})},{"../../lib/codemirror":10,"../meta":13,"../xml/xml":14}],13:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../lib/codemirror")):"function"==typeof e&&e.amd?e(["../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.modeInfo=[{name:"APL",mime:"text/apl",mode:"apl",ext:["dyalog","apl"]},{name:"PGP",mimes:["application/pgp","application/pgp-keys","application/pgp-signature"],mode:"asciiarmor",ext:["pgp"]},{name:"ASN.1",mime:"text/x-ttcn-asn",mode:"asn.1",ext:["asn","asn1"]},{name:"Asterisk",mime:"text/x-asterisk",mode:"asterisk",file:/^extensions\.conf$/i},{name:"Brainfuck",mime:"text/x-brainfuck",mode:"brainfuck",ext:["b","bf"]},{name:"C",mime:"text/x-csrc",mode:"clike",ext:["c","h"]},{name:"C++",mime:"text/x-c++src",mode:"clike",ext:["cpp","c++","cc","cxx","hpp","h++","hh","hxx"],alias:["cpp"]},{name:"Cobol",mime:"text/x-cobol",mode:"cobol",ext:["cob","cpy"]},{name:"C#",mime:"text/x-csharp",mode:"clike",ext:["cs"],alias:["csharp"]},{name:"Clojure",mime:"text/x-clojure",mode:"clojure",ext:["clj","cljc","cljx"]},{name:"ClojureScript",mime:"text/x-clojurescript",mode:"clojure",ext:["cljs"]},{name:"Closure Stylesheets (GSS)",mime:"text/x-gss",mode:"css",ext:["gss"]},{name:"CMake",mime:"text/x-cmake",mode:"cmake",ext:["cmake","cmake.in"],file:/^CMakeLists.txt$/},{name:"CoffeeScript",mime:"text/x-coffeescript",mode:"coffeescript",ext:["coffee"],alias:["coffee","coffee-script"]},{name:"Common Lisp",mime:"text/x-common-lisp",mode:"commonlisp",ext:["cl","lisp","el"],alias:["lisp"]},{name:"Cypher",mime:"application/x-cypher-query",mode:"cypher",ext:["cyp","cypher"]},{name:"Cython",mime:"text/x-cython",mode:"python",ext:["pyx","pxd","pxi"]},{name:"Crystal",mime:"text/x-crystal",mode:"crystal",ext:["cr"]},{name:"CSS",mime:"text/css",mode:"css",ext:["css"]},{name:"CQL",mime:"text/x-cassandra",mode:"sql",ext:["cql"]},{name:"D",mime:"text/x-d",mode:"d",ext:["d"]},{name:"Dart",mimes:["application/dart","text/x-dart"],mode:"dart",ext:["dart"]},{name:"diff",mime:"text/x-diff",mode:"diff",ext:["diff","patch"]},{name:"Django",mime:"text/x-django",mode:"django"},{name:"Dockerfile",mime:"text/x-dockerfile",mode:"dockerfile",file:/^Dockerfile$/},{name:"DTD",mime:"application/xml-dtd",mode:"dtd",ext:["dtd"]},{name:"Dylan",mime:"text/x-dylan",mode:"dylan",ext:["dylan","dyl","intr"]},{name:"EBNF",mime:"text/x-ebnf",mode:"ebnf"},{name:"ECL",mime:"text/x-ecl",mode:"ecl",ext:["ecl"]},{name:"edn",mime:"application/edn",mode:"clojure",ext:["edn"]},{name:"Eiffel",mime:"text/x-eiffel",mode:"eiffel",ext:["e"]},{name:"Elm",mime:"text/x-elm",mode:"elm",ext:["elm"]},{name:"Embedded Javascript",mime:"application/x-ejs",mode:"htmlembedded",ext:["ejs"]},{name:"Embedded Ruby",mime:"application/x-erb",mode:"htmlembedded",ext:["erb"]},{name:"Erlang",mime:"text/x-erlang",mode:"erlang",ext:["erl"]},{name:"Factor",mime:"text/x-factor",mode:"factor",ext:["factor"]},{name:"FCL",mime:"text/x-fcl",mode:"fcl"},{name:"Forth",mime:"text/x-forth",mode:"forth",ext:["forth","fth","4th"]},{name:"Fortran",mime:"text/x-fortran",mode:"fortran",ext:["f","for","f77","f90"]},{name:"F#",mime:"text/x-fsharp",mode:"mllike",ext:["fs"],alias:["fsharp"]},{name:"Gas",mime:"text/x-gas",mode:"gas",ext:["s"]},{name:"Gherkin",mime:"text/x-feature",mode:"gherkin",ext:["feature"]},{name:"GitHub Flavored Markdown",mime:"text/x-gfm",mode:"gfm",file:/^(readme|contributing|history).md$/i},{name:"Go",mime:"text/x-go",mode:"go",ext:["go"]},{name:"Groovy",mime:"text/x-groovy",mode:"groovy",ext:["groovy","gradle"]},{name:"HAML",mime:"text/x-haml",mode:"haml",ext:["haml"]},{name:"Haskell",mime:"text/x-haskell",mode:"haskell",ext:["hs"]},{name:"Haskell (Literate)",mime:"text/x-literate-haskell",mode:"haskell-literate",ext:["lhs"]},{name:"Haxe",mime:"text/x-haxe",mode:"haxe",ext:["hx"]},{name:"HXML",mime:"text/x-hxml",mode:"haxe",ext:["hxml"]},{name:"ASP.NET",mime:"application/x-aspx",mode:"htmlembedded",ext:["aspx"],alias:["asp","aspx"]},{name:"HTML",mime:"text/html",mode:"htmlmixed",ext:["html","htm"],alias:["xhtml"]},{name:"HTTP",mime:"message/http",mode:"http"},{name:"IDL",mime:"text/x-idl",mode:"idl",ext:["pro"]},{name:"Jade",mime:"text/x-jade",mode:"jade",ext:["jade"]},{name:"Java",mime:"text/x-java",mode:"clike",ext:["java"]},{name:"Java Server Pages",mime:"application/x-jsp",mode:"htmlembedded",ext:["jsp"],alias:["jsp"]},{name:"JavaScript",mimes:["text/javascript","text/ecmascript","application/javascript","application/x-javascript","application/ecmascript"],mode:"javascript",ext:["js"],alias:["ecmascript","js","node"]},{name:"JSON",mimes:["application/json","application/x-json"],mode:"javascript",ext:["json","map"],alias:["json5"]},{name:"JSON-LD",mime:"application/ld+json",mode:"javascript",ext:["jsonld"],alias:["jsonld"]},{name:"JSX",mime:"text/jsx",mode:"jsx",ext:["jsx"]},{name:"Jinja2",mime:"null",mode:"jinja2"},{name:"Julia",mime:"text/x-julia",mode:"julia",ext:["jl"]},{name:"Kotlin",mime:"text/x-kotlin",mode:"clike",ext:["kt"]},{name:"LESS",mime:"text/x-less",mode:"css",ext:["less"]},{name:"LiveScript",mime:"text/x-livescript",mode:"livescript",ext:["ls"],alias:["ls"]},{name:"Lua",mime:"text/x-lua",mode:"lua",ext:["lua"]},{name:"Markdown",mime:"text/x-markdown",mode:"markdown",ext:["markdown","md","mkd"]},{name:"mIRC",mime:"text/mirc",mode:"mirc"},{name:"MariaDB SQL",mime:"text/x-mariadb",mode:"sql"},{name:"Mathematica",mime:"text/x-mathematica",mode:"mathematica",ext:["m","nb"]},{name:"Modelica",mime:"text/x-modelica",mode:"modelica",ext:["mo"]},{name:"MUMPS",mime:"text/x-mumps",mode:"mumps",ext:["mps"]},{name:"MS SQL",mime:"text/x-mssql",mode:"sql"},{name:"mbox",mime:"application/mbox",mode:"mbox",ext:["mbox"]},{name:"MySQL",mime:"text/x-mysql",mode:"sql"},{name:"Nginx",mime:"text/x-nginx-conf",mode:"nginx",file:/nginx.*\.conf$/i},{name:"NSIS",mime:"text/x-nsis",mode:"nsis",ext:["nsh","nsi"]},{name:"NTriples",mime:"text/n-triples",mode:"ntriples",ext:["nt"]},{name:"Objective C",mime:"text/x-objectivec",mode:"clike",ext:["m","mm"],alias:["objective-c","objc"]},{name:"OCaml",mime:"text/x-ocaml",mode:"mllike",ext:["ml","mli","mll","mly"]},{name:"Octave",mime:"text/x-octave",mode:"octave",ext:["m"]},{name:"Oz",mime:"text/x-oz",mode:"oz",ext:["oz"]},{name:"Pascal",mime:"text/x-pascal",mode:"pascal",ext:["p","pas"]},{name:"PEG.js",mime:"null",mode:"pegjs",ext:["jsonld"]},{name:"Perl",mime:"text/x-perl",mode:"perl",ext:["pl","pm"]},{name:"PHP",mime:"application/x-httpd-php",mode:"php",ext:["php","php3","php4","php5","phtml"]},{name:"Pig",mime:"text/x-pig",mode:"pig",ext:["pig"]},{name:"Plain Text",mime:"text/plain",mode:"null",ext:["txt","text","conf","def","list","log"]},{name:"PLSQL",mime:"text/x-plsql",mode:"sql",ext:["pls"]},{name:"PowerShell",mime:"application/x-powershell",mode:"powershell",ext:["ps1","psd1","psm1"]},{name:"Properties files",mime:"text/x-properties",mode:"properties",ext:["properties","ini","in"],alias:["ini","properties"]},{name:"ProtoBuf",mime:"text/x-protobuf",mode:"protobuf",ext:["proto"]},{name:"Python",mime:"text/x-python",mode:"python",ext:["BUILD","bzl","py","pyw"],file:/^(BUCK|BUILD)$/},{name:"Puppet",mime:"text/x-puppet",mode:"puppet",ext:["pp"]},{name:"Q",mime:"text/x-q",mode:"q",ext:["q"]},{name:"R",mime:"text/x-rsrc",mode:"r",ext:["r"],alias:["rscript"]},{name:"reStructuredText",mime:"text/x-rst",mode:"rst",ext:["rst"],alias:["rst"]},{name:"RPM Changes",mime:"text/x-rpm-changes",mode:"rpm"},{name:"RPM Spec",mime:"text/x-rpm-spec",mode:"rpm",ext:["spec"]},{name:"Ruby",mime:"text/x-ruby",mode:"ruby",ext:["rb"],alias:["jruby","macruby","rake","rb","rbx"]},{name:"Rust",mime:"text/x-rustsrc",mode:"rust",ext:["rs"]},{name:"SAS",mime:"text/x-sas",mode:"sas",ext:["sas"]},{name:"Sass",mime:"text/x-sass",mode:"sass",ext:["sass"]},{name:"Scala",mime:"text/x-scala",mode:"clike",ext:["scala"]},{name:"Scheme",mime:"text/x-scheme",mode:"scheme",ext:["scm","ss"]},{name:"SCSS",mime:"text/x-scss",mode:"css",ext:["scss"]},{name:"Shell",mime:"text/x-sh",mode:"shell",ext:["sh","ksh","bash"],alias:["bash","sh","zsh"],file:/^PKGBUILD$/},{name:"Sieve",mime:"application/sieve",mode:"sieve",ext:["siv","sieve"]},{name:"Slim",mimes:["text/x-slim","application/x-slim"],mode:"slim",ext:["slim"]},{name:"Smalltalk",mime:"text/x-stsrc",mode:"smalltalk",ext:["st"]},{name:"Smarty",mime:"text/x-smarty",mode:"smarty",ext:["tpl"]},{name:"Solr",mime:"text/x-solr",mode:"solr"},{name:"Soy",mime:"text/x-soy",mode:"soy",ext:["soy"],alias:["closure template"]},{name:"SPARQL",mime:"application/sparql-query",mode:"sparql",ext:["rq","sparql"],alias:["sparul"]},{name:"Spreadsheet",mime:"text/x-spreadsheet",mode:"spreadsheet",alias:["excel","formula"]},{name:"SQL",mime:"text/x-sql",mode:"sql",ext:["sql"]},{name:"Squirrel",mime:"text/x-squirrel",mode:"clike",ext:["nut"]},{name:"Swift",mime:"text/x-swift",mode:"swift",ext:["swift"]},{name:"sTeX",mime:"text/x-stex",mode:"stex"},{name:"LaTeX",mime:"text/x-latex",mode:"stex",ext:["text","ltx"],alias:["tex"]},{name:"SystemVerilog",mime:"text/x-systemverilog",mode:"verilog",ext:["v"]},{name:"Tcl",mime:"text/x-tcl",mode:"tcl",ext:["tcl"]},{name:"Textile",mime:"text/x-textile",mode:"textile",ext:["textile"]},{name:"TiddlyWiki ",mime:"text/x-tiddlywiki",mode:"tiddlywiki"},{name:"Tiki wiki",mime:"text/tiki",mode:"tiki"},{name:"TOML",mime:"text/x-toml",mode:"toml",ext:["toml"]},{name:"Tornado",mime:"text/x-tornado",mode:"tornado"},{name:"troff",mime:"text/troff",mode:"troff",ext:["1","2","3","4","5","6","7","8","9"]},{name:"TTCN",mime:"text/x-ttcn",mode:"ttcn",ext:["ttcn","ttcn3","ttcnpp"]},{name:"TTCN_CFG",mime:"text/x-ttcn-cfg",mode:"ttcn-cfg",ext:["cfg"]},{name:"Turtle",mime:"text/turtle",mode:"turtle",ext:["ttl"]},{name:"TypeScript",mime:"application/typescript",mode:"javascript",ext:["ts"],alias:["ts"]},{name:"Twig",mime:"text/x-twig",mode:"twig"},{name:"Web IDL",mime:"text/x-webidl",mode:"webidl",ext:["webidl"]},{name:"VB.NET",mime:"text/x-vb",mode:"vb",ext:["vb"]},{name:"VBScript",mime:"text/vbscript",mode:"vbscript",ext:["vbs"]},{name:"Velocity",mime:"text/velocity",mode:"velocity",ext:["vtl"]},{name:"Verilog",mime:"text/x-verilog",mode:"verilog",ext:["v"]},{name:"VHDL",mime:"text/x-vhdl",mode:"vhdl",ext:["vhd","vhdl"]},{name:"XML",mimes:["application/xml","text/xml"],mode:"xml",ext:["xml","xsl","xsd"],alias:["rss","wsdl","xsd"]},{name:"XQuery",mime:"application/xquery",mode:"xquery",ext:["xy","xquery"]},{name:"Yacas",mime:"text/x-yacas",mode:"yacas",ext:["ys"]},{name:"YAML",mime:"text/x-yaml",mode:"yaml",ext:["yaml","yml"],alias:["yml"]},{name:"Z80",mime:"text/x-z80",mode:"z80",ext:["z80"]},{name:"mscgen",mime:"text/x-mscgen",mode:"mscgen",ext:["mscgen","mscin","msc"]},{name:"xu",mime:"text/x-xu",mode:"mscgen",ext:["xu"]},{name:"msgenny",mime:"text/x-msgenny",mode:"mscgen",ext:["msgenny"]}];for(var t=0;t-1&&t.substring(i+1,t.length);return o?e.findModeByExtension(o):void 0},e.findModeByName=function(t){t=t.toLowerCase();for(var n=0;n")):null:e.match("--")?n(s("comment","-->")):e.match("DOCTYPE",!0,!0)?(e.eatWhile(/[\w\._\-]/),n(c(1))):null:e.eat("?")?(e.eatWhile(/[\w\._\-]/),t.tokenize=s("meta","?>"),"meta"):(T=e.eat("/")?"closeTag":"openTag",t.tokenize=a,"tag bracket");if("&"==r){var i;return i=e.eat("#")?e.eat("x")?e.eatWhile(/[a-fA-F\d]/)&&e.eat(";"):e.eatWhile(/[\d]/)&&e.eat(";"):e.eatWhile(/[\w\.\-:]/)&&e.eat(";"),i?"atom":"error"}return e.eatWhile(/[^&<]/),null}function a(e,t){var n=e.next();if(">"==n||"/"==n&&e.eat(">"))return t.tokenize=o,T=">"==n?"endTag":"selfcloseTag","tag bracket";if("="==n)return T="equals",null;if("<"==n){t.tokenize=o,t.state=d,t.tagName=t.tagStart=null;var r=t.tokenize(e,t);return r?r+" tag error":"tag error"}return/[\'\"]/.test(n)?(t.tokenize=l(n),t.stringStartCol=e.column(),t.tokenize(e,t)):(e.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function l(e){var t=function(t,n){for(;!t.eol();)if(t.next()==e){n.tokenize=a;break}return"string"};return t.isInAttribute=!0,t}function s(e,t){return function(n,r){for(;!n.eol();){if(n.match(t)){r.tokenize=o;break}n.next()}return e}}function c(e){return function(t,n){for(var r;null!=(r=t.next());){if("<"==r)return n.tokenize=c(e+1),n.tokenize(t,n);if(">"==r){if(1==e){n.tokenize=o;break}return n.tokenize=c(e-1),n.tokenize(t,n)}}return"meta"}}function u(e,t,n){this.prev=e.context,this.tagName=t,this.indent=e.indented,this.startOfLine=n,(S.doNotIndent.hasOwnProperty(t)||e.context&&e.context.noIndent)&&(this.noIndent=!0)}function f(e){e.context&&(e.context=e.context.prev)}function h(e,t){for(var n;;){if(!e.context)return;if(n=e.context.tagName,!S.contextGrabbers.hasOwnProperty(n)||!S.contextGrabbers[n].hasOwnProperty(t))return;f(e)}}function d(e,t,n){return"openTag"==e?(n.tagStart=t.column(),p):"closeTag"==e?m:d}function p(e,t,n){return"word"==e?(n.tagName=t.current(),M="tag",y):(M="error",p)}function m(e,t,n){if("word"==e){var r=t.current();return n.context&&n.context.tagName!=r&&S.implicitlyClosed.hasOwnProperty(n.context.tagName)&&f(n),n.context&&n.context.tagName==r||S.matchClosing===!1?(M="tag",g):(M="tag error",v)}return M="error",v}function g(e,t,n){return"endTag"!=e?(M="error",g):(f(n),d)}function v(e,t,n){return M="error",g(e,t,n)}function y(e,t,n){if("word"==e)return M="attribute",x;if("endTag"==e||"selfcloseTag"==e){var r=n.tagName,i=n.tagStart;return n.tagName=n.tagStart=null,"selfcloseTag"==e||S.autoSelfClosers.hasOwnProperty(r)?h(n,r):(h(n,r),n.context=new u(n,r,i==n.indented)),d}return M="error",y}function x(e,t,n){return"equals"==e?b:(S.allowMissing||(M="error"),y(e,t,n))}function b(e,t,n){return"string"==e?w:"word"==e&&S.allowUnquoted?(M="string",y):(M="error",y(e,t,n))}function w(e,t,n){return"string"==e?w:y(e,t,n)}var k=r.indentUnit,S={},C=i.htmlMode?t:n;for(var L in C)S[L]=C[L];for(var L in i)S[L]=i[L];var T,M;return o.isInText=!0,{startState:function(e){var t={tokenize:o,state:d,indented:e||0,tagName:null,tagStart:null,context:null};return null!=e&&(t.baseIndent=e),t},token:function(e,t){if(!t.tagName&&e.sol()&&(t.indented=e.indentation()),e.eatSpace())return null;T=null;var n=t.tokenize(e,t);return(n||T)&&"comment"!=n&&(M=null,t.state=t.state(T||n,e,t),M&&(n="error"==M?n+" error":M)),n},indent:function(t,n,r){var i=t.context;if(t.tokenize.isInAttribute)return t.tagStart==t.indented?t.stringStartCol+1:t.indented+k;if(i&&i.noIndent)return e.Pass;if(t.tokenize!=a&&t.tokenize!=o)return r?r.match(/^(\s*)/)[0].length:0;if(t.tagName)return S.multilineTagIndentPastTag!==!1?t.tagStart+t.tagName.length+2:t.tagStart+k*(S.multilineTagIndentFactor||1);if(S.alignCDATA&&/$/,blockCommentStart:"",configuration:S.htmlMode?"html":"xml",helperType:S.htmlMode?"html":"xml",skipAttribute:function(e){e.state==b&&(e.state=y)}}}),e.defineMIME("text/xml","xml"),e.defineMIME("application/xml","xml"),e.mimeModes.hasOwnProperty("text/html")||e.defineMIME("text/html",{name:"xml",htmlMode:!0})})},{"../../lib/codemirror":10}],15:[function(e,t,n){n.read=function(e,t,n,r,i){var o,a,l=8*i-r-1,s=(1<>1,u=-7,f=n?i-1:0,h=n?-1:1,d=e[t+f];for(f+=h,o=d&(1<<-u)-1,d>>=-u,u+=l;u>0;o=256*o+e[t+f],f+=h,u-=8);for(a=o&(1<<-u)-1,o>>=-u,u+=r;u>0;a=256*a+e[t+f],f+=h,u-=8);if(0===o)o=1-c;else{if(o===s)return a?NaN:(d?-1:1)*(1/0);a+=Math.pow(2,r),o-=c}return(d?-1:1)*a*Math.pow(2,o-r)},n.write=function(e,t,n,r,i,o){var a,l,s,c=8*o-i-1,u=(1<>1,h=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,d=r?0:o-1,p=r?1:-1,m=0>t||0===t&&0>1/t?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(l=isNaN(t)?1:0,a=u):(a=Math.floor(Math.log(t)/Math.LN2),t*(s=Math.pow(2,-a))<1&&(a--,s*=2),t+=a+f>=1?h/s:h*Math.pow(2,1-f),t*s>=2&&(a++,s/=2),a+f>=u?(l=0,a=u):a+f>=1?(l=(t*s-1)*Math.pow(2,i),a+=f):(l=t*Math.pow(2,f-1)*Math.pow(2,i),a=0));i>=8;e[n+d]=255&l,d+=p,l/=256,i-=8);for(a=a<0;e[n+d]=255&a,d+=p,a/=256,c-=8);e[n+d-p]|=128*m}},{}],16:[function(e,t,n){var r={}.toString;t.exports=Array.isArray||function(e){return"[object Array]"==r.call(e)}},{}],17:[function(t,n,r){(function(t){(function(){function t(e){this.tokens=[],this.tokens.links={},this.options=e||h.defaults,this.rules=d.normal,this.options.gfm&&(this.options.tables?this.rules=d.tables:this.rules=d.gfm)}function i(e,t){if(this.options=t||h.defaults,this.links=e,this.rules=p.normal,this.renderer=this.options.renderer||new o,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=p.breaks:this.rules=p.gfm:this.options.pedantic&&(this.rules=p.pedantic)}function o(e){this.options=e||{}}function a(e){this.tokens=[],this.token=null,this.options=e||h.defaults,this.options.renderer=this.options.renderer||new o,this.renderer=this.options.renderer,this.renderer.options=this.options}function l(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function s(e){return e.replace(/&([#\w]+);/g,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function c(e,t){return e=e.source,t=t||"",function n(r,i){return r?(i=i.source||i,i=i.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,i),n):new RegExp(e,t)}}function u(){}function f(e){for(var t,n,r=1;rAn error occured:

    "+l(u.message+"",!0)+"
    ";throw u}}var d={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:u,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:u,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:u,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};d.bullet=/(?:[*+-]|\d+\.)/,d.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,d.item=c(d.item,"gm")(/bull/g,d.bullet)(),d.list=c(d.list)(/bull/g,d.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+d.def.source+")")(),d.blockquote=c(d.blockquote)("def",d.def)(),d._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b",d.html=c(d.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,d._tag)(),d.paragraph=c(d.paragraph)("hr",d.hr)("heading",d.heading)("lheading",d.lheading)("blockquote",d.blockquote)("tag","<"+d._tag)("def",d.def)(),d.normal=f({},d),d.gfm=f({},d.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/}),d.gfm.paragraph=c(d.paragraph)("(?!","(?!"+d.gfm.fences.source.replace("\\1","\\2")+"|"+d.list.source.replace("\\1","\\3")+"|")(),d.tables=f({},d.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),t.rules=d,t.lex=function(e,n){var r=new t(n);return r.lex(e)},t.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},t.prototype.token=function(e,t,n){for(var r,i,o,a,l,s,c,u,f,e=e.replace(/^ +$/gm,"");e;)if((o=this.rules.newline.exec(e))&&(e=e.substring(o[0].length),o[0].length>1&&this.tokens.push({type:"space"})),o=this.rules.code.exec(e))e=e.substring(o[0].length),o=o[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?o:o.replace(/\n+$/,"")});else if(o=this.rules.fences.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"code",lang:o[2],text:o[3]||""});else if(o=this.rules.heading.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"heading",depth:o[1].length,text:o[2]});else if(t&&(o=this.rules.nptable.exec(e))){for(e=e.substring(o[0].length),s={type:"table",header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/\n$/,"").split("\n")},u=0;u ?/gm,""),this.token(o,t,!0),this.tokens.push({type:"blockquote_end"});else if(o=this.rules.list.exec(e)){for(e=e.substring(o[0].length),a=o[2],this.tokens.push({type:"list_start",ordered:a.length>1}),o=o[0].match(this.rules.item),r=!1,f=o.length,u=0;f>u;u++)s=o[u],c=s.length,s=s.replace(/^ *([*+-]|\d+\.) +/,""),~s.indexOf("\n ")&&(c-=s.length,s=this.options.pedantic?s.replace(/^ {1,4}/gm,""):s.replace(new RegExp("^ {1,"+c+"}","gm"),"")),this.options.smartLists&&u!==f-1&&(l=d.bullet.exec(o[u+1])[0],a===l||a.length>1&&l.length>1||(e=o.slice(u+1).join("\n")+e,u=f-1)),i=r||/\n\n(?!\s*$)/.test(s),u!==f-1&&(r="\n"===s.charAt(s.length-1),i||(i=r)),this.tokens.push({type:i?"loose_item_start":"list_item_start"}),this.token(s,!1,n),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(o=this.rules.html.exec(e))e=e.substring(o[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===o[1]||"script"===o[1]||"style"===o[1]),text:o[0]});else if(!n&&t&&(o=this.rules.def.exec(e)))e=e.substring(o[0].length),this.tokens.links[o[1].toLowerCase()]={href:o[2],title:o[3]};else if(t&&(o=this.rules.table.exec(e))){for(e=e.substring(o[0].length),s={type:"table", +header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/,p.link=c(p.link)("inside",p._inside)("href",p._href)(),p.reflink=c(p.reflink)("inside",p._inside)(),p.normal=f({},p),p.pedantic=f({},p.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),p.gfm=f({},p.normal,{escape:c(p.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:c(p.text)("]|","~]|")("|","|https?://|")()}),p.breaks=f({},p.gfm,{br:c(p.br)("{2,}","*")(),text:c(p.gfm.text)("{2,}","*")()}),i.rules=p,i.output=function(e,t,n){var r=new i(t,n);return r.output(e)},i.prototype.output=function(e){for(var t,n,r,i,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=l(i[1]),r=n),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):l(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,o+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(l(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),o+=this.renderer.text(l(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=l(i[1]),r=n,o+=this.renderer.link(r,null,n);return o},i.prototype.outputLink=function(e,t){var n=l(t.href),r=t.title?l(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,l(e[1]))},i.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},i.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;r>i;i++)t=e.charCodeAt(i),Math.random()>.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},o.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'
    '+(n?e:l(e,!0))+"\n
    \n":"
    "+(n?e:l(e,!0))+"\n
    "},o.prototype.blockquote=function(e){return"
    \n"+e+"
    \n"},o.prototype.html=function(e){return e},o.prototype.heading=function(e,t,n){return"'+e+"\n"},o.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"},o.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+"\n"},o.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},o.prototype.paragraph=function(e){return"

    "+e+"

    \n"},o.prototype.table=function(e,t){return"\n\n"+e+"\n\n"+t+"\n
    \n"},o.prototype.tablerow=function(e){return"\n"+e+"\n"},o.prototype.tablecell=function(e,t){var n=t.header?"th":"td",r=t.align?"<"+n+' style="text-align:'+t.align+'">':"<"+n+">";return r+e+"\n"},o.prototype.strong=function(e){return""+e+""},o.prototype.em=function(e){return""+e+""},o.prototype.codespan=function(e){return""+e+""},o.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},o.prototype.del=function(e){return""+e+""},o.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(s(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(i){return""}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:"))return""}var o='
    "},o.prototype.image=function(e,t,n){var r=''+n+'":">"},o.prototype.text=function(e){return e},a.parse=function(e,t,n){var r=new a(t,n);return r.parse(e)},a.prototype.parse=function(e){this.inline=new i(e.links,this.options,this.renderer),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},a.prototype.next=function(){return this.token=this.tokens.pop()},a.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},a.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},a.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i,o="",a="";for(n="",e=0;ea;a++)for(var s=this.compoundRules[a],c=0,u=s.length;u>c;c++)this.compoundRuleCodes[s[c]]=[];"ONLYINCOMPOUND"in this.flags&&(this.compoundRuleCodes[this.flags.ONLYINCOMPOUND]=[]),this.dictionaryTable=this._parseDIC(n);for(var a in this.compoundRuleCodes)0==this.compoundRuleCodes[a].length&&delete this.compoundRuleCodes[a];for(var a=0,l=this.compoundRules.length;l>a;a++){for(var f=this.compoundRules[a],h="",c=0,u=f.length;u>c;c++){var d=f[c];h+=d in this.compoundRuleCodes?"("+this.compoundRuleCodes[d].join("|")+")":d}this.compoundRules[a]=new RegExp(h,"i")}}return this};i.prototype={load:function(e){for(var t in e)this[t]=e[t];return this},_readFile:function(t,r){if(r||(r="utf8"),"undefined"!=typeof XMLHttpRequest){var i=new XMLHttpRequest;return i.open("GET",t,!1),i.overrideMimeType&&i.overrideMimeType("text/plain; charset="+r),i.send(null),i.responseText}if("undefined"!=typeof e){var o=e("fs");try{if(o.existsSync(t)){var a=o.statSync(t),l=o.openSync(t,"r"),s=new n(a.size);return o.readSync(l,s,0,s.length,null),s.toString(r,0,s.length)}console.log("Path "+t+" does not exist.")}catch(c){return console.log(c),""}}},_parseAFF:function(e){var t={};e=this._removeAffixComments(e);for(var n=e.split("\n"),r=0,i=n.length;i>r;r++){var o=n[r],a=o.split(/\s+/),l=a[0];if("PFX"==l||"SFX"==l){for(var s=a[1],c=a[2],u=parseInt(a[3],10),f=[],h=r+1,d=r+1+u;d>h;h++){var o=n[h],p=o.split(/\s+/),m=p[2],g=p[3].split("/"),v=g[0];"0"===v&&(v="");var y=this.parseRuleCodes(g[1]),x=p[4],b={};b.add=v,y.length>0&&(b.continuationClasses=y),"."!==x&&("SFX"===l?b.match=new RegExp(x+"$"):b.match=new RegExp("^"+x)),"0"!=m&&("SFX"===l?b.remove=new RegExp(m+"$"):b.remove=m),f.push(b)}t[s]={type:l,combineable:"Y"==c,entries:f},r+=u}else if("COMPOUNDRULE"===l){for(var u=parseInt(a[1],10),h=r+1,d=r+1+u;d>h;h++){var o=n[h],p=o.split(/\s+/);this.compoundRules.push(p[1])}r+=u}else if("REP"===l){var p=o.split(/\s+/);3===p.length&&this.replacementTable.push([p[1],p[2]])}else this.flags[l]=a[1]}return t},_removeAffixComments:function(e){return e=e.replace(/#.*$/gm,""),e=e.replace(/^\s\s*/m,"").replace(/\s\s*$/m,""),e=e.replace(/\n{2,}/g,"\n"),e=e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")},_parseDIC:function(e){function t(e,t){e in r&&"object"==typeof r[e]||(r[e]=[]),r[e].push(t)}e=this._removeDicComments(e);for(var n=e.split("\n"),r={},i=1,o=n.length;o>i;i++){var a=n[i],l=a.split("/",2),s=l[0];if(l.length>1){var c=this.parseRuleCodes(l[1]);"NEEDAFFIX"in this.flags&&-1!=c.indexOf(this.flags.NEEDAFFIX)||t(s,c);for(var u=0,f=c.length;f>u;u++){var h=c[u],d=this.rules[h];if(d)for(var p=this._applyRule(s,d),m=0,g=p.length;g>m;m++){var v=p[m];if(t(v,[]),d.combineable)for(var y=u+1;f>y;y++){var x=c[y],b=this.rules[x];if(b&&b.combineable&&d.type!=b.type)for(var w=this._applyRule(v,b),k=0,S=w.length;S>k;k++){var C=w[k];t(C,[])}}}h in this.compoundRuleCodes&&this.compoundRuleCodes[h].push(s)}}else t(s.trim(),[])}return r},_removeDicComments:function(e){return e=e.replace(/^\t.*$/gm,"")},parseRuleCodes:function(e){if(!e)return[];if(!("FLAG"in this.flags))return e.split("");if("long"===this.flags.FLAG){for(var t=[],n=0,r=e.length;r>n;n+=2)t.push(e.substr(n,2));return t}return"num"===this.flags.FLAG?textCode.split(","):void 0},_applyRule:function(e,t){for(var n=t.entries,r=[],i=0,o=n.length;o>i;i++){var a=n[i];if(!a.match||e.match(a.match)){var l=e;if(a.remove&&(l=l.replace(a.remove,"")),"SFX"===t.type?l+=a.add:l=a.add+l,r.push(l),"continuationClasses"in a)for(var s=0,c=a.continuationClasses.length;c>s;s++){var u=this.rules[a.continuationClasses[s]];u&&(r=r.concat(this._applyRule(l,u)))}}}return r},check:function(e){var t=e.replace(/^\s\s*/,"").replace(/\s\s*$/,"");if(this.checkExact(t))return!0;if(t.toUpperCase()===t){var n=t[0]+t.substring(1).toLowerCase();if(this.hasFlag(n,"KEEPCASE"))return!1;if(this.checkExact(n))return!0}var r=t.toLowerCase();if(r!==t){if(this.hasFlag(r,"KEEPCASE"))return!1;if(this.checkExact(r))return!0}return!1},checkExact:function(e){var t=this.dictionaryTable[e];if("undefined"==typeof t){if("COMPOUNDMIN"in this.flags&&e.length>=this.flags.COMPOUNDMIN)for(var n=0,r=this.compoundRules.length;r>n;n++)if(e.match(this.compoundRules[n]))return!0;return!1}if("object"==typeof t){for(var n=0,r=t.length;r>n;n++)if(!this.hasFlag(e,"ONLYINCOMPOUND",t[n]))return!0;return!1}},hasFlag:function(e,t,n){if(t in this.flags){if("undefined"==typeof n)var n=Array.prototype.concat.apply([],this.dictionaryTable[e]);if(n&&-1!==n.indexOf(this.flags[t]))return!0}return!1},alphabet:"",suggest:function(e,t){function n(e){for(var t=[],n=0,r=e.length;r>n;n++){for(var i=e[n],o=[],a=0,l=i.length+1;l>a;a++)o.push([i.substring(0,a),i.substring(a,i.length)]);for(var s=[],a=0,l=o.length;l>a;a++){var u=o[a];u[1]&&s.push(u[0]+u[1].substring(1))}for(var f=[],a=0,l=o.length;l>a;a++){var u=o[a];u[1].length>1&&f.push(u[0]+u[1][1]+u[1][0]+u[1].substring(2))}for(var h=[],a=0,l=o.length;l>a;a++){var u=o[a];if(u[1])for(var d=0,p=c.alphabet.length;p>d;d++)h.push(u[0]+c.alphabet[d]+u[1].substring(1))}for(var m=[],a=0,l=o.length;l>a;a++){var u=o[a];if(u[1])for(var d=0,p=c.alphabet.length;p>d;d++)h.push(u[0]+c.alphabet[d]+u[1])}t=t.concat(s),t=t.concat(f),t=t.concat(h),t=t.concat(m)}return t}function r(e){for(var t=[],n=0;nu;u++)l[u]in s?s[l[u]]+=1:s[l[u]]=1;var h=[];for(var u in s)h.push([u,s[u]]);h.sort(i).reverse();for(var d=[],u=0,f=Math.min(t,h.length);f>u;u++)c.hasFlag(h[u][0],"NOSUGGEST")||d.push(h[u][0]);return d}if(t||(t=5),this.check(e))return[];for(var o=0,a=this.replacementTable.length;a>o;o++){var l=this.replacementTable[o];if(-1!==e.indexOf(l[0])){var s=e.replace(l[0],l[1]);if(this.check(s))return[s]}}var c=this;return c.alphabet="abcdefghijklmnopqrstuvwxyz",i(e)}},"undefined"!=typeof t&&(t.exports=i)}).call(this,e("buffer").Buffer,"/node_modules/typo-js")},{buffer:3,fs:2}],19:[function(e,t,n){var r=e("codemirror");r.commands.tabAndIndentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentMore");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}},r.commands.shiftTabAndUnindentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentLess");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}}},{codemirror:10}],20:[function(e,t,n){"use strict";function r(e){return e=U?e.replace("Ctrl","Cmd"):e.replace("Cmd","Ctrl")}function i(e,t,n){e=e||{};var r=document.createElement("a");return t=void 0==t?!0:t,e.title&&t&&(r.title=a(e.title,e.action,n),U&&(r.title=r.title.replace("Ctrl","⌘"),r.title=r.title.replace("Alt","⌥"))),r.tabIndex=-1,r.className=e.className,r}function o(){var e=document.createElement("i");return e.className="separator",e.innerHTML="|",e}function a(e,t,n){var i,o=e;return t&&(i=Y(t),n[i]&&(o+=" ("+r(n[i])+")")),o}function l(e,t){t=t||e.getCursor("start");var n=e.getTokenAt(t);if(!n.type)return{};for(var r,i,o=n.type.split(" "),a={},l=0;l=0&&(d=c.getLineHandle(o),!t(d));o--);var v,y,x,b,w=c.getTokenAt({line:o,ch:1}),k=n(w).fencedChars;t(c.getLineHandle(u.line))?(v="",y=u.line):t(c.getLineHandle(u.line-1))?(v="",y=u.line-1):(v=k+"\n",y=u.line),t(c.getLineHandle(f.line))?(x="",b=f.line,0===f.ch&&(b+=1)):0!==f.ch&&t(c.getLineHandle(f.line+1))?(x="",b=f.line+1):(x=k+"\n",b=f.line+1),0===f.ch&&(b-=1),c.operation(function(){c.replaceRange(x,{line:b,ch:0},{line:b+(x?0:1),ch:0}),c.replaceRange(v,{line:y,ch:0},{line:y+(v?0:1),ch:0})}),c.setSelection({line:y+(v?1:0),ch:0},{line:b+(v?1:-1),ch:0}),c.focus()}else{var S=u.line;if(t(c.getLineHandle(u.line))&&("fenced"===r(c,u.line+1)?(o=u.line,S=u.line+1):(a=u.line,S=u.line-1)),void 0===o)for(o=S;o>=0&&(d=c.getLineHandle(o),!t(d));o--);if(void 0===a)for(l=c.lineCount(),a=S;l>a&&(d=c.getLineHandle(a),!t(d));a++);c.operation(function(){c.replaceRange("",{line:o,ch:0},{line:o+1,ch:0}),c.replaceRange("",{line:a-1,ch:0},{line:a,ch:0})}),c.focus()}else if("indented"===p){if(u.line!==f.line||u.ch!==f.ch)o=u.line,a=f.line,0===f.ch&&a--;else{for(o=u.line;o>=0;o--)if(d=c.getLineHandle(o),!d.text.match(/^\s*$/)&&"indented"!==r(c,o,d)){o+=1;break}for(l=c.lineCount(),a=u.line;l>a;a++)if(d=c.getLineHandle(a),!d.text.match(/^\s*$/)&&"indented"!==r(c,a,d)){a-=1;break}}var C=c.getLineHandle(a+1),L=C&&c.getTokenAt({line:a+1,ch:C.text.length-1}),T=L&&n(L).indentedCode;T&&c.replaceRange("\n",{line:a+1,ch:0});for(var M=o;a>=M;M++)c.indentLine(M,"subtract");c.focus()}else{var N=u.line===f.line&&u.ch===f.ch&&0===u.ch,A=u.line!==f.line;N||A?i(c,u,f,s):E(c,!1,["`","`"])}}function d(e){var t=e.codemirror;I(t,"quote")}function p(e){var t=e.codemirror;O(t,"smaller")}function m(e){var t=e.codemirror;O(t,"bigger")}function g(e){var t=e.codemirror;O(t,void 0,1)}function v(e){var t=e.codemirror;O(t,void 0,2)}function y(e){var t=e.codemirror;O(t,void 0,3)}function x(e){var t=e.codemirror;I(t,"unordered-list")}function b(e){var t=e.codemirror;I(t,"ordered-list")}function w(e){var t=e.codemirror;R(t)}function k(e){var t=e.codemirror,n=l(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.link),!i)?!1:void E(t,n.link,r.insertTexts.link,i)}function S(e){var t=e.codemirror,n=l(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.image),!i)?!1:void E(t,n.image,r.insertTexts.image,i)}function C(e){var t=e.codemirror,n=l(t),r=e.options;E(t,n.table,r.insertTexts.table)}function L(e){var t=e.codemirror,n=l(t),r=e.options;E(t,n.image,r.insertTexts.horizontalRule)}function T(e){var t=e.codemirror;t.undo(),t.focus()}function M(e){var t=e.codemirror;t.redo(),t.focus()}function N(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.nextSibling,i=e.toolbarElements["side-by-side"],o=!1;/editor-preview-active-side/.test(r.className)?(r.className=r.className.replace(/\s*editor-preview-active-side\s*/g,""),i.className=i.className.replace(/\s*active\s*/g,""),n.className=n.className.replace(/\s*CodeMirror-sided\s*/g," ")):(setTimeout(function(){t.getOption("fullScreen")||s(e),r.className+=" editor-preview-active-side"},1),i.className+=" active",n.className+=" CodeMirror-sided",o=!0);var a=n.lastChild;if(/editor-preview-active/.test(a.className)){a.className=a.className.replace(/\s*editor-preview-active\s*/g,"");var l=e.toolbarElements.preview,c=n.previousSibling;l.className=l.className.replace(/\s*active\s*/g,""),c.className=c.className.replace(/\s*disabled-for-preview*/g,"")}var u=function(){r.innerHTML=e.options.previewRender(e.value(),r)};t.sideBySideRenderingFunction||(t.sideBySideRenderingFunction=u),o?(r.innerHTML=e.options.previewRender(e.value(),r),t.on("update",t.sideBySideRenderingFunction)):t.off("update",t.sideBySideRenderingFunction),t.refresh()}function A(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.previousSibling,i=e.options.toolbar?e.toolbarElements.preview:!1,o=n.lastChild;o&&/editor-preview/.test(o.className)||(o=document.createElement("div"),o.className="editor-preview",n.appendChild(o)),/editor-preview-active/.test(o.className)?(o.className=o.className.replace(/\s*editor-preview-active\s*/g,""),i&&(i.className=i.className.replace(/\s*active\s*/g,""),r.className=r.className.replace(/\s*disabled-for-preview*/g,""))):(setTimeout(function(){o.className+=" editor-preview-active"},1),i&&(i.className+=" active",r.className+=" disabled-for-preview")),o.innerHTML=e.options.previewRender(e.value(),o);var a=t.getWrapperElement().nextSibling;/editor-preview-active-side/.test(a.className)&&N(e)}function E(e,t,n,r){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){var i,o=n[0],a=n[1],l=e.getCursor("start"),s=e.getCursor("end");r&&(a=a.replace("#url#",r)),t?(i=e.getLine(l.line),o=i.slice(0,l.ch),a=i.slice(l.ch),e.replaceRange(o+a,{line:l.line,ch:0})):(i=e.getSelection(),e.replaceSelection(o+i+a),l.ch+=o.length,l!==s&&(s.ch+=o.length)),e.setSelection(l,s),e.focus()}}function O(e,t,n){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var r=e.getCursor("start"),i=e.getCursor("end"),o=r.line;o<=i.line;o++)!function(r){var i=e.getLine(r),o=i.search(/[^#]/);i=void 0!==t?0>=o?"bigger"==t?"###### "+i:"# "+i:6==o&&"smaller"==t?i.substr(7):1==o&&"bigger"==t?i.substr(2):"bigger"==t?i.substr(1):"#"+i:1==n?0>=o?"# "+i:o==n?i.substr(o+1):"# "+i.substr(o+1):2==n?0>=o?"## "+i:o==n?i.substr(o+1):"## "+i.substr(o+1):0>=o?"### "+i:o==n?i.substr(o+1):"### "+i.substr(o+1),e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(o);e.focus()}}function I(e,t){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var n=l(e),r=e.getCursor("start"),i=e.getCursor("end"),o={quote:/^(\s*)\>\s+/,"unordered-list":/^(\s*)(\*|\-|\+)\s+/,"ordered-list":/^(\s*)\d+\.\s+/},a={quote:"> ","unordered-list":"* ","ordered-list":"1. "},s=r.line;s<=i.line;s++)!function(r){var i=e.getLine(r);i=n[t]?i.replace(o[t],"$1"):a[t]+i,e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(s);e.focus()}}function P(e,t,n,r){if(!/editor-preview-active/.test(e.codemirror.getWrapperElement().lastChild.className)){r="undefined"==typeof r?n:r;var i,o=e.codemirror,a=l(o),s=n,c=r,u=o.getCursor("start"),f=o.getCursor("end");a[t]?(i=o.getLine(u.line),s=i.slice(0,u.ch),c=i.slice(u.ch),"bold"==t?(s=s.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/,""),c=c.replace(/(\*\*|__)/,"")):"italic"==t?(s=s.replace(/(\*|_)(?![\s\S]*(\*|_))/,""),c=c.replace(/(\*|_)/,"")):"strikethrough"==t&&(s=s.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/,""),c=c.replace(/(\*\*|~~)/,"")),o.replaceRange(s+c,{line:u.line,ch:0},{line:u.line,ch:99999999999999}),"bold"==t||"strikethrough"==t?(u.ch-=2,u!==f&&(f.ch-=2)):"italic"==t&&(u.ch-=1,u!==f&&(f.ch-=1))):(i=o.getSelection(),"bold"==t?(i=i.split("**").join(""),i=i.split("__").join("")):"italic"==t?(i=i.split("*").join(""),i=i.split("_").join("")):"strikethrough"==t&&(i=i.split("~~").join("")),o.replaceSelection(s+i+c),u.ch+=n.length,f.ch=u.ch+i.length),o.setSelection(u,f),o.focus()}}function R(e){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className))for(var t,n=e.getCursor("start"),r=e.getCursor("end"),i=n.line;i<=r.line;i++)t=e.getLine(i),t=t.replace(/^[ ]*([# ]+|\*|\-|[> ]+|[0-9]+(.|\)))[ ]*/,""),e.replaceRange(t,{line:i,ch:0},{line:i,ch:99999999999999})}function D(e,t){for(var n in t)t.hasOwnProperty(n)&&(t[n]instanceof Array?e[n]=t[n].concat(e[n]instanceof Array?e[n]:[]):null!==t[n]&&"object"==typeof t[n]&&t[n].constructor===Object?e[n]=D(e[n]||{},t[n]):e[n]=t[n]);return e}function H(e){for(var t=1;t=19968?n[i].length:1;return r}function B(e){e=e||{},e.parent=this;var t=!0;if(e.autoDownloadFontAwesome===!1&&(t=!1),e.autoDownloadFontAwesome!==!0)for(var n=document.styleSheets,r=0;r-1&&(t=!1);if(t){var i=document.createElement("link");i.rel="stylesheet",i.href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css",document.getElementsByTagName("head")[0].appendChild(i)}if(e.element)this.element=e.element;else if(null===e.element)return void console.log("SimpleMDE: Error. No element was found.");if(void 0===e.toolbar){e.toolbar=[];for(var o in K)K.hasOwnProperty(o)&&(-1!=o.indexOf("separator-")&&e.toolbar.push("|"),(K[o]["default"]===!0||e.showIcons&&e.showIcons.constructor===Array&&-1!=e.showIcons.indexOf(o))&&e.toolbar.push(o))}e.hasOwnProperty("status")||(e.status=["autosave","lines","words","cursor"]),e.previewRender||(e.previewRender=function(e){return this.parent.markdown(e)}),e.parsingConfig=H({highlightFormatting:!0},e.parsingConfig||{}),e.insertTexts=H({},X,e.insertTexts||{}),e.promptTexts=Z,e.blockStyles=H({},J,e.blockStyles||{}),e.shortcuts=H({},G,e.shortcuts||{}),void 0!=e.autosave&&void 0!=e.autosave.unique_id&&""!=e.autosave.unique_id&&(e.autosave.uniqueId=e.autosave.unique_id),this.options=e,this.render(),!e.initialValue||this.options.autosave&&this.options.autosave.foundSavedValue===!0||this.value(e.initialValue)}function _(){if("object"!=typeof localStorage)return!1;try{localStorage.setItem("smde_localStorage",1),localStorage.removeItem("smde_localStorage")}catch(e){return!1}return!0}var F=e("codemirror");e("codemirror/addon/edit/continuelist.js"),e("./codemirror/tablist"),e("codemirror/addon/display/fullscreen.js"),e("codemirror/mode/markdown/markdown.js"),e("codemirror/addon/mode/overlay.js"),e("codemirror/addon/display/placeholder.js"),e("codemirror/addon/selection/mark-selection.js"),e("codemirror/mode/gfm/gfm.js"),e("codemirror/mode/xml/xml.js");var z=e("codemirror-spell-checker"),j=e("marked"),U=/Mac/.test(navigator.platform),q={toggleBold:c,toggleItalic:u,drawLink:k,toggleHeadingSmaller:p,toggleHeadingBigger:m,drawImage:S,toggleBlockquote:d,toggleOrderedList:b,toggleUnorderedList:x,toggleCodeBlock:h,togglePreview:A,toggleStrikethrough:f,toggleHeading1:g,toggleHeading2:v,toggleHeading3:y,cleanBlock:w,drawTable:C,drawHorizontalRule:L,undo:T,redo:M,toggleSideBySide:N,toggleFullScreen:s},G={toggleBold:"Cmd-B",toggleItalic:"Cmd-I",drawLink:"Cmd-K",toggleHeadingSmaller:"Cmd-H",toggleHeadingBigger:"Shift-Cmd-H",cleanBlock:"Cmd-E",drawImage:"Cmd-Alt-I",toggleBlockquote:"Cmd-'",toggleOrderedList:"Cmd-Alt-L",toggleUnorderedList:"Cmd-L",toggleCodeBlock:"Cmd-Alt-C",togglePreview:"Cmd-P",toggleSideBySide:"F9",toggleFullScreen:"F11"},Y=function(e){for(var t in q)if(q[t]===e)return t;return null},$=function(){var e=!1;return function(t){(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(t)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(t.substr(0,4)))&&(e=!0); +}(navigator.userAgent||navigator.vendor||window.opera),e},V="",K={bold:{name:"bold",action:c,className:"fa fa-bold",title:"Bold","default":!0},italic:{name:"italic",action:u,className:"fa fa-italic",title:"Italic","default":!0},strikethrough:{name:"strikethrough",action:f,className:"fa fa-strikethrough",title:"Strikethrough"},heading:{name:"heading",action:p,className:"fa fa-header",title:"Heading","default":!0},"heading-smaller":{name:"heading-smaller",action:p,className:"fa fa-header fa-header-x fa-header-smaller",title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:m,className:"fa fa-header fa-header-x fa-header-bigger",title:"Bigger Heading"},"heading-1":{name:"heading-1",action:g,className:"fa fa-header fa-header-x fa-header-1",title:"Big Heading"},"heading-2":{name:"heading-2",action:v,className:"fa fa-header fa-header-x fa-header-2",title:"Medium Heading"},"heading-3":{name:"heading-3",action:y,className:"fa fa-header fa-header-x fa-header-3",title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:h,className:"fa fa-code",title:"Code"},quote:{name:"quote",action:d,className:"fa fa-quote-left",title:"Quote","default":!0},"unordered-list":{name:"unordered-list",action:x,className:"fa fa-list-ul",title:"Generic List","default":!0},"ordered-list":{name:"ordered-list",action:b,className:"fa fa-list-ol",title:"Numbered List","default":!0},"clean-block":{name:"clean-block",action:w,className:"fa fa-eraser fa-clean-block",title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:k,className:"fa fa-link",title:"Create Link","default":!0},image:{name:"image",action:S,className:"fa fa-picture-o",title:"Insert Image","default":!0},table:{name:"table",action:C,className:"fa fa-table",title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:L,className:"fa fa-minus",title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:A,className:"fa fa-eye no-disable",title:"Toggle Preview","default":!0},"side-by-side":{name:"side-by-side",action:N,className:"fa fa-columns no-disable no-mobile",title:"Toggle Side by Side","default":!0},fullscreen:{name:"fullscreen",action:s,className:"fa fa-arrows-alt no-disable no-mobile",title:"Toggle Fullscreen","default":!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://simplemde.com/markdown-guide",className:"fa fa-question-circle",title:"Markdown Guide","default":!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:T,className:"fa fa-undo no-disable",title:"Undo"},redo:{name:"redo",action:M,className:"fa fa-repeat no-disable",title:"Redo"}},X={link:["[","](#url#)"],image:["![](","#url#)"],table:["","\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],horizontalRule:["","\n\n-----\n\n"]},Z={link:"URL for the link:",image:"URL of the image:"},J={bold:"**",code:"```",italic:"*"};B.prototype.markdown=function(e){if(j){var t={};return this.options&&this.options.renderingConfig&&this.options.renderingConfig.singleLineBreaks===!1?t.breaks=!1:t.breaks=!0,this.options&&this.options.renderingConfig&&this.options.renderingConfig.codeSyntaxHighlighting===!0&&window.hljs&&(t.highlight=function(e){return window.hljs.highlightAuto(e).value}),j.setOptions(t),j(e)}},B.prototype.render=function(e){if(e||(e=this.element||document.getElementsByTagName("textarea")[0]),!this._rendered||this._rendered!==e){this.element=e;var t=this.options,n=this,i={};for(var o in t.shortcuts)null!==t.shortcuts[o]&&null!==q[o]&&!function(e){i[r(t.shortcuts[e])]=function(){q[e](n)}}(o);i.Enter="newlineAndIndentContinueMarkdownList",i.Tab="tabAndIndentMarkdownList",i["Shift-Tab"]="shiftTabAndUnindentMarkdownList",i.Esc=function(e){e.getOption("fullScreen")&&s(n)},document.addEventListener("keydown",function(e){e=e||window.event,27==e.keyCode&&n.codemirror.getOption("fullScreen")&&s(n)},!1);var a,l;if(t.spellChecker!==!1?(a="spell-checker",l=t.parsingConfig,l.name="gfm",l.gitHubSpice=!1,z({codeMirrorInstance:F})):(a=t.parsingConfig,a.name="gfm",a.gitHubSpice=!1),this.codemirror=F.fromTextArea(e,{mode:a,backdrop:l,theme:"paper",tabSize:void 0!=t.tabSize?t.tabSize:2,indentUnit:void 0!=t.tabSize?t.tabSize:2,indentWithTabs:t.indentWithTabs!==!1,lineNumbers:!1,autofocus:t.autofocus===!0,extraKeys:i,lineWrapping:t.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:t.placeholder||e.getAttribute("placeholder")||"",styleSelectedText:void 0!=t.styleSelectedText?t.styleSelectedText:!0}),t.forceSync===!0){var c=this.codemirror;c.on("change",function(){c.save()})}this.gui={},t.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),t.status!==!1&&(this.gui.statusbar=this.createStatusbar()),void 0!=t.autosave&&t.autosave.enabled===!0&&this.autosave(),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element;var u=this.codemirror;setTimeout(function(){u.refresh()}.bind(u),0)}},B.prototype.autosave=function(){if(_()){var e=this;if(void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to use the autosave feature");null!=e.element.form&&void 0!=e.element.form&&e.element.form.addEventListener("submit",function(){localStorage.removeItem("smde_"+e.options.autosave.uniqueId)}),this.options.autosave.loaded!==!0&&("string"==typeof localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&""!=localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&(this.codemirror.setValue(localStorage.getItem("smde_"+this.options.autosave.uniqueId)),this.options.autosave.foundSavedValue=!0),this.options.autosave.loaded=!0),localStorage.setItem("smde_"+this.options.autosave.uniqueId,e.value());var t=document.getElementById("autosaved");if(null!=t&&void 0!=t&&""!=t){var n=new Date,r=n.getHours(),i=n.getMinutes(),o="am",a=r;a>=12&&(a=r-12,o="pm"),0==a&&(a=12),i=10>i?"0"+i:i,t.innerHTML="Autosaved: "+a+":"+i+" "+o}this.autosaveTimeoutId=setTimeout(function(){e.autosave()},this.options.autosave.delay||1e4)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},B.prototype.clearAutosavedValue=function(){if(_()){if(void 0==this.options.autosave||void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to clear the autosave value");localStorage.removeItem("smde_"+this.options.autosave.uniqueId)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},B.prototype.createSideBySide=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;n&&/editor-preview-side/.test(n.className)||(n=document.createElement("div"),n.className="editor-preview-side",t.parentNode.insertBefore(n,t.nextSibling));var r=!1,i=!1;return e.on("scroll",function(e){if(r)return void(r=!1);i=!0;var t=e.getScrollInfo().height-e.getScrollInfo().clientHeight,o=parseFloat(e.getScrollInfo().top)/t,a=(n.scrollHeight-n.clientHeight)*o;n.scrollTop=a}),n.onscroll=function(){if(i)return void(i=!1);r=!0;var t=n.scrollHeight-n.clientHeight,o=parseFloat(n.scrollTop)/t,a=(e.getScrollInfo().height-e.getScrollInfo().clientHeight)*o;e.scrollTo(0,a)},n},B.prototype.createToolbar=function(e){if(e=e||this.options.toolbar,e&&0!==e.length){var t;for(t=0;t - {{ field.label if label }} -
    - - - - - - - - - - - - - - - - - -
    - {{ field() }} - {% for error in field.errors %} - {{ error }} - {% endfor %} - + + {{ field.label if label }} + {{ field() }} + + {% for error in field.errors %} + {{ error }} + {% endfor %} {% endmacro %} diff --git a/app/utils/render.py b/app/utils/render.py index 262944f..ee98c05 100644 --- a/app/utils/render.py +++ b/app/utils/render.py @@ -17,19 +17,18 @@ def render(*args, styles=[], scripts=[], **kwargs): 'css/header.css', 'css/container.css', 'css/widgets.css', - 'css/editor.css', 'css/form.css', 'css/footer.css', 'css/flash.css', 'css/table.css', 'css/pagination.css', 'css/responsive.css', + 'css/simplemde.min.css', ] scripts_ = [ 'scripts/trigger_menu.js', 'scripts/pc-utils.js', 'scripts/smartphone_patch.js', - 'scripts/editor.js', ] for s in styles: From b5f1ea1d1d86bb30377dd8e9ecb5db27b1536ada Mon Sep 17 00:00:00 2001 From: Eragon Date: Fri, 18 Sep 2020 11:53:50 +0200 Subject: [PATCH 34/94] =?UTF-8?q?Un=20seul=20chargement=20du=20script=20m?= =?UTF-8?q?=C3=AAme=20avec=20plusieurs=20textarea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le script est chargé de manière standard, comme les autres scripts. L'init du script est lancé seulement après le chargement complet de toute les dépendances de la page. --- app/templates/widgets/editor.html | 29 ++++++++++++++++------------- app/utils/render.py | 1 + 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 882035c..9562ae4 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -1,19 +1,22 @@ {% macro text_editor(field, label=True) %} - {{ field.label if label }} {{ field() }} - + {% for error in field.errors %} {{ error }} {% endfor %} diff --git a/app/utils/render.py b/app/utils/render.py index ee98c05..f61b25a 100644 --- a/app/utils/render.py +++ b/app/utils/render.py @@ -29,6 +29,7 @@ def render(*args, styles=[], scripts=[], **kwargs): 'scripts/trigger_menu.js', 'scripts/pc-utils.js', 'scripts/smartphone_patch.js', + 'scripts/simplemde.min.js' ] for s in styles: From ed0f3b47f6192ddd1d300a524615f049d26836a3 Mon Sep 17 00:00:00 2001 From: Eragon Date: Fri, 18 Sep 2020 14:59:38 +0200 Subject: [PATCH 35/94] Meilleur gestion avec plusieurs forms dans une page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'éditeur markdown est lié à l'id du textarea qui l'invoque (Acte Vaudou) --- app/templates/widgets/editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 9562ae4..02d270c 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -4,7 +4,7 @@ {% for error in field.errors %} From e265e003b9e98199640552b349163bb09cf95cd7 Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 24 Sep 2020 00:22:20 +0200 Subject: [PATCH 41/94] admin: fixed the forum panel --- app/models/forum.py | 3 ++- app/templates/admin/forums.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/forum.py b/app/models/forum.py index d661b4b..0e07687 100644 --- a/app/models/forum.py +++ b/app/models/forum.py @@ -38,7 +38,8 @@ class Forum(db.Model): def post_count(self): """Number of posts in every topic of the forum, without subforums.""" - return sum(len(t.thread.comments) for t in self.topics) + # TODO: optimize this with real ORM + return sum(t.thread.comments.count() for t in self.topics) def __repr__(self): return f'' diff --git a/app/templates/admin/forums.html b/app/templates/admin/forums.html index 3d8848a..774f4e0 100644 --- a/app/templates/admin/forums.html +++ b/app/templates/admin/forums.html @@ -7,7 +7,7 @@
    {{ f.name }} - {{ f.topics | length }} + {{ f.topics.count() }} {{ f.post_count() }} From 67c3ebba42abcf5cc02926a7056a2259dc1b3667 Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 24 Sep 2020 22:43:39 +0200 Subject: [PATCH 42/94] editor: fix #73 --- app/templates/widgets/editor.html | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 03e8f00..7c641ad 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -37,8 +37,27 @@ shortcuts: { toggleFullScreen: null, }, + status: false, + }); + + // Ctrl+Enter submits form + ta = document.querySelector("div.CodeMirror"); + ta.addEventListener('keydown', function(e) { + var keyCode = e.keyCode || e.which; + if (e.ctrlKey && keyCode == 13) { + var e = e.target; + while(! (e instanceof HTMLFormElement)) { + e = e.parentNode; + } + try { + e.submit(); + } catch(exception) { + e.submit.click(); + } + } }); }); + {% for error in field.errors %} {{ error }} From 5253f8ec8f73011c6d5d29f9cfb32adb64cd52ae Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 24 Sep 2020 23:25:25 +0200 Subject: [PATCH 43/94] =?UTF-8?q?post=20edition:=20added=20redirection=20t?= =?UTF-8?q?o=20topic=20(#49)=20Beware=20that=20the=20urlparse=20method=20m?= =?UTF-8?q?ay=20return=20unsafe=20results=E2=80=A6=20IDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/posts/edit.py | 10 +++++++--- app/templates/forum/topic.html | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index 5b3c1b9..8824fb9 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -2,13 +2,18 @@ from app import app, db from app.models.post import Post from app.utils.render import render from app.forms.forum import CommentEditForm, AnonymousCommentEditForm -from flask import redirect, url_for, abort +from urllib.parse import urlparse +from flask import redirect, url_for, abort, request from flask_login import login_required, current_user @app.route('/post/', methods=['GET','POST']) # TODO: Allow guest edit of posts @login_required def edit_post(postid): + # TODO: Maybe not safe + referrer = urlparse(request.args.get('r', default = '/', type = str)).path + print(referrer) + p = Post.query.filter_by(id=postid).first_or_404() # TODO: Check whether privileged user has access to board @@ -25,8 +30,7 @@ def edit_post(postid): db.session.add(p) db.session.commit() - # TODO: Proper redirection - return redirect(url_for('index')) + return redirect(referrer) form.message.data = p.text return render('forum/edit_comment.html', comment=p, form=form) diff --git a/app/templates/forum/topic.html b/app/templates/forum/topic.html index 2aacaac..8a4d0f2 100644 --- a/app/templates/forum/topic.html +++ b/app/templates/forum/topic.html @@ -30,7 +30,7 @@ Posté le {{ c.date_created|dyndate }} {% endif %} | # - | Modifier + | Modifier | Supprimer From e308fca0ba0d7def5339885dbb2269bb3fb40cf9 Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 26 Sep 2020 11:56:07 +0200 Subject: [PATCH 44/94] templates: added a widget for threads It will be used for program comments, etc. --- app/templates/forum/topic.html | 49 ++++++++----------------------- app/templates/widgets/thread.html | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 app/templates/widgets/thread.html diff --git a/app/templates/forum/topic.html b/app/templates/forum/topic.html index 8a4d0f2..6dc54c1 100644 --- a/app/templates/forum/topic.html +++ b/app/templates/forum/topic.html @@ -1,8 +1,8 @@ {% extends "base/base.html" %} {% import "widgets/editor.html" as widget_editor %} +{% import "widgets/thread.html" as widget_thread %} {% import "widgets/user.html" as widget_user %} {% import "widgets/pagination.html" as widget_pagination with context %} -{% import "widgets/attachments.html" as widget_attachments %} {% block title %} Forum de Planète Casio » {{ t.forum.name }} »

    {{ t.title }}

    @@ -13,64 +13,39 @@

    {{ t.title }}

    - -
    {{ widget_user.profile(t.author ) }}{{ t.thread.top_comment.text|md }}
    + {{ t.thread.top_comment.text|md }} + {{ widget_pagination.paginate(comments, 'forum_topic', t, {'f': t.forum}) }} - - {% for c in comments.items %} - - {% if c != t.thread.top_comment %} - - - - {% endfor %} -
    {{ widget_user.profile(c.author) }} -
    {% if c.date_created != c.date_modified %} - Posté le {{ c.date_created|date }} (Modifié le {{ c.date_modified|date }}) - {% else %} - Posté le {{ c.date_created|dyndate }} - {% endif %} - | # - | Modifier - | Supprimer -
    - - {{ c.text|md }} - {{ widget_attachments.attachments(c) }} - {{ c.author.signature|md }} - {% elif loop.index0 != 0 %} -
    Ce message est le top comment
    - {% endif %} -
    + {{ widget_thread.thread(comments, t.thread.top_comment) }} {{ widget_pagination.paginate(comments, 'forum_topic', t, {'f': t.forum}) }} {% if current_user.is_authenticated or V5Config.ENABLE_GUEST_POST %}
    -

    Commenter le sujet

    +

    Commenter le sujet

    - {{ form.hidden_tag() }} + {{ form.hidden_tag() }} {% if form.pseudo %} {{ form.pseudo.label }} {{ form.pseudo }} {% for error in form.pseudo.errors %} - {{ error }} - {% endfor %} + {{ error }} + {% endfor %} {% endif %} - {{ widget_editor.text_editor(form.message, label=False) }} + {{ widget_editor.text_editor(form.message, label=False) }} {{ form.attachments }} {% for error in form.attachments.errors %} {{ error }} {% endfor %} -
    {{ form.submit(class_='bg-ok') }}
    -
    +
    {{ form.submit(class_='bg-ok') }}
    + {% endif %} -
    + {% endblock %} diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html new file mode 100644 index 0000000..871bd10 --- /dev/null +++ b/app/templates/widgets/thread.html @@ -0,0 +1,31 @@ +{% import "widgets/user.html" as widget_user %} +{% import "widgets/attachments.html" as widget_attachments %} + +{% macro thread(comments, top_comment, anchor) %} + +{% for c in comments.items %} + + {% if c != top_comment %} + + + +{% endfor %} +
    {{ widget_user.profile(c.author) }} +
    {% if c.date_created != c.date_modified %} + Posté le {{ c.date_created|date }} (Modifié le {{ c.date_modified|date }}) + {% else %} + Posté le {{ c.date_created|dyndate }} + {% endif %} + | # + | Modifier + | Supprimer +
    + + {{ c.text|md }} + {{ widget_attachments.attachments(c) }} + {{ c.author.signature|md }} + {% elif loop.index0 != 0 %} +
    Ce message est le top comment
    + {% endif %} +
    +{% endmacro %} From 8edeb052e27c4ac8dba0466b07b1492358270be5 Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 26 Sep 2020 12:31:17 +0200 Subject: [PATCH 45/94] templates: added
    separation between comment and signature --- app/templates/widgets/attachments.html | 1 + app/templates/widgets/thread.html | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/templates/widgets/attachments.html b/app/templates/widgets/attachments.html index c10ab09..7c11844 100644 --- a/app/templates/widgets/attachments.html +++ b/app/templates/widgets/attachments.html @@ -1,5 +1,6 @@ {% macro attachments(comment) %} {% if comment.attachments %} +
    Pièces-jointes
    diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index 871bd10..35fa537 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -17,9 +17,12 @@ | Modifier | Supprimer - + {{ c.text|md }} + {{ widget_attachments.attachments(c) }} + +
    {{ c.author.signature|md }} {% elif loop.index0 != 0 %}
    Ce message est le top comment
    From 0d8dd70956d82c437a0ac04ae7701a36ad49c649 Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 26 Sep 2020 14:48:30 +0200 Subject: [PATCH 46/94] security: add CSRF protection globally The decorator @check_csrf can be used to check CSRF protection when using GET links (like delete a post or logout) --- app/__init__.py | 3 +++ app/routes/account/login.py | 2 ++ app/templates/base/navbar/account.html | 2 +- app/utils/check_csrf.py | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 app/utils/check_csrf.py diff --git a/app/__init__.py b/app/__init__.py index aaf0713..969e8ed 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,6 +3,7 @@ from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager from flask_mail import Mail +from flask_wtf.csrf import CSRFProtect from config import Config app = Flask(__name__) @@ -15,11 +16,13 @@ if Config.SECRET_KEY == "a-random-secret-key": db = SQLAlchemy(app) migrate = Migrate(app, db) mail = Mail(app) +csrf = CSRFProtect(app) login = LoginManager(app) login.login_view = 'login' login.login_message = "Veuillez vous authentifier avant de continuer." + # Register converters (needed for routing) from app.utils.converters import * app.url_map.converters['forum'] = ForumConverter diff --git a/app/routes/account/login.py b/app/routes/account/login.py index 058219f..b00293b 100644 --- a/app/routes/account/login.py +++ b/app/routes/account/login.py @@ -7,6 +7,7 @@ from app.models.user import Member from app.models.priv import Group from app.utils.render import render from app.utils.send_mail import send_validation_mail +from app.utils.check_csrf import check_csrf import datetime @@ -68,6 +69,7 @@ def login(): @app.route('/deconnexion') @login_required +@check_csrf def logout(): logout_user() flash('Déconnexion réussie', 'info') diff --git a/app/templates/base/navbar/account.html b/app/templates/base/navbar/account.html index d34cea4..000e0a7 100644 --- a/app/templates/base/navbar/account.html +++ b/app/templates/base/navbar/account.html @@ -36,7 +36,7 @@ Paramètres - + Déconnexion diff --git a/app/utils/check_csrf.py b/app/utils/check_csrf.py new file mode 100644 index 0000000..971d572 --- /dev/null +++ b/app/utils/check_csrf.py @@ -0,0 +1,18 @@ +from functools import wraps +from flask import request, abort +from flask_wtf import csrf +from wtforms.validators import ValidationError +from app import app + +def check_csrf(func): + """ + Check csrf_token GET parameter + """ + @wraps(func) + def wrapped(*args, **kwargs): + try: + csrf.validate_csrf(request.args.get('csrf_token')) + except ValidationError: + abort(404) + return func(*args, **kwargs) + return wrapped From 3b188e3babeffd3ef050109e9fc79fdb78bc8970 Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 26 Sep 2020 14:55:55 +0200 Subject: [PATCH 47/94] posts: add deletion --- app/routes/posts/edit.py | 19 +++++++++++++++++++ app/templates/widgets/thread.html | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index 8824fb9..861ec1c 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -1,6 +1,7 @@ from app import app, db from app.models.post import Post from app.utils.render import render +from app.utils.check_csrf import check_csrf from app.forms.forum import CommentEditForm, AnonymousCommentEditForm from urllib.parse import urlparse from flask import redirect, url_for, abort, request @@ -36,3 +37,21 @@ def edit_post(postid): return render('forum/edit_comment.html', comment=p, form=form) else: abort(404) + +@app.route('/post/supprimer/', methods=['GET','POST']) +@login_required +@check_csrf +def delete_post(postid): + p = Post.query.filter_by(id=postid).first_or_404() + + # TODO: Check whether privileged user has access to board + if p.author != current_user and not current_user.priv("delete-posts"): + abort(403) + + for a in p.attachments: + db.session.delete(a) + db.session.commit() + + db.session.delete(p) + db.session.commit() + return redirect(request.referrer) diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index 35fa537..e0a0c57 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -15,7 +15,7 @@ {% endif %} | # | Modifier - | Supprimer + | Supprimer {{ c.text|md }} From 510b57c08c0ac71209ac2db3d68f51a512193982 Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 26 Sep 2020 15:17:55 +0200 Subject: [PATCH 48/94] posts: fix attachements deletion --- app/routes/posts/edit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routes/posts/edit.py b/app/routes/posts/edit.py index 861ec1c..f8de2aa 100644 --- a/app/routes/posts/edit.py +++ b/app/routes/posts/edit.py @@ -49,6 +49,7 @@ def delete_post(postid): abort(403) for a in p.attachments: + a.delete_file() db.session.delete(a) db.session.commit() From 227087ebbc9f184f7a38944dc88cb345e7ee603b Mon Sep 17 00:00:00 2001 From: Eragon Date: Thu, 1 Oct 2020 22:16:15 +0200 Subject: [PATCH 49/94] Issue #76 Refactoriser les messages flash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Javascript : Supression du javascript inutile, l'entièreté du bloc du flash. CSS : Alignement plus correct des notifications flash, fini le fixed, 15% place au margin auto. Ajout d'une marge pour éviter l'effet «bloc». Héritage des propriétés de couleur du texte et du fond de la classe supèrieure. Modification des templates : Les messages sont désormais chargés avant le contenu et après le header. --- app/static/css/flash.css | 28 ++++++++---------------- app/static/scripts/pc-utils.js | 39 ---------------------------------- app/templates/base/base.html | 3 +-- app/templates/base/flash.html | 4 ++-- 4 files changed, 12 insertions(+), 62 deletions(-) diff --git a/app/static/css/flash.css b/app/static/css/flash.css index c865bbe..7f08e3b 100644 --- a/app/static/css/flash.css +++ b/app/static/css/flash.css @@ -3,15 +3,14 @@ */ .flash { - position: fixed; left: 15%; - display: flex; align-items: center; - width: 70%; z-index: 10; - font-family: NotoSans; font-size: 14px; color: var(--text); - background: var(--background); + margin: 5px auto; + display: flex; + align-items: center; + width: 80%; + font-size: 14px; border-bottom: 5px solid var(--info); - border-radius: 1px; box-shadow: var(--shadow); - transition: opacity .15s ease; - transition: top .2s ease; + border-radius: 1px; + box-shadow: var(--shadow); } .flash.info { border-color: var(--info); @@ -26,18 +25,9 @@ border-color: var(--error); } .flash span { - flex-grow: 1; margin: 15px 10px 10px 0; + flex-grow: 1; + margin: 15px 10px 10px 0; } .flash svg { margin: 15px 20px 10px 30px; } - -.flash input[type="button"] { - margin: 3px 30px 0 0; padding: 10px 15px; - border: none; - background: var(--btn-bg); color: var(--btn-text); -} -.flash input[type="button"]:hover, -.flash input[type="button"]:focus { - background: var(--btn-bg-active); -} diff --git a/app/static/scripts/pc-utils.js b/app/static/scripts/pc-utils.js index 56434ec..9fb92c1 100644 --- a/app/static/scripts/pc-utils.js +++ b/app/static/scripts/pc-utils.js @@ -11,42 +11,3 @@ function getCookie(name) { if( end == -1 ) end = document.cookie.length; return unescape( document.cookie.substring( debut+name.length+1, end ) ); } - -/* - Flash messages - TODO: Find a way to have good flash messages in a KISS & DRY way -*/ -function flash_add(type, message) { - template = `
    - - {{ icon }} - - - {{ message }} - - -
    `; - paths = { - 'error': '', - 'warning': '', - 'ok': '', - 'info': '' - }; - var top = (document.getElementsByClassName('flash').length + 1) * 70 - 45; - template = template.replace("{{ category }}", type); - template = template.replace("{{ top }}", top); - template = template.replace("{{ icon }}", paths[type]); - template = template.replace("{{ message }}", message); - document.body.innerHTML += template; -} -function flash_close(element) { - element.style.opacity = 0; - setTimeout(function(){ - var parent = element.parentNode; - parent.removeChild(element); - var childs = parent.getElementsByClassName('flash'); - for(var i = 0; i < childs.length; i++) { - childs[i].style.top = ((i + 1) * 70 - 45) + 'px'; - } - }, 0); -} diff --git a/app/templates/base/base.html b/app/templates/base/base.html index 2cfa12e..92d8b1b 100644 --- a/app/templates/base/base.html +++ b/app/templates/base/base.html @@ -10,6 +10,7 @@
    {% block title %}

    Planète Casio

    {% endblock %}
    {% include "base/header.html" %} + {% include "base/flash.html" %} {% block content %} {% endblock %} @@ -17,8 +18,6 @@ {% include "base/footer.html" %} - {% include "base/flash.html" %} - {% include "base/scripts.html" %} diff --git a/app/templates/base/flash.html b/app/templates/base/flash.html index 795fd74..13bc864 100644 --- a/app/templates/base/flash.html +++ b/app/templates/base/flash.html @@ -1,7 +1,7 @@ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} -
    +
    {% if category=="error" %}{% endif %} {% if category=="warning" %}{% endif %} @@ -11,8 +11,8 @@ {{ message }} -
    {% endfor %} {% endif %} {% endwith %} + From ed231f652469189aafadea7c9d600d2a3a851a94 Mon Sep 17 00:00:00 2001 From: Darks Date: Wed, 7 Oct 2020 21:47:21 +0200 Subject: [PATCH 50/94] fix CSRF token for message preview (#77) --- app/templates/widgets/editor.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index 7c641ad..c4dc0a4 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -24,7 +24,10 @@ data = {text: plainText}; fetch('{{ url_for("api_markdown") }}', { method: "POST", - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() }}' + }, body: JSON.stringify(data) }) .then(response => response.text()) From e06363715e1fc92dffe8d2961cb75567f28aac46 Mon Sep 17 00:00:00 2001 From: Lephe Date: Fri, 30 Oct 2020 09:36:21 +0100 Subject: [PATCH 51/94] forum: match topic title length in creation form with model Model has titles of up to 128 characters, but the form limits that input to 32 characters. Bump that to 128 too. --- app/forms/forum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/forms/forum.py b/app/forms/forum.py index 0648fbf..a6aac90 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -26,7 +26,7 @@ class AnonymousCommentEditForm(CommentEditForm, AnonymousCommentForm): class TopicCreationForm(CommentForm): title = StringField('Nom du sujet', - validators=[InputRequired(), Length(min=3, max=32)]) + validators=[InputRequired(), Length(min=3, max=128)]) submit = SubmitField('Créer le sujet') From a95a88f3b1ed236ea1ac8cfb6072ba1879495819 Mon Sep 17 00:00:00 2001 From: Lephe Date: Fri, 30 Oct 2020 14:05:34 +0100 Subject: [PATCH 52/94] app: send cookies with Secure and SameSite=Lax (#60) Sending cookies without Secure and without SameSite causes Firefox to ignore or invalidate them, which disconnects accounts seemingly randomly. --- app/static/scripts/pc-utils.js | 2 +- config.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/static/scripts/pc-utils.js b/app/static/scripts/pc-utils.js index 9fb92c1..16c7619 100644 --- a/app/static/scripts/pc-utils.js +++ b/app/static/scripts/pc-utils.js @@ -1,7 +1,7 @@ function setCookie(name, value) { var end = new Date(); end.setTime( end.getTime() + 3600 * 1000 ); - var str=name+"="+escape(value)+"; expires="+end.toGMTString()+"; path=/"; + var str=name+"="+escape(value)+"; expires="+end.toGMTString()+"; path=/; Secure; SameSite=lax"; document.cookie = str; } function getCookie(name) { diff --git a/config.py b/config.py index be5e06f..f4cd896 100644 --- a/config.py +++ b/config.py @@ -18,6 +18,12 @@ class Config(object): MAIL_DEFAULT_SENDER = "noreply@v5.planet-casio.com" MAIL_SUPPRESS_SEND = None + # Only send cookies over HTTPS connections (use only if HTTPS is enabled) + SESSION_COOKIE_SECURE = True + # Only send cookies in requests, do not expose them to Javascript + SESSION_COOKIE_HTTPONLY = True + # Do not attach cookies to cross-origin requests + SESSION_COOKIE_SAMESITE = "Lax" class DefaultConfig(object): """Every value here can be overrided in the local_config.py class""" From 93e5fc380a3a02446f2d57c5a42d4b91b0e145a5 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 31 Oct 2020 09:51:00 +0100 Subject: [PATCH 53/94] admin: improve display of trophies --- app/templates/admin/edit_trophy.html | 5 +++++ app/templates/admin/trophies.html | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/templates/admin/edit_trophy.html b/app/templates/admin/edit_trophy.html index d56f7a0..ec4f375 100644 --- a/app/templates/admin/edit_trophy.html +++ b/app/templates/admin/edit_trophy.html @@ -10,6 +10,11 @@ {{ form.hidden_tag() }}

    Éditer le trophée

    +
    + + {{ trophy.name }} +
    +
    {{ form.name.label }} {{ form.name(value=trophy.name) }} diff --git a/app/templates/admin/trophies.html b/app/templates/admin/trophies.html index 88b06c7..c6f2349 100644 --- a/app/templates/admin/trophies.html +++ b/app/templates/admin/trophies.html @@ -13,17 +13,17 @@

    Titres et trophées

    - + {% for trophy in trophies %} - - + {% if trophy | is_title %} - + {% else %} - + {% endif %} From ee01a4c768c00e99b54aca9e49f7c6bc7b37a3df Mon Sep 17 00:00:00 2001 From: Darks Date: Sat, 31 Oct 2020 10:15:07 +0100 Subject: [PATCH 54/94] master.py: split generation of trophies and generation of icons May be used in development environment when database is populated and deleting/recreating trophies throws an error because it will broke non-null relationships. --- master.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/master.py b/master.py index dce4592..0261b04 100755 --- a/master.py +++ b/master.py @@ -186,8 +186,15 @@ def create_trophies(): db.session.commit() print(f"Created {len(tr)} trophies.") + # Create their icons + create_trophies_icons() + + +def create_trophies_icons(): + tr = [] + with open(os.path.join(app.root_path, "data", "trophies.yaml")) as fp: + tr = yaml.safe_load(fp.read()) - # Create their icons in /app/static/images/trophies names = [slugify.slugify(t["name"]) for t in tr] src = os.path.join(app.root_path, "data", "trophies.png") dst = os.path.join(app.root_path, "static", "images", "trophies") @@ -210,6 +217,7 @@ def create_trophies(): for (name, icon) in zip(names, trophy_iterator(img)): icon.save(os.path.join(dst, f"{name}.png")) + def create_forums(): # Clean up forums forums("clear") @@ -285,6 +293,7 @@ commands = { "forums": forums, "create-groups-and-privs": create_groups_and_privs, "create-trophies": create_trophies, + "create-trophies-icons": create_trophies_icons, "create-forums": create_forums, "add-group": add_group, "enable-user": enable_user, From 05c16b8fa81efa3efcf9fc1225bc5d9f9e1864ad Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 31 Oct 2020 10:11:07 +0100 Subject: [PATCH 55/94] threads: hide signature delimiter if there is no signature --- app/templates/widgets/thread.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index e0a0c57..b5cf1e1 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -22,8 +22,10 @@ {{ widget_attachments.attachments(c) }} -
    - {{ c.author.signature|md }} + {% if c.author.signature != "" %} +
    + {{ c.author.signature|md }} + {% endif %} {% elif loop.index0 != 0 %}
    Ce message est le top comment
    {% endif %} From c7846c4f8b3e5c9b75a2ec7ee1680c3d052796b7 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 31 Oct 2020 15:15:44 +0100 Subject: [PATCH 56/94] threads: improve layout and flow of messages * When there is enough space, put links and date on the right * On small screens, compact profile information * On small screens, leave links and date on the right to maximize message width --- app/static/css/responsive.css | 8 +++---- app/static/css/table.css | 35 ++++++++++++++++++++++++++++--- app/static/css/widgets.css | 19 +++++++++++++++++ app/templates/widgets/thread.html | 16 +++++++------- app/templates/widgets/user.html | 2 +- 5 files changed, 64 insertions(+), 16 deletions(-) diff --git a/app/static/css/responsive.css b/app/static/css/responsive.css index 3886763..5aad72f 100644 --- a/app/static/css/responsive.css +++ b/app/static/css/responsive.css @@ -13,6 +13,10 @@ #menu a { font-size: 13px; } + + section { + width: 90%; + } } @media all and (min-width: 1400px) { @@ -33,10 +37,6 @@ .home-pinned-content article:nth-child(5) { display: none; } - - section { - width: 90%; - } } @media screen and (max-width: 849px) { diff --git a/app/static/css/table.css b/app/static/css/table.css index 54d4aae..7b99124 100644 --- a/app/static/css/table.css +++ b/app/static/css/table.css @@ -70,13 +70,42 @@ table.topiclist tr > td:last-child { table.thread { width: 100%; + border-width: 1px 0; +} +table.thread.topcomment { + border: none; } table.thread td.author { - width: 20%; + width: 256px; } table.thread td { - vertical-align: top; + vertical-align: top; } table.thread td:nth-child(2) { - padding-top: 10px; + padding-top: 0; + padding-bottom: 0; +} + +table.thread div.info { + float: right; + text-align: right; + opacity: 0.7; + padding-top: 8px; + margin-left: 16px; +} +@media screen and (max-width: 1199px) { + table.thread div.info { + float: none; + display: flex; + flex-direction: row; + margin-left: 0; + } + table.thread div.info > *:not(:last-child):after { + content: '·'; + margin: 0 4px; + } + table.thread td.author { + /* Includes padding */ + width: 136px; + } } diff --git a/app/static/css/widgets.css b/app/static/css/widgets.css index 6e7f6c8..a788e11 100644 --- a/app/static/css/widgets.css +++ b/app/static/css/widgets.css @@ -56,6 +56,25 @@ margin-bottom: 8px; } +@media screen and (max-width: 1199px) { + table.thread .profile { + flex-direction: column; + width: 128px; + } + table.thread .profile-avatar { + order: 1; + margin-right: 0; + } + table.thread .profile-title, + table.thread .profile-points, + table.thread .profile-xp { + display: none; + } + table.thread .profile-points-small { + display: inline; + } +} + /* Trophies */ .trophies { display: flex; flex-wrap: wrap; justify-content: space-between; diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index b5cf1e1..d34c793 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -8,14 +8,14 @@ {% if c != top_comment %}
    IDIcôneNomTitre
    NomTitre StyleModifierSupprimer
    {{ trophy.id }}{{ trophy.name }}
    + {{ trophy.name }} {{ trophy.name }}OuiOuiNonNon{{ trophy.css }} Modifier{{ widget_user.profile(c.author) }} -
    {% if c.date_created != c.date_modified %} - Posté le {{ c.date_created|date }} (Modifié le {{ c.date_modified|date }}) - {% else %} - Posté le {{ c.date_created|dyndate }} - {% endif %} - | # - | Modifier - | Supprimer +
    +
    Posté le {{ c.date_created|date }}
    + {% if c.date_created != c.date_modified %} +
    Modifié le {{ c.date_modified|date }}
    + {% endif %} + + +
    {{ c.text|md }} diff --git a/app/templates/widgets/user.html b/app/templates/widgets/user.html index 0b50e23..659927f 100644 --- a/app/templates/widgets/user.html +++ b/app/templates/widgets/user.html @@ -10,7 +10,7 @@
    Membre
    {% endif %}
    Niveau {{ user.level[0] }} ({{ user.xp }})
    -
    N{{ user.level[0] }} ({{ user.xp }})
    +
    Niv. {{ user.level[0] }}
    {% if user.level[0] <= 100 %}
    {% else %} From 36ccb165366849b41caf8b24a9b29b3247a11278 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 31 Oct 2020 15:33:29 +0100 Subject: [PATCH 57/94] thread: use the thread widget for top comments This allows the top comment to be permalinked and edited as any comment, following the intent of pinning normal comments to the top. --- app/templates/forum/topic.html | 7 ++----- app/templates/widgets/thread.html | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/templates/forum/topic.html b/app/templates/forum/topic.html index 6dc54c1..3c48fda 100644 --- a/app/templates/forum/topic.html +++ b/app/templates/forum/topic.html @@ -11,14 +11,11 @@ {% block content %}

    {{ t.title }}

    - - - -
    {{ widget_user.profile(t.author ) }}{{ t.thread.top_comment.text|md }}
    + {{ widget_thread.thread([t.thread.top_comment], None) }} {{ widget_pagination.paginate(comments, 'forum_topic', t, {'f': t.forum}) }} - {{ widget_thread.thread(comments, t.thread.top_comment) }} + {{ widget_thread.thread(comments.items, t.thread.top_comment) }} {{ widget_pagination.paginate(comments, 'forum_topic', t, {'f': t.forum}) }} diff --git a/app/templates/widgets/thread.html b/app/templates/widgets/thread.html index d34c793..f46817d 100644 --- a/app/templates/widgets/thread.html +++ b/app/templates/widgets/thread.html @@ -1,9 +1,9 @@ {% import "widgets/user.html" as widget_user %} {% import "widgets/attachments.html" as widget_attachments %} -{% macro thread(comments, top_comment, anchor) %} - -{% for c in comments.items %} +{% macro thread(comments, top_comment) %} +
    +{% for c in comments %} {% if c != top_comment %} From bb6450bda24ae993152f20b645250267ee2997ce Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 31 Oct 2020 22:15:03 +0100 Subject: [PATCH 58/94] editor: don't autofocus by default Autofocus prompts the browser to scroll to the editor when the page loads, which makes little to no sense on topic pages where the important information is at the top. --- app/templates/forum/edit_comment.html | 2 +- app/templates/widgets/editor.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/templates/forum/edit_comment.html b/app/templates/forum/edit_comment.html index 18ba28a..de4d7af 100644 --- a/app/templates/forum/edit_comment.html +++ b/app/templates/forum/edit_comment.html @@ -31,7 +31,7 @@ {% endfor %} {% endif %} - {{ widget_editor.text_editor(form.message, label=False) }} + {{ widget_editor.text_editor(form.message, label=False, autofocus=True) }}
    {{ form.submit(class_='bg-ok') }}
    diff --git a/app/templates/widgets/editor.html b/app/templates/widgets/editor.html index c4dc0a4..5ac3706 100644 --- a/app/templates/widgets/editor.html +++ b/app/templates/widgets/editor.html @@ -1,11 +1,11 @@ -{% macro text_editor(field, label=True) %} +{% macro text_editor(field, label=True, autofocus=false) %} {{ field.label if label }} {{ field() }}
    {{ widget_user.profile(c.author) }}