diff options
32 files changed, 1046 insertions, 852 deletions
@@ -1,7 +1,6 @@ /debug.log test.php /.git -/htdocs/css /node_modules/ /vendor/ .DS_Store @@ -10,3 +9,5 @@ test.php config-static.php config-local.php /.idea +/htdocs/dist-css +/htdocs/dist-js diff --git a/build_static.php b/build_static.php deleted file mode 100755 index 63c6e0e..0000000 --- a/build_static.php +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env php8.1 -<?php - -function gethash(string $path): string { - return substr(sha1(file_get_contents($path)), 0, 8); -} - -function sassc(string $src_file, string $dst_file): int { - $cmd = 'sassc -t compressed '.escapeshellarg($src_file).' '.escapeshellarg($dst_file); - exec($cmd, $output, $code); - return $code; -} - -function clean_css(string $file) { - $output = $file.'.out'; - if (file_exists($output)) - unlink($output); - - $cmd = ROOT.'/node_modules/clean-css-cli/bin/cleancss -O2 "all:on;mergeSemantically:on;restructureRules:on" '.escapeshellarg($file).' > '.escapeshellarg($output); - system($cmd); - - if (file_exists($output)) { - unlink($file); - rename($output, $file); - } else { - fwrite(STDERR, "error: could not cleancss $file\n"); - } -} - -function dark_diff(string $light_file, string $dark_file): void { - $temp_output = $dark_file.'.diff'; - $cmd = ROOT.'/dark-theme-diff.js '.escapeshellarg($light_file).' '.$dark_file.' > '.$temp_output; - exec($cmd, $output, $code); - if ($code != 0) { - fwrite(STDERR, "dark_diff failed with code $code\n"); - return; - } - - unlink($dark_file); - rename($temp_output, $dark_file); -} - -require __DIR__.'/init.php'; - -function build_static(): void { - $css_dir = ROOT.'/htdocs/css'; - $hashes = []; - - if (!file_exists($css_dir)) - mkdir($css_dir); - - // 1. scss -> css - $themes = ['light', 'dark']; - $entries = ['common', 'admin']; - foreach ($themes as $theme) { - foreach ($entries as $entry) { - $input = ROOT.'/htdocs/scss/entries/'.$entry.'/'.$theme.'.scss'; - $output = $css_dir.'/'.$entry.($theme == 'dark' ? '_dark' : '').'.css'; - if (sassc($input, $output) != 0) - fwrite(STDERR, "error: could not compile entries/$entry/$theme.scss\n"); - } - } - - // 2. generate dark theme diff - foreach ($entries as $entry) { - $light_file = $css_dir.'/'.$entry.'.css'; - $dark_file = str_replace('.css', '_dark.css', $light_file); - dark_diff($light_file, $dark_file); - - // 2.1. apply cleancss (must be done _after_ css-patch) - clean_css($light_file); - clean_css($dark_file); - } - - // 3. calculate hashes - foreach (['css', 'js'] as $type) { - $reldir = ROOT.'/htdocs/'; - $entries = glob_recursive($reldir.$type.'/*.'.$type); - if (empty($entries)) { - continue; - } - foreach ($entries as $file) { - $name = preg_replace('/^'.preg_quote($reldir, '/').'/', '', $file); - $hashes[$name] = gethash($file); - } - } - logInfo($hashes); - - // 4. write config-static.php - $scfg = "<?php\n\n"; - $scfg .= "return ".var_export($hashes, true).";\n"; - - file_put_contents(ROOT.'/config-static.php', $scfg); -} - -build_static();
\ No newline at end of file diff --git a/deploy/build_common.sh b/deploy/build_common.sh new file mode 100644 index 0000000..acac5da --- /dev/null +++ b/deploy/build_common.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +set -e + +INDIR= +OUTDIR= + +error() { + >&2 echo "error: $@" +} + +warning() { + >&2 echo "warning: $@" +} + +die() { + error "$@" + exit 1 +} + +usage() { + local code="$1" + cat <<EOF +usage: $PROGNAME [OPTIONS] + +Options: + -o output directory + -i input directory + -h show this help +EOF + exit $code +} + +input_args() { + [ -z "$1" ] && usage + + while [[ $# -gt 0 ]]; do + case $1 in + -o) + OUTDIR="$2" + shift + ;; + -i) + INDIR="$2" + shift + ;; + -h) + usage + ;; + *) + die "unexpected argument: $1" + ;; + esac + shift + done +} + +check_args() { + [ -z "$OUTDIR" ] && { + error "output directory not specified" + usage 1 + } + [ -z "$INDIR" ] && { + error "input directory not specified" + usage 1 + } + + if [ ! -d "$OUTDIR" ]; then + mkdir "$OUTDIR" + else + warning "$OUTDIR already exists, erasing it" + rm "$OUTDIR"/* + fi +}
\ No newline at end of file diff --git a/deploy/build_css.sh b/deploy/build_css.sh new file mode 100755 index 0000000..2129ea2 --- /dev/null +++ b/deploy/build_css.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +PROGNAME="$0" +DIR="$( cd "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )" && pwd )" +ROOT="$(realpath "$DIR/../")" +CLEANCSS="$ROOT"/node_modules/clean-css-cli/bin/cleancss + +. $DIR/build_common.sh + +build_scss() { + local entry_name="$1" + local theme="$2" + + local input="$INDIR/entries/$entry_name/$theme.scss" + local output="$OUTDIR/$entry_name" + [ "$theme" = "dark" ] && output="${output}_dark" + output="${output}.css" + + sassc -t compressed "$input" "$output" +} + +cleancss() { + local entry_name="$1" + local theme="$2" + + local file="$OUTDIR/$entry_name" + [ "$theme" = "dark" ] && file="${file}_dark" + file="${file}.css" + + $CLEANCSS -O2 "all:on;mergeSemantically:on;restructureRules:on" "$file" > "$file.tmp" + rm "$file" + mv "$file.tmp" "$file" +} + +create_dark_patch() { + local entry_name="$1" + local light_file="$OUTDIR/$entry_name.css" + local dark_file="$OUTDIR/${entry_name}_dark.css" + + "$DIR"/gen_css_diff.js "$light_file" "$dark_file" > "$dark_file.diff" + rm "$dark_file" + mv "$dark_file.diff" "$dark_file" +} + +THEMES="light dark" +TARGETS="common admin" + +input_args "$@" +check_args + +[ -x "$CLEANCSS" ] || die "cleancss is not found" + +for theme in $THEMES; do + for target in $TARGETS; do + build_scss "$target" "$theme" + done +done + +for target in $TARGETS; do + create_dark_patch "$target" + for theme in $THEMES; do cleancss "$target" "$theme"; done +done
\ No newline at end of file diff --git a/deploy/build_js.sh b/deploy/build_js.sh new file mode 100755 index 0000000..b1019f7 --- /dev/null +++ b/deploy/build_js.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +PROGNAME="$0" +DIR="$( cd "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )" && pwd )" + +. $DIR/build_common.sh + +# suckless version of webpack +# watch and learn, bitches! +build_chunk() { + local name="$1" + local output="$OUTDIR/$name.js" + local not_first=0 + for file in "$INDIR/$name"/*.js; do + # insert newline before out comment + [ "$not_first" = "1" ] && echo "" >> "$output" + echo "/* $(basename "$file") */" >> "$output" + + cat "$file" >> "$output" + not_first=1 + done +} + +TARGETS="common admin" + +input_args "$@" +check_args + +for f in $TARGETS; do + build_chunk "$f" +done
\ No newline at end of file diff --git a/deploy.sh b/deploy/deploy.sh index 9e418e4..7ef28a3 100755 --- a/deploy.sh +++ b/deploy/deploy.sh @@ -2,9 +2,9 @@ set -e -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +DIR="$( cd "$( dirname "$(readlink -f "${BASH_SOURCE[0]}")" )" && pwd )" -DEV_DIR="${DIR}" +DEV_DIR="$(realpath "$DIR/../")" STAGING_DIR="$HOME/staging" PROD_DIR="$HOME/prod" PHP=/usr/bin/php8.1 @@ -33,7 +33,9 @@ fi cp "$DEV_DIR/config-local.php" . sed -i '/is_dev/d' ./config-local.php -$PHP build_static.php +"$DIR"/build_js.sh -i "$DEV_DIR/htdocs/js" -o "$STAGING_DIR/htdocs/dist-js" || die "build_js failed" +"$DIR"/build_css.sh -i "$DEV_DIR/htdocs/scss" -o "$STAGING_DIR/htdocs/dist-css" || die "build_css failed" +$PHP "$DIR"/gen_static_config.php > "$STAGING_DIR/config-static.php" || die "gen_static_config failed" popd @@ -43,6 +45,8 @@ rsync -a --delete --delete-excluded --info=progress2 "$STAGING_DIR/" "$PROD_DIR/ --exclude debug.log \ --exclude='/composer.*' \ --exclude='/htdocs/scss' \ + --exclude='/htdocs/js' \ --exclude='/htdocs/sass.php' \ + --exclude='/htdocs/js.php' \ --exclude='*.sh' \ --exclude='*.sql' diff --git a/dark-theme-diff.js b/deploy/gen_css_diff.js index 5ca1945..5ca1945 100755 --- a/dark-theme-diff.js +++ b/deploy/gen_css_diff.js diff --git a/deploy/gen_static_config.php b/deploy/gen_static_config.php new file mode 100755 index 0000000..09058ad --- /dev/null +++ b/deploy/gen_static_config.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php8.1 +<?php + +require __DIR__.'/../init.php'; + +if ($argc <= 1) { + usage(); + exit(1); +} + +$input_dir = null; + +array_shift($argv); +while (count($argv) > 0) { + switch ($argv[0]) { + case '-i': + array_shift($argv); + $input_dir = array_shift($argv); + break; + + default: + cli::die('unsupported argument: '.$argv[0]); + } +} + +if (is_null($input_dir)) + cli::die("input directory has not been specified"); + +$hashes = []; +foreach (['css', 'js'] as $type) { + $entries = glob_recursive($input_dir.'/dist-'.$type.'/*.'.$type); + if (empty($entries)) { + cli::error("warning: no files found in $input_dir/dist-$type"); + continue; + } + + foreach ($entries as $file) + $hashes[$type.'/'.basename($file)] = get_hash($file); +} + +echo "<?php\n\n"; +echo "return ".var_export($hashes, true).";\n"; + +function usage(): void { + global $argv; + echo <<<EOF +usage: {$argv[0]} [OPTIONS] + +Options: + -i input htdocs directory + +EOF; +} + +function get_hash(string $path): string { + return substr(sha1(file_get_contents($path)), 0, 8); +}
\ No newline at end of file diff --git a/engine/RequestDispatcher.php b/engine/RequestDispatcher.php index 3c3f684..ec31516 100644 --- a/engine/RequestDispatcher.php +++ b/engine/RequestDispatcher.php @@ -39,8 +39,8 @@ class RequestDispatcher { } $skin = new Skin(); - $skin->static[] = '/css/common.css'; - $skin->static[] = '/js/common.js'; + $skin->static[] = 'css/common.css'; + $skin->static[] = 'js/common.js'; $lang = LangData::getInstance(); $skin->addLangKeys($lang->search('/^theme_/')); diff --git a/engine/logging.php b/engine/logging.php index 1bc946b..24803cf 100644 --- a/engine/logging.php +++ b/engine/logging.php @@ -72,6 +72,9 @@ class logging { } public static function logCustom(LogLevel $level, ...$args): void { + global $config; + if (!$config['is_dev'] && $level == LogLevel::DEBUG) + return; self::write($level, self::strVars($args)); } diff --git a/handler/admin/AdminRequestHandler.php b/handler/admin/AdminRequestHandler.php index 04b7cde..5a6bd12 100644 --- a/handler/admin/AdminRequestHandler.php +++ b/handler/admin/AdminRequestHandler.php @@ -8,8 +8,8 @@ use Response; class AdminRequestHandler extends \RequestHandler { public function beforeDispatch(): ?Response { - $this->skin->static[] = '/css/admin.css'; - $this->skin->static[] = '/js/admin.js'; + $this->skin->static[] = 'css/admin.css'; + $this->skin->static[] = 'js/admin.js'; if (!($this instanceof Login) && !admin::isAdmin()) throw new \ForbiddenException('looks like you are not admin'); diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css new file mode 100644 index 0000000..286fb72 --- /dev/null +++ b/htdocs/css/admin.css @@ -0,0 +1 @@ +.admin-page{line-height:155%}
\ No newline at end of file diff --git a/htdocs/css/admin_dark.css b/htdocs/css/admin_dark.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/htdocs/css/admin_dark.css diff --git a/htdocs/css/common.css b/htdocs/css/common.css new file mode 100644 index 0000000..4a0a74b --- /dev/null +++ b/htdocs/css/common.css @@ -0,0 +1 @@ +body,input[type=password],input[type=text],textarea{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;font-size:16px}.pt h3,body{font-size:16px}.md-file-attach-icon,.pt a,.pt h3{top:1px;position:relative}.empty,.md-image.align-center{text-align:center}.contact-img,.head-logo,.md-file-attach-icon,.md-image-wrap,.pt h3{display:inline-block}.head-logo,a.head-item>span>span>span.moon-icon{left:0;position:absolute}.blog-post-text table.table-100,.blog-write-options-table,.blog-write-table{table-layout:fixed;border-collapse:collapse}.blog-post-text table.table-100 td,.blog-write-table>tbody>tr>td{vertical-align:top;text-align:left}.blog-list-table,.blog-write-options-table,.blog-write-table,table.contacts{border-collapse:collapse}.clearfix:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}body,html{padding:0;margin:0;border:0;background-color:#fff;color:#222;height:100%;min-height:100%}.base-width{max-width:900px;margin:0 auto;position:relative}.md-image,.md-image-wrap,.md-image-wrap>a,.md-images,.md-images>span,.md-video video{max-width:100%}body.full-width .base-width{max-width:100%;margin-left:auto;margin-right:auto}input[type=password],input[type=text],textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;border:1px solid #e0e0e0;border-radius:3px;background-color:#f7f7f7;color:#222;padding:6px;outline:0;-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px}a,table.contacts div.note>a:hover{color:#116fd4;text-decoration:none}input[type=password]:focus,input[type=text]:focus,textarea:focus{border-color:#e0e0e0}textarea{resize:vertical}a{outline:0}a:hover{text-decoration:underline}p,p code{line-height:150%}.unicode{font-family:sans-serif}.blog-post-text code,.blog-post-text pre,.blog-write-form textarea.form-field-input,.ff_ms,.head-logo,table.contacts td.value span{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}.blog-upload-item-actions,.fl_r{float:right}.fl_l{float:left}.pos_rel{position:relative}.pos_abs{position:absolute}.pos_fxd{position:fixed}.head,.page-content{padding:0 25px}.page-content-inner{padding:18px 0}table.contacts{border:0;margin:8px auto 0}table.contacts td{white-space:nowrap;padding-bottom:15px;vertical-align:top}table.contacts td.label{text-align:right;width:30%;color:#777}table.contacts td.value{text-align:left;padding-left:8px}table.contacts td.value span{background:#f1f1f1;padding:3px 7px 4px;border-radius:3px;color:#222;font-size:15px}table.contacts td b{font-weight:600}table.contacts td pre{padding:0;margin:0;font-size:12px}table.contacts div.note{font-size:13px;padding-top:2px;color:#777}table.contacts div.note>a{color:#777;border-bottom:1px solid #e0e0e0}.empty,.head-logo>a:hover .head-logo-path:not(.alwayshover),.pt{color:#999}table.contacts div.note>a:hover{border-bottom-color:#116fd4}.pt{margin:5px 0 20px;padding-bottom:7px;border-bottom:2px solid rgba(255,255,255,.12)}.pt h3{margin:0;font-weight:700;color:#222}.blog-post-date>a,.page-edit-links>a,.pt a:not(:first-child),.pt h3:not(:first-child){margin-left:5px}.pt a{margin-right:5px}.blog-post-text h1:first-child,.blog-post-text h2:first-child,.pt_r{margin-top:5px}.empty{padding:40px 20px;-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#f7f7f7}.md-file-attach,.md-image,.md-images{padding:3px 0}.blog-post-text pre,.head-logo-path-mapped{-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px}.contact-img{width:77px;height:12px;background:url(/img/contact.gif?1) 0 0/77px 12px no-repeat}@media (-o-min-device-pixel-ratio:3/2),(-webkit-min-device-pixel-ratio:1.5),(min--moz-device-pixel-ratio:1.5),(min-resolution:1.5dppx){.contact-img{background-image:url(/img/contact@2x.gif?1)}}.md-file-attach-icon{width:14px;height:14px;background:url(/img/attachment.svg) center center/14px 14px no-repeat;margin-right:5px}.md-file-attach-size{color:#888;margin-left:2px}.md-file-attach-note{color:#222;margin-left:2px}.md-image{line-height:0}.md-images{margin-bottom:-8px}.md-images .md-image{padding-top:0;padding-bottom:0}.md-images>span{display:block;float:left;margin:0 8px 8px 0}.md-image.align-left,td.blog-item-title-cell{text-align:left}.md-image.align-right{text-align:right}.md-image-wrap{overflow:hidden}.md-image-wrap>a{display:block}.md-image-note{line-height:150%;color:#777;padding:2px 0 4px}.language-ascii{line-height:125%!important}.head-inner{position:relative;border-bottom:2px solid #e0e0e0}.head-logo{font-size:15px;padding:16px 0;background-color:#fff}body:not(.theme-changing) .head-logo{transition:background-color .03s linear;-webkit-transition:background-color .03s linear;-moz-transition:background-color .03s linear}.head-logo:after{content:'';display:block;width:40px;position:absolute;right:-40px;top:0;bottom:0;border-left:8px solid #fff;box-sizing:border-box;background:linear-gradient(to left,rgba(255,255,255,0) 0,#fff 100%)}.head-logo>a{color:#222;font-size:14px}.head-logo>a:hover,a.head-item:hover{text-decoration:none}.head-logo-enter{background:#f3f3f3;color:#333;display:inline;opacity:0;font-size:11px;position:relative;padding:2px 5px;font-weight:400;vertical-align:middle;top:-1px}body:not(.theme-changing) .head-logo-enter{transition:opacity .03s linear;-webkit-transition:opacity .03s linear;-moz-transition:opacity .03s linear}.head-logo-enter-icon{width:12px;height:7px;display:inline-block;margin-right:5px;position:relative;top:1px}.head-logo-enter-icon>svg path{fill:#333}.head-logo>a:hover .head-logo-enter{opacity:1}.head-logo-path{color:#222;font-weight:700;-webkit-font-smoothing:antialiased}body:not(.theme-changing) .head-logo-path{transition:color .03s linear;-webkit-transition:color .03s linear;-moz-transition:color .03s linear}.head-logo-path:not(.neverhover):hover{color:#222!important}.head-logo-dolsign{color:#0bad19;font-weight:400}.head-logo-dolsign.is_root{color:#ce1a1a}.head-logo-cd{display:none}.head-logo>a:hover .head-logo-cd{display:inline}.head-logo-path-mapped{padding:3px 5px;background:#f1f1f1;pointer-events:none;border-radius:3px;margin:0 2px}.head-items{float:right;color:#777}a.head-item{color:#222;font-size:15px;display:block;float:left;padding:16px 0}a.head-item>span{position:relative;padding:0 12px;border-right:1px solid #d0d0d0}a.head-item>span>span{padding:2px 0}a.head-item>span>span>span.moon-icon>svg path{fill:#222}a.head-item.is-theme-switcher>span{padding-left:20px}a.head-item:last-child>span{border-right:0;padding-right:1px}a.head-item:hover>span>span{border-bottom:1px solid #d0d0d0}.blog-write-link-wrap{margin-bottom:18px}.blog-write-table{border:0;width:100%}.blog-write-table>tbody>tr>td:first-child{padding-right:8px;width:45%}.blog-write-table>tbody>tr>td:last-child{padding-left:8px}.blog-write-form .form-field-input{width:100%}.blog-write-form textarea.form-field-input{height:400px;font-size:12px}.blog-write-form textarea.form-field-input.nowrap{white-space:pre;overflow-wrap:normal}.blog-write-options-table{width:100%}.blog-write-options-table td{padding-top:12px}.blog-write-options-table td:first-child{width:70%}.blog-write-options-table td:nth-child(2){width:30%;padding-left:10px}.blog-item-row-year:first-child td,.blog-write-options-table tr:first-child td,.form-layout-v .form-field-wrap:first-child{padding-top:0}.blog-write-options-table button[type=submit]{margin-left:3px}.blog-write-form-toggle-link{margin-top:3px;display:inline-block}.blog-upload-form{padding-bottom:18px}.blog-upload-item{border-top:1px solid #e0e0e0;padding:10px 0}.blog-upload-item-name{font-weight:700;margin-bottom:2px}.blog-upload-item-info{color:#888;font-size:14px}.blog-upload-item-note{padding:0 0 4px}.blog-upload-item-md{margin-top:3px}.blog-post-title{margin:0 0 16px}.blog-post-title h1{font-size:22px;font-weight:700;padding:0;margin:0}.blog-post-date{color:#888;margin-top:5px;font-size:15px}.blog-post-tags{margin-top:16px;margin-bottom:-1px}.blog-post-tags>a{display:block;float:left;font-size:15px;margin-right:8px;cursor:pointer}.blog-post-tags>a:last-child{margin-right:0}.blog-post-tags>a>span{opacity:.5}.blog-post-text li{margin:13px 0}.blog-post-text p{margin-top:13px;margin-bottom:13px}.blog-post-text h3:first-child,.blog-post-text h4:first-child,.blog-post-text h5:first-child,.blog-post-text h6:first-child,.blog-post-text p:first-child,.blog-post-text td>pre:first-child{margin-top:0}.blog-post-text p:last-child,.blog-post-text td>pre:last-child{margin-bottom:0}.blog-post-text pre{background-color:#f3f3f3;overflow:auto;border-radius:3px}.blog-post-text code,.form-error{-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px}.blog-post-text code{background-color:#f1f1f1;font-size:85%;padding:3px 5px;border-radius:3px}.blog-post-text pre code{display:block;padding:12px;line-height:145%;background-color:#f3f3f3}.blog-post-text pre code span.term-prompt{color:#999;-moz-user-select:none;-webkit-user-select:none;user-select:none}.blog-post-text blockquote{border-left:3px solid #e0e0e0;margin-left:0;padding:5px 0 5px 12px;color:#888}.blog-post-text table.table-100{border:0;margin:0;width:100%}.blog-post-text table.table-100 td{padding:0 4px;border:0}.blog-post-text table.table-100 td:first-child{padding-left:0}.blog-post-text table.table-100 td:last-child{padding-right:0}.blog-post-text h1{margin:40px 0 16px;font-weight:600;font-size:30px;border-bottom:1px solid #e0e0e0;padding-bottom:8px}.blog-post-text h2{margin:35px 0 16px;font-weight:500;font-size:25px;border-bottom:1px solid #e0e0e0;padding-bottom:6px}.blog-post-text h3{margin:27px 0 16px;font-size:24px;font-weight:500}.blog-post-text h4{font-size:18px;margin:24px 0 16px}.blog-post-text h5{font-size:15px;margin:24px 0 16px}.blog-post-text h6{font-size:13px;margin:24px 0 16px;color:#666}.blog-post-text hr{height:1px;border:0;background:#e0e0e0;margin:17px 0}.blog-post-comments{margin-top:18px;padding:12px 15px;border:1px solid #e0e0e0;-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.blog-post-comments img{vertical-align:middle;position:relative;top:-1px;margin-left:2px}.index-blog-block{margin-top:23px}.blog-list.withtags{margin-right:211px}.blog-list-title{font-size:22px;margin-bottom:15px}.blog-list-title>span{margin-left:2px}.blog-list-title>span>a{font-size:16px;margin-left:2px}.blog-list-table-wrap{padding:5px 0}.blog-list-table td{vertical-align:top;padding:0 0 13px}.blog-list-table tr:last-child td,.form-layout-v .form-field-wrap:last-child{padding-bottom:0}td.blog-item-date-cell{width:1px;white-space:nowrap;text-align:right;padding-right:10px}.blog-item-date{color:#888}.blog-item-row{font-size:16px;line-height:140%}.blog-item-row.ishidden a.blog-item-title,.blog-tag-item>a{color:#222}.blog-item-row-year td{padding-top:10px;text-align:right;font-size:20px;letter-spacing:-.5px}.blog-tags{float:right;width:175px;padding-left:8px;border-left:1px solid #e0e0e0}.blog-tags-title{margin-bottom:15px;font-size:22px;padding:0 7px}.blog-tag-item{padding:6px 10px;font-size:15px}.form-error,.form-success{padding:10px 13px;margin-bottom:18px}.blog-tag-item-count{color:#aaa;margin-left:6px;text-decoration:none!important}form{display:block;margin:0}.form-layout-h .form-field-wrap{padding:8px 0}.form-layout-h .form-field-label{float:left;width:120px;text-align:right;padding:7px 0 0}.form-layout-h .form-field{margin-left:130px}.form-layout-v .form-field-wrap{padding:6px 0}.form-layout-v .form-field-label{padding:0 0 4px 4px;font-weight:700;font-size:12px;letter-spacing:.5px;text-transform:uppercase;color:#888}.form-error{background-color:#f9eeee;color:#d13d3d;border-radius:3px}.form-success{background-color:#eff5f0;color:#2a6f34;-o-border-radius:3px;-ms-border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.page-edit-links{display:none;float:right;font-size:15px}.hljs,.page-content-inner:hover .page-edit-links{display:block}.hljs{overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-meta,.hljs-strong{font-weight:700}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}@media screen and (max-width:600px){textarea{-webkit-overflow-scrolling:touch}.head-logo{position:static;display:block;overflow:hidden;white-space:nowrap;padding-bottom:0}.blog-tags,.head-logo::after{display:none}.head-items{float:none}a.head-item:active,a.head-item:hover{-webkit-tap-highlight-color:transparent!important}a.head-item:last-child>span{border-right:0;padding-right:12px}a.head-item:first-child>span{padding-left:1px}.blog-list.withtags{margin-right:0}}
\ No newline at end of file diff --git a/htdocs/css/common_dark.css b/htdocs/css/common_dark.css new file mode 100644 index 0000000..a7b4a4e --- /dev/null +++ b/htdocs/css/common_dark.css @@ -0,0 +1 @@ +.head-logo,body,html{background-color:#222}.blog-item-date,.blog-post-date,.blog-post-text blockquote,.blog-post-text pre code span.term-prompt,.blog-upload-item-info,.form-layout-v .form-field-label,.head-logo>a:hover .head-logo-path:not(.alwayshover),.md-file-attach-size,.md-image-note,table.contacts div.note,table.contacts td.label{color:#798086}.blog-item-row.ishidden a.blog-item-title,.blog-tag-item>a,.head-logo-path,.head-logo>a,.md-file-attach-note,.pt h3,a.head-item,body,html{color:#eee}.blog-post-comments{border:1px solid #48535a}.blog-post-text code,.blog-post-text pre,.blog-post-text pre code{background-color:#394146}.blog-post-text h1,.blog-post-text h2{border-bottom:1px solid #48535a}.blog-post-text hr{background:#48535a}.blog-tags{border-left:1px solid #48535a}.blog-upload-item{border-top:1px solid #48535a}.head-inner{border-bottom:2px solid #48535a}.head-logo-dolsign.is_root{color:#e23636}.head-logo-enter{background:#394146;color:#cdd3d8}.head-logo-enter-icon>svg path{fill:#CDD3D8}.head-logo-path:not(.neverhover):hover{color:#eee!important}.head-logo:after{background:linear-gradient(to left,rgba(34,34,34,0) 0,#222 100%);border-left:8px solid #222}.hljs{background:#2b2b2d;color:#cdd3d8}.hljs-addition{background:#144212}.hljs-built_in,.hljs-builtin-name,.hljs-bullet,.hljs-symbol{color:#c792ea}.hljs-comment,.hljs-quote{color:#6272a4}.hljs-deletion{background:#e6e1dc}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#cdd3d8}.hljs-meta,.hljs-section,.hljs-selector-id,.hljs-title{color:#75a5ff}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:#bd93f9}.hljs-link,.hljs-regexp{color:#f77669}.hljs-doctag,.hljs-string{color:#f1fa8c}.hljs-attribute,.hljs-name,.hljs-tag{color:#abb2bf}.hljs-class .hljs-title,.hljs-type{color:#da4939}a{color:#71abe5}a.head-item:hover>span>span{border-bottom:1px solid #5e6264}a.head-item>span{border-right:1px solid #5e6264}a.head-item>span>span>span.moon-icon>svg path{fill:#eee}input[type=password],input[type=text],textarea{background-color:#30373b;border:1px solid #48535a;color:#eee}input[type=password]:focus,input[type=text]:focus,textarea:focus{border-color:#48535a}table.contacts div.note>a{border-bottom:1px solid #48535a;color:#798086}table.contacts div.note>a:hover{border-bottom-color:#71abe5;color:#71abe5}table.contacts td.value span{background:#394146;color:#eee}
\ No newline at end of file diff --git a/htdocs/js.php b/htdocs/js.php new file mode 100644 index 0000000..c9939fe --- /dev/null +++ b/htdocs/js.php @@ -0,0 +1,31 @@ +<?php + +require __DIR__.'/../init.php'; +global $config; + +$name = $_REQUEST['name'] ?? ''; + +if (!$config['is_dev'] || !$name || !is_dir($path = ROOT.'/htdocs/js/'.$name)) { + http_response_code(403); + exit; +} + +header('Content-Type: application/javascript'); +header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); +header("Cache-Control: post-check=0, pre-check=0", false); +header("Pragma: no-cache"); + +$files = scandir($path, SCANDIR_SORT_ASCENDING); +$first = true; +foreach ($files as $file) { + if ($file == '.' || $file == '..') + continue; + // logDebug(__FILE__.': reading '.$path.'/'.$file); + if (!$first) + echo "\n"; + else + $first = false; + echo "/* $file */\n"; + if (readfile($path.'/'.$file) === false) + logError(__FILE__.': failed to readfile('.$path.'/'.$file.')'); +} diff --git a/htdocs/js/admin/00-common.js b/htdocs/js/admin/00-common.js new file mode 100644 index 0000000..999099d --- /dev/null +++ b/htdocs/js/admin/00-common.js @@ -0,0 +1 @@ +var LS = window.localStorage; diff --git a/htdocs/js/admin/10-draft.js b/htdocs/js/admin/10-draft.js new file mode 100644 index 0000000..99106f2 --- /dev/null +++ b/htdocs/js/admin/10-draft.js @@ -0,0 +1,29 @@ +var Draft = { + get: function() { + if (!LS) return null; + + var title = LS.getItem('draft_title') || null; + var text = LS.getItem('draft_text') || null; + + return { + title: title, + text: text + }; + }, + + setTitle: function(text) { + if (!LS) return null; + LS.setItem('draft_title', text); + }, + + setText: function(text) { + if (!LS) return null; + LS.setItem('draft_text', text); + }, + + reset: function() { + if (!LS) return; + LS.removeItem('draft_title'); + LS.removeItem('draft_text'); + } +};
\ No newline at end of file diff --git a/htdocs/js/admin.js b/htdocs/js/admin/11-write-form.js index a717d5c..b49a523 100644 --- a/htdocs/js/admin.js +++ b/htdocs/js/admin/11-write-form.js @@ -1,35 +1,3 @@ -var LS = window.localStorage; - -var Draft = { - get: function() { - if (!LS) return null; - - var title = LS.getItem('draft_title') || null; - var text = LS.getItem('draft_text') || null; - - return { - title: title, - text: text - }; - }, - - setTitle: function(text) { - if (!LS) return null; - LS.setItem('draft_title', text); - }, - - setText: function(text) { - if (!LS) return null; - LS.setItem('draft_text', text); - }, - - reset: function() { - if (!LS) return; - LS.removeItem('draft_title'); - LS.removeItem('draft_text'); - } -}; - var AdminWriteForm = { form: null, previewTimeout: null, @@ -170,24 +138,5 @@ var AdminWriteForm = { } } }; -bindEventHandlers(AdminWriteForm); - -var BlogUploadList = { - submitNoteEdit: function(action, note) { - if (note === null) - return; - - var form = document.createElement('form'); - form.setAttribute('method', 'post'); - form.setAttribute('action', action); - var input = document.createElement('input'); - input.setAttribute('type', 'hidden'); - input.setAttribute('name', 'note'); - input.setAttribute('value', note); - - form.appendChild(input); - document.body.appendChild(form); - form.submit(); - } -}; +bindEventHandlers(AdminWriteForm); diff --git a/htdocs/js/admin/12-upload-list.js b/htdocs/js/admin/12-upload-list.js new file mode 100644 index 0000000..5b496f6 --- /dev/null +++ b/htdocs/js/admin/12-upload-list.js @@ -0,0 +1,19 @@ +var BlogUploadList = { + submitNoteEdit: function(action, note) { + if (note === null) + return; + + var form = document.createElement('form'); + form.setAttribute('method', 'post'); + form.setAttribute('action', action); + + var input = document.createElement('input'); + input.setAttribute('type', 'hidden'); + input.setAttribute('name', 'note'); + input.setAttribute('value', note); + + form.appendChild(input); + document.body.appendChild(form); + form.submit(); + } +}; diff --git a/htdocs/js/common.js b/htdocs/js/common.js deleted file mode 100644 index 959b672..0000000 --- a/htdocs/js/common.js +++ /dev/null @@ -1,679 +0,0 @@ -if (!String.prototype.startsWith) { - String.prototype.startsWith = function(search, pos) { - pos = !pos || pos < 0 ? 0 : +pos; - return this.substring(pos, pos + search.length) === search; - }; -} - -if (!String.prototype.endsWith) { - String.prototype.endsWith = function(search, this_len) { - if (this_len === undefined || this_len > this.length) { - this_len = this.length; - } - return this.substring(this_len - search.length, this_len) === search; - }; -} - -if (!Object.assign) { - Object.defineProperty(Object, 'assign', { - enumerable: false, - configurable: true, - writable: true, - value: function(target, firstSource) { - 'use strict'; - if (target === undefined || target === null) { - throw new TypeError('Cannot convert first argument to object'); - } - - var to = Object(target); - for (var i = 1; i < arguments.length; i++) { - var nextSource = arguments[i]; - if (nextSource === undefined || nextSource === null) { - continue; - } - - var keysArray = Object.keys(Object(nextSource)); - for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { - var nextKey = keysArray[nextIndex]; - var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); - if (desc !== undefined && desc.enumerable) { - to[nextKey] = nextSource[nextKey]; - } - } - } - return to; - } - }); -} - - -// -// AJAX -// -(function() { - - var defaultOpts = { - json: true - }; - - function createXMLHttpRequest() { - if (window.XMLHttpRequest) { - return new XMLHttpRequest(); - } - - var xhr; - try { - xhr = new ActiveXObject('Msxml2.XMLHTTP'); - } catch (e) { - try { - xhr = new ActiveXObject('Microsoft.XMLHTTP'); - } catch (e) {} - } - if (!xhr) { - console.error('Your browser doesn\'t support XMLHttpRequest.'); - } - return xhr; - } - - function request(method, url, data, optarg1, optarg2) { - data = data || null; - - var opts, callback; - if (optarg2 !== undefined) { - opts = optarg1; - callback = optarg2; - } else { - callback = optarg1; - } - - opts = opts || {}; - - if (typeof callback != 'function') { - throw new Error('callback must be a function'); - } - - if (!url) { - throw new Error('no url specified'); - } - - switch (method) { - case 'GET': - if (isObject(data)) { - for (var k in data) { - if (data.hasOwnProperty(k)) { - url += (url.indexOf('?') == -1 ? '?' : '&')+encodeURIComponent(k)+'='+encodeURIComponent(data[k]) - } - } - } - break; - - case 'POST': - if (isObject(data)) { - var sdata = []; - for (var k in data) { - if (data.hasOwnProperty(k)) { - sdata.push(encodeURIComponent(k)+'='+encodeURIComponent(data[k])); - } - } - data = sdata.join('&'); - } - break; - } - - opts = Object.assign({}, defaultOpts, opts); - - var xhr = createXMLHttpRequest(); - xhr.open(method, url); - - xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - if (method == 'POST') { - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - } - - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - if ('status' in xhr && !/^2|1223/.test(xhr.status)) { - throw new Error('http code '+xhr.status) - } - if (opts.json) { - var resp = JSON.parse(xhr.responseText) - if (!isObject(resp)) { - throw new Error('ajax: object expected') - } - if (resp.error) { - throw new Error(resp.error) - } - callback(null, resp.response); - } else { - callback(null, xhr.responseText); - } - } - }; - - xhr.onerror = function(e) { - callback(e); - }; - - xhr.send(method == 'GET' ? null : data); - - return xhr; - } - - window.ajax = { - get: request.bind(request, 'GET'), - post: request.bind(request, 'POST') - } - -})(); - -function bindEventHandlers(obj) { - for (var k in obj) { - if (obj.hasOwnProperty(k) - && typeof obj[k] == 'function' - && k.length > 2 - && k.startsWith('on') - && k[2].charCodeAt(0) >= 65 - && k[2].charCodeAt(0) <= 90) { - obj[k] = obj[k].bind(obj) - } - } -} - -// -// DOM helpers -// -function ge(id) { - return document.getElementById(id) -} - -function hasClass(el, name) { - return el && el.nodeType === 1 && (" " + el.className + " ").replace(/[\t\r\n\f]/g, " ").indexOf(" " + name + " ") >= 0 -} - -function addClass(el, name) { - if (!el) { - return console.warn('addClass: el is', el) - } - if (!hasClass(el, name)) { - el.className = (el.className ? el.className + ' ' : '') + name - } -} - -function removeClass(el, name) { - if (!el) { - return console.warn('removeClass: el is', el) - } - if (isArray(name)) { - for (var i = 0; i < name.length; i++) { - removeClass(el, name[i]); - } - return; - } - el.className = ((el.className || '').replace((new RegExp('(\\s|^)' + name + '(\\s|$)')), ' ')).trim() -} - -function addEvent(el, type, f, useCapture) { - if (!el) { - return console.warn('addEvent: el is', el, stackTrace()) - } - - if (isArray(type)) { - for (var i = 0; i < type.length; i++) { - addEvent(el, type[i], f, useCapture); - } - return; - } - - if (el.addEventListener) { - el.addEventListener(type, f, useCapture || false); - return true; - } else if (el.attachEvent) { - return el.attachEvent('on' + type, f); - } - - return false; -} - -function removeEvent(el, type, f, useCapture) { - if (isArray(type)) { - for (var i = 0; i < type.length; i++) { - var t = type[i]; - removeEvent(el, type[i], f, useCapture); - } - return; - } - - if (el.removeEventListener) { - el.removeEventListener(type, f, useCapture || false); - } else if (el.detachEvent) { - return el.detachEvent('on' + type, f); - } - - return false; -} - -function cancelEvent(evt) { - if (!evt) { - return console.warn('cancelEvent: event is', evt) - } - - if (evt.preventDefault) evt.preventDefault(); - if (evt.stopPropagation) evt.stopPropagation(); - - evt.cancelBubble = true; - evt.returnValue = false; - - return false; -} - - -// -// Cookies -// -function setCookie(name, value, days) { - var expires = ""; - if (days) { - var date = new Date(); - date.setTime(date.getTime() + (days*24*60*60*1000)); - expires = "; expires=" + date.toUTCString(); - } - document.cookie = name + "=" + (value || "") + expires + "; domain=" + window.appConfig.cookieHost + "; path=/"; -} - -function unsetCookie(name) { - document.cookie = name + '=; Max-Age=-99999999; domain=' + window.appConfig.cookieHost + "; path=/"; -} - -function getCookie(name) { - var nameEQ = name + "="; - var ca = document.cookie.split(';'); - for (var i = 0; i < ca.length; i++) { - var c = ca[i]; - while (c.charAt(0) === ' ') - c = c.substring(1, c.length); - if (c.indexOf(nameEQ) === 0) - return c.substring(nameEQ.length, c.length); - } - return null; -} - -// -// Misc -// -function isObject(o) { - return Object.prototype.toString.call(o) === '[object Object]'; -} - -function isArray(a) { - return Object.prototype.toString.call(a) === '[object Array]'; -} - -function extend(dst, src) { - if (!isObject(dst)) { - return console.error('extend: dst is not an object'); - } - if (!isObject(src)) { - return console.error('extend: src is not an object'); - } - for (var key in src) { - dst[key] = src[key]; - } - return dst; -} - -function stackTrace(split) { - if (split === undefined) { - split = true; - } - try { - o.lo.lo += 0; - } catch(e) { - if (e.stack) { - var stack = split ? e.stack.split('\n') : e.stack; - stack.shift(); - stack.shift(); - return stack.join('\n'); - } - } - return null; -} - -function escape(str) { - var pre = document.createElement('pre'); - var text = document.createTextNode(str); - pre.appendChild(text); - return pre.innerHTML; -} - -function parseUrl(uri) { - var parser = document.createElement('a'); - parser.href = uri; - - return { - protocol: parser.protocol, // => "http:" - host: parser.host, // => "example.com:3000" - hostname: parser.hostname, // => "example.com" - port: parser.port, // => "3000" - pathname: parser.pathname, // => "/pathname/" - hash: parser.hash, // => "#hash" - search: parser.search, // => "?search=test" - origin: parser.origin, // => "http://example.com:3000" - path: (parser.pathname || '') + (parser.search || '') - } -} - -function once(fn, context) { - var result; - return function() { - if (fn) { - result = fn.apply(context || this, arguments); - fn = null; - } - return result; - }; -} - -// -// - -function lang(key) { - return __lang[key] !== undefined ? __lang[key] : '{'+key+'}'; -} - -var DynamicLogo = { - dynLink: null, - afr: null, - afrUrl: null, - - init: function() { - this.dynLink = ge('head_dyn_link'); - this.cdText = ge('head_cd_text'); - - if (!this.dynLink) { - return console.warn('DynamicLogo.init: !this.dynLink'); - } - - var spans = this.dynLink.querySelectorAll('span.head-logo-path'); - for (var i = 0; i < spans.length; i++) { - var span = spans[i]; - addEvent(span, 'mouseover', this.onSpanOver); - addEvent(span, 'mouseout', this.onSpanOut); - } - }, - - setUrl: function(url) { - if (this.afr !== null) { - cancelAnimationFrame(this.afr); - } - this.afrUrl = url; - this.afr = requestAnimationFrame(this.onAnimationFrame); - }, - - onAnimationFrame: function() { - var url = this.afrUrl; - - // update link - this.dynLink.setAttribute('href', url); - - // update console text - if (this.afrUrl === '/') { - url = '~'; - } else { - url = '~'+url.replace(/\/$/, ''); - } - this.cdText.innerHTML = escape(url); - - this.afr = null; - }, - - onSpanOver: function() { - var span = event.target; - this.setUrl(span.getAttribute('data-url')); - cancelEvent(event); - }, - - onSpanOut: function() { - var span = event.target; - this.setUrl('/'); - cancelEvent(event); - } -}; -bindEventHandlers(DynamicLogo); - -window.__lang = {}; - -// set/remove retina cookie -(function() { - var isRetina = window.devicePixelRatio >= 1.5; - if (isRetina) { - setCookie('is_retina', 1, 365); - } else { - unsetCookie('is_retina'); - } -})(); - -var StaticManager = { - loadedStyles: [], - versions: {}, - - setStyles: function(list, versions) { - this.loadedStyles = list; - this.versions = versions; - }, - - loadStyle: function(name, theme, callback) { - var url; - if (!window.appConfig.devMode) { - var filename = name + (theme === 'dark' ? '_dark' : '') + '.css'; - url = '/css/'+filename+'?'+this.versions[filename]; - } else { - url = '/sass.php?name='+name+'&theme='+theme; - } - - var el = document.createElement('link'); - el.onerror = callback - el.onload = callback - el.setAttribute('rel', 'stylesheet'); - el.setAttribute('type', 'text/css'); - el.setAttribute('id', 'style_'+name+'_dark'); - el.setAttribute('href', url); - - document.getElementsByTagName('head')[0].appendChild(el); - } -}; - -var ThemeSwitcher = (function() { - /** - * @type {string[]} - */ - var modes = ['auto', 'dark', 'light']; - - /** - * @type {number} - */ - var currentModeIndex = -1; - - /** - * @type {boolean|null} - */ - var systemState = null; - - /** - * @returns {boolean} - */ - function isSystemModeSupported() { - try { - // crashes on: - // Mozilla/5.0 (Windows NT 6.2; ARM; Trident/7.0; Touch; rv:11.0; WPDesktop; Lumia 630 Dual SIM) like Gecko - // Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1 - // Mozilla/5.0 (iPad; CPU OS 12_5_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1 - // - // error examples: - // - window.matchMedia("(prefers-color-scheme: dark)").addEventListener is not a function. (In 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",this.onSystemSettingChange.bind(this))', 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener' is undefined) - // - Object [object MediaQueryList] has no method 'addEventListener' - return !!window['matchMedia'] - && typeof window.matchMedia("(prefers-color-scheme: dark)").addEventListener === 'function'; - } catch (e) { - return false - } - } - - /** - * @returns {boolean} - */ - function isDarkModeApplied() { - var st = StaticManager.loadedStyles; - for (var i = 0; i < st.length; i++) { - var name = st[i]; - if (ge('style_'+name+'_dark')) - return true; - } - return false; - } - - /** - * @returns {string} - */ - function getSavedMode() { - var val = getCookie('theme'); - if (!val) - return modes[0]; - if (modes.indexOf(val) === -1) { - console.error('[ThemeSwitcher getSavedMode] invalid cookie value') - unsetCookie('theme') - return modes[0] - } - return val - } - - /** - * @param {boolean} dark - */ - function changeTheme(dark) { - addClass(document.body, 'theme-changing'); - - var onDone = function() { - window.requestAnimationFrame(function() { - removeClass(document.body, 'theme-changing'); - }) - }; - - window.requestAnimationFrame(function() { - if (dark) - enableDark(onDone); - else - disableDark(onDone); - }) - } - - /** - * @param {function} callback - */ - function enableDark(callback) { - var names = []; - StaticManager.loadedStyles.forEach(function(name) { - var el = ge('style_'+name+'_dark'); - if (el) - return; - names.push(name); - }); - - var left = names.length; - names.forEach(function(name) { - StaticManager.loadStyle(name, 'dark', once(function(e) { - left--; - if (left === 0) - callback(); - })); - }) - } - - /** - * @param {function} callback - */ - function disableDark(callback) { - StaticManager.loadedStyles.forEach(function(name) { - var el = ge('style_'+name+'_dark'); - if (el) - el.remove(); - }) - callback(); - } - - /** - * @param {string} mode - */ - function setLabel(mode) { - var labelEl = ge('theme-switcher-label'); - labelEl.innerHTML = escape(lang('theme_'+mode)); - } - - return { - init: function() { - var cur = getSavedMode(); - currentModeIndex = modes.indexOf(cur); - - var systemSupported = isSystemModeSupported(); - if (!systemSupported) { - if (currentModeIndex === 0) { - modes.shift(); // remove 'auto' from the list - currentModeIndex = 1; // set to 'light' - if (isDarkModeApplied()) - disableDark(); - } - } else { - /** - * @param {boolean} dark - */ - var onSystemChange = function(dark) { - var prevSystemState = systemState; - systemState = dark; - - if (modes[currentModeIndex] !== 'auto') - return; - - if (systemState !== prevSystemState) - changeTheme(systemState); - }; - - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { - onSystemChange(e.matches === true) - }); - - onSystemChange(window.matchMedia('(prefers-color-scheme: dark)').matches === true); - } - - setLabel(modes[currentModeIndex]); - }, - - next: function(e) { - if (hasClass(document.body, 'theme-changing')) { - console.log('next: theme changing is in progress, ignoring...') - return; - } - - currentModeIndex = (currentModeIndex + 1) % modes.length; - switch (modes[currentModeIndex]) { - case 'auto': - if (systemState !== null) - changeTheme(systemState); - break; - - case 'light': - if (isDarkModeApplied()) - changeTheme(false); - break; - - case 'dark': - if (!isDarkModeApplied()) - changeTheme(true); - break; - } - - setLabel(modes[currentModeIndex]); - setCookie('theme', modes[currentModeIndex]); - - return cancelEvent(e); - } - }; -})();
\ No newline at end of file diff --git a/htdocs/js/common/00-polyfills.js b/htdocs/js/common/00-polyfills.js new file mode 100644 index 0000000..74ec195 --- /dev/null +++ b/htdocs/js/common/00-polyfills.js @@ -0,0 +1,47 @@ +if (!String.prototype.startsWith) { + String.prototype.startsWith = function(search, pos) { + pos = !pos || pos < 0 ? 0 : +pos; + return this.substring(pos, pos + search.length) === search; + }; +} + +if (!String.prototype.endsWith) { + String.prototype.endsWith = function(search, this_len) { + if (this_len === undefined || this_len > this.length) { + this_len = this.length; + } + return this.substring(this_len - search.length, this_len) === search; + }; +} + +if (!Object.assign) { + Object.defineProperty(Object, 'assign', { + enumerable: false, + configurable: true, + writable: true, + value: function(target, firstSource) { + 'use strict'; + if (target === undefined || target === null) { + throw new TypeError('Cannot convert first argument to object'); + } + + var to = Object(target); + for (var i = 1; i < arguments.length; i++) { + var nextSource = arguments[i]; + if (nextSource === undefined || nextSource === null) { + continue; + } + + var keysArray = Object.keys(Object(nextSource)); + for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { + var nextKey = keysArray[nextIndex]; + var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); + if (desc !== undefined && desc.enumerable) { + to[nextKey] = nextSource[nextKey]; + } + } + } + return to; + } + }); +}
\ No newline at end of file diff --git a/htdocs/js/common/02-ajax.js b/htdocs/js/common/02-ajax.js new file mode 100644 index 0000000..c432a51 --- /dev/null +++ b/htdocs/js/common/02-ajax.js @@ -0,0 +1,118 @@ +// +// AJAX +// +(function() { + + var defaultOpts = { + json: true + }; + + function createXMLHttpRequest() { + if (window.XMLHttpRequest) { + return new XMLHttpRequest(); + } + + var xhr; + try { + xhr = new ActiveXObject('Msxml2.XMLHTTP'); + } catch (e) { + try { + xhr = new ActiveXObject('Microsoft.XMLHTTP'); + } catch (e) {} + } + if (!xhr) { + console.error('Your browser doesn\'t support XMLHttpRequest.'); + } + return xhr; + } + + function request(method, url, data, optarg1, optarg2) { + data = data || null; + + var opts, callback; + if (optarg2 !== undefined) { + opts = optarg1; + callback = optarg2; + } else { + callback = optarg1; + } + + opts = opts || {}; + + if (typeof callback != 'function') { + throw new Error('callback must be a function'); + } + + if (!url) { + throw new Error('no url specified'); + } + + switch (method) { + case 'GET': + if (isObject(data)) { + for (var k in data) { + if (data.hasOwnProperty(k)) { + url += (url.indexOf('?') == -1 ? '?' : '&')+encodeURIComponent(k)+'='+encodeURIComponent(data[k]) + } + } + } + break; + + case 'POST': + if (isObject(data)) { + var sdata = []; + for (var k in data) { + if (data.hasOwnProperty(k)) { + sdata.push(encodeURIComponent(k)+'='+encodeURIComponent(data[k])); + } + } + data = sdata.join('&'); + } + break; + } + + opts = Object.assign({}, defaultOpts, opts); + + var xhr = createXMLHttpRequest(); + xhr.open(method, url); + + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + if (method == 'POST') { + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + } + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if ('status' in xhr && !/^2|1223/.test(xhr.status)) { + throw new Error('http code '+xhr.status) + } + if (opts.json) { + var resp = JSON.parse(xhr.responseText) + if (!isObject(resp)) { + throw new Error('ajax: object expected') + } + if (resp.error) { + throw new Error(resp.error) + } + callback(null, resp.response); + } else { + callback(null, xhr.responseText); + } + } + }; + + xhr.onerror = function(e) { + callback(e); + }; + + xhr.send(method == 'GET' ? null : data); + + return xhr; + } + + window.ajax = { + get: request.bind(request, 'GET'), + post: request.bind(request, 'POST') + } + +})(); diff --git a/htdocs/js/common/03-dom.js b/htdocs/js/common/03-dom.js new file mode 100644 index 0000000..d05bcd0 --- /dev/null +++ b/htdocs/js/common/03-dom.js @@ -0,0 +1,117 @@ +// +// DOM helpers +// +function ge(id) { + return document.getElementById(id) +} + +function hasClass(el, name) { + return el && el.nodeType === 1 && (" " + el.className + " ").replace(/[\t\r\n\f]/g, " ").indexOf(" " + name + " ") >= 0 +} + +function addClass(el, name) { + if (!el) { + return console.warn('addClass: el is', el) + } + if (!hasClass(el, name)) { + el.className = (el.className ? el.className + ' ' : '') + name + } +} + +function removeClass(el, name) { + if (!el) { + return console.warn('removeClass: el is', el) + } + if (isArray(name)) { + for (var i = 0; i < name.length; i++) { + removeClass(el, name[i]); + } + return; + } + el.className = ((el.className || '').replace((new RegExp('(\\s|^)' + name + '(\\s|$)')), ' ')).trim() +} + +function addEvent(el, type, f, useCapture) { + if (!el) { + return console.warn('addEvent: el is', el, stackTrace()) + } + + if (isArray(type)) { + for (var i = 0; i < type.length; i++) { + addEvent(el, type[i], f, useCapture); + } + return; + } + + if (el.addEventListener) { + el.addEventListener(type, f, useCapture || false); + return true; + } else if (el.attachEvent) { + return el.attachEvent('on' + type, f); + } + + return false; +} + +function removeEvent(el, type, f, useCapture) { + if (isArray(type)) { + for (var i = 0; i < type.length; i++) { + var t = type[i]; + removeEvent(el, type[i], f, useCapture); + } + return; + } + + if (el.removeEventListener) { + el.removeEventListener(type, f, useCapture || false); + } else if (el.detachEvent) { + return el.detachEvent('on' + type, f); + } + + return false; +} + +function cancelEvent(evt) { + if (!evt) { + return console.warn('cancelEvent: event is', evt) + } + + if (evt.preventDefault) evt.preventDefault(); + if (evt.stopPropagation) evt.stopPropagation(); + + evt.cancelBubble = true; + evt.returnValue = false; + + return false; +} + + +// +// Cookies +// +function setCookie(name, value, days) { + var expires = ""; + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days*24*60*60*1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; domain=" + window.appConfig.cookieHost + "; path=/"; +} + +function unsetCookie(name) { + document.cookie = name + '=; Max-Age=-99999999; domain=' + window.appConfig.cookieHost + "; path=/"; +} + +function getCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') + c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) + return c.substring(nameEQ.length, c.length); + } + return null; +} diff --git a/htdocs/js/common/04-util.js b/htdocs/js/common/04-util.js new file mode 100644 index 0000000..da95e39 --- /dev/null +++ b/htdocs/js/common/04-util.js @@ -0,0 +1,89 @@ +function bindEventHandlers(obj) { + for (var k in obj) { + if (obj.hasOwnProperty(k) + && typeof obj[k] == 'function' + && k.length > 2 + && k.startsWith('on') + && k[2].charCodeAt(0) >= 65 + && k[2].charCodeAt(0) <= 90) { + obj[k] = obj[k].bind(obj) + } + } +} + +function isObject(o) { + return Object.prototype.toString.call(o) === '[object Object]'; +} + +function isArray(a) { + return Object.prototype.toString.call(a) === '[object Array]'; +} + +function extend(dst, src) { + if (!isObject(dst)) { + return console.error('extend: dst is not an object'); + } + if (!isObject(src)) { + return console.error('extend: src is not an object'); + } + for (var key in src) { + dst[key] = src[key]; + } + return dst; +} + +function timestamp() { + return Math.floor(Date.now() / 1000) +} + +function stackTrace(split) { + if (split === undefined) { + split = true; + } + try { + o.lo.lo += 0; + } catch(e) { + if (e.stack) { + var stack = split ? e.stack.split('\n') : e.stack; + stack.shift(); + stack.shift(); + return stack.join('\n'); + } + } + return null; +} + +function escape(str) { + var pre = document.createElement('pre'); + var text = document.createTextNode(str); + pre.appendChild(text); + return pre.innerHTML; +} + +function parseUrl(uri) { + var parser = document.createElement('a'); + parser.href = uri; + + return { + protocol: parser.protocol, // => "http:" + host: parser.host, // => "example.com:3000" + hostname: parser.hostname, // => "example.com" + port: parser.port, // => "3000" + pathname: parser.pathname, // => "/pathname/" + hash: parser.hash, // => "#hash" + search: parser.search, // => "?search=test" + origin: parser.origin, // => "http://example.com:3000" + path: (parser.pathname || '') + (parser.search || '') + } +} + +function once(fn, context) { + var result; + return function() { + if (fn) { + result = fn.apply(context || this, arguments); + fn = null; + } + return result; + }; +}
\ No newline at end of file diff --git a/htdocs/js/common/10-lang.js b/htdocs/js/common/10-lang.js new file mode 100644 index 0000000..bd0d8e7 --- /dev/null +++ b/htdocs/js/common/10-lang.js @@ -0,0 +1,5 @@ +function lang(key) { + return __lang[key] !== undefined ? __lang[key] : '{'+key+'}'; +} + +window.__lang = {};
\ No newline at end of file diff --git a/htdocs/js/common/20-dynlogo.js b/htdocs/js/common/20-dynlogo.js new file mode 100644 index 0000000..2d62a27 --- /dev/null +++ b/htdocs/js/common/20-dynlogo.js @@ -0,0 +1,60 @@ +var DynamicLogo = { + dynLink: null, + afr: null, + afrUrl: null, + + init: function() { + this.dynLink = ge('head_dyn_link'); + this.cdText = ge('head_cd_text'); + + if (!this.dynLink) { + return console.warn('DynamicLogo.init: !this.dynLink'); + } + + var spans = this.dynLink.querySelectorAll('span.head-logo-path'); + for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + addEvent(span, 'mouseover', this.onSpanOver); + addEvent(span, 'mouseout', this.onSpanOut); + } + }, + + setUrl: function(url) { + if (this.afr !== null) { + cancelAnimationFrame(this.afr); + } + this.afrUrl = url; + this.afr = requestAnimationFrame(this.onAnimationFrame); + }, + + onAnimationFrame: function() { + var url = this.afrUrl; + + // update link + this.dynLink.setAttribute('href', url); + + // update console text + if (this.afrUrl === '/') { + url = '~'; + } else { + url = '~'+url.replace(/\/$/, ''); + } + this.cdText.innerHTML = escape(url); + + this.afr = null; + }, + + onSpanOver: function() { + var span = event.target; + this.setUrl(span.getAttribute('data-url')); + cancelEvent(event); + }, + + onSpanOut: function() { + var span = event.target; + this.setUrl('/'); + cancelEvent(event); + } +}; + +bindEventHandlers(DynamicLogo);
\ No newline at end of file diff --git a/htdocs/js/common/30-static-manager.js b/htdocs/js/common/30-static-manager.js new file mode 100644 index 0000000..772df32 --- /dev/null +++ b/htdocs/js/common/30-static-manager.js @@ -0,0 +1,30 @@ +var StaticManager = { + loadedStyles: [], + versions: {}, + + init: function(loadedStyles, versions) { + this.loadedStyles = loadedStyles; + this.versions = versions; + }, + + loadStyle: function(name, theme, callback) { + var url; + if (!window.appConfig.devMode) { + if (theme === 'dark') + name += '_dark'; + url = '/css/'+name+'.css?'+this.versions.css[name]; + } else { + url = '/sass.php?name='+name+'&theme='+theme+'&v='+timestamp(); + } + + var el = document.createElement('link'); + el.onerror = callback; + el.onload = callback; + el.setAttribute('rel', 'stylesheet'); + el.setAttribute('type', 'text/css'); + el.setAttribute('id', 'style_'+name); + el.setAttribute('href', url); + + document.getElementsByTagName('head')[0].appendChild(el); + } +}; diff --git a/htdocs/js/common/35-theme-switcher.js b/htdocs/js/common/35-theme-switcher.js new file mode 100644 index 0000000..e716e4b --- /dev/null +++ b/htdocs/js/common/35-theme-switcher.js @@ -0,0 +1,195 @@ +var ThemeSwitcher = (function() { + /** + * @type {string[]} + */ + var modes = ['auto', 'dark', 'light']; + + /** + * @type {number} + */ + var currentModeIndex = -1; + + /** + * @type {boolean|null} + */ + var systemState = null; + + /** + * @returns {boolean} + */ + function isSystemModeSupported() { + try { + // crashes on: + // Mozilla/5.0 (Windows NT 6.2; ARM; Trident/7.0; Touch; rv:11.0; WPDesktop; Lumia 630 Dual SIM) like Gecko + // Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1 + // Mozilla/5.0 (iPad; CPU OS 12_5_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1 + // + // error examples: + // - window.matchMedia("(prefers-color-scheme: dark)").addEventListener is not a function. (In 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",this.onSystemSettingChange.bind(this))', 'window.matchMedia("(prefers-color-scheme: dark)").addEventListener' is undefined) + // - Object [object MediaQueryList] has no method 'addEventListener' + return !!window['matchMedia'] + && typeof window.matchMedia("(prefers-color-scheme: dark)").addEventListener === 'function'; + } catch (e) { + return false + } + } + + /** + * @returns {boolean} + */ + function isDarkModeApplied() { + var st = StaticManager.loadedStyles; + for (var i = 0; i < st.length; i++) { + var name = st[i]; + if (ge('style_'+name+'_dark')) + return true; + } + return false; + } + + /** + * @returns {string} + */ + function getSavedMode() { + var val = getCookie('theme'); + if (!val) + return modes[0]; + if (modes.indexOf(val) === -1) { + console.error('[ThemeSwitcher getSavedMode] invalid cookie value') + unsetCookie('theme') + return modes[0] + } + return val + } + + /** + * @param {boolean} dark + */ + function changeTheme(dark) { + addClass(document.body, 'theme-changing'); + + var onDone = function() { + window.requestAnimationFrame(function() { + removeClass(document.body, 'theme-changing'); + }) + }; + + window.requestAnimationFrame(function() { + if (dark) + enableDark(onDone); + else + disableDark(onDone); + }) + } + + /** + * @param {function} callback + */ + function enableDark(callback) { + var names = []; + StaticManager.loadedStyles.forEach(function(name) { + var el = ge('style_'+name+'_dark'); + if (el) + return; + names.push(name); + }); + + var left = names.length; + names.forEach(function(name) { + StaticManager.loadStyle(name, 'dark', once(function(e) { + left--; + if (left === 0) + callback(); + })); + }) + } + + /** + * @param {function} callback + */ + function disableDark(callback) { + StaticManager.loadedStyles.forEach(function(name) { + var el = ge('style_'+name+'_dark'); + if (el) + el.remove(); + }) + callback(); + } + + /** + * @param {string} mode + */ + function setLabel(mode) { + var labelEl = ge('theme-switcher-label'); + labelEl.innerHTML = escape(lang('theme_'+mode)); + } + + return { + init: function() { + var cur = getSavedMode(); + currentModeIndex = modes.indexOf(cur); + + var systemSupported = isSystemModeSupported(); + if (!systemSupported) { + if (currentModeIndex === 0) { + modes.shift(); // remove 'auto' from the list + currentModeIndex = 1; // set to 'light' + if (isDarkModeApplied()) + disableDark(); + } + } else { + /** + * @param {boolean} dark + */ + var onSystemChange = function(dark) { + var prevSystemState = systemState; + systemState = dark; + + if (modes[currentModeIndex] !== 'auto') + return; + + if (systemState !== prevSystemState) + changeTheme(systemState); + }; + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { + onSystemChange(e.matches === true) + }); + + onSystemChange(window.matchMedia('(prefers-color-scheme: dark)').matches === true); + } + + setLabel(modes[currentModeIndex]); + }, + + next: function(e) { + if (hasClass(document.body, 'theme-changing')) { + console.log('next: theme changing is in progress, ignoring...') + return; + } + + currentModeIndex = (currentModeIndex + 1) % modes.length; + switch (modes[currentModeIndex]) { + case 'auto': + if (systemState !== null) + changeTheme(systemState); + break; + + case 'light': + if (isDarkModeApplied()) + changeTheme(false); + break; + + case 'dark': + if (!isDarkModeApplied()) + changeTheme(true); + break; + } + + setLabel(modes[currentModeIndex]); + setCookie('theme', modes[currentModeIndex]); + + return cancelEvent(e); + } + }; +})();
\ No newline at end of file diff --git a/htdocs/js/common/90-retina.js b/htdocs/js/common/90-retina.js new file mode 100644 index 0000000..46b9f17 --- /dev/null +++ b/htdocs/js/common/90-retina.js @@ -0,0 +1,9 @@ +// set/remove retina cookie +(function() { + var isRetina = window.devicePixelRatio >= 1.5; + if (isRetina) { + setCookie('is_retina', 1, 365); + } else { + unsetCookie('is_retina'); + } +})();
\ No newline at end of file diff --git a/lib/cli.php b/lib/cli.php index 3bda20f..a860871 100644 --- a/lib/cli.php +++ b/lib/cli.php @@ -63,8 +63,12 @@ class cli { } public static function die($error): void { - fwrite(STDERR, "error: {$error}\n"); + self::error($error); exit(1); } + public static function error($error): void { + fwrite(STDERR, "error: {$error}\n"); + } + }
\ No newline at end of file diff --git a/skin/base.skin.php b/skin/base.skin.php index 2d71312..616e1b3 100644 --- a/skin/base.skin.php +++ b/skin/base.skin.php @@ -45,11 +45,20 @@ function renderScript($ctx, $unsafe_js, $unsafe_lang, $enable_dynlogo) { global $config; $styles = json_encode($ctx->styleNames); -$versions = !$config['is_dev'] ? json_encode($config['static']) : '{}'; +if ($config['is_dev']) + $versions = '{}'; +else { + $versions = []; + foreach ($config['static'] as $name => $v) { + list($type, $bname) = getStaticNameParts($name); + $versions[$type][$bname] = $v; + } + $versions = json_encode($versions); +} return <<<HTML <script type="text/javascript"> -StaticManager.setStyles({$styles}, {$versions}); +StaticManager.init({$styles}, {$versions}); {$ctx->if_true($unsafe_js, '(function(){'.$unsafe_js.'})();')} {$ctx->if_true($unsafe_lang, 'extend(__lang, '.$unsafe_lang.');')} {$ctx->if_true($enable_dynlogo, 'DynamicLogo.init();')} @@ -77,40 +86,46 @@ function renderStatic($ctx, $static, $theme) { $ctx->styleNames = []; foreach ($static as $name) { // javascript - if (str_ends_with($name, '.js')) + if (str_starts_with($name, 'js/')) $html[] = jsLink($name); - // cs - else if (str_ends_with($name, '.css')) { + // css + else if (str_starts_with($name, 'css/')) { $html[] = cssLink($name, 'light', $style_name); $ctx->styleNames[] = $style_name; if ($dark) $html[] = cssLink($name, 'dark', $style_name); else if (!$config['is_dev']) - $html[] = cssPrefetchLink(str_replace('.css', '_dark.css', $name)); + $html[] = cssPrefetchLink($style_name.'_dark'); } + else + logError(__FUNCTION__.': unexpected static entry: '.$name); } return implode("\n", $html); } function jsLink(string $name): string { - return '<script src="'.$name.'?'.getStaticVersion($name).'" type="text/javascript"></script>'; + global $config; + list (, $bname) = getStaticNameParts($name); + if ($config['is_dev']) { + $href = '/js.php?name='.urlencode($bname).'&v='.time(); + } else { + $href = '/dist-js/'.$bname.'.js?'.getStaticVersion($name); + } + return '<script src="'.$href.'" type="text/javascript"></script>'; } function cssLink(string $name, string $theme, &$bname = null): string { global $config; - $dname = dirname($name); - $bname = basename($name); - if (($pos = strrpos($bname, '.'))) - $bname = substr($bname, 0, $pos); + list(, $bname) = getStaticNameParts($name); if ($config['is_dev']) { $href = '/sass.php?name='.urlencode($bname).'&theme='.$theme.'&v='.time(); } else { $version = getStaticVersion('css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css'); - $href = $dname.'/'.$bname.($theme == 'dark' ? '_dark' : '').'.css?'.$version; + $href = '/dist-css/'.$bname.($theme == 'dark' ? '_dark' : '').'.css?'.$version; } $id = 'style_'.$bname; @@ -121,17 +136,33 @@ function cssLink(string $name, string $theme, &$bname = null): string { } function cssPrefetchLink(string $name): string { - $url = $name.'?'.getStaticVersion($name); - return <<<HTML +$url = '/dist-css/'.$name.'.css?'.getStaticVersion('css/'.$name.'.css'); +return <<<HTML <link rel="prefetch" href="{$url}" /> HTML; } +function getStaticNameParts(string $name): array { + $dname = dirname($name); + $bname = basename($name); + if (($pos = strrpos($bname, '.'))) { + $ext = substr($bname, $pos+1); + $bname = substr($bname, 0, $pos); + } else { + $ext = ''; + } + return [$dname, $bname, $ext]; +} + function getStaticVersion(string $name): string { global $config; - if (str_starts_with($name, '/')) + if ($config['is_dev']) + return time(); + if (str_starts_with($name, '/')) { + logWarning(__FUNCTION__.': '.$name.' starts with /'); $name = substr($name, 1); - return $config['is_dev'] ? time() : $config['static'][$name] ?? 'notfound'; + } + return $config['static'][$name] ?? 'notfound'; } function renderHeader($ctx, $theme, $unsafe_logo_html) { |