diff --git a/.gitignore b/.gitignore index 0f521b3..e731746 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ + /vendor/ +.docker_vols sftp.json \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..e69de29 diff --git a/docker_dev/Dockerfile b/docker_dev/Dockerfile index 6440854..300aee9 100644 --- a/docker_dev/Dockerfile +++ b/docker_dev/Dockerfile @@ -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) \ No newline at end of file diff --git a/docker_dev/compose.yaml b/docker_dev/compose.yaml index 1b9493d..5a25529 100644 --- a/docker_dev/compose.yaml +++ b/docker_dev/compose.yaml @@ -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 diff --git a/docker_dev/mysql_schema.sql b/docker_dev/mysql_schema.sql index baf0a58..77520a8 100644 --- a/docker_dev/mysql_schema.sql +++ b/docker_dev/mysql_schema.sql @@ -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) ); \ No newline at end of file diff --git a/dragon_fire.code-workspace b/dragon_fire.code-workspace new file mode 100644 index 0000000..265fc75 --- /dev/null +++ b/dragon_fire.code-workspace @@ -0,0 +1,19 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../dragon_fire_content" + } + ], + "settings": { + "conventionalCommits.scopes": [ + "search", + "templates", + "css", + "database", + "analytics" + ] + } +} \ No newline at end of file diff --git a/www/.htaccess b/www/.htaccess index eb86f63..1ceb07b 100644 --- a/www/.htaccess +++ b/www/.htaccess @@ -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 - Header set Cache-Control "max-age=315360, public" + Header set Cache-Control "max-age=60, public" - Header set Cache-Control "max-age=315360, public" - \ No newline at end of file + Header set Cache-Control "max-age=60, public" + diff --git a/www/composer.lock b/www/composer.lock index c157e3a..4773590 100644 --- a/www/composer.lock +++ b/www/composer.lock @@ -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": [], diff --git a/www/dergdown.php b/www/dergdown.php deleted file mode 100644 index 8380be7..0000000 --- a/www/dergdown.php +++ /dev/null @@ -1,39 +0,0 @@ - - -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; - } -} \ No newline at end of file diff --git a/www/fontawesome.php b/www/fontawesome.php deleted file mode 100644 index 91b82d5..0000000 --- a/www/fontawesome.php +++ /dev/null @@ -1,11 +0,0 @@ - '', - 'image' => '', - 'images' => '', - 'folder' => '', - 'rss' => '' -]; - -?> \ No newline at end of file diff --git a/www/post_adapter.php b/www/post_adapter.php deleted file mode 100644 index 65b4ced..0000000 --- a/www/post_adapter.php +++ /dev/null @@ -1,226 +0,0 @@ -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); - } -} - -?> \ No newline at end of file diff --git a/www/secrets/.gitignore b/www/secrets/.gitignore index f573c46..bb1f315 100644 --- a/www/secrets/.gitignore +++ b/www/secrets/.gitignore @@ -1,2 +1,3 @@ *.json +*.yml api_admin_key \ No newline at end of file diff --git a/www/src/db_handler/analytics_interface.php b/www/src/db_handler/analytics_interface.php new file mode 100644 index 0000000..473c1e6 --- /dev/null +++ b/www/src/db_handler/analytics_interface.php @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/www/src/db_handler/db_interface.php b/www/src/db_handler/db_interface.php new file mode 100644 index 0000000..c41b31a --- /dev/null +++ b/www/src/db_handler/db_interface.php @@ -0,0 +1,109 @@ +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); +} + +?> \ No newline at end of file diff --git a/www/src/db_handler/mysql_analytics_handler.php b/www/src/db_handler/mysql_analytics_handler.php new file mode 100644 index 0000000..2906e59 --- /dev/null +++ b/www/src/db_handler/mysql_analytics_handler.php @@ -0,0 +1,335 @@ +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; + } + } +} + +?> \ No newline at end of file diff --git a/www/src/db_handler/mysql_handler.php b/www/src/db_handler/mysql_handler.php new file mode 100644 index 0000000..105be1d --- /dev/null +++ b/www/src/db_handler/mysql_handler.php @@ -0,0 +1,529 @@ + '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; + } +} + +?> \ No newline at end of file diff --git a/www/src/db_handler/mysql_taglist_handling.php b/www/src/db_handler/mysql_taglist_handling.php new file mode 100644 index 0000000..2acd999 --- /dev/null +++ b/www/src/db_handler/mysql_taglist_handling.php @@ -0,0 +1,81 @@ + $search_modifiers, + 'parameter_string' => $search_params + ]; +} + +?> \ No newline at end of file diff --git a/www/src/db_handler/mysql_yaps_handler.php b/www/src/db_handler/mysql_yaps_handler.php new file mode 100644 index 0000000..39ef8af --- /dev/null +++ b/www/src/db_handler/mysql_yaps_handler.php @@ -0,0 +1,158 @@ +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; + } +} + +?> \ No newline at end of file diff --git a/www/src/db_handler/post.php b/www/src/db_handler/post.php new file mode 100644 index 0000000..d26ec04 --- /dev/null +++ b/www/src/db_handler/post.php @@ -0,0 +1,250 @@ + -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; + } +} + +?> diff --git a/www/src/db_handler/post_handler.php b/www/src/db_handler/post_handler.php new file mode 100644 index 0000000..51fbbe2 --- /dev/null +++ b/www/src/db_handler/post_handler.php @@ -0,0 +1,76 @@ +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; + } +} + +?> \ No newline at end of file diff --git a/www/src/db_handler/yaps_interface.php b/www/src/db_handler/yaps_interface.php new file mode 100644 index 0000000..f9a2727 --- /dev/null +++ b/www/src/db_handler/yaps_interface.php @@ -0,0 +1,68 @@ + \ No newline at end of file diff --git a/www/src/dbtest.php b/www/src/dbtest.php new file mode 100644 index 0000000..01c795a --- /dev/null +++ b/www/src/dbtest.php @@ -0,0 +1,201 @@ +'; + echo 'Error number: ' . mysqli_connect_errno() . '
'; + echo 'Error message: ' . mysqli_connect_error() . '
'; + 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"; +?> \ No newline at end of file diff --git a/www/src/dergdown.php b/www/src/dergdown.php new file mode 100644 index 0000000..c720d01 --- /dev/null +++ b/www/src/dergdown.php @@ -0,0 +1,113 @@ +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' => ' +
+

Error in a dergen template!

+ YAML could not be parsed properly:
+ + ' . $ex->getMessage() . '
' + ); + } + + 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' => ' +
+

Error in a dergen template!

+ Rendering engine threw an error:
+ + ' . $ex->getMessage() . '
' + ); + } + + 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; + } +} \ No newline at end of file diff --git a/www/src/fontawesome.php b/www/src/fontawesome.php new file mode 100644 index 0000000..88b7c07 --- /dev/null +++ b/www/src/fontawesome.php @@ -0,0 +1,17 @@ + '', + 'markdown' => '', + 'image' => '', + 'images' => '', + 'turn-up' => '', + 'rectangle-list' => '', + 'folder-tree' => '', + 'folder' => '', + 'folder-open' => '', + 'rss' => '', + 'magnifying-glass' => '' +]; + +?> \ No newline at end of file diff --git a/www/mysql_adapter.php b/www/src/old_mysql_adapter.php similarity index 97% rename from www/mysql_adapter.php rename to www/src/old_mysql_adapter.php index 3d845b6..7a98c09 100644 --- a/www/mysql_adapter.php +++ b/www/src/old_mysql_adapter.php @@ -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; diff --git a/www/router.php b/www/src/old_router.php similarity index 75% rename from www/router.php rename to www/src/old_router.php index 18c4641..e15593d 100644 --- a/www/router.php +++ b/www/src/old_router.php @@ -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
'; + echo 'Error number: ' . mysqli_connect_errno() . '
'; + echo 'Error message: ' . mysqli_connect_error() . '
'; + 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); } } diff --git a/www/src/router.php b/www/src/router.php new file mode 100644 index 0000000..b3e62b4 --- /dev/null +++ b/www/src/router.php @@ -0,0 +1,35 @@ + \ No newline at end of file diff --git a/www/src/serve/ajax.php b/www/src/serve/ajax.php new file mode 100644 index 0000000..80709ea --- /dev/null +++ b/www/src/serve/ajax.php @@ -0,0 +1,36 @@ +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); + +?> \ No newline at end of file diff --git a/www/src/serve/api.php b/www/src/serve/api.php new file mode 100644 index 0000000..b108ec0 --- /dev/null +++ b/www/src/serve/api.php @@ -0,0 +1,137 @@ +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; +} + + +?> \ No newline at end of file diff --git a/www/src/serve/post.php b/www/src/serve/post.php new file mode 100644 index 0000000..4c409d3 --- /dev/null +++ b/www/src/serve/post.php @@ -0,0 +1,134 @@ + $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); +} + + +?> diff --git a/www/src/setup/analytics.php b/www/src/setup/analytics.php new file mode 100644 index 0000000..edeb50e --- /dev/null +++ b/www/src/setup/analytics.php @@ -0,0 +1,112 @@ +log_path_access($REQUEST_PATH, + deduce_user_agent(), + $referrer, + $compute_time, $analytics_return_status); + + if($analytics_enable_tail) { + echo ""; + } + + if(isset($analytics_post)) { + $analytics_post->increment_counter("compute_time", $compute_time); + + if(analytics_is_user()) { + $analytics_post->increment_counter("views"); + } + } +}); + +?> \ No newline at end of file diff --git a/www/src/setup/db.php b/www/src/setup/db.php new file mode 100644 index 0000000..52c5574 --- /dev/null +++ b/www/src/setup/db.php @@ -0,0 +1,70 @@ +'; + echo 'Error number: ' . mysqli_connect_errno() . '
'; + echo 'Error message: ' . mysqli_connect_error() . '
'; + 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']; + +?> \ No newline at end of file diff --git a/www/src/setup/derg_insert.php b/www/src/setup/derg_insert.php new file mode 100644 index 0000000..b70f821 --- /dev/null +++ b/www/src/setup/derg_insert.php @@ -0,0 +1,46 @@ +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); + } +} + +?> \ No newline at end of file diff --git a/www/src/setup/permissions.php b/www/src/setup/permissions.php new file mode 100644 index 0000000..654f5a8 --- /dev/null +++ b/www/src/setup/permissions.php @@ -0,0 +1,26 @@ + 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']; +} + +?> \ No newline at end of file diff --git a/www/src/setup/site_config.php b/www/src/setup/site_config.php new file mode 100644 index 0000000..683cb91 --- /dev/null +++ b/www/src/setup/site_config.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/www/src/setup/twig.php b/www/src/setup/twig.php new file mode 100644 index 0000000..e674428 --- /dev/null +++ b/www/src/setup/twig.php @@ -0,0 +1,24 @@ + 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()); + } + } +}); + + +?> \ No newline at end of file diff --git a/www/static/.htaccess b/www/static/.htaccess index 1c117e2..cf3ad7c 100644 --- a/www/static/.htaccess +++ b/www/static/.htaccess @@ -3,9 +3,9 @@ Allow from all Options +Indexes - Header set Cache-Control "max-age=315360, public" + Header set Cache-Control "max-age=60, public" - Header set Cache-Control "max-age=315360, public" + Header set Cache-Control "max-age=60, public" \ No newline at end of file diff --git a/www/static/article_blop.css b/www/static/article_blop.css new file mode 100644 index 0000000..1c46c37 --- /dev/null +++ b/www/static/article_blop.css @@ -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); + + +} \ No newline at end of file diff --git a/www/static/banner.js b/www/static/banner.js index 1dc78c1..5ef9ee3 100644 --- a/www/static/banner.js +++ b/www/static/banner.js @@ -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() +} diff --git a/www/static/dergstyle.css b/www/static/dergstyle.css index ccde5fa..96b3726 100644 --- a/www/static/dergstyle.css +++ b/www/static/dergstyle.css @@ -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%; + } } diff --git a/www/static/htmx.min.js b/www/static/htmx.min.js new file mode 100644 index 0000000..59937d7 --- /dev/null +++ b/www/static/htmx.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){return e.getRootNode({composed:true})===document}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){O(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function u(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return u(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(t,r,n){if(r.indexOf("global ")===0){return p(t,r.slice(7),true)}t=y(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=ge(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),ge(r.substr(8)))}else if(r.indexOf("find ")===0){e=u(f(t),ge(r.substr(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,ge(r.substr(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,ge(r.substr(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=m(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=f(m(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return u(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){O('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(o(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!he(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){he(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=u("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=u("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=u("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","
");e=u("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ae(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ne(f(e));he(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function a(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}he(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function C(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=C(e,Qe).trim();e.shift()}else{t=C(e,v)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{C(o,w);const l=o.length;const c=C(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};C(o,w);u.pollInterval=d(C(o,/[,\[\s]/));C(o,w);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}C(o,w);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=d(C(o,v))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=C(o,v);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=d(C(o,v))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=C(o,v)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=C(o,v)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=ne().location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){b(n);return}de(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(h(n,'input[type="submit"], button')&&(h(n,"[form]")||g(n,"form")!==null)){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(a||ht(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){he(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){he(l,"htmx:trigger");c(l,e)},u.delay)}else{he(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){he(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){he(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;he(e,"htmx:trigger");t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(r,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){b(n);return}de(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Nt(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){he(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;he(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;he(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){he(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});he(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!h(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:An(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function dn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!dn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.slice(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{O("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function R(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return de(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||r.source&&!e&&!y(r.source)){e=ve}return de(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return de(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return he(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}else{return e[t]}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function de(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const A=ee(a,"formmethod");if(A!=null){if(A.toLowerCase()!=="dialog"){t=A}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return de(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(he(r,"htmx:confirm",G)===false){oe(s);return e}}let h=r;let d=re(r,"hx-sync");let g=null;let F=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ue(ae(r,I))}d=(N[1]||"drop").trim();u=ie(h);if(d==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(d==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(d==="replace"){he(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){he(h,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){de(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!he(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=hn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:An(w),unfilteredFormData:v,unfilteredParameters:An(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!he(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){he(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}he(r,"htmx:afterRequest",H);he(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){he(e,"htmx:afterRequest",H);he(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!he(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){he(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});he(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function Nn(e,t){const n=t.xhr;let r=null;let o=null;if(R(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(R(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(R(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){he(e,"htmx:restored",{document:ne(),triggerEvent:he})})}else{if(n){n(e)}}};E().setTimeout(function(){he(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/www/static/modest.css b/www/static/modest.css index d45177d..f59b797 100644 --- a/www/static/modest.css +++ b/www/static/modest.css @@ -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; + } } \ No newline at end of file diff --git a/www/static/styles/age_gate.css b/www/static/styles/age_gate.css new file mode 100644 index 0000000..bc4728f --- /dev/null +++ b/www/static/styles/age_gate.css @@ -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; + } +} \ No newline at end of file diff --git a/www/static/styles/post_navbar.css b/www/static/styles/post_navbar.css new file mode 100644 index 0000000..4d125de --- /dev/null +++ b/www/static/styles/post_navbar.css @@ -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; + } + } + } +} diff --git a/www/static/styles/search.css b/www/static/styles/search.css new file mode 100644 index 0000000..0141876 --- /dev/null +++ b/www/static/styles/search.css @@ -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; + } + } +} \ No newline at end of file diff --git a/www/static/styles/toc.css b/www/static/styles/toc.css new file mode 100644 index 0000000..ecbb9bf --- /dev/null +++ b/www/static/styles/toc.css @@ -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; + } +} \ No newline at end of file diff --git a/www/static/three-dots.svg b/www/static/three-dots.svg new file mode 100644 index 0000000..47092c2 --- /dev/null +++ b/www/static/three-dots.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/static/toc.js b/www/static/toc.js new file mode 100644 index 0000000..66aeb36 --- /dev/null +++ b/www/static/toc.js @@ -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 = " #" + pathItem.level + '' + pathItem.name + '' + + 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 = "" + element.name + ""; + 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()); diff --git a/www/templates/_dev_add_yap.html b/www/templates/_dev_add_yap.html new file mode 100644 index 0000000..15662ef --- /dev/null +++ b/www/templates/_dev_add_yap.html @@ -0,0 +1,36 @@ + + +{% extends "root.html" %} + +{% block second_title %} +

YAP SOMETHIN'

+{% endblock %} + +{%block main_content%} +
+
+ + + + + + + + + + + + +
+ + Return data: + + + +
+{%endblock%} diff --git a/www/templates/post_types/rrror.html b/www/templates/derg_error.html similarity index 100% rename from www/templates/post_types/rrror.html rename to www/templates/derg_error.html diff --git a/www/templates/fragments/blog/card.html b/www/templates/fragments/blog/card.html new file mode 100644 index 0000000..f084f0a --- /dev/null +++ b/www/templates/fragments/blog/card.html @@ -0,0 +1,22 @@ + + +
+
+ +
+

+ {{post.title}} +

+ +
    + {% for tag in post.tags %} +
  • {{ tag }}
  • + {% endfor %} +
+ + + {{ post.brief }} + +
+
+
\ No newline at end of file diff --git a/www/templates/fragments/blog/listing.html b/www/templates/fragments/blog/listing.html new file mode 100644 index 0000000..003848f --- /dev/null +++ b/www/templates/fragments/blog/listing.html @@ -0,0 +1,4 @@ + +{% for post in search_results %} + {{ include('fragments/blog/card.html') }} +{% endfor %} \ No newline at end of file diff --git a/www/templates/fragments/decoration/blog_header.html b/www/templates/fragments/decoration/blog_header.html new file mode 100644 index 0000000..7dd4d48 --- /dev/null +++ b/www/templates/fragments/decoration/blog_header.html @@ -0,0 +1,10 @@ + +
+

+ {{ post.title }} +

+ + {% if post.authors %} + Written by {{ post.authors }} + {% endif %} +
\ No newline at end of file diff --git a/www/templates/fragments/decoration/navbar.html b/www/templates/fragments/decoration/navbar.html new file mode 100644 index 0000000..f8b65e4 --- /dev/null +++ b/www/templates/fragments/decoration/navbar.html @@ -0,0 +1,96 @@ + diff --git a/www/templates/fragments/directory/compact/entry.html b/www/templates/fragments/directory/compact/entry.html new file mode 100644 index 0000000..1d2b40b --- /dev/null +++ b/www/templates/fragments/directory/compact/entry.html @@ -0,0 +1,27 @@ + + +
  • + {% set folder_key = random() %} + + + + + + {{ post.basename }} - {{ post.title }} + + +
      +
    • + + Loading... + +
    • +
    +
  • \ No newline at end of file diff --git a/www/templates/fragments/directory/compact/listing.html b/www/templates/fragments/directory/compact/listing.html new file mode 100644 index 0000000..6471614 --- /dev/null +++ b/www/templates/fragments/directory/compact/listing.html @@ -0,0 +1,4 @@ + +{% for post in page.child_posts %} +{{ include('fragments/directory/compact/entry.html') }} +{% endfor %} \ No newline at end of file diff --git a/www/templates/fragments/directory/inline.html b/www/templates/fragments/directory/inline.html new file mode 100644 index 0000000..2796d82 --- /dev/null +++ b/www/templates/fragments/directory/inline.html @@ -0,0 +1,39 @@ +

    Directory contents for {{page.basename}}:

    + + + + + + + + + + + + + + + {% for post in page.child_posts %} + + + + + + + {% endfor %} +
    NameTitleModified
    + {{ fa['turn-up'] | raw }} + + .. + + .. + +
    + {{ fa[post.icon] | raw }} + + {{post.basename}} + + {{ post.title }} + + {{ post.updated_at }} +
    diff --git a/www/templates/fragments/filepath_bar.html b/www/templates/fragments/filepath_bar.html deleted file mode 100644 index 31b5265..0000000 --- a/www/templates/fragments/filepath_bar.html +++ /dev/null @@ -1,33 +0,0 @@ -
    - -
  • - {{ post.post_metadata.title }} -
  • -
    - - {% set split_post = post.post_path |split('/') %} - {% for i in range(0, split_post|length - 1) %} -
  • - {% if i != 0 %} - - > {{ split_post[i] }} - - {% else %} - root - {% endif %} -
  • - {% endfor %} - - -
  • - raw - api -
  • -
  • - - {{ fa['rss']|raw }} - -
  • -
    -
    \ No newline at end of file diff --git a/www/templates/fragments/search/inline.html b/www/templates/fragments/search/inline.html new file mode 100644 index 0000000..c6c985c --- /dev/null +++ b/www/templates/fragments/search/inline.html @@ -0,0 +1,10 @@ + +{% for post in search_results %} + {# We can't boost because text-fragments HAVE to be user-inited! #} +
  • + {{post.title}} +
  • +
    +{% endfor %} \ No newline at end of file diff --git a/www/templates/gallery/gallery_entry.html b/www/templates/gallery/gallery_entry.html deleted file mode 100644 index 991901d..0000000 --- a/www/templates/gallery/gallery_entry.html +++ /dev/null @@ -1,28 +0,0 @@ - -{% extends "root.html" %} - -{% block extra_head %} - -{% endblock %} - -{% block second_title %} -

    Gallery

    -{% endblock %} - -{%block main_content%} -
    - - - - - - -
    -{%endblock%} diff --git a/www/templates/gallery/gallery_overview.html b/www/templates/gallery/gallery_overview.html deleted file mode 100644 index 91543b9..0000000 --- a/www/templates/gallery/gallery_overview.html +++ /dev/null @@ -1,18 +0,0 @@ - -{% extends "root.html" %} - -{% block extra_head %} - -{% endblock %} - -{% block second_title %} -

    Gallery

    -{% endblock %} - -{%block main_content%} -