diff --git a/.gitignore b/.gitignore index 78ecffe..68fd4d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ -shepich resume.pdf \ No newline at end of file +shepich resume.pdf +**/.venv +**/.env +tmp +build +dist +**/__pycache__ +custom.yaml \ No newline at end of file diff --git a/agenda.md b/agenda.md deleted file mode 100644 index c543ab3..0000000 --- a/agenda.md +++ /dev/null @@ -1,21 +0,0 @@ -## Pages -- Home -- About -- CV - - Everything in as much detail as possible -- Projects - - E&E - - M|Chroma - - SI-Formatter -- Socials (maybe better as a footer) - - Instagram - - Facebook - - LinkedIn - - GitHub - - MAL -- Bookmarks (?) - -## Reference Material -- https://www.tomscott.com/ -- http://vihart.com/ -- https://www.singingbanana.com/ diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..93a5532 --- /dev/null +++ b/config.yaml @@ -0,0 +1,41 @@ +author: Jim Shepich III +templates_folder: ./site/templates +sites: + main: + title: Jimlab + base_url: http://localhost:8000 + web_root: ./dist + build_cache: ./site + assets: + - /assets + articles: + - ./pages/*.md + template_selections: + article: templates.components.simple_article + + resume: + title: Resume + base_url: http://localhost:8000 + web_root: ./dist + git_repo: + url: ssh://gitea/jim/resume.git + build_cache: ./build/resume + assets: + - 'shepich_resume.pdf' + + dogma_jimfinium: + title: Dogma Jimfinium + description: May it bolster the skills of all who read it. + base_url: http://localhost:8000/dogma-jimfinium + git_repo: + url: ssh://gitea/jim/dogma-jimfinium.git + branch: pub + build_cache: ./build/dogma-jimfinium + web_root: ./dist/dogma-jimfinium + assets: + - assets + articles: + - '*.md' + addons: + - rss + \ No newline at end of file diff --git a/data/lists.json b/data/lists.json deleted file mode 100644 index 5608897..0000000 --- a/data/lists.json +++ /dev/null @@ -1,1137 +0,0 @@ -{ - "master":{ - "title":"Master List", - "type":"master", - "description":"A lot of the things I want to convey on this website (my likes, my dislikes, useful links, helpful tips, etc.) can be sorted into lists. So, I thought that it would be a good idea to make a page of all my lists instead of having all this information scattered across a bunch of different articles. I was inspired in large part by a book I saw called Listography: Your Life in Lists, which is a journal with a bunch of prompts for making lists of things about yourself." - }, - "quotes":{ - "title":"My Favorite Quotes", - "type":"quotes", - "description":"This list contains quotes that I live by, quotes that have shaped my fundamental understanding of things, and quotes that I otherwise just like.", - "list":[ - { - "quote":"You're not going crazy. You're going sane in a crazy world!", - "quotee":"The Tick (Ben Edlund)", - "source":"The Tick vs. The Idea Men" - }, - { - "quote":"Disrespect is a two-way street.", - "quotee":"Henry Rollins", - "source":"Get in the Van: On the Road with Black Flag" - }, - { - "quote":"No man has the right to be an amateur in the matter of physical training. It is a shame for a man to grow old without seeing the beauty and strength of which his body is capable.", - "quotee":"Socrates", - "translated":true - }, - { - "quote":"A fascist worked out today. Did you?", - "quotee":"Unknown Anarchist" - }, - { - "quote":"To progress again, man must remake himself. And he cannot remake himself without suffering. For he is both the marble and the sculptor. In order to uncover his true visage he must shatter his own substance with heavy blows of his hammer.", - "quotee":"Alexis Carrel", - "source":"Man, The Unknown", - "translated":true - }, - { - "quote":"To train the mind, first train the body.", - "quotee":"Izumi Curtis (Hiromu Arakawa)", - "source":"Fullmetal Alchemist", - "translated":true - }, - { - "title":"Bene Gesserit Litany Against Fear", - "quote":"I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear. I will permit it to pass over me and through me. And when it has gone past, I will turn the inner eye to see its path. Where the fear has gone there will be nothing. Only I will remain.", - "quotee":"Frank Herbert", - "source":"Dune" - }, - { - "quote":"It's not a bug, it's a feature." - }, - { - "quote":"...do these things happen to other people or am I just the chosen one? I decided it was the latter...", - "quotee":"Charles Bukowski", - "source":"the freeway life" - }, - { - "quote":"There are two possible outcomes: if the result confirms the hypothesis, then you've made a measurement. If the result is contrary to the hypothesis, then you've made a discovery", - "quotee":"Enrico Fermi" - }, - { - "quote":"No gods, no masters" - }, - { - "quote":"One must imagine Sisyphus happy.", - "quotee":"Albert Camus", - "source":"The Myth of Sisyphus", - "translated":true - }, - { - "quote":"It is important to draw wisdom from many different places. If we take it from only one place, it becomes rigid and stale. Understanding others, the other elements, and the other nations will help you become whole.", - "quotee":"Uncle Iroh", - "source":"Avatar: the Last Airbender" - }, - { - "quote":"It is better to be a warrior in a garden than a gardener in a war." - }, - { - "quote":"Sucking at something is the first step towards being sorta good at someething.", - "quotee":"Jake the Dog", - "source":"Adventure Time" - }, - { - "quote":"You know, I mean, like, Old-Lady Science, you know? She's a real — You got to hang on tight, you know? Because she — she — she bucks pretty hard.", - "quotee":"Morty Smith (Justin Roiland)", - "source":"Rick & Morty" - }, - { - "quote":"Through even our darkest days, we must never cease creating. Each new invention brings value to the world, be it beauty, utility, or both.", - "quotee":"Rashmi, Aether-Seer", - "card":"Ornithopter of Paradise", - "multiverseid":"522308" - }, - { - "quote":"What senses do we lack that we cannot see or hear another world all around us?", - "quotee":"Frank Herbert", - "source":"Dune" - }, - { - "quote":"Humankind cannot gain anything without first giving something in return. To obtain, something of equal value must be lost.", - "quotee":"Hiromu Arakawa", - "source":"Fullmetal Alchemist", - "title":"Law of Equivalent Exchange" - }, - { - "quote":"I would rather have had you by my side than all the blue in the world.", - "quotee":"Maggie Nelson", - "source":"Bluets" - }, - { - "quote":"Where the head goes, the body follows.", - "quotee":"Sensei Steve Tack" - }, - { - "quote":"When you enter the dojo, leave your emotions at the door.", - "quotee":"Richard Goist" - }, - { - "quote":"You're in the top half of any group.", - "quotee":"Houston Webb" - }, - { - "quote":"A fool knows no fear. A hero shows no fear.", - "card":"Intrepid Hero", - "multiverseid":"280320" - }, - { - "quote":"Only in mirrors do heroes find their equal.", - "card":"Mirror Gallery", - "multiverseid":"74555" - }, - { - "quote":"You can't truly call yourself “peaceful” unless you're capable of great violence. If you're not capable of violence you're not peaceful, you're harmless.", - "quotee":"Stefan Grant" - }, - { - "quote":"The convoluted wording of legalisms grew up around the necessity to hide from ourselves the violence we intend toward each other. Between depriving a man of one hour from his life and depriving him of his life there exists only a difference of degree. You have done violence to him, consumed his energy. Elaborate euphemisms may conceal your intent to kill, but behind any use of power over another the ultimate assumption remains: \"I feed on your energy.\"", - "quotee":"Frank Herbert", - "source":"Dune Messiah" - }, - { - "quote":"If you don't think your life is worth more than someone else's, sign your donor card and kill yourself.", - "quotee":"Dr. Gregory House", - "source":"House MD (S5E09 Last Resort)" - }, - { - "quote":"If there can be no victory, then I will fight forever.", - "quotee":"Koth of the Hammer", - "card":"Darksteel Plate", - "multiverseid":"213749" - }, - { - "quote":"If you want something done right, do it yourself.", - "quotee":"Grandma Mary Kay Shepich" - }, - { - "quote":"Now, look, we're gonna be dealing with some real serious stuff today. You might have heard of it. It's called math! And without it, none of us would even exist, so let's jump right in.", - "quotee":"Mr. Goldenfold", - "source":"Rick & Morty" - }, - { - "quote":"Life was not gentle to him,\n And the elements so mixed in him\nThat he made warfare on life\nIn the which he was slain.", - "quotee":"Edgar Lee Masters", - "source":"Spoon River Anthology (Cassius Hueffer)" - }, - { - "quote":"You don't tug on superman's cape, you don't spit into the wind, you don't pull the mask off that old lone ranger, and you don't mess around with Jim.", - "quotee":"Jim Croce", - "source":"You Don't Mess Around with Jim" - }, - { - "quote":"Inspiration is reciprocal: we all have a responsibility to each other to create.", - "card":"Oviya Pashiri, Sage Lifecrafter", - "multiverseid":"417738" - }, - { - "quote":"You're the only one who can get up when you're down. No one else.", - "quotee":"Daniel LaRusso", - "source":"Cobra Kai" - }, - { - "quote":"A true champion never stops training. You gotta keep moving forward, or else you could get stuck exactly where you are.", - "quotee":"Johnny Lawrence", - "source":"Cobra Kai" - }, - { - "quote":"Some convictions are so strong that the world must break to accommodate them.", - "card":"Vindicate", - "multiverseid":"19135" - }, - { - "quote":"Madness and genius are separated only by degrees of success.", - "quotee":"Sidar Jabari", - "card":"Inspiration", - "multiverseid":"3642" - } - - ] - }, - "words":{ - "title":"Words I Like", - "description":"This list contains a bunch of words I like. Some of them I like because of how they sound, some of them I like because of what they mean, and some of them I like because of what I associate with them.", - "list":["guppy","wrought","confectionery","immaculate","phantasmal","imperious","chevalier","glisten","hasami","genuflect","spectre","delineate","confluence","libation","Pythonic"] - }, - "mal":{ - "title":"All of the Anime I've Watched", - "description":"MyAnimeList", - "type":"external", - "link":"https://myanimelist.net/animelist/epicshepich" - }, - "goodreads":{ - "title":"Books I've Read Since High School", - "description":"My Goodreads list", - "type":"external", - "link":"https://www.goodreads.com/review/list/110528977-jim-shepich?shelf=read" - }, - "mnemonics":{ - "title":"Mnemonics I Use", - "type":"key-value", - "description":"When you've been in school as long as I have, you pick up a few tricks to help remember things.

Since antiquity, culture was passed down throughout the ages via oral tradition. Grand works of epic poetry, sacred knowledge, and more were passed from generation to generation, with each new generation memorizing these works through the use of mantras, chants, and verse. Sounds and rhythms carved out a place in people's memories that stood the test of time.

When I memorized the Periodic Table of the Elements, I found that my general strategy was to just say a group of symbols as if they spelled out a word, and then I memorized that sound (it got a little bit awkward with dysprosium-holmium-erbium). A lot of the mnemonics I present here are short, otherwise-meaningless mantras such as Roy G. Biv, SOH CAH TOA, etc. And with these mantras, I welcome you into my own oral tradition.", - "list":[ - { - "k":"An Ox and a Red Cat", - "v":"In electrochemical cells (both voltaic/galvanic and electrolytic), oxidation occurs at the anode, and reduction occurs at the cathode." - }, - { - "k":"Oil Rig", - "v":"Oxidation is loss of electrons, Reduction is gain of electrons." - }, - { - "k":"M Vem Jsun P", - "v":"Listed in order of distance from the sun, the planets of the solar system are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, (Pluto)" - }, - { - "k":"Ben Stiller's Grandma Likes Corn", - "v":"Strata of the epidermis listed in deep-to-superficial order: basale, spinosum, granulum, lucidum, corneum" - }, - { - "k":"WX water xylem, OP organics phloem", - "v":"The types of transport tissue in vascular plants, xylem transports fresh water up from the roots, while phloem transports sugars and other organics down from the leaves." - }, - { - "k":"SOH CAH TOA", - "v":"the trigonometric functions are ratios of the side lengths of a right triangle: sine is opposite over hypotenuse, cosine is adjacent over hypotenuse, and tangent is opposite over adjacent." - }, - { - "k":"HOMES", - "v":"The Great Lakes of Michigan are: Huron, Ontario, Michigan, Erie, Superior." - }, - { - "k":"Roy G. Biv", - "v":"The colors of a rainbow (visible light) in order of increasing wavelength is: red, orange, yellow, green, blue, indigo, violet." - } - ] - }, - "albums":{ - "type":"gallery", - "subtype":"album", - "title":"My Favorite Albums", - "description":"Albums I listen to in the car, albums I listen to when I'm going to sleep, albums I listen to while I work, and more. Since, I was a kid, I've had an unhealthy relationship with collecting things (bordering on hoarding), so I won't allow myself to get into collecting vinyls, but think of this as my digital \"vinyl\" collection. I've noticed that an unusally large fraction of these albums were released in 1977 and/or have covers that depict spaceships.", - "list":[ - { - "title":"Boston", - "artist":"Boston", - "year":"1976", - "link":"https://youtu.be/wu77H0gsAYY", - "cover":"https://i.pinimg.com/736x/53/4c/6e/534c6e1931b4c123d1c14d0e65ad25eb--more-than-a-feeling-boston-boston.jpg" - }, - { - "title":"Rumours", - "artist":"Fleetwood Mac", - "year":"1977", - "link":"https://youtu.be/uzEt9cATWFw", - "cover":"https://qph.fs.quoracdn.net/main-qimg-b3593ea30145790da58d4f39f4ed0231" - }, - { - "title":"Fleetwood Mac", - "artist":"Fleetwood Mac", - "year":"1975", - "link":"https://youtu.be/qs-hn1y7T7c", - "cover":"https://static-musique.qub.ca/images/covers/7b/ov/n65tnqwnjov7b_max.jpg" - }, - { - "title":"The Green Letter", - "artist":"Mitsukiyo", - "year":"2017", - "link":"https://youtu.be/e2MhdE9szv8", - "cover":"https://i.discogs.com/8x-MsOu_eY-RZkr1qyyxT3jyYWMYw5IteCG3KbHoQ-Y/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWltYWdlcy9SLTEz/NDQzMzA5LTE1NTQz/MTAxMjktMzExNi5q/cGVn.jpeg" - }, - { - "title":"Moenie and Kitchi", - "artist":"Gregory and the Hawk", - "year":"2008", - "link":"https://youtu.be/EMZOtHIL7QM", - "cover":"https://f4.bcbits.com/img/0011833953_10.jpg" - }, - { - "title":"Don't Look Back", - "artist":"Boston", - "year":"1978", - "link":"https://youtu.be/s2_xH7vOK7k", - "cover":"https://i.etsystatic.com/20562574/r/il/6769bc/2377662835/il_570xN.2377662835_juft.jpg" - }, - { - "title":"Out of the Blue", - "artist":"Electric Light Orchestra", - "year":"1977", - "link":"https://youtu.be/2f9CoaIH9FE", - "cover":"https://m.media-amazon.com/images/I/71-4vcunM+L._SL1500_.jpg" - }, - { - "title":"Master of Puppets", - "artist":"Metallica", - "year":"1986", - "link":"https://youtu.be/K6LA7v1PApU", - "cover":"https://m.media-amazon.com/images/I/71SziOTzXrL._AC_SL1425_.jpg" - }, - { - "title":"Ride the Lightning", - "artist":"Metallica", - "year":"1984", - "link":"https://youtu.be/H0XGswUuZU0", - "cover":"https://www.revolvermag.com/sites/default/files/styles/original_image__844px_x_473px_/public/media/section-media/ridethelightning.jpg?itok=Fd0KtaS2×tamp=1549044407" - }, - { - "title":"The Black Album", - "artist":"Metallica", - "year":"1991", - "link":"https://youtu.be/DqDeH3hwxfw", - "cover":"https://m.media-amazon.com/images/I/71z4zm5yohL._SL1425_.jpg" - }, - { - "title":"Point of Know Return", - "artist":"Kansas", - "year":"1977", - "link":"https://youtu.be/LepSiqpC6hA", - "cover":"http://static-1.ivoox.com/audios/2/9/5/0/7241500500592_XXL.jpg" - }, - { - "title":"Leftoverture", - "artist":"Kansas", - "year":"1976", - "link":"https://youtu.be/f5jom_YYeGU", - "cover":"https://m.media-amazon.com/images/I/91CLL0FhHbL._SL1500_.jpg" - }, - { - "title":"Heroes", - "artist":"David Bowie", - "year":"1977", - "link":"https://youtu.be/TFLLE8LPA_k", - "cover":"https://m.media-amazon.com/images/I/81VycvvC49L._SL1300_.jpg" - }, - { - "title":"Frontiers", - "artist":"Journey", - "year":"1983", - "link":"https://youtu.be/eD7HC7g0dG8", - "cover":"https://m.media-amazon.com/images/I/81RPFmr49sL._SL1500_.jpg" - }, - { - "title":"...And Justice For All", - "artist":"Metallica", - "year":"1988", - "link":"https://youtu.be/7PktvdsPXjI", - "cover":"https://images.genius.com/79908883b42660f49d71b42f4f82216c.1000x1000x1.jpg" - }, - { - "title":"Escape", - "artist":"Journey", - "year":"1981", - "link":"https://youtu.be/T8gTlHInJIA", - "cover":"https://i.discogs.com/J6fEDZdk6kTWDArqmpjsTo5rrHnl9jUr655FedNoErU/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWltYWdlcy9SLTU0/MzIwNS0xMzY1NDUw/ODczLTkzMTQuanBl/Zw.jpeg" - }, - { - "title":"Babymetal", - "artist":"Babymetal", - "year":"2014", - "link":"https://youtu.be/yIVRs6YSbOM", - "cover":"https://pbs.twimg.com/media/D0QrY-UX0AUTyfe?format=jpg&name=4096x4096" - }, - { - "title":"OMNI", - "artist":"Minus the Bear", - "year":"2010", - "link":"https://www.youtube.com/watch?v=0RsY_mjiqk4&list=PL9B0A0664758349FA", - "cover":"https://m.media-amazon.com/images/I/91DhrWe+g4L._SL1500_.jpg" - }, - { - "title":"The Stranger", - "artist":"Billy Joel", - "year":"1977", - "link":"https://youtu.be/DglOd7Wdueg", - "cover":"https://m.media-amazon.com/images/I/71JxCGwe9cL._SL1500_.jpg" - }, - { - "title":"Planet of Ice", - "artist":"Minus the Bear", - "year":"2007", - "link":"https://www.youtube.com/watch?v=iZaBD3pymH0&list=OLAK5uy_lvNJl_TeMmqtjJJj0qH3ZhW5msijTqhvQ", - "cover":"https://f4.bcbits.com/img/a2748888567_10.jpg" - }, - { - "title":"idyll", - "artist":"Steven Naylor", - "year":"2021", - "link":"https://youtu.be/dN6KMdvx8sM", - "cover":"https://i.scdn.co/image/ab67616d0000b2739117378d83714dd38a0340b7" - }, - { - "title":"Ocean Eyes", - "artist":"Owl City", - "year":"2009", - "link":"https://www.youtube.com/watch?v=Kiwea1iV6cs&list=PLOKKW8hN_oU8Tc41hbE8QzuL6Zi8Ri0pt", - "cover":"https://m.media-amazon.com/images/I/51wQ1iymmsL.jpg" - }, - { - "title":"All Things Bright and Beautiful", - "artist":"Owl City", - "year":"2011", - "link":"https://www.youtube.com/watch?v=LhTHXZfw3vY&list=PL23315D4CDDFE9D6C", - "cover":"https://m.media-amazon.com/images/I/71pTeXLg9kL._SL1200_.jpg" - }, - { - "title":"An Airplane Carried Me To Bed", - "artist":"Sky Sailing", - "year":"2010", - "link":"https://www.youtube.com/watch?v=SlOQPOR8z7g&list=PL7MiJHw2jDfFJOh3NoL6x_2EtYExminHg", - "cover":"https://m.media-amazon.com/images/I/91nhQdHvTrL._SL1500_.jpg" - }, - { - "title":"You Don't Mess Around with Jim", - "artist":"Jim Croce", - "year":"1972", - "link":"https://youtu.be/38B81cpwjNA", - "cover":"https://m.media-amazon.com/images/I/71ld2tNpCEL._SL1425_.jpg" - } - ] - }, - "_test":{ - "title":"Test", - "hidden":true, - "list":["My Favorite Movies",{"test":"It works"},"My Favorite Food",{"nested lists":["Useful Websites","Favorite foods",{"title":"cereals","dropdown":true,"list":["Cocoa Pebbles","Froot Loops"]}]}] - }, - "foods":{ - "title":"My Favorite Foods", - "dropdown":true, - "sections":[ - { - "title":"Breakfast", - "list":[{"Cereal":["Reese's Puffs","Honeycombs","Frosted Mini Wheats","Eggo Cereal","Froot Loops","Frosted Flakes"]}, "Crepes", "Pancakes","Bacon","Hash browns","Smokey links","French toast","French fries"] - }, - { - "title":"Snacks", - "list":["Pretzels (especially Old Dutch)","Popcorn (especially Orville Redenbacher)",{"Cheese":["Swiss","Colby Jack","Cheddar","Mozzarella"]},{"Candy":["Take 5","Kit Kat","Snickers","Reese's cups","Milky Way",{"Gummy bears":["Albanese","Meijer brand"]}]}] - }, - { - "title":"Fruits", - "description":"Here are some of my favorite fruits presented in a tier-list along with tips on how to get the best experience out of them. One metric I use to rank the fruits that you might find odd is the shelf life. The reason I care about the shelf life of my fruits is that I hate going to the grocery store, so I prefer fruits that I can stockpile.", - "sections":[ - { - "title":"S-Tier", - "list":[{"Mango":"When you can smell the aroma through the skin, the mango is ready to eat. Usually takes around a week from when you buy them. I loved Rubicon mango juice so much as a kid that my grandparents went to Toronto to buy it for me. Mango is my preferred flavor of smoothie."},{"Watermelon":"The best watermelons sound hollow when you knock on them and have a faded spot on their rind. Nothing beats a cool, juicy watermelon on a warm summer afternoon."},{"Lychee":"Out of all the fruits on my list, I think lychees and watermelons are the only ones that are consistently sweet. Lychees dry out your mouth a little bit, which annoying, but it's also kinda neat because that's not what you'd expect from a fruit. Lychee flavored Ramune is awesome."}] - }, - { - "title":"A-Tier", - "list":[{"Apple":"Loses points because they can be tough and hard to eat, and sometimes they just taste too sour, but follow my instructions and they'll usually be good. Plus, they can last up to 3 weeks on the shelf, and they're very versatile (apple juice, cider, doughnuts, pies, etc.). Honeycrisp is my favorite cultivar, and the best honeycrisps are the biggest and the yellowest. Apples usually taste better a week after you buy them. I'm also a fan of the Opal cultivar, which tastes like a cross-breed with pears."},{"Banana":"Taste is A-tier, but the short shelf life (and concomitant fruit flies) is a big negative. What brings them back is that bananas have such good synergy with other fruits (and other foods: see peanutbutter banana sandwich). Healthiest when they're green, sweetest when they're brown, and best when they're just yellow. I like to make banana smoothies: 150 mL milk, two bananas, a tablespoon of sugar, optionally a spoon or two of yogurt, and 7 ice cubes."},{"Peach":"Taste is S-tier, but they go bad way too fast and the window where they are soft enough to eat but not overripe is way too small. I've heard you can tell that a peach is ready to eat when it's as squishy as the meat of your thumb, but obviously I've still had issues."},{"Pear":"Like peaches, ripe pears taste divine. I don't like flavors that are too overpowering, and I find that pears are perfectly subtle. However, unless the pear is at the perfect point of ripeness, it is too tough to bite into and tastes starchy like an unripe banana."},{"Cherry":"It's hard to come by bad cherries in Michigan (just avoid the maraschinos). I'd say that the taste is B-tier, but they get points because the seeds are so fun to spit."},{"Clementine":"Ironically, the \"sweet orange\" can often be a bit too sour for my taste. Plus, I got the stomach flu during my orange juice phase, so I developed a bit of an aversion. But, clementines are usually pretty sweet, don't have that undertone that I dislike, and they last pretty long on the shelf."},{"Grape":"Grapes taste pretty good. They aren't super sweet or really juicy like other fruits, but they still taste good. The great advantage of grapes over all other fruits is that you don't need to wash your hands or your face after eating them because of their durable dry skin."}] - }, - { - "title":"B-Tier", - "list":[{"Blueberry":"Good blueberries can be really good, but bad blueberries can be really sour and nasty, and the worst thing is that since they're so small, it's always a mixed bag when you buy them. I've noticed that blueberries, like apples, tend to follow a bigger is better rule."},{"Raspberry":"Like blueberries, raspberries are a mixed bag. On average, I'd say the taste of plain raspberries is C-tier, but raspberry jelly is really good, so here they are."},{"Apricot":"They have a good flavor, but like peaches and pears, apricots have a small window where they are squishy but not overripe."}] - } - ] - }, - { - "title":"Sandwiches", - "list":[{"Peanut butter and...":["Jelly","Banana","M&M","Chocolate chip","Nutella"]},"Ham and Cheese","Hamburger"] - } - ] - }, - "mtg":{ - "title":"My Favorite Magic: the Gathering Cards", - "type":"gallery", - "subtype":"mtg-card", - "description":"", - "sections":[ - { - "title":"Best of the Best", - "list":[ - { - "name":"Battle of Wits", - "multiverseid":"83133" - }, - { - "name":"Enter the Infinite", - "multiverseid":"366411" - }, - { - "name":"Intrepid Hero", - "multiverseid":"280320" - }, - { - "name":"Ral Zarek", - "multiverseid":"470744" - }, - { - "name":"Tamiyo, the Moon Sage", - "multiverseid":"240070" - }, - { - "name":"Zuran Spellcaster", - "multiverseid":"184683" - }, - { - "name":"Sublime Epiphany", - "multiverseid":"485397" - }, - { - "name":"Ponder", - "multiverseid":"519160" - }, - { - "name":"Force of Will", - "multiverseid":"489724" - }, - { - "name":"Niv Mizzet, the Firemind", - "multiverseid":"96952" - } - ] - }, - { - "title":"Best Art/Aesthetics", - "list":[ - { - "name":"Divination", - "multiverseid":"447187" - }, - { - "name":"Syr Elenora, the Discerning", - "multiverseid":"473029" - }, - { - "name":"Serra Angel", - "multiverseid":"370602" - }, - { - "name":"Captain of the Watch", - "multiverseid":"394354" - }, - { - "name":"Sunblade Elf", - "multiverseid":"383406" - }, - { - "name":"Angelic Destiny", - "multiverseid":"220230" - }, - { - "name":"Island", - "multiverseid":"289315" - }, - { - "name":"Island", - "multiverseid":"473219" - }, - { - "name":"Aether Storm", - "multiverseid":"184722" - }, - { - "name":"Leviathan", - "multiverseid":"26619" - }, - { - "name":"Time of Ice", - "multiverseid":"442958" - }, - { - "name":"Auramancer", - "multiverseid":"441995" - }, - { - "name":"Wall of Frost", - "multiverseid":"383432" - }, - { - "name":"Gift of Orzhova", - "multiverseid":"366339" - }, - { - "name":"Fylgja", - "multiverseid":"2686" - }, - { - "name":"Call to Mind", - "multiverseid":"208218" - }, - { - "name":"Thassa's Bounty", - "multiverseid":"373662" - }, - { - "name":"Ephara, God of the Polis", - "multiverseid":"378517" - }, - { - "name":"Ephara's Radiance", - "multiverseid":"378381" - }, - { - "name":"Stream of Unconsciousness", - "multiverseid":"152720" - }, - { - "name":"Hana Kami", - "multiverseid":"370475" - } - ] - }, - { - "title":"Best Mechanics", - "list":[ - { - "name":"Cheatyface", - "multiverseid":"479416" - }, - { - "name":"Pestilence", - "multiverseid":"2119" - }, - { - "name":"Underworld Connections", - "multiverseid":"545758" - }, - { - "name":"Isochron Scepter", - "multiverseid":"292752" - }, - { - "name":"Cramped Bunker", - "multiverseid":"439518" - }, - { - "name":"Skywise Teachings", - "multiverseid":"438639" - }, - { - "name":"Serpent Generator", - "multiverseid":"159826" - }, - { - "name":"Judge's Familiar", - "multiverseid":"460143" - }, - { - "name":"Elite Arcanist", - "multiverseid":"370747" - }, - { - "name":"Crackling Perimeter", - "multiverseid":"366255" - }, - { - "name":"Aetherflux Reservoir", - "multiverseid":"417765" - }, - { - "name":"Lighthouse Chronologist", - "multiverseid":"193590" - }, - { - "name":"Intruder Alarm", - "multiverseid":"5174" - }, - { - "name":"Lobber Crew", - "multiverseid":"289218" - }, - { - "name":"Dream Halls", - "multiverseid":"397553" - }, - { - "name":"Arcane Melee", - "multiverseid":"376246" - }, - { - "name":"Disruptive Student", - "multiverseid":"21123" - }, - { - "name":"The Magic Mirror", - "multiverseid":"473013" - }, - { - "name":"Chalice of Life / Chalice of Death", - "multiverseid":["226735","226721"] - }, - { - "name":"Ovinomancer", - "multiverseid":"108863" - }, - { - "name":"Talrand, Sky Summoner", - "multiverseid":"253701" - }, - { - "name":"Odric, Master Tactician", - "multiverseid":"259670" - }, - { - "name":"Quicksilver Dagger", - "multiverseid":"292757" - }, - { - "name":"Young Pyromancer", - "multiverseid":"370600" - }, - { - "name":"Koth of the Hammer", - "multiverseid":"266362" - }, - { - "name":"Curiosity", - "multiverseid":"489304" - }, - { - "name":"Censorship", - "multiverseid":"9747" - }, - { - "name":"Bureaucracy", - "multiverseid":"9778" - }, - { - "name":"Stuffy Doll", - "multiverseid":"509639" - }, - { - "name":"Epic Struggle", - "multiverseid":"33697" - }, - { - "name":"Azor's Elocutors", - "multiverseid":"265418" - } - ] - }, - { - "title":"Best Flavor Text", - "list":[ - { - "name":"Ornithopter of Paradise", - "multiverseid":"522308" - }, - { - "name":"Mirror Gallery", - "multiverseid":"74555" - }, - { - "name":"Darksteel Plate", - "multiverseid":"213749" - }, - { - "name":"Oviya Pashiri, Sage Lifecrafter", - "multiverseid":"417738" - }, - { - "name":"Vindicate", - "image":"https://c1.scryfall.com/file/scryfall-cards/large/front/2/c/2c2d88dd-813a-4cd5-9a6a-ca6f80564078.jpg?1561756842", - "link":"https://scryfall.com/card/g07/4/vindicate" - }, - { - "name":"Raging Goblin", - "multiverseid":"393980" - }, - { - "name":"Inspiration", - "multiverseid":"3642" - }, - { - "name":"Pygmy Giant", - "multiverseid":"74333" - } - ] - }, - { - "title":"Best Fake Cards", - "list":[ - { - "name":"Slidshocking Krow", - "image":"https://i.imgur.com/57fkA9S.jpg", - "link":"https://www.reddit.com/r/MTGLardFetcher/comments/6la7e4/roborosewater_slidshocking_krow/" - }, - { - "name":"Eight Fucking Bears", - "image":"https://i.imgur.com/QT4H77w.jpg", - "link":"https://www.reddit.com/r/magicTCG/comments/3x2qwu/eight_fucking_bears/" - }, - { - "name":"Soulbond Giant", - "image":"http://cdn.themis-media.com/media/global/images/library/deriv/940/940220.jpg", - "link":"https://www.escapistmagazine.com/the-most-interesting-magic-the-gathering-cards-made-by-artificial-intelligence/" - }, - { - "name":"Black Lotus But Bri'ish", - "image":"https://i.redd.it/zv7jw7bur6871.png", - "link":"https://www.reddit.com/r/magicthecirclejerking/comments/oa5yc8/black_lotus_but_briish/" - }, - { - "name":"Countercounterspell", - "image":"https://i.imgur.com/wjm2Haw.png", - "link":"https://www.reddit.com/r/MTGLardFetcher/comments/ghi157/countercounterspell/" - }, - { - "name":"Stroke", - "image":"https://i.redd.it/onnf1xqgk7d51.png", - "link":"https://www.reddit.com/r/MTGLardFetcher/comments/hy8vk7/stroke/" - }, - { - "name":"Enter the Finite", - "image":"https://i.redd.it/5hi1qup8l7661.jpg", - "link":"https://www.reddit.com/r/MTGLardFetcher/comments/kggc1a/enter_the_finite/" - }, - { - "name":"[V]indicate", - "image":"https://i.redd.it/k4ng1j106dw71.jpg", - "link":"https://www.reddit.com/r/MTGLardFetcher/comments/qi9haz/indicate/" - }, - { - "name":"Crip Moon", - "image":"https://i.redd.it/9zwqyay1aiz71.png", - "link":"https://www.reddit.com/r/MTGLardFetcher/comments/qtjwg3/an_idea_that_came_to_me_visionlike/" - }, - { - "name":"Krenko's Command", - "image":"https://i.redd.it/7w67a5ml3wb51.png", - "link":"https://www.reddit.com/r/MTGLardFetcher/comments/hu9ij2/krenkos_command/" - }, - { - "name":"Strict Denial", - "image":"https://i.redd.it/2mp6r83ywlc41.jpg", - "link":"https://www.reddit.com/r/MTGLardFetcher/comments/et0vjw/strict_denial/" - } - ] - } - ] - }, - "movies":{ - "title":"My Favorite Movies", - "description":"This list has most of the movies I consider to be my favorites, except for the ones I forgot to add. I am not much of a film buff, so if something is missing that it seems should be on here, it's either because I forgot it or because I haven't seen it.", - "type":"gallery", - "subtype":"movie", - "list":[ - { - "title":"The Road to El Dorado", - "year":"2000", - "poster":"https://m.media-amazon.com/images/M/MV5BOTEzNWIwMzctOTE1YS00YjIyLTgwZGEtMTMxZDAzNzlmODMxXkEyXkFqcGdeQXVyMjgyMDk1MzY@._V1_FMjpg_UX1000_.jpg" - }, - { - "title":"Shrek", - "year":"2001", - "poster":"https://m.media-amazon.com/images/M/MV5BOGZhM2FhNTItODAzNi00YjA0LWEyN2UtNjJlYWQzYzU1MDg5L2ltYWdlL2ltYWdlXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_FMjpg_UX1007_.jpg" - }, - { - "title":"Click", - "year":"2006", - "poster":"https://m.media-amazon.com/images/M/MV5BMTA1MTUxNDY4NzReQTJeQWpwZ15BbWU2MDE3ODAxNw@@._V1_FMjpg_UX450_.jpg" - }, - { - "title":"Princess Mononoke", - "year":"1997", - "poster":"https://m.media-amazon.com/images/M/MV5BNGIzY2IzODQtNThmMi00ZDE4LWI5YzAtNzNlZTM1ZjYyYjUyXkEyXkFqcGdeQXVyODEzNjM5OTQ@._V1_.jpg" - }, - { - "title":"Spirited Away", - "year":"2001", - "poster":"https://m.media-amazon.com/images/M/MV5BMjlmZmI5MDctNDE2YS00YWE0LWE5ZWItZDBhYWQ0NTcxNWRhXkEyXkFqcGdeQXVyMTMxODk2OTU@._V1_.jpg" - }, - { - "title":"Space Jam", - "year":"1996", - "poster":"https://m.media-amazon.com/images/M/MV5BMDgyZTI2YmYtZmI4ZC00MzE0LWIxZWYtMWRlZWYxNjliNTJjXkEyXkFqcGdeQXVyNjY5NDU4NzI@._V1_.jpg" - }, - { - "title":"Fight Club", - "year":"1999", - "poster":"https://m.media-amazon.com/images/M/MV5BMmEzNTkxYjQtZTc0MC00YTVjLTg5ZTEtZWMwOWVlYzY0NWIwXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_.jpg" - }, - { - "title":"Shrek 2", - "year":"2002", - "poster":"https://m.media-amazon.com/images/M/MV5BMDJhMGRjN2QtNDUxYy00NGM3LThjNGQtMmZiZTRhNjM4YzUxL2ltYWdlL2ltYWdlXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_.jpg" - }, - { - "title":"Elf", - "year":"2003", - "poster":"https://m.media-amazon.com/images/M/MV5BMzUxNzkzMzQtYjIxZC00NzU0LThkYTQtZjNhNTljMTA1MDA1L2ltYWdlL2ltYWdlXkEyXkFqcGdeQXVyMTMxODk2OTU@._V1_FMjpg_UX1000_.jpg" - }, - { - "title":"Die Hard", - "year":"1988", - "poster":"https://m.media-amazon.com/images/M/MV5BZjRlNDUxZjAtOGQ4OC00OTNlLTgxNmQtYTBmMDgwZmNmNjkxXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_.jpg" - }, - { - "title":"The Karate Kid", - "year":"1984", - "poster":"https://m.media-amazon.com/images/M/MV5BNTkzY2YzNmYtY2ViMS00MThiLWFlYTEtOWQ1OTBiOGEwMTdhXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_.jpg" - }, - { - "title":"Tim and Eric's Billion Dollar Movie", - "year":"2012", - "poster":"https://m.media-amazon.com/images/M/MV5BMTU0NTQ5NDYwMV5BMl5BanBnXkFtZTcwNjUzNzUxNw@@._V1_.jpg" - }, - { - "title":"Monty Python and the Holy Grail", - "year":"1975", - "poster":"https://m.media-amazon.com/images/M/MV5BN2IyNTE4YzUtZWU0Mi00MGIwLTgyMmQtMzQ4YzQxYWNlYWE2XkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_.jpg" - }, - { - "title":"Dragon Quest: Your Story", - "year":"2019", - "poster":"https://m.media-amazon.com/images/M/MV5BM2Q5YTI0NTQtOGFlOC00MTEzLTg2NDYtM2VhNDk1ZTllNTNiXkEyXkFqcGdeQXVyMjU0ODQ5NTA@._V1_FMjpg_UX1000_.jpg" - }, - { - "title":"Dune", - "year":"2021", - "poster":"https://m.media-amazon.com/images/M/MV5BN2FjNmEyNWMtYzM0ZS00NjIyLTg5YzYtYThlMGVjNzE1OGViXkEyXkFqcGdeQXVyMTkxNjUyNQ@@._V1_.jpg" - }, - { - "title":"The Princess Bride", - "year":"1987", - "poster":"https://m.media-amazon.com/images/M/MV5BMGM4M2Q5N2MtNThkZS00NTc1LTk1NTItNWEyZjJjNDRmNDk5XkEyXkFqcGdeQXVyMjA0MDQ0Mjc@._V1_.jpg" - }, - { - "title":"Sky High", - "year":"2005", - "poster":"https://m.media-amazon.com/images/M/MV5BZjA2NmY1OTQtMjE4Mi00NGRkLWFmODUtM2Q3ZTRlYjZhNWYwXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_.jpg" - }, - { - "title":"The Pacifier", - "year":"2005", - "poster":"https://m.media-amazon.com/images/M/MV5BMTE5MTcxOTQxNl5BMl5BanBnXkFtZTYwMzk3Nzg2._V1_FMjpg_UX1000_.jpg" - }, - { - "title":"Back to the Future", - "year":"1985", - "poster":"https://m.media-amazon.com/images/M/MV5BZmU0M2Y1OGUtZjIxNi00ZjBkLTg1MjgtOWIyNThiZWIwYjRiXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_.jpg" - }, - { - "title":"Matilda", - "year":"1996", - "poster":"https://m.media-amazon.com/images/M/MV5BNzRhYmZhOWEtZjA4NC00MTU1LWE1ODgtN2Q5Y2QxN2JlNDljXkEyXkFqcGdeQXVyMjUzOTY1NTc@._V1_.jpg" - }, - { - "title":"Kung Fu Panda", - "year":"2008", - "poster":"https://m.media-amazon.com/images/M/MV5BODJkZTZhMWItMDI3Yy00ZWZlLTk4NjQtOTI1ZjU5NjBjZTVjXkEyXkFqcGdeQXVyODE5NzE3OTE@._V1_.jpg" - }, - { - "title":"Megamind", - "year":"2010", - "poster":"https://m.media-amazon.com/images/M/MV5BMTAzMzI0NTMzNDBeQTJeQWpwZ15BbWU3MDM3NTAyOTM@._V1_.jpg" - }, - { - "title":"Labyrinth", - "year":"1986", - "poster":"https://m.media-amazon.com/images/M/MV5BMjM2MDE4OTQwOV5BMl5BanBnXkFtZTgwNjgxMTg2NzE@._V1_.jpg" - }, - { - "title":"Frozen", - "year":"2013", - "poster":"https://m.media-amazon.com/images/M/MV5BMTQ1MjQwMTE5OF5BMl5BanBnXkFtZTgwNjk3MTcyMDE@._V1_.jpg" - }, - { - "title":"Tangled", - "year":"2010", - "poster":"https://m.media-amazon.com/images/M/MV5BMTAxNDYxMjg0MjNeQTJeQWpwZ15BbWU3MDcyNTk2OTM@._V1_.jpg" - }, - { - "title":"Cloudy with a Chance of Meatballs", - "year":"2009", - "poster":"https://m.media-amazon.com/images/M/MV5BMTg0MjAwNDI5MV5BMl5BanBnXkFtZTcwODkyMzg2Mg@@._V1_.jpg" - }, - { - "title":"Joe Dirt", - "year":"2001", - "poster":"https://m.media-amazon.com/images/M/MV5BMTE5NDgxNzU1MV5BMl5BanBnXkFtZTYwODQ4ODE3._V1_.jpg" - }, - { - "title":"Star Wars III: Revenge of the Sith", - "year":"2005", - "poster":"https://m.media-amazon.com/images/M/MV5BNTc4MTc3NTQ5OF5BMl5BanBnXkFtZTcwOTg0NjI4NA@@._V1_.jpg" - }, - { - "title":"Deadpool", - "year":"2016", - "poster":"https://m.media-amazon.com/images/M/MV5BYzE5MjY1ZDgtMTkyNC00MTMyLThhMjAtZGI5OTE1NzFlZGJjXkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_FMjpg_UX1000_.jpg" - }, - { - "title":"Kung Pow: Enter the Fist", - "year":"2002", - "poster":"https://m.media-amazon.com/images/M/MV5BMGQxZDEwZDctMjNkMi00YmIxLTgyN2MtYmJhYjEzZGY0NjljXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_FMjpg_UX1000_.jpg" - }, - { - "title":"Django Unchained", - "year":"2012", - "poster":"https://m.media-amazon.com/images/M/MV5BMjIyNTQ5NjQ1OV5BMl5BanBnXkFtZTcwODg1MDU4OA@@._V1_.jpg" - }, - { - "title":"The Prestige", - "year":"2006", - "poster":"https://m.media-amazon.com/images/M/MV5BMjA4NDI0MTIxNF5BMl5BanBnXkFtZTYwNTM0MzY2._V1_FMjpg_UX1000_.jpg" - }, - { - "title":"Oldboy", - "year":"2003", - "poster":"https://m.media-amazon.com/images/M/MV5BMTI3NTQyMzU5M15BMl5BanBnXkFtZTcwMTM2MjgyMQ@@._V1_FMjpg_UX1000_.jpg" - }, - { - "title":"Zombieland", - "year":"2009", - "poster":"https://m.media-amazon.com/images/M/MV5BMTU5MDg0NTQ1N15BMl5BanBnXkFtZTcwMjA4Mjg3Mg@@._V1_FMjpg_UX1000_.jpg" - } - ] - }, - "countries":{ - "title":"Countries I've Visited", - "description":"This list does not include countries that I have been in but not outside an airport (e.g. Germany).", - "list":["United States","Canada","Mexico","Belize","Puerto Rico","Jamaica","Bahamas","Japan","India"] - }, - "internet":{ - "title":"Guide to the Internet", - "description":"This is a collection of all of the websites on the Internet I like to use.", - "type":"key-value", - "dropdown-open":"true", - "sections":[ - { - "title":"Computer Science", - "description":"When I was in middle school, I taught myself how to program using various tutorials and resources on the Internet. Here are the resources that I've found useful over the years.", - "sections":[ - { - "title":"General Purpose", - "list":[ - {"k":"StackOverflow","link":"https://stackoverflow.com/","v":"A Q&A forum about all things computer science. If you're trying to figure out how to do something, Google it, and most of the time you'll find a StackOverflow thread about it. If you're trying to debug an error, just paste the error into Google and again, StackOverflow will probably have the answer."}, - {"k":"Codecademy","link":"https://www.codecademy.com/","v":"This was my starting point for learning most of the languages I know. Codecademy has courses of tutorials to get you up and running with almost all of the most popular languages. It has in-browser interpreters/compilers, which are really convenient. In the last few years, a lot of the content has become paid, which is really a travesty, but the free courses can still get you started."}, - {"k":"GitHub","link":"https://github.com/","v":"The most popular platform for hosting code. GitHub allows developers to manage and track changes with a version control software called Git."}, - {"k":"Esolang Wiki","v":"A Wiki about esoteric programming languages (esolangs). Many of these languages were created as jokes, but trying to use these languages can be a genuinely beneficial exercise. Because of how different esolangs are from normal languages, they make you approach problems in new and creative ways.","link":"https://esolangs.org/"}, - {"k":"Atom Editor","dropdown":true,"link":"https://atom.io/","v":"Atom is my IDE of choice. It's free, open-source, integrates nicely with GitHub, and has a slick, uncluttered UI (as compared to something like Eclipse). Some of the packages I've found that improve my Atom experience are:","list":[ - {"script":"By far the most important Atom package, script allows you to run code from most popular languages within the Atom editor. Hit CTRL+SHIFT+B and the console will pop up at the bottom and your code will do its thing. Go to the settings and change the first setting (CWD) to \"Directory of the script\"."}, - {"autoclose-html":"Pretty self-explanatory; this package will automatically create the corresponding close tag when you type in an open HTML tag. If you don't want the close tag on the next line, then type the wildcard (*) in the Force Inline field."}, - {"markdown-preview-plus":"Will render Markdown syntax in a new pane when you press CTRL+SHIFT+M. If you want to include LaTeX-formatted math script in your Markdown, tick \"Enable Math Rendering By Default\" in the Math Options."}, - {"minimap":"Shows a zoomed-out view of your code, which can be helpful for moving through a long script, especially if you know what regions of the code look like."}, - {"pentagons":"Animates floating polygons in the background of the code, just for fun :)"}, - {"php-server":"Allows you to spin up a localhost PHP server in the directory of your choosing, which is really convenient for if you want to develop PHP and actually be able to run it."}, - {"sync-settings":"Allows you to sync your Atom settings and installed packages across multiple devices/installations by storing data in a private GitHub gist."} - ]} - ] - }, - { - "title":"Web Development", - "list":[ - {"k":"W3Schools","link":"https://www.w3schools.com/","v":"The best documentation for learning JavaScript, HTML, and CSS. You can often learn a lot by just browsing through documentation to learn what you can do with the language."}, - {"k":"Font Awesome","link":"https://fontawesome.com/start","v":"A huge library of icons that you can use on your website. Font Awesome integrates beautifully with CSS, and it's what I used to make those silver social media icons on the footer of this website. There are paid upgrades, but the basic free package still offers a huge selection of icons. Check out some cool Font Awesome icons: "} - ] - }, - { - "title":"Home Server", - "list":[ - {"k":"Installing Apache","v":"Apache HTTP Server is a widely popular open-source server software. This link is to a tutorial for setting up Apache on a Windows machine.","link":"https://www.sitepoint.com/how-to-install-apache-on-windows/"}, - {"k":"Configuring PHP","v":"Back-end development is a key component of creating a dynamic, interactive website, and PHP is a popular language for back-end. This link is a tutorial for installing and configuring PHP on your Apache server.","link":"https://www.sitepoint.com/how-to-install-php-on-windows/"}, - {"k":"Certbot","link":"https://certbot.eff.org/instructions","v":"A free SSL/TLS certificate generator application. Just follow the instructions, and Certbot will take care of the rest for you. Make sure you renew your certificate periodically or set up automatic renewal."}, - {"k":"DokuWiki","link":"https://www.dokuwiki.org/","v":"If you want to create a wiki on your server, DokuWiki is the way to do it. It's free, open-source, and user-friendly. All you have to do is download a version of it and extract the files into whatever directory of your server that you want to be the root of the wiki. After that, everything can be configured in your browser through the wiki's settings."} - ] - } - ] - }, - { - "title":"Websites for Students", - "sections":[ - { - "title":"General", - "list":[ - {"k":"BibMe","v":"Generates formatted citations (MLA, APA, ACS, etc.) from the link to or title of a source. Makes writing a bibliography a lot less painful.","link":"https://www.bibme.org/"} - ] - }, - { - "title":"Johns Hopkins Engineering for Professionals", - "list":[ - {"k":"Course Evaluation Results Database","v":"This is where all the course/instructor evaluations are compiled.","link":"https://ep.jhu.edu/course-evaluation-results-public-reports"} - ] - }, - { - "title":"University of Michigan", - "list":[ - {"k":"LSA Course Guide","link":"https://www.lsa.umich.edu/cg/","v":"This may have changed, but when I was a student, the course catalog on Wolverine Access had an awful interface, so I always used the LSA Course Guide."}, - {"k":"Vertere","v":"The inventory management system for the chemistry department. This link was always way too difficult for me to find, so I finally ended up bookmarking it. You'll need login credentials, which are hard to come by.","link":"http://chem-vim.chem.lsa.umich.edu/VimWebV2/(S(jao5q1jgvvfjwzqi0rlpwejc))/Login.aspx"}, - {"k":"NMR Scheduler","v":"If you want to use the department NMR spectrometers, schedule your time here.","link":"https://apps-prod.chem.lsa.umich.edu/nmr/horarios/index.php#now"}, - {"k":"GC-MS Scheduler","v":"If you want to use the department GC-MS, schedule your time here.","link":"https://sites.google.com/a/umich.edu/lsa-chemistry-instrumentation-reservations/home/shimadzu-gcms"} - ] - }, - { - "title":"Learning Japanese", - "list":[ - {"k":"Free Japanese Lessons","v":"Using this website, I learned enough Japanese to find my way around Japan as well as place out of the first year of college Japanese. \"Free\" is not entirely accurate, but the one-time payment of $12.95 is well worth it. The site had a major update since I last used it, but I checked it out and it seems even higher quality than before.","link":"https://freejapaneselessons.com/"}, - {"k":"Rikaikun","v":"A Google Chrome extension that displays the reading and English translation of Japanese words when you hover over them. Rikaikun is very helpful when you're trying to read something that has a bunch of kanji that you don't know.","link":"https://chrome.google.com/webstore/detail/rikaikun/jipdnfibhldikgcjhfnomkfpcebammhp?hl=en"}, - {"k":"Suzuki-Kun","v":"This software analyzes inputted Japanese text and predicts the pitch-accent of the text. It displays pitch contours over the text and will generate synthesized speech so you can listen along. Use this tool if you want to take your pronunciation to the next level, or if you want an mp3 of some text (maybe for help memorizing a speech).","link":"http://www.gavo.t.u-tokyo.ac.jp/ojad/eng/phrasing/index"}, - {"k":"Vocabulary Lists","v":"Free Japanese Lessons has a lot of great wordbanks, but if you're looking for some esoteric subjects, you're going to go off the beaten path. These are some of the lists that I've found useful:", - "list":[ - "Board Games", - "Chemical Elements", - "Pokemon Names" - ] - } - ] - } - ] - }, - { - "title":"Digital Acquisitions", - "description":"Useful links for all you 21st century swashbucklers out there.", - "list":[ - {"k":"Library Genesis (libgen)","link":"https://libgen.is/","v":"Textbooks, comic books, novels, you name it and there's a good chance that libgen has it."}, - {"k":"SciHub","link":"https://sci-hub.se/","v":"Paste in the link or DOI of a scientific article, SciHub will almost always get the PDF for you. Research is paid for largely by government grants funded by tax dollars, so you should be entitled to its results."}, - {"k":"YT1s","link":"https://yt1s.com/","v":"Allows you to download videos or audio files from YouTube."}, - {"k":"WatchCartoonOnline","link":"https://www.wcostream.com/","v":"This has my site for cartoons and anime since around 2013."}, - {"k":"Nyaa","link":"https://nyaa.si/","v":"The best magnets for torrenting anime."}, - {"k":"AnimeShow","link":"https://www2.animeshow.tv/","v":"The only site I've found for watching raw (unsubtitled) anime, in case you want to practice your Japanese listening comprehension."}, - {"k":"AQ Stream","link":"https://aqstream.com/jp","v":"For streaming live Japanese television. It only has a few channels, but they're the big ones: TV Asahi, TV Tokyo, Fuji TV, TBS, NHK, and a few others."}, - {"k":"Lj Video Downloader","v":"This is an app for Android, but it is the best video downloader I've found. Paste in any website with a video in it, and Lj will find the m3u8 link and let you download the video.","link":"https://play.google.com/store/apps/details?id=com.leavjenn.m3u8downloader&hl=en_US&gl=US"}, - {"k":"r/Piracy","link":"https://www.reddit.com/r/Piracy/","v":"If you're trying to figure out how to pirate something, chances are that somebody else has tried too. Maybe they posted about it in the Piracy subreddit."}, - {"k":"Internet Archive","link":"https://archive.org/","v":"The Internet Archive isn't traditionally considered a piracy website, but if you can rent a book, you can flip through all the pages and get scans of them in your browser cache. If your browser is Chrome, you can use ChromeCacheView to extract those scans. Then, just use an image to PDF converter, like this one I made, and you're in business."} - ] - } - ] - } -} diff --git a/data/pages.json b/data/pages.json deleted file mode 100644 index 29791ab..0000000 --- a/data/pages.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "home" : { - "name" : "Home", - "query_value" : "home", - "file" : "home.html", - "index" : 0 - }, - - "404" : { - "name" : "404", - "query_value" : "404", - "file" : "404.html", - "index" : -1 - }, - - "about" : { - "name" : "About", - "query_value" : "about", - "file" : "about.html", - "index" : 1 - }, - - "resume" : { - "name" : "Resume", - "query_value" : "resume", - "link" : "pages/shepich resume.pdf", - "index" : 2 - }, - - "epics" : { - "name" : "Epics & Emprises", - "query_value" : "epics", - "link" : "https://epics.shepich.com", - "index" : -1 - }, - - "lists" : { - "name" : "Lists", - "query_value" : "lists", - "file" : "lists.html", - "index" : -1 - }, - - "don-info" : { - "name" : "Info for Don", - "query_value" : "don-info", - "file" : "don-info.html", - "index" : -1 - } -} diff --git a/data/socials.json b/data/socials.json deleted file mode 100644 index 85d4e4c..0000000 --- a/data/socials.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "platform": "Instagram", - "icon": "instagram", - "link": "https://instagram.com/epicshepich" - }, - { - "platform": "GitHub", - "icon": "github", - "link": "https://github.com/epicshepich" - }, - { - "platform": "Facebook", - "icon": "facebook-square", - "link": "https://www.facebook.com/jim.shepich/" - }, - { - "platform": "LinkedIn", - "icon": "linkedin", - "link": "https://www.linkedin.com/in/jshepich/" - }, - { - "platform": "Discord", - "icon": "discord", - "link": "https://discordapp.com/users/epicshepich#0131" - } -] diff --git a/index.php b/index.php deleted file mode 100644 index dc0525b..0000000 --- a/index.php +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - <?php - echo $page->name." | Jim Shepich"; - ?> - - -
- Jim Shepich III -
- -
- link)){ - echo ""; - //If the directory assigns the page to an external link, redirect to that location. - } - - if(isset($page->iframe)){ - echo ""; - //If the directory says to embed the page in an iframe, then do that. - } else { - echo file_get_contents(__DIR__ ."/pages/".$page->file); - //Otherwise, just write the HTML of the page in the content area. - } - - ?> -
- - - - diff --git a/info.php b/info.php deleted file mode 100644 index 47b3cb9..0000000 --- a/info.php +++ /dev/null @@ -1,5 +0,0 @@ - \ No newline at end of file diff --git a/jimsite/__init__.py b/jimsite/__init__.py new file mode 100644 index 0000000..76f716a --- /dev/null +++ b/jimsite/__init__.py @@ -0,0 +1,78 @@ +import os +import yaml +from dotmap import DotMap +import logging + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access +from .templating import format_html_template, map_templates, TemplateSelections +from .assets import pull_git_repo, copy_assets +from .articles import ArticleMetadata, load_markdown, build_articles, build_index +from .blog import build_blog_archive, build_rss_feed + + +def build_site(site: SiteConfig, templates: DotMap): + '''The pipeline for building a site defined in config.yaml.''' + + logger.info(f'Building site "{site.title}".') + + # Do not build a site marked as unpublished. + if not site.published: + logger.info(f'"{site.title}" is not published. Skipping.') + return None + + # Initialize the build cache and web root, in case they do not exist. + logger.debug(f'Creating build cache: {site.build_cache}') + os.makedirs(site.build_cache, exist_ok = True) + logger.debug(f'Creating web root: {site.web_root}') + os.makedirs(site.web_root, exist_ok = True) + + # If the site is built from a git repo, pull that repo into the build cache. + if site.git_repo: + logger.info(f'Cloning/pulling git repo from {site.git_repo}') + pull_git_repo(site.git_repo, site.build_cache) + + # Copy the sites assets into the web root. + logger.info(f'Copying static assets.') + copy_assets(site) + + # Load the site's articles into an index. + logger.info('Building index of articles.') + index = build_index(site) + + # Determine which templates are to be used for explicit applications, e.g. + # the tag component. + logger.info('Loading selected templates.') + template_selections = TemplateSelections(site, templates) + + # Generate HTML pages for the articles. + logger.info('Building articles.') + build_articles(site, index, templates, template_selections) + + if len(site.articles or []): + logger.info('Building archive of articles.') + build_blog_archive(site, index, template_selections, templates = templates) + + if 'rss' in (site.addons or []): + logger.info('Building RSS feed.') + build_rss_feed(site, index) + else: + logger.debug('Addon "rss" not elected.') + + +def build(config_filepath = './config.yaml'): + '''Pipeline for building the entire website, including all sites defined in config.yaml.''' + logger.info('Loading config.') + with open(config_filepath, 'r') as config_file: + config = yaml.safe_load(config_file.read()) + + logger.info('Loading global templates.') + templates = map_templates(config['templates_folder']) + + for site in config['sites'].values(): + build_site(SiteConfig(**site), templates) + + + diff --git a/jimsite/__main__.py b/jimsite/__main__.py new file mode 100644 index 0000000..e6cf236 --- /dev/null +++ b/jimsite/__main__.py @@ -0,0 +1,15 @@ +import argparse +from jimsite import build + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + prog='Jimsite', + description='A Python-based templating engine for building static websites from Markdown documents.', + epilog='Gonna be a hot one!' + ) + + parser.add_argument('-c', '--config', type=str, default='config.yaml', help='Specifies the path to a YAML config file; defaults to config.yaml in the CWD.') + + args = parser.parse_args() + + build(config_filepath = args.config) \ No newline at end of file diff --git a/jimsite/articles.py b/jimsite/articles.py new file mode 100644 index 0000000..ec803b1 --- /dev/null +++ b/jimsite/articles.py @@ -0,0 +1,132 @@ +import os +import glob +import yaml +import markdown +import pydantic +from typing import Optional, Union +from dotmap import DotMap +from datetime import date + +from .common import filepath_or_string, SiteConfig +from .templating import format_html_template, TemplateSelections + +class ArticleMetadata(pydantic.BaseModel): + '''A model for the YAML frontmatter included with Markdown articles.''' + title: str + date: date + published: bool + tags: list + author: Optional[str] = None + lastmod: Optional[date] = None + thumbnail: Optional[str] = None + description: Optional[str] = None + +class Article(pydantic.BaseModel): + '''A model for a Markdown article, including its YAML frontmatter + metadata, as well as its path relative to the build cache root.''' + path: str + content: str + metadata: Optional[ArticleMetadata] = None + + +def load_markdown(md: str) -> tuple[Optional[ArticleMetadata], str]: + '''Loads a Markdown file into a (metadata: ArticleMetadata, content: str) pair.''' + + # Load the file contents if a filepath is specified, and strip document delimiters ('---'). + md = filepath_or_string(md).strip().strip('---').strip() + + # If there is no `---` delimiter, then the article has no metadata. + if '---' not in md.strip('---'): + return None, md + + # Split the metadata from the contents. + [raw_metadata, raw_article] = md.split('---') + + # Use YAML to parse the metadata. + metadata = yaml.safe_load(raw_metadata) + + # Convert the contents to a HTML string. + content = markdown.markdown(raw_article) + + return ArticleMetadata(**metadata), content + + +def build_index(site: SiteConfig) -> dict[str, Article]: + '''Loads the sites articles into an index mapping the filename + to an Article object.''' + + index = {} + + # Expand any globbed expressions. + expanded_article_list = [] + for a in site.articles or {}: + expanded_article_list.extend( + # Article paths are defined relative to the build cache; construct the full path. + glob.glob(f"{site.build_cache}/{a.removeprefix('./').lstrip('/')}") + ) + + + for article_full_path in expanded_article_list: + metadata, content = load_markdown(article_full_path) + + # Skip unpublished articles. + if not metadata.published: + continue + + # Construct the article's path for the index by discarding the build cache + # and replacing .md with .html. + # article_path = article_full_path\ + # .removeprefix(site.build_cache)\ + # .lstrip('/')\ + # .replace('.md','.html') + # TODO: add tolerance for a hierarchical article directory structure. + article_path = os.path.basename(article_full_path).replace('.md', '.html') + + index[article_path] = Article( + path = article_path, + content = content, + metadata = metadata + ) + + return index + + +def format_article_tags(tags: list[str], tag_template, **kwargs) -> list[str]: + '''Generates HTML article tag components from a list of tag names.''' + return [ + format_html_template(tag_template, tag_name = t, **kwargs) for t in tags + ] + + +def build_articles( + site: SiteConfig, + index: dict[str, Article], + templates: DotMap, + template_selections: TemplateSelections + ): + '''Generates HTML files for all of a given site's Markdown articles + by interpolating the contents and metadata into the HTML templates.''' + + for article in index.values(): + article_html = format_html_template( + template_selections['article'], + article = article, + blog_tags = ' '.join(format_article_tags( + article.metadata.tags, template_selections['tag'], site = site + )), + templates = templates, + site = site + ) + + page_html = format_html_template( + template_selections['page'], + content = article_html, + templates = templates, + site = site + + ) + + with open(f"{site.web_root.rstrip('/')}/{article.path}", 'w') as f: + f.write(page_html) + + \ No newline at end of file diff --git a/jimsite/assets.py b/jimsite/assets.py new file mode 100644 index 0000000..70f13dd --- /dev/null +++ b/jimsite/assets.py @@ -0,0 +1,54 @@ +import os +import glob +import shutil +from .common import run, GitRepo, SiteConfig + + +def pull_git_repo(repo: GitRepo, build_cache: str) -> None: + '''Pulls/clones a repo into the build cache directory.''' + + # If a repo exists in the build cache, pull to it. + if os.path.exists(f'{build_cache}/.git'): + + # If a branch is specified, check out to it. + if repo.branch is not None: + run(f"git checkout {repo.branch}") + run(f"git -C {build_cache} pull {repo.remote}{' '+repo.branch if repo.branch else ''}") + + # If the build cache is empty, clone the repo into it. + else: + run(f"git clone {repo.url}{(' -b '+repo.branch) if repo.branch else ''} -o {repo.remote} {build_cache}") + + +def copy_assets(site: SiteConfig) -> None: + '''Copies the list of site assets from the build cache to the web root. + The asset list can include globs (*). All paths are resolved relative to + the build cache root, and will be copied to an analogous location in the + web root.''' + + # Expand any globbed expressions. + expanded_asset_list = [] + for a in site.assets: + expanded_asset_list.extend( + # Assets are defined relative to the build cache; construct the full path. + glob.glob(f'{site.build_cache}/{a.lstrip("/")}') + ) + + for asset in expanded_asset_list: + + # Construct the destination path analogous to the source path + # but in the web root instead of the build cache. + destination = f'{site.web_root}/What the program does{a.lstrip("/")}' + + # Delete existing files. + shutil.rmtree(destination, ignore_errors=True) + + # Copy the asset. + if os.path.isdir(asset): + shutil.copytree(asset, destination) + elif os.path.isfile(asset): + shutil.copyfile(asset, destination) + else: + continue + + return None \ No newline at end of file diff --git a/jimsite/blog.py b/jimsite/blog.py new file mode 100644 index 0000000..59229a3 --- /dev/null +++ b/jimsite/blog.py @@ -0,0 +1,136 @@ +import rfeed +from datetime import datetime + + +from .common import SiteConfig +from .articles import ArticleMetadata, Article, format_article_tags +from .templating import format_html_template, TemplateSelections + + +def build_tag_index(index: dict[str, Article], wildcard ='*') -> dict[str, list[Article]]: + '''Creates an inverted index mapping each article tag to a postings list of articles + with said tag.''' + tag_index = {} + + # Index the articles in ascending order of original publication date. + for article in sorted(index.values(), key = lambda a: a.metadata.date): + + # Add all articles to the wildcard tag's postings list. + if wildcard is not None: + tag_index[wildcard] = (tag_index.get(wildcard,[])) + [article] + + # Add the article to each of its tags' posting lists. + for tag in article.metadata.tags: + tag_index[tag] = (tag_index.get(tag,[])) + [article] + + return tag_index + + +def build_blog_archive( + site: SiteConfig, + index: dict[str, Article], + template_selections: TemplateSelections, + **kwargs + ) -> str: + '''Converts an index, formatted as filestem: (metadata, contents) dict, + into an HTML page containing the list of articles, sorted from newest to oldest. + + Note: partials must be expanded into the kwargs, as they are needed to generate + the overall page. + ''' + + # Add each article as a list item to an unordered list. + archive_html_list = '' + + tag_index = build_tag_index(index) + + tag_selector_options = [] + tag_selector_css_rules = [f''' + body:has(input[name="tag-selector"][value="*"]:checked) li:has(.blog-tag){{{{ + display: list-item!important; + }}}} + '''] + + # Add tag selector options in descending order of article count. + for tag, articles in sorted(tag_index.items(), key = lambda item : -len(item[1])): + tag_selector_options.append(format_html_template( + template_selections['tag_selector_option'], + tag_name = tag, + number_with_tag = len(articles), + site = site, + **kwargs + )) + if tag == '*': + continue + tag_selector_css_rules.append(f''' + body:has(input[name="tag-selector"]:not([value="{tag}"]):checked) li:has(.blog-tag[data="{tag}"]){{{{ + display: none; + }}}} + body:has(input[name="tag-selector"][value="{tag}"]:checked) li:has(.blog-tag[data="{tag}"]){{{{ + display: list-item!important; + }}}} + ''') + + # For Python 3.9-friendliness. + newline = '\n' + + # Generate the archive article. + archive_html_article = format_html_template( + template_selections['archive_article'], + content = archive_html_list, + tag_selector_options = ' '.join(tag_selector_options), + tag_selector_css_rules = f'', + site = site, + **kwargs + ) + + # Interpolate the article into the overall page template. + archive_html_page = format_html_template( + template_selections['page'], + content = archive_html_article, + site = site, + **kwargs + ) + + with open(f"{site.web_root.rstrip('/')}/archive.html", 'w') as f: + f.write(archive_html_page) + + +def build_rss_feed(site: SiteConfig, index: dict[str, Article]): + feed = rfeed.Feed( + title = site.title, + link = f"{site.base_url.rstrip('/')}/rss.xml", + description = site.description, + language = "en-US", + lastBuildDate = datetime.now(), + items = [ + rfeed.Item( + title = article.metadata.title, + link = f"{site.base_url.rstrip('/')}/{article.path}", + description = article.metadata.description, + author = article.metadata.author, + guid = rfeed.Guid(article.path), + pubDate = datetime( + article.metadata.date.year, + article.metadata.date.month, + article.metadata.date.day + ) + ) + for article in index.values() + ] + ) + + with open(f"{site.web_root.rstrip('/')}/rss.xml", 'w') as f: + f.write(feed.rss()) \ No newline at end of file diff --git a/jimsite/common.py b/jimsite/common.py new file mode 100644 index 0000000..05752b3 --- /dev/null +++ b/jimsite/common.py @@ -0,0 +1,78 @@ +import os +import inspect +import subprocess +import pydantic +from dotmap import DotMap +from typing import Optional +from datetime import date, datetime + +run = lambda cmd: subprocess.run( + cmd.split(' '), + stdout = subprocess.PIPE, + stderr = subprocess.PIPE +) + +def filepath_or_string(s: str) -> str: + '''Loads the contents of a string if it is a filepath, otherwise returns the string.''' + if os.path.isfile(s): + with open(s, 'r') as f: + return f.read() + else: + return s + + +class GlobalVars(pydantic.BaseModel): + '''Static-valued global variables to be interpolated into any HTML templates.''' + today: date = datetime.today() + +class GitRepo(pydantic.BaseModel): + '''A nested model used in SiteConfig to represent a Git repo.''' + url: str + branch: Optional[str] = None + remote: Optional[str] = 'origin' + +class SiteConfig(pydantic.BaseModel): + base_url: str + web_root: str + build_cache: str + title: str + description: Optional[str] = None + published: Optional[bool] = True + git_repo: Optional[GitRepo] = None + assets: Optional[list] = None + articles: Optional[list] = None + template_selections: Optional[dict] = {} + addons: Optional[list] = None + + +def get_var_names(var): + '''Gets the name(s) of an input variable. + From https://stackoverflow.com/questions/18425225/getting-the-name-of-a-variable-as-a-string.''' + callers_local_vars = inspect.currentframe().f_back.f_back.f_locals.items() + return [var_name for var_name, var_val in callers_local_vars if var_val is var] + + +def dotmap_access(d: DotMap, s: str): + '''Facilitates nested subscripting into a DotMap using a + string containing period-delimited keys. + + # Example + ```python + dd = DotMap({'a':{'b':{'c':42}}}) + dotmap_access(dd, 'dd.a.b.c') + >>> 42 + ``` + ''' + + keys = s.split('.') + + # If the string starts with the dotmap's name, ignore it. + if keys[0] in get_var_names(d): + keys.pop(0) + + # Iteratively subscript into the nested dotmap until it's done. + result = d + for k in keys: + result = result[k] + + return result diff --git a/jimsite/requirements.txt b/jimsite/requirements.txt new file mode 100644 index 0000000..10264cd --- /dev/null +++ b/jimsite/requirements.txt @@ -0,0 +1,5 @@ +ipykernel +markdown +pyyaml +rfeed +dotmap \ No newline at end of file diff --git a/jimsite/templating.py b/jimsite/templating.py new file mode 100644 index 0000000..edefc19 --- /dev/null +++ b/jimsite/templating.py @@ -0,0 +1,154 @@ +import os +import re +from dotmap import DotMap +from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access + + +def extract_placeholders(s: str) -> set: + '''Extracts placeholder variables in the format `{variable}` from + an unformatted template string.''' + + # Regex pattern to match placeholders with alphanumerics, dots, and underscores. + placeholder_pattern = r'\{([\w\.]+)\}' + + # Find all matches in the string. + matches = re.findall(placeholder_pattern, s) + + # Return the set of distinct placeholders. + return set(matches) + + +def find_cyclical_placeholders(s: str, _parents: tuple = None, _cycles: set = None, **kwargs) -> set[tuple]: + '''Recursively interpolates supplied kwargs into a template string to validate + that there are no cyclical dependencies that would cause infinite recursion. + + Returns a list of paths (expressed as tuples of nodes) of cyclical placeholders. + + # Example + + ```python + kwargs = {'a': '1', 'b': '2', 'c': '{d}+{e}', 'd': '3', 'e': '{c}'} + s = '{a} + {b} = {c}' + find_cyclical_placeholders(s, **kwargs) + + >>> {('c', 'e', 'c')} + ``` + ''' + + # Track the lineage of each placeholder so we can see if it is its own ancestor. + if _parents is None: + _parents = tuple() + + # Keep track of any cycles encountered. + if _cycles is None: + _cycles = set() + + # Extract the placeholders from the input. + placeholders = extract_placeholders(s) + + # Recursion will naturally end once there are no more nested placeholders. + for p in placeholders: + + # Any placeholder that has itself in its ancestry forms a cycle. + if p in _parents: + _cycles.add(_parents + (p,)) + + # For placeholders that are not their own ancestor, recursively + # interpolate the kwargs into the nested placeholders until we reach + # strings without placeholders. + else: + find_cyclical_placeholders( + ('{'+p+'}').format(**kwargs), + _parents = _parents+(p,), + _cycles = _cycles, + **kwargs + ) + + return _cycles + + +def format_html_template(template: str, **kwargs) -> str: + '''Interpolates variables specified as keyword arguments + into the given HTML template. + ''' + + # Load the template if a filepath is given. + template = filepath_or_string(template) + + # Ensure the template does not have cyclical placeholder references. + cycles = find_cyclical_placeholders(template, globalvars = GlobalVars(), **kwargs) + + if len(cycles) > 0: + raise ValueError('Template has cyclical dependencies: {cycles}') + + # Iteratively interpolate global variables and the kwargs into the template until + # there are no more placeholders. The loop is used to account for nested template references. + formatted_html = template + while len(extract_placeholders(formatted_html)) > 0: + formatted_html = formatted_html.format(globalvars = GlobalVars(), **kwargs) + + # Return the formatted HTML. + return formatted_html + + +def map_templates(dir: str, parent = '') -> DotMap: + '''Recursively maps the templates directory into a nested dict structure. + Leaves map the filestems of .html template files to their contents. + ''' + + output = {} + + # List the files and subdirectories at the top level. + for sub in os.listdir(os.path.join(parent,dir)): + + # Construct the full path to the file or subdir from the root of the tree. + full_path = os.path.join(parent,dir,sub) + + # Recursively map subdirectories. + if os.path.isdir(full_path): + output[sub] = map_templates(sub, parent = dir) + continue + + # Templates must be .html files. + filestem, ext = os.path.splitext(sub) + if ext != '.html': + continue + + # Load template file. + with open(full_path, 'r') as file: + html = file.read() + + output[filestem] = html + + return DotMap(output) + + +# TODO: Come up with a more consistent naming scheme for defaults. + +class TemplateSelections: + '''An interface for retrieving templates that are explicitly used in the + compiler, such as the article and tag components, as well as the default page. + + Relies on `SiteConfig.template_selection` for custom overrides, falling back on + `config[template_default_selections]`, and finally on defaults hard-coded into + the definition of this class. + ''' + def __init__(self, site: SiteConfig, templates: DotMap, config: dict = None): + self.site = site + self.templates = templates + self.defaults = dict( + tag = 'templates.components.blog_tag', + article = 'templates.components.blog_article', + page = 'templates.pages.default', + archive_li = 'templates.components.blog_archive_li', + archive_article = 'templates.components.blog_archive', + tag_selector = 'templates.components.blog_archive_tag_selector', + tag_selector_option = 'templates.components.blog_archive_tag_selector_option' + ) | (config or {}).get('template_default_selections', {}) + + def __getitem__(self, key: str) -> str: + + # Templates variable must be named `templates` for dotmap_access to work correctly. + templates = self.templates + return dotmap_access(templates, self.site.template_selections.get(key, self.defaults[key])) + diff --git a/pages/home.html b/pages/home.html deleted file mode 100644 index 8b05c10..0000000 --- a/pages/home.html +++ /dev/null @@ -1,8 +0,0 @@ -
-

Welcome!

-
-

Welcome to my little corner of the Internet! My name is Jim Shepich (@epicshepich). I like to introduce myself as a jack-of-all-trades with a Master's in data science. My main interests include STEM, martial arts (especially jūjutsu), sci-fi and fantasy, tabletop and video gaming, cartoons and comics, and DIY, but I also love to branch out and learn new things! My dream is to become "The Most Interesting Man in the World."

- -

This website started as a way for me to exercise and showcase my skills with vanilla HTML, JavaScript, CSS, and PHP. Now, it's just a matter of filling the site with content! My vision for this website is for it to serve as a gift to my future reincarnated self (if that's what happens) — a way to pass on the little tips and tricks, bits of wisdom, resources, and treasures I've accumulated over the course of my life (think new game plus). And if some other kindred spirits find something here that helps them on their journey, that's a bonus!

- -
diff --git a/scripts/character_sheet.js b/scripts/character_sheet.js deleted file mode 100644 index e0a8ada..0000000 --- a/scripts/character_sheet.js +++ /dev/null @@ -1,177 +0,0 @@ -if(document.getElementsByName("keywords")[0].content.indexOf("player")>-1){ - main(); -} - -function main(){ - var tables = document.getElementsByTagName("table"); - var abilities = {"-":0}; - var skills = {}; - var stats = {}; - var features = {}; - var masteries = []; - var EXP = {}; - - for(i=0;i tr -> th elements - for(k=0;k-1){ - output += 1; - } - } - return output; - } - - function align(cell,how="center"){ - cell.className = cell.className.replace("leftalign",""); - cell.className = cell.className.replace("rightalign",""); - cell.className = cell.className.replace("centeralign",""); - cell.className += " "+how+"align"; - - } - - for(i=0;i-1) { - stats["HD Improvement"] = parseInt(exp_value.substring(exp_value.indexOf("d")+1,exp_value.length)); - stats["HD Increase"] = parseInt(exp_value.substring(0,exp_value.indexOf("d"))); - exp_value = stats["HD Improvement"]+stats["HD Increase"]; - } else { - exp_value = 0; - exp.innerHTML = "0"; - } - - } - EXP[stat.innerHTML.trim()] = exp_value; - } - - for(i=0;i-1){ - exp = parseInt(exp_str.substring(0,exp_str.indexOf("/"))); - } else { - exp = parseInt(exp_str); - } - EXP[feature.innerHTML.trim()] = exp; - } - } - - var HP = 1 + Math.floor((skills["Persistence"]+skills["Toughness"])/2); - if(HP<1){ - HP = 1; - } - for(h=1;h<=EXP["HP"];h++){ - if(h%5==0 && abilities["CON"]>=1){ - HP += abilities["CON"]; - } else { - HP += 1; - } - } - stats["HP"].innerHTML = " "+HP+" "; - - if(!("HD Increase" in stats) && !("HD Improvement" in stats)){ - stats["HD Increase"] = 0; - stats["HD Improvement"] = 0; - - } - var hd = [Math.floor(stats["HD Increase"]/2)+1]; - hd.push(4+2*Math.floor(stats["HD Improvement"]/2)); - stats["Hit Dice"].innerHTML = hd[0]+"d"+hd[1]; - - - var AC = 8 + Math.ceil(skills["Toughness"]/3) + Math.ceil(skills["Evasion"]/3); - stats["AC"].innerHTML = " "+AC+" "; - - var exp_total = [0,parseInt(stats["EXP Total"].innerHTML.trim())]; - for(key in EXP){ - var temp_exp = parseInt(EXP[key]); - if(!isNaN(temp_exp)){ - exp_total[0] += temp_exp; - } - } - stats["EXP Total"].innerHTML = (exp_total[1]-exp_total[0])+"/"+exp_total[1]; - -} diff --git a/scripts/footer.php b/scripts/footer.php deleted file mode 100644 index 7df9b0e..0000000 --- a/scripts/footer.php +++ /dev/null @@ -1,11 +0,0 @@ -link . "'>"; - } - - echo "
Copyright © 2021-".date("Y")." Jim Shepich"; -?> diff --git a/scripts/json_scraper.js b/scripts/json_scraper.js deleted file mode 100644 index 8e3363c..0000000 --- a/scripts/json_scraper.js +++ /dev/null @@ -1,63 +0,0 @@ -function extract_data(){ - var data = document.getElementsByTagName("tr"); - var master = {}; - var header = null; - for(row of data){ - if(row.className == "row0"){ - continue; - //Skip table headers - } else if (row.className == "row1"){ - header = row.parentElement.parentElement.parentElement.parentElement.previousElementSibling.innerText; - //If we're at a new table, then we have to go up the DOM tree until we - //find that section's header. - master[header] = []; - //And create a new list for that header in the master object. - } - - var row_object = {}; - row_object["link"] = row.children[0].children[0].href; - //The link can be found in the anchor element in the first cell of the - //row. - var text = row.children[0].children[0].innerText; - //Get the text of the link. - row_object["song"] = text.substring(0,text.indexOf("(")-1); - //The name of the song is the start of the text until the space before - //the parenthesis. I've made sure to use hyphens in place of parentheses in - //song titles. - row_object["source"] = text.substring(text.indexOf("(")+1, text.indexOf(")")); - //The work that the song is from is indicated between the parentheses. - - row_object["vibes"] = row.children[1].innerText.split(", ") - //The vibes are in the second column, delimited by a comma-space. - - - master[header].push(row_object); - - } - - return master; -} - - - - -function download(filename, text) { - //Borrowed shamelessly from: - //https://ourcodeworld.com/articles/read/189/how-to-create-a-file-and-generate-a-download-with-javascript-in-the-browser-without-a-server - var element = document.createElement('a'); - element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); - element.setAttribute('download', filename); - - element.style.display = 'none'; - document.body.appendChild(element); - - element.click(); - - document.body.removeChild(element); -} - -var button = document.getElementById("download-JSON"); -button.addEventListener("click",function(){ - var json_data = JSON.stringify(extract_data(),null,2); - download("epics_bgm.json",json_data); -}); diff --git a/scripts/lists.js b/scripts/lists.js deleted file mode 100644 index 8ee9710..0000000 --- a/scripts/lists.js +++ /dev/null @@ -1,385 +0,0 @@ -//Variale `list` is imported from JSON with query_handler.php. -//var lists = JSON.parse(document.getElementById("lists-json").innerText); -var list_id = document.getElementById("query-list").innerText; -//Get list id from tag created in query-handler.php. -var selected_list = null; -if(lists.hasOwnProperty(list_id)){ - selected_list = lists[list_id]; -} else { - selected_list = lists["master"]; - //If the list id in the query is invalid, go back to the main list. -} - - - -function gen_list_html(list){ - //This function creates HTML from a list's JSON object by iterativel calling the gen_item_html function to convert individual items. This design paradigm facilitates the construction of nested lists or lists with sections. - var html = ""; - if(!list.hasOwnProperty("type")){ - list.type = "default"; - } - - if(!list.hasOwnProperty("subtype")){ - list.subtype = "default"; - } - - var list_type = list.type; - - if(list.hasOwnProperty("sections")){ - list_type = "sectioned"; - //Lists that have separate sections require special handling involving recursion. - } - - switch(list_type){ - case "master": - for(id in lists){ - html += gen_item_html(id,type="list-id"); - } - html = `
    ${html}
`; - break; - - case "quotes": - html += "
"; - for(quote of list.list){ - html += gen_item_html(quote,type="quote"); - } - break; - - case "key-value": - for(pair of list.list){ - html += gen_item_html(pair,type="kv-pair"); - } - html = (list.hasOwnProperty("ordered") && list.ordered) ? `
    ${html}
` : `
    ${html}
`; - //Create an ordered list if list has the property "ordered". - break; - - - case "gallery": - document.getElementById("lists").style.textAlign="center"; - //Center the rows of exhibits on the page. - if(!list.hasOwnProperty("subtype")){ - list.subtype="default"; - } - for(exhibit of list.list){ - html += gen_item_html(exhibit,type="exhibit",subtype=list.subtype); - } - break; - - case "external": - //This branch will only run if someone manually enters the list name into the query; clicking an external-type list from the Master List will just open the link in a new tab. - setTimeout(function(){location.assign(list["link"])}, 600); - //If the list specified in the query has the external property, then redirect to the external link. I use setTimeout in order to allow the rest of the code to run. - list.title = `Redirecting to List of ${list.title}...`; - list.list = []; - //It takes a second to redirect, so put some filler on the page while the reader waits. - break; - - case "sectioned": - if(!list.hasOwnProperty("section-level")||list["section-level"]==null){ - list["section-level"] = 1; - //Section-level defines the heading level of the section. Top level is 1. - } - if(!list.hasOwnProperty("dropdown")||list.dropdown==null){ - list.dropdown = true; - //By default, sections ARE in details/summary tags. - } - if(!list.hasOwnProperty("dropdown-open")||list["dropdown-open"]==null){ - list["dropdown-open"] = false; - //By default, sections are collapsed. - } - for(section of list.sections){ - var section_html = ""; - var level = list["section-level"] + 1; - section["section-level"] = level; - //Nested secions are of lower levels. - if(!section.hasOwnProperty("type")||section.type==undefined){ - section.type = list.type; - //This branch transfers the list type down from higher levels. By default, the bottom-level lists will inherit the type of the top-level object unless otherwise specified. - } - if(!section.hasOwnProperty("subtype")||section.subtype==undefined){ - section.subtype = list.subtype; - //Sections should also inherit subtypes. - } - if(!section.hasOwnProperty("dropdown")||section.dropdown==undefined){ - section.dropdown = list.dropdown; - //Inherit dropdown-ness unless otherwise specified. - } - if(!section.hasOwnProperty("dropdown-open")||section["dropdown-open"]==undefined){ - section["dropdown-open"] = list["dropdown-open"]; - //Inherit whether the dropdown should be collapsed or open. - } - var description = (section.hasOwnProperty("description")) ? `

${section.description}

` : ""; - //Sections can have their own descriptions. - - var title = `${section.title}`; - //Wrap the section title in a header tag. - - if(section.hasOwnProperty("dropdown") && section.dropdown){ - //If the section is marked with the "dropdown" attribute, then nest the section's data in a details/summary tag. - var open = (section["dropdown-open"]) ? "open":""; - section_html = `
${title}${description}${gen_list_html(section)}
`; - } else { - section_html = `${title}${description}${gen_list_html(section)}`; - } - //Sectioned - html += `
${section_html}
`; - //Assigning ids allows for the possibility of fragment linking. - } - break; - - default: - for(item of list.list){ - html += gen_item_html(item); - } - - html = (list.hasOwnProperty("ordered") && list.ordered) ? `
    ${html}
` : `
    ${html}
`; - //Create an ordered list if list has the property "ordered". - } - - return html; - -} - -function gen_item_html(item,type="default",subtype=null){ - var item_html = ""; - switch(type){ - case "list-id": - if(lists[item].hasOwnProperty("hidden") && lists[item].hidden){ - return ""; - //Lists marked with the "hidden" attribute are in-development and should not be displayed on the Master List. - } - if(!lists[item].hasOwnProperty("title")){ - lists[item].title = item; - //If a title is not set for the list, then just use its id. - } - var tooltip = ""; - if(lists[item].hasOwnProperty("description")){ - tooltip=lists[item]["description"]; - //When hovering over a list in the directory, display its description as a tooltip. - } - var link = `href='index.php?page=lists&list=${item}' target='_self'`; - //By default, lists have internal links that change the query value to that list's title. Internal links should open in the same tab. - if(lists[id].hasOwnProperty("type") && lists[item].type=="external"){ - link = `href='${lists[item]["link"]}' target='_blank'`; - //For external lists, use their link instead of an internal link. External links should open in a new tab. - } - item_html = `
  • ${lists[item].title}
  • `; - //The Master List contains a link to every other list in the JSON file. - break; - - case "quote": - //Format quotes like they are formatted on Goodreads. - if(!item.hasOwnProperty("quote")){ - //If the quote is blank, then things are going to get real weird. Most likely, this will be caused by a typo, so this branch is a safeguard against any resulting errors. - return ""; - } - item_html = "

    "; - if(item.hasOwnProperty("title")){ - item_html += `${item.title}
    `; - //Add a title if the quote has one (e.g. "The Litany Against Fear"). - } - item_html += `“${item.quote}”
    `; - //Add the text of the quote. - if(item.hasOwnProperty("card") && !item.hasOwnProperty("quotee")){ - item.quotee=""; - //If a flavor text doesn't have a quotee, don't write "Unknown". - } - - item_html += ` — ${item.hasOwnProperty("quotee") ? item.quotee : "Unknown"}`; - //Add the quotee's name, or "Unknown" if one is not specified. - if(item.hasOwnProperty("source")){ - if(item.hasOwnProperty("quotee") && item.quotee!=""){ - item_html += ", "; - //Unless there is no quotee, separate the quotee from the source with ", " - } - item_html += item.source; - //Add the source if the quote has one. - } - - if(item.hasOwnProperty("card")){ - if(item.quotee!=""||item.hasOwnProperty("source")){ - item_html += `, `; - //Separate quotee or source from card title with ", " - } - item_html += `${item.card} (Magic: the Gathering)`; - } - item_html += "


    "; - //Delimit the quotes with a horizontal rule. - break; - - case "kv-pair": - if(!(item.hasOwnProperty("k")&&item.hasOwnProperty("v"))||Object.keys(item).length==1){ - var key = Object.keys(item)[0]; - item["k"] = key; - item["v"] = item[key]; - //If the item is an object containing a single key-value pair, then use that pair as the k and v for displaying. - } - if(item.hasOwnProperty("link")){ - item_html = `${item["k"]}`; - } else { - item_html = `${item["k"]}`; - } - item_html += ` — ${item["v"]}`; - - if(item.hasOwnProperty("list") && Array.isArray(item.list)){ - //Sublist time, baby! - var temp = { - "title":item_html, - "list":item.list, - "ordered": (item.hasOwnProperty("ordered")) ? item.ordered:undefined, - "dropdown": (item.hasOwnProperty("dropdown")) ? item.dropdown:undefined, - "dropdown-open": (item.hasOwnProperty("dropdown-open")) ? item["dropdown-open"]:undefined - }; - //If a key-value pair has a list key with an array attribute, then reformat it as a sublist whose title is the kv pair and whose list is the array. - item_html = gen_item_html(temp,type="sublist") - } else { - item_html = `
  • ${item_html}
  • `; - } - break; - - - case "exhibit": - var tooltip, alt, text, img_src, image; - tooltip = text = img_src = image = ""; - classes = {"div":[],"img":[],"a":[]} - switch(subtype){ - case "album": - tooltip = `${item.title} (${item.year}) - ${item.artist}`; - alt = tooltip; - img_src = item.cover; - text = `${item.title}
    ${item.artist}
    (${item.year})`; - break; - case "movie": - tooltip = `${item.title} (${item.year})`; - alt = tooltip; - img_src = item.poster; - text = `${item.title}
    (${item.year})`; - break; - case "mtg-card": - var gatherer_link = "https://gatherer.wizards.com/pages/card/Details.aspx?multiverseid="; - var gatherer_image = "https://gatherer.wizards.com/Handlers/Image.ashx?multiverseid="; - - if(item.hasOwnProperty("multiverseid")&&!item.hasOwnProperty("link")){ - if(Array.isArray(item.multiverseid)){ - //If the multiverseid is an array, treat it as a transform card and link to the Gatherer page for the front half. - item.link = gatherer_link+item.multiverseid[0]; - } else { - item.link = gatherer_link+item.multiverseid; - } - //If there's no alternate link specified, then use the multiverseid to generate the Gatherer link. - } - if(item.hasOwnProperty("multiverseid")&&!item.hasOwnProperty("image")){ - if(Array.isArray(item.multiverseid)){ - img_src = []; - for(id of item.multiverseid){ - img_src.push(gatherer_image+id+"&type=card") - } - //If there are multiple (two) multiverseids, make a link src for each of them for convenience. - } else { - img_src = gatherer_image+item.multiverseid+"&type=card"; - } - //If there's no alternate link specified, then use the multiverseid to generate the Gatherer link for the image. - } else { - if(Array.isArray(item.image)){ - img_src=[]; - for(src of item.image){ - img_src.push(src); - } - //If multiple (two) image sources are specified, create an array of both. - } else { - img_src = item.image; - } - } - if(item.hasOwnProperty("name")){ - alt = tooltip = item.name; - } - - if(Array.isArray(img_src)){ - image = `${tooltip}${tooltip}` - //Two srcs are to be treated as those of a transform card, which switches on hover. - classes.div.push("card-transform"); - } - - - break; - default: - } - - if(image==""||!image){ - image = image = `${tooltip}`; - //If the image variable is not already defined, then generate it. - } - //Gallery items must have an image. - if(item.hasOwnProperty("link")){ - image = `${image}`; - //If there's a link associated with the exhibit, put it on the image. - } - item_html = (text=="") ? image : `${image}
    ${text}`; - //If there's no text, then there's no need for a line break. - item_html = `` - - break; - - case "sublist": - //Sublists can be any type (most likely default or key-value), so the best way to deal with them is recursion. - if(item.hasOwnProperty("dropdown") && item.dropdown){ - var open = (item.hasOwnProperty("dropdown-open")&&item["dropdown-open"])?"open":""; - item_html = `
  • ${item.title}${gen_list_html(item)}
  • `; - } else { - item_html = `
  • ${item.title}${gen_list_html(item)}
  • `; - } - - - break; - - default: - if(["string","number"].includes(typeof item)){ - item_html = `
  • ${item}
  • `; - //If the element is a simple string or number, then we don't need to do any special formatting. - } else if (typeof item === "object"){ - var keys = Object.keys(item); - if(keys.length == 1){ - if (Array.isArray(item[keys[0]])){ - //An item that is a dictionary only containing a list is probably a sublist. Format it as such, and pass it back through this switch statement. - var temp = { - "title":keys[0], - "type":"default", - "list":item[keys[0]] - }; - item_html = gen_item_html(temp,"sublist"); - } else { - item_html = gen_item_html(item,"kv-pair"); - //A item that is dictionary with one key, whose value is not a list, is to be treated as a term-explanation type key-value pair. - } - } else { - item_html = gen_item_html(item,"sublist"); - //At this point, if there is no other specification, an item that's an object is probably a sublist. - } - } - } - return item_html; -} - -function str2html(md){ - //This function replaces escapes commands and Markdown with their HTML equivalents. - html = md.replaceAll("\n","
    "); - return html; -} - - -document.getElementById("list-title").innerHTML = selected_list.title; -if(selected_list.hasOwnProperty("description")){ - document.getElementById("list-description").innerHTML = selected_list.description; -} else { - document.getElementById("list-description").style.display = "none"; - //If the list has no description, hide the element to remove the awkward space from the padding. -} -//Generate the article header from the list's title and description. - -document.getElementById("list-container").innerHTML += str2html(gen_list_html(selected_list)); -//Call the gen_list_html function to convert the list from a JSON object to sophisticated HTML. - -if(list_id != "master"){ - document.getElementById("lists").innerHTML += "

    Return to Master List ↩

    "; - //Add a return link to the bottom of the article. -} diff --git a/scripts/nav.php b/scripts/nav.php deleted file mode 100644 index 65f4608..0000000 --- a/scripts/nav.php +++ /dev/null @@ -1,44 +0,0 @@ -index <=> $b->index; - //For some reason, PHP8 requires the spaceship operator (<=>) - //instead of a greater than symbol. -} - -function gen_nav_element($page){ - $iframe = false; - //By default, echo the contents of a file instead of embedding it in an iframe. - if(isset($page->file)){ - $href = "index.php?page=".$page->query_value; - $target = "_self"; - //If the page is associated with a file, then point the navibar href to the file's query value. - - $iframe = (isset($page->iframe)) ? true : false; - //If the page has the iframe attribute, then keep track of that. - - } elseif(isset($page->link)) { - $href = $page->link; - $target = "_blank"; - //If instead the page is associated with an external link, then point the navibar href there and make it open in a new window. - } - echo ""; -} - -$page_array = get_object_vars($pages); -//Convert $pages from object to an associative array of [key=>val, key=>val] -//where the keys are the names of the page objects from the JSON file and the -//vals are the page objects themselves. -usort($page_array,"page_comparator"); -//Sort the pages in order of increasing "index" value. - -foreach($page_array as $key=>$p){ - if($p->index>-1){ - gen_nav_element($p); - //Pages with indices less than 0 are hidden from the navibar. - } -} - -?> diff --git a/scripts/query_handler.php b/scripts/query_handler.php deleted file mode 100644 index 4114cca..0000000 --- a/scripts/query_handler.php +++ /dev/null @@ -1,23 +0,0 @@ -$query_page)) ? $pages->$query_page : $pages->{404}; - //If the query value ($query_page) is not in the dictionary ($pages), then load the 404 page instead. - echo "".$query_page.""; - //Store the page id in a var tag in case we need to access it with JavaScript. - - $list = (isset($_REQUEST['list']) && !empty($_REQUEST['list'])) ? $_GET['list'] : "master"; - //Get the value of "list" from the query key; default is "master" (the master list). - echo "".$list.""; - //Store the list id in a var tag so that we can easily figure out what list to generate with list.js. - - if($query_page=="lists"){ - $lists_json = json_decode(file_get_contents(__DIR__ ."/../data/lists.json")); - echo ""; - //Copy the data from lists.json and paste it into a script tag, saving it as a variable called `list` to be accessed by later JavaScript code (i.e. lists.js). - //I would have stored it in a var tag, but there was a weird bug with some of the tags leaking out and making my title underlined, and this seemed to avoid that (and it's probably more efficient too). - } -?> diff --git a/scripts/resize.js b/scripts/resize.js deleted file mode 100644 index 1711628..0000000 --- a/scripts/resize.js +++ /dev/null @@ -1 +0,0 @@ -//$("#main-header").css({"font-size":$("#main-header").height()+"px"}) diff --git a/scripts/vendor/jquery-3.6.0.min.js b/scripts/vendor/jquery-3.6.0.min.js deleted file mode 100644 index c4c6022..0000000 --- a/scripts/vendor/jquery-3.6.0.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/assets/img/rss.svg b/site/assets/img/rss.svg new file mode 100644 index 0000000..98dc7b2 --- /dev/null +++ b/site/assets/img/rss.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/site/assets/js/blog_archive_query.js b/site/assets/js/blog_archive_query.js new file mode 100644 index 0000000..4fddc32 --- /dev/null +++ b/site/assets/js/blog_archive_query.js @@ -0,0 +1,11 @@ +function selectTagByUrlQuery(){ + query = new URLSearchParams(window.location.search); + tag = query.get('tag'); + + if(tag){ + document.querySelector(`#tag-selector-${tag}`).checked = true; + } +} + +document.addEventListener('load', selectTagByUrlQuery); +selectTagByUrlQuery(); \ No newline at end of file diff --git a/pages/404.html b/site/pages/404.html similarity index 100% rename from pages/404.html rename to site/pages/404.html diff --git a/pages/about.html b/site/pages/about.html similarity index 100% rename from pages/about.html rename to site/pages/about.html diff --git a/pages/don-info.html b/site/pages/don-info.html similarity index 100% rename from pages/don-info.html rename to site/pages/don-info.html diff --git a/site/pages/index.md b/site/pages/index.md new file mode 100644 index 0000000..588b968 --- /dev/null +++ b/site/pages/index.md @@ -0,0 +1,10 @@ +--- +title: Welcome +date: 2026-01-30 +published: true +tags: [] +--- + +Welcome to my little corner of the Internet! My name is Jim Shepich (@epicshepich). I like to introduce myself as a jack-of-all-trades with a Master's in data science. My main interests include STEM, martial arts (especially jūjutsu), sci-fi and fantasy, tabletop and video gaming, cartoons and comics, and DIY, but I also love to branch out and learn new things! My dream is to become "The Most Interesting Man in the World." + +This website started as a way for me to exercise and showcase my skills with vanilla HTML, JavaScript, CSS, and PHP. Now, it's just a matter of filling the site with content! My vision for this website is for it to serve as a gift to my future reincarnated self (if that's what happens) — a way to pass on the little tips and tricks, bits of wisdom, resources, and treasures I've accumulated over the course of my life (think _new game plus_). And if some other kindred spirits find something here that helps them on their journey, that's a bonus! diff --git a/pages/lists.html b/site/pages/lists.html similarity index 100% rename from pages/lists.html rename to site/pages/lists.html diff --git a/site/templates/components/blog_archive.html b/site/templates/components/blog_archive.html new file mode 100644 index 0000000..2315645 --- /dev/null +++ b/site/templates/components/blog_archive.html @@ -0,0 +1,12 @@ +
    +

    {site.title} Archive

    +
    +

    Tag Inventory

    + {templates.components.blog_archive_tag_selector} +
    +

    Post History

    + {content} +


    +

    Last Updated: {globalvars.today}{templates.components.rss_icon}

    +
    + \ No newline at end of file diff --git a/site/templates/components/blog_archive_li.html b/site/templates/components/blog_archive_li.html new file mode 100644 index 0000000..6944216 --- /dev/null +++ b/site/templates/components/blog_archive_li.html @@ -0,0 +1 @@ +
  • {article.metadata.date} - {article.metadata.title} {blog_tags}
  • diff --git a/site/templates/components/blog_archive_tag_selector.html b/site/templates/components/blog_archive_tag_selector.html new file mode 100644 index 0000000..09f89a7 --- /dev/null +++ b/site/templates/components/blog_archive_tag_selector.html @@ -0,0 +1,4 @@ +
    + {tag_selector_options} +
    +{tag_selector_css_rules} \ No newline at end of file diff --git a/site/templates/components/blog_archive_tag_selector_option.html b/site/templates/components/blog_archive_tag_selector_option.html new file mode 100644 index 0000000..6f8cf07 --- /dev/null +++ b/site/templates/components/blog_archive_tag_selector_option.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/site/templates/components/blog_article.html b/site/templates/components/blog_article.html new file mode 100644 index 0000000..e209b46 --- /dev/null +++ b/site/templates/components/blog_article.html @@ -0,0 +1,11 @@ +
    +

    {article.metadata.title}

    +
    By
    +
    First published: +
    Last modified: +

    + {article.content} +


    +

    Tags: {blog_tags}{templates.components.rss_icon}

    +
    \ No newline at end of file diff --git a/site/templates/components/blog_tag.html b/site/templates/components/blog_tag.html new file mode 100644 index 0000000..764e4c8 --- /dev/null +++ b/site/templates/components/blog_tag.html @@ -0,0 +1 @@ +{tag_name} \ No newline at end of file diff --git a/site/templates/components/blog_tag_reference.html b/site/templates/components/blog_tag_reference.html new file mode 100644 index 0000000..c850452 --- /dev/null +++ b/site/templates/components/blog_tag_reference.html @@ -0,0 +1,11 @@ +
    +

    {site.title} Tag Reference

    +
    By
    +
    First published: +
    Last modified: +

    + {article.content} +


    +

    {blog_tags}

    +
    \ No newline at end of file diff --git a/site/templates/components/rss_icon.html b/site/templates/components/rss_icon.html new file mode 100644 index 0000000..9368675 --- /dev/null +++ b/site/templates/components/rss_icon.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/templates/components/simple_article.html b/site/templates/components/simple_article.html new file mode 100644 index 0000000..82fa3fc --- /dev/null +++ b/site/templates/components/simple_article.html @@ -0,0 +1,5 @@ +
    +

    {article.metadata.title}

    +
    + {article.content} +
    \ No newline at end of file diff --git a/site/templates/pages/default.html b/site/templates/pages/default.html new file mode 100644 index 0000000..608c0ce --- /dev/null +++ b/site/templates/pages/default.html @@ -0,0 +1,18 @@ + + + + + + + {templates.partials.default_css} + {templates.partials.header} + {templates.partials.nav} + {site.title} + + + +
    + {content} +
    + {templates.partials.footer} + \ No newline at end of file diff --git a/site/templates/partials/default_css.html b/site/templates/partials/default_css.html new file mode 100644 index 0000000..0bc0f4c --- /dev/null +++ b/site/templates/partials/default_css.html @@ -0,0 +1,4 @@ + + + + diff --git a/site/templates/partials/footer.html b/site/templates/partials/footer.html new file mode 100644 index 0000000..dd714d4 --- /dev/null +++ b/site/templates/partials/footer.html @@ -0,0 +1,3 @@ +
    +
    Copyright © 2021-{globalvars.today.year} Jim Shepich +
    \ No newline at end of file diff --git a/site/templates/partials/header.html b/site/templates/partials/header.html new file mode 100644 index 0000000..45c5c32 --- /dev/null +++ b/site/templates/partials/header.html @@ -0,0 +1,3 @@ +
    + Jimlab +
    \ No newline at end of file diff --git a/site/templates/partials/nav.html b/site/templates/partials/nav.html new file mode 100644 index 0000000..ed5c746 --- /dev/null +++ b/site/templates/partials/nav.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/testbench.ipynb b/testbench.ipynb new file mode 100644 index 0000000..72d680d --- /dev/null +++ b/testbench.ipynb @@ -0,0 +1,222 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "207d2510", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "import shutil\n", + "import markdown\n", + "import yaml\n", + "import subprocess\n", + "import rfeed\n", + "import pydantic\n", + "import glob\n", + "from dotmap import DotMap\n", + "from typing import Optional, Union, Literal, BinaryIO, Any\n", + "\n", + "\n", + "\n", + "from datetime import datetime\n", + "from jimsite import *" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68b107f1", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8f435a12", + "metadata": {}, + "outputs": [], + "source": [ + "with open('config.yaml', 'r') as config_file:\n", + " config = yaml.safe_load(config_file.read())\n", + " \n", + "templates = map_templates(config['templates_folder'])\n", + "\n", + "sites = {k:SiteConfig(**v) for k,v in config['sites'].items()}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e32458c7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Dogma Jimfiniumhttp://localhost:8000/dogma-jimfinium/rssDogma Jimfiniumen-USThu, 29 Jan 2026 16:29:57 GMTrfeed v1.1.1https://github.com/svpino/rfeed/blob/master/README.mdSuperlockhttp://localhost:8000/dogma-jimfinium/superlockJim Shepich IIIWed, 26 Nov 2025 00:00:00 GMTsuperlockSustainable Livinghttp://localhost:8000/dogma-jimfinium/sustainable-livingJim Shepich IIIThu, 20 Nov 2025 00:00:00 GMTsustainable-livingStocking Uphttp://localhost:8000/dogma-jimfinium/stocking-upJim Shepich IIIWed, 19 Nov 2025 00:00:00 GMTstocking-upSet Up the Toyshttp://localhost:8000/dogma-jimfinium/set-up-the-toysJim Shepich IIIWed, 14 Jan 2026 00:00:00 GMTset-up-the-toysDo What You Lovehttp://localhost:8000/dogma-jimfinium/do-what-you-loveJim Shepich IIITue, 10 Jun 2025 00:00:00 GMTdo-what-you-loveSelf-Care is not Selfishhttp://localhost:8000/dogma-jimfinium/self-care-is-not-selfishJim Shepich IIISun, 18 May 2025 00:00:00 GMTself-care-is-not-selfishBlowoutshttp://localhost:8000/dogma-jimfinium/blowoutsJim Shepich IIIWed, 26 Nov 2025 00:00:00 GMTblowoutsVitamins & Supplementshttp://localhost:8000/dogma-jimfinium/vitaminsJim Shepich IIISun, 18 May 2025 00:00:00 GMTvitaminsGear for New Parentshttp://localhost:8000/dogma-jimfinium/gear-for-new-parentsJim Shepich IIIFri, 12 Jul 2024 00:00:00 GMTgear-for-new-parents\n" + ] + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70408b85", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7de0d84d", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "index = {}\n", + "\n", + "for article in glob.glob('build/dogma-jimfinium/*.md'):\n", + " metadata, content = load_markdown(article)\n", + "\n", + " if metadata is None:\n", + " print(article)\n", + "\n", + " # Skip unpublished articles.\n", + " if not metadata.published:\n", + " continue\n", + "\n", + " article_filestem = os.path.splitext(os.path.basename(article))[0]\n", + "\n", + " # Add the article to the index.\n", + " index[article_filestem] = (metadata, content)\n", + "\n", + " # Interpolate the article contents into the webpage template.\n", + " article_html = format_html_template(\n", + " 'templates/components/blog_article.html',\n", + " content = content,\n", + " blog_tags = ' '.join(format_article_tags(metadata.tags)),\n", + " metadata = metadata\n", + " )\n", + " html = format_html_template('templates/pages/default.html', content = article_html, **PARTIALS)\n", + " \n", + " # Write the HTML file to /dist/dogma-jimfinium.\n", + " with open(f'dist/dogma-jimfinium/{article_filestem}.html', 'w') as f:\n", + " f.write(html)\n", + "\n", + "\n", + "index_html = build_blog_archive(index, **PARTIALS)\n", + "# Write the HTML file to /dist/dogma-jimfinium.\n", + "with open(f'dist/dogma-jimfinium/index.html', 'w') as f:\n", + " f.write(index_html)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3171afd", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a28b95a6", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'build_site' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mbuild_site\u001b[49m(sites[\u001b[33m'\u001b[39m\u001b[33mresume\u001b[39m\u001b[33m'\u001b[39m])\n", + "\u001b[31mNameError\u001b[39m: name 'build_site' is not defined" + ] + } + ], + "source": [ + "build_site(sites['resume'])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "60378277", + "metadata": {}, + "outputs": [], + "source": [ + "import inspect" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd7fb9f3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DotMap(a=DotMap(b=DotMap(c=42)))\n", + "a\n", + "DotMap(b=DotMap(c=42))\n", + "b\n", + "DotMap(c=42)\n", + "c\n" + ] + }, + { + "data": { + "text/plain": [ + "42" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcd2faf5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}