1
0
mirror of https://github.com/tiyn/beaker-blog.git synced 2025-10-15 04:11:17 +02:00

1 Commits

Author SHA1 Message Date
dependabot[bot]
9fa7c9339c Bump flask from 1.1.2 to 2.3.2 in /src
Bumps [flask](https://github.com/pallets/flask) from 1.1.2 to 2.3.2.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/1.1.2...2.3.2)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 21:40:50 +00:00
24 changed files with 403 additions and 1041 deletions

View File

@@ -2,18 +2,6 @@ FROM python:3
MAINTAINER tiyn tiyn@mail-mk.eu
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
RUN apt-get update && \
apt-get install -y locales && \
sed -i -e 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen && \
sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales
RUN apt-get install -y espeak
COPY src /blog
WORKDIR /blog
@@ -22,8 +10,6 @@ RUN pip3 install -r requirements.txt
VOLUME /blog/templates/entry
VOLUME /blog/static/graphics
EXPOSE 5000
ENTRYPOINT [ "python3" ]

View File

@@ -13,16 +13,12 @@ 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
- [x] Links to standalone article
- [x] Standalone article page
- [x] Links to scrolling blog page
- [x] TTS Functionality
- [x] RSS feed
- [x] Navigation
- [x] Header
@@ -30,9 +26,6 @@ via plain text files.
- [x] Switchable CSS
- [x] CSS dark-theme
- [x] CSS light-theme
- [x] Language Support
- [x] English
- [x] German
- [x] Config file
- [x] Docker installation
- [x] Logo
@@ -65,13 +58,12 @@ The `config.py` can be found in the `src` folder.
Set the following volumes with the -v tag.
| Volume-Name | Container mount | Description |
| ------------- | ----------------------- | ------------------------------------------------------------ |
| `config-file` | `/blog/config.py` | Config file |
| `entries` | `/blog/templates/entry` | Directory for blog entries |
| `graphics` | `/blog/static/graphics` | Directory for images needed for entries |
| `css` | `/blog/static/css` | (optional) Directory for css files |
| `html` | `/blog/templates` | (optional) Directory for templates (entry-volume not needed) |
| Volume-Name | Container mount | Description |
| ------------- | --------------------------- | ------------------------------------------------------------ |
| `config-file` | `/blog/src/config.py` | Config file |
| `entries` | `/blog/src/templates/entry` | Directory for blog entries |
| `css` | `/blog/src/static/css` | (optional) Directory for css files |
| `html` | `/blog/src/templates` | (optional) Directory for templates (entry-volume not needed) |
#### Ports

View File

@@ -6,6 +6,4 @@ docker run --name beaker-blog \
--restart unless-stopped \
-p "5000:5000" \
-e FLASK_ENV=development \
-v entries:/blog/templates/entry \
-v graphics:/blog/static/graphics \
-d tiyn/beaker-blog

View File

@@ -1,145 +1,53 @@
from flask import (Flask, abort, make_response, redirect, render_template, request, url_for)
from flask_font_awesome import FontAwesome
from flask import Flask, make_response, render_template, abort
import config
import content as con_gen
from forms import SearchForm, register_csrf
import config
TITLE = config.TITLE
STITLE = config.STITLE
STYLE = config.STYLE
LANGUAGE = config.LANGUAGE
DESCRIPTION = config.DESCRIPTION
WEBSITE = config.WEBSITE
MAIL = config.MAIL
app = Flask(__name__)
register_csrf(app)
font_awesome = FontAwesome(app)
TITLE = config.TITLE
STYLE = config.STYLE
DESCRIPTION = config.DESCRIPTION
WEBSITE = config.WEBSITE
@app.errorhandler(404)
def page_not_found(e):
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"))
return render_template("error.html", title=TITLE, errorcode="404", style=STYLE), 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)
@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")
def imprint():
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"))
content = con_gen.gen_index_string()
return render_template("index.html", title=TITLE, content_string=content, style=STYLE)
@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, content_string=content, style=STYLE)
@app.route("/entry/<path>")
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, content_string=content, style=STYLE)
abort(404)
@app.route("/feed.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():
content = con_gen.get_rss_string()
feed_xml = render_template("feed.xml",
content_string=content,
title=TITLE,
description=DESCRIPTION,
website=WEBSITE,
language=LANGUAGE)
response = make_response(feed_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__":
con_gen.prepare_tts()
app.run(host="0.0.0.0")
app.run(host="0.0.0.0")

View File

@@ -1,9 +1,6 @@
# Name/title of your blog
TITLE = "Beaker Blog"
# Short name/title of your blog
STITLE = "Beaker Blog"
# Description for RSS of your blog
DESCRIPTION = "This is your personal Beaker Blog."
@@ -12,15 +9,3 @@ WEBSITE = "localhost:5000"
# Theme for the blog: dark, light
STYLE = "dark"
# Language for the titles: en-us or de-de
LANGUAGE = "en-us"
# Mail address for the imprint
MAIL = "dummy@mail.com"
# Directory to store entries in
ENTRY_DIR = "templates/entry"
# Set the timezone of your blog
TIMEZONE = "+0000"

View File

@@ -1,331 +1,186 @@
import glob
import locale
import os
import pathlib
import urllib.parse
from datetime import datetime
from os import path
import markdown
from bs4 import BeautifulSoup
from gtts import gTTS, gTTSError
import os
from os import path
import pathlib
import config
import search
WEBSITE = config.WEBSITE
ENTRY_DIR = config.ENTRY_DIR
LANGUAGE = config.LANGUAGE
LOCAL = "de_DE.UTF-8" if LANGUAGE == "de-de" else "en_US.UTF-8"
TIMEZONE = config.TIMEZONE
locale.setlocale(locale.LC_TIME, LOCAL)
ENTRY_DIR = "templates/entry"
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:
string: html-formatted archive-string
"""
path_ex = ENTRY_DIR
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 = ""
last_month = ""
for file in reversed(contents):
curr_date = datetime.fromtimestamp(os.path.getmtime(file)).strftime("%Y-%m-%d")
curr_month = datetime.fromtimestamp(os.path.getmtime(file)).strftime("%B %Y")
if curr_month != last_month:
if last_month != "":
content_string += "</ul>\n"
content_string += "<h2>" + curr_month + "</h2>\n"
content_string += "<ul>\n"
last_month = curr_month
filename = pathlib.PurePath(file)
title = open(filename).readline().rstrip("\n")
filename = filename.name
if filename[0] != ".":
filename = filename.split(".", 1)[0]
content_string += "<li>"
content_string += "<a href=\"" + "/index.html#" + \
filename + "\">" + curr_date + "</a> - "
content_string += "<a href=\"" + "/entry/" + \
pathlib.PurePath(file).name + "\"><b>" + title + "</b></a>"
content_string += "<br>"
content_string += "</li>\n"
content_string += "</ul>\n"
return content_string
Returns:
string: html-formatted archive-string
"""
path_ex = ENTRY_DIR
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.getctime)
content_string = ""
last_month = ""
for file in reversed(contents):
curr_date = datetime.fromtimestamp(
os.path.getctime(file)).strftime("%Y-%m-%d")
curr_month = datetime.fromtimestamp(
os.path.getctime(file)).strftime("%b %Y")
if curr_month != last_month:
if last_month != "":
content_string += "</ul>\n"
content_string += "<h2>" + curr_month + "</h2>\n"
content_string += "<ul>\n"
last_month = curr_month
filename = pathlib.PurePath(file)
title = open(filename).readline().rstrip("\n")
filename = filename.name
if filename[0] != ".":
filename = filename.split(".", 1)[0]
content_string += "<li>"
content_string += curr_date + " - "
content_string += title + " ["
content_string += "<a href=\"" + "/index.html#" + \
filename + "\">" + "link" + "</a> - "
content_string += "<a href=\"" + "/entry/" + \
pathlib.PurePath(file).name + "\">" + "standalone" + "</a>"
content_string += "] <br>"
content_string += "</li>\n"
content_string += "</ul>\n"
return content_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:
string: html-formatted index string
"""
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)
for file in reversed(contents):
filename = pathlib.PurePath(file)
# purefile = filename
title = open(filename).readline().rstrip("\n")
text = open(filename).readlines()[1:]
filename = filename.name
if filename[0] != ".":
filename = filename.split(".", 1)[0]
content_string += "<div class=\"entry\">\n"
content_string += "<h2 id=\"" + filename + "\">"
content_string += "<a href=\"" + "/entry/" + \
pathlib.PurePath(file).name + "\">" + \
title + "</a>" +"</h2>\n"
content_string += "<small>" + \
datetime.fromtimestamp(os.path.getmtime(
file)).strftime("%Y-%m-%d") + "</small><br><br>"
if file.endswith(".html"):
for line in text:
content_string += line
if file.endswith(".md"):
content_string += gen_md_content(file, 2)
content_string += "</div>"
content_string = absolutize_html(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)
Returns:
string: html-formatted index string
"""
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.getctime)
for file in reversed(contents):
filename = pathlib.PurePath(file)
purefile = filename
title = open(filename).readline().rstrip("\n")
text = open(filename).readlines()[1:]
filename = filename.name
if filename[0] != ".":
filename = filename.split(".", 1)[0]
content_string += "<div class=\"entry\">\n"
content_string += "<h2 id=\"" + filename + "\">" + title + "</h2>\n"
content_string += "[<a href=\"" + "/entry/" + \
pathlib.PurePath(file).name + "\">" + \
"standalone" + "</a>]<br>\n"
if file.endswith(".html"):
for line in text:
content_string += line
content_string += "<br>"
if file.endswith(".md"):
content_string += gen_md_content(file, 2)
content_string += "<small>" + \
datetime.fromtimestamp(os.path.getctime(
file)).strftime("%Y-%m-%d") + "</small>"
content_string += "</div>"
return content_string
def gen_stand_string(path_ex):
"""
Creates a html-string for a file.
If the file is markdown it will convert it.
This functions ensures upscaling for future formats.
"""
Creates a html-string for a file.
If the file is markdown it will convert it.
This functions ensures upscaling for future formats.
Parameters:
path_ex: path to a file.
Parameters:
path_ex: path to a file.
Returns:
string: html-formatted string string equivalent to the file
"""
filename = os.path.join(ENTRY_DIR, path_ex)
content_string = ""
if path.exists(filename):
title = open(filename).readline().rstrip("\n")
text = open(filename).readlines()[1:]
curr_date = datetime.fromtimestamp(os.path.getmtime(filename)).strftime("%Y-%m-%d")
filename_no_end = filename.split(".", 1)[0]
filename_no_end = filename_no_end.split("/")[-1]
content_string += "<h1>" + title + "</h1>\n"
content_string += "<a href=\"" + "/index.html#" + \
filename_no_end + "\">" + curr_date + "</a>"
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"):
for line in text:
content_string += line
if filename.endswith(".md"):
content_string += gen_md_content(filename, 1)
content_string = absolutize_html(content_string)
return content_string
Returns:
string: html-formatted string string equivalent to the file
"""
filename = os.path.join(ENTRY_DIR, path_ex)
content_string = ""
if path.exists(filename):
title = open(filename).readline().rstrip("\n")
text = open(filename).readlines()[1:]
filename_no_end = filename.split(".", 1)[0]
content_string += "<h1>" + title + "</h1>\n"
content_string += "["
content_string += "<a href=\"" + "/index.html#" + \
filename_no_end + "\">" + "link" + "</a>"
content_string += "]<br>\n"
if filename.endswith(".html"):
for line in text:
content_string += line
content_string += "<br>"
if filename.endswith(".md"):
content_string += gen_md_content(filename, 1)
return content_string
def gen_md_content(path_ex, depth):
"""
Convert a markdown file to a html string.
"""
Convert a markdown file to a html string.
Parameters:
path_ex (string): path to the markdown file
depth (int): starting depth for markdown headings
Parameters:
path_ex (string): path to the markdown file
depth (int): starting depth for markdown headings
Returns:
string: html-formatted string string equivalent to the markdown file
"""
content_string = ""
if path.exists(path_ex):
header = "#"
for _ in range(depth):
header += "#"
header += " "
markdown_lines = open(path_ex, "r").readlines()[1:]
markdown_text = ""
for line in markdown_lines:
markdown_text += line.replace("# ", header)
content_string = markdown.markdown(markdown_text, extensions=["fenced_code", "tables"])
return content_string
Returns:
string: html-formatted string string equivalent to the markdown file
"""
content_string = ""
if path.exists(path_ex):
filename = path_ex.split(".", 1)
fileend = filename[len(filename) - 1]
header = "#"
for i in range(depth):
header += "#"
header += " "
markdown_lines = open(path_ex, "r").readlines()[1:]
markdown_text = ""
for line in markdown_lines:
markdown_text += line.replace("# ", header)
content_string = markdown.markdown(
markdown_text, extensions=["fenced_code", "tables"]
)
return content_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:
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)
for file in reversed(contents):
filename = pathlib.PurePath(file)
title = open(filename).readline().rstrip("\n")
text = open(filename).readlines()[1:]
filename = filename.name
if filename[0] != ".":
filename = filename.split(".", 1)[0]
content_string += "<item>\n"
content_string += "<title>" + title + "</title>\n"
content_string += "<guid>" + WEBSITE + \
"/index.html#" + filename + "</guid>\n"
locale.setlocale(locale.LC_TIME, "en_US.UTF-8")
content_string += "<pubDate>" + \
datetime.fromtimestamp(os.path.getmtime(file)).strftime(
"%a, %d %b %Y %H:%M:%S") + " " + TIMEZONE + "</pubDate>\n"
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:
html_string += line
content_string += absolutize_html(html_string)
content_string += "\n</body></html>\n]]>\n</description>\n"
content_string += "</item>\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 += "<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
Returns:
string: rss-string of everything that is in the ENTRY_DIR.
"""
path_ex = ENTRY_DIR
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.getctime)
content_string = ""
for file in reversed(contents):
filename = pathlib.PurePath(file)
title = open(filename).readline().rstrip("\n")
text = open(filename).readlines()[1:]
filename = filename.name
if filename[0] != ".":
filename = filename.split(".", 1)[0]
content_string += "<item>\n"
content_string += "<title>" + title + "</title>\n"
content_string += "<guid>" + config.WEBSITE + \
"/index.html#" + filename + "</guid>\n"
content_string += "<pubDate>" + \
datetime.fromtimestamp(os.path.getctime(file)).strftime(
"%Y-%m-%d") + "</pubDate>\n"
content_string += "<description>"
for line in text:
content_string += line
content_string += "</description>\n"
content_string += "</item>\n"
return content_string

View File

@@ -1,16 +0,0 @@
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")

View File

@@ -1,8 +1,2 @@
Flask
Markdown
Whoosh
WTForms
Flask_WTF
Font-Awesome-Flask
BeautifulSoup4
gTTS
Flask==2.3.2
Markdown==3.1.1

View File

@@ -1,71 +0,0 @@
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)

View File

@@ -1,96 +1,83 @@
@import 'style.css';
: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);
--link1: rgb(255,255,255);
--menulink0: rgb(220, 120, 0);
--menulink1: rgb(255,255,255);
--menubg0: rgb(29,32,33);
--text0: rgb(235,219,178);
--text1: rgb(220, 120, 0);
--bg0: rgb(29,32,33);
--color0: rgb(220,120,0);
--error: rgb(255,0,0);
--footerbg0: rgb(29,32,33);
--link0: rgb(220, 120, 0);
--link1: rgb(255,255,255);
--menulink0: rgb(220, 120, 0);
--menulink1: rgb(255,255,255);
--menubg0: rgb(29,32,33);
--text0: rgb(235,219,178);
--text1: rgb(220, 120, 0);
}
a {
color: var(--link0);
transition: var(--transtime);
color: var(--link0);
transition: var(--transtime);
}
a:hover {
color: var(--link1);
color: var(--link1);
}
body {
background: var(--bg0);
background: var(--bg0);
}
footer {
background: var(--footerbg0);
color: var(--text0);
background: var(--footerbg0);
color: var(--text0);
}
span {
color: var(--text1);
color: var(--text1);
}
.container {
color: var(--text0);
color: var(--text0);
}
.container h1,
.container h2 {
color: var(--text1);
color: var(--text1);
}
.container .flash {
background-color: var(--error);
background-color: var(--error);
}
.hide-menu:hover,
.main-menu a:hover,
.main-menu-dropdown a:hover,
.show-menu:hover {
color: var(--menulink1);
color: var(--menulink1);
}
.main-menu a,
.main-menu-dropdown a {
color: var(--menulink0);
.main-menu a {
color: var(--menulink0);
}
.main-menu-dropdown {
background: var(--menubg0);
color: var(--menulink0);
background: var(--menubg0);
color: var(--menulink0);
}
@media screen and (max-width:800px) {
.main-menu {
background: var(--menubg0);
}
.main-menu {
background: var(--menubg0);
}
}
.entry {
background: var(--bg0);
border-left: 10px solid var(--color0);
color: var(--text0);
background: var(--bg0);
border-left: 10px solid var(--color0);
color: var(--text0);
}
.entry h1,
.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);
color: var(--text1);
}

View File

@@ -1,102 +1,83 @@
@import 'style.css';
: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);
--link1: rgb(192,192,192);
--menulink0: rgb(0,0,120);
--menulink1: rgb(255,255,255);
--menubg0: rgb(192,192,192);
--text0: rgb(0,0,0);
--text1: rgb(0,0,120);
--bg0: rgb(255,255,255);
--color0: rgb(0,0,120);
--error: rgb(255,0,0);
--footerbg0: rgb(192,192,192);
--link0: rgb(0,0,120);
--link1: rgb(255,255,255);
--menulink0: rgb(0,0,120);
--menulink1: rgb(255,255,255);
--menubg0: rgb(192,192,192);
--text0: rgb(0,0,0);
--text1: rgb(0,0,120);
}
a {
color: var(--link0);
transition: var(--transtime);
color: var(--link0);
transition: var(--transtime);
}
a:hover {
color: var(--link1);
color: var(--link1);
}
body {
background: var(--bg0);
background: var(--bg0);
}
footer {
background: var(--footerbg0);
color: var(--text0);
}
footer a {
color: var(--menulink0);
}
footer a:hover {
color: var(--bg0);
background: var(--footerbg0);
color: var(--text0);
}
span {
color: var(--text1);
color: var(--text1);
}
.container {
color: var(--text0);
color: var(--text0);
}
.container h1,
.container h2 {
color: var(--text1);
color: var(--text1);
}
.container .flash {
background-color: var(--error);
background-color: var(--error);
}
.hide-menu:hover,
.main-menu a:hover,
.main-menu-dropdown a:hover,
.show-menu:hover {
color: var(--menulink1);
color: var(--menulink1);
}
.main-menu a,
.main-menu-dropdown a {
color: var(--menulink0);
.main-menu a {
color: var(--menulink0);
}
.main-menu-dropdown {
background: var(--menubg0);
color: var(--menulink0);
background: var(--menubg0);
color: var(--menulink0);
}
@media screen and (max-width:800px) {
.main-menu {
background: var(--menubg0);
}
.main-menu {
background: var(--menubg0);
}
}
.entry {
background: var(--bg0);
border-left: 10px solid var(--color0);
color: var(--text0);
background: var(--bg0);
border-left: 10px solid var(--color0);
color: var(--text0);
}
.entry h1,
.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);
color: var(--text1);
}

View File

@@ -1,280 +1,169 @@
:root {
--error: rgb(255,0,0);
--transtime: 0.7s;
--error: rgb(255,0,0);
--transtime: 0.7s;
}
* {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
a {
text-decoration: none;
transition: var(--transtime);
text-decoration: none;
transition: var(--transtime);
}
a:hover {
cursor: pointer;
cursor: pointer;
}
body {
margin: 0;
margin: 0;
}
body,
html {
font-family: sans-serif;
max-width: 100%;
overflow-x: hidden;
font-family: sans-serif;
height: 100%;
max-width: 100%;
overflow-x: hidden;
}
footer {
height: 100px;
padding-top: 20px;
width: 100%;
height: 100px;
padding-top: 20px;
}
footer .center {
text-align: center;
text-align: center;
}
.container {
padding-bottom: 50px;
padding-left: 10%;
padding-right: 10%;
padding-top: 5vh;
/* position: relative; */
min-height: calc(100vh - 50px - 5vh - 100px - 100px);
min-height: 100%;
padding-bottom: 50px;
padding-left: 10%;
padding-right: 10%;
padding-top: 5%;
}
.container .flash {
padding: 10px;
width: 400px;
padding: 10px;
width: 400px;
}
.hide-menu,
.show-menu {
cursor: pointer;
display: none;
font-size: 30px;
transition: var(--transtime);
cursor: pointer;
display: none;
font-size: 30px;
transition: var(--transtime);
}
.important {
font-size: xx-large;
padding-left: 25vw;
padding-right: 25vw;
padding-top: 30vh;
text-align: left;
font-size: xx-large;
padding-left: 25vw;
padding-right: 25vw;
padding-top: 30vh;
text-align: left;
}
.important span {
font-weight: bold;
font-weight: bold;
}
.logo {
height: 80px;
padding-top: 10px;
height: 80px;
padding-top: 10px;
}
.main-menu-dropdown img {
float: left;
}
.main-menu-dropdown span,
.main-menu-dropdown a {
float: left;
font-family: Georgia, serif;
font-size: 30px;
font-weight: bold;
line-height: 100px;
padding: 0 10px;
text-decoration: none;
text-transform: uppercase;
transition: 0.7s;
.main-menu-dropdown span {
float: left;
font-family: monospace;
font-size: 30px;
font-weight: bold;
line-height: 100px;
padding: 0 10px;
text-decoration: none;
text-transform: uppercase;
transition: 0.7s;
}
.main-menu {
float: right;
font-family: Georgia, serif;
font-size: 30px;
font-weight: bold;
line-height: 100px;
float: right;
font-family: monospace;
font-size: 30px;
font-weight: bold;
line-height: 100px;
}
.main-menu a {
padding: 0 10px;
text-decoration: none;
text-transform: uppercase;
transition: 0.7s;
padding: 0 10px;
text-decoration: none;
text-transform: uppercase;
transition: 0.7s;
}
.main-menu-dropdown {
height: 100px;
padding: 0 20px;
height: 100px;
padding: 0 20px;
}
.show-menu {
float: right;
line-height: 100px;
float: right;
line-height: 100px;
}
#main-menu-check {
position: absolute;
visibility: hidden;
z-index: -1111;
position: absolute;
visibility: hidden;
z-index: -1111;
}
@media screen and (max-width:800px) {
.hide-menu {
position: absolute;
right: 40px;
top: 40px;
}
.hide-menu {
position: absolute;
right: 40px;
top: 40px;
}
.hide-menu,
.show-menu {
display: block;
}
.hide-menu,
.show-menu {
display: block;
}
.main-menu {
height: 100vh;
line-height: normal;
padding: 80px 0;
position: fixed;
right: -100%;
text-align: center;
top: 0;
transition: var(--transtime);
width: 100%;
}
.main-menu {
height: 100vh;
line-height: normal;
padding: 80px 0;
position: fixed;
right: -100%;
text-align: center;
top: 0;
transition: var(--transtime);
width: 100%;
}
.main-menu a {
display: block;
padding: 20px;
}
.main-menu a {
display: block;
padding: 20px;
}
#main-menu-check:checked ~ .main-menu {
right: 0;
}
}
form {
margin-bottom: 40px;
}
.standalone {
min-height: 100vh;
#main-menu-check:checked ~ .main-menu {
right: 0;
}
}
.entry {
/* border-radius: 0 10px 30px 0; */
margin-bottom: 20px;
padding-left: 20px;
border-radius: 0 10px 30px 0;
margin-bottom: 20px;
padding: 10px;
}
h1, h2 {
padding-top: 20px;
padding-bottom: 20px;
}
h3 {
padding-top: 10px;
padding-bottom: 10px;
}
.standalone h1:first-child {
padding-bottom: 0;
}
.imprint h1:first-child {
padding-bottom: 20px;
}
.blog h1:first-child {
padding-bottom: 20px;
}
.entry h2:first-child {
padding-top: 0;
padding-bottom: 0;
}
.blogarchive h1:first-child {
padding-bottom: 0;
}
.blogarchive h2:first-child {
padding-top: 0;
padding-bottom: 0;
.entry h1,
.entry h2 {
margin: 5px auto 2px auto;
}
.entry ul {
padding-left: 20;
padding-left: 20;
}
figure {
padding-top: 20px;
padding-bottom: 20px;
}
.entry figure:last-child {
padding-bottom:0
}
ul {
padding-left:20px;
}
ol {
padding-left:20px;
}
code {
/* border-radius: 25px; */
padding-left: 20px;
padding-right: 20px;
page-break-inside: avoid;
font-family: monospace;
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: 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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,11 +1,11 @@
{% extends "template.html" %}
{% block content %}
<div class="container">
<div class="blogarchive">
<h1>{% if language=="de-de" %}Archiv{% else %}Archive{% endif %}</h1><br>
{% autoescape off %}
{{ content_string }}
{% endautoescape %}
</div>
<div class="blogarchive">
<h1>Archive</h1><br>
{% autoescape off %}
{{ content_string }}
{% endautoescape %}
</div>
</div>
{% endblock %}

View File

@@ -1,9 +1,9 @@
{% extends "template.html" %}
{% block content %}
<div class="container">
<div class="important">
{% if language=="de-de" %}Fehler{% else %}Error{% endif %}<br>
<span>{{ errorcode }}</span>
</div>
<div class="important">
Error<br>
<span>{{ errorcode }}</span>
</div>
</div>
{% endblock %}

View File

@@ -1,91 +0,0 @@
{% extends "template.html" %}
{% block content %}
<div class="container">
<div class="imprint">
<h1>{% if language=="de-de" %}Impressum{% else %}Imprint{% endif %}</h1><br>
{% if language=="de-de" %}
<h2>Kontakt:</h2>
<ul>
<li><a href='mailto:{{ mail }}'>E-Mail</a></li>
</ul>
<h2>Haftungsausschluss:</h2>
<h3>Haftung für Inhalte</h3>
<p>
Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und
Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß § 7 Abs.1
TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind
wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu
überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur
Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt.
Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung
möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend
entfernen.
</p>
<h3>Haftung für Links</h3>
<p>
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb
können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist
stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum
Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der
Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete
Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir
derartige Links umgehend entfernen.
</p>
<h3>Urheberrecht</h3>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen
Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen
des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und
Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf
dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden
Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam
werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir
derartige Inhalte umgehend entfernen.
</p>
{% else %}
<h2>Contact:</h2>
<p><a href='mailto:{{ mail }}'>E-Mail</a></p>
<h2>Disclaimer:</h2>
<h3>Liability for Content</h3>
<p>
The contents of our website have been created with the greatest possible care. However, we cannot guarantee the
contents' accuracy, completeness, or topicality. According to Section 7, paragraph 1 of the TMG (Telemediengesetz
-
German Telemedia Act), we as service providers are liable for our content on these pages by general laws. However,
according to Sections 8 to 10 of the TMG, we service providers are not obliged to monitor external information
transmitted or stored or investigate circumstances pointing to illegal activity. Obligations to remove or block
the
use of information under general laws remain unaffected. However, a liability in this regard is only possible from
the moment of knowledge of a specific infringement. Upon notification of such violations, we will remove the
content
immediately.
</p>
<h3>Liability for Links</h3>
<p>
Our website contains links to external websites, over whose contents we have no control. Therefore, we cannot
accept
any liability for these external contents. The respective provider or operator of the websites is always
responsible
for the contents of the linked pages. The linked pages were checked for possible legal violations at the time of
linking. Illegal contents were not identified at the time of linking. However, permanent monitoring of the
contents
of the linked pages is not reasonable without specific indications of a violation. Upon notification of
violations,
we will remove such links immediately.
</p>
<h3>Copyright</h3>
<p>
The contents and works on these pages created by the site operator are subject to German copyright law. The
duplication, processing, distribution, and any kind of utilization outside the limits of copyright require the
written consent of the respective author or creator. Downloads and copies of these pages are only permitted for
private, non-commercial use. In so far as the contents on this site were not created by the operator, the
copyrights
of third parties are respected. In particular, third-party content is marked as such. Should you become aware of a
copyright infringement, please inform us accordingly. Upon notification of violations, we will remove such
contents
immediately.
</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -1,11 +1,11 @@
{% extends "template.html" %}
{% block content %}
<div class="container">
<div class="blog">
<h1>Feed</h1><br>
{% autoescape off %}
{{ content_string }}
{% endautoescape %}
</div>
<div class="blog">
<h1>Index</h1><br>
{% autoescape off %}
{{ content_string }}
{% endautoescape %}
</div>
</div>
{% endblock %}

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /static/

View File

@@ -4,9 +4,9 @@
<channel>
<title>{{ title }}</title>
<description>{{ description }}</description>
<language>{{ language }}</language>
<link>{{ website }}</link>
<atom:link href="{{ website }}{{ url_for('feed') }}" rel="self" type="application/rss+xml"/>
<language>en-us</language>
<link>{{ website }}/feed.xml</link>
<atom:link href="/feed.xml" rel="self" type="application/rss+xml" />
{% autoescape off %}
{{ content_string }}

View File

@@ -1,19 +0,0 @@
{% 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 %}

View File

@@ -1,10 +1,10 @@
{% extends "template.html" %}
{% block content %}
<div class="container">
<div class="standalone">
{% autoescape off %}
{{ content_string }}
{% endautoescape %}
</div>
<div class="standalone">
{% autoescape off %}
{{ content_string }}
{% endautoescape %}
</div>
</div>
{% endblock %}

View File

@@ -1,46 +1,32 @@
<!DOCTYPE html>
<html lang={% if language=="de-de" %}de{% else %}en{% endif %}>
<html>
<head>
<title>{{ title }}</title>
<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')}}">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
{{ font_awesome.load_js() }}
<title>{{ title }}</title>
<link href="{{ url_for('static', filename='css/' + style + '.css') }}" rel="stylesheet" type="text/css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width" initial-scale=1.0>
</head>
<body>
<!-- Menu -->
<div class="main-menu-dropdown">
<a href="{{ url_for('index') }}">
<img class="logo" src="{{ url_for('static', filename='graphics/logo.png') }}" alt="Logo von
Mittelerde mit Marten. Zu sehen sind 3 'M's mit Serifen, wobei die beiden Äußeren etwas
kleiner sind">
{{ stitle }}
</a>
<input type="checkbox" id="main-menu-check">
<label for="main-menu-check" class="show-menu">&#9776;</label>
<div class="main-menu">
<a href="{{ url_for('index') }}">Blog</a>
<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>
<!-- Menu -->
<div class="main-menu-dropdown">
<!-- <img class="logo" src="/static/images/logo.png"> -->
<span>{{ title }}</span>
<input type="checkbox" id="main-menu-check">
<label for="main-menu-check" class="show-menu">&#9776;</label>
<div class="main-menu">
<a href="{{ url_for('index') }}">Blog</a>
<a href="{{ url_for('archive') }}">Archive</a>
<label for="main-menu-check" class="hide-menu">X</label>
</div>
</div>
</div>
<!-- Menu -->
<!-- Content -->
{% block content %}
{% endblock %}
<!-- Content -->
<footer>
<div class="center">
<a href="{{ url_for('imprint') }}">{% if language=="de-de" %}Impressum und Kontakt{% else %}Imprint and Contact{%
endif %}</a>.<br>
<a href="{{ url_for('feed') }}">{% if language=="de-de" %}RSS-Feed{% else %}RSS feed{% endif %}</a>.<br>
Made with <a href="https://github.com/tiyn/beaker-blog">Beaker Blog</a>.
</div>
</footer>
<!-- Menu -->
<!-- Content -->
{% block content %}
{% endblock %}
<!-- Content -->
<footer>
<div class="center">
Made with <a href="https://github.com/tiyn/beaker-blog">Beaker Blog </a>.
</div>
</footer>
</body>
</html>