#!/usr/bin/env python3
""" ================================= |
| staticoso |
| Just a simple Static Site Generator |
| |
| Licensed under the AGPLv3 license |
| Copyright (C) 2022, OctoSpacc |
| ================================= """
import argparse
import json
from Libs import htmlmin
import os
import shutil
from ast import literal_eval
from Libs.bs4 import BeautifulSoup
#from html.parser import HTMLParser
from markdown import Markdown
from pathlib import Path
Extensions = {
'Pages': ('md', 'pug')}
"""
class HTMLParser(HTMLParser):
Tags = []
def handle_starttag(self, tag, attrs):
#print(tag, attrs)
#self.Tags += [tag, attrs]
self.Tags += [[tag,attrs]]
def handle_data(self, data):
#print(data)
if self.Tags:
#self.Tags += [data]
self.Tags[-1] += [data]
def Clean(self):
self.Tags = []
self.reset()
self.close()
"""
def ReadFile(p):
try:
with open(p, 'r') as f:
return f.read()
except Exception:
print("Error reading file {}".format(p))
return None
def WriteFile(p, c):
try:
with open(p, 'w') as f:
f.write(c)
return True
except Exception:
print("Error writing file {}".format(p))
return False
def LoadLocale(Lang):
Lang = Lang + '.json'
Folder = os.path.dirname(os.path.abspath(__file__)) + '/../Locale/'
File = ReadFile(Folder + Lang)
if File:
return json.loads(File)
else:
return json.loads(ReadFile(Folder + 'en.json'))
def StripExt(Path):
return ".".join(Path.split('.')[:-1])
def ResetPublic():
try:
shutil.rmtree('public')
except FileNotFoundError:
pass
def GetLevels(Path, AsNum=False, Add=0, Sub=0):
n = Path.count('/') + Add - Sub
return n if AsNum else '../' * n
def UndupeStr(Str, Known, Split):
while Str in Known:
Sections = Title.split(Split)
try:
Sections[-1] = str(int(Sections[-1]) + 1)
except ValueError:
Sections[-1] = Sections[-1] + str(Split) + '2'
Str = Split.join(Sections)
return Str
def DashifyStr(s, Limit=32):
Str, lc = '', Limit
for c in s[:Limit].replace(' ','-').replace(' ','-'):
if c.lower() in '0123456789qwfpbjluyarstgmneiozxcdvkh-':
Str += c
return '-' + Str
def DashifyTitle(Title, Done=[]):
return UndupeStr(DashifyStr(Title), Done, '-')
def GetTitle(Meta, Titles, Prefer='MetaTitle'):
if Prefer == 'BodyTitle':
Title = Titles[0].lstrip('#') if Titles else Meta['Title'] if Meta['Title'] else 'Untitled'
elif Prefer == 'MetaTitle':
Title = Meta['Title'] if Meta['Title'] else Titles[0].lstrip('#') if Titles else 'Untitled'
elif Prefer == 'HTMLTitle':
Title = Meta['HTMLTitle'] if Meta['HTMLTitle'] else Meta['Title'] if Meta['Title'] else Titles[0].lstrip('#') if Titles else 'Untitled'
if Meta['Type'] == 'Post':
# TODO: This hardcodes my blog name, bad, will fix asap
Title += ' - blogoctt'
return Title
def GetDescription(Meta, BodyDescription, Prefer='MetaDescription'):
if Prefer == 'BodyDescription':
Description = BodyDescription if BodyDescription else Meta['Description'] if Meta['Description'] else ''
elif Prefer == 'MetaDescription':
Description = Meta['Description'] if Meta['Description'] else BodyDescription if BodyDescription else ''
return Description
def GetImage(Meta, BodyImage, Prefer='MetaImage'):
if Prefer == 'BodyImage':
Image = BodyImage if BodyImage else Meta['Image'] if Meta['Image'] else ''
elif Prefer == 'MetaImage':
Image = Meta['Image'] if Meta['Image'] else BodyImage if BodyImage else ''
return Image
def MakeLinkableTitle(Line, Title, DashTitle, Type):
if Type == 'md':
Index = Title.split(' ')[0].count('#')
return '
[HTML:Category:{}]'.format(i), Categories[i]) return Template def FileToStr(File, Truncate=''): return str(File)[len(Truncate):] def OrderPages(Old): New = [] NoOrder = [] Max = 0 for i,e in enumerate(Old): Curr = e[3]['Order'] if Curr: if Curr > Max: Max = Curr else: NoOrder += [e] for i in range(Max+1): New += [[]] for i,e in enumerate(Old): Curr = e[3]['Order'] if Curr: New[Curr] = e while [] in New: New.remove([]) return New + NoOrder def GetHTMLPagesList(Pages, SiteRoot, PathPrefix, Type='Page', Category=None): List, ToPop, LastParent = '', [], [] IndexPages = Pages.copy() for e in IndexPages: if e[3]['Index'] == 'False' or e[3]['Index'] == 'None': IndexPages.remove(e) for i,e in enumerate(IndexPages): if e[3]['Type'] != Type: ToPop += [i] ToPop = RevSort(ToPop) for i in ToPop: IndexPages.pop(i) if Type == 'Page': IndexPages = OrderPages(IndexPages) for File, Content, Titles, Meta in IndexPages: if Meta['Type'] == Type and (Meta['Index'] != 'False' or Meta['Index'] != 'None') and GetTitle(Meta, Titles, Prefer='HTMLTitle') != 'Untitled' and (not Category or Category in Meta['Categories']): n = File.count('/') + 1 if n > 1: CurParent = File.split('/')[:-1] for i,s in enumerate(CurParent): if LastParent != CurParent: LastParent = CurParent Levels = '- ' * (n-1+i) if File[:-3].endswith('index.'): Title = MakeListTitle(File, Meta, Titles, 'HTMLTitle', SiteRoot, PathPrefix) else: Title = CurParent[n-2+i] List += Levels + Title + '\n' if not (n > 1 and File[:-3].endswith('index.')): Levels = '- ' * n Title = MakeListTitle(File, Meta, Titles, 'HTMLTitle', SiteRoot, PathPrefix) List += Levels + Title + '\n' return Markdown().convert(List) def DelTmp(): for Ext in Extensions['Pages']: for File in Path('public').rglob('*.{}'.format(Ext)): os.remove(File) def RevSort(List): List.sort() List.reverse() return List def DoMinify(HTML): return htmlmin.minify( input=HTML, remove_comments=True, remove_empty_space=True, remove_all_empty_space=False, reduce_empty_attributes=True, reduce_boolean_attributes=True, remove_optional_attribute_quotes=True, convert_charrefs=True, keep_pre=True) def MakeSite(TemplatesText, PartsText, ContextParts, ContextPartsText, SiteRoot, FolderRoots, Reserved, Locale, Minify, Sorting): PagesPaths, PostsPaths, Pages, Categories = [], [], [], {} for Ext in Extensions['Pages']: for File in Path('Pages').rglob('*.{}'.format(Ext)): PagesPaths += [FileToStr(File, 'Pages/')] for File in Path('Posts').rglob('*.{}'.format(Ext)): PostsPaths += [FileToStr(File, 'Posts/')] # TODO: Slim this down? if Sorting['Pages'] == 'Standard': PagesPaths.sort() elif Sorting['Pages'] == 'Inverse': PagesPaths = RevSort(PagesPaths) if Sorting['Posts'] == 'Standard': PostsPaths.sort() elif Sorting['Posts'] == 'Inverse': PostsPaths = RevSort(PostsPaths) for Type in ['Page', 'Post']: if Type == 'Page': Files = PagesPaths elif Type == 'Post': Files = PostsPaths for File in Files: Content, Titles, Meta = PreProcessor('{}s/{}'.format(Type, File), SiteRoot) if Type != 'Page': File = Type + 's/' + File if not Meta['Type']: Meta['Type'] = Type Pages += [[File, Content, Titles, Meta]] for Category in Meta['Categories']: Categories.update({Category:''}) PugCompileList(Pages) for Category in Categories: Categories[Category] = GetHTMLPagesList( Pages=Pages, SiteRoot=SiteRoot, PathPrefix=GetLevels(Reserved['Categories']), # This hardcodes paths, TODO make it somehow guess the path for every page containing the [HTML:Category] macro Type='Post', Category=Category) for File, Content, Titles, Meta in Pages: HTMLPagesList = GetHTMLPagesList( Pages=Pages, SiteRoot=SiteRoot, PathPrefix=GetLevels(File), Type='Page') PagePath = 'public/{}.html'.format(StripExt(File)) if File.endswith('.md'): Content = Markdown().convert(Content) elif File.endswith('.pug'): Content = ReadFile(PagePath) HTML = PatchHTML( Template=TemplatesText[Meta['Template']], PartsText=PartsText, ContextParts=ContextParts, ContextPartsText=ContextPartsText, HTMLPagesList=HTMLPagesList, PagePath=PagePath[len('public/'):], Content=Content, Titles=Titles, Meta=Meta, SiteRoot=SiteRoot, FolderRoots=FolderRoots, Categories=Categories, Locale=Locale, Reserved=Reserved) if Minify != 'False' and Minify != 'None': HTML = DoMinify(HTML) WriteFile(PagePath, HTML) def SetReserved(Reserved): for i in ['Categories']: if i not in Reserved: Reserved.update({i:i}) for i in Reserved: if not Reserved[i].endswith('/'): Reserved[i] = '{}/'.format(Reserved[i]) return Reserved def SetSorting(Sorting): Default = { 'Pages':'Standard', 'Posts':'Inverse'} for i in Default: if i not in Sorting: Sorting.update({i:Default[i]}) return Sorting def Main(Args): ResetPublic() if os.path.isdir('Pages'): shutil.copytree('Pages', 'public') if os.path.isdir('Posts'): shutil.copytree('Posts', 'public/Posts') MakeSite( TemplatesText=LoadFromDir('Templates', '*.html'), PartsText=LoadFromDir('Parts', '*.html'), ContextParts=literal_eval(Args.ContextParts) if Args.ContextParts else {}, ContextPartsText=LoadFromDir('ContextParts', '*.html'), SiteRoot=Args.SiteRoot if Args.SiteRoot else '/', FolderRoots=literal_eval(Args.FolderRoots) if Args.FolderRoots else {}, Reserved=SetReserved(literal_eval(Args.ReservedPaths) if Args.ReservedPaths else {}), Locale=LoadLocale(Args.SiteLang if Args.SiteLang else 'en'), Minify=Args.Minify if Args.Minify else 'None', Sorting=SetSorting(literal_eval(Args.ContextParts) if Args.ContextParts else {})) DelTmp() os.system("cp -R Assets/* public/") if __name__ == '__main__': Parser = argparse.ArgumentParser() Parser.add_argument('--Minify', type=str) Parser.add_argument('--Sorting', type=str) Parser.add_argument('--SiteLang', type=str) Parser.add_argument('--SiteRoot', type=str) Parser.add_argument('--FolderRoots', type=str) Parser.add_argument('--ContextParts', type=str) Parser.add_argument('--ReservedPaths', type=str) Main( Args=Parser.parse_args())