~hxii/saisho

92320f63a35dedcfbc2f89f1853d5337fe4b39b3 — Paul (hxii) Glushak 4 years ago 0afe9ea
Update to 20200830
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> &mdash; {$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 . ' &mdash; ' . 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 template/footer.php => template/footer.php +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'] . ' &mdash; ' . 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.&#x1F603; 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';