From c449f9d9ac242ba648bd1a8846312ec3822261f5 Mon Sep 17 00:00:00 2001 From: ClariSys Date: Tue, 11 Mar 2025 21:03:39 -0700 Subject: [PATCH] initial commit --- .gitignore | 1 + LICENSE | 2 +- README.md | 6 +- generate.py | 23 +++++++ parser.py | 89 ++++++++++++++++++++++++++ stitcher.py | 35 ++++++++++ templates/article.html | 22 +++++++ templates/footer.html | 13 ++++ templates/main.html | 23 +++++++ templates/navbar.html | 19 ++++++ templates/stylesheet.css | 134 +++++++++++++++++++++++++++++++++++++++ webgen.py | 94 +++++++++++++++++++++++++++ 12 files changed, 457 insertions(+), 4 deletions(-) create mode 100644 generate.py create mode 100644 parser.py create mode 100644 stitcher.py create mode 100644 templates/article.html create mode 100644 templates/footer.html create mode 100644 templates/main.html create mode 100644 templates/navbar.html create mode 100755 templates/stylesheet.css create mode 100644 webgen.py diff --git a/.gitignore b/.gitignore index ab3e8ce..07d2806 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +output/ diff --git a/LICENSE b/LICENSE index c783be6..e00417e 100644 --- a/LICENSE +++ b/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. diff --git a/README.md b/README.md index 466434a..4f49b39 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/generate.py b/generate.py new file mode 100644 index 0000000..8de9830 --- /dev/null +++ b/generate.py @@ -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.") diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..79ad561 --- /dev/null +++ b/parser.py @@ -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) diff --git a/stitcher.py b/stitcher.py new file mode 100644 index 0000000..4f82682 --- /dev/null +++ b/stitcher.py @@ -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 = "

{}

\n

{}

\n
\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 = '

{}

\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 + + diff --git a/templates/article.html b/templates/article.html new file mode 100644 index 0000000..9a63def --- /dev/null +++ b/templates/article.html @@ -0,0 +1,22 @@ + + + + + {title} + + + +
+ +
+
+ {main} +
+ + + + diff --git a/templates/footer.html b/templates/footer.html new file mode 100644 index 0000000..b470279 --- /dev/null +++ b/templates/footer.html @@ -0,0 +1,13 @@ +Contact me:
+Sharkey @Chloe@catwithaclari.net • +Email chloe@catwithaclari.net
+A pink-purple-blue gradient with the text ChloeCat written in a fancy script font +WC3 HTML 4.01 Validated +W3C CSS Validated + diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..96a4549 --- /dev/null +++ b/templates/main.html @@ -0,0 +1,23 @@ + + + + + {title} + + + +
+ +
+
+

Welcome to {title}!

+ {toc} +
+ + + + diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 0000000..17965a2 --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,19 @@ +
|
+
+ +
diff --git a/templates/stylesheet.css b/templates/stylesheet.css new file mode 100755 index 0000000..d49e862 --- /dev/null +++ b/templates/stylesheet.css @@ -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; +} diff --git a/webgen.py b/webgen.py new file mode 100644 index 0000000..fa4a680 --- /dev/null +++ b/webgen.py @@ -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!")