M inc/Parsedown.php => inc/Parsedown.php +2 -1
@@ 1371,7 1371,8 @@ class Parsedown
'element' => array(
'name' => 'img',
'attributes' => array(
- 'src' => $Link['element']['attributes']['href'],
+ 'src' => '/load.png',
+ 'data-src' => $Link['element']['attributes']['href'],
'alt' => $Link['element']['handler']['argument'],
'loading' => 'lazy',
),
M index.php => index.php +256 -187
@@ 2,243 2,312 @@
/*
最小 — Saishō MK. 2
https://paulglushak.com/saisho
- */
-$time = microtime(true);
-define('SAISHO_VERSION', '20200725');
+*/
+define('SAISHO_VERSION', '20200830');
define('META_TITLE', 'Saisho');
-define('META_DESCRIPTION', 'Saisho Website');
+define('META_DESCRIPTION', 'Very (VERY) simple and quick semi-static site engine');
define('HOST', '//' . $_SERVER['HTTP_HOST']);
define('DS', DIRECTORY_SEPARATOR);
define('ROOT_DIR', __DIR__);
define('DATA_DIR', ROOT_DIR . DS . 'data');
+define('TEMPLATE_DIR', ROOT_DIR . DS . 'template');
define('CACHE_DIR', ROOT_DIR . DS . 'cache');
define('INC_DIR', ROOT_DIR . DS . 'inc');
-define('CACHE_TIME', 600);
+define('CACHE_TIME', 1000);
+define('TAGS_TO_HIDE', ['hide']);
+class Saisho {
+ public $requestedPage, $pagePath, $cachedPagePath, $time;
-class Saisho
-{
- public function getRequestedPage()
+ public function __construct()
{
- return (string) trim($_SERVER['REQUEST_URI'], '/');
+ $this->setPagePaths($this->getRequestedPage());
+ $this->time = microtime(true);
+ $this->handleRequest($this->requestedPage);
}
- public function handleRequest(string $requestedPage)
- {
- if ($requestedPage == '') { /* Empty request - Homepage */
- $list = $this->listPages();
- return (object) ['type' => 'list', 'content' => $this->renderPageList($list)];
- }
- $pagePath = DATA_DIR . DS . $requestedPage . '.md';
- if ($this->pageExists($pagePath)) {
- return (object) ['type' => 'page', 'content' => $this->renderPage($pagePath)];
- } else {
- http_response_code(404);
- return (object) ['type' => 'notfound', 'content' => '<h1>(ノಠ益ಠ)ノ彡 Not found</h1>'];
+ /**
+ * Get the requested page from the URI
+ *
+ * @return string
+ */
+ public function getRequestedPage() {
+ return (string)trim($_SERVER['REQUEST_URI'], '/');
+ }
+ /**
+ * Set the data and cache page paths for the requested page
+ *
+ * @param string $page the requested page
+ * @return void
+ */
+ public function setPagePaths(string $page) {
+ $this->requestedPage = $page;
+ $this->pagePath = DATA_DIR . DS . $this->requestedPage. '.md';
+ $this->cachedPagePath = CACHE_DIR . DS . $this->requestedPage. '.html';
+ }
+ /**
+ * Request handle aka router.
+ * Constructs page object based on requested page where empty is a list and a string is a page.
+ *
+ * @param string $requestedPage
+ * @return void
+ */
+ public function handleRequest(string $requestedPage) {
+ switch ($requestedPage) {
+ case '':
+ $list = $this->listPages();
+ $page = (object)['type' => 'list', 'content' => $this->renderPageList($list, true) ];
+ break;
+ case 'projects':
+ $list = $this->listPages();
+ $page = (object)['type'=>'list', 'content'=>$this->renderPageList($list, 'project')];
+ break;
+ default:
+ if (file_exists($this->pagePath)) {
+ if (!$this->tryCache($this->cachedPagePath)) {
+ $page = (object)['type' => 'page', 'content' => $this->renderPage($this->pagePath) ];
+ }
+ } else {
+ http_response_code(404);
+ $this->setPagePaths('notfound');
+ $page = (object)['type' => 'notfound', 'content' => ''];
+ }
+ break;
}
+ $this->servePage($page->type, ['content'=>$page->content]);
}
-
- public function listPages()
- {
+ /**
+ * Returns a list of the available pages in the DATA_DIR folder.
+ *
+ * @return array page list
+ */
+ public function listPages() {
$pages = glob(DATA_DIR . DS . '*.md');
$list = [];
foreach ($pages as $page) {
$pageName = str_replace('.md', '', basename($page));
$list[$pageName] = $page;
}
- return $list;
+ return (array)$list;
}
-
- public function sortPageList(array &$list)
- {
+
+ /**
+ * Sorts a list of pages based on date attribute.
+ *
+ * @return array sorted page list
+ */
+ public function sortPageList(array & $list) {
uasort($list, function ($a, $b) {
- return strtotime($b['date']) - strtotime($a['date']);
+ return strtotime($b->date) - strtotime($a->date);
});
- return (object) $list;
+ return (array)$list;
}
-
- public function parsePageList(array $list)
- {
- if (!empty($list)) {
- $parsedPages = [];
- foreach ($list as $name => $path) {
- $page = $this->renderPage($path, true);
- $pageProperties = get_object_vars($page);
- foreach ($pageProperties as $property => $value) {
- $parsedPages[$name][$property] = $value;
- }
- }
- return (array) $parsedPages;
- } else {
- return false;
+ /**
+ * Parses a list of pages and filters.
+ *
+ * @param array $list list of pages
+ * @param mixed $filter true for hidden tags, string for specific tag
+ * @return array parsed and filtered list of pages
+ */
+ public function parsePageList(array $list, $filter) {
+ if (empty($list)) {
+ return;
}
- }
-
- public function renderPageList(array $list)
- {
- if (!empty($list)) {
- $list = $this->parsePageList($list);
- $this->sortPageList($list);
- $html = '<ol reversed>' . PHP_EOL;
- foreach ($list as $handle => $page) {
- $html .= "<li><a href='{$handle}'>{$page['title']}</a> — {$page['date']}</li>" . PHP_EOL;
+ $parsedPages = [];
+ foreach ($list as $name => $path) {
+ $page = $this->renderPage($path, true);
+ $parsedPages[$name] = $page;
+ }
+ return (array)array_filter($parsedPages, function ($item) use ($filter) {
+ if ($filter === true) {
+ return !array_intersect(TAGS_TO_HIDE, $item->tags);
+ } elseif (is_string($filter)) {
+ return in_array($filter, $item->tags);
+ } else {
+ return 1;
}
- $html .= '</ol>' . PHP_EOL;
- return (string) $html;
- } else {
+ });
+ }
+ /**
+ * Creates the HTML for a list of pages.
+ *
+ * @param array $list a list of pages
+ * @param mixed $filter true for hidden tags, string for specific tag
+ * @return string OL list of pages
+ */
+ public function renderPageList(array $list, $filter) {
+ if (empty($list)) {
return '<h1>ʅ(°⊱,°)ʃ Nothing found</h1>';
}
+ $list = $this->parsePageList($list, $filter);
+ $this->sortPageList($list);
+ $html = '<ol reversed class="pl">' . PHP_EOL;
+ foreach ($list as $handle => $page) {
+ if (isset($page->style)) {
+ $style = ' style="' . $page->style . '"';
+ } else {
+ $style = '';
+ }
+ if (!empty($page->tags)) {
+ $tags = ' [' . implode(', ', $page->tags) . ']';
+ } else {
+ $tags = '';
+ }
+ // $html.= "<li><a{$style} href='{$handle}'>{$page->title}</a> <span class='g'> {$page->date}{$tags}</span></li>" . PHP_EOL;
+ $html.= "<li><span class='g'>{$page->date}</span> <a{$style} href='{$handle}'>{$page->title}</a></li>" . PHP_EOL;
+ }
+ $html.= '</ol>' . PHP_EOL;
+ return (string)$html;
}
-
- private function pageExists(string $pageName)
- {
- return file_exists($pageName);
- }
-
- private function getRawContent(string $pageName)
- {
+ /**
+ * Get raw content of page
+ *
+ * @param string $pageName the page to read
+ * @return string raw page content
+ */
+ private function getRawContent(string $pageName) {
return (string) file_get_contents($pageName);
}
-
- private function parseYaml(string &$str)
- {
+ /**
+ * Parse page YAML Front Matter header, return an array of attributes and body.
+ *
+ * @param string $str raw page content to parse
+ * @return array array of page attributes
+ */
+ private function parseYaml(string $str) {
$parsed = [];
preg_match("'^---(.+?)---'si", $str, $yaml);
- if (isset($yaml[0])) {
- $str = str_replace($yaml[0], '', $str);
- $parsed['body'] = $str;
- $yaml = trim($yaml[0]);
- preg_match_all("'(\w+):\s?(.+)'m", $yaml, $yaml_attribs, PREG_SET_ORDER);
- foreach ($yaml_attribs as $attribute) {
- $parsed[$attribute[1]] = trim($attribute[2]);
- }
- return (object) $parsed;
- } else {
- return (object) ['body' => $str];
+ if (!isset($yaml[0])) {
+ return (object)['body' => $str];
+ }
+ $str = str_replace($yaml[0], '', $str);
+ $parsed['body'] = $str;
+ $yaml = trim($yaml[0]);
+ preg_match_all("'(\w+):\s?(.+)'m", $yaml, $yaml_attribs, PREG_SET_ORDER);
+ foreach ($yaml_attribs as $attribute) {
+ $parsed[$attribute[1]] = trim($attribute[2]);
}
+ return (array) $parsed;
}
-
- public function renderPage(string $pageName, bool $onlyYaml = false)
- {
+ /**
+ * Render page markdown and assign meta values. If no value present, required
+ * attributes are given default values.
+ *
+ * @param string $pageName
+ * @param boolean $onlyYaml if true only returns page metadata with no body.
+ * @return object page object
+ */
+ public function renderPage(string $pageName, bool $onlyYaml = false) {
include_once INC_DIR . DS . 'Parsedown.php';
include_once INC_DIR . DS . 'ParsedownExtra.php';
include_once INC_DIR . DS . 'ParsedownExtended.php';
$contentRaw = $this->getRawContent($pageName);
$page = $this->parseYaml($contentRaw);
- $page->title = $page->title ?? 'untitled'; /* Set default values if missing */
- $page->date = $page->date ?? '1969-01-01';
+ $page['title'] = $page['title']??'untitled';
+ $page['date'] = $page['date']??'1969-01-01';
+ $page['modified'] = filectime($pageName);
+ $page['tags'] = isset($page['tags']) ? explode(',', $page['tags']) : [];
$parsedown = new ParsedownExtended(["toc" => ["enable" => true, "inline" => true], "mark" => true, "insert" => true, "task" => true, "kbd" => true]);
if (!$onlyYaml) {
- $page->body = $parsedown->text($page->body);
+ $page['body'] = $parsedown->text($page['body']);
} else {
- unset($page->body);
+ unset($page['body']);
}
- return (object) $page;
+ return (object)$page;
}
-
- private function buildFeed()
- {
+ /**
+ * Construct XML RSS feed
+ *
+ * @return string RSS feed
+ */
+ private function buildFeed() {
$pages = $this->listPages();
- $pages = $this->parsePageList($pages);
- if ($pages) {
- $xml = '<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
- <channel>
- <title>' . META_TITLE . '</title>
- <link>' . HOST . '</link>
- <description>' . META_DESCRIPTION . '</description>
- <generator>Saisho</generator>
- <language>en-us</language>
- <lastBuildDate>' . date('r') . '</lastBuildDate>
- <atom:link href="' . HOST . '/feed.xml" rel="self" type="application/rss+xml"/>' . PHP_EOL;
- foreach ($pages as $handle => $page) {
- $xml .= '<item>' . PHP_EOL;
- $xml .= '<title>' . $page['title'] . '</title>' . PHP_EOL;
- $xml .= '<link>' . HOST . DS . $handle . '</link>' . PHP_EOL;
- $xml .= '<pubDate>' . date('r', strtotime($page['date'])) . '</pubDate>' . PHP_EOL;
- $xml .= '<description>' . ($page['description'] ?? '') . '</description>' . PHP_EOL;
- $xml .= '</item>' . PHP_EOL;
- }
- $xml .= '</channel></rss>';
- return $xml;
+ $pages = $this->parsePageList($pages, true);
+ $xml = '
+ <rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
+ <channel>
+ <title>' . META_TITLE . '</title>
+ <link>' . HOST . '</link>
+ <description>' . META_DESCRIPTION . '</description>
+ <generator>Saisho</generator>
+ <language>en-us</language>
+ <lastBuildDate>' . date('r') . '</lastBuildDate>
+ <atom:link href="' . HOST . '/feed.xml" rel="self" type="application/rss+xml"/>' . PHP_EOL;
+ foreach ($pages as $handle => $page) {
+ $xml.= '<item>' . PHP_EOL;
+ $xml.= '<title>' . $page->title . '</title>' . PHP_EOL;
+ $xml.= '<link>' . HOST . DS . $handle . '</link>' . PHP_EOL;
+ $xml.= '<pubDate>' . date('r', strtotime($page->date)) . '</pubDate>' . PHP_EOL;
+ $xml.= '<description>' . ($page->description??'') . '</description>' . PHP_EOL;
+ $xml.= '</item>' . PHP_EOL;
}
+ $xml.= '</channel></rss>';
+ return $xml;
}
-
- public function saveFeed($filename = 'feed.xml')
- {
- if ((time() - filemtime($filename) >= CACHE_TIME) || !file_exists($filename)) {
+ /**
+ * Save RSS feed
+ *
+ * @param string $filename filename to use, feed.xml by default.
+ * @return string RSS feed URL
+ */
+ public function saveFeed($filename = 'feed.xml') {
+ if ((time() - filectime($filename) >= CACHE_TIME) || !file_exists($filename)) {
$fh = fopen($filename, 'w');
- if ($fh) {
- fwrite($fh, $this->buildFeed());
- fclose($fh);
- return HOST . DS . 'feed.xml';
+ if (!$fh) {
+ return false;
}
+ fwrite($fh, $this->buildFeed());
+ fclose($fh);
+ return HOST . DS . 'feed.xml';
}
}
-}
-
-$saisho = new Saisho();
-$pageName = $saisho->getRequestedPage();
-$page = $saisho->handleRequest($pageName);
-$filePath = CACHE_DIR . DS . $pageName . '.html';
-header('x-generator: Saisho Mk.2 ' . SAISHO_VERSION);
-header('Cache-Control: public, max-age=' . CACHE_TIME . ', immutable');
-if (($pageName !== '') && (time()-@filemtime($filePath) <= CACHE_TIME)) {
- header('Last-Modified:' . gmdate('D, d M Y H:i:s ', @filemtime($filePath)) . 'GMT');
- readfile($filePath);
- exit;
-}
-ob_start();
-echo '<!-- Saisho Cached Copy - ' . date('Y-m-d h:i:s') . ' -->' . PHP_EOL;
-?>
-<!DOCTYPE html>
-<head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <link href="data:," rel="icon">
- <link href="feed.xml" rel="alternate" type="application/rss+xml" title="<?php echo META_TITLE ?> RSS Feed">
- <title><?= isset($page->content->title) ? $page->content->title . ' — ' . META_TITLE : META_TITLE; ?>
- </title>
- <style>
- body { font: 17px/1.6 sans-serif; text-rendering: optimizeLegibility; padding: 2rem }
- pre { white-space: pre-wrap }
- img { image-rendering: pixelated; max-width: 100% }
- a, a:visited { color: initial }
- a:hover, a.heading-link { text-decoration: none }
- .c { max-width: 70ch }
- .fa-link:before { font-style: normal; content: "§" }
- @media screen and (max-width: 800px) { .c { max-width: 100% } }
- </style>
-</head>
-<body>
- <div class="c">
- <?php
- switch ($page->type) {
- case 'list':
- echo '<h1>' . META_TITLE . '</h1>';
- echo '<div>' . $page->content . '</div>';
- break;
- case 'page':
- echo '<a href="' . HOST . '">↩</a> / <time datetime="' . $page->content->date . '">' . $page->content->date . '</time>';
- echo <<<EOD
- <h1>{$page->content->title}</h1>
- <div class="content">{$page->content->body}</div>
- EOD;
- break;
- case 'notfound':
- echo '<a href="' . HOST . '">escape</a>';
- echo $page->content;
- break;
- default:
- echo '<a href="' . HOST . '">escape</a>';
- echo "Sorry, don't know what {$page->type} is.";
- break;
+ public function servePage(string $template, array $args) {
+ $render = $this->renderTemplate( $template, $args);
+ if ($this->requestedPage!== '') {
+ file_put_contents($this->cachedPagePath, $render);
+ $this->saveFeed();
+ }
+ echo $render;
+ }
+ /**
+ * Try serving page from cached HTML.
+ * Returns false if page not found or out of date.
+ *
+ * @param string $filePath file to read
+ * @return void|bool reads the file and exits if cache exits, returns false if not
+ */
+ private function tryCache(string $filePath) {
+ $filectime = @filectime($filePath);
+ if ($filectime && (time() - $filectime <= CACHE_TIME)) {
+ readfile($filePath);
+ echo '<pre>' . round(((microtime(true) - $this->time) * 1000), 3) . 'ms</pre>';
+ exit;
+ }
+ return false;
+ }
+ /**
+ * Render template and return result from output buffer
+ *
+ * @param string $template template name
+ * @param array $args arguments to pass
+ * @return void
+ */
+ private function renderTemplate(string $template, array $args)
+ {
+ ob_start();
+ if (!file_exists(TEMPLATE_DIR.DS.$template.'.php')) return (string) "Template $template.php not found!";
+ $page = (object)[];
+ foreach ( $args as $key => $value) {
+ $page->{$key} = &$value;
+ }
+ if ($this->requestedPage!== '') {
+ header('Last-Modified:' . gmdate('D, d M Y H:i:s ', $page->content->modified??time()) . 'GMT');
+ echo '<!-- Saisho Cached Copy - ' . date('Y-m-d h:i:s') . ' -->' . PHP_EOL;
+ }
+ include TEMPLATE_DIR.DS.$template.'.php';
+ return ob_get_clean();
+ }
+ public function debug($var) {
+ echo '<pre>' . var_export($var, true) . '</pre>';
}
- ?>
- </div>
-</body>
-</html>
-<?php
-if ($pageName !== '') {
- file_put_contents($filePath, ob_get_contents());
- ob_end_flush();
}
-$saisho->saveFeed();>
\ No newline at end of file
+$saisho = new Saisho();
A inst.js => inst.js +236 -0
@@ 0,0 1,236 @@
+/*! instant.page v5.1.0 - (C) 2019-2020 Alexandre Dieulot - https://instant.page/license */
+
+let mouseoverTimer
+let lastTouchTimestamp
+const prefetches = new Set()
+const prefetchElement = document.createElement('link')
+const isSupported = prefetchElement.relList && prefetchElement.relList.supports && prefetchElement.relList.supports('prefetch')
+ && window.IntersectionObserver && 'isIntersecting' in IntersectionObserverEntry.prototype
+const allowQueryString = 'instantAllowQueryString' in document.body.dataset
+const allowExternalLinks = 'instantAllowExternalLinks' in document.body.dataset
+const useWhitelist = 'instantWhitelist' in document.body.dataset
+const mousedownShortcut = 'instantMousedownShortcut' in document.body.dataset
+const DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION = 1111
+
+let delayOnHover = 65
+let useMousedown = false
+let useMousedownOnly = false
+let useViewport = false
+
+if ('instantIntensity' in document.body.dataset) {
+ const intensity = document.body.dataset.instantIntensity
+
+ if (intensity.substr(0, 'mousedown'.length) == 'mousedown') {
+ useMousedown = true
+ if (intensity == 'mousedown-only') {
+ useMousedownOnly = true
+ }
+ }
+ else if (intensity.substr(0, 'viewport'.length) == 'viewport') {
+ if (!(navigator.connection && (navigator.connection.saveData || (navigator.connection.effectiveType && navigator.connection.effectiveType.includes('2g'))))) {
+ if (intensity == "viewport") {
+ /* Biggest iPhone resolution (which we want): 414 896 = 370944
+ * Small 7" tablet resolution (which we dont want): 600 1024 = 614400
+ * Note that the viewport (which we check here) is smaller than the resolution due to the UIs chrome */
+ if (document.documentElement.clientWidth * document.documentElement.clientHeight < 450000) {
+ useViewport = true
+ }
+ }
+ else if (intensity == "viewport-all") {
+ useViewport = true
+ }
+ }
+ }
+ else {
+ const milliseconds = parseInt(intensity)
+ if (!isNaN(milliseconds)) {
+ delayOnHover = milliseconds
+ }
+ }
+}
+
+if (isSupported) {
+ const eventListenersOptions = {
+ capture: true,
+ passive: true,
+ }
+
+ if (!useMousedownOnly) {
+ document.addEventListener('touchstart', touchstartListener, eventListenersOptions)
+ }
+
+ if (!useMousedown) {
+ document.addEventListener('mouseover', mouseoverListener, eventListenersOptions)
+ }
+ else if (!mousedownShortcut) {
+ document.addEventListener('mousedown', mousedownListener, eventListenersOptions)
+ }
+
+ if (mousedownShortcut) {
+ document.addEventListener('mousedown', mousedownShortcutListener, eventListenersOptions)
+ }
+
+ if (useViewport) {
+ let triggeringFunction
+ if (window.requestIdleCallback) {
+ triggeringFunction = (callback) => {
+ requestIdleCallback(callback, {
+ timeout: 1500,
+ })
+ }
+ }
+ else {
+ triggeringFunction = (callback) => {
+ callback()
+ }
+ }
+
+ triggeringFunction(() => {
+ const intersectionObserver = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ const linkElement = entry.target
+ intersectionObserver.unobserve(linkElement)
+ preload(linkElement.href)
+ }
+ })
+ })
+
+ document.querySelectorAll('a').forEach((linkElement) => {
+ if (isPreloadable(linkElement)) {
+ intersectionObserver.observe(linkElement)
+ }
+ })
+ })
+ }
+}
+
+function touchstartListener(event) {
+ /* Chrome on Android calls mouseover before touchcancel so `lastTouchTimestamp`
+ * must be assigned on touchstart to be measured on mouseover. */
+ lastTouchTimestamp = performance.now()
+
+ const linkElement = event.target.closest('a')
+
+ if (!isPreloadable(linkElement)) {
+ return
+ }
+
+ preload(linkElement.href)
+}
+
+function mouseoverListener(event) {
+ if (performance.now() - lastTouchTimestamp < DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION) {
+ return
+ }
+
+ const linkElement = event.target.closest('a')
+
+ if (!isPreloadable(linkElement)) {
+ return
+ }
+
+ linkElement.addEventListener('mouseout', mouseoutListener, {passive: true})
+
+ mouseoverTimer = setTimeout(() => {
+ preload(linkElement.href)
+ mouseoverTimer = undefined
+ }, delayOnHover)
+}
+
+function mousedownListener(event) {
+ const linkElement = event.target.closest('a')
+
+ if (!isPreloadable(linkElement)) {
+ return
+ }
+
+ preload(linkElement.href)
+}
+
+function mouseoutListener(event) {
+ if (event.relatedTarget && event.target.closest('a') == event.relatedTarget.closest('a')) {
+ return
+ }
+
+ if (mouseoverTimer) {
+ clearTimeout(mouseoverTimer)
+ mouseoverTimer = undefined
+ }
+}
+
+function mousedownShortcutListener(event) {
+ if (performance.now() - lastTouchTimestamp < DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION) {
+ return
+ }
+
+ const linkElement = event.target.closest('a')
+
+ if (event.which > 1 || event.metaKey || event.ctrlKey) {
+ return
+ }
+
+ if (!linkElement) {
+ return
+ }
+
+ linkElement.addEventListener('click', function (event) {
+ if (event.detail == 1337) {
+ return
+ }
+
+ event.preventDefault()
+ }, {capture: true, passive: false, once: true})
+
+ const customEvent = new MouseEvent('click', {view: window, bubbles: true, cancelable: false, detail: 1337})
+ linkElement.dispatchEvent(customEvent)
+}
+
+function isPreloadable(linkElement) {
+ if (!linkElement || !linkElement.href) {
+ return
+ }
+
+ if (useWhitelist && !('instant' in linkElement.dataset)) {
+ return
+ }
+
+ if (!allowExternalLinks && linkElement.origin != location.origin && !('instant' in linkElement.dataset)) {
+ return
+ }
+
+ if (!['http:', 'https:'].includes(linkElement.protocol)) {
+ return
+ }
+
+ if (linkElement.protocol == 'http:' && location.protocol == 'https:') {
+ return
+ }
+
+ if (!allowQueryString && linkElement.search && !('instant' in linkElement.dataset)) {
+ return
+ }
+
+ if (linkElement.hash && linkElement.pathname + linkElement.search == location.pathname + location.search) {
+ return
+ }
+
+ if ('noInstant' in linkElement.dataset) {
+ return
+ }
+
+ return true
+}
+
+function preload(url) {
+ if (prefetches.has(url)) {
+ return
+ }
+
+ const prefetcher = document.createElement('link')
+ prefetcher.rel = 'prefetch'
+ prefetcher.href = url
+ document.head.appendChild(prefetcher)
+
+ prefetches.add(url)
+}
A load.png => load.png +0 -0
A robots.txt => robots.txt +13 -0
@@ 0,0 1,13 @@
+User-agent: *
+Disallow: /cache/
+Disallow: /data/
+Disallow: /inc/
+
+User-agent: *
+Disallow: /*.jpg$
+
+User-agent: *
+Disallow: /*.png$
+
+User-agent: *
+Disallow: /*.html$
A => +14 -0
@@ 0,0 1,14 @@
</div>
<script src="inst.js" type="module" defer></script>
<script type="text/javascript" defer>
const images = document.querySelectorAll("img[data-src]");
images.forEach(function(image) {
image.addEventListener('click', e => {
e.target.src = e.target.dataset.src;
image.removeEventListener('click', e);
});
});
</script>
<span><a class="excl" href="https://0xff.nu/saisho">最小</a></span>
</body>
</html>
A template/head.php => template/head.php +32 -0
@@ 0,0 1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="generator" content="Saisho Mk.2">
+ <link href="data:image/x-icon;base64,AAA" rel="icon">
+ <link href="feed.xml" rel="alternate" type="application/rss+xml" title="<?php echo META_TITLE ?> RSS Feed">
+ <title><?php echo isset($content['title']) ? $content['title'] . ' — ' . META_TITLE : META_TITLE; ?></title>
+ <style>
+ body { font: 17px/1.6 sans-serif; text-rendering: optimizeLegibility; padding: 2rem; contain: layout }
+ pre { white-space: pre-wrap; word-break: break-all }
+ img:not(.excl) { image-rendering: pixelated; max-width: 100%; display: block }
+ a, a:visited { color: initial }
+ a:hover, a.heading-link { text-decoration: none }
+ a[href*="//"]:not([href*="<?= HOST ?>"]):not(.excl):after { content:"\219D" }
+ .pl { line-height: 2; list-style-type: none; padding-left: 0 }
+ .g { color: #777 }
+ .c { max-width: 70ch }
+ .m, hr { margin: 1.5rem 0 }
+ .fa-link:before { font-style: normal; content: "#"; opacity: 0 }
+ :is(h1,h2,h3,h4,h5):hover .fa-link:before { opacity:1 }
+ textarea, button { border: 0 }
+ textarea { width: 100%; resize: none }
+ button { background: #000; color: #fff; padding: .5rem }
+ @media screen and (max-width: 760px) {
+ .c { max-width: 100% }
+ }
+ </style>
+</head>
+<body>
+ <div class="c">
A template/list.php => template/list.php +12 -0
@@ 0,0 1,12 @@
+<?php
+
+include 'head.php';
+
+echo '<strong><span role="heading"><a href="' . HOST . '">' . META_TITLE . '</a></span></strong>';
+?>
+<div><p>Welcome to <a href="https://0xff.nu/saisho">Saisho Mk.2</a> version <?= SAISHO_VERSION ?>!</p>
+<p>You can change this text in <code>template/list.php</code>.</div>
+<?php
+echo '<div>' . $page->content . '</div>';
+
+include 'footer.php';
A template/notfound.php => template/notfound.php +11 -0
@@ 0,0 1,11 @@
+<?php
+
+include 'head.php';
+
+echo '<a href="' . HOST . '">↩</a> / oops';
+$html = <<<EOD
+<div class="content"><h1>(ノಠ益ಠ)ノ彡 Not found</h1></div>
+EOD;
+echo $html;
+
+include 'footer.php';<
\ No newline at end of file
A template/page.php => template/page.php +24 -0
@@ 0,0 1,24 @@
+<?php
+
+include 'head.php';
+
+echo '<a href="' . HOST . '">'.META_TITLE.'</a> / <strong><span role="heading" aria-level=1>'.$page->content->title.'</span></strong>';
+$html = <<<EOD
+ <div class="content">{$page->content->body}</div>
+ <div class="meta g">Last Modified: {$page->content->modified}</div>
+ EOD;
+echo $html;
+/* -- CUSTOM ------------ */
+echo <<<EOD
+ <hr>
+ <form id="contactform" action="https://formsubmit.io/send/b6458e9e-99b8-4e08-9cbc-d758377014e1" method="POST">
+ <textarea name="comment" id="comment" rows="4" aria-label="Your comment" placeholder="Let's discuss this.😃 This form sends me an anonymized email using Formsubmit. Don't forget to mention your name and e-mail address if you'd like me to respond."></textarea>
+ <input name="entry" type="hidden" value="{$page->content->title}">
+ <input name="_formsubmit_id" type="text" style="display:none">
+ <button>Submit</button>
+ </form>
+ EOD;
+// echo $form;
+/* -- END --------------- */
+
+include 'footer.php';