first commit - needs content
commit
a24627c747
|
@ -0,0 +1,5 @@
|
|||
output
|
||||
venv
|
||||
**/__pycache__/
|
||||
template-dev
|
||||
.idea
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
[metadata]
|
||||
# the title of the page
|
||||
title = Dark Horse Linux News
|
||||
uid = dhlp-feeds-page
|
||||
parent = dark-horse-linux
|
|
@ -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.
|
|
@ -0,0 +1,5 @@
|
|||
[metadata]
|
||||
# the title of the page
|
||||
title = Focus on Purpose
|
||||
uid = focus-on-utility
|
||||
parent = dark-horse-linux
|
|
@ -0,0 +1,7 @@
|
|||
# Dark Horse Linux LiveCD
|
||||
|
||||
https://www.darkhorselinux.org/downloads
|
||||
|
||||
# Dark Horse Linux Installer
|
||||
|
||||
TBD
|
|
@ -0,0 +1,4 @@
|
|||
[metadata]
|
||||
title = Downloads
|
||||
uid = downloads
|
||||
parent = dark-horse-linux
|
|
@ -0,0 +1,5 @@
|
|||
[metadata]
|
||||
# the title of the page
|
||||
title = Pyrois News
|
||||
uid = pyrois-feeds-page
|
||||
parent = pyrois
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
|
@ -0,0 +1,4 @@
|
|||
[main]
|
||||
logo = rsrc/logo.png
|
||||
title = Dark Horse Linux Project
|
||||
landing = products/dark-horse-linux
|
|
@ -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
|
|
@ -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 »
|
||||
link_url = ../../pages/focus-on-utility
|
||||
parent = dark-horse-linux
|
|
@ -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 »
|
||||
link_url = https://www.darkhorselinux.org/pages/parent-leaf-status
|
||||
parent = dark-horse-linux
|
|
@ -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
|
|
@ -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 »
|
||||
link_url = https://www.darkhorselinux.org/pages/vanilla-flavored-distro
|
||||
parent = dark-horse-linux
|
|
@ -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()
|
|
@ -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 )
|
|
@ -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
|
|
@ -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)
|
|
@ -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))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 )
|
|
@ -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 ) )
|
|
@ -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 )
|
|
@ -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 -->
|
|
@ -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 -->
|
||||
|
|
@ -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>
|
|
@ -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 -->
|
|
@ -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' %}
|
|
@ -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
|
@ -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
Loading…
Reference in New Issue