Compare commits

...

9 commits

22 changed files with 437 additions and 157 deletions

View file

@ -4,7 +4,7 @@ WORKDIR /app
COPY www/composer.* .
COPY www/vendor/* vendor/
FROM php:8.2-apache
FROM php:8.3-apache
WORKDIR /var/www/html
COPY --from=0 /app/ ./

View file

@ -8,11 +8,9 @@ USE dragon_fire;
-- DROP TABLE path_errcodes;
-- DROP TABLE feed_cache;
CREATE TABLE posts (
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,
@ -29,13 +27,13 @@ CREATE TABLE posts (
post_settings_cache 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(host, post_path_depth, post_path),
INDEX(post_path),
INDEX(post_path_depth, post_path),
INDEX(host, post_created_at),
INDEX(host, post_updated_at),
INDEX(post_created_at),
INDEX(post_updated_at),
FULLTEXT(post_path),
FULLTEXT(post_tags),
@ -43,13 +41,13 @@ CREATE TABLE posts (
FULLTEXT(post_brief)
);
CREATE TABLE post_markdown (
CREATE TABLE dev_post_markdown (
post_id INTEGER,
post_markdown TEXT,
PRIMARY KEY(post_id),
FOREIGN KEY(post_id) REFERENCES posts(post_id)
FOREIGN KEY(post_id) REFERENCES dev_posts(post_id)
ON DELETE CASCADE,
FULLTEXT(post_markdown)

View file

@ -11,7 +11,8 @@
"conventionalCommits.scopes": [
"search",
"templates",
"css"
"css",
"database"
]
}
}

View file

@ -17,6 +17,10 @@ RewriteRule ^/?raw/(.*)$ raw/%{HTTP_HOST}/$1 [L,END]
# RewriteCond %{HTTPS} !on
# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,END]
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
@ -30,4 +34,4 @@ Options +Indexes
<filesMatch ".(js)$">
Header set Cache-Control "max-age=60, public"
</filesMatch>
</filesMatch>

97
www/composer.lock generated
View file

@ -184,16 +184,16 @@
},
{
"name": "laminas/laminas-escaper",
"version": "2.14.0",
"version": "2.15.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-escaper.git",
"reference": "0f7cb975f4443cf22f33408925c231225cfba8cb"
"reference": "c612b0488ae486284c39885efca494c180f16351"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/0f7cb975f4443cf22f33408925c231225cfba8cb",
"reference": "0f7cb975f4443cf22f33408925c231225cfba8cb",
"url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/c612b0488ae486284c39885efca494c180f16351",
"reference": "c612b0488ae486284c39885efca494c180f16351",
"shasum": ""
},
"require": {
@ -205,12 +205,12 @@
"zendframework/zend-escaper": "*"
},
"require-dev": {
"infection/infection": "^0.27.9",
"laminas/laminas-coding-standard": "~3.0.0",
"infection/infection": "^0.27.11",
"laminas/laminas-coding-standard": "~3.0.1",
"maglnet/composer-require-checker": "^3.8.0",
"phpunit/phpunit": "^9.6.16",
"phpunit/phpunit": "^9.6.22",
"psalm/plugin-phpunit": "^0.19.0",
"vimeo/psalm": "^5.21.1"
"vimeo/psalm": "^5.26.1"
},
"type": "library",
"autoload": {
@ -242,7 +242,7 @@
"type": "community_bridge"
}
],
"time": "2024-10-24T10:12:53+00:00"
"time": "2024-12-17T19:39:54+00:00"
},
{
"name": "laminas/laminas-feed",
@ -519,16 +519,16 @@
},
{
"name": "league/commonmark",
"version": "2.5.3",
"version": "2.6.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "b650144166dfa7703e62a22e493b853b58d874b0"
"reference": "d990688c91cedfb69753ffc2512727ec646df2ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/b650144166dfa7703e62a22e493b853b58d874b0",
"reference": "b650144166dfa7703e62a22e493b853b58d874b0",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad",
"reference": "d990688c91cedfb69753ffc2512727ec646df2ad",
"shasum": ""
},
"require": {
@ -553,8 +553,9 @@
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0 || ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.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.6-dev"
"dev-main": "2.7-dev"
}
},
"autoload": {
@ -621,7 +622,7 @@
"type": "tidelift"
}
],
"time": "2024-08-16T11:46:16+00:00"
"time": "2024-12-29T14:10:59+00:00"
},
{
"name": "league/config",
@ -1062,12 +1063,12 @@
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
@ -1136,8 +1137,8 @@
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1215,8 +1216,8 @@
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1289,8 +1290,8 @@
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1369,8 +1370,8 @@
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1427,16 +1428,16 @@
},
{
"name": "symfony/yaml",
"version": "v7.2.0",
"version": "v7.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "099581e99f557e9f16b43c5916c26380b54abb22"
"reference": "ac238f173df0c9c1120f862d0f599e17535a87ec"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/099581e99f557e9f16b43c5916c26380b54abb22",
"reference": "099581e99f557e9f16b43c5916c26380b54abb22",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec",
"reference": "ac238f173df0c9c1120f862d0f599e17535a87ec",
"shasum": ""
},
"require": {
@ -1479,7 +1480,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.2.0"
"source": "https://github.com/symfony/yaml/tree/v7.2.3"
},
"funding": [
{
@ -1495,20 +1496,20 @@
"type": "tidelift"
}
],
"time": "2024-10-23T06:56:12+00:00"
"time": "2025-01-07T12:55:42+00:00"
},
{
"name": "twig/markdown-extra",
"version": "v3.16.0",
"version": "v3.19.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/markdown-extra.git",
"reference": "25f23c02936f8c7157a8413154c06a462c9c20d3"
"reference": "6c464fc3e016ada9f17be4511daf2576ba4085c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/25f23c02936f8c7157a8413154c06a462c9c20d3",
"reference": "25f23c02936f8c7157a8413154c06a462c9c20d3",
"url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/6c464fc3e016ada9f17be4511daf2576ba4085c5",
"reference": "6c464fc3e016ada9f17be4511daf2576ba4085c5",
"shasum": ""
},
"require": {
@ -1517,7 +1518,7 @@
"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",
@ -1555,7 +1556,7 @@
"twig"
],
"support": {
"source": "https://github.com/twigphp/markdown-extra/tree/v3.16.0"
"source": "https://github.com/twigphp/markdown-extra/tree/v3.19.0"
},
"funding": [
{
@ -1567,20 +1568,20 @@
"type": "tidelift"
}
],
"time": "2024-09-03T20:17:35+00:00"
"time": "2025-01-19T15:54:05+00:00"
},
{
"name": "twig/twig",
"version": "v3.16.0",
"version": "v3.19.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "475ad2dc97d65d8631393e721e7e44fb544f0561"
"reference": "d4f8c2b86374f08efc859323dbcd95c590f7124e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/475ad2dc97d65d8631393e721e7e44fb544f0561",
"reference": "475ad2dc97d65d8631393e721e7e44fb544f0561",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/d4f8c2b86374f08efc859323dbcd95c590f7124e",
"reference": "d4f8c2b86374f08efc859323dbcd95c590f7124e",
"shasum": ""
},
"require": {
@ -1635,7 +1636,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.16.0"
"source": "https://github.com/twigphp/Twig/tree/v3.19.0"
},
"funding": [
{
@ -1647,7 +1648,7 @@
"type": "tidelift"
}
],
"time": "2024-11-29T08:27:05+00:00"
"time": "2025-01-29T07:06:14+00:00"
}
],
"packages-dev": [],

View file

@ -15,14 +15,17 @@ class MySQLHandler
CONST SQL_WRITE_COLUMNS = ['path', 'title', 'brief'];
private $sql_connection;
private $db_prefix;
public $hostname;
public $debugging;
function __construct($sql_connection, $hostname) {
function __construct($sql_connection, $hostname, $db_prefix) {
$this->sql_connection = $sql_connection;
$this->hostname = $hostname;
$this->db_prefix = $db_prefix;
$this->debugging = false;
}
@ -48,10 +51,10 @@ class MySQLHandler
$post_path = sanitize_post_path($post_path);
$this->_exec("
UPDATE posts
UPDATE {$this->db_prefix}_posts
SET post_settings_cache=NULL
WHERE host = ? AND post_path LIKE ?;
", "ss", $this->hostname, $post_path . "%");
WHERE post_path LIKE ?;
", "s", $post_path . "%");
}
public function stub_postdata($path) {
@ -60,14 +63,13 @@ class MySQLHandler
$qry = "
INSERT INTO posts
(host, post_path, post_path_depth)
INSERT INTO {$this->db_prefix}_posts
(post_path, post_path_depth)
VALUES
( ?, ?, ?) AS new
( ?, ?) AS new
ON DUPLICATE KEY UPDATE post_path=new.post_path;";
$this->_exec($qry, "ssi",
$this->hostname,
$this->_exec($qry, "si",
$post_path,
$path_depth);
}
@ -108,7 +110,6 @@ class MySQLHandler
);
$sql_args = [
$this->hostname,
$post_path,
substr_count($post_path, "/"),
$data['title'],
@ -126,13 +127,12 @@ class MySQLHandler
array_push($sql_args, json_encode($data));
$qry =
"INSERT INTO posts
(host,
post_path, post_path_depth,
"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
( ?, ?, ?, ?, ?, ?, null) AS new
ON DUPLICATE KEY
UPDATE post_title=new.post_title,
post_tags=new.post_tags,
@ -141,7 +141,7 @@ class MySQLHandler
post_updated_at=CURRENT_TIMESTAMP;
";
$this->_exec($qry, "ssissss", ...$sql_args);
$this->_exec($qry, "sissss", ...$sql_args);
if(isset($post_markdown)) {
$this->set_post_markdown($this->sql_connection->insert_id, $post_markdown);
@ -152,7 +152,7 @@ class MySQLHandler
public function set_post_markdown($id, $markdown) {
$qry =
"INSERT INTO post_markdown ( post_id, post_markdown )
"INSERT INTO {$this->db_prefix}_post_markdown ( post_id, post_markdown )
VALUES (?, ?) AS new
ON DUPLICATE KEY UPDATE post_markdown=new.post_markdown;
";
@ -167,9 +167,9 @@ class MySQLHandler
$post_settings = $this->_exec("
SELECT post_settings_cache
FROM posts
WHERE post_path = ? AND host = ?
", "ss", $post_path, $this->hostname)->fetch_assoc();
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");
@ -192,9 +192,9 @@ class MySQLHandler
$post_settings = [];
$post_metadata = $this->_exec("
SELECT post_path, post_metadata
FROM posts
WHERE post_path = ? AND host = ?
", "ss", $post_path, $this->hostname)->fetch_assoc();
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);
@ -209,9 +209,9 @@ class MySQLHandler
$this->_dbg("-> gps: Merged post settings are " . json_encode($post_settings) . ", saving...\n");
$this->_exec("
UPDATE posts SET post_settings_cache=? WHERE post_path=? AND host=?
", "sss",
json_encode($post_settings), $post_path, $this->hostname);
UPDATE {$this->db_prefix}_posts SET post_settings_cache=? WHERE post_path=?
", "ss",
json_encode($post_settings), $post_path);
return $post_settings;
}
@ -245,6 +245,7 @@ class MySQLHandler
}
$outdata = array_merge($post_settings, $post_metadata, $outdata);
$outdata['host'] ??= $this->hostname;
return $outdata;
}
@ -254,11 +255,11 @@ class MySQLHandler
$qry = "
SELECT *
FROM posts
WHERE post_path = ? AND host = ?;
FROM {$this->db_prefix}_posts
WHERE post_path = ?;
";
$data = $this->_exec($qry, "ss", $path, $this->hostname)->fetch_assoc();
$data = $this->_exec($qry, "s", $path)->fetch_assoc();
return $this->process_postdata($data);
}
@ -291,7 +292,7 @@ class MySQLHandler
$qry = "
SELECT *
FROM posts
FROM {$this->db_prefix}_posts
WHERE post_path_depth BETWEEN ? AND ?
AND post_path LIKE ?
ORDER BY " . $order_by .
@ -314,17 +315,17 @@ class MySQLHandler
public function get_post_markdown($id) {
$qry =
"SELECT post_markdown
FROM post_markdown
FROM {$this->db_prefix}_post_markdown
WHERE post_id = ?
";
$data = $this->_exec($qry, "i", $id)->fetch_assoc();
if(!isset($data)) {
return "";
return '';
}
return $data['post_markdown'];
return $data['post_markdown'] ?? '';
}
public function parse_search_query_string($text) {
@ -439,7 +440,7 @@ class MySQLHandler
if(strlen($options['text']) > 0) {
$text_search_scores = [0];
$text_search_wheres = [];
foreach([['title', 6], ['brief', 4], ['markdown', 1]] as $arg) {
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';
@ -462,15 +463,16 @@ class MySQLHandler
}
if(count($qry_wheres) == 0) {
throw new Exception("No search filtering options supplied!");
return [];
}
$options['offset'] ??= 0;
$qry =
"SELECT " . implode(', ', $qry_selects) . "
FROM posts
LEFT JOIN post_markdown ON posts.post_id = post_markdown.post_id
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 post_search_score DESC
LIMIT " . $options['limit'] . "

View file

@ -96,9 +96,6 @@ class Post implements ArrayAccess {
$data['basename'] ??= 'root';
}
$post_data['host'] ??= 'localhost:8081';
$data['url'] ??= 'http://' . $post_data['host'] . $post_data['path'];
$data['basename'] ??= basename($data['path']);
$data['title'] ??= basename($data['path']);
@ -107,6 +104,17 @@ class Post implements ArrayAccess {
$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']);
@ -234,4 +242,4 @@ class Post implements ArrayAccess {
}
}
?>
?>

View file

@ -10,7 +10,8 @@ $FONT_AWESOME_ARRAY=[
'folder-tree' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32v96V384c0 35.3 28.7 64 64 64H256V384H64V160H256V96H64V32zM288 192c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4L409.4 9.4c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V192zm0 288c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V352c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4l-13.3-13.3c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V480z"/></svg>',
'folder' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z"/></svg>',
'folder-open' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M88.7 223.8L0 375.8V96C0 60.7 28.7 32 64 32H181.5c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7H416c35.3 0 64 28.7 64 64v32H144c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224H544c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480H32c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z"/></svg>',
'rss' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" class="fa-icn" width="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>'
'rss' => '<svg xmlns="http://www.w3.org/2000/svg" height="16" class="fa-icn" width="14" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>',
'magnifying-glass' => '<svg xmlns="http://www.w3.org/2000/svg" class="fa-icn" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>'
];
?>

View file

@ -17,8 +17,13 @@ if(isset($REQUEST_QUERY['page'])) {
$ajax_args['page'] = $adapter->get_post($REQUEST_QUERY['page']);
}
if(isset($REQUEST_QUERY['query'])) {
$REQUEST_QUERY['search'] ??= $REQUEST_QUERY['query'];
}
if(isset($REQUEST_QUERY['search'])) {
$ajax_args['search_results'] = $post->handler->search_posts($REQUEST_QUERY['search']);
$ajax_args['search_query'] = $REQUEST_QUERY['search'];
$ajax_args['search_results'] = $adapter->search_posts($REQUEST_QUERY['search']);
}

View file

@ -48,7 +48,7 @@ switch($API_FUNCTION) {
$file_dir = dirname($physical_file_path);
if(!is_dir($file_dir)) {
mkdir(dirname($physical_file_path), recursive: true);
mkdir($file_dir, recursive: true);
}
move_uploaded_file($_FILES['file']['tmp_name'], $physical_file_path);

View file

@ -18,7 +18,8 @@ function render_root_template($template, $args = []) {
"site_name" => $page['site_name'] ?? 'Nameless Site',
"title" => $page['title'] ?? 'Titleless',
"url" => $page['url'] ?? $page['path'] ?? 'No URL set',
"description" => $page['description'] ?? 'No description set'
"description" => $page['brief'] ?? $page['description'] ?? 'No description set',
"image" => $page['preview_image'] ?? $page['banners'][0]
];
$args['banners'] = json_encode($page['banners'] ?? []);
@ -114,4 +115,4 @@ if(!isset($post)) {
}
?>
?>

View file

@ -25,7 +25,9 @@ try {
die();
}
$sql_adapter = new MySQLHandler($db_connection, $SERVER_HOST);
$sql_adapter = new MySQLHandler($db_connection,
$SITE_CONFIG['site_defaults']['uri_prefix'],
$db_params['prefix']);
$adapter = new PostHandler($sql_adapter);
require_once 'dergdown.php';

View file

@ -29,6 +29,7 @@ body {
--highlight_0: #ee9015b1;
--highlight_1: #ee9015;
--highlight_1a: #ee901540;
--highlight_2: #edd29e;
--text_1: #FFFFFF;
@ -39,8 +40,10 @@ body {
--content-padding: max(0.5rem, min(1rem, var(--content-total-margin)));
--content-margin: max(0px, calc(var(--content-total-margin) - 1rem));
color: var(--text_1);
width: 100vw;
color: var(--text_1);
background: var(--bg_1);
margin: 0px;
@ -48,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) {
@ -68,6 +76,23 @@ body {
}
}
/* 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;
@ -140,8 +165,11 @@ 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 {
@ -174,6 +202,7 @@ a:hover {
min-height: 100%;
background: var(--bg_2);
min-width: 0;
}
body::before {
@ -197,10 +226,37 @@ body::before {
z-index: 10;
pointer-events: none;
transition: opacity 0.2s;
transition: opacity 0.2s;
}
body.htmx-request::before {
opacity: 1;
.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;
}

File diff suppressed because one or more lines are too long

View file

@ -200,4 +200,7 @@ article {
& ol {
padding-left: 2em;
}
& li {
margin-bottom: 1em;
}
}

View file

@ -30,12 +30,112 @@
}
}
& > ._path {
& ._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;
padding-left: 0.5rem;
background: var(--highlight_1);
@ -49,7 +149,7 @@
overflow-y: hidden;
white-space: nowrap;
& a {
color: var(--text_1);
padding-right: 0.2rem;

View file

@ -15,7 +15,7 @@
box-shadow: -0.2em 0.2em 0.2em black;
--toc-fg: #cecece;
--toc-bg: #EE901544;
--toc-bg: var(--highlight_1a);
background-color: var(--bg_2);
@ -37,7 +37,7 @@
color: var(--toc-fg) !important;
}
& .active {
& .active > a {
background-color: var(--toc-bg);
border-bottom: 1px solid var(--highlight_1);
@ -57,4 +57,11 @@
margin-bottom: 0.2em;
transition: all 1s;
}
& .toc_collapsing {
display: none;
}
& li:is(:has(.active),.active) > ol > .toc_collapsing {
display: block;
}
}

View file

@ -7,7 +7,7 @@ let tocId = "toc";
const TOC_IDENTIFIER = "toc";
const TOC_MAIN_CONTENT_ID = "#content_article"
const TOC_TOP_PIXEL_MARGIN = 300;
const TOC_TOP_PIXEL_MARGIN = 150;
class TocTracker {
known_heading_elements = [];
@ -49,14 +49,25 @@ class TocTracker {
throw Error("A `main` tag section is required to query headings from.");
}
let headings = main.querySelectorAll("h1, h2, h3, h4, h5, h6");
let headings = main.querySelectorAll("h1, h2, h3, h4, h5, h6, li strong");
headings.forEach((heading, index) => {
const heading_level = parseInt(heading.tagName.slice(-1));
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.innerHTML,
name: heading_name,
dom: heading,
collapse: heading_collapsed
});
});
}
@ -65,6 +76,23 @@ class TocTracker {
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;
});
}
@ -182,21 +210,19 @@ class TocNavBarUpdater {
trackingRebuildCallback() {
this._removeNavbarElements();
this.navbar_dom = document.querySelector('#main_navbar_path');
this.navbar_dom = document.querySelector('#main_navbar_path_list');
}
trackingUpdateCallback(element) {
this._removeNavbarElements();
const navbar_prev_node = this.navbar_dom.children[this.navbar_dom.children.length -1];
element.path.forEach(pathItem => {
let newNode = document.createElement('li');
const pathURL = location.pathname + '#' + pathItem.id + location.search;
newNode.innerHTML = "<a href=" + pathURL + "> #<sub>" + pathItem.level + '</sub>' + pathItem.name + '</a>'
newNode.innerHTML = "<a hx-boost=false href=" + pathURL + "> #<sub>" + pathItem.level + '</sub>' + pathItem.name + '</a>'
this.navbar_dom.insertBefore(newNode, navbar_prev_node);
this.navbar_dom.appendChild(newNode);
this.added_navbar_elements.push(newNode);
});
@ -226,17 +252,37 @@ class TocSidemenu {
}
_generateSidebar() {
this.toc_tracker.known_heading_elements.forEach(element => {
let new_element = document.createElement('li');
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.style = "padding-left: " + element.level * 0.8 + "em";
new_element.innerHTML = "<a href=" + pathURL + ">" + element.name + "</a>";
new_element.innerHTML = "<a hx-boost=false style=\"padding-left: " + element.level * 0.8 + "em\" href=" + pathURL + ">" + element.name + "</a>";
if(element.collapse)
new_element.classList.add('toc_collapsing');
this.sidebar_elements[element.id] = new_element;
this.sidebar_dom.appendChild(new_element);
toc_stack[toc_stack.length-1].appendChild(new_element);
});
}
@ -253,13 +299,18 @@ class TocSidemenu {
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);
let sidebar_updater = new TocSidemenu(tracker);
document.addEventListener('DOMContentLoaded', (event) => tracker.reloadHeadings());

View file

@ -4,22 +4,51 @@
{{ page.basename }}
</li>
</menu>
<menu class="_path" id="main_navbar_path">
{% set split_post = page.path |split('/') %}
{% for i in range(0, split_post|length - 1) %}
<li>
{% if i != 0 %}
<a href="{{split_post|slice(0,i+1)|join('/')}}">
> {{ split_post[i] }}
</a>
{% else %}
<a href="/">root</a>
{% endif %}
</li>
{% endfor %}
<li style="margin-left: auto;">
<menu class="_path" id="main_navbar_path">
<ul class="_path_list" id="main_navbar_path_list">
{% set split_post = page.path |split('/') %}
{% for i in range(0, split_post|length - 1) %}
<li>
{% if i != 0 %}
<a href="{{split_post|slice(0,i+1)|join('/')}}">
> {{ split_post[i] }}
</a>
{% else %}
<a href="/">root</a>
{% endif %}
</li>
{% endfor %}
</ul>
<form class="_search_field"
action="/search"
method="GET"
style="margin-left: auto;">
{{ fa['magnifying-glass']|raw }}
<input class="_search_input"
type="text"
id="inline_search_query" name="query"
autocomplete="off"
hx-target="#inline_search_result_list"
hx-indicator="#inline_search_result_list"
hx-sync="this:replace"
hx-trigger="keypress changed delay:0.25s, input changed delay:0.25s"
hx-get="/ajax/fragments/search/inline.html">
</input>
<ul class="_search_list htmx-indicator"
id="inline_search_result_list"
tabindex="1">
Type to search...
</ul>
</form>
<li>
<label for="navbar-expander" class="expandable"> {{ fa['bars'] | raw }} </label>
<a rel="alternate" type="application/rss+xml" target="_blank"
style="padding-left: 0.3rem;" href="/feed/rss{{page.path}}">

View file

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

View file

@ -20,10 +20,6 @@
<script id="main_banner_script" src="/static/banner.js"></script>
<script src="/static/toc.js"></script>
{% if page.base %}
<base href="{{ page.base }}">
{% endif %}
{% block feed_links %}
<link rel="alternate" type="application/rss+xml" title="{{opengraph.site_name}} Global Feed" href="{{site_config.uri_prefix}}/feed">
{% endblock %}

View file

@ -42,7 +42,9 @@
<ul>
<li>Type normal words to search for post title, brief descriptions and main content</li>
<li>Use the syntax <code>tags:tag_a,tag_b,...</code> to search for specific tags</li>
<li>Use <code>path:/some/example/path</code> to limit search to a specific path </li>
<li>Use the type selector to more precisely look for images, blogs, etc.</li>
<li>Annoy the dragons to implement more metadata search tags</li>
</ul>
{% elseif display_type == 'image' %}
@ -61,14 +63,17 @@
{% endfor %}
</ul>
{%elseif display_type == 'no results' %}
<h4>How sad. There are no images yet... What a real shame :c </h4>
<h4>How sad. There are no search results here... :c </h4>
{%else%}
<ul class="dergen_search_result_listing">
{% for post in search_results %}
<li>
<ul class="_details">
<li class="_path_details"> <span>Score: {{post.search_score|round(2)}}</span> <span>Path: {{post.path}}</span> </li>
<li class="_title"> <a href="{{post.url}}" target="_blank">
<li class="_title">
<a href="{{post.url}}#:~:text={{ search_query.user_input|url_encode }}"
target="_blank"
rel="noopener">
{{post.title}} </a>
</li>
<li class="_brief"> {{post.brief}} </li>