mirror of
https://gitlab.com/octtspacc/staticoso
synced 2025-06-05 22:09:23 +02:00
XML feeds without libs; Check system tools; Provide software version
This commit is contained in:
@ -179,7 +179,7 @@ def BuildMain(Args, FeedEntries):
|
|||||||
# os.chdir(Args.InputDir)
|
# os.chdir(Args.InputDir)
|
||||||
# print(f"[I] Current directory: {Args.InputDir}")
|
# print(f"[I] Current directory: {Args.InputDir}")
|
||||||
|
|
||||||
SiteName = Flags['SiteName'] = OptChoose('', Args.SiteName, ReadConf(SiteConf, 'Site', 'Name'))
|
SiteName = Flags['SiteName'] = DefConfOptChoose('SiteName', Args.SiteName, ReadConf(SiteConf, 'Site', 'Name'))
|
||||||
if SiteName:
|
if SiteName:
|
||||||
logging.info(f"Compiling: {SiteName}")
|
logging.info(f"Compiling: {SiteName}")
|
||||||
|
|
||||||
@ -246,6 +246,12 @@ def BuildMain(Args, FeedEntries):
|
|||||||
SiteDomain = Flags['SiteDomain'] = SiteDomain.removesuffix('/')
|
SiteDomain = Flags['SiteDomain'] = SiteDomain.removesuffix('/')
|
||||||
Locale = LoadLocale(SiteLang)
|
Locale = LoadLocale(SiteLang)
|
||||||
|
|
||||||
|
if not InSystemPath('pug'):
|
||||||
|
logging.warning("⚠ `pug` not found in system PATH. If you have any .pug pages to be compiled, the program will fail.")
|
||||||
|
if Flags['GemtextOutput'] and not InSystemPath('html2gmi'):
|
||||||
|
logging.warning("⚠ `html2gmi` not found in system PATH. Gemtext generation will be disabled.")
|
||||||
|
Flags['GemtextOutput'] = False
|
||||||
|
|
||||||
if DiffBuild:
|
if DiffBuild:
|
||||||
logging.info("Build mode: Differential")
|
logging.info("Build mode: Differential")
|
||||||
LimitFiles = GetModifiedFiles(OutDir)
|
LimitFiles = GetModifiedFiles(OutDir)
|
||||||
@ -347,13 +353,13 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
ConfigLogging(Args.Logging)
|
ConfigLogging(Args.Logging)
|
||||||
|
|
||||||
try:
|
#try:
|
||||||
import lxml
|
# import lxml
|
||||||
from Modules.Feed import *
|
from Modules.Feed import *
|
||||||
FeedEntries = Args.FeedEntries if Args.FeedEntries else 'Default'
|
FeedEntries = Args.FeedEntries if Args.FeedEntries else 'Default'
|
||||||
except:
|
#except:
|
||||||
logging.warning("⚠ Can't load the XML libraries. XML Feeds Generation is Disabled. Make sure the 'lxml' library is installed.")
|
# logging.warning("⚠ Can't load the XML libraries. XML Feeds Generation is Disabled. Make sure the 'lxml' library is installed.")
|
||||||
FeedEntries = 0
|
# FeedEntries = 0
|
||||||
|
|
||||||
BuildMain(Args=Args, FeedEntries=FeedEntries)
|
BuildMain(Args=Args, FeedEntries=FeedEntries)
|
||||||
logging.info(f"✅ Done! ({round(time.time()-StartTime, 3)}s)")
|
logging.info(f"✅ Done! ({round(time.time()-StartTime, 3)}s)")
|
||||||
|
@ -15,6 +15,7 @@ DefConf = {
|
|||||||
"Threads": 0,
|
"Threads": 0,
|
||||||
"DiffBuild": False,
|
"DiffBuild": False,
|
||||||
"OutDir": "public",
|
"OutDir": "public",
|
||||||
|
"SiteName": "Untitled Site",
|
||||||
"SiteLang": "en",
|
"SiteLang": "en",
|
||||||
"SiteTemplate": "Default.html",
|
"SiteTemplate": "Default.html",
|
||||||
"ActivityPubTypeFilter": "Post",
|
"ActivityPubTypeFilter": "Post",
|
||||||
@ -22,7 +23,7 @@ DefConf = {
|
|||||||
"CategoriesUncategorized": "Uncategorized",
|
"CategoriesUncategorized": "Uncategorized",
|
||||||
"FeedCategoryFilter": "Blog",
|
"FeedCategoryFilter": "Blog",
|
||||||
"FeedEntries": 10,
|
"FeedEntries": 10,
|
||||||
"JournalRedirect": False
|
"JournalRedirect": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def LoadConfFile(File):
|
def LoadConfFile(File):
|
||||||
|
@ -29,18 +29,19 @@ CategoryPageTemplate = """\
|
|||||||
|
|
||||||
<div><staticoso:Category:{Name}></div>
|
<div><staticoso:Category:{Name}></div>
|
||||||
"""
|
"""
|
||||||
RedirectPageTemplate = """\
|
RedirectPageTemplate = f"""\
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>{TitlePrefix}Redirect</title>
|
<title>{{TitlePrefix}}Redirect</title>
|
||||||
<link rel="canonical" href="{SiteDomain}/{DestURL}"/>
|
<link rel="canonical" href="{{SiteDomain}}/{{DestURL}}"/>
|
||||||
<meta http-equiv="refresh" content="0; url='{DestURL}'"/>
|
<meta http-equiv="refresh" content="0; url='{{DestURL}}'"/>
|
||||||
|
<meta name="generator" content="{staticosoNameVersion()}"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p><a href="{DestURL}">{StrClick}</a> {StrRedirect}.</p>
|
<p><a href="{{DestURL}}">{{StrClick}}</a> {{StrRedirect}}.</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
@ -7,56 +7,153 @@
|
|||||||
| Copyright (C) 2022-2023, OctoSpacc |
|
| Copyright (C) 2022-2023, OctoSpacc |
|
||||||
| ================================== """
|
| ================================== """
|
||||||
|
|
||||||
# TODO: Either switch feed generation lib, or rewrite the 'lxml' module, so that no modules have to be compiled and the program is 100% portable
|
from datetime import datetime
|
||||||
|
|
||||||
from Libs.feedgen.feed import FeedGenerator
|
|
||||||
from Modules.Utils import *
|
from Modules.Utils import *
|
||||||
|
|
||||||
def MakeFeed(Flags:dict, Pages:list, FullSite=False):
|
FeedGenerator = None
|
||||||
|
try:
|
||||||
|
import lxml
|
||||||
|
from Libs.feedgen.feed import FeedGenerator
|
||||||
|
except:
|
||||||
|
logging.warning("⚠ Can't load the native XML libraries. XML Feeds Generation will use the interpreted module.")
|
||||||
|
|
||||||
|
def MakeFeed(Flags:dict, Pages:list, FullSite:bool=False):
|
||||||
f = NameSpace(Flags)
|
f = NameSpace(Flags)
|
||||||
CategoryFilter = Flags['FeedCategoryFilter']
|
CategoryFilter = Flags['FeedCategoryFilter']
|
||||||
MaxEntries = Flags['FeedEntries']
|
MaxEntries = Flags['FeedEntries']
|
||||||
|
|
||||||
|
if FeedGenerator:
|
||||||
Feed = FeedGenerator()
|
Feed = FeedGenerator()
|
||||||
Link = Flags['SiteDomain'] if Flags['SiteDomain'] else ' '
|
Link = Flags['SiteDomain'] if Flags['SiteDomain'] else ' '
|
||||||
Feed.id(Link)
|
Feed.id(Link)
|
||||||
Feed.title(Flags['SiteName'] if Flags['SiteName'] else 'Untitled Site')
|
|
||||||
Feed.link(href=Link, rel='alternate')
|
Feed.link(href=Link, rel='alternate')
|
||||||
|
Feed.title(Flags['SiteName'])
|
||||||
Feed.description(Flags['SiteTagline'] if Flags['SiteTagline'] else ' ')
|
Feed.description(Flags['SiteTagline'] if Flags['SiteTagline'] else ' ')
|
||||||
if Flags['SiteDomain']:
|
if Flags['SiteDomain']:
|
||||||
Feed.logo(Flags['SiteDomain'] + '/favicon.png')
|
Feed.logo(f'{Flags["SiteDomain"]}/favicon.png')
|
||||||
Feed.language(Flags['SiteLang'])
|
Feed.language(Flags['SiteLang'])
|
||||||
|
else:
|
||||||
|
FeedData = {
|
||||||
|
'Link': Flags['SiteDomain'],
|
||||||
|
'Title': Flags['SiteName'],
|
||||||
|
'Description': Flags['SiteTagline'],
|
||||||
|
'Language': Flags['SiteLang'],
|
||||||
|
'Entries': [],
|
||||||
|
}
|
||||||
|
|
||||||
DoPages = []
|
DoPages = []
|
||||||
for e in Pages:
|
for e in Pages:
|
||||||
if FullSite or (not FullSite and MaxEntries != 0 and e[3]['Type'] == 'Post' and e[3]['Feed'] == 'True'): # No entry limit if site feed
|
# No entry limit if site feed
|
||||||
|
if FullSite or (not FullSite and MaxEntries != 0 and e[3]['Type'] == 'Post' and e[3]['Feed'] == 'True'):
|
||||||
DoPages += [e]
|
DoPages += [e]
|
||||||
MaxEntries -= 1
|
MaxEntries -= 1
|
||||||
DoPages.reverse()
|
DoPages.reverse()
|
||||||
|
|
||||||
for File, Content, Titles, Meta, ContentHTML, SlimHTML, Description, Image in DoPages:
|
for File, Content, Titles, Meta, ContentHTML, SlimHTML, Description, Image in DoPages:
|
||||||
if FullSite or (not FullSite and Meta['Type'] == 'Post' and (not CategoryFilter or (CategoryFilter and (CategoryFilter in Meta['Categories'] or CategoryFilter == '*')))):
|
if FullSite or (not FullSite and Meta['Type'] == 'Post' and (not CategoryFilter or (CategoryFilter and (CategoryFilter in Meta['Categories'] or CategoryFilter == '*')))):
|
||||||
Entry = Feed.add_entry()
|
File = f'{StripExt(File)}.html'
|
||||||
FileName = File.split('/')[-1]
|
Link = Flags['SiteDomain'] + '/' + File if Flags['SiteDomain'] else File
|
||||||
File = f"{StripExt(File)}.html"
|
Title = Meta['Title'] if Meta['Title'] else Titles[0].lstrip('#') if Titles else File.split('/')[-1]
|
||||||
Content = ReadFile(f"{Flags['OutDir']}/{File}")
|
Title = Title.lstrip().rstrip()
|
||||||
Link = Flags['SiteDomain'] + '/' + File if Flags['SiteDomain'] else ' '
|
|
||||||
CreatedOn = GetFullDate(Meta['CreatedOn'])
|
CreatedOn = GetFullDate(Meta['CreatedOn'])
|
||||||
EditedOn = GetFullDate(Meta['EditedOn'])
|
EditedOn = GetFullDate(Meta['EditedOn'])
|
||||||
|
if FullSite: # Avoid making an enormous site feed file...
|
||||||
|
ContentHTML = ''
|
||||||
|
if FeedGenerator:
|
||||||
|
Entry = Feed.add_entry()
|
||||||
Entry.id(Link)
|
Entry.id(Link)
|
||||||
Title = Meta['Title'] if Meta['Title'] else Titles[0].lstrip('#') if Titles else FileName
|
|
||||||
Entry.title(Title.lstrip().rstrip())
|
|
||||||
Entry.description(Description)
|
|
||||||
Entry.link(href=Link, rel='alternate')
|
Entry.link(href=Link, rel='alternate')
|
||||||
if not FullSite: # Avoid making an enormous site feed file...
|
Entry.title(Title)
|
||||||
|
Entry.description(Description)
|
||||||
|
if ContentHTML:
|
||||||
Entry.content(ContentHTML, type='html')
|
Entry.content(ContentHTML, type='html')
|
||||||
if CreatedOn:
|
if CreatedOn:
|
||||||
Entry.pubDate(CreatedOn)
|
Entry.pubDate(CreatedOn)
|
||||||
EditedOn = EditedOn if EditedOn else CreatedOn if CreatedOn and not EditedOn else '1970-01-01T00:00+00:00'
|
EditedOn = EditedOn if EditedOn else CreatedOn if CreatedOn and not EditedOn else '1970-01-01T00:00+00:00'
|
||||||
Entry.updated(EditedOn)
|
Entry.updated(EditedOn)
|
||||||
|
else:
|
||||||
|
FeedData['Entries'] += [{
|
||||||
|
'Link': Link,
|
||||||
|
'Title': Title,
|
||||||
|
'Description': Description,
|
||||||
|
'Content': ContentHTML,
|
||||||
|
'PublishedOn': CreatedOn,
|
||||||
|
'UpdatedOn': EditedOn,
|
||||||
|
}]
|
||||||
|
|
||||||
if not os.path.exists(f"{Flags['OutDir']}/feed"):
|
if not os.path.exists(f"{Flags['OutDir']}/feed"):
|
||||||
os.mkdir(f"{Flags['OutDir']}/feed")
|
os.mkdir(f"{Flags['OutDir']}/feed")
|
||||||
FeedType = 'site.' if FullSite else ''
|
FeedType = 'site.' if FullSite else ''
|
||||||
|
if FeedGenerator:
|
||||||
Feed.atom_file(f"{Flags['OutDir']}/feed/{FeedType}atom.xml", pretty=(not Flags['MinifyOutput']))
|
Feed.atom_file(f"{Flags['OutDir']}/feed/{FeedType}atom.xml", pretty=(not Flags['MinifyOutput']))
|
||||||
Feed.rss_file(f"{Flags['OutDir']}/feed/{FeedType}rss.xml", pretty=(not Flags['MinifyOutput']))
|
Feed.rss_file(f"{Flags['OutDir']}/feed/{FeedType}rss.xml", pretty=(not Flags['MinifyOutput']))
|
||||||
|
else:
|
||||||
|
Feeds = PyFeedGenerator(FeedData)
|
||||||
|
for Format in ('atom', 'rss'):
|
||||||
|
WriteFile(f"{Flags['OutDir']}/feed/{FeedType}{Format}.xml", Feeds[Format])
|
||||||
|
|
||||||
|
def PyFeedGenerator(Data:dict, Format:bool=None):
|
||||||
|
XmlEntries = {'atom': '', 'rss': ''}
|
||||||
|
XmlExtra, AtomExtra, RssExtra = '', '', ''
|
||||||
|
XmlHeader = '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
XmlLang = f'xml:lang="{Data["Language"]}"'
|
||||||
|
XmlTitle = f'<title>{Data["Title"]}</title>'
|
||||||
|
XmlExtra += XmlTitle
|
||||||
|
if Data['Description']:
|
||||||
|
AtomExtra += f'<subtitle>{Data["Description"]}</subtitle>'
|
||||||
|
RssExtra += f'<description>{Data["Description"]}</description>'
|
||||||
|
if Data['Link']:
|
||||||
|
IconUrl = f'{Data["Link"]}/favicon.png'
|
||||||
|
AtomExtra += f'<id>{Data["Link"]}</id><link href="{Data["Link"]}"/><logo>{IconUrl}</logo>'
|
||||||
|
RssExtra += f'<link>{Data["Link"]}</link><image>{XmlTitle}<url>{IconUrl}</url><link>{Data["Link"]}</link></image>'
|
||||||
|
Entries = Data['Entries'] if Data['Entries'] else ()
|
||||||
|
for Entry in Data['Entries']:
|
||||||
|
XmlEntryExtra, AtomEntryExtra, RssEntryExtra = '', '', ''
|
||||||
|
XmlEntryExtra += f'<title>{Entry["Title"]}</title>'
|
||||||
|
if Entry['Description']:
|
||||||
|
RssEntryExtra += f'<description>{Entry["Description"]}</description>'
|
||||||
|
if Entry['Content']:
|
||||||
|
AtomEntryExtra += f'<content type="html">{Entry["Content"]}</content>'
|
||||||
|
RssEntryExtra += f'<content:encoded>{Entry["Content"]}</content:encoded>'
|
||||||
|
if Entry['PublishedOn']:
|
||||||
|
AtomEntryExtra += f'<published>{Entry["PublishedOn"]}</published>'
|
||||||
|
RssEntryExtra += f'<pubDate>{Entry["PublishedOn"]}</pubDate>'
|
||||||
|
if Entry['UpdatedOn']:
|
||||||
|
AtomEntryExtra += f'<updated>{Entry["UpdatedOn"]}</updated>'
|
||||||
|
XmlEntries['atom'] += f'''
|
||||||
|
<entry>
|
||||||
|
<id>{Entry['Link']}</id>
|
||||||
|
<link href="{Entry['Link']}"/>
|
||||||
|
{XmlEntryExtra}
|
||||||
|
{AtomEntryExtra}
|
||||||
|
</entry>
|
||||||
|
'''
|
||||||
|
XmlEntries['rss'] += f'''
|
||||||
|
<item>
|
||||||
|
<guid>{Entry['Link']}</guid>
|
||||||
|
<link>{Entry['Link']}</link>
|
||||||
|
{XmlEntryExtra}
|
||||||
|
{RssEntryExtra}
|
||||||
|
</item>
|
||||||
|
'''
|
||||||
|
Feeds = {'atom': f'''{XmlHeader}
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom" {XmlLang}>
|
||||||
|
{XmlExtra}
|
||||||
|
{AtomExtra}
|
||||||
|
<updated>{datetime.now()}</updated>
|
||||||
|
<generator uri="https://gitlab.com/octtspacc/staticoso" version="{staticosoNameVersion().split(" ")[1]}">staticoso</generator>
|
||||||
|
{XmlEntries['atom']}
|
||||||
|
</feed>''', 'rss': f'''{XmlHeader}
|
||||||
|
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0" {XmlLang}>
|
||||||
|
<channel>
|
||||||
|
{XmlExtra}
|
||||||
|
{RssExtra}
|
||||||
|
<language>{Data["Language"]}</language>
|
||||||
|
<lastBuildDate>{datetime.now()}</lastBuildDate>
|
||||||
|
<generator>{staticosoNameVersion()}</generator>
|
||||||
|
{XmlEntries['rss']}
|
||||||
|
</channel>
|
||||||
|
</rss>'''}
|
||||||
|
if Format:
|
||||||
|
Feeds = Feeds[Format.lower()]
|
||||||
|
return Feeds
|
||||||
|
@ -7,8 +7,7 @@
|
|||||||
| Copyright (C) 2022-2023, OctoSpacc |
|
| Copyright (C) 2022-2023, OctoSpacc |
|
||||||
| ================================== """
|
| ================================== """
|
||||||
|
|
||||||
import json
|
import json, os, subprocess
|
||||||
import os
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from multiprocessing import Pool, cpu_count
|
from multiprocessing import Pool, cpu_count
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -22,6 +21,14 @@ def SureList(e):
|
|||||||
def staticosoBaseDir():
|
def staticosoBaseDir():
|
||||||
return f"{os.path.dirname(os.path.abspath(__file__))}/../../"
|
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'):
|
def ReadFile(p:str, m:str='r'):
|
||||||
try:
|
try:
|
||||||
with open(p, m) as f:
|
with open(p, m) as f:
|
||||||
|
3
App/TODO
3
App/TODO
@ -2,7 +2,6 @@
|
|||||||
- .html input pages bug: // metadata lines not being removed from final file after parsing
|
- .html input pages bug: // metadata lines not being removed from final file after parsing
|
||||||
- Multi-line metadata flags
|
- Multi-line metadata flags
|
||||||
- Category-based feeds
|
- Category-based feeds
|
||||||
- Meta tag generator showing software version (git commit hash)
|
|
||||||
- Customize date format
|
- Customize date format
|
||||||
- Misskey for ActivityPub
|
- Misskey for ActivityPub
|
||||||
- Section marking in pages? (for use with external translators)
|
- Section marking in pages? (for use with external translators)
|
||||||
@ -25,7 +24,6 @@
|
|||||||
- Support for HTML comment lines (<!-- -->) in any format
|
- Support for HTML comment lines (<!-- -->) in any format
|
||||||
- Support for Wikitext, rST, AsciiDoc (?)
|
- Support for Wikitext, rST, AsciiDoc (?)
|
||||||
- Posts in draft state (will not be compiled) (?)
|
- Posts in draft state (will not be compiled) (?)
|
||||||
- Check if external tools (pug-cli, html2gmi) are installed
|
|
||||||
- Static code syntax highlighing
|
- Static code syntax highlighing
|
||||||
- Override internal HTML snippets (meta lines, page lists, redirects, ...) with config file in Templates/NAME.ini
|
- Override internal HTML snippets (meta lines, page lists, redirects, ...) with config file in Templates/NAME.ini
|
||||||
- Specify input folder(s)
|
- Specify input folder(s)
|
||||||
@ -39,7 +37,6 @@
|
|||||||
- Change FolderRoots arg name to CustomPaths
|
- Change FolderRoots arg name to CustomPaths
|
||||||
- Accept Macros as CLI arguments + Deprecate FolderRoots (Macros make it redundant)
|
- Accept Macros as CLI arguments + Deprecate FolderRoots (Macros make it redundant)
|
||||||
- Fix ordering menu in Site.ini (not working for inner pages)
|
- Fix ordering menu in Site.ini (not working for inner pages)
|
||||||
- Feed generation optionally without native libraries
|
|
||||||
- JSON feeds
|
- JSON feeds
|
||||||
- Full XML sitemap
|
- Full XML sitemap
|
||||||
- SCSS support
|
- SCSS support
|
||||||
|
Reference in New Issue
Block a user