mirror of
https://github.com/tiyn/beaker-blog.git
synced 2025-10-18 22:01:15 +02:00
Compare commits
20 Commits
a936fd5ee6
...
master
Author | SHA1 | Date | |
---|---|---|---|
9fd714eef3 | |||
a927a18e39 | |||
6e844a3cb1 | |||
9ea9ab3c53 | |||
73d9faea42 | |||
586549a7f9 | |||
41ba108e3f | |||
84750323c1 | |||
e4744ee451 | |||
6fb7411156 | |||
0ff2bffc99 | |||
60be3da149 | |||
a862ac0966 | |||
070be5b0e2 | |||
2a2a5f77b6 | |||
a4d2290e47 | |||
910e6bcc0a | |||
9d11c25bf1 | |||
00686923b4 | |||
8796169ff7 |
18
Dockerfile
18
Dockerfile
@@ -2,11 +2,9 @@ FROM python:3
|
|||||||
|
|
||||||
MAINTAINER tiyn tiyn@mail-mk.eu
|
MAINTAINER tiyn tiyn@mail-mk.eu
|
||||||
|
|
||||||
COPY src /blog
|
ENV LANG en_US.UTF-8
|
||||||
|
ENV LANGUAGE en_US:en
|
||||||
WORKDIR /blog
|
ENV LC_ALL en_US.UTF-8
|
||||||
|
|
||||||
RUN pip3 install -r requirements.txt
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y locales && \
|
apt-get install -y locales && \
|
||||||
@@ -14,9 +12,13 @@ RUN apt-get update && \
|
|||||||
sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
|
sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
|
||||||
dpkg-reconfigure --frontend=noninteractive locales
|
dpkg-reconfigure --frontend=noninteractive locales
|
||||||
|
|
||||||
ENV LANG en_US.UTF-8
|
RUN apt-get install -y espeak
|
||||||
ENV LANGUAGE en_US:en
|
|
||||||
ENV LC_ALL en_US.UTF-8
|
COPY src /blog
|
||||||
|
|
||||||
|
WORKDIR /blog
|
||||||
|
|
||||||
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
VOLUME /blog/templates/entry
|
VOLUME /blog/templates/entry
|
||||||
|
|
||||||
|
@@ -13,12 +13,16 @@ via plain text files.
|
|||||||
- [x] HTML files (.html)
|
- [x] HTML files (.html)
|
||||||
- [x] Markdown Files (.md)
|
- [x] Markdown Files (.md)
|
||||||
- [x] Infinite-scroll blog page
|
- [x] Infinite-scroll blog page
|
||||||
|
- [x] Search page
|
||||||
|
- [x] Full-text search
|
||||||
|
- [x] Preview panel
|
||||||
- [x] Archive page
|
- [x] Archive page
|
||||||
- [x] Months as headings
|
- [x] Months as headings
|
||||||
- [x] Links to scrolling blog page
|
- [x] Links to scrolling blog page
|
||||||
- [x] Links to standalone article
|
- [x] Links to standalone article
|
||||||
- [x] Standalone article page
|
- [x] Standalone article page
|
||||||
- [x] Links to scrolling blog page
|
- [x] Links to scrolling blog page
|
||||||
|
- [x] TTS Functionality
|
||||||
- [x] RSS feed
|
- [x] RSS feed
|
||||||
- [x] Navigation
|
- [x] Navigation
|
||||||
- [x] Header
|
- [x] Header
|
||||||
|
135
src/app.py
135
src/app.py
@@ -1,10 +1,9 @@
|
|||||||
from flask import Flask, make_response, render_template, abort
|
from flask import (Flask, abort, make_response, redirect, render_template, request, url_for)
|
||||||
|
from flask_font_awesome import FontAwesome
|
||||||
|
|
||||||
import content as con_gen
|
|
||||||
import config
|
import config
|
||||||
|
import content as con_gen
|
||||||
|
from forms import SearchForm, register_csrf
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
TITLE = config.TITLE
|
TITLE = config.TITLE
|
||||||
STITLE = config.STITLE
|
STITLE = config.STITLE
|
||||||
@@ -14,47 +13,133 @@ DESCRIPTION = config.DESCRIPTION
|
|||||||
WEBSITE = config.WEBSITE
|
WEBSITE = config.WEBSITE
|
||||||
MAIL = config.MAIL
|
MAIL = config.MAIL
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
register_csrf(app)
|
||||||
|
font_awesome = FontAwesome(app)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def page_not_found(e):
|
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("/index.html")
|
||||||
|
def index_re():
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@app.route("/index.html")
|
|
||||||
def index():
|
def index():
|
||||||
content = con_gen.gen_index_string()
|
content = con_gen.gen_index_string()
|
||||||
return render_template("index.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE)
|
return render_template("index.html",
|
||||||
|
title=TITLE,
|
||||||
|
stitle=STITLE,
|
||||||
|
content_string=content,
|
||||||
|
style=STYLE,
|
||||||
|
language=LANGUAGE)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/search.html", methods=["GET", "POST"])
|
||||||
|
def search_re():
|
||||||
|
return redirect(url_for("search"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/search", 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.html")
|
||||||
|
def imprint_re():
|
||||||
|
return redirect(url_for("imprint"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/imprint")
|
@app.route("/imprint")
|
||||||
@app.route("/imprint.html")
|
|
||||||
def imprint():
|
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.html")
|
||||||
|
def archive_re():
|
||||||
|
return redirect(url_for("archive"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/archive")
|
@app.route("/archive")
|
||||||
@app.route("/archive.html")
|
|
||||||
def archive():
|
def archive():
|
||||||
content = con_gen.gen_arch_string()
|
content = con_gen.gen_arch_string()
|
||||||
return render_template("archive.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE)
|
return render_template("archive.html",
|
||||||
|
title=TITLE,
|
||||||
|
stitle=STITLE,
|
||||||
|
content_string=content,
|
||||||
|
style=STYLE,
|
||||||
|
language=LANGUAGE)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/entry/<path>")
|
@app.route("/entry/<path>")
|
||||||
def entry(path):
|
def entry(path):
|
||||||
content = con_gen.gen_stand_string(path)
|
content = con_gen.gen_stand_string(path)
|
||||||
if content != "":
|
if content != "":
|
||||||
return render_template("standalone.html", title=TITLE, stitle=STITLE, content_string=content, style=STYLE, language=LANGUAGE)
|
return render_template("standalone.html",
|
||||||
abort(404)
|
title=TITLE,
|
||||||
|
stitle=STITLE,
|
||||||
|
content_string=content,
|
||||||
|
style=STYLE,
|
||||||
|
language=LANGUAGE)
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/feed.xml")
|
@app.route("/feed.xml")
|
||||||
@app.route("/rss.xml")
|
@app.route("/rss.xml")
|
||||||
|
@app.route("/rss")
|
||||||
|
def feed_re():
|
||||||
|
return redirect(url_for("feed"))
|
||||||
|
|
||||||
|
@app.route("/robots.txt")
|
||||||
|
def robots():
|
||||||
|
return render_template("robots.txt")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/feed")
|
||||||
def feed():
|
def feed():
|
||||||
content = con_gen.get_rss_string()
|
content = con_gen.get_rss_string()
|
||||||
rss_xml = render_template("rss.xml", content_string=content, title=TITLE,
|
feed_xml = render_template("feed.xml",
|
||||||
description=DESCRIPTION, website=WEBSITE)
|
content_string=content,
|
||||||
response = make_response(rss_xml)
|
title=TITLE,
|
||||||
response.headers["Content-Type"] = "application/rss+xml"
|
description=DESCRIPTION,
|
||||||
return response
|
website=WEBSITE,
|
||||||
|
language=LANGUAGE)
|
||||||
|
response = make_response(feed_xml)
|
||||||
|
response.headers["Content-Type"] = "application/rss+xml"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0")
|
con_gen.prepare_tts()
|
||||||
|
app.run(host="0.0.0.0")
|
||||||
|
@@ -18,3 +18,9 @@ LANGUAGE = "en-us"
|
|||||||
|
|
||||||
# Mail address for the imprint
|
# Mail address for the imprint
|
||||||
MAIL = "dummy@mail.com"
|
MAIL = "dummy@mail.com"
|
||||||
|
|
||||||
|
# Directory to store entries in
|
||||||
|
ENTRY_DIR = "templates/entry"
|
||||||
|
|
||||||
|
# Set the timezone of your blog
|
||||||
|
TIMEZONE = "+0000"
|
||||||
|
229
src/content.py
229
src/content.py
@@ -1,28 +1,34 @@
|
|||||||
|
import glob
|
||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import urllib.parse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
import config
|
|
||||||
import markdown
|
import markdown
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from gtts import gTTS, gTTSError
|
||||||
|
|
||||||
ENTRY_DIR = "templates/entry"
|
import config
|
||||||
|
import search
|
||||||
|
|
||||||
|
WEBSITE = config.WEBSITE
|
||||||
|
ENTRY_DIR = config.ENTRY_DIR
|
||||||
LANGUAGE = config.LANGUAGE
|
LANGUAGE = config.LANGUAGE
|
||||||
LOCAL = "de_DE.UTF-8" if LANGUAGE == "de-de" else "en_US.UTF-8"
|
LOCAL = "de_DE.UTF-8" if LANGUAGE == "de-de" else "en_US.UTF-8"
|
||||||
|
TIMEZONE = config.TIMEZONE
|
||||||
|
|
||||||
locale.setlocale(locale.LC_TIME, LOCAL)
|
locale.setlocale(locale.LC_TIME, LOCAL)
|
||||||
|
|
||||||
standalone_str = "Artikel" if LANGUAGE == "de-de" else "standalone"
|
|
||||||
|
|
||||||
|
|
||||||
def gen_arch_string():
|
def gen_arch_string():
|
||||||
"""
|
"""
|
||||||
Creates and returns a archive string of every file in ENTRY_DIR.
|
Creates and returns a archive string of every file in ENTRY_DIR.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
string: html-formatted archive-string
|
string: html-formatted archive-string
|
||||||
"""
|
"""
|
||||||
path_ex = ENTRY_DIR
|
path_ex = ENTRY_DIR
|
||||||
if path.exists(path_ex):
|
if path.exists(path_ex):
|
||||||
name_list = os.listdir(path_ex)
|
name_list = os.listdir(path_ex)
|
||||||
@@ -57,11 +63,11 @@ def gen_arch_string():
|
|||||||
|
|
||||||
def gen_index_string():
|
def gen_index_string():
|
||||||
"""
|
"""
|
||||||
Create and returns a string including every file in the ENTRY_DIR as an index.
|
Create and returns a string including every file in the ENTRY_DIR as an index.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
string: html-formatted index string
|
string: html-formatted index string
|
||||||
"""
|
"""
|
||||||
path_ex = ENTRY_DIR
|
path_ex = ENTRY_DIR
|
||||||
content_string = ""
|
content_string = ""
|
||||||
if path.exists(path_ex):
|
if path.exists(path_ex):
|
||||||
@@ -70,7 +76,7 @@ def gen_index_string():
|
|||||||
contents = sorted(full_list, key=os.path.getmtime)
|
contents = sorted(full_list, key=os.path.getmtime)
|
||||||
for file in reversed(contents):
|
for file in reversed(contents):
|
||||||
filename = pathlib.PurePath(file)
|
filename = pathlib.PurePath(file)
|
||||||
purefile = filename
|
# purefile = filename
|
||||||
title = open(filename).readline().rstrip("\n")
|
title = open(filename).readline().rstrip("\n")
|
||||||
text = open(filename).readlines()[1:]
|
text = open(filename).readlines()[1:]
|
||||||
filename = filename.name
|
filename = filename.name
|
||||||
@@ -90,21 +96,44 @@ def gen_index_string():
|
|||||||
if file.endswith(".md"):
|
if file.endswith(".md"):
|
||||||
content_string += gen_md_content(file, 2)
|
content_string += gen_md_content(file, 2)
|
||||||
content_string += "</div>"
|
content_string += "</div>"
|
||||||
|
content_string = absolutize_html(content_string)
|
||||||
return content_string
|
return content_string
|
||||||
|
|
||||||
|
|
||||||
|
def absolutize_html(string):
|
||||||
|
"""
|
||||||
|
Creates a html string from another string that only uses absolute links that use the full domain.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
string: html-formatted string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string: html-formatted string with absolute linksn
|
||||||
|
"""
|
||||||
|
soup = BeautifulSoup(string, "html.parser")
|
||||||
|
for a_tag in soup.find_all("a"):
|
||||||
|
href = str(a_tag.get("href"))
|
||||||
|
if href.startswith("/") or href.startswith("."):
|
||||||
|
a_tag["href"] = urllib.parse.urljoin(WEBSITE, href)
|
||||||
|
for img_tag in soup.find_all("img"):
|
||||||
|
src = str(img_tag.get("src"))
|
||||||
|
if src.startswith("/") or src.startswith("."):
|
||||||
|
img_tag["src"] = urllib.parse.urljoin(WEBSITE, src)
|
||||||
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
def gen_stand_string(path_ex):
|
def gen_stand_string(path_ex):
|
||||||
"""
|
"""
|
||||||
Creates a html-string for a file.
|
Creates a html-string for a file.
|
||||||
If the file is markdown it will convert it.
|
If the file is markdown it will convert it.
|
||||||
This functions ensures upscaling for future formats.
|
This functions ensures upscaling for future formats.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
path_ex: path to a file.
|
path_ex: path to a file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
string: html-formatted string string equivalent to the file
|
string: html-formatted string string equivalent to the file
|
||||||
"""
|
"""
|
||||||
filename = os.path.join(ENTRY_DIR, path_ex)
|
filename = os.path.join(ENTRY_DIR, path_ex)
|
||||||
content_string = ""
|
content_string = ""
|
||||||
if path.exists(filename):
|
if path.exists(filename):
|
||||||
@@ -116,33 +145,36 @@ def gen_stand_string(path_ex):
|
|||||||
content_string += "<h1>" + title + "</h1>\n"
|
content_string += "<h1>" + title + "</h1>\n"
|
||||||
content_string += "<a href=\"" + "/index.html#" + \
|
content_string += "<a href=\"" + "/index.html#" + \
|
||||||
filename_no_end + "\">" + curr_date + "</a>"
|
filename_no_end + "\">" + curr_date + "</a>"
|
||||||
content_string += "<br>\n"
|
content_string += "<br><br>\n"
|
||||||
|
if os.path.isfile("static/tmp/" + filename_no_end + ".mp3"):
|
||||||
|
content_string += "<audio controls>\n"
|
||||||
|
content_string += '<source src="/static/tmp/' + filename_no_end + '.mp3" type="audio/mp3">\n'
|
||||||
|
content_string += "</audio>\n"
|
||||||
|
content_string += "<br><br>\n"
|
||||||
if filename.endswith(".html"):
|
if filename.endswith(".html"):
|
||||||
for line in text:
|
for line in text:
|
||||||
content_string += line
|
content_string += line
|
||||||
content_string += "<br>"
|
|
||||||
if filename.endswith(".md"):
|
if filename.endswith(".md"):
|
||||||
content_string += gen_md_content(filename, 1)
|
content_string += gen_md_content(filename, 1)
|
||||||
|
content_string = absolutize_html(content_string)
|
||||||
return content_string
|
return content_string
|
||||||
|
|
||||||
|
|
||||||
def gen_md_content(path_ex, depth):
|
def gen_md_content(path_ex, depth):
|
||||||
"""
|
"""
|
||||||
Convert a markdown file to a html string.
|
Convert a markdown file to a html string.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
path_ex (string): path to the markdown file
|
path_ex (string): path to the markdown file
|
||||||
depth (int): starting depth for markdown headings
|
depth (int): starting depth for markdown headings
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
string: html-formatted string string equivalent to the markdown file
|
string: html-formatted string string equivalent to the markdown file
|
||||||
"""
|
"""
|
||||||
content_string = ""
|
content_string = ""
|
||||||
if path.exists(path_ex):
|
if path.exists(path_ex):
|
||||||
filename = path_ex.split(".", 1)
|
|
||||||
fileend = filename[len(filename) - 1]
|
|
||||||
header = "#"
|
header = "#"
|
||||||
for i in range(depth):
|
for _ in range(depth):
|
||||||
header += "#"
|
header += "#"
|
||||||
header += " "
|
header += " "
|
||||||
markdown_lines = open(path_ex, "r").readlines()[1:]
|
markdown_lines = open(path_ex, "r").readlines()[1:]
|
||||||
@@ -155,17 +187,17 @@ def gen_md_content(path_ex, depth):
|
|||||||
|
|
||||||
def get_rss_string():
|
def get_rss_string():
|
||||||
"""
|
"""
|
||||||
Create a rss-string of the blog and return it.
|
Create a rss-string of the blog and return it.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
string: rss-string of everything that is in the ENTRY_DIR.
|
string: rss-string of everything that is in the ENTRY_DIR.
|
||||||
"""
|
"""
|
||||||
path_ex = ENTRY_DIR
|
path_ex = ENTRY_DIR
|
||||||
|
content_string = ""
|
||||||
if path.exists(path_ex):
|
if path.exists(path_ex):
|
||||||
name_list = os.listdir(path_ex)
|
name_list = os.listdir(path_ex)
|
||||||
full_list = [os.path.join(path_ex, i) for i in name_list]
|
full_list = [os.path.join(path_ex, i) for i in name_list]
|
||||||
contents = sorted(full_list, key=os.path.getmtime)
|
contents = sorted(full_list, key=os.path.getmtime)
|
||||||
content_string = ""
|
|
||||||
for file in reversed(contents):
|
for file in reversed(contents):
|
||||||
filename = pathlib.PurePath(file)
|
filename = pathlib.PurePath(file)
|
||||||
title = open(filename).readline().rstrip("\n")
|
title = open(filename).readline().rstrip("\n")
|
||||||
@@ -175,14 +207,125 @@ def get_rss_string():
|
|||||||
filename = filename.split(".", 1)[0]
|
filename = filename.split(".", 1)[0]
|
||||||
content_string += "<item>\n"
|
content_string += "<item>\n"
|
||||||
content_string += "<title>" + title + "</title>\n"
|
content_string += "<title>" + title + "</title>\n"
|
||||||
content_string += "<guid>" + config.WEBSITE + \
|
content_string += "<guid>" + WEBSITE + \
|
||||||
"/index.html#" + filename + "</guid>\n"
|
"/index.html#" + filename + "</guid>\n"
|
||||||
|
locale.setlocale(locale.LC_TIME, "en_US.UTF-8")
|
||||||
content_string += "<pubDate>" + \
|
content_string += "<pubDate>" + \
|
||||||
datetime.fromtimestamp(os.path.getmtime(file)).strftime(
|
datetime.fromtimestamp(os.path.getmtime(file)).strftime(
|
||||||
"%Y-%m-%d") + "</pubDate>\n"
|
"%a, %d %b %Y %H:%M:%S") + " " + TIMEZONE + "</pubDate>\n"
|
||||||
content_string += "<description>"
|
locale.setlocale(locale.LC_TIME, LOCAL)
|
||||||
|
content_string += "<description>\n<![CDATA[<html>\n<head>\n</head>\n<body>\n"
|
||||||
|
html_string = ""
|
||||||
for line in text:
|
for line in text:
|
||||||
content_string += line
|
html_string += line
|
||||||
content_string += "</description>\n"
|
content_string += absolutize_html(html_string)
|
||||||
|
content_string += "\n</body></html>\n]]>\n</description>\n"
|
||||||
content_string += "</item>\n"
|
content_string += "</item>\n"
|
||||||
return content_string
|
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 += "<div class=\"entry\">"
|
||||||
|
res_string += "<a href=\"" + path + "\"><h2>" + title + "</h2></a>"
|
||||||
|
res_string += "<small>"
|
||||||
|
res_string += "<a href=\"" + "/index.html#" + \
|
||||||
|
filename + "\">" + curr_date + "</a>"
|
||||||
|
res_string += "</small><br><br>"
|
||||||
|
res_string += preview + "</div>"
|
||||||
|
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")
|
||||||
|
lines = file.read()
|
||||||
|
if is_markdown:
|
||||||
|
lines += markdown.markdown(lines)
|
||||||
|
preview = ""
|
||||||
|
first_p = BeautifulSoup(lines).find('p')
|
||||||
|
if first_p is not None:
|
||||||
|
preview = "\n<p>" + first_p.text + "</p>\n"
|
||||||
|
preview += "...<br>"
|
||||||
|
return preview
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_only(filename):
|
||||||
|
"""
|
||||||
|
Convert a file to text only to use in tts
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
path (string): path to the article
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string: unformatted string containing the contents of the file
|
||||||
|
"""
|
||||||
|
# filename = os.path.join(ENTRY_DIR, path)
|
||||||
|
clean_text = ""
|
||||||
|
if path.exists(filename):
|
||||||
|
title = open(filename).readline().rstrip("\n")
|
||||||
|
text = open(filename).readlines()[1:]
|
||||||
|
filename_no_end = filename.split(".", 1)[0]
|
||||||
|
filename_no_end = filename_no_end.split("/")[-1]
|
||||||
|
content_string = ""
|
||||||
|
if filename.endswith(".html"):
|
||||||
|
for line in text:
|
||||||
|
content_string += line
|
||||||
|
if filename.endswith(".md"):
|
||||||
|
content_string += gen_md_content(filename, 1)
|
||||||
|
content_string = absolutize_html(content_string)
|
||||||
|
soup = BeautifulSoup(content_string, "html.parser")
|
||||||
|
tag_to_remove = soup.find("figure")
|
||||||
|
if tag_to_remove:
|
||||||
|
tag_to_remove.decompose()
|
||||||
|
clean_text = soup.get_text(separator=" ")
|
||||||
|
clean_text = title + "\n\n" + clean_text
|
||||||
|
return clean_text
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_tts():
|
||||||
|
files = glob.glob('static/tmp/*')
|
||||||
|
for f in files:
|
||||||
|
os.remove(f)
|
||||||
|
files = glob.glob('templates/entry/*')
|
||||||
|
clean_text = ""
|
||||||
|
for f in files:
|
||||||
|
clean_text = get_text_only(f)
|
||||||
|
_, tail = os.path.split(f)
|
||||||
|
new_filename = "static/tmp/" + os.path.splitext(tail)[0] + ".mp3"
|
||||||
|
try:
|
||||||
|
tts = gTTS(clean_text, lang=LANGUAGE.split("-")[0])
|
||||||
|
tts.save(new_filename)
|
||||||
|
except gTTSError as e:
|
||||||
|
print("Too many request to the google servers. Try it again later.")
|
||||||
|
os.remove(new_filename)
|
||||||
|
return e
|
||||||
|
16
src/forms.py
Normal file
16
src/forms.py
Normal file
@@ -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")
|
@@ -1,2 +1,8 @@
|
|||||||
Flask
|
Flask
|
||||||
Markdown
|
Markdown
|
||||||
|
Whoosh
|
||||||
|
WTForms
|
||||||
|
Flask_WTF
|
||||||
|
Font-Awesome-Flask
|
||||||
|
BeautifulSoup4
|
||||||
|
gTTS
|
||||||
|
71
src/search.py
Normal file
71
src/search.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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)
|
@@ -1,85 +1,96 @@
|
|||||||
@import 'style.css';
|
@import 'style.css';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg0: rgb(29,32,33);
|
--bg0: rgb(29,32,33);
|
||||||
--color0: rgb(220,120,0);
|
--color0: rgb(220,120,0);
|
||||||
--error: rgb(255,0,0);
|
--color1: rgb(280,180,0);
|
||||||
--footerbg0: rgb(29,32,33);
|
--error: rgb(255,0,0);
|
||||||
--link0: rgb(220, 120, 0);
|
--footerbg0: rgb(29,32,33);
|
||||||
--link1: rgb(255,255,255);
|
--link0: rgb(220, 120, 0);
|
||||||
--menulink0: rgb(220, 120, 0);
|
--link1: rgb(255,255,255);
|
||||||
--menulink1: rgb(255,255,255);
|
--menulink0: rgb(220, 120, 0);
|
||||||
--menubg0: rgb(29,32,33);
|
--menulink1: rgb(255,255,255);
|
||||||
--text0: rgb(235,219,178);
|
--menubg0: rgb(29,32,33);
|
||||||
--text1: rgb(220, 120, 0);
|
--text0: rgb(235,219,178);
|
||||||
|
--text1: rgb(220, 120, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--link0);
|
color: var(--link0);
|
||||||
transition: var(--transtime);
|
transition: var(--transtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--link1);
|
color: var(--link1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--bg0);
|
background: var(--bg0);
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background: var(--footerbg0);
|
background: var(--footerbg0);
|
||||||
color: var(--text0);
|
color: var(--text0);
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: var(--text1);
|
color: var(--text1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
color: var(--text0);
|
color: var(--text0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container h1,
|
.container h1,
|
||||||
.container h2 {
|
.container h2 {
|
||||||
color: var(--text1);
|
color: var(--text1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .flash {
|
.container .flash {
|
||||||
background-color: var(--error);
|
background-color: var(--error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-menu:hover,
|
.hide-menu:hover,
|
||||||
.main-menu a:hover,
|
.main-menu a:hover,
|
||||||
.main-menu-dropdown a:hover,
|
.main-menu-dropdown a:hover,
|
||||||
.show-menu:hover {
|
.show-menu:hover {
|
||||||
color: var(--menulink1);
|
color: var(--menulink1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu a,
|
.main-menu a,
|
||||||
.main-menu-dropdown a {
|
.main-menu-dropdown a {
|
||||||
color: var(--menulink0);
|
color: var(--menulink0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu-dropdown {
|
.main-menu-dropdown {
|
||||||
background: var(--menubg0);
|
background: var(--menubg0);
|
||||||
color: var(--menulink0);
|
color: var(--menulink0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width:800px) {
|
@media screen and (max-width:800px) {
|
||||||
|
|
||||||
.main-menu {
|
.main-menu {
|
||||||
background: var(--menubg0);
|
background: var(--menubg0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background: var(--bg0);
|
background: var(--bg0);
|
||||||
border-left: 10px solid var(--color0);
|
border-left: 10px solid var(--color0);
|
||||||
color: var(--text0);
|
color: var(--text0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry h1,
|
.entry h1,
|
||||||
.entry h2 {
|
.entry h2 {
|
||||||
color: var(--text1);
|
color: var(--text1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
form.search button[type=submit] {
|
||||||
|
background: var(--color0);
|
||||||
|
color: var(--bg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.search button[type=submit]:hover {
|
||||||
|
background: var(--color1);
|
||||||
}
|
}
|
||||||
|
@@ -1,93 +1,102 @@
|
|||||||
@import 'style.css';
|
@import 'style.css';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg0: rgb(255,255,255);
|
--bg0: rgb(255,255,255);
|
||||||
--color0: rgb(0,0,120);
|
--color0: rgb(0,0,120);
|
||||||
--error: rgb(255,0,0);
|
--color1: rgb(0,0,200);
|
||||||
--footerbg0: rgb(192,192,192);
|
--error: rgb(255,0,0);
|
||||||
--link0: rgb(0,0,120);
|
--footerbg0: rgb(192,192,192);
|
||||||
--link1: rgb(192,192,192);
|
--link0: rgb(0,0,120);
|
||||||
--menulink0: rgb(0,0,120);
|
--link1: rgb(192,192,192);
|
||||||
--menulink1: rgb(255,255,255);
|
--menulink0: rgb(0,0,120);
|
||||||
--menubg0: rgb(192,192,192);
|
--menulink1: rgb(255,255,255);
|
||||||
--text0: rgb(0,0,0);
|
--menubg0: rgb(192,192,192);
|
||||||
--text1: rgb(0,0,120);
|
--text0: rgb(0,0,0);
|
||||||
|
--text1: rgb(0,0,120);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--link0);
|
color: var(--link0);
|
||||||
transition: var(--transtime);
|
transition: var(--transtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--link1);
|
color: var(--link1);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--bg0);
|
background: var(--bg0);
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background: var(--footerbg0);
|
background: var(--footerbg0);
|
||||||
color: var(--text0);
|
color: var(--text0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer a {
|
footer a {
|
||||||
color: var(--menulink0);
|
color: var(--menulink0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer a:hover {
|
footer a:hover {
|
||||||
color: var(--menulink1);
|
color: var(--bg0);
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: var(--text1);
|
color: var(--text1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
color: var(--text0);
|
color: var(--text0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container h1,
|
.container h1,
|
||||||
.container h2 {
|
.container h2 {
|
||||||
color: var(--text1);
|
color: var(--text1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .flash {
|
.container .flash {
|
||||||
background-color: var(--error);
|
background-color: var(--error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-menu:hover,
|
.hide-menu:hover,
|
||||||
.main-menu a:hover,
|
.main-menu a:hover,
|
||||||
.main-menu-dropdown a:hover,
|
.main-menu-dropdown a:hover,
|
||||||
.show-menu:hover {
|
.show-menu:hover {
|
||||||
color: var(--menulink1);
|
color: var(--menulink1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu a,
|
.main-menu a,
|
||||||
.main-menu-dropdown a {
|
.main-menu-dropdown a {
|
||||||
color: var(--menulink0);
|
color: var(--menulink0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu-dropdown {
|
.main-menu-dropdown {
|
||||||
background: var(--menubg0);
|
background: var(--menubg0);
|
||||||
color: var(--menulink0);
|
color: var(--menulink0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width:800px) {
|
@media screen and (max-width:800px) {
|
||||||
|
.main-menu {
|
||||||
.main-menu {
|
background: var(--menubg0);
|
||||||
background: var(--menubg0);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
background: var(--bg0);
|
background: var(--bg0);
|
||||||
border-left: 10px solid var(--color0);
|
border-left: 10px solid var(--color0);
|
||||||
color: var(--text0);
|
color: var(--text0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry h1,
|
.entry h1,
|
||||||
.entry h2 {
|
.entry h2 {
|
||||||
color: var(--text1);
|
color: var(--text1);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.search button[type=submit] {
|
||||||
|
background: var(--color0);
|
||||||
|
color: var(--bg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.search button[type=submit]:hover {
|
||||||
|
background: var(--color1);
|
||||||
}
|
}
|
||||||
|
@@ -1,218 +1,280 @@
|
|||||||
:root {
|
:root {
|
||||||
--error: rgb(255,0,0);
|
--error: rgb(255,0,0);
|
||||||
--transtime: 0.7s;
|
--transtime: 0.7s;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: var(--transtime);
|
transition: var(--transtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
height: 100%;
|
max-width: 100%;
|
||||||
max-width: 100%;
|
overflow-x: hidden;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .center {
|
footer .center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-height: 100%;
|
padding-bottom: 50px;
|
||||||
padding-bottom: 50px;
|
padding-left: 10%;
|
||||||
padding-left: 10%;
|
padding-right: 10%;
|
||||||
padding-right: 10%;
|
padding-top: 5vh;
|
||||||
padding-top: 5%;
|
/* position: relative; */
|
||||||
|
min-height: calc(100vh - 50px - 5vh - 100px - 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container .flash {
|
.container .flash {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-menu,
|
.hide-menu,
|
||||||
.show-menu {
|
.show-menu {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
display: none;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
transition: var(--transtime);
|
transition: var(--transtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
.important {
|
.important {
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
padding-left: 25vw;
|
padding-left: 25vw;
|
||||||
padding-right: 25vw;
|
padding-right: 25vw;
|
||||||
padding-top: 30vh;
|
padding-top: 30vh;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.important span {
|
.important span {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 80px;
|
height: 80px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.main-menu-dropdown img {
|
.main-menu-dropdown img {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu-dropdown span,
|
.main-menu-dropdown span,
|
||||||
.main-menu-dropdown a {
|
.main-menu-dropdown a {
|
||||||
float: left;
|
float: left;
|
||||||
font-family: Georgia, serif;
|
font-family: Georgia, serif;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 100px;
|
line-height: 100px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
transition: 0.7s;
|
transition: 0.7s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu {
|
.main-menu {
|
||||||
float: right;
|
float: right;
|
||||||
font-family: Georgia, serif;
|
font-family: Georgia, serif;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 100px;
|
line-height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu a {
|
.main-menu a {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
transition: 0.7s;
|
transition: 0.7s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu-dropdown {
|
.main-menu-dropdown {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-menu {
|
.show-menu {
|
||||||
float: right;
|
float: right;
|
||||||
line-height: 100px;
|
line-height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main-menu-check {
|
#main-menu-check {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
z-index: -1111;
|
z-index: -1111;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width:800px) {
|
@media screen and (max-width:800px) {
|
||||||
.hide-menu {
|
.hide-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
top: 40px;
|
top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-menu,
|
.hide-menu,
|
||||||
.show-menu {
|
.show-menu {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-menu {
|
.main-menu {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
padding: 80px 0;
|
padding: 80px 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: -100%;
|
right: -100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
top: 0;
|
top: 0;
|
||||||
transition: var(--transtime);
|
transition: var(--transtime);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.main-menu a {
|
|
||||||
display: block;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main-menu-check:checked ~ .main-menu {
|
.main-menu a {
|
||||||
right: 0;
|
display: block;
|
||||||
}
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-menu-check:checked ~ .main-menu {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standalone {
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
border-radius: 0 10px 30px 0;
|
/* border-radius: 0 10px 30px 0; */
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standalone h1:first-child {
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imprint h1:first-child {
|
.imprint h1:first-child {
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blog h1:first-child {
|
.blog h1:first-child {
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry h2:first-child {
|
.entry h2:first-child {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogarchive h1:first-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blogarchive h2:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry ul {
|
.entry ul {
|
||||||
padding-left: 20;
|
padding-left: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry figure:last-child {
|
.entry figure:last-child {
|
||||||
padding-bottom:0
|
padding-bottom:0
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding-left:20px;
|
padding-left:20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
padding-left:20px;
|
padding-left:20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
border-radius: 25px;
|
/* border-radius: 25px; */
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
display: inline-block
|
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: 10px;
|
||||||
|
font-size: 17px;
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-left: none; /* Prevent double borders */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.search::after {
|
||||||
|
content: "";
|
||||||
|
clear: both;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standalone img {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry img {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio {
|
||||||
|
width: 50%;
|
||||||
|
height: 5vh;
|
||||||
}
|
}
|
||||||
|
0
src/static/tmp/.gitkeep
Normal file
0
src/static/tmp/.gitkeep
Normal file
@@ -1,11 +1,11 @@
|
|||||||
{% extends "template.html" %}
|
{% extends "template.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="blogarchive">
|
<div class="blogarchive">
|
||||||
<h1>{% if language=="de-de" %}Archiv{% else %}Archive{% endif %}</h1><br>
|
<h1>{% if language=="de-de" %}Archiv{% else %}Archive{% endif %}</h1><br>
|
||||||
{% autoescape off %}
|
{% autoescape off %}
|
||||||
{{ content_string }}
|
{{ content_string }}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
{% extends "template.html" %}
|
{% extends "template.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="important">
|
<div class="important">
|
||||||
{% if language=="de-de" %}Fehler{% else %}Error{% endif %}<br>
|
{% if language=="de-de" %}Fehler{% else %}Error{% endif %}<br>
|
||||||
<span>{{ errorcode }}</span>
|
<span>{{ errorcode }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -5,8 +5,8 @@
|
|||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<description>{{ description }}</description>
|
<description>{{ description }}</description>
|
||||||
<language>{{ language }}</language>
|
<language>{{ language }}</language>
|
||||||
<link>{{ website }}/feed.xml</link>
|
<link>{{ website }}</link>
|
||||||
<atom:link href="/feed.xml" rel="self" type="application/rss+xml" />
|
<atom:link href="{{ website }}{{ url_for('feed') }}" rel="self" type="application/rss+xml"/>
|
||||||
|
|
||||||
{% autoescape off %}
|
{% autoescape off %}
|
||||||
{{ content_string }}
|
{{ content_string }}
|
@@ -1,11 +1,11 @@
|
|||||||
{% extends "template.html" %}
|
{% extends "template.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="blog">
|
<div class="blog">
|
||||||
<h1>Feed</h1><br>
|
<h1>Feed</h1><br>
|
||||||
{% autoescape off %}
|
{% autoescape off %}
|
||||||
{{ content_string }}
|
{{ content_string }}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
2
src/templates/robots.txt
Normal file
2
src/templates/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /static/
|
19
src/templates/search.html
Normal file
19
src/templates/search.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "template.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="search">
|
||||||
|
<h1>{% if language=="de-de" %}Suche{% else %}Search{% endif %}</h1><br>
|
||||||
|
<form class="search" action="{{ url_for('search') }}" method=post>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ form.query_str }}
|
||||||
|
<!-- {{ form.submit }} -->
|
||||||
|
<button id="submit" name="submit" type="submit" value="Search">{{ font_awesome.render_icon("fas fa-search")
|
||||||
|
}}</button>
|
||||||
|
</form>
|
||||||
|
{% autoescape off %}
|
||||||
|
{{ content }}
|
||||||
|
{% endautoescape %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@@ -1,10 +1,10 @@
|
|||||||
{% extends "template.html" %}
|
{% extends "template.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="standalone">
|
<div class="standalone">
|
||||||
{% autoescape off %}
|
{% autoescape off %}
|
||||||
{{ content_string }}
|
{{ content_string }}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,38 +1,46 @@
|
|||||||
<html>
|
<!DOCTYPE html>
|
||||||
|
<html lang={% if language=="de-de" %}de{% else %}en{% endif %}>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<link href="{{ url_for('static', filename='css/' + style + '.css') }}" rel="stylesheet" type="text/css">
|
<link href="{{ url_for('static', filename='css/' + style + '.css') }}" rel="stylesheet" type="text/css">
|
||||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='graphics/logo.png') }}">
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='graphics/logo.png')}}">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width" initial-scale=1.0>
|
<meta name="viewport" content="width=device-width">
|
||||||
|
{{ font_awesome.load_js() }}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Menu -->
|
<!-- Menu -->
|
||||||
<div class="main-menu-dropdown">
|
<div class="main-menu-dropdown">
|
||||||
<a href="{{ url_for('index') }}">
|
<a href="{{ url_for('index') }}">
|
||||||
<img class="logo" src="{{ url_for('static', filename='graphics/logo.png') }}">
|
<img class="logo" src="{{ url_for('static', filename='graphics/logo.png') }}" alt="Logo von
|
||||||
{{ stitle }}
|
Mittelerde mit Marten. Zu sehen sind 3 'M's mit Serifen, wobei die beiden Äußeren etwas
|
||||||
</a>
|
kleiner sind">
|
||||||
<input type="checkbox" id="main-menu-check">
|
{{ stitle }}
|
||||||
<label for="main-menu-check" class="show-menu">☰</label>
|
</a>
|
||||||
<div class="main-menu">
|
<input type="checkbox" id="main-menu-check">
|
||||||
<a href="{{ url_for('index') }}">Blog</a>
|
<label for="main-menu-check" class="show-menu">☰</label>
|
||||||
<a href="{{ url_for('archive') }}">{% if language=="de-de" %}Archiv{% else %}Archive{% endif %}</a>
|
<div class="main-menu">
|
||||||
<label for="main-menu-check" class="hide-menu">X</label>
|
<a href="{{ url_for('index') }}">Blog</a>
|
||||||
</div>
|
<a href="{{ url_for('archive') }}">{% if language=="de-de" %}Archiv{% else %}Archive{% endif %}</a>
|
||||||
|
<a href="{{ url_for('search') }}">{% if language=="de-de" %}Suche{% else %}Search{% endif %}</a>
|
||||||
|
<label for="main-menu-check" class="hide-menu">X</label>
|
||||||
</div>
|
</div>
|
||||||
<!-- Menu -->
|
</div>
|
||||||
<!-- Content -->
|
<!-- Menu -->
|
||||||
{% block content %}
|
<!-- Content -->
|
||||||
{% endblock %}
|
{% block content %}
|
||||||
<!-- Content -->
|
{% endblock %}
|
||||||
<footer>
|
<!-- Content -->
|
||||||
<div class="center">
|
<footer>
|
||||||
<a href="{{ url_for('imprint') }}">
|
<div class="center">
|
||||||
{% if language=="de-de" %}Impressum und Kontakt{% else %}Imprint and Contact{% endif %}
|
<a href="{{ url_for('imprint') }}">{% if language=="de-de" %}Impressum und Kontakt{% else %}Imprint and Contact{%
|
||||||
</a><br>
|
endif %}</a>.<br>
|
||||||
Made with <a href="https://github.com/tiyn/beaker-blog">Beaker Blog</a>.
|
<a href="{{ url_for('feed') }}">{% if language=="de-de" %}RSS-Feed{% else %}RSS feed{% endif %}</a>.<br>
|
||||||
</div>
|
Made with <a href="https://github.com/tiyn/beaker-blog">Beaker Blog</a>.
|
||||||
</footer>
|
</div>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
Reference in New Issue
Block a user