initial commit
This commit is contained in:
parent
46328a92a8
commit
c449f9d9ac
12 changed files with 457 additions and 4 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -162,3 +162,4 @@ cython_debug/
|
|||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
output/
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -220,7 +220,7 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
python-webgen
|
||||
Copyright (C) 2025 ClariSys
|
||||
Copyright (C) 2025 ChloeCat
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
# python-webgen
|
||||
|
||||
a set of python scripts to generate a blog-style website from a set of markdown pages.
|
||||
a set of python scripts and html templates to generate blog-style websites
|
||||
|
||||
|
||||
---
|
||||
## Getting Started
|
||||
|
||||
Welcome to your new KitsuDev Repo, ClariSys! Feel free to make this place your own!
|
||||
Welcome to your new KitsuDev Repo, ChloeCat! Feel free to make this place your own!
|
||||
|
||||
## Make your first push!
|
||||
Once you're ready to push your files, use these commands to get to your spot!
|
||||
```bash
|
||||
git remote add origin https://kitsunes.dev/ClariSys/python-webgen.git
|
||||
git remote add origin https://kitsunes.dev/ChloeCat/python-webgen.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```
|
||||
|
|
23
generate.py
Normal file
23
generate.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
# Argument Parser
|
||||
argparser = argparser = argparse.ArgumentParser(description = "Generate a .md file with a pre-filled title, date, etc.")
|
||||
argparser.add_argument("output", help = "The location to generate the template to")
|
||||
argparser.add_argument("--title", help = "The title of the article (default: Blog Post)", default = "Blog Post")
|
||||
argparser.add_argument("--author", help = "The name of the author (default: Blog Author)", default = "Blog Author")
|
||||
argparser.add_argument("-v", help = "more output", action = "store_true", dest = "verbose")
|
||||
args = argparser.parse_args()
|
||||
|
||||
date = datetime.now().astimezone().strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||
template = "+++\ntitle = '{}'\ndate = {}\nauthor = '{}'\ndraft = True\n+++"
|
||||
output = template.format(args.title, date, args.author)
|
||||
if args.verbose: print(template)
|
||||
|
||||
# time to write the file :3
|
||||
with open(args.output, "w+") as file:
|
||||
if args.verbose: print("Writing to {}...".format(args.output))
|
||||
file.write(output)
|
||||
print("Finished writing file to {}".format(args.output))
|
||||
|
||||
print("Done.")
|
89
parser.py
Normal file
89
parser.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# import modules
|
||||
import markdown as md
|
||||
|
||||
# function definitions
|
||||
|
||||
# convertFile(): takes in a path to a markdown file and
|
||||
# outputs an html snippet.
|
||||
def convertFile(infilePath):
|
||||
rawfile = open(infilePath)
|
||||
output = ""
|
||||
isComment = False
|
||||
for line in rawfile:
|
||||
if "+++" in line:
|
||||
if not isComment:
|
||||
isComment = True
|
||||
continue
|
||||
else:
|
||||
isComment = False
|
||||
continue
|
||||
if isComment:
|
||||
continue
|
||||
else:
|
||||
output = output + line
|
||||
output = md.markdown(output)
|
||||
return output
|
||||
|
||||
# getMetadata(): returns the metadata as a dictionary
|
||||
def getMetadata(infilePath):
|
||||
rawfile = open(infilePath)
|
||||
isComment = False
|
||||
metadata = {}
|
||||
for line in rawfile:
|
||||
if "+++" in line:
|
||||
if not isComment:
|
||||
isComment = True
|
||||
continue
|
||||
else:
|
||||
isComment=False
|
||||
break
|
||||
if isComment:
|
||||
key = line.split(" = ")[0]
|
||||
value = line.split(' = ')[1][:-1]
|
||||
metadata[key] = value
|
||||
if metadata[key] == "true":
|
||||
metadata[key] = True
|
||||
if metadata[key] == "false":
|
||||
metadata[key] = False
|
||||
if isinstance(metadata[key], str):
|
||||
metadata[key] = metadata[key][1:-1]
|
||||
return metadata
|
||||
|
||||
# getTitle(): returns the title from the metadata
|
||||
def getTitle(infilePath):
|
||||
return getMetadata(infilePath)['title']
|
||||
|
||||
def convertDate(timestamp):
|
||||
dateStr = ""
|
||||
newTime = timestamp.split('T')[0]
|
||||
date = newTime.split("-")
|
||||
day = date[2].lstrip("0")
|
||||
year = "20" + date[0][-2:]
|
||||
match date[1]:
|
||||
case "01":
|
||||
month = "January"
|
||||
case "02":
|
||||
month = "February"
|
||||
case "03":
|
||||
month = "March"
|
||||
case "04":
|
||||
month = "April"
|
||||
case "05":
|
||||
month = "May"
|
||||
case "06":
|
||||
month = "June"
|
||||
case "07":
|
||||
month = "July"
|
||||
case "08":
|
||||
month = "August"
|
||||
case "09":
|
||||
month = "September"
|
||||
case "10":
|
||||
month = "October"
|
||||
case "11":
|
||||
month = "November"
|
||||
case "12":
|
||||
month = "December"
|
||||
case _:
|
||||
month = ""
|
||||
return "{} {}, {}".format(day, month, year)
|
35
stitcher.py
Normal file
35
stitcher.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
import parser
|
||||
|
||||
# function defs
|
||||
|
||||
def generateBody(infilePath):
|
||||
title = parser.getTitle(infilePath)
|
||||
body = parser.convertFile(infilePath)
|
||||
timestamp = parser.getMetadata(infilePath)["date"]
|
||||
date = parser.convertDate(timestamp)
|
||||
final = "<h1> {} </h1>\n<h3 class=\"date\">{}</h3>\n<hr/>\n{}".format(title, date, body)
|
||||
return final
|
||||
|
||||
def createArticle(infilePath, templatePath, mainTitle = ""):
|
||||
if mainTitle != "":
|
||||
mainTitle = " | " + mainTitle
|
||||
template = open("{}article.html".format(templatePath), 'r').read()
|
||||
navbar = open("{}navbar.html".format(templatePath), 'r').read()
|
||||
foot = open("{}footer.html".format(templatePath), 'r').read()
|
||||
body = generateBody(infilePath)
|
||||
articleTitle = parser.getTitle(infilePath) + mainTitle
|
||||
article = template.format(title = articleTitle, main = body, nav = navbar, footer = foot)
|
||||
return article
|
||||
|
||||
def createMainPage(titles, templatePath, main_title):
|
||||
template = open("{}main.html".format(templatePath), 'r').read()
|
||||
navbar = open("{}navbar.html".format(templatePath), 'r').read()
|
||||
foot = open("{}footer.html".format(templatePath), 'r').read()
|
||||
articles = ""
|
||||
fstring = '<article><h2><a href = "{}">{}</a></h2></article>\n'
|
||||
for title in titles:
|
||||
articles = articles + fstring.format(title[1], title[0])
|
||||
final = template.format(title = main_title, toc = articles, nav = navbar, footer = foot)
|
||||
return final
|
||||
|
||||
|
22
templates/article.html
Normal file
22
templates/article.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title> {title} </title>
|
||||
<link rel="stylesheet" href="/stylesheet.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
{nav}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{main}
|
||||
</main>
|
||||
<footer>
|
||||
{footer}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
13
templates/footer.html
Normal file
13
templates/footer.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
Contact me:<br/>
|
||||
Sharkey <a href = "https://catwithaclari.net/@Chloe">@Chloe@catwithaclari.net</a> •
|
||||
Email <a href = "mailto:chloe@catwitaclari.net">chloe@catwithaclari.net</a><br/>
|
||||
<a href="https://blog.catwithaclari.net/"><img src="/button.png" alt="A pink-purple-blue gradient with the text ChloeCat written in a fancy script font"/></a>
|
||||
<a id="w3chtml" href="https://validator.w3.org/nu/?doc=https%3A%2F%2Fblog.catwithaclari.net%2F"><img src="https://www.w3.org/Icons/valid-html401.png" alt="WC3 HTML 4.01 Validated"/></a>
|
||||
<a id="w3ccss" href="https://jigsaw.w3.org/css-validator/validator?uri=https%253A%252F%252Fblog.catwithaclari.net"><img src="https://www.w3.org/Icons/valid-css.png" alt="W3C CSS Validated"/></a>
|
||||
<script>
|
||||
href = document.location;
|
||||
w3chtml = document.getElementById("w3chtml");
|
||||
w3ccss = document.getElementById("w3ccss");
|
||||
w3chtml.href = "https://validator.w3.org/nu/?doc=" + href;
|
||||
w3ccss.href = "https://jigsaw.w3.org/css-validator/validator?uri=" + href;
|
||||
</script>
|
23
templates/main.html
Normal file
23
templates/main.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title> {title} </title>
|
||||
<link rel="stylesheet" href="/stylesheet.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
{nav}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h1>Welcome to {title}!</h1>
|
||||
{toc}
|
||||
</main>
|
||||
<footer>
|
||||
{footer}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
19
templates/navbar.html
Normal file
19
templates/navbar.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<div class = "logo"> <a href = "https://blog.catwithaclari.net">ChloeCat</a> </div><div class = sep> | </div>
|
||||
<div class="theme-switch">
|
||||
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
|
||||
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
134
templates/stylesheet.css
Executable file
134
templates/stylesheet.css
Executable file
|
@ -0,0 +1,134 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Comfortaa:wght@300..700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg-color: rgb(39, 08, 46);
|
||||
--fg-color: rgb(255, 186, 255);
|
||||
}
|
||||
|
||||
#sun {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--fg-color: rgb(39, 08, 46);
|
||||
--bg-color: rgb(255, 186, 255);
|
||||
}
|
||||
|
||||
#moon {
|
||||
display: none;
|
||||
}
|
||||
#sun {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-switch button {
|
||||
background: none;
|
||||
border: none;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
stroke: var(--fg-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: lch(from var(--fg-color) calc(l - 20) c h);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
font-size: 150%;
|
||||
align-items: center;
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: var(--fg-color);
|
||||
border: solid lch(from var(--bg-color) calc(l + 15) c h);
|
||||
border-radius: 10px;
|
||||
background-color: lch(from var(--bg-color) calc(l + 10) c h);
|
||||
font-family: 'Caveat', sans-serif;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
text-decoration: none;
|
||||
margin: 15px;
|
||||
}
|
||||
.logo a:visited{
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.sep {
|
||||
font-size: 175%;
|
||||
font-family: 'Fira Code', 'Noto Sans Mono', monospace;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.theme-switch {
|
||||
margin-right: 25px
|
||||
}
|
||||
|
||||
article{
|
||||
border: solid lch(from var(--bg-color) calc(l + 8) c h);
|
||||
border-radius: 15px;
|
||||
margin: 10px;
|
||||
background-color: lch(from var(--bg-color) calc(l + 5) c h);
|
||||
}
|
||||
|
||||
article h2{
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 300%;
|
||||
}
|
||||
|
||||
hr {
|
||||
max-width: 720px;
|
||||
margin-left: 0px;
|
||||
margin-right: 20%;
|
||||
color: var(--fg-color)
|
||||
}
|
||||
|
||||
h3 .date {
|
||||
font-size: 80%
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
font-family: 'Comfortaa', 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Fira Code", monospace;
|
||||
border: solid lch(from var(--bg-color) calc(l + 15) c h);
|
||||
border-radius: 5px;
|
||||
background-color: lch(from var(--bg-color) calc(l + 5) c h);
|
||||
}
|
||||
|
||||
main * {
|
||||
max-width: 720px;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
footer {
|
||||
justify-self: center;
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
margin-top: 25px;
|
||||
}
|
94
webgen.py
Normal file
94
webgen.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
# imports
|
||||
import parser
|
||||
import stitcher
|
||||
import argparse
|
||||
import os
|
||||
from bs4 import BeautifulSoup as bs
|
||||
|
||||
# argument parser
|
||||
argparser = argparse.ArgumentParser(description = "A simple set of python scripts to generate HTML files from a set of markdown files.")
|
||||
argparser.add_argument("input_path", help = "the location of the folder to parse")
|
||||
argparser.add_argument("output_path", help = "the location to output the finalised files")
|
||||
argparser.add_argument("--template_path", help = "the location of the template files (default: ./templates/)", default = "./templates/")
|
||||
argparser.add_argument("--css_file", help = "the location of a css file to copy to the output directory (default: ./templates/stylesheet.css)", default = "./templates/stylesheet.css")
|
||||
argparser.add_argument("--site_title", help = "the title shown on the main page (default: \"Blog\")", default="Blog")
|
||||
argparser.add_argument("--title_all", help = "add the main title to all articles", action="store_true", dest="allTitles")
|
||||
argparser.add_argument("-v", help = "more output", action = "store_true", dest = "verbose")
|
||||
args = argparser.parse_args()
|
||||
|
||||
# global vars
|
||||
articles = []
|
||||
root = args.input_path
|
||||
htmls = {}
|
||||
article_timestamps = {}
|
||||
titles = []
|
||||
|
||||
for rootDir, dirs, files in os.walk(args.input_path):
|
||||
for article in files:
|
||||
articles.append(article)
|
||||
|
||||
print("\nDiscovering articles...\n")
|
||||
for article in articles:
|
||||
article = root + article
|
||||
fstring = "Found {draft}{title} at {rootPath}{path}"
|
||||
isDraft = "[Draft] " if parser.getMetadata(article)['draft'] else ""
|
||||
print(fstring.format(title = parser.getTitle(article), rootPath = root, path = article, draft = isDraft))
|
||||
|
||||
print("\nGenerating HTML files from template...")
|
||||
for article in articles:
|
||||
article = root + article
|
||||
if parser.getMetadata(article)['draft'] == True:
|
||||
if args.verbose: print('"{}" is a draft, skipping...'.format(parser.getTitle(article)))
|
||||
continue
|
||||
if args.allTitles:
|
||||
htmls[article] = stitcher.createArticle(article, args.template_path, args.site_title)
|
||||
else:
|
||||
htmls[article] = stitcher.createArticle(article, args.template_path)
|
||||
if args.verbose:
|
||||
print(htmls[article], end='\n')
|
||||
|
||||
print("\nWriting files to {}...\n".format(args.output_path))
|
||||
for article in articles:
|
||||
workingDir = args.output_path + article[:-3] + "/"
|
||||
article = root + article
|
||||
if parser.getMetadata(article)['draft'] == True:
|
||||
if args.verbose: print('{} is a draft, skipping...'.format(article))
|
||||
continue
|
||||
html = bs(htmls[article]).prettify()
|
||||
if not os.path.exists(workingDir):
|
||||
os.makedirs(workingDir)
|
||||
if args.verbose: print("creating directory {}".format(workingDir))
|
||||
open(workingDir + "index.html", 'w').write(html)
|
||||
if args.verbose:
|
||||
print("wrote {} to {}".format(article, workingDir))
|
||||
print("Files written")
|
||||
|
||||
print("\nSorting non-draft articles by timestamp...\n")
|
||||
for article in articles:
|
||||
article = root + article
|
||||
if parser.getMetadata(article)['draft'] == True:
|
||||
if args.verbose: print('{} is a draft, skipping...'.format(parser.getTitle(article)))
|
||||
continue
|
||||
article_timestamps[article] = parser.getMetadata(article)['date']
|
||||
sorted_articles = sorted(article_timestamps.items(), key = lambda item: item[1], reverse = True)
|
||||
if args.verbose: print(sorted_articles)
|
||||
|
||||
print("\nGenerating main page...")
|
||||
for article in sorted_articles:
|
||||
article = article[0]
|
||||
title = parser.getTitle(article)
|
||||
link = article.split("/")[-1][:-3]
|
||||
titles.append([title, link])
|
||||
if args.verbose: print("added {} to titles list with link {}".format(title, link))
|
||||
mainPage = bs(stitcher.createMainPage(titles, args.template_path, args.site_title)).prettify()
|
||||
print("Generated main page")
|
||||
print("Writing main page to {}".format(args.output_path))
|
||||
open(args.output_path + "index.html", 'w').write(mainPage)
|
||||
print("Succesfully wrote main page to {}index.html".format(args.output_path))
|
||||
|
||||
print("\nCopying CSS file...")
|
||||
if args.verbose: print(args.css_file, "-->", args.output_path + "stylesheet.css")
|
||||
css = open(args.css_file, 'r').read()
|
||||
open(args.output_path + "stylesheet.css", 'w').write(css)
|
||||
print("Copied CSS file")
|
||||
print("\nAll done!")
|
Loading…
Add table
Add a link
Reference in a new issue