mirror of https://gitlab.com/octtspacc/staticoso
280 lines
8.6 KiB
Python
280 lines
8.6 KiB
Python
""" ================================== |
|
|
| This file is part of |
|
|
| staticoso |
|
|
| Just a simple Static Site Generator |
|
|
| |
|
|
| Licensed under the AGPLv3 license |
|
|
| Copyright (C) 2022-2023, OctoSpacc |
|
|
| ================================== """
|
|
|
|
import json, os, subprocess
|
|
from datetime import datetime
|
|
from multiprocessing import Pool, cpu_count
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from Modules.Globals import *
|
|
|
|
def SureList(e):
|
|
return e if type(e) == list else [e]
|
|
|
|
# Get base directory path of the staticoso program
|
|
def staticosoBaseDir():
|
|
return f"{os.path.dirname(os.path.abspath(__file__))}/../../"
|
|
|
|
def staticosoNameVersion():
|
|
Version = subprocess.run(('sh', '-c', 'git log | head -n 1'), stdout=subprocess.PIPE).stdout.decode().strip()
|
|
Version = ' v.' + Version.split(' ')[1][:8] if Version else ''
|
|
return f'staticoso{Version}'
|
|
|
|
def InSystemPath(Exec:str):
|
|
return subprocess.run(('sh', '-c', f'which {Exec}'), stdout=subprocess.PIPE).stdout.decode().strip()
|
|
|
|
def ReadFile(p:str, m:str='r'):
|
|
try:
|
|
with open(p, m) as f:
|
|
return f.read()
|
|
except Exception:
|
|
logging.error(f"Error reading file {p}")
|
|
return None
|
|
|
|
def WriteFile(p, c, m='w'):
|
|
try:
|
|
with open(p, m) as f:
|
|
return f.write(c)
|
|
except Exception:
|
|
logging.error(f"[E] Error writing file {p}")
|
|
return False
|
|
|
|
def AnyCaseGlob(Glob:str):
|
|
New = ''
|
|
for c in Glob:
|
|
New += '[%s%s]' % (c.lower(), c.upper()) if c.isalpha() else c
|
|
return New
|
|
|
|
def FileToStr(File:str, Truncate:str=''):
|
|
return str(File)[len(Truncate):]
|
|
|
|
# With shutil.copytree copy only folder struct, no files; https://stackoverflow.com/a/15664273
|
|
def IgnoreFiles(Dir:str, Files:list):
|
|
return [f for f in Files if os.path.isfile(os.path.join(Dir, f))]
|
|
|
|
def LoadFromDir(Dir:str, Matchs:list):
|
|
Contents = {}
|
|
Matchs = SureList(Matchs)
|
|
for Match in Matchs:
|
|
for File in Path(Dir).rglob(AnyCaseGlob(Match)):
|
|
if os.path.isfile(File):
|
|
Name = str(File)[len(Dir)+1:]
|
|
Contents.update({Name: ReadFile(File)})
|
|
return Contents
|
|
|
|
def mkdirps(Dir:str):
|
|
return Path(Dir).mkdir(parents=True, exist_ok=True)
|
|
|
|
def StripExt(Path:str):
|
|
return ".".join(Path.split('.')[:-1])
|
|
|
|
def UndupeStr(Str, Known, Split):
|
|
while Str in Known:
|
|
Sections = Str.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:str, Limit:int=32):
|
|
Str = ''
|
|
for c in s[:Limit].replace('\n','-').replace('\t','-').replace(' ','-'):
|
|
if c.lower() in '0123456789qwfpbjluyarstgmneiozxcdvkh-':
|
|
Str += c
|
|
return '-' + Str
|
|
|
|
def GetPathLevels(Path:str, AsNum:bool=False, Add:int=0, Sub:int=0):
|
|
n = Path.count('/') + Add - Sub
|
|
return n if AsNum else '../' * n
|
|
|
|
# https://stackoverflow.com/a/34445090
|
|
def FindAllIndex(Str:str, Sub:str):
|
|
i = Str.find(Sub)
|
|
while i != -1:
|
|
yield i
|
|
i = Str.find(Sub, i+1)
|
|
|
|
# Find all occurrences of substrings wrapped inside some characters
|
|
def StrFindWrapped(Str:str, Left:str, Right:str):
|
|
Res = []
|
|
Start = Str.find(Left)
|
|
while Start != -1:
|
|
Start = Start + len(Left)
|
|
Str = Str[Start:]
|
|
Stop = Str.find(Right)
|
|
if Stop != -1:
|
|
Res += [Str[:Stop]]
|
|
Start = Str.find(Left)
|
|
return Res
|
|
|
|
# Replace substrings in a string, except when an escape char is prepended
|
|
def ReplWithEsc(Str:str, Find:str, Repl:str, Html:bool=True, Esc:str='\\'):
|
|
New = ''
|
|
Sects = Str.split(Find)
|
|
# Every time a substring is found
|
|
for i,e in enumerate(Sects):
|
|
# If it's the first split, append it directly to the New string
|
|
if i == 0:
|
|
New += e
|
|
# Wrapping parts of the escaped substrings in HTML tags is done to avoid multiple calls of this function nullifying escaping
|
|
elif i > 0:
|
|
# If prev. split ends with 2 times the escape (escaping of the escape)
|
|
if Sects[i-1].endswith(Esc*2):
|
|
Wrap1 = f'<span>{New[-1]}</span>' if Html else New[-1]
|
|
Wrap2 = f'<span>{New[-2]}</span>' if Html else New[-2]
|
|
New = New[:-3] + Wrap2 + Wrap1
|
|
New += Repl + e
|
|
# If prev. split ends with 1 time the escape (escaping of the substring)
|
|
elif Sects[i-1].endswith(Esc):
|
|
New = New[:-1]
|
|
Wrap1 = f'<span>{Find[0]}</span>' if Html else Find[0]
|
|
New += Wrap1 + Find[1:] + e
|
|
# If no escape char
|
|
else:
|
|
New += Repl + e
|
|
return New
|
|
|
|
def DictReplWithEsc(Str:str, Dict:dict, Esc:str='\\'):
|
|
for Item in Dict:
|
|
Str = ReplWithEsc(Str, Item, Dict[Item], Esc='\\')
|
|
return Str
|
|
|
|
def WrapDictReplWithEsc(Str:str, Dict:dict, Wraps:list=[], Esc:str='\\'):
|
|
NewDict = {}
|
|
for Item in Dict:
|
|
for Wrap in Wraps:
|
|
NewDict.update({f'{Wrap[0]}{Item}{Wrap[1]}': Dict[Item]})
|
|
return DictReplWithEsc(Str, NewDict, Esc)
|
|
|
|
def NumsFromFileName(Path:str):
|
|
Name = Path.split('/')[-1]
|
|
Split = len(Name)
|
|
for i,e in enumerate(Name):
|
|
if e.lower() in 'qwfpbjluyarstgmneiozxcdvkh':
|
|
return Name[:i]
|
|
return Path
|
|
|
|
def RevSort(List:list):
|
|
List.sort()
|
|
List.reverse()
|
|
return List
|
|
|
|
def FileNameDateSort(Old): # TODO: Test this for files not starting with date, and dated folders
|
|
New = []
|
|
if Old:
|
|
Old.sort()
|
|
New.insert(0, Old[0])
|
|
for i,e in enumerate(Old):
|
|
if i == 0:
|
|
continue
|
|
Done = False
|
|
for j,f in enumerate(New):
|
|
if NumsFromFileName(e) != e and NumsFromFileName(f) != f and NumsFromFileName(e) < NumsFromFileName(f):
|
|
New.insert(j, e)
|
|
Done = True
|
|
break
|
|
if not Done:
|
|
New += [e]
|
|
return New
|
|
|
|
def FirstRealItem(List):
|
|
return next(e for e in List if e)
|
|
|
|
def GetFullDate(Date):
|
|
if not Date:
|
|
return None
|
|
return datetime.strftime(datetime.strptime(Date, '%Y-%m-%d'), '%Y-%m-%dT%H:%M+00:00')
|
|
|
|
def LoadLocale(Lang:str):
|
|
Lang = Lang + '.json'
|
|
Folder = f'{staticosoBaseDir()}Locale/'
|
|
File = ReadFile(Folder + Lang)
|
|
if File:
|
|
return json.loads(File)
|
|
else:
|
|
return json.loads(ReadFile(Folder + 'en.json'))
|
|
|
|
def IsLightRun(File, LimitFiles):
|
|
return False if LimitFiles == False or File in LimitFiles else True
|
|
|
|
def NameSpace(From):
|
|
return SimpleNamespace(**From)
|
|
|
|
def PrintProcPercentDots(Proc:dict, DivMult=1):
|
|
Div = 5 * DivMult # 100/5 = 20 chars
|
|
Num, Count = Proc['Num'], Proc['Count']
|
|
if int(((Num/Count)*100)/Div) != int((((Num+1)/Count)*100)/Div):
|
|
os.system('printf "="') # Using sys shell since for some reason print() without newline breaks here (doesn't print everytime)
|
|
return True
|
|
return False
|
|
|
|
def MultiProcFuncWrap(Args:dict):
|
|
PrintProcPercentDots(Args['Process'])
|
|
return Args['Process']['Func'](Args)
|
|
|
|
def DoMultiProc(Func, ArgsCollection:list, Threads:int=cpu_count(), Progress:bool=False):
|
|
FinalArgsCollection = []
|
|
for Index, Args in enumerate(ArgsCollection):
|
|
FinalArgsCollection.append(Args)
|
|
FinalArgsCollection[Index].update({"Process": {"Func": Func, "Num": Index, "Count": len(ArgsCollection)}})
|
|
Results = []
|
|
if Progress:
|
|
os.system('printf "["') # Using system print because (see PrintProcPercentDots())
|
|
with Pool(Threads) as MultiprocPool:
|
|
Results = MultiprocPool.map(MultiProcFuncWrap if Progress else Func, FinalArgsCollection)
|
|
if Progress:
|
|
os.system('printf "]\n"') # Newline after percentage dots
|
|
return Results
|
|
|
|
# Execute a function, whose output is compared to one input argument, as long as the output is different from the previous cycle; the moment it's equal, return
|
|
def WhileFuncResultChanges(Func, Args:dict, ResultKey:str):
|
|
Result = Args[ResultKey]
|
|
while True:
|
|
ResultOld = Result
|
|
Args.update({ResultKey: Result})
|
|
Result = Func(**Args)
|
|
if ResultOld == Result:
|
|
return Result
|
|
|
|
# Ellipsize text if it isn't already, optionally writing over the last chars instead of appending
|
|
def TryEllipsizeText(Text:str, Overwrite:bool=False, Ellipses:str='...'):
|
|
if not Text.endswith(Ellipses):
|
|
if Overwrite:
|
|
Text = Text[:-len(Ellipses)] + Ellipses
|
|
# Append normally
|
|
else:
|
|
Text += Ellipses
|
|
return Text
|
|
|
|
# Limit the length of a text, and account for if paragraphs should be sliced or entirely deleted to fit the limit
|
|
def LimitText(Text:str, MaxChars:int, SliceParagraphs:bool=False, ParagraphSep:str='\n'):
|
|
New = ''
|
|
Paras = Text.split(ParagraphSep)
|
|
if not Paras:
|
|
return ''
|
|
# The first paragraph; must always be present, ellipsized if needed
|
|
New = Paras[0]
|
|
if len(New) > MaxChars:
|
|
New = TryEllipsizeText(New[:MaxChars], Overwrite=True)
|
|
# Add a newline to the first paragraph if it wasn't ellipsized
|
|
else:
|
|
New += ParagraphSep
|
|
# All other paragraphs
|
|
for Par in Paras[1:]:
|
|
# If adding this paragraph to the new text would go over the limit, and we are allowed to slice, append and ellipsize it
|
|
if len(New + Par) > MaxChars:
|
|
if SliceParagraphs:
|
|
New = TryEllipsizeText(New[:MaxChars], Overwrite=True)
|
|
# If we still are whitin the limit, just append the paragraph
|
|
else:
|
|
New += Par + ParagraphSep
|
|
return New.strip()
|