first commit - needs content

master
phanes 2023-04-29 18:10:05 -04:00
commit a24627c747
47 changed files with 1230 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
output
venv
**/__pycache__/
template-dev
.idea

11
config.ini Normal file
View File

@ -0,0 +1,11 @@
[general]
# the path to the top-level content directory
# this is where source content resides, organized by recognized type
content_dir = content
[themes]
theme_dir = themes
theme = material-dark
[output]
artifact_root_dir = output

View File

@ -0,0 +1,7 @@
[feed]
name = phanes_canon
title = Phanes' Canon
tags = dhl
tag_filter = True
url = https://phanes.silogroup.org/feed
parent = dhlp-feeds-page

View File

@ -0,0 +1,7 @@
[feed]
name = pyrois_commits
title = Commit
tags = Uncategorized
tag_filter = False
url = https://github.com/SILO-GROUP/pyrois/commits.atom
parent = pyrois-feeds-page

View File

View File

@ -0,0 +1,5 @@
[metadata]
# the title of the page
title = Dark Horse Linux News
uid = dhlp-feeds-page
parent = dark-horse-linux

View File

@ -0,0 +1,31 @@
# On Meritocracy
Meritocracy has turned into something else in recent years.
Too many projects accept or reject contributions based entirely off who is making the contribution, or what company they work for, or mix up contribution merit with appeal to group politics, or a contributor's social status, or, the relationship between those projects and the developer's employer.
This creates many problems that distract from the goal of the F/OSS movement -- from elitism, exclusive cliques and subcultures, cronyism, vulnerability to project hijacking by sponsor interest, and more, ultimately resulting in the stifling of innovation, and, often, the derailment of project development to create dependencies on external projects.
Meritocracy is well intended, and was a step in the right direction, but it's not enough.
# An Egalitocracy
DHLP accepts contributions from almost anyone, however, the inclusion of contributions is a decision based entirely on the contributions' merit, purpose, and utility.
Contributors and decision-makers on DHLP projects are expected to operate with this imperative.
A utilitarian, egalitocratic paradigm in the context of a Linux distribution places emphasis on selecting contributions based on their utility, merit, and purpose, while consciously disregarding the identity of the contributor.
This approach diverges from traditional meritocratic systems, which, although valuing the merit and effectiveness of contributions, still acknowledges the contributor's identity as a secondary factor of the inclusion decision.
By solely focusing on the utility and merit of contributions, the utilitarian philosophy aims to create an environment free from personal biases, cronyism, and toxic politics, ultimately promoting a more equitable and inclusive software ecosystem. This paradigm shift within the open source community fosters the development of a robust, efficient, and fair Linux distribution that maximizes the overall benefit for all users and developers, transcending the limitations of conventional merit-based systems.
# Inclusion
This allows for maximal inclusion.
# Exceptions
The primary exceptions to this philosophy are those that abuse its intent. This is an exception provided by its utility.

View File

@ -0,0 +1,5 @@
[metadata]
# the title of the page
title = Focus on Purpose
uid = focus-on-utility
parent = dark-horse-linux

View File

@ -0,0 +1,7 @@
# Dark Horse Linux LiveCD
https://www.darkhorselinux.org/downloads
# Dark Horse Linux Installer
TBD

View File

@ -0,0 +1,4 @@
[metadata]
title = Downloads
uid = downloads
parent = dark-horse-linux

View File

@ -0,0 +1,5 @@
[metadata]
# the title of the page
title = Pyrois News
uid = pyrois-feeds-page
parent = pyrois

View File

@ -0,0 +1,7 @@
[product]
title = Dark Horse Linux
uid = dark-horse-linux
description = Introducing Dark Horse Linux, a bold and defiant Linux distribution designed to prioritize user freedom and functionality amidst unwanted interference. By harnessing carefully chosen components, Dark Horse Linux delivers a transparent computing experience, staying true to the essence of open-source software. Join a growing, tenacious community committed to preserving an open and user-centric approach.
link_url = ../../pages/downloads
link_url_description = Downloads
type = primary

View File

@ -0,0 +1,7 @@
[product]
title = Pyrois
uid = pyrois
description = Not enough freedom? DHLP has you covered: Pyrois is a project that automates the compilation of a Linux distribution of your own, based largely on the <a href="">Linux From Scratch</a> documentation. It compiles an entire system from raw component sources, and is designed in such a way that you can kick off your own custom distribution easily and start fresh with any kind of modifications you can integrate, with a resulting system that you can gaurantee full chain of custody over from raw source to production. Enjoy a non-GMO, no-preservative diet of verifiably pure, untouched Linux. Dark Horse Linux was built using Pyrois. Good luck!
link_url = https://github.com
link_url_description = Enter Hard Mode
type = secondary

View File

@ -0,0 +1,7 @@
[product]
title = Rex
uid = rex
description = Rex is a capable automation system that loads potentially long chains of executables written in any language, and executes them as a workflow in a similar manner to many build systems, with the benefit of creating detailed log captures of what it executes in a highly organized manner. It provides safety rails around chains of automations potentially provided by disparate sources. Rex is the core tool used by the Pyrois project to compile Dark Horse Linux.
link_url = https://github.com
link_url_description = Learn about Rex
type = secondary

BIN
content/rsrc/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

4
content/site.ini Normal file
View File

@ -0,0 +1,4 @@
[main]
logo = rsrc/logo.png
title = Dark Horse Linux Project
landing = products/dark-horse-linux

View File

@ -0,0 +1,7 @@
[tearoff]
name = dhlp-documentation-tearoff
title = Documentation
content = Contribute to and consume the DHLP documentation.
link_title = Documentation
link_url = https://www.darkhorselinux.org/pages/dark-horse-linux-documentation
parent = dark-horse-linux

View File

@ -0,0 +1,7 @@
[tearoff]
name = focus-on-utility-tearoff
title = Focused on Purpose
content = While DHLP accepts contributions from anyone, the inclusion of contributions is a decision based entirely on its merit and purpose.
link_title = Read More &raquo;
link_url = ../../pages/focus-on-utility
parent = dark-horse-linux

View File

@ -0,0 +1,7 @@
[tearoff]
title = Parent Leaf Status
name = parent-leaf-status
content = While still providing the full Linux/GNU environment you are accustomed to, such as SystemD, RPM, glibc, GCC -- DHLP is not based on another Linux distribution, and so is not beholden to any upstream projects' influence in any capacity -- because no such project exists.
link_title = Read More &raquo;
link_url = https://www.darkhorselinux.org/pages/parent-leaf-status
parent = dark-horse-linux

View File

@ -0,0 +1,7 @@
[tearoff]
title = Documentation
name = pyrois-documentation-tearoff
content = Contribute to and consume the Pyrois documentation.
link_title = Documentation
link_url = https://www.darkhorselinux.org/pages/pyrois-documentation
parent = pyrois

View File

@ -0,0 +1,7 @@
[tearoff]
title = Vanilla Flavored Distro
name = vanilla-flavored-distro-tearoff
content = Some level of patching is always necessary; in the case of DHLP, all patches are incorporated into a publicly auditable build process for transparency. Outside of security-related patching, DHLP utilizes minimal patching of upstream sources in an auditable fashion, ensuring you get the unadulterated F/OSS components you desire.
link_title = Read More &raquo;
link_url = https://www.darkhorselinux.org/pages/vanilla-flavored-distro
parent = dark-horse-linux

13
generate.py Normal file
View File

@ -0,0 +1,13 @@
from src.Config import Config
from src.ContentLoader import ContentLoader
from src.SiteGenerator import SiteGenerator
def main():
config = Config('config.ini')
content = ContentLoader( config )
generator = SiteGenerator( config, content )
if __name__ == '__main__':
main()

35
src/Config.py Normal file
View File

@ -0,0 +1,35 @@
import os
from configparser import ConfigParser
class Site:
def __init__( self, config ):
self._config = config
self._site_parser = ConfigParser()
site_config_file = os.path.join( config.content_dir, 'site.ini' )
self._site_parser.read(site_config_file)
self.logo_path = self._site_parser.get('main', 'logo')
self.title = self._site_parser.get( 'main', 'title')
self.landing = self._site_parser.get( 'main', 'landing')
# paths can be provided as relative paths or absolute paths
class Config:
def __init__(self, filename ):
self._parser = ConfigParser(allow_no_value=True)
self._parser.read(filename)
self.theme_dir = self._parser.get("themes", "theme_dir")
self.theme_name = self._parser.get("themes", "theme")
self.content_dir = self._parser.get("general", "content_dir")
self.artifact_root_dir = self._parser.get('output', 'artifact_root_dir')
# convert all paths to absolute paths
self.theme_dir = os.path.abspath( self.theme_dir )
self.content_dir = os.path.abspath( self.content_dir )
self.artifact_root_dir = os.path.abspath( self.artifact_root_dir )
self.site = Site( self )

33
src/ContentAbstraction.py Normal file
View File

@ -0,0 +1,33 @@
class CompositeContent:
def __init__(self):
self.items = list()
def __iter__(self):
return iter(self.items)
def __str__(self):
return str(self.items)
def __repr__(self):
return "{0}(items={1})".format(self.__class__.__name__, self.items)
def extend(self, other):
if not isinstance(other, CompositeContent):
raise TypeError("The other instance must inherit from the BaseCollection class")
merged_items = self.items + other.items
self.items = merged_items
return self
def add_item( self, other ):
if not isinstance(other, CompositeContent):
raise TypeError("The new item must inherit from the BaseCollection class")
self.items.append( other )
def get_item_by_uname(self, name):
for item in self.items:
if item.unique_name == name:
return item
return None

56
src/ContentLoader.py Normal file
View File

@ -0,0 +1,56 @@
from src.Page import Pages
from src.Hier import Hier
from configparser import ConfigParser
import os
import feedparser
from datetime import datetime
from time import mktime
# imports all the different types of content
from src.ContentAbstraction import CompositeContent
from src.Feed import Feeds
from src.Product import Products
from src.Tearoff import Tearoffs
# loader of all types of content
class ContentLoader:
def __init__( self, config ):
self._config = config
self._raw_content = CompositeContent()
# load content of type 'Page'
self._pages = Pages( config )
# feeds specify what pages they are available to in a comma-delimited list
# this allows the generator to make them available to generating certain pages by unique_name
self._feeds = Feeds( config )
# products to showcase in the websites (if different from a page)
self._products = Products( config )
# tearoffs are short descriptions with a call to action link attached to products and pages
self._tearoffs = Tearoffs( config )
self._raw_content.extend( self._pages )
self._raw_content.extend( self._feeds )
self._raw_content.extend( self._products )
self._raw_content.extend( self._tearoffs )
# create a hierarchical data structure representing the relation of content to parents/children
self.content = Hier(config, self._raw_content)
def __repr__(self):
return repr(self.content.top_down)
def __setitem__(self, key, value):
self.content.top_down[key] = value
def __delitem__(self, key):
del self.content.top_down[key]
def __iter__(self):
return iter(self.content.top_down)
def __len__(self):
return len(self.content.top_down)

118
src/Feed.py Normal file
View File

@ -0,0 +1,118 @@
import os
from src.ContentAbstraction import CompositeContent
from configparser import ConfigParser
import feedparser
from datetime import datetime
from time import mktime
import re
def get_top_level_subdirectories(path):
try:
entries = os.listdir(path)
subdirectories = [os.path.join(path, entry) for entry in entries if os.path.isdir(os.path.join(path, entry))]
return subdirectories
except FileNotFoundError:
print( "Error: The path '{0}' does not exist.".format( path ) )
return []
class FeedEntry:
def __init__(self, entry, parent_feed):
self.title = entry
self.url = entry['link']
self.parent_feed = parent_feed
if self.url.startswith("https://github.com"):
self.is_commit_feed = True
else:
self.is_commit_feed = False
if self.is_commit_feed:
self.title = re.split('\/', self.url)[-1]
else:
self.title = entry['title']
self.author = entry['author']
self.summary = entry['summary']
self.content = entry['content'][0]['value']
try:
self._date_raw = entry['published_parsed']
except KeyError:
self._date_raw = entry['updated_parsed']
self._datetime_obj = datetime.fromtimestamp(mktime(self._date_raw))
self.date = self._datetime_obj.strftime('%Y-%m-%d')
self.tags = list()
if not self.is_commit_feed:
self._tags_raw = entry['tags']
for raw_tag in self._tags_raw:
self.tags.append(raw_tag['term'])
else:
self.tags = None
def __repr__(self):
return "FeedEntry(title={0})".format( self.title )
class Feed:
def __init__( self, iPath ):
self._metadata_file = os.path.join( iPath, 'metadata.ini' )
metadata_parser = ConfigParser()
metadata_parser.read(self._metadata_file)
self.title = metadata_parser.get('feed', 'title')
self.tags = metadata_parser.get('feed', 'tags')
self.tag_filter = metadata_parser.getboolean('feed', 'tag_filter')
self.feed_url = metadata_parser.get('feed', 'url')
self.parent = metadata_parser.get('feed', 'parent')
self.unique_name = metadata_parser.get('feed', 'name')
feed_result = feedparser.parse(self.feed_url)
self.children = None
# fetch and parse feed
try:
self.subtitle = feed_result['feed']['subtitle']
except KeyError:
self.subtitle = ""
self.site_url = feed_result['feed']['link']
self._entries_raw = feed_result['entries']
self.entries = list()
for raw_entry in self._entries_raw:
feed_entry = FeedEntry(raw_entry, parent_feed=self)
if self.tag_filter:
if [x in self.tags for x in feed_entry.tags if x in self.tags]:
self.entries.append( feed_entry )
else:
self.entries.append(feed_entry)
def __repr__(self):
return "Feed(title={0})".format( self.title )
class Feeds(CompositeContent):
def __init__(self, config):
super().__init__()
self._feeds_dir = os.path.join(config.content_dir, 'feeds')
self._item_paths = get_top_level_subdirectories(self._feeds_dir)
for iPath in self._item_paths:
feed = Feed(iPath)
self.items.append(feed)
self.sorted_entries = self.get_sorted_entries()
def get_sorted_entries(self):
all_entries = []
for feed in self.items:
all_entries.extend(feed.entries)
sorted_entries = sorted(all_entries, key=lambda entry: entry.date, reverse=True)
return sorted_entries
def __repr__(self):
return "Feeds(items={0})".format(len(self.items))

45
src/Hier.py Normal file
View File

@ -0,0 +1,45 @@
# a representation of unique names in hierarchal order to represent item hierarchy
# useful for generating navbars and associated specific content types with each other
class Hier:
def __init__( self, config, content_items ):
self._bottom_up = content_items
# throw an exception if the hiearchy doesn't add up
self.validate_hierarchy(content_items)
# top level is represented as "_top_"
self._content_tree = self.build_tree(content_items, "_top_")
self.top_down = self.build_instance_tree(self._content_tree)
def validate_hierarchy( self, items ):
unique_names = {item.unique_name for item in items}
for item in items:
if item.parent is not None and item.parent != "_top_" and item.parent not in unique_names:
raise ValueError("Parent '{0}' of item '{1}' does not exist".format(item.parent, item.unique_name))
if item.parent == item.unique_name:
raise ValueError("Self-referential parent in content definition.")
def build_tree( self, items, parent_unique_name=None ):
tree = {}
for item in items:
if item.parent == parent_unique_name:
tree[item.unique_name] = self.build_tree( items, item.unique_name )
return tree
def print_tree( self, tree, level=0 ):
for unique_name, children in tree.items():
print( "\t" * level + unique_name )
self.print_tree( children, level + 1 )
def build_instance_tree( self, tree ):
instance_list = list()
for unique_name, children in tree.items():
item_instance = self._bottom_up.get_item_by_uname(unique_name)
item_instance.children = self.build_instance_tree( children )
instance_list.append(item_instance)
return instance_list
def __repr__(self):
return repr(self.top_down)

68
src/Page.py Normal file
View File

@ -0,0 +1,68 @@
from configparser import ConfigParser
import os
import markdown
from src.ContentAbstraction import CompositeContent
import uuid
def get_top_level_subdirectories(path):
try:
entries = os.listdir(path)
subdirectories = [os.path.join(path, entry) for entry in entries if os.path.isdir(os.path.join(path, entry))]
return subdirectories
except FileNotFoundError:
print( "Error: The path '{0}' does not exist.".format( path ) )
return []
# reads a page from the pages_dir
# a page draws its:
# - content from a markdown file named 'content.md'
# - metadata from an ini file named 'metadata.ini'
class Page:
def __init__( self, page_dir, config ):
self.hash = uuid.uuid4().hex
self._metadata_file = os.path.join( page_dir, 'metadata.ini' )
self._markdown_file = os.path.join( page_dir, 'content.md' )
metadata_parser = ConfigParser()
metadata_parser.read( self._metadata_file )
# the title of the page
self.title = metadata_parser.get( 'metadata', 'title' )
# a unique identifier for each page
self.unique_name = metadata_parser.get( 'metadata', 'uid' )
# the unique_name property for the parent page object
self.parent = metadata_parser.get( 'metadata', 'parent' )
# the intended url of the generated page, relative to config.artifact_root_dir
self.url = "../../pages/{0}".format( self.unique_name )
self._raw_markdown = self.get_raw_markdown(self._markdown_file)
self.html_content = markdown.markdown(self._raw_markdown)
self.children = None
def get_raw_markdown( self, filename ):
with open( filename, 'r' ) as markdown_file:
markdown_string = markdown_file.read()
return markdown_string
def __str__(self):
return "Page(title={0})".format( self.title )
def __repr__(self):
return "Page(title={0})".format( self.title )
# a self-loading collection of pages
class Pages(CompositeContent):
def __init__( self, config ):
super().__init__()
self.pages_dir = os.path.join(config.content_dir, 'pages')
self._item_paths = get_top_level_subdirectories(self.pages_dir)
for iPath in self._item_paths:
page = Page( iPath, config )
self.items.append(page)

62
src/Product.py Normal file
View File

@ -0,0 +1,62 @@
from configparser import ConfigParser
from src.ContentAbstraction import CompositeContent
import os
import uuid
def get_top_level_subdirectories( path ):
try:
entries = os.listdir( path )
subdirectories = [ os.path.join( path, entry ) for entry in entries if os.path.isdir( os.path.join( path, entry ) ) ]
return subdirectories
except FileNotFoundError:
print( "Error: The path '{0}' does not exist.".format( path ) )
return []
class Product:
def __init__( self, product_dir ):
self.hash = uuid.uuid4().hex
self._metadata_file = os.path.join( product_dir, 'metadata.ini' )
metadata_parser = ConfigParser()
metadata_parser.read( self._metadata_file )
# title used in the landing splash
self.title = metadata_parser.get( 'product', 'title' )
# unique name for the object referred to by other objects for parent/child relationship
self.unique_name = metadata_parser.get( 'product', 'uid' )
self.description = metadata_parser.get( 'product', 'description' )
# product landings have a link to a dedicated page or some other url
self.link_url = metadata_parser.get( 'product', 'link_url' )
# name for the link button
self.link_url_description = metadata_parser.get( 'product', 'link_url_description' )
# primary vs secondary product (allows theme to differentiate between primary product and secondary product)
self.type = metadata_parser.get( 'product', 'type' )
self.url = "../../products/{0}".format( self.unique_name )
self.children = list()
self.parent = "_top_"
def __str__(self):
return str( self.unique_name )
def __repr__(self):
return "Product(title={0})".format(self.title)
class Products(CompositeContent):
def __init__( self, config ):
super().__init__()
self.product_dir = os.path.join( config.content_dir, 'products' )
self._item_paths = get_top_level_subdirectories( self.product_dir )
for iPath in self._item_paths:
product = Product( iPath )
self.items.append( product )

138
src/SiteGenerator.py Normal file
View File

@ -0,0 +1,138 @@
import os
import shutil
from src.Page import Page
from src.Product import Product
from src.Tearoff import Tearoff
from src.Feed import Feed
from jinja2 import Environment, FileSystemLoader
def get_type(obj):
return type(obj).__name__
class SiteGenerator:
def __init__( self, config, content ):
self.config = config
# raw content with hierarchical associations set
self.content = content.content.top_down
# output directory where content is generated
self.output_root_dir = self.config.artifact_root_dir
# copy everything from content/rsrc to output/rsrc
self.content_rsrc_src = os.path.join( self.config.content_dir, 'rsrc' )
self.rsrc_dest = os.path.join( self.output_root_dir, 'rsrc' )
self.copy_dir_contents( self.content_rsrc_src, self.rsrc_dest )
# copy everything from themes/$theme_name/rsrc to output/rsrc
self.theme_root_dir = os.path.join( self.config.theme_dir, self.config.theme_name )
self.theme_rsrc = os.path.join( self.theme_root_dir, 'rsrc' )
self.copy_dir_contents( self.theme_rsrc, self.rsrc_dest )
# iterate through all content items and generate accordingly
self.generate_content_loop( self.content )
# allow for a landing page
self.generate_index()
def generate_content_loop(self, obj_list, original_obj_list=None):
if original_obj_list is None:
original_obj_list = obj_list
for item in obj_list:
self.generate_content_item( item, original_obj_list )
if len(item.children) > 0:
self.generate_content_loop(item.children, original_obj_list)
def generate_index(self):
env = Environment(
loader=FileSystemLoader(self.theme_root_dir),
extensions=['jinja2.ext.loopcontrols']
)
env.filters['get_type'] = get_type
url_prefix = self.output_root_dir
target_path = os.path.join(url_prefix, "index.html")
index_tmpl = env.get_template('index.tmpl')
index_content = index_tmpl.render(
config=self.config
)
with open( target_path, "w" ) as output_file:
output_file.write(index_content)
def generate_content_item( self, content_item, all_content_items ):
# child-only types are rendered as part of the parent type
if isinstance( content_item, Feed ):
return
if isinstance( content_item, Tearoff ):
return
env = Environment(
loader=FileSystemLoader(self.theme_root_dir),
extensions=['jinja2.ext.loopcontrols']
)
env.filters['get_type'] = get_type
relevant_tearoffs = list()
relevant_feeds = list()
url_prefix = self.output_root_dir
target_path = os.path.join(url_prefix, content_item.unique_name + "/index.html" )
if isinstance( content_item, Product ):
url_prefix = os.path.join( url_prefix, 'products' )
target_path = os.path.join( url_prefix, content_item.unique_name + "/index.html")
content_tmpl = env.get_template('product.tmpl')
for child in content_item.children:
if isinstance( child, Tearoff ):
if child.parent == content_item.unique_name:
relevant_tearoffs.append(child)
if isinstance( child, Feed ):
if child.parent == content_item.unique_name:
relevant_feeds.append( child )
if isinstance( content_item, Page ):
url_prefix = os.path.join( url_prefix, 'pages' )
target_path = os.path.join( url_prefix, content_item.unique_name + "/index.html" )
content_tmpl = env.get_template('page.tmpl')
for child in content_item.children:
if isinstance( child, Tearoff ):
if child.parent == content_item.unique_name:
relevant_tearoffs.append(child)
if isinstance( child, Feed ):
if child.parent == content_item.unique_name:
relevant_feeds.append( child )
all_entries = []
for ifeed in relevant_feeds:
all_entries.extend(ifeed.entries)
sorted_entries = sorted(all_entries, key=lambda entry: entry.date, reverse=True)
content_html = content_tmpl.render(
config=self.config,
all_content=all_content_items,
tearoffs=relevant_tearoffs,
feeds=relevant_feeds,
sorted_feeds=sorted_entries,
this_content=content_item
)
os.makedirs( os.path.dirname(target_path), exist_ok=True )
with open( target_path, "w" ) as output_file:
output_file.write(content_html)
# preserves files in rsrc_dest that are not in rsrc_src to allow cascade merging by subsequent copying
def copy_dir_contents(self, src, dst):
if not os.path.exists( dst ):
os.makedirs( dst )
for root, dirs, files in os.walk(src):
relative_path = os.path.relpath(root, src)
dest_dir = os.path.join( dst, relative_path )
for d in dirs:
os.makedirs( os.path.join( dest_dir, d ), exist_ok=True )
for f in files:
shutil.copy2( os.path.join( root, f ), os.path.join( dest_dir, f ) )

47
src/Tearoff.py Normal file
View File

@ -0,0 +1,47 @@
from configparser import ConfigParser
from src.ContentAbstraction import CompositeContent
import os
def get_top_level_subdirectories( path ):
try:
entries = os.listdir( path )
subdirectories = [ os.path.join( path, entry ) for entry in entries if os.path.isdir( os.path.join( path, entry ) ) ]
return subdirectories
except FileNotFoundError:
print( "Error: The path '{0}' does not exist.".format( path ) )
return []
class Tearoff:
def __init__( self, tearoff_dir ):
self.metadata_file = os.path.join( tearoff_dir, 'metadata.ini' )
metadata_parser = ConfigParser()
metadata_parser.read( self.metadata_file )
self.title = metadata_parser.get( 'tearoff', 'title' )
self.content = metadata_parser.get( 'tearoff', 'content' )
self.link_title = metadata_parser.get( 'tearoff', 'link_title' )
self.link_url = metadata_parser.get( 'tearoff', 'link_url' )
self.unique_name = metadata_parser.get( 'tearoff', 'name' )
# parent.unique_name
self.parent = metadata_parser.get( 'tearoff', 'parent' )
self.children = list()
def __str__(self):
return str( self.unique_name )
def __repr__(self):
return "Tearoff(title={0}, parent={1})".format( self.title, self.parent )
class Tearoffs(CompositeContent):
def __init__( self, config ):
super().__init__()
self.tearoff_dir = os.path.join( config.content_dir, 'tearoffs' )
self._item_paths = get_top_level_subdirectories( self.tearoff_dir )
for iPath in self._item_paths:
tearoff = Tearoff( iPath )
self.items.append( tearoff )

View File

@ -0,0 +1,16 @@
<!-- start footer -->
<footer class="bg-dark text-center text-white fixed-bottom footer">
<div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.2);">
© 2023 SILO GROUP, LLC
</div>
</footer>
<script src="../../rsrc/jquery-3.2.1.slim.min.js"></script>
<script>window.jQuery || document.write('<script src="../../rsrc/jquery-slim.min.js"><\/script>')</script>
<script src="../../rsrc/popper.min.js"></script>
<script src="../../rsrc/bootstrap.min.js"></script>
</body>
</html>
<!-- end footer -->

View File

@ -0,0 +1,21 @@
<!-- start header -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="../../favicon.ico">
<title>{{ config.site.title }} - {{ this_content.title }}</title>
<link href="../../rsrc/bootstrap.min.css" rel="stylesheet">
<link href="../../rsrc/jumbotron.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@500&display=swap" rel="stylesheet">
</head>
<body class="bg-light">
<!-- end header -->

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url={{ config.site.landing }}">
<title>Redirecting to {{ config.site.landing }}</title>
</head>
<body>
<p>You are being redirected to <a href="{{ config.site.landing }}">{{ config.site.landing }}</a></p>
</body>
</html>

View File

@ -0,0 +1,65 @@
{% macro generate_single_item(item) %}
<li class="nav-item">
<a class="nav-link" href="{{ item.url }}" type="{{ item|get_type }}">{{ item.title }}</a>
</li>
{% endmacro %}
{% macro generate_item_with_children(item) %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="{{ item.hash }}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ item.title }}</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="{{ item.hash }}">
<a type="{{ item|get_type }}" class="dropdown-item" href="{{ item.url }}">{{ item.title }}</a>
{% for child in item.children %}
{% if child|get_type == 'Tearoff' %}
{% continue %}
{% endif %}
{% if child|get_type == 'Feed' %}
{% continue %}
{% endif %}
<a type="{{ child|get_type }}" class="dropdown-item" href="{{ child.url }}">{{ child.title }}</a>
{% endfor %}
</div>
</li>
{% endmacro %}
{% macro generate_navbar(content_items) %}
{% for item in all_content %}
{% if item|get_type == 'Feed' %}
{% continue %}
{% endif %}
{% if item|get_type == 'Tearoff' %}
{% continue %}
{% endif %}
{% if item.children|length == 0 %}
{{ generate_single_item(item) }}
{% else %}
{{ generate_item_with_children(item) }}
{% endif %}
{% endfor %}
{% endmacro %}
<!-- start navbar -->
<nav class="navbar navbar-expand-md custom-navbar navbar-dark fixed-top bg-dark">
<a href="/" class="navbar-brand">
<img src="../../{{ config.site.logo_path }}" height="64" alt="DHLP">
<span class="project-name pl-3">{{ config.site.title }}</span>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav ml-auto">
{{ generate_navbar(all_content) }}
</ul>
</div>
</nav>
<!-- end navbar -->

View File

@ -0,0 +1,49 @@
{% include 'header.tmpl' %}
{% include 'navbar.tmpl' %}
<!-- start content view for page -->
<div class="content">
<main role="main">
<div class="jumbotron">
<div class="container">
<h1 class="display-4 text-center title-text">{{ this_content.title }}</h1>
</div>
</div>
<div class="p-3 mb-5 rounded">
<div class="media pt-3">
<div class="media-body pb-3 mb-0 lh-125 border-bottom cuscol">
<div class="list-group">
<div class="list-group-item list-group-item-action">
<div class="w-100">
{{ this_content.html_content }}
</div>
</div>
<p></p>
{% for entry in sorted_feeds %}
<div class="list-group-item list-group-item-action">
<div class="d-flex flex-row justify-content-between">
<div class="d-flex flex-column">
<h5 class="mb-1">
<a href="{{ entry.parent_feed.site_url }}">{{ entry.parent_feed.title }}</a>:
<a href="{{ entry.url }}">{{ entry.title }}</a>
</h5>
{{ entry.content|safe }}
</div>
<div class="d-flex flex-column align-items-end">
<div class="feed_date">{{ entry.date }}</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</main>
</div>
<!-- end page view -->
{% include 'footer.tmpl' %}

View File

@ -0,0 +1,42 @@
{% include 'header.tmpl' %}
{% include 'navbar.tmpl' %}
<!-- start content view for product -->
<div class="content">
<main role="main">
<!-- Main jumbotron for a primary marketing message or call to action -->
{% if this_content.type == 'primary' %}
<div class="jumbotron">
{% else %}
<div class="jumbotron secondary-jumbotron">
{% endif %}
<div class="container">
<h1 class="display-3">{{ this_content.title }}</h1>
<p>{{ this_content.description }}</p>
<p><a class="btn btn-primary btn-lg" href="{{ this_content.link_url }}" role="button">{{ this_content.link_url_description }}</a></p>
</div>
</div>
<div class="container">
<hr>
<div class="row">
{% for tearoff in tearoffs %}
<div class="col-md-3">
<h2>{{ tearoff.title }}</h2>
<p>{{ tearoff.content }}</p>
<p><a class="btn btn-secondary" href="{{ tearoff.link_url }}" role="button">{{ tearoff.link_title }}</a></p>
</div>
{% endfor %}
</div>
<hr>
</div>
</main>
</div>
<!-- end content view for product -->
{% include 'footer.tmpl' %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,229 @@
/* Move down content because we have a fixed navbar that is 3.5rem tall */
body {
padding-top: 7rem;
}
/* fix right-aligned dropdown menus so that they do not render offscreen */
.dropdown-menu-right {
right: 0;
left: auto;
}
/* set navbar to brand colors */
.navbar.custom-navbar {
background-color: #003153 !important;
}
/* nexted submenus */
.dropdown-submenu {
position: relative;
}
.dropdown-submenu a::after {
transform: rotate(-90deg);
position: absolute;
right: 6px;
top: .8em;
}
.dropdown-submenu .dropdown-menu {
top: 0;
left: 100%;
margin-left: .1rem;
margin-right: .1rem;
}
@media (max-width: 767.98px) {
.dropdown-submenu .dropdown-menu {
left: auto;
right: 100%;
margin-left: 0;
margin-right: .1rem;
}
}
.dropdown-submenu {
position: relative;
}
.dropdown-submenu a::after {
transform: rotate(-90deg);
position: absolute;
right: 6px;
top: .8em;
}
.dropdown-submenu .dropdown-menu {
top: 0;
left: auto;
margin-left: .1rem;
margin-right: .1rem;
}
@media (max-width: 767.98px) {
.dropdown-submenu .dropdown-menu {
left: auto;
right: 100%;
margin-left: 0;
margin-right: .1rem;
}
}
footer {
background-color: #003153 !important;
}
footer.bg-dark {
background-color: #003153 !important;
}
.content {
margin-bottom: 5%; /* Same as footer height */
}
footer {
position: fixed;
bottom: 0;
width: 100%;
background-color: #003153;
color: white;
text-align: center;
}
.secondary-jumbotron {
background-color: #ffe6e6;
}
.cuscol {
margin: 0 auto; /* centers the div horizontally */
text-align: left; /* left-aligns the text */
max-width: 80ch;
}
.cuscol p {
overflow-wrap: break-word;
max-width: 80ch;
}
h1 {
word-break: break-all;
max-width: 80ch;
}
hr {
max-width: 80ch;
}
.shadow-lg {
box-shadow: 0 1rem 3rem rgba(0,0,0,.175)!important;
}
.bg-white {
max-width: 21cm;
margin: 1.27cm auto;
padding: 1.27cm auto;
}
@media (max-width: 767px) {
h1 {
font-size: 30px;
}
.shadow-lg {
box-shadow: none !important;
}
}
@media (max-width: 1000px) {
h1 {
font-size: 30px;
}
.shadow-lg {
box-shadow: none !important;
}
}
@media (max-width: 767px) {
.bg-white {
margin: 1.27cm auto;
padding: 1.27cm auto;
}
}
h1 {
word-break: break-word;
overflow-wrap: break-word;
}
* {
font-family: 'Roboto', sans-serif;
}
[class^="wp-block-cover"], [class^="wp-image-"], .wp-block-cover, .wp-block-cover__image-background {
height: auto !important;
width: 75% !important;
display: block !important;
margin: 0 auto;
text-align: center;
}
.wp-block-image.is-style-default {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.aligncenter {
display: block;
margin: 0 auto;
}
figcaption {
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.feed_date {
white-space: nowrap;
}
.title-text {
font-family: 'Roboto', sans-serif;
font-weight: 400;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
padding: 10px;
margin: 10px 0;
color: #444;
}
.jumbotron {
padding-top: 1rem;
padding-bottom: 1rem;
}
.project-name {
color: white;
text-shadow: 0 0 5px white;
position: relative;
animation: glowing 7s infinite; /* Change the duration here */
background-color: transparent;
padding: 5px;
display: inline-block;
box-sizing: border-box;
}
@keyframes glowing {
0% {
text-shadow: 0 0 5px white;
}
50% {
text-shadow: 0 0 20px white, 0 0 30px white;
}
100% {
text-shadow: 0 0 5px white;
}
}

File diff suppressed because one or more lines are too long