Compare commits
46 commits
main
...
dev/post_d
Author | SHA1 | Date | |
---|---|---|---|
f8af3c5c59 | |||
143c932c88 | |||
bbc093b497 | |||
73414fa639 | |||
6ae06b4765 | |||
5e2f0a7185 | |||
31080cae2b | |||
a3604d21c9 | |||
5ddbae91d2 | |||
91500ccfe5 | |||
77b3f858d1 | |||
9870967093 | |||
da25a786d1 | |||
0f2634749d | |||
c60b052951 | |||
f560bdc3f0 | |||
85fe57ea0c | |||
3e1e61bf4a | |||
05996b2ebf | |||
de1f1446a3 | |||
771e9a2ec8 | |||
31150b9b12 | |||
d609265862 | |||
95e3fc0b00 | |||
fc0d38e118 | |||
2fe4d20187 | |||
7c8d0191d2 | |||
02054d418d | |||
c877c8ce31 | |||
cd5b6f2942 | |||
9de4838081 | |||
9a5b9e609e | |||
bf2486caa4 | |||
22c953793c | |||
f9cfb81079 | |||
aabe0c72d5 | |||
626a4bbaf6 | |||
2e012b4fd5 | |||
c9808f90f8 | |||
c607d57221 | |||
0dcf36052e | |||
76ca7b9c32 | |||
0f2761cd61 | |||
b8e449006b | |||
a2c6842b71 | |||
b420a6eafa |
72 changed files with 4657 additions and 992 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
|
||||
/vendor/
|
||||
.docker_vols
|
||||
|
||||
sftp.json
|
0
Rakefile
Normal file
0
Rakefile
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
);
|
19
dragon_fire.code-workspace
Normal file
19
dragon_fire.code-workspace
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../dragon_fire_content"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"conventionalCommits.scopes": [
|
||||
"search",
|
||||
"templates",
|
||||
"css",
|
||||
"database",
|
||||
"analytics"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
335
www/composer.lock
generated
|
@ -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": [],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>'
|
||||
];
|
||||
|
||||
?>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
1
www/secrets/.gitignore
vendored
1
www/secrets/.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
*.json
|
||||
*.yml
|
||||
api_admin_key
|
20
www/src/db_handler/analytics_interface.php
Normal file
20
www/src/db_handler/analytics_interface.php
Normal 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);
|
||||
}
|
||||
|
||||
?>
|
109
www/src/db_handler/db_interface.php
Normal file
109
www/src/db_handler/db_interface.php
Normal 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);
|
||||
}
|
||||
|
||||
?>
|
335
www/src/db_handler/mysql_analytics_handler.php
Normal file
335
www/src/db_handler/mysql_analytics_handler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
529
www/src/db_handler/mysql_handler.php
Normal file
529
www/src/db_handler/mysql_handler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
81
www/src/db_handler/mysql_taglist_handling.php
Normal file
81
www/src/db_handler/mysql_taglist_handling.php
Normal 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
|
||||
];
|
||||
}
|
||||
|
||||
?>
|
158
www/src/db_handler/mysql_yaps_handler.php
Normal file
158
www/src/db_handler/mysql_yaps_handler.php
Normal 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
250
www/src/db_handler/post.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
76
www/src/db_handler/post_handler.php
Normal file
76
www/src/db_handler/post_handler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
68
www/src/db_handler/yaps_interface.php
Normal file
68
www/src/db_handler/yaps_interface.php
Normal 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
201
www/src/dbtest.php
Normal 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
113
www/src/dergdown.php
Normal 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
17
www/src/fontawesome.php
Normal 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>'
|
||||
];
|
||||
|
||||
?>
|
|
@ -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;
|
||||
|
|
@ -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
35
www/src/router.php
Normal 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
36
www/src/serve/ajax.php
Normal 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
137
www/src/serve/api.php
Normal 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
134
www/src/serve/post.php
Normal 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
112
www/src/setup/analytics.php
Normal 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
70
www/src/setup/db.php
Normal 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'];
|
||||
|
||||
?>
|
46
www/src/setup/derg_insert.php
Normal file
46
www/src/setup/derg_insert.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
26
www/src/setup/permissions.php
Normal file
26
www/src/setup/permissions.php
Normal 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'];
|
||||
}
|
||||
|
||||
?>
|
19
www/src/setup/site_config.php
Normal file
19
www/src/setup/site_config.php
Normal 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
24
www/src/setup/twig.php
Normal 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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
?>
|
|
@ -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>
|
81
www/static/article_blop.css
Normal file
81
www/static/article_blop.css
Normal 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);
|
||||
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
1
www/static/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
}
|
||||
}
|
42
www/static/styles/age_gate.css
Normal file
42
www/static/styles/age_gate.css
Normal 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;
|
||||
}
|
||||
}
|
222
www/static/styles/post_navbar.css
Normal file
222
www/static/styles/post_navbar.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
www/static/styles/search.css
Normal file
113
www/static/styles/search.css
Normal 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
67
www/static/styles/toc.css
Normal 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
15
www/static/three-dots.svg
Normal 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
316
www/static/toc.js
Normal 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());
|
36
www/templates/_dev_add_yap.html
Normal file
36
www/templates/_dev_add_yap.html
Normal 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%}
|
22
www/templates/fragments/blog/card.html
Normal file
22
www/templates/fragments/blog/card.html
Normal 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>
|
4
www/templates/fragments/blog/listing.html
Normal file
4
www/templates/fragments/blog/listing.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
{% for post in search_results %}
|
||||
{{ include('fragments/blog/card.html') }}
|
||||
{% endfor %}
|
10
www/templates/fragments/decoration/blog_header.html
Normal file
10
www/templates/fragments/decoration/blog_header.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
<div>
|
||||
<h1>
|
||||
{{ post.title }}
|
||||
</h1>
|
||||
|
||||
{% if post.authors %}
|
||||
Written by {{ post.authors }}
|
||||
{% endif %}
|
||||
</div>
|
96
www/templates/fragments/decoration/navbar.html
Normal file
96
www/templates/fragments/decoration/navbar.html
Normal 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>
|
27
www/templates/fragments/directory/compact/entry.html
Normal file
27
www/templates/fragments/directory/compact/entry.html
Normal 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>
|
4
www/templates/fragments/directory/compact/listing.html
Normal file
4
www/templates/fragments/directory/compact/listing.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
{% for post in page.child_posts %}
|
||||
{{ include('fragments/directory/compact/entry.html') }}
|
||||
{% endfor %}
|
39
www/templates/fragments/directory/inline.html
Normal file
39
www/templates/fragments/directory/inline.html
Normal 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>
|
|
@ -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>
|
10
www/templates/fragments/search/inline.html
Normal file
10
www/templates/fragments/search/inline.html
Normal 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 %}
|
|
@ -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%}
|
|
@ -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%}
|
20
www/templates/old_post_types/blog_list.html
Normal file
20
www/templates/old_post_types/blog_list.html
Normal 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%}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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%}
|
|
@ -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%}
|
13
www/templates/render_templates/directory.html
Normal file
13
www/templates/render_templates/directory.html
Normal 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%}
|
27
www/templates/render_templates/gallery.html
Normal file
27
www/templates/render_templates/gallery.html
Normal 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%}
|
|
@ -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%}
|
|
@ -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 %}
|
|
@ -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
85
www/templates/search.html
Normal 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%}
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue