Compare commits

...
Sign in to create a new pull request.

76 commits

Author SHA1 Message Date
dfd8e299cd feat: small tweaks
All checks were successful
/ phplint (push) Successful in 7s
2024-01-22 16:48:52 +01:00
a65dd96f8f feat: rework post normalization systems 2024-01-22 16:34:49 +01:00
12d3838eae feat: add post search function 2024-01-22 16:32:38 +01:00
add698651e feat: add gallery page 2024-01-22 16:31:16 +01:00
9354f9f556 feat: add highlighting Markdown parser <3 2024-01-22 16:22:28 +01:00
8e69a9139f feat: stylistic tweaks for mobile compatibility 2024-01-22 16:15:01 +01:00
614bf9d3b6 perf: add twig caching 2024-01-11 16:55:19 +01:00
7f2c7edf76 feat: add age-gating check 2024-01-11 16:54:37 +01:00
2ea575f229 feat: rework metadata loading to use site-specific config file 2024-01-11 16:53:37 +01:00
c202b778e0 feat: add new file metadata handling (.ext.md instead of .mddesc) 2024-01-11 14:37:06 +01:00
d39595d577 feat: make the website load for different subdomains 2024-01-11 10:37:19 +01:00
f00e97d294 fix: fix problem with URL parsing 2024-01-09 22:30:03 +01:00
f4824f8ca8 fix: 🩹 fix up wrong ordering
All checks were successful
/ phplint (push) Successful in 6s
2024-01-09 00:12:54 +01:00
5d1d4cdc32 refactor: move as many OG tags to root page as possible 2024-01-09 00:12:31 +01:00
454161e98a refactor: ♻️ break out page type handling code and opengraph tag generation 2024-01-09 00:11:52 +01:00
1b8bff401b feat: rewrite banner javascript system to accept custom banner data from PHP template 2024-01-08 16:42:29 +01:00
30019b4c47 refactor: remove long-unused "user-content" dir 2024-01-08 16:40:37 +01:00
d584a2c373 misc: prepare test entires for new banner system 2024-01-08 16:40:04 +01:00
d44b854673 refactor: remove static banner images in prep for new banner javascript 2024-01-08 16:39:29 +01:00
83b849fda8 fix: small naming fix to make writing directory README data easier 2024-01-04 16:07:29 +01:00
98e062611e feat: add dedicated class to hide contents on small-width screens 2024-01-04 16:06:52 +01:00
91224e634a feat: add OpenGraph tags 2024-01-04 16:06:06 +01:00
62089b9ee2 feat: add FontAwesome symbols for directories 2024-01-04 16:05:16 +01:00
87a14e0624 feat: missing tweaks for feed writing 2024-01-04 16:03:21 +01:00
596cc0e1a3 feat: add RSS and Atom feeds with caching and nice links 2023-12-25 20:13:00 +01:00
c6297fd81b feat: add robots.txt, google may monch 2023-12-23 10:52:47 +01:00
b089bfc551 feat: add wrapper function to log page access and exit 2023-12-23 10:52:29 +01:00
70a2c45509 fix: fix graphical desing of error page to remove double-nested article 2023-12-23 10:51:42 +01:00
e777ea5675 fix: small fix to post data normalization handling missing posts better 2023-12-23 10:51:07 +01:00
2add216157 feat: add line-format output for access counters 2023-12-23 10:50:33 +01:00
a4fe4c4489 feat: transition to time-block based access count storage, including referrer storage 2023-12-23 10:49:54 +01:00
35ff4951ad feat: add small article footer to show created/edited times and view count 2023-12-20 19:20:24 +01:00
edeecb8cf9 feat: re-add post access counter for get_post function 2023-12-20 19:19:35 +01:00
226d19e62c fix: 🩹 fix error on empty argtypes list (no bindings needed) 2023-12-20 18:55:12 +01:00
6cb60a6652 feat: add settings construct cache
This will avoid the awkward recursive query for
each post by fetching cached post settings if available, and
caching them nicely.
2023-12-20 18:54:31 +01:00
94b65aec8c feat: add post access usage metrics 2023-12-20 18:52:28 +01:00
eb87a78625 feat: use a proper path sanitization function for permitted paths 2023-12-20 18:50:07 +01:00
b552562f31 feat: add implicit README settings to directory settings copy 2023-12-19 10:18:34 +01:00
d2dc57a36a test: add test entires using the new settings 2023-12-19 10:14:31 +01:00
40a270059f feat: pull hierarchical settings dict from post metadata 2023-12-19 10:14:14 +01:00
2ee294b012 fix: 🐛 fix post metadata fill-out failing on non-found posts 2023-12-18 16:02:31 +01:00
37357da4b0 refactor: ♻️ move error page into proper post_type dir 2023-12-18 16:01:29 +01:00
b8ae2c5617 fix: properly use HTTP_ACCEPT to detect image rather than SEC_FETCH_DEST 2023-12-18 16:01:06 +01:00
f2f8a235c8 feat: properly utilise 404 page again 2023-12-18 16:00:28 +01:00
d47c4e2072 ci: 💚 remove Markdownlint, we'll write it our style :>
All checks were successful
/ phplint (push) Successful in 4s
2023-12-14 11:51:15 +01:00
c3fa88f3fd ci: try and fix markdownlint
All checks were successful
/ phplint (push) Successful in 8s
2023-12-14 11:50:06 +01:00
2f5d59dd32 fix: 🐛 fix wrong usave of copy (should have been move_uploaded_file)
Some checks failed
/ phplint (push) Failing after 3s
2023-12-14 11:48:03 +01:00
a82290cf73 feat: add api lock key 2023-12-14 11:47:33 +01:00
bc286413ad test: add some test entry content for good measure 2023-12-14 11:46:17 +01:00
ed80a21495 feat: add simple uploading bash script <3 2023-12-14 11:45:55 +01:00
881ea81488 test: add a test image markdown descriptor 2023-12-13 10:27:59 +01:00
653b91cb1e feat: clean up image template to fit other post layouts, add caption 2023-12-13 10:23:21 +01:00
41c7e0cdc3 feat: use directory's own content field instead of README lookup 2023-12-13 10:22:49 +01:00
38a5056ff0 refactor: ♻️ refactor out post-type specific code 2023-12-13 10:19:00 +01:00
9e855fba36 feat: allow for SQL server port specification 2023-12-12 22:39:06 +01:00
0d9bedcaca feat: 🎨 adjust CSS style to give more room to the post on small screens 2023-12-12 22:36:33 +01:00
16fb45aaf4 fix: fix access setting, adding .htaccess 2023-12-11 23:32:13 +01:00
4baa737d95 feat: add option to use secrets JSON file instead 2023-12-11 23:30:39 +01:00
f02e6b3168 feat: move SQL connection parameters into Env. vars 2023-12-11 23:04:49 +01:00
a7a7c738ec refactor: ♻️ clean up unused Twig templates 2023-12-11 22:52:30 +01:00
d54d3077cb feat: use new adapter upload function 2023-12-11 16:22:42 +01:00
b6b8c587a1 feat: add backend for uploading files 2023-12-11 16:20:27 +01:00
de2dea4b18 feat: add "raw" dir to directory, make it indexable 2023-12-11 16:18:26 +01:00
53fc1fac5a test: add test entries to upload 2023-12-11 16:16:31 +01:00
906014cf05 test: add lorem ipsum filler 2023-12-11 16:15:31 +01:00
81056ea42e feat: add writable directory for server content 2023-12-11 16:14:55 +01:00
8b01664b71 chore: add test markdown files for the uploading
Some checks reported warnings
/ phplint (push) Has been cancelled
2023-10-25 09:12:20 +02:00
8b9be26987 feat: add upload functionality 2023-10-25 09:12:03 +02:00
f6b8611d90 feat: work on testing out MySQLi adpater class
Some checks reported warnings
/ phplint (push) Has been cancelled
2023-10-23 22:38:36 +02:00
8f9d94faab feat: add first draft of MySQL adapter class 2023-10-23 22:38:14 +02:00
1146e69f84 deps: add Frontmatter parser lib 2023-10-23 22:37:54 +02:00
fcc42bde20 feat: added first draft of Database based website rendering
Some checks failed
/ phplint (push) Failing after 3s
2023-10-22 12:59:20 +02:00
5bcba0b689 refactor: move all HTTP-Website code into www subdirectory
Some checks failed
/ phplint (push) Failing after 4s
2023-10-19 21:54:37 +02:00
502ddd1ad9 ci: moved lint file name 2023-10-19 21:53:41 +02:00
57d6d82be9 ci: add first draft of MySQL data schema 2023-10-19 21:53:14 +02:00
1862ccbbb6 feat: [first attempt at using MySQLi in the docker dev env] 2023-10-16 16:15:24 +02:00
70 changed files with 3229 additions and 558 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
/vendor/ /vendor/
sftp.json

View file

@ -1,6 +0,0 @@
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_URI} !^/?static/.*
RewriteRule (.*) router.php

View file

@ -1,7 +0,0 @@
{
"require": {
"twig/twig": "^3.0",
"twig/markdown-extra": "^3.6",
"league/commonmark": "^2.4"
}
}

View file

@ -1,7 +1,7 @@
FROM composer FROM composer
WORKDIR /app WORKDIR /app
COPY composer.* . COPY www/composer.* .
RUN composer install RUN composer install
FROM php:apache FROM php:apache
@ -12,6 +12,9 @@ RUN chmod -R a+r ./vendor
RUN a2enmod rewrite RUN a2enmod rewrite
RUN a2enmod headers RUN a2enmod headers
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
COPY . . RUN mkdir raw
RUN chmod -R a+r $(ls -I vendor)
COPY www/ .
RUN chmod -R a+rw $(ls -I vendor)

View file

@ -0,0 +1,4 @@
FROM mysql:8.0-debian
WORKDIR /docker-entrypoint-initdb.d
COPY mysql_schema.sql ./

View file

@ -5,22 +5,44 @@ services:
dockerfile: docker_dev/Dockerfile dockerfile: docker_dev/Dockerfile
ports: ports:
- 8081:80 - 8081:80
environment:
MYSQL_USER: root
MYSQL_PASSWORD: example
MYSQL_DATABASE: dragon_fire
MYSQL_HOST: mysql
MYSQL_PORT: 3306
develop: develop:
watch: watch:
- path: ./ - path: ./
action: rebuild action: rebuild
- path: ../. - path: ../www/composer.*
action: rebuild
- path: ../www/
action: sync action: sync
target: /usr/local/apache2/htdocs/ target: /var/www/html
ignore:
- ../.git
- mysql_schema.sql
volumes:
- website_datavolume:/var/www/html/raw
mysql: mysql:
image: mysql:8.0-debian build:
dockerfile: MysqlDockerfile
# NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password # NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password
# (this is just an example, not intended to be a production configuration) # (this is just an example, not intended to be a production configuration)
command: --default-authentication-plugin=mysql_native_password command: --default-authentication-plugin=mysql_native_password
restart: always restart: always
environment: environment:
MYSQL_ROOT_PASSWORD: example MYSQL_ROOT_PASSWORD: example
ports:
- 3306:3306
develop:
watch:
- path: mysql_schema.sql
action: rebuild
volumes: volumes:
- sqlvolume:/var/lib/mysql - sqlvolume:/var/lib/mysql
volumes: volumes:
sqlvolume: {} sqlvolume: {}
website_datavolume: {}

148
docker_dev/mysql_schema.sql Normal file
View file

@ -0,0 +1,148 @@
CREATE DATABASE dragon_fire;
USE dragon_fire;
CREATE TABLE posts (
post_id INTEGER AUTO_INCREMENT,
host VARCHAR(64) NOT NULL,
post_path VARCHAR(255) NOT NULL,
post_path_depth INTEGER NOT NULL DEFAULT 0,
post_create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_access_count INTEGER DEFAULT 0,
post_metadata JSON NOT NULL,
post_settings_cache JSON DEFAULT NULL,
post_content MEDIUMTEXT,
PRIMARY KEY(post_id),
CONSTRAINT unique_post UNIQUE (host, post_path),
INDEX(host, post_path),
INDEX(post_path_depth, post_path),
INDEX(post_create_time),
INDEX(post_update_time)
);
CREATE TABLE path_access_counts (
access_time DATETIME NOT NULL,
host VARCHAR(64) NOT NULL,
post_path VARCHAR(255),
agent VARCHAR(255),
referrer VARCHAR(255),
path_access_count INTEGER DEFAULT 0,
path_processing_time DOUBLE PRECISION DEFAULT 0,
PRIMARY KEY(access_time, host, post_path, agent, referrer)
);
CREATE TABLE feed_cache (
host VARCHAR(64) NOT NULL,
search_path VARCHAR(255),
export_type VARCHAR(255),
feed_created_on DATETIME DEFAULT CURRENT_TIMESTAMP,
feed_content MEDIUMTEXT,
PRIMARY KEY(host, search_path, export_type)
);
INSERT INTO posts (post_path, post_path_depth, post_metadata, post_content)
VALUES (
'/about',
0,
'
{
"tags": ["test", "test2", "hellorld"],
"brief": "This is a simple test indeed",
"type": "text/markdown",
"title": "About the dergen"
}
',
'
# About the dergs indeed
This is just a simple test. Might be nice, though!
'
), (
'/about/neira',
1,
'
{
"tags": ["test", "test2", "hellorld", "neira"],
"brief": "This is a soft grab of Neira",
"type": "text/markdown",
"title": "About her"
}
',
'
# Nothing here yet!
Sorry for this. She is working hard :>
'
), (
'/about/xasin',
1,
'
{
"tags": ["test", "test2", "hellorld", "xasin"],
"brief": "This is a soft grab of Xasin",
"type": "text/markdown",
"title": "About her"
}
',
'
# Nothing here yet!
Sorry for this. He is working hard :>
'
), (
'/about/mesh',
1,
'
{
"tags": ["test", "test2", "hellorld", "mesh"],
"brief": "This is a soft grab of Mesh",
"type": "text/markdown",
"title": "About her"
}
',
'
# Nothing here yet!
Sorry for this. Shi is working hard :>
'
), (
'/about/alviere',
1,
'
{
"tags": ["test", "test2", "hellorld", "mesh"],
"brief": "SHE GRABS",
"type": "text/markdown",
"title": "SHE GRABS"
}
',
'
# Nothing here yet!
Sorry for this. She GRABS A LOT
----
## And now, for the lorem:
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis orci. Risus commodo viverra maecenas accumsan lacus vel facilisis. Sed viverra tellus in hac habitasse. Nulla malesuada pellentesque elit eget gravida cum. Posuere sollicitudin aliquam ultrices sagittis orci a. Libero nunc consequat interdum varius sit amet. Bibendum arcu vitae elementum curabitur vitae nunc sed velit. Amet mauris commodo quis imperdiet massa tincidunt nunc pulvinar. Sed adipiscing diam donec adipiscing. Laoreet id donec ultrices tincidunt arcu non sodales. Id semper risus in hendrerit gravida rutrum quisque non. Ut venenatis tellus in metus vulputate eu.
Risus sed vulputate odio ut enim blandit volutpat. Placerat in egestas erat imperdiet. Non curabitur gravida arcu ac tortor dignissim convallis aenean. Neque aliquam vestibulum morbi blandit cursus risus at. Elementum integer enim neque volutpat ac tincidunt vitae semper. Eu ultrices vitae auctor eu augue ut. In mollis nunc sed id semper risus in hendrerit gravida. Lectus arcu bibendum at varius vel pharetra vel turpis nunc. In pellentesque massa placerat duis. Non quam lacus suspendisse faucibus. Vitae aliquet nec ullamcorper sit amet risus nullam. Accumsan lacus vel facilisis volutpat est velit egestas dui.
Risus feugiat in ante metus dictum at tempor commodo. Duis ut diam quam nulla. Nunc aliquet bibendum enim facilisis gravida neque convallis. Tincidunt augue interdum velit euismod in pellentesque. Praesent semper feugiat nibh sed pulvinar proin gravida hendrerit lectus. Non odio euismod lacinia at quis risus sed vulputate odio. Nunc sed blandit libero volutpat sed cras ornare arcu. Adipiscing enim eu turpis egestas pretium aenean pharetra magna. Ut tristique et egestas quis ipsum suspendisse. Blandit cursus risus at ultrices mi tempus imperdiet nulla malesuada.
'
);

View file

@ -1,43 +0,0 @@
<?php
require_once 'vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader(['./templates', './user_content']);
$twig = new \Twig\Environment($loader,['debug' => true]);
$twig->addExtension(new Twig\Extra\Markdown\MarkdownExtension());
use Twig\Extra\Markdown\DefaultMarkdown;
use Twig\Extra\Markdown\MarkdownRuntime;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
$twig->addRuntimeLoader(new class implements RuntimeLoaderInterface {
public function load($class) {
if (MarkdownRuntime::class === $class) {
return new MarkdownRuntime(new DefaultMarkdown());
}
}
});
if($_SERVER['REQUEST_URI'] == '/') {
echo $twig->render('root.html');
} elseif(preg_match('/^\/about(.html)?$/', $_SERVER['REQUEST_URI'])) {
echo $twig->render('about.html');
} elseif(preg_match('/^\/gallery\/([^\?]+)/', $_SERVER['REQUEST_URI'])) {
echo $twig->render('/gallery/gallery_entry.html', [
'image_url' => '/static/banner/0.png',
'image_title' => 'Test!',
'image_desc' => 'A soft piece made by a dear friend',
'artist_name' => 'Doggonaut',
'artist_src_link' => 'https://twitter.com/doggonaut'
]);
} else {
echo $twig->render('rrror.html',[
"error_code" => '404 Hoard not found!',
"error_description" => "Well, we searched
far and wide for `" . $_SERVER['REQUEST_URI'] . "` but
somehow it must have gotten lost... Sorry!"
]);
}
?>

15
scripts/upload_file.sh Executable file
View file

@ -0,0 +1,15 @@
CURL_URL=${DERG_UPLOAD_URL:-https://lucidragons.de/api/admin/upload}
CURL_KEY=${DERG_UPLOAD_KEY:-SoftDragonKeys}
for FILE_PATH in "$@"
do
if [ -f "${FILE_PATH}" ]; then
POST_PATH="/${FILE_PATH#"./"}"
echo "Uploading ${FILE_PATH} to ${POST_PATH}"
curl -i -X POST -H "Content-Type: multipart/form-data" \
-F "api_key=${CURL_KEY}" -F "post_path=${POST_PATH}" -F "post_data=@${FILE_PATH}" "${CURL_URL}"
fi
done

View file

@ -1,69 +0,0 @@
const banner_show_time = 600 * 1000.0
const banner_animated_style = "opacity 0.8s linear, transform 0.1s linear"
var banner_current_src = localStorage.getItem('main_banner_img')
function getBannerTime() {
return (new Date()).getTime() / banner_show_time
}
function getBannerSrc() {
return "/static/banner/" + Math.floor(getBannerTime() + 1000/banner_show_time) % 2 + ".png"
}
function update_banner_top(banner, banner_container) {
const banner_top_max = 0
const banner_top_min = -banner.clientHeight + banner_container.clientHeight
const banner_top = (1-(getBannerTime()%1)) * banner_top_min
banner.style.transform = "translateY(" + banner_top + 'px' + ")"
}
let banner_update_src = banner_current_src
function update_banner(banner, banner_container) {
image_select = getBannerSrc()
update_banner_top(banner, banner_container)
if(image_select != banner_update_src) {
banner.style.opacity = 0
setTimeout(() => {
banner.src = image_select
}, 1000)
banner_update_src = image_select
localStorage.setItem('main_banner_img', image_select)
document.getElementById("main_banner_img_link").href = "/gallery/test"
}
}
const banner_container = document.getElementById("main_header")
const banner = document.getElementById("main_banner_img")
banner.addEventListener('load', () => {
update_banner_top(banner, banner_container)
const next_banner_src = getBannerSrc()
if(banner_current_src != next_banner_src) {
banner.style.transition = banner_animated_style
setTimeout(() => banner.style.opacity = 0.3, 1000)
}
else {
banner.style.opacity = 0.3
setTimeout(() => banner.style.transition = banner_animated_style, 0)
}
banner_current_src = next_banner_src
})
document.addEventListener("DOMContentLoaded", function () {
banner.src = getBannerSrc()
document.getElementById("main_banner_img_link").href = "/gallery/test"
})
setInterval(() => update_banner(banner, banner_container), 100)
addEventListener("resize", () => update_banner(banner, banner_container));

View file

@ -1,134 +0,0 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
color: #B0B0B0;
background: #201c2a;
margin: 0px;
position: relative;
min-height: 100vh;
padding-bottom: 4em;
}
:link {
color: cyan;
font-style: italic;
text-decoration: none;
transition: color 0.2s;
}
a:visited {
color: cyan;
}
a:hover {
color: lightblue;
}
#main_header {
overflow: hidden;
position: relative;
padding-bottom: 0.7em;
}
#main_banner_img {
position: absolute;
left: 0px;
right: 0px;
width: 100vw;
z-index: -1;
opacity: 0;
top: 0px;
}
#main_banner_img_link {
position: absolute;
right: 2vw;
bottom: 0.5em;
font-size: 0.8em;
}
#nav_bar {
display: flex;
flex-direction: row;
justify-content: center;
list-style-type: none;
margin-top: 1em;
padding: 0px;
}
#nav_bar li {
padding: 0em 0.3em 0em 0.3em;
}
#big_title {
text-align: center;
font-size: 2.5em;
margin-bottom: 0.2em;
}
#main_header h2 {
text-align: center;
font-size: 2em;
margin-bottom: 0.2em;
}
#title_separator {
height: 1.5px;
background-color: #ddd;
opacity: 0.5;
margin-left: 2em;
margin-right: 2em;
}
#main_content_wrapper {
padding: 3vmin 1em 1em 1em;
width: auto;
margin-left: max(0.75em, min(max(20vmin, 50vw - 30rem), 50vw - 25rem));
margin-right: max(0.75em, min(max(20vmin, 50vw - 30rem), 50vw - 25rem));
min-height: 100%;
background: #3e355479;
}
#main_content_wrapper article {
background: #3e3554;
color: #c6c3c3;
border-radius: 0.3em;
box-shadow: 3px 7px 7px 0px #00000040;
padding: 0.75em;
}
#main_content_wrapper article h1 {
text-align: left;
padding-left: 3vmin;
margin-bottom: 0.2em;
border-bottom: solid 1px darkgrey;
}
#main_footer {
display: flex;
height: 2.5em;
text-align: center;
background-color: #3a3a3a;
margin: 0px;
position: absolute;
bottom: 0px;
width: 100%;
}
#main_footer span {
align-self: flex-end;
width: 100%;
}

View file

@ -1,13 +0,0 @@
#rrr_header {
font-size: 2em;
border-bottom: 1px solid grey;
padding-bottom: 0.2em;
margin-bottom: 0.3em;
}
#rrr_code {
font-size: 1.5em;
}

View file

@ -1,13 +0,0 @@
{% extends "root.html" %}
{% block second_title %}
<h2> Abouts </h2>
{% endblock %}
{%block main_content%}
<article>
{{ include('about.md')|markdown_to_html }}
</article>
{%endblock%}

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Lucidragons' Fire</title>
<link rel="stylesheet" href="/dergstyle.css">
</head>
<body>
<h1 id="big_title">The dergsite</h1>
<div id="title_separator"></div>
<ul>
<li><a href="/about"> About </a></li>
<li><a href="/blog"> Blog </a></li>
<li><a href="/projects"> Projects </a></li>
<li><a href="/artwork"> Artworks </a></li>
</ul>
<div id="main_content_wrapper">
{% block content %}<h3>Soon there shall be content!</h3>{% endblock %}
</div>
<div id="sidebar-wrapper">
{%block sidebar %}
<ul>
<li> Test 1 </li>
</ul>
{% endblock %}
</div>
</body>
</html>

View file

@ -1,18 +0,0 @@
<html>
<head>
<title>PHP Test</title>
</head>
<body>
<?php
echo '<p>Hello World</p>';
require 'src/templater.php';
$test = new TemplateFillout('test.dergplate');
$test->render();
?>
</body>
</html>

View file

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Lucidragons' Fire</title>
<link rel="stylesheet" href="/static/dergstyle.css">
<link rel="icon" type="image/x-icon" href="/static/icon.jpeg">
<meta name="viewport" content="width=device-width,initial-scale=1">
{% block extra_head %}{% endblock %}
<script src="/static/banner.js" defer></script>
</head>
<body>
<header id="main_header">
<img id="main_banner_img"></img>
<a id="main_banner_img_link" href="/gallery"> full picture</a>
<h1 id="big_title">{% block big_title %}The dergsite{%endblock%}</h1>
{% block second_title %}{% endblock %}
<div id="title_separator"></div>
<menu id="nav_bar">
<li><a href="/about"> About </a></li>
<li><a href="/blog"> Blog </a></li>
<li><a href="/projects"> Projects </a></li>
<li><a href="/artwork"> Artworks </a></li>
</menu>
</header>
<main id="main_content_wrapper">
{% block main_content %}<h3>Soon there shall be content!</h3>{% endblock %}
</main>
<footer id="main_footer">
{% block main_footer %}
<span> test? </span>
{% endblock %}
</footer>
</body>
</html>

View file

@ -1,21 +0,0 @@
{% extends "root.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/rrrorstyle.css">
{% endblock %}
{% block second_title %}
<h2> (Broken) </h2>
{% endblock %}
{% block main_content %}
<article>
<h1 id="rrr_header"> The Dergs are confused:</h2>
<h2 id="rrr_code"> {{ error_code }}</h1>
<div>
{{ error_description|markdown_to_html }}
</div>
</article>
{% endblock %}

8
test_entries/README.md Normal file
View file

@ -0,0 +1,8 @@
---
settings:
colourscheme: fun
post_style: generic
banners:
- src: /banner/0.png
- src: /banner/1.png
---

View file

@ -0,0 +1,11 @@
---
tags: [what]
directory_data:
type: text/markdown
---
# The dergens
The
yes

View file

@ -0,0 +1,8 @@
---
---
# About her
This is some test content, to simulate a little markdown page for... Well, our website.
I wouldn't worry too much about it. Just know it's here :)

View file

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before After
Before After

View file

@ -0,0 +1,8 @@
---
title: A cuddly image <3
author: Shaky // Doggonaut
---
# Cuddly dragons
A dear picture made by a dear friend. Shaky - we hope you are OK.

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

View file

@ -0,0 +1,10 @@
---
directory:
settings:
banners:
- src: /about/neira/Neira_Queen.png
from: 0.5
to: 0.95
---
# She is soft~<3

View file

@ -0,0 +1,5 @@
---
title: Oh, the pain
---
# AAA

View file

@ -0,0 +1,9 @@
---
tags:
- neira
- comfy
- testing
title: Subtest, it's nice :>
---
## Subtests, because it's nice

View file

@ -0,0 +1,12 @@
---
title: A little image test idea
---
# README concept
This file is just to show the README concept - it's its own file but will be
rendered under a directory listing :)
There's also a test for an image! Let's hope that works:
![A very cute image :>](1.png)

View file

Before

Width:  |  Height:  |  Size: 527 KiB

After

Width:  |  Height:  |  Size: 527 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -0,0 +1,8 @@
---
title: A cuddly image <3
author: Shaky // Doggonaut
---
# Cuddly dragons
A dear picture made by a dear friend. Shaky - we hope you are OK.

View file

@ -0,0 +1,17 @@
---
title: A little image test idea
directory:
settings:
colourscheme: spicy
banners:
- src: /about/neira/Neira_Queen.png
---
# README concept
This file is just to show the README concept - it's its own file but will be
rendered under a directory listing :)
There's also a test for an image! Let's hope that works:
![A very cute image :>](1.png)

View file

@ -1,15 +0,0 @@
# About the dergens
### Who we are
Just little things, but it does feel good.
No spacing, huh...
### What we like to do
nom Nom Nom test?
Though I don't understand why this is not quite functional...
Markdown truly is a delight to work with!

1
www/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
vendor/

View file

@ -1 +1,34 @@
AddType text/plain .md
AddType text/plain .atom
AddType text/plain .rss
# php_value upload_max_filesize 40M
# php_value post_max_size 42M
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_URI} !^/(raw|static)/
RewriteRule ^.*\.(flv|gif|ico|jpg|jpeg|mp4|mpeg|png|svg|swf|webp)$ raw/%{HTTP_HOST}%{REQUEST_URI} [L,END]
RewriteRule ^/?raw/(.*)$ raw/%{HTTP_HOST}/$1 [L,END]
RewriteEngine On
RewriteCond %{HTTPS} !on
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,END]
RewriteCond %{REQUEST_URI} !^/?(static|raw|robots\.txt).*
RewriteRule (.*) router.php
Allow from all Allow from all
Options +Indexes
<filesMatch ".(flv|gif|ico|jpg|jpeg|mp4|mpeg|png|svg|swf|webp)$">
Header set Cache-Control "max-age=315360, public"
</filesMatch>
<filesMatch ".(js)$">
Header set Cache-Control "max-age=315360, public"
</filesMatch>

12
www/composer.json Normal file
View file

@ -0,0 +1,12 @@
{
"require": {
"twig/twig": "^3.0",
"twig/markdown-extra": "^3.6",
"league/commonmark": "^2.4",
"spatie/yaml-front-matter": "^2.0",
"laminas/laminas-feed": "^2.6",
"erusev/parsedown": "^1.7",
"erusev/parsedown-extra": "^0.8.1",
"scrivo/highlight.php": "v9.18.1.10"
}
}

File diff suppressed because it is too large Load diff

39
www/dergdown.php Normal file
View file

@ -0,0 +1,39 @@
<?php
use Highlight\Highlighter;
class Dergdown extends ParsedownExtra
{
protected $highlighter;
public function __construct()
{
$this->highlighter = new Highlighter();
}
protected function blockFencedCodeComplete($block)
{
if (! isset($block['element']['text']['attributes'])) {
return $block;
}
$code = $block['element']['text']['text'];
$languageClass = $block['element']['text']['attributes']['class'];
$language = explode('-', $languageClass);
try {
$highlighted = $this->highlighter->highlight($language[1], $code);
$block['element']['text']['attributes']['class'] = vsprintf('%s hljs %s', [
$languageClass,
$highlighted->language,
]);
$block['element']['text']['rawHtml'] = $highlighted->value;
unset($block['element']['text']['text']);
} catch (DomainException $e) {
}
return $block;
}
}

View file

@ -1,42 +0,0 @@
body {
color: #B0B0B0;
background: #302A3F;
}
:link {
color: cyan;
font-style: italic;
text-decoration: none;
transition: color 0.2s;
}
:link:hover {
color: lightblue;
}
ul {
display: flex;
flex-direction: row;
justify-content: center;
list-style-type: none;
margin-top: 1em;
padding: 0px;
}
ul li {
padding: 0em 0.3em 0em 0.3em;
}
#big_title {
text-align: center;
font-size: 2.5em;
margin-bottom: 0.2em;
}
#title_separator {
height: 1.5px;
background-color: #ddd;
opacity: 0.5;
margin-left: 2em;
margin-right: 2em;
}

11
www/fontawesome.php Normal file
View file

@ -0,0 +1,11 @@
<?php
$FONT_AWESOME_ARRAY=[
'markdown' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M593.8 59.1H46.2C20.7 59.1 0 79.8 0 105.2v301.5c0 25.5 20.7 46.2 46.2 46.2h547.7c25.5 0 46.2-20.7 46.1-46.1V105.2c0-25.4-20.7-46.1-46.2-46.1zM338.5 360.6H277v-120l-61.5 76.9-61.5-76.9v120H92.3V151.4h61.5l61.5 76.9 61.5-76.9h61.5v209.2zm135.3 3.1L381.5 256H443V151.4h61.5V256H566z"/></svg>',
'image' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6h96 32H424c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>',
'images' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M160 32c-35.3 0-64 28.7-64 64V320c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H160zM396 138.7l96 144c4.9 7.4 5.4 16.8 1.2 24.6S480.9 320 472 320H328 280 200c-9.2 0-17.6-5.3-21.6-13.6s-2.9-18.2 2.9-25.4l64-80c4.6-5.7 11.4-9 18.7-9s14.2 3.3 18.7 9l17.3 21.6 56-84C360.5 132 368 128 376 128s15.5 4 20 10.7zM192 128a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM48 120c0-13.3-10.7-24-24-24S0 106.7 0 120V344c0 75.1 60.9 136 136 136H456c13.3 0 24-10.7 24-24s-10.7-24-24-24H136c-48.6 0-88-39.4-88-88V120z"/></svg>',
'folder' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z"/></svg>',
'rss' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>'
];
?>

View file

@ -1,11 +0,0 @@
<?php
require_once '../vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader('../templates');
$twig = new \Twig\Environment($loader);
echo $twig->render('index.html',[
"a_variable" => "is very spicy. In a good way."
]);
?>

431
www/mysql_adapter.php Normal file
View file

@ -0,0 +1,431 @@
<?php
class MySQLAdapter {
public $raw;
function __construct($SITE_CONFIG) {
$this->SITE_CONFIG = $SITE_CONFIG;
$db_params = $SITE_CONFIG['db'];
try {
if(false !== getenv('MYSQL_HOST')) {
$this->raw = mysqli_connect(getenv('MYSQL_HOST'),
getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
getenv('MYSQL_PORT'));
}
else {
$this->raw = mysqli_connect($db_params['host'],
$db_params['user'], $db_params['password'],
$db_params['database'],
$db_params['port']);
}
} catch (\Throwable $th) {
echo 'Connection failed<br>';
echo 'Error number: ' . mysqli_connect_errno() . '<br>';
echo 'Error message: ' . mysqli_connect_error() . '<br>';
die();
}
}
function _sanitize_path($post_path) {
$post_path = chop($post_path, '/');
if($post_path == "") {
return "";
}
if(!preg_match('/^(?:\/[\w-]+)+(?:\.[\w-]+)*$/', $post_path)) {
echo "Post path match against " . $post_path . " failed!";
die();
}
return $post_path;
}
function _exec($qery, $argtypes = '', ...$args) {
$stmt = $this->raw->prepare($qery);
if($argtypes != ""){
$stmt->bind_param($argtypes, ...$args);
}
$stmt->execute();
return $stmt->get_result();
}
function _normalize_post_data($post_data) {
$post_data ??= ['found' => null];
if(isset($post_data['found']) && $post_data['found'] == false) {
return $post_data;
}
$post_data["found"] = true;
$post_data['post_metadata'] = json_decode($post_data["post_metadata"], true) ?? [];
$post_data["post_content"] ??= '';
return $post_data;
}
function _normalize_post_array($post_data) {
$post_data ??= [];
return array_map(function($post) {
return $this->_normalize_post_data($post);
}, $post_data);
}
function bump_post($post_path, $post_metadata = [], $create_dirs = true) {
$post_path = $this->_sanitize_path($post_path);
$path_depth = substr_count($post_path, "/");
if($create_dirs) {
$this->make_post_directory(dirname($post_path));
}
$qry = "
INSERT INTO posts
(host, post_path, post_path_depth, post_metadata, post_content)
VALUES
( ?, ?, ?, ?, ?) AS new
ON DUPLICATE KEY UPDATE post_path=new.post_path;";
$this->_exec($qry, "ssiss",
$this->SITE_CONFIG['HTTP_HOST'],
$post_path,
$path_depth,
json_encode($post_metadata),
'');
}
function make_post_directory($directory) {
$json_metadata = ["type" => 'directory'];
while(strlen($directory) > 1) {
try {
$this->bump_post($directory, $json_metadata, false);
}
catch(Exception $e) {
}
$directory = dirname($directory);
}
}
function log_post_access($post_path, $agent, $referrer, $time) {
$post_path = $this->_sanitize_path($post_path);
$qry = "INSERT INTO path_access_counts
(access_time,
host, post_path, agent, referrer,
path_access_count,
path_processing_time)
VALUES ( from_unixtime(floor(unix_timestamp(CURRENT_TIMESTAMP) / 300)*300),
?, ?, ?, ?, 1, ?
) AS new
ON DUPLICATE KEY
UPDATE path_access_count=path_access_counts.path_access_count+1,
path_processing_time=path_access_counts.path_processing_time+new.path_processing_time;
";
$this->_exec($qry, "ssssd", $this->SITE_CONFIG['HTTP_HOST'], $post_path, $agent, $referrer, $time);
if(preg_match('/^user/', $agent)) {
$this->_exec("UPDATE posts SET post_access_count=post_access_count+1 WHERE post_path=? AND host=?", "ss",
$post_path, $this->SITE_CONFIG['HTTP_HOST']);
}
}
function get_post_access_counters() {
$qry = "
SELECT host, post_path, agent, path_access_count, path_processing_time
FROM path_access_counts
WHERE path_last_access_time > ( CURRENT_TIMESTAMP - INTERVAL 10 MINUTE );
";
$data = $this->_exec($qry, "")->fetch_all(MYSQLI_ASSOC);
$out_data = [];
foreach($data AS $post_data) {
$path = $post_data['post_path'];
$agent_data = ($out_data[$path] ?? []);
$agent_data[$post_data['agent']] = [
'count' => $post_data['path_access_count'],
'time' => round($post_data['path_processing_time'], 6)
];
$out_data[$path] = $agent_data;
}
return $out_data;
}
function get_post_access_counters_line() {
$qry = "
SELECT host, access_time, post_path, agent, referrer, path_access_count, path_processing_time
FROM path_access_counts
WHERE access_time < ( CURRENT_TIMESTAMP - INTERVAL 6 MINUTE )
ORDER BY access_time DESC;
";
$this->raw->begin_transaction();
$top_access_time = null;
try {
$data = $this->_exec($qry, "")->fetch_all(MYSQLI_ASSOC);
$data_prefix="access_metrics";
$out_data = "";
foreach($data AS $post_data) {
$top_access_time ??= $post_data['access_time'];
$path = $post_data['post_path'];
if($path == '') {
$path = '/';
}
$out_data .= $data_prefix . ",host=" . $post_data['host'] . ",agent=".$post_data['agent'];
$out_data .= ",path=".$path.",referrer=".$post_data['referrer'];
$out_data .= " access_sum=" . $post_data['path_access_count'] . ",time_sum=" . $post_data['path_processing_time'];
$out_data .= " " . strtotime($post_data['access_time']) . "000000000\n";
}
$this->_exec("DELETE FROM path_access_counts WHERE access_time <= ?", "s", $top_access_time);
$this->raw->commit();
return $out_data;
} catch (\Throwable $th) {
$this->raw->rollback();
throw $th;
}
}
function reset_post_settings_cache($post_path) {
$post_path = $this->_sanitize_path($post_path);
$this->_exec("
UPDATE posts
SET post_settings_cache=NULL
WHERE host = ? AND post_path LIKE ?;
", "ss", $this->SITE_CONFIG['HTTP_HOST'], $post_path . "%");
}
function escape_tag($tag) {
return preg_replace_callback('/[\WZ]/', function($match) {
return "Z" . ord($match[0]);
}, strtolower($tag));
}
function escape_search_tag($tag) {
preg_match("/^([\+\-]?)(.*?)(\*?)$/", $tag, $matches);
if(!isset($matches[1])) {
echo "Problem with tag!";
var_dump($tag);
}
return $matches[1] . $this->escape_tag($matches[2]) . $matches[3];
}
function update_post_search_data($post_path, $post_tags) {
$post_tags []= "path:" . $post_path;
$post_tags []= "host:" . $this->SITE_CONFIG['HTTP_HOST'];
$post_tags = array_unique($post_tags);
$post_tags = array_map(function($val) {
return $this->escape_tag($val);
}, $post_tags);
asort($post_tags);
$post_tags = join(' ', $post_tags);
$qry = "
INSERT INTO posts
( host, post_path, post_tags )
VALUES
( ?, ?, ? ) AS new
ON DUPLICATE KEY
UPDATE post_tags=new.post_tags;
";
$this->_exec($qry, "sss",
$this->SITE_CONFIG['HTTP_HOST'], $post_path, $post_tags);
}
function perform_post_search($taglist, $order = null, $limit = 20, $page = 0) {
$allowed_ordering = [
"post_create_time"
];
$qry = "
SELECT post_path, post_metadata
FROM posts
WHERE MATCH(post_tags) AGAINST (? IN BOOLEAN MODE)
";
if(!is_array($taglist)) {
$taglist = explode(' ', $taglist);
}
$taglist []= '+host:' . $this->SITE_CONFIG['HTTP_HOST'];
$taglist = array_unique($taglist);
$taglist = array_map(function($key) {
return $this->escape_search_tag($key);
}, $taglist);
$taglist = implode(' ', $taglist);
if(isset($order) and in_array($order, $allowed_ordering)) {
$qry = $qry . " ORDER BY " . $order;
}
$qry = $qry . " LIMIT ? OFFSET ?";
$search_results = $this->_exec($qry, "sii", $taglist, $limit, $limit * $page)->fetch_all(MYSQLI_ASSOC);
$search_results = [
"query_string" => $taglist,
"results" => $this->_normalize_post_array($search_results)
];
return $search_results;
}
function update_or_create_post($post_path, $post_metadata, $post_content) {
$post_path = $this->_sanitize_path($post_path);
$path_depth = substr_count($post_path, "/");
$this->make_post_directory(dirname($post_path));
$this->reset_post_settings_cache($post_path);
$qry = "
INSERT INTO posts
(host, post_path, post_path_depth, post_metadata, post_content)
VALUES
( ?, ?, ?, ?, ?) AS new
ON DUPLICATE KEY
UPDATE post_metadata=new.post_metadata,
post_content=new.post_content,
post_update_time=CURRENT_TIMESTAMP;";
$this->_exec($qry, "ssiss",
$this->SITE_CONFIG['HTTP_HOST'],
$post_path,
$path_depth,
json_encode($post_metadata),
$post_content);
$this->update_post_search_data($post_path, $post_metadata['tags'] ?? []);
}
function get_settings_for_path($post_path) {
$post_path = $this->_sanitize_path($post_path);
$post_settings = $this->_exec("
SELECT post_path, post_settings_cache
FROM posts
WHERE post_path = ? AND host = ?
", "ss", $post_path, $this->SITE_CONFIG['HTTP_HOST'])->fetch_assoc();
if(!isset($post_settings)) {
return [];
}
if(isset($post_settings['post_settings_cache'])) {
return json_decode($post_settings['post_settings_cache'], true);
}
$parent_settings = [];
if($post_path != "") {
$parent_settings = $this->get_settings_for_path(dirname($post_path));
}
$post_settings = [];
$post_metadata = $this->_exec("
SELECT post_path, post_metadata
FROM posts
WHERE post_path = ? AND host = ?
", "ss", $post_path, $this->SITE_CONFIG['HTTP_HOST'])->fetch_assoc();
if(isset($post_metadata['post_metadata'])) {
$post_metadata = json_decode($post_metadata['post_metadata'], true);
if(isset($post_metadata['settings'])) {
$post_settings = $post_metadata['settings'];
}
}
$post_settings = array_merge($parent_settings, $post_settings);
$this->_exec("UPDATE posts SET post_settings_cache=? WHERE post_path=?", "ss",
json_encode($post_settings), $post_path);
return $post_settings;
}
function get_post_by_path($post_path,
$with_subposts = false, $with_settings = true) {
$qry = "SELECT *
FROM posts
WHERE post_path = ? AND host = ?
";
$post_path = $this->_sanitize_path($post_path);
$post_data = $this->_exec($qry, "ss", $post_path, $this->SITE_CONFIG['HTTP_HOST'])->fetch_assoc();
$post_data ??= ['found' => false];
$post_data['post_path'] = $post_path;
$post_data = $this->_normalize_post_data($post_data);
if(!$post_data['found']) {
return $post_data;
}
if($with_subposts) {
$post_data['subposts'] = $this->get_subposts_by_path($post_path);
}
if($with_settings) {
$post_data['settings'] = $this->get_settings_for_path($post_path);
}
return $post_data;
}
function get_subposts_by_path($path) {
global $sql;
$path = $this->_sanitize_path($path);
$path_depth = substr_count($path, "/");
$qry = "SELECT post_path, post_metadata, post_update_time
FROM posts
WHERE
host = ?
AND (post_path LIKE CONCAT(?,'/%'))
AND post_path_depth = ?
ORDER BY post_path ASC
LIMIT 50";
$post_data = $this->_exec($qry, "ssi", $this->SITE_CONFIG['HTTP_HOST'],
$path, $path_depth+1)->fetch_all(MYSQLI_ASSOC);
$post_data = $this->_normalize_post_array($post_data);
return $post_data;
}
}
?>

226
www/post_adapter.php Normal file
View file

@ -0,0 +1,226 @@
<?php
require_once 'mysql_adapter.php';
use Spatie\YamlFrontMatter\YamlFrontMatter;
use Laminas\Feed\Writer\Feed;
class PostHandler extends MySQLAdapter {
public $data_directory;
function __construct($SITE_CONFIG) {
parent::__construct($SITE_CONFIG);
$this->data_directory = 'raw/' . $this->SITE_CONFIG['HTTP_HOST'];
}
function deduce_post_type($post_path) {
$ext = pathinfo($post_path, PATHINFO_EXTENSION);
if(preg_match("/\.(\w+)\.md$/", $post_path, $ext_match)) {
$ext = $ext_match[1];
}
$ext_mapping = [
'' => 'directory',
'md' => 'text/markdown',
'png' => 'image',
'jpg' => 'image',
'jpeg' => 'image'
];
return $ext_mapping[$ext] ?? '?';
}
function fill_in_post_meta($post_path, $meta) {
$icon_mapping = [
'' => 'question',
'text/markdown' => 'markdown',
'directory' => 'folder',
'gallery' => 'images',
'image' => 'image'
];
$meta["title"] ??= basename($post_path);
if($meta["title"] == "") {
$meta["title"] = "root";
}
if(!isset($meta['media_file']) and preg_match("/\.(\w+)\.md$/", $post_path)) {
$meta['media_file'] = "https://" . $this->SITE_CONFIG['HTTP_HOST'] . chop($post_path, ".md");
}
$meta['tags'] ??= [];
$meta['type'] ??= $this->deduce_post_type($post_path);
$meta['icon'] ??= $icon_mapping[$meta['type']] ?? 'question';
return $meta;
}
function _normalize_post_data($post_data) {
$post_data = parent::_normalize_post_data($post_data);
if(!$post_data['found']) {
return $post_data;
}
$post_data["post_basename"] = basename($post_data["post_path"]);
$post_meta = $post_data['post_metadata'];
$post_data['post_metadata'] = $this->fill_in_post_meta(
$post_data['post_path'],
$post_meta);
$post_data["post_file_dir"] = '/raw' . $post_data["post_path"];
return $post_data;
}
function make_post_directory($directory) {
$data_directory = $this->data_directory . $directory;
is_dir($data_directory) || mkdir($data_directory, 0777, true);
parent::make_post_directory($directory);
}
function update_or_create_post(...$args) {
$this->_exec("TRUNCATE feed_cache");
parent::update_or_create_post(...$args);
}
function save_file($post_path, $file_path) {
move_uploaded_file($file_path, $this->data_directory . $post_path);
}
function save_markdown_post($post_path, $post_data) {
$frontmatter_post = YamlFrontMatter::parse($post_data);
$post_path = $this->_sanitize_path($post_path);
$post_content = $frontmatter_post->body();
$post_metadata = $frontmatter_post->matter();
$post_metadata = $this->fill_in_post_meta(
$post_path,
$post_metadata);
$post_metadata['tags'][]= 'type:' . $post_metadata['type'];
if(basename($post_path) == "README.md") {
$readme_metadata = [];
if(isset($post_metadata['settings'])) {
$readme_metadata['settings'] = $post_metadata['settings'];
}
if(isset($post_metadata['directory'])) {
$readme_metadata = $post_metadata['directory'];
}
$this->update_or_create_post(dirname($post_path),
$readme_metadata, $post_content);
}
$this->update_or_create_post($post_path, $post_metadata, $post_content);
}
function handle_upload($post_path, $file_path) {
$ext = pathinfo($post_path, PATHINFO_EXTENSION);
switch($ext) {
case "md":
$this->save_markdown_post($post_path, file_get_contents($file_path));
move_uploaded_file($file_path, $this->data_directory . $post_path);
break;
default:
$this->save_file($post_path, $file_path);
}
}
function try_get_cached_feed($path, $export_opt) {
$post_cache = $this->_exec("SELECT feed_content, feed_created_on
FROM feed_cache
WHERE host=? AND search_path=? AND export_type=?",
"sss", $this->SITE_CONFIG['HTTP_HOST'], $path, $export_opt)->fetch_assoc();
if(!isset($post_cache)) {
return null;
}
return ['feed' => $post_cache['feed_content'], 'feed_ts' => $post_cache['feed_created_on']];
}
function construct_feed($path) {
$path = $this->_sanitize_path($path);
$feed = @new Feed;
$feed->setTitle("DergFeed");
$feed->setLink($this->SITE_CONFIG['uri_prefix'] . $path);
$feed->setFeedLink($this->SITE_CONFIG['uri_prefix'] . "/feeds/atom" . $path, "atom");
$feed->setDateModified(time());
$feed->setDescription("DergenFeed for all your " . $path . " needs");
$feed_posts = $this->_exec("SELECT
post_path,
post_create_time, post_update_time,
post_content,
post_metadata
FROM posts
WHERE (host = ?) AND ((post_path = ?) OR (post_path LIKE ?))
ORDER BY post_create_time DESC LIMIT 200",
"sss", $this->SITE_CONFIG['HTTP_HOST'], $path, $path . '/%');
while($row = $feed_posts->fetch_array(MYSQLI_ASSOC)) {
$row = $this->_normalize_post_data($row);
$pmeta = $row['post_metadata'];
if($pmeta['type'] == 'directory') {
continue;
}
$entry = $feed->createEntry();
$entry->setTitle($row['post_path'] . '> ' . $pmeta['title']);
$entry->setLink($this->SITE_CONFIG['uri_prefix'] . $row['post_path']);
$entry->setDateModified(strtotime($row['post_update_time']));
$entry->setDateCreated(strtotime($row['post_create_time']));
$entry->setDescription($pmeta['brief'] ?? $pmeta['title']);
$feed->addEntry($entry);
}
return $feed;
}
function get_laminas_feed($path, $export_opt) {
$path = $this->_sanitize_path($path);
$feed_cache = $this->try_get_cached_feed($path, $export_opt);
if(isset($feed_cache)) {
return $feed_cache;
}
$feed = $this->construct_feed($path);
$this->_exec("INSERT INTO feed_cache
(host, search_path, export_type, feed_content)
VALUES
(?, ?, 'atom', ?),
(?, ?, 'rss', ?)",
"ssssss",
$this->SITE_CONFIG['HTTP_HOST'], $path, $feed->export('atom'),
$this->SITE_CONFIG['HTTP_HOST'], $path, $feed->export('rss'));
return $this->try_get_cached_feed($path, $export_opt);
}
}
?>

2
www/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Allow: /

268
www/router.php Normal file
View file

@ -0,0 +1,268 @@
<?php
$data_time_start = microtime(true);
require_once 'vendor/autoload.php';
require_once 'post_adapter.php';
require_once 'fontawesome.php';
require_once 'dergdown.php';
use Symfony\Component\Yaml\Yaml;
$SERVER_HOST = $_SERVER['HTTP_HOST'];
if(!preg_match('/^[\w\.]+$/', $SERVER_HOST)) {
die();
}
$SERVER_PREFIX = "https://" . $SERVER_HOST;
$SITE_CONFIG = Yaml::parseFile('secrets/' . $SERVER_HOST . '.config.yml');
$SITE_CONFIG['uri_prefix'] = $SERVER_PREFIX;
$SITE_CONFIG['HTTP_HOST'] = $SERVER_HOST;
$adapter = new PostHandler($SITE_CONFIG);
$loader = new \Twig\Loader\FilesystemLoader(['./templates', './user_content']);
$twig = new \Twig\Environment($loader,[
'debug' => true,
'cache' => 'twig_cache'
]);
$twig->addExtension(new Twig\Extra\Markdown\MarkdownExtension());
use Twig\Extra\Markdown\DefaultMarkdown;
use Twig\Extra\Markdown\MarkdownRuntime;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
function dergdown_to_html($text) {
$Parsedown = new Dergdown();
return $Parsedown->text($text);
}
function deduce_user_agent() {
$real_agent=$_SERVER['HTTP_USER_AGENT'];
if(preg_match('/(Googlebot|\w*Google\w*)/', $real_agent, $match)) {
return "bot/google/" . $match[1];
}
elseif(preg_match('/(Mozilla|Chrome|Chromium)/', $real_agent, $match)) {
return "user/" . $match[1];
}
else {
return "unidentified";
}
}
function log_and_die($path, $die_code = 0, $referrer = null) {
global $data_time_start;
global $adapter;
$data_time_end = microtime(true);
if(!isset($referrer)) {
$referrer = 'magic';
if(isset($_SERVER['HTTP_REFERER'])) {
$referrer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
}
}
$adapter->log_post_access($path,
deduce_user_agent(),
$referrer,
$data_time_end - $data_time_start);
die($die_code);
}
$twig->addRuntimeLoader(new class implements RuntimeLoaderInterface {
public function load($class) {
if (MarkdownRuntime::class === $class) {
return new MarkdownRuntime(new DefaultMarkdown());
}
}
});
function render_twig($template, $args = []) {
global $twig;
global $FONT_AWESOME_ARRAY;
global $SITE_CONFIG;
$args['fa'] = $FONT_AWESOME_ARRAY;
$post = $args['post'] ?? [];
$settings = $post['settings'] ?? [];
$meta = $post['post_metadata'] ?? [];
$args['banner'] ??= $settings['banners'] ?? $SITE_CONFIG['banners'];
$args['og'] = array_merge([
"site_name" => $SITE_CONFIG['opengraph']['site_name'],
"title" => $meta['title'] ?? $SITE_CONFIG['opengraph']['site_name'],
"url" => $_SERVER['REQUEST_URI'],
"type" => "article",
"description" => $meta['description']
?? $settings['description']
?? $SITE_CONFIG['opengraph']['description']
], $args['og'] ?? []);
if(($meta['type'] ?? '') == 'image') {
$args['og']['image'] ??= $meta['media_file'];
$args['og']['type'] = "image";
}
$args['og']['image'] ??= $args['banner'][0]["src"];
$args['banner'] = json_encode($args['banner']);
$args['site_config'] = $SITE_CONFIG;
$args['age_gate'] = (!isset($_COOKIE['AgeConfirmed'])) && isset($SITE_CONFIG['age_gate']);
$args['content_html'] ??= dergdown_to_html($post['post_content'] ?? '');
echo $twig->render($template, $args);
}
function try_render_post($SURI) {
global $adapter;
$post = $adapter->get_post_by_path($SURI);
if(!$post['found']) {
echo render_twig('post_types/rrror.html',[
"error_code" => '404 Hoard not found!',
"error_description" => "Well, we searched
far and wide for `" . $SURI . "` but
somehow it must have gotten lost... Sorry!",
"post" => array_merge($post, [
"post_metadata" => ["title" => "404 ???"]
])
]);
log_and_die('/404', referrer: ($_SERVER['HTTP_REFERER'] ?? 'magic'));
}
switch($post['post_metadata']['type']) {
case 'directory':
if(preg_match('/^(.*[^\/])((?:#.*)?)$/', $SURI, $match)) {
header('Location: ' . $match[1] . '/' . $match[2]);
die();
}
echo render_twig('post_types/directory.html', [
"post" => $post,
"subposts" => $adapter->get_subposts_by_path($SURI)
]);
break;
case 'text/markdown':
echo render_twig('post_types/markdown.html', [
"post" => $post
]);
break;
case 'gallery':
if(preg_match('/^(.*[^\/])((?:#.*)?)$/', $SURI, $match)) {
header('Location: ' . $match[1] . '/' . $match[2]);
die();
}
$search_query = $post['post_metadata']['search_tags'] ??
('+type:image +path:' . $post['post_path'] . '/*');
$search_result = $adapter->perform_post_search($search_query);
echo render_twig('post_types/gallery.html', [
"post" => $post,
"subposts" => $adapter->get_subposts_by_path($SURI),
"gallery_images" => $search_result['results']
]);
break;
case 'image':
echo render_twig('post_types/image.html', [
"post" => $post,
]);
break;
}
}
function generate_website($SURI) {
global $adapter;
global $FONT_AWESOME_ARRAY;
if(preg_match('/^\/api\/admin/', $SURI)) {
header('Content-Type: application/json');
$user_api_key = '';
if(isset($_GET['api_key'])) {
$user_api_key = $_GET['api_key'];
}
if(isset($_POST['api_key'])) {
$user_api_key = $_POST['api_key'];
}
if($user_api_key != file_get_contents('secrets/api_admin_key')) {
http_response_code(401);
echo json_encode([
"authorized" => false
]);
log_and_die('/api/401');
}
if($SURI = '/api/admin/upload') {
$adapter->handle_upload($_POST['post_path'], $_FILES['post_data']['tmp_name']);
echo json_encode(["ok" => true]);
}
} elseif(preg_match('/^\/api/', $SURI)) {
if($SURI == '/api/post_counters') {
header('Content-Type: application/json');
echo json_encode($adapter->get_post_access_counters());
} elseif($SURI == '/api/metrics') {
header('Content-Type: application/line');
echo $adapter->get_post_access_counters_line();
} elseif(preg_match('/^\/api\/posts(.*)$/', $SURI, $match)) {
header('Content-Type: application/json');
echo json_encode($adapter->get_post_by_path($match[1]));
} elseif(preg_match('/^\/api\/subposts(.*)$/', $SURI, $match)) {
header('Content-Type: application/json');
echo json_encode(get_subposts($match[1]));
} elseif($SURI == '/api/upload') {
echo $twig->render('upload.html');
} elseif($SURI == '/api/search') {
header('Content-Type: application/json');
echo json_encode($adapter->perform_post_search($_GET['search_query']));
}
} elseif(preg_match('/^\/feed(?:\/(rss|atom)(.*))?$/', $SURI, $match)) {
$feed = $adapter->get_laminas_feed($match[2] ?? '/', $match[1] ?? 'rss');
header('Content-Type: application/xml');
header('Cache-Control: max-age=1800');
header('Etag: W/"' . $SURI . '/' . strtotime($feed['feed_ts']) . '"');
echo $feed['feed'];
} elseif(true) {
try_render_post($SURI);
}
}
$URL_PATH = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
generate_website($URL_PATH);
log_and_die($URL_PATH);
?>

2
www/secrets/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.json
api_admin_key

2
www/secrets/.htaccess Normal file
View file

@ -0,0 +1,2 @@
Deny from all

View file

@ -1,5 +1,7 @@
Allow from all Allow from all
Options +Indexes
<filesMatch ".(flv|gif|ico|jpg|jpeg|mp4|mpeg|png|svg|swf|webp)$"> <filesMatch ".(flv|gif|ico|jpg|jpeg|mp4|mpeg|png|svg|swf|webp)$">
Header set Cache-Control "max-age=315360, public" Header set Cache-Control "max-age=315360, public"
</filesMatch> </filesMatch>

View file

@ -0,0 +1,12 @@
function confirmAge() {
let ageGate = document.getElementById("age_gate_block");
ageGate.style.opacity = 0;
setTimeout(() => {
ageGate.parentElement.removeChild(ageGate);
}, 1000);
document.cookie += "AgeConfirmed=true; path=/; max-age=31536000"
}

160
www/static/banner.js Normal file
View file

@ -0,0 +1,160 @@
const BANNER_TIME = 600 * 1000.0
const BANNER_ANIMATION = "opacity 0.8s linear, transform 0.1s linear"
class BannerHandler {
constructor(banner_container, banner_image, banner_link) {
this.bannerContainerDOM = banner_container
this.bannerDOM = banner_image
this.bannerLinkDOM = banner_link
this.bannerUpdateTimer = null
this.currentPhase = 0
this.currentBannerData = null
try {
this.currentBannerData = JSON.parse(localStorage.getItem('main_banner_img'))
} catch(e) {}
this.currentBannerData ||= {}
this.bannerDOM.onload=() => { this.onBannerLoaded() }
this.bannerDOM.onerror=() => {
this.fadeOut();
setTimeout(() => this.loadNextBanner(), 1000);
}
}
startUpdateTick() {
if(this.bannerUpdateTimer !== null) {
return
}
console.log("Starting tick")
this.bannerUpdateTimer = setInterval(() => { this.updateTick() }, 100);
}
stopUpdateTick() {
if(this.bannerUpdateTimer === null) {
return
}
console.log("Stopping tick!")
clearInterval(this.bannerUpdateTimer);
this.bannerUpdateTimer = null
}
getPhase() {
return (new Date()).getTime() / BANNER_TIME;
}
getTargetBanner() {
if(window.dergBannerOptions == null) {
return {}
}
var banner_index = Math.floor(this.getPhase()) % window.dergBannerOptions.length
var banner_choice = window.dergBannerOptions[banner_index]
return banner_choice
}
updateTranslation() {
const bannerTranslateMax = -this.bannerDOM.clientHeight + this.bannerContainerDOM.clientHeight
const bannerPercentageFrom = this.currentBannerData.from || 0;
const bannerPercentageTo = this.currentBannerData.to || 1;
const bannerPercentage = (bannerPercentageFrom + (bannerPercentageTo - bannerPercentageFrom) * this.currentPhase)
const banner_top = (1-bannerPercentage) * bannerTranslateMax
this.bannerDOM.style.transform = "translateY(" + banner_top + 'px' + ")"
}
fadeOut() {
this.bannerDOM.style.opacity = 0;
}
fadeIn() {
this.bannerDOM.style.opacity = this.currentBannerData.opacity || 0.3;
}
loadNextBanner() {
this.currentBannerData = this.getTargetBanner()
this.currentBannerData.bannerTime = new Date()
this.loadBanner()
}
loadBanner() {
console.log("Target banner:");
console.log(this.currentBannerData);
localStorage.setItem("main_banner_img", JSON.stringify(this.currentBannerData))
this.currentPhase = 0
if((this.currentBannerData === null)
|| (this.currentBannerData.src === undefined)) {
this.onBannerLoaded()
return
}
this.bannerDOM.src = this.currentBannerData.src
this.bannerLinkDOM.href = this.currentBannerData.href || this.currentBannerData.src
}
onBannerLoaded() {
console.log("Loaded?");
this.currentPhase = this.getPhase() % 1
this.updateTranslation()
this.fadeIn()
setTimeout(() => {
this.animateOn()
this.startUpdateTick()
}, 10)
}
updateTick() {
console.log("tick")
const nextPhase = this.getPhase() % 1;
if((nextPhase > this.currentPhase)
&& (this.currentBannerData.src == this.getTargetBanner().src)) {
this.currentPhase = nextPhase;
this.updateTranslation();
} else {
this.fadeOut()
setTimeout(() => {
this.loadNextBanner()
}, 1000);
this.stopUpdateTick()
}
}
animateOn() {
this.bannerDOM.style.transition = BANNER_ANIMATION
}
start() {
this.fadeIn()
this.loadBanner()
}
}
var bannerHandler = new BannerHandler(
document.getElementById("main_header"),
document.getElementById("main_banner_img"),
document.getElementById("main_banner_img_link"))
bannerHandler.start()
// addEventListener("resize", () => update_banner(banner, banner_container));

273
www/static/dergstyle.css Normal file
View file

@ -0,0 +1,273 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
svg {
fill: var(--text_1);
padding-top: 0.1rem;
}
body {
--bg_1: #0e0a2a;
--bg_2: #2c2943;
--bg_3: #3f4148;
--highlight_1: #ee9015;
--highlight_2: #edd29e;
--text_1: #FFFFFF;
--text_border: #A0A0A080;
color: var(--text_1);
background: var(--bg_1);
margin: 0px;
position: relative;
min-height: 100vh;
padding-bottom: 4rem;
}
@media only screen and (max-width: 600px) {
.hsmol_hide {
display: none !important;
visibility: hidden !important;
}
}
:link {
color: var(--highlight_1);
font-style: italic;
text-decoration: none;
transition: color 0.2s;
}
a:visited {
color: var(--highlight_1);
}
a:hover {
color: var(--highlight_2);
}
#age_gate_block {
position: fixed;
top: 0;
left: 0;
z-index: 1040;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(20px);
transition: opacity 0.8s linear;
}
#age_gate_block div {
background-color: var(--bg_3);
border-radius: 1em;
padding: 1em;
width: 12em;
margin:0 auto;
display: table;
position: absolute;
left: 0;
right:0;
top: 50%;
-webkit-transform:translateY(-50%);
-moz-transform:translateY(-50%);
-ms-transform:translateY(-50%);
-o-transform:translateY(-50%);
transform:translateY(-50%);
}
#age_gate_block p {
min-height: 5em;
}
#main_header {
overflow: hidden;
position: relative;
padding-bottom: 0.7rem;
}
#main_banner_img {
position: absolute;
left: 0px;
right: 0px;
width: 100vw;
z-index: -1;
opacity: 0;
top: 0px;
}
#main_banner_img_link {
position: absolute;
right: 2vw;
bottom: 0.5rem;
font-size: 0.8rem;
}
#nav_bar {
display: flex;
flex-direction: row;
justify-content: center;
list-style-type: none;
margin-top: 1rem;
padding: 0px;
}
#nav_bar li {
padding: 0rem 0.3rem 0rem 0.3rem;
}
#big_title {
text-align: center;
font-size: 2.5rem;
margin-bottom: 0.2rem;
}
#main_header h2 {
text-align: center;
font-size: 2rem;
margin-bottom: 0.2rem;
}
#title_separator {
height: 1.5px;
background-color: #ddd;
opacity: 0.5;
margin-left: 2rem;
margin-right: 2rem;
}
:target {
scroll-margin-top: 6rem;
}
#main_content_wrapper {
--content-width: min(100vw, calc(20rem + 40vw));
--content-total-margin: calc(calc(100vw - var(--content-width)) / 2);
--content-padding: max(0.5rem, min(1rem, var(--content-total-margin)));
--content-margin: max(0px, calc(var(--content-total-margin) - 1rem));
padding: 0rem var(--content-padding) 1rem var(--content-padding);
width: auto;
margin-left: var(--content-margin);
margin-right: var(--content-margin);
margin-top: 0px;
min-height: 100%;
background: var(--bg_2);
}
#post_file_bar {
position: sticky;
top: 0px;
background: var(--bg_2);
box-shadow: 0px 5px 5px 0px #00000040;
z-index: 5;
}
#post_file_titles {
display: flex;
flex-direction: row;
justify-content: left;
list-style-type: none;
padding: 0px;
}
#post_file_titles * {
padding: 0.5rem;
font-style: bold;
font-size: 1.3rem;
background: var(--highlight_1);
}
#post_file_path {
width: 100%;
font-style: italic;
padding-left: 0.5rem;
background: var(--highlight_1);
display: flex;
flex-direction: row;
list-style-type: none;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
}
#post_file_path a {
color: var(--text_1);
padding-right: 0.2rem;
}
#main_content_wrapper article {
background: var(--bg_3);
border-radius: 0rem 0rem 0.8rem 0.8rem;
box-shadow: 3px 7px 7px 0px #00000040;
padding: 0.75rem;
}
#main_content_wrapper article img {
display: block;
max-width: 100%;
max-height: 70vh;
margin: 1vmin;
margin-right: auto;
margin-left: auto;
}
#content_footer {
display: block;
max-width: 100%;
margin-top: 0.5em;
font-size: 0.8em;
border-top: solid 1px darkgrey;
opacity: 0.7;
}
#main_footer {
display: flex;
height: 2.5rem;
text-align: center;
background-color: #3a3a3a;
margin: 0px;
position: absolute;
bottom: 0px;
width: 100%;
}
#main_footer span {
align-self: flex-end;
width: 100%;
}

View file

@ -0,0 +1,36 @@
table.directory {
width: 100%;
}
table.directory caption {
text-align: left;
font-size: 1.5rem;
padding-left: 3vmin;
}
table.directory td {
padding: 0.2rem;
text-align: left;
}
table.directory th {
padding: 0.2rem;
text-align: left;
padding-bottom: 0.05rem;
}
table.directory tr.entry:hover {
background: rgba(255, 255, 255, 0.1);
}
table.directory tr.entry .entry_title {
width: 100%;
}
table.directory .entry_update_time {
display: block;
width: 12rem;
}

View file

@ -0,0 +1,64 @@
.gallery {
display: flex;
flex-direction: row;
flex-flow: row wrap;
justify-content: center;
list-style-type: none;
margin-top: 1rem;
padding: 0px;
}
.gallery li {
margin: 0.2rem;
margin-bottom: 1rem;
border: 1px solid var(--text_border);
float: left;
border-radius: 1rem;
box-shadow: 0px 5px 5px 0px #00000040;
background: var(--bg_2);
height: auto;
transition: 0.3s;
z-index: 1;
}
.gallery li:hover {
border: 1px solid var(--text_1);
transform: scale(1.02);
}
#main_content_wrapper .gallery img {
width: 100%;
height: 15rem;
margin: 0;
border-radius: 1rem 1rem 0 0;
object-fit: cover;
}
@media screen and (max-width: 48rem) {
.gallery li {
width: 45%;
}
#main_content_wrapper .gallery img {
height: 100%;
width: auto;
}
#main_content_wrapper .gallery figure {
height: auto;
}
}
.gallery figcaption {
padding: 0.1rem;
text-align: center;
}

View file

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

View file

@ -1,18 +1,12 @@
article { #main_content_wrapper figcaption h1 {
margin-top: 0.8em;
margin-left: 10%;
margin-right: 10%;
}
figcaption {
text-align: center; text-align: center;
border-bottom: solid 3px darkgrey; border-bottom: solid 3px darkgrey;
font-size: 2em; font-size: 1.8rem;
margin-bottom: 0.3em; margin-bottom: 0.3rem;
} }
#gallery_image { #gallery_image {

203
www/static/modest.css Normal file
View file

@ -0,0 +1,203 @@
/*
modest.css, licensed under MIT license, Crafted with <3 by John Otander (@4lpine).
Taken from: https://github.com/markdowncss/modest
Modified to fit Dergsite needs
*/
@media print {
*,
*:before,
*:after {
background: transparent !important;
color: #000 !important;
box-shadow: none !important;
text-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
a[href^="#"]:after,
a[href^="javascript:"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}
pre,
code {
font-family: Menlo, Monaco, "Courier New", monospace;
}
.modest-no-decoration {
text-decoration: none;
}
html {
font-size: 14px;
}
@media screen and (min-width: 32rem) and (max-width: 48rem) {
html {
font-size: 16px;
}
}
@media screen and (min-width: 48rem) {
html {
font-size: 17px;
}
}
article {
line-height: 1.5;
}
article p,
.modest-p {
font-size: 1rem;
margin-bottom: 1.3rem;
}
article h1,
.modest-h1,
article h2,
.modest-h2,
article h3,
.modest-h3,
article h4,
.modest-h4 {
margin: 1.5em 0 .3em;
font-weight: inherit;
line-height: 1.42;
padding-left: 1.5rem;
}
article > :first-child {
margin-top: 0.3rem !important;
}
article h1,
.modest-h1 {
margin-top: 0;
font-size: 1.998rem;
}
article h2,
.modest-h2 {
font-size: 1.427rem;
}
article h3,
.modest-h3 {
font-size: 1.299rem;
}
article h4,
.modest-h4 {
font-size: 1.1rem;
}
article h5,
.modest-h5 {
font-size: 1rem;
}
article h6,
.modest-h6 {
font-size: .88rem;
}
article small,
.modest-small {
font-size: .707rem;
}
/* https://github.com/mrmrs/fluidity */
article h1,
article h2,
article h3 {
border-bottom: 1px solid var(--text_border);
padding-bottom: .3rem;
}
blockquote {
padding-left: 0.8rem;
margin-left: 0.8rem;
margin-right: 4em;
}
blockquote {
border-left: 4px solid var(--text_border);
text-align: justify;
}
pre {
border-radius: 0.5rem;
box-shadow: 2px 5px 5px 0px #00000040;
border-left: 4px solid #206475;
background-color: var(--bg_2);
margin-bottom: 1.3rem;
margin-left: 0.8rem;
margin-right: 4em;
}
pre code {
border-radius: 0.5rem;
}
@media screen and (max-width: 32rem) {
pre, blockquote {
margin-right: 1.5em;
}
}
article ul,
article ol {
padding-left: 2em;
}

13
www/static/rrrorstyle.css Normal file
View file

@ -0,0 +1,13 @@
#rrr_header {
font-size: 2rem;
border-bottom: 1px solid grey;
padding-bottom: 0.2rem;
margin-bottom: 0.3rem;
}
#rrr_code {
font-size: 1.5rem;
}

View file

@ -0,0 +1,84 @@
/*
Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull <sourdrums@gmail.com>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #002b36;
color: #839496;
}
.hljs-comment,
.hljs-quote {
color: #586e75;
}
/* Solarized Green */
.hljs-keyword,
.hljs-selector-tag,
.hljs-addition {
color: #859900;
}
/* Solarized Cyan */
.hljs-number,
.hljs-string,
.hljs-meta .hljs-meta-string,
.hljs-literal,
.hljs-doctag,
.hljs-regexp {
color: #2aa198;
}
/* Solarized Blue */
.hljs-title,
.hljs-section,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #268bd2;
}
/* Solarized Yellow */
.hljs-attribute,
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-class .hljs-title,
.hljs-type {
color: #b58900;
}
/* Solarized Orange */
.hljs-symbol,
.hljs-bullet,
.hljs-subst,
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-link {
color: #cb4b16;
}
/* Solarized Red */
.hljs-built_in,
.hljs-deletion {
color: #dc322f;
}
.hljs-formula {
background: #073642;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View file

@ -0,0 +1,33 @@
<div id="post_file_bar">
<menu id="post_file_titles">
<li>
{{ post.post_metadata.title }}
</li>
</menu>
<menu id="post_file_path">
{% set split_post = post.post_path |split('/') %}
{% for i in range(0, split_post|length - 1) %}
<li>
{% if i != 0 %}
<a href="{{split_post|slice(0,i+1)|join('/')}}">
> {{ split_post[i] }}
</a>
{% else %}
<a href="/">root</a>
{% endif %}
</li>
{% endfor %}
<li class="hsmol_hide" style="margin-left: auto;">
<a href="/raw{{post.post_path}}">raw</a>
<a href="/api/posts{{post.post_path}}">api</a>
</li>
<li class="hsmol_hide">
<a rel="alternate" type="application/rss+xml" target="_blank"
style="padding-left: 0.3rem;" href="/feed/rss{{post.post_path}}">
{{ fa['rss']|raw }}
</a>
</li>
</menu>
</div>

View file

@ -0,0 +1,29 @@
{% extends "root.html" %}
{% block feed_links %}
{{ parent() }}
<link rel="alternate" type="application/atom+xml" title="{{og.site_name}} Feed for {{post.post_path}}" href="{{site_config.uri_prefix}}/feed/atom{{post.post_path}}">
{% endblock %}
{% block second_title %}
<h2> {{ post.post_metadata.title }} </h2>
{% endblock %}
{%block main_content%}
{{ include('fragments/filepath_bar.html') }}
<article>
{%block content_article %}
{%endblock%}
<span id="content_footer">
This article was created on {{ post.post_create_time }}, last edited {{ post.post_update_time }}, and was viewed {{ post.post_access_count }} times~
</span>
</article>
{%endblock%}

View file

@ -0,0 +1,43 @@
{% extends "pathed_content.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/directorystyle.css">
{%endblock%}
{%block content_article%}
{% if subposts|length > 0 %}
<h3>Directory contents:</h3>
<table class="directory">
<tr>
<th></th>
<th class="hsmol_hide">Name</th>
<th>Title</th>
<th class="entry_update_time hsmol_hide">Modified</th>
</tr>
{% for subpost in subposts %}
<tr class="entry">
<td>
{{ fa[subpost.post_metadata.icon] | raw }}
</td>
<td class="hsmol_hide">
<a href={{subpost.post_path}}>{{subpost.post_basename}}</a>
</td>
<td class="entry_title">
<a href={{subpost.post_path}}>{{ subpost.post_metadata.title }}</a>
</td>
<td class="entry_update_time hsmol_hide">
{{ subpost.post_update_time }}
</td>
</tr>
{% endfor %}
</table>
{%else%}
<h4>This directory appears to be empty...</h4>
<span>This shouldn't happen, sorry!</span>
{% endif %}
{{ post.post_content|markdown_to_html }}
{%endblock%}

View file

@ -0,0 +1,59 @@
{% extends "pathed_content.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/directorystyle.css">
<link rel="stylesheet" href="/static/gallerystyle.css">
{%endblock%}
{%block content_article%}
{% if subposts|length > 0 %}
<table class="directory">
<h3>Art Subfolders:</h3>
<tr>
<th></th>
<th class="hsmol_hide">Name</th>
<th>Title</th>
<th class="entry_update_time hsmol_hide">Modified</th>
</tr>
{% for subpost in subposts %}
<tr class="entry">
<td>
{{ fa[subpost.post_metadata.icon] | raw }}
</td>
<td class="hsmol_hide">
<a href={{subpost.post_path}}>{{subpost.post_basename}}</a>
</td>
<td class="entry_title">
<a href={{subpost.post_path}}>{{ subpost.post_metadata.title }}</a>
</td>
<td class="entry_update_time hsmol_hide">
{{ subpost.post_update_time }}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{{ content_html|raw }}
{% if gallery_images|length > 0 %}
<ul class="gallery">
{% for post in gallery_images %}
<li class="entry">
<a href={{post.post_path}}>
<figure>
<img src="{{post.post_metadata.media_file}}">
<figcaption>
{{ post.post_metadata.title }}
</figcaption>
</figure>
</a>
</li>
{% endfor %}
</ul>
{%else%}
<h4>How sad. There are no images yet... What a real shame :c </h4>
{% endif %}
{%endblock%}

View file

@ -0,0 +1,25 @@
{% extends "pathed_content.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/imagestyle.css">
{%endblock%}
{% block opengraph_tags %}
{{ parent() }}
<meta property="og:type" content="image" />
{%endblock %}
{%block content_article%}
<figure>
<a target="_blank" href="{{post.post_metadata.media_file}}">
<img id="gallery_image" src="{{post.post_metadata.media_file}}"></img>
</a>
<figcaption>
{{ content_html|raw }}
</figcaption>
</figure>
{%endblock%}

View file

@ -0,0 +1,13 @@
{% extends "pathed_content.html" %}
{% block opengraph_tags %}
{{ parent() }}
<meta property="og:type" content="article" />
{%endblock %}
{%block content_article%}
{{ content_html|raw }}
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends "pathed_content.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/rrrorstyle.css">
{% endblock %}
{% block second_title %}
<h2> (Broken) </h2>
{% endblock %}
{% block content_article %}
<h1 id="rrr_header"> The Dergs are confused:</h2>
<h2 id="rrr_code"> {{ error_code }}</h1>
<div>
{{ error_description|markdown_to_html }}
</div>
{% endblock %}

90
www/templates/root.html Normal file
View file

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<head>
<title>{{og.site_name}} - {{og.title}}</title>
<link rel="stylesheet" href="/static/dergstyle.css">
<link rel="stylesheet" href="/static/modest.css">
<link rel="stylesheet" href="/static/solarized-dark.css">
<link rel="icon" type="image/x-icon" href="/static/icon.jpeg">
<meta name="viewport" content="width=device-width,initial-scale=1">
{% block feed_links %}
<link rel="alternate" type="application/rss+xml" title="{{og.site_name}} Global Feed" href="{{site_config.uri_prefix}}/feed">
{% endblock %}
{% block extra_head %}{% endblock %}
{% block opengraph_tags %}
<meta property="og:site_name" content="{{og.site_name}}">
<meta property="og:url" content="{{og.url}}" />
<meta property="og:title" content="{{ og.title|e }}" />
<meta name="twitter:title" content="{{ og.title|e }}" />
<meta property="og:type" content="{{og.type}}" />
<meta property="og:description" content="{{ og.description|e }}" />
<meta name="twitter:description" content="{{ og.description|e }}" />
<meta property="og:image" content="{{og.image}}" />
<meta name="twitter:image" content="{{og.image}}" />
<meta name="twitter:card" content="summary_large_image">
<meta name="robots" content="max-image-preview:large">
<meta property="al:android:app_name" content="Medium" />
{% endblock %}
<script type="text/javascript">
window.dergBannerOptions = JSON.parse('{{banner|raw}}');
</script>
</head>
<body>
{%if age_gate %}
<div id="age_gate_block">
<div>
<p>
This website may contain content meant for an 18+ audience.
</p>
<input type="button" onclick="confirmAge()" value="I understand and am over 18.">
</div>
</div>
<script src="/static/age_gate_check.js"></script>
{% endif %}
<header id="main_header">
<img id="main_banner_img"></img>
<a id="main_banner_img_link" href="/gallery"> full picture</a>
<script src="/static/banner.js"></script>
<h1 id="big_title">{% block big_title %}{{og.site_name}}{%endblock%}</h1>
{% block second_title %}{% endblock %}
<div id="title_separator"></div>
<menu id="nav_bar">
<li><a href="/about"> About </a></li>
<li><a href="/blog"> Blog </a></li>
<li><a href="/projects"> Projects </a></li>
<li><a href="/artwork"> Artworks </a></li>
</menu>
</header>
<main id="main_content_wrapper">
{% block main_content %}<h3>Soon there shall be content!</h3>{% endblock %}
</main>
<footer id="main_footer">
{% block main_footer %}
<span> test? </span>
{% endblock %}
</footer>
</body>
</html>

19
www/templates/upload.html Normal file
View file

@ -0,0 +1,19 @@
{% extends "root.html" %}
{% block second_title %}
<h2> UPLOAD CONTENT </h2>
{% endblock %}
{%block main_content%}
<article>
<form method="post" enctype="multipart/form-data" action="/api/admin/upload">
<input type="text" id="post_path" name="post_path"/>
<input type="password" id="api_key" name="api_key"/>
<input type="file" id="post_data" name="post_data"/>
<button>Submit</button>
</form>
</article>
{%endblock%}