import markdown
from os import listdir
from markdown.extensions.toc import TocExtension
+import re
+import mmap
app = Flask(__name__)
md = markdown.Markdown(extensions = ['meta', 'tables', 'footnotes', TocExtension(title = "Table of contents")])
class Page():
- def __init__(
- self,
- title: str = "Unamed page",
- abstract: str = "...",
- keywords: list = ["Undefined"],
- date: str = "n.d.",
- content: str = "",
- template: str = "page.html"
- ):
-
- self.title = title
- self.abstract = abstract
- self.keywords = keywords
- self.date = date
- self.content = content
-
- def from_metadata(self, metadata):
- if "title" in metadata: self.title = metadata["title"].title()
- if "abstract" in metadata: self.abstract = metadata["abstract"]
- if "keywords" in metadata: self.keywords = [x.strip().title() for x in metadata["keywords"].split(",")]
- if "date" in metadata: self.date = metadata["date"]
-
- def make(self):
- format_keywords = " - ".join([f"<a href='/categories?name={key.lower()}'>{key}</a>" for key in self.keywords])
-
- page = render_template(
- "page.html",
- title = self.title,
- date = self.date,
- abstract = self.abstract,
- keywords = format_keywords,
- content = self.content
- )
-
- return Response(page, mimetype="text/html")
+ def __init__(
+ self,
+ title: str = "Unamed page",
+ abstract: str = "",
+ keywords: list = ["Undefined"],
+ date: str = "n.d.",
+ content: str = "",
+ template: str = "page.html"
+ ):
+
+ self.title = title
+ self.abstract = abstract
+ self.keywords = keywords
+ self.date = date
+ self.content = content
+
+ def from_metadata(self, metadata):
+ if "title" in metadata: self.title = metadata["title"].title()
+ if "abstract" in metadata: self.abstract = metadata["abstract"]
+ if "keywords" in metadata: self.keywords = [x.strip().title() for x in metadata["keywords"].split(",")]
+ if "date" in metadata: self.date = metadata["date"]
+
+ def make(self):
+ format_keywords = " - ".join([f"<a href='/categories?name={key.lower()}'>{key}</a>" for key in self.keywords])
+
+ page = render_template(
+ "page.html",
+ title = self.title,
+ date = self.date,
+ abstract = self.abstract,
+ keywords = format_keywords,
+ content = self.content
+ )
+
+ return Response(page, mimetype="text/html")
@app.route("/")
def homepage():
- page = Page(
- title = "Home",
- abstract = "",
- content = "",
- template = "home.html"
- )
- return page.make()
+ page = Page(
+ title = "Home",
+ abstract = "Blog and portfolio of Will Greenwood.",
+ keywords = [],
+ date = "",
+ template = "home.html"
+ )
+ return page.make()
+
+def get_search():
+ names = [re.sub('[\W_]+', '', name) for name in request.args.get("name").split()]
+
+ name_string = "</em>' '<em>".join(names)
+ content = f"<h1>Search '<em>{name_string}</em>'</h1><ul>"
+
+ for file_name in listdir("./pages"):
+ with open(f"./pages/{file_name}", "r") as f:
+ s = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
+ for name in names:
+ i = s.find(str.encode(name))
+ if i != -1:
+ post_content = f.read()
+
+ post_content = re.sub('<.*>', ' ', post_content)
+ post_content = re.sub('.[^\s\d\w]', ' ', post_content)
+ post_content = re.sub(' ', ' ', post_content)
+
+ i = post_content.find(name)
+ excerpt = "'..." + post_content[i-50:i] + "</em><b>" + post_content[i:i+len(name)] + "</b><em>" + post_content[i+len(name):i+50] + "...'"
+
+ lines = post_content.split("\n")
+ title = "Unamed page"
+ for line in lines:
+ line = line.split()
+ print(line)
+ if len(line) == 2 and line[0] == "title":
+ title = line[1].strip().title()
+ elif len(line) == 2: pass
+ else: break
+
+ content += f"<li><a href='/{file_name[:-3]}'>{title}</a> - <em>{excerpt}</em></li>"
+ break
+
+ content += "</ul>"
+
+ page = Page(
+ title = "Post search",
+ date = "",
+ keywords = [],
+ content = content
+ )
+ return page.make()
@app.route("/categories")
def get_categories():
- name = request.args.get('name')
- index = {}
-
- if name == "all":
- return redirect("/categories")
-
- for file_name in listdir("./pages"):
- keywords = ["Undefined"]
- title = "Untitled post"
- abstract = "..."
- with open(f"./pages/{file_name}", "r") as f:
- while keywords == ["Undefined"]:
- line = f.readline().split(":")
- if len(line) == 2:
- key, value = line
- match key:
- case "title":
- title = value.strip().title()
- case "abstract":
- abstract = value.strip()
- case "keywords":
- keywords = [x.strip() for x in value.split(",")]
- else:
- break
-
- entry = {"title": title, "abstract": abstract, "path": f"/{file_name[:-3]}"}
- for key in keywords:
- if key in index:
- index[key] += [entry]
- elif key == name or name is None:
- index.update({key: [entry]})
-
- content = ""
- for c in index:
- content += f"<h1>{c}</h1><ul>"
-
- for post in index[c]:
- content += f"<li><a href='{post['path']}'>{post['title']} - <em>{abstract}</em></a></li>"
-
- content += "</ul>"
-
- page = Page(
- title = "Keywords",
- keywords = [c.title() for c in index] + ["All"],
- content = content
- )
- return page.make()
+ name = request.args.get("name")
+ index = {}
+
+ if name == "all":
+ return redirect("/categories")
+
+ for file_name in listdir("./pages"):
+ keywords = ["Undefined"]
+ title = "Untitled post"
+ abstract = "..."
+ with open(f"./pages/{file_name}", "r") as f:
+ while keywords == ["Undefined"]:
+ line = f.readline().split(":")
+ if len(line) == 2:
+ key, value = line
+ match key:
+ case "title":
+ title = value.strip().title()
+ case "abstract":
+ abstract = value.strip()
+ case "keywords":
+ keywords = [x.strip() for x in value.split(",")]
+ else:
+ break
+
+ entry = {"title": title, "abstract": abstract, "path": f"/{file_name[:-3]}"}
+ for key in keywords:
+ if key in index:
+ index[key] += [entry]
+ elif key == name or name is None:
+ index.update({key: [entry]})
+
+ content = ""
+ for c in index:
+ content += f"<h1>{c}</h1><ul>"
+
+ for post in index[c]:
+ content += f"<li><a href='{post['path']}'>{post['title']}</a> - <em>{abstract}</em></li>"
+
+ content += "</ul>"
+
+ page = Page(
+ title = "Keyword search",
+ abstract = "Click on a keyword to see all pages tagged with that keyword.",
+ date = "",
+ keywords = [c.title() for c in index] + ["All"],
+ content = content
+ )
+ return page.make()
@app.route("/<string:title>")
def generate_page(title):
- title = title.replace("/", "")
- try:
- with open(f"pages/{title}.md", "r") as f:
- html = md.convert(f.read())
- metadata = {k: v[0] for k, v in md.Meta.items()}
- md.reset()
+ title = title.replace("/", "")
+ try:
+ with open(f"pages/{title}.md", "r") as f:
+ html = md.convert(f.read())
+ metadata = {k: v[0] for k, v in md.Meta.items()}
+ md.reset()
- page = Page(content = html)
- page.from_metadata(metadata)
- return page.make()
+ page = Page(content = html)
+ page.from_metadata(metadata)
+ return page.make()
- except FileNotFoundError:
- page = Page(title = "404", abstract = "Page not found!")
- return page.make()
+ except FileNotFoundError:
+ page = Page(title = "404", abstract = "Page not found!")
+ return page.make()
if __name__ == "__main__":
- app.run(host='127.0.0.1', port=5000)
+ app.run(host='127.0.0.1', port=5000)
--- /dev/null
+title: A bottle to smash on the hull of my boat
+abstract: My boat is a small mini-project dynamic blog. My breakaway bottle is sticky to the touch, filled with tap-water.
+keywords: programming,meta
+date: 29/10/2024
+
+Last year I decided to setup a small blog. Knowing full well that I could build my own, but also that it would be a huge time-sink that I really didnt have the time for, I decided to simply use Blogspot. It's surpisingly good and uses rich-text levels of formatting to produce slightly disapointing looking pages. My usual lean towards minimalism and accessibility was completely undermined by the control I had over how the blog looked. So it has sinc upset me, as somone so keen on self-hosting, that my blog (and emails) is managed by Google.
+
+# One year on and It's been a huge time-sink.
+
+Well about a week. But it's more difficult now. I'm at that sweet spot in learning a skill where i'm just good enough to basically make whatever I might want, but I still mess up enough to keep it interesting. It makes such a strong pull I have been able to think about very little that *isn't* dynamic page generation.
+
+I am, yet again, going with minimalism, combined with the nomenclature of acedemic writing.
+
+This is mirrored in both the approach i've taken to the various important desisions when building the generator, as well as words and styling of the pages. For example, the title / abstract / keywords / content structure, whether accurate to acedemia or not, reflects some kind of constant confort-structure. Not only this but it serves, as in acedemia, to give the reader some taste of the body of text before the continue. Combined with keywords and a future search system, this provides both this confort-structure and a robust way of narrowing down your reading. These features are handeled when generating every page, and although it is possible to exclude the date when not neccicary (such as the keyword search page), this must be done consiously and manually.
+
+This kind of structure (written out by hand, by me, every time) ensures that each page is sorted and labeled properly in a way that Blogger made barely dooable and also unplesant.
+
+2 semi-structural elements also included is the table of contents (TOC) and footnotes. Although the TOC could be programatically included within the generator, I made the desision to require it to be consiously included within the page. This not only adds weight to larger pages, requiring a TOC, but also allows page-by-page desision on where that TOC should be placed.
+
+I've also chosen to move away from any concept of time. With gaps between posts unpredictable, I would rather it not be immediately obvious that I did 2 posts a day for a month and then stopped for 5. Not that date-of-writing will not be open information, but it will not be sorted by the metric, and will not be ranked anywhere by most recent.
+
+I would describe this approach as:
+
+# Just enough.
+
+There are just enough features, and if I really need anything else, it can be added. If search proves too difficult, or breaks with the approach, it can be left out. Sites are simple HTML, with few stylings, all preloaded on hover for speed. And the "just enough" amount of marking-down for this minimalist writing is markdown. Future-proof should I ever choose to migrate again, lightweight should I keep writing and it can even do tables! The code for the generator is open-source and can be see at the repo. Over the next few weeks, I will atempt to migrate the old blog over to this system.
+
+Thanks,
+
+Will