Compare commits

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

46 commits

Author SHA1 Message Date
f8af3c5c59 feat(yaps): first lifesigns of the yapping system
Some checks reported warnings
/ phplint (push) Has been cancelled
2025-04-28 10:46:56 +02:00
143c932c88 feat(database): add first version of the feeds table 2025-04-10 10:34:33 +02:00
bbc093b497 feat(analytics): add filters/detectors for assumed-bogus requests
Some checks reported warnings
/ phplint (push) Has been cancelled
2025-04-08 10:05:13 +02:00
73414fa639 feat(analytics): add request status field 2025-04-08 10:04:39 +02:00
6ae06b4765 fix(search): 🩹 fix blog post listing
Some checks reported warnings
/ phplint (push) Has been cancelled
2025-03-27 23:33:49 +01:00
5e2f0a7185 feat(database): add completely new analytics backend 2025-03-27 23:33:27 +01:00
31080cae2b fix: 🩹 re-add the 404 error page 2025-03-27 23:32:31 +01:00
a3604d21c9 feat(database): re-add per-post counters to track page views etc. 2025-03-27 23:31:29 +01:00
5ddbae91d2 fix(templates): 🐛 fix issue with Dergdown templates not being closed properly 2025-02-24 22:42:49 +01:00
91500ccfe5 fix(search): 🐛 fix lack of order_by selectability 2025-02-24 22:41:39 +01:00
77b3f858d1 chore: small chore changes
Some checks reported warnings
/ phplint (push) Has been cancelled
2025-02-13 10:46:09 +01:00
9870967093 feat(toc): add folding TOC elements for more compact TOC table 2025-02-13 10:45:32 +01:00
da25a786d1 fix: 🩹 small fixes for OpenGraph 2025-02-13 10:44:33 +01:00
0f2634749d fix: 🐛 fix handling of URLS (now uses config file prefix) 2025-02-13 10:43:06 +01:00
c60b052951 style: 🗑️ removing unused variable 2025-02-13 10:40:30 +01:00
f560bdc3f0 build: ⬆️ bump up to PHP8.3 2025-02-13 10:39:56 +01:00
85fe57ea0c feat(database): add database table prefixing 2025-02-13 10:39:04 +01:00
3e1e61bf4a chore: 📦 update HTMX 2025-02-03 10:48:25 +01:00
05996b2ebf feat(search): add inline search field 2025-02-03 10:47:32 +01:00
de1f1446a3 feat: use FlexBox to properly arrange the sidebar layouts
Some checks reported warnings
/ phplint (push) Has been cancelled
2025-01-27 10:24:41 +01:00
771e9a2ec8 feat: added TOC :D 2025-01-16 23:28:37 +01:00
31150b9b12 style(css): change out CSS classes according to stylesheet update 2025-01-06 22:46:28 +01:00
d609265862 style(css): use of new CSS features (@import and nested CSS) 2025-01-06 22:45:37 +01:00
95e3fc0b00 feat: add function to get raw Database output, for debugging 2025-01-06 22:45:01 +01:00
fc0d38e118 feat: add small search hooks (to be improved later, presumably) 2025-01-06 22:44:09 +01:00
2fe4d20187 fix: adjust default gallery search to new search format 2025-01-06 22:40:15 +01:00
7c8d0191d2 feat(search): add actual searching 2025-01-06 22:39:28 +01:00
02054d418d fix: add guard against whitespace-only entries from messing up tag search 2025-01-06 22:33:44 +01:00
c877c8ce31 fix(search): add FULLTEXT indices to actually be able to search these 2025-01-06 22:33:01 +01:00
cd5b6f2942 fix: 📌 pin PHP version to that used by actual servers 2025-01-06 22:32:23 +01:00
9de4838081 feat(templates): 🎨 adjust templates to fit the new rendering data format 2024-12-09 10:53:05 +01:00
9a5b9e609e feat(search): Add search_tags//search_results post rendering data 2024-12-09 10:52:39 +01:00
bf2486caa4 feat: 🎨 clean up function naming a little 2024-12-09 10:51:02 +01:00
22c953793c feat(search): add proper DB tag searching 2024-12-09 10:50:36 +01:00
f9cfb81079 feature(posts): Added template parameter 2024-11-19 08:59:58 +01:00
aabe0c72d5 feature(post rendering): ability to add additional argument contents 2024-11-18 23:00:24 +01:00
626a4bbaf6 feature(security): add can-upload check 2024-11-18 22:48:28 +01:00
2e012b4fd5 feature(api): add more API functions such as upload 2024-11-18 22:48:13 +01:00
c9808f90f8 feat: reworked the template structure 2024-08-29 20:40:36 +02:00
c607d57221 feat(auth,api): begin work on API input 2024-08-24 15:47:23 +02:00
0dcf36052e ... unsure
Some checks reported warnings
/ phplint (push) Has been cancelled
2024-08-24 13:09:28 +02:00
76ca7b9c32 everything(everything): simply saving work 2024-08-15 22:53:55 +02:00
0f2761cd61 feat: preliminary work on revised Post system
Some checks reported warnings
/ phplint (push) Has been cancelled
2024-02-12 23:20:43 +01:00
b8e449006b style: add HTMX 2024-02-12 23:07:58 +01:00
a2c6842b71 feat: add new Post wrapper and Blog overview page 2024-01-25 15:40:32 +01:00
b420a6eafa feat: simplify README handling 2024-01-22 17:05:11 +01:00
72 changed files with 4657 additions and 992 deletions

2
.gitignore vendored
View file

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

0
Rakefile Normal file
View file

View file

@ -2,9 +2,9 @@ FROM composer
WORKDIR /app
COPY www/composer.* .
RUN composer install
COPY www/vendor/* vendor/
FROM php:apache
FROM php:8.3-apache
WORKDIR /var/www/html
COPY --from=0 /app/ ./
@ -15,6 +15,7 @@ RUN a2enmod headers
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
RUN mkdir raw
RUN chmod a+wr raw
COPY www/ .
RUN chmod -R a+rw $(ls -I vendor)
RUN chmod -R a+rw $(ls -I vendor)

View file

@ -24,7 +24,7 @@ services:
- ../.git
- mysql_schema.sql
volumes:
- website_datavolume:/var/www/html/raw
- ../.docker_vols/web:/var/www/html/raw
mysql:
build:
@ -42,7 +42,4 @@ services:
- path: mysql_schema.sql
action: rebuild
volumes:
- sqlvolume:/var/lib/mysql
volumes:
sqlvolume: {}
website_datavolume: {}
- ../.docker_vols/sql:/var/lib/mysql

View file

@ -3,146 +3,106 @@ CREATE DATABASE dragon_fire;
USE dragon_fire;
CREATE TABLE posts (
-- DROP TABLE posts;
-- DROP TABLE path_access_counts;
-- DROP TABLE path_errcodes;
-- DROP TABLE feed_cache;
CREATE TABLE dev_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_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_access_count INTEGER DEFAULT 0,
post_view_count INTEGER DEFAULT 0,
post_metadata JSON NOT NULL,
post_title VARCHAR(1024),
post_tags VARCHAR(1024),
post_brief TEXT(2048),
post_metadata JSON DEFAULT NULL,
post_settings_cache JSON DEFAULT NULL,
post_content MEDIUMTEXT,
post_counters JSON DEFAULT NULL,
PRIMARY KEY(post_id),
CONSTRAINT unique_post UNIQUE (host, post_path),
CONSTRAINT unique_post UNIQUE (post_path),
INDEX(host, post_path),
INDEX(post_path),
INDEX(post_path_depth, post_path),
INDEX(post_create_time),
INDEX(post_update_time)
INDEX(post_created_at),
INDEX(post_updated_at),
FULLTEXT(post_path),
FULLTEXT(post_tags),
FULLTEXT(post_title),
FULLTEXT(post_brief)
);
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,
CREATE TABLE dev_post_markdown (
post_id INTEGER,
PRIMARY KEY(access_time, host, post_path, agent, referrer)
post_markdown TEXT,
PRIMARY KEY(post_id),
FOREIGN KEY(post_id) REFERENCES dev_posts(post_id)
ON DELETE CASCADE,
FULLTEXT(post_markdown)
);
CREATE TABLE feed_cache (
host VARCHAR(64) NOT NULL,
search_path VARCHAR(255),
export_type VARCHAR(255),
CREATE TABLE dev_yaps (
yap_id INTEGER AUTO_INCREMENT,
PRIMARY KEY(yap_id),
feed_created_on DATETIME DEFAULT CURRENT_TIMESTAMP,
post_path VARCHAR(255) NOT NULL,
yap_category VARCHAR(32) NOT NULL,
yap_tag VARCHAR(40) NOT NULL,
feed_content MEDIUMTEXT,
-- Uniqueness detection based on associated path, category and tag
yap_hash CHAR(32) AS (MD5(CONCAT(post_path, yap_category, yap_tag))),
CONSTRAINT YAPS_UNIQUE UNIQUE(yap_hash),
PRIMARY KEY(host, search_path, export_type)
yap_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
yap_metadata JSON DEFAULT NULL,
yap_text TEXT,
-- Make it possible to look up changes from e.g. a commit hash inexpensively
INDEX(yap_tag),
-- Make it possible to look up specific post feeds efficiently (e.g. changelog)
INDEX(post_path, yap_category, yap_created_at),
-- Make it possible to globally look up specific feeds efficiently
INDEX(yap_category, yap_created_at),
-- And just in general, make searching feeds in a timeframe efficient
INDEX(yap_created_at),
FULLTEXT(yap_text)
);
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
CREATE TABLE analytics_summations (
time_bucket DATETIME NOT NULL,
metric VARCHAR(16) NOT NULL,
tags JSON NOT NULL,
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!
metric_value DOUBLE PRECISION DEFAULT 0,
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!
tags_md5 CHAR(32) AS (MD5(tags)),
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!
INDEX(time_bucket, metric),
CONSTRAINT unique_analytic UNIQUE(time_bucket, metric, tags_md5)
);
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!
CREATE TABLE analytics_events (
event_time DATETIME NOT NULL,
metric VARCHAR(64) NOT NULL DEFAULT 'error_msg',
tags JSON NOT NULL,
Sorry for this. She GRABS A LOT
event_text TEXT,
----
## 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.
'
INDEX(event_time)
);

View file

@ -0,0 +1,19 @@
{
"folders": [
{
"path": "."
},
{
"path": "../dragon_fire_content"
}
],
"settings": {
"conventionalCommits.scopes": [
"search",
"templates",
"css",
"database",
"analytics"
]
}
}

View file

@ -3,8 +3,8 @@ AddType text/plain .md
AddType text/plain .atom
AddType text/plain .rss
# php_value upload_max_filesize 40M
# php_value post_max_size 42M
php_value upload_max_filesize 40M
php_value post_max_size 42M
RewriteEngine On
RewriteBase /
@ -14,21 +14,24 @@ RewriteRule ^.*\.(flv|gif|ico|jpg|jpeg|mp4|mpeg|png|svg|swf|webp)$ raw/%{HTTP_HO
RewriteRule ^/?raw/(.*)$ raw/%{HTTP_HOST}/$1 [L,END]
RewriteEngine On
RewriteCond %{HTTPS} !on
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,END]
# RewriteCond %{HTTPS} !on
# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,END]
RewriteCond %{REQUEST_URI} !^/?(static|raw|robots\.txt).*
RewriteRule (.*) router.php
RewriteCond %{REQUEST_URI} !^/api/
RewriteCond %{REQUEST_URI} /\w+$
RewriteRule (.*) $1/ [R=301,L,END]
RewriteCond %{REQUEST_URI} !^/?(src/dbtest\.php|static|raw|robots\.txt).*
RewriteRule (.*) src/router.php
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"
Header set Cache-Control "max-age=60, public"
</filesMatch>
<filesMatch ".(js)$">
Header set Cache-Control "max-age=315360, public"
</filesMatch>
Header set Cache-Control "max-age=60, public"
</filesMatch>

335
www/composer.lock generated
View file

@ -8,16 +8,16 @@
"packages": [
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "f41715465d65213d644d3141a6a93081be5d3549"
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549",
"reference": "f41715465d65213d644d3141a6a93081be5d3549",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
@ -77,9 +77,9 @@
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2"
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
},
"time": "2022-10-27T11:44:00+00:00"
"time": "2024-07-08T12:26:09+00:00"
},
{
"name": "erusev/parsedown",
@ -184,33 +184,33 @@
},
{
"name": "laminas/laminas-escaper",
"version": "2.13.0",
"version": "2.15.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-escaper.git",
"reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba"
"reference": "c612b0488ae486284c39885efca494c180f16351"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/af459883f4018d0f8a0c69c7a209daef3bf973ba",
"reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/c612b0488ae486284c39885efca494c180f16351",
"reference": "c612b0488ae486284c39885efca494c180f16351",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-mbstring": "*",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0"
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"conflict": {
"zendframework/zend-escaper": "*"
},
"require-dev": {
"infection/infection": "^0.27.0",
"laminas/laminas-coding-standard": "~2.5.0",
"infection/infection": "^0.27.11",
"laminas/laminas-coding-standard": "~3.0.1",
"maglnet/composer-require-checker": "^3.8.0",
"phpunit/phpunit": "^9.6.7",
"psalm/plugin-phpunit": "^0.18.4",
"vimeo/psalm": "^5.9"
"phpunit/phpunit": "^9.6.22",
"psalm/plugin-phpunit": "^0.19.0",
"vimeo/psalm": "^5.26.1"
},
"type": "library",
"autoload": {
@ -242,7 +242,7 @@
"type": "community_bridge"
}
],
"time": "2023-10-10T08:35:13+00:00"
"time": "2024-12-17T19:39:54+00:00"
},
{
"name": "laminas/laminas-feed",
@ -519,16 +519,16 @@
},
{
"name": "league/commonmark",
"version": "2.4.1",
"version": "2.6.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5"
"reference": "d990688c91cedfb69753ffc2512727ec646df2ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
"reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad",
"reference": "d990688c91cedfb69753ffc2512727ec646df2ad",
"shasum": ""
},
"require": {
@ -541,8 +541,8 @@
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.30.0",
"commonmark/commonmark.js": "0.30.0",
"commonmark/cmark": "0.31.1",
"commonmark/commonmark.js": "0.31.1",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
@ -551,10 +551,11 @@
"michelf/php-markdown": "^1.4 || ^2.0",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0",
"symfony/finder": "^5.3 | ^6.0 | ^7.0",
"symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0"
},
@ -564,7 +565,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.5-dev"
"dev-main": "2.7-dev"
}
},
"autoload": {
@ -621,7 +622,7 @@
"type": "tidelift"
}
],
"time": "2023-08-30T16:55:00+00:00"
"time": "2024-12-29T14:10:59+00:00"
},
{
"name": "league/config",
@ -707,31 +708,31 @@
},
{
"name": "nette/schema",
"version": "v1.2.5",
"version": "v1.3.2",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "0462f0166e823aad657c9224d0f849ecac1ba10a"
"reference": "da801d52f0354f70a638673c4a0f04e16529431d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a",
"reference": "0462f0166e823aad657c9224d0f849ecac1ba10a",
"url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d",
"shasum": ""
},
"require": {
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0",
"php": "7.1 - 8.3"
"nette/utils": "^4.0",
"php": "8.1 - 8.4"
},
"require-dev": {
"nette/tester": "^2.3 || ^2.4",
"nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^1.0",
"tracy/tracy": "^2.7"
"tracy/tracy": "^2.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
"dev-master": "1.3-dev"
}
},
"autoload": {
@ -763,26 +764,26 @@
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.2.5"
"source": "https://github.com/nette/schema/tree/v1.3.2"
},
"time": "2023-10-05T20:37:59+00:00"
"time": "2024-10-06T23:10:23+00:00"
},
{
"name": "nette/utils",
"version": "v4.0.3",
"version": "v4.0.5",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "a9d127dd6a203ce6d255b2e2db49759f7506e015"
"reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/a9d127dd6a203ce6d255b2e2db49759f7506e015",
"reference": "a9d127dd6a203ce6d255b2e2db49759f7506e015",
"url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
"reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96",
"shasum": ""
},
"require": {
"php": ">=8.0 <8.4"
"php": "8.0 - 8.4"
},
"conflict": {
"nette/finder": "<3",
@ -849,9 +850,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.3"
"source": "https://github.com/nette/utils/tree/v4.0.5"
},
"time": "2023-10-29T21:02:13+00:00"
"time": "2024-08-07T15:39:19+00:00"
},
{
"name": "psr/event-dispatcher",
@ -983,21 +984,21 @@
},
{
"name": "spatie/yaml-front-matter",
"version": "2.0.8",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/yaml-front-matter.git",
"reference": "f2f1f749a405fafc9d6337067c92c062d51a581c"
"reference": "5d0009289dd19a23e5f6cbb72c959a9fc1881e32"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/yaml-front-matter/zipball/f2f1f749a405fafc9d6337067c92c062d51a581c",
"reference": "f2f1f749a405fafc9d6337067c92c062d51a581c",
"url": "https://api.github.com/repos/spatie/yaml-front-matter/zipball/5d0009289dd19a23e5f6cbb72c959a9fc1881e32",
"reference": "5d0009289dd19a23e5f6cbb72c959a9fc1881e32",
"shasum": ""
},
"require": {
"php": "^7.0|^8.0",
"symfony/yaml": "^3.0|^4.0|^5.0|^6.0|^7.0"
"php": "^8.0",
"symfony/yaml": "^6.0|^7.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
@ -1029,7 +1030,7 @@
"yaml"
],
"support": {
"source": "https://github.com/spatie/yaml-front-matter/tree/2.0.8"
"source": "https://github.com/spatie/yaml-front-matter/tree/2.1.0"
},
"funding": [
{
@ -1041,20 +1042,20 @@
"type": "github"
}
],
"time": "2023-12-04T10:02:52+00:00"
"time": "2024-12-02T08:40:45+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.4.0",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"shasum": ""
},
"require": {
@ -1062,12 +1063,12 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.4-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
}
},
"autoload": {
@ -1092,7 +1093,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
},
"funding": [
{
@ -1108,24 +1109,24 @@
"type": "tidelift"
}
],
"time": "2023-05-23T14:45:45+00:00"
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.28.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
@ -1135,12 +1136,9 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1174,7 +1172,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
@ -1190,24 +1188,24 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.28.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "42292d99c55abe617799667f454222c54c60e229"
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
"reference": "42292d99c55abe617799667f454222c54c60e229",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
@ -1217,12 +1215,9 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1257,7 +1252,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
@ -1273,33 +1268,30 @@
"type": "tidelift"
}
],
"time": "2023-07-28T09:04:16+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.28.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1340,7 +1332,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
},
"funding": [
{
@ -1356,24 +1348,101 @@
"type": "tidelift"
}
],
"time": "2023-01-26T09:26:14+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/yaml",
"version": "v7.0.0",
"name": "symfony/polyfill-php81",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "0055b230c408428b9b5cde7c55659555be5c0278"
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/0055b230c408428b9b5cde7c55659555be5c0278",
"reference": "0055b230c408428b9b5cde7c55659555be5c0278",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/yaml",
"version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "ac238f173df0c9c1120f862d0f599e17535a87ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec",
"reference": "ac238f173df0c9c1120f862d0f599e17535a87ec",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
@ -1411,7 +1480,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.0.0"
"source": "https://github.com/symfony/yaml/tree/v7.2.3"
},
"funding": [
{
@ -1427,28 +1496,29 @@
"type": "tidelift"
}
],
"time": "2023-11-07T10:26:03+00:00"
"time": "2025-01-07T12:55:42+00:00"
},
{
"name": "twig/markdown-extra",
"version": "v3.8.0",
"version": "v3.19.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/markdown-extra.git",
"reference": "b6e4954ab60030233df5d293886b5404558daac8"
"reference": "6c464fc3e016ada9f17be4511daf2576ba4085c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/b6e4954ab60030233df5d293886b5404558daac8",
"reference": "b6e4954ab60030233df5d293886b5404558daac8",
"url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/6c464fc3e016ada9f17be4511daf2576ba4085c5",
"reference": "6c464fc3e016ada9f17be4511daf2576ba4085c5",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"twig/twig": "^3.0"
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"twig/twig": "^3.13|^4.0"
},
"require-dev": {
"erusev/parsedown": "^1.7",
"erusev/parsedown": "dev-master as 1.x-dev",
"league/commonmark": "^1.0|^2.0",
"league/html-to-markdown": "^4.8|^5.0",
"michelf/php-markdown": "^1.8|^2.0",
@ -1456,6 +1526,9 @@
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Twig\\Extra\\Markdown\\": ""
},
@ -1483,7 +1556,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/markdown-extra/tree/v3.8.0"
"source": "https://github.com/twigphp/markdown-extra/tree/v3.19.0"
},
"funding": [
{
@ -1495,34 +1568,42 @@
"type": "tidelift"
}
],
"time": "2023-11-21T14:02:01+00:00"
"time": "2025-01-19T15:54:05+00:00"
},
{
"name": "twig/twig",
"version": "v3.8.0",
"version": "v3.19.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
"reference": "d4f8c2b86374f08efc859323dbcd95c590f7124e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/d4f8c2b86374f08efc859323dbcd95c590f7124e",
"reference": "d4f8c2b86374f08efc859323dbcd95c590f7124e",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php80": "^1.22"
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
@ -1555,7 +1636,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.8.0"
"source": "https://github.com/twigphp/Twig/tree/v3.19.0"
},
"funding": [
{
@ -1567,7 +1648,7 @@
"type": "tidelift"
}
],
"time": "2023-11-21T18:54:41+00:00"
"time": "2025-01-29T07:06:14+00:00"
}
],
"packages-dev": [],

View file

@ -1,39 +0,0 @@
<?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,11 +0,0 @@
<?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,226 +0,0 @@
<?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);
}
}
?>

View file

@ -1,2 +1,3 @@
*.json
*.yml
api_admin_key

View file

@ -0,0 +1,20 @@
<?php
interface AnalyticsInterface {
public function get_current_timestamp();
public function increment_counter($tags, $counter, $value = 1, $timestamp = null);
public function log_path_access(
$path,
$agent,
$referrer, $runtime, $status = 200);
public function log_path_errcode(
$path,
$code, $message);
public function pop_analytics($delete = true);
}
?>

View file

@ -0,0 +1,109 @@
<?php
function sanitize_post_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 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];
}
interface PostdataInterface {
/* Postdata format:
*
* The Postdata array is a simple intermediate data format
* for the Post content and metadata.
* It is slightly abstracted from the SQL format itself but will
* only reformat keys, *not* do any alteration of the data itself.
*
* Any supported fields will be integrated into the database.
* Other fields will be saved in a JSON structure, and will
* be restored afterward.
*
* The following fields are mandatory for *writing*
* - path: String, must be sanitized to consist of just alphanumeric
* characters, `_-./`
* used to identify the post itself
*
* The following fields may be returned by the database:
* - id
* - created_at
* - updated_at
* - view_count
*
* The following fields may be supported by the database:
* - markdown: String, markdown of the post. May be
* stored separately and won't be returned by default!
* - type: String, defining the type of the post
* - title: String, self-explanatory
* - tags: Array of strings
* - settings: Hash, recursively merged settings (calculated by DB!)
*
* The following fields are *recommended*, but nothing more:
* - icon: String, optionally defining
*/
public function stub_postdata($path);
public function stub_postdata_tree($path);
public function set_postdata($data);
public function set_post_markdown($id, $markdown);
public function increment_post_counter($path, $counter = 'views', $value = 1);
public function get_postdata($path);
// Returns a key-value pair of child paths => child data
public function get_post_children($path,
$limit = 50, $depth_start = 1, $depth_end = 1,
$order_by = 'path');
public function get_post_markdown($id);
// Returns an array of PostData information
// based on various search parameters.
//
// search_options can either be:
// - An Array
// - Or a String
//
// In case of it being an Array, it may include
// the keys:
// - "query" (which will be processed similar
// to how $search_options will be processed),
// - "text", which is searched for in text fields
// (title, brief, fulltext),
// - "tags", which is matched IN BINARY MODE against
// the post tags
// - "path", which is used as filter
// - "order_by": determines which column to order by. NULL
// will order by FULLTEXT match scores
// - "limit" and "offset", self-explanatory
public function search_posts($search_options);
}
?>

View file

@ -0,0 +1,335 @@
<?php
require_once 'analytics_interface.php';
class MySQLAnalyticsHandler
implements AnalyticsInterface {
private $sql_connection;
private $hostname;
function __construct($sql_connection, $hostname) {
$this->sql_connection = $sql_connection;
$this->hostname = $hostname;
}
private function _exec($qery, $argtypes = '', ...$args) {
$stmt = $this->sql_connection->prepare($qery);
if($argtypes != ""){
$stmt->bind_param($argtypes, ...$args);
}
$stmt->execute();
return $stmt->get_result();
}
public function get_current_timestamp() {
return (int)($this->_exec(
"SELECT unix_timestamp(NOW()) AS ctime"
)->fetch_assoc()['ctime']);
}
public function increment_counter($tags, $counter, $value = 1, $timestamp = null) {
$timestamp ??= $this->get_current_timestamp();
$qry =
"INSERT INTO analytics_summations
(
time_bucket,
metric,
tags,
metric_value
)
VALUES
(
from_unixtime(floor(? / 300) * 300),
?,
?,
?
) AS new
ON DUPLICATE KEY
UPDATE metric_value=analytics_summations.metric_value + new.metric_value;
";
$this->_exec($qry,
"dssd",
$timestamp, $counter, json_encode($tags), $value);
}
public function insert_event($event_tags, $event_text) {
$qry =
"INSERT INTO analytics_events (
event_time, tags, event_text
)
VALUES (NOW(), ?, ?)";
$this->_exec($qry, "ss",
json_encode($event_tags), $event_text);
}
public function log_path_access(
$path,
$agent,
$referrer,
$time, $status = 200) {
if(strlen($path) == 0) {
$path = '/';
}
$this->increment_counter([
'host' => $this->hostname,
'path' => $path,
'agent' => $agent,
'referrer' => $referrer,
'status' => $status
], 'access_sum');
$this->increment_counter([
'host' => $this->hostname,
'path' => $path
], 'runtime', $time);
}
public function log_path_errcode(
$path, $code, $message) {
$this->insert_event([
'host' => $this->hostname,
'path' => $path,
'code' => $code
], $message);
}
public function generate_lp_line($table, $tags, $values, $timestamp) {
$out_str = $table;
$line_tags = [];
foreach($tags AS $tag_key => $tag_value) {
if(!preg_match('/^[\w_]+$/', $tag_key)) {
throw new Exception('Invalid line tag key (' . $tag_key . ')!');
}
$tag_value = preg_replace('/([,=\s])/', '\\\\$0', $tag_value);
$line_tags []= $tag_key . '=' . $tag_value;
}
$line_values = [];
foreach($values AS $tag_key => $tag_value) {
if(!preg_match('/^[\w_]+$/', $tag_key)) {
throw new Exception('Invalid line value key (' . $tag_key . ')!');
}
if(gettype($tag_value) == 'string') {
$tag_value = preg_replace('/(["\])/', '\\\\$0', $tag_value);
$tag_value = preg_replace('/\n/', '\\\\n', $tag_value);
$tag_value = '"' . $tag_value . '"';
}
elseif (gettype($tag_value) == 'integer') {
$tag_value = $tag_value . 'i';
}
$line_values []= $tag_key . '=' . $tag_value;
}
return $table
. ',' . implode(',', $line_tags)
. ' ' . implode(',', $line_values)
. ' ' . $timestamp;
}
public function pop_analytics($delete = true) {
$this->sql_connection->begin_transaction();
try {
$barrier_time = $this->_exec("SELECT NOW() - INTERVAL 6 MINUTE AS ctime")->fetch_assoc()['ctime'];
$result = $this->_exec("
SELECT *
FROM analytics_summations
WHERE time_bucket < ?
ORDER BY metric, time_bucket DESC", "s", $barrier_time);
$data_category = "access_metrics";
$row = $result->fetch_assoc();
$out_str = '';
while(isset($row)) {
$row_tags = json_decode($row['tags']);
$row_value = $row['metric_value'];
$row_metric = $row['metric'];
$out_str .= $this->generate_lp_line($data_category, $row_tags, [
$row_metric => $row_value
], strtotime($row['time_bucket']) . "000000000") . "\n";
$row = $result->fetch_assoc();
}
$result = $this->_exec("
SELECT *
FROM analytics_events
WHERE event_time < ?
ORDER BY event_time DESC", "s", $barrier_time);
while(isset($row)) {
$row_tags = json_decode($row['tags']);
$row_value = $row['event_text'];
$row_metric = $row['metric'];
$out_str .= $this->generate_lp_line($data_category, $row_tags, [
$row_metric => $row_value
], strtotime($row['time_bucket']) . "000000000") . "\n";
$row = $result->fetch_assoc();
}
if($delete) {
$this->_exec("DELETE FROM analytics_summations WHERE time_bucket <= ?", "s", $barrier_time);
}
$this->sql_connection->commit();
return $out_str;
} catch (\Throwable $th) {
$this->sql_connection->rollback();
throw $th;
}
}
public function pop_analytics_json($delete = true) {
$this->sql_connection->begin_transaction();
try {
$barrier_time = $this->_exec("SELECT NOW() - INTERVAL 6 MINUTE AS ctime")->fetch_assoc()['ctime'];
$out_data = [];
$result = $this->_exec("
SELECT *
FROM analytics_summations
WHERE time_bucket < ?
ORDER BY metric, time_bucket DESC", "s", $barrier_time);
$row = $result->fetch_assoc();
$current_metric_collection = [];
$current_time_bucket_collection = [];
$current_metric = $row['metric'] ?? null;
$current_time_bucket = $row['time_bucket'] ?? null;
while(isset($row)) {
$current_time_bucket_collection[]= [
'tags' => json_decode($row['tags']),
'value' => floatval($row['metric_value'])
];
$row = $result->fetch_assoc();
if(!isset($row)
OR ($row['time_bucket'] != $current_time_bucket)
OR ($row['metric'] != $current_metric)) {
$current_metric_collection []= [
'time' => $current_time_bucket,
'data' => $current_time_bucket_collection
];
$current_time_bucket_collection = [];
$current_time_bucket = $row['time_bucket'] ?? null;
}
if(!isset($row) OR ($row['metric'] != $current_metric)) {
$out_data []= [
'metric' => $current_metric,
'data' => $current_metric_collection
];
$current_metric_collection = [];
$current_metric = $row['metric'] ?? null;
}
}
if($delete) {
$this->_exec("DELETE FROM analytics_summations WHERE time_bucket <= ?", "s", $barrier_time);
}
$this->sql_connection->commit();
return json_encode($out_data);
} catch (\Throwable $th) {
$this->sql_connection->rollback();
throw $th;
}
}
public function pop_analytics_old($delete = true) {
$this->sql_connection->begin_transaction();
$out_data = "";
try {
$barrier_time = $this->_exec("SELECT NOW() - INTERVAL 6 MINUTE AS ctime")->fetch_assoc()['ctime'];
$data = $this->_exec("
SELECT *
FROM analytics_access_sums
WHERE time_bucket < ?
", "s", $barrier_time)->fetch_all(MYSQLI_ASSOC);
$data_prefix="analytics_access_sums";
foreach($data AS $post_data) {
$path = $post_data['request_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['access_sum'];
$out_data .= " " . strtotime($post_data['time_bucket']) . "000000000\n";
}
$data = $this->_exec("
SELECT *
FROM analytics_processing_time_sums
WHERE time_bucket < ?
", "s", $barrier_time)->fetch_all(MYSQLI_ASSOC);
$data_prefix="analytics_processing_time_sums";
foreach($data AS $post_data) {
$path = $post_data['request_path'];
if($path == '') {
$path = '/';
}
$out_data .= $data_prefix . ",host=" . $post_data['host'];
$out_data .= ",path=".$path;
$out_data .= " time_sum=" . $post_data['time_sum'];
$out_data .= " " . strtotime($post_data['time_bucket']) . "000000000\n";
}
if($delete) {
$this->_exec("DELETE FROM analytics_access_sums WHERE time_bucket <= ?", "s", $barrier_time);
$this->_exec("DELETE FROM analytics_processing_time_sums WHERE time_bucket <= ?", "s", $barrier_time);
}
$this->sql_connection->commit();
return $out_data;
} catch (\Throwable $th) {
$this->sql_connection->rollback();
throw $th;
}
}
}
?>

View file

@ -0,0 +1,529 @@
<?php
require_once 'db_interface.php';
require_once 'mysql_taglist_handling.php';
class MySQLHandler
implements PostdataInterface {
CONST SQL_READ_COLUMNS = [
'id', 'path', 'created_at', 'updated_at',
'title', 'view_count', 'brief', 'search_score'];
CONST SQL_WRITE_COLUMNS = ['path', 'title', 'brief'];
CONST SQL_ORDER_BY_OPTIONS = [
'search_score' => 'post_search_score',
'search_score_desc' => 'post_search_score DESC',
'path' => 'post_path',
'path_desc' => 'post_path DESC',
'created_at' => 'post_created_at',
'created_at_desc' => 'post_created_at DESC'
];
private $sql_connection;
private $db_prefix;
public $hostname;
public $debugging;
function __construct($sql_connection, $hostname, $db_prefix) {
$this->sql_connection = $sql_connection;
$this->hostname = $hostname;
$this->db_prefix = $db_prefix;
$this->debugging = false;
}
private function _dbg($message) {
if($this->debugging) {
echo $message;
}
}
private function _exec($qery, $argtypes = '', ...$args) {
$stmt = $this->sql_connection->prepare($qery);
if($argtypes != ""){
$stmt->bind_param($argtypes, ...$args);
}
$stmt->execute();
return $stmt->get_result();
}
private function clear_post_settings_cache($post_path) {
$post_path = sanitize_post_path($post_path);
$this->_exec("
UPDATE {$this->db_prefix}_posts
SET post_settings_cache=NULL
WHERE post_path LIKE ?;
", "s", $post_path . "%");
}
public function stub_postdata($path) {
$post_path = sanitize_post_path($path);
$path_depth = substr_count($post_path, "/");
$qry = "
INSERT INTO {$this->db_prefix}_posts
(post_path, post_path_depth)
VALUES
( ?, ?) AS new
ON DUPLICATE KEY UPDATE post_path=new.post_path;";
$this->_exec($qry, "si",
$post_path,
$path_depth);
}
public function stub_postdata_tree($path) {
$post_path = sanitize_post_path($path);
while(true) {
if($post_path == '/') {
$post_path = '';
}
try {
$this->stub_postdata($post_path);
}
catch(Exception $e) {
}
$post_path = dirname($post_path);
if(strlen($post_path) == 0) {
break;
}
}
}
public function set_postdata($data) {
$data['path'] = sanitize_post_path($data['path']);
$post_path = $data['path'];
unset($data['path']);
$this->stub_postdata_tree($post_path);
$data['title'] ??= basename($post_path);
$post_tags = $data['tags'] ?? [];
array_push($post_tags,
'path:' . $post_path
);
if(isset($data['type'])) {
$post_tags []= 'type:' . $data['type'];
}
$sql_args = [
$post_path,
substr_count($post_path, "/"),
$data['title'],
TagList\create_db_str($post_tags),
$data['brief'] ?? null
];
unset($data['title']);
unset($data['brief']);
$post_markdown = $data['markdown'] ?? null;
unset($data['markdown']);
unset($data['html']);
array_push($sql_args, json_encode($data));
$qry =
"INSERT INTO {$this->db_prefix}_posts
(post_path, post_path_depth,
post_title, post_tags, post_brief,
post_metadata, post_settings_cache)
VALUES
( ?, ?, ?, ?, ?, ?, null) AS new
ON DUPLICATE KEY
UPDATE post_title=new.post_title,
post_tags=new.post_tags,
post_brief=new.post_brief,
post_metadata=new.post_metadata,
post_updated_at=CURRENT_TIMESTAMP;
";
$this->_exec($qry, "sissss", ...$sql_args);
if(isset($post_markdown)) {
$this->set_post_markdown($this->sql_connection->insert_id, $post_markdown);
}
$this->clear_post_settings_cache($post_path);
}
public function set_post_markdown($id, $markdown) {
$qry =
"INSERT INTO {$this->db_prefix}_post_markdown ( post_id, post_markdown )
VALUES (?, ?) AS new
ON DUPLICATE KEY UPDATE post_markdown=new.post_markdown;
";
$this->_exec($qry, "is", $id, $markdown);
}
public function increment_post_counter($path, $counter = 'views', $value = 1) {
$path = sanitize_post_path($path);
$qry =
"UPDATE {$this->db_prefix}_posts
SET post_counters = JSON_SET(COALESCE(post_counters, \"{}\"), ?, COALESCE(JSON_EXTRACT(post_counters, ?), 0) + ?)
WHERE post_path = ?";
$json_path = "$.{$counter}";
$this->_exec($qry, "ssds", $json_path, $json_path, $value, $path);
}
private function get_post_settings($post_path) {
$post_path = sanitize_post_path($post_path);
$this->_dbg("-> gps: getting path " . $post_path . "\n");
$post_settings = $this->_exec("
SELECT post_settings_cache
FROM {$this->db_prefix}_posts
WHERE post_path = ?
", "s", $post_path)->fetch_assoc();
if(!isset($post_settings)) {
$this->_dbg("-> gps: Returning because of no result\n");
return [];
}
if(isset($post_settings['post_settings_cache'])) {
$result = json_decode($post_settings['post_settings_cache'], true);
if($this->debugging) {
echo "-> gps: Returning because of cached result:\n";
echo "--> " . json_encode($result) . "\n";
}
return $result;
}
$parent_settings = [];
if($post_path != "") {
$parent_settings = $this->get_post_settings(dirname($post_path));
}
$post_settings = [];
$post_metadata = $this->_exec("
SELECT post_path, post_metadata
FROM {$this->db_prefix}_posts
WHERE post_path = ?
", "s", $post_path)->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->_dbg("-> gps: Merged post settings are " . json_encode($post_settings) . ", saving...\n");
$this->_exec("
UPDATE {$this->db_prefix}_posts SET post_settings_cache=? WHERE post_path=?
", "ss",
json_encode($post_settings), $post_path);
return $post_settings;
}
private function process_postdata($data) {
if(!isset($data)) {
return null;
}
if(!isset($data['post_path'])) {
echo "ERROR, trying to get a post data package without path!";
die();
}
$outdata = [];
foreach($this::SQL_READ_COLUMNS as $key) {
if(isset($data['post_' . $key])) {
$outdata[$key] = $data['post_' . $key];
}
}
$post_metadata = json_decode($data['post_metadata'] ?? '{}', true);
$post_settings = [];
if(isset($data['post_settings_cache'])) {
$post_settings = json_decode($data['post_settings_cache'], true);
}
else {
$post_settings = $this->get_post_settings($data['post_path']);
}
if(isset($data['post_counters'])) {
$outdata['counters'] = json_decode($data['post_counters'], true);
}
else {
$outdata['counters'] = [];
}
$outdata = array_merge($post_settings, $post_metadata, $outdata);
$outdata['host'] ??= $this->hostname;
return $outdata;
}
public function get_postdata($path) {
$path = sanitize_post_path($path);
$qry = "
SELECT *
FROM {$this->db_prefix}_posts
WHERE post_path = ?;
";
$data = $this->_exec($qry, "s", $path)->fetch_assoc();
return $this->process_postdata($data);
}
public function get_post_children($path,
$limit = 50, $depth_start = 1, $depth_end = 1,
$order_by = 'path') {
$path = sanitize_post_path($path);
$path_depth = substr_count($path, "/");
$allowed_ordering = [
'path' => true,
'path DESC' => true,
'created_at' => true,
'created_at DESC' => true,
'updated_at' => true,
'updated_at DESC' => true
];
if(!isset($allowed_ordering[$order_by])) {
throw new Exception('Children ordering not allowed');
}
$order_by = 'post_' . $order_by;
if($this->debugging) {
echo "-> GPC: Getting children for path " . $path;
}
$qry = "
SELECT *
FROM {$this->db_prefix}_posts
WHERE post_path_depth BETWEEN ? AND ?
AND post_path LIKE ?
ORDER BY " . $order_by .
" LIMIT ?";
$data = $this->_exec($qry, "iisi",
$path_depth + $depth_start, $path_depth + $depth_end,
$path.'/%', $limit
)->fetch_all(MYSQLI_ASSOC);
$outdata = [];
foreach($data AS $post_element) {
$outdata[$post_element['post_path']] =
$this->process_postdata($post_element);
}
return $outdata;
}
public function get_post_markdown($id) {
$qry =
"SELECT post_markdown
FROM {$this->db_prefix}_post_markdown
WHERE post_id = ?
";
$data = $this->_exec($qry, "i", $id)->fetch_assoc();
if(!isset($data)) {
return '';
}
return $data['post_markdown'] ?? '';
}
public function parse_search_query_string($text) {
$element_array = explode(' ', $text);
$return_text = '';
$return_tags = [];
$return_options = [];
foreach($element_array as $element) {
if(strlen($element) == 0)
continue;
if(preg_match('/^(\w+):(.+)$/', $element, $match)) {
if($match[1] == 'tags') {
$return_tags = array_merge($return_tags, explode(',', $match[2]));
} else {
$return_options[$match[1]] = $match[2];
}
} else {
$return_text .= $element . ' ';
}
}
return [
'text' => $return_text,
'tags' => $return_tags,
'options' => $return_options
];
}
public function search_posts($options) {
// Function to perform an arbitrary search across
// the database.
//
// "options" input is a Hash with the following
// possible keys:
// - query: This text will be interpreted
// as a combination of text to search as well as
// tags, order-by requirements, etc.
// - text: This text will be used as unmodified
// input to the FULLTEXT matching
// - tags: This may be either a list or a string of tags
// to use for searching
// - path: Which path to search within
// - order_by: What column (if any) to search by
// - limit: Number of results to return, at most
// - offset: Number of results to skip before returning
if(gettype($options) == 'string') {
$options = [
'query' => $options
];
}
// Arrays to construct the query selection later
$qry_selects = ['posts.*'];
$qry_select_data = [];
$qry_select_types = '';
$qry_wheres = [];
$qry_where_data = [];
$qry_where_types = '';
$options['text'] ??= '';
if(gettype($options['tags'] ?? null) == 'string') {
$options['tags'] = TagList\_str_to_raw_taglist($options['tags']);
} else {
$options['tags'] ??= [];
}
// This code will take a generic user-input string, and will process it
// to see if there are any special options to consider.
//
// These options will always be overridden by the original "options"
// array. Text and Tags will be merged. For the limit, the minimum will
// be chosen.
if(isset($options['query'])) {
$search_options = $this->parse_search_query_string($options['query']);
if(strlen($search_options['text']) > 0) {
$options['text'] ??= '';
$options['text'] .= ' ' . $search_options['text'];
}
$options['tags'] = array_merge($options['tags'], $search_options['tags']);
if(isset($search_options['limit'])) {
$options['limit'] = min($options['limit'], intval($search_options['limit']));
}
if(isset($search_options['offset'])) {
$options['offset'] = intval($options['offset']);
}
$options = array_merge($options, $search_options['options']);
}
// If we have any tags, construct a tag-matching query
if(count($options['tags']) > 0) {
$tag_search_string = TagList\create_db_search($options['tags'])['parameter_string'];
$qry_wheres []= "MATCH(post_tags) AGAINST (? IN BOOLEAN MODE)";
$qry_where_data []= $tag_search_string;
$qry_where_types .= 's';
}
// If we have any text query strings, we get to construct a rather fun, complex
// array of MATCH() AGAINST() text queries.
if(strlen($options['text']) > 0) {
$text_search_scores = [0];
$text_search_wheres = [];
foreach([['title', 15], ['brief', 6], ['markdown', 1]] as $arg) {
$text_search_scores []= "((MATCH(post_" . $arg[0] . ") AGAINST (?)) * " . $arg[1] . ')';
$qry_select_data []= $options['text'];
$qry_select_types .= 's';
$text_search_wheres []= "(MATCH(post_" . $arg[0] . ") AGAINST (?))";
$qry_where_data []= $options['text'];
$qry_where_types .= 's';
}
$qry_selects []= '(' . implode('+', $text_search_scores) . ') AS post_search_score';
$qry_wheres []= '(' . implode(' OR ', $text_search_wheres) . ')';
} else {
$qry_selects []= '0 AS post_search_score';
}
if(isset($options['path']) && strlen($options['path']) > 0) {
$qry_wheres []= "post_path LIKE ?";
$qry_where_data []= $options['path'] . '%';
$qry_where_types .= 's';
}
if(count($qry_wheres) == 0) {
return [];
}
$options['offset'] ??= 0;
$options['limit'] = min($options['limit'] ?? 100, 100);
$options['order_by'] ??= 'search_score_desc';
if(!isset($this::SQL_ORDER_BY_OPTIONS[$options['order_by']])) {
throw new Exception("Incorrect order_by option chosen!");
}
$qry_order_by = $this::SQL_ORDER_BY_OPTIONS[$options['order_by']];
$qry =
"SELECT " . implode(', ', $qry_selects) . "
FROM {$this->db_prefix}_posts AS posts
LEFT JOIN {$this->db_prefix}_post_markdown AS post_markdown
ON posts.post_id = post_markdown.post_id
WHERE " . implode(' and ', $qry_wheres) . "
ORDER BY " . $qry_order_by . "
LIMIT ? OFFSET ?";
$search_results = $this->_exec($qry, $qry_select_types . $qry_where_types . "ii",
...array_merge($qry_select_data, $qry_where_data, [$options['limit'], $options['offset']]))->fetch_all(MYSQLI_ASSOC);
$outdata = [];
foreach($search_results AS $post_element) {
$outdata []=
$this->process_postdata($post_element);
}
return $outdata;
}
}
?>

View file

@ -0,0 +1,81 @@
<?php
namespace TagList;
function escape_entry($tag) {
return preg_replace_callback('/[\WZ]/', function($match) {
return "Z" . ord($match[0]);
}, strtolower($tag));
}
function escape_search_entry($tag) {
preg_match("/^([\+\-]?)([^\*]*)(\*?)$/", $tag, $matches);
if(!isset($matches[1])) {
echo "Problem with tag!";
var_dump($tag);
}
return $matches[1] . escape_entry($matches[2]) . $matches[3];
}
function _str_to_raw_taglist($taglist) {
$split_list = explode(' ', $taglist);
$split_list = array_filter($split_list, function($entry) {
if(strlen($entry) == 0)
return false;
if(preg_match('/^\s*$/', $entry))
return false;
return true;
});
return $split_list;
}
function create_db_str($taglist) {
if(gettype($taglist) == 'string') {
$taglist = _str_to_raw_taglist($taglist);
}
$taglist = array_unique($taglist);
$taglist = array_map(function($val) {
return escape_entry($val);
}, $taglist);
asort($taglist);
$taglist = join(' ', $taglist);
return $taglist;
}
function create_db_search($taglist) {
if(gettype($taglist) == 'string') {
$taglist = _str_to_raw_taglist($taglist);
}
$search_params = [];
$search_modifiers = [];
foreach($taglist as $tag) {
if(preg_match('/^(order|limit):(.*)$/i', $tag, $match)) {
$search_modifiers[$match[1]] = $match[2];
} else {
array_push($search_params, $tag);
}
}
$search_params = array_map(function($val) {
return escape_search_entry($val);
}, $search_params);
asort($search_params);
$search_params = join(' ', $search_params);
return [
'modifiers' => $search_modifiers,
'parameter_string' => $search_params
];
}
?>

View file

@ -0,0 +1,158 @@
<?php
require_once 'db_interface.php';
require_once 'yaps_interface.php';
class MySQL_YapsHandler
implements YapsInterface {
private $debugging;
private $sql_connection;
private $hostname;
private $db_prefix;
function __construct($sql_connection, $hostname, $db_prefix) {
$this->sql_connection = $sql_connection;
$this->hostname = $hostname;
$this->db_prefix = $db_prefix;
$this->debugging = false;
}
private function _exec($qery, $argtypes = '', ...$args) {
$stmt = $this->sql_connection->prepare($qery);
if($argtypes != ""){
$stmt->bind_param($argtypes, ...$args);
}
$stmt->execute();
return $stmt->get_result();
}
public function add_yap($yap_data_ext) {
# A little copy to prevent accidentally mutating
# external data
$yap_data = $yap_data_ext;
if(!isset($yap_data['tag'])) {
throw new Exception('Yap is missing a tag!');
}
if(!isset($yap_data['post'])) {
throw new Exception('Yap is missing a post!');
}
if(!isset($yap_data['category'])) {
throw new Exception('Yap is missing a category!');
}
var_dump($yap_data);
$yap_tag = $yap_data['tag'];
unset($yap_data['tag']);
$yap_post = $yap_data['post'];
unset($yap_data['post']);
$yap_post = sanitize_post_path($yap_post);
$yap_cateogry = $yap_data['category'];
unset($yap_data['category']);
$yap_text = $yap_data['text'] ?? null;
unset($yap_data['text']);
date_default_timezone_set('UTC');
$yap_created_at = $yap_data['created_at'] ?? null;
unset($yap_data['created_at']);
$yap_data = json_encode($yap_data);
$qry = "
INSERT INTO {$this->db_prefix}_yaps
(`post_path`, `yap_category`, `yap_tag`,
`yap_created_at`, `yap_metadata`, `yap_text` )
VALUES ( ?, ?, ?,
COALESCE(?, NOW()), ?, ? ) AS new
ON DUPLICATE KEY
UPDATE
{$this->db_prefix}_yaps.yap_metadata = new.yap_metadata,
{$this->db_prefix}_yaps.yap_text = new.yap_text;
";
$this->_exec($qry,
"ssssss",
$yap_post, $yap_cateogry, $yap_tag,
$yap_created_at, $yap_data, $yap_text);
}
public function get_yaplist($args = []) {
$category = $args['category'] ?? null;
$post = $args['post'] ?? $args['path'] ?? null;
$tag = $args['tag'] ?? null;
$before = $args['before'] ?? null;
$limit = $args['limit'] ?? null;
$qry_where = [];
$qry_where_data = [];
$qry_where_types = '';
if(isset($category)) {
if($category[-1] = '%') {
$qry_where []= 'yap_category LIKE ?';
}
else {
$qry_where []= 'yap_category = ?';
}
$qry_where_data []= $category;
$qry_where_types .= 's';
}
if(isset($post)) {
if($post[-1] = '%') {
$qry_where []= 'post_path LIKE ?';
}
else {
$qry_where []= 'post_path = ?';
}
$qry_where_data []= $post;
$qry_where_types .= 's';
}
if(isset($tag)) {
$qry_where []= 'yap_tag = ?';
$qry_where_data [] = $tag;
$qry_where_types .= 's';
}
if(isset($before)) {
$qry_where []= 'yap_created_at < ?';
$qry_where_data []= $before;
$qry_where_types .= 's';
}
$limit = min(max(intval($limit ?? 30), 0), 30);
$qry_where []= 'TRUE';
$qry = "
SELECT *
FROM {$this->db_prefix}_yaps
WHERE
" . implode(' AND ', $qry_where) .
" ORDER BY yap_created_at DESC
LIMIT ?";
$yaplist = $this->_exec($qry,
$qry_where_types . "i",
...array_merge($qry_where_data, [$limit]))->fetch_all(MYSQLI_ASSOC);
return $yaplist;
}
}
?>

250
www/src/db_handler/post.php Normal file
View file

@ -0,0 +1,250 @@
<?php
class Post implements ArrayAccess {
public $handler;
private $content_html;
private $content_markdown;
public $data;
public $html_data;
public $raw_data;
private $_child_posts;
private $_parent_post;
public static function _generate_404($post_data) {
$post_data ??= [
'id' => -1,
'path' => '/404.md',
'title' => '404 Page',
'metadata' => [
'type' => '404'
],
'markdown' => 'Whoops! The dergen could not quite find that...'
];
return $post_data;
}
public static function deduce_type($path) {
$ext = pathinfo($path, PATHINFO_EXTENSION);
if(preg_match("/\.(\w+)\.md$/", $path, $ext_match)) {
$ext = $ext_match[1];
}
$ext_mapping = [
'' => 'directory',
'md' => 'text/markdown',
'png' => 'image',
'jpg' => 'image',
'jpeg' => 'image'
];
return $ext_mapping[$ext] ?? '?';
}
public static function deduce_media_url($path) {
if(preg_match("/^(.*\.\w+)\.md$/", $path, $ext_match)) {
return $ext_match[1];
}
return null;
}
public static function deduce_icon($type) {
$icon_mapping = [
'' => 'question',
'text/markdown' => 'markdown',
'markdown' => 'markdown',
'blog' => 'markdown',
'blog_list' => 'rectangle-list',
'directory' => 'folder',
'gallery' => 'images',
'image' => 'image'
];
return $icon_mapping[$type] ?? 'unknown';
}
public static function deduce_template($type) {
$template_mapping = [
'directory' => 'directory',
'gallery' => 'gallery',
'image' => 'image'
];
return $template_mapping[$type] ?? 'vanilla';
}
function __construct($post_handler, $post_data, $site_defaults) {
$this->handler = $post_handler;
$this->content_html = null;
$this->content_markdown = null;
if(!isset($post_data) or !isset($post_data['id'])) {
$post_data = $this->_generate_404($post_data);
}
$data = array_merge($site_defaults, $post_data);
if($data['path'] == '') {
$data['path'] = '/';
$data['title'] ??= 'root';
$data['basename'] ??= 'root';
}
$data['basename'] ??= basename($data['path']);
$data['title'] ??= basename($data['path']);
$data['tags'] ??= [];
$data['type'] ??= self::deduce_type($post_data['path']);
if(!isset($data['url'])) {
$url = $site_defaults['uri_prefix'] . $post_data['path'];
// Adding a trailing slash for "non-filename" paths that
// need a trail to render properly
if(!preg_match('/\.\w+$/', $url)) {
$url .= '/';
}
$data['url'] = $url;
}
$data['icon'] ??= self::deduce_icon($data['type']);
$data['template'] ??= self::deduce_template($data['type']);
$data['media_url'] ??= self::deduce_media_url($data['path']);
$data['media_preview_url'] ??= $data['media_url'];
// TODO: Try to check for thumb image automatically here
$data['preview_image'] ??= $data['media_preview_url'] ??
$data['banners'][0]['src'] ?? null;
$data['brief'] ??= $data['title'];
if($data['type'] == 'gallery') {
$data['search'] ??= [
'path' => $data['path'],
'tags' => [
'+type:image'
]
];
}
$this->data = $data;
}
public function increment_counter($type = 'views', $value = 1) {
$this->handler->increment_post_counter($this->data['path'], $type, $value);
}
public function __get($name) {
if($name == 'html') {
return $this->get_html();
}
if($name == 'markdown') {
return $this->get_markdown();
}
if($name == 'json') {
return $this->to_json();
}
if($name == 'child_posts') {
return $this->get_child_posts();
}
if($name == 'parent') {
return $this->get_parent_post();
}
if(isset($this->data[$name])) {
return $this->data[$name];
}
return null;
}
public function offsetGet($offset) : mixed {
return $this->__get($offset) ?? null;
}
public function offsetExists($offset) : bool {
if(isset($this->data[$offset])) {
return true;
}
return !is_null($this->offsetGet($offset));
}
public function offsetSet($offset, $value) : void {
$this->data[$offset] = $value;
}
public function offsetUnset($offset) : void {
unset($this->data[$offset]);
}
public function get_html() {
$this->content_html ??= $this->handler->render_post($this);
return $this->content_html;
}
public function get_markdown() {
$this->content_markdown ??=
$this->handler->get_markdown_for($this);
return $this->content_markdown;
}
public function get_child_posts(...$search_args) {
if(count($search_args) == 0) {
$this->_child_posts ??=
$this->handler->get_children_for($this);
return $this->_child_posts;
}
else {
return $this->handler->get_children_for($this, ...$search_args);
}
}
public function to_array($options = []) {
$out_data = $this->data;
if(isset($options['markdown'])) {
$out_data['markdown'] = $this->get_markdown();
}
if(isset($options['html'])) {
$out_data['html'] = $this->get_html();
}
if(isset($options['children'])) {
$children = $this->get_child_posts();
$child_arrays = [];
foreach($children AS $child) {
array_push($child_arrays, $child->to_array());
}
$out_data['children'] = $child_arrays;
}
return $out_data;
}
public function to_json($options = []) {
return json_encode($this->to_array($options));
}
public function get_parent_post() {
$parent_path = dirname($this->data['path']);
if($parent_path == '')
return null;
$this->_parent_post ??= $this->handler->get_post($parent_path);
return $this->_parent_post;
}
}
?>

View file

@ -0,0 +1,76 @@
<?php
require_once 'db_interface.php';
require_once 'db_handler/post.php';
class PostHandler {
private $db;
private $posts;
public $markdown_engine;
public $site_defaults;
function __construct($db_adapter) {
$this->db = $db_adapter;
$this->posts = [];
$this->site_defaults = null;
$this->markdown_engine = null;
}
public function get_post($key) {
$key = sanitize_post_path($key);
if(isset($this->posts[$key])) {
return $this->posts[$key];
}
$post_data = $this->db->get_postdata($key);
$post = null;
if(isset($post_data)) {
$post = new Post($this, $post_data, $this->site_defaults);
}
$this->posts[$key] = $post;
return $post;
}
public function get_markdown_for($post) {
return $this->db->get_post_markdown($post->id);
}
public function render_post($post) {
return ($this->markdown_engine)($post);
}
public function increment_post_counter(...$opts) {
$this->db->increment_post_counter(...$opts);
}
public function get_children_for($post, ...$search_opts) {
$child_list = $this->db->get_post_children($post->path, ...$search_opts);
$out_list = [];
foreach($child_list as $child_data) {
array_push($out_list, new Post($this, $child_data, $this->site_defaults));
}
return $out_list;
}
public function search_posts($search_query) {
$search_results = $this->db->search_posts($search_query);
$out_list = [];
foreach($search_results as $search_result) {
array_push($out_list, new Post($this, $search_result, $this->site_defaults));
}
return $out_list;
}
}
?>

View file

@ -0,0 +1,68 @@
<?php
interface YapsInterface {
/* Yaps format:
*
* The yaps are even simpler versions of Postdata,
* as they will have almost no defined metadata.
*
* The purpose of yaps is to provide very lightweight
* additions to existing posts.
*
* The only properly defined fields are the following:
* - Relevant Post ID
* - yap category ("changelog", "comment", "build log", etc.)
* - yap tag/hash (will e.g. use the Git Commit Hash for changelogs)
* - yap creation time
* - yap text
* - other yap data (free-form :D)
*
* There will be little to no post-processing by the
* database, as a lot is somewhat free-form.
* No settings, no caching, etc.
*
* Instead, searching yaps will be a fair bit more
* important, as they act as a lot of the feeds!
* RSS/Atom will run over "changelog" yaps, and changelogs
* will be displayed in most posts when present.
* The system might also serve user comments, as well as
* a posting alternative to social media feeds...
* As well as a few WebMentions comments ^^'
*
* Oh yes, Yaps will always be ordered by time, descending.
* It's feeds :P
* */
/* Adds a yap, or overwrites/updates an existing one
* The yap-data MUST include the following:
* - post (string path!)
* - tag
* - category
*
* And may include:
* - text (which is added to yap_text)
* - created_at (if not set, NOW() is used)
*
* With other fields slapped into yap_metadata
*/
public function add_yap($yap_data);
/* Returns a list of yaps for the given filters.
* - Category must always be set, but may be a "LIKE" wildcard
* - post *may* be set. If set, only yaps for that given post are returned
* - $since, which, if set and a valid timestamp, will return yaps after that time
* - $limit, which should be obvious
*
* This function always returns an array of yaps, chronologically ordered.
*
* The following search args are supported:
* - category: String, matched to the Yap Category. Can have a trailing % for LIKE search
* - post: String, post path to look for. Can have a trailing % for LIKE search
* - tag: Yap tag to look for.
* - before: Datetime string. Shows yaps before the given date
* - limit: Limit the number of returned yaps
*/
public function get_yaplist($args);
}
?>

201
www/src/dbtest.php Normal file
View file

@ -0,0 +1,201 @@
<?php
header("content-type: text/plain; charset=UTF-8; imeanit=yes");
header("X-Content-Type-Options: nosniff");
header('Content-Disposition: inline');
// Reporting E_NOTICE can be good too (to report uninitialized
// variables or catch variable name misspellings ...)
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);
$data_time_start = microtime(true);
require_once '../vendor/autoload.php';
require_once 'db_handler/mysql_handler.php';
require_once 'db_handler/post_handler.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)) {
http_response_code(500);
echo "Not a valid server host (was " . $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;
$db_params = $SITE_CONFIG['db'];
$db_connection = null;
try {
if(false !== getenv('MYSQL_HOST')) {
$db_connection = mysqli_connect(getenv('MYSQL_HOST'),
getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
getenv('MYSQL_PORT'));
}
else {
$db_connection = 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();
}
$db_connection->execute_query("DELETE FROM posts;");
$sql_adapter = new MySQLHandler($db_connection, $SERVER_HOST);
$adapter = new PostHandler($sql_adapter);
$adapter->site_defaults = [];
$sql_adapter->debugging = true;
function test_accounce($title) {
echo "\n\n===========================================
_______ ______ _____ _______
|__ __| ____|/ ____|__ __|
| | | |__ | (___ | | (_)
| | | __| \___ \ | |
| | | |____ ____) | | | _
|_| |______|_____/ |_| (_)
";
echo "==== " . $title . "\n";
echo "===========================================\n";
}
function adapter_fetch($post_path) {
global $db_connection;
global $sql_adapter;
echo "-> Fetching path " . $post_path . "\n";
echo json_encode($db_connection->execute_query("SELECT * FROM posts WHERE post_path=?", [
$post_path
])->fetch_assoc(), JSON_PRETTY_PRINT);
echo "\n-> Adapter output:\n";
echo json_encode($sql_adapter->get_postdata($post_path), JSON_PRETTY_PRINT) . "\n";
}
echo "Starting test...\n";
echo "Trying just a stub...\n";
$sql_adapter->stub_postdata_tree('/testing/stubtest/1/2/3.md');
echo "Stubbed~\n\n";
echo "Getting the stub post...\n";
echo json_encode($sql_adapter->get_postdata('/testing'), JSON_PRETTY_PRINT);
echo "\n\n";
test_accounce("Basic postdata setting");
$sql_adapter->set_postdata([
'path' => '/testing/settest/test.md',
'title' => 'One heck of a test!',
'type' => 'text/markdown',
'tags' => [
'test',
'type:text'
],
'overridetest' => 'metadata'
]);
echo "\nDone!";
adapter_fetch('/testing/settest/test.md');
echo "Done!\n\n";
test_accounce("Setting post markdown...");
$sql_adapter->set_postdata([
'path' => '/testing/markdowntest',
'markdown' => 'Inline markdown test should work...',
'title' => "A Markdown Test",
'brief' => "The dragons explore markdown, sort of properly... Maybe.",
'tags' => ['one', 'two', 'three', 'sexee']
]);
$post = $sql_adapter->get_postdata('/testing/markdowntest');
var_dump($sql_adapter->get_post_markdown($post['id']));
$sql_adapter->set_post_markdown($post['id'],
'
This is one hell of a cute test!
> Just checking in...
{{
template: fragments/blog/card.html
}}
'
);
var_dump($sql_adapter->get_post_markdown($post['id']));
unset($post);
test_accounce("Settings inheritance test...");
echo "Setting on a parent file...\n";
$sql_adapter->set_postdata([
'path' => '/testing/settest',
'settings' => [
'nom' => true,
'type' => 'frame',
'overridetest' => 'settings'
]
]);
echo "\nAnd checking if that held!\n";
adapter_fetch('/testing/settest');
adapter_fetch('/testing/settest/test.md');
test_accounce("Testing getting child posts");
echo json_encode($sql_adapter->get_post_children('/testing'), JSON_PRETTY_PRINT);
echo "\n\n------------------------------------------------------\n";
echo "TEST PHASE: Adapter testing";
echo "\n------------------------------------------------------\n\n";
$post = $adapter->get_post('/testing/markdowntest');
echo "Post path is " . $post->path . "\n";
echo "Post markdown is " . $post->markdown . "\n";
echo $post->to_json();
echo "\n\n";
echo $post->to_json([
'markdown' => true
]);
test_accounce("Fetching child posts");
echo "Root children:\n". json_encode(array_map(function($data) {
return $data->to_array();
}, $adapter->get_post('/')->child_posts), JSON_PRETTY_PRINT);
echo "\n\n";
echo "Root children, extended:\n" . json_encode(array_map(function($data) {
return $data->to_array();
}, $adapter->get_post('/')->get_child_posts(depth_end: 3)), JSON_PRETTY_PRINT);
echo "\n\n";
?>

113
www/src/dergdown.php Normal file
View file

@ -0,0 +1,113 @@
<?php
use Symfony\Component\Yaml\Yaml;
use Highlight\Highlighter;
class Dergdown extends ParsedownExtra
{
protected $highlighter;
protected $dergInsertRenderer;
public function __construct()
{
$this->highlighter = null;
$this->BlockTypes['{'] []= 'DergInsert';
$this->dergInsertRenderer = null;
}
public function setDergRenderer($dergRenderer) {
$this->dergInsertRenderer = $dergRenderer;
}
protected function blockDergInsert($Line, $currentBlock) {
if (preg_match('/^{{\s?(.*)$/', $Line['body'], $match)) {
return array(
'text' => $match[1] ?? ''
);
}
}
protected function blockDergInsertContinue($Line, $Block) {
if(isset($Block['complete'])) {
return;
}
if(preg_match('/(.*)}}/', $Line['body'], $match)) {
$Block['text'] .= "\n" . $match[1];
$Block['complete'] = true;
return $Block;
}
$Block['text'] .= "\n" . $Line['body'];
return $Block;
}
protected function blockDergInsertComplete($Block) {
try {
$parsed_data = Yaml::parse($Block['text']);
}
catch (Exception $ex) {
return array(
'markup' => '
<div class="derg-insert-error">
<h3> Error in a dergen template! </h3>
YAML could not be parsed properly: <br>
<code>
' . $ex->getMessage() . '</code> </div>'
);
}
try {
if(!isset($this->dergInsertRenderer)) {
throw new Exception("No Dergen Renderer was set!");
}
$render_output = $this->dergInsertRenderer->dergRender($parsed_data);
} catch (Exception $ex) {
return array(
'markup' => '
<div class="derg-insert-error">
<h3> Error in a dergen template! </h3>
Rendering engine threw an error: <br>
<code>
' . $ex->getMessage() . '</code> </div>'
);
}
return array(
'markup' => $render_output,
'interrupted' => true
);
}
protected function blockFencedCodeComplete($block)
{
if (! isset($block['element']['text']['attributes'])) {
return $block;
}
if(!isset($this->highlighter)) {
$this->highlighter = new Highlighter();
}
$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;
}
}

17
www/src/fontawesome.php Normal file
View file

@ -0,0 +1,17 @@
<?php
$FONT_AWESOME_ARRAY=[
'bars' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>',
'markdown' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" class="fa-icn" 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" class="fa-icn" 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" class="fa-icn" 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>',
'turn-up' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M350 177.5c3.8-8.8 2-19-4.6-26l-136-144C204.9 2.7 198.6 0 192 0s-12.9 2.7-17.4 7.5l-136 144c-6.6 7-8.4 17.2-4.6 26s12.5 14.5 22 14.5h88l0 192c0 17.7-14.3 32-32 32H32c-17.7 0-32 14.3-32 32v32c0 17.7 14.3 32 32 32l80 0c70.7 0 128-57.3 128-128l0-192h88c9.6 0 18.2-5.7 22-14.5z"/></svg>',
'rectangle-list' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" 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="M64 80c-8.8 0-16 7.2-16 16V416c0 8.8 7.2 16 16 16H512c8.8 0 16-7.2 16-16V96c0-8.8-7.2-16-16-16H64zM0 96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zm96 64a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm104 0c0-13.3 10.7-24 24-24H448c13.3 0 24 10.7 24 24s-10.7 24-24 24H224c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24H448c13.3 0 24 10.7 24 24s-10.7 24-24 24H224c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24H448c13.3 0 24 10.7 24 24s-10.7 24-24 24H224c-13.3 0-24-10.7-24-24zm-72-64a32 32 0 1 1 0-64 32 32 0 1 1 0 64zM96 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>',
'folder-tree' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" 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="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32v96V384c0 35.3 28.7 64 64 64H256V384H64V160H256V96H64V32zM288 192c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4L409.4 9.4c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V192zm0 288c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V352c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4l-13.3-13.3c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V480z"/></svg>',
'folder' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" 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>',
'folder-open' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" 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="M88.7 223.8L0 375.8V96C0 60.7 28.7 32 64 32H181.5c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7H416c35.3 0 64 28.7 64 64v32H144c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224H544c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480H32c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z"/></svg>',
'rss' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" class="fa-icn" 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>',
'magnifying-glass' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>'
];
?>

View file

@ -268,7 +268,7 @@ class MySQLAdapter {
];
$qry = "
SELECT post_path, post_metadata
SELECT *
FROM posts
WHERE MATCH(post_tags) AGAINST (? IN BOOLEAN MODE)
";
@ -386,7 +386,8 @@ class MySQLAdapter {
$post_data ??= ['found' => false];
$post_data['post_path'] = $post_path;
$post_data['parent_path'] = dirname($post_path);
$post_data = $this->_normalize_post_data($post_data);
if(!$post_data['found']) {
@ -403,6 +404,19 @@ class MySQLAdapter {
return $post_data;
}
function get_markdown_for_id($id) {
$qry = "SELECT post_content
FROM posts
WHERE post_id = ? AND host = ?
";
$post_data = $this->_exec($qry, "is", $id,
$this->SITE_CONFIG['HTTP_HOST']
)->fetch_assoc();
return $post_data['post_content'];
}
function get_subposts_by_path($path) {
global $sql;

View file

@ -4,7 +4,10 @@ $data_time_start = microtime(true);
require_once 'vendor/autoload.php';
require_once 'post_adapter.php';
require_once 'db_handler/mysql_handler.php';
require_once 'db_handler/post_handler.php';
require_once 'post.php';
require_once 'fontawesome.php';
require_once 'dergdown.php';
@ -21,7 +24,30 @@ $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);
$db_params = $SITE_CONFIG['db'];
$db_connection = null;
try {
if(false !== getenv('MYSQL_HOST')) {
$db_connection = mysqli_connect(getenv('MYSQL_HOST'),
getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
getenv('MYSQL_PORT'));
}
else {
$db_connection = 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();
}
$sql_adapter = new MySQLHandler($db_connection, $SERVER_HOST);
$adapter = new PostHandler($sql_adapter);
$loader = new \Twig\Loader\FilesystemLoader(['./templates', './user_content']);
@ -40,6 +66,10 @@ function dergdown_to_html($text) {
return $Parsedown->text($text);
}
function post_to_html($post) {
return dergdown_to_html($post->markdown);
}
PostData::$markdown_engine = "post_to_html";
function deduce_user_agent() {
$real_agent=$_SERVER['HTTP_USER_AGENT'];
@ -126,6 +156,25 @@ function render_twig($template, $args = []) {
echo $twig->render($template, $args);
}
function try_render_ajax($SURI) {
global $adapter;
$match = null;
preg_match('/^\/ajax\/([^\/]+)(.*)$/', $SURI, $match);
if(!isset($match)) {
die();
}
$post = $adapter->get_post_by_path($match[2]);
$subposts = $adapter->get_subposts_by_path($match[2]);
echo render_twig('ajax/' . $match[1] . '.html', [
"post" => $post,
"subposts" => $subposts
]);
}
function try_render_post($SURI) {
global $adapter;
@ -160,6 +209,7 @@ function try_render_post($SURI) {
break;
case 'blog':
case 'text/markdown':
echo render_twig('post_types/markdown.html', [
"post" => $post
@ -185,6 +235,30 @@ function try_render_post($SURI) {
]);
break;
case 'blog_list':
if(preg_match('/^(.*[^\/])((?:#.*)?)$/', $SURI, $match)) {
header('Location: ' . $match[1] . '/' . $match[2]);
die();
}
$search_query = $post['post_metadata']['search_tags'] ??
('+type:blog +path:' . $post['post_path'] . '/*');
$search_result = $adapter->perform_post_search($search_query);
$search_result = array_map(function($key) {
$post = new PostData($adapter, $key);
return $post->data;
}, $search_result['results']);
echo render_twig('post_types/blog_list.html', [
"post" => $post,
"subposts" => $adapter->get_subposts_by_path($SURI),
"blog_posts" => $search_result
]);
break;
case 'image':
echo render_twig('post_types/image.html', [
"post" => $post,
@ -233,7 +307,9 @@ function generate_website($SURI) {
} elseif(preg_match('/^\/api\/posts(.*)$/', $SURI, $match)) {
header('Content-Type: application/json');
echo json_encode($adapter->get_post_by_path($match[1]));
$post = new PostData($adapter, $adapter->get_post_by_path($match[1]));
echo $post->to_json(with_markdown: true, with_html: true);
} elseif(preg_match('/^\/api\/subposts(.*)$/', $SURI, $match)) {
@ -247,6 +323,8 @@ function generate_website($SURI) {
header('Content-Type: application/json');
echo json_encode($adapter->perform_post_search($_GET['search_query']));
}
} elseif(preg_match('/^\/ajax\//', $SURI)) {
try_render_ajax($SURI);
} elseif(preg_match('/^\/feed(?:\/(rss|atom)(.*))?$/', $SURI, $match)) {
$feed = $adapter->get_laminas_feed($match[2] ?? '/', $match[1] ?? 'rss');
@ -255,7 +333,7 @@ function generate_website($SURI) {
header('Etag: W/"' . $SURI . '/' . strtotime($feed['feed_ts']) . '"');
echo $feed['feed'];
} elseif(true) {
} else {
try_render_post($SURI);
}
}

35
www/src/router.php Normal file
View file

@ -0,0 +1,35 @@
<?php
$data_time_start = microtime(true);
require_once '../vendor/autoload.php';
require_once 'setup/site_config.php';
require_once 'setup/db.php';
require_once 'fontawesome.php';
require_once 'dergdown.php';
require_once 'setup/twig.php';
$REQUEST_URI = parse_url($_SERVER['REQUEST_URI']);
$REQUEST_PATH = $REQUEST_URI['path'];
parse_str($REQUEST_URI['query'] ?? '', $REQUEST_QUERY);
require_once 'setup/permissions.php';
require_once 'setup/analytics.php';
if(preg_match('/^\/api/', $REQUEST_PATH)) {
require_once 'serve/api.php';
}
elseif(preg_match('/^\/ajax/', $REQUEST_PATH)) {
require_once 'serve/ajax.php';
}
else {
require_once 'serve/post.php';
}
?>

36
www/src/serve/ajax.php Normal file
View file

@ -0,0 +1,36 @@
<?php
$match = null;
preg_match('/^\/ajax\/(.*)$/', $REQUEST_PATH, $match);
if(!isset($match)) {
die();
}
$AJAX_REQUEST_TEMPLATE = $match[1];
$ajax_args = [
];
if(isset($REQUEST_QUERY['page'])) {
$ajax_args['page'] = $adapter->get_post($REQUEST_QUERY['page']);
}
if(isset($REQUEST_QUERY['query'])) {
$REQUEST_QUERY['search'] ??= $REQUEST_QUERY['query'];
}
if(isset($REQUEST_QUERY['search'])) {
$ajax_args['search_query'] = $REQUEST_QUERY['search'];
$ajax_args['search_results'] = $adapter->search_posts($REQUEST_QUERY['search']);
}
$ajax_args['fa'] = $FONT_AWESOME_ARRAY;
$ajax_args['page'] ??= $SITE_CONFIG['site_defaults'];
$ajax_args['post'] ??= $ajax_args['page'];
echo $twig->render($AJAX_REQUEST_TEMPLATE, $ajax_args);
?>

137
www/src/serve/api.php Normal file
View file

@ -0,0 +1,137 @@
<?php
header('Content-Type: application/json');
use Spatie\YamlFrontMatter\YamlFrontMatter;
preg_match('/^\/api\/([^\/]*)(.*)/', $REQUEST_PATH, $match);
$API_FUNCTION = $match[1];
switch($API_FUNCTION) {
case 'posts':
$post = $adapter->get_post($match[2]);
if(!isset($post)) {
echo json_encode([
'found' => false,
'status' => 404
]);
} else {
echo $post->to_json($REQUEST_QUERY);
}
break;
case 'db_post':
echo json_encode($sql_adapter->get_postdata($match[2]));
break;
case 'metrics':
// TODO Change this to a "can access metrics", but whatever :>
if(!access_can_upload()) {
http_response_code(401);
echo json_encode([
'status' => '401 Unauthorized'
]);
die();
}
echo $analytics_adapter->pop_analytics($delete = true);
break;
case 'db_yaps':
echo json_encode($yap_adapter->get_yaplist($_GET));
break;
case 'add_yap':
if( !isset($_POST['path'])
or !isset($_POST['yap_category'])
or !isset($_POST['yap_text'])) {
echo json_encode([
'status' => 'Missing paramters (must POST path, category and a text!)'
]);
die();
}
$yap_data = [
'post' => $_POST['path'],
'category' => $_POST['yap_category'],
'text' => $_POST['yap_text']
];
$yap_data['tag'] = MD5($_POST['path'] . microtime());
$yap_adapter->add_yap($yap_data);
echo json_encode([
'status' => '200 OK',
'yap' => $yap_data
]);
break;
case 'upload':
if(!access_can_upload()) {
http_response_code(401);
echo json_encode([
'status' => '401 Unauthorized'
]);
die();
}
if( !isset($_POST['path']) or
!isset($_FILES['file'])) {
echo json_encode([
'status' => 'Missing paramters (must POST path and file!)'
]);
die();
}
$file_path = sanitize_post_path($_POST['path']);
$physical_file_path = $SITE_CONFIG['upload']['file_path'] . $file_path;
$file_dir = dirname($physical_file_path);
if(!is_dir($file_dir)) {
mkdir($file_dir, recursive: true);
}
move_uploaded_file($_FILES['file']['tmp_name'], $physical_file_path);
$file_ext = pathinfo($file_path, PATHINFO_EXTENSION);
if($file_ext == 'md') {
$is_directory = false;
$original_file_path = $file_path;
if(basename($file_path) == 'README.md') {
$is_directory = true;
$file_path = dirname($file_path);
}
$post_matter = YamlFrontMatter::parse(file_get_contents($physical_file_path));
$post_data = $post_matter->matter();
$post_data['path'] = $file_path;
$post_data['markdown'] = $post_matter->body();
// TODO: This should be moved to an appropriately abstracted prep function
if($is_directory) {
$post_data['base'] ??= $original_file_path;
$post_data['type'] ??= 'directory';
}
$post_data['tags'] ??= [];
$sql_adapter->set_postdata($post_data);
}
break;
}
?>

134
www/src/serve/post.php Normal file
View file

@ -0,0 +1,134 @@
<?php
require_once 'fontawesome.php';
function render_root_template($template, $args = []) {
global $twig;
global $FONT_AWESOME_ARRAY;
global $SITE_CONFIG;
$args['fa'] = $FONT_AWESOME_ARRAY;
$args['page'] ??= $SITE_CONFIG['site_defaults'];
$page = $args['page'];
$page['base'] ??= $page['url'] ?? null;
$args['opengraph'] = [
"site_name" => $page['site_name'] ?? 'Nameless Site',
"title" => $page['title'] ?? 'Titleless',
"url" => $page['url'] ?? $page['path'] ?? 'No URL set',
"description" => $page['brief'] ?? $page['description'] ?? 'No description set',
"image" => $page['preview_image'] ?? $page['banners'][0]
];
$args['banners'] = json_encode($page['banners'] ?? []);
$args['age_gate'] = (!isset($_COOKIE['AgeConfirmed']))
&& isset($SITE_CONFIG['age_gate']);
echo $twig->render($template, $args);
}
function render_pathed_content_template($template, $args = []) {
render_root_template($template, $args);
}
function render_post($post, $args = []) {
$template = $post['template']
?? ($post['markdown'] == '' ? 'directory' : 'vanilla');
if(isset($post['search_tags'])) {
$post['search'] = $post['search_tags'];
}
if(isset($post['search'])) {
$args['search_results'] = $post->handler->search_posts($post['search']);
}
$args['page'] = $post;
render_pathed_content_template(
'render_templates/' . $template . '.html',
$args);
}
if($REQUEST_PATH == '/upload.php') {
render_root_template('upload.html');
die();
}
if($REQUEST_PATH == '/yap.php') {
render_root_template('_dev_add_yap.html');
die();
}
if($REQUEST_PATH == '/search/') {
$search_results = [];
$display_type = 'none';
$search_query = [
'user_input' => $_GET['query'] ?? null,
'user_type' => $_GET['search_type'] ?? 'all',
'types' => [['All', 'all'], ['Images', 'image'], ['Blog Posts', 'blog']]
];
if(isset($_GET['query']) && strlen($_GET['query']) > 0) {
$search_request['query'] = $_GET['query'];
if(isset($_GET['search_type']) && $_GET['search_type'] != 'all') {
$search_request['tags'] []= 'type:' . $_GET['search_type'];
}
$search_results = $adapter->search_posts($search_request);
$type_count = [];
if(count($search_results) > 0) {
foreach($search_results as $result) {
$type_count[$result['type']] ??= 0;
$type_count[$result['type']] += 1;
}
$display_type = array_search(max($type_count), $type_count);
}
} else {
$display_type = 'no_search';
}
$search_page = $SITE_CONFIG['site_defaults'];
$search_page['path'] = '/search';
$search_page['title'] = 'Search';
$search_page['basename'] = 'search';
render_root_template('search.html', [
'search_results' => $search_results,
'page' => $search_page,
'display_type' => $display_type,
'search_query' => $search_query
]);
die();
}
$post = $adapter->get_post($REQUEST_PATH);
if(!isset($post)) {
$analytics_return_status = 404;
$error_page = $SITE_CONFIG['site_defaults'];
$error_page['path'] = '/404';
$error_page['title'] = '404 oh no';
$error_page['basename'] = '404.md';
render_root_template('derg_error.html', [
'page' => $error_page,
'error_code' => '404 Hoard not found!',
'error_description' => "Looks like we couldn't find `" . $REQUEST_PATH . "` for you. Check in later!"
]);
}
else {
$post->increment_counter();
render_post($post);
}
?>

112
www/src/setup/analytics.php Normal file
View file

@ -0,0 +1,112 @@
<?php
$data_time_start = microtime(true);
$analytics_enable_tail = false;
$analytics_post = null;
$analytics_return_status = 200;
$analytics_known_bogus_requests = [
'/\.(git|env|aws|xmlrpc|well-known|svn)/',
'/^\/wp-/',
'/^\/ID3/',
'/^\/config/',
'/^\/web\.config/',
'/^\/storage/',
'/^\/web\/config\.php/',
'/^\/phpinfo\.php/',
'/^\/swagger\.json/',
'/^\/package\.json/',
'/^\/info\.php/',
'/^\/db\.ini/',
'/^\/administrator/'
];
$analytics_request_is_bogus = null;
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 analytics_is_bogus_request() {
global $analytics_request_is_bogus;
global $analytics_known_bogus_requests;
global $REQUEST_PATH;
if(isset($analytics_request_is_bogus)) {
return $analytics_request_is_bogus;
}
foreach($analytics_known_bogus_requests AS $bogus_check) {
if(preg_match($bogus_check, $REQUEST_PATH)) {
$analytics_request_is_bogus = true;
return true;
}
}
$analytics_request_is_bogus = false;
return false;
}
function analytics_is_user() {
return preg_match('/^user/', deduce_user_agent());
}
register_shutdown_function(function() {
$data_end_time = microtime(true);
global $data_time_start;
global $analytics_adapter;
global $REQUEST_PATH;
global $REQUEST_QUERY;
global $analytics_enable_tail;
global $analytics_return_status;
$data_time_end = microtime(true);
$http_referer = 'magic';
if(isset($_SERVER['HTTP_REFERER'])) {
$http_referer = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
}
$referrer = $REQUEST_QUERY['referer'] ?? $REQUEST_QUERY['ref'] ?? $http_referer;
$compute_time = $data_time_end - $data_time_start;
if(analytics_is_bogus_request()) {
$analytics_return_status = 'bogus';
}
$analytics_adapter->log_path_access($REQUEST_PATH,
deduce_user_agent(),
$referrer,
$compute_time, $analytics_return_status);
if($analytics_enable_tail) {
echo "<!-- Total page time was: " . $compute_time . " -->";
}
if(isset($analytics_post)) {
$analytics_post->increment_counter("compute_time", $compute_time);
if(analytics_is_user()) {
$analytics_post->increment_counter("views");
}
}
});
?>

70
www/src/setup/db.php Normal file
View file

@ -0,0 +1,70 @@
<?php
require_once 'db_handler/mysql_handler.php';
require_once 'db_handler/mysql_analytics_handler.php';
require_once 'db_handler/mysql_yaps_handler.php';
require_once 'db_handler/post_handler.php';
$db_params = $SITE_CONFIG['db'];
$db_connection = null;
try {
if(false !== getenv('MYSQL_HOST')) {
$db_connection = mysqli_connect(getenv('MYSQL_HOST'),
getenv('MYSQL_USER'), getenv('MYSQL_PASSWORD'),
getenv('MYSQL_DATABASE'),
getenv('MYSQL_PORT'));
}
else {
$db_connection = 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();
}
$sql_adapter = new MySQLHandler($db_connection,
$SITE_CONFIG['site_defaults']['uri_prefix'],
$db_params['prefix']);
$analytics_adapter = new MySQLAnalyticsHandler($db_connection,
$SITE_CONFIG['site_defaults']['uri_prefix']);
$yap_adapter = new MySQL_YapsHandler($db_connection,
$SITE_CONFIG['site_defaults']['uri_prefix'],
$db_params['prefix']);
$adapter = new PostHandler($sql_adapter);
require_once 'dergdown.php';
require_once 'setup/derg_insert.php';
function dergdown_to_html($post) {
$DergInsert = new DergInsertRenderer($post);
$Parsedown = new Dergdown();
$Parsedown->setDergRenderer($DergInsert);
$markdown = $post->markdown;
if($markdown == '') {
$markdown = '
{{
template: fragments/directory/inline.html
}}
';
}
return $Parsedown->text($markdown);
}
function post_to_html($post) {
return dergdown_to_html($post);
}
$adapter->markdown_engine = "post_to_html";
$adapter->site_defaults = $SITE_CONFIG['site_defaults'];
?>

View file

@ -0,0 +1,46 @@
<?php
class DergInsertRenderer {
protected $twig;
protected $post;
protected $postAdapter;
public function __construct($post) {
global $twig;
global $adapter;
$this->twig = $twig;
$this->post = $post;
$this->postAdapter = $adapter;
}
public function dergRender($renderConfig) {
global $FONT_AWESOME_ARRAY;
if(!isset($renderConfig['template'])) {
throw new Exception("No template type given!");
}
$template = $renderConfig['template'];
$args = [
'post' => $this->post,
'page' => $this->post,
'fa' => $FONT_AWESOME_ARRAY
];
if(isset($renderConfig['post'])) {
$args['post'] = $this->postAdapter->get_post($renderConfig['post']);
}
if(isset($renderConfig['page'])) {
$args['page'] = $this->postAdapter->get_post($renderConfig['page']);
}
if(isset($renderConfig['search'])) {
$args['search_results'] = $this->postAdapter->search_posts($renderConfig['search']);
}
return $this->twig->render($template, $args);
}
}
?>

View file

@ -0,0 +1,26 @@
<?php
$ACCESS_PERMISSIONS = [
"read" => true,
"upload" => false
];
$ACCESS_KEY = $REQUEST_QUERY['ACCESS_KEY']
?? $_POST['ACCESS_KEY']
?? $_COOKIE['ACCESS_KEY']
?? '';
if($ACCESS_KEY == $SITE_CONFIG['ACCESS_KEY']) {
$ACCESS_PERMISSIONS = [
"read" => true,
"upload" => true
];
}
function access_can_upload() {
global $ACCESS_PERMISSIONS;
return $ACCESS_PERMISSIONS['upload'];
}
?>

View file

@ -0,0 +1,19 @@
<?php
use Symfony\Component\Yaml\Yaml;
$SERVER_HOST = $_SERVER['HTTP_HOST'];
if(!preg_match('/^[\w\.\:]+$/', $SERVER_HOST)) {
http_response_code(500);
echo "Not a valid server host (was " . $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;
?>

24
www/src/setup/twig.php Normal file
View file

@ -0,0 +1,24 @@
<?php
$loader = new \Twig\Loader\FilesystemLoader(['../templates']);
$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;
$twig->addRuntimeLoader(new class implements RuntimeLoaderInterface {
public function load($class) {
if (MarkdownRuntime::class === $class) {
return new MarkdownRuntime(new DefaultMarkdown());
}
}
});
?>

View file

@ -3,9 +3,9 @@ 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"
Header set Cache-Control "max-age=60, public"
</filesMatch>
<filesMatch ".(js)$">
Header set Cache-Control "max-age=315360, public"
Header set Cache-Control "max-age=60, public"
</filesMatch>

View file

@ -0,0 +1,81 @@
.article_blop {
position: relative;
overflow: clip;
z-index: 0;
background: var(--bg_2);
margin: 2rem;
padding: 0;
border-radius: 1rem;
box-shadow: 0px 5px 5px 0px #00000040;
transition: 0.3s;
min-height: 10rem;
}
.article_blop_bg {
position: absolute;
left: 0px;
right: 0;
top: 0;
bottom: 0;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
opacity: 0.2;
z-index: -1;
}
.article_blop:hover {
box-shadow: 0px 8px 8px 0px #00000040;
}
.article_blop_content {
padding: 1rem;
padding-top: 5rem;
}
.article_blop_tags {
position: absolute;
top: 0.5rem;
right: 0.2rem;
width: 40%;
height: auto;
list-style: none;
display: flex;
flex-direction: row;
flex-flow: row wrap;
justify-content: right;
}
.article_blop_tags :first-child {
margin-left: auto;
}
.article_blop_tags li {
margin-right: 0.4rem;
margin-bottom: 0.4rem;
padding-left: 0.2rem;
padding-right: 0.2rem;
font-size: 0.8rem;
font-style: normal;
font-weight: bold;
background-color: var(--highlight_1);
border-radius: 0.3rem;
color: var(--bg_2);
}

View file

@ -1,6 +1,8 @@
const BANNER_TIME = 600 * 1000.0
const BANNER_ANIMATION = "opacity 0.8s linear, transform 0.1s linear"
const BANNER_ANIMATION = "opacity 0.8s linear, transform 1s linear"
// const BANNER_ANIMATION = "opacity 0.8s linear"
class BannerHandler {
constructor(banner_container, banner_image, banner_link) {
@ -32,7 +34,7 @@ class BannerHandler {
console.log("Starting tick")
this.bannerUpdateTimer = setInterval(() => { this.updateTick() }, 100);
this.bannerUpdateTimer = setInterval(() => { this.updateTick() }, 1000);
}
stopUpdateTick() {
if(this.bannerUpdateTimer === null) {
@ -61,6 +63,8 @@ class BannerHandler {
}
updateTranslation() {
this.bannerContainerDOM = document.getElementById("main_header")
const bannerTranslateMax = -this.bannerDOM.clientHeight + this.bannerContainerDOM.clientHeight
const bannerPercentageFrom = this.currentBannerData.from || 0;
@ -69,7 +73,7 @@ class BannerHandler {
const bannerPercentage = (bannerPercentageFrom + (bannerPercentageTo - bannerPercentageFrom) * this.currentPhase)
const banner_top = (1-bannerPercentage) * bannerTranslateMax
this.bannerDOM.style.transform = "translateY(" + banner_top + 'px' + ")"
this.bannerDOM.style.transform = "translateZ(0.1px) translateY(" + banner_top + 'px)'
}
fadeOut() {
@ -120,7 +124,7 @@ class BannerHandler {
}
updateTick() {
console.log("tick")
const nextPhase = this.getPhase() % 1;
@ -150,11 +154,17 @@ class BannerHandler {
}
}
var bannerHandler = new BannerHandler(
document.getElementById("main_header"),
document.getElementById("main_banner_img"),
document.getElementById("main_banner_img_link"))
let bannerHandler = null;
bannerHandler.start()
function startBanner() {
if(bannerHandler !== null) {
return;
}
// addEventListener("resize", () => update_banner(banner, banner_container));
bannerHandler = new BannerHandler(
document.getElementById("main_header"),
document.getElementById("main_banner_img"),
document.getElementById("main_banner_img_link"))
bannerHandler.start()
}

View file

@ -1,4 +1,13 @@
@import "styles/age_gate.css";
@import "styles/post_navbar.css";
@import "styles/search.css";
@import "styles/toc.css";
/********************
* GENERAL SETTINGS *
********************
*/
* {
box-sizing: border-box;
@ -6,9 +15,11 @@
padding: 0;
}
svg {
.fa-icn {
fill: var(--text_1);
padding-top: 0.1rem;
height: 1em;
vertical-align: -0.125em;
}
body {
@ -16,13 +27,23 @@ body {
--bg_2: #2c2943;
--bg_3: #3f4148;
--highlight_0: #ee9015b1;
--highlight_1: #ee9015;
--highlight_1a: #ee901540;
--highlight_2: #edd29e;
--text_1: #FFFFFF;
--text_border: #A0A0A080;
color: var(--text_1);
--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));
width: 100vw;
color: var(--text_1);
background: var(--bg_1);
margin: 0px;
@ -30,6 +51,11 @@ body {
min-height: 100vh;
padding-bottom: 4rem;
overflow-y: overlay;
overflow-x: clip;
scrollbar-gutter: stable;
}
@media only screen and (max-width: 600px) {
@ -39,6 +65,34 @@ body {
}
}
@media only screen and (max-width: 1000px) {
#toc_container {
display: none !important;
visibility: hidden !important;
}
#main_content_flexbox:before {
flex: 0.5 0 0 !important;
}
}
/* width */
::-webkit-scrollbar {
width: 3px;
height: 3px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--highlight_1);
}
:link {
color: var(--highlight_1);
font-style: italic;
@ -53,45 +107,6 @@ 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;
@ -150,97 +165,157 @@ a:hover {
margin-right: 2rem;
}
:target {
scroll-margin-top: 6rem;
:target, html, body {
scroll-margin-top: 140px;
}
:target {
outline: 1px solid var(--highlight_1);
}
#main_content_flexbox {
width: 100vw;
display: flex;
justify-content: left;
align-items: flex-start;
padding-left: 1em;
padding-right: 1em;
&:before {
content: '';
flex: 0.2 0 0;
}
}
#main_content_wrapper {
--content-width: min(100vw, calc(20rem + 40vw));
--content-total-margin: calc(calc(100vw - var(--content-width)) / 2);
/*padding: 0rem var(--content-padding) 1rem var(--content-padding);*/
/*width: var(--content-width);*/
flex: 0 1 var(--content-width);
--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-left: calc(var(--content-margin));
//margin-right: var(--content-margin);*/
margin-top: 0px;
min-height: 100%;
background: var(--bg_2);
min-width: 0;
}
#post_file_bar {
position: sticky;
top: 0px;
background: var(--bg_2);
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
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;
height: 100%;
background-image: url('/static/three-dots.svg');
background-repeat: no-repeat;
background-position: center;
opacity: 0;
z-index: 10;
pointer-events: none;
transition: opacity 0.2s;
}
#post_file_path a {
color: var(--text_1);
padding-right: 0.2rem;
.htmx-indicator {
position: relative;
}
.htmx-indicator::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-image: url('/static/three-dots.svg');
background-repeat: no-repeat;
background-position: center;
opacity: 0;
z-index: 10;
pointer-events: none;
transition: opacity 0.2s;
}
.htmx-request::before {
opacity: 0.5;
transition-delay: 0.5s;
}
#main_content_wrapper article {
.folder_listing {
& input {
display: none;
}
& input ~ ul {
display: none;
}
& input:checked ~ ul {
display: block;
}
label > :nth-child(2) {
display: none;
}
input:checked ~ label > :nth-child(1) {
display: none;
}
input:checked ~ label > :nth-child(2) {
display: inline-block;
}
}
label.expandable {
cursor: pointer;
}
input.expandable {
display: none;
}
input.expandable + .expandable {
display: none;
}
input.expandable:checked + .expandable {
display: block;
}
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;
& img {
display: block;
max-width: 100%;
max-height: 70vh;
margin: 1vmin;
margin-right: auto;
margin-left: auto;
}
}
#content_footer {
@ -265,9 +340,8 @@ a:hover {
position: absolute;
bottom: 0px;
width: 100%;
}
#main_footer span {
align-self: flex-end;
width: 100%;
& span {
align-self: flex-end;
width: 100%;
}
}

1
www/static/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -93,111 +93,114 @@ html {
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;
& p,
.modest-p {
font-size: 1rem;
margin-bottom: 1.3rem;
}
}
article ul,
article ol {
padding-left: 2em;
& h1,
.modest-h1,
& h2,
.modest-h2,
& h3,
.modest-h3,
& h4,
.modest-h4 {
margin: 1.5em 0 .3em;
font-weight: inherit;
line-height: 1.42;
padding-left: 1.5rem;
}
& > :first-child {
margin-top: 0.3rem !important;
}
& h1,
.modest-h1 {
margin-top: 0;
font-size: 1.998rem;
}
& h2,
.modest-h2 {
font-size: 1.427rem;
}
& h3,
.modest-h3 {
font-size: 1.299rem;
}
& h4,
.modest-h4 {
font-size: 1.1rem;
}
& h5,
.modest-h5 {
font-size: 1rem;
}
& h6,
.modest-h6 {
font-size: .88rem;
}
& small,
.modest-small {
font-size: .707rem;
}
/* https://github.com/mrmrs/fluidity */
& h1,
& h2,
& 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;
}
}
& ul,
& ol {
padding-left: 2em;
}
& li {
margin-bottom: 1em;
}
}

View file

@ -0,0 +1,42 @@
/****************
* AGE GATE CSS *
****************/
#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;
& 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%);
}
& p {
min-height: 5em;
}
}

View file

@ -0,0 +1,222 @@
.navbar {
position: sticky;
top: 0px;
background: var(--bg_2);
box-shadow: 0px 5px 5px 0px #00000040;
z-index: 5;
& > ._titles {
display: flex;
flex-direction: row;
justify-content: left;
height: 2.3rem;
list-style-type: none;
padding: 0px;
& li {
padding: 0.2rem 0.8rem;
font-style: bold;
font-size: 1.5rem;
background: var(--highlight_1);
}
}
& ._search_field {
position: relative;
width: 1.3em;
height: auto;
margin-right: 0.2em;
overflow-x: clip;
/*transition: width 0.5s ease;*/
& > input {
color: var(--text_1);
height: 1em;
width: 100%;
font-size: 1em;
background-color: transparent;
outline: none;
border: none;
padding-left: 1.3rem;
}
& .fa-icn {
position: absolute;
left: 0.1em;
top: 0.3em;
pointer-events: none;
}
&:focus-within, &:hover, &:has(:focus) {
width: min(20em, 60vw);
min-width: min(20em, 60vw);
& ._search_list {
display: block;
opacity: 1;
}
& input {
border-bottom: 0.15em solid var(--bg_2);
}
}
& ._search_list {
display: none;
opacity: 0;
background: var(--bg_2);
transition: opacity 0.3s ease 0.5s;
transition: visibility 0s none 1s;
width: 100%
max-height: 50vh;
overflow-y: scroll;
padding: 0.5em;
z-index: 6;
& li {
border-bottom: 1px solid var(--highlight_1);
&:hover {
background: var(--highlight_1a);
}
}
}
}
& ._path {
width: 100%;
height: 1.5rem;
padding-left: 0.5rem;
padding-right: 0.6rem;
background: var(--highlight_1);
font-size: 1.1rem;
display: flex;
flex-direction: row;
list-style-type: none;
white-space: nowrap;
& > a {
color: var(--text_1);
padding-right: 0.2rem;
}
}
& ._path_list {
width: 100%;
height: 100%;
margin-right: 1em;
font-style: italic;
background: var(--highlight_1);
font-size: 1.1rem;
display: flex;
flex-direction: row;
list-style-type: none;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
& a {
color: var(--text_1);
padding-right: 0.2rem;
}
}
& > ._expand_label {
cursor: pointer;
}
& > ._details {
background-color: var(--bg_3);
border: 0.15rem solid var(--highlight_1);
border-top: none;
border-radius: 0 0 0.2rem 0.2rem;
overflow: clip;
padding-bottom: 0.2rem;
padding-left: 0.3rem;
max-height: 80vh;
overflow-y: scroll;
& > ._full_path {
width: 100%;
font-style: italic;
padding-left: 0.2rem;
display: flex;
flex-direction: row;
list-style-type: none;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
& li {
margin-left: 0.3rem;
}
}
& > ._folder_list {
list-style: none;
padding-left: 0.4rem;
margin-left: 0.4rem;
border-left: 1px solid var(--text_border);
& ul {
list-style: none;
padding-left: 0.4rem;
margin-left: 0.4rem;
border-left: 1px solid var(--text_border);
}
& a:hover {
background-color: rgba(255, 255, 255, 0.1);
}
& span, & label {
width: 1rem;
display: inline-block;
cursor: pointer;
}
}
}
}

View file

@ -0,0 +1,113 @@
.dergen_search_form {
text-align: center;
& h1 {
padding-left: 0px;
border: none;
margin-bottom: 0.1em;
}
& input[type='text'] {
color: var(--text_1);
height: 2.5rem;
min-width: min(80%, 30em);
font-size: 1.2em;
background-color: #616161;
border: none;
border-radius: 1.25rem;
padding-left: 1.5rem;
margin-bottom: 0.8em;
box-shadow: 3px 3px 3px #00000033;
}
& button {
color: var(--text_1);
background-color: #515151;
border: none;
border-radius: 0.5rem;
cursor: pointer;
width: 15em;
height: 2.5em;
margin-left: 1em;
margin-right: 1em;
margin-bottom: 1em;
box-shadow: 3px 3px 2px #00000033;
}
button, input[type='text'] {
transition: background-color 0.3s;
&:hover {
background-color: #777777;
}
}
& ._search_type {
text-align: left;
margin-top: 1em;
& input {
display: none;
}
& label {
min-width: 5em;
text-align: center;
padding-left: 0.7em;
padding-right: 0.7em;
cursor: pointer;
display: inline-block;
border-bottom: 2px solid #FFFFFF50;
}
& input:checked + label {
border-bottom: 2px solid #FFFFFF;
}
}
}
.dergen_search_result_listing {
list-style: none;
margin-left: 0em;
padding-left: 0;
& > li {
margin-bottom: 2em;
}
& ._details {
list-style: none;
padding-left: 1.3em;
& ._path_details {
color: #AAAAAA;
& span {
font-size: 0.8rem;
min-width: 5rem;
display: inline-block;
}
}
& ._title {
font-size: 1.5rem;
}
}
}

67
www/static/styles/toc.css Normal file
View file

@ -0,0 +1,67 @@
.table_of_contents {
position: sticky;
float: left;
top: 0px;
flex: 0.1 0 15em;
padding-right: 0.3em;
padding-bottom: 0.5em;
border-radius: 0 0em 0em 0.5em;
box-shadow: -0.2em 0.2em 0.2em black;
--toc-fg: #cecece;
--toc-bg: var(--highlight_1a);
background-color: var(--bg_2);
& a {
display: inline-block;
transition: all 0.5s !important;
line-height: 1em;
text-align: justify;
font-size: 0.9rem;
padding-left: 0.2em;
padding-right: 0.2em;
padding-bottom: 0.2em;
color: var(--toc-fg) !important;
}
& .active > a {
background-color: var(--toc-bg);
border-bottom: 1px solid var(--highlight_1);
transition: all 0.2s;
}
& ol {
padding-left: 0.5em;
list-style: none;
}
& > li {
border-radius: 0.2em;
border-bottom: 1px solid transparent;
margin-bottom: 0.2em;
transition: all 1s;
}
& .toc_collapsing {
display: none;
}
& li:is(:has(.active),.active) > ol > .toc_collapsing {
display: block;
}
}

15
www/static/three-dots.svg Normal file
View file

@ -0,0 +1,15 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="120" height="30" viewBox="0 0 120 30" xmlns="http://www.w3.org/2000/svg" fill="#fff">
<circle cx="15" cy="15" r="15">
<animate attributeName="r" from="15" to="15" begin="0s" dur="0.8s" values="15;9;15" calcMode="linear" repeatCount="indefinite"/>
<animate attributeName="fill-opacity" from="1" to="1" begin="0s" dur="0.8s" values="1;.5;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="60" cy="15" r="9" fill-opacity="0.3">
<animate attributeName="r" from="9" to="9" begin="0s" dur="0.8s" values="9;15;9" calcMode="linear" repeatCount="indefinite"/>
<animate attributeName="fill-opacity" from="0.5" to="0.5" begin="0s" dur="0.8s" values=".5;1;.5" calcMode="linear" repeatCount="indefinite"/>
</circle>
<circle cx="105" cy="15" r="15">
<animate attributeName="r" from="15" to="15" begin="0s" dur="0.8s" values="15;9;15" calcMode="linear" repeatCount="indefinite"/>
<animate attributeName="fill-opacity" from="1" to="1" begin="0s" dur="0.8s" values="1;.5;1" calcMode="linear" repeatCount="indefinite"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

316
www/static/toc.js Normal file
View file

@ -0,0 +1,316 @@
// Code By Webdevtrick ( https://webdevtrick.com )
// https://webdevtrick.com/dynamic-table-of-contents/
//
// Modified by dergens
let tocId = "toc";
const TOC_IDENTIFIER = "toc";
const TOC_MAIN_CONTENT_ID = "#content_article"
const TOC_TOP_PIXEL_MARGIN = 150;
class TocTracker {
known_heading_elements = [];
constructor() {
this.intersection_observer = null;
this.tracking_update_callbacks = [];
this.tracking_rebuild_callbacks = [];
}
_disconnectIntersectionObserver() {
if(this.intersection_observer) {
this.intersection_observer.disconnect();
this.intersection_observer = null;
}
}
_setupIntersectionObserver() {
let options = {
root: null,
rootMargin: `-${TOC_TOP_PIXEL_MARGIN}px 0px 0px 0px`,
threshold: [1]
};
this.intersection_observer = new IntersectionObserver(
(entries) => this._findActiveHeading(),
options
);
this.known_heading_elements.forEach((element) => {
this.intersection_observer.observe(element.dom);
});
}
_searchForHeadings() {
const main = document.querySelector(TOC_MAIN_CONTENT_ID);
if (!main) {
throw Error("A `main` tag section is required to query headings from.");
}
let headings = main.querySelectorAll("h1, h2, h3, h4, h5, h6, li strong");
headings.forEach((heading, index) => {
let heading_level = parseInt(heading.tagName.slice(-1));
let heading_collapsed = false;
let heading_name = heading.innerHTML;
if(heading.tagName == 'STRONG') {
heading_level = -1;
heading_collapsed = true;
heading = heading.closest('li');
}
this.known_heading_elements.push({
level: heading_level,
name: heading_name,
dom: heading,
collapse: heading_collapsed
});
});
}
_sortHeadings() {
this.known_heading_elements.sort((a, b) => {
return a.dom.getBoundingClientRect().y
- b.dom.getBoundingClientRect().y;
});
let lastHeadingLevel = 0;
this.known_heading_elements.forEach((element) => {
if(element.level == -1) {
let extra_depth = 0;
let current_dom = element.dom;
while(current_dom.tagName != 'ARTICLE') {
if((current_dom.tagName == 'OL') || (current_dom.tagName == 'UL'))
extra_depth += 1;
current_dom = current_dom.parentElement.closest('ol,ul,article');
}
element.level = lastHeadingLevel+extra_depth;
}
else
lastHeadingLevel = element.level;
});
}
_generateHeadingPaths() {
let current_path = [];
this.known_heading_elements.forEach(heading => {
while((current_path.length != 0) && (current_path[current_path.length-1].level >= heading.level))
current_path.pop();
current_path.push(heading);
// The zero does nothing. But according to stackoverflow, it makes it faster.
// Why? No idea.
// Just leave it in I suppose.
heading.path = current_path.slice(0);
});
}
_fillHeadingIDs() {
this.known_heading_elements.forEach(heading => {
heading.id = heading.dom.id;
if(heading.id)
return;
heading.dom.id = heading.path.map(i =>
i.name.replace(/\W+/g, '-').trim()
).join('--').toLowerCase();
heading.id = heading.dom.id;
});
}
_findActiveHeading() {
const arr = this.known_heading_elements;
if(arr.length == 0)
return;
// Start should always be higher
// End always lower
let start = 0, end = arr.length - 1;
let mid = Math.floor((start + end) / 2);
// Special case handling, if all items are above the scroll margin
if(arr[end].dom.getBoundingClientRect().y < TOC_TOP_PIXEL_MARGIN) {
start = end;
mid = end;
}
// Iterate until we find the boundary
while (mid > start) {
// Check if the mid is above our boundary ((lower Y))
if (arr[mid].dom.getBoundingClientRect().y < TOC_TOP_PIXEL_MARGIN)
start = mid;
else
end = mid;
// Find the mid index
mid = Math.floor((start + end) / 2);
}
this.tracking_update_callbacks.forEach(callback => callback(arr[start]));
return arr[start];
}
reloadHeadings() {
this.known_heading_elements = [];
this._disconnectIntersectionObserver();
this._searchForHeadings();
this._sortHeadings();
this._generateHeadingPaths();
this._fillHeadingIDs();
this.tracking_rebuild_callbacks.forEach(item => item());
this._setupIntersectionObserver();
}
onTrackingUpdate(callback) {
this.tracking_update_callbacks.push(callback);
return callback;
}
onTrackingRebuild(callback) {
this.tracking_rebuild_callbacks.push(callback);
return callback;
}
}
class TocNavBarUpdater {
constructor(toc_tracker) {
this.toc_tracker = toc_tracker;
this.navbar_dom = null;
this.added_navbar_elements = [];
this.toc_tracker.onTrackingRebuild(() =>
this.trackingRebuildCallback() );
this.toc_tracker.onTrackingUpdate((element) => this.trackingUpdateCallback(element) );
}
_removeNavbarElements() {
this.added_navbar_elements.forEach(dom => dom.remove());
this.added_navbar_elements = [];
}
trackingRebuildCallback() {
this._removeNavbarElements();
this.navbar_dom = document.querySelector('#main_navbar_path_list');
}
trackingUpdateCallback(element) {
this._removeNavbarElements();
element.path.forEach(pathItem => {
let newNode = document.createElement('li');
const pathURL = location.pathname + '#' + pathItem.id + location.search;
newNode.innerHTML = "<a hx-boost=false href=" + pathURL + "> #<sub>" + pathItem.level + '</sub>' + pathItem.name + '</a>'
this.navbar_dom.appendChild(newNode);
this.added_navbar_elements.push(newNode);
});
}
}
class TocSidemenu {
constructor(toc_tracker) {
this.toc_tracker = toc_tracker;
this.sidebar_dom = null;
this.sidebar_elements = {};
this.currently_active_entry = null;
toc_tracker.onTrackingRebuild(() => this.trackingRebuildCallback());
toc_tracker.onTrackingUpdate((element) => this.trackingUpdateCallback(element));
}
_clearSidebar() {
if(this.sidebar_dom) {
this.sidebar_dom.remove();
}
this.sidebar_elements = {};
this.currently_active_entry = null;
}
_generateSidebar() {
let toc_stack = [this.sidebar_dom];
let last_added_li = null;
this.toc_tracker.known_heading_elements.forEach(element => {
while(element.level > toc_stack.length) {
if(!last_added_li) {
last_added_li = document.createElement('li');
toc_stack[toc_stack.length-1].appendChild(last_added_li);
}
let new_ol = document.createElement('ol');
last_added_li.appendChild(new_ol);
last_added_li = false;
toc_stack.push(new_ol);
}
while(element.level < toc_stack.length)
toc_stack.pop();
let new_element = document.createElement('li');
last_added_li = new_element;
const pathURL = location.pathname + '#' + element.id + location.search;
new_element.innerHTML = "<a hx-boost=false style=\"padding-left: " + element.level * 0.8 + "em\" href=" + pathURL + ">" + element.name + "</a>";
if(element.collapse)
new_element.classList.add('toc_collapsing');
this.sidebar_elements[element.id] = new_element;
toc_stack[toc_stack.length-1].appendChild(new_element);
});
}
trackingRebuildCallback() {
this._clearSidebar();
this.sidebar_dom = document.createElement('ol');
document.querySelector('#toc').appendChild(this.sidebar_dom);
this._generateSidebar();
}
trackingUpdateCallback(entry) {
if(this.currently_active_entry) {
this.currently_active_entry.classList.remove('active');
}
Object.values(this.sidebar_elements).forEach((entry) => entry.classList.remove('contains_active'));
let active_entry = this.sidebar_elements[entry.dom.id];
active_entry.classList.add('active');
this.currently_active_entry = active_entry;
entry.path.forEach((path_piece) => this.sidebar_elements[path_piece.id].classList.add('contains_active'));
}
}
let tracker = new TocTracker();
let navbar_updater = new TocNavBarUpdater(tracker);
let sidebar_updater = new TocSidemenu(tracker);
document.addEventListener('DOMContentLoaded', (event) => tracker.reloadHeadings());

View file

@ -0,0 +1,36 @@
{% extends "root.html" %}
{% block second_title %}
<h2> YAP SOMETHIN' </h2>
{% endblock %}
{%block main_content%}
<article>
<form method="post" enctype="multipart/form-data"
action="/api/add_yap"
hx-target="#yap-result"
hx-swap="innerHTML">
<label for="ACCESS_KEY"> Password: </label>
<input type="password" id="ACCESS_KEY" name="ACCESS_KEY"/>
<label for="path"> Path: </label>
<input type="text" id="path" name="path"/>
<label for="yap_category"> Yap Category: </label>
<input type="text" id="yap_category" name="yap_category"
value="yap"/>
<textarea type="file" id="yap_text" name="yap_text">
</textarea>
<button>Submit</button>
</form>
Return data:
<code id="yap-result">
</code>
</article>
{%endblock%}

View file

@ -0,0 +1,22 @@
<a href="{{post.url}}">
<div class="article_blop">
<div class="article_blop_bg" style="background-image:url({{post.preview_image}});"></div>
<div class="article_blop_content">
<h1 class="article_blop_title">
{{post.title}}
</h1>
<ul class="article_blop_tags">
{% for tag in post.tags %}
<li> {{ tag }} </li>
{% endfor %}
</ul>
<span class="article_blop_excerpt">
{{ post.brief }}
</span>
</div>
</div>
</a>

View file

@ -0,0 +1,4 @@
{% for post in search_results %}
{{ include('fragments/blog/card.html') }}
{% endfor %}

View file

@ -0,0 +1,10 @@
<div>
<h1>
{{ post.title }}
</h1>
{% if post.authors %}
Written by {{ post.authors }}
{% endif %}
</div>

View file

@ -0,0 +1,96 @@
<div class="navbar">
<menu class="_titles">
<li>
{{ page.basename }}
</li>
</menu>
<menu class="_path" id="main_navbar_path">
<ul class="_path_list" id="main_navbar_path_list">
{% set split_post = page.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 %}
</ul>
<form class="_search_field"
action="/search"
method="GET"
style="margin-left: auto;">
{{ fa['magnifying-glass']|raw }}
<input class="_search_input"
type="text"
id="inline_search_query" name="query"
autocomplete="off"
hx-target="#inline_search_result_list"
hx-indicator="#inline_search_result_list"
hx-sync="this:replace"
hx-trigger="keypress changed delay:0.25s, input changed delay:0.25s"
hx-get="/ajax/fragments/search/inline.html">
</input>
<ul class="_search_list htmx-indicator"
id="inline_search_result_list"
tabindex="1">
Type to search...
</ul>
</form>
<li>
<label for="navbar-expander" class="expandable"> {{ fa['bars'] | raw }} </label>
<a rel="alternate" type="application/rss+xml" target="_blank"
style="padding-left: 0.3rem;" href="/feed/rss{{page.path}}">
{{ fa['rss']|raw }}
</a>
</li>
</menu>
<input id="navbar-expander" class="expandable" type="checkbox" hx-preserve>
<div class="_details expandable">
Full path:
<ul class="_full_path">
{% set split_post = page.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 %}
</ul>
Dev links:
<a hx-boost="false" href="/raw{{page.path}}">raw</a>
<a hx-boost="false" href="/api/posts{{page.path}}">api</a>
<h4>Folder browser: </h4>
<ul class="_folder_list">
<li>
<span>
{{ fa['turn-up'] | raw}}
</span>
<a href={{page.parent.path}}>..</a>
</li>
{{ include('fragments/directory/compact/listing.html') }}
</ul>
</div>
</div>

View file

@ -0,0 +1,27 @@
<li class="folder_listing">
{% set folder_key = random() %}
<input type="checkbox" id="folder-open-{{folder_key}}" name="folder-open-{{folder_key}}">
<label for="folder-open-{{folder_key}}"
hx-trigger="click once queue:last, mouseenter once queue:all, intersect once queue:all"
hx-get="/ajax/fragments/directory/compact/listing.html?page={{ post.path }}"
hx-target="next ul">
{{ fa['folder'] | raw }}
{{ fa['folder-open'] | raw }}
</label>
<a href={{post.path}}>
{{ post.basename }} - {{ post.title }}
</a>
<ul class="folder_listing">
<li>
<span>
Loading...
</span>
</li>
</ul>
</li>

View file

@ -0,0 +1,4 @@
{% for post in page.child_posts %}
{{ include('fragments/directory/compact/entry.html') }}
{% endfor %}

View file

@ -0,0 +1,39 @@
<h3>Directory contents for {{page.basename}}:</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>
<tr class="entry">
<td>
{{ fa['turn-up'] | raw }}
</td>
<td class="hsmol_hide">
<a href={{page.parent.path}}> .. </a>
</td>
<td class="entry_title">
<a href={{page.parent.path}}> .. </a>
</td>
<td class="entry_update_time hsmol_hide">
</td>
</tr>
{% for post in page.child_posts %}
<tr class="entry">
<td>
{{ fa[post.icon] | raw }}
</td>
<td class="hsmol_hide">
<a href={{post.path}}>{{post.basename}}</a>
</td>
<td class="entry_title">
<a href={{post.path}}>{{ post.title }}</a>
</td>
<td class="entry_update_time hsmol_hide">
{{ post.updated_at }}
</td>
</tr>
{% endfor %}
</table>

View file

@ -1,33 +0,0 @@
<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,10 @@
{% for post in search_results %}
<a href="{{post.url}}#:~:text={{ search_query|url_encode }}"
rel="noopener"
hx-boost="off"> {# We can't boost because text-fragments HAVE to be user-inited! #}
<li>
{{post.title}}
</li>
</a>
{% endfor %}

View file

@ -1,28 +0,0 @@
{% extends "root.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/gallerystyle.css">
{% endblock %}
{% block second_title %}
<h2> Gallery </h2>
{% endblock %}
{%block main_content%}
<figure>
<a href="{{ image_url }}" target="_blank">
<img id="gallery_image" src="{{ image_url }}"> </img>
</a>
<article>
<figcaption id="gallery_title"> {{ image_title }} </figcaption>
<span> {{ image_desc }} </span>
<br>
<a href="{{ artist_src_link }}"> Artist: {{ artist_name }} </a>
<br>
<a href="{{ image_src_link }}"> Source link </a>
</article>
</figure>
{%endblock%}

View file

@ -1,18 +0,0 @@
{% extends "root.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/gallerystyle.css">
{% endblock %}
{% block second_title %}
<h2> Gallery </h2>
{% endblock %}
{%block main_content%}
<div id="gallery_root_grid">
{% for key,value in array_path %}
<div class=""
{% endfor %}
</div>
{%endblock%}

View file

@ -0,0 +1,20 @@
{% extends "pathed_content.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/directorystyle.css">
<link rel="stylesheet" href="/static/article_blop.css">
{%endblock%}
{%block content_article%}
{{ content_html|raw }}
{% if blog_posts|length > 0 %}
{% for post in blog_posts %}
{% include('fragments/article_blop.html') %}
{% endfor %}
{%else%}
<h4>How sad. There are no blog posts yet... What a real shame :c </h4>
{% endif %}
{%endblock%}

View file

@ -6,23 +6,23 @@
{{ 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}}">
<link rel="alternate" type="application/atom+xml" title="{{opengraph.site_name}} Feed for {{page.path}}" href="{{page.uri_prefix}}/feed/atom{{page.path}}">
{% endblock %}
{% block second_title %}
<h2> {{ post.post_metadata.title }} </h2>
<h2> {{ page.title }} </h2>
{% endblock %}
{%block main_content%}
{{ include('fragments/filepath_bar.html') }}
{{ include('fragments/decoration/navbar.html') }}
<article>
<article id="content_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~
This page was created on {{ page.created_at }}, last edited {{ page.updated_at }}, and was viewed {{ page.counters.views }} times~
</span>
</article>

View file

@ -1,43 +0,0 @@
{% 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

@ -1,59 +0,0 @@
{% 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,13 @@
{% extends "pathed_content.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/directorystyle.css">
{%endblock%}
{%block content_article%}
{{ include('fragments/directory/inline.html') }}
{{ page.html|raw }}
{%endblock%}

View file

@ -0,0 +1,27 @@
{% extends "pathed_content.html" %}
{%block content_article%}
{{ include('fragments/directory/inline.html') }}
{{ content_html|raw }}
{% if search_results|length > 0 %}
<ul class="gallery">
{% for post in search_results %}
<li class="entry">
<a href={{post.path}}>
<figure>
<img src="{{post.media_preview_url}}">
<figcaption>
{{ post.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

@ -1,5 +1,4 @@
{% extends "pathed_content.html" %}
{% block extra_head %}
@ -14,12 +13,12 @@
{%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 target="_blank" href="{{page.media_url}}">
<img id="gallery_image" src="{{page.media_url}}"></img>
</a>
<figcaption>
{{ content_html|raw }}
{{ page.html|raw }}
</figcaption>
</figure>
{%endblock%}

View file

@ -9,5 +9,9 @@
{%endblock %}
{%block content_article%}
{{ content_html|raw }}
{% if page.title and (page.title != page.basename) %}
<h1 class="content-title"> {{ page.title }} </h1>
{% endif %}
{{ page.html|raw }}
{% endblock %}

View file

@ -1,36 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>{{og.site_name}} - {{og.title}}</title>
<title>{{opengraph.site_name}} - {{opengraph.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="stylesheet" href="/static/directorystyle.css">
<link rel="stylesheet" href="/static/article_blop.css">
<link rel="stylesheet" href="/static/gallerystyle.css">
<link rel="icon" type="image/x-icon" href="/static/icon.jpeg">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="/static/htmx.min.js"></script>
<script id="main_banner_script" src="/static/banner.js"></script>
<script src="/static/toc.js"></script>
{% block feed_links %}
<link rel="alternate" type="application/rss+xml" title="{{og.site_name}} Global Feed" href="{{site_config.uri_prefix}}/feed">
<link rel="alternate" type="application/rss+xml" title="{{opengraph.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:site_name" content="{{opengraph.site_name}}">
<meta property="og:url" content="{{og.url}}" />
<meta property="og:url" content="{{opengraph.url}}" />
<meta property="og:title" content="{{ og.title|e }}" />
<meta name="twitter:title" content="{{ og.title|e }}" />
<meta property="og:title" content="{{ opengraph.title|e }}" />
<meta name="twitter:title" content="{{ opengraph.title|e }}" />
<meta property="og:type" content="{{og.type}}" />
<meta property="og:type" content="{{opengraph.type}}" />
<meta property="og:description" content="{{ og.description|e }}" />
<meta name="twitter:description" content="{{ og.description|e }}" />
<meta property="og:description" content="{{ opengraph.description|e }}" />
<meta name="twitter:description" content="{{ opengraph.description|e }}" />
<meta property="og:image" content="{{og.image}}" />
<meta name="twitter:image" content="{{og.image}}" />
<meta property="og:image" content="{{opengraph.image}}" />
<meta name="twitter:image" content="{{opengraph.image}}" />
<meta name="twitter:card" content="summary_large_image">
<meta name="robots" content="max-image-preview:large">
@ -38,11 +48,9 @@
<meta property="al:android:app_name" content="Medium" />
{% endblock %}
<script type="text/javascript">
window.dergBannerOptions = JSON.parse('{{banner|raw}}');
</script>
</head>
<body>
<body id="main_body" hx-sync="a:replace img:drop" hx-boost="true" hx-indicator="#main_body">
{%if age_gate %}
<div id="age_gate_block">
@ -59,32 +67,54 @@
{% endif %}
<header id="main_header">
<img id="main_banner_img"></img>
<a id="main_banner_img_link" href="/gallery"> full picture</a>
<img id="main_banner_img" hx-preserve></img>
<a id="main_banner_img_link" href="/gallery" hx-preserve> full picture</a>
<script type="text/javascript">
window.dergBannerOptions = JSON.parse('{{banners|raw}}');
<script src="/static/banner.js"></script>
startBanner();
</script>
<h1 id="big_title">{% block big_title %}{{og.site_name}}{%endblock%}</h1>
<h1 id="big_title">{% block big_title %}{{opengraph.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>
{% for link in page.navbar_links %}
<li><a href="{{link.href}}"> {{link.text}} </a></li>
{% endfor %}
</menu>
</header>
<main id="main_content_wrapper">
{% block main_content %}<h3>Soon there shall be content!</h3>{% endblock %}
</main>
<div id="main_content_flexbox">
<nav id="toc_container" class="table_of_contents navbar">
<div class="_titles">
</div>
<div class="_path">
toc
</div>
<div id="toc">
</div>
</nav>
<main id="main_content_wrapper">
{% block main_content %}
<h3>This here should have been replaced by content.
</h3>
If you can see this, complain to your nearest dragon.
{% endblock %}
</main>
</div>
<footer id="main_footer">
{% block main_footer %}
<span> test? </span>
{% endblock %}
</footer>
<script> tracker.reloadHeadings(); </script>
</body>
</html>

85
www/templates/search.html Normal file
View file

@ -0,0 +1,85 @@
{% extends "pathed_content.html" %}
{% block second_title %}
<h2> Dergen Search </h2>
{% endblock %}
{%block content_article %}
<form class="dergen_search_form" method="get" enctype="multipart/form-data" action="/search">
<h1>
Dergle
</h1>
<input type="text" id="query" autocomplete="on" name="query" placeholder="Search for..."
{% if search_query.user_type %}
value="{{search_query.user_input}}"
{% endif %}
/> <br>
<button type="submit">Search!</button>
<button>I'm feeling cuddly</button>
<div class="_search_type">
{% for type in search_query.types %}
<input type="radio" name="search_type"
id="type_{{type[1]}}" value="{{type[1]}}"
onchange="this.form.submit();"
{% if type[1] == search_query.user_type %}
checked
{% endif %}
>
<label for="type_{{type[1]}}">
{{type[0]}}
</label>
{% endfor %}
</div>
</form>
{% if display_type == 'no_search' %}
<h3>Search for something :D</h3>
<ul>
<li>Type normal words to search for post title, brief descriptions and main content</li>
<li>Use the syntax <code>tags:tag_a,tag_b,...</code> to search for specific tags</li>
<li>Use <code>path:/some/example/path</code> to limit search to a specific path </li>
<li>Use the type selector to more precisely look for images, blogs, etc.</li>
<li>Annoy the dragons to implement more metadata search tags</li>
</ul>
{% elseif display_type == 'image' %}
<ul class="gallery">
{% for post in search_results %}
<li class="entry">
<a href={{post.path}}>
<figure>
<img src="{{post.media_preview_url}}">
<figcaption>
{{ post.title }} ({{ post.search_score }})
</figcaption>
</figure>
</a>
</li>
{% endfor %}
</ul>
{%elseif display_type == 'no results' %}
<h4>How sad. There are no search results here... :c </h4>
{%else%}
<ul class="dergen_search_result_listing">
{% for post in search_results %}
<li>
<ul class="_details">
<li class="_path_details"> <span>Score: {{post.search_score|round(2)}}</span> <span>Path: {{post.path}}</span> </li>
<li class="_title">
<a href="{{post.url}}#:~:text={{ search_query.user_input|url_encode }}"
target="_blank"
rel="noopener">
{{post.title}} </a>
</li>
<li class="_brief"> {{post.brief}} </li>
</ul>
</li>
{% endfor %}
</ul>
{% endif %}
{%endblock%}

View file

@ -8,10 +8,10 @@
{%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"/>
<form method="post" enctype="multipart/form-data" action="/api/upload">
<input type="text" id="path" name="path"/>
<input type="password" id="ACCESS_KEY" name="ACCESS_KEY"/>
<input type="file" id="file" name="file"/>
<button>Submit</button>
</form>