diff --git a/README.md b/README.md index 93a49e0..5ccc114 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ via plain text files. - [x] HTML files (.html) - [x] Markdown Files (.md) - [x] Infinite-scroll blog page +- [x] Search page +- [x] Full-text search +- [x] Preview panel - [x] Archive page - [x] Months as headings - [x] Links to scrolling blog page diff --git a/src/app.py b/src/app.py index 9f51654..b397dfb 100644 --- a/src/app.py +++ b/src/app.py @@ -1,10 +1,9 @@ -from flask import Flask, make_response, render_template, abort +from flask import Flask, abort, make_response, render_template, request +from flask_font_awesome import FontAwesome -import content as con_gen import config - - -app = Flask(__name__) +import content as con_gen +from forms import SearchForm, register_csrf TITLE = config.TITLE STITLE = config.STITLE @@ -14,47 +13,105 @@ DESCRIPTION = config.DESCRIPTION WEBSITE = config.WEBSITE MAIL = config.MAIL +app = Flask(__name__) +register_csrf(app) +font_awesome = FontAwesome(app) + + @app.errorhandler(404) def page_not_found(e): - return render_template("error.html", title=TITLE, stitle=STITLE, errorcode="404", style=STYLE, language=LANGUAGE), 404 + return render_template("error.html", + title=TITLE, + stitle=STITLE, + errorcode="404", + style=STYLE, + language=LANGUAGE), 404 @app.route("/") @app.route("/index.html") def index(): - content = con_gen.gen_index_string() - return render_template("index.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE) + content = con_gen.gen_index_string() + return render_template("index.html", + title=TITLE, + stitle=STITLE, + content_string=content, + style=STYLE, + language=LANGUAGE) + + +@app.route("/search", methods=["GET", "POST"]) +@app.route("/search.html", methods=["GET", "POST"]) +def search(): + form = SearchForm() + if request.method == "POST": + query_str = request.form["query_str"] + content = con_gen.gen_query_res_string(query_str) + return render_template("search.html", + title=TITLE, + stitle=STITLE, + style=STYLE, + form=form, + content=content, + language=LANGUAGE), 200 + return render_template("search.html", + title=TITLE, + stitle=STITLE, + style=STYLE, + form=form, + content="", + language=LANGUAGE), 200 + @app.route("/imprint") @app.route("/imprint.html") def imprint(): - return render_template("imprint.html", title=TITLE, stitle=STITLE, mail=MAIL, style=STYLE, language=LANGUAGE) + return render_template("imprint.html", + title=TITLE, + stitle=STITLE, + mail=MAIL, + style=STYLE, + language=LANGUAGE) + @app.route("/archive") @app.route("/archive.html") def archive(): - content = con_gen.gen_arch_string() - return render_template("archive.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE) + content = con_gen.gen_arch_string() + return render_template("archive.html", + title=TITLE, + stitle=STITLE, + content_string=content, + style=STYLE, + language=LANGUAGE) @app.route("/entry/") def entry(path): - content = con_gen.gen_stand_string(path) - if content != "": - return render_template("standalone.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE) - abort(404) + content = con_gen.gen_stand_string(path) + if content != "": + return render_template("standalone.html", + title=TITLE, + stitle=STITLE, + content_string=content, + style=STYLE, + language=LANGUAGE) + abort(404) @app.route("/feed.xml") @app.route("/rss.xml") def feed(): - content = con_gen.get_rss_string() - rss_xml = render_template("rss.xml", content_string=content, title=TITLE, - description=DESCRIPTION, website=WEBSITE) - response = make_response(rss_xml) - response.headers["Content-Type"] = "application/rss+xml" - return response + content = con_gen.get_rss_string() + rss_xml = render_template("rss.xml", + content_string=content, + title=TITLE, + description=DESCRIPTION, + website=WEBSITE) + response = make_response(rss_xml) + response.headers["Content-Type"] = "application/rss+xml" + return response if __name__ == "__main__": - app.run(host="0.0.0.0") + app.run(host="0.0.0.0") diff --git a/src/config.py b/src/config.py index 055835d..274cda3 100644 --- a/src/config.py +++ b/src/config.py @@ -18,3 +18,6 @@ LANGUAGE = "en-us" # Mail address for the imprint MAIL = "dummy@mail.com" + +# Directory to store entries in +ENTRY_DIR = "templates/entry" diff --git a/src/content.py b/src/content.py index af229a7..3e41206 100644 --- a/src/content.py +++ b/src/content.py @@ -4,17 +4,17 @@ import pathlib from datetime import datetime from os import path -import config import markdown -ENTRY_DIR = "templates/entry" +import config +import search + +ENTRY_DIR = config.ENTRY_DIR LANGUAGE = config.LANGUAGE LOCAL = "de_DE.UTF-8" if LANGUAGE == "de-de" else "en_US.UTF-8" locale.setlocale(locale.LC_TIME, LOCAL) -standalone_str = "Artikel" if LANGUAGE == "de-de" else "standalone" - def gen_arch_string(): """ @@ -70,7 +70,7 @@ def gen_index_string(): contents = sorted(full_list, key=os.path.getmtime) for file in reversed(contents): filename = pathlib.PurePath(file) - purefile = filename + # purefile = filename title = open(filename).readline().rstrip("\n") text = open(filename).readlines()[1:] filename = filename.name @@ -161,11 +161,11 @@ def get_rss_string(): string: rss-string of everything that is in the ENTRY_DIR. """ path_ex = ENTRY_DIR + content_string = "" if path.exists(path_ex): name_list = os.listdir(path_ex) full_list = [os.path.join(path_ex, i) for i in name_list] contents = sorted(full_list, key=os.path.getmtime) - content_string = "" for file in reversed(contents): filename = pathlib.PurePath(file) title = open(filename).readline().rstrip("\n") @@ -186,3 +186,66 @@ def get_rss_string(): content_string += "\n" content_string += "\n" return content_string + + +def gen_query_res_string(query_str): + """ + Return the results of a query. + + Parameters: + query_str (string): term to search + + Returns: + string: html-formated search result + """ + src_results = search.search(query_str) + res_string = "" + for result in src_results: + title = result["title"] + path = result["path"] + filename = pathlib.PurePath(path) + filename = filename.name + if filename[0] != ".": + filename = filename.split(".", 1)[0] + curr_date = datetime.fromtimestamp(os.path.getmtime(path)).strftime("%Y-%m-%d") + is_markdown = path.endswith(".md") + preview = create_preview(path, is_markdown) + path = "/entry/" + path.split("/", 2)[2] + res_string += "
" + res_string += "

" + title + "

" + res_string += "" + res_string += "" + curr_date + "" + res_string += "

" + res_string += preview + "
" + return res_string + + +def create_preview(path, is_markdown): + """ + Create a preview of a given article and return it. + + Parameters: + path (string): path to the article + + Returns: + string: html-formated preview + """ + file = open(path, "r", encoding="utf-8") + first_lines = file.readlines() + preview = "" + preview_length = 3 + for i, line in enumerate(first_lines): + if i == 0: + continue + if i > preview_length: + break + if not line.isspace(): + if is_markdown: + preview += markdown.markdown(line) + else: + preview += line + else: + preview_length += 1 + preview += "
...
" + return preview diff --git a/src/forms.py b/src/forms.py new file mode 100644 index 0000000..b975e89 --- /dev/null +++ b/src/forms.py @@ -0,0 +1,16 @@ +import os + +from flask_wtf import CSRFProtect, FlaskForm +from wtforms import StringField, SubmitField, ValidationError, validators + + +def register_csrf(app): + csrf = CSRFProtect() + SECRET_KEY = os.urandom(32) + app.secret_key = SECRET_KEY + csrf.init_app(app) + + +class SearchForm(FlaskForm): + query_str = StringField("Query", [validators.DataRequired("Please enter the search term")]) + # submit = SubmitField("Search") diff --git a/src/requirements.txt b/src/requirements.txt index 71e3541..e06d658 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,2 +1,7 @@ Flask Markdown +Whoosh +WTForms +Flask_WTF +MarkupSafe +Font-Awesome-Flask diff --git a/src/search.py b/src/search.py new file mode 100644 index 0000000..18d4af1 --- /dev/null +++ b/src/search.py @@ -0,0 +1,72 @@ +import os + +from whoosh import scoring +from whoosh.fields import ID, TEXT, Schema +from whoosh.index import create_in, open_dir +from whoosh.qparser import QueryParser + +import config + +INDEX_DIR = "indexdir" +DEF_TOPN = 10 +ENTRY_DIR = config.ENTRY_DIR + + +def createSearchableData(root): + """ + + Schema definition: title(name of file), path(as ID), content(indexed but not stored), textdata (stored text content) + source: + https://appliedmachinelearning.blog/2018/07/31/developing-a-fast-indexing-and-full-text-search-engine-with-whoosh-a-pure-pythhon-library/ + """ + schema = Schema(title=TEXT(stored=True), path=ID(stored=True), content=TEXT) + if not os.path.exists(INDEX_DIR): + os.mkdir(INDEX_DIR) + ix = create_in(INDEX_DIR, schema) + writer = ix.writer() + for r, _, f in os.walk(root): + for file in f: + path = os.path.join(r, file) + fp = open(path, encoding="utf-8") + title = fp.readline() + text = title + fp.read() + writer.add_document(title=title, path=path, content=text) + fp.close() + writer.commit() + + +def search_times(query_str, topN): + """ + Search for a given term and returns a specific amount of results. + + Parameters: + query_str (string): term to search for + topN (int): number of results to return + + Returns: + string: html-formatted string including the hits of the search + """ + ix = open_dir(INDEX_DIR) + results = [] + with ix.searcher(weighting=scoring.BM25F) as s: + query = QueryParser("content", ix.schema).parse(query_str) + matches = s.search(query, limit=topN) + for match in matches: + results.append({"title": match["title"], "path": match["path"], "match": match.score}) + return results + + +def search(query_str): + """ + Search for a given term and show the predefined amount of results. + + Parameters: + query_str (string): term to search for + + Returns: + string: html-formatted string including the hits of the search + """ + return search_times(query_str, DEF_TOPN) + + +createSearchableData(ENTRY_DIR) diff --git a/src/static/css/dark.css b/src/static/css/dark.css index 7bc3a58..3b2752d 100644 --- a/src/static/css/dark.css +++ b/src/static/css/dark.css @@ -3,6 +3,7 @@ :root { --bg0: rgb(29,32,33); --color0: rgb(220,120,0); + --color1: rgb(280,180,0); --error: rgb(255,0,0); --footerbg0: rgb(29,32,33); --link0: rgb(220, 120, 0); @@ -83,3 +84,13 @@ span { .entry h2 { color: var(--text1); } + + +form.search button[type=submit] { + background: var(--color0); + color: var(--bg0); +} + +form.search button[type=submit]:hover { + background: var(--color1); +} diff --git a/src/static/css/light.css b/src/static/css/light.css index 43feba9..b06d237 100644 --- a/src/static/css/light.css +++ b/src/static/css/light.css @@ -3,6 +3,7 @@ :root { --bg0: rgb(255,255,255); --color0: rgb(0,0,120); + --color1: rgb(0,0,200); --error: rgb(255,0,0); --footerbg0: rgb(192,192,192); --link0: rgb(0,0,120); @@ -91,3 +92,12 @@ span { .entry h2 { color: var(--text1); } + +form.search button[type=submit] { + background: var(--color0); + color: var(--bg0); +} + +form.search button[type=submit]:hover { + background: var(--color1); +} diff --git a/src/static/css/style.css b/src/static/css/style.css index c232bd8..31aca74 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -157,6 +157,10 @@ footer .center { } } +form { + margin-bottom: 40px; +} + .entry { border-radius: 0 10px 30px 0; margin-bottom: 20px; @@ -186,6 +190,15 @@ h3 { padding-bottom: 0; } +.blogarchive h1:first-child { + padding-bottom: 0; +} + +.blogarchive h2:first-child { + padding-top: 0; + padding-bottom: 0; +} + .entry ul { padding-left: 20; } @@ -216,3 +229,28 @@ code { white-space: pre; display: inline-block } + +form.search input[type=text] { + padding: 10px; + font-size: 17px; + border: 1px solid grey; + float: left; + width: 50%; + background: #f1f1f1; +} + +form.search button[type=submit] { + float: left; + width: 5%; + padding: 12px; + font-size: 17px; + border: 1px solid grey; + border-left: none; /* Prevent double borders */ + cursor: pointer; +} + +form.search::after { + content: ""; + clear: both; + display: table; +} diff --git a/src/templates/archive.html b/src/templates/archive.html index c0b6bd8..d0869dc 100644 --- a/src/templates/archive.html +++ b/src/templates/archive.html @@ -1,11 +1,11 @@ {% extends "template.html" %} {% block content %}
-
-

{% if language=="de-de" %}Archiv{% else %}Archive{% endif %}


- {% autoescape off %} - {{ content_string }} - {% endautoescape %} -
+
+

{% if language=="de-de" %}Archiv{% else %}Archive{% endif %}


+ {% autoescape off %} + {{ content_string }} + {% endautoescape %} +
{% endblock %} diff --git a/src/templates/error.html b/src/templates/error.html index f03923e..51b8cbf 100644 --- a/src/templates/error.html +++ b/src/templates/error.html @@ -1,9 +1,9 @@ {% extends "template.html" %} {% block content %}
-
- {% if language=="de-de" %}Fehler{% else %}Error{% endif %}
- {{ errorcode }} -
+
+ {% if language=="de-de" %}Fehler{% else %}Error{% endif %}
+ {{ errorcode }} +
{% endblock %} diff --git a/src/templates/index.html b/src/templates/index.html index 16399e4..b593541 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -1,11 +1,11 @@ {% extends "template.html" %} {% block content %}
-
-

Feed


- {% autoescape off %} - {{ content_string }} - {% endautoescape %} -
+
+

Feed


+ {% autoescape off %} + {{ content_string }} + {% endautoescape %} +
{% endblock %} diff --git a/src/templates/search.html b/src/templates/search.html new file mode 100644 index 0000000..7427fef --- /dev/null +++ b/src/templates/search.html @@ -0,0 +1,19 @@ +{% extends "template.html" %} +{% block content %} + +
+ +
+{% endblock %} diff --git a/src/templates/standalone.html b/src/templates/standalone.html index 0eebc6d..d41a8a8 100644 --- a/src/templates/standalone.html +++ b/src/templates/standalone.html @@ -1,10 +1,10 @@ {% extends "template.html" %} {% block content %}
-
- {% autoescape off %} - {{ content_string }} - {% endautoescape %} -
+
+ {% autoescape off %} + {{ content_string }} + {% endautoescape %} +
{% endblock %} diff --git a/src/templates/template.html b/src/templates/template.html index 5952ec4..d962258 100644 --- a/src/templates/template.html +++ b/src/templates/template.html @@ -1,38 +1,43 @@ + - {{ title }} - - - - + {{ title }} + + + + + {{ font_awesome.load_js() }} + - -